NFVBENCH-215 Fix wrong throughput ratio in latency tests
[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                 with open('/tmp/trex.log') as trex_log_file:
740                     print(trex_log_file.read(), end="")
741             except FileNotFoundError:
742                 print("No TRex log file found!")
743             sys.exit(0)
744
745         # mask info logging in case of further config dump
746         if opts.show_config or opts.show_pre_config:
747             LOG.setLevel(log.logging.WARNING)
748
749         config.name = ''
750         if opts.config:
751             # do not check extra_specs in flavor as it can contain any key/value pairs
752             # the same principle applies also to the optional user_info open property
753             whitelist_keys = ['extra_specs', 'user_info']
754             # override default config options with start config at path parsed from CLI
755             # check if it is an inline yaml/json config or a file name
756             if os.path.isfile(opts.config):
757                 LOG.info('Loading configuration file: %s', opts.config)
758                 config = config_load(opts.config, config, whitelist_keys)
759                 config.name = os.path.basename(opts.config)
760             else:
761                 LOG.info('Loading configuration string: %s', opts.config)
762                 config = config_loads(opts.config, config, whitelist_keys)
763
764         # show current config in json format (before CLI overriding)
765         if opts.show_pre_config:
766             print((json.dumps(config, sort_keys=True, indent=4)))
767             sys.exit(0)
768
769         # setup the fluent logger as soon as possible right after the config plugin is called,
770         # if there is any logging or result tag is set then initialize the fluent logger
771         for fluentd in config.fluentd:
772             if fluentd.logging_tag or fluentd.result_tag:
773                 fluent_logger = FluentLogHandler(config.fluentd)
774                 LOG.addHandler(fluent_logger)
775                 break
776
777         # traffic profile override options
778         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
779
780         # Copy over some of the cli options that are used in config.
781         # This explicit copy is sometimes necessary
782         # because some early evaluation depends on them
783         # and cannot wait for _update_config() coming further.
784         # It is good practice then to set them to None (<=> done)
785         # and even required if a specific conversion is performed here
786         # that would be corrupted by a default update (simple copy).
787         # On the other hand, some excessive assignments have been removed
788         # from here, since the _update_config() procedure does them well.
789
790         config.generator_profile = opts.generator_profile
791         if opts.sriov is not None:
792             config.sriov = True
793             opts.sriov = None
794         if opts.log_file is not None:
795             config.log_file = opts.log_file
796             opts.log_file = None
797         if opts.user_id is not None:
798             config.user_id = opts.user_id
799             opts.user_id = None
800         if opts.group_id is not None:
801             config.group_id = opts.group_id
802             opts.group_id = None
803         if opts.service_chain is not None:
804             config.service_chain = opts.service_chain
805             opts.service_chain = None
806         if opts.hypervisor is not None:
807             # can be any of 'comp1', 'nova:', 'nova:comp1'
808             config.compute_nodes = opts.hypervisor
809             opts.hypervisor = None
810         if opts.debug_mask is not None:
811             config.debug_mask = opts.debug_mask
812             opts.debug_mask = None
813
814         # convert 'user_info' opt from json string to dictionnary
815         # and merge the result with the current config dictionnary
816         if opts.user_info is not None:
817             for user_info_json in opts.user_info:
818                 user_info_dict = json.loads(user_info_json)
819                 if config.user_info:
820                     config.user_info = config.user_info + user_info_dict
821                 else:
822                     config.user_info = user_info_dict
823             opts.user_info = None
824
825         # port to port loopback (direct or through switch)
826         # we accept the following syntaxes for the CLI argument
827         #   'false'   : mode not enabled
828         #   'true'    : mode enabled with currently defined vlan IDs
829         #   'no-tag'  : mode enabled with no vlan tagging
830         #   <vlan IDs>: mode enabled using the given (pair of) vlan ID lists
831         #     - If present, a '_' char will separate left an right ports lists
832         #         e.g. 'a_x'         => vlans: [[a],[x]]
833         #              'a,b,c_x,y,z' =>        [[a,b,c],[x,y,z]]
834         #     - Otherwise the given vlan ID list applies to both sides
835         #         e.g. 'a'           => vlans: [[a],[a]]
836         #              'a,b'         =>        [[a,b],[a,b]]
837         #     - Vlan lists size needs to be at least the actual SCC value
838         #     - Unless overriden in CLI opts, config.service_chain_count
839         #       is adjusted to the size of the VLAN ID lists given here.
840
841         if opts.l2_loopback is not None:
842             arg_pair = opts.l2_loopback.lower().split('_')
843             if arg_pair[0] == 'false':
844                 config.l2_loopback = False
845             else:
846                 config.l2_loopback = True
847                 if config.service_chain != ChainType.EXT:
848                     LOG.info('Changing service chain type to EXT')
849                     config.service_chain = ChainType.EXT
850                 if not config.no_arp:
851                     LOG.info('Disabling ARP')
852                     config.no_arp = True
853                 if arg_pair[0] == 'true':
854                     pass
855                 else:
856                     # here explicit (not)tagging is not CLI overridable
857                     opts.vlan_tagging = None
858                     if arg_pair[0] == 'no-tag':
859                         config.vlan_tagging = False
860                     else:
861                         config.vlan_tagging = True
862                         if len(arg_pair) == 1 or not arg_pair[1]:
863                             arg_pair = [arg_pair[0], arg_pair[0]]
864                         vlans = [[], []]
865
866                         def append_vlan(port, vlan_id):
867                             # a vlan tag value must be in [0..4095]
868                             if vlan_id not in range(0, 4096):
869                                 raise ValueError
870                             vlans[port].append(vlan_id)
871                         try:
872                             for port in [0, 1]:
873                                 vlan_ids = arg_pair[port].split(',')
874                                 for vlan_id in vlan_ids:
875                                     append_vlan(port, int(vlan_id))
876                             if len(vlans[0]) != len(vlans[1]):
877                                 raise ValueError
878                         except ValueError:
879                             # at least one invalid tag => no tagging
880                             config.vlan_tagging = False
881                         if config.vlan_tagging:
882                             config.vlans = vlans
883                             # force service chain count if not CLI overriden
884                             if opts.service_chain_count is None:
885                                 config.service_chain_count = len(vlans[0])
886             opts.l2_loopback = None
887
888         if config.i40e_mixed is None:
889             config.i40e_mixed = 'ignore'
890         if config.use_sriov_middle_net is None:
891             config.use_sriov_middle_net = False
892         if opts.use_sriov_middle_net is not None:
893             config.use_sriov_middle_net = opts.use_sriov_middle_net
894             opts.use_sriov_middle_net = None
895         if (config.use_sriov_middle_net and (
896                 (not config.sriov) or (config.service_chain != ChainType.PVVP))):
897             raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
898
899         if config.sriov and config.service_chain != ChainType.EXT:
900             # if sriov is requested (does not apply to ext chains)
901             # make sure the physnet names are specified
902             check_physnet("left", config.internal_networks.left)
903             check_physnet("right", config.internal_networks.right)
904             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
905                 check_physnet("middle", config.internal_networks.middle)
906
907         # update the config in the config plugin as it might have changed
908         # in a copy of the dict (config plugin still holds the original dict)
909         config_plugin.set_config(config)
910
911         if opts.status or opts.cleanup or opts.force_cleanup:
912             status_cleanup(config, opts.cleanup, opts.force_cleanup)
913
914         # add file log if requested
915         if config.log_file:
916             log.add_file_logger(config.log_file)
917             # possibly change file ownership
918             uid = config.user_id
919             gid = config.group_id
920             if gid is None:
921                 gid = uid
922             if uid is not None:
923                 os.chown(config.log_file, uid, gid)
924
925         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
926             else None
927
928         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
929
930         if opts.server:
931             server = WebServer(nfvbench_instance, fluent_logger)
932             try:
933                 port = int(opts.port)
934             except ValueError:
935                 server.run(host=opts.host)
936             else:
937                 server.run(host=opts.host, port=port)
938             # server.run() should never return
939         else:
940             dry_run = opts.show_config
941             with utils.RunLock():
942                 run_summary_required = True
943                 if unknown_opts:
944                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
945                     LOG.error(err_msg)
946                     raise Exception(err_msg)
947
948                 # remove unfilled values
949                 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
950                 # get CLI args
951                 params = ' '.join(str(e) for e in sys.argv[1:])
952                 result = nfvbench_instance.run(opts, params, dry_run=dry_run)
953                 if 'error_message' in result:
954                     raise Exception(result['error_message'])
955
956                 if 'result' in result and result['status']:
957                     nfvbench_instance.save(result['result'])
958                     nfvbench_instance.prepare_summary(result['result'])
959     except Exception as exc:
960         run_summary_required = True
961         LOG.error({
962             'status': NFVBench.STATUS_ERROR,
963             'error_message': traceback.format_exc()
964         })
965         print((str(exc)))
966     finally:
967         if fluent_logger:
968             # only send a summary record if there was an actual nfvbench run or
969             # if an error/exception was logged.
970             fluent_logger.send_run_summary(run_summary_required)
971
972
973 if __name__ == '__main__':
974     main()