Skip to content

Commit

Permalink
Merge 85f8aaf into 118101c
Browse files Browse the repository at this point in the history
  • Loading branch information
ancorgs committed Dec 13, 2019
2 parents 118101c + 85f8aaf commit 2130863
Show file tree
Hide file tree
Showing 19 changed files with 568 additions and 249 deletions.
91 changes: 36 additions & 55 deletions doc/boot-requirements.md
Expand Up @@ -103,63 +103,44 @@
- **requires it to be at most 8 MiB (some firmwares will fail to load bigger ones)**

## needed partitions in a Raspberry Pi
- with a partitions-based proposal
- if there are no EFI partitions
- and there are firmware partitions in several disks, including the target
- **requires a new /boot/efi partition**
- **requires to mount at /boot/vc the firmware partition from the target disk**
- and there is a firmware partition in another disk
- **requires a new /boot/efi partition**
- **requires to mount the existing firmware partition at /boot/vc**
- and there is no firmware partition in the system
- **requires only a new /boot/efi partition**
- if there is suitable EFI partition
- and there are firmware partitions in several disks, including the target
- **requires to use the existing EFI partition**
- **requires to mount at /boot/vc the firmware partition from the target disk**
- and there is a firmware partition in another disk
- **requires to use the existing EFI partition**
- **requires to mount the existing firmware partition at /boot/vc**
- and there is no firmware partition in the system
- **only requires to use the existing EFI partition**
- with a LVM-based proposal
- if there are no EFI partitions
- and there are firmware partitions in several disks, including the target
- **requires a new /boot/efi partition**
- **requires to mount at /boot/vc the firmware partition from the target disk**
- and there is a firmware partition in another disk
- **requires a new /boot/efi partition**
- **requires to mount the existing firmware partition at /boot/vc**
- and there is no firmware partition in the system
- if the target boot disk uses a MBR partition table
- and the first partition in the boot disk has id DOS32 and a FAT filesystem
- and it contains an EFI directory
- **only requires to use the existing EFI partition (bootcode will be installed there)**
- and it contains a Raspberry Pi boot code but no EFI directory
- and there is also a suitable standard EFI partition in the target disk
- **requires to mount at /boot/vc the firmware partition from the target disk**
- **requires to use the existing EFI partition**
- and there is no suitable EFI partition in the target disk
- **requires to mount at /boot/vc the firmware partition from the target disk**
- **requires a new /boot/efi partition**
- **does not enforce the /boot/efi partition to be the first**
- **requires the /boot/efi partition to have id ESP (according to the EFI standard)**
- and the first partition contains no boot code or EFI files
- **requires only a new /boot/efi partition**
- if there is suitable EFI partition
- and there are firmware partitions in several disks, including the target
- **requires to use the existing EFI partition**
- **requires to mount at /boot/vc the firmware partition from the target disk**
- and there is a firmware partition in another disk
- **requires to use the existing EFI partition**
- **requires to mount the existing firmware partition at /boot/vc**
- and there is no firmware partition in the system
- **only requires to use the existing EFI partition**
- with an encrypted proposal
- if there are no EFI partitions
- and there are firmware partitions in several disks, including the target
- **requires a new /boot/efi partition**
- **requires to mount at /boot/vc the firmware partition from the target disk**
- and there is a firmware partition in another disk
- **requires a new /boot/efi partition**
- **requires to mount the existing firmware partition at /boot/vc**
- and there is no firmware partition in the system
- **requires /boot/efi to be at the start of an MBR partition table (bootcode installed there)**
- **requires the /boot/efi partition to have id DOS32**
- and there are no partitions in the boot disk
- **requires only a new /boot/efi partition**
- if there is suitable EFI partition
- and there are firmware partitions in several disks, including the target
- **requires to use the existing EFI partition**
- **requires to mount at /boot/vc the firmware partition from the target disk**
- and there is a firmware partition in another disk
- **requires to use the existing EFI partition**
- **requires to mount the existing firmware partition at /boot/vc**
- and there is no firmware partition in the system
- **only requires to use the existing EFI partition**
- **requires /boot/efi to be at the start of an MBR partition table (bootcode installed there)**
- **requires the /boot/efi partition to have id DOS32**
- and the first partition in the boot disk is a standard EFI with id ESP
- **requires only a new /boot/efi partition**
- **requires /boot/efi to be at the start of an MBR partition table (bootcode installed there)**
- **requires the /boot/efi partition to have id DOS32**
- if the target boot disk uses a GPT partition table
- even if the first partition has id DOS32 and a FAT filesystem with an EFI system
- **requires only a new /boot/efi partition**
- **requires /boot/efi to be at the start of an MBR partition table (bootcode installed there)**
- **requires the /boot/efi partition to have id DOS32**
- if there are no partitions in the boot disk
- **requires only a new /boot/efi partition**
- **requires /boot/efi to be at the start of an MBR partition table (bootcode installed there)**
- **requires the /boot/efi partition to have id DOS32**
- if the target boot disk contains no partition table
- **requires only a new /boot/efi partition**
- **requires /boot/efi to be at the start of an MBR partition table (bootcode installed there)**
- **requires the /boot/efi partition to have id DOS32**
- when proposing a new EFI partition
- **requires /boot/efi to be on the boot disk**
- **requires /boot/efi to be a non-encrypted vfat partition**
Expand Down
169 changes: 113 additions & 56 deletions src/lib/y2storage/boot_requirements_strategies/raspi.rb
Expand Up @@ -25,14 +25,23 @@ module BootRequirementsStrategies
# systems
#
# (open)SUSE approach for booting Raspberry Pi devices is using a firmware
# located in a separate partition that makes the raspi basically work
# as a normal PC-like UEFI system. For details, see fate#323484 and
# located in the first partition of the disk that makes the raspi basically
# work as a normal PC-like UEFI system. For details, see fate#323484 and
# https://www.suse.com/media/article/UEFI_on_Top_of_U-Boot.pdf
#
# So this is just a special case of UEFI boot in which the firmware
# partition must be kept (and mounted to allow updating its content).
# partition must be kept (and mounted to allow updating its content) or
# created. In both cases, the ESP partition can be used to allocate the
# firmware, so a completely dedicated partition is not always needed.
class Raspi < UEFI
RPI_BOOT_MOUNT_PATH = "/boot/vc"
private_constant :RPI_BOOT_MOUNT_PATH
# Path where the dedicated firmware partition, if any, should be mounted
FIRMWARE_MOUNT_PATH = "/boot/vc"
private_constant :FIRMWARE_MOUNT_PATH

