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 config import config_load
34 from config import config_loads
35 import credentials as credentials
36 from factory import BasicFactory
37 from fluentd import FluentLogHandler
40 from nfvbenchd import WebSocketIoServer
41 from specs import ChainType
42 from specs import Specs
43 from summarizer import NFVBenchSummarizer
44 from traffic_client import TrafficGeneratorFactory
50 class NFVBench(object):
51 """Main class of NFV benchmarking tool."""
53 STATUS_ERROR = 'ERROR'
55 def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
56 self.base_config = config
58 self.config_plugin = config_plugin
59 self.factory = factory
60 self.notifier = notifier
61 self.cred = credentials.Credentials(config.openrc_file, None, False) \
62 if config.openrc_file else None
63 self.chain_runner = None
65 self.specs.set_openstack_spec(openstack_spec)
66 self.clients = defaultdict(lambda: None)
71 self.specs.set_run_spec(self.config_plugin.get_run_spec(self.specs.openstack))
72 self.chain_runner = ChainRunner(self.config,
79 def set_notifier(self, notifier):
80 self.notifier = notifier
82 def run(self, opts, args):
83 status = NFVBench.STATUS_OK
87 # take a snapshot of the current time for this new run
88 # so that all subsequent logs can relate to this run
89 fluent_logger.start_new_run()
92 self.update_config(opts)
96 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
97 "nfvbench_version": __version__,
98 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
101 "service_chain": self.chain_runner.run(),
102 "versions": self.chain_runner.get_version(),
106 if self.specs.openstack:
107 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
108 "encaps": self.specs.openstack.encaps}
109 result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
111 status = NFVBench.STATUS_ERROR
112 message = traceback.format_exc()
113 except KeyboardInterrupt:
114 status = NFVBench.STATUS_ERROR
115 message = traceback.format_exc()
117 if self.chain_runner:
118 self.chain_runner.close()
120 if status == NFVBench.STATUS_OK:
121 result = utils.dict_to_json_dict(result)
128 'error_message': message
131 def prepare_summary(self, result):
132 """Prepares summary of the result to print and send it to logger (eg: fluentd)"""
134 summary = NFVBenchSummarizer(result, fluent_logger)
135 LOG.info(str(summary))
137 def save(self, result):
138 """Save results in json format file."""
139 utils.save_json_result(result,
140 self.config.json_file,
141 self.config.std_json_path,
142 self.config.service_chain,
143 self.config.service_chain_count,
144 self.config.flow_count,
145 self.config.frame_sizes)
147 def update_config(self, opts):
148 self.config = AttrDict(dict(self.base_config))
149 self.config.update(opts)
151 self.config.service_chain = self.config.service_chain.upper()
152 self.config.service_chain_count = int(self.config.service_chain_count)
153 self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
154 required_flow_count = self.config.service_chain_count * 2
155 if self.config.flow_count < required_flow_count:
156 LOG.info("Flow count %d has been set to minimum value of '%d' "
157 "for current configuration", self.config.flow_count,
159 self.config.flow_count = required_flow_count
161 if self.config.flow_count % 2 != 0:
162 self.config.flow_count += 1
164 self.config.duration_sec = float(self.config.duration_sec)
165 self.config.interval_sec = float(self.config.interval_sec)
167 # Check length of mac_addrs_left/right for serivce_chain EXT with no_arp
168 if self.config.service_chain == ChainType.EXT and self.config.no_arp:
169 if not (self.config.generator_config.mac_addrs_left is None and
170 self.config.generator_config.mac_addrs_right is None):
171 if (self.config.generator_config.mac_addrs_left is None or
172 self.config.generator_config.mac_addrs_right is None):
173 raise Exception("mac_addrs_left and mac_addrs_right must either "
174 "both be None or have a number of entries matching "
175 "service_chain_count")
176 if not (len(self.config.generator_config.mac_addrs_left) ==
177 self.config.service_chain_count and
178 len(self.config.generator_config.mac_addrs_right) ==
179 self.config.service_chain_count):
180 raise Exception("length of mac_addrs_left ({a}) and/or mac_addrs_right ({b}) "
181 "does not match service_chain_count ({c})"
182 .format(a=len(self.config.generator_config.mac_addrs_left),
183 b=len(self.config.generator_config.mac_addrs_right),
184 c=self.config.service_chain_count))
186 # Get traffic generator profile config
187 if not self.config.generator_profile:
188 self.config.generator_profile = self.config.traffic_generator.default_profile
190 generator_factory = TrafficGeneratorFactory(self.config)
191 self.config.generator_config = \
192 generator_factory.get_generator_config(self.config.generator_profile)
194 if not any(self.config.generator_config.pcis):
195 raise Exception("PCI addresses configuration for selected traffic generator profile "
196 "({tg_profile}) are missing. Please specify them in configuration file."
197 .format(tg_profile=self.config.generator_profile))
199 if self.config.traffic is None or not self.config.traffic:
200 raise Exception("No traffic profile found in traffic configuration, "
201 "please fill 'traffic' section in configuration file.")
203 if isinstance(self.config.traffic, tuple):
204 self.config.traffic = self.config.traffic[0]
206 self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
208 self.config.ipv6_mode = False
209 self.config.no_dhcp = True
210 self.config.same_network_only = True
211 if self.config.openrc_file:
212 self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
214 self.config.ndr_run = (not self.config.no_traffic
215 and 'ndr' in self.config.rate.strip().lower().split('_'))
216 self.config.pdr_run = (not self.config.no_traffic
217 and 'pdr' in self.config.rate.strip().lower().split('_'))
218 self.config.single_run = (not self.config.no_traffic
219 and not (self.config.ndr_run or self.config.pdr_run))
221 if self.config.vlans and len(self.config.vlans) != 2:
222 raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
224 self.config.json_file = self.config.json if self.config.json else None
225 if self.config.json_file:
226 (path, _filename) = os.path.split(self.config.json)
227 if not os.path.exists(path):
228 raise Exception('Please provide existing path for storing results in JSON file. '
229 'Path used: {path}'.format(path=path))
231 self.config.std_json_path = self.config.std_json if self.config.std_json else None
232 if self.config.std_json_path:
233 if not os.path.exists(self.config.std_json):
234 raise Exception('Please provide existing path for storing results in JSON file. '
235 'Path used: {path}'.format(path=self.config.std_json_path))
237 self.config_plugin.validate_config(self.config, self.specs.openstack)
240 def parse_opts_from_cli():
241 parser = argparse.ArgumentParser()
243 parser.add_argument('-c', '--config', dest='config',
245 help='Override default values with a config file or '
246 'a yaml/json config string',
247 metavar='<file_name_or_yaml>')
249 parser.add_argument('--server', dest='server',
252 metavar='<http_root_pathname>',
253 help='Run nfvbench in server mode and pass'
254 ' the HTTP root folder full pathname')
256 parser.add_argument('--host', dest='host',
259 help='Host IP address on which server will be listening (default 0.0.0.0)')
261 parser.add_argument('-p', '--port', dest='port',
264 help='Port on which server will be listening (default 7555)')
266 parser.add_argument('-sc', '--service-chain', dest='service_chain',
267 choices=BasicFactory.chain_classes,
269 help='Service chain to run')
271 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
273 help='Set number of service chains to run',
274 metavar='<service_chain_count>')
276 parser.add_argument('-fc', '--flow-count', dest='flow_count',
278 help='Set number of total flows for all chains and all directions',
279 metavar='<flow_count>')
281 parser.add_argument('--rate', dest='rate',
283 help='Specify rate in pps, bps or %% as total for all directions',
286 parser.add_argument('--duration', dest='duration_sec',
288 help='Set duration to run traffic generator (in seconds)',
289 metavar='<duration_sec>')
291 parser.add_argument('--interval', dest='interval_sec',
293 help='Set interval to record traffic generator stats (in seconds)',
294 metavar='<interval_sec>')
296 parser.add_argument('--inter-node', dest='inter_node',
299 help='run VMs in different compute nodes (PVVP only)')
301 parser.add_argument('--sriov', dest='sriov',
304 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
306 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
309 help='Use SRIOV to handle the middle network traffic '
310 '(PVVP with SRIOV only)')
312 parser.add_argument('-d', '--debug', dest='debug',
315 help='print debug messages (verbose)')
317 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
319 help='Traffic generator profile to use')
321 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
324 help='Check config and connectivity only - do not generate traffic')
326 parser.add_argument('--no-arp', dest='no_arp',
329 help='Do not use ARP to find MAC addresses, '
330 'instead use values in config file')
332 parser.add_argument('--no-reset', dest='no_reset',
335 help='Do not reset counters prior to running')
337 parser.add_argument('--no-int-config', dest='no_int_config',
340 help='Skip interfaces config on EXT service chain')
342 parser.add_argument('--no-tor-access', dest='no_tor_access',
345 help='Skip TOR switch configuration and retrieving of stats')
347 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
350 help='Skip vswitch configuration and retrieving of stats')
352 parser.add_argument('--no-cleanup', dest='no_cleanup',
355 help='no cleanup after run')
357 parser.add_argument('--json', dest='json',
359 help='store results in json format file',
360 metavar='<path>/<filename>')
362 parser.add_argument('--std-json', dest='std_json',
364 help='store results in json format file with nfvbench standard filename: '
365 '<service-chain-type>-<service-chain-count>-<flow-count>'
366 '-<packet-sizes>.json',
369 parser.add_argument('--show-default-config', dest='show_default_config',
372 help='print the default config in yaml format (unedited)')
374 parser.add_argument('--show-config', dest='show_config',
377 help='print the running config in json format')
379 parser.add_argument('-ss', '--show-summary', dest='summary',
381 help='Show summary from nfvbench json file',
384 parser.add_argument('-v', '--version', dest='version',
389 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
391 help='Override traffic profile frame sizes',
392 metavar='<frame_size_bytes or IMIX>')
394 parser.add_argument('--unidir', dest='unidir',
397 help='Override traffic profile direction (requires -fs)')
399 parser.add_argument('--log-file', '--logfile', dest='log_file',
401 help='Filename for saving logs',
402 metavar='<log_file>')
404 parser.add_argument('--user-label', '--userlabel', dest='user_label',
406 help='Custom label for performance records')
408 opts, unknown_opts = parser.parse_known_args()
409 return opts, unknown_opts
412 def load_default_config():
413 default_cfg = resource_string(__name__, "cfg.default.yaml")
414 config = config_loads(default_cfg)
415 config.name = '(built-in default config)'
416 return config, default_cfg
419 def override_custom_traffic(config, frame_sizes, unidir):
420 """Override the traffic profiles with a custom one
422 if frame_sizes is not None:
423 traffic_profile_name = "custom_traffic_profile"
424 config.traffic_profile = [
426 "l2frame_size": frame_sizes,
427 "name": traffic_profile_name
431 traffic_profile_name = config.traffic["profile"]
433 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
435 "bidirectional": bidirectional,
436 "profile": traffic_profile_name
440 def check_physnet(name, netattrs):
441 if not netattrs.physical_network:
442 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
444 if not netattrs.segmentation_id:
445 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
451 run_summary_required = False
454 # load default config file
455 config, default_cfg = load_default_config()
456 # create factory for platform specific classes
458 factory_module = importlib.import_module(config['factory_module'])
459 factory = getattr(factory_module, config['factory_class'])()
460 except AttributeError:
461 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
462 .format(m=config['factory_module'], c=config['factory_class']))
463 # create config plugin for this platform
464 config_plugin = factory.get_config_plugin_class()(config)
465 config = config_plugin.get_config()
467 opts, unknown_opts = parse_opts_from_cli()
468 log.set_level(debug=opts.debug)
470 # setup the fluent logger as soon as possible right after the config plugin is called,
471 # if there is any logging or result tag is set then initialize the fluent logger
472 for fluentd in config.fluentd:
473 if fluentd.logging_tag or fluentd.result_tag:
474 fluent_logger = FluentLogHandler(config.fluentd)
475 LOG.addHandler(fluent_logger)
479 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
483 with open(opts.summary) as json_data:
484 result = json.load(json_data)
486 result['config']['user_label'] = opts.user_label
487 print NFVBenchSummarizer(result, fluent_logger)
490 # show default config in text/yaml format
491 if opts.show_default_config:
497 # do not check extra_specs in flavor as it can contain any key/value pairs
498 whitelist_keys = ['extra_specs']
499 # override default config options with start config at path parsed from CLI
500 # check if it is an inline yaml/json config or a file name
501 if os.path.isfile(opts.config):
502 LOG.info('Loading configuration file: %s', opts.config)
503 config = config_load(opts.config, config, whitelist_keys)
504 config.name = os.path.basename(opts.config)
506 LOG.info('Loading configuration string: %s', opts.config)
507 config = config_loads(opts.config, config, whitelist_keys)
509 # traffic profile override options
510 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
512 # copy over cli options that are used in config
513 config.generator_profile = opts.generator_profile
517 config.log_file = opts.log_file
518 if opts.service_chain:
519 config.service_chain = opts.service_chain
520 if opts.service_chain_count:
521 config.service_chain_count = opts.service_chain_count
522 if opts.no_vswitch_access:
523 config.no_vswitch_access = opts.no_vswitch_access
524 if opts.no_int_config:
525 config.no_int_config = opts.no_int_config
527 if opts.use_sriov_middle_net:
528 if (not config.sriov) or (not config.service_chain == ChainType.PVVP):
529 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
530 config.use_sriov_middle_net = True
532 if config.sriov and config.service_chain != ChainType.EXT:
533 # if sriov is requested (does not apply to ext chains)
534 # make sure the physnet names are specified
535 check_physnet("left", config.internal_networks.left)
536 check_physnet("right", config.internal_networks.right)
537 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
538 check_physnet("middle", config.internal_networks.middle)
540 # show running config in json format
542 print json.dumps(config, sort_keys=True, indent=4)
545 # check that an empty openrc file (no OpenStack) is only allowed
547 if not config.openrc_file:
548 if config.service_chain == ChainType.EXT:
549 LOG.info('EXT chain with OpenStack mode disabled')
551 raise Exception("openrc_file is empty in the configuration and is required")
553 # update the config in the config plugin as it might have changed
554 # in a copy of the dict (config plugin still holds the original dict)
555 config_plugin.set_config(config)
557 # add file log if requested
559 log.add_file_logger(config.log_file)
561 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
564 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
567 if os.path.isdir(opts.server):
568 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
569 nfvbench_instance.set_notifier(server)
571 port = int(opts.port)
573 server.run(host=opts.host)
575 server.run(host=opts.host, port=port)
577 print 'Invalid HTTP root directory: ' + opts.server
580 with utils.RunLock():
581 run_summary_required = True
583 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
585 raise Exception(err_msg)
587 # remove unfilled values
588 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
590 params = ' '.join(str(e) for e in sys.argv[1:])
591 result = nfvbench_instance.run(opts, params)
592 if 'error_message' in result:
593 raise Exception(result['error_message'])
595 if 'result' in result and result['status']:
596 nfvbench_instance.save(result['result'])
597 nfvbench_instance.prepare_summary(result['result'])
598 except Exception as exc:
599 run_summary_required = True
601 'status': NFVBench.STATUS_ERROR,
602 'error_message': traceback.format_exc()
607 # only send a summary record if there was an actual nfvbench run or
608 # if an error/exception was logged.
609 fluent_logger.send_run_summary(run_summary_required)
612 if __name__ == '__main__':