Don't create default SG in transaction

Creating default security groups should not be done inside
another transaction because the creation code emits AFTER events.
Handlers for the push notification code expect to be able to read
from the DB during an after event using a new transaction, which will
not work if they are inside of exising transactions.

This patch adjusts the default security group creation logic to avoid
using layered transactions for the default security group creation and
adjusts ML2 to stop calling ensure_default_security_group inside of a
port/network transaction.

Partially-Implements: blueprint push-notifications
Change-Id: Iaa83c8664d5bfde31fdcdd694f6f18d9ef9bf14a
This commit is contained in:
Kevin Benton 2017-01-06 14:06:11 -08:00
parent 499f3c6d56
commit ffc4489a58
3 changed files with 48 additions and 17 deletions

View File

@ -83,6 +83,11 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase):
if not default_sg: if not default_sg:
self._ensure_default_security_group(context, tenant_id) self._ensure_default_security_group(context, tenant_id)
else:
existing_def_sg_id = self._get_default_sg_id(context, tenant_id)
if existing_def_sg_id is not None:
# default already exists, return it
return self.get_security_group(context, existing_def_sg_id)
with db_api.autonested_transaction(context.session): with db_api.autonested_transaction(context.session):
security_group_db = sg_models.SecurityGroup(id=s.get('id') or ( security_group_db = sg_models.SecurityGroup(id=s.get('id') or (
@ -661,27 +666,30 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase):
port[ext_sg.SECURITYGROUPS] = (security_group_ids and port[ext_sg.SECURITYGROUPS] = (security_group_ids and
list(security_group_ids) or []) list(security_group_ids) or [])
def _ensure_default_security_group(self, context, tenant_id): def _get_default_sg_id(self, context, tenant_id):
"""Create a default security group if one doesn't exist.
:returns: the default security group id for given tenant.
"""
try: try:
query = self._model_query(context, sg_models.DefaultSecurityGroup) query = self._model_query(context, sg_models.DefaultSecurityGroup)
default_group = query.filter_by(tenant_id=tenant_id).one() default_group = query.filter_by(tenant_id=tenant_id).one()
return default_group['security_group_id'] return default_group['security_group_id']
except exc.NoResultFound: except exc.NoResultFound:
security_group = { pass
'security_group':
{'name': 'default', def _ensure_default_security_group(self, context, tenant_id):
'tenant_id': tenant_id, """Create a default security group if one doesn't exist.
'description': _('Default security group')}
} :returns: the default security group id for given tenant.
# starting a transaction before create to avoid db retries """
with context.session.begin(subtransactions=True): existing = self._get_default_sg_id(context, tenant_id)
sg_id = self.create_security_group( if existing is not None:
context, security_group, default_sg=True)['id'] return existing
return sg_id security_group = {
'security_group':
{'name': 'default',
'tenant_id': tenant_id,
'description': _('Default security group')}
}
return self.create_security_group(context, security_group,
default_sg=True)['id']
def _get_security_groups_on_port(self, context, port): def _get_security_groups_on_port(self, context, port):
"""Check that all security groups on port belong to tenant. """Check that all security groups on port belong to tenant.

View File

@ -740,7 +740,6 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
tenant_id = net_data['tenant_id'] tenant_id = net_data['tenant_id']
session = context.session session = context.session
with session.begin(subtransactions=True): with session.begin(subtransactions=True):
self._ensure_default_security_group(context, tenant_id)
net_db = self.create_network_db(context, network) net_db = self.create_network_db(context, network)
result = self._make_network_dict(net_db, process_extensions=False, result = self._make_network_dict(net_db, process_extensions=False,
context=context) context=context)
@ -776,6 +775,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
@utils.transaction_guard @utils.transaction_guard
@db_api.retry_if_session_inactive() @db_api.retry_if_session_inactive()
def create_network(self, context, network): def create_network(self, context, network):
self._ensure_default_security_group(context,
network['network']['tenant_id'])
result, mech_context = self._create_network_db(context, network) result, mech_context = self._create_network_db(context, network)
kwargs = {'context': context, 'network': result} kwargs = {'context': context, 'network': result}
registry.notify(resources.NETWORK, events.AFTER_CREATE, self, **kwargs) registry.notify(resources.NETWORK, events.AFTER_CREATE, self, **kwargs)
@ -792,6 +793,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
@utils.transaction_guard @utils.transaction_guard
@db_api.retry_if_session_inactive() @db_api.retry_if_session_inactive()
def create_network_bulk(self, context, networks): def create_network_bulk(self, context, networks):
tenants = {n['network']['tenant_id'] for n in networks['networks']}
map(lambda t: self._ensure_default_security_group(context, t), tenants)
objects = self._create_bulk_ml2(attributes.NETWORK, context, networks) objects = self._create_bulk_ml2(attributes.NETWORK, context, networks)
return [obj['result'] for obj in objects] return [obj['result'] for obj in objects]
@ -1245,6 +1248,9 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
if not attrs.get('status'): if not attrs.get('status'):
attrs['status'] = const.PORT_STATUS_DOWN attrs['status'] = const.PORT_STATUS_DOWN
# NOTE(kevinbenton): triggered outside of transaction since it
# emits 'AFTER' events if it creates.
self._ensure_default_security_group(context, attrs['tenant_id'])
session = context.session session = context.session
with session.begin(subtransactions=True): with session.begin(subtransactions=True):
dhcp_opts = attrs.get(edo_ext.EXTRADHCPOPTS, []) dhcp_opts = attrs.get(edo_ext.EXTRADHCPOPTS, [])

View File

@ -20,6 +20,9 @@ import mock
from neutron_lib import constants as const from neutron_lib import constants as const
from neutron_lib.plugins import directory from neutron_lib.plugins import directory
from neutron.callbacks import events
from neutron.callbacks import registry
from neutron.callbacks import resources
from neutron import context from neutron import context
from neutron.extensions import securitygroup as ext_sg from neutron.extensions import securitygroup as ext_sg
from neutron.tests import tools from neutron.tests import tools
@ -148,6 +151,20 @@ class TestMl2SecurityGroups(Ml2SecurityGroupsTestCase,
# the or_ function should only have one argument # the or_ function should only have one argument
or_mock.assert_called_once_with(mock.ANY) or_mock.assert_called_once_with(mock.ANY)
def test_security_groups_created_outside_transaction(self):
def record_after_state(r, e, t, context, *args, **kwargs):
self.was_active = context.session.is_active
registry.subscribe(record_after_state, resources.SECURITY_GROUP,
events.AFTER_CREATE)
with self.subnet() as s:
self.assertFalse(self.was_active)
self._delete(
'security-groups',
self._list('security-groups')['security_groups'][0]['id'])
with self.port(subnet=s):
self.assertFalse(self.was_active)
class TestMl2SGServerRpcCallBack( class TestMl2SGServerRpcCallBack(
Ml2SecurityGroupsTestCase, Ml2SecurityGroupsTestCase,