Merge "Get rid of delete_subnet method in ML2"
This commit is contained in:
commit
72916b54c9
@ -27,6 +27,7 @@ from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import uuidutils
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy import exc as sql_exc
|
||||
from sqlalchemy import not_
|
||||
|
||||
from neutron._i18n import _, _LE, _LI
|
||||
@ -883,10 +884,6 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
||||
|
||||
return result
|
||||
|
||||
def _subnet_check_ip_allocations(self, context, subnet_id):
|
||||
return (context.session.query(models_v2.IPAllocation).
|
||||
filter_by(subnet_id=subnet_id).join(models_v2.Port).first())
|
||||
|
||||
def _subnet_get_user_allocation(self, context, subnet_id):
|
||||
"""Check if there are any user ports on subnet and return first."""
|
||||
# need to join with ports table as IPAllocation's port
|
||||
@ -911,55 +908,71 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
||||
"cannot delete", subnet_id)
|
||||
raise exc.SubnetInUse(subnet_id=subnet_id)
|
||||
|
||||
@db_api.retry_if_session_inactive()
|
||||
def _remove_subnet_from_port(self, context, sub_id, port_id, auto_subnet):
|
||||
try:
|
||||
fixed = [f for f in self.get_port(context, port_id)['fixed_ips']
|
||||
if f['subnet_id'] != sub_id]
|
||||
if auto_subnet:
|
||||
# special flag to avoid re-allocation on auto subnets
|
||||
fixed.append({'subnet_id': sub_id, 'delete_subnet': True})
|
||||
data = {attributes.PORT: {'fixed_ips': fixed}}
|
||||
self.update_port(context, port_id, data)
|
||||
except exc.PortNotFound:
|
||||
# port is gone
|
||||
return
|
||||
except exc.SubnetNotFound as e:
|
||||
# another subnet in the fixed ips was concurrently removed. retry
|
||||
raise os_db_exc.RetryRequest(e)
|
||||
|
||||
def _ensure_no_user_ports_on_subnet(self, context, id):
|
||||
alloc = self._subnet_get_user_allocation(context, id)
|
||||
if alloc:
|
||||
LOG.info(_LI("Found port (%(port_id)s, %(ip)s) having IP "
|
||||
"allocation on subnet "
|
||||
"%(subnet)s, cannot delete"),
|
||||
{'ip': alloc.ip_address,
|
||||
'port_id': alloc.port_id,
|
||||
'subnet': id})
|
||||
raise exc.SubnetInUse(subnet_id=id)
|
||||
|
||||
@db_api.retry_if_session_inactive()
|
||||
def _remove_subnet_ip_allocations_from_ports(self, context, id):
|
||||
# Do not allow a subnet to be deleted if a router is attached to it
|
||||
self._subnet_check_ip_allocations_internal_router_ports(
|
||||
context, id)
|
||||
subnet = self._get_subnet(context, id)
|
||||
is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet)
|
||||
if not is_auto_addr_subnet:
|
||||
# we only automatically remove IP addresses from user ports if
|
||||
# the IPs come from auto allocation subnets.
|
||||
self._ensure_no_user_ports_on_subnet(context, id)
|
||||
net_allocs = (context.session.query(models_v2.IPAllocation.port_id).
|
||||
filter_by(subnet_id=id))
|
||||
port_ids_on_net = [ipal.port_id for ipal in net_allocs]
|
||||
for port_id in port_ids_on_net:
|
||||
self._remove_subnet_from_port(context, id, port_id,
|
||||
auto_subnet=is_auto_addr_subnet)
|
||||
|
||||
@db_api.retry_if_session_inactive()
|
||||
def delete_subnet(self, context, id):
|
||||
with context.session.begin(subtransactions=True):
|
||||
subnet = self._get_subnet(context, id)
|
||||
|
||||
# Make sure the subnet isn't used by other resources
|
||||
_check_subnet_not_used(context, id)
|
||||
|
||||
# Delete all network owned ports
|
||||
qry_network_ports = (
|
||||
context.session.query(models_v2.IPAllocation).
|
||||
filter_by(subnet_id=subnet['id']).
|
||||
join(models_v2.Port))
|
||||
# Remove network owned ports, and delete IP allocations
|
||||
# for IPv6 addresses which were automatically generated
|
||||
# via SLAAC
|
||||
is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(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)))
|
||||
network_ports = qry_network_ports.all()
|
||||
if network_ports:
|
||||
for port in network_ports:
|
||||
context.session.delete(port)
|
||||
# Check if there are more IP allocations, unless
|
||||
# is_auto_address_subnet is True. In that case the check is
|
||||
# unnecessary. This additional check not only would be wasteful
|
||||
# for this class of subnet, but is also error-prone since when
|
||||
# the isolation level is set to READ COMMITTED allocations made
|
||||
# concurrently will be returned by this query
|
||||
if not is_auto_addr_subnet:
|
||||
alloc = self._subnet_check_ip_allocations(context, id)
|
||||
if alloc:
|
||||
LOG.info(_LI("Found port (%(port_id)s, %(ip)s) having IP "
|
||||
"allocation on subnet "
|
||||
"%(subnet)s, cannot delete"),
|
||||
{'ip': alloc.ip_address,
|
||||
'port_id': alloc.port_id,
|
||||
'subnet': id})
|
||||
raise exc.SubnetInUse(subnet_id=id)
|
||||
|
||||
context.session.delete(subnet)
|
||||
LOG.debug("Deleting subnet %s", id)
|
||||
# Make sure the subnet isn't used by other resources
|
||||
_check_subnet_not_used(context, id)
|
||||
self._remove_subnet_ip_allocations_from_ports(context, id)
|
||||
# retry integrity errors to catch ip allocation races
|
||||
with db_api.exc_to_retry(sql_exc.IntegrityError), \
|
||||
context.session.begin(subtransactions=True):
|
||||
subnet_db = self._get_subnet(context, id)
|
||||
subnet = self._make_subnet_dict(subnet_db, context=context)
|
||||
registry.notify(resources.SUBNET, events.PRECOMMIT_DELETE,
|
||||
self, context=context, subnet_id=id)
|
||||
context.session.delete(subnet_db)
|
||||
# Delete related ipam subnet manually,
|
||||
# since there is no FK relationship
|
||||
self.ipam.delete_subnet(context, id)
|
||||
registry.notify(resources.SUBNET, events.AFTER_DELETE,
|
||||
self, context=context, subnet=subnet)
|
||||
|
||||
@db_api.retry_if_session_inactive()
|
||||
def get_subnet(self, context, id, fields=None):
|
||||
|
@ -43,7 +43,6 @@ from neutron.callbacks import exceptions
|
||||
from neutron.callbacks import registry
|
||||
from neutron.callbacks import resources
|
||||
from neutron.common import constants as n_const
|
||||
from neutron.common import ipv6_utils
|
||||
from neutron.common import rpc as n_rpc
|
||||
from neutron.common import topics
|
||||
from neutron.common import utils
|
||||
@ -964,142 +963,30 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
return updated_subnet
|
||||
|
||||
@utils.transaction_guard
|
||||
@db_api.retry_if_session_inactive()
|
||||
def delete_subnet(self, context, id):
|
||||
# REVISIT(rkukura) The super(Ml2Plugin, self).delete_subnet()
|
||||
# function is not used because it deallocates the subnet's addresses
|
||||
# from ports in the DB without invoking the derived class's
|
||||
# update_port(), preventing mechanism drivers from being called.
|
||||
# This approach should be revisited when the API layer is reworked
|
||||
# during icehouse.
|
||||
# the only purpose of this override is to protect this from being
|
||||
# called inside of a transaction.
|
||||
return super(Ml2Plugin, self).delete_subnet(context, id)
|
||||
|
||||
LOG.debug("Deleting subnet %s", id)
|
||||
session = context.session
|
||||
deallocated = set()
|
||||
while True:
|
||||
# NOTE(kevinbenton): this loop keeps db objects in scope
|
||||
# so we must expire them or risk stale reads.
|
||||
# see bug/1623990
|
||||
session.expire_all()
|
||||
with session.begin(subtransactions=True):
|
||||
record = self._get_subnet(context, id)
|
||||
subnet = self._make_subnet_dict(record, None, context=context)
|
||||
qry_allocated = (session.query(models_v2.IPAllocation).
|
||||
filter_by(subnet_id=id).
|
||||
join(models_v2.Port))
|
||||
is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet)
|
||||
# Remove network owned ports, and delete IP allocations
|
||||
# for IPv6 addresses which were automatically generated
|
||||
# via SLAAC
|
||||
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)))
|
||||
allocated = set(qry_allocated.all())
|
||||
LOG.debug("Ports to auto-deallocate: %s", allocated)
|
||||
if not is_auto_addr_subnet:
|
||||
user_alloc = self._subnet_get_user_allocation(
|
||||
context, id)
|
||||
if user_alloc:
|
||||
LOG.info(_LI("Found port (%(port_id)s, %(ip)s) "
|
||||
"having IP allocation on subnet "
|
||||
"%(subnet)s, cannot delete"),
|
||||
{'ip': user_alloc.ip_address,
|
||||
'port_id': user_alloc.port_id,
|
||||
'subnet': id})
|
||||
raise exc.SubnetInUse(subnet_id=id)
|
||||
@registry.receives(resources.SUBNET, [events.PRECOMMIT_DELETE])
|
||||
def _subnet_delete_precommit_handler(self, rtype, event, trigger,
|
||||
context, subnet_id, **kwargs):
|
||||
record = self._get_subnet(context, subnet_id)
|
||||
subnet = self._make_subnet_dict(record, context=context)
|
||||
network = self.get_network(context, subnet['network_id'])
|
||||
mech_context = driver_context.SubnetContext(self, context,
|
||||
subnet, network)
|
||||
# TODO(kevinbenton): move this mech context into something like
|
||||
# a 'delete context' so it's not polluting the real context object
|
||||
setattr(context, '_mech_context', mech_context)
|
||||
self.mechanism_manager.delete_subnet_precommit(mech_context)
|
||||
|
||||
db_base_plugin_v2._check_subnet_not_used(context, id)
|
||||
|
||||
# SLAAC allocations currently can not be removed using
|
||||
# update_port workflow, and will persist in 'allocated'.
|
||||
# So for now just make sure update_port is called once for
|
||||
# them so MechanismDrivers is aware of the change.
|
||||
# This way SLAAC allocation is deleted by FK on subnet deletion
|
||||
# TODO(pbondar): rework update_port workflow to allow deletion
|
||||
# of SLAAC allocation via update_port.
|
||||
to_deallocate = allocated - deallocated
|
||||
|
||||
# If to_deallocate is blank, then all known IPAllocations
|
||||
# (except SLAAC allocations) were correctly deleted
|
||||
# during the previous pass.
|
||||
# Check if there are more IP allocations, unless
|
||||
# is_auto_address_subnet is True. If transaction isolation
|
||||
# level is set to READ COMMITTED allocations made
|
||||
# concurrently will be returned by this query and transaction
|
||||
# will be restarted. It works for REPEATABLE READ isolation
|
||||
# level too because this query is executed only once during
|
||||
# transaction, and if concurrent allocations are detected
|
||||
# transaction gets restarted. Executing this query second time
|
||||
# in transaction would result in not seeing allocations
|
||||
# committed by concurrent transactions.
|
||||
if not to_deallocate:
|
||||
if (not is_auto_addr_subnet and
|
||||
self._subnet_check_ip_allocations(context, id)):
|
||||
# allocation found and it was DHCP port
|
||||
# that appeared after autodelete ports were
|
||||
# removed - continue iteration to find offending port
|
||||
# to update or raise SubnetInUse
|
||||
continue
|
||||
network = self.get_network(context, subnet['network_id'])
|
||||
mech_context = driver_context.SubnetContext(self, context,
|
||||
subnet,
|
||||
network)
|
||||
self.mechanism_manager.delete_subnet_precommit(
|
||||
mech_context)
|
||||
|
||||
LOG.debug("Deleting subnet record")
|
||||
session.delete(record)
|
||||
|
||||
# The super(Ml2Plugin, self).delete_subnet() is not called,
|
||||
# so need to manually call delete_subnet for pluggable ipam
|
||||
self.ipam.delete_subnet(context, id)
|
||||
|
||||
LOG.debug("Committing transaction")
|
||||
break
|
||||
|
||||
for a in to_deallocate:
|
||||
if a.port:
|
||||
# calling update_port() for each allocation to remove the
|
||||
# IP from the port and call the MechanismDrivers
|
||||
fixed_ips = [{'subnet_id': ip.subnet_id,
|
||||
'ip_address': ip.ip_address}
|
||||
for ip in a.port.fixed_ips
|
||||
if ip.subnet_id != id]
|
||||
# By default auto-addressed ips are not removed from port
|
||||
# on port update, so mark subnet with 'delete_subnet' flag
|
||||
# to force ip deallocation on port update.
|
||||
if is_auto_addr_subnet:
|
||||
fixed_ips.append({'subnet_id': id,
|
||||
'delete_subnet': True})
|
||||
data = {attributes.PORT: {'fixed_ips': fixed_ips}}
|
||||
try:
|
||||
# NOTE Don't inline port_id; needed for PortNotFound.
|
||||
port_id = a.port_id
|
||||
self.update_port(context, port_id, data)
|
||||
except exc.PortNotFound:
|
||||
# NOTE Attempting to access a.port_id here is an error.
|
||||
LOG.debug("Port %s deleted concurrently", port_id)
|
||||
except exc.SubnetNotFound:
|
||||
# NOTE we hit here if another subnet was concurrently
|
||||
# removed that the port has a fixed_ip on. we just
|
||||
# continue so the loop re-iterates and the IPs are
|
||||
# looked up again
|
||||
continue
|
||||
except Exception as e:
|
||||
with excutils.save_and_reraise_exception():
|
||||
utils.attach_exc_details(
|
||||
e, _LE("Exception deleting fixed_ip from "
|
||||
"port %s"), port_id)
|
||||
deallocated.add(a)
|
||||
|
||||
kwargs = {'context': context, 'subnet': subnet}
|
||||
registry.notify(resources.SUBNET, events.AFTER_DELETE, self, **kwargs)
|
||||
@registry.receives(resources.SUBNET, [events.AFTER_DELETE])
|
||||
def _subnet_delete_after_delete_handler(self, rtype, event, trigger,
|
||||
context, subnet, **kwargs):
|
||||
try:
|
||||
self.mechanism_manager.delete_subnet_postcommit(mech_context)
|
||||
self.mechanism_manager.delete_subnet_postcommit(
|
||||
context._mech_context)
|
||||
except ml2_exc.MechanismDriverError:
|
||||
# TODO(apech) - One or more mechanism driver failed to
|
||||
# delete the subnet. Ideally we'd notify the caller of
|
||||
|
@ -515,7 +515,7 @@ class TestMl2SubnetsV2(test_plugin.TestSubnetsV2,
|
||||
subnet_id = subnet['subnet']['id']
|
||||
attempt = [0]
|
||||
|
||||
def check_and_create_ports(context, subnet_id):
|
||||
def create_dhcp_port(*args, **kwargs):
|
||||
"""A method to emulate race condition.
|
||||
|
||||
Adds dhcp port in the middle of subnet delete
|
||||
@ -534,26 +534,17 @@ class TestMl2SubnetsV2(test_plugin.TestSubnetsV2,
|
||||
port_req = self.new_create_request('ports', data)
|
||||
port_res = port_req.get_response(self.api)
|
||||
self.assertEqual(201, port_res.status_int)
|
||||
return (context.session.query(models_v2.IPAllocation).
|
||||
filter_by(subnet_id=subnet_id).
|
||||
join(models_v2.Port).first())
|
||||
|
||||
plugin = directory.get_plugin()
|
||||
# we mock _subnet_check_ip_allocations with method
|
||||
# that creates DHCP port 'in the middle' of subnet_delete
|
||||
# causing retry this way subnet is deleted on the
|
||||
# second attempt
|
||||
with mock.patch.object(plugin, '_subnet_check_ip_allocations',
|
||||
side_effect=check_and_create_ports):
|
||||
req = self.new_delete_request('subnets', subnet_id)
|
||||
res = req.get_response(self.api)
|
||||
self.assertEqual(204, res.status_int)
|
||||
# Validate chat check is called twice, i.e. after the
|
||||
# first check transaction gets restarted.
|
||||
calls = [mock.call(mock.ANY, subnet_id),
|
||||
mock.call(mock.ANY, subnet_id)]
|
||||
plugin._subnet_check_ip_allocations.assert_has_calls = (
|
||||
calls)
|
||||
registry.subscribe(create_dhcp_port, resources.SUBNET,
|
||||
events.PRECOMMIT_DELETE)
|
||||
req = self.new_delete_request('subnets', subnet_id)
|
||||
res = req.get_response(self.api)
|
||||
self.assertEqual(204, res.status_int)
|
||||
self.assertEqual(1, attempt[0])
|
||||
|
||||
def test_create_subnet_check_mtu_in_mech_context(self):
|
||||
plugin = directory.get_plugin()
|
||||
|
Loading…
x
Reference in New Issue
Block a user