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
This commit is contained in:
Julia Kreger 2018-07-17 14:36:09 -07:00
parent 6b87367a0f
commit c787f6806f
18 changed files with 1500 additions and 207 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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