# Size of local disk in GB
   disk: 0
   # metadata are supported and can be added if needed, optional
-  # note that if your openstack does not have NUMA optimization
+  # note that if your OpenStack does not have NUMA optimization
   # (cpu pinning and huge pages)
   # you must comment out extra_specs completely otherwise
   # loopback VM creation will fail
 # When multiqueue is used the recommended setting is to set it to same value as the
 # number of vCPU used - up to a max of 8 queues.
 # Setting to a lower value than vCPU should also work. For example if using 4 vCPU and
-# vif_multiqueue_size is set to 2, openstack will create 4 queues per interface but the
+# vif_multiqueue_size is set to 2, OpenStack will create 4 queues per interface but the
 # test VM will only use the first 2 queues.
 vif_multiqueue_size: 1
 
 availability_zone:
 # To force placement on a given hypervisor, set the name here
 # (if multiple names are provided, the first will be used)
-# Leave empty to let openstack pick the hypervisor
+# Leave empty to let OpenStack pick the hypervisor
 compute_nodes:
 # If openrc is not admin set a valid value for hypervisor hostname
 # Example of value: hypervisor_hostname: "server1"
 sriov: false
 
 # Perform port to port loopback (direct or through switch)
-# Should be used with EXT service chain and no ARP (no_arp: true)
-# When enabled, the vlans property must contain the same VLAN id for all chains.
-# Can be overriden by --l2-loopback
+# e.g. for unitary testing of the switch or the bench itself.
+# When selected, this mode forces EXT service chain and no ARP mode
+# Destination MAC for each port is set to the other (peer) port MAC.
+# VLAN tagging is defined by 'vlans' & 'vlan_tagging' properties.
+# Can be overriden by --l2-loopback (including vlan tagging spec).
 l2_loopback: false
+# No assumption is made about the loop implementation.
+# Multiple L2 vlan tagged service chains are allowed,
+# the vlan ID lists' size must be at least service_chain_count.
+# If not vlan tagging, the service chain count is forced to 1.
 
 # Resources created by NFVbench will not be removed
 # Can be overriden by --no-cleanup
 # name of the loop VM
 loop_vm_name: 'nfvbench-loop-vm'
 
-# Default names, subnets and CIDRs for PVP/PVVP networks (openstack only)
+# Default names, subnets and CIDRs for PVP/PVVP networks (OpenStack only)
 #
 # If a network with given name already exists it will be reused.
 # - PVP only uses left and right
 # is not supported). Use the vtep_vlan option to enable vlan tagging for the VxLAN overlay network.
 vlan_tagging: true
 
-# Used only in the case of EXT chain and no openstack or not admin access to specify the VLAN IDs to use.
-# This property is ignored when OpenStakc is used or in the case of l2-loopback.
+# Used only in the case of EXT chain and no OpenStack or not admin access to specify the VLAN IDs to use.
+# This property is ignored when OpenStack is used or when 'vlan_tagging' is disabled.
 # If OpenStack is used leave the list empty, VLAN IDs are retrieved from OpenStack networks using Neutron API.
 # If networks are shared across all chains (service_chain_shared_net=true), the list should have exactly 2 values
 # If networks are not shared across chains (service_chain_shared_net=false), the list should have
 # 2 list of vlan IDs
-# In the special case of l2-loopback the list should have the same VLAN id for all chains
 # Examples:
 #   [1998, 1999] left network uses vlan 1998 right network uses vlan 1999
 #   [[1,2],[3,4]] chain 0 left vlan 1, right vlan 2 - chain 1 left vlan 3 right vlan 4
-#   [1010, 1010] same VLAN id with l2-loopback enabled
-#
+#   [1010, 1010] same vlan ID on both sides, for a typical l2-loopback test (*)
+# The vlan lists may be oversized, compared to the actual service chain count
+# (lowest indexes are used) but an exception is raised if they are too short.
 vlans: []
+# (*) actually there is no restriction, left/right IDs may differ
+#     for some exotic purpose - see also the l2_loopback parameter.
 
 # ARP is used to discover the MAC address of VNFs that run L3 routing.
 # Used only with EXT chain.
 # Can be overriden by --no-traffic
 no_traffic: false
 
-# Use an L3 router in the packet path. This option if set will create or reuse an openstack neutron
+# Use an L3 router in the packet path. This option if set will create or reuse an OpenStack neutron
 # router (PVP, PVVP) or reuse an existing L3 router (EXT) to route traffic to the destination VM.
 # Can be overriden by --l3-router
 l3_router: false
 
         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
 
     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('--user-info', dest='user_info',
-                        action='store',
+                        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}}\'')
+                        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,
                         action='store',
                         default=None,
                         help='Override the NFVbench \'intf_speed\' '