# Partition id that must be used in the first partition. Otherwise,
# Raspberry Pi will not recognize it as a valid partition to boot from.
BOOT_PARTITION_ID = Y2Storage::PartitionId::DOS32
private_constant :BOOT_PARTITION_ID

# Constructor, see base class
def initialize(*args)
Expand All @@ -42,86 +51,134 @@ def initialize(*args)

# @see Base#needed_partitions
def needed_partitions(target)
planned_partitions = super

if free_mountpoint?(RPI_BOOT_MOUNT_PATH) && reusable_rpi_boot
planned = Planned::Partition.new(RPI_BOOT_MOUNT_PATH)
planned.reuse_name = reusable_rpi_boot.name
planned_partitions << planned
planned_partitions = []

if boot_partition
if efi_in_boot_partition?
@reusable_efi = boot_partition
elsif firmware_in_boot_partition? && !mounted_firmware?
@reusable_firmware_partition = boot_partition
planned_partitions << planned_firmware
@reusable_efi = biggest_efi_in_boot_device
end
end

planned_partitions << efi_partition(target) if efi_missing?
planned_partitions
end

protected

# System partition containing an usable Raspberry Pi boot code.
# @return [Partition, nil] existing partition that must be reused as /boot/efi,
# nil if there is no such partition or whether the existing one(s) cannot be
# used to boot
attr_reader :reusable_efi

