diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index 3f8adc3850..b6288ddfab 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -1279,9 +1279,13 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2, filter_by(network_id=subnet['network_id']). with_lockmode('update')) - # remove network owned ports + # Remove network owned ports, and delete IP allocations + # for IPv6 addresses which were automatically generated + # via SLAAC + is_ipv6_slaac_subnet = ipv6_utils.is_slaac_subnet(subnet) for a in allocated: - if a.ports.device_owner in AUTO_DELETE_PORT_OWNERS: + if (is_ipv6_slaac_subnet or + a.ports.device_owner in AUTO_DELETE_PORT_OWNERS): NeutronDbPluginV2._delete_ip_allocation( context, subnet.network_id, id, a.ip_address) else: diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 0f596a66c2..09c6e95239 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -31,6 +31,7 @@ from neutron.api.rpc.handlers import securitygroups_rpc from neutron.api.v2 import attributes from neutron.common import constants as const from neutron.common import exceptions as exc +from neutron.common import ipv6_utils from neutron.common import rpc as n_rpc from neutron.common import topics from neutron.common import utils @@ -729,7 +730,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, LOG.debug(_("Ports to auto-deallocate: %s"), allocated) only_auto_del = all(not a.port_id or a.ports.device_owner in db_base_plugin_v2. - AUTO_DELETE_PORT_OWNERS + AUTO_DELETE_PORT_OWNERS or + ipv6_utils.is_slaac_subnet(subnet) for a in allocated) if not only_auto_del: LOG.debug(_("Tenant-owned ports exist")) diff --git a/neutron/tests/unit/test_db_plugin.py b/neutron/tests/unit/test_db_plugin.py index 465183fcdd..b5051b38e2 100644 --- a/neutron/tests/unit/test_db_plugin.py +++ b/neutron/tests/unit/test_db_plugin.py @@ -2641,6 +2641,36 @@ 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.""" + 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']) + port = self.deserialize(self.fmt, res) + self.assertEqual(1, len(port['port']['fixed_ips'])) + + # The port should have an address from the subnet + 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'])) + + # Delete the subnet + req = self.new_delete_request('subnets', subnet['subnet']['id']) + res = req.get_response(self.api) + self.assertEqual(webob.exc.HTTPNoContent.code, res.status_int) + # The port should no longer have an address from the deleted subnet + 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(0, len(sport['port']['fixed_ips'])) + def test_delete_network(self): gateway_ip = '10.0.0.1' cidr = '10.0.0.0/24'