Merge "Add an ability to run in-band deploy steps"
This commit is contained in:
commit
b9e320e76f
@ -263,24 +263,26 @@ class IncompatibleHardwareMethodError(RESTError):
|
|||||||
super(IncompatibleHardwareMethodError, self).__init__(details)
|
super(IncompatibleHardwareMethodError, self).__init__(details)
|
||||||
|
|
||||||
|
|
||||||
class CleanVersionMismatch(RESTError):
|
class VersionMismatch(RESTError):
|
||||||
"""Error raised when Ironic and the Agent have different versions.
|
"""Error raised when Ironic and the Agent have different versions.
|
||||||
|
|
||||||
If the agent version has changed since get_clean_steps was called by
|
If the agent version has changed since get_clean_steps or get_deploy_steps
|
||||||
the Ironic conductor, it indicates the agent has been updated (either
|
was called by the Ironic conductor, it indicates the agent has been updated
|
||||||
on purpose, or a new agent was deployed and the node was rebooted).
|
(either on purpose, or a new agent was deployed and the node was rebooted).
|
||||||
Since we cannot know if the upgraded IPA will work with cleaning as it
|
Since we cannot know if the upgraded IPA will work with cleaning/deploy as
|
||||||
stands (steps could have different priorities, either in IPA or in
|
it stands (steps could have different priorities, either in IPA or in
|
||||||
other Ironic interfaces), we should restart cleaning from the start.
|
other Ironic interfaces), we should restart the process from the start.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
message = 'Clean version mismatch, reload agent with correct version'
|
message = (
|
||||||
|
'Hardware managers version mismatch, reload agent with correct version'
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, agent_version, node_version):
|
def __init__(self, agent_version, node_version):
|
||||||
self.status_code = 409
|
self.status_code = 409
|
||||||
details = ('Agent clean version: {}, node clean version: {}'
|
details = ('Current versions: {}, versions used by ironic: {}'
|
||||||
.format(agent_version, node_version))
|
.format(agent_version, node_version))
|
||||||
super(CleanVersionMismatch, self).__init__(details)
|
super(VersionMismatch, self).__init__(details)
|
||||||
|
|
||||||
|
|
||||||
class CleaningError(RESTError):
|
class CleaningError(RESTError):
|
||||||
@ -292,6 +294,15 @@ class CleaningError(RESTError):
|
|||||||
super(CleaningError, self).__init__(details)
|
super(CleaningError, self).__init__(details)
|
||||||
|
|
||||||
|
|
||||||
|
class DeploymentError(RESTError):
|
||||||
|
"""Error raised when a deploy step fails."""
|
||||||
|
|
||||||
|
message = 'Deploy step failed'
|
||||||
|
|
||||||
|
def __init__(self, details=None):
|
||||||
|
super(DeploymentError, self).__init__(details)
|
||||||
|
|
||||||
|
|
||||||
class ISCSIError(RESTError):
|
class ISCSIError(RESTError):
|
||||||
"""Error raised when an image cannot be written to a device."""
|
"""Error raised when an image cannot be written to a device."""
|
||||||
|
|
||||||
|
@ -33,7 +33,9 @@ class AgentCommandStatus(object):
|
|||||||
RUNNING = u'RUNNING'
|
RUNNING = u'RUNNING'
|
||||||
SUCCEEDED = u'SUCCEEDED'
|
SUCCEEDED = u'SUCCEEDED'
|
||||||
FAILED = u'FAILED'
|
FAILED = u'FAILED'
|
||||||
CLEAN_VERSION_MISMATCH = u'CLEAN_VERSION_MISMATCH'
|
# TODO(dtantsur): keeping the same text for backward compatibility, change
|
||||||
|
# to just VERSION_MISMATCH one release after ironic is updated.
|
||||||
|
VERSION_MISMATCH = u'CLEAN_VERSION_MISMATCH'
|
||||||
|
|
||||||
|
|
||||||
class BaseCommandResult(encoding.SerializableComparable):
|
class BaseCommandResult(encoding.SerializableComparable):
|
||||||
@ -167,10 +169,10 @@ class AsyncCommandResult(BaseCommandResult):
|
|||||||
with self.command_state_lock:
|
with self.command_state_lock:
|
||||||
self.command_result = result
|
self.command_result = result
|
||||||
self.command_status = AgentCommandStatus.SUCCEEDED
|
self.command_status = AgentCommandStatus.SUCCEEDED
|
||||||
except errors.CleanVersionMismatch as e:
|
except errors.VersionMismatch as e:
|
||||||
with self.command_state_lock:
|
with self.command_state_lock:
|
||||||
self.command_error = e
|
self.command_error = e
|
||||||
self.command_status = AgentCommandStatus.CLEAN_VERSION_MISMATCH
|
self.command_status = AgentCommandStatus.VERSION_MISMATCH
|
||||||
self.command_result = None
|
self.command_result = None
|
||||||
LOG.error('Clean version mismatch for command %s',
|
LOG.error('Clean version mismatch for command %s',
|
||||||
self.command_name)
|
self.command_name)
|
||||||
|
@ -12,8 +12,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import collections
|
|
||||||
|
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
from ironic_python_agent import errors
|
from ironic_python_agent import errors
|
||||||
@ -42,12 +40,12 @@ class CleanExtension(base.BaseAgentExtension):
|
|||||||
node, ports)
|
node, ports)
|
||||||
|
|
||||||
LOG.debug('Clean steps before deduplication: %s', candidate_steps)
|
LOG.debug('Clean steps before deduplication: %s', candidate_steps)
|
||||||
clean_steps = _deduplicate_steps(candidate_steps)
|
clean_steps = hardware.deduplicate_steps(candidate_steps)
|
||||||
LOG.debug('Returning clean steps: %s', clean_steps)
|
LOG.debug('Returning clean steps: %s', clean_steps)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'clean_steps': clean_steps,
|
'clean_steps': clean_steps,
|
||||||
'hardware_manager_version': _get_current_clean_version()
|
'hardware_manager_version': hardware.get_current_versions(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@base.async_command('execute_clean_step')
|
@base.async_command('execute_clean_step')
|
||||||
@ -59,7 +57,7 @@ class CleanExtension(base.BaseAgentExtension):
|
|||||||
:param node: A dict representation of a node
|
:param node: A dict representation of a node
|
||||||
:param ports: A dict representation of ports attached to node
|
:param ports: A dict representation of ports attached to node
|
||||||
:param clean_version: The clean version as returned by
|
:param clean_version: The clean version as returned by
|
||||||
_get_current_clean_version() at the beginning
|
hardware.get_current_versions() at the beginning
|
||||||
of cleaning/zapping
|
of cleaning/zapping
|
||||||
:returns: a CommandResult object with command_result set to whatever
|
:returns: a CommandResult object with command_result set to whatever
|
||||||
the step returns.
|
the step returns.
|
||||||
@ -67,7 +65,7 @@ class CleanExtension(base.BaseAgentExtension):
|
|||||||
# Ensure the agent is still the same version, or raise an exception
|
# Ensure the agent is still the same version, or raise an exception
|
||||||
LOG.debug('Executing clean step %s', step)
|
LOG.debug('Executing clean step %s', step)
|
||||||
hardware.cache_node(node)
|
hardware.cache_node(node)
|
||||||
_check_clean_version(clean_version)
|
hardware.check_versions(clean_version)
|
||||||
|
|
||||||
if 'step' not in step:
|
if 'step' not in step:
|
||||||
msg = 'Malformed clean_step, no "step" key: %s' % step
|
msg = 'Malformed clean_step, no "step" key: %s' % step
|
||||||
@ -95,104 +93,3 @@ class CleanExtension(base.BaseAgentExtension):
|
|||||||
'clean_result': result,
|
'clean_result': result,
|
||||||
'clean_step': step
|
'clean_step': step
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _deduplicate_steps(candidate_steps):
|
|
||||||
"""Remove duplicated clean steps
|
|
||||||
|
|
||||||
Deduplicates clean steps returned from HardwareManagers to prevent
|
|
||||||
running a given step more than once. Other than individual step
|
|
||||||
priority, it doesn't actually impact the cleaning run which specific
|
|
||||||
steps are kept and what HardwareManager they are associated with.
|
|
||||||
However, in order to make testing easier, this method returns
|
|
||||||
deterministic results.
|
|
||||||
|
|
||||||
Uses the following filtering logic to decide which step "wins":
|
|
||||||
- Keep the step that belongs to HardwareManager with highest
|
|
||||||
HardwareSupport (larger int) value.
|
|
||||||
- If equal support level, keep the step with the higher defined priority
|
|
||||||
(larger int).
|
|
||||||
- If equal support level and priority, keep the step associated with the
|
|
||||||
HardwareManager whose name comes earlier in the alphabet.
|
|
||||||
|
|
||||||
:param candidate_steps: A dict containing all possible clean steps from
|
|
||||||
all managers, key=manager, value=list of clean steps
|
|
||||||
:returns: A deduplicated dictionary of {hardware_manager:
|
|
||||||
[clean-steps]}
|
|
||||||
"""
|
|
||||||
support = hardware.dispatch_to_all_managers(
|
|
||||||
'evaluate_hardware_support')
|
|
||||||
|
|
||||||
steps = collections.defaultdict(list)
|
|
||||||
deduped_steps = collections.defaultdict(list)
|
|
||||||
|
|
||||||
for manager, manager_steps in candidate_steps.items():
|
|
||||||
# We cannot deduplicate steps with unknown hardware support
|
|
||||||
if manager not in support:
|
|
||||||
LOG.warning('Unknown hardware support for %(manager)s, '
|
|
||||||
'dropping clean steps: %(steps)s',
|
|
||||||
{'manager': manager, 'steps': manager_steps})
|
|
||||||
continue
|
|
||||||
|
|
||||||
for step in manager_steps:
|
|
||||||
# build a new dict of steps that's easier to filter
|
|
||||||
step['hwm'] = {'name': manager,
|
|
||||||
'support': support[manager]}
|
|
||||||
steps[step['step']].append(step)
|
|
||||||
|
|
||||||
for step_name, step_list in steps.items():
|
|
||||||
# determine the max support level among candidate steps
|
|
||||||
max_support = max([x['hwm']['support'] for x in step_list])
|
|
||||||
# filter out any steps that are not at the max support for this step
|
|
||||||
max_support_steps = [x for x in step_list
|
|
||||||
if x['hwm']['support'] == max_support]
|
|
||||||
|
|
||||||
# determine the max priority among remaining steps
|
|
||||||
max_priority = max([x['priority'] for x in max_support_steps])
|
|
||||||
# filter out any steps that are not at the max priority for this step
|
|
||||||
max_priority_steps = [x for x in max_support_steps
|
|
||||||
if x['priority'] == max_priority]
|
|
||||||
|
|
||||||
# if there are still multiple steps, sort by hwm name and take
|
|
||||||
# the first result
|
|
||||||
winning_step = sorted(max_priority_steps,
|
|
||||||
key=lambda x: x['hwm']['name'])[0]
|
|
||||||
# Remove extra metadata we added to the step for filtering
|
|
||||||
manager = winning_step.pop('hwm')['name']
|
|
||||||
# Add winning step to deduped_steps
|
|
||||||
deduped_steps[manager].append(winning_step)
|
|
||||||
|
|
||||||
return deduped_steps
|
|
||||||
|
|
||||||
|
|
||||||
def _check_clean_version(clean_version=None):
|
|
||||||
"""Ensure the clean version hasn't changed.
|
|
||||||
|
|
||||||
:param clean_version: Hardware manager versions used during this
|
|
||||||
cleaning cycle.
|
|
||||||
:raises: errors.CleanVersionMismatch if any hardware manager version on
|
|
||||||
the currently running agent doesn't match the one stored in
|
|
||||||
clean_version.
|
|
||||||
:returns: None
|
|
||||||
"""
|
|
||||||
# If the version is None, assume this is the first run
|
|
||||||
if clean_version is None:
|
|
||||||
return
|
|
||||||
agent_version = _get_current_clean_version()
|
|
||||||
if clean_version != agent_version:
|
|
||||||
LOG.warning('Mismatched clean versions. Agent version: %(agent)s, '
|
|
||||||
'node version: %(node)s', {'agent': agent_version,
|
|
||||||
'node': clean_version})
|
|
||||||
raise errors.CleanVersionMismatch(agent_version=agent_version,
|
|
||||||
node_version=clean_version)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_current_clean_version():
|
|
||||||
"""Fetches versions from all hardware managers.
|
|
||||||
|
|
||||||
:returns: Dict in the format {name: version} containing one entry for
|
|
||||||
every hardware manager.
|
|
||||||
"""
|
|
||||||
return {version.get('name'): version.get('version')
|
|
||||||
for version in hardware.dispatch_to_all_managers(
|
|
||||||
'get_version').values()}
|
|
||||||
|
95
ironic_python_agent/extensions/deploy.py
Normal file
95
ironic_python_agent/extensions/deploy.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from ironic_python_agent import errors
|
||||||
|
from ironic_python_agent.extensions import base
|
||||||
|
from ironic_python_agent import hardware
|
||||||
|
|
||||||
|
LOG = log.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
class DeployExtension(base.BaseAgentExtension):
|
||||||
|
@base.sync_command('get_deploy_steps')
|
||||||
|
def get_deploy_steps(self, node, ports):
|
||||||
|
"""Get the list of deploy steps supported for the node and ports
|
||||||
|
|
||||||
|
:param node: A dict representation of a node
|
||||||
|
:param ports: A dict representation of ports attached to node
|
||||||
|
|
||||||
|
:returns: A list of deploy steps with keys step, priority, and
|
||||||
|
reboot_requested
|
||||||
|
"""
|
||||||
|
LOG.debug('Getting deploy steps, called with node: %(node)s, '
|
||||||
|
'ports: %(ports)s', {'node': node, 'ports': ports})
|
||||||
|
hardware.cache_node(node)
|
||||||
|
# Results should be a dict, not a list
|
||||||
|
candidate_steps = hardware.dispatch_to_all_managers('get_deploy_steps',
|
||||||
|
node, ports)
|
||||||
|
|
||||||
|
LOG.debug('Deploy steps before deduplication: %s', candidate_steps)
|
||||||
|
deploy_steps = hardware.deduplicate_steps(candidate_steps)
|
||||||
|
LOG.debug('Returning deploy steps: %s', deploy_steps)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'deploy_steps': deploy_steps,
|
||||||
|
'hardware_manager_version': hardware.get_current_versions(),
|
||||||
|
}
|
||||||
|
|
||||||
|
@base.async_command('execute_deploy_step')
|
||||||
|
def execute_deploy_step(self, step, node, ports, deploy_version=None,
|
||||||
|
**kwargs):
|
||||||
|
"""Execute a deploy step.
|
||||||
|
|
||||||
|
:param step: A deploy step with 'step', 'priority' and 'interface' keys
|
||||||
|
:param node: A dict representation of a node
|
||||||
|
:param ports: A dict representation of ports attached to node
|
||||||
|
:param deploy_version: The deploy version as returned by
|
||||||
|
hardware.get_current_versions() at the beginning
|
||||||
|
of deploying.
|
||||||
|
:param kwargs: The remaining arguments are passed to the step.
|
||||||
|
:returns: a CommandResult object with command_result set to whatever
|
||||||
|
the step returns.
|
||||||
|
"""
|
||||||
|
# Ensure the agent is still the same version, or raise an exception
|
||||||
|
LOG.debug('Executing deploy step %s', step)
|
||||||
|
hardware.cache_node(node)
|
||||||
|
hardware.check_versions(deploy_version)
|
||||||
|
|
||||||
|
if 'step' not in step:
|
||||||
|
msg = 'Malformed deploy_step, no "step" key: %s' % step
|
||||||
|
LOG.error(msg)
|
||||||
|
raise ValueError(msg)
|
||||||
|
try:
|
||||||
|
result = hardware.dispatch_to_managers(step['step'], node, ports,
|
||||||
|
**kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
msg = ('Error performing deploy_step %(step)s: %(err)s' %
|
||||||
|
{'step': step['step'], 'err': e})
|
||||||
|
LOG.exception(msg)
|
||||||
|
raise errors.DeploymentError(msg)
|
||||||
|
|
||||||
|
LOG.info('Deploy step completed: %(step)s, result: %(result)s',
|
||||||
|
{'step': step, 'result': result})
|
||||||
|
|
||||||
|
# Cast result tuples (like output of utils.execute) as lists, or
|
||||||
|
# API throws errors
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
result = list(result)
|
||||||
|
|
||||||
|
# Return the step that was executed so we can dispatch
|
||||||
|
# to the appropriate Ironic interface
|
||||||
|
return {
|
||||||
|
'deploy_result': result,
|
||||||
|
'deploy_step': step
|
||||||
|
}
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import abc
|
import abc
|
||||||
import binascii
|
import binascii
|
||||||
|
import collections
|
||||||
import functools
|
import functools
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
@ -720,6 +721,53 @@ class HardwareManager(object, metaclass=abc.ABCMeta):
|
|||||||
"""
|
"""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def get_deploy_steps(self, node, ports):
|
||||||
|
"""Get a list of deploy steps with priority.
|
||||||
|
|
||||||
|
Returns a list of steps. Each step is represented by a dict::
|
||||||
|
|
||||||
|
{
|
||||||
|
'interface': the name of the driver interface that should execute
|
||||||
|
the step.
|
||||||
|
'step': the HardwareManager function to call.
|
||||||
|
'priority': the order steps will be run in. Ironic will sort all
|
||||||
|
the deploy steps from all the drivers, with the largest
|
||||||
|
priority step being run first. If priority is set to 0,
|
||||||
|
the step will not be run during deployment, but may be
|
||||||
|
run during zapping.
|
||||||
|
'reboot_requested': Whether the agent should request Ironic reboots
|
||||||
|
the node via the power driver after the
|
||||||
|
operation completes.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
If multiple hardware managers return the same step name, the following
|
||||||
|
logic will be used to determine which manager's step "wins":
|
||||||
|
|
||||||
|
* Keep the step that belongs to HardwareManager with highest
|
||||||
|
HardwareSupport (larger int) value.
|
||||||
|
* If equal support level, keep the step with the higher defined
|
||||||
|
priority (larger int).
|
||||||
|
* If equal support level and priority, keep the step associated
|
||||||
|
with the HardwareManager whose name comes earlier in the
|
||||||
|
alphabet.
|
||||||
|
|
||||||
|
The steps will be called using `hardware.dispatch_to_managers` and
|
||||||
|
handled by the best suited hardware manager. If you need a step to be
|
||||||
|
executed by only your hardware manager, ensure it has a unique step
|
||||||
|
name.
|
||||||
|
|
||||||
|
`node` and `ports` can be used by other hardware managers to further
|
||||||
|
determine if a deploy step is supported for the node.
|
||||||
|
|
||||||
|
:param node: Ironic node object
|
||||||
|
:param ports: list of Ironic port objects
|
||||||
|
:return: a list of deploying steps, where each step is described as a
|
||||||
|
dict as defined above
|
||||||
|
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
def get_version(self):
|
def get_version(self):
|
||||||
"""Get a name and version for this hardware manager.
|
"""Get a name and version for this hardware manager.
|
||||||
|
|
||||||
@ -1486,6 +1534,22 @@ class GenericHardwareManager(HardwareManager):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_deploy_steps(self, node, ports):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'step': 'delete_configuration',
|
||||||
|
'priority': 0,
|
||||||
|
'interface': 'raid',
|
||||||
|
'reboot_requested': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'step': 'create_configuration',
|
||||||
|
'priority': 0,
|
||||||
|
'interface': 'raid',
|
||||||
|
'reboot_requested': False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
def create_configuration(self, node, ports):
|
def create_configuration(self, node, ports):
|
||||||
"""Create a RAID configuration.
|
"""Create a RAID configuration.
|
||||||
|
|
||||||
@ -2088,3 +2152,102 @@ def cache_node(node):
|
|||||||
def get_cached_node():
|
def get_cached_node():
|
||||||
"""Guard function around the module variable NODE."""
|
"""Guard function around the module variable NODE."""
|
||||||
return NODE
|
return NODE
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_versions():
|
||||||
|
"""Fetches versions from all hardware managers.
|
||||||
|
|
||||||
|
:returns: Dict in the format {name: version} containing one entry for
|
||||||
|
every hardware manager.
|
||||||
|
"""
|
||||||
|
return {version.get('name'): version.get('version')
|
||||||
|
for version in dispatch_to_all_managers('get_version').values()}
|
||||||
|
|
||||||
|
|
||||||
|
def check_versions(provided_version=None):
|
||||||
|
"""Ensure the version of hardware managers hasn't changed.
|
||||||
|
|
||||||
|
:param provided_version: Hardware manager versions used by ironic.
|
||||||
|
:raises: errors.VersionMismatch if any hardware manager version on
|
||||||
|
the currently running agent doesn't match the one stored in
|
||||||
|
provided_version.
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
# If the version is None, assume this is the first run
|
||||||
|
if provided_version is None:
|
||||||
|
return
|
||||||
|
agent_version = get_current_versions()
|
||||||
|
if provided_version != agent_version:
|
||||||
|
LOG.warning('Mismatched hardware managers versions. Agent version: '
|
||||||
|
'%(agent)s, node version: %(node)s',
|
||||||
|
{'agent': agent_version, 'node': provided_version})
|
||||||
|
raise errors.VersionMismatch(agent_version=agent_version,
|
||||||
|
node_version=provided_version)
|
||||||
|
|
||||||
|
|
||||||
|
def deduplicate_steps(candidate_steps):
|
||||||
|
"""Remove duplicated clean or deploy steps
|
||||||
|
|
||||||
|
Deduplicates steps returned from HardwareManagers to prevent running
|
||||||
|
a given step more than once. Other than individual step priority,
|
||||||
|
it doesn't actually impact the deployment which specific steps are kept
|
||||||
|
and what HardwareManager they are associated with.
|
||||||
|
However, in order to make testing easier, this method returns
|
||||||
|
deterministic results.
|
||||||
|
|
||||||
|
Uses the following filtering logic to decide which step "wins":
|
||||||
|
|
||||||
|
- Keep the step that belongs to HardwareManager with highest
|
||||||
|
HardwareSupport (larger int) value.
|
||||||
|
- If equal support level, keep the step with the higher defined priority
|
||||||
|
(larger int).
|
||||||
|
- If equal support level and priority, keep the step associated with the
|
||||||
|
HardwareManager whose name comes earlier in the alphabet.
|
||||||
|
|
||||||
|
:param candidate_steps: A dict containing all possible steps from
|
||||||
|
all managers, key=manager, value=list of steps
|
||||||
|
:returns: A deduplicated dictionary of {hardware_manager: [steps]}
|
||||||
|
"""
|
||||||
|
support = dispatch_to_all_managers(
|
||||||
|
'evaluate_hardware_support')
|
||||||
|
|
||||||
|
steps = collections.defaultdict(list)
|
||||||
|
deduped_steps = collections.defaultdict(list)
|
||||||
|
|
||||||
|
for manager, manager_steps in candidate_steps.items():
|
||||||
|
# We cannot deduplicate steps with unknown hardware support
|
||||||
|
if manager not in support:
|
||||||
|
LOG.warning('Unknown hardware support for %(manager)s, '
|
||||||
|
'dropping steps: %(steps)s',
|
||||||
|
{'manager': manager, 'steps': manager_steps})
|
||||||
|
continue
|
||||||
|
|
||||||
|
for step in manager_steps:
|
||||||
|
# build a new dict of steps that's easier to filter
|
||||||
|
step['hwm'] = {'name': manager,
|
||||||
|
'support': support[manager]}
|
||||||
|
steps[step['step']].append(step)
|
||||||
|
|
||||||
|
for step_name, step_list in steps.items():
|
||||||
|
# determine the max support level among candidate steps
|
||||||
|
max_support = max([x['hwm']['support'] for x in step_list])
|
||||||
|
# filter out any steps that are not at the max support for this step
|
||||||
|
max_support_steps = [x for x in step_list
|
||||||
|
if x['hwm']['support'] == max_support]
|
||||||
|
|
||||||
|
# determine the max priority among remaining steps
|
||||||
|
max_priority = max([x['priority'] for x in max_support_steps])
|
||||||
|
# filter out any steps that are not at the max priority for this step
|
||||||
|
max_priority_steps = [x for x in max_support_steps
|
||||||
|
if x['priority'] == max_priority]
|
||||||
|
|
||||||
|
# if there are still multiple steps, sort by hwm name and take
|
||||||
|
# the first result
|
||||||
|
winning_step = sorted(max_priority_steps,
|
||||||
|
key=lambda x: x['hwm']['name'])[0]
|
||||||
|
# Remove extra metadata we added to the step for filtering
|
||||||
|
manager = winning_step.pop('hwm')['name']
|
||||||
|
# Add winning step to deduped_steps
|
||||||
|
deduped_steps[manager].append(winning_step)
|
||||||
|
|
||||||
|
return deduped_steps
|
||||||
|
@ -34,8 +34,8 @@ class TestCleanExtension(base.IronicAgentTest):
|
|||||||
}
|
}
|
||||||
self.version = {'generic': '1', 'specific': '1'}
|
self.version = {'generic': '1', 'specific': '1'}
|
||||||
|
|
||||||
@mock.patch('ironic_python_agent.extensions.clean.'
|
@mock.patch('ironic_python_agent.hardware.get_current_versions',
|
||||||
'_get_current_clean_version', autospec=True)
|
autospec=True)
|
||||||
@mock.patch('ironic_python_agent.hardware.dispatch_to_all_managers',
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_all_managers',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_get_clean_steps(self, mock_dispatch, mock_version,
|
def test_get_clean_steps(self, mock_dispatch, mock_version,
|
||||||
@ -141,7 +141,7 @@ class TestCleanExtension(base.IronicAgentTest):
|
|||||||
|
|
||||||
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch('ironic_python_agent.extensions.clean._check_clean_version',
|
@mock.patch('ironic_python_agent.hardware.check_versions',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_execute_clean_step(self, mock_version, mock_dispatch,
|
def test_execute_clean_step(self, mock_version, mock_dispatch,
|
||||||
mock_cache_node):
|
mock_cache_node):
|
||||||
@ -167,7 +167,7 @@ class TestCleanExtension(base.IronicAgentTest):
|
|||||||
|
|
||||||
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch('ironic_python_agent.extensions.clean._check_clean_version',
|
@mock.patch('ironic_python_agent.hardware.check_versions',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_execute_clean_step_tuple_result(self, mock_version,
|
def test_execute_clean_step_tuple_result(self, mock_version,
|
||||||
mock_dispatch, mock_cache_node):
|
mock_dispatch, mock_cache_node):
|
||||||
@ -191,7 +191,7 @@ class TestCleanExtension(base.IronicAgentTest):
|
|||||||
self.assertEqual(expected_result, async_result.command_result)
|
self.assertEqual(expected_result, async_result.command_result)
|
||||||
mock_cache_node.assert_called_once_with(self.node)
|
mock_cache_node.assert_called_once_with(self.node)
|
||||||
|
|
||||||
@mock.patch('ironic_python_agent.extensions.clean._check_clean_version',
|
@mock.patch('ironic_python_agent.hardware.check_versions',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_execute_clean_step_no_step(self, mock_version, mock_cache_node):
|
def test_execute_clean_step_no_step(self, mock_version, mock_cache_node):
|
||||||
async_result = self.agent_extension.execute_clean_step(
|
async_result = self.agent_extension.execute_clean_step(
|
||||||
@ -205,7 +205,7 @@ class TestCleanExtension(base.IronicAgentTest):
|
|||||||
|
|
||||||
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch('ironic_python_agent.extensions.clean._check_clean_version',
|
@mock.patch('ironic_python_agent.hardware.check_versions',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_execute_clean_step_fail(self, mock_version, mock_dispatch,
|
def test_execute_clean_step_fail(self, mock_version, mock_dispatch,
|
||||||
mock_cache_node):
|
mock_cache_node):
|
||||||
@ -226,12 +226,12 @@ class TestCleanExtension(base.IronicAgentTest):
|
|||||||
|
|
||||||
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@mock.patch('ironic_python_agent.extensions.clean._check_clean_version',
|
@mock.patch('ironic_python_agent.hardware.check_versions',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_execute_clean_step_version_mismatch(self, mock_version,
|
def test_execute_clean_step_version_mismatch(self, mock_version,
|
||||||
mock_dispatch,
|
mock_dispatch,
|
||||||
mock_cache_node):
|
mock_cache_node):
|
||||||
mock_version.side_effect = errors.CleanVersionMismatch(
|
mock_version.side_effect = errors.VersionMismatch(
|
||||||
{'GenericHardwareManager': 1}, {'GenericHardwareManager': 2})
|
{'GenericHardwareManager': 1}, {'GenericHardwareManager': 2})
|
||||||
|
|
||||||
async_result = self.agent_extension.execute_clean_step(
|
async_result = self.agent_extension.execute_clean_step(
|
||||||
@ -241,24 +241,3 @@ class TestCleanExtension(base.IronicAgentTest):
|
|||||||
self.assertEqual('CLEAN_VERSION_MISMATCH', async_result.command_status)
|
self.assertEqual('CLEAN_VERSION_MISMATCH', async_result.command_status)
|
||||||
|
|
||||||
mock_version.assert_called_once_with(self.version)
|
mock_version.assert_called_once_with(self.version)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('ironic_python_agent.hardware.dispatch_to_all_managers',
|
|
||||||
autospec=True)
|
|
||||||
class TestCleanVersion(base.IronicAgentTest):
|
|
||||||
version = {'generic': '1', 'specific': '1'}
|
|
||||||
|
|
||||||
def test__get_current_clean_version(self, mock_dispatch):
|
|
||||||
mock_dispatch.return_value = {'SpecificHardwareManager':
|
|
||||||
{'name': 'specific', 'version': '1'},
|
|
||||||
'GenericHardwareManager':
|
|
||||||
{'name': 'generic', 'version': '1'}}
|
|
||||||
self.assertEqual(self.version, clean._get_current_clean_version())
|
|
||||||
|
|
||||||
def test__check_clean_version_fail(self, mock_dispatch):
|
|
||||||
mock_dispatch.return_value = {'SpecificHardwareManager':
|
|
||||||
{'name': 'specific', 'version': '1'}}
|
|
||||||
|
|
||||||
self.assertRaises(errors.CleanVersionMismatch,
|
|
||||||
clean._check_clean_version,
|
|
||||||
{'not_specific': '1'})
|
|
||||||
|
241
ironic_python_agent/tests/unit/extensions/test_deploy.py
Normal file
241
ironic_python_agent/tests/unit/extensions/test_deploy.py
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
# 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 ironic_python_agent import errors
|
||||||
|
from ironic_python_agent.extensions import deploy
|
||||||
|
from ironic_python_agent.tests.unit import base
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.cache_node', autospec=True)
|
||||||
|
class TestDeployExtension(base.IronicAgentTest):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestDeployExtension, self).setUp()
|
||||||
|
self.agent_extension = deploy.DeployExtension()
|
||||||
|
self.node = {'uuid': 'dda135fb-732d-4742-8e72-df8f3199d244'}
|
||||||
|
self.ports = []
|
||||||
|
self.step = {
|
||||||
|
'GenericHardwareManager':
|
||||||
|
[{'step': 'erase_devices',
|
||||||
|
'priority': 10,
|
||||||
|
'interface': 'deploy'}]
|
||||||
|
}
|
||||||
|
self.version = {'generic': '1', 'specific': '1'}
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.get_current_versions',
|
||||||
|
autospec=True)
|
||||||
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_all_managers',
|
||||||
|
autospec=True)
|
||||||
|
def test_get_deploy_steps(self, mock_dispatch, mock_version,
|
||||||
|
mock_cache_node):
|
||||||
|
mock_version.return_value = self.version
|
||||||
|
|
||||||
|
manager_steps = {
|
||||||
|
'SpecificHardwareManager': [
|
||||||
|
{
|
||||||
|
'step': 'erase_devices',
|
||||||
|
'priority': 10,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'step': 'upgrade_bios',
|
||||||
|
'priority': 20,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'step': 'upgrade_firmware',
|
||||||
|
'priority': 60,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': False
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'FirmwareHardwareManager': [
|
||||||
|
{
|
||||||
|
'step': 'upgrade_firmware',
|
||||||
|
'priority': 10,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'step': 'erase_devices',
|
||||||
|
'priority': 40,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': False
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'DiskHardwareManager': [
|
||||||
|
{
|
||||||
|
'step': 'erase_devices',
|
||||||
|
'priority': 50,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': False
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_steps = {
|
||||||
|
'SpecificHardwareManager': [
|
||||||
|
# Only manager upgrading BIOS
|
||||||
|
{
|
||||||
|
'step': 'upgrade_bios',
|
||||||
|
'priority': 20,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': True
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'FirmwareHardwareManager': [
|
||||||
|
# Higher support than specific, even though lower priority
|
||||||
|
{
|
||||||
|
'step': 'upgrade_firmware',
|
||||||
|
'priority': 10,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': False
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'DiskHardwareManager': [
|
||||||
|
# Higher support than specific, higher priority than firmware
|
||||||
|
{
|
||||||
|
'step': 'erase_devices',
|
||||||
|
'priority': 50,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': False
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
hardware_support = {
|
||||||
|
'SpecificHardwareManager': 3,
|
||||||
|
'FirmwareHardwareManager': 4,
|
||||||
|
'DiskHardwareManager': 4
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_dispatch.side_effect = [manager_steps, hardware_support]
|
||||||
|
expected_return = {
|
||||||
|
'hardware_manager_version': self.version,
|
||||||
|
'deploy_steps': expected_steps
|
||||||
|
}
|
||||||
|
|
||||||
|
async_results = self.agent_extension.get_deploy_steps(node=self.node,
|
||||||
|
ports=self.ports)
|
||||||
|
|
||||||
|
# Ordering of the deploy steps doesn't matter; they're sorted by
|
||||||
|
# 'priority' in Ironic
|
||||||
|
self.assertEqual(expected_return,
|
||||||
|
async_results.join().command_result)
|
||||||
|
mock_cache_node.assert_called_once_with(self.node)
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
||||||
|
autospec=True)
|
||||||
|
@mock.patch('ironic_python_agent.hardware.check_versions',
|
||||||
|
autospec=True)
|
||||||
|
def test_execute_deploy_step(self, mock_version, mock_dispatch,
|
||||||
|
mock_cache_node):
|
||||||
|
result = 'deployed'
|
||||||
|
mock_dispatch.return_value = result
|
||||||
|
|
||||||
|
expected_result = {
|
||||||
|
'deploy_step': self.step['GenericHardwareManager'][0],
|
||||||
|
'deploy_result': result
|
||||||
|
}
|
||||||
|
async_result = self.agent_extension.execute_deploy_step(
|
||||||
|
step=self.step['GenericHardwareManager'][0],
|
||||||
|
node=self.node, ports=self.ports,
|
||||||
|
deploy_version=self.version)
|
||||||
|
async_result.join()
|
||||||
|
|
||||||
|
mock_version.assert_called_once_with(self.version)
|
||||||
|
mock_dispatch.assert_called_once_with(
|
||||||
|
self.step['GenericHardwareManager'][0]['step'],
|
||||||
|
self.node, self.ports)
|
||||||
|
self.assertEqual(expected_result, async_result.command_result)
|
||||||
|
mock_cache_node.assert_called_once_with(self.node)
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
||||||
|
autospec=True)
|
||||||
|
@mock.patch('ironic_python_agent.hardware.check_versions',
|
||||||
|
autospec=True)
|
||||||
|
def test_execute_deploy_step_tuple_result(self, mock_version,
|
||||||
|
mock_dispatch, mock_cache_node):
|
||||||
|
result = ('stdout', 'stderr')
|
||||||
|
mock_dispatch.return_value = result
|
||||||
|
|
||||||
|
expected_result = {
|
||||||
|
'deploy_step': self.step['GenericHardwareManager'][0],
|
||||||
|
'deploy_result': ['stdout', 'stderr']
|
||||||
|
}
|
||||||
|
async_result = self.agent_extension.execute_deploy_step(
|
||||||
|
step=self.step['GenericHardwareManager'][0],
|
||||||
|
node=self.node, ports=self.ports,
|
||||||
|
deploy_version=self.version)
|
||||||
|
async_result.join()
|
||||||
|
|
||||||
|
mock_version.assert_called_once_with(self.version)
|
||||||
|
mock_dispatch.assert_called_once_with(
|
||||||
|
self.step['GenericHardwareManager'][0]['step'],
|
||||||
|
self.node, self.ports)
|
||||||
|
self.assertEqual(expected_result, async_result.command_result)
|
||||||
|
mock_cache_node.assert_called_once_with(self.node)
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.check_versions',
|
||||||
|
autospec=True)
|
||||||
|
def test_execute_deploy_step_no_step(self, mock_version, mock_cache_node):
|
||||||
|
async_result = self.agent_extension.execute_deploy_step(
|
||||||
|
step={}, node=self.node, ports=self.ports,
|
||||||
|
deploy_version=self.version)
|
||||||
|
async_result.join()
|
||||||
|
|
||||||
|
self.assertEqual('FAILED', async_result.command_status)
|
||||||
|
mock_version.assert_called_once_with(self.version)
|
||||||
|
mock_cache_node.assert_called_once_with(self.node)
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
||||||
|
autospec=True)
|
||||||
|
@mock.patch('ironic_python_agent.hardware.check_versions',
|
||||||
|
autospec=True)
|
||||||
|
def test_execute_deploy_step_fail(self, mock_version, mock_dispatch,
|
||||||
|
mock_cache_node):
|
||||||
|
mock_dispatch.side_effect = RuntimeError
|
||||||
|
|
||||||
|
async_result = self.agent_extension.execute_deploy_step(
|
||||||
|
step=self.step['GenericHardwareManager'][0], node=self.node,
|
||||||
|
ports=self.ports, deploy_version=self.version)
|
||||||
|
async_result.join()
|
||||||
|
|
||||||
|
self.assertEqual('FAILED', async_result.command_status)
|
||||||
|
|
||||||
|
mock_version.assert_called_once_with(self.version)
|
||||||
|
mock_dispatch.assert_called_once_with(
|
||||||
|
self.step['GenericHardwareManager'][0]['step'],
|
||||||
|
self.node, self.ports)
|
||||||
|
mock_cache_node.assert_called_once_with(self.node)
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
||||||
|
autospec=True)
|
||||||
|
@mock.patch('ironic_python_agent.hardware.check_versions',
|
||||||
|
autospec=True)
|
||||||
|
def test_execute_deploy_step_version_mismatch(self, mock_version,
|
||||||
|
mock_dispatch,
|
||||||
|
mock_cache_node):
|
||||||
|
mock_version.side_effect = errors.VersionMismatch(
|
||||||
|
{'GenericHardwareManager': 1}, {'GenericHardwareManager': 2})
|
||||||
|
|
||||||
|
async_result = self.agent_extension.execute_deploy_step(
|
||||||
|
step=self.step['GenericHardwareManager'][0], node=self.node,
|
||||||
|
ports=self.ports, deploy_version=self.version)
|
||||||
|
async_result.join()
|
||||||
|
self.assertEqual('CLEAN_VERSION_MISMATCH', async_result.command_status)
|
||||||
|
|
||||||
|
mock_version.assert_called_once_with(self.version)
|
@ -4336,3 +4336,24 @@ def create_hdparm_info(supported=False, enabled=False, locked=False,
|
|||||||
update_values(values, enhanced_erase, 'enhanced_erase')
|
update_values(values, enhanced_erase, 'enhanced_erase')
|
||||||
|
|
||||||
return HDPARM_INFO_TEMPLATE % values
|
return HDPARM_INFO_TEMPLATE % values
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_all_managers',
|
||||||
|
autospec=True)
|
||||||
|
class TestVersions(base.IronicAgentTest):
|
||||||
|
version = {'generic': '1', 'specific': '1'}
|
||||||
|
|
||||||
|
def test_get_current_versions(self, mock_dispatch):
|
||||||
|
mock_dispatch.return_value = {'SpecificHardwareManager':
|
||||||
|
{'name': 'specific', 'version': '1'},
|
||||||
|
'GenericHardwareManager':
|
||||||
|
{'name': 'generic', 'version': '1'}}
|
||||||
|
self.assertEqual(self.version, hardware.get_current_versions())
|
||||||
|
|
||||||
|
def test_check_versions(self, mock_dispatch):
|
||||||
|
mock_dispatch.return_value = {'SpecificHardwareManager':
|
||||||
|
{'name': 'specific', 'version': '1'}}
|
||||||
|
|
||||||
|
self.assertRaises(errors.VersionMismatch,
|
||||||
|
hardware.check_versions,
|
||||||
|
{'not_specific': '1'})
|
||||||
|
@ -36,6 +36,7 @@ console_scripts =
|
|||||||
ironic_python_agent.extensions =
|
ironic_python_agent.extensions =
|
||||||
standby = ironic_python_agent.extensions.standby:StandbyExtension
|
standby = ironic_python_agent.extensions.standby:StandbyExtension
|
||||||
clean = ironic_python_agent.extensions.clean:CleanExtension
|
clean = ironic_python_agent.extensions.clean:CleanExtension
|
||||||
|
deploy = ironic_python_agent.extensions.deploy:DeployExtension
|
||||||
flow = ironic_python_agent.extensions.flow:FlowExtension
|
flow = ironic_python_agent.extensions.flow:FlowExtension
|
||||||
iscsi = ironic_python_agent.extensions.iscsi:ISCSIExtension
|
iscsi = ironic_python_agent.extensions.iscsi:ISCSIExtension
|
||||||
image = ironic_python_agent.extensions.image:ImageExtension
|
image = ironic_python_agent.extensions.image:ImageExtension
|
||||||
|
Loading…
Reference in New Issue
Block a user