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 def _update_config(self, opts):
171 """Recalculate the running config based on the base config and opts.
173 Sanity check on the config is done here as well.
175 self.config = AttrDict(dict(self.base_config))
176 # Update log file handler if needed after a config update (REST mode)
177 if 'log_file' in opts:
179 (path, _filename) = os.path.split(opts['log_file'])
180 if not os.path.exists(path):
182 'Path %s does not exist. Please verify root path is shared with host. Path '
183 'will be created.', path)
185 LOG.info('%s is created.', path)
186 for h in log.getLogger().handlers:
187 if isinstance(h, FileHandler) and h.baseFilename != opts['log_file']:
188 # clean log file handler
189 log.getLogger().removeHandler(h)
190 # add handler if not existing to avoid duplicates handlers
191 if len(log.getLogger().handlers) == 1:
192 log.add_file_logger(opts['log_file'])
194 self.config.update(opts)
197 config.service_chain = config.service_chain.upper()
198 config.service_chain_count = int(config.service_chain_count)
199 if config.l2_loopback:
200 # force the number of chains to be 1 in case of l2 loopback
201 config.service_chain_count = 1
202 config.service_chain = ChainType.EXT
204 LOG.info('Running L2 loopback: using EXT chain/no ARP')
206 # traffic profile override options
207 if 'frame_sizes' in opts:
210 unidir = opts['unidir']
211 override_custom_traffic(config, opts['frame_sizes'], unidir)
212 LOG.info("Frame size has been set to %s for current configuration", opts['frame_sizes'])
214 config.flow_count = utils.parse_flow_count(config.flow_count)
215 required_flow_count = config.service_chain_count * 2
216 if config.flow_count < required_flow_count:
217 LOG.info("Flow count %d has been set to minimum value of '%d' "
218 "for current configuration", config.flow_count,
220 config.flow_count = required_flow_count
222 if config.flow_count % 2:
223 config.flow_count += 1
225 # Possibly adjust the cache size
226 if config.cache_size < 0:
227 config.cache_size = config.flow_count
229 # The size must be capped to 10000 (where does this limit come from?)
230 if config.cache_size > 10000:
231 config.cache_size = 10000
233 config.duration_sec = float(config.duration_sec)
234 config.interval_sec = float(config.interval_sec)
235 config.pause_sec = float(config.pause_sec)
237 if config.traffic is None or not config.traffic:
238 raise Exception("Missing traffic property in configuration")
240 if config.openrc_file:
241 config.openrc_file = os.path.expanduser(config.openrc_file)
242 if config.flavor.vcpus < 2:
243 raise Exception("Flavor vcpus must be >= 2")
245 config.ndr_run = (not config.no_traffic and
246 'ndr' in config.rate.strip().lower().split('_'))
247 config.pdr_run = (not config.no_traffic and
248 'pdr' in config.rate.strip().lower().split('_'))
249 config.single_run = (not config.no_traffic and
250 not (config.ndr_run or config.pdr_run))
252 config.json_file = config.json if config.json else None
254 (path, _filename) = os.path.split(config.json)
255 if not os.path.exists(path):
256 raise Exception('Please provide existing path for storing results in JSON file. '
257 'Path used: {path}'.format(path=path))
259 config.std_json_path = config.std_json if config.std_json else None
260 if config.std_json_path:
261 if not os.path.exists(config.std_json):
262 raise Exception('Please provide existing path for storing results in JSON file. '
263 'Path used: {path}'.format(path=config.std_json_path))
265 # Check that multiqueue is between 1 and 8 (8 is the max allowed by libvirt/qemu)
266 if config.vif_multiqueue_size < 1 or config.vif_multiqueue_size > 8:
267 raise Exception('vif_multiqueue_size (%d) must be in [1..8]' %
268 config.vif_multiqueue_size)
270 # VxLAN and MPLS sanity checks
271 if config.vxlan or config.mpls:
272 if config.vlan_tagging:
273 config.vlan_tagging = False
274 config.no_latency_streams = True
275 config.no_latency_stats = True
276 config.no_flow_stats = True
277 LOG.info('VxLAN or MPLS: vlan_tagging forced to False '
278 '(inner VLAN tagging must be disabled)')
280 self.config_plugin.validate_config(config, self.specs.openstack)
284 """Argument type to be used in parser.add_argument()
285 When a boolean like value is expected to be given
287 return (str(x).lower() != 'false') \
288 and (str(x).lower() != 'no') \
289 and (str(x).lower() != '0')
293 """Argument type to be used in parser.add_argument()
294 When an integer type value is expected to be given
295 (returns 0 if argument is invalid, hexa accepted)
300 def _parse_opts_from_cli():
301 parser = argparse.ArgumentParser()
303 parser.add_argument('--status', dest='status',
306 help='Provide NFVbench status')
308 parser.add_argument('-c', '--config', dest='config',
310 help='Override default values with a config file or '
311 'a yaml/json config string',
312 metavar='<file_name_or_yaml>')
314 parser.add_argument('--server', dest='server',
317 help='Run nfvbench in server mode')
319 parser.add_argument('--host', dest='host',
322 help='Host IP address on which server will be listening (default 0.0.0.0)')
324 parser.add_argument('-p', '--port', dest='port',
327 help='Port on which server will be listening (default 7555)')
329 parser.add_argument('-sc', '--service-chain', dest='service_chain',
330 choices=ChainType.names,
332 help='Service chain to run')
334 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
336 help='Set number of service chains to run',
337 metavar='<service_chain_count>')
339 parser.add_argument('-fc', '--flow-count', dest='flow_count',
341 help='Set number of total flows for all chains and all directions',
342 metavar='<flow_count>')
344 parser.add_argument('--rate', dest='rate',
346 help='Specify rate in pps, bps or %% as total for all directions',
349 parser.add_argument('--duration', dest='duration_sec',
351 help='Set duration to run traffic generator (in seconds)',
352 metavar='<duration_sec>')
354 parser.add_argument('--interval', dest='interval_sec',
356 help='Set interval to record traffic generator stats (in seconds)',
357 metavar='<interval_sec>')
359 parser.add_argument('--inter-node', dest='inter_node',
364 parser.add_argument('--sriov', dest='sriov',
367 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
369 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
372 help='Use SRIOV to handle the middle network traffic '
373 '(PVVP with SRIOV only)')
375 parser.add_argument('-d', '--debug', dest='debug',
378 help='print debug messages (verbose)')
380 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
382 help='Traffic generator profile to use')
384 parser.add_argument('-l3', '--l3-router', dest='l3_router',
387 help='Use L3 neutron routers to handle traffic')
389 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
392 help='Check config and connectivity only - do not generate traffic')
394 parser.add_argument('--no-arp', dest='no_arp',
397 help='Do not use ARP to find MAC addresses, '
398 'instead use values in config file')
400 parser.add_argument('--loop-vm-arp', dest='loop_vm_arp',
403 help='Use ARP to find MAC addresses '
404 'instead of using values from TRex ports (VPP forwarder only)')
406 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
409 help='Skip vswitch configuration and retrieving of stats')
411 parser.add_argument('--vxlan', dest='vxlan',
414 help='Enable VxLan encapsulation')
416 parser.add_argument('--mpls', dest='mpls',
419 help='Enable MPLS encapsulation')
421 parser.add_argument('--no-cleanup', dest='no_cleanup',
424 help='no cleanup after run')
426 parser.add_argument('--cleanup', dest='cleanup',
429 help='Cleanup NFVbench resources (prompt to confirm)')
431 parser.add_argument('--force-cleanup', dest='force_cleanup',
434 help='Cleanup NFVbench resources (do not prompt)')
436 parser.add_argument('--restart', dest='restart',
439 help='Restart TRex server')
441 parser.add_argument('--json', dest='json',
443 help='store results in json format file',
444 metavar='<path>/<filename>')
446 parser.add_argument('--std-json', dest='std_json',
448 help='store results in json format file with nfvbench standard filename: '
449 '<service-chain-type>-<service-chain-count>-<flow-count>'
450 '-<packet-sizes>.json',
453 parser.add_argument('--show-default-config', dest='show_default_config',
456 help='print the default config in yaml format (unedited)')
458 parser.add_argument('--show-config', dest='show_config',
461 help='print the running config in json format')
463 parser.add_argument('-ss', '--show-summary', dest='summary',
465 help='Show summary from nfvbench json file',
468 parser.add_argument('-v', '--version', dest='version',
473 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
475 help='Override traffic profile frame sizes',
476 metavar='<frame_size_bytes or IMIX>')
478 parser.add_argument('--unidir', dest='unidir',
481 help='Override traffic profile direction (requires -fs)')
483 parser.add_argument('--log-file', '--logfile', dest='log_file',
485 help='Filename for saving logs',
486 metavar='<log_file>')
488 parser.add_argument('--user-label', '--userlabel', dest='user_label',
490 help='Custom label for performance records')
492 parser.add_argument('--hypervisor', dest='hypervisor',
494 metavar='<hypervisor name>',
495 help='Where chains must run ("compute", "az:", "az:compute")')
497 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
500 help='Port to port or port to switch to port L2 loopback with VLAN id')
502 parser.add_argument('--user-info', dest='user_info',
505 help='Custom data to be included as is in the json report config branch - '
506 + ' example, pay attention! no space: '
507 + '--user-info=\'{"status":"explore","description":'
508 + '{"target":"lab","ok":true,"version":2020}}\'')
510 parser.add_argument('--vlan-tagging', dest='vlan_tagging',
515 help='Override the NFVbench \'vlan_tagging\' parameter')
517 parser.add_argument('--intf-speed', dest='intf_speed',
521 help='Override the NFVbench \'intf_speed\' '
522 + 'parameter (e.g. 10Gbps, auto, 16.72Gbps)')
524 parser.add_argument('--cores', dest='cores',
529 help='Override the T-Rex \'cores\' parameter')
531 parser.add_argument('--cache-size', dest='cache_size',
536 help='Specify the FE cache size (default: 0, flow-count if < 0)')
538 parser.add_argument('--service-mode', dest='service_mode',
541 help='Enable T-Rex service mode (for debugging purpose)')
543 parser.add_argument('--no-e2e-check', dest='no_e2e_check',
546 help='Skip "end to end" connectivity check (on test purpose)')
548 parser.add_argument('--no-flow-stats', dest='no_flow_stats',
551 help='Disable additional flow stats (on high load traffic)')
553 parser.add_argument('--no-latency-stats', dest='no_latency_stats',
556 help='Disable flow stats for latency traffic')
558 parser.add_argument('--no-latency-streams', dest='no_latency_streams',
561 help='Disable latency measurements (no streams)')
563 parser.add_argument('--debug-mask', dest='debug_mask',
567 default='0x00000000',
568 help='General purpose register (debugging flags), '
569 + 'the hexadecimal notation (0x...) is accepted.'
570 + 'Designed for development needs.')
572 opts, unknown_opts = parser.parse_known_args()
573 return opts, unknown_opts
576 def load_default_config():
577 default_cfg = resource_string(__name__, "cfg.default.yaml")
578 config = config_loads(default_cfg)
579 config.name = '(built-in default config)'
580 return config, default_cfg
583 def override_custom_traffic(config, frame_sizes, unidir):
584 """Override the traffic profiles with a custom one."""
585 if frame_sizes is not None:
586 traffic_profile_name = "custom_traffic_profile"
587 config.traffic_profile = [
589 "l2frame_size": frame_sizes,
590 "name": traffic_profile_name
594 traffic_profile_name = config.traffic["profile"]
596 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
598 "bidirectional": bidirectional,
599 "profile": traffic_profile_name
603 def check_physnet(name, netattrs):
604 if not netattrs.physical_network:
605 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
607 if not netattrs.segmentation_id:
608 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
611 def status_cleanup(config, cleanup, force_cleanup):
612 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
613 # check if another run is pending
616 with utils.RunLock():
617 LOG.info('Status: idle')
619 LOG.info('Status: busy (run pending)')
621 # check nfvbench resources
622 if config.openrc_file and config.service_chain != ChainType.EXT:
623 cleaner = Cleaner(config)
624 count = cleaner.show_resources()
625 if count and (cleanup or force_cleanup):
626 cleaner.clean(not force_cleanup)
631 run_summary_required = False
634 # load default config file
635 config, default_cfg = load_default_config()
636 # create factory for platform specific classes
638 factory_module = importlib.import_module(config['factory_module'])
639 factory = getattr(factory_module, config['factory_class'])()
640 except AttributeError:
641 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
642 .format(m=config['factory_module'],
643 c=config['factory_class'])) from AttributeError
644 # create config plugin for this platform
645 config_plugin = factory.get_config_plugin_class()(config)
646 config = config_plugin.get_config()
648 opts, unknown_opts = _parse_opts_from_cli()
649 log.set_level(debug=opts.debug)
652 print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
656 with open(opts.summary) as json_data:
657 result = json.load(json_data)
659 result['config']['user_label'] = opts.user_label
660 print((NFVBenchSummarizer(result, fluent_logger)))
663 # show default config in text/yaml format
664 if opts.show_default_config:
665 print((default_cfg.decode("utf-8")))
670 # do not check extra_specs in flavor as it can contain any key/value pairs
671 # the same principle applies also to the optional user_info open property
672 whitelist_keys = ['extra_specs', 'user_info']
673 # override default config options with start config at path parsed from CLI
674 # check if it is an inline yaml/json config or a file name
675 if os.path.isfile(opts.config):
676 LOG.info('Loading configuration file: %s', opts.config)
677 config = config_load(opts.config, config, whitelist_keys)
678 config.name = os.path.basename(opts.config)
680 LOG.info('Loading configuration string: %s', opts.config)
681 config = config_loads(opts.config, config, whitelist_keys)
683 # setup the fluent logger as soon as possible right after the config plugin is called,
684 # if there is any logging or result tag is set then initialize the fluent logger
685 for fluentd in config.fluentd:
686 if fluentd.logging_tag or fluentd.result_tag:
687 fluent_logger = FluentLogHandler(config.fluentd)
688 LOG.addHandler(fluent_logger)
691 # convert 'user_info' opt from json string to dictionnary
692 # and merge the result with the current config dictionnary
694 opts.user_info = json.loads(opts.user_info)
696 config.user_info = config.user_info + opts.user_info
698 config.user_info = opts.user_info
699 # hide the option to further _update_config()
700 opts.user_info = None
702 # traffic profile override options
703 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
705 # copy over cli options that are used in config
706 config.generator_profile = opts.generator_profile
710 config.log_file = opts.log_file
711 if opts.service_chain:
712 config.service_chain = opts.service_chain
713 if opts.service_chain_count:
714 config.service_chain_count = opts.service_chain_count
715 if opts.no_vswitch_access:
716 config.no_vswitch_access = opts.no_vswitch_access
718 # can be any of 'comp1', 'nova:', 'nova:comp1'
719 config.compute_nodes = opts.hypervisor
725 config.restart = True
726 if opts.service_mode:
727 config.service_mode = True
728 if opts.no_flow_stats:
729 config.no_flow_stats = True
730 if opts.no_latency_stats:
731 config.no_latency_stats = True
732 if opts.no_latency_streams:
733 config.no_latency_streams = True
734 # port to port loopback (direct or through switch)
736 config.l2_loopback = True
737 if config.service_chain != ChainType.EXT:
738 LOG.info('Changing service chain type to EXT')
739 config.service_chain = ChainType.EXT
740 if not config.no_arp:
741 LOG.info('Disabling ARP')
743 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
744 LOG.info('Running L2 loopback: using EXT chain/no ARP')
746 if opts.use_sriov_middle_net:
747 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
748 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
749 config.use_sriov_middle_net = True
751 if config.sriov and config.service_chain != ChainType.EXT:
752 # if sriov is requested (does not apply to ext chains)
753 # make sure the physnet names are specified
754 check_physnet("left", config.internal_networks.left)
755 check_physnet("right", config.internal_networks.right)
756 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
757 check_physnet("middle", config.internal_networks.middle)
759 # show running config in json format
761 print((json.dumps(config, sort_keys=True, indent=4)))
764 # update the config in the config plugin as it might have changed
765 # in a copy of the dict (config plugin still holds the original dict)
766 config_plugin.set_config(config)
768 if opts.status or opts.cleanup or opts.force_cleanup:
769 status_cleanup(config, opts.cleanup, opts.force_cleanup)
771 # add file log if requested
773 log.add_file_logger(config.log_file)
775 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
778 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
781 server = WebServer(nfvbench_instance, fluent_logger)
783 port = int(opts.port)
785 server.run(host=opts.host)
787 server.run(host=opts.host, port=port)
788 # server.run() should never return
790 with utils.RunLock():
791 run_summary_required = True
793 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
795 raise Exception(err_msg)
797 # remove unfilled values
798 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
800 params = ' '.join(str(e) for e in sys.argv[1:])
801 result = nfvbench_instance.run(opts, params)
802 if 'error_message' in result:
803 raise Exception(result['error_message'])
805 if 'result' in result and result['status']:
806 nfvbench_instance.save(result['result'])
807 nfvbench_instance.prepare_summary(result['result'])
808 except Exception as exc:
809 run_summary_required = True
811 'status': NFVBench.STATUS_ERROR,
812 'error_message': traceback.format_exc()
817 # only send a summary record if there was an actual nfvbench run or
818 # if an error/exception was logged.
819 fluent_logger.send_run_summary(run_summary_required)
822 if __name__ == '__main__':