Merge "Cover both enabled and disabled cases in nested snat validation test"
diff --git a/neutron_tempest_plugin/api/base_routers.py b/neutron_tempest_plugin/api/base_routers.py
index 94db116..37a84b8 100644
--- a/neutron_tempest_plugin/api/base_routers.py
+++ b/neutron_tempest_plugin/api/base_routers.py
@@ -31,10 +31,14 @@
             pass
 
     def _create_router(self, name, admin_state_up=False,
-                       external_network_id=None, enable_snat=None, **kwargs):
+                       external_network_id=None, enable_snat=None,
+                       client=None, **kwargs):
         # associate a cleanup with created routers to avoid quota limits
-        router = self.create_router(name, admin_state_up,
-                                    external_network_id, enable_snat, **kwargs)
+        client = client or self.client
+        router = self._create_router_with_client(
+            client, router_name=name, admin_state_up=admin_state_up,
+            external_network_id=external_network_id, enable_snat=enable_snat,
+            **kwargs)
         self.addCleanup(self._cleanup_router, router)
         return router
 
diff --git a/neutron_tempest_plugin/api/test_routers.py b/neutron_tempest_plugin/api/test_routers.py
index 1430b81..1470a7b 100644
--- a/neutron_tempest_plugin/api/test_routers.py
+++ b/neutron_tempest_plugin/api/test_routers.py
@@ -377,7 +377,7 @@
         router = self._create_router(data_utils.rand_name('router'))
         self.assertEqual(len(router['external_gateways']), 0)
 
