Merge "Remove explicit adding QoS policy  to the cleianup"
diff --git a/.coveragerc b/.coveragerc
deleted file mode 100644
index 5d5b32c..0000000
--- a/.coveragerc
+++ /dev/null
@@ -1,6 +0,0 @@
-[run]
-branch = True
-source = neutron_tempest_plugin
-
-[report]
-ignore_errors = True
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2ff8c11..e675c10 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -31,3 +31,9 @@
         entry: flake8
         files: '^.*\.py$'
         exclude: '^(doc|releasenotes|tools)/.*$'
+  - repo: https://opendev.org/openstack/hacking
+    rev: 7.0.0
+    hooks:
+      - id: hacking
+        additional_dependencies: ['neutron-lib']
+        exclude: '^(doc|releasenotes|tools)/.*$'
diff --git a/babel.cfg b/babel.cfg
deleted file mode 100644
index 15cd6cb..0000000
--- a/babel.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[python: **.py]
-
diff --git a/neutron_tempest_plugin/api/admin/test_network_segment_range.py b/neutron_tempest_plugin/api/admin/test_network_segment_range.py
index eacfbba..0ff45ac 100644
--- a/neutron_tempest_plugin/api/admin/test_network_segment_range.py
+++ b/neutron_tempest_plugin/api/admin/test_network_segment_range.py
@@ -13,6 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from neutron_lib import constants as n_constants
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
@@ -21,6 +22,7 @@
 
 TEST_SEGMENT_RANGE_MINIMUM_ID = 1100
 TEST_SEGMENT_RANGE_MAXIMUM_ID = 1105
+NETWORK_TYPE = n_constants.TYPE_VXLAN
 
 
 class NetworkSegmentRangeTestBase(base.BaseAdminNetworkTest):
@@ -34,7 +36,7 @@
     @classmethod
     def resource_setup(cls):
         super(NetworkSegmentRangeTestBase, cls).resource_setup()
-        network_type = "vxlan"
+        network_type = NETWORK_TYPE
         physical_network = ""
         minimum = TEST_SEGMENT_RANGE_MINIMUM_ID
         maximum = TEST_SEGMENT_RANGE_MAXIMUM_ID
@@ -132,7 +134,8 @@
         # Creates a network
         name = data_utils.rand_name('test_network_for_' + project_id)
         network = self.create_network(
-            name, client=self.admin_client, project_id=project_id)
+            name, client=self.admin_client, project_id=project_id,
+            provider_network_type=NETWORK_TYPE)
         # Updates a network segment range
         updated_maximum = TEST_SEGMENT_RANGE_MAXIMUM_ID + 50
         self.assertRaises(lib_exc.Conflict,
@@ -237,7 +240,8 @@
         # Creates a network
         name = data_utils.rand_name('test_network_for_' + project_id)
         network = self.create_network(
-            name, client=self.admin_client, project_id=project_id)
+            name, client=self.admin_client, project_id=project_id,
+            provider_network_type=NETWORK_TYPE)
         # Deletes a network segment range
         self.assertRaises(lib_exc.Conflict,
                           self.admin_client.delete_network_segment_range,
diff --git a/neutron_tempest_plugin/api/admin/test_networks.py b/neutron_tempest_plugin/api/admin/test_networks.py
index 17a8990..a67afa3 100644
--- a/neutron_tempest_plugin/api/admin/test_networks.py
+++ b/neutron_tempest_plugin/api/admin/test_networks.py
@@ -86,3 +86,55 @@
         network = self.admin_client.show_network(
             network['id'])['network']
         self.assertEqual('vxlan', network['provider:network_type'])
+
+    @decorators.idempotent_id('bbb9a2be-c9a7-4693-ac8e-d51b5371b68d')
+    def test_list_network_filter_provider_attributes(self):
+        if not config.CONF.neutron_plugin_options.provider_vlans:
+            raise self.skipException("No provider VLAN networks available")
+        project_id = self.client.project_id
+        physnet_name = config.CONF.neutron_plugin_options.provider_vlans[0]
+        # Check project networks pre-created.
+        body = self.client.list_networks(project_id=project_id)['networks']
+        num_networks_precreated = len(body)
+
+        networks = []
+        num_networks = 5
+        for _ in range(num_networks):
+            networks.append(self.create_network(
+                provider_network_type='vlan',
+                provider_physical_network=physnet_name,
+                project_id=project_id))
+
+        # Check new project networks created.
+        body = self.client.list_networks(project_id=project_id)['networks']
+        self.assertEqual(num_networks + num_networks_precreated, len(body))
+
+        vlan_ids = [net['provider:segmentation_id'] for net in networks]
+
+        # List networks with limit (from 1 to num_networks).
+        # Each filter (except from the 'provider:segmentation_id'), uses the
+        # value directly and in a list.
+        for idx in range(1, num_networks + 1):
+            # Filter by 'provider:network_type'
+            kwargs = {'provider:network_type': 'vlan',
+                      'project_id': project_id, 'limit': idx}
+            body = self.client.list_networks(**kwargs)['networks']
+            self.assertEqual(idx, len(body))
+            kwargs['provider:network_type'] = ['vlan']
+            body = self.client.list_networks(**kwargs)['networks']
+            self.assertEqual(idx, len(body))
+
+            # Filter by 'provider:physical_network'.
+            kwargs = {'provider:physical_network': physnet_name,
+                      'project_id': project_id, 'limit': idx}
+            body = self.client.list_networks(**kwargs)['networks']
+            self.assertEqual(idx, len(body))
+            kwargs['provider:physical_network'] = [physnet_name]
+            body = self.client.list_networks(**kwargs)['networks']
+            self.assertEqual(idx, len(body))
+
+            # Filter by 'provider:segmentation_id'
+            kwargs = {'provider:segmentation_id': vlan_ids,
+                      'project_id': project_id, 'limit': idx}
+            body = self.client.list_networks(**kwargs)['networks']
+            self.assertEqual(idx, len(body))
diff --git a/neutron_tempest_plugin/api/base.py b/neutron_tempest_plugin/api/base.py
index 4bcc6d2..178bf99 100644
--- a/neutron_tempest_plugin/api/base.py
+++ b/neutron_tempest_plugin/api/base.py
@@ -18,6 +18,7 @@
 import time
 
 import netaddr
+from neutron_lib._i18n import _
 from neutron_lib import constants as const
 from oslo_log import log
 from tempest.common import utils as tutils
@@ -492,7 +493,7 @@
             if ip_version:
                 if ip_version != gateway_ip.version:
                     raise ValueError(
-                        "Gateway IP version doesn't match IP version")
+                        _("Gateway IP version doesn't match IP version"))
             else:
                 ip_version = gateway_ip.version
         else:
@@ -541,8 +542,8 @@
         """
 
         if not cls.try_reserve_subnet_cidr(addr, **ipnetwork_kwargs):
-            raise ValueError('Subnet CIDR already reserved: {0!r}'.format(
-                addr))
+            raise ValueError(_('Subnet CIDR already reserved: {0!r}'.format(
+                addr)))
 
     @classmethod
     def try_reserve_subnet_cidr(cls, addr, **ipnetwork_kwargs):
@@ -601,7 +602,8 @@
                 mask_bits = CONF.network.project_network_v6_mask_bits
                 cidr = netaddr.IPNetwork(CONF.network.project_network_v6_cidr)
             else:
-                raise ValueError('Invalid IP version: {!r}'.format(ip_version))
+                raise ValueError(_(
+                    'Invalid IP version: {!r}'.format(ip_version)))
 
         if mask_bits:
             subnet_cidrs = cidr.subnet(mask_bits)
@@ -687,8 +689,9 @@
         if port:
             port_id = kwargs.setdefault('port_id', port['id'])
             if port_id != port['id']:
-                message = "Port ID specified twice: {!s} != {!s}".format(
-                    port_id, port['id'])
+                message = _(
+                    "Port ID specified twice: {!s} != {!s}".format(
+                        port_id, port['id']))
                 raise ValueError(message)
 
         fip = client.create_floatingip(external_network_id,
@@ -985,7 +988,7 @@
             project_id = kwargs.setdefault('project_id', project['id'])
             tenant_id = kwargs.setdefault('tenant_id', project['id'])
             if project_id != project['id'] or tenant_id != project['id']:
-                raise ValueError('Project ID specified multiple times')
+                raise ValueError(_('Project ID specified multiple times'))
         else:
             client = client or cls.client
 
@@ -1008,7 +1011,7 @@
         for security_group in security_groups:
             if security_group['name'] == name:
                 return security_group
-        raise ValueError("No such security group named {!r}".format(name))
+        raise ValueError(_("No such security group named {!r}".format(name)))
 
     @classmethod
     def create_security_group_rule(cls, security_group=None, project=None,
@@ -1018,7 +1021,7 @@
             project_id = kwargs.setdefault('project_id', project['id'])
             tenant_id = kwargs.setdefault('tenant_id', project['id'])
             if project_id != project['id'] or tenant_id != project['id']:
-                raise ValueError('Project ID specified multiple times')
+                raise ValueError(_('Project ID specified multiple times'))
 
         if 'security_group_id' not in kwargs:
             security_group = (security_group or
@@ -1029,7 +1032,8 @@
             security_group_id = kwargs.setdefault('security_group_id',
                                                   security_group['id'])
             if security_group_id != security_group['id']:
-                raise ValueError('Security group ID specified multiple times.')
+                raise ValueError(
+                    _('Security group ID specified multiple times.'))
 
         ip_version = ip_version or cls._ip_version
         default_params = (
diff --git a/neutron_tempest_plugin/api/test_networks.py b/neutron_tempest_plugin/api/test_networks.py
index d79b7ab..b9298c3 100644
--- a/neutron_tempest_plugin/api/test_networks.py
+++ b/neutron_tempest_plugin/api/test_networks.py
@@ -132,8 +132,6 @@
         _check_list_networks_fields(['project_id', 'tenant_id'], True, True)
 
 
-# TODO(ihrachys): check that bad mtu is not allowed; current API extension
-# definition doesn't enforce values
 # TODO(ihrachys): check that new segment reservation updates mtu, once
 # https://review.opendev.org/#/c/353115/ is merged
 class NetworksMtuTestJSON(base.BaseNetworkTest):
diff --git a/neutron_tempest_plugin/api/test_networks_negative.py b/neutron_tempest_plugin/api/test_networks_negative.py
index d4941e4..f0f6995 100644
--- a/neutron_tempest_plugin/api/test_networks_negative.py
+++ b/neutron_tempest_plugin/api/test_networks_negative.py
@@ -43,3 +43,37 @@
         with testtools.ExpectedException(lib_exc.BadRequest):
             self.client.create_network(
                 mtu=CONF.neutron_plugin_options.max_mtu + 1)
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('53537bba-d6c3-4a2e-bda4-ab5b009fb7d9')
+    def test_create_subnet_mtu_below_minimum_ipv4(self):
+        network = self.create_network(mtu=67)
+        with testtools.ExpectedException(lib_exc.Conflict):
+            self.create_subnet(network, ip_version=4, cidr='10.0.0.0/24')
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('1de68cb6-e6d4-47df-b820-c5048796f33a')
+    @testtools.skipUnless(config.CONF.network_feature_enabled.ipv6,
+                          'IPv6 is not enabled')
+    def test_create_subnet_mtu_below_minimum_ipv6(self):
+        network = self.create_network(mtu=1279)
+        with testtools.ExpectedException(lib_exc.Conflict):
+            self.create_subnet(network, ip_version=6, cidr='2001:db8:0:1::/64')
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('5213df6d-7141-40b2-90ea-a958d9bc97e5')
+    def test_update_network_mtu_below_minimum_ipv4(self):
+        network = self.create_network(mtu=1280)
+        self.create_subnet(network, ip_version=4, cidr='10.0.0.0/24')
+        with testtools.ExpectedException(lib_exc.Conflict):
+            self.client.update_network(network['id'], mtu=67)
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('1a714fc4-24b1-4c07-a005-d5c218672eab')
+    @testtools.skipUnless(config.CONF.network_feature_enabled.ipv6,
+                          'IPv6 is not enabled')
+    def test_update_network_mtu_below_minimum_ipv6(self):
+        network = self.create_network(mtu=1280)
+        self.create_subnet(network, ip_version=6, cidr='2001:db8:0:1::/64')
+        with testtools.ExpectedException(lib_exc.Conflict):
+            self.client.update_network(network['id'], mtu=1279)
diff --git a/neutron_tempest_plugin/api/test_routers.py b/neutron_tempest_plugin/api/test_routers.py
index 1470a7b..0012ffe 100644
--- a/neutron_tempest_plugin/api/test_routers.py
+++ b/neutron_tempest_plugin/api/test_routers.py
@@ -506,9 +506,17 @@
                     remove_gateways[0])
 
         external_gateways[1] = remove_gateways[0]
-        res_update_gws = self.admin_client.router_update_external_gateways(
-            router['id'],
-            external_gateways)
+        try:
+            res_update_gws = self.admin_client.router_update_external_gateways(
+                router['id'],
+                external_gateways)
+        except lib_exc.Conflict as exc:
+            if 'IpAddressAlreadyAllocated' in str(exc):
+                self.skipTest(
+                    'The IP address of the removed gateway port is already '
+                    'used by other test, thus this exception is dismissed and '
+                    'the rest of the test skipped')
+            raise exc
 
         self.assertEqual(len(res_update_gws['router']['external_gateways']), 2)
         for n in range(0, 2):
diff --git a/neutron_tempest_plugin/bgpvpn/base.py b/neutron_tempest_plugin/bgpvpn/base.py
index b436a5d..aeecbfc 100644
--- a/neutron_tempest_plugin/bgpvpn/base.py
+++ b/neutron_tempest_plugin/bgpvpn/base.py
@@ -72,13 +72,8 @@
     @classmethod
     def skip_checks(cls):
         super(BaseBgpvpnTest, cls).skip_checks()
-        msg = None
         if not utils.is_extension_enabled('bgpvpn', 'network'):
             msg = "Bgpvpn extension not enabled."
-        elif not CONF.bgpvpn.run_bgpvpn_tests:
-            msg = ("Running of bgpvpn related tests is disabled in "
-                   "plugin configuration.")
-        if msg:
             raise cls.skipException(msg)
 
     def create_bgpvpn(self, client, **kwargs):
diff --git a/neutron_tempest_plugin/bgpvpn/scenario/test_bgpvpn_basic.py b/neutron_tempest_plugin/bgpvpn/scenario/test_bgpvpn_basic.py
index 9cca602..c9f4bcc 100644
--- a/neutron_tempest_plugin/bgpvpn/scenario/test_bgpvpn_basic.py
+++ b/neutron_tempest_plugin/bgpvpn/scenario/test_bgpvpn_basic.py
@@ -17,6 +17,7 @@
 import random
 
 import netaddr
+from neutron_lib._i18n import _
 from neutron_lib.utils import test
 from oslo_concurrency import lockutils
 from oslo_log import log as logging
@@ -42,8 +43,8 @@
 if "SUBNETPOOL_PREFIX_V4" in os.environ:
     subnet_base = netaddr.IPNetwork(os.environ['SUBNETPOOL_PREFIX_V4'])
     if subnet_base.prefixlen > 21:
-        raise Exception("if SUBNETPOOL_PREFIX_V4 is set, it needs to offer "
-                        "space for at least 8 /24 subnets")
+        raise Exception(_("if SUBNETPOOL_PREFIX_V4 is set, it needs to offer "
+                          "space for at least 8 /24 subnets"))
 else:
     subnet_base = netaddr.IPNetwork("10.100.0.0/16")
 
diff --git a/neutron_tempest_plugin/common/ip.py b/neutron_tempest_plugin/common/ip.py
index 5335219..bab9064 100644
--- a/neutron_tempest_plugin/common/ip.py
+++ b/neutron_tempest_plugin/common/ip.py
@@ -19,6 +19,7 @@
 import subprocess
 
 import netaddr
+from neutron_lib._i18n import _
 from neutron_lib import constants
 from oslo_log import log
 from oslo_utils import excutils
@@ -89,9 +90,9 @@
             for ip, prefix_len in _get_ip_address_prefix_len_pairs(
                 port=subport, subnets=subnets)]
         if not subport_ips:
-            raise ValueError(
+            raise ValueError(_(
                 "Unable to get IP address and subnet prefix lengths for "
-                "subport")
+                "subport"))
 
         return self.configure_vlan(addresses, port, vlan_tag, subport_ips,
                                    subport['mac_address'])
@@ -159,21 +160,22 @@
         # ip addr del 192.168.1.1/24 dev em1
         return self.execute('address', 'del', address, 'dev', device)
 
-    def add_route(self, address, device, gateway=None):
+    def add_route(self, address, device, gateway=None, ip_version=4):
         if gateway:
-            # ip route add 192.168.1.0/24 via 192.168.22.1 dev em1
             return self.execute(
                 'route', 'add', address, 'via', gateway, 'dev', device)
         else:
-            # ip route add 192.168.1.0/24 dev em1
-            return self.execute('route', 'add', address, 'dev', device)
+            return self.execute(
+                f'-{ip_version}', 'route', 'add', address, 'dev', device)
 
-    def delete_route(self, address, device):
-        # ip route del 192.168.1.0/24 dev em1
-        return self.execute('route', 'del', address, 'dev', device)
+    def delete_route(self, address, device, ip_version=4):
+        return self.execute(
+            f'-{ip_version}', 'route', 'del', address, 'dev', device)
 
-    def list_routes(self, *args):
-        output = self.execute('route', 'show', *args)
+    def list_routes(self, *args, device=None, ip_version=4):
+        if not args and device:
+            args = ("dev", device)
+        output = self.execute(f'-{ip_version}', 'route', 'show', *args)
         return list(parse_routes(output))
 
     def get_nic_name_by_mac(self, mac_address):
@@ -352,7 +354,7 @@
     for address in list_ip_addresses(addresses=addresses, port=port):
         return address.device.name
 
-    msg = "Port {0!r} fixed IPs not found on server.".format(port['id'])
+    msg = _("Port {0!r} fixed IPs not found on server.".format(port['id']))
     raise ValueError(msg)
 
 
@@ -361,7 +363,8 @@
             ip_addresses=ip_addresses):
         return address.device.name
 
-    msg = "Fixed IPs {0!r} not found on server.".format(' '.join(ip_addresses))
+    msg = _(
+        "Fixed IPs {0!r} not found on server.".format(' '.join(ip_addresses)))
     raise ValueError(msg)
 
 
diff --git a/neutron_tempest_plugin/common/shell.py b/neutron_tempest_plugin/common/shell.py
index 073bf55..723c30e 100644
--- a/neutron_tempest_plugin/common/shell.py
+++ b/neutron_tempest_plugin/common/shell.py
@@ -16,7 +16,6 @@
 
 import collections
 import subprocess
-import sys
 
 from oslo_log import log
 from tempest.lib import exceptions as lib_exc
@@ -131,12 +130,6 @@
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
 
-    if timeout and sys.version_info < (3, 3):
-        # TODO(fressi): re-implement to timeout support on older Pythons
-        LOG.warning("Popen.communicate method doens't support for timeout "
-                    "on Python %r", sys.version)
-        timeout = None
-
     # Wait for process execution while reading STDERR and STDOUT streams
     if timeout:
         try:
diff --git a/neutron_tempest_plugin/common/ssh.py b/neutron_tempest_plugin/common/ssh.py
index 700d21b..b0dd9c1 100644
--- a/neutron_tempest_plugin/common/ssh.py
+++ b/neutron_tempest_plugin/common/ssh.py
@@ -17,6 +17,7 @@
 import socket
 import time
 
+from neutron_lib._i18n import _
 from oslo_log import log
 import paramiko
 from tempest.lib.common import ssh
@@ -70,8 +71,8 @@
         host = cls.proxy_jump_host
         if not host:
             # proxy_jump_host string cannot be empty or None
-            raise ValueError(
-                "'proxy_jump_host' configuration option is empty.")
+            raise ValueError(_(
+                "'proxy_jump_host' configuration option is empty."))
 
         # Let accept an empty string as a synonymous of default value on below
         # options
@@ -82,9 +83,9 @@
         # Port must be a positive integer
         port = cls.proxy_jump_port
         if port <= 0 or port > 65535:
-            raise ValueError(
+            raise ValueError(_(
                 "Invalid value for 'proxy_jump_port' configuration option: "
-                "{!r}".format(port))
+                "{!r}".format(port)))
 
         login = "{username}@{host}:{port}".format(username=username, host=host,
                                                   port=port)
@@ -99,9 +100,9 @@
             else:
                 # This message could help the user to identify a
                 # mis-configuration in tempest.conf
-                raise ValueError(
+                raise ValueError(_(
                     "Cannot find file specified as 'proxy_jump_keyfile' "
-                    "option: {!r}".format(key_file))
+                    "option: {!r}".format(key_file)))
 
         elif password:
             LOG.debug("Going to create SSH connection to %r using password.",
@@ -230,8 +231,6 @@
 
             # Update local environment
             lang, encoding = locale.getlocale()
-            if not lang:
-                lang, encoding = locale.getdefaultlocale()
             _locale = '.'.join([lang, encoding])
             channel.update_environment({'LC_ALL': _locale,
                                         'LANG': _locale})
diff --git a/neutron_tempest_plugin/common/utils.py b/neutron_tempest_plugin/common/utils.py
index 62191bf..184444f 100644
--- a/neutron_tempest_plugin/common/utils.py
+++ b/neutron_tempest_plugin/common/utils.py
@@ -25,8 +25,7 @@
 except ImportError:
     from urllib import parse as urlparse
 
-import eventlet
-
+from neutron_lib._i18n import _
 from oslo_log import log
 from tempest.lib import exceptions
 
@@ -79,15 +78,14 @@
     :param exception: Exception instance to raise on timeout. If None is passed
                       (default) then WaitTimeout exception is raised.
     """
-    try:
-        with eventlet.Timeout(timeout):
-            while not predicate():
-                eventlet.sleep(sleep)
-    except eventlet.Timeout:
-        if exception is not None:
-            # pylint: disable=raising-bad-type
-            raise exception
-        raise WaitTimeout("Timed out after %d seconds" % timeout)
+    start_time = time.time()
+    while not predicate():
+        elapsed_time = time.time() - start_time
+        if elapsed_time > timeout:
+            raise exception if exception else WaitTimeout(
+                _("Timed out after %d seconds") % timeout
+            )
+        time.sleep(sleep)
 
 
 def override_class(overriden_class, overrider_class):
@@ -167,7 +165,7 @@
         self.server_ssh.exec_command(
                 'echo "{}" > input.txt'.format(self.test_str))
         server_exec_method('tail -f input.txt | sudo nc -lp '
-                '{} &> output.txt &'.format(self.port))
+                '{} &> output.txt & sleep 1'.format(self.port))
         self.client_ssh.exec_command(
                 'echo "{}" > input.txt'.format(self.test_str))
         client_exec_method('tail -f input.txt | sudo nc {} {} &>'
diff --git a/neutron_tempest_plugin/config.py b/neutron_tempest_plugin/config.py
index 38d6ac6..1f5c34c 100644
--- a/neutron_tempest_plugin/config.py
+++ b/neutron_tempest_plugin/config.py
@@ -100,6 +100,7 @@
                help='User name used to connect to "ssh_proxy_jump_host".'),
     cfg.StrOpt('ssh_proxy_jump_password',
                default=None,
+               secret=True,
                help='Password used to connect to "ssh_proxy_jump_host".'),
     cfg.StrOpt('ssh_proxy_jump_keyfile',
                default=None,
@@ -143,14 +144,12 @@
     # while testing in parallel.
     cfg.BoolOpt('create_shared_resources',
                 default=False,
-                help='Allow creation of shared resources.'
-                     'The default value is false.'),
+                help='Allow creation of shared resources.'),
     cfg.BoolOpt('is_igmp_snooping_enabled',
                 default=False,
                 help='Indicates whether IGMP snooping is enabled or not. '
                      'If True, multicast test(s) will assert that multicast '
-                     'traffic is not being flooded to all ports. Defaults '
-                     'to False.'),
+                     'traffic is not being flooded to all ports.'),
     # Option for scheduling BGP speakers to agents explicitly
     # The default is false with automatic scheduling on creation
     # happening with the default scheduler
@@ -158,22 +157,11 @@
                 default=False,
                 help='Schedule BGP speakers to agents explicitly.'),
 ]
+neutron_group = cfg.OptGroup(name="neutron_plugin_options",
+                             title="Neutron Plugin Options")
 
-# TODO(amuller): Redo configuration options registration as part of the planned
-# transition to the Tempest plugin architecture
-for opt in NeutronPluginOptions:
-    CONF.register_opt(opt, 'neutron_plugin_options')
 
 BgpvpnGroup = [
-    # TODO(tkajinam): This has been required since the plugin tests was merged
-    # into neutron-tempest-plugin in Train. Remove this after 2025.1 release.
-    cfg.BoolOpt('run_bgpvpn_tests',
-                default=True,
-                help=("If it is set to False bgpvpn api and scenario tests "
-                      "will be skipped"),
-                deprecated_for_removal=True,
-                deprecated_reason='Tests are skipped according to '
-                                  'the available extensions.'),
     cfg.IntOpt('min_asn',
                default=100,
                help=("Minimum number for the range of "
@@ -191,48 +179,17 @@
                help=("Maximum number for the range of "
                      "assigned number for distinguishers.")),
 ]
-
 bgpvpn_group = cfg.OptGroup(name="bgpvpn", title=("Networking-Bgpvpn Service "
                                                   "Options"))
-CONF.register_group(bgpvpn_group)
-CONF.register_opts(BgpvpnGroup, group="bgpvpn")
 
 FwaasGroup = [
-    # TODO(tkajinam): This has been required since the plugin tests was merged
-    # into neutron-tempest-plugin in Train. Remove this after 2025.1 release.
-    cfg.BoolOpt('run_fwaas_tests',
-                default=True,
-                help=("If it is set to False fwaas api and scenario tests "
-                      "will be skipped"),
-                deprecated_for_removal=True,
-                deprecated_reason='Tests are skipped according to '
-                                  'the available extensions.'),
     cfg.StrOpt('driver',
                default=None,
                choices=['openvswitch', 'ovn'],
                help='Driver used by the FWaaS plugin.'),
 ]
-
 fwaas_group = cfg.OptGroup(
     name="fwaas", title=("Neutron-fwaas Service Options"))
-CONF.register_group(fwaas_group)
-CONF.register_opts(FwaasGroup, group="fwaas")
-
-SfcGroup = [
-    # TODO(tkajinam): This has been required since the plugin tests was merged
-    # into neutron-tempest-plugin in Train. Remove this after 2025.1 release.
-    cfg.BoolOpt('run_sfc_tests',
-                default=True,
-                help=("If it is set to False SFC api and scenario tests "
-                      "will be skipped"),
-                deprecated_for_removal=True,
-                deprecated_reason='Tests are skipped according to '
-                                  'the available extensions.'),
-]
-
-sfc_group = cfg.OptGroup(name="sfc", title=("Networking-sfc Service Options"))
-CONF.register_group(sfc_group)
-CONF.register_opts(SfcGroup, group="sfc")
 
 
 TaasGroup = [
@@ -249,8 +206,6 @@
 ]
 taas_group = cfg.OptGroup(name='taas',
                           title='TaaS Tempest Options')
-CONF.register_group(taas_group)
-CONF.register_opts(TaasGroup, group="taas")
 
 
 DynamicRoutingGroup = [
@@ -259,12 +214,9 @@
                help=('Base image used to build the image for connectivity '
                      'check')),
 ]
