454 lines
14 KiB
Go
454 lines
14 KiB
Go
// Package disk contains data types and functions to define and modify
|
|
// disk-related and partition-table-related entities.
|
|
//
|
|
// The disk package is a collection of interfaces and structs that can be used
|
|
// to represent a disk image with its layout. Various concrete types, such as
|
|
// PartitionTable, Partition and Filesystem types are defined to model a given
|
|
// disk layout. These implement a collection of interfaces that can be used to
|
|
// navigate and operate on the various possible combinations of entities in a
|
|
// generic way. The entity data model is very generic so that it can represent
|
|
// all possible layouts, which can be arbitrarily complex, since technologies
|
|
// like logical volume management, LUKS2 containers and file systems, that can
|
|
// have sub-volumes, allow for complex and nested layouts.
|
|
//
|
|
// Entity and Container are the two main interfaces that are used to model the
|
|
// tree structure of a disk image layout. The other entity interfaces, such as
|
|
// Sizeable and Mountable, then describe various properties and capabilities
|
|
// of a given entity.
|
|
package disk
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"slices"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/osbuild/images/pkg/arch"
|
|
)
|
|
|
|
const (
|
|
// Default sector size in bytes
|
|
DefaultSectorSize = 512
|
|
|
|
// Default grain size in bytes. The grain controls how sizes of certain
|
|
// entities are rounded. For example, by default, partition sizes are
|
|
// rounded to the next MiB.
|
|
DefaultGrainBytes = uint64(1048576) // 1 MiB
|
|
|
|
// GUIDs (partition types) for partitions on GPT disks
|
|
// The SD_GPT name next to each constant is the partition type shown in
|
|
// systemd-gpt-auto-generator(8) (if present)
|
|
// See also
|
|
// - https://www.freedesktop.org/wiki/Specifications/DiscoverablePartitionsSpec/
|
|
// - https://uapi-group.org/specifications/specs/discoverable_partitions_specification/
|
|
BIOSBootPartitionGUID = "21686148-6449-6E6F-744E-656564454649"
|
|
FilesystemDataGUID = "0FC63DAF-8483-4772-8E79-3D69D8477DE4" // SD_GPT_LINUX_GENERIC
|
|
EFISystemPartitionGUID = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" // SD_GPT_ESP
|
|
LVMPartitionGUID = "E6D6D379-F507-44C2-A23C-238F2A3DF928"
|
|
PRePartitionGUID = "9E1A2D38-C612-4316-AA26-8B49521E5A8B"
|
|
SwapPartitionGUID = "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F" // SD_GPT_SWAP
|
|
XBootLDRPartitionGUID = "BC13C2FF-59E6-4262-A352-B275FD6F7172" // SD_GPT_XBOOTLDR
|
|
|
|
RootPartitionX86_64GUID = "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709" // SD_GPT_ROOT_X86_64
|
|
RootPartitionAarch64GUID = "B921B045-1DF0-41C3-AF44-4C6F280D3FAE" // SD_GPT_ROOT_ARM64
|
|
RootPartitionPpc64leGUID = "C31C45E6-3F39-412E-80FB-4809C4980599" // SD_GPT_ROOT_PPC64_LE
|
|
RootPartitionS390xGUID = "5EEAD9A9-FE09-4A1E-A1D7-520D00531306" // SD_GPT_ROOT_S390X
|
|
|
|
UsrPartitionX86_64GUID = "8484680C-9521-48C6-9C11-B0720656F69E" // SD_GPT_USR_X86_64
|
|
UsrPartitionAarch64GUID = "B0E01050-EE5F-4390-949A-9101B17104E9" // SD_GPT_USR_ARM64
|
|
UsrPartitionPpc64leGUID = "15BB03AF-77E7-4D4A-B12B-C0D084F7491C" // SD_GPT_USR_PPC64_LE
|
|
UsrPartitionS390xGUID = "8A4F5770-50AA-4ED3-874A-99B710DB6FEA" // SD_GPT_USR_S390X
|
|
|
|
// Partition type IDs for DOS disks
|
|
|
|
// Partition type ID for BIOS boot partition on dos.
|
|
// Type ID is for 'empty'.
|
|
// TODO: drop this completely when we convert the bios BOOT space to a
|
|
// partitionless gap/offset.
|
|
BIOSBootPartitionDOSID = "00"
|
|
|
|
// Partition type ID for any native Linux filesystem on dos
|
|
FilesystemLinuxDOSID = "83"
|
|
|
|
// FAT16BDOSID used for the ESP-System partition
|
|
FAT16BDOSID = "06"
|
|
|
|
// Partition type ID for LVM on dos
|
|
LVMPartitionDOSID = "8e"
|
|
|
|
// Partition type ID for ESP on dos
|
|
EFISystemPartitionDOSID = "ef"
|
|
|
|
// Partition type ID for swap
|
|
SwapPartitionDOSID = "82"
|
|
|
|
// Partition type ID for PRep on dos
|
|
PRepPartitionDOSID = "41"
|
|
|
|
// static UUIDs for partitions and filesystems
|
|
// NOTE(akoutsou): These are unnecessary and have stuck around since the
|
|
// beginning where (I believe) the goal was to have predictable,
|
|
// reproducible partition tables. They might be removed soon in favour of
|
|
// proper, random UUIDs, with reproducibility being controlled by fixing
|
|
// rng seeds.
|
|
BIOSBootPartitionUUID = "FAC7F1FB-3E8D-4137-A512-961DE09A5549"
|
|
RootPartitionUUID = "6264D520-3FB9-423F-8AB8-7A0A8E3D3562"
|
|
DataPartitionUUID = "CB07C243-BC44-4717-853E-28852021225B"
|
|
EFISystemPartitionUUID = "68B2905B-DF3E-4FB3-80FA-49D1E773AA33"
|
|
|
|
EFIFilesystemUUID = "7B77-95E7"
|
|
)
|
|
|
|
func getPartitionTypeIDfor(ptType PartitionTableType, partTypeName string, architecture arch.Arch) (string, error) {
|
|
switch ptType {
|
|
case PT_DOS:
|
|
switch partTypeName {
|
|
case "bios":
|
|
return BIOSBootPartitionDOSID, nil
|
|
case "data", "boot", "root", "usr":
|
|
return FilesystemLinuxDOSID, nil
|
|
case "esp":
|
|
return EFISystemPartitionDOSID, nil
|
|
case "lvm":
|
|
return LVMPartitionDOSID, nil
|
|
case "swap":
|
|
return SwapPartitionDOSID, nil
|
|
default:
|
|
return "", fmt.Errorf("unknown or unsupported partition type name: %s", partTypeName)
|
|
}
|
|
case PT_GPT:
|
|
switch partTypeName {
|
|
case "bios":
|
|
return BIOSBootPartitionGUID, nil
|
|
case "boot":
|
|
return XBootLDRPartitionGUID, nil
|
|
case "data":
|
|
return FilesystemDataGUID, nil
|
|
case "esp":
|
|
return EFISystemPartitionGUID, nil
|
|
case "lvm":
|
|
return LVMPartitionGUID, nil
|
|
case "swap":
|
|
return SwapPartitionGUID, nil
|
|
case "root":
|
|
switch architecture {
|
|
case arch.ARCH_X86_64:
|
|
return RootPartitionX86_64GUID, nil
|
|
case arch.ARCH_AARCH64:
|
|
return RootPartitionAarch64GUID, nil
|
|
case arch.ARCH_PPC64LE:
|
|
return RootPartitionPpc64leGUID, nil
|
|
case arch.ARCH_S390X:
|
|
return RootPartitionS390xGUID, nil
|
|
case arch.ARCH_UNSET:
|
|
return "", fmt.Errorf("architecture must be specified for selecting GUID for %q partition", partTypeName)
|
|
default:
|
|
return "", fmt.Errorf("unknown or unsupported architecture enum value: %d", architecture)
|
|
}
|
|
case "usr":
|
|
switch architecture {
|
|
case arch.ARCH_X86_64:
|
|
return UsrPartitionX86_64GUID, nil
|
|
case arch.ARCH_AARCH64:
|
|
return UsrPartitionAarch64GUID, nil
|
|
case arch.ARCH_PPC64LE:
|
|
return UsrPartitionPpc64leGUID, nil
|
|
case arch.ARCH_S390X:
|
|
return UsrPartitionS390xGUID, nil
|
|
case arch.ARCH_UNSET:
|
|
return "", fmt.Errorf("architecture must be specified for selecting GUID for %q partition", partTypeName)
|
|
default:
|
|
return "", fmt.Errorf("unknown or unsupported architecture enum value: %d", architecture)
|
|
}
|
|
default:
|
|
return "", fmt.Errorf("unknown or unsupported partition type name: %s", partTypeName)
|
|
}
|
|
default:
|
|
return "", fmt.Errorf("unknown or unsupported partition table enum: %d", ptType)
|
|
}
|
|
}
|
|
|
|
// FSType is the filesystem type enum.
|
|
//
|
|
// There should always be one value for each filesystem type supported by
|
|
// osbuild stages (stages/org.osbuild.mkfs.*) and the unset/none value.
|
|
type FSType uint64
|
|
|
|
const (
|
|
FS_NONE FSType = iota
|
|
FS_VFAT
|
|
FS_EXT4
|
|
FS_XFS
|
|
FS_BTRFS
|
|
)
|
|
|
|
func (f FSType) String() string {
|
|
switch f {
|
|
case FS_NONE:
|
|
return ""
|
|
case FS_VFAT:
|
|
return "vfat"
|
|
case FS_EXT4:
|
|
return "ext4"
|
|
case FS_XFS:
|
|
return "xfs"
|
|
case FS_BTRFS:
|
|
return "btrfs"
|
|
default:
|
|
panic(fmt.Sprintf("unknown or unsupported filesystem type with enum value %d", f))
|
|
}
|
|
}
|
|
|
|
func NewFSType(s string) (FSType, error) {
|
|
switch s {
|
|
case "":
|
|
return FS_NONE, nil
|
|
case "vfat":
|
|
return FS_VFAT, nil
|
|
case "ext4":
|
|
return FS_EXT4, nil
|
|
case "xfs":
|
|
return FS_XFS, nil
|
|
case "btrfs":
|
|
return FS_BTRFS, nil
|
|
default:
|
|
return FS_NONE, fmt.Errorf("unknown or unsupported filesystem type name: %s", s)
|
|
}
|
|
}
|
|
|
|
// PartitionTableType is the partition table type enum.
|
|
type PartitionTableType uint64
|
|
|
|
const (
|
|
PT_NONE PartitionTableType = iota
|
|
PT_DOS
|
|
PT_GPT
|
|
)
|
|
|
|
func (t PartitionTableType) String() string {
|
|
switch t {
|
|
case PT_NONE:
|
|
return ""
|
|
case PT_DOS:
|
|
return "dos"
|
|
case PT_GPT:
|
|
return "gpt"
|
|
default:
|
|
panic(fmt.Sprintf("unknown or unsupported partition table type with enum value %d", t))
|
|
}
|
|
}
|
|
|
|
func (t PartitionTableType) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(t.String())
|
|
}
|
|
|
|
func (t *PartitionTableType) UnmarshalJSON(data []byte) error {
|
|
var s string
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
return err
|
|
}
|
|
|
|
new, err := NewPartitionTableType(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*t = new
|
|
return nil
|
|
}
|
|
|
|
func (t *PartitionTableType) UnmarshalYAML(unmarshal func(any) error) error {
|
|
return unmarshalYAMLviaJSON(t, unmarshal)
|
|
}
|
|
|
|
func NewPartitionTableType(s string) (PartitionTableType, error) {
|
|
switch s {
|
|
case "":
|
|
return PT_NONE, nil
|
|
case "dos":
|
|
return PT_DOS, nil
|
|
case "gpt":
|
|
return PT_GPT, nil
|
|
default:
|
|
return PT_NONE, fmt.Errorf("unknown or unsupported partition table type name: %s", s)
|
|
}
|
|
}
|
|
|
|
// Entity is the base interface for all disk-related entities.
|
|
type Entity interface {
|
|
// Clone returns a deep copy of the entity.
|
|
Clone() Entity
|
|
}
|
|
|
|
// PayloadEntity is an entity that can be used as a Payload for a Container.
|
|
type PayloadEntity interface {
|
|
Entity
|
|
|
|
// EntityName is the type name of the Entity, used for marshaling
|
|
EntityName() string
|
|
}
|
|
|
|
var payloadEntityMap = map[string]reflect.Type{}
|
|
|
|
// Container is the interface for entities that can contain other entities.
|
|
// Together with the base Entity interface this allows to model a generic
|
|
// entity tree of theoretically arbitrary depth and width.
|
|
type Container interface {
|
|
Entity
|
|
|
|
// GetItemCount returns the number of actual child entities.
|
|
GetItemCount() uint
|
|
|
|
// GetChild returns the child entity at the given index.
|
|
GetChild(n uint) Entity
|
|
}
|
|
|
|
// Sizeable is implemented by entities that carry size information.
|
|
type Sizeable interface {
|
|
// EnsureSize will resize the entity to the given size in case
|
|
// it is currently smaller. Returns if the size was changed.
|
|
EnsureSize(size uint64) bool
|
|
|
|
// GetSize returns the size of the entity in bytes.
|
|
GetSize() uint64
|
|
}
|
|
|
|
// A Mountable entity is an entity that can be mounted.
|
|
type Mountable interface {
|
|
|
|
// GetMountPoint returns the path of the mount point.
|
|
GetMountpoint() string
|
|
|
|
FSTabEntity
|
|
}
|
|
|
|
// FSTabEntity describes any entity that can appear in the fstab file.
|
|
type FSTabEntity interface {
|
|
// FSSpec for the entity (UUID and Label); the first field of fstab(5).
|
|
GetFSSpec() FSSpec
|
|
|
|
// The mount point (target) for a filesystem or "none" for swap areas; the second field of fstab(5).
|
|
GetFSFile() string
|
|
|
|
// The type of the filesystem or swap for swap areas; the third field of fstab(5).
|
|
GetFSType() string
|
|
|
|
// The mount options, freq, and passno for the entity; the fourth fifth, and sixth fields of fstab(5) respectively.
|
|
GetFSTabOptions() (FSTabOptions, error)
|
|
}
|
|
|
|
// A MountpointCreator is a container that is able to create new volumes.
|
|
//
|
|
// CreateMountpoint creates a new mountpoint with the given size and
|
|
// returns the entity that represents the new mountpoint.
|
|
type MountpointCreator interface {
|
|
CreateMountpoint(mountpoint string, size uint64) (Entity, error)
|
|
|
|
// AlignUp will align the given bytes according to the
|
|
// requirements of the container type.
|
|
AlignUp(size uint64) uint64
|
|
}
|
|
|
|
// A UniqueEntity is an entity that can be uniquely identified via a UUID.
|
|
//
|
|
// GenUUID generates a UUID for the entity if it does not yet have one.
|
|
type UniqueEntity interface {
|
|
Entity
|
|
GenUUID(rng *rand.Rand)
|
|
}
|
|
|
|
// VolumeContainer is a specific container that contains volume entities
|
|
type VolumeContainer interface {
|
|
|
|
// MetadataSize returns the size of the container's metadata (in
|
|
// bytes), i.e. the storage space that needs to be reserved for
|
|
// the container itself, in contrast to the data it contains.
|
|
MetadataSize() uint64
|
|
|
|
// minSize returns the size for the VolumeContainer that is either the
|
|
// provided desired size value or the sum of all children if that is
|
|
// larger. It will also add any space required for metadata. The returned
|
|
// value should, at minimum, be large enough to fit all the children, their
|
|
// metadata, and the VolumeContainer's metadata. In other words, the
|
|
// VolumeContainer's size, or its parent size, will be able to hold the
|
|
// VolumeContainer if it is created with the exact size returned by the
|
|
// function.
|
|
minSize(size uint64) uint64
|
|
}
|
|
|
|
// FSSpec for a filesystem (UUID and Label); the first field of fstab(5)
|
|
type FSSpec struct {
|
|
UUID string
|
|
Label string
|
|
}
|
|
|
|
type FSTabOptions struct {
|
|
// The fourth field of fstab(5); fs_mntops
|
|
MntOps string
|
|
// The fifth field of fstab(5); fs_freq
|
|
Freq uint64
|
|
// The sixth field of fstab(5); fs_passno
|
|
PassNo uint64
|
|
}
|
|
|
|
// ReadOnly returns true if the filesystem is mounted read-only.
|
|
func (o FSTabOptions) ReadOnly() bool {
|
|
opts := strings.Split(o.MntOps, ",")
|
|
|
|
// filesystem is mounted read-only if:
|
|
// - there's ro (because rw is the default)
|
|
// - AND there's no rw (because rw overrides ro)
|
|
return slices.Contains(opts, "ro") && !slices.Contains(opts, "rw")
|
|
}
|
|
|
|
// uuid generator helpers
|
|
|
|
// GeneratesnewRandomUUIDFromReader generates a new random UUID (version
|
|
// 4 using) via the given random number generator.
|
|
func newRandomUUIDFromReader(r io.Reader) (uuid.UUID, error) {
|
|
var id uuid.UUID
|
|
_, err := io.ReadFull(r, id[:])
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
id[6] = (id[6] & 0x0f) | 0x40 // Version 4
|
|
id[8] = (id[8] & 0x3f) | 0x80 // Variant is 10
|
|
return id, nil
|
|
}
|
|
|
|
// NewVolIDFromRand creates a random 32 bit hex string to use as a volume ID
|
|
// for FAT filesystems.
|
|
func NewVolIDFromRand(r *rand.Rand) string {
|
|
volid := make([]byte, 4)
|
|
len, _ := r.Read(volid)
|
|
if len != 4 {
|
|
panic("expected four random bytes")
|
|
}
|
|
return hex.EncodeToString(volid)
|
|
}
|
|
|
|
// genUniqueString returns a string based on base that does does not exist in
|
|
// the existing set. If the base itself does not exist, it is returned as is,
|
|
// otherwise a two digit number is added and incremented until a unique string
|
|
// is found.
|
|
// This function is mimicking what blivet does for avoiding name collisions.
|
|
// See blivet/blivet.py#L1060 commit 2eb4bd4
|
|
func genUniqueString(base string, existing map[string]bool) (string, error) {
|
|
if !existing[base] {
|
|
return base, nil
|
|
}
|
|
|
|
for i := 0; i < 100; i++ {
|
|
uniq := fmt.Sprintf("%s%02d", base, i)
|
|
if !existing[uniq] {
|
|
return uniq, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("name collision: could not generate unique version of %q", base)
|
|
}
|