NFVBENCH-215 Fix wrong throughput ratio in latency tests
[nfvbench.git] / nfvbench / nfvbench.py
index 06ca19d..740dca2 100644 (file)
@@ -60,8 +60,8 @@ class NFVBench(object):
         self.config_plugin = config_plugin
         self.factory = factory
         self.notifier = notifier
-        self.cred = credentials.Credentials(config.openrc_file, None, False) \
-            if config.openrc_file else None
+        self.cred = credentials.Credentials(config.openrc_file, config.clouds_detail, None, False) \
+            if config.openrc_file or config.clouds_detail else None
         self.chain_runner = None
         self.specs = Specs()
         self.specs.set_openstack_spec(openstack_spec)
@@ -71,11 +71,12 @@ class NFVBench(object):
     def set_notifier(self, notifier):
         self.notifier = notifier
 
-    def run(self, opts, args):
+    def run(self, opts, args, dry_run=False):
         """This run() method is called for every NFVbench benchmark request.
 
         In CLI mode, this method is called only once per invocation.
         In REST server mode, this is called once per REST POST request
+        On dry_run, show the running config in json format then exit
         """
         status = NFVBench.STATUS_OK
         result = None
@@ -88,12 +89,17 @@ class NFVBench(object):
         try:
             # recalc the running config based on the base config and options for this run
             self._update_config(opts)
-            if int(self.config.cache_size) < 0:
-                self.config.cache_size = self.config.flow_count
+
+            if dry_run:
+                print((json.dumps(self.config, sort_keys=True, indent=4)))
+                sys.exit(0)
+
             # check that an empty openrc file (no OpenStack) is only allowed
             # with EXT chain
-            if not self.config.openrc_file and self.config.service_chain != ChainType.EXT:
-                raise Exception("openrc_file in the configuration is required for PVP/PVVP chains")
+            if (not self.config.openrc_file and not self.config.clouds_detail) and \
+                    self.config.service_chain != ChainType.EXT:
+                raise Exception("openrc_file or clouds_detail in the configuration is required"
+                                " for PVP/PVVP chains")
 
             self.specs.set_run_spec(self.config_plugin.get_run_spec(self.config,
                                                                     self.specs.openstack))
@@ -166,7 +172,9 @@ class NFVBench(object):
                                self.config.service_chain,
                                self.config.service_chain_count,
                                self.config.flow_count,
