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