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