Merge "Add an ability to run in-band deploy steps"

This commit is contained in:
Zuul 2020-04-09 09:31:49 +00:00 committed by Gerrit Code Review
commit b9e320e76f
9 changed files with 559 additions and 149 deletions

View File

@ -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."""

View File

@ -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)

View File

@ -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()}

View 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
}

View File

@ -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

View File

@ -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'})

View 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)

View File

@ -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'})

View File

@ -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