Adds ilo-uefi-https boot interface to ilo5

Change-Id: I224eca4d8b331711369b17903098daa9fec27d7d
Story: #2008073
Task: #40761
This commit is contained in:
vmud213 2020-08-10 08:58:52 +00:00
parent f1ea2ee6d1
commit 6d36b0b785
11 changed files with 1463 additions and 20 deletions

View File

@ -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=<ilo-ip-address> \
--driver-info ilo_username=<ilo-username> \
--driver-info ilo_password=<ilo-password> \
--driver-info ilo_deploy_kernel=<glance-uuid-of-deploy-kernel> \
--driver-info ilo_deploy_ramdisk=<glance-uuid-of-rescue-ramdisk> \
--driver-info ilo_bootloader=<glance-uuid-of-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

View File

@ -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``.')),
]

View File

@ -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."""

View File

@ -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 <property name>:<property description> 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')

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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