virt/ironic: Implement rescue and unrescue

This patch adds implementation of rescue and unrescue for ironic virt
driver.

Implements: blueprint ironic-rescue-mode

Change-Id: I7c20a0c5f566c3255350fd494d1a2cde84a99440
Signed-off-by: Taku Izumi <izumi.taku@jp.fujitsu.com>
Co-Authored-By: Hironori Shiina <shiina.hironori@jp.fujitsu.com>
This commit is contained in:
Taku Izumi 2016-12-20 23:09:37 +09:00 committed by Julia Kreger
parent 4acbf4fee3
commit a07b68ea92
8 changed files with 253 additions and 8 deletions

View File

@ -562,7 +562,7 @@ driver-impl-libvirt-lxc=missing
driver-impl-libvirt-xen=complete
driver-impl-vmware=complete
driver-impl-hyperv=complete
driver-impl-ironic=missing
driver-impl-ironic=complete
driver-impl-libvirt-vz-vm=complete
driver-impl-libvirt-vz-ct=complete
driver-impl-powervm=missing

View File

@ -2297,5 +2297,13 @@ class CertificateValidationNotYetAvailable(NovaException):
code = 409
class InstanceRescueFailure(NovaException):
msg_fmt = _("Failed to move instance to rescue mode: %(reason)s")
class InstanceUnRescueFailure(NovaException):
msg_fmt = _("Failed to unrescue instance: %(reason)s")
class IronicAPIVersionNotAvailable(NovaException):
msg_fmt = _('Ironic API version %(version)s is not available.')

View File

@ -81,12 +81,12 @@ class IronicClientWrapperTestCase(test.NoDBTestCase):
# nova.utils.get_ksa_adapter().get_endpoint()
self.get_ksa_adapter.assert_called_once_with(
'baremetal', ksa_auth=self.get_auth_plugin.return_value,
ksa_session='session', min_version=(1, 37),
ksa_session='session', min_version=(1, 38),
max_version=(1, ksa_disc.LATEST))
expected = {'session': 'session',
'max_retries': CONF.ironic.api_max_retries,
'retry_interval': CONF.ironic.api_retry_interval,
'os_ironic_api_version': ['1.37', '1.37'],
'os_ironic_api_version': ['1.38', '1.37'],
'ironic_url':
self.get_ksa_adapter.return_value.get_endpoint.return_value}
mock_ir_cli.assert_called_once_with(1, **expected)
@ -106,13 +106,13 @@ class IronicClientWrapperTestCase(test.NoDBTestCase):
# nova.utils.get_endpoint_data
self.get_ksa_adapter.assert_called_once_with(
'baremetal', ksa_auth=self.get_auth_plugin.return_value,
ksa_session='session', min_version=(1, 37),
ksa_session='session', min_version=(1, 38),
max_version=(1, ksa_disc.LATEST))
# When get_endpoint_data raises any ServiceNotFound, None is returned.
expected = {'session': 'session',
'max_retries': CONF.ironic.api_max_retries,
'retry_interval': CONF.ironic.api_retry_interval,
'os_ironic_api_version': ['1.37', '1.37'],
'os_ironic_api_version': ['1.38', '1.37'],
'ironic_url': None}
mock_ir_cli.assert_called_once_with(1, **expected)
@ -130,7 +130,7 @@ class IronicClientWrapperTestCase(test.NoDBTestCase):
expected = {'session': 'session',
'max_retries': CONF.ironic.api_max_retries,
'retry_interval': CONF.ironic.api_retry_interval,
'os_ironic_api_version': ['1.37', '1.37'],
'os_ironic_api_version': ['1.38', '1.37'],
'ironic_url': endpoint}
mock_ir_cli.assert_called_once_with(1, **expected)

View File

