Merge "Add common method for advanced image configuration"
diff --git a/neutron_tempest_plugin/common/ip.py b/neutron_tempest_plugin/common/ip.py
index bab9064..d5966d2 100644
--- a/neutron_tempest_plugin/common/ip.py
+++ b/neutron_tempest_plugin/common/ip.py
@@ -184,6 +184,11 @@
             if mac_address in nic_line:
                 return nic_line.split(":")[1].strip()
 
+    def has_dadfailed(self, device):
+        """Check if device has any IPv6 addresses in dadfailed state"""
+        output = self.execute('address', 'show', 'dev', device)
+        return 'dadfailed' in output
+
 
 def parse_addresses(command_output):
     address = device = None
diff --git a/neutron_tempest_plugin/config.py b/neutron_tempest_plugin/config.py
index 1f5c34c..98fd48e 100644
--- a/neutron_tempest_plugin/config.py
+++ b/neutron_tempest_plugin/config.py
@@ -185,7 +185,7 @@
 FwaasGroup = [
     cfg.StrOpt('driver',
                default=None,
-               choices=['openvswitch', 'ovn'],
+               choices=['iptables_v2', 'ovn'],
                help='Driver used by the FWaaS plugin.'),
 ]
 fwaas_group = cfg.OptGroup(
diff --git a/neutron_tempest_plugin/fwaas/scenario/test_fwaas_v2.py b/neutron_tempest_plugin/fwaas/scenario/test_fwaas_v2.py
index 6af27fc..c60f8b4 100644
--- a/neutron_tempest_plugin/fwaas/scenario/test_fwaas_v2.py
+++ b/neutron_tempest_plugin/fwaas/scenario/test_fwaas_v2.py
@@ -37,6 +37,12 @@
     - public_network_id
     """
 
+    @classmethod
+    def setup_credentials(cls):
+        # Create no default network resources; tests create their own topology.
+        cls.set_network_resources()
+        super(TestFWaaS_v2, cls).setup_credentials()
+
     def setUp(self):
         LOG.debug("Initializing FWaaSScenarioTest Setup")
         super(TestFWaaS_v2, self).setUp()
@@ -126,10 +132,15 @@
                         subnet_id=subnet_id)
         return resp
 
-    def _create_network_subnet(self):
-        network = self.create_network()
-        subnet_kwargs = dict(network=network)
-        subnet = self.create_subnet(**subnet_kwargs)
+    def _create_network_subnet(self, prefix="smoke-",
+                              port_security_enabled=True):
+        network_prefix = "network-%s" % prefix
+        subnet_prefix = "subnet-%s" % prefix
+        network = self.create_network(
+            namestart=network_prefix,
+            port_security_enabled=port_security_enabled)
+        subnet = self.create_subnet(
+            network=network, namestart=subnet_prefix)
         return network, subnet
 
     def _create_test_server(self, network, security_group):
@@ -310,3 +321,108 @@
 
         # Disassociate ports of this firewall group for cleanup resources
         self.update_firewall_group_and_wait(fw_group['id'], ports=[])
+
+    def _create_fip_topology(self):
+        """Create topology: network, subnet, router, VM with FIP.
+
+        VM is created without security group (network has
+        port_security_enabled=False).
+
+        +--------+             +-------------+
+        |"server"|             | "subnet"    |
+        |   VM   +-------------+ "network"   |
+        +--------+             +----+--------+
+                                    |
+                                    | router interface port
+                               +----+-----+
+                               | "router"|
+                               +----+-----+
+                                    |
+                                    | external gateway
+                                    |
+                               [external network]
+        """
+        # No security group: network has port_security_enabled=False
+        network, subnet = self._create_network_subnet(
+            prefix='fwaas-ssh-fip-',
+            port_security_enabled=False)
+        router = self._create_router(namestart='fwaas-ssh-fip-router')
+        pub_network_id = CONF.network.public_network_id
+        router = self.routers_client.update_router(
+            router['id'],
+            external_gateway_info=dict(network_id=pub_network_id))['router']
+        resp = self._add_router_interface(
+            router['id'], subnet_id=subnet['id'])
+        router_port_id = resp['port_id']
+        server, keys = self._create_server(network)
+        floating_ip = self.create_floating_ip(server, pub_network_id)
+
+        return {
+            'server': server,
+            'private_key': keys['private_key'],
+            'floating_ip': floating_ip,
+            'router_port_id': router_port_id,
+        }
+
+    @decorators.idempotent_id('a8c2e1f4-9b3d-4f5a-8e6c-7d9f2b1a0c3e')
+    def test_ssh_via_fip_with_fwaas_rules(self):
+        """Test SSH access to VM with FIP controlled by FWaaS rules.
+
+        Verifies that:
+        1. FWaaS is enabled (skipped in setUp if not)
+        2. Baseline: VM reachable before firewall (SSH works)
+        3. Firewall group with empty policy denies all traffic (SSH blocked)
+        4. Adding allow SSH rules to ingress and egress policy permits SSH
+        """
+        topology = self._create_fip_topology()
+        fip_address = topology['floating_ip']['floating_ip_address']
+        ssh_login = CONF.validation.image_ssh_user
+        private_key = topology['private_key']
+
+        # Baseline: Ensure VM is reachable before applying firewall
+        self.check_vm_connectivity(
+            ip_address=fip_address,
+            username=ssh_login,
+            private_key=private_key)
+
+        # Phase 1: Attach firewall group with empty policy - SSH blocked
+        fw_policy = self.create_firewall_policy()
+        fw_group = self.create_firewall_group(
+            ports=[topology['router_port_id']],
+            ingress_firewall_policy_id=fw_policy['id'],
+            egress_firewall_policy_id=fw_policy['id'])
+        self.addCleanup(self.update_firewall_group_and_wait, fw_group['id'],
+                        ports=[])
+        self._wait_firewall_group_ready(fw_group['id'])
+        LOG.debug('Firewall group with empty policy attached to router port')
+
+        self.check_connectivity(
+            ip_address=fip_address,
+            username=ssh_login,
+            private_key=private_key,
+            should_connect=False,
+            check_icmp=False,
+            check_ssh=True)
+
+        # Phase 2: Add allow SSH rules - ingress dport 22, egress sport 22
+        fw_allow_ssh_rule = self.create_firewall_rule(
+            action="allow", protocol="tcp", destination_port=22)
+        fw_allow_egress_ssh_rule = self.create_firewall_rule(
+            action="allow", protocol="tcp", source_port=22)
+        self.insert_firewall_rule_in_policy_and_wait(
+            firewall_group_id=fw_group['id'],
+            firewall_rule_id=fw_allow_ssh_rule['id'],
+            firewall_policy_id=fw_policy['id'])
+        self.insert_firewall_rule_in_policy_and_wait(
+            firewall_group_id=fw_group['id'],
+            firewall_rule_id=fw_allow_egress_ssh_rule['id'],
+            firewall_policy_id=fw_policy['id'])
+        LOG.debug('Added allow SSH rules to ingress and egress policy')
+
+        self.check_connectivity(
+            ip_address=fip_address,
+            username=ssh_login,
+            private_key=private_key,
+            should_connect=True,
+            check_icmp=False,
+            check_ssh=True)
diff --git a/neutron_tempest_plugin/scenario/admin/test_floatingip.py b/neutron_tempest_plugin/scenario/admin/test_floatingip.py
index d9abaf5..52218ea 100644
--- a/neutron_tempest_plugin/scenario/admin/test_floatingip.py
+++ b/neutron_tempest_plugin/scenario/admin/test_floatingip.py
@@ -57,14 +57,31 @@
             secgroup_id=cls.secgroup['id'],
             client=network_client),
 
-    def _list_hypervisors(self):
-        # List of hypervisors
-        return self.os_admin.hv_client.list_hypervisors()['hypervisors']
-
-    def _list_availability_zones(self):
-        # List of availability zones
-        func = self.os_admin.az_client.list_availability_zones
-        return func()['availabilityZoneInfo']
+    def _choose_az_and_node(self):
+        az_list = self.os_admin.az_client.list_availability_zones(
+            detail=True)['availabilityZoneInfo']
+        hv_list = self.os_admin.hv_client.list_hypervisors()['hypervisors']
+        for az in az_list:
+            if not az['zoneState']['available']:
+                continue
+            for host, services in az['hosts'].items():
+                for service, info in services.items():
+                    if (
+                        service == 'nova-compute' and
+                        info['active'] and info['available']
+                    ):
+                        hv = [
+                            h for h in hv_list
+                            if (
+                                h['hypervisor_hostname'].startswith(host) and
+                                h["state"] == "up" and
+                                h["status"] == "enabled"
+                            )
+                        ]
+                        if not hv:
+                            continue
+                        return az['zoneName'], hv[0]['hypervisor_hostname']
+        return None, None
 
     def _create_vms(self, hyper, avail_zone, num_servers=2):
         servers, fips, server_ssh_clients = ([], [], [])
@@ -103,10 +120,9 @@
         that were created in the same compute node and same availability zone
         to reach each other.
         """
-        # Get hypervisor list to pass it for vm creation
-        hyper = self._list_hypervisors()[0]['hypervisor_hostname']
-        # Get availability zone list to pass it for vm creation
-        avail_zone = self._list_availability_zones()[0]['zoneName']
+        avail_zone, hyper = self._choose_az_and_node()
+        if not (avail_zone and hyper):
+            self.fail("No compute host is available")
         servers, server_ssh_clients, fips = self._create_vms(hyper, avail_zone)
         self.check_remote_connectivity(
             server_ssh_clients[0], fips[1]['floating_ip_address'],
diff --git a/neutron_tempest_plugin/scenario/base.py b/neutron_tempest_plugin/scenario/base.py
index 36d15d8..1be8afe 100644
--- a/neutron_tempest_plugin/scenario/base.py
+++ b/neutron_tempest_plugin/scenario/base.py
@@ -686,7 +686,7 @@
             body = client.show_router(router_id)
             return body['router']
         elif network_id:
-            router = self.create_router_by_client()
+            router = self.create_router_by_client(tenant_id=tenant_id)
             self.addCleanup(test_utils.call_and_ignore_notfound_exc,
                             client.delete_router, router['id'])
             kwargs = {'external_gateway_info': dict(network_id=network_id)}
diff --git a/neutron_tempest_plugin/scenario/test_dns_integration.py b/neutron_tempest_plugin/scenario/test_dns_integration.py
index d6bafef..2784dc1 100644
--- a/neutron_tempest_plugin/scenario/test_dns_integration.py
+++ b/neutron_tempest_plugin/scenario/test_dns_integration.py
@@ -13,10 +13,9 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import importlib
 import ipaddress
 
-import testtools
-
 from oslo_log import log
 from tempest.common import utils
 from tempest.common import waiters
@@ -37,9 +36,21 @@
 
 # Note(jh): Need to do a bit of juggling here in order to avoid failures
 # when designate_tempest_plugin is not available
-dns_base = testtools.try_import('designate_tempest_plugin.tests.base')
-dns_waiters = testtools.try_import('designate_tempest_plugin.common.waiters')
-dns_data_utils = testtools.try_import('designate_tempest_plugin.data_utils')
+try:
+    dns_base = importlib.import_module('designate_tempest_plugin.tests.base')
+except ImportError:
+    dns_base = None
+try:
+    dns_waiters = importlib.import_module(
+        'designate_tempest_plugin.common.waiters')
+except ImportError:
+    dns_waiters = None
+try:
+    dns_data_utils = importlib.import_module(
+        'designate_tempest_plugin.data_utils')
+except ImportError:
+    dns_data_utils = None
+
 
 if dns_base:
     DNSMixin = dns_base.BaseDnsV2Test
diff --git a/neutron_tempest_plugin/scenario/test_ipv6.py b/neutron_tempest_plugin/scenario/test_ipv6.py
index 41ac2e6..3239417 100644
--- a/neutron_tempest_plugin/scenario/test_ipv6.py
+++ b/neutron_tempest_plugin/scenario/test_ipv6.py
@@ -33,21 +33,21 @@
 LOG = log.getLogger(__name__)
 
 
-def turn_nic6_on(ssh, ipv6_port, config_nic=True):
+def turn_nic6_on(ssh, nic, config_nic=True):
     """Turns the IPv6 vNIC on
 
     Required because guest images usually set only the first vNIC on boot.
-    Searches for the IPv6 vNIC's MAC and brings it up.
+    Brings the specified NIC up.
     # NOTE(slaweq): on RHEL based OS ifcfg file for new interface is
     # needed to make IPv6 working on it, so if
     # /etc/sysconfig/network-scripts directory exists ifcfg-%(nic)s file
     # should be added in it
 
     @param ssh: RemoteClient ssh instance to server
-    @param ipv6_port: port from IPv6 network attached to the server
+    @param nic: network interface name
+    @param config_nic: whether to configure with NetworkManager
     """
     ip_command = ip.IPCommand(ssh)
-    nic = ip_command.get_nic_name_by_mac(ipv6_port['mac_address'])
 
     if config_nic:
         try:
@@ -150,16 +150,26 @@
     def _test_ipv6_address_configured(self, ssh_client, vm, ipv6_port):
         ipv6_address = ipv6_port['fixed_ips'][0]['ip_address']
         ip_command = ip.IPCommand(ssh_client)
+        nic = ip_command.get_nic_name_by_mac(ipv6_port['mac_address'])
 
         def guest_has_address(expected_address):
             ip_addresses = [a.address for a in ip_command.list_addresses()]
             for ip_address in ip_addresses:
                 if expected_address in ip_address:
                     return True
+
+            # NOTE(ykarel): Sometimes with the cirros VM a race is seen with
+            # ovs_create_tap feature https://launchpad.net/bugs/2069718 and
+            # ipv6 is not configured, adding nic restart if dad failure is
+            # detected to workaround this
+            if ip_command.has_dadfailed(nic):
+                LOG.debug('DAD failure detected on %s, restarting', nic)
+                ip_command.set_link(nic, "down")
+                ip_command.set_link(nic, "up")
             return False
         # Set NIC with IPv6 to be UP and wait until IPv6 address
         # will be configured on this NIC
-        turn_nic6_on(ssh_client, ipv6_port, False)
+        turn_nic6_on(ssh_client, nic, False)
         # And check if IPv6 address will be properly configured
         # on this NIC
         try:
@@ -179,7 +189,7 @@
             try:
                 # Set NIC with IPv6 to be UP and wait until IPv6 address
                 # will be configured on this NIC
-                turn_nic6_on(ssh_client, ipv6_port)
+                turn_nic6_on(ssh_client, nic)
                 # And check if IPv6 address will be properly configured
                 # on this NIC
                 utils.wait_until_true(
diff --git a/neutron_tempest_plugin/tap_as_a_service/scenario/test_tap_mirror.py b/neutron_tempest_plugin/tap_as_a_service/scenario/test_tap_mirror.py
index d4a482c..31e5aa7 100644
--- a/neutron_tempest_plugin/tap_as_a_service/scenario/test_tap_mirror.py
+++ b/neutron_tempest_plugin/tap_as_a_service/scenario/test_tap_mirror.py
@@ -111,6 +111,9 @@
         self.monitor_client.validate_authentication()
 
         r_ip = vm_mon_fip['floating_ip_address']
+        directions = {'IN': '101', 'OUT': '102'}
+        if utils.is_extension_enabled('tap-mirror-both-direction', 'network'):
+            directions['BOTH'] = '103'
         # Create GRE mirror, as tcpdump cant extract ERSPAN
         # it is just visible as a type of GRE traffic.
         # direction IN and that the test pings from vm0 to vm1
@@ -119,7 +122,7 @@
         tap_mirror = self.tap_mirrors_client.create_tap_mirror(
             name=data_utils.rand_name("tap_mirror"),
             port_id=vm1_port['id'],
-            directions={'IN': '101', 'OUT': '102'},
+            directions=directions,
             remote_ip=r_ip,
             mirror_type='gre',
         )
@@ -139,3 +142,54 @@
         self.assertIn('key=0x65', output)
         # GRE Key for Direction OUT:102
         self.assertIn('key=0x66', output)
+        if 'BOTH' in directions:
+            # GRE Key for Direction BOTH:103
+            self.assertIn('key=0x67', output)
+
+        vm0_ip = vm0_port['fixed_ips'][0]['ip_address']
+        output_lines = output.splitlines()
+
+        self._check_icmp_mirror_direction(output_lines, vm0_ip, vm1_ip, "IN",
+                                          "key=0x65")
+        self._check_icmp_mirror_direction(output_lines, vm0_ip, vm1_ip, "OUT",
+                                          "key=0x66")
+        if 'BOTH' in directions:
+            self._check_icmp_mirror_direction(output_lines, vm0_ip, vm1_ip,
+                                              "BOTH", "key=0x67")
+
+    def _check_icmp_mirror_direction(self, output_lines, ip_sender,
+                                     ip_receiver, direction, key):
+        """Check direction of the mirroring is consistent with what is expected
+
+        output_lines[i+2] should have the following format for OUT:
+        [ip_receiver] > [ip_sender]: ICMP echo reply, id ...
+
+        output_lines[i+2] should have the following format for IN:
+        [ip_sender] > [ip_receiver]: ICMP echo request, id ...
+
+        BOTH direction should have at least one iteration of each.
+        """
+
+        directions = [direction] if direction != "BOTH" else ['IN', 'OUT']
+        for d in directions:
+            found_log = False
+            if d == 'IN':
+                left_ip, right_ip = ip_sender, ip_receiver
+                icmp_msg = 'ICMP echo request'
+            elif d == 'OUT':
+                left_ip, right_ip = ip_receiver, ip_sender
+                icmp_msg = 'ICMP echo reply'
+            for i, line in enumerate(output_lines):
+                icmp_log = None
+                if key not in line:
+                    continue
+                icmp_log = output_lines[i + 2].split(':')
+                if icmp_msg in icmp_log[1]:
+                    self.assertIn(left_ip + ' > ' + right_ip, icmp_log[0])
+                    found_log = True
+                    break
+            # Make sure we have found at least one coincidence of the target
+            # string for this direction.
+            self.assertTrue(found_log, msg=f"Did not find direction "
+                f"{direction} and key {key} in the tcpdump log. ICMP "
+                f"log: {icmp_log}")
diff --git a/requirements.txt b/requirements.txt
index eee78f8..7c8b715 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,5 +11,5 @@
 tempest>=29.2.0 # Apache-2.0
 tenacity>=3.2.1 # Apache-2.0
 ddt>=1.0.1 # MIT
-testtools>=2.2.0 # MIT
+testtools>=2.8.4 # MIT
 debtcollector>=1.2.0 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index 0962924..d35b58a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,7 +6,7 @@
 author = OpenStack
 author_email = openstack-discuss@lists.openstack.org
 home_page = https://opendev.org/openstack/neutron-tempest-plugin
-python_requires = >=3.9
+python_requires = >=3.10
 classifier =
     Environment :: OpenStack
     Intended Audience :: Information Technology
@@ -15,10 +15,11 @@
     Operating System :: POSIX :: Linux
     Programming Language :: Python
     Programming Language :: Python :: 3
-    Programming Language :: Python :: 3.9
     Programming Language :: Python :: 3.10
     Programming Language :: Python :: 3.11
     Programming Language :: Python :: 3.12
+    Programming Language :: Python :: 3.13
+    Programming Language :: Python :: 3 :: Only
 
 [files]
 packages =
diff --git a/test-requirements.txt b/test-requirements.txt
index ee399b8..d23827a 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,6 +1,6 @@
 hacking>=6.1.0,<6.2.0 # Apache-2.0
 
-flake8-import-order>=0.18.0,<0.19.0 # LGPLv3
+flake8-import-order>=0.19.0 # LGPLv3
 oslotest>=3.2.0 # Apache-2.0
 stestr>=1.0.0 # Apache-2.0
 testtools>=2.2.0 # MIT
diff --git a/zuul.d/2024_1_jobs.yaml b/zuul.d/2024_1_jobs.yaml
index 1e313fa..fcaf6dc 100644
--- a/zuul.d/2024_1_jobs.yaml
+++ b/zuul.d/2024_1_jobs.yaml
@@ -2,17 +2,17 @@
     name: neutron-tempest-plugin-openvswitch-2024-1
     parent: neutron-tempest-plugin-openvswitch
     nodeset: neutron-nested-virt-ubuntu-jammy
+    override-checkout: unmaintained/2024.1
     required-projects: &required-projects-2024-1
       - openstack/neutron
       - name: openstack/neutron-tempest-plugin
-        override-checkout: 2024.1-last
+        override-checkout: 2.12.0
       - openstack/tempest
     vars:
       network_api_extensions_openvswitch: &api_extensions_openvswitch
         - dhcp_agent_scheduler
         - local_ip
         - qos-bw-minimum-ingress
-      stable_constraints_file: "https://releases.openstack.org/constraints/upper/master"
       tempest_test_regex: "\
           (^neutron_tempest_plugin.api)|\
           (^neutron_tempest_plugin.scenario)|\
@@ -104,7 +104,6 @@
       devstack_localrc:
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_openvswitch) | join(',') }}"
         NEUTRON_DEPLOY_MOD_WSGI: false
