Avoid grub2-install when on UEFI boot mode
This patch changes the workflow for whole disk images when using uefi. If we can identify the bootloader and it's valid we can update using efibootmgr since grub2-install have problems specially on secure boot mode. We also updated the regex to search for the uefi partition on the disk, since in some cases the parted command output can be without the FS for the partition with esp Flag. Change-Id: I7167e71e5d2352a045565289b200e5530d0ba11d Story: #2006847 Task: #37435
This commit is contained in:
parent
1b20dd9b96
commit
b6210be196
ironic_python_agent
releasenotes/notes
@ -34,6 +34,8 @@ LOG = log.getLogger(__name__)
|
||||
|
||||
BIND_MOUNTS = ('/dev', '/proc', '/run')
|
||||
|
||||
BOOTLOADERS_EFI = ['bootx64.efi', 'grubaa64.efi', 'winload.efi']
|
||||
|
||||
|
||||
def _get_partition(device, uuid):
|
||||
"""Find the partition of a given device."""
|
||||
@ -181,6 +183,151 @@ def _is_bootloader_loaded(dev):
|
||||
return False
|
||||
|
||||
|
||||
def _get_efi_bootloaders(location):
|
||||
"""Get all valid efi bootloaders in a given location
|
||||
|
||||
:param location: the location where it should start looking for the
|
||||
efi files.
|
||||
:return: a list of valid efi bootloaders
|
||||
"""
|
||||
|
||||
# Let's find all files with .efi or .EFI extension
|
||||
LOG.debug('Looking for all efi files on %s', location)
|
||||
valid_bootloaders = []
|
||||
for root, dirs, files in os.walk(location):
|
||||
efi_files = [f for f in files if f.lower() in BOOTLOADERS_EFI]
|
||||
LOG.debug('efi files found in %(location)s : %(efi_files)s',
|
||||
{'location': location, 'efi_files': str(efi_files)})
|
||||
for name in efi_files:
|
||||
efi_f = os.path.join(root, name)
|
||||
LOG.debug('Checking if %s is executable', efi_f)
|
||||
if os.access(efi_f, os.X_OK):
|
||||
v_bl = efi_f.split('/boot/efi')[-1].replace('/', '\\')
|
||||
LOG.debug('%s is a valid bootloader', v_bl)
|
||||
valid_bootloaders.append(v_bl)
|
||||
return valid_bootloaders
|
||||
|
||||
|
||||
def _run_efibootmgr(valid_efi_bootloaders, device, efi_partition):
|
||||
"""Executes efibootmgr and removes duplicate entries.
|
||||
|
||||
:param valid_efi_bootloaders: the list of valid efi bootloaders
|
||||
:param device: the device to be used
|
||||
:param efi_partition: the efi partition on the device
|
||||
"""
|
||||
|
||||
# Before updating let's get information about the bootorder
|
||||
LOG.debug("Getting information about boot order")
|
||||
utils.execute('efibootmgr')
|
||||
# NOTE(iurygregory): regex used to identify the Warning in the stderr after
|
||||
# we add the new entry. Example:
|
||||
# "efibootmgr: ** Warning ** : Boot0004 has same label ironic"
|
||||
duplicated_label = re.compile(r'^.*:\s\*\*.*\*\*\s:\s.*'
|
||||
r'Boot([0-9a-f-A-F]+)\s.*$')
|
||||
label_id = 1
|
||||
for v_efi_bl_path in valid_efi_bootloaders:
|
||||
# Update the nvram using efibootmgr
|
||||
# https://linux.die.net/man/8/efibootmgr
|
||||
label = 'ironic' + str(label_id)
|
||||
LOG.debug("Adding loader %(path)s on partition %(part)s of device "
|
||||
" %(dev)s", {'path': v_efi_bl_path, 'part': efi_partition,
|
||||
'dev': device})
|
||||
cmd = utils.execute('efibootmgr', '-c', '-d', device,
|
||||
'-p', efi_partition, '-w', '-L', label,
|
||||
'-l', v_efi_bl_path)
|
||||
for line in cmd[1].split('\n'):
|
||||
match = duplicated_label.match(line)
|
||||
if match:
|
||||
boot_num = match.group(1)
|
||||
LOG.debug("Found bootnum %s matching label", boot_num)
|
||||
utils.execute('efibootmgr', '-b', boot_num, '-B')
|
||||
label_id += 1
|
||||
|
||||
|
||||
def _manage_uefi(device, efi_system_part_uuid=None):
|
||||
"""Manage the device looking for valid efi bootloaders to update the nvram.
|
||||
|
||||
This method checks for valid efi bootloaders in the device, if they exists
|
||||
it updates the nvram using the efibootmgr.
|
||||
|
||||
:param device: the device to be checked.
|
||||
:param efi_system_part_uuid: efi partition uuid.
|
||||
:return: True - if it founds any efi bootloader and the nvram was updated
|
||||
using the efibootmgr.
|
||||
False - if no efi bootloader is found.
|
||||
"""
|
||||
efi_partition = None
|
||||
efi_partition_mount_point = None
|
||||
efi_mounted = False
|
||||
|
||||
try:
|
||||
local_path = tempfile.mkdtemp()
|
||||
# Trust the contents on the disk in the event of a whole disk image.
|
||||
efi_partition = utils.get_efi_part_on_device(device)
|
||||
if not efi_partition:
|
||||
# _get_partition returns <device>+<partition> and we only need the
|
||||
# partition number
|
||||
partition = _get_partition(device, uuid=efi_system_part_uuid)
|
||||
efi_partition = int(partition.replace(device, ""))
|
||||
|
||||
if efi_partition:
|
||||
efi_partition_mount_point = os.path.join(local_path, "boot/efi")
|
||||
if not os.path.exists(efi_partition_mount_point):
|
||||
os.makedirs(efi_partition_mount_point)
|
||||
|
||||
# The mount needs the device with the partition, in case the
|
||||
# device ends with a digit we add a `p` and the partition number we
|
||||
# found, otherwise we just join the device and the partition number
|
||||
if device[-1].isdigit():
|
||||
efi_device_part = '{}p{}'.format(device, efi_partition)
|
||||
utils.execute('mount', efi_device_part,
|
||||
efi_partition_mount_point)
|
||||
else:
|
||||
efi_device_part = '{}{}'.format(device, efi_partition)
|
||||
utils.execute('mount', efi_device_part,
|
||||
efi_partition_mount_point)
|
||||
efi_mounted = True
|
||||
else:
|
||||
# If we can't find the partition we need to decide what should
|
||||
# happen
|
||||
return False
|
||||
valid_efi_bootloaders = _get_efi_bootloaders(efi_partition_mount_point)
|
||||
if valid_efi_bootloaders:
|
||||
_run_efibootmgr(valid_efi_bootloaders, device, efi_partition)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
except processutils.ProcessExecutionError as e:
|
||||
error_msg = ('Could not verify uefi on device %(dev)s'
|
||||
'failed with %(err)s.' % {'dev': device, 'err': e})
|
||||
LOG.error(error_msg)
|
||||
raise errors.CommandExecutionError(error_msg)
|
||||
finally:
|
||||
umount_warn_msg = "Unable to umount %(local_path)s. Error: %(error)s"
|
||||
|
||||
try:
|
||||
if efi_mounted:
|
||||
utils.execute('umount', efi_partition_mount_point,
|
||||
attempts=3, delay_on_retry=True)
|
||||
except processutils.ProcessExecutionError as e:
|
||||
error_msg = ('Umounting efi system partition failed. '
|
||||
'Attempted 3 times. Error: %s' % e)
|
||||
LOG.error(error_msg)
|
||||
raise errors.CommandExecutionError(error_msg)
|
||||
|
||||
else:
|
||||
# If umounting the binds succeed then we can try to delete it
|
||||
try:
|
||||
utils.execute('sync')
|
||||
except processutils.ProcessExecutionError as e:
|
||||
LOG.warning(umount_warn_msg, {'path': local_path, 'error': e})
|
||||
else:
|
||||
# After everything is umounted we can then remove the
|
||||
# temporary directory
|
||||
shutil.rmtree(local_path)
|
||||
|
||||
|
||||
def _install_grub2(device, root_uuid, efi_system_part_uuid=None,
|
||||
prep_boot_part_uuid=None):
|
||||
"""Install GRUB2 bootloader on a given device."""
|
||||
@ -265,6 +412,7 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None,
|
||||
# --removable flag, per the grub-install source code changes
|
||||
# the default file to be copied, destination file name, and
|
||||
# prevents NVRAM from being updated.
|
||||
# We only run grub2_install for uefi if we can't verify the uefi bits
|
||||
if efi_partition:
|
||||
utils.execute('chroot %(path)s /bin/sh -c '
|
||||
'"%(bin)s-install %(dev)s --removable"' %
|
||||
@ -367,6 +515,22 @@ class ImageExtension(base.BaseAgentExtension):
|
||||
"""
|
||||
device = hardware.dispatch_to_managers('get_os_install_device')
|
||||
iscsi.clean_up(device)
|
||||
boot = hardware.dispatch_to_managers('get_boot_info')
|
||||
if boot.current_boot_mode == 'uefi':
|
||||
has_efibootmgr = True
|
||||
try:
|
||||
utils.execute('efibootmgr', '--version')
|
||||
except errors.CommandExecutionError:
|
||||
LOG.warning("efibootmgr is not available in the ramdisk")
|
||||
has_efibootmgr = False
|
||||
|
||||
if has_efibootmgr:
|
||||
if _manage_uefi(device,
|
||||
efi_system_part_uuid=efi_system_part_uuid):
|
||||
return
|
||||
|
||||
# In case we can't use efibootmgr for uefi we will continue using grub2
|
||||
LOG.debug('Using grub2-install to set up boot files')
|
||||
_install_grub2(device,
|
||||
root_uuid=root_uuid,
|
||||
efi_system_part_uuid=efi_system_part_uuid,
|
||||
|
@ -48,25 +48,36 @@ class TestImageExtension(base.IronicAgentTest):
|
||||
|
||||
@mock.patch.object(iscsi, 'clean_up', autospec=True)
|
||||
@mock.patch.object(image, '_install_grub2', autospec=True)
|
||||
def test_install_bootloader_bios(self, mock_grub2, mock_iscsi_clean,
|
||||
mock_execute, mock_dispatch):
|
||||
mock_dispatch.return_value = self.fake_dev
|
||||
def test__install_bootloader_bios(self, mock_grub2, mock_iscsi_clean,
|
||||
mock_execute, mock_dispatch):
|
||||
mock_dispatch.side_effect = [
|
||||
self.fake_dev, hardware.BootInfo(current_boot_mode='bios')
|
||||
]
|
||||
self.agent_extension.install_bootloader(root_uuid=self.fake_root_uuid)
|
||||
mock_dispatch.assert_called_once_with('get_os_install_device')
|
||||
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=None, prep_boot_part_uuid=None)
|
||||
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(self, mock_grub2, mock_iscsi_clean,
|
||||
mock_execute, mock_dispatch):
|
||||
mock_dispatch.return_value = self.fake_dev
|
||||
def test__install_bootloader_uefi(self, mock_grub2, mock_uefi,
|
||||
mock_iscsi_clean,
|
||||
mock_execute, mock_dispatch):
|
||||
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)
|
||||
mock_dispatch.assert_called_once_with('get_os_install_device')
|
||||
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,
|
||||
@ -74,16 +85,207 @@ class TestImageExtension(base.IronicAgentTest):
|
||||
prep_boot_part_uuid=None)
|
||||
mock_iscsi_clean.assert_called_once_with(self.fake_dev)
|
||||
|
||||
@mock.patch.object(os.path, 'exists', lambda *_: False)
|
||||
@mock.patch.object(iscsi, 'clean_up', autospec=True)
|
||||
@mock.patch.object(image, '_get_efi_bootloaders', autospec=True)
|
||||
@mock.patch.object(image, '_get_partition', autospec=True)
|
||||
@mock.patch.object(utils, 'get_efi_part_on_device', autospec=False)
|
||||
@mock.patch.object(os, 'makedirs', autospec=True)
|
||||
def test__uefi_bootloader_given_partition(
|
||||
self, mkdir_mock, mock_utils_efi_part, mock_partition,
|
||||
mock_efi_bl, mock_iscsi_clean, mock_execute, mock_dispatch):
|
||||
mock_dispatch.side_effect = [
|
||||
self.fake_dev, hardware.BootInfo(current_boot_mode='uefi')
|
||||
]
|
||||
mock_partition.side_effect = [self.fake_dev, self.fake_efi_system_part]
|
||||
mock_efi_bl.return_value = ['\\EFI\\BOOT\\BOOTX64.EFI']
|
||||
mock_utils_efi_part.return_value = '1'
|
||||
|
||||
mock_execute.side_effect = iter([('', ''), ('', ''),
|
||||
('', ''), ('', ''),
|
||||
('', ''), ('', '')])
|
||||
|
||||
expected = [mock.call('efibootmgr', '--version'),
|
||||
mock.call('mount', self.fake_efi_system_part,
|
||||
self.fake_dir + '/boot/efi'),
|
||||
mock.call('efibootmgr'),
|
||||
mock.call('efibootmgr', '-c', '-d', self.fake_dev,
|
||||
'-p', '1', '-w',
|
||||
'-L', 'ironic1', '-l',
|
||||
'\\EFI\\BOOT\\BOOTX64.EFI'),
|
||||
mock.call('umount', self.fake_dir + '/boot/efi',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('sync')]
|
||||
|
||||
self.agent_extension.install_bootloader(
|
||||
root_uuid=self.fake_root_uuid,
|
||||
efi_system_part_uuid=self.fake_efi_system_part_uuid)
|
||||
|
||||
mock_dispatch.assert_any_call('get_os_install_device')
|
||||
mock_dispatch.assert_any_call('get_boot_info')
|
||||
mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_execute.assert_has_calls(expected)
|
||||
mock_utils_efi_part.assert_called_once_with(self.fake_dev)
|
||||
self.assertEqual(6, mock_execute.call_count)
|
||||
|
||||
@mock.patch.object(os.path, 'exists', lambda *_: False)
|
||||
@mock.patch.object(iscsi, 'clean_up', autospec=True)
|
||||
@mock.patch.object(image, '_get_efi_bootloaders', autospec=True)
|
||||
@mock.patch.object(image, '_get_partition', autospec=True)
|
||||
@mock.patch.object(utils, 'get_efi_part_on_device', autospec=True)
|
||||
@mock.patch.object(os, 'makedirs', autospec=True)
|
||||
def test__uefi_bootloader_find_partition(
|
||||
self, mkdir_mock, mock_utils_efi_part, mock_partition,
|
||||
mock_efi_bl, mock_iscsi_clean, mock_execute, mock_dispatch):
|
||||
mock_dispatch.side_effect = [
|
||||
self.fake_dev, hardware.BootInfo(current_boot_mode='uefi')
|
||||
]
|
||||
mock_partition.return_value = self.fake_dev
|
||||
mock_utils_efi_part.return_value = '1'
|
||||
mock_efi_bl.return_value = ['\\EFI\\BOOT\\BOOTX64.EFI']
|
||||
mock_execute.side_effect = iter([('', ''), ('', ''),
|
||||
('', ''), ('', ''),
|
||||
('', ''), ('', '')])
|
||||
|
||||
expected = [mock.call('efibootmgr', '--version'),
|
||||
mock.call('mount', self.fake_efi_system_part,
|
||||
self.fake_dir + '/boot/efi'),
|
||||
mock.call('efibootmgr'),
|
||||
mock.call('efibootmgr', '-c', '-d', self.fake_dev,
|
||||
'-p', '1', '-w',
|
||||
'-L', 'ironic1', '-l',
|
||||
'\\EFI\\BOOT\\BOOTX64.EFI'),
|
||||
mock.call('umount', self.fake_dir + '/boot/efi',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('sync')]
|
||||
|
||||
self.agent_extension.install_bootloader(
|
||||
root_uuid=self.fake_root_uuid,
|
||||
efi_system_part_uuid=None)
|
||||
|
||||
mock_dispatch.assert_any_call('get_os_install_device')
|
||||
mock_dispatch.assert_any_call('get_boot_info')
|
||||
mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_execute.assert_has_calls(expected)
|
||||
mock_utils_efi_part.assert_called_once_with(self.fake_dev)
|
||||
self.assertEqual(6, mock_execute.call_count)
|
||||
|
||||
@mock.patch.object(os.path, 'exists', lambda *_: False)
|
||||
@mock.patch.object(iscsi, 'clean_up', autospec=True)
|
||||
@mock.patch.object(image, '_get_efi_bootloaders', autospec=True)
|
||||
@mock.patch.object(image, '_get_partition', autospec=True)
|
||||
@mock.patch.object(utils, 'get_efi_part_on_device', autospec=True)
|
||||
@mock.patch.object(os, 'makedirs', autospec=True)
|
||||
def test__uefi_bootloader_with_entry_removal(
|
||||
self, mkdir_mock, mock_utils_efi_part, mock_partition,
|
||||
mock_efi_bl, mock_iscsi_clean, mock_execute, mock_dispatch):
|
||||
mock_dispatch.side_effect = [
|
||||
self.fake_dev, hardware.BootInfo(current_boot_mode='uefi')
|
||||
]
|
||||
mock_partition.return_value = self.fake_dev
|
||||
mock_utils_efi_part.return_value = '1'
|
||||
mock_efi_bl.return_value = ['\\EFI\\BOOT\\BOOTX64.EFI']
|
||||
stdeer_msg = """
|
||||
efibootmgr: ** Warning ** : Boot0004 has same label ironic1\n
|
||||
efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n
|
||||
"""
|
||||
mock_execute.side_effect = iter([('', ''), ('', ''),
|
||||
('', ''), ('', stdeer_msg),
|
||||
('', ''), ('', ''),
|
||||
('', ''), ('', '')])
|
||||
|
||||
expected = [mock.call('efibootmgr', '--version'),
|
||||
mock.call('mount', self.fake_efi_system_part,
|
||||
self.fake_dir + '/boot/efi'),
|
||||
mock.call('efibootmgr'),
|
||||
mock.call('efibootmgr', '-c', '-d', self.fake_dev,
|
||||
'-p', '1', '-w',
|
||||
'-L', 'ironic1', '-l',
|
||||
'\\EFI\\BOOT\\BOOTX64.EFI'),
|
||||
mock.call('efibootmgr', '-b', '0004', '-B'),
|
||||
mock.call('efibootmgr', '-b', '0005', '-B'),
|
||||
mock.call('umount', self.fake_dir + '/boot/efi',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('sync')]
|
||||
|
||||
self.agent_extension.install_bootloader(
|
||||
root_uuid=self.fake_root_uuid,
|
||||
efi_system_part_uuid=None)
|
||||
|
||||
mock_dispatch.assert_any_call('get_os_install_device')
|
||||
mock_dispatch.assert_any_call('get_boot_info')
|
||||
mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_execute.assert_has_calls(expected)
|
||||
mock_utils_efi_part.assert_called_once_with(self.fake_dev)
|
||||
self.assertEqual(8, mock_execute.call_count)
|
||||
|
||||
@mock.patch.object(os.path, 'exists', lambda *_: False)
|
||||
@mock.patch.object(iscsi, 'clean_up', autospec=True)
|
||||
@mock.patch.object(image, '_get_efi_bootloaders', autospec=True)
|
||||
@mock.patch.object(image, '_get_partition', autospec=True)
|
||||
@mock.patch.object(utils, 'get_efi_part_on_device', autospec=True)
|
||||
@mock.patch.object(os, 'makedirs', autospec=True)
|
||||
def test__add_multi_bootloaders(
|
||||
self, mkdir_mock, mock_utils_efi_part, mock_partition,
|
||||
mock_efi_bl, mock_iscsi_clean, mock_execute, mock_dispatch):
|
||||
mock_dispatch.side_effect = [
|
||||
self.fake_dev, hardware.BootInfo(current_boot_mode='uefi')
|
||||
]
|
||||
mock_partition.return_value = self.fake_dev
|
||||
mock_utils_efi_part.return_value = '1'
|
||||
mock_efi_bl.return_value = ['\\EFI\\BOOT\\BOOTX64.EFI',
|
||||
'\\WINDOWS\\system32\\winload.efi']
|
||||
|
||||
mock_execute.side_effect = iter([('', ''), ('', ''),
|
||||
('', ''), ('', ''),
|
||||
('', ''), ('', ''),
|
||||
('', '')])
|
||||
|
||||
expected = [mock.call('efibootmgr', '--version'),
|
||||
mock.call('mount', self.fake_efi_system_part,
|
||||
self.fake_dir + '/boot/efi'),
|
||||
mock.call('efibootmgr'),
|
||||
mock.call('efibootmgr', '-c', '-d', self.fake_dev,
|
||||
'-p', '1', '-w',
|
||||
'-L', 'ironic1', '-l',
|
||||
'\\EFI\\BOOT\\BOOTX64.EFI'),
|
||||
mock.call('efibootmgr', '-c', '-d', self.fake_dev,
|
||||
'-p', '1', '-w',
|
||||
'-L', 'ironic2', '-l',
|
||||
'\\WINDOWS\\system32\\winload.efi'),
|
||||
mock.call('umount', self.fake_dir + '/boot/efi',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('sync')]
|
||||
|
||||
self.agent_extension.install_bootloader(
|
||||
root_uuid=self.fake_root_uuid,
|
||||
efi_system_part_uuid=None)
|
||||
|
||||
mock_dispatch.assert_any_call('get_os_install_device')
|
||||
mock_dispatch.assert_any_call('get_boot_info')
|
||||
mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_execute.assert_has_calls(expected)
|
||||
mock_utils_efi_part.assert_called_once_with(self.fake_dev)
|
||||
self.assertEqual(7, mock_execute.call_count)
|
||||
|
||||
@mock.patch.object(iscsi, 'clean_up', autospec=True)
|
||||
@mock.patch.object(image, '_install_grub2', autospec=True)
|
||||
def test_install_bootloader_prep(self, mock_grub2, mock_iscsi_clean,
|
||||
mock_execute, mock_dispatch):
|
||||
mock_dispatch.return_value = self.fake_dev
|
||||
def test__install_bootloader_prep(self, mock_grub2, mock_iscsi_clean,
|
||||
mock_execute, mock_dispatch):
|
||||
mock_dispatch.side_effect = [
|
||||
self.fake_dev, hardware.BootInfo(current_boot_mode='bios')
|
||||
]
|
||||
self.agent_extension.install_bootloader(
|
||||
root_uuid=self.fake_root_uuid,
|
||||
efi_system_part_uuid=None,
|
||||
prep_boot_part_uuid=self.fake_prep_boot_part_uuid)
|
||||
mock_dispatch.assert_called_once_with('get_os_install_device')
|
||||
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,
|
||||
@ -190,6 +392,7 @@ class TestImageExtension(base.IronicAgentTest):
|
||||
uuid=self.fake_prep_boot_part_uuid)
|
||||
self.assertFalse(mock_dispatch.called)
|
||||
|
||||
@mock.patch.object(os.path, 'exists', lambda *_: False)
|
||||
@mock.patch.object(image, '_is_bootloader_loaded', lambda *_: True)
|
||||
@mock.patch.object(hardware, 'is_md_device', autospec=True)
|
||||
@mock.patch.object(hardware, 'md_get_raid_devices', autospec=True)
|
||||
@ -523,3 +726,160 @@ class TestImageExtension(base.IronicAgentTest):
|
||||
mock_execute.return_value = (parted_output, '')
|
||||
result = image._is_bootloader_loaded(self.fake_dev)
|
||||
self.assertFalse(result)
|
||||
|
||||
@mock.patch.object(image, '_get_partition', autospec=True)
|
||||
@mock.patch.object(utils, 'get_efi_part_on_device', autospec=True)
|
||||
def test__manage_uefi_no_partition(self, mock_utils_efi_part,
|
||||
mock_get_part_uuid,
|
||||
mock_execute, mock_dispatch):
|
||||
mock_utils_efi_part.return_value = None
|
||||
mock_get_part_uuid.return_value = self.fake_root_part
|
||||
result = image._manage_uefi(self.fake_dev, self.fake_root_uuid)
|
||||
self.assertFalse(result)
|
||||
|
||||
@mock.patch.object(os.path, 'exists', lambda *_: False)
|
||||
@mock.patch.object(image, '_get_efi_bootloaders', autospec=True)
|
||||
@mock.patch.object(image, '_get_partition', autospec=True)
|
||||
@mock.patch.object(utils, 'get_efi_part_on_device', autospec=True)
|
||||
@mock.patch.object(os, 'makedirs', autospec=True)
|
||||
def test__manage_uefi(self, mkdir_mock, mock_utils_efi_part,
|
||||
mock_get_part_uuid, mock_efi_bl, mock_execute,
|
||||
mock_dispatch):
|
||||
mock_utils_efi_part.return_value = '1'
|
||||
mock_get_part_uuid.return_value = self.fake_dev
|
||||
|
||||
mock_efi_bl.return_value = ['\\EFI\\BOOT\\BOOTX64.EFI']
|
||||
|
||||
mock_execute.side_effect = iter([('', ''), ('', ''),
|
||||
('', ''), ('', ''),
|
||||
('', '')])
|
||||
|
||||
expected = [mock.call('mount', self.fake_efi_system_part,
|
||||
self.fake_dir + '/boot/efi'),
|
||||
mock.call('efibootmgr'),
|
||||
mock.call('efibootmgr', '-c', '-d', self.fake_dev,
|
||||
'-p', '1', '-w',
|
||||
'-L', 'ironic1', '-l',
|
||||
'\\EFI\\BOOT\\BOOTX64.EFI'),
|
||||
mock.call('umount', self.fake_dir + '/boot/efi',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('sync')]
|
||||
|
||||
result = image._manage_uefi(self.fake_dev, self.fake_root_uuid)
|
||||
self.assertTrue(result)
|
||||
mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_execute.assert_has_calls(expected)
|
||||
self.assertEqual(5, mock_execute.call_count)
|
||||
|
||||
@mock.patch.object(os.path, 'exists', lambda *_: False)
|
||||
@mock.patch.object(image, '_get_efi_bootloaders', autospec=True)
|
||||
@mock.patch.object(image, '_get_partition', autospec=True)
|
||||
@mock.patch.object(utils, 'get_efi_part_on_device', autospec=True)
|
||||
@mock.patch.object(os, 'makedirs', autospec=True)
|
||||
def test__manage_uefi_wholedisk(
|
||||
self, mkdir_mock, mock_utils_efi_part,
|
||||
mock_get_part_uuid, mock_efi_bl, mock_execute,
|
||||
mock_dispatch):
|
||||
mock_utils_efi_part.return_value = '1'
|
||||
mock_get_part_uuid.side_effect = Exception
|
||||
|
||||
mock_efi_bl.return_value = ['\\EFI\\BOOT\\BOOTX64.EFI']
|
||||
|
||||
mock_execute.side_effect = iter([('', ''), ('', ''),
|
||||
('', ''), ('', ''),
|
||||
('', '')])
|
||||
|
||||
expected = [mock.call('mount', self.fake_efi_system_part,
|
||||
self.fake_dir + '/boot/efi'),
|
||||
mock.call('efibootmgr'),
|
||||
mock.call('efibootmgr', '-c', '-d', self.fake_dev,
|
||||
'-p', '1', '-w',
|
||||
'-L', 'ironic1', '-l',
|
||||
'\\EFI\\BOOT\\BOOTX64.EFI'),
|
||||
mock.call('umount', self.fake_dir + '/boot/efi',
|
||||
attempts=3, delay_on_retry=True),
|
||||
mock.call('sync')]
|
||||
|
||||
result = image._manage_uefi(self.fake_dev, None)
|
||||
self.assertTrue(result)
|
||||
mkdir_mock.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_efi_bl.assert_called_once_with(self.fake_dir + '/boot/efi')
|
||||
mock_execute.assert_has_calls(expected)
|
||||
self.assertEqual(5, mock_execute.call_count)
|
||||
|
||||
@mock.patch.object(os, 'walk', autospec=True)
|
||||
@mock.patch.object(os, 'access', autospec=False)
|
||||
def test__no_efi_bootloaders(self, mock_access, mock_walk, mock_execute,
|
||||
mock_dispatch):
|
||||
# No valid efi file.
|
||||
mock_walk.return_value = [
|
||||
('/boot/efi', ['EFI'], []),
|
||||
('/boot/efi/EFI', ['centos', 'BOOT'], []),
|
||||
('/boot/efi/EFI/centos', ['fw', 'fonts'],
|
||||
['shimx64-centos.efi', 'BOOT.CSV', 'BOOTX64.CSV',
|
||||
'MokManager.efi', 'mmx64.efi', 'shim.efi', 'fwupia32.efi',
|
||||
'fwupx64.efi', 'shimx64.efi', 'grubenv', 'grubx64.efi',
|
||||
'grub.cfg']),
|
||||
('/boot/efi/EFI/centos/fw', [], []),
|
||||
('/boot/efi/EFI/centos/fonts', [], ['unicode.pf2']),
|
||||
('/boot/efi/EFI/BOOT', [], [])
|
||||
]
|
||||
|
||||
result = image._get_efi_bootloaders("/boot/efi")
|
||||
self.assertEqual(result, [])
|
||||
mock_access.assert_not_called()
|
||||
|
||||
@mock.patch.object(os, 'walk', autospec=True)
|
||||
@mock.patch.object(os, 'access', autospec=True)
|
||||
def test__get_efi_bootloaders(self, mock_access, mock_walk, mock_execute,
|
||||
mock_dispatch):
|
||||
mock_walk.return_value = [
|
||||
('/boot/efi', ['EFI'], []),
|
||||
('/boot/efi/EFI', ['centos', 'BOOT'], []),
|
||||
('/boot/efi/EFI/centos', ['fw', 'fonts'],
|
||||
['shimx64-centos.efi', 'BOOT.CSV', 'BOOTX64.CSV',
|
||||
'MokManager.efi', 'mmx64.efi', 'shim.efi', 'fwupia32.efi',
|
||||
'fwupx64.efi', 'shimx64.efi', 'grubenv', 'grubx64.efi',
|
||||
'grub.cfg']),
|
||||
('/boot/efi/EFI/centos/fw', [], []),
|
||||
('/boot/efi/EFI/centos/fonts', [], ['unicode.pf2']),
|
||||
('/boot/efi/EFI/BOOT', [],
|
||||
['BOOTX64.EFI', 'fallback.efi', 'fbx64.efi'])
|
||||
]
|
||||
mock_access.return_value = True
|
||||
result = image._get_efi_bootloaders("/boot/efi")
|
||||
self.assertEqual(result[0], '\\EFI\\BOOT\\BOOTX64.EFI')
|
||||
|
||||
@mock.patch.object(os, 'walk', autospec=True)
|
||||
@mock.patch.object(os, 'access', autospec=True)
|
||||
def test__get_windows_efi_bootloaders(self, mock_access, mock_walk,
|
||||
mock_execute, mock_dispatch):
|
||||
mock_walk.return_value = [
|
||||
('/boot/efi', ['WINDOWS'], []),
|
||||
('/boot/efi/WINDOWS', ['system32'], []),
|
||||
('/boot/efi/WINDOWS/system32', [],
|
||||
['winload.efi'])
|
||||
]
|
||||
mock_access.return_value = True
|
||||
result = image._get_efi_bootloaders("/boot/efi")
|
||||
self.assertEqual(result[0], '\\WINDOWS\\system32\\winload.efi')
|
||||
|
||||
def test__run_efibootmgr_no_bootloaders(self, mock_execute, mock_dispatch):
|
||||
result = image._run_efibootmgr([], self.fake_dev,
|
||||
self.fake_efi_system_part)
|
||||
expected = []
|
||||
self.assertIsNone(result)
|
||||
mock_execute.assert_has_calls(expected)
|
||||
|
||||
def test__run_efibootmgr(self, mock_execute, mock_dispatch):
|
||||
result = image._run_efibootmgr(['\\EFI\\BOOT\\BOOTX64.EFI'],
|
||||
self.fake_dev,
|
||||
self.fake_efi_system_part)
|
||||
expected = [mock.call('efibootmgr'),
|
||||
mock.call('efibootmgr', '-c', '-d', self.fake_dev,
|
||||
'-p', self.fake_efi_system_part, '-w',
|
||||
'-L', 'ironic1', '-l',
|
||||
'\\EFI\\BOOT\\BOOTX64.EFI')]
|
||||
self.assertIsNone(result)
|
||||
mock_execute.assert_has_calls(expected)
|
||||
|
@ -45,6 +45,18 @@ Number Start End Size File system Name Flags
|
||||
1 116MB 2361MB 2245MB ext4
|
||||
'''
|
||||
|
||||
PARTED_OUTPUT_UNFORMATTED_NOFS = '''Model: whatever
|
||||
Disk /dev/sda: 480GB
|
||||
Sector size (logical/physical): 512B/512B
|
||||
Partition Table: gpt
|
||||
Disk Flags:
|
||||
|
||||
Number Start End Size File system Name Flags
|
||||
1 1049kB 9437kB 8389kB ESP boot, esp
|
||||
2 9437kB 17.8MB 8389kB BSP bios_grub
|
||||
3 17.8MB 40.0GB 40.0GB
|
||||
4 479GB 480GB 68.1MB
|
||||
'''
|
||||
|
||||
PARTED_OUTPUT_NO_EFI = '''Model: whatever
|
||||
Disk /dev/sda: 450GB
|
||||
@ -630,6 +642,18 @@ class TestUtils(testtools.TestCase):
|
||||
)
|
||||
self.assertEqual('15', ret)
|
||||
|
||||
@mock.patch.object(utils, 'execute', autospec=True)
|
||||
def test_get_efi_part_on_device_without_fs(self, mocked_execute):
|
||||
parted_ret = PARTED_OUTPUT_UNFORMATTED_NOFS.format('gpt')
|
||||
mocked_execute.side_effect = [
|
||||
(parted_ret, None)
|
||||
]
|
||||
ret = utils.get_efi_part_on_device('/dev/sda')
|
||||
mocked_execute.assert_has_calls(
|
||||
[mock.call('parted', '-s', '/dev/sda', '--', 'print')]
|
||||
)
|
||||
self.assertEqual('1', ret)
|
||||
|
||||
@mock.patch.object(utils, 'execute', autospec=True)
|
||||
def test_get_efi_part_on_device_not_found(self, mocked_execute):
|
||||
mocked_execute.side_effect = [
|
||||
|
@ -68,7 +68,7 @@ COLLECT_LOGS_COMMANDS = {
|
||||
|
||||
DEVICE_EXTRACTOR = re.compile(r'^(?:(.*\d)p|(.*\D))(?:\d+)$')
|
||||
|
||||
PARTED_ESP_PATTERN = re.compile(r'^\s*(\d+)\s.*\sfat\d*\s.*esp(,|\s|$).*$')
|
||||
PARTED_ESP_PATTERN = re.compile(r'^\s*(\d+)\s.*\s\s.*\s.*esp(,|\s|$).*$')
|
||||
|
||||
|
||||
def execute(*cmd, **kwargs):
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
fixes:
|
||||
- |
|
||||
Fixes the workflow for wholedisk images when using uefi boot mode, when
|
||||
possible it will use efibootmgr instead of grub2 to update the nvram.
|
Loading…
x
Reference in New Issue
Block a user