diff --git a/nova/conductor/api.py b/nova/conductor/api.py index eea5886a470e..f6944a8fa065 100644 --- a/nova/conductor/api.py +++ b/nova/conductor/api.py @@ -327,6 +327,9 @@ class LocalAPI(object): def compute_unrescue(self, context, instance): return self._manager.compute_unrescue(context, instance) + def object_backport(self, context, objinst, target_version): + return self._manager.object_backport(context, objinst, target_version) + class LocalComputeTaskAPI(object): def __init__(self): diff --git a/nova/conductor/manager.py b/nova/conductor/manager.py index c1c958bdb2c9..fe84ad73b20c 100644 --- a/nova/conductor/manager.py +++ b/nova/conductor/manager.py @@ -599,6 +599,9 @@ class ConductorManager(manager.Manager): def compute_reboot(self, context, instance, reboot_type): self.compute_api.reboot(context, instance, reboot_type) + def object_backport(self, context, objinst, target_version): + return objinst.obj_to_primitive(target_version=target_version) + class ComputeTaskManager(base.Base): """Namespace for compute methods. diff --git a/nova/conductor/rpcapi.py b/nova/conductor/rpcapi.py index b4b56eb00ee1..fd7809a5f6b4 100644 --- a/nova/conductor/rpcapi.py +++ b/nova/conductor/rpcapi.py @@ -496,6 +496,11 @@ class ConductorAPI(rpcclient.RpcProxy): return cctxt.call(context, 'object_action', objinst=objinst, objmethod=objmethod, args=args, kwargs=kwargs) + def object_backport(self, context, objinst, target_version): + cctxt = self.client.prepare(version='1.62') + return cctxt.call(context, 'object_backport', objinst=objinst, + target_version=target_version) + class ComputeTaskAPI(rpcclient.RpcProxy): """Client side of the conductor 'compute' namespaced RPC API diff --git a/nova/objects/base.py b/nova/objects/base.py index f9a0c9944128..dc0df4cabacf 100644 --- a/nova/objects/base.py +++ b/nova/objects/base.py @@ -214,10 +214,18 @@ class NovaObject(object): '%(objtype)s') % dict(objtype=objname)) raise exception.UnsupportedObjectError(objtype=objname) + latest = None compatible_match = None for objclass in cls._obj_classes[objname]: if objclass.VERSION == objver: return objclass + + version_bits = tuple([int(x) for x in objclass.VERSION.split(".")]) + if latest is None: + latest = version_bits + elif latest < version_bits: + latest = version_bits + try: check_object_version(objclass.VERSION, objver) compatible_match = objclass @@ -227,8 +235,10 @@ class NovaObject(object): if compatible_match: return compatible_match + latest_ver = '%i.%i' % latest raise exception.IncompatibleObjectVersion(objname=objname, - objver=objver) + objver=objver, + supported=latest_ver) def _attr_from_primitive(self, attribute, value): """Attribute deserialization dispatcher. @@ -510,6 +520,22 @@ class NovaObjectSerializer(nova.openstack.common.rpc.serializer.Serializer): that needs to accept or return NovaObjects as arguments or result values should pass this to its RpcProxy and RpcDispatcher objects. """ + + @property + def conductor(self): + if not hasattr(self, '_conductor'): + from nova.conductor import api as conductor_api + self._conductor = conductor_api.API() + return self._conductor + + def _process_object(self, context, objprim): + try: + objinst = NovaObject.obj_from_primitive(objprim, context=context) + except exception.IncompatibleObjectVersion as e: + objinst = self.conductor.object_backport(context, objprim, + e.kwargs['supported']) + return objinst + def _process_iterable(self, context, action_fn, values): """Process an iterable, taking an action on each value. :param:context: Request context @@ -537,7 +563,7 @@ class NovaObjectSerializer(nova.openstack.common.rpc.serializer.Serializer): def deserialize_entity(self, context, entity): if isinstance(entity, dict) and 'nova_object.name' in entity: - entity = NovaObject.obj_from_primitive(entity, context=context) + entity = self._process_object(context, entity) elif isinstance(entity, (tuple, list, set)): entity = self._process_iterable(context, self.deserialize_entity, entity) diff --git a/nova/tests/objects/test_objects.py b/nova/tests/objects/test_objects.py index 96ef1d106170..fb5f035d0edd 100644 --- a/nova/tests/objects/test_objects.py +++ b/nova/tests/objects/test_objects.py @@ -15,6 +15,8 @@ import contextlib import datetime import iso8601 + +import mock import netaddr from nova.conductor import rpcapi as conductor_rpcapi @@ -469,6 +471,16 @@ class _TestObject(object): self.assertRaises(exception.UnsupportedObjectError, base.NovaObject.obj_class_from_name, 'foo', '1.0') + def test_obj_class_from_name_supported_version(self): + error = None + try: + base.NovaObject.obj_class_from_name('MyObj', '1.25') + except exception.IncompatibleObjectVersion as error: + pass + + self.assertIsNotNone(error) + self.assertEqual('1.5', error.kwargs['supported']) + def test_with_alternate_context(self): ctxt1 = context.RequestContext('foo', 'foo') ctxt2 = context.RequestContext('bar', 'alternate') @@ -694,6 +706,19 @@ class TestObjectSerializer(_BaseTestCase): for thing in (1, 'foo', [1, 2], {'foo': 'bar'}): self.assertEqual(thing, ser.deserialize_entity(None, thing)) + def test_deserialize_entity_newer_version(self): + ser = base.NovaObjectSerializer() + ser._conductor = mock.Mock() + ser._conductor.object_backport.return_value = 'backported' + obj = MyObj() + obj.VERSION = '1.25' + primitive = obj.obj_to_primitive() + result = ser.deserialize_entity(self.context, primitive) + self.assertEqual('backported', result) + ser._conductor.object_backport.assert_called_with(self.context, + primitive, + '1.5') + def test_object_serialization(self): ser = base.NovaObjectSerializer() obj = MyObj()