Add BIOS interface to Redfish hardware type

The Redfish hardware type does not currently implement the new BIOS
hardware interface.

This patch implements the Redfish BIOS interface, allowing operators to
perform BIOS configuration actions on Ironic Redfish nodes.

Change-Id: I44a2a465b08bc15465b7096b1e4838aebb460c1b
Story: 2001791
Task: 12507
This commit is contained in:
Bill Dodd 2018-07-12 12:34:08 -05:00
parent c5414620c5
commit 9880262def
9 changed files with 526 additions and 4 deletions

View File

@ -16,7 +16,7 @@ python-xclarityclient>=0.1.6
ImcSdk>=0.7.2
# The Redfish hardware type uses the Sushy library
sushy
sushy>=1.6.0
# Ansible-deploy interface
ansible>=2.4

View File

@ -0,0 +1,283 @@
# 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 ironic_lib import metrics_utils
from oslo_log import log
from oslo_utils import importutils
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import states
from ironic.conductor import task_manager
from ironic.conductor import utils as manager_utils
from ironic.drivers import base
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 RedfishBIOS(base.BIOSInterface):
def __init__(self):
super(RedfishBIOS, self).__init__()
if sushy is None:
raise exception.DriverLoadError(
driver='redfish',
reason=_("Unable to import the sushy library"))
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
"""
node_id = task.node.id
system = redfish_utils.get_system(task.node)
attributes = system.bios.attributes
settings = []
# Convert Redfish BIOS attributes to Ironic BIOS settings
if attributes:
settings = [{'name': k, 'value': v} for k, v in attributes.items()]
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)
@base.clean_step(priority=0)
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)
bios = system.bios
LOG.debug('Factory reset BIOS settings for node %(node_uuid)s',
{'node_uuid': task.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': task.node.uuid, 'error': e})
LOG.error(error_msg)
raise exception.RedfishError(error=error_msg)
self.post_reset(task)
self._set_cleaning_reboot(task)
@base.clean_step(priority=0, argsinfo={
'settings': {
'description': (
'A list of BIOS settings to be applied'
),
'required': True
}
})
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)
bios = system.bios
# Convert Ironic BIOS settings to Redfish BIOS attributes
attributes = {s['name']: s['value'] for s in settings}
info = task.node.driver_internal_info
reboot_requested = info.get('post_config_reboot_requested')
if not reboot_requested:
# 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})
try:
bios.set_attributes(attributes)
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.post_configuration(task, settings)
self._set_reboot_requested(task, attributes)
return states.CLEANWAIT
else:
# Step 2: Verify requested BIOS settings applied
requested_attrs = info.get('requested_bios_attrs')
current_attrs = bios.attributes
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)
self._check_bios_attrs(task, current_attrs, requested_attrs)
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.
"""
self._reboot(task)
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.
"""
self._reboot(task)
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 _check_bios_attrs(self, task, current_attrs, requested_attrs):
"""Checks that the requested BIOS settings were applied to the service.
:param task: a TaskManager instance containing the node to act on.
:param current_attrs: the current BIOS attributes from the system.
:param requested_attrs: the requested BIOS attributes to update.
"""
attrs_not_updated = {}
for attr in requested_attrs:
if requested_attrs[attr] != current_attrs.get(attr):
attrs_not_updated[attr] = requested_attrs[attr]
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_clean_failed(task, attrs_not_updated)
else:
LOG.debug('Verification of BIOS settings for node %(node_uuid)s '
'successful.', {'node_uuid': task.node.uuid})
@task_manager.require_exclusive_lock
def _reboot(self, task):
"""Reboot the target Redfish service.
:param task: a TaskManager instance containing the node to act on.
:raises: InvalidParameterValue when the wrong state is specified
or the wrong driver info is specified.
:raises: RedfishError on an error from the Sushy library
"""
manager_utils.node_power_action(task, states.REBOOT)
def _set_cleaning_reboot(self, task):
"""Set driver_internal_info flags for cleaning reboot.
:param task: a TaskManager instance containing the node to act on.
"""
info = task.node.driver_internal_info
info['cleaning_reboot'] = True
task.node.driver_internal_info = info
task.node.save()
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.
"""
info = task.node.driver_internal_info
info['post_config_reboot_requested'] = True
info['cleaning_reboot'] = True
info['requested_bios_attrs'] = attributes
info['skip_current_clean_step'] = False
task.node.driver_internal_info = info
task.node.save()
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.
"""
info = task.node.driver_internal_info
if 'post_config_reboot_requested' in info:
del info['post_config_reboot_requested']
if 'requested_bios_attrs' in info:
del info['requested_bios_attrs']
task.node.driver_internal_info = info
task.node.save()
def _set_clean_failed(self, task, attrs_not_updated):
"""Fail the cleaning 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})
LOG.error(error_msg)
task.node.last_error = last_error
if task.node.provision_state in [states.CLEANING, states.CLEANWAIT]:
task.process_event('fail')

View File

