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
This commit is contained in:
Julia Kreger 2023-11-14 14:55:57 -08:00
parent 76f68582d6
commit 041a7d7064
11 changed files with 1271 additions and 21 deletions

View File

@ -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
=====================================

View File

@ -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"

View File

@ -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.")

View File

@ -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 <property name>:<property description> 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)

View File

@ -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') %

View File

@ -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):

View File

@ -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()

View File

@ -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(

View File

@ -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.

View File

@ -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

View File

@ -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