-        res = self.client.router_add_external_gateways(
+        res = self.admin_client.router_add_external_gateways(
             router['id'],
             [{'network_id': CONF.network.public_network_id,
               'enable_snat': False}])
@@ -391,7 +391,7 @@
         router = self._create_router(data_utils.rand_name('router'))
         self.assertEqual(len(router['external_gateways']), 0)
 
-        res = self.client.router_add_external_gateways(
+        res = self.admin_client.router_add_external_gateways(
             router['id'],
             [
                 {'network_id': CONF.network.public_network_id,
@@ -424,9 +424,11 @@
         router = self._create_router(
             data_utils.rand_name('router'),
             external_network_id=CONF.network.public_network_id,
-            enable_snat=False)
+            enable_snat=False,
+            client=self.admin_client,
+        )
         self.assertEqual(len(router['external_gateways']), 1)
-        res = self.client.router_add_external_gateways(
+        res = self.admin_client.router_add_external_gateways(
             router['id'],
             [{'network_id': CONF.network.public_network_id,
               'enable_snat': False}])
@@ -437,9 +439,10 @@
         router = self._create_router(
             data_utils.rand_name('router'),
             external_network_id=CONF.network.public_network_id,
-            enable_snat=False)
+            enable_snat=False,
+            client=self.admin_client)
         self.assertEqual(len(router['external_gateways']), 1)
-        res = self.client.router_remove_external_gateways(
+        res = self.admin_client.router_remove_external_gateways(
             router['id'],
             [{'network_id': CONF.network.public_network_id}])
         self.assertEqual(len(res['router']['external_gateways']), 0)
@@ -449,7 +452,7 @@
         router = self._create_router(data_utils.rand_name('router'))
         self.assertEqual(len(router['external_gateways']), 0)
 
-        res = self.client.router_add_external_gateways(
+        res = self.admin_client.router_add_external_gateways(
             router['id'],
             [
                 {'network_id': CONF.network.public_network_id,
@@ -480,7 +483,7 @@
         router = self._create_router(data_utils.rand_name('router'))
         self.assertEqual(len(router['external_gateways']), 0)
 
-        res = self.client.router_add_external_gateways(
+        res = self.admin_client.router_add_external_gateways(
             router['id'],
             [
                 {'network_id': CONF.network.public_network_id,
@@ -503,7 +506,7 @@
                     remove_gateways[0])
 
         external_gateways[1] = remove_gateways[0]
-        res_update_gws = self.client.router_update_external_gateways(
+        res_update_gws = self.admin_client.router_update_external_gateways(
             router['id'],
             external_gateways)
 
diff --git a/neutron_tempest_plugin/common/ip.py b/neutron_tempest_plugin/common/ip.py
index 07bbe69..5335219 100644
--- a/neutron_tempest_plugin/common/ip.py
+++ b/neutron_tempest_plugin/common/ip.py
@@ -96,7 +96,7 @@
         return self.configure_vlan(addresses, port, vlan_tag, subport_ips,
                                    subport['mac_address'])
 
-    def configure_vlan_transparent(self, port, vlan_tag, ip_addresses):
+    def configure_inner_vlan(self, port, vlan_tag, ip_addresses):
         addresses = self.list_addresses()
         try:
             subport_device = get_vlan_device_name(addresses, ip_addresses)
@@ -108,6 +108,11 @@
 
         return self.configure_vlan(addresses, port, vlan_tag, ip_addresses)
 
+    # NOTE(ralonsoh): some projects, like whitebox-neutron-tempest-plugin, are
+    # using ``configure_vlan_transparent`` method. The concept of "inner VLAN"
+    # does not exist in the VLAN transparency feature.
+    configure_vlan_transparent = configure_inner_vlan
+
     def list_namespaces(self):
         namespaces_output = self.execute("netns")
         ns_list = []
diff --git a/neutron_tempest_plugin/scenario/test_multiple_gws.py b/neutron_tempest_plugin/scenario/test_multiple_gws.py
index 0a540f6..e4f1d3d 100644
--- a/neutron_tempest_plugin/scenario/test_multiple_gws.py
+++ b/neutron_tempest_plugin/scenario/test_multiple_gws.py
@@ -630,7 +630,8 @@
     def test_update_router_single_gw_bfd(self):
         ext_network_id = self.ext_networks[0][0]['id']
         bfd_container = self.containers[0]
-        router = self.create_router(
+        router = self._create_router_with_client(
+            self.admin_client,
             router_name=data_utils.rand_name('router'),
             admin_state_up=True,
             enable_snat=False,
diff --git a/neutron_tempest_plugin/scenario/test_vlan_transparency.py b/neutron_tempest_plugin/scenario/test_vlan_transparency.py
index 11f12e9..85648bc 100644
--- a/neutron_tempest_plugin/scenario/test_vlan_transparency.py
+++ b/neutron_tempest_plugin/scenario/test_vlan_transparency.py
@@ -30,20 +30,27 @@
 MAX_VLAN_ID = 4094
 
 
-class VlanTransparencyTest(base.BaseTempestTestCase):
-    credentials = ['primary', 'admin']
-    force_tenant_isolation = False
+class BaseVlanTest(base.BaseAdminTempestTestCase):
 
-    required_extensions = ['vlan-transparent', 'allowed-address-pairs']
+    """Base class common for the tests for the "vlan_transparent" and "qinq".
+
+    This base class covers common things for the tests for networks with
+    enabled either `qinq` or `vlan_transparent` attributes. Those 2 attributes
+    are functionally the same even from the end user's point of view. The only
+    difference between them is ethtype used for the outer VLAN tag but this
+    can't really be tested in the neutron-tempest-plugin tests.
+    """
+
+    network_kwargs = {}
 
     @classmethod
     def resource_setup(cls):
-        super(VlanTransparencyTest, cls).resource_setup()
+        super(BaseVlanTest, cls).resource_setup()
         # setup basic topology for servers we can log into
         cls.rand_name = data_utils.rand_name(
             cls.__name__.rsplit('.', 1)[-1])
         cls.network = cls.create_network(name=cls.rand_name,
-                                         vlan_transparent=True)
+                                         **cls.network_kwargs)
         cls.subnet = cls.create_subnet(network=cls.network,
                                        name=cls.rand_name)
         cls.router = cls.create_router_by_client()
@@ -63,7 +70,7 @@
 
     @classmethod
     def skip_checks(cls):
-        super(VlanTransparencyTest, cls).skip_checks()
+        super().skip_checks()
         if not (CONF.neutron_plugin_options.advanced_image_ref or
                 CONF.neutron_plugin_options.default_image_is_advanced):
             raise cls.skipException(
@@ -89,12 +96,11 @@
                                   networks=[{'port': self.vm_ports[-1]['id']}],
                                   name=server_name)['server']
 
-    def _configure_vlan_transparent(self, port, ssh_client,
-                                    vlan_tag, vlan_ip):
+    def _configure_inner_vlan(self, port, ssh_client, vlan_tag, vlan_ip):
         ip_command = ip.IPCommand(ssh_client=ssh_client)
         addresses = ip_command.list_addresses(port=port)
         port_iface = ip.get_port_device_name(addresses, port)
-        subport_iface = ip_command.configure_vlan_transparent(
+        subport_iface = ip_command.configure_inner_vlan(
             port=port, vlan_tag=vlan_tag, ip_addresses=[vlan_ip])
 
         for address in ip_command.list_addresses(ip_addresses=vlan_ip):
@@ -113,8 +119,9 @@
                           username=username,
                           pkey=self.keypair['private_key'])
 
-    def _test_basic_vlan_transparency_connectivity(
+    def _test_basic_inner_vlan_connectivity(
             self, port_security=True, use_allowed_address_pairs=False):
+        self._ensure_ethtype()
         vlan_tag = data_utils.rand_int_id(start=MIN_VLAN_ID, end=MAX_VLAN_ID)
         vlan_ipmask_template = '192.168.%d.{ip_last_byte}/24' % (vlan_tag %
                                                                  256)
@@ -139,10 +146,10 @@
                 self._create_ssh_client(floating_ip=floating_ips[i]))
 
             self.check_connectivity(ssh_client=ssh_clients[i])
-            self._configure_vlan_transparent(port=self.vm_ports[-1],
-                                             ssh_client=ssh_clients[i],
-                                             vlan_tag=vlan_tag,
-                                             vlan_ip=vlan_ipmasks[i])
+            self._configure_inner_vlan(port=self.vm_ports[-1],
+                                       ssh_client=ssh_clients[i],
+                                       vlan_tag=vlan_tag,
+                                       vlan_ip=vlan_ipmasks[i])
 
         if port_security:
             # Ping from vm0 to vm1 via VLAN interface should fail because
@@ -173,12 +180,63 @@
             self.vm_ports[-2]['fixed_ips'][0]['ip_address'],
             servers=vms)
 
+
+class VlanTransparencyTest(BaseVlanTest):
+    credentials = ['primary', 'admin']
+    force_tenant_isolation = False
+
+    required_extensions = ['vlan-transparent', 'allowed-address-pairs']
+
+    network_kwargs = {'vlan_transparent': True}
+
+    def _ensure_ethtype(self):
+        self.assertTrue(self.network.get('vlan_transparent'))
+        self.assertFalse(self.network.get('qinq'))
+
     @decorators.idempotent_id('a2694e3a-6d4d-4a23-9fcc-c3ed3ef37b16')
     def test_vlan_transparent_port_sec_disabled(self):
-        self._test_basic_vlan_transparency_connectivity(
+        self._test_basic_inner_vlan_connectivity(
             port_security=False, use_allowed_address_pairs=False)
 
     @decorators.idempotent_id('2dd03b4f-9c20-4cda-8c6a-40fa453ec69a')
     def test_vlan_transparent_allowed_address_pairs(self):
-        self._test_basic_vlan_transparency_connectivity(
+        self._test_basic_inner_vlan_connectivity(
+            port_security=True, use_allowed_address_pairs=True)
+
+
+class VlanQinqTest(BaseVlanTest):
+    credentials = ['primary', 'admin']
+    force_tenant_isolation = False
+
+    required_extensions = ['qinq', 'allowed-address-pairs']
+
+    network_kwargs = {
+        'qinq': True,
+        'provider_network_type': 'vlan'}
+
+    @classmethod
+    def resource_setup(cls):
+        cls.network_kwargs['provider_physical_network'] = (
+            CONF.neutron_plugin_options.provider_vlans[0])
+        super().resource_setup()
+
+    @classmethod
+    def skip_checks(cls):
+        super().skip_checks()
+        if not CONF.neutron_plugin_options.provider_vlans:
+            raise cls.skipException(
+                'Physical network is required to run these tests.')
+
+    def _ensure_ethtype(self):
+        self.assertFalse(self.network.get('vlan_transparent'))
+        self.assertTrue(self.network.get('qinq'))
+
+    @decorators.idempotent_id('ae78398e-9242-46b4-a5fc-227581821fca')
+    def test_vlan_transparent_port_sec_disabled(self):
+        self._test_basic_inner_vlan_connectivity(
+            port_security=False, use_allowed_address_pairs=False)
+
+    @decorators.idempotent_id('6ca983cd-b1c5-4e2c-949e-4be8ffa22a9c')
+    def test_vlan_transparent_allowed_address_pairs(self):
+        self._test_basic_inner_vlan_connectivity(
             port_security=True, use_allowed_address_pairs=True)
diff --git a/zuul.d/master_jobs.yaml b/zuul.d/master_jobs.yaml
index b8d2103..b2c79a1 100644
--- a/zuul.d/master_jobs.yaml
+++ b/zuul.d/master_jobs.yaml
@@ -628,6 +628,7 @@
     vars:
       network_api_extensions_ovn:
         - vlan-transparent
+        - qinq
         - external-gateway-multihoming
       devstack_localrc:
         Q_AGENT: ovn
@@ -678,6 +679,7 @@
             DEFAULT:
               enable_dvr: false
               vlan_transparent: true
+              vlan_qinq: true
           /$NEUTRON_CORE_PLUGIN_CONF:
             ml2:
               type_drivers: local,flat,vlan,geneve
@@ -692,6 +694,7 @@
             network-feature-enabled:
               available_features: "{{ network_available_features | join(',') }}"
             neutron_plugin_options:
+              provider_vlans: public,
               available_type_drivers: local,flat,vlan,geneve
               is_igmp_snooping_enabled: True
               firewall_driver: ovn
@@ -1437,7 +1440,7 @@
 - job:
     name: neutron-tempest-plugin-vpnaas
     parent: neutron-tempest-plugin-base
-    timeout: 3900
+    timeout: 4500
     required-projects:
       - openstack/neutron
       - openstack/neutron-vpnaas
@@ -1536,6 +1539,7 @@
       devstack_localrc:
         IPSEC_PACKAGE: strongswan
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_vpnaas) | join(',') }}"
+        Q_ML2_PLUGIN_MECHANISM_DRIVERS: ovn
       devstack_services:
         q-ovn-vpn-agent: true
       devstack_local_conf:
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index ee75bf4..1457981 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -3,7 +3,6 @@
     check:
       jobs:
         - neutron-tempest-plugin-openvswitch
-        - neutron-tempest-plugin-openvswitch-iptables_hybrid
         - neutron-tempest-plugin-ovn
         - neutron-tempest-plugin-designate-scenario:
             # TODO(slaweq) make job voting again once bug
@@ -15,19 +14,19 @@
       jobs:
         - neutron-tempest-plugin-openvswitch
         - neutron-tempest-plugin-ovn
-        - neutron-tempest-plugin-openvswitch-iptables_hybrid
     #TODO(slaweq): Move neutron-tempest-plugin-dvr-multinode-scenario out of
     #              the experimental queue when it will be more stable
     experimental:
       jobs:
-        - neutron-tempest-plugin-linuxbridge
         - neutron-tempest-plugin-dvr-multinode-scenario
         - neutron-tempest-plugin-openvswitch-distributed-dhcp
         - neutron-tempest-plugin-openvswitch-iptables_hybrid-distributed-dhcp
         - neutron-tempest-plugin-ovn-enforce-scope-old-defaults
+        - neutron-tempest-plugin-openvswitch-iptables_hybrid
     periodic:
       jobs:
         - neutron-tempest-plugin-ovn-enforce-scope-old-defaults
+        - neutron-tempest-plugin-openvswitch-iptables_hybrid
 
 
 - project-template:
@@ -218,7 +217,14 @@
         - neutron-tempest-plugin-bgpvpn-bagpipe-2023-2
         - neutron-tempest-plugin-bgpvpn-bagpipe-2024-1
         - neutron-tempest-plugin-bgpvpn-bagpipe-2024-2
-        - neutron-tempest-plugin-dynamic-routing
+        - neutron-tempest-plugin-dynamic-routing:
+            # TODO(ralonsoh): this job is temporarily disabled; it will be
+            # restored once [1] is merged. This patch has been successfully
+            # tested in [2]. This job is removed from the gate queue,
+            # thus **remember to restore it in this queue too**.
+            # [1]https://review.opendev.org/c/openstack/neutron/+/941202
+            # [2]https://review.opendev.org/c/openstack/neutron-tempest-plugin/+/940906
+            voting: false
         - neutron-tempest-plugin-dynamic-routing-2023-2
         - neutron-tempest-plugin-dynamic-routing-2024-1
         - neutron-tempest-plugin-dynamic-routing-2024-2
@@ -240,6 +246,5 @@
       jobs:
         - neutron-tempest-plugin-sfc
         - neutron-tempest-plugin-bgpvpn-bagpipe
-        - neutron-tempest-plugin-dynamic-routing
         - neutron-tempest-plugin-fwaas
         - neutron-tempest-plugin-vpnaas-ovn