diff --git a/doc/source/admin/drivers/ilo.rst b/doc/source/admin/drivers/ilo.rst index 919a5064f9..afe6df7397 100644 --- a/doc/source/admin/drivers/ilo.rst +++ b/doc/source/admin/drivers/ilo.rst @@ -62,6 +62,7 @@ features: * `Out of Band RAID Support`_ * `Out of Band Sanitize Disk Erase Support`_ * `Out of Band One Button Secure Erase Support`_ +* `UEFI-HTTPS Boot support`_ Hardware interfaces ^^^^^^^^^^^^^^^^^^^ @@ -191,7 +192,8 @@ The ``ilo`` hardware type supports following hardware interfaces: The ``ilo5`` hardware type supports all the ``ilo`` interfaces described above, -except for ``raid`` interface. The details of ``raid`` interface is as under: +except for ``boot`` and ``raid`` interfaces. The details of ``boot`` and +``raid`` interfaces is as under: * raid Supports ``ilo5`` and ``no-raid``. The default is ``ilo5``. @@ -204,6 +206,19 @@ except for ``raid`` interface. The details of ``raid`` interface is as under: enabled_hardware_types = ilo5 enabled_raid_interfaces = ilo5,no-raid +* boot + Supports ``ilo-uefi-https`` apart from the other boot interfaces supported + by ``ilo`` hardware type. + This can be enabled by using the ``[DEFAULT]enabled_boot_interfaces`` + option in ``ironic.conf`` as given below: + + .. code-block:: ini + + [DEFAULT] + enabled_hardware_types = ilo5 + enabled_boot_interfaces = ilo-uefi-https,ilo-virtual-media + + The ``ilo`` and ``ilo5`` hardware type support all standard ``deploy`` and ``network`` interface implementations, see :ref:`enable-hardware-interfaces` @@ -290,6 +305,27 @@ Node configuration This is optional property and is used when ``rescue`` interface is set to ``agent``. +* The following properties are also required in node object's + ``driver_info`` if ``ilo-uefi-https`` boot interface is used for ``ilo5`` + hardware type: + + - ``ilo_deploy_kernel``: The glance UUID or a HTTPS URL of the deployment kernel. + - ``ilo_deploy_ramdisk``: The glance UUID or a HTTPS URL of the deployment ramdisk. + - ``ilo_bootloader``: The glance UUID or a HTTPS URL of the bootloader. + - ``ilo_rescue_kernel``: The glance UUID or a HTTPS URL of the rescue kernel. + This is optional property and is used when ``rescue`` interface is set to + ``agent``. + - ``ilo_rescue_ramdisk``: The glance UUID or a HTTP(S) URL of the rescue ramdisk. + This is optional property and is used when ``rescue`` interface is set to + ``agent``. + + .. note:: + ``ilo-uefi-https`` boot interface is supported by only ``ilo5`` hardware + type. If the images are not hosted in glance, the references + must be HTTPS URLs hosted by secure webserver. This boot interface can + be used only when the current boot mode is ``UEFI``. + + * The following parameters are mandatory in ``driver_info`` if ``ilo-inspect`` inspect inteface is used and SNMPv3 inspection (`SNMPv3 Authentication` in `HPE iLO4 User Guide`_) is desired: @@ -438,7 +474,9 @@ the intermediate floppy image and the boot ISO. .. note:: HTTPS is strongly recommended over HTTP web server configuration for security enhancement. The ``ilo-virtual-media`` boot interface will send the instance's - configdrive over an encrypted channel if web server is HTTPS enabled. + configdrive over an encrypted channel if web server is HTTPS enabled. However + for ``ilo-uefi-https`` boot interface HTTPS webserver is mandatory as this + interface only supports HTTPS URLs. Enable driver ============= @@ -2081,6 +2119,45 @@ Below are the steps to perform this clean step: .. note:: Do not perform any iLO 5 configuration changes until this process is completed. +UEFI-HTTPS Boot support +^^^^^^^^^^^^^^^^^^^^^^^ +The UEFI firmware on Gen10 HPE Proliant servers supports booting from secured URLs. +With this capability ``ilo5`` hardware with ``ilo-uefi-https`` boot interface supports +deploy/rescue features in more secured environments. + +If swift is used as glance backend and ironic is configured to use swift to store +temporary images, it is required that swift is configured on HTTPS so that the tempurl +generated is HTTPS URL. + +If the webserver is used for hosting the temporary images, then the webserver is required +to serve requests on HTTPS. + +If the images are hosted on a HTTPS webserver or swift configured with HTTPS with +custom certificates, the user is required to export SSL certificates into iLO. +Refer to `HPE Integrated Lights-Out Security Technology Brief`_ for more information. + +The following command can be used to enroll a ProLiant node with ``ilo5`` hardware type +and ``ilo-uefi-https`` boot interface: + +.. code-block:: console + + openstack baremetal node create \ + --driver ilo5 \ + --boot-interface ilo-uefi-https \ + --deploy-interface direct \ + --raid-interface ilo5 \ + --rescue-interface agent \ + --driver-info ilo_address= \ + --driver-info ilo_username= \ + --driver-info ilo_password= \ + --driver-info ilo_deploy_kernel= \ + --driver-info ilo_deploy_ramdisk= \ + --driver-info ilo_bootloader= + +.. note:: + UEFI secure boot is not supported with ``ilo-uefi-https`` boot interface. + + .. _`ssacli documentation`: https://support.hpe.com/hpsc/doc/public/display?docId=c03909334 .. _`proliant-tools`: https://docs.openstack.org/diskimage-builder/latest/elements/proliant-tools/README.html .. _`HPE iLO4 User Guide`: https://h20566.www2.hpe.com/hpsc/doc/public/display?docId=c03334051 @@ -2093,3 +2170,4 @@ Below are the steps to perform this clean step: .. _`SUM`: https://h17007.www1.hpe.com/us/en/enterprise/servers/products/service_pack/hpsum/index.aspx .. _`SUM User Guide`: https://h20565.www2.hpe.com/hpsc/doc/public/display?docId=c05210448 .. [1] `ironic-python-agent-builder`: https://docs.openstack.org/ironic-python-agent-builder/latest/install/index.html +.. _`HPE Integrated Lights-Out Security Technology Brief`: http://h20564.www2.hpe.com/hpsc/doc/public/display?docId=c04530504 diff --git a/ironic/conf/ilo.py b/ironic/conf/ilo.py index 8e5ca1d899..cb78edea6c 100644 --- a/ironic/conf/ilo.py +++ b/ironic/conf/ilo.py @@ -103,6 +103,14 @@ opts = [ '"auto" for backward compatibility. When "auto" is ' 'specified, default boot mode will be selected based ' 'on boot mode settings on the system.')), + cfg.IntOpt('file_permission', + default=0o644, + help=_('File permission for swift-less image hosting with the ' + 'octal permission representation of file access ' + 'permissions. This setting defaults to ``644``, ' + 'or as the octal number ``0o644`` in Python. ' + 'This setting must be set to the octal number ' + 'representation, meaning starting with ``0o``.')), ] diff --git a/ironic/drivers/ilo.py b/ironic/drivers/ilo.py index 4b824fffcc..10676b4112 100644 --- a/ironic/drivers/ilo.py +++ b/ironic/drivers/ilo.py @@ -77,6 +77,12 @@ class Ilo5Hardware(IloHardware): iLO5 hardware type is targeted for iLO5 based Proliant Gen10 servers. """ + @property + def supported_boot_interfaces(self): + """List of supported boot interfaces.""" + return super(Ilo5Hardware, + self).supported_boot_interfaces + [boot.IloUefiHttpsBoot] + @property def supported_raid_interfaces(self): """List of supported raid interfaces.""" diff --git a/ironic/drivers/modules/ilo/boot.py b/ironic/drivers/modules/ilo/boot.py index b6142bb8be..32ad168281 100644 --- a/ironic/drivers/modules/ilo/boot.py +++ b/ironic/drivers/modules/ilo/boot.py @@ -38,6 +38,7 @@ from ironic.drivers import base from ironic.drivers.modules import boot_mode_utils from ironic.drivers.modules import deploy_utils from ironic.drivers.modules.ilo import common as ilo_common +from ironic.drivers.modules import image_utils from ironic.drivers.modules import ipxe from ironic.drivers.modules import pxe @@ -56,6 +57,29 @@ RESCUE_PROPERTIES = { "required if rescue mode is being used and ironic is " "managing booting the rescue ramdisk.") } +REQUIRED_PROPERTIES_UEFI_HTTPS_BOOT = { + 'ilo_deploy_kernel': _("URL or Glance UUID of the deployment kernel. " + "Required."), + 'ilo_deploy_ramdisk': _("URL or Glance UUID of the ramdisk that is " + "mounted at boot time. Required."), + 'ilo_bootloader': _("URL or Glance UUID of the EFI system partition " + "image containing EFI boot loader. This image will " + "be used by ironic when building UEFI-bootable ISO " + "out of kernel and ramdisk. Required for UEFI " + "boot from partition images.") +} +RESCUE_PROPERTIES_UEFI_HTTPS_BOOT = { + 'ilo_rescue_kernel': _('URL or Glance UUID of the rescue kernel. This ' + 'value is required for rescue mode.'), + 'ilo_rescue_ramdisk': _('URL or Glance UUID of the rescue ramdisk with ' + 'agent that is used at node rescue time. ' + 'The value is required for rescue mode.'), + 'ilo_bootloader': _("URL or Glance UUID of the EFI system partition " + "image containing EFI boot loader. This image will " + "be used by ironic when building UEFI-bootable ISO " + "out of kernel and ramdisk. Required for UEFI " + "boot from partition images.") +} COMMON_PROPERTIES = REQUIRED_PROPERTIES @@ -871,3 +895,363 @@ class IloiPXEBoot(ipxe.iPXEBoot): # Volume boot in BIOS boot mode is handled using # PXE boot interface super(IloiPXEBoot, self).clean_up_instance(task) + + +class IloUefiHttpsBoot(base.BootInterface): + + capabilities = ['ramdisk_boot'] + + def get_properties(self): + """Return the properties of the interface. + + :returns: dictionary of : entries. + """ + return REQUIRED_PROPERTIES_UEFI_HTTPS_BOOT + + def _validate_hrefs(self, image_dict): + """Validates if the given URLs are secured URLs. + + If the given URLs are not glance images then validates if the URLs + are secured. + + :param image_dict: a dictionary containing property/URL pair. + :returns: None + :raises: InvalidParameterValue, if any of URLs provided are insecure. + """ + insecure_props = [] + + for prop in image_dict: + image_ref = image_dict.get(prop) + if not service_utils.is_glance_image(image_ref): + prefix = urlparse.urlparse(image_ref).scheme.lower() + if prefix == 'http': + insecure_props.append(prop) + + if len(insecure_props) > 0: + error = (_('Secure URLs exposed over HTTPS are expected. ' + 'Insecure URLs are provided for %s') % insecure_props) + raise exception.InvalidParameterValue(error) + + def _parse_deploy_info(self, node): + """Gets the instance and driver specific Node deployment info. + + This method validates whether the 'instance_info' and 'driver_info' + property of the supplied node contains the required information for + this driver to deploy images to the node. + + :param node: a target node of the deployment + :returns: a dict with the instance_info and driver_info values. + :raises: MissingParameterValue, if any of the required parameters are + missing. + :raises: InvalidParameterValue, if any of the parameters have invalid + value. + """ + deploy_info = {} + deploy_info.update(deploy_utils.get_image_instance_info(node)) + deploy_info.update(self._parse_driver_info(node)) + + return deploy_info + + def _parse_driver_info(self, node, mode='deploy'): + """Gets the node specific deploy/rescue info. + + This method validates whether the 'driver_info' property of the + supplied node contains the required information for this driver to + deploy images to the node. + + :param node: a single Node. + :param mode: Label indicating a deploy or rescue operation being + carried out on the node. Supported values are 'deploy' and + 'rescue'. Defaults to 'deploy', indicating deploy operation + is being carried out. + :returns: A dict with the driver_info values. + :raises: MissingParameterValue, if any of the required parameters are + missing. + """ + info = node.driver_info + + if mode == 'rescue': + params_to_check = RESCUE_PROPERTIES_UEFI_HTTPS_BOOT.keys() + else: + params_to_check = REQUIRED_PROPERTIES_UEFI_HTTPS_BOOT.keys() + + deploy_info = {option: info.get(option) + for option in params_to_check} + + self._validate_hrefs(deploy_info) + + error_msg = (_("Error validating %s for iLO UEFI HTTPS boot. Some " + "parameters were missing in node's driver_info") % mode) + deploy_utils.check_for_missing_params(deploy_info, error_msg) + + deploy_info.update(ilo_common.parse_driver_info(node)) + + return deploy_info + + def _validate_driver_info(self, task): + """Validates the prerequisites for ilo-uefi-https boot interface. + + This method validates whether the 'driver_info' property of the + supplied node contains the required information for this driver. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue if any parameters are incorrect + :raises: MissingParameterValue if some mandatory information + is missing on the node + """ + node = task.node + + self._parse_driver_info(node) + + def _validate_instance_image_info(self, task): + """Validate instance image information for the task's node. + + :param task: a TaskManager instance containing the node to act on. + :raises: InvalidParameterValue, if some information is invalid. + :raises: MissingParameterValue if 'kernel_id' and 'ramdisk_id' are + missing in the Glance image or 'kernel' and 'ramdisk' not provided + in instance_info for non-Glance image. + """ + node = task.node + + d_info = deploy_utils.get_image_instance_info(node) + + self._validate_hrefs(d_info) + + if node.driver_internal_info.get('is_whole_disk_image'): + props = [] + elif service_utils.is_glance_image(d_info['image_source']): + props = ['kernel_id', 'ramdisk_id'] + else: + props = ['kernel', 'ramdisk'] + deploy_utils.validate_image_properties(task.context, d_info, props) + + @METRICS.timer('IloUefiHttpsBoot.validate') + def validate(self, task): + """Validate the deployment information for the task's node. + + This method validates whether the 'driver_info' and/or 'instance_info' + properties of the task's node contains the required information for + this interface to function. + + :param task: A TaskManager instance containing the node to act on. + :raises: InvalidParameterValue on malformed parameter(s) + :raises: MissingParameterValue on missing parameter(s) + """ + node = task.node + boot_option = deploy_utils.get_boot_option(node) + try: + boot_mode = ilo_common.get_current_boot_mode(task.node) + except exception.IloOperationError: + error = _("Validation for 'ilo-uefi-https' boot interface failed. " + "Could not determine current boot mode for node " + "%(node)s.") % node.uuid + raise exception.InvalidParameterValue(error) + + if boot_mode.lower() != 'uefi': + error = _("Validation for 'ilo-uefi-https' boot interface failed. " + "The node is required to be in 'UEFI' boot mode.") + raise exception.InvalidParameterValue(error) + + boot_iso = node.instance_info.get('ilo_boot_iso') + if (boot_option == "ramdisk" and boot_iso): + if not service_utils.is_glance_image(boot_iso): + try: + image_service.HttpImageService().validate_href(boot_iso) + except exception.ImageRefValidationFailed: + with excutils.save_and_reraise_exception(): + LOG.error("UEFI-HTTPS boot with 'ramdisk' " + "boot_option accepts only Glance images or " + "HTTPS URLs as " + "instance_info['ilo_boot_iso']. Either %s " + "is not a valid HTTPS URL or is not " + "reachable.", boot_iso) + return + + self._validate_driver_info(task) + + if task.driver.storage.should_write_image(task): + self._validate_instance_image_info(task) + + def validate_inspection(self, task): + """Validate that the node has required properties for inspection. + + :param task: A TaskManager instance with the node being checked + :raises: MissingParameterValue if node is missing one or more required + parameters + :raises: UnsupportedDriverExtension + """ + try: + self._validate_driver_info(task) + except exception.MissingParameterValue: + # Fall back to non-managed in-band inspection + raise exception.UnsupportedDriverExtension( + driver=task.node.driver, extension='inspection') + + @METRICS.timer('IloUefiHttpsBoot.prepare_ramdisk') + def prepare_ramdisk(self, task, ramdisk_params): + """Prepares the boot of deploy ramdisk using UEFI-HTTPS boot. + + This method prepares the boot of the deploy or rescue ramdisk after + reading relevant information from the node's driver_info and + instance_info. + + :param task: a task from TaskManager. + :param ramdisk_params: the parameters to be passed to the ramdisk. + :returns: None + :raises: MissingParameterValue, if some information is missing in + node's driver_info or instance_info. + :raises: InvalidParameterValue, if some information provided is + invalid. + :raises: IronicException, if some power or set boot boot device + operation failed on the node. + :raises: IloOperationError, if some operation on iLO failed. + """ + node = task.node + # NOTE(TheJulia): If this method is being called by something + # aside from deployment, clean and rescue, such as conductor takeover, + # we should treat this as a no-op and move on otherwise we would + # modify the state of the node due to virtual media operations. + if node.provision_state not in (states.DEPLOYING, + states.CLEANING, + states.RESCUING, + states.INSPECTING): + return + + prepare_node_for_deploy(task) + + # Clear ilo_boot_iso if it's a glance image to force recreate + # another one again (or use existing one in glance). + # This is mainly for rebuild and rescue scenario. + if service_utils.is_glance_image( + node.instance_info.get('image_source')): + instance_info = node.instance_info + instance_info.pop('ilo_boot_iso', None) + node.instance_info = instance_info + node.save() + + # NOTE(TheJulia): Since we're deploying, cleaning, or rescuing, + # with virtual media boot, we should generate a token! + manager_utils.add_secret_token(node, pregenerated=True) + ramdisk_params['ipa-agent-token'] = \ + task.node.driver_internal_info['agent_secret_token'] + task.node.save() + + deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task) + ramdisk_params['BOOTIF'] = deploy_nic_mac + + mode = 'deploy' + if node.provision_state == states.RESCUING: + mode = 'rescue' + + d_info = self._parse_driver_info(node, mode) + + iso_ref = image_utils.prepare_deploy_iso(task, ramdisk_params, + mode, d_info) + + LOG.debug("Set 'UEFIHTTP' as one time boot option on the node " + "%(node)s to boot from URL %(iso_ref)s.", + {'node': node.uuid, 'iso_ref': iso_ref}) + + ilo_common.setup_uefi_https(task, iso_ref) + + @METRICS.timer('IloUefiHttpsBoot.clean_up_ramdisk') + def clean_up_ramdisk(self, task): + """Cleans up the boot of ironic ramdisk. + + This method cleans up the environment that was setup for booting the + deploy ramdisk. + + :param task: A task from TaskManager. + :returns: None + """ + LOG.debug("Cleaning up deploy boot for " + "%(node)s", {'node': task.node.uuid}) + + image_utils.cleanup_iso_image(task) + + @METRICS.timer('IloUefiHttpsBoot.prepare_instance') + def prepare_instance(self, task): + """Prepares the boot of instance. + + This method prepares the boot of the instance after reading + relevant information from the node's instance_info. + It does the following depending on boot_option for deploy: + + - If the boot_option requested for this deploy is 'local' or image is + a whole disk image, then it sets the node to boot from disk. + - Otherwise it finds/creates the boot ISO, sets the node boot option + to UEFIHTTP and sets the URL as the boot ISO to boot the instance + image. + + :param task: a task from TaskManager. + :returns: None + :raises: IloOperationError, if some operation on iLO failed. + :raises: InstanceDeployFailure, if its try to boot iSCSI volume in + 'BIOS' boot mode. + """ + node = task.node + image_utils.cleanup_iso_image(task) + boot_option = deploy_utils.get_boot_option(task.node) + + iwdi = node.driver_internal_info.get('is_whole_disk_image') + if boot_option == "local" or iwdi: + manager_utils.node_set_boot_device(task, boot_devices.DISK, + persistent=True) + LOG.debug("Node %(node)s is set to permanently boot from local " + "%(device)s", {'node': task.node.uuid, + 'device': boot_devices.DISK}) + return + + params = {} + + if boot_option != 'ramdisk': + root_uuid = node.driver_internal_info.get('root_uuid_or_disk_id') + if not root_uuid and task.driver.storage.should_write_image(task): + LOG.warning( + "The UUID of the root partition could not be found for " + "node %s. Booting instance from disk anyway.", node.uuid) + manager_utils.node_set_boot_device(task, boot_devices.DISK, + persistent=True) + + return + params.update(root_uuid=root_uuid) + + d_info = self._parse_deploy_info(node) + iso_ref = image_utils.prepare_boot_iso(task, d_info, **params) + + if boot_option != 'ramdisk': + i_info = node.instance_info + i_info['ilo_boot_iso'] = iso_ref + node.instance_info = i_info + node.save() + + ilo_common.setup_uefi_https(task, iso_ref, persistent=True) + + LOG.debug("Node %(node)s is set to boot from UEFIHTTP " + "boot option", {'node': task.node.uuid}) + + @METRICS.timer('IloUefiHttpsBoot.clean_up_instance') + def clean_up_instance(self, task): + """Cleans up the boot of instance. + + This method cleans up the environment that was setup for booting + the instance. + + :param task: A task from TaskManager. + :returns: None + """ + LOG.debug("Cleaning up instance boot for " + "%(node)s", {'node': task.node.uuid}) + + image_utils.cleanup_iso_image(task) + + @METRICS.timer('IloUefiHttpsBoot.validate_rescue') + def validate_rescue(self, task): + """Validate that the node has required properties for rescue. + + :param task: a TaskManager instance with the node being checked + :raises: MissingParameterValue if node is missing one or more required + parameters + """ + self._parse_driver_info(task.node, mode='rescue') diff --git a/ironic/drivers/modules/ilo/common.py b/ironic/drivers/modules/ilo/common.py index f297a6b3cd..4ef8dc85e5 100644 --- a/ironic/drivers/modules/ilo/common.py +++ b/ironic/drivers/modules/ilo/common.py @@ -922,3 +922,49 @@ def get_server_post_state(node): except ilo_error.IloError as ilo_exception: raise exception.IloOperationError(operation=operation, error=ilo_exception) + + +def setup_uefi_https(task, iso, persistent=False): + """Sets up system to boot from UEFIHTTP boot device. + + Sets the one-time/persistent boot device to UEFIHTTP based + on the argument supplied. + + :param task: a TaskManager instance containing the node to act on. + :param iso: ISO URL to be set to boot from. + :param persistent: Indicates whether the system should be set to boot + from the given device one-time or each time. + :raises: IloOperationError on an error from IloClient library. + :raises: IloOperationNotSupported if retrieving post state is not + supported on the server. + """ + node = task.node + ilo_object = get_ilo_object(node) + scheme = urlparse.urlparse(iso).scheme.lower() + + operation = (_("Setting up node %(node)s to boot from URL %(iso)s.") % + {'iso': iso, 'node': node.uuid}) + + if scheme != 'https': + msg = (_('Error setting up node %(node)s to boot from ' + 'URL %(iso)s. A secure URL is expected that is exposed ' + 'over HTTPS.') % + {'node': node.uuid, 'iso': iso}) + raise exception.IloOperationNotSupported(operation=operation, + error=msg) + + try: + ilo_object.set_http_boot_url(iso) + LOG.info("Set the node %(node)s to boot from URL %(iso)s " + "successfully.", {'node': node.uuid, 'iso': iso}) + if not persistent: + ilo_object.set_one_time_boot('UEFIHTTP') + else: + ilo_object.update_persistent_boot(['UEFIHTTP']) + + except ilo_error.IloCommandNotSupportedInBiosError as ilo_exception: + raise exception.IloOperationNotSupported(operation=operation, + error=ilo_exception) + except ilo_error.IloError as ilo_exception: + raise exception.IloOperationError(operation=operation, + error=ilo_exception) diff --git a/ironic/drivers/modules/image_utils.py b/ironic/drivers/modules/image_utils.py index f7a3a80d26..f341d68731 100644 --- a/ironic/drivers/modules/image_utils.py +++ b/ironic/drivers/modules/image_utils.py @@ -44,7 +44,14 @@ class ImageHandler(object): "timeout": CONF.redfish.swift_object_expiry_timeout, "image_subdir": "redfish", "file_permission": CONF.redfish.file_permission - } + }, + "ilo5": { + "swift_enabled": not CONF.ilo.use_web_server_for_images, + "container": CONF.ilo.swift_ilo_container, + "timeout": CONF.ilo.swift_object_expiry_timeout, + "image_subdir": "ilo", + "file_permission": CONF.ilo.file_permission + }, } def __init__(self, driver): @@ -380,6 +387,14 @@ def _prepare_iso_image(task, kernel_href, ramdisk_href, return image_url +def _find_param(param_str, param_dict): + val = None + for param_key in param_dict: + if param_str in param_key: + val = param_dict.get(param_key) + return val + + def prepare_deploy_iso(task, params, mode, d_info): """Prepare deploy or rescue ISO image @@ -406,9 +421,13 @@ def prepare_deploy_iso(task, params, mode, d_info): :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') + kernel_str = '%s_kernel' % mode + ramdisk_str = '%s_ramdisk' % mode + bootloader_str = 'bootloader' + + kernel_href = _find_param(kernel_str, d_info) + ramdisk_href = _find_param(ramdisk_str, d_info) + bootloader_href = _find_param(bootloader_str, d_info) # TODO(TheJulia): At some point we should support something like # boot_iso for the deploy interface, perhaps when we support config @@ -494,7 +513,8 @@ def prepare_boot_iso(task, d_info, root_uuid=None): "to generate boot ISO for %(node)s") % {'node': task.node.uuid}) - bootloader_href = d_info.get('bootloader') + bootloader_str = 'bootloader' + bootloader_href = _find_param(bootloader_str, d_info) return _prepare_iso_image( task, kernel_href, ramdisk_href, bootloader_href, diff --git a/ironic/tests/unit/drivers/modules/ilo/test_boot.py b/ironic/tests/unit/drivers/modules/ilo/test_boot.py index f348dd52a7..d681cb72cd 100644 --- a/ironic/tests/unit/drivers/modules/ilo/test_boot.py +++ b/ironic/tests/unit/drivers/modules/ilo/test_boot.py @@ -18,6 +18,7 @@ import io import tempfile from unittest import mock +from urllib import parse as urlparse from ironic_lib import utils as ironic_utils from oslo_config import cfg @@ -36,14 +37,19 @@ from ironic.drivers.modules import deploy_utils from ironic.drivers.modules.ilo import boot as ilo_boot from ironic.drivers.modules.ilo import common as ilo_common from ironic.drivers.modules.ilo import management as ilo_management +from ironic.drivers.modules import image_utils from ironic.drivers.modules import ipxe from ironic.drivers.modules import pxe from ironic.drivers.modules.storage import noop as noop_storage from ironic.drivers import utils as driver_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.drivers.modules.ilo import test_common +from ironic.tests.unit.objects import utils as obj_utils CONF = cfg.CONF +INFO_DICT = db_utils.get_test_ilo_info() class IloBootCommonMethodsTestCase(test_common.BaseIloTest): @@ -1591,3 +1597,746 @@ class IloiPXEBootTestCase(test_common.BaseIloTest): update_secure_boot_mode_mock.assert_called_once_with(task, True) self.assertTrue(task.node.driver_internal_info.get( 'ilo_uefi_iscsi_boot')) + + +class IloUefiHttpsBootTestCase(db_base.DbTestCase): + def setUp(self): + super(IloUefiHttpsBootTestCase, self).setUp() + self.driver = mock.Mock(boot=ilo_boot.IloUefiHttpsBoot()) + n = { + 'driver': 'ilo5', + 'driver_info': INFO_DICT + } + self.config(enabled_hardware_types=['ilo5'], + enabled_boot_interfaces=['ilo-uefi-https'], + enabled_console_interfaces=['ilo'], + enabled_deploy_interfaces=['iscsi'], + enabled_inspect_interfaces=['ilo'], + enabled_management_interfaces=['ilo5'], + enabled_power_interfaces=['ilo'], + enabled_raid_interfaces=['ilo5']) + self.node = obj_utils.create_test_node(self.context, **n) + + @mock.patch.object(urlparse, 'urlparse', spec_set=True, + autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + def test__validate_hrefs_https_image(self, is_glance_mock, urlparse_mock): + is_glance_mock.return_value = False + urlparse_mock.return_value.scheme = 'https' + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + data = { + 'ilo_deploy_kernel': 'https://a.b.c.d/kernel', + 'ilo_deploy_ramdisk': 'https://a.b.c.d/ramdisk', + 'ilo_bootloader': 'https://a.b.c.d/bootloader' + } + task.driver.boot._validate_hrefs(data) + + glance_calls = [ + mock.call('https://a.b.c.d/kernel'), + mock.call('https://a.b.c.d/ramdisk'), + mock.call('https://a.b.c.d/bootloader') + ] + + is_glance_mock.assert_has_calls(glance_calls) + urlparse_mock.assert_has_calls(glance_calls) + + @mock.patch.object(urlparse, 'urlparse', spec_set=True, + autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + def test__validate_hrefs_http_image(self, is_glance_mock, urlparse_mock): + is_glance_mock.return_value = False + scheme_mock = mock.PropertyMock( + side_effect=['http', 'https', 'http']) + type(urlparse_mock.return_value).scheme = scheme_mock + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + data = { + 'ilo_deploy_kernel': 'http://a.b.c.d/kernel', + 'ilo_deploy_ramdisk': 'https://a.b.c.d/ramdisk', + 'ilo_bootloader': 'http://a.b.c.d/bootloader' + } + + glance_calls = [ + mock.call('http://a.b.c.d/kernel'), + mock.call('https://a.b.c.d/ramdisk'), + mock.call('http://a.b.c.d/bootloader') + ] + self.assertRaisesRegex(exception.InvalidParameterValue, + "Secure URLs exposed over HTTPS are .*" + "['ilo_deploy_kernel', 'ilo_bootloader']", + task.driver.boot._validate_hrefs, data) + is_glance_mock.assert_has_calls(glance_calls) + urlparse_mock.assert_has_calls(glance_calls) + + @mock.patch.object(urlparse, 'urlparse', spec_set=True, + autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + def test__validate_hrefs_glance_image(self, is_glance_mock, urlparse_mock): + is_glance_mock.return_value = True + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + data = { + 'ilo_deploy_kernel': 'https://a.b.c.d/kernel', + 'ilo_deploy_ramdisk': 'https://a.b.c.d/ramdisk', + 'ilo_bootloader': 'https://a.b.c.d/bootloader' + } + + task.driver.boot._validate_hrefs(data) + + glance_calls = [ + mock.call('https://a.b.c.d/kernel'), + mock.call('https://a.b.c.d/ramdisk'), + mock.call('https://a.b.c.d/bootloader') + ] + + is_glance_mock.assert_has_calls(glance_calls) + urlparse_mock.assert_not_called() + + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_parse_driver_info', + autospec=True) + @mock.patch.object(deploy_utils, 'get_image_instance_info', + autospec=True) + def test__parse_deploy_info(self, get_img_inst_mock, + parse_driver_mock): + parse_driver_mock.return_value = { + 'ilo_deploy_kernel': 'deploy-kernel', + 'ilo_deploy_ramdisk': 'deploy-ramdisk', + 'ilo_bootloader': 'bootloader' + } + get_img_inst_mock.return_value = { + 'ilo_boot_iso': 'boot-iso', + 'image_source': '6b2f0c0c-79e8-4db6-842e-43c9764204af' + } + instance_info = self.node.instance_info + driver_info = self.node.driver_info + + instance_info['ilo_boot_iso'] = 'boot-iso' + instance_info['image_source'] = '6b2f0c0c-79e8-4db6-842e-43c9764204af' + self.node.instance_info = instance_info + + driver_info['ilo_deploy_kernel'] = 'deploy-kernel' + driver_info['ilo_deploy_ramdisk'] = 'deploy-ramdisk' + driver_info['ilo_bootloader'] = 'bootloader' + self.node.driver_info = driver_info + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + expected_info = { + 'ilo_deploy_kernel': 'deploy-kernel', + 'ilo_deploy_ramdisk': 'deploy-ramdisk', + 'ilo_bootloader': 'bootloader', + 'ilo_boot_iso': 'boot-iso', + 'image_source': '6b2f0c0c-79e8-4db6-842e-43c9764204af' + } + + actual_info = task.driver.boot._parse_deploy_info(task.node) + get_img_inst_mock.assert_called_once_with(task.node) + parse_driver_mock.assert_called_once_with(mock.ANY, task.node) + self.assertEqual(expected_info, actual_info) + + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_validate_hrefs', + autospec=True) + @mock.patch.object(deploy_utils, 'check_for_missing_params', + autospec=True) + @mock.patch.object(ilo_common, 'parse_driver_info', autospec=True) + def test__parse_driver_info_default_mode( + self, parse_driver_mock, check_missing_mock, validate_href_mock): + parse_driver_mock.return_value = { + 'ilo_username': 'admin', + 'ilo_password': 'admin' + } + driver_info = self.node.driver_info + driver_info['ilo_deploy_kernel'] = 'deploy-kernel' + driver_info['ilo_rescue_kernel'] = 'rescue-kernel' + driver_info['ilo_deploy_ramdisk'] = 'deploy-ramdisk' + driver_info['ilo_rescue_ramdisk'] = 'rescue-ramdisk' + driver_info['ilo_bootloader'] = 'bootloader' + driver_info['dummy_key'] = 'dummy-value' + self.node.driver_info = driver_info + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + deploy_info = { + 'ilo_deploy_kernel': 'deploy-kernel', + 'ilo_deploy_ramdisk': 'deploy-ramdisk', + 'ilo_bootloader': 'bootloader' + } + actual_info = deploy_info + actual_info.update({'ilo_username': 'admin', + 'ilo_password': 'admin'}) + + expected_info = task.driver.boot._parse_driver_info(task.node) + validate_href_mock.assert_called_once_with(mock.ANY, deploy_info) + check_missing_mock.assert_called_once_with(deploy_info, mock.ANY) + parse_driver_mock.assert_called_once_with(task.node) + self.assertEqual(actual_info, expected_info) + + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_validate_hrefs', + autospec=True) + @mock.patch.object(deploy_utils, 'check_for_missing_params', + autospec=True) + @mock.patch.object(ilo_common, 'parse_driver_info', autospec=True) + def test__parse_driver_info_rescue_mode( + self, parse_driver_mock, check_missing_mock, validate_href_mock): + parse_driver_mock.return_value = { + 'ilo_username': 'admin', + 'ilo_password': 'admin' + } + mode = 'rescue' + driver_info = self.node.driver_info + driver_info['ilo_deploy_kernel'] = 'deploy-kernel' + driver_info['ilo_rescue_kernel'] = 'rescue-kernel' + driver_info['ilo_deploy_ramdisk'] = 'deploy-ramdisk' + driver_info['ilo_rescue_ramdisk'] = 'rescue-ramdisk' + driver_info['ilo_bootloader'] = 'bootloader' + driver_info['dummy_key'] = 'dummy-value' + self.node.driver_info = driver_info + self.node.save() + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + deploy_info = { + 'ilo_rescue_kernel': 'rescue-kernel', + 'ilo_rescue_ramdisk': 'rescue-ramdisk', + 'ilo_bootloader': 'bootloader' + } + actual_info = deploy_info + actual_info.update({'ilo_username': 'admin', + 'ilo_password': 'admin'}) + + expected_info = task.driver.boot._parse_driver_info( + task.node, mode) + check_missing_mock.assert_called_once_with(deploy_info, mock.ANY) + validate_href_mock.assert_called_once_with(mock.ANY, deploy_info) + parse_driver_mock.assert_called_once_with(task.node) + self.assertEqual(actual_info, expected_info) + + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_validate_hrefs', + autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_image_instance_info', + spec_set=True, autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + def test__validate_instance_image_info_not_iwdi( + self, glance_mock, get_image_inst_mock, validate_image_mock, + validate_href_mock): + instance_info = { + 'ilo_boot_iso': 'boot-iso', + 'image_source': '6b2f0c0c-79e8-4db6-842e-43c9764204af' + } + driver_internal_info = self.node.driver_internal_info + driver_internal_info.pop('is_whole_disk_image', None) + self.node.driver_internal_info = driver_internal_info + self.node.instance_info = instance_info + self.node.save() + get_image_inst_mock.return_value = instance_info + glance_mock.return_value = True + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + + task.driver.boot._validate_instance_image_info(task) + get_image_inst_mock.assert_called_once_with(task.node) + glance_mock.assert_called_once_with( + '6b2f0c0c-79e8-4db6-842e-43c9764204af') + validate_image_mock.assert_called_once_with(task.context, + instance_info, + ['kernel_id', + 'ramdisk_id']) + validate_href_mock.assert_called_once_with(mock.ANY, instance_info) + + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_validate_hrefs', + autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_image_instance_info', + spec_set=True, autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + def test__validate_instance_image_info_neither_iwdi_nor_glance( + self, glance_mock, get_image_inst_mock, validate_image_mock, + validate_href_mock): + + instance_info = { + 'ilo_boot_iso': 'boot-iso', + 'image_source': '6b2f0c0c-79e8-4db6-842e-43c9764204af' + } + driver_internal_info = self.node.driver_internal_info + driver_internal_info.pop('is_whole_disk_image', None) + self.node.driver_internal_info = driver_internal_info + self.node.instance_info = instance_info + self.node.save() + get_image_inst_mock.return_value = instance_info + glance_mock.return_value = False + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + + task.driver.boot._validate_instance_image_info(task) + get_image_inst_mock.assert_called_once_with(task.node) + glance_mock.assert_called_once_with( + '6b2f0c0c-79e8-4db6-842e-43c9764204af') + validate_image_mock.assert_called_once_with(task.context, + instance_info, + ['kernel', + 'ramdisk']) + validate_href_mock.assert_called_once_with(mock.ANY, instance_info) + + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_validate_hrefs', + autospec=True) + @mock.patch.object(deploy_utils, 'validate_image_properties', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_image_instance_info', + spec_set=True, autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + def test__validate_instance_image_info_iwdi( + self, glance_mock, get_image_inst_mock, validate_image_mock, + validate_href_mock): + instance_info = { + 'ilo_boot_iso': 'boot-iso', + 'image_source': '6b2f0c0c-79e8-4db6-842e-43c9764204af' + } + driver_internal_info = self.node.driver_internal_info or {} + driver_internal_info['is_whole_disk_image'] = True + self.node.driver_internal_info = driver_internal_info + self.node.save() + get_image_inst_mock.return_value = instance_info + glance_mock.return_value = True + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + + task.driver.boot._validate_instance_image_info(task) + get_image_inst_mock.assert_called_once_with(task.node) + glance_mock.assert_not_called() + validate_image_mock.assert_called_once_with(task.context, + instance_info, []) + validate_href_mock.assert_called_once_with(mock.ANY, instance_info) + + @mock.patch.object(ilo_common, 'get_current_boot_mode', + autospec=True) + @mock.patch.object(noop_storage.NoopStorage, 'should_write_image', + autospec=True) + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_validate_driver_info', + spec_set=True, autospec=True) + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, + '_validate_instance_image_info', + spec_set=True, autospec=True) + def test_validate(self, mock_val_instance_image_info, + mock_val_driver_info, storage_mock, get_boot_mock): + get_boot_mock.return_value = 'UEFI' + instance_info = self.node.instance_info + + instance_info['ilo_boot_iso'] = 'boot-iso' + instance_info['image_source'] = '6b2f0c0c-79e8-4db6-842e-43c9764204af' + self.node.instance_info = instance_info + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + + task.node.driver_info['ilo_deploy_kernel'] = 'deploy-kernel' + task.node.driver_info['ilo_deploy_ramdisk'] = 'deploy-ramdisk' + task.node.driver_info['ilo_bootloader'] = 'bootloader' + storage_mock.return_value = True + task.driver.boot.validate(task) + mock_val_instance_image_info.assert_called_once_with( + mock.ANY, task) + mock_val_driver_info.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(ilo_common, 'get_current_boot_mode', + autospec=True) + @mock.patch.object(noop_storage.NoopStorage, 'should_write_image', + autospec=True) + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_validate_driver_info', + spec_set=True, autospec=True) + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, + '_validate_instance_image_info', + spec_set=True, autospec=True) + def test_validate_bios(self, mock_val_instance_image_info, + mock_val_driver_info, storage_mock, get_boot_mock): + get_boot_mock.return_value = 'LEGACY' + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaisesRegex(exception.InvalidParameterValue, + "Validation for 'ilo-uefi-https' boot " + "interface failed.*", + task.driver.boot.validate, task) + mock_val_instance_image_info.assert_not_called() + mock_val_driver_info.assert_not_called() + + @mock.patch.object(ilo_common, 'get_current_boot_mode', + autospec=True) + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_validate_driver_info', + spec_set=True, autospec=True) + @mock.patch.object(image_service.HttpImageService, 'validate_href', + spec_set=True, autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + def test_validate_ramdisk_boot_option_glance(self, is_glance_image_mock, + validate_href_mock, + val_driver_info_mock, + get_boot_mock): + get_boot_mock.return_value = 'UEFI' + instance_info = self.node.instance_info + boot_iso = '6b2f0c0c-79e8-4db6-842e-43c9764204af' + instance_info['ilo_boot_iso'] = boot_iso + instance_info['capabilities'] = '{"boot_option": "ramdisk"}' + self.node.instance_info = instance_info + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + is_glance_image_mock.return_value = True + task.driver.boot.validate(task) + is_glance_image_mock.assert_called_once_with(boot_iso) + self.assertFalse(validate_href_mock.called) + self.assertFalse(val_driver_info_mock.called) + + @mock.patch.object(ilo_common, 'get_current_boot_mode', + autospec=True) + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_validate_driver_info', + spec_set=True, autospec=True) + @mock.patch.object(image_service.HttpImageService, 'validate_href', + spec_set=True, autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + def test_validate_ramdisk_boot_option_webserver(self, is_glance_image_mock, + validate_href_mock, + val_driver_info_mock, + get_boot_mock): + get_boot_mock.return_value = 'UEFI' + instance_info = self.node.instance_info + boot_iso = 'http://myserver/boot.iso' + instance_info['ilo_boot_iso'] = boot_iso + instance_info['capabilities'] = '{"boot_option": "ramdisk"}' + self.node.instance_info = instance_info + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + is_glance_image_mock.return_value = False + task.driver.boot.validate(task) + is_glance_image_mock.assert_called_once_with(boot_iso) + validate_href_mock.assert_called_once_with(mock.ANY, boot_iso) + self.assertFalse(val_driver_info_mock.called) + + @mock.patch.object(ilo_common, 'get_current_boot_mode', + autospec=True) + @mock.patch.object(ilo_boot.LOG, 'error', spec_set=True, autospec=True) + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_validate_driver_info', + spec_set=True, autospec=True) + @mock.patch.object(image_service.HttpImageService, 'validate_href', + spec_set=True, autospec=True) + @mock.patch.object(service_utils, 'is_glance_image', spec_set=True, + autospec=True) + def test_validate_ramdisk_boot_option_webserver_exc( + self, is_glance_image_mock, validate_href_mock, + val_driver_info_mock, log_mock, get_boot_mock): + + get_boot_mock.return_value = 'UEFI' + instance_info = self.node.instance_info + validate_href_mock.side_effect = exception.ImageRefValidationFailed( + image_href='http://myserver/boot.iso', reason='fail') + boot_iso = 'http://myserver/boot.iso' + instance_info['ilo_boot_iso'] = boot_iso + instance_info['capabilities'] = '{"boot_option": "ramdisk"}' + self.node.instance_info = instance_info + self.node.save() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + + is_glance_image_mock.return_value = False + self.assertRaisesRegex(exception.ImageRefValidationFailed, + "Validation of image href " + "http://myserver/boot.iso failed", + task.driver.boot.validate, task) + is_glance_image_mock.assert_called_once_with(boot_iso) + validate_href_mock.assert_called_once_with(mock.ANY, boot_iso) + self.assertFalse(val_driver_info_mock.called) + self.assertIn("UEFI-HTTPS boot with 'ramdisk' boot_option " + "accepts only Glance images or HTTPS URLs as " + "instance_info['ilo_boot_iso'].", + log_mock.call_args[0][0]) + + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_validate_driver_info', + autospec=True) + def test_validate_inspection(self, mock_val_driver_info): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.boot.validate_inspection(task) + mock_val_driver_info.assert_called_once_with(mock.ANY, task) + + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_parse_driver_info', + spec_set=True, autospec=True) + def test_validate_inspection_missing(self, mock_parse_driver_info): + mock_parse_driver_info.side_effect = exception.MissingParameterValue( + "Error validating iLO UEFIHTTPS for deploy.") + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.UnsupportedDriverExtension, + task.driver.boot.validate_inspection, task) + + @mock.patch.object(ilo_common, 'setup_uefi_https', + spec_set=True, autospec=True) + @mock.patch.object(image_utils, 'prepare_deploy_iso', + spec_set=True, autospec=True) + @mock.patch.object(ilo_boot, 'prepare_node_for_deploy', + spec_set=True, autospec=True) + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_parse_driver_info', + spec_set=True, autospec=True) + @mock.patch.object(deploy_utils, 'get_single_nic_with_vif_port_id', + spec_set=True, autospec=True) + def _test_prepare_ramdisk(self, get_nic_mock, + parse_driver_mock, + prepare_node_for_deploy_mock, + prepare_deploy_iso_mock, + setup_uefi_https_mock, + ilo_boot_iso, image_source, + ramdisk_params={'a': 'b'}, + mode='deploy', state=states.DEPLOYING): + self.node.provision_state = state + self.node.save() + instance_info = self.node.instance_info + instance_info['ilo_boot_iso'] = ilo_boot_iso + instance_info['image_source'] = image_source + self.node.instance_info = instance_info + self.node.save() + iso = 'provisioning-iso' + + d_info = { + 'ilo_' + mode + '_kernel': mode + '-kernel', + 'ilo_' + mode + '_ramdisk': mode + '-ramdisk', + 'ilo_' + 'bootloader': 'bootloader' + } + parse_driver_mock.return_value = d_info + prepare_deploy_iso_mock.return_value = 'recreated-iso' + + get_nic_mock.return_value = '12:34:56:78:90:ab' + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + + driver_info = task.node.driver_info + driver_info['ilo_%s_iso' % mode] = iso + task.node.driver_info = driver_info + + task.driver.boot.prepare_ramdisk(task, ramdisk_params) + + prepare_node_for_deploy_mock.assert_called_once_with(task) + + get_nic_mock.assert_called_once_with(task) + parse_driver_mock.assert_called_once_with( + mock.ANY, task.node, mode) + prepare_deploy_iso_mock.assert_called_once_with( + task, ramdisk_params, mode, d_info) + setup_uefi_https_mock.assert_called_once_with(task, + 'recreated-iso') + + def test_prepare_ramdisk_rescue_glance_image(self): + self._test_prepare_ramdisk( + ilo_boot_iso='swift:abcdef', + image_source='6b2f0c0c-79e8-4db6-842e-43c9764204af', + mode='rescue', state=states.RESCUING) + self.node.refresh() + self.assertNotIn('ilo_boot_iso', self.node.instance_info) + + def test_prepare_ramdisk_rescue_not_a_glance_image(self): + self._test_prepare_ramdisk( + ilo_boot_iso='http://mybootiso', + image_source='http://myimage', + mode='rescue', state=states.RESCUING) + self.node.refresh() + self.assertEqual('http://mybootiso', + self.node.instance_info['ilo_boot_iso']) + + def test_prepare_ramdisk_glance_image(self): + self._test_prepare_ramdisk( + ilo_boot_iso='swift:abcdef', + image_source='6b2f0c0c-79e8-4db6-842e-43c9764204af') + self.node.refresh() + self.assertNotIn('ilo_boot_iso', self.node.instance_info) + + def test_prepare_ramdisk_not_a_glance_image(self): + self._test_prepare_ramdisk( + ilo_boot_iso='http://mybootiso', + image_source='http://myimage') + self.node.refresh() + self.assertEqual('http://mybootiso', + self.node.instance_info['ilo_boot_iso']) + + def test_prepare_ramdisk_glance_image_cleaning(self): + self._test_prepare_ramdisk( + ilo_boot_iso='swift:abcdef', + image_source='6b2f0c0c-79e8-4db6-842e-43c9764204af', + mode='deploy', state=states.CLEANING) + self.node.refresh() + self.assertNotIn('ilo_boot_iso', self.node.instance_info) + + def test_prepare_ramdisk_not_a_glance_image_cleaning(self): + self._test_prepare_ramdisk( + ilo_boot_iso='http://mybootiso', + image_source='http://myimage', + mode='deploy', state=states.CLEANING) + self.node.refresh() + self.assertEqual('http://mybootiso', + self.node.instance_info['ilo_boot_iso']) + + @mock.patch.object(image_utils, 'cleanup_iso_image', spec_set=True, + autospec=True) + def test_clean_up_ramdisk(self, cleanup_iso_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.clean_up_ramdisk(task) + cleanup_iso_mock.assert_called_once_with(task) + + @mock.patch.object(image_utils, 'cleanup_iso_image', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'setup_uefi_https', + spec_set=True, autospec=True) + @mock.patch.object(image_utils, 'prepare_deploy_iso', + spec_set=True, autospec=True) + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_parse_deploy_info', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + def _test_prepare_instance_local_or_whole_disk_image( + self, set_boot_device_mock, + parse_deploy_mock, prepare_iso_mock, setup_uefi_https_mock, + cleanup_iso_mock): + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.prepare_instance(task) + + set_boot_device_mock.assert_called_once_with(task, + boot_devices.DISK, + persistent=True) + cleanup_iso_mock.assert_called_once_with(task) + prepare_iso_mock.assert_not_called() + setup_uefi_https_mock.assert_not_called() + + def test_prepare_instance_image_local(self): + self.node.instance_info = {'capabilities': '{"boot_option": "local"}'} + self.node.save() + self._test_prepare_instance_local_or_whole_disk_image() + + def test_prepare_instance_whole_disk_image(self): + self.node.driver_internal_info = {'is_whole_disk_image': True} + self.node.save() + self._test_prepare_instance_local_or_whole_disk_image() + + @mock.patch.object(image_utils, 'cleanup_iso_image', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'setup_uefi_https', + spec_set=True, autospec=True) + @mock.patch.object(image_utils, 'prepare_boot_iso', + spec_set=True, autospec=True) + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_parse_deploy_info', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + def test_prepare_instance_partition_image( + self, set_boot_device_mock, + parse_deploy_mock, prepare_iso_mock, setup_uefi_https_mock, + cleanup_iso_mock): + + self.node.instance_info = { + 'capabilities': '{"boot_option": "netboot"}' + } + self.node.driver_internal_info = { + 'root_uuid_or_disk_id': ( + "12312642-09d3-467f-8e09-12385826a123") + } + self.node.driver_internal_info.update({'is_whole_disk_image': False}) + self.node.save() + d_info = {'a': 'x', 'b': 'y'} + parse_deploy_mock.return_value = d_info + prepare_iso_mock.return_value = "recreated-iso" + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.prepare_instance(task) + + cleanup_iso_mock.assert_called_once_with(task) + set_boot_device_mock.assert_not_called() + parse_deploy_mock.assert_called_once_with(mock.ANY, task.node) + prepare_iso_mock.assert_called_once_with( + task, d_info, root_uuid='12312642-09d3-467f-8e09-12385826a123') + setup_uefi_https_mock.assert_called_once_with( + task, "recreated-iso", True) + self.assertEqual(task.node.instance_info['ilo_boot_iso'], + "recreated-iso") + + @mock.patch.object(image_utils, 'cleanup_iso_image', spec_set=True, + autospec=True) + @mock.patch.object(ilo_common, 'setup_uefi_https', + spec_set=True, autospec=True) + @mock.patch.object(image_utils, 'prepare_boot_iso', + spec_set=True, autospec=True) + @mock.patch.object(ilo_boot.IloUefiHttpsBoot, '_parse_deploy_info', + spec_set=True, autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', spec_set=True, + autospec=True) + def test_prepare_instance_boot_ramdisk( + self, set_boot_device_mock, + parse_deploy_mock, prepare_iso_mock, setup_uefi_https_mock, + cleanup_iso_mock): + + self.node.driver_internal_info.update({'is_whole_disk_image': False}) + self.node.save() + d_info = {'a': 'x', 'b': 'y'} + parse_deploy_mock.return_value = d_info + prepare_iso_mock.return_value = "recreated-iso" + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + instance_info = task.node.instance_info + instance_info['capabilities'] = '{"boot_option": "ramdisk"}' + task.node.instance_info = instance_info + task.node.save() + task.driver.boot.prepare_instance(task) + + cleanup_iso_mock.assert_called_once_with(task) + set_boot_device_mock.assert_not_called() + parse_deploy_mock.assert_called_once_with(mock.ANY, task.node) + prepare_iso_mock.assert_called_once_with( + task, d_info) + setup_uefi_https_mock.assert_called_once_with( + task, "recreated-iso", True) + self.assertTrue('ilo_boot_iso' not in task.node.instance_info) + + @mock.patch.object(image_utils, 'cleanup_iso_image', spec_set=True, + autospec=True) + def test_clean_up_instance(self, cleanup_iso_mock): + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.boot.clean_up_instance(task) + cleanup_iso_mock.assert_called_once_with(task) + + def test_validate_rescue(self): + driver_info = self.node.driver_info + driver_info['ilo_rescue_kernel'] = 'rescue-kernel' + driver_info['ilo_rescue_ramdisk'] = 'rescue-ramdisk' + driver_info['ilo_bootloader'] = 'bootloader' + self.node.driver_info = driver_info + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + task.driver.boot.validate_rescue(task) + + def test_validate_rescue_no_rescue_ramdisk(self): + driver_info = self.node.driver_info + driver_info['ilo_rescue_kernel'] = 'rescue-kernel' + driver_info['ilo_rescue_ramdisk'] = 'rescue-ramdisk' + driver_info.pop('ilo_bootloader', None) + self.node.driver_info = driver_info + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaisesRegex(exception.MissingParameterValue, + "Error validating rescue for iLO UEFI " + "HTTPS boot.* ['ilo_bootloader']", + task.driver.boot.validate_rescue, task) diff --git a/ironic/tests/unit/drivers/modules/ilo/test_common.py b/ironic/tests/unit/drivers/modules/ilo/test_common.py index c743a62604..2877e61955 100644 --- a/ironic/tests/unit/drivers/modules/ilo/test_common.py +++ b/ironic/tests/unit/drivers/modules/ilo/test_common.py @@ -675,7 +675,6 @@ class IloCommonMethodsTestCase(BaseIloTest): swift_obj_mock = swift_api_mock.return_value boot_iso = 'swift:object-name' swift_obj_mock.get_temp_url.return_value = 'image_url' - CONF.keystone_authtoken.auth_uri = 'http://authurl' CONF.ilo.swift_ilo_container = 'ilo_cont' CONF.ilo.swift_object_expiry_timeout = 1 with task_manager.acquire(self.context, self.node.uuid, @@ -1206,3 +1205,79 @@ class IloCommonMethodsTestCase(BaseIloTest): ilo_common.get_server_post_state, task.node) ilo_mock_object.get_host_post_state.assert_called_once_with() + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_setup_uefi_https_scheme_http(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + iso = "http://1.1.1.1/image.iso" + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationNotSupported, + ilo_common.setup_uefi_https, + task, iso, True) + + ilo_mock_object.set_http_boot_url.assert_not_called() + ilo_mock_object.set_one_time_boot.assert_not_called() + ilo_mock_object.update_persistent_boot.assert_not_called() + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def _test_setup_uefi_https(self, get_ilo_object_mock, persistent): + + ilo_mock_object = get_ilo_object_mock.return_value + + iso = "https://1.1.1.1/image.iso" + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + ilo_common.setup_uefi_https(task, iso, persistent=persistent) + + ilo_mock_object.set_http_boot_url.assert_called_once_with(iso) + if persistent: + ilo_mock_object.set_one_time_boot.assert_not_called() + ilo_mock_object.update_persistent_boot.assert_called_once_with( + ['UEFIHTTP']) + else: + ilo_mock_object.set_one_time_boot.assert_called_once_with( + 'UEFIHTTP') + ilo_mock_object.update_persistent_boot.assert_not_called() + + def test_setup_uefi_https_persistent_true(self): + self._test_setup_uefi_https(persistent=True) + + def test_setup_uefi_https_persistent_false(self): + self._test_setup_uefi_https(persistent=False) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_setup_uefi_https_raises_not_supported(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + + exc = ilo_error.IloCommandNotSupportedInBiosError('error') + ilo_mock_object.set_http_boot_url.side_effect = exc + + iso = "https://1.1.1.1/image.iso" + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationNotSupported, + ilo_common.setup_uefi_https, + task, iso, True) + + @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True, + autospec=True) + def test_setup_uefi_https_raises_ilo_error(self, get_ilo_object_mock): + ilo_mock_object = get_ilo_object_mock.return_value + + exc = ilo_error.IloError('error') + ilo_mock_object.set_http_boot_url.side_effect = exc + + iso = "https://1.1.1.1/image.iso" + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IloOperationError, + ilo_common.setup_uefi_https, + task, iso, True) diff --git a/ironic/tests/unit/drivers/modules/test_image_utils.py b/ironic/tests/unit/drivers/modules/test_image_utils.py index dc7750fb6b..54204995b5 100644 --- a/ironic/tests/unit/drivers/modules/test_image_utils.py +++ b/ironic/tests/unit/drivers/modules/test_image_utils.py @@ -314,18 +314,51 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase): root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', base_iso='/path/to/baseiso') + def test__find_param(self): + param_dict = { + 'ilo_deploy_kernel': 'kernel', + 'ilo_deploy_ramdisk': 'ramdisk', + 'ilo_bootloader': 'bootloader' + } + param_str = "deploy_kernel" + expected = "kernel" + + actual = image_utils._find_param(param_str, param_dict) + self.assertEqual(actual, expected) + + def test__find_param_not_found(self): + param_dict = { + 'ilo_deploy_ramdisk': 'ramdisk', + 'ilo_bootloader': 'bootloader' + } + param_str = "deploy_kernel" + expected = None + actual = image_utils._find_param(param_str, param_dict) + self.assertEqual(actual, expected) + + @mock.patch.object(image_utils, '_find_param', autospec=True) @mock.patch.object(image_utils, '_prepare_iso_image', autospec=True) - def test_prepare_deploy_iso(self, mock__prepare_iso_image): + def test_prepare_deploy_iso(self, mock__prepare_iso_image, + find_mock): with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: d_info = { - 'deploy_kernel': 'kernel', - 'deploy_ramdisk': 'ramdisk', - 'bootloader': 'bootloader' + 'ilo_deploy_kernel': 'kernel', + 'ilo_deploy_ramdisk': 'ramdisk', + 'ilo_bootloader': 'bootloader' } task.node.driver_info.update(d_info) + find_call_list = [ + mock.call('deploy_kernel', d_info), + mock.call('deploy_ramdisk', d_info), + mock.call('bootloader', d_info) + ] + find_mock.side_effect = [ + 'kernel', 'ramdisk', 'bootloader' + ] + task.node.instance_info.update(deploy_boot_mode='uefi') image_utils.prepare_deploy_iso(task, {}, 'deploy', d_info) @@ -333,21 +366,33 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase): mock__prepare_iso_image.assert_called_once_with( task, 'kernel', 'ramdisk', 'bootloader', params={}) + find_mock.assert_has_calls(find_call_list) + + @mock.patch.object(image_utils, '_find_param', autospec=True) @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): + self, mock_create_vfat_image, mock__prepare_iso_image, + find_mock): with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: d_info = { - 'deploy_kernel': 'kernel', - 'deploy_ramdisk': 'ramdisk' + 'ilo_deploy_kernel': 'kernel', + 'ilo_deploy_ramdisk': 'ramdisk' } task.node.driver_info.update(d_info) task.node.instance_info.update() + find_call_list = [ + mock.call('deploy_kernel', d_info), + mock.call('deploy_ramdisk', d_info), + mock.call('bootloader', d_info) + ] + find_mock.side_effect = [ + 'kernel', 'ramdisk', None + ] network_data = {'a': ['b']} mock_get_node_nw_data = mock.MagicMock(return_value=network_data) @@ -362,16 +407,19 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase): task, 'kernel', 'ramdisk', bootloader_href=None, configdrive=mock.ANY, params={}) + find_mock.assert_has_calls(find_call_list) + + @mock.patch.object(image_utils, '_find_param', autospec=True) @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): + mock__prepare_iso_image, find_mock): with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: d_info = { - 'deploy_kernel': 'kernel', - 'deploy_ramdisk': 'ramdisk', - 'bootloader': 'bootloader' + 'ilo_deploy_kernel': 'kernel', + 'ilo_deploy_ramdisk': 'ramdisk', + 'ilo_bootloader': 'bootloader' } task.node.driver_info.update(d_info) @@ -380,6 +428,14 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase): 'kernel': 'http://kernel/img', 'ramdisk': 'http://ramdisk/img'}) + find_call_list = [ + mock.call('bootloader', d_info) + ] + + find_mock.side_effect = [ + 'bootloader' + ] + image_utils.prepare_boot_iso( task, d_info, root_uuid=task.node.uuid) @@ -388,10 +444,14 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase): 'bootloader', root_uuid=task.node.uuid, base_iso=None) + find_mock.assert_has_calls(find_call_list) + + @mock.patch.object(image_utils, '_find_param', autospec=True) @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): + mock__prepare_iso_image, + find_mock): with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: d_info = { @@ -404,6 +464,13 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase): task.node.instance_info.update( {'boot_iso': 'http://boot/iso'}) + find_call_list = [ + mock.call('bootloader', d_info) + ] + + find_mock.side_effect = [ + 'bootloader' + ] image_utils.prepare_boot_iso( task, d_info, root_uuid=task.node.uuid) @@ -411,3 +478,5 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase): mock.ANY, None, None, 'bootloader', root_uuid=task.node.uuid, base_iso='http://boot/iso') + + find_mock.assert_has_calls(find_call_list) diff --git a/releasenotes/notes/add-ilo-uefi-https-boot-interface-f3b163a8a6243283.yaml b/releasenotes/notes/add-ilo-uefi-https-boot-interface-f3b163a8a6243283.yaml new file mode 100644 index 0000000000..06bc83094b --- /dev/null +++ b/releasenotes/notes/add-ilo-uefi-https-boot-interface-f3b163a8a6243283.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds ``ilo-uefi-https`` boot interface to ``ilo5`` hardware type. + This boot interface levereges the iLO UEFI firmware capability to + boot from given HTTPS URLs hosted securely over HTTPS webserver + with standard/custom certificates. diff --git a/setup.cfg b/setup.cfg index c314e21a5d..f4beb96a5e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -69,6 +69,7 @@ ironic.hardware.interfaces.boot = ilo-pxe = ironic.drivers.modules.ilo.boot:IloPXEBoot ilo-ipxe = ironic.drivers.modules.ilo.boot:IloiPXEBoot ilo-virtual-media = ironic.drivers.modules.ilo.boot:IloVirtualMediaBoot + ilo-uefi-https = ironic.drivers.modules.ilo.boot:IloUefiHttpsBoot ipxe = ironic.drivers.modules.ipxe:iPXEBoot irmc-pxe = ironic.drivers.modules.irmc.boot:IRMCPXEBoot irmc-virtual-media = ironic.drivers.modules.irmc.boot:IRMCVirtualMediaBoot