-
 dynamic_routing_group = cfg.OptGroup(
     name="dynamic_routing",
     title=("Neutron-Dynamic-Routing Service Options"))
-CONF.register_group(dynamic_routing_group)
-CONF.register_opts(DynamicRoutingGroup, group="dynamic_routing")
 
 
 # DNS Integration with an External Service
@@ -276,5 +228,3 @@
 ]
 dns_feature_group = cfg.OptGroup(
     name='designate_feature_enabled', title='Enabled Designate Features')
-CONF.register_group(dns_feature_group)
-CONF.register_opts(DnsFeatureGroup, group="designate_feature_enabled")
diff --git a/neutron_tempest_plugin/fwaas/api/fwaas_v2_base.py b/neutron_tempest_plugin/fwaas/api/fwaas_v2_base.py
index f4f63ec..e54c643 100644
--- a/neutron_tempest_plugin/fwaas/api/fwaas_v2_base.py
+++ b/neutron_tempest_plugin/fwaas/api/fwaas_v2_base.py
@@ -21,13 +21,4 @@
 
 
 class BaseFWaaSTest(fwaas_v2_client.FWaaSClientMixin, base.BaseNetworkTest):
-
-    @classmethod
-    def skip_checks(cls):
-        super(BaseFWaaSTest, cls).skip_checks()
-        msg = None
-        if not CONF.fwaas.run_fwaas_tests:
-            msg = ("Running of fwaas related tests is disabled in "
-                   "plugin configuration.")
-        if msg:
-            raise cls.skipException(msg)
+    pass
diff --git a/neutron_tempest_plugin/fwaas/scenario/fwaas_v2_manager.py b/neutron_tempest_plugin/fwaas/scenario/fwaas_v2_manager.py
index 9cc0a6a..371a186 100644
--- a/neutron_tempest_plugin/fwaas/scenario/fwaas_v2_manager.py
+++ b/neutron_tempest_plugin/fwaas/scenario/fwaas_v2_manager.py
@@ -31,16 +31,6 @@
 
     credentials = ['primary']
 
-    @classmethod
-    def skip_checks(cls):
-        super(ScenarioTest, cls).skip_checks()
-        msg = None
-        if not CONF.fwaas.run_fwaas_tests:
-            msg = ("Running of fwaas related tests is disabled in "
-                   "plugin configuration.")
-        if msg:
-            raise cls.skipException(msg)
-
 
 class NetworkScenarioTest(ScenarioTest):
     """Base class for network scenario tests.
diff --git a/neutron_tempest_plugin/plugin.py b/neutron_tempest_plugin/plugin.py
index fc41bdd..029fdab 100644
--- a/neutron_tempest_plugin/plugin.py
+++ b/neutron_tempest_plugin/plugin.py
@@ -16,8 +16,11 @@
 
 import os
 
+from tempest import config
 from tempest.test_discover import plugins
 
+from neutron_tempest_plugin import config as neutron_config
+
 
 class NeutronTempestPlugin(plugins.TempestPlugin):
     def load_tests(self):
@@ -28,7 +31,31 @@
         return full_test_dir, base_path
 
     def register_opts(self, conf):
-        pass
+        config.register_opt_group(conf, neutron_config.neutron_group,
+                                  neutron_config.NeutronPluginOptions)
+        config.register_opt_group(conf, neutron_config.bgpvpn_group,
+                                  neutron_config.BgpvpnGroup)
+        config.register_opt_group(conf, neutron_config.fwaas_group,
+                                  neutron_config.FwaasGroup)
+        config.register_opt_group(conf, neutron_config.taas_group,
+                                  neutron_config.TaasGroup)
+        config.register_opt_group(conf, neutron_config.dynamic_routing_group,
+                                  neutron_config.DynamicRoutingGroup)
+        config.register_opt_group(conf, neutron_config.dns_feature_group,
+                                  neutron_config.DnsFeatureGroup)
 
     def get_opt_lists(self):
-        pass
+        return [
+            (neutron_config.neutron_group.name,
+             neutron_config.NeutronPluginOptions),
+            (neutron_config.bgpvpn_group.name,
+             neutron_config.BgpvpnGroup),
+            (neutron_config.fwaas_group.name,
+             neutron_config.FwaasGroup),
+            (neutron_config.taas_group.name,
+             neutron_config.TaasGroup),
+            (neutron_config.dynamic_routing_group.name,
+             neutron_config.DynamicRoutingGroup),
+            (neutron_config.dns_feature_group.name,
+             neutron_config.DnsFeatureGroup)
+        ]
diff --git a/neutron_tempest_plugin/scenario/base.py b/neutron_tempest_plugin/scenario/base.py
index 70cb2dc..f7d08eb 100644
--- a/neutron_tempest_plugin/scenario/base.py
+++ b/neutron_tempest_plugin/scenario/base.py
@@ -18,6 +18,7 @@
 
 from debtcollector import removals
 import netaddr
+from neutron_lib._i18n import _
 from neutron_lib.api import validators
 from neutron_lib import constants as neutron_lib_constants
 from oslo_log import log
@@ -176,7 +177,7 @@
                                        client=None):
         """This rule is intended to permit inbound ssh
 
-        Allowing ssh traffic traffic from all sources, so no group_id is
+        Allowing ssh traffic from all sources, so no group_id is
         provided.
         Setting a group_id would only permit traffic from ports
         belonging to the same security group.
@@ -680,8 +681,8 @@
             router = client.update_router(router['id'], **kwargs)['router']
             return router
         else:
-            raise Exception("Neither of 'public_router_id' or "
-                            "'public_network_id' has been defined.")
+            raise Exception(_("Neither of 'public_router_id' or "
+                              "'public_network_id' has been defined."))
 
     def _update_router_admin_state(self, router, admin_state_up):
         kwargs = dict(admin_state_up=admin_state_up)
diff --git a/neutron_tempest_plugin/scenario/test_multicast.py b/neutron_tempest_plugin/scenario/test_multicast.py
index 390e0f0..a28328b 100644
--- a/neutron_tempest_plugin/scenario/test_multicast.py
+++ b/neutron_tempest_plugin/scenario/test_multicast.py
@@ -333,12 +333,20 @@
                     "Receiver {!r} didn't get multicast message".format(
                         receiver['id'])))
 
-        # TODO(slaweq): add validation of answears on sended server
-        replies_result = sender['ssh_client'].execute_script(
-            "cat {path} || echo '{path} not exists yet'".format(
-                path=self.sender_output_file))
-        for receiver_id in receiver_ids:
-            self.assertIn(receiver_id, replies_result)
+        def _sender_completed():
+            replies_result = sender['ssh_client'].execute_script(
+                "cat {path} 2>/dev/null || echo ''".format(
+                    path=self.sender_output_file))
+            for receiver_id in receiver_ids:
+                expected_pattern = "received reply b'{}' from".format(
+                    receiver_id)
+                if expected_pattern not in replies_result:
+                    return False
+            return replies_result.count('received reply') == len(receiver_ids)
+
+        utils.wait_until_true(
+            _sender_completed,
+            exception=RuntimeError("Sender didn't complete properly"))
 
         def check_unregistered_host():
             unregistered_result = unregistered['ssh_client'].execute_script(
diff --git a/neutron_tempest_plugin/scenario/test_multiple_gws.py b/neutron_tempest_plugin/scenario/test_multiple_gws.py
index e4f1d3d..51d047a 100644
--- a/neutron_tempest_plugin/scenario/test_multiple_gws.py
+++ b/neutron_tempest_plugin/scenario/test_multiple_gws.py
@@ -12,7 +12,7 @@
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 #    License for the specific language governing permissions and limitations
 #    under the License.
-import json
+
 import os
 import subprocess
 import time
@@ -29,6 +29,7 @@
 from neutron_lib import constants as const
 
 from oslo_log import log
+from oslo_serialization import jsonutils
 
 from os_ken.tests.integrated.common import docker_base as ctn_base
 
@@ -288,7 +289,7 @@
         )
 
     def show_bfd_peer(self, peer: str) -> typing.Dict[str, typing.Any]:
-        return json.loads(self.vtysh([f'show bfd peer {peer} json']))
+        return jsonutils.loads(self.vtysh([f'show bfd peer {peer} json']))
 
     def wait_for_bfd_peer_status(
         self, peer: str, status: str, try_times=30, interval=1
diff --git a/neutron_tempest_plugin/scenario/test_security_groups.py b/neutron_tempest_plugin/scenario/test_security_groups.py
index dc0f5ef..b963887 100644
--- a/neutron_tempest_plugin/scenario/test_security_groups.py
+++ b/neutron_tempest_plugin/scenario/test_security_groups.py
@@ -437,6 +437,9 @@
         # configure sec group to support SSH connectivity
         self.create_loginable_secgroup_rule(
             secgroup_id=ssh_secgrp['id'])
+        if self.stateless_sg:
+            self.create_ingress_metadata_secgroup_rule(
+                secgroup_id=ssh_secgrp['id'])
         # spawn two instances with the sec group created
         server_ssh_clients, fips, servers = self.create_vm_testing_sec_grp(
             security_groups=[{'name': ssh_secgrp['name']}])
@@ -464,9 +467,13 @@
                              should_succeed=False)
 
         # add ICMP rule with remote address group
+        address_set = [str(netaddr.IPNetwork(fips[0]['fixed_ip_address']))]
+        if self.stateless_sg:
+            address_set.append(
+                str(netaddr.IPNetwork(fips[1]['fixed_ip_address'])))
         test_ag = self.create_address_group(
             name=data_utils.rand_name('test_ag'),
-            addresses=[str(netaddr.IPNetwork(fips[0]['fixed_ip_address']))])
+            addresses=address_set)
         rule_list = [{'protocol': constants.PROTO_NUM_ICMP,
                       'direction': constants.INGRESS_DIRECTION,
                       'remote_address_group_id': test_ag['id']}]
@@ -762,8 +769,8 @@
         self._test_remote_group()
 
     @testtools.skipUnless(
-        CONF.neutron_plugin_options.firewall_driver == 'openvswitch',
-        "Openvswitch agent is required to run this test")
+        CONF.neutron_plugin_options.firewall_driver in ['openvswitch', 'ovn'],
+        "Openvswitch agent or Ml2/OVN is required to run this test")
     @decorators.idempotent_id('678dd4c0-2953-4626-b89c-8e7e4110ec4b')
     @tempest_utils.requires_ext(extension="address-group", service="network")
     @tempest_utils.requires_ext(
@@ -949,8 +956,8 @@
         self._test_remote_group()
 
     @testtools.skipUnless(
-        CONF.neutron_plugin_options.firewall_driver == 'openvswitch',
-        "Openvswitch agent is required to run this test")
+        CONF.neutron_plugin_options.firewall_driver in ['openvswitch', 'ovn'],
+        "Openvswitch agent or Ml2/OVN is required to run this test")
     @decorators.idempotent_id('9fae530d-2711-4c61-a4a5-8efe6e58ab14')
     @tempest_utils.requires_ext(extension="address-group", service="network")
     @tempest_utils.requires_ext(
diff --git a/neutron_tempest_plugin/scenario/test_vlan_transparency.py b/neutron_tempest_plugin/scenario/test_vlan_transparency.py
index 85648bc..11ff510 100644
--- a/neutron_tempest_plugin/scenario/test_vlan_transparency.py
+++ b/neutron_tempest_plugin/scenario/test_vlan_transparency.py
@@ -123,8 +123,8 @@
             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)
+        vtag = vlan_tag % 256
+        vlan_ipmask_template = '192.%d.%d.{ip_last_byte}/24' % (vtag, vtag)
         vms = []
         vlan_ipmasks = []
         floating_ips = []
diff --git a/neutron_tempest_plugin/services/network/json/network_client.py b/neutron_tempest_plugin/services/network/json/network_client.py
index 289ef61..f5583f2 100644
--- a/neutron_tempest_plugin/services/network/json/network_client.py
+++ b/neutron_tempest_plugin/services/network/json/network_client.py
@@ -13,6 +13,7 @@
 import time
 from urllib import parse as urlparse
 
