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)"""
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('-d', '--debug', dest='debug',
290 help='print debug messages (verbose)')
292 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
294 help='Traffic generator profile to use')
296 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
299 help='Check config and connectivity only - do not generate traffic')
301 parser.add_argument('--no-arp', dest='no_arp',
304 help='Do not use ARP to find MAC addresses, '
305 'instead use values in config file')
307 parser.add_argument('--no-reset', dest='no_reset',
310 help='Do not reset counters prior to running')
312 parser.add_argument('--no-int-config', dest='no_int_config',
315 help='Skip interfaces config on EXT service chain')
317 parser.add_argument('--no-tor-access', dest='no_tor_access',
320 help='Skip TOR switch configuration and retrieving of stats')
322 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
325 help='Skip vswitch configuration and retrieving of stats')
327 parser.add_argument('--no-cleanup', dest='no_cleanup',
330 help='no cleanup after run')
332 parser.add_argument('--json', dest='json',
334 help='store results in json format file',
335 metavar='<path>/<filename>')
337 parser.add_argument('--std-json', dest='std_json',
339 help='store results in json format file with nfvbench standard filename: '
340 '<service-chain-type>-<service-chain-count>-<flow-count>'
341 '-<packet-sizes>.json',
344 parser.add_argument('--show-default-config', dest='show_default_config',
347 help='print the default config in yaml format (unedited)')
349 parser.add_argument('--show-config', dest='show_config',
352 help='print the running config in json format')
354 parser.add_argument('-ss', '--show-summary', dest='summary',
356 help='Show summary from nfvbench json file',
359 parser.add_argument('-v', '--version', dest='version',
364 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
366 help='Override traffic profile frame sizes',
367 metavar='<frame_size_bytes or IMIX>')
369 parser.add_argument('--unidir', dest='unidir',
372 help='Override traffic profile direction (requires -fs)')
374 parser.add_argument('--log-file', '--logfile', dest='log_file',
376 help='Filename for saving logs',
377 metavar='<log_file>')
379 parser.add_argument('--user-label', '--userlabel', dest='user_label',
381 help='Custom label for performance records')
383 opts, unknown_opts = parser.parse_known_args()
384 return opts, unknown_opts
387 def load_default_config():
388 default_cfg = resource_string(__name__, "cfg.default.yaml")
389 config = config_loads(default_cfg)
390 config.name = '(built-in default config)'
391 return config, default_cfg
394 def override_custom_traffic(config, frame_sizes, unidir):
395 """Override the traffic profiles with a custom one
397 if frame_sizes is not None:
398 traffic_profile_name = "custom_traffic_profile"
399 config.traffic_profile = [
401 "l2frame_size": frame_sizes,
402 "name": traffic_profile_name
406 traffic_profile_name = config.traffic["profile"]
408 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
410 "bidirectional": bidirectional,
411 "profile": traffic_profile_name
415 def check_physnet(name, netattrs):
416 if not netattrs.physical_network:
417 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
419 if not netattrs.segmentation_id:
420 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
426 run_summary_required = False
429 # load default config file
430 config, default_cfg = load_default_config()
431 # create factory for platform specific classes
433 factory_module = importlib.import_module(config['factory_module'])
434 factory = getattr(factory_module, config['factory_class'])()
435 except AttributeError:
436 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
437 .format(m=config['factory_module'], c=config['factory_class']))
438 # create config plugin for this platform
439 config_plugin = factory.get_config_plugin_class()(config)
440 config = config_plugin.get_config()
441 openstack_spec = config_plugin.get_openstack_spec()
443 opts, unknown_opts = parse_opts_from_cli()
444 log.set_level(debug=opts.debug)
446 # setup the fluent logger as soon as possible right after the config plugin is called,
447 # if there is any logging or result tag is set then initialize the fluent logger
448 for fluentd in config.fluentd:
449 if fluentd.logging_tag or fluentd.result_tag:
450 fluent_logger = FluentLogHandler(config.fluentd)
451 LOG.addHandler(fluent_logger)
455 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
459 with open(opts.summary) as json_data:
460 result = json.load(json_data)
462 result['config']['user_label'] = opts.user_label
463 print NFVBenchSummarizer(result, fluent_logger)
466 # show default config in text/yaml format
467 if opts.show_default_config:
473 # do not check extra_specs in flavor as it can contain any key/value pairs
474 whitelist_keys = ['extra_specs']
475 # override default config options with start config at path parsed from CLI
476 # check if it is an inline yaml/json config or a file name
477 if os.path.isfile(opts.config):
478 LOG.info('Loading configuration file: ' + opts.config)
479 config = config_load(opts.config, config, whitelist_keys)
480 config.name = os.path.basename(opts.config)
482 LOG.info('Loading configuration string: ' + opts.config)
483 config = config_loads(opts.config, config, whitelist_keys)
485 # traffic profile override options
486 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
488 # copy over cli options that are used in config
489 config.generator_profile = opts.generator_profile
493 config.log_file = opts.log_file
495 # show running config in json format
497 print json.dumps(config, sort_keys=True, indent=4)
500 if config.sriov and config.service_chain != ChainType.EXT:
501 # if sriov is requested (does not apply to ext chains)
502 # make sure the physnet names are specified
503 check_physnet("left", config.internal_networks.left)
504 check_physnet("right", config.internal_networks.right)
505 if config.service_chain == ChainType.PVVP:
506 check_physnet("middle", config.internal_networks.middle)
508 # update the config in the config plugin as it might have changed
509 # in a copy of the dict (config plugin still holds the original dict)
510 config_plugin.set_config(config)
512 # add file log if requested
514 log.add_file_logger(config.log_file)
516 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
519 if os.path.isdir(opts.server):
520 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
521 nfvbench_instance.set_notifier(server)
523 port = int(opts.port)
525 server.run(host=opts.host)
527 server.run(host=opts.host, port=port)
529 print 'Invalid HTTP root directory: ' + opts.server
532 with utils.RunLock():
533 run_summary_required = True
535 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
537 raise Exception(err_msg)
539 # remove unfilled values
540 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
542 params = ' '.join(str(e) for e in sys.argv[1:])
543 result = nfvbench_instance.run(opts, params)
544 if 'error_message' in result:
545 raise Exception(result['error_message'])
547 if 'result' in result and result['status']:
548 nfvbench_instance.save(result['result'])
549 nfvbench_instance.prepare_summary(result['result'])
550 except Exception as exc:
551 run_summary_required = True
553 'status': NFVBench.STATUS_ERROR,
554 'error_message': traceback.format_exc()
559 # only send a summary record if there was an actual nfvbench run or
560 # if an error/exception was logged.
561 fluent_logger.send_run_summary(run_summary_required)
564 if __name__ == '__main__':