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 import credentials as 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)
91 # check that an empty openrc file (no OpenStack) is only allowed
93 if not self.config.openrc_file and self.config.service_chain != ChainType.EXT:
94 raise Exception("openrc_file in the configuration is required for PVP/PVVP chains")
96 self.specs.set_run_spec(self.config_plugin.get_run_spec(self.config,
97 self.specs.openstack))
98 self.chain_runner = ChainRunner(self.config,
104 # make sure that the min frame size is 64
106 for frame_size in self.config.frame_sizes:
108 if int(frame_size) < min_packet_size:
109 frame_size = str(min_packet_size)
110 LOG.info("Adjusting frame size %s bytes to minimum size %s bytes",
111 frame_size, min_packet_size)
112 if frame_size not in new_frame_sizes:
113 new_frame_sizes.append(frame_size)
115 new_frame_sizes.append(frame_size.upper())
116 self.config.frame_sizes = new_frame_sizes
118 "date": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
119 "nfvbench_version": __version__,
120 "config": self.config_plugin.prepare_results_config(copy.deepcopy(self.config)),
123 "service_chain": self.chain_runner.run(),
124 "versions": self.chain_runner.get_version(),
128 if self.specs.openstack:
129 result['openstack_spec'] = {"vswitch": self.specs.openstack.vswitch,
130 "encaps": self.specs.openstack.encaps}
131 result['benchmarks']['network']['versions'].update(self.config_plugin.get_version())
133 status = NFVBench.STATUS_ERROR
134 message = traceback.format_exc()
135 except KeyboardInterrupt:
136 status = NFVBench.STATUS_ERROR
137 message = traceback.format_exc()
139 if self.chain_runner:
140 self.chain_runner.close()
142 if status == NFVBench.STATUS_OK:
143 # result2 = utils.dict_to_json_dict(result)
150 'error_message': message
153 def prepare_summary(self, result):
154 """Prepare summary of the result to print and send it to logger (eg: fluentd)."""
156 summary = NFVBenchSummarizer(result, fluent_logger)
157 LOG.info(str(summary))
159 def save(self, result):
160 """Save results in json format file."""
161 utils.save_json_result(result,
162 self.config.json_file,
163 self.config.std_json_path,
164 self.config.service_chain,
165 self.config.service_chain_count,
166 self.config.flow_count,
167 self.config.frame_sizes)
169 def _update_config(self, opts):
170 """Recalculate the running config based on the base config and opts.
172 Sanity check on the config is done here as well.
174 self.config = AttrDict(dict(self.base_config))
175 self.config.update(opts)
178 config.service_chain = config.service_chain.upper()
179 config.service_chain_count = int(config.service_chain_count)
180 if config.l2_loopback:
181 # force the number of chains to be 1 in case of l2 loopback
182 config.service_chain_count = 1
183 config.service_chain = ChainType.EXT
185 LOG.info('Running L2 loopback: using EXT chain/no ARP')
186 config.flow_count = utils.parse_flow_count(config.flow_count)
187 required_flow_count = config.service_chain_count * 2
188 if config.flow_count < required_flow_count:
189 LOG.info("Flow count %d has been set to minimum value of '%d' "
190 "for current configuration", config.flow_count,
192 config.flow_count = required_flow_count
194 if config.flow_count % 2:
195 config.flow_count += 1
197 config.duration_sec = float(config.duration_sec)
198 config.interval_sec = float(config.interval_sec)
199 config.pause_sec = float(config.pause_sec)
201 if config.traffic is None or not config.traffic:
202 raise Exception("Missing traffic property in configuration")
204 if config.openrc_file:
205 config.openrc_file = os.path.expanduser(config.openrc_file)
206 if config.flavor.vcpus < 2:
207 raise Exception("Flavor vcpus must be >= 2")
210 config.ndr_run = (not config.no_traffic and
211 'ndr' in config.rate.strip().lower().split('_'))
212 config.pdr_run = (not config.no_traffic and
213 'pdr' in config.rate.strip().lower().split('_'))
214 config.single_run = (not config.no_traffic and
215 not (config.ndr_run or config.pdr_run))
217 config.json_file = config.json if config.json else None
219 (path, _filename) = os.path.split(config.json)
220 if not os.path.exists(path):
221 raise Exception('Please provide existing path for storing results in JSON file. '
222 'Path used: {path}'.format(path=path))
224 config.std_json_path = config.std_json if config.std_json else None
225 if config.std_json_path:
226 if not os.path.exists(config.std_json):
227 raise Exception('Please provide existing path for storing results in JSON file. '
228 'Path used: {path}'.format(path=config.std_json_path))
230 # Check that multiqueue is between 1 and 8 (8 is the max allowed by libvirt/qemu)
231 if config.vif_multiqueue_size < 1 or config.vif_multiqueue_size > 8:
232 raise Exception('vif_multiqueue_size (%d) must be in [1..8]' %
233 config.vif_multiqueue_size)
235 # VxLAN sanity checks
237 if config.vlan_tagging:
238 config.vlan_tagging = False
239 LOG.info('VxLAN: vlan_tagging forced to False '
240 '(inner VLAN tagging must be disabled)')
242 self.config_plugin.validate_config(config, self.specs.openstack)
245 def _parse_opts_from_cli():
246 parser = argparse.ArgumentParser()
248 parser.add_argument('--status', dest='status',
251 help='Provide NFVbench status')
253 parser.add_argument('-c', '--config', dest='config',
255 help='Override default values with a config file or '
256 'a yaml/json config string',
257 metavar='<file_name_or_yaml>')
259 parser.add_argument('--server', dest='server',
262 help='Run nfvbench in server mode')
264 parser.add_argument('--host', dest='host',
267 help='Host IP address on which server will be listening (default 0.0.0.0)')
269 parser.add_argument('-p', '--port', dest='port',
272 help='Port on which server will be listening (default 7555)')
274 parser.add_argument('-sc', '--service-chain', dest='service_chain',
275 choices=ChainType.names,
277 help='Service chain to run')
279 parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
281 help='Set number of service chains to run',
282 metavar='<service_chain_count>')
284 parser.add_argument('-fc', '--flow-count', dest='flow_count',
286 help='Set number of total flows for all chains and all directions',
287 metavar='<flow_count>')
289 parser.add_argument('--rate', dest='rate',
291 help='Specify rate in pps, bps or %% as total for all directions',
294 parser.add_argument('--duration', dest='duration_sec',
296 help='Set duration to run traffic generator (in seconds)',
297 metavar='<duration_sec>')
299 parser.add_argument('--interval', dest='interval_sec',
301 help='Set interval to record traffic generator stats (in seconds)',
302 metavar='<interval_sec>')
304 parser.add_argument('--inter-node', dest='inter_node',
309 parser.add_argument('--sriov', dest='sriov',
312 help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
314 parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
317 help='Use SRIOV to handle the middle network traffic '
318 '(PVVP with SRIOV only)')
320 parser.add_argument('-d', '--debug', dest='debug',
323 help='print debug messages (verbose)')
325 parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
327 help='Traffic generator profile to use')
329 parser.add_argument('-l3', '--l3-router', dest='l3_router',
332 help='Use L3 neutron routers to handle traffic')
334 parser.add_argument('-0', '--no-traffic', dest='no_traffic',
337 help='Check config and connectivity only - do not generate traffic')
339 parser.add_argument('--no-arp', dest='no_arp',
342 help='Do not use ARP to find MAC addresses, '
343 'instead use values in config file')
345 parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
348 help='Skip vswitch configuration and retrieving of stats')
350 parser.add_argument('--vxlan', dest='vxlan',
353 help='Enable VxLan encapsulation')
355 parser.add_argument('--no-cleanup', dest='no_cleanup',
358 help='no cleanup after run')
360 parser.add_argument('--cleanup', dest='cleanup',
363 help='Cleanup NFVbench resources (prompt to confirm)')
365 parser.add_argument('--force-cleanup', dest='force_cleanup',
368 help='Cleanup NFVbench resources (do not prompt)')
370 parser.add_argument('--restart', dest='restart',
373 help='Restart TRex server')
375 parser.add_argument('--json', dest='json',
377 help='store results in json format file',
378 metavar='<path>/<filename>')
380 parser.add_argument('--std-json', dest='std_json',
382 help='store results in json format file with nfvbench standard filename: '
383 '<service-chain-type>-<service-chain-count>-<flow-count>'
384 '-<packet-sizes>.json',
387 parser.add_argument('--show-default-config', dest='show_default_config',
390 help='print the default config in yaml format (unedited)')
392 parser.add_argument('--show-config', dest='show_config',
395 help='print the running config in json format')
397 parser.add_argument('-ss', '--show-summary', dest='summary',
399 help='Show summary from nfvbench json file',
402 parser.add_argument('-v', '--version', dest='version',
407 parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
409 help='Override traffic profile frame sizes',
410 metavar='<frame_size_bytes or IMIX>')
412 parser.add_argument('--unidir', dest='unidir',
415 help='Override traffic profile direction (requires -fs)')
417 parser.add_argument('--log-file', '--logfile', dest='log_file',
419 help='Filename for saving logs',
420 metavar='<log_file>')
422 parser.add_argument('--user-label', '--userlabel', dest='user_label',
424 help='Custom label for performance records')
426 parser.add_argument('--hypervisor', dest='hypervisor',
428 metavar='<hypervisor name>',
429 help='Where chains must run ("compute", "az:", "az:compute")')
431 parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
434 help='Port to port or port to switch to port L2 loopback with VLAN id')
436 opts, unknown_opts = parser.parse_known_args()
437 return opts, unknown_opts
440 def load_default_config():
441 default_cfg = resource_string(__name__, "cfg.default.yaml")
442 config = config_loads(default_cfg)
443 config.name = '(built-in default config)'
444 return config, default_cfg
447 def override_custom_traffic(config, frame_sizes, unidir):
448 """Override the traffic profiles with a custom one."""
449 if frame_sizes is not None:
450 traffic_profile_name = "custom_traffic_profile"
451 config.traffic_profile = [
453 "l2frame_size": frame_sizes,
454 "name": traffic_profile_name
458 traffic_profile_name = config.traffic["profile"]
460 bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
462 "bidirectional": bidirectional,
463 "profile": traffic_profile_name
467 def check_physnet(name, netattrs):
468 if not netattrs.physical_network:
469 raise Exception("SRIOV requires physical_network to be specified for the {n} network"
471 if not netattrs.segmentation_id:
472 raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
475 def status_cleanup(config, cleanup, force_cleanup):
476 LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
477 # check if another run is pending
480 with utils.RunLock():
481 LOG.info('Status: idle')
483 LOG.info('Status: busy (run pending)')
485 # check nfvbench resources
486 if config.openrc_file and config.service_chain != ChainType.EXT:
487 cleaner = Cleaner(config)
488 count = cleaner.show_resources()
489 if count and (cleanup or force_cleanup):
490 cleaner.clean(not force_cleanup)
495 run_summary_required = False
498 # load default config file
499 config, default_cfg = load_default_config()
500 # create factory for platform specific classes
502 factory_module = importlib.import_module(config['factory_module'])
503 factory = getattr(factory_module, config['factory_class'])()
504 except AttributeError:
505 raise Exception("Requested factory module '{m}' or class '{c}' was not found."
506 .format(m=config['factory_module'], c=config['factory_class']))
507 # create config plugin for this platform
508 config_plugin = factory.get_config_plugin_class()(config)
509 config = config_plugin.get_config()
511 opts, unknown_opts = _parse_opts_from_cli()
512 log.set_level(debug=opts.debug)
515 print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
519 with open(opts.summary) as json_data:
520 result = json.load(json_data)
522 result['config']['user_label'] = opts.user_label
523 print NFVBenchSummarizer(result, fluent_logger)
526 # show default config in text/yaml format
527 if opts.show_default_config:
533 # do not check extra_specs in flavor as it can contain any key/value pairs
534 whitelist_keys = ['extra_specs']
535 # override default config options with start config at path parsed from CLI
536 # check if it is an inline yaml/json config or a file name
537 if os.path.isfile(opts.config):
538 LOG.info('Loading configuration file: %s', opts.config)
539 config = config_load(opts.config, config, whitelist_keys)
540 config.name = os.path.basename(opts.config)
542 LOG.info('Loading configuration string: %s', opts.config)
543 config = config_loads(opts.config, config, whitelist_keys)
545 # setup the fluent logger as soon as possible right after the config plugin is called,
546 # if there is any logging or result tag is set then initialize the fluent logger
547 for fluentd in config.fluentd:
548 if fluentd.logging_tag or fluentd.result_tag:
549 fluent_logger = FluentLogHandler(config.fluentd)
550 LOG.addHandler(fluent_logger)
553 # traffic profile override options
554 override_custom_traffic(config, opts.frame_sizes, opts.unidir)
556 # copy over cli options that are used in config
557 config.generator_profile = opts.generator_profile
561 config.log_file = opts.log_file
562 if opts.service_chain:
563 config.service_chain = opts.service_chain
564 if opts.service_chain_count:
565 config.service_chain_count = opts.service_chain_count
566 if opts.no_vswitch_access:
567 config.no_vswitch_access = opts.no_vswitch_access
569 # can be any of 'comp1', 'nova:', 'nova:comp1'
570 config.compute_nodes = opts.hypervisor
574 config.restart = True
575 # port to port loopback (direct or through switch)
577 config.l2_loopback = True
578 if config.service_chain != ChainType.EXT:
579 LOG.info('Changing service chain type to EXT')
580 config.service_chain = ChainType.EXT
581 if not config.no_arp:
582 LOG.info('Disabling ARP')
584 config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
585 LOG.info('Running L2 loopback: using EXT chain/no ARP')
587 if opts.use_sriov_middle_net:
588 if (not config.sriov) or (config.service_chain != ChainType.PVVP):
589 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
590 config.use_sriov_middle_net = True
592 if config.sriov and config.service_chain != ChainType.EXT:
593 # if sriov is requested (does not apply to ext chains)
594 # make sure the physnet names are specified
595 check_physnet("left", config.internal_networks.left)
596 check_physnet("right", config.internal_networks.right)
597 if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
598 check_physnet("middle", config.internal_networks.middle)
600 # show running config in json format
602 print json.dumps(config, sort_keys=True, indent=4)
605 # update the config in the config plugin as it might have changed
606 # in a copy of the dict (config plugin still holds the original dict)
607 config_plugin.set_config(config)
609 if opts.status or opts.cleanup or opts.force_cleanup:
610 status_cleanup(config, opts.cleanup, opts.force_cleanup)
612 # add file log if requested
614 log.add_file_logger(config.log_file)
616 openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
619 nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
622 server = WebServer(nfvbench_instance, fluent_logger)
624 port = int(opts.port)
626 server.run(host=opts.host)
628 server.run(host=opts.host, port=port)
629 # server.run() should never return
631 with utils.RunLock():
632 run_summary_required = True
634 err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
636 raise Exception(err_msg)
638 # remove unfilled values
639 opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
641 params = ' '.join(str(e) for e in sys.argv[1:])
642 result = nfvbench_instance.run(opts, params)
643 if 'error_message' in result:
644 raise Exception(result['error_message'])
646 if 'result' in result and result['status']:
647 nfvbench_instance.save(result['result'])
648 nfvbench_instance.prepare_summary(result['result'])
649 except Exception as exc:
650 run_summary_required = True
652 'status': NFVBench.STATUS_ERROR,
653 'error_message': traceback.format_exc()
658 # only send a summary record if there was an actual nfvbench run or
659 # if an error/exception was logged.
660 fluent_logger.send_run_summary(run_summary_required)
663 if __name__ == '__main__':