NFVBENCH-157 Add possibility to not use the ARP static configuration for VPP loop VM
[nfvbench.git] / nfvbench / nfvbench.py
1 #!/usr/bin/env python
2 # Copyright 2016 Cisco Systems, Inc.  All rights reserved.
3 #
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
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
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
14 #    under the License.
15 #
16
17 import argparse
18 import copy
19 import datetime
20 import importlib
21 import json
22 import os
23 import sys
24 import traceback
25
26 from attrdict import AttrDict
27 import pbr.version
28 from pkg_resources import resource_string
29
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
37 from . import log
38 from .log import LOG
39 from .nfvbenchd import WebServer
40 from .specs import ChainType
41 from .specs import Specs
42 from .summarizer import NFVBenchSummarizer
43 from . import utils
44
45 fluent_logger = None
46
47
48 class NFVBench(object):
49     """Main class of NFV benchmarking tool."""
50
51     STATUS_OK = 'OK'
52     STATUS_ERROR = 'ERROR'
53
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()
58         self.config = None
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
65         self.specs = Specs()
66         self.specs.set_openstack_spec(openstack_spec)
67         self.vni_ports = []
68         sys.stdout.flush()
69
70     def set_notifier(self, notifier):
71         self.notifier = notifier
72
73     def run(self, opts, args):
74         """This run() method is called for every NFVbench benchmark request.
75
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
78         """
79         status = NFVBench.STATUS_OK
80         result = None
81         message = ''
82         if fluent_logger:
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()
86         LOG.info(args)
87         try:
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
93             # with EXT chain
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")
96
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,
100                                             self.cred,
101                                             self.specs,
102                                             self.factory,
103                                             self.notifier)
104             new_frame_sizes = []
105             # make sure that the min frame size is 64
106             min_packet_size = 64
107             for frame_size in self.config.frame_sizes:
108                 try:
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)
115                 except ValueError:
116                     new_frame_sizes.append(frame_size.upper())
117             self.config.frame_sizes = new_frame_sizes
118             result = {
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)),
122                 "benchmarks": {
123                     "network": {
124                         "service_chain": self.chain_runner.run(),
125                         "versions": self.chain_runner.get_version(),
126                     }
127                 }
128             }
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())
133         except Exception:
134             status = NFVBench.STATUS_ERROR
135             message = traceback.format_exc()
136         except KeyboardInterrupt:
137             status = NFVBench.STATUS_ERROR
138             message = traceback.format_exc()
139         finally:
140             if self.chain_runner:
141                 self.chain_runner.close()
142
143         if status == NFVBench.STATUS_OK:
144             # result2 = utils.dict_to_json_dict(result)
145             return {
146                 'status': status,
147                 'result': result
148             }
149         return {
150             'status': status,
151             'error_message': message
152         }
153
154     def prepare_summary(self, result):
155         """Prepare summary of the result to print and send it to logger (eg: fluentd)."""
156         global fluent_logger
157         summary = NFVBenchSummarizer(result, fluent_logger)
158         LOG.info(str(summary))
159
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)
169
170     def _update_config(self, opts):
171         """Recalculate the running config based on the base config and opts.
172
173         Sanity check on the config is done here as well.
174         """
175         self.config = AttrDict(dict(self.base_config))
176         self.config.update(opts)
177         config = self.config
178
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
185             config.no_arp = True
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,
192                      required_flow_count)
193             config.flow_count = required_flow_count
194
195         if config.flow_count % 2:
196             config.flow_count += 1
197
198         config.duration_sec = float(config.duration_sec)
199         config.interval_sec = float(config.interval_sec)
200         config.pause_sec = float(config.pause_sec)
201
202         if config.traffic is None or not config.traffic:
203             raise Exception("Missing traffic property in configuration")
204
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")
209
210
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))
217
218         config.json_file = config.json if config.json else None
219         if config.json_file:
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))
224
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))
230
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)
235
236         # VxLAN sanity checks
237         if config.vxlan:
238             if config.vlan_tagging:
239                 config.vlan_tagging = False
240                 LOG.info('VxLAN: vlan_tagging forced to False '
241                          '(inner VLAN tagging must be disabled)')
242
243         self.config_plugin.validate_config(config, self.specs.openstack)
244
245
246 def _parse_opts_from_cli():
247     parser = argparse.ArgumentParser()
248
249     parser.add_argument('--status', dest='status',
250                         action='store_true',
251                         default=None,
252                         help='Provide NFVbench status')
253
254     parser.add_argument('-c', '--config', dest='config',
255                         action='store',
256                         help='Override default values with a config file or '
257                              'a yaml/json config string',
258                         metavar='<file_name_or_yaml>')
259
260     parser.add_argument('--server', dest='server',
261                         default=None,
262                         action='store_true',
263                         help='Run nfvbench in server mode')
264
265     parser.add_argument('--host', dest='host',
266                         action='store',
267                         default='0.0.0.0',
268                         help='Host IP address on which server will be listening (default 0.0.0.0)')
269
270     parser.add_argument('-p', '--port', dest='port',
271                         action='store',
272                         default=7555,
273                         help='Port on which server will be listening (default 7555)')
274
275     parser.add_argument('-sc', '--service-chain', dest='service_chain',
276                         choices=ChainType.names,
277                         action='store',
278                         help='Service chain to run')
279
280     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
281                         action='store',
282                         help='Set number of service chains to run',
283                         metavar='<service_chain_count>')
284
285     parser.add_argument('-fc', '--flow-count', dest='flow_count',
286                         action='store',
287                         help='Set number of total flows for all chains and all directions',
288                         metavar='<flow_count>')
289
290     parser.add_argument('--rate', dest='rate',
291                         action='store',
292                         help='Specify rate in pps, bps or %% as total for all directions',
293                         metavar='<rate>')
294
295     parser.add_argument('--duration', dest='duration_sec',
296                         action='store',
297                         help='Set duration to run traffic generator (in seconds)',
298                         metavar='<duration_sec>')
299
300     parser.add_argument('--interval', dest='interval_sec',
301                         action='store',
302                         help='Set interval to record traffic generator stats (in seconds)',
303                         metavar='<interval_sec>')
304
305     parser.add_argument('--inter-node', dest='inter_node',
306                         default=None,
307                         action='store_true',
308                         help='(deprecated)')
309
310     parser.add_argument('--sriov', dest='sriov',
311                         default=None,
312                         action='store_true',
313                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
314
315     parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
316                         default=None,
317                         action='store_true',
318                         help='Use SRIOV to handle the middle network traffic '
319                              '(PVVP with SRIOV only)')
320
321     parser.add_argument('-d', '--debug', dest='debug',
322                         action='store_true',
323                         default=None,
324                         help='print debug messages (verbose)')
325
326     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
327                         action='store',
328                         help='Traffic generator profile to use')
329
330     parser.add_argument('-l3', '--l3-router', dest='l3_router',
331                         default=None,
332                         action='store_true',
333                         help='Use L3 neutron routers to handle traffic')
334
335     parser.add_argument('-0', '--no-traffic', dest='no_traffic',
336                         default=None,
337                         action='store_true',
338                         help='Check config and connectivity only - do not generate traffic')
339
340     parser.add_argument('--no-arp', dest='no_arp',
341                         default=None,
342                         action='store_true',
343                         help='Do not use ARP to find MAC addresses, '
344                              'instead use values in config file')
345
346     parser.add_argument('--loop-vm-arp', dest='loop_vm_arp',
347                         default=None,
348                         action='store_true',
349                         help='Use ARP to find MAC addresses '
350                              'instead of using values from TRex ports (VPP forwarder only)')
351
352     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
353                         default=None,
354                         action='store_true',
355                         help='Skip vswitch configuration and retrieving of stats')
356
357     parser.add_argument('--vxlan', dest='vxlan',
358                         default=None,
359                         action='store_true',
360                         help='Enable VxLan encapsulation')
361
362     parser.add_argument('--no-cleanup', dest='no_cleanup',
363                         default=None,
364                         action='store_true',
365                         help='no cleanup after run')
366
367     parser.add_argument('--cleanup', dest='cleanup',
368                         default=None,
369                         action='store_true',
370                         help='Cleanup NFVbench resources (prompt to confirm)')
371
372     parser.add_argument('--force-cleanup', dest='force_cleanup',
373                         default=None,
374                         action='store_true',
375                         help='Cleanup NFVbench resources (do not prompt)')
376
377     parser.add_argument('--restart', dest='restart',
378                         default=None,
379                         action='store_true',
380                         help='Restart TRex server')
381
382     parser.add_argument('--json', dest='json',
383                         action='store',
384                         help='store results in json format file',
385                         metavar='<path>/<filename>')
386
387     parser.add_argument('--std-json', dest='std_json',
388                         action='store',
389                         help='store results in json format file with nfvbench standard filename: '
390                              '<service-chain-type>-<service-chain-count>-<flow-count>'
391                              '-<packet-sizes>.json',
392                         metavar='<path>')
393
394     parser.add_argument('--show-default-config', dest='show_default_config',
395                         default=None,
396                         action='store_true',
397                         help='print the default config in yaml format (unedited)')
398
399     parser.add_argument('--show-config', dest='show_config',
400                         default=None,
401                         action='store_true',
402                         help='print the running config in json format')
403
404     parser.add_argument('-ss', '--show-summary', dest='summary',
405                         action='store',
406                         help='Show summary from nfvbench json file',
407                         metavar='<json>')
408
409     parser.add_argument('-v', '--version', dest='version',
410                         default=None,
411                         action='store_true',
412                         help='Show version')
413
414     parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
415                         action='append',
416                         help='Override traffic profile frame sizes',
417                         metavar='<frame_size_bytes or IMIX>')
418
419     parser.add_argument('--unidir', dest='unidir',
420                         action='store_true',
421                         default=None,
422                         help='Override traffic profile direction (requires -fs)')
423
424     parser.add_argument('--log-file', '--logfile', dest='log_file',
425                         action='store',
426                         help='Filename for saving logs',
427                         metavar='<log_file>')
428
429     parser.add_argument('--user-label', '--userlabel', dest='user_label',
430                         action='store',
431                         help='Custom label for performance records')
432
433     parser.add_argument('--hypervisor', dest='hypervisor',
434                         action='store',
435                         metavar='<hypervisor name>',
436                         help='Where chains must run ("compute", "az:", "az:compute")')
437
438     parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
439                         action='store',
440                         metavar='<vlan>',
441                         help='Port to port or port to switch to port L2 loopback with VLAN id')
442
443     parser.add_argument('--cache-size', dest='cache_size',
444                         action='store',
445                         default='0',
446                         help='Specify the FE cache size (default: 0, flow-count if < 0)')
447
448     parser.add_argument('--service-mode', dest='service_mode',
449                         action='store_true',
450                         default=False,
451                         help='Enable T-Rex service mode for debugging only')
452
453     parser.add_argument('--no-flow-stats', dest='no_flow_stats',
454                         action='store_true',
455                         default=False,
456                         help='Disable extra flow stats (on high load traffic)')
457
458     parser.add_argument('--no-latency-stats', dest='no_latency_stats',
459                         action='store_true',
460                         default=False,
461                         help='Disable flow stats for latency traffic')
462
463     parser.add_argument('--no-latency-streams', dest='no_latency_streams',
464                         action='store_true',
465                         default=False,
466                         help='Disable latency measurements (no streams)')
467
468     opts, unknown_opts = parser.parse_known_args()
469     return opts, unknown_opts
470
471
472 def load_default_config():
473     default_cfg = resource_string(__name__, "cfg.default.yaml")
474     config = config_loads(default_cfg)
475     config.name = '(built-in default config)'
476     return config, default_cfg
477
478
479 def override_custom_traffic(config, frame_sizes, unidir):
480     """Override the traffic profiles with a custom one."""
481     if frame_sizes is not None:
482         traffic_profile_name = "custom_traffic_profile"
483         config.traffic_profile = [
484             {
485                 "l2frame_size": frame_sizes,
486                 "name": traffic_profile_name
487             }
488         ]
489     else:
490         traffic_profile_name = config.traffic["profile"]
491
492     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
493     config.traffic = {
494         "bidirectional": bidirectional,
495         "profile": traffic_profile_name
496     }
497
498
499 def check_physnet(name, netattrs):
500     if not netattrs.physical_network:
501         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
502                         .format(n=name))
503     if not netattrs.segmentation_id:
504         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
505                         .format(n=name))
506
507 def status_cleanup(config, cleanup, force_cleanup):
508     LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
509     # check if another run is pending
510     ret_code = 0
511     try:
512         with utils.RunLock():
513             LOG.info('Status: idle')
514     except Exception:
515         LOG.info('Status: busy (run pending)')
516         ret_code = 1
517     # check nfvbench resources
518     if config.openrc_file and config.service_chain != ChainType.EXT:
519         cleaner = Cleaner(config)
520         count = cleaner.show_resources()
521         if count and (cleanup or force_cleanup):
522             cleaner.clean(not force_cleanup)
523     sys.exit(ret_code)
524
525 def main():
526     global fluent_logger
527     run_summary_required = False
528     try:
529         log.setup()
530         # load default config file
531         config, default_cfg = load_default_config()
532         # create factory for platform specific classes
533         try:
534             factory_module = importlib.import_module(config['factory_module'])
535             factory = getattr(factory_module, config['factory_class'])()
536         except AttributeError:
537             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
538                             .format(m=config['factory_module'], c=config['factory_class']))
539         # create config plugin for this platform
540         config_plugin = factory.get_config_plugin_class()(config)
541         config = config_plugin.get_config()
542
543         opts, unknown_opts = _parse_opts_from_cli()
544         log.set_level(debug=opts.debug)
545
546         if opts.version:
547             print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
548             sys.exit(0)
549
550         if opts.summary:
551             with open(opts.summary) as json_data:
552                 result = json.load(json_data)
553                 if opts.user_label:
554                     result['config']['user_label'] = opts.user_label
555                 print((NFVBenchSummarizer(result, fluent_logger)))
556             sys.exit(0)
557
558         # show default config in text/yaml format
559         if opts.show_default_config:
560             print((default_cfg.decode("utf-8")))
561             sys.exit(0)
562
563         config.name = ''
564         if opts.config:
565             # do not check extra_specs in flavor as it can contain any key/value pairs
566             whitelist_keys = ['extra_specs']
567             # override default config options with start config at path parsed from CLI
568             # check if it is an inline yaml/json config or a file name
569             if os.path.isfile(opts.config):
570                 LOG.info('Loading configuration file: %s', opts.config)
571                 config = config_load(opts.config, config, whitelist_keys)
572                 config.name = os.path.basename(opts.config)
573             else:
574                 LOG.info('Loading configuration string: %s', opts.config)
575                 config = config_loads(opts.config, config, whitelist_keys)
576
577         # setup the fluent logger as soon as possible right after the config plugin is called,
578         # if there is any logging or result tag is set then initialize the fluent logger
579         for fluentd in config.fluentd:
580             if fluentd.logging_tag or fluentd.result_tag:
581                 fluent_logger = FluentLogHandler(config.fluentd)
582                 LOG.addHandler(fluent_logger)
583                 break
584
585         # traffic profile override options
586         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
587
588         # copy over cli options that are used in config
589         config.generator_profile = opts.generator_profile
590         if opts.sriov:
591             config.sriov = True
592         if opts.log_file:
593             config.log_file = opts.log_file
594         if opts.service_chain:
595             config.service_chain = opts.service_chain
596         if opts.service_chain_count:
597             config.service_chain_count = opts.service_chain_count
598         if opts.no_vswitch_access:
599             config.no_vswitch_access = opts.no_vswitch_access
600         if opts.hypervisor:
601             # can be any of 'comp1', 'nova:', 'nova:comp1'
602             config.compute_nodes = opts.hypervisor
603         if opts.vxlan:
604             config.vxlan = True
605         if opts.restart:
606             config.restart = True
607         if opts.service_mode:
608             config.service_mode = True
609         if opts.no_flow_stats:
610             config.no_flow_stats = True
611         if opts.no_latency_stats:
612             config.no_latency_stats = True
613         if opts.no_latency_streams:
614             config.no_latency_streams = True
615         # port to port loopback (direct or through switch)
616         if opts.l2_loopback:
617             config.l2_loopback = True
618             if config.service_chain != ChainType.EXT:
619                 LOG.info('Changing service chain type to EXT')
620                 config.service_chain = ChainType.EXT
621             if not config.no_arp:
622                 LOG.info('Disabling ARP')
623                 config.no_arp = True
624             config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
625             LOG.info('Running L2 loopback: using EXT chain/no ARP')
626
627         if opts.use_sriov_middle_net:
628             if (not config.sriov) or (config.service_chain != ChainType.PVVP):
629                 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
630             config.use_sriov_middle_net = True
631
632         if config.sriov and config.service_chain != ChainType.EXT:
633             # if sriov is requested (does not apply to ext chains)
634             # make sure the physnet names are specified
635             check_physnet("left", config.internal_networks.left)
636             check_physnet("right", config.internal_networks.right)
637             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
638                 check_physnet("middle", config.internal_networks.middle)
639
640         # show running config in json format
641         if opts.show_config:
642             print((json.dumps(config, sort_keys=True, indent=4)))
643             sys.exit(0)
644
645         # update the config in the config plugin as it might have changed
646         # in a copy of the dict (config plugin still holds the original dict)
647         config_plugin.set_config(config)
648
649         if opts.status or opts.cleanup or opts.force_cleanup:
650             status_cleanup(config, opts.cleanup, opts.force_cleanup)
651
652         # add file log if requested
653         if config.log_file:
654             log.add_file_logger(config.log_file)
655
656         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
657             else None
658
659         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
660
661         if opts.server:
662             server = WebServer(nfvbench_instance, fluent_logger)
663             try:
664                 port = int(opts.port)
665             except ValueError:
666                 server.run(host=opts.host)
667             else:
668                 server.run(host=opts.host, port=port)
669             # server.run() should never return
670         else:
671             with utils.RunLock():
672                 run_summary_required = True
673                 if unknown_opts:
674                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
675                     LOG.error(err_msg)
676                     raise Exception(err_msg)
677
678                 # remove unfilled values
679                 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
680                 # get CLI args
681                 params = ' '.join(str(e) for e in sys.argv[1:])
682                 result = nfvbench_instance.run(opts, params)
683                 if 'error_message' in result:
684                     raise Exception(result['error_message'])
685
686                 if 'result' in result and result['status']:
687                     nfvbench_instance.save(result['result'])
688                     nfvbench_instance.prepare_summary(result['result'])
689     except Exception as exc:
690         run_summary_required = True
691         LOG.error({
692             'status': NFVBench.STATUS_ERROR,
693             'error_message': traceback.format_exc()
694         })
695         print((str(exc)))
696     finally:
697         if fluent_logger:
698             # only send a summary record if there was an actual nfvbench run or
699             # if an error/exception was logged.
700             fluent_logger.send_run_summary(run_summary_required)
701
702
703 if __name__ == '__main__':
704     main()