-        TEMPEST_VENV_UPPER_CONSTRAINTS: master
       devstack_local_conf:
         test-config:
           $TEMPEST_CONFIG:
@@ -120,7 +119,6 @@
       network_api_extensions_common: *api_extensions
       network_api_extensions_openvswitch: *api_extensions_openvswitch
       network_available_features: *available_features
-      stable_constraints_file: "https://releases.openstack.org/constraints/upper/master"
       tempest_test_regex: "\
           (^neutron_tempest_plugin.api)|\
           (^neutron_tempest_plugin.scenario)|\
@@ -137,7 +135,6 @@
       devstack_localrc:
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_openvswitch) | join(',') }}"
         NEUTRON_DEPLOY_MOD_WSGI: false
-        TEMPEST_VENV_UPPER_CONSTRAINTS: master
       devstack_local_conf:
         test-config:
           $TEMPEST_CONFIG:
@@ -175,7 +172,6 @@
         - dhcp_agent_scheduler
         - vlan-transparent
       network_available_features: *available_features
-      stable_constraints_file: "https://releases.openstack.org/constraints/upper/master"
       tempest_test_regex: "\
           (^neutron_tempest_plugin.api)|\
           (^neutron_tempest_plugin.scenario)|\
@@ -198,7 +194,6 @@
       devstack_localrc:
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_linuxbridge) | join(',') }}"
         NEUTRON_DEPLOY_MOD_WSGI: false
