From fa59565be049209cee7cafa5aac0bed55d5ef0c6 Mon Sep 17 00:00:00 2001 From: Luong Anh Tuan Date: Wed, 17 Jan 2018 09:18:45 +0700 Subject: [PATCH] Implement iRMC BIOS configuration This patch supports BIOS configuration for iRMC drivers using out-of-band method. Tested successfully on TX2540 M1 along with eLCM license. Story: #1743674 Task: #10651 Co-Authored-By: Nguyen Van Trung Change-Id: I61f15e7c65c4ef5cc5f959d2b016b053e70ba19b --- doc/source/admin/drivers/irmc.rst | 66 +++++++- driver-requirements.txt | 2 +- ironic/drivers/irmc.py | 6 + ironic/drivers/modules/irmc/bios.py | 140 ++++++++++++++++ .../unit/drivers/modules/irmc/test_bios.py | 152 ++++++++++++++++++ .../unit/drivers/modules/irmc/test_common.py | 1 + ironic/tests/unit/drivers/test_irmc.py | 30 +++- ...n-bios-configuration-1ad24831501456d5.yaml | 6 + setup.cfg | 1 + 9 files changed, 401 insertions(+), 3 deletions(-) create mode 100644 ironic/drivers/modules/irmc/bios.py create mode 100644 ironic/tests/unit/drivers/modules/irmc/test_bios.py create mode 100644 releasenotes/notes/irmc-manual-clean-bios-configuration-1ad24831501456d5.yaml diff --git a/doc/source/admin/drivers/irmc.rst b/doc/source/admin/drivers/irmc.rst index 769f285c46..ef1cba4858 100644 --- a/doc/source/admin/drivers/irmc.rst +++ b/doc/source/admin/drivers/irmc.rst @@ -18,7 +18,7 @@ Prerequisites * Install `python-scciclient `_ and `pysnmp `_ packages:: - $ pip install "python-scciclient>=0.7.0" pysnmp + $ pip install "python-scciclient>=0.7.1" pysnmp Hardware Type ============= @@ -33,6 +33,10 @@ Hardware interfaces The ``irmc`` hardware type overrides the selection of the following hardware interfaces: +* bios + Supports ``irmc`` and ``no-bios``. + The default is ``irmc``. + * boot Supports ``irmc-virtual-media``, ``irmc-pxe``, and ``pxe``. The default is ``irmc-virtual-media``. The ``irmc-virtual-media`` boot @@ -81,6 +85,7 @@ interfaces enabled for ``irmc`` hardware type. [DEFAULT] enabled_hardware_types = irmc + enabled_bios_interfaces = irmc enabled_boot_interfaces = irmc-virtual-media,irmc-pxe enabled_console_interfaces = ipmitool-socat,ipmitool-shellinabox,no-console enabled_deploy_interfaces = iscsi,direct @@ -97,6 +102,7 @@ Here is a command example to enroll a node with ``irmc`` hardware type. .. code-block:: console openstack baremetal node create --os-baremetal-api-version=1.31 \ + --bios-interface irmc \ --boot-interface irmc-pxe \ --deploy-interface direct \ --inspect-interface irmc \ @@ -481,6 +487,64 @@ The RAID configuration is supported as a manual cleaning step. See :ref:`raid` for more details and examples. +BIOS configuration Support +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``irmc`` hardware type provides the iRMC BIOS configuration with ``irmc`` +bios interface. + +.. warning:: + ``irmc`` bios interface does not support``factory_reset``. + + +Configuration +~~~~~~~~~~~~~ + +The BIOS configuration in the iRMC driver supports the following settings: + + - ``boot_option_filter``: Specifies from which drives can be booted. This + supports following options: ``UefiAndLegacy``, ``LegacyOnly``, ``UefiOnly``. + - ``check_controllers_health_status_enabled``: The UEFI FW checks the + controller health status. This supports following options: ``true``, ``false``. + - ``cpu_active_processor_cores``: The number of active processor cores 1...n. + Option 0 indicates that all available processor cores are active. + - ``cpu_adjacent_cache_line_prefetch_enabled``: The processor loads the requested + cache line and the adjacent cache line. This supports following options: + ``true``, ``false``. + - ``cpu_vt_enabled``: Supports the virtualization of platform hardware and + several software environments, based on Virtual Machine Extensions to + support the use of several software environments using virtual computers. + This supports following options: ``true``, ``false``. + - ``flash_write_enabled``: The system BIOS can be written. Flash BIOS update + is possible. This supports following options: ``true``, ``false``. + - ``hyper_threading_enabled``: Hyper-threading technology allows a single + physical processor core to appear as several logical processors. This + supports following options: ``true``, ``false``. + - ``keep_void_boot_options_enabled``: Boot Options will not be removed from + "Boot Option Priority" list. This supports following options: ``true``, + ``false``. + - ``launch_csm_enabled``: Specifies whether the Compatibility Support Module + (CSM) is executed. This supports following options: ``true``, ``false``. + - ``os_energy_performance_override_enabled``: Prevents the OS from overruling + any energy efficiency policy setting of the setup. This supports following + options: ``true``, ``false``. + - ``pci_aspm_support``: Active State Power Management (ASPM) is used to + power-manage the PCI Express links, thus consuming less power. This + supports following options: ``Disabled``, ``Auto``, ``L0Limited``, + ``L1only``, ``L0Force``. + - ``pci_above_4g_decoding_enabled``: Specifies if memory resources above the + 4GB address boundary can be assigned to PCI devices. This supports + following options: ``true``, ``false``. + - ``power_on_source``: Specifies whether the switch on sources for the system + are managed by the BIOS or the ACPI operating system. This supports + following options: ``BiosControlled``, ``AcpiControlled``. + - ``single_root_io_virtualization_support_enabled``: Single Root IO + Virtualization Support is active. This supports following + options: ``true``, ``false``. + +The BIOS configuration is supported as a manual cleaning step. See :ref:`bios` +for more details and examples. + Supported platforms =================== This driver supports FUJITSU PRIMERGY BX S4 or RX S8 servers and above. diff --git a/driver-requirements.txt b/driver-requirements.txt index 5b4c7e22eb..7b572f4026 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -8,7 +8,7 @@ proliantutils>=2.5.0 pysnmp python-ironic-inspector-client>=1.5.0 python-oneviewclient<3.0.0,>=2.5.2 -python-scciclient>=0.7.0 +python-scciclient>=0.7.1 python-ilorest-library>=2.1.0 hpOneView>=4.4.0 UcsSdk==0.8.2.2 diff --git a/ironic/drivers/irmc.py b/ironic/drivers/irmc.py index d35ba4e2b4..0a61e1fb1f 100644 --- a/ironic/drivers/irmc.py +++ b/ironic/drivers/irmc.py @@ -20,6 +20,7 @@ from ironic.drivers import generic from ironic.drivers.modules import agent from ironic.drivers.modules import inspector from ironic.drivers.modules import ipmitool +from ironic.drivers.modules.irmc import bios from ironic.drivers.modules.irmc import boot from ironic.drivers.modules.irmc import inspect from ironic.drivers.modules.irmc import management @@ -36,6 +37,11 @@ class IRMCHardware(generic.GenericHardware): have iRMC S4 management system. """ + @property + def supported_bios_interfaces(self): + """List of supported bios interfaces.""" + return [bios.IRMCBIOS, noop.NoBIOS] + @property def supported_boot_interfaces(self): """List of supported boot interfaces.""" diff --git a/ironic/drivers/modules/irmc/bios.py b/ironic/drivers/modules/irmc/bios.py new file mode 100644 index 0000000000..d8a2671b30 --- /dev/null +++ b/ironic/drivers/modules/irmc/bios.py @@ -0,0 +1,140 @@ +# Copyright 2018 FUJITSU LIMITED +# +# 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. + +""" +iRMC BIOS configuration specific methods +""" +from ironic_lib import metrics_utils +from oslo_log import log as logging +from oslo_utils import importutils + +from ironic.common import exception +from ironic.drivers import base +from ironic.drivers.modules.irmc import common as irmc_common +from ironic import objects + + +irmc = importutils.try_import('scciclient.irmc') + +LOG = logging.getLogger(__name__) + +METRICS = metrics_utils.get_metrics_logger(__name__) + + +class IRMCBIOS(base.BIOSInterface): + + def get_properties(self): + """Return the properties of the interface.""" + return irmc_common.COMMON_PROPERTIES + + @METRICS.timer('IRMCBIOS.validate') + def validate(self, task): + """Validate the driver-specific Node info. + + This method validates whether the 'driver_info' property of the + supplied node contains the required information for this driver to + manage the BIOS settings of the node. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue if required driver_info attribute + is missing or invalid on the node. + :raises: MissingParameterValue if a required parameter is missing + in the driver_info property. + """ + irmc_common.parse_driver_info(task.node) + + @METRICS.timer('IRMCBIOS.apply_configuration') + @base.clean_step(priority=0, abortable=False, argsinfo={ + 'settings': { + 'description': "Dictionary containing the BIOS configuration.", + 'required': True + } + }) + def apply_configuration(self, task, settings): + """Applies BIOS configuration on the given node. + + This method takes the BIOS settings from the settings param and + applies BIOS configuration on the given node. + After the BIOS configuration is done, self.cache_bios_settings() may + be called to sync the node's BIOS-related information with the BIOS + configuration applied on the node. + It will also validate the given settings before applying any + settings and manage failures when setting an invalid BIOS config. + In the case of needing password to update the BIOS config, it will be + taken from the driver_info properties. + + :param task: a TaskManager instance. + :param settings: Dictionary containing the BIOS configuration. It + may be an empty dictionary as well. + :raises: IRMCOperationError,if apply bios settings failed. + """ + + irmc_info = irmc_common.parse_driver_info(task.node) + + try: + LOG.info('Apply BIOS configuration for node %(node_uuid)s: ' + '%(settings)s', {'settings': settings, + 'node_uuid': task.node.uuid}) + irmc.elcm.set_bios_configuration(irmc_info, settings) + except irmc.scci.SCCIError as e: + LOG.error('Failed to apply BIOS configuration on node ' + '%(node_uuid)s. Error: %(error)s', + {'node_uuid': task.node.uuid, 'error': e}) + raise exception.IRMCOperationError( + operation='Apply BIOS configuration', error=e) + + @METRICS.timer('IRMCBIOS.factory_reset') + def factory_reset(self, task): + """Reset BIOS configuration to factory default on the given node. + + :param task: a TaskManager instance. + :raises: UnsupportedDriverExtension, if the node's driver doesn't + support BIOS reset. + """ + + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='factory_reset') + + @METRICS.timer('IRMCBIOS.cache_bios_settings') + def cache_bios_settings(self, task): + """Store or update BIOS settings on the given node. + + This method stores BIOS properties to the bios settings db + + :param task: a TaskManager instance. + :raises: IRMCOperationError,if get bios settings failed. + :returns: None if it is complete. + """ + + irmc_info = irmc_common.parse_driver_info(task.node) + node_id = task.node.id + try: + settings = irmc.elcm.get_bios_settings(irmc_info) + except irmc.scci.SCCIError as e: + LOG.error('Failed to retrieve the current BIOS settings for node ' + '%(node)s. Error: %(error)s', {'node': task.node.uuid, + 'error': e}) + raise exception.IRMCOperationError(operation='Cache BIOS settings', + error=e) + create_list, update_list, delete_list, nochange_list = ( + objects.BIOSSettingList.sync_node_setting(task.context, node_id, + settings)) + if len(create_list) > 0: + objects.BIOSSettingList.create(task.context, node_id, create_list) + if len(update_list) > 0: + objects.BIOSSettingList.save(task.context, node_id, update_list) + if len(delete_list) > 0: + delete_names = [setting['name'] for setting in delete_list] + objects.BIOSSettingList.delete(task.context, node_id, + delete_names) diff --git a/ironic/tests/unit/drivers/modules/irmc/test_bios.py b/ironic/tests/unit/drivers/modules/irmc/test_bios.py new file mode 100644 index 0000000000..0ae7292a0c --- /dev/null +++ b/ironic/tests/unit/drivers/modules/irmc/test_bios.py @@ -0,0 +1,152 @@ +# Copyright 2018 FUJITSU LIMITED +# +# 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. + +""" +Test class for IRMC BIOS configuration +""" + +import mock + +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.irmc import bios as irmc_bios +from ironic.drivers.modules.irmc import common as irmc_common +from ironic import objects +from ironic.tests.unit.drivers.modules.irmc import test_common + + +class IRMCBIOSTestCase(test_common.BaseIRMCTest): + + def setUp(self): + super(IRMCBIOSTestCase, self).setUp() + self.config(enabled_bios_interfaces=['irmc']) + + @mock.patch.object(irmc_common, 'parse_driver_info', + autospec=True) + def test_validate(self, parse_driver_info_mock): + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.bios.validate(task) + parse_driver_info_mock.assert_called_once_with(task.node) + + @mock.patch.object(irmc_bios.irmc.elcm, 'set_bios_configuration', + autospec=True) + def test_apply_configuration(self, set_bios_configuration_mock): + settings = [{ + "name": "launch_csm_enabled", + "value": True + }, { + "name": "hyper_threading_enabled", + "value": True + }, { + "name": "cpu_vt_enabled", + "value": True + }] + with task_manager.acquire(self.context, self.node.uuid) as task: + irmc_info = irmc_common.parse_driver_info(task.node) + task.driver.bios.apply_configuration(task, settings) + set_bios_configuration_mock.assert_called_once_with(irmc_info, + settings) + + @mock.patch.object(irmc_bios.irmc.elcm, 'set_bios_configuration', + autospec=True) + def test_apply_configuration_failed(self, set_bios_configuration_mock): + settings = [{ + "name": "launch_csm_enabled", + "value": True + }, { + "name": "hyper_threading_enabled", + "value": True + }, { + "name": "setting", + "value": True + }] + irmc_bios.irmc.scci.SCCIError = Exception + set_bios_configuration_mock.side_effect = Exception + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.IRMCOperationError, + task.driver.bios.apply_configuration, + task, settings) + + def test_factory_reset(self): + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.UnsupportedDriverExtension, + task.driver.bios.factory_reset, task) + + @mock.patch.object(objects.BIOSSettingList, 'sync_node_setting') + @mock.patch.object(objects.BIOSSettingList, 'create') + @mock.patch.object(objects.BIOSSettingList, 'save') + @mock.patch.object(objects.BIOSSettingList, 'delete') + @mock.patch.object(irmc_bios.irmc.elcm, 'get_bios_settings', + autospec=True) + def test_cache_bios_settings(self, get_bios_settings_mock, + delete_mock, save_mock, create_mock, + sync_node_setting_mock): + settings = [{ + "name": "launch_csm_enabled", + "value": True + }, { + "name": "hyper_threading_enabled", + "value": True + }, { + "name": "cpu_vt_enabled", + "value": True + }] + with task_manager.acquire(self.context, self.node.uuid) as task: + irmc_info = irmc_common.parse_driver_info(task.node) + get_bios_settings_mock.return_value = settings + sync_node_setting_mock.return_value = \ + ( + [ + { + "name": "launch_csm_enabled", + "value": True + }], + [ + { + "name": "hyper_threading_enabled", + "value": True + }], + [ + { + "name": "cpu_vt_enabled", + "value": True + }], + [] + ) + task.driver.bios.cache_bios_settings(task) + get_bios_settings_mock.assert_called_once_with(irmc_info) + sync_node_setting_mock.assert_called_once_with(task.context, + task.node.id, + settings) + create_mock.assert_called_once_with( + task.context, task.node.id, + sync_node_setting_mock.return_value[0]) + save_mock.assert_called_once_with( + task.context, task.node.id, + sync_node_setting_mock.return_value[1]) + delete_names = \ + [setting['name'] for setting in + sync_node_setting_mock.return_value[2]] + delete_mock.assert_called_once_with(task.context, task.node.id, + delete_names) + + @mock.patch.object(irmc_bios.irmc.elcm, 'get_bios_settings', + autospec=True) + def test_cache_bios_settings_failed(self, get_bios_settings_mock): + irmc_bios.irmc.scci.SCCIError = Exception + get_bios_settings_mock.side_effect = Exception + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaises(exception.IRMCOperationError, + task.driver.bios.cache_bios_settings, + task) diff --git a/ironic/tests/unit/drivers/modules/irmc/test_common.py b/ironic/tests/unit/drivers/modules/irmc/test_common.py index 586c8cb9fa..11c0da4463 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_common.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_common.py @@ -39,6 +39,7 @@ class BaseIRMCTest(db_base.DbTestCase): self.config(enabled_hardware_types=['irmc', 'fake-hardware'], enabled_power_interfaces=['irmc', 'fake'], enabled_management_interfaces=['irmc', 'fake'], + enabled_bios_interfaces=['irmc', 'no-bios', 'fake'], enabled_boot_interfaces=[self.boot_interface, 'fake'], enabled_inspect_interfaces=['irmc', 'no-inspect', 'fake']) self.info = db_utils.get_test_irmc_info() diff --git a/ironic/tests/unit/drivers/test_irmc.py b/ironic/tests/unit/drivers/test_irmc.py index 0f712c56c2..af5c8dffef 100644 --- a/ironic/tests/unit/drivers/test_irmc.py +++ b/ironic/tests/unit/drivers/test_irmc.py @@ -21,6 +21,7 @@ from ironic.drivers import irmc from ironic.drivers.modules import agent from ironic.drivers.modules import inspector from ironic.drivers.modules import ipmitool +from ironic.drivers.modules.irmc import bios as irmc_bios from ironic.drivers.modules.irmc import raid from ironic.drivers.modules import iscsi_deploy from ironic.drivers.modules import noop @@ -42,7 +43,8 @@ class IRMCHardwareTestCase(db_base.DbTestCase): enabled_management_interfaces=['irmc'], enabled_power_interfaces=['irmc', 'ipmitool'], enabled_raid_interfaces=['no-raid', 'agent', 'irmc'], - enabled_rescue_interfaces=['no-rescue', 'agent']) + enabled_rescue_interfaces=['no-rescue', 'agent'], + enabled_bios_interfaces=['irmc', 'no-bios', 'fake']) def test_default_interfaces(self): node = obj_utils.create_test_node(self.context, driver='irmc') @@ -63,6 +65,8 @@ class IRMCHardwareTestCase(db_base.DbTestCase): noop.NoRAID) self.assertIsInstance(task.driver.rescue, noop.NoRescue) + self.assertIsInstance(task.driver.bios, + irmc_bios.IRMCBIOS) def test_override_with_inspector(self): self.config(enabled_inspect_interfaces=['inspector', 'irmc']) @@ -157,3 +161,27 @@ class IRMCHardwareTestCase(db_base.DbTestCase): raid.IRMCRAID) self.assertIsInstance(task.driver.rescue, agent.AgentRescue) + + def test_override_with_bios_configuration(self): + node = obj_utils.create_test_node( + self.context, driver='irmc', + deploy_interface='direct', + rescue_interface='agent', + bios_interface='irmc') + with task_manager.acquire(self.context, node.id) as task: + self.assertIsInstance(task.driver.boot, + irmc.boot.IRMCVirtualMediaBoot) + self.assertIsInstance(task.driver.console, + ipmitool.IPMISocatConsole) + self.assertIsInstance(task.driver.deploy, + agent.AgentDeploy) + self.assertIsInstance(task.driver.inspect, + irmc.inspect.IRMCInspect) + self.assertIsInstance(task.driver.management, + irmc.management.IRMCManagement) + self.assertIsInstance(task.driver.power, + irmc.power.IRMCPower) + self.assertIsInstance(task.driver.bios, + irmc_bios.IRMCBIOS) + self.assertIsInstance(task.driver.rescue, + agent.AgentRescue) diff --git a/releasenotes/notes/irmc-manual-clean-bios-configuration-1ad24831501456d5.yaml b/releasenotes/notes/irmc-manual-clean-bios-configuration-1ad24831501456d5.yaml new file mode 100644 index 0000000000..fd5abce8ff --- /dev/null +++ b/releasenotes/notes/irmc-manual-clean-bios-configuration-1ad24831501456d5.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Adds new ``bios`` interface to ``irmc`` hardware type. And provide + out-of-band BIOS configuration solution for iRMC driver which makes + the functionality available via manual cleaning. \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index eb81574e3a..131f4432e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,6 +54,7 @@ ironic.dhcp = ironic.hardware.interfaces.bios = fake = ironic.drivers.modules.fake:FakeBIOS + irmc = ironic.drivers.modules.irmc.bios:IRMCBIOS no-bios = ironic.drivers.modules.noop:NoBIOS ironic.hardware.interfaces.boot =