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 min_packet_size = "68" if self.config.vlan_tagging else "64"
94 for frame_size in self.config.frame_sizes:
96 if int(frame_size) < int(min_packet_size):
97 new_frame_sizes.append(min_packet_size)
98 LOG.info("Adjusting frame size %s Bytes to minimum size %s Bytes due to " +
99 "traffic generator restriction", frame_size, min_packet_size)
101 new_frame_sizes.append(frame_size)
103 new_frame_sizes.append(frame_size)
104 self.config.actual_frame_sizes = tuple(new_frame_sizes)
106 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
107 "nfvbench_version": __version__,
108 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
111 "service_chain": self.chain_runner.run(),
112 "versions": self.chain_runner.get_version(),
116 if self.specs.openstack:
117 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
118 "encaps": self.specs.openstack.encaps}
119 result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
121 status = NFVBench.STATUS_ERROR
122 message = traceback.format_exc()
123 except KeyboardInterrupt:
124 status = NFVBench.STATUS_ERROR
125 message = traceback.format_exc()
127 if self.chain_runner:
128 self.chain_runner.close()
130 if status == NFVBench.STATUS_OK:
131 # result2 = utils.dict_to_json_dict(result)
138 'error_message': message
141 def prepare_summary(self, result):
142 """Prepare summary of the result to print and send it to logger (eg: fluentd)."""
144 summary = NFVBenchSummarizer(result, fluent_logger)
145 LOG.info(str(summary))
147 def save(self, result):
148 """Save results in json format file."""
149 utils.save_json_result(result,
150 self.config.json_file,
151 self.config.std_json_path,
152 self.config.service_chain,
153 self.config.service_chain_count,
154 self.config.flow_count,
155 self.config.frame_sizes)
157 def _update_config(self, opts):
158 """Recalculate the running config based on the base config and opts.
160 Sanity check on the config is done here as well.
162 self.config = AttrDict(dict(self.base_config))
163 self.config.update(opts)
166 config.service_chain = config.service_chain.upper()
167 config.service_chain_count = int(config.service_chain_count)
168 if config.l2_loopback:
169 # force the number of chains to be 1 in case of l2 loopback
170 config.service_chain_count = 1
171 config.service_chain = ChainType.EXT
173 LOG.info('Running L2 loopback: using EXT chain/no ARP')
174 config.flow_count = utils.parse_flow_count(config.flow_count)
175 required_flow_count = config.service_chain_count * 2
176 if config.flow_count < required_flow_count:
177 LOG.info("Flow count %d has been set to minimum value of '%d' "
178 "for current configuration", config.flow_count,
180 config.flow_count = required_flow_count
182 if config.flow_count % 2:
183 config.flow_count += 1
185 config.duration_sec = float(config.duration_sec)
186 config.interval_sec = float(config.interval_sec)
187 config.pause_sec = float(config.pause_sec)
189 if config.traffic is None or not config.traffic:
190 raise Exception("Missing traffic property in configuration")
192 if config.openrc_file:
193 config.openrc_file = os.path.expanduser(config.openrc_file)
195 config.ndr_run = (not config.no_traffic and
196 'ndr' in config.rate.strip().lower().split('_'))
197 config.pdr_run = (not config.no_traffic and
198 'pdr' in config.rate.strip().lower().split('_'))
199 config.single_run = (not config.no_traffic and
200 not (config.ndr_run or config.pdr_run))
202 config.json_file = config.json if config.json else None
204 (path, _filename) = os.path.split(config.json)
205 if not os.path.exists(path):
206 raise Exception('Please provide existing path for storing results in JSON file. '
207 'Path used: {path}'.format(path=path))
209 config.std_json_path = config.std_json if config.std_json else None
210 if config.std_json_path:
211 if not os.path.exists(config.std_json):
212 raise Exception('Please provide existing path for storing results in JSON file. '
213 'Path used: {path}'.format(path=config.std_json_path))
215 self.config_plugin.validate_config(config, self.specs.openstack)
218 def parse_opts_from_cli():
219 parser = argparse.ArgumentParser()
221 parser.add_argument('--status', dest='status',
224 help='Provide NFVbench status')
226 parser.add_argument('-c', '--config', dest='config',
228 help='Override default values with a config file or '
229 'a yaml/json config string',
230 metavar='<file_name_or_yaml>')
232 parser.add_argument('--server', dest='server',
235 metavar='<http_root_pathname>',
236 help='Run nfvbench in server mode and pass'
237 ' the HTTP root folder full pathname')
239 parser.add_argument('--host', dest='host',
242 help='Host IP address on which server will be listening (default 0.0.0.0)')
244 parser.add_argument('-p', '--port', dest='port',
247 help='Port on which server will be listening (default 7555)')
249 parser.add_argument('-sc', '--service-chain', dest='service_chain',
250 choices=ChainType.names,
252 help='Service chain to run')
254 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
256 help='Set number of service chains to run',
257 metavar='<service_chain_count>')
259 parser.add_argument('-fc', '--flow-count', dest='flow_count',
261 help='Set number of total flows for all chains and all directions',
262 metavar='<flow_count>')
264 parser.add_argument('--rate', dest='rate',
266 help='Specify rate in pps, bps or %% as total for all directions',
269 parser.add_argument('--duration', dest='duration_sec',
271 help='Set duration to run traffic generator (in seconds)',
272 metavar='<duration_sec>')
274 parser.add_argument('--interval', dest='interval_sec',
276 help='Set interval to record traffic generator stats (in seconds)',
277 metavar='<interval_sec>')
279 parser.add_argument('--inter-node', dest='inter_node',
282 help='run VMs in different compute nodes (PVVP only)')
284 parser.add_argument('--sriov', dest='sriov',
287 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
289 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
292 help='Use SRIOV to handle the middle network traffic '
293 '(PVVP with SRIOV only)')
295 parser.add_argument('-d', '--debug', dest='debug',
298 help='print debug messages (verbose)')
300 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
302 help='Traffic generator profile to use')
304 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
307 help='Check config and connectivity only - do not generate traffic')
309 parser.add_argument('--no-arp', dest='no_arp',
312 help='Do not use ARP to find MAC addresses, '
313 'instead use values in config file')
315 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
318 help='Skip vswitch configuration and retrieving of stats')
320 parser.add_argument('--no-cleanup', dest='no_cleanup',
323 help='no cleanup after run')
325 parser.add_argument('--cleanup', dest='cleanup',
328 help='Cleanup NFVbench resources (prompt to confirm)')
330 parser.add_argument('--force-cleanup', dest='force_cleanup',
333 help='Cleanup NFVbench resources (do not prompt)')
335 parser.add_argument('--json', dest='json',
337 help='store results in json format file',
338 metavar='<path>/<filename>')
340 parser.add_argument('--std-json', dest='std_json',
342 help='store results in json format file with nfvbench standard filename: '
343 '<service-chain-type>-<service-chain-count>-<flow-count>'
344 '-<packet-sizes>.json',
347 parser.add_argument('--show-default-config', dest='show_default_config',
350 help='print the default config in yaml format (unedited)')
352 parser.add_argument('--show-config', dest='show_config',
355 help='print the running config in json format')
357 parser.add_argument('-ss', '--show-summary', dest='summary',
359 help='Show summary from nfvbench json file',
362 parser.add_argument('-v', '--version', dest='version',
367 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
369 help='Override traffic profile frame sizes',
370 metavar='<frame_size_bytes or IMIX>')
372 parser.add_argument('--unidir', dest='unidir',
375 help='Override traffic profile direction (requires -fs)')
377 parser.add_argument('--log-file', '--logfile', dest='log_file',
379 help='Filename for saving logs',
380 metavar='<log_file>')
382 parser.add_argument('--user-label', '--userlabel', dest='user_label',
384 help='Custom label for performance records')
386 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
389 help='Port to port or port to switch to port L2 loopback with VLAN id')
391 opts, unknown_opts = parser.parse_known_args()
392 return opts, unknown_opts
395 def load_default_config():
396 default_cfg = resource_string(__name__, "cfg.default.yaml")
397 config = config_loads(default_cfg)
398 config.name = '(built-in default config)'
399 return config, default_cfg
402 def override_custom_traffic(config, frame_sizes, unidir):
403 """Override the traffic profiles with a custom one."""
404 if frame_sizes is not None:
405 traffic_profile_name = "custom_traffic_profile"
406 config.traffic_profile = [
408 "l2frame_size": frame_sizes,
409 "name": traffic_profile_name
413 traffic_profile_name = config.traffic["profile"]
415 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
417 "bidirectional": bidirectional,
418 "profile": traffic_profile_name
422 def check_physnet(name, netattrs):
423 if not netattrs.physical_network:
424 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
426 if not netattrs.segmentation_id:
427 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
430 def status_cleanup(config, cleanup, force_cleanup):
431 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
432 # check if another run is pending
435 with utils.RunLock():
436 LOG.info('Status: idle')
438 LOG.info('Status: busy (run pending)')
440 # check nfvbench resources
441 if config.openrc_file and config.service_chain != ChainType.EXT:
442 cleaner = Cleaner(config)
443 count = cleaner.show_resources()
444 if count and (cleanup or force_cleanup):
445 cleaner.clean(not force_cleanup)
450 run_summary_required = False
453 # load default config file
454 config, default_cfg = load_default_config()
455 # create factory for platform specific classes
457 factory_module = importlib.import_module(config['factory_module'])
458 factory = getattr(factory_module, config['factory_class'])()
459 except AttributeError:
460 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
461 .format(m=config['factory_module'], c=config['factory_class']))
462 # create config plugin for this platform
463 config_plugin = factory.get_config_plugin_class()(config)
464 config = config_plugin.get_config()
466 opts, unknown_opts = parse_opts_from_cli()
467 log.set_level(debug=opts.debug)
470 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
474 with open(opts.summary) as json_data:
475 result = json.load(json_data)
477 result['config']['user_label'] = opts.user_label
478 print NFVBenchSummarizer(result, fluent_logger)
481 # show default config in text/yaml format
482 if opts.show_default_config:
488 # do not check extra_specs in flavor as it can contain any key/value pairs
489 whitelist_keys = ['extra_specs']
490 # override default config options with start config at path parsed from CLI
491 # check if it is an inline yaml/json config or a file name
492 if os.path.isfile(opts.config):
493 LOG.info('Loading configuration file: %s', opts.config)
494 config = config_load(opts.config, config, whitelist_keys)
495 config.name = os.path.basename(opts.config)
497 LOG.info('Loading configuration string: %s', opts.config)
498 config = config_loads(opts.config, config, whitelist_keys)
500 # setup the fluent logger as soon as possible right after the config plugin is called,
501 # if there is any logging or result tag is set then initialize the fluent logger
502 for fluentd in config.fluentd:
503 if fluentd.logging_tag or fluentd.result_tag:
504 fluent_logger = FluentLogHandler(config.fluentd)
505 LOG.addHandler(fluent_logger)
508 # traffic profile override options
509 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
511 # copy over cli options that are used in config
512 config.generator_profile = opts.generator_profile
516 config.log_file = opts.log_file
517 if opts.service_chain:
518 config.service_chain = opts.service_chain
519 if opts.service_chain_count:
520 config.service_chain_count = opts.service_chain_count
521 if opts.no_vswitch_access:
522 config.no_vswitch_access = opts.no_vswitch_access
524 # port to port loopback (direct or through switch)
526 config.l2_loopback = True
527 if config.service_chain != ChainType.EXT:
528 LOG.info('Changing service chain type to EXT')
529 config.service_chain = ChainType.EXT
530 if not config.no_arp:
531 LOG.info('Disabling ARP')
533 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
534 LOG.info('Running L2 loopback: using EXT chain/no ARP')
536 if opts.use_sriov_middle_net:
537 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
538 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
539 config.use_sriov_middle_net = True
541 if config.sriov and config.service_chain != ChainType.EXT:
542 # if sriov is requested (does not apply to ext chains)
543 # make sure the physnet names are specified
544 check_physnet("left", config.internal_networks.left)
545 check_physnet("right", config.internal_networks.right)
546 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
547 check_physnet("middle", config.internal_networks.middle)
549 # show running config in json format
551 print json.dumps(config, sort_keys=True, indent=4)
554 # check that an empty openrc file (no OpenStack) is only allowed
556 if not config.openrc_file:
557 if config.service_chain == ChainType.EXT:
558 LOG.info('EXT chain with OpenStack mode disabled')
560 raise Exception("openrc_file is empty in the configuration and is required")
562 # update the config in the config plugin as it might have changed
563 # in a copy of the dict (config plugin still holds the original dict)
564 config_plugin.set_config(config)
566 if opts.status or opts.cleanup or opts.force_cleanup:
567 status_cleanup(config, opts.cleanup, opts.force_cleanup)
569 # add file log if requested
571 log.add_file_logger(config.log_file)
573 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
576 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
579 if os.path.isdir(opts.server):
580 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
581 nfvbench_instance.set_notifier(server)
583 port = int(opts.port)
585 server.run(host=opts.host)
587 server.run(host=opts.host, port=port)
589 print 'Invalid HTTP root directory: ' + opts.server
592 with utils.RunLock():
593 run_summary_required = True
595 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
597 raise Exception(err_msg)
599 # remove unfilled values
600 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
602 params = ' '.join(str(e) for e in sys.argv[1:])
603 result = nfvbench_instance.run(opts, params)
604 if 'error_message' in result:
605 raise Exception(result['error_message'])
607 if 'result' in result and result['status']:
608 nfvbench_instance.save(result['result'])
609 nfvbench_instance.prepare_summary(result['result'])
610 except Exception as exc:
611 run_summary_required = True
613 'status': NFVBench.STATUS_ERROR,
614 'error_message': traceback.format_exc()
619 # only send a summary record if there was an actual nfvbench run or
620 # if an error/exception was logged.
621 fluent_logger.send_run_summary(run_summary_required)
624 if __name__ == '__main__':