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 7abf7b3e23..fb786ea12c 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