Add an option to create inspector-compatible boot.ipxe

Currently the default boot.ipxe is not suitable for ironic-inspector
in a standalone configuration. This change adds a new option
[pxe]ipxe_fallback_script that makes boot.ipxe fall back to the provided
script.

Story: #2009294
Task: #43982
Change-Id: Id5547885e75beafb4423e9e2056c79c54b286275
This commit is contained in:
Dmitry Tantsur
2021-11-15 19:40:03 +01:00
parent 2ef65aa368
commit dbc24610d9
7 changed files with 90 additions and 5 deletions

View File

@@ -379,7 +379,8 @@ def create_ipxe_boot_script():
"""Render the iPXE boot script into the HTTP root directory""" """Render the iPXE boot script into the HTTP root directory"""
boot_script = utils.render_template( boot_script = utils.render_template(
CONF.pxe.ipxe_boot_script, CONF.pxe.ipxe_boot_script,
{'ipxe_for_mac_uri': PXE_CFG_DIR_NAME + '/'}) {'ipxe_for_mac_uri': PXE_CFG_DIR_NAME + '/',
'ipxe_fallback_script': CONF.pxe.ipxe_fallback_script})
bootfile_path = os.path.join( bootfile_path = os.path.join(
CONF.deploy.http_root, CONF.deploy.http_root,
os.path.basename(CONF.pxe.ipxe_boot_script)) os.path.basename(CONF.pxe.ipxe_boot_script))

View File

@@ -144,6 +144,10 @@ opts = [
'$pybasedir', 'drivers/modules/boot.ipxe'), '$pybasedir', 'drivers/modules/boot.ipxe'),
help=_('On ironic-conductor node, the path to the main iPXE ' help=_('On ironic-conductor node, the path to the main iPXE '
'script file.')), 'script file.')),
cfg.StrOpt('ipxe_fallback_script',
help=_('File name (e.g. inspector.ipxe) of an iPXE script to '
'fall back to when booting to a MAC-specific script '
'fails. When not set, booting will fail in this case.')),
cfg.IntOpt('ipxe_timeout', cfg.IntOpt('ipxe_timeout',
default=0, default=0,
help=_('Timeout value (in seconds) for downloading an image ' help=_('Timeout value (in seconds) for downloading an image '

View File

@@ -11,12 +11,22 @@ echo Attempting to boot from MAC ${net${netid}/mac:hexhyp}
chain {{ ipxe_for_mac_uri }}${net${netid}/mac:hexhyp} || goto loop chain {{ ipxe_for_mac_uri }}${net${netid}/mac:hexhyp} || goto loop
:loop_done :loop_done
{% if ipxe_fallback_script -%}
chain {{ ipxe_fallback_script }} | goto boot_failed
:boot_failed
{% endif -%}
echo PXE boot failed! No configuration found for any of the present NICs. echo PXE boot failed! No configuration found for any of the present NICs.
echo Press any key to reboot... echo Press any key to reboot...
prompt --timeout 180 prompt --timeout 180
reboot reboot
:old_rom :old_rom
{% if ipxe_fallback_script -%}
chain {{ ipxe_fallback_script }} | goto boot_failed_old_rom
:boot_failed_old_rom
{% endif -%}
echo PXE boot failed! No configuration found for NIC ${mac:hexhyp}. echo PXE boot failed! No configuration found for NIC ${mac:hexhyp}.
echo Please update your iPXE ROM and retry. echo Please update your iPXE ROM and retry.
echo Press any key to reboot... echo Press any key to reboot...

View File

@@ -154,6 +154,17 @@ class TestPXEUtils(db_base.DbTestCase):
self.assertEqual(str(expected_template), rendered_template) self.assertEqual(str(expected_template), rendered_template)
def test_fallback_ipxe_boot_script(self):
rendered_template = utils.render_template(
CONF.pxe.ipxe_boot_script,
{'ipxe_for_mac_uri': 'pxelinux.cfg/',
'ipxe_fallback_script': 'inspector.ipxe'})
with open('ironic/tests/unit/drivers/boot-fallback.ipxe') as f:
expected_template = f.read().rstrip()
self.assertEqual(str(expected_template), rendered_template)
def test_default_ipxe_config(self): def test_default_ipxe_config(self):
# NOTE(lucasagomes): iPXE is just an extension of the PXE driver, # NOTE(lucasagomes): iPXE is just an extension of the PXE driver,
# it doesn't have it's own configuration option for template. # it doesn't have it's own configuration option for template.
@@ -714,7 +725,8 @@ class TestPXEUtils(db_base.DbTestCase):
'foo') 'foo')
render_mock.assert_called_once_with( render_mock.assert_called_once_with(
CONF.pxe.ipxe_boot_script, CONF.pxe.ipxe_boot_script,
{'ipxe_for_mac_uri': 'pxelinux.cfg/'}) {'ipxe_for_mac_uri': 'pxelinux.cfg/',
'ipxe_fallback_script': None})
@mock.patch.object(os.path, 'isfile', lambda path: True) @mock.patch.object(os.path, 'isfile', lambda path: True)
@mock.patch('ironic.common.utils.file_has_content', autospec=True) @mock.patch('ironic.common.utils.file_has_content', autospec=True)
@@ -735,7 +747,27 @@ class TestPXEUtils(db_base.DbTestCase):
'foo') 'foo')
render_mock.assert_called_once_with( render_mock.assert_called_once_with(
CONF.pxe.ipxe_boot_script, CONF.pxe.ipxe_boot_script,
{'ipxe_for_mac_uri': 'pxelinux.cfg/'}) {'ipxe_for_mac_uri': 'pxelinux.cfg/',
'ipxe_fallback_script': None})
@mock.patch.object(os.path, 'isfile', lambda path: False)
@mock.patch('ironic.common.utils.file_has_content', autospec=True)
@mock.patch('ironic.common.utils.write_to_file', autospec=True)
@mock.patch('ironic.common.utils.render_template', autospec=True)
def test_create_ipxe_boot_script_fallback(self, render_mock, write_mock,
file_has_content_mock):
self.config(ipxe_fallback_script='inspector.ipxe', group='pxe')
render_mock.return_value = 'foo'
pxe_utils.create_ipxe_boot_script()
self.assertFalse(file_has_content_mock.called)
write_mock.assert_called_once_with(
os.path.join(CONF.deploy.http_root,
os.path.basename(CONF.pxe.ipxe_boot_script)),
'foo')
render_mock.assert_called_once_with(
CONF.pxe.ipxe_boot_script,
{'ipxe_for_mac_uri': 'pxelinux.cfg/',
'ipxe_fallback_script': 'inspector.ipxe'})
@mock.patch.object(os.path, 'isfile', lambda path: True) @mock.patch.object(os.path, 'isfile', lambda path: True)
@mock.patch('ironic.common.utils.file_has_content', autospec=True) @mock.patch('ironic.common.utils.file_has_content', autospec=True)

