Merge "Generalize clean step functions to support deploy steps"

This commit is contained in:
Zuul 2020-03-28 20:51:49 +00:00 committed by Gerrit Code Review
commit d927dd0d0a
7 changed files with 561 additions and 316 deletions

View File

@ -444,7 +444,7 @@ 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 deploying_error_handler(task, logmsg, errmsg, traceback=False, def deploying_error_handler(task, logmsg, errmsg=None, traceback=False,
clean_up=True): clean_up=True):
"""Put a failed node in DEPLOYFAIL. """Put a failed node in DEPLOYFAIL.
@ -454,6 +454,7 @@ def deploying_error_handler(task, logmsg, errmsg, traceback=False,
:param traceback: Boolean; True to log a traceback :param traceback: Boolean; True to log a traceback
:param clean_up: Boolean; True to clean up :param clean_up: Boolean; True to clean up
""" """
errmsg = errmsg or logmsg
node = task.node node = task.node
LOG.error(logmsg, exc_info=traceback) LOG.error(logmsg, exc_info=traceback)
node.last_error = errmsg node.last_error = errmsg
@ -755,15 +756,15 @@ def validate_instance_info_traits(node):
raise exception.InvalidParameterValue(err) raise exception.InvalidParameterValue(err)
def _notify_conductor_resume_operation(task, operation, method): def notify_conductor_resume_operation(task, operation):
"""Notify the conductor to resume an operation. """Notify the conductor to resume an operation.
:param task: the task :param task: the task
:param operation: the operation, a string :param operation: the operation, a string
:param method: The name of the RPC method, a string
""" """
LOG.debug('Sending RPC to conductor to resume %(op)s for node %(node)s', LOG.debug('Sending RPC to conductor to resume %(op)s steps for node '
{'op': operation, 'node': task.node.uuid}) '%(node)s', {'op': operation, 'node': task.node.uuid})
method = 'continue_node_%s' % operation
from ironic.conductor import rpcapi from ironic.conductor import rpcapi
uuid = task.node.uuid uuid = task.node.uuid
rpc = rpcapi.ConductorAPI() rpc = rpcapi.ConductorAPI()
@ -774,12 +775,11 @@ def _notify_conductor_resume_operation(task, operation, method):
def notify_conductor_resume_clean(task): def notify_conductor_resume_clean(task):
_notify_conductor_resume_operation(task, 'cleaning', 'continue_node_clean') notify_conductor_resume_operation(task, 'clean')
def notify_conductor_resume_deploy(task): def notify_conductor_resume_deploy(task):
_notify_conductor_resume_operation(task, 'deploying', notify_conductor_resume_operation(task, 'deploy')
'continue_node_deploy')
def skip_automated_cleaning(node): def skip_automated_cleaning(node):

View File

@ -42,26 +42,27 @@ LOG = log.getLogger(__name__)
METRICS = metrics_utils.get_metrics_logger(__name__) METRICS = metrics_utils.get_metrics_logger(__name__)
# This contains a nested dictionary containing the post clean step # This contains a nested dictionary containing the post clean/deploy step hooks
# hooks registered for each clean step of every interface. # registered for each clean/deploy step of every interface.
# Every key of POST_CLEAN_STEP_HOOKS is an interface and its value # Every key is an interface and its value is a dictionary. For this inner
# is a dictionary. For this inner dictionary, the key is the name of # dictionary, the key is the name of the clean-/deploy-step method in the
# the clean-step method in the interface, and the value is the post # interface, and the value is the post clean-/deploy-step hook -- the function
# clean-step hook -- the function that is to be called after successful # that is to be called after successful completion of the clean/deploy step.
# completion of the clean step.
# #
# For example: # For example:
# POST_CLEAN_STEP_HOOKS = # _POST_STEP_HOOKS = {
# {'clean':
# { # {
# 'raid': {'create_configuration': <post-create function>, # 'raid': {'create_configuration': <post-create function>,
# 'delete_configuration': <post-delete function>} # 'delete_configuration': <post-delete function>}
# } # }
# }
# #
# It means that method '<post-create function>' is to be called after # It means that method '<post-create function>' is to be called after
# successfully completing the clean step 'create_configuration' of # successfully completing the clean step 'create_configuration' of
# raid interface. '<post-delete function>' is to be called after # raid interface. '<post-delete function>' is to be called after
# completing 'delete_configuration' of raid interface. # completing 'delete_configuration' of raid interface.
POST_CLEAN_STEP_HOOKS = {} _POST_STEP_HOOKS = {'clean': {}, 'deploy': {}}
VENDOR_PROPERTIES = { VENDOR_PROPERTIES = {
'deploy_forces_oob_reboot': _( 'deploy_forces_oob_reboot': _(
@ -114,39 +115,70 @@ def post_clean_step_hook(interface, step):
step hook. step hook.
""" """
def decorator(func): def decorator(func):
POST_CLEAN_STEP_HOOKS.setdefault(interface, {})[step] = func _POST_STEP_HOOKS['clean'].setdefault(interface, {})[step] = func
return func return func
return decorator return decorator
def _get_post_clean_step_hook(node): @METRICS.timer('post_deploy_step_hook')
"""Get post clean step hook for the currently executing clean step. def post_deploy_step_hook(interface, step):
"""Decorator method for adding a post deploy step hook.
This method reads node.clean_step and returns the post clean This is a mechanism for adding a post deploy step hook for a particular
step hook for the currently executing clean step. deploy step. The hook will get executed after the deploy step gets
executed successfully. The hook is not invoked on failure of the deploy
step.
Any method to be made as a hook may be decorated with
@post_deploy_step_hook mentioning the interface and step after which the
hook should be executed. A TaskManager instance and the object for the
last completed command (provided by agent) will be passed to the hook
method. The return value of this method will be ignored. Any exception
raised by this method will be treated as a failure of the deploy step and
the node will be moved to DEPLOYFAIL state.
:param interface: name of the interface
:param step: The name of the step after which it should be executed.
:returns: A method which registers the given method as a post deploy
step hook.
"""
def decorator(func):
_POST_STEP_HOOKS['deploy'].setdefault(interface, {})[step] = func
return func
return decorator
def _get_post_step_hook(node, step_type):
"""Get post clean/deploy step hook for the currently executing step.
:param node: a node object :param node: a node object
:param step_type: 'clean' or 'deploy'
:returns: a method if there is a post clean step hook for this clean :returns: a method if there is a post clean step hook for this clean
step; None otherwise step; None otherwise
""" """
interface = node.clean_step.get('interface') step_obj = node.clean_step if step_type == 'clean' else node.deploy_step
step = node.clean_step.get('step') interface = step_obj.get('interface')
step = step_obj.get('step')
try: try:
return POST_CLEAN_STEP_HOOKS[interface][step] return _POST_STEP_HOOKS[step_type][interface][step]
except KeyError: except KeyError:
pass pass
def _cleaning_reboot(task): def _post_step_reboot(task, step_type):
"""Reboots a node out of band after a clean step that requires it. """Reboots a node out of band after a clean/deploy step that requires it.
If an agent clean step has 'reboot_requested': True, reboots the If an agent step has 'reboot_requested': True, reboots the node when
node when the step is completed. Will put the node in CLEANFAIL the step is completed. Will put the node in CLEANFAIL/DEPLOYFAIL if
if the node cannot be rebooted. the node cannot be rebooted.
:param task: a TaskManager instance :param task: a TaskManager instance
:param step_type: 'clean' or 'deploy'
""" """
current_step = (task.node.clean_step if step_type == 'clean'
else task.node.deploy_step)
try: try:
# NOTE(fellypefca): Call prepare_ramdisk on ensure that the # NOTE(fellypefca): Call prepare_ramdisk on ensure that the
# baremetal node boots back into the ramdisk after reboot. # baremetal node boots back into the ramdisk after reboot.
@ -154,19 +186,25 @@ def _cleaning_reboot(task):
task.driver.boot.prepare_ramdisk(task, deploy_opts) task.driver.boot.prepare_ramdisk(task, deploy_opts)
manager_utils.node_power_action(task, states.REBOOT) manager_utils.node_power_action(task, states.REBOOT)
except Exception as e: except Exception as e:
msg = (_('Reboot requested by clean step %(step)s failed for ' msg = (_('Reboot requested by %(type)s step %(step)s failed for '
'node %(node)s: %(err)s') % 'node %(node)s: %(err)s') %
{'step': task.node.clean_step, {'step': current_step,
'node': task.node.uuid, 'node': task.node.uuid,
'err': e}) 'err': e,
'type': step_type})
LOG.error(msg, exc_info=not isinstance(e, exception.IronicException)) LOG.error(msg, exc_info=not isinstance(e, exception.IronicException))
# do not set cleaning_reboot if we didn't reboot # do not set cleaning_reboot if we didn't reboot
if step_type == 'clean':
manager_utils.cleaning_error_handler(task, msg) manager_utils.cleaning_error_handler(task, msg)
else:
manager_utils.deploying_error_handler(task, msg)
return return
# Signify that we've rebooted # Signify that we've rebooted
driver_internal_info = task.node.driver_internal_info driver_internal_info = task.node.driver_internal_info
driver_internal_info['cleaning_reboot'] = True field = ('cleaning_reboot' if step_type == 'clean'
else 'deployment_reboot')
driver_internal_info[field] = True
if not driver_internal_info.get('agent_secret_token_pregenerated', False): if not driver_internal_info.get('agent_secret_token_pregenerated', False):
# Wipes out the existing recorded token because the machine will # Wipes out the existing recorded token because the machine will
# need to re-establish the token. # need to re-establish the token.
@ -175,8 +213,8 @@ def _cleaning_reboot(task):
task.node.save() task.node.save()
def _get_completed_cleaning_command(task, commands): def _get_completed_command(task, commands, step_type):
"""Returns None or a completed cleaning command from the agent. """Returns None or a completed clean/deploy command from the agent.
:param task: a TaskManager instance to act on. :param task: a TaskManager instance to act on.
:param commands: a set of command results from the agent, typically :param commands: a set of command results from the agent, typically
@ -187,28 +225,32 @@ def _get_completed_cleaning_command(task, commands):
last_command = commands[-1] last_command = commands[-1]
if last_command['command_name'] != 'execute_clean_step': if last_command['command_name'] != 'execute_%s_step' % step_type:
# catches race condition where execute_clean_step is still # catches race condition where execute_step is still
# processing so the command hasn't started yet # processing so the command hasn't started yet
LOG.debug('Expected agent last command to be "execute_clean_step" ' LOG.debug('Expected agent last command to be "execute_%(type)s_step" '
'for node %(node)s, instead got "%(command)s". Waiting ' 'for node %(node)s, instead got "%(command)s". Waiting '
'for next heartbeat.', 'for next heartbeat.',
{'node': task.node.uuid, {'node': task.node.uuid,
'command': last_command['command_name']}) 'command': last_command['command_name'],
'type': step_type})
return return
last_result = last_command.get('command_result') or {} last_result = last_command.get('command_result') or {}
last_step = last_result.get('clean_step') last_step = last_result.get('%s_step' % step_type)
current_step = (task.node.clean_step if step_type == 'clean'
else task.node.deploy_step)
if last_command['command_status'] == 'RUNNING': if last_command['command_status'] == 'RUNNING':
LOG.debug('Clean step still running for node %(node)s: %(step)s', LOG.debug('%(type)s step still running for node %(node)s: %(step)s',
{'step': last_step, 'node': task.node.uuid}) {'step': last_step, 'node': task.node.uuid,
'type': step_type.capitalize()})
return return
elif (last_command['command_status'] == 'SUCCEEDED' elif (last_command['command_status'] == 'SUCCEEDED'
and last_step != task.node.clean_step): and last_step != current_step):
# A previous clean_step was running, the new command has not yet # A previous step was running, the new command has not yet started.
# started. LOG.debug('%(type)s step not yet started for node %(node)s: %(step)s',
LOG.debug('Clean step not yet started for node %(node)s: %(step)s', {'step': last_step, 'node': task.node.uuid,
{'step': last_step, 'node': task.node.uuid}) 'type': step_type.capitalize()})
return return
else: else:
return last_command return last_command
@ -233,32 +275,29 @@ def log_and_raise_deployment_error(task, msg, collect_logs=True, exc=None):
raise exception.InstanceDeployFailure(msg) raise exception.InstanceDeployFailure(msg)
def get_clean_steps(task, interface=None, override_priorities=None): def get_steps(task, step_type, interface=None, override_priorities=None):
"""Get the list of cached clean steps from the agent. """Get the list of cached clean or deploy steps from the agent.
#TODO(JoshNang) move to BootInterface The steps cache is updated at the beginning of cleaning or deploy.
The clean steps cache is updated at the beginning of cleaning.
:param task: a TaskManager object containing the node :param task: a TaskManager object containing the node
:param interface: The interface for which clean steps :param step_type: 'clean' or 'deploy'
:param interface: The interface for which clean/deploy steps
are to be returned. If this is not provided, it returns the are to be returned. If this is not provided, it returns the
clean steps for all interfaces. steps for all interfaces.
:param override_priorities: a dictionary with keys being step names and :param override_priorities: a dictionary with keys being step names and
values being new priorities for them. If a step isn't in this values being new priorities for them. If a step isn't in this
dictionary, the step's original priority is used. dictionary, the step's original priority is used.
:raises NodeCleaningFailure: if the clean steps are not yet cached, :returns: A list of clean/deploy step dictionaries
for example, when a node has just been enrolled and has not been
cleaned yet.
:returns: A list of clean step dictionaries
""" """
node = task.node node = task.node
try: try:
all_steps = node.driver_internal_info['agent_cached_clean_steps'] all_steps = node.driver_internal_info['agent_cached_%s_steps'
% step_type]
except KeyError: except KeyError:
raise exception.NodeCleaningFailure(_('Cleaning steps are not yet ' LOG.debug('%(type)s steps are not yet available for node %(node)s',
'available for node %(node)s') {'type': step_type.capitalize(), 'node': node.uuid})
% {'node': node.uuid}) return []
if interface: if interface:
steps = [step.copy() for step in all_steps.get(interface, [])] steps = [step.copy() for step in all_steps.get(interface, [])]
@ -277,26 +316,40 @@ def get_clean_steps(task, interface=None, override_priorities=None):
return steps return steps
def execute_clean_step(task, step): def _raise(step_type, msg):
"""Execute a clean step asynchronously on the agent. assert step_type in ('clean', 'deploy')
exc = (exception.NodeCleaningFailure if step_type == 'clean'
else exception.InstanceDeployFailure)
raise exc(msg)
#TODO(JoshNang) move to BootInterface
def execute_step(task, step, step_type):
"""Execute a clean or deploy step asynchronously on the agent.
:param task: a TaskManager object containing the node :param task: a TaskManager object containing the node
:param step: a clean step dictionary to execute :param step: a step dictionary to execute
:raises: NodeCleaningFailure if the agent does not return a command status :param step_type: 'clean' or 'deploy'
:returns: states.CLEANWAIT to signify the step will be completed async :raises: NodeCleaningFailure (clean step) or InstanceDeployFailure (deploy
step) if the agent does not return a command status.
:returns: states.CLEANWAIT/DEPLOYWAIT to signify the step will be
completed async
""" """
client = _get_client() client = _get_client()
ports = objects.Port.list_by_node_id( ports = objects.Port.list_by_node_id(
task.context, task.node.id) task.context, task.node.id)
result = client.execute_clean_step(step, task.node, ports) call = getattr(client, 'execute_%s_step' % step_type)
result = call(step, task.node, ports)
if not result.get('command_status'): if not result.get('command_status'):
raise exception.NodeCleaningFailure(_( _raise(step_type, _(
'Agent on node %(node)s returned bad command result: ' 'Agent on node %(node)s returned bad command result: '
'%(result)s') % {'node': task.node.uuid, '%(result)s') % {'node': task.node.uuid,
'result': result.get('command_error')}) 'result': result.get('command_error')})
return states.CLEANWAIT return states.CLEANWAIT if step_type == 'clean' else states.DEPLOYWAIT
def execute_clean_step(task, step):
# NOTE(dtantsur): left for compatibility with agent-based hardware types.
return execute_step(task, step, 'clean')
class HeartbeatMixin(object): class HeartbeatMixin(object):
@ -346,19 +399,33 @@ class HeartbeatMixin(object):
""" """
def refresh_steps(self, task, step_type):
"""Refresh the node's cached clean steps
:param task: a TaskManager instance
:param step_type: "clean" or "deploy"
"""
def refresh_clean_steps(self, task): def refresh_clean_steps(self, task):
"""Refresh the node's cached clean steps """Refresh the node's cached clean steps
:param task: a TaskManager instance :param task: a TaskManager instance
"""
return self.refresh_steps(task, 'clean')
def process_next_step(self, task, step_type):
"""Start the next clean/deploy step if the previous one is complete.
:param task: a TaskManager instance
:param step_type: "clean" or "deploy"
""" """
def continue_cleaning(self, task): def continue_cleaning(self, task):
"""Start the next cleaning step if the previous one is complete. """Start the next cleaning step if the previous one is complete.
:param task: a TaskManager instance :param task: a TaskManager instance
""" """
return self.process_next_step(task, 'clean')
@property @property
def heartbeat_allowed_states(self): def heartbeat_allowed_states(self):
@ -555,53 +622,60 @@ class AgentDeployMixin(HeartbeatMixin):
'erase_devices_metadata': 'erase_devices_metadata':
CONF.deploy.erase_devices_metadata_priority, CONF.deploy.erase_devices_metadata_priority,
} }
return get_clean_steps( return get_steps(
task, interface='deploy', task, 'clean', interface='deploy',
override_priorities=new_priorities) override_priorities=new_priorities)
@METRICS.timer('AgentDeployMixin.refresh_clean_steps') @METRICS.timer('AgentDeployMixin.refresh_steps')
def refresh_clean_steps(self, task): def refresh_steps(self, task, step_type):
"""Refresh the node's cached clean steps from the booted agent. """Refresh the node's cached clean/deploy steps from the booted agent.
Gets the node's clean steps from the booted agent and caches them. Gets the node's steps from the booted agent and caches them.
The steps are cached to make get_clean_steps() calls synchronous, and The steps are cached to make get_clean_steps() calls synchronous, and
should be refreshed as soon as the agent boots to start cleaning or should be refreshed as soon as the agent boots to start cleaning/deploy
if cleaning is restarted because of a cleaning version mismatch. or if cleaning is restarted because of a hardware manager version
mismatch.
:param task: a TaskManager instance :param task: a TaskManager instance
:raises: NodeCleaningFailure if the agent returns invalid results :param step_type: 'clean' or 'deploy'
:raises: NodeCleaningFailure or InstanceDeployFailure if the agent
returns invalid results
""" """
node = task.node node = task.node
previous_steps = node.driver_internal_info.get( previous_steps = node.driver_internal_info.get(
'agent_cached_clean_steps') 'agent_cached_%s_steps' % step_type)
LOG.debug('Refreshing agent clean step cache for node %(node)s. ' LOG.debug('Refreshing agent %(type)s step cache for node %(node)s. '
'Previously cached steps: %(steps)s', 'Previously cached steps: %(steps)s',
{'node': node.uuid, 'steps': previous_steps}) {'node': node.uuid, 'type': step_type,
'steps': previous_steps})
agent_result = self._client.get_clean_steps(node, task.ports).get( call = getattr(self._client, 'get_%s_steps' % step_type)
'command_result', {}) agent_result = call(node, task.ports).get('command_result', {})
missing = set(['clean_steps', 'hardware_manager_version']).difference( missing = set(['%s_steps' % step_type,
agent_result) 'hardware_manager_version']).difference(agent_result)
if missing: if missing:
raise exception.NodeCleaningFailure(_( _raise(step_type, _(
'agent get_clean_steps for node %(node)s returned an invalid ' 'agent get_%(type)s_steps for node %(node)s returned an '
'result. Keys: %(keys)s are missing from result: %(result)s.') 'invalid result. Keys: %(keys)s are missing from result: '
'%(result)s.')
% ({'node': node.uuid, 'keys': missing, % ({'node': node.uuid, 'keys': missing,
'result': agent_result})) 'result': agent_result, 'type': step_type}))
# agent_result['clean_steps'] looks like # agent_result['clean_steps'] looks like
# {'HardwareManager': [{step1},{steps2}...], ...} # {'HardwareManager': [{step1},{steps2}...], ...}
steps = collections.defaultdict(list) steps = collections.defaultdict(list)
for step_list in agent_result['clean_steps'].values(): for step_list in agent_result['%s_steps' % step_type].values():
for step in step_list: for step in step_list:
missing = set(['interface', 'step', 'priority']).difference( missing = set(['interface', 'step', 'priority']).difference(
step) step)
if missing: if missing:
raise exception.NodeCleaningFailure(_( _raise(step_type, _(
'agent get_clean_steps for node %(node)s returned an ' 'agent get_%(type)s_steps for node %(node)s returned '
'invalid clean step. Keys: %(keys)s are missing from ' 'an invalid %(type)s step. Keys: %(keys)s are missing'
'step: %(step)s.') % ({'node': node.uuid, 'from step: %(step)s.') % ({'node': node.uuid,
'keys': missing, 'step': step})) 'keys': missing,
'step': step,
'type': step_type}))
steps[step['interface']].append(step) steps[step['interface']].append(step)
@ -609,12 +683,14 @@ class AgentDeployMixin(HeartbeatMixin):
info = node.driver_internal_info info = node.driver_internal_info
info['hardware_manager_version'] = agent_result[ info['hardware_manager_version'] = agent_result[
'hardware_manager_version'] 'hardware_manager_version']
info['agent_cached_clean_steps'] = dict(steps) info['agent_cached_%s_steps' % step_type] = dict(steps)
info['agent_cached_clean_steps_refreshed'] = str(timeutils.utcnow()) info['agent_cached_%s_steps_refreshed' % step_type] = str(
timeutils.utcnow())
node.driver_internal_info = info node.driver_internal_info = info
node.save() node.save()
LOG.debug('Refreshed agent clean step cache for node %(node)s: ' LOG.debug('Refreshed agent %(type)s step cache for node %(node)s: '
'%(steps)s', {'node': node.uuid, 'steps': steps}) '%(steps)s', {'node': node.uuid, 'steps': steps,
'type': step_type})
@METRICS.timer('AgentDeployMixin.execute_clean_step') @METRICS.timer('AgentDeployMixin.execute_clean_step')
def execute_clean_step(self, task, step): def execute_clean_step(self, task, step):
@ -626,24 +702,27 @@ class AgentDeployMixin(HeartbeatMixin):
status status
:returns: states.CLEANWAIT to signify the step will be completed async :returns: states.CLEANWAIT to signify the step will be completed async
""" """
return execute_clean_step(task, step) return execute_step(task, step, 'clean')
@METRICS.timer('AgentDeployMixin.continue_cleaning') @METRICS.timer('AgentDeployMixin.process_next_step')
def continue_cleaning(self, task, **kwargs): def process_next_step(self, task, step_type, **kwargs):
"""Start the next cleaning step if the previous one is complete. """Start the next clean/deploy step if the previous one is complete.
In order to avoid errors and make agent upgrades painless, the agent In order to avoid errors and make agent upgrades painless, the agent
compares the version of all hardware managers at the start of the compares the version of all hardware managers at the start of the
cleaning (the agent's get_clean_steps() call) and before executing process (the agent's get_clean|deploy_steps() call) and before
each clean step. If the version has changed between steps, the agent is executing each step. If the version has changed between steps,
unable to tell if an ordering change will cause a cleaning issue so the agent is unable to tell if an ordering change will cause an issue
it returns CLEAN_VERSION_MISMATCH. For automated cleaning, we restart so it returns CLEAN_VERSION_MISMATCH. For automated cleaning, we
the entire cleaning cycle. For manual cleaning, we don't. restart the entire cleaning cycle. For manual cleaning or deploy,
we don't.
Additionally, if a clean_step includes the reboot_requested property Additionally, if a step includes the reboot_requested property
set to True, this method will coordinate the reboot once the step is set to True, this method will coordinate the reboot once the step is
completed. completed.
""" """
assert step_type in ('clean', 'deploy')
node = task.node node = task.node
# For manual clean, the target provision state is MANAGEABLE, whereas # For manual clean, the target provision state is MANAGEABLE, whereas
# for automated cleaning, it is (the default) AVAILABLE. # for automated cleaning, it is (the default) AVAILABLE.
@ -651,47 +730,61 @@ class AgentDeployMixin(HeartbeatMixin):
agent_commands = self._client.get_commands_status(task.node) agent_commands = self._client.get_commands_status(task.node)
if not agent_commands: if not agent_commands:
if task.node.driver_internal_info.get('cleaning_reboot'): field = ('cleaning_reboot' if step_type == 'clean'
else 'deployment_reboot')
if task.node.driver_internal_info.get(field):
# Node finished a cleaning step that requested a reboot, and # Node finished a cleaning step that requested a reboot, and
# this is the first heartbeat after booting. Continue cleaning. # this is the first heartbeat after booting. Continue cleaning.
info = task.node.driver_internal_info info = task.node.driver_internal_info
info.pop('cleaning_reboot', None) info.pop(field, None)
task.node.driver_internal_info = info task.node.driver_internal_info = info
task.node.save() task.node.save()
manager_utils.notify_conductor_resume_clean(task) manager_utils.notify_conductor_resume_operation(task,
step_type)
return return
else: else:
# Agent has no commands whatsoever # Agent has no commands whatsoever
return return
command = _get_completed_cleaning_command(task, agent_commands) current_step = (node.clean_step if step_type == 'clean'
LOG.debug('Cleaning command status for node %(node)s on step %(step)s:' else node.deploy_step)
command = _get_completed_command(task, agent_commands, step_type)
LOG.debug('%(type)s command status for node %(node)s on step %(step)s:'
' %(command)s', {'node': node.uuid, ' %(command)s', {'node': node.uuid,
'step': node.clean_step, 'step': current_step,
'command': command}) 'command': command,
'type': step_type})
if not command: if not command:
# Agent command in progress # Agent command in progress
return return
if command.get('command_status') == 'FAILED': if command.get('command_status') == 'FAILED':
msg = (_('Agent returned error for clean step %(step)s on node ' msg = (_('Agent returned error for %(type)s step %(step)s on node '
'%(node)s : %(err)s.') % '%(node)s : %(err)s.') %
{'node': node.uuid, {'node': node.uuid,
'err': command.get('command_error'), 'err': command.get('command_error'),
'step': node.clean_step}) 'step': current_step,
'type': step_type})
LOG.error(msg) LOG.error(msg)
return manager_utils.cleaning_error_handler(task, msg) return manager_utils.cleaning_error_handler(task, msg)
elif command.get('command_status') == 'CLEAN_VERSION_MISMATCH': elif command.get('command_status') in ('CLEAN_VERSION_MISMATCH',
'DEPLOY_VERSION_MISMATCH'):
# Cache the new clean steps (and 'hardware_manager_version') # Cache the new clean steps (and 'hardware_manager_version')
try: try:
self.refresh_clean_steps(task) self.refresh_steps(task, step_type)
except exception.NodeCleaningFailure as e: except exception.NodeCleaningFailure as e:
msg = (_('Could not continue cleaning on node ' msg = (_('Could not continue cleaning on node '
'%(node)s: %(err)s.') % '%(node)s: %(err)s.') %
{'node': node.uuid, 'err': e}) {'node': node.uuid, 'err': e})
LOG.exception(msg) LOG.exception(msg)
return manager_utils.cleaning_error_handler(task, msg) return manager_utils.cleaning_error_handler(task, msg)
except exception.InstanceDeployFailure as e:
msg = (_('Could not continue deployment on node '
'%(node)s: %(err)s.') %
{'node': node.uuid, 'err': e})
LOG.exception(msg)
return manager_utils.deploying_error_handler(task, msg)
if manual_clean: if manual_clean:
# Don't restart manual cleaning if agent reboots to a new # Don't restart manual cleaning if agent reboots to a new
@ -708,60 +801,77 @@ class AgentDeployMixin(HeartbeatMixin):
node.driver_internal_info = driver_internal_info node.driver_internal_info = driver_internal_info
node.save() node.save()
else: else:
# Restart cleaning, agent must have rebooted to new version # Restart the process, agent must have rebooted to new version
LOG.info('During automated cleaning, node %s detected a ' LOG.info('During %(type)s, node %(node)s detected a '
'clean version mismatch. Resetting clean steps ' '%(type)s version mismatch. Resetting %(type)s steps '
'and rebooting the node.', node.uuid) 'and rebooting the node.',
{'type': step_type, 'node': node.uuid})
try: try:
conductor_steps.set_node_cleaning_steps(task) conductor_steps.set_node_cleaning_steps(task)
except exception.NodeCleaningFailure: except exception.NodeCleaningFailure as e:
msg = (_('Could not restart automated cleaning on node ' msg = (_('Could not restart automated cleaning on node '
'%(node)s: %(err)s.') % '%(node)s after step %(step)s: %(err)s.') %
{'node': node.uuid, {'node': node.uuid, 'err': e,
'err': command.get('command_error'),
'step': node.clean_step}) 'step': node.clean_step})
LOG.exception(msg) LOG.exception(msg)
return manager_utils.cleaning_error_handler(task, msg) return manager_utils.cleaning_error_handler(task, msg)
except exception.InstanceDeployFailure as e:
msg = (_('Could not restart deployment on node '
'%(node)s after step %(step)s: %(err)s.') %
{'node': node.uuid, 'err': e,
'step': node.deploy_step})
LOG.exception(msg)
return manager_utils.deploying_error_handler(task, msg)
manager_utils.notify_conductor_resume_clean(task) manager_utils.notify_conductor_resume_operation(task, step_type)
elif command.get('command_status') == 'SUCCEEDED': elif command.get('command_status') == 'SUCCEEDED':
clean_step_hook = _get_post_clean_step_hook(node) step_hook = _get_post_step_hook(node, step_type)
if clean_step_hook is not None: if step_hook is not None:
LOG.debug('For node %(node)s, executing post clean step ' LOG.debug('For node %(node)s, executing post %(type)s step '
'hook %(method)s for clean step %(step)s', 'hook %(method)s for %(type)s step %(step)s',
{'method': clean_step_hook.__name__, {'method': step_hook.__name__,
'node': node.uuid, 'node': node.uuid,
'step': node.clean_step}) 'step': current_step,
'type': step_type})
try: try:
clean_step_hook(task, command) step_hook(task, command)
except Exception as e: except Exception as e:
msg = (_('For node %(node)s, post clean step hook ' msg = (_('For node %(node)s, post %(type)s step hook '
'%(method)s failed for clean step %(step)s.' '%(method)s failed for %(type)s step %(step)s.'
'%(cls)s: %(error)s') % '%(cls)s: %(error)s') %
{'method': clean_step_hook.__name__, {'method': step_hook.__name__,
'node': node.uuid, 'node': node.uuid,
'error': e, 'error': e,
'cls': e.__class__.__name__, 'cls': e.__class__.__name__,
'step': node.clean_step}) 'step': current_step,
'type': step_type})
LOG.exception(msg) LOG.exception(msg)
if step_type == 'clean':
return manager_utils.cleaning_error_handler(task, msg) return manager_utils.cleaning_error_handler(task, msg)
else:
return manager_utils.deploying_error_handler(task, msg)
if task.node.clean_step.get('reboot_requested'): if current_step.get('reboot_requested'):
_cleaning_reboot(task) _post_step_reboot(task, step_type)
return return
LOG.info('Agent on node %s returned cleaning command success, ' LOG.info('Agent on node %(node)s returned %(type)s command '
'moving to next clean step', node.uuid) 'success, moving to next step',
manager_utils.notify_conductor_resume_clean(task) {'node': node.uuid, 'type': step_type})
manager_utils.notify_conductor_resume_operation(task, step_type)
else: else:
msg = (_('Agent returned unknown status for clean step %(step)s ' msg = (_('Agent returned unknown status for %(type)s step %(step)s'
' on node %(node)s : %(err)s.') % ' on node %(node)s : %(err)s.') %
{'node': node.uuid, {'node': node.uuid,
'err': command.get('command_status'), 'err': command.get('command_status'),
'step': node.clean_step}) 'step': current_step,
'type': step_type})
LOG.error(msg) LOG.error(msg)
if step_type == 'clean':
return manager_utils.cleaning_error_handler(task, msg) return manager_utils.cleaning_error_handler(task, msg)
else:
return manager_utils.deploying_error_handler(task, msg)
@METRICS.timer('AgentDeployMixin.reboot_and_finish_deploy') @METRICS.timer('AgentDeployMixin.reboot_and_finish_deploy')
def reboot_and_finish_deploy(self, task): def reboot_and_finish_deploy(self, task):

View File

@ -163,6 +163,9 @@ class AgentClient(object):
* a dictionary containing keys clean_result * a dictionary containing keys clean_result
and clean_step for the command and clean_step for the command
clean.execute_clean_step; clean.execute_clean_step;
* a dictionary containing keys deploy_result
and deploy_step for the command
deploy.execute_deploy_step;
* a string representing result message for * a string representing result message for
the command standby.cache_image; the command standby.cache_image;
* None for the command standby.sync.> * None for the command standby.sync.>
@ -336,6 +339,69 @@ class AgentClient(object):
method='clean.execute_clean_step', method='clean.execute_clean_step',
params=params) params=params)
@METRICS.timer('AgentClient.get_deploy_steps')
def get_deploy_steps(self, node, ports):
"""Get deploy steps from agent.
:param node: A node object.
:param ports: Ports associated with the node.
:raises: IronicException when failed to issue the request or there was
a malformed response from the agent.
:raises: AgentAPIError when agent failed to execute specified command.
:returns: A dict containing command response from agent.
See :func:`get_commands_status` for a command result sample.
The value of key command_result is in the form of:
::
{
'deploy_steps': <a list of deploy steps>,
'hardware_manager_version': <manager version>
}
"""
params = {
'node': node.as_dict(secure=True),
'ports': [port.as_dict() for port in ports]
}
return self._command(node=node,
method='deploy.get_deploy_steps',
params=params,
wait=True)
@METRICS.timer('AgentClient.execute_deploy_step')
def execute_deploy_step(self, step, node, ports):
"""Execute specified deploy step.
:param step: A deploy step dictionary to execute.
:param node: A Node object.
:param ports: Ports associated with the node.
:raises: IronicException when failed to issue the request or there was
a malformed response from the agent.
:raises: AgentAPIError when agent failed to execute specified command.
:returns: A dict containing command response from agent.
See :func:`get_commands_status` for a command result sample.
The value of key command_result is in the form of:
::
{
'deploy_result': <the result of execution, step specific>,
'deploy_step': <the deploy step issued to agent>
}
"""
params = {
'step': step,
'node': node.as_dict(secure=True),
'ports': [port.as_dict() for port in ports],
'deploy_version': node.driver_internal_info.get(
'hardware_manager_version')
}
return self._command(node=node,
method='deploy.execute_deploy_step',
params=params)
@METRICS.timer('AgentClient.power_off') @METRICS.timer('AgentClient.power_off')
def power_off(self, node): def power_off(self, node):
"""Soft powers off the bare metal node by shutting down ramdisk OS. """Soft powers off the bare metal node by shutting down ramdisk OS.

