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
|
Additional configuration is required for this boot interface - see
|
||||||
:doc:`/install/configure-pxe` for details.
|
: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
|
Kernel parameters
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -48,6 +48,13 @@ is being worked in Neutron
|
|||||||
`change 890683 <https://review.opendev.org/c/openstack/neutron/+/890683>`_ and
|
`change 890683 <https://review.opendev.org/c/openstack/neutron/+/890683>`_ and
|
||||||
`bug 20305201 <https://bugs.launchpad.net/neutron/+bug/20305201>`_.
|
`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
|
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
|
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
|
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
|
.. Warning:: This docuemntation is geared for use of OVS with Neutron along
|
||||||
with the ``neutron-dhcp-agent``. It *is* possible to use OVN
|
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
|
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
|
#. Set the ``[dhcp]/dhcp_provider`` to ``neutron`` in the Bare Metal Service's
|
||||||
configuration file (``/etc/ironic/ironic.conf``):
|
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
|
defaults, and when you create subnet, DHCP is also enabled if you do not add
|
||||||
any dhcp options at "openstack subnet create" command.
|
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.
|
#. 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
|
Finally, put ``ironic-python-agent.kernel`` and
|
||||||
``ironic-python-agent.initramfs`` to ``/httpboot``.
|
``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
|
* The conductor needs access to the `management controller`_ of each node
|
||||||
it manages.
|
it manages.
|
||||||
|
|
||||||
* The conductor co-exists with TFTP (for PXE) and/or HTTP (for iPXE) services
|
* The conductor co-exists with TFTP (for PXE) and/or HTTP (for HTTPBoot and
|
||||||
that provide the kernel and ramdisk to boot the nodes. The conductor
|
iPXE) services that provide the kernel and ramdisk to boot the nodes.
|
||||||
manages them by writing files to their root directories.
|
The conductor manages them by writing files to their root directories.
|
||||||
|
|
||||||
* If serial console is used, the conductor launches console processes
|
* If serial console is used, the conductor launches console processes
|
||||||
locally. If the ``nova-serialproxy`` service (part of the Compute service)
|
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
|
PXE_CFG_DIR_NAME = CONF.pxe.pxe_config_subdir
|
||||||
|
|
||||||
|
DHCP_VENDOR_CLASS_ID = '60' # rfc2132
|
||||||
DHCP_CLIENT_ID = '61' # rfc2132
|
DHCP_CLIENT_ID = '61' # rfc2132
|
||||||
DHCP_TFTP_SERVER_NAME = '66' # rfc2132
|
DHCP_TFTP_SERVER_NAME = '66' # rfc2132
|
||||||
DHCP_BOOTFILE_NAME = '67' # rfc2132
|
DHCP_BOOTFILE_NAME = '67' # rfc2132
|
||||||
@ -66,8 +67,8 @@ KERNEL_RAMDISK_LABELS = {'deploy': DEPLOY_KERNEL_RAMDISK_LABELS,
|
|||||||
'rescue': RESCUE_KERNEL_RAMDISK_LABELS}
|
'rescue': RESCUE_KERNEL_RAMDISK_LABELS}
|
||||||
|
|
||||||
|
|
||||||
def _get_root_dir(ipxe_enabled):
|
def _get_root_dir(use_http_root):
|
||||||
if ipxe_enabled:
|
if use_http_root:
|
||||||
return CONF.deploy.http_root
|
return CONF.deploy.http_root
|
||||||
else:
|
else:
|
||||||
return CONF.pxe.tftp_root
|
return CONF.pxe.tftp_root
|
||||||
@ -239,7 +240,8 @@ def get_kernel_ramdisk_info(node_uuid, driver_info, mode='deploy',
|
|||||||
return image_info
|
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.
|
"""Generate the path for the node's PXE configuration file.
|
||||||
|
|
||||||
:param node_uuid: the UUID of the node.
|
: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.
|
: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):
|
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))
|
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.
|
"""Returns the appropriate file or URL.
|
||||||
|
|
||||||
:param task: A TaskManager object.
|
: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
|
:param ip_version: Integer representing the version of IP of
|
||||||
to return options for DHCP. Possible options
|
to return options for DHCP. Possible options
|
||||||
are 4, and 6.
|
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:
|
try:
|
||||||
if task.driver.boot.ipxe_enabled:
|
if task.driver.boot.ipxe_enabled:
|
||||||
boot_file = deploy_utils.get_ipxe_boot_file(task.node)
|
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
|
# NOTE(TheJulia): There are additional cases as we add new
|
||||||
# features, so the logic below is in the form of if/elif/elif
|
# features, so the logic below is in the form of if/elif/elif
|
||||||
if not urlboot:
|
if not urlboot:
|
||||||
return boot_file
|
result = boot_file
|
||||||
elif urlboot:
|
elif urlboot and not http_boot_enabled:
|
||||||
if CONF.my_ipv6 and ip_version == 6:
|
if CONF.my_ipv6 and ip_version == 6:
|
||||||
host = utils.wrap_ipv6(CONF.my_ipv6)
|
host = utils.wrap_ipv6(CONF.my_ipv6)
|
||||||
else:
|
elif not http_boot_enabled:
|
||||||
host = utils.wrap_ipv6(CONF.pxe.tftp_server)
|
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)
|
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,
|
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.
|
"""Retrieves the DHCP PXE boot options.
|
||||||
|
|
||||||
:param task: A TaskManager instance.
|
: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.
|
Possible options are integers 4 or 6.
|
||||||
:returns: Dictionary to be sent to the networking service describing
|
:returns: Dictionary to be sent to the networking service describing
|
||||||
the DHCP options to be set.
|
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:
|
if ip_version:
|
||||||
|
# IP version defines *which* parameter is used for file name.
|
||||||
use_ip_version = ip_version
|
use_ip_version = ip_version
|
||||||
else:
|
else:
|
||||||
use_ip_version = int(CONF.pxe.ip_version)
|
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.
|
# a URL reply.
|
||||||
boot_file_param = DHCPV6_BOOTFILE_NAME
|
boot_file_param = DHCPV6_BOOTFILE_NAME
|
||||||
url_boot = True
|
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
|
# NOTE(TheJulia): The ip_version value config from the PXE config is
|
||||||
# guarded in the configuration, so there is no real sense in having
|
# guarded in the configuration, so there is no real sense in having
|
||||||
# anything else here in the event the value is something aside from
|
# anything else here in the event the value is something aside from
|
||||||
# 4 or 6, as there are no other possible values.
|
# 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:
|
if ipxe_enabled:
|
||||||
# TODO(TheJulia): DHCPv6 through dnsmasq + ipxe matching simply
|
# 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:
|
else:
|
||||||
dhcp_opts.append({'opt_name': boot_file_param,
|
dhcp_opts.append({'opt_name': boot_file_param,
|
||||||
'opt_value': boot_file})
|
'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:
|
if not url_boot:
|
||||||
dhcp_opts.append({'opt_name': DHCP_TFTP_SERVER_NAME,
|
dhcp_opts.append({'opt_name': DHCP_TFTP_SERVER_NAME,
|
||||||
@ -1168,7 +1205,8 @@ def prepare_instance_pxe_config(task, image_info,
|
|||||||
iscsi_boot=False,
|
iscsi_boot=False,
|
||||||
ramdisk_boot=False,
|
ramdisk_boot=False,
|
||||||
ipxe_enabled=False,
|
ipxe_enabled=False,
|
||||||
anaconda_boot=False):
|
anaconda_boot=False,
|
||||||
|
http_boot_enabled=False):
|
||||||
"""Prepares the config file for PXE boot
|
"""Prepares the config file for PXE boot
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
: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
|
:param ipxe_enabled: Default false boolean to indicate if ipxe
|
||||||
is in use by the caller.
|
is in use by the caller.
|
||||||
:param anaconda_boot: if the boot is to a anaconda ramdisk configuration.
|
: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
|
:returns: None
|
||||||
"""
|
"""
|
||||||
node = task.node
|
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
|
# development cycle so that we call a single method and return
|
||||||
# combined options. The method we currently call is relied upon
|
# combined options. The method we currently call is relied upon
|
||||||
# by two eternal projects, to changing the behavior is not ideal.
|
# by two eternal projects, to changing the behavior is not ideal.
|
||||||
dhcp_opts = dhcp_options_for_instance(task, ipxe_enabled,
|
dhcp_opts = dhcp_options_for_instance(
|
||||||
ip_version=4)
|
task, ipxe_enabled,
|
||||||
dhcp_opts += dhcp_options_for_instance(task, ipxe_enabled,
|
ip_version=4,
|
||||||
ip_version=6)
|
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 = dhcp_factory.DHCPFactory()
|
||||||
provider.update_dhcp(task, dhcp_opts)
|
provider.update_dhcp(task, dhcp_opts)
|
||||||
pxe_config_path = get_pxe_config_file_path(
|
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):
|
if not os.path.isfile(pxe_config_path):
|
||||||
pxe_options = build_pxe_config_options(
|
pxe_options = build_pxe_config_options(
|
||||||
task, image_info, service=ramdisk_boot or anaconda_boot,
|
task, image_info, service=ramdisk_boot or anaconda_boot,
|
||||||
@ -1354,15 +1399,16 @@ def place_common_config():
|
|||||||
if not CONF.pxe.initial_grub_template:
|
if not CONF.pxe.initial_grub_template:
|
||||||
return
|
return
|
||||||
|
|
||||||
grub_dir_path = os.path.join(_get_root_dir(False), 'grub')
|
for use_http in [False, True]:
|
||||||
if not os.path.isdir(grub_dir_path):
|
# Create paths
|
||||||
fileutils.ensure_tree(grub_dir_path)
|
grub_dir_path = os.path.join(_get_root_dir(use_http), 'grub')
|
||||||
if CONF.pxe.dir_permission:
|
if not os.path.isdir(grub_dir_path):
|
||||||
os.chmod(grub_dir_path, CONF.pxe.dir_permission)
|
fileutils.ensure_tree(grub_dir_path)
|
||||||
|
if CONF.pxe.dir_permission:
|
||||||
initial_grub = utils.render_template(
|
os.chmod(grub_dir_path, CONF.pxe.dir_permission)
|
||||||
CONF.pxe.initial_grub_template,
|
# Write templates
|
||||||
{'tftp_root': _get_root_dir(False)})
|
initial_grub = utils.render_template(
|
||||||
initial_grub_path = os.path.join(grub_dir_path, 'grub.cfg')
|
CONF.pxe.initial_grub_template,
|
||||||
|
{'tftp_root': _get_root_dir(False)})
|
||||||
utils.write_to_file(initial_grub_path, initial_grub)
|
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
|
@property
|
||||||
def supported_boot_interfaces(self):
|
def supported_boot_interfaces(self):
|
||||||
"""List of supported boot interfaces."""
|
"""List of supported boot interfaces."""
|
||||||
return [ipxe.iPXEBoot, pxe.PXEBoot]
|
return [ipxe.iPXEBoot, pxe.PXEBoot, ipxe.iPXEHttpBoot, pxe.HttpBoot]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_deploy_interfaces(self):
|
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)
|
pxe_utils.place_loaders_for_boot(CONF.deploy.http_root)
|
||||||
# This is required to serve the iPXE binary via tftp
|
# This is required to serve the iPXE binary via tftp
|
||||||
pxe_utils.place_loaders_for_boot(CONF.pxe.tftp_root)
|
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)
|
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,
|
class PXEAnacondaDeploy(agent_base.AgentBaseMixin, agent_base.HeartbeatMixin,
|
||||||
base.DeployInterface):
|
base.DeployInterface):
|
||||||
|
|
||||||
|
@ -63,6 +63,8 @@ class PXEBaseMixin(object):
|
|||||||
|
|
||||||
ipxe_enabled = False
|
ipxe_enabled = False
|
||||||
|
|
||||||
|
http_boot_enabled = False
|
||||||
|
|
||||||
def get_properties(self):
|
def get_properties(self):
|
||||||
"""Return the properties of the interface.
|
"""Return the properties of the interface.
|
||||||
|
|
||||||
@ -70,6 +72,13 @@ class PXEBaseMixin(object):
|
|||||||
"""
|
"""
|
||||||
return COMMON_PROPERTIES
|
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')
|
@METRICS.timer('PXEBaseMixin.clean_up_ramdisk')
|
||||||
def clean_up_ramdisk(self, task):
|
def clean_up_ramdisk(self, task):
|
||||||
"""Cleans up the boot of ironic ramdisk.
|
"""Cleans up the boot of ironic ramdisk.
|
||||||
@ -90,14 +99,14 @@ class PXEBaseMixin(object):
|
|||||||
mode = deploy_utils.rescue_or_deploy_mode(node)
|
mode = deploy_utils.rescue_or_deploy_mode(node)
|
||||||
try:
|
try:
|
||||||
images_info = pxe_utils.get_image_info(
|
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:
|
except exception.MissingParameterValue as e:
|
||||||
LOG.warning('Could not get %(mode)s image info '
|
LOG.warning('Could not get %(mode)s image info '
|
||||||
'to clean up images for node %(node)s: %(err)s',
|
'to clean up images for node %(node)s: %(err)s',
|
||||||
{'mode': mode, 'node': node.uuid, 'err': e})
|
{'mode': mode, 'node': node.uuid, 'err': e})
|
||||||
else:
|
else:
|
||||||
pxe_utils.clean_up_pxe_env(
|
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')
|
@METRICS.timer('PXEBaseMixin.clean_up_instance')
|
||||||
def clean_up_instance(self, task):
|
def clean_up_instance(self, task):
|
||||||
@ -114,14 +123,14 @@ class PXEBaseMixin(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
images_info = pxe_utils.get_instance_image_info(
|
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:
|
except exception.MissingParameterValue as e:
|
||||||
LOG.warning('Could not get instance image info '
|
LOG.warning('Could not get instance image info '
|
||||||
'to clean up images for node %(node)s: %(err)s',
|
'to clean up images for node %(node)s: %(err)s',
|
||||||
{'node': node.uuid, 'err': e})
|
{'node': node.uuid, 'err': e})
|
||||||
else:
|
else:
|
||||||
pxe_utils.clean_up_pxe_env(task, images_info,
|
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)
|
boot_mode_utils.deconfigure_secure_boot_if_needed(task)
|
||||||
|
|
||||||
@ -146,7 +155,6 @@ class PXEBaseMixin(object):
|
|||||||
operation failed on the node.
|
operation failed on the node.
|
||||||
"""
|
"""
|
||||||
node = task.node
|
node = task.node
|
||||||
|
|
||||||
# Label indicating a deploy or rescue operation being carried out on
|
# Label indicating a deploy or rescue operation being carried out on
|
||||||
# the node, 'deploy' or 'rescue'. Unless the node is in a rescue like
|
# the node, 'deploy' or 'rescue'. Unless the node is in a rescue like
|
||||||
# state, the mode is set to 'deploy', indicating deploy operation is
|
# 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
|
# combined options. The method we currently call is relied upon
|
||||||
# by two eternal projects, to changing the behavior is not ideal.
|
# by two eternal projects, to changing the behavior is not ideal.
|
||||||
dhcp_opts = pxe_utils.dhcp_options_for_instance(
|
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(
|
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 = dhcp_factory.DHCPFactory()
|
||||||
provider.update_dhcp(task, dhcp_opts)
|
provider.update_dhcp(task, dhcp_opts)
|
||||||
|
# TODO(TheJulia): We need to change the parameter name for
|
||||||
pxe_info = pxe_utils.get_image_info(node, mode=mode,
|
# ipxe_enabled in pxe_utils at some point since here it is
|
||||||
ipxe_enabled=self.ipxe_enabled)
|
# 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
|
# NODE: Try to validate and fetch instance images only
|
||||||
# if we are in DEPLOYING state.
|
# if we are in DEPLOYING state.
|
||||||
@ -201,8 +214,7 @@ class PXEBaseMixin(object):
|
|||||||
pxe_utils.create_pxe_config(task, pxe_options,
|
pxe_utils.create_pxe_config(task, pxe_options,
|
||||||
pxe_config_template,
|
pxe_config_template,
|
||||||
ipxe_enabled=self.ipxe_enabled)
|
ipxe_enabled=self.ipxe_enabled)
|
||||||
manager_utils.node_set_boot_device(task, boot_devices.PXE,
|
self._node_set_boot_device_for_network_boot(task)
|
||||||
persistent=False)
|
|
||||||
|
|
||||||
if self.ipxe_enabled and CONF.pxe.ipxe_use_swift:
|
if self.ipxe_enabled and CONF.pxe.ipxe_use_swift:
|
||||||
kernel_label = '%s_kernel' % mode
|
kernel_label = '%s_kernel' % mode
|
||||||
@ -211,13 +223,43 @@ class PXEBaseMixin(object):
|
|||||||
pxe_info.pop(ramdisk_label, None)
|
pxe_info.pop(ramdisk_label, None)
|
||||||
|
|
||||||
if pxe_info:
|
if pxe_info:
|
||||||
pxe_utils.cache_ramdisk_kernel(task, pxe_info,
|
pxe_utils.cache_ramdisk_kernel(
|
||||||
ipxe_enabled=self.ipxe_enabled)
|
task, pxe_info,
|
||||||
|
ipxe_enabled=self._use_http_folder())
|
||||||
|
|
||||||
LOG.debug('Ramdisk (i)PXE boot for node %(node)s has been prepared '
|
LOG.debug('Ramdisk (i)PXE boot for node %(node)s has been prepared '
|
||||||
'with kernel params %(params)s',
|
'with kernel params %(params)s',
|
||||||
{'node': node.uuid, 'params': pxe_options})
|
{'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')
|
@METRICS.timer('PXEBaseMixin.prepare_instance')
|
||||||
def prepare_instance(self, task):
|
def prepare_instance(self, task):
|
||||||
"""Prepares the boot of instance.
|
"""Prepares the boot of instance.
|
||||||
@ -241,8 +283,9 @@ class PXEBaseMixin(object):
|
|||||||
if boot_option == "ramdisk" or boot_option == "kickstart":
|
if boot_option == "ramdisk" or boot_option == "kickstart":
|
||||||
instance_image_info = pxe_utils.get_instance_image_info(
|
instance_image_info = pxe_utils.get_instance_image_info(
|
||||||
task, ipxe_enabled=self.ipxe_enabled)
|
task, ipxe_enabled=self.ipxe_enabled)
|
||||||
pxe_utils.cache_ramdisk_kernel(task, instance_image_info,
|
pxe_utils.cache_ramdisk_kernel(
|
||||||
ipxe_enabled=self.ipxe_enabled)
|
task, instance_image_info,
|
||||||
|
ipxe_enabled=self._use_http_folder())
|
||||||
if 'ks_template' in instance_image_info:
|
if 'ks_template' in instance_image_info:
|
||||||
ks_cfg = pxe_utils.validate_kickstart_template(
|
ks_cfg = pxe_utils.validate_kickstart_template(
|
||||||
instance_image_info['ks_template'][1]
|
instance_image_info['ks_template'][1]
|
||||||
@ -256,7 +299,8 @@ class PXEBaseMixin(object):
|
|||||||
iscsi_boot=deploy_utils.is_iscsi_boot(task),
|
iscsi_boot=deploy_utils.is_iscsi_boot(task),
|
||||||
ramdisk_boot=(boot_option == "ramdisk"),
|
ramdisk_boot=(boot_option == "ramdisk"),
|
||||||
anaconda_boot=(boot_option == "kickstart"),
|
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(
|
pxe_utils.prepare_instance_kickstart_config(
|
||||||
task, instance_image_info,
|
task, instance_image_info,
|
||||||
anaconda_boot=(boot_option == "kickstart"))
|
anaconda_boot=(boot_option == "kickstart"))
|
||||||
@ -285,8 +329,14 @@ class PXEBaseMixin(object):
|
|||||||
# during takeover
|
# during takeover
|
||||||
if boot_device and (task.node.provision_state not in
|
if boot_device and (task.node.provision_state not in
|
||||||
(states.ACTIVE, states.ADOPTING)):
|
(states.ACTIVE, states.ADOPTING)):
|
||||||
manager_utils.node_set_boot_device(task, boot_device,
|
if boot_device == boot_devices.PXE:
|
||||||
persistent=True)
|
# 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)
|
||||||
|
|
||||||
def _validate_common(self, task):
|
def _validate_common(self, task):
|
||||||
node = task.node
|
node = task.node
|
||||||
@ -423,8 +473,7 @@ class PXEBaseMixin(object):
|
|||||||
'timeout': CONF.pxe.boot_retry_timeout})
|
'timeout': CONF.pxe.boot_retry_timeout})
|
||||||
|
|
||||||
manager_utils.node_power_action(task, states.POWER_OFF)
|
manager_utils.node_power_action(task, states.POWER_OFF)
|
||||||
manager_utils.node_set_boot_device(task, boot_devices.PXE,
|
self._node_set_boot_device_for_network_boot(task)
|
||||||
persistent=False)
|
|
||||||
manager_utils.node_power_action(task, states.POWER_ON)
|
manager_utils.node_power_action(task, states.POWER_ON)
|
||||||
|
|
||||||
|
|
||||||
|
@ -858,7 +858,8 @@ class TestPXEUtils(db_base.DbTestCase):
|
|||||||
'config'),
|
'config'),
|
||||||
pxe_utils.get_pxe_config_file_path(self.node.uuid))
|
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')
|
self.config(ip_version=ip_version, group='pxe')
|
||||||
if ip_version == 4:
|
if ip_version == 4:
|
||||||
self.config(tftp_server='192.0.2.1', group='pxe')
|
self.config(tftp_server='192.0.2.1', group='pxe')
|
||||||
@ -878,12 +879,14 @@ class TestPXEUtils(db_base.DbTestCase):
|
|||||||
else:
|
else:
|
||||||
self.config(pxe_bootfile_name='fake-bootfile', group='pxe')
|
self.config(pxe_bootfile_name='fake-bootfile', group='pxe')
|
||||||
self.config(tftp_root='/tftp-path', group='pxe')
|
self.config(tftp_root='/tftp-path', group='pxe')
|
||||||
|
if http_boot:
|
||||||
|
self.config(http_url='https://foo.bar', group='deploy')
|
||||||
if ipxe:
|
if ipxe:
|
||||||
bootfile = 'fake-bootfile-ipxe'
|
bootfile = 'fake-bootfile-ipxe'
|
||||||
else:
|
else:
|
||||||
bootfile = 'fake-bootfile'
|
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
|
# NOTE(TheJulia): DHCPv6 RFCs seem to indicate that the prior
|
||||||
# options are not imported, although they may be supported
|
# options are not imported, although they may be supported
|
||||||
# by vendors. The apparent proper option is to return a
|
# by vendors. The apparent proper option is to return a
|
||||||
@ -891,7 +894,22 @@ class TestPXEUtils(db_base.DbTestCase):
|
|||||||
expected_info = [{'opt_name': '59',
|
expected_info = [{'opt_name': '59',
|
||||||
'opt_value': 'tftp://[ff80::1]/%s' % bootfile,
|
'opt_value': 'tftp://[ff80::1]/%s' % bootfile,
|
||||||
'ip_version': ip_version}]
|
'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',
|
expected_info = [{'opt_name': '67',
|
||||||
'opt_value': bootfile,
|
'opt_value': bootfile,
|
||||||
'ip_version': ip_version},
|
'ip_version': ip_version},
|
||||||
@ -905,9 +923,41 @@ class TestPXEUtils(db_base.DbTestCase):
|
|||||||
'opt_value': '192.0.2.1',
|
'opt_value': '192.0.2.1',
|
||||||
'ip_version': ip_version}
|
'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:
|
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,
|
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):
|
def test_dhcp_options_for_instance(self):
|
||||||
self.config(default_boot_mode='uefi', group='deploy')
|
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.config(default_boot_mode='bios', group='deploy')
|
||||||
self._dhcp_options_for_instance(ip_version=6)
|
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',
|
def _test_get_kernel_ramdisk_info(self, expected_dir, mode='deploy',
|
||||||
ipxe_enabled=False):
|
ipxe_enabled=False):
|
||||||
node_uuid = 'fake-node'
|
node_uuid = 'fake-node'
|
||||||
@ -1105,7 +1173,7 @@ class TestPXEUtils(db_base.DbTestCase):
|
|||||||
self.config(group='pxe', dir_permission=0o777)
|
self.config(group='pxe', dir_permission=0o777)
|
||||||
|
|
||||||
def write_to_file(path, contents):
|
def write_to_file(path, contents):
|
||||||
self.assertEqual('/tftpboot/grub/grub.cfg', path)
|
self.assertIn('/grub/grub.cfg', path)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
'configfile /tftpboot/$net_default_mac.conf',
|
'configfile /tftpboot/$net_default_mac.conf',
|
||||||
contents
|
contents
|
||||||
@ -1115,9 +1183,18 @@ class TestPXEUtils(db_base.DbTestCase):
|
|||||||
wraps=write_to_file):
|
wraps=write_to_file):
|
||||||
pxe_utils.place_common_config()
|
pxe_utils.place_common_config()
|
||||||
|
|
||||||
mock_isdir.assert_called_once_with('/tftpboot/grub')
|
mock_isdir.assert_has_calls([
|
||||||
mock_makedirs.assert_called_once_with('/tftpboot/grub', 511)
|
mock.call('/tftpboot/grub'),
|
||||||
mock_chmod.assert_called_once_with('/tftpboot/grub', 0o777)
|
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, 'makedirs', autospec=True)
|
||||||
@mock.patch.object(os.path, 'isdir', 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',
|
with mock.patch('ironic.common.utils.write_to_file',
|
||||||
autospec=True) as mock_write:
|
autospec=True) as mock_write:
|
||||||
pxe_utils.place_common_config()
|
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_makedirs.assert_not_called()
|
||||||
mock_chmod.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 agent_base
|
||||||
from ironic.drivers.modules import boot_mode_utils
|
from ironic.drivers.modules import boot_mode_utils
|
||||||
from ironic.drivers.modules import deploy_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 ipxe
|
||||||
from ironic.drivers.modules import pxe_base
|
from ironic.drivers.modules import pxe_base
|
||||||
from ironic.drivers.modules.storage import noop as noop_storage
|
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,
|
self.port = obj_utils.create_test_port(self.context,
|
||||||
node_id=self.node.id)
|
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):
|
def test_get_properties(self):
|
||||||
expected = pxe_base.COMMON_PROPERTIES
|
expected = pxe_base.COMMON_PROPERTIES
|
||||||
expected.update(agent_base.VENDOR_PROPERTIES)
|
expected.update(agent_base.VENDOR_PROPERTIES)
|
||||||
@ -964,3 +970,192 @@ class iPXEValidateRescueTestCase(db_base.DbTestCase):
|
|||||||
self.assertRaisesRegex(exception.MissingParameterValue,
|
self.assertRaisesRegex(exception.MissingParameterValue,
|
||||||
'Missing.*rescue_kernel',
|
'Missing.*rescue_kernel',
|
||||||
task.driver.boot.validate_rescue, task)
|
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',
|
'Missing.*rescue_kernel',
|
||||||
task.driver.boot.validate_rescue, task)
|
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(ipxe.iPXEBoot, '__init__', lambda self: None)
|
||||||
@mock.patch.object(pxe.PXEBoot, '__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 = 'ipxe'
|
||||||
boot_interface_class = ipxe.iPXEBoot
|
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
|
pxe = ironic.drivers.modules.pxe:PXEBoot
|
||||||
redfish-virtual-media = ironic.drivers.modules.redfish.boot:RedfishVirtualMediaBoot
|
redfish-virtual-media = ironic.drivers.modules.redfish.boot:RedfishVirtualMediaBoot
|
||||||
redfish-https = ironic.drivers.modules.redfish.boot:RedfishHttpsBoot
|
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 =
|
ironic.hardware.interfaces.console =
|
||||||
fake = ironic.drivers.modules.fake:FakeConsole
|
fake = ironic.drivers.modules.fake:FakeConsole
|
||||||
|
Loading…
Reference in New Issue
Block a user