From 041a7d7064491958278725123af0c1b8fa8aefe5 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 14 Nov 2023 14:55:57 -0800 Subject: [PATCH] Redfish UefiHttp boot support Adds a redfish-https boot interface, based upon the redfish-virtual-media boot interface, however substantially copies some base methods because of simplification offered to use by putting "attach/detach" logic into how the sushy library handles the application and reset of a URL as a boot setting. This feature also increases the requirement for the Sushy library to version 4.7.0 which includes support to set the HttpBootUri field in the BMC and automatically unset it as well. Closes-Bug: #2032380 Change-Id: I991611cd67cb91aea21fc30bbae7cd24409dbbfa --- doc/source/admin/drivers/redfish.rst | 40 +- ironic/common/boot_devices.py | 3 + ironic/common/exception.py | 6 + ironic/drivers/modules/redfish/boot.py | 303 +++++++ ironic/drivers/modules/redfish/management.py | 20 +- ironic/drivers/redfish.py | 3 +- .../unit/drivers/modules/redfish/test_boot.py | 850 ++++++++++++++++++ .../modules/redfish/test_management.py | 57 +- ...ish-httpboot-support-8d516158860c9d43.yaml | 7 + requirements.txt | 2 +- setup.cfg | 1 + 11 files changed, 1271 insertions(+), 21 deletions(-) create mode 100644 releasenotes/notes/add-redfish-httpboot-support-8d516158860c9d43.yaml diff --git a/doc/source/admin/drivers/redfish.rst b/doc/source/admin/drivers/redfish.rst index c4694b85d6..806f9240f9 100644 --- a/doc/source/admin/drivers/redfish.rst +++ b/doc/source/admin/drivers/redfish.rst @@ -23,13 +23,14 @@ Enabling the Redfish driver #. Add ``redfish`` to the list of ``enabled_hardware_types``, ``enabled_power_interfaces``, ``enabled_management_interfaces`` and ``enabled_inspect_interfaces`` as well as ``redfish-virtual-media`` - to ``enabled_boot_interfaces`` in ``/etc/ironic/ironic.conf``. + and ``redfish-https`` to ``enabled_boot_interfaces`` in + ``/etc/ironic/ironic.conf``. For example:: [DEFAULT] ... enabled_hardware_types = ipmi,redfish - enabled_boot_interfaces = ipxe,redfish-virtual-media + enabled_boot_interfaces = ipxe,redfish-virtual-media,redfish-https enabled_power_interfaces = ipmitool,redfish enabled_management_interfaces = ipmitool,redfish enabled_inspect_interfaces = inspector,redfish @@ -386,6 +387,41 @@ Layer 3 or DHCP-less ramdisk booting DHCP-less deploy is supported by the Redfish virtual media boot. See :doc:`/admin/dhcp-less` for more information. +Redfish HTTP(s) Boot +==================== + +The ``redfish-https`` boot interface is very similar to the +``redfish-virtual-media`` boot interface. In this driver, we compose an ISO +image, and request the BMC to inform the UEFI firmware to boot the Ironic +ramdisk, or a other ramdisk image. This approach is intended to allow a +pattern of engagement where we have minimal reliance on addressing and +discovery of the Ironic deployment through autoconfiguration like DHCP, +and somewhat mirrors vendor examples of booting from an HTTP URL. + +This interface has some basic constraints. + +* There is no configuration drive functionality, while Virtual Media did + help provide such functionality. +* This interface *is* dependent upon BMC, EFI Firmware, and Bootloader, + which means we may not see additional embedded files an contents in + an ISO image. This is the same basic constraint over the ``ramdisk`` + deploy interface when using Network Booting. +* This is a UEFI-Only boot interface. No legacy boot is possible with + this interface. + +A good starting point for this interface, is to think of it as +higher security network boot, as we are explicitly telling the BMC +where the node should boot from. + +Like the ``redfish-virtual-media`` boot interface, you will need +to create an EFI System Partition image (ESP_), see +`Configuring an ESP image`_ for details on how to do this. + +Additionally, if you would like to use the ``ramdisk`` deployment +interface, the same basic instructions covered in `Virtual Media Ramdisk`_ +apply, just use ``redfish-https`` as the boot_interface, and keep in mind, +no configuration drives exist with the ``redfish-https`` boot interface. + Firmware update using manual cleaning ===================================== diff --git a/ironic/common/boot_devices.py b/ironic/common/boot_devices.py index 1ec4ab0554..65736faf49 100644 --- a/ironic/common/boot_devices.py +++ b/ironic/common/boot_devices.py @@ -52,3 +52,6 @@ FLOPPY = 'floppy' VMEDIA_DEVICES = [DISK, CDROM, FLOPPY] """Devices that make sense for virtual media attachment.""" + +UEFIHTTP = "uefihttp" +"Boot from a UEFI HTTP(s) URL" diff --git a/ironic/common/exception.py b/ironic/common/exception.py index 45672476ac..67a48d9204 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -883,3 +883,9 @@ class FirmwareComponentNotFound(NotFound): class InvalidNodeInventory(Invalid): _msg_fmt = _("Inventory for node %(node)s is invalid: %(reason)s") + + +class UnsupportedHardwareFeature(Invalid): + _msg_fmt = _("Node %(node)s hardware does not support feature " + "%(feature)s, which is required based upon the " + "requested configuration.") diff --git a/ironic/drivers/modules/redfish/boot.py b/ironic/drivers/modules/redfish/boot.py index 8b16be9f1a..fcf6ba64a4 100644 --- a/ironic/drivers/modules/redfish/boot.py +++ b/ironic/drivers/modules/redfish/boot.py @@ -21,6 +21,7 @@ from ironic.common import boot_devices from ironic.common import exception from ironic.common.i18n import _ from ironic.common import states +from ironic.common import utils as common_utils from ironic.conductor import utils as manager_utils from ironic.conf import CONF from ironic.drivers import base @@ -712,6 +713,11 @@ class RedfishVirtualMediaBoot(base.BootInterface): if not configdrive: return + if 'ramdisk_boot_configdrive' not in self.capabilities: + raise exception.InstanceDeployFailure( + _('Cannot attach a configdrive to node %s, as it is not ' + 'supported in the driver.') % task.node.uuid) + _eject_vmedia(task, managers, sushy.VIRTUAL_MEDIA_USBSTICK) cd_ref = image_utils.prepare_configdrive_image(task, configdrive) try: @@ -775,3 +781,300 @@ class RedfishVirtualMediaBoot(base.BootInterface): ManagementInterface fails. """ manager_utils.node_set_boot_device(task, device, persistent) + + +class RedfishHttpsBoot(base.BootInterface): + """A driver which utilizes UefiHttp like virtual media. + + Utilizes the virtual media image build to craft a ISO image to + signal to remote BMC to boot. + + This interface comes with some constraints. For example, this + interface is built under the operating assumption that DHCP is + used. The UEFI Firmware needs to load some base configuration, + regardless. Also depending on UEFI Firmware, and how it handles + UefiHttp Boot, additional ISO contents, such as "configuration drive" + materials might be unavailable. A similar constraint exists with + ``ramdisk`` deployment. + """ + + capabilities = ['ramdisk_boot'] + + def get_properties(self): + """Return the properties of the interface. + + :returns: dictionary of : entries. + """ + return REQUIRED_PROPERTIES + + def _validate_driver_info(self, task): + """Validate the prerequisites for Redfish HTTPS based boot. + + This method validates whether the 'driver_info' property of the + supplied node contains the required information for this driver. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue if any parameters are incorrect + :raises: MissingParameterValue if some mandatory information + is missing on the node + """ + node = task.node + + _parse_driver_info(node) + # Issue the deprecation warning if needed + driver_utils.get_agent_iso(node, deprecated_prefix='redfish') + + def _validate_instance_info(self, task): + """Validate instance image information for the task's node. + + This method validates whether the 'instance_info' property of the + supplied node contains the required information for this driver. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue if any parameters are incorrect + :raises: MissingParameterValue if some mandatory information + is missing on the node + """ + node = task.node + + # NOTE(dtantsur): if we're are writing an image with local boot + # the boot interface does not care about image parameters and + # must not validate them. + if (not task.driver.storage.should_write_image(task) + or deploy_utils.get_boot_option(node) == 'local'): + return + + d_info = _parse_deploy_info(node) + deploy_utils.validate_image_properties(task, d_info) + + def _validate_hardware(self, task): + """Validates hardware support. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue if vendor not supported + """ + system = redfish_utils.get_system(task.node) + if "UefiHttp" not in system.boot.allowed_values: + raise exception.UnsupportedHardwareFeature( + node=task.node.uuid, + feature="UefiHttp boot") + + def validate(self, task): + """Validate the deployment information for the task's node. + + This method validates whether the 'driver_info' and/or 'instance_info' + properties of the task's node contains the required information for + this interface to function. + + :param task: A TaskManager instance containing the node to act on. + :raises: InvalidParameterValue on malformed parameter(s) + :raises: MissingParameterValue on missing parameter(s) + """ + self._validate_hardware(task) + self._validate_driver_info(task) + self._validate_instance_info(task) + + def validate_inspection(self, task): + """Validate that the node has required properties for inspection. + + :param task: A TaskManager instance with the node being checked + :raises: MissingParameterValue if node is missing one or more required + parameters + :raises: UnsupportedDriverExtension + """ + try: + self._validate_driver_info(task) + except exception.MissingParameterValue: + # Fall back to non-managed in-band inspection + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='inspection') + + def prepare_ramdisk(self, task, ramdisk_params): + """Prepares the boot of the agent ramdisk. + + This method prepares the boot of the deploy or rescue ramdisk after + reading relevant information from the node's driver_info and + instance_info. + + :param task: A task from TaskManager. + :param ramdisk_params: the parameters to be passed to the ramdisk. + :returns: None + :raises: MissingParameterValue, if some information is missing in + node's driver_info or instance_info. + :raises: InvalidParameterValue, if some information provided is + invalid. + :raises: IronicException, if some power or set boot boot device + operation failed on the node. + """ + node = task.node + if not driver_utils.need_prepare_ramdisk(node): + return + + d_info = _parse_driver_info(node) + + if manager_utils.is_fast_track(task): + LOG.debug('Fast track operation for node %s, not setting up ' + 'a HTTP Boot url', node.uuid) + return + + can_config = d_info.pop('can_provide_config', True) + if can_config: + manager_utils.add_secret_token(node, pregenerated=True) + node.save() + ramdisk_params['ipa-agent-token'] = \ + node.driver_internal_info['agent_secret_token'] + + manager_utils.node_power_action(task, states.POWER_OFF) + + deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task) + if deploy_nic_mac is not None: + ramdisk_params['BOOTIF'] = deploy_nic_mac + if CONF.debug and 'ipa-debug' not in ramdisk_params: + ramdisk_params['ipa-debug'] = '1' + + # NOTE(TheJulia): This is a mandatory setting for virtual media + # based deployment operations and boot modes similar where we + # want the ramdisk to consider embedded configuration. + ramdisk_params['boot_method'] = 'vmedia' + + mode = deploy_utils.rescue_or_deploy_mode(node) + + iso_ref = image_utils.prepare_deploy_iso(task, ramdisk_params, + mode, d_info) + boot_mode_utils.sync_boot_mode(task) + + self._set_boot_device(task, boot_devices.UEFIHTTP, + http_boot_url=iso_ref) + + LOG.debug("Node %(node)s is set to one time boot from " + "%(device)s", {'node': task.node.uuid, + 'device': boot_devices.UEFIHTTP}) + + def clean_up_ramdisk(self, task): + """Cleans up the boot of ironic ramdisk. + + This method cleans up the environment that was setup for booting the + deploy ramdisk. + + :param task: A task from TaskManager. + :returns: None + """ + if manager_utils.is_fast_track(task): + LOG.debug('Fast track operation for node %s, not ejecting ' + 'any devices', task.node.uuid) + return + + LOG.debug("Cleaning up deploy boot for " + "%(node)s", {'node': task.node.uuid}) + self._clean_up(task) + + def prepare_instance(self, task): + """Prepares the boot of instance over virtual media. + + This method prepares the boot of the instance after reading + relevant information from the node's instance_info. + + The internal logic is as follows: + + - Cleanup any related files + - Sync the boot mode with the machine. + - Configure Secure boot, if required. + - If local boot, or a whole disk image was deployed, + set the next boot device as disk. + - If "ramdisk" is the desired, then the UefiHttp boot + option is set to the BMC with a request for this to + be persistent. + + :param task: a task from TaskManager. + :returns: None + :raises: InstanceDeployFailure, if its try to boot iSCSI volume in + 'BIOS' boot mode. + """ + node = task.node + + self._clean_up(task) + + boot_mode_utils.sync_boot_mode(task) + boot_mode_utils.configure_secure_boot_if_needed(task) + + boot_option = deploy_utils.get_boot_option(node) + iwdi = node.driver_internal_info.get('is_whole_disk_image') + if boot_option == "local" or iwdi: + self._set_boot_device(task, boot_devices.DISK, persistent=True) + + LOG.debug("Node %(node)s is set to permanently boot from local " + "%(device)s", {'node': task.node.uuid, + 'device': boot_devices.DISK}) + return + + params = {} + + if boot_option != 'ramdisk': + root_uuid = node.driver_internal_info.get('root_uuid_or_disk_id') + if not root_uuid and task.driver.storage.should_write_image(task): + LOG.warning( + "The UUID of the root partition could not be found for " + "node %s. Booting instance from disk anyway.", node.uuid) + + self._set_boot_device(task, boot_devices.DISK, persistent=True) + + return + + params.update(root_uuid=root_uuid) + + deploy_info = _parse_deploy_info(node) + + iso_ref = image_utils.prepare_boot_iso(task, deploy_info, **params) + self._set_boot_device(task, boot_devices.UEFIHTTP, persistent=True, + http_boot_url=iso_ref) + + LOG.debug("Node %(node)s is set to permanently boot from " + "%(device)s", {'node': task.node.uuid, + 'device': boot_devices.UEFIHTTP}) + + def _clean_up(self, task): + image_utils.cleanup_iso_image(task) + + def clean_up_instance(self, task): + """Cleans up the boot of instance. + + This method cleans up the environment that was setup for booting + the instance. + + :param task: A task from TaskManager. + :returns: None + """ + LOG.debug("Cleaning up instance boot for " + "%(node)s", {'node': task.node.uuid}) + self._clean_up(task) + boot_mode_utils.deconfigure_secure_boot_if_needed(task) + + @classmethod + def _set_boot_device(cls, task, device, persistent=False, + http_boot_url=None): + """Set the boot device for a node. + + This is a hook method which can be used by other drivers based upon + this class, in order to facilitate vendor specific logic, + if needed. + + Furthermore, we are not considering a *lack* of a URL as fatal. + A driver could easily update DHCP and send the message to the BMC. + + :param task: a TaskManager instance. + :param device: the boot device, one of + :mod:`ironic.common.boot_devices`. + :param persistent: Whether to set next-boot, or make the change + permanent. Default: False. + :param http_boot_url: The URL to send to the BMC in order to boot + the node via UEFIHTTP. + :raises: InvalidParameterValue if the validation of the + ManagementInterface fails. + """ + + if http_boot_url: + common_utils.set_node_nested_field( + task.node, 'driver_internal_info', + 'redfish_uefi_http_url', http_boot_url) + task.node.save() + manager_utils.node_set_boot_device(task, device, persistent) diff --git a/ironic/drivers/modules/redfish/management.py b/ironic/drivers/modules/redfish/management.py index 0b4472f411..11deb45e17 100644 --- a/ironic/drivers/modules/redfish/management.py +++ b/ironic/drivers/modules/redfish/management.py @@ -52,7 +52,8 @@ if sushy: sushy.BOOT_SOURCE_TARGET_PXE: boot_devices.PXE, sushy.BOOT_SOURCE_TARGET_HDD: boot_devices.DISK, sushy.BOOT_SOURCE_TARGET_CD: boot_devices.CDROM, - sushy.BOOT_SOURCE_TARGET_BIOS_SETUP: boot_devices.BIOS + sushy.BOOT_SOURCE_TARGET_BIOS_SETUP: boot_devices.BIOS, + sushy.BOOT_SOURCE_TARGET_UEFI_HTTP: boot_devices.UEFIHTTP } BOOT_DEVICE_MAP_REV = {v: k for k, v in BOOT_DEVICE_MAP.items()} @@ -101,7 +102,8 @@ _FIRMWARE_UPDATE_ARGS = { }} -def _set_boot_device(task, system, device, persistent=False): +def _set_boot_device(task, system, device, persistent=False, + http_boot_url=None): """An internal routine to set the boot device. :param task: a task from TaskManager. @@ -110,6 +112,8 @@ def _set_boot_device(task, system, device, persistent=False): :param persistent: Boolean value. True if the boot device will persist to all future boots, False if not. Default: False. + :param http_boot_url: A string value to be sent to the sushy library, + which is sent to the BMC as the url to boot from. :raises: SushyError on an error from the Sushy library """ @@ -133,7 +137,10 @@ def _set_boot_device(task, system, device, persistent=False): enabled = (desired_enabled if desired_enabled != current_enabled else None) try: - system.set_system_boot_options(device, enabled=enabled) + # NOTE(TheJulia): In sushy, it is uri, due to the convention used + # in the standard. URL is used internally in ironic. + system.set_system_boot_options(device, enabled=enabled, + http_boot_uri=http_boot_url) except sushy.exceptions.SushyError as e: if enabled == sushy.BOOT_SOURCE_ENABLED_CONTINUOUS: # NOTE(dtantsur): continuous boot device settings have been @@ -146,7 +153,8 @@ def _set_boot_device(task, system, device, persistent=False): 'falling back to one-time boot settings', {'error': e, 'node': task.node.uuid}) system.set_system_boot_options( - device, enabled=sushy.BOOT_SOURCE_ENABLED_ONCE) + device, enabled=sushy.BOOT_SOURCE_ENABLED_ONCE, + http_boot_uri=http_boot_url) LOG.warning('Could not set persistent boot device to ' '%(dev)s for node %(node)s, using one-time ' 'boot device instead', @@ -254,6 +262,8 @@ class RedfishManagement(base.ManagementInterface): """ utils.pop_node_nested_field( task.node, 'driver_internal_info', 'redfish_boot_device') + http_boot_url = utils.pop_node_nested_field( + task.node, 'driver_internal_info', 'redfish_uefi_http_url') task.node.save() system = redfish_utils.get_system(task.node) @@ -261,7 +271,7 @@ class RedfishManagement(base.ManagementInterface): try: _set_boot_device( task, system, BOOT_DEVICE_MAP_REV[device], - persistent=persistent) + persistent=persistent, http_boot_url=http_boot_url) except sushy.exceptions.SushyError as e: error_msg = (_('Redfish set boot device failed for node ' '%(node)s. Error: %(error)s') % diff --git a/ironic/drivers/redfish.py b/ironic/drivers/redfish.py index 094119e7d2..0ff1b209da 100644 --- a/ironic/drivers/redfish.py +++ b/ironic/drivers/redfish.py @@ -59,7 +59,8 @@ class RedfishHardware(generic.GenericHardware): # NOTE(dtantsur): virtual media goes last because of limited hardware # vendors support. return [ipxe.iPXEBoot, pxe.PXEBoot, - redfish_boot.RedfishVirtualMediaBoot] + redfish_boot.RedfishVirtualMediaBoot, + redfish_boot.RedfishHttpsBoot] @property def supported_vendor_interfaces(self): diff --git a/ironic/tests/unit/drivers/modules/redfish/test_boot.py b/ironic/tests/unit/drivers/modules/redfish/test_boot.py index 5d873b27a9..1aaed186f4 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_boot.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_boot.py @@ -1671,3 +1671,853 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): sushy.VIRTUAL_MEDIA_FLOPPY, redfish_boot._has_vmedia_device( [mock_manager], sushy.VIRTUAL_MEDIA_FLOPPY, inserted=True)) + + +@mock.patch('oslo_utils.eventletutils.EventletEvent.wait', + lambda *args, **kwargs: None) +class RedfishHTTPBootTestCase(db_base.DbTestCase): + + def setUp(self): + super(RedfishHTTPBootTestCase, self).setUp() + self.config(enabled_hardware_types=['redfish'], + enabled_power_interfaces=['redfish'], + enabled_boot_interfaces=['redfish-https'], + enabled_management_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) + + @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', + autospec=True) + @mock.patch.object(image_utils, 'prepare_deploy_iso', autospec=True) + @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) + @mock.patch.object(redfish_boot.manager_utils, 'node_power_action', + autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_prepare_ramdisk_with_params( + self, mock_system, mock_boot_mode_utils, mock_node_power_action, + mock__parse_driver_info, + mock_prepare_deploy_iso, mock_node_set_boot_device): + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.DEPLOYING + + mock__parse_driver_info.return_value = {} + mock_prepare_deploy_iso.return_value = 'image-url' + + task.driver.boot.prepare_ramdisk(task, {}) + + mock_node_power_action.assert_called_once_with( + task, states.POWER_OFF) + + token = task.node.driver_internal_info['agent_secret_token'] + self.assertTrue(token) + + expected_params = { + 'ipa-agent-token': token, + 'ipa-debug': '1', + 'boot_method': 'vmedia', + } + + mock_prepare_deploy_iso.assert_called_once_with( + task, expected_params, 'deploy', {}) + + mock_node_set_boot_device.assert_called_once_with( + task, boot_devices.UEFIHTTP, False) + self.assertEqual('image-url', + task.node.driver_internal_info.get( + 'redfish_uefi_http_url')) + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + self.assertTrue(task.node.driver_internal_info[ + 'agent_secret_token_pregenerated']) + + @mock.patch.object(deploy_utils, 'get_boot_option', lambda node: 'ramdisk') + def test_parse_driver_info_ramdisk(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_info = {} + task.node.automated_clean = False + actual_driver_info = redfish_boot._parse_driver_info(task.node) + self.assertEqual({'can_provide_config': False}, + actual_driver_info) + + def test_parse_driver_info_deploy(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_info.update( + {'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader'} + ) + + actual_driver_info = redfish_boot._parse_driver_info(task.node) + + self.assertIn('kernel', actual_driver_info['deploy_kernel']) + self.assertIn('ramdisk', actual_driver_info['deploy_ramdisk']) + self.assertIn('bootloader', actual_driver_info['bootloader']) + self.assertTrue(actual_driver_info['can_provide_config']) + + def test_parse_driver_info_iso(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_info.update( + {'deploy_iso': 'http://boot.iso'}) + + actual_driver_info = redfish_boot._parse_driver_info(task.node) + + self.assertEqual('http://boot.iso', + actual_driver_info['deploy_iso']) + self.assertFalse(actual_driver_info['can_provide_config']) + + def test_parse_driver_info_rescue(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.RESCUING + task.node.driver_info.update( + {'rescue_kernel': 'kernel', + 'rescue_ramdisk': 'ramdisk', + 'bootloader': 'bootloader'} + ) + + actual_driver_info = redfish_boot._parse_driver_info(task.node) + + self.assertIn('kernel', actual_driver_info['rescue_kernel']) + self.assertIn('ramdisk', actual_driver_info['rescue_ramdisk']) + self.assertIn('bootloader', actual_driver_info['bootloader']) + + def test_parse_driver_info_exc(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.MissingParameterValue, + redfish_boot._parse_driver_info, + task.node) + + def _test_parse_driver_info_from_conf(self, mode='deploy', by_arch=False): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + if mode == 'rescue': + task.node.provision_state = states.RESCUING + + if by_arch: + ramdisk = 'glance://%s_ramdisk_uuid' % mode + kernel = 'glance://%s_kernel_uuid' % mode + + config = { + '%s_ramdisk_by_arch' % mode: {'x86_64': ramdisk}, + '%s_kernel_by_arch' % mode: {'x86_64': kernel} + } + expected = { + '%s_ramdisk' % mode: ramdisk, + '%s_kernel' % mode: kernel + } + else: + expected = { + '%s_ramdisk' % mode: 'glance://%s_ramdisk_uuid' % mode, + '%s_kernel' % mode: 'glance://%s_kernel_uuid' % mode + } + config = expected + + self.config(group='conductor', **config) + + image_info = redfish_boot._parse_driver_info(task.node) + + for key, value in expected.items(): + self.assertEqual(value, image_info[key]) + + def test_parse_driver_info_from_conf_deploy(self): + self._test_parse_driver_info_from_conf() + + def test_parse_driver_info_from_conf_rescue(self): + self._test_parse_driver_info_from_conf(mode='rescue') + + def test_parse_driver_info_from_conf_deploy_by_arch(self): + self._test_parse_driver_info_from_conf(by_arch=True) + + def test_parse_driver_info_from_conf_rescue_by_arch(self): + self._test_parse_driver_info_from_conf(mode='rescue', by_arch=True) + + def _test_parse_driver_info_mixed_source(self, mode='deploy', + by_arch=False): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + if mode == 'rescue': + task.node.provision_state = states.RESCUING + + if by_arch: + kernel_config = { + '%s_kernel_by_arch' % mode: { + 'x86': 'glance://%s_kernel_uuid' % mode + } + } + else: + kernel_config = { + '%s_kernel' % mode: 'glance://%s_kernel_uuid' % mode + } + + ramdisk_config = { + '%s_ramdisk' % mode: 'glance://%s_ramdisk_uuid' % mode, + } + + self.config(group='conductor', **kernel_config) + + task.node.driver_info.update(ramdisk_config) + + self.assertRaises(exception.MissingParameterValue, + redfish_boot._parse_driver_info, task.node) + + def test_parse_driver_info_mixed_source_deploy(self): + self._test_parse_driver_info_mixed_source() + + def test_parse_driver_info_mixed_source_rescue(self): + self._test_parse_driver_info_mixed_source(mode='rescue') + + def test_parse_driver_info_mixed_source_deploy_by_arch(self): + self._test_parse_driver_info_mixed_source(by_arch=True) + + def test_parse_driver_info_mixed_source_rescue_by_arch(self): + self._test_parse_driver_info_mixed_source(mode='rescue', by_arch=True) + + def _test_parse_driver_info_choose_by_arch(self, mode='deploy'): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + if mode == 'rescue': + task.node.provision_state = states.RESCUING + task.node.properties['cpu_arch'] = 'aarch64' + wrong_ramdisk = 'glance://wrong_%s_ramdisk_uuid' % mode + wrong_kernel = 'glance://wrong_%s_kernel_uuid' % mode + ramdisk = 'glance://%s_ramdisk_uuid' % mode + kernel = 'glance://%s_kernel_uuid' % mode + + config = { + '%s_ramdisk_by_arch' % mode: { + 'x86_64': wrong_ramdisk, 'aarch64': ramdisk}, + '%s_kernel_by_arch' % mode: { + 'x86_64': wrong_kernel, 'aarch64': kernel} + } + expected = { + '%s_ramdisk' % mode: ramdisk, + '%s_kernel' % mode: kernel + } + + self.config(group='conductor', **config) + + image_info = redfish_boot._parse_driver_info(task.node) + + for key, value in expected.items(): + self.assertEqual(value, image_info[key]) + + def test_parse_driver_info_choose_by_arch_deploy(self): + self._test_parse_driver_info_choose_by_arch() + + def test_parse_driver_info_choose_by_arch_rescue(self): + self._test_parse_driver_info_choose_by_arch(mode='rescue') + + def _test_parse_driver_info_choose_by_hierarchy(self, mode='deploy', + ramdisk_missing=False): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + if mode == 'rescue': + task.node.provision_state = states.RESCUING + + ramdisk = 'glance://def_%s_ramdisk_uuid' % mode + kernel = 'glance://def_%s_kernel_uuid' % mode + ramdisk_by_arch = 'glance://%s_ramdisk_by_arch_uuid' % mode + kernel_by_arch = 'glance://%s_kernel_by_arch_uuid' % mode + + config = { + '%s_kernel_by_arch' % mode: { + 'x86_64': kernel_by_arch}, + '%s_ramdisk' % mode: ramdisk, + '%s_kernel' % mode: kernel + } + if not ramdisk_missing: + config['%s_ramdisk_by_arch' % mode] = { + 'x86_64': ramdisk_by_arch} + expected = { + '%s_ramdisk' % mode: ramdisk_by_arch, + '%s_kernel' % mode: kernel_by_arch + } + else: + expected = { + '%s_ramdisk' % mode: ramdisk, + '%s_kernel' % mode: kernel + } + + self.config(group='conductor', **config) + + image_info = redfish_boot._parse_driver_info(task.node) + + for key, value in expected.items(): + self.assertEqual(value, image_info[key]) + + def test_parse_driver_info_choose_by_hierarchy_deploy(self): + self._test_parse_driver_info_choose_by_hierarchy() + + def test_parse_driver_info_choose_by_hierarchy_rescue(self): + self._test_parse_driver_info_choose_by_hierarchy(mode='rescue') + + def test_parse_driver_info_choose_by_hierarchy_missing_param_deploy(self): + self._test_parse_driver_info_choose_by_hierarchy(ramdisk_missing=True) + + def test_parse_driver_info_choose_by_hierarchy_missing_param_rescue(self): + self._test_parse_driver_info_choose_by_hierarchy( + mode='rescue', ramdisk_missing=True) + + def test_parse_deploy_info(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_info.update( + {'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader'} + ) + + task.node.instance_info.update( + {'image_source': 'http://boot/iso', + 'kernel': 'http://kernel/img', + 'ramdisk': 'http://ramdisk/img'}) + + actual_instance_info = redfish_boot._parse_deploy_info(task.node) + + self.assertEqual( + 'http://boot/iso', actual_instance_info['image_source']) + self.assertEqual( + 'http://kernel/img', actual_instance_info['kernel']) + self.assertEqual( + 'http://ramdisk/img', actual_instance_info['ramdisk']) + + def test_parse_deploy_info_exc(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.MissingParameterValue, + redfish_boot._parse_deploy_info, + task.node) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + def test_validate_local(self, mock_parse_driver_info, mock_get_system): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.instance_info = {} + mock_get_system.return_value.boot.allowed_values = [ + "UefiHttp", "Hdd"] + + task.node.driver_info.update( + {'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader'} + ) + task.driver.boot.validate(task) + + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + def test_validate_errors_with_lack_of_support( + self, mock_parse_driver_info, mock_get_system): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.instance_info = {} + mock_get_system.return_value.boot.allowed_values = ["Hdd"] + + task.node.driver_info.update( + {'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader'} + ) + msg = ("Node %s hardware does not support feature UefiHttp boot, " + "which is required based upon the requested configuration." + % task.node.uuid) + self.assertRaisesRegex( + exception.UnsupportedHardwareFeature, + msg, task.driver.boot.validate, task) + + @mock.patch.object(redfish_boot.RedfishHttpsBoot, '_validate_hardware', + autospec=True) + @mock.patch.object(deploy_utils, 'get_boot_option', lambda node: 'ramdisk') + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + autospec=True) + def test_validate_kernel_ramdisk(self, mock_validate_image_properties, + mock_parse_driver_info, + mock_validate_hardware): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.instance_info.update( + {'kernel': 'kernel', + 'ramdisk': 'ramdisk', + 'image_source': 'http://image/source'} + ) + + task.node.driver_info.update( + {'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader'} + ) + + task.driver.boot.validate(task) + + mock_validate_image_properties.assert_called_once_with( + task, mock.ANY) + mock_validate_hardware.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(redfish_boot.RedfishHttpsBoot, '_validate_hardware', + autospec=True) + @mock.patch.object(deploy_utils, 'get_boot_option', lambda node: 'ramdisk') + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + autospec=True) + def test_validate_boot_iso(self, mock_validate_image_properties, + mock_parse_driver_info, + mock_validate_hardware): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.instance_info.update( + {'boot_iso': 'http://localhost/file.iso'} + ) + + task.node.driver_info.update( + {'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader'} + ) + + task.driver.boot.validate(task) + + mock_validate_image_properties.assert_called_once_with( + task, mock.ANY) + mock_validate_hardware.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(redfish_boot.RedfishHttpsBoot, '_validate_hardware', + autospec=True) + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + autospec=True) + def test_validate_correct_vendor(self, mock_validate_image_properties, + mock_parse_driver_info, + mock_validate_hardware): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.instance_info.update( + {'kernel': 'kernel', + 'ramdisk': 'ramdisk', + 'image_source': 'http://image/source'} + ) + + task.node.driver_info.update( + {'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader'} + ) + + task.node.properties['vendor'] = "Ironic Co." + + task.driver.boot.validate(task) + mock_validate_hardware.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(redfish_boot.RedfishHttpsBoot, '_validate_hardware', + autospec=True) + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + autospec=True) + def test_validate_missing(self, mock_validate_image_properties, + mock_parse_driver_info, mock_validate_hardware): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.MissingParameterValue, + task.driver.boot.validate, task) + mock_validate_hardware.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + def test_validate_inspection(self, mock_parse_driver_info): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_info.update( + {'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader'} + ) + + task.driver.boot.validate_inspection(task) + + mock_parse_driver_info.assert_called_once_with(task.node) + + @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', + autospec=True) + @mock.patch.object(image_utils, 'prepare_deploy_iso', autospec=True) + @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) + @mock.patch.object(redfish_boot.manager_utils, 'node_power_action', + autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_prepare_ramdisk_no_debug( + self, mock_system, mock_boot_mode_utils, mock_node_power_action, + mock__parse_driver_info, + mock_prepare_deploy_iso, mock_node_set_boot_device): + self.config(debug=False) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + + mock__parse_driver_info.return_value = {} + mock_prepare_deploy_iso.return_value = 'image-url' + + task.driver.boot.prepare_ramdisk(task, {}) + + mock_node_power_action.assert_called_once_with( + task, states.POWER_OFF) + + expected_params = { + 'ipa-agent-token': mock.ANY, + 'boot_method': 'vmedia', + } + + mock_prepare_deploy_iso.assert_called_once_with( + task, expected_params, 'deploy', {}) + + mock_node_set_boot_device.assert_called_once_with( + task, boot_devices.UEFIHTTP, False) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + + @mock.patch.object(manager_utils, 'is_fast_track', lambda task: True) + @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', + autospec=True) + @mock.patch.object(image_utils, 'prepare_deploy_iso', autospec=True) + @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) + @mock.patch.object(redfish_boot.manager_utils, 'node_power_action', + autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_prepare_ramdisk_fast_track( + self, mock_system, mock_boot_mode_utils, mock_node_power_action, + mock__parse_driver_info, + mock_prepare_deploy_iso, mock_node_set_boot_device): + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.node.provision_state = states.DEPLOYING + + task.driver.boot.prepare_ramdisk(task, {}) + + mock_node_power_action.assert_not_called() + mock_prepare_deploy_iso.assert_not_called() + mock_node_set_boot_device.assert_not_called() + mock_boot_mode_utils.sync_boot_mode.assert_not_called() + + @mock.patch.object(image_utils, 'cleanup_iso_image', autospec=True) + @mock.patch.object(image_utils, 'cleanup_floppy_image', autospec=True) + @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_clean_up_ramdisk( + self, mock_system, mock__parse_driver_info, + mock_cleanup_floppy_image, mock_cleanup_iso_image): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + task.node.driver_info['config_via_removable'] = True + + task.driver.boot.clean_up_ramdisk(task) + + mock_cleanup_iso_image.assert_called_once_with(task) + + @mock.patch.object(redfish_boot.RedfishHttpsBoot, + '_clean_up', autospec=True) + @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True) + @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) + @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) + @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_prepare_instance_normal_boot( + self, mock_system, mock_boot_mode_utils, mock_deploy_utils, + mock_manager_utils, mock__parse_deploy_info, + mock_prepare_boot_iso, mock_clean_up_instance): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + task.node.driver_internal_info[ + 'root_uuid_or_disk_id'] = self.node.uuid + + mock_deploy_utils.get_boot_option.return_value = 'net' + + d_info = { + 'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader' + } + + mock__parse_deploy_info.return_value = d_info + + mock_prepare_boot_iso.return_value = 'image-url' + + task.driver.boot.prepare_instance(task) + + expected_params = { + 'root_uuid': self.node.uuid + } + + mock_prepare_boot_iso.assert_called_once_with( + task, d_info, **expected_params) + + mock_manager_utils.node_set_boot_device.assert_called_once_with( + task, boot_devices.UEFIHTTP, persistent=True) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + csb = mock_boot_mode_utils.configure_secure_boot_if_needed + csb.assert_called_once_with(task) + + @mock.patch.object(redfish_boot.RedfishHttpsBoot, + '_clean_up', autospec=True) + @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True) + @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) + @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', + autospec=True) + @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_prepare_instance_ramdisk_boot( + self, mock_system, mock_boot_mode_utils, mock_deploy_utils, + mock_node_set_boot_device, mock__parse_deploy_info, + mock_prepare_boot_iso, mock_clean_up_instance): + + configdrive = 'Y29udGVudA==' + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + task.node.driver_internal_info[ + 'root_uuid_or_disk_id'] = self.node.uuid + task.node.instance_info['configdrive'] = configdrive + + mock_deploy_utils.get_boot_option.return_value = 'ramdisk' + + d_info = { + 'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader' + } + mock__parse_deploy_info.return_value = d_info + + mock_prepare_boot_iso.return_value = 'image-url' + + task.driver.boot.prepare_instance(task) + + mock_clean_up_instance.assert_called_once_with(mock.ANY, task) + + mock_prepare_boot_iso.assert_called_once_with(task, d_info) + + mock_node_set_boot_device.assert_called_once_with( + task, boot_devices.UEFIHTTP, persistent=True) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + + @mock.patch.object(redfish_boot.RedfishHttpsBoot, + '_clean_up', autospec=True) + @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True) + @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) + @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', + autospec=True) + @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_prepare_instance_ramdisk_boot_iso( + self, mock_system, mock_boot_mode_utils, mock_deploy_utils, + mock_node_set_boot_device, mock__parse_deploy_info, + mock_prepare_boot_iso, + mock_clean_up_instance): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + task.node.driver_internal_info[ + 'root_uuid_or_disk_id'] = self.node.uuid + task.node.instance_info['configdrive'] = None + + mock_deploy_utils.get_boot_option.return_value = 'ramdisk' + + d_info = { + 'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader' + } + + mock__parse_deploy_info.return_value = d_info + mock_prepare_boot_iso.return_value = 'image-url' + + task.driver.boot.prepare_instance(task) + + mock_prepare_boot_iso.assert_called_once_with(task, d_info) + + mock_node_set_boot_device.assert_called_once_with( + task, boot_devices.UEFIHTTP, persistent=True) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + + @mock.patch.object(image_utils, 'cleanup_disk_image', autospec=True) + @mock.patch.object(image_utils, 'cleanup_iso_image', autospec=True) + @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True) + @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) + @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', + autospec=True) + @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_prepare_instance_ramdisk_boot_iso_boot( + self, mock_system, mock_boot_mode_utils, mock_deploy_utils, + mock_node_set_boot_device, mock__parse_deploy_info, + mock_prepare_boot_iso, + mock_image_cleanup, mock_disk_cleanup): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + i_info = task.node.instance_info + i_info['boot_iso'] = "super-magic" + del i_info['configdrive'] + task.node.instance_info = i_info + mock_deploy_utils.get_boot_option.return_value = 'ramdisk' + mock__parse_deploy_info.return_value = {} + + mock_prepare_boot_iso.return_value = 'image-url' + + task.driver.boot.prepare_instance(task) + + mock_prepare_boot_iso.assert_called_once_with(task, {}) + + mock_node_set_boot_device.assert_called_once_with( + task, boot_devices.UEFIHTTP, persistent=True) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + mock_image_cleanup.assert_called_once_with(task) + mock_disk_cleanup.assert_not_called() + + @mock.patch.object(image_utils, 'cleanup_disk_image', autospec=True) + @mock.patch.object(image_utils, 'cleanup_iso_image', autospec=True) + @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True) + @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) + @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', + autospec=True) + @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_prepare_instance_ramdisk_boot_render_configdrive( + self, mock_system, mock_boot_mode_utils, mock_deploy_utils, + mock_node_set_boot_device, mock__parse_deploy_info, + mock_prepare_boot_iso, + mock_image_cleanup, mock_disk_cleanup): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + task.node.driver_internal_info[ + 'root_uuid_or_disk_id'] = self.node.uuid + task.node.instance_info['configdrive'] = {'meta_data': {}} + + mock_deploy_utils.get_boot_option.return_value = 'ramdisk' + + d_info = { + 'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader' + } + mock__parse_deploy_info.return_value = d_info + + mock_prepare_boot_iso.return_value = 'image-url' + + task.driver.boot.prepare_instance(task) + + mock_prepare_boot_iso.assert_called_once_with(task, d_info) + + mock_node_set_boot_device.assert_called_once_with( + task, boot_devices.UEFIHTTP, persistent=True) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + mock_image_cleanup.assert_called_once_with(task) + mock_disk_cleanup.assert_not_called() + + @mock.patch.object(boot_mode_utils, 'configure_secure_boot_if_needed', + autospec=True) + @mock.patch.object(boot_mode_utils, 'sync_boot_mode', autospec=True) + @mock.patch.object(image_utils, 'cleanup_iso_image', autospec=True) + @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def _test_prepare_instance_local_boot( + self, mock_system, mock_manager_utils, + mock_cleanup_iso_image, mock_sync_boot_mode, + mock_secure_boot): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + task.node.driver_internal_info[ + 'root_uuid_or_disk_id'] = self.node.uuid + + task.driver.boot.prepare_instance(task) + + mock_manager_utils.node_set_boot_device.assert_called_once_with( + task, boot_devices.DISK, persistent=True) + mock_cleanup_iso_image.assert_called_once_with(task) + mock_sync_boot_mode.assert_called_once_with(task) + mock_secure_boot.assert_called_once_with(task) + + def test_prepare_instance_local_whole_disk_image(self): + self.node.driver_internal_info = {'is_whole_disk_image': True} + self.node.save() + self._test_prepare_instance_local_boot() + + def test_prepare_instance_local_boot_option(self): + instance_info = self.node.instance_info + instance_info['capabilities'] = '{"boot_option": "local"}' + self.node.instance_info = instance_info + self.node.save() + self._test_prepare_instance_local_boot() + + @mock.patch.object(boot_mode_utils, 'deconfigure_secure_boot_if_needed', + autospec=True) + @mock.patch.object(image_utils, 'cleanup_iso_image', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def _test_clean_up_instance(self, mock_system, mock_cleanup_iso_image, + mock_secure_boot): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + + task.driver.boot.clean_up_instance(task) + + mock_cleanup_iso_image.assert_called_once_with(task) + mock_secure_boot.assert_called_once_with(task) + + def test_clean_up_instance_only_cdrom(self): + self._test_clean_up_instance() + + def test_clean_up_instance_cdrom_and_floppy(self): + driver_info = self.node.driver_info + driver_info['config_via_removable'] = True + self.node.driver_info = driver_info + self.node.save() + self._test_clean_up_instance() + + @mock.patch.object(boot_mode_utils, 'deconfigure_secure_boot_if_needed', + autospec=True) + @mock.patch.object(deploy_utils, 'get_boot_option', autospec=True) + @mock.patch.object(image_utils, 'cleanup_disk_image', autospec=True) + @mock.patch.object(image_utils, 'cleanup_iso_image', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_clean_up_instance_ramdisk(self, mock_system, + mock_cleanup_iso_image, + mock_cleanup_disk_image, + mock_get_boot_option, + mock_secure_boot): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_get_boot_option.return_value = 'ramdisk' + + task.driver.boot.clean_up_instance(task) + + mock_cleanup_iso_image.assert_called_once_with(task) + + mock_secure_boot.assert_called_once_with(task) + mock_cleanup_disk_image.assert_not_called() diff --git a/ironic/tests/unit/drivers/modules/redfish/test_management.py b/ironic/tests/unit/drivers/modules/redfish/test_management.py index 02a0ac7a9c..684031238d 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_management.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_management.py @@ -121,7 +121,8 @@ class RedfishManagementTestCase(db_base.DbTestCase): (boot_devices.PXE, sushy.BOOT_SOURCE_TARGET_PXE), (boot_devices.DISK, sushy.BOOT_SOURCE_TARGET_HDD), (boot_devices.CDROM, sushy.BOOT_SOURCE_TARGET_CD), - (boot_devices.BIOS, sushy.BOOT_SOURCE_TARGET_BIOS_SETUP) + (boot_devices.BIOS, sushy.BOOT_SOURCE_TARGET_BIOS_SETUP), + (boot_devices.UEFIHTTP, sushy.BOOT_SOURCE_TARGET_UEFI_HTTP) ] for target, expected in expected_values: @@ -130,7 +131,8 @@ class RedfishManagementTestCase(db_base.DbTestCase): # Asserts fake_system.set_system_boot_options.assert_has_calls( [mock.call(expected, - enabled=sushy.BOOT_SOURCE_ENABLED_ONCE)]) + enabled=sushy.BOOT_SOURCE_ENABLED_ONCE, + http_boot_uri=None)]) mock_get_system.assert_called_with(task.node) self.assertNotIn('redfish_boot_device', task.node.driver_internal_info) @@ -156,7 +158,7 @@ class RedfishManagementTestCase(db_base.DbTestCase): fake_system.set_system_boot_options.assert_has_calls( [mock.call(sushy.BOOT_SOURCE_TARGET_PXE, - enabled=expected)]) + enabled=expected, http_boot_uri=None)]) mock_get_system.assert_called_with(task.node) self.assertNotIn('redfish_boot_device', task.node.driver_internal_info) @@ -183,7 +185,8 @@ class RedfishManagementTestCase(db_base.DbTestCase): task, boot_devices.PXE, persistent=target) fake_system.set_system_boot_options.assert_has_calls( - [mock.call(sushy.BOOT_SOURCE_TARGET_PXE, enabled=None)]) + [mock.call(sushy.BOOT_SOURCE_TARGET_PXE, enabled=None, + http_boot_uri=None)]) mock_get_system.assert_called_with(task.node) # Reset mocks @@ -205,7 +208,8 @@ class RedfishManagementTestCase(db_base.DbTestCase): task.driver.management.set_boot_device, task, boot_devices.PXE) fake_system.set_system_boot_options.assert_called_once_with( sushy.BOOT_SOURCE_TARGET_PXE, - enabled=sushy.BOOT_SOURCE_ENABLED_ONCE) + enabled=sushy.BOOT_SOURCE_ENABLED_ONCE, + http_boot_uri=None) mock_get_system.assert_called_once_with(task.node) self.assertNotIn('redfish_boot_device', task.node.driver_internal_info) @@ -232,7 +236,8 @@ class RedfishManagementTestCase(db_base.DbTestCase): task.driver.management.set_boot_device, task, boot_devices.PXE, persistent=target) fake_system.set_system_boot_options.assert_called_once_with( - sushy.BOOT_SOURCE_TARGET_PXE, enabled=None) + sushy.BOOT_SOURCE_TARGET_PXE, enabled=None, + http_boot_uri=None) mock_get_system.assert_called_once_with(task.node) self.assertNotIn('redfish_boot_device', task.node.driver_internal_info) @@ -258,9 +263,11 @@ class RedfishManagementTestCase(db_base.DbTestCase): task, boot_devices.PXE, persistent=True) fake_system.set_system_boot_options.assert_has_calls([ mock.call(sushy.BOOT_SOURCE_TARGET_PXE, - enabled=sushy.BOOT_SOURCE_ENABLED_CONTINUOUS), + enabled=sushy.BOOT_SOURCE_ENABLED_CONTINUOUS, + http_boot_uri=None), mock.call(sushy.BOOT_SOURCE_TARGET_PXE, - enabled=sushy.BOOT_SOURCE_ENABLED_ONCE) + enabled=sushy.BOOT_SOURCE_ENABLED_ONCE, + http_boot_uri=None) ]) mock_get_system.assert_called_with(task.node) @@ -292,7 +299,8 @@ class RedfishManagementTestCase(db_base.DbTestCase): task.driver.management.set_boot_device( task, boot_devices.PXE, persistent=True) fake_system.set_system_boot_options.assert_called_once_with( - sushy.BOOT_SOURCE_TARGET_PXE, enabled=expected) + sushy.BOOT_SOURCE_TARGET_PXE, enabled=expected, + http_boot_uri=None) if vendor == 'SuperMicro': mock_sync_boot_mode.assert_called_once_with(task) else: @@ -303,6 +311,28 @@ class RedfishManagementTestCase(db_base.DbTestCase): mock_sync_boot_mode.reset_mock() mock_get_system.reset_mock() + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_set_boot_device_http_boot(self, mock_get_system): + fake_system = mock.Mock() + mock_get_system.return_value = fake_system + self.node.driver_internal_info = { + 'redfish_uefi_http_url': 'http://foo.url'} + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.management.set_boot_device(task, + boot_devices.UEFIHTTP) + fake_system.set_system_boot_options.assert_has_calls( + [mock.call(sushy.BOOT_SOURCE_TARGET_UEFI_HTTP, + enabled=sushy.BOOT_SOURCE_ENABLED_ONCE, + http_boot_uri='http://foo.url')]) + mock_get_system.assert_called_with(task.node) + self.assertNotIn('redfish_boot_device', + task.node.driver_internal_info) + task.node.refresh() + self.assertNotIn('redfish_uefi_http_url', + task.node.driver_internal_info) + def test_restore_boot_device(self): fake_system = mock.Mock() with task_manager.acquire(self.context, self.node.uuid, @@ -315,7 +345,8 @@ class RedfishManagementTestCase(db_base.DbTestCase): fake_system.set_system_boot_options.assert_called_once_with( sushy.BOOT_SOURCE_TARGET_HDD, - enabled=sushy.BOOT_SOURCE_ENABLED_ONCE) + enabled=sushy.BOOT_SOURCE_ENABLED_ONCE, + http_boot_uri=None) # The stored boot device is kept intact self.assertEqual( boot_devices.DISK, @@ -332,7 +363,8 @@ class RedfishManagementTestCase(db_base.DbTestCase): fake_system.set_system_boot_options.assert_called_once_with( sushy.BOOT_SOURCE_TARGET_HDD, - enabled=sushy.BOOT_SOURCE_ENABLED_ONCE) + enabled=sushy.BOOT_SOURCE_ENABLED_ONCE, + http_boot_uri=None) # The stored boot device is kept intact self.assertEqual( "hdd", @@ -362,7 +394,8 @@ class RedfishManagementTestCase(db_base.DbTestCase): fake_system.set_system_boot_options.assert_called_once_with( sushy.BOOT_SOURCE_TARGET_HDD, - enabled=sushy.BOOT_SOURCE_ENABLED_ONCE) + enabled=sushy.BOOT_SOURCE_ENABLED_ONCE, + http_boot_uri=None) self.assertTrue(mock_log.called) # The stored boot device is kept intact self.assertEqual( diff --git a/releasenotes/notes/add-redfish-httpboot-support-8d516158860c9d43.yaml b/releasenotes/notes/add-redfish-httpboot-support-8d516158860c9d43.yaml new file mode 100644 index 0000000000..09859e2070 --- /dev/null +++ b/releasenotes/notes/add-redfish-httpboot-support-8d516158860c9d43.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds support for Redfish based HTTPBoot, which leveragings the DMTF Redfish + ``HttpBootUri`` ``ComputerSystem`` resource in a BMC, to assert the URL + for the next boot operation. This requires Sushy 4.7.0 as the minimum + version. diff --git a/requirements.txt b/requirements.txt index 7df883dc07..cc17303f90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,6 +46,6 @@ psutil>=3.2.2 # BSD futurist>=1.2.0 # Apache-2.0 tooz>=2.7.0 # Apache-2.0 openstacksdk>=0.48.0 # Apache-2.0 -sushy>=4.3.0 +sushy>=4.7.0 construct>=2.9.39 # MIT netaddr>=0.9.0 # BSD diff --git a/setup.cfg b/setup.cfg index 705069ef6a..99d2972e28 100644 --- a/setup.cfg +++ b/setup.cfg @@ -79,6 +79,7 @@ ironic.hardware.interfaces.boot = irmc-virtual-media = ironic.drivers.modules.irmc.boot:IRMCVirtualMediaBoot pxe = ironic.drivers.modules.pxe:PXEBoot redfish-virtual-media = ironic.drivers.modules.redfish.boot:RedfishVirtualMediaBoot + redfish-https = ironic.drivers.modules.redfish.boot:RedfishHttpsBoot ironic.hardware.interfaces.console = fake = ironic.drivers.modules.fake:FakeConsole