Merge "Option to enable bootloader config failure bypass"

This commit is contained in:
Zuul 2020-12-14 09:04:54 +00:00 committed by Gerrit Code Review
commit b42719f3fa
4 changed files with 178 additions and 9 deletions

View File

@ -300,6 +300,18 @@ cli_opts = [
'via lldp. If "all" is set then IPA should attempt ' 'via lldp. If "all" is set then IPA should attempt '
'to bring up all VLANs from lldp on all interfaces. ' 'to bring up all VLANs from lldp on all interfaces. '
'By default, no VLANs will be brought up.'), 'By default, no VLANs will be brought up.'),
cfg.BoolOpt('ignore_bootloader_failure',
default=APARAMS.get('ipa-ignore-bootloader-failure'),
help='If the agent should ignore failures to install a '
'bootloader configuration into UEFI NVRAM. This '
'option should only be considered if the hardware '
'is automatically searching and adding UEFI '
'bootloaders from partitions. Use on a system '
'which is NOT doing this will likely cause the '
'deployment to fail. This setting should only be '
'used if you are absolutely sure of what you are '
'doing and that your hardware supports '
'such functionality. Hint: Most hardware does not.'),
] ]
CONF.register_cli_opts(cli_opts) CONF.register_cli_opts(cli_opts)

View File