+from neutron_lib._i18n import _
 from oslo_serialization import jsonutils
 from tempest.lib.common import rest_client as service_client
 from tempest.lib import exceptions as lib_exc
@@ -304,7 +305,7 @@
         try:
             getattr(self, method)(id)
         except AttributeError:
-            raise Exception("Unknown resource type %s " % resource_type)
+            raise Exception(_("Unknown resource type %s " % resource_type))
         except lib_exc.NotFound:
             return True
         return False
diff --git a/neutron_tempest_plugin/sfc/tests/api/base.py b/neutron_tempest_plugin/sfc/tests/api/base.py
index 606aed6..329bed8 100644
--- a/neutron_tempest_plugin/sfc/tests/api/base.py
+++ b/neutron_tempest_plugin/sfc/tests/api/base.py
@@ -34,16 +34,6 @@
 ):
 
     @classmethod
-    def skip_checks(cls):
-        super(BaseFlowClassifierTest, cls).skip_checks()
-        msg = None
-        if not CONF.sfc.run_sfc_tests:
-            msg = ("Running of SFC related tests is disabled in "
-                   "plugin configuration.")
-        if msg:
-            raise cls.skipException(msg)
-
-    @classmethod
     def resource_setup(cls):
         super(BaseFlowClassifierTest, cls).resource_setup()
         if not utils.is_extension_enabled('flow_classifier', 'network'):
diff --git a/neutron_tempest_plugin/sfc/tests/scenario/base.py b/neutron_tempest_plugin/sfc/tests/scenario/base.py
index 44b5cd2..5945aef 100644
--- a/neutron_tempest_plugin/sfc/tests/scenario/base.py
+++ b/neutron_tempest_plugin/sfc/tests/scenario/base.py
@@ -31,16 +31,6 @@
     manager.NetworkScenarioTest
 ):
 
-    @classmethod
-    def skip_checks(cls):
-        super(SfcScenarioTest, cls).skip_checks()
-        msg = None
-        if not CONF.sfc.run_sfc_tests:
-            msg = ("Running of SFC related tests is disabled in "
-                   "plugin configuration.")
-        if msg:
-            raise cls.skipException(msg)
-
     def _check_connectivity(
         self, source_ip, destination_ip, routes=None,
         username=None, private_key=None
diff --git a/neutron_tempest_plugin/tap_as_a_service/api/test_tap_mirror.py b/neutron_tempest_plugin/tap_as_a_service/api/test_tap_mirror.py
new file mode 100644
index 0000000..85c37c6
--- /dev/null
+++ b/neutron_tempest_plugin/tap_as_a_service/api/test_tap_mirror.py
@@ -0,0 +1,218 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from oslo_utils import uuidutils
+
+from tempest.common import utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from neutron_tempest_plugin.tap_as_a_service import base
+
+
+class TapMirrorTestJSON(base.BaseTaasTest):
+
+    @classmethod
+    @utils.requires_ext(extension='tap-mirror', service='network')
+    def skip_checks(cls):
+        super().skip_checks()
+
+    @classmethod
+    def resource_setup(cls):
+        super().resource_setup()
+        cls.network = cls.create_network()
+        cls.tap_mirror_port = cls.create_port(cls.network)
+        cls.in_direction = {'IN': 101}
+        cls.out_direction = {'OUT': 102}
+        cls.both_direction = cls.in_direction | cls.out_direction
+        cls.remote_ip = '192.101.0.42'
+        cls.remote_ip2 = '192.101.3.43'
+        cls.gre = 'gre'
+        cls.erspan = 'erspanv1'
+
+    @decorators.idempotent_id('628f202c-ed0a-4eb1-8547-4954f67a84b7')
+    def test_create_tap_mirror(self):
+        tap_mirror = self.create_tap_mirror(
+            port_id=self.tap_mirror_port['id'],
+            directions=self.in_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+        self.assertEqual(self.tap_mirror_port['id'], tap_mirror['port_id'])
+        self.assertEqual('gre', tap_mirror['mirror_type'])
+        self.assertEqual(self.in_direction, tap_mirror['directions'])
+        self.tap_mirrors_client.delete_tap_mirror(tap_mirror['id'])
+
+        tap_mirror = self.create_tap_mirror(
+            port_id=self.tap_mirror_port['id'],
+            directions=self.both_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.erspan
+        )
+        self.assertEqual(self.tap_mirror_port['id'], tap_mirror['port_id'])
+        self.assertEqual(self.erspan, tap_mirror['mirror_type'])
+        self.assertEqual(self.both_direction, tap_mirror['directions'])
+
+    @decorators.idempotent_id('299c251b-e0bc-4449-98db-959a5d8038c2')
+    def test_list_show_tap_mirror(self):
+        tap_mirror = self.create_tap_mirror(
+            port_id=self.tap_mirror_port['id'],
+            directions=self.out_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+        tap_mirrors = self.tap_mirrors_client.list_tap_mirrors()
+        is_t_m_found = False
+        for t_m in tap_mirrors['tap_mirrors']:
+            if t_m['id'] == tap_mirror['id']:
+                is_t_m_found = True
+                break
+        self.assertTrue(is_t_m_found)
+        tap_mirror_show_res = self.tap_mirrors_client.show_tap_mirror(
+            tap_mirror['id'])['tap_mirror']
+        self.assertEqual(tap_mirror['id'], tap_mirror_show_res['id'])
+        self.assertEqual(self.gre, tap_mirror_show_res['mirror_type'])
+        self.assertEqual(self.remote_ip,
+                         tap_mirror_show_res['remote_ip'])
+        self.assertEqual(self.out_direction,
+                         tap_mirror_show_res['directions'])
+
+    @decorators.idempotent_id('19c40379-bda5-48c9-8873-fc990739d1b5')
+    def test_update_tap_mirror(self):
+        tap_mirror = self.create_tap_mirror(
+            port_id=self.tap_mirror_port['id'],
+            directions=self.in_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+        self.tap_mirrors_client.update_tap_mirror(
+            tap_mirror_id=tap_mirror['id'],
+            name='new_name',
+            description='My fancy Tap Mirror'
+        )
+        tap_mirror_show_res = self.tap_mirrors_client.show_tap_mirror(
+            tap_mirror['id'])['tap_mirror']
+        self.assertEqual('new_name', tap_mirror_show_res['name'])
+        self.assertEqual('My fancy Tap Mirror',
+                         tap_mirror_show_res['description'])
+
+    @decorators.idempotent_id('9ed165af-7c54-43ac-b14f-077e8f9601f6')
+    def test_delete_mirror_port_deletes_tap_mirror(self):
+        port1 = self.create_port(self.network)
+        tap_mirror = self.create_tap_mirror(
+            port_id=port1['id'],
+            directions=self.out_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+        # Delete port will result in deteltion of the tap_mirror
+        self.ports_client.delete_port(port1['id'])
+        self.assertRaises(lib_exc.NotFound,
+                          self.tap_mirrors_client.show_tap_mirror,
+                          tap_mirror['id'])
+
+    @decorators.idempotent_id('abdd4451-bd9d-4f1e-ab7f-e949b9246714')
+    def test_delete_tap_mirror_port_remains(self):
+        port1 = self.create_port(self.network)
+        tap_mirror = self.create_tap_mirror(
+            port_id=port1['id'],
+            directions=self.out_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+        # Delete tap_mirror will keep the port
+        self.tap_mirrors_client.delete_tap_mirror(tap_mirror['id'])
+        port_res = self.ports_client.show_port(port1['id'])['port']
+        self.assertEqual(port1['name'], port_res['name'])
+
+    @decorators.idempotent_id('1d8b68fc-a600-4b9e-bd17-9469c3a6c95b')
+    def test_create_tap_mirror_negative(self):
+        # directions keys' valid values are IN and OUT
+        self.assertRaises(lib_exc.BadRequest,
+                          self.create_tap_mirror,
+                          port_id=self.tap_mirror_port['id'],
+                          directions={'something': 101},
+                          remote_ip=self.remote_ip,
+                          mirror_type=self.gre)
+        # mirror_type valid values are erspanv1 and gre
+        self.assertRaises(lib_exc.BadRequest,
+                          self.create_tap_mirror,
+                          port_id=self.tap_mirror_port['id'],
+                          directions=self.out_direction,
+                          remote_ip=self.remote_ip,
+                          mirror_type='erspanv2')
+        # remote_ip must be a valid IP
+        self.assertRaises(lib_exc.BadRequest,
+                          self.create_tap_mirror,
+                          port_id=self.tap_mirror_port['id'],
+                          directions=self.in_direction,
+                          remote_ip='192.101.0.420',
+                          mirror_type=self.gre)
+
+    @decorators.idempotent_id('2b7850b3-3920-4f16-96b7-05e2efd96877')
+    def test_create_tap_service_tunnel_id_conflict(self):
+        self.create_tap_mirror(
+            port_id=self.tap_mirror_port['id'],
+            directions=self.in_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+
+        port2 = self.create_port(self.network)
+        self.addCleanup(self.ports_client.delete_port, port2['id'])
+        self.assertRaises(lib_exc.Conflict,
+                          self.create_tap_mirror,
+                          port_id=port2['id'],
+                          directions=self.in_direction,
+                          remote_ip='192.101.0.4',
+                          mirror_type=self.gre)
+
+    @decorators.idempotent_id('95ef1cc1-cd57-4193-a88e-716795e39ebf')
+    def test_create_tap_mirror_non_existing_port(self):
+        not_exists = uuidutils.generate_uuid()
+        self.assertRaises(lib_exc.NotFound,
+                          self.create_tap_mirror,
+                          port_id=not_exists,
+                          directions=self.out_direction,
+                          remote_ip=self.remote_ip,
+                          mirror_type=self.gre)
+
+    @decorators.idempotent_id('123202cd-d810-4c15-bae7-26d69b24a1a4')
+    def test_multiple_mirrors_for_port(self):
+        port1 = self.create_port(self.network)
+        tap_mirror = self.create_tap_mirror(
+            port_id=port1['id'],
+            directions=self.out_direction,
+            remote_ip=self.remote_ip,
+            mirror_type=self.gre
+        )
+        self.addCleanup(self.tap_mirrors_client.delete_tap_mirror,
+                        tap_mirror['id'])
+
+        # Creation of the 2nd mirror in case the direction: tunnel_id dict
+        # is different.
+        tap_mirror2 = self.create_tap_mirror(
+            port_id=port1['id'],
+            directions={'OUT': 103},
+            remote_ip=self.remote_ip2,
+            mirror_type=self.gre
+        )
+
+        # We have a conflict if the direction: tunnel_id dict is the
+        # same
+        self.tap_mirrors_client.delete_tap_mirror(tap_mirror2['id'])
+        self.assertRaises(lib_exc.Conflict,
+                          self.create_tap_mirror,
+                          port_id=port1['id'],
+                          directions=self.out_direction,
+                          remote_ip='192.101.0.4',
+                          mirror_type=self.gre)
diff --git a/neutron_tempest_plugin/tap_as_a_service/base.py b/neutron_tempest_plugin/tap_as_a_service/base.py
index 3ddc797..99bd3cd 100644
--- a/neutron_tempest_plugin/tap_as_a_service/base.py
+++ b/neutron_tempest_plugin/tap_as_a_service/base.py
@@ -42,6 +42,14 @@
             build_interval=CONF.network.build_interval,
             build_timeout=CONF.network.build_timeout,
             **os_primary.default_params)
+        cls.tap_mirrors_client = taas_client.TapMirrorsClient(
+            os_primary.auth_provider,
+            CONF.network.catalog_type,
+            CONF.network.region or CONF.identity.region,
+            endpoint_type=CONF.network.endpoint_type,
+            build_interval=CONF.network.build_interval,
+            build_timeout=CONF.network.build_timeout,
+            **os_primary.default_params)
 
     def create_tap_service(self, **kwargs):
         body = self.tap_services_client.create_tap_service(
@@ -80,3 +88,22 @@
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
                         self.tap_flows_client.delete_tap_flow,
                         tap_flow['id'])
+
+    def create_tap_mirror(self, **kwargs):
+        body = self.tap_mirrors_client.create_tap_mirror(
+            name=data_utils.rand_name("tap_mirror"),
+            **kwargs)
+        tap_mirror = body['tap_mirror']
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.tap_mirrors_client.delete_tap_mirror,
+                        tap_mirror['id'])
+        return tap_mirror
+
+    def update_tap_mirror(self, tap_mirror_id, **kwargs):
+        body = self.tap_mirrors_client.update_tap_mirror(
+            tap_mirror_id,
+            **kwargs)
+        tap_mirror = body['tap_mirror']
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.tap_mirrors_client.delete_tap_mirror,
+                        tap_mirror['id'])
diff --git a/neutron_tempest_plugin/tap_as_a_service/scenario/manager.py b/neutron_tempest_plugin/tap_as_a_service/scenario/manager.py
index 80389c1..d3c0d8b 100644
--- a/neutron_tempest_plugin/tap_as_a_service/scenario/manager.py
+++ b/neutron_tempest_plugin/tap_as_a_service/scenario/manager.py
@@ -59,6 +59,14 @@
             build_interval=CONF.network.build_interval,
             build_timeout=CONF.network.build_timeout,
             **cls.os_primary.default_params)
+        cls.tap_mirrors_client = taas_client.TapMirrorsClient(
+            cls.os_primary.auth_provider,
+            CONF.network.catalog_type,
+            CONF.network.region or CONF.identity.region,
+            endpoint_type=CONF.network.endpoint_type,
+            build_interval=CONF.network.build_interval,
+            build_timeout=CONF.network.build_timeout,
+            **cls.os_primary.default_params)
 
     def _create_subnet(self, network, subnets_client=None,
                        namestart='subnet-smoke', **kwargs):
@@ -214,8 +222,10 @@
         return network, subnet, router
 
     def _create_server_with_floatingip(self, use_taas_cloud_image=False,
-                                       provider_net=False, **kwargs):
-        network = self.network
+                                       provider_net=False, network=None,
+                                       **kwargs):
+        if not network:
+            network = self.network
         if use_taas_cloud_image:
             image = CONF.neutron_plugin_options.advanced_image_ref
             flavor = CONF.neutron_plugin_options.advanced_image_flavor_ref
@@ -226,17 +236,26 @@
         if provider_net:
             network = self.provider_network
 
-        port = self.create_port(
-            network=network, security_groups=[self.secgroup['id']], **kwargs)
+        server_params = {
+            'flavor_ref': flavor,
+            'image_ref': image,
+            'key_name': self.keypair['name'],
+        }
+        if 'security_group' in kwargs:
+            server_params['security_groups'] = [
+                {'name': kwargs.pop('security_group')}]
+
+        if kwargs.get('port_security_enabled', None) is False:
+            port = self.create_port(network=network, **kwargs)
+        else:
+            port = self.create_port(
+                network=network, security_groups=[self.secgroup['id']],
+                **kwargs)
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
                         self.client.delete_port, port['id'])
 
-        params = {
-            'flavor_ref': flavor,
-            'image_ref': image,
-            'key_name': self.keypair['name']
-        }
-        vm = self.create_server(networks=[{'port': port['id']}], **params)
+        vm = self.create_server(networks=[{'port': port['id']}],
+                                **server_params)
         self.wait_for_server_active(vm['server'])
         self.wait_for_guest_os_ready(vm['server'])
 
@@ -291,3 +310,40 @@
             test_utils.call_and_ignore_notfound_exc,
             self.admin_network_client.remove_router_interface_with_subnet_id,
             self.router['id'], subnet_id=result['subnet']['id'])
