Merge "objects: expose database model for NeutronDbObject instances"

This commit is contained in:
Jenkins 2016-09-14 11:43:03 +00:00 committed by Gerrit Code Review
commit 32acfee767
10 changed files with 136 additions and 64 deletions

View File

@ -58,7 +58,7 @@ class AllowedAddressPairsMixin(object):
def get_allowed_address_pairs(self, context, port_id):
pairs = self._get_allowed_address_pairs_objs(context, port_id)
return [self._make_allowed_address_pairs_dict(pair)
return [self._make_allowed_address_pairs_dict(pair.db_obj)
for pair in pairs]
def _get_allowed_address_pairs_objs(self, context, port_id):

View File

@ -167,8 +167,7 @@ class DbBasePluginCommon(common_db_mixin.CommonDbMixin):
'max_prefixlen': max_prefixlen,
'is_default': subnetpool['is_default'],
'shared': subnetpool['shared'],
'prefixes': [str(prefix)
for prefix in subnetpool['prefixes']],
'prefixes': [prefix.cidr for prefix in subnetpool['prefixes']],
'ip_version': subnetpool['ip_version'],
'default_quota': subnetpool['default_quota'],
'address_scope_id': subnetpool['address_scope_id']}

View File

@ -1030,7 +1030,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
subnetpool = subnetpool_obj.SubnetPool(context, **pool_args)
subnetpool.create()
return self._make_subnetpool_dict(subnetpool)
return self._make_subnetpool_dict(subnetpool.db_obj)
@db_api.retry_if_session_inactive()
def update_subnetpool(self, context, id, subnetpool):
@ -1067,13 +1067,13 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
for key in ['min_prefixlen', 'max_prefixlen', 'default_prefixlen']:
updated['key'] = str(updated[key])
self._apply_dict_extend_functions(attributes.SUBNETPOOLS,
updated, orig_sp)
updated, orig_sp.db_obj)
return updated
@db_api.retry_if_session_inactive()
def get_subnetpool(self, context, id, fields=None):
subnetpool = self._get_subnetpool(context, id)
return self._make_subnetpool_dict(subnetpool, fields)
return self._make_subnetpool_dict(subnetpool.db_obj, fields)
@db_api.retry_if_session_inactive()
def get_subnetpools(self, context, filters=None, fields=None,
@ -1083,7 +1083,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
subnetpools = subnetpool_obj.SubnetPool.get_objects(
context, _pager=pager, validate_filters=False, **filters)
return [
self._make_subnetpool_dict(pool, fields)
self._make_subnetpool_dict(pool.db_obj, fields)
for pool in subnetpools
]

View File

@ -358,7 +358,7 @@ class SubnetPoolReader(object):
for prefix in prefix_list:
ip_set.add(netaddr.IPNetwork(prefix))
ip_set.compact()
return [str(x.cidr) for x in ip_set.iter_cidrs()]
return [x.cidr for x in ip_set.iter_cidrs()]
class SubnetPoolHelper(object):

View File

@ -12,6 +12,7 @@
import abc
import copy
import functools
import itertools
from neutron_lib import exceptions
@ -223,6 +224,24 @@ class NeutronObject(obj_base.VersionedObject,
return len(cls.get_objects(context, **kwargs))
def _detach_db_obj(func):
"""Decorator to detach db_obj from the session."""
@functools.wraps(func)
def decorator(self, *args, **kwargs):
synthetic_changed = bool(self._get_changed_synthetic_fields())
res = func(self, *args, **kwargs)
# some relationship based fields may be changed since we
# captured the model, let's refresh it for the latest database
# state
if synthetic_changed:
# TODO(ihrachys) consider refreshing just changed attributes
self.obj_context.session.refresh(self.db_obj)
# detach the model so that consequent fetches don't reuse it
self.obj_context.session.expunge(self.db_obj)
return res
return decorator
class DeclarativeObject(abc.ABCMeta):
def __init__(cls, name, bases, dct):
@ -245,19 +264,23 @@ class DeclarativeObject(abc.ABCMeta):
fields_no_update_set.add(key)
cls.fields_no_update = list(fields_no_update_set)
# generate unique_keys from the model
model = getattr(cls, 'db_model', None)
if model and not getattr(cls, 'unique_keys', None):
cls.unique_keys = []
obj_field_names = set(cls.fields.keys())
model_to_obj_translation = {
v: k for (k, v) in cls.fields_need_translation.items()}
if model:
# generate unique_keys from the model
if not getattr(cls, 'unique_keys', None):
cls.unique_keys = []
obj_field_names = set(cls.fields.keys())
model_to_obj_translation = {
v: k for (k, v) in cls.fields_need_translation.items()}
for model_unique_key in model_base.get_unique_keys(model):
obj_unique_key = [model_to_obj_translation.get(key, key)
for key in model_unique_key]
if obj_field_names.issuperset(obj_unique_key):
cls.unique_keys.append(obj_unique_key)
for model_unique_key in model_base.get_unique_keys(model):
obj_unique_key = [model_to_obj_translation.get(key, key)
for key in model_unique_key]
if obj_field_names.issuperset(obj_unique_key):
cls.unique_keys.append(obj_unique_key)
# detach db_obj right after object is loaded from the model
cls.create = _detach_db_obj(cls.create)
cls.update = _detach_db_obj(cls.update)
if (hasattr(cls, 'has_standard_attributes') and
cls.has_standard_attributes()):
@ -304,12 +327,22 @@ class NeutronDbObject(NeutronObject):
# in to_dict()
# obj_extra_fields = []
def __init__(self, *args, **kwargs):
super(NeutronDbObject, self).__init__(*args, **kwargs)
self._captured_db_model = None
@property
def db_obj(self):
'''Return a database model that persists object data.'''
return self._captured_db_model
def from_db_object(self, db_obj):
fields = self.modify_fields_from_db(db_obj)
for field in self.fields:
if field in fields and not self.is_synthetic(field):
setattr(self, field, fields[field])
self.load_synthetic_db_fields(db_obj)
self._captured_db_model = db_obj
self.obj_reset_changes()
@classmethod
@ -365,6 +398,8 @@ class NeutronDbObject(NeutronObject):
def _load_object(cls, context, db_obj):
obj = cls(context)
obj.from_db_object(db_obj)
# detach the model so that consequent fetches don't reuse it
context.session.expunge(obj.db_obj)
return obj
def obj_load_attr(self, attrname):
@ -446,6 +481,13 @@ class NeutronDbObject(NeutronObject):
del fields[field]
return fields
def _get_changed_synthetic_fields(self):
fields = self.obj_get_changes()
for field in self._get_changed_persistent_fields():
if field in fields:
del fields[field]
return fields
def _validate_changed_fields(self, fields):
fields = fields.copy()
forbidden_updates = set(self.fields_no_update) & set(fields.keys())
@ -497,8 +539,7 @@ class NeutronDbObject(NeutronObject):
if synth_db_objs is not None:
if not isinstance(synth_db_objs, list):
synth_db_objs = [synth_db_objs]
synth_objs = [objclass._load_object(self.obj_context,
objclass.modify_fields_from_db(obj))
synth_objs = [objclass._load_object(self.obj_context, obj)
for obj in synth_db_objs]
else:
synth_objs = objclass.get_objects(
@ -551,19 +592,19 @@ class NeutronDbObject(NeutronObject):
updates = self._get_changed_persistent_fields()
updates = self._validate_changed_fields(updates)
if updates:
with db_api.autonested_transaction(self.obj_context.session):
db_obj = obj_db_api.update_object(
self.obj_context, self.db_model,
self.modify_fields_to_db(updates),
**self.modify_fields_to_db(
self._get_composite_keys()))
self.from_db_object(db_obj)
with db_api.autonested_transaction(self.obj_context.session):
db_obj = obj_db_api.update_object(
self.obj_context, self.db_model,
self.modify_fields_to_db(updates),
**self.modify_fields_to_db(
self._get_composite_keys()))
self.from_db_object(db_obj)
def delete(self):
obj_db_api.delete_object(self.obj_context, self.db_model,
**self.modify_fields_to_db(
self._get_composite_keys()))
self._captured_db_model = None
@classmethod
def count(cls, context, **kwargs):

View File

@ -145,7 +145,8 @@ class RbacNeutronDbObjectMixin(rbac_db_mixin.RbacPluginMixin,
if policy['action'] != models.ACCESS_SHARED:
return
target_tenant = policy['target_tenant']
db_obj = cls.get_object(context, id=policy['object_id'])
db_obj = obj_db_api.get_object(
context.elevated(), cls.db_model, id=policy['object_id'])
if db_obj.tenant_id == target_tenant:
return
cls._validate_rbac_policy_delete(context=context,
@ -182,7 +183,8 @@ class RbacNeutronDbObjectMixin(rbac_db_mixin.RbacPluginMixin,
# (hopefully) melded with this one.
if object_type != cls.rbac_db_model.object_type:
return
db_obj = cls.get_object(context.elevated(), id=policy['object_id'])
db_obj = obj_db_api.get_object(
context.elevated(), cls.db_model, id=policy['object_id'])
if event in (events.BEFORE_CREATE, events.BEFORE_UPDATE):
if (not context.is_admin and
db_obj['tenant_id'] != context.tenant_id):
@ -207,8 +209,9 @@ class RbacNeutronDbObjectMixin(rbac_db_mixin.RbacPluginMixin,
def update_shared(self, is_shared_new, obj_id):
admin_context = self.obj_context.elevated()
shared_prev = obj_db_api.get_object(admin_context, self.rbac_db_model,
object_id=obj_id, target_tenant='*',
action=models.ACCESS_SHARED)
object_id=obj_id,
target_tenant='*',
action=models.ACCESS_SHARED)
is_shared_prev = bool(shared_prev)
if is_shared_prev == is_shared_new:
return

View File

@ -100,13 +100,6 @@ class SubnetPool(base.NeutronDbObject):
obj.reload_prefixes()
return objs
def _get_changed_synthetic_fields(self):
fields = self.obj_get_changes()
for field in self._get_changed_persistent_fields():
if field in fields:
del fields[field]
return fields
# TODO(ihrachys): Consider extending base to trigger registered methods
def create(self):
synthetic_changes = self._get_changed_synthetic_fields()

View File

@ -511,6 +511,11 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase):
self.model_map = collections.defaultdict(list)
self.model_map[self._test_class.db_model] = self.db_objs
self.pager_map = collections.defaultdict(lambda: None)
# don't validate refresh and expunge in tests that don't touch database
# because otherwise it will fail due to db models not being injected
# into active session in the first place
mock.patch.object(self.context.session, 'refresh').start()
mock.patch.object(self.context.session, 'expunge').start()
# TODO(ihrachys) document the intent of all common test cases in docstrings
def test_get_object(self):
@ -756,16 +761,6 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase):
project_id = self.obj_fields[0]['project_id']
self.assertEqual(project_id, obj.tenant_id)
@mock.patch.object(obj_db_api, 'update_object')
def test_update_no_changes(self, update_mock):
with mock.patch.object(base.NeutronDbObject,
'_get_changed_persistent_fields',
return_value={}):
obj_keys = self.generate_object_keys(self._test_class)
obj = self._test_class(self.context, **obj_keys)
obj.update()
self.assertFalse(update_mock.called)
@mock.patch.object(obj_db_api, 'update_object')
def test_update_changes(self, update_mock):
fields_to_update = self.get_updatable_fields(
@ -1245,6 +1240,12 @@ class BaseDbObjectTestCase(_BaseObjectTestCase,
objclass = self._get_ovo_object_class(cls_, field)
if not objclass:
continue
# check that the stored database model does not have non-empty
# relationships
dbattr = obj.fields_need_translation.get(field, field)
self.assertFalse(getattr(obj.db_obj, dbattr, None))
objclass_fields = self._get_non_synth_fields(objclass,
db_obj[field][0])
@ -1256,17 +1257,19 @@ class BaseDbObjectTestCase(_BaseObjectTestCase,
synth_field_obj = objclass(self.context, **objclass_fields)
synth_field_obj.create()
# populate the base object synthetic fields with created children
if isinstance(cls_.fields[field], obj_fields.ObjectField):
setattr(obj, field, synth_field_obj)
else:
setattr(obj, field, [synth_field_obj])
# reload the parent object under test
obj = cls_.get_object(self.context, **obj._get_composite_keys())
# check that the stored database model now has filled relationships
dbattr = obj.fields_need_translation.get(field, field)
self.assertTrue(getattr(obj.db_obj, dbattr, None))
# reset the object so that we can compare it to other clean objects
obj.obj_reset_changes([field])
return obj
def test_get_object_with_synthetic_fields(self):
def _test_get_with_synthetic_fields(self, getter):
object_fields = self._get_object_synthetic_fields(self._test_class)
if not object_fields:
self.skipTest(
@ -1274,11 +1277,21 @@ class BaseDbObjectTestCase(_BaseObjectTestCase,
'in test class %r' % self._test_class
)
obj = self._create_object_with_synthetic_fields(self.db_objs[0])
listed_obj = self._test_class.get_object(
self.context, **obj._get_composite_keys())
listed_obj = getter(self.context, **obj._get_composite_keys())
self.assertTrue(listed_obj)
self.assertEqual(obj, listed_obj)
def test_get_object_with_synthetic_fields(self):
self._test_get_with_synthetic_fields(self._test_class.get_object)
def test_get_objects_with_synthetic_fields(self):
def getter(*args, **kwargs):
objs = self._test_class.get_objects(*args, **kwargs)
self.assertEqual(1, len(objs))
return objs[0]
self._test_get_with_synthetic_fields(getter)
# NOTE(korzen) _list method is used in neutron.tests.db.unit.db.
# test_db_base_plugin_v2.DbOperationBoundMixin in _list_and_count_queries()
# This is used in test_subnet for asserting that number of queries is
@ -1301,6 +1314,25 @@ class BaseDbObjectTestCase(_BaseObjectTestCase,
self.assertEqual(
len(self.obj_fields), self._test_class.count(self.context))
def test_db_obj(self):
obj = self._make_object(self.obj_fields[0])
self.assertIsNone(obj.db_obj)
obj.create()
self.assertIsNotNone(obj.db_obj)
fields_to_update = self.get_updatable_fields(self.obj_fields[1])
if fields_to_update:
old_model = copy.deepcopy(obj.db_obj)
for key, val in fields_to_update.items():
setattr(obj, key, val)
obj.update()
self.assertIsNotNone(obj.db_obj)
self.assertNotEqual(old_model, obj.db_obj)
obj.delete()
self.assertIsNone(obj.db_obj)
class UniqueObjectBase(test_base.BaseTestCase):
def setUp(self):

View File

@ -19,6 +19,7 @@ from oslo_versionedobjects import fields as obj_fields
import sqlalchemy as sa
from neutron.callbacks import events
from neutron import context as n_context
from neutron.db import rbac_db_models
from neutron.extensions import rbac as ext_rbac
from neutron.objects.db import api as obj_db_api
@ -129,7 +130,7 @@ class RbacNeutronDbObjectTestCase(test_base.BaseObjectIfaceTestCase,
mock_validate_rbac_update.assert_not_called()
@mock.patch.object(_test_class, 'validate_rbac_policy_update')
@mock.patch.object(_test_class, 'get_object',
@mock.patch.object(obj_db_api, 'get_object',
return_value={'tenant_id': 'tyrion_lannister'})
def test_validate_rbac_policy_change_allowed_for_admin_or_owner(
self, mock_get_object, mock_validate_update):
@ -143,7 +144,7 @@ class RbacNeutronDbObjectTestCase(test_base.BaseObjectIfaceTestCase,
self.assertTrue(self._test_class.validate_rbac_policy_update.called)
@mock.patch.object(_test_class, 'validate_rbac_policy_update')
@mock.patch.object(_test_class, 'get_object',
@mock.patch.object(obj_db_api, 'get_object',
return_value={'tenant_id': 'king_beyond_the_wall'})
def test_validate_rbac_policy_change_forbidden_for_outsiders(
self, mock_get_object, mock_validate_update):
@ -162,7 +163,7 @@ class RbacNeutronDbObjectTestCase(test_base.BaseObjectIfaceTestCase,
self, policy, mock_validate_delete):
self._test_class.validate_rbac_policy_delete(
resource=mock.Mock(), event=events.BEFORE_DELETE,
trigger='dummy_trigger', context=mock.Mock(),
trigger='dummy_trigger', context=n_context.get_admin_context(),
object_type=self._test_class.rbac_db_model.object_type,
policy=policy)
mock_validate_delete.assert_not_called()
@ -171,7 +172,7 @@ class RbacNeutronDbObjectTestCase(test_base.BaseObjectIfaceTestCase,
self._test_validate_rbac_policy_delete_handles_policy(
{'action': 'unknown_action'})
@mock.patch.object(_test_class, 'get_object')
@mock.patch.object(obj_db_api, 'get_object')
def test_validate_rbac_policy_delete_skips_db_object_owner(self,
mock_get_object):
policy = {'action': rbac_db_models.ACCESS_SHARED,
@ -181,7 +182,7 @@ class RbacNeutronDbObjectTestCase(test_base.BaseObjectIfaceTestCase,
mock_get_object.return_value.tenant_id = policy['target_tenant']
self._test_validate_rbac_policy_delete_handles_policy(policy)
@mock.patch.object(_test_class, 'get_object')
@mock.patch.object(obj_db_api, 'get_object')
@mock.patch.object(_test_class, 'get_bound_tenant_ids',
return_value='tenant_id_shared_with')
def test_validate_rbac_policy_delete_fails_single_tenant_and_in_use(
@ -237,7 +238,7 @@ class RbacNeutronDbObjectTestCase(test_base.BaseObjectIfaceTestCase,
'tenant_id': 'object_owner_tenant_id',
'object_id': 'fake_obj_id'}
context = mock.Mock()
with mock.patch.object(self._test_class, 'get_object'):
with mock.patch.object(obj_db_api, 'get_object'):
self.assertRaises(
ext_rbac.RbacPolicyInUse,
self._test_class.validate_rbac_policy_delete,

View File

@ -56,6 +56,9 @@ class TestQosPlugin(base.BaseQosTestCase):
self.qos_plugin.notification_driver_manager = mock.Mock()
self.ctxt = context.Context('fake_user', 'fake_tenant')
mock.patch.object(self.ctxt.session, 'refresh').start()
mock.patch.object(self.ctxt.session, 'expunge').start()
self.policy_data = {
'policy': {'id': uuidutils.generate_uuid(),
'tenant_id': uuidutils.generate_uuid(),