Skip to content

Commit 88fcf8a

Browse files
Zuulopenstack-gerrit
Zuul
authored andcommitted
Merge "Redfish UefiHttp boot support"
2 parents 336f799 + 041a7d7 commit 88fcf8a

File tree

11 files changed

+1271
-21
lines changed

11 files changed

+1271
-21
lines changed

doc/source/admin/drivers/redfish.rst

+38-2
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ Enabling the Redfish driver
2323
#. Add ``redfish`` to the list of ``enabled_hardware_types``,
2424
``enabled_power_interfaces``, ``enabled_management_interfaces`` and
2525
``enabled_inspect_interfaces`` as well as ``redfish-virtual-media``
26-
to ``enabled_boot_interfaces`` in ``/etc/ironic/ironic.conf``.
26+
and ``redfish-https`` to ``enabled_boot_interfaces`` in
27+
``/etc/ironic/ironic.conf``.
2728
For example::
2829

2930
[DEFAULT]
3031
...
3132
enabled_hardware_types = ipmi,redfish
32-
enabled_boot_interfaces = ipxe,redfish-virtual-media
33+
enabled_boot_interfaces = ipxe,redfish-virtual-media,redfish-https
3334
enabled_power_interfaces = ipmitool,redfish
3435
enabled_management_interfaces = ipmitool,redfish
3536
enabled_inspect_interfaces = inspector,redfish
@@ -386,6 +387,41 @@ Layer 3 or DHCP-less ramdisk booting
386387
DHCP-less deploy is supported by the Redfish virtual media boot. See
387388
:doc:`/admin/dhcp-less` for more information.
388389

390+
Redfish HTTP(s) Boot
391+
====================
392+
393+
The ``redfish-https`` boot interface is very similar to the
394+
``redfish-virtual-media`` boot interface. In this driver, we compose an ISO
395+
image, and request the BMC to inform the UEFI firmware to boot the Ironic
396+
ramdisk, or a other ramdisk image. This approach is intended to allow a
397+
pattern of engagement where we have minimal reliance on addressing and
398+
discovery of the Ironic deployment through autoconfiguration like DHCP,
399+
and somewhat mirrors vendor examples of booting from an HTTP URL.
400+
401+
This interface has some basic constraints.
402+
403+
* There is no configuration drive functionality, while Virtual Media did
404+
help provide such functionality.
405+
* This interface *is* dependent upon BMC, EFI Firmware, and Bootloader,
406+
which means we may not see additional embedded files an contents in
407+
an ISO image. This is the same basic constraint over the ``ramdisk``
408+
deploy interface when using Network Booting.
409+
* This is a UEFI-Only boot interface. No legacy boot is possible with
410+
this interface.
411+
412+
A good starting point for this interface, is to think of it as
413+
higher security network boot, as we are explicitly telling the BMC
414+
where the node should boot from.
415+
416+
Like the ``redfish-virtual-media`` boot interface, you will need
417+
to create an EFI System Partition image (ESP_), see
418+
`Configuring an ESP image`_ for details on how to do this.
419+
420+
Additionally, if you would like to use the ``ramdisk`` deployment
421+
interface, the same basic instructions covered in `Virtual Media Ramdisk`_
422+
apply, just use ``redfish-https`` as the boot_interface, and keep in mind,
423+
no configuration drives exist with the ``redfish-https`` boot interface.
424+
389425
Firmware update using manual cleaning
390426
=====================================
391427

ironic/common/boot_devices.py

+3
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,6 @@
5252

5353
VMEDIA_DEVICES = [DISK, CDROM, FLOPPY]
5454
"""Devices that make sense for virtual media attachment."""
55+
56+
UEFIHTTP = "uefihttp"
57+
"Boot from a UEFI HTTP(s) URL"

ironic/common/exception.py

+6
Original file line numberDiff line numberDiff line change
@@ -883,3 +883,9 @@ class FirmwareComponentNotFound(NotFound):
883883

884884
class InvalidNodeInventory(Invalid):
885885
_msg_fmt = _("Inventory for node %(node)s is invalid: %(reason)s")
886+
887+
888+
class UnsupportedHardwareFeature(Invalid):
889+
_msg_fmt = _("Node %(node)s hardware does not support feature "
890+
"%(feature)s, which is required based upon the "
891+
"requested configuration.")

