"Fossies" - the Fresh Open Source Software Archive

Member "ironic-16.0.3/ironic/drivers/modules/image_utils.py" (18 Jan 2021, 18486 Bytes) of package /linux/misc/openstack/ironic-16.0.3.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "image_utils.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 16.0.2_vs_16.0.3.

    1 # Copyright 2019 Red Hat, Inc.
    2 # All Rights Reserved.
    3 #
    4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    5 #    not use this file except in compliance with the License. You may obtain
    6 #    a copy of the License at
    7 #
    8 #         http://www.apache.org/licenses/LICENSE-2.0
    9 #
   10 #    Unless required by applicable law or agreed to in writing, software
   11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   13 #    License for the specific language governing permissions and limitations
   14 #    under the License.
   15 
   16 import functools
   17 import json
   18 import os
   19 import shutil
   20 import tempfile
   21 from urllib import parse as urlparse
   22 
   23 from ironic_lib import utils as ironic_utils
   24 from oslo_log import log
   25 
   26 from ironic.common import exception
   27 from ironic.common.i18n import _
   28 from ironic.common import images
   29 from ironic.common import swift
   30 from ironic.conf import CONF
   31 from ironic.drivers.modules import boot_mode_utils
   32 from ironic.drivers.modules import deploy_utils
   33 
   34 LOG = log.getLogger(__name__)
   35 
   36 
   37 class ImageHandler(object):
   38 
   39     _SWIFT_MAP = {
   40         "redfish": {
   41             "swift_enabled": CONF.redfish.use_swift,
   42             "container": CONF.redfish.swift_container,
   43             "timeout": CONF.redfish.swift_object_expiry_timeout,
   44             "image_subdir": "redfish",
   45             "file_permission": CONF.redfish.file_permission,
   46             "kernel_params": CONF.redfish.kernel_append_params
   47         },
   48         "idrac": {
   49             "swift_enabled": CONF.redfish.use_swift,
   50             "container": CONF.redfish.swift_container,
   51             "timeout": CONF.redfish.swift_object_expiry_timeout,
   52             "image_subdir": "redfish",
   53             "file_permission": CONF.redfish.file_permission,
   54             "kernel_params": CONF.redfish.kernel_append_params
   55         },
   56         "ilo5": {
   57             "swift_enabled": not CONF.ilo.use_web_server_for_images,
   58             "container": CONF.ilo.swift_ilo_container,
   59             "timeout": CONF.ilo.swift_object_expiry_timeout,
   60             "image_subdir": "ilo",
   61             "file_permission": CONF.ilo.file_permission,
   62             "kernel_params": CONF.pxe.pxe_append_params
   63         },
   64         "ilo": {
   65             "swift_enabled": not CONF.ilo.use_web_server_for_images,
   66             "container": CONF.ilo.swift_ilo_container,
   67             "timeout": CONF.ilo.swift_object_expiry_timeout,
   68             "image_subdir": "ilo",
   69             "file_permission": CONF.ilo.file_permission,
   70             "kernel_params": CONF.pxe.pxe_append_params
   71         },
   72     }
   73 
   74     def __init__(self, driver):
   75         self._driver = driver
   76         self._container = self._SWIFT_MAP[driver].get("container")
   77         self._timeout = self._SWIFT_MAP[driver].get("timeout")
   78         self._image_subdir = self._SWIFT_MAP[driver].get("image_subdir")
   79         self._file_permission = self._SWIFT_MAP[driver].get("file_permission")
   80         # To get the kernel parameters
   81         self.kernel_params = self._SWIFT_MAP[driver].get("kernel_params")
   82 
   83     def _is_swift_enabled(self):
   84         try:
   85             return self._SWIFT_MAP[self._driver].get("swift_enabled")
   86         except KeyError:
   87             return False
   88 
   89     def unpublish_image(self, object_name):
   90         """Withdraw the image previously made downloadable.
   91 
   92         Depending on ironic settings, removes previously published file
   93         from where it has been published - Swift or local HTTP server's
   94         document root.
   95 
   96         :param object_name: name of the published file (optional)
   97         """
   98         if self._is_swift_enabled():
   99             container = self._container
  100 
  101             swift_api = swift.SwiftAPI()
  102 
  103             LOG.debug("Cleaning up image %(name)s from Swift container "
  104                       "%(container)s", {'name': object_name,
  105                                         'container': container})
  106 
  107             try:
  108                 swift_api.delete_object(container, object_name)
  109 
  110             except exception.SwiftOperationError as exc:
  111                 LOG.warning("Failed to clean up image %(image)s. Error: "
  112                             "%(error)s.", {'image': object_name,
  113                                            'error': exc})
  114 
  115         else:
  116             published_file = os.path.join(
  117                 CONF.deploy.http_root, self._image_subdir, object_name)
  118 
  119             ironic_utils.unlink_without_raise(published_file)
  120 
  121     def _append_filename_param(self, url, filename):
  122         """Append 'filename=<file>' parameter to given URL.
  123 
  124         Some BMCs seem to validate boot image URL requiring the URL to end
  125         with something resembling ISO image file name.
  126 
  127         This function tries to add, hopefully, meaningless 'filename'
  128         parameter to URL's query string in hope to make the entire boot image
  129         URL looking more convincing to the BMC.
  130 
  131         However, `url` with fragments might not get cured by this hack.
  132 
  133         :param url: a URL to work on
  134         :param filename: name of the file to append to the URL
  135         :returns: original URL with 'filename' parameter appended
  136         """
  137         parsed_url = urlparse.urlparse(url)
  138         parsed_qs = urlparse.parse_qsl(parsed_url.query)
  139 
  140         has_filename = [x for x in parsed_qs if x[0].lower() == 'filename']
  141         if has_filename:
  142             return url
  143 
  144         parsed_qs.append(('filename', filename))
  145         parsed_url = list(parsed_url)
  146         parsed_url[4] = urlparse.urlencode(parsed_qs)
  147 
  148         return urlparse.urlunparse(parsed_url)
  149 
  150     def publish_image(self, image_file, object_name):
  151         """Make image file downloadable.
  152 
  153         Depending on ironic settings, pushes given file into Swift or copies
  154         it over to local HTTP server's document root and returns publicly
  155         accessible URL leading to the given file.
  156 
  157         :param image_file: path to file to publish
  158         :param object_name: name of the published file
  159         :return: a URL to download published file
  160         """
  161 
  162         if self._is_swift_enabled():
  163             container = self._container
  164             timeout = self._timeout
  165 
  166             object_headers = {'X-Delete-After': str(timeout)}
  167 
  168             swift_api = swift.SwiftAPI()
  169 
  170             swift_api.create_object(container, object_name, image_file,
  171                                     object_headers=object_headers)
  172 
  173             image_url = swift_api.get_temp_url(container, object_name, timeout)
  174 
  175         else:
  176             public_dir = os.path.join(CONF.deploy.http_root,
  177                                       self._image_subdir)
  178 
  179             if not os.path.exists(public_dir):
  180                 os.mkdir(public_dir, 0o755)
  181 
  182             published_file = os.path.join(public_dir, object_name)
  183 
  184             try:
  185                 os.link(image_file, published_file)
  186                 os.chmod(image_file, self._file_permission)
  187 
  188             except OSError as exc:
  189                 LOG.debug(
  190                     "Could not hardlink image file %(image)s to public "
  191                     "location %(public)s (will copy it over): "
  192                     "%(error)s", {'image': image_file,
  193                                   'public': published_file,
  194                                   'error': exc})
  195 
  196                 shutil.copyfile(image_file, published_file)
  197                 os.chmod(published_file, self._file_permission)
  198 
  199             image_url = os.path.join(
  200                 CONF.deploy.http_url, self._image_subdir, object_name)
  201 
  202         image_url = self._append_filename_param(
  203             image_url, os.path.basename(image_file))
  204 
  205         return image_url
  206 
  207 
  208 def _get_floppy_image_name(node):
  209     """Returns the floppy image name for a given node.
  210 
  211     :param node: the node for which image name is to be provided.
  212     """
  213     return "image-%s" % node.uuid
  214 
  215 
  216 def _get_iso_image_name(node):
  217     """Returns the boot iso image name for a given node.
  218 
  219     :param node: the node for which image name is to be provided.
  220     """
  221     return "boot-%s.iso" % node.uuid
  222 
  223 
  224 def cleanup_iso_image(task):
  225     """Deletes the ISO if it was created for the instance.
  226 
  227     :param task: A task from TaskManager.
  228     """
  229     iso_object_name = _get_iso_image_name(task.node)
  230     img_handler = ImageHandler(task.node.driver)
  231 
  232     img_handler.unpublish_image(iso_object_name)
  233 
  234 
  235 def prepare_floppy_image(task, params=None):
  236     """Prepares the floppy image for passing the parameters.
  237 
  238     This method prepares a temporary VFAT filesystem image and adds
  239     a file into the image which contains parameters to be passed to
  240     the ramdisk. Then this method uploads built image to Swift
  241     '[redfish]swift_container', setting it to auto expire after
  242     '[redfish]swift_object_expiry_timeout' seconds. Finally, a
  243     temporary Swift URL is returned addressing Swift object just
  244     created.
  245 
  246     :param task: a TaskManager instance containing the node to act on.
  247     :param params: a dictionary containing 'parameter name'->'value'
  248         mapping to be passed to deploy or rescue image via floppy image.
  249     :raises: ImageCreationFailed, if it failed while creating the floppy
  250         image.
  251     :raises: SwiftOperationError, if any operation with Swift fails.
  252     :returns: image URL for the floppy image.
  253     """
  254     object_name = _get_floppy_image_name(task.node)
  255 
  256     LOG.debug("Trying to create floppy image for node "
  257               "%(node)s", {'node': task.node.uuid})
  258 
  259     with tempfile.NamedTemporaryFile(
  260             dir=CONF.tempdir, suffix='.img') as vfat_image_tmpfile_obj:
  261 
  262         vfat_image_tmpfile = vfat_image_tmpfile_obj.name
  263         images.create_vfat_image(vfat_image_tmpfile, parameters=params)
  264 
  265         img_handler = ImageHandler(task.node.driver)
  266 
  267         image_url = img_handler.publish_image(vfat_image_tmpfile, object_name)
  268 
  269     LOG.debug("Created floppy image %(name)s in Swift for node %(node)s, "
  270               "exposed as temporary URL "
  271               "%(url)s", {'node': task.node.uuid,
  272                           'name': object_name,
  273                           'url': image_url})
  274 
  275     return image_url
  276 
  277 
  278 def cleanup_floppy_image(task):
  279     """Deletes the floppy image if it was created for the node.
  280 
  281     :param task: an ironic node object.
  282     """
  283     floppy_object_name = _get_floppy_image_name(task.node)
  284 
  285     img_handler = ImageHandler(task.node.driver)
  286     img_handler.unpublish_image(floppy_object_name)
  287 
  288 
  289 def _prepare_iso_image(task, kernel_href, ramdisk_href,
  290                        bootloader_href=None, root_uuid=None, params=None,
  291                        base_iso=None, inject_files=None):
  292     """Prepare an ISO to boot the node.
  293 
  294     Build bootable ISO out of `kernel_href` and `ramdisk_href` (and
  295     `bootloader` if it's UEFI boot), then push built image up to Swift and
  296     return a temporary URL.
  297 
  298     :param task: a TaskManager instance containing the node to act on.
  299     :param kernel_href: URL or Glance UUID of the kernel to use
  300     :param ramdisk_href: URL or Glance UUID of the ramdisk to use
  301     :param bootloader_href: URL or Glance UUID of the EFI bootloader
  302          image to use when creating UEFI bootbable ISO
  303     :param root_uuid: optional uuid of the root partition.
  304     :param params: a dictionary containing 'parameter name'->'value'
  305         mapping to be passed to kernel command line.
  306     :param inject_files: Mapping of local source file paths to their location
  307         on the final ISO image.
  308     :returns: bootable ISO HTTP URL.
  309     :raises: MissingParameterValue, if any of the required parameters are
  310         missing.
  311     :raises: InvalidParameterValue, if any of the parameters have invalid
  312         value.
  313     :raises: ImageCreationFailed, if creating ISO image failed.
  314     """
  315     if (not kernel_href or not ramdisk_href) and not base_iso:
  316         raise exception.InvalidParameterValue(_(
  317             "Unable to find kernel, ramdisk for "
  318             "building ISO, or explicit ISO for %(node)s") %
  319             {'node': task.node.uuid})
  320 
  321     img_handler = ImageHandler(task.node.driver)
  322     k_param = img_handler.kernel_params
  323 
  324     i_info = task.node.instance_info
  325 
  326     # NOTE(TheJulia): Until we support modifying a base iso, most of
  327     # this logic actually does nothing in the end. But it should!
  328     if deploy_utils.get_boot_option(task.node) == "ramdisk":
  329         if not base_iso:
  330             kernel_params = "root=/dev/ram0 text "
  331             kernel_params += i_info.get("ramdisk_kernel_arguments", "")
  332         else:
  333             kernel_params = None
  334 
  335     else:
  336         kernel_params = i_info.get('kernel_append_params', k_param)
  337 
  338     if params and not base_iso:
  339         kernel_params = ' '.join(
  340             (kernel_params, ' '.join(
  341                 '%s=%s' % kv for kv in params.items())))
  342 
  343     boot_mode = boot_mode_utils.get_boot_mode(task.node)
  344 
  345     LOG.debug("Trying to create %(boot_mode)s ISO image for node %(node)s "
  346               "with kernel %(kernel_href)s, ramdisk %(ramdisk_href)s, "
  347               "bootloader %(bootloader_href)s and kernel params %(params)s"
  348               "", {'node': task.node.uuid,
  349                    'boot_mode': boot_mode,
  350                    'kernel_href': kernel_href,
  351                    'ramdisk_href': ramdisk_href,
  352                    'bootloader_href': bootloader_href,
  353                    'params': kernel_params})
  354 
  355     with tempfile.NamedTemporaryFile(
  356             dir=CONF.tempdir, suffix='.iso') as boot_fileobj:
  357 
  358         boot_iso_tmp_file = boot_fileobj.name
  359         images.create_boot_iso(
  360             task.context, boot_iso_tmp_file,
  361             kernel_href, ramdisk_href,
  362             esp_image_href=bootloader_href,
  363             root_uuid=root_uuid,
  364             kernel_params=kernel_params,
  365             boot_mode=boot_mode,
  366             base_iso=base_iso,
  367             inject_files=inject_files)
  368 
  369         iso_object_name = _get_iso_image_name(task.node)
  370 
  371         image_url = img_handler.publish_image(
  372             boot_iso_tmp_file, iso_object_name)
  373 
  374     LOG.debug("Created ISO %(name)s in object store for node %(node)s, "
  375               "exposed as temporary URL "
  376               "%(url)s", {'node': task.node.uuid,
  377                           'name': iso_object_name,
  378                           'url': image_url})
  379 
  380     return image_url
  381 
  382 
  383 def _find_param(param_str, param_dict):
  384     val = None
  385     for param_key in param_dict:
  386         if param_str in param_key:
  387             val = param_dict.get(param_key)
  388     return val
  389 
  390 
  391 def prepare_deploy_iso(task, params, mode, d_info):
  392     """Prepare deploy or rescue ISO image
  393 
  394     Build bootable ISO out of
  395     `[driver_info]/deploy_kernel`/`[driver_info]/deploy_ramdisk` or
  396     `[driver_info]/rescue_kernel`/`[driver_info]/rescue_ramdisk`
  397     and `[driver_info]/bootloader`, then push built image up to Glance
  398     and return temporary Swift URL to the image.
  399 
  400     If network interface supplies network configuration (`network_data`),
  401     a `network_data.json` will be written into an appropriate location on
  402     the final ISO.
  403 
  404     :param task: a TaskManager instance containing the node to act on.
  405     :param params: a dictionary containing 'parameter name'->'value'
  406         mapping to be passed to kernel command line.
  407     :param mode: either 'deploy' or 'rescue'.
  408     :param d_info: Deployment information of the node
  409     :returns: bootable ISO HTTP URL.
  410     :raises: MissingParameterValue, if any of the required parameters are
  411         missing.
  412     :raises: InvalidParameterValue, if any of the parameters have invalid
  413         value.
  414     :raises: ImageCreationFailed, if creating ISO image failed.
  415     """
  416 
  417     kernel_str = '%s_kernel' % mode
  418     ramdisk_str = '%s_ramdisk' % mode
  419     bootloader_str = 'bootloader'
  420 
  421     kernel_href = _find_param(kernel_str, d_info)
  422     ramdisk_href = _find_param(ramdisk_str, d_info)
  423     bootloader_href = _find_param(bootloader_str, d_info)
  424 
  425     # TODO(TheJulia): At some point we should support something like
  426     # boot_iso for the deploy interface, perhaps when we support config
  427     # injection.
  428     prepare_iso_image = functools.partial(
  429         _prepare_iso_image, task, kernel_href, ramdisk_href,
  430         bootloader_href=bootloader_href, params=params)
  431 
  432     inject_files = {}
  433     network_data = task.driver.network.get_node_network_data(task)
  434     if network_data:
  435         LOG.debug('Injecting custom network data for node %s',
  436                   task.node.uuid)
  437         network_data = json.dumps(network_data, indent=2).encode('utf-8')
  438         inject_files[network_data] = (
  439             'openstack/latest/network_data.json'
  440         )
  441 
  442     return prepare_iso_image(inject_files=inject_files)
  443 
  444 
  445 def prepare_boot_iso(task, d_info, root_uuid=None):
  446     """Prepare boot ISO image
  447 
  448     Build bootable ISO out of `[instance_info]/kernel`,
  449     `[instance_info]/ramdisk` and `[driver_info]/bootloader` if present.
  450     Otherwise, read `kernel_id` and `ramdisk_id` from
  451     `[instance_info]/image_source` Glance image metadata.
  452 
  453     Push produced ISO image up to Glance and return temporary Swift
  454     URL to the image.
  455 
  456     :param task: a TaskManager instance containing the node to act on.
  457     :param d_info: Deployment information of the node
  458     :param root_uuid: Root UUID
  459     :returns: bootable ISO HTTP URL.
  460     :raises: MissingParameterValue, if any of the required parameters are
  461         missing.
  462     :raises: InvalidParameterValue, if any of the parameters have invalid
  463         value.
  464     :raises: ImageCreationFailed, if creating ISO image failed.
  465     """
  466     node = task.node
  467 
  468     kernel_href = node.instance_info.get('kernel')
  469     ramdisk_href = node.instance_info.get('ramdisk')
  470     base_iso = node.instance_info.get('boot_iso')
  471 
  472     if (not kernel_href or not ramdisk_href) and not base_iso:
  473 
  474         image_href = d_info['image_source']
  475 
  476         image_properties = (
  477             images.get_image_properties(
  478                 task.context, image_href, ['kernel_id', 'ramdisk_id']))
  479 
  480         if not kernel_href:
  481             kernel_href = image_properties.get('kernel_id')
  482 
  483         if not ramdisk_href:
  484             ramdisk_href = image_properties.get('ramdisk_id')
  485 
  486         if (not kernel_href or not ramdisk_href):
  487             raise exception.InvalidParameterValue(_(
  488                 "Unable to find kernel or ramdisk for "
  489                 "to generate boot ISO for %(node)s") %
  490                 {'node': task.node.uuid})
  491 
  492     bootloader_str = 'bootloader'
  493     bootloader_href = _find_param(bootloader_str, d_info)
  494 
  495     return _prepare_iso_image(
  496         task, kernel_href, ramdisk_href, bootloader_href,
  497         root_uuid=root_uuid, base_iso=base_iso)