Capture NotFound Exceptions in subnet postcommit

Change I28323eddcf34fd12002cc706f4a9bcbb3f976ceb introduced
a subtle difference in the types of exceptions that can be
encountered during the operations after creating a subnet.

Before that change, a giant enclosing transaction would provide
REPEATABLE READ guarantees, so once a port was looked up by
IPAM, calling update_port on that port afterwards in the same
transaction would not result in a PortNotFound exception.
The same applied to looking up a router and then updating its
gateway port.

Now that there is no giant enclosing transaction, we have to
capture the possible PortNotFound and RouterNotFound exceptions
that can occur during these post commit subnet create operations.

Closes-Bug: #1644502
Change-Id: I29e2bf7bcda37ebbcee193f94c8b03df90f24ef1
This commit is contained in:
Kevin Benton 2016-11-24 04:17:15 -08:00
parent 41d2c23560
commit 9e006cbe64
3 changed files with 62 additions and 23 deletions

View File

@ -588,28 +588,37 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
gw_ports = self._get_router_gw_ports_by_network(context, gw_ports = self._get_router_gw_ports_by_network(context,
network['id']) network['id'])
router_ids = [p['device_id'] for p in gw_ports] router_ids = [p['device_id'] for p in gw_ports]
ctx_admin = context.elevated()
ext_subnets_dict = {s['id']: s for s in network['subnets']}
for id in router_ids: for id in router_ids:
router = l3plugin.get_router(ctx_admin, id) try:
external_gateway_info = router['external_gateway_info'] self._update_router_gw_port(context, id, network, subnet)
# Get all stateful (i.e. non-SLAAC/DHCPv6-stateless) fixed ips except l3.RouterNotFound:
fips = [f for f in external_gateway_info['external_fixed_ips'] LOG.debug("Router %(id)s was concurrently deleted while "
if not ipv6_utils.is_auto_address_subnet( "updating GW port for subnet %(s)s",
ext_subnets_dict[f['subnet_id']])] {'id': id, 's': subnet})
num_fips = len(fips)
# Don't add the fixed IP to the port if it already def _update_router_gw_port(self, context, router_id, network, subnet):
# has a stateful fixed IP of the same IP version l3plugin = directory.get_plugin(constants.L3)
if num_fips > 1: ctx_admin = context.elevated()
continue ext_subnets_dict = {s['id']: s for s in network['subnets']}
if num_fips == 1 and netaddr.IPAddress( router = l3plugin.get_router(ctx_admin, router_id)
fips[0]['ip_address']).version == subnet['ip_version']: external_gateway_info = router['external_gateway_info']
continue # Get all stateful (i.e. non-SLAAC/DHCPv6-stateless) fixed ips
external_gateway_info['external_fixed_ips'].append( fips = [f for f in external_gateway_info['external_fixed_ips']
{'subnet_id': subnet['id']}) if not ipv6_utils.is_auto_address_subnet(
info = {'router': {'external_gateway_info': ext_subnets_dict[f['subnet_id']])]
external_gateway_info}} num_fips = len(fips)
l3plugin.update_router(context, id, info) # Don't add the fixed IP to the port if it already
# has a stateful fixed IP of the same IP version
if num_fips > 1:
return
if num_fips == 1 and netaddr.IPAddress(
fips[0]['ip_address']).version == subnet['ip_version']:
return
external_gateway_info['external_fixed_ips'].append(
{'subnet_id': subnet['id']})
info = {'router': {'external_gateway_info':
external_gateway_info}}
l3plugin.update_router(context, router_id, info)
@db_api.retry_if_session_inactive() @db_api.retry_if_session_inactive()
def _create_subnet_postcommit(self, context, result, network, ipam_subnet): def _create_subnet_postcommit(self, context, result, network, ipam_subnet):
@ -624,7 +633,12 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
result, ipam_subnet) result, ipam_subnet)
for port_id in updated_ports: for port_id in updated_ports:
port_info = {'port': {'id': port_id}} port_info = {'port': {'id': port_id}}
self.update_port(context, port_id, port_info) try:
self.update_port(context, port_id, port_info)
except exc.PortNotFound:
LOG.debug("Port %(p)s concurrently deleted while adding "
"address for new subnet %(s)s.", {'p': port_id,
's': result})
def _get_subnetpool_id(self, context, subnet): def _get_subnetpool_id(self, context, subnet):
"""Return the subnetpool id for this request """Return the subnetpool id for this request

