da069bf4a7274c4a5bc5f0c6debefc6347f08c90
[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('--debug-mask', dest='debug_mask',
580                         type=int_arg,
581                         metavar='<mask>',
582                         action='store',
583                         default='0x00000000',
584                         help='General purpose register (debugging flags), '
585                                 + 'the hexadecimal notation (0x...) is accepted.'
586                                 + 'Designed for development needs.')
587
588     opts, unknown_opts = parser.parse_known_args()
589     return opts, unknown_opts
590
591
592 def load_default_config():
593     default_cfg = resource_string(__name__, "cfg.default.yaml")
594     config = config_loads(default_cfg)
595     config.name = '(built-in default config)'
596     return config, default_cfg
597
598
599 def override_custom_traffic(config, frame_sizes, unidir):
600     """Override the traffic profiles with a custom one."""
601     if frame_sizes is not None:
602         traffic_profile_name = "custom_traffic_profile"
603         config.traffic_profile = [
604             {
605                 "l2frame_size": frame_sizes,
606                 "name": traffic_profile_name
607             }
608         ]
609     else:
610         traffic_profile_name = config.traffic["profile"]
611
612     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
613     config.traffic = {
614         "bidirectional": bidirectional,
615         "profile": traffic_profile_name
616     }
617
618
619 def check_physnet(name, netattrs):
620     if not netattrs.physical_network:
621         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
622                         .format(n=name))
623     if not netattrs.segmentation_id:
624         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
625                         .format(n=name))
626
627 def status_cleanup(config, cleanup, force_cleanup):
628     LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
629     # check if another run is pending
630     ret_code = 0
631     try:
632         with utils.RunLock():
633             LOG.info('Status: idle')
634     except Exception:
635         LOG.info('Status: busy (run pending)')
636         ret_code = 1
637     # check nfvbench resources
638     if config.openrc_file and config.service_chain != ChainType.EXT:
639         cleaner = Cleaner(config)
640         count = cleaner.show_resources()
641         if count and (cleanup or force_cleanup):
642             cleaner.clean(not force_cleanup)
643     sys.exit(ret_code)
644
645 def main():
646     global fluent_logger
647     run_summary_required = False
648     try:
649         log.setup()
650         # load default config file
651         config, default_cfg = load_default_config()
652         # possibly override the default user_id & group_id values
653         if 'USER_ID' in os.environ:
654             config.user_id = int(os.environ['USER_ID'])
655         if 'GROUP_ID' in os.environ:
656             config.group_id = int(os.environ['GROUP_ID'])
657
658         # create factory for platform specific classes
659         try:
660             factory_module = importlib.import_module(config['factory_module'])
661             factory = getattr(factory_module, config['factory_class'])()
662         except AttributeError:
663             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
664                             .format(m=config['factory_module'],
665                                     c=config['factory_class'])) from AttributeError
666         # create config plugin for this platform
667         config_plugin = factory.get_config_plugin_class()(config)
668         config = config_plugin.get_config()
669
670         opts, unknown_opts = _parse_opts_from_cli()
671         log.set_level(debug=opts.debug)
672
673         if opts.version:
674             print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
675             sys.exit(0)
676
677         if opts.summary:
678             with open(opts.summary) as json_data:
679                 result = json.load(json_data)
680                 if opts.user_label:
681                     result['config']['user_label'] = opts.user_label
682                 print((NFVBenchSummarizer(result, fluent_logger)))
683             sys.exit(0)
684
685         # show default config in text/yaml format
686         if opts.show_default_config:
687             print((default_cfg.decode("utf-8")))
688             sys.exit(0)
689
690         config.name = ''
691         if opts.config:
692             # do not check extra_specs in flavor as it can contain any key/value pairs
693             # the same principle applies also to the optional user_info open property
694             whitelist_keys = ['extra_specs', 'user_info']
695             # override default config options with start config at path parsed from CLI
696             # check if it is an inline yaml/json config or a file name
697             if os.path.isfile(opts.config):
698                 LOG.info('Loading configuration file: %s', opts.config)
699                 config = config_load(opts.config, config, whitelist_keys)
700                 config.name = os.path.basename(opts.config)
701             else:
702                 LOG.info('Loading configuration string: %s', opts.config)
703                 config = config_loads(opts.config, config, whitelist_keys)
704
705         # setup the fluent logger as soon as possible right after the config plugin is called,
706         # if there is any logging or result tag is set then initialize the fluent logger
707         for fluentd in config.fluentd:
708             if fluentd.logging_tag or fluentd.result_tag:
709                 fluent_logger = FluentLogHandler(config.fluentd)
710                 LOG.addHandler(fluent_logger)
711                 break
712
713         # convert 'user_info' opt from json string to dictionnary
714         # and merge the result with the current config dictionnary
715         if opts.user_info:
716             opts.user_info = json.loads(opts.user_info)
717             if config.user_info:
718                 config.user_info = config.user_info + opts.user_info
719             else:
720                 config.user_info = opts.user_info
721             # hide the option to further _update_config()
722             opts.user_info = None
723
724         # traffic profile override options
725         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
726
727         # copy over cli options that are used in config
728         config.generator_profile = opts.generator_profile
729         if opts.sriov:
730             config.sriov = True
731         if opts.log_file:
732             config.log_file = opts.log_file
733         if opts.service_chain:
734             config.service_chain = opts.service_chain
735         if opts.service_chain_count:
736             config.service_chain_count = opts.service_chain_count
737         if opts.no_vswitch_access:
738             config.no_vswitch_access = opts.no_vswitch_access
739         if opts.hypervisor:
740             # can be any of 'comp1', 'nova:', 'nova:comp1'
741             config.compute_nodes = opts.hypervisor
742         if opts.vxlan:
743             config.vxlan = True
744         if opts.mpls:
745             config.mpls = True
746         if opts.restart:
747             config.restart = True
748         if opts.service_mode:
749             config.service_mode = True
750         if opts.no_flow_stats:
751             config.no_flow_stats = True
752         if opts.no_latency_stats:
753             config.no_latency_stats = True
754         if opts.no_latency_streams:
755             config.no_latency_streams = True
756         # port to port loopback (direct or through switch)
757         if opts.l2_loopback:
758             config.l2_loopback = True
759             if config.service_chain != ChainType.EXT:
760                 LOG.info('Changing service chain type to EXT')
761                 config.service_chain = ChainType.EXT
762             if not config.no_arp:
763                 LOG.info('Disabling ARP')
764                 config.no_arp = True
765             config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
766             LOG.info('Running L2 loopback: using EXT chain/no ARP')
767
768         if opts.use_sriov_middle_net:
769             if (not config.sriov) or (config.service_chain != ChainType.PVVP):
770                 raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
771             config.use_sriov_middle_net = True
772
773         if config.sriov and config.service_chain != ChainType.EXT:
774             # if sriov is requested (does not apply to ext chains)
775             # make sure the physnet names are specified
776             check_physnet("left", config.internal_networks.left)
777             check_physnet("right", config.internal_networks.right)
778             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
779                 check_physnet("middle", config.internal_networks.middle)
780
781         # show running config in json format
782         if opts.show_config:
783             print((json.dumps(config, sort_keys=True, indent=4)))
784             sys.exit(0)
785
786         # update the config in the config plugin as it might have changed
787         # in a copy of the dict (config plugin still holds the original dict)
788         config_plugin.set_config(config)
789
790         if opts.status or opts.cleanup or opts.force_cleanup:
791             status_cleanup(config, opts.cleanup, opts.force_cleanup)
792
793         # add file log if requested
794         if config.log_file:
795             log.add_file_logger(config.log_file)
796             # possibly change file ownership
797             uid = config.user_id
798             gid = config.group_id
799             if gid is None:
800                 gid = uid
801             if uid is not None:
802                 os.chown(config.log_file, uid, gid)
803
804         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
805             else None
806
807         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
808
809         if opts.server:
810             server = WebServer(nfvbench_instance, fluent_logger)
811             try:
812                 port = int(opts.port)
813             except ValueError:
814                 server.run(host=opts.host)
815             else:
816                 server.run(host=opts.host, port=port)
817             # server.run() should never return
818         else:
819             with utils.RunLock():
820                 run_summary_required = True
821                 if unknown_opts:
822                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
823                     LOG.error(err_msg)
824                     raise Exception(err_msg)
825
826                 # remove unfilled values
827                 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
828                 # get CLI args
829                 params = ' '.join(str(e) for e in sys.argv[1:])
830                 result = nfvbench_instance.run(opts, params)
831                 if 'error_message' in result:
832                     raise Exception(result['error_message'])
833
834                 if 'result' in result and result['status']:
835                     nfvbench_instance.save(result['result'])
836                     nfvbench_instance.prepare_summary(result['result'])
837     except Exception as exc:
838         run_summary_required = True
839         LOG.error({
840             'status': NFVBench.STATUS_ERROR,
841             'error_message': traceback.format_exc()
842         })
843         print((str(exc)))
844     finally:
845         if fluent_logger:
846             # only send a summary record if there was an actual nfvbench run or
847             # if an error/exception was logged.
848             fluent_logger.send_run_summary(run_summary_required)
849
850
851 if __name__ == '__main__':
852     main()