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