Agent rescue implementation

This implements agent based rescue interface.

Partial-Bug: #1526449

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: I9b4c1278dc5fab7888fbfe586c15e31ed3958978
This commit is contained in:
Shivanand Tendulker 2017-11-16 13:23:05 -05:00 committed by Ruby Loo
parent f5654bfd00
commit 4624c572e2
22 changed files with 1128 additions and 206 deletions

View File

@ -4029,11 +4029,11 @@
# 6 - <No description provided> # 6 - <No description provided>
#ip_version = 4 #ip_version = 4
# Download deploy images directly from swift using temporary # Download deploy and rescue images directly from swift using
# URLs. If set to false (default), images are downloaded to # temporary URLs. If set to false (default), images are
# the ironic-conductor node and served over its local HTTP # downloaded to the ironic-conductor node and served over its
# server. Applicable only when 'ipxe_enabled' option is set to # local HTTP server. Applicable only when 'ipxe_enabled'
# true. (boolean value) # option is set to true. (boolean value)
#ipxe_use_swift = false #ipxe_use_swift = false

View File

@ -39,6 +39,10 @@ DHCP_BOOTFILE_NAME = '67' # rfc2132
DHCP_TFTP_SERVER_ADDRESS = '150' # rfc5859 DHCP_TFTP_SERVER_ADDRESS = '150' # rfc5859
DHCP_IPXE_ENCAP_OPTS = '175' # Tentatively Assigned DHCP_IPXE_ENCAP_OPTS = '175' # Tentatively Assigned
DHCP_TFTP_PATH_PREFIX = '210' # rfc5071 DHCP_TFTP_PATH_PREFIX = '210' # rfc5071
DEPLOY_KERNEL_RAMDISK_LABELS = ['deploy_kernel', 'deploy_ramdisk']
RESCUE_KERNEL_RAMDISK_LABELS = ['rescue_kernel', 'rescue_ramdisk']
KERNEL_RAMDISK_LABELS = {'deploy': DEPLOY_KERNEL_RAMDISK_LABELS,
'rescue': RESCUE_KERNEL_RAMDISK_LABELS}
def get_root_dir(): def get_root_dir():
@ -158,14 +162,25 @@ def _get_pxe_ip_address_path(ip_address, hex_form):
) )
def get_deploy_kr_info(node_uuid, driver_info): def get_kernel_ramdisk_info(node_uuid, driver_info, mode='deploy'):
"""Get href and tftp path for deploy kernel and ramdisk. """Get href and tftp path for deploy or rescue kernel and ramdisk.
:param node_uuid: UUID of the node
:param driver_info: Node's driver_info dict
:param mode: A label to indicate whether paths for deploy or rescue
ramdisk are being requested. Supported values are 'deploy'
'rescue'. Defaults to 'deploy', indicating deploy paths will
be returned.
:returns: a dictionary whose keys are deploy_kernel and deploy_ramdisk or
rescue_kernel and rescue_ramdisk and whose values are the
absolute paths to them.
Note: driver_info should be validated outside of this method. Note: driver_info should be validated outside of this method.
""" """
root_dir = get_root_dir() root_dir = get_root_dir()
image_info = {} image_info = {}
for label in ('deploy_kernel', 'deploy_ramdisk'): labels = KERNEL_RAMDISK_LABELS[mode]
for label in labels:
image_info[label] = ( image_info[label] = (
str(driver_info[label]), str(driver_info[label]),
os.path.join(root_dir, node_uuid, label) os.path.join(root_dir, node_uuid, label)

View File

@ -398,48 +398,74 @@ def cleaning_error_handler(task, msg, tear_down_cleaning=True,
task.process_event('fail', target_state=target_state) task.process_event('fail', target_state=target_state)
def rescuing_error_handler(task, msg, set_fail_state=True):
"""Cleanup rescue task after timeout or failure.
:param task: a TaskManager instance.
:param msg: a message to set into node's last_error field
:param set_fail_state: a boolean flag to indicate if node needs to be
transitioned to a failed state. By default node
would be transitioned to a failed state.
"""
node = task.node
try:
node_power_action(task, states.POWER_OFF)
task.driver.rescue.clean_up(task)
node.last_error = msg
except exception.IronicException as e:
node.last_error = (_('Rescue operation was unsuccessful, clean up '
'failed for node: %(error)s') % {'error': e})
LOG.error(('Rescue operation was unsuccessful, clean up failed for '
'node %(node)s: %(error)s'),
{'node': node.uuid, 'error': e})
except Exception as e:
node.last_error = (_('Rescue failed, but an unhandled exception was '
'encountered while aborting: %(error)s') %
{'error': e})
LOG.exception('Rescue failed for node %(node)s, an exception was '
'encountered while aborting.', {'node': node.uuid})
finally:
node.save()
if set_fail_state:
try:
task.process_event('fail')
except exception.InvalidState:
node = task.node
LOG.error('Internal error. Node %(node)s in provision state '
'"%(state)s" could not transition to a failed state.',
{'node': node.uuid, 'state': node.provision_state})
@task_manager.require_exclusive_lock @task_manager.require_exclusive_lock
def cleanup_rescuewait_timeout(task): def cleanup_rescuewait_timeout(task):
"""Cleanup rescue task after timeout. """Cleanup rescue task after timeout.
:param task: a TaskManager instance. :param task: a TaskManager instance.
""" """
node = task.node
msg = _('Timeout reached while waiting for rescue ramdisk callback ' msg = _('Timeout reached while waiting for rescue ramdisk callback '
'for node') 'for node')
errmsg = msg + ' %(node)s' errmsg = msg + ' %(node)s'
LOG.error(errmsg, {'node': node.uuid}) LOG.error(errmsg, {'node': task.node.uuid})
try: rescuing_error_handler(task, msg, set_fail_state=False)
node_power_action(task, states.POWER_OFF)
task.driver.rescue.clean_up(task)
node.last_error = msg
node.save()
except Exception as e:
if isinstance(e, exception.IronicException):
error_msg = _('Cleanup failed for %(node_info)s after rescue '
'timeout: %(error)s')
node_info = ('node')
node.last_error = error_msg % {'node_info': node_info, 'error': e}
node_info = ('node %s') % node.uuid
LOG.error(error_msg, {'node_info': node_info, 'error': e})
else:
node.last_error = _('Rescue timed out, but an unhandled '
'exception was encountered while aborting. '
'More info may be found in the log file.')
LOG.exception('Rescue timed out for node %(node)s, an exception '
'was encountered while aborting. Error: %(err)s',
{'node': node.uuid, 'err': e})
node.save()
def _spawn_error_handler(e, node, state): def _spawn_error_handler(e, node, operation):
"""Handle spawning error for node.""" """Handle error while trying to spawn a process.
Handle error while trying to spawn a process to perform an
operation on a node.
:param e: the exception object that was raised.
:param node: an Ironic node object.
:param operation: the operation being performed on the node.
"""
if isinstance(e, exception.NoFreeConductorWorker): if isinstance(e, exception.NoFreeConductorWorker):
node.last_error = (_("No free conductor workers available")) node.last_error = (_("No free conductor workers available"))
node.save() node.save()
LOG.warning("No free conductor workers available to perform " LOG.warning("No free conductor workers available to perform "
"%(operation)s on node %(node)s", "%(operation)s on node %(node)s",
{'operation': state, 'node': node.uuid}) {'operation': operation, 'node': node.uuid})
def spawn_cleaning_error_handler(e, node): def spawn_cleaning_error_handler(e, node):

View File

@ -118,8 +118,8 @@ opts = [
'Defaults to 4. EXPERIMENTAL')), 'Defaults to 4. EXPERIMENTAL')),
cfg.BoolOpt('ipxe_use_swift', cfg.BoolOpt('ipxe_use_swift',
default=False, default=False,
help=_("Download deploy images directly from swift using " help=_("Download deploy and rescue images directly from swift "
"temporary URLs. " "using temporary URLs. "
"If set to false (default), images are downloaded " "If set to false (default), images are downloaded "
"to the ironic-conductor node and served over its " "to the ironic-conductor node and served over its "
"local HTTP server. " "local HTTP server. "

View File

@ -419,10 +419,10 @@ class BootInterface(BaseInterface):
interface_type = 'boot' interface_type = 'boot'
@abc.abstractmethod @abc.abstractmethod
def prepare_ramdisk(self, task, ramdisk_params): def prepare_ramdisk(self, task, ramdisk_params, mode='deploy'):
"""Prepares the boot of Ironic ramdisk. """Prepares the boot of Ironic ramdisk.
This method prepares the boot of the deploy ramdisk after This method prepares the boot of the deploy or rescue ramdisk after
reading relevant information from the node's database. reading relevant information from the node's database.
:param task: a task from TaskManager. :param task: a task from TaskManager.
@ -436,17 +436,25 @@ class BootInterface(BaseInterface):
Other implementations can make use of ramdisk_params to pass such Other implementations can make use of ramdisk_params to pass such
information. Different implementations of boot interface will information. Different implementations of boot interface will
have different ways of passing parameters to the ramdisk. have different ways of passing parameters to the ramdisk.
:param mode: Label indicating a deploy or rescue operation
being carried out on the node. Supported values are 'deploy' and
'rescue'. Defaults to 'deploy', indicating deploy operation is
being carried out.
:returns: None :returns: None
""" """
@abc.abstractmethod @abc.abstractmethod
def clean_up_ramdisk(self, task): def clean_up_ramdisk(self, task, mode='deploy'):
"""Cleans up the boot of ironic ramdisk. """Cleans up the boot of ironic ramdisk.
This method cleans up the environment that was setup for booting the This method cleans up the environment that was setup for booting the
deploy ramdisk. deploy or rescue ramdisk.
:param task: a task from TaskManager. :param task: a task from TaskManager.
:param mode: Label indicating a deploy or rescue operation
was carried out on the node. Supported values are 'deploy' and
'rescue'. Defaults to 'deploy', indicating deploy operation was
carried out.
:returns: None :returns: None
""" """

View File

@ -34,8 +34,7 @@ from ironic.drivers.modules.storage import noop as noop_storage
class GenericHardware(hardware_type.AbstractHardwareType): class GenericHardware(hardware_type.AbstractHardwareType):
"""Abstract base class representing generic hardware. """Abstract base class representing generic hardware.
This class provides reasonable defaults for boot, deploy, inspect, network This class provides reasonable defaults for all of the interfaces.
and raid interfaces.
""" """
@property @property
@ -69,6 +68,13 @@ class GenericHardware(hardware_type.AbstractHardwareType):
# default. Hence, even if AgentRAID is enabled, NoRAID is the default. # default. Hence, even if AgentRAID is enabled, NoRAID is the default.
return [noop.NoRAID, agent.AgentRAID] return [noop.NoRAID, agent.AgentRAID]
@property
def supported_rescue_interfaces(self):
"""List of supported rescue interfaces."""
# AgentRescue requires IPA with the rescue extension enabled, so
# NoRescue is the default
return [noop.NoRescue, agent.AgentRescue]
@property @property
def supported_storage_interfaces(self): def supported_storage_interfaces(self):
"""List of supported storage interfaces.""" """List of supported storage interfaces."""

View File

@ -15,6 +15,7 @@
from ironic_lib import metrics_utils from ironic_lib import metrics_utils
from ironic_lib import utils as il_utils from ironic_lib import utils as il_utils
from oslo_log import log from oslo_log import log
from oslo_utils import reflection
from oslo_utils import units from oslo_utils import units
import six.moves.urllib_parse as urlparse import six.moves.urllib_parse as urlparse
@ -32,6 +33,7 @@ from ironic.conf import CONF
from ironic.drivers import base from ironic.drivers import base
from ironic.drivers.modules import agent_base_vendor from ironic.drivers.modules import agent_base_vendor
from ironic.drivers.modules import deploy_utils from ironic.drivers.modules import deploy_utils
from ironic.drivers.modules.network import neutron
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -58,6 +60,14 @@ OPTIONAL_PROPERTIES = {
'``image_https_proxy`` are not specified. Optional.'), '``image_https_proxy`` are not specified. Optional.'),
} }
RESCUE_PROPERTIES = {
'rescue_kernel': _('UUID (from Glance) of the rescue kernel. This value '
'is required for rescue mode.'),
'rescue_ramdisk': _('UUID (from Glance) of the rescue ramdisk with agent '
'that is used at node rescue time. This value is '
'required for rescue mode.'),
}
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
COMMON_PROPERTIES.update(agent_base_vendor.VENDOR_PROPERTIES) COMMON_PROPERTIES.update(agent_base_vendor.VENDOR_PROPERTIES)
@ -460,13 +470,15 @@ class AgentDeploy(AgentDeployMixin, base.DeployInterface):
# backend storage system, and we can return to the caller # backend storage system, and we can return to the caller
# as we do not need to boot the agent to deploy. # as we do not need to boot the agent to deploy.
return return
if node.provision_state == states.ACTIVE: if node.provision_state in (states.ACTIVE, states.UNRESCUING):
# Call is due to conductor takeover # Call is due to conductor takeover
task.driver.boot.prepare_instance(task) task.driver.boot.prepare_instance(task)
elif node.provision_state != states.ADOPTING: elif node.provision_state != states.ADOPTING:
node.instance_info = deploy_utils.build_instance_info_for_deploy( if node.provision_state not in (states.RESCUING, states.RESCUEWAIT,
task) states.RESCUE, states.RESCUEFAIL):
node.save() node.instance_info = (
deploy_utils.build_instance_info_for_deploy(task))
node.save()
if CONF.agent.manage_agent_boot: if CONF.agent.manage_agent_boot:
deploy_opts = deploy_utils.build_agent_options(node) deploy_opts = deploy_utils.build_agent_options(node)
task.driver.boot.prepare_ramdisk(task, deploy_opts) task.driver.boot.prepare_ramdisk(task, deploy_opts)
@ -693,3 +705,133 @@ class AgentRAID(base.RAIDInterface):
""" """
task.node.raid_config = {} task.node.raid_config = {}
task.node.save() task.node.save()
class AgentRescue(base.RescueInterface):
"""Implementation of RescueInterface which uses agent ramdisk."""
def get_properties(self):
"""Return the properties of the interface.
:returns: dictionary of <property name>:<property description> entries.
"""
return RESCUE_PROPERTIES.copy()
@METRICS.timer('AgentRescue.rescue')
@task_manager.require_exclusive_lock
def rescue(self, task):
"""Boot a rescue ramdisk on the node.
:param task: a TaskManager instance.
:raises: NetworkError if the tenant ports cannot be removed.
:raises: InvalidParameterValue when the wrong power state is specified
or the wrong driver info is specified for power management.
:raises: other exceptions by the node's power driver if something
wrong occurred during the power action.
:raises: any boot interface's prepare_ramdisk exceptions.
:returns: Returns states.RESCUEWAIT
"""
manager_utils.node_power_action(task, states.POWER_OFF)
task.driver.boot.clean_up_instance(task)
task.driver.network.unconfigure_tenant_networks(task)
task.driver.network.add_rescuing_network(task)
if CONF.agent.manage_agent_boot:
ramdisk_opts = deploy_utils.build_agent_options(task.node)
# prepare_ramdisk will set the boot device
task.driver.boot.prepare_ramdisk(task, ramdisk_opts, mode='rescue')
manager_utils.node_power_action(task, states.POWER_ON)
return states.RESCUEWAIT
@METRICS.timer('AgentRescue.unrescue')
@task_manager.require_exclusive_lock
def unrescue(self, task):
"""Attempt to move a rescued node back to active state.
:param task: a TaskManager instance.
:raises: NetworkError if the rescue ports cannot be removed.
:raises: InvalidParameterValue when the wrong power state is specified
or the wrong driver info is specified for power management.
:raises: other exceptions by the node's power driver if something
wrong occurred during the power action.
:raises: any boot interface's prepare_instance exceptions.
:returns: Returns states.ACTIVE
"""
manager_utils.node_power_action(task, states.POWER_OFF)
self.clean_up(task)
task.driver.network.configure_tenant_networks(task)
task.driver.boot.prepare_instance(task)
manager_utils.node_power_action(task, states.POWER_ON)
return states.ACTIVE
@METRICS.timer('AgentRescue.validate')
def validate(self, task):
"""Validate that the node has required properties for agent rescue.
:param task: a TaskManager instance with the node being checked
:raises: InvalidParameterValue if 'instance_info/rescue_password' has
empty password or rescuing network UUID config option
has an invalid value when 'neutron' network is used.
:raises: MissingParameterValue if node is missing one or more required
parameters
:raises: IncompatibleInterface if 'prepare_ramdisk' and
'clean_up_ramdisk' of node's boot interface do not support 'mode'
argument.
"""
node = task.node
missing_params = []
# Validate rescuing network if node is using 'neutron' network
if isinstance(task.driver.network, neutron.NeutronNetwork):
task.driver.network.get_rescuing_network_uuid(task)
if CONF.agent.manage_agent_boot:
if ('mode' not in reflection.get_signature(
task.driver.boot.prepare_ramdisk).parameters or
'mode' not in reflection.get_signature(
task.driver.boot.clean_up_ramdisk).parameters):
raise exception.IncompatibleInterface(
interface_type='boot',
interface_impl="of 'prepare_ramdisk' and/or "
"'clean_up_ramdisk' with 'mode' argument",
hardware_type=node.driver)
# TODO(stendulker): boot.validate() performs validation of
# provisioning related parameters which is not required during
# rescue operation.
task.driver.boot.validate(task)
for req in RESCUE_PROPERTIES:
if node.driver_info.get(req) is None:
missing_params.append('driver_info/' + req)
rescue_pass = node.instance_info.get('rescue_password')
if rescue_pass is None:
missing_params.append('instance_info/rescue_password')
if missing_params:
msg = _('Node %(node)s is missing parameter(s): '
'%(params)s. These are required for rescuing node.')
raise exception.MissingParameterValue(
msg % {'node': node.uuid,
'params': ', '.join(missing_params)})
if not rescue_pass.strip():
msg = (_("The 'instance_info/rescue_password' is an empty string "
"for node %s. The 'rescue_password' must be a non-empty "
"string value.") % node.uuid)
raise exception.InvalidParameterValue(msg)
@METRICS.timer('AgentRescue.clean_up')
def clean_up(self, task):
"""Clean up after RESCUEWAIT timeout/failure or finishing rescue.
Rescue password should be removed from the node and ramdisk boot
environment should be cleaned if Ironic is managing the ramdisk boot.
:param task: a TaskManager instance with the node.
:raises: NetworkError if the rescue ports cannot be removed.
"""
manager_utils.remove_node_rescue_password(task.node, save=True)
if CONF.agent.manage_agent_boot:
task.driver.boot.clean_up_ramdisk(task, mode='rescue')
task.driver.network.remove_rescuing_network(task)

View File

@ -269,7 +269,7 @@ class HeartbeatMixin(object):
@property @property
def heartbeat_allowed_states(self): def heartbeat_allowed_states(self):
"""Define node states where heartbeating is allowed""" """Define node states where heartbeating is allowed"""
return (states.DEPLOYWAIT, states.CLEANWAIT) return (states.DEPLOYWAIT, states.CLEANWAIT, states.RESCUEWAIT)
@METRICS.timer('HeartbeatMixin.heartbeat') @METRICS.timer('HeartbeatMixin.heartbeat')
def heartbeat(self, task, callback_url, agent_version): def heartbeat(self, task, callback_url, agent_version):
@ -334,17 +334,50 @@ class HeartbeatMixin(object):
else: else:
msg = _('Node failed to check cleaning progress.') msg = _('Node failed to check cleaning progress.')
self.continue_cleaning(task) self.continue_cleaning(task)
elif (node.provision_state == states.RESCUEWAIT):
msg = _('Node failed to perform rescue operation.')
self._finalize_rescue(task)
except Exception as e: except Exception as e:
err_info = {'node': node.uuid, 'msg': msg, 'e': e} err_info = {'msg': msg, 'e': e}
last_error = _('Asynchronous exception for node %(node)s: ' last_error = _('Asynchronous exception: %(msg)s '
'%(msg)s Exception: %(e)s') % err_info 'Exception: %(e)s for node') % err_info
LOG.exception(last_error) errmsg = last_error + ' %(node)s'
LOG.exception(errmsg, {'node': node.uuid})
if node.provision_state in (states.CLEANING, states.CLEANWAIT): if node.provision_state in (states.CLEANING, states.CLEANWAIT):
manager_utils.cleaning_error_handler(task, last_error) manager_utils.cleaning_error_handler(task, last_error)
elif node.provision_state in (states.DEPLOYING, states.DEPLOYWAIT): elif node.provision_state in (states.DEPLOYING, states.DEPLOYWAIT):
deploy_utils.set_failed_state( deploy_utils.set_failed_state(
task, last_error, collect_logs=bool(self._client)) task, last_error, collect_logs=bool(self._client))
elif node.provision_state in (states.RESCUING, states.RESCUEWAIT):
manager_utils.rescuing_error_handler(task, last_error)
def _finalize_rescue(self, task):
"""Call ramdisk to prepare rescue mode and verify result.
:param task: A TaskManager instance
:raises: InstanceRescueFailure, if rescuing failed
"""
node = task.node
try:
result = self._client.finalize_rescue(node)
except exception.IronicException as e:
raise exception.InstanceRescueFailure(node=node.uuid,
instance=node.instance_uuid,
reason=e)
if ((not result.get('command_status')) or
result.get('command_status') != 'SUCCEEDED'):
# NOTE(mariojv) Caller will clean up failed rescue in exception
# handler.
fail_reason = (_('Agent returned bad result for command '
'finalize_rescue: %(result)s') %
{'result': result.get('command_error')})
raise exception.InstanceRescueFailure(node=node.uuid,
instance=node.instance_uuid,
reason=fail_reason)
task.process_event('resume')
task.driver.rescue.clean_up(task)
task.driver.network.configure_tenant_networks(task)
task.process_event('done')
class AgentDeployMixin(HeartbeatMixin): class AgentDeployMixin(HeartbeatMixin):

View File

@ -213,3 +213,16 @@ class AgentClient(object):
method='log.collect_system_logs', method='log.collect_system_logs',
params={}, params={},
wait=True) wait=True)
@METRICS.timer('AgentClient.finalize_rescue')
def finalize_rescue(self, node):
"""Instruct the ramdisk to finalize entering of rescue mode."""
rescue_pass = node.instance_info.get('rescue_password')
if not rescue_pass:
raise exception.IronicException(_('Agent rescue requires '
'rescue_password in '
'instance_info'))
params = {'rescue_password': rescue_pass}
return self._command(node=node,
method='rescue.finalize_rescue',
params=params)

