summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Grasza <grzegorz.grasza@intel.com>2015-09-16 15:12:42 +0200
committerJim Rollenhagen <jim@jimrollenhagen.com>2015-09-24 00:26:33 +0000
commitdb9ddd39d3f3d854d38e4b05655ebddd98f1a727 (patch)
treef5d327f0ffb81126ecf93fc152da5bc6add4dbb6
parentcd32fa542193fc26b4ee77c7d004e056f1ce9bf8 (diff)
downloadironic-db9ddd39d3f3d854d38e4b05655ebddd98f1a727.tar.gz
Implement indirection_api
During a rolling upgrade, ironic conductor and api services are running with different versions. When an object is received in an incompatible version, IncompatibleObjectVersion is raised. Implementation of the indirection API allows the object to be backported to a supported version by the conductor (conductor has to be the first service to be upgraded). This change enables backporting of objects from Mitaka. This lays the foundation to be able to do rolling upgrades from Liberty (or from this patch onwards) to Mitaka. There may still be other issues that will need fixing in Mitaka before we will be able to do a full rolling upgrade. Enabling the indirection_api causes all object methods decorated with the remotable or remotable_classmethod to do RPC from ironic-api to ironic-conductor service. To keep the current behavior, I'm removing all remotable decorators on object methods and thus not enabling object RPC calls in this patch. These calls caused random timeouts on the CI gates (probably due to a race in which Nova calls the ironic-api service before ironic-conductor is up). RPC calls made via the indirection_api should be enabled one-by-one, when the implications are fully understood. Change-Id: Ia381348da93f95d764c83f3ec2a2ed5a1db5ad6d Closes-Bug: 1493816 Depends-On: I81400305f166d62aa4612aab54602abb8178b64c
-rw-r--r--ironic/cmd/api.py4
-rw-r--r--ironic/common/exception.py4
-rw-r--r--ironic/conductor/manager.py105
-rw-r--r--ironic/conductor/rpcapi.py76
-rw-r--r--ironic/objects/base.py30
-rw-r--r--ironic/objects/chassis.py44
-rw-r--r--ironic/objects/conductor.py16
-rw-r--r--ironic/objects/node.py73
-rw-r--r--ironic/objects/port.py56
-rw-r--r--ironic/tests/conductor/test_manager.py105
-rw-r--r--ironic/tests/conductor/test_rpcapi.py56
-rw-r--r--ironic/tests/objects/test_objects.py46
12 files changed, 565 insertions, 50 deletions
diff --git a/ironic/cmd/api.py b/ironic/cmd/api.py
index 0ad81996d..ce531a79d 100644
--- a/ironic/cmd/api.py
+++ b/ironic/cmd/api.py
@@ -28,6 +28,7 @@ from six.moves import socketserver
from ironic.api import app
from ironic.common.i18n import _LI
from ironic.common import service as ironic_service
+from ironic.objects import base
CONF = cfg.CONF
@@ -42,6 +43,9 @@ def main():
# Parse config file and command line options, then start logging
ironic_service.prepare_service(sys.argv)
+ # Enable object backporting via the conductor
+ base.IronicObject.indirection_api = base.IronicObjectIndirectionAPI()
+
# Build and start the WSGI app
host = CONF.api.host_ip
port = CONF.api.port
diff --git a/ironic/common/exception.py b/ironic/common/exception.py
index afa140dda..5a46d67ea 100644
--- a/ironic/common/exception.py
+++ b/ironic/common/exception.py
@@ -350,10 +350,6 @@ class UnsupportedDriverExtension(Invalid):
'(disabled or not implemented).')
-class IncompatibleObjectVersion(IronicException):
- message = _('Version %(objver)s of %(objname)s is not supported')
-
-
class GlanceConnectionFailed(IronicException):
message = _("Connection to glance host %(host)s:%(port)s failed: "
"%(reason)s")
diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py
index 931515d53..1354a39be 100644
--- a/ironic/conductor/manager.py
+++ b/ironic/conductor/manager.py
@@ -77,6 +77,7 @@ from ironic.conductor import task_manager
from ironic.conductor import utils
from ironic.db import api as dbapi
from ironic import objects
+from ironic.objects import base as objects_base
MANAGER_TOPIC = 'ironic.conductor_manager'
WORKER_SPAWN_lOCK = "conductor_worker_spawn"
@@ -211,7 +212,7 @@ class ConductorManager(periodic_task.PeriodicTasks):
"""Ironic Conductor manager main class."""
# NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's.
- RPC_API_VERSION = '1.30'
+ RPC_API_VERSION = '1.31'
target = messaging.Target(version=RPC_API_VERSION)
@@ -2030,6 +2031,108 @@ class ConductorManager(periodic_task.PeriodicTasks):
return driver.raid.get_logical_disk_properties()
+ def _object_dispatch(self, target, method, context, args, kwargs):
+ """Dispatch a call to an object method.
+
+ This ensures that object methods get called and any exception
+ that is raised gets wrapped in an ExpectedException for forwarding
+ back to the caller (without spamming the conductor logs).
+ """
+ try:
+ # NOTE(danms): Keep the getattr inside the try block since
+ # a missing method is really a client problem
+ return getattr(target, method)(context, *args, **kwargs)
+ except Exception:
+ # NOTE(danms): This is oslo.messaging fu. ExpectedException()
+ # grabs sys.exc_info here and forwards it along. This allows the
+ # caller to see the exception information, but causes us *not* to
+ # log it as such in this service. This is something that is quite
+ # critical so that things that conductor does on behalf of another
+ # node are not logged as exceptions in conductor logs. Otherwise,
+ # you'd have the same thing logged in both places, even though an
+ # exception here *always* means that the caller screwed up, so
+ # there's no reason to log it here.
+ raise messaging.ExpectedException()
+
+ def object_class_action_versions(self, context, objname, objmethod,
+ object_versions, args, kwargs):
+ """Perform an action on a VersionedObject class.
+
+ :param context: The context within which to perform the action
+ :param objname: The registry name of the object
+ :param objmethod: The name of the action method to call
+ :param object_versions: A dict of {objname: version} mappings
+ :param args: The positional arguments to the action method
+ :param kwargs: The keyword arguments to the action method
+ :returns: The result of the action method, which may (or may not)
+ be an instance of the implementing VersionedObject class.
+ """
+ objclass = objects_base.IronicObject.obj_class_from_name(
+ objname, object_versions[objname])
+ result = self._object_dispatch(objclass, objmethod, context,
+ args, kwargs)
+ # NOTE(danms): The RPC layer will convert to primitives for us,
+ # but in this case, we need to honor the version the client is
+ # asking for, so we do it before returning here.
+ if isinstance(result, objects_base.IronicObject):
+ result = result.obj_to_primitive(
+ target_version=object_versions[objname],
+ version_manifest=object_versions)
+ return result
+
+ def object_action(self, context, objinst, objmethod, args, kwargs):
+ """Perform an action on a VersionedObject instance.
+
+ :param context: The context within which to perform the action
+ :param objinst: The object instance on which to perform the action
+ :param objmethod: The name of the action method to call
+ :param args: The positional arguments to the action method
+ :param kwargs: The keyword arguments to the action method
+ :returns: A tuple with the updates made to the object and
+ the result of the action method
+ """
+
+ oldobj = objinst.obj_clone()
+ result = self._object_dispatch(objinst, objmethod, context,
+ args, kwargs)
+ updates = dict()
+ # NOTE(danms): Diff the object with the one passed to us and
+ # generate a list of changes to forward back
+ for name, field in objinst.fields.items():
+ if not objinst.obj_attr_is_set(name):
+ # Avoid demand-loading anything
+ continue
+ if (not oldobj.obj_attr_is_set(name) or
+ getattr(oldobj, name) != getattr(objinst, name)):
+ updates[name] = field.to_primitive(objinst, name,
+ getattr(objinst, name))
+ # This is safe since a field named this would conflict with the
+ # method anyway
+ updates['obj_what_changed'] = objinst.obj_what_changed()
+ return updates, result
+
+ def object_backport_versions(self, context, objinst, object_versions):
+ """Perform a backport of an object instance.
+
+ The default behavior of the base VersionedObjectSerializer, upon
+ receiving an object with a version newer than what is in the local
+ registry, is to call this method to request a backport of the object.
+
+ :param context: The context within which to perform the backport
+ :param objinst: An instance of a VersionedObject to be backported
+ :param object_versions: A dict of {objname: version} mappings
+ :returns: The downgraded instance of objinst
+ """
+ target = object_versions[objinst.obj_name()]
+ LOG.debug('Backporting %(obj)s to %(ver)s with versions %(manifest)s',
+ {'obj': objinst.obj_name(),
+ 'ver': target,
+ 'manifest': ','.join(
+ ['%s=%s' % (name, ver)
+ for name, ver in object_versions.items()])})
+ return objinst.obj_to_primitive(target_version=target,
+ version_manifest=object_versions)
+
def get_vendor_passthru_metadata(route_dict):
d = {}
diff --git a/ironic/conductor/rpcapi.py b/ironic/conductor/rpcapi.py
index 8cb2ef205..830743023 100644
--- a/ironic/conductor/rpcapi.py
+++ b/ironic/conductor/rpcapi.py
@@ -75,11 +75,14 @@ class ConductorAPI(object):
| driver_vendor_passthru to a dictionary
| 1.30 - Added set_target_raid_config and
| get_raid_logical_disk_properties
+ | 1.31 - Added Versioned Objects indirection API methods:
+ | object_class_action_versions, object_action and
+ | object_backport_versions
"""
# NOTE(rloo): This must be in sync with manager.ConductorManager's.
- RPC_API_VERSION = '1.30'
+ RPC_API_VERSION = '1.31'
def __init__(self, topic=None):
super(ConductorAPI, self).__init__()
@@ -589,3 +592,74 @@ class ConductorAPI(object):
cctxt = self.client.prepare(topic=topic or self.topic, version='1.30')
return cctxt.call(context, 'get_raid_logical_disk_properties',
driver_name=driver_name)
+
+ def object_class_action_versions(self, context, objname, objmethod,
+ object_versions, args, kwargs):
+ """Perform an action on a VersionedObject class.
+
+ We want any conductor to handle this, so it is intentional that there
+ is no topic argument for this method.
+
+ :param context: The context within which to perform the action
+ :param objname: The registry name of the object
+ :param objmethod: The name of the action method to call
+ :param object_versions: A dict of {objname: version} mappings
+ :param args: The positional arguments to the action method
+ :param kwargs: The keyword arguments to the action method
+ :raises: NotImplemented when an operator makes an error during upgrade
+ :returns: The result of the action method, which may (or may not)
+ be an instance of the implementing VersionedObject class.
+ """
+ if not self.client.can_send_version('1.31'):
+ raise NotImplemented(_('Incompatible conductor version - '
+ 'please upgrade ironic-conductor first'))
+ cctxt = self.client.prepare(topic=self.topic, version='1.31')
+ return cctxt.call(context, 'object_class_action_versions',
+ objname=objname, objmethod=objmethod,
+ object_versions=object_versions,
+ args=args, kwargs=kwargs)
+
+ def object_action(self, context, objinst, objmethod, args, kwargs):
+ """Perform an action on a VersionedObject instance.
+
+ We want any conductor to handle this, so it is intentional that there
+ is no topic argument for this method.
+
+ :param context: The context within which to perform the action
+ :param objinst: The object instance on which to perform the action
+ :param objmethod: The name of the action method to call
+ :param args: The positional arguments to the action method
+ :param kwargs: The keyword arguments to the action method
+ :raises: NotImplemented when an operator makes an error during upgrade
+ :returns: A tuple with the updates made to the object and
+ the result of the action method
+ """
+ if not self.client.can_send_version('1.31'):
+ raise NotImplemented(_('Incompatible conductor version - '
+ 'please upgrade ironic-conductor first'))
+ cctxt = self.client.prepare(topic=self.topic, version='1.31')
+ return cctxt.call(context, 'object_action', objinst=objinst,
+ objmethod=objmethod, args=args, kwargs=kwargs)
+
+ def object_backport_versions(self, context, objinst, object_versions):
+ """Perform a backport of an object instance.
+
+ The default behavior of the base VersionedObjectSerializer, upon
+ receiving an object with a version newer than what is in the local
+ registry, is to call this method to request a backport of the object.
+
+ We want any conductor to handle this, so it is intentional that there
+ is no topic argument for this method.
+
+ :param context: The context within which to perform the backport
+ :param objinst: An instance of a VersionedObject to be backported
+ :param object_versions: A dict of {objname: version} mappings
+ :raises: NotImplemented when an operator makes an error during upgrade
+ :returns: The downgraded instance of objinst
+ """
+ if not self.client.can_send_version('1.31'):
+ raise NotImplemented(_('Incompatible conductor version - '
+ 'please upgrade ironic-conductor first'))
+ cctxt = self.client.prepare(topic=self.topic, version='1.31')
+ return cctxt.call(context, 'object_backport_versions', objinst=objinst,
+ object_versions=object_versions)
diff --git a/ironic/objects/base.py b/ironic/objects/base.py
index 14ef1aadf..56bb71ed7 100644
--- a/ironic/objects/base.py
+++ b/ironic/objects/base.py
@@ -65,6 +65,36 @@ class IronicObject(object_base.VersionedObject):
self[field] = loaded_object[field]
+class IronicObjectIndirectionAPI(object_base.VersionedObjectIndirectionAPI):
+ def __init__(self):
+ super(IronicObjectIndirectionAPI, self).__init__()
+ # FIXME(xek): importing here due to a cyclical import error
+ from ironic.conductor import rpcapi as conductor_api
+ self._conductor = conductor_api.ConductorAPI()
+
+ def object_action(self, context, objinst, objmethod, args, kwargs):
+ return self._conductor.object_action(context, objinst, objmethod,
+ args, kwargs)
+
+ def object_class_action(self, context, objname, objmethod, objver,
+ args, kwargs):
+ # NOTE(xek): This method is implemented for compatibility with
+ # oslo.versionedobjects 0.10.0 and older. It will be replaced by
+ # object_class_action_versions.
+ versions = object_base.obj_tree_get_versions(objname)
+ return self.object_class_action_versions(
+ context, objname, objmethod, versions, args, kwargs)
+
+ def object_class_action_versions(self, context, objname, objmethod,
+ object_versions, args, kwargs):
+ return self._conductor.object_class_action_versions(
+ context, objname, objmethod, object_versions, args, kwargs)
+
+ def object_backport_versions(self, context, objinst, object_versions):
+ return self._conductor.object_backport_versions(context, objinst,
+ object_versions)
+
+
class IronicObjectSerializer(object_base.VersionedObjectSerializer):
# Base class to use for object hydration
OBJ_BASE_CLASS = IronicObject
diff --git a/ironic/objects/chassis.py b/ironic/objects/chassis.py
index 226a5db4a..8983ca0a7 100644
--- a/ironic/objects/chassis.py
+++ b/ironic/objects/chassis.py
@@ -55,7 +55,11 @@ class Chassis(base.IronicObject, object_base.VersionedObjectDictCompat):
chassis.obj_reset_changes()
return chassis
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get(cls, context, chassis_id):
"""Find a chassis based on its id or uuid and return a Chassis object.
@@ -69,7 +73,11 @@ class Chassis(base.IronicObject, object_base.VersionedObjectDictCompat):
else:
raise exception.InvalidIdentity(identity=chassis_id)
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get_by_id(cls, context, chassis_id):
"""Find a chassis based on its integer id and return a Chassis object.
@@ -80,7 +88,11 @@ class Chassis(base.IronicObject, object_base.VersionedObjectDictCompat):
chassis = Chassis._from_db_object(cls(context), db_chassis)
return chassis
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get_by_uuid(cls, context, uuid):
"""Find a chassis based on uuid and return a :class:`Chassis` object.
@@ -92,7 +104,11 @@ class Chassis(base.IronicObject, object_base.VersionedObjectDictCompat):
chassis = Chassis._from_db_object(cls(context), db_chassis)
return chassis
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def list(cls, context, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Return a list of Chassis objects.
@@ -112,7 +128,10 @@ class Chassis(base.IronicObject, object_base.VersionedObjectDictCompat):
return [Chassis._from_db_object(cls(context), obj)
for obj in db_chassis]
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def create(self, context=None):
"""Create a Chassis record in the DB.
@@ -133,7 +152,10 @@ class Chassis(base.IronicObject, object_base.VersionedObjectDictCompat):
db_chassis = self.dbapi.create_chassis(values)
self._from_db_object(self, db_chassis)
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def destroy(self, context=None):
"""Delete the Chassis from the DB.
@@ -147,7 +169,10 @@ class Chassis(base.IronicObject, object_base.VersionedObjectDictCompat):
self.dbapi.destroy_chassis(self.uuid)
self.obj_reset_changes()
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def save(self, context=None):
"""Save updates to this Chassis.
@@ -165,7 +190,10 @@ class Chassis(base.IronicObject, object_base.VersionedObjectDictCompat):
updated_chassis = self.dbapi.update_chassis(self.uuid, updates)
self._from_db_object(self, updated_chassis)
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def refresh(self, context=None):
"""Loads and applies updates for this Chassis.
diff --git a/ironic/objects/conductor.py b/ironic/objects/conductor.py
index eaf98447c..e0028e93a 100644
--- a/ironic/objects/conductor.py
+++ b/ironic/objects/conductor.py
@@ -42,7 +42,11 @@ class Conductor(base.IronicObject, object_base.VersionedObjectDictCompat):
conductor.obj_reset_changes()
return conductor
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get_by_hostname(cls, context, hostname):
"""Get a Conductor record by its hostname.
@@ -58,7 +62,10 @@ class Conductor(base.IronicObject, object_base.VersionedObjectDictCompat):
raise NotImplementedError(
_('Cannot update a conductor record directly.'))
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def refresh(self, context=None):
"""Loads and applies updates for this Conductor.
@@ -77,7 +84,10 @@ class Conductor(base.IronicObject, object_base.VersionedObjectDictCompat):
hostname=self.hostname)
self.obj_refresh(current)
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def touch(self, context):
"""Touch this conductor's DB record, marking it as up-to-date."""
self.dbapi.touch_conductor(self.hostname)
diff --git a/ironic/objects/node.py b/ironic/objects/node.py
index 483b65426..b358c3adf 100644
--- a/ironic/objects/node.py
+++ b/ironic/objects/node.py
@@ -106,7 +106,11 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
node.obj_reset_changes()
return node
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get(cls, context, node_id):
"""Find a node based on its id or uuid and return a Node object.
@@ -120,7 +124,11 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
else:
raise exception.InvalidIdentity(identity=node_id)
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get_by_id(cls, context, node_id):
"""Find a node based on its integer id and return a Node object.
@@ -131,7 +139,11 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
node = Node._from_db_object(cls(context), db_node)
return node
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get_by_uuid(cls, context, uuid):
"""Find a node based on uuid and return a Node object.
@@ -142,7 +154,11 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
node = Node._from_db_object(cls(context), db_node)
return node
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get_by_name(cls, context, name):
"""Find a node based on name and return a Node object.
@@ -153,7 +169,11 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
node = Node._from_db_object(cls(context), db_node)
return node
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get_by_instance_uuid(cls, context, instance_uuid):
"""Find a node based on the instance uuid and return a Node object.
@@ -164,7 +184,11 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
node = Node._from_db_object(cls(context), db_node)
return node
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def list(cls, context, limit=None, marker=None, sort_key=None,
sort_dir=None, filters=None):
"""Return a list of Node objects.
@@ -183,7 +207,11 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
sort_dir=sort_dir)
return [Node._from_db_object(cls(context), obj) for obj in db_nodes]
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def reserve(cls, context, tag, node_id):
"""Get and reserve a node.
@@ -201,7 +229,11 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
node = Node._from_db_object(cls(context), db_node)
return node
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def release(cls, context, tag, node_id):
"""Release the reservation on a node.
@@ -213,7 +245,10 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
"""
cls.dbapi.release_node(tag, node_id)
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def create(self, context=None):
"""Create a Node record in the DB.
@@ -234,7 +269,10 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
db_node = self.dbapi.create_node(values)
self._from_db_object(self, db_node)
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def destroy(self, context=None):
"""Delete the Node from the DB.
@@ -248,7 +286,10 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
self.dbapi.destroy_node(self.uuid)
self.obj_reset_changes()
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def save(self, context=None):
"""Save updates to this Node.
@@ -272,7 +313,10 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
self.dbapi.update_node(self.uuid, updates)
self.obj_reset_changes()
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def refresh(self, context=None):
"""Refresh the object by re-fetching from the DB.
@@ -286,7 +330,10 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
current = self.__class__.get_by_uuid(self._context, self.uuid)
self.obj_refresh(current)
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def touch_provisioning(self, context=None):
"""Touch the database record to mark the provisioning as alive."""
self.dbapi.touch_node_provisioning(self.id)
diff --git a/ironic/objects/port.py b/ironic/objects/port.py
index c145d66ed..5e9cd8cd5 100644
--- a/ironic/objects/port.py
+++ b/ironic/objects/port.py
@@ -58,7 +58,11 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
"""Converts a list of database entities to a list of formal objects."""
return [Port._from_db_object(cls(context), obj) for obj in db_objects]
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get(cls, context, port_id):
"""Find a port based on its id or uuid and return a Port object.
@@ -76,7 +80,11 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
else:
raise exception.InvalidIdentity(identity=port_id)
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get_by_id(cls, context, port_id):
"""Find a port based on its integer id and return a Port object.
@@ -89,7 +97,11 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
port = Port._from_db_object(cls(context), db_port)
return port
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get_by_uuid(cls, context, uuid):
"""Find a port based on uuid and return a :class:`Port` object.
@@ -103,7 +115,11 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
port = Port._from_db_object(cls(context), db_port)
return port
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def get_by_address(cls, context, address):
"""Find a port based on address and return a :class:`Port` object.
@@ -117,7 +133,11 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
port = Port._from_db_object(cls(context), db_port)
return port
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def list(cls, context, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Return a list of Port objects.
@@ -137,7 +157,11 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
sort_dir=sort_dir)
return Port._from_db_object_list(db_ports, cls, context)
- @object_base.remotable_classmethod
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable_classmethod
+ @classmethod
def list_by_node_id(cls, context, node_id, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Return a list of Port objects associated with a given node ID.
@@ -157,7 +181,10 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
sort_dir=sort_dir)
return Port._from_db_object_list(db_ports, cls, context)
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def create(self, context=None):
"""Create a Port record in the DB.
@@ -175,7 +202,10 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
db_port = self.dbapi.create_port(values)
self._from_db_object(self, db_port)
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def destroy(self, context=None):
"""Delete the Port from the DB.
@@ -191,7 +221,10 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
self.dbapi.destroy_port(self.uuid)
self.obj_reset_changes()
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def save(self, context=None):
"""Save updates to this Port.
@@ -212,7 +245,10 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
updated_port = self.dbapi.update_port(self.uuid, updates)
self._from_db_object(self, updated_port)
- @object_base.remotable
+ # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable
+ # methods can be used in the future to replace current explicit RPC calls.
+ # Implications of calling new remote procedures should be thought through.
+ # @object_base.remotable
def refresh(self, context=None):
"""Loads updates for this Port.
diff --git a/ironic/tests/conductor/test_manager.py b/ironic/tests/conductor/test_manager.py
index d49faaea3..39fb5b689 100644
--- a/ironic/tests/conductor/test_manager.py
+++ b/ironic/tests/conductor/test_manager.py
@@ -27,6 +27,8 @@ from oslo_db import exception as db_exception
import oslo_messaging as messaging
from oslo_utils import strutils
from oslo_utils import uuidutils
+from oslo_versionedobjects import base as ovo_base
+from oslo_versionedobjects import fields
from ironic.common import boot_devices
from ironic.common import driver_factory
@@ -41,6 +43,7 @@ from ironic.db import api as dbapi
from ironic.drivers import base as drivers_base
from ironic.drivers.modules import fake
from ironic import objects
+from ironic.objects import base as obj_base
from ironic.tests import base as tests_base
from ironic.tests.conductor import utils as mgr_utils
from ironic.tests.db import base as tests_db_base
@@ -4354,3 +4357,105 @@ class ManagerCheckDeployingStatusTestCase(_ServiceSetUpMixin,
'provision_updated_at',
callback_method=conductor_utils.cleanup_after_timeout,
err_handler=manager.provisioning_error_handler)
+
+
+class TestIndirectionApiConductor(tests_db_base.DbTestCase):
+
+ def setUp(self):
+ super(TestIndirectionApiConductor, self).setUp()
+ self.conductor = manager.ConductorManager('test-host', 'test-topic')
+
+ def _test_object_action(self, is_classmethod, raise_exception,
+ return_object=False):
+ @obj_base.IronicObjectRegistry.register
+ class TestObject(obj_base.IronicObject):
+ context = self.context
+
+ def foo(self, context, raise_exception=False, return_object=False):
+ if raise_exception:
+ raise Exception('test')
+ elif return_object:
+ return obj
+ else:
+ return 'test'
+
+ @classmethod
+ def bar(cls, context, raise_exception=False, return_object=False):
+ if raise_exception:
+ raise Exception('test')
+ elif return_object:
+ return obj
+ else:
+ return 'test'
+
+ obj = TestObject(self.context)
+ if is_classmethod:
+ versions = ovo_base.obj_tree_get_versions(TestObject.obj_name())
+ result = self.conductor.object_class_action_versions(
+ self.context, TestObject.obj_name(), 'bar', versions,
+ tuple(), {'raise_exception': raise_exception,
+ 'return_object': return_object})
+ else:
+ updates, result = self.conductor.object_action(
+ self.context, obj, 'foo', tuple(),
+ {'raise_exception': raise_exception,
+ 'return_object': return_object})
+ if return_object:
+ self.assertEqual(obj, result)
+ else:
+ self.assertEqual('test', result)
+
+ def test_object_action(self):
+ self._test_object_action(False, False)
+
+ def test_object_action_on_raise(self):
+ self.assertRaises(messaging.ExpectedException,
+ self._test_object_action, False, True)
+
+ def test_object_action_on_object(self):
+ self._test_object_action(False, False, True)
+
+ def test_object_class_action(self):
+ self._test_object_action(True, False)
+
+ def test_object_class_action_on_raise(self):
+ self.assertRaises(messaging.ExpectedException,
+ self._test_object_action, True, True)
+
+ def test_object_class_action_on_object(self):
+ self._test_object_action(True, False, False)
+
+ def test_object_action_copies_object(self):
+ @obj_base.IronicObjectRegistry.register
+ class TestObject(obj_base.IronicObject):
+ fields = {'dict': fields.DictOfStringsField()}
+
+ def touch_dict(self, context):
+ self.dict['foo'] = 'bar'
+ self.obj_reset_changes()
+
+ obj = TestObject(self.context)
+ obj.dict = {}
+ obj.obj_reset_changes()
+ updates, result = self.conductor.object_action(
+ self.context, obj, 'touch_dict', tuple(), {})
+ # NOTE(danms): If conductor did not properly copy the object, then
+ # the new and reference copies of the nested dict object will be
+ # the same, and thus 'dict' will not be reported as changed
+ self.assertIn('dict', updates)
+ self.assertEqual({'foo': 'bar'}, updates['dict'])
+
+ def test_object_backport_versions(self):
+ fake_backported_obj = 'fake-backported-obj'
+ obj_name = 'fake-obj'
+ test_obj = mock.Mock()
+ test_obj.obj_name.return_value = obj_name
+ test_obj.obj_to_primitive.return_value = fake_backported_obj
+ fake_version_manifest = {obj_name: '1.0'}
+
+ result = self.conductor.object_backport_versions(
+ self.context, test_obj, fake_version_manifest)
+
+ self.assertEqual(result, fake_backported_obj)
+ test_obj.obj_to_primitive.assert_called_once_with(
+ target_version='1.0', version_manifest=fake_version_manifest)
diff --git a/ironic/tests/conductor/test_rpcapi.py b/ironic/tests/conductor/test_rpcapi.py
index dda8728a5..30380b8b3 100644
--- a/ironic/tests/conductor/test_rpcapi.py
+++ b/ironic/tests/conductor/test_rpcapi.py
@@ -22,6 +22,7 @@ import copy
import mock
from oslo_config import cfg
+from oslo_messaging import _utils as messaging_utils
from ironic.common import boot_devices
from ironic.common import exception
@@ -145,6 +146,10 @@ class RPCAPITestCase(base.DbTestCase):
self.fake_args = None
self.fake_kwargs = None
+ def _fake_can_send_version_method(version):
+ return messaging_utils.version_is_compatible(
+ rpcapi.RPC_API_VERSION, version)
+
def _fake_prepare_method(*args, **kwargs):
for kwd in kwargs:
self.assertEqual(kwargs[kwd], target[kwd])
@@ -156,16 +161,21 @@ class RPCAPITestCase(base.DbTestCase):
if expected_retval:
return expected_retval
- with mock.patch.object(rpcapi.client, "prepare") as mock_prepared:
- mock_prepared.side_effect = _fake_prepare_method
-
- with mock.patch.object(rpcapi.client, rpc_method) as mock_method:
- mock_method.side_effect = _fake_rpc_method
- retval = getattr(rpcapi, method)(self.context, **kwargs)
- self.assertEqual(retval, expected_retval)
- expected_args = [self.context, method, expected_msg]
- for arg, expected_arg in zip(self.fake_args, expected_args):
- self.assertEqual(arg, expected_arg)
+ with mock.patch.object(rpcapi.client,
+ "can_send_version") as mock_can_send_version:
+ mock_can_send_version.side_effect = _fake_can_send_version_method
+ with mock.patch.object(rpcapi.client, "prepare") as mock_prepared:
+ mock_prepared.side_effect = _fake_prepare_method
+
+ with mock.patch.object(rpcapi.client,
+ rpc_method) as mock_method:
+ mock_method.side_effect = _fake_rpc_method
+ retval = getattr(rpcapi, method)(self.context, **kwargs)
+ self.assertEqual(retval, expected_retval)
+ expected_args = [self.context, method, expected_msg]
+ for arg, expected_arg in zip(self.fake_args,
+ expected_args):
+ self.assertEqual(arg, expected_arg)
def test_update_node(self):
self._test_rpcapi('update_node',
@@ -306,3 +316,29 @@ class RPCAPITestCase(base.DbTestCase):
version='1.30',
node_id=self.fake_node['uuid'],
target_raid_config='config')
+
+ def test_object_action(self):
+ self._test_rpcapi('object_action',
+ 'call',
+ version='1.31',
+ objinst='fake-object',
+ objmethod='foo',
+ args=tuple(),
+ kwargs=dict())
+
+ def test_object_class_action_versions(self):
+ self._test_rpcapi('object_class_action_versions',
+ 'call',
+ version='1.31',
+ objname='fake-object',
+ objmethod='foo',
+ object_versions={'fake-object': '1.0'},
+ args=tuple(),
+ kwargs=dict())
+
+ def test_object_backport_versions(self):
+ self._test_rpcapi('object_backport_versions',
+ 'call',
+ version='1.31',
+ objinst='fake-object',
+ object_versions={'fake-object': '1.0'})
diff --git a/ironic/tests/objects/test_objects.py b/ironic/tests/objects/test_objects.py
index c001b906d..1cb67fe26 100644
--- a/ironic/tests/objects/test_objects.py
+++ b/ironic/tests/objects/test_objects.py
@@ -17,6 +17,7 @@ import datetime
import gettext
import iso8601
+import mock
from oslo_context import context
from oslo_versionedobjects import base as object_base
from oslo_versionedobjects import exception as object_exception
@@ -439,3 +440,48 @@ class TestObjectSerializer(test_base.TestCase):
self.assertEqual(1, len(thing2))
for item in thing2:
self.assertIsInstance(item, MyObj)
+
+ @mock.patch('ironic.objects.base.IronicObject.indirection_api')
+ def _test_deserialize_entity_newer(self, obj_version, backported_to,
+ mock_indirection_api,
+ my_version='1.6'):
+ ser = base.IronicObjectSerializer()
+ mock_indirection_api.object_backport_versions.return_value \
+ = 'backported'
+
+ @base.IronicObjectRegistry.register
+ class MyTestObj(MyObj):
+ VERSION = my_version
+
+ obj = MyTestObj(self.context)
+ obj.VERSION = obj_version
+ primitive = obj.obj_to_primitive()
+ result = ser.deserialize_entity(self.context, primitive)
+ if backported_to is None:
+ self.assertFalse(
+ mock_indirection_api.object_backport_versions.called)
+ else:
+ self.assertEqual('backported', result)
+ versions = object_base.obj_tree_get_versions('MyTestObj')
+ mock_indirection_api.object_backport_versions.assert_called_with(
+ self.context, primitive, versions)
+
+ def test_deserialize_entity_newer_version_backports(self):
+ "Test object with unsupported (newer) version"
+ self._test_deserialize_entity_newer('1.25', '1.6')
+
+ def test_deserialize_entity_same_revision_does_not_backport(self):
+ "Test object with supported revision"
+ self._test_deserialize_entity_newer('1.6', None)
+
+ def test_deserialize_entity_newer_revision_does_not_backport_zero(self):
+ "Test object with supported revision"
+ self._test_deserialize_entity_newer('1.6.0', None)
+
+ def test_deserialize_entity_newer_revision_does_not_backport(self):
+ "Test object with supported (newer) revision"
+ self._test_deserialize_entity_newer('1.6.1', None)
+
+ 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')