From 81af1ab488930b511cac056cdc4ac2a8a26f0d7b Mon Sep 17 00:00:00 2001 From: Dan Smith Date: Mon, 2 Dec 2013 12:25:04 -0800 Subject: [PATCH] Make Serializer able to backlevel objects This improves handling the situation where nova-api sends newer objects to an older compute node. In this case, the serializer detects the IncompatibleObjectVersion error and asks conductor to backlevel it. This means we call into the conductor with the primitive object that we can't handle, and basically ask it to hydrate it and call obj_make_compatible() on our behalf and pass it back to us. This eliminates the need for API to know whether it's talking to an older compute and allows a newer conductor to assist older compute nodes during upgrades to a significant degree. Adapted for havana/stable from master commit: 0eb7e35 Related-Bug: #1258256 Change-Id: I4b17c8382619e4f73b83c026bf68549ac67a68a2 --- nova/conductor/api.py | 3 +++ nova/conductor/manager.py | 3 +++ nova/conductor/rpcapi.py | 5 +++++ nova/objects/base.py | 30 ++++++++++++++++++++++++++++-- nova/tests/objects/test_objects.py | 25 +++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) 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()