NFVBENCH-203: Make --show-config really print the actual running config
[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('-0', '--no-traffic', dest='no_traffic',
409                         default=None,
410                         action='store_true',
411                         help='Check config and connectivity only - do not generate traffic')
412
413     parser.add_argument('--no-arp', dest='no_arp',
414                         default=None,
415                         action='store_true',
416                         help='Do not use ARP to find MAC addresses, '
417                              'instead use values in config file')
418
419     parser.add_argument('--loop-vm-arp', dest='loop_vm_arp',
420                         default=None,
421                         action='store_true',
422                         help='Use ARP to find MAC addresses '
423                              'instead of using values from TRex ports (VPP forwarder only)')
424
425     parser.add_argument('--no-vswitch-access', dest='no_vswitch_access',
426                         default=None,
427                         action='store_true',
428                         help='Skip vswitch configuration and retrieving of stats')
429
430     parser.add_argument('--vxlan', dest='vxlan',
431                         default=None,
432                         action='store_true',
433                         help='Enable VxLan encapsulation')
434
435     parser.add_argument('--mpls', dest='mpls',
436                         default=None,
437                         action='store_true',
438                         help='Enable MPLS encapsulation')
439
440     parser.add_argument('--no-cleanup', dest='no_cleanup',
441                         default=None,
442                         action='store_true',
443                         help='no cleanup after run')
444
445     parser.add_argument('--cleanup', dest='cleanup',
446                         default=None,
447                         action='store_true',
448                         help='Cleanup NFVbench resources (prompt to confirm)')
449
450     parser.add_argument('--force-cleanup', dest='force_cleanup',
451                         default=None,
452                         action='store_true',
453                         help='Cleanup NFVbench resources (do not prompt)')
454
455     parser.add_argument('--restart', dest='restart',
456                         default=None,
457                         action='store_true',
458                         help='Restart TRex server')
459
460     parser.add_argument('--json', dest='json',
461                         action='store',
462                         help='store results in json format file',
463                         metavar='<path>/<filename>')
464
465     parser.add_argument('--std-json', dest='std_json',
466                         action='store',
467                         help='store results in json format file with nfvbench standard filename: '
468                              '<service-chain-type>-<service-chain-count>-<flow-count>'
469                              '-<packet-sizes>.json',
470                         metavar='<path>')
471
472     parser.add_argument('--show-default-config', dest='show_default_config',
473                         default=None,
474                         action='store_true',
475                         help='print the default config in yaml format (unedited)')
476
477     parser.add_argument('--show-pre-config', dest='show_pre_config',
478                         default=None,
479                         action='store_true',
480                         help='print the config in json format (cfg file applied)')
481
482     parser.add_argument('--show-config', dest='show_config',
483                         default=None,
484                         action='store_true',
485                         help='print the running config in json format (final)')
486
487     parser.add_argument('-ss', '--show-summary', dest='summary',
488                         action='store',
489                         help='Show summary from nfvbench json file',
490                         metavar='<json>')
491
492     parser.add_argument('-v', '--version', dest='version',
493                         default=None,
494                         action='store_true',
495                         help='Show version')
496
497     parser.add_argument('-fs', '--frame-size', dest='frame_sizes',
498                         action='append',
499                         help='Override traffic profile frame sizes',
500                         metavar='<frame_size_bytes or IMIX>')
501
502     parser.add_argument('--unidir', dest='unidir',
503                         action='store_true',
504                         default=None,
505                         help='Override traffic profile direction (requires -fs)')
506
507     parser.add_argument('--log-file', '--logfile', dest='log_file',
508                         action='store',
509                         help='Filename for saving logs',
510                         metavar='<log_file>')
511
512     parser.add_argument('--user-label', '--userlabel', dest='user_label',
513                         action='store',
514                         help='Custom label for performance records')
515
516     parser.add_argument('--hypervisor', dest='hypervisor',
517                         action='store',
518                         metavar='<hypervisor name>',
519                         help='Where chains must run ("compute", "az:", "az:compute")')
520
521     parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
522                         action='store',
523                         metavar='<vlan(s)|no-tag|true|false>',
524                         help='Port to port or port to switch to port L2 loopback '
525                              'tagged with given VLAN id(s) or not (given \'no-tag\') '
526                              '\'true\': use current vlans; \'false\': disable this mode.')
527
528     parser.add_argument('--user-info', dest='user_info',
529                         action='append',
530                         metavar='<data>',
531                         help='Custom data to be included as is '
532                              'in the json report config branch - '
533                              ' example, pay attention! no space: '
534                              '--user-info=\'{"status":"explore","description":'
535                              '{"target":"lab","ok":true,"version":2020}}\' - '
536                              'this option may be repeated; given data will be merged.')
537
538     parser.add_argument('--vlan-tagging', dest='vlan_tagging',
539                         type=bool_arg,
540                         metavar='<boolean>',
541                         action='store',
542                         default=None,
543                         help='Override the NFVbench \'vlan_tagging\' parameter')
544
545     parser.add_argument('--intf-speed', dest='intf_speed',
546                         metavar='<speed>',
547                         action='store',
548                         default=None,
549                         help='Override the NFVbench \'intf_speed\' '
550                              'parameter (e.g. 10Gbps, auto, 16.72Gbps)')
551
552     parser.add_argument('--cores', dest='cores',
553                         type=int_arg,
554                         metavar='<number>',
555                         action='store',
556                         default=None,
557                         help='Override the T-Rex \'cores\' parameter')
558
559     parser.add_argument('--cache-size', dest='cache_size',
560                         type=int_arg,
561                         metavar='<size>',
562                         action='store',
563                         default=None,
564                         help='Specify the FE cache size (default: 0, flow-count if < 0)')
565
566     parser.add_argument('--service-mode', dest='service_mode',
567                         action='store_true',
568                         default=None,
569                         help='Enable T-Rex service mode (for debugging purpose)')
570
571     parser.add_argument('--no-e2e-check', dest='no_e2e_check',
572                         action='store_true',
573                         default=None,
574                         help='Skip "end to end" connectivity check (on test purpose)')
575
576     parser.add_argument('--no-flow-stats', dest='no_flow_stats',
577                         action='store_true',
578                         default=None,
579                         help='Disable additional flow stats (on high load traffic)')
580
581     parser.add_argument('--no-latency-stats', dest='no_latency_stats',
582                         action='store_true',
583                         default=None,
584                         help='Disable flow stats for latency traffic')
585
586     parser.add_argument('--no-latency-streams', dest='no_latency_streams',
587                         action='store_true',
588                         default=None,
589                         help='Disable latency measurements (no streams)')
590
591     parser.add_argument('--user-id', dest='user_id',
592                         type=int_arg,
593                         metavar='<uid>',
594                         action='store',
595                         default=None,
596                         help='Change json/log files ownership with this user (int)')
597
598     parser.add_argument('--group-id', dest='group_id',
599                         type=int_arg,
600                         metavar='<gid>',
601                         action='store',
602                         default=None,
603                         help='Change json/log files ownership with this group (int)')
604
605     parser.add_argument('--show-trex-log', dest='show_trex_log',
606                         default=None,
607                         action='store_true',
608                         help='Show the current TRex local server log file contents'
609                              ' => diagnostic/help in case of configuration problems')
610
611     parser.add_argument('--debug-mask', dest='debug_mask',
612                         type=int_arg,
613                         metavar='<mask>',
614                         action='store',
615                         default=None,
616                         help='General purpose register (debugging flags), '
617                              'the hexadecimal notation (0x...) is accepted.'
618                              'Designed for development needs (default: 0).')
619
620     opts, unknown_opts = parser.parse_known_args()
621     return opts, unknown_opts
622
623
624 def load_default_config():
625     default_cfg = resource_string(__name__, "cfg.default.yaml")
626     config = config_loads(default_cfg)
627     config.name = '(built-in default config)'
628     return config, default_cfg
629
630
631 def override_custom_traffic(config, frame_sizes, unidir):
632     """Override the traffic profiles with a custom one."""
633     if frame_sizes is not None:
634         traffic_profile_name = "custom_traffic_profile"
635         config.traffic_profile = [
636             {
637                 "l2frame_size": frame_sizes,
638                 "name": traffic_profile_name
639             }
640         ]
641     else:
642         traffic_profile_name = config.traffic["profile"]
643
644     bidirectional = config.traffic['bidirectional'] if unidir is None else not unidir
645     config.traffic = {
646         "bidirectional": bidirectional,
647         "profile": traffic_profile_name
648     }
649
650
651 def check_physnet(name, netattrs):
652     if not netattrs.physical_network:
653         raise Exception("SRIOV requires physical_network to be specified for the {n} network"
654                         .format(n=name))
655     if not netattrs.segmentation_id:
656         raise Exception("SRIOV requires segmentation_id to be specified for the {n} network"
657                         .format(n=name))
658
659 def status_cleanup(config, cleanup, force_cleanup):
660     LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs())
661     # check if another run is pending
662     ret_code = 0
663     try:
664         with utils.RunLock():
665             LOG.info('Status: idle')
666     except Exception:
667         LOG.info('Status: busy (run pending)')
668         ret_code = 1
669     # check nfvbench resources
670     if config.openrc_file and config.service_chain != ChainType.EXT:
671         cleaner = Cleaner(config)
672         count = cleaner.show_resources()
673         if count and (cleanup or force_cleanup):
674             cleaner.clean(not force_cleanup)
675     sys.exit(ret_code)
676
677 def main():
678     global fluent_logger
679     run_summary_required = False
680     try:
681         log.setup()
682         # load default config file
683         config, default_cfg = load_default_config()
684         # possibly override the default user_id & group_id values
685         if 'USER_ID' in os.environ:
686             config.user_id = int(os.environ['USER_ID'])
687         if 'GROUP_ID' in os.environ:
688             config.group_id = int(os.environ['GROUP_ID'])
689
690         # create factory for platform specific classes
691         try:
692             factory_module = importlib.import_module(config['factory_module'])
693             factory = getattr(factory_module, config['factory_class'])()
694         except AttributeError:
695             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
696                             .format(m=config['factory_module'],
697                                     c=config['factory_class'])) from AttributeError
698         # create config plugin for this platform
699         config_plugin = factory.get_config_plugin_class()(config)
700         config = config_plugin.get_config()
701
702         opts, unknown_opts = _parse_opts_from_cli()
703         log.set_level(debug=opts.debug)
704
705         if opts.version:
706             print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
707             sys.exit(0)
708
709         if opts.summary:
710             with open(opts.summary) as json_data:
711                 result = json.load(json_data)
712                 if opts.user_label:
713                     result['config']['user_label'] = opts.user_label
714                 print((NFVBenchSummarizer(result, fluent_logger)))
715             sys.exit(0)
716
717         # show default config in text/yaml format
718         if opts.show_default_config:
719             print((default_cfg.decode("utf-8")))
720             sys.exit(0)
721
722         # dump the contents of the trex log file
723         if opts.show_trex_log:
724             try:
725                 print(open('/tmp/trex.log').read(), end="")
726             except FileNotFoundError:
727                 print("No TRex log file found!")
728             sys.exit(0)
729
730         # mask info logging in case of further config dump
731         if opts.show_config or opts.show_pre_config:
732             LOG.setLevel(log.logging.WARNING)
733
734         config.name = ''
735         if opts.config:
736             # do not check extra_specs in flavor as it can contain any key/value pairs
737             # the same principle applies also to the optional user_info open property
738             whitelist_keys = ['extra_specs', 'user_info']
739             # override default config options with start config at path parsed from CLI
740             # check if it is an inline yaml/json config or a file name
741             if os.path.isfile(opts.config):
742                 LOG.info('Loading configuration file: %s', opts.config)
743                 config = config_load(opts.config, config, whitelist_keys)
744                 config.name = os.path.basename(opts.config)
745             else:
746                 LOG.info('Loading configuration string: %s', opts.config)
747                 config = config_loads(opts.config, config, whitelist_keys)
748
749         # show current config in json format (before CLI overriding)
750         if opts.show_pre_config:
751             print((json.dumps(config, sort_keys=True, indent=4)))
752             sys.exit(0)
753
754         # setup the fluent logger as soon as possible right after the config plugin is called,
755         # if there is any logging or result tag is set then initialize the fluent logger
756         for fluentd in config.fluentd:
757             if fluentd.logging_tag or fluentd.result_tag:
758                 fluent_logger = FluentLogHandler(config.fluentd)
759                 LOG.addHandler(fluent_logger)
760                 break
761
762         # traffic profile override options
763         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
764
765         # Copy over some of the cli options that are used in config.
766         # This explicit copy is sometimes necessary
767         # because some early evaluation depends on them
768         # and cannot wait for _update_config() coming further.
769         # It is good practice then to set them to None (<=> done)
770         # and even required if a specific conversion is performed here
771         # that would be corrupted by a default update (simple copy).
772         # On the other hand, some excessive assignments have been removed
773         # from here, since the _update_config() procedure does them well.
774
775         config.generator_profile = opts.generator_profile
776         if opts.sriov is not None:
777             config.sriov = True
778             opts.sriov = None
779         if opts.log_file is not None:
780             config.log_file = opts.log_file
781             opts.log_file = None
782         if opts.user_id is not None:
783             config.user_id = opts.user_id
784             opts.user_id = None
785         if opts.group_id is not None:
786             config.group_id = opts.group_id
787             opts.group_id = None
788         if opts.service_chain is not None:
789             config.service_chain = opts.service_chain
790             opts.service_chain = None
791         if opts.hypervisor is not None:
792             # can be any of 'comp1', 'nova:', 'nova:comp1'
793             config.compute_nodes = opts.hypervisor
794             opts.hypervisor = None
795         if opts.debug_mask is not None:
796             config.debug_mask = opts.debug_mask
797             opts.debug_mask = None
798
799         # convert 'user_info' opt from json string to dictionnary
800         # and merge the result with the current config dictionnary
801         if opts.user_info is not None:
802             for user_info_json in opts.user_info:
803                 user_info_dict = json.loads(user_info_json)
804                 if config.user_info:
805                     config.user_info = config.user_info + user_info_dict
806                 else:
807                     config.user_info = user_info_dict
808             opts.user_info = None
809
810         # port to port loopback (direct or through switch)
811         # we accept the following syntaxes for the CLI argument
812         #   'false'   : mode not enabled
813         #   'true'    : mode enabled with currently defined vlan IDs
814         #   'no-tag'  : mode enabled with no vlan tagging
815         #   <vlan IDs>: mode enabled using the given (pair of) vlan ID lists
816         #     - If present, a '_' char will separate left an right ports lists
817         #         e.g. 'a_x'         => vlans: [[a],[x]]
818         #              'a,b,c_x,y,z' =>        [[a,b,c],[x,y,z]]
819         #     - Otherwise the given vlan ID list applies to both sides
820         #         e.g. 'a'           => vlans: [[a],[a]]
821         #              'a,b'         =>        [[a,b],[a,b]]
822         #     - Vlan lists size needs to be at least the actual SCC value
823         #     - Unless overriden in CLI opts, config.service_chain_count
824         #       is adjusted to the size of the VLAN ID lists given here.
825
826         if opts.l2_loopback is not None:
827             arg_pair = opts.l2_loopback.lower().split('_')
828             if arg_pair[0] == 'false':
829                 config.l2_loopback = False
830             else:
831                 config.l2_loopback = True
832                 if config.service_chain != ChainType.EXT:
833                     LOG.info('Changing service chain type to EXT')
834                     config.service_chain = ChainType.EXT
835                 if not config.no_arp:
836                     LOG.info('Disabling ARP')
837                     config.no_arp = True
838                 if arg_pair[0] == 'true':
839                     pass
840                 else:
841                     # here explicit (not)tagging is not CLI overridable
842                     opts.vlan_tagging = None
843                     if arg_pair[0] == 'no-tag':
844                         config.vlan_tagging = False
845                     else:
846                         config.vlan_tagging = True
847                         if len(arg_pair) == 1 or not arg_pair[1]:
848                             arg_pair = [arg_pair[0], arg_pair[0]]
849                         vlans = [[], []]
850
851                         def append_vlan(port, vlan_id):
852                             # a vlan tag value must be in [0..4095]
853                             if vlan_id not in range(0, 4096):
854                                 raise ValueError
855                             vlans[port].append(vlan_id)
856                         try:
857                             for port in [0, 1]:
858                                 vlan_ids = arg_pair[port].split(',')
859                                 for vlan_id in vlan_ids:
860                                     append_vlan(port, int(vlan_id))
861                             if len(vlans[0]) != len(vlans[1]):
862                                 raise ValueError
863                         except ValueError:
864                             # at least one invalid tag => no tagging
865                             config.vlan_tagging = False
866                         if config.vlan_tagging:
867                             config.vlans = vlans
868                             # force service chain count if not CLI overriden
869                             if opts.service_chain_count is None:
870                                 config.service_chain_count = len(vlans[0])
871             opts.l2_loopback = None
872
873         if config.use_sriov_middle_net is None:
874             config.use_sriov_middle_net = False
875         if opts.use_sriov_middle_net is not None:
876             config.use_sriov_middle_net = opts.use_sriov_middle_net
877             opts.use_sriov_middle_net = None
878         if (config.use_sriov_middle_net and (
879                 (not config.sriov) or (config.service_chain != ChainType.PVVP))):
880             raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
881
882         if config.sriov and config.service_chain != ChainType.EXT:
883             # if sriov is requested (does not apply to ext chains)
884             # make sure the physnet names are specified
885             check_physnet("left", config.internal_networks.left)
886             check_physnet("right", config.internal_networks.right)
887             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
888                 check_physnet("middle", config.internal_networks.middle)
889
890         # update the config in the config plugin as it might have changed
891         # in a copy of the dict (config plugin still holds the original dict)
892         config_plugin.set_config(config)
893
894         if opts.status or opts.cleanup or opts.force_cleanup:
895             status_cleanup(config, opts.cleanup, opts.force_cleanup)
896
897         # add file log if requested
898         if config.log_file:
899             log.add_file_logger(config.log_file)
900             # possibly change file ownership
901             uid = config.user_id
902             gid = config.group_id
903             if gid is None:
904                 gid = uid
905             if uid is not None:
906                 os.chown(config.log_file, uid, gid)
907
908         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
909             else None
910
911         nfvbench_instance = NFVBench(config, openstack_spec, config_plugin, factory)
912
913         if opts.server:
914             server = WebServer(nfvbench_instance, fluent_logger)
915             try:
916                 port = int(opts.port)
917             except ValueError:
918                 server.run(host=opts.host)
919             else:
920                 server.run(host=opts.host, port=port)
921             # server.run() should never return
922         else:
923             dry_run = opts.show_config
924             with utils.RunLock():
925                 run_summary_required = True
926                 if unknown_opts:
927                     err_msg = 'Unknown options: ' + ' '.join(unknown_opts)
928                     LOG.error(err_msg)
929                     raise Exception(err_msg)
930
931                 # remove unfilled values
932                 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
933                 # get CLI args
934                 params = ' '.join(str(e) for e in sys.argv[1:])
935                 result = nfvbench_instance.run(opts, params, dry_run=dry_run)
936                 if 'error_message' in result:
937                     raise Exception(result['error_message'])
938
939                 if 'result' in result and result['status']:
940                     nfvbench_instance.save(result['result'])
941                     nfvbench_instance.prepare_summary(result['result'])
942     except Exception as exc:
943         run_summary_required = True
944         LOG.error({
945             'status': NFVBench.STATUS_ERROR,
946             'error_message': traceback.format_exc()
947         })
948         print((str(exc)))
949     finally:
950         if fluent_logger:
951             # only send a summary record if there was an actual nfvbench run or
952             # if an error/exception was logged.
953             fluent_logger.send_run_summary(run_summary_required)
954
955
956 if __name__ == '__main__':
957     main()