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