@ -2816,6 +2816,123 @@ class IronicDriverSyncTestCase(IronicDriverTestCase):
self.driver._pike_flavor_migration([uuids.node1])
mock_normalize.assert_not_called()
@mock.patch.object(loopingcall, 'FixedIntervalLoopingCall')
@mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
def test_rescue(self, mock_sps, mock_looping):
node = ironic_utils.get_test_node()
fake_looping_call = FakeLoopingCall()
mock_looping.return_value = fake_looping_call
instance = fake_instance.fake_instance_obj(self.ctx,
node=node.uuid)
self.driver.rescue(self.ctx, instance, None, None, 'xyz')
mock_sps.assert_called_once_with(node.uuid, 'rescue',
rescue_password='xyz')
@mock.patch.object(loopingcall, 'FixedIntervalLoopingCall')
@mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
def test_rescue_provision_state_fail(self, mock_sps, mock_looping):
node = ironic_utils.get_test_node()
fake_looping_call = FakeLoopingCall()
mock_looping.return_value = fake_looping_call
mock_sps.side_effect = ironic_exception.BadRequest()
instance = fake_instance.fake_instance_obj(self.ctx,
node=node.uuid)
self.assertRaises(exception.InstanceRescueFailure,
self.driver.rescue,
self.ctx, instance, None, None, 'xyz')
@mock.patch.object(ironic_driver.IronicDriver,
'_validate_instance_and_node')
@mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
def test_rescue_instance_not_found(self, mock_sps, fake_validate):
node = ironic_utils.get_test_node(driver='fake')
instance = fake_instance.fake_instance_obj(self.ctx, node=node.uuid)
fake_validate.side_effect = exception.InstanceNotFound(
instance_id='fake')
self.assertRaises(exception.InstanceRescueFailure,
self.driver.rescue,
self.ctx, instance, None, None, 'xyz')
@mock.patch.object(ironic_driver.IronicDriver,
'_validate_instance_and_node')
@mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
def test_rescue_rescue_fail(self, mock_sps, fake_validate):
node = ironic_utils.get_test_node(
provision_state=ironic_states.RESCUEFAIL,
last_error='rescue failed')
fake_validate.return_value = node
instance = fake_instance.fake_instance_obj(self.ctx,
node=node.uuid)
self.assertRaises(exception.InstanceRescueFailure,
self.driver.rescue,
self.ctx, instance, None, None, 'xyz')
@mock.patch.object(loopingcall, 'FixedIntervalLoopingCall')
@mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
def test_unrescue(self, mock_sps, mock_looping):
node = ironic_utils.get_test_node()
fake_looping_call = FakeLoopingCall()
mock_looping.return_value = fake_looping_call
instance = fake_instance.fake_instance_obj(self.ctx,
node=node.uuid)
self.driver.unrescue(instance, None)
mock_sps.assert_called_once_with(node.uuid, 'unrescue')
@mock.patch.object(loopingcall, 'FixedIntervalLoopingCall')
@mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
def test_unrescue_provision_state_fail(self, mock_sps, mock_looping):
node = ironic_utils.get_test_node()
fake_looping_call = FakeLoopingCall()
mock_looping.return_value = fake_looping_call
mock_sps.side_effect = ironic_exception.BadRequest()
instance = fake_instance.fake_instance_obj(self.ctx,
node=node.uuid)
self.assertRaises(exception.InstanceUnRescueFailure,
self.driver.unrescue,
instance, None)
@mock.patch.object(ironic_driver.IronicDriver,
'_validate_instance_and_node')
@mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
def test_unrescue_instance_not_found(self, mock_sps, fake_validate):
node = ironic_utils.get_test_node(driver='fake')
instance = fake_instance.fake_instance_obj(self.ctx, node=node.uuid)
fake_validate.side_effect = exception.InstanceNotFound(
instance_id='fake')
self.assertRaises(exception.InstanceUnRescueFailure,
self.driver.unrescue,
instance, None)
@mock.patch.object(ironic_driver.IronicDriver,
'_validate_instance_and_node')
@mock.patch.object(FAKE_CLIENT.node, 'set_provision_state')
def test_unrescue_unrescue_fail(self, mock_sps, fake_validate):
node = ironic_utils.get_test_node(
provision_state=ironic_states.UNRESCUEFAIL,
last_error='unrescue failed')
fake_validate.return_value = node
instance = fake_instance.fake_instance_obj(self.ctx,
node=node.uuid)
self.assertRaises(exception.InstanceUnRescueFailure,
self.driver.unrescue,
instance, None)
def test__can_send_version(self):
self.assertIsNone(
self.driver._can_send_version(

View File

@ -32,7 +32,7 @@ ironic = None
IRONIC_GROUP = nova.conf.ironic.ironic_group
# The API version required by the Ironic driver
IRONIC_API_VERSION = (1, 37)
IRONIC_API_VERSION = (1, 38)
# NOTE(TheJulia): This version should ALWAYS be the _last_ release
# supported version of the API version used by nova. If a feature
# needs 1.38 to be negotiated to operate properly, then the version

View File

@ -74,7 +74,10 @@ _POWER_STATE_MAP = {
_UNPROVISION_STATES = (ironic_states.ACTIVE, ironic_states.DEPLOYFAIL,
ironic_states.ERROR, ironic_states.DEPLOYWAIT,
ironic_states.DEPLOYING)
ironic_states.DEPLOYING, ironic_states.RESCUE,
ironic_states.RESCUING, ironic_states.RESCUEWAIT,
ironic_states.RESCUEFAIL, ironic_states.UNRESCUING,
ironic_states.UNRESCUEFAIL)
_NODE_FIELDS = ('uuid', 'power_state', 'target_power_state', 'provision_state',
'target_provision_state', 'last_error', 'maintenance',
@ -1976,3 +1979,87 @@ class IronicDriver(virt_driver.ComputeDriver):
version.StrictVersion(max_version)):
raise exception.IronicAPIVersionNotAvailable(
version=max_version)
def rescue(self, context, instance, network_info, image_meta,
rescue_password):
"""Rescue the specified instance.
:param nova.context.RequestContext context:
The context for the rescue.
:param nova.objects.instance.Instance instance:
The instance being rescued.
:param nova.network.model.NetworkInfo network_info:
Necessary network information for the rescue. Ignored by this
driver.
:param nova.objects.ImageMeta image_meta:
The metadata of the image of the instance. Ignored by this driver.
:param rescue_password: new root password to set for rescue.
:raise InstanceRescueFailure if rescue fails.
"""
LOG.debug('Rescue called for instance', instance=instance)
node_uuid = instance.node
def _wait_for_rescue():
try:
node = self._validate_instance_and_node(instance)
except exception.InstanceNotFound as e:
raise exception.InstanceRescueFailure(reason=six.text_type(e))
if node.provision_state == ironic_states.RESCUE:
raise loopingcall.LoopingCallDone()
if node.provision_state == ironic_states.RESCUEFAIL:
raise exception.InstanceRescueFailure(
reason=node.last_error)
try:
self._can_send_version(min_version='1.38')
self.ironicclient.call("node.set_provision_state",
node_uuid, ironic_states.RESCUE,
rescue_password=rescue_password)
except Exception as e:
raise exception.InstanceRescueFailure(reason=six.text_type(e))
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_rescue)
timer.start(interval=CONF.ironic.api_retry_interval).wait()
LOG.info('Successfully rescued Ironic node %(node)s',
{'node': node_uuid}, instance=instance)
def unrescue(self, instance, network_info):
"""Unrescue the specified instance.
:param instance: nova.objects.instance.Instance
:param nova.network.model.NetworkInfo network_info:
Necessary network information for the unrescue. Ignored by this
driver.
"""
LOG.debug('Unrescue called for instance', instance=instance)
node_uuid = instance.node
def _wait_for_unrescue():
try:
node = self._validate_instance_and_node(instance)
except exception.InstanceNotFound as e:
raise exception.InstanceUnRescueFailure(
reason=six.text_type(e))
if node.provision_state == ironic_states.ACTIVE:
raise loopingcall.LoopingCallDone()
if node.provision_state == ironic_states.UNRESCUEFAIL:
raise exception.InstanceUnRescueFailure(
reason=node.last_error)
try:
self._can_send_version(min_version='1.38')
self.ironicclient.call("node.set_provision_state",
node_uuid, ironic_states.UNRESCUE)
except Exception as e:
raise exception.InstanceUnRescueFailure(reason=six.text_type(e))
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_unrescue)
timer.start(interval=CONF.ironic.api_retry_interval).wait()
LOG.info('Successfully unrescued Ironic node %(node)s',
{'node': node_uuid}, instance=instance)

View File

@ -132,6 +132,32 @@ INSPECTFAIL = 'inspect failed'
""" Node inspection failed. """
RESCUE = 'rescue'
""" Node is in rescue mode.
This is also used as a "verb" when changing the node's provision_state via the
REST API"""
RESCUEFAIL = 'rescue failed'
""" Node rescue failed. """
RESCUEWAIT = 'rescue wait'
""" Node is waiting for rescue callback. """
RESCUING = 'rescuing'
""" Node is waiting to be rescued. """
UNRESCUE = 'unrescue'
""" Node is to be unrescued.
This is not used as a state, but rather as a "verb" when changing the node's
provision_state via the REST API.
"""
UNRESCUEFAIL = 'unrescue failed'
""" Node unrescue failed. """
UNRESCUING = "unrescuing"
""" Node is unrescuing. """
##############
# Power states
##############

View File

@ -0,0 +1,7 @@
---
features:
- |
Supports instance rescue and unrescue with ironic virt driver. This feature
requires an ironic service supporting API version 1.38 or later, which is
present in ironic releases >= 10.1. It also requires python-ironicclient >=
2.3.0.