View File

@ -65,6 +65,10 @@ SUPPORTED_CAPABILITIES = {
'disk_label': ('msdos', 'gpt'), 'disk_label': ('msdos', 'gpt'),
} }
# States related to rescue mode.
RESCUE_LIKE_STATES = (states.RESCUING, states.RESCUEWAIT, states.RESCUEFAIL,
states.UNRESCUING, states.UNRESCUEFAIL)
DISK_LAYOUT_PARAMS = ('root_gb', 'swap_mb', 'ephemeral_gb') DISK_LAYOUT_PARAMS = ('root_gb', 'swap_mb', 'ephemeral_gb')

View File

@ -79,10 +79,10 @@ class FakeBoot(base.BootInterface):
def validate(self, task): def validate(self, task):
pass pass
def prepare_ramdisk(self, task, ramdisk_params): def prepare_ramdisk(self, task, ramdisk_params, mode='deploy'):
pass pass
def clean_up_ramdisk(self, task): def clean_up_ramdisk(self, task, mode='deploy'):
pass pass
def prepare_instance(self, task): def prepare_instance(self, task):

View File

@ -59,19 +59,26 @@ COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
def _parse_driver_info(node): def _parse_driver_info(node, mode='deploy'):
"""Gets the driver specific Node deployment info. """Gets the driver specific Node deployment info.
This method validates whether the 'driver_info' property of the This method validates whether the 'driver_info' property of the
supplied node contains the required information for this driver to supplied node contains the required information for this driver to
deploy images to the node. deploy images to, or rescue, the node.
:param node: a single Node. :param node: a single Node.
:param mode: Label indicating a deploy or rescue operation being
carried out on the node. Supported values are
'deploy' and 'rescue'. Defaults to 'deploy', indicating
deploy operation is being carried out.
:returns: A dict with the driver_info values. :returns: A dict with the driver_info values.
:raises: MissingParameterValue :raises: MissingParameterValue
""" """
info = node.driver_info info = node.driver_info
d_info = {k: info.get(k) for k in ('deploy_kernel', 'deploy_ramdisk')}
params_to_check = pxe_utils.KERNEL_RAMDISK_LABELS[mode]
d_info = {k: info.get(k) for k in params_to_check}
error_msg = _("Cannot validate PXE bootloader. Some parameters were" error_msg = _("Cannot validate PXE bootloader. Some parameters were"
" missing in node's driver_info") " missing in node's driver_info")
deploy_utils.check_for_missing_params(d_info, error_msg) deploy_utils.check_for_missing_params(d_info, error_msg)
@ -121,29 +128,37 @@ def _get_instance_image_info(node, ctx):
return image_info return image_info
def _get_deploy_image_info(node): def _get_image_info(node, mode='deploy'):
"""Generate the paths for TFTP files for deploy images. """Generate the paths for TFTP files for deploy or rescue images.
This method generates the paths for the deploy kernel and This method generates the paths for the deploy (or rescue) kernel and
deploy ramdisk. deploy (or rescue) ramdisk.
:param node: a node object :param node: a node object
:returns: a dictionary whose keys are the names of the images ( :param mode: Label indicating a deploy or rescue operation being
deploy_kernel, deploy_ramdisk) and values are the absolute carried out on the node. Supported values are 'deploy' and 'rescue'.
paths of them. Defaults to 'deploy', indicating deploy operation is being carried out.
:raises: MissingParameterValue, if deploy_kernel/deploy_ramdisk is :returns: a dictionary whose keys are the names of the images
missing in node's driver_info. (deploy_kernel, deploy_ramdisk, or rescue_kernel, rescue_ramdisk) and
values are the absolute paths of them.
:raises: MissingParameterValue, if deploy_kernel/deploy_ramdisk or
rescue_kernel/rescue_ramdisk is missing in node's driver_info.
""" """
d_info = _parse_driver_info(node) d_info = _parse_driver_info(node, mode=mode)
return pxe_utils.get_deploy_kr_info(node.uuid, d_info)
return pxe_utils.get_kernel_ramdisk_info(
node.uuid, d_info, mode=mode)
def _build_deploy_pxe_options(task, pxe_info): def _build_deploy_pxe_options(task, pxe_info, mode='deploy'):
pxe_opts = {} pxe_opts = {}
node = task.node node = task.node
for label, option in (('deploy_kernel', 'deployment_aki_path'), kernel_label = '%s_kernel' % mode
('deploy_ramdisk', 'deployment_ari_path')): ramdisk_label = '%s_ramdisk' % mode
for label, option in ((kernel_label, 'deployment_aki_path'),
(ramdisk_label, 'deployment_ari_path')):
if CONF.pxe.ipxe_enabled: if CONF.pxe.ipxe_enabled:
image_href = pxe_info[label][0] image_href = pxe_info[label][0]
if (CONF.pxe.ipxe_use_swift and if (CONF.pxe.ipxe_use_swift and
@ -218,20 +233,25 @@ def _build_pxe_config_options(task, pxe_info, service=False):
:returns: A dictionary of pxe options to be used in the pxe bootfile :returns: A dictionary of pxe options to be used in the pxe bootfile
template. template.
""" """
node = task.node
mode = ('rescue' if node.provision_state in deploy_utils.RESCUE_LIKE_STATES
else 'deploy')
if service: if service:
pxe_options = {} pxe_options = {}
elif (task.node.driver_internal_info.get('boot_from_volume') and elif (node.driver_internal_info.get('boot_from_volume') and
CONF.pxe.ipxe_enabled): CONF.pxe.ipxe_enabled):
pxe_options = _get_volume_pxe_options(task) pxe_options = _get_volume_pxe_options(task)
else: else:
pxe_options = _build_deploy_pxe_options(task, pxe_info) pxe_options = _build_deploy_pxe_options(task, pxe_info, mode=mode)
if mode == 'deploy':
# NOTE(pas-ha) we still must always add user image kernel and ramdisk
# info as later during switching PXE config to service mode the
# template will not be regenerated anew, but instead edited as-is.
# This can be changed later if/when switching PXE config will also use
# proper templating instead of editing existing files on disk.
pxe_options.update(_build_instance_pxe_options(task, pxe_info))
# NOTE(pas-ha) we still must always add user image kernel and ramdisk info
# as later during switching PXE config to service mode the template
# will not be regenerated anew, but instead edited as-is.
# This can be changed later if/when switching PXE config will also use
# proper templating instead of editing existing files on disk.
pxe_options.update(_build_instance_pxe_options(task, pxe_info))
pxe_options.update(_build_extra_pxe_options()) pxe_options.update(_build_extra_pxe_options())
return pxe_options return pxe_options
@ -241,10 +261,10 @@ def _build_service_pxe_config(task, instance_image_info,
root_uuid_or_disk_id): root_uuid_or_disk_id):
node = task.node node = task.node
pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid) pxe_config_path = pxe_utils.get_pxe_config_file_path(node.uuid)
# NOTE(pas-ha) if it is takeover of ACTIVE node, # NOTE(pas-ha) if it is takeover of ACTIVE node or node performing
# first ensure that basic PXE configs and links # unrescue operation, first ensure that basic PXE configs and links
# are in place before switching pxe config # are in place before switching pxe config
if (node.provision_state == states.ACTIVE and if (node.provision_state in [states.ACTIVE, states.UNRESCUING] and
not os.path.isfile(pxe_config_path)): not os.path.isfile(pxe_config_path)):
pxe_options = _build_pxe_config_options(task, instance_image_info, pxe_options = _build_pxe_config_options(task, instance_image_info,
service=True) service=True)
@ -435,7 +455,7 @@ class PXEBoot(base.BootInterface):
_parse_driver_info(node) _parse_driver_info(node)
# NOTE(TheJulia): If we're not writing an image, we can skip # NOTE(TheJulia): If we're not writing an image, we can skip
# the remainder of this method. # the remainder of this method.
if not task.driver.storage.should_write_image(task): if (not task.driver.storage.should_write_image(task)):
return return
d_info = deploy_utils.get_image_instance_info(node) d_info = deploy_utils.get_image_instance_info(node)
@ -449,17 +469,21 @@ class PXEBoot(base.BootInterface):
deploy_utils.validate_image_properties(task.context, d_info, props) deploy_utils.validate_image_properties(task.context, d_info, props)
@METRICS.timer('PXEBoot.prepare_ramdisk') @METRICS.timer('PXEBoot.prepare_ramdisk')
def prepare_ramdisk(self, task, ramdisk_params): def prepare_ramdisk(self, task, ramdisk_params, mode='deploy'):
"""Prepares the boot of Ironic ramdisk using PXE. """Prepares the boot of Ironic ramdisk using PXE.
This method prepares the boot of the deploy kernel/ramdisk after This method prepares the boot of the deploy or rescue kernel/ramdisk
reading relevant information from the node's driver_info and after reading relevant information from the node's driver_info and
instance_info. instance_info.
:param task: a task from TaskManager. :param task: a task from TaskManager.
:param ramdisk_params: the parameters to be passed to the ramdisk. :param ramdisk_params: the parameters to be passed to the ramdisk.
pxe driver passes these parameters as kernel command-line pxe driver passes these parameters as kernel command-line
arguments. arguments.
:param mode: Label indicating a deploy or rescue operation
being carried out on the node. Supported values are
'deploy' and 'rescue'. Defaults to 'deploy', indicating
deploy operation is being carried out.
:returns: None :returns: None
:raises: MissingParameterValue, if some information is missing in :raises: MissingParameterValue, if some information is missing in
node's driver_info or instance_info. node's driver_info or instance_info.
@ -482,7 +506,7 @@ class PXEBoot(base.BootInterface):
provider = dhcp_factory.DHCPFactory() provider = dhcp_factory.DHCPFactory()
provider.update_dhcp(task, dhcp_opts) provider.update_dhcp(task, dhcp_opts)
pxe_info = _get_deploy_image_info(node) pxe_info = _get_image_info(node, mode=mode)
# NODE: Try to validate and fetch instance images only # NODE: Try to validate and fetch instance images only
# if we are in DEPLOYING state. # if we are in DEPLOYING state.
@ -503,29 +527,37 @@ class PXEBoot(base.BootInterface):
persistent=persistent) persistent=persistent)
if CONF.pxe.ipxe_enabled and CONF.pxe.ipxe_use_swift: if CONF.pxe.ipxe_enabled and CONF.pxe.ipxe_use_swift:
pxe_info.pop('deploy_kernel', None) kernel_label = '%s_kernel' % mode
pxe_info.pop('deploy_ramdisk', None) ramdisk_label = '%s_ramdisk' % mode
pxe_info.pop(kernel_label, None)
pxe_info.pop(ramdisk_label, None)
if pxe_info: if pxe_info:
_cache_ramdisk_kernel(task.context, node, pxe_info) _cache_ramdisk_kernel(task.context, node, pxe_info)
@METRICS.timer('PXEBoot.clean_up_ramdisk') @METRICS.timer('PXEBoot.clean_up_ramdisk')
def clean_up_ramdisk(self, task): def clean_up_ramdisk(self, task, mode='deploy'):
"""Cleans up the boot of ironic ramdisk. """Cleans up the boot of ironic ramdisk.
This method cleans up the PXE environment that was setup for booting This method cleans up the PXE environment that was setup for booting
the deploy ramdisk. It unlinks the deploy kernel/ramdisk in the node's the deploy or rescue ramdisk. It unlinks the deploy/rescue
directory in tftproot and removes it's PXE config. kernel/ramdisk in the node's directory in tftproot and removes it's PXE
config.
:param task: a task from TaskManager. :param task: a task from TaskManager.
:param mode: Label indicating a deploy or rescue operation
was carried out on the node. Supported values are 'deploy' and
'rescue'. Defaults to 'deploy', indicating deploy operation was
carried out.
:returns: None :returns: None
""" """
node = task.node node = task.node
try: try:
images_info = _get_deploy_image_info(node) images_info = _get_image_info(node, mode=mode)
except exception.MissingParameterValue as e: except exception.MissingParameterValue as e:
LOG.warning('Could not get deploy image info ' LOG.warning('Could not get %(mode)s image info '
'to clean up images for node %(node)s: %(err)s', 'to clean up images for node %(node)s: %(err)s',
{'node': node.uuid, 'err': e}) {'mode': mode, 'node': node.uuid, 'err': e})
else: else:
_clean_up_pxe_env(task, images_info) _clean_up_pxe_env(task, images_info)