# @return [Partition, nil] existing partition dedicated only to store the
# booting firmware and associated files, nil if there is no such partition
attr_reader :reusable_firmware_partition

# Existing partition from which Raspberry Pi would try to load the bootcode,
# if any
#
# According to fate#323484 and to the "UEFI on Top of U-Boot" paper
# (https://www.suse.com/media/article/UEFI_on_Top_of_U-Boot.pdf),
# the Raspberry Pi boot code resides in a file called "bootcode.bin" that
# is placed in the first partition of a partition table of type MBR. That
# partition must be of type DOS32 (id 0xC) and formatted as VFAT.
#
# FIXME: this involves mounting some of the appealing partitions (first
# FAT partition in a MBR partition table) to check whether the boot code
# is actually there. Thus, caching the result for the whole life of the
# (probed) devicegraph would be desirable.
# @return [Partition, nil] nil if the boot disk contains no partition that
# could be used for booting
def boot_partition
return @boot_partition if @boot_partition_calculated

@boot_partition_calculated = true
@boot_partition = boot_partition_in(boot_disk)
end

# Whether the boot partition contains a Raspberry Pi boot code.
#
# @return [Partition, nil] nil if no suitable partition is found
def reusable_rpi_boot
return @reusable_rpi_boot if @reusable_rpi_checked

@reusable_rpi_checked = true

# First check if there is a firmware partition in the target disk
# If not, keep searching any firmware partition in the other devices
disks = ([boot_disk] + devicegraph.disk_devices).uniq
disks.each do |disk|
@reusable_rpi_boot = suitable_rpi_boot(disk)
return @reusable_rpi_boot if @reusable_rpi_boot
end
# @return [Boolean]
def firmware_in_boot_partition?
return false if boot_partition.nil?

@firmware_in_boot_partition ||= boot_partition.filesystem.rpi_boot?
end

# Whether the boot partition contains the directories layout of an ESP
# partition
#
# @return [Boolean]
def efi_in_boot_partition?
return false if boot_partition.nil?

nil
# Calling suitable_efi_partition?(boot_partition) would not work here because
# that relies on the partition id instead of the content. But our method to
# boot Raspberry Pi does not exactly follow the EFI standards.
@efi_in_boot_partition ||= boot_partition.filesystem.efi?
end

# Partition in the given disk containing an usable Raspberry Pi
# boot code.
# Planned partition to reuse the existing dedicated firmware partition
#
# @see #reusable_rpi_boot
# This method only makes sense if there is a firmware partition to reuse.
#
# @param disk [Partitionable] disk to analyze
# @return [Partition, nil] nil if no suitable partition is found
def suitable_rpi_boot(disk)
return nil unless msdos_ptable?(disk)
# @return [Planned::Partition]
def planned_firmware
planned = Planned::Partition.new(FIRMWARE_MOUNT_PATH)
planned.reuse_name = reusable_firmware_partition.name
planned
end

partition = first_partition(disk)
# In our experience, partition ids are too often set to a wrong value
# and even the firmwares are kind of relaxed about the ids they accept
# for a given purpose.
# So, for the time being, let's skip the check for partition.id.is?(:dos32)
return nil if partition.nil?
# See {UEFI#efi_partition}, this overrides the method in the parent class
# with some small differences between a standard EFI partition and the
# Raspberry Pi version of it
#
# @return [Planned::Partition]
def efi_partition(_target)
planned = super
# raspi-specific modifications of the standard EFI partition are only
# needed if the EFI partition is going to be created (not reused) and
# there is not a dedicated separate partition for the firmware
return planned if reusable_firmware_partition || planned.reuse?

planned.ptable_type = PartitionTables::Type::MSDOS
planned.partition_id = BOOT_PARTITION_ID
planned.max_start_offset = max_offset_for_first_partition
planned
end

