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('--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()
447 openstack_spec = config_plugin.get_openstack_spec()
449 opts, unknown_opts = parse_opts_from_cli()
450 log.set_level(debug=opts.debug)
452 # setup the fluent logger as soon as possible right after the config plugin is called,
453 # if there is any logging or result tag is set then initialize the fluent logger
454 for fluentd in config.fluentd:
455 if fluentd.logging_tag or fluentd.result_tag:
456 fluent_logger = FluentLogHandler(config.fluentd)
457 LOG.addHandler(fluent_logger)
461 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
465 with open(opts.summary) as json_data:
466 result = json.load(json_data)
468 result['config']['user_label'] = opts.user_label
469 print NFVBenchSummarizer(result, fluent_logger)
472 # show default config in text/yaml format
473 if opts.show_default_config:
479 # do not check extra_specs in flavor as it can contain any key/value pairs
480 whitelist_keys = ['extra_specs']
481 # override default config options with start config at path parsed from CLI
482 # check if it is an inline yaml/json config or a file name
483 if os.path.isfile(opts.config):
484 LOG.info('Loading configuration file: ' + opts.config)
485 config = config_load(opts.config, config, whitelist_keys)
486 config.name = os.path.basename(opts.config)
488 LOG.info('Loading configuration string: ' + opts.config)
489 config = config_loads(opts.config, config, whitelist_keys)
491 # traffic profile override options
492 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
494 # copy over cli options that are used in config
495 config.generator_profile = opts.generator_profile
499 config.log_file = opts.log_file
500 if opts.service_chain:
501 config.service_chain = opts.service_chain
502 if opts.service_chain_count:
503 config.service_chain_count = opts.service_chain_count
505 if opts.use_sriov_middle_net:
506 if (not config.sriov) or (not config.service_chain == ChainType.PVVP):
507 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
508 config.use_sriov_middle_net = True
510 if config.sriov and config.service_chain != ChainType.EXT:
511 # if sriov is requested (does not apply to ext chains)
512 # make sure the physnet names are specified
513 check_physnet("left", config.internal_networks.left)
514 check_physnet("right", config.internal_networks.right)
515 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
516 check_physnet("middle", config.internal_networks.middle)
518 # show running config in json format
520 print json.dumps(config, sort_keys=True, indent=4)
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__':