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.config, 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)
95 min_packet_size = "68" if self.config.vlan_tagging else "64"
96 for frame_size in self.config.frame_sizes:
98 if int(frame_size) < int(min_packet_size):
99 new_frame_sizes.append(min_packet_size)
100 LOG.info("Adjusting frame size %s Bytes to minimum size %s Bytes due to "
101 + "traffic generator restriction", frame_size, min_packet_size)
103 new_frame_sizes.append(frame_size)
105 new_frame_sizes.append(frame_size)
106 self.config.frame_sizes = tuple(new_frame_sizes)
108 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
109 "nfvbench_version": __version__,
110 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
113 "service_chain": self.chain_runner.run(),
114 "versions": self.chain_runner.get_version(),
118 if self.specs.openstack:
119 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
120 "encaps": self.specs.openstack.encaps}
121 result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
123 status = NFVBench.STATUS_ERROR
124 message = traceback.format_exc()
125 except KeyboardInterrupt:
126 status = NFVBench.STATUS_ERROR
127 message = traceback.format_exc()
129 if self.chain_runner:
130 self.chain_runner.close()
132 if status == NFVBench.STATUS_OK:
133 result = utils.dict_to_json_dict(result)
140 'error_message': message
143 def prepare_summary(self, result):
144 """Prepares summary of the result to print and send it to logger (eg: fluentd)"""
146 summary = NFVBenchSummarizer(result, fluent_logger)
147 LOG.info(str(summary))
149 def save(self, result):
150 """Save results in json format file."""
151 utils.save_json_result(result,
152 self.config.json_file,
153 self.config.std_json_path,
154 self.config.service_chain,
155 self.config.service_chain_count,
156 self.config.flow_count,
157 self.config.frame_sizes)
159 def update_config(self, opts):
160 self.config = AttrDict(dict(self.base_config))
161 self.config.update(opts)
163 self.config.service_chain = self.config.service_chain.upper()
164 self.config.service_chain_count = int(self.config.service_chain_count)
165 self.config.flow_count = utils.parse_flow_count(self.config.flow_count)
166 required_flow_count = self.config.service_chain_count * 2
167 if self.config.flow_count < required_flow_count:
168 LOG.info("Flow count %d has been set to minimum value of '%d' "
169 "for current configuration", self.config.flow_count,
171 self.config.flow_count = required_flow_count
173 if self.config.flow_count % 2 != 0:
174 self.config.flow_count += 1
176 self.config.duration_sec = float(self.config.duration_sec)
177 self.config.interval_sec = float(self.config.interval_sec)
179 # Get traffic generator profile config
180 if not self.config.generator_profile:
181 self.config.generator_profile = self.config.traffic_generator.default_profile
183 generator_factory = TrafficGeneratorFactory(self.config)
184 self.config.generator_config = \
185 generator_factory.get_generator_config(self.config.generator_profile)
187 # Check length of mac_addrs_left/right for serivce_chain EXT with no_arp
188 if self.config.service_chain == ChainType.EXT and self.config.no_arp:
189 if not (self.config.generator_config.mac_addrs_left is None and
190 self.config.generator_config.mac_addrs_right is None):
191 if (self.config.generator_config.mac_addrs_left is None or
192 self.config.generator_config.mac_addrs_right is None):
193 raise Exception("mac_addrs_left and mac_addrs_right must either "
194 "both be None or have a number of entries matching "
195 "service_chain_count")
196 if not (len(self.config.generator_config.mac_addrs_left) ==
197 self.config.service_chain_count and
198 len(self.config.generator_config.mac_addrs_right) ==
199 self.config.service_chain_count):
200 raise Exception("length of mac_addrs_left ({a}) and/or mac_addrs_right ({b}) "
201 "does not match service_chain_count ({c})"
202 .format(a=len(self.config.generator_config.mac_addrs_left),
203 b=len(self.config.generator_config.mac_addrs_right),
204 c=self.config.service_chain_count))
206 if not any(self.config.generator_config.pcis):
207 raise Exception("PCI addresses configuration for selected traffic generator profile "
208 "({tg_profile}) are missing. Please specify them in configuration file."
209 .format(tg_profile=self.config.generator_profile))
211 if self.config.traffic is None or not self.config.traffic:
212 raise Exception("No traffic profile found in traffic configuration, "
213 "please fill 'traffic' section in configuration file.")
215 if isinstance(self.config.traffic, tuple):
216 self.config.traffic = self.config.traffic[0]
218 self.config.frame_sizes = generator_factory.get_frame_sizes(self.config.traffic.profile)
220 self.config.ipv6_mode = False
221 self.config.no_dhcp = True
222 self.config.same_network_only = True
223 if self.config.openrc_file:
224 self.config.openrc_file = os.path.expanduser(self.config.openrc_file)
226 self.config.ndr_run = (not self.config.no_traffic
227 and 'ndr' in self.config.rate.strip().lower().split('_'))
228 self.config.pdr_run = (not self.config.no_traffic
229 and 'pdr' in self.config.rate.strip().lower().split('_'))
230 self.config.single_run = (not self.config.no_traffic
231 and not (self.config.ndr_run or self.config.pdr_run))
233 if self.config.vlans and len(self.config.vlans) != 2:
234 raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.')
236 self.config.json_file = self.config.json if self.config.json else None
237 if self.config.json_file:
238 (path, _filename) = os.path.split(self.config.json)
239 if not os.path.exists(path):
240 raise Exception('Please provide existing path for storing results in JSON file. '
241 'Path used: {path}'.format(path=path))
243 self.config.std_json_path = self.config.std_json if self.config.std_json else None
244 if self.config.std_json_path:
245 if not os.path.exists(self.config.std_json):
246 raise Exception('Please provide existing path for storing results in JSON file. '
247 'Path used: {path}'.format(path=self.config.std_json_path))
249 self.config_plugin.validate_config(self.config, self.specs.openstack)
252 def parse_opts_from_cli():
253 parser = argparse.ArgumentParser()
255 parser.add_argument('-c', '--config', dest='config',
257 help='Override default values with a config file or '
258 'a yaml/json config string',
259 metavar='<file_name_or_yaml>')
261 parser.add_argument('--server', dest='server',
264 metavar='<http_root_pathname>',
265 help='Run nfvbench in server mode and pass'
266 ' the HTTP root folder full pathname')
268 parser.add_argument('--host', dest='host',
271 help='Host IP address on which server will be listening (default 0.0.0.0)')
273 parser.add_argument('-p', '--port', dest='port',
276 help='Port on which server will be listening (default 7555)')
278 parser.add_argument('-sc', '--service-chain', dest='service_chain',
279 choices=BasicFactory.chain_classes,
281 help='Service chain to run')
283 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
285 help='Set number of service chains to run',
286 metavar='<service_chain_count>')
288 parser.add_argument('-fc', '--flow-count', dest='flow_count',
290 help='Set number of total flows for all chains and all directions',
291 metavar='<flow_count>')
293 parser.add_argument('--rate', dest='rate',
295 help='Specify rate in pps, bps or %% as total for all directions',
298 parser.add_argument('--duration', dest='duration_sec',
300 help='Set duration to run traffic generator (in seconds)',
301 metavar='<duration_sec>')
303 parser.add_argument('--interval', dest='interval_sec',
305 help='Set interval to record traffic generator stats (in seconds)',
306 metavar='<interval_sec>')
308 parser.add_argument('--inter-node', dest='inter_node',
311 help='run VMs in different compute nodes (PVVP only)')
313 parser.add_argument('--sriov', dest='sriov',
316 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
318 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
321 help='Use SRIOV to handle the middle network traffic '
322 '(PVVP with SRIOV only)')
324 parser.add_argument('-d', '--debug', dest='debug',
327 help='print debug messages (verbose)')
329 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
331 help='Traffic generator profile to use')
333 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
336 help='Check config and connectivity only - do not generate traffic')
338 parser.add_argument('--no-arp', dest='no_arp',
341 help='Do not use ARP to find MAC addresses, '
342 'instead use values in config file')
344 parser.add_argument('--no-reset', dest='no_reset',
347 help='Do not reset counters prior to running')
349 parser.add_argument('--no-int-config', dest='no_int_config',
352 help='Skip interfaces config on EXT service chain')
354 parser.add_argument('--no-tor-access', dest='no_tor_access',
357 help='Skip TOR switch configuration and retrieving of stats')
359 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
362 help='Skip vswitch configuration and retrieving of stats')
364 parser.add_argument('--no-cleanup', dest='no_cleanup',
367 help='no cleanup after run')
369 parser.add_argument('--json', dest='json',
371 help='store results in json format file',
372 metavar='<path>/<filename>')
374 parser.add_argument('--std-json', dest='std_json',
376 help='store results in json format file with nfvbench standard filename: '
377 '<service-chain-type>-<service-chain-count>-<flow-count>'
378 '-<packet-sizes>.json',
381 parser.add_argument('--show-default-config', dest='show_default_config',
384 help='print the default config in yaml format (unedited)')
386 parser.add_argument('--show-config', dest='show_config',
389 help='print the running config in json format')
391 parser.add_argument('-ss', '--show-summary', dest='summary',
393 help='Show summary from nfvbench json file',
396 parser.add_argument('-v', '--version', dest='version',
401 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
403 help='Override traffic profile frame sizes',
404 metavar='<frame_size_bytes or IMIX>')
406 parser.add_argument('--unidir', dest='unidir',
409 help='Override traffic profile direction (requires -fs)')
411 parser.add_argument('--log-file', '--logfile', dest='log_file',
413 help='Filename for saving logs',
414 metavar='<log_file>')
416 parser.add_argument('--user-label', '--userlabel', dest='user_label',
418 help='Custom label for performance records')
420 opts, unknown_opts = parser.parse_known_args()
421 return opts, unknown_opts
424 def load_default_config():
425 default_cfg = resource_string(__name__, "cfg.default.yaml")
426 config = config_loads(default_cfg)
427 config.name = '(built-in default config)'
428 return config, default_cfg
431 def override_custom_traffic(config, frame_sizes, unidir):
432 """Override the traffic profiles with a custom one
434 if frame_sizes is not None:
435 traffic_profile_name = "custom_traffic_profile"
436 config.traffic_profile = [
438 "l2frame_size": frame_sizes,
439 "name": traffic_profile_name
443 traffic_profile_name = config.traffic["profile"]
445 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
447 "bidirectional": bidirectional,
448 "profile": traffic_profile_name
452 def check_physnet(name, netattrs):
453 if not netattrs.physical_network:
454 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
456 if not netattrs.segmentation_id:
457 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
463 run_summary_required = False
466 # load default config file
467 config, default_cfg = load_default_config()
468 # create factory for platform specific classes
470 factory_module = importlib.import_module(config['factory_module'])
471 factory = getattr(factory_module, config['factory_class'])()
472 except AttributeError:
473 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
474 .format(m=config['factory_module'], c=config['factory_class']))
475 # create config plugin for this platform
476 config_plugin = factory.get_config_plugin_class()(config)
477 config = config_plugin.get_config()
479 opts, unknown_opts = parse_opts_from_cli()
480 log.set_level(debug=opts.debug)
483 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
487 with open(opts.summary) as json_data:
488 result = json.load(json_data)
490 result['config']['user_label'] = opts.user_label
491 print NFVBenchSummarizer(result, fluent_logger)
494 # show default config in text/yaml format
495 if opts.show_default_config:
501 # do not check extra_specs in flavor as it can contain any key/value pairs
502 whitelist_keys = ['extra_specs']
503 # override default config options with start config at path parsed from CLI
504 # check if it is an inline yaml/json config or a file name
505 if os.path.isfile(opts.config):
506 LOG.info('Loading configuration file: %s', opts.config)
507 config = config_load(opts.config, config, whitelist_keys)
508 config.name = os.path.basename(opts.config)
510 LOG.info('Loading configuration string: %s', opts.config)
511 config = config_loads(opts.config, config, whitelist_keys)
513 # setup the fluent logger as soon as possible right after the config plugin is called,
514 # if there is any logging or result tag is set then initialize the fluent logger
515 for fluentd in config.fluentd:
516 if fluentd.logging_tag or fluentd.result_tag:
517 fluent_logger = FluentLogHandler(config.fluentd)
518 LOG.addHandler(fluent_logger)
521 # traffic profile override options
522 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
524 # copy over cli options that are used in config
525 config.generator_profile = opts.generator_profile
529 config.log_file = opts.log_file
530 if opts.service_chain:
531 config.service_chain = opts.service_chain
532 if opts.service_chain_count:
533 config.service_chain_count = opts.service_chain_count
534 if opts.no_vswitch_access:
535 config.no_vswitch_access = opts.no_vswitch_access
536 if opts.no_int_config:
537 config.no_int_config = opts.no_int_config
539 if opts.use_sriov_middle_net:
540 if (not config.sriov) or (not config.service_chain == ChainType.PVVP):
541 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
542 config.use_sriov_middle_net = True
544 if config.sriov and config.service_chain != ChainType.EXT:
545 # if sriov is requested (does not apply to ext chains)
546 # make sure the physnet names are specified
547 check_physnet("left", config.internal_networks.left)
548 check_physnet("right", config.internal_networks.right)
549 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
550 check_physnet("middle", config.internal_networks.middle)
552 # show running config in json format
554 print json.dumps(config, sort_keys=True, indent=4)
557 # check that an empty openrc file (no OpenStack) is only allowed
559 if not config.openrc_file:
560 if config.service_chain == ChainType.EXT:
561 LOG.info('EXT chain with OpenStack mode disabled')
563 raise Exception("openrc_file is empty in the configuration and is required")
565 # update the config in the config plugin as it might have changed
566 # in a copy of the dict (config plugin still holds the original dict)
567 config_plugin.set_config(config)
569 # add file log if requested
571 log.add_file_logger(config.log_file)
573 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
576 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
579 if os.path.isdir(opts.server):
580 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
581 nfvbench_instance.set_notifier(server)
583 port = int(opts.port)
585 server.run(host=opts.host)
587 server.run(host=opts.host, port=port)
589 print 'Invalid HTTP root directory: ' + opts.server
592 with utils.RunLock():
593 run_summary_required = True
595 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
597 raise Exception(err_msg)
599 # remove unfilled values
600 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
602 params = ' '.join(str(e) for e in sys.argv[1:])
603 result = nfvbench_instance.run(opts, params)
604 if 'error_message' in result:
605 raise Exception(result['error_message'])
607 if 'result' in result and result['status']:
608 nfvbench_instance.save(result['result'])
609 nfvbench_instance.prepare_summary(result['result'])
610 except Exception as exc:
611 run_summary_required = True
613 'status': NFVBench.STATUS_ERROR,
614 'error_message': traceback.format_exc()
619 # only send a summary record if there was an actual nfvbench run or
620 # if an error/exception was logged.
621 fluent_logger.send_run_summary(run_summary_required)
624 if __name__ == '__main__':