@ -16,6 +16,7 @@
from ironic.drivers import generic
from ironic.drivers.modules import inspector
from ironic.drivers.modules import noop
from ironic.drivers.modules.redfish import bios as redfish_bios
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 power as redfish_power
@ -24,6 +25,11 @@ from ironic.drivers.modules.redfish import power as redfish_power
class RedfishHardware(generic.GenericHardware):
"""Redfish hardware type."""
@property
def supported_bios_interfaces(self):
"""List of supported bios interfaces."""
return [redfish_bios.RedfishBIOS, noop.NoBIOS]
@property
def supported_management_interfaces(self):
"""List of supported management interfaces."""

View File

@ -0,0 +1,226 @@
# 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.
import mock
from oslo_utils import importutils
from ironic.common import exception
from ironic.common import states
from ironic.conductor import task_manager
from ironic.conductor import utils as manager_utils
from ironic.drivers.modules.redfish import bios as redfish_bios
from ironic.drivers.modules.redfish import utils as redfish_utils
from ironic import objects
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()
class MockedSushyError(Exception):
pass
@mock.patch('eventlet.greenthread.sleep', lambda _t: None)
class RedfishBiosTestCase(db_base.DbTestCase):
def setUp(self):
super(RedfishBiosTestCase, self).setUp()
self.config(enabled_bios_interfaces=['redfish'],
enabled_hardware_types=['redfish'],
enabled_power_interfaces=['redfish'],
enabled_management_interfaces=['redfish'])
self.node = obj_utils.create_test_node(
self.context, driver='redfish', driver_info=INFO_DICT)
@mock.patch.object(redfish_bios, 'sushy', None)
def test_loading_error(self):
self.assertRaisesRegex(
exception.DriverLoadError,
'Unable to import the sushy library',
redfish_bios.RedfishBIOS)
def test_get_properties(self):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
properties = task.driver.get_properties()
for prop in redfish_utils.COMMON_PROPERTIES:
self.assertIn(prop, properties)
@mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True)
def test_validate(self, mock_parse_driver_info):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.driver.bios.validate(task)
mock_parse_driver_info.assert_called_once_with(task.node)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
@mock.patch.object(objects, 'BIOSSettingList', autospec=True)
def test_cache_bios_settings_noop(self, mock_setting_list,
mock_get_system):
create_list = []
update_list = []
delete_list = []
nochange_list = [{'name': 'EmbeddedSata', 'value': 'Raid'},
{'name': 'NicBoot1', 'value': 'NetworkBoot'}]
mock_setting_list.sync_node_setting.return_value = (
create_list, update_list, delete_list, nochange_list
)
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
attributes = mock_get_system(task.node).bios.attributes
settings = [{'name': k, 'value': v} for k, v in attributes.items()]
mock_get_system.reset_mock()
task.driver.bios.cache_bios_settings(task)
mock_get_system.assert_called_once_with(task.node)
mock_setting_list.sync_node_setting.assert_called_once_with(
task.context, task.node.id, settings)
mock_setting_list.create.assert_not_called()
mock_setting_list.save.assert_not_called()
mock_setting_list.delete.assert_not_called()
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
@mock.patch.object(objects, 'BIOSSettingList', autospec=True)
def test_cache_bios_settings(self, mock_setting_list, mock_get_system):
create_list = [{'name': 'DebugMode', 'value': 'enabled'}]
update_list = [{'name': 'BootMode', 'value': 'Uefi'},
{'name': 'NicBoot2', 'value': 'NetworkBoot'}]
delete_list = [{'name': 'AdminPhone', 'value': '555-867-5309'}]
nochange_list = [{'name': 'EmbeddedSata', 'value': 'Raid'},
{'name': 'NicBoot1', 'value': 'NetworkBoot'}]
delete_names = []
for setting in delete_list:
delete_names.append(setting.get('name'))
mock_setting_list.sync_node_setting.return_value = (
create_list, update_list, delete_list, nochange_list
)
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
attributes = mock_get_system(task.node).bios.attributes
settings = [{'name': k, 'value': v} for k, v in attributes.items()]
mock_get_system.reset_mock()
task.driver.bios.cache_bios_settings(task)
mock_get_system.assert_called_once_with(task.node)
mock_setting_list.sync_node_setting.assert_called_once_with(
task.context, task.node.id, settings)
mock_setting_list.create.assert_called_once_with(
task.context, task.node.id, create_list)
mock_setting_list.save.assert_called_once_with(
task.context, task.node.id, update_list)
mock_setting_list.delete.assert_called_once_with(
task.context, task.node.id, delete_names)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
def test_factory_reset(self, mock_power_action, mock_get_system):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.bios.factory_reset(task)
mock_get_system.assert_called_with(task.node)
mock_power_action.assert_called_once_with(task, states.REBOOT)
bios = mock_get_system(task.node).bios
bios.reset_bios.assert_called_once()
@mock.patch('ironic.drivers.modules.redfish.bios.sushy')
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_factory_reset_fail(self, mock_get_system, mock_sushy):
mock_sushy.exceptions.SushyError = MockedSushyError
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
bios = mock_get_system(task.node).bios
bios.reset_bios.side_effect = MockedSushyError
self.assertRaisesRegex(
exception.RedfishError, 'BIOS factory reset failed',
task.driver.bios.factory_reset, task)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
def test_apply_configuration_step1(self, mock_power_action,
mock_get_system):
settings = [{'name': 'ProcTurboMode', 'value': 'Disabled'},
{'name': 'NicBoot1', 'value': 'NetworkBoot'}]
attributes = {s['name']: s['value'] for s in settings}
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.bios.apply_configuration(task, settings)
mock_get_system.assert_called_with(task.node)
mock_power_action.assert_called_once_with(task, states.REBOOT)
bios = mock_get_system(task.node).bios
bios.set_attributes.assert_called_once_with(attributes)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_apply_configuration_step2(self, mock_get_system):
settings = [{'name': 'ProcTurboMode', 'value': 'Disabled'},
{'name': 'NicBoot1', 'value': 'NetworkBoot'}]
requested_attrs = {'ProcTurboMode': 'Enabled'}
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.node.driver_internal_info[
'post_config_reboot_requested'] = True
task.node.driver_internal_info[
'requested_bios_attrs'] = requested_attrs
task.driver.bios._clear_reboot_requested = mock.MagicMock()
task.driver.bios.apply_configuration(task, settings)
mock_get_system.assert_called_with(task.node)
task.driver.bios._clear_reboot_requested\
.assert_called_once_with(task)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_check_bios_attrs(self, mock_get_system):
settings = [{'name': 'ProcTurboMode', 'value': 'Disabled'},
{'name': 'NicBoot1', 'value': 'NetworkBoot'}]
requested_attrs = {'ProcTurboMode': 'Enabled'}
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
attributes = mock_get_system(task.node).bios.attributes
task.node.driver_internal_info[
'post_config_reboot_requested'] = True
task.node.driver_internal_info[
'requested_bios_attrs'] = requested_attrs
task.driver.bios._check_bios_attrs = mock.MagicMock()
task.driver.bios.apply_configuration(task, settings)
task.driver.bios._check_bios_attrs \
.assert_called_once_with(task, attributes, requested_attrs)
@mock.patch('ironic.drivers.modules.redfish.bios.sushy')
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_apply_configuration_fail(self, mock_get_system, mock_sushy):
settings = [{'name': 'ProcTurboMode', 'value': 'Disabled'},
{'name': 'NicBoot1', 'value': 'NetworkBoot'}]
mock_sushy.exceptions.SushyError = MockedSushyError
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
bios = mock_get_system(task.node).bios
bios.set_attributes.side_effect = MockedSushyError
self.assertRaisesRegex(
exception.RedfishError, 'BIOS apply configuration failed',
task.driver.bios.apply_configuration, task, settings)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_post_configuration(self, mock_get_system):
settings = [{'name': 'ProcTurboMode', 'value': 'Disabled'},
{'name': 'NicBoot1', 'value': 'NetworkBoot'}]
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.driver.bios.post_configuration = mock.MagicMock()
task.driver.bios.apply_configuration(task, settings)
task.driver.bios.post_configuration\
.assert_called_once_with(task, settings)