View File

@ -646,43 +646,53 @@ class TestPXEUtils(db_base.DbTestCase):
def test_dhcp_options_for_instance_ipv6(self): def test_dhcp_options_for_instance_ipv6(self):
self._dhcp_options_for_instance(ip_version=6) self._dhcp_options_for_instance(ip_version=6)
def _test_get_deploy_kr_info(self, expected_dir): def _test_get_kernel_ramdisk_info(self, expected_dir, mode='deploy'):
node_uuid = 'fake-node' node_uuid = 'fake-node'
driver_info = { driver_info = {
'deploy_kernel': 'glance://deploy-kernel', '%s_kernel' % mode: 'glance://%s-kernel' % mode,
'deploy_ramdisk': 'glance://deploy-ramdisk', '%s_ramdisk' % mode: 'glance://%s-ramdisk' % mode,
} }
expected = { expected = {}
'deploy_kernel': ('glance://deploy-kernel', for k, v in driver_info.items():
expected_dir + '/fake-node/deploy_kernel'), expected[k] = (v, expected_dir + '/fake-node/%s' % k)
'deploy_ramdisk': ('glance://deploy-ramdisk', kr_info = pxe_utils.get_kernel_ramdisk_info(node_uuid,
expected_dir + '/fake-node/deploy_ramdisk'), driver_info,
} mode=mode)
kr_info = pxe_utils.get_deploy_kr_info(node_uuid, driver_info)
self.assertEqual(expected, kr_info) self.assertEqual(expected, kr_info)
def test_get_deploy_kr_info(self): def test_get_kernel_ramdisk_info(self):
expected_dir = '/tftp' expected_dir = '/tftp'
self.config(tftp_root=expected_dir, group='pxe') self.config(tftp_root=expected_dir, group='pxe')
self._test_get_deploy_kr_info(expected_dir) self._test_get_kernel_ramdisk_info(expected_dir)
def test_get_deploy_kr_info_ipxe(self): def test_get_kernel_ramdisk_info_ipxe(self):
expected_dir = '/http' expected_dir = '/http'
self.config(ipxe_enabled=True, group='pxe') self.config(ipxe_enabled=True, group='pxe')
self.config(http_root=expected_dir, group='deploy') self.config(http_root=expected_dir, group='deploy')
self._test_get_deploy_kr_info(expected_dir) self._test_get_kernel_ramdisk_info(expected_dir)
def test_get_deploy_kr_info_bad_driver_info(self): def test_get_kernel_ramdisk_info_bad_driver_info(self):
self.config(tftp_root='/tftp', group='pxe') self.config(tftp_root='/tftp', group='pxe')
node_uuid = 'fake-node' node_uuid = 'fake-node'
driver_info = {} driver_info = {}
self.assertRaises(KeyError, self.assertRaises(KeyError,
pxe_utils.get_deploy_kr_info, pxe_utils.get_kernel_ramdisk_info,
node_uuid, node_uuid,
driver_info) driver_info)
def test_get_rescue_kr_info(self):
expected_dir = '/tftp'
self.config(tftp_root=expected_dir, group='pxe')
self._test_get_kernel_ramdisk_info(expected_dir, mode='rescue')
def test_get_rescue_kr_info_ipxe(self):
expected_dir = '/http'
self.config(ipxe_enabled=True, group='pxe')
self.config(http_root=expected_dir, group='deploy')
self._test_get_kernel_ramdisk_info(expected_dir, mode='rescue')
def _dhcp_options_for_instance_ipxe(self, task, boot_file): def _dhcp_options_for_instance_ipxe(self, task, boot_file):
self.config(tftp_server='192.0.2.1', group='pxe') self.config(tftp_server='192.0.2.1', group='pxe')
self.config(ipxe_enabled=True, group='pxe') self.config(ipxe_enabled=True, group='pxe')

