ovo: Introduce standard attributes to objects

Patch adds hook to NeutronDbObject that checks if db model has standard
attributes. If so, it extends object fields with standard attributes.

Partial-Bug: 1541928
Change-Id: Ib5ef2cc23a48f092ebebe9a7d5ab753c9b9a33df
This commit is contained in:
Jakub Libosvar 2016-03-15 10:50:28 +00:00
parent f6a933d640
commit 633d8acd33
8 changed files with 122 additions and 14 deletions

View File

@ -23,7 +23,9 @@ import six
from neutron._i18n import _
from neutron.db import api as db_api
from neutron.db import model_base
from neutron.objects.db import api as obj_db_api
from neutron.objects.extensions import standardattributes
class NeutronObjectUpdateForbidden(exceptions.NeutronException):
@ -146,6 +148,9 @@ class DeclarativeObject(abc.ABCMeta):
cls.fields_no_update += base.primary_keys
# avoid duplicate entries
cls.fields_no_update = list(set(cls.fields_no_update))
if (hasattr(cls, 'has_standard_attributes') and
cls.has_standard_attributes()):
standardattributes.add_standard_attributes(cls)
@six.add_metaclass(DeclarativeObject)
@ -179,6 +184,11 @@ class NeutronDbObject(NeutronObject):
self.load_synthetic_db_fields()
self.obj_reset_changes()
@classmethod
def has_standard_attributes(cls):
return bool(cls.db_model and
issubclass(cls.db_model, model_base.HasStandardAttributes))
@classmethod
def modify_fields_to_db(cls, fields):
"""
@ -201,20 +211,22 @@ class NeutronDbObject(NeutronObject):
@classmethod
def modify_fields_from_db(cls, db_obj):
"""
This method enables to modify the fields and its
content after data was fetched from DB.
"""Modify the fields after data were fetched from DB.
It uses the fields_need_translation dict with structure:
{
'field_name_in_object': 'field_name_in_db'
}
:param db_obj: dict of object fetched from database
:param db_obj: model fetched from database
:return: modified dict of DB values
"""
result = {field: value for field, value in dict(db_obj).items()
if value is not None}
# db models can have declarative proxies that are not exposed into
# db.keys() so we must fetch data based on object fields definition
potential_fields = (list(cls.fields.keys()) +
list(cls.fields_need_translation.values()))
result = {field: db_obj[field] for field in potential_fields
if db_obj.get(field) is not None}
for field, field_db in cls.fields_need_translation.items():
if field_db in result:
result[field] = result.pop(field_db)

View File

View File

@ -0,0 +1,26 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_versionedobjects import fields as obj_fields
STANDARD_ATTRIBUTES = {
'description': obj_fields.StringField(),
'created_at': obj_fields.DateTimeField(nullable=True, tzinfo_aware=False),
'updated_at': obj_fields.DateTimeField(nullable=True, tzinfo_aware=False),
}
def add_standard_attributes(cls):
# Don't use parent's fields in case child class doesn't create
# its own instance of list
cls.fields = cls.fields.copy()
cls.fields.update(STANDARD_ATTRIBUTES)

View File

@ -0,0 +1,47 @@
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_versionedobjects import base as obj_base
from oslo_versionedobjects import fields as obj_fields
import sqlalchemy as sa
from neutron.db import model_base
from neutron.objects import base as objects_base
from neutron.tests.unit.objects import test_base
from neutron.tests.unit import testlib_api
class FakeDbModelWithStandardAttributes(
model_base.HasStandardAttributes, model_base.BASEV2):
id = sa.Column(sa.String(36), primary_key=True, nullable=False)
item = sa.Column(sa.String(64))
@obj_base.VersionedObjectRegistry.register_if(False)
class FakeObjectWithStandardAttributes(objects_base.NeutronDbObject):
VERSION = '1.0'
db_model = FakeDbModelWithStandardAttributes
fields = {
'id': obj_fields.UUIDField(),
'item': obj_fields.StringField(),
}
class HasStandardAttributesDbTestCase(test_base.BaseDbObjectTestCase,
testlib_api.SqlTestCase):
_test_class = FakeObjectWithStandardAttributes
class HasStandardAttributesTestCase(test_base.BaseObjectIfaceTestCase):
_test_class = FakeObjectWithStandardAttributes

View File

