Merge "Deploy Templates: factor out ironic.conductor.steps"
This commit is contained in:
commit
244a533800
@ -33,7 +33,7 @@ from ironic.api.controllers.v1 import utils as api_utils
|
||||
from ironic.api import expose
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.conductor import utils as conductor_utils
|
||||
from ironic.conductor import steps as conductor_steps
|
||||
import ironic.conf
|
||||
from ironic import objects
|
||||
|
||||
@ -44,7 +44,7 @@ METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||
_DEFAULT_RETURN_FIELDS = ('uuid', 'name')
|
||||
|
||||
_DEPLOY_INTERFACE_TYPE = wtypes.Enum(
|
||||
wtypes.text, *conductor_utils.DEPLOYING_INTERFACE_PRIORITY)
|
||||
wtypes.text, *conductor_steps.DEPLOYING_INTERFACE_PRIORITY)
|
||||
|
||||
|
||||
class DeployStepType(wtypes.Base, base.AsDictMixin):
|
||||
|
@ -43,7 +43,7 @@ from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import policy
|
||||
from ironic.common import states as ir_states
|
||||
from ironic.conductor import utils as conductor_utils
|
||||
from ironic.conductor import steps as conductor_steps
|
||||
import ironic.conf
|
||||
from ironic import objects
|
||||
|
||||
@ -63,7 +63,7 @@ _CLEAN_STEPS_SCHEMA = {
|
||||
"properties": {
|
||||
"interface": {
|
||||
"description": "driver interface",
|
||||
"enum": list(conductor_utils.CLEANING_INTERFACE_PRIORITY)
|
||||
"enum": list(conductor_steps.CLEANING_INTERFACE_PRIORITY)
|
||||
# interface value must be one of the valid interfaces
|
||||
},
|
||||
"step": {
|
||||
|
@ -69,6 +69,7 @@ from ironic.common import swift
|
||||
from ironic.conductor import allocations
|
||||
from ironic.conductor import base_manager
|
||||
from ironic.conductor import notification_utils as notify_utils
|
||||
from ironic.conductor import steps as conductor_steps
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.conductor import utils
|
||||
from ironic.conf import CONF
|
||||
@ -853,7 +854,7 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
task.driver.power.validate(task)
|
||||
task.driver.deploy.validate(task)
|
||||
utils.validate_instance_info_traits(task.node)
|
||||
utils.validate_deploy_templates(task)
|
||||
conductor_steps.validate_deploy_templates(task)
|
||||
except exception.InvalidParameterValue as e:
|
||||
raise exception.InstanceDeployFailure(
|
||||
_("Failed to validate deploy or power info for node "
|
||||
@ -1338,7 +1339,7 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
return
|
||||
|
||||
try:
|
||||
utils.set_node_cleaning_steps(task)
|
||||
conductor_steps.set_node_cleaning_steps(task)
|
||||
except (exception.InvalidParameterValue,
|
||||
exception.NodeCleaningFailure) as e:
|
||||
msg = (_('Cannot clean node %(node)s. Error: %(msg)s')
|
||||
@ -2205,7 +2206,7 @@ class ConductorManager(base_manager.BaseConductorManager):
|
||||
iface.validate(task)
|
||||
if iface_name == 'deploy':
|
||||
utils.validate_instance_info_traits(task.node)
|
||||
utils.validate_deploy_templates(task)
|
||||
conductor_steps.validate_deploy_templates(task)
|
||||
result = True
|
||||
except (exception.InvalidParameterValue,
|
||||
exception.UnsupportedDriverExtension) as e:
|
||||
@ -3692,7 +3693,7 @@ def do_node_deploy(task, conductor_id=None, configdrive=None):
|
||||
try:
|
||||
# This gets the deploy steps (if any) and puts them in the node's
|
||||
# driver_internal_info['deploy_steps'].
|
||||
utils.set_node_deployment_steps(task)
|
||||
conductor_steps.set_node_deployment_steps(task)
|
||||
except exception.InstanceDeployFailure as e:
|
||||
with excutils.save_and_reraise_exception():
|
||||
utils.deploying_error_handler(
|
||||
|
601
ironic/conductor/steps.py
Normal file
601
ironic/conductor/steps.py
Normal file
@ -0,0 +1,601 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import states
|
||||
from ironic.objects import deploy_template
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
CLEANING_INTERFACE_PRIORITY = {
|
||||
# When two clean steps have the same priority, their order is determined
|
||||
# by which interface is implementing the clean step. The clean step of the
|
||||
# interface with the highest value here, will be executed first in that
|
||||
# case.
|
||||
'power': 5,
|
||||
'management': 4,
|
||||
'deploy': 3,
|
||||
'bios': 2,
|
||||
'raid': 1,
|
||||
}
|
||||
|
||||
DEPLOYING_INTERFACE_PRIORITY = {
|
||||
# When two deploy steps have the same priority, their order is determined
|
||||
# by which interface is implementing the step. The step of the interface
|
||||
# with the highest value here, will be executed first in that case.
|
||||
# TODO(rloo): If we think it makes sense to have the interface priorities
|
||||
# the same for cleaning & deploying, replace the two with one e.g.
|
||||
# 'INTERFACE_PRIORITIES'.
|
||||
'power': 5,
|
||||
'management': 4,
|
||||
'deploy': 3,
|
||||
'bios': 2,
|
||||
'raid': 1,
|
||||
}
|
||||
|
||||
|
||||
def _clean_step_key(step):
|
||||
"""Sort by priority, then interface priority in event of tie.
|
||||
|
||||
:param step: cleaning step dict to get priority for.
|
||||
"""
|
||||
return (step.get('priority'),
|
||||
CLEANING_INTERFACE_PRIORITY[step.get('interface')])
|
||||
|
||||
|
||||
def _deploy_step_key(step):
|
||||
"""Sort by priority, then interface priority in event of tie.
|
||||
|
||||
:param step: deploy step dict to get priority for.
|
||||
"""
|
||||
return (step.get('priority'),
|
||||
DEPLOYING_INTERFACE_PRIORITY[step.get('interface')])
|
||||
|
||||
|
||||
def _sorted_steps(steps, sort_step_key):
|
||||
"""Return a sorted list of steps.
|
||||
|
||||
:param sort_step_key: If set, this is a method (key) used to sort the steps
|
||||
from highest priority to lowest priority. For steps having the same
|
||||
priority, they are sorted from highest interface priority to lowest.
|
||||
:returns: A list of sorted step dictionaries.
|
||||
"""
|
||||
# Sort the steps from higher priority to lower priority
|
||||
return sorted(steps, key=sort_step_key, reverse=True)
|
||||
|
||||
|
||||
def _get_steps(task, interfaces, get_method, enabled=False,
|
||||
sort_step_key=None):
|
||||
"""Get steps for task.node.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param interfaces: A dictionary of (key) interfaces and their
|
||||
(value) priorities. These are the interfaces that will have steps of
|
||||
interest. The priorities are used for deciding the priorities of steps
|
||||
having the same priority.
|
||||
:param get_method: The method used to get the steps from the node's
|
||||
interface; a string.
|
||||
:param enabled: If True, returns only enabled (priority > 0) steps. If
|
||||
False, returns all steps.
|
||||
:param sort_step_key: If set, this is a method (key) used to sort the steps
|
||||
from highest priority to lowest priority. For steps having the same
|
||||
priority, they are sorted from highest interface priority to lowest.
|
||||
:raises: NodeCleaningFailure or InstanceDeployFailure if there was a
|
||||
problem getting the steps.
|
||||
:returns: A list of step dictionaries
|
||||
"""
|
||||
# Get steps from each interface
|
||||
steps = list()
|
||||
for interface in interfaces:
|
||||
interface = getattr(task.driver, interface)
|
||||
if interface:
|
||||
interface_steps = [x for x in getattr(interface, get_method)(task)
|
||||
if not enabled or x['priority'] > 0]
|
||||
steps.extend(interface_steps)
|
||||
if sort_step_key:
|
||||
steps = _sorted_steps(steps, sort_step_key)
|
||||
return steps
|
||||
|
||||
|
||||
def _get_cleaning_steps(task, enabled=False, sort=True):
|
||||
"""Get cleaning steps for task.node.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param enabled: If True, returns only enabled (priority > 0) steps. If
|
||||
False, returns all clean steps.
|
||||
:param sort: If True, the steps are sorted from highest priority to lowest
|
||||
priority. For steps having the same priority, they are sorted from
|
||||
highest interface priority to lowest.
|
||||
:raises: NodeCleaningFailure if there was a problem getting the
|
||||
clean steps.
|
||||
:returns: A list of clean step dictionaries
|
||||
"""
|
||||
sort_key = _clean_step_key if sort else None
|
||||
return _get_steps(task, CLEANING_INTERFACE_PRIORITY, 'get_clean_steps',
|
||||
enabled=enabled, sort_step_key=sort_key)
|
||||
|
||||
|
||||
def _get_deployment_steps(task, enabled=False, sort=True):
|
||||
"""Get deployment steps for task.node.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param enabled: If True, returns only enabled (priority > 0) steps. If
|
||||
False, returns all deploy steps.
|
||||
:param sort: If True, the steps are sorted from highest priority to lowest
|
||||
priority. For steps having the same priority, they are sorted from
|
||||
highest interface priority to lowest.
|
||||
:raises: InstanceDeployFailure if there was a problem getting the
|
||||
deploy steps.
|
||||
:returns: A list of deploy step dictionaries
|
||||
"""
|
||||
sort_key = _deploy_step_key if sort else None
|
||||
return _get_steps(task, DEPLOYING_INTERFACE_PRIORITY, 'get_deploy_steps',
|
||||
enabled=enabled, sort_step_key=sort_key)
|
||||
|
||||
|
||||
def set_node_cleaning_steps(task):
|
||||
"""Set up the node with clean step information for cleaning.
|
||||
|
||||
For automated cleaning, get the clean steps from the driver.
|
||||
For manual cleaning, the user's clean steps are known but need to be
|
||||
validated against the driver's clean steps.
|
||||
|
||||
:raises: InvalidParameterValue if there is a problem with the user's
|
||||
clean steps.
|
||||
:raises: NodeCleaningFailure if there was a problem getting the
|
||||
clean steps.
|
||||
"""
|
||||
node = task.node
|
||||
driver_internal_info = node.driver_internal_info
|
||||
|
||||
# For manual cleaning, the target provision state is MANAGEABLE, whereas
|
||||
# for automated cleaning, it is AVAILABLE.
|
||||
manual_clean = node.target_provision_state == states.MANAGEABLE
|
||||
|
||||
if not manual_clean:
|
||||
# Get the prioritized steps for automated cleaning
|
||||
driver_internal_info['clean_steps'] = _get_cleaning_steps(task,
|
||||
enabled=True)
|
||||
else:
|
||||
# For manual cleaning, the list of cleaning steps was specified by the
|
||||
# user and already saved in node.driver_internal_info['clean_steps'].
|
||||
# Now that we know what the driver's available clean steps are, we can
|
||||
# do further checks to validate the user's clean steps.
|
||||
steps = node.driver_internal_info['clean_steps']
|
||||
driver_internal_info['clean_steps'] = (
|
||||
_validate_user_clean_steps(task, steps))
|
||||
|
||||
node.clean_step = {}
|
||||
driver_internal_info['clean_step_index'] = None
|
||||
node.driver_internal_info = driver_internal_info
|
||||
node.save()
|
||||
|
||||
|
||||
def _get_deployment_templates(task):
|
||||
"""Get deployment templates for task.node.
|
||||
|
||||
Return deployment templates where the name of the deployment template
|
||||
matches one of the node's instance traits (the subset of the node's traits
|
||||
requested by the user via a flavor or image).
|
||||
|
||||
:param task: A TaskManager object
|
||||
:returns: a list of DeployTemplate objects.
|
||||
"""
|
||||
node = task.node
|
||||
if not node.instance_info.get('traits'):
|
||||
return []
|
||||
instance_traits = node.instance_info['traits']
|
||||
return deploy_template.DeployTemplate.list_by_names(task.context,
|
||||
instance_traits)
|
||||
|
||||
|
||||
def _get_steps_from_deployment_templates(task, templates):
|
||||
"""Get deployment template steps for task.node.
|
||||
|
||||
Given a list of deploy template objects, return a list of all deploy steps
|
||||
combined.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param templates: a list of deploy templates
|
||||
:returns: A list of deploy step dictionaries
|
||||
"""
|
||||
steps = []
|
||||
# NOTE(mgoddard): The steps from the object include id, created_at, etc.,
|
||||
# which we don't want to include when we assign them to
|
||||
# node.driver_internal_info. Include only the relevant fields.
|
||||
step_fields = ('interface', 'step', 'args', 'priority')
|
||||
for template in templates:
|
||||
steps.extend([{key: step[key] for key in step_fields}
|
||||
for step in template.steps])
|
||||
return steps
|
||||
|
||||
|
||||
def _get_validated_steps_from_templates(task):
|
||||
"""Return a list of validated deploy steps from deploy templates.
|
||||
|
||||
Deployment template steps are those steps defined in deployment templates
|
||||
where the name of the deployment template matches one of the node's
|
||||
instance traits (the subset of the node's traits requested by the user via
|
||||
a flavor or image). There may be many such matching templates, each with a
|
||||
list of steps to execute.
|
||||
|
||||
This method gathers the steps from all matching deploy templates for a
|
||||
node, and validates those steps against the node's driver interfaces,
|
||||
raising an error if validation fails.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:raises: InvalidParameterValue if validation of steps fails.
|
||||
:raises: InstanceDeployFailure if there was a problem getting the
|
||||
deploy steps.
|
||||
:returns: A list of validated deploy step dictionaries
|
||||
"""
|
||||
# Gather deploy templates matching the node's instance traits.
|
||||
templates = _get_deployment_templates(task)
|
||||
|
||||
# Gather deploy steps from deploy templates.
|
||||
user_steps = _get_steps_from_deployment_templates(task, templates)
|
||||
|
||||
# Validate the steps.
|
||||
error_prefix = (_('Validation of deploy steps from deploy templates '
|
||||
'matching this node\'s instance traits failed. Matching '
|
||||
'deploy templates: %(templates)s. Errors: ') %
|
||||
{'templates': ','.join(t.name for t in templates)})
|
||||
return _validate_user_deploy_steps(task, user_steps,
|
||||
error_prefix=error_prefix)
|
||||
|
||||
|
||||
def _get_all_deployment_steps(task):
|
||||
"""Get deployment steps for task.node.
|
||||
|
||||
Deployment steps from matching deployment templates are combined with those
|
||||
from driver interfaces and all enabled steps returned in priority order.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:raises: InstanceDeployFailure if there was a problem getting the
|
||||
deploy steps.
|
||||
:returns: A list of deploy step dictionaries
|
||||
"""
|
||||
# Gather deploy steps from deploy templates and validate.
|
||||
# NOTE(mgoddard): although we've probably just validated the templates in
|
||||
# do_node_deploy, they may have changed in the DB since we last checked, so
|
||||
# validate again.
|
||||
user_steps = _get_validated_steps_from_templates(task)
|
||||
|
||||
# Gather enabled deploy steps from drivers.
|
||||
driver_steps = _get_deployment_steps(task, enabled=True, sort=False)
|
||||
|
||||
# Remove driver steps that have been disabled or overridden by user steps.
|
||||
user_step_keys = {(s['interface'], s['step']) for s in user_steps}
|
||||
steps = [s for s in driver_steps
|
||||
if (s['interface'], s['step']) not in user_step_keys]
|
||||
|
||||
# Add enabled user steps.
|
||||
enabled_user_steps = [s for s in user_steps if s['priority'] > 0]
|
||||
steps.extend(enabled_user_steps)
|
||||
|
||||
return _sorted_steps(steps, _deploy_step_key)
|
||||
|
||||
|
||||
def set_node_deployment_steps(task):
|
||||
"""Set up the node with deployment step information for deploying.
|
||||
|
||||
Get the deploy steps from the driver.
|
||||
|
||||
:raises: InstanceDeployFailure if there was a problem getting the
|
||||
deployment steps.
|
||||
"""
|
||||
node = task.node
|
||||
driver_internal_info = node.driver_internal_info
|
||||
driver_internal_info['deploy_steps'] = _get_all_deployment_steps(task)
|
||||
node.deploy_step = {}
|
||||
driver_internal_info['deploy_step_index'] = None
|
||||
node.driver_internal_info = driver_internal_info
|
||||
node.save()
|
||||
|
||||
|
||||
def _step_id(step):
|
||||
"""Return the 'ID' of a deploy step.
|
||||
|
||||
The ID is a string, <interface>.<step>.
|
||||
|
||||
:param step: the step dictionary.
|
||||
:return: the step's ID string.
|
||||
"""
|
||||
return '.'.join([step['interface'], step['step']])
|
||||
|
||||
|
||||
def _validate_deploy_steps_unique(user_steps):
|
||||
"""Validate that deploy steps from deploy templates are unique.
|
||||
|
||||
:param user_steps: a list of user steps. A user step is a dictionary
|
||||
with required keys 'interface', 'step', 'args', and 'priority'::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_step>,
|
||||
'args': {<arg1>: <value1>, ..., <argn>: <valuen>},
|
||||
'priority': <priority_of_step> }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': deploy',
|
||||
'step': 'upgrade_firmware',
|
||||
'args': {'force': True},
|
||||
'priority': 10 }
|
||||
|
||||
:return: a list of validation error strings for the steps.
|
||||
"""
|
||||
# Check for duplicate steps. Each interface/step combination can be
|
||||
# specified at most once.
|
||||
errors = []
|
||||
counter = collections.Counter(_step_id(step) for step in user_steps)
|
||||
duplicates = {step_id for step_id, count in counter.items() if count > 1}
|
||||
if duplicates:
|
||||
err = (_('deploy steps from all deploy templates matching this '
|
||||
'node\'s instance traits cannot have the same interface '
|
||||
'and step. Duplicate deploy steps for %(duplicates)s') %
|
||||
{'duplicates': ', '.join(duplicates)})
|
||||
errors.append(err)
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_user_step(task, user_step, driver_step, step_type):
|
||||
"""Validate a user-specified step.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param user_step: a user step dictionary with required keys 'interface'
|
||||
and 'step', and optional keys 'args' and 'priority'::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_step>,
|
||||
'args': {<arg1>: <value1>, ..., <argn>: <valuen>},
|
||||
'priority': <optional_priority> }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': deploy',
|
||||
'step': 'upgrade_firmware',
|
||||
'args': {'force': True} }
|
||||
|
||||
:param driver_step: a driver step dictionary::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_step>,
|
||||
'priority': <integer>
|
||||
'abortable': Optional for clean steps, absent for deploy steps.
|
||||
<Boolean>.
|
||||
'argsinfo': Optional. A dictionary of
|
||||
{<arg_name>:<arg_info_dict>} entries.
|
||||
<arg_info_dict> is a dictionary with
|
||||
{ 'description': <description>,
|
||||
'required': <Boolean> } }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': deploy',
|
||||
'step': 'upgrade_firmware',
|
||||
'priority': 10,
|
||||
'abortable': True,
|
||||
'argsinfo': {
|
||||
'force': { 'description': 'Whether to force the upgrade',
|
||||
'required': False } } }
|
||||
|
||||
:param step_type: either 'clean' or 'deploy'.
|
||||
:return: a list of validation error strings for the step.
|
||||
"""
|
||||
errors = []
|
||||
# Check that the user-specified arguments are valid
|
||||
argsinfo = driver_step.get('argsinfo') or {}
|
||||
user_args = user_step.get('args') or {}
|
||||
unexpected = set(user_args) - set(argsinfo)
|
||||
if unexpected:
|
||||
error = (_('%(type)s step %(step)s has these unexpected arguments: '
|
||||
'%(unexpected)s') %
|
||||
{'type': step_type, 'step': user_step,
|
||||
'unexpected': ', '.join(unexpected)})
|
||||
errors.append(error)
|
||||
|
||||
if step_type == 'clean' or user_step['priority'] > 0:
|
||||
# Check that all required arguments were specified by the user
|
||||
missing = []
|
||||
for (arg_name, arg_info) in argsinfo.items():
|
||||
if arg_info.get('required', False) and arg_name not in user_args:
|
||||
msg = arg_name
|
||||
if arg_info.get('description'):
|
||||
msg += ' (%(desc)s)' % {'desc': arg_info['description']}
|
||||
missing.append(msg)
|
||||
if missing:
|
||||
error = (_('%(type)s step %(step)s is missing these required '
|
||||
'arguments: %(miss)s') %
|
||||
{'type': step_type, 'step': user_step,
|
||||
'miss': ', '.join(missing)})
|
||||
errors.append(error)
|
||||
|
||||
if step_type == 'clean':
|
||||
# Copy fields that should not be provided by a user
|
||||
user_step['abortable'] = driver_step.get('abortable', False)
|
||||
user_step['priority'] = driver_step.get('priority', 0)
|
||||
elif user_step['priority'] > 0:
|
||||
# 'core' deploy steps can only be disabled.
|
||||
|
||||
# NOTE(mgoddard): we'll need something a little more sophisticated to
|
||||
# track core steps once we split out the single core step.
|
||||
is_core = (driver_step['interface'] == 'deploy' and
|
||||
driver_step['step'] == 'deploy')
|
||||
if is_core:
|
||||
error = (_('deploy step %(step)s on interface %(interface)s is a '
|
||||
'core step and cannot be overridden by user steps. It '
|
||||
'may be disabled by setting the priority to 0') %
|
||||
{'step': user_step['step'],
|
||||
'interface': user_step['interface']})
|
||||
errors.append(error)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_user_steps(task, user_steps, driver_steps, step_type,
|
||||
error_prefix=None):
|
||||
"""Validate the user-specified steps.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param user_steps: a list of user steps. A user step is a dictionary
|
||||
with required keys 'interface' and 'step', and optional keys 'args'
|
||||
and 'priority'::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_step>,
|
||||
'args': {<arg1>: <value1>, ..., <argn>: <valuen>},
|
||||
'priority': <optional_priority> }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': deploy',
|
||||
'step': 'upgrade_firmware',
|
||||
'args': {'force': True} }
|
||||
|
||||
:param driver_steps: a list of driver steps::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_step>,
|
||||
'priority': <integer>
|
||||
'abortable': Optional for clean steps, absent for deploy steps.
|
||||
<Boolean>.
|
||||
'argsinfo': Optional. A dictionary of
|
||||
{<arg_name>:<arg_info_dict>} entries.
|
||||
<arg_info_dict> is a dictionary with
|
||||
{ 'description': <description>,
|
||||
'required': <Boolean> } }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': deploy',
|
||||
'step': 'upgrade_firmware',
|
||||
'priority': 10,
|
||||
'abortable': True,
|
||||
'argsinfo': {
|
||||
'force': { 'description': 'Whether to force the upgrade',
|
||||
'required': False } } }
|
||||
|
||||
:param step_type: either 'clean' or 'deploy'.
|
||||
:param error_prefix: String to use as a prefix for exception messages, or
|
||||
None.
|
||||
:raises: InvalidParameterValue if validation of steps fails.
|
||||
:raises: NodeCleaningFailure or InstanceDeployFailure if
|
||||
there was a problem getting the steps from the driver.
|
||||
:return: validated steps updated with information from the driver
|
||||
"""
|
||||
|
||||
errors = []
|
||||
|
||||
# Convert driver steps to a dict.
|
||||
driver_steps = {_step_id(s): s for s in driver_steps}
|
||||
|
||||
for user_step in user_steps:
|
||||
# Check if this user-specified step isn't supported by the driver
|
||||
try:
|
||||
driver_step = driver_steps[_step_id(user_step)]
|
||||
except KeyError:
|
||||
error = (_('node does not support this %(type)s step: %(step)s')
|
||||
% {'type': step_type, 'step': user_step})
|
||||
errors.append(error)
|
||||
continue
|
||||
|
||||
step_errors = _validate_user_step(task, user_step, driver_step,
|
||||
step_type)
|
||||
errors.extend(step_errors)
|
||||
|
||||
if step_type == 'deploy':
|
||||
# Deploy steps should be unique across all combined templates.
|
||||
dup_errors = _validate_deploy_steps_unique(user_steps)
|
||||
errors.extend(dup_errors)
|
||||
|
||||
if errors:
|
||||
err = error_prefix or ''
|
||||
err += '; '.join(errors)
|
||||
raise exception.InvalidParameterValue(err=err)
|
||||
|
||||
return user_steps
|
||||
|
||||
|
||||
def _validate_user_clean_steps(task, user_steps):
|
||||
"""Validate the user-specified clean steps.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param user_steps: a list of clean steps. A clean step is a dictionary
|
||||
with required keys 'interface' and 'step', and optional key 'args'::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_clean_step>,
|
||||
'args': {<arg1>: <value1>, ..., <argn>: <valuen>} }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': 'deploy',
|
||||
'step': 'upgrade_firmware',
|
||||
'args': {'force': True} }
|
||||
:raises: InvalidParameterValue if validation of clean steps fails.
|
||||
:raises: NodeCleaningFailure if there was a problem getting the
|
||||
clean steps from the driver.
|
||||
:return: validated clean steps update with information from the driver
|
||||
"""
|
||||
driver_steps = _get_cleaning_steps(task, enabled=False, sort=False)
|
||||
return _validate_user_steps(task, user_steps, driver_steps, 'clean')
|
||||
|
||||
|
||||
def _validate_user_deploy_steps(task, user_steps, error_prefix=None):
|
||||
"""Validate the user-specified deploy steps.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param user_steps: a list of deploy steps. A deploy step is a dictionary
|
||||
with required keys 'interface', 'step', 'args', and 'priority'::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_deploy_step>,
|
||||
'args': {<arg1>: <value1>, ..., <argn>: <valuen>},
|
||||
'priority': <priority_of_deploy_step> }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': 'bios',
|
||||
'step': 'apply_configuration',
|
||||
'args': { 'settings': [ { 'foo': 'bar' } ] },
|
||||
'priority': 150 }
|
||||
:param error_prefix: String to use as a prefix for exception messages, or
|
||||
None.
|
||||
:raises: InvalidParameterValue if validation of deploy steps fails.
|
||||
:raises: InstanceDeployFailure if there was a problem getting the deploy
|
||||
steps from the driver.
|
||||
:return: validated deploy steps update with information from the driver
|
||||
"""
|
||||
driver_steps = _get_deployment_steps(task, enabled=False, sort=False)
|
||||
return _validate_user_steps(task, user_steps, driver_steps, 'deploy',
|
||||
error_prefix=error_prefix)
|
||||
|
||||
|
||||
def validate_deploy_templates(task):
|
||||
"""Validate the deploy templates for a node.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:raises: InvalidParameterValue if the instance has traits that map to
|
||||
deploy steps that are unsupported by the node's driver interfaces.
|
||||
:raises: InstanceDeployFailure if there was a problem getting the deploy
|
||||
steps from the driver.
|
||||
"""
|
||||
# Gather deploy steps from matching deploy templates and validate them.
|
||||
_get_validated_steps_from_templates(task)
|
@ -12,7 +12,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import time
|
||||
|
||||
@ -33,38 +32,11 @@ from ironic.common import network
|
||||
from ironic.common import states
|
||||
from ironic.conductor import notification_utils as notify_utils
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.objects import deploy_template
|
||||
from ironic.objects import fields
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
CLEANING_INTERFACE_PRIORITY = {
|
||||
# When two clean steps have the same priority, their order is determined
|
||||
# by which interface is implementing the clean step. The clean step of the
|
||||
# interface with the highest value here, will be executed first in that
|
||||
# case.
|
||||
'power': 5,
|
||||
'management': 4,
|
||||
'deploy': 3,
|
||||
'bios': 2,
|
||||
'raid': 1,
|
||||
}
|
||||
|
||||
DEPLOYING_INTERFACE_PRIORITY = {
|
||||
# When two deploy steps have the same priority, their order is determined
|
||||
# by which interface is implementing the step. The step of the interface
|
||||
# with the highest value here, will be executed first in that case.
|
||||
# TODO(rloo): If we think it makes sense to have the interface priorities
|
||||
# the same for cleaning & deploying, replace the two with one e.g.
|
||||
# 'INTERFACE_PRIORITIES'.
|
||||
'power': 5,
|
||||
'management': 4,
|
||||
'deploy': 3,
|
||||
'bios': 2,
|
||||
'raid': 1,
|
||||
}
|
||||
|
||||
|
||||
@task_manager.require_exclusive_lock
|
||||
def node_set_boot_device(task, device, persistent=False):
|
||||
@ -632,543 +604,6 @@ def power_state_error_handler(e, node, power_state):
|
||||
{'node': node.uuid, 'power_state': power_state})
|
||||
|
||||
|
||||
def _clean_step_key(step):
|
||||
"""Sort by priority, then interface priority in event of tie.
|
||||
|
||||
:param step: cleaning step dict to get priority for.
|
||||
"""
|
||||
return (step.get('priority'),
|
||||
CLEANING_INTERFACE_PRIORITY[step.get('interface')])
|
||||
|
||||
|
||||
def _deploy_step_key(step):
|
||||
"""Sort by priority, then interface priority in event of tie.
|
||||
|
||||
:param step: deploy step dict to get priority for.
|
||||
"""
|
||||
return (step.get('priority'),
|
||||
DEPLOYING_INTERFACE_PRIORITY[step.get('interface')])
|
||||
|
||||
|
||||
def _sorted_steps(steps, sort_step_key):
|
||||
"""Return a sorted list of steps.
|
||||
|
||||
:param sort_step_key: If set, this is a method (key) used to sort the steps
|
||||
from highest priority to lowest priority. For steps having the same
|
||||
priority, they are sorted from highest interface priority to lowest.
|
||||
:returns: A list of sorted step dictionaries.
|
||||
"""
|
||||
# Sort the steps from higher priority to lower priority
|
||||
return sorted(steps, key=sort_step_key, reverse=True)
|
||||
|
||||
|
||||
def _get_steps(task, interfaces, get_method, enabled=False,
|
||||
sort_step_key=None):
|
||||
"""Get steps for task.node.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param interfaces: A dictionary of (key) interfaces and their
|
||||
(value) priorities. These are the interfaces that will have steps of
|
||||
interest. The priorities are used for deciding the priorities of steps
|
||||
having the same priority.
|
||||
:param get_method: The method used to get the steps from the node's
|
||||
interface; a string.
|
||||
:param enabled: If True, returns only enabled (priority > 0) steps. If
|
||||
False, returns all steps.
|
||||
:param sort_step_key: If set, this is a method (key) used to sort the steps
|
||||
from highest priority to lowest priority. For steps having the same
|
||||
priority, they are sorted from highest interface priority to lowest.
|
||||
:raises: NodeCleaningFailure or InstanceDeployFailure if there was a
|
||||
problem getting the steps.
|
||||
:returns: A list of step dictionaries
|
||||
"""
|
||||
# Get steps from each interface
|
||||
steps = list()
|
||||
for interface in interfaces:
|
||||
interface = getattr(task.driver, interface)
|
||||
if interface:
|
||||
interface_steps = [x for x in getattr(interface, get_method)(task)
|
||||
if not enabled or x['priority'] > 0]
|
||||
steps.extend(interface_steps)
|
||||
if sort_step_key:
|
||||
steps = _sorted_steps(steps, sort_step_key)
|
||||
return steps
|
||||
|
||||
|
||||
def _get_cleaning_steps(task, enabled=False, sort=True):
|
||||
"""Get cleaning steps for task.node.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param enabled: If True, returns only enabled (priority > 0) steps. If
|
||||
False, returns all clean steps.
|
||||
:param sort: If True, the steps are sorted from highest priority to lowest
|
||||
priority. For steps having the same priority, they are sorted from
|
||||
highest interface priority to lowest.
|
||||
:raises: NodeCleaningFailure if there was a problem getting the
|
||||
clean steps.
|
||||
:returns: A list of clean step dictionaries
|
||||
"""
|
||||
sort_key = _clean_step_key if sort else None
|
||||
return _get_steps(task, CLEANING_INTERFACE_PRIORITY, 'get_clean_steps',
|
||||
enabled=enabled, sort_step_key=sort_key)
|
||||
|
||||
|
||||
def _get_deployment_steps(task, enabled=False, sort=True):
|
||||
"""Get deployment steps for task.node.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param enabled: If True, returns only enabled (priority > 0) steps. If
|
||||
False, returns all deploy steps.
|
||||
:param sort: If True, the steps are sorted from highest priority to lowest
|
||||
priority. For steps having the same priority, they are sorted from
|
||||
highest interface priority to lowest.
|
||||
:raises: InstanceDeployFailure if there was a problem getting the
|
||||
deploy steps.
|
||||
:returns: A list of deploy step dictionaries
|
||||
"""
|
||||
sort_key = _deploy_step_key if sort else None
|
||||
return _get_steps(task, DEPLOYING_INTERFACE_PRIORITY, 'get_deploy_steps',
|
||||
enabled=enabled, sort_step_key=sort_key)
|
||||
|
||||
|
||||
def set_node_cleaning_steps(task):
|
||||
"""Set up the node with clean step information for cleaning.
|
||||
|
||||
For automated cleaning, get the clean steps from the driver.
|
||||
For manual cleaning, the user's clean steps are known but need to be
|
||||
validated against the driver's clean steps.
|
||||
|
||||
:raises: InvalidParameterValue if there is a problem with the user's
|
||||
clean steps.
|
||||
:raises: NodeCleaningFailure if there was a problem getting the
|
||||
clean steps.
|
||||
"""
|
||||
node = task.node
|
||||
driver_internal_info = node.driver_internal_info
|
||||
|
||||
# For manual cleaning, the target provision state is MANAGEABLE, whereas
|
||||
# for automated cleaning, it is AVAILABLE.
|
||||
manual_clean = node.target_provision_state == states.MANAGEABLE
|
||||
|
||||
if not manual_clean:
|
||||
# Get the prioritized steps for automated cleaning
|
||||
driver_internal_info['clean_steps'] = _get_cleaning_steps(task,
|
||||
enabled=True)
|
||||
else:
|
||||
# For manual cleaning, the list of cleaning steps was specified by the
|
||||
# user and already saved in node.driver_internal_info['clean_steps'].
|
||||
# Now that we know what the driver's available clean steps are, we can
|
||||
# do further checks to validate the user's clean steps.
|
||||
steps = node.driver_internal_info['clean_steps']
|
||||
driver_internal_info['clean_steps'] = (
|
||||
_validate_user_clean_steps(task, steps))
|
||||
|
||||
node.clean_step = {}
|
||||
driver_internal_info['clean_step_index'] = None
|
||||
node.driver_internal_info = driver_internal_info
|
||||
node.save()
|
||||
|
||||
|
||||
def _get_deployment_templates(task):
|
||||
"""Get deployment templates for task.node.
|
||||
|
||||
Return deployment templates where the name of the deployment template
|
||||
matches one of the node's instance traits (the subset of the node's traits
|
||||
requested by the user via a flavor or image).
|
||||
|
||||
:param task: A TaskManager object
|
||||
:returns: a list of DeployTemplate objects.
|
||||
"""
|
||||
node = task.node
|
||||
if not node.instance_info.get('traits'):
|
||||
return []
|
||||
instance_traits = node.instance_info['traits']
|
||||
return deploy_template.DeployTemplate.list_by_names(task.context,
|
||||
instance_traits)
|
||||
|
||||
|
||||
def _get_steps_from_deployment_templates(task, templates):
|
||||
"""Get deployment template steps for task.node.
|
||||
|
||||
Given a list of deploy template objects, return a list of all deploy steps
|
||||
combined.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param templates: a list of deploy templates
|
||||
:returns: A list of deploy step dictionaries
|
||||
"""
|
||||
steps = []
|
||||
# NOTE(mgoddard): The steps from the object include id, created_at, etc.,
|
||||
# which we don't want to include when we assign them to
|
||||
# node.driver_internal_info. Include only the relevant fields.
|
||||
step_fields = ('interface', 'step', 'args', 'priority')
|
||||
for template in templates:
|
||||
steps.extend([{key: step[key] for key in step_fields}
|
||||
for step in template.steps])
|
||||
return steps
|
||||
|
||||
|
||||
def _get_validated_steps_from_templates(task):
|
||||
"""Return a list of validated deploy steps from deploy templates.
|
||||
|
||||
Deployment template steps are those steps defined in deployment templates
|
||||
where the name of the deployment template matches one of the node's
|
||||
instance traits (the subset of the node's traits requested by the user via
|
||||
a flavor or image). There may be many such matching templates, each with a
|
||||
list of steps to execute.
|
||||
|
||||
This method gathers the steps from all matching deploy templates for a
|
||||
node, and validates those steps against the node's driver interfaces,
|
||||
raising an error if validation fails.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:raises: InvalidParameterValue if validation of steps fails.
|
||||
:raises: InstanceDeployFailure if there was a problem getting the
|
||||
deploy steps.
|
||||
:returns: A list of validated deploy step dictionaries
|
||||
"""
|
||||
# Gather deploy templates matching the node's instance traits.
|
||||
templates = _get_deployment_templates(task)
|
||||
|
||||
# Gather deploy steps from deploy templates.
|
||||
user_steps = _get_steps_from_deployment_templates(task, templates)
|
||||
|
||||
# Validate the steps.
|
||||
error_prefix = (_('Validation of deploy steps from deploy templates '
|
||||
'matching this node\'s instance traits failed. Matching '
|
||||
'deploy templates: %(templates)s. Errors: ') %
|
||||
{'templates': ','.join(t.name for t in templates)})
|
||||
return _validate_user_deploy_steps(task, user_steps,
|
||||
error_prefix=error_prefix)
|
||||
|
||||
|
||||
def _get_all_deployment_steps(task):
|
||||
"""Get deployment steps for task.node.
|
||||
|
||||
Deployment steps from matching deployment templates are combined with those
|
||||
from driver interfaces and all enabled steps returned in priority order.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:raises: InstanceDeployFailure if there was a problem getting the
|
||||
deploy steps.
|
||||
:returns: A list of deploy step dictionaries
|
||||
"""
|
||||
# Gather deploy steps from deploy templates and validate.
|
||||
# NOTE(mgoddard): although we've probably just validated the templates in
|
||||
# do_node_deploy, they may have changed in the DB since we last checked, so
|
||||
# validate again.
|
||||
user_steps = _get_validated_steps_from_templates(task)
|
||||
|
||||
# Gather enabled deploy steps from drivers.
|
||||
driver_steps = _get_deployment_steps(task, enabled=True, sort=False)
|
||||
|
||||
# Remove driver steps that have been disabled or overridden by user steps.
|
||||
user_step_keys = {(s['interface'], s['step']) for s in user_steps}
|
||||
steps = [s for s in driver_steps
|
||||
if (s['interface'], s['step']) not in user_step_keys]
|
||||
|
||||
# Add enabled user steps.
|
||||
enabled_user_steps = [s for s in user_steps if s['priority'] > 0]
|
||||
steps.extend(enabled_user_steps)
|
||||
|
||||
return _sorted_steps(steps, _deploy_step_key)
|
||||
|
||||
|
||||
def set_node_deployment_steps(task):
|
||||
"""Set up the node with deployment step information for deploying.
|
||||
|
||||
Get the deploy steps from the driver.
|
||||
|
||||
:raises: InstanceDeployFailure if there was a problem getting the
|
||||
deployment steps.
|
||||
"""
|
||||
node = task.node
|
||||
driver_internal_info = node.driver_internal_info
|
||||
driver_internal_info['deploy_steps'] = _get_all_deployment_steps(task)
|
||||
node.deploy_step = {}
|
||||
driver_internal_info['deploy_step_index'] = None
|
||||
node.driver_internal_info = driver_internal_info
|
||||
node.save()
|
||||
|
||||
|
||||
def _step_id(step):
|
||||
"""Return the 'ID' of a deploy step.
|
||||
|
||||
The ID is a string, <interface>.<step>.
|
||||
|
||||
:param step: the step dictionary.
|
||||
:return: the step's ID string.
|
||||
"""
|
||||
return '.'.join([step['interface'], step['step']])
|
||||
|
||||
|
||||
def _validate_deploy_steps_unique(user_steps):
|
||||
"""Validate that deploy steps from deploy templates are unique.
|
||||
|
||||
:param user_steps: a list of user steps. A user step is a dictionary
|
||||
with required keys 'interface', 'step', 'args', and 'priority'::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_step>,
|
||||
'args': {<arg1>: <value1>, ..., <argn>: <valuen>},
|
||||
'priority': <priority_of_step> }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': deploy',
|
||||
'step': 'upgrade_firmware',
|
||||
'args': {'force': True},
|
||||
'priority': 10 }
|
||||
|
||||
:return: a list of validation error strings for the steps.
|
||||
"""
|
||||
# Check for duplicate steps. Each interface/step combination can be
|
||||
# specified at most once.
|
||||
errors = []
|
||||
counter = collections.Counter(_step_id(step) for step in user_steps)
|
||||
duplicates = {step_id for step_id, count in counter.items() if count > 1}
|
||||
if duplicates:
|
||||
err = (_('deploy steps from all deploy templates matching this '
|
||||
'node\'s instance traits cannot have the same interface '
|
||||
'and step. Duplicate deploy steps for %(duplicates)s') %
|
||||
{'duplicates': ', '.join(duplicates)})
|
||||
errors.append(err)
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_user_step(task, user_step, driver_step, step_type):
|
||||
"""Validate a user-specified step.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param user_step: a user step dictionary with required keys 'interface'
|
||||
and 'step', and optional keys 'args' and 'priority'::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_step>,
|
||||
'args': {<arg1>: <value1>, ..., <argn>: <valuen>},
|
||||
'priority': <optional_priority> }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': deploy',
|
||||
'step': 'upgrade_firmware',
|
||||
'args': {'force': True} }
|
||||
|
||||
:param driver_step: a driver step dictionary::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_step>,
|
||||
'priority': <integer>
|
||||
'abortable': Optional for clean steps, absent for deploy steps.
|
||||
<Boolean>.
|
||||
'argsinfo': Optional. A dictionary of
|
||||
{<arg_name>:<arg_info_dict>} entries.
|
||||
<arg_info_dict> is a dictionary with
|
||||
{ 'description': <description>,
|
||||
'required': <Boolean> } }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': deploy',
|
||||
'step': 'upgrade_firmware',
|
||||
'priority': 10,
|
||||
'abortable': True,
|
||||
'argsinfo': {
|
||||
'force': { 'description': 'Whether to force the upgrade',
|
||||
'required': False } } }
|
||||
|
||||
:param step_type: either 'clean' or 'deploy'.
|
||||
:return: a list of validation error strings for the step.
|
||||
"""
|
||||
errors = []
|
||||
# Check that the user-specified arguments are valid
|
||||
argsinfo = driver_step.get('argsinfo') or {}
|
||||
user_args = user_step.get('args') or {}
|
||||
unexpected = set(user_args) - set(argsinfo)
|
||||
if unexpected:
|
||||
error = (_('%(type)s step %(step)s has these unexpected arguments: '
|
||||
'%(unexpected)s') %
|
||||
{'type': step_type, 'step': user_step,
|
||||
'unexpected': ', '.join(unexpected)})
|
||||
errors.append(error)
|
||||
|
||||
if step_type == 'clean' or user_step['priority'] > 0:
|
||||
# Check that all required arguments were specified by the user
|
||||
missing = []
|
||||
for (arg_name, arg_info) in argsinfo.items():
|
||||
if arg_info.get('required', False) and arg_name not in user_args:
|
||||
msg = arg_name
|
||||
if arg_info.get('description'):
|
||||
msg += ' (%(desc)s)' % {'desc': arg_info['description']}
|
||||
missing.append(msg)
|
||||
if missing:
|
||||
error = (_('%(type)s step %(step)s is missing these required '
|
||||
'arguments: %(miss)s') %
|
||||
{'type': step_type, 'step': user_step,
|
||||
'miss': ', '.join(missing)})
|
||||
errors.append(error)
|
||||
|
||||
if step_type == 'clean':
|
||||
# Copy fields that should not be provided by a user
|
||||
user_step['abortable'] = driver_step.get('abortable', False)
|
||||
user_step['priority'] = driver_step.get('priority', 0)
|
||||
elif user_step['priority'] > 0:
|
||||
# 'core' deploy steps can only be disabled.
|
||||
|
||||
# NOTE(mgoddard): we'll need something a little more sophisticated to
|
||||
# track core steps once we split out the single core step.
|
||||
is_core = (driver_step['interface'] == 'deploy' and
|
||||
driver_step['step'] == 'deploy')
|
||||
if is_core:
|
||||
error = (_('deploy step %(step)s on interface %(interface)s is a '
|
||||
'core step and cannot be overridden by user steps. It '
|
||||
'may be disabled by setting the priority to 0') %
|
||||
{'step': user_step['step'],
|
||||
'interface': user_step['interface']})
|
||||
errors.append(error)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_user_steps(task, user_steps, driver_steps, step_type,
|
||||
error_prefix=None):
|
||||
"""Validate the user-specified steps.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param user_steps: a list of user steps. A user step is a dictionary
|
||||
with required keys 'interface' and 'step', and optional keys 'args'
|
||||
and 'priority'::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_step>,
|
||||
'args': {<arg1>: <value1>, ..., <argn>: <valuen>},
|
||||
'priority': <optional_priority> }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': deploy',
|
||||
'step': 'upgrade_firmware',
|
||||
'args': {'force': True} }
|
||||
|
||||
:param driver_steps: a list of driver steps::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_step>,
|
||||
'priority': <integer>
|
||||
'abortable': Optional for clean steps, absent for deploy steps.
|
||||
<Boolean>.
|
||||
'argsinfo': Optional. A dictionary of
|
||||
{<arg_name>:<arg_info_dict>} entries.
|
||||
<arg_info_dict> is a dictionary with
|
||||
{ 'description': <description>,
|
||||
'required': <Boolean> } }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': deploy',
|
||||
'step': 'upgrade_firmware',
|
||||
'priority': 10,
|
||||
'abortable': True,
|
||||
'argsinfo': {
|
||||
'force': { 'description': 'Whether to force the upgrade',
|
||||
'required': False } } }
|
||||
|
||||
:param step_type: either 'clean' or 'deploy'.
|
||||
:param error_prefix: String to use as a prefix for exception messages, or
|
||||
None.
|
||||
:raises: InvalidParameterValue if validation of steps fails.
|
||||
:raises: NodeCleaningFailure or InstanceDeployFailure if
|
||||
there was a problem getting the steps from the driver.
|
||||
:return: validated steps updated with information from the driver
|
||||
"""
|
||||
|
||||
errors = []
|
||||
|
||||
# Convert driver steps to a dict.
|
||||
driver_steps = {_step_id(s): s for s in driver_steps}
|
||||
|
||||
for user_step in user_steps:
|
||||
# Check if this user-specified step isn't supported by the driver
|
||||
try:
|
||||
driver_step = driver_steps[_step_id(user_step)]
|
||||
except KeyError:
|
||||
error = (_('node does not support this %(type)s step: %(step)s')
|
||||
% {'type': step_type, 'step': user_step})
|
||||
errors.append(error)
|
||||
continue
|
||||
|
||||
step_errors = _validate_user_step(task, user_step, driver_step,
|
||||
step_type)
|
||||
errors.extend(step_errors)
|
||||
|
||||
if step_type == 'deploy':
|
||||
# Deploy steps should be unique across all combined templates.
|
||||
dup_errors = _validate_deploy_steps_unique(user_steps)
|
||||
errors.extend(dup_errors)
|
||||
|
||||
if errors:
|
||||
err = error_prefix or ''
|
||||
err += '; '.join(errors)
|
||||
raise exception.InvalidParameterValue(err=err)
|
||||
|
||||
return user_steps
|
||||
|
||||
|
||||
def _validate_user_clean_steps(task, user_steps):
|
||||
"""Validate the user-specified clean steps.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param user_steps: a list of clean steps. A clean step is a dictionary
|
||||
with required keys 'interface' and 'step', and optional key 'args'::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_clean_step>,
|
||||
'args': {<arg1>: <value1>, ..., <argn>: <valuen>} }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': 'deploy',
|
||||
'step': 'upgrade_firmware',
|
||||
'args': {'force': True} }
|
||||
:raises: InvalidParameterValue if validation of clean steps fails.
|
||||
:raises: NodeCleaningFailure if there was a problem getting the
|
||||
clean steps from the driver.
|
||||
:return: validated clean steps update with information from the driver
|
||||
"""
|
||||
driver_steps = _get_cleaning_steps(task, enabled=False, sort=False)
|
||||
return _validate_user_steps(task, user_steps, driver_steps, 'clean')
|
||||
|
||||
|
||||
def _validate_user_deploy_steps(task, user_steps, error_prefix=None):
|
||||
"""Validate the user-specified deploy steps.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:param user_steps: a list of deploy steps. A deploy step is a dictionary
|
||||
with required keys 'interface', 'step', 'args', and 'priority'::
|
||||
|
||||
{ 'interface': <driver_interface>,
|
||||
'step': <name_of_deploy_step>,
|
||||
'args': {<arg1>: <value1>, ..., <argn>: <valuen>},
|
||||
'priority': <priority_of_deploy_step> }
|
||||
|
||||
For example::
|
||||
|
||||
{ 'interface': 'bios',
|
||||
'step': 'apply_configuration',
|
||||
'args': { 'settings': [ { 'foo': 'bar' } ] },
|
||||
'priority': 150 }
|
||||
:param error_prefix: String to use as a prefix for exception messages, or
|
||||
None.
|
||||
:raises: InvalidParameterValue if validation of deploy steps fails.
|
||||
:raises: InstanceDeployFailure if there was a problem getting the deploy
|
||||
steps from the driver.
|
||||
:return: validated deploy steps update with information from the driver
|
||||
"""
|
||||
driver_steps = _get_deployment_steps(task, enabled=False, sort=False)
|
||||
return _validate_user_steps(task, user_steps, driver_steps, 'deploy',
|
||||
error_prefix=error_prefix)
|
||||
|
||||
|
||||
@task_manager.require_exclusive_lock
|
||||
def validate_port_physnet(task, port_obj):
|
||||
"""Validate the consistency of physical networks of ports in a portgroup.
|
||||
@ -1372,19 +807,6 @@ def restore_power_state_if_needed(task, power_state_to_restore):
|
||||
node_power_action(task, power_state_to_restore)
|
||||
|
||||
|
||||
def validate_deploy_templates(task):
|
||||
"""Validate the deploy templates for a node.
|
||||
|
||||
:param task: A TaskManager object
|
||||
:raises: InvalidParameterValue if the instance has traits that map to
|
||||
deploy steps that are unsupported by the node's driver interfaces.
|
||||
:raises: InstanceDeployFailure if there was a problem getting the deploy
|
||||
steps from the driver.
|
||||
"""
|
||||
# Gather deploy steps from matching deploy templates and validate them.
|
||||
_get_validated_steps_from_templates(task)
|
||||
|
||||
|
||||
def build_configdrive(node, configdrive):
|
||||
"""Build a configdrive from provided meta_data, network_data and user_data.
|
||||
|
||||
|
@ -1438,7 +1438,7 @@ def clean_step(priority, abortable=False, argsinfo=None):
|
||||
For automated cleaning, only steps with priorities greater than 0 are
|
||||
used. These steps are ordered by priority from highest value to lowest
|
||||
value. For steps with the same priority, they are ordered by driver
|
||||
interface priority (see conductor.manager.CLEANING_INTERFACE_PRIORITY).
|
||||
interface priority (see conductor.steps.CLEANING_INTERFACE_PRIORITY).
|
||||
execute_clean_step() will be called on each step.
|
||||
|
||||
For manual cleaning, the clean steps will be executed in a similar fashion
|
||||
@ -1514,7 +1514,7 @@ def deploy_step(priority, argsinfo=None):
|
||||
Only steps with priorities greater than 0 are used.
|
||||
These steps are ordered by priority from highest value to lowest
|
||||
value. For steps with the same priority, they are ordered by driver
|
||||
interface priority (see conductor.manager.DEPLOYING_INTERFACE_PRIORITY).
|
||||
interface priority (see conductor.steps.DEPLOYING_INTERFACE_PRIORITY).
|
||||
execute_deploy_step() will be called on each step.
|
||||
|
||||
Decorated deploy steps must take as the only positional argument, a
|
||||
|
@ -28,6 +28,7 @@ from ironic.common import boot_devices
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import states
|
||||
from ironic.conductor import steps as conductor_steps
|
||||
from ironic.conductor import utils as manager_utils
|
||||
from ironic.conf import CONF
|
||||
from ironic.drivers.modules import agent_client
|
||||
@ -352,7 +353,7 @@ class HeartbeatMixin(object):
|
||||
# First, cache the clean steps
|
||||
self.refresh_clean_steps(task)
|
||||
# Then set/verify node clean steps and start cleaning
|
||||
manager_utils.set_node_cleaning_steps(task)
|
||||
conductor_steps.set_node_cleaning_steps(task)
|
||||
# The exceptions from RPC are not possible as we using cast
|
||||
# here
|
||||
manager_utils.notify_conductor_resume_clean(task)
|
||||
@ -553,7 +554,7 @@ class AgentDeployMixin(HeartbeatMixin):
|
||||
'clean version mismatch. Resetting clean steps '
|
||||
'and rebooting the node.', node.uuid)
|
||||
try:
|
||||
manager_utils.set_node_cleaning_steps(task)
|
||||
conductor_steps.set_node_cleaning_steps(task)
|
||||
except exception.NodeCleaningFailure:
|
||||
msg = (_('Could not restart automated cleaning on node '
|
||||
'%(node)s: %(err)s.') %
|
||||
|
@ -36,6 +36,7 @@ from ironic.common.i18n import _
|
||||
from ironic.common import images
|
||||
from ironic.common import states
|
||||
from ironic.common import utils
|
||||
from ironic.conductor import steps as conductor_steps
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.conductor import utils as manager_utils
|
||||
from ironic.conf import CONF
|
||||
@ -547,7 +548,7 @@ class AnsibleDeploy(agent_base.HeartbeatMixin, base.DeployInterface):
|
||||
:returns: None or states.CLEANWAIT for async prepare.
|
||||
"""
|
||||
node = task.node
|
||||
manager_utils.set_node_cleaning_steps(task)
|
||||
conductor_steps.set_node_cleaning_steps(task)
|
||||
if not node.driver_internal_info['clean_steps']:
|
||||
# no clean steps configured, nothing to do.
|
||||
return
|
||||
|
@ -41,6 +41,7 @@ from ironic.common import states
|
||||
from ironic.common import swift
|
||||
from ironic.conductor import manager
|
||||
from ironic.conductor import notification_utils
|
||||
from ironic.conductor import steps as conductor_steps
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.conductor import utils as conductor_utils
|
||||
from ironic.db import api as dbapi
|
||||
@ -1325,7 +1326,7 @@ class ServiceDoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin,
|
||||
mock_iwdi):
|
||||
self._test_do_node_deploy_validate_fail(mock_validate, mock_iwdi)
|
||||
|
||||
@mock.patch.object(conductor_utils, 'validate_deploy_templates')
|
||||
@mock.patch.object(conductor_steps, 'validate_deploy_templates')
|
||||
def test_do_node_deploy_validate_template_fail(self, mock_validate,
|
||||
mock_iwdi):
|
||||
self._test_do_node_deploy_validate_fail(mock_validate, mock_iwdi)
|
||||
@ -2238,7 +2239,7 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
@mock.patch.object(manager, '_do_next_deploy_step', autospec=True)
|
||||
@mock.patch.object(manager, '_old_rest_of_do_node_deploy',
|
||||
autospec=True)
|
||||
@mock.patch.object(conductor_utils, 'set_node_deployment_steps',
|
||||
@mock.patch.object(conductor_steps, 'set_node_deployment_steps',
|
||||
autospec=True)
|
||||
def test_do_node_deploy_deprecated(self, mock_set_steps, mock_old_way,
|
||||
mock_deploy_step):
|
||||
@ -2259,7 +2260,7 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
@mock.patch.object(manager, '_do_next_deploy_step', autospec=True)
|
||||
@mock.patch.object(manager, '_old_rest_of_do_node_deploy',
|
||||
autospec=True)
|
||||
@mock.patch.object(conductor_utils, 'set_node_deployment_steps',
|
||||
@mock.patch.object(conductor_steps, 'set_node_deployment_steps',
|
||||
autospec=True)
|
||||
def test_do_node_deploy_steps(self, mock_set_steps, mock_old_way,
|
||||
mock_deploy_step):
|
||||
@ -2288,7 +2289,7 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
@mock.patch.object(manager, '_do_next_deploy_step', autospec=True)
|
||||
@mock.patch.object(manager, '_old_rest_of_do_node_deploy',
|
||||
autospec=True)
|
||||
@mock.patch.object(conductor_utils, 'set_node_deployment_steps',
|
||||
@mock.patch.object(conductor_steps, 'set_node_deployment_steps',
|
||||
autospec=True)
|
||||
def test_do_node_deploy_steps_old_rpc(self, mock_set_steps, mock_old_way,
|
||||
mock_deploy_step):
|
||||
@ -3499,7 +3500,7 @@ class DoNodeCleanTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
self.__do_node_clean_validate_fail(mock_validate, clean_steps=[])
|
||||
|
||||
@mock.patch.object(manager, 'LOG', autospec=True)
|
||||
@mock.patch.object(conductor_utils, 'set_node_cleaning_steps',
|
||||
@mock.patch.object(conductor_steps, 'set_node_cleaning_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.conductor.manager.ConductorManager.'
|
||||
'_do_next_clean_step', autospec=True)
|
||||
@ -3756,7 +3757,7 @@ class DoNodeCleanTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
self.__do_node_clean_prepare_clean_wait(clean_steps=[self.deploy_raid])
|
||||
|
||||
@mock.patch.object(n_flat.FlatNetwork, 'validate', autospec=True)
|
||||
@mock.patch.object(conductor_utils, 'set_node_cleaning_steps',
|
||||
@mock.patch.object(conductor_steps, 'set_node_cleaning_steps',
|
||||
autospec=True)
|
||||
def __do_node_clean_steps_fail(self, mock_steps, mock_validate,
|
||||
clean_steps=None, invalid_exc=True):
|
||||
@ -3788,7 +3789,7 @@ class DoNodeCleanTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
|
||||
self.__do_node_clean_steps_fail(clean_steps=[self.deploy_raid],
|
||||
invalid_exc=invalid)
|
||||
|
||||
@mock.patch.object(conductor_utils, 'set_node_cleaning_steps',
|
||||
@mock.patch.object(conductor_steps, 'set_node_cleaning_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.conductor.manager.ConductorManager.'
|
||||
'_do_next_clean_step', autospec=True)
|
||||
@ -4852,7 +4853,7 @@ class MiscTestCase(mgr_utils.ServiceSetUpMixin, mgr_utils.CommonMixIn,
|
||||
node = obj_utils.create_test_node(self.context, driver='fake-hardware',
|
||||
network_interface='noop')
|
||||
with mock.patch(
|
||||
'ironic.conductor.utils.validate_deploy_templates'
|
||||
'ironic.conductor.steps.validate_deploy_templates'
|
||||
) as mock_validate:
|
||||
reason = 'fake reason'
|
||||
mock_validate.side_effect = exception.InvalidParameterValue(reason)
|
||||
|
724
ironic/tests/unit/conductor/test_steps.py
Normal file
724
ironic/tests/unit/conductor/test_steps.py
Normal file
@ -0,0 +1,724 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import states
|
||||
from ironic.conductor import steps as conductor_steps
|
||||
from ironic.conductor import task_manager
|
||||
from ironic import objects
|
||||
from ironic.tests.unit.db import base as db_base
|
||||
from ironic.tests.unit.db import utils as db_utils
|
||||
from ironic.tests.unit.objects import utils as obj_utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class NodeDeployStepsTestCase(db_base.DbTestCase):
|
||||
def setUp(self):
|
||||
super(NodeDeployStepsTestCase, self).setUp()
|
||||
|
||||
self.deploy_start = {
|
||||
'step': 'deploy_start', 'priority': 50, 'interface': 'deploy'}
|
||||
self.power_one = {
|
||||
'step': 'power_one', 'priority': 40, 'interface': 'power'}
|
||||
self.deploy_middle = {
|
||||
'step': 'deploy_middle', 'priority': 40, 'interface': 'deploy'}
|
||||
self.deploy_end = {
|
||||
'step': 'deploy_end', 'priority': 20, 'interface': 'deploy'}
|
||||
self.power_disable = {
|
||||
'step': 'power_disable', 'priority': 0, 'interface': 'power'}
|
||||
self.deploy_core = {
|
||||
'step': 'deploy', 'priority': 100, 'interface': 'deploy'}
|
||||
# enabled steps
|
||||
self.deploy_steps = [self.deploy_start, self.power_one,
|
||||
self.deploy_middle, self.deploy_end]
|
||||
# Deploy step with argsinfo.
|
||||
self.deploy_raid = {
|
||||
'step': 'build_raid', 'priority': 0, 'interface': 'deploy',
|
||||
'argsinfo': {'arg1': {'description': 'desc1', 'required': True},
|
||||
'arg2': {'description': 'desc2'}}}
|
||||
self.node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware')
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_deploy_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.get_deploy_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeManagement.get_deploy_steps',
|
||||
autospec=True)
|
||||
def test__get_deployment_steps(self, mock_mgt_steps, mock_power_steps,
|
||||
mock_deploy_steps):
|
||||
# Test getting deploy steps, with one driver returning None, two
|
||||
# conflicting priorities, and asserting they are ordered properly.
|
||||
|
||||
mock_power_steps.return_value = [self.power_disable, self.power_one]
|
||||
mock_deploy_steps.return_value = [
|
||||
self.deploy_start, self.deploy_middle, self.deploy_end]
|
||||
|
||||
expected = self.deploy_steps + [self.power_disable]
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
steps = conductor_steps._get_deployment_steps(task, enabled=False)
|
||||
|
||||
self.assertEqual(expected, steps)
|
||||
mock_mgt_steps.assert_called_once_with(mock.ANY, task)
|
||||
mock_power_steps.assert_called_once_with(mock.ANY, task)
|
||||
mock_deploy_steps.assert_called_once_with(mock.ANY, task)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_deploy_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.get_deploy_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeManagement.get_deploy_steps',
|
||||
autospec=True)
|
||||
def test__get_deploy_steps_unsorted(self, mock_mgt_steps, mock_power_steps,
|
||||
mock_deploy_steps):
|
||||
|
||||
mock_deploy_steps.return_value = [self.deploy_end,
|
||||
self.deploy_start,
|
||||
self.deploy_middle]
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
steps = conductor_steps._get_deployment_steps(task, enabled=False,
|
||||
sort=False)
|
||||
self.assertEqual(mock_deploy_steps.return_value, steps)
|
||||
mock_mgt_steps.assert_called_once_with(mock.ANY, task)
|
||||
mock_power_steps.assert_called_once_with(mock.ANY, task)
|
||||
mock_deploy_steps.assert_called_once_with(mock.ANY, task)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_deploy_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.get_deploy_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeManagement.get_deploy_steps',
|
||||
autospec=True)
|
||||
def test__get_deployment_steps_only_enabled(
|
||||
self, mock_mgt_steps, mock_power_steps, mock_deploy_steps):
|
||||
# Test getting only deploy steps, with one driver returning None, two
|
||||
# conflicting priorities, and asserting they are ordered properly.
|
||||
# Should discard zero-priority deploy step.
|
||||
|
||||
mock_power_steps.return_value = [self.power_one, self.power_disable]
|
||||
mock_deploy_steps.return_value = [self.deploy_end,
|
||||
self.deploy_middle,
|
||||
self.deploy_start]
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=True) as task:
|
||||
steps = conductor_steps._get_deployment_steps(task, enabled=True)
|
||||
|
||||
self.assertEqual(self.deploy_steps, steps)
|
||||
mock_mgt_steps.assert_called_once_with(mock.ANY, task)
|
||||
mock_power_steps.assert_called_once_with(mock.ANY, task)
|
||||
mock_deploy_steps.assert_called_once_with(mock.ANY, task)
|
||||
|
||||
@mock.patch.object(objects.DeployTemplate, 'list_by_names')
|
||||
def test__get_deployment_templates_no_traits(self, mock_list):
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
templates = conductor_steps._get_deployment_templates(task)
|
||||
self.assertEqual([], templates)
|
||||
self.assertFalse(mock_list.called)
|
||||
|
||||
@mock.patch.object(objects.DeployTemplate, 'list_by_names')
|
||||
def test__get_deployment_templates(self, mock_list):
|
||||
traits = ['CUSTOM_DT1', 'CUSTOM_DT2']
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, uuid=uuidutils.generate_uuid(),
|
||||
instance_info={'traits': traits})
|
||||
template1 = obj_utils.get_test_deploy_template(self.context)
|
||||
template2 = obj_utils.get_test_deploy_template(
|
||||
self.context, name='CUSTOM_DT2', uuid=uuidutils.generate_uuid(),
|
||||
steps=[{'interface': 'bios', 'step': 'apply_configuration',
|
||||
'args': {}, 'priority': 1}])
|
||||
mock_list.return_value = [template1, template2]
|
||||
expected = [template1, template2]
|
||||
with task_manager.acquire(
|
||||
self.context, node.uuid, shared=False) as task:
|
||||
templates = conductor_steps._get_deployment_templates(task)
|
||||
self.assertEqual(expected, templates)
|
||||
mock_list.assert_called_once_with(task.context, traits)
|
||||
|
||||
def test__get_steps_from_deployment_templates(self):
|
||||
template1 = obj_utils.get_test_deploy_template(self.context)
|
||||
template2 = obj_utils.get_test_deploy_template(
|
||||
self.context, name='CUSTOM_DT2', uuid=uuidutils.generate_uuid(),
|
||||
steps=[{'interface': 'bios', 'step': 'apply_configuration',
|
||||
'args': {}, 'priority': 1}])
|
||||
step1 = template1.steps[0]
|
||||
step2 = template2.steps[0]
|
||||
expected = [
|
||||
{
|
||||
'interface': step1['interface'],
|
||||
'step': step1['step'],
|
||||
'args': step1['args'],
|
||||
'priority': step1['priority'],
|
||||
},
|
||||
{
|
||||
'interface': step2['interface'],
|
||||
'step': step2['step'],
|
||||
'args': step2['args'],
|
||||
'priority': step2['priority'],
|
||||
}
|
||||
]
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
steps = conductor_steps._get_steps_from_deployment_templates(
|
||||
task, [template1, template2])
|
||||
self.assertEqual(expected, steps)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_validated_steps_from_templates',
|
||||
autospec=True)
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_steps', autospec=True)
|
||||
def _test__get_all_deployment_steps(self, user_steps, driver_steps,
|
||||
expected_steps, mock_steps,
|
||||
mock_validated):
|
||||
mock_validated.return_value = user_steps
|
||||
mock_steps.return_value = driver_steps
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
steps = conductor_steps._get_all_deployment_steps(task)
|
||||
self.assertEqual(expected_steps, steps)
|
||||
mock_validated.assert_called_once_with(task)
|
||||
mock_steps.assert_called_once_with(task, enabled=True, sort=False)
|
||||
|
||||
def test__get_all_deployment_steps_no_steps(self):
|
||||
# Nothing in -> nothing out.
|
||||
user_steps = []
|
||||
driver_steps = []
|
||||
expected_steps = []
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
def test__get_all_deployment_steps_no_user_steps(self):
|
||||
# Only driver steps in -> only driver steps out.
|
||||
user_steps = []
|
||||
driver_steps = self.deploy_steps
|
||||
expected_steps = self.deploy_steps
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
def test__get_all_deployment_steps_no_driver_steps(self):
|
||||
# Only user steps in -> only user steps out.
|
||||
user_steps = self.deploy_steps
|
||||
driver_steps = []
|
||||
expected_steps = self.deploy_steps
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
def test__get_all_deployment_steps_user_and_driver_steps(self):
|
||||
# Driver and user steps in -> driver and user steps out.
|
||||
user_steps = self.deploy_steps[:2]
|
||||
driver_steps = self.deploy_steps[2:]
|
||||
expected_steps = self.deploy_steps
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
def test__get_all_deployment_steps_disable_core_steps(self):
|
||||
# User steps can disable core driver steps.
|
||||
user_steps = [self.deploy_core.copy()]
|
||||
user_steps[0].update({'priority': 0})
|
||||
driver_steps = [self.deploy_core]
|
||||
expected_steps = []
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
def test__get_all_deployment_steps_override_driver_steps(self):
|
||||
# User steps override non-core driver steps.
|
||||
user_steps = [step.copy() for step in self.deploy_steps[:2]]
|
||||
user_steps[0].update({'priority': 200})
|
||||
user_steps[1].update({'priority': 100})
|
||||
driver_steps = self.deploy_steps
|
||||
expected_steps = user_steps + self.deploy_steps[2:]
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
def test__get_all_deployment_steps_duplicate_user_steps(self):
|
||||
# Duplicate user steps override non-core driver steps.
|
||||
|
||||
# NOTE(mgoddard): This case is currently prevented by the API and
|
||||
# conductor - the interface/step must be unique across all enabled
|
||||
# steps. This test ensures that we can support this case, in case we
|
||||
# choose to allow it in future.
|
||||
user_steps = [self.deploy_start.copy(), self.deploy_start.copy()]
|
||||
user_steps[0].update({'priority': 200})
|
||||
user_steps[1].update({'priority': 100})
|
||||
driver_steps = self.deploy_steps
|
||||
# Each user invocation of the deploy_start step should be included, but
|
||||
# not the default deploy_start from the driver.
|
||||
expected_steps = user_steps + self.deploy_steps[1:]
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_validated_steps_from_templates',
|
||||
autospec=True)
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_steps', autospec=True)
|
||||
def test__get_all_deployment_steps_error(self, mock_steps, mock_validated):
|
||||
mock_validated.side_effect = exception.InvalidParameterValue('foo')
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
conductor_steps._get_all_deployment_steps, task)
|
||||
mock_validated.assert_called_once_with(task)
|
||||
self.assertFalse(mock_steps.called)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_all_deployment_steps',
|
||||
autospec=True)
|
||||
def test_set_node_deployment_steps(self, mock_steps):
|
||||
mock_steps.return_value = self.deploy_steps
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
conductor_steps.set_node_deployment_steps(task)
|
||||
self.node.refresh()
|
||||
self.assertEqual(self.deploy_steps,
|
||||
self.node.driver_internal_info['deploy_steps'])
|
||||
self.assertEqual({}, self.node.deploy_step)
|
||||
self.assertIsNone(
|
||||
self.node.driver_internal_info['deploy_step_index'])
|
||||
mock_steps.assert_called_once_with(task)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps(self, mock_steps):
|
||||
mock_steps.return_value = self.deploy_steps
|
||||
|
||||
user_steps = [{'step': 'deploy_start', 'interface': 'deploy',
|
||||
'priority': 100},
|
||||
{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = conductor_steps._validate_user_deploy_steps(task,
|
||||
user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
self.assertEqual(user_steps, result)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_no_steps(self, mock_steps):
|
||||
mock_steps.return_value = self.deploy_steps
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
conductor_steps._validate_user_deploy_steps(task, [])
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_get_steps_exception(self, mock_steps):
|
||||
mock_steps.side_effect = exception.InstanceDeployFailure('bad')
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaises(exception.InstanceDeployFailure,
|
||||
conductor_steps._validate_user_deploy_steps,
|
||||
task, [])
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_not_supported(self, mock_steps):
|
||||
mock_steps.return_value = self.deploy_steps
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200},
|
||||
{'step': 'bad_step', 'interface': 'deploy',
|
||||
'priority': 100}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"does not support.*bad_step",
|
||||
conductor_steps._validate_user_deploy_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_invalid_arg(self, mock_steps):
|
||||
mock_steps.return_value = self.deploy_steps
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'args': {'arg1': 'val1', 'arg2': 'val2'},
|
||||
'priority': 200}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"power_one.*unexpected.*arg1",
|
||||
conductor_steps._validate_user_deploy_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_missing_required_arg(self,
|
||||
mock_steps):
|
||||
mock_steps.return_value = [self.power_one, self.deploy_raid]
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200},
|
||||
{'step': 'build_raid', 'interface': 'deploy',
|
||||
'priority': 100}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"build_raid.*missing.*arg1",
|
||||
conductor_steps._validate_user_deploy_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_disable_non_core(self, mock_steps):
|
||||
# Required arguments don't apply to disabled steps.
|
||||
mock_steps.return_value = [self.power_one, self.deploy_raid]
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200},
|
||||
{'step': 'build_raid', 'interface': 'deploy',
|
||||
'priority': 0}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = conductor_steps._validate_user_deploy_steps(task,
|
||||
user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
self.assertEqual(user_steps, result)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_disable_core(self, mock_steps):
|
||||
mock_steps.return_value = [self.power_one, self.deploy_core]
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200},
|
||||
{'step': 'deploy', 'interface': 'deploy', 'priority': 0}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = conductor_steps._validate_user_deploy_steps(task,
|
||||
user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
self.assertEqual(user_steps, result)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_override_core(self, mock_steps):
|
||||
mock_steps.return_value = [self.power_one, self.deploy_core]
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200},
|
||||
{'step': 'deploy', 'interface': 'deploy',
|
||||
'priority': 200}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"deploy.*is a core step",
|
||||
conductor_steps._validate_user_deploy_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_duplicates(self, mock_steps):
|
||||
mock_steps.return_value = [self.power_one, self.deploy_core]
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200},
|
||||
{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 100}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"Duplicate deploy steps for "
|
||||
"power.power_one",
|
||||
conductor_steps._validate_user_deploy_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
|
||||
class NodeCleaningStepsTestCase(db_base.DbTestCase):
|
||||
def setUp(self):
|
||||
super(NodeCleaningStepsTestCase, self).setUp()
|
||||
|
||||
self.power_update = {
|
||||
'step': 'update_firmware', 'priority': 10, 'interface': 'power'}
|
||||
self.deploy_update = {
|
||||
'step': 'update_firmware', 'priority': 10, 'interface': 'deploy'}
|
||||
self.deploy_erase = {
|
||||
'step': 'erase_disks', 'priority': 20, 'interface': 'deploy',
|
||||
'abortable': True}
|
||||
# Automated cleaning should be executed in this order
|
||||
self.clean_steps = [self.deploy_erase, self.power_update,
|
||||
self.deploy_update]
|
||||
# Manual clean step
|
||||
self.deploy_raid = {
|
||||
'step': 'build_raid', 'priority': 0, 'interface': 'deploy',
|
||||
'argsinfo': {'arg1': {'description': 'desc1', 'required': True},
|
||||
'arg2': {'description': 'desc2'}}}
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',
|
||||
lambda self, task: [])
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_clean_steps')
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.get_clean_steps')
|
||||
def test__get_cleaning_steps(self, mock_power_steps, mock_deploy_steps):
|
||||
# Test getting cleaning steps, with one driver returning None, two
|
||||
# conflicting priorities, and asserting they are ordered properly.
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.CLEANING,
|
||||
target_provision_state=states.AVAILABLE)
|
||||
|
||||
mock_power_steps.return_value = [self.power_update]
|
||||
mock_deploy_steps.return_value = [self.deploy_erase,
|
||||
self.deploy_update]
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, node.uuid, shared=False) as task:
|
||||
steps = conductor_steps._get_cleaning_steps(task, enabled=False)
|
||||
|
||||
self.assertEqual(self.clean_steps, steps)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',
|
||||
lambda self, task: [])
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_clean_steps')
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.get_clean_steps')
|
||||
def test__get_cleaning_steps_unsorted(self, mock_power_steps,
|
||||
mock_deploy_steps):
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.CLEANING,
|
||||
target_provision_state=states.MANAGEABLE)
|
||||
|
||||
mock_deploy_steps.return_value = [self.deploy_raid,
|
||||
self.deploy_update,
|
||||
self.deploy_erase]
|
||||
with task_manager.acquire(
|
||||
self.context, node.uuid, shared=False) as task:
|
||||
steps = conductor_steps._get_cleaning_steps(task, enabled=False,
|
||||
sort=False)
|
||||
self.assertEqual(mock_deploy_steps.return_value, steps)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_clean_steps')
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.get_clean_steps')
|
||||
def test__get_cleaning_steps_only_enabled(self, mock_power_steps,
|
||||
mock_deploy_steps):
|
||||
# Test getting only cleaning steps, with one driver returning None, two
|
||||
# conflicting priorities, and asserting they are ordered properly.
|
||||
# Should discard zero-priority (manual) clean step
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.CLEANING,
|
||||
target_provision_state=states.AVAILABLE)
|
||||
|
||||
mock_power_steps.return_value = [self.power_update]
|
||||
mock_deploy_steps.return_value = [self.deploy_erase,
|
||||
self.deploy_update,
|
||||
self.deploy_raid]
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, node.uuid, shared=True) as task:
|
||||
steps = conductor_steps._get_cleaning_steps(task, enabled=True)
|
||||
|
||||
self.assertEqual(self.clean_steps, steps)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_validate_user_clean_steps')
|
||||
@mock.patch.object(conductor_steps, '_get_cleaning_steps')
|
||||
def test_set_node_cleaning_steps_automated(self, mock_steps,
|
||||
mock_validate_user_steps):
|
||||
mock_steps.return_value = self.clean_steps
|
||||
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.CLEANING,
|
||||
target_provision_state=states.AVAILABLE,
|
||||
last_error=None,
|
||||
clean_step=None)
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, node.uuid, shared=False) as task:
|
||||
conductor_steps.set_node_cleaning_steps(task)
|
||||
node.refresh()
|
||||
self.assertEqual(self.clean_steps,
|
||||
node.driver_internal_info['clean_steps'])
|
||||
self.assertEqual({}, node.clean_step)
|
||||
mock_steps.assert_called_once_with(task, enabled=True)
|
||||
self.assertFalse(mock_validate_user_steps.called)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_validate_user_clean_steps')
|
||||
@mock.patch.object(conductor_steps, '_get_cleaning_steps')
|
||||
def test_set_node_cleaning_steps_manual(self, mock_steps,
|
||||
mock_validate_user_steps):
|
||||
clean_steps = [self.deploy_raid]
|
||||
mock_steps.return_value = self.clean_steps
|
||||
mock_validate_user_steps.return_value = clean_steps
|
||||
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.CLEANING,
|
||||
target_provision_state=states.MANAGEABLE,
|
||||
last_error=None,
|
||||
clean_step=None,
|
||||
driver_internal_info={'clean_steps': clean_steps})
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, node.uuid, shared=False) as task:
|
||||
conductor_steps.set_node_cleaning_steps(task)
|
||||
node.refresh()
|
||||
self.assertEqual(clean_steps,
|
||||
node.driver_internal_info['clean_steps'])
|
||||
self.assertEqual({}, node.clean_step)
|
||||
self.assertFalse(mock_steps.called)
|
||||
mock_validate_user_steps.assert_called_once_with(task, clean_steps)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_cleaning_steps')
|
||||
def test__validate_user_clean_steps(self, mock_steps):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_steps.return_value = self.clean_steps
|
||||
|
||||
user_steps = [{'step': 'update_firmware', 'interface': 'power'},
|
||||
{'step': 'erase_disks', 'interface': 'deploy'}]
|
||||
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
result = conductor_steps._validate_user_clean_steps(task,
|
||||
user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
expected = [{'step': 'update_firmware', 'interface': 'power',
|
||||
'priority': 10, 'abortable': False},
|
||||
{'step': 'erase_disks', 'interface': 'deploy',
|
||||
'priority': 20, 'abortable': True}]
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_cleaning_steps')
|
||||
def test__validate_user_clean_steps_no_steps(self, mock_steps):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_steps.return_value = self.clean_steps
|
||||
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
conductor_steps._validate_user_clean_steps(task, [])
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_cleaning_steps')
|
||||
def test__validate_user_clean_steps_get_steps_exception(self, mock_steps):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_steps.side_effect = exception.NodeCleaningFailure('bad')
|
||||
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
self.assertRaises(exception.NodeCleaningFailure,
|
||||
conductor_steps._validate_user_clean_steps,
|
||||
task, [])
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_cleaning_steps')
|
||||
def test__validate_user_clean_steps_not_supported(self, mock_steps):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_steps.return_value = [self.power_update, self.deploy_raid]
|
||||
user_steps = [{'step': 'update_firmware', 'interface': 'power'},
|
||||
{'step': 'bad_step', 'interface': 'deploy'}]
|
||||
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"does not support.*bad_step",
|
||||
conductor_steps._validate_user_clean_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_cleaning_steps')
|
||||
def test__validate_user_clean_steps_invalid_arg(self, mock_steps):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_steps.return_value = self.clean_steps
|
||||
user_steps = [{'step': 'update_firmware', 'interface': 'power',
|
||||
'args': {'arg1': 'val1', 'arg2': 'val2'}},
|
||||
{'step': 'erase_disks', 'interface': 'deploy'}]
|
||||
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"update_firmware.*unexpected.*arg1",
|
||||
conductor_steps._validate_user_clean_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_cleaning_steps')
|
||||
def test__validate_user_clean_steps_missing_required_arg(self, mock_steps):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_steps.return_value = [self.power_update, self.deploy_raid]
|
||||
user_steps = [{'step': 'update_firmware', 'interface': 'power'},
|
||||
{'step': 'build_raid', 'interface': 'deploy'}]
|
||||
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"build_raid.*missing.*arg1",
|
||||
conductor_steps._validate_user_clean_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_deployment_templates',
|
||||
autospec=True)
|
||||
@mock.patch.object(conductor_steps, '_get_steps_from_deployment_templates',
|
||||
autospec=True)
|
||||
@mock.patch.object(conductor_steps, '_validate_user_deploy_steps',
|
||||
autospec=True)
|
||||
class GetValidatedStepsFromTemplatesTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(GetValidatedStepsFromTemplatesTestCase, self).setUp()
|
||||
self.node = obj_utils.create_test_node(self.context,
|
||||
driver='fake-hardware')
|
||||
self.template = obj_utils.get_test_deploy_template(self.context)
|
||||
|
||||
def test_ok(self, mock_validate, mock_steps, mock_templates):
|
||||
mock_templates.return_value = [self.template]
|
||||
steps = [db_utils.get_test_deploy_template_step()]
|
||||
mock_steps.return_value = steps
|
||||
mock_validate.return_value = steps
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
result = conductor_steps._get_validated_steps_from_templates(task)
|
||||
self.assertEqual(steps, result)
|
||||
mock_templates.assert_called_once_with(task)
|
||||
mock_steps.assert_called_once_with(task, [self.template])
|
||||
mock_validate.assert_called_once_with(task, steps, mock.ANY)
|
||||
|
||||
def test_invalid_parameter_value(self, mock_validate, mock_steps,
|
||||
mock_templates):
|
||||
mock_templates.return_value = [self.template]
|
||||
mock_validate.side_effect = exception.InvalidParameterValue('fake')
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
conductor_steps._get_validated_steps_from_templates, task)
|
||||
|
||||
def test_instance_deploy_failure(self, mock_validate, mock_steps,
|
||||
mock_templates):
|
||||
mock_templates.return_value = [self.template]
|
||||
mock_validate.side_effect = exception.InstanceDeployFailure('foo')
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
self.assertRaises(
|
||||
exception.InstanceDeployFailure,
|
||||
conductor_steps._get_validated_steps_from_templates, task)
|
||||
|
||||
|
||||
@mock.patch.object(conductor_steps, '_get_validated_steps_from_templates',
|
||||
autospec=True)
|
||||
class ValidateDeployTemplatesTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ValidateDeployTemplatesTestCase, self).setUp()
|
||||
self.node = obj_utils.create_test_node(self.context,
|
||||
driver='fake-hardware')
|
||||
|
||||
def test_ok(self, mock_validated):
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
result = conductor_steps.validate_deploy_templates(task)
|
||||
self.assertIsNone(result)
|
||||
mock_validated.assert_called_once_with(task)
|
||||
|
||||
def test_error(self, mock_validated):
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
mock_validated.side_effect = exception.InvalidParameterValue('foo')
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
conductor_steps.validate_deploy_templates, task)
|
||||
mock_validated.assert_called_once_with(task)
|
@ -967,631 +967,6 @@ class DeployingErrorHandlerTestCase(tests_base.TestCase):
|
||||
self.task.process_event.assert_called_once_with('fail')
|
||||
|
||||
|
||||
class NodeDeployStepsTestCase(db_base.DbTestCase):
|
||||
def setUp(self):
|
||||
super(NodeDeployStepsTestCase, self).setUp()
|
||||
|
||||
self.deploy_start = {
|
||||
'step': 'deploy_start', 'priority': 50, 'interface': 'deploy'}
|
||||
self.power_one = {
|
||||
'step': 'power_one', 'priority': 40, 'interface': 'power'}
|
||||
self.deploy_middle = {
|
||||
'step': 'deploy_middle', 'priority': 40, 'interface': 'deploy'}
|
||||
self.deploy_end = {
|
||||
'step': 'deploy_end', 'priority': 20, 'interface': 'deploy'}
|
||||
self.power_disable = {
|
||||
'step': 'power_disable', 'priority': 0, 'interface': 'power'}
|
||||
self.deploy_core = {
|
||||
'step': 'deploy', 'priority': 100, 'interface': 'deploy'}
|
||||
# enabled steps
|
||||
self.deploy_steps = [self.deploy_start, self.power_one,
|
||||
self.deploy_middle, self.deploy_end]
|
||||
# Deploy step with argsinfo.
|
||||
self.deploy_raid = {
|
||||
'step': 'build_raid', 'priority': 0, 'interface': 'deploy',
|
||||
'argsinfo': {'arg1': {'description': 'desc1', 'required': True},
|
||||
'arg2': {'description': 'desc2'}}}
|
||||
self.node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware')
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_deploy_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.get_deploy_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeManagement.get_deploy_steps',
|
||||
autospec=True)
|
||||
def test__get_deployment_steps(self, mock_mgt_steps, mock_power_steps,
|
||||
mock_deploy_steps):
|
||||
# Test getting deploy steps, with one driver returning None, two
|
||||
# conflicting priorities, and asserting they are ordered properly.
|
||||
|
||||
mock_power_steps.return_value = [self.power_disable, self.power_one]
|
||||
mock_deploy_steps.return_value = [
|
||||
self.deploy_start, self.deploy_middle, self.deploy_end]
|
||||
|
||||
expected = self.deploy_steps + [self.power_disable]
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
steps = conductor_utils._get_deployment_steps(task, enabled=False)
|
||||
|
||||
self.assertEqual(expected, steps)
|
||||
mock_mgt_steps.assert_called_once_with(mock.ANY, task)
|
||||
mock_power_steps.assert_called_once_with(mock.ANY, task)
|
||||
mock_deploy_steps.assert_called_once_with(mock.ANY, task)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_deploy_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.get_deploy_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeManagement.get_deploy_steps',
|
||||
autospec=True)
|
||||
def test__get_deploy_steps_unsorted(self, mock_mgt_steps, mock_power_steps,
|
||||
mock_deploy_steps):
|
||||
|
||||
mock_deploy_steps.return_value = [self.deploy_end,
|
||||
self.deploy_start,
|
||||
self.deploy_middle]
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
steps = conductor_utils._get_deployment_steps(task, enabled=False,
|
||||
sort=False)
|
||||
self.assertEqual(mock_deploy_steps.return_value, steps)
|
||||
mock_mgt_steps.assert_called_once_with(mock.ANY, task)
|
||||
mock_power_steps.assert_called_once_with(mock.ANY, task)
|
||||
mock_deploy_steps.assert_called_once_with(mock.ANY, task)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_deploy_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.get_deploy_steps',
|
||||
autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeManagement.get_deploy_steps',
|
||||
autospec=True)
|
||||
def test__get_deployment_steps_only_enabled(
|
||||
self, mock_mgt_steps, mock_power_steps, mock_deploy_steps):
|
||||
# Test getting only deploy steps, with one driver returning None, two
|
||||
# conflicting priorities, and asserting they are ordered properly.
|
||||
# Should discard zero-priority deploy step.
|
||||
|
||||
mock_power_steps.return_value = [self.power_one, self.power_disable]
|
||||
mock_deploy_steps.return_value = [self.deploy_end,
|
||||
self.deploy_middle,
|
||||
self.deploy_start]
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=True) as task:
|
||||
steps = conductor_utils._get_deployment_steps(task, enabled=True)
|
||||
|
||||
self.assertEqual(self.deploy_steps, steps)
|
||||
mock_mgt_steps.assert_called_once_with(mock.ANY, task)
|
||||
mock_power_steps.assert_called_once_with(mock.ANY, task)
|
||||
mock_deploy_steps.assert_called_once_with(mock.ANY, task)
|
||||
|
||||
@mock.patch.object(objects.DeployTemplate, 'list_by_names')
|
||||
def test__get_deployment_templates_no_traits(self, mock_list):
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
templates = conductor_utils._get_deployment_templates(task)
|
||||
self.assertEqual([], templates)
|
||||
self.assertFalse(mock_list.called)
|
||||
|
||||
@mock.patch.object(objects.DeployTemplate, 'list_by_names')
|
||||
def test__get_deployment_templates(self, mock_list):
|
||||
traits = ['CUSTOM_DT1', 'CUSTOM_DT2']
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, uuid=uuidutils.generate_uuid(),
|
||||
instance_info={'traits': traits})
|
||||
template1 = obj_utils.get_test_deploy_template(self.context)
|
||||
template2 = obj_utils.get_test_deploy_template(
|
||||
self.context, name='CUSTOM_DT2', uuid=uuidutils.generate_uuid(),
|
||||
steps=[{'interface': 'bios', 'step': 'apply_configuration',
|
||||
'args': {}, 'priority': 1}])
|
||||
mock_list.return_value = [template1, template2]
|
||||
expected = [template1, template2]
|
||||
with task_manager.acquire(
|
||||
self.context, node.uuid, shared=False) as task:
|
||||
templates = conductor_utils._get_deployment_templates(task)
|
||||
self.assertEqual(expected, templates)
|
||||
mock_list.assert_called_once_with(task.context, traits)
|
||||
|
||||
def test__get_steps_from_deployment_templates(self):
|
||||
template1 = obj_utils.get_test_deploy_template(self.context)
|
||||
template2 = obj_utils.get_test_deploy_template(
|
||||
self.context, name='CUSTOM_DT2', uuid=uuidutils.generate_uuid(),
|
||||
steps=[{'interface': 'bios', 'step': 'apply_configuration',
|
||||
'args': {}, 'priority': 1}])
|
||||
step1 = template1.steps[0]
|
||||
step2 = template2.steps[0]
|
||||
expected = [
|
||||
{
|
||||
'interface': step1['interface'],
|
||||
'step': step1['step'],
|
||||
'args': step1['args'],
|
||||
'priority': step1['priority'],
|
||||
},
|
||||
{
|
||||
'interface': step2['interface'],
|
||||
'step': step2['step'],
|
||||
'args': step2['args'],
|
||||
'priority': step2['priority'],
|
||||
}
|
||||
]
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
steps = conductor_utils._get_steps_from_deployment_templates(
|
||||
task, [template1, template2])
|
||||
self.assertEqual(expected, steps)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_validated_steps_from_templates',
|
||||
autospec=True)
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
|
||||
def _test__get_all_deployment_steps(self, user_steps, driver_steps,
|
||||
expected_steps, mock_steps,
|
||||
mock_validated):
|
||||
mock_validated.return_value = user_steps
|
||||
mock_steps.return_value = driver_steps
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
steps = conductor_utils._get_all_deployment_steps(task)
|
||||
self.assertEqual(expected_steps, steps)
|
||||
mock_validated.assert_called_once_with(task)
|
||||
mock_steps.assert_called_once_with(task, enabled=True, sort=False)
|
||||
|
||||
def test__get_all_deployment_steps_no_steps(self):
|
||||
# Nothing in -> nothing out.
|
||||
user_steps = []
|
||||
driver_steps = []
|
||||
expected_steps = []
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
def test__get_all_deployment_steps_no_user_steps(self):
|
||||
# Only driver steps in -> only driver steps out.
|
||||
user_steps = []
|
||||
driver_steps = self.deploy_steps
|
||||
expected_steps = self.deploy_steps
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
def test__get_all_deployment_steps_no_driver_steps(self):
|
||||
# Only user steps in -> only user steps out.
|
||||
user_steps = self.deploy_steps
|
||||
driver_steps = []
|
||||
expected_steps = self.deploy_steps
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
def test__get_all_deployment_steps_user_and_driver_steps(self):
|
||||
# Driver and user steps in -> driver and user steps out.
|
||||
user_steps = self.deploy_steps[:2]
|
||||
driver_steps = self.deploy_steps[2:]
|
||||
expected_steps = self.deploy_steps
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
def test__get_all_deployment_steps_disable_core_steps(self):
|
||||
# User steps can disable core driver steps.
|
||||
user_steps = [self.deploy_core.copy()]
|
||||
user_steps[0].update({'priority': 0})
|
||||
driver_steps = [self.deploy_core]
|
||||
expected_steps = []
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
def test__get_all_deployment_steps_override_driver_steps(self):
|
||||
# User steps override non-core driver steps.
|
||||
user_steps = [step.copy() for step in self.deploy_steps[:2]]
|
||||
user_steps[0].update({'priority': 200})
|
||||
user_steps[1].update({'priority': 100})
|
||||
driver_steps = self.deploy_steps
|
||||
expected_steps = user_steps + self.deploy_steps[2:]
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
def test__get_all_deployment_steps_duplicate_user_steps(self):
|
||||
# Duplicate user steps override non-core driver steps.
|
||||
|
||||
# NOTE(mgoddard): This case is currently prevented by the API and
|
||||
# conductor - the interface/step must be unique across all enabled
|
||||
# steps. This test ensures that we can support this case, in case we
|
||||
# choose to allow it in future.
|
||||
user_steps = [self.deploy_start.copy(), self.deploy_start.copy()]
|
||||
user_steps[0].update({'priority': 200})
|
||||
user_steps[1].update({'priority': 100})
|
||||
driver_steps = self.deploy_steps
|
||||
# Each user invocation of the deploy_start step should be included, but
|
||||
# not the default deploy_start from the driver.
|
||||
expected_steps = user_steps + self.deploy_steps[1:]
|
||||
self._test__get_all_deployment_steps(user_steps, driver_steps,
|
||||
expected_steps)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_validated_steps_from_templates',
|
||||
autospec=True)
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
|
||||
def test__get_all_deployment_steps_error(self, mock_steps, mock_validated):
|
||||
mock_validated.side_effect = exception.InvalidParameterValue('foo')
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
conductor_utils._get_all_deployment_steps, task)
|
||||
mock_validated.assert_called_once_with(task)
|
||||
self.assertFalse(mock_steps.called)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_all_deployment_steps',
|
||||
autospec=True)
|
||||
def test_set_node_deployment_steps(self, mock_steps):
|
||||
mock_steps.return_value = self.deploy_steps
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
conductor_utils.set_node_deployment_steps(task)
|
||||
self.node.refresh()
|
||||
self.assertEqual(self.deploy_steps,
|
||||
self.node.driver_internal_info['deploy_steps'])
|
||||
self.assertEqual({}, self.node.deploy_step)
|
||||
self.assertIsNone(
|
||||
self.node.driver_internal_info['deploy_step_index'])
|
||||
mock_steps.assert_called_once_with(task)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps(self, mock_steps):
|
||||
mock_steps.return_value = self.deploy_steps
|
||||
|
||||
user_steps = [{'step': 'deploy_start', 'interface': 'deploy',
|
||||
'priority': 100},
|
||||
{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = conductor_utils._validate_user_deploy_steps(task,
|
||||
user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
self.assertEqual(user_steps, result)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_no_steps(self, mock_steps):
|
||||
mock_steps.return_value = self.deploy_steps
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
conductor_utils._validate_user_deploy_steps(task, [])
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_get_steps_exception(self, mock_steps):
|
||||
mock_steps.side_effect = exception.InstanceDeployFailure('bad')
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaises(exception.InstanceDeployFailure,
|
||||
conductor_utils._validate_user_deploy_steps,
|
||||
task, [])
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_not_supported(self, mock_steps):
|
||||
mock_steps.return_value = self.deploy_steps
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200},
|
||||
{'step': 'bad_step', 'interface': 'deploy',
|
||||
'priority': 100}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"does not support.*bad_step",
|
||||
conductor_utils._validate_user_deploy_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_invalid_arg(self, mock_steps):
|
||||
mock_steps.return_value = self.deploy_steps
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'args': {'arg1': 'val1', 'arg2': 'val2'},
|
||||
'priority': 200}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"power_one.*unexpected.*arg1",
|
||||
conductor_utils._validate_user_deploy_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_missing_required_arg(self,
|
||||
mock_steps):
|
||||
mock_steps.return_value = [self.power_one, self.deploy_raid]
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200},
|
||||
{'step': 'build_raid', 'interface': 'deploy',
|
||||
'priority': 100}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"build_raid.*missing.*arg1",
|
||||
conductor_utils._validate_user_deploy_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_disable_non_core(self, mock_steps):
|
||||
# Required arguments don't apply to disabled steps.
|
||||
mock_steps.return_value = [self.power_one, self.deploy_raid]
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200},
|
||||
{'step': 'build_raid', 'interface': 'deploy',
|
||||
'priority': 0}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = conductor_utils._validate_user_deploy_steps(task,
|
||||
user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
self.assertEqual(user_steps, result)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_disable_core(self, mock_steps):
|
||||
mock_steps.return_value = [self.power_one, self.deploy_core]
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200},
|
||||
{'step': 'deploy', 'interface': 'deploy', 'priority': 0}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = conductor_utils._validate_user_deploy_steps(task,
|
||||
user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
self.assertEqual(user_steps, result)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_override_core(self, mock_steps):
|
||||
mock_steps.return_value = [self.power_one, self.deploy_core]
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200},
|
||||
{'step': 'deploy', 'interface': 'deploy',
|
||||
'priority': 200}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"deploy.*is a core step",
|
||||
conductor_utils._validate_user_deploy_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_steps', autospec=True)
|
||||
def test__validate_user_deploy_steps_duplicates(self, mock_steps):
|
||||
mock_steps.return_value = [self.power_one, self.deploy_core]
|
||||
user_steps = [{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 200},
|
||||
{'step': 'power_one', 'interface': 'power',
|
||||
'priority': 100}]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"Duplicate deploy steps for "
|
||||
"power.power_one",
|
||||
conductor_utils._validate_user_deploy_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
|
||||
class NodeCleaningStepsTestCase(db_base.DbTestCase):
|
||||
def setUp(self):
|
||||
super(NodeCleaningStepsTestCase, self).setUp()
|
||||
|
||||
self.power_update = {
|
||||
'step': 'update_firmware', 'priority': 10, 'interface': 'power'}
|
||||
self.deploy_update = {
|
||||
'step': 'update_firmware', 'priority': 10, 'interface': 'deploy'}
|
||||
self.deploy_erase = {
|
||||
'step': 'erase_disks', 'priority': 20, 'interface': 'deploy',
|
||||
'abortable': True}
|
||||
# Automated cleaning should be executed in this order
|
||||
self.clean_steps = [self.deploy_erase, self.power_update,
|
||||
self.deploy_update]
|
||||
# Manual clean step
|
||||
self.deploy_raid = {
|
||||
'step': 'build_raid', 'priority': 0, 'interface': 'deploy',
|
||||
'argsinfo': {'arg1': {'description': 'desc1', 'required': True},
|
||||
'arg2': {'description': 'desc2'}}}
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',
|
||||
lambda self, task: [])
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_clean_steps')
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.get_clean_steps')
|
||||
def test__get_cleaning_steps(self, mock_power_steps, mock_deploy_steps):
|
||||
# Test getting cleaning steps, with one driver returning None, two
|
||||
# conflicting priorities, and asserting they are ordered properly.
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.CLEANING,
|
||||
target_provision_state=states.AVAILABLE)
|
||||
|
||||
mock_power_steps.return_value = [self.power_update]
|
||||
mock_deploy_steps.return_value = [self.deploy_erase,
|
||||
self.deploy_update]
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, node.uuid, shared=False) as task:
|
||||
steps = conductor_utils._get_cleaning_steps(task, enabled=False)
|
||||
|
||||
self.assertEqual(self.clean_steps, steps)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',
|
||||
lambda self, task: [])
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_clean_steps')
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.get_clean_steps')
|
||||
def test__get_cleaning_steps_unsorted(self, mock_power_steps,
|
||||
mock_deploy_steps):
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.CLEANING,
|
||||
target_provision_state=states.MANAGEABLE)
|
||||
|
||||
mock_deploy_steps.return_value = [self.deploy_raid,
|
||||
self.deploy_update,
|
||||
self.deploy_erase]
|
||||
with task_manager.acquire(
|
||||
self.context, node.uuid, shared=False) as task:
|
||||
steps = conductor_utils._get_cleaning_steps(task, enabled=False,
|
||||
sort=False)
|
||||
self.assertEqual(mock_deploy_steps.return_value, steps)
|
||||
|
||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_clean_steps')
|
||||
@mock.patch('ironic.drivers.modules.fake.FakePower.get_clean_steps')
|
||||
def test__get_cleaning_steps_only_enabled(self, mock_power_steps,
|
||||
mock_deploy_steps):
|
||||
# Test getting only cleaning steps, with one driver returning None, two
|
||||
# conflicting priorities, and asserting they are ordered properly.
|
||||
# Should discard zero-priority (manual) clean step
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.CLEANING,
|
||||
target_provision_state=states.AVAILABLE)
|
||||
|
||||
mock_power_steps.return_value = [self.power_update]
|
||||
mock_deploy_steps.return_value = [self.deploy_erase,
|
||||
self.deploy_update,
|
||||
self.deploy_raid]
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, node.uuid, shared=True) as task:
|
||||
steps = conductor_utils._get_cleaning_steps(task, enabled=True)
|
||||
|
||||
self.assertEqual(self.clean_steps, steps)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_validate_user_clean_steps')
|
||||
@mock.patch.object(conductor_utils, '_get_cleaning_steps')
|
||||
def test_set_node_cleaning_steps_automated(self, mock_steps,
|
||||
mock_validate_user_steps):
|
||||
mock_steps.return_value = self.clean_steps
|
||||
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.CLEANING,
|
||||
target_provision_state=states.AVAILABLE,
|
||||
last_error=None,
|
||||
clean_step=None)
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, node.uuid, shared=False) as task:
|
||||
conductor_utils.set_node_cleaning_steps(task)
|
||||
node.refresh()
|
||||
self.assertEqual(self.clean_steps,
|
||||
node.driver_internal_info['clean_steps'])
|
||||
self.assertEqual({}, node.clean_step)
|
||||
mock_steps.assert_called_once_with(task, enabled=True)
|
||||
self.assertFalse(mock_validate_user_steps.called)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_validate_user_clean_steps')
|
||||
@mock.patch.object(conductor_utils, '_get_cleaning_steps')
|
||||
def test_set_node_cleaning_steps_manual(self, mock_steps,
|
||||
mock_validate_user_steps):
|
||||
clean_steps = [self.deploy_raid]
|
||||
mock_steps.return_value = self.clean_steps
|
||||
mock_validate_user_steps.return_value = clean_steps
|
||||
|
||||
node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-hardware',
|
||||
provision_state=states.CLEANING,
|
||||
target_provision_state=states.MANAGEABLE,
|
||||
last_error=None,
|
||||
clean_step=None,
|
||||
driver_internal_info={'clean_steps': clean_steps})
|
||||
|
||||
with task_manager.acquire(
|
||||
self.context, node.uuid, shared=False) as task:
|
||||
conductor_utils.set_node_cleaning_steps(task)
|
||||
node.refresh()
|
||||
self.assertEqual(clean_steps,
|
||||
node.driver_internal_info['clean_steps'])
|
||||
self.assertEqual({}, node.clean_step)
|
||||
self.assertFalse(mock_steps.called)
|
||||
mock_validate_user_steps.assert_called_once_with(task, clean_steps)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_cleaning_steps')
|
||||
def test__validate_user_clean_steps(self, mock_steps):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_steps.return_value = self.clean_steps
|
||||
|
||||
user_steps = [{'step': 'update_firmware', 'interface': 'power'},
|
||||
{'step': 'erase_disks', 'interface': 'deploy'}]
|
||||
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
result = conductor_utils._validate_user_clean_steps(task,
|
||||
user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
expected = [{'step': 'update_firmware', 'interface': 'power',
|
||||
'priority': 10, 'abortable': False},
|
||||
{'step': 'erase_disks', 'interface': 'deploy',
|
||||
'priority': 20, 'abortable': True}]
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_cleaning_steps')
|
||||
def test__validate_user_clean_steps_no_steps(self, mock_steps):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_steps.return_value = self.clean_steps
|
||||
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
conductor_utils._validate_user_clean_steps(task, [])
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_cleaning_steps')
|
||||
def test__validate_user_clean_steps_get_steps_exception(self, mock_steps):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_steps.side_effect = exception.NodeCleaningFailure('bad')
|
||||
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
self.assertRaises(exception.NodeCleaningFailure,
|
||||
conductor_utils._validate_user_clean_steps,
|
||||
task, [])
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_cleaning_steps')
|
||||
def test__validate_user_clean_steps_not_supported(self, mock_steps):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_steps.return_value = [self.power_update, self.deploy_raid]
|
||||
user_steps = [{'step': 'update_firmware', 'interface': 'power'},
|
||||
{'step': 'bad_step', 'interface': 'deploy'}]
|
||||
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"does not support.*bad_step",
|
||||
conductor_utils._validate_user_clean_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_cleaning_steps')
|
||||
def test__validate_user_clean_steps_invalid_arg(self, mock_steps):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_steps.return_value = self.clean_steps
|
||||
user_steps = [{'step': 'update_firmware', 'interface': 'power',
|
||||
'args': {'arg1': 'val1', 'arg2': 'val2'}},
|
||||
{'step': 'erase_disks', 'interface': 'deploy'}]
|
||||
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"update_firmware.*unexpected.*arg1",
|
||||
conductor_utils._validate_user_clean_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_cleaning_steps')
|
||||
def test__validate_user_clean_steps_missing_required_arg(self, mock_steps):
|
||||
node = obj_utils.create_test_node(self.context)
|
||||
mock_steps.return_value = [self.power_update, self.deploy_raid]
|
||||
user_steps = [{'step': 'update_firmware', 'interface': 'power'},
|
||||
{'step': 'build_raid', 'interface': 'deploy'}]
|
||||
|
||||
with task_manager.acquire(self.context, node.uuid) as task:
|
||||
self.assertRaisesRegex(exception.InvalidParameterValue,
|
||||
"build_raid.*missing.*arg1",
|
||||
conductor_utils._validate_user_clean_steps,
|
||||
task, user_steps)
|
||||
mock_steps.assert_called_once_with(task, enabled=False, sort=False)
|
||||
|
||||
|
||||
class ErrorHandlersTestCase(tests_base.TestCase):
|
||||
def setUp(self):
|
||||
super(ErrorHandlersTestCase, self).setUp()
|
||||
@ -2467,79 +1842,6 @@ class ValidateInstanceInfoTraitsTestCase(tests_base.TestCase):
|
||||
self.node)
|
||||
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_deployment_templates',
|
||||
autospec=True)
|
||||
@mock.patch.object(conductor_utils, '_get_steps_from_deployment_templates',
|
||||
autospec=True)
|
||||
@mock.patch.object(conductor_utils, '_validate_user_deploy_steps',
|
||||
autospec=True)
|
||||
class GetValidatedStepsFromTemplatesTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(GetValidatedStepsFromTemplatesTestCase, self).setUp()
|
||||
self.node = obj_utils.create_test_node(self.context,
|
||||
driver='fake-hardware')
|
||||
self.template = obj_utils.get_test_deploy_template(self.context)
|
||||
|
||||
def test_ok(self, mock_validate, mock_steps, mock_templates):
|
||||
mock_templates.return_value = [self.template]
|
||||
steps = [db_utils.get_test_deploy_template_step()]
|
||||
mock_steps.return_value = steps
|
||||
mock_validate.return_value = steps
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
result = conductor_utils._get_validated_steps_from_templates(task)
|
||||
self.assertEqual(steps, result)
|
||||
mock_templates.assert_called_once_with(task)
|
||||
mock_steps.assert_called_once_with(task, [self.template])
|
||||
mock_validate.assert_called_once_with(task, steps, mock.ANY)
|
||||
|
||||
def test_invalid_parameter_value(self, mock_validate, mock_steps,
|
||||
mock_templates):
|
||||
mock_templates.return_value = [self.template]
|
||||
mock_validate.side_effect = exception.InvalidParameterValue('fake')
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
conductor_utils._get_validated_steps_from_templates, task)
|
||||
|
||||
def test_instance_deploy_failure(self, mock_validate, mock_steps,
|
||||
mock_templates):
|
||||
mock_templates.return_value = [self.template]
|
||||
mock_validate.side_effect = exception.InstanceDeployFailure('foo')
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
self.assertRaises(
|
||||
exception.InstanceDeployFailure,
|
||||
conductor_utils._get_validated_steps_from_templates, task)
|
||||
|
||||
|
||||
@mock.patch.object(conductor_utils, '_get_validated_steps_from_templates',
|
||||
autospec=True)
|
||||
class ValidateDeployTemplatesTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ValidateDeployTemplatesTestCase, self).setUp()
|
||||
self.node = obj_utils.create_test_node(self.context,
|
||||
driver='fake-hardware')
|
||||
|
||||
def test_ok(self, mock_validated):
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
result = conductor_utils.validate_deploy_templates(task)
|
||||
self.assertIsNone(result)
|
||||
mock_validated.assert_called_once_with(task)
|
||||
|
||||
def test_error(self, mock_validated):
|
||||
with task_manager.acquire(
|
||||
self.context, self.node.uuid, shared=False) as task:
|
||||
mock_validated.side_effect = exception.InvalidParameterValue('foo')
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
conductor_utils.validate_deploy_templates, task)
|
||||
mock_validated.assert_called_once_with(task)
|
||||
|
||||
|
||||
@mock.patch.object(fake.FakePower, 'get_power_state', autospec=True)
|
||||
class FastTrackTestCase(db_base.DbTestCase):
|
||||
|
||||
|
@ -20,6 +20,7 @@ import six
|
||||
from ironic.common import exception
|
||||
from ironic.common import states
|
||||
from ironic.common import utils as com_utils
|
||||
from ironic.conductor import steps
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.conductor import utils
|
||||
from ironic.drivers.modules.ansible import deploy as ansible_deploy
|
||||
@ -734,7 +735,7 @@ class TestAnsibleDeploy(AnsibleDeployTestCaseBase):
|
||||
self.assertFalse(log_mock.info.called)
|
||||
|
||||
@mock.patch.object(ansible_deploy, '_run_playbook', autospec=True)
|
||||
@mock.patch.object(utils, 'set_node_cleaning_steps', autospec=True)
|
||||
@mock.patch.object(steps, 'set_node_cleaning_steps', autospec=True)
|
||||
@mock.patch.object(utils, 'node_power_action', autospec=True)
|
||||
@mock.patch('ironic.drivers.modules.deploy_utils.build_agent_options',
|
||||
return_value={'op1': 'test1'}, autospec=True)
|
||||
@ -764,7 +765,7 @@ class TestAnsibleDeploy(AnsibleDeployTestCaseBase):
|
||||
self.assertFalse(run_playbook_mock.called)
|
||||
self.assertEqual(states.CLEANWAIT, state)
|
||||
|
||||
@mock.patch.object(utils, 'set_node_cleaning_steps', autospec=True)
|
||||
@mock.patch.object(steps, 'set_node_cleaning_steps', autospec=True)
|
||||
def test_prepare_cleaning_callback_no_steps(self,
|
||||
set_node_cleaning_steps):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
@ -1047,7 +1048,7 @@ class TestAnsibleDeploy(AnsibleDeployTestCaseBase):
|
||||
@mock.patch.object(utils, 'restore_power_state_if_needed', autospec=True)
|
||||
@mock.patch.object(utils, 'power_on_node_if_needed', autospec=True)
|
||||
@mock.patch.object(ansible_deploy, '_run_playbook', autospec=True)
|
||||
@mock.patch.object(utils, 'set_node_cleaning_steps', autospec=True)
|
||||
@mock.patch.object(steps, 'set_node_cleaning_steps', autospec=True)
|
||||
@mock.patch.object(utils, 'node_power_action', autospec=True)
|
||||
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
|
||||
@mock.patch.object(pxe.PXEBoot, 'prepare_ramdisk')
|
||||
|
@ -22,6 +22,7 @@ from oslo_config import cfg
|
||||
from ironic.common import boot_devices
|
||||
from ironic.common import exception
|
||||
from ironic.common import states
|
||||
from ironic.conductor import steps as conductor_steps
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.conductor import utils as manager_utils
|
||||
from ironic.drivers import base as drivers_base
|
||||
@ -213,7 +214,8 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
|
||||
@mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True)
|
||||
@mock.patch.object(agent_base_vendor.HeartbeatMixin,
|
||||
'refresh_clean_steps', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'set_node_cleaning_steps', autospec=True)
|
||||
@mock.patch.object(conductor_steps, 'set_node_cleaning_steps',
|
||||
autospec=True)
|
||||
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean',
|
||||
autospec=True)
|
||||
def test_heartbeat_resume_clean(self, mock_notify, mock_set_steps,
|
||||
@ -234,7 +236,8 @@ class HeartbeatMixinTest(AgentDeployMixinBaseTest):
|
||||
@mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True)
|
||||
@mock.patch.object(agent_base_vendor.HeartbeatMixin,
|
||||
'refresh_clean_steps', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'set_node_cleaning_steps', autospec=True)
|
||||
@mock.patch.object(conductor_steps, 'set_node_cleaning_steps',
|
||||
autospec=True)
|
||||
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean',
|
||||
autospec=True)
|
||||
def test_heartbeat_resume_clean_fails(self, mock_notify, mock_set_steps,
|
||||
@ -1351,7 +1354,8 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
|
||||
self.deploy.continue_cleaning(task)
|
||||
error_mock.assert_called_once_with(task, mock.ANY)
|
||||
|
||||
@mock.patch.object(manager_utils, 'set_node_cleaning_steps', autospec=True)
|
||||
@mock.patch.object(conductor_steps, 'set_node_cleaning_steps',
|
||||
autospec=True)
|
||||
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean',
|
||||
autospec=True)
|
||||
@mock.patch.object(agent_base_vendor.AgentDeployMixin,
|
||||
@ -1390,7 +1394,8 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
|
||||
self._test_continue_cleaning_clean_version_mismatch(manual=True)
|
||||
|
||||
@mock.patch.object(manager_utils, 'cleaning_error_handler', autospec=True)
|
||||
@mock.patch.object(manager_utils, 'set_node_cleaning_steps', autospec=True)
|
||||
@mock.patch.object(conductor_steps, 'set_node_cleaning_steps',
|
||||
autospec=True)
|
||||
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean',
|
||||
autospec=True)
|
||||
@mock.patch.object(agent_base_vendor.AgentDeployMixin,
|
||||
|
Loading…
Reference in New Issue
Block a user