View File

@ -1271,10 +1271,94 @@ class ErrorHandlersTestCase(tests_base.TestCase):
self.assertTrue(log_mock.error.called) self.assertTrue(log_mock.error.called)
node_power_mock.assert_called_once_with(mock.ANY, states.POWER_OFF) node_power_mock.assert_called_once_with(mock.ANY, states.POWER_OFF)
self.task.driver.rescue.clean_up.assert_called_once_with(self.task) self.task.driver.rescue.clean_up.assert_called_once_with(self.task)
self.assertIn('Rescue timed out', self.node.last_error) self.assertIn('Rescue failed', self.node.last_error)
self.node.save.assert_called_once_with() self.node.save.assert_called_once_with()
self.assertTrue(log_mock.exception.called) self.assertTrue(log_mock.exception.called)
@mock.patch.object(conductor_utils, 'node_power_action')
def _test_rescuing_error_handler(self, node_power_mock,
set_state=True):
self.node.provision_state = states.RESCUEWAIT
conductor_utils.rescuing_error_handler(self.task,
'some exception for node',
set_fail_state=set_state)
node_power_mock.assert_called_once_with(mock.ANY, states.POWER_OFF)
self.task.driver.rescue.clean_up.assert_called_once_with(self.task)
self.node.save.assert_called_once_with()
if set_state:
self.assertTrue(self.task.process_event.called)
else:
self.assertFalse(self.task.process_event.called)
def test_rescuing_error_handler(self):
self._test_rescuing_error_handler()
def test_rescuing_error_handler_set_failed_state_false(self):
self._test_rescuing_error_handler(set_state=False)
@mock.patch.object(conductor_utils.LOG, 'error')
@mock.patch.object(conductor_utils, 'node_power_action')
def test_rescuing_error_handler_ironic_exc(self, node_power_mock,
log_mock):
self.node.provision_state = states.RESCUEWAIT
expected_exc = exception.IronicException('moocow')
clean_up_mock = self.task.driver.rescue.clean_up
clean_up_mock.side_effect = expected_exc
conductor_utils.rescuing_error_handler(self.task,
'some exception for node')
node_power_mock.assert_called_once_with(mock.ANY, states.POWER_OFF)
self.task.driver.rescue.clean_up.assert_called_once_with(self.task)
log_mock.assert_called_once_with('Rescue operation was unsuccessful, '
'clean up failed for node %(node)s: '
'%(error)s',
{'node': self.node.uuid,
'error': expected_exc})
self.node.save.assert_called_once_with()
@mock.patch.object(conductor_utils.LOG, 'exception')
@mock.patch.object(conductor_utils, 'node_power_action')
def test_rescuing_error_handler_other_exc(self, node_power_mock,
log_mock):
self.node.provision_state = states.RESCUEWAIT
expected_exc = RuntimeError()
clean_up_mock = self.task.driver.rescue.clean_up
clean_up_mock.side_effect = expected_exc
conductor_utils.rescuing_error_handler(self.task,
'some exception for node')
node_power_mock.assert_called_once_with(mock.ANY, states.POWER_OFF)
self.task.driver.rescue.clean_up.assert_called_once_with(self.task)
log_mock.assert_called_once_with('Rescue failed for node '
'%(node)s, an exception was '
'encountered while aborting.',
{'node': self.node.uuid})
self.node.save.assert_called_once_with()
@mock.patch.object(conductor_utils.LOG, 'error')
@mock.patch.object(conductor_utils, 'node_power_action')
def test_rescuing_error_handler_bad_state(self, node_power_mock,
log_mock):
self.node.provision_state = states.RESCUE
self.task.process_event.side_effect = exception.InvalidState
expected_exc = exception.IronicException('moocow')
clean_up_mock = self.task.driver.rescue.clean_up
clean_up_mock.side_effect = expected_exc
conductor_utils.rescuing_error_handler(self.task,
'some exception for node')
node_power_mock.assert_called_once_with(mock.ANY, states.POWER_OFF)
self.task.driver.rescue.clean_up.assert_called_once_with(self.task)
self.task.process_event.assert_called_once_with('fail')
log_calls = [mock.call('Rescue operation was unsuccessful, clean up '
'failed for node %(node)s: %(error)s',
{'node': self.node.uuid,
'error': expected_exc}),
mock.call('Internal error. Node %(node)s in provision '
'state "%(state)s" could not transition to a '
'failed state.',
{'node': self.node.uuid,
'state': self.node.provision_state})]
log_mock.assert_has_calls(log_calls)
self.node.save.assert_called_once_with()
class ValidatePortPhysnetTestCase(db_base.DbTestCase): class ValidatePortPhysnetTestCase(db_base.DbTestCase):

View File

@ -53,6 +53,8 @@ def get_test_pxe_driver_info():
return { return {
"deploy_kernel": "glance://deploy_kernel_uuid", "deploy_kernel": "glance://deploy_kernel_uuid",
"deploy_ramdisk": "glance://deploy_ramdisk_uuid", "deploy_ramdisk": "glance://deploy_ramdisk_uuid",
"rescue_kernel": "glance://rescue_kernel_uuid",
"rescue_ramdisk": "glance://rescue_ramdisk_uuid"
} }
@ -66,6 +68,7 @@ def get_test_pxe_instance_info():
return { return {
"image_source": "glance://image_uuid", "image_source": "glance://image_uuid",
"root_gb": 100, "root_gb": 100,
"rescue_password": "password"
} }

View File

