NSXv3: Add plugin-specific create_subnet_bulk function

When neutron's create_subnet_bulk is invoked for
creating multiple subnets in one session, if a DB
deadlock exception happened in the middle of the
bulk command, neutron will try to rollback the
session and re-create all the subnets.

This could cause problem in the backend because
neutron only rollback those entries temporarily
stored in the session, such as the neuron subnets
and neutron ports created during individual
create_subnet operation. Those backend entries
created before the exception have references or
mappings also temporarily stored in the session.
Once the session is rolled back, the plugin will
lose the bindings to those backend entries while
they still exist there.

This patch allows a user-provided rollback function
inside create_subnet_bulk, which will be invoked
before the session is rolled back. Thus the backend
entities can be removed as well as those entries
stored in the session.


Change-Id: I05b6fa193115a3c5c31341411075a3cf1d443610
This commit is contained in:
Shih-Hao Li 2016-10-07 20:26:28 -07:00
parent 97916ebf85
commit be1b8ff0bc
2 changed files with 135 additions and 0 deletions

View File

@ -1022,6 +1022,47 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
msg = _("Subnet overlaps with shared address space 100.64.0.0/10")
raise n_exc.InvalidInput(error_message=msg)
def _create_bulk_with_rollback(self, resource, context, request_items,
rollback_func=None):
# This is a copy of the _create_bulk() in db_base_plugin_v2.py,
# but extended with a user-provided rollback function.
objects = []
collection = "%ss" % resource
items = request_items[collection]
context.session.begin(subtransactions=True)
try:
for item in items:
obj_creator = getattr(self, 'create_%s' % resource)
objects.append(obj_creator(context, item))
context.session.commit()
except Exception:
if rollback_func:
# The rollback function is called before session is reset.
for obj in objects:
rollback_func(obj)
context.session.rollback()
with excutils.save_and_reraise_exception():
LOG.error(_LE("An exception occurred while creating "
"the %(resource)s:%(item)s"),
{'resource': resource, 'item': item})
return objects
def _rollback_subnet(self, context, subnet):
if subnet['enable_dhcp']:
LOG.debug("Rollback native DHCP entries for network %s",
subnet['network_id'])
self._disable_native_dhcp(context, subnet['network_id'])
def create_subnet_bulk(self, context, subnets):
def _rollback(subnet):
self._rollback_subnet(context, subnet)
if cfg.CONF.nsx_v3.native_dhcp_metadata:
return self._create_bulk_with_rollback('subnet', context, subnets,
_rollback)
else:
return self._create_bulk('subnet', context, subnets)
def create_subnet(self, context, subnet):
self._validate_address_space(subnet['subnet'])

View File

@ -55,6 +55,31 @@ class NsxNativeDhcpTestCase(test_plugin.NsxV3PluginTestCaseMixin):
self._orig_native_dhcp_metadata, 'nsx_v3')
super(NsxNativeDhcpTestCase, self).tearDown()
def _make_subnet_data(self,
name=None,
network_id=None,
cidr=None,
gateway_ip=None,
tenant_id=None,
allocation_pools=None,
enable_dhcp=True,
dns_nameservers=None,
ip_version=4,
host_routes=None,
shared=False):
return {'subnet': {
'name': name,
'network_id': network_id,
'cidr': cidr,
'gateway_ip': gateway_ip,
'tenant_id': tenant_id,
'allocation_pools': allocation_pools,
'ip_version': ip_version,
'enable_dhcp': enable_dhcp,
'dns_nameservers': dns_nameservers,
'host_routes': host_routes,
'shared': shared}}
def _verify_dhcp_service(self, network_id, tenant_id, enabled):
# Verify if DHCP service is enabled on a network.
port_res = self._list_ports('json', 200, network_id,
@ -155,6 +180,75 @@ class NsxNativeDhcpTestCase(test_plugin.NsxV3PluginTestCaseMixin):
network['network']['tenant_id'],
True)
def test_dhcp_service_with_create_dhcp_subnet_bulk(self):
# Test if DHCP service is enabled on all networks after a
# create_subnet_bulk operation.
with self.network() as network1, self.network() as network2:
subnet1 = self._make_subnet_data(
network_id=network1['network']['id'], cidr='10.0.0.0/24',
tenant_id=network1['network']['tenant_id'])
subnet2 = self._make_subnet_data(
network_id=network2['network']['id'], cidr='20.0.0.0/24',
tenant_id=network2['network']['tenant_id'])
subnets = {'subnets': [subnet1, subnet2]}
self.plugin.create_subnet_bulk(
context.get_admin_context(), subnets)
# Check if the bindings to backend DHCP entries are created.
dhcp_service = nsx_db.get_nsx_service_binding(
context.get_admin_context().session,
network1['network']['id'], nsx_constants.SERVICE_DHCP)
self.assertTrue(dhcp_service)
dhcp_service = nsx_db.get_nsx_service_binding(
context.get_admin_context().session,
network2['network']['id'], nsx_constants.SERVICE_DHCP)
self.assertTrue(dhcp_service)
def test_dhcp_service_with_create_dhcp_subnet_bulk_failure(self):
# Test if user-provided rollback function is invoked when
# exception occurred during a create_subnet_bulk operation.
with self.network() as network1, self.network() as network2:
subnet1 = self._make_subnet_data(
network_id=network1['network']['id'], cidr='10.0.0.0/24',
tenant_id=network1['network']['tenant_id'])
subnet2 = self._make_subnet_data(
network_id=network2['network']['id'], cidr='20.0.0.0/24',
tenant_id=network2['network']['tenant_id'])
subnets = {'subnets': [subnet1, subnet2]}
# Inject an exception on the second create_subnet call.
orig_create_subnet = self.plugin.create_subnet
with mock.patch.object(self.plugin,
'create_subnet') as create_subnet:
def side_effect(*args, **kwargs):
return self._fail_second_call(
create_subnet, orig_create_subnet, *args, **kwargs)
create_subnet.side_effect = side_effect
with mock.patch.object(self.plugin,
'_rollback_subnet') as rollback_subnet:
try:
admin_context = context.get_admin_context()
self.plugin.create_subnet_bulk(admin_context, subnets)
except Exception:
pass
# Check if rollback function has been called for
# the subnet in the first network.
rollback_subnet.assert_called_once_with(admin_context,
mock.ANY)
subnet_arg = rollback_subnet.call_args[0][1]
self.assertEqual(network1['network']['id'],
subnet_arg['network_id'])
# Check if the bindings to backend DHCP entries are
# removed.
dhcp_service = nsx_db.get_nsx_service_binding(
context.get_admin_context().session,
network1['network']['id'], nsx_constants.SERVICE_DHCP)
self.assertFalse(dhcp_service)
dhcp_service = nsx_db.get_nsx_service_binding(
context.get_admin_context().session,
network2['network']['id'], nsx_constants.SERVICE_DHCP)
self.assertFalse(dhcp_service)
def test_dhcp_service_with_create_multiple_dhcp_subnets(self):
# Test if multiple DHCP-enabled subnets cannot be created in a network.
with self.network() as network: