Provide a path to set explicit ipxe bootloaders

I did something stupid when started driving forth the split of ipxe
from the pxe interface: I didn't think about the need to actually
separate bootloaders. In part, because the use case was a mixed
Power8/Power9 and x86 cluster. Mainly because the Power hardware
does not honor or care about the bootfile name provided over DHCP.
The firmware knows how to read the PXELINUX boot file format
and the machines are able to boot from there.

Where this all goes sideways is when:
* Enabled boot interfaces are set to ipxe,pxe
* No default boot interface is set
* Node is created without a default for x86 hardware.
* Node uses ipxe boot_interface, and creates files under /httpboot
* bootfile transmitted via DHCP is pxelinux.0.

Fun right?

The simple workaround for the power user is to just define the iPXE
loader, or maybe use UEFI. But that is neither here nor there, this
is still a bug and a possible use case is GRUB2 via PXE and iPXE.
Not that would really work via ipxe, but hopefully people get the
idea.

The solution kind of seems clear, duplicate configuration and
fallback if not defined.

Story: #2007003
Task: #40282
Change-Id: I4419254c23095929e52a0fda11789f2f5167dc6b
This commit is contained in:
Julia Kreger 2020-05-14 17:43:08 -07:00
parent 36ea661bfc
commit 5f7d84f483
10 changed files with 227 additions and 53 deletions

View File