@ -16,14 +16,17 @@ import types
import mock import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_utils import reflection
from ironic.common import dhcp_factory from ironic.common import dhcp_factory
from ironic.common import exception from ironic.common import exception
from ironic.common import images from ironic.common import images
from ironic.common import neutron as neutron_common
from ironic.common import raid from ironic.common import raid
from ironic.common import states from ironic.common import states
from ironic.conductor import task_manager from ironic.conductor import task_manager
from ironic.conductor import utils as manager_utils from ironic.conductor import utils as manager_utils
from ironic.drivers import base as drivers_base
from ironic.drivers.modules import agent from ironic.drivers.modules import agent
from ironic.drivers.modules import agent_base_vendor from ironic.drivers.modules import agent_base_vendor
from ironic.drivers.modules import agent_client from ironic.drivers.modules import agent_client
@ -388,23 +391,48 @@ class TestAgentDeploy(db_base.DbTestCase):
self.node.refresh() self.node.refresh()
self.assertEqual('bar', self.node.instance_info['foo']) self.assertEqual('bar', self.node.instance_info['foo'])
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk')
@mock.patch.object(deploy_utils, 'build_agent_options')
@mock.patch.object(deploy_utils, 'build_instance_info_for_deploy')
def _test_prepare_rescue_states(
self, build_instance_info_mock, build_options_mock,
pxe_prepare_ramdisk_mock, prov_state):
with task_manager.acquire(
self.context, self.node['uuid'], shared=False) as task:
task.node.provision_state = prov_state
build_options_mock.return_value = {'a': 'b'}
self.driver.prepare(task)
self.assertFalse(build_instance_info_mock.called)
build_options_mock.assert_called_once_with(task.node)
pxe_prepare_ramdisk_mock.assert_called_once_with(
task, {'a': 'b'})
def test_prepare_rescue_states(self):
for state in (states.RESCUING, states.RESCUEWAIT,
states.RESCUE, states.RESCUEFAIL):
self._test_prepare_rescue_states(prov_state=state)
@mock.patch.object(noop_storage.NoopStorage, 'attach_volumes', @mock.patch.object(noop_storage.NoopStorage, 'attach_volumes',
autospec=True) autospec=True)
@mock.patch.object(deploy_utils, 'populate_storage_driver_internal_info') @mock.patch.object(deploy_utils, 'populate_storage_driver_internal_info')
@mock.patch.object(flat_network.FlatNetwork, 'add_provisioning_network', @mock.patch.object(flat_network.FlatNetwork, 'add_provisioning_network',
spec_set=True, autospec=True) spec_set=True, autospec=True)
@mock.patch.object(pxe.PXEBoot, 'prepare_instance') @mock.patch.object(pxe.PXEBoot, 'prepare_instance',
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk') spec_set=True, autospec=True)
@mock.patch.object(deploy_utils, 'build_agent_options') @mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk',
@mock.patch.object(deploy_utils, 'build_instance_info_for_deploy') spec_set=True, autospec=True)
def test_prepare_active( @mock.patch.object(deploy_utils, 'build_agent_options',
spec_set=True, autospec=True)
@mock.patch.object(deploy_utils, 'build_instance_info_for_deploy',
spec_set=True, autospec=True)
def _test_prepare_conductor_takeover(
self, build_instance_info_mock, build_options_mock, self, build_instance_info_mock, build_options_mock,
pxe_prepare_ramdisk_mock, pxe_prepare_instance_mock, pxe_prepare_ramdisk_mock, pxe_prepare_instance_mock,
add_provisioning_net_mock, storage_driver_info_mock, add_provisioning_net_mock, storage_driver_info_mock,
storage_attach_volumes_mock): storage_attach_volumes_mock, prov_state):
with task_manager.acquire( with task_manager.acquire(
self.context, self.node['uuid'], shared=False) as task: self.context, self.node['uuid'], shared=False) as task:
task.node.provision_state = states.ACTIVE task.node.provision_state = prov_state
self.driver.prepare(task) self.driver.prepare(task)
@ -416,6 +444,11 @@ class TestAgentDeploy(db_base.DbTestCase):
self.assertTrue(storage_driver_info_mock.called) self.assertTrue(storage_driver_info_mock.called)
self.assertFalse(storage_attach_volumes_mock.called) self.assertFalse(storage_attach_volumes_mock.called)
def test_prepare_active_and_unrescue_states(self):
for prov_state in (states.ACTIVE, states.UNRESCUING):
self._test_prepare_conductor_takeover(
prov_state=prov_state)
@mock.patch.object(noop_storage.NoopStorage, 'should_write_image', @mock.patch.object(noop_storage.NoopStorage, 'should_write_image',
autospec=True) autospec=True)
@mock.patch.object(noop_storage.NoopStorage, 'attach_volumes', @mock.patch.object(noop_storage.NoopStorage, 'attach_volumes',
@ -1193,3 +1226,266 @@ class AgentRAIDTestCase(db_base.DbTestCase):
self.node.refresh() self.node.refresh()
self.assertEqual({}, self.node.raid_config) self.assertEqual({}, self.node.raid_config)
class AgentRescueTestCase(db_base.DbTestCase):
def setUp(self):
super(AgentRescueTestCase, self).setUp()
for iface in drivers_base.ALL_INTERFACES:
impl = 'fake'
if iface == 'network':
impl = 'flat'
if iface == 'rescue':
impl = 'agent'
config_kwarg = {'enabled_%s_interfaces' % iface: [impl],
'default_%s_interface' % iface: impl}
self.config(**config_kwarg)
self.config(enabled_hardware_types=['fake-hardware'])
instance_info = INSTANCE_INFO
instance_info.update({'rescue_password': 'password'})
driver_info = DRIVER_INFO
driver_info.update({'rescue_ramdisk': 'my_ramdisk',
'rescue_kernel': 'my_kernel'})
n = {
'driver': 'fake-hardware',
'instance_info': instance_info,
'driver_info': driver_info,
'driver_internal_info': DRIVER_INTERNAL_INFO,
}
self.node = object_utils.create_test_node(self.context, **n)
@mock.patch.object(flat_network.FlatNetwork, 'add_rescuing_network',
spec_set=True, autospec=True)
@mock.patch.object(flat_network.FlatNetwork, 'unconfigure_tenant_networks',
spec_set=True, autospec=True)
@mock.patch.object(fake.FakeBoot, 'prepare_ramdisk', autospec=True)
@mock.patch.object(fake.FakeBoot, 'clean_up_instance', autospec=True)
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
def test_agent_rescue(self, mock_node_power_action, mock_build_agent_opts,
mock_clean_up_instance, mock_prepare_ramdisk,
mock_unconf_tenant_net, mock_add_rescue_net):
self.config(manage_agent_boot=True, group='agent')
mock_build_agent_opts.return_value = {'ipa-api-url': 'fake-api'}
with task_manager.acquire(self.context, self.node.uuid) as task:
result = task.driver.rescue.rescue(task)
mock_node_power_action.assert_has_calls(
[mock.call(task, states.POWER_OFF),
mock.call(task, states.POWER_ON)])
mock_clean_up_instance.assert_called_once_with(mock.ANY, task)
mock_unconf_tenant_net.assert_called_once_with(mock.ANY, task)
mock_add_rescue_net.assert_called_once_with(mock.ANY, task)
mock_build_agent_opts.assert_called_once_with(task.node)
mock_prepare_ramdisk.assert_called_once_with(
mock.ANY, task, {'ipa-api-url': 'fake-api'}, mode='rescue')
self.assertEqual(states.RESCUEWAIT, result)
@mock.patch.object(flat_network.FlatNetwork, 'add_rescuing_network',
spec_set=True, autospec=True)
@mock.patch.object(flat_network.FlatNetwork, 'unconfigure_tenant_networks',
spec_set=True, autospec=True)
@mock.patch.object(fake.FakeBoot, 'prepare_ramdisk', autospec=True)
@mock.patch.object(fake.FakeBoot, 'clean_up_instance', autospec=True)
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
def test_agent_rescue_no_manage_agent_boot(self, mock_node_power_action,
mock_build_agent_opts,
mock_clean_up_instance,
mock_prepare_ramdisk,
mock_unconf_tenant_net,
mock_add_rescue_net):
self.config(manage_agent_boot=False, group='agent')
with task_manager.acquire(self.context, self.node.uuid) as task:
result = task.driver.rescue.rescue(task)
mock_node_power_action.assert_has_calls(
[mock.call(task, states.POWER_OFF),
mock.call(task, states.POWER_ON)])
mock_clean_up_instance.assert_called_once_with(mock.ANY, task)
mock_unconf_tenant_net.assert_called_once_with(mock.ANY, task)
mock_add_rescue_net.assert_called_once_with(mock.ANY, task)
self.assertFalse(mock_build_agent_opts.called)
self.assertFalse(mock_prepare_ramdisk.called)
self.assertEqual(states.RESCUEWAIT, result)
@mock.patch.object(flat_network.FlatNetwork, 'remove_rescuing_network',
spec_set=True, autospec=True)
@mock.patch.object(flat_network.FlatNetwork, 'configure_tenant_networks',
spec_set=True, autospec=True)
@mock.patch.object(fake.FakeBoot, 'prepare_instance', autospec=True)
@mock.patch.object(fake.FakeBoot, 'clean_up_ramdisk', autospec=True)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
def test_agent_unrescue(self, mock_node_power_action, mock_clean_ramdisk,
mock_prepare_instance, mock_conf_tenant_net,
mock_remove_rescue_net):
"""Test unrescue in case where boot driver prepares instance reboot."""
self.config(manage_agent_boot=True, group='agent')
with task_manager.acquire(self.context, self.node.uuid) as task:
result = task.driver.rescue.unrescue(task)
mock_node_power_action.assert_has_calls(
[mock.call(task, states.POWER_OFF),
mock.call(task, states.POWER_ON)])
mock_clean_ramdisk.assert_called_once_with(
mock.ANY, task, mode='rescue')
mock_remove_rescue_net.assert_called_once_with(mock.ANY, task)
mock_conf_tenant_net.assert_called_once_with(mock.ANY, task)
mock_prepare_instance.assert_called_once_with(mock.ANY, task)
self.assertEqual(states.ACTIVE, result)
@mock.patch.object(flat_network.FlatNetwork, 'remove_rescuing_network',
spec_set=True, autospec=True)
@mock.patch.object(flat_network.FlatNetwork, 'configure_tenant_networks',
spec_set=True, autospec=True)
@mock.patch.object(fake.FakeBoot, 'prepare_instance', autospec=True)
@mock.patch.object(fake.FakeBoot, 'clean_up_ramdisk', autospec=True)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
def test_agent_unrescue_no_manage_agent_boot(self, mock_node_power_action,
mock_clean_ramdisk,
mock_prepare_instance,
mock_conf_tenant_net,
mock_remove_rescue_net):
"""Test unrescue in case where boot driver prepares instance reboot."""
self.config(manage_agent_boot=False, group='agent')
with task_manager.acquire(self.context, self.node.uuid) as task:
result = task.driver.rescue.unrescue(task)
mock_node_power_action.assert_has_calls(
[mock.call(task, states.POWER_OFF),
mock.call(task, states.POWER_ON)])
self.assertFalse(mock_clean_ramdisk.called)
mock_remove_rescue_net.assert_called_once_with(mock.ANY, task)
mock_conf_tenant_net.assert_called_once_with(mock.ANY, task)
mock_prepare_instance.assert_called_once_with(mock.ANY, task)
self.assertEqual(states.ACTIVE, result)
@mock.patch.object(neutron_common, 'validate_network', autospec=True)
@mock.patch.object(fake.FakeBoot, 'validate', autospec=True)
def test_agent_rescue_validate(self, mock_boot_validate,
mock_validate_network):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.rescue.validate(task)
self.assertFalse(mock_validate_network.called)
mock_boot_validate.assert_called_once_with(mock.ANY, task)
@mock.patch.object(neutron_common, 'validate_network', autospec=True)
@mock.patch.object(fake.FakeBoot, 'validate', autospec=True)
def test_agent_rescue_validate_neutron_net(self, mock_boot_validate,
mock_validate_network):
self.config(enabled_network_interfaces=['neutron'])
self.node.network_interface = 'neutron'
self.node.save()
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.rescue.validate(task)
mock_validate_network.assert_called_once_with(
CONF.neutron.rescuing_network, 'rescuing network',
context=task.context)
mock_boot_validate.assert_called_once_with(mock.ANY, task)
@mock.patch.object(neutron_common, 'validate_network', autospec=True)
@mock.patch.object(fake.FakeBoot, 'validate', autospec=True)
def test_agent_rescue_validate_no_manage_agent(self, mock_boot_validate,
mock_validate_network):
# If ironic's not managing booting of ramdisks, we don't set up PXE for
# the ramdisk/kernel, so validation can pass without this info
self.config(manage_agent_boot=False, group='agent')
driver_info = self.node.driver_info
del driver_info['rescue_ramdisk']
del driver_info['rescue_kernel']
self.node.driver_info = driver_info
self.node.save()
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.rescue.validate(task)
self.assertFalse(mock_validate_network.called)
self.assertFalse(mock_boot_validate.called)
@mock.patch.object(neutron_common, 'validate_network', autospec=True)
@mock.patch.object(fake.FakeBoot, 'validate', autospec=True)
def test_agent_rescue_validate_fails_no_rescue_ramdisk(
self, mock_boot_validate, mock_validate_network):
driver_info = self.node.driver_info
del driver_info['rescue_ramdisk']
self.node.driver_info = driver_info
self.node.save()
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.MissingParameterValue,
task.driver.rescue.validate, task)
self.assertFalse(mock_validate_network.called)
mock_boot_validate.assert_called_once_with(mock.ANY, task)
@mock.patch.object(neutron_common, 'validate_network', autospec=True)
@mock.patch.object(fake.FakeBoot, 'validate', autospec=True)
def test_agent_rescue_validate_fails_no_rescue_kernel(
self, mock_boot_validate, mock_validate_network):
driver_info = self.node.driver_info
del driver_info['rescue_kernel']
self.node.driver_info = driver_info
self.node.save()
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.MissingParameterValue,
task.driver.rescue.validate, task)
self.assertFalse(mock_validate_network.called)
mock_boot_validate.assert_called_once_with(mock.ANY, task)
@mock.patch.object(neutron_common, 'validate_network', autospec=True)
@mock.patch.object(fake.FakeBoot, 'validate', autospec=True)
def test_agent_rescue_validate_fails_no_rescue_password(
self, mock_boot_validate, mock_validate_network):
instance_info = self.node.instance_info
del instance_info['rescue_password']
self.node.instance_info = instance_info
self.node.save()
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.MissingParameterValue,
task.driver.rescue.validate, task)
self.assertFalse(mock_validate_network.called)
mock_boot_validate.assert_called_once_with(mock.ANY, task)
@mock.patch.object(neutron_common, 'validate_network', autospec=True)
@mock.patch.object(fake.FakeBoot, 'validate', autospec=True)
def test_agent_rescue_validate_fails_empty_rescue_password(
self, mock_boot_validate, mock_validate_network):
instance_info = self.node.instance_info
instance_info['rescue_password'] = " "
self.node.instance_info = instance_info
self.node.save()
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.InvalidParameterValue,
task.driver.rescue.validate, task)
self.assertFalse(mock_validate_network.called)
mock_boot_validate.assert_called_once_with(mock.ANY, task)
@mock.patch.object(neutron_common, 'validate_network', autospec=True)
@mock.patch.object(reflection, 'get_signature', autospec=True)
@mock.patch.object(fake.FakeBoot, 'validate', autospec=True)
def test_agent_rescue_validate_incompat_exc(self, mock_boot_validate,
mock_get_signature,
mock_validate_network):
mock_get_signature.return_value.parameters = ['task']
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.IncompatibleInterface,
task.driver.rescue.validate, task)
self.assertFalse(mock_validate_network.called)
self.assertFalse(mock_boot_validate.called)
@mock.patch.object(flat_network.FlatNetwork, 'remove_rescuing_network',
spec_set=True, autospec=True)
@mock.patch.object(fake.FakeBoot, 'clean_up_ramdisk', autospec=True)
def test_agent_rescue_clean_up(self, mock_clean_ramdisk,
mock_remove_rescue_net):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.rescue.clean_up(task)
self.assertNotIn('rescue_password', task.node.instance_info)
mock_clean_ramdisk.assert_called_once_with(
mock.ANY, task, mode='rescue')
mock_remove_rescue_net.assert_called_once_with(mock.ANY, task)
@mock.patch.object(flat_network.FlatNetwork, 'remove_rescuing_network',
spec_set=True, autospec=True)
@mock.patch.object(fake.FakeBoot, 'clean_up_ramdisk', autospec=True)
def test_agent_rescue_clean_up_no_manage_boot(self, mock_clean_ramdisk,
mock_remove_rescue_net):
self.config(manage_agent_boot=False, group='agent')
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.rescue.clean_up(task)
self.assertNotIn('rescue_password', task.node.instance_info)
self.assertFalse(mock_clean_ramdisk.called)
mock_remove_rescue_net.assert_called_once_with(mock.ANY, task)

