2 # Copyright 2016 Cisco Systems, Inc. All rights reserved.
4 # Licensed under the Apache License, Version 2.0 (the "License"); you may
5 # not use this file except in compliance with the License. You may obtain
6 # a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # License for the specific language governing permissions and limitations
26 from attrdict import AttrDict
28 from pkg_resources import resource_string
30 from __init__ import __version__
31 from chain_runner import ChainRunner
32 from cleanup import Cleaner
33 from config import config_load
34 from config import config_loads
35 import credentials as credentials
36 from fluentd import FluentLogHandler
39 from nfvbenchd import WebServer
40 from specs import ChainType
41 from specs import Specs
42 from summarizer import NFVBenchSummarizer
48 class NFVBench(object):
49 """Main class of NFV benchmarking tool."""
52 STATUS_ERROR = 'ERROR'
54 def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
55 # the base config never changes for a given NFVbench instance
56 self.base_config = config
57 # this is the running config, updated at every run()
59 self.config_plugin = config_plugin
60 self.factory = factory
61 self.notifier = notifier
62 self.cred = credentials.Credentials(config.openrc_file, None, False) \
63 if config.openrc_file else None
64 self.chain_runner = None
66 self.specs.set_openstack_spec(openstack_spec)
70 def set_notifier(self, notifier):
71 self.notifier = notifier
73 def run(self, opts, args):
74 """This run() method is called for every NFVbench benchmark request.
76 In CLI mode, this method is called only once per invocation.
77 In REST server mode, this is called once per REST POST request
79 status = NFVBench.STATUS_OK
83 # take a snapshot of the current time for this new run
84 # so that all subsequent logs can relate to this run
85 fluent_logger.start_new_run()
88 # recalc the running config based on the base config and options for this run
89 self._update_config(opts)
91 # check that an empty openrc file (no OpenStack) is only allowed
93 if not self.config.openrc_file and self.config.service_chain != ChainType.EXT:
94 raise Exception("openrc_file in the configuration is required for PVP/PVVP chains")
96 self.specs.set_run_spec(self.config_plugin.get_run_spec(self.config,
97 self.specs.openstack))
98 self.chain_runner = ChainRunner(self.config,
104 # make sure that the min frame size is 64
106 for frame_size in self.config.frame_sizes:
108 if int(frame_size) < min_packet_size:
109 frame_size = str(min_packet_size)
110 LOG.info("Adjusting frame size %s bytes to minimum size %s bytes",
111 frame_size, min_packet_size)
112 if frame_size not in new_frame_sizes:
113 new_frame_sizes.append(frame_size)
115 new_frame_sizes.append(frame_size.upper())
116 self.config.frame_sizes = new_frame_sizes
118 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
119 "nfvbench_version": __version__,
120 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
123 "service_chain": self.chain_runner.run(),
124 "versions": self.chain_runner.get_version(),
128 if self.specs.openstack:
129 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
130 "encaps": self.specs.openstack.encaps}
131 result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
133 status = NFVBench.STATUS_ERROR
134 message = traceback.format_exc()
135 except KeyboardInterrupt:
136 status = NFVBench.STATUS_ERROR
137 message = traceback.format_exc()
139 if self.chain_runner:
140 self.chain_runner.close()
142 if status == NFVBench.STATUS_OK:
143 # result2 = utils.dict_to_json_dict(result)
150 'error_message': message
153 def prepare_summary(self, result):
154 """Prepare summary of the result to print and send it to logger (eg: fluentd)."""
156 summary = NFVBenchSummarizer(result, fluent_logger)
157 LOG.info(str(summary))
159 def save(self, result):
160 """Save results in json format file."""
161 utils.save_json_result(result,
162 self.config.json_file,
163 self.config.std_json_path,
164 self.config.service_chain,
165 self.config.service_chain_count,
166 self.config.flow_count,
167 self.config.frame_sizes)
169 def _update_config(self, opts):
170 """Recalculate the running config based on the base config and opts.
172 Sanity check on the config is done here as well.
174 self.config = AttrDict(dict(self.base_config))
175 self.config.update(opts)
178 config.service_chain = config.service_chain.upper()
179 config.service_chain_count = int(config.service_chain_count)
180 if config.l2_loopback:
181 # force the number of chains to be 1 in case of l2 loopback
182 config.service_chain_count = 1
183 config.service_chain = ChainType.EXT
185 LOG.info('Running L2 loopback: using EXT chain/no ARP')
186 config.flow_count = utils.parse_flow_count(config.flow_count)
187 required_flow_count = config.service_chain_count * 2
188 if config.flow_count < required_flow_count:
189 LOG.info("Flow count %d has been set to minimum value of '%d' "
190 "for current configuration", config.flow_count,
192 config.flow_count = required_flow_count
194 if config.flow_count % 2:
195 config.flow_count += 1
197 config.duration_sec = float(config.duration_sec)
198 config.interval_sec = float(config.interval_sec)
199 config.pause_sec = float(config.pause_sec)
201 if config.traffic is None or not config.traffic:
202 raise Exception("Missing traffic property in configuration")
204 if config.openrc_file:
205 config.openrc_file = os.path.expanduser(config.openrc_file)
207 config.ndr_run = (not config.no_traffic and
208 'ndr' in config.rate.strip().lower().split('_'))
209 config.pdr_run = (not config.no_traffic and
210 'pdr' in config.rate.strip().lower().split('_'))
211 config.single_run = (not config.no_traffic and
212 not (config.ndr_run or config.pdr_run))
214 config.json_file = config.json if config.json else None
216 (path, _filename) = os.path.split(config.json)
217 if not os.path.exists(path):
218 raise Exception('Please provide existing path for storing results in JSON file. '
219 'Path used: {path}'.format(path=path))
221 config.std_json_path = config.std_json if config.std_json else None
222 if config.std_json_path:
223 if not os.path.exists(config.std_json):
224 raise Exception('Please provide existing path for storing results in JSON file. '
225 'Path used: {path}'.format(path=config.std_json_path))
227 # VxLAN sanity checks
229 if config.vlan_tagging:
230 config.vlan_tagging = False
231 LOG.info('VxLAN: vlan_tagging forced to False '
232 '(inner VLAN tagging must be disabled)')
234 self.config_plugin.validate_config(config, self.specs.openstack)
237 def _parse_opts_from_cli():
238 parser = argparse.ArgumentParser()
240 parser.add_argument('--status', dest='status',
243 help='Provide NFVbench status')
245 parser.add_argument('-c', '--config', dest='config',
247 help='Override default values with a config file or '
248 'a yaml/json config string',
249 metavar='<file_name_or_yaml>')
251 parser.add_argument('--server', dest='server',
254 help='Run nfvbench in server mode')
256 parser.add_argument('--host', dest='host',
259 help='Host IP address on which server will be listening (default 0.0.0.0)')
261 parser.add_argument('-p', '--port', dest='port',
264 help='Port on which server will be listening (default 7555)')
266 parser.add_argument('-sc', '--service-chain', dest='service_chain',
267 choices=ChainType.names,
269 help='Service chain to run')
271 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
273 help='Set number of service chains to run',
274 metavar='<service_chain_count>')
276 parser.add_argument('-fc', '--flow-count', dest='flow_count',
278 help='Set number of total flows for all chains and all directions',
279 metavar='<flow_count>')
281 parser.add_argument('--rate', dest='rate',
283 help='Specify rate in pps, bps or %% as total for all directions',
286 parser.add_argument('--duration', dest='duration_sec',
288 help='Set duration to run traffic generator (in seconds)',
289 metavar='<duration_sec>')
291 parser.add_argument('--interval', dest='interval_sec',
293 help='Set interval to record traffic generator stats (in seconds)',
294 metavar='<interval_sec>')
296 parser.add_argument('--inter-node', dest='inter_node',
301 parser.add_argument('--sriov', dest='sriov',
304 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
306 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
309 help='Use SRIOV to handle the middle network traffic '
310 '(PVVP with SRIOV only)')
312 parser.add_argument('-d', '--debug', dest='debug',
315 help='print debug messages (verbose)')
317 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
319 help='Traffic generator profile to use')
321 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
324 help='Check config and connectivity only - do not generate traffic')
326 parser.add_argument('--no-arp', dest='no_arp',
329 help='Do not use ARP to find MAC addresses, '
330 'instead use values in config file')
332 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
335 help='Skip vswitch configuration and retrieving of stats')
337 parser.add_argument('--vxlan', dest='vxlan',
340 help='Enable VxLan encapsulation')
342 parser.add_argument('--no-cleanup', dest='no_cleanup',
345 help='no cleanup after run')
347 parser.add_argument('--cleanup', dest='cleanup',
350 help='Cleanup NFVbench resources (prompt to confirm)')
352 parser.add_argument('--force-cleanup', dest='force_cleanup',
355 help='Cleanup NFVbench resources (do not prompt)')
357 parser.add_argument('--restart', dest='restart',
360 help='Restart TRex server')
362 parser.add_argument('--json', dest='json',
364 help='store results in json format file',
365 metavar='<path>/<filename>')
367 parser.add_argument('--std-json', dest='std_json',
369 help='store results in json format file with nfvbench standard filename: '
370 '<service-chain-type>-<service-chain-count>-<flow-count>'
371 '-<packet-sizes>.json',
374 parser.add_argument('--show-default-config', dest='show_default_config',
377 help='print the default config in yaml format (unedited)')
379 parser.add_argument('--show-config', dest='show_config',
382 help='print the running config in json format')
384 parser.add_argument('-ss', '--show-summary', dest='summary',
386 help='Show summary from nfvbench json file',
389 parser.add_argument('-v', '--version', dest='version',
394 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
396 help='Override traffic profile frame sizes',
397 metavar='<frame_size_bytes or IMIX>')
399 parser.add_argument('--unidir', dest='unidir',
402 help='Override traffic profile direction (requires -fs)')
404 parser.add_argument('--log-file', '--logfile', dest='log_file',
406 help='Filename for saving logs',
407 metavar='<log_file>')
409 parser.add_argument('--user-label', '--userlabel', dest='user_label',
411 help='Custom label for performance records')
413 parser.add_argument('--hypervisor', dest='hypervisor',
415 metavar='<hypervisor name>',
416 help='Where chains must run ("compute", "az:", "az:compute")')
418 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
421 help='Port to port or port to switch to port L2 loopback with VLAN id')
423 opts, unknown_opts = parser.parse_known_args()
424 return opts, unknown_opts
427 def load_default_config():
428 default_cfg = resource_string(__name__, "cfg.default.yaml")
429 config = config_loads(default_cfg)
430 config.name = '(built-in default config)'
431 return config, default_cfg
434 def override_custom_traffic(config, frame_sizes, unidir):
435 """Override the traffic profiles with a custom one."""
436 if frame_sizes is not None:
437 traffic_profile_name = "custom_traffic_profile"
438 config.traffic_profile = [
440 "l2frame_size": frame_sizes,
441 "name": traffic_profile_name
445 traffic_profile_name = config.traffic["profile"]
447 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
449 "bidirectional": bidirectional,
450 "profile": traffic_profile_name
454 def check_physnet(name, netattrs):
455 if not netattrs.physical_network:
456 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
458 if not netattrs.segmentation_id:
459 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
462 def status_cleanup(config, cleanup, force_cleanup):
463 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
464 # check if another run is pending
467 with utils.RunLock():
468 LOG.info('Status: idle')
470 LOG.info('Status: busy (run pending)')
472 # check nfvbench resources
473 if config.openrc_file and config.service_chain != ChainType.EXT:
474 cleaner = Cleaner(config)
475 count = cleaner.show_resources()
476 if count and (cleanup or force_cleanup):
477 cleaner.clean(not force_cleanup)
482 run_summary_required = False
485 # load default config file
486 config, default_cfg = load_default_config()
487 # create factory for platform specific classes
489 factory_module = importlib.import_module(config['factory_module'])
490 factory = getattr(factory_module, config['factory_class'])()
491 except AttributeError:
492 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
493 .format(m=config['factory_module'], c=config['factory_class']))
494 # create config plugin for this platform
495 config_plugin = factory.get_config_plugin_class()(config)
496 config = config_plugin.get_config()
498 opts, unknown_opts = _parse_opts_from_cli()
499 log.set_level(debug=opts.debug)
502 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
506 with open(opts.summary) as json_data:
507 result = json.load(json_data)
509 result['config']['user_label'] = opts.user_label
510 print NFVBenchSummarizer(result, fluent_logger)
513 # show default config in text/yaml format
514 if opts.show_default_config:
520 # do not check extra_specs in flavor as it can contain any key/value pairs
521 whitelist_keys = ['extra_specs']
522 # override default config options with start config at path parsed from CLI
523 # check if it is an inline yaml/json config or a file name
524 if os.path.isfile(opts.config):
525 LOG.info('Loading configuration file: %s', opts.config)
526 config = config_load(opts.config, config, whitelist_keys)
527 config.name = os.path.basename(opts.config)
529 LOG.info('Loading configuration string: %s', opts.config)
530 config = config_loads(opts.config, config, whitelist_keys)
532 # setup the fluent logger as soon as possible right after the config plugin is called,
533 # if there is any logging or result tag is set then initialize the fluent logger
534 for fluentd in config.fluentd:
535 if fluentd.logging_tag or fluentd.result_tag:
536 fluent_logger = FluentLogHandler(config.fluentd)
537 LOG.addHandler(fluent_logger)
540 # traffic profile override options
541 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
543 # copy over cli options that are used in config
544 config.generator_profile = opts.generator_profile
548 config.log_file = opts.log_file
549 if opts.service_chain:
550 config.service_chain = opts.service_chain
551 if opts.service_chain_count:
552 config.service_chain_count = opts.service_chain_count
553 if opts.no_vswitch_access:
554 config.no_vswitch_access = opts.no_vswitch_access
556 # can be any of 'comp1', 'nova:', 'nova:comp1'
557 config.compute_nodes = opts.hypervisor
561 config.restart = True
562 # port to port loopback (direct or through switch)
564 config.l2_loopback = True
565 if config.service_chain != ChainType.EXT:
566 LOG.info('Changing service chain type to EXT')
567 config.service_chain = ChainType.EXT
568 if not config.no_arp:
569 LOG.info('Disabling ARP')
571 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
572 LOG.info('Running L2 loopback: using EXT chain/no ARP')
574 if opts.use_sriov_middle_net:
575 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
576 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
577 config.use_sriov_middle_net = True
579 if config.sriov and config.service_chain != ChainType.EXT:
580 # if sriov is requested (does not apply to ext chains)
581 # make sure the physnet names are specified
582 check_physnet("left", config.internal_networks.left)
583 check_physnet("right", config.internal_networks.right)
584 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
585 check_physnet("middle", config.internal_networks.middle)
587 # show running config in json format
589 print json.dumps(config, sort_keys=True, indent=4)
592 # update the config in the config plugin as it might have changed
593 # in a copy of the dict (config plugin still holds the original dict)
594 config_plugin.set_config(config)
596 if opts.status or opts.cleanup or opts.force_cleanup:
597 status_cleanup(config, opts.cleanup, opts.force_cleanup)
599 # add file log if requested
601 log.add_file_logger(config.log_file)
603 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
606 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
609 server = WebServer(nfvbench_instance, fluent_logger)
611 port = int(opts.port)
613 server.run(host=opts.host)
615 server.run(host=opts.host, port=port)
616 # server.run() should never return
618 with utils.RunLock():
619 run_summary_required = True
621 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
623 raise Exception(err_msg)
625 # remove unfilled values
626 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
628 params = ' '.join(str(e) for e in sys.argv[1:])
629 result = nfvbench_instance.run(opts, params)
630 if 'error_message' in result:
631 raise Exception(result['error_message'])
633 if 'result' in result and result['status']:
634 nfvbench_instance.save(result['result'])
635 nfvbench_instance.prepare_summary(result['result'])
636 except Exception as exc:
637 run_summary_required = True
639 'status': NFVBench.STATUS_ERROR,
640 'error_message': traceback.format_exc()
645 # only send a summary record if there was an actual nfvbench run or
646 # if an error/exception was logged.
647 fluent_logger.send_run_summary(run_summary_required)
650 if __name__ == '__main__':