MPLS support + loop_vm_arp test fix
[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 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)')
245
246         self.config_plugin.validate_config(config, self.specs.openstack)
247
248
249 def _parse_opts_from_cli():
250     parser = argparse.ArgumentParser()
251
252     parser.add_argument('--status', dest='status',
253                         action='store_true',
254                         default=None,
255                         help='Provide NFVbench status')
256
257     parser.add_argument('-c', '--config', dest='config',
258                         action='store',
259                         help='Override default values with a config file or '
260                              'a yaml/json config string',
261                         metavar='<file_name_or_yaml>')
262
263     parser.add_argument('--server', dest='server',
264                         default=None,
265                         action='store_true',
266                         help='Run nfvbench in server mode')
267
268     parser.add_argument('--host', dest='host',
269                         action='store',
270                         default='0.0.0.0',
271                         help='Host IP address on which server will be listening (default 0.0.0.0)')
272
273     parser.add_argument('-p', '--port', dest='port',
274                         action='store',
275                         default=7555,
276                         help='Port on which server will be listening (default 7555)')
277
278     parser.add_argument('-sc', '--service-chain', dest='service_chain',
279                         choices=ChainType.names,
280                         action='store',
281                         help='Service chain to run')
282
283     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
284                         action='store',
285                         help='Set number of service chains to run',
286                         metavar='<service_chain_count>')
287
288     parser.add_argument('-fc', '--flow-count', dest='flow_count',
289                         action='store',
290                         help='Set number of total flows for all chains and all directions',
291                         metavar='<flow_count>')
292
293     parser.add_argument('--rate', dest='rate',
294                         action='store',
295                         help='Specify rate in pps, bps or %% as total for all directions',
296                         metavar='<rate>')
297
298     parser.add_argument('--duration', dest='duration_sec',
299                         action='store',
300                         help='Set duration to run traffic generator (in seconds)',
301                         metavar='<duration_sec>')
302
303     parser.add_argument('--interval', dest='interval_sec',
304                         action='store',
305                         help='Set interval to record traffic generator stats (in seconds)',
306                         metavar='<interval_sec>')
307
308     parser.add_argument('--inter-node', dest='inter_node',
309                         default=None,
310                         action='store_true',
311                         help='(deprecated)')
312
313     parser.add_argument('--sriov', dest='sriov',
314                         default=None,
315                         action='store_true',
316                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
317
318     parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
319                         default=None,
320                         action='store_true',
321                         help='Use SRIOV to handle the middle network traffic '
322                              '(PVVP with SRIOV only)')
323
324     parser.add_argument('-d', '--debug', dest='debug',
325                         action='store_true',
326                         default=None,
327                         help='print debug messages (verbose)')
328
329     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
330                         action='store',
331                         help='Traffic generator profile to use')
332
333     parser.add_argument('-l3', '--l3-router', dest='l3_router',
334                         default=None,
335                         action='store_true',
336                         help='Use L3 neutron routers to handle traffic')
337
338     parser.add_argument('-0', '--no-traffic', dest='no_traffic',
339                         default=None,
340                         action='store_true',
341                         help='Check config and connectivity only - do not generate traffic')
342
343     parser.add_argument('--no-arp', dest='no_arp',
344                         default=None,
345                         action='store_true',
346                         help='Do not use ARP to find MAC addresses, '
347                              'instead use values in config file')
348
349     parser.add_argument('--loop-vm-arp', dest='loop_vm_arp',
350                         default=None,
351                         action='store_true',
352                         help='Use ARP to find MAC addresses '
353                              'instead of using values from TRex ports (VPP forwarder only)')
354
355     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
356                         default=None,
357                         action='store_true',
358                         help='Skip vswitch configuration and retrieving of stats')
359
360     parser.add_argument('--vxlan', dest='vxlan',
361                         default=None,
362                         action='store_true',
363                         help='Enable VxLan encapsulation')
364
365     parser.add_argument('--mpls', dest='mpls',
366                         default=None,
367                         action='store_true',
368                         help='Enable MPLS encapsulation')
369
370     parser.add_argument('--no-cleanup', dest='no_cleanup',
371                         default=None,
372                         action='store_true',
373                         help='no cleanup after run')
374
375     parser.add_argument('--cleanup', dest='cleanup',
376                         default=None,
377                         action='store_true',
378                         help='Cleanup NFVbench resources (prompt to confirm)')
379
380     parser.add_argument('--force-cleanup', dest='force_cleanup',
381                         default=None,
382                         action='store_true',
383                         help='Cleanup NFVbench resources (do not prompt)')
384
385     parser.add_argument('--restart', dest='restart',
386                         default=None,
387                         action='store_true',
388                         help='Restart TRex server')
389
390     parser.add_argument('--json', dest='json',
391                         action='store',
392                         help='store results in json format file',
393                         metavar='<path>/<filename>')
394
395     parser.add_argument('--std-json', dest='std_json',
396                         action='store',
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',
400                         metavar='<path>')
401
402     parser.add_argument('--show-default-config', dest='show_default_config',
403                         default=None,
404                         action='store_true',
405                         help='print the default config in yaml format (unedited)')
406
407     parser.add_argument('--show-config', dest='show_config',
408                         default=None,
409                         action='store_true',
410                         help='print the running config in json format')
411
412     parser.add_argument('-ss', '--show-summary', dest='summary',
413                         action='store',
414                         help='Show summary from nfvbench json file',
415                         metavar='<json>')
416
417     parser.add_argument('-v', '--version', dest='version',
418                         default=None,
419                         action='store_true',
420                         help='Show version')
421
422     parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
423                         action='append',
424                         help='Override traffic profile frame sizes',
425                         metavar='<frame_size_bytes or IMIX>')
426
427     parser.add_argument('--unidir', dest='unidir',
428                         action='store_true',
429                         default=None,
430                         help='Override traffic profile direction (requires -fs)')
431
432     parser.add_argument('--log-file', '--logfile', dest='log_file',
433                         action='store',
434                         help='Filename for saving logs',
435                         metavar='<log_file>')
436
437     parser.add_argument('--user-label', '--userlabel', dest='user_label',
438                         action='store',
439                         help='Custom label for performance records')
440
441     parser.add_argument('--hypervisor', dest='hypervisor',
442                         action='store',
443                         metavar='<hypervisor name>',
444                         help='Where chains must run ("compute", "az:", "az:compute")')
445
446     parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
447                         action='store',
448                         metavar='<vlan>',
449                         help='Port to port or port to switch to port L2 loopback with VLAN id')
450
451     parser.add_argument('--cache-size', dest='cache_size',
452                         action='store',
453                         default='0',
454                         help='Specify the FE cache size (default: 0, flow-count if < 0)')
455
456     parser.add_argument('--service-mode', dest='service_mode',
457                         action='store_true',
458                         default=False,
459                         help='Enable T-Rex service mode for debugging only')
460
461     parser.add_argument('--no-flow-stats', dest='no_flow_stats',
462                         action='store_true',
463                         default=False,
464                         help='Disable extra flow stats (on high load traffic)')
465
466     parser.add_argument('--no-latency-stats', dest='no_latency_stats',
467                         action='store_true',
468                         default=False,
469                         help='Disable flow stats for latency traffic')
470
471     parser.add_argument('--no-latency-streams', dest='no_latency_streams',
472                         action='store_true',
473                         default=False,
474                         help='Disable latency measurements (no streams)')
475
476     opts, unknown_opts = parser.parse_known_args()
477     return opts, unknown_opts
478
479
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
485
486
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 = [
492             {
493                 "l2frame_size": frame_sizes,
494                 "name": traffic_profile_name
495             }
496         ]
497     else:
498         traffic_profile_name = config.traffic["profile"]
499
500     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
501     config.traffic = {
502         "bidirectional": bidirectional,
503         "profile": traffic_profile_name
504     }
505
506
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"
510                         .format(n=name))
511     if not netattrs.segmentation_id:
512         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
513                         .format(n=name))
514
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
518     ret_code = 0
519     try:
520         with utils.RunLock():
521             LOG.info('Status: idle')
522     except Exception:
523         LOG.info('Status: busy (run pending)')
524         ret_code = 1
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)
531     sys.exit(ret_code)
532
533 def main():
534     global fluent_logger
535     run_summary_required = False
536     try:
537         log.setup()
538         # load default config file
539         config, default_cfg = load_default_config()
540         # create factory for platform specific classes
541         try:
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()
550
551         opts, unknown_opts = _parse_opts_from_cli()
552         log.set_level(debug=opts.debug)
553
554         if opts.version:
555             print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
556             sys.exit(0)
557
558         if opts.summary:
559             with open(opts.summary) as json_data:
560                 result = json.load(json_data)
561                 if opts.user_label:
562                     result['config']['user_label'] = opts.user_label
563                 print((NFVBenchSummarizer(result, fluent_logger)))
564             sys.exit(0)
565
566         # show default config in text/yaml format
567         if opts.show_default_config:
568             print((default_cfg.decode("utf-8")))
569             sys.exit(0)
570
571         config.name = ''
572         if opts.config:
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)
581             else:
582                 LOG.info('Loading configuration string: %s', opts.config)
583                 config = config_loads(opts.config, config, whitelist_keys)
584
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)
591                 break
592
593         # traffic profile override options
594         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
595
596         # copy over cli options that are used in config
597         config.generator_profile = opts.generator_profile
598         if opts.sriov:
599             config.sriov = True
600         if opts.log_file:
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
608         if opts.hypervisor:
609             # can be any of 'comp1', 'nova:', 'nova:comp1'
610             config.compute_nodes = opts.hypervisor
611         if opts.vxlan:
612             config.vxlan = True
613         if opts.mpls:
614             config.mpls = True
615         if opts.restart:
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)
626         if opts.l2_loopback:
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')
633                 config.no_arp = True
634             config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
635             LOG.info('Running L2 loopback: using EXT chain/no ARP')
636
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
641
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)
649
650         # show running config in json format
651         if opts.show_config:
652             print((json.dumps(config, sort_keys=True, indent=4)))
653             sys.exit(0)
654
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)
658
659         if opts.status or opts.cleanup or opts.force_cleanup:
660             status_cleanup(config, opts.cleanup, opts.force_cleanup)
661
662         # add file log if requested
663         if config.log_file:
664             log.add_file_logger(config.log_file)
665
666         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
667             else None
668
669         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
670
671         if opts.server:
672             server = WebServer(nfvbench_instance, fluent_logger)
673             try:
674                 port = int(opts.port)
675             except ValueError:
676                 server.run(host=opts.host)
677             else:
678                 server.run(host=opts.host, port=port)
679             # server.run() should never return
680         else:
681             with utils.RunLock():
682                 run_summary_required = True
683                 if unknown_opts:
684                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
685                     LOG.error(err_msg)
686                     raise Exception(err_msg)
687
688                 # remove unfilled values
689                 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
690                 # get CLI args
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'])
695
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
701         LOG.error({
702             'status': NFVBench.STATUS_ERROR,
703             'error_message': traceback.format_exc()
704         })
705         print((str(exc)))
706     finally:
707         if fluent_logger:
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)
711
712
713 if __name__ == '__main__':
714     main()