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('--debug-mask', dest='debug_mask',
583 default='0x00000000',
584 help='General purpose register (debugging flags), '
585 + 'the hexadecimal notation (0x...) is accepted.'
586 + 'Designed for development needs.')
588 opts, unknown_opts = parser.parse_known_args()
589 return opts, unknown_opts
592 def load_default_config():
593 default_cfg = resource_string(__name__, "cfg.default.yaml")
594 config = config_loads(default_cfg)
595 config.name = '(built-in default config)'
596 return config, default_cfg
599 def override_custom_traffic(config, frame_sizes, unidir):
600 """Override the traffic profiles with a custom one."""
601 if frame_sizes is not None:
602 traffic_profile_name = "custom_traffic_profile"
603 config.traffic_profile = [
605 "l2frame_size": frame_sizes,
606 "name": traffic_profile_name
610 traffic_profile_name = config.traffic["profile"]
612 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
614 "bidirectional": bidirectional,
615 "profile": traffic_profile_name
619 def check_physnet(name, netattrs):
620 if not netattrs.physical_network:
621 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
623 if not netattrs.segmentation_id:
624 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
627 def status_cleanup(config, cleanup, force_cleanup):
628 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
629 # check if another run is pending
632 with utils.RunLock():
633 LOG.info('Status: idle')
635 LOG.info('Status: busy (run pending)')
637 # check nfvbench resources
638 if config.openrc_file and config.service_chain != ChainType.EXT:
639 cleaner = Cleaner(config)
640 count = cleaner.show_resources()
641 if count and (cleanup or force_cleanup):
642 cleaner.clean(not force_cleanup)
647 run_summary_required = False
650 # load default config file
651 config, default_cfg = load_default_config()
652 # possibly override the default user_id & group_id values
653 if 'USER_ID' in os.environ:
654 config.user_id = int(os.environ['USER_ID'])
655 if 'GROUP_ID' in os.environ:
656 config.group_id = int(os.environ['GROUP_ID'])
658 # create factory for platform specific classes
660 factory_module = importlib.import_module(config['factory_module'])
661 factory = getattr(factory_module, config['factory_class'])()
662 except AttributeError:
663 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
664 .format(m=config['factory_module'],
665 c=config['factory_class'])) from AttributeError
666 # create config plugin for this platform
667 config_plugin = factory.get_config_plugin_class()(config)
668 config = config_plugin.get_config()
670 opts, unknown_opts = _parse_opts_from_cli()
671 log.set_level(debug=opts.debug)
674 print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
678 with open(opts.summary) as json_data:
679 result = json.load(json_data)
681 result['config']['user_label'] = opts.user_label
682 print((NFVBenchSummarizer(result, fluent_logger)))
685 # show default config in text/yaml format
686 if opts.show_default_config:
687 print((default_cfg.decode("utf-8")))
692 # do not check extra_specs in flavor as it can contain any key/value pairs
693 # the same principle applies also to the optional user_info open property
694 whitelist_keys = ['extra_specs', 'user_info']
695 # override default config options with start config at path parsed from CLI
696 # check if it is an inline yaml/json config or a file name
697 if os.path.isfile(opts.config):
698 LOG.info('Loading configuration file: %s', opts.config)
699 config = config_load(opts.config, config, whitelist_keys)
700 config.name = os.path.basename(opts.config)
702 LOG.info('Loading configuration string: %s', opts.config)
703 config = config_loads(opts.config, config, whitelist_keys)
705 # setup the fluent logger as soon as possible right after the config plugin is called,
706 # if there is any logging or result tag is set then initialize the fluent logger
707 for fluentd in config.fluentd:
708 if fluentd.logging_tag or fluentd.result_tag:
709 fluent_logger = FluentLogHandler(config.fluentd)
710 LOG.addHandler(fluent_logger)
713 # convert 'user_info' opt from json string to dictionnary
714 # and merge the result with the current config dictionnary
716 opts.user_info = json.loads(opts.user_info)
718 config.user_info = config.user_info + opts.user_info
720 config.user_info = opts.user_info
721 # hide the option to further _update_config()
722 opts.user_info = None
724 # traffic profile override options
725 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
727 # copy over cli options that are used in config
728 config.generator_profile = opts.generator_profile
732 config.log_file = opts.log_file
733 if opts.service_chain:
734 config.service_chain = opts.service_chain
735 if opts.service_chain_count:
736 config.service_chain_count = opts.service_chain_count
737 if opts.no_vswitch_access:
738 config.no_vswitch_access = opts.no_vswitch_access
740 # can be any of 'comp1', 'nova:', 'nova:comp1'
741 config.compute_nodes = opts.hypervisor
747 config.restart = True
748 if opts.service_mode:
749 config.service_mode = True
750 if opts.no_flow_stats:
751 config.no_flow_stats = True
752 if opts.no_latency_stats:
753 config.no_latency_stats = True
754 if opts.no_latency_streams:
755 config.no_latency_streams = True
756 # port to port loopback (direct or through switch)
758 config.l2_loopback = True
759 if config.service_chain != ChainType.EXT:
760 LOG.info('Changing service chain type to EXT')
761 config.service_chain = ChainType.EXT
762 if not config.no_arp:
763 LOG.info('Disabling ARP')
765 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
766 LOG.info('Running L2 loopback: using EXT chain/no ARP')
768 if opts.use_sriov_middle_net:
769 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
770 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
771 config.use_sriov_middle_net = True
773 if config.sriov and config.service_chain != ChainType.EXT:
774 # if sriov is requested (does not apply to ext chains)
775 # make sure the physnet names are specified
776 check_physnet("left", config.internal_networks.left)
777 check_physnet("right", config.internal_networks.right)
778 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
779 check_physnet("middle", config.internal_networks.middle)
781 # show running config in json format
783 print((json.dumps(config, sort_keys=True, indent=4)))
786 # update the config in the config plugin as it might have changed
787 # in a copy of the dict (config plugin still holds the original dict)
788 config_plugin.set_config(config)
790 if opts.status or opts.cleanup or opts.force_cleanup:
791 status_cleanup(config, opts.cleanup, opts.force_cleanup)
793 # add file log if requested
795 log.add_file_logger(config.log_file)
796 # possibly change file ownership
798 gid = config.group_id
802 os.chown(config.log_file, uid, gid)
804 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
807 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
810 server = WebServer(nfvbench_instance, fluent_logger)
812 port = int(opts.port)
814 server.run(host=opts.host)
816 server.run(host=opts.host, port=port)
817 # server.run() should never return
819 with utils.RunLock():
820 run_summary_required = True
822 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
824 raise Exception(err_msg)
826 # remove unfilled values
827 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
829 params = ' '.join(str(e) for e in sys.argv[1:])
830 result = nfvbench_instance.run(opts, params)
831 if 'error_message' in result:
832 raise Exception(result['error_message'])
834 if 'result' in result and result['status']:
835 nfvbench_instance.save(result['result'])
836 nfvbench_instance.prepare_summary(result['result'])
837 except Exception as exc:
838 run_summary_required = True
840 'status': NFVBench.STATUS_ERROR,
841 'error_message': traceback.format_exc()
846 # only send a summary record if there was an actual nfvbench run or
847 # if an error/exception was logged.
848 fluent_logger.send_run_summary(run_summary_required)
851 if __name__ == '__main__':