Merge "Test metadata query over IPv6 only network with OVS and LB"
diff --git a/neutron_tempest_plugin/api/base.py b/neutron_tempest_plugin/api/base.py
index 99fa946..e17cf5e 100644
--- a/neutron_tempest_plugin/api/base.py
+++ b/neutron_tempest_plugin/api/base.py
@@ -1466,6 +1466,10 @@
     def _extract_resources(cls, body):
         return body[cls.plural_name]
 
+    @classmethod
+    def _test_resources(cls, resources):
+        return [res for res in resources if res["name"] in cls.resource_names]
+
     def _test_list_sorts(self, direction):
         sort_args = {
             'sort_dir': direction,
@@ -1517,11 +1521,12 @@
             'sort_key': self.field
         }
         body = self.list_method(**sort_args)
-        expected_resources = self._extract_resources(body)
+        total_resources = self._extract_resources(body)
+        expected_resources = self._test_resources(total_resources)
         self.assertNotEmpty(expected_resources)
 
         resources = lister(
-            len(expected_resources), sort_args
+            len(total_resources), sort_args
         )
 
         # finally, compare that the list retrieved in one go is identical to
@@ -1539,9 +1544,11 @@
                 pagination_args['marker'] = resources[-1]['id']
             body = self.list_method(**pagination_args)
             resources_ = self._extract_resources(body)
-            self.assertEqual(1, len(resources_))
+            # Empty resource list can be returned when any concurrent
+            # tests delete them
+            self.assertGreaterEqual(1, len(resources_))
             resources.extend(resources_)
-        return resources
+        return self._test_resources(resources)
 
     @_require_pagination
     @_require_sorting
@@ -1564,8 +1571,10 @@
                 self.plural_name, uri
             )
             resources_ = self._extract_resources(body)
-            self.assertEqual(1, len(resources_))
-            resources.extend(resources_)
+            # Empty resource list can be returned when any concurrent
+            # tests delete them
+            self.assertGreaterEqual(1, len(resources_))
+            resources.extend(self._test_resources(resources_))
 
         # The last element is empty and does not contain 'next' link
         uri = self.get_bare_url(prev_links['next'])
@@ -1582,8 +1591,10 @@
                 self.plural_name, uri
             )
             resources_ = self._extract_resources(body)
-            self.assertEqual(1, len(resources_))
-            resources2.extend(resources_)
+            # Empty resource list can be returned when any concurrent
+            # tests delete them
+            self.assertGreaterEqual(1, len(resources_))
+            resources2.extend(self._test_resources(resources_))
 
         self.assertSameOrder(resources, reversed(resources2))
 
@@ -1603,14 +1614,15 @@
             'sort_key': self.field,
         }
         body = self.list_method(**pagination_args)
-        expected_resources = self._extract_resources(body)
+        total_resources = self._extract_resources(body)
+        expected_resources = self._test_resources(total_resources)
 
         page_size = 2
         pagination_args['limit'] = page_size
 
         prev_links = {}
         resources = []
-        num_resources = len(expected_resources)
+        num_resources = len(total_resources)
         niterations = int(math.ceil(float(num_resources) / page_size))
         for i in range(niterations):
             if prev_links:
@@ -1622,7 +1634,7 @@
             prev_links, body = self.list_client.get_uri_with_links(
                 self.plural_name, uri
             )
-            resources_ = self._extract_resources(body)
+            resources_ = self._test_resources(self._extract_resources(body))
             self.assertGreaterEqual(page_size, len(resources_))
             resources.extend(reversed(resources_))
 
diff --git a/neutron_tempest_plugin/config.py b/neutron_tempest_plugin/config.py
index a6d5c09..55d9d9e 100644
--- a/neutron_tempest_plugin/config.py
+++ b/neutron_tempest_plugin/config.py
@@ -77,6 +77,12 @@
                default='openstackgate.local',
                help='dns_domain value configured at neutron.conf, which will '
                     'be used for the DNS configuration of the instances'),
