From c787f6806f7dd22220330402b2cb3c90e17418d1 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 17 Jul 2018 14:36:09 -0700 Subject: [PATCH] ipxe boot interface A long time ago, in a galaxy not so far away, the ironic comunity reached consensus that we should refactor the PXE interface such that we had separate PXE and iPXE interfaces. In looking at what it would take for something like a standalone deployment to have multiarchitecture support in their environment, it seems even more urgent and necessary that we begin to make this delineation. This is because while some ARM iPXE support exists, the binary is not a commonly shipped artifiact, so operators are largely only left with grubaa64.efi as their bootloader. A similar issue exists with ppc64le deployments where they must disable iPXE, as the ppc64le hardware expects reading a syslinux compatible file, similarlly no iPXE loader exists. To start this effort, we need to promote iPXE functionality to a dedicated interface, and remove the necessity of setting the [pxe]ipxe_enabled setting. Next steps, beyond this patch, would be to begin to tease out the common private method code in the underlying PXE interface that both the iPXE and PXE interfaces use, and appropriately relocate and refactor that code as necesary. During this process, we can create an [ipxe] configuration section, and migrate settings. Finally, once the deprecation cycle is complete, we will be able to remove the ipxe logic with-in the PXE interface. Change-Id: I392616417c48986e84e50a3ddc7567344bfe3571 Story: #1628069 Task: #10516 --- ironic/common/pxe_utils.py | 164 ++-- ironic/conf/pxe.py | 10 +- ironic/drivers/generic.py | 3 +- ironic/drivers/modules/deploy_utils.py | 14 +- ironic/drivers/modules/fake.py | 4 + ironic/drivers/modules/ipxe.py | 358 ++++++++ ironic/drivers/modules/pxe.py | 77 +- ironic/drivers/modules/storage/cinder.py | 10 +- ironic/drivers/modules/storage/external.py | 6 +- ironic/tests/unit/common/test_pxe_utils.py | 21 +- .../unit/drivers/modules/test_deploy_utils.py | 20 +- .../tests/unit/drivers/modules/test_ipxe.py | 824 ++++++++++++++++++ .../unit/drivers/modules/test_iscsi_deploy.py | 3 +- ironic/tests/unit/drivers/modules/test_pxe.py | 153 ++-- ...t-interface-addition-faacb344a72389f2.yaml | 20 + setup.cfg | 1 + zuul.d/ironic-jobs.yaml | 9 + zuul.d/project.yaml | 10 +- 18 files changed, 1500 insertions(+), 207 deletions(-) create mode 100644 ironic/drivers/modules/ipxe.py create mode 100644 ironic/tests/unit/drivers/modules/test_ipxe.py create mode 100644 releasenotes/notes/ipxe-boot-interface-addition-faacb344a72389f2.yaml diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py index 292c228307..79db827b28 100644 --- a/ironic/common/pxe_utils.py +++ b/ironic/common/pxe_utils.py @@ -20,6 +20,7 @@ from ironic_lib import utils as ironic_utils from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import fileutils +from oslo_utils import importutils from ironic.common import dhcp_factory from ironic.common import exception @@ -58,14 +59,22 @@ def get_root_dir(): return CONF.pxe.tftp_root -def _ensure_config_dirs_exist(node_uuid): +def get_ipxe_root_dir(): + return CONF.deploy.http_root + + +def _ensure_config_dirs_exist(task, ipxe_enabled=False): """Ensure that the node's and PXE configuration directories exist. - :param node_uuid: the UUID of the node. - + :param task: A TaskManager instance + :param ipxe_enabled: Default false boolean to indicate if ipxe + is in use by the caller. """ - root_dir = get_root_dir() - node_dir = os.path.join(root_dir, node_uuid) + if ipxe_enabled: + root_dir = get_ipxe_root_dir() + else: + root_dir = get_root_dir() + node_dir = os.path.join(root_dir, task.node.uuid) pxe_dir = os.path.join(root_dir, PXE_CFG_DIR_NAME) # NOTE: We should only change the permissions if the folder # does not exist. i.e. if defined, an operator could have @@ -78,11 +87,12 @@ def _ensure_config_dirs_exist(node_uuid): os.chmod(directory, CONF.pxe.dir_permission) -def _link_mac_pxe_configs(task): +def _link_mac_pxe_configs(task, ipxe_enabled=False): """Link each MAC address with the PXE configuration file. :param task: A TaskManager instance. - + :param ipxe_enabled: Default false boolean to indicate if ipxe + is in use by the caller. """ def create_link(mac_path): @@ -91,26 +101,32 @@ def _link_mac_pxe_configs(task): pxe_config_file_path, os.path.dirname(mac_path)) utils.create_link_without_raise(relative_source_path, mac_path) - pxe_config_file_path = get_pxe_config_file_path(task.node.uuid) + pxe_config_file_path = get_pxe_config_file_path( + task.node.uuid, ipxe_enabled=ipxe_enabled) for port in task.ports: client_id = port.extra.get('client-id') # Syslinux, ipxe, depending on settings. - create_link(_get_pxe_mac_path(port.address, client_id=client_id)) + create_link(_get_pxe_mac_path(port.address, client_id=client_id, + ipxe_enabled=ipxe_enabled)) # Grub2 MAC address only create_link(_get_pxe_grub_mac_path(port.address)) -def _link_ip_address_pxe_configs(task, hex_form): +def _link_ip_address_pxe_configs(task, hex_form, ipxe_enabled=False): """Link each IP address with the PXE configuration file. :param task: A TaskManager instance. :param hex_form: Boolean value indicating if the conf file name should be hexadecimal equivalent of supplied ipv4 address. + :param ipxe_enabled: Default false boolean to indicate if ipxe + is in use by the caller. :raises: FailedToGetIPAddressOnPort :raises: InvalidIPv4Address """ - pxe_config_file_path = get_pxe_config_file_path(task.node.uuid) + pxe_config_file_path = get_pxe_config_file_path( + task.node.uuid, + ipxe_enabled=ipxe_enabled) api = dhcp_factory.DHCPFactory().provider ip_addrs = api.get_ip_addresses(task) @@ -132,23 +148,29 @@ def _get_pxe_grub_mac_path(mac): return os.path.join(get_root_dir(), mac + '.conf') -def _get_pxe_mac_path(mac, delimiter='-', client_id=None): +def _get_pxe_mac_path(mac, delimiter='-', client_id=None, + ipxe_enabled=False): """Convert a MAC address into a PXE config file name. :param mac: A MAC address string in the format xx:xx:xx:xx:xx:xx. :param delimiter: The MAC address delimiter. Defaults to dash ('-'). :param client_id: client_id indicate InfiniBand port. Defaults is None (Ethernet) + :param ipxe_enabled: A default False boolean value to tell the method + if the caller is using iPXE. :returns: the path to the config file. """ mac_file_name = mac.replace(':', delimiter).lower() - if not CONF.pxe.ipxe_enabled: + if not ipxe_enabled: hw_type = '01-' if client_id: hw_type = '20-' mac_file_name = hw_type + mac_file_name - return os.path.join(get_root_dir(), PXE_CFG_DIR_NAME, mac_file_name) + return os.path.join(get_root_dir(), PXE_CFG_DIR_NAME, + mac_file_name) + return os.path.join(get_ipxe_root_dir(), PXE_CFG_DIR_NAME, + mac_file_name) def _get_pxe_ip_address_path(ip_address, hex_form): @@ -173,7 +195,8 @@ def _get_pxe_ip_address_path(ip_address, hex_form): ) -def get_kernel_ramdisk_info(node_uuid, driver_info, mode='deploy'): +def get_kernel_ramdisk_info(node_uuid, driver_info, mode='deploy', + ipxe_enabled=False): """Get href and tftp path for deploy or rescue kernel and ramdisk. :param node_uuid: UUID of the node @@ -182,13 +205,18 @@ def get_kernel_ramdisk_info(node_uuid, driver_info, mode='deploy'): ramdisk are being requested. Supported values are 'deploy' 'rescue'. Defaults to 'deploy', indicating deploy paths will be returned. + :param ipxe_enabled: A default False boolean value to tell the method + if the caller is using iPXE. :returns: a dictionary whose keys are deploy_kernel and deploy_ramdisk or rescue_kernel and rescue_ramdisk and whose values are the absolute paths to them. Note: driver_info should be validated outside of this method. """ - root_dir = get_root_dir() + if ipxe_enabled: + root_dir = get_ipxe_root_dir() + else: + root_dir = get_root_dir() image_info = {} labels = KERNEL_RAMDISK_LABELS[mode] for label in labels: @@ -199,17 +227,22 @@ def get_kernel_ramdisk_info(node_uuid, driver_info, mode='deploy'): return image_info -def get_pxe_config_file_path(node_uuid): +def get_pxe_config_file_path(node_uuid, ipxe_enabled=False): """Generate the path for the node's PXE configuration file. :param node_uuid: the UUID of the node. + :param ipxe_enabled: A default False boolean value to tell the method + if the caller is using iPXE. :returns: The path to the node's PXE configuration file. """ - return os.path.join(get_root_dir(), node_uuid, 'config') + if ipxe_enabled: + return os.path.join(get_ipxe_root_dir(), node_uuid, 'config') + else: + return os.path.join(get_root_dir(), node_uuid, 'config') -def create_pxe_config(task, pxe_options, template=None): +def create_pxe_config(task, pxe_options, template=None, ipxe_enabled=False): """Generate PXE configuration file and MAC address links for it. This method will generate the PXE configuration file for the task's @@ -231,13 +264,14 @@ def create_pxe_config(task, pxe_options, template=None): """ LOG.debug("Building PXE config for node %s", task.node.uuid) - if template is None: template = deploy_utils.get_pxe_config_template(task.node) - _ensure_config_dirs_exist(task.node.uuid) + _ensure_config_dirs_exist(task, ipxe_enabled) - pxe_config_file_path = get_pxe_config_file_path(task.node.uuid) + pxe_config_file_path = get_pxe_config_file_path( + task.node.uuid, + ipxe_enabled=ipxe_enabled) is_uefi_boot_mode = (boot_mode_utils.get_boot_mode_for_deploy(task.node) == 'uefi') @@ -269,10 +303,10 @@ def create_pxe_config(task, pxe_options, template=None): utils.write_to_file(pxe_config_file_path, pxe_config) # Always write the mac addresses - _link_mac_pxe_configs(task) - if is_uefi_boot_mode and not CONF.pxe.ipxe_enabled: + _link_mac_pxe_configs(task, ipxe_enabled=ipxe_enabled) + if is_uefi_boot_mode and not ipxe_enabled: try: - _link_ip_address_pxe_configs(task, hex_form) + _link_ip_address_pxe_configs(task, hex_form, ipxe_enabled) # NOTE(TheJulia): The IP address support will fail if the # dhcp_provider interface is set to none. This will result # in the MAC addresses and DHCP files being written, and @@ -312,7 +346,9 @@ def clean_up_pxe_config(task): is_uefi_boot_mode = (boot_mode_utils.get_boot_mode_for_deploy(task.node) == 'uefi') - if is_uefi_boot_mode and not CONF.pxe.ipxe_enabled: + ipxe_enabled = is_ipxe_enabled(task) + + if is_uefi_boot_mode and not ipxe_enabled: api = dhcp_factory.DHCPFactory().provider ip_addresses = api.get_ip_addresses(task) if not ip_addresses: @@ -341,12 +377,17 @@ def clean_up_pxe_config(task): client_id = port.extra.get('client-id') # syslinux, ipxe, etc. ironic_utils.unlink_without_raise( - _get_pxe_mac_path(port.address, client_id=client_id)) + _get_pxe_mac_path(port.address, client_id=client_id, + ipxe_enabled=ipxe_enabled)) # Grub2 MAC address based confiuration ironic_utils.unlink_without_raise( _get_pxe_grub_mac_path(port.address)) - utils.rmtree_without_raise(os.path.join(get_root_dir(), - task.node.uuid)) + if ipxe_enabled: + utils.rmtree_without_raise(os.path.join(get_ipxe_root_dir(), + task.node.uuid)) + else: + utils.rmtree_without_raise(os.path.join(get_root_dir(), + task.node.uuid)) def dhcp_options_for_instance(task): @@ -358,7 +399,7 @@ def dhcp_options_for_instance(task): boot_file = deploy_utils.get_pxe_boot_file(task.node) - if CONF.pxe.ipxe_enabled: + if is_ipxe_enabled(task): script_name = os.path.basename(CONF.pxe.ipxe_boot_script) ipxe_script_url = '/'.join([CONF.deploy.http_url, script_name]) dhcp_provider_name = CONF.dhcp.dhcp_provider @@ -441,16 +482,16 @@ def is_ipxe_enabled(task): :returns: boolean true if ``[pxe]ipxe_enabled`` is configured or if the task driver instance is the iPXE driver. """ - # TODO(TheJulia): Due to the order being shuffled of the patches, - # I'm mostly leaving this in place. # NOTE(TheJulia): importutils used here as we seem to get in circular # import weirdness otherwise, specifically when the classes that use # the pxe interface as their parent. - # iPXEBoot = importutils.import_class( - # 'ironic.drivers.modules.ipxe.iPXEBoot') - # return CONF.pxe.ipxe_enabled or isinstance(task.driver.boot, - # iPXEBoot) - return CONF.pxe.ipxe_enabled + # TODO(TheJulia): We should remove this as soon as it is no longer + # required to help us bridge the split of the interfaces and helper + # methods. + iPXEBoot = importutils.import_class( + 'ironic.drivers.modules.ipxe.iPXEBoot') + return CONF.pxe.ipxe_enabled or isinstance(task.driver.boot, + iPXEBoot) def parse_driver_info(node, mode='deploy'): @@ -479,28 +520,33 @@ def parse_driver_info(node, mode='deploy'): return d_info -def get_instance_image_info(node, ctx): +def get_instance_image_info(task, ipxe_enabled=False): """Generate the paths for TFTP files for instance related images. This method generates the paths for instance kernel and instance ramdisk. This method also updates the node, so caller should already have a non-shared lock on the node. - :param node: a node object - :param ctx: context + :param task: A TaskManager instance containing node and context. + :param ipxe_enabled: Default false boolean to indicate if ipxe + is in use by the caller. :returns: a dictionary whose keys are the names of the images (kernel, ramdisk) and values are the absolute paths of them. If it's a whole disk image or node is configured for localboot, it returns an empty dictionary. """ + ctx = task.context + node = task.node image_info = {} # NOTE(pas-ha) do not report image kernel and ramdisk for # local boot or whole disk images so that they are not cached if (node.driver_internal_info.get('is_whole_disk_image') or deploy_utils.get_boot_option(node) == 'local'): return image_info - - root_dir = get_root_dir() + if ipxe_enabled: + root_dir = get_ipxe_root_dir() + else: + root_dir = get_root_dir() i_info = node.instance_info labels = ('kernel', 'ramdisk') d_info = deploy_utils.get_image_instance_info(node) @@ -618,7 +664,8 @@ def build_extra_pxe_options(): 'ipxe_timeout': CONF.pxe.ipxe_timeout * 1000} -def build_pxe_config_options(task, pxe_info, service=False): +def build_pxe_config_options(task, pxe_info, service=False, + ipxe_enabled=False): """Build the PXE config options for a node This method builds the PXE boot options for a node, @@ -632,6 +679,8 @@ def build_pxe_config_options(task, pxe_info, service=False): :param service: if True, build "service mode" pxe config for netboot-ed user image and skip adding deployment image kernel and ramdisk info to PXE options. + :param ipxe_enabled: Default false boolean to indicate if ipxe + is in use by the caller. :returns: A dictionary of pxe options to be used in the pxe bootfile template. """ @@ -640,7 +689,7 @@ def build_pxe_config_options(task, pxe_info, service=False): if service: pxe_options = {} elif (node.driver_internal_info.get('boot_from_volume') - and is_ipxe_enabled(task)): + and ipxe_enabled): pxe_options = get_volume_pxe_options(task) else: pxe_options = build_deploy_pxe_options(task, pxe_info, mode=mode) @@ -659,7 +708,8 @@ def build_pxe_config_options(task, pxe_info, service=False): def build_service_pxe_config(task, instance_image_info, root_uuid_or_disk_id, - ramdisk_boot=False): + ramdisk_boot=False, + ipxe_enabled=False): node = task.node pxe_config_path = get_pxe_config_file_path(node.uuid) # NOTE(pas-ha) if it is takeover of ACTIVE node or node performing @@ -668,17 +718,18 @@ def build_service_pxe_config(task, instance_image_info, if (node.provision_state in [states.ACTIVE, states.UNRESCUING] and not os.path.isfile(pxe_config_path)): pxe_options = build_pxe_config_options(task, instance_image_info, - service=True) + service=True, + ipxe_enabled=ipxe_enabled) pxe_config_template = deploy_utils.get_pxe_config_template(node) - create_pxe_config(task, pxe_options, pxe_config_template) + create_pxe_config(task, pxe_options, pxe_config_template, + ipxe_enabled=ipxe_enabled) iwdi = node.driver_internal_info.get('is_whole_disk_image') deploy_utils.switch_pxe_config( pxe_config_path, root_uuid_or_disk_id, boot_mode_utils.get_boot_mode_for_deploy(node), iwdi, deploy_utils.is_trusted_boot_requested(node), - deploy_utils.is_iscsi_boot(task), ramdisk_boot) - # TODO(TheJulia): Add with ipxe interface - # ipxe_enabled=is_ipxe_enabled(task)) + deploy_utils.is_iscsi_boot(task), ramdisk_boot, + ipxe_enabled=ipxe_enabled) def get_volume_pxe_options(task): @@ -773,7 +824,8 @@ def validate_boot_parameters_for_trusted_boot(node): def prepare_instance_pxe_config(task, image_info, iscsi_boot=False, - ramdisk_boot=False): + ramdisk_boot=False, + ipxe_enabled=False): """Prepares the config file for PXE boot :param task: a task from TaskManager. @@ -781,6 +833,8 @@ def prepare_instance_pxe_config(task, image_info, metadata to set on the configuration file. :param iscsi_boot: if boot is from an iSCSI volume or not. :param ramdisk_boot: if the boot is to a ramdisk configuration. + :param ipxe_enabled: Default false boolean to indicate if ipxe + is in use by the caller. :returns: None """ @@ -792,13 +846,15 @@ def prepare_instance_pxe_config(task, image_info, node.uuid) if not os.path.isfile(pxe_config_path): pxe_options = build_pxe_config_options( - task, image_info, service=ramdisk_boot) + task, image_info, service=ramdisk_boot, + ipxe_enabled=ipxe_enabled) pxe_config_template = ( deploy_utils.get_pxe_config_template(node)) create_pxe_config( - task, pxe_options, pxe_config_template) + task, pxe_options, pxe_config_template, + ipxe_enabled=ipxe_enabled) deploy_utils.switch_pxe_config( pxe_config_path, None, boot_mode_utils.get_boot_mode_for_deploy(node), False, iscsi_boot=iscsi_boot, ramdisk_boot=ramdisk_boot, - ipxe_enabled=is_ipxe_enabled(task)) + ipxe_enabled=ipxe_enabled) diff --git a/ironic/conf/pxe.py b/ironic/conf/pxe.py index 7e00dfc1b4..042046001d 100644 --- a/ironic/conf/pxe.py +++ b/ironic/conf/pxe.py @@ -108,7 +108,15 @@ opts = [ 'For example: aarch64:grubaa64.efi')), cfg.BoolOpt('ipxe_enabled', default=False, - help=_('Enable iPXE boot.')), + help=_('Defaults the PXE interface to only use iPXE.'), + deprecated_for_removal=True, + deprecated_reason=_("This global setting has been " + "superseded by an 'ipxe' boot " + "interface. Set the " + "[default]default_boot_interface " + "to 'ipxe' and/or manually set the node " + "boot interface to 'ipxe' to maintain " + "the same functionality.")), cfg.StrOpt('ipxe_boot_script', default=os.path.join( '$pybasedir', 'drivers/modules/boot.ipxe'), diff --git a/ironic/drivers/generic.py b/ironic/drivers/generic.py index 693c2a84d3..7f9c402815 100644 --- a/ironic/drivers/generic.py +++ b/ironic/drivers/generic.py @@ -21,6 +21,7 @@ from ironic.drivers.modules import agent from ironic.drivers.modules.ansible import deploy as ansible_deploy from ironic.drivers.modules import fake from ironic.drivers.modules import inspector +from ironic.drivers.modules import ipxe from ironic.drivers.modules import iscsi_deploy from ironic.drivers.modules.network import flat as flat_net from ironic.drivers.modules.network import neutron @@ -41,7 +42,7 @@ class GenericHardware(hardware_type.AbstractHardwareType): @property def supported_boot_interfaces(self): """List of supported boot interfaces.""" - return [pxe.PXEBoot] + return [ipxe.iPXEBoot, pxe.PXEBoot] @property def supported_deploy_interfaces(self): diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py index 3443e9daf8..1dbf9c49ad 100644 --- a/ironic/drivers/modules/deploy_utils.py +++ b/ironic/drivers/modules/deploy_utils.py @@ -287,7 +287,7 @@ def _replace_root_uuid(path, root_uuid): def _replace_boot_line(path, boot_mode, is_whole_disk_image, trusted_boot=False, iscsi_boot=False, - ramdisk_boot=False): + ramdisk_boot=False, ipxe_enabled=False): if is_whole_disk_image: boot_disk_type = 'boot_whole_disk' elif trusted_boot: @@ -299,11 +299,11 @@ def _replace_boot_line(path, boot_mode, is_whole_disk_image, else: boot_disk_type = 'boot_partition' - if boot_mode == 'uefi' and not CONF.pxe.ipxe_enabled: + if boot_mode == 'uefi' and not ipxe_enabled: pattern = '^((set )?default)=.*$' boot_line = '\\1=%s' % boot_disk_type else: - pxe_cmd = 'goto' if CONF.pxe.ipxe_enabled else 'default' + pxe_cmd = 'goto' if ipxe_enabled else 'default' pattern = '^%s .*$' % pxe_cmd boot_line = '%s %s' % (pxe_cmd, boot_disk_type) @@ -315,9 +315,11 @@ def _replace_disk_identifier(path, disk_identifier): _replace_lines_in_file(path, pattern, disk_identifier) +# NOTE(TheJulia): This should likely be migrated to pxe_utils. def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode, is_whole_disk_image, trusted_boot=False, - iscsi_boot=False, ramdisk_boot=False): + iscsi_boot=False, ramdisk_boot=False, + ipxe_enabled=False): """Switch a pxe config from deployment mode to service mode. :param path: path to the pxe config file in tftpboot. @@ -330,6 +332,8 @@ def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode, have one or neither, but not both. :param iscsi_boot: if boot is from an iSCSI volume or not. :param ramdisk_boot: if the boot is to be to a ramdisk configuration. + :param ipxe_enabled: A default False boolean value to tell the method + if the caller is using iPXE. """ if not ramdisk_boot: if not is_whole_disk_image: @@ -338,7 +342,7 @@ def switch_pxe_config(path, root_uuid_or_disk_id, boot_mode, _replace_disk_identifier(path, root_uuid_or_disk_id) _replace_boot_line(path, boot_mode, is_whole_disk_image, trusted_boot, - iscsi_boot, ramdisk_boot) + iscsi_boot, ramdisk_boot, ipxe_enabled) def get_dev(address, port, iqn, lun): diff --git a/ironic/drivers/modules/fake.py b/ironic/drivers/modules/fake.py index 9abbe6c247..acb3a59835 100644 --- a/ironic/drivers/modules/fake.py +++ b/ironic/drivers/modules/fake.py @@ -68,6 +68,10 @@ class FakePower(base.PowerInterface): class FakeBoot(base.BootInterface): """Example implementation of a simple boot interface.""" + # NOTE(TheJulia): default capabilities to make unit tests + # happy with the fake boot interface. + capabilities = ['ipxe_boot', 'pxe_boot'] + def get_properties(self): return {} diff --git a/ironic/drivers/modules/ipxe.py b/ironic/drivers/modules/ipxe.py new file mode 100644 index 0000000000..160f7275ad --- /dev/null +++ b/ironic/drivers/modules/ipxe.py @@ -0,0 +1,358 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +""" +iPXE Boot Interface +""" + +from ironic_lib import metrics_utils +from oslo_log import log as logging +from oslo_utils import strutils + +from ironic.common import boot_devices +from ironic.common import dhcp_factory +from ironic.common import exception +from ironic.common.glance_service import service_utils +from ironic.common.i18n import _ +from ironic.common import pxe_utils as common_pxe_utils +from ironic.common import states +from ironic.conductor import utils as manager_utils +from ironic.conf import CONF +from ironic.drivers import base +from ironic.drivers.modules import boot_mode_utils +from ironic.drivers.modules import deploy_utils +from ironic.drivers.modules import pxe +from ironic.drivers import utils as driver_utils +LOG = logging.getLogger(__name__) + +METRICS = metrics_utils.get_metrics_logger(__name__) + +# TODO(TheJulia): Lets rip these out ASAP and move them to a pxe_common. +# One chunk moving at a time for sanity. +REQUIRED_PROPERTIES = { + 'deploy_kernel': _("UUID (from Glance) of the deployment kernel. " + "Required."), + 'deploy_ramdisk': _("UUID (from Glance) of the ramdisk that is " + "mounted at boot time. Required."), +} +OPTIONAL_PROPERTIES = { + 'force_persistent_boot_device': _("True to enable persistent behavior " + "when the boot device is set during " + "deploy and cleaning operations. " + "Defaults to False. Optional."), +} +RESCUE_PROPERTIES = { + 'rescue_kernel': _('UUID (from Glance) of the rescue kernel. This value ' + 'is required for rescue mode.'), + 'rescue_ramdisk': _('UUID (from Glance) of the rescue ramdisk with agent ' + 'that is used at node rescue time. This value is ' + 'required for rescue mode.'), +} +COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() +COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) +COMMON_PROPERTIES.update(RESCUE_PROPERTIES) +# TODO(TheJulia): Use these as the copy to move, no reason to touch pxe.py at +# the same time as doing the initial split out as deduplication goes on. + + +class iPXEBoot(base.BootInterface): + + capabilities = ['iscsi_volume_boot', 'ramdisk_boot', 'ipxe_boot'] + + def __init__(self): + common_pxe_utils.create_ipxe_boot_script() + + def get_properties(self): + """Return the properties of the interface. + + :returns: dictionary of : entries. + """ + # TODO(stendulker): COMMON_PROPERTIES should also include rescue + # related properties (RESCUE_PROPERTIES). We can add them in Rocky, + # when classic drivers get removed. + return COMMON_PROPERTIES + + @METRICS.timer('iPXEBoot.validate') + def validate(self, task): + """Validate the PXE-specific info for booting deploy/instance images. + + This method validates the PXE-specific info for booting the + ramdisk and instance on the node. If invalid, raises an + exception; otherwise returns None. + + :param task: a task from TaskManager. + :returns: None + :raises: InvalidParameterValue, if some parameters are invalid. + :raises: MissingParameterValue, if some required parameters are + missing. + """ + node = task.node + + if not driver_utils.get_node_mac_addresses(task): + raise exception.MissingParameterValue( + _("Node %s does not have any port associated with it.") + % node.uuid) + + if not CONF.deploy.http_url or not CONF.deploy.http_root: + raise exception.MissingParameterValue(_( + "iPXE boot is enabled but no HTTP URL or HTTP " + "root was specified.")) + + # Check the trusted_boot capabilities value. + deploy_utils.validate_capabilities(node) + if deploy_utils.is_trusted_boot_requested(node): + # Check if 'boot_option' and boot mode is compatible with + # trusted boot. + # NOTE(TheJulia): So in theory (huge theory here, not put to + # practice or tested), that one can define the kernel as tboot + # and define the actual kernel and ramdisk as appended data. + # Similar to how one can iPXE load the XEN hypervisor. + # tboot mailing list seem to indicate pxe/ipxe support, or + # more specifically avoiding breaking the scenarios of use, + # but there is also no definitive documentation on the subject. + LOG.warning('Trusted boot has been requested for %(node)s in ' + 'concert with iPXE. This is not a supported ' + 'configuration for an ironic deployment.', + {'node': node.uuid}) + pxe.validate_boot_parameters_for_trusted_boot(node) + + pxe._parse_driver_info(node) + # NOTE(TheJulia): If we're not writing an image, we can skip + # the remainder of this method. + if (not task.driver.storage.should_write_image(task)): + return + + d_info = deploy_utils.get_image_instance_info(node) + if (node.driver_internal_info.get('is_whole_disk_image') + or deploy_utils.get_boot_option(node) == 'local'): + 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('iPXEBoot.prepare_ramdisk') + def prepare_ramdisk(self, task, ramdisk_params): + """Prepares the boot of Ironic ramdisk using PXE. + + This method prepares the boot of the deploy or rescue kernel/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. + pxe driver passes these parameters as kernel command-line + arguments. + :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: 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. + """ + node = task.node + mode = deploy_utils.rescue_or_deploy_mode(node) + + # NOTE(mjturek): At this point, the ipxe boot script should + # already exist as it is created at startup time. However, we + # call the boot script create method here to assert its + # existence and handle the unlikely case that it wasn't created + # or was deleted. + common_pxe_utils.create_ipxe_boot_script() + + dhcp_opts = common_pxe_utils.dhcp_options_for_instance(task) + provider = dhcp_factory.DHCPFactory() + provider.update_dhcp(task, dhcp_opts) + + pxe_info = pxe._get_image_info(node, mode=mode) + + # NODE: Try to validate and fetch instance images only + # if we are in DEPLOYING state. + if node.provision_state == states.DEPLOYING: + pxe_info.update( + pxe._get_instance_image_info(task, ipxe_enabled=True)) + boot_mode_utils.sync_boot_mode(task) + + pxe_options = pxe._build_pxe_config_options(task, pxe_info) + pxe_options.update(ramdisk_params) + + pxe_config_template = deploy_utils.get_pxe_config_template(node) + + common_pxe_utils.create_pxe_config(task, pxe_options, + pxe_config_template, + ipxe_enabled=True) + persistent = strutils.bool_from_string( + node.driver_info.get('force_persistent_boot_device', + False)) + manager_utils.node_set_boot_device(task, boot_devices.PXE, + persistent=persistent) + + if CONF.pxe.ipxe_use_swift: + kernel_label = '%s_kernel' % mode + ramdisk_label = '%s_ramdisk' % mode + pxe_info.pop(kernel_label, None) + pxe_info.pop(ramdisk_label, None) + + if pxe_info: + pxe._cache_ramdisk_kernel(task, pxe_info) + + @METRICS.timer('iPXEBoot.clean_up_ramdisk') + def clean_up_ramdisk(self, task): + """Cleans up the boot of ironic ramdisk. + + This method cleans up the PXE environment that was setup for booting + the deploy or rescue ramdisk. It unlinks the deploy/rescue + kernel/ramdisk in the node's directory in tftproot and removes it's PXE + config. + + :param task: a task from TaskManager. + :param mode: Label indicating a deploy or rescue operation + was carried out on the node. Supported values are 'deploy' and + 'rescue'. Defaults to 'deploy', indicating deploy operation was + carried out. + :returns: None + """ + node = task.node + mode = deploy_utils.rescue_or_deploy_mode(node) + try: + images_info = pxe._get_image_info(node, mode=mode) + except exception.MissingParameterValue as e: + LOG.warning('Could not get %(mode)s image info ' + 'to clean up images for node %(node)s: %(err)s', + {'mode': mode, 'node': node.uuid, 'err': e}) + else: + pxe._clean_up_pxe_env(task, images_info) + + @METRICS.timer('iPXEBoot.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. In case of netboot, + it updates the dhcp entries and switches the PXE config. In case of + localboot, it cleans up the PXE config. + + :param task: a task from TaskManager. + :returns: None + """ + boot_mode_utils.sync_boot_mode(task) + + node = task.node + boot_option = deploy_utils.get_boot_option(node) + boot_device = None + instance_image_info = {} + + if boot_option == "ramdisk": + instance_image_info = pxe._get_instance_image_info( + task, ipxe_enabled=True) + pxe._cache_ramdisk_kernel(task, instance_image_info) + + if deploy_utils.is_iscsi_boot(task) or boot_option == "ramdisk": + pxe._prepare_instance_pxe_config( + task, instance_image_info, + iscsi_boot=deploy_utils.is_iscsi_boot(task), + ramdisk_boot=(boot_option == "ramdisk"), + ipxe_enabled=True) + boot_device = boot_devices.PXE + + elif boot_option != "local": + if task.driver.storage.should_write_image(task): + # Make sure that the instance kernel/ramdisk is cached. + # This is for the takeover scenario for active nodes. + instance_image_info = pxe._get_instance_image_info( + task, ipxe_enabled=True) + pxe._cache_ramdisk_kernel(task, instance_image_info) + + # If it's going to PXE boot we need to update the DHCP server + dhcp_opts = common_pxe_utils.dhcp_options_for_instance(task) + provider = dhcp_factory.DHCPFactory() + provider.update_dhcp(task, dhcp_opts) + + iwdi = task.node.driver_internal_info.get('is_whole_disk_image') + try: + root_uuid_or_disk_id = task.node.driver_internal_info[ + 'root_uuid_or_disk_id' + ] + except KeyError: + if not task.driver.storage.should_write_image(task): + pass + elif not iwdi: + LOG.warning("The UUID for the root partition can't be " + "found, unable to switch the pxe config from " + "deployment mode to service (boot) mode for " + "node %(node)s", {"node": task.node.uuid}) + else: + LOG.warning("The disk id for the whole disk image can't " + "be found, unable to switch the pxe config " + "from deployment mode to service (boot) mode " + "for node %(node)s. Booting the instance " + "from disk.", {"node": task.node.uuid}) + common_pxe_utils.clean_up_pxe_config(task) + boot_device = boot_devices.DISK + else: + pxe._build_service_pxe_config(task, instance_image_info, + root_uuid_or_disk_id, + ipxe_enabled=True) + boot_device = boot_devices.PXE + else: + # If it's going to boot from the local disk, we don't need + # PXE config files. They still need to be generated as part + # of the prepare() because the deployment does PXE boot the + # deploy ramdisk + common_pxe_utils.clean_up_pxe_config(task) + boot_device = boot_devices.DISK + + # NOTE(pas-ha) do not re-set boot device on ACTIVE nodes + # during takeover + if boot_device and task.node.provision_state != states.ACTIVE: + manager_utils.node_set_boot_device(task, boot_device, + persistent=True) + + @METRICS.timer('iPXEBoot.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. It unlinks the instance kernel/ramdisk in node's + directory in tftproot and removes the PXE config. + + :param task: a task from TaskManager. + :returns: None + """ + node = task.node + + try: + images_info = pxe._get_instance_image_info(task, + ipxe_enabled=True) + except exception.MissingParameterValue as e: + LOG.warning('Could not get instance image info ' + 'to clean up images for node %(node)s: %(err)s', + {'node': node.uuid, 'err': e}) + else: + pxe._clean_up_pxe_env(task, images_info) + + @METRICS.timer('iPXEBoot.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 + """ + pxe._parse_driver_info(task.node, mode='rescue') diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py index bbe88f9aa7..ada15727e0 100644 --- a/ironic/drivers/modules/pxe.py +++ b/ironic/drivers/modules/pxe.py @@ -100,10 +100,15 @@ class TFTPImageCache(image_cache.ImageCache): cache_ttl=CONF.pxe.image_cache_ttl * 60) -def _cache_ramdisk_kernel(ctx, node, pxe_info): +def _cache_ramdisk_kernel(task, pxe_info): """Fetch the necessary kernels and ramdisks for the instance.""" - fileutils.ensure_tree( - os.path.join(pxe_utils.get_root_dir(), node.uuid)) + ctx = task.context + node = task.node + if CONF.pxe.ipxe_enabled: + path = os.path.join(pxe_utils.get_ipxe_root_dir(), node.uuid) + else: + path = os.path.join(pxe_utils.get_root_dir(), node.uuid) + fileutils.ensure_tree(path) LOG.debug("Fetching necessary kernel and ramdisk for node %s", node.uuid) deploy_utils.fetch_images(ctx, TFTPImageCache(), list(pxe_info.values()), @@ -131,9 +136,12 @@ def _clean_up_pxe_env(task, images_info): class PXEBoot(base.BootInterface): - capabilities = ['iscsi_volume_boot', 'ramdisk_boot'] + capabilities = ['iscsi_volume_boot', 'ramdisk_boot', 'ipxe_boot', + 'pxe_boot'] def __init__(self): + # TODO(TheJulia): Once the pxe/ipxe interfaces split is complete, + # this can be removed. if CONF.pxe.ipxe_enabled: pxe_utils.create_ipxe_boot_script() @@ -168,6 +176,8 @@ class PXEBoot(base.BootInterface): _("Node %s does not have any port associated with it.") % node.uuid) + # TODO(TheJulia): Once ipxe support is remove from the pxe + # interface, this can be removed. if CONF.pxe.ipxe_enabled: if (not CONF.deploy.http_url or not CONF.deploy.http_root): @@ -242,7 +252,7 @@ class PXEBoot(base.BootInterface): # NODE: Try to validate and fetch instance images only # if we are in DEPLOYING state. if node.provision_state == states.DEPLOYING: - pxe_info.update(_get_instance_image_info(node, task.context)) + pxe_info.update(_get_instance_image_info(task)) boot_mode_utils.sync_boot_mode(task) pxe_options = _build_pxe_config_options(task, pxe_info) @@ -251,7 +261,8 @@ class PXEBoot(base.BootInterface): pxe_config_template = deploy_utils.get_pxe_config_template(node) pxe_utils.create_pxe_config(task, pxe_options, - pxe_config_template) + pxe_config_template, + ipxe_enabled=CONF.pxe.ipxe_enabled) persistent = strutils.bool_from_string( node.driver_info.get('force_persistent_boot_device', False)) @@ -265,7 +276,7 @@ class PXEBoot(base.BootInterface): pxe_info.pop(ramdisk_label, None) if pxe_info: - _cache_ramdisk_kernel(task.context, node, pxe_info) + _cache_ramdisk_kernel(task, pxe_info) @METRICS.timer('PXEBoot.clean_up_ramdisk') def clean_up_ramdisk(self, task): @@ -294,37 +305,6 @@ class PXEBoot(base.BootInterface): else: _clean_up_pxe_env(task, images_info) - def _prepare_instance_pxe_config(self, task, image_info, - iscsi_boot=False, - ramdisk_boot=False): - """Prepares the config file for PXE boot - - :param task: a task from TaskManager. - :param image_info: a dict of values of instance image - metadata to set on the configuration file. - :param iscsi_boot: if boot is from an iSCSI volume or not. - :param ramdisk_boot: if the boot is to a ramdisk configuration. - :returns: None - """ - - node = task.node - dhcp_opts = pxe_utils.dhcp_options_for_instance(task) - provider = dhcp_factory.DHCPFactory() - provider.update_dhcp(task, dhcp_opts) - pxe_config_path = pxe_utils.get_pxe_config_file_path( - node.uuid) - if not os.path.isfile(pxe_config_path): - pxe_options = _build_pxe_config_options( - task, image_info, service=ramdisk_boot) - pxe_config_template = ( - deploy_utils.get_pxe_config_template(node)) - pxe_utils.create_pxe_config( - task, pxe_options, pxe_config_template) - deploy_utils.switch_pxe_config( - pxe_config_path, None, - boot_mode_utils.get_boot_mode_for_deploy(node), False, - iscsi_boot=iscsi_boot, ramdisk_boot=ramdisk_boot) - @METRICS.timer('PXEBoot.prepare_instance') def prepare_instance(self, task): """Prepares the boot of instance. @@ -337,6 +317,7 @@ class PXEBoot(base.BootInterface): :param task: a task from TaskManager. :returns: None """ + ipxe_enabled = CONF.pxe.ipxe_enabled boot_mode_utils.sync_boot_mode(task) node = task.node @@ -344,25 +325,24 @@ class PXEBoot(base.BootInterface): boot_device = None instance_image_info = {} if boot_option == "ramdisk": - instance_image_info = _get_instance_image_info( - task.node, task.context) - _cache_ramdisk_kernel(task.context, task.node, + instance_image_info = _get_instance_image_info(task) + _cache_ramdisk_kernel(task, instance_image_info) if deploy_utils.is_iscsi_boot(task) or boot_option == "ramdisk": - self._prepare_instance_pxe_config( + _prepare_instance_pxe_config( task, instance_image_info, iscsi_boot=deploy_utils.is_iscsi_boot(task), - ramdisk_boot=(boot_option == "ramdisk")) + ramdisk_boot=(boot_option == "ramdisk"), + ipxe_enabled=CONF.pxe.ipxe_enabled) boot_device = boot_devices.PXE elif boot_option != "local": if task.driver.storage.should_write_image(task): # Make sure that the instance kernel/ramdisk is cached. # This is for the takeover scenario for active nodes. - instance_image_info = _get_instance_image_info( - task.node, task.context) - _cache_ramdisk_kernel(task.context, task.node, + instance_image_info = _get_instance_image_info(task) + _cache_ramdisk_kernel(task, instance_image_info) # If it's going to PXE boot we need to update the DHCP server @@ -393,7 +373,8 @@ class PXEBoot(base.BootInterface): boot_device = boot_devices.DISK else: _build_service_pxe_config(task, instance_image_info, - root_uuid_or_disk_id) + root_uuid_or_disk_id, + ipxe_enabled=ipxe_enabled) boot_device = boot_devices.PXE else: # If it's going to boot from the local disk, we don't need @@ -423,7 +404,7 @@ class PXEBoot(base.BootInterface): node = task.node try: - images_info = _get_instance_image_info(node, task.context) + images_info = _get_instance_image_info(task) except exception.MissingParameterValue as e: LOG.warning('Could not get instance image info ' 'to clean up images for node %(node)s: %(err)s', diff --git a/ironic/drivers/modules/storage/cinder.py b/ironic/drivers/modules/storage/cinder.py index 29caa03649..e2a0a7731a 100644 --- a/ironic/drivers/modules/storage/cinder.py +++ b/ironic/drivers/modules/storage/cinder.py @@ -76,7 +76,12 @@ class CinderStorage(base.StorageInterface): iscsi_uuids_found = [] wwpn_found = 0 wwnn_found = 0 - ipxe_enabled = CONF.pxe.ipxe_enabled + ipxe_enabled = False + if 'pxe_boot' in task.driver.boot.capabilities: + if CONF.pxe.ipxe_enabled: + ipxe_enabled = True + elif 'ipxe_boot' in task.driver.boot.capabilities: + ipxe_enabled = True for connector in task.volume_connectors: if (connector.type in VALID_ISCSI_TYPES @@ -84,7 +89,8 @@ class CinderStorage(base.StorageInterface): iscsi_uuids_found.append(connector.uuid) if not ipxe_enabled: msg = _("The [pxe]/ipxe_enabled option must " - "be set to True to support network " + "be set to True or the boot interface " + "must be set to ``ipxe`` to support network " "booting to an iSCSI volume.") self._fail_validation(task, msg) diff --git a/ironic/drivers/modules/storage/external.py b/ironic/drivers/modules/storage/external.py index 87128c1f74..94e1699a47 100644 --- a/ironic/drivers/modules/storage/external.py +++ b/ironic/drivers/modules/storage/external.py @@ -15,6 +15,7 @@ from oslo_log import log from ironic.common import exception from ironic.common.i18n import _ +from ironic.common import pxe_utils as common_pxe_utils from ironic.drivers import base CONF = cfg.CONF @@ -35,10 +36,11 @@ class ExternalStorage(base.StorageInterface): raise exception(msg) if (not self.should_write_image(task) - and not CONF.pxe.ipxe_enabled): + and not common_pxe_utils.is_ipxe_enabled(task)): msg = _("The [pxe]/ipxe_enabled option must " "be set to True to support network " - "booting to an iSCSI volume.") + "booting to an iSCSI volume or the boot " + "interface must be set to ``ipxe``.") _fail_validation(task, msg) def get_properties(self): diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py index edf884b595..1088775960 100644 --- a/ironic/tests/unit/common/test_pxe_utils.py +++ b/ironic/tests/unit/common/test_pxe_utils.py @@ -344,7 +344,7 @@ class TestPXEUtils(db_base.DbTestCase): ] with task_manager.acquire(self.context, self.node.uuid) as task: task.ports = [port_1, port_2] - pxe_utils._link_mac_pxe_configs(task) + pxe_utils._link_mac_pxe_configs(task, ipxe_enabled=True) unlink_mock.assert_has_calls(unlink_calls) create_link_mock.assert_has_calls(create_link_calls) @@ -487,7 +487,7 @@ class TestPXEUtils(db_base.DbTestCase): {'pxe_options': self.pxe_options, 'ROOT': '{{ ROOT }}', 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'}) - link_ip_configs_mock.assert_called_once_with(task, True) + link_ip_configs_mock.assert_called_once_with(task, True, False) pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) write_mock.assert_called_with(pxe_cfg_file_path, @@ -519,7 +519,7 @@ class TestPXEUtils(db_base.DbTestCase): {'pxe_options': self.pxe_options, 'ROOT': '(( ROOT ))', 'DISK_IDENTIFIER': '(( DISK_IDENTIFIER ))'}) - link_ip_configs_mock.assert_called_once_with(task, False) + link_ip_configs_mock.assert_called_once_with(task, False, False) pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) write_mock.assert_called_with(pxe_cfg_file_path, @@ -558,8 +558,9 @@ class TestPXEUtils(db_base.DbTestCase): {'pxe_options': self.pxe_options, 'ROOT': '(( ROOT ))', 'DISK_IDENTIFIER': '(( DISK_IDENTIFIER ))'}) - link_mac_pxe_configs_mock.assert_called_once_with(task) - link_ip_configs_mock.assert_called_once_with(task, False) + link_mac_pxe_configs_mock.assert_called_once_with( + task, ipxe_enabled=False) + link_ip_configs_mock.assert_called_once_with(task, False, False) pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) write_mock.assert_called_with(pxe_cfg_file_path, @@ -578,7 +579,7 @@ class TestPXEUtils(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid) as task: task.node.properties['capabilities'] = 'boot_mode:uefi' pxe_utils.create_pxe_config(task, self.ipxe_options, - ipxe_template) + ipxe_template, ipxe_enabled=True) ensure_calls = [ mock.call(os.path.join(CONF.deploy.http_root, self.node.uuid)), @@ -591,9 +592,10 @@ class TestPXEUtils(db_base.DbTestCase): {'pxe_options': self.ipxe_options, 'ROOT': '{{ ROOT }}', 'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}'}) - link_mac_pxe_mock.assert_called_once_with(task) + link_mac_pxe_mock.assert_called_once_with(task, ipxe_enabled=True) - pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path(self.node.uuid) + pxe_cfg_file_path = pxe_utils.get_pxe_config_file_path( + self.node.uuid, ipxe_enabled=True) write_mock.assert_called_with(pxe_cfg_file_path, render_mock.return_value) @@ -672,11 +674,10 @@ class TestPXEUtils(db_base.DbTestCase): pxe_utils._get_pxe_mac_path(mac)) def test__get_pxe_mac_path_ipxe(self): - self.config(ipxe_enabled=True, group='pxe') self.config(http_root='/httpboot', group='deploy') mac = '00:11:22:33:AA:BB:CC' self.assertEqual('/httpboot/pxelinux.cfg/00-11-22-33-aa-bb-cc', - pxe_utils._get_pxe_mac_path(mac)) + pxe_utils._get_pxe_mac_path(mac, ipxe_enabled=True)) def test__get_pxe_ip_address_path(self): ipaddress = '10.10.0.1' diff --git a/ironic/tests/unit/drivers/modules/test_deploy_utils.py b/ironic/tests/unit/drivers/modules/test_deploy_utils.py index fa345cf03b..4fe516d78b 100644 --- a/ironic/tests/unit/drivers/modules/test_deploy_utils.py +++ b/ironic/tests/unit/drivers/modules/test_deploy_utils.py @@ -874,24 +874,24 @@ class SwitchPxeConfigTestCase(tests_base.TestCase): def test_switch_ipxe_config_partition_image(self): boot_mode = 'bios' - cfg.CONF.set_override('ipxe_enabled', True, 'pxe') fname = self._create_config(ipxe=True) utils.switch_pxe_config(fname, '12345678-1234-1234-1234-1234567890abcdef', boot_mode, - False) + False, + ipxe_enabled=True) with open(fname, 'r') as f: pxeconf = f.read() self.assertEqual(_IPXECONF_BOOT_PARTITION, pxeconf) def test_switch_ipxe_config_whole_disk_image(self): boot_mode = 'bios' - cfg.CONF.set_override('ipxe_enabled', True, 'pxe') fname = self._create_config(ipxe=True) utils.switch_pxe_config(fname, '0x12345678', boot_mode, - True) + True, + ipxe_enabled=True) with open(fname, 'r') as f: pxeconf = f.read() self.assertEqual(_IPXECONF_BOOT_WHOLE_DISK, pxeconf) @@ -946,36 +946,36 @@ class SwitchPxeConfigTestCase(tests_base.TestCase): def test_switch_uefi_ipxe_config_partition_image(self): boot_mode = 'uefi' - cfg.CONF.set_override('ipxe_enabled', True, 'pxe') fname = self._create_config(boot_mode=boot_mode, ipxe=True) utils.switch_pxe_config(fname, '12345678-1234-1234-1234-1234567890abcdef', boot_mode, - False) + False, + ipxe_enabled=True) with open(fname, 'r') as f: pxeconf = f.read() self.assertEqual(_IPXECONF_BOOT_PARTITION, pxeconf) def test_switch_uefi_ipxe_config_whole_disk_image(self): boot_mode = 'uefi' - cfg.CONF.set_override('ipxe_enabled', True, 'pxe') fname = self._create_config(boot_mode=boot_mode, ipxe=True) utils.switch_pxe_config(fname, '0x12345678', boot_mode, - True) + True, + ipxe_enabled=True) with open(fname, 'r') as f: pxeconf = f.read() self.assertEqual(_IPXECONF_BOOT_WHOLE_DISK, pxeconf) def test_switch_ipxe_iscsi_boot(self): boot_mode = 'iscsi' - cfg.CONF.set_override('ipxe_enabled', True, 'pxe') fname = self._create_config(boot_mode=boot_mode, ipxe=True) utils.switch_pxe_config(fname, '0x12345678', boot_mode, - False, False, True) + False, False, True, + ipxe_enabled=True) with open(fname, 'r') as f: pxeconf = f.read() self.assertEqual(_IPXECONF_BOOT_ISCSI_NO_CONFIG, pxeconf) diff --git a/ironic/tests/unit/drivers/modules/test_ipxe.py b/ironic/tests/unit/drivers/modules/test_ipxe.py new file mode 100644 index 0000000000..64108b1e6d --- /dev/null +++ b/ironic/tests/unit/drivers/modules/test_ipxe.py @@ -0,0 +1,824 @@ +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Test class for iPXE driver.""" + +import os + +import mock +from oslo_config import cfg +from oslo_serialization import jsonutils as json +from oslo_utils import uuidutils + +from ironic.common import boot_devices +from ironic.common import boot_modes +from ironic.common import dhcp_factory +from ironic.common import exception +from ironic.common.glance_service import base_image_service +from ironic.common import pxe_utils +from ironic.common import states +from ironic.common import utils as common_utils +from ironic.conductor import task_manager +from ironic.conductor import utils as manager_utils +from ironic.drivers import base as drivers_base +from ironic.drivers.modules import agent_base_vendor +from ironic.drivers.modules import deploy_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.tests.unit.db import base as db_base +from ironic.tests.unit.db import utils as db_utils +from ironic.tests.unit.objects import utils as obj_utils + +CONF = cfg.CONF + +INST_INFO_DICT = db_utils.get_test_pxe_instance_info() +DRV_INFO_DICT = db_utils.get_test_pxe_driver_info() +DRV_INTERNAL_INFO_DICT = db_utils.get_test_pxe_driver_internal_info() + + +# NOTE(TheJulia): This code is essentially a bulk copy of the +# test_pxe file with some contextual modifications to enforce +# use of ipxe while also explicitly having it globally disabled +# in the conductor. +@mock.patch.object(ipxe.iPXEBoot, '__init__', lambda self: None) +class iPXEBootTestCase(db_base.DbTestCase): + + driver = 'fake-hardware' + boot_interface = 'ipxe' + driver_info = DRV_INFO_DICT + driver_internal_info = DRV_INTERNAL_INFO_DICT + + def setUp(self): + super(iPXEBootTestCase, self).setUp() + self.context.auth_token = 'fake' + self.config_temp_dir('tftp_root', group='pxe') + self.config_temp_dir('images_path', group='pxe') + self.config_temp_dir('http_root', group='deploy') + self.config(group='deploy', http_url='http://myserver') + instance_info = INST_INFO_DICT + + self.config(enabled_boot_interfaces=[self.boot_interface, + 'ipxe', 'fake']) + self.node = obj_utils.create_test_node( + self.context, + driver=self.driver, + boot_interface=self.boot_interface, + # Avoid fake properties in get_properties() output + vendor_interface='no-vendor', + instance_info=instance_info, + driver_info=self.driver_info, + driver_internal_info=self.driver_internal_info) + self.port = obj_utils.create_test_port(self.context, + node_id=self.node.id) + self.config(group='conductor', api_url='http://127.0.0.1:1234/') + + def test_get_properties(self): + expected = ipxe.COMMON_PROPERTIES + expected.update(agent_base_vendor.VENDOR_PROPERTIES) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertEqual(expected, task.driver.get_properties()) + + @mock.patch.object(base_image_service.BaseImageService, '_show', + autospec=True) + def test_validate_good(self, mock_glance): + mock_glance.return_value = {'properties': {'kernel_id': 'fake-kernel', + 'ramdisk_id': 'fake-initr'}} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.boot.validate(task) + + @mock.patch.object(base_image_service.BaseImageService, '_show', + autospec=True) + def test_validate_good_whole_disk_image(self, mock_glance): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_internal_info['is_whole_disk_image'] = True + task.driver.boot.validate(task) + + @mock.patch.object(base_image_service.BaseImageService, '_show', + autospec=True) + @mock.patch.object(noop_storage.NoopStorage, 'should_write_image', + autospec=True) + def test_validate_skip_check_write_image_false(self, mock_write, + mock_glance): + mock_write.return_value = False + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.boot.validate(task) + self.assertFalse(mock_glance.called) + + def test_validate_fail_missing_deploy_kernel(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + del task.node.driver_info['deploy_kernel'] + self.assertRaises(exception.MissingParameterValue, + task.driver.boot.validate, task) + + def test_validate_fail_missing_deploy_ramdisk(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + del task.node.driver_info['deploy_ramdisk'] + self.assertRaises(exception.MissingParameterValue, + task.driver.boot.validate, task) + + def test_validate_fail_missing_image_source(self): + info = dict(INST_INFO_DICT) + del info['image_source'] + self.node.instance_info = json.dumps(info) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node['instance_info'] = json.dumps(info) + self.assertRaises(exception.MissingParameterValue, + task.driver.boot.validate, task) + + def test_validate_fail_no_port(self): + new_node = obj_utils.create_test_node( + self.context, + uuid='aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + driver=self.driver, boot_interface=self.boot_interface, + instance_info=INST_INFO_DICT, driver_info=DRV_INFO_DICT) + with task_manager.acquire(self.context, new_node.uuid, + shared=True) as task: + self.assertRaises(exception.MissingParameterValue, + task.driver.boot.validate, task) + + def test_validate_fail_trusted_boot_with_secure_boot(self): + instance_info = {"boot_option": "netboot", + "secure_boot": "true", + "trusted_boot": "true"} + properties = {'capabilities': 'trusted_boot:true'} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.instance_info['capabilities'] = instance_info + task.node.properties = properties + task.node.driver_internal_info['is_whole_disk_image'] = False + self.assertRaises(exception.InvalidParameterValue, + task.driver.boot.validate, task) + + def test_validate_fail_invalid_trusted_boot_value(self): + properties = {'capabilities': 'trusted_boot:value'} + instance_info = {"trusted_boot": "value"} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.properties = properties + task.node.instance_info['capabilities'] = instance_info + self.assertRaises(exception.InvalidParameterValue, + task.driver.boot.validate, task) + + @mock.patch.object(base_image_service.BaseImageService, '_show', + autospec=True) + def test_validate_fail_no_image_kernel_ramdisk_props(self, mock_glance): + mock_glance.return_value = {'properties': {}} + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.MissingParameterValue, + task.driver.boot.validate, + task) + + @mock.patch.object(base_image_service.BaseImageService, '_show', + autospec=True) + def test_validate_fail_glance_image_doesnt_exists(self, mock_glance): + mock_glance.side_effect = exception.ImageNotFound('not found') + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.boot.validate, task) + + @mock.patch.object(base_image_service.BaseImageService, '_show', + autospec=True) + def test_validate_fail_glance_conn_problem(self, mock_glance): + exceptions = (exception.GlanceConnectionFailed('connection fail'), + exception.ImageNotAuthorized('not authorized'), + exception.Invalid('invalid')) + mock_glance.side_effect = exceptions + for exc in exceptions: + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + self.assertRaises(exception.InvalidParameterValue, + task.driver.boot.validate, task) + + # TODO(TheJulia): Many of the interfaces mocked below are private PXE + # interface methods. As time progresses, these will need to be migrated + # and refactored as we begin to separate PXE and iPXE interfaces. + @mock.patch.object(manager_utils, 'node_get_boot_mode', autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + @mock.patch.object(dhcp_factory, 'DHCPFactory') + @mock.patch.object(pxe, '_get_instance_image_info', autospec=True) + @mock.patch.object(pxe, '_get_image_info', autospec=True) + @mock.patch.object(pxe, '_cache_ramdisk_kernel', autospec=True) + @mock.patch.object(pxe, '_build_pxe_config_options', autospec=True) + @mock.patch.object(pxe_utils, 'create_pxe_config', autospec=True) + def _test_prepare_ramdisk(self, mock_pxe_config, + mock_build_pxe, mock_cache_r_k, + mock_deploy_img_info, + mock_instance_img_info, + dhcp_factory_mock, + set_boot_device_mock, + get_boot_mode_mock, + uefi=False, + cleaning=False, + ipxe_use_swift=False, + whole_disk_image=False, + mode='deploy', + node_boot_mode=None): + mock_build_pxe.return_value = {} + kernel_label = '%s_kernel' % mode + ramdisk_label = '%s_ramdisk' % mode + mock_deploy_img_info.return_value = {kernel_label: 'a', + ramdisk_label: 'r'} + if whole_disk_image: + mock_instance_img_info.return_value = {} + else: + mock_instance_img_info.return_value = {'kernel': 'b'} + mock_pxe_config.return_value = None + mock_cache_r_k.return_value = None + provider_mock = mock.MagicMock() + dhcp_factory_mock.return_value = provider_mock + get_boot_mode_mock.return_value = node_boot_mode + driver_internal_info = self.node.driver_internal_info + driver_internal_info['is_whole_disk_image'] = whole_disk_image + self.node.driver_internal_info = driver_internal_info + if mode == 'rescue': + mock_deploy_img_info.return_value = { + 'rescue_kernel': 'a', + 'rescue_ramdisk': 'r'} + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + task.driver.boot.prepare_ramdisk(task, {'foo': 'bar'}) + mock_deploy_img_info.assert_called_once_with(task.node, mode=mode) + provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) + if self.node.provision_state == states.DEPLOYING: + get_boot_mode_mock.assert_called_once_with(task) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.PXE, + persistent=False) + if ipxe_use_swift: + if whole_disk_image: + self.assertFalse(mock_cache_r_k.called) + else: + mock_cache_r_k.assert_called_once_with( + task, {'kernel': 'b'}) + mock_instance_img_info.assert_called_once_with( + task, ipxe_enabled=True) + elif not cleaning and mode == 'deploy': + mock_cache_r_k.assert_called_once_with( + task, {'deploy_kernel': 'a', 'deploy_ramdisk': 'r', + 'kernel': 'b'}) + mock_instance_img_info.assert_called_once_with( + task, ipxe_enabled=True) + elif mode == 'deploy': + mock_cache_r_k.assert_called_once_with( + task, {'deploy_kernel': 'a', 'deploy_ramdisk': 'r'}) + elif mode == 'rescue': + mock_cache_r_k.assert_called_once_with( + task, {'rescue_kernel': 'a', 'rescue_ramdisk': 'r'}) + if uefi: + mock_pxe_config.assert_called_once_with( + task, {'foo': 'bar'}, CONF.pxe.uefi_pxe_config_template, + ipxe_enabled=True) + else: + mock_pxe_config.assert_called_once_with( + task, {'foo': 'bar'}, CONF.pxe.pxe_config_template, + ipxe_enabled=True) + + def test_prepare_ramdisk(self): + self.node.provision_state = states.DEPLOYING + self.node.save() + self._test_prepare_ramdisk() + + def test_prepare_ramdisk_rescue(self): + self.node.provision_state = states.RESCUING + self.node.save() + self._test_prepare_ramdisk(mode='rescue') + + def test_prepare_ramdisk_uefi(self): + self.node.provision_state = states.DEPLOYING + self.node.save() + properties = self.node.properties + properties['capabilities'] = 'boot_mode:uefi' + self.node.properties = properties + self.node.save() + self._test_prepare_ramdisk(uefi=True) + + @mock.patch.object(os.path, 'isfile', lambda path: True) + @mock.patch.object(common_utils, 'file_has_content', lambda *args: False) + @mock.patch('ironic.common.utils.write_to_file', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) + def test_prepare_ramdisk_ipxe_with_copy_file_different( + self, render_mock, write_mock): + self.node.provision_state = states.DEPLOYING + self.node.save() + self.config(group='pxe', ipxe_enabled=False) + render_mock.return_value = 'foo' + self._test_prepare_ramdisk() + write_mock.assert_called_once_with( + os.path.join( + CONF.deploy.http_root, + os.path.basename(CONF.pxe.ipxe_boot_script)), + 'foo') + render_mock.assert_called_once_with( + CONF.pxe.ipxe_boot_script, + {'ipxe_for_mac_uri': 'pxelinux.cfg/'}) + + @mock.patch.object(os.path, 'isfile', lambda path: False) + @mock.patch('ironic.common.utils.file_has_content', autospec=True) + @mock.patch('ironic.common.utils.write_to_file', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) + def test_prepare_ramdisk_ipxe_with_copy_no_file( + self, render_mock, write_mock, file_has_content_mock): + self.node.provision_state = states.DEPLOYING + self.node.save() + self.config(group='pxe', ipxe_enabled=False) + render_mock.return_value = 'foo' + self._test_prepare_ramdisk() + self.assertFalse(file_has_content_mock.called) + write_mock.assert_called_once_with( + os.path.join( + CONF.deploy.http_root, + os.path.basename(CONF.pxe.ipxe_boot_script)), + 'foo') + render_mock.assert_called_once_with( + CONF.pxe.ipxe_boot_script, + {'ipxe_for_mac_uri': 'pxelinux.cfg/'}) + + @mock.patch.object(os.path, 'isfile', lambda path: True) + @mock.patch.object(common_utils, 'file_has_content', lambda *args: True) + @mock.patch('ironic.common.utils.write_to_file', autospec=True) + @mock.patch('ironic.common.utils.render_template', autospec=True) + def test_prepare_ramdisk_ipxe_without_copy( + self, render_mock, write_mock): + self.node.provision_state = states.DEPLOYING + self.node.save() + self.config(group='pxe', ipxe_enabled=False) + self._test_prepare_ramdisk() + self.assertFalse(write_mock.called) + + @mock.patch.object(common_utils, 'render_template', lambda *args: 'foo') + @mock.patch('ironic.common.utils.write_to_file', autospec=True) + def test_prepare_ramdisk_ipxe_swift(self, write_mock): + self.node.provision_state = states.DEPLOYING + self.node.save() + self.config(group='pxe', ipxe_enabled=False) + self.config(group='pxe', ipxe_use_swift=True) + self._test_prepare_ramdisk(ipxe_use_swift=True) + write_mock.assert_called_once_with( + os.path.join( + CONF.deploy.http_root, + os.path.basename(CONF.pxe.ipxe_boot_script)), + 'foo') + + @mock.patch.object(common_utils, 'render_template', lambda *args: 'foo') + @mock.patch('ironic.common.utils.write_to_file', autospec=True) + def test_prepare_ramdisk_ipxe_swift_whole_disk_image( + self, write_mock): + self.node.provision_state = states.DEPLOYING + self.node.save() + self.config(group='pxe', ipxe_enabled=False) + self.config(group='pxe', ipxe_use_swift=True) + self._test_prepare_ramdisk(ipxe_use_swift=True, whole_disk_image=True) + write_mock.assert_called_once_with( + os.path.join( + CONF.deploy.http_root, + os.path.basename(CONF.pxe.ipxe_boot_script)), + 'foo') + + def test_prepare_ramdisk_cleaning(self): + self.node.provision_state = states.CLEANING + self.node.save() + self._test_prepare_ramdisk(cleaning=True) + + @mock.patch.object(manager_utils, 'node_set_boot_mode', autospec=True) + def test_prepare_ramdisk_set_boot_mode_on_bm( + self, set_boot_mode_mock): + self.node.provision_state = states.DEPLOYING + properties = self.node.properties + properties['capabilities'] = 'boot_mode:uefi' + self.node.properties = properties + self.node.save() + self._test_prepare_ramdisk(uefi=True) + set_boot_mode_mock.assert_called_once_with(mock.ANY, boot_modes.UEFI) + + @mock.patch.object(manager_utils, 'node_set_boot_mode', autospec=True) + def test_prepare_ramdisk_set_boot_mode_on_ironic( + self, set_boot_mode_mock): + self.node.provision_state = states.DEPLOYING + self.node.save() + self._test_prepare_ramdisk(node_boot_mode=boot_modes.LEGACY_BIOS) + + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_internal_info = task.node.driver_internal_info + self.assertIn('deploy_boot_mode', driver_internal_info) + self.assertEqual(boot_modes.LEGACY_BIOS, + driver_internal_info['deploy_boot_mode']) + self.assertEqual(set_boot_mode_mock.call_count, 0) + + @mock.patch.object(manager_utils, 'node_set_boot_mode', autospec=True) + def test_prepare_ramdisk_set_default_boot_mode_on_ironic_bios( + self, set_boot_mode_mock): + self.node.provision_state = states.DEPLOYING + self.node.save() + + self.config(default_boot_mode=boot_modes.LEGACY_BIOS, group='deploy') + + self._test_prepare_ramdisk() + + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_internal_info = task.node.driver_internal_info + self.assertIn('deploy_boot_mode', driver_internal_info) + self.assertEqual(boot_modes.LEGACY_BIOS, + driver_internal_info['deploy_boot_mode']) + self.assertEqual(set_boot_mode_mock.call_count, 1) + + @mock.patch.object(manager_utils, 'node_set_boot_mode', autospec=True) + def test_prepare_ramdisk_set_default_boot_mode_on_ironic_uefi( + self, set_boot_mode_mock): + self.node.provision_state = states.DEPLOYING + self.node.save() + + self.config(default_boot_mode=boot_modes.UEFI, group='deploy') + + self._test_prepare_ramdisk(uefi=True) + + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_internal_info = task.node.driver_internal_info + self.assertIn('deploy_boot_mode', driver_internal_info) + self.assertEqual(boot_modes.UEFI, + driver_internal_info['deploy_boot_mode']) + self.assertEqual(set_boot_mode_mock.call_count, 1) + + @mock.patch.object(manager_utils, 'node_set_boot_mode', autospec=True) + def test_prepare_ramdisk_conflicting_boot_modes( + self, set_boot_mode_mock): + self.node.provision_state = states.DEPLOYING + properties = self.node.properties + properties['capabilities'] = 'boot_mode:uefi' + self.node.properties = properties + self.node.save() + self._test_prepare_ramdisk(uefi=True, + node_boot_mode=boot_modes.LEGACY_BIOS) + set_boot_mode_mock.assert_called_once_with(mock.ANY, boot_modes.UEFI) + + @mock.patch.object(manager_utils, 'node_set_boot_mode', autospec=True) + def test_prepare_ramdisk_conflicting_boot_modes_set_unsupported( + self, set_boot_mode_mock): + self.node.provision_state = states.DEPLOYING + properties = self.node.properties + properties['capabilities'] = 'boot_mode:uefi' + self.node.properties = properties + self.node.save() + set_boot_mode_mock.side_effect = exception.UnsupportedDriverExtension( + extension='management', driver='test-driver' + ) + self.assertRaises(exception.UnsupportedDriverExtension, + self._test_prepare_ramdisk, + uefi=True, node_boot_mode=boot_modes.LEGACY_BIOS) + + @mock.patch.object(manager_utils, 'node_set_boot_mode', autospec=True) + def test_prepare_ramdisk_set_boot_mode_not_called( + self, set_boot_mode_mock): + self.node.provision_state = states.DEPLOYING + self.node.save() + properties = self.node.properties + properties['capabilities'] = 'boot_mode:uefi' + self.node.properties = properties + self.node.save() + self._test_prepare_ramdisk(uefi=True, node_boot_mode=boot_modes.UEFI) + self.assertEqual(set_boot_mode_mock.call_count, 0) + + @mock.patch.object(pxe, '_clean_up_pxe_env', autospec=True) + @mock.patch.object(pxe, '_get_image_info', autospec=True) + def _test_clean_up_ramdisk(self, get_image_info_mock, + clean_up_pxe_env_mock, mode='deploy'): + with task_manager.acquire(self.context, self.node.uuid) as task: + kernel_label = '%s_kernel' % mode + ramdisk_label = '%s_ramdisk' % mode + image_info = {kernel_label: ['', '/path/to/' + kernel_label], + ramdisk_label: ['', '/path/to/' + ramdisk_label]} + get_image_info_mock.return_value = image_info + task.driver.boot.clean_up_ramdisk(task) + clean_up_pxe_env_mock.assert_called_once_with(task, image_info) + get_image_info_mock.assert_called_once_with(task.node, mode=mode) + + def test_clean_up_ramdisk(self): + self.node.provision_state = states.DEPLOYING + self.node.save() + self._test_clean_up_ramdisk() + + def test_clean_up_ramdisk_rescue(self): + self.node.provision_state = states.RESCUING + self.node.save() + self._test_clean_up_ramdisk(mode='rescue') + + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + @mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True) + @mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True) + @mock.patch.object(pxe, '_cache_ramdisk_kernel', autospec=True) + @mock.patch.object(pxe, '_get_instance_image_info', autospec=True) + def test_prepare_instance_netboot( + self, get_image_info_mock, cache_mock, + dhcp_factory_mock, switch_pxe_config_mock, + set_boot_device_mock): + provider_mock = mock.MagicMock() + dhcp_factory_mock.return_value = provider_mock + image_info = {'kernel': ('', '/path/to/kernel'), + 'ramdisk': ('', '/path/to/ramdisk')} + get_image_info_mock.return_value = image_info + with task_manager.acquire(self.context, self.node.uuid) as task: + dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + pxe_config_path = pxe_utils.get_pxe_config_file_path( + task.node.uuid) + task.node.properties['capabilities'] = 'boot_mode:bios' + task.node.driver_internal_info['root_uuid_or_disk_id'] = ( + "30212642-09d3-467f-8e09-21685826ab50") + task.node.driver_internal_info['is_whole_disk_image'] = False + + task.driver.boot.prepare_instance(task) + + get_image_info_mock.assert_called_once_with( + task, ipxe_enabled=True) + cache_mock.assert_called_once_with(task, image_info) + provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) + switch_pxe_config_mock.assert_called_once_with( + pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", + 'bios', False, False, False, False, ipxe_enabled=True) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.PXE, + persistent=True) + + @mock.patch('os.path.isfile', return_value=False) + @mock.patch.object(pxe_utils, 'create_pxe_config', autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + @mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True) + @mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True) + @mock.patch.object(pxe, '_cache_ramdisk_kernel', autospec=True) + @mock.patch.object(pxe, '_get_instance_image_info', autospec=True) + def test_prepare_instance_netboot_active( + self, get_image_info_mock, cache_mock, + dhcp_factory_mock, switch_pxe_config_mock, + set_boot_device_mock, create_pxe_config_mock, isfile_mock): + provider_mock = mock.MagicMock() + dhcp_factory_mock.return_value = provider_mock + image_info = {'kernel': ('', '/path/to/kernel'), + 'ramdisk': ('', '/path/to/ramdisk')} + get_image_info_mock.return_value = image_info + self.node.provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + pxe_config_path = pxe_utils.get_pxe_config_file_path( + task.node.uuid) + task.node.properties['capabilities'] = 'boot_mode:bios' + task.node.driver_internal_info['root_uuid_or_disk_id'] = ( + "30212642-09d3-467f-8e09-21685826ab50") + task.node.driver_internal_info['is_whole_disk_image'] = False + + task.driver.boot.prepare_instance(task) + + get_image_info_mock.assert_called_once_with( + task, ipxe_enabled=True) + cache_mock.assert_called_once_with(task, image_info) + provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) + create_pxe_config_mock.assert_called_once_with( + task, mock.ANY, CONF.pxe.pxe_config_template, + ipxe_enabled=True) + switch_pxe_config_mock.assert_called_once_with( + pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", + 'bios', False, False, False, False, ipxe_enabled=True) + self.assertFalse(set_boot_device_mock.called) + + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + @mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True) + @mock.patch.object(dhcp_factory, 'DHCPFactory') + @mock.patch.object(pxe, '_cache_ramdisk_kernel', autospec=True) + @mock.patch.object(pxe, '_get_instance_image_info', autospec=True) + def test_prepare_instance_netboot_missing_root_uuid( + self, get_image_info_mock, cache_mock, + dhcp_factory_mock, switch_pxe_config_mock, + set_boot_device_mock): + provider_mock = mock.MagicMock() + dhcp_factory_mock.return_value = provider_mock + image_info = {'kernel': ('', '/path/to/kernel'), + 'ramdisk': ('', '/path/to/ramdisk')} + get_image_info_mock.return_value = image_info + with task_manager.acquire(self.context, self.node.uuid) as task: + dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + task.node.properties['capabilities'] = 'boot_mode:bios' + task.node.driver_internal_info['is_whole_disk_image'] = False + + task.driver.boot.prepare_instance(task) + + get_image_info_mock.assert_called_once_with( + task, ipxe_enabled=True) + cache_mock.assert_called_once_with(task, image_info) + provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) + self.assertFalse(switch_pxe_config_mock.called) + self.assertFalse(set_boot_device_mock.called) + + # NOTE(TheJulia): The log mock below is attached to the iPXE interface + # which directly logs the warning that is being checked for. + @mock.patch.object(ipxe.LOG, 'warning', autospec=True) + @mock.patch.object(pxe_utils, 'clean_up_pxe_config', autospec=True) + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + @mock.patch.object(dhcp_factory, 'DHCPFactory') + @mock.patch.object(pxe, '_cache_ramdisk_kernel', autospec=True) + @mock.patch.object(pxe, '_get_instance_image_info', autospec=True) + def test_prepare_instance_whole_disk_image_missing_root_uuid( + self, get_image_info_mock, cache_mock, + dhcp_factory_mock, set_boot_device_mock, + clean_up_pxe_mock, log_mock): + provider_mock = mock.MagicMock() + dhcp_factory_mock.return_value = provider_mock + get_image_info_mock.return_value = {} + with task_manager.acquire(self.context, self.node.uuid) as task: + dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + task.node.properties['capabilities'] = 'boot_mode:bios' + task.node.driver_internal_info['is_whole_disk_image'] = True + task.driver.boot.prepare_instance(task) + get_image_info_mock.assert_called_once_with( + task, ipxe_enabled=True) + cache_mock.assert_called_once_with(task, {}) + provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) + self.assertTrue(log_mock.called) + clean_up_pxe_mock.assert_called_once_with(task) + set_boot_device_mock.assert_called_once_with( + task, boot_devices.DISK, persistent=True) + + @mock.patch('os.path.isfile', lambda filename: False) + @mock.patch.object(pxe_utils, 'create_pxe_config', autospec=True) + @mock.patch.object(deploy_utils, 'is_iscsi_boot', lambda task: True) + @mock.patch.object(noop_storage.NoopStorage, 'should_write_image', + lambda task: False) + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + @mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True) + @mock.patch.object(dhcp_factory, 'DHCPFactory', autospec=True) + @mock.patch.object(pxe, '_cache_ramdisk_kernel', autospec=True) + @mock.patch.object(pxe, '_get_instance_image_info', autospec=True) + def test_prepare_instance_netboot_iscsi( + self, get_image_info_mock, cache_mock, + dhcp_factory_mock, switch_pxe_config_mock, + set_boot_device_mock, create_pxe_config_mock): + http_url = 'http://192.1.2.3:1234' + self.config(ipxe_enabled=False, group='pxe') + self.config(http_url=http_url, group='deploy') + provider_mock = mock.MagicMock() + dhcp_factory_mock.return_value = provider_mock + vol_id = uuidutils.generate_uuid() + obj_utils.create_test_volume_target( + self.context, node_id=self.node.id, volume_type='iscsi', + boot_index=0, volume_id='1234', uuid=vol_id, + properties={'target_lun': 0, + 'target_portal': 'fake_host:3260', + 'target_iqn': 'fake_iqn', + 'auth_username': 'fake_username', + 'auth_password': 'fake_password'}) + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.driver_internal_info = { + 'boot_from_volume': vol_id} + dhcp_opts = pxe_utils.dhcp_options_for_instance(task) + pxe_config_path = pxe_utils.get_pxe_config_file_path( + task.node.uuid) + task.node.properties['capabilities'] = 'boot_mode:bios' + task.driver.boot.prepare_instance(task) + self.assertFalse(get_image_info_mock.called) + self.assertFalse(cache_mock.called) + provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) + create_pxe_config_mock.assert_called_once_with( + task, mock.ANY, CONF.pxe.pxe_config_template, + ipxe_enabled=True) + switch_pxe_config_mock.assert_called_once_with( + pxe_config_path, None, boot_modes.LEGACY_BIOS, False, + ipxe_enabled=True, iscsi_boot=True, ramdisk_boot=False) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.PXE, + persistent=True) + + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + @mock.patch.object(pxe_utils, 'clean_up_pxe_config', autospec=True) + def test_prepare_instance_localboot(self, clean_up_pxe_config_mock, + set_boot_device_mock): + with task_manager.acquire(self.context, self.node.uuid) as task: + instance_info = task.node.instance_info + instance_info['capabilities'] = {'boot_option': 'local'} + task.node.instance_info = instance_info + task.node.save() + task.driver.boot.prepare_instance(task) + clean_up_pxe_config_mock.assert_called_once_with(task) + set_boot_device_mock.assert_called_once_with(task, + boot_devices.DISK, + persistent=True) + + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + @mock.patch.object(pxe_utils, 'clean_up_pxe_config', autospec=True) + def test_is_force_persistent_boot_device_enabled( + self, clean_up_pxe_config_mock, set_boot_device_mock): + with task_manager.acquire(self.context, self.node.uuid) as task: + instance_info = task.node.instance_info + instance_info['capabilities'] = {'boot_option': 'local'} + task.node.instance_info = instance_info + task.node.save() + task.driver.boot.prepare_instance(task) + clean_up_pxe_config_mock.assert_called_once_with(task) + driver_info = task.node.driver_info + driver_info['force_persistent _boot_device'] = True + task.node.driver_info = driver_info + set_boot_device_mock.assert_called_once_with(task, + boot_devices.DISK, + persistent=True) + + @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) + @mock.patch.object(pxe_utils, 'clean_up_pxe_config', autospec=True) + def test_prepare_instance_localboot_active(self, clean_up_pxe_config_mock, + set_boot_device_mock): + self.node.provision_state = states.ACTIVE + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + instance_info = task.node.instance_info + instance_info['capabilities'] = {'boot_option': 'local'} + task.node.instance_info = instance_info + task.node.save() + task.driver.boot.prepare_instance(task) + clean_up_pxe_config_mock.assert_called_once_with(task) + self.assertFalse(set_boot_device_mock.called) + + @mock.patch.object(pxe, '_clean_up_pxe_env', autospec=True) + @mock.patch.object(pxe, '_get_instance_image_info', autospec=True) + def test_clean_up_instance(self, get_image_info_mock, + clean_up_pxe_env_mock): + with task_manager.acquire(self.context, self.node.uuid) as task: + image_info = {'kernel': ['', '/path/to/kernel'], + 'ramdisk': ['', '/path/to/ramdisk']} + get_image_info_mock.return_value = image_info + task.driver.boot.clean_up_instance(task) + clean_up_pxe_env_mock.assert_called_once_with(task, image_info) + get_image_info_mock.assert_called_once_with( + task, ipxe_enabled=True) + + +@mock.patch.object(ipxe.iPXEBoot, '__init__', lambda self: None) +class iPXEValidateRescueTestCase(db_base.DbTestCase): + + def setUp(self): + super(iPXEValidateRescueTestCase, self).setUp() + for iface in drivers_base.ALL_INTERFACES: + impl = 'fake' + if iface == 'network': + impl = 'flat' + if iface == 'rescue': + impl = 'agent' + if iface == 'boot': + impl = 'ipxe' + config_kwarg = {'enabled_%s_interfaces' % iface: [impl], + 'default_%s_interface' % iface: impl} + self.config(**config_kwarg) + self.config(enabled_hardware_types=['fake-hardware']) + driver_info = DRV_INFO_DICT + driver_info.update({'rescue_ramdisk': 'my_ramdisk', + 'rescue_kernel': 'my_kernel'}) + instance_info = INST_INFO_DICT + instance_info.update({'rescue_password': 'password'}) + n = { + 'driver': 'fake-hardware', + 'instance_info': instance_info, + 'driver_info': driver_info, + 'driver_internal_info': DRV_INTERNAL_INFO_DICT, + } + self.node = obj_utils.create_test_node(self.context, **n) + + def test_validate_rescue(self): + 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 + del driver_info['rescue_ramdisk'] + self.node.driver_info = driver_info + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaisesRegex(exception.MissingParameterValue, + 'Missing.*rescue_ramdisk', + task.driver.boot.validate_rescue, task) + + def test_validate_rescue_fails_no_rescue_kernel(self): + driver_info = self.node.driver_info + del driver_info['rescue_kernel'] + self.node.driver_info = driver_info + self.node.save() + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertRaisesRegex(exception.MissingParameterValue, + 'Missing.*rescue_kernel', + task.driver.boot.validate_rescue, task) diff --git a/ironic/tests/unit/drivers/modules/test_iscsi_deploy.py b/ironic/tests/unit/drivers/modules/test_iscsi_deploy.py index b6d14f357a..02bfd0e4a1 100644 --- a/ironic/tests/unit/drivers/modules/test_iscsi_deploy.py +++ b/ironic/tests/unit/drivers/modules/test_iscsi_deploy.py @@ -1016,8 +1016,7 @@ class CleanUpFullFlowTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: task.driver.deploy.clean_up(task) - mock_get_instance_image_info.assert_called_with(task.node, - task.context) + mock_get_instance_image_info.assert_called_with(task) mock_get_deploy_image_info.assert_called_with(task.node, mode='deploy') set_dhcp_provider_mock.assert_called_once_with() diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py index 765d5781f5..44e9a3a05b 100644 --- a/ironic/tests/unit/drivers/modules/test_pxe.py +++ b/ironic/tests/unit/drivers/modules/test_pxe.py @@ -38,6 +38,7 @@ from ironic.conductor import utils as manager_utils from ironic.drivers import base as drivers_base from ironic.drivers.modules import agent_base_vendor from ironic.drivers.modules import deploy_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.tests.unit.db import base as db_base @@ -51,6 +52,9 @@ DRV_INFO_DICT = db_utils.get_test_pxe_driver_info() DRV_INTERNAL_INFO_DICT = db_utils.get_test_pxe_driver_internal_info() +# NOTE(TheJulia): This will need to be split until pxe interface code is +# refactored and cleaned up. +@mock.patch.object(ipxe.iPXEBoot, '__init__', lambda self: None) @mock.patch.object(pxe.PXEBoot, '__init__', lambda self: None) class PXEPrivateMethodsTestCase(db_base.DbTestCase): @@ -146,20 +150,22 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): 'kernel'))} show_mock.return_value = properties self.context.auth_token = 'fake' - image_info = pxe._get_instance_image_info(self.node, self.context) - show_mock.assert_called_once_with(mock.ANY, 'glance://image_uuid', - method='get') - self.assertEqual(expected_info, image_info) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + image_info = pxe._get_instance_image_info(task) + show_mock.assert_called_once_with(mock.ANY, 'glance://image_uuid', + method='get') + self.assertEqual(expected_info, image_info) - # test with saved info - show_mock.reset_mock() - image_info = pxe._get_instance_image_info(self.node, self.context) - self.assertEqual(expected_info, image_info) - self.assertFalse(show_mock.called) - self.assertEqual('instance_kernel_uuid', - self.node.instance_info['kernel']) - self.assertEqual('instance_ramdisk_uuid', - self.node.instance_info['ramdisk']) + # test with saved info + show_mock.reset_mock() + image_info = pxe._get_instance_image_info(task) + self.assertEqual(expected_info, image_info) + self.assertFalse(show_mock.called) + self.assertEqual('instance_kernel_uuid', + task.node.instance_info['kernel']) + self.assertEqual('instance_ramdisk_uuid', + task.node.instance_info['ramdisk']) def test__get_instance_image_info(self): # Tests when 'is_whole_disk_image' exists in driver_internal_info @@ -177,17 +183,21 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): def test__get_instance_image_info_localboot(self, boot_opt_mock): self.node.driver_internal_info['is_whole_disk_image'] = False self.node.save() - image_info = pxe._get_instance_image_info(self.node, self.context) - self.assertEqual({}, image_info) - boot_opt_mock.assert_called_once_with(self.node) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + image_info = pxe._get_instance_image_info(task) + self.assertEqual({}, image_info) + boot_opt_mock.assert_called_once_with(task.node) @mock.patch.object(base_image_service.BaseImageService, '_show', autospec=True) def test__get_instance_image_info_whole_disk_image(self, show_mock): properties = {'properties': None} show_mock.return_value = properties - self.node.driver_internal_info['is_whole_disk_image'] = True - image_info = pxe._get_instance_image_info(self.node, self.context) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.driver_internal_info['is_whole_disk_image'] = True + image_info = pxe._get_instance_image_info(task) self.assertEqual({}, image_info) @mock.patch('ironic.common.utils.render_template', autospec=True) @@ -442,7 +452,8 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): with task_manager.acquire(self.context, self.node.uuid, shared=True) as task: - options = pxe._build_pxe_config_options(task, image_info) + options = pxe._build_pxe_config_options( + task, image_info, ipxe_enabled=True) self.assertEqual(expected_options, options) def test__build_pxe_config_options_ipxe(self): @@ -637,10 +648,11 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): 'deploy_kernel') image_info = {'deploy_kernel': ('deploy_kernel', image_path)} fileutils.ensure_tree(CONF.pxe.tftp_master_path) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + pxe._cache_ramdisk_kernel(task, image_info) - pxe._cache_ramdisk_kernel(None, self.node, image_info) - - mock_fetch_image.assert_called_once_with(None, + mock_fetch_image.assert_called_once_with(self.context, mock.ANY, [('deploy_kernel', image_path)], @@ -653,8 +665,9 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): self.config(ipxe_enabled=False, group='pxe') fake_pxe_info = {'foo': 'bar'} expected_path = os.path.join(CONF.pxe.tftp_root, self.node.uuid) - - pxe._cache_ramdisk_kernel(self.context, self.node, fake_pxe_info) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + pxe._cache_ramdisk_kernel(task, fake_pxe_info) mock_ensure_tree.assert_called_with(expected_path) mock_fetch_image.assert_called_once_with( self.context, mock.ANY, list(fake_pxe_info.values()), True) @@ -668,8 +681,9 @@ class PXEPrivateMethodsTestCase(db_base.DbTestCase): fake_pxe_info = {'foo': 'bar'} expected_path = os.path.join(CONF.deploy.http_root, self.node.uuid) - - pxe._cache_ramdisk_kernel(self.context, self.node, fake_pxe_info) + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + pxe._cache_ramdisk_kernel(task, fake_pxe_info) mock_ensure_tree.assert_called_with(expected_path) mock_fetch_image.assert_called_once_with(self.context, mock.ANY, list(fake_pxe_info.values()), @@ -748,6 +762,9 @@ class CleanUpPxeEnvTestCase(db_base.DbTestCase): mock_cache.return_value.clean_up.assert_called_once_with() +# NOTE(TheJulia): cover the ipxe interface's init as well until +# the testing is separated apart. +@mock.patch.object(ipxe.iPXEBoot, '__init__', lambda self: None) @mock.patch.object(pxe.PXEBoot, '__init__', lambda self: None) class PXEBootTestCase(db_base.DbTestCase): @@ -765,7 +782,8 @@ class PXEBootTestCase(db_base.DbTestCase): instance_info = INST_INFO_DICT instance_info['deploy_key'] = 'fake-56789' - self.config(enabled_boot_interfaces=[self.boot_interface, 'fake']) + self.config(enabled_boot_interfaces=[self.boot_interface, + 'ipxe', 'fake']) self.node = obj_utils.create_test_node( self.context, driver=self.driver, @@ -963,31 +981,31 @@ class PXEBootTestCase(db_base.DbTestCase): self.assertFalse(mock_cache_r_k.called) else: mock_cache_r_k.assert_called_once_with( - self.context, task.node, + task, {'kernel': 'b'}) - mock_instance_img_info.assert_called_once_with(task.node, - self.context) + mock_instance_img_info.assert_called_once_with(task) elif not cleaning and mode == 'deploy': mock_cache_r_k.assert_called_once_with( - self.context, task.node, + task, {'deploy_kernel': 'a', 'deploy_ramdisk': 'r', 'kernel': 'b'}) - mock_instance_img_info.assert_called_once_with(task.node, - self.context) + mock_instance_img_info.assert_called_once_with(task) elif mode == 'deploy': mock_cache_r_k.assert_called_once_with( - self.context, task.node, + task, {'deploy_kernel': 'a', 'deploy_ramdisk': 'r'}) elif mode == 'rescue': mock_cache_r_k.assert_called_once_with( - self.context, task.node, + task, {'rescue_kernel': 'a', 'rescue_ramdisk': 'r'}) if uefi: mock_pxe_config.assert_called_once_with( - task, {'foo': 'bar'}, CONF.pxe.uefi_pxe_config_template) + task, {'foo': 'bar'}, CONF.pxe.uefi_pxe_config_template, + ipxe_enabled=CONF.pxe.ipxe_enabled) else: mock_pxe_config.assert_called_once_with( - task, {'foo': 'bar'}, CONF.pxe.pxe_config_template) + task, {'foo': 'bar'}, CONF.pxe.pxe_config_template, + ipxe_enabled=CONF.pxe.ipxe_enabled) def test_prepare_ramdisk(self): self.node.provision_state = states.DEPLOYING @@ -1248,13 +1266,12 @@ class PXEBootTestCase(db_base.DbTestCase): task.driver.boot.prepare_instance(task) get_image_info_mock.assert_called_once_with( - task.node, task.context) - cache_mock.assert_called_once_with( - task.context, task.node, image_info) + task) + cache_mock.assert_called_once_with(task, image_info) provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", - 'bios', False, False, False, False) + 'bios', False, False, False, False, ipxe_enabled=False) set_boot_device_mock.assert_called_once_with(task, boot_devices.PXE, persistent=True) @@ -1289,15 +1306,16 @@ class PXEBootTestCase(db_base.DbTestCase): task.driver.boot.prepare_instance(task) get_image_info_mock.assert_called_once_with( - task.node, task.context) + task) cache_mock.assert_called_once_with( - task.context, task.node, image_info) + task, image_info) provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) create_pxe_config_mock.assert_called_once_with( - task, mock.ANY, CONF.pxe.pxe_config_template) + task, mock.ANY, CONF.pxe.pxe_config_template, + ipxe_enabled=False) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50", - 'bios', False, False, False, False) + 'bios', False, False, False, False, ipxe_enabled=False) self.assertFalse(set_boot_device_mock.called) @mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True) @@ -1321,10 +1339,8 @@ class PXEBootTestCase(db_base.DbTestCase): task.driver.boot.prepare_instance(task) - get_image_info_mock.assert_called_once_with( - task.node, task.context) - cache_mock.assert_called_once_with( - task.context, task.node, image_info) + get_image_info_mock.assert_called_once_with(task) + cache_mock.assert_called_once_with(task, image_info) provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) self.assertFalse(switch_pxe_config_mock.called) self.assertFalse(set_boot_device_mock.called) @@ -1347,10 +1363,9 @@ class PXEBootTestCase(db_base.DbTestCase): task.node.properties['capabilities'] = 'boot_mode:bios' task.node.driver_internal_info['is_whole_disk_image'] = True task.driver.boot.prepare_instance(task) - get_image_info_mock.assert_called_once_with( - task.node, task.context) + get_image_info_mock.assert_called_once_with(task) cache_mock.assert_called_once_with( - task.context, task.node, {}) + task, {}) provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) self.assertTrue(log_mock.called) clean_up_pxe_mock.assert_called_once_with(task) @@ -1397,10 +1412,11 @@ class PXEBootTestCase(db_base.DbTestCase): self.assertFalse(cache_mock.called) provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) create_pxe_config_mock.assert_called_once_with( - task, mock.ANY, CONF.pxe.pxe_config_template) + task, mock.ANY, CONF.pxe.pxe_config_template, + ipxe_enabled=True) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, None, boot_modes.LEGACY_BIOS, False, - iscsi_boot=True, ramdisk_boot=False) + ipxe_enabled=True, ramdisk_boot=False, iscsi_boot=True) set_boot_device_mock.assert_called_once_with(task, boot_devices.PXE, persistent=True) @@ -1481,19 +1497,19 @@ class PXEBootTestCase(db_base.DbTestCase): task.node.uuid) task.driver.boot.prepare_instance(task) - get_image_info_mock.assert_called_once_with( - task.node, task.context) - cache_mock.assert_called_once_with( - task.context, task.node, image_info) + get_image_info_mock.assert_called_once_with(task) + cache_mock.assert_called_once_with(task, image_info) provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) if config_file_exits: self.assertFalse(create_pxe_config_mock.called) else: create_pxe_config_mock.assert_called_once_with( - task, mock.ANY, CONF.pxe.pxe_config_template) + task, mock.ANY, CONF.pxe.pxe_config_template, + ipxe_enabled=False) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, None, - 'bios', False, iscsi_boot=False, ramdisk_boot=True) + 'bios', False, ipxe_enabled=False, iscsi_boot=False, + ramdisk_boot=True) set_boot_device_mock.assert_called_once_with(task, boot_devices.PXE, persistent=True) @@ -1516,8 +1532,7 @@ class PXEBootTestCase(db_base.DbTestCase): get_image_info_mock.return_value = image_info task.driver.boot.clean_up_instance(task) clean_up_pxe_env_mock.assert_called_once_with(task, image_info) - get_image_info_mock.assert_called_once_with( - task.node, task.context) + get_image_info_mock.assert_called_once_with(task) class PXERamdiskDeployTestCase(db_base.DbTestCase): @@ -1576,14 +1591,14 @@ class PXERamdiskDeployTestCase(db_base.DbTestCase): task.driver.deploy.prepare(task) task.driver.deploy.deploy(task) - get_image_info_mock.assert_called_once_with( - task.node, task.context) + get_image_info_mock.assert_called_once_with(task) cache_mock.assert_called_once_with( - task.context, task.node, image_info) + task, image_info) provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts) switch_pxe_config_mock.assert_called_once_with( pxe_config_path, None, - 'bios', False, iscsi_boot=False, ramdisk_boot=True) + 'bios', False, ipxe_enabled=False, iscsi_boot=False, + ramdisk_boot=True) set_boot_device_mock.assert_called_once_with(task, boot_devices.PXE, persistent=True) @@ -1604,10 +1619,8 @@ class PXERamdiskDeployTestCase(db_base.DbTestCase): self.node.save() with task_manager.acquire(self.context, self.node.uuid) as task: self.assertIsNone(task.driver.deploy.deploy(task)) - mock_image_info.assert_called_once_with( - task.node, task.context) - mock_cache.assert_called_once_with( - task.context, task.node, image_info) + mock_image_info.assert_called_once_with(task) + mock_cache.assert_called_once_with(task, image_info) self.assertFalse(mock_warning.called) i_info['configdrive'] = 'meow' self.node.instance_info = i_info diff --git a/releasenotes/notes/ipxe-boot-interface-addition-faacb344a72389f2.yaml b/releasenotes/notes/ipxe-boot-interface-addition-faacb344a72389f2.yaml new file mode 100644 index 0000000000..e9433f1933 --- /dev/null +++ b/releasenotes/notes/ipxe-boot-interface-addition-faacb344a72389f2.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + Adds an ``ipxe`` boot interface which allows for instance level iPXE + enablement as opposed to conductor-wide enablement of iPXE. +upgrade: + - | + Deployments utilizing iPXE should consider use of the ``ipxe`` + boot interface as opposed to the ``pxe`` boot interface. iPXE + functionality in the ``pxe`` boot interface is deprecated and will + be removed during the U* development cycle. +deprecations: + - | + The ``[pxe]ipxe_enabled`` configuration option has been deprecated in + preference for the ``ipxe`` boot interface. The configuration option + will be removed during the U* development cycle. + - | + Support for iPXE in the ``pxe`` boot interface has been deprecated, + and will be removed during the U* development cycle. The ``ipxe`` + boot interface should be used instead. diff --git a/setup.cfg b/setup.cfg index eb4cfb2e8a..196043613d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -62,6 +62,7 @@ ironic.hardware.interfaces.boot = fake = ironic.drivers.modules.fake:FakeBoot ilo-pxe = ironic.drivers.modules.ilo.boot:IloPXEBoot ilo-virtual-media = ironic.drivers.modules.ilo.boot:IloVirtualMediaBoot + ipxe = ironic.drivers.modules.ipxe:iPXEBoot irmc-pxe = ironic.drivers.modules.irmc.boot:IRMCPXEBoot irmc-virtual-media = ironic.drivers.modules.irmc.boot:IRMCVirtualMediaBoot pxe = ironic.drivers.modules.pxe:PXEBoot diff --git a/zuul.d/ironic-jobs.yaml b/zuul.d/ironic-jobs.yaml index ab2735d30f..240a8d3b65 100644 --- a/zuul.d/ironic-jobs.yaml +++ b/zuul.d/ironic-jobs.yaml @@ -227,6 +227,15 @@ c-vol: True cinder: True +- job: + name: ironic-tempest-dsvm-ipxe-bfv + description: ironic-tempest-dsvm-ipxe-bfv + parent: ironic-tempest-dsvm-bfv + vars: + devstack_localrc: + IRONIC_ENABLED_BOOT_INTERFACES: ipxe,pxe,fake + IRONIC_DEFAULT_BOOT_INTERFACE: ipxe + - job: name: ironic-tempest-dsvm-ironic-inspector description: ironic-tempest-dsvm-ironic-inspector diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index eab5c4d6a2..0618040980 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -16,7 +16,6 @@ - ironic-tempest-dsvm-functional-python3 - ironic-grenade-dsvm - ironic-grenade-dsvm-multinode-multitenant - - ironic-tempest-dsvm-bfv - ironic-tempest-dsvm-ipa-partition-pxe_ipmitool-tinyipa-python3 - ironic-tempest-dsvm-ipa-partition-redfish-tinyipa - ironic-tempest-dsvm-ipa-partition-uefi-pxe_ipmitool-tinyipa @@ -24,7 +23,14 @@ - ironic-tempest-dsvm-ipa-wholedisk-bios-agent_ipmitool-tinyipa - ironic-tempest-dsvm-ipa-wholedisk-bios-agent_ipmitool-tinyipa-indirect - ironic-tempest-dsvm-ipa-partition-bios-agent_ipmitool-tinyipa-indirect + - ironic-tempest-dsvm-ipxe-bfv # Non-voting jobs + # NOTE(TheJulia): BFV default job moves to use the ipxe interface, + # this non-voting job is just a safety net as we continue to do + # refactoring/cleanup and can be removed later in the stein + # cycle or after. + - ironic-tempest-dsvm-bfv: + voting: false - ironic-tempest-dsvm-ipa-wholedisk-bios-pxe_snmp-tinyipa: voting: false - ironic-tempest-dsvm-ironic-inspector: @@ -39,7 +45,6 @@ - ironic-tempest-dsvm-functional-python3 - ironic-grenade-dsvm - ironic-grenade-dsvm-multinode-multitenant - - ironic-tempest-dsvm-bfv - ironic-tempest-dsvm-ipa-partition-pxe_ipmitool-tinyipa-python3 - ironic-tempest-dsvm-ipa-partition-redfish-tinyipa - ironic-tempest-dsvm-ipa-partition-uefi-pxe_ipmitool-tinyipa @@ -47,3 +52,4 @@ - ironic-tempest-dsvm-ipa-wholedisk-bios-agent_ipmitool-tinyipa - ironic-tempest-dsvm-ipa-wholedisk-bios-agent_ipmitool-tinyipa-indirect - ironic-tempest-dsvm-ipa-partition-bios-agent_ipmitool-tinyipa-indirect + - ironic-tempest-dsvm-ipxe-bfv