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 '
508 'in the json report config branch - '
509 ' example, pay attention! no space: '
510 '--user-info=\'{"status":"explore","description":'
511 '{"target":"lab","ok":true,"version":2020}}\' - '
512 'this option may be repeated; given data will be merged.')
514 parser.add_argument('--vlan-tagging', dest='vlan_tagging',
519 help='Override the NFVbench \'vlan_tagging\' parameter')
521 parser.add_argument('--intf-speed', dest='intf_speed',
525 help='Override the NFVbench \'intf_speed\' '
526 'parameter (e.g. 10Gbps, auto, 16.72Gbps)')
528 parser.add_argument('--cores', dest='cores',
533 help='Override the T-Rex \'cores\' parameter')
535 parser.add_argument('--cache-size', dest='cache_size',
540 help='Specify the FE cache size (default: 0, flow-count if < 0)')
542 parser.add_argument('--service-mode', dest='service_mode',
545 help='Enable T-Rex service mode (for debugging purpose)')
547 parser.add_argument('--no-e2e-check', dest='no_e2e_check',
550 help='Skip "end to end" connectivity check (on test purpose)')
552 parser.add_argument('--no-flow-stats', dest='no_flow_stats',
555 help='Disable additional flow stats (on high load traffic)')
557 parser.add_argument('--no-latency-stats', dest='no_latency_stats',
560 help='Disable flow stats for latency traffic')
562 parser.add_argument('--no-latency-streams', dest='no_latency_streams',
565 help='Disable latency measurements (no streams)')
567 parser.add_argument('--user-id', dest='user_id',
572 help='Change json/log files ownership with this user (int)')
574 parser.add_argument('--group-id', dest='group_id',
579 help='Change json/log files ownership with this group (int)')
581 parser.add_argument('--show-trex-log', dest='show_trex_log',
584 help='Show the current TRex local server log file contents'
585 ' => diagnostic/help in case of configuration problems')
587 parser.add_argument('--debug-mask', dest='debug_mask',
592 help='General purpose register (debugging flags), '
593 'the hexadecimal notation (0x...) is accepted.'
594 'Designed for development needs (default: 0).')
596 opts, unknown_opts = parser.parse_known_args()
597 return opts, unknown_opts
600 def load_default_config():
601 default_cfg = resource_string(__name__, "cfg.default.yaml")
602 config = config_loads(default_cfg)
603 config.name = '(built-in default config)'
604 return config, default_cfg
607 def override_custom_traffic(config, frame_sizes, unidir):
608 """Override the traffic profiles with a custom one."""
609 if frame_sizes is not None:
610 traffic_profile_name = "custom_traffic_profile"
611 config.traffic_profile = [
613 "l2frame_size": frame_sizes,
614 "name": traffic_profile_name
618 traffic_profile_name = config.traffic["profile"]
620 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
622 "bidirectional": bidirectional,
623 "profile": traffic_profile_name
627 def check_physnet(name, netattrs):
628 if not netattrs.physical_network:
629 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
631 if not netattrs.segmentation_id:
632 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
635 def status_cleanup(config, cleanup, force_cleanup):
636 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
637 # check if another run is pending
640 with utils.RunLock():
641 LOG.info('Status: idle')
643 LOG.info('Status: busy (run pending)')
645 # check nfvbench resources
646 if config.openrc_file and config.service_chain != ChainType.EXT:
647 cleaner = Cleaner(config)
648 count = cleaner.show_resources()
649 if count and (cleanup or force_cleanup):
650 cleaner.clean(not force_cleanup)
655 run_summary_required = False
658 # load default config file
659 config, default_cfg = load_default_config()
660 # possibly override the default user_id & group_id values
661 if 'USER_ID' in os.environ:
662 config.user_id = int(os.environ['USER_ID'])
663 if 'GROUP_ID' in os.environ:
664 config.group_id = int(os.environ['GROUP_ID'])
666 # create factory for platform specific classes
668 factory_module = importlib.import_module(config['factory_module'])
669 factory = getattr(factory_module, config['factory_class'])()
670 except AttributeError:
671 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
672 .format(m=config['factory_module'],
673 c=config['factory_class'])) from AttributeError
674 # create config plugin for this platform
675 config_plugin = factory.get_config_plugin_class()(config)
676 config = config_plugin.get_config()
678 opts, unknown_opts = _parse_opts_from_cli()
679 log.set_level(debug=opts.debug)
682 print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
686 with open(opts.summary) as json_data:
687 result = json.load(json_data)
689 result['config']['user_label'] = opts.user_label
690 print((NFVBenchSummarizer(result, fluent_logger)))
693 # show default config in text/yaml format
694 if opts.show_default_config:
695 print((default_cfg.decode("utf-8")))
698 # dump the contents of the trex log file
699 if opts.show_trex_log:
701 print(open('/tmp/trex.log').read(), end="")
702 except FileNotFoundError:
703 print("No TRex log file found!")
708 # do not check extra_specs in flavor as it can contain any key/value pairs
709 # the same principle applies also to the optional user_info open property
710 whitelist_keys = ['extra_specs', 'user_info']
711 # override default config options with start config at path parsed from CLI
712 # check if it is an inline yaml/json config or a file name
713 if os.path.isfile(opts.config):
714 LOG.info('Loading configuration file: %s', opts.config)
715 config = config_load(opts.config, config, whitelist_keys)
716 config.name = os.path.basename(opts.config)
718 LOG.info('Loading configuration string: %s', opts.config)
719 config = config_loads(opts.config, config, whitelist_keys)
721 # setup the fluent logger as soon as possible right after the config plugin is called,
722 # if there is any logging or result tag is set then initialize the fluent logger
723 for fluentd in config.fluentd:
724 if fluentd.logging_tag or fluentd.result_tag:
725 fluent_logger = FluentLogHandler(config.fluentd)
726 LOG.addHandler(fluent_logger)
729 # convert 'user_info' opt from json string to dictionnary
730 # and merge the result with the current config dictionnary
732 opts.user_info = json.loads(opts.user_info)
734 config.user_info = config.user_info + opts.user_info
736 config.user_info = opts.user_info
737 # hide the option to further _update_config()
738 opts.user_info = None
740 # traffic profile override options
741 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
743 # copy over cli options that are used in config
744 config.generator_profile = opts.generator_profile
748 config.log_file = opts.log_file
749 if opts.service_chain:
750 config.service_chain = opts.service_chain
751 if opts.service_chain_count:
752 config.service_chain_count = opts.service_chain_count
753 if opts.no_vswitch_access:
754 config.no_vswitch_access = opts.no_vswitch_access
756 # can be any of 'comp1', 'nova:', 'nova:comp1'
757 config.compute_nodes = opts.hypervisor
763 config.restart = True
764 if opts.service_mode:
765 config.service_mode = True
766 if opts.no_flow_stats:
767 config.no_flow_stats = True
768 if opts.no_latency_stats:
769 config.no_latency_stats = True
770 if opts.no_latency_streams:
771 config.no_latency_streams = True
772 # port to port loopback (direct or through switch)
774 config.l2_loopback = True
775 if config.service_chain != ChainType.EXT:
776 LOG.info('Changing service chain type to EXT')
777 config.service_chain = ChainType.EXT
778 if not config.no_arp:
779 LOG.info('Disabling ARP')
781 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
782 LOG.info('Running L2 loopback: using EXT chain/no ARP')
784 if opts.use_sriov_middle_net:
785 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
786 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
787 config.use_sriov_middle_net = True
789 if config.sriov and config.service_chain != ChainType.EXT:
790 # if sriov is requested (does not apply to ext chains)
791 # make sure the physnet names are specified
792 check_physnet("left", config.internal_networks.left)
793 check_physnet("right", config.internal_networks.right)
794 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
795 check_physnet("middle", config.internal_networks.middle)
797 # show running config in json format
799 print((json.dumps(config, sort_keys=True, indent=4)))
802 # update the config in the config plugin as it might have changed
803 # in a copy of the dict (config plugin still holds the original dict)
804 config_plugin.set_config(config)
806 if opts.status or opts.cleanup or opts.force_cleanup:
807 status_cleanup(config, opts.cleanup, opts.force_cleanup)
809 # add file log if requested
811 log.add_file_logger(config.log_file)
812 # possibly change file ownership
814 gid = config.group_id
818 os.chown(config.log_file, uid, gid)
820 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
823 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
826 server = WebServer(nfvbench_instance, fluent_logger)
828 port = int(opts.port)
830 server.run(host=opts.host)
832 server.run(host=opts.host, port=port)
833 # server.run() should never return
835 with utils.RunLock():
836 run_summary_required = True
838 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
840 raise Exception(err_msg)
842 # remove unfilled values
843 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
845 params = ' '.join(str(e) for e in sys.argv[1:])
846 result = nfvbench_instance.run(opts, params)
847 if 'error_message' in result:
848 raise Exception(result['error_message'])
850 if 'result' in result and result['status']:
851 nfvbench_instance.save(result['result'])
852 nfvbench_instance.prepare_summary(result['result'])
853 except Exception as exc:
854 run_summary_required = True
856 'status': NFVBench.STATUS_ERROR,
857 'error_message': traceback.format_exc()
862 # only send a summary record if there was an actual nfvbench run or
863 # if an error/exception was logged.
864 fluent_logger.send_run_summary(run_summary_required)
867 if __name__ == '__main__':