diff --git a/driver-requirements.txt b/driver-requirements.txt index e8f1c615e5..5ee2943f02 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -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 diff --git a/ironic/drivers/modules/redfish/bios.py b/ironic/drivers/modules/redfish/bios.py new file mode 100644 index 0000000000..53b5b35723 --- /dev/null +++ b/ironic/drivers/modules/redfish/bios.py @@ -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 : 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') diff --git a/ironic/drivers/redfish.py b/ironic/drivers/redfish.py index 9e5c933311..1610a2613e 100644 --- a/ironic/drivers/redfish.py +++ b/ironic/drivers/redfish.py @@ -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.""" diff --git a/ironic/tests/unit/drivers/modules/redfish/test_bios.py b/ironic/tests/unit/drivers/modules/redfish/test_bios.py new file mode 100644 index 0000000000..3edcbf1e1e --- /dev/null +++ b/ironic/tests/unit/drivers/modules/redfish/test_bios.py @@ -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) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_management.py b/ironic/tests/unit/drivers/modules/redfish/test_management.py index cce6c829e9..7876b2087f 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_management.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_management.py @@ -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) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_power.py b/ironic/tests/unit/drivers/modules/redfish/test_power.py index 4c3f61d6f5..96903328e8 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_power.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_power.py @@ -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) diff --git a/ironic/tests/unit/drivers/test_redfish.py b/ironic/tests/unit/drivers/test_redfish.py index 073df7d772..1b4e44585a 100644 --- a/ironic/tests/unit/drivers/test_redfish.py +++ b/ironic/tests/unit/drivers/test_redfish.py @@ -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') diff --git a/releasenotes/notes/redfish-bios-interface-a1acd8122c896a38.yaml b/releasenotes/notes/redfish-bios-interface-a1acd8122c896a38.yaml new file mode 100644 index 0000000000..df916fe2fb --- /dev/null +++ b/releasenotes/notes/redfish-bios-interface-a1acd8122c896a38.yaml @@ -0,0 +1,3 @@ +--- +features: + - Adds ``bios`` interface to the ``redfish`` hardware type. diff --git a/setup.cfg b/setup.cfg index d7a6460eb4..128b75e5ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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