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