View File

@ -4226,7 +4226,7 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
def _test_create_subnet_ipv6_auto_addr_with_port_on_network( def _test_create_subnet_ipv6_auto_addr_with_port_on_network(
self, addr_mode, device_owner=DEVICE_OWNER_COMPUTE, self, addr_mode, device_owner=DEVICE_OWNER_COMPUTE,
insert_db_reference_error=False): insert_db_reference_error=False, insert_port_not_found=False):
# Create a network with one IPv4 subnet and one port # Create a network with one IPv4 subnet and one port
with self.network() as network,\ with self.network() as network,\
self.subnet(network=network) as v4_subnet,\ self.subnet(network=network) as v4_subnet,\
@ -4254,6 +4254,9 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
# Add an IPv6 auto-address subnet to the network # Add an IPv6 auto-address subnet to the network
with mock.patch.object(directory.get_plugin(), with mock.patch.object(directory.get_plugin(),
'update_port') as mock_updated_port: 'update_port') as mock_updated_port:
if insert_port_not_found:
mock_updated_port.side_effect = lib_exc.PortNotFound(
port_id=port['port']['id'])
v6_subnet = self._make_subnet(self.fmt, network, 'fe80::1', v6_subnet = self._make_subnet(self.fmt, network, 'fe80::1',
'fe80::/64', ip_version=6, 'fe80::/64', ip_version=6,
ipv6_ra_mode=addr_mode, ipv6_ra_mode=addr_mode,
@ -4306,6 +4309,10 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
self._test_create_subnet_ipv6_auto_addr_with_port_on_network( self._test_create_subnet_ipv6_auto_addr_with_port_on_network(
constants.IPV6_SLAAC, insert_db_reference_error=True) constants.IPV6_SLAAC, insert_db_reference_error=True)
def test_create_subnet_ipv6_slaac_with_port_not_found(self):
self._test_create_subnet_ipv6_auto_addr_with_port_on_network(
constants.IPV6_SLAAC, insert_port_not_found=True)
def test_update_subnet_no_gateway(self): def test_update_subnet_no_gateway(self):
with self.subnet() as subnet: with self.subnet() as subnet:
data = {'subnet': {'gateway_ip': '10.0.0.1'}} data = {'subnet': {'gateway_ip': '10.0.0.1'}}

View File

@ -871,6 +871,24 @@ class L3NatTestCaseBase(L3NatTestCaseMixin):
self.assertNotEqual(fip1['ip_address'], self.assertNotEqual(fip1['ip_address'],
fip2['ip_address']) fip2['ip_address'])
def test_router_concurrent_delete_upon_subnet_create(self):
with self.network() as n:
with self.subnet(network=n) as s1, self.router() as r:
self._set_net_external(n['network']['id'])
self._add_external_gateway_to_router(
r['router']['id'],
n['network']['id'],
ext_ips=[{'subnet_id': s1['subnet']['id']}])
plugin = directory.get_plugin(lib_constants.L3)
mock.patch.object(
plugin, 'update_router',
side_effect=l3.RouterNotFound(router_id='1')).start()
# ensure the router disappearing doesn't interfere with subnet
# creation
self._create_subnet(self.fmt, net_id=n['network']['id'],
ip_version=6, cidr='2001:db8::/32',
expected_res_status=(exc.HTTPCreated.code))
def test_router_update_gateway_upon_subnet_create_ipv6(self): def test_router_update_gateway_upon_subnet_create_ipv6(self):
with self.network() as n: with self.network() as n:
with self.subnet(network=n) as s1, self.router() as r: with self.subnet(network=n) as s1, self.router() as r: