"Fossies" - the Fresh Open Source Software Archive

Member "ironic-16.0.3/ironic/drivers/modules/redfish/boot.py" (18 Jan 2021, 22689 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 "boot.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 from oslo_log import log
   17 from oslo_utils import importutils
   18 
   19 from ironic.common import boot_devices
   20 from ironic.common import exception
   21 from ironic.common.glance_service import service_utils
   22 from ironic.common.i18n import _
   23 from ironic.common import states
   24 from ironic.conductor import utils as manager_utils
   25 from ironic.conf import CONF
   26 from ironic.drivers import base
   27 from ironic.drivers.modules import boot_mode_utils
   28 from ironic.drivers.modules import deploy_utils
   29 from ironic.drivers.modules import image_utils
   30 from ironic.drivers.modules.redfish import utils as redfish_utils
   31 
   32 LOG = log.getLogger(__name__)
   33 
   34 REQUIRED_PROPERTIES = {
   35     'deploy_kernel': _("URL or Glance UUID of the deployment kernel. "
   36                        "Required."),
   37     'deploy_ramdisk': _("URL or Glance UUID of the ramdisk that is "
   38                         "mounted at boot time. Required.")
   39 }
   40 
   41 OPTIONAL_PROPERTIES = {
   42     'config_via_floppy': _("Boolean value to indicate whether or not the "
   43                            "driver should use virtual media Floppy device "
   44                            "for passing configuration information to the "
   45                            "ramdisk. Defaults to False. Optional."),
   46     'kernel_append_params': _("Additional kernel parameters to pass down to "
   47                               "instance kernel. These parameters can be "
   48                               "consumed by the kernel or by the applications "
   49                               "by reading /proc/cmdline. Mind severe cmdline "
   50                               "size limit. Overrides "
   51                               "[redfish]/kernel_append_params ironic "
   52                               "option."),
   53     'bootloader': _("URL or Glance UUID  of the EFI system partition "
   54                     "image containing EFI boot loader. This image will be "
   55                     "used by ironic when building UEFI-bootable ISO "
   56                     "out of kernel and ramdisk. Required for UEFI "
   57                     "boot from partition images."),
   58 
   59 }
   60 
   61 RESCUE_PROPERTIES = {
   62     'rescue_kernel': _('URL or Glance UUID of the rescue kernel. This value '
   63                        'is required for rescue mode.'),
   64     'rescue_ramdisk': _('URL or Glance UUID of the rescue ramdisk with agent '
   65                         'that is used at node rescue time. This value is '
   66                         'required for rescue mode.'),
   67 }
   68 
   69 COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
   70 COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
   71 COMMON_PROPERTIES.update(RESCUE_PROPERTIES)
   72 
   73 KERNEL_RAMDISK_LABELS = {
   74     'deploy': REQUIRED_PROPERTIES,
   75     'rescue': RESCUE_PROPERTIES
   76 }
   77 
   78 IMAGE_SUBDIR = 'redfish'
   79 
   80 sushy = importutils.try_import('sushy')
   81 
   82 
   83 def _parse_driver_info(node):
   84     """Gets the driver specific Node deployment info.
   85 
   86     This method validates whether the 'driver_info' property of the
   87     supplied node contains the required or optional information properly
   88     for this driver to deploy images to the node.
   89 
   90     :param node: a target node of the deployment
   91     :returns: the driver_info values of the node.
   92     :raises: MissingParameterValue, if any of the required parameters are
   93         missing.
   94     :raises: InvalidParameterValue, if any of the parameters have invalid
   95         value.
   96     """
   97     d_info = node.driver_info
   98 
   99     mode = deploy_utils.rescue_or_deploy_mode(node)
  100     params_to_check = KERNEL_RAMDISK_LABELS[mode]
  101 
  102     deploy_info = {option: d_info.get(option)
  103                    for option in params_to_check}
  104 
  105     if not any(deploy_info.values()):
  106         # NOTE(dtantsur): avoid situation when e.g. deploy_kernel comes
  107         # from driver_info but deploy_ramdisk comes from configuration,
  108         # since it's a sign of a potential operator's mistake.
  109         deploy_info = {k: getattr(CONF.conductor, k)
  110                        for k in params_to_check}
  111 
  112     error_msg = _("Error validating Redfish virtual media. Some "
  113                   "parameters were missing in node's driver_info")
  114 
  115     deploy_utils.check_for_missing_params(deploy_info, error_msg)
  116 
  117     deploy_info.update(
  118         {option: d_info.get(option, getattr(CONF.conductor, option, None))
  119          for option in OPTIONAL_PROPERTIES})
  120 
  121     deploy_info.update(redfish_utils.parse_driver_info(node))
  122 
  123     return deploy_info
  124 
  125 
  126 def _parse_instance_info(node):
  127     """Gets the instance specific Node deployment info.
  128 
  129     This method validates whether the 'instance_info' property of the
  130     supplied node contains the required or optional information properly
  131     for this driver to deploy images to the node.
  132 
  133     :param node: a target node of the deployment
  134     :returns:  the instance_info values of the node.
  135     :raises: InvalidParameterValue, if any of the parameters have invalid
  136         value.
  137     """
  138     deploy_info = node.instance_info.copy()
  139 
  140     # NOTE(etingof): this method is currently no-op, here for completeness
  141     return deploy_info
  142 
  143 
  144 def _insert_vmedia(task, boot_url, boot_device):
  145     """Insert bootable ISO image into virtual CD or DVD
  146 
  147     :param task: A task from TaskManager.
  148     :param boot_url: URL to a bootable ISO image
  149     :param boot_device: sushy boot device e.g. `VIRTUAL_MEDIA_CD`,
  150         `VIRTUAL_MEDIA_DVD` or `VIRTUAL_MEDIA_FLOPPY`
  151     :raises: InvalidParameterValue, if no suitable virtual CD or DVD is
  152         found on the node.
  153     """
  154     system = redfish_utils.get_system(task.node)
  155 
  156     for manager in system.managers:
  157         for v_media in manager.virtual_media.get_members():
  158             if boot_device not in v_media.media_types:
  159                 continue
  160 
  161             if v_media.inserted:
  162                 if v_media.image == boot_url:
  163                     LOG.debug("Boot media %(boot_url)s is already "
  164                               "inserted into %(boot_device)s for node "
  165                               "%(node)s", {'node': task.node.uuid,
  166                                            'boot_url': boot_url,
  167                                            'boot_device': boot_device})
  168                     return
  169 
  170                 continue
  171 
  172             v_media.insert_media(boot_url, inserted=True,
  173                                  write_protected=True)
  174 
  175             LOG.info("Inserted boot media %(boot_url)s into "
  176                      "%(boot_device)s for node "
  177                      "%(node)s", {'node': task.node.uuid,
  178                                   'boot_url': boot_url,
  179                                   'boot_device': boot_device})
  180             return
  181 
  182     raise exception.InvalidParameterValue(
  183         _('No suitable virtual media device found'))
  184 
  185 
  186 def _eject_vmedia(task, boot_device=None):
  187     """Eject virtual CDs and DVDs
  188 
  189     :param task: A task from TaskManager.
  190     :param boot_device: sushy boot device e.g. `VIRTUAL_MEDIA_CD`,
  191         `VIRTUAL_MEDIA_DVD` or `VIRTUAL_MEDIA_FLOPPY` or `None` to
  192         eject everything (default).
  193     :raises: InvalidParameterValue, if no suitable virtual CD or DVD is
  194         found on the node.
  195     """
  196     system = redfish_utils.get_system(task.node)
  197 
  198     for manager in system.managers:
  199         for v_media in manager.virtual_media.get_members():
  200             if boot_device and boot_device not in v_media.media_types:
  201                 continue
  202 
  203             inserted = v_media.inserted
  204 
  205             if inserted:
  206                 v_media.eject_media()
  207 
  208             LOG.info("Boot media is%(already)s ejected from "
  209                      "%(boot_device)s for node %(node)s"
  210                      "", {'node': task.node.uuid,
  211                           'already': '' if inserted else ' already',
  212                           'boot_device': v_media.name})
  213 
  214 
  215 def _has_vmedia_device(task, boot_device):
  216     """Indicate if device exists at any of the managers
  217 
  218     :param task: A task from TaskManager.
  219     :param boot_device: sushy boot device e.g. `VIRTUAL_MEDIA_CD`,
  220         `VIRTUAL_MEDIA_DVD` or `VIRTUAL_MEDIA_FLOPPY`.
  221     """
  222     system = redfish_utils.get_system(task.node)
  223 
  224     for manager in system.managers:
  225         for v_media in manager.virtual_media.get_members():
  226             if boot_device in v_media.media_types:
  227                 return True
  228 
  229 
  230 def _parse_deploy_info(node):
  231     """Gets the instance and driver specific Node deployment info.
  232 
  233     This method validates whether the 'instance_info' and 'driver_info'
  234     property of the supplied node contains the required information for
  235     this driver to deploy images to the node.
  236 
  237     :param node: a target node of the deployment
  238     :returns: a dict with the instance_info and driver_info values.
  239     :raises: MissingParameterValue, if any of the required parameters are
  240         missing.
  241     :raises: InvalidParameterValue, if any of the parameters have invalid
  242         value.
  243     """
  244     deploy_info = {}
  245     deploy_info.update(deploy_utils.get_image_instance_info(node))
  246     deploy_info.update(_parse_driver_info(node))
  247     deploy_info.update(_parse_instance_info(node))
  248 
  249     return deploy_info
  250 
  251 
  252 class RedfishVirtualMediaBoot(base.BootInterface):
  253     """Virtual media boot interface over Redfish.
  254 
  255     Virtual Media allows booting the system from the "virtual"
  256     CD/DVD drive containing the user image that BMC "inserts"
  257     into the drive.
  258 
  259     The CD/DVD images must be in ISO format and (depending on
  260     BMC implementation) could be pulled over HTTP, served as
  261     iSCSI targets or NFS volumes.
  262 
  263     The baseline boot workflow looks like this:
  264 
  265     1. Pull kernel, ramdisk and ESP (FAT partition image with EFI boot
  266        loader) images (ESP is only needed for UEFI boot)
  267     2. Create bootable ISO out of images (#1), push it to Glance and
  268        pass to the BMC as Swift temporary URL
  269     3. Optionally create floppy image with desired system configuration data,
  270        push it to Glance and pass to the BMC as Swift temporary URL
  271     4. Insert CD/DVD and (optionally) floppy images and set proper boot mode
  272 
  273     For building deploy or rescue ISO, redfish boot interface uses
  274     `deploy_kernel`/`deploy_ramdisk` or `rescue_kernel`/`rescue_ramdisk`
  275     properties from `[instance_info]` or `[driver_info]`.
  276 
  277     For building boot (user) ISO, redfish boot interface seeks `kernel_id`
  278     and `ramdisk_id` properties in the Glance image metadata found in
  279     `[instance_info]image_source` node property.
  280     """
  281 
  282     capabilities = ['iscsi_volume_boot', 'ramdisk_boot']
  283 
  284     def __init__(self):
  285         """Initialize the Redfish virtual media boot interface.
  286 
  287         :raises: DriverLoadError if the driver can't be loaded due to
  288             missing dependencies
  289         """
  290         super(RedfishVirtualMediaBoot, self).__init__()
  291         if not sushy:
  292             raise exception.DriverLoadError(
  293                 driver='redfish',
  294                 reason=_('Unable to import the sushy library'))
  295 
  296     def get_properties(self):
  297         """Return the properties of the interface.
  298 
  299         :returns: dictionary of <property name>:<property description> entries.
  300         """
  301         return REQUIRED_PROPERTIES
  302 
  303     def _validate_driver_info(self, task):
  304         """Validate the prerequisites for virtual media based boot.
  305 
  306         This method validates whether the 'driver_info' property of the
  307         supplied node contains the required information for this driver.
  308 
  309         :param task: a TaskManager instance containing the node to act on.
  310         :raises: InvalidParameterValue if any parameters are incorrect
  311         :raises: MissingParameterValue if some mandatory information
  312             is missing on the node
  313         """
  314         node = task.node
  315 
  316         _parse_driver_info(node)
  317 
  318     def _validate_instance_info(self, task):
  319         """Validate instance image information for the task's node.
  320 
  321         This method validates whether the 'instance_info' property of the
  322         supplied node contains the required information for this driver.
  323 
  324         :param task: a TaskManager instance containing the node to act on.
  325         :raises: InvalidParameterValue if any parameters are incorrect
  326         :raises: MissingParameterValue if some mandatory information
  327             is missing on the node
  328         """
  329         node = task.node
  330 
  331         d_info = _parse_deploy_info(node)
  332 
  333         if node.driver_internal_info.get('is_whole_disk_image'):
  334             props = []
  335         elif d_info.get('boot_iso'):
  336             props = ['boot_iso']
  337         elif service_utils.is_glance_image(d_info['image_source']):
  338             props = ['kernel_id', 'ramdisk_id']
  339 
  340         else:
  341             props = ['kernel', 'ramdisk']
  342 
  343         deploy_utils.validate_image_properties(task.context, d_info, props)
  344 
  345     def validate(self, task):
  346         """Validate the deployment information for the task's node.
  347 
  348         This method validates whether the 'driver_info' and/or 'instance_info'
  349         properties of the task's node contains the required information for
  350         this interface to function.
  351 
  352         :param task: A TaskManager instance containing the node to act on.
  353         :raises: InvalidParameterValue on malformed parameter(s)
  354         :raises: MissingParameterValue on missing parameter(s)
  355         """
  356         self._validate_driver_info(task)
  357 
  358         if task.driver.storage.should_write_image(task):
  359             self._validate_instance_info(task)
  360 
  361     def validate_inspection(self, task):
  362         """Validate that the node has required properties for inspection.
  363 
  364         :param task: A TaskManager instance with the node being checked
  365         :raises: MissingParameterValue if node is missing one or more required
  366             parameters
  367         :raises: UnsupportedDriverExtension
  368         """
  369         try:
  370             self._validate_driver_info(task)
  371         except exception.MissingParameterValue:
  372             # Fall back to non-managed in-band inspection
  373             raise exception.UnsupportedDriverExtension(
  374                 driver=task.node.driver, extension='inspection')
  375 
  376     def prepare_ramdisk(self, task, ramdisk_params):
  377         """Prepares the boot of deploy or rescue ramdisk over virtual media.
  378 
  379         This method prepares the boot of the deploy or rescue ramdisk after
  380         reading relevant information from the node's driver_info and
  381         instance_info.
  382 
  383         :param task: A task from TaskManager.
  384         :param ramdisk_params: the parameters to be passed to the ramdisk.
  385         :returns: None
  386         :raises: MissingParameterValue, if some information is missing in
  387             node's driver_info or instance_info.
  388         :raises: InvalidParameterValue, if some information provided is
  389             invalid.
  390         :raises: IronicException, if some power or set boot boot device
  391             operation failed on the node.
  392         """
  393         node = task.node
  394         # NOTE(TheJulia): If this method is being called by something
  395         # aside from deployment, clean and rescue, such as conductor takeover,
  396         # we should treat this as a no-op and move on otherwise we would
  397         # modify the state of the node due to virtual media operations.
  398         if node.provision_state not in (states.DEPLOYING,
  399                                         states.CLEANING,
  400                                         states.RESCUING,
  401                                         states.INSPECTING):
  402             return
  403 
  404         # NOTE(TheJulia): Since we're deploying, cleaning, or rescuing,
  405         # with virtual media boot, we should generate a token!
  406         manager_utils.add_secret_token(node, pregenerated=True)
  407         node.save()
  408         ramdisk_params['ipa-agent-token'] = \
  409             node.driver_internal_info['agent_secret_token']
  410 
  411         manager_utils.node_power_action(task, states.POWER_OFF)
  412 
  413         d_info = _parse_driver_info(node)
  414 
  415         config_via_floppy = d_info.get('config_via_floppy')
  416 
  417         deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task)
  418         if deploy_nic_mac is not None:
  419             ramdisk_params['BOOTIF'] = deploy_nic_mac
  420         if CONF.debug and 'ipa-debug' not in ramdisk_params:
  421             ramdisk_params['ipa-debug'] = '1'
  422 
  423         if config_via_floppy:
  424 
  425             if _has_vmedia_device(task, sushy.VIRTUAL_MEDIA_FLOPPY):
  426                 # NOTE (etingof): IPA will read the diskette only if
  427                 # we tell it to
  428                 ramdisk_params['boot_method'] = 'vmedia'
  429 
  430                 floppy_ref = image_utils.prepare_floppy_image(
  431                     task, params=ramdisk_params)
  432 
  433                 _eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
  434                 _insert_vmedia(
  435                     task, floppy_ref, sushy.VIRTUAL_MEDIA_FLOPPY)
  436 
  437                 LOG.debug('Inserted virtual floppy with configuration for '
  438                           'node %(node)s', {'node': task.node.uuid})
  439 
  440             else:
  441                 LOG.warning('Config via floppy is requested, but '
  442                             'Floppy drive is not available on node '
  443                             '%(node)s', {'node': task.node.uuid})
  444 
  445         mode = deploy_utils.rescue_or_deploy_mode(node)
  446 
  447         iso_ref = image_utils.prepare_deploy_iso(task, ramdisk_params,
  448                                                  mode, d_info)
  449 
  450         _eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
  451         _insert_vmedia(task, iso_ref, sushy.VIRTUAL_MEDIA_CD)
  452 
  453         boot_mode_utils.sync_boot_mode(task)
  454 
  455         self._set_boot_device(task, boot_devices.CDROM)
  456 
  457         LOG.debug("Node %(node)s is set to one time boot from "
  458                   "%(device)s", {'node': task.node.uuid,
  459                                  'device': boot_devices.CDROM})
  460 
  461     def clean_up_ramdisk(self, task):
  462         """Cleans up the boot of ironic ramdisk.
  463 
  464         This method cleans up the environment that was setup for booting the
  465         deploy ramdisk.
  466 
  467         :param task: A task from TaskManager.
  468         :returns: None
  469         """
  470         d_info = _parse_driver_info(task.node)
  471 
  472         config_via_floppy = d_info.get('config_via_floppy')
  473 
  474         LOG.debug("Cleaning up deploy boot for "
  475                   "%(node)s", {'node': task.node.uuid})
  476 
  477         _eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
  478         image_utils.cleanup_iso_image(task)
  479 
  480         if (config_via_floppy
  481                 and _has_vmedia_device(task, sushy.VIRTUAL_MEDIA_FLOPPY)):
  482             _eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
  483 
  484             image_utils.cleanup_floppy_image(task)
  485 
  486     def prepare_instance(self, task):
  487         """Prepares the boot of instance over virtual media.
  488 
  489         This method prepares the boot of the instance after reading
  490         relevant information from the node's instance_info.
  491 
  492         The internal logic is as follows:
  493 
  494         - If `boot_option` requested for this deploy is 'local', then set the
  495           node to boot from disk.
  496         - Unless `boot_option` requested for this deploy is 'ramdisk', pass
  497           root disk/partition ID to virtual media boot image
  498         - Otherwise build boot image, insert it into virtual media device
  499           and set node to boot from CD.
  500 
  501         :param task: a task from TaskManager.
  502         :returns: None
  503         :raises: InstanceDeployFailure, if its try to boot iSCSI volume in
  504                  'BIOS' boot mode.
  505         """
  506         node = task.node
  507 
  508         boot_mode_utils.sync_boot_mode(task)
  509 
  510         boot_option = deploy_utils.get_boot_option(node)
  511         self.clean_up_instance(task)
  512         iwdi = node.driver_internal_info.get('is_whole_disk_image')
  513         if boot_option == "local" or iwdi:
  514             self._set_boot_device(task, boot_devices.DISK, persistent=True)
  515 
  516             LOG.debug("Node %(node)s is set to permanently boot from local "
  517                       "%(device)s", {'node': task.node.uuid,
  518                                      'device': boot_devices.DISK})
  519             return
  520 
  521         params = {}
  522 
  523         if boot_option != 'ramdisk':
  524             root_uuid = node.driver_internal_info.get('root_uuid_or_disk_id')
  525             if not root_uuid and task.driver.storage.should_write_image(task):
  526                 LOG.warning(
  527                     "The UUID of the root partition could not be found for "
  528                     "node %s. Booting instance from disk anyway.", node.uuid)
  529 
  530                 self._set_boot_device(task, boot_devices.DISK, persistent=True)
  531 
  532                 return
  533 
  534             params.update(root_uuid=root_uuid)
  535 
  536         deploy_info = _parse_deploy_info(node)
  537         iso_ref = image_utils.prepare_boot_iso(task, deploy_info, **params)
  538         _eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
  539         _insert_vmedia(task, iso_ref, sushy.VIRTUAL_MEDIA_CD)
  540 
  541         self._set_boot_device(task, boot_devices.CDROM, persistent=True)
  542 
  543         LOG.debug("Node %(node)s is set to permanently boot from "
  544                   "%(device)s", {'node': task.node.uuid,
  545                                  'device': boot_devices.CDROM})
  546 
  547     def clean_up_instance(self, task):
  548         """Cleans up the boot of instance.
  549 
  550         This method cleans up the environment that was setup for booting
  551         the instance.
  552 
  553         :param task: A task from TaskManager.
  554         :returns: None
  555         """
  556         LOG.debug("Cleaning up instance boot for "
  557                   "%(node)s", {'node': task.node.uuid})
  558 
  559         _eject_vmedia(task, sushy.VIRTUAL_MEDIA_CD)
  560         d_info = task.node.driver_info
  561         config_via_floppy = d_info.get('config_via_floppy')
  562         if config_via_floppy:
  563             _eject_vmedia(task, sushy.VIRTUAL_MEDIA_FLOPPY)
  564 
  565         image_utils.cleanup_iso_image(task)
  566 
  567     @classmethod
  568     def _set_boot_device(cls, task, device, persistent=False):
  569         """Set the boot device for a node.
  570 
  571         This is a hook to allow other boot interfaces, inheriting from standard
  572         `redfish` boot interface, implement their own weird ways of setting
  573         boot device.
  574 
  575         :param task: a TaskManager instance.
  576         :param device: the boot device, one of
  577                        :mod:`ironic.common.boot_devices`.
  578         :param persistent: Whether to set next-boot, or make the change
  579             permanent. Default: False.
  580         :raises: InvalidParameterValue if the validation of the
  581             ManagementInterface fails.
  582         """
  583         manager_utils.node_set_boot_device(task, device, persistent)