19c402fcdceddeff063beab683cb166a5946a0c9
[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
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                                self.config.user_id,
170                                self.config.group_id)
171
172     def _update_config(self, opts):
173         """Recalculate the running config based on the base config and opts.
174
175         Sanity check on the config is done here as well.
176         """
177         self.config = AttrDict(dict(self.base_config))
178         # Update log file handler if needed after a config update (REST mode)
179         if 'log_file' in opts:
180             if opts['log_file']:
181                 (path, _filename) = os.path.split(opts['log_file'])
182                 if not os.path.exists(path):
183                     LOG.warning(
184                         'Path %s does not exist. Please verify root path is shared with host. Path '
185                         'will be created.', path)
186                     os.makedirs(path)
187                     LOG.info('%s is created.', path)
188                 for h in log.getLogger().handlers:
189                     if isinstance(h, FileHandler) and h.baseFilename != opts['log_file']:
190                         # clean log file handler
191                         log.getLogger().removeHandler(h)
192                 # add handler if not existing to avoid duplicates handlers
193                 if len(log.getLogger().handlers) == 1:
194                     log.add_file_logger(opts['log_file'])
195
196         self.config.update(opts)
197         config = self.config
198
199         config.service_chain = config.service_chain.upper()
200         config.service_chain_count = int(config.service_chain_count)
201         if config.l2_loopback:
202             # force the number of chains to be 1 in case of l2 loopback
203             config.service_chain_count = 1
204             config.service_chain = ChainType.EXT
205             config.no_arp = True
206             LOG.info('Running L2 loopback: using EXT chain/no ARP')
207
208         # traffic profile override options
209         if 'frame_sizes' in opts:
210             unidir = False
211             if 'unidir' in opts:
212                 unidir = opts['unidir']
213             override_custom_traffic(config, opts['frame_sizes'], unidir)
214             LOG.info("Frame size has been set to %s for current configuration", opts['frame_sizes'])
215
216         config.flow_count = utils.parse_flow_count(config.flow_count)
217         required_flow_count = config.service_chain_count * 2
218         if config.flow_count < required_flow_count:
219             LOG.info("Flow count %d has been set to minimum value of '%d' "
220                      "for current configuration", config.flow_count,
221                      required_flow_count)
222             config.flow_count = required_flow_count
223
224         if config.flow_count % 2:
225             config.flow_count += 1
226
227         # Possibly adjust the cache size
228         if config.cache_size < 0:
229             config.cache_size = config.flow_count
230
231         # The size must be capped to 10000 (where does this limit come from?)
232         if config.cache_size > 10000:
233             config.cache_size = 10000
234
235         config.duration_sec = float(config.duration_sec)
236         config.interval_sec = float(config.interval_sec)
237         config.pause_sec = float(config.pause_sec)
238
239         if config.traffic is None or not config.traffic:
240             raise Exception("Missing traffic property in configuration")
241
242         if config.openrc_file:
243             config.openrc_file = os.path.expanduser(config.openrc_file)
244             if config.flavor.vcpus < 2:
245                 raise Exception("Flavor vcpus must be >= 2")
246
247         config.ndr_run = (not config.no_traffic and
248                           'ndr' in config.rate.strip().lower().split('_'))
249         config.pdr_run = (not config.no_traffic and
250                           'pdr' in config.rate.strip().lower().split('_'))
251         config.single_run = (not config.no_traffic and
252                              not (config.ndr_run or config.pdr_run))
253
254         config.json_file = config.json if config.json else None
255         if config.json_file:
256             (path, _filename) = os.path.split(config.json)
257             if not os.path.exists(path):
258                 raise Exception('Please provide existing path for storing results in JSON file. '
259                                 'Path used: {path}'.format(path=path))
260
261         config.std_json_path = config.std_json if config.std_json else None
262         if config.std_json_path:
263             if not os.path.exists(config.std_json):
264                 raise Exception('Please provide existing path for storing results in JSON file. '
265                                 'Path used: {path}'.format(path=config.std_json_path))
266
267         # Check that multiqueue is between 1 and 8 (8 is the max allowed by libvirt/qemu)
268         if config.vif_multiqueue_size < 1 or config.vif_multiqueue_size > 8:
269             raise Exception('vif_multiqueue_size (%d) must be in [1..8]' %
270                             config.vif_multiqueue_size)
271
272         # VxLAN and MPLS sanity checks
273         if config.vxlan or config.mpls:
274             if config.vlan_tagging:
275                 config.vlan_tagging = False
276                 config.no_latency_streams = True
277                 config.no_latency_stats = True
278                 config.no_flow_stats = True
279                 LOG.info('VxLAN or MPLS: vlan_tagging forced to False '
280                          '(inner VLAN tagging must be disabled)')
281
282         self.config_plugin.validate_config(config, self.specs.openstack)
283
284
285 def bool_arg(x):
286     """Argument type to be used in parser.add_argument()
287     When a boolean like value is expected to be given
288     """
289     return (str(x).lower() != 'false') \
290         and (str(x).lower() != 'no') \
291         and (str(x).lower() != '0')
292
293
294 def int_arg(x):
295     """Argument type to be used in parser.add_argument()
296     When an integer type value is expected to be given
297     (returns 0 if argument is invalid, hexa accepted)
298     """
299     return int(x, 0)
300
301
302 def _parse_opts_from_cli():
303     parser = argparse.ArgumentParser()
304
305     parser.add_argument('--status', dest='status',
306                         action='store_true',
307                         default=None,
308                         help='Provide NFVbench status')
309
310     parser.add_argument('-c', '--config', dest='config',
311                         action='store',
312                         help='Override default values with a config file or '
313                              'a yaml/json config string',
314                         metavar='<file_name_or_yaml>')
315
316     parser.add_argument('--server', dest='server',
317                         default=None,
318                         action='store_true',
319                         help='Run nfvbench in server mode')
320
321     parser.add_argument('--host', dest='host',
322                         action='store',
323                         default='0.0.0.0',
324                         help='Host IP address on which server will be listening (default 0.0.0.0)')
325
326     parser.add_argument('-p', '--port', dest='port',
327                         action='store',
328                         default=7555,
329                         help='Port on which server will be listening (default 7555)')
330
331     parser.add_argument('-sc', '--service-chain', dest='service_chain',
332                         choices=ChainType.names,
333                         action='store',
334                         help='Service chain to run')
335
336     parser.add_argument('-scc', '--service-chain-count', dest='service_chain_count',
337                         action='store',
338                         help='Set number of service chains to run',
339                         metavar='<service_chain_count>')
340
341     parser.add_argument('-fc', '--flow-count', dest='flow_count',
342                         action='store',
343                         help='Set number of total flows for all chains and all directions',
344                         metavar='<flow_count>')
345
346     parser.add_argument('--rate', dest='rate',
347                         action='store',
348                         help='Specify rate in pps, bps or %% as total for all directions',
349                         metavar='<rate>')
350
351     parser.add_argument('--duration', dest='duration_sec',
352                         action='store',
353                         help='Set duration to run traffic generator (in seconds)',
354                         metavar='<duration_sec>')
355
356     parser.add_argument('--interval', dest='interval_sec',
357                         action='store',
358                         help='Set interval to record traffic generator stats (in seconds)',
359                         metavar='<interval_sec>')
360
361     parser.add_argument('--inter-node', dest='inter_node',
362                         default=None,
363                         action='store_true',
364                         help='(deprecated)')
365
366     parser.add_argument('--sriov', dest='sriov',
367                         default=None,
368                         action='store_true',
369                         help='Use SRIOV (no vswitch - requires SRIOV support in compute nodes)')
370
371     parser.add_argument('--use-sriov-middle-net', dest='use_sriov_middle_net',
372                         default=None,
373                         action='store_true',
374                         help='Use SRIOV to handle the middle network traffic '
375                              '(PVVP with SRIOV only)')
376
377     parser.add_argument('-d', '--debug', dest='debug',
378                         action='store_true',
379                         default=None,
380                         help='print debug messages (verbose)')
381
382     parser.add_argument('-g', '--traffic-gen', dest='generator_profile',
383                         action='store',
384                         help='Traffic generator profile to use')
385
386     parser.add_argument('-l3', '--l3-router', dest='l3_router',
387                         default=None,
388                         action='store_true',
389                         help='Use L3 neutron routers to handle traffic')
390
391     parser.add_argument('-0', '--no-traffic', dest='no_traffic',
392                         default=None,
393                         action='store_true',
394                         help='Check config and connectivity only - do not generate traffic')
395
396     parser.add_argument('--no-arp', dest='no_arp',
397                         default=None,
398                         action='store_true',
399                         help='Do not use ARP to find MAC addresses, '
400                              'instead use values in config file')
401
402     parser.add_argument('--loop-vm-arp', dest='loop_vm_arp',
403                         default=None,
404                         action='store_true',
405                         help='Use ARP to find MAC addresses '
406                              'instead of using values from TRex ports (VPP forwarder only)')
407
408     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
409                         default=None,
410                         action='store_true',
411                         help='Skip vswitch configuration and retrieving of stats')
412
413     parser.add_argument('--vxlan', dest='vxlan',
414                         default=None,
415                         action='store_true',
416                         help='Enable VxLan encapsulation')
417
418     parser.add_argument('--mpls', dest='mpls',
419                         default=None,
420                         action='store_true',
421                         help='Enable MPLS encapsulation')
422
423     parser.add_argument('--no-cleanup', dest='no_cleanup',
424                         default=None,
425                         action='store_true',
426                         help='no cleanup after run')
427
428     parser.add_argument('--cleanup', dest='cleanup',
429                         default=None,
430                         action='store_true',
431                         help='Cleanup NFVbench resources (prompt to confirm)')
432
433     parser.add_argument('--force-cleanup', dest='force_cleanup',
434                         default=None,
435                         action='store_true',
436                         help='Cleanup NFVbench resources (do not prompt)')
437
438     parser.add_argument('--restart', dest='restart',
439                         default=None,
440                         action='store_true',
441                         help='Restart TRex server')
442
443     parser.add_argument('--json', dest='json',
444                         action='store',
445                         help='store results in json format file',
446                         metavar='<path>/<filename>')
447
448     parser.add_argument('--std-json', dest='std_json',
449                         action='store',
450                         help='store results in json format file with nfvbench standard filename: '
451                              '<service-chain-type>-<service-chain-count>-<flow-count>'
452                              '-<packet-sizes>.json',
453                         metavar='<path>')
454
455     parser.add_argument('--show-default-config', dest='show_default_config',
456                         default=None,
457                         action='store_true',
458                         help='print the default config in yaml format (unedited)')
459
460     parser.add_argument('--show-config', dest='show_config',
461                         default=None,
462                         action='store_true',
463                         help='print the running config in json format')
464
465     parser.add_argument('-ss', '--show-summary', dest='summary',
466                         action='store',
467                         help='Show summary from nfvbench json file',
468                         metavar='<json>')
469
470     parser.add_argument('-v', '--version', dest='version',
471                         default=None,
472                         action='store_true',
473                         help='Show version')
474
475     parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
476                         action='append',
477                         help='Override traffic profile frame sizes',
478                         metavar='<frame_size_bytes or IMIX>')
479
480     parser.add_argument('--unidir', dest='unidir',
481                         action='store_true',
482                         default=None,
483                         help='Override traffic profile direction (requires -fs)')
484
485     parser.add_argument('--log-file', '--logfile', dest='log_file',
486                         action='store',
487                         help='Filename for saving logs',
488                         metavar='<log_file>')
489
490     parser.add_argument('--user-label', '--userlabel', dest='user_label',
491                         action='store',
492                         help='Custom label for performance records')
493
494     parser.add_argument('--hypervisor', dest='hypervisor',
495                         action='store',
496                         metavar='<hypervisor name>',
497                         help='Where chains must run ("compute", "az:", "az:compute")')
498
499     parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
500                         action='store',
501                         metavar='<vlan>',
502                         help='Port to port or port to switch to port L2 loopback with VLAN id')
503
504     parser.add_argument('--user-info', dest='user_info',
505                         action='store',
506                         metavar='<data>',
507                         help='Custom data to be included as is in the json report config branch - '
508                                + ' example, pay attention! no space: '
509                                + '--user-info=\'{"status":"explore","description":'
510                                + '{"target":"lab","ok":true,"version":2020}}\'')
511
512     parser.add_argument('--vlan-tagging', dest='vlan_tagging',
513                         type=bool_arg,
514                         metavar='<boolean>',
515                         action='store',
516                         default=None,
517                         help='Override the NFVbench \'vlan_tagging\' parameter')
518
519     parser.add_argument('--intf-speed', dest='intf_speed',
520                         metavar='<speed>',
521                         action='store',
522                         default=None,
523                         help='Override the NFVbench \'intf_speed\' '
524                                 + 'parameter (e.g. 10Gbps, auto, 16.72Gbps)')
525
526     parser.add_argument('--cores', dest='cores',
527                         type=int_arg,
528                         metavar='<number>',
529                         action='store',
530                         default=None,
531                         help='Override the T-Rex \'cores\' parameter')
532
533     parser.add_argument('--cache-size', dest='cache_size',
534                         type=int_arg,
535                         metavar='<size>',
536                         action='store',
537                         default='0',
538                         help='Specify the FE cache size (default: 0, flow-count if < 0)')
539
540     parser.add_argument('--service-mode', dest='service_mode',
541                         action='store_true',
542                         default=False,
543                         help='Enable T-Rex service mode (for debugging purpose)')
544
545     parser.add_argument('--no-e2e-check', dest='no_e2e_check',
546                         action='store_true',
547                         default=False,
548                         help='Skip "end to end" connectivity check (on test purpose)')
549
550     parser.add_argument('--no-flow-stats', dest='no_flow_stats',
551                         action='store_true',
552                         default=False,
553                         help='Disable additional flow stats (on high load traffic)')
554
555     parser.add_argument('--no-latency-stats', dest='no_latency_stats',
556                         action='store_true',
557                         default=False,
558                         help='Disable flow stats for latency traffic')
559
560     parser.add_argument('--no-latency-streams', dest='no_latency_streams',
561                         action='store_true',
562                         default=False,
563                         help='Disable latency measurements (no streams)')
564
565     parser.add_argument('--user-id', dest='user_id',
566                         type=int_arg,
567                         metavar='<uid>',
568                         action='store',
569                         default=None,
570                         help='Change json/log files ownership with this user (int)')
571
572     parser.add_argument('--group-id', dest='group_id',
573                         type=int_arg,
574                         metavar='<gid>',
575                         action='store',
576                         default=None,
577                         help='Change json/log files ownership with this group (int)')
578
579     parser.add_argument('--show-trex-log', dest='show_trex_log',
580                         default=None,
581                         action='store_true',
582                         help='Show the current TRex local server log file contents'
583                                + ' => diagnostic/help in case of configuration problems')
584
585     parser.add_argument('--debug-mask', dest='debug_mask',
586                         type=int_arg,
587                         metavar='<mask>',
588                         action='store',
589                         default='0x00000000',
590                         help='General purpose register (debugging flags), '
591                                 + 'the hexadecimal notation (0x...) is accepted.'
592                                 + 'Designed for development needs.')
593
594     opts, unknown_opts = parser.parse_known_args()
595     return opts, unknown_opts
596
597
598 def load_default_config():
599     default_cfg = resource_string(__name__, "cfg.default.yaml")
600     config = config_loads(default_cfg)
601     config.name = '(built-in default config)'
602     return config, default_cfg
603
604
605 def override_custom_traffic(config, frame_sizes, unidir):
606     """Override the traffic profiles with a custom one."""
607     if frame_sizes is not None:
608         traffic_profile_name = "custom_traffic_profile"
609         config.traffic_profile = [
610             {
611                 "l2frame_size": frame_sizes,
612                 "name": traffic_profile_name
613             }
614         ]
615     else:
616         traffic_profile_name = config.traffic["profile"]
617
618     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
619     config.traffic = {
620         "bidirectional": bidirectional,
621         "profile": traffic_profile_name
622     }
623
624
625 def check_physnet(name, netattrs):
626     if not netattrs.physical_network:
627         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
628                         .format(n=name))
629     if not netattrs.segmentation_id:
630         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
631                         .format(n=name))
632
633 def status_cleanup(config, cleanup, force_cleanup):
634     LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
635     # check if another run is pending
636     ret_code = 0
637     try:
638         with utils.RunLock():
639             LOG.info('Status: idle')
640     except Exception:
641         LOG.info('Status: busy (run pending)')
642         ret_code = 1
643     # check nfvbench resources
644     if config.openrc_file and config.service_chain != ChainType.EXT:
645         cleaner = Cleaner(config)
646         count = cleaner.show_resources()
647         if count and (cleanup or force_cleanup):
648             cleaner.clean(not force_cleanup)
649     sys.exit(ret_code)
650
651 def main():
652     global fluent_logger
653     run_summary_required = False
654     try:
655         log.setup()
656         # load default config file
657         config, default_cfg = load_default_config()
658         # possibly override the default user_id & group_id values
659         if 'USER_ID' in os.environ:
660             config.user_id = int(os.environ['USER_ID'])
661         if 'GROUP_ID' in os.environ:
662             config.group_id = int(os.environ['GROUP_ID'])
663
664         # create factory for platform specific classes
665         try:
666             factory_module = importlib.import_module(config['factory_module'])
667             factory = getattr(factory_module, config['factory_class'])()
668         except AttributeError:
669             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
670                             .format(m=config['factory_module'],
671                                     c=config['factory_class'])) from AttributeError
672         # create config plugin for this platform
673         config_plugin = factory.get_config_plugin_class()(config)
674         config = config_plugin.get_config()
675
676         opts, unknown_opts = _parse_opts_from_cli()
677         log.set_level(debug=opts.debug)
678
679         if opts.version:
680             print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
681             sys.exit(0)
682
683         if opts.summary:
684             with open(opts.summary) as json_data:
685                 result = json.load(json_data)
686                 if opts.user_label:
687                     result['config']['user_label'] = opts.user_label
688                 print((NFVBenchSummarizer(result, fluent_logger)))
689             sys.exit(0)
690
691         # show default config in text/yaml format
692         if opts.show_default_config:
693             print((default_cfg.decode("utf-8")))
694             sys.exit(0)
695
696         # dump the contents of the trex log file
697         if opts.show_trex_log:
698             try:
699                 print(open('/tmp/trex.log').read(), end="")
700             except FileNotFoundError:
701                 print("No TRex log file found!")
702             sys.exit(0)
703
704         config.name = ''
705         if opts.config:
706             # do not check extra_specs in flavor as it can contain any key/value pairs
707             # the same principle applies also to the optional user_info open property
708             whitelist_keys = ['extra_specs', 'user_info']
709             # override default config options with start config at path parsed from CLI
710             # check if it is an inline yaml/json config or a file name
711             if os.path.isfile(opts.config):
712                 LOG.info('Loading configuration file: %s', opts.config)
713                 config = config_load(opts.config, config, whitelist_keys)
714                 config.name = os.path.basename(opts.config)
715             else:
716                 LOG.info('Loading configuration string: %s', opts.config)
717                 config = config_loads(opts.config, config, whitelist_keys)
718
719         # setup the fluent logger as soon as possible right after the config plugin is called,
720         # if there is any logging or result tag is set then initialize the fluent logger
721         for fluentd in config.fluentd:
722             if fluentd.logging_tag or fluentd.result_tag:
723                 fluent_logger = FluentLogHandler(config.fluentd)
724                 LOG.addHandler(fluent_logger)
725                 break
726
727         # convert 'user_info' opt from json string to dictionnary
728         # and merge the result with the current config dictionnary
729         if opts.user_info:
730             opts.user_info = json.loads(opts.user_info)
731             if config.user_info:
732                 config.user_info = config.user_info + opts.user_info
733             else:
734                 config.user_info = opts.user_info
735             # hide the option to further _update_config()
736             opts.user_info = None
737
738         # traffic profile override options
739         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
740
741         # copy over cli options that are used in config
742         config.generator_profile = opts.generator_profile
743         if opts.sriov:
744             config.sriov = True
745         if opts.log_file:
746             config.log_file = opts.log_file
747         if opts.service_chain:
748             config.service_chain = opts.service_chain
749         if opts.service_chain_count:
750             config.service_chain_count = opts.service_chain_count
751         if opts.no_vswitch_access:
752             config.no_vswitch_access = opts.no_vswitch_access
753         if opts.hypervisor:
754             # can be any of 'comp1', 'nova:', 'nova:comp1'
755             config.compute_nodes = opts.hypervisor
756         if opts.vxlan:
757             config.vxlan = True
758         if opts.mpls:
759             config.mpls = True
760         if opts.restart:
761             config.restart = True
762         if opts.service_mode:
763             config.service_mode = True
764         if opts.no_flow_stats:
765             config.no_flow_stats = True
766         if opts.no_latency_stats:
767             config.no_latency_stats = True
768         if opts.no_latency_streams:
769             config.no_latency_streams = True
770         # port to port loopback (direct or through switch)
771         if opts.l2_loopback:
772             config.l2_loopback = True
773             if config.service_chain != ChainType.EXT:
774                 LOG.info('Changing service chain type to EXT')
775                 config.service_chain = ChainType.EXT
776             if not config.no_arp:
777                 LOG.info('Disabling ARP')
778                 config.no_arp = True
779             config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
780             LOG.info('Running L2 loopback: using EXT chain/no ARP')
781
782         if opts.use_sriov_middle_net:
783             if (not config.sriov) or (config.service_chain != ChainType.PVVP):
784                 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
785             config.use_sriov_middle_net = True
786
787         if config.sriov and config.service_chain != ChainType.EXT:
788             # if sriov is requested (does not apply to ext chains)
789             # make sure the physnet names are specified
790             check_physnet("left", config.internal_networks.left)
791             check_physnet("right", config.internal_networks.right)
792             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
793                 check_physnet("middle", config.internal_networks.middle)
794
795         # show running config in json format
796         if opts.show_config:
797             print((json.dumps(config, sort_keys=True, indent=4)))
798             sys.exit(0)
799
800         # update the config in the config plugin as it might have changed
801         # in a copy of the dict (config plugin still holds the original dict)
802         config_plugin.set_config(config)
803
804         if opts.status or opts.cleanup or opts.force_cleanup:
805             status_cleanup(config, opts.cleanup, opts.force_cleanup)
806
807         # add file log if requested
808         if config.log_file:
809             log.add_file_logger(config.log_file)
810             # possibly change file ownership
811             uid = config.user_id
812             gid = config.group_id
813             if gid is None:
814                 gid = uid
815             if uid is not None:
816                 os.chown(config.log_file, uid, gid)
817
818         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
819             else None
820
821         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
822
823         if opts.server:
824             server = WebServer(nfvbench_instance, fluent_logger)
825             try:
826                 port = int(opts.port)
827             except ValueError:
828                 server.run(host=opts.host)
829             else:
830                 server.run(host=opts.host, port=port)
831             # server.run() should never return
832         else:
833             with utils.RunLock():
834                 run_summary_required = True
835                 if unknown_opts:
836                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
837                     LOG.error(err_msg)
838                     raise Exception(err_msg)
839
840                 # remove unfilled values
841                 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
842                 # get CLI args
843                 params = ' '.join(str(e) for e in sys.argv[1:])
844                 result = nfvbench_instance.run(opts, params)
845                 if 'error_message' in result:
846                     raise Exception(result['error_message'])
847
848                 if 'result' in result and result['status']:
849                     nfvbench_instance.save(result['result'])
850                     nfvbench_instance.prepare_summary(result['result'])
851     except Exception as exc:
852         run_summary_required = True
853         LOG.error({
854             'status': NFVBench.STATUS_ERROR,
855             'error_message': traceback.format_exc()
856         })
857         print((str(exc)))
858     finally:
859         if fluent_logger:
860             # only send a summary record if there was an actual nfvbench run or
861             # if an error/exception was logged.
862             fluent_logger.send_run_summary(run_summary_required)
863
864
865 if __name__ == '__main__':
866     main()