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)
def test_get_efi_part_on_device(self, mocked_type, mocked_parts): mocked_parts.return_value = [ {'number': '1', 'flags': ''}, {'number': '14', 'flags': 'bios_grub'}, {'number': '15', 'flags': 'esp, boot'}, ] ret = utils.get_efi_part_on_device('/dev/sda') self.assertEqual('15', ret)
def test_get_efi_part_on_device_only_boot_flag_mbr(self, mocked_type, mocked_parts): mocked_type.return_value = 'msdos' mocked_parts.return_value = [ {'number': '1', 'flags': ''}, {'number': '14', 'flags': 'bios_grub'}, {'number': '15', 'flags': 'boot'}, ] self.assertIsNone(utils.get_efi_part_on_device('/dev/sda'))
def test_get_efi_part_on_device_not_found(self, mocked_type, mocked_parts): mocked_parts.return_value = [ { 'number': '1', 'flags': '' }, { 'number': '14', 'flags': 'bios_grub' }, ] self.assertIsNone(utils.get_efi_part_on_device('/dev/sda'))
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: # Force UEFI to rescan the device. Required if the deployment # was over iscsi. _rescan_device(device) 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 _prepare_boot_partitions_for_softraid(device, holders, efi_part, target_boot_mode): """Prepare boot partitions when relevant. Create either efi partitions or bios boot partitions for softraid, according to both target boot mode and disk holders partition table types. :param device: the softraid device path :param holders: the softraid drive members :param efi_part: when relevant the efi partition coming from the image deployed on softraid device, can be/is often None :param target_boot_mode: target boot mode can be bios/uefi/None or anything else for unspecified :returns: the efi partition paths on softraid disk holders when target boot mode is uefi, empty list otherwise. """ efi_partitions = [] # Actually any fat partition could be a candidate. Let's assume the # partition also has the esp flag if target_boot_mode == 'uefi': if not efi_part: LOG.debug( "No explicit EFI partition provided. Scanning for any " "EFI partition located on software RAID device %s to " "be relocated", device) # NOTE: for whole disk images, no efi part uuid will be provided. # Let's try to scan for esp on the root softraid device. If not # found, it's fine in most cases to just create an empty esp and # let grub handle the magic. efi_part = utils.get_efi_part_on_device(device) if efi_part: efi_part = '{}p{}'.format(device, efi_part) LOG.info("Creating EFI partitions on software RAID holder disks") # We know that we kept this space when configuring raid,see # hardware.GenericHardwareManager.create_configuration. # We could also directly get the EFI partition size. partsize_mib = raid_utils.ESP_SIZE_MIB partlabel_prefix = 'uefi-holder-' for number, holder in enumerate(holders): # NOTE: see utils.get_partition_table_type_from_specs # for uefi we know that we have setup a gpt partition table, # sgdisk can be used to edit table, more user friendly # for alignment and relative offsets partlabel = '{}{}'.format(partlabel_prefix, number) out, _u = utils.execute('sgdisk', '-F', holder) start_sector = '{}s'.format(out.splitlines()[-1].strip()) out, _u = utils.execute( 'sgdisk', '-n', '0:{}:+{}MiB'.format(start_sector, partsize_mib), '-t', '0:ef00', '-c', '0:{}'.format(partlabel), holder) # Refresh part table utils.execute("partprobe") utils.execute("blkid") target_part, _u = utils.execute("blkid", "-l", "-t", "PARTLABEL={}".format(partlabel), holder) target_part = target_part.splitlines()[-1].split(':', 1)[0] LOG.debug("EFI partition %s created on holder disk %s", target_part, holder) if efi_part: LOG.debug("Relocating EFI %s to holder part %s", efi_part, target_part) # Blockdev copy utils.execute("cp", efi_part, target_part) else: # Creating a label is just to make life easier if number == 0: fslabel = 'efi-part' else: # bak, label is limited to 11 chars fslabel = 'efi-part-b' ilib_utils.mkfs(fs='vfat', path=target_part, label=fslabel) efi_partitions.append(target_part) # TBD: Would not hurt to destroy source efi part when defined, # for clarity. elif target_boot_mode == 'bios': partlabel_prefix = 'bios-boot-part-' for number, holder in enumerate(holders): label = utils.scan_partition_table_type(holder) if label == 'gpt': LOG.debug("Creating bios boot partition on disk holder %s", holder) out, _u = utils.execute('sgdisk', '-F', holder) start_sector = '{}s'.format(out.splitlines()[-1].strip()) partlabel = '{}{}'.format(partlabel_prefix, number) out, _u = utils.execute('sgdisk', '-n', '0:{}:+2MiB'.format(start_sector), '-t', '0:ef02', '-c', '0:{}'.format(partlabel), holder) # Q: MBR case, could we dd the boot code from the softraid # (446 first bytes) if we detect a bootloader with # _is_bootloader_loaded? # A: This won't work. Because it includes the address on the # disk, as in virtual disk, where to load the data from. # Since there is a structural difference, this means it will # fail. # Just an empty list if not uefi boot mode, nvm, not used anyway return efi_partitions
def test_get_efi_part_on_device_not_found(self, mocked_execute): mocked_execute.side_effect = [(PARTED_OUTPUT_NO_EFI, None)] self.assertIsNone(utils.get_efi_part_on_device('/dev/sda')) mocked_execute.assert_has_calls( [mock.call('parted', '-s', '/dev/sda', '--', 'print')])
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. :raises: DeviceNotFound if the efi partition cannot be found. :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_mount_point = None efi_mounted = False try: # Force UEFI to rescan the device. Required if the deployment # was over iscsi. _rescan_device(device) 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 and efi_system_part_uuid: # _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 not efi_partition: # NOTE(dtantsur): we cannot have a valid EFI deployment without an # EFI partition at all. This code path is easily hit when using an # image that is not UEFI compatible (which sadly applies to most # cloud images out there, with a nice exception of Ubuntu). raise errors.DeviceNotFound( "No EFI partition could be detected on device %s and " "EFI partition UUID has not been recorded during deployment " "(which is often the case for whole disk images). " "Are you using a UEFI-compatible image?" % device) 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 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: # NOTE(dtantsur): if we have an empty EFI partition, try to use # grub-install to populate it. 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: LOG.debug('Executing _manage_uefi clean-up.') 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)