-        TEMPEST_VENV_UPPER_CONSTRAINTS: master
       devstack_local_conf:
         test-config:
           $TEMPEST_CONFIG:
@@ -217,7 +212,6 @@
     vars:
       network_api_extensions_ovn:
         - vlan-transparent
-      stable_constraints_file: "https://releases.openstack.org/constraints/upper/master"
       tempest_test_regex: "\
           (^neutron_tempest_plugin.api)|\
           (^neutron_tempest_plugin.scenario)|\
@@ -226,7 +220,6 @@
       devstack_localrc:
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_ovn) | join(',') }}"
         NEUTRON_DEPLOY_MOD_WSGI: false
-        TEMPEST_VENV_UPPER_CONSTRAINTS: master
       devstack_local_conf:
         test-config:
           $TEMPEST_CONFIG:
@@ -247,11 +240,9 @@
       network_api_extensions_dvr:
         - dhcp_agent_scheduler
         - dvr
-      stable_constraints_file: "https://releases.openstack.org/constraints/upper/master"
       devstack_localrc:
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_dvr) | join(',') }}"
         NEUTRON_DEPLOY_MOD_WSGI: false
-        TEMPEST_VENV_UPPER_CONSTRAINTS: master
 
 - job:
     name: neutron-tempest-plugin-designate-scenario-2024-1
