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)
71 def check_options(self):
72 if self.base_config.vxlan:
73 if self.base_config.vlan_tagging:
75 'Inner VLAN tagging is not currently supported for VXLAN')
76 vtep_vlan = self.base_config.traffic_generator.get('vtep_vlan')
78 LOG.warning('Warning: VXLAN mode enabled, but VTEP vlan is not defined')
80 def set_notifier(self, notifier):
81 self.notifier = notifier
83 def run(self, opts, args):
84 status = NFVBench.STATUS_OK
88 # take a snapshot of the current time for this new run
89 # so that all subsequent logs can relate to this run
90 fluent_logger.start_new_run()
93 # recalc the running config based on the base config and options for this run
94 self._update_config(opts)
95 self.specs.set_run_spec(self.config_plugin.get_run_spec(self.config,
96 self.specs.openstack))
97 self.chain_runner = ChainRunner(self.config,
103 # make sure that the min frame size is 64
105 for frame_size in self.config.frame_sizes:
107 if int(frame_size) < min_packet_size:
108 frame_size = str(min_packet_size)
109 LOG.info("Adjusting frame size %s bytes to minimum size %s bytes",
110 frame_size, min_packet_size)
111 if frame_size not in new_frame_sizes:
112 new_frame_sizes.append(frame_size)
114 new_frame_sizes.append(frame_size.upper())
115 self.config.frame_sizes = new_frame_sizes
117 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
118 "nfvbench_version": __version__,
119 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
122 "service_chain": self.chain_runner.run(),
123 "versions": self.chain_runner.get_version(),
127 if self.specs.openstack:
128 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
129 "encaps": self.specs.openstack.encaps}
130 result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
132 status = NFVBench.STATUS_ERROR
133 message = traceback.format_exc()
134 except KeyboardInterrupt:
135 status = NFVBench.STATUS_ERROR
136 message = traceback.format_exc()
138 if self.chain_runner:
139 self.chain_runner.close()
141 if status == NFVBench.STATUS_OK:
142 # result2 = utils.dict_to_json_dict(result)
149 'error_message': message
152 def prepare_summary(self, result):
153 """Prepare summary of the result to print and send it to logger (eg: fluentd)."""
155 summary = NFVBenchSummarizer(result, fluent_logger)
156 LOG.info(str(summary))
158 def save(self, result):
159 """Save results in json format file."""
160 utils.save_json_result(result,
161 self.config.json_file,
162 self.config.std_json_path,
163 self.config.service_chain,
164 self.config.service_chain_count,
165 self.config.flow_count,
166 self.config.frame_sizes)
168 def _update_config(self, opts):
169 """Recalculate the running config based on the base config and opts.
171 Sanity check on the config is done here as well.
173 self.config = AttrDict(dict(self.base_config))
174 self.config.update(opts)
177 config.service_chain = config.service_chain.upper()
178 config.service_chain_count = int(config.service_chain_count)
179 if config.l2_loopback:
180 # force the number of chains to be 1 in case of l2 loopback
181 config.service_chain_count = 1
182 config.service_chain = ChainType.EXT
184 LOG.info('Running L2 loopback: using EXT chain/no ARP')
185 config.flow_count = utils.parse_flow_count(config.flow_count)
186 required_flow_count = config.service_chain_count * 2
187 if config.flow_count < required_flow_count:
188 LOG.info("Flow count %d has been set to minimum value of '%d' "
189 "for current configuration", config.flow_count,
191 config.flow_count = required_flow_count
193 if config.flow_count % 2:
194 config.flow_count += 1
196 config.duration_sec = float(config.duration_sec)
197 config.interval_sec = float(config.interval_sec)
198 config.pause_sec = float(config.pause_sec)
200 if config.traffic is None or not config.traffic:
201 raise Exception("Missing traffic property in configuration")
203 if config.openrc_file:
204 config.openrc_file = os.path.expanduser(config.openrc_file)
206 config.ndr_run = (not config.no_traffic and
207 'ndr' in config.rate.strip().lower().split('_'))
208 config.pdr_run = (not config.no_traffic and
209 'pdr' in config.rate.strip().lower().split('_'))
210 config.single_run = (not config.no_traffic and
211 not (config.ndr_run or config.pdr_run))
213 config.json_file = config.json if config.json else None
215 (path, _filename) = os.path.split(config.json)
216 if not os.path.exists(path):
217 raise Exception('Please provide existing path for storing results in JSON file. '
218 'Path used: {path}'.format(path=path))
220 config.std_json_path = config.std_json if config.std_json else None
221 if config.std_json_path:
222 if not os.path.exists(config.std_json):
223 raise Exception('Please provide existing path for storing results in JSON file. '
224 'Path used: {path}'.format(path=config.std_json_path))
226 self.config_plugin.validate_config(config, self.specs.openstack)
229 def _parse_opts_from_cli():
230 parser = argparse.ArgumentParser()
232 parser.add_argument('--status', dest='status',
235 help='Provide NFVbench status')
237 parser.add_argument('-c', '--config', dest='config',
239 help='Override default values with a config file or '
240 'a yaml/json config string',
241 metavar='<file_name_or_yaml>')
243 parser.add_argument('--server', dest='server',
246 metavar='<http_root_pathname>',
247 help='Run nfvbench in server mode and pass'
248 ' the HTTP root folder full pathname')
250 parser.add_argument('--host', dest='host',
253 help='Host IP address on which server will be listening (default 0.0.0.0)')
255 parser.add_argument('-p', '--port', dest='port',
258 help='Port on which server will be listening (default 7555)')
260 parser.add_argument('-sc', '--service-chain', dest='service_chain',
261 choices=ChainType.names,
263 help='Service chain to run')
265 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
267 help='Set number of service chains to run',
268 metavar='<service_chain_count>')
270 parser.add_argument('-fc', '--flow-count', dest='flow_count',
272 help='Set number of total flows for all chains and all directions',
273 metavar='<flow_count>')
275 parser.add_argument('--rate', dest='rate',
277 help='Specify rate in pps, bps or %% as total for all directions',
280 parser.add_argument('--duration', dest='duration_sec',
282 help='Set duration to run traffic generator (in seconds)',
283 metavar='<duration_sec>')
285 parser.add_argument('--interval', dest='interval_sec',
287 help='Set interval to record traffic generator stats (in seconds)',
288 metavar='<interval_sec>')
290 parser.add_argument('--inter-node', dest='inter_node',
295 parser.add_argument('--sriov', dest='sriov',
298 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
300 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
303 help='Use SRIOV to handle the middle network traffic '
304 '(PVVP with SRIOV only)')
306 parser.add_argument('-d', '--debug', dest='debug',
309 help='print debug messages (verbose)')
311 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
313 help='Traffic generator profile to use')
315 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
318 help='Check config and connectivity only - do not generate traffic')
320 parser.add_argument('--no-arp', dest='no_arp',
323 help='Do not use ARP to find MAC addresses, '
324 'instead use values in config file')
326 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
329 help='Skip vswitch configuration and retrieving of stats')
331 parser.add_argument('--vxlan', dest='vxlan',
334 help='Enable VxLan encapsulation')
336 parser.add_argument('--no-cleanup', dest='no_cleanup',
339 help='no cleanup after run')
341 parser.add_argument('--cleanup', dest='cleanup',
344 help='Cleanup NFVbench resources (prompt to confirm)')
346 parser.add_argument('--force-cleanup', dest='force_cleanup',
349 help='Cleanup NFVbench resources (do not prompt)')
351 parser.add_argument('--json', dest='json',
353 help='store results in json format file',
354 metavar='<path>/<filename>')
356 parser.add_argument('--std-json', dest='std_json',
358 help='store results in json format file with nfvbench standard filename: '
359 '<service-chain-type>-<service-chain-count>-<flow-count>'
360 '-<packet-sizes>.json',
363 parser.add_argument('--show-default-config', dest='show_default_config',
366 help='print the default config in yaml format (unedited)')
368 parser.add_argument('--show-config', dest='show_config',
371 help='print the running config in json format')
373 parser.add_argument('-ss', '--show-summary', dest='summary',
375 help='Show summary from nfvbench json file',
378 parser.add_argument('-v', '--version', dest='version',
383 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
385 help='Override traffic profile frame sizes',
386 metavar='<frame_size_bytes or IMIX>')
388 parser.add_argument('--unidir', dest='unidir',
391 help='Override traffic profile direction (requires -fs)')
393 parser.add_argument('--log-file', '--logfile', dest='log_file',
395 help='Filename for saving logs',
396 metavar='<log_file>')
398 parser.add_argument('--user-label', '--userlabel', dest='user_label',
400 help='Custom label for performance records')
402 parser.add_argument('--hypervisor', dest='hypervisor',
404 metavar='<hypervisor name>',
405 help='Where chains must run ("compute", "az:", "az:compute")')
407 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
410 help='Port to port or port to switch to port L2 loopback with VLAN id')
412 opts, unknown_opts = parser.parse_known_args()
413 return opts, unknown_opts
416 def load_default_config():
417 default_cfg = resource_string(__name__, "cfg.default.yaml")
418 config = config_loads(default_cfg)
419 config.name = '(built-in default config)'
420 return config, default_cfg
423 def override_custom_traffic(config, frame_sizes, unidir):
424 """Override the traffic profiles with a custom one."""
425 if frame_sizes is not None:
426 traffic_profile_name = "custom_traffic_profile"
427 config.traffic_profile = [
429 "l2frame_size": frame_sizes,
430 "name": traffic_profile_name
434 traffic_profile_name = config.traffic["profile"]
436 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
438 "bidirectional": bidirectional,
439 "profile": traffic_profile_name
443 def check_physnet(name, netattrs):
444 if not netattrs.physical_network:
445 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
447 if not netattrs.segmentation_id:
448 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
451 def status_cleanup(config, cleanup, force_cleanup):
452 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
453 # check if another run is pending
456 with utils.RunLock():
457 LOG.info('Status: idle')
459 LOG.info('Status: busy (run pending)')
461 # check nfvbench resources
462 if config.openrc_file and config.service_chain != ChainType.EXT:
463 cleaner = Cleaner(config)
464 count = cleaner.show_resources()
465 if count and (cleanup or force_cleanup):
466 cleaner.clean(not force_cleanup)
471 run_summary_required = False
474 # load default config file
475 config, default_cfg = load_default_config()
476 # create factory for platform specific classes
478 factory_module = importlib.import_module(config['factory_module'])
479 factory = getattr(factory_module, config['factory_class'])()
480 except AttributeError:
481 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
482 .format(m=config['factory_module'], c=config['factory_class']))
483 # create config plugin for this platform
484 config_plugin = factory.get_config_plugin_class()(config)
485 config = config_plugin.get_config()
487 opts, unknown_opts = _parse_opts_from_cli()
488 log.set_level(debug=opts.debug)
491 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
495 with open(opts.summary) as json_data:
496 result = json.load(json_data)
498 result['config']['user_label'] = opts.user_label
499 print NFVBenchSummarizer(result, fluent_logger)
502 # show default config in text/yaml format
503 if opts.show_default_config:
509 # do not check extra_specs in flavor as it can contain any key/value pairs
510 whitelist_keys = ['extra_specs']
511 # override default config options with start config at path parsed from CLI
512 # check if it is an inline yaml/json config or a file name
513 if os.path.isfile(opts.config):
514 LOG.info('Loading configuration file: %s', opts.config)
515 config = config_load(opts.config, config, whitelist_keys)
516 config.name = os.path.basename(opts.config)
518 LOG.info('Loading configuration string: %s', opts.config)
519 config = config_loads(opts.config, config, whitelist_keys)
521 # setup the fluent logger as soon as possible right after the config plugin is called,
522 # if there is any logging or result tag is set then initialize the fluent logger
523 for fluentd in config.fluentd:
524 if fluentd.logging_tag or fluentd.result_tag:
525 fluent_logger = FluentLogHandler(config.fluentd)
526 LOG.addHandler(fluent_logger)
529 # traffic profile override options
530 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
532 # copy over cli options that are used in config
533 config.generator_profile = opts.generator_profile
537 config.log_file = opts.log_file
538 if opts.service_chain:
539 config.service_chain = opts.service_chain
540 if opts.service_chain_count:
541 config.service_chain_count = opts.service_chain_count
542 if opts.no_vswitch_access:
543 config.no_vswitch_access = opts.no_vswitch_access
545 # can be any of 'comp1', 'nova:', 'nova:comp1'
546 config.compute_nodes = opts.hypervisor
548 # port to port loopback (direct or through switch)
550 config.l2_loopback = True
551 if config.service_chain != ChainType.EXT:
552 LOG.info('Changing service chain type to EXT')
553 config.service_chain = ChainType.EXT
554 if not config.no_arp:
555 LOG.info('Disabling ARP')
557 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
558 LOG.info('Running L2 loopback: using EXT chain/no ARP')
560 if opts.use_sriov_middle_net:
561 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
562 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
563 config.use_sriov_middle_net = True
565 if config.sriov and config.service_chain != ChainType.EXT:
566 # if sriov is requested (does not apply to ext chains)
567 # make sure the physnet names are specified
568 check_physnet("left", config.internal_networks.left)
569 check_physnet("right", config.internal_networks.right)
570 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
571 check_physnet("middle", config.internal_networks.middle)
573 # show running config in json format
575 print json.dumps(config, sort_keys=True, indent=4)
578 # check that an empty openrc file (no OpenStack) is only allowed
580 if not config.openrc_file:
581 if config.service_chain == ChainType.EXT:
582 LOG.info('EXT chain with OpenStack mode disabled')
584 raise Exception("openrc_file is empty in the configuration and is required")
586 # update the config in the config plugin as it might have changed
587 # in a copy of the dict (config plugin still holds the original dict)
588 config_plugin.set_config(config)
590 if opts.status or opts.cleanup or opts.force_cleanup:
591 status_cleanup(config, opts.cleanup, opts.force_cleanup)
593 # add file log if requested
595 log.add_file_logger(config.log_file)
597 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
600 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
603 if os.path.isdir(opts.server):
604 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
605 nfvbench_instance.set_notifier(server)
607 port = int(opts.port)
609 server.run(host=opts.host)
611 server.run(host=opts.host, port=port)
613 print 'Invalid HTTP root directory: ' + opts.server
616 with utils.RunLock():
617 run_summary_required = True
619 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
621 raise Exception(err_msg)
623 # remove unfilled values
624 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
626 params = ' '.join(str(e) for e in sys.argv[1:])
627 result = nfvbench_instance.run(opts, params)
628 if 'error_message' in result:
629 raise Exception(result['error_message'])
631 if 'result' in result and result['status']:
632 nfvbench_instance.save(result['result'])
633 nfvbench_instance.prepare_summary(result['result'])
634 except Exception as exc:
635 run_summary_required = True
637 'status': NFVBench.STATUS_ERROR,
638 'error_message': traceback.format_exc()
643 # only send a summary record if there was an actual nfvbench run or
644 # if an error/exception was logged.
645 fluent_logger.send_run_summary(run_summary_required)
648 if __name__ == '__main__':