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
27 from logging import FileHandler
29 from pkg_resources import resource_string
31 from .__init__ import __version__
32 from .chain_runner import ChainRunner
33 from .cleanup import Cleaner
34 from .config import config_load
35 from .config import config_loads
36 from . import credentials
37 from .fluentd import FluentLogHandler
40 from .nfvbenchd import WebServer
41 from .specs import ChainType
42 from .specs import Specs
43 from .summarizer import NFVBenchSummarizer
49 class NFVBench(object):
50 """Main class of NFV benchmarking tool."""
53 STATUS_ERROR = 'ERROR'
55 def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
56 # the base config never changes for a given NFVbench instance
57 self.base_config = config
58 # this is the running config, updated at every run()
60 self.config_plugin = config_plugin
61 self.factory = factory
62 self.notifier = notifier
63 self.cred = credentials.Credentials(config.openrc_file, None, False) \
64 if config.openrc_file else None
65 self.chain_runner = None
67 self.specs.set_openstack_spec(openstack_spec)
71 def set_notifier(self, notifier):
72 self.notifier = notifier
74 def run(self, opts, args):
75 """This run() method is called for every NFVbench benchmark request.
77 In CLI mode, this method is called only once per invocation.
78 In REST server mode, this is called once per REST POST request
80 status = NFVBench.STATUS_OK
84 # take a snapshot of the current time for this new run
85 # so that all subsequent logs can relate to this run
86 fluent_logger.start_new_run()
89 # recalc the running config based on the base config and options for this run
90 self._update_config(opts)
91 if int(self.config.cache_size) < 0:
92 self.config.cache_size = self.config.flow_count
93 # check that an empty openrc file (no OpenStack) is only allowed
95 if not self.config.openrc_file and self.config.service_chain != ChainType.EXT:
96 raise Exception("openrc_file in the configuration is required for PVP/PVVP chains")
98 self.specs.set_run_spec(self.config_plugin.get_run_spec(self.config,
99 self.specs.openstack))
100 self.chain_runner = ChainRunner(self.config,
106 # make sure that the min frame size is 64
108 for frame_size in self.config.frame_sizes:
110 if int(frame_size) < min_packet_size:
111 frame_size = str(min_packet_size)
112 LOG.info("Adjusting frame size %s bytes to minimum size %s bytes",
113 frame_size, min_packet_size)
114 if frame_size not in new_frame_sizes:
115 new_frame_sizes.append(frame_size)
117 new_frame_sizes.append(frame_size.upper())
118 self.config.frame_sizes = new_frame_sizes
120 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
121 "nfvbench_version": __version__,
122 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
125 "service_chain": self.chain_runner.run(),
126 "versions": self.chain_runner.get_version(),
130 if self.specs.openstack:
131 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
132 "encaps": self.specs.openstack.encaps}
133 result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
135 status = NFVBench.STATUS_ERROR
136 message = traceback.format_exc()
137 except KeyboardInterrupt:
138 status = NFVBench.STATUS_ERROR
139 message = traceback.format_exc()
141 if self.chain_runner:
142 self.chain_runner.close()
144 if status == NFVBench.STATUS_OK:
145 # result2 = utils.dict_to_json_dict(result)
152 'error_message': message
155 def prepare_summary(self, result):
156 """Prepare summary of the result to print and send it to logger (eg: fluentd)."""
158 summary = NFVBenchSummarizer(result, fluent_logger)
159 LOG.info(str(summary))
161 def save(self, result):
162 """Save results in json format file."""
163 utils.save_json_result(result,
164 self.config.json_file,
165 self.config.std_json_path,
166 self.config.service_chain,
167 self.config.service_chain_count,
168 self.config.flow_count,
169 self.config.frame_sizes)
171 def _update_config(self, opts):
172 """Recalculate the running config based on the base config and opts.
174 Sanity check on the config is done here as well.
176 self.config = AttrDict(dict(self.base_config))
177 # Update log file handler if needed after a config update (REST mode)
178 if 'log_file' in opts:
180 (path, _filename) = os.path.split(opts['log_file'])
181 if not os.path.exists(path):
183 'Path %s does not exist. Please verify root path is shared with host. Path '
184 'will be created.', path)
186 LOG.info('%s is created.', path)
187 for h in log.getLogger().handlers:
188 if isinstance(h, FileHandler) and h.baseFilename != opts['log_file']:
189 # clean log file handler
190 log.getLogger().removeHandler(h)
191 # add handler if not existing to avoid duplicates handlers
192 if len(log.getLogger().handlers) == 1:
193 log.add_file_logger(opts['log_file'])
195 self.config.update(opts)
198 config.service_chain = config.service_chain.upper()
199 config.service_chain_count = int(config.service_chain_count)
200 if config.l2_loopback:
201 # force the number of chains to be 1 in case of l2 loopback
202 config.service_chain_count = 1
203 config.service_chain = ChainType.EXT
205 LOG.info('Running L2 loopback: using EXT chain/no ARP')
207 # traffic profile override options
208 if 'frame_sizes' in opts:
211 unidir = opts['unidir']
212 override_custom_traffic(config, opts['frame_sizes'], unidir)
213 LOG.info("Frame size has been set to %s for current configuration", opts['frame_sizes'])
215 config.flow_count = utils.parse_flow_count(config.flow_count)
216 required_flow_count = config.service_chain_count * 2
217 if config.flow_count < required_flow_count:
218 LOG.info("Flow count %d has been set to minimum value of '%d' "
219 "for current configuration", config.flow_count,
221 config.flow_count = required_flow_count
223 if config.flow_count % 2:
224 config.flow_count += 1
226 config.duration_sec = float(config.duration_sec)
227 config.interval_sec = float(config.interval_sec)
228 config.pause_sec = float(config.pause_sec)
230 if config.traffic is None or not config.traffic:
231 raise Exception("Missing traffic property in configuration")
233 if config.openrc_file:
234 config.openrc_file = os.path.expanduser(config.openrc_file)
235 if config.flavor.vcpus < 2:
236 raise Exception("Flavor vcpus must be >= 2")
239 config.ndr_run = (not config.no_traffic and
240 'ndr' in config.rate.strip().lower().split('_'))
241 config.pdr_run = (not config.no_traffic and
242 'pdr' in config.rate.strip().lower().split('_'))
243 config.single_run = (not config.no_traffic and
244 not (config.ndr_run or config.pdr_run))
246 config.json_file = config.json if config.json else None
248 (path, _filename) = os.path.split(config.json)
249 if not os.path.exists(path):
250 raise Exception('Please provide existing path for storing results in JSON file. '
251 'Path used: {path}'.format(path=path))
253 config.std_json_path = config.std_json if config.std_json else None
254 if config.std_json_path:
255 if not os.path.exists(config.std_json):
256 raise Exception('Please provide existing path for storing results in JSON file. '
257 'Path used: {path}'.format(path=config.std_json_path))
259 # Check that multiqueue is between 1 and 8 (8 is the max allowed by libvirt/qemu)
260 if config.vif_multiqueue_size < 1 or config.vif_multiqueue_size > 8:
261 raise Exception('vif_multiqueue_size (%d) must be in [1..8]' %
262 config.vif_multiqueue_size)
264 # VxLAN and MPLS sanity checks
265 if config.vxlan or config.mpls:
266 if config.vlan_tagging:
267 config.vlan_tagging = False
268 config.no_latency_streams = True
269 config.no_latency_stats = True
270 config.no_flow_stats = True
271 LOG.info('VxLAN or MPLS: vlan_tagging forced to False '
272 '(inner VLAN tagging must be disabled)')
274 self.config_plugin.validate_config(config, self.specs.openstack)
277 def _parse_opts_from_cli():
278 parser = argparse.ArgumentParser()
280 parser.add_argument('--status', dest='status',
283 help='Provide NFVbench status')
285 parser.add_argument('-c', '--config', dest='config',
287 help='Override default values with a config file or '
288 'a yaml/json config string',
289 metavar='<file_name_or_yaml>')
291 parser.add_argument('--server', dest='server',
294 help='Run nfvbench in server mode')
296 parser.add_argument('--host', dest='host',
299 help='Host IP address on which server will be listening (default 0.0.0.0)')
301 parser.add_argument('-p', '--port', dest='port',
304 help='Port on which server will be listening (default 7555)')
306 parser.add_argument('-sc', '--service-chain', dest='service_chain',
307 choices=ChainType.names,
309 help='Service chain to run')
311 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
313 help='Set number of service chains to run',
314 metavar='<service_chain_count>')
316 parser.add_argument('-fc', '--flow-count', dest='flow_count',
318 help='Set number of total flows for all chains and all directions',
319 metavar='<flow_count>')
321 parser.add_argument('--rate', dest='rate',
323 help='Specify rate in pps, bps or %% as total for all directions',
326 parser.add_argument('--duration', dest='duration_sec',
328 help='Set duration to run traffic generator (in seconds)',
329 metavar='<duration_sec>')
331 parser.add_argument('--interval', dest='interval_sec',
333 help='Set interval to record traffic generator stats (in seconds)',
334 metavar='<interval_sec>')
336 parser.add_argument('--inter-node', dest='inter_node',
341 parser.add_argument('--sriov', dest='sriov',
344 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
346 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
349 help='Use SRIOV to handle the middle network traffic '
350 '(PVVP with SRIOV only)')
352 parser.add_argument('-d', '--debug', dest='debug',
355 help='print debug messages (verbose)')
357 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
359 help='Traffic generator profile to use')
361 parser.add_argument('-l3', '--l3-router', dest='l3_router',
364 help='Use L3 neutron routers to handle traffic')
366 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
369 help='Check config and connectivity only - do not generate traffic')
371 parser.add_argument('--no-arp', dest='no_arp',
374 help='Do not use ARP to find MAC addresses, '
375 'instead use values in config file')
377 parser.add_argument('--loop-vm-arp', dest='loop_vm_arp',
380 help='Use ARP to find MAC addresses '
381 'instead of using values from TRex ports (VPP forwarder only)')
383 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
386 help='Skip vswitch configuration and retrieving of stats')
388 parser.add_argument('--vxlan', dest='vxlan',
391 help='Enable VxLan encapsulation')
393 parser.add_argument('--mpls', dest='mpls',
396 help='Enable MPLS encapsulation')
398 parser.add_argument('--no-cleanup', dest='no_cleanup',
401 help='no cleanup after run')
403 parser.add_argument('--cleanup', dest='cleanup',
406 help='Cleanup NFVbench resources (prompt to confirm)')
408 parser.add_argument('--force-cleanup', dest='force_cleanup',
411 help='Cleanup NFVbench resources (do not prompt)')
413 parser.add_argument('--restart', dest='restart',
416 help='Restart TRex server')
418 parser.add_argument('--json', dest='json',
420 help='store results in json format file',
421 metavar='<path>/<filename>')
423 parser.add_argument('--std-json', dest='std_json',
425 help='store results in json format file with nfvbench standard filename: '
426 '<service-chain-type>-<service-chain-count>-<flow-count>'
427 '-<packet-sizes>.json',
430 parser.add_argument('--show-default-config', dest='show_default_config',
433 help='print the default config in yaml format (unedited)')
435 parser.add_argument('--show-config', dest='show_config',
438 help='print the running config in json format')
440 parser.add_argument('-ss', '--show-summary', dest='summary',
442 help='Show summary from nfvbench json file',
445 parser.add_argument('-v', '--version', dest='version',
450 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
452 help='Override traffic profile frame sizes',
453 metavar='<frame_size_bytes or IMIX>')
455 parser.add_argument('--unidir', dest='unidir',
458 help='Override traffic profile direction (requires -fs)')
460 parser.add_argument('--log-file', '--logfile', dest='log_file',
462 help='Filename for saving logs',
463 metavar='<log_file>')
465 parser.add_argument('--user-label', '--userlabel', dest='user_label',
467 help='Custom label for performance records')
469 parser.add_argument('--hypervisor', dest='hypervisor',
471 metavar='<hypervisor name>',
472 help='Where chains must run ("compute", "az:", "az:compute")')
474 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
477 help='Port to port or port to switch to port L2 loopback with VLAN id')
479 parser.add_argument('--cache-size', dest='cache_size',
482 help='Specify the FE cache size (default: 0, flow-count if < 0)')
484 parser.add_argument('--service-mode', dest='service_mode',
487 help='Enable T-Rex service mode for debugging only')
489 parser.add_argument('--no-flow-stats', dest='no_flow_stats',
492 help='Disable extra flow stats (on high load traffic)')
494 parser.add_argument('--no-latency-stats', dest='no_latency_stats',
497 help='Disable flow stats for latency traffic')
499 parser.add_argument('--no-latency-streams', dest='no_latency_streams',
502 help='Disable latency measurements (no streams)')
504 opts, unknown_opts = parser.parse_known_args()
505 return opts, unknown_opts
508 def load_default_config():
509 default_cfg = resource_string(__name__, "cfg.default.yaml")
510 config = config_loads(default_cfg)
511 config.name = '(built-in default config)'
512 return config, default_cfg
515 def override_custom_traffic(config, frame_sizes, unidir):
516 """Override the traffic profiles with a custom one."""
517 if frame_sizes is not None:
518 traffic_profile_name = "custom_traffic_profile"
519 config.traffic_profile = [
521 "l2frame_size": frame_sizes,
522 "name": traffic_profile_name
526 traffic_profile_name = config.traffic["profile"]
528 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
530 "bidirectional": bidirectional,
531 "profile": traffic_profile_name
535 def check_physnet(name, netattrs):
536 if not netattrs.physical_network:
537 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
539 if not netattrs.segmentation_id:
540 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
543 def status_cleanup(config, cleanup, force_cleanup):
544 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
545 # check if another run is pending
548 with utils.RunLock():
549 LOG.info('Status: idle')
551 LOG.info('Status: busy (run pending)')
553 # check nfvbench resources
554 if config.openrc_file and config.service_chain != ChainType.EXT:
555 cleaner = Cleaner(config)
556 count = cleaner.show_resources()
557 if count and (cleanup or force_cleanup):
558 cleaner.clean(not force_cleanup)
563 run_summary_required = False
566 # load default config file
567 config, default_cfg = load_default_config()
568 # create factory for platform specific classes
570 factory_module = importlib.import_module(config['factory_module'])
571 factory = getattr(factory_module, config['factory_class'])()
572 except AttributeError:
573 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
574 .format(m=config['factory_module'], c=config['factory_class']))
575 # create config plugin for this platform
576 config_plugin = factory.get_config_plugin_class()(config)
577 config = config_plugin.get_config()
579 opts, unknown_opts = _parse_opts_from_cli()
580 log.set_level(debug=opts.debug)
583 print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
587 with open(opts.summary) as json_data:
588 result = json.load(json_data)
590 result['config']['user_label'] = opts.user_label
591 print((NFVBenchSummarizer(result, fluent_logger)))
594 # show default config in text/yaml format
595 if opts.show_default_config:
596 print((default_cfg.decode("utf-8")))
601 # do not check extra_specs in flavor as it can contain any key/value pairs
602 whitelist_keys = ['extra_specs']
603 # override default config options with start config at path parsed from CLI
604 # check if it is an inline yaml/json config or a file name
605 if os.path.isfile(opts.config):
606 LOG.info('Loading configuration file: %s', opts.config)
607 config = config_load(opts.config, config, whitelist_keys)
608 config.name = os.path.basename(opts.config)
610 LOG.info('Loading configuration string: %s', opts.config)
611 config = config_loads(opts.config, config, whitelist_keys)
613 # setup the fluent logger as soon as possible right after the config plugin is called,
614 # if there is any logging or result tag is set then initialize the fluent logger
615 for fluentd in config.fluentd:
616 if fluentd.logging_tag or fluentd.result_tag:
617 fluent_logger = FluentLogHandler(config.fluentd)
618 LOG.addHandler(fluent_logger)
621 # traffic profile override options
622 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
624 # copy over cli options that are used in config
625 config.generator_profile = opts.generator_profile
629 config.log_file = opts.log_file
630 if opts.service_chain:
631 config.service_chain = opts.service_chain
632 if opts.service_chain_count:
633 config.service_chain_count = opts.service_chain_count
634 if opts.no_vswitch_access:
635 config.no_vswitch_access = opts.no_vswitch_access
637 # can be any of 'comp1', 'nova:', 'nova:comp1'
638 config.compute_nodes = opts.hypervisor
644 config.restart = True
645 if opts.service_mode:
646 config.service_mode = True
647 if opts.no_flow_stats:
648 config.no_flow_stats = True
649 if opts.no_latency_stats:
650 config.no_latency_stats = True
651 if opts.no_latency_streams:
652 config.no_latency_streams = True
653 # port to port loopback (direct or through switch)
655 config.l2_loopback = True
656 if config.service_chain != ChainType.EXT:
657 LOG.info('Changing service chain type to EXT')
658 config.service_chain = ChainType.EXT
659 if not config.no_arp:
660 LOG.info('Disabling ARP')
662 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
663 LOG.info('Running L2 loopback: using EXT chain/no ARP')
665 if opts.use_sriov_middle_net:
666 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
667 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
668 config.use_sriov_middle_net = True
670 if config.sriov and config.service_chain != ChainType.EXT:
671 # if sriov is requested (does not apply to ext chains)
672 # make sure the physnet names are specified
673 check_physnet("left", config.internal_networks.left)
674 check_physnet("right", config.internal_networks.right)
675 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
676 check_physnet("middle", config.internal_networks.middle)
678 # show running config in json format
680 print((json.dumps(config, sort_keys=True, indent=4)))
683 # update the config in the config plugin as it might have changed
684 # in a copy of the dict (config plugin still holds the original dict)
685 config_plugin.set_config(config)
687 if opts.status or opts.cleanup or opts.force_cleanup:
688 status_cleanup(config, opts.cleanup, opts.force_cleanup)
690 # add file log if requested
692 log.add_file_logger(config.log_file)
694 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
697 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
700 server = WebServer(nfvbench_instance, fluent_logger)
702 port = int(opts.port)
704 server.run(host=opts.host)
706 server.run(host=opts.host, port=port)
707 # server.run() should never return
709 with utils.RunLock():
710 run_summary_required = True
712 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
714 raise Exception(err_msg)
716 # remove unfilled values
717 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
719 params = ' '.join(str(e) for e in sys.argv[1:])
720 result = nfvbench_instance.run(opts, params)
721 if 'error_message' in result:
722 raise Exception(result['error_message'])
724 if 'result' in result and result['status']:
725 nfvbench_instance.save(result['result'])
726 nfvbench_instance.prepare_summary(result['result'])
727 except Exception as exc:
728 run_summary_required = True
730 'status': NFVBench.STATUS_ERROR,
731 'error_message': traceback.format_exc()
736 # only send a summary record if there was an actual nfvbench run or
737 # if an error/exception was logged.
738 fluent_logger.send_run_summary(run_summary_required)
741 if __name__ == '__main__':