Guard against concurrent port removal in DVR

The delete_csnat_router_interface_ports method constructs
a generator expression to retrieve the ports attached
to a router. If a concurrent process manages to delete
one of the ports referenced in the generator from the DB
after it is constructed (but before iteration), some of the
ports will be None objects on iteration.

This patch verifies that the port db object is there
before trying to extract the ID from it.

More details:

This is because the query expression[1] is evaluated immediately
at the time the generator is defined. If there are ports in the
DB at the time of this evaluation, RouterPort objects will be
prepared to be iterated over. However, before the iteration over the
generator happens, something else may delete the port from the DB.
If that happens, when the iteration starts and the 'port' attribute
is accessed on the router port, a SELECT statement will be issued to
the DB to get that port[2] and it will return None.

1. router.attached_ports.filter_by(port_type=DEVICE_OWNER_DVR_SNAT)
2. The RouterPorts table's relationship to the Ports table is using
   the default 'select' lazy loading.

Closes-Bug: #1381263
Change-Id: Ia371545d641aaa1cbaa0fd10cd233250ec5769e5
This commit is contained in:
Kevin Benton 2014-10-15 18:03:37 -07:00
parent 6896befbfd
commit 47a846b34f
2 changed files with 34 additions and 0 deletions

View File

@ -578,6 +578,7 @@ class L3_NAT_with_dvr_db_mixin(l3_db.L3_NAT_db_mixin,
ports = (
rp.port.id for rp in
router.attached_ports.filter_by(port_type=DEVICE_OWNER_DVR_SNAT)
if rp.port
)
c_snat_ports = self._core_plugin.get_ports(

View File

@ -24,6 +24,7 @@ from neutron.common import exceptions as exc
from neutron.common import utils
from neutron import context
from neutron.db import db_base_plugin_v2 as base_plugin
from neutron.db import l3_db
from neutron.extensions import external_net as external_net
from neutron.extensions import l3agentscheduler
from neutron.extensions import multiprovidernet as mpnet
@ -296,6 +297,38 @@ class TestMl2DvrPortsV2(TestMl2PortsV2):
self._test_delete_dvr_serviced_port(
device_owner=constants.DEVICE_OWNER_LOADBALANCER)
def test_concurrent_csnat_port_delete(self):
plugin = manager.NeutronManager.get_service_plugins()[
service_constants.L3_ROUTER_NAT]
r = plugin.create_router(
self.context,
{'router': {'name': 'router', 'admin_state_up': True}})
with self.subnet() as s:
p = plugin.add_router_interface(self.context, r['id'],
{'subnet_id': s['subnet']['id']})
# lie to turn the port into an SNAT interface
with self.context.session.begin():
rp = self.context.session.query(l3_db.RouterPort).filter_by(
port_id=p['port_id']).first()
rp.port_type = constants.DEVICE_OWNER_ROUTER_SNAT
# take the port away before csnat gets a chance to delete it
# to simulate a concurrent delete
orig_get_ports = plugin._core_plugin.get_ports
def get_ports_with_delete_first(*args, **kwargs):
plugin._core_plugin.delete_port(self.context,
p['port_id'],
l3_port_check=False)
return orig_get_ports(*args, **kwargs)
plugin._core_plugin.get_ports = get_ports_with_delete_first
# This should be able to handle a concurrent delete without raising
# an exception
router = plugin._get_router(self.context, r['id'])
plugin.delete_csnat_router_interface_ports(self.context, router)
class TestMl2PortBinding(Ml2PluginV2TestCase,
test_bindings.PortBindingsTestCase):