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 WebSocketIoServer
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 status = NFVBench.STATUS_OK
78 # take a snapshot of the current time for this new run
79 # so that all subsequent logs can relate to this run
80 fluent_logger.start_new_run()
83 # recalc the running config based on the base config and options for this run
84 self._update_config(opts)
85 self.specs.set_run_spec(self.config_plugin.get_run_spec(self.config,
86 self.specs.openstack))
87 self.chain_runner = ChainRunner(self.config,
93 # make sure that the min frame size is 64
95 for frame_size in self.config.frame_sizes:
97 if int(frame_size) < min_packet_size:
98 frame_size = str(min_packet_size)
99 LOG.info("Adjusting frame size %s bytes to minimum size %s bytes",
100 frame_size, min_packet_size)
101 if frame_size not in new_frame_sizes:
102 new_frame_sizes.append(frame_size)
104 new_frame_sizes.append(frame_size.upper())
105 self.config.frame_sizes = new_frame_sizes
107 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
108 "nfvbench_version": __version__,
109 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
112 "service_chain": self.chain_runner.run(),
113 "versions": self.chain_runner.get_version(),
117 if self.specs.openstack:
118 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
119 "encaps": self.specs.openstack.encaps}
120 result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
122 status = NFVBench.STATUS_ERROR
123 message = traceback.format_exc()
124 except KeyboardInterrupt:
125 status = NFVBench.STATUS_ERROR
126 message = traceback.format_exc()
128 if self.chain_runner:
129 self.chain_runner.close()
131 if status == NFVBench.STATUS_OK:
132 # result2 = utils.dict_to_json_dict(result)
139 'error_message': message
142 def prepare_summary(self, result):
143 """Prepare summary of the result to print and send it to logger (eg: fluentd)."""
145 summary = NFVBenchSummarizer(result, fluent_logger)
146 LOG.info(str(summary))
148 def save(self, result):
149 """Save results in json format file."""
150 utils.save_json_result(result,
151 self.config.json_file,
152 self.config.std_json_path,
153 self.config.service_chain,
154 self.config.service_chain_count,
155 self.config.flow_count,
156 self.config.frame_sizes)
158 def _update_config(self, opts):
159 """Recalculate the running config based on the base config and opts.
161 Sanity check on the config is done here as well.
163 self.config = AttrDict(dict(self.base_config))
164 self.config.update(opts)
167 config.service_chain = config.service_chain.upper()
168 config.service_chain_count = int(config.service_chain_count)
169 if config.l2_loopback:
170 # force the number of chains to be 1 in case of l2 loopback
171 config.service_chain_count = 1
172 config.service_chain = ChainType.EXT
174 LOG.info('Running L2 loopback: using EXT chain/no ARP')
175 config.flow_count = utils.parse_flow_count(config.flow_count)
176 required_flow_count = config.service_chain_count * 2
177 if config.flow_count < required_flow_count:
178 LOG.info("Flow count %d has been set to minimum value of '%d' "
179 "for current configuration", config.flow_count,
181 config.flow_count = required_flow_count
183 if config.flow_count % 2:
184 config.flow_count += 1
186 config.duration_sec = float(config.duration_sec)
187 config.interval_sec = float(config.interval_sec)
188 config.pause_sec = float(config.pause_sec)
190 if config.traffic is None or not config.traffic:
191 raise Exception("Missing traffic property in configuration")
193 if config.openrc_file:
194 config.openrc_file = os.path.expanduser(config.openrc_file)
196 config.ndr_run = (not config.no_traffic and
197 'ndr' in config.rate.strip().lower().split('_'))
198 config.pdr_run = (not config.no_traffic and
199 'pdr' in config.rate.strip().lower().split('_'))
200 config.single_run = (not config.no_traffic and
201 not (config.ndr_run or config.pdr_run))
203 config.json_file = config.json if config.json else None
205 (path, _filename) = os.path.split(config.json)
206 if not os.path.exists(path):
207 raise Exception('Please provide existing path for storing results in JSON file. '
208 'Path used: {path}'.format(path=path))
210 config.std_json_path = config.std_json if config.std_json else None
211 if config.std_json_path:
212 if not os.path.exists(config.std_json):
213 raise Exception('Please provide existing path for storing results in JSON file. '
214 'Path used: {path}'.format(path=config.std_json_path))
216 # VxLAN sanity checks
218 if config.vlan_tagging:
219 config.vlan_tagging = False
220 LOG.info('VxLAN: vlan_tagging forced to False '
221 '(inner VLAN tagging must be disabled)')
223 self.config_plugin.validate_config(config, self.specs.openstack)
226 def _parse_opts_from_cli():
227 parser = argparse.ArgumentParser()
229 parser.add_argument('--status', dest='status',
232 help='Provide NFVbench status')
234 parser.add_argument('-c', '--config', dest='config',
236 help='Override default values with a config file or '
237 'a yaml/json config string',
238 metavar='<file_name_or_yaml>')
240 parser.add_argument('--server', dest='server',
243 metavar='<http_root_pathname>',
244 help='Run nfvbench in server mode and pass'
245 ' the HTTP root folder full pathname')
247 parser.add_argument('--host', dest='host',
250 help='Host IP address on which server will be listening (default 0.0.0.0)')
252 parser.add_argument('-p', '--port', dest='port',
255 help='Port on which server will be listening (default 7555)')
257 parser.add_argument('-sc', '--service-chain', dest='service_chain',
258 choices=ChainType.names,
260 help='Service chain to run')
262 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
264 help='Set number of service chains to run',
265 metavar='<service_chain_count>')
267 parser.add_argument('-fc', '--flow-count', dest='flow_count',
269 help='Set number of total flows for all chains and all directions',
270 metavar='<flow_count>')
272 parser.add_argument('--rate', dest='rate',
274 help='Specify rate in pps, bps or %% as total for all directions',
277 parser.add_argument('--duration', dest='duration_sec',
279 help='Set duration to run traffic generator (in seconds)',
280 metavar='<duration_sec>')
282 parser.add_argument('--interval', dest='interval_sec',
284 help='Set interval to record traffic generator stats (in seconds)',
285 metavar='<interval_sec>')
287 parser.add_argument('--inter-node', dest='inter_node',
292 parser.add_argument('--sriov', dest='sriov',
295 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
297 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
300 help='Use SRIOV to handle the middle network traffic '
301 '(PVVP with SRIOV only)')
303 parser.add_argument('-d', '--debug', dest='debug',
306 help='print debug messages (verbose)')
308 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
310 help='Traffic generator profile to use')
312 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
315 help='Check config and connectivity only - do not generate traffic')
317 parser.add_argument('--no-arp', dest='no_arp',
320 help='Do not use ARP to find MAC addresses, '
321 'instead use values in config file')
323 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
326 help='Skip vswitch configuration and retrieving of stats')
328 parser.add_argument('--vxlan', dest='vxlan',
331 help='Enable VxLan encapsulation')
333 parser.add_argument('--no-cleanup', dest='no_cleanup',
336 help='no cleanup after run')
338 parser.add_argument('--cleanup', dest='cleanup',
341 help='Cleanup NFVbench resources (prompt to confirm)')
343 parser.add_argument('--force-cleanup', dest='force_cleanup',
346 help='Cleanup NFVbench resources (do not prompt)')
348 parser.add_argument('--json', dest='json',
350 help='store results in json format file',
351 metavar='<path>/<filename>')
353 parser.add_argument('--std-json', dest='std_json',
355 help='store results in json format file with nfvbench standard filename: '
356 '<service-chain-type>-<service-chain-count>-<flow-count>'
357 '-<packet-sizes>.json',
360 parser.add_argument('--show-default-config', dest='show_default_config',
363 help='print the default config in yaml format (unedited)')
365 parser.add_argument('--show-config', dest='show_config',
368 help='print the running config in json format')
370 parser.add_argument('-ss', '--show-summary', dest='summary',
372 help='Show summary from nfvbench json file',
375 parser.add_argument('-v', '--version', dest='version',
380 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
382 help='Override traffic profile frame sizes',
383 metavar='<frame_size_bytes or IMIX>')
385 parser.add_argument('--unidir', dest='unidir',
388 help='Override traffic profile direction (requires -fs)')
390 parser.add_argument('--log-file', '--logfile', dest='log_file',
392 help='Filename for saving logs',
393 metavar='<log_file>')
395 parser.add_argument('--user-label', '--userlabel', dest='user_label',
397 help='Custom label for performance records')
399 parser.add_argument('--hypervisor', dest='hypervisor',
401 metavar='<hypervisor name>',
402 help='Where chains must run ("compute", "az:", "az:compute")')
404 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
407 help='Port to port or port to switch to port L2 loopback with VLAN id')
409 opts, unknown_opts = parser.parse_known_args()
410 return opts, unknown_opts
413 def load_default_config():
414 default_cfg = resource_string(__name__, "cfg.default.yaml")
415 config = config_loads(default_cfg)
416 config.name = '(built-in default config)'
417 return config, default_cfg
420 def override_custom_traffic(config, frame_sizes, unidir):
421 """Override the traffic profiles with a custom one."""
422 if frame_sizes is not None:
423 traffic_profile_name = "custom_traffic_profile"
424 config.traffic_profile = [
426 "l2frame_size": frame_sizes,
427 "name": traffic_profile_name
431 traffic_profile_name = config.traffic["profile"]
433 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
435 "bidirectional": bidirectional,
436 "profile": traffic_profile_name
440 def check_physnet(name, netattrs):
441 if not netattrs.physical_network:
442 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
444 if not netattrs.segmentation_id:
445 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
448 def status_cleanup(config, cleanup, force_cleanup):
449 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
450 # check if another run is pending
453 with utils.RunLock():
454 LOG.info('Status: idle')
456 LOG.info('Status: busy (run pending)')
458 # check nfvbench resources
459 if config.openrc_file and config.service_chain != ChainType.EXT:
460 cleaner = Cleaner(config)
461 count = cleaner.show_resources()
462 if count and (cleanup or force_cleanup):
463 cleaner.clean(not force_cleanup)
468 run_summary_required = False
471 # load default config file
472 config, default_cfg = load_default_config()
473 # create factory for platform specific classes
475 factory_module = importlib.import_module(config['factory_module'])
476 factory = getattr(factory_module, config['factory_class'])()
477 except AttributeError:
478 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
479 .format(m=config['factory_module'], c=config['factory_class']))
480 # create config plugin for this platform
481 config_plugin = factory.get_config_plugin_class()(config)
482 config = config_plugin.get_config()
484 opts, unknown_opts = _parse_opts_from_cli()
485 log.set_level(debug=opts.debug)
488 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
492 with open(opts.summary) as json_data:
493 result = json.load(json_data)
495 result['config']['user_label'] = opts.user_label
496 print NFVBenchSummarizer(result, fluent_logger)
499 # show default config in text/yaml format
500 if opts.show_default_config:
506 # do not check extra_specs in flavor as it can contain any key/value pairs
507 whitelist_keys = ['extra_specs']
508 # override default config options with start config at path parsed from CLI
509 # check if it is an inline yaml/json config or a file name
510 if os.path.isfile(opts.config):
511 LOG.info('Loading configuration file: %s', opts.config)
512 config = config_load(opts.config, config, whitelist_keys)
513 config.name = os.path.basename(opts.config)
515 LOG.info('Loading configuration string: %s', opts.config)
516 config = config_loads(opts.config, config, whitelist_keys)
518 # setup the fluent logger as soon as possible right after the config plugin is called,
519 # if there is any logging or result tag is set then initialize the fluent logger
520 for fluentd in config.fluentd:
521 if fluentd.logging_tag or fluentd.result_tag:
522 fluent_logger = FluentLogHandler(config.fluentd)
523 LOG.addHandler(fluent_logger)
526 # traffic profile override options
527 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
529 # copy over cli options that are used in config
530 config.generator_profile = opts.generator_profile
534 config.log_file = opts.log_file
535 if opts.service_chain:
536 config.service_chain = opts.service_chain
537 if opts.service_chain_count:
538 config.service_chain_count = opts.service_chain_count
539 if opts.no_vswitch_access:
540 config.no_vswitch_access = opts.no_vswitch_access
542 # can be any of 'comp1', 'nova:', 'nova:comp1'
543 config.compute_nodes = opts.hypervisor
547 # port to port loopback (direct or through switch)
549 config.l2_loopback = True
550 if config.service_chain != ChainType.EXT:
551 LOG.info('Changing service chain type to EXT')
552 config.service_chain = ChainType.EXT
553 if not config.no_arp:
554 LOG.info('Disabling ARP')
556 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
557 LOG.info('Running L2 loopback: using EXT chain/no ARP')
559 if opts.use_sriov_middle_net:
560 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
561 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
562 config.use_sriov_middle_net = True
564 if config.sriov and config.service_chain != ChainType.EXT:
565 # if sriov is requested (does not apply to ext chains)
566 # make sure the physnet names are specified
567 check_physnet("left", config.internal_networks.left)
568 check_physnet("right", config.internal_networks.right)
569 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
570 check_physnet("middle", config.internal_networks.middle)
572 # show running config in json format
574 print json.dumps(config, sort_keys=True, indent=4)
577 # check that an empty openrc file (no OpenStack) is only allowed
579 if not config.openrc_file:
580 if config.service_chain == ChainType.EXT:
581 LOG.info('EXT chain with OpenStack mode disabled')
583 raise Exception("openrc_file is empty in the configuration and is required")
585 # update the config in the config plugin as it might have changed
586 # in a copy of the dict (config plugin still holds the original dict)
587 config_plugin.set_config(config)
589 if opts.status or opts.cleanup or opts.force_cleanup:
590 status_cleanup(config, opts.cleanup, opts.force_cleanup)
592 # add file log if requested
594 log.add_file_logger(config.log_file)
596 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
599 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
602 if os.path.isdir(opts.server):
603 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
604 nfvbench_instance.set_notifier(server)
606 port = int(opts.port)
608 server.run(host=opts.host)
610 server.run(host=opts.host, port=port)
612 print 'Invalid HTTP root directory: ' + opts.server
615 with utils.RunLock():
616 run_summary_required = True
618 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
620 raise Exception(err_msg)
622 # remove unfilled values
623 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
625 params = ' '.join(str(e) for e in sys.argv[1:])
626 result = nfvbench_instance.run(opts, params)
627 if 'error_message' in result:
628 raise Exception(result['error_message'])
630 if 'result' in result and result['status']:
631 nfvbench_instance.save(result['result'])
632 nfvbench_instance.prepare_summary(result['result'])
633 except Exception as exc:
634 run_summary_required = True
636 'status': NFVBench.STATUS_ERROR,
637 'error_message': traceback.format_exc()
642 # only send a summary record if there was an actual nfvbench run or
643 # if an error/exception was logged.
644 fluent_logger.send_run_summary(run_summary_required)
647 if __name__ == '__main__':