
Adds service steps on a variety of internal interfaces, and begins to tie documentation together to provide clarity on the use and purpose of service steps. Change-Id: Ifd7241f06648c8d73c1b97fcf08673496f049f45
959 lines
38 KiB
Python
959 lines
38 KiB
Python
# 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
|
|
import time
|
|
|
|
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.conductor import utils
|
|
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.
|
|
'vendor': 6,
|
|
'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'.
|
|
'vendor': 6,
|
|
'power': 5,
|
|
'management': 4,
|
|
'deploy': 3,
|
|
'bios': 2,
|
|
'raid': 1,
|
|
}
|
|
|
|
SERVICING_INTERFACE_PRIORITY = DEPLOYING_INTERFACE_PRIORITY.copy()
|
|
|
|
VERIFYING_INTERFACE_PRIORITY = {
|
|
# When two verify steps have the same priority, their order is determined
|
|
# by which interface is implementing the verify step. The verifying step of
|
|
# the interface with the highest value here, will be executed first in
|
|
# that case.
|
|
'power': 12,
|
|
'management': 11,
|
|
'boot': 8,
|
|
'inspect': 10,
|
|
'deploy': 9,
|
|
'bios': 7,
|
|
'raid': 6,
|
|
'vendor': 5,
|
|
'network': 4,
|
|
'storage': 3,
|
|
'console': 2,
|
|
'rescue': 1,
|
|
}
|
|
|
|
# Reserved step names to to map to methods which need to be
|
|
# called, where node conductor logic wraps a driver's internal
|
|
# logic. Example, removing tokens based upon state before
|
|
# rebooting the node.
|
|
RESERVED_STEP_HANDLER_MAPPING = {
|
|
'power_on': [utils.node_power_action, states.POWER_ON],
|
|
'power_off': [utils.node_power_action, states.POWER_OFF],
|
|
'reboot': [utils.node_power_action, states.REBOOT],
|
|
}
|
|
|
|
# values to enable declariation of how to handle reserved step names
|
|
USED_HANDLER = 'used_handler'
|
|
EXIT_STEPS = 'exit_steps'
|
|
|
|
|
|
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 _verify_step_key(step):
|
|
"""Sort by priority, then interface priority in event of tie.
|
|
|
|
:param step: verify step dict to get priority for.
|
|
"""
|
|
return (step.get('priority'),
|
|
VERIFYING_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 _service_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'),
|
|
SERVICING_INTERFACE_PRIORITY[step.get('interface')])
|
|
|
|
|
|
def is_equivalent(step1, step2):
|
|
"""Compare steps, ignoring their priority."""
|
|
return (step1.get('interface') == step2.get('interface')
|
|
and step1.get('step') == step2.get('step'))
|
|
|
|
|
|
def find_step(steps, step):
|
|
"""Find an identical step in the list of steps."""
|
|
return next((x for x in steps if is_equivalent(x, step)), None)
|
|
|
|
|
|
def _get_steps(task, interfaces, get_method, enabled=False,
|
|
sort_step_key=None, prio_overrides=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.
|
|
:param prio_overrides: An optional dictionary of priority overrides for
|
|
steps, e.g:
|
|
{'deploy.erase_devices_metadata': '123',
|
|
'management.reset_bios_to_default': '234'}
|
|
: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:
|
|
# NOTE(janders) get all steps to start with, regardless of whether
|
|
# enabled is True and priority is zero or not; we need to apply
|
|
# priority overrides prior to filtering out disabled steps
|
|
interface_steps = [x for x in getattr(interface, get_method)(task)]
|
|
steps.extend(interface_steps)
|
|
# Iterate over steps to apply prio overrides if set
|
|
if prio_overrides is not None:
|
|
for step in steps:
|
|
override_key = '%(interface)s.%(step)s' % step
|
|
override_value = prio_overrides.get(override_key)
|
|
if override_value:
|
|
step["priority"] = int(override_value)
|
|
# NOTE(janders) If enabled is set to True, we filter out steps with zero
|
|
# priority now, after applying priority overrides
|
|
if enabled:
|
|
steps = [x for x in steps if not (x.get('priority') == 0)]
|
|
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
|
|
if CONF.conductor.clean_step_priority_override:
|
|
csp_override = {}
|
|
for element in CONF.conductor.clean_step_priority_override:
|
|
csp_override.update(element)
|
|
|
|
cleaning_steps = _get_steps(task, CLEANING_INTERFACE_PRIORITY,
|
|
'get_clean_steps', enabled=enabled,
|
|
sort_step_key=sort_key,
|
|
prio_overrides=csp_override)
|
|
|
|
LOG.debug('cleaning_steps after applying '
|
|
'clean_step_priority_override for node %(node)s: %(steps)s',
|
|
{'node': task.node.uuid, 'steps': cleaning_steps})
|
|
else:
|
|
cleaning_steps = _get_steps(task, CLEANING_INTERFACE_PRIORITY,
|
|
'get_clean_steps', enabled=enabled,
|
|
sort_step_key=sort_key)
|
|
return cleaning_steps
|
|
|
|
|
|
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 _get_service_steps(task, enabled=False, sort=True):
|
|
"""Get service 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: NodeServicingFailure if there was a problem getting the
|
|
clean steps.
|
|
:returns: A list of clean step dictionaries
|
|
"""
|
|
sort_key = _service_step_key if sort else None
|
|
service_steps = _get_steps(task, SERVICING_INTERFACE_PRIORITY,
|
|
'get_service_steps', enabled=enabled,
|
|
sort_step_key=sort_key)
|
|
return service_steps
|
|
|
|
|
|
def _get_verify_steps(task, enabled=False, sort=True):
|
|
"""Get verify steps for task.node.
|
|
|
|
:param task: A TaskManager object
|
|
:param enabled: If True, returns only enabled (priority > 0) steps. If
|
|
False, returns all verify 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: NodeVerifyFailure if there was a problem getting the
|
|
verify steps.
|
|
:returns: A list of verify step dictionaries
|
|
"""
|
|
sort_key = _verify_step_key if sort else None
|
|
if CONF.conductor.verify_step_priority_override:
|
|
vsp_override = {}
|
|
for element in CONF.conductor.verify_step_priority_override:
|
|
vsp_override.update(element)
|
|
|
|
verify_steps = _get_steps(task, VERIFYING_INTERFACE_PRIORITY,
|
|
'get_verify_steps', enabled=enabled,
|
|
sort_step_key=sort_key,
|
|
prio_overrides=vsp_override)
|
|
|
|
else:
|
|
verify_steps = _get_steps(task, VERIFYING_INTERFACE_PRIORITY,
|
|
'get_verify_steps', enabled=enabled,
|
|
sort_step_key=sort_key)
|
|
return verify_steps
|
|
|
|
|
|
def set_node_cleaning_steps(task, disable_ramdisk=False):
|
|
"""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.
|
|
|
|
:param disable_ramdisk: If `True`, only steps with requires_ramdisk=False
|
|
are accepted.
|
|
: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
|
|
|
|
# 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
|
|
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 = _validate_user_clean_steps(
|
|
task, node.driver_internal_info['clean_steps'],
|
|
disable_ramdisk=disable_ramdisk)
|
|
|
|
LOG.debug('List of the steps for %(type)s cleaning of node %(node)s: '
|
|
'%(steps)s', {'type': 'manual' if manual_clean else 'automated',
|
|
'node': node.uuid,
|
|
'steps': steps})
|
|
|
|
node.clean_step = {}
|
|
node.set_driver_internal_info('clean_steps', steps)
|
|
node.set_driver_internal_info('clean_step_index', None)
|
|
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, skip_missing=False):
|
|
"""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,
|
|
skip_missing=skip_missing)
|
|
|
|
|
|
def _get_all_deployment_steps(task, skip_missing=False):
|
|
"""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
|
|
"""
|
|
# Get deploy steps provided by user via argument if any. These steps
|
|
# override template and driver steps when overlap.
|
|
user_steps = _get_validated_user_deploy_steps(
|
|
task, skip_missing=skip_missing)
|
|
|
|
# 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.
|
|
template_steps = _get_validated_steps_from_templates(
|
|
task, skip_missing=skip_missing)
|
|
|
|
# Take only template steps that are not already provided by user
|
|
user_step_keys = {(s['interface'], s['step']) for s in user_steps}
|
|
new_template_steps = [s for s in template_steps
|
|
if (s['interface'], s['step']) not in user_step_keys]
|
|
user_steps.extend(new_template_steps)
|
|
|
|
# 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, reset_current=True, skip_missing=False):
|
|
"""Set up the node with deployment step information for deploying.
|
|
|
|
Get the deploy steps from the driver.
|
|
|
|
:param reset_current: Whether to reset the current step to the first one.
|
|
:raises: InstanceDeployFailure if there was a problem getting the
|
|
deployment steps.
|
|
"""
|
|
node = task.node
|
|
node.set_driver_internal_info('deploy_steps', _get_all_deployment_steps(
|
|
task, skip_missing=skip_missing))
|
|
|
|
LOG.debug('List of the deploy steps for node %(node)s: %(steps)s', {
|
|
'node': node.uuid,
|
|
'steps': node.driver_internal_info['deploy_steps']
|
|
})
|
|
if reset_current:
|
|
node.deploy_step = {}
|
|
node.set_driver_internal_info('deploy_step_index', None)
|
|
node.save()
|
|
|
|
|
|
def set_node_service_steps(task, disable_ramdisk=False):
|
|
"""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.
|
|
|
|
:param disable_ramdisk: If `True`, only steps with requires_ramdisk=False
|
|
are accepted.
|
|
:raises: InvalidParameterValue if there is a problem with the user's
|
|
clean steps.
|
|
:raises: NodeCleaningFailure if there was a problem getting the
|
|
service steps.
|
|
"""
|
|
node = task.node
|
|
steps = _validate_user_service_steps(
|
|
task, node.driver_internal_info.get('service_steps', []),
|
|
disable_ramdisk=disable_ramdisk)
|
|
LOG.debug('List of the steps for service of node %(node)s: '
|
|
'%(steps)s', {'node': node.uuid,
|
|
'steps': steps})
|
|
|
|
node.service_step = {}
|
|
node.set_driver_internal_info('service_steps', steps)
|
|
node.set_driver_internal_info('service_step_index', None)
|
|
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,
|
|
disable_ramdisk=False):
|
|
"""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'.
|
|
:param disable_ramdisk: If `True`, only steps with requires_ramdisk=False
|
|
are accepted. Only makes sense for manual cleaning at the moment.
|
|
: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 disable_ramdisk and driver_step.get('requires_ramdisk', True):
|
|
error = _('clean step %s requires booting a ramdisk') % user_step
|
|
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, skip_missing=False,
|
|
disable_ramdisk=False):
|
|
"""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.
|
|
:param skip_missing: Whether to silently ignore unknown steps.
|
|
:param disable_ramdisk: If `True`, only steps with requires_ramdisk=False
|
|
are accepted. Only makes sense for manual cleaning at the moment.
|
|
: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}
|
|
|
|
result = []
|
|
|
|
for user_step in user_steps:
|
|
if user_step.get('execute_on_child_nodes'):
|
|
# NOTE(TheJulia): This input is validated on the API side
|
|
# as we have the original API request context to leverage
|
|
# for RBAC validation.
|
|
continue
|
|
if user_step.get('step') in ['power_on', 'power_off', 'reboot']:
|
|
# NOTE(TheJulia): These are flow related steps the conductor
|
|
# resolves internally.
|
|
continue
|
|
# Check if this user-specified step isn't supported by the driver
|
|
try:
|
|
driver_step = driver_steps[step_id(user_step)]
|
|
except KeyError:
|
|
if skip_missing:
|
|
LOG.debug('%(type)s step %(step)s is not currently known for '
|
|
'node %(node)s, delaying its validation until '
|
|
'in-band steps are loaded',
|
|
{'type': step_type.capitalize(),
|
|
'step': user_step, 'node': task.node.uuid})
|
|
else:
|
|
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, disable_ramdisk)
|
|
errors.extend(step_errors)
|
|
result.append(user_step)
|
|
|
|
if step_type == 'deploy':
|
|
# Deploy steps should be unique across all combined templates or passed
|
|
# deploy_steps argument.
|
|
dup_errors = _validate_deploy_steps_unique(result)
|
|
errors.extend(dup_errors)
|
|
|
|
if errors:
|
|
err = error_prefix or ''
|
|
err += '; '.join(errors)
|
|
raise exception.InvalidParameterValue(err=err)
|
|
return result
|
|
|
|
|
|
def _validate_user_clean_steps(task, user_steps, disable_ramdisk=False):
|
|
"""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} }
|
|
:param disable_ramdisk: If `True`, only steps with requires_ramdisk=False
|
|
are accepted.
|
|
: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',
|
|
disable_ramdisk=disable_ramdisk)
|
|
|
|
|
|
def _validate_user_deploy_steps(task, user_steps, error_prefix=None,
|
|
skip_missing=False):
|
|
"""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,
|
|
skip_missing=skip_missing)
|
|
|
|
|
|
def _validate_user_service_steps(task, user_steps, disable_ramdisk=False):
|
|
"""Validate the user-specified service 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} }
|
|
:param disable_ramdisk: If `True`, only steps with requires_ramdisk=False
|
|
are accepted.
|
|
:raises: InvalidParameterValue if validation of clean steps fails.
|
|
:raises: NodeCleaningFailure if there was a problem getting the
|
|
clean steps from the driver.
|
|
:return: validated service steps update with information from the driver
|
|
"""
|
|
# We call with enabled = False below so we pickup auto-disabled
|
|
# steps, since service steps are not automagic like cleaning can be.
|
|
driver_steps = _get_service_steps(task, enabled=False, sort=False)
|
|
return _validate_user_steps(task, user_steps, driver_steps, 'service',
|
|
disable_ramdisk=disable_ramdisk)
|
|
|
|
|
|
def _get_validated_user_deploy_steps(task, deploy_steps=None,
|
|
skip_missing=False):
|
|
"""Validate the deploy steps for a node.
|
|
|
|
:param task: A TaskManager object
|
|
:param deploy_steps: Deploy steps to validate. Optional. If not provided
|
|
then will check node's driver internal info.
|
|
:param skip_missing: whether skip missing steps that are not yet available
|
|
at the time of validation.
|
|
:raises: InvalidParameterValue if deploy steps are unsupported by the
|
|
node's driver interfaces.
|
|
:raises: InstanceDeployFailure if there was a problem getting the deploy
|
|
steps from the driver.
|
|
"""
|
|
if not deploy_steps:
|
|
deploy_steps = task.node.driver_internal_info.get('user_deploy_steps')
|
|
|
|
if deploy_steps:
|
|
error_prefix = (_('Validation of deploy steps from "deploy steps" '
|
|
'argument failed.'))
|
|
return _validate_user_deploy_steps(task, deploy_steps,
|
|
error_prefix=error_prefix,
|
|
skip_missing=skip_missing)
|
|
else:
|
|
return []
|
|
|
|
|
|
def validate_user_deploy_steps_and_templates(task, deploy_steps=None,
|
|
skip_missing=False):
|
|
"""Validate the user deploy steps and the deploy templates for a node.
|
|
|
|
:param task: A TaskManager object
|
|
:param deploy_steps: Deploy steps to validate. Optional. If not provided
|
|
then will check node's driver internal info.
|
|
:param skip_missing: whether skip missing steps that are not yet available
|
|
at the time of validation.
|
|
:raises: InvalidParameterValue if the instance has traits that map to
|
|
deploy steps that are unsupported by the node's driver interfaces or
|
|
user deploy steps 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, skip_missing=skip_missing)
|
|
# Validate steps from passed argument or stored on the node.
|
|
_get_validated_user_deploy_steps(task, deploy_steps, skip_missing)
|
|
|
|
|
|
def use_reserved_step_handler(task, step):
|
|
"""Returns guidance for reserved step execution or process is used.
|
|
|
|
This method is utilized to handle some specific cases with the execution
|
|
of steps. For example, reserved step names, or reserved names which
|
|
have specific meaning in the state machine.
|
|
|
|
:param task: a TaskManager object.
|
|
:param step: The requested step.
|
|
"""
|
|
step_name = step.get('step')
|
|
step_args = step.get('args', {})
|
|
if step_name and step_name in RESERVED_STEP_HANDLER_MAPPING.keys():
|
|
call_to_use = RESERVED_STEP_HANDLER_MAPPING[step_name]
|
|
method = call_to_use[0]
|
|
parameter = call_to_use[1]
|
|
method(task, parameter)
|
|
return USED_HANDLER
|
|
if step_name == 'hold':
|
|
task.process_event('hold')
|
|
return EXIT_STEPS
|
|
# If we've reached this point, we're going to return None as
|
|
# there is no work for us to do. This allows the caller to
|
|
# take its normal path.
|
|
if step_name == 'wait':
|
|
# By default, we enter a wait state.
|
|
task.process_event('wait')
|
|
if 'seconds' in step_args:
|
|
# If we have a seconds argument, just pause.
|
|
rec_seconds = int(step_args['seconds'])
|
|
if rec_seconds > CONF.conductor.max_conductor_wait_step_seconds:
|
|
warning = (
|
|
_('A wait time exceeding the configured maximum '
|
|
'has been requested. Holding for %s, got %s.') %
|
|
(rec_seconds,
|
|
CONF.conductor.max_conductor_wait_step_seconds)
|
|
)
|
|
utils.node_history_record(task.node, event=warning,
|
|
event_type=task.node.provision_state)
|
|
LOG.warning(warning)
|
|
rec_seconds = CONF.conductor.max_conductor_wait_step_seconds
|
|
_sleep_wrapper(rec_seconds)
|
|
# Explicitly resume.
|
|
task.process_event('resume')
|
|
# Return True, which closed out execution until the next heartbeat.
|
|
return EXIT_STEPS
|
|
|
|
|
|
def _sleep_wrapper(seconds):
|
|
"""Wrapper for sleep to allow for unit testing."""
|
|
time.sleep(seconds)
|