-                                + 'parameter (e.g. 10Gbps, auto, 16.72Gbps)')
+                             'parameter (e.g. 10Gbps, auto, 16.72Gbps)')
 
     parser.add_argument('--cores', dest='cores',
                         type=int_arg,
                         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,
+                        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=False,
+                        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,
+                        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',
                         default=None,
                         action='store_true',
                         help='Show the current TRex local server log file contents'
-                               + ' => diagnostic/help in case of configuration problems')
+                             ' => diagnostic/help in case of configuration problems')
 
     parser.add_argument('--debug-mask', dest='debug_mask',
                         type=int_arg,
                         metavar='<mask>',
                         action='store',
-                        default='0x00000000',
+                        default=None,
                         help='General purpose register (debugging flags), '
-                                + 'the hexadecimal notation (0x...) is accepted.'
-                                + 'Designed for development needs.')
+                             'the hexadecimal notation (0x...) is accepted.'
+                             'Designed for development needs (default: 0).')
 
     opts, unknown_opts = parser.parse_known_args()
     return opts, unknown_opts
                 LOG.addHandler(fluent_logger)
                 break
 
-        # convert 'user_info' opt from json string to dictionnary
-        # and merge the result with the current config dictionnary
-        if opts.user_info:
-            opts.user_info = json.loads(opts.user_info)
-            if config.user_info:
-                config.user_info = config.user_info + opts.user_info
-            else:
-                config.user_info = opts.user_info
-            # hide the option to further _update_config()
-            opts.user_info = None
-
         # 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
 
-        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
+        # 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
+
+        # 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.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)
 
         """
         streams = []
         pg_id, lat_pg_id = self.get_pg_id(port, chain_id)
-        if self.config.no_flow_stats:
-            LOG.info("Traffic flow statistics are disabled.")
         if l2frame == 'IMIX':
             for ratio, l2_frame_size in zip(IMIX_RATIOS, IMIX_L2_SIZES):
                 pkt = self._create_pkt(stream_cfg, l2_frame_size)
                         streams.append(STLStream(packet=pkt,
                                                  flow_stats=STLFlowStats(pg_id=pg_id,
                                                                          vxlan=True)
-                                                 if not self.config.no_flow_stats else None,
+                                                    if not self.config.no_flow_stats else None,
                                                  mode=STLTXCont(pps=ratio)))
                     else:
                         streams.append(STLStream(packet=pkt,
                                                  flow_stats=STLFlowStats(pg_id=pg_id)
-                                                 if not self.config.no_flow_stats else None,
+                                                    if not self.config.no_flow_stats else None,
                                                  mode=STLTXCont(pps=ratio)))
 
             if latency:
                     streams.append(STLStream(packet=pkt,
                                              flow_stats=STLFlowStats(pg_id=pg_id,
                                                                      vxlan=True)
-                                             if not self.config.no_flow_stats else None,
+                                                if not self.config.no_flow_stats else None,
                                              mode=STLTXCont()))
                 else:
                     streams.append(STLStream(packet=pkt,
                                              flow_stats=STLFlowStats(pg_id=pg_id)
-                                             if not self.config.no_flow_stats else None,
+                                                if not self.config.no_flow_stats else None,
                                              mode=STLTXCont()))
             # for the latency stream, the minimum payload is 16 bytes even in case of vlan tagging
             # without vlan, the min l2 frame size is 64
                 streams.append(STLStream(packet=pkt,
                                          flow_stats=STLFlowLatencyStats(pg_id=lat_pg_id,
                                                                         vxlan=True)
-                                         if not self.config.no_latency_stats else None,
+                                            if not self.config.no_latency_stats else None,
                                          mode=STLTXCont(pps=self.LATENCY_PPS)))
             else:
                 streams.append(STLStream(packet=pkt,
                                          flow_stats=STLFlowLatencyStats(pg_id=lat_pg_id)
-                                         if not self.config.no_latency_stats else None,
+                                            if not self.config.no_latency_stats else None,
                                          mode=STLTXCont(pps=self.LATENCY_PPS)))
         return streams
 
         latency: True if latency measurement is needed
         e2e: True if performing "end to end" connectivity check
         """
+        if self.config.no_flow_stats:
+            LOG.info("Traffic flow statistics are disabled.")
         r = self.__is_rate_enough(l2frame_size, rates, bidirectional, latency)
         if not r['result']:
             raise TrafficGeneratorException(