From c165f71a562a31f0541118b940fb0fb935b73fc6 Mon Sep 17 00:00:00 2001 From: vmud213 Date: Fri, 3 Jul 2020 06:15:25 +0000 Subject: [PATCH] Decouple the ISO creation logic from redfish Currently the functionality of creating the ISO and floppy images is tightly coupled with the redfish boot interface implementation. Move this to a common place so that this can be levereged when needed. Change-Id: Iea1991690e28d31a54afeaf5231ddc5798c8213c Story: #2007940 Task: #40405 --- ironic/drivers/modules/image_utils.py | 501 ++++++++++++++++++ ironic/drivers/modules/redfish/boot.py | 465 +--------------- .../unit/drivers/modules/redfish/test_boot.py | 494 +++-------------- .../unit/drivers/modules/test_image_utils.py | 413 +++++++++++++++ 4 files changed, 1007 insertions(+), 866 deletions(-) create mode 100644 ironic/drivers/modules/image_utils.py create mode 100644 ironic/tests/unit/drivers/modules/test_image_utils.py diff --git a/ironic/drivers/modules/image_utils.py b/ironic/drivers/modules/image_utils.py new file mode 100644 index 0000000000..862a6c689a --- /dev/null +++ b/ironic/drivers/modules/image_utils.py @@ -0,0 +1,501 @@ +# 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 functools +import json +import os +import shutil +import tempfile +from urllib import parse as urlparse + +from ironic_lib import utils as ironic_utils +from oslo_log import log +from oslo_serialization import base64 + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.common import images +from ironic.common import swift +from ironic.conf import CONF +from ironic.drivers.modules import boot_mode_utils +from ironic.drivers.modules import deploy_utils + +LOG = log.getLogger(__name__) + + +class ImageHandler(object): + + _SWIFT_MAP = { + "redfish": { + "swift_enabled": CONF.redfish.use_swift, + "container": CONF.redfish.swift_container, + "timeout": CONF.redfish.swift_object_expiry_timeout, + "image_subdir": "redfish", + "file_permission": CONF.redfish.file_permission + } + } + + def __init__(self, driver): + self._driver = driver + self._container = self._SWIFT_MAP[driver].get("container") + self._timeout = self._SWIFT_MAP[driver].get("timeout") + self._image_subdir = self._SWIFT_MAP[driver].get("image_subdir") + self._file_permission = self._SWIFT_MAP[driver].get("file_permission") + + def _is_swift_enabled(self): + try: + return self._SWIFT_MAP[self._driver].get("swift_enabled") + except KeyError: + return False + + def unpublish_image(self, object_name): + """Withdraw the image previously made downloadable. + + Depending on ironic settings, removes previously published file + from where it has been published - Swift or local HTTP server's + document root. + + :param object_name: name of the published file (optional) + """ + if self._is_swift_enabled(): + container = self._container + + swift_api = swift.SwiftAPI() + + LOG.debug("Cleaning up image %(name)s from Swift container " + "%(container)s", {'name': object_name, + 'container': container}) + + try: + swift_api.delete_object(container, object_name) + + except exception.SwiftOperationError as exc: + LOG.warning("Failed to clean up image %(image)s. Error: " + "%(error)s.", {'image': object_name, + 'error': exc}) + + else: + published_file = os.path.join( + CONF.deploy.http_root, self._image_subdir, object_name) + + ironic_utils.unlink_without_raise(published_file) + + def _append_filename_param(self, url, filename): + """Append 'filename=' parameter to given URL. + + Some BMCs seem to validate boot image URL requiring the URL to end + with something resembling ISO image file name. + + This function tries to add, hopefully, meaningless 'filename' + parameter to URL's query string in hope to make the entire boot image + URL looking more convincing to the BMC. + + However, `url` with fragments might not get cured by this hack. + + :param url: a URL to work on + :param filename: name of the file to append to the URL + :returns: original URL with 'filename' parameter appended + """ + parsed_url = urlparse.urlparse(url) + parsed_qs = urlparse.parse_qsl(parsed_url.query) + + has_filename = [x for x in parsed_qs if x[0].lower() == 'filename'] + if has_filename: + return url + + parsed_qs.append(('filename', filename)) + parsed_url = list(parsed_url) + parsed_url[4] = urlparse.urlencode(parsed_qs) + + return urlparse.urlunparse(parsed_url) + + def publish_image(self, image_file, object_name): + """Make image file downloadable. + + Depending on ironic settings, pushes given file into Swift or copies + it over to local HTTP server's document root and returns publicly + accessible URL leading to the given file. + + :param image_file: path to file to publish + :param object_name: name of the published file + :return: a URL to download published file + """ + + if self._is_swift_enabled(): + container = self._container + timeout = self._timeout + + object_headers = {'X-Delete-After': str(timeout)} + + swift_api = swift.SwiftAPI() + + swift_api.create_object(container, object_name, image_file, + object_headers=object_headers) + + image_url = swift_api.get_temp_url(container, object_name, timeout) + + else: + public_dir = os.path.join(CONF.deploy.http_root, + self._image_subdir) + + if not os.path.exists(public_dir): + os.mkdir(public_dir, 0o755) + + published_file = os.path.join(public_dir, object_name) + + try: + os.link(image_file, published_file) + os.chmod(image_file, self._file_permission) + + except OSError as exc: + LOG.debug( + "Could not hardlink image file %(image)s to public " + "location %(public)s (will copy it over): " + "%(error)s", {'image': image_file, + 'public': published_file, + 'error': exc}) + + shutil.copyfile(image_file, published_file) + os.chmod(published_file, self._file_permission) + + image_url = os.path.join( + CONF.deploy.http_url, self._image_subdir, object_name) + + image_url = self._append_filename_param( + image_url, os.path.basename(image_file)) + + return image_url + + +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 + + +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 + + +def cleanup_iso_image(task): + """Deletes the ISO if it was created for the instance. + + :param task: A task from TaskManager. + """ + iso_object_name = _get_iso_image_name(task.node) + img_handler = ImageHandler(task.node.driver) + + img_handler.unpublish_image(iso_object_name) + + +def prepare_floppy_image(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 = _get_floppy_image_name(task.node) + + 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) + + img_handler = ImageHandler(task.node.driver) + + image_url = img_handler.publish_image(vfat_image_tmpfile, object_name) + + 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 + + +def cleanup_floppy_image(task): + """Deletes the floppy image if it was created for the node. + + :param task: an ironic node object. + """ + floppy_object_name = _get_floppy_image_name(task.node) + + img_handler = ImageHandler(task.node.driver) + img_handler.unpublish_image(floppy_object_name) + + +def _prepare_iso_image(task, kernel_href, ramdisk_href, + bootloader_href=None, configdrive=None, + root_uuid=None, params=None, base_iso=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. + + If `configdrive` is specified it will be eventually written onto + the boot ISO image. + + :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 configdrive: URL to or a compressed blob of a ISO9660 or + FAT-formatted OpenStack config drive image. This image will be + written onto the built ISO image. Optional. + :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) and not base_iso: + raise exception.InvalidParameterValue(_( + "Unable to find kernel, ramdisk for " + "building ISO, or explicit ISO for %(node)s") % + {'node': task.node.uuid}) + + i_info = task.node.instance_info + + # NOTE(TheJulia): Until we support modifying a base iso, most of + # this logic actually does nothing in the end. But it should! + if deploy_utils.get_boot_option(task.node) == "ramdisk": + if not base_iso: + kernel_params = "root=/dev/ram0 text " + kernel_params += i_info.get("ramdisk_kernel_arguments", "") + else: + kernel_params = None + + else: + kernel_params = i_info.get( + 'kernel_append_params', CONF.redfish.kernel_append_params) + + if params and not base_iso: + 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 boot_fileobj: + + with tempfile.NamedTemporaryFile( + dir=CONF.tempdir, suffix='.img') as cfgdrv_fileobj: + + configdrive_href = configdrive + + # FIXME(TheJulia): This is treated as conditional with + # a base_iso as the intent, eventually, is to support + # injection into the supplied image. + + if configdrive and not base_iso: + parsed_url = urlparse.urlparse(configdrive) + if not parsed_url.scheme: + cfgdrv_blob = base64.decode_as_bytes(configdrive) + + with open(cfgdrv_fileobj.name, 'wb') as f: + f.write(cfgdrv_blob) + + configdrive_href = urlparse.urlunparse( + ('file', '', cfgdrv_fileobj.name, '', '', '')) + + LOG.debug("Built configdrive out of configdrive blob " + "for node %(node)s", {'node': task.node.uuid}) + + boot_iso_tmp_file = boot_fileobj.name + images.create_boot_iso( + task.context, boot_iso_tmp_file, + kernel_href, ramdisk_href, + esp_image_href=bootloader_href, + configdrive_href=configdrive_href, + root_uuid=root_uuid, + kernel_params=kernel_params, + boot_mode=boot_mode, + base_iso=base_iso) + + iso_object_name = _get_iso_image_name(task.node) + + img_handler = ImageHandler(task.node.driver) + image_url = img_handler.publish_image( + boot_iso_tmp_file, iso_object_name) + + LOG.debug("Created ISO %(name)s in object store for node %(node)s, " + "exposed as temporary URL " + "%(url)s", {'node': task.node.uuid, + 'name': iso_object_name, + 'url': image_url}) + + return image_url + + +def prepare_deploy_iso(task, params, mode, d_info): + """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. + + If network interface supplies network configuration (`network_data`), + a new `configdrive` will be created with `network_data.json` inside, + and eventually written down onto the boot ISO. + + :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'. + :param d_info: Deployment information of the node + :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. + """ + + kernel_href = d_info.get('%s_kernel' % mode) + ramdisk_href = d_info.get('%s_ramdisk' % mode) + bootloader_href = d_info.get('bootloader') + + # TODO(TheJulia): At some point we should support something like + # boot_iso for the deploy interface, perhaps when we support config + # injection. + prepare_iso_image = functools.partial( + _prepare_iso_image, task, kernel_href, ramdisk_href, + bootloader_href=bootloader_href, params=params) + + network_data = task.driver.network.get_node_network_data(task) + if network_data: + with tempfile.NamedTemporaryFile(dir=CONF.tempdir, + suffix='.iso') as metadata_fileobj: + + with open(metadata_fileobj.name, 'w') as f: + json.dump(network_data, f, indent=2) + + files_info = { + metadata_fileobj.name: 'openstack/latest/meta' + 'data/network_data.json' + } + + with tempfile.NamedTemporaryFile( + dir=CONF.tempdir, suffix='.img') as cfgdrv_fileobj: + + images.create_vfat_image(cfgdrv_fileobj.name, files_info) + + configdrive_href = urlparse.urlunparse( + ('file', '', cfgdrv_fileobj.name, '', '', '')) + + LOG.debug("Built configdrive %(name)s out of network data " + "for node %(node)s", {'name': configdrive_href, + 'node': task.node.uuid}) + + return prepare_iso_image(configdrive=configdrive_href) + + return prepare_iso_image() + + +def prepare_boot_iso(task, d_info, 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. + :param d_info: Deployment information of the node + :param root_uuid: Root UUID + :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 + + kernel_href = node.instance_info.get('kernel') + ramdisk_href = node.instance_info.get('ramdisk') + base_iso = node.instance_info.get('boot_iso') + + if (not kernel_href or not ramdisk_href) and not base_iso: + + 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 _prepare_iso_image( + task, kernel_href, ramdisk_href, bootloader_href, + root_uuid=root_uuid, base_iso=base_iso) diff --git a/ironic/drivers/modules/redfish/boot.py b/ironic/drivers/modules/redfish/boot.py index b7cb3170c9..576979cb35 100644 --- a/ironic/drivers/modules/redfish/boot.py +++ b/ironic/drivers/modules/redfish/boot.py @@ -13,30 +13,20 @@ # License for the specific language governing permissions and limitations # under the License. -import functools -import json -import os -import shutil -import tempfile -from urllib import parse as urlparse - -from ironic_lib import utils as ironic_utils from oslo_log import log -from oslo_serialization import base64 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 import image_utils from ironic.drivers.modules.redfish import utils as redfish_utils LOG = log.getLogger(__name__) @@ -151,52 +141,6 @@ def _parse_instance_info(node): return deploy_info -def _append_filename_param(url, filename): - """Append 'filename=' parameter to given URL. - - Some BMCs seem to validate boot image URL requiring the URL to end - with something resembling ISO image file name. - - This function tries to add, hopefully, meaningless 'filename' - parameter to URL's query string in hope to make the entire boot image - URL looking more convincing to the BMC. - - However, `url` with fragments might not get cured by this hack. - - :param url: a URL to work on - :param filename: name of the file to append to the URL - :returns: original URL with 'filename' parameter appended - """ - parsed_url = urlparse.urlparse(url) - parsed_qs = urlparse.parse_qsl(parsed_url.query) - - has_filename = [x for x in parsed_qs if x[0].lower() == 'filename'] - if has_filename: - return url - - parsed_qs.append(('filename', filename)) - parsed_url = list(parsed_url) - parsed_url[4] = urlparse.urlencode(parsed_qs) - - return urlparse.urlunparse(parsed_url) - - -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 - - -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 - - def _insert_vmedia(task, boot_url, boot_device): """Insert bootable ISO image into virtual CD or DVD @@ -283,157 +227,6 @@ def _has_vmedia_device(task, boot_device): return True -def _cleanup_iso_image(task): - """Deletes the ISO if it was created for the instance. - - :param task: A task from TaskManager. - """ - iso_object_name = _get_iso_image_name(task.node) - - _unpublish_image(iso_object_name) - - -def _unpublish_image(object_name): - """Withdraw the image previously made downloadable. - - Depending on ironic settings, removes previously published file - from where it has been published - Swift or local HTTP server's - document root. - - :param object_name: name of the published file (optional) - """ - if CONF.redfish.use_swift: - container = CONF.redfish.swift_container - - swift_api = swift.SwiftAPI() - - LOG.debug("Cleaning up image %(name)s from Swift container " - "%(container)s", {'name': object_name, - 'container': container}) - - try: - swift_api.delete_object(container, object_name) - - except exception.SwiftOperationError as exc: - LOG.warning("Failed to clean up image %(image)s. Error: " - "%(error)s.", {'image': object_name, - 'error': exc}) - - else: - published_file = os.path.join( - CONF.deploy.http_root, IMAGE_SUBDIR, object_name) - - ironic_utils.unlink_without_raise(published_file) - - -def _prepare_floppy_image(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 = _get_floppy_image_name(task.node) - - 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) - - image_url = _publish_image(vfat_image_tmpfile, object_name) - - 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 - - -def _publish_image(image_file, object_name): - """Make image file downloadable. - - Depending on ironic settings, pushes given file into Swift or copies - it over to local HTTP server's document root and returns publicly - accessible URL leading to the given file. - - :param image_file: path to file to publish - :param object_name: name of the published file - :return: a URL to download published file - """ - - if CONF.redfish.use_swift: - 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, object_name, image_file, - object_headers=object_headers) - - image_url = swift_api.get_temp_url(container, object_name, timeout) - - else: - public_dir = os.path.join(CONF.deploy.http_root, IMAGE_SUBDIR) - - if not os.path.exists(public_dir): - os.mkdir(public_dir, 0o755) - - published_file = os.path.join(public_dir, object_name) - - try: - os.link(image_file, published_file) - os.chmod(image_file, CONF.redfish.file_permission) - - except OSError as exc: - LOG.debug( - "Could not hardlink image file %(image)s to public " - "location %(public)s (will copy it over): " - "%(error)s", {'image': image_file, - 'public': published_file, - 'error': exc}) - - shutil.copyfile(image_file, published_file) - os.chmod(published_file, CONF.redfish.file_permission) - - image_url = os.path.join( - CONF.deploy.http_url, IMAGE_SUBDIR, object_name) - - image_url = _append_filename_param( - image_url, os.path.basename(image_file)) - - return image_url - - -def _cleanup_floppy_image(task): - """Deletes the floppy image if it was created for the node. - - :param task: an ironic node object. - """ - floppy_object_name = _get_floppy_image_name(task.node) - - _unpublish_image(floppy_object_name) - - def _parse_deploy_info(node): """Gets the instance and driver specific Node deployment info. @@ -456,248 +249,6 @@ def _parse_deploy_info(node): return deploy_info -def _prepare_iso_image(task, kernel_href, ramdisk_href, - bootloader_href=None, configdrive=None, - root_uuid=None, params=None, base_iso=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. - - If `configdrive` is specified it will be eventually written onto - the boot ISO image. - - :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 configdrive: URL to or a compressed blob of a ISO9660 or - FAT-formatted OpenStack config drive image. This image will be - written onto the built ISO image. Optional. - :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) and not base_iso: - raise exception.InvalidParameterValue(_( - "Unable to find kernel, ramdisk for " - "building ISO, or explicit ISO for %(node)s") % - {'node': task.node.uuid}) - - i_info = task.node.instance_info - - # NOTE(TheJulia): Until we support modifying a base iso, most of - # this logic actually does nothing in the end. But it should! - if deploy_utils.get_boot_option(task.node) == "ramdisk": - if not base_iso: - kernel_params = "root=/dev/ram0 text " - kernel_params += i_info.get("ramdisk_kernel_arguments", "") - else: - kernel_params = None - - else: - kernel_params = i_info.get( - 'kernel_append_params', CONF.redfish.kernel_append_params) - - if params and not base_iso: - 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 boot_fileobj: - - with tempfile.NamedTemporaryFile( - dir=CONF.tempdir, suffix='.img') as cfgdrv_fileobj: - - configdrive_href = configdrive - - # FIXME(TheJulia): This is treated as conditional with - # a base_iso as the intent, eventually, is to support - # injection into the supplied image. - - if configdrive and not base_iso: - parsed_url = urlparse.urlparse(configdrive) - if not parsed_url.scheme: - cfgdrv_blob = base64.decode_as_bytes(configdrive) - - with open(cfgdrv_fileobj.name, 'wb') as f: - f.write(cfgdrv_blob) - - configdrive_href = urlparse.urlunparse( - ('file', '', cfgdrv_fileobj.name, '', '', '')) - - LOG.debug("Built configdrive out of configdrive blob " - "for node %(node)s", {'node': task.node.uuid}) - - boot_iso_tmp_file = boot_fileobj.name - images.create_boot_iso( - task.context, boot_iso_tmp_file, - kernel_href, ramdisk_href, - esp_image_href=bootloader_href, - configdrive_href=configdrive_href, - root_uuid=root_uuid, - kernel_params=kernel_params, - boot_mode=boot_mode, - base_iso=base_iso) - - iso_object_name = _get_iso_image_name(task.node) - - image_url = _publish_image( - boot_iso_tmp_file, iso_object_name) - - LOG.debug("Created ISO %(name)s in object store for node %(node)s, " - "exposed as temporary URL " - "%(url)s", {'node': task.node.uuid, - 'name': iso_object_name, - 'url': image_url}) - - return image_url - - -def _prepare_deploy_iso(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. - - If network interface supplies network configuration (`network_data`), - a new `configdrive` will be created with `network_data.json` inside, - and eventually written down onto the boot ISO. - - :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 = _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') - - # TODO(TheJulia): At some point we should support something like - # boot_iso for the deploy interface, perhaps when we support config - # injection. - prepare_iso_image = functools.partial( - _prepare_iso_image, task, kernel_href, ramdisk_href, - bootloader_href=bootloader_href, params=params) - - network_data = task.driver.network.get_node_network_data(task) - if network_data: - with tempfile.NamedTemporaryFile( - dir=CONF.tempdir, suffix='.iso') as metadata_fileobj: - - with open(metadata_fileobj.name, 'w') as f: - json.dump(network_data, f, indent=2) - - files_info = { - metadata_fileobj.name: 'openstack/latest/' - 'network_data.json' - } - - with tempfile.NamedTemporaryFile( - dir=CONF.tempdir, suffix='.img') as cfgdrv_fileobj: - - images.create_vfat_image(cfgdrv_fileobj.name, files_info) - - configdrive_href = urlparse.urlunparse( - ('file', '', cfgdrv_fileobj.name, '', '', '')) - - LOG.debug("Built configdrive %(name)s out of network data " - "for node %(node)s", {'name': configdrive_href, - 'node': task.node.uuid}) - - return prepare_iso_image(configdrive=configdrive_href) - - return prepare_iso_image() - - -def _prepare_boot_iso(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 = _parse_deploy_info(node) - - kernel_href = node.instance_info.get('kernel') - ramdisk_href = node.instance_info.get('ramdisk') - base_iso = node.instance_info.get('boot_iso') - - if (not kernel_href or not ramdisk_href) and not base_iso: - - 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 _prepare_iso_image( - task, kernel_href, ramdisk_href, bootloader_href, - root_uuid=root_uuid, base_iso=base_iso) - - class RedfishVirtualMediaBoot(base.BootInterface): """Virtual media boot interface over Redfish. @@ -875,7 +426,7 @@ class RedfishVirtualMediaBoot(base.BootInterface): # we tell it to ramdisk_params['boot_method'] = 'vmedia' - floppy_ref = _prepare_floppy_image( + floppy_ref = image_utils.prepare_floppy_image( task, params=ramdisk_params) _eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY) @@ -892,7 +443,8 @@ class RedfishVirtualMediaBoot(base.BootInterface): mode = deploy_utils.rescue_or_deploy_mode(node) - iso_ref = _prepare_deploy_iso(task, ramdisk_params, mode) + iso_ref = image_utils.prepare_deploy_iso(task, ramdisk_params, + mode, d_info) _eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD) _insert_vmedia(task, iso_ref, sushy.VIRTUAL_MEDIA_CD) @@ -922,13 +474,13 @@ class RedfishVirtualMediaBoot(base.BootInterface): "%(node)s", {'node': task.node.uuid}) _eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD) - _cleanup_iso_image(task) + image_utils.cleanup_iso_image(task) if (config_via_floppy and _has_vmedia_device(task, sushy.VIRTUAL_MEDIA_FLOPPY)): _eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY) - _cleanup_floppy_image(task) + image_utils.cleanup_floppy_image(task) def prepare_instance(self, task): """Prepares the boot of instance over virtual media. @@ -978,7 +530,8 @@ class RedfishVirtualMediaBoot(base.BootInterface): params.update(root_uuid=root_uuid) - iso_ref = _prepare_boot_iso(task, **params) + deploy_info = _parse_deploy_info(node) + iso_ref = image_utils.prepare_boot_iso(task, deploy_info, **params) _eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD) _insert_vmedia(task, iso_ref, sushy.VIRTUAL_MEDIA_CD) @@ -1008,7 +561,7 @@ class RedfishVirtualMediaBoot(base.BootInterface): if config_via_floppy: _eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY) - _cleanup_iso_image(task) + image_utils.cleanup_iso_image(task) @classmethod def _set_boot_device(cls, task, device, persistent=False): diff --git a/ironic/tests/unit/drivers/modules/redfish/test_boot.py b/ironic/tests/unit/drivers/modules/redfish/test_boot.py index b365eb8307..c723457b86 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_boot.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_boot.py @@ -13,18 +13,17 @@ # License for the specific language governing permissions and limitations # under the License. -import os from unittest 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 import image_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 @@ -177,356 +176,6 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): redfish_boot._parse_deploy_info, task.node) - def test__append_filename_param_without_qs(self): - res = redfish_boot._append_filename_param( - 'http://a.b/c', 'b.img') - expected = 'http://a.b/c?filename=b.img' - self.assertEqual(expected, res) - - def test__append_filename_param_with_qs(self): - res = redfish_boot._append_filename_param( - 'http://a.b/c?d=e&f=g', 'b.img') - expected = 'http://a.b/c?d=e&f=g&filename=b.img' - self.assertEqual(expected, res) - - def test__append_filename_param_with_filename(self): - res = redfish_boot._append_filename_param( - 'http://a.b/c?filename=bootme.img', 'b.img') - expected = 'http://a.b/c?filename=bootme.img' - self.assertEqual(expected, res) - - @mock.patch.object(redfish_boot, 'swift', autospec=True) - def test__publish_image_swift(self, mock_swift): - mock_swift_api = mock_swift.SwiftAPI.return_value - mock_swift_api.get_temp_url.return_value = 'https://a.b/c.f?e=f' - - url = redfish_boot._publish_image('file.iso', 'boot.iso') - - self.assertEqual( - 'https://a.b/c.f?e=f&filename=file.iso', url) - - 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) - def test__unpublish_image_swift(self, mock_swift): - with task_manager.acquire(self.context, self.node.uuid, - shared=True) as task: - object_name = 'image-%s' % task.node.uuid - - redfish_boot._unpublish_image(object_name) - - 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', object_name) - - @mock.patch.object(os, 'chmod', autospec=True) - @mock.patch.object(redfish_boot, 'shutil', autospec=True) - @mock.patch.object(os, 'link', autospec=True) - @mock.patch.object(os, 'mkdir', autospec=True) - def test__publish_image_local_link( - self, mock_mkdir, mock_link, mock_shutil, mock_chmod): - self.config(use_swift=False, group='redfish') - self.config(http_url='http://localhost', group='deploy') - - url = redfish_boot._publish_image('file.iso', 'boot.iso') - - self.assertEqual( - 'http://localhost/redfish/boot.iso?filename=file.iso', url) - - mock_mkdir.assert_called_once_with('/httpboot/redfish', 0o755) - mock_link.assert_called_once_with( - 'file.iso', '/httpboot/redfish/boot.iso') - mock_chmod.assert_called_once_with('file.iso', 0o644) - - @mock.patch.object(os, 'chmod', autospec=True) - @mock.patch.object(redfish_boot, 'shutil', autospec=True) - @mock.patch.object(os, 'link', autospec=True) - @mock.patch.object(os, 'mkdir', autospec=True) - def test__publish_image_local_copy( - self, mock_mkdir, mock_link, mock_shutil, mock_chmod): - self.config(use_swift=False, group='redfish') - self.config(http_url='http://localhost', group='deploy') - - mock_link.side_effect = OSError() - - url = redfish_boot._publish_image('file.iso', 'boot.iso') - - self.assertEqual( - 'http://localhost/redfish/boot.iso?filename=file.iso', url) - - mock_mkdir.assert_called_once_with('/httpboot/redfish', 0o755) - - mock_shutil.copyfile.assert_called_once_with( - 'file.iso', '/httpboot/redfish/boot.iso') - mock_chmod.assert_called_once_with('/httpboot/redfish/boot.iso', - 0o644) - - @mock.patch.object(redfish_boot, 'ironic_utils', autospec=True) - def test__unpublish_image_local(self, mock_ironic_utils): - self.config(use_swift=False, group='redfish') - - with task_manager.acquire(self.context, self.node.uuid, - shared=True) as task: - object_name = 'image-%s' % task.node.uuid - - expected_file = '/httpboot/redfish/' + object_name - - redfish_boot._unpublish_image(object_name) - - mock_ironic_utils.unlink_without_raise.assert_called_once_with( - expected_file) - - @mock.patch.object(redfish_boot, '_unpublish_image', autospec=True) - def test__cleanup_floppy_image(self, mock_unpublish): - with task_manager.acquire(self.context, self.node.uuid, - shared=True) as task: - redfish_boot._cleanup_floppy_image(task) - - object_name = 'image-%s' % task.node.uuid - - mock_unpublish.assert_called_once_with(object_name) - - @mock.patch.object(redfish_boot, '_publish_image', autospec=True) - @mock.patch.object(images, 'create_vfat_image', autospec=True) - def test__prepare_floppy_image( - self, mock_create_vfat_image, mock__publish_image): - with task_manager.acquire(self.context, self.node.uuid, - shared=True) as task: - expected_url = 'https://a.b/c.f?e=f' - - mock__publish_image.return_value = expected_url - - url = redfish_boot._prepare_floppy_image(task) - - object_name = 'image-%s' % task.node.uuid - - mock__publish_image.assert_called_once_with( - mock.ANY, object_name) - - mock_create_vfat_image.assert_called_once_with( - mock.ANY, parameters=mock.ANY) - - self.assertEqual(expected_url, url) - - @mock.patch.object(redfish_boot, '_unpublish_image', autospec=True) - def test__cleanup_iso_image(self, mock_unpublish): - with task_manager.acquire(self.context, self.node.uuid, - shared=True) as task: - redfish_boot._cleanup_iso_image(task) - - object_name = 'boot-%s' % task.node.uuid - - mock_unpublish.assert_called_once_with(object_name) - - @mock.patch.object(redfish_boot, '_publish_image', autospec=True) - @mock.patch.object(images, 'create_boot_iso', autospec=True) - def test__prepare_iso_image_uefi( - self, mock_create_boot_iso, mock__publish_image): - with task_manager.acquire(self.context, self.node.uuid, - shared=True) as task: - task.node.instance_info.update(deploy_boot_mode='uefi') - - expected_url = 'https://a.b/c.f?e=f' - - mock__publish_image.return_value = expected_url - - url = redfish_boot._prepare_iso_image( - task, 'http://kernel/img', 'http://ramdisk/img', - 'http://bootloader/img', root_uuid=task.node.uuid, - base_iso=None) - - object_name = 'boot-%s' % task.node.uuid - - mock__publish_image.assert_called_once_with( - mock.ANY, object_name) - - 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', - configdrive_href=mock.ANY, - kernel_params='nofb nomodeset vga=normal', - root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', - base_iso=None) - - self.assertEqual(expected_url, url) - - @mock.patch.object(redfish_boot, '_publish_image', autospec=True) - @mock.patch.object(images, 'create_boot_iso', autospec=True) - def test__prepare_iso_image_bios( - self, mock_create_boot_iso, mock__publish_image): - with task_manager.acquire(self.context, self.node.uuid, - shared=True) as task: - - expected_url = 'https://a.b/c.f?e=f' - - mock__publish_image.return_value = expected_url - - url = redfish_boot._prepare_iso_image( - task, 'http://kernel/img', 'http://ramdisk/img', - bootloader_href=None, root_uuid=task.node.uuid) - - object_name = 'boot-%s' % task.node.uuid - - mock__publish_image.assert_called_once_with( - mock.ANY, object_name) - - 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, - configdrive_href=mock.ANY, - kernel_params='nofb nomodeset vga=normal', - root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', - base_iso=None) - - self.assertEqual(expected_url, url) - - @mock.patch.object(redfish_boot, '_publish_image', autospec=True) - @mock.patch.object(images, 'create_boot_iso', autospec=True) - def test__prepare_iso_image_kernel_params( - self, mock_create_boot_iso, mock__publish_image): - with task_manager.acquire(self.context, self.node.uuid, - shared=True) as task: - kernel_params = 'network-config=base64-cloudinit-blob' - - task.node.instance_info.update(kernel_append_params=kernel_params) - - redfish_boot._prepare_iso_image( - task, 'http://kernel/img', 'http://ramdisk/img', - bootloader_href=None, root_uuid=task.node.uuid, - base_iso=None) - - 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, - configdrive_href=mock.ANY, - kernel_params=kernel_params, - root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', - base_iso=None) - - @mock.patch.object(redfish_boot, '_publish_image', autospec=True) - @mock.patch.object(images, 'create_boot_iso', autospec=True) - def test__prepare_iso_image_boot_iso( - self, mock_create_boot_iso, mock__publish_image): - with task_manager.acquire(self.context, self.node.uuid, - shared=True) as task: - - task.node.instance_info = {'boot_iso': 'http://host/boot.iso', - 'capabilities': { - 'boot_option': 'ramdisk'}} - - redfish_boot._prepare_iso_image( - task, None, None, root_uuid=None, - base_iso='http://host/boot.iso') - - mock_create_boot_iso.assert_called_once_with( - mock.ANY, mock.ANY, None, None, - boot_mode=None, esp_image_href=None, - configdrive_href=None, - kernel_params=None, - root_uuid=None, - base_iso='http://host/boot.iso') - - @mock.patch.object(redfish_boot, '_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') - - redfish_boot._prepare_deploy_iso(task, {}, 'deploy') - - mock__prepare_iso_image.assert_called_once_with( - task, 'kernel', 'ramdisk', 'bootloader', params={}) - - @mock.patch.object(redfish_boot, '_prepare_iso_image', autospec=True) - @mock.patch.object(images, 'create_vfat_image', autospec=True) - def test__prepare_deploy_iso_network_data( - self, mock_create_vfat_image, 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'} - ) - - task.node.instance_info.update() - - network_data = {'a': ['b']} - - mock_get_node_nw_data = mock.MagicMock(return_value=network_data) - task.driver.network.get_node_network_data = mock_get_node_nw_data - - redfish_boot._prepare_deploy_iso(task, {}, 'deploy') - - mock_create_vfat_image.assert_called_once_with( - mock.ANY, mock.ANY) - - mock__prepare_iso_image.assert_called_once_with( - task, 'kernel', 'ramdisk', bootloader_href=None, - configdrive=mock.ANY, params={}) - - @mock.patch.object(redfish_boot, '_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'}) - - redfish_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, base_iso=None) - - @mock.patch.object(redfish_boot, '_prepare_iso_image', autospec=True) - @mock.patch.object(images, 'create_boot_iso', autospec=True) - def test__prepare_boot_iso_user_supplied(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( - {'boot_iso': 'http://boot/iso'}) - - redfish_boot._prepare_boot_iso( - task, root_uuid=task.node.uuid) - - mock__prepare_iso_image.assert_called_once_with( - mock.ANY, None, None, - 'bootloader', root_uuid=task.node.uuid, - base_iso='http://boot/iso') - @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) @mock.patch.object(deploy_utils, 'validate_image_properties', autospec=True) @@ -676,7 +325,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', autospec=True) - @mock.patch.object(redfish_boot, '_prepare_deploy_iso', autospec=True) + @mock.patch.object(image_utils, 'prepare_deploy_iso', autospec=True) @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) @@ -686,14 +335,14 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): def test_prepare_ramdisk_with_params( self, mock_boot_mode_utils, mock_node_power_action, mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia, - mock__prepare_deploy_iso, mock_node_set_boot_device): + mock_prepare_deploy_iso, mock_node_set_boot_device): with task_manager.acquire(self.context, self.node.uuid, shared=False) as task: task.node.provision_state = states.DEPLOYING mock__parse_driver_info.return_value = {} - mock__prepare_deploy_iso.return_value = 'image-url' + mock_prepare_deploy_iso.return_value = 'image-url' task.driver.boot.prepare_ramdisk(task, {}) @@ -712,8 +361,8 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): 'ipa-debug': '1', } - mock__prepare_deploy_iso.assert_called_once_with( - task, expected_params, 'deploy') + mock_prepare_deploy_iso.assert_called_once_with( + task, expected_params, 'deploy', {}) mock_node_set_boot_device.assert_called_once_with( task, boot_devices.CDROM, False) @@ -722,7 +371,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', autospec=True) - @mock.patch.object(redfish_boot, '_prepare_deploy_iso', autospec=True) + @mock.patch.object(image_utils, 'prepare_deploy_iso', autospec=True) @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) @@ -732,14 +381,14 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): def test_prepare_ramdisk_no_debug( self, mock_boot_mode_utils, mock_node_power_action, mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia, - mock__prepare_deploy_iso, mock_node_set_boot_device): + mock_prepare_deploy_iso, mock_node_set_boot_device): self.config(debug=False) with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: task.node.provision_state = states.DEPLOYING mock__parse_driver_info.return_value = {} - mock__prepare_deploy_iso.return_value = 'image-url' + mock_prepare_deploy_iso.return_value = 'image-url' task.driver.boot.prepare_ramdisk(task, {}) @@ -757,8 +406,8 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): 'ipa-agent-token': mock.ANY, } - mock__prepare_deploy_iso.assert_called_once_with( - task, expected_params, 'deploy') + mock_prepare_deploy_iso.assert_called_once_with( + task, expected_params, 'deploy', {}) mock_node_set_boot_device.assert_called_once_with( task, boot_devices.CDROM, False) @@ -767,8 +416,8 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', autospec=True) - @mock.patch.object(redfish_boot, '_prepare_floppy_image', autospec=True) - @mock.patch.object(redfish_boot, '_prepare_deploy_iso', autospec=True) + @mock.patch.object(image_utils, 'prepare_floppy_image', autospec=True) + @mock.patch.object(image_utils, 'prepare_deploy_iso', autospec=True) @mock.patch.object(redfish_boot, '_has_vmedia_device', autospec=True) @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) @@ -779,20 +428,22 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): def test_prepare_ramdisk_with_floppy( self, mock_boot_mode_utils, mock_node_power_action, mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia, - mock__has_vmedia_device, mock__prepare_deploy_iso, - mock__prepare_floppy_image, mock_node_set_boot_device): + mock__has_vmedia_device, mock_prepare_deploy_iso, + mock_prepare_floppy_image, mock_node_set_boot_device): with task_manager.acquire(self.context, self.node.uuid, shared=False) as task: task.node.provision_state = states.DEPLOYING - mock__parse_driver_info.return_value = { + d_info = { 'config_via_floppy': True } + mock__parse_driver_info.return_value = d_info + 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' + mock_prepare_floppy_image.return_value = 'floppy-image-url' + mock_prepare_deploy_iso.return_value = 'cd-image-url' task.driver.boot.prepare_ramdisk(task, {}) @@ -825,8 +476,8 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): 'ipa-agent-token': mock.ANY, } - mock__prepare_deploy_iso.assert_called_once_with( - task, expected_params, 'deploy') + mock_prepare_deploy_iso.assert_called_once_with( + task, expected_params, 'deploy', d_info) mock_node_set_boot_device.assert_called_once_with( task, boot_devices.CDROM, False) @@ -835,12 +486,12 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): @mock.patch.object(redfish_boot, '_has_vmedia_device', autospec=True) @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) - @mock.patch.object(redfish_boot, '_cleanup_iso_image', autospec=True) - @mock.patch.object(redfish_boot, '_cleanup_floppy_image', autospec=True) + @mock.patch.object(image_utils, 'cleanup_iso_image', autospec=True) + @mock.patch.object(image_utils, 'cleanup_floppy_image', autospec=True) @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) def test_clean_up_ramdisk( - self, mock__parse_driver_info, mock__cleanup_floppy_image, - mock__cleanup_iso_image, mock__eject_vmedia, + 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, @@ -852,9 +503,9 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): task.driver.boot.clean_up_ramdisk(task) - mock__cleanup_iso_image.assert_called_once_with(task) + mock_cleanup_iso_image.assert_called_once_with(task) - mock__cleanup_floppy_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) @@ -868,17 +519,17 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'clean_up_instance', autospec=True) - @mock.patch.object(redfish_boot, '_prepare_boot_iso', autospec=True) + @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True) @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) - @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) + @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) 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): + mock__parse_deploy_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: @@ -888,8 +539,15 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): mock_deploy_utils.get_boot_option.return_value = 'net' - mock__parse_driver_info.return_value = {} - mock__prepare_boot_iso.return_value = 'image-url' + d_info = { + 'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader' + } + + mock__parse_deploy_info.return_value = d_info + + mock_prepare_boot_iso.return_value = 'image-url' task.driver.boot.prepare_instance(task) @@ -897,8 +555,8 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): 'root_uuid': self.node.uuid } - mock__prepare_boot_iso.assert_called_once_with( - task, **expected_params) + mock_prepare_boot_iso.assert_called_once_with( + task, d_info, **expected_params) mock__eject_vmedia.assert_called_once_with( task, sushy.VIRTUAL_MEDIA_CD) @@ -913,17 +571,17 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'clean_up_instance', autospec=True) - @mock.patch.object(redfish_boot, '_prepare_boot_iso', autospec=True) + @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True) @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) - @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) + @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) 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): + mock__parse_deploy_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: @@ -933,11 +591,20 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): mock_deploy_utils.get_boot_option.return_value = 'ramdisk' - mock__prepare_boot_iso.return_value = 'image-url' + d_info = { + 'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader' + } + mock__parse_deploy_info.return_value = d_info + + mock_prepare_boot_iso.return_value = 'image-url' task.driver.boot.prepare_instance(task) - mock__prepare_boot_iso.assert_called_once_with(task) + mock_clean_up_instance.assert_called_once_with(mock.ANY, task) + + mock_prepare_boot_iso.assert_called_once_with(task, d_info) mock__eject_vmedia.assert_called_once_with( task, sushy.VIRTUAL_MEDIA_CD) @@ -952,32 +619,38 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'clean_up_instance', autospec=True) - @mock.patch.object(redfish_boot, '_prepare_boot_iso', autospec=True) + @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True) @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) - @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) + @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) def test_prepare_instance_ramdisk_boot_iso( 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): + mock__parse_deploy_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 - task.node.instance_info = {'boot_iso': 'http://host/boot.iso'} mock_deploy_utils.get_boot_option.return_value = 'ramdisk' - mock__prepare_boot_iso.return_value = 'image-url' + d_info = { + 'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader' + } + + mock__parse_deploy_info.return_value = d_info + mock_prepare_boot_iso.return_value = 'image-url' task.driver.boot.prepare_instance(task) - mock__prepare_boot_iso.assert_called_once_with(task) + mock_prepare_boot_iso.assert_called_once_with(task, d_info) mock__eject_vmedia.assert_called_once_with( task, sushy.VIRTUAL_MEDIA_CD) @@ -992,17 +665,17 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, 'clean_up_instance', autospec=True) - @mock.patch.object(redfish_boot, '_prepare_boot_iso', autospec=True) + @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True) @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) - @mock.patch.object(redfish_boot, '_parse_driver_info', autospec=True) + @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) def test_prepare_instance_ramdisk_boot_iso_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): + mock__parse_deploy_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: @@ -1011,12 +684,13 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): i_info['boot_iso'] = "super-magic" task.node.instance_info = i_info mock_deploy_utils.get_boot_option.return_value = 'ramdisk' + mock__parse_deploy_info.return_value = {} - mock__prepare_boot_iso.return_value = 'image-url' + mock_prepare_boot_iso.return_value = 'image-url' task.driver.boot.prepare_instance(task) - mock__prepare_boot_iso.assert_called_once_with(task) + mock_prepare_boot_iso.assert_called_once_with(task, {}) mock__eject_vmedia.assert_called_once_with( task, sushy.VIRTUAL_MEDIA_CD) @@ -1030,11 +704,11 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) - @mock.patch.object(redfish_boot, '_cleanup_iso_image', autospec=True) + @mock.patch.object(image_utils, '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): + mock_cleanup_iso_image, mock__eject_vmedia): with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: @@ -1046,7 +720,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): 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_cleanup_iso_image.assert_called_once_with(task) mock__eject_vmedia.assert_called_once_with( task, sushy.VIRTUAL_MEDIA_CD) @@ -1063,8 +737,8 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): self._test_prepare_instance_local_boot() @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) - @mock.patch.object(redfish_boot, '_cleanup_iso_image', autospec=True) - def _test_clean_up_instance(self, mock__cleanup_iso_image, + @mock.patch.object(image_utils, '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, @@ -1072,7 +746,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): task.driver.boot.clean_up_instance(task) - mock__cleanup_iso_image.assert_called_once_with(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)) diff --git a/ironic/tests/unit/drivers/modules/test_image_utils.py b/ironic/tests/unit/drivers/modules/test_image_utils.py new file mode 100644 index 0000000000..6b9f85ca5c --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_image_utils.py @@ -0,0 +1,413 @@ +# 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 os +from unittest import mock + +from oslo_utils import importutils + +from ironic.common import images +from ironic.conductor import task_manager +from ironic.drivers.modules import image_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() + + +class RedfishImageHandlerTestCase(db_base.DbTestCase): + + def setUp(self): + super(RedfishImageHandlerTestCase, 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) + + def test__append_filename_param_without_qs(self): + img_handler_obj = image_utils.ImageHandler(self.node.driver) + res = img_handler_obj._append_filename_param( + 'http://a.b/c', 'b.img') + expected = 'http://a.b/c?filename=b.img' + self.assertEqual(expected, res) + + def test__append_filename_param_with_qs(self): + img_handler_obj = image_utils.ImageHandler(self.node.driver) + res = img_handler_obj._append_filename_param( + 'http://a.b/c?d=e&f=g', 'b.img') + expected = 'http://a.b/c?d=e&f=g&filename=b.img' + self.assertEqual(expected, res) + + def test__append_filename_param_with_filename(self): + img_handler_obj = image_utils.ImageHandler(self.node.driver) + res = img_handler_obj._append_filename_param( + 'http://a.b/c?filename=bootme.img', 'b.img') + expected = 'http://a.b/c?filename=bootme.img' + self.assertEqual(expected, res) + + @mock.patch.object(image_utils, 'swift', autospec=True) + def test_publish_image_swift(self, mock_swift): + img_handler_obj = image_utils.ImageHandler(self.node.driver) + mock_swift_api = mock_swift.SwiftAPI.return_value + mock_swift_api.get_temp_url.return_value = 'https://a.b/c.f?e=f' + + url = img_handler_obj.publish_image('file.iso', 'boot.iso') + + self.assertEqual( + 'https://a.b/c.f?e=f&filename=file.iso', url) + + 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(image_utils, 'swift', autospec=True) + def test_unpublish_image_swift(self, mock_swift): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + img_handler_obj = image_utils.ImageHandler(self.node.driver) + object_name = 'image-%s' % task.node.uuid + + img_handler_obj.unpublish_image(object_name) + + 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', object_name) + + @mock.patch.object(image_utils.ImageHandler, '_is_swift_enabled', + autospec=True) + @mock.patch.object(os, 'chmod', autospec=True) + @mock.patch.object(image_utils, 'shutil', autospec=True) + @mock.patch.object(os, 'link', autospec=True) + @mock.patch.object(os, 'mkdir', autospec=True) + def test_publish_image_local_link( + self, mock_mkdir, mock_link, mock_shutil, mock_chmod, + mock__is_swift): + img_handler_obj = image_utils.ImageHandler(self.node.driver) + mock__is_swift.return_value = False + self.config(use_swift=False, group='redfish') + self.config(http_url='http://localhost', group='deploy') + + url = img_handler_obj.publish_image('file.iso', 'boot.iso') + + self.assertEqual( + 'http://localhost/redfish/boot.iso?filename=file.iso', url) + + mock_mkdir.assert_called_once_with('/httpboot/redfish', 0o755) + mock_link.assert_called_once_with( + 'file.iso', '/httpboot/redfish/boot.iso') + mock_chmod.assert_called_once_with('file.iso', 0o644) + + @mock.patch.object(image_utils.ImageHandler, '_is_swift_enabled', + autospec=True) + @mock.patch.object(os, 'chmod', autospec=True) + @mock.patch.object(image_utils, 'shutil', autospec=True) + @mock.patch.object(os, 'link', autospec=True) + @mock.patch.object(os, 'mkdir', autospec=True) + def test_publish_image_local_copy(self, mock_mkdir, mock_link, + mock_shutil, mock_chmod, + mock__is_swift): + mock__is_swift.return_value = False + self.config(use_swift=False, group='redfish') + self.config(http_url='http://localhost', group='deploy') + img_handler_obj = image_utils.ImageHandler(self.node.driver) + + mock_link.side_effect = OSError() + + url = img_handler_obj.publish_image('file.iso', 'boot.iso') + + self.assertEqual( + 'http://localhost/redfish/boot.iso?filename=file.iso', url) + + mock_mkdir.assert_called_once_with('/httpboot/redfish', 0o755) + + mock_shutil.copyfile.assert_called_once_with( + 'file.iso', '/httpboot/redfish/boot.iso') + mock_chmod.assert_called_once_with('/httpboot/redfish/boot.iso', + 0o644) + + @mock.patch.object(image_utils.ImageHandler, '_is_swift_enabled', + autospec=True) + @mock.patch.object(image_utils, 'ironic_utils', autospec=True) + def test_unpublish_image_local(self, mock_ironic_utils, mock__is_swift): + self.config(use_swift=False, group='redfish') + mock__is_swift.return_value = False + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + img_handler_obj = image_utils.ImageHandler(self.node.driver) + object_name = 'image-%s' % task.node.uuid + + expected_file = '/httpboot/redfish/' + object_name + + img_handler_obj.unpublish_image(object_name) + + mock_ironic_utils.unlink_without_raise.assert_called_once_with( + expected_file) + + +class RedfishImageUtilsTestCase(db_base.DbTestCase): + + def setUp(self): + super(RedfishImageUtilsTestCase, 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(image_utils.ImageHandler, 'unpublish_image', + autospec=True) + def test_cleanup_floppy_image(self, mock_unpublish): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + image_utils.cleanup_floppy_image(task) + + object_name = 'image-%s' % task.node.uuid + + mock_unpublish.assert_called_once_with(mock.ANY, object_name) + + @mock.patch.object(image_utils.ImageHandler, 'publish_image', + autospec=True) + @mock.patch.object(images, 'create_vfat_image', autospec=True) + def test_prepare_floppy_image( + self, mock_create_vfat_image, mock_publish_image): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + expected_url = 'https://a.b/c.f?e=f' + + mock_publish_image.return_value = expected_url + + url = image_utils.prepare_floppy_image(task) + + object_name = 'image-%s' % task.node.uuid + + mock_publish_image.assert_called_once_with(mock.ANY, + mock.ANY, object_name) + + mock_create_vfat_image.assert_called_once_with( + mock.ANY, parameters=mock.ANY) + + self.assertEqual(expected_url, url) + + @mock.patch.object(image_utils.ImageHandler, 'unpublish_image', + autospec=True) + def test_cleanup_iso_image(self, mock_unpublish): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + image_utils.cleanup_iso_image(task) + + object_name = 'boot-%s' % task.node.uuid + + mock_unpublish.assert_called_once_with(mock.ANY, object_name) + + @mock.patch.object(image_utils.ImageHandler, 'publish_image', + autospec=True) + @mock.patch.object(images, 'create_boot_iso', autospec=True) + def test__prepare_iso_image_uefi( + self, mock_create_boot_iso, mock_publish_image): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.instance_info.update(deploy_boot_mode='uefi') + + expected_url = 'https://a.b/c.f?e=f' + + mock_publish_image.return_value = expected_url + + url = image_utils._prepare_iso_image( + task, 'http://kernel/img', 'http://ramdisk/img', + 'http://bootloader/img', root_uuid=task.node.uuid) + + object_name = 'boot-%s' % task.node.uuid + + mock_publish_image.assert_called_once_with( + mock.ANY, mock.ANY, object_name) + + 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', + configdrive_href=mock.ANY, + kernel_params='nofb nomodeset vga=normal', + root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', + base_iso=None) + + self.assertEqual(expected_url, url) + + @mock.patch.object(image_utils.ImageHandler, 'publish_image', + autospec=True) + @mock.patch.object(images, 'create_boot_iso', autospec=True) + def test__prepare_iso_image_bios( + self, mock_create_boot_iso, mock_publish_image): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + + expected_url = 'https://a.b/c.f?e=f' + + mock_publish_image.return_value = expected_url + + url = image_utils._prepare_iso_image( + task, 'http://kernel/img', 'http://ramdisk/img', + bootloader_href=None, root_uuid=task.node.uuid, base_iso=None) + + object_name = 'boot-%s' % task.node.uuid + + mock_publish_image.assert_called_once_with( + mock.ANY, mock.ANY, object_name) + + 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, + configdrive_href=mock.ANY, + kernel_params='nofb nomodeset vga=normal', + root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', + base_iso=None) + + self.assertEqual(expected_url, url) + + @mock.patch.object(image_utils.ImageHandler, 'publish_image', + autospec=True) + @mock.patch.object(images, 'create_boot_iso', autospec=True) + def test__prepare_iso_image_kernel_params( + self, mock_create_boot_iso, mock_publish_image): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + kernel_params = 'network-config=base64-cloudinit-blob' + + task.node.instance_info.update(kernel_append_params=kernel_params) + + image_utils._prepare_iso_image( + task, 'http://kernel/img', 'http://ramdisk/img', + bootloader_href=None, root_uuid=task.node.uuid, + base_iso='/path/to/baseiso') + + 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, + configdrive_href=mock.ANY, + kernel_params=kernel_params, + root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', + base_iso='/path/to/baseiso') + + @mock.patch.object(image_utils, '_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: + + d_info = { + 'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader' + } + task.node.driver_info.update(d_info) + + task.node.instance_info.update(deploy_boot_mode='uefi') + + image_utils.prepare_deploy_iso(task, {}, 'deploy', d_info) + + mock__prepare_iso_image.assert_called_once_with( + task, 'kernel', 'ramdisk', 'bootloader', params={}) + + @mock.patch.object(image_utils, '_prepare_iso_image', autospec=True) + @mock.patch.object(images, 'create_vfat_image', autospec=True) + def test_prepare_deploy_iso_network_data( + self, mock_create_vfat_image, mock__prepare_iso_image): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + + d_info = { + 'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk' + } + task.node.driver_info.update(d_info) + + task.node.instance_info.update() + + network_data = {'a': ['b']} + + mock_get_node_nw_data = mock.MagicMock(return_value=network_data) + task.driver.network.get_node_network_data = mock_get_node_nw_data + + image_utils.prepare_deploy_iso(task, {}, 'deploy', d_info) + + mock_create_vfat_image.assert_called_once_with( + mock.ANY, mock.ANY) + + mock__prepare_iso_image.assert_called_once_with( + task, 'kernel', 'ramdisk', bootloader_href=None, + configdrive=mock.ANY, params={}) + + @mock.patch.object(image_utils, '_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: + d_info = { + 'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader' + } + task.node.driver_info.update(d_info) + + task.node.instance_info.update( + {'image_source': 'http://boot/iso', + 'kernel': 'http://kernel/img', + 'ramdisk': 'http://ramdisk/img'}) + + image_utils.prepare_boot_iso( + task, d_info, 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, + base_iso=None) + + @mock.patch.object(image_utils, '_prepare_iso_image', autospec=True) + @mock.patch.object(images, 'create_boot_iso', autospec=True) + def test_prepare_boot_iso_user_supplied(self, mock_create_boot_iso, + mock__prepare_iso_image): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + d_info = { + 'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader' + } + task.node.driver_info.update(d_info) + + task.node.instance_info.update( + {'boot_iso': 'http://boot/iso'}) + + image_utils.prepare_boot_iso( + task, d_info, root_uuid=task.node.uuid) + + mock__prepare_iso_image.assert_called_once_with( + mock.ANY, None, None, + 'bootloader', root_uuid=task.node.uuid, + base_iso='http://boot/iso')