diff --git a/driver-requirements.txt b/driver-requirements.txt index c7cbae4c3a..e8cb3f36ae 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -4,7 +4,7 @@ # python projects they should package as optional dependencies for Ironic. # These are available on pypi -proliantutils>=2.5.0 +proliantutils>=2.6.0 pysnmp>=4.3.0,<5.0.0 python-ironic-inspector-client>=1.5.0 python-oneviewclient<3.0.0,>=2.5.2 diff --git a/ironic/drivers/ilo.py b/ironic/drivers/ilo.py index 413a92f6f1..4cf4a6317e 100644 --- a/ironic/drivers/ilo.py +++ b/ironic/drivers/ilo.py @@ -16,6 +16,7 @@ iLO Driver for managing HP Proliant Gen8 and above servers. """ from ironic.drivers import generic +from ironic.drivers.modules.ilo import bios from ironic.drivers.modules.ilo import boot from ironic.drivers.modules.ilo import console from ironic.drivers.modules.ilo import inspect @@ -38,6 +39,11 @@ class IloHardware(generic.GenericHardware): """List of supported boot interfaces.""" return [boot.IloVirtualMediaBoot, boot.IloPXEBoot] + @property + def supported_bios_interfaces(self): + """List of supported bios interfaces.""" + return [bios.IloBIOS, noop.NoBIOS] + @property def supported_console_interfaces(self): """List of supported console interfaces.""" diff --git a/ironic/drivers/modules/ilo/bios.py b/ironic/drivers/modules/ilo/bios.py new file mode 100644 index 0000000000..3d6359914e --- /dev/null +++ b/ironic/drivers/modules/ilo/bios.py @@ -0,0 +1,157 @@ +# Copyright 2018 Hewlett-Packard Development Company, L.P. +# +# 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. +""" +iLO BIOS Interface +""" + +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.common.i18n import _ +from ironic.drivers import base +from ironic.drivers.modules.ilo import common as ilo_common +from ironic import objects + +LOG = logging.getLogger(__name__) + +METRICS = metrics_utils.get_metrics_logger(__name__) + +ilo_error = importutils.try_import('proliantutils.exception') + + +class IloBIOS(base.BIOSInterface): + + def get_properties(self): + return ilo_common.REQUIRED_PROPERTIES + + @METRICS.timer('IloBIOS.validate') + def validate(self, task): + """Check that 'driver_info' contains required ILO credentials. + + Validates whether the 'driver_info' property of the supplied + task's node contains the required credentials information. + + :param task: a task from TaskManager. + :raises: InvalidParameterValue if required iLO parameters + are not valid. + :raises: MissingParameterValue if a required parameter is missing. + """ + ilo_common.parse_driver_info(task.node) + + @METRICS.timer('IloBIOS.apply_configuration') + @base.clean_step(priority=0, abortable=False, argsinfo={ + 'settings': { + 'description': "Dictionary with current BIOS configuration.", + 'required': True + } + }) + def apply_configuration(self, task, settings): + """Applies the provided configuration on the node. + + :param task: a TaskManager instance. + :param settings: Settings intended to be applied on the node. + :raises: NodeCleaningFailure when applying the configuration on + the node fails. + + """ + data = {} + for setting in settings: + data.update({setting['name']: setting['value']}) + + node = task.node + + errmsg = _("Clean step \"apply_configuration\" failed " + "on node %(node)s with error: %(err)s") + + try: + ilo_object = ilo_common.get_ilo_object(node) + ilo_object.set_bios_settings(data) + except (exception.MissingParameterValue, + exception.InvalidParameterValue, + ilo_error.IloError, + ilo_error.IloCommandNotSupportedError) as ir_exception: + raise exception.NodeCleaningFailure( + errmsg % {'node': node.uuid, 'err': ir_exception}) + + @METRICS.timer('IloBIOS.factory_reset') + @base.clean_step(priority=0, abortable=False) + def factory_reset(self, task): + """Reset the BIOS settings to factory configuration. + + :param task: a TaskManager instance. + :raises: NodeCleaningFailure when IloError or any other exception + is caught. + + """ + node = task.node + + errmsg = _("Clean step \"factory_reset\" failed " + "on node %(node)s with error: %(err)s") + + try: + ilo_object = ilo_common.get_ilo_object(node) + ilo_object.reset_bios_to_default() + except (exception.MissingParameterValue, + exception.InvalidParameterValue, + ilo_error.IloError, + ilo_error.IloCommandNotSupportedError) as ir_exception: + raise exception.NodeCleaningFailure( + errmsg % {'node': node.uuid, 'err': ir_exception}) + + @METRICS.timer('IloBIOS.cache_bios_settings') + def cache_bios_settings(self, task): + """Store the BIOS settings in the database. + + :param task: a TaskManager instance. + :raises: NodeCleaningFailure when IloError or any other exception + is caught. + + """ + node = task.node + nodeid = node.id + + errmsg = _("Caching BIOS settings failed " + "on node %(node)s with error: %(err)s") + try: + ilo_object = ilo_common.get_ilo_object(node) + bios_settings = ilo_object.get_pending_bios_settings() + + except (exception.MissingParameterValue, + exception.InvalidParameterValue, + ilo_error.IloError, + ilo_error.IloCommandNotSupportedError) as ir_exception: + raise exception.NodeCleaningFailure( + errmsg % {'node': node.uuid, 'err': ir_exception}) + + fmt_bios_settings = [] + + for setting in bios_settings: + fmt_bios_settings.append({"name": setting, + "value": bios_settings[setting]}) + + create_list, update_list, delete_list, nochange_list = ( + objects.BIOSSettingList.sync_node_setting(task.context, + nodeid, + fmt_bios_settings)) + if len(create_list) > 0: + objects.BIOSSettingList.create(task.context, nodeid, create_list) + if len(update_list) > 0: + objects.BIOSSettingList.save(task.context, nodeid, update_list) + if len(delete_list) > 0: + delete_name_list = [delete_name.get( + "name") for delete_name in delete_list] + objects.BIOSSettingList.delete( + task.context, nodeid, delete_name_list) diff --git a/ironic/tests/unit/drivers/modules/ilo/test_bios.py b/ironic/tests/unit/drivers/modules/ilo/test_bios.py new file mode 100644 index 0000000000..e285d879c6 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/ilo/test_bios.py @@ -0,0 +1,287 @@ +# Copyright 2018 Hewlett-Packard Development Company, L.P. +# 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. + +"""Test class for IloPower module.""" + +import mock +from oslo_config import cfg +from oslo_utils import importutils + +from ironic.common import exception +from ironic.conductor import task_manager +from ironic.drivers.modules.ilo import common as ilo_common +from ironic import objects +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.drivers.modules.ilo import test_common + +ilo_error = importutils.try_import('proliantutils.exception') + +INFO_DICT = db_utils.get_test_ilo_info() +CONF = cfg.CONF + + +class IloBiosTestCase(test_common.BaseIloTest): + + def test_get_properties(self): + expected = ilo_common.REQUIRED_PROPERTIES + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(expected, task.driver.bios.get_properties()) + + @mock.patch.object(ilo_common, 'parse_driver_info', spec_set=True, + autospec=True) + def test_validate(self, mock_drvinfo): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.bios.validate(task) + mock_drvinfo.assert_called_once_with(task.node) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def _test_ilo_error(self, error_type, + test_methods_not_called, method_details, ilo_mock): + error_dict = { + "missing_parameter": exception.MissingParameterValue, + "invalid_parameter": exception.InvalidParameterValue + } + + exc = error_dict.get(error_type)('error') + ilo_mock.side_effect = exc + method = method_details.get("name") + args = method_details.get("args") + self.assertRaises(exception.NodeCleaningFailure, + method, + *args) + for test_method in test_methods_not_called: + eval("ilo_mock.return_value.%s.assert_not_called()" % ( + test_method)) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_apply_configuration(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_object_mock = get_ilo_object_mock.return_value + data = [ + { + "name": "SET_A", "value": "VAL_A", + }, + { + "name": "SET_B", "value": "VAL_B", + }, + { + "name": "SET_C", "value": "VAL_C", + }, + { + "name": "SET_D", "value": "VAL_D", + } + ] + task.driver.bios.apply_configuration(task, data) + expected = { + "SET_A": "VAL_A", + "SET_B": "VAL_B", + "SET_C": "VAL_C", + "SET_D": "VAL_D" + } + ilo_object_mock.set_bios_settings.assert_called_once_with(expected) + + def test_apply_configuration_missing_parameter(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mdobj = { + "name": task.driver.bios.apply_configuration, + "args": (task, []) + } + self._test_ilo_error("missing_parameter", ["set_bios_settings"], + mdobj) + + def test_apply_configuration_invalid_parameter(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mdobj = { + "name": task.driver.bios.apply_configuration, + "args": (task, []) + } + self._test_ilo_error("invalid_parameter", ["set_bios_settings"], + mdobj) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_apply_configuration_with_ilo_error(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_object_mock = get_ilo_object_mock.return_value + data = [ + { + "name": "SET_A", "value": "VAL_A", + }, + { + "name": "SET_B", "value": "VAL_B", + }, + ] + exc = ilo_error.IloError('error') + ilo_object_mock.set_bios_settings.side_effect = exc + self.assertRaises(exception.NodeCleaningFailure, + task.driver.bios.apply_configuration, + task, data) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_factory_reset(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_object_mock = get_ilo_object_mock.return_value + task.driver.bios.factory_reset(task) + ilo_object_mock.reset_bios_to_default.assert_called_once_with() + + def test_factory_reset_missing_parameter(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mdobj = { + "name": task.driver.bios.factory_reset, + "args": (task,) + } + self._test_ilo_error("missing_parameter", + ["reset_bios_to_default"], mdobj) + + def test_factory_reset_invalid_parameter(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mdobj = { + "name": task.driver.bios.factory_reset, + "args": (task,) + } + self._test_ilo_error("invalid_parameter", + ["reset_bios_to_default"], mdobj) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_factory_reset_with_ilo_error(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_object_mock = get_ilo_object_mock.return_value + exc = ilo_error.IloError('error') + ilo_object_mock.reset_bios_to_default.side_effect = exc + self.assertRaises(exception.NodeCleaningFailure, + task.driver.bios.factory_reset, task) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_factory_reset_with_unknown_error(self, get_ilo_object_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_object_mock = get_ilo_object_mock.return_value + exc = ilo_error.IloCommandNotSupportedError('error') + ilo_object_mock.reset_bios_to_default.side_effect = exc + self.assertRaises(exception.NodeCleaningFailure, + task.driver.bios.factory_reset, task) + + @mock.patch.object(objects.BIOSSettingList, 'create') + @mock.patch.object(objects.BIOSSettingList, 'save') + @mock.patch.object(objects.BIOSSettingList, 'delete') + @mock.patch.object(objects.BIOSSettingList, 'sync_node_setting') + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_cache_bios_settings(self, get_ilo_object_mock, sync_node_mock, + delete_mock, save_mock, create_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_object_mock = get_ilo_object_mock.return_value + settings = { + "SET_A": True, + "SET_B": True, + "SET_C": True, + "SET_D": True + } + + ilo_object_mock.get_pending_bios_settings.return_value = settings + expected_bios_settings = [ + {"name": "SET_A", "value": True}, + {"name": "SET_B", "value": True}, + {"name": "SET_C", "value": True}, + {"name": "SET_D", "value": True} + ] + sync_node_mock.return_value = ([], [], [], []) + all_settings = ( + [ + {"name": "C_1", "value": "C_1_VAL"}, + {"name": "C_2", "value": "C_2_VAL"} + ], + [ + {"name": "U_1", "value": "U_1_VAL"}, + {"name": "U_2", "value": "U_2_VAL"} + ], + [ + {"name": "D_1", "value": "D_1_VAL"}, + {"name": "D_2", "value": "D_2_VAL"} + ], + [] + ) + sync_node_mock.return_value = all_settings + task.driver.bios.cache_bios_settings(task) + ilo_object_mock.get_pending_bios_settings.assert_called_once_with() + actual_arg = sorted(sync_node_mock.call_args[0][2], + key=lambda x: x.get("name")) + expected_arg = sorted(expected_bios_settings, + key=lambda x: x.get("name")) + self.assertEqual(actual_arg, expected_arg) + create_mock.assert_called_once_with( + self.context, task.node.id, all_settings[0]) + save_mock.assert_called_once_with( + self.context, task.node.id, all_settings[1]) + del_names = [setting.get("name") for setting in all_settings[2]] + delete_mock.assert_called_once_with( + self.context, task.node.id, del_names) + + def test_cache_bios_settings_missing_parameter(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mdobj = { + "name": task.driver.bios.cache_bios_settings, + "args": (task,) + } + self._test_ilo_error("missing_parameter", + ["get_pending_bios_settings"], mdobj) + + def test_cache_bios_settings_invalid_parameter(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mdobj = { + "name": task.driver.bios.cache_bios_settings, + "args": (task,) + } + self._test_ilo_error("invalid_parameter", + ["get_pending_bios_settings"], mdobj) + + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_cache_bios_settings_with_ilo_error(self, get_ilo_object_mock): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_object_mock = get_ilo_object_mock.return_value + exc = ilo_error.IloError('error') + ilo_object_mock.get_pending_bios_settings.side_effect = exc + self.assertRaises(exception.NodeCleaningFailure, + task.driver.bios.cache_bios_settings, task) + + @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True) + def test_cache_bios_settings_with_unknown_error(self, get_ilo_object_mock): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + ilo_object_mock = get_ilo_object_mock.return_value + exc = ilo_error.IloCommandNotSupportedError('error') + ilo_object_mock.get_pending_bios_settings.side_effect = exc + self.assertRaises(exception.NodeCleaningFailure, + task.driver.bios.cache_bios_settings, task) diff --git a/ironic/tests/unit/drivers/modules/ilo/test_common.py b/ironic/tests/unit/drivers/modules/ilo/test_common.py index aadb1a20cc..5299b5a95f 100644 --- a/ironic/tests/unit/drivers/modules/ilo/test_common.py +++ b/ironic/tests/unit/drivers/modules/ilo/test_common.py @@ -62,6 +62,7 @@ class BaseIloTest(db_base.DbTestCase): self.config(enabled_hardware_types=['ilo', 'fake-hardware'], enabled_boot_interfaces=['ilo-pxe', 'ilo-virtual-media', 'fake'], + enabled_bios_interfaces=['ilo', 'no-bios'], enabled_power_interfaces=['ilo', 'fake'], enabled_management_interfaces=['ilo', 'fake'], enabled_inspect_interfaces=['ilo', 'fake', 'no-inspect'], @@ -71,6 +72,7 @@ class BaseIloTest(db_base.DbTestCase): self.node = obj_utils.create_test_node( self.context, uuid=uuidutils.generate_uuid(), driver='ilo', boot_interface=self.boot_interface, + bios_interface='ilo', driver_info=self.info) diff --git a/ironic/tests/unit/drivers/test_ilo.py b/ironic/tests/unit/drivers/test_ilo.py index 5142708239..321ace576f 100644 --- a/ironic/tests/unit/drivers/test_ilo.py +++ b/ironic/tests/unit/drivers/test_ilo.py @@ -32,6 +32,7 @@ class IloHardwareTestCase(db_base.DbTestCase): super(IloHardwareTestCase, self).setUp() self.config(enabled_hardware_types=['ilo'], enabled_boot_interfaces=['ilo-virtual-media', 'ilo-pxe'], + enabled_bios_interfaces=['no-bios', 'ilo'], enabled_console_interfaces=['ilo'], enabled_deploy_interfaces=['iscsi', 'direct'], enabled_inspect_interfaces=['ilo'], @@ -47,6 +48,8 @@ class IloHardwareTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, node.id) as task: self.assertIsInstance(task.driver.boot, ilo.boot.IloVirtualMediaBoot) + self.assertIsInstance(task.driver.bios, + ilo.bios.IloBIOS) self.assertIsInstance(task.driver.console, ilo.console.IloConsoleInterface) self.assertIsInstance(task.driver.deploy, @@ -143,3 +146,22 @@ class IloHardwareTestCase(db_base.DbTestCase): agent.AgentRescue) self.assertIsInstance(task.driver.vendor, ilo.vendor.VendorPassthru) + + def test_override_with_no_bios(self): + node = obj_utils.create_test_node( + self.context, driver='ilo', + boot_interface='ilo-pxe', + bios_interface='no-bios', + deploy_interface='direct', + raid_interface='agent') + with task_manager.acquire(self.context, node.id) as task: + self.assertIsInstance(task.driver.boot, + ilo.boot.IloPXEBoot) + self.assertIsInstance(task.driver.bios, + noop.NoBIOS) + self.assertIsInstance(task.driver.console, + ilo.console.IloConsoleInterface) + self.assertIsInstance(task.driver.deploy, + agent.AgentDeploy) + self.assertIsInstance(task.driver.raid, + agent.AgentRAID) diff --git a/releasenotes/notes/ilo-bios-settings-bc91524c459a4fd9.yaml b/releasenotes/notes/ilo-bios-settings-bc91524c459a4fd9.yaml new file mode 100644 index 0000000000..1a85657072 --- /dev/null +++ b/releasenotes/notes/ilo-bios-settings-bc91524c459a4fd9.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Implements ``bios`` interface for ``ilo`` hardware type. + Adds the list of supported bios interfaces for the `ilo` hardware type. + Adds manual cleaning steps ``apply_configuration`` and ``factory_reset`` + which support managing the BIOS settings for the iLO servers using `ilo` + hardware type. diff --git a/setup.cfg b/setup.cfg index 131f4432e6..ae07551a06 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,6 +54,7 @@ ironic.dhcp = ironic.hardware.interfaces.bios = fake = ironic.drivers.modules.fake:FakeBIOS + ilo = ironic.drivers.modules.ilo.bios:IloBIOS irmc = ironic.drivers.modules.irmc.bios:IRMCBIOS no-bios = ironic.drivers.modules.noop:NoBIOS