@@ -260,10 +251,8 @@
     required-projects: *required-projects-2024-1
     vars:
       network_api_extensions_common: *api_extensions
-      stable_constraints_file: "https://releases.openstack.org/constraints/upper/master"
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
-        TEMPEST_VENV_UPPER_CONSTRAINTS: master
 
 - job:
     name: neutron-tempest-plugin-sfc-2024-1
@@ -271,10 +260,8 @@
     nodeset: openstack-single-node-jammy
     required-projects: *required-projects-2024-1
     vars:
-      stable_constraints_file: "https://releases.openstack.org/constraints/upper/master"
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
-        TEMPEST_VENV_UPPER_CONSTRAINTS: master
 
 - job:
     name: neutron-tempest-plugin-bgpvpn-bagpipe-2024-1
@@ -282,10 +269,8 @@
     nodeset: openstack-single-node-jammy
     required-projects: *required-projects-2024-1
     vars:
-      stable_constraints_file: "https://releases.openstack.org/constraints/upper/master"
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
-        TEMPEST_VENV_UPPER_CONSTRAINTS: master
 
 - job:
     name: neutron-tempest-plugin-dynamic-routing-2024-1
@@ -294,10 +279,8 @@
     nodeset: openstack-single-node-jammy
     required-projects: *required-projects-2024-1
     vars:
