Merge "Adds rescue_interface to base driver class"

This commit is contained in:
Zuul 2018-01-09 06:11:20 +00:00 committed by Gerrit Code Review
commit b5563b4384
19 changed files with 297 additions and 17 deletions

View File

@ -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

View File

@ -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'],

View File

@ -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."""

View File

@ -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:

View File

@ -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')),

View File

@ -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',

View File

@ -66,6 +66,11 @@ class FakeHardware(hardware_type.AbstractHardwareType):
"""List of classes of supported raid interfaces."""
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."""

View File

@ -83,6 +83,11 @@ class AbstractHardwareType(object):
"""List of supported raid interfaces."""
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."""

View File

@ -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

View File

@ -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.

View File

@ -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'])

View File

@ -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'}

View File

@ -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)
@ -580,6 +583,11 @@ class TestFakeHardware(hardware_type.AbstractHardwareType):
"""List of supported raid interfaces."""
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."""
@ -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'])
}

View File

@ -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')

View File

@ -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
)

View File

@ -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']

View File

@ -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())

View File

@ -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',

View File

@ -153,6 +153,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 =