View File

@ -24,14 +24,16 @@ from ironic.common import exception
from ironic.common import states from ironic.common import states
from ironic.conductor import task_manager from ironic.conductor import task_manager
from ironic.conductor import utils as manager_utils from ironic.conductor import utils as manager_utils
from ironic.drivers import base as drivers_base
from ironic.drivers.modules import agent
from ironic.drivers.modules import agent_base_vendor from ironic.drivers.modules import agent_base_vendor
from ironic.drivers.modules import agent_client from ironic.drivers.modules import agent_client
from ironic.drivers.modules import deploy_utils from ironic.drivers.modules import deploy_utils
from ironic.drivers.modules import fake from ironic.drivers.modules import fake
from ironic.drivers.modules.network import flat as flat_network
from ironic.drivers.modules import pxe from ironic.drivers.modules import pxe
from ironic.drivers import utils as driver_utils from ironic.drivers import utils as driver_utils
from ironic import objects from ironic import objects
from ironic.tests.unit.conductor import mgr_utils
from ironic.tests.unit.db import base as db_base from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.db import utils as db_utils from ironic.tests.unit.db import utils as db_utils
from ironic.tests.unit.objects import utils as object_utils from ironic.tests.unit.objects import utils as object_utils
@ -47,10 +49,23 @@ class AgentDeployMixinBaseTest(db_base.DbTestCase):
def setUp(self): def setUp(self):
super(AgentDeployMixinBaseTest, self).setUp() super(AgentDeployMixinBaseTest, self).setUp()
mgr_utils.mock_the_extension_manager(driver="fake_agent") for iface in drivers_base.ALL_INTERFACES:
impl = 'fake'
if iface == 'deploy':
impl = 'direct'
if iface == 'boot':
impl = 'pxe'
if iface == 'rescue':
impl = 'agent'
if iface == 'network':
continue
config_kwarg = {'enabled_%s_interfaces' % iface: [impl],
'default_%s_interface' % iface: impl}
self.config(**config_kwarg)
self.config(enabled_hardware_types=['fake-hardware'])
self.deploy = agent_base_vendor.AgentDeployMixin() self.deploy = agent_base_vendor.AgentDeployMixin()
n = { n = {
'driver': 'fake_agent', 'driver': 'fake-hardware',
'instance_info': INSTANCE_INFO, 'instance_info': INSTANCE_INFO,
'driver_info': DRIVER_INFO, 'driver_info': DRIVER_INFO,
'driver_internal_info': DRIVER_INTERNAL_INFO, 'driver_internal_info': DRIVER_INTERNAL_INFO,
@ -132,9 +147,9 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
failed_mock.assert_called_once_with( failed_mock.assert_called_once_with(
task, mock.ANY, collect_logs=True) task, mock.ANY, collect_logs=True)
log_mock.assert_called_once_with( log_mock.assert_called_once_with(
'Asynchronous exception for node ' 'Asynchronous exception: Failed checking if deploy is done. '
'1be26c0b-03f2-4d2e-ae87-c02d7f33c123: Failed checking if deploy ' 'Exception: LlamaException for node %(node)s',
'is done. Exception: LlamaException') {'node': '1be26c0b-03f2-4d2e-ae87-c02d7f33c123'})
@mock.patch.object(agent_base_vendor.HeartbeatMixin, @mock.patch.object(agent_base_vendor.HeartbeatMixin,
'deploy_has_started', autospec=True) 'deploy_has_started', autospec=True)
@ -164,9 +179,9 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
# deploy_utils.set_failed_state anymore # deploy_utils.set_failed_state anymore
self.assertFalse(failed_mock.called) self.assertFalse(failed_mock.called)
log_mock.assert_called_once_with( log_mock.assert_called_once_with(
'Asynchronous exception for node ' 'Asynchronous exception: Failed checking if deploy is done. '
'1be26c0b-03f2-4d2e-ae87-c02d7f33c123: Failed checking if deploy ' 'Exception: LlamaException for node %(node)s',
'is done. Exception: LlamaException') {'node': '1be26c0b-03f2-4d2e-ae87-c02d7f33c123'})
@mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True) @mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True)
@mock.patch.object(agent_base_vendor.HeartbeatMixin, @mock.patch.object(agent_base_vendor.HeartbeatMixin,
@ -265,6 +280,34 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
mock_continue.assert_called_once_with(mock.ANY, task) mock_continue.assert_called_once_with(mock.ANY, task)
mock_handler.assert_called_once_with(task, mock.ANY) mock_handler.assert_called_once_with(task, mock.ANY)
@mock.patch.object(agent_base_vendor.HeartbeatMixin, '_finalize_rescue',
autospec=True)
def test_heartbeat_rescue(self, mock_finalize_rescue):
self.node.provision_state = states.RESCUEWAIT
self.node.save()
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
self.deploy.heartbeat(task, 'http://127.0.0.1:8080', '1.0.0')
mock_finalize_rescue.assert_called_once_with(mock.ANY, task)
@mock.patch.object(manager_utils, 'rescuing_error_handler')
@mock.patch.object(agent_base_vendor.HeartbeatMixin, '_finalize_rescue',
autospec=True)
def test_heartbeat_rescue_fails(self, mock_finalize,
mock_rescue_err_handler):
self.node.provision_state = states.RESCUEWAIT
self.node.save()
mock_finalize.side_effect = Exception('some failure')
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
self.deploy.heartbeat(task, 'http://127.0.0.1:8080', '1.0.0')
mock_finalize.assert_called_once_with(mock.ANY, task)
mock_rescue_err_handler.assert_called_once_with(
task, 'Asynchronous exception: Node failed to perform '
'rescue operation. Exception: some failure for node')
@mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True) @mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True)
@mock.patch.object(agent_base_vendor.HeartbeatMixin, @mock.patch.object(agent_base_vendor.HeartbeatMixin,
'deploy_has_started', autospec=True) 'deploy_has_started', autospec=True)
@ -285,8 +328,100 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
mock_touch.assert_called_once_with(mock.ANY) mock_touch.assert_called_once_with(mock.ANY)
class AgentDeployMixinTest(AgentDeployMixinBaseTest): class AgentRescueTests(db_base.DbTestCase):
def setUp(self):
super(AgentRescueTests, self).setUp()
for iface in drivers_base.ALL_INTERFACES:
impl = 'fake'
if iface == 'deploy':
impl = 'direct'
if iface == 'boot':
impl = 'pxe'
if iface == 'rescue':
impl = 'agent'
if iface == 'network':
impl = 'flat'
config_kwarg = {'enabled_%s_interfaces' % iface: [impl],
'default_%s_interface' % iface: impl}
self.config(**config_kwarg)
self.config(enabled_hardware_types=['fake-hardware'])
instance_info = INSTANCE_INFO
driver_info = DRIVER_INFO
self.deploy = agent_base_vendor.AgentDeployMixin()
n = {
'driver': 'fake-hardware',
'instance_info': instance_info,
'driver_info': driver_info,
'driver_internal_info': DRIVER_INTERNAL_INFO,
}
self.node = object_utils.create_test_node(self.context, **n)
@mock.patch.object(flat_network.FlatNetwork, 'configure_tenant_networks',
spec_set=True, autospec=True)
@mock.patch.object(agent.AgentRescue, 'clean_up',
spec_set=True, autospec=True)
@mock.patch.object(agent_client.AgentClient, 'finalize_rescue',
spec=types.FunctionType)
def test__finalize_rescue(self, mock_finalize_rescue,
mock_clean_up, mock_conf_tenant_net):
node = self.node
node.provision_state = states.RESCUEWAIT
node.save()
mock_finalize_rescue.return_value = {'command_status': 'SUCCEEDED'}
with task_manager.acquire(self.context, self.node['uuid'],
shared=False) as task:
task.process_event = mock.Mock()
self.deploy._finalize_rescue(task)
mock_finalize_rescue.assert_called_once_with(task.node)
task.process_event.assert_has_calls([mock.call('resume'),
mock.call('done')])
mock_clean_up.assert_called_once_with(mock.ANY, task)
mock_conf_tenant_net.assert_called_once_with(mock.ANY, task)
@mock.patch.object(agent_client.AgentClient, 'finalize_rescue',
spec=types.FunctionType)
def test__finalize_rescue_bad_command_result(self, mock_finalize_rescue):
node = self.node
node.provision_state = states.RESCUEWAIT
node.save()
mock_finalize_rescue.return_value = {'command_status': 'FAILED',
'command_error': 'bad'}
with task_manager.acquire(self.context, self.node['uuid'],
shared=False) as task:
self.assertRaises(exception.InstanceRescueFailure,
self.deploy._finalize_rescue, task)
mock_finalize_rescue.assert_called_once_with(task.node)
@mock.patch.object(agent_client.AgentClient, 'finalize_rescue',
spec=types.FunctionType)
def test__finalize_rescue_exc(self, mock_finalize_rescue):
node = self.node
node.provision_state = states.RESCUEWAIT
node.save()
mock_finalize_rescue.side_effect = exception.IronicException("No pass")
with task_manager.acquire(self.context, self.node['uuid'],
shared=False) as task:
self.assertRaises(exception.InstanceRescueFailure,
self.deploy._finalize_rescue, task)
mock_finalize_rescue.assert_called_once_with(task.node)
@mock.patch.object(agent_client.AgentClient, 'finalize_rescue',
spec=types.FunctionType)
def test__finalize_rescue_missing_command_result(self,
mock_finalize_rescue):
node = self.node
node.provision_state = states.RESCUEWAIT
node.save()
mock_finalize_rescue.return_value = {}
with task_manager.acquire(self.context, self.node['uuid'],
shared=False) as task:
self.assertRaises(exception.InstanceRescueFailure,
self.deploy._finalize_rescue, task)
mock_finalize_rescue.assert_called_once_with(task.node)
class AgentDeployMixinTest(AgentDeployMixinBaseTest):
@mock.patch.object(driver_utils, 'collect_ramdisk_logs', autospec=True) @mock.patch.object(driver_utils, 'collect_ramdisk_logs', autospec=True)
@mock.patch.object(time, 'sleep', lambda seconds: None) @mock.patch.object(time, 'sleep', lambda seconds: None)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True) @mock.patch.object(manager_utils, 'node_power_action', autospec=True)

View File

@ -284,3 +284,22 @@ class TestAgentClient(base.TestCase):
self.client.sync(self.node) self.client.sync(self.node)
self.client._command.assert_called_once_with( self.client._command.assert_called_once_with(
node=self.node, method='standby.sync', params={}, wait=True) node=self.node, method='standby.sync', params={}, wait=True)
def test_finalize_rescue(self):
self.client._command = mock.MagicMock(spec_set=[])
self.node.instance_info['rescue_password'] = 'password'
expected_params = {
'rescue_password': 'password',
}
self.client.finalize_rescue(self.node)
self.client._command.assert_called_once_with(
node=self.node, method='rescue.finalize_rescue',
params=expected_params)
def test_finalize_rescue_exc(self):
# node does not have 'rescue_password' set in its 'instance_info'
self.client._command = mock.MagicMock(spec_set=[])
self.assertRaises(exception.IronicException,
self.client.finalize_rescue,
self.node)
self.assertFalse(self.client._command.called)

View File