@ -1691,10 +1691,8 @@ function configure_ironic_conductor {
local pxebin
pxebin=`basename $IRONIC_PXE_BOOT_IMAGE`
uefipxebin=`basename $(get_uefi_ipxe_boot_file)`
iniset $IRONIC_CONF_FILE pxe pxe_config_template '$pybasedir/drivers/modules/ipxe_config.template'
iniset $IRONIC_CONF_FILE pxe pxe_bootfile_name $pxebin
iniset $IRONIC_CONF_FILE pxe uefi_pxe_config_template '$pybasedir/drivers/modules/ipxe_config.template'
iniset $IRONIC_CONF_FILE pxe uefi_pxe_bootfile_name $uefipxebin
iniset $IRONIC_CONF_FILE pxe ipxe_bootfile_name $pxebin
iniset $IRONIC_CONF_FILE pxe uefi_ipxe_bootfile_name $uefipxebin
iniset $IRONIC_CONF_FILE deploy http_root $IRONIC_HTTP_DIR
iniset $IRONIC_CONF_FILE deploy http_url "http://$([[ $IRONIC_HTTP_SERVER =~ : ]] && echo "[$IRONIC_HTTP_SERVER]" || echo $IRONIC_HTTP_SERVER):$IRONIC_HTTP_PORT"
if [[ "$IRONIC_IPXE_USE_SWIFT" == "True" ]]; then

View File

@ -357,41 +357,59 @@ on the Bare Metal service node(s) where ``ironic-conductor`` is running.
Ubuntu::
cp /usr/lib/ipxe/{undionly.kpxe,ipxe.efi} /tftpboot
cp /usr/lib/ipxe/{undionly.kpxe,ipxe.efi,snponly.efi} /tftpboot
Fedora/RHEL7/CentOS7::
cp /usr/share/ipxe/{undionly.kpxe,ipxe.efi} /tftpboot
cp /usr/share/ipxe/{undionly.kpxe,ipxe.efi,snponly.efi} /tftpboot
#. Enable/Configure iPXE in the Bare Metal Service's configuration file
(/etc/ironic/ironic.conf):
#. Enable/Configure iPXE overrides in the Bare Metal Service's configuration
file **if required** (/etc/ironic/ironic.conf):
.. code-block:: ini
[pxe]
# Enable iPXE boot. (boolean value)
ipxe_enabled=True
# Neutron bootfile DHCP parameter. (string value)
pxe_bootfile_name=undionly.kpxe
ipxe_bootfile_name=undionly.kpxe
# Bootfile DHCP parameter for UEFI boot mode. (string value)
uefi_pxe_bootfile_name=ipxe.efi
uefi_ipxe_bootfile_name=ipxe.efi
# Template file for PXE configuration. (string value)
pxe_config_template=$pybasedir/drivers/modules/ipxe_config.template
# Template file for PXE configuration for UEFI boot loader.
# (string value)
uefi_pxe_config_template=$pybasedir/drivers/modules/ipxe_config.template
ipxe_config_template=$pybasedir/drivers/modules/ipxe_config.template
.. note::
The ``[pxe]ipxe_enabled`` option has been deprecated and will be removed
in the T* development cycle. Users should instead consider use of the
``ipxe`` boot interface. The same default use of iPXE functionality can
be achieved by setting the ``[DEFAULT]default_boot_interface`` option
to ``ipxe``.
Most UEFI systems have integrated networking which means the
``[pxe]uefi_ipxe_bootfile_name`` setting should be set to
``snponly.efi``.
.. note::
Setting the iPXE parameters noted in the code block above to no value,
in other words setting a line to something like ``ipxe_bootfile_name=``
will result in ironic falling back to the default values of the non-iPXE
PXE settings. This is for backwards compatability.
#. Ensure iPXE is the default PXE, if applicable.
In earlier versions of ironic, a ``[pxe]ipxe_enabled`` setting allowing
operators to declare the behavior of the conductor to exclusively operate
as if only iPXE was to be used. As time moved on, iPXE functionality was
moved to it's own ``ipxe`` boot interface.
If you want to emulate that same hehavior, set the following in the
configuration file (/etc/ironic/ironic.conf):
.. code-block:: ini
[DEFAULT]
default_boot_interface=ipxe
enabled_boot_interfaces=ipxe,pxe
.. note::
The ``[DEFAULT]enabled_boot_interfaces`` setting may be exclusively set
to ``ipxe``, however ironic has multiple interfaces available depending
on the hardware types available for use.
#. It is possible to configure the Bare Metal service in such a way
that nodes will boot into the deploy image directly from Object Storage.
@ -442,7 +460,6 @@ on the Bare Metal service node(s) where ``ironic-conductor`` is running.
sudo service ironic-conductor restart
PXE multi-architecture setup
----------------------------
@ -498,6 +515,10 @@ nodes will be deployed by 'grubaa64.efi', and ppc64 nodes by 'bootppc64'::
commands, you'll need to switch to use ``linux`` and ``initrd`` command
instead.
.. note::
A ``[pxe]ipxe_bootfile_name_by_arch`` setting is available for multi-arch
iPXE based deployment, and defaults to the same behavior as the comperable
``[pxe]pxe_bootfile_by_arch`` setting for standard PXE.
PXE timeouts tuning
-------------------

View File

@ -265,7 +265,10 @@ def create_pxe_config(task, pxe_options, template=None, ipxe_enabled=False):
"""
LOG.debug("Building PXE config for node %s", task.node.uuid)
if template is None:
template = deploy_utils.get_pxe_config_template(task.node)
if ipxe_enabled:
template = deploy_utils.get_ipxe_config_template(task.node)
else:
template = deploy_utils.get_pxe_config_template(task.node)
_ensure_config_dirs_exist(task, ipxe_enabled)
@ -384,7 +387,16 @@ def _dhcp_option_file_or_url(task, urlboot=False, ip_version=None):
to return options for DHCP. Possible options
are 4, and 6.
"""
boot_file = deploy_utils.get_pxe_boot_file(task.node)
try:
if task.driver.boot.ipxe_enabled:
boot_file = deploy_utils.get_ipxe_boot_file(task.node)
else:
boot_file = deploy_utils.get_pxe_boot_file(task.node)
except AttributeError:
# Support boot interfaces that lack an explicit ipxe_enabled
# attribute flag.
boot_file = deploy_utils.get_pxe_boot_file(task.node)
# 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:
@ -800,7 +812,10 @@ def build_service_pxe_config(task, instance_image_info,
pxe_options = build_pxe_config_options(task, instance_image_info,
service=True,
ipxe_enabled=ipxe_enabled)
pxe_config_template = deploy_utils.get_pxe_config_template(node)
if ipxe_enabled:
pxe_config_template = deploy_utils.get_ipxe_config_template(node)
else:
pxe_config_template = deploy_utils.get_pxe_config_template(node)
create_pxe_config(task, pxe_options, pxe_config_template,
ipxe_enabled=ipxe_enabled)
@ -942,8 +957,12 @@ def prepare_instance_pxe_config(task, image_info,
pxe_options = build_pxe_config_options(
task, image_info, service=ramdisk_boot,
ipxe_enabled=ipxe_enabled)
pxe_config_template = (
deploy_utils.get_pxe_config_template(node))
if ipxe_enabled:
pxe_config_template = (
deploy_utils.get_ipxe_config_template(node))
else:
pxe_config_template = (
deploy_utils.get_pxe_config_template(node))
create_pxe_config(
task, pxe_options, pxe_config_template,
ipxe_enabled=ipxe_enabled)

View File

@ -54,14 +54,21 @@ opts = [
'$pybasedir', 'drivers/modules/pxe_config.template'),
mutable=True,
help=_('On ironic-conductor node, template file for PXE '
'configuration.')),
'loader configuration.')),
cfg.StrOpt('ipxe_config_template',
default=os.path.join(
'$pybasedir', 'drivers/modules/ipxe_config.template'),
mutable=True,
help=_('On ironic-conductor node, template file for iPXE '
'operations.')),
cfg.StrOpt('uefi_pxe_config_template',
default=os.path.join(
'$pybasedir',
'drivers/modules/pxe_grub_config.template'),
mutable=True,
help=_('On ironic-conductor node, template file for PXE '
'configuration for UEFI boot loader.')),
'configuration for UEFI boot loader. Generally this '
'is used for GRUB specific templates.')),
cfg.DictOpt('pxe_config_template_by_arch',
default={},
mutable=True,
@ -107,10 +114,22 @@ opts = [
cfg.StrOpt('uefi_pxe_bootfile_name',
default='bootx64.efi',
help=_('Bootfile DHCP parameter for UEFI boot mode.')),
cfg.StrOpt('ipxe_bootfile_name',
default='undionly.kpxe',
help=_('Bootfile DHCP parameter.')),
cfg.StrOpt('uefi_ipxe_bootfile_name',
default='ipxe.efi',
help=_('Bootfile DHCP parameter for UEFI boot mode. If you '
'experience problems with booting using it, try '
'snponly.efi.')),
cfg.DictOpt('pxe_bootfile_name_by_arch',
default={},
help=_('Bootfile DHCP parameter per node architecture. '
'For example: aarch64:grubaa64.efi')),
cfg.DictOpt('ipxe_bootfile_name_by_arch',
default={},
help=_('Bootfile DHCP parameter per node architecture. '
'For example: aarch64:ipxe_aa64.efi')),
cfg.StrOpt('ipxe_boot_script',
default=os.path.join(
'$pybasedir', 'drivers/modules/boot.ipxe'),

View File

@ -378,6 +378,54 @@ def get_pxe_boot_file(node):
return boot_file
def get_ipxe_boot_file(node):
"""Return the iPXE boot file name requested for deploy.
This method returns iPXE boot file name to be used for deploy.
Architecture specific boot file is searched first. BIOS/UEFI
boot file is used if no valid architecture specific file found.
If no valid value is found, the default reverts to the
``get_pxe_boot_file`` method and thus the
``[pxe]pxe_bootfile_name`` and ``[pxe]uefi_ipxe_bootfile_name``
settings.
:param node: A single Node.
:returns: The iPXE boot file name.
"""
cpu_arch = node.properties.get('cpu_arch')
boot_file = CONF.pxe.ipxe_bootfile_name_by_arch.get(cpu_arch)
if boot_file is None:
if boot_mode_utils.get_boot_mode(node) == 'uefi':
boot_file = CONF.pxe.uefi_ipxe_bootfile_name
else:
boot_file = CONF.pxe.ipxe_bootfile_name
if boot_file is None:
boot_file = get_pxe_boot_file(node)
return boot_file
def get_ipxe_config_template(node):
"""Return the iPXE config template file name requested of deploy.
This method returns the iPXE configuration template file.
:param node: A single Node.
:returns: The iPXE config template file name.
"""
# NOTE(TheJulia): iPXE configuration files don't change based upon the
# architecture and we're not trying to support multiple different boot
# loaders by architecture as they are all consistent. Where as PXE
# could need to be grub for one arch, PXELINUX for another.
configured_template = CONF.pxe.ipxe_config_template
override_template = node.driver_info.get('pxe_template')
if override_template:
configured_template = override_template
return configured_template or get_pxe_config_template(node)
def get_pxe_config_template(node):
"""Return the PXE config template file name requested for deploy.

View File

@ -200,7 +200,10 @@ class PXEBaseMixin(object):
if ramdisk_params.get("ipa-api-url"):
pxe_options["ipa-api-url"] = ramdisk_params["ipa-api-url"]
pxe_config_template = deploy_utils.get_pxe_config_template(node)
if self.ipxe_enabled:
pxe_config_template = deploy_utils.get_ipxe_config_template(node)
else:
pxe_config_template = deploy_utils.get_pxe_config_template(node)
pxe_utils.create_pxe_config(task, pxe_options,
pxe_config_template,

View File

@ -645,7 +645,7 @@ 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):
def _dhcp_options_for_instance(self, ip_version=4, ipxe=False):
self.config(ip_version=ip_version, group='pxe')
if ip_version == 4:
self.config(tftp_server='192.0.2.1', group='pxe')
@ -653,6 +653,10 @@ class TestPXEUtils(db_base.DbTestCase):
self.config(tftp_server='ff80::1', group='pxe')
self.config(pxe_bootfile_name='fake-bootfile', group='pxe')
self.config(tftp_root='/tftp-path/', group='pxe')
if ipxe:
bootfile = 'fake-bootfile-ipxe'
else:
bootfile = 'fake-bootfile'
if ip_version == 6:
# NOTE(TheJulia): DHCPv6 RFCs seem to indicate that the prior
@ -660,11 +664,11 @@ class TestPXEUtils(db_base.DbTestCase):
# by vendors. The apparent proper option is to return a
# URL in the field https://tools.ietf.org/html/rfc5970#section-3
expected_info = [{'opt_name': '59',
'opt_value': 'tftp://[ff80::1]/fake-bootfile',
'opt_value': 'tftp://[ff80::1]/%s' % bootfile,
'ip_version': ip_version}]
elif ip_version == 4:
expected_info = [{'opt_name': '67',
'opt_value': 'fake-bootfile',
'opt_value': bootfile,
'ip_version': ip_version},
{'opt_name': '210',
'opt_value': '/tftp-path/',
@ -1320,7 +1324,7 @@ class iPXEBuildConfigOptionsTestCase(db_base.DbTestCase):
# URL in the field https://tools.ietf.org/html/rfc5970#section-3
expected_boot_script_url = 'http://[ff80::1]:1234/boot.ipxe'
expected_info = [{'opt_name': '!175,59',
'opt_value': 'tftp://[ff80::1]/fake-bootfile',
'opt_value': 'tftp://[ff80::1]/%s' % boot_file,
'ip_version': ip_version},
{'opt_name': '59',
'opt_value': expected_boot_script_url,
@ -1352,7 +1356,7 @@ class iPXEBuildConfigOptionsTestCase(db_base.DbTestCase):
if ip_version == 6:
# Boot URL variable set from prior test of isc parameters.
expected_info = [{'opt_name': 'tag:!ipxe6,59',
'opt_value': 'tftp://[ff80::1]/fake-bootfile',
'opt_value': 'tftp://[ff80::1]/%s' % boot_file,
'ip_version': ip_version},
{'opt_name': 'tag:ipxe6,59',
'opt_value': expected_boot_script_url,
@ -1381,23 +1385,23 @@ class iPXEBuildConfigOptionsTestCase(db_base.DbTestCase):
def test_dhcp_options_for_instance_ipxe_bios(self):
self.config(ip_version=4, group='pxe')
boot_file = 'fake-bootfile-bios'
self.config(pxe_bootfile_name=boot_file, group='pxe')
boot_file = 'fake-bootfile-bios-ipxe'
self.config(ipxe_bootfile_name=boot_file, group='pxe')
with task_manager.acquire(self.context, self.node.uuid) as task:
self._dhcp_options_for_instance_ipxe(task, boot_file)
def test_dhcp_options_for_instance_ipxe_uefi(self):
self.config(ip_version=4, group='pxe')
boot_file = 'fake-bootfile-uefi'
self.config(uefi_pxe_bootfile_name=boot_file, group='pxe')
boot_file = 'fake-bootfile-uefi-ipxe'
self.config(uefi_ipxe_bootfile_name=boot_file, group='pxe')
with task_manager.acquire(self.context, self.node.uuid) as task:
task.node.properties['capabilities'] = 'boot_mode:uefi'
self._dhcp_options_for_instance_ipxe(task, boot_file)
def test_dhcp_options_for_ipxe_ipv6(self):
self.config(ip_version=6, group='pxe')
boot_file = 'fake-bootfile'
self.config(pxe_bootfile_name=boot_file, group='pxe')
boot_file = 'fake-bootfile-ipxe'
self.config(ipxe_bootfile_name=boot_file, group='pxe')
with task_manager.acquire(self.context, self.node.uuid) as task:
self._dhcp_options_for_instance_ipxe(task, boot_file, ip_version=6)

View File

@ -582,6 +582,34 @@ class GetPxeBootConfigTestCase(db_base.DbTestCase):
result = utils.get_pxe_boot_file(self.node)
self.assertEqual('bios-bootfile', result)
def test_get_ipxe_boot_file(self):
self.config(ipxe_bootfile_name='meow', group='pxe')
result = utils.get_ipxe_boot_file(self.node)
self.assertEqual('meow', result)
def test_get_ipxe_boot_file_uefi(self):
self.config(uefi_ipxe_bootfile_name='ipxe-uefi-bootfile', group='pxe')
properties = {'capabilities': 'boot_mode:uefi'}
self.node.properties = properties
result = utils.get_ipxe_boot_file(self.node)
self.assertEqual('ipxe-uefi-bootfile', result)
def test_get_ipxe_boot_file_other_arch(self):
arch_names = {'aarch64': 'ipxe-aa64.efi',
'x86_64': 'ipxe.kpxe'}
self.config(ipxe_bootfile_name_by_arch=arch_names, group='pxe')
properties = {'cpu_arch': 'aarch64', 'capabilities': 'boot_mode:uefi'}
self.node.properties = properties
result = utils.get_ipxe_boot_file(self.node)
self.assertEqual('ipxe-aa64.efi', result)
def test_get_ipxe_boot_file_fallback(self):
self.config(ipxe_bootfile_name=None, group='pxe')
self.config(uefi_ipxe_bootfile_name=None, group='pxe')
self.config(pxe_bootfile_name='lolcat', group='pxe')
result = utils.get_ipxe_boot_file(self.node)
self.assertEqual('lolcat', result)
def test_get_pxe_config_template_emtpy_property(self):
self.node.properties = {}
self.config(pxe_config_template_by_arch=self.template_by_arch,
@ -597,6 +625,28 @@ class GetPxeBootConfigTestCase(db_base.DbTestCase):
result = utils.get_pxe_config_template(node)
self.assertEqual('fake-template', result)
def test_get_ipxe_config_template(self):
node = obj_utils.create_test_node(
self.context, driver='fake-hardware')
self.assertIn('ipxe_config.template',
utils.get_ipxe_config_template(node))
def test_get_ipxe_config_template_none(self):
self.config(ipxe_config_template=None, group='pxe')
self.config(pxe_config_template='magical_bootloader',
group='pxe')
node = obj_utils.create_test_node(
self.context, driver='fake-hardware')
self.assertEqual('magical_bootloader',
utils.get_ipxe_config_template(node))
def test_get_ipxe_config_template_override_pxe_fallback(self):
node = obj_utils.create_test_node(
self.context, driver='fake-hardware',
driver_info={'pxe_template': 'magical'})
self.assertEqual('magical',
utils.get_ipxe_config_template(node))
@mock.patch('time.sleep', lambda sec: None)
class OtherFunctionTestCase(db_base.DbTestCase):

View File

@ -309,14 +309,9 @@ class iPXEBootTestCase(db_base.DbTestCase):
mock_cache_r_k.assert_called_once_with(
task, {'rescue_kernel': 'a', 'rescue_ramdisk': 'r'},
ipxe_enabled=True)
if uefi:
mock_pxe_config.assert_called_once_with(
task, {}, CONF.pxe.uefi_pxe_config_template,
ipxe_enabled=True)
else:
mock_pxe_config.assert_called_once_with(
task, {}, CONF.pxe.pxe_config_template,
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
@ -699,7 +694,7 @@ class iPXEBootTestCase(db_base.DbTestCase):
ipxe_enabled=True)
provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts)
create_pxe_config_mock.assert_called_once_with(
task, mock.ANY, CONF.pxe.pxe_config_template,
task, mock.ANY, CONF.pxe.ipxe_config_template,
ipxe_enabled=True)
switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, "30212642-09d3-467f-8e09-21685826ab50",
@ -816,7 +811,7 @@ class iPXEBootTestCase(db_base.DbTestCase):
self.assertFalse(cache_mock.called)
provider_mock.update_dhcp.assert_called_once_with(task, dhcp_opts)
create_pxe_config_mock.assert_called_once_with(
task, mock.ANY, CONF.pxe.pxe_config_template,
task, mock.ANY, CONF.pxe.ipxe_config_template,
ipxe_enabled=True)
switch_pxe_config_mock.assert_called_once_with(
pxe_config_path, None, boot_modes.LEGACY_BIOS, False,

View File

@ -0,0 +1,17 @@
---
upgrade:
- |
Operators upgrading from earlier versions using PXE should explicitly set
``[pxe]ipxe_bootfile_name``, ``[pxe]uefi_ipxe_bootfile_name``, and
possibly ``[pxe]ipxe_bootfile_name_by_arch`` settings, as well as a
iPXE specific ``[pxe]ipxe_config_template`` override, if required.
Setting the ``[pxe]ipxe_config_template`` to no value will result in the
``[pxe]pxe_config_template`` being used. The default value points to the
supplied standard iPXE template, so only highly customized operators may
have to tune this setting.
fixes:
- |
Addresses the lack of an ability to explicitly set different bootloaders
for ``iPXE`` and ``PXE`` based boot operations via their respective
``ipxe`` and ``pxe`` boot interfaces.