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)
278 """Argument type to be used in parser.add_argument()
279 When a boolean like value is expected to be given
281 return (str(x).lower() != 'false') \
282 and (str(x).lower() != 'no') \
283 and (str(x).lower() != '0')
287 """Argument type to be used in parser.add_argument()
288 When an integer type value is expected to be given
289 (returns 0 if argument is invalid, hexa accepted)
294 def _parse_opts_from_cli():
295 parser = argparse.ArgumentParser()
297 parser.add_argument('--status', dest='status',
300 help='Provide NFVbench status')
302 parser.add_argument('-c', '--config', dest='config',
304 help='Override default values with a config file or '
305 'a yaml/json config string',
306 metavar='<file_name_or_yaml>')
308 parser.add_argument('--server', dest='server',
311 help='Run nfvbench in server mode')
313 parser.add_argument('--host', dest='host',
316 help='Host IP address on which server will be listening (default 0.0.0.0)')
318 parser.add_argument('-p', '--port', dest='port',
321 help='Port on which server will be listening (default 7555)')
323 parser.add_argument('-sc', '--service-chain', dest='service_chain',
324 choices=ChainType.names,
326 help='Service chain to run')
328 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
330 help='Set number of service chains to run',
331 metavar='<service_chain_count>')
333 parser.add_argument('-fc', '--flow-count', dest='flow_count',
335 help='Set number of total flows for all chains and all directions',
336 metavar='<flow_count>')
338 parser.add_argument('--rate', dest='rate',
340 help='Specify rate in pps, bps or %% as total for all directions',
343 parser.add_argument('--duration', dest='duration_sec',
345 help='Set duration to run traffic generator (in seconds)',
346 metavar='<duration_sec>')
348 parser.add_argument('--interval', dest='interval_sec',
350 help='Set interval to record traffic generator stats (in seconds)',
351 metavar='<interval_sec>')
353 parser.add_argument('--inter-node', dest='inter_node',
358 parser.add_argument('--sriov', dest='sriov',
361 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
363 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
366 help='Use SRIOV to handle the middle network traffic '
367 '(PVVP with SRIOV only)')
369 parser.add_argument('-d', '--debug', dest='debug',
372 help='print debug messages (verbose)')
374 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
376 help='Traffic generator profile to use')
378 parser.add_argument('-l3', '--l3-router', dest='l3_router',
381 help='Use L3 neutron routers to handle traffic')
383 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
386 help='Check config and connectivity only - do not generate traffic')
388 parser.add_argument('--no-arp', dest='no_arp',
391 help='Do not use ARP to find MAC addresses, '
392 'instead use values in config file')
394 parser.add_argument('--loop-vm-arp', dest='loop_vm_arp',
397 help='Use ARP to find MAC addresses '
398 'instead of using values from TRex ports (VPP forwarder only)')
400 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
403 help='Skip vswitch configuration and retrieving of stats')
405 parser.add_argument('--vxlan', dest='vxlan',
408 help='Enable VxLan encapsulation')
410 parser.add_argument('--mpls', dest='mpls',
413 help='Enable MPLS encapsulation')
415 parser.add_argument('--no-cleanup', dest='no_cleanup',
418 help='no cleanup after run')
420 parser.add_argument('--cleanup', dest='cleanup',
423 help='Cleanup NFVbench resources (prompt to confirm)')
425 parser.add_argument('--force-cleanup', dest='force_cleanup',
428 help='Cleanup NFVbench resources (do not prompt)')
430 parser.add_argument('--restart', dest='restart',
433 help='Restart TRex server')
435 parser.add_argument('--json', dest='json',
437 help='store results in json format file',
438 metavar='<path>/<filename>')
440 parser.add_argument('--std-json', dest='std_json',
442 help='store results in json format file with nfvbench standard filename: '
443 '<service-chain-type>-<service-chain-count>-<flow-count>'
444 '-<packet-sizes>.json',
447 parser.add_argument('--show-default-config', dest='show_default_config',
450 help='print the default config in yaml format (unedited)')
452 parser.add_argument('--show-config', dest='show_config',
455 help='print the running config in json format')
457 parser.add_argument('-ss', '--show-summary', dest='summary',
459 help='Show summary from nfvbench json file',
462 parser.add_argument('-v', '--version', dest='version',
467 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
469 help='Override traffic profile frame sizes',
470 metavar='<frame_size_bytes or IMIX>')
472 parser.add_argument('--unidir', dest='unidir',
475 help='Override traffic profile direction (requires -fs)')
477 parser.add_argument('--log-file', '--logfile', dest='log_file',
479 help='Filename for saving logs',
480 metavar='<log_file>')
482 parser.add_argument('--user-label', '--userlabel', dest='user_label',
484 help='Custom label for performance records')
486 parser.add_argument('--hypervisor', dest='hypervisor',
488 metavar='<hypervisor name>',
489 help='Where chains must run ("compute", "az:", "az:compute")')
491 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
494 help='Port to port or port to switch to port L2 loopback with VLAN id')
496 """Option to allow for passing custom information to results post-processing"""
497 parser.add_argument('--user-info', dest='user_info',
500 help='Custom data to be included as is in the json report config branch - '
501 + ' example, pay attention! no space: '
502 + '--user-info=\'{"status":"explore","description":{"target":"lab"'
503 + ',"ok":true,"version":2020}\'')
505 """Option to allow for overriding the NFVbench 'vlan_tagging' option"""
506 parser.add_argument('--vlan-tagging', dest='vlan_tagging',
511 help='Override the NFVbench \'vlan_tagging\' parameter')
513 """Option to allow for overriding the T-Rex 'intf_speed' parameter"""
514 parser.add_argument('--intf-speed', dest='intf_speed',
518 help='Override the NFVbench \'intf_speed\' parameter '
519 + '(e.g. 10Gbps, auto, 16.72Gbps)')
521 """Option to allow for overriding the T-Rex 'cores' parameter"""
522 parser.add_argument('--cores', dest='cores',
527 help='Override the T-Rex \'cores\' parameter')
529 parser.add_argument('--cache-size', dest='cache_size',
532 help='Specify the FE cache size (default: 0, flow-count if < 0)')
534 parser.add_argument('--service-mode', dest='service_mode',
537 help='Enable T-Rex service mode for debugging only')
539 parser.add_argument('--no-flow-stats', dest='no_flow_stats',
542 help='Disable extra flow stats (on high load traffic)')
544 parser.add_argument('--no-latency-stats', dest='no_latency_stats',
547 help='Disable flow stats for latency traffic')
549 parser.add_argument('--no-latency-streams', dest='no_latency_streams',
552 help='Disable latency measurements (no streams)')
554 opts, unknown_opts = parser.parse_known_args()
555 return opts, unknown_opts
558 def load_default_config():
559 default_cfg = resource_string(__name__, "cfg.default.yaml")
560 config = config_loads(default_cfg)
561 config.name = '(built-in default config)'
562 return config, default_cfg
565 def override_custom_traffic(config, frame_sizes, unidir):
566 """Override the traffic profiles with a custom one."""
567 if frame_sizes is not None:
568 traffic_profile_name = "custom_traffic_profile"
569 config.traffic_profile = [
571 "l2frame_size": frame_sizes,
572 "name": traffic_profile_name
576 traffic_profile_name = config.traffic["profile"]
578 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
580 "bidirectional": bidirectional,
581 "profile": traffic_profile_name
585 def check_physnet(name, netattrs):
586 if not netattrs.physical_network:
587 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
589 if not netattrs.segmentation_id:
590 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
593 def status_cleanup(config, cleanup, force_cleanup):
594 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
595 # check if another run is pending
598 with utils.RunLock():
599 LOG.info('Status: idle')
601 LOG.info('Status: busy (run pending)')
603 # check nfvbench resources
604 if config.openrc_file and config.service_chain != ChainType.EXT:
605 cleaner = Cleaner(config)
606 count = cleaner.show_resources()
607 if count and (cleanup or force_cleanup):
608 cleaner.clean(not force_cleanup)
613 run_summary_required = False
616 # load default config file
617 config, default_cfg = load_default_config()
618 # create factory for platform specific classes
620 factory_module = importlib.import_module(config['factory_module'])
621 factory = getattr(factory_module, config['factory_class'])()
622 except AttributeError:
623 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
624 .format(m=config['factory_module'],
625 c=config['factory_class'])) from AttributeError
626 # create config plugin for this platform
627 config_plugin = factory.get_config_plugin_class()(config)
628 config = config_plugin.get_config()
630 opts, unknown_opts = _parse_opts_from_cli()
631 log.set_level(debug=opts.debug)
634 print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
638 with open(opts.summary) as json_data:
639 result = json.load(json_data)
641 result['config']['user_label'] = opts.user_label
642 print((NFVBenchSummarizer(result, fluent_logger)))
645 # show default config in text/yaml format
646 if opts.show_default_config:
647 print((default_cfg.decode("utf-8")))
652 # do not check extra_specs in flavor as it can contain any key/value pairs
653 # the same principle applies also to the optional user_info open property
654 whitelist_keys = ['extra_specs', 'user_info']
655 # override default config options with start config at path parsed from CLI
656 # check if it is an inline yaml/json config or a file name
657 if os.path.isfile(opts.config):
658 LOG.info('Loading configuration file: %s', opts.config)
659 config = config_load(opts.config, config, whitelist_keys)
660 config.name = os.path.basename(opts.config)
662 LOG.info('Loading configuration string: %s', opts.config)
663 config = config_loads(opts.config, config, whitelist_keys)
665 # setup the fluent logger as soon as possible right after the config plugin is called,
666 # if there is any logging or result tag is set then initialize the fluent logger
667 for fluentd in config.fluentd:
668 if fluentd.logging_tag or fluentd.result_tag:
669 fluent_logger = FluentLogHandler(config.fluentd)
670 LOG.addHandler(fluent_logger)
673 # convert 'user_info' opt from json string to dictionnary
674 # and merge the result with the current config dictionnary
676 opts.user_info = json.loads(opts.user_info)
678 config.user_info = config.user_info + opts.user_info
680 config.user_info = opts.user_info
681 # hide the option to further _update_config()
682 opts.user_info = None
684 # traffic profile override options
685 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
687 # copy over cli options that are used in config
688 config.generator_profile = opts.generator_profile
692 config.log_file = opts.log_file
693 if opts.service_chain:
694 config.service_chain = opts.service_chain
695 if opts.service_chain_count:
696 config.service_chain_count = opts.service_chain_count
697 if opts.no_vswitch_access:
698 config.no_vswitch_access = opts.no_vswitch_access
700 # can be any of 'comp1', 'nova:', 'nova:comp1'
701 config.compute_nodes = opts.hypervisor
707 config.restart = True
708 if opts.service_mode:
709 config.service_mode = True
710 if opts.no_flow_stats:
711 config.no_flow_stats = True
712 if opts.no_latency_stats:
713 config.no_latency_stats = True
714 if opts.no_latency_streams:
715 config.no_latency_streams = True
716 # port to port loopback (direct or through switch)
718 config.l2_loopback = True
719 if config.service_chain != ChainType.EXT:
720 LOG.info('Changing service chain type to EXT')
721 config.service_chain = ChainType.EXT
722 if not config.no_arp:
723 LOG.info('Disabling ARP')
725 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
726 LOG.info('Running L2 loopback: using EXT chain/no ARP')
728 if opts.use_sriov_middle_net:
729 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
730 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
731 config.use_sriov_middle_net = True
733 if config.sriov and config.service_chain != ChainType.EXT:
734 # if sriov is requested (does not apply to ext chains)
735 # make sure the physnet names are specified
736 check_physnet("left", config.internal_networks.left)
737 check_physnet("right", config.internal_networks.right)
738 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
739 check_physnet("middle", config.internal_networks.middle)
741 # show running config in json format
743 print((json.dumps(config, sort_keys=True, indent=4)))
746 # update the config in the config plugin as it might have changed
747 # in a copy of the dict (config plugin still holds the original dict)
748 config_plugin.set_config(config)
750 if opts.status or opts.cleanup or opts.force_cleanup:
751 status_cleanup(config, opts.cleanup, opts.force_cleanup)
753 # add file log if requested
755 log.add_file_logger(config.log_file)
757 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
760 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
763 server = WebServer(nfvbench_instance, fluent_logger)
765 port = int(opts.port)
767 server.run(host=opts.host)
769 server.run(host=opts.host, port=port)
770 # server.run() should never return
772 with utils.RunLock():
773 run_summary_required = True
775 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
777 raise Exception(err_msg)
779 # remove unfilled values
780 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
782 params = ' '.join(str(e) for e in sys.argv[1:])
783 result = nfvbench_instance.run(opts, params)
784 if 'error_message' in result:
785 raise Exception(result['error_message'])
787 if 'result' in result and result['status']:
788 nfvbench_instance.save(result['result'])
789 nfvbench_instance.prepare_summary(result['result'])
790 except Exception as exc:
791 run_summary_required = True
793 'status': NFVBench.STATUS_ERROR,
794 'error_message': traceback.format_exc()
799 # only send a summary record if there was an actual nfvbench run or
800 # if an error/exception was logged.
801 fluent_logger.send_run_summary(run_summary_required)
804 if __name__ == '__main__':