@ -22,6 +22,7 @@ import tempfile
from ironic_lib import utils as ilib_utils from ironic_lib import utils as ilib_utils
from oslo_concurrency import processutils from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log from oslo_log import log
from ironic_python_agent import errors from ironic_python_agent import errors
@ -33,6 +34,7 @@ from ironic_python_agent import utils
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
CONF = cfg.CONF
BIND_MOUNTS = ('/dev', '/proc', '/run') BIND_MOUNTS = ('/dev', '/proc', '/run')
@ -712,7 +714,8 @@ class ImageExtension(base.BaseAgentExtension):
@base.async_command('install_bootloader') @base.async_command('install_bootloader')
def install_bootloader(self, root_uuid, efi_system_part_uuid=None, def install_bootloader(self, root_uuid, efi_system_part_uuid=None,
prep_boot_part_uuid=None, prep_boot_part_uuid=None,
target_boot_mode='bios'): target_boot_mode='bios',
ignore_bootloader_failure=None):
"""Install the GRUB2 bootloader on the image. """Install the GRUB2 bootloader on the image.
:param root_uuid: The UUID of the root partition. :param root_uuid: The UUID of the root partition.
@ -734,6 +737,13 @@ class ImageExtension(base.BaseAgentExtension):
if self.agent.iscsi_started: if self.agent.iscsi_started:
iscsi.clean_up(device) iscsi.clean_up(device)
# Always allow the API client to be the final word on if this is
# overridden or not.
if ignore_bootloader_failure is None:
ignore_failure = CONF.ignore_bootloader_failure
else:
ignore_failure = ignore_bootloader_failure
boot = hardware.dispatch_to_managers('get_boot_info') boot = hardware.dispatch_to_managers('get_boot_info')
if boot.current_boot_mode != target_boot_mode: if boot.current_boot_mode != target_boot_mode:
LOG.warning('Boot mode mismatch: target boot mode is %(target)s, ' LOG.warning('Boot mode mismatch: target boot mode is %(target)s, '
@ -753,9 +763,15 @@ class ImageExtension(base.BaseAgentExtension):
has_efibootmgr = False has_efibootmgr = False
if has_efibootmgr: if has_efibootmgr:
if _manage_uefi(device, try:
efi_system_part_uuid=efi_system_part_uuid): if _manage_uefi(
return device,
efi_system_part_uuid=efi_system_part_uuid):
return
except Exception as e:
LOG.error('Error setting up bootloader. Error %s', e)
if not ignore_failure:
raise
# We don't have a working root UUID detection for whole disk images. # We don't have a working root UUID detection for whole disk images.
# Until we can do it, avoid a confusing traceback. # Until we can do it, avoid a confusing traceback.
@ -766,8 +782,13 @@ class ImageExtension(base.BaseAgentExtension):
# In case we can't use efibootmgr for uefi we will continue using grub2 # In case we can't use efibootmgr for uefi we will continue using grub2
LOG.debug('Using grub2-install to set up boot files') LOG.debug('Using grub2-install to set up boot files')
_install_grub2(device, try:
root_uuid=root_uuid, _install_grub2(device,
efi_system_part_uuid=efi_system_part_uuid, root_uuid=root_uuid,
prep_boot_part_uuid=prep_boot_part_uuid, efi_system_part_uuid=efi_system_part_uuid,
target_boot_mode=target_boot_mode) prep_boot_part_uuid=prep_boot_part_uuid,
target_boot_mode=target_boot_mode)
except Exception as e:
LOG.error('Error setting up bootloader. Error %s', e)
if not ignore_failure:
raise

View File

@ -95,6 +95,127 @@ class TestImageExtension(base.IronicAgentTest):
) )
mock_iscsi_clean.assert_called_once_with(self.fake_dev) mock_iscsi_clean.assert_called_once_with(self.fake_dev)
@mock.patch.object(iscsi, 'clean_up', autospec=True)
@mock.patch.object(image, '_manage_uefi', autospec=True)
@mock.patch.object(image, '_install_grub2', autospec=True)
def test__install_bootloader_uefi_ignores_manage_failure(
self, mock_grub2, mock_uefi,
mock_iscsi_clean,
mock_execute, mock_dispatch):
self.config(ignore_bootloader_failure=True)
mock_uefi.side_effect = OSError('meow')
mock_dispatch.side_effect = [
self.fake_dev, hardware.BootInfo(current_boot_mode='uefi')
]
mock_uefi.return_value = False
self.agent_extension.install_bootloader(
root_uuid=self.fake_root_uuid,
efi_system_part_uuid=self.fake_efi_system_part_uuid,
target_boot_mode='uefi'
).join()
mock_dispatch.assert_any_call('get_os_install_device')
mock_dispatch.assert_any_call('get_boot_info')
self.assertEqual(2, mock_dispatch.call_count)
mock_grub2.assert_called_once_with(
self.fake_dev,
root_uuid=self.fake_root_uuid,
efi_system_part_uuid=self.fake_efi_system_part_uuid,
prep_boot_part_uuid=None,
target_boot_mode='uefi'
)
mock_iscsi_clean.assert_called_once_with(self.fake_dev)
@mock.patch.object(iscsi, 'clean_up', autospec=True)
@mock.patch.object(image, '_manage_uefi', autospec=True)
@mock.patch.object(image, '_install_grub2', autospec=True)
def test__install_bootloader_uefi_ignores_grub_failure(
self, mock_grub2, mock_uefi,
mock_iscsi_clean,
mock_execute, mock_dispatch):
self.config(ignore_bootloader_failure=True)
mock_grub2.side_effect = OSError('meow')
mock_dispatch.side_effect = [
self.fake_dev, hardware.BootInfo(current_boot_mode='uefi')
]
mock_uefi.return_value = False
self.agent_extension.install_bootloader(
root_uuid=self.fake_root_uuid,
efi_system_part_uuid=self.fake_efi_system_part_uuid,
target_boot_mode='uefi'
).join()
mock_dispatch.assert_any_call('get_os_install_device')
mock_dispatch.assert_any_call('get_boot_info')
self.assertEqual(2, mock_dispatch.call_count)
mock_grub2.assert_called_once_with(
self.fake_dev,
root_uuid=self.fake_root_uuid,
efi_system_part_uuid=self.fake_efi_system_part_uuid,
prep_boot_part_uuid=None,
target_boot_mode='uefi'
)
mock_iscsi_clean.assert_called_once_with(self.fake_dev)
@mock.patch.object(iscsi, 'clean_up', autospec=True)
@mock.patch.object(image, '_manage_uefi', autospec=True)
@mock.patch.object(image, '_install_grub2', autospec=True)
def test__install_bootloader_uefi_ignores_grub_failure_api_override(
self, mock_grub2, mock_uefi,
mock_iscsi_clean,
mock_execute, mock_dispatch):
self.config(ignore_bootloader_failure=False)
mock_grub2.side_effect = OSError('meow')
mock_dispatch.side_effect = [
self.fake_dev, hardware.BootInfo(current_boot_mode='uefi')
]
mock_uefi.return_value = False
self.agent_extension.install_bootloader(
root_uuid=self.fake_root_uuid,
efi_system_part_uuid=self.fake_efi_system_part_uuid,
target_boot_mode='uefi', ignore_bootloader_failure=True,
).join()
mock_dispatch.assert_any_call('get_os_install_device')
mock_dispatch.assert_any_call('get_boot_info')
self.assertEqual(2, mock_dispatch.call_count)
mock_grub2.assert_called_once_with(
self.fake_dev,
root_uuid=self.fake_root_uuid,
efi_system_part_uuid=self.fake_efi_system_part_uuid,
prep_boot_part_uuid=None,
target_boot_mode='uefi'
)
mock_iscsi_clean.assert_called_once_with(self.fake_dev)
@mock.patch.object(iscsi, 'clean_up', autospec=True)
@mock.patch.object(image, '_manage_uefi', autospec=True)
@mock.patch.object(image, '_install_grub2', autospec=True)
def test__install_bootloader_uefi_grub_failure_api_override(
self, mock_grub2, mock_uefi,
mock_iscsi_clean,
mock_execute, mock_dispatch):
self.config(ignore_bootloader_failure=True)
mock_grub2.side_effect = OSError('meow')
mock_dispatch.side_effect = [
self.fake_dev, hardware.BootInfo(current_boot_mode='uefi')
]
mock_uefi.return_value = False
result = self.agent_extension.install_bootloader(
root_uuid=self.fake_root_uuid,
efi_system_part_uuid=self.fake_efi_system_part_uuid,
target_boot_mode='uefi', ignore_bootloader_failure=False,
).join()
self.assertIsNotNone(result.command_error)
mock_dispatch.assert_any_call('get_os_install_device')
mock_dispatch.assert_any_call('get_boot_info')
self.assertEqual(2, mock_dispatch.call_count)
mock_grub2.assert_called_once_with(
self.fake_dev,
root_uuid=self.fake_root_uuid,
efi_system_part_uuid=self.fake_efi_system_part_uuid,
prep_boot_part_uuid=None,
target_boot_mode='uefi'
)
mock_iscsi_clean.assert_called_once_with(self.fake_dev)
@mock.patch.object(iscsi, 'clean_up', autospec=True) @mock.patch.object(iscsi, 'clean_up', autospec=True)
@mock.patch.object(image, '_install_grub2', autospec=True) @mock.patch.object(image, '_install_grub2', autospec=True)
def test__install_bootloader_no_root(self, mock_grub2, mock_iscsi_clean, def test__install_bootloader_no_root(self, mock_grub2, mock_iscsi_clean,

View File

@ -0,0 +1,15 @@
---
features:
- |
Adds an configuration option which can be encoded into the ramdisk itself
or the PXE parameters being provided to instruct the agent to ignore
bootloader installation or configuration failures. This functionality is
useful to work around well-intentioned hardware which is auto-populating
all possible device into the UEFI nvram firmware in order to try and help
ensure the machine boots. Except, this can also mean any
explict configuration attempt will fail. Operators needing this bypass
can use the ``ipa-ignore-bootloader-failure`` configuration option on the
PXE command line or utilize the ``ignore_bootloader_failure`` option
for the Ramdisk configuration.
In a future version of ironic, this setting may be able to be overriden
by ironic node level configuration.