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('--hypervisor', dest='hypervisor',
388 metavar='<hypervisor name>',
389 help='Where chains must run ("compute", "az:", "az:compute")')
391 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
394 help='Port to port or port to switch to port L2 loopback with VLAN id')
396 opts, unknown_opts = parser.parse_known_args()
397 return opts, unknown_opts
400 def load_default_config():
401 default_cfg = resource_string(__name__, "cfg.default.yaml")
402 config = config_loads(default_cfg)
403 config.name = '(built-in default config)'
404 return config, default_cfg
407 def override_custom_traffic(config, frame_sizes, unidir):
408 """Override the traffic profiles with a custom one."""
409 if frame_sizes is not None:
410 traffic_profile_name = "custom_traffic_profile"
411 config.traffic_profile = [
413 "l2frame_size": frame_sizes,
414 "name": traffic_profile_name
418 traffic_profile_name = config.traffic["profile"]
420 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
422 "bidirectional": bidirectional,
423 "profile": traffic_profile_name
427 def check_physnet(name, netattrs):
428 if not netattrs.physical_network:
429 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
431 if not netattrs.segmentation_id:
432 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
435 def status_cleanup(config, cleanup, force_cleanup):
436 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
437 # check if another run is pending
440 with utils.RunLock():
441 LOG.info('Status: idle')
443 LOG.info('Status: busy (run pending)')
445 # check nfvbench resources
446 if config.openrc_file and config.service_chain != ChainType.EXT:
447 cleaner = Cleaner(config)
448 count = cleaner.show_resources()
449 if count and (cleanup or force_cleanup):
450 cleaner.clean(not force_cleanup)
455 run_summary_required = False
458 # load default config file
459 config, default_cfg = load_default_config()
460 # create factory for platform specific classes
462 factory_module = importlib.import_module(config['factory_module'])
463 factory = getattr(factory_module, config['factory_class'])()
464 except AttributeError:
465 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
466 .format(m=config['factory_module'], c=config['factory_class']))
467 # create config plugin for this platform
468 config_plugin = factory.get_config_plugin_class()(config)
469 config = config_plugin.get_config()
471 opts, unknown_opts = parse_opts_from_cli()
472 log.set_level(debug=opts.debug)
475 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
479 with open(opts.summary) as json_data:
480 result = json.load(json_data)
482 result['config']['user_label'] = opts.user_label
483 print NFVBenchSummarizer(result, fluent_logger)
486 # show default config in text/yaml format
487 if opts.show_default_config:
493 # do not check extra_specs in flavor as it can contain any key/value pairs
494 whitelist_keys = ['extra_specs']
495 # override default config options with start config at path parsed from CLI
496 # check if it is an inline yaml/json config or a file name
497 if os.path.isfile(opts.config):
498 LOG.info('Loading configuration file: %s', opts.config)
499 config = config_load(opts.config, config, whitelist_keys)
500 config.name = os.path.basename(opts.config)
502 LOG.info('Loading configuration string: %s', opts.config)
503 config = config_loads(opts.config, config, whitelist_keys)
505 # setup the fluent logger as soon as possible right after the config plugin is called,
506 # if there is any logging or result tag is set then initialize the fluent logger
507 for fluentd in config.fluentd:
508 if fluentd.logging_tag or fluentd.result_tag:
509 fluent_logger = FluentLogHandler(config.fluentd)
510 LOG.addHandler(fluent_logger)
513 # traffic profile override options
514 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
516 # copy over cli options that are used in config
517 config.generator_profile = opts.generator_profile
521 config.log_file = opts.log_file
522 if opts.service_chain:
523 config.service_chain = opts.service_chain
524 if opts.service_chain_count:
525 config.service_chain_count = opts.service_chain_count
526 if opts.no_vswitch_access:
527 config.no_vswitch_access = opts.no_vswitch_access
529 # can be any of 'comp1', 'nova:', 'nova:comp1'
530 config.compute_nodes = opts.hypervisor
532 # port to port loopback (direct or through switch)
534 config.l2_loopback = True
535 if config.service_chain != ChainType.EXT:
536 LOG.info('Changing service chain type to EXT')
537 config.service_chain = ChainType.EXT
538 if not config.no_arp:
539 LOG.info('Disabling ARP')
541 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
542 LOG.info('Running L2 loopback: using EXT chain/no ARP')
544 if opts.use_sriov_middle_net:
545 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
546 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
547 config.use_sriov_middle_net = True
549 if config.sriov and config.service_chain != ChainType.EXT:
550 # if sriov is requested (does not apply to ext chains)
551 # make sure the physnet names are specified
552 check_physnet("left", config.internal_networks.left)
553 check_physnet("right", config.internal_networks.right)
554 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
555 check_physnet("middle", config.internal_networks.middle)
557 # show running config in json format
559 print json.dumps(config, sort_keys=True, indent=4)
562 # check that an empty openrc file (no OpenStack) is only allowed
564 if not config.openrc_file:
565 if config.service_chain == ChainType.EXT:
566 LOG.info('EXT chain with OpenStack mode disabled')
568 raise Exception("openrc_file is empty in the configuration and is required")
570 # update the config in the config plugin as it might have changed
571 # in a copy of the dict (config plugin still holds the original dict)
572 config_plugin.set_config(config)
574 if opts.status or opts.cleanup or opts.force_cleanup:
575 status_cleanup(config, opts.cleanup, opts.force_cleanup)
577 # add file log if requested
579 log.add_file_logger(config.log_file)
581 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
584 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
587 if os.path.isdir(opts.server):
588 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
589 nfvbench_instance.set_notifier(server)
591 port = int(opts.port)
593 server.run(host=opts.host)
595 server.run(host=opts.host, port=port)
597 print 'Invalid HTTP root directory: ' + opts.server
600 with utils.RunLock():
601 run_summary_required = True
603 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
605 raise Exception(err_msg)
607 # remove unfilled values
608 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
610 params = ' '.join(str(e) for e in sys.argv[1:])
611 result = nfvbench_instance.run(opts, params)
612 if 'error_message' in result:
613 raise Exception(result['error_message'])
615 if 'result' in result and result['status']:
616 nfvbench_instance.save(result['result'])
617 nfvbench_instance.prepare_summary(result['result'])
618 except Exception as exc:
619 run_summary_required = True
621 'status': NFVBench.STATUS_ERROR,
622 'error_message': traceback.format_exc()
627 # only send a summary record if there was an actual nfvbench run or
628 # if an error/exception was logged.
629 fluent_logger.send_run_summary(run_summary_required)
632 if __name__ == '__main__':