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
28 from pkg_resources import resource_string
30 from .__init__ import __version__
31 from .chain_runner import ChainRunner
32 from .cleanup import Cleaner
33 from .config import config_load
34 from .config import config_loads
35 from . import credentials
36 from .fluentd import FluentLogHandler
39 from .nfvbenchd import WebServer
40 from .specs import ChainType
41 from .specs import Specs
42 from .summarizer import NFVBenchSummarizer
48 class NFVBench(object):
49 """Main class of NFV benchmarking tool."""
52 STATUS_ERROR = 'ERROR'
54 def __init__(self, config, openstack_spec, config_plugin, factory, notifier=None):
55 # the base config never changes for a given NFVbench instance
56 self.base_config = config
57 # this is the running config, updated at every run()
59 self.config_plugin = config_plugin
60 self.factory = factory
61 self.notifier = notifier
62 self.cred = credentials.Credentials(config.openrc_file, None, False) \
63 if config.openrc_file else None
64 self.chain_runner = None
66 self.specs.set_openstack_spec(openstack_spec)
70 def set_notifier(self, notifier):
71 self.notifier = notifier
73 def run(self, opts, args):
74 """This run() method is called for every NFVbench benchmark request.
76 In CLI mode, this method is called only once per invocation.
77 In REST server mode, this is called once per REST POST request
79 status = NFVBench.STATUS_OK
83 # take a snapshot of the current time for this new run
84 # so that all subsequent logs can relate to this run
85 fluent_logger.start_new_run()
88 # recalc the running config based on the base config and options for this run
89 self._update_config(opts)
90 if int(self.config.cache_size) < 0:
91 self.config.cache_size = self.config.flow_count
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 self.config.update(opts)
179 config.service_chain = config.service_chain.upper()
180 config.service_chain_count = int(config.service_chain_count)
181 if config.l2_loopback:
182 # force the number of chains to be 1 in case of l2 loopback
183 config.service_chain_count = 1
184 config.service_chain = ChainType.EXT
186 LOG.info('Running L2 loopback: using EXT chain/no ARP')
187 config.flow_count = utils.parse_flow_count(config.flow_count)
188 required_flow_count = config.service_chain_count * 2
189 if config.flow_count < required_flow_count:
190 LOG.info("Flow count %d has been set to minimum value of '%d' "
191 "for current configuration", config.flow_count,
193 config.flow_count = required_flow_count
195 if config.flow_count % 2:
196 config.flow_count += 1
198 config.duration_sec = float(config.duration_sec)
199 config.interval_sec = float(config.interval_sec)
200 config.pause_sec = float(config.pause_sec)
202 if config.traffic is None or not config.traffic:
203 raise Exception("Missing traffic property in configuration")
205 if config.openrc_file:
206 config.openrc_file = os.path.expanduser(config.openrc_file)
207 if config.flavor.vcpus < 2:
208 raise Exception("Flavor vcpus must be >= 2")
211 config.ndr_run = (not config.no_traffic and
212 'ndr' in config.rate.strip().lower().split('_'))
213 config.pdr_run = (not config.no_traffic and
214 'pdr' in config.rate.strip().lower().split('_'))
215 config.single_run = (not config.no_traffic and
216 not (config.ndr_run or config.pdr_run))
218 config.json_file = config.json if config.json else None
220 (path, _filename) = os.path.split(config.json)
221 if not os.path.exists(path):
222 raise Exception('Please provide existing path for storing results in JSON file. '
223 'Path used: {path}'.format(path=path))
225 config.std_json_path = config.std_json if config.std_json else None
226 if config.std_json_path:
227 if not os.path.exists(config.std_json):
228 raise Exception('Please provide existing path for storing results in JSON file. '
229 'Path used: {path}'.format(path=config.std_json_path))
231 # Check that multiqueue is between 1 and 8 (8 is the max allowed by libvirt/qemu)
232 if config.vif_multiqueue_size < 1 or config.vif_multiqueue_size > 8:
233 raise Exception('vif_multiqueue_size (%d) must be in [1..8]' %
234 config.vif_multiqueue_size)
236 # VxLAN and MPLS sanity checks
237 if config.vxlan or config.mpls:
238 if config.vlan_tagging:
239 config.vlan_tagging = False
240 config.no_latency_streams = True
241 config.no_latency_stats = True
242 config.no_flow_stats = True
243 LOG.info('VxLAN or MPLS: vlan_tagging forced to False '
244 '(inner VLAN tagging must be disabled)')
246 self.config_plugin.validate_config(config, self.specs.openstack)
249 def _parse_opts_from_cli():
250 parser = argparse.ArgumentParser()
252 parser.add_argument('--status', dest='status',
255 help='Provide NFVbench status')
257 parser.add_argument('-c', '--config', dest='config',
259 help='Override default values with a config file or '
260 'a yaml/json config string',
261 metavar='<file_name_or_yaml>')
263 parser.add_argument('--server', dest='server',
266 help='Run nfvbench in server mode')
268 parser.add_argument('--host', dest='host',
271 help='Host IP address on which server will be listening (default 0.0.0.0)')
273 parser.add_argument('-p', '--port', dest='port',
276 help='Port on which server will be listening (default 7555)')
278 parser.add_argument('-sc', '--service-chain', dest='service_chain',
279 choices=ChainType.names,
281 help='Service chain to run')
283 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
285 help='Set number of service chains to run',
286 metavar='<service_chain_count>')
288 parser.add_argument('-fc', '--flow-count', dest='flow_count',
290 help='Set number of total flows for all chains and all directions',
291 metavar='<flow_count>')
293 parser.add_argument('--rate', dest='rate',
295 help='Specify rate in pps, bps or %% as total for all directions',
298 parser.add_argument('--duration', dest='duration_sec',
300 help='Set duration to run traffic generator (in seconds)',
301 metavar='<duration_sec>')
303 parser.add_argument('--interval', dest='interval_sec',
305 help='Set interval to record traffic generator stats (in seconds)',
306 metavar='<interval_sec>')
308 parser.add_argument('--inter-node', dest='inter_node',
313 parser.add_argument('--sriov', dest='sriov',
316 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
318 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
321 help='Use SRIOV to handle the middle network traffic '
322 '(PVVP with SRIOV only)')
324 parser.add_argument('-d', '--debug', dest='debug',
327 help='print debug messages (verbose)')
329 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
331 help='Traffic generator profile to use')
333 parser.add_argument('-l3', '--l3-router', dest='l3_router',
336 help='Use L3 neutron routers to handle traffic')
338 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
341 help='Check config and connectivity only - do not generate traffic')
343 parser.add_argument('--no-arp', dest='no_arp',
346 help='Do not use ARP to find MAC addresses, '
347 'instead use values in config file')
349 parser.add_argument('--loop-vm-arp', dest='loop_vm_arp',
352 help='Use ARP to find MAC addresses '
353 'instead of using values from TRex ports (VPP forwarder only)')
355 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
358 help='Skip vswitch configuration and retrieving of stats')
360 parser.add_argument('--vxlan', dest='vxlan',
363 help='Enable VxLan encapsulation')
365 parser.add_argument('--mpls', dest='mpls',
368 help='Enable MPLS encapsulation')
370 parser.add_argument('--no-cleanup', dest='no_cleanup',
373 help='no cleanup after run')
375 parser.add_argument('--cleanup', dest='cleanup',
378 help='Cleanup NFVbench resources (prompt to confirm)')
380 parser.add_argument('--force-cleanup', dest='force_cleanup',
383 help='Cleanup NFVbench resources (do not prompt)')
385 parser.add_argument('--restart', dest='restart',
388 help='Restart TRex server')
390 parser.add_argument('--json', dest='json',
392 help='store results in json format file',
393 metavar='<path>/<filename>')
395 parser.add_argument('--std-json', dest='std_json',
397 help='store results in json format file with nfvbench standard filename: '
398 '<service-chain-type>-<service-chain-count>-<flow-count>'
399 '-<packet-sizes>.json',
402 parser.add_argument('--show-default-config', dest='show_default_config',
405 help='print the default config in yaml format (unedited)')
407 parser.add_argument('--show-config', dest='show_config',
410 help='print the running config in json format')
412 parser.add_argument('-ss', '--show-summary', dest='summary',
414 help='Show summary from nfvbench json file',
417 parser.add_argument('-v', '--version', dest='version',
422 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
424 help='Override traffic profile frame sizes',
425 metavar='<frame_size_bytes or IMIX>')
427 parser.add_argument('--unidir', dest='unidir',
430 help='Override traffic profile direction (requires -fs)')
432 parser.add_argument('--log-file', '--logfile', dest='log_file',
434 help='Filename for saving logs',
435 metavar='<log_file>')
437 parser.add_argument('--user-label', '--userlabel', dest='user_label',
439 help='Custom label for performance records')
441 parser.add_argument('--hypervisor', dest='hypervisor',
443 metavar='<hypervisor name>',
444 help='Where chains must run ("compute", "az:", "az:compute")')
446 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
449 help='Port to port or port to switch to port L2 loopback with VLAN id')
451 parser.add_argument('--cache-size', dest='cache_size',
454 help='Specify the FE cache size (default: 0, flow-count if < 0)')
456 parser.add_argument('--service-mode', dest='service_mode',
459 help='Enable T-Rex service mode for debugging only')
461 parser.add_argument('--no-flow-stats', dest='no_flow_stats',
464 help='Disable extra flow stats (on high load traffic)')
466 parser.add_argument('--no-latency-stats', dest='no_latency_stats',
469 help='Disable flow stats for latency traffic')
471 parser.add_argument('--no-latency-streams', dest='no_latency_streams',
474 help='Disable latency measurements (no streams)')
476 opts, unknown_opts = parser.parse_known_args()
477 return opts, unknown_opts
480 def load_default_config():
481 default_cfg = resource_string(__name__, "cfg.default.yaml")
482 config = config_loads(default_cfg)
483 config.name = '(built-in default config)'
484 return config, default_cfg
487 def override_custom_traffic(config, frame_sizes, unidir):
488 """Override the traffic profiles with a custom one."""
489 if frame_sizes is not None:
490 traffic_profile_name = "custom_traffic_profile"
491 config.traffic_profile = [
493 "l2frame_size": frame_sizes,
494 "name": traffic_profile_name
498 traffic_profile_name = config.traffic["profile"]
500 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
502 "bidirectional": bidirectional,
503 "profile": traffic_profile_name
507 def check_physnet(name, netattrs):
508 if not netattrs.physical_network:
509 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
511 if not netattrs.segmentation_id:
512 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
515 def status_cleanup(config, cleanup, force_cleanup):
516 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
517 # check if another run is pending
520 with utils.RunLock():
521 LOG.info('Status: idle')
523 LOG.info('Status: busy (run pending)')
525 # check nfvbench resources
526 if config.openrc_file and config.service_chain != ChainType.EXT:
527 cleaner = Cleaner(config)
528 count = cleaner.show_resources()
529 if count and (cleanup or force_cleanup):
530 cleaner.clean(not force_cleanup)
535 run_summary_required = False
538 # load default config file
539 config, default_cfg = load_default_config()
540 # create factory for platform specific classes
542 factory_module = importlib.import_module(config['factory_module'])
543 factory = getattr(factory_module, config['factory_class'])()
544 except AttributeError:
545 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
546 .format(m=config['factory_module'], c=config['factory_class']))
547 # create config plugin for this platform
548 config_plugin = factory.get_config_plugin_class()(config)
549 config = config_plugin.get_config()
551 opts, unknown_opts = _parse_opts_from_cli()
552 log.set_level(debug=opts.debug)
555 print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
559 with open(opts.summary) as json_data:
560 result = json.load(json_data)
562 result['config']['user_label'] = opts.user_label
563 print((NFVBenchSummarizer(result, fluent_logger)))
566 # show default config in text/yaml format
567 if opts.show_default_config:
568 print((default_cfg.decode("utf-8")))
573 # do not check extra_specs in flavor as it can contain any key/value pairs
574 whitelist_keys = ['extra_specs']
575 # override default config options with start config at path parsed from CLI
576 # check if it is an inline yaml/json config or a file name
577 if os.path.isfile(opts.config):
578 LOG.info('Loading configuration file: %s', opts.config)
579 config = config_load(opts.config, config, whitelist_keys)
580 config.name = os.path.basename(opts.config)
582 LOG.info('Loading configuration string: %s', opts.config)
583 config = config_loads(opts.config, config, whitelist_keys)
585 # setup the fluent logger as soon as possible right after the config plugin is called,
586 # if there is any logging or result tag is set then initialize the fluent logger
587 for fluentd in config.fluentd:
588 if fluentd.logging_tag or fluentd.result_tag:
589 fluent_logger = FluentLogHandler(config.fluentd)
590 LOG.addHandler(fluent_logger)
593 # traffic profile override options
594 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
596 # copy over cli options that are used in config
597 config.generator_profile = opts.generator_profile
601 config.log_file = opts.log_file
602 if opts.service_chain:
603 config.service_chain = opts.service_chain
604 if opts.service_chain_count:
605 config.service_chain_count = opts.service_chain_count
606 if opts.no_vswitch_access:
607 config.no_vswitch_access = opts.no_vswitch_access
609 # can be any of 'comp1', 'nova:', 'nova:comp1'
610 config.compute_nodes = opts.hypervisor
616 config.restart = True
617 if opts.service_mode:
618 config.service_mode = True
619 if opts.no_flow_stats:
620 config.no_flow_stats = True
621 if opts.no_latency_stats:
622 config.no_latency_stats = True
623 if opts.no_latency_streams:
624 config.no_latency_streams = True
625 # port to port loopback (direct or through switch)
627 config.l2_loopback = True
628 if config.service_chain != ChainType.EXT:
629 LOG.info('Changing service chain type to EXT')
630 config.service_chain = ChainType.EXT
631 if not config.no_arp:
632 LOG.info('Disabling ARP')
634 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
635 LOG.info('Running L2 loopback: using EXT chain/no ARP')
637 if opts.use_sriov_middle_net:
638 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
639 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
640 config.use_sriov_middle_net = True
642 if config.sriov and config.service_chain != ChainType.EXT:
643 # if sriov is requested (does not apply to ext chains)
644 # make sure the physnet names are specified
645 check_physnet("left", config.internal_networks.left)
646 check_physnet("right", config.internal_networks.right)
647 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
648 check_physnet("middle", config.internal_networks.middle)
650 # show running config in json format
652 print((json.dumps(config, sort_keys=True, indent=4)))
655 # update the config in the config plugin as it might have changed
656 # in a copy of the dict (config plugin still holds the original dict)
657 config_plugin.set_config(config)
659 if opts.status or opts.cleanup or opts.force_cleanup:
660 status_cleanup(config, opts.cleanup, opts.force_cleanup)
662 # add file log if requested
664 log.add_file_logger(config.log_file)
666 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
669 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
672 server = WebServer(nfvbench_instance, fluent_logger)
674 port = int(opts.port)
676 server.run(host=opts.host)
678 server.run(host=opts.host, port=port)
679 # server.run() should never return
681 with utils.RunLock():
682 run_summary_required = True
684 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
686 raise Exception(err_msg)
688 # remove unfilled values
689 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
691 params = ' '.join(str(e) for e in sys.argv[1:])
692 result = nfvbench_instance.run(opts, params)
693 if 'error_message' in result:
694 raise Exception(result['error_message'])
696 if 'result' in result and result['status']:
697 nfvbench_instance.save(result['result'])
698 nfvbench_instance.prepare_summary(result['result'])
699 except Exception as exc:
700 run_summary_required = True
702 'status': NFVBench.STATUS_ERROR,
703 'error_message': traceback.format_exc()
708 # only send a summary record if there was an actual nfvbench run or
709 # if an error/exception was logged.
710 fluent_logger.send_run_summary(run_summary_required)
713 if __name__ == '__main__':