diff --git a/internal/disk/disk.go b/internal/disk/disk.go index 8f6933eec..4b9486be8 100644 --- a/internal/disk/disk.go +++ b/internal/disk/disk.go @@ -6,15 +6,10 @@ package disk import ( "encoding/hex" - "errors" - "fmt" "io" - "math/rand" - "sort" "github.com/google/uuid" osbuild "github.com/osbuild/osbuild-composer/internal/osbuild1" - "github.com/osbuild/osbuild-composer/internal/osbuild2" ) const ( @@ -24,14 +19,19 @@ const ( DefaultGrainBytes = uint64(1024 * 1024) // 1 MiB ) -type PartitionTable struct { - Size uint64 // Size of the disk (in bytes). - UUID string // Unique identifier of the partition table (GPT only). - Type string // Partition table type, e.g. dos, gpt. - Partitions []Partition +// FSSpec for a filesystem (UUID and Label); the first field of fstab(5) +type FSSpec struct { + UUID string + Label string +} - SectorSize uint64 // Sector size in bytes - ExtraPadding uint64 // Extra space at the end of the partition table (sectors) +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 } type Partition struct { @@ -61,355 +61,6 @@ type Filesystem struct { FSTabPassNo uint64 } -// AlignUp will align the given bytes to next aligned grain if not already -// aligned -func (pt *PartitionTable) AlignUp(size uint64) uint64 { - grain := DefaultGrainBytes - if size%grain == 0 { - // already aligned: return unchanged - return size - } - return ((size + grain) / grain) * grain -} - -// Convert the given bytes to the number of sectors. -func (pt *PartitionTable) BytesToSectors(size uint64) uint64 { - sectorSize := pt.SectorSize - if sectorSize == 0 { - sectorSize = DefaultSectorSize - } - return size / sectorSize -} - -// Convert the given number of sectors to bytes. -func (pt *PartitionTable) SectorsToBytes(size uint64) uint64 { - sectorSize := pt.SectorSize - if sectorSize == 0 { - sectorSize = DefaultSectorSize - } - return size * sectorSize -} - -// Clone the partition table (deep copy). -func (pt *PartitionTable) Clone() *PartitionTable { - if pt == nil { - return nil - } - - var partitions []Partition - for _, p := range pt.Partitions { - p.Filesystem = p.Filesystem.Clone() - partitions = append(partitions, p) - } - return &PartitionTable{ - Size: pt.Size, - UUID: pt.UUID, - Type: pt.Type, - Partitions: partitions, - - SectorSize: pt.SectorSize, - ExtraPadding: pt.ExtraPadding, - } -} - -// Converts PartitionTable to osbuild.QEMUAssemblerOptions that encode -// the same partition table. -func (pt *PartitionTable) QEMUAssemblerOptions() osbuild.QEMUAssemblerOptions { - var partitions []osbuild.QEMUPartition - for _, p := range pt.Partitions { - partitions = append(partitions, p.QEMUPartition()) - } - - return osbuild.QEMUAssemblerOptions{ - Size: pt.Size, - PTUUID: pt.UUID, - PTType: pt.Type, - Partitions: partitions, - } -} - -// Generates org.osbuild.fstab stage options from this partition table. -func (pt *PartitionTable) FSTabStageOptions() *osbuild.FSTabStageOptions { - var options osbuild.FSTabStageOptions - for _, p := range pt.Partitions { - fs := p.Filesystem - if fs == nil { - continue - } - - options.AddFilesystem(fs.UUID, fs.Type, fs.Mountpoint, fs.FSTabOptions, fs.FSTabFreq, fs.FSTabPassNo) - } - - // sort the entries by PassNo to maintain backward compatibility - sort.Slice(options.FileSystems, func(i, j int) bool { - return options.FileSystems[i].PassNo < options.FileSystems[j].PassNo - }) - - return &options -} - -// Generates org.osbuild.fstab stage options from this partition table. -func (pt *PartitionTable) FSTabStageOptionsV2() *osbuild2.FSTabStageOptions { - var options osbuild2.FSTabStageOptions - for _, p := range pt.Partitions { - fs := p.Filesystem - if fs == nil { - continue - } - - options.AddFilesystem(fs.UUID, fs.Type, fs.Mountpoint, fs.FSTabOptions, fs.FSTabFreq, fs.FSTabPassNo) - } - - // sort the entries by PassNo to maintain backward compatibility - sort.Slice(options.FileSystems, func(i, j int) bool { - return options.FileSystems[i].PassNo < options.FileSystems[j].PassNo - }) - - return &options -} - -func (pt *PartitionTable) FindPartitionForMountpoint(mountpoint string) *Partition { - for idx, p := range pt.Partitions { - if p.Filesystem == nil { - continue - } - - if p.Filesystem.Mountpoint == mountpoint { - return &pt.Partitions[idx] - } - } - - return nil -} - -// Returns the root partition (the partition whose filesystem has / as -// a mountpoint) of the partition table. Nil is returned if there's no such -// partition. -func (pt *PartitionTable) RootPartition() *Partition { - return pt.FindPartitionForMountpoint("/") -} - -// Returns the /boot partition (the partition whose filesystem has /boot as -// a mountpoint) of the partition table. Nil is returned if there's no such -// partition. -func (pt *PartitionTable) BootPartition() *Partition { - return pt.FindPartitionForMountpoint("/boot") -} - -// Returns the index of the boot partition: the partition whose filesystem has -// /boot as a mountpoint. If there is no explicit boot partition, the root -// partition is returned. -// If neither boot nor root partitions are found, returns -1. -func (pt *PartitionTable) BootPartitionIndex() int { - // find partition with '/boot' mountpoint and fallback to '/' - rootIdx := -1 - for idx, part := range pt.Partitions { - if part.Filesystem == nil { - continue - } - if part.Filesystem.Mountpoint == "/boot" { - return idx - } else if part.Filesystem.Mountpoint == "/" { - rootIdx = idx - } - } - return rootIdx -} - -// StopIter is used as a return value from iterator function to indicate -// the iteration should not continue. Not an actual error and thus not -// returned by iterator function. -var StopIter = errors.New("stop the iteration") - -// ForEachFileSystemFunc is a type of function called by ForEachFilesystem -// to iterate over every filesystem in the partition table. -// -// If the function returns an error, the iteration stops. -type ForEachFileSystemFunc func(fs *Filesystem) error - -// Iterates over all filesystems in the partition table and calls the -// callback on each one. The iteration continues as long as the callback -// does not return an error. -func (pt *PartitionTable) ForEachFilesystem(cb ForEachFileSystemFunc) error { - for _, part := range pt.Partitions { - if part.Filesystem == nil { - continue - } - - if err := cb(part.Filesystem); err != nil { - if err == StopIter { - return nil - } - return err - } - } - - return nil -} - -// Returns the Filesystem instance for a given mountpoint, if it exists. -func (pt *PartitionTable) FindFilesystemForMountpoint(mountpoint string) *Filesystem { - var res *Filesystem - _ = pt.ForEachFilesystem(func(fs *Filesystem) error { - if fs.Mountpoint == mountpoint { - res = fs - return StopIter - } - - return nil - }) - - return res -} - -// Returns if the partition table contains a filesystem with the given -// mount point. -func (pt *PartitionTable) ContainsMountpoint(mountpoint string) bool { - return pt.FindFilesystemForMountpoint(mountpoint) != nil -} - -// Returns the Filesystem instance that corresponds to the root -// filesystem, i.e. the filesystem whose mountpoint is '/'. -func (pt *PartitionTable) RootFilesystem() *Filesystem { - return pt.FindFilesystemForMountpoint("/") -} - -// Returns the Filesystem instance that corresponds to the boot -// filesystem, i.e. the filesystem whose mountpoint is '/boot', -// if /boot is on a separate partition, otherwise nil -func (pt *PartitionTable) BootFilesystem() *Filesystem { - return pt.FindFilesystemForMountpoint("/boot") -} - -// Create a new filesystem within the partition table at the given mountpoint -// with the given minimum size in bytes. -func (pt *PartitionTable) CreateFilesystem(mountpoint string, size uint64) error { - filesystem := Filesystem{ - Type: "xfs", - Mountpoint: mountpoint, - FSTabOptions: "defaults", - FSTabFreq: 0, - FSTabPassNo: 0, - } - - partition := Partition{ - Size: size, - Filesystem: &filesystem, - } - - n := len(pt.Partitions) - var maxNo int - - if pt.Type == "gpt" { - partition.Type = FilesystemDataGUID - maxNo = 128 - } else { - maxNo = 4 - } - - if n == maxNo { - return fmt.Errorf("maximum number of partitions reached (%d)", maxNo) - } - - pt.Partitions = append(pt.Partitions, partition) - - return nil -} - -// Generate all needed UUIDs for all the partiton and filesystems -// -// Will not overwrite existing UUIDs and only generate UUIDs for -// partitions if the layout is GPT. -func (pt *PartitionTable) GenerateUUIDs(rng *rand.Rand) { - _ = pt.ForEachFilesystem(func(fs *Filesystem) error { - if fs.UUID == "" { - fs.UUID = uuid.Must(newRandomUUIDFromReader(rng)).String() - } - return nil - }) - - // if this is a MBR partition table, there is no need to generate - // uuids for the partitions themselves - if pt.Type != "gpt" { - return - } - - for idx, part := range pt.Partitions { - if part.UUID == "" { - pt.Partitions[idx].UUID = uuid.Must(newRandomUUIDFromReader(rng)).String() - } - } -} - -// Dynamically calculate and update the start point for each of the existing -// partitions. Adjusts the overall size of image to either the supplied -// value in `size` or to the sum of all partitions if that is lager. -// Will grow the root partition if there is any empty space. -// Returns the updated start point. -func (pt *PartitionTable) updatePartitionStartPointOffsets(size uint64) uint64 { - - // always reserve one extra sector for the GPT header - - header := pt.SectorsToBytes(1) - footer := uint64(0) - - if pt.Type == "gpt" { - - // calculate the space we need for - parts := len(pt.Partitions) - - // reserver a minimum of 128 partition entires - if parts < 128 { - parts = 128 - } - - header += uint64(parts * 128) - - footer = header - } - - start := pt.AlignUp(header) - size = pt.AlignUp(size) - - var rootIdx = -1 - for i := range pt.Partitions { - partition := &pt.Partitions[i] - if partition.Filesystem != nil && partition.Filesystem.Mountpoint == "/" { - rootIdx = i - continue - } - partition.Start = start - partition.Size = pt.AlignUp(partition.Size) - start += partition.Size - } - - root := &pt.Partitions[rootIdx] - root.Start = start - - // add the extra padding specified in the partition table - footer += pt.ExtraPadding - - // If the sum of all partitions is bigger then the specified size, - // we use that instead. Grow the partition table size if needed. - end := pt.AlignUp(root.Start + footer + root.Size) - if end > size { - size = end - } - - if size > pt.Size { - pt.Size = size - } - - // If there is space left in the partition table, grow root - root.Size = pt.Size - root.Start - - // Finally we shrink the last partition, i.e. the root partition, - // to leave space for the footer, e.g. the secondary GPT header. - root.Size -= footer - - return start -} - // Ensure the partition has at least the given size. Will do nothing // if the partition is already larger. Returns if the size changed. func (p *Partition) EnsureSize(s uint64) bool { diff --git a/internal/disk/partition_table.go b/internal/disk/partition_table.go new file mode 100644 index 000000000..9e3cc0d3c --- /dev/null +++ b/internal/disk/partition_table.go @@ -0,0 +1,371 @@ +package disk + +import ( + "errors" + "fmt" + "math/rand" + "sort" + + "github.com/google/uuid" + osbuild "github.com/osbuild/osbuild-composer/internal/osbuild1" + "github.com/osbuild/osbuild-composer/internal/osbuild2" +) + +type PartitionTable struct { + Size uint64 // Size of the disk (in bytes). + UUID string // Unique identifier of the partition table (GPT only). + Type string // Partition table type, e.g. dos, gpt. + Partitions []Partition + + SectorSize uint64 // Sector size in bytes + ExtraPadding uint64 // Extra space at the end of the partition table (sectors) +} + +// AlignUp will align the given bytes to next aligned grain if not already +// aligned +func (pt *PartitionTable) AlignUp(size uint64) uint64 { + grain := DefaultGrainBytes + if size%grain == 0 { + // already aligned: return unchanged + return size + } + return ((size + grain) / grain) * grain +} + +// Convert the given bytes to the number of sectors. +func (pt *PartitionTable) BytesToSectors(size uint64) uint64 { + sectorSize := pt.SectorSize + if sectorSize == 0 { + sectorSize = DefaultSectorSize + } + return size / sectorSize +} + +// Convert the given number of sectors to bytes. +func (pt *PartitionTable) SectorsToBytes(size uint64) uint64 { + sectorSize := pt.SectorSize + if sectorSize == 0 { + sectorSize = DefaultSectorSize + } + return size * sectorSize +} + +// Clone the partition table (deep copy). +func (pt *PartitionTable) Clone() *PartitionTable { + if pt == nil { + return nil + } + + var partitions []Partition + for _, p := range pt.Partitions { + p.Filesystem = p.Filesystem.Clone() + partitions = append(partitions, p) + } + return &PartitionTable{ + Size: pt.Size, + UUID: pt.UUID, + Type: pt.Type, + Partitions: partitions, + + SectorSize: pt.SectorSize, + ExtraPadding: pt.ExtraPadding, + } +} + +// Converts PartitionTable to osbuild.QEMUAssemblerOptions that encode +// the same partition table. +func (pt *PartitionTable) QEMUAssemblerOptions() osbuild.QEMUAssemblerOptions { + var partitions []osbuild.QEMUPartition + for _, p := range pt.Partitions { + partitions = append(partitions, p.QEMUPartition()) + } + + return osbuild.QEMUAssemblerOptions{ + Size: pt.Size, + PTUUID: pt.UUID, + PTType: pt.Type, + Partitions: partitions, + } +} + +// Generates org.osbuild.fstab stage options from this partition table. +func (pt *PartitionTable) FSTabStageOptions() *osbuild.FSTabStageOptions { + var options osbuild.FSTabStageOptions + for _, p := range pt.Partitions { + fs := p.Filesystem + if fs == nil { + continue + } + + options.AddFilesystem(fs.UUID, fs.Type, fs.Mountpoint, fs.FSTabOptions, fs.FSTabFreq, fs.FSTabPassNo) + } + + // sort the entries by PassNo to maintain backward compatibility + sort.Slice(options.FileSystems, func(i, j int) bool { + return options.FileSystems[i].PassNo < options.FileSystems[j].PassNo + }) + + return &options +} + +// Generates org.osbuild.fstab stage options from this partition table. +func (pt *PartitionTable) FSTabStageOptionsV2() *osbuild2.FSTabStageOptions { + var options osbuild2.FSTabStageOptions + for _, p := range pt.Partitions { + fs := p.Filesystem + if fs == nil { + continue + } + + options.AddFilesystem(fs.UUID, fs.Type, fs.Mountpoint, fs.FSTabOptions, fs.FSTabFreq, fs.FSTabPassNo) + } + + // sort the entries by PassNo to maintain backward compatibility + sort.Slice(options.FileSystems, func(i, j int) bool { + return options.FileSystems[i].PassNo < options.FileSystems[j].PassNo + }) + + return &options +} + +func (pt *PartitionTable) FindPartitionForMountpoint(mountpoint string) *Partition { + for idx, p := range pt.Partitions { + if p.Filesystem == nil { + continue + } + + if p.Filesystem.Mountpoint == mountpoint { + return &pt.Partitions[idx] + } + } + + return nil +} + +// Returns the root partition (the partition whose filesystem has / as +// a mountpoint) of the partition table. Nil is returned if there's no such +// partition. +func (pt *PartitionTable) RootPartition() *Partition { + return pt.FindPartitionForMountpoint("/") +} + +// Returns the /boot partition (the partition whose filesystem has /boot as +// a mountpoint) of the partition table. Nil is returned if there's no such +// partition. +func (pt *PartitionTable) BootPartition() *Partition { + return pt.FindPartitionForMountpoint("/boot") +} + +// Returns the index of the boot partition: the partition whose filesystem has +// /boot as a mountpoint. If there is no explicit boot partition, the root +// partition is returned. +// If neither boot nor root partitions are found, returns -1. +func (pt *PartitionTable) BootPartitionIndex() int { + // find partition with '/boot' mountpoint and fallback to '/' + rootIdx := -1 + for idx, part := range pt.Partitions { + if part.Filesystem == nil { + continue + } + if part.Filesystem.Mountpoint == "/boot" { + return idx + } else if part.Filesystem.Mountpoint == "/" { + rootIdx = idx + } + } + return rootIdx +} + +// StopIter is used as a return value from iterator function to indicate +// the iteration should not continue. Not an actual error and thus not +// returned by iterator function. +var StopIter = errors.New("stop the iteration") + +// ForEachFileSystemFunc is a type of function called by ForEachFilesystem +// to iterate over every filesystem in the partition table. +// +// If the function returns an error, the iteration stops. +type ForEachFileSystemFunc func(fs *Filesystem) error + +// Iterates over all filesystems in the partition table and calls the +// callback on each one. The iteration continues as long as the callback +// does not return an error. +func (pt *PartitionTable) ForEachFilesystem(cb ForEachFileSystemFunc) error { + for _, part := range pt.Partitions { + if part.Filesystem == nil { + continue + } + + if err := cb(part.Filesystem); err != nil { + if err == StopIter { + return nil + } + return err + } + } + + return nil +} + +// Returns the Filesystem instance for a given mountpoint, if it exists. +func (pt *PartitionTable) FindFilesystemForMountpoint(mountpoint string) *Filesystem { + var res *Filesystem + _ = pt.ForEachFilesystem(func(fs *Filesystem) error { + if fs.Mountpoint == mountpoint { + res = fs + return StopIter + } + + return nil + }) + + return res +} + +// Returns if the partition table contains a filesystem with the given +// mount point. +func (pt *PartitionTable) ContainsMountpoint(mountpoint string) bool { + return pt.FindFilesystemForMountpoint(mountpoint) != nil +} + +// Returns the Filesystem instance that corresponds to the root +// filesystem, i.e. the filesystem whose mountpoint is '/'. +func (pt *PartitionTable) RootFilesystem() *Filesystem { + return pt.FindFilesystemForMountpoint("/") +} + +// Returns the Filesystem instance that corresponds to the boot +// filesystem, i.e. the filesystem whose mountpoint is '/boot', +// if /boot is on a separate partition, otherwise nil +func (pt *PartitionTable) BootFilesystem() *Filesystem { + return pt.FindFilesystemForMountpoint("/boot") +} + +// Create a new filesystem within the partition table at the given mountpoint +// with the given minimum size in bytes. +func (pt *PartitionTable) CreateFilesystem(mountpoint string, size uint64) error { + filesystem := Filesystem{ + Type: "xfs", + Mountpoint: mountpoint, + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + } + + partition := Partition{ + Size: size, + Filesystem: &filesystem, + } + + n := len(pt.Partitions) + var maxNo int + + if pt.Type == "gpt" { + partition.Type = FilesystemDataGUID + maxNo = 128 + } else { + maxNo = 4 + } + + if n == maxNo { + return fmt.Errorf("maximum number of partitions reached (%d)", maxNo) + } + + pt.Partitions = append(pt.Partitions, partition) + + return nil +} + +// Generate all needed UUIDs for all the partiton and filesystems +// +// Will not overwrite existing UUIDs and only generate UUIDs for +// partitions if the layout is GPT. +func (pt *PartitionTable) GenerateUUIDs(rng *rand.Rand) { + _ = pt.ForEachFilesystem(func(fs *Filesystem) error { + if fs.UUID == "" { + fs.UUID = uuid.Must(newRandomUUIDFromReader(rng)).String() + } + return nil + }) + + // if this is a MBR partition table, there is no need to generate + // uuids for the partitions themselves + if pt.Type != "gpt" { + return + } + + for idx, part := range pt.Partitions { + if part.UUID == "" { + pt.Partitions[idx].UUID = uuid.Must(newRandomUUIDFromReader(rng)).String() + } + } +} + +// Dynamically calculate and update the start point for each of the existing +// partitions. Adjusts the overall size of image to either the supplied +// value in `size` or to the sum of all partitions if that is lager. +// Will grow the root partition if there is any empty space. +// Returns the updated start point. +func (pt *PartitionTable) updatePartitionStartPointOffsets(size uint64) uint64 { + + // always reserve one extra sector for the GPT header + + header := pt.SectorsToBytes(1) + footer := uint64(0) + + if pt.Type == "gpt" { + + // calculate the space we need for + parts := len(pt.Partitions) + + // reserver a minimum of 128 partition entires + if parts < 128 { + parts = 128 + } + + header += uint64(parts * 128) + + footer = header + } + + start := pt.AlignUp(header) + size = pt.AlignUp(size) + + var rootIdx = -1 + for i := range pt.Partitions { + partition := &pt.Partitions[i] + if partition.Filesystem != nil && partition.Filesystem.Mountpoint == "/" { + rootIdx = i + continue + } + partition.Start = start + partition.Size = pt.AlignUp(partition.Size) + start += partition.Size + } + + root := &pt.Partitions[rootIdx] + root.Start = start + + // add the extra padding specified in the partition table + footer += pt.ExtraPadding + + // If the sum of all partitions is bigger then the specified size, + // we use that instead. Grow the partition table size if needed. + end := pt.AlignUp(root.Start + footer + root.Size) + if end > size { + size = end + } + + if size > pt.Size { + pt.Size = size + } + + // If there is space left in the partition table, grow root + root.Size = pt.Size - root.Start + + // Finally we shrink the last partition, i.e. the root partition, + // to leave space for the footer, e.g. the secondary GPT header. + root.Size -= footer + + return start +}