From 289de798cc77391aa9cb49592f3dca996cb9e832 Mon Sep 17 00:00:00 2001 From: kesper Date: Mon, 10 Jul 2017 11:48:21 +0000 Subject: [PATCH] Redfish: Adding the ability to set boot mode and devices This commits adds new function 'set_pending_boot_mode', 'update_persistent_boot' and 'set_one_time_boot' to the server Change-Id: I7609eac74143f4b529b011ed1cbab96b578770b8 --- proliantutils/ilo/client.py | 4 +- proliantutils/redfish/redfish.py | 83 ++++++++++++++++- .../redfish/resources/system/bios.py | 16 ++++ .../redfish/resources/system/constants.py | 7 ++ .../redfish/resources/system/system.py | 60 ++++++++++++ .../tests/redfish/json_samples/bios_boot.json | 11 ++- .../redfish/resources/system/test_bios.py | 17 ++++ .../redfish/resources/system/test_system.py | 92 +++++++++++++++++++ proliantutils/tests/redfish/test_redfish.py | 69 ++++++++++++++ 9 files changed, 355 insertions(+), 4 deletions(-) diff --git a/proliantutils/ilo/client.py b/proliantutils/ilo/client.py index ecae5fa6..7db735e1 100644 --- a/proliantutils/ilo/client.py +++ b/proliantutils/ilo/client.py @@ -72,8 +72,10 @@ SUPPORTED_REDFISH_METHODS = [ 'insert_virtual_media', 'set_vm_status' 'update_firmware', - 'set_vm_status', 'get_persistent_boot_device', + 'set_one_time_boot', + 'update_persistent_boot', + 'set_pending_boot_mode', ] LOG = log.get_logger(__name__) diff --git a/proliantutils/redfish/redfish.py b/proliantutils/redfish/redfish.py index b1f6b11d..2720f581 100644 --- a/proliantutils/redfish/redfish.py +++ b/proliantutils/redfish/redfish.py @@ -485,7 +485,88 @@ class RedfishOperations(operations.IloOperations): return PERSISTENT_BOOT_MAP.get(boot_device) except sushy.exceptions.SushyError as e: msg = (self._("The Redfish controller is unable to get " - "persistent boot device.' Error %(error)s") % + "persistent boot device. Error %(error)s") % {'error': str(e)}) LOG.debug(msg) raise exception.IloError(msg) + + def set_pending_boot_mode(self, boot_mode): + """Sets the boot mode of the system for next boot. + + :param boot_mode: either 'uefi' or 'legacy'. + :raises: IloInvalidInputError, on an invalid input. + :raises: IloError, on an error from iLO. + """ + sushy_system = self._get_sushy_system(PROLIANT_SYSTEM_ID) + + if boot_mode.upper() not in BOOT_MODE_MAP_REV.keys(): + msg = (('Invalid Boot mode: "%(boot_mode)s" specified, valid boot ' + 'modes are either "uefi" or "legacy"') + % {'boot_mode': boot_mode}) + raise exception.IloInvalidInputError(msg) + + try: + sushy_system.bios_settings.pending_settings.set_pending_boot_mode( + BOOT_MODE_MAP_REV.get(boot_mode.upper())) + except sushy.exceptions.SushyError as e: + msg = (self._('The Redfish controller failed to set ' + 'pending boot mode to %(boot_mode)s. ' + 'Error: %(error)s') % + {'boot_mode': boot_mode, 'error': str(e)}) + LOG.debug(msg) + raise exception.IloError(msg) + + def update_persistent_boot(self, devices=[], mac=None): + """Changes the persistent boot device order for the host + + :param devices: ordered list of boot devices + :param mac: intiator mac address, mandatory for iSCSI uefi boot + :raises: IloError, on an error from iLO. + :raises: IloInvalidInputError, if the given input is not valid. + """ + sushy_system = self._get_sushy_system(PROLIANT_SYSTEM_ID) + # Check if the input is valid + for item in devices: + if item.upper() not in DEVICE_COMMON_TO_REDFISH: + msg = (self._('Invalid input "%(device)s". Valid devices: ' + 'NETWORK, HDD, ISCSI or CDROM.') % + {'device': item}) + raise exception.IloInvalidInputError(msg) + + try: + sushy_system.update_persistent_boot( + devices, persistent=True, mac=mac) + except sushy.exceptions.SushyError as e: + msg = (self._('The Redfish controller failed to update ' + 'persistent boot device %(devices)s.' + 'Error: %(error)s') % + {'devices': devices, 'error': str(e)}) + LOG.debug(msg) + raise exception.IloError(msg) + + def set_one_time_boot(self, device, mac=None): + """Configures a single boot from a specific device. + + :param device: Device to be set as a one time boot device + :param mac: intiator mac address, optional parameter + :raises: IloError, on an error from iLO. + :raises: IloInvalidInputError, if the given input is not valid. + """ + sushy_system = self._get_sushy_system(PROLIANT_SYSTEM_ID) + # Check if the input is valid + if device.upper() not in DEVICE_COMMON_TO_REDFISH: + msg = (self._('Invalid input "%(device)s". Valid devices: ' + 'NETWORK, HDD, ISCSI or CDROM.') % + {'device': device}) + raise exception.IloInvalidInputError(msg) + + try: + sushy_system.update_persistent_boot( + [device], persistent=False, mac=mac) + except sushy.exceptions.SushyError as e: + msg = (self._('The Redfish controller failed to set ' + 'one time boot device %(device)s. ' + 'Error: %(error)s') % + {'device': device, 'error': str(e)}) + LOG.debug(msg) + raise exception.IloError(msg) diff --git a/proliantutils/redfish/resources/system/bios.py b/proliantutils/redfish/resources/system/bios.py index 0988ada4..85ad4d8f 100644 --- a/proliantutils/redfish/resources/system/bios.py +++ b/proliantutils/redfish/resources/system/bios.py @@ -17,6 +17,7 @@ from sushy.resources import base from proliantutils import exception from proliantutils import log +from proliantutils.redfish.resources.system import constants as sys_cons from proliantutils.redfish.resources.system import mappings from proliantutils.redfish import utils @@ -75,6 +76,21 @@ class BIOSPendingSettings(base.ResourceBase): boot_mode = base.MappedField(["Attributes", "BootMode"], mappings.GET_BIOS_BOOT_MODE_MAP) + def set_pending_boot_mode(self, boot_mode): + """Sets the boot mode of the system for next boot. + + :param boot_mode: either sys_cons.BIOS_BOOT_MODE_LEGACY_BIOS, + sys_cons.BIOS_BOOT_MODE_UEFI. + """ + bios_properties = {} + bios_properties['BootMode'] = ( + mappings.GET_BIOS_BOOT_MODE_MAP_REV.get(boot_mode)) + + if boot_mode == sys_cons.BIOS_BOOT_MODE_UEFI: + bios_properties['UefiOptimizedBoot'] = 'Enabled' + + self._conn.patch(self._path, bios_properties) + class BIOSBootSettings(base.ResourceBase): diff --git a/proliantutils/redfish/resources/system/constants.py b/proliantutils/redfish/resources/system/constants.py index 8f866ac9..1a2ff12f 100644 --- a/proliantutils/redfish/resources/system/constants.py +++ b/proliantutils/redfish/resources/system/constants.py @@ -23,3 +23,10 @@ PUSH_POWER_BUTTON_PRESS_AND_HOLD = 'press and hold' BIOS_BOOT_MODE_LEGACY_BIOS = 'legacy bios' BIOS_BOOT_MODE_UEFI = 'uefi' + +# Persistent boot device for set + +BOOT_SOURCE_TARGET_CD = 'Cd' +BOOT_SOURCE_TARGET_PXE = 'Pxe' +BOOT_SOURCE_TARGET_UEFI_TARGET = 'UefiTarget' +BOOT_SOURCE_TARGET_HDD = 'Hdd' diff --git a/proliantutils/redfish/resources/system/system.py b/proliantutils/redfish/resources/system/system.py index ae658c30..7c913afa 100644 --- a/proliantutils/redfish/resources/system/system.py +++ b/proliantutils/redfish/resources/system/system.py @@ -14,17 +14,26 @@ __author__ = 'HPE' +import sushy from sushy.resources import base from sushy.resources.system import system from proliantutils import exception from proliantutils import log from proliantutils.redfish.resources.system import bios +from proliantutils.redfish.resources.system import constants as sys_cons from proliantutils.redfish.resources.system import mappings from proliantutils.redfish import utils LOG = log.get_logger(__name__) +PERSISTENT_BOOT_DEVICE_MAP = { + 'CDROM': sys_cons.BOOT_SOURCE_TARGET_CD, + 'NETWORK': sys_cons.BOOT_SOURCE_TARGET_PXE, + 'ISCSI': sys_cons.BOOT_SOURCE_TARGET_UEFI_TARGET, + 'HDD': sys_cons.BOOT_SOURCE_TARGET_HDD +} + class PowerButtonActionField(base.CompositeField): allowed_values = base.Field('PushType@Redfish.AllowableValues', @@ -95,3 +104,54 @@ class HPESystem(system.System): redfish_version=self.redfish_version) return self._bios_settings + + def update_persistent_boot(self, devices=[], persistent=False, + mac=None): + """Changes the persistent boot device order in BIOS boot mode for host + + Note: It uses first boot device from the devices and ignores rest. + + :param devices: ordered list of boot devices + :param persistent: Boolean flag to indicate if the device to be set as + a persistent boot device + :param mac: intiator mac address, mandotory for iSCSI uefi boot + :raises: IloError, on an error from iLO. + :raises: IloInvalidInputError, if the given input is not valid. + """ + new_device = devices[0] + tenure = 'Continuous' if persistent else 'Once' + + try: + boot_sources = self.bios_settings.boot_settings.boot_sources + except sushy.exceptions.SushyError: + msg = ('The BIOS Boot Settings was not found.') + raise exception.IloError(msg) + + if devices[0].upper() in PERSISTENT_BOOT_DEVICE_MAP: + new_device = PERSISTENT_BOOT_DEVICE_MAP[devices[0].upper()] + + new_boot_settings = {} + if new_device is 'UefiTarget': + if not mac: + msg = ('Mac is needed for iscsi uefi boot') + raise exception.IloInvalidInputError(msg) + + boot_string = None + for boot_source in boot_sources: + if(mac.upper() in boot_source['UEFIDevicePath'] and + 'iSCSI' in boot_source['UEFIDevicePath']): + boot_string = boot_source['StructuredBootString'] + break + + if not boot_string: + msg = ('MAC provided "%s" is Invalid' % mac) + raise exception.IloInvalidInputError(msg) + + uefi_boot_settings = {} + uefi_boot_settings['Boot'] = ( + {'UefiTargetBootSourceOverride': boot_string}) + self._conn.patch(self._path, uefi_boot_settings) + + new_boot_settings['Boot'] = {'BootSourceOverrideEnabled': tenure, + 'BootSourceOverrideTarget': new_device} + self._conn.patch(self._path, new_boot_settings) diff --git a/proliantutils/tests/redfish/json_samples/bios_boot.json b/proliantutils/tests/redfish/json_samples/bios_boot.json index e8892d18..37f22024 100644 --- a/proliantutils/tests/redfish/json_samples/bios_boot.json +++ b/proliantutils/tests/redfish/json_samples/bios_boot.json @@ -105,7 +105,14 @@ "CorrelatableID": "PciRoot(0x0)/Pci(0x1C,0x4)/Pci(0x0,0x4)/USB(0x2,0x0)", "StructuredBootString": "FD.Virtual.4.1", "UEFIDevicePath": "PciRoot(0x0)/Pci(0x1C,0x4)/Pci(0x0,0x4)/USB(0x2,0x0)" - } + }, + { + + "BootString": "Embedded LOM 1 Port 1 : HP Ethernet 1Gb 2-port 361i Adapter - NIC (iSCSI IPv4) ", + "CorrelatableID": "PciRoot(0x0)/Pci(0x2,0x3)/Pci(0x0,0x0)", + "StructuredBootString": "NIC.LOM.1.1.iSCSI", + "UEFIDevicePath": "PciRoot(0x0)/Pci(0x2,0x3)/Pci(0x0,0x0)/MAC (C4346BB7EF30,0x1)/IPv4(0.0.0.0)/iSCSI(iqn.2016.org.de:,0x1,0x0,None,None,None,TCP)" + } ], "DefaultBootOrder": [ @@ -225,7 +232,7 @@ "Id": "boot", "Name": "Boot Order Current Settings" }, - + "BIOS_boot_without_boot_sources": { "@Redfish.Settings": { diff --git a/proliantutils/tests/redfish/resources/system/test_bios.py b/proliantutils/tests/redfish/resources/system/test_bios.py index 78f3015b..ba8965c6 100644 --- a/proliantutils/tests/redfish/resources/system/test_bios.py +++ b/proliantutils/tests/redfish/resources/system/test_bios.py @@ -97,6 +97,23 @@ class BIOSPendingSettingsTestCase(testtools.TestCase): self.assertEqual(sys_cons.BIOS_BOOT_MODE_UEFI, self.bios_settings_inst.boot_mode) + def test_set_pending_boot_mode_bios(self): + self.bios_settings_inst.set_pending_boot_mode( + sys_cons.BIOS_BOOT_MODE_LEGACY_BIOS) + data = {} + data['BootMode'] = 'LegacyBios' + self.bios_settings_inst._conn.patch.assert_called_once_with( + '/redfish/v1/Systems/1/bios/settings', data) + + def test_set_pending_boot_mode_uefi(self): + self.bios_settings_inst.set_pending_boot_mode( + sys_cons.BIOS_BOOT_MODE_UEFI) + data = {} + data['UefiOptimizedBoot'] = 'Enabled' + data['BootMode'] = 'Uefi' + self.bios_settings_inst._conn.patch.assert_called_once_with( + '/redfish/v1/Systems/1/bios/settings', data) + class BIOSBootSettingsTestCase(testtools.TestCase): diff --git a/proliantutils/tests/redfish/resources/system/test_system.py b/proliantutils/tests/redfish/resources/system/test_system.py index 4bf015ac..2f947950 100644 --- a/proliantutils/tests/redfish/resources/system/test_system.py +++ b/proliantutils/tests/redfish/resources/system/test_system.py @@ -16,6 +16,7 @@ import json import mock +import sushy import testtools from proliantutils import exception @@ -79,3 +80,94 @@ class HPESystemTestCase(testtools.TestCase): self.assertIs(actual_bios, self.sys_inst.bios_settings) self.conn.get.return_value.json.assert_not_called() + + def test_update_persistent_boot_persistent(self): + with open('proliantutils/tests/redfish/' + 'json_samples/bios.json', 'r') as f: + bios_mock = json.loads(f.read()) + with open('proliantutils/tests/redfish/' + 'json_samples/bios_boot.json', 'r') as g: + boot_mock = json.loads(g.read()) + self.conn.get.return_value.json.side_effect = [bios_mock['Default'], + boot_mock['Default']] + self.sys_inst.update_persistent_boot(['CDROM'], True) + data = {} + data['Boot'] = {'BootSourceOverrideEnabled': 'Continuous', + 'BootSourceOverrideTarget': 'Cd'} + self.sys_inst._conn.patch.assert_called_once_with( + '/redfish/v1/Systems/1', data) + + def test_update_persistent_boot_not_persistent(self): + with open('proliantutils/tests/redfish/' + 'json_samples/bios.json', 'r') as f: + bios_mock = json.loads(f.read()) + with open('proliantutils/tests/redfish/' + 'json_samples/bios_boot.json', 'r') as f: + boot_mock = json.loads(f.read()) + self.conn.get.return_value.json.side_effect = [bios_mock['Default'], + boot_mock['Default']] + self.sys_inst.update_persistent_boot(['CDROM'], False) + data = {} + data['Boot'] = {'BootSourceOverrideEnabled': 'Once', + 'BootSourceOverrideTarget': 'Cd'} + self.sys_inst._conn.patch.assert_called_once_with( + '/redfish/v1/Systems/1', data) + + def test_update_persistent_boot_uefi_target(self): + with open('proliantutils/tests/redfish/' + 'json_samples/bios.json', 'r') as f: + bios_mock = json.loads(f.read()) + with open('proliantutils/tests/redfish/' + 'json_samples/bios_boot.json', 'r') as f: + boot_mock = json.loads(f.read()) + self.conn.get.return_value.json.side_effect = [bios_mock['Default'], + boot_mock['Default']] + self.sys_inst.update_persistent_boot(['ISCSI'], persistent=True, + mac='C4346BB7EF30') + data = {} + data['Boot'] = {'UefiTargetBootSourceOverride': 'NIC.LOM.1.1.iSCSI'} + new_data = {} + new_data['Boot'] = {'BootSourceOverrideEnabled': 'Continuous', + 'BootSourceOverrideTarget': 'UefiTarget'} + calls = [mock.call('/redfish/v1/Systems/1', data), + mock.call('/redfish/v1/Systems/1', new_data)] + self.sys_inst._conn.patch.assert_has_calls(calls) + + def test_update_persistent_boot_uefi_target_without_mac(self): + with open('proliantutils/tests/redfish/' + 'json_samples/bios.json', 'r') as f: + bios_mock = json.loads(f.read()) + with open('proliantutils/tests/redfish/' + 'json_samples/bios_boot.json', 'r') as f: + boot_mock = json.loads(f.read()) + self.conn.get.return_value.json.side_effect = [bios_mock['Default'], + boot_mock['Default']] + self.assertRaisesRegex( + exception.IloInvalidInputError, + 'Mac is needed for iscsi uefi boot', + self.sys_inst.update_persistent_boot, ['ISCSI'], True, None) + + def test_update_persistent_boot_uefi_target_invalid_mac(self): + with open('proliantutils/tests/redfish/' + 'json_samples/bios.json', 'r') as f: + bios_mock = json.loads(f.read()) + with open('proliantutils/tests/redfish/' + 'json_samples/bios_boot.json', 'r') as f: + boot_mock = json.loads(f.read()) + self.conn.get.return_value.json.side_effect = [bios_mock['Default'], + boot_mock['Default']] + self.assertRaisesRegex( + exception.IloInvalidInputError, + 'MAC provided "12345678" is Invalid', + self.sys_inst.update_persistent_boot, ['ISCSI'], True, '12345678') + + def test_update_persistent_boot_fail(self): + with open('proliantutils/tests/redfish/' + 'json_samples/bios.json', 'r') as f: + bios_mock = json.loads(f.read()) + self.conn.get.return_value.json.side_effect = ( + [bios_mock['Default'], sushy.exceptions.SushyError]) + self.assertRaisesRegex( + exception.IloError, + 'The BIOS Boot Settings was not found.', + self.sys_inst.update_persistent_boot, ['CDROM'], True, None) diff --git a/proliantutils/tests/redfish/test_redfish.py b/proliantutils/tests/redfish/test_redfish.py index 7203e3b8..28fe1c57 100644 --- a/proliantutils/tests/redfish/test_redfish.py +++ b/proliantutils/tests/redfish/test_redfish.py @@ -504,3 +504,72 @@ class RedfishOperationsTestCase(testtools.TestCase): exception.IloError, 'The Redfish controller is unable to get persistent boot device.', self.rf_client.get_persistent_boot_device) + + @mock.patch.object(redfish.RedfishOperations, '_get_sushy_system') + def test_set_pending_boot_mode(self, get_system_mock): + self.rf_client.set_pending_boot_mode('uefi') + (get_system_mock.return_value. + bios_settings.pending_settings.set_pending_boot_mode. + assert_called_once_with('uefi')) + + def test_set_pending_boot_mode_invalid_input(self): + self.assertRaisesRegex( + exception.IloInvalidInputError, + 'Invalid Boot mode: "test" specified', + self.rf_client.set_pending_boot_mode, 'test') + + @mock.patch.object(redfish.RedfishOperations, '_get_sushy_system') + def test_set_pending_boot_mode_fail(self, get_system_mock): + (get_system_mock.return_value.bios_settings. + pending_settings.set_pending_boot_mode.side_effect) = ( + sushy.exceptions.SushyError) + self.assertRaisesRegex( + exception.IloError, + 'The Redfish controller failed to set pending boot mode.', + self.rf_client.set_pending_boot_mode, 'uefi') + + @mock.patch.object(redfish.RedfishOperations, '_get_sushy_system') + def test_update_persistent_boot(self, get_system_mock): + self.rf_client.update_persistent_boot(['NETWORK']) + (get_system_mock.return_value.update_persistent_boot. + assert_called_once_with(['NETWORK'], mac=None, persistent=True)) + + def test_update_persistent_boot_invalid_input(self): + self.assertRaisesRegex( + exception.IloInvalidInputError, + ('Invalid input "test". Valid devices: NETWORK, ' + 'HDD, ISCSI or CDROM.'), + self.rf_client.update_persistent_boot, ['test']) + + @mock.patch.object(redfish.RedfishOperations, '_get_sushy_system') + def test_update_persistent_boot_fail(self, get_system_mock): + get_system_mock.return_value.update_persistent_boot.side_effect = ( + sushy.exceptions.SushyError) + self.assertRaisesRegex( + exception.IloError, + 'The Redfish controller failed to update persistent boot.', + self.rf_client.update_persistent_boot, + ['NETWORK']) + + @mock.patch.object(redfish.RedfishOperations, '_get_sushy_system') + def test_set_one_time_boot(self, get_system_mock): + self.rf_client.set_one_time_boot('CDROM') + (get_system_mock.return_value.update_persistent_boot. + assert_called_once_with(['CDROM'], mac=None, persistent=False)) + + def test_set_one_time_boot_invalid_input(self): + self.assertRaisesRegex( + exception.IloInvalidInputError, + ('Invalid input "test". Valid devices: NETWORK, ' + 'HDD, ISCSI or CDROM.'), + self.rf_client.set_one_time_boot, 'test') + + @mock.patch.object(redfish.RedfishOperations, '_get_sushy_system') + def test_set_one_time_boot_fail(self, get_system_mock): + get_system_mock.return_value.update_persistent_boot.side_effect = ( + sushy.exceptions.SushyError) + self.assertRaisesRegex( + exception.IloError, + 'The Redfish controller failed to set one time boot.', + self.rf_client.set_one_time_boot, + 'CDROM')