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.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 opts, unknown_opts = parser.parse_known_args()
438 return opts, unknown_opts
441 def load_default_config():
442 default_cfg = resource_string(__name__, "cfg.default.yaml")
443 config = config_loads(default_cfg)
444 config.name = '(built-in default config)'
445 return config, default_cfg
448 def override_custom_traffic(config, frame_sizes, unidir):
449 """Override the traffic profiles with a custom one."""
450 if frame_sizes is not None:
451 traffic_profile_name = "custom_traffic_profile"
452 config.traffic_profile = [
454 "l2frame_size": frame_sizes,
455 "name": traffic_profile_name
459 traffic_profile_name = config.traffic["profile"]
461 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
463 "bidirectional": bidirectional,
464 "profile": traffic_profile_name
468 def check_physnet(name, netattrs):
469 if not netattrs.physical_network:
470 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
472 if not netattrs.segmentation_id:
473 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
476 def status_cleanup(config, cleanup, force_cleanup):
477 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
478 # check if another run is pending
481 with utils.RunLock():
482 LOG.info('Status: idle')
484 LOG.info('Status: busy (run pending)')
486 # check nfvbench resources
487 if config.openrc_file and config.service_chain != ChainType.EXT:
488 cleaner = Cleaner(config)
489 count = cleaner.show_resources()
490 if count and (cleanup or force_cleanup):
491 cleaner.clean(not force_cleanup)
496 run_summary_required = False
499 # load default config file
500 config, default_cfg = load_default_config()
501 # create factory for platform specific classes
503 factory_module = importlib.import_module(config['factory_module'])
504 factory = getattr(factory_module, config['factory_class'])()
505 except AttributeError:
506 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
507 .format(m=config['factory_module'], c=config['factory_class']))
508 # create config plugin for this platform
509 config_plugin = factory.get_config_plugin_class()(config)
510 config = config_plugin.get_config()
512 opts, unknown_opts = parse_opts_from_cli()
513 log.set_level(debug=opts.debug)
516 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
520 with open(opts.summary) as json_data:
521 result = json.load(json_data)
523 result['config']['user_label'] = opts.user_label
524 print NFVBenchSummarizer(result, fluent_logger)
527 # show default config in text/yaml format
528 if opts.show_default_config:
534 # do not check extra_specs in flavor as it can contain any key/value pairs
535 whitelist_keys = ['extra_specs']
536 # override default config options with start config at path parsed from CLI
537 # check if it is an inline yaml/json config or a file name
538 if os.path.isfile(opts.config):
539 LOG.info('Loading configuration file: %s', opts.config)
540 config = config_load(opts.config, config, whitelist_keys)
541 config.name = os.path.basename(opts.config)
543 LOG.info('Loading configuration string: %s', opts.config)
544 config = config_loads(opts.config, config, whitelist_keys)
546 # setup the fluent logger as soon as possible right after the config plugin is called,
547 # if there is any logging or result tag is set then initialize the fluent logger
548 for fluentd in config.fluentd:
549 if fluentd.logging_tag or fluentd.result_tag:
550 fluent_logger = FluentLogHandler(config.fluentd)
551 LOG.addHandler(fluent_logger)
554 # traffic profile override options
555 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
557 # copy over cli options that are used in config
558 config.generator_profile = opts.generator_profile
562 config.log_file = opts.log_file
563 if opts.service_chain:
564 config.service_chain = opts.service_chain
565 if opts.service_chain_count:
566 config.service_chain_count = opts.service_chain_count
567 if opts.no_vswitch_access:
568 config.no_vswitch_access = opts.no_vswitch_access
569 if opts.no_int_config:
570 config.no_int_config = opts.no_int_config
572 if opts.use_sriov_middle_net:
573 if (not config.sriov) or (not config.service_chain == ChainType.PVVP):
574 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
575 config.use_sriov_middle_net = True
577 if config.sriov and config.service_chain != ChainType.EXT:
578 # if sriov is requested (does not apply to ext chains)
579 # make sure the physnet names are specified
580 check_physnet("left", config.internal_networks.left)
581 check_physnet("right", config.internal_networks.right)
582 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
583 check_physnet("middle", config.internal_networks.middle)
585 # show running config in json format
587 print json.dumps(config, sort_keys=True, indent=4)
590 # check that an empty openrc file (no OpenStack) is only allowed
592 if not config.openrc_file:
593 if config.service_chain == ChainType.EXT:
594 LOG.info('EXT chain with OpenStack mode disabled')
596 raise Exception("openrc_file is empty in the configuration and is required")
598 # update the config in the config plugin as it might have changed
599 # in a copy of the dict (config plugin still holds the original dict)
600 config_plugin.set_config(config)
602 if opts.status or opts.cleanup or opts.force_cleanup:
603 status_cleanup(config, opts.cleanup, opts.force_cleanup)
605 # add file log if requested
607 log.add_file_logger(config.log_file)
609 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
612 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
615 if os.path.isdir(opts.server):
616 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
617 nfvbench_instance.set_notifier(server)
619 port = int(opts.port)
621 server.run(host=opts.host)
623 server.run(host=opts.host, port=port)
625 print 'Invalid HTTP root directory: ' + opts.server
628 with utils.RunLock():
629 run_summary_required = True
631 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
633 raise Exception(err_msg)
635 # remove unfilled values
636 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
638 params = ' '.join(str(e) for e in sys.argv[1:])
639 result = nfvbench_instance.run(opts, params)
640 if 'error_message' in result:
641 raise Exception(result['error_message'])
643 if 'result' in result and result['status']:
644 nfvbench_instance.save(result['result'])
645 nfvbench_instance.prepare_summary(result['result'])
646 except Exception as exc:
647 run_summary_required = True
649 'status': NFVBench.STATUS_ERROR,
650 'error_message': traceback.format_exc()
655 # only send a summary record if there was an actual nfvbench run or
656 # if an error/exception was logged.
657 fluent_logger.send_run_summary(run_summary_required)
660 if __name__ == '__main__':