From 946ea0a5158506c094580f7402c64e2df5229d0f Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Mon, 28 Dec 2015 23:03:51 +0000 Subject: [PATCH] Refactor objects into a magic registry This adds a register hook to the objects base class that allows us to late-import objects classes into ironic.objects. This is useful to allow object classes to depend on other objects. For example, this will allow us to do the Port.node_uuid munging that we currently do in the API layer, in the objects layer instead. This is completely taken from Nova, much thanks to comstud and dansmith and whoever else worked on that. Change-Id: Ic655e7af533497bda5e8e72d3bd90c27394681a7 --- ironic/common/service.py | 2 ++ ironic/objects/__init__.py | 27 +++++++++--------- ironic/objects/base.py | 16 ++++++++++- ironic/tests/unit/__init__.py | 7 +++++ ironic/tests/unit/objects/test_objects.py | 34 +++++++++++++++++++++++ 5 files changed, 72 insertions(+), 14 deletions(-) diff --git a/ironic/common/service.py b/ironic/common/service.py index a657dc5905..7da3f426f0 100644 --- a/ironic/common/service.py +++ b/ironic/common/service.py @@ -33,6 +33,7 @@ from ironic.common.i18n import _ from ironic.common.i18n import _LE from ironic.common.i18n import _LI from ironic.common import rpc +from ironic import objects from ironic.objects import base as objects_base @@ -140,6 +141,7 @@ def prepare_service(argv=[]): ]) config.parse_args(argv) log.setup(CONF, 'ironic') + objects.register_all() def process_launcher(): diff --git a/ironic/objects/__init__.py b/ironic/objects/__init__.py index f022421fd1..72fe9d9862 100644 --- a/ironic/objects/__init__.py +++ b/ironic/objects/__init__.py @@ -12,18 +12,19 @@ # License for the specific language governing permissions and limitations # under the License. -from ironic.objects import chassis -from ironic.objects import conductor -from ironic.objects import node -from ironic.objects import port +# NOTE(comstud): You may scratch your head as you see code that imports +# this module and then accesses attributes for objects such as Node, +# etc, yet you do not see these attributes in here. Never fear, there is +# a little bit of magic. When objects are registered, an attribute is set +# on this module automatically, pointing to the newest/latest version of +# the object. -Chassis = chassis.Chassis -Conductor = conductor.Conductor -Node = node.Node -Port = port.Port - -__all__ = (Chassis, - Conductor, - Node, - Port) +def register_all(): + # NOTE(danms): You must make sure your object gets imported in this + # function in order for it to be registered by services that may + # need to receive it via RPC. + __import__('ironic.objects.chassis') + __import__('ironic.objects.conductor') + __import__('ironic.objects.node') + __import__('ironic.objects.port') diff --git a/ironic/objects/base.py b/ironic/objects/base.py index f72477b35e..25d4add641 100644 --- a/ironic/objects/base.py +++ b/ironic/objects/base.py @@ -14,13 +14,27 @@ """Ironic common internal object model""" +from oslo_utils import versionutils from oslo_versionedobjects import base as object_base +from ironic import objects from ironic.objects import fields as object_fields class IronicObjectRegistry(object_base.VersionedObjectRegistry): - pass + def registration_hook(self, cls, index): + # NOTE(jroll): blatantly stolen from nova + # NOTE(danms): This is called when an object is registered, + # and is responsible for maintaining ironic.objects.$OBJECT + # as the highest-versioned implementation of a given object. + version = versionutils.convert_version_to_tuple(cls.VERSION) + if not hasattr(objects, cls.obj_name()): + setattr(objects, cls.obj_name(), cls) + else: + cur_version = versionutils.convert_version_to_tuple( + getattr(objects, cls.obj_name()).VERSION) + if version >= cur_version: + setattr(objects, cls.obj_name(), cls) class IronicObject(object_base.VersionedObject): diff --git a/ironic/tests/unit/__init__.py b/ironic/tests/unit/__init__.py index 0b16a622c6..dcc2f0d687 100644 --- a/ironic/tests/unit/__init__.py +++ b/ironic/tests/unit/__init__.py @@ -26,4 +26,11 @@ import eventlet +from ironic import objects + eventlet.monkey_patch(os=False) + +# NOTE(comstud): Make sure we have all of the objects loaded. We do this +# at module import time, because we may be using mock decorators in our +# tests that run at import time. +objects.register_all() diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py index 0a13d16fe3..4616c6c88d 100644 --- a/ironic/tests/unit/objects/test_objects.py +++ b/ironic/tests/unit/objects/test_objects.py @@ -496,3 +496,37 @@ class TestObjectSerializer(test_base.TestCase): def test_deserialize_entity_newer_version_passes_revision(self): "Test object with unsupported (newer) version and revision" self._test_deserialize_entity_newer('1.7', '1.6.1', my_version='1.6.1') + + +class TestRegistry(test_base.TestCase): + @mock.patch('ironic.objects.base.objects') + def test_hook_chooses_newer_properly(self, mock_objects): + reg = base.IronicObjectRegistry() + reg.registration_hook(MyObj, 0) + + class MyNewerObj(object): + VERSION = '1.123' + + @classmethod + def obj_name(cls): + return 'MyObj' + + self.assertEqual(MyObj, mock_objects.MyObj) + reg.registration_hook(MyNewerObj, 0) + self.assertEqual(MyNewerObj, mock_objects.MyObj) + + @mock.patch('ironic.objects.base.objects') + def test_hook_keeps_newer_properly(self, mock_objects): + reg = base.IronicObjectRegistry() + reg.registration_hook(MyObj, 0) + + class MyOlderObj(object): + VERSION = '1.1' + + @classmethod + def obj_name(cls): + return 'MyObj' + + self.assertEqual(MyObj, mock_objects.MyObj) + reg.registration_hook(MyOlderObj, 0) + self.assertEqual(MyObj, mock_objects.MyObj)