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)
92 # check that an empty openrc file (no OpenStack) is only allowed
94 if not self.config.openrc_file and self.config.service_chain != ChainType.EXT:
95 raise Exception("openrc_file in the configuration is required for PVP/PVVP chains")
97 self.specs.set_run_spec(self.config_plugin.get_run_spec(self.config,
98 self.specs.openstack))
99 self.chain_runner = ChainRunner(self.config,
105 # make sure that the min frame size is 64
107 for frame_size in self.config.frame_sizes:
109 if int(frame_size) < min_packet_size:
110 frame_size = str(min_packet_size)
111 LOG.info("Adjusting frame size %s bytes to minimum size %s bytes",
112 frame_size, min_packet_size)
113 if frame_size not in new_frame_sizes:
114 new_frame_sizes.append(frame_size)
116 new_frame_sizes.append(frame_size.upper())
117 self.config.frame_sizes = new_frame_sizes
119 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
120 "nfvbench_version": __version__,
121 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
124 "service_chain": self.chain_runner.run(),
125 "versions": self.chain_runner.get_version(),
129 if self.specs.openstack:
130 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
131 "encaps": self.specs.openstack.encaps}
132 result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
134 status = NFVBench.STATUS_ERROR
135 message = traceback.format_exc()
136 except KeyboardInterrupt:
137 status = NFVBench.STATUS_ERROR
138 message = traceback.format_exc()
140 if self.chain_runner:
141 self.chain_runner.close()
143 if status == NFVBench.STATUS_OK:
144 # result2 = utils.dict_to_json_dict(result)
151 'error_message': message
154 def prepare_summary(self, result):
155 """Prepare summary of the result to print and send it to logger (eg: fluentd)."""
157 summary = NFVBenchSummarizer(result, fluent_logger)
158 LOG.info(str(summary))
160 def save(self, result):
161 """Save results in json format file."""
162 utils.save_json_result(result,
163 self.config.json_file,
164 self.config.std_json_path,
165 self.config.service_chain,
166 self.config.service_chain_count,
167 self.config.flow_count,
168 self.config.frame_sizes,
170 self.config.group_id)
172 def _update_config(self, opts):
173 """Recalculate the running config based on the base config and opts.
175 Sanity check on the config is done here as well.
177 self.config = AttrDict(dict(self.base_config))
178 # Update log file handler if needed after a config update (REST mode)
179 if 'log_file' in opts:
181 (path, _filename) = os.path.split(opts['log_file'])
182 if not os.path.exists(path):
184 'Path %s does not exist. Please verify root path is shared with host. Path '
185 'will be created.', path)
187 LOG.info('%s is created.', path)
188 for h in log.getLogger().handlers:
189 if isinstance(h, FileHandler) and h.baseFilename != opts['log_file']:
190 # clean log file handler
191 log.getLogger().removeHandler(h)
192 # add handler if not existing to avoid duplicates handlers
193 if len(log.getLogger().handlers) == 1:
194 log.add_file_logger(opts['log_file'])
196 self.config.update(opts)
199 config.service_chain = config.service_chain.upper()
200 config.service_chain_count = int(config.service_chain_count)
201 if config.l2_loopback:
202 # force the number of chains to be 1 in case of l2 loopback
203 config.service_chain_count = 1
204 config.service_chain = ChainType.EXT
206 LOG.info('Running L2 loopback: using EXT chain/no ARP')
208 # traffic profile override options
209 if 'frame_sizes' in opts:
212 unidir = opts['unidir']
213 override_custom_traffic(config, opts['frame_sizes'], unidir)
214 LOG.info("Frame size has been set to %s for current configuration", opts['frame_sizes'])
216 config.flow_count = utils.parse_flow_count(config.flow_count)
217 required_flow_count = config.service_chain_count * 2
218 if config.flow_count < required_flow_count:
219 LOG.info("Flow count %d has been set to minimum value of '%d' "
220 "for current configuration", config.flow_count,
222 config.flow_count = required_flow_count
224 if config.flow_count % 2:
225 config.flow_count += 1
227 # Possibly adjust the cache size
228 if config.cache_size < 0:
229 config.cache_size = config.flow_count
231 # The size must be capped to 10000 (where does this limit come from?)
232 if config.cache_size > 10000:
233 config.cache_size = 10000
235 config.duration_sec = float(config.duration_sec)
236 config.interval_sec = float(config.interval_sec)
237 config.pause_sec = float(config.pause_sec)
239 if config.traffic is None or not config.traffic:
240 raise Exception("Missing traffic property in configuration")
242 if config.openrc_file:
243 config.openrc_file = os.path.expanduser(config.openrc_file)
244 if config.flavor.vcpus < 2:
245 raise Exception("Flavor vcpus must be >= 2")
247 config.ndr_run = (not config.no_traffic and
248 'ndr' in config.rate.strip().lower().split('_'))
249 config.pdr_run = (not config.no_traffic and
250 'pdr' in config.rate.strip().lower().split('_'))
251 config.single_run = (not config.no_traffic and
252 not (config.ndr_run or config.pdr_run))
254 config.json_file = config.json if config.json else None
256 (path, _filename) = os.path.split(config.json)
257 if not os.path.exists(path):
258 raise Exception('Please provide existing path for storing results in JSON file. '
259 'Path used: {path}'.format(path=path))
261 config.std_json_path = config.std_json if config.std_json else None
262 if config.std_json_path:
263 if not os.path.exists(config.std_json):
264 raise Exception('Please provide existing path for storing results in JSON file. '
265 'Path used: {path}'.format(path=config.std_json_path))
267 # Check that multiqueue is between 1 and 8 (8 is the max allowed by libvirt/qemu)
268 if config.vif_multiqueue_size < 1 or config.vif_multiqueue_size > 8:
269 raise Exception('vif_multiqueue_size (%d) must be in [1..8]' %
270 config.vif_multiqueue_size)
272 # VxLAN and MPLS sanity checks
273 if config.vxlan or config.mpls:
274 if config.vlan_tagging:
275 config.vlan_tagging = False
276 config.no_latency_streams = True
277 config.no_latency_stats = True
278 config.no_flow_stats = True
279 LOG.info('VxLAN or MPLS: vlan_tagging forced to False '
280 '(inner VLAN tagging must be disabled)')
282 self.config_plugin.validate_config(config, self.specs.openstack)
286 """Argument type to be used in parser.add_argument()
287 When a boolean like value is expected to be given
289 return (str(x).lower() != 'false') \
290 and (str(x).lower() != 'no') \
291 and (str(x).lower() != '0')
295 """Argument type to be used in parser.add_argument()
296 When an integer type value is expected to be given
297 (returns 0 if argument is invalid, hexa accepted)
302 def _parse_opts_from_cli():
303 parser = argparse.ArgumentParser()
305 parser.add_argument('--status', dest='status',
308 help='Provide NFVbench status')
310 parser.add_argument('-c', '--config', dest='config',
312 help='Override default values with a config file or '
313 'a yaml/json config string',
314 metavar='<file_name_or_yaml>')
316 parser.add_argument('--server', dest='server',
319 help='Run nfvbench in server mode')
321 parser.add_argument('--host', dest='host',
324 help='Host IP address on which server will be listening (default 0.0.0.0)')
326 parser.add_argument('-p', '--port', dest='port',
329 help='Port on which server will be listening (default 7555)')
331 parser.add_argument('-sc', '--service-chain', dest='service_chain',
332 choices=ChainType.names,
334 help='Service chain to run')
336 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
338 help='Set number of service chains to run',
339 metavar='<service_chain_count>')
341 parser.add_argument('-fc', '--flow-count', dest='flow_count',
343 help='Set number of total flows for all chains and all directions',
344 metavar='<flow_count>')
346 parser.add_argument('--rate', dest='rate',
348 help='Specify rate in pps, bps or %% as total for all directions',
351 parser.add_argument('--duration', dest='duration_sec',
353 help='Set duration to run traffic generator (in seconds)',
354 metavar='<duration_sec>')
356 parser.add_argument('--interval', dest='interval_sec',
358 help='Set interval to record traffic generator stats (in seconds)',
359 metavar='<interval_sec>')
361 parser.add_argument('--inter-node', dest='inter_node',
366 parser.add_argument('--sriov', dest='sriov',
369 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
371 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
374 help='Use SRIOV to handle the middle network traffic '
375 '(PVVP with SRIOV only)')
377 parser.add_argument('-d', '--debug', dest='debug',
380 help='print debug messages (verbose)')
382 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
384 help='Traffic generator profile to use')
386 parser.add_argument('-l3', '--l3-router', dest='l3_router',
389 help='Use L3 neutron routers to handle traffic')
391 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
394 help='Check config and connectivity only - do not generate traffic')
396 parser.add_argument('--no-arp', dest='no_arp',
399 help='Do not use ARP to find MAC addresses, '
400 'instead use values in config file')
402 parser.add_argument('--loop-vm-arp', dest='loop_vm_arp',
405 help='Use ARP to find MAC addresses '
406 'instead of using values from TRex ports (VPP forwarder only)')
408 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
411 help='Skip vswitch configuration and retrieving of stats')
413 parser.add_argument('--vxlan', dest='vxlan',
416 help='Enable VxLan encapsulation')
418 parser.add_argument('--mpls', dest='mpls',
421 help='Enable MPLS encapsulation')
423 parser.add_argument('--no-cleanup', dest='no_cleanup',
426 help='no cleanup after run')
428 parser.add_argument('--cleanup', dest='cleanup',
431 help='Cleanup NFVbench resources (prompt to confirm)')
433 parser.add_argument('--force-cleanup', dest='force_cleanup',
436 help='Cleanup NFVbench resources (do not prompt)')
438 parser.add_argument('--restart', dest='restart',
441 help='Restart TRex server')
443 parser.add_argument('--json', dest='json',
445 help='store results in json format file',
446 metavar='<path>/<filename>')
448 parser.add_argument('--std-json', dest='std_json',
450 help='store results in json format file with nfvbench standard filename: '
451 '<service-chain-type>-<service-chain-count>-<flow-count>'
452 '-<packet-sizes>.json',
455 parser.add_argument('--show-default-config', dest='show_default_config',
458 help='print the default config in yaml format (unedited)')
460 parser.add_argument('--show-config', dest='show_config',
463 help='print the running config in json format')
465 parser.add_argument('-ss', '--show-summary', dest='summary',
467 help='Show summary from nfvbench json file',
470 parser.add_argument('-v', '--version', dest='version',
475 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
477 help='Override traffic profile frame sizes',
478 metavar='<frame_size_bytes or IMIX>')
480 parser.add_argument('--unidir', dest='unidir',
483 help='Override traffic profile direction (requires -fs)')
485 parser.add_argument('--log-file', '--logfile', dest='log_file',
487 help='Filename for saving logs',
488 metavar='<log_file>')
490 parser.add_argument('--user-label', '--userlabel', dest='user_label',
492 help='Custom label for performance records')
494 parser.add_argument('--hypervisor', dest='hypervisor',
496 metavar='<hypervisor name>',
497 help='Where chains must run ("compute", "az:", "az:compute")')
499 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
502 help='Port to port or port to switch to port L2 loopback with VLAN id')
504 parser.add_argument('--user-info', dest='user_info',
507 help='Custom data to be included as is in the json report config branch - '
508 + ' example, pay attention! no space: '
509 + '--user-info=\'{"status":"explore","description":'
510 + '{"target":"lab","ok":true,"version":2020}}\'')
512 parser.add_argument('--vlan-tagging', dest='vlan_tagging',
517 help='Override the NFVbench \'vlan_tagging\' parameter')
519 parser.add_argument('--intf-speed', dest='intf_speed',
523 help='Override the NFVbench \'intf_speed\' '
524 + 'parameter (e.g. 10Gbps, auto, 16.72Gbps)')
526 parser.add_argument('--cores', dest='cores',
531 help='Override the T-Rex \'cores\' parameter')
533 parser.add_argument('--cache-size', dest='cache_size',
538 help='Specify the FE cache size (default: 0, flow-count if < 0)')
540 parser.add_argument('--service-mode', dest='service_mode',
543 help='Enable T-Rex service mode (for debugging purpose)')
545 parser.add_argument('--no-e2e-check', dest='no_e2e_check',
548 help='Skip "end to end" connectivity check (on test purpose)')
550 parser.add_argument('--no-flow-stats', dest='no_flow_stats',
553 help='Disable additional flow stats (on high load traffic)')
555 parser.add_argument('--no-latency-stats', dest='no_latency_stats',
558 help='Disable flow stats for latency traffic')
560 parser.add_argument('--no-latency-streams', dest='no_latency_streams',
563 help='Disable latency measurements (no streams)')
565 parser.add_argument('--user-id', dest='user_id',
570 help='Change json/log files ownership with this user (int)')
572 parser.add_argument('--group-id', dest='group_id',
577 help='Change json/log files ownership with this group (int)')
579 parser.add_argument('--show-trex-log', dest='show_trex_log',
582 help='Show the current TRex local server log file contents'
583 + ' => diagnostic/help in case of configuration problems')
585 parser.add_argument('--debug-mask', dest='debug_mask',
589 default='0x00000000',
590 help='General purpose register (debugging flags), '
591 + 'the hexadecimal notation (0x...) is accepted.'
592 + 'Designed for development needs.')
594 opts, unknown_opts = parser.parse_known_args()
595 return opts, unknown_opts
598 def load_default_config():
599 default_cfg = resource_string(__name__, "cfg.default.yaml")
600 config = config_loads(default_cfg)
601 config.name = '(built-in default config)'
602 return config, default_cfg
605 def override_custom_traffic(config, frame_sizes, unidir):
606 """Override the traffic profiles with a custom one."""
607 if frame_sizes is not None:
608 traffic_profile_name = "custom_traffic_profile"
609 config.traffic_profile = [
611 "l2frame_size": frame_sizes,
612 "name": traffic_profile_name
616 traffic_profile_name = config.traffic["profile"]
618 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
620 "bidirectional": bidirectional,
621 "profile": traffic_profile_name
625 def check_physnet(name, netattrs):
626 if not netattrs.physical_network:
627 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
629 if not netattrs.segmentation_id:
630 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
633 def status_cleanup(config, cleanup, force_cleanup):
634 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
635 # check if another run is pending
638 with utils.RunLock():
639 LOG.info('Status: idle')
641 LOG.info('Status: busy (run pending)')
643 # check nfvbench resources
644 if config.openrc_file and config.service_chain != ChainType.EXT:
645 cleaner = Cleaner(config)
646 count = cleaner.show_resources()
647 if count and (cleanup or force_cleanup):
648 cleaner.clean(not force_cleanup)
653 run_summary_required = False
656 # load default config file
657 config, default_cfg = load_default_config()
658 # possibly override the default user_id & group_id values
659 if 'USER_ID' in os.environ:
660 config.user_id = int(os.environ['USER_ID'])
661 if 'GROUP_ID' in os.environ:
662 config.group_id = int(os.environ['GROUP_ID'])
664 # create factory for platform specific classes
666 factory_module = importlib.import_module(config['factory_module'])
667 factory = getattr(factory_module, config['factory_class'])()
668 except AttributeError:
669 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
670 .format(m=config['factory_module'],
671 c=config['factory_class'])) from AttributeError
672 # create config plugin for this platform
673 config_plugin = factory.get_config_plugin_class()(config)
674 config = config_plugin.get_config()
676 opts, unknown_opts = _parse_opts_from_cli()
677 log.set_level(debug=opts.debug)
680 print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
684 with open(opts.summary) as json_data:
685 result = json.load(json_data)
687 result['config']['user_label'] = opts.user_label
688 print((NFVBenchSummarizer(result, fluent_logger)))
691 # show default config in text/yaml format
692 if opts.show_default_config:
693 print((default_cfg.decode("utf-8")))
696 # dump the contents of the trex log file
697 if opts.show_trex_log:
699 print(open('/tmp/trex.log').read(), end="")
700 except FileNotFoundError:
701 print("No TRex log file found!")
706 # do not check extra_specs in flavor as it can contain any key/value pairs
707 # the same principle applies also to the optional user_info open property
708 whitelist_keys = ['extra_specs', 'user_info']
709 # override default config options with start config at path parsed from CLI
710 # check if it is an inline yaml/json config or a file name
711 if os.path.isfile(opts.config):
712 LOG.info('Loading configuration file: %s', opts.config)
713 config = config_load(opts.config, config, whitelist_keys)
714 config.name = os.path.basename(opts.config)
716 LOG.info('Loading configuration string: %s', opts.config)
717 config = config_loads(opts.config, config, whitelist_keys)
719 # setup the fluent logger as soon as possible right after the config plugin is called,
720 # if there is any logging or result tag is set then initialize the fluent logger
721 for fluentd in config.fluentd:
722 if fluentd.logging_tag or fluentd.result_tag:
723 fluent_logger = FluentLogHandler(config.fluentd)
724 LOG.addHandler(fluent_logger)
727 # convert 'user_info' opt from json string to dictionnary
728 # and merge the result with the current config dictionnary
730 opts.user_info = json.loads(opts.user_info)
732 config.user_info = config.user_info + opts.user_info
734 config.user_info = opts.user_info
735 # hide the option to further _update_config()
736 opts.user_info = None
738 # traffic profile override options
739 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
741 # copy over cli options that are used in config
742 config.generator_profile = opts.generator_profile
746 config.log_file = opts.log_file
747 if opts.service_chain:
748 config.service_chain = opts.service_chain
749 if opts.service_chain_count:
750 config.service_chain_count = opts.service_chain_count
751 if opts.no_vswitch_access:
752 config.no_vswitch_access = opts.no_vswitch_access
754 # can be any of 'comp1', 'nova:', 'nova:comp1'
755 config.compute_nodes = opts.hypervisor
761 config.restart = True
762 if opts.service_mode:
763 config.service_mode = True
764 if opts.no_flow_stats:
765 config.no_flow_stats = True
766 if opts.no_latency_stats:
767 config.no_latency_stats = True
768 if opts.no_latency_streams:
769 config.no_latency_streams = True
770 # port to port loopback (direct or through switch)
772 config.l2_loopback = True
773 if config.service_chain != ChainType.EXT:
774 LOG.info('Changing service chain type to EXT')
775 config.service_chain = ChainType.EXT
776 if not config.no_arp:
777 LOG.info('Disabling ARP')
779 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
780 LOG.info('Running L2 loopback: using EXT chain/no ARP')
782 if opts.use_sriov_middle_net:
783 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
784 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
785 config.use_sriov_middle_net = True
787 if config.sriov and config.service_chain != ChainType.EXT:
788 # if sriov is requested (does not apply to ext chains)
789 # make sure the physnet names are specified
790 check_physnet("left", config.internal_networks.left)
791 check_physnet("right", config.internal_networks.right)
792 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
793 check_physnet("middle", config.internal_networks.middle)
795 # show running config in json format
797 print((json.dumps(config, sort_keys=True, indent=4)))
800 # update the config in the config plugin as it might have changed
801 # in a copy of the dict (config plugin still holds the original dict)
802 config_plugin.set_config(config)
804 if opts.status or opts.cleanup or opts.force_cleanup:
805 status_cleanup(config, opts.cleanup, opts.force_cleanup)
807 # add file log if requested
809 log.add_file_logger(config.log_file)
810 # possibly change file ownership
812 gid = config.group_id
816 os.chown(config.log_file, uid, gid)
818 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
821 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
824 server = WebServer(nfvbench_instance, fluent_logger)
826 port = int(opts.port)
828 server.run(host=opts.host)
830 server.run(host=opts.host, port=port)
831 # server.run() should never return
833 with utils.RunLock():
834 run_summary_required = True
836 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
838 raise Exception(err_msg)
840 # remove unfilled values
841 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
843 params = ' '.join(str(e) for e in sys.argv[1:])
844 result = nfvbench_instance.run(opts, params)
845 if 'error_message' in result:
846 raise Exception(result['error_message'])
848 if 'result' in result and result['status']:
849 nfvbench_instance.save(result['result'])
850 nfvbench_instance.prepare_summary(result['result'])
851 except Exception as exc:
852 run_summary_required = True
854 'status': NFVBench.STATUS_ERROR,
855 'error_message': traceback.format_exc()
860 # only send a summary record if there was an actual nfvbench run or
861 # if an error/exception was logged.
862 fluent_logger.send_run_summary(run_summary_required)
865 if __name__ == '__main__':