Merge "Add Redfish Virtual Media Boot support"
This commit is contained in:
commit
4564aa9f19
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.')),
|
||||
]
|
||||
|
||||
|
||||
|
@ -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.')),
|
||||
]
|
||||
|
||||
|
||||
|
818
ironic/drivers/modules/redfish/boot.py
Normal file
818
ironic/drivers/modules/redfish/boot.py
Normal file
@ -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 <property name>:<property description> 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
|
@ -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]
|
||||
|
@ -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)
|
||||
|
867
ironic/tests/unit/drivers/modules/redfish/test_boot.py
Normal file
867
ironic/tests/unit/drivers/modules/redfish/test_boot.py
Normal file
@ -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)
|
@ -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(
|
||||
|
@ -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'])
|
||||
|
@ -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'])
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -136,6 +136,8 @@ SUSHY_SPEC = (
|
||||
'STATE_ENABLED',
|
||||
'STATE_DISABLED',
|
||||
'STATE_ABSENT',
|
||||
'VIRTUAL_MEDIA_CD',
|
||||
'VIRTUAL_MEDIA_FLOPPY',
|
||||
)
|
||||
|
||||
SUSHY_AUTH_SPEC = (
|
||||
|
@ -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
|
||||
|
@ -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.
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user