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
26 from attrdict import AttrDict
28 from pkg_resources import resource_string
30 from __init__ import __version__
31 from chain_runner import ChainRunner
32 from cleanup import Cleaner
33 from config import config_load
34 from config import config_loads
35 import credentials as credentials
36 from fluentd import FluentLogHandler
39 from nfvbenchd import WebSocketIoServer
40 from specs import ChainType
41 from specs import Specs
42 from summarizer import NFVBenchSummarizer
48 class NFVBench(object):
49 """Main class of NFV benchmarking tool."""
52 STATUS_ERROR = 'ERROR'
54 def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
55 # the base config never changes for a given NFVbench instance
56 self.base_config = config
57 # this is the running config, updated at every run()
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 if config.openrc_file else None
64 self.chain_runner = None
66 self.specs.set_openstack_spec(openstack_spec)
70 def set_notifier(self, notifier):
71 self.notifier = notifier
73 def run(self, opts, args):
74 status = NFVBench.STATUS_OK
78 # take a snapshot of the current time for this new run
79 # so that all subsequent logs can relate to this run
80 fluent_logger.start_new_run()
83 # recalc the running config based on the base config and options for this run
84 self._update_config(opts)
85 self.specs.set_run_spec(self.config_plugin.get_run_spec(self.config,
86 self.specs.openstack))
87 self.chain_runner = ChainRunner(self.config,
93 # make sure that the min frame size is 64
95 for frame_size in self.config.frame_sizes:
97 if int(frame_size) < min_packet_size:
98 frame_size = str(min_packet_size)
99 LOG.info("Adjusting frame size %s bytes to minimum size %s bytes",
100 frame_size, min_packet_size)
101 if frame_size not in new_frame_sizes:
102 new_frame_sizes.append(frame_size)
104 new_frame_sizes.append(frame_size.upper())
105 self.config.frame_sizes = new_frame_sizes
107 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
108 "nfvbench_version": __version__,
109 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
112 "service_chain": self.chain_runner.run(),
113 "versions": self.chain_runner.get_version(),
117 if self.specs.openstack:
118 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
119 "encaps": self.specs.openstack.encaps}
120 result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
122 status = NFVBench.STATUS_ERROR
123 message = traceback.format_exc()
124 except KeyboardInterrupt:
125 status = NFVBench.STATUS_ERROR
126 message = traceback.format_exc()
128 if self.chain_runner:
129 self.chain_runner.close()
131 if status == NFVBench.STATUS_OK:
132 # result2 = utils.dict_to_json_dict(result)
139 'error_message': message
142 def prepare_summary(self, result):
143 """Prepare summary of the result to print and send it to logger (eg: fluentd)."""
145 summary = NFVBenchSummarizer(result, fluent_logger)
146 LOG.info(str(summary))
148 def save(self, result):
149 """Save results in json format file."""
150 utils.save_json_result(result,
151 self.config.json_file,
152 self.config.std_json_path,
153 self.config.service_chain,
154 self.config.service_chain_count,
155 self.config.flow_count,
156 self.config.frame_sizes)
158 def _update_config(self, opts):
159 """Recalculate the running config based on the base config and opts.
161 Sanity check on the config is done here as well.
163 self.config = AttrDict(dict(self.base_config))
164 self.config.update(opts)
167 config.service_chain = config.service_chain.upper()
168 config.service_chain_count = int(config.service_chain_count)
169 if config.l2_loopback:
170 # force the number of chains to be 1 in case of l2 loopback
171 config.service_chain_count = 1
172 config.service_chain = ChainType.EXT
174 LOG.info('Running L2 loopback: using EXT chain/no ARP')
175 config.flow_count = utils.parse_flow_count(config.flow_count)
176 required_flow_count = config.service_chain_count * 2
177 if config.flow_count < required_flow_count:
178 LOG.info("Flow count %d has been set to minimum value of '%d' "
179 "for current configuration", config.flow_count,
181 config.flow_count = required_flow_count
183 if config.flow_count % 2:
184 config.flow_count += 1
186 config.duration_sec = float(config.duration_sec)
187 config.interval_sec = float(config.interval_sec)
188 config.pause_sec = float(config.pause_sec)
190 if config.traffic is None or not config.traffic:
191 raise Exception("Missing traffic property in configuration")
193 if config.openrc_file:
194 config.openrc_file = os.path.expanduser(config.openrc_file)
196 config.ndr_run = (not config.no_traffic and
197 'ndr' in config.rate.strip().lower().split('_'))
198 config.pdr_run = (not config.no_traffic and
199 'pdr' in config.rate.strip().lower().split('_'))
200 config.single_run = (not config.no_traffic and
201 not (config.ndr_run or config.pdr_run))
203 config.json_file = config.json if config.json else None
205 (path, _filename) = os.path.split(config.json)
206 if not os.path.exists(path):
207 raise Exception('Please provide existing path for storing results in JSON file. '
208 'Path used: {path}'.format(path=path))
210 config.std_json_path = config.std_json if config.std_json else None
211 if config.std_json_path:
212 if not os.path.exists(config.std_json):
213 raise Exception('Please provide existing path for storing results in JSON file. '
214 'Path used: {path}'.format(path=config.std_json_path))
216 self.config_plugin.validate_config(config, self.specs.openstack)
219 def parse_opts_from_cli():
220 parser = argparse.ArgumentParser()
222 parser.add_argument('--status', dest='status',
225 help='Provide NFVbench status')
227 parser.add_argument('-c', '--config', dest='config',
229 help='Override default values with a config file or '
230 'a yaml/json config string',
231 metavar='<file_name_or_yaml>')
233 parser.add_argument('--server', dest='server',
236 metavar='<http_root_pathname>',
237 help='Run nfvbench in server mode and pass'
238 ' the HTTP root folder full pathname')
240 parser.add_argument('--host', dest='host',
243 help='Host IP address on which server will be listening (default 0.0.0.0)')
245 parser.add_argument('-p', '--port', dest='port',
248 help='Port on which server will be listening (default 7555)')
250 parser.add_argument('-sc', '--service-chain', dest='service_chain',
251 choices=ChainType.names,
253 help='Service chain to run')
255 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
257 help='Set number of service chains to run',
258 metavar='<service_chain_count>')
260 parser.add_argument('-fc', '--flow-count', dest='flow_count',
262 help='Set number of total flows for all chains and all directions',
263 metavar='<flow_count>')
265 parser.add_argument('--rate', dest='rate',
267 help='Specify rate in pps, bps or %% as total for all directions',
270 parser.add_argument('--duration', dest='duration_sec',
272 help='Set duration to run traffic generator (in seconds)',
273 metavar='<duration_sec>')
275 parser.add_argument('--interval', dest='interval_sec',
277 help='Set interval to record traffic generator stats (in seconds)',
278 metavar='<interval_sec>')
280 parser.add_argument('--inter-node', dest='inter_node',
283 help='run VMs in different compute nodes (PVVP only)')
285 parser.add_argument('--sriov', dest='sriov',
288 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
290 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
293 help='Use SRIOV to handle the middle network traffic '
294 '(PVVP with SRIOV only)')
296 parser.add_argument('-d', '--debug', dest='debug',
299 help='print debug messages (verbose)')
301 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
303 help='Traffic generator profile to use')
305 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
308 help='Check config and connectivity only - do not generate traffic')
310 parser.add_argument('--no-arp', dest='no_arp',
313 help='Do not use ARP to find MAC addresses, '
314 'instead use values in config file')
316 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
319 help='Skip vswitch configuration and retrieving of stats')
321 parser.add_argument('--no-cleanup', dest='no_cleanup',
324 help='no cleanup after run')
326 parser.add_argument('--cleanup', dest='cleanup',
329 help='Cleanup NFVbench resources (prompt to confirm)')
331 parser.add_argument('--force-cleanup', dest='force_cleanup',
334 help='Cleanup NFVbench resources (do not prompt)')
336 parser.add_argument('--json', dest='json',
338 help='store results in json format file',
339 metavar='<path>/<filename>')
341 parser.add_argument('--std-json', dest='std_json',
343 help='store results in json format file with nfvbench standard filename: '
344 '<service-chain-type>-<service-chain-count>-<flow-count>'
345 '-<packet-sizes>.json',
348 parser.add_argument('--show-default-config', dest='show_default_config',
351 help='print the default config in yaml format (unedited)')
353 parser.add_argument('--show-config', dest='show_config',
356 help='print the running config in json format')
358 parser.add_argument('-ss', '--show-summary', dest='summary',
360 help='Show summary from nfvbench json file',
363 parser.add_argument('-v', '--version', dest='version',
368 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
370 help='Override traffic profile frame sizes',
371 metavar='<frame_size_bytes or IMIX>')
373 parser.add_argument('--unidir', dest='unidir',
376 help='Override traffic profile direction (requires -fs)')
378 parser.add_argument('--log-file', '--logfile', dest='log_file',
380 help='Filename for saving logs',
381 metavar='<log_file>')
383 parser.add_argument('--user-label', '--userlabel', dest='user_label',
385 help='Custom label for performance records')
387 parser.add_argument('--hypervisor', dest='hypervisor',
389 metavar='<hypervisor name>',
390 help='Where chains must run ("compute", "az:", "az:compute")')
392 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
395 help='Port to port or port to switch to port L2 loopback with VLAN id')
397 opts, unknown_opts = parser.parse_known_args()
398 return opts, unknown_opts
401 def load_default_config():
402 default_cfg = resource_string(__name__, "cfg.default.yaml")
403 config = config_loads(default_cfg)
404 config.name = '(built-in default config)'
405 return config, default_cfg
408 def override_custom_traffic(config, frame_sizes, unidir):
409 """Override the traffic profiles with a custom one."""
410 if frame_sizes is not None:
411 traffic_profile_name = "custom_traffic_profile"
412 config.traffic_profile = [
414 "l2frame_size": frame_sizes,
415 "name": traffic_profile_name
419 traffic_profile_name = config.traffic["profile"]
421 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
423 "bidirectional": bidirectional,
424 "profile": traffic_profile_name
428 def check_physnet(name, netattrs):
429 if not netattrs.physical_network:
430 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
432 if not netattrs.segmentation_id:
433 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
436 def status_cleanup(config, cleanup, force_cleanup):
437 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
438 # check if another run is pending
441 with utils.RunLock():
442 LOG.info('Status: idle')
444 LOG.info('Status: busy (run pending)')
446 # check nfvbench resources
447 if config.openrc_file and config.service_chain != ChainType.EXT:
448 cleaner = Cleaner(config)
449 count = cleaner.show_resources()
450 if count and (cleanup or force_cleanup):
451 cleaner.clean(not force_cleanup)
456 run_summary_required = False
459 # load default config file
460 config, default_cfg = load_default_config()
461 # create factory for platform specific classes
463 factory_module = importlib.import_module(config['factory_module'])
464 factory = getattr(factory_module, config['factory_class'])()
465 except AttributeError:
466 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
467 .format(m=config['factory_module'], c=config['factory_class']))
468 # create config plugin for this platform
469 config_plugin = factory.get_config_plugin_class()(config)
470 config = config_plugin.get_config()
472 opts, unknown_opts = parse_opts_from_cli()
473 log.set_level(debug=opts.debug)
476 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
480 with open(opts.summary) as json_data:
481 result = json.load(json_data)
483 result['config']['user_label'] = opts.user_label
484 print NFVBenchSummarizer(result, fluent_logger)
487 # show default config in text/yaml format
488 if opts.show_default_config:
494 # do not check extra_specs in flavor as it can contain any key/value pairs
495 whitelist_keys = ['extra_specs']
496 # override default config options with start config at path parsed from CLI
497 # check if it is an inline yaml/json config or a file name
498 if os.path.isfile(opts.config):
499 LOG.info('Loading configuration file: %s', opts.config)
500 config = config_load(opts.config, config, whitelist_keys)
501 config.name = os.path.basename(opts.config)
503 LOG.info('Loading configuration string: %s', opts.config)
504 config = config_loads(opts.config, config, whitelist_keys)
506 # setup the fluent logger as soon as possible right after the config plugin is called,
507 # if there is any logging or result tag is set then initialize the fluent logger
508 for fluentd in config.fluentd:
509 if fluentd.logging_tag or fluentd.result_tag:
510 fluent_logger = FluentLogHandler(config.fluentd)
511 LOG.addHandler(fluent_logger)
514 # traffic profile override options
515 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
517 # copy over cli options that are used in config
518 config.generator_profile = opts.generator_profile
522 config.log_file = opts.log_file
523 if opts.service_chain:
524 config.service_chain = opts.service_chain
525 if opts.service_chain_count:
526 config.service_chain_count = opts.service_chain_count
527 if opts.no_vswitch_access:
528 config.no_vswitch_access = opts.no_vswitch_access
530 # can be any of 'comp1', 'nova:', 'nova:comp1'
531 config.compute_nodes = opts.hypervisor
533 # port to port loopback (direct or through switch)
535 config.l2_loopback = True
536 if config.service_chain != ChainType.EXT:
537 LOG.info('Changing service chain type to EXT')
538 config.service_chain = ChainType.EXT
539 if not config.no_arp:
540 LOG.info('Disabling ARP')
542 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
543 LOG.info('Running L2 loopback: using EXT chain/no ARP')
545 if opts.use_sriov_middle_net:
546 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
547 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
548 config.use_sriov_middle_net = True
550 if config.sriov and config.service_chain != ChainType.EXT:
551 # if sriov is requested (does not apply to ext chains)
552 # make sure the physnet names are specified
553 check_physnet("left", config.internal_networks.left)
554 check_physnet("right", config.internal_networks.right)
555 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
556 check_physnet("middle", config.internal_networks.middle)
558 # show running config in json format
560 print json.dumps(config, sort_keys=True, indent=4)
563 # check that an empty openrc file (no OpenStack) is only allowed
565 if not config.openrc_file:
566 if config.service_chain == ChainType.EXT:
567 LOG.info('EXT chain with OpenStack mode disabled')
569 raise Exception("openrc_file is empty in the configuration and is required")
571 # update the config in the config plugin as it might have changed
572 # in a copy of the dict (config plugin still holds the original dict)
573 config_plugin.set_config(config)
575 if opts.status or opts.cleanup or opts.force_cleanup:
576 status_cleanup(config, opts.cleanup, opts.force_cleanup)
578 # add file log if requested
580 log.add_file_logger(config.log_file)
582 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
585 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
588 if os.path.isdir(opts.server):
589 server = WebSocketIoServer(opts.server, nfvbench_instance, fluent_logger)
590 nfvbench_instance.set_notifier(server)
592 port = int(opts.port)
594 server.run(host=opts.host)
596 server.run(host=opts.host, port=port)
598 print 'Invalid HTTP root directory: ' + opts.server
601 with utils.RunLock():
602 run_summary_required = True
604 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
606 raise Exception(err_msg)
608 # remove unfilled values
609 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
611 params = ' '.join(str(e) for e in sys.argv[1:])
612 result = nfvbench_instance.run(opts, params)
613 if 'error_message' in result:
614 raise Exception(result['error_message'])
616 if 'result' in result and result['status']:
617 nfvbench_instance.save(result['result'])
618 nfvbench_instance.prepare_summary(result['result'])
619 except Exception as exc:
620 run_summary_required = True
622 'status': NFVBench.STATUS_ERROR,
623 'error_message': traceback.format_exc()
628 # only send a summary record if there was an actual nfvbench run or
629 # if an error/exception was logged.
630 fluent_logger.send_run_summary(run_summary_required)
633 if __name__ == '__main__':