Enable IPAM drivers to allocate from more than one subnet
Calling in to the IPAM driver independently for each subnet on a network can be costly, especially if the IPAM system is external. This change opens the possibility of optimizing the operation of allocating one IP address from a list of candidate subnets by allowing drivers to subclass the default implementation. Change-Id: I07b32752e1e2e84641dd6504a93cfef95ae368c5 Partially-Implements: blueprint bp/routed-networks
This commit is contained in:
parent
f494de47fc
commit
303e1c1db8
@ -89,28 +89,6 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
||||
"external system for %s"), addresses)
|
||||
return deallocated
|
||||
|
||||
def _ipam_try_allocate_ip(self, context, ipam_driver, port, ip_dict):
|
||||
factory = ipam_driver.get_address_request_factory()
|
||||
ip_request = factory.get_request(context, port, ip_dict)
|
||||
ipam_subnet = ipam_driver.get_subnet(ip_dict['subnet_id'])
|
||||
return ipam_subnet.allocate(ip_request)
|
||||
|
||||
def _ipam_allocate_single_ip(self, context, ipam_driver, port, subnets):
|
||||
"""Allocates single ip from set of subnets
|
||||
|
||||
Raises n_exc.IpAddressGenerationFailure if allocation failed for
|
||||
all subnets.
|
||||
"""
|
||||
for subnet in subnets:
|
||||
try:
|
||||
return [self._ipam_try_allocate_ip(context, ipam_driver,
|
||||
port, subnet),
|
||||
subnet]
|
||||
except ipam_exc.IpAddressGenerationFailure:
|
||||
continue
|
||||
raise n_exc.IpAddressGenerationFailure(
|
||||
net_id=port['network_id'])
|
||||
|
||||
def _ipam_allocate_ips(self, context, ipam_driver, port, ips,
|
||||
revert_on_fail=True):
|
||||
"""Allocate set of ips over IPAM.
|
||||
@ -129,13 +107,20 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
||||
# By default IP info is dict, used to allocate single ip
|
||||
# from single subnet.
|
||||
# IP info can be list, used to allocate single ip from
|
||||
# multiple subnets (i.e. first successful ip allocation
|
||||
# is returned)
|
||||
# multiple subnets
|
||||
ip_list = [ip] if isinstance(ip, dict) else ip
|
||||
ip_address, ip_subnet = self._ipam_allocate_single_ip(
|
||||
context, ipam_driver, port, ip_list)
|
||||
subnets = [ip_dict['subnet_id'] for ip_dict in ip_list]
|
||||
try:
|
||||
factory = ipam_driver.get_address_request_factory()
|
||||
ip_request = factory.get_request(context, port, ip_list[0])
|
||||
ipam_allocator = ipam_driver.get_allocator(subnets)
|
||||
ip_address, subnet_id = ipam_allocator.allocate(ip_request)
|
||||
except ipam_exc.IpAddressGenerationFailureAllSubnets:
|
||||
raise n_exc.IpAddressGenerationFailure(
|
||||
net_id=port['network_id'])
|
||||
|
||||
allocated.append({'ip_address': ip_address,
|
||||
'subnet_id': ip_subnet['subnet_id']})
|
||||
'subnet_id': subnet_id})
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.debug("An exception occurred during IP allocation.")
|
||||
|
@ -110,6 +110,15 @@ class Pool(object):
|
||||
"""
|
||||
return ipam_req.AddressRequestFactory
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_allocator(self, subnet_ids):
|
||||
"""Gets an allocator for subnets passed in
|
||||
|
||||
:param subnet_ids: ids for subnets from which the IP can be allocated
|
||||
:returns: An instance of IPAM SubnetGroup
|
||||
:raises: TODO(Carl) What sort of errors do we need to plan for?
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Subnet(object):
|
||||
@ -148,3 +157,25 @@ class Subnet(object):
|
||||
|
||||
:returns: An instance of SpecificSubnetRequest with the subnet detail.
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class SubnetGroup(object):
|
||||
"""Interface definition for a filtered group of IPAM Subnets
|
||||
|
||||
Allocates from a group of semantically equivalent subnets. The list of
|
||||
candidate subnets *may* be ordered by preference but all of the subnets
|
||||
must be suitable for fulfilling the request. For example, all of them must
|
||||
be associated with the network we're trying to allocate an address for.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def allocate(self, address_request):
|
||||
"""Allocates an IP address based on the request passed in
|
||||
|
||||
:param address_request: Specifies what to allocate.
|
||||
:type address_request: An instance of a subclass of AddressRequest
|
||||
:returns: A netaddr.IPAddress, subnet_id tuple
|
||||
:raises: AddressNotAvailable, AddressOutsideAllocationPool,
|
||||
AddressOutsideSubnet, IpAddressGenerationFailureAllSubnets
|
||||
"""
|
||||
|
@ -64,6 +64,10 @@ class IpAddressGenerationFailure(exceptions.Conflict):
|
||||
message = _("No more IP addresses available for subnet %(subnet_id)s.")
|
||||
|
||||
|
||||
class IpAddressGenerationFailureAllSubnets(IpAddressGenerationFailure):
|
||||
message = _("No more IP addresses available.")
|
||||
|
||||
|
||||
class IPAllocationFailed(exceptions.NeutronException):
|
||||
message = _("IP allocation failed. Try again later.")
|
||||
|
||||
|
@ -26,6 +26,7 @@ from neutron._i18n import _
|
||||
from neutron.common import exceptions as n_exc
|
||||
from neutron.db import models_v2
|
||||
from neutron.ipam import driver
|
||||
from neutron.ipam import exceptions as ipam_exc
|
||||
from neutron.ipam import requests as ipam_req
|
||||
from neutron.ipam import utils as ipam_utils
|
||||
|
||||
@ -184,6 +185,9 @@ class SubnetAllocator(driver.Pool):
|
||||
def remove_subnet(self, subnet_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_allocator(self, subnet_ids):
|
||||
return IpamSubnetGroup(self, subnet_ids)
|
||||
|
||||
|
||||
class IpamSubnet(driver.Subnet):
|
||||
|
||||
@ -210,6 +214,27 @@ class IpamSubnet(driver.Subnet):
|
||||
return self._req
|
||||
|
||||
|
||||
class IpamSubnetGroup(driver.SubnetGroup):
|
||||
def __init__(self, driver, subnet_ids):
|
||||
self._driver = driver
|
||||
self._subnet_ids = subnet_ids
|
||||
|
||||
def allocate(self, address_request):
|
||||
'''Originally, the Neutron pluggable IPAM backend would ask the driver
|
||||
to try to allocate an IP from each subnet in turn, one by one. This
|
||||
implementation preserves that behavior so that existing drivers work
|
||||
as they did before while giving them the opportunity to optimize it
|
||||
by overridding the implementation.
|
||||
'''
|
||||
for subnet_id in self._subnet_ids:
|
||||
try:
|
||||
ipam_subnet = self._driver.get_subnet(subnet_id)
|
||||
return ipam_subnet.allocate(address_request), subnet_id
|
||||
except ipam_exc.IpAddressGenerationFailure:
|
||||
continue
|
||||
raise ipam_exc.IpAddressGenerationFailureAllSubnets()
|
||||
|
||||
|
||||
class SubnetPoolReader(object):
|
||||
'''Class to assist with reading a subnetpool, loading defaults, and
|
||||
inferring IP version from prefix list. Provides a common way of
|
||||
|
@ -74,6 +74,7 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
mocks = {
|
||||
'driver': mock.Mock(),
|
||||
'subnet': mock.Mock(),
|
||||
'subnets': mock.Mock(),
|
||||
'subnet_request': ipam_req.SpecificSubnetRequest(
|
||||
self.tenant_id,
|
||||
self.subnet_id,
|
||||
@ -83,6 +84,9 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
}
|
||||
mocks['driver'].get_subnet.return_value = mocks['subnet']
|
||||
mocks['driver'].allocate_subnet.return_value = mocks['subnet']
|
||||
mocks['driver'].get_allocator.return_value = mocks['subnets']
|
||||
mocks['subnets'].allocate.return_value = (
|
||||
mock.sentinel.address, mock.sentinel.subnet_id)
|
||||
mocks['driver'].get_subnet_request_factory.return_value = (
|
||||
subnet_factory)
|
||||
mocks['driver'].get_address_request_factory.return_value = (
|
||||
@ -102,7 +106,7 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
pool_mock.get_instance.return_value = mocks['driver']
|
||||
return mocks
|
||||
|
||||
def _get_allocate_mock(self, auto_ip='10.0.0.2',
|
||||
def _get_allocate_mock(self, subnet_id, auto_ip='10.0.0.2',
|
||||
fail_ip='127.0.0.1',
|
||||
exception=None):
|
||||
if exception is None:
|
||||
@ -113,9 +117,9 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
if request.address == netaddr.IPAddress(fail_ip):
|
||||
raise exception
|
||||
else:
|
||||
return str(request.address)
|
||||
return str(request.address), subnet_id
|
||||
else:
|
||||
return auto_ip
|
||||
return auto_ip, subnet_id
|
||||
|
||||
return allocate_mock
|
||||
|
||||
@ -130,9 +134,9 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
return deallocate_mock
|
||||
|
||||
def _validate_allocate_calls(self, expected_calls, mocks):
|
||||
self.assertTrue(mocks['subnet'].allocate.called)
|
||||
self.assertTrue(mocks['subnets'].allocate.called)
|
||||
|
||||
actual_calls = mocks['subnet'].allocate.call_args_list
|
||||
actual_calls = mocks['subnets'].allocate.call_args_list
|
||||
self.assertEqual(len(expected_calls), len(actual_calls))
|
||||
|
||||
i = 0
|
||||
@ -193,10 +197,10 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
allocated_ips = mocks['ipam']._ipam_allocate_ips(
|
||||
mock.ANY, mocks['driver'], mock.ANY, ips)
|
||||
|
||||
mocks['driver'].get_subnet.assert_called_once_with(subnet)
|
||||
mocks['driver'].get_allocator.assert_called_once_with([subnet])
|
||||
|
||||
self.assertTrue(mocks['subnet'].allocate.called)
|
||||
request = mocks['subnet'].allocate.call_args[0][0]
|
||||
self.assertTrue(mocks['subnets'].allocate.called)
|
||||
request = mocks['subnets'].allocate.call_args[0][0]
|
||||
|
||||
return {'ips': allocated_ips,
|
||||
'request': request}
|
||||
@ -204,12 +208,13 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
def test_allocate_single_fixed_ip(self):
|
||||
mocks = self._prepare_ipam()
|
||||
ip = '192.168.15.123'
|
||||
mocks['subnet'].allocate.return_value = ip
|
||||
subnet_id = self._gen_subnet_id()
|
||||
mocks['subnets'].allocate.return_value = ip, subnet_id
|
||||
|
||||
results = self._single_ip_allocate_helper(mocks,
|
||||
ip,
|
||||
'192.168.15.0/24',
|
||||
self._gen_subnet_id())
|
||||
subnet_id)
|
||||
|
||||
self.assertIsInstance(results['request'],
|
||||
ipam_req.SpecificAddressRequest)
|
||||
@ -222,10 +227,11 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
mocks = self._prepare_ipam()
|
||||
network = '192.168.15.0/24'
|
||||
ip = '192.168.15.83'
|
||||
mocks['subnet'].allocate.return_value = ip
|
||||
subnet_id = self._gen_subnet_id()
|
||||
mocks['subnets'].allocate.return_value = ip, subnet_id
|
||||
|
||||
results = self._single_ip_allocate_helper(mocks, '', network,
|
||||
self._gen_subnet_id())
|
||||
subnet_id)
|
||||
|
||||
self.assertIsInstance(results['request'], ipam_req.AnyAddressRequest)
|
||||
self.assertEqual(ip, results['ips'][0]['ip_address'])
|
||||
@ -241,23 +247,25 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
mocks['ipam']._ipam_allocate_ips(mock.ANY, mocks['driver'],
|
||||
mock.ANY, [ip])
|
||||
|
||||
request = mocks['subnet'].allocate.call_args[0][0]
|
||||
request = mocks['subnets'].allocate.call_args[0][0]
|
||||
self.assertIsInstance(request, ipam_req.AutomaticAddressRequest)
|
||||
self.assertEqual(eui64_ip, request.address)
|
||||
|
||||
def test_allocate_multiple_ips(self):
|
||||
mocks = self._prepare_ipam()
|
||||
data = {'': ['172.23.128.0/17', self._gen_subnet_id()],
|
||||
subnet_id = self._gen_subnet_id()
|
||||
data = {'': ['172.23.128.0/17', subnet_id],
|
||||
'192.168.43.15': ['192.168.43.0/24', self._gen_subnet_id()],
|
||||
'8.8.8.8': ['8.0.0.0/8', self._gen_subnet_id()]}
|
||||
ips = self._convert_to_ips(data)
|
||||
mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
|
||||
auto_ip='172.23.128.94')
|
||||
mocks['subnets'].allocate.side_effect = self._get_allocate_mock(
|
||||
subnet_id, auto_ip='172.23.128.94')
|
||||
|
||||
mocks['ipam']._ipam_allocate_ips(
|
||||
mock.ANY, mocks['driver'], mock.ANY, ips)
|
||||
get_calls = [mock.call(data[ip][1]) for ip in data]
|
||||
mocks['driver'].get_subnet.assert_has_calls(get_calls, any_order=True)
|
||||
get_calls = [mock.call([data[ip][1]]) for ip in data]
|
||||
mocks['driver'].get_allocator.assert_has_calls(
|
||||
get_calls, any_order=True)
|
||||
|
||||
self._validate_allocate_calls(ips, mocks)
|
||||
|
||||
@ -266,15 +274,15 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
mocks = self._prepare_ipam()
|
||||
fail_ip = '192.168.43.15'
|
||||
auto_ip = '172.23.128.94'
|
||||
data = {'': ['172.23.128.0/17', self._gen_subnet_id()],
|
||||
subnet_id = self._gen_subnet_id()
|
||||
data = {'': ['172.23.128.0/17', subnet_id],
|
||||
fail_ip: ['192.168.43.0/24', self._gen_subnet_id()],
|
||||
'8.8.8.8': ['8.0.0.0/8', self._gen_subnet_id()]}
|
||||
ips = self._convert_to_ips(data)
|
||||
|
||||
mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
|
||||
auto_ip=auto_ip, fail_ip=fail_ip, exception=db_exc.DBDeadlock())
|
||||
if exc_on_deallocate:
|
||||
mocks['subnet'].deallocate.side_effect = ValueError('Invalid IP')
|
||||
mocks['subnets'].allocate.side_effect = self._get_allocate_mock(
|
||||
subnet_id, auto_ip=auto_ip, fail_ip=fail_ip,
|
||||
exception=db_exc.DBDeadlock())
|
||||
|
||||
# Exception should be raised on attempt to allocate second ip.
|
||||
# Revert action should be performed for the already allocated ips,
|
||||
@ -288,8 +296,9 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
ips)
|
||||
|
||||
# get_subnet should be called only for the first two networks
|
||||
get_calls = [mock.call(data[ip][1]) for ip in ['', fail_ip]]
|
||||
mocks['driver'].get_subnet.assert_has_calls(get_calls, any_order=True)
|
||||
get_calls = [mock.call([data[ip][1]]) for ip in ['', fail_ip]]
|
||||
mocks['driver'].get_allocator.assert_has_calls(
|
||||
get_calls, any_order=True)
|
||||
|
||||
# Allocate should be called for the first two ips only
|
||||
self._validate_allocate_calls(ips[:-1], mocks)
|
||||
@ -323,7 +332,7 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
mocks['driver'],
|
||||
mock.ANY,
|
||||
ips)
|
||||
mocks['subnet'].allocate.assert_called_once_with(mock.ANY)
|
||||
mocks['subnets'].allocate.assert_called_once_with(mock.ANY)
|
||||
|
||||
@mock.patch('neutron.ipam.driver.Pool')
|
||||
def test_create_subnet_over_ipam(self, pool_mock):
|
||||
@ -472,9 +481,9 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
mocks = self._prepare_mocks_with_pool_mock(pool_mock)
|
||||
auto_ip = '10.0.0.2'
|
||||
expected_calls = [{'ip_address': ''}]
|
||||
mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
|
||||
auto_ip=auto_ip)
|
||||
with self.subnet() as subnet:
|
||||
mocks['subnets'].allocate.side_effect = self._get_allocate_mock(
|
||||
subnet['subnet']['id'], auto_ip=auto_ip)
|
||||
with self.port(subnet=subnet) as port:
|
||||
ips = port['port']['fixed_ips']
|
||||
self.assertEqual(1, len(ips))
|
||||
@ -509,9 +518,9 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
auto_ip = '10.0.0.2'
|
||||
new_ip = '10.0.0.15'
|
||||
expected_calls = [{'ip_address': ip} for ip in ['', new_ip]]
|
||||
mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
|
||||
auto_ip=auto_ip)
|
||||
with self.subnet() as subnet:
|
||||
mocks['subnets'].allocate.side_effect = self._get_allocate_mock(
|
||||
subnet['subnet']['id'], auto_ip=auto_ip)
|
||||
with self.port(subnet=subnet) as port:
|
||||
ips = port['port']['fixed_ips']
|
||||
self.assertEqual(1, len(ips))
|
||||
@ -536,9 +545,9 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
||||
def test_delete_port_ipam(self, pool_mock):
|
||||
mocks = self._prepare_mocks_with_pool_mock(pool_mock)
|
||||
auto_ip = '10.0.0.2'
|
||||
mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
|
||||
auto_ip=auto_ip)
|
||||
with self.subnet() as subnet:
|
||||
mocks['subnets'].allocate.side_effect = self._get_allocate_mock(
|
||||
subnet['subnet']['id'], auto_ip=auto_ip)
|
||||
with self.port(subnet=subnet) as port:
|
||||
ips = port['port']['fixed_ips']
|
||||
self.assertEqual(1, len(ips))
|
||||
|
@ -28,6 +28,9 @@ class FakeDriver(driver.Pool):
|
||||
def get_subnet(self, cidr):
|
||||
return driver.Subnet()
|
||||
|
||||
def get_allocator(self, subnet_ids):
|
||||
return driver.SubnetGroup()
|
||||
|
||||
def update_subnet(self, request):
|
||||
return driver.Subnet()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user