# Copyright 2018 DMTF. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log
import sushy
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import metrics_utils
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 utils as redfish_utils
from ironic import objects
LOG = log.getLogger(__name__)
METRICS = metrics_utils.get_metrics_logger(__name__)
registry_fields = ('attribute_type', 'allowable_values', 'lower_bound',
'max_length', 'min_length', 'read_only',
'reset_required', 'unique', 'upper_bound')
BIOS_REBOOT_STATES = {
sushy.BootProgressStates.OS_BOOT_STARTED,
sushy.BootProgressStates.OS_RUNNING,
}
_DII_STATE = 'redfish_bios_state'
_REBOOT_REQUESTED = 'reboot_requested'
_REQUESTED_BIOS_ATTRS = 'requested_bios_attrs'
[docs]
class RedfishBIOS(base.BIOSInterface):
_APPLY_CONFIGURATION_ARGSINFO = {
'settings': {
'description': (
'A list of BIOS settings to be applied'
),
'required': True
}
}
def _parse_allowable_values(self, node, allowable_values):
"""Convert the BIOS registry allowable_value list to expected strings
:param allowable_values: list of dicts of valid values for enumeration
:returns: list containing only allowable value names
"""
# Get name from ValueName if it exists, otherwise use ValueDisplayName
new_list = []
for dic in allowable_values:
key = dic.get('ValueName') or dic.get('ValueDisplayName')
if key:
new_list.append(key)
else:
LOG.warning('Cannot detect the value name for enumeration '
'item %(item)s for node %(node)s',
{'item': dic, 'node': node.uuid})
return new_list
[docs]
def cache_bios_settings(self, task):
"""Store or update the current BIOS settings for the node.
Get the current BIOS settings and store them in the bios_settings
database table.
:param task: a TaskManager instance containing the node to act on.
:raises: RedfishConnectionError when it fails to connect to Redfish
:raises: RedfishError on an error from the Sushy library
:raises: UnsupportedDriverExtension if the system does not support BIOS
settings
"""
node_id = task.node.id
system = redfish_utils.get_system(task.node)
try:
attributes = system.bios.attributes
except sushy.exceptions.MissingAttributeError:
error_msg = _('Cannot fetch BIOS attributes for node %s, '
'BIOS settings are not supported.') % task.node.uuid
LOG.error(error_msg)
raise exception.UnsupportedDriverExtension(error_msg)
settings = []
# Convert Redfish BIOS attributes to Ironic BIOS settings
if attributes:
settings = [{'name': k, 'value': v}
for k, v in attributes.items()]
# Get the BIOS Registry
registry_attributes = []
try:
bios_registry = system.bios.get_attribute_registry()
if bios_registry:
registry_attributes = bios_registry.registry_entries.attributes
except Exception as e:
LOG.info('Cannot get BIOS Registry attributes for node %(node)s, '
'Error %(exc)s.', {'node': task.node.uuid, 'exc': e})
# TODO(bfournier): use a common list for registry field names
# e.g. registry_fields = objects.BIOSSetting.registry_fields
# The BIOS registry will contain more entries than the BIOS settings
# Find the registry entry matching the setting name and get the fields
if registry_attributes:
for setting in settings:
reg = next((r for r in registry_attributes
if r.name == setting['name']), None)
fields = [attr for attr in dir(reg)
if not attr.startswith("_")]
settable_keys = [f for f in fields if f in registry_fields]
# Set registry fields to current values
for k in settable_keys:
setting[k] = getattr(reg, k, None)
if k == "allowable_values" and isinstance(setting[k],
list):
setting[k] = self._parse_allowable_values(
task.node, setting[k])
LOG.debug('Cache BIOS settings for node %(node_uuid)s',
{'node_uuid': task.node.uuid})
create_list, update_list, delete_list, nochange_list = (
objects.BIOSSettingList.sync_node_setting(
task.context, node_id, settings))
if create_list:
objects.BIOSSettingList.create(
task.context, node_id, create_list)
if update_list:
objects.BIOSSettingList.save(
task.context, node_id, update_list)
if delete_list:
delete_names = [d['name'] for d in delete_list]
objects.BIOSSettingList.delete(
task.context, node_id, delete_names)
[docs]
@base.service_step(priority=0, requires_ramdisk=False)
@base.clean_step(priority=0, requires_ramdisk=False)
@base.deploy_step(priority=0)
@base.cache_bios_settings
def factory_reset(self, task):
"""Reset the BIOS settings of the node to the factory default.
:param task: a TaskManager instance containing the node to act on.
:raises: RedfishConnectionError when it fails to connect to Redfish
:raises: RedfishError on an error from the Sushy library
"""
system = redfish_utils.get_system(task.node)
try:
bios = system.bios
except sushy.exceptions.MissingAttributeError:
error_msg = (_('Redfish BIOS factory reset failed for node '
'%s, because BIOS settings are not supported.') %
task.node.uuid)
LOG.error(error_msg)
raise exception.RedfishError(error=error_msg)
node = task.node
info = node.driver_internal_info
bios_state = info.get(_DII_STATE) or {}
reboot_requested = bios_state.get(_REBOOT_REQUESTED, False)
if not reboot_requested:
LOG.debug('Factory reset BIOS configuration for node %(node)s',
{'node': node.uuid})
try:
bios.reset_bios()
except sushy.exceptions.SushyError as e:
error_msg = (_('Redfish BIOS factory reset failed for node '
'%(node)s. Error: %(error)s') %
{'node': node.uuid, 'error': e})
LOG.error(error_msg)
raise exception.RedfishError(error=error_msg)
self._set_reboot_requested(
task,
attributes=None)
return self.post_reset(task)
else:
current_attrs = bios.attributes
LOG.debug('Post factory reset, BIOS configuration for node '
'%(node_uuid)s: %(attrs)r',
{'node_uuid': node.uuid, 'attrs': current_attrs})
self._clear_reboot_requested(task)
[docs]
@base.service_step(priority=0, argsinfo=_APPLY_CONFIGURATION_ARGSINFO,
requires_ramdisk=False)
@base.clean_step(priority=0, argsinfo=_APPLY_CONFIGURATION_ARGSINFO,
requires_ramdisk=False)
@base.deploy_step(priority=0, argsinfo=_APPLY_CONFIGURATION_ARGSINFO)
@base.cache_bios_settings
def apply_configuration(self, task, settings):
"""Apply the BIOS settings to the node.
:param task: a TaskManager instance containing the node to act on.
:param settings: a list of BIOS settings to be updated.
:raises: RedfishConnectionError when it fails to connect to Redfish
:raises: RedfishError on an error from the Sushy library
"""
system = redfish_utils.get_system(task.node)
try:
bios = system.bios
except sushy.exceptions.MissingAttributeError:
error_msg = (_('Redfish BIOS factory reset failed for node '
'%s, because BIOS settings are not supported.') %
task.node.uuid)
LOG.error(error_msg)
raise exception.RedfishError(error=error_msg)
info = task.node.driver_internal_info
bios_state = info.get(_DII_STATE) or {}
reboot_requested = bios_state.get(_REBOOT_REQUESTED, False)
if not reboot_requested:
self._validate_and_convert_settings(task, settings)
# Convert Ironic BIOS settings to Redfish BIOS attributes
attributes = {s['name']: s['value'] for s in settings}
# Check if all requested settings already match the current
# BIOS values. When a client of the Ironic API re-sends
# the same request after a successful apply, this avoids an
# unnecessary reboot cycle.
current_attrs = bios.attributes or {}
try:
pending_attrs = bios.pending_attributes
except (sushy.exceptions.SushyError, AttributeError):
pending_attrs = {}
if not isinstance(pending_attrs, dict):
pending_attrs = {}
all_match = True
for s in settings:
name, value = s['name'], s['value']
if current_attrs.get(name) != value:
all_match = False
break
if name in pending_attrs:
if pending_attrs[name] != value:
# A conflicting pending change exists.
all_match = False
break
if all_match:
LOG.info('All requested BIOS settings for node '
'%(node_uuid)s already match the current '
'values, skipping apply and reboot.',
{'node_uuid': task.node.uuid})
return
# Step 1: Apply settings and issue a reboot
LOG.debug('Apply BIOS configuration for node %(node_uuid)s: '
'%(settings)r', {'node_uuid': task.node.uuid,
'settings': settings})
apply_time = None
try:
if bios.supported_apply_times and (
sushy.APPLY_TIME_ON_RESET in
bios.supported_apply_times):
apply_time = sushy.APPLY_TIME_ON_RESET
except AttributeError:
LOG.warning('SupportedApplyTimes attribute missing for BIOS'
' configuration on node %(node_uuid)s: ',
{'node_uuid': task.node.uuid})
try:
bios.set_attributes(attributes, apply_time=apply_time)
except sushy.exceptions.SushyError as e:
error_msg = (_('Redfish BIOS apply configuration failed for '
'node %(node)s. Error: %(error)s') %
{'node': task.node.uuid, 'error': e})
LOG.error(error_msg)
raise exception.RedfishError(error=error_msg)
self._set_reboot_requested(
task,
attributes)
return self.post_configuration(task, settings)
else:
# Step 2: Verify requested BIOS settings applied
requested_attrs = (
bios_state.get(_REQUESTED_BIOS_ATTRS)
or info.get(_REQUESTED_BIOS_ATTRS))
LOG.debug('Verify BIOS configuration for node %(node_uuid)s: '
'%(attrs)r', {'node_uuid': task.node.uuid,
'attrs': requested_attrs})
self._clear_reboot_requested(task)
attrs_not_updated = self._get_unapplied_bios_attrs(
task, requested_attrs, bios)
if attrs_not_updated:
LOG.debug('BIOS settings %(attrs)s for node %(node_uuid)s '
'not updated.', {'attrs': attrs_not_updated,
'node_uuid': task.node.uuid})
self._set_step_failed(task, attrs_not_updated)
else:
LOG.debug('Verification of BIOS settings for node '
'%(node_uuid)s successful.',
{'node_uuid': task.node.uuid})
[docs]
def post_reset(self, task):
"""Perform post reset action to apply the BIOS factory reset.
Extension point to allow vendor implementations to extend this class
and override this method to perform a custom action to apply the BIOS
factory reset to the Redfish service. The default implementation
performs a reboot.
:param task: a TaskManager instance containing the node to act on.
"""
return deploy_utils.reboot_to_finish_step(task)
[docs]
def post_configuration(self, task, settings):
"""Perform post configuration action to store the BIOS settings.
Extension point to allow vendor implementations to extend this class
and override this method to perform a custom action to write the BIOS
settings to the Redfish service. The default implementation performs
a reboot.
:param task: a TaskManager instance containing the node to act on.
:param settings: a list of BIOS settings to be updated.
"""
return deploy_utils.reboot_to_finish_step(task)
[docs]
def get_properties(self):
"""Return the properties of the interface.
:returns: dictionary of <property name>:<property description> entries.
"""
return redfish_utils.COMMON_PROPERTIES.copy()
[docs]
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 _validate_and_convert_settings(self, task, settings):
"""Validate and convert BIOS settings using the cached registry.
Validates each setting against the cached BIOS registry, then
converts values to the types expected by the Redfish API.
Settings arrive as strings but the Redfish BIOS API expects
typed JSON values (integers for Integer attributes, booleans
for Boolean attributes).
:param task: a TaskManager instance containing the node to act on.
:param settings: a list of BIOS settings (modified in place).
:raises: InvalidParameterValue if any setting value is invalid
according to the cached registry, including: read-only settings,
Enumeration values not in allowable_values, Integer values outside
lower_bound/upper_bound, and String values outside
min_length/max_length.
"""
cached_settings = {
s.name: s
for s in objects.BIOSSettingList.get_by_node_id(
task.context, task.node.id)
}
invalid = []
for s in settings:
name, value = s['name'], s['value']
cached = cached_settings.get(name)
if not cached:
continue
if cached.read_only:
invalid.append(name)
continue
if (cached.attribute_type == 'Enumeration'
and cached.allowable_values
and value not in cached.allowable_values):
invalid.append(name)
elif cached.attribute_type == 'Integer':
try:
int_value = int(value)
except (ValueError, TypeError):
invalid.append(name)
continue
if ((cached.lower_bound is not None
and int_value < cached.lower_bound)
or (cached.upper_bound is not None
and int_value > cached.upper_bound)):
invalid.append(name)
else:
s['value'] = int_value
elif cached.attribute_type == 'Boolean':
if isinstance(value, str):
s['value'] = value.lower() == 'true'
elif cached.attribute_type == 'String':
str_len = len(value)
if ((cached.min_length is not None
and str_len < cached.min_length)
or (cached.max_length is not None
and str_len > cached.max_length)):
invalid.append(name)
if invalid:
error_msg = (
_('Redfish BIOS apply_configuration failed for node '
'%(node)s: invalid value for setting(s): %(fields)s') %
{'node': task.node.uuid,
'fields': ', '.join(invalid)})
LOG.error(error_msg)
raise exception.InvalidParameterValue(error_msg)
def _get_unapplied_bios_attrs(self, task, requested_attrs, bios):
"""Return requested BIOS attrs that have not yet been applied.
:param task: a TaskManager instance containing the node to act on.
:param requested_attrs: the requested BIOS attributes to update.
:param bios: BIOS resource object from the system.
:returns: dict of attributes not yet applied, empty if all applied.
"""
bios.refresh(force=True)
current_attrs = bios.attributes
pending_attrs = bios.pending_attributes
if not isinstance(pending_attrs, dict):
pending_attrs = {}
attrs_not_updated = {}
# On some systems pending area only contain the modified attributes
# (eg. dell) and must be clean before another modification can be done.
# On others it always contains all the values of the writable
# attributes (eg. hpe, lenovo)
purge_pending_behavior = len(pending_attrs) <= len(requested_attrs)
for attr in requested_attrs:
if requested_attrs[attr] != current_attrs.get(attr):
attrs_not_updated[attr] = requested_attrs[attr]
elif purge_pending_behavior and attr in pending_attrs:
attrs_not_updated[attr] = requested_attrs[attr]
return attrs_not_updated
def _set_reboot_requested(self, task, attributes):
"""Set driver_internal_info flags for reboot requested.
:param task: a TaskManager instance containing the node to act on.
:param attributes: the requested BIOS attributes to update.
"""
node = task.node
bios_state = {_REBOOT_REQUESTED: True}
if attributes:
bios_state[_REQUESTED_BIOS_ATTRS] = attributes
node.set_driver_internal_info(_DII_STATE, bios_state)
node.save()
disable_ramdisk = deploy_utils.is_ramdisk_disabled(node)
# polling=True tells the IPA heartbeat handler to stand down so
# that only our periodic task (_query_bios_apply_status) drives
# step completion. When ramdisk is active (polling=False), both
# the heartbeat and the periodic task may race; the exclusive
# lock in continue_node_clean/deploy/service ensures only one
# wins.
deploy_utils.set_async_step_flags(task.node, reboot=True,
skip_current_step=False,
polling=disable_ramdisk)
def _clear_reboot_requested(self, task):
"""Clear driver_internal_info flags after reboot completed.
:param task: a TaskManager instance containing the node to act on.
"""
node = task.node
node.del_driver_internal_info(_DII_STATE)
# Drop legacy fields if present from older runs.
node.del_driver_internal_info('post_bios_reboot_requested')
node.del_driver_internal_info(_REQUESTED_BIOS_ATTRS)
node.save()
def _set_step_failed(self, task, attrs_not_updated):
"""Fail the cleaning or deployment step and log the error.
:param task: a TaskManager instance containing the node to act on.
:param attrs_not_updated: the BIOS attributes that were not updated.
"""
error_msg = (_('Redfish BIOS apply_configuration step failed for node '
'%(node)s. Attributes %(attrs)s are not updated.') %
{'node': task.node.uuid, 'attrs': attrs_not_updated})
last_error = (_('Redfish BIOS apply_configuration step failed. '
'Attributes %(attrs)s are not updated.') %
{'attrs': attrs_not_updated})
deploy_utils.step_error_handler(task, error_msg, last_error)
@METRICS.timer('RedfishBIOS._query_bios_apply_status')
@periodics.node_periodic(
purpose='checking async redfish BIOS apply/reset status',
spacing=CONF.redfish.firmware_update_status_interval,
filters={'reserved': False,
'provision_state_in': {states.CLEANWAIT,
states.SERVICEWAIT,
states.DEPLOYWAIT}},
predicate_extra_fields=['driver_internal_info'],
predicate=lambda n: n.driver_internal_info.get(_DII_STATE),
)
def _query_bios_apply_status(self, task, manager, context):
self._check_node_redfish_bios_apply(task)
@METRICS.timer('RedfishBIOS._check_node_redfish_bios_apply')
def _check_node_redfish_bios_apply(self, task):
node = task.node
bios_state = node.driver_internal_info.get(_DII_STATE) or {}
if not bios_state:
LOG.debug('BIOS state cleared for node %(node)s before periodic '
'could process it (likely a timeout race).',
{'node': node.uuid})
return
try:
system = redfish_utils.get_system(node)
except (exception.RedfishError,
exception.RedfishConnectionError,
sushy.exceptions.SushyError) as e:
LOG.warning('Unable to query Redfish system for node %(node)s '
'while waiting for BIOS reboot completion: '
'%(error)s',
{'node': node.uuid, 'error': e})
return
last_state = (system.boot_progress.last_state
if system.boot_progress is not None else None)
if last_state is not None:
# BootProgress is reported — touch provisioning to prevent the
# global timeout from firing while we observe meaningful progress.
node.touch_provisioning()
if last_state not in BIOS_REBOOT_STATES:
LOG.debug('Node %(node)s boot progress: %(state)s. '
'Waiting for boot progress to reach OS started.',
{'node': node.uuid, 'state': last_state})
return
# When BootProgress is unavailable (last_state is None), fall
# through to the attrs check below. Do NOT touch provisioning
# so the global timeout remains the safety net.
requested_attrs = bios_state.get(_REQUESTED_BIOS_ATTRS)
if requested_attrs:
attrs_not_updated = self._get_unapplied_bios_attrs(
task, requested_attrs, system.bios)
if attrs_not_updated:
LOG.debug('BIOS settings %(attrs)s for node %(node_uuid)s '
'not yet applied; continue polling.',
{'attrs': attrs_not_updated,
'node_uuid': node.uuid})
return
LOG.info('Detected post-BIOS reboot completion for node %(node)s, '
'resuming the current step.',
{'node': node.uuid})
if node.clean_step:
manager_utils.notify_conductor_resume_clean(task)
elif node.service_step:
manager_utils.notify_conductor_resume_service(task)
elif node.deploy_step:
manager_utils.notify_conductor_resume_deploy(task)