@ -16,6 +16,7 @@ import random
import mock
from oslo_db import exception as obj_exc
from oslo_utils import timeutils
from oslo_utils import uuidutils
from oslo_versionedobjects import base as obj_base
from oslo_versionedobjects import fields as obj_fields
@ -36,6 +37,7 @@ from neutron.tests import tools
SQLALCHEMY_COMMIT = 'sqlalchemy.engine.Connection._commit_impl'
OBJECTS_BASE_OBJ_FROM_PRIMITIVE = ('oslo_versionedobjects.base.'
'VersionedObject.obj_from_primitive')
TIMESTAMP_FIELDS = ['created_at', 'updated_at']
class FakeModel(object):
@ -237,6 +239,7 @@ FIELD_TYPE_VALUE_GENERATOR_MAP = {
common_types.IPNetworkPrefixLenField: tools.get_random_prefixlen,
common_types.ListOfIPNetworksField: get_list_of_random_networks,
common_types.IPVersionEnumField: tools.get_random_ip_version,
obj_fields.DateTimeField: timeutils.utcnow,
}
@ -245,6 +248,11 @@ def get_obj_db_fields(obj):
if field not in obj.synthetic_fields}
def remove_timestamps_from_fields(obj_fields):
return {field: value for field, value in obj_fields.items()
if field not in TIMESTAMP_FIELDS}
class _BaseObjectTestCase(object):
_test_class = FakeNeutronObject
@ -474,6 +482,10 @@ class BaseObjectIfaceTestCase(_BaseObjectTestCase, test_base.BaseTestCase):
with mock.patch.object(obj_db_api, 'get_objects',
side_effect=self.fake_get_objects):
obj = self._test_class(self.context, **self.db_obj)
# get new values and fix keys
update_mock.return_value = self.db_objs[1].copy()
for key, value in obj._get_composite_keys().items():
update_mock.return_value[key] = value
obj.update()
update_mock.assert_called_once_with(
self.context, self._test_class.db_model,
@ -631,8 +643,14 @@ class BaseDbObjectTestCase(_BaseObjectTestCase):
'device_id': 'fake_device',
'device_owner': 'fake_owner'})
def _make_object(self, fields):
return self._test_class(
self.context, **remove_timestamps_from_fields(fields))
def test_get_object_create_update_delete(self):
obj = self._test_class(self.context, **self.obj_fields[0])
# Timestamps can't be initialized and multiple objects may use standard
# attributes so we need to remove timestamps when creating objects
obj = self._make_object(self.obj_fields[0])
obj.create()
new = self._test_class.get_object(self.context,
@ -657,7 +675,7 @@ class BaseDbObjectTestCase(_BaseObjectTestCase):
self.assertIsNone(new)
def test_update_non_existent_object_raises_not_found(self):
obj = self._test_class(self.context, **self.obj_fields[0])
obj = self._make_object(self.obj_fields[0])
obj.obj_reset_changes()
fields_to_update = self.get_updatable_fields(self.obj_fields[0])
@ -670,17 +688,17 @@ class BaseDbObjectTestCase(_BaseObjectTestCase):
self.assertRaises(n_exc.ObjectNotFound, obj.update)
def test_delete_non_existent_object_raises_not_found(self):
obj = self._test_class(self.context, **self.obj_fields[0])
obj = self._make_object(self.obj_fields[0])
self.assertRaises(n_exc.ObjectNotFound, obj.delete)
@mock.patch(SQLALCHEMY_COMMIT)
def test_create_single_transaction(self, mock_commit):
obj = self._test_class(self.context, **self.obj_fields[0])
obj = self._make_object(self.obj_fields[0])
obj.create()
self.assertEqual(1, mock_commit.call_count)
def test_update_single_transaction(self):
obj = self._test_class(self.context, **self.obj_fields[0])
obj = self._make_object(self.obj_fields[0])
obj.create()
fields_to_update = self.get_updatable_fields(self.obj_fields[1])
@ -695,7 +713,7 @@ class BaseDbObjectTestCase(_BaseObjectTestCase):
self.assertEqual(1, mock_commit.call_count)
def test_delete_single_transaction(self):
obj = self._test_class(self.context, **self.obj_fields[0])
obj = self._make_object(self.obj_fields[0])
obj.create()
with mock.patch(SQLALCHEMY_COMMIT) as mock_commit:
@ -709,7 +727,7 @@ class BaseDbObjectTestCase(_BaseObjectTestCase):
@mock.patch(SQLALCHEMY_COMMIT)
def test_get_object_single_transaction(self, mock_commit):
obj = self._test_class(self.context, **self.obj_fields[0])
obj = self._make_object(self.obj_fields[0])
obj.create()
obj = self._test_class.get_object(self.context,

View File

@ -32,7 +32,7 @@ object_data = {
'QosDscpMarkingRule': '1.1-0313c6554b34fd10c753cb63d638256c',
'QosRuleType': '1.1-8a53fef4c6a43839d477a85b787d22ce',
'QosPolicy': '1.1-721fa60ea8f0e8f15d456d6e917dfe59',
'SubnetPool': '1.0-6e03cee0148ced4a60dd8342fed3d0be',
'SubnetPool': '1.0-320598830183ee739cbc9f32ebc26bba',
'SubnetPoolPrefix': '1.0-13c15144135eb869faa4a76dc3ee3b6c',
}

View File

@ -39,6 +39,11 @@ class TestQosPlugin(base.BaseQosTestCase):
mock.patch('neutron.objects.db.api.get_object').start()
mock.patch(
'neutron.objects.qos.policy.QosPolicy.obj_load_attr').start()
# We don't use real models as per mocks above. We also need to mock-out
# methods that work with real data types
mock.patch(
'neutron.objects.base.NeutronDbObject.modify_fields_from_db'
).start()
cfg.CONF.set_override("core_plugin", DB_PLUGIN_KLASS)
cfg.CONF.set_override("service_plugins", ["qos"])