diff --git a/internal/testdisk/partition.go b/internal/testdisk/partition.go index b2a8d5d69b..827583b58f 100644 --- a/internal/testdisk/partition.go +++ b/internal/testdisk/partition.go @@ -1,29 +1,407 @@ package testdisk import ( + "math/rand" + "github.com/osbuild/images/pkg/datasizes" "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{ + { + Name: "root", + Size: 0, + Mountpoint: "/", + GroupID: 0, + }, + { + Name: "var", + 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. +// 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, @@ -39,11 +417,16 @@ 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, - Size: 10 * datasizes.GiB, + Size: 10 * GiB, Partitions: []disk.Partition{}, } size := uint64(0) @@ -52,35 +435,48 @@ 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 + 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, }, ) @@ -89,14 +485,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 @@ -104,11 +500,16 @@ 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, - Size: 10 * datasizes.GiB, + Size: 10 * GiB, Partitions: []disk.Partition{}, } size := uint64(0) @@ -117,24 +518,36 @@ 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 + 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 == "/" { @@ -155,13 +568,14 @@ 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{ + Name: "rootvg", LogicalVolumes: lvs, }, }) - size += 9 * datasizes.GiB + size += 9 * GiB pt.Size = size return pt 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 { 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..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, }, } @@ -248,13 +255,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/disk_test.go b/pkg/disk/disk_test.go index 0f6d9d2787..372b073c0c 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 @@ -971,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) + }) + } +} 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/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{}) } diff --git a/pkg/disk/partition_table.go b/pkg/disk/partition_table.go index 978b34d7fb..04c64ecda7 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 { @@ -812,6 +838,7 @@ type partitionTableFeatures struct { FAT bool EXT4 bool LUKS bool + Swap bool } // features examines all of the PartitionTable entities @@ -836,6 +863,8 @@ func (pt *PartitionTable) features() partitionTableFeatures { case "ext4": ptFeatures.EXT4 = true } + case *Swap: + ptFeatures.Swap = true case *LUKSContainer: ptFeatures.LUKS = true } @@ -1254,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 @@ -1310,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_internal_test.go b/pkg/disk/partition_table_internal_test.go index d94710f2d5..2d0e917c0c 100644 --- a/pkg/disk/partition_table_internal_test.go +++ b/pkg/disk/partition_table_internal_test.go @@ -14,310 +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-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}}, - {"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..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", + }, + }, }, }, }, @@ -2130,3 +2282,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)) + + } +} 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() + } +} 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/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 } 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) + }) + } +} 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..62e9c311ba 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) { - pt := testdisk.MakeFakePartitionTable("/", "/boot", "/boot/efi") - stages := GenMkfsStages(pt, "file.img") +func TestGenFsStages(t *testing.T) { + pt := testdisk.MakeFakePartitionTable("/", "/boot", "/boot/efi", "swap") + stages := GenFsStages(pt, "file.img") assert.Equal(t, []*Stage{ { Type: "org.osbuild.mkfs.ext4", @@ -128,13 +128,30 @@ func TestGenMkfsStages(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 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") + pt := testdisk.MakeFakeBtrfsPartitionTable("/", "/boot", "/boot/efi", "/extra", "swap") + stages := GenFsStages(pt, "file.img") assert.Equal(t, []*Stage{ { Type: "org.osbuild.mkfs.ext4", @@ -167,6 +184,24 @@ func TestGenMkfsStagesBtrfs(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{ @@ -174,6 +209,139 @@ func TestGenMkfsStagesBtrfs(t *testing.T) { }, Devices: map[string]Device{ "device": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Start: (512*datasizes.MiB + datasizes.GiB + 100*datasizes.MiB) / disk.DefaultSectorSize, + Size: 9 * datasizes.GiB / disk.DefaultSectorSize, + Lock: true, + }, + }, + }, + }, + { + 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: (512*datasizes.MiB + 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 TestGenFsStagesLVM(t *testing.T) { + pt := testdisk.MakeFakeLVMPartitionTable("/", "/boot", "/boot/efi", "/home", "swap") + 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", + }, + }, + }, + }, + { + 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", @@ -182,12 +350,19 @@ func TestGenMkfsStagesBtrfs(t *testing.T) { Lock: true, }, }, + "device": { + Type: "org.osbuild.lvm2.lv", + Parent: "rootvg", + Options: &LVM2LVDeviceOptions{ + Volume: "lv-for-swap", + }, + }, }, }, }, stages) } -func TestGenMkfsStagesUnhappy(t *testing.T) { +func TestGenFsStagesUnhappy(t *testing.T) { pt := &disk.PartitionTable{ Type: disk.PT_GPT, Partitions: []disk.Partition{ @@ -199,7 +374,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") }) } 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, + } +} 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)) +} 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" } ] }