Make Serializer/Conductor 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 a new method in 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.

Related-Bug: #1258256
Change-Id: I4b17c8382619e4f73b83c026bf68549ac67a68a2
This commit is contained in:
Dan Smith 2013-12-02 12:25:04 -08:00
parent b32d01d44c
commit 0eb7e35fbf
5 changed files with 64 additions and 3 deletions

View File

@ -304,6 +304,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):

View File

@ -76,7 +76,7 @@ class ConductorManager(manager.Manager):
namespace. See the ComputeTaskManager class for details.
"""
RPC_API_VERSION = '1.61'
RPC_API_VERSION = '1.62'
def __init__(self, *args, **kwargs):
super(ConductorManager, self).__init__(service_name='conductor',
@ -616,6 +616,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.

View File

@ -120,6 +120,7 @@ class ConductorAPI(rpcclient.RpcProxy):
... - Remove security_group_get_by_instance() and
security_group_rule_get_by_security_group()
1.61 - Return deleted instance from instance_destroy()
1.62 - Added object_backport()
"""
BASE_RPC_API_VERSION = '1.0'
@ -473,6 +474,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

View File

@ -201,19 +201,28 @@ 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
if versionutils.is_compatible(objver, objclass.VERSION):
compatible_match = objclass
if compatible_match:
return compatible_match
latest_ver = '%i.%i' % latest
raise exception.IncompatibleObjectVersion(objname=objname,
objver=objver)
objver=objver,
supported=latest_ver)
@classmethod
def obj_from_primitive(cls, primitive, context=None):
@ -501,6 +510,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
@ -528,7 +553,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)

View File

@ -16,6 +16,7 @@ import contextlib
import datetime
import iso8601
import mock
import netaddr
import six
from testtools import matchers
@ -498,6 +499,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')
@ -754,6 +765,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()