+
+    def _check_icmp_traffic(self, monitor_client, left_client,
+                            left_port, right_port,
+                            tcpdump_cmd=None):
+        log_location = "/tmp/tcpdumplog"
+
+        right_ip = right_port['fixed_ips'][0]['ip_address']
+        left_ip = left_port['fixed_ips'][0]['ip_address']
+
+        # Run tcpdump in background
+        if tcpdump_cmd:
+            self._run_in_background(monitor_client, tcpdump_cmd % log_location)
+        else:
+            self._run_in_background(monitor_client,
+                                    "sudo tcpdump -n -nn > %s" % log_location)
+
+        # Ensure tcpdump is up and running
+        psax = monitor_client.exec_command("ps -ax")
+        self.assertIn("tcpdump", psax)
+
+        # Run traffic from left_vm to right_vm
+        LOG.debug('Check ICMP traffic: ping %s ', right_ip)
+        self.check_remote_connectivity(left_client, right_ip,
+                                       ping_count=50)
+
+        # Collect tcpdump results
+        output = self.monitor_client.exec_command("cat %s" % log_location)
+        self.assertLess(0, len(output))
+
+        looking_for = ["%s > %s: ICMP echo request" % (left_ip, right_ip),
+                       "%s > %s: ICMP echo reply" % (right_ip, left_ip)]
+
+        results = []
+        for tcpdump_line in looking_for:
+            results.append(tcpdump_line in output)
+
+        return all(results), output
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
new file mode 100644
index 0000000..d4a482c
--- /dev/null
+++ b/neutron_tempest_plugin/tap_as_a_service/scenario/test_tap_mirror.py
@@ -0,0 +1,141 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.common import utils
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils.linux import remote_client
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+from neutron_tempest_plugin.tap_as_a_service.scenario import manager
+
+CONF = config.CONF
+
+
+class TestTapMirror(manager.BaseTaasScenarioTests):
+
+    @classmethod
+    @utils.requires_ext(extension='security-group', service='network')
+    @utils.requires_ext(extension='tap-mirror', service='network')
+    def skip_checks(cls):
+        super().skip_checks()
+
+    @classmethod
+    def resource_setup(cls):
+        super().resource_setup()
+        cls.keypair = cls.create_keypair()
+        cls.secgroup = cls.create_security_group(
+            name=data_utils.rand_name('secgroup'))
+        cls.create_loginable_secgroup_rule(secgroup_id=cls.secgroup['id'])
+        cls.create_pingable_secgroup_rule(secgroup_id=cls.secgroup['id'])
+
+    @decorators.idempotent_id('d9cfca96-fa83-417a-b111-1c02f6fe2796')
+    def test_tap_mirror_connectivity(self):
+        """Test that traffic between 2 VMs mirrored to a FIP
+
+        .. code-block:: HTML
+
+           +------------+
+           | Monitor VM |
+           |   FIP      |
+           +-----+------+
+                 |
+                 |
+           +-----+------+
+           |   NetMon   |
+           +------------+
+
+           +---------------+
+           |   Net0        |
+           +---+---------+-+
+               |         |
+               |         |
+           +---+-+     +-+---+
+           | VM0 |     | VM1 |
+           +-----+     +-----+
+
+        This is a simplified scenario adapted to the CI machinery.
+        The mirroring destination should be outside of the cloud.
+        """
+
+        # Create the topology for the 2 VMs of which the traffic
+        # will be mirrored
+        self.network, self.subnet, self.router = self.create_networks()
+
+        vm0_port, vm0_fip = self._create_server_with_floatingip(
+            security_group=self.secgroup['name']
+        )
+        vm1_port, vm1_fip = self._create_server_with_floatingip(
+            security_group=self.secgroup['name']
+        )
+        vm1_ip = vm1_port['fixed_ips'][0]['ip_address']
+
+        vm0_client = remote_client.RemoteClient(
+            vm0_fip['floating_ip_address'],
+            CONF.validation.image_ssh_user,
+            pkey=self.keypair['private_key'],
+            ssh_key_type=CONF.validation.ssh_key_type)
+        vm0_client.validate_authentication()
+        vm1_client = remote_client.RemoteClient(
+            vm1_fip['floating_ip_address'],
+            CONF.validation.image_ssh_user,
+            pkey=self.keypair['private_key'],
+            ssh_key_type=CONF.validation.ssh_key_type)
+        vm1_client.validate_authentication()
+
+        self.check_remote_connectivity(vm0_client, vm1_ip, ping_count=5)
+
+        # Create the VM which will be the destination of the mirror
+        netmon, _, _ = self.create_networks()
+        _, vm_mon_fip = self._create_server_with_floatingip(
+            use_taas_cloud_image=True, network=netmon,
+            security_group=self.secgroup['name'],
+            port_security_enabled=False,
+        )
+
+        user = CONF.neutron_plugin_options.advanced_image_ssh_user
+        self.monitor_client = remote_client.RemoteClient(
+            vm_mon_fip['floating_ip_address'], user,
+            pkey=self.keypair['private_key'],
+            ssh_key_type=CONF.validation.ssh_key_type)
+        self.monitor_client.validate_authentication()
+
+        r_ip = vm_mon_fip['floating_ip_address']
+        # 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
+        # means that ICMP echo request will be in the dump.
+        # 101 as tunnel id means that we will see 0x65 as key
+        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'},
+            remote_ip=r_ip,
+            mirror_type='gre',
+        )
+        self.addCleanup(
+            test_utils.call_and_ignore_notfound_exc,
+            self.tap_mirrors_client.delete_tap_mirror,
+            tap_mirror['tap_mirror']['id']
+        )
+
+        res, output = self._check_icmp_traffic(
+            self.monitor_client,
+            vm0_client, vm0_port, vm1_port,
+            tcpdump_cmd="sudo tcpdump -vvv -n -nn proto GRE > %s")
+
+        self.assertTrue(res)
+        # GRE Key for Direction IN:101
+        self.assertIn('key=0x65', output)
+        # GRE Key for Direction OUT:102
+        self.assertIn('key=0x66', output)
diff --git a/neutron_tempest_plugin/tap_as_a_service/scenario/test_traffic_impact.py b/neutron_tempest_plugin/tap_as_a_service/scenario/test_traffic_impact.py
index 904335d..6eaeb6b 100644
--- a/neutron_tempest_plugin/tap_as_a_service/scenario/test_traffic_impact.py
+++ b/neutron_tempest_plugin/tap_as_a_service/scenario/test_traffic_impact.py
@@ -138,40 +138,6 @@
         self.right_client.validate_authentication()
         yield
 