@ -996,7 +996,7 @@ class CleanUpFullFlowTestCase(db_base.DbTestCase):
@mock.patch('ironic.common.dhcp_factory.DHCPFactory._set_dhcp_provider') @mock.patch('ironic.common.dhcp_factory.DHCPFactory._set_dhcp_provider')
@mock.patch('ironic.common.dhcp_factory.DHCPFactory.clean_dhcp') @mock.patch('ironic.common.dhcp_factory.DHCPFactory.clean_dhcp')
@mock.patch.object(pxe, '_get_instance_image_info', autospec=True) @mock.patch.object(pxe, '_get_instance_image_info', autospec=True)
@mock.patch.object(pxe, '_get_deploy_image_info', autospec=True) @mock.patch.object(pxe, '_get_image_info', autospec=True)
def test_clean_up_with_master(self, mock_get_deploy_image_info, def test_clean_up_with_master(self, mock_get_deploy_image_info,
mock_get_instance_image_info, mock_get_instance_image_info,
clean_dhcp_mock, set_dhcp_provider_mock): clean_dhcp_mock, set_dhcp_provider_mock):
@ -1010,7 +1010,8 @@ class CleanUpFullFlowTestCase(db_base.DbTestCase):
task.driver.deploy.clean_up(task) task.driver.deploy.clean_up(task)
mock_get_instance_image_info.assert_called_with(task.node, mock_get_instance_image_info.assert_called_with(task.node,
task.context) task.context)
mock_get_deploy_image_info.assert_called_with(task.node) mock_get_deploy_image_info.assert_called_with(task.node,
mode='deploy')
set_dhcp_provider_mock.assert_called_once_with() set_dhcp_provider_mock.assert_called_once_with()
clean_dhcp_mock.assert_called_once_with(task) clean_dhcp_mock.assert_called_once_with(task)
for path in ([self.kernel_path, self.image_path, self.config_path] for path in ([self.kernel_path, self.image_path, self.config_path]

View File

@ -63,21 +63,43 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
mgr_utils.mock_the_extension_manager(driver="fake_pxe") mgr_utils.mock_the_extension_manager(driver="fake_pxe")
self.node = obj_utils.create_test_node(self.context, **n) self.node = obj_utils.create_test_node(self.context, **n)
def test__parse_driver_info_missing_deploy_kernel(self): def _test__parse_driver_info_missing_kernel(self, mode='deploy'):
del self.node.driver_info['deploy_kernel'] del self.node.driver_info['%s_kernel' % mode]
if mode == 'rescue':
self.node.provision_state = states.RESCUING
self.assertRaises(exception.MissingParameterValue, self.assertRaises(exception.MissingParameterValue,
pxe._parse_driver_info, self.node) pxe._parse_driver_info, self.node, mode=mode)
def test__parse_driver_info_missing_deploy_kernel(self):
self._test__parse_driver_info_missing_kernel()
def test__parse_driver_info_missing_rescue_kernel(self):
self._test__parse_driver_info_missing_kernel(mode='rescue')
def _test__parse_driver_info_missing_ramdisk(self, mode='deploy'):
del self.node.driver_info['%s_ramdisk' % mode]
if mode == 'rescue':
self.node.provision_state = states.RESCUING
self.assertRaises(exception.MissingParameterValue,
pxe._parse_driver_info, self.node, mode=mode)
def test__parse_driver_info_missing_deploy_ramdisk(self): def test__parse_driver_info_missing_deploy_ramdisk(self):
del self.node.driver_info['deploy_ramdisk'] self._test__parse_driver_info_missing_ramdisk()
self.assertRaises(exception.MissingParameterValue,
pxe._parse_driver_info, self.node)
def test__parse_driver_info(self): def test__parse_driver_info_missing_rescue_ramdisk(self):
expected_info = {'deploy_ramdisk': 'glance://deploy_ramdisk_uuid', self._test__parse_driver_info_missing_ramdisk(mode='rescue')
'deploy_kernel': 'glance://deploy_kernel_uuid'}
image_info = pxe._parse_driver_info(self.node) def _test__parse_driver_info(self, mode='deploy'):
self.assertEqual(expected_info, image_info) exp_info = {'%s_ramdisk' % mode: 'glance://%s_ramdisk_uuid' % mode,
'%s_kernel' % mode: 'glance://%s_kernel_uuid' % mode}
image_info = pxe._parse_driver_info(self.node, mode=mode)
self.assertEqual(exp_info, image_info)
def test__parse_driver_info_deploy(self):
self._test__parse_driver_info()
def test__parse_driver_info_rescue(self):
self._test__parse_driver_info(mode='rescue')
def test__get_deploy_image_info(self): def test__get_deploy_image_info(self):
expected_info = {'deploy_ramdisk': expected_info = {'deploy_ramdisk':
@ -90,18 +112,18 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
os.path.join(CONF.pxe.tftp_root, os.path.join(CONF.pxe.tftp_root,
self.node.uuid, self.node.uuid,
'deploy_kernel'))} 'deploy_kernel'))}
image_info = pxe._get_deploy_image_info(self.node) image_info = pxe._get_image_info(self.node)
self.assertEqual(expected_info, image_info) self.assertEqual(expected_info, image_info)
def test__get_deploy_image_info_missing_deploy_kernel(self): def test__get_deploy_image_info_missing_deploy_kernel(self):
del self.node.driver_info['deploy_kernel'] del self.node.driver_info['deploy_kernel']
self.assertRaises(exception.MissingParameterValue, self.assertRaises(exception.MissingParameterValue,
pxe._get_deploy_image_info, self.node) pxe._get_image_info, self.node)
def test__get_deploy_image_info_deploy_ramdisk(self): def test__get_deploy_image_info_deploy_ramdisk(self):
del self.node.driver_info['deploy_ramdisk'] del self.node.driver_info['deploy_ramdisk']
self.assertRaises(exception.MissingParameterValue, self.assertRaises(exception.MissingParameterValue,
pxe._get_deploy_image_info, self.node) pxe._get_image_info, self.node)
@mock.patch.object(base_image_service.BaseImageService, '_show', @mock.patch.object(base_image_service.BaseImageService, '_show',
autospec=True) autospec=True)
@ -168,7 +190,7 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
@mock.patch('ironic.common.utils.render_template', autospec=True) @mock.patch('ironic.common.utils.render_template', autospec=True)
def _test_build_pxe_config_options_pxe(self, render_mock, def _test_build_pxe_config_options_pxe(self, render_mock,
whle_dsk_img=False, whle_dsk_img=False,
debug=False): debug=False, mode='deploy'):
self.config(debug=debug) self.config(debug=debug)
self.config(pxe_append_params='test_param', group='pxe') self.config(pxe_append_params='test_param', group='pxe')
# NOTE: right '/' should be removed from url string # NOTE: right '/' should be removed from url string
@ -181,21 +203,24 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
tftp_server = CONF.pxe.tftp_server tftp_server = CONF.pxe.tftp_server
deploy_kernel = os.path.join(self.node.uuid, 'deploy_kernel') kernel_label = '%s_kernel' % mode
deploy_ramdisk = os.path.join(self.node.uuid, 'deploy_ramdisk') ramdisk_label = '%s_ramdisk' % mode
pxe_kernel = os.path.join(self.node.uuid, kernel_label)
pxe_ramdisk = os.path.join(self.node.uuid, ramdisk_label)
kernel = os.path.join(self.node.uuid, 'kernel') kernel = os.path.join(self.node.uuid, 'kernel')
ramdisk = os.path.join(self.node.uuid, 'ramdisk') ramdisk = os.path.join(self.node.uuid, 'ramdisk')
root_dir = CONF.pxe.tftp_root root_dir = CONF.pxe.tftp_root
image_info = { image_info = {
'deploy_kernel': ('deploy_kernel', kernel_label: (kernel_label,
os.path.join(root_dir, os.path.join(root_dir,
self.node.uuid, self.node.uuid,
'deploy_kernel')), kernel_label)),
'deploy_ramdisk': ('deploy_ramdisk', ramdisk_label: (ramdisk_label,
os.path.join(root_dir, os.path.join(root_dir,
self.node.uuid, self.node.uuid,
'deploy_ramdisk')) ramdisk_label))
} }
if (whle_dsk_img or if (whle_dsk_img or
@ -219,15 +244,19 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
expected_pxe_params += ' ipa-debug=1' expected_pxe_params += ' ipa-debug=1'
expected_options = { expected_options = {
'ari_path': ramdisk, 'deployment_ari_path': pxe_ramdisk,
'deployment_ari_path': deploy_ramdisk,
'pxe_append_params': expected_pxe_params, 'pxe_append_params': expected_pxe_params,
'aki_path': kernel, 'deployment_aki_path': pxe_kernel,
'deployment_aki_path': deploy_kernel,
'tftp_server': tftp_server, 'tftp_server': tftp_server,
'ipxe_timeout': 0, 'ipxe_timeout': 0,
} }
if mode == 'deploy':
expected_options.update({'ari_path': ramdisk, 'aki_path': kernel})
elif mode == 'rescue':
self.node.provision_state = states.RESCUING
self.node.save()
with task_manager.acquire(self.context, self.node.uuid, with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task: shared=True) as task:
options = pxe._build_pxe_config_options(task, image_info) options = pxe._build_pxe_config_options(task, image_info)
@ -239,6 +268,14 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
def test__build_pxe_config_options_pxe_ipa_debug(self): def test__build_pxe_config_options_pxe_ipa_debug(self):
self._test_build_pxe_config_options_pxe(debug=True) self._test_build_pxe_config_options_pxe(debug=True)
def test__build_pxe_config_options_pxe_rescue(self):
del self.node.driver_internal_info['is_whole_disk_image']
self._test_build_pxe_config_options_pxe(mode='rescue')
def test__build_pxe_config_options_ipa_debug_rescue(self):
del self.node.driver_internal_info['is_whole_disk_image']
self._test_build_pxe_config_options_pxe(debug=True, mode='rescue')
def test__build_pxe_config_options_pxe_local_boot(self): def test__build_pxe_config_options_pxe_local_boot(self):
del self.node.driver_internal_info['is_whole_disk_image'] del self.node.driver_internal_info['is_whole_disk_image']
i_info = self.node.instance_info i_info = self.node.instance_info
@ -289,7 +326,8 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
ipxe_timeout=0, ipxe_timeout=0,
ipxe_use_swift=False, ipxe_use_swift=False,
debug=False, debug=False,
boot_from_volume=False): boot_from_volume=False,
mode='deploy'):
self.config(debug=debug) self.config(debug=debug)
self.config(pxe_append_params='test_param', group='pxe') self.config(pxe_append_params='test_param', group='pxe')
# NOTE: right '/' should be removed from url string # NOTE: right '/' should be removed from url string
@ -307,37 +345,41 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
http_url = 'http://192.1.2.3:1234' http_url = 'http://192.1.2.3:1234'
self.config(ipxe_enabled=True, group='pxe') self.config(ipxe_enabled=True, group='pxe')
self.config(http_url=http_url, group='deploy') self.config(http_url=http_url, group='deploy')
kernel_label = '%s_kernel' % mode
ramdisk_label = '%s_ramdisk' % mode
if ipxe_use_swift: if ipxe_use_swift:
self.config(ipxe_use_swift=True, group='pxe') self.config(ipxe_use_swift=True, group='pxe')
glance = mock.Mock() glance = mock.Mock()
glance_mock.return_value = glance glance_mock.return_value = glance
glance.swift_temp_url.side_effect = [ glance.swift_temp_url.side_effect = [
deploy_kernel, deploy_ramdisk] = [ pxe_kernel, pxe_ramdisk] = [
'swift_kernel', 'swift_ramdisk'] 'swift_kernel', 'swift_ramdisk']
image_info = { image_info = {
'deploy_kernel': (uuidutils.generate_uuid(), kernel_label: (uuidutils.generate_uuid(),
os.path.join(root_dir, os.path.join(root_dir,
self.node.uuid, self.node.uuid,
'deploy_kernel')), kernel_label)),
'deploy_ramdisk': (uuidutils.generate_uuid(), ramdisk_label: (uuidutils.generate_uuid(),
os.path.join(root_dir, os.path.join(root_dir,
self.node.uuid, self.node.uuid,
'deploy_ramdisk')) ramdisk_label))
} }
else: else:
deploy_kernel = os.path.join(http_url, self.node.uuid, pxe_kernel = os.path.join(http_url, self.node.uuid,
'deploy_kernel') kernel_label)
deploy_ramdisk = os.path.join(http_url, self.node.uuid, pxe_ramdisk = os.path.join(http_url, self.node.uuid,
'deploy_ramdisk') ramdisk_label)
image_info = { image_info = {
'deploy_kernel': ('deploy_kernel', kernel_label: (kernel_label,
os.path.join(root_dir, os.path.join(root_dir,
self.node.uuid, self.node.uuid,
'deploy_kernel')), kernel_label)),
'deploy_ramdisk': ('deploy_ramdisk', ramdisk_label: (ramdisk_label,
os.path.join(root_dir, os.path.join(root_dir,
self.node.uuid, self.node.uuid,
'deploy_ramdisk')) ramdisk_label))
} }
kernel = os.path.join(http_url, self.node.uuid, 'kernel') kernel = os.path.join(http_url, self.node.uuid, 'kernel')
@ -365,14 +407,17 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
expected_pxe_params += ' ipa-debug=1' expected_pxe_params += ' ipa-debug=1'
expected_options = { expected_options = {
'ari_path': ramdisk, 'deployment_ari_path': pxe_ramdisk,
'deployment_ari_path': deploy_ramdisk,
'pxe_append_params': expected_pxe_params, 'pxe_append_params': expected_pxe_params,
'aki_path': kernel, 'deployment_aki_path': pxe_kernel,
'deployment_aki_path': deploy_kernel,
'tftp_server': tftp_server, 'tftp_server': tftp_server,
'ipxe_timeout': ipxe_timeout_in_ms, 'ipxe_timeout': ipxe_timeout_in_ms,
} }
if mode == 'deploy':
expected_options.update({'ari_path': ramdisk, 'aki_path': kernel})
elif mode == 'rescue':
self.node.provision_state = states.RESCUING
self.node.save()
if boot_from_volume: if boot_from_volume:
expected_options.update({ expected_options.update({
@ -549,6 +594,17 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase):
options = pxe._get_volume_pxe_options(task) options = pxe._get_volume_pxe_options(task)
self.assertEqual([], options['iscsi_volumes']) self.assertEqual([], options['iscsi_volumes'])
def test__build_pxe_config_options_ipxe_rescue(self):
self._test_build_pxe_config_options_ipxe(mode='rescue')
def test__build_pxe_config_options_ipxe_rescue_swift(self):
self._test_build_pxe_config_options_ipxe(mode='rescue',
ipxe_use_swift=True)
def test__build_pxe_config_options_ipxe_rescue_timeout(self):
self._test_build_pxe_config_options_ipxe(mode='rescue',
ipxe_timeout=120)
@mock.patch.object(deploy_utils, 'fetch_images', autospec=True) @mock.patch.object(deploy_utils, 'fetch_images', autospec=True)
def test__cache_tftp_images_master_path(self, mock_fetch_image): def test__cache_tftp_images_master_path(self, mock_fetch_image):
temp_dir = tempfile.mkdtemp() temp_dir = tempfile.mkdtemp()
@ -823,7 +879,7 @@ class PXEBootTestCase(db_base.DbTestCase):
@mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True)
@mock.patch.object(dhcp_factory, 'DHCPFactory') @mock.patch.object(dhcp_factory, 'DHCPFactory')
@mock.patch.object(pxe, '_get_instance_image_info', autospec=True) @mock.patch.object(pxe, '_get_instance_image_info', autospec=True)
@mock.patch.object(pxe, '_get_deploy_image_info', autospec=True) @mock.patch.object(pxe, '_get_image_info', autospec=True)
@mock.patch.object(pxe, '_cache_ramdisk_kernel', autospec=True) @mock.patch.object(pxe, '_cache_ramdisk_kernel', autospec=True)
@mock.patch.object(pxe, '_build_pxe_config_options', autospec=True) @mock.patch.object(pxe, '_build_pxe_config_options', autospec=True)
@mock.patch.object(pxe_utils, 'create_pxe_config', autospec=True) @mock.patch.object(pxe_utils, 'create_pxe_config', autospec=True)
@ -836,9 +892,13 @@ class PXEBootTestCase(db_base.DbTestCase):
uefi=False, uefi=False,
cleaning=False, cleaning=False,
ipxe_use_swift=False, ipxe_use_swift=False,
whole_disk_image=False): whole_disk_image=False,
mode='deploy'):
mock_build_pxe.return_value = {} mock_build_pxe.return_value = {}
mock_deploy_img_info.return_value = {'deploy_kernel': 'a'} kernel_label = '%s_kernel' % mode
ramdisk_label = '%s_ramdisk' % mode
mock_deploy_img_info.return_value = {kernel_label: 'a',
ramdisk_label: 'r'}
if whole_disk_image: if whole_disk_image:
mock_instance_img_info.return_value = {} mock_instance_img_info.return_value = {}
else: else:
@ -850,11 +910,16 @@ class PXEBootTestCase(db_base.DbTestCase):
driver_internal_info = self.node.driver_internal_info driver_internal_info = self.node.driver_internal_info
driver_internal_info['is_whole_disk_image'] = whole_disk_image driver_internal_info['is_whole_disk_image'] = whole_disk_image
self.node.driver_internal_info = driver_internal_info self.node.driver_internal_info = driver_internal_info
if mode == 'rescue':
mock_deploy_img_info.return_value = {
'rescue_kernel': 'a',
'rescue_ramdisk': 'r'}
self.node.provision_state = states.RESCUING
self.node.save() self.node.save()
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
dhcp_opts = pxe_utils.dhcp_options_for_instance(task) dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
task.driver.boot.prepare_ramdisk(task, {'foo': 'bar'}) task.driver.boot.prepare_ramdisk(task, {'foo': 'bar'}, mode=mode)
mock_deploy_img_info.assert_called_once_with(task.node) mock_deploy_img_info.assert_called_once_with(task.node, mode=mode)
provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts)
set_boot_device_mock.assert_called_once_with(task, set_boot_device_mock.assert_called_once_with(task,
boot_devices.PXE, boot_devices.PXE,
@ -868,16 +933,21 @@ class PXEBootTestCase(db_base.DbTestCase):
{'kernel': 'b'}) {'kernel': 'b'})
mock_instance_img_info.assert_called_once_with(task.node, mock_instance_img_info.assert_called_once_with(task.node,
self.context) self.context)
elif cleaning is False: elif not cleaning and mode == 'deploy':
mock_cache_r_k.assert_called_once_with( mock_cache_r_k.assert_called_once_with(
self.context, task.node, self.context, task.node,
{'deploy_kernel': 'a', 'kernel': 'b'}) {'deploy_kernel': 'a', 'deploy_ramdisk': 'r',
'kernel': 'b'})
mock_instance_img_info.assert_called_once_with(task.node, mock_instance_img_info.assert_called_once_with(task.node,
self.context) self.context)
else: elif mode == 'deploy':
mock_cache_r_k.assert_called_once_with( mock_cache_r_k.assert_called_once_with(
self.context, task.node, self.context, task.node,
{'deploy_kernel': 'a'}) {'deploy_kernel': 'a', 'deploy_ramdisk': 'r'})
elif mode == 'rescue':
mock_cache_r_k.assert_called_once_with(
self.context, task.node,
{'rescue_kernel': 'a', 'rescue_ramdisk': 'r'})
if uefi: if uefi:
mock_pxe_config.assert_called_once_with( mock_pxe_config.assert_called_once_with(
task, {'foo': 'bar'}, CONF.pxe.uefi_pxe_config_template) task, {'foo': 'bar'}, CONF.pxe.uefi_pxe_config_template)
@ -890,6 +960,11 @@ class PXEBootTestCase(db_base.DbTestCase):
self.node.save() self.node.save()
self._test_prepare_ramdisk() self._test_prepare_ramdisk()
def test_prepare_ramdisk_rescue(self):
self.node.provision_state = states.RESCUING
self.node.save()
self._test_prepare_ramdisk(mode='rescue')
def test_prepare_ramdisk_uefi(self): def test_prepare_ramdisk_uefi(self):
self.node.provision_state = states.DEPLOYING self.node.provision_state = states.DEPLOYING
self.node.save() self.node.save()
@ -992,16 +1067,24 @@ class PXEBootTestCase(db_base.DbTestCase):
self._test_prepare_ramdisk(cleaning=True) self._test_prepare_ramdisk(cleaning=True)
@mock.patch.object(pxe, '_clean_up_pxe_env', autospec=True) @mock.patch.object(pxe, '_clean_up_pxe_env', autospec=True)
@mock.patch.object(pxe, '_get_deploy_image_info', autospec=True) @mock.patch.object(pxe, '_get_image_info', autospec=True)
def test_clean_up_ramdisk(self, get_deploy_image_info_mock, def _test_clean_up_ramdisk(self, get_image_info_mock,
clean_up_pxe_env_mock): clean_up_pxe_env_mock, mode='deploy'):
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
image_info = {'deploy_kernel': ['', '/path/to/deploy_kernel'], kernel_label = '%s_kernel' % mode
'deploy_ramdisk': ['', '/path/to/deploy_ramdisk']} ramdisk_label = '%s_ramdisk' % mode
get_deploy_image_info_mock.return_value = image_info image_info = {kernel_label: ['', '/path/to/' + kernel_label],
task.driver.boot.clean_up_ramdisk(task) ramdisk_label: ['', '/path/to/' + ramdisk_label]}
get_image_info_mock.return_value = image_info
task.driver.boot.clean_up_ramdisk(task, mode=mode)
clean_up_pxe_env_mock.assert_called_once_with(task, image_info) clean_up_pxe_env_mock.assert_called_once_with(task, image_info)
get_deploy_image_info_mock.assert_called_once_with(task.node) get_image_info_mock.assert_called_once_with(task.node, mode=mode)
def test_clean_up_ramdisk(self):
self._test_clean_up_ramdisk()
def test_clean_up_ramdisk_rescue(self):
self._test_clean_up_ramdisk(mode='rescue')
@mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True)
@mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True) @mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True)

