Use next available address for dhcp ports

Preserve sequential IP allocation for DHCP ports to
avoid interferring with templating systems like heat
that expect a freshly created subnet to have higher
numbered IP addresses available for resources.

The change to randomized allocations was breaking
heat templates that tried to use a specific address
in the subnet that didn't use to be near where DHCP
ports were allocated (e.g. the 10th address).

Change-Id: I4dbda44460adc873b2e4dc1638a34bfac9bb1fc4
This commit is contained in:
Kevin Benton 2016-06-09 00:37:34 -07:00
parent 746f91c7c7
commit 3f9cb90ca2
6 changed files with 62 additions and 17 deletions

View File

@ -41,7 +41,7 @@ LOG = logging.getLogger(__name__)
class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
@staticmethod
def _generate_ip(context, subnets, filtered_ips=None):
def _generate_ip(context, subnets, filtered_ips=None, prefer_next=False):
"""Generate an IP address.
The IP address will be generated from one of the subnets defined on
@ -84,7 +84,10 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
# Compute a window size, select an index inside the window, then
# select the IP address at the selected index within the window
window = min(av_set_size, 10)
if prefer_next:
window = 1
else:
window = min(av_set_size, 10)
ip_index = random.randint(1, window)
candidate_ips = list(itertools.islice(av_set, ip_index))
if candidate_ips:
@ -294,7 +297,8 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
self._validate_max_ips_per_port(fixed_ip_set, device_owner)
return fixed_ip_set
def _allocate_fixed_ips(self, context, fixed_ips, mac_address):
def _allocate_fixed_ips(self, context, fixed_ips, mac_address,
prefer_next=False):
"""Allocate IP addresses according to the configured fixed_ips."""
ips = []
@ -326,7 +330,8 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
else:
subnets = [subnet]
# IP address allocation
result = self._generate_ip(context, subnets, allocated_ips)
result = self._generate_ip(context, subnets, allocated_ips,
prefer_next)
allocated_ips.append(result['ip_address'])
ips.append({'ip_address': result['ip_address'],
'subnet_id': result['subnet_id']})
@ -378,6 +383,8 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
v4, v6_stateful, v6_stateless = self._classify_subnets(
context, subnets)
# preserve previous behavior of DHCP ports choosing start of pool
prefer_next = p['device_owner'] == constants.DEVICE_OWNER_DHCP
fixed_configured = p['fixed_ips'] is not constants.ATTR_NOT_SPECIFIED
if fixed_configured:
configured_ips = self._test_fixed_ips_for_port(context,
@ -387,15 +394,16 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
subnets)
ips = self._allocate_fixed_ips(context,
configured_ips,
p['mac_address'])
p['mac_address'],
prefer_next=prefer_next)
else:
ips = []
version_subnets = [v4, v6_stateful]
for subnets in version_subnets:
if subnets:
result = IpamNonPluggableBackend._generate_ip(context,
subnets)
result = IpamNonPluggableBackend._generate_ip(
context, subnets, prefer_next=prefer_next)
ips.append({'ip_address': result['ip_address'],
'subnet_id': result['subnet_id']})

View File

@ -207,7 +207,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
netaddr.IPAddress(ip_range.last).format())
session.add(av_range)
def _generate_ip(self, session):
def _generate_ip(self, session, prefer_next=False):
"""Generate an IP address from the set of available addresses."""
ip_allocations = netaddr.IPSet()
for ipallocation in self.subnet_manager.list_allocations(session):
@ -220,8 +220,11 @@ class NeutronDbSubnet(ipam_base.Subnet):
if av_set.size == 0:
continue
# Compute a value for the selection window
window = min(av_set.size, 10)
if prefer_next:
window = 1
else:
# Compute a value for the selection window
window = min(av_set.size, 10)
ip_index = random.randint(1, window)
candidate_ips = list(itertools.islice(av_set, ip_index))
allocated_ip = candidate_ips[-1]
@ -246,7 +249,9 @@ class NeutronDbSubnet(ipam_base.Subnet):
ip_address = str(address_request.address)
self._verify_ip(session, ip_address)
else:
ip_address, all_pool_id = self._generate_ip(session)
prefer_next = isinstance(address_request,
ipam_req.PreferNextAddressRequest)
ip_address, all_pool_id = self._generate_ip(session, prefer_next)
# Create IP allocation request object
# The only defined status at this stage is 'ALLOCATED'.

View File

@ -206,6 +206,10 @@ class AnyAddressRequest(AddressRequest):
"""Used to request any available address from the pool."""
class PreferNextAddressRequest(AnyAddressRequest):
"""Used to request next available IP address from the pool."""
class AutomaticAddressRequest(SpecificAddressRequest):
"""Used to create auto generated addresses, such as EUI64"""
EUI64 = 'eui64'
@ -265,6 +269,9 @@ class AddressRequestFactory(object):
elif ip_dict.get('eui64_address'):
return AutomaticAddressRequest(prefix=ip_dict['subnet_cidr'],
mac=ip_dict['mac'])
elif port['device_owner'] == constants.DEVICE_OWNER_DHCP:
# preserve previous behavior of DHCP ports choosing start of pool
return PreferNextAddressRequest()
else:
return AnyAddressRequest()

View File

@ -1408,6 +1408,19 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s
for fixed_ip in updated_fixed_ips:
self.assertIn(fixed_ip, result['port']['fixed_ips'])
def test_dhcp_port_ips_prefer_next_available_ip(self):
# test to check that DHCP ports get the first available IP in the
# allocation range
with self.subnet() as subnet:
port_ips = []
for _ in range(10):
with self.port(device_owner=constants.DEVICE_OWNER_DHCP,
subnet=subnet) as port:
port_ips.append(port['port']['fixed_ips'][0]['ip_address'])
first_ip = netaddr.IPAddress(port_ips[0])
expected = [str(first_ip + i) for i in range(10)]
self.assertEqual(expected, port_ips)
def test_update_port_mac_ip(self):
with self.subnet() as subnet:
updated_fixed_ips = [{'subnet_id': subnet['subnet']['id'],

View File

@ -75,6 +75,7 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
'driver': mock.Mock(),
'subnet': mock.Mock(),
'subnets': mock.Mock(),
'port': {'device_owner': 'compute:None'},
'subnet_request': ipam_req.SpecificSubnetRequest(
self.tenant_id,
self.subnet_id,
@ -195,7 +196,7 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
ips[0]['ip_address'] = ip
allocated_ips = mocks['ipam']._ipam_allocate_ips(
mock.ANY, mocks['driver'], mock.ANY, ips)
mock.ANY, mocks['driver'], mocks['port'], ips)
mocks['driver'].get_allocator.assert_called_once_with([subnet])
@ -262,7 +263,7 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
subnet_id, auto_ip='172.23.128.94')
mocks['ipam']._ipam_allocate_ips(
mock.ANY, mocks['driver'], mock.ANY, ips)
mock.ANY, mocks['driver'], mocks['port'], ips)
get_calls = [mock.call([data[ip][1]]) for ip in data]
mocks['driver'].get_allocator.assert_has_calls(
get_calls, any_order=True)
@ -292,7 +293,7 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
mocks['ipam']._ipam_allocate_ips,
mock.ANY,
mocks['driver'],
mock.ANY,
mocks['port'],
ips)
# get_subnet should be called only for the first two networks

View File

@ -288,25 +288,36 @@ class TestAddressRequestFactory(base.BaseTestCase):
def test_specific_address_request_is_loaded(self):
for address in ('10.12.0.15', 'fffe::1'):
ip = {'ip_address': address}
port = {'device_owner': 'compute:None'}
self.assertIsInstance(
ipam_req.AddressRequestFactory.get_request(None, None, ip),
ipam_req.AddressRequestFactory.get_request(None, port, ip),
ipam_req.SpecificAddressRequest)
def test_any_address_request_is_loaded(self):
for addr in [None, '']:
ip = {'ip_address': addr}
port = {'device_owner': 'compute:None'}
self.assertIsInstance(
ipam_req.AddressRequestFactory.get_request(None, None, ip),
ipam_req.AddressRequestFactory.get_request(None, port, ip),
ipam_req.AnyAddressRequest)
def test_automatic_address_request_is_loaded(self):
ip = {'mac': '6c:62:6d:de:cf:49',
'subnet_cidr': '2001:470:abcd::/64',
'eui64_address': True}
port = {'device_owner': 'compute:None'}
self.assertIsInstance(
ipam_req.AddressRequestFactory.get_request(None, None, ip),
ipam_req.AddressRequestFactory.get_request(None, port, ip),
ipam_req.AutomaticAddressRequest)
def test_prefernext_address_request_on_dhcp_port(self):
ip = {}
port = {'device_owner': 'network:dhcp'}
self.assertIsInstance(
ipam_req.AddressRequestFactory.get_request(None, port, ip),
ipam_req.PreferNextAddressRequest)
class TestSubnetRequestFactory(IpamSubnetRequestTestCase):