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
This commit is contained in:
Rodolfo Alonso Hernandez
2025-06-11 08:17:22 +00:00
committed by Rodolfo Alonso
parent 7c478bad67
commit 5cccd2112f
4 changed files with 118 additions and 12 deletions

View File

@ -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,
)

View File

@ -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()

View File

@ -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'],

View File

@ -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)