Merge "Specify network type in ``NetworkSegmentRangeTestJson``"
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/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 e87219b..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'])
@@ -353,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)
 
 
@@ -362,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 6e7e2c5..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.",
diff --git a/neutron_tempest_plugin/common/utils.py b/neutron_tempest_plugin/common/utils.py
index 62191bf..5d6a26a 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):
diff --git a/neutron_tempest_plugin/config.py b/neutron_tempest_plugin/config.py
index 880d3a6..1f5c34c 100644
--- a/neutron_tempest_plugin/config.py
+++ b/neutron_tempest_plugin/config.py
@@ -144,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
diff --git a/neutron_tempest_plugin/scenario/base.py b/neutron_tempest_plugin/scenario/base.py
index d299c47..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
@@ -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/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/requirements.txt b/requirements.txt
index 957f186..478c1fa 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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/tox.ini b/tox.ini
index 21574f4..5f4d1e9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -62,6 +62,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/2025_1_jobs.yaml b/zuul.d/2025_1_jobs.yaml
index f0c1da5..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:
diff --git a/zuul.d/master_jobs.yaml b/zuul.d/master_jobs.yaml
index 4c2e400..4484768 100644
--- a/zuul.d/master_jobs.yaml
+++ b/zuul.d/master_jobs.yaml
@@ -34,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
@@ -127,6 +125,7 @@
         - trunk
         - trunk-details
         - uplink-status-propagation
+        - uplink-status-propagation-updatable
       devstack_services:
         tempest: true
         neutron-dns: true
@@ -1024,7 +1023,6 @@
       - ^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).*$
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 1720745..69a380c 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -213,14 +213,7 @@
         - neutron-tempest-plugin-bgpvpn-bagpipe-2024-1
         - neutron-tempest-plugin-bgpvpn-bagpipe-2024-2
         - neutron-tempest-plugin-bgpvpn-bagpipe-2025-1
-        - 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
         - neutron-tempest-plugin-dynamic-routing-2024-1
         - neutron-tempest-plugin-dynamic-routing-2024-2
         - neutron-tempest-plugin-dynamic-routing-2025-1
@@ -245,5 +238,6 @@
       jobs:
         - neutron-tempest-plugin-sfc
         - neutron-tempest-plugin-bgpvpn-bagpipe
+        - neutron-tempest-plugin-dynamic-routing
         - neutron-tempest-plugin-fwaas-ovn
         - neutron-tempest-plugin-vpnaas-ovn