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)
180 self.config.pause_sec = float(self.config.pause_sec)
182 # Get traffic generator profile config
183 if not self.config.generator_profile:
184 self.config.generator_profile = self.config.traffic_generator.default_profile
186 generator_factory = TrafficGeneratorFactory(self.config)
187 self.config.generator_config = \
188 generator_factory.get_generator_config(self.config.generator_profile)
190 # Check length of mac_addrs_left/right for serivce_chain EXT with no_arp
191 if self.config.service_chain == ChainType.EXT and self.config.no_arp:
192 if not (self.config.generator_config.mac_addrs_left is None and
193 self.config.generator_config.mac_addrs_right is None):
194 if (self.config.generator_config.mac_addrs_left is None or
195 self.config.generator_config.mac_addrs_right is None):
196 raise Exception("mac_addrs_left and mac_addrs_right must either "
197 "both be None or have a number of entries matching "
198 "service_chain_count")
199 if not (len(self.config.generator_config.mac_addrs_left) ==
200 self.config.service_chain_count and
201 len(self.config.generator_config.mac_addrs_right) ==
202 self.config.service_chain_count):
203 raise Exception("length of mac_addrs_left ({a}) and/or mac_addrs_right ({b}) "
204 "does not match service_chain_count ({c})"
205 .format(a=len(self.config.generator_config.mac_addrs_left),
206 b=len(self.config.generator_config.mac_addrs_right),
207 c=self.config.service_chain_count))
209 if not any(self.config.generator_config.pcis):
210 raise Exception("PCI addresses configuration for selected traffic generator profile "
211 "({tg_profile}) are missing. Please specify them in configuration file."
212 .format(tg_profile=self.config.generator_profile))
214 if self.config.traffic is None or not self.config.traffic:
215 raise Exception("No traffic profile found in traffic configuration, "
216 "please fill 'traffic' section in configuration file.")
218 if isinstance(self.config.traffic, tuple):
219 self.config.traffic = self.config.traffic[0]
221 self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
223 self.config.ipv6_mode = False
224 self.config.no_dhcp = True
225 self.config.same_network_only = True
226 if self.config.openrc_file:
227 self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
229 self.config.ndr_run = (not self.config.no_traffic and
230 'ndr' in self.config.rate.strip().lower().split('_'))
231 self.config.pdr_run = (not self.config.no_traffic and
232 'pdr' in self.config.rate.strip().lower().split('_'))
233 self.config.single_run = (not self.config.no_traffic and
234 not (self.config.ndr_run or self.config.pdr_run))
236 if self.config.vlans and len(self.config.vlans) != 2:
237 raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
239 self.config.json_file = self.config.json if self.config.json else None
240 if self.config.json_file:
241 (path, _filename) = os.path.split(self.config.json)
242 if not os.path.exists(path):
243 raise Exception('Please provide existing path for storing results in JSON file. '
244 'Path used: {path}'.format(path=path))
246 self.config.std_json_path = self.config.std_json if self.config.std_json else None
247 if self.config.std_json_path:
248 if not os.path.exists(self.config.std_json):
249 raise Exception('Please provide existing path for storing results in JSON file. '
250 'Path used: {path}'.format(path=self.config.std_json_path))
252 self.config_plugin.validate_config(self.config, self.specs.openstack)
255 def parse_opts_from_cli():
256 parser = argparse.ArgumentParser()
258 parser.add_argument('--status', dest='status',
261 help='Provide NFVbench status')
263 parser.add_argument('-c', '--config', dest='config',
265 help='Override default values with a config file or '
266 'a yaml/json config string',
267 metavar='<file_name_or_yaml>')
269 parser.add_argument('--server', dest='server',
272 metavar='<http_root_pathname>',
273 help='Run nfvbench in server mode and pass'
274 ' the HTTP root folder full pathname')
276 parser.add_argument('--host', dest='host',
279 help='Host IP address on which server will be listening (default 0.0.0.0)')
281 parser.add_argument('-p', '--port', dest='port',
284 help='Port on which server will be listening (default 7555)')
286 parser.add_argument('-sc', '--service-chain', dest='service_chain',
287 choices=BasicFactory.chain_classes,
289 help='Service chain to run')
291 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
293 help='Set number of service chains to run',
294 metavar='<service_chain_count>')
296 parser.add_argument('-fc', '--flow-count', dest='flow_count',
298 help='Set number of total flows for all chains and all directions',
299 metavar='<flow_count>')
301 parser.add_argument('--rate', dest='rate',
303 help='Specify rate in pps, bps or %% as total for all directions',
306 parser.add_argument('--duration', dest='duration_sec',
308 help='Set duration to run traffic generator (in seconds)',
309 metavar='<duration_sec>')
311 parser.add_argument('--interval', dest='interval_sec',
313 help='Set interval to record traffic generator stats (in seconds)',
314 metavar='<interval_sec>')
316 parser.add_argument('--inter-node', dest='inter_node',
319 help='run VMs in different compute nodes (PVVP only)')
321 parser.add_argument('--sriov', dest='sriov',
324 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
326 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
329 help='Use SRIOV to handle the middle network traffic '
330 '(PVVP with SRIOV only)')
332 parser.add_argument('-d', '--debug', dest='debug',
335 help='print debug messages (verbose)')
337 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
339 help='Traffic generator profile to use')
341 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
344 help='Check config and connectivity only - do not generate traffic')
346 parser.add_argument('--no-arp', dest='no_arp',
349 help='Do not use ARP to find MAC addresses, '
350 'instead use values in config file')
352 parser.add_argument('--no-reset', dest='no_reset',
355 help='Do not reset counters prior to running')
357 parser.add_argument('--no-int-config', dest='no_int_config',
360 help='Skip interfaces config on EXT service chain')
362 parser.add_argument('--no-tor-access', dest='no_tor_access',
365 help='Skip TOR switch configuration and retrieving of stats')
367 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
370 help='Skip vswitch configuration and retrieving of stats')
372 parser.add_argument('--no-cleanup', dest='no_cleanup',
375 help='no cleanup after run')
377 parser.add_argument('--cleanup', dest='cleanup',
380 help='Cleanup NFVbench resources (prompt to confirm)')
382 parser.add_argument('--force-cleanup', dest='force_cleanup',
385 help='Cleanup NFVbench resources (do not prompt)')
387 parser.add_argument('--json', dest='json',
389 help='store results in json format file',
390 metavar='<path>/<filename>')
392 parser.add_argument('--std-json', dest='std_json',
394 help='store results in json format file with nfvbench standard filename: '
395 '<service-chain-type>-<service-chain-count>-<flow-count>'
396 '-<packet-sizes>.json',
399 parser.add_argument('--show-default-config', dest='show_default_config',
402 help='print the default config in yaml format (unedited)')
404 parser.add_argument('--show-config', dest='show_config',
407 help='print the running config in json format')
409 parser.add_argument('-ss', '--show-summary', dest='summary',
411 help='Show summary from nfvbench json file',
414 parser.add_argument('-v', '--version', dest='version',
419 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
421 help='Override traffic profile frame sizes',
422 metavar='<frame_size_bytes or IMIX>')
424 parser.add_argument('--unidir', dest='unidir',
427 help='Override traffic profile direction (requires -fs)')
429 parser.add_argument('--log-file', '--logfile', dest='log_file',
431 help='Filename for saving logs',
432 metavar='<log_file>')
434 parser.add_argument('--user-label', '--userlabel', dest='user_label',
436 help='Custom label for performance records')
438 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
441 help='Port to port or port to switch to port L2 loopback with VLAN id')
443 opts, unknown_opts = parser.parse_known_args()
444 return opts, unknown_opts
447 def load_default_config():
448 default_cfg = resource_string(__name__, "cfg.default.yaml")
449 config = config_loads(default_cfg)
450 config.name = '(built-in default config)'
451 return config, default_cfg
454 def override_custom_traffic(config, frame_sizes, unidir):
455 """Override the traffic profiles with a custom one."""
456 if frame_sizes is not None:
457 traffic_profile_name = "custom_traffic_profile"
458 config.traffic_profile = [
460 "l2frame_size": frame_sizes,
461 "name": traffic_profile_name
465 traffic_profile_name = config.traffic["profile"]
467 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
469 "bidirectional": bidirectional,
470 "profile": traffic_profile_name
474 def check_physnet(name, netattrs):
475 if not netattrs.physical_network:
476 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
478 if not netattrs.segmentation_id:
479 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
482 def status_cleanup(config, cleanup, force_cleanup):
483 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
484 # check if another run is pending
487 with utils.RunLock():
488 LOG.info('Status: idle')
490 LOG.info('Status: busy (run pending)')
492 # check nfvbench resources
493 if config.openrc_file and config.service_chain != ChainType.EXT:
494 cleaner = Cleaner(config)
495 count = cleaner.show_resources()
496 if count and (cleanup or force_cleanup):
497 cleaner.clean(not force_cleanup)
502 run_summary_required = False
505 # load default config file
506 config, default_cfg = load_default_config()
507 # create factory for platform specific classes
509 factory_module = importlib.import_module(config['factory_module'])
510 factory = getattr(factory_module, config['factory_class'])()
511 except AttributeError:
512 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
513 .format(m=config['factory_module'], c=config['factory_class']))
514 # create config plugin for this platform
515 config_plugin = factory.get_config_plugin_class()(config)
516 config = config_plugin.get_config()
518 opts, unknown_opts = parse_opts_from_cli()
519 log.set_level(debug=opts.debug)
522 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
526 with open(opts.summary) as json_data:
527 result = json.load(json_data)
529 result['config']['user_label'] = opts.user_label
530 print NFVBenchSummarizer(result, fluent_logger)
533 # show default config in text/yaml format
534 if opts.show_default_config:
540 # do not check extra_specs in flavor as it can contain any key/value pairs
541 whitelist_keys = ['extra_specs']
542 # override default config options with start config at path parsed from CLI
543 # check if it is an inline yaml/json config or a file name
544 if os.path.isfile(opts.config):
545 LOG.info('Loading configuration file: %s', opts.config)
546 config = config_load(opts.config, config, whitelist_keys)
547 config.name = os.path.basename(opts.config)
549 LOG.info('Loading configuration string: %s', opts.config)
550 config = config_loads(opts.config, config, whitelist_keys)
552 # setup the fluent logger as soon as possible right after the config plugin is called,
553 # if there is any logging or result tag is set then initialize the fluent logger
554 for fluentd in config.fluentd:
555 if fluentd.logging_tag or fluentd.result_tag:
556 fluent_logger = FluentLogHandler(config.fluentd)
557 LOG.addHandler(fluent_logger)
560 # traffic profile override options
561 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
563 # copy over cli options that are used in config
564 config.generator_profile = opts.generator_profile
568 config.log_file = opts.log_file
569 if opts.service_chain:
570 config.service_chain = opts.service_chain
571 if opts.service_chain_count:
572 config.service_chain_count = opts.service_chain_count
573 if opts.no_vswitch_access:
574 config.no_vswitch_access = opts.no_vswitch_access
575 if opts.no_int_config:
576 config.no_int_config = opts.no_int_config
578 # port to port loopback (direct or through switch)
580 config.l2_loopback = True
581 if config.service_chain != ChainType.EXT:
582 LOG.info('Changing service chain type to EXT')
583 config.service_chain = ChainType.EXT
584 if not config.no_arp:
585 LOG.info('Disabling ARP')
587 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
588 # disable any form of interface config since we loop at the switch level
589 config.no_int_config = True
590 LOG.info('Running L2 loopback: using EXT chain/no ARP')
592 if opts.use_sriov_middle_net:
593 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
594 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
595 config.use_sriov_middle_net = True
597 if config.sriov and config.service_chain != ChainType.EXT:
598 # if sriov is requested (does not apply to ext chains)
599 # make sure the physnet names are specified
600 check_physnet("left", config.internal_networks.left)
601 check_physnet("right", config.internal_networks.right)
602 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
603 check_physnet("middle", config.internal_networks.middle)
605 # show running config in json format
607 print json.dumps(config, sort_keys=True, indent=4)
610 # check that an empty openrc file (no OpenStack) is only allowed
612 if not config.openrc_file:
613 if config.service_chain == ChainType.EXT:
614 LOG.info('EXT chain with OpenStack mode disabled')
616 raise Exception("openrc_file is empty in the configuration and is required")
618 # update the config in the config plugin as it might have changed
619 # in a copy of the dict (config plugin still holds the original dict)
620 config_plugin.set_config(config)
622 if opts.status or opts.cleanup or opts.force_cleanup:
623 status_cleanup(config, opts.cleanup, opts.force_cleanup)
625 # add file log if requested
627 log.add_file_logger(config.log_file)
629 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
632 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
635 if os.path.isdir(opts.server):
636 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
637 nfvbench_instance.set_notifier(server)
639 port = int(opts.port)
641 server.run(host=opts.host)
643 server.run(host=opts.host, port=port)
645 print 'Invalid HTTP root directory: ' + opts.server
648 with utils.RunLock():
649 run_summary_required = True
651 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
653 raise Exception(err_msg)
655 # remove unfilled values
656 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
658 params = ' '.join(str(e) for e in sys.argv[1:])
659 result = nfvbench_instance.run(opts, params)
660 if 'error_message' in result:
661 raise Exception(result['error_message'])
663 if 'result' in result and result['status']:
664 nfvbench_instance.save(result['result'])
665 nfvbench_instance.prepare_summary(result['result'])
666 except Exception as exc:
667 run_summary_required = True
669 'status': NFVBench.STATUS_ERROR,
670 'error_message': traceback.format_exc()
675 # only send a summary record if there was an actual nfvbench run or
676 # if an error/exception was logged.
677 fluent_logger.send_run_summary(run_summary_required)
680 if __name__ == '__main__':