diff --git a/neutron/db/ipam_non_pluggable_backend.py b/neutron/db/ipam_non_pluggable_backend.py index d024a96e43f..ef378e70b27 100644 --- a/neutron/db/ipam_non_pluggable_backend.py +++ b/neutron/db/ipam_non_pluggable_backend.py @@ -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']}) diff --git a/neutron/ipam/drivers/neutrondb_ipam/driver.py b/neutron/ipam/drivers/neutrondb_ipam/driver.py index cf46000426e..43cd54f0f3c 100644 --- a/neutron/ipam/drivers/neutrondb_ipam/driver.py +++ b/neutron/ipam/drivers/neutrondb_ipam/driver.py @@ -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'. diff --git a/neutron/ipam/requests.py b/neutron/ipam/requests.py index 9076133f279..3a96efed406 100644 --- a/neutron/ipam/requests.py +++ b/neutron/ipam/requests.py @@ -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() diff --git a/neutron/tests/unit/db/test_db_base_plugin_v2.py b/neutron/tests/unit/db/test_db_base_plugin_v2.py index c75aae30352..1774a941b37 100644 --- a/neutron/tests/unit/db/test_db_base_plugin_v2.py +++ b/neutron/tests/unit/db/test_db_base_plugin_v2.py @@ -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'], diff --git a/neutron/tests/unit/db/test_ipam_pluggable_backend.py b/neutron/tests/unit/db/test_ipam_pluggable_backend.py index 57a02e69f2f..0314a997f59 100644 --- a/neutron/tests/unit/db/test_ipam_pluggable_backend.py +++ b/neutron/tests/unit/db/test_ipam_pluggable_backend.py @@ -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 diff --git a/neutron/tests/unit/ipam/test_requests.py b/neutron/tests/unit/ipam/test_requests.py index 735a1dd0b85..81aeab77e01 100644 --- a/neutron/tests/unit/ipam/test_requests.py +++ b/neutron/tests/unit/ipam/test_requests.py @@ -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):