+    cfg.BoolOpt('snat_rules_apply_to_nested_networks',
+                default=False,
+                help='Whether SNAT rules apply recursively to all connected '
+                'networks. This is the default behavior for ovs and '
+                'linuxbridge drivers. OVN requires '
+                'ovn_router_indirect_snat=True setting to implement it.'),
 
     # Multicast tests settings
     cfg.StrOpt('multicast_group_range',
diff --git a/neutron_tempest_plugin/scenario/base.py b/neutron_tempest_plugin/scenario/base.py
index 6149b06..70cb2dc 100644
--- a/neutron_tempest_plugin/scenario/base.py
+++ b/neutron_tempest_plugin/scenario/base.py
@@ -337,7 +337,7 @@
         if create_fip:
             self.fip = self.create_floatingip(port=self.port)
 
-    def check_connectivity(self, host, ssh_user=None, ssh_key=None,
+    def check_connectivity(self, host=None, ssh_user=None, ssh_key=None,
                            servers=None, ssh_timeout=None, ssh_client=None):
         # Either ssh_client or ssh_user+ssh_key is mandatory.
         if ssh_client is None:
diff --git a/neutron_tempest_plugin/scenario/test_floatingip.py b/neutron_tempest_plugin/scenario/test_floatingip.py
index f222396..9b7bfcc 100644
--- a/neutron_tempest_plugin/scenario/test_floatingip.py
+++ b/neutron_tempest_plugin/scenario/test_floatingip.py
@@ -18,7 +18,6 @@
 import ddt
 from neutron_lib import constants as lib_constants
 from neutron_lib.services.qos import constants as qos_consts
-from neutron_lib.utils import test
 from oslo_log import log
 from tempest.common import utils
 from tempest.common import waiters
@@ -147,7 +146,6 @@
 
     same_network = True
 
-    @test.unstable_test("bug 1717302")
     @decorators.idempotent_id('05c4e3b3-7319-4052-90ad-e8916436c23b')
     @ddt.unpack
     @ddt.data({'src_has_fip': True, 'dest_has_fip': True},
@@ -165,7 +163,6 @@
 
     same_network = False
 
-    @test.unstable_test("bug 1717302")
     @decorators.idempotent_id('f18f0090-3289-4783-b956-a0f8ac511e8b')
     @ddt.unpack
     @ddt.data({'src_has_fip': True, 'dest_has_fip': True},
@@ -204,6 +201,60 @@
                                        gateway_external_ip,
                                        servers=[proxy, src_server])
 
+    @decorators.idempotent_id('b911b124-b6cb-449d-83d9-b34f3665741d')
+    @utils.requires_ext(extension='extraroute', service='network')
+    @testtools.skipUnless(
+        CONF.neutron_plugin_options.snat_rules_apply_to_nested_networks,
+        "Backend doesn't enable nested SNAT.")
+    def test_nested_snat_external_ip(self):
+        """Check connectivity to an external IP from a nested network."""
+        gateway_external_ip = self._get_external_gateway()
+
+        if not gateway_external_ip:
+            raise self.skipTest("IPv4 gateway is not configured for public "
+                                "network or public_network_id is not "
+                                "configured")
+        proxy = self._create_server()
+        proxy_client = ssh.Client(proxy['fip']['floating_ip_address'],
+                                  CONF.validation.image_ssh_user,
+                                  pkey=self.keypair['private_key'])
+
+        # Create a nested router
+        router = self.create_router(
+            router_name=data_utils.rand_name('router'),
+            admin_state_up=True)
+
+        # Attach outer subnet to it
+        outer_port = self.create_port(self.network)
+        self.client.add_router_interface_with_port_id(router['id'],
+                                                      outer_port['id'])
+
+        # Attach a nested subnet to it
+        network = self.create_network()
+        subnet = self.create_subnet(network)
+        self.create_router_interface(router['id'], subnet['id'])
+
+        # Set up static routes in both directions
+        self.client.update_extra_routes(
+            self.router['id'],
+            outer_port['fixed_ips'][0]['ip_address'], subnet['cidr'])
+        self.client.update_extra_routes(
+            router['id'], self.subnet['gateway_ip'], '0.0.0.0/0')
+
+        # Create a server inside the nested network
+        src_server = self._create_server(create_floating_ip=False,
+                                         network=network)
+
+        # Validate that it can access external gw ip (via nested snat)
+        src_server_ip = src_server['port']['fixed_ips'][0]['ip_address']
+        ssh_client = ssh.Client(src_server_ip,
+                                CONF.validation.image_ssh_user,
+                                pkey=self.keypair['private_key'],
+                                proxy_client=proxy_client)
+        self.check_remote_connectivity(ssh_client,
+                                       gateway_external_ip,
+                                       servers=[proxy, src_server])
+
 
 class FloatingIPPortDetailsTest(FloatingIpTestCasesMixin,
                                 base.BaseTempestTestCase):
diff --git a/neutron_tempest_plugin/scenario/test_metadata.py b/neutron_tempest_plugin/scenario/test_metadata.py
index 1c63816..26fbab6 100644
--- a/neutron_tempest_plugin/scenario/test_metadata.py
+++ b/neutron_tempest_plugin/scenario/test_metadata.py
@@ -169,8 +169,7 @@
             self.network, use_advanced_image=use_advanced_image)
         self.wait_for_server_active(server=vm.server)
         self.wait_for_guest_os_ready(vm.server)
-        self.check_connectivity(host=vm.floating_ip['floating_ip_address'],
-                                ssh_client=vm.ssh_client)
+        self.check_connectivity(ssh_client=vm.ssh_client)
         interface = self._get_primary_interface(vm.ssh_client)
 
         try:
diff --git a/neutron_tempest_plugin/scenario/test_trunk.py b/neutron_tempest_plugin/scenario/test_trunk.py
index 2ba8f13..47b8415 100644
--- a/neutron_tempest_plugin/scenario/test_trunk.py
+++ b/neutron_tempest_plugin/scenario/test_trunk.py
@@ -193,7 +193,6 @@
         self._wait_for_port(port=vm.port)
         self._wait_for_port(port=vm.subport)
         self.check_connectivity(
-            host=vm.floating_ip['floating_ip_address'],
             ssh_client=vm.ssh_client,
             servers=[vm.server])
 
diff --git a/neutron_tempest_plugin/scenario/test_vlan_transparency.py b/neutron_tempest_plugin/scenario/test_vlan_transparency.py
index d9a529c..11f12e9 100644
--- a/neutron_tempest_plugin/scenario/test_vlan_transparency.py
+++ b/neutron_tempest_plugin/scenario/test_vlan_transparency.py
@@ -138,9 +138,7 @@
             ssh_clients.append(
                 self._create_ssh_client(floating_ip=floating_ips[i]))
 
-            self.check_connectivity(
-                host=floating_ips[i]['floating_ip_address'],
-                ssh_client=ssh_clients[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,
diff --git a/zuul.d/master_jobs.yaml b/zuul.d/master_jobs.yaml
index 90379a1..3695565 100644
--- a/zuul.d/master_jobs.yaml
+++ b/zuul.d/master_jobs.yaml
@@ -194,6 +194,7 @@
               image_is_advanced: true
               available_type_drivers: flat,geneve,vlan,gre,local,vxlan
               provider_net_base_segm_id: 1
+              snat_rules_apply_to_nested_networks: true
     irrelevant-files:
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
@@ -285,6 +286,7 @@
             neutron_plugin_options:
               available_type_drivers: flat,vlan,local,vxlan
               firewall_driver: openvswitch
+              snat_rules_apply_to_nested_networks: true
     irrelevant-files:
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
@@ -400,6 +402,7 @@
             neutron_plugin_options:
               available_type_drivers: flat,vlan,local,vxlan
               firewall_driver: iptables_hybrid
+              snat_rules_apply_to_nested_networks: true
     irrelevant-files:
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
@@ -575,6 +578,7 @@
               available_type_drivers: flat,vlan,local,vxlan
               q_agent: linuxbridge
               firewall_driver: iptables
+              snat_rules_apply_to_nested_networks: true
     irrelevant-files:
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
@@ -715,6 +719,7 @@
               available_type_drivers: local,flat,vlan,geneve
               is_igmp_snooping_enabled: True
               firewall_driver: ovn
+              snat_rules_apply_to_nested_networks: false
       zuul_copy_output:
         '{{ devstack_base_dir }}/data/ovs': 'logs'
         '{{ devstack_base_dir }}/data/ovn': 'logs'
@@ -937,6 +942,7 @@
               available_type_drivers: flat,geneve,vlan,gre,local,vxlan
               l3_agent_mode: dvr_snat
               firewall_driver: openvswitch
+              snat_rules_apply_to_nested_networks: true
     group-vars:
       subnode:
         devstack_services:
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 9a20f80..35f3384 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -190,10 +190,7 @@
       - release-notes-jobs-python3
     check:
       jobs:
-        - neutron-tempest-plugin-sfc:
-            # Note(lajoskatona): make it voting when #2068727
-            # is fixed.
-            voting: false
+        - neutron-tempest-plugin-sfc
         - neutron-tempest-plugin-sfc-2023-1
         - neutron-tempest-plugin-sfc-2023-2
         - neutron-tempest-plugin-sfc-2024-1
@@ -221,6 +218,7 @@
 
     gate:
       jobs:
+        - neutron-tempest-plugin-sfc
         - neutron-tempest-plugin-bgpvpn-bagpipe
         - neutron-tempest-plugin-dynamic-routing
         - neutron-tempest-plugin-fwaas