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