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',
534 help='Specify the FE cache size (default: 0, flow-count if < 0)')
536 parser.add_argument('--service-mode', dest='service_mode',
539 help='Enable T-Rex service mode for debugging only')
541 parser.add_argument('--no-flow-stats', dest='no_flow_stats',
544 help='Disable extra flow stats (on high load traffic)')
546 parser.add_argument('--no-latency-stats', dest='no_latency_stats',
549 help='Disable flow stats for latency traffic')
551 parser.add_argument('--no-latency-streams', dest='no_latency_streams',
554 help='Disable latency measurements (no streams)')
556 parser.add_argument('--debug-mask', dest='debug_mask',
560 default='0x00000000',
561 help='General purpose register (debugging flags), '
562 + 'the hexadecimal notation (0x...) is accepted.'
563 + 'Designed for development needs.')
565 opts, unknown_opts = parser.parse_known_args()
566 return opts, unknown_opts
569 def load_default_config():
570 default_cfg = resource_string(__name__, "cfg.default.yaml")
571 config = config_loads(default_cfg)
572 config.name = '(built-in default config)'
573 return config, default_cfg
576 def override_custom_traffic(config, frame_sizes, unidir):
577 """Override the traffic profiles with a custom one."""
578 if frame_sizes is not None:
579 traffic_profile_name = "custom_traffic_profile"
580 config.traffic_profile = [
582 "l2frame_size": frame_sizes,
583 "name": traffic_profile_name
587 traffic_profile_name = config.traffic["profile"]
589 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
591 "bidirectional": bidirectional,
592 "profile": traffic_profile_name
596 def check_physnet(name, netattrs):
597 if not netattrs.physical_network:
598 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
600 if not netattrs.segmentation_id:
601 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
604 def status_cleanup(config, cleanup, force_cleanup):
605 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
606 # check if another run is pending
609 with utils.RunLock():
610 LOG.info('Status: idle')
612 LOG.info('Status: busy (run pending)')
614 # check nfvbench resources
615 if config.openrc_file and config.service_chain != ChainType.EXT:
616 cleaner = Cleaner(config)
617 count = cleaner.show_resources()
618 if count and (cleanup or force_cleanup):
619 cleaner.clean(not force_cleanup)
624 run_summary_required = False
627 # load default config file
628 config, default_cfg = load_default_config()
629 # create factory for platform specific classes
631 factory_module = importlib.import_module(config['factory_module'])
632 factory = getattr(factory_module, config['factory_class'])()
633 except AttributeError:
634 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
635 .format(m=config['factory_module'],
636 c=config['factory_class'])) from AttributeError
637 # create config plugin for this platform
638 config_plugin = factory.get_config_plugin_class()(config)
639 config = config_plugin.get_config()
641 opts, unknown_opts = _parse_opts_from_cli()
642 log.set_level(debug=opts.debug)
645 print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
649 with open(opts.summary) as json_data:
650 result = json.load(json_data)
652 result['config']['user_label'] = opts.user_label
653 print((NFVBenchSummarizer(result, fluent_logger)))
656 # show default config in text/yaml format
657 if opts.show_default_config:
658 print((default_cfg.decode("utf-8")))
663 # do not check extra_specs in flavor as it can contain any key/value pairs
664 # the same principle applies also to the optional user_info open property
665 whitelist_keys = ['extra_specs', 'user_info']
666 # override default config options with start config at path parsed from CLI
667 # check if it is an inline yaml/json config or a file name
668 if os.path.isfile(opts.config):
669 LOG.info('Loading configuration file: %s', opts.config)
670 config = config_load(opts.config, config, whitelist_keys)
671 config.name = os.path.basename(opts.config)
673 LOG.info('Loading configuration string: %s', opts.config)
674 config = config_loads(opts.config, config, whitelist_keys)
676 # setup the fluent logger as soon as possible right after the config plugin is called,
677 # if there is any logging or result tag is set then initialize the fluent logger
678 for fluentd in config.fluentd:
679 if fluentd.logging_tag or fluentd.result_tag:
680 fluent_logger = FluentLogHandler(config.fluentd)
681 LOG.addHandler(fluent_logger)
684 # convert 'user_info' opt from json string to dictionnary
685 # and merge the result with the current config dictionnary
687 opts.user_info = json.loads(opts.user_info)
689 config.user_info = config.user_info + opts.user_info
691 config.user_info = opts.user_info
692 # hide the option to further _update_config()
693 opts.user_info = None
695 # traffic profile override options
696 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
698 # copy over cli options that are used in config
699 config.generator_profile = opts.generator_profile
703 config.log_file = opts.log_file
704 if opts.service_chain:
705 config.service_chain = opts.service_chain
706 if opts.service_chain_count:
707 config.service_chain_count = opts.service_chain_count
708 if opts.no_vswitch_access:
709 config.no_vswitch_access = opts.no_vswitch_access
711 # can be any of 'comp1', 'nova:', 'nova:comp1'
712 config.compute_nodes = opts.hypervisor
718 config.restart = True
719 if opts.service_mode:
720 config.service_mode = True
721 if opts.no_flow_stats:
722 config.no_flow_stats = True
723 if opts.no_latency_stats:
724 config.no_latency_stats = True
725 if opts.no_latency_streams:
726 config.no_latency_streams = True
727 # port to port loopback (direct or through switch)
729 config.l2_loopback = True
730 if config.service_chain != ChainType.EXT:
731 LOG.info('Changing service chain type to EXT')
732 config.service_chain = ChainType.EXT
733 if not config.no_arp:
734 LOG.info('Disabling ARP')
736 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
737 LOG.info('Running L2 loopback: using EXT chain/no ARP')
739 if opts.use_sriov_middle_net:
740 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
741 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
742 config.use_sriov_middle_net = True
744 if config.sriov and config.service_chain != ChainType.EXT:
745 # if sriov is requested (does not apply to ext chains)
746 # make sure the physnet names are specified
747 check_physnet("left", config.internal_networks.left)
748 check_physnet("right", config.internal_networks.right)
749 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
750 check_physnet("middle", config.internal_networks.middle)
752 # show running config in json format
754 print((json.dumps(config, sort_keys=True, indent=4)))
757 # update the config in the config plugin as it might have changed
758 # in a copy of the dict (config plugin still holds the original dict)
759 config_plugin.set_config(config)
761 if opts.status or opts.cleanup or opts.force_cleanup:
762 status_cleanup(config, opts.cleanup, opts.force_cleanup)
764 # add file log if requested
766 log.add_file_logger(config.log_file)
768 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
771 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
774 server = WebServer(nfvbench_instance, fluent_logger)
776 port = int(opts.port)
778 server.run(host=opts.host)
780 server.run(host=opts.host, port=port)
781 # server.run() should never return
783 with utils.RunLock():
784 run_summary_required = True
786 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
788 raise Exception(err_msg)
790 # remove unfilled values
791 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
793 params = ' '.join(str(e) for e in sys.argv[1:])
794 result = nfvbench_instance.run(opts, params)
795 if 'error_message' in result:
796 raise Exception(result['error_message'])
798 if 'result' in result and result['status']:
799 nfvbench_instance.save(result['result'])
800 nfvbench_instance.prepare_summary(result['result'])
801 except Exception as exc:
802 run_summary_required = True
804 'status': NFVBench.STATUS_ERROR,
805 'error_message': traceback.format_exc()
810 # only send a summary record if there was an actual nfvbench run or
811 # if an error/exception was logged.
812 fluent_logger.send_run_summary(run_summary_required)
815 if __name__ == '__main__':