-      stable_constraints_file: "https://releases.openstack.org/constraints/upper/master"
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
-        TEMPEST_VENV_UPPER_CONSTRAINTS: master
 
 - job:
     name: neutron-tempest-plugin-fwaas-2024-1
@@ -305,10 +288,8 @@
     nodeset: openstack-single-node-jammy
     required-projects: *required-projects-2024-1
     vars:
-      stable_constraints_file: "https://releases.openstack.org/constraints/upper/master"
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
-        TEMPEST_VENV_UPPER_CONSTRAINTS: master
 
 - job:
     name: neutron-tempest-plugin-vpnaas-2024-1
@@ -316,10 +297,8 @@
     nodeset: openstack-single-node-jammy
     required-projects: *required-projects-2024-1
     vars:
-      stable_constraints_file: "https://releases.openstack.org/constraints/upper/master"
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
-        TEMPEST_VENV_UPPER_CONSTRAINTS: master
 
 - job:
     name: neutron-tempest-plugin-tap-as-a-service-2024-1
@@ -327,11 +306,9 @@
     nodeset: openstack-single-node-jammy
     required-projects: *required-projects-2024-1
     vars:
-      stable_constraints_file: "https://releases.openstack.org/constraints/upper/master"
       network_api_extensions_common: *api_extensions
       network_api_extensions_tempest:
         - taas
         - taas-vlan-filter
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
-        TEMPEST_VENV_UPPER_CONSTRAINTS: master
diff --git a/zuul.d/2025_1_jobs.yaml b/zuul.d/2025_1_jobs.yaml
index 793d8e3..7b295e4 100644
--- a/zuul.d/2025_1_jobs.yaml
+++ b/zuul.d/2025_1_jobs.yaml
@@ -259,3 +259,6 @@
     parent: neutron-tempest-plugin-tap-as-a-service-ovn
     nodeset: openstack-single-node-noble
     override-checkout: stable/2025.1
