summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShivanand Tendulker <stendulker@gmail.com>2017-10-03 10:45:10 -0400
committerShivanand Tendulker <stendulker@gmail.com>2017-12-21 03:01:48 -0500
commit433b1fd197480d95f79539e61f1a9222d349e102 (patch)
tree3f7ceaabb7a843916de36a06321e75a95bc4760b
parent2924c3efb6f6003dd49951dd78bdb44bae2ee367 (diff)
downloadironic-433b1fd197480d95f79539e61f1a9222d349e102.tar.gz
Adds rescue_interface to base driver class
This commit adds `rescue` interface to `BaseDriver` and implements it for `fake-hardware` hardware type. It adds configuration parameters '[DEFAULT]/enabled_rescue_interfaces' and '[DEFAULT]/default_rescue_interface'. The default value of configuration parameter '[DEFAULT]/enabled_rescue_interfaces' is `no-rescue`. It adds new rescue states and a new 'rescue' field to the Node object. It adds objects.node.Node._convert_to_version(). The method handles converting the new rescue_interface field between different versions of the Node. Partial-bug: #1526449 Co-Authored-By: Jay Faulkner <jay@jvf.cc> Co-Authored-By: Josh Gachnang <josh@pcsforeducation.com> Co-Authored-By: Jesse J. Cook <jesse.j.cook@member.fsf.org> Co-Authored-By: Mario Villaplana <mario.villaplana@gmail.com> Co-Authored-By: Aparna <aparnavtce@gmail.com> Co-Authored-By: Shivanand Tendulker <stendulker@gmail.com> Change-Id: I1534247bf207a20a7a58534988192aef392eaff2
-rw-r--r--etc/ironic/ironic.conf.sample26
-rw-r--r--ironic/common/release_mappings.py2
-rw-r--r--ironic/common/states.py22
-rw-r--r--ironic/conductor/manager.py5
-rw-r--r--ironic/conf/default.py17
-rw-r--r--ironic/drivers/base.py25
-rw-r--r--ironic/drivers/fake_hardware.py5
-rw-r--r--ironic/drivers/hardware_type.py5
-rw-r--r--ironic/drivers/modules/fake.py16
-rw-r--r--ironic/objects/node.py45
-rw-r--r--ironic/tests/unit/api/controllers/v1/test_driver.py7
-rw-r--r--ironic/tests/unit/api/controllers/v1/test_node.py31
-rw-r--r--ironic/tests/unit/common/test_driver_factory.py30
-rw-r--r--ironic/tests/unit/conductor/mgr_utils.py1
-rw-r--r--ironic/tests/unit/drivers/test_base.py3
-rw-r--r--ironic/tests/unit/drivers/test_fake.py1
-rw-r--r--ironic/tests/unit/objects/test_node.py70
-rw-r--r--ironic/tests/unit/objects/test_objects.py2
-rw-r--r--setup.cfg1
19 files changed, 297 insertions, 17 deletions
diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample
index 0d54a145c..afcc3416b 100644
--- a/etc/ironic/ironic.conf.sample
+++ b/etc/ironic/ironic.conf.sample
@@ -227,6 +227,32 @@
# "ironic.hardware.interfaces.raid" entrypoint. (string value)
#default_raid_interface = <None>
+# Specify the list of rescue interfaces to load during service
+# initialization. Missing rescue interfaces, or rescue
+# interfaces which fail to initialize, will prevent the
+# ironic-conductor service from starting. At least one rescue
+# interface that is supported by each enabled hardware type
+# must be enabled here, or the ironic-conductor service will
+# not start. Must not be an empty list. The default value is a
+# recommended set of production-oriented rescue interfaces. A
+# complete list of rescue interfaces present on your system
+# may be found by enumerating the
+# "ironic.hardware.interfaces.rescue" entrypoint. When setting
+# this value, please make sure that every enabled hardware
+# type will have the same set of enabled rescue interfaces on
+# every ironic-conductor service. This option is part of
+# rescue feature work, which is not currently exposed to
+# users. (list value)
+#enabled_rescue_interfaces = no-rescue
+
+# Default rescue interface to be used for nodes that do not
+# have rescue_interface field set. A complete list of rescue
+# interfaces present on your system may be found by
+# enumerating the "ironic.hardware.interfaces.rescue"
+# entrypoint. This option is part of rescue feature work,
+# which is not currently exposed to users. (string value)
+#default_rescue_interface = <None>
+
# Specify the list of storage interfaces to load during
# service initialization. Missing storage interfaces, or
# storage interfaces which fail to initialize, will prevent
diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py
index 031c6a8fa..55c6856fb 100644
--- a/ironic/common/release_mappings.py
+++ b/ironic/common/release_mappings.py
@@ -111,7 +111,7 @@ RELEASE_MAPPING = {
'api': '1.36',
'rpc': '1.42',
'objects': {
- 'Node': ['1.21'],
+ 'Node': ['1.22'],
'Conductor': ['1.2'],
'Chassis': ['1.3'],
'Port': ['1.7'],
diff --git a/ironic/common/states.py b/ironic/common/states.py
index e270e7831..ddd767db0 100644
--- a/ironic/common/states.py
+++ b/ironic/common/states.py
@@ -185,6 +185,28 @@ potentially due to invalid or incompatible information being defined for the
node.
"""
+RESCUE = 'rescue'
+""" Node is in rescue mode. """
+
+RESCUEFAIL = 'rescue failed'
+""" Node rescue failed. """
+
+RESCUEWAIT = 'rescue wait'
+""" Node is waiting on an external callback.
+
+This will be the node `provision_state` while the node is waiting for
+the driver to finish rescuing the node.
+"""
+
+RESCUING = 'rescuing'
+""" Node is in process of being rescued. """
+
+UNRESCUEFAIL = 'unrescue failed'
+""" Node unrescue failed. """
+
+UNRESCUING = 'unrescuing'
+""" Node is being restored from rescue mode (to active state). """
+
UPDATE_ALLOWED_STATES = (DEPLOYFAIL, INSPECTING, INSPECTFAIL, CLEANFAIL, ERROR,
VERIFYING, ADOPTFAIL)
"""Transitional states in which we allow updating a node."""
diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py
index cae632229..0f8c37f5c 100644
--- a/ironic/conductor/manager.py
+++ b/ironic/conductor/manager.py
@@ -1555,6 +1555,11 @@ class ConductorManager(base_manager.BaseConductorManager):
task.node.instance_info)
task.node.driver_internal_info['is_whole_disk_image'] = iwdi
for iface_name in task.driver.non_vendor_interfaces:
+ # TODO(stendulker): Remove this check in 'rescue' API patch
+ # Do not have to return the validation result for 'rescue'
+ # interface.
+ if iface_name == 'rescue':
+ continue
iface = getattr(task.driver, iface_name, None)
result = reason = None
if iface:
diff --git a/ironic/conf/default.py b/ironic/conf/default.py
index 58cd7753a..573622028 100644
--- a/ironic/conf/default.py
+++ b/ironic/conf/default.py
@@ -52,6 +52,18 @@ _DEFAULT_IFACE_HELP = _('Default {0} interface to be used for nodes that '
'be found by enumerating the '
'"ironic.hardware.interfaces.{0}" entrypoint.')
+# TODO(stendulker) Remove this in rescue API patch.
+_ENABLED_IFACE_HELP_FOR_RESCUE = (_ENABLED_IFACE_HELP +
+ _(' This option is part of rescue feature '
+ 'work, which is not currently exposed to '
+ 'users.'))
+
+# TODO(stendulker) Remove this in rescue API patch.
+_DEFAULT_IFACE_HELP_FOR_RESCUE = (_DEFAULT_IFACE_HELP +
+ _(' This option is part of rescue feature '
+ 'work, which is not currently exposed to '
+ 'users.'))
+
api_opts = [
cfg.StrOpt(
'auth_strategy',
@@ -137,6 +149,11 @@ driver_opts = [
help=_ENABLED_IFACE_HELP.format('raid')),
cfg.StrOpt('default_raid_interface',
help=_DEFAULT_IFACE_HELP.format('raid')),
+ cfg.ListOpt('enabled_rescue_interfaces',
+ default=['no-rescue'],
+ help=_ENABLED_IFACE_HELP_FOR_RESCUE.format('rescue')),
+ cfg.StrOpt('default_rescue_interface',
+ help=_DEFAULT_IFACE_HELP_FOR_RESCUE.format('rescue')),
cfg.ListOpt('enabled_storage_interfaces',
default=['cinder', 'noop'],
help=_ENABLED_IFACE_HELP.format('storage')),
diff --git a/ironic/drivers/base.py b/ironic/drivers/base.py
index 6231831b9..1f1e4d2b9 100644
--- a/ironic/drivers/base.py
+++ b/ironic/drivers/base.py
@@ -81,9 +81,6 @@ class BaseDriver(object):
"""
rescue = None
- # NOTE(deva): hide rescue from the interface list in Icehouse
- # because the API for this has not been created yet.
- # standard_interfaces.append('rescue')
"""`Standard` attribute for accessing rescue features.
A reference to an instance of :class:RescueInterface.
@@ -170,7 +167,9 @@ class BareDriver(BaseDriver):
A reference to an instance of :class:StorageInterface.
"""
- standard_interfaces = BaseDriver.standard_interfaces + ('storage',)
+
+ standard_interfaces = (BaseDriver.standard_interfaces + ('rescue',
+ 'storage',))
ALL_INTERFACES = set(BareDriver().all_interfaces)
@@ -562,6 +561,10 @@ class RescueInterface(BaseInterface):
"""Boot the task's node into a rescue environment.
:param task: a TaskManager instance containing the node to act on.
+ :raises: InstanceRescueFailure if node validation or rescue operation
+ fails.
+ :returns: states.RESCUEWAIT if rescue is in progress asynchronously
+ or states.RESCUE if it is complete.
"""
@abc.abstractmethod
@@ -569,8 +572,22 @@ class RescueInterface(BaseInterface):
"""Tear down the rescue environment, and return to normal.
:param task: a TaskManager instance containing the node to act on.
+ :raises: InstanceUnrescueFailure if node validation or unrescue
+ operation fails.
+ :returns: states.ACTIVE if it is successful.
"""
+ def clean_up(self, task):
+ """Clean up the rescue environment for the task's node.
+
+ This is particularly useful for nodes where rescuing is asynchronous
+ and a timeout occurs.
+
+ :param task: a TaskManager instance containing the node to act on.
+ :returns: None
+ """
+ pass
+
# Representation of a single vendor method metadata
VendorMetadata = collections.namedtuple('VendorMetadata', ['method',
diff --git a/ironic/drivers/fake_hardware.py b/ironic/drivers/fake_hardware.py
index f6320de0c..6cf6f0d56 100644
--- a/ironic/drivers/fake_hardware.py
+++ b/ironic/drivers/fake_hardware.py
@@ -67,6 +67,11 @@ class FakeHardware(hardware_type.AbstractHardwareType):
return [fake.FakeRAID]
@property
+ def supported_rescue_interfaces(self):
+ """List of classes of supported rescue interfaces."""
+ return [fake.FakeRescue]
+
+ @property
def supported_storage_interfaces(self):
"""List of classes of supported storage interfaces."""
return [fake.FakeStorage]
diff --git a/ironic/drivers/hardware_type.py b/ironic/drivers/hardware_type.py
index 2d0713a58..301101953 100644
--- a/ironic/drivers/hardware_type.py
+++ b/ironic/drivers/hardware_type.py
@@ -84,6 +84,11 @@ class AbstractHardwareType(object):
return [noop.NoRAID]
@property
+ def supported_rescue_interfaces(self):
+ """List of supported rescue interfaces."""
+ return [noop.NoRescue]
+
+ @property
def supported_storage_interfaces(self):
"""List of supported storage interfaces."""
return [noop_storage.NoopStorage]
diff --git a/ironic/drivers/modules/fake.py b/ironic/drivers/modules/fake.py
index 4ab7772ef..f60c55258 100644
--- a/ironic/drivers/modules/fake.py
+++ b/ironic/drivers/modules/fake.py
@@ -260,3 +260,19 @@ class FakeStorage(base.StorageInterface):
def should_write_image(self, task):
return True
+
+
+class FakeRescue(base.RescueInterface):
+ """Example implementation of a simple rescue interface."""
+
+ def get_properties(self):
+ return {}
+
+ def validate(self, task):
+ pass
+
+ def rescue(self, task):
+ return states.RESCUE
+
+ def unrescue(self, task):
+ return states.ACTIVE
diff --git a/ironic/objects/node.py b/ironic/objects/node.py
index cffd53d9b..b53efcbbe 100644
--- a/ironic/objects/node.py
+++ b/ironic/objects/node.py
@@ -15,6 +15,7 @@
from oslo_utils import strutils
from oslo_utils import uuidutils
+from oslo_utils import versionutils
from oslo_versionedobjects import base as object_base
from ironic.common import exception
@@ -55,7 +56,8 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
# power_interface, raid_interface, vendor_interface
# Version 1.20: Type of network_interface changed to just nullable string
# Version 1.21: Add storage_interface field
- VERSION = '1.21'
+ # Version 1.22: Add rescue_interface field
+ VERSION = '1.22'
dbapi = db_api.get_instance()
@@ -123,6 +125,7 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
'network_interface': object_fields.StringField(nullable=True),
'power_interface': object_fields.StringField(nullable=True),
'raid_interface': object_fields.StringField(nullable=True),
+ 'rescue_interface': object_fields.StringField(nullable=True),
'storage_interface': object_fields.StringField(nullable=True),
'vendor_interface': object_fields.StringField(nullable=True),
}
@@ -415,6 +418,41 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
node = cls._from_db_object(context, cls(), db_node)
return node
+ def _convert_to_version(self, target_version,
+ remove_unavailable_fields=True):
+ """Convert to the target version.
+
+ Convert the object to the target version. The target version may be
+ the same, older, or newer than the version of the object. This is
+ used for DB interactions as well as for serialization/deserialization.
+
+ Version 1.22: rescue_interface field was added. Its default value is
+ None. For versions prior to this, it should be set to None (or
+ removed).
+
+ :param target_version: the desired version of the object
+ :param remove_unavailable_fields: True to remove fields that are
+ unavailable in the target version; set this to True when
+ (de)serializing. False to set the unavailable fields to appropriate
+ values; set this to False for DB interactions.
+ """
+ target_version = versionutils.convert_version_to_tuple(target_version)
+ # Convert the rescue_interface field.
+ rescue_iface_is_set = self.obj_attr_is_set('rescue_interface')
+ if target_version >= (1, 22):
+ # Target version supports rescue_interface.
+ if not rescue_iface_is_set:
+ # Set it to its default value if it is not set.
+ self.rescue_interface = None
+ elif rescue_iface_is_set:
+ # Target version does not support rescue_interface, and it is set.
+ if remove_unavailable_fields:
+ # (De)serialising: remove unavailable fields.
+ delattr(self, 'rescue_interface')
+ elif self.rescue_interface is not None:
+ # DB: set unavailable field to the default of None.
+ self.rescue_interface = None
+
@base.IronicObjectRegistry.register
class NodePayload(notification.NotificationPayloadBase):
@@ -461,6 +499,11 @@ class NodePayload(notification.NotificationPayloadBase):
'uuid': ('node', 'uuid')
}
+ # TODO(stendulker): At a later point in time, once rescue_interface
+ # is able to be leveraged, we need to add the rescue_interface
+ # field to payload and increment the object versions for all objects
+ # that inherit the NodePayload object.
+
# Version 1.0: Initial version, based off of Node version 1.18.
# Version 1.1: Type of network_interface changed to just nullable string
# similar to version 1.20 of Node.
diff --git a/ironic/tests/unit/api/controllers/v1/test_driver.py b/ironic/tests/unit/api/controllers/v1/test_driver.py
index 61795ece3..349980676 100644
--- a/ironic/tests/unit/api/controllers/v1/test_driver.py
+++ b/ironic/tests/unit/api/controllers/v1/test_driver.py
@@ -208,7 +208,12 @@ class TestListDrivers(base.BaseApiTest):
if use_dynamic:
for iface in driver_base.ALL_INTERFACES:
- if storage_if or iface != 'storage':
+ # TODO(stendulker): Remove this check in 'rescue' API
+ # patch.
+ if iface == 'rescue':
+ self.assertNotIn('default_rescue_interface', data)
+ self.assertNotIn('enabled_rescue_interfaces', data)
+ elif storage_if or iface != 'storage':
self.assertIn('default_%s_interface' % iface, data)
self.assertIn('enabled_%s_interfaces' % iface, data)
self.assertIsNotNone(data['default_deploy_interface'])
diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py
index be6faf14e..eab547299 100644
--- a/ironic/tests/unit/api/controllers/v1/test_node.py
+++ b/ironic/tests/unit/api/controllers/v1/test_node.py
@@ -2174,17 +2174,36 @@ class TestPost(test_api_base.BaseApiTest):
self.assertEqual('neutron', result['network_interface'])
def test_create_node_specify_interfaces(self):
- headers = {api_base.Version.string: '1.31'}
- for field in api_utils.V31_FIELDS:
- cfg.CONF.set_override('enabled_%ss' % field, ['fake'])
- for field in api_utils.V31_FIELDS:
+ headers = {api_base.Version.string: '1.33'}
+ all_interface_fields = api_utils.V31_FIELDS + ['network_interface',
+ 'rescue_interface',
+ 'storage_interface']
+ for field in all_interface_fields:
+ if field == 'network_interface':
+ cfg.CONF.set_override('enabled_%ss' % field, ['flat'])
+ elif field == 'storage_interface':
+ cfg.CONF.set_override('enabled_%ss' % field, ['noop'])
+ else:
+ cfg.CONF.set_override('enabled_%ss' % field, ['fake'])
+
+ for field in all_interface_fields:
+ expected = 'fake'
+ if field == 'network_interface':
+ expected = 'flat'
+ elif field == 'storage_interface':
+ expected = 'noop'
+ elif field == 'rescue_interface':
+ # TODO(stendulker): Enable testing of rescue interface
+ # in its API patch.
+ continue
+
node = {
'uuid': uuidutils.generate_uuid(),
- field: 'fake',
+ field: expected,
'driver': 'fake-hardware'
}
result = self._test_create_node(headers=headers, **node)
- self.assertEqual('fake', result[field])
+ self.assertEqual(expected, result[field])
def test_create_node_specify_interfaces_bad_version(self):
headers = {api_base.Version.string: '1.30'}
diff --git a/ironic/tests/unit/common/test_driver_factory.py b/ironic/tests/unit/common/test_driver_factory.py
index daaf7193f..dc0a01144 100644
--- a/ironic/tests/unit/common/test_driver_factory.py
+++ b/ironic/tests/unit/common/test_driver_factory.py
@@ -97,7 +97,10 @@ class DriverLoadTestCase(db_base.DbTestCase):
with task_manager.acquire(self.context, node.id) as task:
for iface in drivers_base.ALL_INTERFACES:
impl = getattr(task.driver, iface)
- self.assertIsNotNone(impl)
+ if iface == 'rescue':
+ self.assertIsNone(impl)
+ else:
+ self.assertIsNotNone(impl)
@mock.patch.object(driver_factory, '_attach_interfaces_to_driver',
autospec=True)
@@ -581,6 +584,11 @@ class TestFakeHardware(hardware_type.AbstractHardwareType):
return [fake.FakeRAID]
@property
+ def supported_rescue_interfaces(self):
+ """List of supported rescue interfaces."""
+ return [fake.FakeRescue]
+
+ @property
def supported_vendor_interfaces(self):
"""List of supported rescue interfaces."""
return [fake.FakeVendorB, fake.FakeVendorA]
@@ -732,6 +740,25 @@ class HardwareTypeLoadTestCase(db_base.DbTestCase):
driver_factory.check_and_update_node_interfaces,
node)
+ def test_none_rescue_interface(self):
+ node = obj_utils.get_test_node(self.context, driver='fake')
+ self.assertTrue(driver_factory.check_and_update_node_interfaces(node))
+ self.assertIsNone(node.rescue_interface)
+
+ def test_no_rescue_interface_default_from_conf(self):
+ self.config(enabled_rescue_interfaces=['fake'])
+ self.config(default_rescue_interface='fake')
+ node = obj_utils.get_test_node(self.context, driver='fake-hardware')
+ self.assertTrue(driver_factory.check_and_update_node_interfaces(node))
+ self.assertEqual('fake', node.rescue_interface)
+
+ def test_invalid_rescue_interface(self):
+ node = obj_utils.get_test_node(self.context, driver='fake-hardware',
+ rescue_interface='scoop')
+ self.assertRaises(exception.InterfaceNotFoundInEntrypoint,
+ driver_factory.check_and_update_node_interfaces,
+ node)
+
def test_no_raid_interface_no_default(self):
# NOTE(rloo): It doesn't seem possible to not have a default interface
# for storage, so we'll test this case with raid.
@@ -753,6 +780,7 @@ class HardwareTypeLoadTestCase(db_base.DbTestCase):
'network': set(['noop']),
'power': set(['fake']),
'raid': set(['fake']),
+ 'rescue': set(['fake']),
'storage': set([]),
'vendor': set(['fake'])
}
diff --git a/ironic/tests/unit/conductor/mgr_utils.py b/ironic/tests/unit/conductor/mgr_utils.py
index 829c644de..a16659469 100644
--- a/ironic/tests/unit/conductor/mgr_utils.py
+++ b/ironic/tests/unit/conductor/mgr_utils.py
@@ -175,6 +175,7 @@ class ServiceSetUpMixin(object):
self.config(enabled_management_interfaces=['fake'])
self.config(enabled_power_interfaces=['fake'])
self.config(enabled_raid_interfaces=['fake', 'no-raid'])
+ self.config(enabled_rescue_interfaces=['fake', 'no-rescue'])
self.config(enabled_vendor_interfaces=['fake', 'no-vendor'])
self.service = manager.ConductorManager(self.hostname, 'test-topic')
diff --git a/ironic/tests/unit/drivers/test_base.py b/ironic/tests/unit/drivers/test_base.py
index 18858dc30..52642ec15 100644
--- a/ironic/tests/unit/drivers/test_base.py
+++ b/ironic/tests/unit/drivers/test_base.py
@@ -447,6 +447,7 @@ class TestBareDriver(base.TestCase):
self.assertEqual(('deploy', 'power', 'network'),
driver_base.BareDriver.core_interfaces)
self.assertEqual(
- ('boot', 'console', 'inspect', 'management', 'raid', 'storage'),
+ ('boot', 'console', 'inspect', 'management', 'raid',
+ 'rescue', 'storage'),
driver_base.BareDriver.standard_interfaces
)
diff --git a/ironic/tests/unit/drivers/test_fake.py b/ironic/tests/unit/drivers/test_fake.py
index c0a849f5a..270916429 100644
--- a/ironic/tests/unit/drivers/test_fake.py
+++ b/ironic/tests/unit/drivers/test_fake.py
@@ -49,7 +49,6 @@ class FakeDriverTestCase(db_base.DbTestCase):
self.assertIsInstance(self.driver.vendor, driver_base.VendorInterface)
self.assertIsInstance(self.driver.console,
driver_base.ConsoleInterface)
- self.assertIsNone(self.driver.rescue)
def test_get_properties(self):
expected = ['A1', 'A2', 'B1', 'B2']
diff --git a/ironic/tests/unit/objects/test_node.py b/ironic/tests/unit/objects/test_node.py
index 9dcfc2f18..a74bcdb3e 100644
--- a/ironic/tests/unit/objects/test_node.py
+++ b/ironic/tests/unit/objects/test_node.py
@@ -260,3 +260,73 @@ class TestNodeObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
def test_payload_schemas(self):
self._check_payload_schemas(objects.node, objects.Node.fields)
+
+
+class TestConvertToVersion(db_base.DbTestCase):
+
+ def setUp(self):
+ super(TestConvertToVersion, self).setUp()
+ self.ctxt = context.get_admin_context()
+ self.fake_node = db_utils.get_test_node(driver='fake-hardware')
+
+ def test_rescue_supported_missing(self):
+ # rescue_interface not set, should be set to default.
+ node = objects.Node(self.context, **self.fake_node)
+ delattr(node, 'rescue_interface')
+ node.obj_reset_changes()
+
+ node._convert_to_version("1.22")
+
+ self.assertIsNone(node.rescue_interface)
+ self.assertEqual({'rescue_interface': None},
+ node.obj_get_changes())
+
+ def test_rescue_supported_set(self):
+ # rescue_interface set, no change required.
+ node = objects.Node(self.context, **self.fake_node)
+
+ node.rescue_interface = 'fake'
+ node.obj_reset_changes()
+ node._convert_to_version("1.22")
+ self.assertEqual('fake', node.rescue_interface)
+ self.assertEqual({}, node.obj_get_changes())
+
+ def test_rescue_unsupported_missing(self):
+ # rescue_interface not set, no change required.
+ node = objects.Node(self.context, **self.fake_node)
+
+ delattr(node, 'rescue_interface')
+ node.obj_reset_changes()
+ node._convert_to_version("1.21")
+ self.assertNotIn('rescue_interface', node)
+ self.assertEqual({}, node.obj_get_changes())
+
+ def test_rescue_unsupported_set_remove(self):
+ # rescue_interface set, should be removed.
+ node = objects.Node(self.context, **self.fake_node)
+
+ node.rescue_interface = 'fake'
+ node.obj_reset_changes()
+ node._convert_to_version("1.21")
+ self.assertNotIn('rescue_interface', node)
+ self.assertEqual({}, node.obj_get_changes())
+
+ def test_rescue_unsupported_set_no_remove_non_default(self):
+ # rescue_interface set, should be set to default.
+ node = objects.Node(self.context, **self.fake_node)
+
+ node.rescue_interface = 'fake'
+ node.obj_reset_changes()
+ node._convert_to_version("1.21", False)
+ self.assertIsNone(node.rescue_interface)
+ self.assertEqual({'rescue_interface': None}, node.obj_get_changes())
+
+ def test_rescue_unsupported_set_no_remove_default(self):
+ # rescue_interface set, no change required.
+ node = objects.Node(self.context, **self.fake_node)
+
+ node.rescue_interface = None
+ node.obj_reset_changes()
+ node._convert_to_version("1.21", False)
+ self.assertIsNone(node.rescue_interface)
+ self.assertEqual({}, node.obj_get_changes())
diff --git a/ironic/tests/unit/objects/test_objects.py b/ironic/tests/unit/objects/test_objects.py
index 7a25b9cd3..c02659f4e 100644
--- a/ironic/tests/unit/objects/test_objects.py
+++ b/ironic/tests/unit/objects/test_objects.py
@@ -684,7 +684,7 @@ class TestObject(_LocalTest, _TestObject):
# version bump. It is an MD5 hash of the object fields and remotable methods.
# The fingerprint values should only be changed if there is a version bump.
expected_object_fingerprints = {
- 'Node': '1.21-52674c214141cf3e09f8688bfed54577',
+ 'Node': '1.22-f2c453dd0b42aec8d4833a69a9ac79f3',
'MyObj': '1.5-9459d30d6954bffc7a9afd347a807ca6',
'Chassis': '1.3-d656e039fd8ae9f34efc232ab3980905',
'Port': '1.7-898a47921f4a1f53fcdddd4eeb179e0b',
diff --git a/setup.cfg b/setup.cfg
index 9ed647b55..02f88f9cb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -150,6 +150,7 @@ ironic.hardware.interfaces.raid =
no-raid = ironic.drivers.modules.noop:NoRAID
ironic.hardware.interfaces.rescue =
+ fake = ironic.drivers.modules.fake:FakeRescue
no-rescue = ironic.drivers.modules.noop:NoRescue
ironic.hardware.interfaces.storage =