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:
Carl Baldwin 2016-04-12 03:41:59 +00:00 committed by Carl Baldwin
parent f494de47fc
commit 303e1c1db8
6 changed files with 116 additions and 59 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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