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 # Get traffic generator profile config
168 if not self.config.generator_profile:
169 self.config.generator_profile = self.config.traffic_generator.default_profile
171 generator_factory = TrafficGeneratorFactory(self.config)
172 self.config.generator_config = \
173 generator_factory.get_generator_config(self.config.generator_profile)
175 if not any(self.config.generator_config.pcis):
176 raise Exception("PCI addresses configuration for selected traffic generator profile "
177 "({tg_profile}) are missing. Please specify them in configuration file."
178 .format(tg_profile=self.config.generator_profile))
180 if self.config.traffic is None or not self.config.traffic:
181 raise Exception("No traffic profile found in traffic configuration, "
182 "please fill 'traffic' section in configuration file.")
184 if isinstance(self.config.traffic, tuple):
185 self.config.traffic = self.config.traffic[0]
187 self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
189 self.config.ipv6_mode = False
190 self.config.no_dhcp = True
191 self.config.same_network_only = True
192 if self.config.openrc_file:
193 self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
195 self.config.ndr_run = (not self.config.no_traffic
196 and 'ndr' in self.config.rate.strip().lower().split('_'))
197 self.config.pdr_run = (not self.config.no_traffic
198 and 'pdr' in self.config.rate.strip().lower().split('_'))
199 self.config.single_run = (not self.config.no_traffic
200 and not (self.config.ndr_run or self.config.pdr_run))
202 if self.config.vlans and len(self.config.vlans) != 2:
203 raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
205 self.config.json_file = self.config.json if self.config.json else None
206 if self.config.json_file:
207 (path, _filename) = os.path.split(self.config.json)
208 if not os.path.exists(path):
209 raise Exception('Please provide existing path for storing results in JSON file. '
210 'Path used: {path}'.format(path=path))
212 self.config.std_json_path = self.config.std_json if self.config.std_json else None
213 if self.config.std_json_path:
214 if not os.path.exists(self.config.std_json):
215 raise Exception('Please provide existing path for storing results in JSON file. '
216 'Path used: {path}'.format(path=self.config.std_json_path))
218 self.config_plugin.validate_config(self.config, self.specs.openstack)
221 def parse_opts_from_cli():
222 parser = argparse.ArgumentParser()
224 parser.add_argument('-c', '--config', dest='config',
226 help='Override default values with a config file or '
227 'a yaml/json config string',
228 metavar='<file_name_or_yaml>')
230 parser.add_argument('--server', dest='server',
233 metavar='<http_root_pathname>',
234 help='Run nfvbench in server mode and pass'
235 ' the HTTP root folder full pathname')
237 parser.add_argument('--host', dest='host',
240 help='Host IP address on which server will be listening (default 0.0.0.0)')
242 parser.add_argument('-p', '--port', dest='port',
245 help='Port on which server will be listening (default 7555)')
247 parser.add_argument('-sc', '--service-chain', dest='service_chain',
248 choices=BasicFactory.chain_classes,
250 help='Service chain to run')
252 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
254 help='Set number of service chains to run',
255 metavar='<service_chain_count>')
257 parser.add_argument('-fc', '--flow-count', dest='flow_count',
259 help='Set number of total flows for all chains and all directions',
260 metavar='<flow_count>')
262 parser.add_argument('--rate', dest='rate',
264 help='Specify rate in pps, bps or %% as total for all directions',
267 parser.add_argument('--duration', dest='duration_sec',
269 help='Set duration to run traffic generator (in seconds)',
270 metavar='<duration_sec>')
272 parser.add_argument('--interval', dest='interval_sec',
274 help='Set interval to record traffic generator stats (in seconds)',
275 metavar='<interval_sec>')
277 parser.add_argument('--inter-node', dest='inter_node',
280 help='run VMs in different compute nodes (PVVP only)')
282 parser.add_argument('--sriov', dest='sriov',
285 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
287 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
290 help='Use SRIOV to handle the middle network traffic '
291 '(PVVP with SRIOV only)')
293 parser.add_argument('-d', '--debug', dest='debug',
296 help='print debug messages (verbose)')
298 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
300 help='Traffic generator profile to use')
302 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
305 help='Check config and connectivity only - do not generate traffic')
307 parser.add_argument('--no-arp', dest='no_arp',
310 help='Do not use ARP to find MAC addresses, '
311 'instead use values in config file')
313 parser.add_argument('--no-reset', dest='no_reset',
316 help='Do not reset counters prior to running')
318 parser.add_argument('--no-int-config', dest='no_int_config',
321 help='Skip interfaces config on EXT service chain')
323 parser.add_argument('--no-tor-access', dest='no_tor_access',
326 help='Skip TOR switch configuration and retrieving of stats')
328 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
331 help='Skip vswitch configuration and retrieving of stats')
333 parser.add_argument('--no-cleanup', dest='no_cleanup',
336 help='no cleanup after run')
338 parser.add_argument('--json', dest='json',
340 help='store results in json format file',
341 metavar='<path>/<filename>')
343 parser.add_argument('--std-json', dest='std_json',
345 help='store results in json format file with nfvbench standard filename: '
346 '<service-chain-type>-<service-chain-count>-<flow-count>'
347 '-<packet-sizes>.json',
350 parser.add_argument('--show-default-config', dest='show_default_config',
353 help='print the default config in yaml format (unedited)')
355 parser.add_argument('--show-config', dest='show_config',
358 help='print the running config in json format')
360 parser.add_argument('-ss', '--show-summary', dest='summary',
362 help='Show summary from nfvbench json file',
365 parser.add_argument('-v', '--version', dest='version',
370 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
372 help='Override traffic profile frame sizes',
373 metavar='<frame_size_bytes or IMIX>')
375 parser.add_argument('--unidir', dest='unidir',
378 help='Override traffic profile direction (requires -fs)')
380 parser.add_argument('--log-file', '--logfile', dest='log_file',
382 help='Filename for saving logs',
383 metavar='<log_file>')
385 parser.add_argument('--user-label', '--userlabel', dest='user_label',
387 help='Custom label for performance records')
389 opts, unknown_opts = parser.parse_known_args()
390 return opts, unknown_opts
393 def load_default_config():
394 default_cfg = resource_string(__name__, "cfg.default.yaml")
395 config = config_loads(default_cfg)
396 config.name = '(built-in default config)'
397 return config, default_cfg
400 def override_custom_traffic(config, frame_sizes, unidir):
401 """Override the traffic profiles with a custom one
403 if frame_sizes is not None:
404 traffic_profile_name = "custom_traffic_profile"
405 config.traffic_profile = [
407 "l2frame_size": frame_sizes,
408 "name": traffic_profile_name
412 traffic_profile_name = config.traffic["profile"]
414 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
416 "bidirectional": bidirectional,
417 "profile": traffic_profile_name
421 def check_physnet(name, netattrs):
422 if not netattrs.physical_network:
423 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
425 if not netattrs.segmentation_id:
426 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
432 run_summary_required = False
435 # load default config file
436 config, default_cfg = load_default_config()
437 # create factory for platform specific classes
439 factory_module = importlib.import_module(config['factory_module'])
440 factory = getattr(factory_module, config['factory_class'])()
441 except AttributeError:
442 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
443 .format(m=config['factory_module'], c=config['factory_class']))
444 # create config plugin for this platform
445 config_plugin = factory.get_config_plugin_class()(config)
446 config = config_plugin.get_config()
448 opts, unknown_opts = parse_opts_from_cli()
449 log.set_level(debug=opts.debug)
451 # setup the fluent logger as soon as possible right after the config plugin is called,
452 # if there is any logging or result tag is set then initialize the fluent logger
453 for fluentd in config.fluentd:
454 if fluentd.logging_tag or fluentd.result_tag:
455 fluent_logger = FluentLogHandler(config.fluentd)
456 LOG.addHandler(fluent_logger)
460 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
464 with open(opts.summary) as json_data:
465 result = json.load(json_data)
467 result['config']['user_label'] = opts.user_label
468 print NFVBenchSummarizer(result, fluent_logger)
471 # show default config in text/yaml format
472 if opts.show_default_config:
478 # do not check extra_specs in flavor as it can contain any key/value pairs
479 whitelist_keys = ['extra_specs']
480 # override default config options with start config at path parsed from CLI
481 # check if it is an inline yaml/json config or a file name
482 if os.path.isfile(opts.config):
483 LOG.info('Loading configuration file: %s', opts.config)
484 config = config_load(opts.config, config, whitelist_keys)
485 config.name = os.path.basename(opts.config)
487 LOG.info('Loading configuration string: %s', opts.config)
488 config = config_loads(opts.config, config, whitelist_keys)
490 # traffic profile override options
491 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
493 # copy over cli options that are used in config
494 config.generator_profile = opts.generator_profile
498 config.log_file = opts.log_file
499 if opts.service_chain:
500 config.service_chain = opts.service_chain
501 if opts.service_chain_count:
502 config.service_chain_count = opts.service_chain_count
504 if opts.use_sriov_middle_net:
505 if (not config.sriov) or (not config.service_chain == ChainType.PVVP):
506 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
507 config.use_sriov_middle_net = True
509 if config.sriov and config.service_chain != ChainType.EXT:
510 # if sriov is requested (does not apply to ext chains)
511 # make sure the physnet names are specified
512 check_physnet("left", config.internal_networks.left)
513 check_physnet("right", config.internal_networks.right)
514 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
515 check_physnet("middle", config.internal_networks.middle)
517 # show running config in json format
519 print json.dumps(config, sort_keys=True, indent=4)
522 # check that an empty openrc file (no OpenStack) is only allowed
524 if not config.openrc_file:
525 if config.service_chain == ChainType.EXT:
526 LOG.info('EXT chain with OpenStack mode disabled')
528 raise Exception("openrc_file is empty in the configuration and is required")
530 # update the config in the config plugin as it might have changed
531 # in a copy of the dict (config plugin still holds the original dict)
532 config_plugin.set_config(config)
534 # add file log if requested
536 log.add_file_logger(config.log_file)
538 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
541 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
544 if os.path.isdir(opts.server):
545 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
546 nfvbench_instance.set_notifier(server)
548 port = int(opts.port)
550 server.run(host=opts.host)
552 server.run(host=opts.host, port=port)
554 print 'Invalid HTTP root directory: ' + opts.server
557 with utils.RunLock():
558 run_summary_required = True
560 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
562 raise Exception(err_msg)
564 # remove unfilled values
565 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
567 params = ' '.join(str(e) for e in sys.argv[1:])
568 result = nfvbench_instance.run(opts, params)
569 if 'error_message' in result:
570 raise Exception(result['error_message'])
572 if 'result' in result and result['status']:
573 nfvbench_instance.save(result['result'])
574 nfvbench_instance.prepare_summary(result['result'])
575 except Exception as exc:
576 run_summary_required = True
578 'status': NFVBench.STATUS_ERROR,
579 'error_message': traceback.format_exc()
584 # only send a summary record if there was an actual nfvbench run or
585 # if an error/exception was logged.
586 fluent_logger.send_run_summary(run_summary_required)
589 if __name__ == '__main__':