+    vars:
+      network_api_extensions_tempest:
+        - tap-mirror
diff --git a/zuul.d/2025_2_jobs.yaml b/zuul.d/2025_2_jobs.yaml
index 516e993..839f9c3 100644
--- a/zuul.d/2025_2_jobs.yaml
+++ b/zuul.d/2025_2_jobs.yaml
@@ -237,3 +237,6 @@
     parent: neutron-tempest-plugin-tap-as-a-service-ovn
     nodeset: openstack-single-node-noble
     override-checkout: stable/2025.2
+    vars:
+      network_api_extensions_tempest:
+        - tap-mirror
diff --git a/zuul.d/2026_1_jobs.yaml b/zuul.d/2026_1_jobs.yaml
new file mode 100644
index 0000000..f2d3c7f
--- /dev/null
+++ b/zuul.d/2026_1_jobs.yaml
@@ -0,0 +1,242 @@
+- job:
+    name: neutron-tempest-plugin-openvswitch-2026-1
+    parent: neutron-tempest-plugin-openvswitch
+    nodeset: neutron-nested-virt-ubuntu-noble
+    override-checkout: stable/2026.1
+    vars:
+      network_api_extensions_openvswitch: &api_extensions_openvswitch
+        - dhcp_agent_scheduler
+        - local_ip
+        - qos-bw-minimum-ingress
+      tempest_test_regex: "\
+          (^neutron_tempest_plugin.api)|\
+          (^neutron_tempest_plugin.scenario)|\
+          (^tempest.api.compute.servers.test_attach_interfaces)|\
+          (^tempest.api.compute.servers.test_multiple_create)"
+      network_available_features: &available_features
+        - ipv6_metadata
+      network_api_extensions_common: &api_extensions
+        - address-group
+        - address-scope
+        - agent
+        - allowed-address-pairs
+        - auto-allocated-topology
+        - availability_zone
+        - binding
+        - default-subnetpools
+        - dns-domain-ports
+        - dns-integration
+        - dns-integration-domain-keywords
+        - empty-string-filtering
+        - expose-port-forwarding-in-fip
+        - expose-l3-conntrack-helper
+        - ext-gw-mode
+        - external-net
+        - extra_dhcp_opt
+        - extraroute
+        - extraroute-atomic
+        - filter-validation
+        - fip-port-details
+        - flavors
+        - floating-ip-port-forwarding
+        - floating-ip-port-forwarding-detail
+        - floatingip-pools
+        - ip-substring-filtering
+        - l3-conntrack-helper
+        - l3-ext-ndp-proxy
+        - l3-flavors
+        - l3-ha
+        - l3-ndp-proxy
+        - l3_agent_scheduler
+        - metering
+        - multi-provider
+        - net-mtu
+        - net-mtu-writable
+        - network-ip-availability
+        - network_availability_zone
+        - network-segment-range
+        - pagination
+        - port-device-profile
+        - port-mac-address-regenerate
+        - port-trusted-vif
+        - port-resource-request
+        - port-resource-request-groups
+        - port-security
+        - port-security-groups-filtering
+        - project-id
+        - provider
+        - qos
+        - qos-fip
+        - quotas
+        - quota_details
+        - rbac-address-group
+        - rbac-address-scope
+        - rbac-policies
+        - rbac-security-groups
+        - rbac-subnetpool
+        - router
+        - router_availability_zone
+        - security-group
+        - security-groups-default-rules
+        - security-groups-normalized-cidr
+        - security-groups-remote-address-group
+        - segment
+        - service-type
+        - sorting
+        - standard-attr-description
+        - standard-attr-revisions
+        - standard-attr-segment
+        - standard-attr-tag
+        - standard-attr-timestamp
+        - stateful-security-group
+        - subnet_allocation
+        - subnet-dns-publish-fixed-ip
+        - subnet-service-types
+        - subnetpool-prefix-ops
+        - tag-ports-during-bulk-creation
+        - trunk
+        - trunk-details
+        - uplink-status-propagation
+        - uplink-status-propagation-updatable
+      devstack_localrc:
+        NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_openvswitch) | join(',') }}"
+      devstack_local_conf:
+        test-config:
+          $TEMPEST_CONFIG:
+            network-feature-enabled:
+              available_features: "{{ network_available_features | join(',') }}"
+
+- job:
+    name: neutron-tempest-plugin-openvswitch-iptables_hybrid-2026-1
+    parent: neutron-tempest-plugin-openvswitch-iptables_hybrid
+    nodeset: neutron-nested-virt-ubuntu-noble
+    override-checkout: stable/2026.1
+    vars:
+      network_api_extensions_common: *api_extensions
+      network_api_extensions_openvswitch: *api_extensions_openvswitch
+      network_available_features: *available_features
+      tempest_test_regex: "\
+          (^neutron_tempest_plugin.api)|\
+          (^neutron_tempest_plugin.scenario)|\
+          (^tempest.api.compute.servers.test_attach_interfaces)|\
+          (^tempest.api.compute.servers.test_multiple_create)"
+      # TODO(slaweq): remove trunks subport_connectivity test from blacklist
+      # when bug https://bugs.launchpad.net/neutron/+bug/1838760 will be fixed
+      # TODO(akatz): remove established tcp session verification test when the
+      # bug https://bugzilla.redhat.com/show_bug.cgi?id=1965036 will be fixed
+      tempest_exclude_regex: "\
+          (^neutron_tempest_plugin.scenario.test_trunk.TrunkTest.test_subport_connectivity)|\
+          (^neutron_tempest_plugin.scenario.test_security_groups.StatefulNetworkSecGroupTest.test_established_tcp_session_after_re_attachinging_sg)|\
+          (^neutron_tempest_plugin.scenario.test_security_groups.StatelessNetworkSecGroupTest.test_established_tcp_session_after_re_attachinging_sg)"
+      devstack_localrc:
+        NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_openvswitch) | join(',') }}"
+      devstack_local_conf:
+        test-config:
+          $TEMPEST_CONFIG:
+            network-feature-enabled:
+              available_features: "{{ network_available_features | join(',') }}"
+            neutron_plugin_options:
+              available_type_drivers: flat,vlan,local,vxlan
+              firewall_driver: iptables_hybrid
+
+- job:
+    name: neutron-tempest-plugin-ovn-enforce-scope-old-defaults-2026-1
+    parent: neutron-tempest-plugin-ovn-2026-1
+    nodeset: neutron-nested-virt-ubuntu-noble
+    override-checkout: stable/2026.1
+    vars:
+      devstack_localrc:
+        NEUTRON_ENFORCE_SCOPE: false
+
+- job:
+    name: neutron-tempest-plugin-ovn-2026-1
+    parent: neutron-tempest-plugin-ovn
+    nodeset: neutron-nested-virt-ubuntu-noble
+    override-checkout: stable/2026.1
+    vars:
+      network_api_extensions_ovn:
+        - vlan-transparent
+        - qinq
+        - external-gateway-multihoming
+      tempest_test_regex: "\
+          (^neutron_tempest_plugin.api)|\
+          (^neutron_tempest_plugin.scenario)|\
+          (^tempest.api.compute.servers.test_attach_interfaces)|\
+          (^tempest.api.compute.servers.test_multiple_create)"
+      devstack_localrc:
+        NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_ovn) | join(',') }}"
+      devstack_local_conf:
+        test-config:
+          $TEMPEST_CONFIG:
+            network-feature-enabled:
+              available_features: ""
+            neutron_plugin_options:
+              available_type_drivers: local,flat,vlan,geneve
+              is_igmp_snooping_enabled: True
+              firewall_driver: ovn
+
+- job:
+    name: neutron-tempest-plugin-dvr-multinode-scenario-2026-1
+    parent: neutron-tempest-plugin-dvr-multinode-scenario
+    nodeset: openstack-two-node-noble
+    override-checkout: stable/2026.1
+    vars:
+      network_api_extensions_common: *api_extensions
+      network_api_extensions_dvr:
+        - dhcp_agent_scheduler
+        - dvr
+      devstack_localrc:
+        NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_dvr) | join(',') }}"
+
+- job:
+    name: neutron-tempest-plugin-designate-scenario-2026-1
+    parent: neutron-tempest-plugin-designate-scenario
+    nodeset: neutron-nested-virt-ubuntu-noble
+    override-checkout: stable/2026.1
+    vars:
+      network_api_extensions_common: *api_extensions
+
+- job:
+    name: neutron-tempest-plugin-sfc-2026-1
+    parent: neutron-tempest-plugin-sfc
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2026.1
+
+- job:
+    name: neutron-tempest-plugin-bgpvpn-bagpipe-2026-1
+    parent: neutron-tempest-plugin-bgpvpn-bagpipe
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2026.1
+
+- job:
+    name: neutron-tempest-plugin-dynamic-routing-2026-1
+    parent: neutron-tempest-plugin-dynamic-routing
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2026.1
+
+- job:
+    name: neutron-tempest-plugin-fwaas-2026-1
+    parent: neutron-tempest-plugin-fwaas-openvswitch
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2026.1
+
+- job:
+    name: neutron-tempest-plugin-vpnaas-2026-1
+    parent: neutron-tempest-plugin-vpnaas
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2026.1
+
+- job:
+    name: neutron-tempest-plugin-tap-as-a-service-2026-1
+    parent: neutron-tempest-plugin-tap-as-a-service
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2026.1
+
+- job:
+    name: neutron-tempest-plugin-tap-as-a-service-ovn-2026-1
+    parent: neutron-tempest-plugin-tap-as-a-service-ovn
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2026.1
+    vars:
+      network_api_extensions_tempest:
+        - tap-mirror
diff --git a/zuul.d/master_jobs.yaml b/zuul.d/master_jobs.yaml
index 62e76d7..28cfb11 100644
--- a/zuul.d/master_jobs.yaml
+++ b/zuul.d/master_jobs.yaml
@@ -1369,12 +1369,11 @@
         q-meta: true
         q-metering: true
         q-l3: true