View File

@@ -0,0 +1,31 @@
#!ipxe
# NOTE(lucasagomes): Loop over all network devices and boot from
# the first one capable of booting. For more information see:
# https://bugs.launchpad.net/ironic/+bug/1504482
set netid:int32 -1
:loop
inc netid || chain pxelinux.cfg/${mac:hexhyp} || goto old_rom
isset ${net${netid}/mac} || goto loop_done
echo Attempting to boot from MAC ${net${netid}/mac:hexhyp}
chain pxelinux.cfg/${net${netid}/mac:hexhyp} || goto loop
:loop_done
chain inspector.ipxe | goto boot_failed
:boot_failed
echo PXE boot failed! No configuration found for any of the present NICs.
echo Press any key to reboot...
prompt --timeout 180
reboot
:old_rom
chain inspector.ipxe | goto boot_failed_old_rom
:boot_failed_old_rom
echo PXE boot failed! No configuration found for NIC ${mac:hexhyp}.
echo Please update your iPXE ROM and retry.
echo Press any key to reboot...
prompt --timeout 180
reboot

View File

@@ -395,7 +395,8 @@ class iPXEBootTestCase(db_base.DbTestCase):
'foo') 'foo')
render_mock.assert_called_once_with( render_mock.assert_called_once_with(
CONF.pxe.ipxe_boot_script, CONF.pxe.ipxe_boot_script,
{'ipxe_for_mac_uri': 'pxelinux.cfg/'}) {'ipxe_for_mac_uri': 'pxelinux.cfg/',
'ipxe_fallback_script': None})
@mock.patch.object(os.path, 'isfile', lambda path: False) @mock.patch.object(os.path, 'isfile', lambda path: False)
@mock.patch('ironic.common.utils.file_has_content', autospec=True) @mock.patch('ironic.common.utils.file_has_content', autospec=True)
@@ -415,7 +416,8 @@ class iPXEBootTestCase(db_base.DbTestCase):
'foo') 'foo')
render_mock.assert_called_once_with( render_mock.assert_called_once_with(
CONF.pxe.ipxe_boot_script, CONF.pxe.ipxe_boot_script,
{'ipxe_for_mac_uri': 'pxelinux.cfg/'}) {'ipxe_for_mac_uri': 'pxelinux.cfg/',
'ipxe_fallback_script': None})
@mock.patch.object(os.path, 'isfile', lambda path: True) @mock.patch.object(os.path, 'isfile', lambda path: True)
@mock.patch.object(common_utils, 'file_has_content', lambda *args: True) @mock.patch.object(common_utils, 'file_has_content', lambda *args: True)

View File

@@ -0,0 +1,5 @@
---
features:
- |
Adds a new configuration option ``[pxe]ipxe_fallback_script`` which allows
iPXE boot to fall back to e.g. ironic-inspector iPXE script.