Replace metaclass-based registry with a decorator-based one

In nova, we used a metaclass to ensure that merely defining an object as
inheriting from the base would register it for use. I don't feel that is
a responsible way for the library to behave, since just importing a module
full of objects would allow things to be registered which might enable
RPC methods that are undesired.

Further, it complicated our ability to register alternate implementations
of certain objects, and provided no end of complication to our tests.

This patch adds a decorator-based object registry that looks much the same,
but requires explicit opt-in registration to objects before they can be
deserialized over RPC and instantiated by name.

Change-Id: Ic50ea4d59df7981eb8a413b5c80ca47155864166
This commit is contained in:
Dan Smith
2015-02-11 09:33:35 -08:00
parent ed7c5377d4
commit 4b76fbde46
4 changed files with 93 additions and 61 deletions

View File

@@ -93,31 +93,26 @@ def make_class_properties(cls):
setattr(cls, name, property(getter, setter))
class NovaObjectMetaclass(type):
"""Metaclass that allows tracking of object classes."""
class VersionedObjectRegistry(object):
_registry = None
# NOTE(danms): This is what controls whether object operations are
# remoted. If this is not None, use it to remote things over RPC.
indirection_api = None
def __init__(cls, names, bases, dict_):
if not hasattr(cls, '_obj_classes'):
# This means this is a base class using the metaclass. I.e.,
# the 'NovaObject' class.
cls._obj_classes = collections.defaultdict(list)
return
def __new__(cls, *args, **kwargs):
if not VersionedObjectRegistry._registry:
VersionedObjectRegistry._registry = \
object.__new__(cls, *args, **kwargs)
VersionedObjectRegistry._registry._obj_classes = \
collections.defaultdict(list)
return VersionedObjectRegistry._registry
def _register_class(self, cls):
def _vers_tuple(obj):
return tuple([int(x) for x in obj.VERSION.split(".")])
# Add the subclass to NovaObject._obj_classes. If the
# same version already exists, replace it. Otherwise,
# keep the list with newest version first.
make_class_properties(cls)
obj_name = cls.obj_name()
for i, obj in enumerate(cls._obj_classes[obj_name]):
for i, obj in enumerate(self._obj_classes[obj_name]):
if cls.VERSION == obj.VERSION:
cls._obj_classes[obj_name][i] = cls
self._obj_classes[obj_name][i] = cls
# Update nova.objects with this newer class.
# FIXME(dhellmann): We can't store library state in
# the application module.
@@ -125,7 +120,7 @@ class NovaObjectMetaclass(type):
break
if _vers_tuple(cls) > _vers_tuple(obj):
# Insert before.
cls._obj_classes[obj_name].insert(i, cls)
self._obj_classes[obj_name].insert(i, cls)
# FIXME(dhellmann): We can't store library state in
# the application module.
# if i == 0:
@@ -134,7 +129,7 @@ class NovaObjectMetaclass(type):
# setattr(objects, obj_name, cls)
break
else:
cls._obj_classes[obj_name].append(cls)
self._obj_classes[obj_name].append(cls)
# Either this is the first time we've seen the object or it's
# an older version than anything we'e seen. Update nova.objects
# only if it's the first time we've seen this object name.
@@ -143,6 +138,17 @@ class NovaObjectMetaclass(type):
# if not hasattr(objects, obj_name):
# setattr(objects, obj_name, cls)
@classmethod
def register(cls, obj_cls):
registry = cls()
registry._register_class(obj_cls)
return obj_cls
@classmethod
def obj_classes(cls):
registry = cls()
return registry._obj_classes
# These are decorators that mark an object's method as remotable.
# If the metaclass is configured to forward object methods to an
@@ -215,7 +221,6 @@ def remotable(fn):
return wrapper
@six.add_metaclass(NovaObjectMetaclass)
class NovaObject(object):
"""Base class and object factory.
@@ -299,7 +304,7 @@ class NovaObject(object):
@classmethod
def obj_class_from_name(cls, objname, objver):
"""Returns a class from the registry based on a name and version."""
if objname not in cls._obj_classes:
if objname not in VersionedObjectRegistry.obj_classes():
LOG.error(_LE('Unable to instantiate unregistered object type '
'%(objtype)s'), dict(objtype=objname))
raise exception.UnsupportedObjectError(objtype=objname)
@@ -310,7 +315,7 @@ class NovaObject(object):
# once below.
compatible_match = None
for objclass in cls._obj_classes[objname]:
for objclass in VersionedObjectRegistry.obj_classes()[objname]:
if objclass.VERSION == objver:
return objclass
if (not compatible_match and
@@ -321,7 +326,7 @@ class NovaObject(object):
return compatible_match
# As mentioned above, latest version is always first in the list.
latest_ver = cls._obj_classes[objname][0].VERSION
latest_ver = VersionedObjectRegistry.obj_classes()[objname][0].VERSION
raise exception.IncompatibleObjectVersion(objname=objname,
objver=objver,
supported=latest_ver)

View File

@@ -24,7 +24,6 @@ inline callbacks.
import eventlet
eventlet.monkey_patch(os=False)
import copy
import inspect
import mock
import os
@@ -43,7 +42,6 @@ import testtools
#from nova import db
#from nova.network import manager as network_manager
#from nova import objects
from oslo_versionedobjects import base as objects_base
from oslo_versionedobjects.tests import obj_fixtures
from oslo_versionedobjects import utils
@@ -177,14 +175,6 @@ class TestCase(testtools.TestCase):
# because sqlalchemy-migrate messes with the warnings filters.
self.useFixture(obj_fixtures.WarningsFixture())
# NOTE(danms): Make sure to reset us back to non-remote objects
# for each test to avoid interactions. Also, backup the object
# registry.
objects_base.NovaObject.indirection_api = None
self._base_test_obj_backup = copy.copy(
objects_base.NovaObject._obj_classes)
self.addCleanup(self._restore_obj_registry)
# NOTE(mnaser): All calls to utils.is_neutron() are cached in
# nova.utils._IS_NEUTRON. We set it to None to avoid any
# caching of that value.
@@ -196,9 +186,6 @@ class TestCase(testtools.TestCase):
self.addCleanup(self._clear_attrs)
self.useFixture(fixtures.EnvironmentVariable('http_proxy'))
def _restore_obj_registry(self):
objects_base.NovaObject._obj_classes = self._base_test_obj_backup
def _clear_attrs(self):
# Delete attributes that don't start with _ so they don't pin
# memory around unnecessarily for the duration of the test

View File

@@ -331,6 +331,7 @@ class TestObject(TestField):
def setUp(self):
super(TestObject, self).setUp()
@obj_base.VersionedObjectRegistry.register
class TestableObject(obj_base.NovaObject):
fields = {
'uuid': fields.StringField(),

View File

@@ -25,7 +25,6 @@ import mock
from oslo_context import context
from oslo_serialization import jsonutils
from oslo_utils import timeutils
import six
from testtools import matchers
# from nova.conductor import rpcapi as conductor_rpcapi
@@ -41,11 +40,21 @@ from oslo_versionedobjects import utils
LOG = logging.getLogger(__name__)
def is_test_object(cls):
"""Return True if class is defined in the tests.
:param cls: Class to inspect
"""
return 'oslo_versionedobjects.tests' in cls.__module__
@base.VersionedObjectRegistry.register
class MyOwnedObject(base.NovaPersistentObject, base.NovaObject):
VERSION = '1.0'
fields = {'baz': fields.Field(fields.Integer())}
@base.VersionedObjectRegistry.register
class MyObj(base.NovaPersistentObject, base.NovaObject,
base.NovaObjectDictCompat):
VERSION = '1.6'
@@ -115,6 +124,7 @@ class MyObj(base.NovaPersistentObject, base.NovaObject,
primitive['bar'] = 'old%s' % primitive['bar']
@base.VersionedObjectRegistry.register
class MyObjDiffVers(MyObj):
VERSION = '1.5'
@@ -123,7 +133,8 @@ class MyObjDiffVers(MyObj):
return 'MyObj'
class MyObj2(object):
@base.VersionedObjectRegistry.register
class MyObj2(base.NovaObject):
@classmethod
def obj_name(cls):
return 'MyObj'
@@ -138,14 +149,15 @@ class RandomMixInWithNoFields(object):
pass
@base.VersionedObjectRegistry.register
class TestSubclassedObject(RandomMixInWithNoFields, MyObj):
fields = {'new_field': fields.Field(fields.String())}
class TestMetaclass(test.TestCase):
class TestRegistry(test.TestCase):
def test_obj_tracking(self):
@six.add_metaclass(base.NovaObjectMetaclass)
@base.VersionedObjectRegistry.register
class NewBaseClass(object):
VERSION = '1.0'
fields = {}
@@ -154,28 +166,35 @@ class TestMetaclass(test.TestCase):
def obj_name(cls):
return cls.__name__
@base.VersionedObjectRegistry.register
class Fake1TestObj1(NewBaseClass):
@classmethod
def obj_name(cls):
return 'fake1'
@base.VersionedObjectRegistry.register
class Fake1TestObj2(Fake1TestObj1):
pass
@base.VersionedObjectRegistry.register
class Fake1TestObj3(Fake1TestObj1):
VERSION = '1.1'
@base.VersionedObjectRegistry.register
class Fake2TestObj1(NewBaseClass):
@classmethod
def obj_name(cls):
return 'fake2'
@base.VersionedObjectRegistry.register
class Fake1TestObj4(Fake1TestObj3):
VERSION = '1.2'
@base.VersionedObjectRegistry.register
class Fake2TestObj2(Fake2TestObj1):
VERSION = '1.1'
@base.VersionedObjectRegistry.register
class Fake1TestObj5(Fake1TestObj1):
VERSION = '1.1'
@@ -183,18 +202,14 @@ class TestMetaclass(test.TestCase):
# newest object.
expected = {'fake1': [Fake1TestObj4, Fake1TestObj5, Fake1TestObj2],
'fake2': [Fake2TestObj2, Fake2TestObj1]}
self.assertEqual(expected, NewBaseClass._obj_classes)
# The following should work, also.
self.assertEqual(expected, Fake1TestObj1._obj_classes)
self.assertEqual(expected, Fake1TestObj2._obj_classes)
self.assertEqual(expected, Fake1TestObj3._obj_classes)
self.assertEqual(expected, Fake1TestObj4._obj_classes)
self.assertEqual(expected, Fake1TestObj5._obj_classes)
self.assertEqual(expected, Fake2TestObj1._obj_classes)
self.assertEqual(expected, Fake2TestObj2._obj_classes)
self.assertEqual(expected['fake1'],
base.VersionedObjectRegistry.obj_classes()['fake1'])
self.assertEqual(expected['fake2'],
base.VersionedObjectRegistry.obj_classes()['fake2'])
def test_field_checking(self):
def create_class(field):
@base.VersionedObjectRegistry.register
class TestField(base.NovaObject):
VERSION = '1.5'
fields = {'foo': field()}
@@ -210,6 +225,7 @@ class TestMetaclass(test.TestCase):
class TestObjToPrimitive(test.TestCase):
def test_obj_to_primitive_list(self):
@base.VersionedObjectRegistry.register
class MyObjElement(base.NovaObject):
fields = {'foo': fields.IntegerField()}
@@ -217,6 +233,7 @@ class TestObjToPrimitive(test.TestCase):
super(MyObjElement, self).__init__()
self.foo = foo
@base.VersionedObjectRegistry.register
class MyList(base.ObjectListBase, base.NovaObject):
fields = {'objects': fields.ListOfObjectsField('MyObjElement')}
@@ -231,6 +248,7 @@ class TestObjToPrimitive(test.TestCase):
base.obj_to_primitive(myobj))
def test_obj_to_primitive_recursive(self):
@base.VersionedObjectRegistry.register
class MyList(base.ObjectListBase, base.NovaObject):
fields = {'objects': fields.ListOfObjectsField('MyObj')}
@@ -241,6 +259,7 @@ class TestObjToPrimitive(test.TestCase):
base.obj_to_primitive(mylist))
def test_obj_to_primitive_with_ip_addr(self):
@base.VersionedObjectRegistry.register
class TestObject(base.NovaObject):
fields = {'addr': fields.IPAddressField(),
'cidr': fields.IPNetworkField()}
@@ -253,6 +272,7 @@ class TestObjToPrimitive(test.TestCase):
class TestObjMakeList(test.TestCase):
def test_obj_make_list(self):
@base.VersionedObjectRegistry.register
class MyList(base.ObjectListBase, base.NovaObject):
pass
@@ -518,6 +538,7 @@ class _TestObject(object):
self.assertEqual(obj.bar, 'loaded!')
def test_load_in_base(self):
@base.VersionedObjectRegistry.register
class Foo(base.NovaObject):
fields = {'foobar': fields.Field(fields.Integer())}
obj = Foo()
@@ -623,6 +644,7 @@ class _TestObject(object):
self.assertRemotes()
def test_changed_with_sub_object(self):
@base.VersionedObjectRegistry.register
class ParentObject(base.NovaObject):
fields = {'foo': fields.IntegerField(),
'bar': fields.ObjectField('MyObj'),
@@ -909,6 +931,7 @@ class TestRemoteObject(_RemoteTest, _TestObject):
class TestObjectListBase(test.TestCase):
def test_list_like_operations(self):
@base.VersionedObjectRegistry.register
class MyElement(base.NovaObject):
fields = {'foo': fields.IntegerField()}
@@ -934,9 +957,11 @@ class TestObjectListBase(test.TestCase):
[x.foo for x in objlist])
def test_serialization(self):
@base.VersionedObjectRegistry.register
class Foo(base.ObjectListBase, base.NovaObject):
fields = {'objects': fields.ListOfObjectsField('Bar')}
@base.VersionedObjectRegistry.register
class Bar(base.NovaObject):
fields = {'foo': fields.Field(fields.String())}
@@ -958,7 +983,10 @@ class TestObjectListBase(test.TestCase):
# Look through all object classes of this type and make sure that
# the versions we find are covered by the parent list class
for item_class in base.NovaObject._obj_classes[item_obj_name]:
obj_classes = base.VersionedObjectRegistry.obj_classes()[item_obj_name]
for item_class in obj_classes:
if is_test_object(item_class):
continue
self.assertIn(
item_class.VERSION,
list_obj_class.child_versions.values(),
@@ -968,15 +996,17 @@ class TestObjectListBase(test.TestCase):
def test_object_version_mappings(self):
# Find all object list classes and make sure that they at least handle
# all the current object versions
for obj_classes in base.NovaObject._obj_classes.values():
for obj_classes in base.VersionedObjectRegistry.obj_classes().values():
for obj_class in obj_classes:
if issubclass(obj_class, base.ObjectListBase):
self._test_object_list_version_mappings(obj_class)
def test_list_changes(self):
@base.VersionedObjectRegistry.register
class Foo(base.ObjectListBase, base.NovaObject):
fields = {'objects': fields.ListOfObjectsField('Bar')}
@base.VersionedObjectRegistry.register
class Bar(base.NovaObject):
fields = {'foo': fields.StringField()}
@@ -1003,9 +1033,11 @@ class TestObjectListBase(test.TestCase):
self.assertEqual(set(), obj.obj_what_changed())
def test_obj_repr(self):
@base.VersionedObjectRegistry.register
class Foo(base.ObjectListBase, base.NovaObject):
fields = {'objects': fields.ListOfObjectsField('Bar')}
@base.VersionedObjectRegistry.register
class Bar(base.NovaObject):
fields = {'uuid': fields.StringField()}
@@ -1034,6 +1066,7 @@ class TestObjectSerializer(_BaseTestCase):
ser._conductor = mock.Mock()
ser._conductor.object_backport.return_value = 'backported'
@base.VersionedObjectRegistry.register
class MyTestObj(MyObj):
VERSION = my_version
@@ -1128,7 +1161,7 @@ object_data = {
'MyOwnedObject': '1.0-fec853730bd02d54cc32771dd67f08a0',
'TestSubclassedObject': '1.6-6c1976a36987b9832b3183a7d9163655',
}
object_data = {}
object_relationships = {
'BlockDeviceMapping': {'Instance': '1.18'},
@@ -1170,7 +1203,7 @@ class TestObjectVersions(test.TestCase):
return None
def _get_fingerprint(self, obj_name):
obj_class = base.NovaObject._obj_classes[obj_name][0]
obj_class = base.VersionedObjectRegistry.obj_classes()[obj_name][0]
fields = obj_class.fields.items()
fields.sort()
methods = []
@@ -1196,7 +1229,11 @@ class TestObjectVersions(test.TestCase):
def test_versions(self):
fingerprints = {}
for obj_name in base.NovaObject._obj_classes:
for obj_name in base.VersionedObjectRegistry.obj_classes():
obj_classes = base.VersionedObjectRegistry._registry._obj_classes
obj_cls = obj_classes[obj_name][0]
if is_test_object(obj_cls):
continue
fingerprints[obj_name] = self._get_fingerprint(obj_name)
if os.getenv('GENERATE_HASHES'):
@@ -1227,7 +1264,8 @@ class TestObjectVersions(test.TestCase):
for name, field in obj_class.fields.items():
if isinstance(field._type, fields.Object):
sub_obj_name = field._type._obj_name
sub_obj_class = base.NovaObject._obj_classes[sub_obj_name][0]
obj_classes = base.VersionedObjectRegistry.obj_classes()
sub_obj_class = obj_classes[sub_obj_name][0]
self._build_tree(tree, sub_obj_class)
tree.setdefault(obj_name, {})
tree[obj_name][sub_obj_name] = sub_obj_class.VERSION
@@ -1235,8 +1273,9 @@ class TestObjectVersions(test.TestCase):
def test_relationships(self):
self.skip('relationship test needs to be rewritten')
tree = {}
for obj_name in base.NovaObject._obj_classes.keys():
self._build_tree(tree, base.NovaObject._obj_classes[obj_name][0])
obj_classes = base.VersionedObjectRegistry.obj_classes()
for obj_name in base.VersionedObjectRegistry.obj_classes().keys():
self._build_tree(tree, obj_classes[obj_name][0])
stored = set([(x, str(y)) for x, y in object_relationships.items()])
computed = set([(x, str(y)) for x, y in tree.items()])
@@ -1259,8 +1298,8 @@ class TestObjectVersions(test.TestCase):
# This doesn't actually test the data conversions, but it at least
# makes sure the method doesn't blow up on something basic like
# expecting the wrong version format.
for obj_name in base.NovaObject._obj_classes:
obj_class = base.NovaObject._obj_classes[obj_name][0]
for obj_name in base.VersionedObjectRegistry.obj_classes():
obj_class = base.VersionedObjectRegistry.obj_classes()[obj_name][0]
version = utils.convert_version_to_tuple(obj_class.VERSION)
for n in range(version[1]):
test_version = '%d.%d' % (version[0], n)
@@ -1274,8 +1313,8 @@ class TestObjectVersions(test.TestCase):
# This doesn't actually test the data conversions, but it at least
# makes sure the method doesn't blow up on something basic like
# expecting the wrong version format.
for obj_name in base.NovaObject._obj_classes:
obj_class = base.NovaObject._obj_classes[obj_name][0]
for obj_name in base.VersionedObjectRegistry.obj_classes():
obj_class = base.VersionedObjectRegistry.obj_classes()[obj_name][0]
for field, versions in obj_class.obj_relationships.items():
last_my_version = (0, 0)
last_child_version = (0, 0)