diff --git a/devstack/files/debs/ironic b/devstack/files/debs/ironic index 50fa67d7f7..e769a2d7f9 100644 --- a/devstack/files/debs/ironic +++ b/devstack/files/debs/ironic @@ -21,6 +21,8 @@ libguestfs-tools libvirt-bin # dist:xenial,bionic NOPRIME open-iscsi openssh-client +# TODO (etingof) pinning to older version in devstack/lib/ironic +#ovmf pxelinux # dist:xenial,bionic python-libguestfs qemu diff --git a/devstack/lib/ironic b/devstack/lib/ironic index 614cdf868c..fa28259ce5 100644 --- a/devstack/lib/ironic +++ b/devstack/lib/ironic @@ -246,6 +246,10 @@ IRONIC_DEPLOY_RAMDISK=${IRONIC_DEPLOY_RAMDISK:-$TOP_DIR/files/ir-deploy-$IRONIC_ IRONIC_DEPLOY_KERNEL=${IRONIC_DEPLOY_KERNEL:-$TOP_DIR/files/ir-deploy-$IRONIC_DEPLOY_DRIVER.kernel} IRONIC_DEPLOY_ISO=${IRONIC_DEPLOY_ISO:-$TOP_DIR/files/ir-deploy-$IRONIC_DEPLOY_DRIVER.iso} +# If present, this file is used to deploy/boot nodes over virtual media +# (The value must be an absolute path) +IRONIC_EFIBOOT=${IRONIC_EFIBOOT:-$TOP_DIR/files/ir-deploy-$IRONIC_DEPLOY_DRIVER.efiboot} + # NOTE(jroll) this needs to be updated when stable branches are cut IPA_DOWNLOAD_BRANCH=${IPA_DOWNLOAD_BRANCH:-master} IPA_DOWNLOAD_BRANCH=$(echo $IPA_DOWNLOAD_BRANCH | tr / -) @@ -528,6 +532,14 @@ if [[ "$IRONIC_BOOT_MODE" == "uefi" ]]; then die $LINENO "Boot mode UEFI only works in Ubuntu or Fedora for now." fi + if is_arch "x86_64"; then + if is_ubuntu; then + install_package grub-efi + elif is_fedora; then + install_package grub2 grub2-efi + fi + fi + if is_ubuntu && [[ -z $IRONIC_GRUB2_FILE ]]; then IRONIC_GRUB2_SHIM_FILE=/usr/lib/shim/shimx64.efi IRONIC_GRUB2_FILE=/usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed @@ -2519,6 +2531,63 @@ function build_ipa_dib_ramdisk { rm -rf $tempdir } +# download EFI boot loader image and upload it to glance +# this function sets ``IRONIC_EFIBOOT_ID`` +function upload_baremetal_ironic_efiboot { + declare -g IRONIC_EFIBOOT_ID + + local efiboot_name + efiboot_name=$(basename $IRONIC_EFIBOOT) + + echo_summary "Building and uploading EFI boot image for ironic" + + if [ ! -e "$IRONIC_EFIBOOT" ]; then + + local efiboot_path + efiboot_path=$(mktemp -d --tmpdir=${DEST})/$efiboot_name + + local efiboot_mount + efiboot_mount=$(mktemp -d --tmpdir=${DEST}) + + dd if=/dev/zero \ + of=$efiboot_path \ + bs=4096 count=1024 + + mkfs.fat -s 4 -r 512 -S 4096 $efiboot_path + + sudo mount $efiboot_path $efiboot_mount + + sudo mkdir -p $efiboot_mount/efi/boot + + sudo grub-mkimage \ + -C xz \ + -O x86_64-efi \ + -p /boot/grub \ + -o $efiboot_mount/efi/boot/bootx64.efi \ + boot linux linuxefi search normal configfile \ + part_gpt btrfs ext2 fat iso9660 loopback \ + test keystatus gfxmenu regexp probe \ + efi_gop efi_uga all_video gfxterm font \ + echo read ls cat png jpeg halt reboot + + sudo umount $efiboot_mount + + # load efiboot into glance + IRONIC_EFIBOOT_ID=$(openstack \ + image create \ + $efiboot_name \ + --public --disk-format=raw \ + --container-format=bare \ + -f value -c id \ + < $efiboot_path) + die_if_not_set $LINENO IRONIC_EFIBOOT_ID "Failed to load EFI bootloader image into glance" + + mv $efiboot_path $IRONIC_EFIBOOT + + iniset $IRONIC_CONF_FILE conductor bootloader $IRONIC_EFIBOOT_ID + fi +} + # build deploy kernel+ramdisk, then upload them to glance # this function sets ``IRONIC_DEPLOY_KERNEL_ID``, ``IRONIC_DEPLOY_RAMDISK_ID`` function upload_baremetal_ironic_deploy { @@ -2611,6 +2680,11 @@ function prepare_baremetal_basic_ops { fi upload_baremetal_ironic_deploy + + if [[ "$IRONIC_BOOT_MODE" == "uefi" && is_deployed_by_redfish ]]; then + upload_baremetal_ironic_efiboot + fi + configure_tftpd configure_iptables } diff --git a/ironic/conf/conductor.py b/ironic/conf/conductor.py index 03c235c769..1dd21355b8 100644 --- a/ironic/conf/conductor.py +++ b/ironic/conf/conductor.py @@ -240,6 +240,13 @@ opts = [ mutable=True, help=_('Glance ID, http:// or file:// URL of the initramfs of ' 'the default rescue image.')), + cfg.StrOpt('bootloader', + mutable=True, + help=_('Glance ID, http:// or file:// URL of the EFI system ' + 'partition image containing EFI boot loader. This image ' + 'will be used by ironic when building UEFI-bootable ISO ' + 'out of kernel and ramdisk. Required for UEFI boot from ' + 'partition images.')), ] diff --git a/ironic/conf/redfish.py b/ironic/conf/redfish.py index a49f2e5d1b..20cd3af5e0 100644 --- a/ironic/conf/redfish.py +++ b/ironic/conf/redfish.py @@ -43,7 +43,18 @@ opts = [ ('auto', _('Try HTTP session authentication first, ' 'fall back to basic HTTP authentication'))], default='auto', - help=_('Redfish HTTP client authentication method.')) + help=_('Redfish HTTP client authentication method.')), + cfg.StrOpt('swift_container', + default='ironic_redfish_container', + help=_('The Swift container to store Redfish driver data.')), + cfg.IntOpt('swift_object_expiry_timeout', + default=900, + help=_('Amount of time in seconds for Swift objects to ' + 'auto-expire.')), + cfg.StrOpt('kernel_append_params', + default='nofb nomodeset vga=normal', + help=_('Additional kernel parameters for baremetal ' + 'Virtual Media boot.')), ] diff --git a/ironic/drivers/modules/redfish/boot.py b/ironic/drivers/modules/redfish/boot.py new file mode 100644 index 0000000000..263649939d --- /dev/null +++ b/ironic/drivers/modules/redfish/boot.py @@ -0,0 +1,818 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import tempfile + +from oslo_log import log +from oslo_utils import importutils + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common.glance_service import service_utils +from ironic.common.i18n import _ +from ironic.common import images +from ironic.common import states +from ironic.common import swift +from ironic.conductor import utils as manager_utils +from ironic.conf import CONF +from ironic.drivers import base +from ironic.drivers.modules import boot_mode_utils +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules.redfish import utils as redfish_utils + +LOG = log.getLogger(__name__) + +REQUIRED_PROPERTIES = { + 'deploy_kernel': _("URL or Glance UUID of the deployment kernel. " + "Required."), + 'deploy_ramdisk': _("URL or Glance UUID of the ramdisk that is " + "mounted at boot time. Required.") +} + +OPTIONAL_PROPERTIES = { + 'config_via_floppy': _("Boolean value to indicate whether or not the " + "driver should use virtual media Floppy device " + "for passing configuration information to the " + "ramdisk. Defaults to False. Optional."), + 'bootloader': _("URL or Glance UUID of the EFI system partition " + "image containing EFI boot loader. This image will be " + "used by ironic when building UEFI-bootable ISO " + "out of kernel and ramdisk. Required for UEFI " + "boot from partition images.") +} + +RESCUE_PROPERTIES = { + 'rescue_kernel': _('URL or Glance UUID of the rescue kernel. This value ' + 'is required for rescue mode.'), + 'rescue_ramdisk': _('URL or Glance UUID of the rescue ramdisk with agent ' + 'that is used at node rescue time. This value is ' + 'required for rescue mode.'), +} + +COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() +COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) +COMMON_PROPERTIES.update(RESCUE_PROPERTIES) + +KERNEL_RAMDISK_LABELS = { + 'deploy': REQUIRED_PROPERTIES, + 'rescue': RESCUE_PROPERTIES +} + +sushy = importutils.try_import('sushy') + + +class RedfishVirtualMediaBoot(base.BootInterface): + """Virtual media boot interface over Redfish. + + Virtual Media allows booting the system from the "virtual" + CD/DVD drive containing the user image that BMC "inserts" + into the drive. + + The CD/DVD images must be in ISO format and (depending on + BMC implementation) could be pulled over HTTP, served as + iSCSI targets or NFS volumes. + + The baseline boot workflow looks like this: + + 1. Pull kernel, ramdisk and ESP (FAT partition image with EFI boot + loader) images (ESP is only needed for UEFI boot) + 2. Create bootable ISO out of images (#1), push it to Glance and + pass to the BMC as Swift temporary URL + 3. Optionally create floppy image with desired system configuration data, + push it to Glance and pass to the BMC as Swift temporary URL + 4. Insert CD/DVD and (optionally) floppy images and set proper boot mode + + For building deploy or rescue ISO, redfish boot interface uses + `deploy_kernel`/`deploy_ramdisk` or `rescue_kernel`/`rescue_ramdisk` + properties from `[instance_info]` or `[driver_info]`. + + For building boot (user) ISO, redfish boot interface seeks `kernel_id` + and `ramdisk_id` properties in the Glance image metadata found in + `[instance_info]image_source` node property. + """ + + capabilities = ['iscsi_volume_boot', 'ramdisk_boot'] + + def __init__(self): + """Initialize the Redfish virtual media boot interface. + + :raises: DriverLoadError if the driver can't be loaded due to + missing dependencies + """ + super(RedfishVirtualMediaBoot, self).__init__() + if not sushy: + raise exception.DriverLoadError( + driver='redfish', + reason=_('Unable to import the sushy library')) + + @staticmethod + def _parse_driver_info(node): + """Gets the driver specific Node deployment info. + + This method validates whether the 'driver_info' property of the + supplied node contains the required or optional information properly + for this driver to deploy images to the node. + + :param node: a target node of the deployment + :returns: the driver_info values of the node. + :raises: MissingParameterValue, if any of the required parameters are + missing. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + """ + d_info = node.driver_info + + mode = deploy_utils.rescue_or_deploy_mode(node) + params_to_check = KERNEL_RAMDISK_LABELS[mode] + + deploy_info = {option: d_info.get(option) + for option in params_to_check} + + if not any(deploy_info.values()): + # NOTE(dtantsur): avoid situation when e.g. deploy_kernel comes + # from driver_info but deploy_ramdisk comes from configuration, + # since it's a sign of a potential operator's mistake. + deploy_info = {k: getattr(CONF.conductor, k) + for k in params_to_check} + + error_msg = _("Error validating Redfish virtual media. Some " + "parameters were missing in node's driver_info") + + deploy_utils.check_for_missing_params(deploy_info, error_msg) + + deploy_info.update( + {option: d_info.get(option, getattr(CONF.conductor, option, None)) + for option in OPTIONAL_PROPERTIES}) + + deploy_info.update(redfish_utils.parse_driver_info(node)) + + return deploy_info + + @staticmethod + def _parse_instance_info(node): + """Gets the instance specific Node deployment info. + + This method validates whether the 'instance_info' property of the + supplied node contains the required or optional information properly + for this driver to deploy images to the node. + + :param node: a target node of the deployment + :returns: the instance_info values of the node. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + """ + deploy_info = node.instance_info.copy() + + # NOTE(etingof): this method is currently no-op, here for completeness + return deploy_info + + @classmethod + def _parse_deploy_info(cls, node): + """Gets the instance and driver specific Node deployment info. + + This method validates whether the 'instance_info' and 'driver_info' + property of the supplied node contains the required information for + this driver to deploy images to the node. + + :param node: a target node of the deployment + :returns: a dict with the instance_info and driver_info values. + :raises: MissingParameterValue, if any of the required parameters are + missing. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + """ + deploy_info = {} + deploy_info.update(deploy_utils.get_image_instance_info(node)) + deploy_info.update(cls._parse_driver_info(node)) + deploy_info.update(cls._parse_instance_info(node)) + + return deploy_info + + @staticmethod + def _delete_from_swift(task, container, object_name): + LOG.debug("Cleaning up image %(name)s from Swift container " + "%(container)s for node " + "%(node)s", {'node': task.node.uuid, + 'name': object_name, + 'container': container}) + + swift_api = swift.SwiftAPI() + + try: + swift_api.delete_object(container, object_name) + + except exception.SwiftOperationError as e: + LOG.warning("Failed to clean up image %(image)s for node " + "%(node)s. Error: %(error)s.", + {'node': task.node.uuid, 'image': object_name, + 'error': e}) + + @staticmethod + def _get_floppy_image_name(node): + """Returns the floppy image name for a given node. + + :param node: the node for which image name is to be provided. + """ + return "image-%s" % node.uuid + + @classmethod + def _cleanup_floppy_image(cls, task): + """Deletes the floppy image if it was created for the node. + + :param task: an ironic node object. + """ + floppy_object_name = cls._get_floppy_image_name(task.node) + + cls._delete_from_swift( + task, CONF.redfish.swift_container, floppy_object_name) + + @classmethod + def _prepare_floppy_image(cls, task, params=None): + """Prepares the floppy image for passing the parameters. + + This method prepares a temporary VFAT filesystem image and adds + a file into the image which contains parameters to be passed to + the ramdisk. Then this method uploads built image to Swift + '[redfish]swift_container', setting it to auto expire after + '[redfish]swift_object_expiry_timeout' seconds. Finally, a + temporary Swift URL is returned addressing Swift object just + created. + + :param task: a TaskManager instance containing the node to act on. + :param params: a dictionary containing 'parameter name'->'value' + mapping to be passed to deploy or rescue image via floppy image. + :raises: ImageCreationFailed, if it failed while creating the floppy + image. + :raises: SwiftOperationError, if any operation with Swift fails. + :returns: image URL for the floppy image. + """ + object_name = cls._get_floppy_image_name(task.node) + + container = CONF.redfish.swift_container + timeout = CONF.redfish.swift_object_expiry_timeout + + object_headers = {'X-Delete-After': str(timeout)} + swift_api = swift.SwiftAPI() + + LOG.debug("Trying to create floppy image for node " + "%(node)s", {'node': task.node.uuid}) + + with tempfile.NamedTemporaryFile( + dir=CONF.tempdir, suffix='.img') as vfat_image_tmpfile_obj: + + vfat_image_tmpfile = vfat_image_tmpfile_obj.name + images.create_vfat_image(vfat_image_tmpfile, parameters=params) + + swift_api.create_object(container, object_name, vfat_image_tmpfile, + object_headers=object_headers) + + image_url = swift_api.get_temp_url(container, object_name, timeout) + + LOG.debug("Created floppy image %(name)s in Swift for node %(node)s, " + "exposed as temporary URL " + "%(url)s", {'node': task.node.uuid, + 'name': object_name, + 'url': image_url}) + + return image_url + + @staticmethod + def _get_iso_image_name(node): + """Returns the boot iso image name for a given node. + + :param node: the node for which image name is to be provided. + """ + return "boot-%s" % node.uuid + + @classmethod + def _cleanup_iso_image(cls, task): + """Deletes the ISO if it was created for the instance. + + :param task: an ironic node object. + """ + iso_object_name = cls._get_iso_image_name(task.node) + + cls._delete_from_swift( + task, CONF.redfish.swift_container, iso_object_name) + + @classmethod + def _prepare_iso_image(cls, task, kernel_href, ramdisk_href, + bootloader_href=None, root_uuid=None, params=None): + """Prepare an ISO to boot the node. + + Build bootable ISO out of `kernel_href` and `ramdisk_href` (and + `bootloader` if it's UEFI boot), then push built image up to Swift and + return a temporary URL. + + :param task: a TaskManager instance containing the node to act on. + :param kernel_href: URL or Glance UUID of the kernel to use + :param ramdisk_href: URL or Glance UUID of the ramdisk to use + :param bootloader_href: URL or Glance UUID of the EFI bootloader + image to use when creating UEFI bootbable ISO + :param root_uuid: optional uuid of the root partition. + :param params: a dictionary containing 'parameter name'->'value' + mapping to be passed to kernel command line. + :returns: bootable ISO HTTP URL. + :raises: MissingParameterValue, if any of the required parameters are + missing. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + :raises: ImageCreationFailed, if creating ISO image failed. + """ + if not kernel_href or not ramdisk_href: + raise exception.InvalidParameterValue(_( + "Unable to find kernel or ramdisk for " + "building ISO for %(node)s") % + {'node': task.node.uuid}) + + if deploy_utils.get_boot_option(task.node) == "ramdisk": + i_info = task.node.instance_info + kernel_params = "root=/dev/ram0 text " + kernel_params += i_info.get("ramdisk_kernel_arguments", "") + else: + kernel_params = CONF.redfish.kernel_append_params + + if params: + kernel_params = ' '.join( + (kernel_params, ' '.join( + '%s=%s' % kv for kv in params.items()))) + + boot_mode = boot_mode_utils.get_boot_mode_for_deploy(task.node) + + LOG.debug("Trying to create %(boot_mode)s ISO image for node %(node)s " + "with kernel %(kernel_href)s, ramdisk %(ramdisk_href)s, " + "bootloader %(bootloader_href)s and kernel params %(params)s" + "", {'node': task.node.uuid, + 'boot_mode': boot_mode, + 'kernel_href': kernel_href, + 'ramdisk_href': ramdisk_href, + 'bootloader_href': bootloader_href, + 'params': kernel_params}) + + with tempfile.NamedTemporaryFile( + dir=CONF.tempdir, suffix='.iso') as fileobj: + boot_iso_tmp_file = fileobj.name + images.create_boot_iso( + task.context, boot_iso_tmp_file, + kernel_href, ramdisk_href, + esp_image_href=bootloader_href, + root_uuid=root_uuid, + kernel_params=kernel_params, + boot_mode=boot_mode) + + iso_object_name = cls._get_iso_image_name(task.node) + + container = CONF.redfish.swift_container + + timeout = CONF.redfish.swift_object_expiry_timeout + + object_headers = {'X-Delete-After': str(timeout)} + + swift_api = swift.SwiftAPI() + + swift_api.create_object(container, iso_object_name, + boot_iso_tmp_file, + object_headers=object_headers) + + boot_iso_url = swift_api.get_temp_url( + container, iso_object_name, timeout) + + LOG.debug("Created ISO %(name)s in Swift for node %(node)s, exposed " + "as temporary URL %(url)s", {'node': task.node.uuid, + 'name': iso_object_name, + 'url': boot_iso_url}) + + return boot_iso_url + + @classmethod + def _prepare_deploy_iso(cls, task, params, mode): + """Prepare deploy or rescue ISO image + + Build bootable ISO out of + `[driver_info]/deploy_kernel`/`[driver_info]/deploy_ramdisk` or + `[driver_info]/rescue_kernel`/`[driver_info]/rescue_ramdisk` + and `[driver_info]/bootloader`, then push built image up to Glance + and return temporary Swift URL to the image. + + :param task: a TaskManager instance containing the node to act on. + :param params: a dictionary containing 'parameter name'->'value' + mapping to be passed to kernel command line. + :param mode: either 'deploy' or 'rescue'. + :returns: bootable ISO HTTP URL. + :raises: MissingParameterValue, if any of the required parameters are + missing. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + :raises: ImageCreationFailed, if creating ISO image failed. + """ + node = task.node + + d_info = cls._parse_driver_info(node) + + kernel_href = d_info.get('%s_kernel' % mode) + ramdisk_href = d_info.get('%s_ramdisk' % mode) + bootloader_href = d_info.get('bootloader') + + return cls._prepare_iso_image( + task, kernel_href, ramdisk_href, bootloader_href, params=params) + + @classmethod + def _prepare_boot_iso(cls, task, root_uuid=None): + """Prepare boot ISO image + + Build bootable ISO out of `[instance_info]/kernel`, + `[instance_info]/ramdisk` and `[driver_info]/bootloader` if present. + Otherwise, read `kernel_id` and `ramdisk_id` from + `[instance_info]/image_source` Glance image metadata. + + Push produced ISO image up to Glance and return temporary Swift + URL to the image. + + :param task: a TaskManager instance containing the node to act on. + :returns: bootable ISO HTTP URL. + :raises: MissingParameterValue, if any of the required parameters are + missing. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + :raises: ImageCreationFailed, if creating ISO image failed. + """ + node = task.node + + d_info = cls._parse_deploy_info(node) + + kernel_href = node.instance_info.get('kernel') + ramdisk_href = node.instance_info.get('ramdisk') + + if not kernel_href or not ramdisk_href: + + image_href = d_info['image_source'] + + image_properties = ( + images.get_image_properties( + task.context, image_href, ['kernel_id', 'ramdisk_id'])) + + if not kernel_href: + kernel_href = image_properties.get('kernel_id') + + if not ramdisk_href: + ramdisk_href = image_properties.get('ramdisk_id') + + if not kernel_href or not ramdisk_href: + raise exception.InvalidParameterValue(_( + "Unable to find kernel or ramdisk for " + "to generate boot ISO for %(node)s") % + {'node': task.node.uuid}) + + bootloader_href = d_info.get('bootloader') + + return cls._prepare_iso_image( + task, kernel_href, ramdisk_href, bootloader_href, + root_uuid=root_uuid) + + def get_properties(self): + """Return the properties of the interface. + + :returns: dictionary of : entries. + """ + return REQUIRED_PROPERTIES + + @classmethod + def _validate_driver_info(cls, task): + """Validate the prerequisites for virtual media 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 + + cls._parse_driver_info(node) + + @classmethod + def _validate_instance_info(cls, 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 + + d_info = cls._parse_deploy_info(node) + + if node.driver_internal_info.get('is_whole_disk_image'): + props = [] + + elif service_utils.is_glance_image(d_info['image_source']): + props = ['kernel_id', 'ramdisk_id'] + + else: + props = ['kernel', 'ramdisk'] + + deploy_utils.validate_image_properties(task.context, d_info, props) + + 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_driver_info(task) + + if task.driver.storage.should_write_image(task): + self._validate_instance_info(task) + + def prepare_ramdisk(self, task, ramdisk_params): + """Prepares the boot of deploy or rescue ramdisk over virtual media. + + 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 + # NOTE(TheJulia): If this method is being called by something + # aside from deployment, clean and rescue, such as conductor takeover, + # we should treat this as a no-op and move on otherwise we would + # modify the state of the node due to virtual media operations. + if node.provision_state not in (states.DEPLOYING, + states.CLEANING, + states.RESCUING): + return + + manager_utils.node_power_action(task, states.POWER_OFF) + + d_info = self._parse_driver_info(node) + + config_via_floppy = d_info.get('config_via_floppy') + + deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task) + ramdisk_params['BOOTIF'] = deploy_nic_mac + + if config_via_floppy: + + if self._has_vmedia_device(task, sushy.VIRTUAL_MEDIA_FLOPPY): + # NOTE (etingof): IPA will read the diskette only if + # we tell it to + ramdisk_params['boot_method'] = 'vmedia' + + floppy_ref = self._prepare_floppy_image( + task, params=ramdisk_params) + + self._eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY) + self._insert_vmedia( + task, floppy_ref, sushy.VIRTUAL_MEDIA_FLOPPY) + + LOG.debug('Inserted virtual floppy with configuration for ' + 'node %(node)s', {'node': task.node.uuid}) + + else: + LOG.warning('Config via floppy is requested, but ' + 'Floppy drive is not available on node ' + '%(node)s', {'node': task.node.uuid}) + + mode = deploy_utils.rescue_or_deploy_mode(node) + + iso_ref = self._prepare_deploy_iso(task, ramdisk_params, mode) + + self._eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD) + self._insert_vmedia(task, iso_ref, sushy.VIRTUAL_MEDIA_CD) + + boot_mode_utils.sync_boot_mode(task) + + manager_utils.node_set_boot_device(task, boot_devices.CDROM) + + LOG.debug("Node %(node)s is set to one time boot from " + "%(device)s", {'node': task.node.uuid, + 'device': boot_devices.CDROM}) + + 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 + """ + node = task.node + + d_info = self._parse_driver_info(node) + + config_via_floppy = d_info.get('config_via_floppy') + + LOG.debug("Cleaning up deploy boot for " + "%(node)s", {'node': task.node.uuid}) + + self._eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD) + self._cleanup_iso_image(task) + + if (config_via_floppy and + self._has_vmedia_device(task, sushy.VIRTUAL_MEDIA_FLOPPY)): + self._eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY) + self._cleanup_floppy_image(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: + + - If `boot_option` requested for this deploy is 'local', then set the + node to boot from disk. + - Unless `boot_option` requested for this deploy is 'ramdisk', pass + root disk/partition ID to virtual media boot image + - Otherwise build boot image, insert it into virtual media device + and set node to boot from CD. + + :param task: a task from TaskManager. + :returns: None + :raises: InstanceDeployFailure, if its try to boot iSCSI volume in + 'BIOS' boot mode. + """ + node = task.node + + boot_option = deploy_utils.get_boot_option(node) + + self.clean_up_instance(task) + iwdi = node.driver_internal_info.get('is_whole_disk_image') + if boot_option == "local" or iwdi: + manager_utils.node_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) + + manager_utils.node_set_boot_device( + task, boot_devices.DISK, persistent=True) + + return + + params.update(root_uuid=root_uuid) + + iso_ref = self._prepare_boot_iso(task, **params) + + self._eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD) + self._insert_vmedia(task, iso_ref, sushy.VIRTUAL_MEDIA_CD) + + boot_mode_utils.sync_boot_mode(task) + + manager_utils.node_set_boot_device( + task, boot_devices.CDROM, persistent=True) + + LOG.debug("Node %(node)s is set to permanently boot from " + "%(device)s", {'node': task.node.uuid, + 'device': boot_devices.CDROM}) + + 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._eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD) + d_info = task.node.driver_info + config_via_floppy = d_info.get('config_via_floppy') + if config_via_floppy: + self._eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY) + + self._cleanup_iso_image(task) + + @staticmethod + def _insert_vmedia(task, boot_url, boot_device): + """Insert bootable ISO image into virtual CD or DVD + + :param task: A task from TaskManager. + :param boot_url: URL to a bootable ISO image + :param boot_device: sushy boot device e.g. `VIRTUAL_MEDIA_CD`, + `VIRTUAL_MEDIA_DVD` or `VIRTUAL_MEDIA_FLOPPY` + :raises: InvalidParameterValue, if no suitable virtual CD or DVD is + found on the node. + """ + system = redfish_utils.get_system(task.node) + + for manager in system.managers: + for v_media in manager.virtual_media.get_members(): + if boot_device not in v_media.media_types: + continue + + if v_media.inserted: + if v_media.image == boot_url: + LOG.debug("Boot media %(boot_url)s is already " + "inserted into %(boot_device)s for node " + "%(node)s", {'node': task.node.uuid, + 'boot_url': boot_url, + 'boot_device': boot_device}) + return + + continue + + v_media.insert_media(boot_url, inserted=True, + write_protected=True) + + LOG.info("Inserted boot media %(boot_url)s into " + "%(boot_device)s for node " + "%(node)s", {'node': task.node.uuid, + 'boot_url': boot_url, + 'boot_device': boot_device}) + return + + raise exception.InvalidParameterValue( + _('No suitable virtual media device found')) + + @staticmethod + def _eject_vmedia(task, boot_device=None): + """Eject virtual CDs and DVDs + + :param task: A task from TaskManager. + :param boot_device: sushy boot device e.g. `VIRTUAL_MEDIA_CD`, + `VIRTUAL_MEDIA_DVD` or `VIRTUAL_MEDIA_FLOPPY` or `None` to + eject everything (default). + :raises: InvalidParameterValue, if no suitable virtual CD or DVD is + found on the node. + """ + system = redfish_utils.get_system(task.node) + + for manager in system.managers: + for v_media in manager.virtual_media.get_members(): + if boot_device and boot_device not in v_media.media_types: + continue + + inserted = v_media.inserted + + if inserted: + v_media.eject_media() + + LOG.info("Boot media is%(already)s ejected from " + "%(boot_device)s for node %(node)s" + "", {'node': task.node.uuid, + 'already': '' if inserted else ' already', + 'boot_device': v_media.name}) + + @staticmethod + def _has_vmedia_device(task, boot_device): + """Indicate if device exists at any of the managers + + :param task: A task from TaskManager. + :param boot_device: sushy boot device e.g. `VIRTUAL_MEDIA_CD`, + `VIRTUAL_MEDIA_DVD` or `VIRTUAL_MEDIA_FLOPPY`. + """ + system = redfish_utils.get_system(task.node) + + for manager in system.managers: + for v_media in manager.virtual_media.get_members(): + if boot_device in v_media.media_types: + return True diff --git a/ironic/drivers/redfish.py b/ironic/drivers/redfish.py index 1610a2613e..fe082cfe4a 100644 --- a/ironic/drivers/redfish.py +++ b/ironic/drivers/redfish.py @@ -15,8 +15,11 @@ from ironic.drivers import generic from ironic.drivers.modules import inspector +from ironic.drivers.modules import ipxe from ironic.drivers.modules import noop +from ironic.drivers.modules import pxe from ironic.drivers.modules.redfish import bios as redfish_bios +from ironic.drivers.modules.redfish import boot as redfish_boot from ironic.drivers.modules.redfish import inspect as redfish_inspect from ironic.drivers.modules.redfish import management as redfish_mgmt from ironic.drivers.modules.redfish import power as redfish_power @@ -45,3 +48,9 @@ class RedfishHardware(generic.GenericHardware): """List of supported power interfaces.""" return [redfish_inspect.RedfishInspect, inspector.Inspector, noop.NoInspect] + + @property + def supported_boot_interfaces(self): + """List of supported boot interfaces.""" + return [redfish_boot.RedfishVirtualMediaBoot, + ipxe.iPXEBoot, pxe.PXEBoot] diff --git a/ironic/tests/unit/drivers/modules/redfish/test_bios.py b/ironic/tests/unit/drivers/modules/redfish/test_bios.py index 228d148667..98c2aa149d 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_bios.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_bios.py @@ -20,8 +20,8 @@ from ironic.common import states from ironic.conductor import task_manager from ironic.conductor import utils as manager_utils from ironic.drivers.modules import deploy_utils -from ironic.drivers.modules import pxe as pxe_boot from ironic.drivers.modules.redfish import bios as redfish_bios +from ironic.drivers.modules.redfish import boot as redfish_boot from ironic.drivers.modules.redfish import utils as redfish_utils from ironic import objects from ironic.tests.unit.db import base as db_base @@ -50,6 +50,7 @@ class RedfishBiosTestCase(db_base.DbTestCase): self.config(enabled_bios_interfaces=['redfish'], enabled_hardware_types=['redfish'], enabled_power_interfaces=['redfish'], + enabled_boot_interfaces=['redfish-virtual-media'], enabled_management_interfaces=['redfish']) self.node = obj_utils.create_test_node( self.context, driver='redfish', driver_info=INFO_DICT) @@ -160,7 +161,7 @@ class RedfishBiosTestCase(db_base.DbTestCase): mock_setting_list.delete.assert_called_once_with( task.context, task.node.id, delete_names) - @mock.patch.object(pxe_boot.PXEBoot, 'prepare_ramdisk', + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'prepare_ramdisk', spec_set=True, autospec=True) @mock.patch.object(deploy_utils, 'build_agent_options', autospec=True) @mock.patch.object(redfish_utils, 'get_system', autospec=True) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_boot.py b/ironic/tests/unit/drivers/modules/redfish/test_boot.py new file mode 100644 index 0000000000..7e0d24766b --- /dev/null +++ b/ironic/tests/unit/drivers/modules/redfish/test_boot.py @@ -0,0 +1,867 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from oslo_utils import importutils + +from ironic.common import boot_devices +from ironic.common import exception +from ironic.common import images +from ironic.common import states +from ironic.conductor import task_manager +from ironic.drivers.modules import boot_mode_utils +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules.redfish import boot as redfish_boot +from ironic.drivers.modules.redfish import utils as redfish_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.objects import utils as obj_utils + +sushy = importutils.try_import('sushy') + +INFO_DICT = db_utils.get_test_redfish_info() + + +@mock.patch('eventlet.greenthread.sleep', lambda _t: None) +class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): + + def setUp(self): + super(RedfishVirtualMediaBootTestCase, self).setUp() + self.config(enabled_hardware_types=['redfish'], + enabled_power_interfaces=['redfish'], + enabled_boot_interfaces=['redfish-virtual-media'], + 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, 'sushy', None) + def test_loading_error(self): + self.assertRaisesRegex( + exception.DriverLoadError, + 'Unable to import the sushy library', + redfish_boot.RedfishVirtualMediaBoot) + + 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 = task.driver.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']) + + 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 = task.driver.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, + task.driver.boot._parse_driver_info, + task.node) + + def _test_parse_driver_info_from_conf(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 + + expected = { + '%s_ramdisk' % mode: 'glance://%s_ramdisk_uuid' % mode, + '%s_kernel' % mode: 'glance://%s_kernel_uuid' % mode + } + + self.config(group='conductor', **expected) + + image_info = task.driver.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_mixed_source(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 + + 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, + task.driver.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_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 = task.driver.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, + task.driver.boot._parse_deploy_info, + task.node) + + @mock.patch.object(redfish_boot, 'swift', autospec=True) + def test__cleanup_floppy_image(self, mock_swift): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.boot._cleanup_floppy_image(task) + + mock_swift.SwiftAPI.assert_called_once_with() + mock_swift_api = mock_swift.SwiftAPI.return_value + + mock_swift_api.delete_object.assert_called_once_with( + 'ironic_redfish_container', 'image-%s' % task.node.uuid + ) + + @mock.patch.object(redfish_boot, 'swift', autospec=True) + @mock.patch.object(images, 'create_vfat_image', autospec=True) + def test__prepare_floppy_image(self, mock_create_vfat_image, mock_swift): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.boot._prepare_floppy_image(task) + + mock_create_vfat_image.assert_called_once_with( + mock.ANY, parameters=mock.ANY) + + mock_swift.SwiftAPI.assert_called_once_with() + mock_swift_api = mock_swift.SwiftAPI.return_value + + mock_swift_api.create_object.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, mock.ANY) + + mock_swift_api.get_temp_url.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY) + + @mock.patch.object(redfish_boot, 'swift', autospec=True) + def test__cleanup_iso_image(self, mock_swift): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.boot._cleanup_iso_image(task) + + mock_swift.SwiftAPI.assert_called_once_with() + mock_swift_api = mock_swift.SwiftAPI.return_value + + mock_swift_api.delete_object.assert_called_once_with( + 'ironic_redfish_container', 'boot-%s' % task.node.uuid + ) + + @mock.patch.object(redfish_boot, 'swift', autospec=True) + @mock.patch.object(images, 'create_boot_iso', autospec=True) + def test__prepare_iso_image_uefi(self, mock_create_boot_iso, mock_swift): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.instance_info.update(deploy_boot_mode='uefi') + + mock_swift_api = mock_swift.SwiftAPI.return_value + mock_swift_api.get_temp_url.return_value = 'https://a.b/c.f?e=f' + + url = task.driver.boot._prepare_iso_image( + task, 'http://kernel/img', 'http://ramdisk/img', + 'http://bootloader/img', root_uuid=task.node.uuid) + + self.assertTrue(url) + + mock_create_boot_iso.assert_called_once_with( + mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img', + boot_mode='uefi', esp_image_href='http://bootloader/img', + kernel_params='nofb nomodeset vga=normal', + root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123') + + mock_swift.SwiftAPI.assert_called_once_with() + + mock_swift_api.create_object.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, mock.ANY) + + mock_swift_api.get_temp_url.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY) + + @mock.patch.object(redfish_boot, 'swift', autospec=True) + @mock.patch.object(images, 'create_boot_iso', autospec=True) + def test__prepare_iso_image_bios(self, mock_create_boot_iso, mock_swift): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + + mock_swift_api = mock_swift.SwiftAPI.return_value + mock_swift_api.get_temp_url.return_value = 'https://a.b/c.f?e=f' + + url = task.driver.boot._prepare_iso_image( + task, 'http://kernel/img', 'http://ramdisk/img', + bootloader_href=None, root_uuid=task.node.uuid) + + self.assertTrue(url) + + mock_create_boot_iso.assert_called_once_with( + mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img', + boot_mode=None, esp_image_href=None, + kernel_params='nofb nomodeset vga=normal', + root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123') + + mock_swift.SwiftAPI.assert_called_once_with() + + mock_swift_api.create_object.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, mock.ANY) + + mock_swift_api.get_temp_url.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY) + + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_prepare_iso_image', autospec=True) + def test__prepare_deploy_iso(self, mock__prepare_iso_image): + 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(deploy_boot_mode='uefi') + + task.driver.boot._prepare_deploy_iso(task, {}, 'deploy') + + mock__prepare_iso_image.assert_called_once_with( + mock.ANY, 'kernel', 'ramdisk', 'bootloader', params={}) + + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_prepare_iso_image', autospec=True) + @mock.patch.object(images, 'create_boot_iso', autospec=True) + def test__prepare_boot_iso(self, mock_create_boot_iso, + mock__prepare_iso_image): + 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'}) + + task.driver.boot._prepare_boot_iso( + task, root_uuid=task.node.uuid) + + mock__prepare_iso_image.assert_called_once_with( + mock.ANY, 'http://kernel/img', 'http://ramdisk/img', + 'bootloader', root_uuid=task.node.uuid) + + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + autospec=True) + @mock.patch.object(boot_mode_utils, 'get_boot_mode_for_deploy', + autospec=True) + def test_validate_uefi_boot(self, mock_get_boot_mode, + mock_validate_image_properties, + mock_parse_driver_info): + 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'} + ) + + mock_get_boot_mode.return_value = 'uefi' + + task.driver.boot.validate(task) + + mock_validate_image_properties.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY) + + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + autospec=True) + @mock.patch.object(boot_mode_utils, 'get_boot_mode_for_deploy', + autospec=True) + def test_validate_bios_boot(self, mock_get_boot_mode, + mock_validate_image_properties, + mock_parse_driver_info): + 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'} + ) + + mock_get_boot_mode.return_value = 'bios' + + task.driver.boot.validate(task) + + mock_validate_image_properties.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY) + + @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): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.MissingParameterValue, + task.driver.boot.validate, task) + + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_prepare_deploy_iso', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_eject_vmedia', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_insert_vmedia', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_parse_driver_info', autospec=True) + @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + def test_prepare_ramdisk_with_params( + self, mock_boot_mode_utils, mock_manager_utils, + mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia, + mock__prepare_deploy_iso): + + 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_manager_utils.node_power_action.assert_called_once_with( + task, states.POWER_OFF) + + mock__eject_vmedia.assert_called_once_with( + task, sushy.VIRTUAL_MEDIA_CD) + + mock__insert_vmedia.assert_called_once_with( + task, 'image-url', sushy.VIRTUAL_MEDIA_CD) + + expected_params = { + 'BOOTIF': None, + } + + mock__prepare_deploy_iso.assert_called_once_with( + task, expected_params, 'deploy') + + mock_manager_utils.node_set_boot_device.assert_called_once_with( + task, boot_devices.CDROM) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_prepare_floppy_image', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_prepare_deploy_iso', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_has_vmedia_device', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_eject_vmedia', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_insert_vmedia', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_parse_driver_info', autospec=True) + @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + def test_prepare_ramdisk_with_floppy( + self, mock_boot_mode_utils, mock_manager_utils, + mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia, + mock__has_vmedia_device, mock__prepare_deploy_iso, + mock__prepare_floppy_image): + + 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 = { + 'config_via_floppy': True + } + + mock__has_vmedia_device.return_value = True + mock__prepare_floppy_image.return_value = 'floppy-image-url' + mock__prepare_deploy_iso.return_value = 'cd-image-url' + + task.driver.boot.prepare_ramdisk(task, {}) + + mock_manager_utils.node_power_action.assert_called_once_with( + task, states.POWER_OFF) + + mock__has_vmedia_device.assert_called_once_with( + task, sushy.VIRTUAL_MEDIA_FLOPPY) + + eject_calls = [ + mock.call(task, sushy.VIRTUAL_MEDIA_FLOPPY), + mock.call(task, sushy.VIRTUAL_MEDIA_CD) + ] + + mock__eject_vmedia.assert_has_calls(eject_calls) + + insert_calls = [ + mock.call(task, 'floppy-image-url', + sushy.VIRTUAL_MEDIA_FLOPPY), + mock.call(task, 'cd-image-url', + sushy.VIRTUAL_MEDIA_CD), + ] + + mock__insert_vmedia.assert_has_calls(insert_calls) + + expected_params = { + 'BOOTIF': None, + 'boot_method': 'vmedia', + } + + mock__prepare_deploy_iso.assert_called_once_with( + task, expected_params, 'deploy') + + mock_manager_utils.node_set_boot_device.assert_called_once_with( + task, boot_devices.CDROM) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_has_vmedia_device', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_eject_vmedia', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_cleanup_iso_image', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_cleanup_floppy_image', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_parse_driver_info', autospec=True) + def test_clean_up_ramdisk( + self, mock__parse_driver_info, mock__cleanup_floppy_image, + mock__cleanup_iso_image, mock__eject_vmedia, + mock__has_vmedia_device): + + 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 = {'config_via_floppy': True} + mock__has_vmedia_device.return_value = True + + task.driver.boot.clean_up_ramdisk(task) + + mock__cleanup_iso_image.assert_called_once_with(task) + + mock__cleanup_floppy_image.assert_called_once_with(task) + + mock__has_vmedia_device.assert_called_once_with( + task, sushy.VIRTUAL_MEDIA_FLOPPY) + + eject_calls = [ + mock.call(task, sushy.VIRTUAL_MEDIA_CD), + mock.call(task, sushy.VIRTUAL_MEDIA_FLOPPY) + ] + + mock__eject_vmedia.assert_has_calls(eject_calls) + + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + 'clean_up_instance', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_prepare_boot_iso', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_eject_vmedia', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_insert_vmedia', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_parse_driver_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) + def test_prepare_instance_normal_boot( + self, mock_boot_mode_utils, mock_deploy_utils, mock_manager_utils, + mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia, + 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' + + mock__parse_driver_info.return_value = {} + 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, **expected_params) + + mock__eject_vmedia.assert_called_once_with( + task, sushy.VIRTUAL_MEDIA_CD) + + mock__insert_vmedia.assert_called_once_with( + task, 'image-url', sushy.VIRTUAL_MEDIA_CD) + + mock_manager_utils.node_set_boot_device.assert_called_once_with( + task, boot_devices.CDROM, persistent=True) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + 'clean_up_instance', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_prepare_boot_iso', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_eject_vmedia', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_insert_vmedia', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_parse_driver_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) + def test_prepare_instance_ramdisk_boot( + self, mock_boot_mode_utils, mock_deploy_utils, mock_manager_utils, + mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia, + 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 = 'ramdisk' + + mock__prepare_boot_iso.return_value = 'image-url' + + task.driver.boot.prepare_instance(task) + + mock__prepare_boot_iso.assert_called_once_with(task) + + mock__eject_vmedia.assert_called_once_with( + task, sushy.VIRTUAL_MEDIA_CD) + + mock__insert_vmedia.assert_called_once_with( + task, 'image-url', sushy.VIRTUAL_MEDIA_CD) + + mock_manager_utils.node_set_boot_device.assert_called_once_with( + task, boot_devices.CDROM, persistent=True) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_eject_vmedia', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_cleanup_iso_image', autospec=True) + @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) + def _test_prepare_instance_local_boot( + self, mock_manager_utils, + mock__cleanup_iso_image, mock__eject_vmedia): + + 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__eject_vmedia.assert_called_once_with( + task, sushy.VIRTUAL_MEDIA_CD) + + 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(redfish_boot.RedfishVirtualMediaBoot, + '_eject_vmedia', autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_cleanup_iso_image', autospec=True) + def _test_clean_up_instance(self, mock__cleanup_iso_image, + mock__eject_vmedia): + + 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) + eject_calls = [mock.call(task, sushy.VIRTUAL_MEDIA_CD)] + if task.node.driver_info.get('config_via_floppy'): + eject_calls.append(mock.call(task, sushy.VIRTUAL_MEDIA_FLOPPY)) + + mock__eject_vmedia.assert_has_calls(eject_calls) + + 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_floppy'] = True + self.node.driver_info = driver_info + self.node.save() + self._test_clean_up_instance() + + @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True) + def test__insert_vmedia_anew(self, mock_redfish_utils): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_vmedia_cd = mock.MagicMock( + inserted=False, + media_types=[sushy.VIRTUAL_MEDIA_CD]) + mock_vmedia_floppy = mock.MagicMock( + inserted=False, + media_types=[sushy.VIRTUAL_MEDIA_FLOPPY]) + + mock_manager = mock.MagicMock() + + mock_manager.virtual_media.get_members.return_value = [ + mock_vmedia_cd, mock_vmedia_floppy] + + mock_redfish_utils.get_system.return_value.managers = [ + mock_manager] + + task.driver.boot._insert_vmedia( + task, 'img-url', sushy.VIRTUAL_MEDIA_CD) + + mock_vmedia_cd.insert_media.assert_called_once_with( + 'img-url', inserted=True, write_protected=True) + + self.assertFalse(mock_vmedia_floppy.insert_media.call_count) + + @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True) + def test__insert_vmedia_already_inserted(self, mock_redfish_utils): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_vmedia_cd = mock.MagicMock( + inserted=True, + image='img-url', + media_types=[sushy.VIRTUAL_MEDIA_CD]) + mock_manager = mock.MagicMock() + + mock_manager.virtual_media.get_members.return_value = [ + mock_vmedia_cd] + + mock_redfish_utils.get_system.return_value.managers = [ + mock_manager] + + task.driver.boot._insert_vmedia( + task, 'img-url', sushy.VIRTUAL_MEDIA_CD) + + self.assertFalse(mock_vmedia_cd.insert_media.call_count) + + @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True) + def test__insert_vmedia_bad_device(self, mock_redfish_utils): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_vmedia_floppy = mock.MagicMock( + inserted=False, + media_types=[sushy.VIRTUAL_MEDIA_FLOPPY]) + mock_manager = mock.MagicMock() + + mock_manager.virtual_media.get_members.return_value = [ + mock_vmedia_floppy] + + mock_redfish_utils.get_system.return_value.managers = [ + mock_manager] + + self.assertRaises( + exception.InvalidParameterValue, + task.driver.boot._insert_vmedia, + task, 'img-url', sushy.VIRTUAL_MEDIA_CD) + + @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True) + def test__eject_vmedia_everything(self, mock_redfish_utils): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_vmedia_cd = mock.MagicMock( + inserted=True, + media_types=[sushy.VIRTUAL_MEDIA_CD]) + mock_vmedia_floppy = mock.MagicMock( + inserted=True, + media_types=[sushy.VIRTUAL_MEDIA_FLOPPY]) + + mock_manager = mock.MagicMock() + + mock_manager.virtual_media.get_members.return_value = [ + mock_vmedia_cd, mock_vmedia_floppy] + + mock_redfish_utils.get_system.return_value.managers = [ + mock_manager] + + task.driver.boot._eject_vmedia(task) + + mock_vmedia_cd.eject_media.assert_called_once_with() + mock_vmedia_floppy.eject_media.assert_called_once_with() + + @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True) + def test__eject_vmedia_specific(self, mock_redfish_utils): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_vmedia_cd = mock.MagicMock( + inserted=True, + media_types=[sushy.VIRTUAL_MEDIA_CD]) + mock_vmedia_floppy = mock.MagicMock( + inserted=True, + media_types=[sushy.VIRTUAL_MEDIA_FLOPPY]) + + mock_manager = mock.MagicMock() + + mock_manager.virtual_media.get_members.return_value = [ + mock_vmedia_cd, mock_vmedia_floppy] + + mock_redfish_utils.get_system.return_value.managers = [ + mock_manager] + + task.driver.boot._eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD) + + mock_vmedia_cd.eject_media.assert_called_once_with() + self.assertFalse(mock_vmedia_floppy.eject_media.call_count) + + @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True) + def test__eject_vmedia_not_inserted(self, mock_redfish_utils): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_vmedia_cd = mock.MagicMock( + inserted=False, + media_types=[sushy.VIRTUAL_MEDIA_CD]) + mock_vmedia_floppy = mock.MagicMock( + inserted=False, + media_types=[sushy.VIRTUAL_MEDIA_FLOPPY]) + + mock_manager = mock.MagicMock() + + mock_manager.virtual_media.get_members.return_value = [ + mock_vmedia_cd, mock_vmedia_floppy] + + mock_redfish_utils.get_system.return_value.managers = [ + mock_manager] + + task.driver.boot._eject_vmedia(task) + + self.assertFalse(mock_vmedia_cd.eject_media.call_count) + self.assertFalse(mock_vmedia_floppy.eject_media.call_count) + + @mock.patch.object(redfish_boot, 'redfish_utils', autospec=True) + def test__eject_vmedia_unknown(self, mock_redfish_utils): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + mock_vmedia_cd = mock.MagicMock( + inserted=False, + media_types=[sushy.VIRTUAL_MEDIA_CD]) + + mock_manager = mock.MagicMock() + + mock_manager.virtual_media.get_members.return_value = [ + mock_vmedia_cd] + + mock_redfish_utils.get_system.return_value.managers = [ + mock_manager] + + task.driver.boot._eject_vmedia(task) + + self.assertFalse(mock_vmedia_cd.eject_media.call_count) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_inspect.py b/ironic/tests/unit/drivers/modules/redfish/test_inspect.py index c5aef3d672..776a68bae3 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_inspect.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_inspect.py @@ -40,6 +40,7 @@ class RedfishInspectTestCase(db_base.DbTestCase): super(RedfishInspectTestCase, self).setUp() self.config(enabled_hardware_types=['redfish'], enabled_power_interfaces=['redfish'], + enabled_boot_interfaces=['redfish-virtual-media'], enabled_management_interfaces=['redfish'], enabled_inspect_interfaces=['redfish']) self.node = obj_utils.create_test_node( diff --git a/ironic/tests/unit/drivers/modules/redfish/test_management.py b/ironic/tests/unit/drivers/modules/redfish/test_management.py index 1a057ec286..a4664f08f4 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_management.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_management.py @@ -37,6 +37,7 @@ class RedfishManagementTestCase(db_base.DbTestCase): super(RedfishManagementTestCase, self).setUp() self.config(enabled_hardware_types=['redfish'], enabled_power_interfaces=['redfish'], + enabled_boot_interfaces=['redfish-virtual-media'], enabled_management_interfaces=['redfish'], enabled_inspect_interfaces=['redfish'], enabled_bios_interfaces=['redfish']) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_power.py b/ironic/tests/unit/drivers/modules/redfish/test_power.py index 96903328e8..b84833acfd 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_power.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_power.py @@ -37,6 +37,7 @@ class RedfishPowerTestCase(db_base.DbTestCase): super(RedfishPowerTestCase, self).setUp() self.config(enabled_hardware_types=['redfish'], enabled_power_interfaces=['redfish'], + enabled_boot_interfaces=['redfish-virtual-media'], enabled_management_interfaces=['redfish'], enabled_inspect_interfaces=['redfish'], enabled_bios_interfaces=['redfish']) diff --git a/ironic/tests/unit/drivers/modules/redfish/test_utils.py b/ironic/tests/unit/drivers/modules/redfish/test_utils.py index 9614c17405..ef56d96c77 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_utils.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_utils.py @@ -40,6 +40,7 @@ class RedfishUtilsTestCase(db_base.DbTestCase): # Default configurations self.config(enabled_hardware_types=['redfish'], enabled_power_interfaces=['redfish'], + enabled_boot_interfaces=['redfish-virtual-media'], enabled_management_interfaces=['redfish']) # Redfish specific configurations self.config(connection_attempts=1, group='redfish') diff --git a/ironic/tests/unit/drivers/test_redfish.py b/ironic/tests/unit/drivers/test_redfish.py index 1b4e44585a..f2675b5c6d 100644 --- a/ironic/tests/unit/drivers/test_redfish.py +++ b/ironic/tests/unit/drivers/test_redfish.py @@ -16,7 +16,7 @@ from ironic.conductor import task_manager from ironic.drivers.modules import iscsi_deploy from ironic.drivers.modules import noop -from ironic.drivers.modules import pxe +from ironic.drivers.modules.redfish import boot as redfish_boot from ironic.drivers.modules.redfish import inspect as redfish_inspect from ironic.drivers.modules.redfish import management as redfish_mgmt from ironic.drivers.modules.redfish import power as redfish_power @@ -30,6 +30,7 @@ class RedfishHardwareTestCase(db_base.DbTestCase): super(RedfishHardwareTestCase, self).setUp() self.config(enabled_hardware_types=['redfish'], enabled_power_interfaces=['redfish'], + enabled_boot_interfaces=['redfish-virtual-media'], enabled_management_interfaces=['redfish'], enabled_inspect_interfaces=['redfish'], enabled_bios_interfaces=['redfish']) @@ -43,7 +44,8 @@ class RedfishHardwareTestCase(db_base.DbTestCase): redfish_mgmt.RedfishManagement) self.assertIsInstance(task.driver.power, redfish_power.RedfishPower) - self.assertIsInstance(task.driver.boot, pxe.PXEBoot) + self.assertIsInstance(task.driver.boot, + redfish_boot.RedfishVirtualMediaBoot) self.assertIsInstance(task.driver.deploy, iscsi_deploy.ISCSIDeploy) self.assertIsInstance(task.driver.console, noop.NoConsole) self.assertIsInstance(task.driver.raid, noop.NoRAID) diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py index db8adc5296..a895730944 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py @@ -136,6 +136,8 @@ SUSHY_SPEC = ( 'STATE_ENABLED', 'STATE_DISABLED', 'STATE_ABSENT', + 'VIRTUAL_MEDIA_CD', + 'VIRTUAL_MEDIA_FLOPPY', ) SUSHY_AUTH_SPEC = ( diff --git a/ironic/tests/unit/drivers/third_party_driver_mocks.py b/ironic/tests/unit/drivers/third_party_driver_mocks.py index 9c351b8a72..53a2b17ae4 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mocks.py +++ b/ironic/tests/unit/drivers/third_party_driver_mocks.py @@ -199,6 +199,8 @@ if not sushy: STATE_ENABLED='enabled', STATE_DISABLED='disabled', STATE_ABSENT='absent', + VIRTUAL_MEDIA_CD='cd', + VIRTUAL_MEDIA_FLOPPY='floppy', ) sys.modules['sushy'] = sushy diff --git a/releasenotes/notes/add-redfish-boot-interface-e7e05bdd2c894d80.yaml b/releasenotes/notes/add-redfish-boot-interface-e7e05bdd2c894d80.yaml new file mode 100644 index 0000000000..eb1f1388dd --- /dev/null +++ b/releasenotes/notes/add-redfish-boot-interface-e7e05bdd2c894d80.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Adds virtual media boot interface to ``redfish`` hardware type supporting + virtual media boot. The ``redfish-virtual-media`` boot interface operates + on the same kernel/ramdisk as, for example, PXE boot interface does, however + ``redfish-virtual-media`` boot interface can additionally require EFI + system partition image (ESP) when performing UEFI boot. New configuration + option ``bootloader`` or ``[driver_info]/bootloader`` property can be used + to convey ESP location to ironic. diff --git a/setup.cfg b/setup.cfg index dfae78963b..5ce50f7173 100644 --- a/setup.cfg +++ b/setup.cfg @@ -69,6 +69,7 @@ ironic.hardware.interfaces.boot = irmc-pxe = ironic.drivers.modules.irmc.boot:IRMCPXEBoot irmc-virtual-media = ironic.drivers.modules.irmc.boot:IRMCVirtualMediaBoot pxe = ironic.drivers.modules.pxe:PXEBoot + redfish-virtual-media = ironic.drivers.modules.redfish.boot:RedfishVirtualMediaBoot ironic.hardware.interfaces.console = fake = ironic.drivers.modules.fake:FakeConsole