From 633d8acd3383995ea52daf0d32057ed169cd7f38 Mon Sep 17 00:00:00 2001 From: Jakub Libosvar Date: Tue, 15 Mar 2016 10:50:28 +0000 Subject: [PATCH] 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 --- neutron/objects/base.py | 24 +++++++--- neutron/objects/extensions/__init__.py | 0 .../objects/extensions/standardattributes.py | 26 ++++++++++ .../tests/unit/objects/extensions/__init__.py | 0 .../extensions/test_standardattributes.py | 47 +++++++++++++++++++ neutron/tests/unit/objects/test_base.py | 32 ++++++++++--- neutron/tests/unit/objects/test_objects.py | 2 +- .../unit/services/qos/test_qos_plugin.py | 5 ++ 8 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 neutron/objects/extensions/__init__.py create mode 100644 neutron/objects/extensions/standardattributes.py create mode 100644 neutron/tests/unit/objects/extensions/__init__.py create mode 100644 neutron/tests/unit/objects/extensions/test_standardattributes.py diff --git a/neutron/objects/base.py b/neutron/objects/base.py index 27323c8df08..0c56d0ff817 100644 --- a/neutron/objects/base.py +++ b/neutron/objects/base.py @@ -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) diff --git a/neutron/objects/extensions/__init__.py b/neutron/objects/extensions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/objects/extensions/standardattributes.py b/neutron/objects/extensions/standardattributes.py new file mode 100644 index 00000000000..5ec36d66e4a --- /dev/null +++ b/neutron/objects/extensions/standardattributes.py @@ -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) diff --git a/neutron/tests/unit/objects/extensions/__init__.py b/neutron/tests/unit/objects/extensions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/objects/extensions/test_standardattributes.py b/neutron/tests/unit/objects/extensions/test_standardattributes.py new file mode 100644 index 00000000000..3db8895c7bd --- /dev/null +++ b/neutron/tests/unit/objects/extensions/test_standardattributes.py @@ -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 diff --git a/neutron/tests/unit/objects/test_base.py b/neutron/tests/unit/objects/test_base.py index e3c484dd13a..8e31eb2536e 100644 --- a/neutron/tests/unit/objects/test_base.py +++ b/neutron/tests/unit/objects/test_base.py @@ -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, diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index a0b6a622b42..bfe04db8c8e 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -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', } diff --git a/neutron/tests/unit/services/qos/test_qos_plugin.py b/neutron/tests/unit/services/qos/test_qos_plugin.py index bc6d22fb07a..f068a3c05bb 100644 --- a/neutron/tests/unit/services/qos/test_qos_plugin.py +++ b/neutron/tests/unit/services/qos/test_qos_plugin.py @@ -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"])