-                               self.config.frame_sizes)
+                               self.config.frame_sizes,
+                               self.config.user_id,
+                               self.config.group_id)
 
     def _update_config(self, opts):
         """Recalculate the running config based on the base config and opts.
@@ -184,13 +192,14 @@ class NFVBench(object):
                         'will be created.', path)
                     os.makedirs(path)
                     LOG.info('%s is created.', path)
-                for h in log.getLogger().handlers:
-                    if isinstance(h, FileHandler) and h.baseFilename != opts['log_file']:
-                        # clean log file handler
-                        log.getLogger().removeHandler(h)
-                # add handler if not existing to avoid duplicates handlers
-                if len(log.getLogger().handlers) == 1:
+                if not any(isinstance(h, FileHandler) for h in log.getLogger().handlers):
                     log.add_file_logger(opts['log_file'])
+                else:
+                    for h in log.getLogger().handlers:
+                        if isinstance(h, FileHandler) and h.baseFilename != opts['log_file']:
+                            # clean log file handler
+                            log.getLogger().removeHandler(h)
+                            log.add_file_logger(opts['log_file'])
 
         self.config.update(opts)
         config = self.config
@@ -198,12 +207,23 @@ class NFVBench(object):
         config.service_chain = config.service_chain.upper()
         config.service_chain_count = int(config.service_chain_count)
         if config.l2_loopback:
-            # force the number of chains to be 1 in case of l2 loopback
-            config.service_chain_count = 1
+            # force the number of chains to be 1 in case of untagged l2 loopback
+            # (on the other hand, multiple L2 vlan tagged service chains are allowed)
+            if not config.vlan_tagging:
+                config.service_chain_count = 1
             config.service_chain = ChainType.EXT
             config.no_arp = True
             LOG.info('Running L2 loopback: using EXT chain/no ARP')
 
+        # allow oversized vlan lists, just clip them
+        try:
+            vlans = [list(v) for v in config.vlans]
+            for v in vlans:
+                del v[config.service_chain_count:]
+            config.vlans = vlans
+        except Exception:
+            pass
+
         # traffic profile override options
         if 'frame_sizes' in opts:
             unidir = False
@@ -223,6 +243,13 @@ class NFVBench(object):
         if config.flow_count % 2:
             config.flow_count += 1
 
+        # Possibly adjust the cache size
+        if config.cache_size < 0:
+            config.cache_size = config.flow_count
+
+        # The size must be capped to 10000 (where does this limit come from?)
+        config.cache_size = min(config.cache_size, 10000)
+
         config.duration_sec = float(config.duration_sec)
         config.interval_sec = float(config.interval_sec)
         config.pause_sec = float(config.pause_sec)
@@ -235,7 +262,6 @@ class NFVBench(object):
             if config.flavor.vcpus < 2:
                 raise Exception("Flavor vcpus must be >= 2")
 
-
         config.ndr_run = (not config.no_traffic and
                           'ndr' in config.rate.strip().lower().split('_'))
         config.pdr_run = (not config.no_traffic and
@@ -274,6 +300,23 @@ class NFVBench(object):
         self.config_plugin.validate_config(config, self.specs.openstack)
 
 
+def bool_arg(x):
+    """Argument type to be used in parser.add_argument()
+    When a boolean like value is expected to be given
+    """
+    return (str(x).lower() != 'false') \
+        and (str(x).lower() != 'no') \
+        and (str(x).lower() != '0')
+
+
+def int_arg(x):
+    """Argument type to be used in parser.add_argument()
+    When an integer type value is expected to be given
+    (returns 0 if argument is invalid, hexa accepted)
+    """
+    return int(x, 0)
+
+
 def _parse_opts_from_cli():
     parser = argparse.ArgumentParser()
 
@@ -363,6 +406,12 @@ def _parse_opts_from_cli():
                         action='store_true',
                         help='Use L3 neutron routers to handle traffic')
 
+    parser.add_argument('-garp', '--gratuitous-arp', dest='periodic_gratuitous_arp',
+                        default=None,
+                        action='store_true',
+                        help='Use gratuitous ARP to maintain session between TG '
+                             'and L3 routers to handle traffic')
+
     parser.add_argument('-0', '--no-traffic', dest='no_traffic',
                         default=None,
                         action='store_true',
@@ -432,10 +481,15 @@ def _parse_opts_from_cli():
                         action='store_true',
                         help='print the default config in yaml format (unedited)')
 
+    parser.add_argument('--show-pre-config', dest='show_pre_config',
+                        default=None,
+                        action='store_true',
+                        help='print the config in json format (cfg file applied)')
+
     parser.add_argument('--show-config', dest='show_config',
                         default=None,
                         action='store_true',
-                        help='print the running config in json format')
+                        help='print the running config in json format (final)')
 
     parser.add_argument('-ss', '--show-summary', dest='summary',
                         action='store',
@@ -473,34 +527,110 @@ def _parse_opts_from_cli():
 
     parser.add_argument('--l2-loopback', '--l2loopback', dest='l2_loopback',
                         action='store',
-                        metavar='<vlan>',
-                        help='Port to port or port to switch to port L2 loopback with VLAN id')
+                        metavar='<vlan(s)|no-tag|true|false>',
+                        help='Port to port or port to switch to port L2 loopback '
+                             'tagged with given VLAN id(s) or not (given \'no-tag\') '
+                             '\'true\': use current vlans; \'false\': disable this mode.')
+
+    parser.add_argument('--i40e-mixed', dest='i40e_mixed',
+                        action='store',
+                        default=None,
+                        metavar='<ignore,check,unbind>',
+                        help='TRex behavior when dealing with a i40e network card driver'
+                             ' [ https://trex-tgn.cisco.com/youtrack/issue/trex-528 ]')
+
+    parser.add_argument('--user-info', dest='user_info',
+                        action='append',
+                        metavar='<data>',
+                        help='Custom data to be included as is '
+                             'in the json report config branch - '
+                             ' example, pay attention! no space: '
+                             '--user-info=\'{"status":"explore","description":'
+                             '{"target":"lab","ok":true,"version":2020}}\' - '
+                             'this option may be repeated; given data will be merged.')
+
+    parser.add_argument('--vlan-tagging', dest='vlan_tagging',
+                        type=bool_arg,
+                        metavar='<boolean>',
+                        action='store',
+                        default=None,
+                        help='Override the NFVbench \'vlan_tagging\' parameter')
+
+    parser.add_argument('--intf-speed', dest='intf_speed',
+                        metavar='<speed>',
+                        action='store',
+                        default=None,
+                        help='Override the NFVbench \'intf_speed\' '
+                             'parameter (e.g. 10Gbps, auto, 16.72Gbps)')
+
+    parser.add_argument('--cores', dest='cores',
+                        type=int_arg,
+                        metavar='<number>',
+                        action='store',
+                        default=None,
+                        help='Override the T-Rex \'cores\' parameter')
 
     parser.add_argument('--cache-size', dest='cache_size',
+                        type=int_arg,
+                        metavar='<size>',
                         action='store',
-                        default='0',
+                        default=None,
                         help='Specify the FE cache size (default: 0, flow-count if < 0)')
 
     parser.add_argument('--service-mode', dest='service_mode',
                         action='store_true',
-                        default=False,
-                        help='Enable T-Rex service mode for debugging only')
+                        default=None,
+                        help='Enable T-Rex service mode (for debugging purpose)')
+
+    parser.add_argument('--no-e2e-check', dest='no_e2e_check',
+                        action='store_true',
+                        default=None,
+                        help='Skip "end to end" connectivity check (on test purpose)')
 
     parser.add_argument('--no-flow-stats', dest='no_flow_stats',
                         action='store_true',
-                        default=False,
-                        help='Disable extra flow stats (on high load traffic)')
+                        default=None,
+                        help='Disable additional flow stats (on high load traffic)')
 
     parser.add_argument('--no-latency-stats', dest='no_latency_stats',
                         action='store_true',
-                        default=False,
+                        default=None,
                         help='Disable flow stats for latency traffic')
 
     parser.add_argument('--no-latency-streams', dest='no_latency_streams',
                         action='store_true',
-                        default=False,
+                        default=None,
                         help='Disable latency measurements (no streams)')
 
+    parser.add_argument('--user-id', dest='user_id',
+                        type=int_arg,
+                        metavar='<uid>',
+                        action='store',
+                        default=None,
+                        help='Change json/log files ownership with this user (int)')
+
+    parser.add_argument('--group-id', dest='group_id',
+                        type=int_arg,
+                        metavar='<gid>',
+                        action='store',
+                        default=None,
+                        help='Change json/log files ownership with this group (int)')
+
+    parser.add_argument('--show-trex-log', dest='show_trex_log',
+                        default=None,
+                        action='store_true',
+                        help='Show the current TRex local server log file contents'
+                             ' => diagnostic/help in case of configuration problems')
+
+    parser.add_argument('--debug-mask', dest='debug_mask',
+                        type=int_arg,
+                        metavar='<mask>',
+                        action='store',
+                        default=None,
+                        help='General purpose register (debugging flags), '
+                             'the hexadecimal notation (0x...) is accepted.'
+                             'Designed for development needs (default: 0).')
+
     opts, unknown_opts = parser.parse_known_args()
     return opts, unknown_opts
 
@@ -565,13 +695,20 @@ def main():
         log.setup()
         # load default config file
         config, default_cfg = load_default_config()
+        # possibly override the default user_id & group_id values
+        if 'USER_ID' in os.environ:
+            config.user_id = int(os.environ['USER_ID'])
+        if 'GROUP_ID' in os.environ:
+            config.group_id = int(os.environ['GROUP_ID'])
+
         # create factory for platform specific classes
         try:
             factory_module = importlib.import_module(config['factory_module'])
             factory = getattr(factory_module, config['factory_class'])()
         except AttributeError:
             raise Exception("Requested factory module '{m}' or class '{c}' was not found."
-                            .format(m=config['factory_module'], c=config['factory_class']))
+                            .format(m=config['factory_module'],
+                                    c=config['factory_class'])) from AttributeError
         # create config plugin for this platform
         config_plugin = factory.get_config_plugin_class()(config)
         config = config_plugin.get_config()
@@ -596,10 +733,24 @@ def main():
             print((default_cfg.decode("utf-8")))
             sys.exit(0)
 
+        # dump the contents of the trex log file
+        if opts.show_trex_log:
+            try:
+                with open('/tmp/trex.log') as trex_log_file:
+                    print(trex_log_file.read(), end="")
+            except FileNotFoundError:
+                print("No TRex log file found!")
+            sys.exit(0)
+
+        # mask info logging in case of further config dump
+        if opts.show_config or opts.show_pre_config:
+            LOG.setLevel(log.logging.WARNING)
+
         config.name = ''
         if opts.config:
             # do not check extra_specs in flavor as it can contain any key/value pairs
-            whitelist_keys = ['extra_specs']
+            # the same principle applies also to the optional user_info open property
+            whitelist_keys = ['extra_specs', 'user_info']
             # override default config options with start config at path parsed from CLI
             # check if it is an inline yaml/json config or a file name
             if os.path.isfile(opts.config):
@@ -610,6 +761,11 @@ def main():
                 LOG.info('Loading configuration string: %s', opts.config)
                 config = config_loads(opts.config, config, whitelist_keys)
 
+        # show current config in json format (before CLI overriding)
+        if opts.show_pre_config:
+            print((json.dumps(config, sort_keys=True, indent=4)))
+            sys.exit(0)
+
         # setup the fluent logger as soon as possible right after the config plugin is called,
         # if there is any logging or result tag is set then initialize the fluent logger
         for fluentd in config.fluentd:
@@ -621,51 +777,124 @@ def main():
         # traffic profile override options
         override_custom_traffic(config, opts.frame_sizes, opts.unidir)
 
-        # copy over cli options that are used in config
+        # Copy over some of the cli options that are used in config.
+        # This explicit copy is sometimes necessary
+        # because some early evaluation depends on them
+        # and cannot wait for _update_config() coming further.
+        # It is good practice then to set them to None (<=> done)
+        # and even required if a specific conversion is performed here
+        # that would be corrupted by a default update (simple copy).
+        # On the other hand, some excessive assignments have been removed
+        # from here, since the _update_config() procedure does them well.
+
         config.generator_profile = opts.generator_profile
-        if opts.sriov:
+        if opts.sriov is not None:
             config.sriov = True
-        if opts.log_file:
+            opts.sriov = None
+        if opts.log_file is not None:
             config.log_file = opts.log_file
-        if opts.service_chain:
+            opts.log_file = None
+        if opts.user_id is not None:
+            config.user_id = opts.user_id
+            opts.user_id = None
+        if opts.group_id is not None:
+            config.group_id = opts.group_id
+            opts.group_id = None
+        if opts.service_chain is not None:
             config.service_chain = opts.service_chain
-        if opts.service_chain_count:
-            config.service_chain_count = opts.service_chain_count
-        if opts.no_vswitch_access:
-            config.no_vswitch_access = opts.no_vswitch_access
-        if opts.hypervisor:
+            opts.service_chain = None
+        if opts.hypervisor is not None:
             # can be any of 'comp1', 'nova:', 'nova:comp1'
             config.compute_nodes = opts.hypervisor
-        if opts.vxlan:
-            config.vxlan = True
-        if opts.mpls:
-            config.mpls = True
-        if opts.restart:
-            config.restart = True
-        if opts.service_mode:
-            config.service_mode = True
-        if opts.no_flow_stats:
-            config.no_flow_stats = True
-        if opts.no_latency_stats:
-            config.no_latency_stats = True
-        if opts.no_latency_streams:
-            config.no_latency_streams = True
-        # port to port loopback (direct or through switch)
-        if opts.l2_loopback:
-            config.l2_loopback = True
-            if config.service_chain != ChainType.EXT:
-                LOG.info('Changing service chain type to EXT')
-                config.service_chain = ChainType.EXT
-            if not config.no_arp:
-                LOG.info('Disabling ARP')
-                config.no_arp = True
-            config.vlans = [int(opts.l2_loopback), int(opts.l2_loopback)]
-            LOG.info('Running L2 loopback: using EXT chain/no ARP')
+            opts.hypervisor = None
+        if opts.debug_mask is not None:
+            config.debug_mask = opts.debug_mask
+            opts.debug_mask = None
+
+        # convert 'user_info' opt from json string to dictionnary
+        # and merge the result with the current config dictionnary
+        if opts.user_info is not None:
+            for user_info_json in opts.user_info:
+                user_info_dict = json.loads(user_info_json)
+                if config.user_info:
+                    config.user_info = config.user_info + user_info_dict
+                else:
+                    config.user_info = user_info_dict
+            opts.user_info = None
 
-        if opts.use_sriov_middle_net:
-            if (not config.sriov) or (config.service_chain != ChainType.PVVP):
-                raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
-            config.use_sriov_middle_net = True
+        # port to port loopback (direct or through switch)
+        # we accept the following syntaxes for the CLI argument
+        #   'false'   : mode not enabled
+        #   'true'    : mode enabled with currently defined vlan IDs
+        #   'no-tag'  : mode enabled with no vlan tagging
+        #   <vlan IDs>: mode enabled using the given (pair of) vlan ID lists
+        #     - If present, a '_' char will separate left an right ports lists
+        #         e.g. 'a_x'         => vlans: [[a],[x]]
+        #              'a,b,c_x,y,z' =>        [[a,b,c],[x,y,z]]
+        #     - Otherwise the given vlan ID list applies to both sides
+        #         e.g. 'a'           => vlans: [[a],[a]]
+        #              'a,b'         =>        [[a,b],[a,b]]
+        #     - Vlan lists size needs to be at least the actual SCC value
+        #     - Unless overriden in CLI opts, config.service_chain_count
+        #       is adjusted to the size of the VLAN ID lists given here.
+
+        if opts.l2_loopback is not None:
+            arg_pair = opts.l2_loopback.lower().split('_')
+            if arg_pair[0] == 'false':
+                config.l2_loopback = False
+            else:
+                config.l2_loopback = True
+                if config.service_chain != ChainType.EXT:
+                    LOG.info('Changing service chain type to EXT')
+                    config.service_chain = ChainType.EXT
+                if not config.no_arp:
+                    LOG.info('Disabling ARP')
+                    config.no_arp = True
+                if arg_pair[0] == 'true':
+                    pass
+                else:
+                    # here explicit (not)tagging is not CLI overridable
+                    opts.vlan_tagging = None
+                    if arg_pair[0] == 'no-tag':
+                        config.vlan_tagging = False
+                    else:
+                        config.vlan_tagging = True
+                        if len(arg_pair) == 1 or not arg_pair[1]:
+                            arg_pair = [arg_pair[0], arg_pair[0]]
+                        vlans = [[], []]
+
+                        def append_vlan(port, vlan_id):
+                            # a vlan tag value must be in [0..4095]
+                            if vlan_id not in range(0, 4096):
+                                raise ValueError
+                            vlans[port].append(vlan_id)
+                        try:
+                            for port in [0, 1]:
+                                vlan_ids = arg_pair[port].split(',')
+                                for vlan_id in vlan_ids:
+                                    append_vlan(port, int(vlan_id))
+                            if len(vlans[0]) != len(vlans[1]):
+                                raise ValueError
+                        except ValueError:
+                            # at least one invalid tag => no tagging
+                            config.vlan_tagging = False
+                        if config.vlan_tagging:
+                            config.vlans = vlans
+                            # force service chain count if not CLI overriden
+                            if opts.service_chain_count is None:
+                                config.service_chain_count = len(vlans[0])
+            opts.l2_loopback = None
+
+        if config.i40e_mixed is None:
+            config.i40e_mixed = 'ignore'
+        if config.use_sriov_middle_net is None:
+            config.use_sriov_middle_net = False
+        if opts.use_sriov_middle_net is not None:
+            config.use_sriov_middle_net = opts.use_sriov_middle_net
+            opts.use_sriov_middle_net = None
+        if (config.use_sriov_middle_net and (
+                (not config.sriov) or (config.service_chain != ChainType.PVVP))):
+            raise Exception("--use-sriov-middle-net is only valid for PVVP with SRIOV")
 
         if config.sriov and config.service_chain != ChainType.EXT:
             # if sriov is requested (does not apply to ext chains)
@@ -675,11 +904,6 @@ def main():
             if config.service_chain == ChainType.PVVP and config.use_sriov_middle_net:
                 check_physnet("middle", config.internal_networks.middle)
 
-        # show running config in json format
-        if opts.show_config:
-            print((json.dumps(config, sort_keys=True, indent=4)))
-            sys.exit(0)
-
         # update the config in the config plugin as it might have changed
         # in a copy of the dict (config plugin still holds the original dict)
         config_plugin.set_config(config)
@@ -690,6 +914,13 @@ def main():
         # add file log if requested
         if config.log_file:
             log.add_file_logger(config.log_file)
+            # possibly change file ownership
+            uid = config.user_id
+            gid = config.group_id
+            if gid is None:
+                gid = uid
+            if uid is not None:
+                os.chown(config.log_file, uid, gid)
 
         openstack_spec = config_plugin.get_openstack_spec() if config.openrc_file \
             else None
@@ -706,6 +937,7 @@ def main():
                 server.run(host=opts.host, port=port)
             # server.run() should never return
         else:
+            dry_run = opts.show_config
             with utils.RunLock():
                 run_summary_required = True
                 if unknown_opts:
@@ -717,7 +949,7 @@ def main():
                 opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
                 # get CLI args
                 params = ' '.join(str(e) for e in sys.argv[1:])
-                result = nfvbench_instance.run(opts, params)
+                result = nfvbench_instance.run(opts, params, dry_run=dry_run)
                 if 'error_message' in result:
                     raise Exception(result['error_message'])