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:
parent
97916ebf85
commit
be1b8ff0bc
@ -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'])
|
||||
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user