Merge "Add HTTP versions of network boot interfaces"
This commit is contained in:
commit
b0e443f77f
@ -39,6 +39,27 @@ their specific implementations of the PXE boot interface.
|
||||
Additional configuration is required for this boot interface - see
|
||||
:doc:`/install/configure-pxe` for details.
|
||||
|
||||
HTTP Boot
|
||||
---------
|
||||
|
||||
The ``http`` and ``http-ipxe`` boot interfaces are based upon the Ironic
|
||||
implementation of the ``pxe`` and ``ipxe`` boot interfaces, respectively,
|
||||
and utilize HTTP in the transmission of the location to start the
|
||||
boot sequence from. These interfaces are specific to UEFI as they are rooted
|
||||
in the UEFI standard v2.5's support for booting from an HTTP URL.
|
||||
|
||||
One caveat to keep in mind is that these interfaces require hardware support
|
||||
and the ability to signal to the remote BMC that the node should boot
|
||||
utilizing ``UEFIHTTP``. If a hardware type does not support that as an option,
|
||||
we will fallback and request ``PXE`` boot, but that realistically may only
|
||||
work if the firmware on the machine is smart enough to check and evaluate
|
||||
for an HTTP Boot URL instead of a PXE boot server and file name.
|
||||
|
||||
It should be noted, that these boot interfaces are available for the vendor
|
||||
independent, generic hardware types of ``ipmi`` and ``redfish``. Hardware
|
||||
vendors typically only include additional interfaces after they have performed
|
||||
their own verification and qualification testing.
|
||||
|
||||
Kernel parameters
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -48,6 +48,13 @@ is being worked in Neutron
|
||||
`change 890683 <https://review.opendev.org/c/openstack/neutron/+/890683>`_ and
|
||||
`bug 20305201 <https://bugs.launchpad.net/neutron/+bug/20305201>`_.
|
||||
|
||||
.. warning::
|
||||
Use of OVN with HTTPBoot interfaces has not been explicitly tested by the
|
||||
Ironic project, and is unlikely to take place until after integrated IPv6
|
||||
support with Neutron is ready for use. The project does not expect any
|
||||
specific issues, but the OVN DHCP server is an entirely different server
|
||||
than the interfaces were tested upon.
|
||||
|
||||
Maxmium Transmission Units
|
||||
--------------------------
|
||||
|
||||
|
@ -4,9 +4,13 @@ Configure the Networking service for bare metal provisioning
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You need to configure Networking so that the bare metal server can communicate
|
||||
with the Networking service for DHCP, PXE boot and other requirements.
|
||||
with the Networking service for DHCP, PXE/HTTP boot and other requirements.
|
||||
This section covers configuring Networking for a single flat network for bare
|
||||
metal provisioning.
|
||||
metal provisioning. In more advanced configurations, we typically refer to
|
||||
the network upon which nodes undergo deployment as the provisioning network,
|
||||
as the underlying resources to provision the node must be available for
|
||||
successful operations.
|
||||
|
||||
|
||||
.. Warning:: This docuemntation is geared for use of OVS with Neutron along
|
||||
with the ``neutron-dhcp-agent``. It *is* possible to use OVN
|
||||
|
@ -1,10 +1,28 @@
|
||||
Configuring PXE and iPXE
|
||||
Configuring Network Boot
|
||||
========================
|
||||
|
||||
Ironic's primary means of booting hardware to perform actions or work on a
|
||||
baremetal node is to perform network booting. Traditionally, this has meant
|
||||
the use of Preboot Execution Environment, or PXE. This support and
|
||||
and functionality has evolve as time has gone on to include support for not
|
||||
just the ``pxe`` ``boot_interface`` in concert with hardware vendor specific
|
||||
variations, but also a distinct ``ipxe`` setting for ``boot_interface`` with
|
||||
default values to enable use of `iPXE <https://ipxe.org/>`_.
|
||||
|
||||
As time passed, ``http`` and ``http-ipxe`` values were also added as valid
|
||||
``boot_interface`` options which may be used, which are functionally identical
|
||||
in behavior to ``pxe`` and ``ipxe``, except HTTP is used as the transport
|
||||
mechanism. Not all hardware supports HTTPBoot, as it is often referred.
|
||||
|
||||
.. note::
|
||||
Support for HTTPBoot interfaces was added during the 2024.1 development
|
||||
cycle. Prior versions of Ironic does not contain the ``http`` and
|
||||
``http-ipxe`` boot interfaces.
|
||||
|
||||
DHCP server setup
|
||||
-----------------
|
||||
|
||||
A DHCP server is required by PXE/iPXE client. You need to follow steps below.
|
||||
A DHCP server is required for network boot clients. You need to follow steps below.
|
||||
|
||||
#. Set the ``[dhcp]/dhcp_provider`` to ``neutron`` in the Bare Metal Service's
|
||||
configuration file (``/etc/ironic/ironic.conf``):
|
||||
@ -15,7 +33,8 @@ A DHCP server is required by PXE/iPXE client. You need to follow steps below.
|
||||
defaults, and when you create subnet, DHCP is also enabled if you do not add
|
||||
any dhcp options at "openstack subnet create" command.
|
||||
|
||||
#. Enable DHCP in the subnet of PXE network.
|
||||
#. Enable DHCP in the subnet of provisioning network to be used for network
|
||||
boot (PXE, iPXE, HTTPBoot) operations.
|
||||
|
||||
#. Set the ip address range in the subnet for DHCP.
|
||||
|
||||
@ -591,3 +610,47 @@ an up-to-date iPXE firmware, you need to bootstrap it from TFTP. The
|
||||
|
||||
Finally, put ``ironic-python-agent.kernel`` and
|
||||
``ironic-python-agent.initramfs`` to ``/httpboot``.
|
||||
|
||||
HTTPBoot
|
||||
--------
|
||||
|
||||
HTTPBoot interfaces in Ironic are built upon the underlying network boot
|
||||
substrate. This means much of the configuration in the ``[pxe]`` and
|
||||
``[deploy]`` impacts the use of HTTPBoot, except when Ironic is setting
|
||||
DHCP parameters, it populates a HTTP(S) URL to the DHCP server, which is
|
||||
then transmitted to the client attempting to Network Boot. In large part,
|
||||
this is because HTTPBoot is an evolution of PXE Boot technique and
|
||||
technology.
|
||||
|
||||
This means a TFTP server is *not* required, but the HTTP server is
|
||||
required as if your utilizing iPXE. This is largely because iPXE
|
||||
has traditionally been leveraged by Operators to limit the TFTP
|
||||
packets being transmitted via UDP across a network.
|
||||
|
||||
One aspect to keep in mind, is HTTPBoot is relatively new when compared
|
||||
to PXE boot, and not all bootloaders may support HTTPBoot, as the underlying
|
||||
UEFI standard upon which it was largely based, UEFI v2.5, was published in
|
||||
2015.
|
||||
|
||||
Ironic contains two distinct flavors of HTTPBoot, largely based
|
||||
upon what configuration defaults are used in terms of boot loader, templates,
|
||||
and overall mechanism style.
|
||||
|
||||
* ``http`` is the boot interface based upon the ``pxe`` boot interface.
|
||||
This is the interface you would want to use if you had, for example, a
|
||||
signed GRUB2 bootloader chain to utilize. In this case it is up to the
|
||||
boot loader to understand how to extract and run with the URL, and then
|
||||
retrieves any additional configuration loader files and configuration
|
||||
templates created on disk.
|
||||
* ``http-ipxe`` is the boot interface based upon the ``ipxe`` boot interface.
|
||||
This interface signals to the client to utilize the configured iPXE loader
|
||||
binary over HTTP, and then the boot sequence proceeds with the pattern and
|
||||
capabilities of iPXE.
|
||||
|
||||
To enable the boot interfaces, you will need to add them to your
|
||||
``[DEFAULT]enabled_boot_interfaces`` configuration entry.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[DEFAULT]
|
||||
enabled_boot_interfaces=ipxe,http-ipxe,pxe,http
|
||||
|
@ -31,9 +31,9 @@ components.
|
||||
* The conductor needs access to the `management controller`_ of each node
|
||||
it manages.
|
||||
|
||||
* The conductor co-exists with TFTP (for PXE) and/or HTTP (for iPXE) services
|
||||
that provide the kernel and ramdisk to boot the nodes. The conductor
|
||||
manages them by writing files to their root directories.
|
||||
* The conductor co-exists with TFTP (for PXE) and/or HTTP (for HTTPBoot and
|
||||
iPXE) services that provide the kernel and ramdisk to boot the nodes.
|
||||
The conductor manages them by writing files to their root directories.
|
||||
|
||||
* If serial console is used, the conductor launches console processes
|
||||
locally. If the ``nova-serialproxy`` service (part of the Compute service)
|
||||
|
@ -48,6 +48,7 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
PXE_CFG_DIR_NAME = CONF.pxe.pxe_config_subdir
|
||||
|
||||
DHCP_VENDOR_CLASS_ID = '60' # rfc2132
|
||||
DHCP_CLIENT_ID = '61' # rfc2132
|
||||
DHCP_TFTP_SERVER_NAME = '66' # rfc2132
|
||||
DHCP_BOOTFILE_NAME = '67' # rfc2132
|
||||
@ -66,8 +67,8 @@ KERNEL_RAMDISK_LABELS = {'deploy': DEPLOY_KERNEL_RAMDISK_LABELS,
|
||||
'rescue': RESCUE_KERNEL_RAMDISK_LABELS}
|
||||
|
||||
|
||||
def _get_root_dir(ipxe_enabled):
|
||||
if ipxe_enabled:
|
||||
def _get_root_dir(use_http_root):
|
||||
if use_http_root:
|
||||
return CONF.deploy.http_root
|
||||
else:
|
||||
return CONF.pxe.tftp_root
|
||||
@ -239,7 +240,8 @@ def get_kernel_ramdisk_info(node_uuid, driver_info, mode='deploy',
|
||||
return image_info
|
||||
|
||||
|
||||
def get_pxe_config_file_path(node_uuid, ipxe_enabled=False):
|
||||
def get_pxe_config_file_path(node_uuid, ipxe_enabled=False,
|
||||
http_boot_enabled=False):
|
||||
"""Generate the path for the node's PXE configuration file.
|
||||
|
||||
:param node_uuid: the UUID of the node.
|
||||
@ -248,7 +250,8 @@ def get_pxe_config_file_path(node_uuid, ipxe_enabled=False):
|
||||
:returns: The path to the node's PXE configuration file.
|
||||
|
||||
"""
|
||||
return os.path.join(_get_root_dir(ipxe_enabled), node_uuid, 'config')
|
||||
return os.path.join(
|
||||
_get_root_dir(ipxe_enabled or http_boot_enabled), node_uuid, 'config')
|
||||
|
||||
|
||||
def get_file_path_from_label(node_uuid, root_dir, label):
|
||||
@ -433,7 +436,8 @@ def clean_up_pxe_config(task, ipxe_enabled=False):
|
||||
task.node.uuid))
|
||||
|
||||
|
||||
def _dhcp_option_file_or_url(task, urlboot=False, ip_version=None):
|
||||
def _dhcp_option_file_or_url(task, urlboot=False, ip_version=None,
|
||||
http_boot_enabled=False):
|
||||
"""Returns the appropriate file or URL.
|
||||
|
||||
:param task: A TaskManager object.
|
||||
@ -443,7 +447,11 @@ def _dhcp_option_file_or_url(task, urlboot=False, ip_version=None):
|
||||
:param ip_version: Integer representing the version of IP of
|
||||
to return options for DHCP. Possible options
|
||||
are 4, and 6.
|
||||
:param http_boot_enabled: If HTTPBoot is utilized, default False.
|
||||
:raises: InvalidParameterValue if the resulting property length
|
||||
exceeds Neutron limitations.
|
||||
"""
|
||||
result = None
|
||||
try:
|
||||
if task.driver.boot.ipxe_enabled:
|
||||
boot_file = deploy_utils.get_ipxe_boot_file(task.node)
|
||||
@ -457,18 +465,30 @@ def _dhcp_option_file_or_url(task, urlboot=False, ip_version=None):
|
||||
# NOTE(TheJulia): There are additional cases as we add new
|
||||
# features, so the logic below is in the form of if/elif/elif
|
||||
if not urlboot:
|
||||
return boot_file
|
||||
elif urlboot:
|
||||
result = boot_file
|
||||
elif urlboot and not http_boot_enabled:
|
||||
if CONF.my_ipv6 and ip_version == 6:
|
||||
host = utils.wrap_ipv6(CONF.my_ipv6)
|
||||
else:
|
||||
elif not http_boot_enabled:
|
||||
host = utils.wrap_ipv6(CONF.pxe.tftp_server)
|
||||
return "tftp://{host}/{boot_file}".format(host=host,
|
||||
result = "tftp://{host}/{boot_file}".format(host=host,
|
||||
boot_file=boot_file)
|
||||
elif http_boot_enabled:
|
||||
result = "{url}/{boot_file}".format(url=CONF.deploy.http_url,
|
||||
boot_file=boot_file)
|
||||
if len(result) > 64:
|
||||
# This is an internal limitation for Neutron. We cannot send it
|
||||
# a value longer than 64 characters.
|
||||
raise exception.InvalidParameterValue('The resulting boot file or '
|
||||
'URL length exceeds Neutron '
|
||||
'limitations. Please explore '
|
||||
'shorter file names or '
|
||||
'hostnames.')
|
||||
return result
|
||||
|
||||
|
||||
def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False,
|
||||
ip_version=None):
|
||||
ip_version=None, http_boot_enabled=False):
|
||||
"""Retrieves the DHCP PXE boot options.
|
||||
|
||||
:param task: A TaskManager instance.
|
||||
@ -485,8 +505,16 @@ def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False,
|
||||
Possible options are integers 4 or 6.
|
||||
:returns: Dictionary to be sent to the networking service describing
|
||||
the DHCP options to be set.
|
||||
:raises: InvalidParameterValue if the underlying configuration cannot
|
||||
be conveyed to Neutron due to resulting value length.
|
||||
"""
|
||||
# FIXME(TheJulia): Presently, we determine if we should generate ipxe
|
||||
# enabled configuration *via* the argument, and that presently gets set
|
||||
# via the driver's interface, but we ought to double check because
|
||||
# otherwise it is easy to miss, like when writing tests if you've touched
|
||||
# this area of the code.
|
||||
if ip_version:
|
||||
# IP version defines *which* parameter is used for file name.
|
||||
use_ip_version = ip_version
|
||||
else:
|
||||
use_ip_version = int(CONF.pxe.ip_version)
|
||||
@ -499,11 +527,15 @@ def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False,
|
||||
# a URL reply.
|
||||
boot_file_param = DHCPV6_BOOTFILE_NAME
|
||||
url_boot = True
|
||||
if http_boot_enabled:
|
||||
# IF this is for httpboot, just always mark url boot as enabled.
|
||||
url_boot = True
|
||||
# NOTE(TheJulia): The ip_version value config from the PXE config is
|
||||
# guarded in the configuration, so there is no real sense in having
|
||||
# anything else here in the event the value is something aside from
|
||||
# 4 or 6, as there are no other possible values.
|
||||
boot_file = _dhcp_option_file_or_url(task, url_boot, use_ip_version)
|
||||
boot_file = _dhcp_option_file_or_url(task, url_boot, use_ip_version,
|
||||
http_boot_enabled=http_boot_enabled)
|
||||
|
||||
if ipxe_enabled:
|
||||
# TODO(TheJulia): DHCPv6 through dnsmasq + ipxe matching simply
|
||||
@ -561,6 +593,11 @@ def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False,
|
||||
else:
|
||||
dhcp_opts.append({'opt_name': boot_file_param,
|
||||
'opt_value': boot_file})
|
||||
if http_boot_enabled and use_ip_version == 4:
|
||||
# So unlike v6 PXE's use of URLs above, we explicitly need
|
||||
# to send a vendor class back (option 60, vendor-class in dnsmasq)
|
||||
dhcp_opts.append({'opt_name': DHCP_VENDOR_CLASS_ID,
|
||||
'opt_value': 'HTTPClient'})
|
||||
|
||||
if not url_boot:
|
||||
dhcp_opts.append({'opt_name': DHCP_TFTP_SERVER_NAME,
|
||||
@ -1168,7 +1205,8 @@ def prepare_instance_pxe_config(task, image_info,
|
||||
iscsi_boot=False,
|
||||
ramdisk_boot=False,
|
||||
ipxe_enabled=False,
|
||||
anaconda_boot=False):
|
||||
anaconda_boot=False,
|
||||
http_boot_enabled=False):
|
||||
"""Prepares the config file for PXE boot
|
||||
|
||||
:param task: a task from TaskManager.
|
||||
@ -1179,6 +1217,8 @@ def prepare_instance_pxe_config(task, image_info,
|
||||
:param ipxe_enabled: Default false boolean to indicate if ipxe
|
||||
is in use by the caller.
|
||||
:param anaconda_boot: if the boot is to a anaconda ramdisk configuration.
|
||||
:param http_boot_enabled: If httpboot models of use are to be used
|
||||
with the underlying boot loaders.
|
||||
:returns: None
|
||||
"""
|
||||
node = task.node
|
||||
@ -1188,14 +1228,19 @@ def prepare_instance_pxe_config(task, image_info,
|
||||
# development cycle so that we call a single method and return
|
||||
# combined options. The method we currently call is relied upon
|
||||
# by two eternal projects, to changing the behavior is not ideal.
|
||||
dhcp_opts = dhcp_options_for_instance(task, ipxe_enabled,
|
||||
ip_version=4)
|
||||
dhcp_opts += dhcp_options_for_instance(task, ipxe_enabled,
|
||||
ip_version=6)
|
||||
dhcp_opts = dhcp_options_for_instance(
|
||||
task, ipxe_enabled,
|
||||
ip_version=4,
|
||||
http_boot_enabled=http_boot_enabled)
|
||||
dhcp_opts += dhcp_options_for_instance(
|
||||
task, ipxe_enabled,
|
||||
ip_version=6,
|
||||
http_boot_enabled=http_boot_enabled)
|
||||
provider = dhcp_factory.DHCPFactory()
|
||||
provider.update_dhcp(task, dhcp_opts)
|
||||
pxe_config_path = get_pxe_config_file_path(
|
||||
node.uuid, ipxe_enabled=ipxe_enabled)
|
||||
node.uuid, ipxe_enabled=ipxe_enabled,
|
||||
http_boot_enabled=http_boot_enabled)
|
||||
if not os.path.isfile(pxe_config_path):
|
||||
pxe_options = build_pxe_config_options(
|
||||
task, image_info, service=ramdisk_boot or anaconda_boot,
|
||||
@ -1354,15 +1399,16 @@ def place_common_config():
|
||||
if not CONF.pxe.initial_grub_template:
|
||||
return
|
||||
|
||||
grub_dir_path = os.path.join(_get_root_dir(False), 'grub')
|
||||
for use_http in [False, True]:
|
||||
# Create paths
|
||||
grub_dir_path = os.path.join(_get_root_dir(use_http), 'grub')
|
||||
if not os.path.isdir(grub_dir_path):
|
||||
fileutils.ensure_tree(grub_dir_path)
|
||||
if CONF.pxe.dir_permission:
|
||||
os.chmod(grub_dir_path, CONF.pxe.dir_permission)
|
||||
|
||||
# Write templates
|
||||
initial_grub = utils.render_template(
|
||||
CONF.pxe.initial_grub_template,
|
||||
{'tftp_root': _get_root_dir(False)})
|
||||
initial_grub_path = os.path.join(grub_dir_path, 'grub.cfg')
|
||||
|
||||
utils.write_to_file(initial_grub_path, initial_grub)
|
||||
|
@ -44,7 +44,7 @@ class GenericHardware(hardware_type.AbstractHardwareType):
|
||||
@property
|
||||
def supported_boot_interfaces(self):
|
||||
"""List of supported boot interfaces."""
|
||||
return [ipxe.iPXEBoot, pxe.PXEBoot]
|
||||
return [ipxe.iPXEBoot, pxe.PXEBoot, ipxe.iPXEHttpBoot, pxe.HttpBoot]
|
||||
|
||||
@property
|
||||
def supported_deploy_interfaces(self):
|
||||
|
@ -32,3 +32,16 @@ class iPXEBoot(pxe_base.PXEBaseMixin, base.BootInterface):
|
||||
pxe_utils.place_loaders_for_boot(CONF.deploy.http_root)
|
||||
# This is required to serve the iPXE binary via tftp
|
||||
pxe_utils.place_loaders_for_boot(CONF.pxe.tftp_root)
|
||||
|
||||
|
||||
class iPXEHttpBoot(pxe_base.PXEBaseMixin, base.BootInterface):
|
||||
|
||||
ipxe_enabled = True
|
||||
|
||||
http_boot_enabled = True
|
||||
|
||||
capabilities = ['iscsi_volume_boot', 'ramdisk_boot', 'ipxe_boot']
|
||||
|
||||
def __init__(self):
|
||||
pxe_utils.create_ipxe_boot_script()
|
||||
pxe_utils.place_loaders_for_boot(CONF.deploy.http_root)
|
||||
|
@ -45,6 +45,17 @@ class PXEBoot(pxe_base.PXEBaseMixin, base.BootInterface):
|
||||
pxe_utils.place_loaders_for_boot(CONF.pxe.tftp_root)
|
||||
|
||||
|
||||
class HttpBoot(pxe_base.PXEBaseMixin, base.BootInterface):
|
||||
|
||||
http_boot_enabled = True
|
||||
|
||||
capabilities = ['ramdisk_boot', 'pxe_boot']
|
||||
|
||||
def __init__(self):
|
||||
pxe_utils.place_common_config()
|
||||
pxe_utils.place_loaders_for_boot(CONF.deploy.http_root)
|
||||
|
||||
|
||||
class PXEAnacondaDeploy(agent_base.AgentBaseMixin, agent_base.HeartbeatMixin,
|
||||
base.DeployInterface):
|
||||
|
||||
|
@ -63,6 +63,8 @@ class PXEBaseMixin(object):
|
||||
|
||||
ipxe_enabled = False
|
||||
|
||||
http_boot_enabled = False
|
||||
|
||||
def get_properties(self):
|
||||
"""Return the properties of the interface.
|
||||
|
||||
@ -70,6 +72,13 @@ class PXEBaseMixin(object):
|
||||
"""
|
||||
return COMMON_PROPERTIES
|
||||
|
||||
def _use_http_folder(self):
|
||||
if self.ipxe_enabled:
|
||||
return True
|
||||
if self.http_boot_enabled:
|
||||
return True
|
||||
return False
|
||||
|
||||
@METRICS.timer('PXEBaseMixin.clean_up_ramdisk')
|
||||
def clean_up_ramdisk(self, task):
|
||||
"""Cleans up the boot of ironic ramdisk.
|
||||
@ -90,14 +99,14 @@ class PXEBaseMixin(object):
|
||||
mode = deploy_utils.rescue_or_deploy_mode(node)
|
||||
try:
|
||||
images_info = pxe_utils.get_image_info(
|
||||
node, mode=mode, ipxe_enabled=self.ipxe_enabled)
|
||||
node, mode=mode, ipxe_enabled=self._use_http_folder())
|
||||
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_utils.clean_up_pxe_env(
|
||||
task, images_info, ipxe_enabled=self.ipxe_enabled)
|
||||
task, images_info, ipxe_enabled=self._use_http_folder())
|
||||
|
||||
@METRICS.timer('PXEBaseMixin.clean_up_instance')
|
||||
def clean_up_instance(self, task):
|
||||
@ -114,14 +123,14 @@ class PXEBaseMixin(object):
|
||||
|
||||
try:
|
||||
images_info = pxe_utils.get_instance_image_info(
|
||||
task, ipxe_enabled=self.ipxe_enabled)
|
||||
task, ipxe_enabled=self._use_http_folder())
|
||||
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_utils.clean_up_pxe_env(task, images_info,
|
||||
ipxe_enabled=self.ipxe_enabled)
|
||||
ipxe_enabled=self._use_http_folder())
|
||||
|
||||
boot_mode_utils.deconfigure_secure_boot_if_needed(task)
|
||||
|
||||
@ -146,7 +155,6 @@ class PXEBaseMixin(object):
|
||||
operation failed on the node.
|
||||
"""
|
||||
node = task.node
|
||||
|
||||
# Label indicating a deploy or rescue operation being carried out on
|
||||
# the node, 'deploy' or 'rescue'. Unless the node is in a rescue like
|
||||
# state, the mode is set to 'deploy', indicating deploy operation is
|
||||
@ -168,14 +176,19 @@ class PXEBaseMixin(object):
|
||||
# combined options. The method we currently call is relied upon
|
||||
# by two eternal projects, to changing the behavior is not ideal.
|
||||
dhcp_opts = pxe_utils.dhcp_options_for_instance(
|
||||
task, ipxe_enabled=self.ipxe_enabled, ip_version=4)
|
||||
task, ipxe_enabled=self.ipxe_enabled, ip_version=4,
|
||||
http_boot_enabled=self.http_boot_enabled)
|
||||
dhcp_opts += pxe_utils.dhcp_options_for_instance(
|
||||
task, ipxe_enabled=self.ipxe_enabled, ip_version=6)
|
||||
task, ipxe_enabled=self.ipxe_enabled, ip_version=6,
|
||||
http_boot_enabled=self.http_boot_enabled)
|
||||
provider = dhcp_factory.DHCPFactory()
|
||||
provider.update_dhcp(task, dhcp_opts)
|
||||
|
||||
pxe_info = pxe_utils.get_image_info(node, mode=mode,
|
||||
ipxe_enabled=self.ipxe_enabled)
|
||||
# TODO(TheJulia): We need to change the parameter name for
|
||||
# ipxe_enabled in pxe_utils at some point since here it is
|
||||
# an indicator of where to put the files on the filesystem.
|
||||
pxe_info = pxe_utils.get_image_info(
|
||||
node, mode=mode,
|
||||
ipxe_enabled=self._use_http_folder())
|
||||
|
||||
# NODE: Try to validate and fetch instance images only
|
||||
# if we are in DEPLOYING state.
|
||||
@ -201,8 +214,7 @@ class PXEBaseMixin(object):
|
||||
pxe_utils.create_pxe_config(task, pxe_options,
|
||||
pxe_config_template,
|
||||
ipxe_enabled=self.ipxe_enabled)
|
||||
manager_utils.node_set_boot_device(task, boot_devices.PXE,
|
||||
persistent=False)
|
||||
self._node_set_boot_device_for_network_boot(task)
|
||||
|
||||
if self.ipxe_enabled and CONF.pxe.ipxe_use_swift:
|
||||
kernel_label = '%s_kernel' % mode
|
||||
@ -211,13 +223,43 @@ class PXEBaseMixin(object):
|
||||
pxe_info.pop(ramdisk_label, None)
|
||||
|
||||
if pxe_info:
|
||||
pxe_utils.cache_ramdisk_kernel(task, pxe_info,
|
||||
ipxe_enabled=self.ipxe_enabled)
|
||||
pxe_utils.cache_ramdisk_kernel(
|
||||
task, pxe_info,
|
||||
ipxe_enabled=self._use_http_folder())
|
||||
|
||||
LOG.debug('Ramdisk (i)PXE boot for node %(node)s has been prepared '
|
||||
'with kernel params %(params)s',
|
||||
{'node': node.uuid, 'params': pxe_options})
|
||||
|
||||
def _node_set_boot_device_for_network_boot(self, task, persistent=False):
|
||||
"""Helper to handle httpboot aware network booting.
|
||||
|
||||
Basic challenge: IPMI doesn't have a field reserved for "httpboot" as
|
||||
httpboot pre-dates IPMI. It is also entirely possible that all logic
|
||||
to support httpboot is coming from OPROM code on network cards, so to
|
||||
sort of handle this, and the nature of PXE being percieved as
|
||||
"network boot", if we are http boot enabled, we attempt to explicitly
|
||||
request as such, but if the driver errors, then we fall back to PXE.
|
||||
|
||||
:params task: a TaskManager object.
|
||||
:params persistent: Default False, if the network boot request is
|
||||
persistent.
|
||||
"""
|
||||
if self.http_boot_enabled:
|
||||
try:
|
||||
manager_utils.node_set_boot_device(task,
|
||||
boot_devices.UEFIHTTP,
|
||||
persistent=persistent)
|
||||
except exception.InvalidParameterValue:
|
||||
LOG.warning('Attempted to set HTTPBOOT for node %s, but it is '
|
||||
'not supported by the driver. Falling back to '
|
||||
'PXE to trigger network boot.', task.node.uuid)
|
||||
manager_utils.node_set_boot_device(task, boot_devices.PXE,
|
||||
persistent=persistent)
|
||||
else:
|
||||
manager_utils.node_set_boot_device(task, boot_devices.PXE,
|
||||
persistent=persistent)
|
||||
|
||||
@METRICS.timer('PXEBaseMixin.prepare_instance')
|
||||
def prepare_instance(self, task):
|
||||
"""Prepares the boot of instance.
|
||||
@ -241,8 +283,9 @@ class PXEBaseMixin(object):
|
||||
if boot_option == "ramdisk" or boot_option == "kickstart":
|
||||
instance_image_info = pxe_utils.get_instance_image_info(
|
||||
task, ipxe_enabled=self.ipxe_enabled)
|
||||
pxe_utils.cache_ramdisk_kernel(task, instance_image_info,
|
||||
ipxe_enabled=self.ipxe_enabled)
|
||||
pxe_utils.cache_ramdisk_kernel(
|
||||
task, instance_image_info,
|
||||
ipxe_enabled=self._use_http_folder())
|
||||
if 'ks_template' in instance_image_info:
|
||||
ks_cfg = pxe_utils.validate_kickstart_template(
|
||||
instance_image_info['ks_template'][1]
|
||||
@ -256,7 +299,8 @@ class PXEBaseMixin(object):
|
||||
iscsi_boot=deploy_utils.is_iscsi_boot(task),
|
||||
ramdisk_boot=(boot_option == "ramdisk"),
|
||||
anaconda_boot=(boot_option == "kickstart"),
|
||||
ipxe_enabled=self.ipxe_enabled)
|
||||
ipxe_enabled=self.ipxe_enabled,
|
||||
http_boot_enabled=self.http_boot_enabled)
|
||||
pxe_utils.prepare_instance_kickstart_config(
|
||||
task, instance_image_info,
|
||||
anaconda_boot=(boot_option == "kickstart"))
|
||||
@ -285,6 +329,12 @@ class PXEBaseMixin(object):
|
||||
# during takeover
|
||||
if boot_device and (task.node.provision_state not in
|
||||
(states.ACTIVE, states.ADOPTING)):
|
||||
if boot_device == boot_devices.PXE:
|
||||
# Implying network booting, we need to handle the case
|
||||
# it might be HTTPBoot instead of PXEBoot.
|
||||
self._node_set_boot_device_for_network_boot(task,
|
||||
persistent=True)
|
||||
else:
|
||||
manager_utils.node_set_boot_device(task, boot_device,
|
||||
persistent=True)
|
||||
|
||||
@ -423,8 +473,7 @@ class PXEBaseMixin(object):
|
||||
'timeout': CONF.pxe.boot_retry_timeout})
|
||||
|
||||
manager_utils.node_power_action(task, states.POWER_OFF)
|
||||
manager_utils.node_set_boot_device(task, boot_devices.PXE,
|
||||
persistent=False)
|
||||
self._node_set_boot_device_for_network_boot(task)
|
||||
manager_utils.node_power_action(task, states.POWER_ON)
|
||||
|
||||
|
||||
|
@ -858,7 +858,8 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
'config'),
|
||||
pxe_utils.get_pxe_config_file_path(self.node.uuid))
|
||||
|
||||
def _dhcp_options_for_instance(self, ip_version=4, ipxe=False):
|
||||
def _dhcp_options_for_instance(self, ip_version=4, ipxe=False,
|
||||
http_boot=False):
|
||||
self.config(ip_version=ip_version, group='pxe')
|
||||
if ip_version == 4:
|
||||
self.config(tftp_server='192.0.2.1', group='pxe')
|
||||
@ -878,12 +879,14 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
else:
|
||||
self.config(pxe_bootfile_name='fake-bootfile', group='pxe')
|
||||
self.config(tftp_root='/tftp-path', group='pxe')
|
||||
if http_boot:
|
||||
self.config(http_url='https://foo.bar', group='deploy')
|
||||
if ipxe:
|
||||
bootfile = 'fake-bootfile-ipxe'
|
||||
else:
|
||||
bootfile = 'fake-bootfile'
|
||||
|
||||
if ip_version == 6:
|
||||
if ip_version == 6 and not http_boot:
|
||||
# NOTE(TheJulia): DHCPv6 RFCs seem to indicate that the prior
|
||||
# options are not imported, although they may be supported
|
||||
# by vendors. The apparent proper option is to return a
|
||||
@ -891,7 +894,22 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
expected_info = [{'opt_name': '59',
|
||||
'opt_value': 'tftp://[ff80::1]/%s' % bootfile,
|
||||
'ip_version': ip_version}]
|
||||
elif ip_version == 4:
|
||||
elif ip_version == 6 and http_boot:
|
||||
if not ipxe:
|
||||
expected_info = [
|
||||
{'ip_version': 6,
|
||||
'opt_name': '59',
|
||||
'opt_value': 'https://foo.bar/%s' % bootfile}]
|
||||
else:
|
||||
expected_info = [
|
||||
{'ip_version': 6,
|
||||
'opt_name': 'tag:!ipxe6,59',
|
||||
'opt_value': 'https://foo.bar/%s' % bootfile},
|
||||
{'ip_version': 6,
|
||||
'opt_name': 'tag:ipxe6,59',
|
||||
'opt_value': 'https://foo.bar/boot.ipxe'},
|
||||
]
|
||||
elif ip_version == 4 and not http_boot:
|
||||
expected_info = [{'opt_name': '67',
|
||||
'opt_value': bootfile,
|
||||
'ip_version': ip_version},
|
||||
@ -905,9 +923,41 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
'opt_value': '192.0.2.1',
|
||||
'ip_version': ip_version}
|
||||
]
|
||||
elif ip_version == 4 and http_boot:
|
||||
if not ipxe:
|
||||
expected_info = [
|
||||
{'ip_version': 4,
|
||||
'opt_name': '67',
|
||||
'opt_value': 'https://foo.bar/%s' % bootfile},
|
||||
{'ip_version': 4,
|
||||
'opt_name': '60',
|
||||
'opt_value': 'HTTPClient'}
|
||||
]
|
||||
else:
|
||||
expected_info = [
|
||||
{'ip_version': 4,
|
||||
'opt_name': 'tag:!ipxe,67',
|
||||
'opt_value': 'https://foo.bar/%s' % bootfile},
|
||||
{'ip_version': 4,
|
||||
'opt_name': 'tag:ipxe,67',
|
||||
'opt_value': 'https://foo.bar/boot.ipxe'},
|
||||
{'ip_version': 4,
|
||||
'opt_name': '60',
|
||||
'opt_value': 'HTTPClient'}
|
||||
]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
if ipxe:
|
||||
# Since we are using fake, we need to somehow assert it
|
||||
# with simplicity :\
|
||||
task.driver.boot.ipxe_enabled = True
|
||||
# NOTE(TheJulia): If we *are* testing ipxe, *always* call the
|
||||
# this method with ipxe_enabled set, because it informed via
|
||||
# the call, not via the task.
|
||||
self.assertEqual(expected_info,
|
||||
pxe_utils.dhcp_options_for_instance(task))
|
||||
pxe_utils.dhcp_options_for_instance(
|
||||
task, ipxe_enabled=ipxe,
|
||||
http_boot_enabled=http_boot))
|
||||
|
||||
def test_dhcp_options_for_instance(self):
|
||||
self.config(default_boot_mode='uefi', group='deploy')
|
||||
@ -926,6 +976,24 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
self.config(default_boot_mode='bios', group='deploy')
|
||||
self._dhcp_options_for_instance(ip_version=6)
|
||||
|
||||
def test_dhcp_options_for_instance_http_ipv4(self):
|
||||
self.config(default_boot_mode='uefi', group='deploy')
|
||||
self._dhcp_options_for_instance(ip_version=4, http_boot=True)
|
||||
|
||||
def test_dhcp_options_for_instance_http_ipv6(self):
|
||||
self.config(default_boot_mode='uefi', group='deploy')
|
||||
self._dhcp_options_for_instance(ip_version=6, http_boot=True)
|
||||
|
||||
def test_dhcp_options_for_instance_http_ipxe_ipv4(self):
|
||||
self.config(default_boot_mode='uefi', group='deploy')
|
||||
self._dhcp_options_for_instance(ip_version=4, ipxe=True,
|
||||
http_boot=True)
|
||||
|
||||
def test_dhcp_options_for_instance_http_ipxe_ipv6(self):
|
||||
self.config(default_boot_mode='uefi', group='deploy')
|
||||
self._dhcp_options_for_instance(ip_version=6, ipxe=True,
|
||||
http_boot=True)
|
||||
|
||||
def _test_get_kernel_ramdisk_info(self, expected_dir, mode='deploy',
|
||||
ipxe_enabled=False):
|
||||
node_uuid = 'fake-node'
|
||||
@ -1105,7 +1173,7 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
self.config(group='pxe', dir_permission=0o777)
|
||||
|
||||
def write_to_file(path, contents):
|
||||
self.assertEqual('/tftpboot/grub/grub.cfg', path)
|
||||
self.assertIn('/grub/grub.cfg', path)
|
||||
self.assertIn(
|
||||
'configfile /tftpboot/$net_default_mac.conf',
|
||||
contents
|
||||
@ -1115,9 +1183,18 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
wraps=write_to_file):
|
||||
pxe_utils.place_common_config()
|
||||
|
||||
mock_isdir.assert_called_once_with('/tftpboot/grub')
|
||||
mock_makedirs.assert_called_once_with('/tftpboot/grub', 511)
|
||||
mock_chmod.assert_called_once_with('/tftpboot/grub', 0o777)
|
||||
mock_isdir.assert_has_calls([
|
||||
mock.call('/tftpboot/grub'),
|
||||
mock.call('/httpboot/grub')
|
||||
])
|
||||
mock_makedirs.assert_has_calls([
|
||||
mock.call('/tftpboot/grub', 511),
|
||||
mock.call('/httpboot/grub', 511)
|
||||
])
|
||||
mock_chmod.assert_has_calls([
|
||||
mock.call('/tftpboot/grub', 0o777),
|
||||
mock.call('/httpboot/grub', 0o777)
|
||||
])
|
||||
|
||||
@mock.patch.object(os, 'makedirs', autospec=True)
|
||||
@mock.patch.object(os.path, 'isdir', autospec=True)
|
||||
@ -1133,9 +1210,12 @@ class TestPXEUtils(db_base.DbTestCase):
|
||||
with mock.patch('ironic.common.utils.write_to_file',
|
||||
autospec=True) as mock_write:
|
||||
pxe_utils.place_common_config()
|
||||
mock_write.assert_called_once()
|
||||
self.assertEqual(2, mock_write.call_count)
|
||||
|
||||
mock_isdir.assert_called_once_with('/tftpboot/grub')
|
||||
mock_isdir.assert_has_calls([
|
||||
mock.call('/tftpboot/grub'),
|
||||
mock.call('/httpboot/grub')
|
||||
])
|
||||
mock_makedirs.assert_not_called()
|
||||
mock_chmod.assert_not_called()
|
||||
|
||||
|
@ -35,6 +35,7 @@ from ironic.drivers import base as drivers_base
|
||||
from ironic.drivers.modules import agent_base
|
||||
from ironic.drivers.modules import boot_mode_utils
|
||||
from ironic.drivers.modules import deploy_utils
|
||||
from ironic.drivers.modules import fake
|
||||
from ironic.drivers.modules import ipxe
|
||||
from ironic.drivers.modules import pxe_base
|
||||
from ironic.drivers.modules.storage import noop as noop_storage
|
||||
@ -84,6 +85,11 @@ class iPXEBootTestCase(db_base.DbTestCase):
|
||||
self.port = obj_utils.create_test_port(self.context,
|
||||
node_id=self.node.id)
|
||||
|
||||
def test_ensure_boot_interface_is_not_http_enabled(self):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
self.assertFalse(task.driver.boot.http_boot_enabled)
|
||||
|
||||
def test_get_properties(self):
|
||||
expected = pxe_base.COMMON_PROPERTIES
|
||||
expected.update(agent_base.VENDOR_PROPERTIES)
|
||||
@ -964,3 +970,192 @@ class iPXEValidateRescueTestCase(db_base.DbTestCase):
|
||||
self.assertRaisesRegex(exception.MissingParameterValue,
|
||||
'Missing.*rescue_kernel',
|
||||
task.driver.boot.validate_rescue, task)
|
||||
|
||||
|
||||
@mock.patch.object(ipxe.iPXEHttpBoot, '__init__', lambda self: None)
|
||||
class iPXEHttpBootTestCase(db_base.DbTestCase):
|
||||
|
||||
driver = 'fake-hardware'
|
||||
boot_interface = 'http-ipxe'
|
||||
driver_info = DRV_INFO_DICT
|
||||
driver_internal_info = DRV_INTERNAL_INFO_DICT
|
||||
|
||||
def setUp(self):
|
||||
super(iPXEHttpBootTestCase, 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,
|
||||
'http-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)
|
||||
|
||||
def test_http_boot_enabled(self):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
self.assertTrue(task.driver.boot.http_boot_enabled)
|
||||
|
||||
# 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', autospec=True)
|
||||
@mock.patch.object(pxe_utils, 'get_instance_image_info', autospec=True)
|
||||
@mock.patch.object(pxe_utils, 'get_image_info', autospec=True)
|
||||
@mock.patch.object(pxe_utils, 'cache_ramdisk_kernel', autospec=True)
|
||||
@mock.patch.object(pxe_utils, '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,
|
||||
persistent=False):
|
||||
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, ipxe_enabled=True, ip_version=4, http_boot_enabled=True)
|
||||
dhcp_opts += pxe_utils.dhcp_options_for_instance(
|
||||
task, ipxe_enabled=True, ip_version=6, http_boot_enabled=True)
|
||||
task.driver.boot.prepare_ramdisk(task, {'foo': 'bar'})
|
||||
mock_deploy_img_info.assert_called_once_with(task.node, mode=mode,
|
||||
ipxe_enabled=True)
|
||||
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.UEFIHTTP,
|
||||
persistent=persistent)
|
||||
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'},
|
||||
ipxe_enabled=True)
|
||||
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'},
|
||||
ipxe_enabled=True)
|
||||
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'},
|
||||
ipxe_enabled=True)
|
||||
elif mode == 'rescue':
|
||||
mock_cache_r_k.assert_called_once_with(
|
||||
task, {'rescue_kernel': 'a', 'rescue_ramdisk': 'r'},
|
||||
ipxe_enabled=True)
|
||||
mock_pxe_config.assert_called_once_with(
|
||||
task, {}, CONF.pxe.ipxe_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(ipxe.iPXEHttpBoot, '__init__', lambda self: None)
|
||||
class iPXEBootBaseUtils(db_base.DbTestCase):
|
||||
|
||||
driver = 'fake-hardware'
|
||||
boot_interface = 'http-ipxe'
|
||||
driver_info = DRV_INFO_DICT
|
||||
driver_internal_info = DRV_INTERNAL_INFO_DICT
|
||||
|
||||
def setUp(self):
|
||||
super(iPXEBootBaseUtils, self).setUp()
|
||||
self.context.auth_token = 'fake'
|
||||
instance_info = INST_INFO_DICT
|
||||
|
||||
self.config(enabled_boot_interfaces=[self.boot_interface,
|
||||
'http-ipxe'])
|
||||
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)
|
||||
|
||||
@mock.patch.object(fake.FakeManagement, 'set_boot_device', autospec=True)
|
||||
def test__node_set_boot_device_for_network_boot(self, mock_set_boot_dev):
|
||||
mock_set_boot_dev.side_effect = [
|
||||
exception.InvalidParameterValue('Invalid boot device'),
|
||||
None
|
||||
]
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
task.driver.boot._node_set_boot_device_for_network_boot(
|
||||
task, persistent=True)
|
||||
mock_set_boot_dev.assert_has_calls([
|
||||
mock.call(mock.ANY, task, boot_devices.UEFIHTTP,
|
||||
persistent=True),
|
||||
mock.call(mock.ANY, task, boot_devices.PXE,
|
||||
persistent=True)
|
||||
])
|
||||
|
@ -934,6 +934,11 @@ class PXEValidateRescueTestCase(db_base.DbTestCase):
|
||||
'Missing.*rescue_kernel',
|
||||
task.driver.boot.validate_rescue, task)
|
||||
|
||||
def test_http_boot_not_enabled(self):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
self.assertFalse(task.driver.boot.http_boot_enabled)
|
||||
|
||||
|
||||
@mock.patch.object(ipxe.iPXEBoot, '__init__', lambda self: None)
|
||||
@mock.patch.object(pxe.PXEBoot, '__init__', lambda self: None)
|
||||
@ -1039,3 +1044,156 @@ class iPXEBootRetryTestCase(PXEBootRetryTestCase):
|
||||
|
||||
boot_interface = 'ipxe'
|
||||
boot_interface_class = ipxe.iPXEBoot
|
||||
|
||||
|
||||
@mock.patch.object(pxe.HttpBoot, '__init__', lambda self: None)
|
||||
class HttpBootTestCase(db_base.DbTestCase):
|
||||
driver = 'fake-hardware'
|
||||
boot_interface = 'http'
|
||||
driver_info = DRV_INFO_DICT
|
||||
driver_internal_info = DRV_INTERNAL_INFO_DICT
|
||||
|
||||
def setUp(self):
|
||||
super(HttpBootTestCase, 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,
|
||||
'http', '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)
|
||||
|
||||
def test_http_boot_enabled(self):
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
self.assertTrue(task.driver.boot.http_boot_enabled)
|
||||
|
||||
# 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', autospec=True)
|
||||
@mock.patch.object(pxe_utils, 'get_instance_image_info', autospec=True)
|
||||
@mock.patch.object(pxe_utils, 'get_image_info', autospec=True)
|
||||
@mock.patch.object(pxe_utils, 'cache_ramdisk_kernel', autospec=True)
|
||||
@mock.patch.object(pxe_utils, '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,
|
||||
persistent=False):
|
||||
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, ipxe_enabled=False, ip_version=4, http_boot_enabled=True)
|
||||
dhcp_opts += pxe_utils.dhcp_options_for_instance(
|
||||
task, ipxe_enabled=False, ip_version=6, http_boot_enabled=True)
|
||||
task.driver.boot.prepare_ramdisk(task, {'foo': 'bar'})
|
||||
if task.driver.boot.http_boot_enabled:
|
||||
# FIXME(TheJulia): We need to change the parameter
|
||||
# name on some of the pxe internal calls because
|
||||
# they boil down to "use the http folder" or
|
||||
# "use the tftp folder"
|
||||
use_http_folder = True
|
||||
else:
|
||||
use_http_folder = False
|
||||
mock_deploy_img_info.assert_called_once_with(
|
||||
task.node, mode=mode, ipxe_enabled=use_http_folder)
|
||||
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.UEFIHTTP,
|
||||
persistent=persistent)
|
||||
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'},
|
||||
ipxe_enabled=use_http_folder)
|
||||
mock_instance_img_info.assert_called_once_with(
|
||||
task, ipxe_enabled=False)
|
||||
elif not cleaning and mode == 'deploy':
|
||||
mock_cache_r_k.assert_called_once_with(
|
||||
task,
|
||||
{'deploy_kernel': 'a', 'deploy_ramdisk': 'r',
|
||||
'kernel': 'b'},
|
||||
ipxe_enabled=use_http_folder)
|
||||
mock_instance_img_info.assert_called_once_with(
|
||||
task, ipxe_enabled=False)
|
||||
elif mode == 'deploy':
|
||||
mock_cache_r_k.assert_called_once_with(
|
||||
task, {'deploy_kernel': 'a', 'deploy_ramdisk': 'r'},
|
||||
ipxe_enabled=use_http_folder)
|
||||
elif mode == 'rescue':
|
||||
mock_cache_r_k.assert_called_once_with(
|
||||
task, {'rescue_kernel': 'a', 'rescue_ramdisk': 'r'},
|
||||
ipxe_enabled=use_http_folder)
|
||||
mock_pxe_config.assert_called_once_with(
|
||||
task, {}, CONF.pxe.uefi_pxe_config_template,
|
||||
ipxe_enabled=False)
|
||||
|
||||
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)
|
||||
|
@ -0,0 +1,21 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds a ``http`` boot interface, based upon the ``pxe`` boot interface
|
||||
which informs the DHCP server of an HTTP URL to boot the machine from,
|
||||
and then requests the BMC boot the machine in UEFI HTTP mode.
|
||||
- |
|
||||
Adds a ``http-ipxe`` boot interface, based upon the ``ipxe`` boot interface
|
||||
which informs the DHCP server of an HTTP URL to boot the machine from,
|
||||
and then requests the BMC boot the machine in UEFI HTTP mode.
|
||||
issues:
|
||||
- |
|
||||
Testing of the ``http`` boot interface with Ubuntu 22.04 provided Grub2
|
||||
yielded some intermittent failures which appear to be more environmental
|
||||
in nature as the signed Shim loader would start, then load the GRUB
|
||||
loader, and then some of the expected files might be attempted to be
|
||||
accessed, and then fail due to an apparent transfer timeout. Consultation
|
||||
with some grub developers concur this is likely environmental, meaning
|
||||
the specific grub build or CI performance related. If you encounter any
|
||||
issues, please do not hestitate to reach out to the Ironic developer
|
||||
community.
|
@ -81,6 +81,8 @@ ironic.hardware.interfaces.boot =
|
||||
pxe = ironic.drivers.modules.pxe:PXEBoot
|
||||
redfish-virtual-media = ironic.drivers.modules.redfish.boot:RedfishVirtualMediaBoot
|
||||
redfish-https = ironic.drivers.modules.redfish.boot:RedfishHttpsBoot
|
||||
http = ironic.drivers.modules.pxe:HttpBoot
|
||||
http-ipxe = ironic.drivers.modules.ipxe:iPXEHttpBoot
|
||||
|
||||
ironic.hardware.interfaces.console =
|
||||
fake = ironic.drivers.modules.fake:FakeConsole
|
||||
|
Loading…
Reference in New Issue
Block a user