rpi_boot?(partition) ? partition : nil
# Value to be used in {Planned::Partition#max_start_offset} to ensure the
# created partition is the first of the disk
#
# @return [DiskSize]
def max_offset_for_first_partition
# This should be enough in the case of a Raspberry Pi. It makes no sense
# to introduce more complicated logic to support devices with strange
# topologies that will never be used in a Pi.
PartitionTables::Msdos.default_mbr_gap
end

# @see #suitable_rpi_boot
# Whether there is already a partition configured to be used as separate
# firmware partition
#
# @param partition [Partition]
# @return [Boolean]
def rpi_boot?(partition)
filesystem = partition.direct_blk_filesystem
return false if filesystem.nil? || !filesystem.type.is?(:vfat)

filesystem.rpi_boot?
def mounted_firmware?
!free_mountpoint?(FIRMWARE_MOUNT_PATH)
end

# First partition in a disk
# @see #boot_partition
#
# @param disk [Partitionable]
# @return [Partition, nil] nil if the disk contains no partitions
def first_partition(disk)
disk.partitions.min_by { |p| p.region.start }
# @return [Partition, nil]
def boot_partition_in(disk)
return nil unless msdos_ptable?(disk)

first = disk.partitions.min_by { |p| p.region.start }
return nil if first.nil? || first.id != BOOT_PARTITION_ID

filesystem = first.direct_blk_filesystem
return nil if filesystem.nil? || !filesystem.type.is?(:vfat)

first
end

# Whether the disk contains an MS-DOS style (a.k.a. MBR) partition table
Expand Down
38 changes: 28 additions & 10 deletions src/lib/y2storage/boot_requirements_strategies/uefi.rb
Expand Up @@ -105,12 +105,26 @@ def efi_volume
@efi_volume
end

# Adjusts the given volume specification to enforce a minimal device that
# never grows beyond its minimum size
#
# @param vol [VolumeSpecification] specification that will be modified by the method
# @return [VolumeSpecification]
def limit_volume_size_to_min(vol)
vol.max_size = vol.min_size
vol.desired_size = vol.min_size
vol
end

# Maximum offset within the boot disk in which the ESP partition can be located
#
# The limit of 2TiB has been used since the first versions of
# yast2-storage-ng, although the origin is not absolutely clear.
#
# @return [DiskSize]
EFI_MAX_START = DiskSize.TiB(2).freeze
private_constant :EFI_MAX_START

# @return [Planned::Partition]
def efi_partition(target)
planned_partition = create_planned_partition(efi_volume, target)
Expand All @@ -121,31 +135,35 @@ def efi_partition(target)
if reusable_efi
planned_partition.reuse_name = reusable_efi.name
else
planned_partition.max_start_offset = DiskSize.TiB(2)
planned_partition.max_start_offset = EFI_MAX_START
planned_partition.disk = boot_disk.name
end

planned_partition
end

def reusable_efi
@reusable_efi = biggest_efi_in_boot_device
@reusable_efi ||= biggest_efi_in_boot_device
end

def biggest_efi_in_boot_device
biggest_partition(suitable_efi_partitions(boot_disk))
end

def biggest_efi
efi_partitions = devicegraph.disk_devices.map { |d| suitable_efi_partitions(d) }.flatten
biggest_partition(efi_partitions)
# Devices on the given disk that are usable as ESP for our purposes
#
# @param device [Y2Storage::Partitionable] disk device
# @return [Array<Y2Storage::Partition>]
def suitable_efi_partitions(device)
device.partitions.select { |part| suitable_efi_partition?(part) }
end

def suitable_efi_partitions(device)
device.partitions.select do |partition|
partition.match_volume?(efi_volume, exclude: :mount_point) &&
partition.id == PartitionId::ESP
end
# Whether the given partition is usable as ESP for our purposes
#
# @param partition [Y2Storage::Partition]
# @return [Boolean]
def suitable_efi_partition?(partition)
partition.match_volume?(efi_volume, exclude: :mount_point) && partition.id == PartitionId::ESP
end

def biggest_partition(partitions)
Expand Down

0 comments on commit 2130863

Please sign in to comment.