NFVBENCH-192: Complete/fix hdrh related processings to consider all cases
[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 '
508                              'in the json report config branch - '
509                              ' example, pay attention! no space: '
510                              '--user-info=\'{"status":"explore","description":'
511                              '{"target":"lab","ok":true,"version":2020}}\' - '
512                              'this option may be repeated; given data will be merged.')
513
514     parser.add_argument('--vlan-tagging', dest='vlan_tagging',
515                         type=bool_arg,
516                         metavar='<boolean>',
517                         action='store',
518                         default=None,
519                         help='Override the NFVbench \'vlan_tagging\' parameter')
520
521     parser.add_argument('--intf-speed', dest='intf_speed',
522                         metavar='<speed>',
523                         action='store',
524                         default=None,
525                         help='Override the NFVbench \'intf_speed\' '
526                              'parameter (e.g. 10Gbps, auto, 16.72Gbps)')
527
528     parser.add_argument('--cores', dest='cores',
529                         type=int_arg,
530                         metavar='<number>',
531                         action='store',
532                         default=None,
533                         help='Override the T-Rex \'cores\' parameter')
534
535     parser.add_argument('--cache-size', dest='cache_size',
536                         type=int_arg,
537                         metavar='<size>',
538                         action='store',
539                         default='0',
540                         help='Specify the FE cache size (default: 0, flow-count if < 0)')
541
542     parser.add_argument('--service-mode', dest='service_mode',
543                         action='store_true',
544                         default=False,
545                         help='Enable T-Rex service mode (for debugging purpose)')
546
547     parser.add_argument('--no-e2e-check', dest='no_e2e_check',
548                         action='store_true',
549                         default=False,
550                         help='Skip "end to end" connectivity check (on test purpose)')
551
552     parser.add_argument('--no-flow-stats', dest='no_flow_stats',
553                         action='store_true',
554                         default=False,
555                         help='Disable additional flow stats (on high load traffic)')
556
557     parser.add_argument('--no-latency-stats', dest='no_latency_stats',
558                         action='store_true',
559                         default=False,
560                         help='Disable flow stats for latency traffic')
561
562     parser.add_argument('--no-latency-streams', dest='no_latency_streams',
563                         action='store_true',
564                         default=False,
565                         help='Disable latency measurements (no streams)')
566
567     parser.add_argument('--user-id', dest='user_id',
568                         type=int_arg,
569                         metavar='<uid>',
570                         action='store',
571                         default=None,
572                         help='Change json/log files ownership with this user (int)')
573
574     parser.add_argument('--group-id', dest='group_id',
575                         type=int_arg,
576                         metavar='<gid>',
577                         action='store',
578                         default=None,
579                         help='Change json/log files ownership with this group (int)')
580
581     parser.add_argument('--show-trex-log', dest='show_trex_log',
582                         default=None,
583                         action='store_true',
584                         help='Show the current TRex local server log file contents'
585                              ' => diagnostic/help in case of configuration problems')
586
587     parser.add_argument('--debug-mask', dest='debug_mask',
588                         type=int_arg,
589                         metavar='<mask>',
590                         action='store',
591                         default=None,
592                         help='General purpose register (debugging flags), '
593                              'the hexadecimal notation (0x...) is accepted.'
594                              'Designed for development needs (default: 0).')
595
596     opts, unknown_opts = parser.parse_known_args()
597     return opts, unknown_opts
598
599
600 def load_default_config():
601     default_cfg = resource_string(__name__, "cfg.default.yaml")
602     config = config_loads(default_cfg)
603     config.name = '(built-in default config)'
604     return config, default_cfg
605
606
607 def override_custom_traffic(config, frame_sizes, unidir):
608     """Override the traffic profiles with a custom one."""
609     if frame_sizes is not None:
610         traffic_profile_name = "custom_traffic_profile"
611         config.traffic_profile = [
612             {
613                 "l2frame_size": frame_sizes,
614                 "name": traffic_profile_name
615             }
616         ]
617     else:
618         traffic_profile_name = config.traffic["profile"]
619
620     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
621     config.traffic = {
622         "bidirectional": bidirectional,
623         "profile": traffic_profile_name
624     }
625
626
627 def check_physnet(name, netattrs):
628     if not netattrs.physical_network:
629         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
630                         .format(n=name))
631     if not netattrs.segmentation_id:
632         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
633                         .format(n=name))
634
635 def status_cleanup(config, cleanup, force_cleanup):
636     LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
637     # check if another run is pending
638     ret_code = 0
639     try:
640         with utils.RunLock():
641             LOG.info('Status: idle')
642     except Exception:
643         LOG.info('Status: busy (run pending)')
644         ret_code = 1
645     # check nfvbench resources
646     if config.openrc_file and config.service_chain != ChainType.EXT:
647         cleaner = Cleaner(config)
648         count = cleaner.show_resources()
649         if count and (cleanup or force_cleanup):
650             cleaner.clean(not force_cleanup)
651     sys.exit(ret_code)
652
653 def main():
654     global fluent_logger
655     run_summary_required = False
656     try:
657         log.setup()
658         # load default config file
659         config, default_cfg = load_default_config()
660         # possibly override the default user_id & group_id values
661         if 'USER_ID' in os.environ:
662             config.user_id = int(os.environ['USER_ID'])
663         if 'GROUP_ID' in os.environ:
664             config.group_id = int(os.environ['GROUP_ID'])
665
666         # create factory for platform specific classes
667         try:
668             factory_module = importlib.import_module(config['factory_module'])
669             factory = getattr(factory_module, config['factory_class'])()
670         except AttributeError:
671             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
672                             .format(m=config['factory_module'],
673                                     c=config['factory_class'])) from AttributeError
674         # create config plugin for this platform
675         config_plugin = factory.get_config_plugin_class()(config)
676         config = config_plugin.get_config()
677
678         opts, unknown_opts = _parse_opts_from_cli()
679         log.set_level(debug=opts.debug)
680
681         if opts.version:
682             print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
683             sys.exit(0)
684
685         if opts.summary:
686             with open(opts.summary) as json_data:
687                 result = json.load(json_data)
688                 if opts.user_label:
689                     result['config']['user_label'] = opts.user_label
690                 print((NFVBenchSummarizer(result, fluent_logger)))
691             sys.exit(0)
692
693         # show default config in text/yaml format
694         if opts.show_default_config:
695             print((default_cfg.decode("utf-8")))
696             sys.exit(0)
697
698         # dump the contents of the trex log file
699         if opts.show_trex_log:
700             try:
701                 print(open('/tmp/trex.log').read(), end="")
702             except FileNotFoundError:
703                 print("No TRex log file found!")
704             sys.exit(0)
705
706         config.name = ''
707         if opts.config:
708             # do not check extra_specs in flavor as it can contain any key/value pairs
709             # the same principle applies also to the optional user_info open property
710             whitelist_keys = ['extra_specs', 'user_info']
711             # override default config options with start config at path parsed from CLI
712             # check if it is an inline yaml/json config or a file name
713             if os.path.isfile(opts.config):
714                 LOG.info('Loading configuration file: %s', opts.config)
715                 config = config_load(opts.config, config, whitelist_keys)
716                 config.name = os.path.basename(opts.config)
717             else:
718                 LOG.info('Loading configuration string: %s', opts.config)
719                 config = config_loads(opts.config, config, whitelist_keys)
720
721         # setup the fluent logger as soon as possible right after the config plugin is called,
722         # if there is any logging or result tag is set then initialize the fluent logger
723         for fluentd in config.fluentd:
724             if fluentd.logging_tag or fluentd.result_tag:
725                 fluent_logger = FluentLogHandler(config.fluentd)
726                 LOG.addHandler(fluent_logger)
727                 break
728
729         # convert 'user_info' opt from json string to dictionnary
730         # and merge the result with the current config dictionnary
731         if opts.user_info:
732             opts.user_info = json.loads(opts.user_info)
733             if config.user_info:
734                 config.user_info = config.user_info + opts.user_info
735             else:
736                 config.user_info = opts.user_info
737             # hide the option to further _update_config()
738             opts.user_info = None
739
740         # traffic profile override options
741         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
742
743         # copy over cli options that are used in config
744         config.generator_profile = opts.generator_profile
745         if opts.sriov:
746             config.sriov = True
747         if opts.log_file:
748             config.log_file = opts.log_file
749         if opts.service_chain:
750             config.service_chain = opts.service_chain
751         if opts.service_chain_count:
752             config.service_chain_count = opts.service_chain_count
753         if opts.no_vswitch_access:
754             config.no_vswitch_access = opts.no_vswitch_access
755         if opts.hypervisor:
756             # can be any of 'comp1', 'nova:', 'nova:comp1'
757             config.compute_nodes = opts.hypervisor
758         if opts.vxlan:
759             config.vxlan = True
760         if opts.mpls:
761             config.mpls = True
762         if opts.restart:
763             config.restart = True
764         if opts.service_mode:
765             config.service_mode = True
766         if opts.no_flow_stats:
767             config.no_flow_stats = True
768         if opts.no_latency_stats:
769             config.no_latency_stats = True
770         if opts.no_latency_streams:
771             config.no_latency_streams = True
772         # port to port loopback (direct or through switch)
773         if opts.l2_loopback:
774             config.l2_loopback = True
775             if config.service_chain != ChainType.EXT:
776                 LOG.info('Changing service chain type to EXT')
777                 config.service_chain = ChainType.EXT
778             if not config.no_arp:
779                 LOG.info('Disabling ARP')
780                 config.no_arp = True
781             config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
782             LOG.info('Running L2 loopback: using EXT chain/no ARP')
783
784         if opts.use_sriov_middle_net:
785             if (not config.sriov) or (config.service_chain != ChainType.PVVP):
786                 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
787             config.use_sriov_middle_net = True
788
789         if config.sriov and config.service_chain != ChainType.EXT:
790             # if sriov is requested (does not apply to ext chains)
791             # make sure the physnet names are specified
792             check_physnet("left", config.internal_networks.left)
793             check_physnet("right", config.internal_networks.right)
794             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
795                 check_physnet("middle", config.internal_networks.middle)
796
797         # show running config in json format
798         if opts.show_config:
799             print((json.dumps(config, sort_keys=True, indent=4)))
800             sys.exit(0)
801
802         # update the config in the config plugin as it might have changed
803         # in a copy of the dict (config plugin still holds the original dict)
804         config_plugin.set_config(config)
805
806         if opts.status or opts.cleanup or opts.force_cleanup:
807             status_cleanup(config, opts.cleanup, opts.force_cleanup)
808
809         # add file log if requested
810         if config.log_file:
811             log.add_file_logger(config.log_file)
812             # possibly change file ownership
813             uid = config.user_id
814             gid = config.group_id
815             if gid is None:
816                 gid = uid
817             if uid is not None:
818                 os.chown(config.log_file, uid, gid)
819
820         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
821             else None
822
823         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
824
825         if opts.server:
826             server = WebServer(nfvbench_instance, fluent_logger)
827             try:
828                 port = int(opts.port)
829             except ValueError:
830                 server.run(host=opts.host)
831             else:
832                 server.run(host=opts.host, port=port)
833             # server.run() should never return
834         else:
835             with utils.RunLock():
836                 run_summary_required = True
837                 if unknown_opts:
838                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
839                     LOG.error(err_msg)
840                     raise Exception(err_msg)
841
842                 # remove unfilled values
843                 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
844                 # get CLI args
845                 params = ' '.join(str(e) for e in sys.argv[1:])
846                 result = nfvbench_instance.run(opts, params)
847                 if 'error_message' in result:
848                     raise Exception(result['error_message'])
849
850                 if 'result' in result and result['status']:
851                     nfvbench_instance.save(result['result'])
852                     nfvbench_instance.prepare_summary(result['result'])
853     except Exception as exc:
854         run_summary_required = True
855         LOG.error({
856             'status': NFVBench.STATUS_ERROR,
857             'error_message': traceback.format_exc()
858         })
859         print((str(exc)))
860     finally:
861         if fluent_logger:
862             # only send a summary record if there was an actual nfvbench run or
863             # if an error/exception was logged.
864             fluent_logger.send_run_summary(run_summary_required)
865
866
867 if __name__ == '__main__':
868     main()