IP allocation with Service Subnets
This changes the way that IPAM decides which subnets to use when assigning IPs to newly created ports. If the port has a defined device_owner, this is used to filter available subnets to choose from only those with a matching service_type or no service_type at all. If the given network has no service subnets, then the existing behaviour is used. A new IPAM exception is introduced to handle the following scenarios: 1. A port is created with a device_owner and only non-matching service subnets exist. 2. A port is created without a device owner, and no subnets exist without a service_type. With this patch, service subnets are now usable. Implements: blueprint service-subnets APIImpact: subnet-create and subnet-update with service_types DocImpact: IPs assigned to new ports will now come from a service subnet matching the port device_owner, if one exists. Closes-Bug: 1544768 Change-Id: If3dd94a46bdee24c13d1f17c4f2e69af0cb8af63
This commit is contained in:
parent
96a371bf50
commit
de3a3cda74
@ -38,6 +38,7 @@ from neutron.db import segments_db
|
||||
from neutron.db import subnet_service_type_db_models as service_type_db
|
||||
from neutron.extensions import portbindings
|
||||
from neutron.extensions import segment
|
||||
from neutron.ipam import exceptions as ipam_exceptions
|
||||
from neutron.ipam import utils as ipam_utils
|
||||
from neutron.objects import subnet as subnet_obj
|
||||
from neutron.services.segments import db as segment_svc_db
|
||||
@ -564,18 +565,50 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
||||
fixed_ip_list.append({'subnet_id': subnet['id']})
|
||||
return fixed_ip_list
|
||||
|
||||
def _ipam_get_subnets(self, context, network_id, host):
|
||||
def _query_filter_service_subnets(self, query, service_type):
|
||||
ServiceType = service_type_db.SubnetServiceType
|
||||
query = query.add_entity(ServiceType)
|
||||
query = query.outerjoin(ServiceType)
|
||||
query = query.filter(or_(ServiceType.service_type.is_(None),
|
||||
ServiceType.service_type == service_type))
|
||||
return query.from_self(models_v2.Subnet)
|
||||
|
||||
def _check_service_subnets(self, query, service_type):
|
||||
"""Raise an exception if empty subnet list is caused by service type"""
|
||||
if not query.limit(1).count():
|
||||
return
|
||||
query = self._query_filter_service_subnets(query, service_type)
|
||||
if query.limit(1).count():
|
||||
return
|
||||
raise ipam_exceptions.IpAddressGenerationFailureNoMatchingSubnet()
|
||||
|
||||
def _sort_service_subnets(self, subnets, query, service_type):
|
||||
"""Give priority to subnets with service_types"""
|
||||
subnets = sorted(subnets,
|
||||
key=lambda subnet: not subnet.get('service_types'))
|
||||
if not subnets:
|
||||
# If we have an empty subnet list, check if it's caused by
|
||||
# the service type.
|
||||
self._check_service_subnets(query, service_type)
|
||||
return subnets
|
||||
|
||||
def _ipam_get_subnets(self, context, network_id, host, service_type=None):
|
||||
Subnet = models_v2.Subnet
|
||||
SegmentHostMapping = segment_svc_db.SegmentHostMapping
|
||||
|
||||
query = self._get_collection_query(context, Subnet)
|
||||
query = query.filter(Subnet.network_id == network_id)
|
||||
unfiltered_query = self._get_collection_query(context, Subnet)
|
||||
unfiltered_query = unfiltered_query.filter(
|
||||
Subnet.network_id == network_id)
|
||||
query = self._query_filter_service_subnets(unfiltered_query,
|
||||
service_type)
|
||||
# Note: This seems redundant, but its not. It has to cover cases
|
||||
# where host is None, ATTR_NOT_SPECIFIED, or '' due to differences in
|
||||
# host binding implementations.
|
||||
if not validators.is_attr_set(host) or not host:
|
||||
query = query.filter(Subnet.segment_id.is_(None))
|
||||
return [self._make_subnet_dict(c, context=context) for c in query]
|
||||
return self._sort_service_subnets(
|
||||
[self._make_subnet_dict(c, context=context)
|
||||
for c in query], unfiltered_query, service_type)
|
||||
|
||||
# A host has been provided. Consider these two scenarios
|
||||
# 1. Not a routed network: subnets are not on segments
|
||||
@ -602,6 +635,7 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
||||
query = query.filter(Subnet.network_id == network_id)
|
||||
query = query.filter(Subnet.segment_id.isnot(None))
|
||||
if query.count() == 0:
|
||||
self._check_service_subnets(unfiltered_query, service_type)
|
||||
return []
|
||||
# It is a routed network but no subnets found for host
|
||||
raise segment_exc.HostNotConnectedToAnySegment(
|
||||
@ -618,8 +652,10 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
||||
raise segment_exc.HostConnectedToMultipleSegments(
|
||||
host=host, network_id=network_id)
|
||||
|
||||
return [self._make_subnet_dict(subnet, context=context)
|
||||
for subnet, _mapping in results]
|
||||
return self._sort_service_subnets(
|
||||
[self._make_subnet_dict(subnet, context=context)
|
||||
for subnet, _mapping in results],
|
||||
unfiltered_query, service_type)
|
||||
|
||||
def _make_subnet_args(self, detail, subnet, subnetpool_id):
|
||||
args = super(IpamBackendMixin, self)._make_subnet_args(
|
||||
|
@ -67,7 +67,7 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
||||
subnet_ip_pools.add(netaddr.IPRange(ip_pool.first_ip,
|
||||
ip_pool.last_ip))
|
||||
|
||||
for subnet_id in ip_pools:
|
||||
for subnet_id in subnet_id_list:
|
||||
subnet_ip_pools = ip_pools[subnet_id]
|
||||
subnet_ip_allocs = ip_allocations[subnet_id]
|
||||
filter_set = netaddr.IPSet()
|
||||
@ -282,7 +282,8 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
||||
p = port['port']
|
||||
subnets = self._ipam_get_subnets(context,
|
||||
network_id=p['network_id'],
|
||||
host=p.get(portbindings.HOST_ID))
|
||||
host=p.get(portbindings.HOST_ID),
|
||||
service_type=p.get('device_owner'))
|
||||
|
||||
v4, v6_stateful, v6_stateless = self._classify_subnets(
|
||||
context, subnets)
|
||||
|
@ -200,7 +200,8 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
||||
p = port['port']
|
||||
subnets = self._ipam_get_subnets(context,
|
||||
network_id=p['network_id'],
|
||||
host=p.get(portbindings.HOST_ID))
|
||||
host=p.get(portbindings.HOST_ID),
|
||||
service_type=p.get('device_owner'))
|
||||
|
||||
v4, v6_stateful, v6_stateless = self._classify_subnets(
|
||||
context, subnets)
|
||||
|
@ -68,6 +68,10 @@ class IpAddressGenerationFailureAllSubnets(IpAddressGenerationFailure):
|
||||
message = _("No more IP addresses available.")
|
||||
|
||||
|
||||
class IpAddressGenerationFailureNoMatchingSubnet(IpAddressGenerationFailure):
|
||||
message = _("No valid service subnet for the given device owner.")
|
||||
|
||||
|
||||
class IPAllocationFailed(exceptions.NeutronException):
|
||||
message = _("IP allocation failed. Try again later.")
|
||||
|
||||
|
@ -44,7 +44,7 @@ class SubnetServiceTypesExtensionTestCase(
|
||||
test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
|
||||
"""Test API extension subnet_service_types attributes.
|
||||
"""
|
||||
CIDR = '10.0.0.0/8'
|
||||
CIDRS = ['10.0.0.0/8', '20.0.0.0/8', '30.0.0.0/8']
|
||||
IP_VERSION = 4
|
||||
|
||||
def setUp(self):
|
||||
@ -54,14 +54,17 @@ class SubnetServiceTypesExtensionTestCase(
|
||||
super(SubnetServiceTypesExtensionTestCase,
|
||||
self).setUp(plugin=plugin, ext_mgr=ext_mgr)
|
||||
|
||||
def _create_service_subnet(self, service_types=None, network=None):
|
||||
def _create_service_subnet(self, service_types=None, cidr=None,
|
||||
network=None):
|
||||
if not network:
|
||||
with self.network() as network:
|
||||
pass
|
||||
network = network['network']
|
||||
if not cidr:
|
||||
cidr = self.CIDRS[0]
|
||||
args = {'net_id': network['id'],
|
||||
'tenant_id': network['tenant_id'],
|
||||
'cidr': self.CIDR,
|
||||
'cidr': cidr,
|
||||
'ip_version': self.IP_VERSION}
|
||||
if service_types:
|
||||
args['service_types'] = service_types
|
||||
@ -158,8 +161,121 @@ class SubnetServiceTypesExtensionTestCase(
|
||||
# Update it with an invalid service type
|
||||
self._test_update_subnet(subnet, service_types, expect_fail=True)
|
||||
|
||||
def _assert_port_res(self, port, service_type, subnet, fallback,
|
||||
error='IpAddressGenerationFailureNoMatchingSubnet'):
|
||||
res = self.deserialize('json', port)
|
||||
if fallback:
|
||||
port = res['port']
|
||||
self.assertEqual(1, len(port['fixed_ips']))
|
||||
self.assertEqual(service_type, port['device_owner'])
|
||||
self.assertEqual(subnet['id'], port['fixed_ips'][0]['subnet_id'])
|
||||
else:
|
||||
self.assertEqual(error, res['NeutronError']['type'])
|
||||
|
||||
def test_create_port_with_matching_service_type(self):
|
||||
with self.network() as network:
|
||||
pass
|
||||
matching_type = 'network:foo'
|
||||
non_matching_type = 'network:bar'
|
||||
# Create a subnet with no service types
|
||||
self._create_service_subnet(network=network)
|
||||
# Create a subnet with a non-matching service type
|
||||
self._create_service_subnet([non_matching_type],
|
||||
cidr=self.CIDRS[2],
|
||||
network=network)
|
||||
# Create a subnet with a service type to match the port device owner
|
||||
res = self._create_service_subnet([matching_type],
|
||||
cidr=self.CIDRS[1],
|
||||
network=network)
|
||||
service_subnet = self.deserialize('json', res)['subnet']
|
||||
# Create a port with device owner matching the correct service subnet
|
||||
network = network['network']
|
||||
port = self._create_port(self.fmt,
|
||||
net_id=network['id'],
|
||||
tenant_id=network['tenant_id'],
|
||||
device_owner=matching_type)
|
||||
self._assert_port_res(port, matching_type, service_subnet, True)
|
||||
|
||||
def test_create_port_without_matching_service_type(self, fallback=True):
|
||||
with self.network() as network:
|
||||
pass
|
||||
subnet = ''
|
||||
matching_type = 'compute:foo'
|
||||
non_matching_type = 'network:foo'
|
||||
if fallback:
|
||||
# Create a subnet with no service types
|
||||
res = self._create_service_subnet(network=network)
|
||||
subnet = self.deserialize('json', res)['subnet']
|
||||
# Create a subnet with a non-matching service type
|
||||
self._create_service_subnet([non_matching_type],
|
||||
cidr=self.CIDRS[1],
|
||||
network=network)
|
||||
# Create a port with device owner not matching the service subnet
|
||||
network = network['network']
|
||||
port = self._create_port(self.fmt,
|
||||
net_id=network['id'],
|
||||
tenant_id=network['tenant_id'],
|
||||
device_owner=matching_type)
|
||||
self._assert_port_res(port, matching_type, subnet, fallback)
|
||||
|
||||
def test_create_port_without_matching_service_type_no_fallback(self):
|
||||
self.test_create_port_without_matching_service_type(fallback=False)
|
||||
|
||||
def test_create_port_no_device_owner(self, fallback=True):
|
||||
with self.network() as network:
|
||||
pass
|
||||
subnet = ''
|
||||
service_type = 'compute:foo'
|
||||
if fallback:
|
||||
# Create a subnet with no service types
|
||||
res = self._create_service_subnet(network=network)
|
||||
subnet = self.deserialize('json', res)['subnet']
|
||||
# Create a subnet with a service_type
|
||||
self._create_service_subnet([service_type],
|
||||
cidr=self.CIDRS[1],
|
||||
network=network)
|
||||
# Create a port without a device owner
|
||||
network = network['network']
|
||||
port = self._create_port(self.fmt,
|
||||
net_id=network['id'],
|
||||
tenant_id=network['tenant_id'])
|
||||
self._assert_port_res(port, '', subnet, fallback)
|
||||
|
||||
def test_create_port_no_device_owner_no_fallback(self):
|
||||
self.test_create_port_no_device_owner(fallback=False)
|
||||
|
||||
def test_create_port_exhausted_subnet(self, fallback=True):
|
||||
with self.network() as network:
|
||||
pass
|
||||
subnet = ''
|
||||
service_type = 'compute:foo'
|
||||
if fallback:
|
||||
# Create a subnet with no service types
|
||||
res = self._create_service_subnet(network=network)
|
||||
subnet = self.deserialize('json', res)['subnet']
|
||||
# Create a subnet with a service_type
|
||||
res = self._create_service_subnet([service_type],
|
||||
cidr=self.CIDRS[1],
|
||||
network=network)
|
||||
service_subnet = self.deserialize('json', res)['subnet']
|
||||
# Update the service subnet with empty allocation pools
|
||||
data = {'subnet': {'allocation_pools': []}}
|
||||
req = self.new_update_request('subnets', data, service_subnet['id'])
|
||||
res = self.deserialize(self.fmt, req.get_response(self.api))
|
||||
# Create a port with a matching device owner
|
||||
network = network['network']
|
||||
port = self._create_port(self.fmt,
|
||||
net_id=network['id'],
|
||||
tenant_id=network['tenant_id'],
|
||||
device_owner=service_type)
|
||||
self._assert_port_res(port, service_type, subnet, fallback,
|
||||
error='IpAddressGenerationFailure')
|
||||
|
||||
def test_create_port_exhausted_subnet_no_fallback(self):
|
||||
self.test_create_port_exhausted_subnet(fallback=False)
|
||||
|
||||
|
||||
class SubnetServiceTypesExtensionTestCasev6(
|
||||
SubnetServiceTypesExtensionTestCase):
|
||||
CIDR = '2001:db8::/64'
|
||||
CIDRS = ['2001:db8:2::/64', '2001:db8:3::/64', '2001:db8:4::/64']
|
||||
IP_VERSION = 6
|
||||
|
@ -0,0 +1,15 @@
|
||||
---
|
||||
features:
|
||||
- Subnets now have a new property 'service_types'.
|
||||
This is a list of port device owners, such that
|
||||
only ports with a matching device owner will be
|
||||
given an IP from this subnet. If no matching
|
||||
service subnet exists for the given device owner,
|
||||
or no service subnets have been defined on the
|
||||
network, the port will be assigned an IP from a
|
||||
subnet with no service-types. This preserves
|
||||
backwards compatibility with older deployments.
|
||||
upgrade:
|
||||
- A new table 'subnet_service_types' has been added
|
||||
to cater for this feature. It uses the ID field
|
||||
from the 'subnets' table as a foreign key.
|
Loading…
Reference in New Issue
Block a user