ironic/drivers/modules/redfish/boot.py

+303
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from ironic.common import exception
2222
from ironic.common.i18n import _
2323
from ironic.common import states
24+
from ironic.common import utils as common_utils
2425
from ironic.conductor import utils as manager_utils
2526
from ironic.conf import CONF
2627
from ironic.drivers import base
@@ -712,6 +713,11 @@ def _attach_configdrive(self, task, managers):
712713
if not configdrive:
713714
return
714715

716+
if 'ramdisk_boot_configdrive' not in self.capabilities:
717+
raise exception.InstanceDeployFailure(
718+
_('Cannot attach a configdrive to node %s, as it is not '
719+
'supported in the driver.') % task.node.uuid)
720+
715721
_eject_vmedia(task, managers, sushy.VIRTUAL_MEDIA_USBSTICK)
716722
cd_ref = image_utils.prepare_configdrive_image(task, configdrive)
717723
try:
@@ -775,3 +781,300 @@ def _set_boot_device(cls, task, device, persistent=False):
775781
ManagementInterface fails.
776782
"""
777783
manager_utils.node_set_boot_device(task, device, persistent)
784+
785+
786+
class RedfishHttpsBoot(base.BootInterface):
787+
"""A driver which utilizes UefiHttp like virtual media.
788+
789+
Utilizes the virtual media image build to craft a ISO image to
790+
signal to remote BMC to boot.
791+
792+
This interface comes with some constraints. For example, this
793+
interface is built under the operating assumption that DHCP is
794+
used. The UEFI Firmware needs to load some base configuration,
795+
regardless. Also depending on UEFI Firmware, and how it handles
796+
UefiHttp Boot, additional ISO contents, such as "configuration drive"
797+
materials might be unavailable. A similar constraint exists with
798+
``ramdisk`` deployment.
799+
"""
800+
801+
capabilities = ['ramdisk_boot']
802+
803+
def get_properties(self):
804+
"""Return the properties of the interface.
805+
806+
:returns: dictionary of <property name>:<property description> entries.
807+
"""
808+
return REQUIRED_PROPERTIES
809+
810+
def _validate_driver_info(self, task):
811+
"""Validate the prerequisites for Redfish HTTPS based boot.
812+
813+
This method validates whether the 'driver_info' property of the
814+
supplied node contains the required information for this driver.
815+
816+
:param task: a TaskManager instance containing the node to act on.
817+
:raises: InvalidParameterValue if any parameters are incorrect
818+
:raises: MissingParameterValue if some mandatory information
819+
is missing on the node
820+
"""
821+
node = task.node
822+
823+
_parse_driver_info(node)
824+
# Issue the deprecation warning if needed
825+
driver_utils.get_agent_iso(node, deprecated_prefix='redfish')
826+
827+
def _validate_instance_info(self, task):
828+
"""Validate instance image information for the task's node.
829+
830+
This method validates whether the 'instance_info' property of the
831+
supplied node contains the required information for this driver.
832+
833+
:param task: a TaskManager instance containing the node to act on.
834+
:raises: InvalidParameterValue if any parameters are incorrect
835+
:raises: MissingParameterValue if some mandatory information
836+
is missing on the node
837+
"""
838+
node = task.node
839+
840+
# NOTE(dtantsur): if we're are writing an image with local boot
841+
# the boot interface does not care about image parameters and
842+
# must not validate them.
843+
if (not task.driver.storage.should_write_image(task)
844+
or deploy_utils.get_boot_option(node) == 'local'):
845+
return
846+
847+
d_info = _parse_deploy_info(node)
848+
deploy_utils.validate_image_properties(task, d_info)
849+
850+
def _validate_hardware(self, task):
851+
"""Validates hardware support.
852+
853+
:param task: a TaskManager instance containing the node to act on.
854+
:raises: InvalidParameterValue if vendor not supported
855+
"""
856+
system = redfish_utils.get_system(task.node)
857+
if "UefiHttp" not in system.boot.allowed_values:
858+
raise exception.UnsupportedHardwareFeature(
859+
node=task.node.uuid,
860+
feature="UefiHttp boot")
861+
862+
def validate(self, task):
863+
"""Validate the deployment information for the task's node.
864+
865+
This method validates whether the 'driver_info' and/or 'instance_info'
866+
properties of the task's node contains the required information for
867+
this interface to function.
868+
869+
:param task: A TaskManager instance containing the node to act on.
870+
:raises: InvalidParameterValue on malformed parameter(s)
871+
:raises: MissingParameterValue on missing parameter(s)
872+
"""
873+
self._validate_hardware(task)
874+
self._validate_driver_info(task)
875+
self._validate_instance_info(task)
876+
877+
def validate_inspection(self, task):
878+
"""Validate that the node has required properties for inspection.
879+
880+
:param task: A TaskManager instance with the node being checked
881+
:raises: MissingParameterValue if node is missing one or more required
882+
parameters
883+
:raises: UnsupportedDriverExtension
884+
"""
885+
try:
886+
self._validate_driver_info(task)
887+
except exception.MissingParameterValue:
888+
# Fall back to non-managed in-band inspection
889+
raise exception.UnsupportedDriverExtension(
890+
driver=task.node.driver, extension='inspection')
891+
892+
def prepare_ramdisk(self, task, ramdisk_params):
893+
"""Prepares the boot of the agent ramdisk.
894+
895+
This method prepares the boot of the deploy or rescue ramdisk after
896+
reading relevant information from the node's driver_info and
897+
instance_info.
898+
899+
:param task: A task from TaskManager.
900+
:param ramdisk_params: the parameters to be passed to the ramdisk.
901+
:returns: None
902+
:raises: MissingParameterValue, if some information is missing in
903+
node's driver_info or instance_info.
904+
:raises: InvalidParameterValue, if some information provided is
905+
invalid.
906+
:raises: IronicException, if some power or set boot boot device
907+
operation failed on the node.
908+
"""
909+
node = task.node
910+
if not driver_utils.need_prepare_ramdisk(node):
911+
return
912+
913+
d_info = _parse_driver_info(node)
914+
915+
if manager_utils.is_fast_track(task):
916+
LOG.debug('Fast track operation for node %s, not setting up '
917+
'a HTTP Boot url', node.uuid)
918+
return
919+
920+
can_config = d_info.pop('can_provide_config', True)
921+
if can_config:
922+
manager_utils.add_secret_token(node, pregenerated=True)
923+
node.save()
924+
ramdisk_params['ipa-agent-token'] = \
925+
node.driver_internal_info['agent_secret_token']
926+
927+
manager_utils.node_power_action(task, states.POWER_OFF)
928+
929+
deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task)
930+
if deploy_nic_mac is not None:
931+
ramdisk_params['BOOTIF'] = deploy_nic_mac
932+
if CONF.debug and 'ipa-debug' not in ramdisk_params:
933+
ramdisk_params['ipa-debug'] = '1'
934+
935+
# NOTE(TheJulia): This is a mandatory setting for virtual media
936+
# based deployment operations and boot modes similar where we
937+
# want the ramdisk to consider embedded configuration.
938+
ramdisk_params['boot_method'] = 'vmedia'
939+
940+
mode = deploy_utils.rescue_or_deploy_mode(node)
941+
942+
iso_ref = image_utils.prepare_deploy_iso(task, ramdisk_params,
943+
mode, d_info)
944+
boot_mode_utils.sync_boot_mode(task)
945+
946+
self._set_boot_device(task, boot_devices.UEFIHTTP,
947+
http_boot_url=iso_ref)
948+
949+
LOG.debug("Node %(node)s is set to one time boot from "
950+
"%(device)s", {'node': task.node.uuid,
951+
'device': boot_devices.UEFIHTTP})
952+
953+
def clean_up_ramdisk(self, task):
954+
"""Cleans up the boot of ironic ramdisk.
955+
956+
This method cleans up the environment that was setup for booting the
957+
deploy ramdisk.
958+
959+
:param task: A task from TaskManager.
960+
:returns: None
961+
"""
962+
if manager_utils.is_fast_track(task):
963+
LOG.debug('Fast track operation for node %s, not ejecting '
964+
'any devices', task.node.uuid)
965+
return
966+
967+
LOG.debug("Cleaning up deploy boot for "
968+
"%(node)s", {'node': task.node.uuid})
969+
self._clean_up(task)
970+
971+
def prepare_instance(self, task):
972+
"""Prepares the boot of instance over virtual media.
973+
974+
This method prepares the boot of the instance after reading
975+
relevant information from the node's instance_info.
976+
977+
The internal logic is as follows:
978+
979+
- Cleanup any related files
980+
- Sync the boot mode with the machine.
981+
- Configure Secure boot, if required.
982+
- If local boot, or a whole disk image was deployed,
983+
set the next boot device as disk.
984+
- If "ramdisk" is the desired, then the UefiHttp boot
985+
option is set to the BMC with a request for this to
986+
be persistent.
987+
988+
:param task: a task from TaskManager.
989+
:returns: None
990+
:raises: InstanceDeployFailure, if its try to boot iSCSI volume in
991+
'BIOS' boot mode.
992+
"""
993+
node = task.node
994+
995+
self._clean_up(task)
996+
997+
boot_mode_utils.sync_boot_mode(task)
998+
boot_mode_utils.configure_secure_boot_if_needed(task)
999+
1000+
boot_option = deploy_utils.get_boot_option(node)
1001+
iwdi = node.driver_internal_info.get('is_whole_disk_image')
1002+
if boot_option == "local" or iwdi:
1003+
self._set_boot_device(task, boot_devices.DISK, persistent=True)
1004+
1005+
LOG.debug("Node %(node)s is set to permanently boot from local "
1006+
"%(device)s", {'node': task.node.uuid,
1007+
'device': boot_devices.DISK})
1008+
return
1009+
1010+
params = {}
1011+
1012+
if boot_option != 'ramdisk':
1013+
root_uuid = node.driver_internal_info.get('root_uuid_or_disk_id')
1014+
if not root_uuid and task.driver.storage.should_write_image(task):
1015+
LOG.warning(
1016+
"The UUID of the root partition could not be found for "
1017+
"node %s. Booting instance from disk anyway.", node.uuid)
1018+
1019+
self._set_boot_device(task, boot_devices.DISK, persistent=True)
1020+
1021+
return
1022+
1023+
params.update(root_uuid=root_uuid)
1024+
1025+
deploy_info = _parse_deploy_info(node)
1026+
1027+
iso_ref = image_utils.prepare_boot_iso(task, deploy_info, **params)
1028+
self._set_boot_device(task, boot_devices.UEFIHTTP, persistent=True,
1029+
http_boot_url=iso_ref)
1030+
1031+
LOG.debug("Node %(node)s is set to permanently boot from "
1032+
"%(device)s", {'node': task.node.uuid,
1033+
'device': boot_devices.UEFIHTTP})
1034+
1035+
def _clean_up(self, task):
1036+
image_utils.cleanup_iso_image(task)
1037+
1038+
def clean_up_instance(self, task):
1039+
"""Cleans up the boot of instance.
1040+
1041+
This method cleans up the environment that was setup for booting
1042+
the instance.
1043+
1044+
:param task: A task from TaskManager.
1045+
:returns: None
1046+
"""
1047+
LOG.debug("Cleaning up instance boot for "
1048+
"%(node)s", {'node': task.node.uuid})
1049+
self._clean_up(task)
1050+
boot_mode_utils.deconfigure_secure_boot_if_needed(task)
1051+
1052+
@classmethod
1053+
def _set_boot_device(cls, task, device, persistent=False,
1054+
http_boot_url=None):
1055+
"""Set the boot device for a node.
1056+
1057+
This is a hook method which can be used by other drivers based upon
1058+
this class, in order to facilitate vendor specific logic,
1059+
if needed.
1060+
1061+
Furthermore, we are not considering a *lack* of a URL as fatal.
1062+
A driver could easily update DHCP and send the message to the BMC.
1063+
1064+
:param task: a TaskManager instance.
1065+
:param device: the boot device, one of
1066+
:mod:`ironic.common.boot_devices`.
1067+
:param persistent: Whether to set next-boot, or make the change
1068+
permanent. Default: False.
1069+
:param http_boot_url: The URL to send to the BMC in order to boot
1070+
the node via UEFIHTTP.
1071+
:raises: InvalidParameterValue if the validation of the
1072+
ManagementInterface fails.
1073+
"""
1074+
1075+
if http_boot_url:
1076+
common_utils.set_node_nested_field(
1077+
task.node, 'driver_internal_info',
1078+
'redfish_uefi_http_url', http_boot_url)
1079+
task.node.save()
1080+
manager_utils.node_set_boot_device(task, device, persistent)

0 commit comments

Comments
 (0)