diff --git a/neutron/db/ipam_backend_mixin.py b/neutron/db/ipam_backend_mixin.py index 6c6ceeb35f0..efa381ac50e 100644 --- a/neutron/db/ipam_backend_mixin.py +++ b/neutron/db/ipam_backend_mixin.py @@ -310,9 +310,17 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon): Finally, verify that each range fall within the subnet's CIDR. """ subnet = netaddr.IPNetwork(subnet_cidr) - subnet_first_ip = netaddr.IPAddress(subnet.first + 1) - # last address is broadcast in v4 - subnet_last_ip = netaddr.IPAddress(subnet.last - (subnet.version == 4)) + if subnet.version == const.IP_VERSION_4: + if subnet.prefixlen <= 30: + subnet_first_ip = netaddr.IPAddress(subnet.first + 1) + # last address is broadcast in v4 + subnet_last_ip = netaddr.IPAddress(subnet.last - 1) + else: + subnet_first_ip = netaddr.IPAddress(subnet.first) + subnet_last_ip = netaddr.IPAddress(subnet.last) + else: # IPv6 case + subnet_first_ip = netaddr.IPAddress(subnet.first + 1) + subnet_last_ip = netaddr.IPAddress(subnet.last) LOG.debug("Performing IP validity checks on allocation pools") ip_sets = [] diff --git a/neutron/ipam/utils.py b/neutron/ipam/utils.py index 1a4b303f959..f1e51f17dea 100644 --- a/neutron/ipam/utils.py +++ b/neutron/ipam/utils.py @@ -62,12 +62,17 @@ def generate_pools(cidr, gateway_ip): if first == last: # handle single address subnet case return [netaddr.IPRange(first, last)] - first_ip = first + 1 - # last address is broadcast in v4 - last_ip = last - (ip_version == 4) - if first_ip >= last_ip: - # /31 lands here - return [] + if ip_version == constants.IP_VERSION_4: + if net.prefixlen <= 30: + first_ip = first + 1 + # last address is broadcast in v4 + last_ip = last - (ip_version == 4) + else: + first_ip = first + last_ip = last + else: # IPv6 case + first_ip = first + 1 + last_ip = last ipset = netaddr.IPSet(netaddr.IPRange(first_ip, last_ip)) if gateway_ip: ipset.remove(netaddr.IPAddress(gateway_ip, ip_version)) diff --git a/neutron/tests/fullstack/resources/machine.py b/neutron/tests/fullstack/resources/machine.py index 3184f10362c..567b9b4cd4c 100644 --- a/neutron/tests/fullstack/resources/machine.py +++ b/neutron/tests/fullstack/resources/machine.py @@ -129,7 +129,10 @@ class FakeFullstackMachine(machine_fixtures.FakeMachineBase): def _configure_static_ipaddress(self): self.port.addr.add(self.ip_cidr) if self.gateway_ip: - net_helpers.set_namespace_gateway(self.port, self.gateway_ip) + net = netaddr.IPNetwork(self.ip_cidr) + gateway_ip = netaddr.IPAddress(self.gateway_ip) + if gateway_ip in net: + net_helpers.set_namespace_gateway(self.port, self.gateway_ip) def _configure_ipaddress_via_dhcp(self): self._start_async_dhclient() diff --git a/neutron/tests/fullstack/test_subnet.py b/neutron/tests/fullstack/test_subnet.py index d65390ea6f2..d95e0ccbaba 100644 --- a/neutron/tests/fullstack/test_subnet.py +++ b/neutron/tests/fullstack/test_subnet.py @@ -20,6 +20,7 @@ from oslo_utils import uuidutils from neutron.tests.common.exclusive_resources import ip_network from neutron.tests.fullstack import base from neutron.tests.fullstack.resources import environment +from neutron.tests.fullstack.resources import machine class TestSubnet(base.BaseFullStackTestCase): @@ -132,3 +133,32 @@ class TestSubnet(base.BaseFullStackTestCase): subnet = self._show_subnet(subnet['id']) self.assertEqual(subnet['subnet']['cidr'], str(subnets[2].cidr)) self.assertEqual(subnet['subnet']['gateway_ip'], str(gateway_ip)) + + def test_subnet_with_prefixlen_31_connectivity(self): + network = self._create_network(self._project_id) + self.safe_client.create_subnet( + self._project_id, network['id'], + cidr='10.14.0.20/31', + gateway_ip='10.14.0.19', + name='subnet-test', + enable_dhcp=False) + + vms = self._prepare_vms_in_net(self._project_id, network, False) + vms.ping_all() + + def test_subnet_with_prefixlen_32_vm_spawn(self): + network = self._create_network(self._project_id) + self.safe_client.create_subnet( + self._project_id, network['id'], + cidr='10.14.0.20/32', + gateway_ip='10.14.0.19', + name='subnet-test', + enable_dhcp=False) + + vm = self.useFixture( + machine.FakeFullstackMachine( + self.environment.hosts[0], + network['id'], + self._project_id, + self.safe_client)) + vm.block_until_boot() 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 175040b677e..9002dbec9c2 100644 --- a/neutron/tests/unit/db/test_db_base_plugin_v2.py +++ b/neutron/tests/unit/db/test_db_base_plugin_v2.py @@ -1693,16 +1693,6 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s self.assertEqual(data['port']['fixed_ips'], res['port']['fixed_ips']) - def test_no_more_port_exception(self): - with self.subnet(cidr='10.0.0.0/31', enable_dhcp=False, - gateway_ip=None) as subnet: - id = subnet['subnet']['network_id'] - res = self._create_port(self.fmt, id) - data = self.deserialize(self.fmt, res) - msg = str(lib_exc.IpAddressGenerationFailure(net_id=id)) - self.assertEqual(data['NeutronError']['message'], msg) - self.assertEqual(webob.exc.HTTPConflict.code, res.status_int) - def test_create_ports_native_quotas(self): quota = 1 cfg.CONF.set_override('quota_port', quota, group='QUOTAS') @@ -5091,6 +5081,43 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): res = req.get_response(self.api) self.assertEqual(webob.exc.HTTPConflict.code, res.status_int) + def test_create_subnet_allocation_pools_with_prefixlen_31(self): + with self.network() as network: + with self.subnet(network=network, + enable_dhcp=False, + gateway_ip='10.14.0.19', + cidr='10.14.0.20/31'): + res = self._create_port(self.fmt, network['network']['id']) + self.assertEqual(webob.exc.HTTPCreated.code, res.status_int) + res = self._create_port(self.fmt, network['network']['id']) + self.assertEqual(webob.exc.HTTPCreated.code, res.status_int) + + def test_update_subnet_allocation_pools_with_prefixlen_31(self): + with self.network() as network: + with self.subnet(network=network, + enable_dhcp=False, + gateway_ip='10.14.0.19', + cidr='10.14.0.20/31') as subnet: + data = {'subnet': {'allocation_pools': [ + {'start': '10.14.0.20', 'end': '10.14.0.21'}]}} + req = self.new_update_request('subnets', data, + subnet['subnet']['id']) + res = req.get_response(self.api) + self.assertEqual(webob.exc.HTTPOk.code, res.status_int) + res = self._create_port(self.fmt, network['network']['id']) + self.assertEqual(webob.exc.HTTPCreated.code, res.status_int) + res = self._create_port(self.fmt, network['network']['id']) + self.assertEqual(webob.exc.HTTPCreated.code, res.status_int) + + def test_create_subnet_allocation_pools_with_prefixlen_32(self): + with self.network() as network: + with self.subnet(network=network, + enable_dhcp=False, + gateway_ip='10.14.0.19', + cidr='10.14.0.20/32'): + res = self._create_port(self.fmt, network['network']['id']) + self.assertEqual(webob.exc.HTTPCreated.code, res.status_int) + def test_create_subnets_native_quotas(self): quota = 1 cfg.CONF.set_override('quota_subnet', quota, group='QUOTAS') diff --git a/neutron/tests/unit/ipam/test_utils.py b/neutron/tests/unit/ipam/test_utils.py index b3b77e6ebea..77090701038 100644 --- a/neutron/tests/unit/ipam/test_utils.py +++ b/neutron/tests/unit/ipam/test_utils.py @@ -89,7 +89,7 @@ class TestIpamUtils(base.BaseTestCase): def test_generate_pools_v4_31(self): cidr = '192.168.0.0/31' - expected = [] + expected = [netaddr.IPRange('192.168.0.0', '192.168.0.1')] self.assertEqual(expected, utils.generate_pools(cidr, None)) def test_generate_pools_v4_gateway_middle(self): diff --git a/releasenotes/notes/create-subnet-with-prefixlen-31-32-4cf8c9417325721a.yaml b/releasenotes/notes/create-subnet-with-prefixlen-31-32-4cf8c9417325721a.yaml new file mode 100644 index 00000000000..b05a6063885 --- /dev/null +++ b/releasenotes/notes/create-subnet-with-prefixlen-31-32-4cf8c9417325721a.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Neutron supports creating IPv4 subnet with prefixlen /31 and /32, + via disabling dhcp on a subnet. + For more information, see bug + `1580927 `_.