View File

@ -38,7 +38,8 @@ class RedfishManagementTestCase(db_base.DbTestCase):
self.config(enabled_hardware_types=['redfish'],
enabled_power_interfaces=['redfish'],
enabled_management_interfaces=['redfish'],
enabled_inspect_interfaces=['redfish'])
enabled_inspect_interfaces=['redfish'],
enabled_bios_interfaces=['redfish'])
self.node = obj_utils.create_test_node(
self.context, driver='redfish', driver_info=INFO_DICT)

View File

@ -38,7 +38,8 @@ class RedfishPowerTestCase(db_base.DbTestCase):
self.config(enabled_hardware_types=['redfish'],
enabled_power_interfaces=['redfish'],
enabled_management_interfaces=['redfish'],
enabled_inspect_interfaces=['redfish'])
enabled_inspect_interfaces=['redfish'],
enabled_bios_interfaces=['redfish'])
self.node = obj_utils.create_test_node(
self.context, driver='redfish', driver_info=INFO_DICT)

View File

@ -31,7 +31,8 @@ class RedfishHardwareTestCase(db_base.DbTestCase):
self.config(enabled_hardware_types=['redfish'],
enabled_power_interfaces=['redfish'],
enabled_management_interfaces=['redfish'],
enabled_inspect_interfaces=['redfish'])
enabled_inspect_interfaces=['redfish'],
enabled_bios_interfaces=['redfish'])
def test_default_interfaces(self):
node = obj_utils.create_test_node(self.context, driver='redfish')

View File

@ -0,0 +1,3 @@
---
features:
- Adds ``bios`` interface to the ``redfish`` hardware type.

View File

@ -58,6 +58,7 @@ ironic.hardware.interfaces.bios =
ilo = ironic.drivers.modules.ilo.bios:IloBIOS
irmc = ironic.drivers.modules.irmc.bios:IRMCBIOS
no-bios = ironic.drivers.modules.noop:NoBIOS
redfish = ironic.drivers.modules.redfish.bios:RedfishBIOS
ironic.hardware.interfaces.boot =
fake = ironic.drivers.modules.fake:FakeBoot