RedfishFirmware Interface
Change-Id: I75b2433fade0c36522024c16608d61cd663b38d5
This commit is contained in:
parent
bc1c89d993
commit
4eb0dbf7b5
@ -4,7 +4,7 @@
|
|||||||
Node Firmware (nodes)
|
Node Firmware (nodes)
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
.. versionadded:: 1.84
|
.. versionadded:: 1.86
|
||||||
|
|
||||||
Given a Node identifier (``uuid`` or ``name``), the API exposes the list of
|
Given a Node identifier (``uuid`` or ``name``), the API exposes the list of
|
||||||
all Firmware Components associated with that Node.
|
all Firmware Components associated with that Node.
|
||||||
|
@ -2018,6 +2018,6 @@ def allow_continue_inspection_endpoint():
|
|||||||
def allow_firmware_interface():
|
def allow_firmware_interface():
|
||||||
"""Check if we should support firmware interface and endpoints.
|
"""Check if we should support firmware interface and endpoints.
|
||||||
|
|
||||||
Version 1.84 of the API added support for firmware interface.
|
Version 1.86 of the API added support for firmware interface.
|
||||||
"""
|
"""
|
||||||
return api.request.version.minor >= versions.MINOR_86_FIRMWARE_INTERFACE
|
return api.request.version.minor >= versions.MINOR_86_FIRMWARE_INTERFACE
|
||||||
|
@ -85,6 +85,8 @@ def do_node_clean(task, clean_steps=None, disable_ramdisk=False):
|
|||||||
# Retrieve BIOS config settings for this node
|
# Retrieve BIOS config settings for this node
|
||||||
utils.node_cache_bios_settings(task, node)
|
utils.node_cache_bios_settings(task, node)
|
||||||
|
|
||||||
|
# Retrieve Firmware Components for this node if possible
|
||||||
|
utils.node_cache_firmware_components(task)
|
||||||
# Allow the deploy driver to set up the ramdisk again (necessary for
|
# Allow the deploy driver to set up the ramdisk again (necessary for
|
||||||
# IPA cleaning)
|
# IPA cleaning)
|
||||||
try:
|
try:
|
||||||
|
@ -31,9 +31,10 @@ CLEANING_INTERFACE_PRIORITY = {
|
|||||||
# by which interface is implementing the clean step. The clean step of the
|
# by which interface is implementing the clean step. The clean step of the
|
||||||
# interface with the highest value here, will be executed first in that
|
# interface with the highest value here, will be executed first in that
|
||||||
# case.
|
# case.
|
||||||
'vendor': 6,
|
'vendor': 7,
|
||||||
'power': 5,
|
'power': 6,
|
||||||
'management': 4,
|
'management': 5,
|
||||||
|
'firmware': 4,
|
||||||
'deploy': 3,
|
'deploy': 3,
|
||||||
'bios': 2,
|
'bios': 2,
|
||||||
'raid': 1,
|
'raid': 1,
|
||||||
@ -46,9 +47,10 @@ DEPLOYING_INTERFACE_PRIORITY = {
|
|||||||
# TODO(rloo): If we think it makes sense to have the interface priorities
|
# TODO(rloo): If we think it makes sense to have the interface priorities
|
||||||
# the same for cleaning & deploying, replace the two with one e.g.
|
# the same for cleaning & deploying, replace the two with one e.g.
|
||||||
# 'INTERFACE_PRIORITIES'.
|
# 'INTERFACE_PRIORITIES'.
|
||||||
'vendor': 6,
|
'vendor': 7,
|
||||||
'power': 5,
|
'power': 6,
|
||||||
'management': 4,
|
'management': 5,
|
||||||
|
'firmware': 4,
|
||||||
'deploy': 3,
|
'deploy': 3,
|
||||||
'bios': 2,
|
'bios': 2,
|
||||||
'raid': 1,
|
'raid': 1,
|
||||||
@ -61,11 +63,12 @@ VERIFYING_INTERFACE_PRIORITY = {
|
|||||||
# by which interface is implementing the verify step. The verifying step of
|
# by which interface is implementing the verify step. The verifying step of
|
||||||
# the interface with the highest value here, will be executed first in
|
# the interface with the highest value here, will be executed first in
|
||||||
# that case.
|
# that case.
|
||||||
'power': 12,
|
'power': 13,
|
||||||
'management': 11,
|
'management': 12,
|
||||||
'boot': 8,
|
'firmware': 11,
|
||||||
'inspect': 10,
|
'inspect': 10,
|
||||||
'deploy': 9,
|
'deploy': 9,
|
||||||
|
'boot': 8,
|
||||||
'bios': 7,
|
'bios': 7,
|
||||||
'raid': 6,
|
'raid': 6,
|
||||||
'vendor': 5,
|
'vendor': 5,
|
||||||
|
@ -1828,3 +1828,15 @@ def servicing_error_handler(task, logmsg, errmsg=None, traceback=False,
|
|||||||
|
|
||||||
if set_fail_state and node.provision_state != states.SERVICEFAIL:
|
if set_fail_state and node.provision_state != states.SERVICEFAIL:
|
||||||
task.process_event('fail')
|
task.process_event('fail')
|
||||||
|
|
||||||
|
|
||||||
|
def node_cache_firmware_components(task):
|
||||||
|
"""Do caching of firmware components if supported by driver"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
LOG.debug('Getting Firmware Components for node %s', task.node.uuid)
|
||||||
|
task.driver.firmware.validate(task)
|
||||||
|
task.driver.firmware.cache_firmware_components(task)
|
||||||
|
except exception.UnsupportedDriverExtension:
|
||||||
|
LOG.warning('Firmware Components are not supported for node %s, '
|
||||||
|
'skipping', task.node.uuid)
|
||||||
|
@ -1536,10 +1536,12 @@ def prepare_agent_boot(task):
|
|||||||
task.driver.boot.prepare_ramdisk(task, deploy_opts)
|
task.driver.boot.prepare_ramdisk(task, deploy_opts)
|
||||||
|
|
||||||
|
|
||||||
def reboot_to_finish_step(task):
|
def reboot_to_finish_step(task, timeout=None):
|
||||||
"""Reboot the node into IPA to finish a deploy/clean step.
|
"""Reboot the node into IPA to finish a deploy/clean step.
|
||||||
|
|
||||||
:param task: a TaskManager instance.
|
:param task: a TaskManager instance.
|
||||||
|
:param timeout: timeout (in seconds) positive integer (> 0) for any
|
||||||
|
power state. ``None`` indicates to use default timeout.
|
||||||
:returns: states.CLEANWAIT if cleaning operation in progress
|
:returns: states.CLEANWAIT if cleaning operation in progress
|
||||||
or states.DEPLOYWAIT if deploy operation in progress.
|
or states.DEPLOYWAIT if deploy operation in progress.
|
||||||
"""
|
"""
|
||||||
@ -1552,7 +1554,7 @@ def reboot_to_finish_step(task):
|
|||||||
manager_utils.node_power_action(task, states.POWER_OFF)
|
manager_utils.node_power_action(task, states.POWER_OFF)
|
||||||
prepare_agent_boot(task)
|
prepare_agent_boot(task)
|
||||||
|
|
||||||
manager_utils.node_power_action(task, states.REBOOT)
|
manager_utils.node_power_action(task, states.REBOOT, timeout)
|
||||||
return get_async_step_return_state(task.node)
|
return get_async_step_return_state(task.node)
|
||||||
|
|
||||||
|
|
||||||
|
@ -477,6 +477,8 @@ class FakeFirmware(base.FirmwareInterface):
|
|||||||
'needs to contain a dictionary with name/value pairs'),
|
'needs to contain a dictionary with name/value pairs'),
|
||||||
'required': True}})
|
'required': True}})
|
||||||
def update(self, task, settings):
|
def update(self, task, settings):
|
||||||
|
LOG.debug('Calling update clean step with settings %s.',
|
||||||
|
settings)
|
||||||
sleep(CONF.fake.firmware_delay)
|
sleep(CONF.fake.firmware_delay)
|
||||||
|
|
||||||
def cache_firmware_components(self, task):
|
def cache_firmware_components(self, task):
|
||||||
|
452
ironic/drivers/modules/redfish/firmware.py
Normal file
452
ironic/drivers/modules/redfish/firmware.py
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
#
|
||||||
|
# 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 urllib.parse import urlparse
|
||||||
|
|
||||||
|
from ironic_lib import metrics_utils
|
||||||
|
from oslo_log import log
|
||||||
|
from oslo_utils import importutils
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
|
||||||
|
from ironic.common import exception
|
||||||
|
from ironic.common.i18n import _
|
||||||
|
from ironic.common import states
|
||||||
|
from ironic.conductor import periodics
|
||||||
|
from ironic.conductor import utils as manager_utils
|
||||||
|
from ironic.conf import CONF
|
||||||
|
from ironic.drivers import base
|
||||||
|
from ironic.drivers.modules import deploy_utils
|
||||||
|
from ironic.drivers.modules.redfish import firmware_utils
|
||||||
|
from ironic.drivers.modules.redfish import utils as redfish_utils
|
||||||
|
from ironic import objects
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||||
|
|
||||||
|
sushy = importutils.try_import('sushy')
|
||||||
|
|
||||||
|
|
||||||
|
class RedfishFirmware(base.FirmwareInterface):
|
||||||
|
|
||||||
|
_FW_SETTINGS_ARGSINFO = {
|
||||||
|
'settings': {
|
||||||
|
'description': (
|
||||||
|
'A list of dicts with firmware components to be updated'
|
||||||
|
),
|
||||||
|
'required': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(RedfishFirmware, self).__init__()
|
||||||
|
if sushy is None:
|
||||||
|
raise exception.DriverLoadError(
|
||||||
|
driver='redfish',
|
||||||
|
reason=_("Unable to import the sushy library"))
|
||||||
|
|
||||||
|
def get_properties(self):
|
||||||
|
"""Return the properties of the interface.
|
||||||
|
|
||||||
|
:returns: dictionary of <property name>:<property description> entries.
|
||||||
|
"""
|
||||||
|
return redfish_utils.COMMON_PROPERTIES.copy()
|
||||||
|
|
||||||
|
def validate(self, task):
|
||||||
|
"""Validates the driver information needed by the redfish driver.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance containing the node to act on.
|
||||||
|
:raises: InvalidParameterValue on malformed parameter(s)
|
||||||
|
:raises: MissingParameterValue on missing parameter(s)
|
||||||
|
"""
|
||||||
|
redfish_utils.parse_driver_info(task.node)
|
||||||
|
|
||||||
|
def cache_firmware_components(self, task):
|
||||||
|
"""Store or update Firmware Components on the given node.
|
||||||
|
|
||||||
|
This method stores Firmware Components to the firmware_information
|
||||||
|
table during 'cleaning' operation. It will also update the timestamp
|
||||||
|
of each Firmware Component.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance.
|
||||||
|
:raises: UnsupportedDriverExtension, if the node's driver doesn't
|
||||||
|
support getting Firmware Components from bare metal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
node_id = task.node.id
|
||||||
|
settings = []
|
||||||
|
# NOTE(iurygregory): currently we will only retrieve BIOS and BMC
|
||||||
|
# firmware information trough the redfish system and manager.
|
||||||
|
|
||||||
|
system = redfish_utils.get_system(task.node)
|
||||||
|
|
||||||
|
bios_fw = {'component': 'bios',
|
||||||
|
'current_version': system.bios_version}
|
||||||
|
settings.append(bios_fw)
|
||||||
|
|
||||||
|
# NOTE(iurygregory): normally we only relay on the System to
|
||||||
|
# perform actions, but to retrieve the BMC Firmware we need to
|
||||||
|
# access the Manager.
|
||||||
|
try:
|
||||||
|
manager = redfish_utils.get_manager(task.node, system)
|
||||||
|
bmc_fw = {'component': 'bmc',
|
||||||
|
'current_version': manager.firmware_version}
|
||||||
|
settings.append(bmc_fw)
|
||||||
|
except exception.RedfishError:
|
||||||
|
LOG.warning('No manager available to retrieve Firmware '
|
||||||
|
'from the bmc of node %s', task.node.uuid)
|
||||||
|
|
||||||
|
if not settings:
|
||||||
|
error_msg = (_('Cannot retrieve firmware for node %s.')
|
||||||
|
% task.node.uuid)
|
||||||
|
LOG.error(error_msg)
|
||||||
|
raise exception.UnsupportedDriverExtension(error_msg)
|
||||||
|
|
||||||
|
create_list, update_list, nochange_list = (
|
||||||
|
objects.FirmwareComponentList.sync_firmware_components(
|
||||||
|
task.context, node_id, settings))
|
||||||
|
|
||||||
|
if create_list:
|
||||||
|
for new_fw in create_list:
|
||||||
|
new_fw_cmp = objects.FirmwareComponent(
|
||||||
|
task.context,
|
||||||
|
node_id=node_id,
|
||||||
|
component=new_fw['component'],
|
||||||
|
current_version=new_fw['current_version']
|
||||||
|
)
|
||||||
|
new_fw_cmp.create()
|
||||||
|
if update_list:
|
||||||
|
for up_fw in update_list:
|
||||||
|
up_fw_cmp = objects.FirmwareComponent.get(
|
||||||
|
task.context,
|
||||||
|
node_id=node_id,
|
||||||
|
name=up_fw['component']
|
||||||
|
)
|
||||||
|
up_fw_cmp.last_version_flashed = up_fw.get('current_version')
|
||||||
|
up_fw_cmp.current_version = up_fw.get('current_version')
|
||||||
|
up_fw_cmp.save()
|
||||||
|
|
||||||
|
@METRICS.timer('RedfishFirmware.update')
|
||||||
|
@base.deploy_step(priority=0, argsinfo=_FW_SETTINGS_ARGSINFO)
|
||||||
|
@base.clean_step(priority=0, abortable=False,
|
||||||
|
argsinfo=_FW_SETTINGS_ARGSINFO,
|
||||||
|
requires_ramdisk=True)
|
||||||
|
@base.cache_firmware_components
|
||||||
|
def update(self, task, settings):
|
||||||
|
"""Update the Firmware on the node using the settings for components.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance.
|
||||||
|
:param settings: a list of dictionaries, each dictionary contains the
|
||||||
|
component name and the url that will be used to update the
|
||||||
|
firmware.
|
||||||
|
:raises: UnsupportedDriverExtension, if the node's driver doesn't
|
||||||
|
support update via the interface.
|
||||||
|
:raises: InvalidParameterValue, if validation of the settings fails.
|
||||||
|
:raises: MissingParamterValue, if some required parameters are
|
||||||
|
missing.
|
||||||
|
:returns: states.CLEANWAIT if Firmware update with the settings is in
|
||||||
|
progress asynchronously of None if it is complete.
|
||||||
|
"""
|
||||||
|
node = task.node
|
||||||
|
|
||||||
|
update_service = redfish_utils.get_update_service(node)
|
||||||
|
|
||||||
|
LOG.debug('Updating Firmware on node %(node_uuid)s with settings '
|
||||||
|
'%(settings)s',
|
||||||
|
{'node_uuid': node.uuid, 'settings': settings})
|
||||||
|
|
||||||
|
self._execute_firmware_update(node, update_service, settings)
|
||||||
|
|
||||||
|
fw_upd = settings[0]
|
||||||
|
wait_interval = fw_upd.get('wait')
|
||||||
|
|
||||||
|
deploy_utils.set_async_step_flags(
|
||||||
|
node,
|
||||||
|
reboot=True,
|
||||||
|
skip_current_step=True,
|
||||||
|
polling=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return deploy_utils.reboot_to_finish_step(task, timeout=wait_interval)
|
||||||
|
|
||||||
|
def _execute_firmware_update(self, node, update_service, settings):
|
||||||
|
"""Executes the next firmware update to the node
|
||||||
|
|
||||||
|
Executes the first firmware update in the settings list to the node.
|
||||||
|
|
||||||
|
:param node: the node that will have a firmware update executed.
|
||||||
|
:param update_service: the sushy firmware update service.
|
||||||
|
:param settings: remaining settings for firmware update that needs
|
||||||
|
to be executed.
|
||||||
|
"""
|
||||||
|
fw_upd = settings[0]
|
||||||
|
component_url, cleanup = self._stage_firmware_file(node, fw_upd)
|
||||||
|
|
||||||
|
LOG.debug('Applying new firmware %(url)s for %(component)s on node '
|
||||||
|
'%(node_uuid)s',
|
||||||
|
{'url': fw_upd['url'], 'component': fw_upd['component'],
|
||||||
|
'node_uuid': node.uuid})
|
||||||
|
|
||||||
|
task_monitor = update_service.simple_update(component_url)
|
||||||
|
|
||||||
|
fw_upd['task_monitor'] = task_monitor.task_monitor_uri
|
||||||
|
node.set_driver_internal_info('redfish_fw_updates', settings)
|
||||||
|
|
||||||
|
if cleanup:
|
||||||
|
fw_clean = node.driver_internal_info.get('firmware_cleanup')
|
||||||
|
if not fw_clean:
|
||||||
|
fw_clean = [cleanup]
|
||||||
|
elif cleanup not in fw_clean:
|
||||||
|
fw_clean.append(cleanup)
|
||||||
|
node.set_driver_internal_info('firmware_cleanup', fw_clean)
|
||||||
|
|
||||||
|
def _continue_updates(self, task, update_service, settings):
|
||||||
|
"""Continues processing the firmware updates
|
||||||
|
|
||||||
|
Continues to process the firmware updates on the node.
|
||||||
|
|
||||||
|
Note that the caller must have an exclusive lock on the node.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance containing the node to act on.
|
||||||
|
:param update_service: the sushy firmware update service
|
||||||
|
:param settings: the remaining firmware updates to apply
|
||||||
|
"""
|
||||||
|
node = task.node
|
||||||
|
fw_upd = settings[0]
|
||||||
|
wait_interval = fw_upd.get('wait')
|
||||||
|
if wait_interval:
|
||||||
|
time_now = str(timeutils.utcnow().isoformat())
|
||||||
|
fw_upd['wait_start_time'] = time_now
|
||||||
|
|
||||||
|
LOG.debug('Waiting at %(time)s for %(seconds)s seconds after '
|
||||||
|
'%(component)s firmware update %(url)s '
|
||||||
|
'on node %(node)s',
|
||||||
|
{'time': time_now,
|
||||||
|
'seconds': wait_interval,
|
||||||
|
'component': fw_upd['component'],
|
||||||
|
'url': fw_upd['url'],
|
||||||
|
'node': node.uuid})
|
||||||
|
|
||||||
|
node.set_driver_internal_info('redfish_fw_updates', settings)
|
||||||
|
node.save()
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(settings) == 1:
|
||||||
|
self._clear_updates(node)
|
||||||
|
|
||||||
|
LOG.info('Firmware updates completed for node %(node)s',
|
||||||
|
{'node': node.uuid})
|
||||||
|
|
||||||
|
manager_utils.notify_conductor_resume_clean(task)
|
||||||
|
else:
|
||||||
|
settings.pop(0)
|
||||||
|
self._execute_firmware_update(node,
|
||||||
|
update_service,
|
||||||
|
settings)
|
||||||
|
node.save()
|
||||||
|
manager_utils.node_power_action(task, states.REBOOT)
|
||||||
|
|
||||||
|
def _clear_updates(self, node):
|
||||||
|
"""Clears firmware updates artifacts
|
||||||
|
|
||||||
|
Clears firmware updates from driver_internal_info and any files
|
||||||
|
that were staged.
|
||||||
|
|
||||||
|
Note that the caller must have an exclusive lock on the node.
|
||||||
|
|
||||||
|
:param node: the node to clear the firmware updates from
|
||||||
|
"""
|
||||||
|
firmware_utils.cleanup(node)
|
||||||
|
node.del_driver_internal_info('redfish_fw_updates')
|
||||||
|
node.del_driver_internal_info('firmware_cleanup')
|
||||||
|
node.save()
|
||||||
|
|
||||||
|
@METRICS.timer('RedfishFirmware._query_update_failed')
|
||||||
|
@periodics.node_periodic(
|
||||||
|
purpose='checking if async update of firmware component failed',
|
||||||
|
spacing=CONF.redfish.firmware_update_fail_interval,
|
||||||
|
filters={'reserved': False, 'provision_state': states.CLEANFAIL,
|
||||||
|
'maintenance': True},
|
||||||
|
predicate_extra_fields=['driver_internal_info'],
|
||||||
|
predicate=lambda n: n.driver_internal_info.get('redfish_fw_updates'),
|
||||||
|
)
|
||||||
|
def _query_update_failed(self, task, manager, context):
|
||||||
|
|
||||||
|
"""Periodic job to check for failed firmware updates."""
|
||||||
|
# A firmware update failed. Discard any remaining firmware
|
||||||
|
# updates so when the user takes the node out of
|
||||||
|
# maintenance mode, pending firmware updates do not
|
||||||
|
# automatically continue.
|
||||||
|
LOG.error('Update firmware failed for node %(node)s. '
|
||||||
|
'Discarding remaining firmware updates.',
|
||||||
|
{'node': task.node.uuid})
|
||||||
|
|
||||||
|
task.upgrade_lock()
|
||||||
|
self._clear_updates(task.node)
|
||||||
|
|
||||||
|
@METRICS.timer('RedfishFirmware._query_update_status')
|
||||||
|
@periodics.node_periodic(
|
||||||
|
purpose='checking async update of firmware component',
|
||||||
|
spacing=CONF.redfish.firmware_update_fail_interval,
|
||||||
|
filters={'reserved': False, 'provision_state': states.CLEANWAIT},
|
||||||
|
predicate_extra_fields=['driver_internal_info'],
|
||||||
|
predicate=lambda n: n.driver_internal_info.get('redfish_fw_updates'),
|
||||||
|
)
|
||||||
|
def _query_update_status(self, task, manager, context):
|
||||||
|
"""Periodic job to check firmware update tasks."""
|
||||||
|
self._check_node_redfish_firmware_update(task)
|
||||||
|
|
||||||
|
@METRICS.timer('RedfishFirmware._check_node_redfish_firmware_update')
|
||||||
|
def _check_node_redfish_firmware_update(self, task):
|
||||||
|
"""Check the progress of running firmware update on a node."""
|
||||||
|
|
||||||
|
node = task.node
|
||||||
|
|
||||||
|
settings = node.driver_internal_info['redfish_fw_updates']
|
||||||
|
current_update = settings[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
update_service = redfish_utils.get_update_service(node)
|
||||||
|
except exception.RedfishConnectionError as e:
|
||||||
|
# If the BMC firmware is being updated, the BMC will be
|
||||||
|
# unavailable for some amount of time.
|
||||||
|
LOG.warning('Unable to communicate with firmware update service '
|
||||||
|
'on node %(node)s. Will try again on the next poll. '
|
||||||
|
'Error: %(error)s',
|
||||||
|
{'node': node.uuid,
|
||||||
|
'error': e})
|
||||||
|
return
|
||||||
|
|
||||||
|
wait_start_time = current_update.get('wait_start_time')
|
||||||
|
if wait_start_time:
|
||||||
|
wait_start = timeutils.parse_isotime(wait_start_time)
|
||||||
|
|
||||||
|
elapsed_time = timeutils.utcnow(True) - wait_start
|
||||||
|
if elapsed_time.seconds >= current_update['wait']:
|
||||||
|
LOG.debug('Finished waiting after firmware update '
|
||||||
|
'%(firmware_image)s on node %(node)s. '
|
||||||
|
'Elapsed time: %(seconds)s seconds',
|
||||||
|
{'firmware_image': current_update['url'],
|
||||||
|
'node': node.uuid,
|
||||||
|
'seconds': elapsed_time.seconds})
|
||||||
|
current_update.pop('wait', None)
|
||||||
|
current_update.pop('wait_start_time', None)
|
||||||
|
|
||||||
|
self._continue_updates(task, update_service, settings)
|
||||||
|
else:
|
||||||
|
LOG.debug('Continuing to wait after firmware update '
|
||||||
|
'%(firmware_image)s on node %(node)s. '
|
||||||
|
'Elapsed time: %(seconds)s seconds',
|
||||||
|
{'firmware_image': current_update['url'],
|
||||||
|
'node': node.uuid,
|
||||||
|
'seconds': elapsed_time.seconds})
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
task_monitor = redfish_utils.get_task_monitor(
|
||||||
|
node, current_update['task_monitor'])
|
||||||
|
except exception.RedfishError:
|
||||||
|
# The BMC deleted the Task before we could query it
|
||||||
|
LOG.warning('Firmware update completed for node %(node)s, '
|
||||||
|
'firmware %(firmware_image)s, but success of the '
|
||||||
|
'update is unknown. Assuming update was successful.',
|
||||||
|
{'node': node.uuid,
|
||||||
|
'firmware_image': current_update['url']})
|
||||||
|
self._continue_updates(task, update_service, settings)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not task_monitor.is_processing:
|
||||||
|
# The last response does not necessarily contain a Task,
|
||||||
|
# so get it
|
||||||
|
sushy_task = task_monitor.get_task()
|
||||||
|
|
||||||
|
# Only parse the messages if the BMC did not return parsed
|
||||||
|
# messages
|
||||||
|
messages = []
|
||||||
|
if sushy_task.messages and not sushy_task.messages[0].message:
|
||||||
|
sushy_task.parse_messages()
|
||||||
|
|
||||||
|
if sushy_task.messages is not None:
|
||||||
|
messages = [m.message for m in sushy_task.messages]
|
||||||
|
|
||||||
|
task.upgrade_lock()
|
||||||
|
if (sushy_task.task_state == sushy.TASK_STATE_COMPLETED
|
||||||
|
and sushy_task.task_status in
|
||||||
|
[sushy.HEALTH_OK, sushy.HEALTH_WARNING]):
|
||||||
|
LOG.info('Firmware update succeeded for node %(node)s, '
|
||||||
|
'firmware %(firmware_image)s: %(messages)s',
|
||||||
|
{'node': node.uuid,
|
||||||
|
'firmware_image': current_update['url'],
|
||||||
|
'messages': ", ".join(messages)})
|
||||||
|
|
||||||
|
self._continue_updates(task, update_service, settings)
|
||||||
|
else:
|
||||||
|
error_msg = (_('Firmware update failed for node %(node)s, '
|
||||||
|
'firmware %(firmware_image)s. '
|
||||||
|
'Error: %(errors)s') %
|
||||||
|
{'node': node.uuid,
|
||||||
|
'firmware_image': current_update['url'],
|
||||||
|
'errors': ", ".join(messages)})
|
||||||
|
|
||||||
|
self._clear_updates(node)
|
||||||
|
if task.node.clean_step:
|
||||||
|
manager_utils.cleaning_error_handler(task, error_msg)
|
||||||
|
else:
|
||||||
|
manager_utils.deploying_error_handler(task, error_msg)
|
||||||
|
|
||||||
|
else:
|
||||||
|
LOG.debug('Firmware update in progress for node %(node)s, '
|
||||||
|
'firmware %(firmware_image)s.',
|
||||||
|
{'node': node.uuid,
|
||||||
|
'firmware_image': current_update['url']})
|
||||||
|
|
||||||
|
def _stage_firmware_file(self, node, component_update):
|
||||||
|
|
||||||
|
try:
|
||||||
|
url = component_update['url']
|
||||||
|
name = component_update['component']
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
scheme = parsed_url.scheme.lower()
|
||||||
|
source = (CONF.redfish.firmware_source).lower()
|
||||||
|
|
||||||
|
# Keep it simple, in further processing TLS does not matter
|
||||||
|
if scheme == 'https':
|
||||||
|
scheme = 'http'
|
||||||
|
|
||||||
|
# If source and scheme is HTTP, then no staging,
|
||||||
|
# returning original location
|
||||||
|
if scheme == 'http' and source == scheme:
|
||||||
|
LOG.debug('For node %(node)s serving firmware for '
|
||||||
|
'%(component)s from original location %(url)s',
|
||||||
|
{'node': node.uuid, 'component': name, 'url': url})
|
||||||
|
return url, None
|
||||||
|
|
||||||
|
# If source and scheme is Swift, then not moving, but
|
||||||
|
# returning Swift temp URL
|
||||||
|
if scheme == 'swift' and source == scheme:
|
||||||
|
temp_url = firmware_utils.get_swift_temp_url(parsed_url)
|
||||||
|
LOG.debug('For node %(node)s serving original firmware at '
|
||||||
|
'for %(component)s at %(url)s via Swift temporary '
|
||||||
|
'url %(temp_url)s',
|
||||||
|
{'node': node.uuid, 'component': name, 'url': url,
|
||||||
|
'temp_url': temp_url})
|
||||||
|
return temp_url, None
|
||||||
|
|
||||||
|
# For remaining, download the image to temporary location
|
||||||
|
temp_file = firmware_utils.download_to_temp(node, url)
|
||||||
|
|
||||||
|
return firmware_utils.stage(node, source, temp_file)
|
||||||
|
|
||||||
|
except exception.IronicException:
|
||||||
|
firmware_utils.cleanup(node)
|
@ -63,6 +63,36 @@ _UPDATE_FIRMWARE_SCHEMA = {
|
|||||||
"additionalProperties": False
|
"additionalProperties": False
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_FIRMWARE_INTERFACE_UPDATE_SCHEMA = {
|
||||||
|
"$schema": "http://json-schema.org/schema#",
|
||||||
|
"title": "update_firmware clean step schema",
|
||||||
|
"type": "array",
|
||||||
|
# list of firmware update images
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["component", "url"],
|
||||||
|
"properties": {
|
||||||
|
"component": {
|
||||||
|
"description": "name of the firmware component to be updated",
|
||||||
|
"type": "string",
|
||||||
|
"minLenght": 1
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"description": "URL for firmware file",
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"wait": {
|
||||||
|
"description": "optional wait time for firmware update",
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_FIRMWARE_SUBDIR = 'firmware'
|
_FIRMWARE_SUBDIR = 'firmware'
|
||||||
|
|
||||||
|
|
||||||
@ -80,6 +110,20 @@ def validate_update_firmware_args(firmware_images):
|
|||||||
% {'firmware_images': firmware_images, 'err': err})
|
% {'firmware_images': firmware_images, 'err': err})
|
||||||
|
|
||||||
|
|
||||||
|
def validate_firmware_interface_update_args(settings):
|
||||||
|
"""Validate ``update`` step input argument
|
||||||
|
|
||||||
|
:param settings: args to validate.
|
||||||
|
:raises: InvalidParameterValue When argument is not valid
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
jsonschema.validate(settings, _FIRMWARE_INTERFACE_UPDATE_SCHEMA)
|
||||||
|
except jsonschema.ValidationError as err:
|
||||||
|
raise exception.InvalidParameterValue(
|
||||||
|
_('Invalid firmware update %(settings)s. Errors: %(err)s')
|
||||||
|
% {'settings': settings, 'err': err})
|
||||||
|
|
||||||
|
|
||||||
def get_swift_temp_url(parsed_url):
|
def get_swift_temp_url(parsed_url):
|
||||||
"""Gets Swift temporary URL
|
"""Gets Swift temporary URL
|
||||||
|
|
||||||
|
@ -475,3 +475,39 @@ def wait_until_get_system_ready(node):
|
|||||||
driver_info = parse_driver_info(node)
|
driver_info = parse_driver_info(node)
|
||||||
system_id = driver_info['system_id']
|
system_id = driver_info['system_id']
|
||||||
return _get_system(driver_info, system_id)
|
return _get_system(driver_info, system_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_manager(node, system, manager_id=None):
|
||||||
|
"""Get a node's manager.
|
||||||
|
|
||||||
|
:param system: a Sushy system object
|
||||||
|
:param manager_id: the id of the manager
|
||||||
|
:return: a sushy Manager
|
||||||
|
:raises: RedfishError when the System doesn't have Managers associated
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
sushy_manager = None
|
||||||
|
available_managers = system.managers
|
||||||
|
if available_managers:
|
||||||
|
if manager_id is None:
|
||||||
|
sushy_manager = available_managers[0]
|
||||||
|
else:
|
||||||
|
for manager in available_managers:
|
||||||
|
if manager.identity == manager_id:
|
||||||
|
sushy_manager = manager
|
||||||
|
if sushy_manager is None:
|
||||||
|
raise Exception("Couldn't find any Sushy Manager")
|
||||||
|
return sushy_manager
|
||||||
|
except sushy.exceptions.MissingAttributeError as e:
|
||||||
|
LOG.error('Redfish Managers for node %(node)s are not associated '
|
||||||
|
'with system %(system)s. Error %(error)s',
|
||||||
|
{'system': system.identity,
|
||||||
|
'node': node.uuid, 'error': e})
|
||||||
|
raise exception.RedfishError(error=e)
|
||||||
|
except Exception as exc:
|
||||||
|
LOG.error('Redfish Manager was not found for '
|
||||||
|
'node %(node)s under system %(system)s. Error %(error)s',
|
||||||
|
{'system': system.identity,
|
||||||
|
'node': node.uuid, 'error': exc})
|
||||||
|
raise exception.RedfishError(error=exc)
|
||||||
|
@ -21,6 +21,7 @@ from ironic.drivers.modules import noop_mgmt
|
|||||||
from ironic.drivers.modules import pxe
|
from ironic.drivers.modules import pxe
|
||||||
from ironic.drivers.modules.redfish import bios as redfish_bios
|
from ironic.drivers.modules.redfish import bios as redfish_bios
|
||||||
from ironic.drivers.modules.redfish import boot as redfish_boot
|
from ironic.drivers.modules.redfish import boot as redfish_boot
|
||||||
|
from ironic.drivers.modules.redfish import firmware as redfish_firmware
|
||||||
from ironic.drivers.modules.redfish import inspect as redfish_inspect
|
from ironic.drivers.modules.redfish import inspect as redfish_inspect
|
||||||
from ironic.drivers.modules.redfish import management as redfish_mgmt
|
from ironic.drivers.modules.redfish import management as redfish_mgmt
|
||||||
from ironic.drivers.modules.redfish import power as redfish_power
|
from ironic.drivers.modules.redfish import power as redfish_power
|
||||||
@ -69,3 +70,7 @@ class RedfishHardware(generic.GenericHardware):
|
|||||||
def supported_raid_interfaces(self):
|
def supported_raid_interfaces(self):
|
||||||
"""List of supported raid interfaces."""
|
"""List of supported raid interfaces."""
|
||||||
return [redfish_raid.RedfishRAID, noop.NoRAID, agent.AgentRAID]
|
return [redfish_raid.RedfishRAID, noop.NoRAID, agent.AgentRAID]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_firmware_interfaces(self):
|
||||||
|
return [redfish_firmware.RedfishFirmware, noop.NoFirmware]
|
||||||
|
@ -145,11 +145,15 @@ class FirmwareComponentList(base.IronicObjectListBase, base.IronicObject):
|
|||||||
for cmp in components:
|
for cmp in components:
|
||||||
if cmp['component'] in current_components_dict:
|
if cmp['component'] in current_components_dict:
|
||||||
values = current_components_dict[cmp['component']]
|
values = current_components_dict[cmp['component']]
|
||||||
|
if values.get('last_version_flashed') is None:
|
||||||
cv_changed = cmp['current_version'] \
|
lvf_changed = False
|
||||||
!= values.get('current_version')
|
cv_changed = cmp['current_version'] \
|
||||||
lvf_changed = cmp['last_version_flashed'] \
|
!= values.get('current_version')
|
||||||
!= values.get('last_version_flashed')
|
else:
|
||||||
|
lvf_changed = cmp['current_version'] \
|
||||||
|
!= values.get('last_version_flashed')
|
||||||
|
cv_changed = cmp['current_version'] \
|
||||||
|
!= values.get('current_version')
|
||||||
|
|
||||||
if cv_changed or lvf_changed:
|
if cv_changed or lvf_changed:
|
||||||
update_list.append(cmp)
|
update_list.append(cmp)
|
||||||
|
@ -585,6 +585,11 @@ class NodeCleaningStepsTestCase(db_base.DbTestCase):
|
|||||||
'abortable': False, 'argsinfo': None, 'interface': 'vendor',
|
'abortable': False, 'argsinfo': None, 'interface': 'vendor',
|
||||||
'priority': 1, 'requires_ramdisk': True,
|
'priority': 1, 'requires_ramdisk': True,
|
||||||
'step': 'log_passthrough'}
|
'step': 'log_passthrough'}
|
||||||
|
self.firmware_step = {
|
||||||
|
'abortable': False, 'argsinfo': {}, 'interface': 'firmware',
|
||||||
|
'priority': 0, 'requires_ramdisk': True,
|
||||||
|
'step': 'update'
|
||||||
|
}
|
||||||
|
|
||||||
# Automated cleaning should be executed in this order
|
# Automated cleaning should be executed in this order
|
||||||
self.clean_steps = [self.deploy_erase, self.power_update,
|
self.clean_steps = [self.deploy_erase, self.power_update,
|
||||||
@ -595,6 +600,8 @@ class NodeCleaningStepsTestCase(db_base.DbTestCase):
|
|||||||
'argsinfo': {'arg1': {'description': 'desc1', 'required': True},
|
'argsinfo': {'arg1': {'description': 'desc1', 'required': True},
|
||||||
'arg2': {'description': 'desc2'}}}
|
'arg2': {'description': 'desc2'}}}
|
||||||
|
|
||||||
|
@mock.patch('ironic.drivers.modules.fake.FakeFirmware.get_clean_steps',
|
||||||
|
lambda self, taks: [])
|
||||||
@mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',
|
@mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',
|
||||||
lambda self, task: [])
|
lambda self, task: [])
|
||||||
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_clean_steps',
|
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_clean_steps',
|
||||||
@ -619,6 +626,8 @@ class NodeCleaningStepsTestCase(db_base.DbTestCase):
|
|||||||
|
|
||||||
self.assertEqual(self.clean_steps, steps)
|
self.assertEqual(self.clean_steps, steps)
|
||||||
|
|
||||||
|
@mock.patch('ironic.drivers.modules.fake.FakeFirmware.get_clean_steps',
|
||||||
|
lambda self, task: [])
|
||||||
@mock.patch('ironic.drivers.modules.fake.FakeVendorB.get_clean_steps',
|
@mock.patch('ironic.drivers.modules.fake.FakeVendorB.get_clean_steps',
|
||||||
lambda self, task: [])
|
lambda self, task: [])
|
||||||
@mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',
|
@mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',
|
||||||
|
@ -1681,7 +1681,7 @@ class Ilo5ManagementTestCase(db_base.DbTestCase):
|
|||||||
ilo_mock_object.do_disk_erase.assert_called_once_with(
|
ilo_mock_object.do_disk_erase.assert_called_once_with(
|
||||||
'HDD', 'overwrite')
|
'HDD', 'overwrite')
|
||||||
self.assertEqual(states.CLEANWAIT, result)
|
self.assertEqual(states.CLEANWAIT, result)
|
||||||
mock_power.assert_called_once_with(task, states.REBOOT)
|
mock_power.assert_called_once_with(task, states.REBOOT, None)
|
||||||
|
|
||||||
@mock.patch.object(deploy_utils, 'build_agent_options',
|
@mock.patch.object(deploy_utils, 'build_agent_options',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@ -1712,7 +1712,7 @@ class Ilo5ManagementTestCase(db_base.DbTestCase):
|
|||||||
ilo_mock_object.do_disk_erase.assert_called_once_with(
|
ilo_mock_object.do_disk_erase.assert_called_once_with(
|
||||||
'SSD', 'block')
|
'SSD', 'block')
|
||||||
self.assertEqual(states.CLEANWAIT, result)
|
self.assertEqual(states.CLEANWAIT, result)
|
||||||
mock_power.assert_called_once_with(task, states.REBOOT)
|
mock_power.assert_called_once_with(task, states.REBOOT, None)
|
||||||
|
|
||||||
@mock.patch.object(deploy_utils, 'build_agent_options',
|
@mock.patch.object(deploy_utils, 'build_agent_options',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@ -1746,7 +1746,7 @@ class Ilo5ManagementTestCase(db_base.DbTestCase):
|
|||||||
ilo_mock_object.do_disk_erase.assert_called_once_with(
|
ilo_mock_object.do_disk_erase.assert_called_once_with(
|
||||||
'SSD', 'block')
|
'SSD', 'block')
|
||||||
self.assertEqual(states.CLEANWAIT, result)
|
self.assertEqual(states.CLEANWAIT, result)
|
||||||
mock_power.assert_called_once_with(task, states.REBOOT)
|
mock_power.assert_called_once_with(task, states.REBOOT, None)
|
||||||
|
|
||||||
@mock.patch.object(ilo_management.LOG, 'info', autospec=True)
|
@mock.patch.object(ilo_management.LOG, 'info', autospec=True)
|
||||||
@mock.patch.object(ilo_management.Ilo5Management,
|
@mock.patch.object(ilo_management.Ilo5Management,
|
||||||
@ -1802,7 +1802,7 @@ class Ilo5ManagementTestCase(db_base.DbTestCase):
|
|||||||
ilo_mock_object.do_disk_erase.assert_called_once_with(
|
ilo_mock_object.do_disk_erase.assert_called_once_with(
|
||||||
'HDD', 'zero')
|
'HDD', 'zero')
|
||||||
self.assertEqual(states.CLEANWAIT, result)
|
self.assertEqual(states.CLEANWAIT, result)
|
||||||
mock_power.assert_called_once_with(task, states.REBOOT)
|
mock_power.assert_called_once_with(task, states.REBOOT, None)
|
||||||
|
|
||||||
@mock.patch.object(ilo_management.LOG, 'info', autospec=True)
|
@mock.patch.object(ilo_management.LOG, 'info', autospec=True)
|
||||||
@mock.patch.object(ilo_common, 'get_ilo_object', autospec=True)
|
@mock.patch.object(ilo_common, 'get_ilo_object', autospec=True)
|
||||||
|
@ -84,7 +84,7 @@ class Ilo5RAIDTestCase(db_base.DbTestCase):
|
|||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
task.node.driver_internal_info.get(
|
task.node.driver_internal_info.get(
|
||||||
'skip_current_deploy_step'))
|
'skip_current_deploy_step'))
|
||||||
mock_reboot.assert_called_once_with(task, states.REBOOT)
|
mock_reboot.assert_called_once_with(task, states.REBOOT, None)
|
||||||
|
|
||||||
def test__prepare_for_read_raid_create_raid_cleaning(self):
|
def test__prepare_for_read_raid_create_raid_cleaning(self):
|
||||||
self.node.clean_step = {'step': 'create_configuration',
|
self.node.clean_step = {'step': 'create_configuration',
|
||||||
@ -122,7 +122,7 @@ class Ilo5RAIDTestCase(db_base.DbTestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
task.node.driver_internal_info.get(
|
task.node.driver_internal_info.get(
|
||||||
'skip_current_deploy_step'), False)
|
'skip_current_deploy_step'), False)
|
||||||
mock_reboot.assert_called_once_with(task, states.REBOOT)
|
mock_reboot.assert_called_once_with(task, states.REBOOT, None)
|
||||||
|
|
||||||
def test__prepare_for_read_raid_delete_raid_cleaning(self):
|
def test__prepare_for_read_raid_delete_raid_cleaning(self):
|
||||||
self.node.clean_step = {'step': 'create_configuration',
|
self.node.clean_step = {'step': 'create_configuration',
|
||||||
|
@ -203,10 +203,11 @@ class RedfishBiosTestCase(db_base.DbTestCase):
|
|||||||
if fast_track:
|
if fast_track:
|
||||||
mock_power_action.assert_has_calls([
|
mock_power_action.assert_has_calls([
|
||||||
mock.call(task, states.POWER_OFF),
|
mock.call(task, states.POWER_OFF),
|
||||||
mock.call(task, states.REBOOT),
|
mock.call(task, states.REBOOT, None),
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
mock_power_action.assert_called_once_with(task, states.REBOOT)
|
mock_power_action.assert_called_once_with(
|
||||||
|
task, states.REBOOT, None)
|
||||||
if step == 'factory_reset':
|
if step == 'factory_reset':
|
||||||
bios.reset_bios.assert_called_once()
|
bios.reset_bios.assert_called_once()
|
||||||
if step == 'apply_configuration':
|
if step == 'apply_configuration':
|
||||||
|
40
ironic/tests/unit/drivers/modules/redfish/test_firmware.py
Normal file
40
ironic/tests/unit/drivers/modules/redfish/test_firmware.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#
|
||||||
|
# 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 oslo_utils import importutils
|
||||||
|
|
||||||
|
from ironic.tests.unit.db import base as db_base
|
||||||
|
from ironic.tests.unit.db import utils as db_utils
|
||||||
|
from ironic.tests.unit.objects import utils as obj_utils
|
||||||
|
|
||||||
|
sushy = importutils.try_import('sushy')
|
||||||
|
|
||||||
|
INFO_DICT = db_utils.get_test_redfish_info()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('oslo_utils.eventletutils.EventletEvent.wait',
|
||||||
|
lambda *args, **kwargs: None)
|
||||||
|
class RedfishFirmwareTestCase(db_base.DbTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(RedfishFirmwareTestCase, self).setUp()
|
||||||
|
self.config(enabled_bios_interfaces=['redfish'],
|
||||||
|
enabled_hardware_types=['redfish'],
|
||||||
|
enabled_power_interfaces=['redfish'],
|
||||||
|
enabled_boot_interfaces=['redfish-virtual-media'],
|
||||||
|
enabled_management_interfaces=['redfish'],
|
||||||
|
enabled_firmware_interfaces=['redfish'])
|
||||||
|
self.node = obj_utils.create_test_node(
|
||||||
|
self.context, driver='redfish', driver_info=INFO_DICT)
|
@ -865,7 +865,8 @@ class RedfishManagementTestCase(db_base.DbTestCase):
|
|||||||
task.node, reboot=True, skip_current_step=True, polling=True)
|
task.node, reboot=True, skip_current_step=True, polling=True)
|
||||||
mock_get_async_step_return_state.assert_called_once_with(
|
mock_get_async_step_return_state.assert_called_once_with(
|
||||||
task.node)
|
task.node)
|
||||||
mock_node_power_action.assert_called_once_with(task, states.REBOOT)
|
mock_node_power_action.assert_called_once_with(
|
||||||
|
task, states.REBOOT, None)
|
||||||
|
|
||||||
@mock.patch.object(redfish_mgmt.RedfishManagement, '_stage_firmware_file',
|
@mock.patch.object(redfish_mgmt.RedfishManagement, '_stage_firmware_file',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@ -919,7 +920,8 @@ class RedfishManagementTestCase(db_base.DbTestCase):
|
|||||||
task.node, reboot=True, skip_current_step=True, polling=True)
|
task.node, reboot=True, skip_current_step=True, polling=True)
|
||||||
mock_get_async_step_return_state.assert_called_once_with(
|
mock_get_async_step_return_state.assert_called_once_with(
|
||||||
task.node)
|
task.node)
|
||||||
mock_node_power_action.assert_called_once_with(task, states.REBOOT)
|
mock_node_power_action.assert_called_once_with(
|
||||||
|
task, states.REBOOT, None)
|
||||||
|
|
||||||
@mock.patch.object(redfish_mgmt.RedfishManagement, '_stage_firmware_file',
|
@mock.patch.object(redfish_mgmt.RedfishManagement, '_stage_firmware_file',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
@ -979,7 +981,8 @@ class RedfishManagementTestCase(db_base.DbTestCase):
|
|||||||
task.node, reboot=True, skip_current_step=True, polling=True)
|
task.node, reboot=True, skip_current_step=True, polling=True)
|
||||||
mock_get_async_step_return_state.assert_called_once_with(
|
mock_get_async_step_return_state.assert_called_once_with(
|
||||||
task.node)
|
task.node)
|
||||||
mock_node_power_action.assert_called_once_with(task, states.REBOOT)
|
mock_node_power_action.assert_called_once_with(
|
||||||
|
task, states.REBOOT, None)
|
||||||
|
|
||||||
def test_update_firmware_invalid_args(self):
|
def test_update_firmware_invalid_args(self):
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
with task_manager.acquire(self.context, self.node.uuid,
|
||||||
|
@ -406,7 +406,8 @@ class RedfishRAIDTestCase(db_base.DbTestCase):
|
|||||||
task.node, reboot=True, skip_current_step=True, polling=True)
|
task.node, reboot=True, skip_current_step=True, polling=True)
|
||||||
mock_get_async_step_return_state.assert_called_once_with(
|
mock_get_async_step_return_state.assert_called_once_with(
|
||||||
task.node)
|
task.node)
|
||||||
mock_node_power_action.assert_called_once_with(task, states.REBOOT)
|
mock_node_power_action.assert_called_once_with(
|
||||||
|
task, states.REBOOT, None)
|
||||||
mock_build_agent_options.assert_called_once_with(task.node)
|
mock_build_agent_options.assert_called_once_with(task.node)
|
||||||
self.assertEqual(mock_prepare_ramdisk.call_count, 1)
|
self.assertEqual(mock_prepare_ramdisk.call_count, 1)
|
||||||
# Async operation, raid_config shouldn't be updated yet
|
# Async operation, raid_config shouldn't be updated yet
|
||||||
@ -1123,7 +1124,8 @@ class RedfishRAIDTestCase(db_base.DbTestCase):
|
|||||||
task.node, reboot=True, skip_current_step=True, polling=True)
|
task.node, reboot=True, skip_current_step=True, polling=True)
|
||||||
mock_get_async_step_return_state.assert_called_once_with(
|
mock_get_async_step_return_state.assert_called_once_with(
|
||||||
task.node)
|
task.node)
|
||||||
mock_node_power_action.assert_called_once_with(task, states.REBOOT)
|
mock_node_power_action.assert_called_once_with(
|
||||||
|
task, states.REBOOT, None)
|
||||||
mock_build_agent_options.assert_called_once_with(task.node)
|
mock_build_agent_options.assert_called_once_with(task.node)
|
||||||
self.assertEqual(mock_prepare_ramdisk.call_count, 1)
|
self.assertEqual(mock_prepare_ramdisk.call_count, 1)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -1593,7 +1593,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
|
|||||||
agent_base._post_step_reboot(task, 'clean')
|
agent_base._post_step_reboot(task, 'clean')
|
||||||
self.assertTrue(mock_build_opt.called)
|
self.assertTrue(mock_build_opt.called)
|
||||||
self.assertTrue(mock_prepare.called)
|
self.assertTrue(mock_prepare.called)
|
||||||
mock_reboot.assert_called_once_with(task, states.REBOOT)
|
mock_reboot.assert_called_once_with(task, states.REBOOT, None)
|
||||||
self.assertTrue(task.node.driver_internal_info['cleaning_reboot'])
|
self.assertTrue(task.node.driver_internal_info['cleaning_reboot'])
|
||||||
self.assertNotIn('agent_secret_token',
|
self.assertNotIn('agent_secret_token',
|
||||||
task.node.driver_internal_info)
|
task.node.driver_internal_info)
|
||||||
@ -1612,7 +1612,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
|
|||||||
agent_base._post_step_reboot(task, 'deploy')
|
agent_base._post_step_reboot(task, 'deploy')
|
||||||
self.assertTrue(mock_build_opt.called)
|
self.assertTrue(mock_build_opt.called)
|
||||||
self.assertTrue(mock_prepare.called)
|
self.assertTrue(mock_prepare.called)
|
||||||
mock_reboot.assert_called_once_with(task, states.REBOOT)
|
mock_reboot.assert_called_once_with(task, states.REBOOT, None)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
task.node.driver_internal_info['deployment_reboot'])
|
task.node.driver_internal_info['deployment_reboot'])
|
||||||
self.assertNotIn('agent_secret_token',
|
self.assertNotIn('agent_secret_token',
|
||||||
@ -1633,7 +1633,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
|
|||||||
agent_base._post_step_reboot(task, 'clean')
|
agent_base._post_step_reboot(task, 'clean')
|
||||||
self.assertTrue(mock_build_opt.called)
|
self.assertTrue(mock_build_opt.called)
|
||||||
self.assertTrue(mock_prepare.called)
|
self.assertTrue(mock_prepare.called)
|
||||||
mock_reboot.assert_called_once_with(task, states.REBOOT)
|
mock_reboot.assert_called_once_with(task, states.REBOOT, None)
|
||||||
self.assertIn('agent_secret_token',
|
self.assertIn('agent_secret_token',
|
||||||
task.node.driver_internal_info)
|
task.node.driver_internal_info)
|
||||||
|
|
||||||
@ -1649,7 +1649,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
|
|||||||
with task_manager.acquire(self.context, self.node['uuid'],
|
with task_manager.acquire(self.context, self.node['uuid'],
|
||||||
shared=False) as task:
|
shared=False) as task:
|
||||||
agent_base._post_step_reboot(task, 'clean')
|
agent_base._post_step_reboot(task, 'clean')
|
||||||
mock_reboot.assert_called_once_with(task, states.REBOOT)
|
mock_reboot.assert_called_once_with(task, states.REBOOT, None)
|
||||||
mock_handler.assert_called_once_with(task, mock.ANY,
|
mock_handler.assert_called_once_with(task, mock.ANY,
|
||||||
traceback=True)
|
traceback=True)
|
||||||
self.assertNotIn('cleaning_reboot',
|
self.assertNotIn('cleaning_reboot',
|
||||||
@ -1667,7 +1667,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
|
|||||||
with task_manager.acquire(self.context, self.node['uuid'],
|
with task_manager.acquire(self.context, self.node['uuid'],
|
||||||
shared=False) as task:
|
shared=False) as task:
|
||||||
agent_base._post_step_reboot(task, 'deploy')
|
agent_base._post_step_reboot(task, 'deploy')
|
||||||
mock_reboot.assert_called_once_with(task, states.REBOOT)
|
mock_reboot.assert_called_once_with(task, states.REBOOT, None)
|
||||||
mock_handler.assert_called_once_with(task, mock.ANY,
|
mock_handler.assert_called_once_with(task, mock.ANY,
|
||||||
traceback=True)
|
traceback=True)
|
||||||
self.assertNotIn('deployment_reboot',
|
self.assertNotIn('deployment_reboot',
|
||||||
@ -1686,7 +1686,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
|
|||||||
with task_manager.acquire(self.context, self.node['uuid'],
|
with task_manager.acquire(self.context, self.node['uuid'],
|
||||||
shared=False) as task:
|
shared=False) as task:
|
||||||
agent_base._post_step_reboot(task, 'service')
|
agent_base._post_step_reboot(task, 'service')
|
||||||
mock_reboot.assert_called_once_with(task, states.REBOOT)
|
mock_reboot.assert_called_once_with(task, states.REBOOT, None)
|
||||||
mock_handler.assert_called_once_with(task, mock.ANY,
|
mock_handler.assert_called_once_with(task, mock.ANY,
|
||||||
traceback=True)
|
traceback=True)
|
||||||
self.assertNotIn('servicing_reboot',
|
self.assertNotIn('servicing_reboot',
|
||||||
@ -1829,7 +1829,7 @@ class ContinueCleaningTest(AgentDeployMixinBaseTest):
|
|||||||
with task_manager.acquire(self.context, self.node['uuid'],
|
with task_manager.acquire(self.context, self.node['uuid'],
|
||||||
shared=False) as task:
|
shared=False) as task:
|
||||||
self.deploy.continue_cleaning(task)
|
self.deploy.continue_cleaning(task)
|
||||||
reboot_mock.assert_called_once_with(task, states.REBOOT)
|
reboot_mock.assert_called_once_with(task, states.REBOOT, None)
|
||||||
|
|
||||||
@mock.patch.object(cleaning, 'continue_node_clean', autospec=True)
|
@mock.patch.object(cleaning, 'continue_node_clean', autospec=True)
|
||||||
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
|
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
|
||||||
@ -2147,7 +2147,7 @@ class ContinueServiceTest(AgentDeployMixinBaseTest):
|
|||||||
with task_manager.acquire(self.context, self.node['uuid'],
|
with task_manager.acquire(self.context, self.node['uuid'],
|
||||||
shared=False) as task:
|
shared=False) as task:
|
||||||
self.deploy.continue_servicing(task)
|
self.deploy.continue_servicing(task)
|
||||||
reboot_mock.assert_called_once_with(task, states.REBOOT)
|
reboot_mock.assert_called_once_with(task, states.REBOOT, None)
|
||||||
|
|
||||||
@mock.patch.object(servicing, 'continue_node_service', autospec=True)
|
@mock.patch.object(servicing, 'continue_node_service', autospec=True)
|
||||||
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
|
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
|
||||||
|
@ -33,7 +33,8 @@ class RedfishHardwareTestCase(db_base.DbTestCase):
|
|||||||
enabled_boot_interfaces=['redfish-virtual-media'],
|
enabled_boot_interfaces=['redfish-virtual-media'],
|
||||||
enabled_management_interfaces=['redfish'],
|
enabled_management_interfaces=['redfish'],
|
||||||
enabled_inspect_interfaces=['redfish'],
|
enabled_inspect_interfaces=['redfish'],
|
||||||
enabled_bios_interfaces=['redfish'])
|
enabled_bios_interfaces=['redfish'],
|
||||||
|
enabled_firmware_interfaces=['redfish'])
|
||||||
|
|
||||||
def test_default_interfaces(self):
|
def test_default_interfaces(self):
|
||||||
node = obj_utils.create_test_node(self.context, driver='redfish')
|
node = obj_utils.create_test_node(self.context, driver='redfish')
|
||||||
|
31
releasenotes/notes/firmware-interface-8ad6f91aa1f746a0.yaml
Normal file
31
releasenotes/notes/firmware-interface-8ad6f91aa1f746a0.yaml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds Firmware Interface support to ironic, we would like to receive
|
||||||
|
feedback since this is a new feature we introduced and we as a developer
|
||||||
|
community have limited hardware access, reach out to us in case of any
|
||||||
|
unexpected behavior.
|
||||||
|
|
||||||
|
- Adds version 1.86 of the Bare Metal API, which includes:
|
||||||
|
|
||||||
|
* List all firmware components of a node via the
|
||||||
|
``GET /v1/nodes/{node_ident}/firmware`` API.
|
||||||
|
|
||||||
|
* The ``firmware_interface`` field of the node resource. A firmware
|
||||||
|
interface can be set when creating or updating a node.
|
||||||
|
|
||||||
|
* The ``default_firmware_interface`` and ``enabled_firmware_interface``
|
||||||
|
fields of the driver resource.
|
||||||
|
|
||||||
|
- Adds new configuration options for the firmware interface feature:
|
||||||
|
|
||||||
|
* Firmware interfaces are enabled via
|
||||||
|
``[DEFAULT]/enabled_firmware_interfaces``. A default firmware
|
||||||
|
interface to use when creating or updating nodes can be specified with
|
||||||
|
``[DEFAULT]/default_firmware_interface``.
|
||||||
|
|
||||||
|
- Available interfaces: ``redfish``, ``no-firmware`` and ``fake``.
|
||||||
|
|
||||||
|
- Support to update firmware of BIOS and BMC via ``update`` step, can be
|
||||||
|
done via clean or deploy steps, the node should be using the
|
||||||
|
``redfish`` driver and set the ``firmware_interface``.
|
@ -97,6 +97,7 @@ ironic.hardware.interfaces.deploy =
|
|||||||
ironic.hardware.interfaces.firmware =
|
ironic.hardware.interfaces.firmware =
|
||||||
fake = ironic.drivers.modules.fake:FakeFirmware
|
fake = ironic.drivers.modules.fake:FakeFirmware
|
||||||
no-firmware = ironic.drivers.modules.noop:NoFirmware
|
no-firmware = ironic.drivers.modules.noop:NoFirmware
|
||||||
|
redfish = ironic.drivers.modules.redfish.firmware:RedfishFirmware
|
||||||
|
|
||||||
ironic.hardware.interfaces.inspect =
|
ironic.hardware.interfaces.inspect =
|
||||||
agent = ironic.drivers.modules.inspector:AgentInspect
|
agent = ironic.drivers.modules.inspector:AgentInspect
|
||||||
|
Loading…
Reference in New Issue
Block a user