From 5cccd2112fad736967d70768e1d6ac60c7d47415 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Wed, 11 Jun 2025 08:17:22 +0000 Subject: [PATCH] Allow empty gateway IP in subnets from subnet pools When a subnet is created from a subnet pool, now is possible to undefine the gateway IP. The new subnet created will have this value assigned to None. $ openstack subnet create --subnet-pool shared-default-subnetpool-v4 \ --network net14 snet14 --gateway None --format value \ --column gateway_ip None Closes-Bug: #2112453 Change-Id: I3bdd260f0f6b0259ff15cfe16a111bfe93b40749 --- neutron/ipam/requests.py | 36 ++++++++++++--- neutron/ipam/subnet_alloc.py | 18 +++++--- .../tests/common/test_db_base_plugin_v2.py | 45 +++++++++++++++++++ neutron/tests/unit/ipam/test_subnet_alloc.py | 31 +++++++++++++ 4 files changed, 118 insertions(+), 12 deletions(-) diff --git a/neutron/ipam/requests.py b/neutron/ipam/requests.py index 86ae1523d24..dde70245933 100644 --- a/neutron/ipam/requests.py +++ b/neutron/ipam/requests.py @@ -36,7 +36,8 @@ class SubnetRequest(metaclass=abc.ABCMeta): instantiated on its own. Rather, a subclass of this class should be used. """ def __init__(self, tenant_id, subnet_id, - gateway_ip=None, allocation_pools=None): + gateway_ip=None, allocation_pools=None, + set_gateway_ip=True): """Initialize and validate :param tenant_id: The tenant id who will own the subnet @@ -50,10 +51,16 @@ class SubnetRequest(metaclass=abc.ABCMeta): of this range if specifically requested. :type allocation_pools: A list of netaddr.IPRange. None if not specified. + :param set_gateway_ip: in case the ``gateway_ip`` value is not defined + (None), the IPAM module will set an IP address within the range of + the subnet CIDR. If ``set_gateway_ip`` is unset, no IP address will + be assigned. + :type set_gateway_ip: boolean """ self._tenant_id = tenant_id self._subnet_id = subnet_id self._gateway_ip = None + self._set_gateway_ip = set_gateway_ip self._allocation_pools = None if gateway_ip is not None: @@ -97,6 +104,10 @@ class SubnetRequest(metaclass=abc.ABCMeta): def gateway_ip(self): return self._gateway_ip + @property + def set_gateway_ip(self): + return self._set_gateway_ip + @property def allocation_pools(self): return self._allocation_pools @@ -144,7 +155,8 @@ class AnySubnetRequest(SubnetRequest): constants.IPv6: '::'} def __init__(self, tenant_id, subnet_id, version, prefixlen, - gateway_ip=None, allocation_pools=None): + gateway_ip=None, allocation_pools=None, + set_gateway_ip=True): """Initialize AnySubnetRequest :param version: Either constants.IPv4 or constants.IPv6 @@ -156,7 +168,9 @@ class AnySubnetRequest(SubnetRequest): tenant_id=tenant_id, subnet_id=subnet_id, gateway_ip=gateway_ip, - allocation_pools=allocation_pools) + allocation_pools=allocation_pools, + set_gateway_ip=set_gateway_ip, + ) net = netaddr.IPNetwork(self.WILDCARDS[version] + '/' + str(prefixlen)) self._validate_with_subnet(net) @@ -176,7 +190,8 @@ class SpecificSubnetRequest(SubnetRequest): blueprints. """ def __init__(self, tenant_id, subnet_id, subnet_cidr, - gateway_ip=None, allocation_pools=None): + gateway_ip=None, allocation_pools=None, + set_gateway_ip=True): """Initialize SpecificSubnetRequest :param subnet: The subnet requested. Can be IPv4 or IPv6. However, @@ -188,7 +203,9 @@ class SpecificSubnetRequest(SubnetRequest): tenant_id=tenant_id, subnet_id=subnet_id, gateway_ip=gateway_ip, - allocation_pools=allocation_pools) + allocation_pools=allocation_pools, + set_gateway_ip=set_gateway_ip, + ) self._subnet_cidr = netaddr.IPNetwork(subnet_cidr) self._validate_with_subnet(self._subnet_cidr) @@ -322,6 +339,7 @@ class SubnetRequestFactory: cidr = subnet.get('cidr') cidr = cidr if validators.is_attr_set(cidr) else None gateway_ip = subnet.get('gateway_ip') + set_gateway_ip = gateway_ip is not None gateway_ip = gateway_ip if validators.is_attr_set(gateway_ip) else None subnet_id = subnet.get('id', uuidutils.generate_uuid()) @@ -335,7 +353,9 @@ class SubnetRequestFactory: subnet['tenant_id'], subnet_id, common_utils.ip_version_from_int(subnetpool['ip_version']), - prefixlen) + prefixlen, + set_gateway_ip=set_gateway_ip, + ) alloc_pools = subnet.get('allocation_pools') alloc_pools = ( alloc_pools if validators.is_attr_set(alloc_pools) else None) @@ -353,4 +373,6 @@ class SubnetRequestFactory: subnet_id, cidr, gateway_ip=gateway_ip, - allocation_pools=alloc_pools) + allocation_pools=alloc_pools, + set_gateway_ip=set_gateway_ip, + ) diff --git a/neutron/ipam/subnet_alloc.py b/neutron/ipam/subnet_alloc.py index faeaa5345fe..c076ce3b1c0 100644 --- a/neutron/ipam/subnet_alloc.py +++ b/neutron/ipam/subnet_alloc.py @@ -129,7 +129,7 @@ class SubnetAllocator(driver.Pool): if request.prefixlen >= prefix.prefixlen: subnet = next(prefix.subnet(request.prefixlen)) gateway_ip = request.gateway_ip - if not gateway_ip: + if not gateway_ip and request.set_gateway_ip: gateway_ip = subnet.network + 1 pools = ipam_utils.generate_pools(subnet.cidr, gateway_ip) @@ -138,7 +138,9 @@ class SubnetAllocator(driver.Pool): request.subnet_id, subnet.cidr, gateway_ip=gateway_ip, - allocation_pools=pools) + allocation_pools=pools, + set_gateway_ip=request.set_gateway_ip, + ) msg = _("Insufficient prefix space to allocate subnet size /%s") raise exceptions.SubnetAllocationError( reason=msg % str(request.prefixlen)) @@ -156,7 +158,9 @@ class SubnetAllocator(driver.Pool): request.subnet_id, cidr, gateway_ip=request.gateway_ip, - allocation_pools=request.allocation_pools) + allocation_pools=request.allocation_pools, + set_gateway_ip=request.set_gateway_ip, + ) msg = _("Cannot allocate requested subnet from the available " "set of prefixes") raise exceptions.SubnetAllocationError(reason=msg) @@ -200,13 +204,17 @@ class IpamSubnet(driver.Subnet): subnet_id, cidr, gateway_ip=None, - allocation_pools=None): + allocation_pools=None, + set_gateway_ip=True, + ): self._req = ipam_req.SpecificSubnetRequest( tenant_id, subnet_id, cidr, gateway_ip=gateway_ip, - allocation_pools=allocation_pools) + allocation_pools=allocation_pools, + set_gateway_ip=set_gateway_ip, + ) def allocate(self, address_request): raise NotImplementedError() diff --git a/neutron/tests/common/test_db_base_plugin_v2.py b/neutron/tests/common/test_db_base_plugin_v2.py index 9843b45a84c..b9e5f78cc7d 100644 --- a/neutron/tests/common/test_db_base_plugin_v2.py +++ b/neutron/tests/common/test_db_base_plugin_v2.py @@ -6456,6 +6456,28 @@ class TestSubnetPoolsV2(NeutronDbPluginV2TestCase): self.assertEqual(subnet.prefixlen, int(sp['subnetpool']['default_prefixlen'])) + def test_allocate_any_subnet_with_default_prefixlen_no_gateway_ip(self): + with self.network() as network: + sp = self._test_create_subnetpool(['10.10.0.0/16'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='21') + + # Request any subnet allocation using default prefix + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'ip_version': constants.IP_VERSION_4, + 'tenant_id': network['network']['tenant_id'], + 'gateway_ip': None, + }} + req = self.new_create_request('subnets', data) + res = self.deserialize(self.fmt, req.get_response(self.api)) + + subnet = netaddr.IPNetwork(res['subnet']['cidr']) + self.assertEqual(subnet.prefixlen, + int(sp['subnetpool']['default_prefixlen'])) + self.assertIsNone(res['subnet']['gateway_ip']) + def test_allocate_specific_subnet_with_mismatch_prefixlen(self): with self.network() as network: sp = self._test_create_subnetpool(['10.10.0.0/16'], @@ -6646,6 +6668,29 @@ class TestSubnetPoolsV2(NeutronDbPluginV2TestCase): res = req.get_response(self.api) self._check_http_response(res, 400) + def test_allocate_specific_subnet_no_gateway_ip(self): + with self.network() as network: + sp = self._test_create_subnetpool(['10.10.0.0/16'], + tenant_id=self._tenant_id, + name=self._POOL_NAME, + min_prefixlen='21') + + # Request a specific subnet allocation + data = {'subnet': {'network_id': network['network']['id'], + 'subnetpool_id': sp['subnetpool']['id'], + 'cidr': '10.10.1.0/24', + 'ip_version': constants.IP_VERSION_4, + 'tenant_id': network['network']['tenant_id'], + 'gateway_ip': None, + }} + req = self.new_create_request('subnets', data) + res = self.deserialize(self.fmt, req.get_response(self.api)) + + # Assert the allocated subnet CIDR is what we expect + subnet = netaddr.IPNetwork(res['subnet']['cidr']) + self.assertEqual(netaddr.IPNetwork('10.10.1.0/24'), subnet) + self.assertIsNone(res['subnet']['gateway_ip']) + def test_delete_subnetpool_existing_allocations(self): with self.network() as network: sp = self._test_create_subnetpool(['10.10.0.0/16'], diff --git a/neutron/tests/unit/ipam/test_subnet_alloc.py b/neutron/tests/unit/ipam/test_subnet_alloc.py index e090aec3bf2..a3b14f459ea 100644 --- a/neutron/tests/unit/ipam/test_subnet_alloc.py +++ b/neutron/tests/unit/ipam/test_subnet_alloc.py @@ -197,3 +197,34 @@ class TestSubnetAllocation(testlib_api.SqlTestCase): 'fe80::/63') with mock.patch("sqlalchemy.orm.query.Query.update", return_value=0): self.assertRaises(db_exc.RetryRequest, sa.allocate_subnet, req) + + def test_subnetpool_any_request_no_gateway_ip_set(self): + sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp', + ['10.1.0.0/16', '192.168.1.0/24'], + 21, 4) + sp = self.plugin._get_subnetpool(self.ctx, sp['id']) + with db_api.CONTEXT_WRITER.using(self.ctx): + sa = subnet_alloc.SubnetAllocator(sp, self.ctx) + req = ipam_req.AnySubnetRequest(self._tenant_id, + uuidutils.generate_uuid(), + constants.IPv4, 21, + set_gateway_ip=False) + res = sa.allocate_subnet(req) + detail = res.get_details() + self.assertIsNone(detail.gateway_ip) + self.assertFalse(detail.set_gateway_ip) + + def test_subnetpool_specific_request_no_gateway_ip_set(self): + sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp', + ['10.1.0.0/16', '192.168.1.0/24'], + 21, 4) + sp = self.plugin._get_subnetpool(self.ctx, sp['id']) + with db_api.CONTEXT_WRITER.using(self.ctx): + sa = subnet_alloc.SubnetAllocator(sp, self.ctx) + req = ipam_req.SpecificSubnetRequest( + self._tenant_id, uuidutils.generate_uuid(), + '10.1.2.0/27', set_gateway_ip=False) + res = sa.allocate_subnet(req) + detail = res.get_details() + self.assertIsNone(detail.gateway_ip) + self.assertFalse(detail.set_gateway_ip)