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
18 from collections import defaultdict
27 from attrdict import AttrDict
29 from pkg_resources import resource_string
31 from __init__ import __version__
32 from chain_runner import ChainRunner
33 from cleanup import Cleaner
34 from config import config_load
35 from config import config_loads
36 import credentials as credentials
37 from factory import BasicFactory
38 from fluentd import FluentLogHandler
41 from nfvbenchd import WebSocketIoServer
42 from specs import ChainType
43 from specs import Specs
44 from summarizer import NFVBenchSummarizer
45 from traffic_client import TrafficGeneratorFactory
51 class NFVBench(object):
52 """Main class of NFV benchmarking tool."""
55 STATUS_ERROR = 'ERROR'
57 def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
58 self.base_config = config
60 self.config_plugin = config_plugin
61 self.factory = factory
62 self.notifier = notifier
63 self.cred = credentials.Credentials(config.openrc_file, None, False) \
64 if config.openrc_file else None
65 self.chain_runner = None
67 self.specs.set_openstack_spec(openstack_spec)
68 self.clients = defaultdict(lambda: None)
73 self.specs.set_run_spec(self.config_plugin.get_run_spec(self.config, self.specs.openstack))
74 self.chain_runner = ChainRunner(self.config,
81 def set_notifier(self, notifier):
82 self.notifier = notifier
84 def run(self, opts, args):
85 status = NFVBench.STATUS_OK
89 # take a snapshot of the current time for this new run
90 # so that all subsequent logs can relate to this run
91 fluent_logger.start_new_run()
94 self.update_config(opts)
97 min_packet_size = "68" if self.config.vlan_tagging else "64"
98 for frame_size in self.config.frame_sizes:
100 if int(frame_size) < int(min_packet_size):
101 new_frame_sizes.append(min_packet_size)
102 LOG.info("Adjusting frame size %s Bytes to minimum size %s Bytes due to " +
103 "traffic generator restriction", frame_size, min_packet_size)
105 new_frame_sizes.append(frame_size)
107 new_frame_sizes.append(frame_size)
108 self.config.actual_frame_sizes = tuple(new_frame_sizes)
110 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
111 "nfvbench_version": __version__,
112 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
115 "service_chain": self.chain_runner.run(),
116 "versions": self.chain_runner.get_version(),
120 if self.specs.openstack:
121 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
122 "encaps": self.specs.openstack.encaps}
123 result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
125 status = NFVBench.STATUS_ERROR
126 message = traceback.format_exc()
127 except KeyboardInterrupt:
128 status = NFVBench.STATUS_ERROR
129 message = traceback.format_exc()
131 if self.chain_runner:
132 self.chain_runner.close()
134 if status == NFVBench.STATUS_OK:
135 result = utils.dict_to_json_dict(result)
142 'error_message': message
145 def prepare_summary(self, result):
146 """Prepare summary of the result to print and send it to logger (eg: fluentd)."""
148 summary = NFVBenchSummarizer(result, fluent_logger)
149 LOG.info(str(summary))
151 def save(self, result):
152 """Save results in json format file."""
153 utils.save_json_result(result,
154 self.config.json_file,
155 self.config.std_json_path,
156 self.config.service_chain,
157 self.config.service_chain_count,
158 self.config.flow_count,
159 self.config.frame_sizes)
161 def update_config(self, opts):
162 self.config = AttrDict(dict(self.base_config))
163 self.config.update(opts)
165 self.config.service_chain = self.config.service_chain.upper()
166 self.config.service_chain_count = int(self.config.service_chain_count)
167 self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
168 required_flow_count = self.config.service_chain_count * 2
169 if self.config.flow_count < required_flow_count:
170 LOG.info("Flow count %d has been set to minimum value of '%d' "
171 "for current configuration", self.config.flow_count,
173 self.config.flow_count = required_flow_count
175 if self.config.flow_count % 2 != 0:
176 self.config.flow_count += 1
178 self.config.duration_sec = float(self.config.duration_sec)
179 self.config.interval_sec = float(self.config.interval_sec)
181 # Get traffic generator profile config
182 if not self.config.generator_profile:
183 self.config.generator_profile = self.config.traffic_generator.default_profile
185 generator_factory = TrafficGeneratorFactory(self.config)
186 self.config.generator_config = \
187 generator_factory.get_generator_config(self.config.generator_profile)
189 # Check length of mac_addrs_left/right for serivce_chain EXT with no_arp
190 if self.config.service_chain == ChainType.EXT and self.config.no_arp:
191 if not (self.config.generator_config.mac_addrs_left is None and
192 self.config.generator_config.mac_addrs_right is None):
193 if (self.config.generator_config.mac_addrs_left is None or
194 self.config.generator_config.mac_addrs_right is None):
195 raise Exception("mac_addrs_left and mac_addrs_right must either "
196 "both be None or have a number of entries matching "
197 "service_chain_count")
198 if not (len(self.config.generator_config.mac_addrs_left) ==
199 self.config.service_chain_count and
200 len(self.config.generator_config.mac_addrs_right) ==
201 self.config.service_chain_count):
202 raise Exception("length of mac_addrs_left ({a}) and/or mac_addrs_right ({b}) "
203 "does not match service_chain_count ({c})"
204 .format(a=len(self.config.generator_config.mac_addrs_left),
205 b=len(self.config.generator_config.mac_addrs_right),
206 c=self.config.service_chain_count))
208 if not any(self.config.generator_config.pcis):
209 raise Exception("PCI addresses configuration for selected traffic generator profile "
210 "({tg_profile}) are missing. Please specify them in configuration file."
211 .format(tg_profile=self.config.generator_profile))
213 if self.config.traffic is None or not self.config.traffic:
214 raise Exception("No traffic profile found in traffic configuration, "
215 "please fill 'traffic' section in configuration file.")
217 if isinstance(self.config.traffic, tuple):
218 self.config.traffic = self.config.traffic[0]
220 self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
222 self.config.ipv6_mode = False
223 self.config.no_dhcp = True
224 self.config.same_network_only = True
225 if self.config.openrc_file:
226 self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
228 self.config.ndr_run = (not self.config.no_traffic and
229 'ndr' in self.config.rate.strip().lower().split('_'))
230 self.config.pdr_run = (not self.config.no_traffic and
231 'pdr' in self.config.rate.strip().lower().split('_'))
232 self.config.single_run = (not self.config.no_traffic and
233 not (self.config.ndr_run or self.config.pdr_run))
235 if self.config.vlans and len(self.config.vlans) != 2:
236 raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
238 self.config.json_file = self.config.json if self.config.json else None
239 if self.config.json_file:
240 (path, _filename) = os.path.split(self.config.json)
241 if not os.path.exists(path):
242 raise Exception('Please provide existing path for storing results in JSON file. '
243 'Path used: {path}'.format(path=path))
245 self.config.std_json_path = self.config.std_json if self.config.std_json else None
246 if self.config.std_json_path:
247 if not os.path.exists(self.config.std_json):
248 raise Exception('Please provide existing path for storing results in JSON file. '
249 'Path used: {path}'.format(path=self.config.std_json_path))
251 self.config_plugin.validate_config(self.config, self.specs.openstack)
254 def parse_opts_from_cli():
255 parser = argparse.ArgumentParser()
257 parser.add_argument('--status', dest='status',
260 help='Provide NFVbench status')
262 parser.add_argument('-c', '--config', dest='config',
264 help='Override default values with a config file or '
265 'a yaml/json config string',
266 metavar='<file_name_or_yaml>')
268 parser.add_argument('--server', dest='server',
271 metavar='<http_root_pathname>',
272 help='Run nfvbench in server mode and pass'
273 ' the HTTP root folder full pathname')
275 parser.add_argument('--host', dest='host',
278 help='Host IP address on which server will be listening (default 0.0.0.0)')
280 parser.add_argument('-p', '--port', dest='port',
283 help='Port on which server will be listening (default 7555)')
285 parser.add_argument('-sc', '--service-chain', dest='service_chain',
286 choices=BasicFactory.chain_classes,
288 help='Service chain to run')
290 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
292 help='Set number of service chains to run',
293 metavar='<service_chain_count>')
295 parser.add_argument('-fc', '--flow-count', dest='flow_count',
297 help='Set number of total flows for all chains and all directions',
298 metavar='<flow_count>')
300 parser.add_argument('--rate', dest='rate',
302 help='Specify rate in pps, bps or %% as total for all directions',
305 parser.add_argument('--duration', dest='duration_sec',
307 help='Set duration to run traffic generator (in seconds)',
308 metavar='<duration_sec>')
310 parser.add_argument('--interval', dest='interval_sec',
312 help='Set interval to record traffic generator stats (in seconds)',
313 metavar='<interval_sec>')
315 parser.add_argument('--inter-node', dest='inter_node',
318 help='run VMs in different compute nodes (PVVP only)')
320 parser.add_argument('--sriov', dest='sriov',
323 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
325 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
328 help='Use SRIOV to handle the middle network traffic '
329 '(PVVP with SRIOV only)')
331 parser.add_argument('-d', '--debug', dest='debug',
334 help='print debug messages (verbose)')
336 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
338 help='Traffic generator profile to use')
340 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
343 help='Check config and connectivity only - do not generate traffic')
345 parser.add_argument('--no-arp', dest='no_arp',
348 help='Do not use ARP to find MAC addresses, '
349 'instead use values in config file')
351 parser.add_argument('--no-reset', dest='no_reset',
354 help='Do not reset counters prior to running')
356 parser.add_argument('--no-int-config', dest='no_int_config',
359 help='Skip interfaces config on EXT service chain')
361 parser.add_argument('--no-tor-access', dest='no_tor_access',
364 help='Skip TOR switch configuration and retrieving of stats')
366 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
369 help='Skip vswitch configuration and retrieving of stats')
371 parser.add_argument('--no-cleanup', dest='no_cleanup',
374 help='no cleanup after run')
376 parser.add_argument('--cleanup', dest='cleanup',
379 help='Cleanup NFVbench resources (prompt to confirm)')
381 parser.add_argument('--force-cleanup', dest='force_cleanup',
384 help='Cleanup NFVbench resources (do not prompt)')
386 parser.add_argument('--json', dest='json',
388 help='store results in json format file',
389 metavar='<path>/<filename>')
391 parser.add_argument('--std-json', dest='std_json',
393 help='store results in json format file with nfvbench standard filename: '
394 '<service-chain-type>-<service-chain-count>-<flow-count>'
395 '-<packet-sizes>.json',
398 parser.add_argument('--show-default-config', dest='show_default_config',
401 help='print the default config in yaml format (unedited)')
403 parser.add_argument('--show-config', dest='show_config',
406 help='print the running config in json format')
408 parser.add_argument('-ss', '--show-summary', dest='summary',
410 help='Show summary from nfvbench json file',
413 parser.add_argument('-v', '--version', dest='version',
418 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
420 help='Override traffic profile frame sizes',
421 metavar='<frame_size_bytes or IMIX>')
423 parser.add_argument('--unidir', dest='unidir',
426 help='Override traffic profile direction (requires -fs)')
428 parser.add_argument('--log-file', '--logfile', dest='log_file',
430 help='Filename for saving logs',
431 metavar='<log_file>')
433 parser.add_argument('--user-label', '--userlabel', dest='user_label',
435 help='Custom label for performance records')
437 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
440 help='Port to port or port to switch to port L2 loopback with VLAN id')
442 opts, unknown_opts = parser.parse_known_args()
443 return opts, unknown_opts
446 def load_default_config():
447 default_cfg = resource_string(__name__, "cfg.default.yaml")
448 config = config_loads(default_cfg)
449 config.name = '(built-in default config)'
450 return config, default_cfg
453 def override_custom_traffic(config, frame_sizes, unidir):
454 """Override the traffic profiles with a custom one."""
455 if frame_sizes is not None:
456 traffic_profile_name = "custom_traffic_profile"
457 config.traffic_profile = [
459 "l2frame_size": frame_sizes,
460 "name": traffic_profile_name
464 traffic_profile_name = config.traffic["profile"]
466 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
468 "bidirectional": bidirectional,
469 "profile": traffic_profile_name
473 def check_physnet(name, netattrs):
474 if not netattrs.physical_network:
475 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
477 if not netattrs.segmentation_id:
478 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
481 def status_cleanup(config, cleanup, force_cleanup):
482 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
483 # check if another run is pending
486 with utils.RunLock():
487 LOG.info('Status: idle')
489 LOG.info('Status: busy (run pending)')
491 # check nfvbench resources
492 if config.openrc_file and config.service_chain != ChainType.EXT:
493 cleaner = Cleaner(config)
494 count = cleaner.show_resources()
495 if count and (cleanup or force_cleanup):
496 cleaner.clean(not force_cleanup)
501 run_summary_required = False
504 # load default config file
505 config, default_cfg = load_default_config()
506 # create factory for platform specific classes
508 factory_module = importlib.import_module(config['factory_module'])
509 factory = getattr(factory_module, config['factory_class'])()
510 except AttributeError:
511 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
512 .format(m=config['factory_module'], c=config['factory_class']))
513 # create config plugin for this platform
514 config_plugin = factory.get_config_plugin_class()(config)
515 config = config_plugin.get_config()
517 opts, unknown_opts = parse_opts_from_cli()
518 log.set_level(debug=opts.debug)
521 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
525 with open(opts.summary) as json_data:
526 result = json.load(json_data)
528 result['config']['user_label'] = opts.user_label
529 print NFVBenchSummarizer(result, fluent_logger)
532 # show default config in text/yaml format
533 if opts.show_default_config:
539 # do not check extra_specs in flavor as it can contain any key/value pairs
540 whitelist_keys = ['extra_specs']
541 # override default config options with start config at path parsed from CLI
542 # check if it is an inline yaml/json config or a file name
543 if os.path.isfile(opts.config):
544 LOG.info('Loading configuration file: %s', opts.config)
545 config = config_load(opts.config, config, whitelist_keys)
546 config.name = os.path.basename(opts.config)
548 LOG.info('Loading configuration string: %s', opts.config)
549 config = config_loads(opts.config, config, whitelist_keys)
551 # setup the fluent logger as soon as possible right after the config plugin is called,
552 # if there is any logging or result tag is set then initialize the fluent logger
553 for fluentd in config.fluentd:
554 if fluentd.logging_tag or fluentd.result_tag:
555 fluent_logger = FluentLogHandler(config.fluentd)
556 LOG.addHandler(fluent_logger)
559 # traffic profile override options
560 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
562 # copy over cli options that are used in config
563 config.generator_profile = opts.generator_profile
567 config.log_file = opts.log_file
568 if opts.service_chain:
569 config.service_chain = opts.service_chain
570 if opts.service_chain_count:
571 config.service_chain_count = opts.service_chain_count
572 if opts.no_vswitch_access:
573 config.no_vswitch_access = opts.no_vswitch_access
574 if opts.no_int_config:
575 config.no_int_config = opts.no_int_config
577 # port to port loopback (direct or through switch)
579 config.l2_loopback = True
580 if config.service_chain != ChainType.EXT:
581 LOG.info('Changing service chain type to EXT')
582 config.service_chain = ChainType.EXT
583 if not config.no_arp:
584 LOG.info('Disabling ARP')
586 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
587 # disable any form of interface config since we loop at the switch level
588 config.no_int_config = True
589 LOG.info('Running L2 loopback: using EXT chain/no ARP')
591 if opts.use_sriov_middle_net:
592 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
593 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
594 config.use_sriov_middle_net = True
596 if config.sriov and config.service_chain != ChainType.EXT:
597 # if sriov is requested (does not apply to ext chains)
598 # make sure the physnet names are specified
599 check_physnet("left", config.internal_networks.left)
600 check_physnet("right", config.internal_networks.right)
601 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
602 check_physnet("middle", config.internal_networks.middle)
604 # show running config in json format
606 print json.dumps(config, sort_keys=True, indent=4)
609 # check that an empty openrc file (no OpenStack) is only allowed
611 if not config.openrc_file:
612 if config.service_chain == ChainType.EXT:
613 LOG.info('EXT chain with OpenStack mode disabled')
615 raise Exception("openrc_file is empty in the configuration and is required")
617 # update the config in the config plugin as it might have changed
618 # in a copy of the dict (config plugin still holds the original dict)
619 config_plugin.set_config(config)
621 if opts.status or opts.cleanup or opts.force_cleanup:
622 status_cleanup(config, opts.cleanup, opts.force_cleanup)
624 # add file log if requested
626 log.add_file_logger(config.log_file)
628 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
631 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
634 if os.path.isdir(opts.server):
635 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
636 nfvbench_instance.set_notifier(server)
638 port = int(opts.port)
640 server.run(host=opts.host)
642 server.run(host=opts.host, port=port)
644 print 'Invalid HTTP root directory: ' + opts.server
647 with utils.RunLock():
648 run_summary_required = True
650 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
652 raise Exception(err_msg)
654 # remove unfilled values
655 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
657 params = ' '.join(str(e) for e in sys.argv[1:])
658 result = nfvbench_instance.run(opts, params)
659 if 'error_message' in result:
660 raise Exception(result['error_message'])
662 if 'result' in result and result['status']:
663 nfvbench_instance.save(result['result'])
664 nfvbench_instance.prepare_summary(result['result'])
665 except Exception as exc:
666 run_summary_required = True
668 'status': NFVBench.STATUS_ERROR,
669 'error_message': traceback.format_exc()
674 # only send a summary record if there was an actual nfvbench run or
675 # if an error/exception was logged.
676 fluent_logger.send_run_summary(run_summary_required)
679 if __name__ == '__main__':