From 67fa2f09ff83e0a3468e2a0c609c2fb4af687753 Mon Sep 17 00:00:00 2001 From: Julia Kreger Date: Tue, 25 Aug 2020 16:14:45 -0700 Subject: [PATCH] Prevent broken partition image UEFI deploys Partition images can sometimes contain a /boot folder structure event he assets for EFI booting on that filesystem. Which is a good thing. The conundrum is that Ironic does not handle this properly and potentially replaces the bootloader in this sequence such that grub2-install is used instead of signed bootloader assets. As such, we should be preserving the assets and using them from a partition image much like we do when we have a wholedisk image and can identify the assets. Now we will preserve the EFI boot assets, copy them to the new EFI boot partition, and call the EFI setup methods to manage the EFI nvram. Note, this change also splits the logic path out that performs the end call of the EFI boot manager into a reusable method but does not retool all of the testing as it is intertwined in the install_grub2 testing. Also adds some additional debug logging, as much of the bootloader installation code has multiple fallback/cleanup points which makes it difficult to debug from logs. Story: 2008070 Task: 40753 Change-Id: If17d4b4c06df5504987e61a1fde6662e9acd6989 (cherry picked from commit f9870d58120a493c40493df6ef22662364138c31) --- ironic_python_agent/extensions/image.py | 447 ++++++++++++--- .../tests/unit/extensions/test_image.py | 522 +++++++++++++++++- ...-efi-folder-contents-ea1e278b3093ec55.yaml | 7 + 3 files changed, 859 insertions(+), 117 deletions(-) create mode 100644 releasenotes/notes/preserve-efi-folder-contents-ea1e278b3093ec55.yaml diff --git a/ironic_python_agent/extensions/image.py b/ironic_python_agent/extensions/image.py index 879ae3b46..f5fc3f82f 100644 --- a/ironic_python_agent/extensions/image.py +++ b/ironic_python_agent/extensions/image.py @@ -68,8 +68,8 @@ def _get_partition(device, uuid): # UUID and use the "normal" discovery instead? if hardware.is_md_device(device): md_partition = device + 'p1' - if (not os.path.exists(md_partition) or - not stat.S_ISBLK(os.stat(md_partition).st_mode)): + if (not os.path.exists(md_partition) + or not stat.S_ISBLK(os.stat(md_partition).st_mode)): error_msg = ("Could not find partition %(part)s on md " "device %(dev)s" % {'part': md_partition, 'dev': device}) @@ -325,6 +325,7 @@ def _manage_uefi(device, efi_system_part_uuid=None): LOG.error(error_msg) raise errors.CommandExecutionError(error_msg) finally: + LOG.debug('Executing _manage_uefi clean-up.') umount_warn_msg = "Unable to umount %(local_path)s. Error: %(error)s" try: @@ -377,9 +378,11 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None, """Install GRUB2 bootloader on a given device.""" LOG.debug("Installing GRUB2 bootloader on device %s", device) - efi_partition = None + efi_partitions = None efi_partition_mount_point = None efi_mounted = False + efi_preserved = False + path_variable = _get_path_variable() # NOTE(TheJulia): Seems we need to get this before ever possibly # restart the device in the case of multi-device RAID as pyudev @@ -403,18 +406,14 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None, "as it is already marked bootable.", device) return try: - # Add /bin to PATH variable as grub requires it to find efibootmgr - # when running in uefi boot mode. - # Add /usr/sbin to PATH variable to ensure it is there as we do - # not use full path to grub binary anymore. - path_variable = os.environ.get('PATH', '') - path_variable = '%s:/bin:/usr/sbin:/sbin' % path_variable - # Mount the partition and binds path = tempfile.mkdtemp() if efi_system_part_uuid: - efi_partition = _get_partition(device, uuid=efi_system_part_uuid) + efi_part = _get_partition(device, uuid=efi_system_part_uuid) + efi_partitions = [efi_part] + + if efi_partitions: efi_partition_mount_point = os.path.join(path, "boot/efi") # For power we want to install grub directly onto the PreP partition @@ -429,16 +428,33 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None, disks = [device] utils.execute('mount', root_partition, path) - for fs in BIND_MOUNTS: - utils.execute('mount', '-o', 'bind', fs, path + fs) - utils.execute('mount', '-t', 'sysfs', 'none', path + '/sys') + _mount_for_chroot(path) - if efi_partition: - if not os.path.exists(efi_partition_mount_point): - os.makedirs(efi_partition_mount_point) - utils.execute('mount', efi_partition, efi_partition_mount_point) - efi_mounted = True + # UEFI asset management for RAID is handled elsewhere + if not hardware.is_md_device(device) and efi_partition_mount_point: + # NOTE(TheJulia): It may make sense to retool all efi + # asset preservation logic at some point since the paths + # can be a little different, but largely this is JUST for + # partition images as there _should not_ be a mount + # point if we have no efi partitions at all. + efi_preserved = _try_preserve_efi_assets( + device, path, efi_system_part_uuid, + efi_partitions, efi_partition_mount_point) + if efi_preserved: + # Success preserving efi assets + return + else: + # Failure, either via exception or not found + # which in this case the partition needs to be + # remounted. + LOG.debug('No EFI assets were preserved for setup or the ' + 'ramdisk was unable to complete the setup. ' + 'falling back to bootloader installation from' + 'deployed image.') + if not os.path.ismount(root_partition): + LOG.debug('Re-mounting the root partition.') + utils.execute('mount', root_partition, path) binary_name = "grub" if os.path.exists(os.path.join(path, 'usr/sbin/grub2-install')): @@ -452,55 +468,81 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None, {'path': path}, shell=True, env_variables={'PATH': path_variable}) - # Install grub. Normally, grub goes to one disk only. In case of - # md devices, grub goes to all underlying holder (RAID-1) disks. - LOG.info("GRUB2 will be installed on disks %s", disks) - for grub_disk in disks: - LOG.debug("Installing GRUB2 on disk %s", grub_disk) - utils.execute('chroot %(path)s /bin/sh -c ' - '"%(bin)s-install %(dev)s"' % - {'path': path, 'bin': binary_name, - 'dev': grub_disk}, - shell=True, env_variables={'PATH': path_variable}) - LOG.debug("GRUB2 successfully installed on device %s", grub_disk) + if efi_partitions: + if not os.path.exists(efi_partition_mount_point): + os.makedirs(efi_partition_mount_point) + LOG.warning("GRUB2 will be installed for UEFI on efi partitions " + "%s using the install command which does not place " + "Secure Boot signed binaries.", efi_partitions) + for efi_partition in efi_partitions: + utils.execute( + 'mount', efi_partition, efi_partition_mount_point) + efi_mounted = True + # FIXME(rg): does not work in cross boot mode case (target + # boot mode differs from ramdisk one) + # Probe for the correct target (depends on the arch, example + # --target=x86_64-efi) + utils.execute('chroot %(path)s /bin/sh -c ' + '"%(bin)s-install"' % + {'path': path, 'bin': binary_name}, + shell=True, + env_variables={ + 'PATH': path_variable + }) + # Also run grub-install with --removable, this installs grub to + # the EFI fallback path. Useful if the NVRAM wasn't written + # correctly, was reset or if testing with virt as libvirt + # resets the NVRAM on instance start. + # This operation is essentially a copy operation. Use of the + # --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 + utils.execute('chroot %(path)s /bin/sh -c ' + '"%(bin)s-install --removable"' % + {'path': path, 'bin': binary_name}, + shell=True, + env_variables={ + 'PATH': path_variable + }) + utils.execute('umount', efi_partition_mount_point, attempts=3, + delay_on_retry=True) + efi_mounted = False + # NOTE: probably never needed for grub-mkconfig, does not hurt in + # case of doubt, cleaned in the finally clause anyway + utils.execute('mount', efi_partitions[0], + efi_partition_mount_point) + efi_mounted = True + else: + # FIXME(rg): does not work if ramdisk boot mode is not the same + # as the target (--target=i386-pc, arch dependent). + # See previous FIXME - # Also run grub-install with --removable, this installs grub to the - # EFI fallback path. Useful if the NVRAM wasn't written correctly, - # was reset or if testing with virt as libvirt resets the NVRAM - # on instance start. - # This operation is essentially a copy operation. Use of the - # --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"' % - {'path': path, 'bin': binary_name, 'dev': device}, - shell=True, env_variables={'PATH': path_variable}) + # Install grub. Normally, grub goes to one disk only. In case of + # md devices, grub goes to all underlying holder (RAID-1) disks. + LOG.info("GRUB2 will be installed on disks %s", disks) + for grub_disk in disks: + LOG.debug("Installing GRUB2 on disk %s", grub_disk) + utils.execute( + 'chroot %(path)s /bin/sh -c "%(bin)s-install %(dev)s"' % + { + 'path': path, + 'bin': binary_name, + 'dev': grub_disk + }, + shell=True, + env_variables={ + 'PATH': path_variable + } + ) + LOG.debug("GRUB2 successfully installed on device %s", + grub_disk) - # If the image has dracut installed, set the rd.md.uuid kernel - # parameter for discovered md devices. - if hardware.is_md_device(device) and _has_dracut(path): - rd_md_uuids = ["rd.md.uuid=%s" % x['UUID'] - for x in hardware.md_get_raid_devices().values()] - - LOG.debug("Setting rd.md.uuid kernel parameters: %s", rd_md_uuids) - with open('%s/etc/default/grub' % path, 'r') as g: - contents = g.read() - with open('%s/etc/default/grub' % path, 'w') as g: - g.write( - re.sub(r'GRUB_CMDLINE_LINUX="(.*)"', - r'GRUB_CMDLINE_LINUX="\1 %s"' - % " ".join(rd_md_uuids), - contents)) - - # Generate the grub configuration file - utils.execute('chroot %(path)s /bin/sh -c ' - '"%(bin)s-mkconfig -o ' - '/boot/%(bin)s/grub.cfg"' % - {'path': path, 'bin': binary_name}, shell=True, - env_variables={'PATH': path_variable}) + # NOTE(TheJulia): Setup grub configuration again since IF we reach + # this point, then we've manually installed grub which is not the + # recommended path. + _configure_grub(device, path) LOG.info("GRUB2 successfully installed on %s", device) @@ -511,6 +553,7 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None, raise errors.CommandExecutionError(error_msg) finally: + LOG.debug('Executing _install_grub2 clean-up.') # Umount binds and partition umount_warn_msg = "Unable to umount %(path)s. Error: %(error)s" @@ -527,7 +570,9 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None, raise errors.CommandExecutionError(error_msg) # If umounting the binds succeed then we can try to delete it - if _umount_all_partitions(path, path_variable, umount_warn_msg): + if _umount_all_partitions(path, + path_variable, + umount_warn_msg): try: utils.execute('umount', path, attempts=3, delay_on_retry=True) except processutils.ProcessExecutionError as e: @@ -538,6 +583,242 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None, shutil.rmtree(path) +def _get_path_variable(): + # Add /bin to PATH variable as grub requires it to find efibootmgr + # when running in uefi boot mode. + # Add /usr/sbin to PATH variable to ensure it is there as we do + # not use full path to grub binary anymore. + path_variable = os.environ.get('PATH', '') + return '%s:/bin:/usr/sbin:/sbin' % path_variable + + +def _configure_grub(device, path): + """Make consolidated grub configuration as it is device aware. + + :param device: The device for the filesystem. + :param path: The path in which the filesystem is mounted. + """ + LOG.debug('Attempting to generate grub Configuration') + path_variable = _get_path_variable() + binary_name = "grub" + if os.path.exists(os.path.join(path, 'usr/sbin/grub2-install')): + binary_name = "grub2" + # If the image has dracut installed, set the rd.md.uuid kernel + # parameter for discovered md devices. + if hardware.is_md_device(device) and _has_dracut(path): + rd_md_uuids = ["rd.md.uuid=%s" % x['UUID'] + for x in hardware.md_get_raid_devices().values()] + LOG.debug("Setting rd.md.uuid kernel parameters: %s", rd_md_uuids) + with open('%s/etc/default/grub' % path, 'r') as g: + contents = g.read() + with open('%s/etc/default/grub' % path, 'w') as g: + g.write( + re.sub(r'GRUB_CMDLINE_LINUX="(.*)"', + r'GRUB_CMDLINE_LINUX="\1 %s"' + % " ".join(rd_md_uuids), + contents)) + + utils.execute('chroot %(path)s /bin/sh -c ' + '"%(bin)s-mkconfig -o ' + '/boot/%(bin)s/grub.cfg"' % + {'path': path, 'bin': binary_name}, shell=True, + env_variables={'PATH': path_variable, + 'GRUB_DISABLE_OS_PROBER': 'true', + 'GRUB_SAVEDEFAULT': 'true'}, + use_standard_locale=True) + LOG.debug('Completed basic grub configuration.') + + +def _mount_for_chroot(path): + """Mount items for grub-mkconfig to succeed.""" + LOG.debug('Mounting Linux standard partitions for bootloader ' + 'configuration generation') + for fs in BIND_MOUNTS: + utils.execute('mount', '-o', 'bind', fs, path + fs) + utils.execute('mount', '-t', 'sysfs', 'none', path + '/sys') + + +def _try_preserve_efi_assets(device, path, + efi_system_part_uuid, + efi_partitions, + efi_partition_mount_point): + """Attempt to preserve UEFI boot assets. + + :param device: The device upon which wich to try to preserve + assets. + :param path: The path in which the filesystem is already mounted + which we should examine to preserve assets from. + :param efi_system_part_uuid: The partition ID representing the + created EFI system partition. + :param efi_partitions: The list of partitions upon wich to + write the preserved assets to. + :param efi_partition_mount_point: The folder at which to mount + the assets for the process of + preservation. + + :returns: True if assets have been preserved, otherwise False. + None is the result of this method if a failure has + occured. + """ + efi_assets_folder = efi_partition_mount_point + '/EFI' + if os.path.exists(efi_assets_folder): + # We appear to have EFI Assets, that need to be preserved + # and as such if we succeed preserving them, we will be returned + # True from _preserve_efi_assets to correspond with success or + # failure in this action. + # NOTE(TheJulia): Still makes sense to invoke grub-install as + # fragmentation of grub has occured. + if (os.path.exists(os.path.join(path, 'usr/sbin/grub2-install')) + or os.path.exists(os.path.join(path, 'usr/sbin/grub-install'))): + _configure_grub(device, path) + # But first, if we have grub, we should try to build a grub config! + LOG.debug('EFI asset folder detected, attempting to preserve assets.') + if _preserve_efi_assets(path, efi_assets_folder, + efi_partitions, + efi_partition_mount_point): + try: + # Since we have preserved the assets, we should be able + # to call the _efi_boot_setup method to scan the device + # and add loader entries + efi_preserved = _efi_boot_setup(device, efi_system_part_uuid) + # Executed before the return so we don't return and then begin + # execution. + return efi_preserved + except Exception as e: + # Remount the partition and proceed as we were. + LOG.debug('Exception encountered while attempting to ' + 'setup the EFI loader from a root ' + 'filesystem. Error: %s', e) + + +def _efi_boot_setup(device, efi_system_part_uuid=None, target_boot_mode=None): + """Identify and setup an EFI bootloader from supplied partition/disk. + + :param device: The device upon which to attempt the EFI bootloader setup. + :param efi_system_part_uuid: The partition UUID to utilize in searching + for an EFI bootloader. + :param target_boot_mode: The requested boot mode target for the + machine. This is optional and is mainly used + for the purposes of identifying a mismatch and + reporting a warning accordingly. + :returns: True if we succeeded in setting up an EFI bootloader in the + EFI nvram table. + False if we were unable to set the machine to EFI boot, + due to inability to locate assets required OR the efibootmgr + tool not being present. + None is returned if the node is NOT in UEFI boot mode or + the system is deploying upon a software RAID device. + """ + boot = hardware.dispatch_to_managers('get_boot_info') + # Explicitly only run if a target_boot_mode is set which prevents + # callers following-up from re-logging the same message + if target_boot_mode and boot.current_boot_mode != target_boot_mode: + LOG.warning('Boot mode mismatch: target boot mode is %(target)s, ' + 'current boot mode is %(current)s. Installing boot ' + 'loader may fail or work incorrectly.', + {'target': target_boot_mode, + 'current': boot.current_boot_mode}) + + # FIXME(arne_wiebalck): make software RAID work with efibootmgr + if (boot.current_boot_mode == 'uefi' + and not hardware.is_md_device(device)): + try: + utils.execute('efibootmgr', '--version') + except FileNotFoundError: + LOG.warning("efibootmgr is not available in the ramdisk") + else: + if _manage_uefi(device, + efi_system_part_uuid=efi_system_part_uuid): + return True + return False + + +def _preserve_efi_assets(path, efi_assets_folder, efi_partitions, + efi_partition_mount_point): + """Preserve the EFI assets in a partition image. + + :param path: The path used for the mounted image filesystem. + :param efi_assets_folder: The folder where we can find the + UEFI assets required for booting. + :param efi_partitions: The list of partitions upon which to + write the perserved assets to. + :param efi_partition_mount_point: The folder at which to mount + the assets for the process of + preservation. + :returns: True if EFI assets were able to be located and preserved + to their appropriate locations based upon the supplied + efi_partitions list. + False if any error is encountered in this process. + """ + try: + save_efi = os.path.join(tempfile.mkdtemp(), 'efi_loader') + LOG.debug('Copying EFI assets to %s.', save_efi) + shutil.copytree(efi_assets_folder, save_efi) + + # Identify grub2 config file for EFI booting as grub may require it + # in the folder. + + destlist = os.listdir(efi_assets_folder) + grub2_file = os.path.join(path, 'boot/grub2/grub.cfg') + if os.path.isfile(grub2_file): + LOG.debug('Local Grub2 configuration detected.') + # A grub2 config seems to be present, we should preserve it! + for dest in destlist: + grub_dest = os.path.join(save_efi, dest, 'grub.cfg') + if not os.path.isfile(grub_dest): + LOG.debug('A grub.cfg file was not found in %s. %s' + 'will be copied to that location.', + grub_dest, grub2_file) + try: + shutil.copy2(grub2_file, grub_dest) + except (IOError, OSError, shutil.SameFileError) as e: + LOG.warning('Failed to copy grub.cfg file for ' + 'EFI boot operation. Error %s', e) + grub2_env_file = os.path.join(path, 'boot/grub2/grubenv') + # NOTE(TheJulia): By saving the default, this file should be created. + # this appears to what diskimage-builder does. + # if the file is just a file, then we'll need to copy it. If it is + # anything else like a link, we're good. This behaivor is inconsistent + # depending on packager install scripts for grub. + if os.path.isfile(grub2_env_file): + LOG.debug('Detected grub environment file %s, will attempt ' + 'to copy this file to align with apparent bootloaders', + grub2_env_file) + for dest in destlist: + grub2env_dest = os.path.join(save_efi, dest, 'grubenv') + if not os.path.isfile(grub2env_dest): + LOG.debug('A grubenv file was not found. Copying ' + 'to %s along with the grub.cfg file as ' + 'grub generally expects it is present.', + grub2env_dest) + try: + shutil.copy2(grub2_env_file, grub2env_dest) + except (IOError, OSError, shutil.SameFileError) as e: + LOG.warning('Failed to copy grubenv file. ' + 'Error: %s', e) + # Loop through partitions because software RAID. + for efi_part in efi_partitions: + utils.execute('mount', '-t', 'vfat', efi_part, + efi_partition_mount_point) + shutil.copytree(save_efi, efi_assets_folder) + LOG.debug('Files preserved to %(disk)s for %(part)s. ' + 'Files: %(filelist)s From: %(from)s', + {'disk': efi_part, + 'part': efi_partition_mount_point, + 'filelist': os.listdir(efi_assets_folder), + 'from': save_efi}) + utils.execute('umount', efi_partition_mount_point) + return True + except Exception as e: + LOG.debug('Failed to preserve EFI assets. Error %s', e) + try: + utils.execute('umount', efi_partition_mount_point) + except Exception as e: + LOG.debug('Exception encountered while attempting unmount ' + 'the EFI partition mount point. Error: %s', e) + return False + + class ImageExtension(base.BaseAgentExtension): @base.sync_command('install_bootloader') @@ -568,31 +849,13 @@ class ImageExtension(base.BaseAgentExtension): else: ignore_failure = ignore_bootloader_failure - boot = hardware.dispatch_to_managers('get_boot_info') - if boot.current_boot_mode == 'uefi': - has_efibootmgr = True - # NOTE(iurygregory): adaptation for py27 since we don't have - # FileNotFoundError defined. - try: - FileNotFoundError - except NameError: - FileNotFoundError = OSError - try: - utils.execute('efibootmgr', '--version') - except FileNotFoundError: - LOG.warning("efibootmgr is not available in the ramdisk") - has_efibootmgr = False - - if has_efibootmgr: - try: - if _manage_uefi( - 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 + try: + if _efi_boot_setup(device, efi_system_part_uuid): + return + except Exception as e: + LOG.error('Error setting up bootloader. Error %s', e) + if not ignore_failure: + raise # In case we can't use efibootmgr for uefi we will continue using grub2 LOG.debug('Using grub2-install to set up boot files') diff --git a/ironic_python_agent/tests/unit/extensions/test_image.py b/ironic_python_agent/tests/unit/extensions/test_image.py index 9f4ae88c3..344a80fee 100644 --- a/ironic_python_agent/tests/unit/extensions/test_image.py +++ b/ironic_python_agent/tests/unit/extensions/test_image.py @@ -204,6 +204,7 @@ class TestImageExtension(base.IronicAgentTest): ) mock_iscsi_clean.assert_called_once_with(self.fake_dev) + @mock.patch.object(hardware, 'is_md_device', lambda *_: False) @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) @@ -252,6 +253,7 @@ class TestImageExtension(base.IronicAgentTest): mock_utils_efi_part.assert_called_once_with(self.fake_dev) self.assertEqual(8, mock_execute.call_count) + @mock.patch.object(hardware, 'is_md_device', lambda *_: False) @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) @@ -299,6 +301,7 @@ class TestImageExtension(base.IronicAgentTest): mock_utils_efi_part.assert_called_once_with(self.fake_dev) self.assertEqual(8, mock_execute.call_count) + @mock.patch.object(hardware, 'is_md_device', lambda *_: False) @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) @@ -353,6 +356,7 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n mock_utils_efi_part.assert_called_once_with(self.fake_dev) self.assertEqual(10, mock_execute.call_count) + @mock.patch.object(hardware, 'is_md_device', lambda *_: False) @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) @@ -428,6 +432,7 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n prep_boot_part_uuid=self.fake_prep_boot_part_uuid) mock_iscsi_clean.assert_called_once_with(self.fake_dev) + @mock.patch.object(hardware, 'is_md_device', lambda *_: False) @mock.patch.object(os.path, 'exists', lambda *_: False) @mock.patch.object(iscsi, 'clean_up', autospec=True) def test_install_bootloader_failure(self, mock_iscsi_clean, mock_execute, @@ -486,8 +491,11 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n '"grub-mkconfig -o ' '/boot/grub/grub.cfg"' % self.fake_dir), shell=True, - env_variables={'PATH': - '/sbin:/bin:/usr/sbin:/sbin'}), + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin', + 'GRUB_DISABLE_OS_PROBER': 'true', + 'GRUB_SAVEDEFAULT': 'true'}, + use_standard_locale=True), mock.call(('chroot %s /bin/sh -c "umount -a -t vfat"' % (self.fake_dir)), shell=True, env_variables={ @@ -546,8 +554,11 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n '"grub-mkconfig -o ' '/boot/grub/grub.cfg"' % self.fake_dir), shell=True, - env_variables={'PATH': - '/sbin:/bin:/usr/sbin:/sbin'}), + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin', + 'GRUB_DISABLE_OS_PROBER': 'true', + 'GRUB_SAVEDEFAULT': 'true'}, + use_standard_locale=True), mock.call(('chroot %s /bin/sh -c "umount -a -t vfat"' % (self.fake_dir)), shell=True, env_variables={ @@ -569,6 +580,7 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n uuid=self.fake_prep_boot_part_uuid) self.assertFalse(mock_dispatch.called) + @mock.patch.object(os.path, 'ismount', lambda *_: True) @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) @@ -599,28 +611,34 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n self.fake_dir + '/run'), mock.call('mount', '-t', 'sysfs', 'none', self.fake_dir + '/sys'), - mock.call('mount', self.fake_efi_system_part, - self.fake_dir + '/boot/efi'), mock.call(('chroot %s /bin/sh -c "mount -a -t vfat"' % (self.fake_dir)), shell=True, env_variables={ 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call('mount', self.fake_efi_system_part, + self.fake_dir + '/boot/efi'), mock.call(('chroot %s /bin/sh -c ' - '"grub-install %s"' % - (self.fake_dir, self.fake_dev)), shell=True, + '"grub-install"' % self.fake_dir), shell=True, env_variables={ 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), mock.call(('chroot %s /bin/sh -c ' - '"grub-install %s --removable"' % - (self.fake_dir, self.fake_dev)), shell=True, + '"grub-install --removable"' % self.fake_dir), + shell=True, env_variables={ 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call('umount', self.fake_dir + '/boot/efi', + attempts=3, delay_on_retry=True), + mock.call('mount', self.fake_efi_system_part, + self.fake_dir + '/boot/efi'), mock.call(('chroot %s /bin/sh -c ' '"grub-mkconfig -o ' '/boot/grub/grub.cfg"' % self.fake_dir), shell=True, env_variables={ - 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + 'PATH': '/sbin:/bin:/usr/sbin:/sbin', + 'GRUB_DISABLE_OS_PROBER': 'true', + 'GRUB_SAVEDEFAULT': 'true'}, + use_standard_locale=True), mock.call('umount', self.fake_dir + '/boot/efi', attempts=3, delay_on_retry=True), mock.call(('chroot %s /bin/sh -c "umount -a -t vfat"' % @@ -645,6 +663,463 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n uuid=self.fake_efi_system_part_uuid) self.assertFalse(mock_dispatch.called) + @mock.patch.object(os.path, 'ismount', lambda *_: False) + @mock.patch.object(os, 'listdir', lambda *_: ['file1', 'file2']) + @mock.patch.object(image, '_is_bootloader_loaded', lambda *_: False) + @mock.patch.object(image, '_efi_boot_setup', autospec=True) + @mock.patch.object(shutil, 'copytree', autospec=True) + @mock.patch.object(os.path, 'exists', autospec=True) + @mock.patch.object(hardware, 'is_md_device', autospec=True) + @mock.patch.object(hardware, 'md_get_raid_devices', autospec=True) + @mock.patch.object(os, 'environ', autospec=True) + @mock.patch.object(os, 'makedirs', autospec=True) + @mock.patch.object(image, '_get_partition', autospec=True) + def test__install_grub2_uefi_partition_image_with_loader( + self, mock_get_part_uuid, mkdir_mock, + environ_mock, mock_md_get_raid_devices, + mock_is_md_device, mock_exists, + mock_copytree, mock_efi_setup, + mock_execute, mock_dispatch): + mock_exists.return_value = True + mock_efi_setup.return_value = True + mock_get_part_uuid.side_effect = [self.fake_root_part, + self.fake_efi_system_part] + environ_mock.get.return_value = '/sbin' + mock_is_md_device.return_value = False + mock_md_get_raid_devices.return_value = {} + + image._install_grub2( + self.fake_dev, root_uuid=self.fake_root_uuid, + efi_system_part_uuid=self.fake_efi_system_part_uuid) + mock_efi_setup.assert_called_once_with(self.fake_dev, + self.fake_efi_system_part_uuid) + mock_copytree.assert_has_calls([ + mock.call(self.fake_dir + '/boot/efi/EFI', + self.fake_dir + '/efi_loader'), + mock.call(self.fake_dir + '/efi_loader', + self.fake_dir + '/boot/efi/EFI')]) + + expected = [mock.call('mount', '/dev/fake2', self.fake_dir), + mock.call('mount', '-o', 'bind', '/dev', + self.fake_dir + '/dev'), + mock.call('mount', '-o', 'bind', '/proc', + self.fake_dir + '/proc'), + mock.call('mount', '-o', 'bind', '/run', + self.fake_dir + '/run'), + mock.call('mount', '-t', 'sysfs', 'none', + self.fake_dir + '/sys'), + mock.call('chroot %s /bin/sh -c "grub2-mkconfig -o ' + '/boot/grub2/grub.cfg"' % self.fake_dir, + shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin', + 'GRUB_DISABLE_OS_PROBER': 'true', + 'GRUB_SAVEDEFAULT': 'true'}, + use_standard_locale=True), + mock.call('mount', '-t', 'vfat', '/dev/fake1', + self.fake_dir + '/boot/efi'), + mock.call('umount', self.fake_dir + '/boot/efi'), + mock.call('chroot %s /bin/sh -c "umount -a -t ' + 'vfat"' % self.fake_dir, shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call('umount', self.fake_dir + '/dev', attempts=3, + delay_on_retry=True), + mock.call('umount', self.fake_dir + '/proc', attempts=3, + delay_on_retry=True), + mock.call('umount', self.fake_dir + '/run', attempts=3, + delay_on_retry=True), + mock.call('umount', self.fake_dir + '/sys', attempts=3, + delay_on_retry=True), + mock.call('umount', self.fake_dir, attempts=3, + delay_on_retry=True)] + mkdir_mock.assert_not_called() + mock_execute.assert_has_calls(expected) + mock_get_part_uuid.assert_any_call(self.fake_dev, + uuid=self.fake_root_uuid) + mock_get_part_uuid.assert_any_call(self.fake_dev, + uuid=self.fake_efi_system_part_uuid) + self.assertFalse(mock_dispatch.called) + + @mock.patch.object(os, 'listdir', lambda *_: ['file1', 'file2']) + @mock.patch.object(image, '_is_bootloader_loaded', lambda *_: False) + @mock.patch.object(shutil, 'copy2', autospec=True) + @mock.patch.object(os.path, 'isfile', autospec=True) + @mock.patch.object(image, '_efi_boot_setup', autospec=True) + @mock.patch.object(shutil, 'copytree', autospec=True) + @mock.patch.object(os.path, 'exists', autospec=True) + @mock.patch.object(hardware, 'is_md_device', autospec=True) + @mock.patch.object(hardware, 'md_get_raid_devices', autospec=True) + @mock.patch.object(os, 'environ', autospec=True) + @mock.patch.object(os, 'makedirs', autospec=True) + @mock.patch.object(image, '_get_partition', autospec=True) + def test__install_grub2_uefi_partition_image_with_loader_with_grubcfg( + self, mock_get_part_uuid, mkdir_mock, + environ_mock, mock_md_get_raid_devices, + mock_is_md_device, mock_exists, + mock_copytree, mock_efi_setup, + mock_isfile, mock_copy2, + mock_execute, mock_dispatch): + mock_exists.return_value = True + mock_efi_setup.return_value = True + mock_get_part_uuid.side_effect = [self.fake_root_part, + self.fake_efi_system_part] + environ_mock.get.return_value = '/sbin' + mock_is_md_device.return_value = False + mock_md_get_raid_devices.return_value = {} + mock_isfile.side_effect = [True, False, False, True, True, False] + + image._install_grub2( + self.fake_dev, root_uuid=self.fake_root_uuid, + efi_system_part_uuid=self.fake_efi_system_part_uuid) + mock_efi_setup.assert_called_once_with(self.fake_dev, + self.fake_efi_system_part_uuid) + mock_copytree.assert_has_calls([ + mock.call(self.fake_dir + '/boot/efi/EFI', + self.fake_dir + '/efi_loader'), + mock.call(self.fake_dir + '/efi_loader', + self.fake_dir + '/boot/efi/EFI')]) + + expected = [mock.call('mount', '/dev/fake2', self.fake_dir), + mock.call('mount', '-o', 'bind', '/dev', + self.fake_dir + '/dev'), + mock.call('mount', '-o', 'bind', '/proc', + self.fake_dir + '/proc'), + mock.call('mount', '-o', 'bind', '/run', + self.fake_dir + '/run'), + mock.call('mount', '-t', 'sysfs', 'none', + self.fake_dir + '/sys'), + mock.call(('chroot ' + self.fake_dir + ' /bin/sh -c ' + '"grub2-mkconfig -o /boot/grub2/grub.cfg"'), + shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin', + 'GRUB_DISABLE_OS_PROBER': 'true', + 'GRUB_SAVEDEFAULT': 'true'}, + use_standard_locale=True), + mock.call('mount', '-t', 'vfat', '/dev/fake1', + self.fake_dir + '/boot/efi'), + mock.call('umount', self.fake_dir + '/boot/efi'), + mock.call(('chroot ' + self.fake_dir + + ' /bin/sh -c "umount -a -t vfat"'), + shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call('umount', self.fake_dir + '/dev', attempts=3, + delay_on_retry=True), + mock.call('umount', self.fake_dir + '/proc', attempts=3, + delay_on_retry=True), + mock.call('umount', self.fake_dir + '/run', attempts=3, + delay_on_retry=True), + mock.call('umount', self.fake_dir + '/sys', attempts=3, + delay_on_retry=True), + mock.call('umount', self.fake_dir, attempts=3, + delay_on_retry=True)] + mkdir_mock.assert_not_called() + mock_execute.assert_has_calls(expected) + mock_copy2.assert_has_calls([]) + mock_get_part_uuid.assert_any_call(self.fake_dev, + uuid=self.fake_root_uuid) + mock_get_part_uuid.assert_any_call(self.fake_dev, + uuid=self.fake_efi_system_part_uuid) + self.assertFalse(mock_dispatch.called) + + @mock.patch.object(os.path, 'ismount', lambda *_: True) + @mock.patch.object(image, '_is_bootloader_loaded', lambda *_: False) + @mock.patch.object(image, '_preserve_efi_assets', autospec=True) + @mock.patch.object(image, '_efi_boot_setup', autospec=True) + @mock.patch.object(os.path, 'exists', autospec=True) + @mock.patch.object(hardware, 'is_md_device', autospec=True) + @mock.patch.object(hardware, 'md_get_raid_devices', autospec=True) + @mock.patch.object(os, 'environ', autospec=True) + @mock.patch.object(os, 'makedirs', autospec=True) + @mock.patch.object(image, '_get_partition', autospec=True) + def test__install_grub2_uefi_partition_image_with_preserve_failure( + self, mock_get_part_uuid, mkdir_mock, + environ_mock, mock_md_get_raid_devices, + mock_is_md_device, mock_exists, + mock_efi_setup, + mock_preserve_efi_assets, + mock_execute, mock_dispatch): + mock_exists.return_value = True + mock_efi_setup.side_effect = Exception('meow') + mock_get_part_uuid.side_effect = [self.fake_root_part, + self.fake_efi_system_part] + environ_mock.get.return_value = '/sbin' + mock_is_md_device.return_value = False + mock_md_get_raid_devices.return_value = {} + mock_preserve_efi_assets.return_value = False + + image._install_grub2( + self.fake_dev, root_uuid=self.fake_root_uuid, + efi_system_part_uuid=self.fake_efi_system_part_uuid) + self.assertFalse(mock_efi_setup.called) + + expected = [mock.call('mount', '/dev/fake2', self.fake_dir), + mock.call('mount', '-o', 'bind', '/dev', + self.fake_dir + '/dev'), + mock.call('mount', '-o', 'bind', '/proc', + self.fake_dir + '/proc'), + mock.call('mount', '-o', 'bind', '/run', + self.fake_dir + '/run'), + mock.call('mount', '-t', 'sysfs', 'none', + self.fake_dir + '/sys'), + mock.call(('chroot %s /bin/sh -c ' + '"grub2-mkconfig -o ' + '/boot/grub2/grub.cfg"' % self.fake_dir), + shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin', + 'GRUB_DISABLE_OS_PROBER': 'true', + 'GRUB_SAVEDEFAULT': 'true'}, + use_standard_locale=True), + mock.call(('chroot %s /bin/sh -c "mount -a -t vfat"' % + (self.fake_dir)), shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call('mount', self.fake_efi_system_part, + self.fake_dir + '/boot/efi'), + mock.call(('chroot %s /bin/sh -c "grub2-install"' % + self.fake_dir), shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call(('chroot %s /bin/sh -c ' + '"grub2-install --removable"' % + self.fake_dir), shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call( + 'umount', self.fake_dir + '/boot/efi', + attempts=3, delay_on_retry=True), + mock.call('mount', self.fake_efi_system_part, + '/tmp/fake-dir/boot/efi'), + mock.call(('chroot %s /bin/sh -c ' + '"grub2-mkconfig -o ' + '/boot/grub2/grub.cfg"' % self.fake_dir), + shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin', + 'GRUB_DISABLE_OS_PROBER': 'true', + 'GRUB_SAVEDEFAULT': 'true'}, + use_standard_locale=True), + mock.call('umount', self.fake_dir + '/boot/efi', + attempts=3, delay_on_retry=True), + mock.call(('chroot %s /bin/sh -c "umount -a -t vfat"' % + (self.fake_dir)), shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call('umount', self.fake_dir + '/dev', + attempts=3, delay_on_retry=True), + mock.call('umount', self.fake_dir + '/proc', + attempts=3, delay_on_retry=True), + mock.call('umount', self.fake_dir + '/run', + attempts=3, delay_on_retry=True), + mock.call('umount', self.fake_dir + '/sys', + attempts=3, delay_on_retry=True), + mock.call('umount', self.fake_dir, attempts=3, + delay_on_retry=True)] + + mkdir_mock.assert_not_called() + mock_execute.assert_has_calls(expected) + mock_get_part_uuid.assert_any_call(self.fake_dev, + uuid=self.fake_root_uuid) + mock_get_part_uuid.assert_any_call(self.fake_dev, + uuid=self.fake_efi_system_part_uuid) + self.assertFalse(mock_dispatch.called) + mock_preserve_efi_assets.assert_called_with( + self.fake_dir, + self.fake_dir + '/boot/efi/EFI', + ['/dev/fake1'], + self.fake_dir + '/boot/efi') + + @mock.patch.object(image, '_is_bootloader_loaded', lambda *_: False) + @mock.patch.object(os, 'listdir', autospec=True) + @mock.patch.object(shutil, 'copy2', autospec=True) + @mock.patch.object(os.path, 'isfile', autospec=True) + @mock.patch.object(image, '_efi_boot_setup', autospec=True) + @mock.patch.object(shutil, 'copytree', autospec=True) + @mock.patch.object(os.path, 'exists', autospec=True) + @mock.patch.object(hardware, 'is_md_device', autospec=True) + @mock.patch.object(hardware, 'md_get_raid_devices', autospec=True) + @mock.patch.object(os, 'environ', autospec=True) + @mock.patch.object(os, 'makedirs', autospec=True) + @mock.patch.object(image, '_get_partition', autospec=True) + def test__install_grub2_uefi_partition_image_with_loader_grubcfg_fails( + self, mock_get_part_uuid, mkdir_mock, + environ_mock, mock_md_get_raid_devices, + mock_is_md_device, mock_exists, + mock_copytree, mock_efi_setup, + mock_isfile, mock_copy2, + mock_oslistdir, mock_execute, + mock_dispatch): + mock_exists.return_value = True + mock_efi_setup.return_value = True + mock_get_part_uuid.side_effect = [self.fake_root_part, + self.fake_efi_system_part] + environ_mock.get.return_value = '/sbin' + mock_is_md_device.return_value = False + mock_md_get_raid_devices.return_value = {} + mock_isfile.side_effect = [True, False, False, True, False, + True, False] + mock_copy2.side_effect = OSError('copy failed') + mock_oslistdir.return_value = ['file1', 'file2'] + + image._install_grub2( + self.fake_dev, root_uuid=self.fake_root_uuid, + efi_system_part_uuid=self.fake_efi_system_part_uuid) + mock_efi_setup.assert_called_once_with(self.fake_dev, + self.fake_efi_system_part_uuid) + mock_copytree.assert_has_calls([ + mock.call(self.fake_dir + '/boot/efi/EFI', + self.fake_dir + '/efi_loader'), + mock.call(self.fake_dir + '/efi_loader', + self.fake_dir + '/boot/efi/EFI')]) + + expected = [mock.call('mount', '/dev/fake2', self.fake_dir), + mock.call('mount', '-o', 'bind', '/dev', + self.fake_dir + '/dev'), + mock.call('mount', '-o', 'bind', '/proc', + self.fake_dir + '/proc'), + mock.call('mount', '-o', 'bind', '/run', + self.fake_dir + '/run'), + mock.call('mount', '-t', 'sysfs', 'none', + self.fake_dir + '/sys'), + mock.call(('chroot ' + self.fake_dir + ' /bin/sh -c ' + '"grub2-mkconfig -o /boot/grub2/grub.cfg"'), + shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin', + 'GRUB_DISABLE_OS_PROBER': 'true', + 'GRUB_SAVEDEFAULT': 'true'}, + use_standard_locale=True), + mock.call('mount', '-t', 'vfat', '/dev/fake1', + self.fake_dir + '/boot/efi'), + mock.call('umount', self.fake_dir + '/boot/efi'), + mock.call(('chroot ' + self.fake_dir + + ' /bin/sh -c "umount -a -t vfat"'), + shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call('umount', self.fake_dir + '/dev', attempts=3, + delay_on_retry=True), + mock.call('umount', self.fake_dir + '/proc', attempts=3, + delay_on_retry=True), + mock.call('umount', self.fake_dir + '/run', attempts=3, + delay_on_retry=True), + mock.call('umount', self.fake_dir + '/sys', attempts=3, + delay_on_retry=True), + mock.call('umount', self.fake_dir, attempts=3, + delay_on_retry=True)] + mkdir_mock.assert_not_called() + mock_execute.assert_has_calls(expected) + self.assertEqual(3, mock_copy2.call_count) + mock_get_part_uuid.assert_any_call(self.fake_dev, + uuid=self.fake_root_uuid) + mock_get_part_uuid.assert_any_call(self.fake_dev, + uuid=self.fake_efi_system_part_uuid) + self.assertFalse(mock_dispatch.called) + self.assertEqual(2, mock_oslistdir.call_count) + + @mock.patch.object(os.path, 'ismount', lambda *_: True) + @mock.patch.object(image, '_is_bootloader_loaded', lambda *_: False) + @mock.patch.object(os, 'listdir', autospec=True) + @mock.patch.object(image, '_efi_boot_setup', autospec=True) + @mock.patch.object(shutil, 'copytree', autospec=True) + @mock.patch.object(os.path, 'exists', autospec=True) + @mock.patch.object(hardware, 'is_md_device', autospec=True) + @mock.patch.object(hardware, 'md_get_raid_devices', autospec=True) + @mock.patch.object(os, 'environ', autospec=True) + @mock.patch.object(os, 'makedirs', autospec=True) + @mock.patch.object(image, '_get_partition', autospec=True) + def test__install_grub2_uefi_partition_image_with_no_loader( + self, mock_get_part_uuid, mkdir_mock, + environ_mock, mock_md_get_raid_devices, + mock_is_md_device, mock_exists, + mock_copytree, mock_efi_setup, + mock_oslistdir, mock_execute, + mock_dispatch): + mock_exists.side_effect = [True, False, False, True, True, True, True] + mock_efi_setup.side_effect = Exception('meow') + mock_oslistdir.return_value = ['file1'] + mock_get_part_uuid.side_effect = [self.fake_root_part, + self.fake_efi_system_part] + environ_mock.get.return_value = '/sbin' + mock_is_md_device.return_value = False + mock_md_get_raid_devices.return_value = {} + + image._install_grub2( + self.fake_dev, root_uuid=self.fake_root_uuid, + efi_system_part_uuid=self.fake_efi_system_part_uuid) + + expected = [mock.call('mount', '/dev/fake2', self.fake_dir), + mock.call('mount', '-o', 'bind', '/dev', + self.fake_dir + '/dev'), + mock.call('mount', '-o', 'bind', '/proc', + self.fake_dir + '/proc'), + mock.call('mount', '-o', 'bind', '/run', + self.fake_dir + '/run'), + mock.call('mount', '-t', 'sysfs', 'none', + self.fake_dir + '/sys'), + mock.call('mount', '-t', 'vfat', '/dev/fake1', + self.fake_dir + '/boot/efi'), + mock.call('umount', self.fake_dir + '/boot/efi'), + + mock.call(('chroot %s /bin/sh -c "mount -a -t vfat"' % + (self.fake_dir)), shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call('mount', self.fake_efi_system_part, + self.fake_dir + '/boot/efi'), + mock.call(('chroot %s /bin/sh -c "grub2-install"' % + self.fake_dir), shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call(('chroot %s /bin/sh -c ' + '"grub2-install --removable"' % + self.fake_dir), shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call( + 'umount', self.fake_dir + '/boot/efi', + attempts=3, delay_on_retry=True), + mock.call('mount', self.fake_efi_system_part, + '/tmp/fake-dir/boot/efi'), + mock.call(('chroot %s /bin/sh -c ' + '"grub2-mkconfig -o ' + '/boot/grub2/grub.cfg"' % self.fake_dir), + shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin', + 'GRUB_DISABLE_OS_PROBER': 'true', + 'GRUB_SAVEDEFAULT': 'true'}, + use_standard_locale=True), + mock.call('umount', self.fake_dir + '/boot/efi', + attempts=3, delay_on_retry=True), + mock.call(('chroot %s /bin/sh -c "umount -a -t vfat"' % + (self.fake_dir)), shell=True, + env_variables={ + 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call('umount', self.fake_dir + '/dev', + attempts=3, delay_on_retry=True), + mock.call('umount', self.fake_dir + '/proc', + attempts=3, delay_on_retry=True), + mock.call('umount', self.fake_dir + '/run', + attempts=3, delay_on_retry=True), + mock.call('umount', self.fake_dir + '/sys', + attempts=3, delay_on_retry=True), + mock.call('umount', self.fake_dir, attempts=3, + delay_on_retry=True)] + + mkdir_mock.assert_not_called() + mock_execute.assert_has_calls(expected) + self.assertEqual(2, mock_copytree.call_count) + self.assertTrue(mock_efi_setup.called) + mock_get_part_uuid.assert_any_call(self.fake_dev, + uuid=self.fake_root_uuid) + mock_get_part_uuid.assert_any_call(self.fake_dev, + uuid=self.fake_efi_system_part_uuid) + self.assertFalse(mock_dispatch.called) + @mock.patch.object(image, '_is_bootloader_loaded', lambda *_: False) @mock.patch.object(hardware, 'is_md_device', autospec=True) @mock.patch.object(hardware, 'md_get_raid_devices', autospec=True) @@ -680,28 +1155,25 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n self.fake_dir + '/run'), mock.call('mount', '-t', 'sysfs', 'none', self.fake_dir + '/sys'), - mock.call('mount', self.fake_efi_system_part, - self.fake_dir + '/boot/efi'), + mock.call('mount', '/dev/fake2', self.fake_dir), mock.call(('chroot %s /bin/sh -c "mount -a -t vfat"' % (self.fake_dir)), shell=True, env_variables={ 'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call('mount', '/dev/fake1', + self.fake_dir + '/boot/efi'), mock.call(('chroot %s /bin/sh -c ' - '"grub-install %s"' % - (self.fake_dir, self.fake_dev)), shell=True, - env_variables={'PATH': - '/sbin:/bin:/usr/sbin:/sbin'}), - mock.call(('chroot %s /bin/sh -c ' - '"grub-install %s --removable"' % - (self.fake_dir, self.fake_dev)), shell=True, - env_variables={'PATH': - '/sbin:/bin:/usr/sbin:/sbin'}), - mock.call(('chroot %s /bin/sh -c ' - '"grub-mkconfig -o ' - '/boot/grub/grub.cfg"' % self.fake_dir), + '"grub-install"' % self.fake_dir), shell=True, env_variables={'PATH': '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call(('chroot %s /bin/sh -c ' + '"grub-install --removable"' % + self.fake_dir), shell=True, + env_variables={'PATH': + '/sbin:/bin:/usr/sbin:/sbin'}), + mock.call('umount', self.fake_dir + '/boot/efi', + attempts=3, delay_on_retry=True), mock.call('umount', self.fake_dir + '/boot/efi', attempts=3, delay_on_retry=True)] mock_execute.assert_has_calls(expected) diff --git a/releasenotes/notes/preserve-efi-folder-contents-ea1e278b3093ec55.yaml b/releasenotes/notes/preserve-efi-folder-contents-ea1e278b3093ec55.yaml new file mode 100644 index 000000000..272548211 --- /dev/null +++ b/releasenotes/notes/preserve-efi-folder-contents-ea1e278b3093ec55.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixes the agent's EFI boot handling such that EFI assets from a partition + image are preserved and used instead of overridden. This should permit + operators to use Secure Boot with partition images IF the assets are + already present in the partition image.