diff --git a/ironic/drivers/modules/drac/management.py b/ironic/drivers/modules/drac/management.py index fe65bffeb1..52646ed0ab 100644 --- a/ironic/drivers/modules/drac/management.py +++ b/ironic/drivers/modules/drac/management.py @@ -2,6 +2,7 @@ # # Copyright 2014 Red Hat, Inc. # All Rights Reserved. +# Copyright (c) 2017-2018 Dell Inc. or its subsidiaries. # # 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 @@ -37,15 +38,40 @@ LOG = logging.getLogger(__name__) METRICS = metrics_utils.get_metrics_logger(__name__) +# This dictionary is used to map boot device names between two (2) name +# spaces. The name spaces are: +# +# 1) ironic boot devices +# 2) iDRAC boot sources +# +# Mapping can be performed in both directions. +# +# The keys are ironic boot device types. Each value is a list of strings +# that appear in the identifiers of iDRAC boot sources. +# +# The iDRAC represents boot sources with class DCIM_BootSourceSetting +# [1]. Each instance of that class contains a unique identifier, which +# is called an instance identifier, InstanceID, +# +# An InstanceID contains the Fully Qualified Device Descriptor (FQDD) of +# the physical device that hosts the boot source [2]. +# +# [1] "Dell EMC BIOS and Boot Management Profile", Version 4.0.0, July +# 10, 2017, Section 7.2 "Boot Management", pp. 44-47 -- +# http://en.community.dell.com/techcenter/extras/m/white_papers/20444495/download +# [2] "Lifecycle Controller Version 3.15.15.15 User's Guide", Dell EMC, +# 2017, Table 13, "Easy-to-use Names of System Components", pp. 71-74 -- +# http://topics-cdn.dell.com/pdf/idrac9-lifecycle-controller-v3.15.15.15_users-guide2_en-us.pdf _BOOT_DEVICES_MAP = { - boot_devices.DISK: 'HardDisk', - boot_devices.PXE: 'NIC', - boot_devices.CDROM: 'Optical', + boot_devices.DISK: ['AHCI', 'Disk', 'RAID'], + boot_devices.PXE: ['NIC'], + boot_devices.CDROM: ['Optical'], } -# BootMode constants -PERSISTENT_BOOT_MODE = 'IPL' -NON_PERSISTENT_BOOT_MODE = 'OneTime' +_DRAC_BOOT_MODES = ['Bios', 'Uefi'] + +# BootMode constant +_NON_PERSISTENT_BOOT_MODE = 'OneTime' def _get_boot_device(node, drac_boot_devices=None): @@ -54,19 +80,31 @@ def _get_boot_device(node, drac_boot_devices=None): try: boot_modes = client.list_boot_modes() next_boot_modes = [mode.id for mode in boot_modes if mode.is_next] - if NON_PERSISTENT_BOOT_MODE in next_boot_modes: - next_boot_mode = NON_PERSISTENT_BOOT_MODE + if _NON_PERSISTENT_BOOT_MODE in next_boot_modes: + next_boot_mode = _NON_PERSISTENT_BOOT_MODE else: next_boot_mode = next_boot_modes[0] if drac_boot_devices is None: drac_boot_devices = client.list_boot_devices() - drac_boot_device = drac_boot_devices[next_boot_mode][0] - boot_device = next(key for (key, value) in _BOOT_DEVICES_MAP.items() - if value in drac_boot_device.id) + # It is possible for there to be no boot device. + boot_device = None + + if next_boot_mode in drac_boot_devices: + drac_boot_device = drac_boot_devices[next_boot_mode][0] + + for key, value in _BOOT_DEVICES_MAP.items(): + for id_component in value: + if id_component in drac_boot_device.id: + boot_device = key + break + + if boot_device: + break + return {'boot_device': boot_device, - 'persistent': next_boot_mode == PERSISTENT_BOOT_MODE} + 'persistent': next_boot_mode != _NON_PERSISTENT_BOOT_MODE} except (drac_exceptions.BaseClientException, IndexError) as exc: LOG.error('DRAC driver failed to get next boot mode for ' 'node %(node_uuid)s. Reason: %(error)s.', @@ -74,6 +112,60 @@ def _get_boot_device(node, drac_boot_devices=None): raise exception.DracOperationError(error=exc) +def _get_next_persistent_boot_mode(node): + client = drac_common.get_drac_client(node) + + try: + boot_modes = client.list_boot_modes() + except drac_exceptions.BaseClientException as exc: + LOG.error('DRAC driver failed to get next persistent boot mode for ' + 'node %(node_uuid)s. Reason: %(error)s', + {'node_uuid': node.uuid, 'error': exc}) + raise exception.DracOperationError(error=exc) + + next_persistent_boot_mode = None + for mode in boot_modes: + if mode.is_next and mode.id != _NON_PERSISTENT_BOOT_MODE: + next_persistent_boot_mode = mode.id + break + + if not next_persistent_boot_mode: + message = _('List of boot modes, %(list_boot_modes)s, does not ' + 'contain a persistent mode') % { + 'list_boot_modes': boot_modes} + LOG.error('DRAC driver failed to get next persistent boot mode for ' + 'node %(node_uuid)s. Reason: %(message)s', + {'node_uuid': node.uuid, 'message': message}) + raise exception.DracOperationError(error=message) + + return next_persistent_boot_mode + + +def _is_boot_order_flexibly_programmable(persistent, bios_settings): + return persistent and 'SetBootOrderFqdd1' in bios_settings + + +def _flexibly_program_boot_order(device, drac_boot_mode): + if device == boot_devices.DISK: + if drac_boot_mode == 'Bios': + bios_settings = {'SetBootOrderFqdd1': 'HardDisk.List.1-1'} + else: + # 'Uefi' + bios_settings = { + 'SetBootOrderFqdd1': '*.*.*', # Disks, which are all else + 'SetBootOrderFqdd2': 'NIC.*.*', + 'SetBootOrderFqdd3': 'Optical.*.*', + 'SetBootOrderFqdd4': 'Floppy.*.*', + } + elif device == boot_devices.PXE: + bios_settings = {'SetBootOrderFqdd1': 'NIC.*.*'} + else: + # boot_devices.CDROM + bios_settings = {'SetBootOrderFqdd1': 'Optical.*.*'} + + return bios_settings + + def set_boot_device(node, device, persistent=False): """Set the boot device for a node. @@ -102,16 +194,58 @@ def set_boot_device(node, device, persistent=False): LOG.debug('DRAC already set to boot from %s', device) return - drac_boot_device = next(drac_device.id for drac_device - in drac_boot_devices[PERSISTENT_BOOT_MODE] - if _BOOT_DEVICES_MAP[device] in drac_device.id) + persistent_boot_mode = _get_next_persistent_boot_mode(node) - if persistent: - boot_list = PERSISTENT_BOOT_MODE + drac_boot_device = None + for drac_device in drac_boot_devices[persistent_boot_mode]: + for id_component in _BOOT_DEVICES_MAP[device]: + if id_component in drac_device.id: + drac_boot_device = drac_device.id + break + + if drac_boot_device: + break + + if drac_boot_device: + if persistent: + boot_list = persistent_boot_mode + else: + boot_list = _NON_PERSISTENT_BOOT_MODE + + client.change_boot_device_order(boot_list, drac_boot_device) else: - boot_list = NON_PERSISTENT_BOOT_MODE + # No DRAC boot device of the type requested by the argument + # 'device' is present. This is normal for UEFI boot mode, + # following deployment's writing of the operating system to + # disk. It can also occur when a server has not been + # powered on after a new boot device has been installed. + # + # If the boot order is flexibly programmable, use that to + # attempt to detect and boot from a device of the requested + # type during the next boot. That avoids the need for an + # extra reboot. Otherwise, this function cannot satisfy the + # request, because it was called with an invalid device. + bios_settings = client.list_bios_settings(by_name=True) + if _is_boot_order_flexibly_programmable(persistent, bios_settings): + drac_boot_mode = bios_settings['BootMode'].current_value + if drac_boot_mode not in _DRAC_BOOT_MODES: + message = _("DRAC reported unknown boot mode " + "'%(drac_boot_mode)s'") % { + 'drac_boot_mode': drac_boot_mode} + LOG.error('DRAC driver failed to change boot device order ' + 'for node %(node_uuid)s. Reason: %(message)s.', + {'node_uuid': node.uuid, 'message': message}) + raise exception.DracOperationError(error=message) + + flexibly_program_settings = _flexibly_program_boot_order( + device, drac_boot_mode) + client.set_bios_settings(flexibly_program_settings) + else: + raise exception.InvalidParameterValue( + _("set_boot_device called with invalid device " + "'%(device)s' for node %(node_id)s.") % + {'device': device, 'node_id': node.uuid}) - client.change_boot_device_order(boot_list, drac_boot_device) client.commit_pending_bios_changes() except drac_exceptions.BaseClientException as exc: LOG.error('DRAC driver failed to change boot device order for ' diff --git a/ironic/tests/unit/drivers/modules/drac/test_management.py b/ironic/tests/unit/drivers/modules/drac/test_management.py index db3e3fffe4..281b0578c1 100644 --- a/ironic/tests/unit/drivers/modules/drac/test_management.py +++ b/ironic/tests/unit/drivers/modules/drac/test_management.py @@ -2,6 +2,7 @@ # # Copyright 2014 Red Hat, Inc. # All Rights Reserved. +# Copyright (c) 2017-2018 Dell Inc. or its subsidiaries. # # 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 @@ -20,6 +21,7 @@ Test class for DRAC management interface """ import mock +from oslo_utils import importutils import ironic.common.boot_devices from ironic.common import exception @@ -33,6 +35,8 @@ from ironic.tests.unit.db import utils as db_utils from ironic.tests.unit.drivers.modules.drac import utils as test_utils from ironic.tests.unit.objects import utils as obj_utils +dracclient_exceptions = importutils.try_import('dracclient.exceptions') + INFO_DICT = db_utils.get_test_drac_info() @@ -58,29 +62,44 @@ class DracManagementInternalMethodsTestCase(db_base.DbTestCase): driver='fake_drac', driver_info=INFO_DICT) - self.boot_device_pxe = { + boot_device_ipl_pxe = { 'id': 'BIOS.Setup.1-1#BootSeq#NIC.Embedded.1-1-1', 'boot_mode': 'IPL', 'current_assigned_sequence': 0, 'pending_assigned_sequence': 0, 'bios_boot_string': 'Embedded NIC 1 Port 1 Partition 1'} - self.boot_device_disk = { + boot_device_ipl_disk = { 'id': 'BIOS.Setup.1-1#BootSeq#HardDisk.List.1-1', 'boot_mode': 'IPL', 'current_assigned_sequence': 1, 'pending_assigned_sequence': 1, 'bios_boot_string': 'Hard drive C: BootSeq'} - self.ipl_boot_devices = [ - test_utils.dict_to_namedtuple(values=self.boot_device_pxe), - test_utils.dict_to_namedtuple(values=self.boot_device_disk)] - self.boot_devices = {'IPL': self.ipl_boot_devices, - 'OneTime': self.ipl_boot_devices} + ipl_boot_device_namedtuples = [ + test_utils.dict_to_namedtuple(values=boot_device_ipl_pxe), + test_utils.dict_to_namedtuple(values=boot_device_ipl_disk)] + ipl_boot_devices = {'IPL': ipl_boot_device_namedtuples, + 'OneTime': ipl_boot_device_namedtuples} + + boot_device_uefi_pxe = { + 'id': 'UEFI:BIOS.Setup.1-1#UefiBootSeq#NIC.PxeDevice.1-1', + 'boot_mode': 'UEFI', + 'current_assigned_sequence': 0, + 'pending_assigned_sequence': 0, + 'bios_boot_string': + 'PXE Device 1: Integrated NIC 1 Port 1 Partition 1'} + uefi_boot_device_namedtuples = [ + test_utils.dict_to_namedtuple(values=boot_device_uefi_pxe)] + uefi_boot_devices = {'UEFI': uefi_boot_device_namedtuples, + 'OneTime': uefi_boot_device_namedtuples} + + self.boot_devices = {'IPL': ipl_boot_devices, + 'UEFI': uefi_boot_devices} def test__get_boot_device(self, mock_get_drac_client): mock_client = mock.Mock() mock_get_drac_client.return_value = mock_client mock_client.list_boot_modes.return_value = self.boot_modes('IPL') - mock_client.list_boot_devices.return_value = self.boot_devices + mock_client.list_boot_devices.return_value = self.boot_devices['IPL'] boot_device = drac_mgmt._get_boot_device(self.node) @@ -96,7 +115,7 @@ class DracManagementInternalMethodsTestCase(db_base.DbTestCase): # persistent boot modes mock_client.list_boot_modes.return_value = self.boot_modes('IPL', 'OneTime') - mock_client.list_boot_devices.return_value = self.boot_devices + mock_client.list_boot_devices.return_value = self.boot_devices['IPL'] boot_device = drac_mgmt._get_boot_device(self.node) @@ -105,6 +124,20 @@ class DracManagementInternalMethodsTestCase(db_base.DbTestCase): mock_client.list_boot_modes.assert_called_once_with() mock_client.list_boot_devices.assert_called_once_with() + def test__get_boot_device_with_no_boot_device(self, + mock_get_drac_client): + mock_client = mock.Mock() + mock_get_drac_client.return_value = mock_client + mock_client.list_boot_modes.return_value = self.boot_modes('IPL') + mock_client.list_boot_devices.return_value = {} + + boot_device = drac_mgmt._get_boot_device(self.node) + + expected_boot_device = {'boot_device': None, 'persistent': True} + self.assertEqual(expected_boot_device, boot_device) + mock_client.list_boot_modes.assert_called_once_with() + mock_client.list_boot_devices.assert_called_once_with() + def test__get_boot_device_with_empty_boot_mode_list(self, mock_get_drac_client): mock_client = mock.Mock() @@ -114,19 +147,124 @@ class DracManagementInternalMethodsTestCase(db_base.DbTestCase): self.assertRaises(exception.DracOperationError, drac_mgmt._get_boot_device, self.node) + def test__get_next_persistent_boot_mode(self, mock_get_drac_client): + mock_client = mock.Mock() + mock_get_drac_client.return_value = mock_client + mock_client.list_boot_modes.return_value = self.boot_modes('IPL') + + boot_mode = drac_mgmt._get_next_persistent_boot_mode(self.node) + + mock_get_drac_client.assert_called_once_with(self.node) + mock_client.list_boot_modes.assert_called_once_with() + expected_boot_mode = 'IPL' + self.assertEqual(expected_boot_mode, boot_mode) + + def test__get_next_persistent_boot_mode_with_non_persistent_boot_mode( + self, mock_get_drac_client): + mock_client = mock.Mock() + mock_get_drac_client.return_value = mock_client + mock_client.list_boot_modes.return_value = self.boot_modes('IPL', + 'OneTime') + + boot_mode = drac_mgmt._get_next_persistent_boot_mode(self.node) + + mock_get_drac_client.assert_called_once_with(self.node) + mock_client.list_boot_modes.assert_called_once_with() + expected_boot_mode = 'IPL' + self.assertEqual(expected_boot_mode, boot_mode) + + def test__get_next_persistent_boot_mode_list_boot_modes_fail( + self, mock_get_drac_client): + mock_client = mock.Mock() + mock_get_drac_client.return_value = mock_client + exc = dracclient_exceptions.BaseClientException('boom') + mock_client.list_boot_modes.side_effect = exc + + self.assertRaises(exception.DracOperationError, + drac_mgmt._get_next_persistent_boot_mode, self.node) + + mock_get_drac_client.assert_called_once_with(self.node) + mock_client.list_boot_modes.assert_called_once_with() + + def test__get_next_persistent_boot_mode_with_empty_boot_mode_list( + self, mock_get_drac_client): + mock_client = mock.Mock() + mock_get_drac_client.return_value = mock_client + mock_client.list_boot_modes.return_value = [] + + self.assertRaises(exception.DracOperationError, + drac_mgmt._get_next_persistent_boot_mode, self.node) + + mock_get_drac_client.assert_called_once_with(self.node) + mock_client.list_boot_modes.assert_called_once_with() + + def test__is_boot_order_flexibly_programmable(self, mock_get_drac_client): + self.assertTrue(drac_mgmt._is_boot_order_flexibly_programmable( + persistent=True, bios_settings={'SetBootOrderFqdd1': ()})) + + def test__is_boot_order_flexibly_programmable_not_persistent( + self, mock_get_drac_client): + self.assertFalse(drac_mgmt._is_boot_order_flexibly_programmable( + persistent=False, bios_settings={'SetBootOrderFqdd1': ()})) + + def test__is_boot_order_flexibly_programmable_with_no_bios_setting( + self, mock_get_drac_client): + self.assertFalse(drac_mgmt._is_boot_order_flexibly_programmable( + persistent=True, bios_settings={})) + + def test__flexibly_program_boot_order_for_disk_and_bios( + self, mock_get_drac_client): + settings = drac_mgmt._flexibly_program_boot_order( + ironic.common.boot_devices.DISK, drac_boot_mode='Bios') + + expected_settings = {'SetBootOrderFqdd1': 'HardDisk.List.1-1'} + self.assertEqual(expected_settings, settings) + + def test__flexibly_program_boot_order_for_disk_and_uefi( + self, mock_get_drac_client): + settings = drac_mgmt._flexibly_program_boot_order( + ironic.common.boot_devices.DISK, drac_boot_mode='Uefi') + + expected_settings = { + 'SetBootOrderFqdd1': '*.*.*', + 'SetBootOrderFqdd2': 'NIC.*.*', + 'SetBootOrderFqdd3': 'Optical.*.*', + 'SetBootOrderFqdd4': 'Floppy.*.*', + } + self.assertEqual(expected_settings, settings) + + def test__flexibly_program_boot_order_for_pxe(self, mock_get_drac_client): + settings = drac_mgmt._flexibly_program_boot_order( + ironic.common.boot_devices.PXE, drac_boot_mode='Uefi') + + expected_settings = {'SetBootOrderFqdd1': 'NIC.*.*'} + self.assertEqual(expected_settings, settings) + + def test__flexibly_program_boot_order_for_cdrom(self, + mock_get_drac_client): + settings = drac_mgmt._flexibly_program_boot_order( + ironic.common.boot_devices.CDROM, drac_boot_mode='Uefi') + + expected_settings = {'SetBootOrderFqdd1': 'Optical.*.*'} + self.assertEqual(expected_settings, settings) + + @mock.patch.object(drac_mgmt, '_get_next_persistent_boot_mode', + spec_set=True, autospec=True) @mock.patch.object(drac_mgmt, '_get_boot_device', spec_set=True, autospec=True) @mock.patch.object(drac_job, 'validate_job_queue', spec_set=True, autospec=True) def test_set_boot_device(self, mock_validate_job_queue, - mock__get_boot_device, mock_get_drac_client): + mock__get_boot_device, + mock__get_next_persistent_boot_mode, + mock_get_drac_client): mock_client = mock.Mock() mock_get_drac_client.return_value = mock_client - mock_client.list_boot_modes.return_value = self.boot_modes('IPL') - mock_client.list_boot_devices.return_value = self.boot_devices + mock_client.list_boot_devices.return_value = self.boot_devices['IPL'] boot_device = {'boot_device': ironic.common.boot_devices.DISK, 'persistent': True} mock__get_boot_device.return_value = boot_device + mock__get_next_persistent_boot_mode.return_value = 'IPL' boot_device = drac_mgmt.set_boot_device( self.node, ironic.common.boot_devices.PXE, persistent=False) @@ -134,39 +272,172 @@ class DracManagementInternalMethodsTestCase(db_base.DbTestCase): mock_validate_job_queue.assert_called_once_with(self.node) mock_client.change_boot_device_order.assert_called_once_with( 'OneTime', 'BIOS.Setup.1-1#BootSeq#NIC.Embedded.1-1-1') + self.assertEqual(0, mock_client.set_bios_settings.call_count) mock_client.commit_pending_bios_changes.assert_called_once_with() + @mock.patch.object(drac_mgmt, '_get_next_persistent_boot_mode', + spec_set=True, autospec=True) @mock.patch.object(drac_mgmt, '_get_boot_device', spec_set=True, autospec=True) @mock.patch.object(drac_job, 'validate_job_queue', spec_set=True, autospec=True) def test_set_boot_device_called_with_no_change( self, mock_validate_job_queue, mock__get_boot_device, - mock_get_drac_client): + mock__get_next_persistent_boot_mode, mock_get_drac_client): mock_client = mock.Mock() mock_get_drac_client.return_value = mock_client - mock_client.list_boot_modes.return_value = self.boot_modes('IPL') - mock_client.list_boot_devices.return_value = self.boot_devices + mock_client.list_boot_devices.return_value = self.boot_devices['IPL'] boot_device = {'boot_device': ironic.common.boot_devices.PXE, 'persistent': True} mock__get_boot_device.return_value = boot_device + mock__get_next_persistent_boot_mode.return_value = 'IPL' boot_device = drac_mgmt.set_boot_device( self.node, ironic.common.boot_devices.PXE, persistent=True) mock_validate_job_queue.assert_called_once_with(self.node) self.assertEqual(0, mock_client.change_boot_device_order.call_count) + self.assertEqual(0, mock_client.set_bios_settings.call_count) self.assertEqual(0, mock_client.commit_pending_bios_changes.call_count) + @mock.patch.object(drac_mgmt, '_flexibly_program_boot_order', + spec_set=True, autospec=True) + @mock.patch.object(drac_mgmt, '_is_boot_order_flexibly_programmable', + spec_set=True, autospec=True) + @mock.patch.object(drac_mgmt, '_get_next_persistent_boot_mode', + spec_set=True, autospec=True) @mock.patch.object(drac_mgmt, '_get_boot_device', spec_set=True, autospec=True) @mock.patch.object(drac_job, 'validate_job_queue', spec_set=True, autospec=True) - def test_set_boot_device_with_invalid_job_queue( + def test_set_boot_device_called_with_no_drac_boot_device( self, mock_validate_job_queue, mock__get_boot_device, + mock__get_next_persistent_boot_mode, + mock__is_boot_order_flexibly_programmable, + mock__flexibly_program_boot_order, mock_get_drac_client): mock_client = mock.Mock() mock_get_drac_client.return_value = mock_client + mock_client.list_boot_devices.return_value = self.boot_devices['UEFI'] + boot_device = {'boot_device': ironic.common.boot_devices.PXE, + 'persistent': False} + mock__get_boot_device.return_value = boot_device + mock__get_next_persistent_boot_mode.return_value = 'UEFI' + settings = [ + { + 'name': 'BootMode', + 'instance_id': 'BIOS.Setup.1-1:BootMode', + 'current_value': 'Uefi', + 'pending_value': None, + 'read_only': False, + 'possible_values': ['Bios', 'Uefi'] + }, + ] + bios_settings = { + s['name']: test_utils.dict_to_namedtuple( + values=s) for s in settings} + mock_client.list_bios_settings.return_value = bios_settings + mock__is_boot_order_flexibly_programmable.return_value = True + flexibly_program_settings = { + 'SetBootOrderFqdd1': '*.*.*', + 'SetBootOrderFqdd2': 'NIC.*.*', + 'SetBootOrderFqdd3': 'Optical.*.*', + 'SetBootOrderFqdd4': 'Floppy.*.*', + } + mock__flexibly_program_boot_order.return_value = \ + flexibly_program_settings + + drac_mgmt.set_boot_device(self.node, ironic.common.boot_devices.DISK, + persistent=True) + + mock_validate_job_queue.assert_called_once_with(self.node) + self.assertEqual(0, mock_client.change_boot_device_order.call_count) + mock_client.set_bios_settings.assert_called_once_with( + flexibly_program_settings) + mock_client.commit_pending_bios_changes.assert_called_once_with() + + @mock.patch.object(drac_mgmt, '_is_boot_order_flexibly_programmable', + spec_set=True, autospec=True) + @mock.patch.object(drac_mgmt, '_get_next_persistent_boot_mode', + spec_set=True, autospec=True) + @mock.patch.object(drac_mgmt, '_get_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(drac_job, 'validate_job_queue', spec_set=True, + autospec=True) + def test_set_boot_device_called_with_not_flexibly_programmable( + self, mock_validate_job_queue, mock__get_boot_device, + mock__get_next_persistent_boot_mode, + mock__is_boot_order_flexibly_programmable, + mock_get_drac_client): + mock_client = mock.Mock() + mock_get_drac_client.return_value = mock_client + mock_client.list_boot_devices.return_value = self.boot_devices['UEFI'] + boot_device = {'boot_device': ironic.common.boot_devices.PXE, + 'persistent': False} + mock__get_boot_device.return_value = boot_device + mock__get_next_persistent_boot_mode.return_value = 'UEFI' + mock__is_boot_order_flexibly_programmable.return_value = False + + self.assertRaises(exception.InvalidParameterValue, + drac_mgmt.set_boot_device, self.node, + ironic.common.boot_devices.CDROM, persistent=False) + + mock_validate_job_queue.assert_called_once_with(self.node) + self.assertEqual(0, mock_client.change_boot_device_order.call_count) + self.assertEqual(0, mock_client.set_bios_settings.call_count) + self.assertEqual(0, mock_client.commit_pending_bios_changes.call_count) + + @mock.patch.object(drac_mgmt, '_is_boot_order_flexibly_programmable', + spec_set=True, autospec=True) + @mock.patch.object(drac_mgmt, '_get_next_persistent_boot_mode', + spec_set=True, autospec=True) + @mock.patch.object(drac_mgmt, '_get_boot_device', spec_set=True, + autospec=True) + @mock.patch.object(drac_job, 'validate_job_queue', spec_set=True, + autospec=True) + def test_set_boot_device_called_with_unknown_boot_mode( + self, mock_validate_job_queue, mock__get_boot_device, + mock__get_next_persistent_boot_mode, + mock__is_boot_order_flexibly_programmable, + mock_get_drac_client): + mock_client = mock.Mock() + mock_get_drac_client.return_value = mock_client + mock_client.list_boot_devices.return_value = self.boot_devices['UEFI'] + boot_device = {'boot_device': ironic.common.boot_devices.PXE, + 'persistent': False} + mock__get_boot_device.return_value = boot_device + mock__get_next_persistent_boot_mode.return_value = 'UEFI' + settings = [ + { + 'name': 'BootMode', + 'instance_id': 'BIOS.Setup.1-1:BootMode', + 'current_value': 'Bad', + 'pending_value': None, + 'read_only': False, + 'possible_values': ['Bios', 'Uefi', 'Bad'] + }, + ] + bios_settings = { + s['name']: test_utils.dict_to_namedtuple( + values=s) for s in settings} + mock_client.list_bios_settings.return_value = bios_settings + mock__is_boot_order_flexibly_programmable.return_value = True + + self.assertRaises(exception.DracOperationError, + drac_mgmt.set_boot_device, self.node, + ironic.common.boot_devices.DISK, persistent=True) + + mock_validate_job_queue.assert_called_once_with(self.node) + self.assertEqual(0, mock_client.change_boot_device_order.call_count) + self.assertEqual(0, mock_client.set_bios_settings.call_count) + self.assertEqual(0, mock_client.commit_pending_bios_changes.call_count) + + @mock.patch.object(drac_job, 'validate_job_queue', spec_set=True, + autospec=True) + def test_set_boot_device_with_invalid_job_queue( + self, mock_validate_job_queue, mock_get_drac_client): + mock_client = mock.Mock() + mock_get_drac_client.return_value = mock_client mock_validate_job_queue.side_effect = exception.DracOperationError( 'boom') @@ -175,6 +446,7 @@ class DracManagementInternalMethodsTestCase(db_base.DbTestCase): ironic.common.boot_devices.PXE, persistent=True) self.assertEqual(0, mock_client.change_boot_device_order.call_count) + self.assertEqual(0, mock_client.set_bios_settings.call_count) self.assertEqual(0, mock_client.commit_pending_bios_changes.call_count) diff --git a/releasenotes/notes/idrac-uefi-boot-mode-86f4694b4247a1ca.yaml b/releasenotes/notes/idrac-uefi-boot-mode-86f4694b4247a1ca.yaml new file mode 100644 index 0000000000..2c78e7ea27 --- /dev/null +++ b/releasenotes/notes/idrac-uefi-boot-mode-86f4694b4247a1ca.yaml @@ -0,0 +1,11 @@ +--- +fixes: + - | + Fixes an issue that caused the integrated Dell Remote Access + Controller (iDRAC) ``management`` hardware interface implementation, + ``idrac``, to fail to boot nodes in Unified Extensible Firmware Interface + (UEFI) boot mode. That interface is supported by the ``idrac`` hardware + type. The issue is resolved for Dell EMC PowerEdge 13th and 14th generation + servers. It is not resolved for PowerEdge 12th generation and earlier + servers. For more information, see `story 1656841 + `_.