From 26248ae9123e1c3b608e837d8e09243f661af8bd Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 27 Nov 2024 14:54:32 +0100 Subject: [PATCH 01/17] disk: new interface: FSTabEntity A new interface FSTabEntity which describes entities that can appear in the /etc/fstab file. This new interface replaces most of the Mountable interface, in which it is now embedded. The new interface will be useful to separate entries that cannot be mounted but do appear in the fstab file (namely, swap areas and swap files). All current Mountables (Filesystem and BtrfsSubvolume) implement GetFSFile() now as an alias to GetMountpoint(). --- pkg/disk/btrfs.go | 4 ++++ pkg/disk/btrfs_test.go | 1 + pkg/disk/disk.go | 16 ++++++++++++---- pkg/disk/filesystem.go | 4 ++++ pkg/disk/filesystem_test.go | 1 + 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/pkg/disk/btrfs.go b/pkg/disk/btrfs.go index 80243bd4bf..991cbc055f 100644 --- a/pkg/disk/btrfs.go +++ b/pkg/disk/btrfs.go @@ -155,6 +155,10 @@ func (bs *BtrfsSubvolume) GetMountpoint() string { return bs.Mountpoint } +func (bs *BtrfsSubvolume) GetFSFile() string { + return bs.GetMountpoint() +} + func (bs *BtrfsSubvolume) GetFSType() string { return "btrfs" } diff --git a/pkg/disk/btrfs_test.go b/pkg/disk/btrfs_test.go index 734997e814..7296c10138 100644 --- a/pkg/disk/btrfs_test.go +++ b/pkg/disk/btrfs_test.go @@ -34,4 +34,5 @@ func TestImplementsInterfacesCompileTimeCheckBtrfs(t *testing.T) { var _ = UniqueEntity(&Btrfs{}) var _ = Mountable(&BtrfsSubvolume{}) var _ = Sizeable(&BtrfsSubvolume{}) + var _ = FSTabEntity(&BtrfsSubvolume{}) } diff --git a/pkg/disk/disk.go b/pkg/disk/disk.go index fdfd027cba..31c586f309 100644 --- a/pkg/disk/disk.go +++ b/pkg/disk/disk.go @@ -248,13 +248,21 @@ type Mountable interface { // GetMountPoint returns the path of the mount point. GetMountpoint() string - // GetFSType returns the file system type, e.g. 'xfs'. - GetFSType() string + FSTabEntity +} - // GetFSSpec returns the file system spec information. +// 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 - // GetFSTabOptions returns options for mounting the entity. + // 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) } diff --git a/pkg/disk/filesystem.go b/pkg/disk/filesystem.go index c5a3bd75b8..fa4856ad19 100644 --- a/pkg/disk/filesystem.go +++ b/pkg/disk/filesystem.go @@ -55,6 +55,10 @@ func (fs *Filesystem) GetMountpoint() string { return fs.Mountpoint } +func (fs *Filesystem) GetFSFile() string { + return fs.GetMountpoint() +} + func (fs *Filesystem) GetFSType() string { if fs == nil { return "" diff --git a/pkg/disk/filesystem_test.go b/pkg/disk/filesystem_test.go index c97b811c99..a6cc67c09f 100644 --- a/pkg/disk/filesystem_test.go +++ b/pkg/disk/filesystem_test.go @@ -9,4 +9,5 @@ import ( func TestImplementsInterfacesCompileTimeCheckFilesystem(t *testing.T) { var _ = disk.Mountable(&disk.Filesystem{}) var _ = disk.UniqueEntity(&disk.Filesystem{}) + var _ = disk.FSTabEntity(&disk.Filesystem{}) } From 8f0f07baa6e9c7a708e37bb0560038eb328fc7bc Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 27 Nov 2024 15:07:25 +0100 Subject: [PATCH 02/17] disk: add ForEachFSTabEntity() function This function is the same idea as ForEachMountable() but for the new FSTabEntity interface. The new function is used in the NewFSTabStageOptions() now which is more appropriate. The distinction will be useful with the addition of swap areas that should appear in /etc/fstab (and therefore, the org.osbuild.fstab stage options) but should not be considered for the mkfs stage generation. --- pkg/disk/partition_table.go | 26 ++++++++++++++++++++++++++ pkg/osbuild/fstab_stage.go | 6 +++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pkg/disk/partition_table.go b/pkg/disk/partition_table.go index 978b34d7fb..0bb096ba54 100644 --- a/pkg/disk/partition_table.go +++ b/pkg/disk/partition_table.go @@ -590,6 +590,32 @@ func (pt *PartitionTable) ForEachMountable(cb MountableCallback) error { return forEachMountable(pt, []Entity{pt}, cb) } +type FSTabEntityCallback func(mnt FSTabEntity, path []Entity) error + +func forEachFSTabEntity(c Container, path []Entity, cb FSTabEntityCallback) error { + for idx := uint(0); idx < c.GetItemCount(); idx++ { + child := c.GetChild(idx) + childPath := append(path, child) + var err error + switch ent := child.(type) { + case FSTabEntity: + err = cb(ent, childPath) + case Container: + err = forEachFSTabEntity(ent, childPath, cb) + } + if err != nil { + return err + } + } + return nil +} + +// ForEachFSTabEntity runs the provided callback function on each FSTabEntity +// in the PartitionTable. +func (pt *PartitionTable) ForEachFSTabEntity(cb FSTabEntityCallback) error { + return forEachFSTabEntity(pt, []Entity{pt}, cb) +} + // FindMountable returns the Mountable entity with the given mountpoint in the // PartitionTable. Returns nil if no Entity has the target as a Mountpoint. func (pt *PartitionTable) FindMountable(mountpoint string) Mountable { diff --git a/pkg/osbuild/fstab_stage.go b/pkg/osbuild/fstab_stage.go index 6f807a52c6..267c069529 100644 --- a/pkg/osbuild/fstab_stage.go +++ b/pkg/osbuild/fstab_stage.go @@ -58,13 +58,13 @@ func (options *FSTabStageOptions) AddFilesystem(id string, vfsType string, path func NewFSTabStageOptions(pt *disk.PartitionTable) (*FSTabStageOptions, error) { var options FSTabStageOptions - genOption := func(mnt disk.Mountable, path []disk.Entity) error { + genOption := func(mnt disk.FSTabEntity, path []disk.Entity) error { fsSpec := mnt.GetFSSpec() fsOptions, err := mnt.GetFSTabOptions() if err != nil { return err } - options.AddFilesystem(fsSpec.UUID, mnt.GetFSType(), mnt.GetMountpoint(), fsOptions.MntOps, fsOptions.Freq, fsOptions.PassNo) + options.AddFilesystem(fsSpec.UUID, mnt.GetFSType(), mnt.GetFSFile(), fsOptions.MntOps, fsOptions.Freq, fsOptions.PassNo) return nil } @@ -72,7 +72,7 @@ func NewFSTabStageOptions(pt *disk.PartitionTable) (*FSTabStageOptions, error) { return fmt.Sprintf("%d%s", fs.PassNo, fs.Path) } - err := pt.ForEachMountable(genOption) + err := pt.ForEachFSTabEntity(genOption) if err != nil { return nil, err } From 8bbbf4aa0912c4a8f854993e1fb402126615cc5c Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 27 Nov 2024 12:16:19 +0100 Subject: [PATCH 03/17] disk: new PayloadEntity: Swap New PayloadEntity for Swap partitions. It resembles a Filesystem, but with fewer fields and fixed values for the fstab entries (fs_file, type, freq, and passno). --- pkg/disk/swap.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 pkg/disk/swap.go diff --git a/pkg/disk/swap.go b/pkg/disk/swap.go new file mode 100644 index 0000000000..42c6050636 --- /dev/null +++ b/pkg/disk/swap.go @@ -0,0 +1,77 @@ +package disk + +import ( + "math/rand" + "reflect" + + "github.com/google/uuid" +) + +// Swap defines the payload for a swap partition. It's similar to a +// [Filesystem] but with fewer fields. It is a [PayloadEntity] and also a +// [FSTabEntity]. +type Swap struct { + UUID string + Label string + + // The fourth field of fstab(5); fs_mntops + FSTabOptions string +} + +func init() { + payloadEntityMap["swap"] = reflect.TypeOf(Swap{}) +} + +func (s *Swap) EntityName() string { + return "swap" +} + +func (s *Swap) Clone() Entity { + if s == nil { + return nil + } + + return &Swap{ + UUID: s.UUID, + Label: s.Label, + FSTabOptions: s.FSTabOptions, + } +} + +// For swap, the fs_file entry in the fstab is always "none". +func (s *Swap) GetFSFile() string { + return "none" +} + +// For swap, the fs_vfstype entry in the fstab is always "swap". +func (s *Swap) GetFSType() string { + return "swap" +} + +func (s *Swap) GetFSSpec() FSSpec { + if s == nil { + return FSSpec{} + } + return FSSpec{ + UUID: s.UUID, + Label: s.Label, + } +} + +// For swap, the Freq and PassNo are always 0. +func (s *Swap) GetFSTabOptions() (FSTabOptions, error) { + if s == nil { + return FSTabOptions{}, nil + } + return FSTabOptions{ + MntOps: s.FSTabOptions, + Freq: 0, + PassNo: 0, + }, nil +} + +func (s *Swap) GenUUID(rng *rand.Rand) { + if s.UUID == "" { + s.UUID = uuid.Must(newRandomUUIDFromReader(rng)).String() + } +} From f649085f3f0bc9be45ae8901058e21e3cfeff93c Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Thu, 28 Nov 2024 14:44:04 +0100 Subject: [PATCH 04/17] disk: add Swap to partitionTableFeatures --- pkg/disk/partition_table.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/disk/partition_table.go b/pkg/disk/partition_table.go index 0bb096ba54..83478d8d0c 100644 --- a/pkg/disk/partition_table.go +++ b/pkg/disk/partition_table.go @@ -838,6 +838,7 @@ type partitionTableFeatures struct { FAT bool EXT4 bool LUKS bool + Swap bool } // features examines all of the PartitionTable entities @@ -862,6 +863,8 @@ func (pt *PartitionTable) features() partitionTableFeatures { case "ext4": ptFeatures.EXT4 = true } + case *Swap: + ptFeatures.Swap = true case *LUKSContainer: ptFeatures.LUKS = true } From e929cfc4cdba0051e4a98823e871fc8fc0c2ed16 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 27 Nov 2024 12:21:14 +0100 Subject: [PATCH 05/17] disk: add swap partition ID constants Add the SwapPartitionGUID for GPT and DosSwapID for DOS. Register both in the idMap. Refs: - https://www.freedesktop.org/wiki/Specifications/DiscoverablePartitionsSpec/ - https://tldp.org/HOWTO/Partition-Mass-Storage-Definitions-Naming-HOWTO/x190.html --- pkg/disk/disk.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/disk/disk.go b/pkg/disk/disk.go index 31c586f309..576687f06a 100644 --- a/pkg/disk/disk.go +++ b/pkg/disk/disk.go @@ -56,6 +56,8 @@ const ( RootPartitionUUID = "6264D520-3FB9-423F-8AB8-7A0A8E3D3562" + SwapPartitionGUID = "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F" + // Extended Boot Loader Partition XBootLDRPartitionGUID = "BC13C2FF-59E6-4262-A352-B275FD6F7172" @@ -70,6 +72,9 @@ const ( // Partition type ID for ESP on dos DosESPID = "ef00" + + // Partition type ID for swap + DosSwapID = "82" ) // pt type -> type -> ID mapping for convenience @@ -80,6 +85,7 @@ var idMap = map[PartitionTableType]map[string]string{ "data": DosLinuxTypeID, "esp": DosESPID, "lvm": DosLinuxTypeID, + "swap": DosSwapID, }, PT_GPT: { "bios": BIOSBootPartitionGUID, @@ -87,6 +93,7 @@ var idMap = map[PartitionTableType]map[string]string{ "data": FilesystemDataGUID, "esp": EFISystemPartitionGUID, "lvm": LVMPartitionGUID, + "swap": SwapPartitionGUID, }, } From 650e0bcaeca579e970d6e9aa6805dc7e855e44fe Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 27 Nov 2024 12:36:38 +0100 Subject: [PATCH 06/17] disk: add a test partition table with swap Add a new partition table to the TestPartitionTables that includes a swap partition. These partition tables are used in multiple tests in disk_test.go. --- pkg/disk/partition_table_internal_test.go | 61 +++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/pkg/disk/partition_table_internal_test.go b/pkg/disk/partition_table_internal_test.go index d94710f2d5..64113b09b3 100644 --- a/pkg/disk/partition_table_internal_test.go +++ b/pkg/disk/partition_table_internal_test.go @@ -67,6 +67,66 @@ var TestPartitionTables = map[string]PartitionTable{ }, }, + "plain-swap": { + UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", + Type: PT_GPT, + Partitions: []Partition{ + { + Size: 1 * MiB, + Bootable: true, + Type: BIOSBootPartitionGUID, + UUID: BIOSBootPartitionUUID, + }, + { + Size: 200 * MiB, + Type: EFISystemPartitionGUID, + UUID: EFISystemPartitionUUID, + Payload: &Filesystem{ + Type: "vfat", + UUID: EFIFilesystemUUID, + Mountpoint: "/boot/efi", + Label: "EFI-SYSTEM", + FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", + FSTabFreq: 0, + FSTabPassNo: 2, + }, + }, + { + Size: 500 * MiB, + Type: FilesystemDataGUID, + UUID: FilesystemDataUUID, + Payload: &Filesystem{ + Type: "xfs", + Mountpoint: "/boot", + Label: "boot", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + Size: 512 * MiB, + Type: SwapPartitionGUID, + Payload: &Swap{ + Label: "swap", + FSTabOptions: "defaults", + }, + }, + { + Type: FilesystemDataGUID, + UUID: RootPartitionUUID, + Payload: &Filesystem{ + Type: "xfs", + Label: "root", + Mountpoint: "/", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + }, + }, + "plain-noboot": { UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", Type: PT_GPT, @@ -306,6 +366,7 @@ func TestPartitionTableFeatures(t *testing.T) { } testCases := []testCase{ {"plain", partitionTableFeatures{XFS: true, FAT: true}}, + {"plain-swap", partitionTableFeatures{XFS: true, FAT: true, Swap: true}}, {"luks", partitionTableFeatures{XFS: true, FAT: true, LUKS: true}}, {"luks+lvm", partitionTableFeatures{XFS: true, FAT: true, LUKS: true, LVM: true}}, {"btrfs", partitionTableFeatures{XFS: true, FAT: true, Btrfs: true}}, From aa777a5575c29bceca27ab857afa8b65b92e1437 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 27 Nov 2024 13:08:21 +0100 Subject: [PATCH 07/17] disk: move TestPartitionTables to internal/testdisk Move the TestPartitionTables from the disk tests to the internal/testdisk package. This makes the test partition tables reusable across the project, but also makes it impossible to use them in internal disk tests (import cycle). The PartitionTableFeatures test is therefore moved to the disk_test package instead and the function is exported for testing in the pkg/disk/export_test.go file. --- internal/testdisk/partition.go | 385 +++++++++++++++++++++- pkg/disk/disk_test.go | 47 +-- pkg/disk/export_test.go | 6 + pkg/disk/partition_table_internal_test.go | 365 -------------------- pkg/disk/partition_table_test.go | 20 ++ 5 files changed, 420 insertions(+), 403 deletions(-) diff --git a/internal/testdisk/partition.go b/internal/testdisk/partition.go index b2a8d5d69b..7061a8fc15 100644 --- a/internal/testdisk/partition.go +++ b/internal/testdisk/partition.go @@ -5,7 +5,362 @@ import ( "github.com/osbuild/images/pkg/disk" ) -const FakePartitionSize = uint64(789) * datasizes.MiB +const ( + KiB = datasizes.KiB + MiB = datasizes.MiB + GiB = datasizes.GiB +) + +const FakePartitionSize = uint64(789) * MiB + +// TODO: Tidy up and unify TestPartitionTables with the fake partition table +// generators below (MakeFake*). Maybe use NewCustomPartitionTable() to +// generate test partition tables instead. + +var TestPartitionTables = map[string]disk.PartitionTable{ + "plain": { + UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", + Type: disk.PT_GPT, + Partitions: []disk.Partition{ + { + Size: 1 * MiB, + Bootable: true, + Type: disk.BIOSBootPartitionGUID, + UUID: disk.BIOSBootPartitionUUID, + }, + { + Size: 200 * MiB, + Type: disk.EFISystemPartitionGUID, + UUID: disk.EFISystemPartitionUUID, + Payload: &disk.Filesystem{ + Type: "vfat", + UUID: disk.EFIFilesystemUUID, + Mountpoint: "/boot/efi", + Label: "EFI-SYSTEM", + FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", + FSTabFreq: 0, + FSTabPassNo: 2, + }, + }, + { + Size: 500 * MiB, + Type: disk.FilesystemDataGUID, + UUID: disk.FilesystemDataUUID, + Payload: &disk.Filesystem{ + Type: "xfs", + Mountpoint: "/boot", + Label: "boot", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + Type: disk.FilesystemDataGUID, + UUID: disk.RootPartitionUUID, + Payload: &disk.Filesystem{ + Type: "xfs", + Label: "root", + Mountpoint: "/", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + }, + }, + + "plain-swap": { + UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", + Type: disk.PT_GPT, + Partitions: []disk.Partition{ + { + Size: 1 * MiB, + Bootable: true, + Type: disk.BIOSBootPartitionGUID, + UUID: disk.BIOSBootPartitionUUID, + }, + { + Size: 200 * MiB, + Type: disk.EFISystemPartitionGUID, + UUID: disk.EFISystemPartitionUUID, + Payload: &disk.Filesystem{ + Type: "vfat", + UUID: disk.EFIFilesystemUUID, + Mountpoint: "/boot/efi", + Label: "EFI-SYSTEM", + FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", + FSTabFreq: 0, + FSTabPassNo: 2, + }, + }, + { + Size: 500 * MiB, + Type: disk.FilesystemDataGUID, + UUID: disk.FilesystemDataUUID, + Payload: &disk.Filesystem{ + Type: "xfs", + Mountpoint: "/boot", + Label: "boot", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + Size: 512 * MiB, + Type: disk.SwapPartitionGUID, + Payload: &disk.Swap{ + Label: "swap", + FSTabOptions: "defaults", + }, + }, + { + Type: disk.FilesystemDataGUID, + UUID: disk.RootPartitionUUID, + Payload: &disk.Filesystem{ + Type: "xfs", + Label: "root", + Mountpoint: "/", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + }, + }, + + "plain-noboot": { + UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", + Type: disk.PT_GPT, + Partitions: []disk.Partition{ + { + Size: 1 * MiB, + Bootable: true, + Type: disk.BIOSBootPartitionGUID, + UUID: disk.BIOSBootPartitionUUID, + }, + { + Size: 200 * MiB, + Type: disk.EFISystemPartitionGUID, + UUID: disk.EFISystemPartitionUUID, + Payload: &disk.Filesystem{ + Type: "vfat", + UUID: disk.EFIFilesystemUUID, + Mountpoint: "/boot/efi", + Label: "EFI-SYSTEM", + FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", + FSTabFreq: 0, + FSTabPassNo: 2, + }, + }, + { + Type: disk.FilesystemDataGUID, + UUID: disk.RootPartitionUUID, + Payload: &disk.Filesystem{ + Type: "xfs", + Label: "root", + Mountpoint: "/", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + }, + }, + + "luks": { + UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", + Type: disk.PT_GPT, + Partitions: []disk.Partition{ + { + Size: 1 * MiB, + Bootable: true, + Type: disk.BIOSBootPartitionGUID, + UUID: disk.BIOSBootPartitionUUID, + }, + { + Size: 200 * MiB, + Type: disk.EFISystemPartitionGUID, + UUID: disk.EFISystemPartitionUUID, + Payload: &disk.Filesystem{ + Type: "vfat", + UUID: disk.EFIFilesystemUUID, + Mountpoint: "/boot/efi", + Label: "EFI-SYSTEM", + FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", + FSTabFreq: 0, + FSTabPassNo: 2, + }, + }, + { + Size: 500 * MiB, + Type: disk.FilesystemDataGUID, + UUID: disk.FilesystemDataUUID, + Payload: &disk.Filesystem{ + Type: "xfs", + Mountpoint: "/boot", + Label: "boot", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + Type: disk.FilesystemDataGUID, + UUID: disk.RootPartitionUUID, + Payload: &disk.LUKSContainer{ + UUID: "", + Label: "crypt_root", + Payload: &disk.Filesystem{ + Type: "xfs", + Label: "root", + Mountpoint: "/", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + }, + }, + }, + "luks+lvm": { + UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", + Type: disk.PT_GPT, + Partitions: []disk.Partition{ + { + Size: 1 * MiB, + Bootable: true, + Type: disk.BIOSBootPartitionGUID, + UUID: disk.BIOSBootPartitionUUID, + }, + { + Size: 200 * MiB, + Type: disk.EFISystemPartitionGUID, + UUID: disk.EFISystemPartitionUUID, + Payload: &disk.Filesystem{ + Type: "vfat", + UUID: disk.EFIFilesystemUUID, + Mountpoint: "/boot/efi", + Label: "EFI-SYSTEM", + FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", + FSTabFreq: 0, + FSTabPassNo: 2, + }, + }, + { + Size: 500 * MiB, + Type: disk.FilesystemDataGUID, + UUID: disk.FilesystemDataUUID, + Payload: &disk.Filesystem{ + Type: "xfs", + Mountpoint: "/boot", + Label: "boot", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + Type: disk.FilesystemDataGUID, + UUID: disk.RootPartitionUUID, + Size: 5 * GiB, + Payload: &disk.LUKSContainer{ + UUID: "", + Payload: &disk.LVMVolumeGroup{ + Name: "", + Description: "", + LogicalVolumes: []disk.LVMLogicalVolume{ + { + Size: 2 * GiB, + Payload: &disk.Filesystem{ + Type: "xfs", + Label: "root", + Mountpoint: "/", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + Size: 2 * GiB, + Payload: &disk.Filesystem{ + Type: "xfs", + Label: "root", + Mountpoint: "/home", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + }, + }, + }, + }, + }, + }, + "btrfs": { + UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", + Type: disk.PT_GPT, + Partitions: []disk.Partition{ + { + Size: 1 * MiB, + Bootable: true, + Type: disk.BIOSBootPartitionGUID, + UUID: disk.BIOSBootPartitionUUID, + }, + { + Size: 200 * MiB, + Type: disk.EFISystemPartitionGUID, + UUID: disk.EFISystemPartitionUUID, + Payload: &disk.Filesystem{ + Type: "vfat", + UUID: disk.EFIFilesystemUUID, + Mountpoint: "/boot/efi", + Label: "EFI-SYSTEM", + FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", + FSTabFreq: 0, + FSTabPassNo: 2, + }, + }, + { + Size: 500 * MiB, + Type: disk.FilesystemDataGUID, + UUID: disk.FilesystemDataUUID, + Payload: &disk.Filesystem{ + Type: "xfs", + Mountpoint: "/boot", + Label: "boot", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + Type: disk.FilesystemDataGUID, + UUID: disk.RootPartitionUUID, + Size: 10 * GiB, + Payload: &disk.Btrfs{ + UUID: "", + Label: "", + Mountpoint: "", + Subvolumes: []disk.BtrfsSubvolume{ + { + Size: 0, + Mountpoint: "/", + GroupID: 0, + }, + { + Size: 5 * GiB, + Mountpoint: "/var", + GroupID: 0, + }, + }, + }, + }, + }, + }, +} // MakeFakePartitionTable is a helper to create partition table structs // for tests. It uses sensible defaults for common scenarios. @@ -43,7 +398,7 @@ func MakeFakeBtrfsPartitionTable(mntPoints ...string) *disk.PartitionTable { var subvolumes []disk.BtrfsSubvolume pt := &disk.PartitionTable{ Type: disk.PT_GPT, - Size: 10 * datasizes.GiB, + Size: 10 * GiB, Partitions: []disk.Partition{}, } size := uint64(0) @@ -52,24 +407,24 @@ func MakeFakeBtrfsPartitionTable(mntPoints ...string) *disk.PartitionTable { case "/boot": pt.Partitions = append(pt.Partitions, disk.Partition{ Start: size, - Size: 1 * datasizes.GiB, + Size: 1 * GiB, Payload: &disk.Filesystem{ Type: "ext4", Mountpoint: mntPoint, }, }) - size += 1 * datasizes.GiB + size += 1 * GiB case "/boot/efi": pt.Partitions = append(pt.Partitions, disk.Partition{ Start: size, - Size: 100 * datasizes.MiB, + Size: 100 * MiB, Payload: &disk.Filesystem{ Type: "vfat", Mountpoint: mntPoint, UUID: disk.EFIFilesystemUUID, }, }) - size += 100 * datasizes.MiB + size += 100 * MiB default: name := mntPoint if name == "/" { @@ -89,14 +444,14 @@ func MakeFakeBtrfsPartitionTable(mntPoints ...string) *disk.PartitionTable { pt.Partitions = append(pt.Partitions, disk.Partition{ Start: size, - Size: 9 * datasizes.GiB, + Size: 9 * GiB, Payload: &disk.Btrfs{ UUID: disk.RootPartitionUUID, Subvolumes: subvolumes, }, }) - size += 9 * datasizes.GiB + size += 9 * GiB pt.Size = size return pt @@ -108,7 +463,7 @@ func MakeFakeLVMPartitionTable(mntPoints ...string) *disk.PartitionTable { var lvs []disk.LVMLogicalVolume pt := &disk.PartitionTable{ Type: disk.PT_GPT, - Size: 10 * datasizes.GiB, + Size: 10 * GiB, Partitions: []disk.Partition{}, } size := uint64(0) @@ -117,24 +472,24 @@ func MakeFakeLVMPartitionTable(mntPoints ...string) *disk.PartitionTable { case "/boot": pt.Partitions = append(pt.Partitions, disk.Partition{ Start: size, - Size: 1 * datasizes.GiB, + Size: 1 * GiB, Payload: &disk.Filesystem{ Type: "ext4", Mountpoint: mntPoint, }, }) - size += 1 * datasizes.GiB + size += 1 * GiB case "/boot/efi": pt.Partitions = append(pt.Partitions, disk.Partition{ Start: size, - Size: 100 * datasizes.MiB, + Size: 100 * MiB, Payload: &disk.Filesystem{ Type: "vfat", Mountpoint: mntPoint, UUID: disk.EFIFilesystemUUID, }, }) - size += 100 * datasizes.MiB + size += 100 * MiB default: name := "lv-for-" + mntPoint if name == "/" { @@ -155,13 +510,13 @@ func MakeFakeLVMPartitionTable(mntPoints ...string) *disk.PartitionTable { pt.Partitions = append(pt.Partitions, disk.Partition{ Start: size, - Size: 9 * datasizes.GiB, + Size: 9 * GiB, Payload: &disk.LVMVolumeGroup{ LogicalVolumes: lvs, }, }) - size += 9 * datasizes.GiB + size += 9 * GiB pt.Size = size return pt diff --git a/pkg/disk/disk_test.go b/pkg/disk/disk_test.go index 0f6d9d2787..615066bea2 100644 --- a/pkg/disk/disk_test.go +++ b/pkg/disk/disk_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/osbuild/images/internal/testdisk" "github.com/osbuild/images/pkg/blueprint" "github.com/osbuild/images/pkg/datasizes" "github.com/osbuild/images/pkg/disk" @@ -121,7 +122,7 @@ func TestForEachEntity(t *testing.T) { count := 0 - plain := disk.TestPartitionTables["plain"] + plain := testdisk.TestPartitionTables["plain"] err := plain.ForEachEntity(func(e disk.Entity, path []disk.Entity) error { assert.NotNil(t, e) assert.NotNil(t, path) @@ -185,8 +186,8 @@ func TestCreatePartitionTable(t *testing.T) { // math/rand is good enough in this case /* #nosec G404 */ rng := rand.New(rand.NewSource(13)) - for ptName := range disk.TestPartitionTables { - pt := disk.TestPartitionTables[ptName] + for ptName := range testdisk.TestPartitionTables { + pt := testdisk.TestPartitionTables[ptName] for bpName, bp := range testBlueprints { ptMode := disk.RawPartitioningMode if ptName == "luks+lvm" { @@ -214,8 +215,8 @@ func TestCreatePartitionTableLVMify(t *testing.T) { /* #nosec G404 */ rng := rand.New(rand.NewSource(13)) for bpName, tbp := range testBlueprints { - for ptName := range disk.TestPartitionTables { - pt := disk.TestPartitionTables[ptName] + for ptName := range testdisk.TestPartitionTables { + pt := testdisk.TestPartitionTables[ptName] if tbp != nil && (ptName == "btrfs" || ptName == "luks") { _, err := disk.NewPartitionTable(&pt, tbp, uint64(13*MiB), disk.AutoLVMPartitioningMode, nil, rng) @@ -252,8 +253,8 @@ func TestCreatePartitionTableBtrfsify(t *testing.T) { /* #nosec G404 */ rng := rand.New(rand.NewSource(13)) for bpName, tbp := range testBlueprints { - for ptName := range disk.TestPartitionTables { - pt := disk.TestPartitionTables[ptName] + for ptName := range testdisk.TestPartitionTables { + pt := testdisk.TestPartitionTables[ptName] if ptName == "auto-lvm" || ptName == "luks" || ptName == "luks+lvm" { _, err := disk.NewPartitionTable(&pt, tbp, uint64(13*MiB), disk.BtrfsPartitioningMode, nil, rng) @@ -290,8 +291,8 @@ func TestCreatePartitionTableLVMOnly(t *testing.T) { /* #nosec G404 */ rng := rand.New(rand.NewSource(13)) for bpName, tbp := range testBlueprints { - for ptName := range disk.TestPartitionTables { - pt := disk.TestPartitionTables[ptName] + for ptName := range testdisk.TestPartitionTables { + pt := testdisk.TestPartitionTables[ptName] if ptName == "btrfs" || ptName == "luks" { _, err := disk.NewPartitionTable(&pt, tbp, uint64(13*MiB), disk.LVMPartitioningMode, nil, rng) @@ -371,7 +372,7 @@ func TestMinimumSizes(t *testing.T) { // math/rand is good enough in this case /* #nosec G404 */ rng := rand.New(rand.NewSource(13)) - pt := disk.TestPartitionTables["plain"] + pt := testdisk.TestPartitionTables["plain"] type testCase struct { Blueprint []blueprint.FilesystemCustomization @@ -486,7 +487,7 @@ func TestLVMExtentAlignment(t *testing.T) { // math/rand is good enough in this case /* #nosec G404 */ rng := rand.New(rand.NewSource(13)) - pt := disk.TestPartitionTables["plain"] + pt := testdisk.TestPartitionTables["plain"] type testCase struct { Blueprint []blueprint.FilesystemCustomization @@ -567,7 +568,7 @@ func TestLVMExtentAlignment(t *testing.T) { } func TestNewBootWithSizeLVMify(t *testing.T) { - pt := disk.TestPartitionTables["plain-noboot"] + pt := testdisk.TestPartitionTables["plain-noboot"] assert := assert.New(t) // math/rand is good enough in this case @@ -607,8 +608,8 @@ func collectEntities(pt *disk.PartitionTable) []disk.Entity { } func TestClone(t *testing.T) { - for name := range disk.TestPartitionTables { - basePT := disk.TestPartitionTables[name] + for name := range testdisk.TestPartitionTables { + basePT := testdisk.TestPartitionTables[name] baseEntities := collectEntities(&basePT) clonePT := basePT.Clone().(*disk.PartitionTable) @@ -640,7 +641,7 @@ func TestFindDirectoryPartition(t *testing.T) { } { - pt := disk.TestPartitionTables["plain"] + pt := testdisk.TestPartitionTables["plain"] assert.Equal("/", disk.FindDirectoryEntityPath(&pt, "/opt")[0].(disk.Mountable).GetMountpoint()) assert.Equal("/boot/efi", disk.FindDirectoryEntityPath(&pt, "/boot/efi/Linux")[0].(disk.Mountable).GetMountpoint()) assert.Equal("/boot", disk.FindDirectoryEntityPath(&pt, "/boot/loader")[0].(disk.Mountable).GetMountpoint()) @@ -657,7 +658,7 @@ func TestFindDirectoryPartition(t *testing.T) { } { - pt := disk.TestPartitionTables["plain-noboot"] + pt := testdisk.TestPartitionTables["plain-noboot"] assert.Equal("/", disk.FindDirectoryEntityPath(&pt, "/opt")[0].(disk.Mountable).GetMountpoint()) assert.Equal("/", disk.FindDirectoryEntityPath(&pt, "/boot")[0].(disk.Mountable).GetMountpoint()) assert.Equal("/", disk.FindDirectoryEntityPath(&pt, "/boot/loader")[0].(disk.Mountable).GetMountpoint()) @@ -673,7 +674,7 @@ func TestFindDirectoryPartition(t *testing.T) { } { - pt := disk.TestPartitionTables["luks"] + pt := testdisk.TestPartitionTables["luks"] assert.Equal("/", disk.FindDirectoryEntityPath(&pt, "/opt")[0].(disk.Mountable).GetMountpoint()) assert.Equal("/boot", disk.FindDirectoryEntityPath(&pt, "/boot")[0].(disk.Mountable).GetMountpoint()) assert.Equal("/boot", disk.FindDirectoryEntityPath(&pt, "/boot/loader")[0].(disk.Mountable).GetMountpoint()) @@ -689,7 +690,7 @@ func TestFindDirectoryPartition(t *testing.T) { } { - pt := disk.TestPartitionTables["luks+lvm"] + pt := testdisk.TestPartitionTables["luks+lvm"] assert.Equal("/", disk.FindDirectoryEntityPath(&pt, "/opt")[0].(disk.Mountable).GetMountpoint()) assert.Equal("/boot", disk.FindDirectoryEntityPath(&pt, "/boot")[0].(disk.Mountable).GetMountpoint()) assert.Equal("/boot", disk.FindDirectoryEntityPath(&pt, "/boot/loader")[0].(disk.Mountable).GetMountpoint()) @@ -705,7 +706,7 @@ func TestFindDirectoryPartition(t *testing.T) { } { - pt := disk.TestPartitionTables["btrfs"] + pt := testdisk.TestPartitionTables["btrfs"] assert.Equal("/", disk.FindDirectoryEntityPath(&pt, "/opt")[0].(disk.Mountable).GetMountpoint()) assert.Equal("/boot", disk.FindDirectoryEntityPath(&pt, "/boot")[0].(disk.Mountable).GetMountpoint()) assert.Equal("/boot", disk.FindDirectoryEntityPath(&pt, "/boot/loader")[0].(disk.Mountable).GetMountpoint()) @@ -743,7 +744,7 @@ func TestEnsureDirectorySizes(t *testing.T) { } { - pt := disk.TestPartitionTables["plain"] + pt := testdisk.TestPartitionTables["plain"] pt = *pt.Clone().(*disk.PartitionTable) // don't modify the original test data { @@ -768,7 +769,7 @@ func TestEnsureDirectorySizes(t *testing.T) { } { - pt := disk.TestPartitionTables["luks+lvm"] + pt := testdisk.TestPartitionTables["luks+lvm"] pt = *pt.Clone().(*disk.PartitionTable) // don't modify the original test data { @@ -807,7 +808,7 @@ func TestEnsureDirectorySizes(t *testing.T) { } { - pt := disk.TestPartitionTables["btrfs"] + pt := testdisk.TestPartitionTables["btrfs"] pt = *pt.Clone().(*disk.PartitionTable) // don't modify the original test data { @@ -841,7 +842,7 @@ func TestMinimumSizesWithRequiredSizes(t *testing.T) { // math/rand is good enough in this case /* #nosec G404 */ rng := rand.New(rand.NewSource(13)) - pt := disk.TestPartitionTables["plain"] + pt := testdisk.TestPartitionTables["plain"] type testCase struct { Blueprint []blueprint.FilesystemCustomization diff --git a/pkg/disk/export_test.go b/pkg/disk/export_test.go index 329e85fe6c..609704838a 100644 --- a/pkg/disk/export_test.go +++ b/pkg/disk/export_test.go @@ -7,6 +7,12 @@ var ( AddPartitionsForBootMode = addPartitionsForBootMode ) +type PartitionTableFeatures = partitionTableFeatures + func FindDirectoryEntityPath(pt *PartitionTable, path string) []Entity { return pt.findDirectoryEntityPath(path) } + +func GetPartitionTableFeatures(pt PartitionTable) PartitionTableFeatures { + return pt.features() +} diff --git a/pkg/disk/partition_table_internal_test.go b/pkg/disk/partition_table_internal_test.go index 64113b09b3..2d0e917c0c 100644 --- a/pkg/disk/partition_table_internal_test.go +++ b/pkg/disk/partition_table_internal_test.go @@ -14,371 +14,6 @@ const ( GiB = datasizes.GiB ) -var TestPartitionTables = map[string]PartitionTable{ - "plain": { - UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", - Type: PT_GPT, - Partitions: []Partition{ - { - Size: 1 * MiB, - Bootable: true, - Type: BIOSBootPartitionGUID, - UUID: BIOSBootPartitionUUID, - }, - { - Size: 200 * MiB, - Type: EFISystemPartitionGUID, - UUID: EFISystemPartitionUUID, - Payload: &Filesystem{ - Type: "vfat", - UUID: EFIFilesystemUUID, - Mountpoint: "/boot/efi", - Label: "EFI-SYSTEM", - FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", - FSTabFreq: 0, - FSTabPassNo: 2, - }, - }, - { - Size: 500 * MiB, - Type: FilesystemDataGUID, - UUID: FilesystemDataUUID, - Payload: &Filesystem{ - Type: "xfs", - Mountpoint: "/boot", - Label: "boot", - FSTabOptions: "defaults", - FSTabFreq: 0, - FSTabPassNo: 0, - }, - }, - { - Type: FilesystemDataGUID, - UUID: RootPartitionUUID, - Payload: &Filesystem{ - Type: "xfs", - Label: "root", - Mountpoint: "/", - FSTabOptions: "defaults", - FSTabFreq: 0, - FSTabPassNo: 0, - }, - }, - }, - }, - - "plain-swap": { - UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", - Type: PT_GPT, - Partitions: []Partition{ - { - Size: 1 * MiB, - Bootable: true, - Type: BIOSBootPartitionGUID, - UUID: BIOSBootPartitionUUID, - }, - { - Size: 200 * MiB, - Type: EFISystemPartitionGUID, - UUID: EFISystemPartitionUUID, - Payload: &Filesystem{ - Type: "vfat", - UUID: EFIFilesystemUUID, - Mountpoint: "/boot/efi", - Label: "EFI-SYSTEM", - FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", - FSTabFreq: 0, - FSTabPassNo: 2, - }, - }, - { - Size: 500 * MiB, - Type: FilesystemDataGUID, - UUID: FilesystemDataUUID, - Payload: &Filesystem{ - Type: "xfs", - Mountpoint: "/boot", - Label: "boot", - FSTabOptions: "defaults", - FSTabFreq: 0, - FSTabPassNo: 0, - }, - }, - { - Size: 512 * MiB, - Type: SwapPartitionGUID, - Payload: &Swap{ - Label: "swap", - FSTabOptions: "defaults", - }, - }, - { - Type: FilesystemDataGUID, - UUID: RootPartitionUUID, - Payload: &Filesystem{ - Type: "xfs", - Label: "root", - Mountpoint: "/", - FSTabOptions: "defaults", - FSTabFreq: 0, - FSTabPassNo: 0, - }, - }, - }, - }, - - "plain-noboot": { - UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", - Type: PT_GPT, - Partitions: []Partition{ - { - Size: 1 * MiB, - Bootable: true, - Type: BIOSBootPartitionGUID, - UUID: BIOSBootPartitionUUID, - }, - { - Size: 200 * MiB, - Type: EFISystemPartitionGUID, - UUID: EFISystemPartitionUUID, - Payload: &Filesystem{ - Type: "vfat", - UUID: EFIFilesystemUUID, - Mountpoint: "/boot/efi", - Label: "EFI-SYSTEM", - FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", - FSTabFreq: 0, - FSTabPassNo: 2, - }, - }, - { - Type: FilesystemDataGUID, - UUID: RootPartitionUUID, - Payload: &Filesystem{ - Type: "xfs", - Label: "root", - Mountpoint: "/", - FSTabOptions: "defaults", - FSTabFreq: 0, - FSTabPassNo: 0, - }, - }, - }, - }, - - "luks": { - UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", - Type: PT_GPT, - Partitions: []Partition{ - { - Size: 1 * MiB, - Bootable: true, - Type: BIOSBootPartitionGUID, - UUID: BIOSBootPartitionUUID, - }, - { - Size: 200 * MiB, - Type: EFISystemPartitionGUID, - UUID: EFISystemPartitionUUID, - Payload: &Filesystem{ - Type: "vfat", - UUID: EFIFilesystemUUID, - Mountpoint: "/boot/efi", - Label: "EFI-SYSTEM", - FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", - FSTabFreq: 0, - FSTabPassNo: 2, - }, - }, - { - Size: 500 * MiB, - Type: FilesystemDataGUID, - UUID: FilesystemDataUUID, - Payload: &Filesystem{ - Type: "xfs", - Mountpoint: "/boot", - Label: "boot", - FSTabOptions: "defaults", - FSTabFreq: 0, - FSTabPassNo: 0, - }, - }, - { - Type: FilesystemDataGUID, - UUID: RootPartitionUUID, - Payload: &LUKSContainer{ - UUID: "", - Label: "crypt_root", - Payload: &Filesystem{ - Type: "xfs", - Label: "root", - Mountpoint: "/", - FSTabOptions: "defaults", - FSTabFreq: 0, - FSTabPassNo: 0, - }, - }, - }, - }, - }, - "luks+lvm": { - UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", - Type: PT_GPT, - Partitions: []Partition{ - { - Size: 1 * MiB, - Bootable: true, - Type: BIOSBootPartitionGUID, - UUID: BIOSBootPartitionUUID, - }, - { - Size: 200 * MiB, - Type: EFISystemPartitionGUID, - UUID: EFISystemPartitionUUID, - Payload: &Filesystem{ - Type: "vfat", - UUID: EFIFilesystemUUID, - Mountpoint: "/boot/efi", - Label: "EFI-SYSTEM", - FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", - FSTabFreq: 0, - FSTabPassNo: 2, - }, - }, - { - Size: 500 * MiB, - Type: FilesystemDataGUID, - UUID: FilesystemDataUUID, - Payload: &Filesystem{ - Type: "xfs", - Mountpoint: "/boot", - Label: "boot", - FSTabOptions: "defaults", - FSTabFreq: 0, - FSTabPassNo: 0, - }, - }, - { - Type: FilesystemDataGUID, - UUID: RootPartitionUUID, - Size: 5 * GiB, - Payload: &LUKSContainer{ - UUID: "", - Payload: &LVMVolumeGroup{ - Name: "", - Description: "", - LogicalVolumes: []LVMLogicalVolume{ - { - Size: 2 * GiB, - Payload: &Filesystem{ - Type: "xfs", - Label: "root", - Mountpoint: "/", - FSTabOptions: "defaults", - FSTabFreq: 0, - FSTabPassNo: 0, - }, - }, - { - Size: 2 * GiB, - Payload: &Filesystem{ - Type: "xfs", - Label: "root", - Mountpoint: "/home", - FSTabOptions: "defaults", - FSTabFreq: 0, - FSTabPassNo: 0, - }, - }, - }, - }, - }, - }, - }, - }, - "btrfs": { - UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", - Type: PT_GPT, - Partitions: []Partition{ - { - Size: 1 * MiB, - Bootable: true, - Type: BIOSBootPartitionGUID, - UUID: BIOSBootPartitionUUID, - }, - { - Size: 200 * MiB, - Type: EFISystemPartitionGUID, - UUID: EFISystemPartitionUUID, - Payload: &Filesystem{ - Type: "vfat", - UUID: EFIFilesystemUUID, - Mountpoint: "/boot/efi", - Label: "EFI-SYSTEM", - FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", - FSTabFreq: 0, - FSTabPassNo: 2, - }, - }, - { - Size: 500 * MiB, - Type: FilesystemDataGUID, - UUID: FilesystemDataUUID, - Payload: &Filesystem{ - Type: "xfs", - Mountpoint: "/boot", - Label: "boot", - FSTabOptions: "defaults", - FSTabFreq: 0, - FSTabPassNo: 0, - }, - }, - { - Type: FilesystemDataGUID, - UUID: RootPartitionUUID, - Size: 10 * GiB, - Payload: &Btrfs{ - UUID: "", - Label: "", - Mountpoint: "", - Subvolumes: []BtrfsSubvolume{ - { - Size: 0, - Mountpoint: "/", - GroupID: 0, - }, - { - Size: 5 * GiB, - Mountpoint: "/var", - GroupID: 0, - }, - }, - }, - }, - }, - }, -} - -func TestPartitionTableFeatures(t *testing.T) { - type testCase struct { - partitionType string - expectedFeatures partitionTableFeatures - } - testCases := []testCase{ - {"plain", partitionTableFeatures{XFS: true, FAT: true}}, - {"plain-swap", partitionTableFeatures{XFS: true, FAT: true, Swap: true}}, - {"luks", partitionTableFeatures{XFS: true, FAT: true, LUKS: true}}, - {"luks+lvm", partitionTableFeatures{XFS: true, FAT: true, LUKS: true, LVM: true}}, - {"btrfs", partitionTableFeatures{XFS: true, FAT: true, Btrfs: true}}, - } - - for _, tc := range testCases { - pt := TestPartitionTables[tc.partitionType] - assert.Equal(t, tc.expectedFeatures, pt.features()) - - } -} - // validatePTSize checks that each Partition is large enough to contain every // sizeable under it. func validatePTSize(pt *PartitionTable) error { diff --git a/pkg/disk/partition_table_test.go b/pkg/disk/partition_table_test.go index 2344ccbd17..193baba0e9 100644 --- a/pkg/disk/partition_table_test.go +++ b/pkg/disk/partition_table_test.go @@ -2130,3 +2130,23 @@ func TestNewCustomPartitionTableErrors(t *testing.T) { }) } } + +func TestPartitionTableFeatures(t *testing.T) { + type testCase struct { + partitionType string + expectedFeatures disk.PartitionTableFeatures + } + testCases := []testCase{ + {"plain", disk.PartitionTableFeatures{XFS: true, FAT: true}}, + {"plain-swap", disk.PartitionTableFeatures{XFS: true, FAT: true, Swap: true}}, + {"luks", disk.PartitionTableFeatures{XFS: true, FAT: true, LUKS: true}}, + {"luks+lvm", disk.PartitionTableFeatures{XFS: true, FAT: true, LUKS: true, LVM: true}}, + {"btrfs", disk.PartitionTableFeatures{XFS: true, FAT: true, Btrfs: true}}, + } + + for _, tc := range testCases { + pt := testdisk.TestPartitionTables[tc.partitionType] + assert.Equal(t, tc.expectedFeatures, disk.GetPartitionTableFeatures(pt)) + + } +} From 96dfc585590a23390fbdf0cde31590dfc6afd1be Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 27 Nov 2024 13:56:00 +0100 Subject: [PATCH 08/17] osbuild: test NewFSTabStageOptions function Test the stage option generator for the org.osbuild.fstab stage using the TestPartitionTables. Note that the btrfs subvolumes were missing a name, making the stage option generation fail, so this is also fixed. --- internal/testdisk/partition.go | 2 + pkg/osbuild/fstab_stage_test.go | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/internal/testdisk/partition.go b/internal/testdisk/partition.go index 7061a8fc15..7daf2cd894 100644 --- a/internal/testdisk/partition.go +++ b/internal/testdisk/partition.go @@ -346,11 +346,13 @@ var TestPartitionTables = map[string]disk.PartitionTable{ Mountpoint: "", Subvolumes: []disk.BtrfsSubvolume{ { + Name: "root", Size: 0, Mountpoint: "/", GroupID: 0, }, { + Name: "var", Size: 5 * GiB, Mountpoint: "/var", GroupID: 0, diff --git a/pkg/osbuild/fstab_stage_test.go b/pkg/osbuild/fstab_stage_test.go index e8b6fc223c..cd12f28246 100644 --- a/pkg/osbuild/fstab_stage_test.go +++ b/pkg/osbuild/fstab_stage_test.go @@ -1,9 +1,12 @@ package osbuild import ( + "math/rand" "testing" + "github.com/osbuild/images/internal/testdisk" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewFSTabStage(t *testing.T) { @@ -50,3 +53,77 @@ func TestAddFilesystem(t *testing.T) { } assert.Equal(t, len(filesystems), len(options.FileSystems)) } + +func TestNewFSTabStageOptions(t *testing.T) { + expectedOptions := map[string]FSTabStageOptions{ + // The names must match the ones in testdisk.TestPartitionTables + "plain": { + FileSystems: []*FSTabEntry{ + {UUID: "6e4ff95f-f662-45ee-a82a-bdf44a2d0b75", VFSType: "xfs", Path: "/", Options: "defaults", Freq: 0, PassNo: 0}, + {UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", VFSType: "xfs", Path: "/boot", Options: "defaults", Freq: 0, PassNo: 0}, + {UUID: "7B77-95E7", VFSType: "vfat", Path: "/boot/efi", Options: "defaults,uid=0,gid=0,umask=077,shortname=winnt", Freq: 0, PassNo: 2}, + }, + }, + "plain-swap": { + FileSystems: []*FSTabEntry{ + {UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", VFSType: "xfs", Path: "/", Options: "defaults", Freq: 0, PassNo: 0}, + {UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", VFSType: "xfs", Path: "/boot", Options: "defaults", Freq: 0, PassNo: 0}, + {UUID: "6e4ff95f-f662-45ee-a82a-bdf44a2d0b75", VFSType: "swap", Path: "none", Options: "defaults", Freq: 0, PassNo: 0}, + {UUID: "7B77-95E7", VFSType: "vfat", Path: "/boot/efi", Options: "defaults,uid=0,gid=0,umask=077,shortname=winnt", Freq: 0, PassNo: 2}, + }, + }, + "plain-noboot": { + FileSystems: []*FSTabEntry{ + {UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", VFSType: "xfs", Path: "/", Options: "defaults", Freq: 0, PassNo: 0}, + {UUID: "7B77-95E7", VFSType: "vfat", Path: "/boot/efi", Options: "defaults,uid=0,gid=0,umask=077,shortname=winnt", Freq: 0, PassNo: 2}, + }, + }, + "luks": { + FileSystems: []*FSTabEntry{ + {UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", VFSType: "xfs", Path: "/", Options: "defaults", Freq: 0, PassNo: 0}, + {UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", VFSType: "xfs", Path: "/boot", Options: "defaults", Freq: 0, PassNo: 0}, + {UUID: "7B77-95E7", VFSType: "vfat", Path: "/boot/efi", Options: "defaults,uid=0,gid=0,umask=077,shortname=winnt", Freq: 0, PassNo: 2}, + }, + }, + "luks+lvm": { + FileSystems: []*FSTabEntry{ + {UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", VFSType: "xfs", Path: "/", Options: "defaults", Freq: 0, PassNo: 0}, + {UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", VFSType: "xfs", Path: "/boot", Options: "defaults", Freq: 0, PassNo: 0}, + {UUID: "a178892e-e285-4ce1-9114-55780875d64e", VFSType: "xfs", Path: "/home", Options: "defaults", Freq: 0, PassNo: 0}, + {UUID: "7B77-95E7", VFSType: "vfat", Path: "/boot/efi", Options: "defaults,uid=0,gid=0,umask=077,shortname=winnt", Freq: 0, PassNo: 2}, + }, + }, + "btrfs": { + FileSystems: []*FSTabEntry{ + {UUID: "6e4ff95f-f662-45ee-a82a-bdf44a2d0b75", VFSType: "btrfs", Path: "/", Options: "subvol=root", Freq: 0, PassNo: 0}, + {UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", VFSType: "xfs", Path: "/boot", Options: "defaults", Freq: 0, PassNo: 0}, + {UUID: "6e4ff95f-f662-45ee-a82a-bdf44a2d0b75", VFSType: "btrfs", Path: "/var", Options: "subvol=var", Freq: 0, PassNo: 0}, + {UUID: "7B77-95E7", VFSType: "vfat", Path: "/boot/efi", Options: "defaults,uid=0,gid=0,umask=077,shortname=winnt", Freq: 0, PassNo: 2}, + }, + }, + } + // Use the test partition tables from the disk package. + for name := range testdisk.TestPartitionTables { + t.Run(name, func(t *testing.T) { + require := require.New(t) + pt := testdisk.TestPartitionTables[name] + + // math/rand is good enough in this case + /* #nosec G404 */ + rng := rand.New(rand.NewSource(0)) + // populate UUIDs + pt.GenerateUUIDs(rng) + + // print an informative failure message if a new test partition + // table is added and this test is not updated (instead of failing + // at the final Equal() check) + exp, ok := expectedOptions[name] + require.True(ok, "expected options not defined for test partition table %q: please update the TestNewFSTabStageOptions test", name) + + options, err := NewFSTabStageOptions(&pt) + require.NoError(err) + require.NotNil(options) + require.Equal(exp, *options) + }) + } +} From 882d135b89b8901ca227043b7ae3d8348fd1210e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Budai?= Date: Fri, 30 Aug 2024 16:01:20 +0200 Subject: [PATCH 09/17] osbuild: add org.osbuild.mkswap stage --- pkg/osbuild/mkswap_stage.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pkg/osbuild/mkswap_stage.go diff --git a/pkg/osbuild/mkswap_stage.go b/pkg/osbuild/mkswap_stage.go new file mode 100644 index 0000000000..d4cfdabd64 --- /dev/null +++ b/pkg/osbuild/mkswap_stage.go @@ -0,0 +1,16 @@ +package osbuild + +type MkswapStageOptions struct { + UUID string `json:"uuid"` + Label string `json:"label,omitempty"` +} + +func (MkswapStageOptions) isStageOptions() {} + +func NewMkswapStage(options *MkswapStageOptions, devices map[string]Device) *Stage { + return &Stage{ + Type: "org.osbuild.mkswap", + Options: options, + Devices: devices, + } +} From a0d222c90521c55ebd380339984175b9628e624c Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 27 Nov 2024 14:31:21 +0100 Subject: [PATCH 10/17] osbuild: rewrite GenMkfsStages() to GenFsStages() and handle swap Replace the GenMkfsStages() function with a more general GenFsStages(). This new function iterates through all entities, not just mountables, and handles, in addition to filesystem creation (org.osbuild.mkfs.*), btrfs subvolume creation (org.osbuild.btrfs.subvol) and swap (org.osbuild.mkswap). This removes the need for calling the separate GenBtrfsSubVolStage() when generating image pipelines. This change has a minor effect on the org.osbuild.btrfs.subvol stage options in manifests: it changes the name of the device (which is not a functional change) and locks the loopback device (which is not necessary but shouldn't cause issues). Tests have been updated accordingly. --- internal/testdisk/partition.go | 74 +++++++++-- pkg/manifest/anaconda_installer_iso_tree.go | 2 +- pkg/manifest/coi_iso_tree.go | 2 +- pkg/osbuild/btrfs_subvol_stage.go | 53 -------- pkg/osbuild/device.go | 2 + pkg/osbuild/disk.go | 10 +- pkg/osbuild/disk_test.go | 8 +- pkg/osbuild/mkfs_stage.go | 137 +++++++++++--------- pkg/osbuild/mkfs_stages_test.go | 47 ++++++- 9 files changed, 192 insertions(+), 143 deletions(-) diff --git a/internal/testdisk/partition.go b/internal/testdisk/partition.go index 7daf2cd894..fd608e3ead 100644 --- a/internal/testdisk/partition.go +++ b/internal/testdisk/partition.go @@ -1,6 +1,8 @@ package testdisk import ( + "math/rand" + "github.com/osbuild/images/pkg/datasizes" "github.com/osbuild/images/pkg/disk" ) @@ -366,21 +368,40 @@ var TestPartitionTables = map[string]disk.PartitionTable{ // MakeFakePartitionTable is a helper to create partition table structs // for tests. It uses sensible defaults for common scenarios. +// Including a "swap" entry creates a swap partition. func MakeFakePartitionTable(mntPoints ...string) *disk.PartitionTable { + // math/rand is good enough in this case + /* #nosec G404 */ + rng := rand.New(rand.NewSource(0)) + var partitions []disk.Partition for _, mntPoint := range mntPoints { - payload := &disk.Filesystem{ - Type: "ext4", - Mountpoint: mntPoint, - } + var payload disk.PayloadEntity switch mntPoint { case "/": - payload.UUID = disk.RootPartitionUUID + payload = &disk.Filesystem{ + Type: "ext4", + Mountpoint: mntPoint, + UUID: disk.RootPartitionUUID, + } case "/boot/efi": - payload.UUID = disk.EFIFilesystemUUID - payload.Type = "vfat" + payload = &disk.Filesystem{ + Type: "vfat", + Mountpoint: mntPoint, + UUID: disk.EFIFilesystemUUID, + } + case "swap": + swap := &disk.Swap{ + Label: "swap", + } + swap.GenUUID(rng) + payload = swap default: - payload.UUID = disk.FilesystemDataUUID + payload = &disk.Filesystem{ + Type: "ext4", + Mountpoint: mntPoint, + UUID: disk.FilesystemDataUUID, + } } partitions = append(partitions, disk.Partition{ Size: FakePartitionSize, @@ -396,7 +417,12 @@ func MakeFakePartitionTable(mntPoints ...string) *disk.PartitionTable { // MakeFakeBtrfsPartitionTable is similar to MakeFakePartitionTable but // creates a btrfs-based partition table. +// Including a "swap" entry creates a swap partition. func MakeFakeBtrfsPartitionTable(mntPoints ...string) *disk.PartitionTable { + // math/rand is good enough in this case + /* #nosec G404 */ + rng := rand.New(rand.NewSource(0)) + var subvolumes []disk.BtrfsSubvolume pt := &disk.PartitionTable{ Type: disk.PT_GPT, @@ -427,17 +453,30 @@ func MakeFakeBtrfsPartitionTable(mntPoints ...string) *disk.PartitionTable { }, }) size += 100 * MiB + case "swap": + swap := &disk.Swap{ + Label: "swap", + } + swap.GenUUID(rng) + pt.Partitions = append(pt.Partitions, disk.Partition{ + Start: size, + Size: 512 * MiB, + Payload: swap, + }) + size += 512 * MiB default: name := mntPoint + uuid := "" if name == "/" { name = "root" + uuid = disk.RootPartitionUUID } subvolumes = append( subvolumes, disk.BtrfsSubvolume{ Mountpoint: mntPoint, Name: name, - UUID: disk.RootPartitionUUID, + UUID: uuid, Compress: disk.DefaultBtrfsCompression, }, ) @@ -461,7 +500,12 @@ func MakeFakeBtrfsPartitionTable(mntPoints ...string) *disk.PartitionTable { // MakeFakeLVMPartitionTable is similar to MakeFakePartitionTable but // creates a lvm-based partition table. +// Including a "swap" entry creates a swap logical volume. func MakeFakeLVMPartitionTable(mntPoints ...string) *disk.PartitionTable { + // math/rand is good enough in this case + /* #nosec G404 */ + rng := rand.New(rand.NewSource(0)) + var lvs []disk.LVMLogicalVolume pt := &disk.PartitionTable{ Type: disk.PT_GPT, @@ -492,6 +536,18 @@ func MakeFakeLVMPartitionTable(mntPoints ...string) *disk.PartitionTable { }, }) size += 100 * MiB + case "swap": + swap := &disk.Swap{ + Label: "swap", + } + swap.GenUUID(rng) + lvs = append( + lvs, + disk.LVMLogicalVolume{ + Name: "lv-for-swap", + Payload: swap, + }, + ) default: name := "lv-for-" + mntPoint if name == "/" { diff --git a/pkg/manifest/anaconda_installer_iso_tree.go b/pkg/manifest/anaconda_installer_iso_tree.go index 6978f78361..65d6fa2bff 100644 --- a/pkg/manifest/anaconda_installer_iso_tree.go +++ b/pkg/manifest/anaconda_installer_iso_tree.go @@ -303,7 +303,7 @@ func (p *AnacondaInstallerISOTree) serialize() osbuild.Pipeline { Size: fmt.Sprintf("%d", p.PartitionTable.Size), })) - for _, stage := range osbuild.GenMkfsStages(p.PartitionTable, filename) { + for _, stage := range osbuild.GenFsStages(p.PartitionTable, filename) { pipeline.AddStage(stage) } diff --git a/pkg/manifest/coi_iso_tree.go b/pkg/manifest/coi_iso_tree.go index a00dbe297a..3fbf1155c5 100644 --- a/pkg/manifest/coi_iso_tree.go +++ b/pkg/manifest/coi_iso_tree.go @@ -111,7 +111,7 @@ func (p *CoreOSISOTree) serialize() osbuild.Pipeline { Size: fmt.Sprintf("%d", p.PartitionTable.Size), })) - for _, stage := range osbuild.GenMkfsStages(p.PartitionTable, filename) { + for _, stage := range osbuild.GenFsStages(p.PartitionTable, filename) { pipeline.AddStage(stage) } diff --git a/pkg/osbuild/btrfs_subvol_stage.go b/pkg/osbuild/btrfs_subvol_stage.go index f7deca5f58..678215f3f1 100644 --- a/pkg/osbuild/btrfs_subvol_stage.go +++ b/pkg/osbuild/btrfs_subvol_stage.go @@ -1,9 +1,5 @@ package osbuild -import ( - "github.com/osbuild/images/pkg/disk" -) - type BtrfsSubVolOptions struct { Subvolumes []BtrfsSubVol `json:"subvolumes"` } @@ -22,52 +18,3 @@ func NewBtrfsSubVol(options *BtrfsSubVolOptions, devices *map[string]Device, mou Mounts: *mounts, } } - -func GenBtrfsSubVolStage(filename string, pt *disk.PartitionTable) *Stage { - var subvolumes []BtrfsSubVol - - genStage := func(mnt disk.Mountable, path []disk.Entity) error { - if mnt.GetFSType() != "btrfs" { - return nil - } - - btrfs := mnt.(*disk.BtrfsSubvolume) - subvolumes = append(subvolumes, BtrfsSubVol{Name: "/" + btrfs.Name}) - - return nil - } - - _ = pt.ForEachMountable(genStage) - - if len(subvolumes) == 0 { - return nil - } - - devices, mounts := genBtrfsMountDevices(filename, pt) - - return NewBtrfsSubVol(&BtrfsSubVolOptions{subvolumes}, devices, mounts) -} - -func genBtrfsMountDevices(filename string, pt *disk.PartitionTable) (*map[string]Device, *[]Mount) { - devices := make(map[string]Device, len(pt.Partitions)) - mounts := make([]Mount, 0, len(pt.Partitions)) - genMounts := func(ent disk.Entity, path []disk.Entity) error { - if _, isBtrfs := ent.(*disk.Btrfs); !isBtrfs { - return nil - } - - stageDevices, name := getDevices(path, filename, false) - - mounts = append(mounts, *NewBtrfsMount(name, name, "/", "", "")) - - // update devices map with new elements from stageDevices - for devName := range stageDevices { - devices[devName] = stageDevices[devName] - } - return nil - } - - _ = pt.ForEachEntity(genMounts) - - return &devices, &mounts -} diff --git a/pkg/osbuild/device.go b/pkg/osbuild/device.go index 969bf51d11..8dbdb7729c 100644 --- a/pkg/osbuild/device.go +++ b/pkg/osbuild/device.go @@ -162,6 +162,8 @@ func deviceName(p disk.Entity) string { return payload.Name case *disk.Btrfs: return "btrfs-" + payload.UUID[:4] + case *disk.Swap: + return "swap-" + payload.UUID[:4] } panic(fmt.Sprintf("unsupported device type in deviceName: '%T'", p)) } diff --git a/pkg/osbuild/disk.go b/pkg/osbuild/disk.go index df000ecff6..30465d9c0b 100644 --- a/pkg/osbuild/disk.go +++ b/pkg/osbuild/disk.go @@ -99,15 +99,11 @@ func GenImagePrepareStages(pt *disk.PartitionTable, filename string, partTool Pa s := GenDeviceCreationStages(pt, filename) stages = append(stages, s...) - // Generate all the filesystems on partitons and devices - s = GenMkfsStages(pt, filename) + // Generate all the filesystems, subvolumes, and swap areas on partitons + // and devices + s = GenFsStages(pt, filename) stages = append(stages, s...) - subvolStage := GenBtrfsSubVolStage(filename, pt) - if subvolStage != nil { - stages = append(stages, subvolStage) - } - return stages } diff --git a/pkg/osbuild/disk_test.go b/pkg/osbuild/disk_test.go index 561e475158..3df666e424 100644 --- a/pkg/osbuild/disk_test.go +++ b/pkg/osbuild/disk_test.go @@ -126,21 +126,21 @@ func TestGenImagePrepareStages(t *testing.T) { { Type: "org.osbuild.btrfs.subvol", Devices: map[string]Device{ - "btrfs-6264": { + "device": { Type: "org.osbuild.loopback", Options: &LoopbackDeviceOptions{ Filename: filename, Start: 1 * datasizes.GiB / 512, Size: 9 * datasizes.GiB / 512, - Lock: false, + Lock: true, }, }, }, Mounts: []Mount{ { - Name: "btrfs-6264", + Name: "volume", Type: "org.osbuild.btrfs", - Source: "btrfs-6264", + Source: "device", Target: "/", Options: BtrfsMountOptions{}, }, diff --git a/pkg/osbuild/mkfs_stage.go b/pkg/osbuild/mkfs_stage.go index b942406ddd..33558a66b0 100644 --- a/pkg/osbuild/mkfs_stage.go +++ b/pkg/osbuild/mkfs_stage.go @@ -7,81 +7,96 @@ import ( "github.com/osbuild/images/pkg/disk" ) -// GenMkfsStages generates a list of org.mkfs.* stages based on a -// partition table description for a single device node -// filename is the path to the underlying image file (to be used as a source for the loopback device) -func GenMkfsStages(pt *disk.PartitionTable, filename string) []*Stage { +// GenFsStages generates a list of stages that create the filesystem and other +// related entities. Specifically, it creates stages for: +// - org.osbuild.mkfs.*: for all filesystems and btrfs volumes +// - org.osbuild.btrfs.subvol: for all btrfs subvolumes +// - org.osbuild.mkswap: for swap areas +func GenFsStages(pt *disk.PartitionTable, filename string) []*Stage { stages := make([]*Stage, 0, len(pt.Partitions)) - processedBtrfsPartitions := make(map[string]bool) - genStage := func(mnt disk.Mountable, path []disk.Entity) error { - t := mnt.GetFSType() - var stage *Stage + genStage := func(ent disk.Entity, path []disk.Entity) error { + switch e := ent.(type) { + case *disk.Filesystem: + // TODO: extract last device renaming into helper + stageDevices, lastName := getDevices(path, filename, true) - stageDevices, lastName := getDevices(path, filename, true) + // The last device in the chain must be named "device", because that's + // the device that mkfs stages run on. See the stage schemas for + // reference. + lastDevice := stageDevices[lastName] + delete(stageDevices, lastName) + stageDevices["device"] = lastDevice - // The last device in the chain must be named "device", because that's the device that mkfs stages run on. - // See their schema for reference. - lastDevice := stageDevices[lastName] - delete(stageDevices, lastName) - stageDevices["device"] = lastDevice - - fsSpec := mnt.GetFSSpec() - switch t { - case "xfs": - options := &MkfsXfsStageOptions{ - UUID: fsSpec.UUID, - Label: fsSpec.Label, - } - stage = NewMkfsXfsStage(options, stageDevices) - case "vfat": - options := &MkfsFATStageOptions{ - VolID: strings.Replace(fsSpec.UUID, "-", "", -1), - } - stage = NewMkfsFATStage(options, stageDevices) - case "btrfs": - // the disk library allows only subvolumes as Mountable, so we need to find the underlying btrfs partition - // and mkfs it - btrfsPart := findBtrfsPartition(path) - if btrfsPart == nil { - panic(fmt.Sprintf("found btrfs subvolume without btrfs partition: %s", mnt.GetMountpoint())) + switch e.GetFSType() { + case "xfs": + options := &MkfsXfsStageOptions{ + UUID: e.UUID, + Label: e.Label, + } + stages = append(stages, NewMkfsXfsStage(options, stageDevices)) + case "vfat": + options := &MkfsFATStageOptions{ + VolID: strings.Replace(e.UUID, "-", "", -1), + } + stages = append(stages, NewMkfsFATStage(options, stageDevices)) + case "ext4": + options := &MkfsExt4StageOptions{ + UUID: e.UUID, + Label: e.Label, + } + stages = append(stages, NewMkfsExt4Stage(options, stageDevices)) + default: + panic(fmt.Sprintf("unknown fs type: %s", e.GetFSType())) } + case *disk.Btrfs: + stageDevices, lastName := getDevices(path, filename, true) - // btrfs partitions can be shared between multiple subvolumes, so we need to make sure we only create - // one - if processedBtrfsPartitions[btrfsPart.UUID] { - return nil - } - processedBtrfsPartitions[btrfsPart.UUID] = true + // The last device in the chain must be named "device", because that's + // the device that mkfs stages run on. See the stage schemas for + // reference. + lastDevice := stageDevices[lastName] + delete(stageDevices, lastName) + stageDevices["device"] = lastDevice options := &MkfsBtrfsStageOptions{ - UUID: btrfsPart.UUID, - Label: btrfsPart.Label, + UUID: e.UUID, + Label: e.Label, } - stage = NewMkfsBtrfsStage(options, stageDevices) - case "ext4": - options := &MkfsExt4StageOptions{ - UUID: fsSpec.UUID, - Label: fsSpec.Label, + stages = append(stages, NewMkfsBtrfsStage(options, stageDevices)) + // Handle subvolumes here directly instead of collecting them in + // their own case, since we already have access to the parent volume. + subvolumes := make([]BtrfsSubVol, len(e.Subvolumes)) + for idx, subvol := range e.Subvolumes { + subvolumes[idx] = BtrfsSubVol{Name: "/" + strings.TrimLeft(subvol.Name, "/")} } - stage = NewMkfsExt4Stage(options, stageDevices) - default: - panic("unknown fs type " + t) - } - stages = append(stages, stage) + // Subvolume creation does not require locking the device, nor does + // it require the renaming to "device", but let's reuse the volume + // device for convenience + mount := *NewBtrfsMount("volume", "device", "/", "", "") + stages = append(stages, NewBtrfsSubVol(&BtrfsSubVolOptions{subvolumes}, &stageDevices, &[]Mount{mount})) + case *disk.Swap: + // TODO: extract last device renaming into helper + stageDevices, lastName := getDevices(path, filename, true) + + // The last device in the chain must be named "device", because that's + // the device that the mkswap stage runs on. See the stage schema + // for reference. + lastDevice := stageDevices[lastName] + delete(stageDevices, lastName) + stageDevices["device"] = lastDevice + + options := &MkswapStageOptions{ + UUID: e.UUID, + Label: e.Label, + } + stages = append(stages, NewMkswapStage(options, stageDevices)) + } return nil } - _ = pt.ForEachMountable(genStage) // genStage always returns nil + _ = pt.ForEachEntity(genStage) // genStage always returns nil return stages -} -func findBtrfsPartition(path []disk.Entity) *disk.Btrfs { - for _, e := range path { - if btrfsPartition, ok := e.(*disk.Btrfs); ok { - return btrfsPartition - } - } - return nil } diff --git a/pkg/osbuild/mkfs_stages_test.go b/pkg/osbuild/mkfs_stages_test.go index 556d8103e5..dd7e7be0df 100644 --- a/pkg/osbuild/mkfs_stages_test.go +++ b/pkg/osbuild/mkfs_stages_test.go @@ -76,9 +76,9 @@ func TestNewMkfsStage(t *testing.T) { assert.Equal(t, mkxfsExpected, mkxfs) } -func TestGenMkfsStages(t *testing.T) { +func TestGenFsStages(t *testing.T) { pt := testdisk.MakeFakePartitionTable("/", "/boot", "/boot/efi") - stages := GenMkfsStages(pt, "file.img") + stages := GenFsStages(pt, "file.img") assert.Equal(t, []*Stage{ { Type: "org.osbuild.mkfs.ext4", @@ -131,10 +131,10 @@ func TestGenMkfsStages(t *testing.T) { }, stages) } -func TestGenMkfsStagesBtrfs(t *testing.T) { +func TestGenFsStagesBtrfs(t *testing.T) { // Let's put there /extra to make sure that / and /extra creates only one btrfs partition pt := testdisk.MakeFakeBtrfsPartitionTable("/", "/boot", "/boot/efi", "/extra") - stages := GenMkfsStages(pt, "file.img") + stages := GenFsStages(pt, "file.img") assert.Equal(t, []*Stage{ { Type: "org.osbuild.mkfs.ext4", @@ -184,10 +184,43 @@ func TestGenMkfsStagesBtrfs(t *testing.T) { }, }, }, + { + Type: "org.osbuild.btrfs.subvol", + Options: &BtrfsSubVolOptions{ + Subvolumes: []BtrfsSubVol{ + { + Name: "/root", + }, + { + Name: "/extra", + }, + }, + }, + Devices: map[string]Device{ + "device": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Start: (datasizes.GiB + 100*datasizes.MiB) / disk.DefaultSectorSize, + Size: 9 * datasizes.GiB / disk.DefaultSectorSize, + Lock: true, + }, + }, + }, + Mounts: []Mount{ + { + Name: "volume", + Type: "org.osbuild.btrfs", + Source: "device", + Target: "/", + Options: BtrfsMountOptions{}, + }, + }, + }, }, stages) } -func TestGenMkfsStagesUnhappy(t *testing.T) { +func TestGenFsStagesUnhappy(t *testing.T) { pt := &disk.PartitionTable{ Type: disk.PT_GPT, Partitions: []disk.Partition{ @@ -199,7 +232,7 @@ func TestGenMkfsStagesUnhappy(t *testing.T) { }, } - assert.PanicsWithValue(t, "unknown fs type ext2", func() { - GenMkfsStages(pt, "file.img") + assert.PanicsWithValue(t, "unknown fs type: ext2", func() { + GenFsStages(pt, "file.img") }) } From 74ef4169f1f8827f04b1a70e181efbfc260149f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Budai?= Date: Fri, 30 Aug 2024 16:01:20 +0200 Subject: [PATCH 11/17] osbuild: add LVM test for GenFsStages --- internal/testdisk/partition.go | 1 + pkg/osbuild/mkfs_stages_test.go | 82 +++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/internal/testdisk/partition.go b/internal/testdisk/partition.go index fd608e3ead..827583b58f 100644 --- a/internal/testdisk/partition.go +++ b/internal/testdisk/partition.go @@ -570,6 +570,7 @@ func MakeFakeLVMPartitionTable(mntPoints ...string) *disk.PartitionTable { Start: size, Size: 9 * GiB, Payload: &disk.LVMVolumeGroup{ + Name: "rootvg", LogicalVolumes: lvs, }, }) diff --git a/pkg/osbuild/mkfs_stages_test.go b/pkg/osbuild/mkfs_stages_test.go index dd7e7be0df..9811c36d1f 100644 --- a/pkg/osbuild/mkfs_stages_test.go +++ b/pkg/osbuild/mkfs_stages_test.go @@ -220,6 +220,88 @@ func TestGenFsStagesBtrfs(t *testing.T) { }, stages) } +func TestGenFsStagesLVM(t *testing.T) { + pt := testdisk.MakeFakeLVMPartitionTable("/", "/boot", "/boot/efi", "/home") + stages := GenFsStages(pt, "file.img") + assert.Equal(t, []*Stage{ + { + Type: "org.osbuild.mkfs.ext4", + Options: &MkfsExt4StageOptions{}, + Devices: map[string]Device{ + "device": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Size: datasizes.GiB / disk.DefaultSectorSize, + Lock: true, + }, + }, + }, + }, + { + Type: "org.osbuild.mkfs.fat", + Options: &MkfsFATStageOptions{ + VolID: strings.ReplaceAll(disk.EFIFilesystemUUID, "-", ""), + }, + Devices: map[string]Device{ + "device": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Start: datasizes.GiB / disk.DefaultSectorSize, + Size: 100 * datasizes.MiB / disk.DefaultSectorSize, + Lock: true, + }, + }, + }, + }, + { + Type: "org.osbuild.mkfs.xfs", + Options: &MkfsXfsStageOptions{}, + Devices: map[string]Device{ + "rootvg": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Start: (datasizes.GiB + 100*datasizes.MiB) / disk.DefaultSectorSize, + Size: 9 * datasizes.GiB / disk.DefaultSectorSize, + Lock: true, + }, + }, + "device": { + Type: "org.osbuild.lvm2.lv", + Parent: "rootvg", + Options: &LVM2LVDeviceOptions{ + Volume: "lv-for-/", + }, + }, + }, + }, + { + Type: "org.osbuild.mkfs.xfs", + Options: &MkfsXfsStageOptions{}, + Devices: map[string]Device{ + "rootvg": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Start: (datasizes.GiB + 100*datasizes.MiB) / disk.DefaultSectorSize, + Size: 9 * datasizes.GiB / disk.DefaultSectorSize, + Lock: true, + }, + }, + "device": { + Type: "org.osbuild.lvm2.lv", + Parent: "rootvg", + Options: &LVM2LVDeviceOptions{ + Volume: "lv-for-/home", + }, + }, + }, + }, + }, stages) +} + func TestGenFsStagesUnhappy(t *testing.T) { pt := &disk.PartitionTable{ Type: disk.PT_GPT, From 3651b9202ffb6adcf3c556a05c6ca9080e0a32d4 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 27 Nov 2024 17:36:23 +0100 Subject: [PATCH 12/17] osbuild: add swap to all GenFSStages() tests --- pkg/osbuild/mkfs_stages_test.go | 70 ++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/pkg/osbuild/mkfs_stages_test.go b/pkg/osbuild/mkfs_stages_test.go index 9811c36d1f..62e9c311ba 100644 --- a/pkg/osbuild/mkfs_stages_test.go +++ b/pkg/osbuild/mkfs_stages_test.go @@ -77,7 +77,7 @@ func TestNewMkfsStage(t *testing.T) { } func TestGenFsStages(t *testing.T) { - pt := testdisk.MakeFakePartitionTable("/", "/boot", "/boot/efi") + pt := testdisk.MakeFakePartitionTable("/", "/boot", "/boot/efi", "swap") stages := GenFsStages(pt, "file.img") assert.Equal(t, []*Stage{ { @@ -128,12 +128,29 @@ func TestGenFsStages(t *testing.T) { }, }, }, + { + Type: "org.osbuild.mkswap", + Options: &MkswapStageOptions{ + UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", + Label: "swap", + }, + Devices: map[string]Device{ + "device": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Size: testdisk.FakePartitionSize / disk.DefaultSectorSize, + Lock: true, + }, + }, + }, + }, }, stages) } func TestGenFsStagesBtrfs(t *testing.T) { // Let's put there /extra to make sure that / and /extra creates only one btrfs partition - pt := testdisk.MakeFakeBtrfsPartitionTable("/", "/boot", "/boot/efi", "/extra") + pt := testdisk.MakeFakeBtrfsPartitionTable("/", "/boot", "/boot/efi", "/extra", "swap") stages := GenFsStages(pt, "file.img") assert.Equal(t, []*Stage{ { @@ -167,6 +184,24 @@ func TestGenFsStagesBtrfs(t *testing.T) { }, }, }, + { + Type: "org.osbuild.mkswap", + Options: &MkswapStageOptions{ + UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", + Label: "swap", + }, + Devices: map[string]Device{ + "device": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Start: (datasizes.GiB + 100*datasizes.MiB) / disk.DefaultSectorSize, + Size: 512 * datasizes.MiB / disk.DefaultSectorSize, + Lock: true, + }, + }, + }, + }, { Type: "org.osbuild.mkfs.btrfs", Options: &MkfsBtrfsStageOptions{ @@ -177,7 +212,7 @@ func TestGenFsStagesBtrfs(t *testing.T) { Type: "org.osbuild.loopback", Options: &LoopbackDeviceOptions{ Filename: "file.img", - Start: (datasizes.GiB + 100*datasizes.MiB) / disk.DefaultSectorSize, + Start: (512*datasizes.MiB + datasizes.GiB + 100*datasizes.MiB) / disk.DefaultSectorSize, Size: 9 * datasizes.GiB / disk.DefaultSectorSize, Lock: true, }, @@ -201,7 +236,7 @@ func TestGenFsStagesBtrfs(t *testing.T) { Type: "org.osbuild.loopback", Options: &LoopbackDeviceOptions{ Filename: "file.img", - Start: (datasizes.GiB + 100*datasizes.MiB) / disk.DefaultSectorSize, + Start: (512*datasizes.MiB + datasizes.GiB + 100*datasizes.MiB) / disk.DefaultSectorSize, Size: 9 * datasizes.GiB / disk.DefaultSectorSize, Lock: true, }, @@ -221,7 +256,7 @@ func TestGenFsStagesBtrfs(t *testing.T) { } func TestGenFsStagesLVM(t *testing.T) { - pt := testdisk.MakeFakeLVMPartitionTable("/", "/boot", "/boot/efi", "/home") + pt := testdisk.MakeFakeLVMPartitionTable("/", "/boot", "/boot/efi", "/home", "swap") stages := GenFsStages(pt, "file.img") assert.Equal(t, []*Stage{ { @@ -299,6 +334,31 @@ func TestGenFsStagesLVM(t *testing.T) { }, }, }, + { + Type: "org.osbuild.mkswap", + Options: &MkswapStageOptions{ + UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", + Label: "swap", + }, + Devices: map[string]Device{ + "rootvg": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Start: (datasizes.GiB + 100*datasizes.MiB) / disk.DefaultSectorSize, + Size: 9 * datasizes.GiB / disk.DefaultSectorSize, + Lock: true, + }, + }, + "device": { + Type: "org.osbuild.lvm2.lv", + Parent: "rootvg", + Options: &LVM2LVDeviceOptions{ + Volume: "lv-for-swap", + }, + }, + }, + }, }, stages) } From ac95d6dd495101abab149ee252e6d937825816cc Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 27 Nov 2024 18:44:22 +0100 Subject: [PATCH 13/17] blueprint: allow "swap" for FSType in FilesystemTypedCustomization Support setting the fileystem type for a FilesystemTypedCustomization to "swap". When this is set, the Mountpoint should be empty. This makes it possible to create swap areas on either plain partitions or on logical volumes. --- pkg/blueprint/disk_customizations.go | 23 ++++++- pkg/blueprint/disk_customizations_test.go | 81 ++++++++++++++++++++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/pkg/blueprint/disk_customizations.go b/pkg/blueprint/disk_customizations.go index 0fd360bfb7..769e96db34 100644 --- a/pkg/blueprint/disk_customizations.go +++ b/pkg/blueprint/disk_customizations.go @@ -75,6 +75,9 @@ type PartitionCustomization struct { // - Does not define a size. The size is defined by its container: a // partition ([PartitionCustomization]) or LVM logical volume // ([LVCustomization]). +// +// Setting the FSType to "swap" creates a swap area (and the Mountpoint must be +// empty). type FilesystemTypedCustomization struct { Mountpoint string `json:"mountpoint" toml:"mountpoint"` Label string `json:"label,omitempty" toml:"label,omitempty"` @@ -332,6 +335,7 @@ func (v *PartitionCustomization) UnmarshalTOML(data any) error { // - Plain filesystem types are valid for the partition type // - All non-empty properties are valid for the partition type (e.g. // LogicalVolumes is empty when the type is "plain" or "btrfs") +// - Filesystems with FSType set to "swap" do not specify a mountpoint. // // Note that in *addition* consumers should also call // ValidateLayoutConstraints() to validate that the policy for disk @@ -450,6 +454,14 @@ var validPlainFSTypes = []string{ } func (p *PartitionCustomization) validatePlain(mountpoints map[string]bool) error { + if p.FSType == "swap" { + // make sure the mountpoint is empty and return + if p.Mountpoint != "" { + return fmt.Errorf("mountpoint for swap partition must be empty (got %q)", p.Mountpoint) + } + return nil + } + if err := validateMountpoint(p.Mountpoint); err != nil { return err } @@ -490,6 +502,13 @@ func (p *PartitionCustomization) validateLVM(mountpoints, vgnames map[string]boo } lvnames[lv.Name] = true + if lv.FSType == "swap" { + // make sure the mountpoint is empty and return + if lv.Mountpoint != "" { + return fmt.Errorf("mountpoint for swap logical volume with name %q in volume group %q must be empty", lv.Name, p.Name) + } + return nil + } if err := validateMountpoint(lv.Mountpoint); err != nil { return fmt.Errorf("invalid logical volume customization: %w", err) } @@ -560,7 +579,9 @@ func CheckDiskMountpointsPolicy(partitioning *DiskCustomization, mountpointAllow mountpoints = append(mountpoints, part.Mountpoint) } for _, lv := range part.LogicalVolumes { - mountpoints = append(mountpoints, lv.Mountpoint) + if lv.Mountpoint != "" { + mountpoints = append(mountpoints, lv.Mountpoint) + } } for _, subvol := range part.Subvolumes { mountpoints = append(mountpoints, subvol.Mountpoint) diff --git a/pkg/blueprint/disk_customizations_test.go b/pkg/blueprint/disk_customizations_test.go index 9c3259b5dc..bf6d340e24 100644 --- a/pkg/blueprint/disk_customizations_test.go +++ b/pkg/blueprint/disk_customizations_test.go @@ -37,7 +37,7 @@ func TestPartitioningValidation(t *testing.T) { }, expectedMsg: "", }, - "happy-plain+btrfs": { + "happy-plain+btrfs+swap": { partitioning: &blueprint.DiskCustomization{ Partitions: []blueprint.PartitionCustomization{ { @@ -46,6 +46,11 @@ func TestPartitioningValidation(t *testing.T) { Mountpoint: "/data", }, }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "swap", + }, + }, { Type: "btrfs", BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ @@ -89,6 +94,39 @@ func TestPartitioningValidation(t *testing.T) { }, expectedMsg: "", }, + "happy-plain+lvm-with-swap": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "plain", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "xfs", + Mountpoint: "/data", + }, + }, + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + Name: "root", + LogicalVolumes: []blueprint.LVCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "swap", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "", + }, "happy-plain-with-boot-and-efi": { partitioning: &blueprint.DiskCustomization{ Partitions: []blueprint.PartitionCustomization{ @@ -825,6 +863,47 @@ func TestPartitioningValidation(t *testing.T) { }, expectedMsg: "invalid partitioning customizations:\nunknown partition type: what", }, + "unhappy-swap-with-mountpoint": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/home", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "swap", + Mountpoint: "/swap", + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nmountpoint for swap partition must be empty (got \"/swap\")", + }, + "unhappy-swaplv-with-mountpoint": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + Name: "badvg", + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "swappylv", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "swap", + Mountpoint: "/var/swap", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nmountpoint for swap logical volume with name \"swappylv\" in volume group \"badvg\" must be empty", + }, } for name := range testCases { From 893aa0ee03ff001592a62624d340047d2c2ce1c9 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 27 Nov 2024 18:42:21 +0100 Subject: [PATCH 14/17] disk: handle swap partitions and LVs in NewCustomPartitionTable() When creating plain partitions of logical volumes, if the fstype is "swap", create a Swap payload instead of a Filesystem. Added a new test case for plain with swap and included swap in the btrfs case and a swap logical volume in the lvm case. --- pkg/disk/partition_table.go | 56 +++++++--- pkg/disk/partition_table_test.go | 176 ++++++++++++++++++++++++++++--- 2 files changed, 206 insertions(+), 26 deletions(-) diff --git a/pkg/disk/partition_table.go b/pkg/disk/partition_table.go index 83478d8d0c..04c64ecda7 100644 --- a/pkg/disk/partition_table.go +++ b/pkg/disk/partition_table.go @@ -1283,25 +1283,43 @@ func addPlainPartition(pt *PartitionTable, partition blueprint.PartitionCustomiz if err != nil { return fmt.Errorf("error creating partition with mountpoint %q: %w", partition.Mountpoint, err) } - // all user-defined partitions are data partitions except boot - typeName := "data" - if partition.Mountpoint == "/boot" { + + // all user-defined partitions are data partitions except boot and swap + var typeName string + switch { + case partition.Mountpoint == "/boot": typeName = "boot" + case fstype == "swap": + typeName = "swap" + default: + typeName = "data" } + partType, err := getPartitionTypeIDfor(pt.Type, typeName) if err != nil { return fmt.Errorf("error getting partition type ID for %q: %w", partition.Mountpoint, err) } - newpart := Partition{ - Type: partType, - Bootable: false, - Size: partition.MinSize, - Payload: &Filesystem{ + + var payload PayloadEntity + switch typeName { + case "swap": + payload = &Swap{ + Label: partition.Label, + FSTabOptions: "defaults", // TODO: add customization + } + default: + payload = &Filesystem{ Type: fstype, Label: partition.Label, Mountpoint: partition.Mountpoint, FSTabOptions: "defaults", // TODO: add customization - }, + } + } + + newpart := Partition{ + Type: partType, + Size: partition.MinSize, + Payload: payload, } pt.Partitions = append(pt.Partitions, newpart) return nil @@ -1339,11 +1357,21 @@ func addLVMPartition(pt *PartitionTable, partition blueprint.PartitionCustomizat if err != nil { return fmt.Errorf("error creating logical volume %q (%s): %w", lv.Name, lv.Mountpoint, err) } - newfs := &Filesystem{ - Type: fstype, - Label: lv.Label, - Mountpoint: lv.Mountpoint, - FSTabOptions: "defaults", // TODO: add customization + + var newfs PayloadEntity + switch fstype { + case "swap": + newfs = &Swap{ + Label: lv.Label, + FSTabOptions: "defaults", // TODO: add customization + } + default: + newfs = &Filesystem{ + Type: fstype, + Label: lv.Label, + Mountpoint: lv.Mountpoint, + FSTabOptions: "defaults", // TODO: add customization + } } if _, err := newvg.CreateLogicalVolume(lv.Name, lv.MinSize, newfs); err != nil { return fmt.Errorf("error creating logical volume %q (%s): %w", lv.Name, lv.Mountpoint, err) diff --git a/pkg/disk/partition_table_test.go b/pkg/disk/partition_table_test.go index 193baba0e9..2924a5f00b 100644 --- a/pkg/disk/partition_table_test.go +++ b/pkg/disk/partition_table_test.go @@ -1259,6 +1259,105 @@ func TestNewCustomPartitionTable(t *testing.T) { }, }, }, + "plain+swap": { + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + MinSize: 20 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/data", + Label: "data", + FSType: "ext4", + }, + }, + { + MinSize: 5 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Label: "swap", + FSType: "swap", + }, + }, + }, + }, + options: &disk.CustomPartitionTableOptions{ + DefaultFSType: disk.FS_XFS, + BootMode: platform.BOOT_HYBRID, + PartitionTableType: disk.PT_DOS, + }, + expected: &disk.PartitionTable{ + Type: disk.PT_DOS, + Size: 227 * datasizes.MiB, + UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", + Partitions: []disk.Partition{ + { + Start: 1 * datasizes.MiB, // header + Size: 1 * datasizes.MiB, + Bootable: true, + Type: disk.DosBIOSBootID, + UUID: disk.BIOSBootPartitionUUID, + }, + { + Start: 2 * datasizes.MiB, + Size: 200 * datasizes.MiB, + Type: disk.DosESPID, + UUID: disk.EFISystemPartitionUUID, + Payload: &disk.Filesystem{ + Type: "vfat", + UUID: disk.EFIFilesystemUUID, + Mountpoint: "/boot/efi", + Label: "EFI-SYSTEM", + FSTabOptions: "defaults,uid=0,gid=0,umask=077,shortname=winnt", + FSTabFreq: 0, + FSTabPassNo: 2, + }, + }, + { + Start: 202 * datasizes.MiB, + Size: 20 * datasizes.MiB, + Type: disk.DosLinuxTypeID, + Bootable: false, + UUID: "", // partitions on dos PTs don't have UUIDs + Payload: &disk.Filesystem{ + Type: "ext4", + Label: "data", + Mountpoint: "/data", + UUID: "6e4ff95f-f662-45ee-a82a-bdf44a2d0b75", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + Start: 222 * datasizes.MiB, + Size: 5 * datasizes.MiB, + Type: disk.DosSwapID, + UUID: "", // partitions on dos PTs don't have UUIDs + Bootable: false, + Payload: &disk.Swap{ + Label: "swap", + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", + FSTabOptions: "defaults", + }, + }, + { + Start: 227 * datasizes.MiB, + Size: 0, + Type: disk.DosLinuxTypeID, + UUID: "", // partitions on dos PTs don't have UUIDs + Bootable: false, + Payload: &disk.Filesystem{ + Type: "xfs", + Label: "root", + Mountpoint: "/", + UUID: "a178892e-e285-4ce1-9114-55780875d64e", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + }, + }, + }, "plain-legacy": { customizations: &blueprint.DiskCustomization{ Partitions: []blueprint.PartitionCustomization{ @@ -1497,6 +1596,13 @@ func TestNewCustomPartitionTable(t *testing.T) { FSType: "ext4", }, }, + { + MinSize: 12 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Label: "swappyswaps", + FSType: "swap", + }, + }, }, }, options: &disk.CustomPartitionTableOptions{ @@ -1507,7 +1613,7 @@ func TestNewCustomPartitionTable(t *testing.T) { }, expected: &disk.PartitionTable{ Type: disk.PT_GPT, - Size: 222*datasizes.MiB + 3*datasizes.GiB + datasizes.MiB, // start + size of last partition + footer + Size: 234*datasizes.MiB + 3*datasizes.GiB + datasizes.MiB, // start + size of last partition + footer UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", Partitions: []disk.Partition{ @@ -1535,10 +1641,10 @@ func TestNewCustomPartitionTable(t *testing.T) { }, // root is aligned to the end but not reindexed { - Start: 222 * datasizes.MiB, + Start: 234 * datasizes.MiB, Size: 3*datasizes.GiB + datasizes.MiB - (disk.DefaultSectorSize + (128 * 128)), // grows by 1 grain size (1 MiB) minus the unaligned size of the header to fit the gpt footer Type: disk.FilesystemDataGUID, - UUID: "a178892e-e285-4ce1-9114-55780875d64e", + UUID: "e2d3d0d0-de6b-48f9-b44c-e85ff044c6b1", Bootable: false, Payload: &disk.Filesystem{ Type: "xfs", @@ -1554,7 +1660,7 @@ func TestNewCustomPartitionTable(t *testing.T) { Start: 202 * datasizes.MiB, Size: 20 * datasizes.MiB, Type: disk.FilesystemDataGUID, - UUID: "e2d3d0d0-de6b-48f9-b44c-e85ff044c6b1", + UUID: "f83b8e88-3bbf-457a-ab99-c5b252c7429c", Bootable: false, Payload: &disk.Filesystem{ Type: "ext4", @@ -1566,6 +1672,17 @@ func TestNewCustomPartitionTable(t *testing.T) { FSTabPassNo: 0, }, }, + { + Start: 222 * datasizes.MiB, + Size: 12 * datasizes.MiB, + Type: disk.SwapPartitionGUID, + UUID: "32f3a8ae-b79e-4856-b659-c18f0dcecc77", + Payload: &disk.Swap{ + Label: "swappyswaps", + UUID: "a178892e-e285-4ce1-9114-55780875d64e", + FSTabOptions: "defaults", + }, + }, }, }, }, @@ -1604,6 +1721,14 @@ func TestNewCustomPartitionTable(t *testing.T) { FSType: "ext4", // TODO: remove when we reintroduce the default fs }, }, + { // swap on LV + Name: "swaplv", + MinSize: 30 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Label: "swap-on-lv", + FSType: "swap", + }, + }, }, }, }, @@ -1616,7 +1741,7 @@ func TestNewCustomPartitionTable(t *testing.T) { expected: &disk.PartitionTable{ Type: disk.PT_GPT, // default when unspecified UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", - Size: 714*datasizes.MiB + 168*datasizes.MiB + datasizes.MiB, // start + size of last partition (VG) + footer + Size: 714*datasizes.MiB + 200*datasizes.MiB + datasizes.MiB, // start + size of last partition (VG) + footer Partitions: []disk.Partition{ { Start: 1 * datasizes.MiB, // header @@ -1644,7 +1769,7 @@ func TestNewCustomPartitionTable(t *testing.T) { Start: 202 * datasizes.MiB, Size: 512 * datasizes.MiB, Type: disk.XBootLDRPartitionGUID, - UUID: "f83b8e88-3bbf-457a-ab99-c5b252c7429c", + UUID: "32f3a8ae-b79e-4856-b659-c18f0dcecc77", Bootable: false, Payload: &disk.Filesystem{ Type: "ext4", @@ -1658,9 +1783,9 @@ func TestNewCustomPartitionTable(t *testing.T) { }, { Start: 714 * datasizes.MiB, - Size: 168*datasizes.MiB + datasizes.MiB - (disk.DefaultSectorSize + (128 * 128)), // the sum of the LVs (rounded to the next 4 MiB extent) grows by 1 grain size (1 MiB) minus the unaligned size of the header to fit the gpt footer + Size: 200*datasizes.MiB + datasizes.MiB - (disk.DefaultSectorSize + (128 * 128)), // the sum of the LVs (rounded to the next 4 MiB extent) grows by 1 grain size (1 MiB) minus the unaligned size of the header to fit the gpt footer Type: disk.LVMPartitionGUID, - UUID: "32f3a8ae-b79e-4856-b659-c18f0dcecc77", + UUID: "c75e7a81-bfde-475f-a7cf-e242cf3cc354", Bootable: false, Payload: &disk.LVMVolumeGroup{ Name: "testvg", @@ -1699,6 +1824,15 @@ func TestNewCustomPartitionTable(t *testing.T) { UUID: "e2d3d0d0-de6b-48f9-b44c-e85ff044c6b1", }, }, + { + Name: "swaplv", + Size: 32 * datasizes.MiB, // rounded up to the next extent (4 MiB) + Payload: &disk.Swap{ + Label: "swap-on-lv", + UUID: "f83b8e88-3bbf-457a-ab99-c5b252c7429c", + FSTabOptions: "defaults", + }, + }, }, }, }, @@ -1878,6 +2012,13 @@ func TestNewCustomPartitionTable(t *testing.T) { }, }, }, + { + MinSize: 120 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Label: "butterswap", + FSType: "swap", + }, + }, }, }, options: &disk.CustomPartitionTableOptions{ @@ -1887,7 +2028,7 @@ func TestNewCustomPartitionTable(t *testing.T) { }, expected: &disk.PartitionTable{ Type: disk.PT_GPT, - Size: 714*datasizes.MiB + 230*datasizes.MiB + datasizes.MiB, // start + size of last partition + footer + Size: 834*datasizes.MiB + 230*datasizes.MiB + datasizes.MiB, // start + size of last partition + footer UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", Partitions: []disk.Partition{ { @@ -1916,7 +2057,7 @@ func TestNewCustomPartitionTable(t *testing.T) { Start: 202 * datasizes.MiB, Size: 512 * datasizes.MiB, Type: disk.XBootLDRPartitionGUID, - UUID: "a178892e-e285-4ce1-9114-55780875d64e", + UUID: "e2d3d0d0-de6b-48f9-b44c-e85ff044c6b1", Bootable: false, Payload: &disk.Filesystem{ Type: "ext4", @@ -1929,10 +2070,10 @@ func TestNewCustomPartitionTable(t *testing.T) { }, }, { - Start: 714 * datasizes.MiB, + Start: 834 * datasizes.MiB, Size: 231*datasizes.MiB - (disk.DefaultSectorSize + (128 * 128)), // grows by 1 grain size (1 MiB) minus the unaligned size of the header to fit the gpt footer Type: disk.FilesystemDataGUID, - UUID: "e2d3d0d0-de6b-48f9-b44c-e85ff044c6b1", + UUID: "f83b8e88-3bbf-457a-ab99-c5b252c7429c", Bootable: false, Payload: &disk.Btrfs{ UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", @@ -1955,6 +2096,17 @@ func TestNewCustomPartitionTable(t *testing.T) { }, }, }, + { + Start: 714 * datasizes.MiB, + Size: 120 * datasizes.MiB, + Type: disk.SwapPartitionGUID, + UUID: "32f3a8ae-b79e-4856-b659-c18f0dcecc77", + Payload: &disk.Swap{ + Label: "butterswap", + UUID: "a178892e-e285-4ce1-9114-55780875d64e", + FSTabOptions: "defaults", + }, + }, }, }, }, From 1e78f3dc26f5cfab3764322bcc139c48ad7435e6 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 27 Nov 2024 18:55:48 +0100 Subject: [PATCH 15/17] test: add swap to all the partitioning test configs --- test/configs/partitioning-btrfs.json | 6 ++++++ test/configs/partitioning-lvm.json | 5 +++++ test/configs/partitioning-plain.json | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/test/configs/partitioning-btrfs.json b/test/configs/partitioning-btrfs.json index 3f7079908a..0451f19e23 100644 --- a/test/configs/partitioning-btrfs.json +++ b/test/configs/partitioning-btrfs.json @@ -47,6 +47,12 @@ "mountpoint": "/srv" } ] + }, + { + "type": "plain", + "fs_type": "swap", + "label": "swap-part", + "minsize": "1 GiB" } ] } diff --git a/test/configs/partitioning-lvm.json b/test/configs/partitioning-lvm.json index 960ec93550..22e525be7d 100644 --- a/test/configs/partitioning-lvm.json +++ b/test/configs/partitioning-lvm.json @@ -63,6 +63,11 @@ "mountpoint": "/srv", "fs_type": "ext4", "minsize": 1073741824 + }, + { + "name": "swap-lv", + "fs_type": "swap", + "minsize": "1 GiB" } ] } diff --git a/test/configs/partitioning-plain.json b/test/configs/partitioning-plain.json index 6c2e06ae33..8f3b6b449d 100644 --- a/test/configs/partitioning-plain.json +++ b/test/configs/partitioning-plain.json @@ -49,6 +49,10 @@ "mountpoint": "/srv", "fs_type": "xfs", "minsize": 1073741824 + }, + { + "fs_type": "swap", + "minsize": "1 GiB" } ] } From c1b9afe627f153d9b0bdcee1a3cdfbffb6998837 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Thu, 28 Nov 2024 14:37:37 +0100 Subject: [PATCH 16/17] disk: test ForEachFSTabEntity() --- pkg/disk/disk_test.go | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pkg/disk/disk_test.go b/pkg/disk/disk_test.go index 615066bea2..372b073c0c 100644 --- a/pkg/disk/disk_test.go +++ b/pkg/disk/disk_test.go @@ -972,3 +972,47 @@ func TestFSTabOptionsReadOnly(t *testing.T) { }) } } + +func TestForEachFSTabEntity(t *testing.T) { + // Use the test partition tables and check that fstab entities are all + // visited by collecting their target fields. + // The names must match the ones in testdisk.TestPartitionTables. + expectedEntityPaths := map[string][]string{ + "plain": {"/", "/boot", "/boot/efi"}, + "plain-swap": {"/", "/boot", "none", "/boot/efi"}, + "plain-noboot": {"/", "/boot/efi"}, + "luks": {"/", "/boot", "/boot/efi"}, + "luks+lvm": {"/", "/boot", "/home", "/boot/efi"}, + "btrfs": {"/", "/boot", "/var", "/boot/efi"}, + } + + for name := range testdisk.TestPartitionTables { + t.Run(name, func(t *testing.T) { + + var targets []string + targetCollectorCB := func(ent disk.FSTabEntity, _ []disk.Entity) error { + targets = append(targets, ent.GetFSFile()) + return nil + } + + require := require.New(t) + pt := testdisk.TestPartitionTables[name] + + // print an informative failure message if a new test partition + // table is added and this test is not updated (instead of failing + // at the final Equal() check) + exp, ok := expectedEntityPaths[name] + require.True(ok, "expected options not defined for test partition table %q: please update the TestNewFSTabStageOptions test", name) + + err := pt.ForEachFSTabEntity(targetCollectorCB) + // the callback never returns an error, but let's check it anyway + // in case the foreach function ever changes to return other errors + require.NoError(err) + + require.NotEmpty(targets) + + // we don't care about the order + require.ElementsMatch(exp, targets) + }) + } +} From de9034dfc8db89973bedfb6a757b58734b089203 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 28 Nov 2024 15:16:14 +0100 Subject: [PATCH 17/17] osbuild: add test for NewMkswapStage() --- pkg/osbuild/mkswap_stage_test.go | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 pkg/osbuild/mkswap_stage_test.go diff --git a/pkg/osbuild/mkswap_stage_test.go b/pkg/osbuild/mkswap_stage_test.go new file mode 100644 index 0000000000..b914349cc6 --- /dev/null +++ b/pkg/osbuild/mkswap_stage_test.go @@ -0,0 +1,37 @@ +package osbuild + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +var expectedJSON = `{ + "type": "org.osbuild.mkswap", + "options": { + "uuid": "8a1fc521-02a0-4917-92a9-90a44d7e6503", + "label": "some-label" + }, + "devices": { + "root": { + "type": "org.osbuild.loopback" + } + } +}` + +func TestNewMkswapStage(t *testing.T) { + devices := make(map[string]Device) + devices["root"] = Device{ + Type: "org.osbuild.loopback", + } + + options := MkswapStageOptions{ + UUID: "8a1fc521-02a0-4917-92a9-90a44d7e6503", + Label: "some-label", + } + stage := NewMkswapStage(&options, devices) + b, err := json.MarshalIndent(stage, "", " ") + assert.NoError(t, err) + assert.Equal(t, expectedJSON, string(b)) +}