View File

@ -61,6 +61,9 @@ class IPMIHardwareTestCase(db_base.DbTestCase):
self.assertIsInstance( self.assertIsInstance(
task.driver.storage, task.driver.storage,
kwargs.get('storage', noop_storage.NoopStorage)) kwargs.get('storage', noop_storage.NoopStorage))
self.assertIsInstance(
task.driver.rescue,
kwargs.get('rescue', noop.NoRescue))
def test_default_interfaces(self): def test_default_interfaces(self):
node = obj_utils.create_test_node(self.context, driver='ipmi') node = obj_utils.create_test_node(self.context, driver='ipmi')
@ -92,6 +95,14 @@ class IPMIHardwareTestCase(db_base.DbTestCase):
with task_manager.acquire(self.context, node.id) as task: with task_manager.acquire(self.context, node.id) as task:
self._validate_interfaces(task, storage=cinder.CinderStorage) self._validate_interfaces(task, storage=cinder.CinderStorage)
def test_override_with_agent_rescue(self):
self.config(enabled_rescue_interfaces=['agent'])
node = obj_utils.create_test_node(
self.context, driver='ipmi',
rescue_interface='agent')
with task_manager.acquire(self.context, node.id) as task:
self._validate_interfaces(task, rescue=agent.AgentRescue)
class IPMIClassicDriversTestCase(testtools.TestCase): class IPMIClassicDriversTestCase(testtools.TestCase):

View File

@ -154,6 +154,7 @@ ironic.hardware.interfaces.raid =
no-raid = ironic.drivers.modules.noop:NoRAID no-raid = ironic.drivers.modules.noop:NoRAID
ironic.hardware.interfaces.rescue = ironic.hardware.interfaces.rescue =
agent = ironic.drivers.modules.agent:AgentRescue
fake = ironic.drivers.modules.fake:FakeRescue fake = ironic.drivers.modules.fake:FakeRescue
no-rescue = ironic.drivers.modules.noop:NoRescue no-rescue = ironic.drivers.modules.noop:NoRescue