Add get_service_steps logic to the agent
Initial code patches for service steps have merged in ironic, and it is now time to add support into the agent which allows service steps to be raised to the service. Updates the default hardware manager version to 1.2, which has *rarely* been incremented due to oversight. Change-Id: Iabd2c6c551389ec3c24e94b71245b1250345f7a7
This commit is contained in:
parent
b68a4c8a92
commit
eb95273ffb
@ -6,7 +6,8 @@ GenericHardwareManager
|
||||
======================
|
||||
|
||||
This is the default hardware manager for ironic-python-agent. It provides
|
||||
support for :ref:`hardware-inventory` and the default deploy and clean steps.
|
||||
support for :ref:`hardware-inventory` and the default deploy, clean,
|
||||
and service steps.
|
||||
|
||||
Deploy steps
|
||||
------------
|
||||
@ -104,6 +105,57 @@ Clean steps
|
||||
and must be used through the :ironic-doc:`ironic RAID feature
|
||||
<admin/raid.html>`.
|
||||
|
||||
Service steps
|
||||
-------------
|
||||
|
||||
Service steps can be invoked by an operator of a baremetal node, to modify
|
||||
or perform some intermediate action outside the realm of normal use of a
|
||||
deployed bare metal instance. This is similar in form of interaction to
|
||||
cleaning, and ultimately some cleaning and deployment steps *are* available
|
||||
to be used.
|
||||
|
||||
``deploy.burnin_cpu``
|
||||
Stress-test the CPUs of a node via stress-ng for a configurable
|
||||
amount of time.
|
||||
``deploy.burnin_memory``
|
||||
Stress-test the memory of a node via stress-ng for a configurable
|
||||
amount of time.
|
||||
``deploy.burnin_network``
|
||||
Stress-test the network of a pair of nodes via fio for a configurable
|
||||
amount of time.
|
||||
``raid.create_configuration``
|
||||
Create a RAID configuration. This step belongs to the ``raid`` interface
|
||||
and must be used through the :ironic-doc:`ironic RAID feature
|
||||
<admin/raid.html>`.
|
||||
``raid.apply_configuration(node, ports, raid_config, delete_existing=True)``
|
||||
Apply a software RAID configuration. It belongs to the ``raid`` interface
|
||||
and must be used through the :ironic-doc:`ironic RAID feature
|
||||
<admin/raid.html>`.
|
||||
``raid.delete_configuration``
|
||||
Delete the RAID configuration. This step belongs to the ``raid`` interface
|
||||
and must be used through the :ironic-doc:`ironic RAID feature
|
||||
<admin/raid.html>`.
|
||||
``deploy.write_image(node, ports, image_info, configdrive=None)``
|
||||
A step backing the ``write_image`` deploy step of the
|
||||
:ironic-doc:`direct deploy interface
|
||||
<admin/interfaces/deploy.html#direct-deploy>`.
|
||||
Should not be used explicitly, but can be overridden to provide a custom
|
||||
way of writing an image.
|
||||
``deploy.inject_files(node, ports, files, verify_ca=True)``
|
||||
A step to inject files into a system. Specifically this step is documented
|
||||
earlier in this documentation.
|
||||
|
||||
.. NOTE::
|
||||
The Ironic Developers chose to limit the items available for service steps
|
||||
such that the risk of data distruction is generally minimized.
|
||||
That being said, it could be reasonable to reconfigure RAID devices through
|
||||
local hardware managers *or* to write the base OS image as part of a
|
||||
service operation. As such, caution should be taken, and if additional data
|
||||
erasure steps are needed you may want to consider moving a node through
|
||||
cleaning to remove the workload. Otherwise, if you have a use case, please
|
||||
feel free to reach out to the Ironic Developers so we can understand and
|
||||
enable your use case.
|
||||
|
||||
Cleaning safeguards
|
||||
-------------------
|
||||
|
||||
@ -194,3 +246,10 @@ Each settings in the list is a dictionary with the following fields:
|
||||
The per-function configuration of the first port of the NIC
|
||||
``function1Config``
|
||||
The per-function configuration of the second port of the NIC
|
||||
|
||||
Service steps
|
||||
-------------
|
||||
|
||||
The Clean steps supported by the MellanoxDeviceHardwareManager are also
|
||||
available as Service steps if an infrastructure operator wishes to apply
|
||||
new firmware for a running machine.
|
||||
|
@ -246,6 +246,52 @@ There are two kinds of deploy steps:
|
||||
def write_a_file(self, node, ports, path, contents, mode=0o644):
|
||||
pass # Mount the disk, write a file.
|
||||
|
||||
Custom HardwareManagers and Service operations
|
||||
----------------------------------------------
|
||||
|
||||
Starting with the Bobcat release cycle, A hardware manager can define
|
||||
*service steps* that may be run during a service operation by exposing a
|
||||
``get_service_steps`` call.
|
||||
|
||||
Service steps are intended to be invoked by an operator to perform an ad-hoc
|
||||
action upon a node. This does not include automatic step execution, but may
|
||||
at some point in the future. The result is that steps can be exposed similar
|
||||
to Clean steps and Deploy steps, just the priority value, should be 0 as
|
||||
the user requested order is what is utilized.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def get_deploy_steps(self, node, ports):
|
||||
return [
|
||||
{
|
||||
# A function on the custom hardware manager
|
||||
'step': 'write_a_file',
|
||||
# Steps with priority 0 don't run by default.
|
||||
'priority': 0,
|
||||
# Should be the deploy interface, unless there is driver-side
|
||||
# support for another interface (as it is for RAID).
|
||||
'interface': 'deploy',
|
||||
# Arguments that can be required or optional.
|
||||
'argsinfo': {
|
||||
'path': {
|
||||
'description': 'Path to file',
|
||||
'required': True,
|
||||
},
|
||||
'content': {
|
||||
'description': 'Content of the file',
|
||||
'required': True,
|
||||
},
|
||||
'mode': {
|
||||
'description': 'Mode of the file, defaults to 0644',
|
||||
'required': False,
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def write_a_file(self, node, ports, path, contents, mode=0o644):
|
||||
pass # Mount the disk, write a file.
|
||||
|
||||
Versioning
|
||||
~~~~~~~~~~
|
||||
Each hardware manager has a name and a version. This version is used during
|
||||
|
@ -312,6 +312,15 @@ class DeploymentError(RESTError):
|
||||
super(DeploymentError, self).__init__(details)
|
||||
|
||||
|
||||
class ServicingError(RESTError):
|
||||
"""Error raised when a service step fails."""
|
||||
|
||||
message = 'Service step failed'
|
||||
|
||||
def __init__(self, details=None):
|
||||
super(ServicingError, self).__init__(details)
|
||||
|
||||
|
||||
class IncompatibleNumaFormatError(RESTError):
|
||||
"""Error raised when unexpected format data in NUMA node."""
|
||||
|
||||
|
103
ironic_python_agent/extensions/service.py
Normal file
103
ironic_python_agent/extensions/service.py
Normal file
@ -0,0 +1,103 @@
|
||||
# Copyright 2015 Rackspace, Inc.
|
||||
#
|
||||
# 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 ironic_lib import exception as il_exc
|
||||
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 ServiceExtension(base.BaseAgentExtension):
|
||||
@base.sync_command('get_service_steps')
|
||||
def get_service_steps(self, node, ports):
|
||||
"""Get the list of service 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 service steps with keys step, priority, and
|
||||
reboot_requested
|
||||
"""
|
||||
LOG.debug('Getting service 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_service_steps', node, ports)
|
||||
|
||||
LOG.debug('Service steps before deduplication: %s', candidate_steps)
|
||||
service_steps = hardware.deduplicate_steps(candidate_steps)
|
||||
LOG.debug('Returning service steps: %s', service_steps)
|
||||
|
||||
return {
|
||||
'service_steps': service_steps,
|
||||
'hardware_manager_version': hardware.get_current_versions(),
|
||||
}
|
||||
|
||||
@base.async_command('execute_service_step')
|
||||
def execute_service_step(self, step, node, ports, service_version=None,
|
||||
**kwargs):
|
||||
"""Execute a service step.
|
||||
|
||||
:param step: A 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 service_version: The service version as returned by
|
||||
hardware.get_current_versions() at the beginning
|
||||
of the service operation.
|
||||
: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 service step %s', step)
|
||||
hardware.cache_node(node)
|
||||
hardware.check_versions(service_version)
|
||||
|
||||
if 'step' not in step:
|
||||
msg = 'Malformed service_step, no "step" key: %s' % step
|
||||
LOG.error(msg)
|
||||
raise ValueError(msg)
|
||||
kwargs.update(step.get('args') or {})
|
||||
try:
|
||||
result = hardware.dispatch_to_managers(step['step'], node, ports,
|
||||
**kwargs)
|
||||
except (errors.RESTError, il_exc.IronicException):
|
||||
LOG.exception('Error performing service step %s', step['step'])
|
||||
raise
|
||||
except Exception as e:
|
||||
msg = ('Unexpected exception performing service step %(step)s. '
|
||||
'%(cls)s: %(err)s' % {'step': step['step'],
|
||||
'cls': e.__class__.__name__,
|
||||
'err': e})
|
||||
LOG.exception(msg)
|
||||
raise errors.ServicingError(msg)
|
||||
|
||||
LOG.info('Service 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 {
|
||||
'service_result': result,
|
||||
'service_step': step
|
||||
}
|
@ -1145,6 +1145,60 @@ class HardwareManager(object, metaclass=abc.ABCMeta):
|
||||
"""
|
||||
return []
|
||||
|
||||
def get_service_steps(self, node, ports):
|
||||
"""Get a list of service steps.
|
||||
|
||||
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 if excuted upon
|
||||
similar to automated cleaning or deployment.
|
||||
In service steps, the order comes from the user request,
|
||||
but this similarity is kept for consistency should we
|
||||
further extend the capability at some point in the
|
||||
future.
|
||||
'reboot_requested': Whether the agent should request Ironic reboots
|
||||
the node via the power driver after the
|
||||
operation completes.
|
||||
'abortable': Boolean value. Whether the service step can be
|
||||
stopped by the operator or not. Some steps may
|
||||
cause non-reversible damage to a machine if interrupted
|
||||
(i.e firmware update), for such steps this parameter
|
||||
should be set to False. If no value is set for this
|
||||
parameter, Ironic will consider False (non-abortable).
|
||||
}
|
||||
|
||||
|
||||
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 step is supported for the node.
|
||||
|
||||
:param node: Ironic node object
|
||||
:param ports: list of Ironic port objects
|
||||
:return: a list of service steps, where each step is described as a
|
||||
dict as defined above
|
||||
|
||||
"""
|
||||
return []
|
||||
|
||||
def get_version(self):
|
||||
"""Get a name and version for this hardware manager.
|
||||
|
||||
@ -1186,7 +1240,8 @@ class HardwareManager(object, metaclass=abc.ABCMeta):
|
||||
class GenericHardwareManager(HardwareManager):
|
||||
HARDWARE_MANAGER_NAME = 'generic_hardware_manager'
|
||||
# 1.1 - Added new clean step called erase_devices_metadata
|
||||
HARDWARE_MANAGER_VERSION = '1.1'
|
||||
# 1.2 - Added new get_service_steps method
|
||||
HARDWARE_MANAGER_VERSION = '1.2'
|
||||
|
||||
def __init__(self):
|
||||
self.lldp_data = {}
|
||||
@ -2399,6 +2454,77 @@ class GenericHardwareManager(HardwareManager):
|
||||
},
|
||||
]
|
||||
|
||||
# TODO(TheJulia): There has to be a better way, we should
|
||||
# make this less copy paste. That being said, I can also see
|
||||
# unique priorites being needed.
|
||||
def get_service_steps(self, node, ports):
|
||||
service_steps = [
|
||||
{
|
||||
'step': 'delete_configuration',
|
||||
'priority': 0,
|
||||
'interface': 'raid',
|
||||
'reboot_requested': False,
|
||||
'abortable': True
|
||||
},
|
||||
{
|
||||
'step': 'apply_configuration',
|
||||
'priority': 0,
|
||||
'interface': 'raid',
|
||||
'reboot_requested': False,
|
||||
'argsinfo': RAID_APPLY_CONFIGURATION_ARGSINFO,
|
||||
},
|
||||
{
|
||||
'step': 'create_configuration',
|
||||
'priority': 0,
|
||||
'interface': 'raid',
|
||||
'reboot_requested': False,
|
||||
'abortable': True
|
||||
},
|
||||
{
|
||||
'step': 'burnin_cpu',
|
||||
'priority': 0,
|
||||
'interface': 'deploy',
|
||||
'reboot_requested': False,
|
||||
'abortable': True
|
||||
},
|
||||
# NOTE(TheJulia): Burnin disk is explicilty not carried in this
|
||||
# list because it would be distructive to data on a disk.
|
||||
# If someone needs to do that, the machine should be
|
||||
# unprovisioned.
|
||||
{
|
||||
'step': 'burnin_memory',
|
||||
'priority': 0,
|
||||
'interface': 'deploy',
|
||||
'reboot_requested': False,
|
||||
'abortable': True
|
||||
},
|
||||
{
|
||||
'step': 'burnin_network',
|
||||
'priority': 0,
|
||||
'interface': 'deploy',
|
||||
'reboot_requested': False,
|
||||
'abortable': True
|
||||
},
|
||||
{
|
||||
'step': 'write_image',
|
||||
# NOTE(dtantsur): this step has to be proxied via an
|
||||
# out-of-band step with the same name, hence the priority here
|
||||
# doesn't really matter.
|
||||
'priority': 0,
|
||||
'interface': 'deploy',
|
||||
'reboot_requested': False,
|
||||
},
|
||||
{
|
||||
'step': 'inject_files',
|
||||
'priority': CONF.inject_files_priority,
|
||||
'interface': 'deploy',
|
||||
'reboot_requested': False,
|
||||
'argsinfo': inject_files.ARGSINFO,
|
||||
},
|
||||
]
|
||||
# TODO(TheJulia): Consider erase_devices and friends...
|
||||
return service_steps
|
||||
|
||||
def apply_configuration(self, node, ports, raid_config,
|
||||
delete_existing=True):
|
||||
"""Apply RAID configuration.
|
||||
|
@ -158,6 +158,14 @@ class MellanoxDeviceHardwareManager(hardware.HardwareManager):
|
||||
}
|
||||
]
|
||||
|
||||
def get_service_steps(self, node, ports):
|
||||
"""Alias wrapper for method get_clean_steps."""
|
||||
# NOTE(TheJulia): Since these steps can be run upon service, why not
|
||||
return self.get_clean_steps(node, ports)
|
||||
|
||||
# TODO(TheJulia): Should there be a get_deploy_steps handler here? Since
|
||||
# flashing firmware on deploy is a valid case.
|
||||
|
||||
def update_nvidia_nic_firmware_image(self, node, ports, images):
|
||||
nvidia_fw_update.update_nvidia_nic_firmware_image(images)
|
||||
|
||||
|
300
ironic_python_agent/tests/unit/extensions/test_service.py
Normal file
300
ironic_python_agent/tests/unit/extensions/test_service.py
Normal file
@ -0,0 +1,300 @@
|
||||
# Copyright 2015 Rackspace, Inc.
|
||||
#
|
||||
# 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 unittest import mock
|
||||
|
||||
from ironic_python_agent import errors
|
||||
from ironic_python_agent.extensions import service
|
||||
from ironic_python_agent.tests.unit import base
|
||||
|
||||
|
||||
@mock.patch('ironic_python_agent.hardware.cache_node', autospec=True)
|
||||
class TestServiceExtension(base.IronicAgentTest):
|
||||
def setUp(self):
|
||||
super(TestServiceExtension, self).setUp()
|
||||
self.agent_extension = service.ServiceExtension()
|
||||
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_service_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,
|
||||
'service_steps': expected_steps
|
||||
}
|
||||
|
||||
async_results = self.agent_extension.get_service_steps(
|
||||
node=self.node,
|
||||
ports=self.ports)
|
||||
|
||||
# Ordering of the service steps doesn't matter; they're sorted by
|
||||
# 'priority' in Ironic, and executed upon by user submission order
|
||||
# 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_service_step(self, mock_version, mock_dispatch,
|
||||
mock_cache_node):
|
||||
result = 'cleaned'
|
||||
mock_dispatch.return_value = result
|
||||
|
||||
expected_result = {
|
||||
'service_step': self.step['GenericHardwareManager'][0],
|
||||
'service_result': result
|
||||
}
|
||||
async_result = self.agent_extension.execute_service_step(
|
||||
step=self.step['GenericHardwareManager'][0],
|
||||
node=self.node, ports=self.ports,
|
||||
service_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_service_step_tuple_result(self, mock_version,
|
||||
mock_dispatch, mock_cache_node):
|
||||
result = ('stdout', 'stderr')
|
||||
mock_dispatch.return_value = result
|
||||
|
||||
expected_result = {
|
||||
'service_step': self.step['GenericHardwareManager'][0],
|
||||
'service_result': ['stdout', 'stderr']
|
||||
}
|
||||
async_result = self.agent_extension.execute_service_step(
|
||||
step=self.step['GenericHardwareManager'][0],
|
||||
node=self.node, ports=self.ports,
|
||||
service_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_service_step_with_args(self, mock_version, mock_dispatch,
|
||||
mock_cache_node):
|
||||
result = 'cleaned'
|
||||
mock_dispatch.return_value = result
|
||||
|
||||
step = self.step['GenericHardwareManager'][0]
|
||||
step['args'] = {'foo': 'bar'}
|
||||
expected_result = {
|
||||
'service_step': step,
|
||||
'service_result': result
|
||||
}
|
||||
async_result = self.agent_extension.execute_service_step(
|
||||
step=self.step['GenericHardwareManager'][0],
|
||||
node=self.node, ports=self.ports,
|
||||
service_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, foo='bar')
|
||||
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_service_step_no_step(self, mock_version, mock_cache_node):
|
||||
async_result = self.agent_extension.execute_service_step(
|
||||
step={}, node=self.node, ports=self.ports,
|
||||
service_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_service_step_fail(self, mock_version, mock_dispatch,
|
||||
mock_cache_node):
|
||||
err = errors.BlockDeviceError("I'm a teapot")
|
||||
mock_dispatch.side_effect = err
|
||||
|
||||
async_result = self.agent_extension.execute_service_step(
|
||||
step=self.step['GenericHardwareManager'][0], node=self.node,
|
||||
ports=self.ports, service_version=self.version)
|
||||
async_result.join()
|
||||
|
||||
self.assertEqual('FAILED', async_result.command_status)
|
||||
self.assertEqual(err, async_result.command_error)
|
||||
|
||||
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_service_step_exception(self, mock_version, mock_dispatch,
|
||||
mock_cache_node):
|
||||
mock_dispatch.side_effect = RuntimeError('boom')
|
||||
|
||||
async_result = self.agent_extension.execute_service_step(
|
||||
step=self.step['GenericHardwareManager'][0], node=self.node,
|
||||
ports=self.ports, service_version=self.version)
|
||||
async_result.join()
|
||||
|
||||
self.assertEqual('FAILED', async_result.command_status)
|
||||
self.assertIn('RuntimeError: boom', str(async_result.command_error))
|
||||
|
||||
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_service_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_service_step(
|
||||
step=self.step['GenericHardwareManager'][0], node=self.node,
|
||||
ports=self.ports, service_version=self.version)
|
||||
async_result.join()
|
||||
# NOTE(TheJulia): This remains CLEAN_VERSION_MISMATCH for backwards
|
||||
# compatability with base.py and API consumers.
|
||||
self.assertEqual('CLEAN_VERSION_MISMATCH',
|
||||
async_result.command_status)
|
||||
|
||||
mock_version.assert_called_once_with(self.version)
|
@ -211,6 +211,10 @@ class TestGenericHardwareManager(base.IronicAgentTest):
|
||||
for step in self.hardware.get_deploy_steps(self.node, []):
|
||||
getattr(self.hardware, step['step'])
|
||||
|
||||
def test_service_steps_exist(self):
|
||||
for step in self.hardware.get_service_steps(self.node, []):
|
||||
getattr(self.hardware, step['step'])
|
||||
|
||||
@mock.patch('binascii.hexlify', autospec=True)
|
||||
@mock.patch('ironic_python_agent.netutils.get_lldp_info', autospec=True)
|
||||
def test_collect_lldp_data(self, mock_lldp_info, mock_hexlify):
|
||||
|
@ -0,0 +1,14 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds a new ``service`` extension which facilitates command handling for
|
||||
Ironic to retrieve a list of service steps.
|
||||
- Adds a new base method to base HardwareManager, ``get_service_steps``
|
||||
which works the same as ``get_clean_steps`` and ``get_deploy_steps``.
|
||||
These methods can be extended by hardware managers to permit them to
|
||||
signal what steps are permitted.
|
||||
- Extends reasonable deploy/clean steps to also be service steps which
|
||||
are embedded in the Ironic agent. For example, CPU, Network, and Memory
|
||||
burnin steps are available as service steps, but not the disk burnin
|
||||
step as that would likely result in the existing disk contents being
|
||||
damaged.
|
@ -43,6 +43,7 @@ ironic_python_agent.extensions =
|
||||
log = ironic_python_agent.extensions.log:LogExtension
|
||||
rescue = ironic_python_agent.extensions.rescue:RescueExtension
|
||||
poll = ironic_python_agent.extensions.poll:PollExtension
|
||||
service = ironic_python_agent.extensions.service:ServiceExtension
|
||||
|
||||
ironic_python_agent.hardware_managers =
|
||||
generic = ironic_python_agent.hardware:GenericHardwareManager
|
||||
|
Loading…
Reference in New Issue
Block a user