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 self.chain_runner = None
64 self.specs.set_openstack_spec(openstack_spec)
65 self.clients = defaultdict(lambda: None)
70 self.specs.set_run_spec(self.config_plugin.get_run_spec(self.specs.openstack))
71 self.chain_runner = ChainRunner(self.config,
78 def set_notifier(self, notifier):
79 self.notifier = notifier
81 def run(self, opts, args):
82 status = NFVBench.STATUS_OK
86 # take a snapshot of the current time for this new run
87 # so that all subsequent logs can relate to this run
88 fluent_logger.start_new_run()
91 self.update_config(opts)
95 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
96 "nfvbench_version": __version__,
98 "vswitch": self.specs.openstack.vswitch,
99 "encaps": self.specs.openstack.encaps
101 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
104 "service_chain": self.chain_runner.run(),
105 "versions": self.chain_runner.get_version(),
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)"""
135 if self.config.fluentd.result_tag:
136 sender = FluentLogHandler(self.config.fluentd.result_tag,
137 fluentd_ip=self.config.fluentd.ip,
138 fluentd_port=self.config.fluentd.port)
139 sender.runlogdate = fluent_logger.runlogdate
140 summary = NFVBenchSummarizer(result, sender)
141 LOG.info(str(summary))
143 def save(self, result):
144 """Save results in json format file."""
145 utils.save_json_result(result,
146 self.config.json_file,
147 self.config.std_json_path,
148 self.config.service_chain,
149 self.config.service_chain_count,
150 self.config.flow_count,
151 self.config.frame_sizes)
153 def update_config(self, opts):
154 self.config = AttrDict(dict(self.base_config))
155 self.config.update(opts)
157 self.config.service_chain = self.config.service_chain.upper()
158 self.config.service_chain_count = int(self.config.service_chain_count)
159 self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
160 required_flow_count = self.config.service_chain_count * 2
161 if self.config.flow_count < required_flow_count:
162 LOG.info("Flow count %d has been set to minimum value of '%d' "
163 "for current configuration", self.config.flow_count,
165 self.config.flow_count = required_flow_count
167 if self.config.flow_count % 2 != 0:
168 self.config.flow_count += 1
170 self.config.duration_sec = float(self.config.duration_sec)
171 self.config.interval_sec = float(self.config.interval_sec)
173 # Get traffic generator profile config
174 if not self.config.generator_profile:
175 self.config.generator_profile = self.config.traffic_generator.default_profile
177 generator_factory = TrafficGeneratorFactory(self.config)
178 self.config.generator_config = \
179 generator_factory.get_generator_config(self.config.generator_profile)
181 if not any(self.config.generator_config.pcis):
182 raise Exception("PCI addresses configuration for selected traffic generator profile "
183 "({tg_profile}) are missing. Please specify them in configuration file."
184 .format(tg_profile=self.config.generator_profile))
186 if self.config.traffic is None or not self.config.traffic:
187 raise Exception("No traffic profile found in traffic configuration, "
188 "please fill 'traffic' section in configuration file.")
190 if isinstance(self.config.traffic, tuple):
191 self.config.traffic = self.config.traffic[0]
193 self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
195 self.config.ipv6_mode = False
196 self.config.no_dhcp = True
197 self.config.same_network_only = True
198 if self.config.openrc_file:
199 self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
201 self.config.ndr_run = (not self.config.no_traffic
202 and 'ndr' in self.config.rate.strip().lower().split('_'))
203 self.config.pdr_run = (not self.config.no_traffic
204 and 'pdr' in self.config.rate.strip().lower().split('_'))
205 self.config.single_run = (not self.config.no_traffic
206 and not (self.config.ndr_run or self.config.pdr_run))
208 if self.config.vlans and len(self.config.vlans) != 2:
209 raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
211 self.config.json_file = self.config.json if self.config.json else None
212 if self.config.json_file:
213 (path, _filename) = os.path.split(self.config.json)
214 if not os.path.exists(path):
215 raise Exception('Please provide existing path for storing results in JSON file. '
216 'Path used: {path}'.format(path=path))
218 self.config.std_json_path = self.config.std_json if self.config.std_json else None
219 if self.config.std_json_path:
220 if not os.path.exists(self.config.std_json):
221 raise Exception('Please provide existing path for storing results in JSON file. '
222 'Path used: {path}'.format(path=self.config.std_json_path))
224 self.config_plugin.validate_config(self.config, self.specs.openstack)
227 def parse_opts_from_cli():
228 parser = argparse.ArgumentParser()
230 parser.add_argument('-c', '--config', dest='config',
232 help='Override default values with a config file or '
233 'a yaml/json config string',
234 metavar='<file_name_or_yaml>')
236 parser.add_argument('--server', dest='server',
239 metavar='<http_root_pathname>',
240 help='Run nfvbench in server mode and pass'
241 ' the HTTP root folder full pathname')
243 parser.add_argument('--host', dest='host',
246 help='Host IP address on which server will be listening (default 0.0.0.0)')
248 parser.add_argument('-p', '--port', dest='port',
251 help='Port on which server will be listening (default 7555)')
253 parser.add_argument('-sc', '--service-chain', dest='service_chain',
254 choices=BasicFactory.chain_classes,
256 help='Service chain to run')
258 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
260 help='Set number of service chains to run',
261 metavar='<service_chain_count>')
263 parser.add_argument('-fc', '--flow-count', dest='flow_count',
265 help='Set number of total flows for all chains and all directions',
266 metavar='<flow_count>')
268 parser.add_argument('--rate', dest='rate',
270 help='Specify rate in pps, bps or %% as total for all directions',
273 parser.add_argument('--duration', dest='duration_sec',
275 help='Set duration to run traffic generator (in seconds)',
276 metavar='<duration_sec>')
278 parser.add_argument('--interval', dest='interval_sec',
280 help='Set interval to record traffic generator stats (in seconds)',
281 metavar='<interval_sec>')
283 parser.add_argument('--inter-node', dest='inter_node',
286 help='run VMs in different compute nodes (PVVP only)')
288 parser.add_argument('--sriov', dest='sriov',
291 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
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()
447 openstack_spec = config_plugin.get_openstack_spec()
449 # setup the fluent logger as soon as possible right after the config plugin is called
450 if config.fluentd.logging_tag:
451 fluent_logger = FluentLogHandler(config.fluentd.logging_tag,
452 fluentd_ip=config.fluentd.ip,
453 fluentd_port=config.fluentd.port)
454 LOG.addHandler(fluent_logger)
458 opts, unknown_opts = parse_opts_from_cli()
459 log.set_level(debug=opts.debug)
462 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
466 with open(opts.summary) as json_data:
467 result = json.load(json_data)
469 result['config']['user_label'] = opts.user_label
470 if config.fluentd.result_tag:
471 sender = FluentLogHandler(config.fluentd.result_tag,
472 fluentd_ip=config.fluentd.ip,
473 fluentd_port=config.fluentd.port)
474 sender.runlogdate = fluent_logger.runlogdate
475 print NFVBenchSummarizer(result, sender)
477 print NFVBenchSummarizer(result, None)
480 # show default config in text/yaml format
481 if opts.show_default_config:
487 # do not check extra_specs in flavor as it can contain any key/value pairs
488 whitelist_keys = ['extra_specs']
489 # override default config options with start config at path parsed from CLI
490 # check if it is an inline yaml/json config or a file name
491 if os.path.isfile(opts.config):
492 LOG.info('Loading configuration file: ' + opts.config)
493 config = config_load(opts.config, config, whitelist_keys)
494 config.name = os.path.basename(opts.config)
496 LOG.info('Loading configuration string: ' + opts.config)
497 config = config_loads(opts.config, config, whitelist_keys)
499 # traffic profile override options
500 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
502 # copy over cli options that are used in config
503 config.generator_profile = opts.generator_profile
507 config.log_file = opts.log_file
509 # show running config in json format
511 print json.dumps(config, sort_keys=True, indent=4)
514 if config.sriov and config.service_chain != ChainType.EXT:
515 # if sriov is requested (does not apply to ext chains)
516 # make sure the physnet names are specified
517 check_physnet("left", config.internal_networks.left)
518 check_physnet("right", config.internal_networks.right)
519 if config.service_chain == ChainType.PVVP:
520 check_physnet("middle", config.internal_networks.middle)
522 # update the config in the config plugin as it might have changed
523 # in a copy of the dict (config plugin still holds the original dict)
524 config_plugin.set_config(config)
526 # add file log if requested
528 log.add_file_logger(config.log_file)
530 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
533 if os.path.isdir(opts.server):
534 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger,
535 config.fluentd.result_tag)
536 nfvbench_instance.set_notifier(server)
538 port = int(opts.port)
540 server.run(host=opts.host)
542 server.run(host=opts.host, port=port)
544 print 'Invalid HTTP root directory: ' + opts.server
547 with utils.RunLock():
548 run_summary_required = True
550 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
552 raise Exception(err_msg)
554 # remove unfilled values
555 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
557 params = ' '.join(str(e) for e in sys.argv[1:])
558 result = nfvbench_instance.run(opts, params)
559 if 'error_message' in result:
560 raise Exception(result['error_message'])
562 if 'result' in result and result['status']:
563 nfvbench_instance.save(result['result'])
564 nfvbench_instance.prepare_summary(result['result'])
565 except Exception as exc:
566 run_summary_required = True
568 'status': NFVBench.STATUS_ERROR,
569 'error_message': traceback.format_exc()
574 # only send a summary record if there was an actual nfvbench run or
575 # if an error/exception was logged.
576 fluent_logger.send_run_summary(run_summary_required)
579 if __name__ == '__main__':