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