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