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:
parent
4acbf4fee3
commit
a07b68ea92
@ -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
|
||||
|
@ -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.')
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
##############
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user