-        neutron-log: false
       devstack_local_conf:
         test-config:
           $TEMPEST_CONFIG:
             fwaas:
-              driver: openvswitch
+              driver: iptables_v2
     irrelevant-files: *fwaas_irrelevant_files
 
 - job:
@@ -1534,6 +1533,7 @@
       tempest_test_regex: ^neutron_tempest_plugin\.tap_as_a_service
       tox_envlist: all
       network_api_extensions_tempest:
+        # taas extension (tap-service and tap-flow) is only supported in ML2/OVS
         - taas
         - taas-vlan-filter
         - tap-mirror
@@ -1651,8 +1651,8 @@
       tempest_test_regex: ^neutron_tempest_plugin\.tap_as_a_service
       tox_envlist: all
       network_api_extensions_tempest:
-        - taas
         - tap-mirror
+        - tap-mirror-both-direction
       devstack_localrc:
         Q_AGENT: ovn
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_tempest) | join(',') }}"
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 5841ee5..e905c71 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -210,6 +210,23 @@
       jobs:
         - neutron-tempest-plugin-dvr-multinode-scenario-2025-2
 
+- project-template:
+    name: neutron-tempest-plugin-jobs-2026-1
+    check:
+      jobs:
+        - neutron-tempest-plugin-openvswitch-2026-1
+        - neutron-tempest-plugin-openvswitch-iptables_hybrid-2026-1
+        - neutron-tempest-plugin-ovn-2026-1
+        - neutron-tempest-plugin-designate-scenario-2026-1
+    gate:
+      jobs:
+        - neutron-tempest-plugin-ovn-2026-1
+    #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-dvr-multinode-scenario-2026-1
+
 - project:
     templates:
       - build-openstack-docs-pti