-    def _check_icmp_traffic(self):
-        log_location = "/tmp/tcpdumplog"
-
-        right_ip = self.right_port['fixed_ips'][0]['ip_address']
-        left_ip = self.left_port['fixed_ips'][0]['ip_address']
-
-        # Run tcpdump in background
-        self._run_in_background(self.monitor_client,
-                                "sudo tcpdump -n -nn > %s" % log_location)
-
-        # Ensure tcpdump is up and running
-        psax = self.monitor_client.exec_command("ps -ax")
-        self.assertIn("tcpdump", psax)
-
-        # Run traffic from left_vm to right_vm
-        LOG.debug('Check ICMP traffic: ping %s ', right_ip)
-        # self.left_client.exec_command(
-        #     "ping -c 50 %s" % self.right_fip['floating_ip_address'])
-        self.check_remote_connectivity(self.left_client, right_ip,
-                                       ping_count=50)
-
-        # Collect tcpdump results
-        output = self.monitor_client.exec_command("cat %s" % log_location)
-        self.assertLess(0, len(output))
-
-        looking_for = ["IP %s > %s: ICMP echo request" % (left_ip, right_ip),
-                       "IP %s > %s: ICMP echo reply" % (right_ip, left_ip)]
-
-        results = []
-        for tcpdump_line in looking_for:
-            results.append(tcpdump_line in output)
-
-        return all(results)
-
     def _test_taas_connectivity(self, use_provider_net=False):
         """Ensure TAAS doesn't break connectivity
 
@@ -222,7 +188,9 @@
 
         with self._setup_topology(use_taas_cloud_image=True):
             # Check that traffic was forwarded to TAAS service
-            self.assertTrue(self._check_icmp_traffic())
+            self.assertTrue(self._check_icmp_traffic(
+                self.monitor_client, self.left_client,
+                self.left_port, self.right_port)[0])
 
     @decorators.idempotent_id('6c54d9c5-075a-4a1f-bbe6-12c3c9abf1e2')
     @testtools.skipUnless(CONF.neutron_plugin_options.advanced_image_ref,
@@ -234,7 +202,9 @@
 
         with self._setup_topology(taas=False, use_taas_cloud_image=True):
             # Check that traffic was NOT forwarded to TAAS service
-            self.assertFalse(self._check_icmp_traffic())
+            self.assertFalse(self._check_icmp_traffic(
+                self.monitor_client, self.left_client,
+                self.left_port, self.right_port)[0])
 
     @decorators.idempotent_id('fcb15ca3-ef61-11e9-9792-f45c89c47e12')
     @testtools.skipUnless(CONF.neutron_plugin_options.advanced_image_ref,
@@ -247,7 +217,9 @@
         with self._setup_topology(use_taas_cloud_image=True,
                                   provider_net=True):
             # Check that traffic was forwarded to TAAS service
-            self.assertTrue(self._check_icmp_traffic())
+            self.assertTrue(self._check_icmp_traffic(
+                self.monitor_client, self.left_client,
+                self.left_port, self.right_port)[0])
 
     @decorators.idempotent_id('6c54d9c5-075a-4a1f-bbe6-12c3c9abf1e3')
     @testtools.skipUnless(CONF.neutron_plugin_options.advanced_image_ref,
@@ -260,4 +232,6 @@
         with self._setup_topology(taas=False, use_taas_cloud_image=True,
                                   provider_net=True):
             # Check that traffic was NOT forwarded to TAAS service
-            self.assertFalse(self._check_icmp_traffic())
+            self.assertFalse(self._check_icmp_traffic(
+                self.monitor_client, self.left_client,
+                self.left_port, self.right_port)[0])
diff --git a/neutron_tempest_plugin/tap_as_a_service/services/taas_client.py b/neutron_tempest_plugin/tap_as_a_service/services/taas_client.py
index 7230cbb..8998f1c 100644
--- a/neutron_tempest_plugin/tap_as_a_service/services/taas_client.py
+++ b/neutron_tempest_plugin/tap_as_a_service/services/taas_client.py
@@ -61,3 +61,27 @@
     def list_tap_flows(self, **filters):
         uri = '/taas/tap_flows'
         return self.list_resources(uri, **filters)
+
+
+class TapMirrorsClient(base.BaseNetworkClient):
+    def create_tap_mirror(self, **kwargs):
+        uri = '/taas/tap_mirrors'
+        post_data = {'tap_mirror': kwargs}
+        return self.create_resource(uri, post_data)
+
+    def update_tap_mirror(self, tap_mirror_id, **kwargs):
+        uri = '/taas/tap_mirrors/%s' % tap_mirror_id
+        post_data = {'tap_mirror': kwargs}
+        return self.update_resource(uri, post_data)
+
+    def show_tap_mirror(self, tap_mirror_id, **fields):
+        uri = '/taas/tap_mirrors/%s' % tap_mirror_id
+        return self.show_resource(uri, **fields)
+
+    def delete_tap_mirror(self, tap_mirror_id):
+        uri = '/taas/tap_mirrors/%s' % tap_mirror_id
+        return self.delete_resource(uri)
+
+    def list_tap_mirrors(self, **filters):
+        uri = '/taas/tap_mirrors'
+        return self.list_resources(uri, **filters)
diff --git a/releasenotes/notes/remove-run-tests-opts-152c092bee1dc81d.yaml b/releasenotes/notes/remove-run-tests-opts-152c092bee1dc81d.yaml
new file mode 100644
index 0000000..bd3fb45
--- /dev/null
+++ b/releasenotes/notes/remove-run-tests-opts-152c092bee1dc81d.yaml
@@ -0,0 +1,8 @@
+---
+upgrade:
+  - |
+    The following options have been removed.
+
+    - ``[bgpvpn] run_bgpvpn_tests``
+    - ``[fwaas] run_fwaas_tests``
+    - ``[sfc] run_sfc_tests``
diff --git a/requirements.txt b/requirements.txt
index 957f186..eee78f8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 pbr>=3.0.0 # Apache-2.0
-neutron-lib>=1.25.0 # Apache-2.0
+neutron-lib>=3.21.1 # Apache-2.0
 oslo.config>=5.2.0 # Apache-2.0
 netaddr>=0.7.18 # BSD
 os-ken>=0.3.0 # Apache-2.0
@@ -12,5 +12,4 @@
 tenacity>=3.2.1 # Apache-2.0
 ddt>=1.0.1 # MIT
 testtools>=2.2.0 # MIT
-eventlet>=0.21.0 # MIT
 debtcollector>=1.2.0 # Apache-2.0
diff --git a/roles/multi-node-setup/tasks/main.yaml b/roles/multi-node-setup/tasks/main.yaml
index 043e70f..8f6772b 100644
--- a/roles/multi-node-setup/tasks/main.yaml
+++ b/roles/multi-node-setup/tasks/main.yaml
@@ -1,12 +1,12 @@
 - name: Ensure the infra bridge exists
   become: yes
-  openvswitch_bridge:
-    bridge: "{{ infra_bridge_name }}"
+  command: >-
+    ovs-vsctl --may-exist add-br {{ infra_bridge_name }}
 
 - name: Ensure the Neutron external bridge exists
   become: yes
-  openvswitch_bridge:
-    bridge: "{{ neutron_external_bridge_name }}"
+  command: >-
+    ovs-vsctl --may-exist add-br {{ neutron_external_bridge_name }}
 
 - name: Create patch port between bridges
   become: yes
diff --git a/setup.py b/setup.py
index 566d844..cd35c3c 100644
--- a/setup.py
+++ b/setup.py
@@ -13,17 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
 import setuptools
 
-# In python < 2.7.4, a lazy loading of package `pbr` will break
-# setuptools if some other modules registered functions in `atexit`.
-# solution from: http://bugs.python.org/issue15881#msg170215
-try:
-    import multiprocessing  # noqa
-except ImportError:
-    pass
-
 setuptools.setup(
     setup_requires=['pbr>=2.0.0'],
     pbr=True)
diff --git a/test-requirements.txt b/test-requirements.txt
index ebde755..ee399b8 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,8 +1,6 @@
 hacking>=6.1.0,<6.2.0 # Apache-2.0
 
-coverage>=4.4.1 # Apache-2.0
 flake8-import-order>=0.18.0,<0.19.0 # LGPLv3
-python-subunit>=1.0.0 # Apache-2.0/BSD
 oslotest>=3.2.0 # Apache-2.0
 stestr>=1.0.0 # Apache-2.0
 testtools>=2.2.0 # MIT
diff --git a/tools/customize_ubuntu_image b/tools/customize_ubuntu_image
index 34b22fe..438d97e 100755
--- a/tools/customize_ubuntu_image
+++ b/tools/customize_ubuntu_image
@@ -69,14 +69,19 @@
     bind_dir "/sys" "${mount_dir}/sys"
     if [ -f /etc/apt/sources.list ]; then
       mirror=$(grep -oP 'https?://\K[^/ ]+' /etc/apt/sources.list|head -1)
-      if [ -f ${mount_dir}/etc/apt/sources.list ]; then
-          source ${mount_dir}/etc/os-release
-          sudo tee ${mount_dir}/etc/apt/sources.list <<EOF
-          deb [ trusted=yes ] https://${mirror}/ubuntu ${UBUNTU_CODENAME} main universe
-          deb [ trusted=yes ] https://${mirror}/ubuntu ${UBUNTU_CODENAME}-updates main universe
-          deb [ trusted=yes ] https://${mirror}/ubuntu ${UBUNTU_CODENAME}-backports main universe
-          deb [ trusted=yes ] https://${mirror}/ubuntu ${UBUNTU_CODENAME}-security main universe
+      if [ -n "${mirror}" ]; then
+          if sudo test -f ${mount_dir}/etc/apt/sources.list.d/ubuntu.sources; then
+              sudo sed -Ei "s|(http[s]?://)([^/]+)|\1${mirror}|g" ${mount_dir}/etc/apt/sources.list.d/ubuntu.sources
+              sudo sed -i "/URIs:/a Trusted: yes" ${mount_dir}/etc/apt/sources.list.d/ubuntu.sources
+          elif sudo test -f ${mount_dir}/etc/apt/sources.list; then
+              source <(sudo cat ${mount_dir}/etc/os-release)
+              sudo tee ${mount_dir}/etc/apt/sources.list <<EOF
+              deb [ trusted=yes ] https://${mirror}/ubuntu ${UBUNTU_CODENAME} main universe
+              deb [ trusted=yes ] https://${mirror}/ubuntu ${UBUNTU_CODENAME}-updates main universe
+              deb [ trusted=yes ] https://${mirror}/ubuntu ${UBUNTU_CODENAME}-backports main universe
+              deb [ trusted=yes ] https://${mirror}/ubuntu ${UBUNTU_CODENAME}-security main universe
 EOF
+          fi
       fi
     fi
 
diff --git a/tox.ini b/tox.ini
index c8add2c..29be112 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,10 +1,8 @@
 [tox]
 minversion = 3.18.0
 envlist = pep8
-ignore_basepython_conflict = True
 
 [testenv]
-basepython = {env:TOX_PYTHON:python3}
 usedevelop = True
 setenv =
    VIRTUAL_ENV={envdir}
@@ -28,16 +26,6 @@
 [testenv:venv]
 commands = {posargs}
 
-[testenv:cover]
-setenv =
-    {[testenv]setenv}
-    PYTHON=coverage run --source neutron_tempest_plugin --parallel-mode
-commands =
-    stestr run --no-subunit-trace {posargs}
-    coverage combine
-    coverage html -d cover
-    coverage xml -o cover/coverage.xml
-
 [testenv:docs]
 deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
        -r{toxinidir}/requirements.txt
@@ -64,6 +52,7 @@
 # E129 visually indented line with same indent as next logical line
 # I202 Additional newline in a group of imports.
 # N530 direct neutron imports not allowed
+# N535 prevent eventlet library import
 # W504 line break after binary operator
 ignore = E126,E128,E129,I202,N530,W504
 # H106: Don't put vim configuration in source files
diff --git a/zuul.d/2023_1_jobs.yaml b/zuul.d/2023_1_jobs.yaml
index cedbc67..824c4b2 100644
--- a/zuul.d/2023_1_jobs.yaml
+++ b/zuul.d/2023_1_jobs.yaml
@@ -2,7 +2,11 @@
     name: neutron-tempest-plugin-openvswitch-2023-1
     parent: neutron-tempest-plugin-openvswitch
     nodeset: neutron-nested-virt-ubuntu-jammy
-    override-checkout: stable/2023.1
+    required-projects: &required-projects-2023-1
+      - openstack/neutron
+      - name: openstack/neutron-tempest-plugin
+        override-checkout: 2023.1-last
+      - openstack/tempest
     vars:
       network_api_extensions_openvswitch:
         - dhcp_agent_scheduler
@@ -107,7 +111,7 @@
     name: neutron-tempest-plugin-openvswitch-iptables_hybrid-2023-1
     parent: neutron-tempest-plugin-openvswitch-iptables_hybrid
     nodeset: neutron-nested-virt-ubuntu-jammy
-    override-checkout: stable/2023.1
+    required-projects: *required-projects-2023-1
     vars:
       network_api_extensions_common: *api_extensions
       network_api_extensions_openvswitch:
@@ -146,7 +150,7 @@
     name: neutron-tempest-plugin-openvswitch-enforce-scope-new-defaults-2023-1
     parent: neutron-tempest-plugin-openvswitch-2023-1
     nodeset: neutron-nested-virt-ubuntu-jammy
-    override-checkout: stable/2023.1
+    required-projects: *required-projects-2023-1
     vars:
       devstack_localrc:
         # Enabeling the scope and new defaults for services.
@@ -164,7 +168,7 @@
     name: neutron-tempest-plugin-linuxbridge-2023-1
     parent: neutron-tempest-plugin-linuxbridge
     nodeset: neutron-nested-virt-ubuntu-jammy
-    override-checkout: stable/2023.1
+    required-projects: *required-projects-2023-1
     vars:
       network_api_extensions_common: *api_extensions
       network_api_extensions_linuxbridge:
@@ -207,7 +211,7 @@
     name: neutron-tempest-plugin-ovn-2023-1
     parent: neutron-tempest-plugin-ovn
     nodeset: neutron-nested-virt-ubuntu-jammy
-    override-checkout: stable/2023.1
+    required-projects: *required-projects-2023-1
     vars:
       network_api_extensions_ovn:
         - vlan-transparent
@@ -216,6 +220,15 @@
           (^neutron_tempest_plugin.scenario)|\
           (^tempest.api.compute.servers.test_attach_interfaces)|\
           (^tempest.api.compute.servers.test_multiple_create)"
+      # NOTE(liushy): This branch of Neutron does not support
+      # the address_group feature for the OVN driver.
+      # NOTE(ralonsoh): The advance image used "ubuntu-22.04-minimal" has a reported issue (LP#2110520)
+      # with the IGMP report messages. Because of that and because ML2/OVN has "igmp_snooping_enable"
+      # set, the receiver VM cannot subscribe to the IGMP group nor receive any IGMP message.
+      tempest_exclude_regex: "\
+          (^neutron_tempest_plugin.scenario.test_security_groups.StatefulNetworkSecGroupTest.test_remote_group_and_remote_address_group)|\
+          (^neutron_tempest_plugin.scenario.test_security_groups.StatelessNetworkSecGroupIPv4Test.test_remote_group_and_remote_address_group)|\
+          (^neutron_tempest_plugin.scenario.test_multicast.MulticastTestIPv4.test_multicast_between_vms_on_same_network)"
       devstack_localrc:
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_ovn) | join(',') }}"
         NEUTRON_DEPLOY_MOD_WSGI: false
@@ -236,7 +249,7 @@
     name: neutron-tempest-plugin-dvr-multinode-scenario-2023-1
     parent: neutron-tempest-plugin-dvr-multinode-scenario
     nodeset: openstack-two-node-jammy
-    override-checkout: stable/2023.1
+    required-projects: *required-projects-2023-1
     vars:
       network_api_extensions_common: *api_extensions
       network_api_extensions_dvr:
@@ -250,7 +263,7 @@
     name: neutron-tempest-plugin-designate-scenario-2023-1
     parent: neutron-tempest-plugin-designate-scenario
     nodeset: neutron-nested-virt-ubuntu-jammy
-    override-checkout: stable/2023.1
+    required-projects: *required-projects-2023-1
     vars:
       network_api_extensions_common: *api_extensions
       devstack_localrc:
@@ -260,7 +273,7 @@
     name: neutron-tempest-plugin-sfc-2023-1
     parent: neutron-tempest-plugin-sfc
     nodeset: openstack-single-node-jammy
-    override-checkout: stable/2023.1
+    required-projects: *required-projects-2023-1
     vars:
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
@@ -269,7 +282,7 @@
     name: neutron-tempest-plugin-bgpvpn-bagpipe-2023-1
     parent: neutron-tempest-plugin-bgpvpn-bagpipe
     nodeset: openstack-single-node-jammy
-    override-checkout: stable/2023.1
+    required-projects: *required-projects-2023-1
     vars:
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
@@ -278,7 +291,7 @@
     name: neutron-tempest-plugin-dynamic-routing-2023-1
     parent: neutron-tempest-plugin-dynamic-routing
     nodeset: openstack-single-node-jammy
-    override-checkout: stable/2023.1
+    required-projects: *required-projects-2023-1
     vars:
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
@@ -293,7 +306,7 @@
     name: neutron-tempest-plugin-fwaas-2023-1
     parent: neutron-tempest-plugin-fwaas-openvswitch
     nodeset: openstack-single-node-jammy
-    override-checkout: stable/2023.1
+    required-projects: *required-projects-2023-1
     vars:
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
@@ -302,7 +315,7 @@
     name: neutron-tempest-plugin-vpnaas-2023-1
     parent: neutron-tempest-plugin-vpnaas
     nodeset: openstack-single-node-jammy
-    override-checkout: stable/2023.1
+    required-projects: *required-projects-2023-1
     vars:
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
@@ -311,7 +324,11 @@
     name: neutron-tempest-plugin-tap-as-a-service-2023-1
     parent: neutron-tempest-plugin-tap-as-a-service
     nodeset: openstack-single-node-jammy
-    override-checkout: stable/2023.1
+    required-projects: *required-projects-2023-1
     vars:
+      network_api_extensions_common: *api_extensions
+      network_api_extensions_tempest:
+        - taas
+        - taas-vlan-filter
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
diff --git a/zuul.d/2024_1_jobs.yaml b/zuul.d/2024_1_jobs.yaml
index 674d1ab..4170be7 100644
--- a/zuul.d/2024_1_jobs.yaml
+++ b/zuul.d/2024_1_jobs.yaml
@@ -271,6 +271,7 @@
 - job:
     name: neutron-tempest-plugin-dynamic-routing-2024-1
     parent: neutron-tempest-plugin-dynamic-routing
+    voting: false
     nodeset: openstack-single-node-jammy
     override-checkout: stable/2024.1
     vars:
@@ -301,5 +302,9 @@
     nodeset: openstack-single-node-jammy
     override-checkout: stable/2024.1
     vars:
+      network_api_extensions_common: *api_extensions
+      network_api_extensions_tempest:
+        - taas
+        - taas-vlan-filter
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
diff --git a/zuul.d/2024_2_jobs.yaml b/zuul.d/2024_2_jobs.yaml
index e19767c..d54cf35 100644
--- a/zuul.d/2024_2_jobs.yaml
+++ b/zuul.d/2024_2_jobs.yaml
@@ -287,3 +287,10 @@
     parent: neutron-tempest-plugin-tap-as-a-service
     nodeset: openstack-single-node-jammy
     override-checkout: stable/2024.2
+    vars:
+      network_api_extensions_common: *api_extensions
+      network_api_extensions_tempest:
+        - taas
+        - taas-vlan-filter
+      devstack_localrc:
+        NEUTRON_DEPLOY_MOD_WSGI: false
diff --git a/zuul.d/2025_1_jobs.yaml b/zuul.d/2025_1_jobs.yaml
index b34db9f..793d8e3 100644
--- a/zuul.d/2025_1_jobs.yaml
+++ b/zuul.d/2025_1_jobs.yaml
@@ -97,6 +97,7 @@
         - 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:
@@ -183,6 +184,18 @@
     name: neutron-tempest-plugin-ovn-ubuntu-jammy-2025-1
     parent: neutron-tempest-plugin-ovn-2025-1
     nodeset: neutron-nested-virt-ubuntu-jammy
+    vars:
+      # NOTE(ralonsoh): The advance image used "ubuntu-22.04-minimal" has a reported issue (LP#2110520)
+      # with the IGMP report messages. Because of that and because ML2/OVN has "igmp_snooping_enable"
+      # set, the receiver VM cannot subscribe to the IGMP group nor receive any IGMP message.
+      # NOTE(ykarel) Known issue in OVN version included in OVN jammy
+      # https://bugs.launchpad.net/neutron/+bug/2112620, skipping random failing tests
+      # neutron_tempest_plugin.scenario.test_vlan_transparency
+      # neutron_tempest_plugin.scenario.test_security_groups
+      tempest_exclude_regex: "\
+        (^neutron_tempest_plugin.scenario.test_multicast.MulticastTestIPv4.test_multicast_between_vms_on_same_network)|\
+        (^neutron_tempest_plugin.scenario.test_vlan_transparency)|\
+        (^neutron_tempest_plugin.scenario.test_security_groups)"
 
 - job:
     name: neutron-tempest-plugin-dvr-multinode-scenario-2025-1
@@ -240,3 +253,9 @@
     parent: neutron-tempest-plugin-tap-as-a-service
     nodeset: openstack-single-node-noble
     override-checkout: stable/2025.1
+
+- job:
+    name: neutron-tempest-plugin-tap-as-a-service-ovn-2025-1
+    parent: neutron-tempest-plugin-tap-as-a-service-ovn
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2025.1
diff --git a/zuul.d/2025_2_jobs.yaml b/zuul.d/2025_2_jobs.yaml
new file mode 100644
index 0000000..516e993
--- /dev/null
+++ b/zuul.d/2025_2_jobs.yaml
@@ -0,0 +1,239 @@
+- job:
+    name: neutron-tempest-plugin-openvswitch-2025-2
+    parent: neutron-tempest-plugin-openvswitch
+    nodeset: neutron-nested-virt-ubuntu-noble
+    override-checkout: stable/2025.2
+    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-2025-2
+    parent: neutron-tempest-plugin-openvswitch-iptables_hybrid
+    nodeset: neutron-nested-virt-ubuntu-noble
+    override-checkout: stable/2025.2
+    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-2025-2
+    parent: neutron-tempest-plugin-ovn-2025-2
+    nodeset: neutron-nested-virt-ubuntu-noble
+    override-checkout: stable/2025.2
+    vars:
+      devstack_localrc:
+        NEUTRON_ENFORCE_SCOPE: false
+
+- job:
+    name: neutron-tempest-plugin-ovn-2025-2
+    parent: neutron-tempest-plugin-ovn
+    nodeset: neutron-nested-virt-ubuntu-noble
+    override-checkout: stable/2025.2
+    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-2025-2
+    parent: neutron-tempest-plugin-dvr-multinode-scenario
+    nodeset: openstack-two-node-noble
+    override-checkout: stable/2025.2
+    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-2025-2
+    parent: neutron-tempest-plugin-designate-scenario
+    nodeset: neutron-nested-virt-ubuntu-noble
+    override-checkout: stable/2025.2
+    vars:
+      network_api_extensions_common: *api_extensions
+
+- job:
+    name: neutron-tempest-plugin-sfc-2025-2
+    parent: neutron-tempest-plugin-sfc
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2025.2
+
+- job:
+    name: neutron-tempest-plugin-bgpvpn-bagpipe-2025-2
+    parent: neutron-tempest-plugin-bgpvpn-bagpipe
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2025.2
+
+- job:
+    name: neutron-tempest-plugin-dynamic-routing-2025-2
+    parent: neutron-tempest-plugin-dynamic-routing
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2025.2
+
+- job:
+    name: neutron-tempest-plugin-fwaas-2025-2
+    parent: neutron-tempest-plugin-fwaas-openvswitch
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2025.2
+
+- job:
+    name: neutron-tempest-plugin-vpnaas-2025-2
+    parent: neutron-tempest-plugin-vpnaas
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2025.2
+
+- job:
+    name: neutron-tempest-plugin-tap-as-a-service-2025-2
+    parent: neutron-tempest-plugin-tap-as-a-service
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2025.2
+
+- job:
+    name: neutron-tempest-plugin-tap-as-a-service-ovn-2025-2
+    parent: neutron-tempest-plugin-tap-as-a-service-ovn
+    nodeset: openstack-single-node-noble
+    override-checkout: stable/2025.2
diff --git a/zuul.d/master_jobs.yaml b/zuul.d/master_jobs.yaml
index 232f0a1..3aee414 100644
--- a/zuul.d/master_jobs.yaml
+++ b/zuul.d/master_jobs.yaml
@@ -23,7 +23,6 @@
           (^tempest.api.compute.servers.test_attach_interfaces)|\
           (^tempest.api.compute.servers.test_multiple_create)"
       devstack_localrc:
-        USE_PYTHON3: true
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_tempest) | join(',') }}"
         PHYSICAL_NETWORK: public
         IMAGE_URLS: https://cloud-images.ubuntu.com/minimal/releases/jammy/release/ubuntu-22.04-minimal-cloudimg-amd64.img
@@ -35,8 +34,6 @@
         ADVANCED_INSTANCE_USER: ubuntu
         CUSTOMIZE_IMAGE: true
         BUILD_TIMEOUT: 784
-        # TODO(lucasagomes): Re-enable MOD_WSGI after
-        # https://bugs.launchpad.net/neutron/+bug/1912359 is implemented
         NEUTRON_DEPLOY_MOD_WSGI: true
       devstack_plugins:
         neutron: https://opendev.org/openstack/neutron.git
@@ -128,6 +125,7 @@
         - trunk
         - trunk-details
         - uplink-status-propagation
+        - uplink-status-propagation-updatable
       devstack_services:
         tempest: true
         neutron-dns: true
@@ -191,6 +189,7 @@
               provider_net_base_segm_id: 1
               snat_rules_apply_to_nested_networks: true
     irrelevant-files:
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -212,6 +211,7 @@
       - ^neutron_tempest_plugin/scenario/.*$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*functional.*$
       - ^vagrant/.*$
       # Ignore everything except for zuul.d/project.yaml
@@ -274,6 +274,7 @@
               firewall_driver: openvswitch
               snat_rules_apply_to_nested_networks: true
     irrelevant-files:
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -308,6 +309,7 @@
       - ^neutron_tempest_plugin/services/bgp/.*$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*dvr-multinode.*$
       - ^playbooks/.*dynamic-routing.*$
       - ^playbooks/.*functional.*$
@@ -381,6 +383,7 @@
               firewall_driver: iptables_hybrid
               snat_rules_apply_to_nested_networks: true
     irrelevant-files:
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -416,6 +419,7 @@
       - ^neutron_tempest_plugin/services/bgp/.*$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*dvr-multinode.*$
       - ^playbooks/.*dynamic-routing.*$
       - ^playbooks/.*functional.*$
@@ -540,6 +544,7 @@
               firewall_driver: iptables
               snat_rules_apply_to_nested_networks: true
     irrelevant-files:
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -576,6 +581,7 @@
       - ^neutron_tempest_plugin/services/bgp/.*$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*dvr-multinode.*$
       - ^playbooks/.*dynamic-routing.*$
       - ^playbooks/.*functional.*$
@@ -679,6 +685,7 @@
         '/var/log/openvswitch': 'logs'
         '/var/lib/ovn': 'logs'
     irrelevant-files:
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -718,6 +725,7 @@
       - ^neutron_tempest_plugin/services/bgp/.*$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*dvr-multinode.*$
       - ^playbooks/.*dynamic-routing.*$
       - ^playbooks/.*functional.*$
@@ -770,7 +778,6 @@
         - dhcp_agent_scheduler
         - dvr
       devstack_localrc:
-        USE_PYTHON3: true
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_dvr) | join(',') }}"
         PHYSICAL_NETWORK: default
         CIRROS_VERSION: 0.6.3
@@ -910,7 +917,6 @@
           s-object: false
           s-proxy: false
         devstack_localrc:
-          USE_PYTHON3: true
           Q_AGENT: openvswitch
           Q_ML2_TENANT_NETWORK_TYPE: vxlan
           Q_ML2_PLUGIN_MECHANISM_DRIVERS: openvswitch
@@ -933,6 +939,7 @@
               agent:
                 availability_zone: nova
     irrelevant-files:
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -1002,6 +1009,7 @@
         - neutron-tempest-plugin
       tempest_test_regex: ^neutron_tempest_plugin\.scenario\.test_dns_integration
     irrelevant-files:
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -1022,13 +1030,13 @@
       - ^neutron/privileged/.*$
       - ^neutron/plugins/ml2/drivers/.*$
       - ^neutron/scheduler/.*$
-      - ^neutron/services/.*$
       - ^neutron_tempest_plugin/api/test_.*$
       - ^neutron_tempest_plugin/api/admin/test_.*$
       - ^neutron_tempest_plugin/(bgpvpn|fwaas|neutron_dynamic_routing|sfc|tap_as_a_service|vpnaas).*$
       - ^neutron_tempest_plugin/services/bgp/.*$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*dvr-multinode.*$
       - ^playbooks/.*dynamic-routing.*$
       - ^playbooks/.*functional.*$
@@ -1108,6 +1116,7 @@
       # https://bugs.launchpad.net/networking-sfc/+bug/1660366
       tempest_concurrency: 1
     irrelevant-files:
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -1131,6 +1140,7 @@
       - ^plugin.spec$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*dvr-multinode.*$
       - ^playbooks/.*dynamic-routing.*$
       - ^playbooks/.*functional.*$
@@ -1180,6 +1190,7 @@
         networking-bgpvpn: https://git.openstack.org/openstack/networking-bgpvpn
         networking-bagpipe: https://git.openstack.org/openstack/networking-bagpipe
     irrelevant-files:
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -1203,6 +1214,7 @@
       - ^plugin.spec$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*dvr-multinode.*$
       - ^playbooks/.*dynamic-routing.*$
       - ^playbooks/.*functional.*$
@@ -1239,6 +1251,7 @@
       tempest_concurrency: 1
       tempest_test_regex: ^neutron_tempest_plugin\.neutron_dynamic_routing
     irrelevant-files:
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -1261,6 +1274,7 @@
       - ^plugin.spec$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*dvr-multinode.*$
       - ^playbooks/.*functional.*$
       - ^playbooks/.*linuxbridge.*$
@@ -1293,6 +1307,7 @@
             fwaas:
               driver: ovn
     irrelevant-files: &fwaas_irrelevant_files
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -1316,6 +1331,7 @@
       - ^plugin.spec$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*dvr-multinode.*$
       - ^playbooks/.*dynamic-routing.*$
       - ^playbooks/.*functional.*$
@@ -1397,6 +1413,7 @@
         q-metering: true
         q-l3: true
     irrelevant-files:
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -1420,6 +1437,7 @@
       - ^plugin.spec$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*dvr-multinode.*$
       - ^playbooks/.*dynamic-routing.*$
       - ^playbooks/.*functional.*$
@@ -1461,6 +1479,7 @@
               skip_6in6_tests: true
 
     irrelevant-files:
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -1484,6 +1503,7 @@
       - ^plugin.spec$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*dvr-multinode.*$
       - ^playbooks/.*dynamic-routing.*$
       - ^playbooks/.*functional.*$
@@ -1512,12 +1532,14 @@
       network_api_extensions_tempest:
         - taas
         - taas-vlan-filter
+        - tap-mirror
       devstack_localrc:
         NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_tempest) | join(',') }}"
         BUILD_TIMEOUT: 784
         Q_AGENT: openvswitch
         Q_ML2_TENANT_NETWORK_TYPE: vxlan,vlan
         Q_ML2_PLUGIN_MECHANISM_DRIVERS: openvswitch
+        OVS_BRANCH: "branch-3.3"
       devstack_local_conf:
         post-config:
           /$NEUTRON_CORE_PLUGIN_CONF:
@@ -1569,10 +1591,12 @@
         q-svc: true
         neutron: true
         taas: true
+        tap_mirror: true
         taas_openvswitch_agent: true
         tempest: true
         dstat: true
-    irrelevant-files:
+    irrelevant-files: &taas_irrelevant_files
+      - ^\.pre-commit-config\.yaml$
       - ^\.pylintrc$
       - ^(test-|)requirements.txt$
       - ^lower-constraints.txt$
@@ -1596,6 +1620,7 @@
       - ^plugin.spec$
       - ^rally-jobs/.*$
       - ^roles/.*functional.*$
+      - ^roles/.*multi-node-setup.*$
       - ^playbooks/.*dvr-multinode.*$
       - ^playbooks/.*dynamic-routing.*$
       - ^playbooks/.*functional.*$
@@ -1604,3 +1629,52 @@
       # Ignore everything except for zuul.d/project.yaml
       - ^zuul.d/.*_jobs\.yaml$
       - ^zuul.d/base-nested-switch.yaml
+
+- job:
+    name: neutron-tempest-plugin-tap-as-a-service-ovn
+    parent: neutron-tempest-plugin-base
+    description: |
+      Test tap-mirrors with OVN
+    roles:
+      - zuul: openstack/devstack
+    required-projects:
+      - openstack/neutron
+      - openstack/neutron-tempest-plugin
+      - openstack/tap-as-a-service
+      - openstack/tempest
+    vars:
+      tempest_concurrency: 4
+      tempest_test_regex: ^neutron_tempest_plugin\.tap_as_a_service
+      tox_envlist: all
+      network_api_extensions_tempest:
+        - taas
+        - tap-mirror
+      devstack_localrc:
+        Q_AGENT: ovn
+        NETWORK_API_EXTENSIONS: "{{ (network_api_extensions_common + network_api_extensions_tempest) | join(',') }}"
+        BUILD_TIMEOUT: 784
+        TAAS_SERVICE_DRIVER: "TAAS:TAAS:neutron_taas.services.taas.service_drivers.ovn.taas_ovn.TaasOvnDriver:default"
+        # mirroring is available from OVN 22.12.0 and use OVS 3.2.1 that also have this
+        # feature and builds with the above OVN
+        OVN_BRANCH: "branch-24.03"
+        OVS_BRANCH: "branch-3.3"
+      devstack_local_conf:
+        test-config:
+          $TEMPEST_CONFIG:
+            neutron_plugin_options:
+              image_is_advanced: true
+              advanced_image_flavor_ref: d1
+            taas:
+              provider_physical_network: public
+              provider_segmentation_id: 100
+            image_feature_enabled:
+              api_v2: true
+      devstack_plugins:
+        neutron: git://opendev.org/openstack/neutron.git
+        neutron-tempest-plugin: https://opendev.org/openstack/neutron-tempest-plugin.git
+        tap-as-a-service: git://opendev.org/openstack/tap-as-a-service.git
+      devstack_services:
+        tap_mirror: true
+        taas: true
+        tempest: true
+    irrelevant-files: *taas_irrelevant_files
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index f9f70dd..5841ee5 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -193,55 +193,71 @@
       jobs:
         - neutron-tempest-plugin-dvr-multinode-scenario-2025-1
 
+- project-template:
+    name: neutron-tempest-plugin-jobs-2025-2
+    check:
+      jobs:
+        - neutron-tempest-plugin-openvswitch-2025-2
+        - neutron-tempest-plugin-openvswitch-iptables_hybrid-2025-2
+        - neutron-tempest-plugin-ovn-2025-2
+        - neutron-tempest-plugin-designate-scenario-2025-2
+    gate:
+      jobs:
+        - neutron-tempest-plugin-ovn-2025-2
+    #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-2025-2
+
 - project:
     templates:
       - build-openstack-docs-pti
       - neutron-tempest-plugin-jobs
-      - neutron-tempest-plugin-jobs-2024-1
       - neutron-tempest-plugin-jobs-2024-2
       - neutron-tempest-plugin-jobs-2025-1
+      - neutron-tempest-plugin-jobs-2025-2
       - check-requirements
       - tempest-plugin-jobs
       - release-notes-jobs-python3
     check:
       jobs:
         - neutron-tempest-plugin-sfc
-        - neutron-tempest-plugin-sfc-2024-1
         - neutron-tempest-plugin-sfc-2024-2
         - neutron-tempest-plugin-sfc-2025-1
+        - neutron-tempest-plugin-sfc-2025-2
         - neutron-tempest-plugin-bgpvpn-bagpipe
-        - neutron-tempest-plugin-bgpvpn-bagpipe-2024-1
         - 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:
-            # 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-2024-1
+            voting: false  # See LP#2130631
         - neutron-tempest-plugin-dynamic-routing-2024-2
         - neutron-tempest-plugin-dynamic-routing-2025-1
+        - neutron-tempest-plugin-dynamic-routing-2025-2
         - neutron-tempest-plugin-fwaas-ovn
         - neutron-tempest-plugin-fwaas-openvswitch
-        - neutron-tempest-plugin-fwaas-2024-1
         - neutron-tempest-plugin-fwaas-2024-2
         - neutron-tempest-plugin-fwaas-2025-1
+        - neutron-tempest-plugin-fwaas-2025-2
         - neutron-tempest-plugin-vpnaas
         - neutron-tempest-plugin-vpnaas-ovn
-        - neutron-tempest-plugin-vpnaas-2024-1
         - neutron-tempest-plugin-vpnaas-2024-2
         - neutron-tempest-plugin-vpnaas-2025-1
+        - neutron-tempest-plugin-vpnaas-2025-2
         - neutron-tempest-plugin-tap-as-a-service
-        - neutron-tempest-plugin-tap-as-a-service-2024-1
+        - 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-ovn-2025-1
+        - neutron-tempest-plugin-tap-as-a-service-ovn-2025-2
 
     gate:
       jobs:
         - neutron-tempest-plugin-sfc
         - neutron-tempest-plugin-bgpvpn-bagpipe
+        - neutron-tempest-plugin-dynamic-routing:
+            voting: false  # See LP#2130631
         - neutron-tempest-plugin-fwaas-ovn
         - neutron-tempest-plugin-vpnaas-ovn
diff --git a/zuul.d/xena_jobs.yaml b/zuul.d/xena_jobs.yaml
index 847c611..7d58efa 100644
--- a/zuul.d/xena_jobs.yaml
+++ b/zuul.d/xena_jobs.yaml
@@ -284,5 +284,8 @@
     required-projects: *required-projects-xena
     vars:
       network_api_extensions_common: *api_extensions
+      network_api_extensions_tempest:
+        - taas
+        - taas-vlan-filter
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
diff --git a/zuul.d/yoga_jobs.yaml b/zuul.d/yoga_jobs.yaml
index 72b0492..72a659e 100644
--- a/zuul.d/yoga_jobs.yaml
+++ b/zuul.d/yoga_jobs.yaml
@@ -185,13 +185,17 @@
           (^tempest.api.compute.servers.test_multiple_create)"
       # NOTE(ralonsoh): tests disabled because of https://bugs.launchpad.net/neutron/+bug/2082070
       # NOTE(ralonsoh): ``NetworkWritableMtuTest`` excluded because of https://bugs.launchpad.net/neutron/+bug/2082344
+      # NOTE(liushy): This branch of Neutron does not support
+      # the address_group feature for the OVN driver.
       tempest_exclude_regex: "\
           (neutron_tempest_plugin.scenario.test_security_groups.StatelessNetworkSecGroupIPv4Test.test_connectivity_between_vms_using_different_sec_groups)|\
           (neutron_tempest_plugin.scenario.test_security_groups.StatelessNetworkSecGroupIPv4Test.test_packets_of_any_connection_state_can_reach_dest)|\
           (neutron_tempest_plugin.scenario.test_vlan_transparency.VlanTransparencyTest.test_vlan_transparent_allowed_address_pairs)|\
           (neutron_tempest_plugin.scenario.test_vlan_transparency.VlanTransparencyTest.test_vlan_transparent_port_sec_disabled)|\
           (neutron_tempest_plugin.scenario.test_mtu.NetworkWritableMtuTest.*)|\
-          (^neutron_tempest_plugin.scenario.test_multicast.MulticastTestIPv4.test_multicast_between_vms_on_same_network)"
+          (^neutron_tempest_plugin.scenario.test_multicast.MulticastTestIPv4.test_multicast_between_vms_on_same_network)|\
+          (neutron_tempest_plugin.scenario.test_security_groups.StatefulNetworkSecGroupTest.test_remote_group_and_remote_address_group)|\
+          (neutron_tempest_plugin.scenario.test_security_groups.StatelessNetworkSecGroupIPv4Test.test_remote_group_and_remote_address_group)"
       network_api_extensions: *api_extensions
       network_api_extensions_ovn:
         - vlan-transparent
@@ -302,5 +306,8 @@
     required-projects: *required-projects-yoga
     vars:
       network_api_extensions_common: *api_extensions
+      network_api_extensions_tempest:
+        - taas
+        - taas-vlan-filter
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false
diff --git a/zuul.d/zed_jobs.yaml b/zuul.d/zed_jobs.yaml
index 72ccfdb..9c40f76 100644
--- a/zuul.d/zed_jobs.yaml
+++ b/zuul.d/zed_jobs.yaml
@@ -189,13 +189,17 @@
           (^tempest.api.compute.servers.test_multiple_create)"
       # NOTE(ralonsoh): tests disabled because of https://bugs.launchpad.net/neutron/+bug/2082070
       # NOTE(ralonsoh): ``NetworkWritableMtuTest`` excluded because of https://bugs.launchpad.net/neutron/+bug/2082344
+      # NOTE(liushy): This branch of Neutron does not support
+      # the address_group feature for the OVN driver.
       tempest_exclude_regex: "\
           (neutron_tempest_plugin.scenario.test_security_groups.StatelessNetworkSecGroupIPv4Test.test_connectivity_between_vms_using_different_sec_groups)|\
           (neutron_tempest_plugin.scenario.test_security_groups.StatelessNetworkSecGroupIPv4Test.test_packets_of_any_connection_state_can_reach_dest)|\
           (neutron_tempest_plugin.scenario.test_vlan_transparency.VlanTransparencyTest.test_vlan_transparent_allowed_address_pairs)|\
           (neutron_tempest_plugin.scenario.test_vlan_transparency.VlanTransparencyTest.test_vlan_transparent_port_sec_disabled)|\
           (neutron_tempest_plugin.scenario.test_mtu.NetworkWritableMtuTest.*)|\
-          (^neutron_tempest_plugin.scenario.test_multicast.MulticastTestIPv4.test_multicast_between_vms_on_same_network)"
+          (^neutron_tempest_plugin.scenario.test_multicast.MulticastTestIPv4.test_multicast_between_vms_on_same_network)|\
+          (neutron_tempest_plugin.scenario.test_security_groups.StatefulNetworkSecGroupTest.test_remote_group_and_remote_address_group)|\
+          (neutron_tempest_plugin.scenario.test_security_groups.StatelessNetworkSecGroupIPv4Test.test_remote_group_and_remote_address_group)"
       network_api_extensions: *api_extensions
       network_api_extensions_ovn:
         - vlan-transparent
@@ -323,5 +327,8 @@
     required-projects: *required-projects-zed
     vars:
       network_api_extensions_common: *api_extensions
+      network_api_extensions_tempest:
+        - taas
+        - taas-vlan-filter
       devstack_localrc:
         NEUTRON_DEPLOY_MOD_WSGI: false