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
51 class NFVBench(object):
52 """Main class of NFV benchmarking tool."""
54 STATUS_ERROR = 'ERROR'
56 def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
57 self.base_config = config
59 self.config_plugin = config_plugin
60 self.factory = factory
61 self.notifier = notifier
62 self.cred = credentials.Credentials(config.openrc_file, None, False)
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__,
99 "vswitch": self.specs.openstack.vswitch,
100 "encaps": self.specs.openstack.encaps
102 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
105 "service_chain": self.chain_runner.run(),
106 "versions": self.chain_runner.get_version(),
110 result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
112 status = NFVBench.STATUS_ERROR
113 message = traceback.format_exc()
114 except KeyboardInterrupt:
115 status = NFVBench.STATUS_ERROR
116 message = traceback.format_exc()
118 if self.chain_runner:
119 self.chain_runner.close()
121 if status == NFVBench.STATUS_OK:
122 result = utils.dict_to_json_dict(result)
129 'error_message': message
132 def prepare_summary(self, result):
133 """Prepares summary of the result to print and send it to logger (eg: fluentd)"""
136 if self.config.fluentd.result_tag:
137 sender = FluentLogHandler(self.config.fluentd.result_tag,
138 fluentd_ip=self.config.fluentd.ip,
139 fluentd_port=self.config.fluentd.port)
140 sender.runlogdate = fluent_logger.runlogdate
141 summary = NFVBenchSummarizer(result, sender)
142 LOG.info(str(summary))
144 def save(self, result):
145 """Save results in json format file."""
146 utils.save_json_result(result,
147 self.config.json_file,
148 self.config.std_json_path,
149 self.config.service_chain,
150 self.config.service_chain_count,
151 self.config.flow_count,
152 self.config.frame_sizes)
154 def update_config(self, opts):
155 self.config = AttrDict(dict(self.base_config))
156 self.config.update(opts)
158 self.config.service_chain = self.config.service_chain.upper()
159 self.config.service_chain_count = int(self.config.service_chain_count)
160 self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
161 required_flow_count = self.config.service_chain_count * 2
162 if self.config.flow_count < required_flow_count:
163 LOG.info("Flow count %d has been set to minimum value of '%d' "
164 "for current configuration", self.config.flow_count,
166 self.config.flow_count = required_flow_count
168 if self.config.flow_count % 2 != 0:
169 self.config.flow_count += 1
171 self.config.duration_sec = float(self.config.duration_sec)
172 self.config.interval_sec = float(self.config.interval_sec)
174 # Get traffic generator profile config
175 if not self.config.generator_profile:
176 self.config.generator_profile = self.config.traffic_generator.default_profile
178 generator_factory = TrafficGeneratorFactory(self.config)
179 self.config.generator_config = \
180 generator_factory.get_generator_config(self.config.generator_profile)
182 if not any(self.config.generator_config.pcis):
183 raise Exception("PCI addresses configuration for selected traffic generator profile "
184 "({tg_profile}) are missing. Please specify them in configuration file."
185 .format(tg_profile=self.config.generator_profile))
187 if self.config.traffic is None or not self.config.traffic:
188 raise Exception("No traffic profile found in traffic configuration, "
189 "please fill 'traffic' section in configuration file.")
191 if isinstance(self.config.traffic, tuple):
192 self.config.traffic = self.config.traffic[0]
194 self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
196 self.config.ipv6_mode = False
197 self.config.no_dhcp = True
198 self.config.same_network_only = True
199 if self.config.openrc_file:
200 self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
202 self.config.ndr_run = (not self.config.no_traffic
203 and 'ndr' in self.config.rate.strip().lower().split('_'))
204 self.config.pdr_run = (not self.config.no_traffic
205 and 'pdr' in self.config.rate.strip().lower().split('_'))
206 self.config.single_run = (not self.config.no_traffic
207 and not (self.config.ndr_run or self.config.pdr_run))
209 if self.config.vlans and len(self.config.vlans) != 2:
210 raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
212 self.config.json_file = self.config.json if self.config.json else None
213 if self.config.json_file:
214 (path, _filename) = os.path.split(self.config.json)
215 if not os.path.exists(path):
216 raise Exception('Please provide existing path for storing results in JSON file. '
217 'Path used: {path}'.format(path=path))
219 self.config.std_json_path = self.config.std_json if self.config.std_json else None
220 if self.config.std_json_path:
221 if not os.path.exists(self.config.std_json):
222 raise Exception('Please provide existing path for storing results in JSON file. '
223 'Path used: {path}'.format(path=self.config.std_json_path))
225 self.config_plugin.validate_config(self.config, self.specs.openstack)
228 def parse_opts_from_cli():
229 parser = argparse.ArgumentParser()
231 parser.add_argument('-c', '--config', dest='config',
233 help='Override default values with a config file or '
234 'a yaml/json config string',
235 metavar='<file_name_or_yaml>')
237 parser.add_argument('--server', dest='server',
240 metavar='<http_root_pathname>',
241 help='Run nfvbench in server mode and pass'
242 ' the HTTP root folder full pathname')
244 parser.add_argument('--host', dest='host',
247 help='Host IP address on which server will be listening (default 0.0.0.0)')
249 parser.add_argument('-p', '--port', dest='port',
252 help='Port on which server will be listening (default 7555)')
254 parser.add_argument('-sc', '--service-chain', dest='service_chain',
255 choices=BasicFactory.chain_classes,
257 help='Service chain to run')
259 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
261 help='Set number of service chains to run',
262 metavar='<service_chain_count>')
264 parser.add_argument('-fc', '--flow-count', dest='flow_count',
266 help='Set number of total flows for all chains and all directions',
267 metavar='<flow_count>')
269 parser.add_argument('--rate', dest='rate',
271 help='Specify rate in pps, bps or %% as total for all directions',
274 parser.add_argument('--duration', dest='duration_sec',
276 help='Set duration to run traffic generator (in seconds)',
277 metavar='<duration_sec>')
279 parser.add_argument('--interval', dest='interval_sec',
281 help='Set interval to record traffic generator stats (in seconds)',
282 metavar='<interval_sec>')
284 parser.add_argument('--inter-node', dest='inter_node',
287 help='run VMs in different compute nodes (PVVP only)')
289 parser.add_argument('--sriov', dest='sriov',
292 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
294 parser.add_argument('-d', '--debug', dest='debug',
297 help='print debug messages (verbose)')
299 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
301 help='Traffic generator profile to use')
303 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
306 help='Check config and connectivity only - do not generate traffic')
308 parser.add_argument('--no-arp', dest='no_arp',
311 help='Do not use ARP to find MAC addresses, '
312 'instead use values in config file')
314 parser.add_argument('--no-reset', dest='no_reset',
317 help='Do not reset counters prior to running')
319 parser.add_argument('--no-int-config', dest='no_int_config',
322 help='Skip interfaces config on EXT service chain')
324 parser.add_argument('--no-tor-access', dest='no_tor_access',
327 help='Skip TOR switch configuration and retrieving of stats')
329 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
332 help='Skip vswitch configuration and retrieving of stats')
334 parser.add_argument('--no-cleanup', dest='no_cleanup',
337 help='no cleanup after run')
339 parser.add_argument('--json', dest='json',
341 help='store results in json format file',
342 metavar='<path>/<filename>')
344 parser.add_argument('--std-json', dest='std_json',
346 help='store results in json format file with nfvbench standard filename: '
347 '<service-chain-type>-<service-chain-count>-<flow-count>'
348 '-<packet-sizes>.json',
351 parser.add_argument('--show-default-config', dest='show_default_config',
354 help='print the default config in yaml format (unedited)')
356 parser.add_argument('--show-config', dest='show_config',
359 help='print the running config in json format')
361 parser.add_argument('-ss', '--show-summary', dest='summary',
363 help='Show summary from nfvbench json file',
366 parser.add_argument('-v', '--version', dest='version',
371 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
373 help='Override traffic profile frame sizes',
374 metavar='<frame_size_bytes or IMIX>')
376 parser.add_argument('--unidir', dest='unidir',
379 help='Override traffic profile direction (requires -fs)')
381 parser.add_argument('--log-file', '--logfile', dest='log_file',
383 help='Filename for saving logs',
384 metavar='<log_file>')
386 parser.add_argument('--user-label', '--userlabel', dest='user_label',
388 help='Custom label for performance records')
390 opts, unknown_opts = parser.parse_known_args()
391 return opts, unknown_opts
394 def load_default_config():
395 default_cfg = resource_string(__name__, "cfg.default.yaml")
396 config = config_loads(default_cfg)
397 config.name = '(built-in default config)'
398 return config, default_cfg
401 def override_custom_traffic(config, frame_sizes, unidir):
402 """Override the traffic profiles with a custom one
404 if frame_sizes is not None:
405 traffic_profile_name = "custom_traffic_profile"
406 config.traffic_profile = [
408 "l2frame_size": frame_sizes,
409 "name": traffic_profile_name
413 traffic_profile_name = config.traffic["profile"]
415 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
417 "bidirectional": bidirectional,
418 "profile": traffic_profile_name
422 def check_physnet(name, netattrs):
423 if not netattrs.physical_network:
424 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
426 if not netattrs.segmentation_id:
427 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
433 run_summary_required = False
436 # load default config file
437 config, default_cfg = load_default_config()
438 # create factory for platform specific classes
440 factory_module = importlib.import_module(config['factory_module'])
441 factory = getattr(factory_module, config['factory_class'])()
442 except AttributeError:
443 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
444 .format(m=config['factory_module'], c=config['factory_class']))
445 # create config plugin for this platform
446 config_plugin = factory.get_config_plugin_class()(config)
447 config = config_plugin.get_config()
448 openstack_spec = config_plugin.get_openstack_spec()
450 # setup the fluent logger as soon as possible right after the config plugin is called
451 if config.fluentd.logging_tag:
452 fluent_logger = FluentLogHandler(config.fluentd.logging_tag,
453 fluentd_ip=config.fluentd.ip,
454 fluentd_port=config.fluentd.port)
455 LOG.addHandler(fluent_logger)
459 opts, unknown_opts = parse_opts_from_cli()
460 log.set_level(debug=opts.debug)
463 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
467 with open(opts.summary) as json_data:
468 result = json.load(json_data)
470 result['config']['user_label'] = opts.user_label
471 if config.fluentd.result_tag:
472 sender = FluentLogHandler(config.fluentd.result_tag,
473 fluentd_ip=config.fluentd.ip,
474 fluentd_port=config.fluentd.port)
475 sender.runlogdate = fluent_logger.runlogdate
476 print NFVBenchSummarizer(result, sender)
478 print NFVBenchSummarizer(result, None)
481 # show default config in text/yaml format
482 if opts.show_default_config:
488 # do not check extra_specs in flavor as it can contain any key/value pairs
489 whitelist_keys = ['extra_specs']
490 # override default config options with start config at path parsed from CLI
491 # check if it is an inline yaml/json config or a file name
492 if os.path.isfile(opts.config):
493 LOG.info('Loading configuration file: ' + opts.config)
494 config = config_load(opts.config, config, whitelist_keys)
495 config.name = os.path.basename(opts.config)
497 LOG.info('Loading configuration string: ' + opts.config)
498 config = config_loads(opts.config, config, whitelist_keys)
500 # traffic profile override options
501 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
503 # copy over cli options that are used in config
504 config.generator_profile = opts.generator_profile
508 config.log_file = opts.log_file
510 # show running config in json format
512 print json.dumps(config, sort_keys=True, indent=4)
515 if config.sriov and config.service_chain != ChainType.EXT:
516 # if sriov is requested (does not apply to ext chains)
517 # make sure the physnet names are specified
518 check_physnet("left", config.internal_networks.left)
519 check_physnet("right", config.internal_networks.right)
520 if config.service_chain == ChainType.PVVP:
521 check_physnet("middle", config.internal_networks.middle)
523 # update the config in the config plugin as it might have changed
524 # in a copy of the dict (config plugin still holds the original dict)
525 config_plugin.set_config(config)
527 # add file log if requested
529 log.add_file_logger(config.log_file)
531 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
534 if os.path.isdir(opts.server):
535 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
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__':