From e80ab8113edf54e5d3623f55bdbd11ab19cac1eb Mon Sep 17 00:00:00 2001 From: Andrew Boik Date: Mon, 23 Feb 2015 12:06:09 -0800 Subject: [PATCH] Raise error upon deleting subnet with router ports Fixes an issue where SLAAC and DHCPV6-stateless subnets can be deleted even if they are attached to an internal router port. This patch raises an exception whenever a subnet is deleted that has existing IP Allocations on an internal router port. Change-Id: I0a16156274b5736236654fca6700ef2d67f4519b Closes-Bug: #1424760 --- neutron/db/db_base_plugin_v2.py | 19 +++++++++++- neutron/plugins/ml2/plugin.py | 5 +++- neutron/tests/unit/test_db_plugin.py | 40 ++++++++++++++++++++++--- neutron/tests/unit/test_l3_plugin.py | 44 ++++++++++++++++++++-------- 4 files changed, 90 insertions(+), 18 deletions(-) diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index 03bbe948f..745806f74 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -1222,6 +1222,20 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, models_v2.IPAllocation).filter_by( subnet_id=subnet_id).join(models_v2.Port).first() + def _subnet_check_ip_allocations_internal_router_ports(self, context, + subnet_id): + # Do not delete the subnet if IP allocations for internal + # router ports still exist + allocs = context.session.query(models_v2.IPAllocation).filter_by( + subnet_id=subnet_id).join(models_v2.Port).filter( + models_v2.Port.device_owner.in_( + constants.ROUTER_INTERFACE_OWNERS) + ).first() + if allocs: + LOG.debug("Subnet %s still has internal router ports, " + "cannot delete", subnet_id) + raise n_exc.SubnetInUse(subnet_id=id) + def delete_subnet(self, context, id): with context.session.begin(subtransactions=True): subnet = self._get_subnet(context, id) @@ -1234,7 +1248,10 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, # for IPv6 addresses which were automatically generated # via SLAAC is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet) - if not is_auto_addr_subnet: + if is_auto_addr_subnet: + self._subnet_check_ip_allocations_internal_router_ports( + context, id) + else: qry_network_ports = ( qry_network_ports.filter(models_v2.Port.device_owner. in_(AUTO_DELETE_PORT_OWNERS))) diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index d490bd09a..d4ccdba3d 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -845,7 +845,10 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, # Remove network owned ports, and delete IP allocations # for IPv6 addresses which were automatically generated # via SLAAC - if not is_auto_addr_subnet: + if is_auto_addr_subnet: + self._subnet_check_ip_allocations_internal_router_ports( + context, id) + else: qry_allocated = ( qry_allocated.filter(models_v2.Port.device_owner. in_(db_base_plugin_v2.AUTO_DELETE_PORT_OWNERS))) diff --git a/neutron/tests/unit/test_db_plugin.py b/neutron/tests/unit/test_db_plugin.py index ae88c86a0..3d72dc5b8 100644 --- a/neutron/tests/unit/test_db_plugin.py +++ b/neutron/tests/unit/test_db_plugin.py @@ -2791,17 +2791,21 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): self.assertEqual(res.status_int, webob.exc.HTTPNoContent.code) - def test_delete_subnet_ipv6_slaac_port_exists(self): - """Test IPv6 SLAAC subnet delete when a port is still using subnet.""" + def _create_slaac_subnet_and_port(self, port_owner=None): + # Create an IPv6 SLAAC subnet and a port using that subnet res = self._create_network(fmt=self.fmt, name='net', admin_state_up=True) network = self.deserialize(self.fmt, res) - # Create an IPv6 SLAAC subnet and a port using that subnet subnet = self._make_subnet(self.fmt, network, gateway='fe80::1', cidr='fe80::/64', ip_version=6, ipv6_ra_mode=constants.IPV6_SLAAC, ipv6_address_mode=constants.IPV6_SLAAC) - res = self._create_port(self.fmt, net_id=network['network']['id']) + if port_owner: + res = self._create_port(self.fmt, net_id=network['network']['id'], + device_owner=port_owner) + else: + res = self._create_port(self.fmt, net_id=network['network']['id']) + port = self.deserialize(self.fmt, res) self.assertEqual(1, len(port['port']['fixed_ips'])) @@ -2811,6 +2815,11 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): sport = self.deserialize(self.fmt, req.get_response(self.api)) self.assertEqual(1, len(sport['port']['fixed_ips'])) + return subnet, port + + def test_delete_subnet_ipv6_slaac_port_exists(self): + """Test IPv6 SLAAC subnet delete when a port is still using subnet.""" + subnet, port = self._create_slaac_subnet_and_port() # Delete the subnet req = self.new_delete_request('subnets', subnet['subnet']['id']) res = req.get_response(self.api) @@ -2821,6 +2830,29 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase): sport = self.deserialize(self.fmt, req.get_response(self.api)) self.assertEqual(0, len(sport['port']['fixed_ips'])) + def test_delete_subnet_ipv6_slaac_router_port_exists(self): + """Test IPv6 SLAAC subnet delete with a router port using the subnet""" + subnet, port = self._create_slaac_subnet_and_port( + constants.DEVICE_OWNER_ROUTER_INTF) + # Delete the subnet and assert that we get a HTTP 409 error + req = self.new_delete_request('subnets', subnet['subnet']['id']) + res = req.get_response(self.api) + self.assertEqual(webob.exc.HTTPConflict.code, res.status_int) + # The subnet should still exist and the port should still have an + # address from the subnet + req = self.new_show_request('subnets', subnet['subnet']['id'], + self.fmt) + res = req.get_response(self.api) + ssubnet = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertIsNotNone(ssubnet) + req = self.new_show_request('ports', port['port']['id'], self.fmt) + res = req.get_response(self.api) + sport = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertEqual(1, len(sport['port']['fixed_ips'])) + port_subnet_ids = [fip['subnet_id'] for fip in + sport['port']['fixed_ips']] + self.assertIn(subnet['subnet']['id'], port_subnet_ids) + def test_delete_network(self): gateway_ip = '10.0.0.1' cidr = '10.0.0.0/24' diff --git a/neutron/tests/unit/test_l3_plugin.py b/neutron/tests/unit/test_l3_plugin.py index e44251a1d..24221b59c 100644 --- a/neutron/tests/unit/test_l3_plugin.py +++ b/neutron/tests/unit/test_l3_plugin.py @@ -1970,21 +1970,41 @@ class L3NatTestCaseBase(L3NatTestCaseMixin): break self.assertTrue(found) + def _test_router_delete_subnet_inuse_returns_409(self, router, subnet): + r, s = router, subnet + self._router_interface_action('add', + r['router']['id'], + s['subnet']['id'], + None) + # subnet cannot be deleted as it's attached to a router + self._delete('subnets', s['subnet']['id'], + expected_code=exc.HTTPConflict.code) + # remove interface so test can exit without errors + self._router_interface_action('remove', + r['router']['id'], + s['subnet']['id'], + None) + + def _ipv6_subnet(self, mode): + return self.subnet(cidr='fd00::1/64', gateway_ip='fd00::1', + ip_version=6, + ipv6_ra_mode=mode, + ipv6_address_mode=mode) + def test_router_delete_subnet_inuse_returns_409(self): with self.router() as r: with self.subnet() as s: - self._router_interface_action('add', - r['router']['id'], - s['subnet']['id'], - None) - # subnet cannot be delete as it's attached to a router - self._delete('subnets', s['subnet']['id'], - expected_code=exc.HTTPConflict.code) - # remove interface so test can exit without errors - self._router_interface_action('remove', - r['router']['id'], - s['subnet']['id'], - None) + self._test_router_delete_subnet_inuse_returns_409(r, s) + + def test_router_delete_ipv6_slaac_subnet_inuse_returns_409(self): + with self.router() as r: + with self._ipv6_subnet(l3_constants.IPV6_SLAAC) as s: + self._test_router_delete_subnet_inuse_returns_409(r, s) + + def test_router_delete_dhcpv6_stateless_subnet_inuse_returns_409(self): + with self.router() as r: + with self._ipv6_subnet(l3_constants.DHCPV6_STATELESS) as s: + self._test_router_delete_subnet_inuse_returns_409(r, s) def test_delete_ext_net_with_disassociated_floating_ips(self): with self.network() as net: -- 2.45.2