Merge "Add get_service_steps logic to the agent"
This commit is contained in:
commit
73b76da5fe
@ -6,7 +6,8 @@ GenericHardwareManager
|
|||||||
======================
|
======================
|
||||||
|
|
||||||
This is the default hardware manager for ironic-python-agent. It provides
|
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
|
Deploy steps
|
||||||
------------
|
------------
|
||||||
@ -104,6 +105,57 @@ Clean steps
|
|||||||
and must be used through the :ironic-doc:`ironic RAID feature
|
and must be used through the :ironic-doc:`ironic RAID feature
|
||||||
<admin/raid.html>`.
|
<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
|
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
|
The per-function configuration of the first port of the NIC
|
||||||
``function1Config``
|
``function1Config``
|
||||||
The per-function configuration of the second port of the NIC
|
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):
|
def write_a_file(self, node, ports, path, contents, mode=0o644):
|
||||||
pass # Mount the disk, write a file.
|
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
|
Versioning
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
Each hardware manager has a name and a version. This version is used during
|
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)
|
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):
|
class IncompatibleNumaFormatError(RESTError):
|
||||||
"""Error raised when unexpected format data in NUMA node."""
|
"""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 []
|
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):
|
def get_version(self):
|
||||||
"""Get a name and version for this hardware manager.
|
"""Get a name and version for this hardware manager.
|
||||||
|
|
||||||
@ -1186,7 +1240,8 @@ class HardwareManager(object, metaclass=abc.ABCMeta):
|
|||||||
class GenericHardwareManager(HardwareManager):
|
class GenericHardwareManager(HardwareManager):
|
||||||
HARDWARE_MANAGER_NAME = 'generic_hardware_manager'
|
HARDWARE_MANAGER_NAME = 'generic_hardware_manager'
|
||||||
# 1.1 - Added new clean step called erase_devices_metadata
|
# 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):
|
def __init__(self):
|
||||||
self.lldp_data = {}
|
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,
|
def apply_configuration(self, node, ports, raid_config,
|
||||||
delete_existing=True):
|
delete_existing=True):
|
||||||
"""Apply RAID configuration.
|
"""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):
|
def update_nvidia_nic_firmware_image(self, node, ports, images):
|
||||||
nvidia_fw_update.update_nvidia_nic_firmware_image(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, []):
|
for step in self.hardware.get_deploy_steps(self.node, []):
|
||||||
getattr(self.hardware, step['step'])
|
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('binascii.hexlify', autospec=True)
|
||||||
@mock.patch('ironic_python_agent.netutils.get_lldp_info', autospec=True)
|
@mock.patch('ironic_python_agent.netutils.get_lldp_info', autospec=True)
|
||||||
def test_collect_lldp_data(self, mock_lldp_info, mock_hexlify):
|
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
|
log = ironic_python_agent.extensions.log:LogExtension
|
||||||
rescue = ironic_python_agent.extensions.rescue:RescueExtension
|
rescue = ironic_python_agent.extensions.rescue:RescueExtension
|
||||||
poll = ironic_python_agent.extensions.poll:PollExtension
|
poll = ironic_python_agent.extensions.poll:PollExtension
|
||||||
|
service = ironic_python_agent.extensions.service:ServiceExtension
|
||||||
|
|
||||||
ironic_python_agent.hardware_managers =
|
ironic_python_agent.hardware_managers =
|
||||||
generic = ironic_python_agent.hardware:GenericHardwareManager
|
generic = ironic_python_agent.hardware:GenericHardwareManager
|
||||||
|
Loading…
Reference in New Issue
Block a user