View File

@ -1689,33 +1689,30 @@ class MiscTestCase(db_base.DbTestCase):
@mock.patch.object(rpcapi.ConductorAPI, 'continue_node_deploy', @mock.patch.object(rpcapi.ConductorAPI, 'continue_node_deploy',
autospec=True) autospec=True)
@mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for', autospec=True) @mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for', autospec=True)
def test__notify_conductor_resume_operation(self, mock_topic, def test_notify_conductor_resume_operation(self, mock_topic,
mock_rpc_call): mock_rpc_call):
mock_topic.return_value = 'topic' mock_topic.return_value = 'topic'
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:
conductor_utils._notify_conductor_resume_operation( conductor_utils.notify_conductor_resume_operation(task, 'deploy')
task, 'deploying', 'continue_node_deploy')
mock_rpc_call.assert_called_once_with( mock_rpc_call.assert_called_once_with(
mock.ANY, task.context, self.node.uuid, topic='topic') mock.ANY, task.context, self.node.uuid, topic='topic')
@mock.patch.object(conductor_utils, '_notify_conductor_resume_operation', @mock.patch.object(conductor_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
def test_notify_conductor_resume_clean(self, mock_resume): def test_notify_conductor_resume_clean(self, mock_resume):
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:
conductor_utils.notify_conductor_resume_clean(task) conductor_utils.notify_conductor_resume_clean(task)
mock_resume.assert_called_once_with( mock_resume.assert_called_once_with(task, 'clean')
task, 'cleaning', 'continue_node_clean')
@mock.patch.object(conductor_utils, '_notify_conductor_resume_operation', @mock.patch.object(conductor_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
def test_notify_conductor_resume_deploy(self, mock_resume): def test_notify_conductor_resume_deploy(self, mock_resume):
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:
conductor_utils.notify_conductor_resume_deploy(task) conductor_utils.notify_conductor_resume_deploy(task)
mock_resume.assert_called_once_with( mock_resume.assert_called_once_with(task, 'deploy')
task, 'deploying', 'continue_node_deploy')
@mock.patch.object(time, 'sleep', autospec=True) @mock.patch.object(time, 'sleep', autospec=True)
@mock.patch.object(fake.FakePower, 'get_power_state', autospec=True) @mock.patch.object(fake.FakePower, 'get_power_state', autospec=True)

View File

@ -1042,33 +1042,33 @@ class TestAgentDeploy(db_base.DbTestCase):
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)
@mock.patch.object(agent_base, 'get_clean_steps', autospec=True) @mock.patch.object(agent_base, 'get_steps', autospec=True)
def test_get_clean_steps(self, mock_get_clean_steps): def test_get_clean_steps(self, mock_get_steps):
# Test getting clean steps # Test getting clean steps
mock_steps = [{'priority': 10, 'interface': 'deploy', mock_steps = [{'priority': 10, 'interface': 'deploy',
'step': 'erase_devices'}] 'step': 'erase_devices'}]
mock_get_clean_steps.return_value = mock_steps mock_get_steps.return_value = mock_steps
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
steps = self.driver.get_clean_steps(task) steps = self.driver.get_clean_steps(task)
mock_get_clean_steps.assert_called_once_with( mock_get_steps.assert_called_once_with(
task, interface='deploy', task, 'clean', interface='deploy',
override_priorities={'erase_devices': None, override_priorities={'erase_devices': None,
'erase_devices_metadata': None}) 'erase_devices_metadata': None})
self.assertEqual(mock_steps, steps) self.assertEqual(mock_steps, steps)
@mock.patch.object(agent_base, 'get_clean_steps', autospec=True) @mock.patch.object(agent_base, 'get_steps', autospec=True)
def test_get_clean_steps_config_priority(self, mock_get_clean_steps): def test_get_clean_steps_config_priority(self, mock_get_steps):
# Test that we can override the priority of get clean steps # Test that we can override the priority of get clean steps
# Use 0 because it is an edge case (false-y) and used in devstack # Use 0 because it is an edge case (false-y) and used in devstack
self.config(erase_devices_priority=0, group='deploy') self.config(erase_devices_priority=0, group='deploy')
self.config(erase_devices_metadata_priority=0, group='deploy') self.config(erase_devices_metadata_priority=0, group='deploy')
mock_steps = [{'priority': 10, 'interface': 'deploy', mock_steps = [{'priority': 10, 'interface': 'deploy',
'step': 'erase_devices'}] 'step': 'erase_devices'}]
mock_get_clean_steps.return_value = mock_steps mock_get_steps.return_value = mock_steps
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
self.driver.get_clean_steps(task) self.driver.get_clean_steps(task)
mock_get_clean_steps.assert_called_once_with( mock_get_steps.assert_called_once_with(
task, interface='deploy', task, 'clean', interface='deploy',
override_priorities={'erase_devices': 0, override_priorities={'erase_devices': 0,
'erase_devices_metadata': 0}) 'erase_devices_metadata': 0})
@ -1774,7 +1774,7 @@ class AgentRAIDTestCase(db_base.DbTestCase):
} }
self.node = object_utils.create_test_node(self.context, **n) self.node = object_utils.create_test_node(self.context, **n)
@mock.patch.object(agent_base, 'get_clean_steps', autospec=True) @mock.patch.object(agent_base, 'get_steps', autospec=True)
def test_get_clean_steps(self, get_steps_mock): def test_get_clean_steps(self, get_steps_mock):
get_steps_mock.return_value = [ get_steps_mock.return_value = [
{'step': 'create_configuration', 'interface': 'raid', {'step': 'create_configuration', 'interface': 'raid',
@ -1789,7 +1789,7 @@ class AgentRAIDTestCase(db_base.DbTestCase):
self.assertEqual(0, ret[1]['priority']) self.assertEqual(0, ret[1]['priority'])
@mock.patch.object(raid, 'filter_target_raid_config') @mock.patch.object(raid, 'filter_target_raid_config')
@mock.patch.object(agent_base, 'execute_clean_step', autospec=True) @mock.patch.object(agent_base, 'execute_step', autospec=True)
def test_create_configuration(self, execute_mock, def test_create_configuration(self, execute_mock,
filter_target_raid_config_mock): filter_target_raid_config_mock):
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
@ -1802,10 +1802,11 @@ class AgentRAIDTestCase(db_base.DbTestCase):
self.assertEqual( self.assertEqual(
self.target_raid_config, self.target_raid_config,
task.node.driver_internal_info['target_raid_config']) task.node.driver_internal_info['target_raid_config'])
execute_mock.assert_called_once_with(task, self.clean_step) execute_mock.assert_called_once_with(task, self.clean_step,
'clean')
@mock.patch.object(raid, 'filter_target_raid_config') @mock.patch.object(raid, 'filter_target_raid_config')
@mock.patch.object(agent_base, 'execute_clean_step', autospec=True) @mock.patch.object(agent_base, 'execute_step', autospec=True)
def test_create_configuration_skip_root(self, execute_mock, def test_create_configuration_skip_root(self, execute_mock,
filter_target_raid_config_mock): filter_target_raid_config_mock):
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
@ -1819,13 +1820,14 @@ class AgentRAIDTestCase(db_base.DbTestCase):
return_value = task.driver.raid.create_configuration( return_value = task.driver.raid.create_configuration(
task, create_root_volume=False) task, create_root_volume=False)
self.assertEqual(states.CLEANWAIT, return_value) self.assertEqual(states.CLEANWAIT, return_value)
execute_mock.assert_called_once_with(task, self.clean_step) execute_mock.assert_called_once_with(task, self.clean_step,
'clean')
self.assertEqual( self.assertEqual(
exp_target_raid_config, exp_target_raid_config,
task.node.driver_internal_info['target_raid_config']) task.node.driver_internal_info['target_raid_config'])
@mock.patch.object(raid, 'filter_target_raid_config') @mock.patch.object(raid, 'filter_target_raid_config')
@mock.patch.object(agent_base, 'execute_clean_step', autospec=True) @mock.patch.object(agent_base, 'execute_step', autospec=True)
def test_create_configuration_skip_nonroot(self, execute_mock, def test_create_configuration_skip_nonroot(self, execute_mock,
filter_target_raid_config_mock): filter_target_raid_config_mock):
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
@ -1839,13 +1841,14 @@ class AgentRAIDTestCase(db_base.DbTestCase):
return_value = task.driver.raid.create_configuration( return_value = task.driver.raid.create_configuration(
task, create_nonroot_volumes=False) task, create_nonroot_volumes=False)
self.assertEqual(states.CLEANWAIT, return_value) self.assertEqual(states.CLEANWAIT, return_value)
execute_mock.assert_called_once_with(task, self.clean_step) execute_mock.assert_called_once_with(task, self.clean_step,
'clean')
self.assertEqual( self.assertEqual(
exp_target_raid_config, exp_target_raid_config,
task.node.driver_internal_info['target_raid_config']) task.node.driver_internal_info['target_raid_config'])
@mock.patch.object(raid, 'filter_target_raid_config') @mock.patch.object(raid, 'filter_target_raid_config')
@mock.patch.object(agent_base, 'execute_clean_step', autospec=True) @mock.patch.object(agent_base, 'execute_step', autospec=True)
def test_create_configuration_no_target_raid_config_after_skipping( def test_create_configuration_no_target_raid_config_after_skipping(
self, execute_mock, filter_target_raid_config_mock): self, execute_mock, filter_target_raid_config_mock):
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
@ -1860,7 +1863,7 @@ class AgentRAIDTestCase(db_base.DbTestCase):
self.assertFalse(execute_mock.called) self.assertFalse(execute_mock.called)
@mock.patch.object(raid, 'filter_target_raid_config') @mock.patch.object(raid, 'filter_target_raid_config')
@mock.patch.object(agent_base, 'execute_clean_step', autospec=True) @mock.patch.object(agent_base, 'execute_step', autospec=True)
def test_create_configuration_empty_target_raid_config( def test_create_configuration_empty_target_raid_config(
self, execute_mock, filter_target_raid_config_mock): self, execute_mock, filter_target_raid_config_mock):
execute_mock.return_value = states.CLEANING execute_mock.return_value = states.CLEANING
@ -1890,7 +1893,7 @@ class AgentRAIDTestCase(db_base.DbTestCase):
self.node.clean_step = {'interface': 'raid', self.node.clean_step = {'interface': 'raid',
'step': 'create_configuration'} 'step': 'create_configuration'}
command = {'command_result': {'clean_result': 'foo'}} command = {'command_result': {'clean_result': 'foo'}}
create_hook = agent_base._get_post_clean_step_hook(self.node) create_hook = agent_base._get_post_step_hook(self.node, 'clean')
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
create_hook(task, command) create_hook(task, command)
update_raid_info_mock.assert_called_once_with(task.node, 'foo') update_raid_info_mock.assert_called_once_with(task.node, 'foo')
@ -1906,13 +1909,14 @@ class AgentRAIDTestCase(db_base.DbTestCase):
task, command) task, command)
self.assertFalse(update_raid_info_mock.called) self.assertFalse(update_raid_info_mock.called)
@mock.patch.object(agent_base, 'execute_clean_step', autospec=True) @mock.patch.object(agent_base, 'execute_step', autospec=True)
def test_delete_configuration(self, execute_mock): def test_delete_configuration(self, execute_mock):
execute_mock.return_value = states.CLEANING execute_mock.return_value = states.CLEANING
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
return_value = task.driver.raid.delete_configuration(task) return_value = task.driver.raid.delete_configuration(task)
execute_mock.assert_called_once_with(task, self.clean_step) execute_mock.assert_called_once_with(task, self.clean_step,
'clean')
self.assertEqual(states.CLEANING, return_value) self.assertEqual(states.CLEANING, return_value)
def test__delete_configuration_final(self): def test__delete_configuration_final(self):
@ -1931,7 +1935,7 @@ class AgentRAIDTestCase(db_base.DbTestCase):
'step': 'delete_configuration'} 'step': 'delete_configuration'}
self.node.raid_config = {'foo': 'bar'} self.node.raid_config = {'foo': 'bar'}
command = {'command_result': {'clean_result': 'foo'}} command = {'command_result': {'clean_result': 'foo'}}
delete_hook = agent_base._get_post_clean_step_hook(self.node) delete_hook = agent_base._get_post_step_hook(self.node, 'clean')
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
delete_hook(task, command) delete_hook(task, command)

View File

@ -222,7 +222,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
autospec=True) autospec=True)
@mock.patch.object(agent_base.HeartbeatMixin, @mock.patch.object(agent_base.HeartbeatMixin,
'reboot_to_instance', autospec=True) 'reboot_to_instance', autospec=True)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
def test_heartbeat_in_maintenance(self, ncrc_mock, rti_mock, cd_mock): def test_heartbeat_in_maintenance(self, ncrc_mock, rti_mock, cd_mock):
# NOTE(pas-ha) checking only for states that are not noop # NOTE(pas-ha) checking only for states that are not noop
@ -253,7 +253,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
autospec=True) autospec=True)
@mock.patch.object(agent_base.HeartbeatMixin, @mock.patch.object(agent_base.HeartbeatMixin,
'reboot_to_instance', autospec=True) 'reboot_to_instance', autospec=True)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
def test_heartbeat_in_maintenance_abort(self, ncrc_mock, rti_mock, def test_heartbeat_in_maintenance_abort(self, ncrc_mock, rti_mock,
cd_mock): cd_mock):
@ -289,7 +289,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
autospec=True) autospec=True)
@mock.patch.object(agent_base.HeartbeatMixin, @mock.patch.object(agent_base.HeartbeatMixin,
'reboot_to_instance', autospec=True) 'reboot_to_instance', autospec=True)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
def test_heartbeat_with_reservation(self, ncrc_mock, rti_mock, cd_mock): def test_heartbeat_with_reservation(self, ncrc_mock, rti_mock, cd_mock):
# NOTE(pas-ha) checking only for states that are not noop # NOTE(pas-ha) checking only for states that are not noop
@ -316,7 +316,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
autospec=True) autospec=True)
@mock.patch.object(agent_base.HeartbeatMixin, @mock.patch.object(agent_base.HeartbeatMixin,
'reboot_to_instance', autospec=True) 'reboot_to_instance', autospec=True)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
def test_heartbeat_noops_in_wrong_state(self, ncrc_mock, rti_mock, def test_heartbeat_noops_in_wrong_state(self, ncrc_mock, rti_mock,
cd_mock, log_mock): cd_mock, log_mock):
@ -343,7 +343,7 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
autospec=True) autospec=True)
@mock.patch.object(agent_base.HeartbeatMixin, @mock.patch.object(agent_base.HeartbeatMixin,
'reboot_to_instance', autospec=True) 'reboot_to_instance', autospec=True)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
def test_heartbeat_noops_in_wrong_state2(self, ncrc_mock, rti_mock, def test_heartbeat_noops_in_wrong_state2(self, ncrc_mock, rti_mock,
cd_mock): cd_mock):
@ -426,10 +426,10 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
@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.HeartbeatMixin, @mock.patch.object(agent_base.HeartbeatMixin,
'refresh_clean_steps', autospec=True) 'refresh_steps', autospec=True)
@mock.patch.object(conductor_steps, 'set_node_cleaning_steps', @mock.patch.object(conductor_steps, 'set_node_cleaning_steps',
autospec=True) autospec=True)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
def test_heartbeat_resume_clean(self, mock_notify, mock_set_steps, def test_heartbeat_resume_clean(self, mock_notify, mock_set_steps,
mock_refresh, mock_touch): mock_refresh, mock_touch):
@ -441,17 +441,17 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
self.deploy.heartbeat(task, 'http://127.0.0.1:8080', '1.0.0') self.deploy.heartbeat(task, 'http://127.0.0.1:8080', '1.0.0')
mock_touch.assert_called_once_with(mock.ANY) mock_touch.assert_called_once_with(mock.ANY)
mock_refresh.assert_called_once_with(mock.ANY, task) mock_refresh.assert_called_once_with(mock.ANY, task, 'clean')
mock_notify.assert_called_once_with(task) mock_notify.assert_called_once_with(task, 'clean')
mock_set_steps.assert_called_once_with(task) mock_set_steps.assert_called_once_with(task)
@mock.patch.object(manager_utils, 'cleaning_error_handler') @mock.patch.object(manager_utils, 'cleaning_error_handler')
@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.HeartbeatMixin, @mock.patch.object(agent_base.HeartbeatMixin,
'refresh_clean_steps', autospec=True) 'refresh_steps', autospec=True)
@mock.patch.object(conductor_steps, 'set_node_cleaning_steps', @mock.patch.object(conductor_steps, 'set_node_cleaning_steps',
autospec=True) autospec=True)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
def test_heartbeat_resume_clean_fails(self, mock_notify, mock_set_steps, def test_heartbeat_resume_clean_fails(self, mock_notify, mock_set_steps,
mock_refresh, mock_touch, mock_refresh, mock_touch,
@ -1496,7 +1496,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
self.assertFalse(prepare_mock.called) self.assertFalse(prepare_mock.called)
self.assertFalse(failed_state_mock.called) self.assertFalse(failed_state_mock.called)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
@mock.patch.object(agent_client.AgentClient, 'get_commands_status', @mock.patch.object(agent_client.AgentClient, 'get_commands_status',
autospec=True) autospec=True)
@ -1519,19 +1519,20 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
with task_manager.acquire(self.context, self.node['uuid'], with task_manager.acquire(self.context, self.node['uuid'],
shared=False) as task: shared=False) as task:
self.deploy.continue_cleaning(task) self.deploy.continue_cleaning(task)
notify_mock.assert_called_once_with(task) notify_mock.assert_called_once_with(task, 'clean')
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True) @mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', spec_set=True, @mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', spec_set=True,
autospec=True) autospec=True)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True) @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
def test__cleaning_reboot(self, mock_reboot, mock_prepare, mock_build_opt): def test__post_step_reboot(self, mock_reboot, mock_prepare,
mock_build_opt):
with task_manager.acquire(self.context, self.node['uuid'], with task_manager.acquire(self.context, self.node['uuid'],
shared=False) as task: shared=False) as task:
i_info = task.node.driver_internal_info i_info = task.node.driver_internal_info
i_info['agent_secret_token'] = 'magicvalue01' i_info['agent_secret_token'] = 'magicvalue01'
task.node.driver_internal_info = i_info task.node.driver_internal_info = i_info
agent_base._cleaning_reboot(task) agent_base._post_step_reboot(task, 'clean')
self.assertTrue(mock_build_opt.called) self.assertTrue(mock_build_opt.called)
self.assertTrue(mock_prepare.called) self.assertTrue(mock_prepare.called)
mock_reboot.assert_called_once_with(task, states.REBOOT) mock_reboot.assert_called_once_with(task, states.REBOOT)
@ -1543,7 +1544,27 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', spec_set=True, @mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', spec_set=True,
autospec=True) autospec=True)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True) @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
def test__cleaning_reboot_pregenerated_token( def test__post_step_reboot_deploy(self, mock_reboot, mock_prepare,
mock_build_opt):
with task_manager.acquire(self.context, self.node['uuid'],
shared=False) as task:
i_info = task.node.driver_internal_info
i_info['agent_secret_token'] = 'magicvalue01'
task.node.driver_internal_info = i_info
agent_base._post_step_reboot(task, 'deploy')
self.assertTrue(mock_build_opt.called)
self.assertTrue(mock_prepare.called)
mock_reboot.assert_called_once_with(task, states.REBOOT)
self.assertTrue(
task.node.driver_internal_info['deployment_reboot'])
self.assertNotIn('agent_secret_token',
task.node.driver_internal_info)
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', spec_set=True,
autospec=True)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
def test__post_step_reboot_pregenerated_token(
self, mock_reboot, mock_prepare, mock_build_opt): self, mock_reboot, mock_prepare, mock_build_opt):
with task_manager.acquire(self.context, self.node['uuid'], with task_manager.acquire(self.context, self.node['uuid'],
shared=False) as task: shared=False) as task:
@ -1551,7 +1572,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
i_info['agent_secret_token'] = 'magicvalue01' i_info['agent_secret_token'] = 'magicvalue01'
i_info['agent_secret_token_pregenerated'] = True i_info['agent_secret_token_pregenerated'] = True
task.node.driver_internal_info = i_info task.node.driver_internal_info = i_info
agent_base._cleaning_reboot(task) agent_base._post_step_reboot(task, 'clean')
self.assertTrue(mock_build_opt.called) self.assertTrue(mock_build_opt.called)
self.assertTrue(mock_prepare.called) self.assertTrue(mock_prepare.called)
mock_reboot.assert_called_once_with(task, states.REBOOT) mock_reboot.assert_called_once_with(task, states.REBOOT)
@ -1563,18 +1584,35 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
autospec=True) autospec=True)
@mock.patch.object(manager_utils, 'cleaning_error_handler', autospec=True) @mock.patch.object(manager_utils, 'cleaning_error_handler', autospec=True)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True) @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
def test__cleaning_reboot_fail(self, mock_reboot, mock_handler, def test__post_step_reboot_fail(self, mock_reboot, mock_handler,
mock_prepare, mock_build_opt): mock_prepare, mock_build_opt):
mock_reboot.side_effect = RuntimeError("broken") mock_reboot.side_effect = RuntimeError("broken")
with task_manager.acquire(self.context, self.node['uuid'], with task_manager.acquire(self.context, self.node['uuid'],
shared=False) as task: shared=False) as task:
agent_base._cleaning_reboot(task) agent_base._post_step_reboot(task, 'clean')
mock_reboot.assert_called_once_with(task, states.REBOOT) mock_reboot.assert_called_once_with(task, states.REBOOT)
mock_handler.assert_called_once_with(task, mock.ANY) mock_handler.assert_called_once_with(task, mock.ANY)
self.assertNotIn('cleaning_reboot', self.assertNotIn('cleaning_reboot',
task.node.driver_internal_info) task.node.driver_internal_info)
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', spec_set=True,
autospec=True)
@mock.patch.object(manager_utils, 'deploying_error_handler', autospec=True)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
def test__post_step_reboot_fail_deploy(self, mock_reboot, mock_handler,
mock_prepare, mock_build_opt):
mock_reboot.side_effect = RuntimeError("broken")
with task_manager.acquire(self.context, self.node['uuid'],
shared=False) as task:
agent_base._post_step_reboot(task, 'deploy')
mock_reboot.assert_called_once_with(task, states.REBOOT)
mock_handler.assert_called_once_with(task, mock.ANY)
self.assertNotIn('deployment_reboot',
task.node.driver_internal_info)
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True) @mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', spec_set=True, @mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk', spec_set=True,
autospec=True) autospec=True)
@ -1603,7 +1641,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
self.deploy.continue_cleaning(task) self.deploy.continue_cleaning(task)
reboot_mock.assert_called_once_with(task, states.REBOOT) reboot_mock.assert_called_once_with(task, states.REBOOT)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
@mock.patch.object(agent_client.AgentClient, 'get_commands_status', @mock.patch.object(agent_client.AgentClient, 'get_commands_status',
autospec=True) autospec=True)
@ -1621,16 +1659,17 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
self.node.save() self.node.save()
# Represents a freshly booted agent with no commands # Represents a freshly booted agent with no commands
status_mock.return_value = [] status_mock.return_value = []
with task_manager.acquire(self.context, self.node['uuid'], with task_manager.acquire(self.context, self.node['uuid'],
shared=False) as task: shared=False) as task:
self.deploy.continue_cleaning(task) self.deploy.continue_cleaning(task)
notify_mock.assert_called_once_with(task) notify_mock.assert_called_once_with(task, 'clean')
self.assertNotIn('cleaning_reboot', self.assertNotIn('cleaning_reboot',
task.node.driver_internal_info) task.node.driver_internal_info)
@mock.patch.object(agent_base, @mock.patch.object(agent_base,
'_get_post_clean_step_hook', autospec=True) '_get_post_step_hook', autospec=True)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
@mock.patch.object(agent_client.AgentClient, 'get_commands_status', @mock.patch.object(agent_client.AgentClient, 'get_commands_status',
autospec=True) autospec=True)
@ -1653,14 +1692,14 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
shared=False) as task: shared=False) as task:
self.deploy.continue_cleaning(task) self.deploy.continue_cleaning(task)
get_hook_mock.assert_called_once_with(task.node) get_hook_mock.assert_called_once_with(task.node, 'clean')
hook_mock.assert_called_once_with(task, command_status) hook_mock.assert_called_once_with(task, command_status)
notify_mock.assert_called_once_with(task) notify_mock.assert_called_once_with(task, 'clean')
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
@mock.patch.object(agent_base, @mock.patch.object(agent_base,
'_get_post_clean_step_hook', autospec=True) '_get_post_step_hook', autospec=True)
@mock.patch.object(manager_utils, 'cleaning_error_handler', autospec=True) @mock.patch.object(manager_utils, 'cleaning_error_handler', autospec=True)
@mock.patch.object(agent_client.AgentClient, 'get_commands_status', @mock.patch.object(agent_client.AgentClient, 'get_commands_status',
autospec=True) autospec=True)
@ -1685,12 +1724,12 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
shared=False) as task: shared=False) as task:
self.deploy.continue_cleaning(task) self.deploy.continue_cleaning(task)
get_hook_mock.assert_called_once_with(task.node) get_hook_mock.assert_called_once_with(task.node, 'clean')
hook_mock.assert_called_once_with(task, command_status) hook_mock.assert_called_once_with(task, command_status)
error_handler_mock.assert_called_once_with(task, mock.ANY) error_handler_mock.assert_called_once_with(task, mock.ANY)
self.assertFalse(notify_mock.called) self.assertFalse(notify_mock.called)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
@mock.patch.object(agent_client.AgentClient, 'get_commands_status', @mock.patch.object(agent_client.AgentClient, 'get_commands_status',
autospec=True) autospec=True)
@ -1719,7 +1758,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
self.deploy.continue_cleaning(task) self.deploy.continue_cleaning(task)
self.assertFalse(notify_mock.called) self.assertFalse(notify_mock.called)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
@mock.patch.object(agent_client.AgentClient, 'get_commands_status', @mock.patch.object(agent_client.AgentClient, 'get_commands_status',
autospec=True) autospec=True)
@ -1752,10 +1791,10 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
@mock.patch.object(conductor_steps, 'set_node_cleaning_steps', @mock.patch.object(conductor_steps, 'set_node_cleaning_steps',
autospec=True) autospec=True)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
@mock.patch.object(agent_base.AgentDeployMixin, @mock.patch.object(agent_base.AgentDeployMixin,
'refresh_clean_steps', autospec=True) 'refresh_steps', autospec=True)
@mock.patch.object(agent_client.AgentClient, 'get_commands_status', @mock.patch.object(agent_client.AgentClient, 'get_commands_status',
autospec=True) autospec=True)
def _test_continue_cleaning_clean_version_mismatch( def _test_continue_cleaning_clean_version_mismatch(
@ -1772,8 +1811,8 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
with task_manager.acquire(self.context, self.node.uuid, with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task: shared=False) as task:
self.deploy.continue_cleaning(task) self.deploy.continue_cleaning(task)
notify_mock.assert_called_once_with(task) notify_mock.assert_called_once_with(task, 'clean')
refresh_steps_mock.assert_called_once_with(mock.ANY, task) refresh_steps_mock.assert_called_once_with(mock.ANY, task, 'clean')
if manual: if manual:
self.assertFalse( self.assertFalse(
task.node.driver_internal_info['skip_current_clean_step']) task.node.driver_internal_info['skip_current_clean_step'])
@ -1792,10 +1831,10 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
@mock.patch.object(manager_utils, 'cleaning_error_handler', autospec=True) @mock.patch.object(manager_utils, 'cleaning_error_handler', autospec=True)
@mock.patch.object(conductor_steps, 'set_node_cleaning_steps', @mock.patch.object(conductor_steps, 'set_node_cleaning_steps',
autospec=True) autospec=True)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean', @mock.patch.object(manager_utils, 'notify_conductor_resume_operation',
autospec=True) autospec=True)
@mock.patch.object(agent_base.AgentDeployMixin, @mock.patch.object(agent_base.AgentDeployMixin,
'refresh_clean_steps', autospec=True) 'refresh_steps', autospec=True)
@mock.patch.object(agent_client.AgentClient, 'get_commands_status', @mock.patch.object(agent_client.AgentClient, 'get_commands_status',
autospec=True) autospec=True)
def test_continue_cleaning_clean_version_mismatch_fail( def test_continue_cleaning_clean_version_mismatch_fail(
@ -1816,7 +1855,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
self.deploy.continue_cleaning(task) self.deploy.continue_cleaning(task)
status_mock.assert_called_once_with(mock.ANY, task.node) status_mock.assert_called_once_with(mock.ANY, task.node)
refresh_steps_mock.assert_called_once_with(mock.ANY, task) refresh_steps_mock.assert_called_once_with(mock.ANY, task, 'clean')
error_mock.assert_called_once_with(task, mock.ANY) error_mock.assert_called_once_with(task, mock.ANY)
self.assertFalse(notify_mock.called) self.assertFalse(notify_mock.called)
self.assertFalse(steps_mock.called) self.assertFalse(steps_mock.called)
@ -1836,37 +1875,8 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
self.deploy.continue_cleaning(task) self.deploy.continue_cleaning(task)
error_mock.assert_called_once_with(task, mock.ANY) error_mock.assert_called_once_with(task, mock.ANY)
def _test_clean_step_hook(self, hook_dict_mock): def _test_clean_step_hook(self):
"""Helper method for unit tests related to clean step hooks. """Helper method for unit tests related to clean step hooks."""
This is a helper method for other unit tests related to
clean step hooks. It acceps a mock 'hook_dict_mock' which is
a MagicMock and sets it up to function as a mock dictionary.
After that, it defines a dummy hook_method for two clean steps
raid.create_configuration and raid.delete_configuration.
:param hook_dict_mock: An instance of mock.MagicMock() which
is the mocked value of agent_base.POST_CLEAN_STEP_HOOKS
:returns: a tuple, where the first item is the hook method created
by this method and second item is the backend dictionary for
the mocked hook_dict_mock
"""
hook_dict = {}
def get(key, default):
return hook_dict.get(key, default)
def getitem(self, key):
return hook_dict[key]
def setdefault(key, default):
if key not in hook_dict:
hook_dict[key] = default
return hook_dict[key]
hook_dict_mock.get = get
hook_dict_mock.__getitem__ = getitem
hook_dict_mock.setdefault = setdefault
some_function_mock = mock.MagicMock() some_function_mock = mock.MagicMock()
@agent_base.post_clean_step_hook( @agent_base.post_clean_step_hook(
@ -1876,43 +1886,41 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
def hook_method(): def hook_method():
some_function_mock('some-arguments') some_function_mock('some-arguments')
return hook_method, hook_dict return hook_method
@mock.patch.object(agent_base, 'POST_CLEAN_STEP_HOOKS', @mock.patch.object(agent_base, '_POST_STEP_HOOKS',
spec_set=dict) {'clean': {}, 'deploy': {}})
def test_post_clean_step_hook(self, hook_dict_mock): def test_post_clean_step_hook(self):
# This unit test makes sure that hook methods are registered # This unit test makes sure that hook methods are registered
# properly and entries are made in # properly and entries are made in
# agent_base.POST_CLEAN_STEP_HOOKS # agent_base.POST_CLEAN_STEP_HOOKS
hook_method, hook_dict = self._test_clean_step_hook(hook_dict_mock) hook_method = self._test_clean_step_hook()
self.assertEqual(hook_method, hooks = agent_base._POST_STEP_HOOKS['clean']
hook_dict['raid']['create_configuration']) self.assertEqual(hook_method, hooks['raid']['create_configuration'])
self.assertEqual(hook_method, self.assertEqual(hook_method, hooks['raid']['delete_configuration'])
hook_dict['raid']['delete_configuration'])
@mock.patch.object(agent_base, 'POST_CLEAN_STEP_HOOKS', @mock.patch.object(agent_base, '_POST_STEP_HOOKS',
spec_set=dict) {'clean': {}, 'deploy': {}})
def test__get_post_clean_step_hook(self, hook_dict_mock): def test__get_post_step_hook(self):
# Check if agent_base._get_post_clean_step_hook can get # Check if agent_base._get_post_step_hook can get
# clean step for which hook is registered. # clean step for which hook is registered.
hook_method, hook_dict = self._test_clean_step_hook(hook_dict_mock) hook_method = self._test_clean_step_hook()
self.node.clean_step = {'step': 'create_configuration', self.node.clean_step = {'step': 'create_configuration',
'interface': 'raid'} 'interface': 'raid'}
self.node.save() self.node.save()
hook_returned = agent_base._get_post_clean_step_hook(self.node) hook_returned = agent_base._get_post_step_hook(self.node, 'clean')
self.assertEqual(hook_method, hook_returned) self.assertEqual(hook_method, hook_returned)
@mock.patch.object(agent_base, 'POST_CLEAN_STEP_HOOKS', @mock.patch.object(agent_base, '_POST_STEP_HOOKS',
spec_set=dict) {'clean': {}, 'deploy': {}})
def test__get_post_clean_step_hook_no_hook_registered( def test__get_post_step_hook_no_hook_registered(self):
self, hook_dict_mock): # Make sure agent_base._get_post_step_hook returns
# Make sure agent_base._get_post_clean_step_hook returns
# None when no clean step hook is registered for the clean step. # None when no clean step hook is registered for the clean step.
hook_method, hook_dict = self._test_clean_step_hook(hook_dict_mock) self._test_clean_step_hook()
self.node.clean_step = {'step': 'some-clean-step', self.node.clean_step = {'step': 'some-clean-step',
'interface': 'some-other-interface'} 'interface': 'some-other-interface'}
self.node.save() self.node.save()
hook_returned = agent_base._get_post_clean_step_hook(self.node) hook_returned = agent_base._get_post_step_hook(self.node, 'clean')
self.assertIsNone(hook_returned) self.assertIsNone(hook_returned)
@mock.patch.object(manager_utils, 'restore_power_state_if_needed', @mock.patch.object(manager_utils, 'restore_power_state_if_needed',
@ -1982,16 +1990,22 @@ class TestRefreshCleanSteps(AgentDeployMixinBaseTest):
] ]
} }
} }
# NOTE(dtantsur): deploy steps are structurally identical to clean
# steps, reusing self.clean_steps for simplicity
self.deploy_steps = {
'hardware_manager_version': '1',
'deploy_steps': self.clean_steps['clean_steps'],
}
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps', @mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
autospec=True) autospec=True)
def test_refresh_clean_steps(self, client_mock): def test_refresh_steps(self, client_mock):
client_mock.return_value = { client_mock.return_value = {
'command_result': self.clean_steps} 'command_result': self.clean_steps}
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:
self.deploy.refresh_clean_steps(task) self.deploy.refresh_steps(task, 'clean')
client_mock.assert_called_once_with(mock.ANY, task.node, client_mock.assert_called_once_with(mock.ANY, task.node,
task.ports) task.ports)
@ -2010,9 +2024,36 @@ class TestRefreshCleanSteps(AgentDeployMixinBaseTest):
self.assertEqual([self.clean_steps['clean_steps'][ self.assertEqual([self.clean_steps['clean_steps'][
'SpecificHardwareManager'][1]], steps['raid']) 'SpecificHardwareManager'][1]], steps['raid'])
@mock.patch.object(agent_client.AgentClient, 'get_deploy_steps',
autospec=True)
def test_refresh_steps_deploy(self, client_mock):
client_mock.return_value = {
'command_result': self.deploy_steps}
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
self.deploy.refresh_steps(task, 'deploy')
client_mock.assert_called_once_with(mock.ANY, task.node,
task.ports)
self.assertEqual('1', task.node.driver_internal_info[
'hardware_manager_version'])
self.assertIn('agent_cached_deploy_steps_refreshed',
task.node.driver_internal_info)
steps = task.node.driver_internal_info['agent_cached_deploy_steps']
self.assertEqual({'deploy', 'raid'}, set(steps))
# Since steps are returned in dicts, they have non-deterministic
# ordering
self.assertIn(self.clean_steps['clean_steps'][
'GenericHardwareManager'][0], steps['deploy'])
self.assertIn(self.clean_steps['clean_steps'][
'SpecificHardwareManager'][0], steps['deploy'])
self.assertEqual([self.clean_steps['clean_steps'][
'SpecificHardwareManager'][1]], steps['raid'])
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps', @mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
autospec=True) autospec=True)
def test_refresh_clean_steps_missing_steps(self, client_mock): def test_refresh_steps_missing_steps(self, client_mock):
del self.clean_steps['clean_steps'] del self.clean_steps['clean_steps']
client_mock.return_value = { client_mock.return_value = {
'command_result': self.clean_steps} 'command_result': self.clean_steps}
@ -2021,14 +2062,14 @@ class TestRefreshCleanSteps(AgentDeployMixinBaseTest):
self.context, self.node.uuid, shared=False) as task: self.context, self.node.uuid, shared=False) as task:
self.assertRaisesRegex(exception.NodeCleaningFailure, self.assertRaisesRegex(exception.NodeCleaningFailure,
'invalid result', 'invalid result',
self.deploy.refresh_clean_steps, self.deploy.refresh_steps,
task) task, 'clean')
client_mock.assert_called_once_with(mock.ANY, task.node, client_mock.assert_called_once_with(mock.ANY, task.node,
task.ports) task.ports)
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps', @mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
autospec=True) autospec=True)
def test_refresh_clean_steps_missing_interface(self, client_mock): def test_refresh_steps_missing_interface(self, client_mock):
step = self.clean_steps['clean_steps']['SpecificHardwareManager'][1] step = self.clean_steps['clean_steps']['SpecificHardwareManager'][1]
del step['interface'] del step['interface']
client_mock.return_value = { client_mock.return_value = {
@ -2038,16 +2079,16 @@ class TestRefreshCleanSteps(AgentDeployMixinBaseTest):
self.context, self.node.uuid, shared=False) as task: self.context, self.node.uuid, shared=False) as task:
self.assertRaisesRegex(exception.NodeCleaningFailure, self.assertRaisesRegex(exception.NodeCleaningFailure,
'invalid clean step', 'invalid clean step',
self.deploy.refresh_clean_steps, self.deploy.refresh_steps,
task) task, 'clean')
client_mock.assert_called_once_with(mock.ANY, task.node, client_mock.assert_called_once_with(mock.ANY, task.node,
task.ports) task.ports)
class CleanStepMethodsTestCase(db_base.DbTestCase): class StepMethodsTestCase(db_base.DbTestCase):
def setUp(self): def setUp(self):
super(CleanStepMethodsTestCase, self).setUp() super(StepMethodsTestCase, self).setUp()
self.clean_steps = { self.clean_steps = {
'deploy': [ 'deploy': [
@ -2072,10 +2113,10 @@ class CleanStepMethodsTestCase(db_base.DbTestCase):
self.ports = [object_utils.create_test_port(self.context, self.ports = [object_utils.create_test_port(self.context,
node_id=self.node.id)] node_id=self.node.id)]
def test_agent_get_clean_steps(self): def test_agent_get_steps(self):
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:
response = agent_base.get_clean_steps(task) response = agent_base.get_steps(task, 'clean')
# Since steps are returned in dicts, they have non-deterministic # Since steps are returned in dicts, they have non-deterministic
# ordering # ordering
@ -2084,40 +2125,55 @@ class CleanStepMethodsTestCase(db_base.DbTestCase):
self.assertIn(self.clean_steps['deploy'][1], response) self.assertIn(self.clean_steps['deploy'][1], response)
self.assertIn(self.clean_steps['raid'][0], response) self.assertIn(self.clean_steps['raid'][0], response)
def test_get_clean_steps_custom_interface(self): def test_agent_get_steps_deploy(self):
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:
response = agent_base.get_clean_steps(task, interface='raid') task.node.driver_internal_info = {
'agent_cached_deploy_steps': self.clean_steps
}
response = agent_base.get_steps(task, 'deploy')
# Since steps are returned in dicts, they have non-deterministic
# ordering
self.assertThat(response, matchers.HasLength(3))
self.assertIn(self.clean_steps['deploy'][0], response)
self.assertIn(self.clean_steps['deploy'][1], response)
self.assertIn(self.clean_steps['raid'][0], response)
def test_get_steps_custom_interface(self):
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
response = agent_base.get_steps(task, 'clean', interface='raid')
self.assertThat(response, matchers.HasLength(1)) self.assertThat(response, matchers.HasLength(1))
self.assertEqual(self.clean_steps['raid'], response) self.assertEqual(self.clean_steps['raid'], response)
def test_get_clean_steps_override_priorities(self): def test_get_steps_override_priorities(self):
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:
new_priorities = {'create_configuration': 42} new_priorities = {'create_configuration': 42}
response = agent_base.get_clean_steps( response = agent_base.get_steps(
task, interface='raid', override_priorities=new_priorities) task, 'clean', interface='raid',
override_priorities=new_priorities)
self.assertEqual(42, response[0]['priority']) self.assertEqual(42, response[0]['priority'])
def test_get_clean_steps_override_priorities_none(self): def test_get_steps_override_priorities_none(self):
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:
# this is simulating the default value of a configuration option # this is simulating the default value of a configuration option
new_priorities = {'create_configuration': None} new_priorities = {'create_configuration': None}
response = agent_base.get_clean_steps( response = agent_base.get_steps(
task, interface='raid', override_priorities=new_priorities) task, 'clean', interface='raid',
override_priorities=new_priorities)
self.assertEqual(10, response[0]['priority']) self.assertEqual(10, response[0]['priority'])
def test_get_clean_steps_missing_steps(self): def test_get_steps_missing_steps(self):
info = self.node.driver_internal_info info = self.node.driver_internal_info
del info['agent_cached_clean_steps'] del info['agent_cached_clean_steps']
self.node.driver_internal_info = info self.node.driver_internal_info = info
self.node.save() self.node.save()
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:
self.assertRaises(exception.NodeCleaningFailure, self.assertEqual([], agent_base.get_steps(task, 'clean'))
agent_base.get_clean_steps,
task)
@mock.patch('ironic.objects.Port.list_by_node_id', @mock.patch('ironic.objects.Port.list_by_node_id',
spec_set=types.FunctionType) spec_set=types.FunctionType)
@ -2130,11 +2186,25 @@ class CleanStepMethodsTestCase(db_base.DbTestCase):
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:
response = agent_base.execute_clean_step( response = agent_base.execute_step(
task, task, self.clean_steps['deploy'][0], 'clean')
self.clean_steps['deploy'][0])
self.assertEqual(states.CLEANWAIT, response) self.assertEqual(states.CLEANWAIT, response)
@mock.patch('ironic.objects.Port.list_by_node_id',
spec_set=types.FunctionType)
@mock.patch.object(agent_client.AgentClient, 'execute_deploy_step',
autospec=True)
def test_execute_deploy_step(self, client_mock, list_ports_mock):
client_mock.return_value = {
'command_status': 'SUCCEEDED'}
list_ports_mock.return_value = self.ports
with task_manager.acquire(
self.context, self.node.uuid, shared=False) as task:
response = agent_base.execute_step(
task, self.clean_steps['deploy'][0], 'deploy')
self.assertEqual(states.DEPLOYWAIT, response)
@mock.patch('ironic.objects.Port.list_by_node_id', @mock.patch('ironic.objects.Port.list_by_node_id',
spec_set=types.FunctionType) spec_set=types.FunctionType)
@mock.patch.object(agent_client.AgentClient, 'execute_clean_step', @mock.patch.object(agent_client.AgentClient, 'execute_clean_step',
@ -2146,9 +2216,8 @@ class CleanStepMethodsTestCase(db_base.DbTestCase):
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:
response = agent_base.execute_clean_step( response = agent_base.execute_step(
task, task, self.clean_steps['deploy'][0], 'clean')
self.clean_steps['deploy'][0])
self.assertEqual(states.CLEANWAIT, response) self.assertEqual(states.CLEANWAIT, response)
@mock.patch('ironic.objects.Port.list_by_node_id', @mock.patch('ironic.objects.Port.list_by_node_id',
@ -2163,7 +2232,6 @@ class CleanStepMethodsTestCase(db_base.DbTestCase):
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:
response = agent_base.execute_clean_step( response = agent_base.execute_step(
task, task, self.clean_steps['deploy'][0], 'clean')
self.clean_steps['deploy'][0])
self.assertEqual(states.CLEANWAIT, response) self.assertEqual(states.CLEANWAIT, response)

View File

@ -968,7 +968,7 @@ class ISCSIDeployTestCase(db_base.DbTestCase):
tear_down_cleaning_mock.assert_called_once_with( tear_down_cleaning_mock.assert_called_once_with(
task, manage_boot=True) task, manage_boot=True)
@mock.patch.object(agent_base, 'get_clean_steps', autospec=True) @mock.patch.object(agent_base, 'get_steps', autospec=True)
def test_get_clean_steps(self, mock_get_clean_steps): def test_get_clean_steps(self, mock_get_clean_steps):
# Test getting clean steps # Test getting clean steps
self.config(group='deploy', erase_devices_priority=10) self.config(group='deploy', erase_devices_priority=10)
@ -981,19 +981,19 @@ class ISCSIDeployTestCase(db_base.DbTestCase):
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
steps = task.driver.deploy.get_clean_steps(task) steps = task.driver.deploy.get_clean_steps(task)
mock_get_clean_steps.assert_called_once_with( mock_get_clean_steps.assert_called_once_with(
task, interface='deploy', task, 'clean', interface='deploy',
override_priorities={ override_priorities={
'erase_devices': 10, 'erase_devices': 10,
'erase_devices_metadata': 5}) 'erase_devices_metadata': 5})
self.assertEqual(mock_steps, steps) self.assertEqual(mock_steps, steps)
@mock.patch.object(agent_base, 'execute_clean_step', autospec=True) @mock.patch.object(agent_base, 'execute_step', autospec=True)
def test_execute_clean_step(self, agent_execute_clean_step_mock): def test_execute_clean_step(self, agent_execute_clean_step_mock):
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.deploy.execute_clean_step( task.driver.deploy.execute_clean_step(
task, {'some-step': 'step-info'}) task, {'some-step': 'step-info'})
agent_execute_clean_step_mock.assert_called_once_with( agent_execute_clean_step_mock.assert_called_once_with(
task, {'some-step': 'step-info'}) task, {'some-step': 'step-info'}, 'clean')
@mock.patch.object(agent_base.AgentDeployMixin, @mock.patch.object(agent_base.AgentDeployMixin,
'reboot_and_finish_deploy', autospec=True) 'reboot_and_finish_deploy', autospec=True)