@@ -217,6 +234,7 @@
       - neutron-tempest-plugin-jobs-2024-2
       - neutron-tempest-plugin-jobs-2025-1
       - neutron-tempest-plugin-jobs-2025-2
+      - neutron-tempest-plugin-jobs-2026-1
       - check-requirements
       - tempest-plugin-jobs
       - release-notes-jobs-python3
@@ -226,38 +244,43 @@
         - neutron-tempest-plugin-sfc-2024-2
         - neutron-tempest-plugin-sfc-2025-1
         - neutron-tempest-plugin-sfc-2025-2
+        - neutron-tempest-plugin-sfc-2026-1
         - neutron-tempest-plugin-bgpvpn-bagpipe
         - neutron-tempest-plugin-bgpvpn-bagpipe-2024-2
         - neutron-tempest-plugin-bgpvpn-bagpipe-2025-1
         - neutron-tempest-plugin-bgpvpn-bagpipe-2025-2
-        - neutron-tempest-plugin-dynamic-routing:
-            voting: false  # See LP#2130631
+        - neutron-tempest-plugin-bgpvpn-bagpipe-2026-1
+        - neutron-tempest-plugin-dynamic-routing
         - neutron-tempest-plugin-dynamic-routing-2024-2
         - neutron-tempest-plugin-dynamic-routing-2025-1
         - neutron-tempest-plugin-dynamic-routing-2025-2
+        - neutron-tempest-plugin-dynamic-routing-2026-1
         - neutron-tempest-plugin-fwaas-ovn
         - neutron-tempest-plugin-fwaas-openvswitch
         - neutron-tempest-plugin-fwaas-2024-2
         - neutron-tempest-plugin-fwaas-2025-1
         - neutron-tempest-plugin-fwaas-2025-2
+        - neutron-tempest-plugin-fwaas-2026-1
         - neutron-tempest-plugin-vpnaas
         - neutron-tempest-plugin-vpnaas-ovn
         - neutron-tempest-plugin-vpnaas-2024-2
         - neutron-tempest-plugin-vpnaas-2025-1
         - neutron-tempest-plugin-vpnaas-2025-2
+        - neutron-tempest-plugin-vpnaas-2026-1
         - neutron-tempest-plugin-tap-as-a-service
         - neutron-tempest-plugin-tap-as-a-service-ovn
         - neutron-tempest-plugin-tap-as-a-service-2024-2
         - neutron-tempest-plugin-tap-as-a-service-2025-1
         - neutron-tempest-plugin-tap-as-a-service-2025-2
+        - neutron-tempest-plugin-tap-as-a-service-2026-1
         - neutron-tempest-plugin-tap-as-a-service-ovn-2025-1
         - neutron-tempest-plugin-tap-as-a-service-ovn-2025-2
+        - neutron-tempest-plugin-tap-as-a-service-ovn-2026-1
 
     gate:
       jobs:
         - neutron-tempest-plugin-sfc
         - neutron-tempest-plugin-bgpvpn-bagpipe
-        - neutron-tempest-plugin-dynamic-routing:
-            voting: false  # See LP#2130631
+        - neutron-tempest-plugin-dynamic-routing
         - neutron-tempest-plugin-fwaas-ovn
         - neutron-tempest-plugin-vpnaas-ovn