From 567f0933f95e807e7d5e4cbd45a2fc5d156c8b60 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 13 Nov 2024 15:56:05 +0100 Subject: [PATCH 01/13] blueprint: unify handling of size customization parsing Size values in the blueprint can be defined as either integers, representing bytes, or strings with SI unit prefixes for bytes. The conversion for the Filesystem.MinSize happens in both the JSON and TOML unmarshallers. With the new partitioning customizations we will have more conversions of the same type. This commit extracts the handling into a generic function for reusability and convenience. This changes the error messages. Tests have been updated accordingly. --- pkg/blueprint/filesystem_customizations.go | 66 ++++++++++--------- .../filesystem_customizations_test.go | 12 ++-- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/pkg/blueprint/filesystem_customizations.go b/pkg/blueprint/filesystem_customizations.go index 68c7126f06..e783930052 100644 --- a/pkg/blueprint/filesystem_customizations.go +++ b/pkg/blueprint/filesystem_customizations.go @@ -24,26 +24,13 @@ func (fsc *FilesystemCustomization) UnmarshalTOML(data interface{}) error { case string: fsc.Mountpoint = d["mountpoint"].(string) default: - return fmt.Errorf("TOML unmarshal: mountpoint must be string, got %v of type %T", d["mountpoint"], d["mountpoint"]) + return fmt.Errorf("TOML unmarshal: mountpoint must be string, got \"%v\" of type %T", d["mountpoint"], d["mountpoint"]) } - - switch d["minsize"].(type) { - case int64: - minSize := d["minsize"].(int64) - if minSize < 0 { - return fmt.Errorf("TOML unmarshal: minsize cannot be negative") - } - fsc.MinSize = uint64(minSize) - case string: - minSize, err := datasizes.Parse(d["minsize"].(string)) - if err != nil { - return fmt.Errorf("TOML unmarshal: minsize is not valid filesystem size (%w)", err) - } - fsc.MinSize = minSize - default: - return fmt.Errorf("TOML unmarshal: minsize must be integer or string, got %v of type %T", d["minsize"], d["minsize"]) + minSize, err := decodeSize(d["minsize"]) + if err != nil { + return fmt.Errorf("TOML unmarshal: error decoding minsize value for mountpoint %q: %w", fsc.Mountpoint, err) } - + fsc.MinSize = minSize return nil } @@ -58,23 +45,14 @@ func (fsc *FilesystemCustomization) UnmarshalJSON(data []byte) error { case string: fsc.Mountpoint = d["mountpoint"].(string) default: - return fmt.Errorf("JSON unmarshal: mountpoint must be string, got %v of type %T", d["mountpoint"], d["mountpoint"]) + return fmt.Errorf("JSON unmarshal: mountpoint must be string, got \"%v\" of type %T", d["mountpoint"], d["mountpoint"]) } - // The JSON specification only mentions float64 and Go defaults to it: https://go.dev/blog/json - switch d["minsize"].(type) { - case float64: - fsc.MinSize = uint64(d["minsize"].(float64)) - case string: - minSize, err := datasizes.Parse(d["minsize"].(string)) - if err != nil { - return fmt.Errorf("JSON unmarshal: minsize is not valid filesystem size (%w)", err) - } - fsc.MinSize = minSize - default: - return fmt.Errorf("JSON unmarshal: minsize must be float64 number or string, got %v of type %T", d["minsize"], d["minsize"]) + minSize, err := decodeSize(d["minsize"]) + if err != nil { + return fmt.Errorf("JSON unmarshal: error decoding minsize value for mountpoint %q: %w", fsc.Mountpoint, err) } - + fsc.MinSize = minSize return nil } @@ -93,3 +71,27 @@ func CheckMountpointsPolicy(mountpoints []FilesystemCustomization, mountpointAll return nil } + +// decodeSize takes an integer or string representing a data size (with a data +// suffix) and returns the uint64 representation. +func decodeSize(size any) (uint64, error) { + switch s := size.(type) { + case string: + return datasizes.Parse(s) + case int64: + if s < 0 { + return 0, fmt.Errorf("cannot be negative") + } + return uint64(s), nil + case float64: + if s < 0 { + return 0, fmt.Errorf("cannot be negative") + } + // TODO: emit warning of possible truncation? + return uint64(s), nil + case uint64: + return s, nil + default: + return 0, fmt.Errorf("failed to convert value \"%v\" to number", size) + } +} diff --git a/pkg/blueprint/filesystem_customizations_test.go b/pkg/blueprint/filesystem_customizations_test.go index 4ea1dcf8f9..cb4860c525 100644 --- a/pkg/blueprint/filesystem_customizations_test.go +++ b/pkg/blueprint/filesystem_customizations_test.go @@ -47,19 +47,19 @@ func TestFilesystemCustomizationUnmarshalTOMLUnhappy(t *testing.T) { name: "mountpoint not string", input: `mountpoint = 42 minsize = 42`, - err: "toml: line 0: TOML unmarshal: mountpoint must be string, got 42 of type int64", + err: `toml: line 0: TOML unmarshal: mountpoint must be string, got "42" of type int64`, }, { name: "misize nor string nor int", input: `mountpoint="/" minsize = true`, - err: "toml: line 0: TOML unmarshal: minsize must be integer or string, got true of type bool", + err: `toml: line 0: TOML unmarshal: error decoding minsize value for mountpoint "/": failed to convert value "true" to number`, }, { name: "misize not parseable", input: `mountpoint="/" minsize = "20 KG"`, - err: "toml: line 0: TOML unmarshal: minsize is not valid filesystem size (unknown data size units in string: 20 KG)", + err: `toml: line 0: TOML unmarshal: error decoding minsize value for mountpoint "/": unknown data size units in string: 20 KG`, }, } @@ -81,17 +81,17 @@ func TestFilesystemCustomizationUnmarshalJSONUnhappy(t *testing.T) { { name: "mountpoint not string", input: `{"mountpoint": 42, "minsize": 42}`, - err: "JSON unmarshal: mountpoint must be string, got 42 of type float64", + err: `JSON unmarshal: mountpoint must be string, got "42" of type float64`, }, { name: "misize nor string nor int", input: `{"mountpoint":"/", "minsize": true}`, - err: "JSON unmarshal: minsize must be float64 number or string, got true of type bool", + err: `JSON unmarshal: error decoding minsize value for mountpoint "/": failed to convert value "true" to number`, }, { name: "misize not parseable", input: `{ "mountpoint": "/", "minsize": "20 KG"}`, - err: "JSON unmarshal: minsize is not valid filesystem size (unknown data size units in string: 20 KG)", + err: `JSON unmarshal: error decoding minsize value for mountpoint "/": unknown data size units in string: 20 KG`, }, } From a2df2e28cd6eb3d44403612e544acff4d3b38b94 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Tue, 27 Aug 2024 21:09:17 +0200 Subject: [PATCH 02/13] blueprint: disk customization - New structure that's more powerful than the existing array of FilesystemCustomization. - The general structure defines a (minimum) size for the overall disk and an array of partitions. - Each partition has one of three types: - plain: a plain disk partition, meaning partitions with traditional filesystem payloads (xfs, ext4, etc). - lvm: a partition containing an LVM volume group, which in turn will contain one or more LVM logical volumes. - btrfs: a partition containing a btrfs volume, which in turn will contain one or more subvolumes. - The new FilesystemTypedCustomization struct supports a label and fs_type and no minsize, compared to the old FilesystemCustomization. This struct is used to define both the payload of a plain partition and the payload of an LVM logical volume. - The PartitionCustomization has a type and size and embeds three substructures, one for each partition type. Decoding/unmarshalling of the PartitionCustomization is handled by a "typeSniffer" which first reads the value of the "type" field and then deserialises the whole object into a struct that only contains the fields valid for that partition type. This ensures that no fields are set for the substructure of a different type than the one defined in the "type" fields. --- pkg/blueprint/customizations.go | 8 + pkg/blueprint/partitioning_customizations.go | 229 +++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 pkg/blueprint/partitioning_customizations.go diff --git a/pkg/blueprint/customizations.go b/pkg/blueprint/customizations.go index 28ea761051..d4d8746a35 100644 --- a/pkg/blueprint/customizations.go +++ b/pkg/blueprint/customizations.go @@ -19,6 +19,7 @@ type Customizations struct { Firewall *FirewallCustomization `json:"firewall,omitempty" toml:"firewall,omitempty"` Services *ServicesCustomization `json:"services,omitempty" toml:"services,omitempty"` Filesystem []FilesystemCustomization `json:"filesystem,omitempty" toml:"filesystem,omitempty"` + Disk *DiskCustomization `json:"disk,omitempty" toml:"disk,omitempty"` InstallationDevice string `json:"installation_device,omitempty" toml:"installation_device,omitempty"` FDO *FDOCustomization `json:"fdo,omitempty" toml:"fdo,omitempty"` OpenSCAP *OpenSCAPCustomization `json:"openscap,omitempty" toml:"openscap,omitempty"` @@ -311,6 +312,13 @@ func (c *Customizations) GetFilesystemsMinSize() uint64 { return agg } +func (c *Customizations) GetPartitioning() *DiskCustomization { + if c == nil { + return nil + } + return c.Disk +} + func (c *Customizations) GetInstallationDevice() string { if c == nil || c.InstallationDevice == "" { return "" diff --git a/pkg/blueprint/partitioning_customizations.go b/pkg/blueprint/partitioning_customizations.go new file mode 100644 index 0000000000..b53cac3e91 --- /dev/null +++ b/pkg/blueprint/partitioning_customizations.go @@ -0,0 +1,229 @@ +package blueprint + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type DiskCustomization struct { + // TODO: Add partition table type: gpt or dos + MinSize uint64 `json:"minsize,omitempty" toml:"minsize,omitempty"` + Partitions []PartitionCustomization `json:"partitions,omitempty" toml:"partitions,omitempty"` +} + +// PartitionCustomization defines a single partition on a disk. The Type +// defines the kind of "payload" for the partition: plain, lvm, or btrfs. +// - plain: the payload will be a filesystem on a partition (e.g. xfs, ext4). +// See [FilesystemTypedCustomization] for extra fields. +// - lvm: the payload will be an LVM volume group. See [VGCustomization] for +// extra fields +// - btrfs: the payload will be a btrfs volume. See +// [BtrfsVolumeCustomization] for extra fields. +type PartitionCustomization struct { + // The type of payload for the partition (optional, defaults to "plain"). + Type string `json:"type" toml:"type"` + + // Minimum size of the partition that contains the filesystem (for "plain" + // filesystem), volume group ("lvm"), or btrfs volume ("btrfs"). The final + // size of the partition will be larger than the minsize if the sum of the + // contained volumes (logical volumes or subvolumes) is larger. In + // addition, certain mountpoints have required minimum sizes. See + // https://osbuild.org/docs/user-guide/partitioning for more details. + // (optional, defaults depend on payload and mountpoints). + MinSize uint64 `json:"minsize" toml:"minsize"` + + BtrfsVolumeCustomization + + VGCustomization + + FilesystemTypedCustomization +} + +// A filesystem on a plain partition or LVM logical volume. +// Note the differences from [FilesystemCustomization]: +// - Adds a label. +// - Adds a filesystem type (fs_type). +// - Does not define a size. The size is defined by its container: a +// partition ([PartitionCustomization]) or LVM logical volume +// ([LVCustomization]). +type FilesystemTypedCustomization struct { + Mountpoint string `json:"mountpoint" toml:"mountpoint"` + Label string `json:"label,omitempty" toml:"label,omitempty"` + FSType string `json:"fs_type,omitempty" toml:"fs_type,omitempty"` +} + +// An LVM volume group with one or more logical volumes. +type VGCustomization struct { + // Volume group name (optional, default will be automatically generated). + Name string `json:"name" toml:"name"` + LogicalVolumes []LVCustomization `json:"logical_volumes,omitempty" toml:"logical_volumes,omitempty"` +} + +type LVCustomization struct { + // Logical volume name + Name string `json:"name,omitempty" toml:"name,omitempty"` + + // Minimum size of the logical volume + MinSize uint64 `json:"minsize,omitempty" toml:"minsize,omitempty"` + + FilesystemTypedCustomization +} + +// Custom JSON unmarshaller for LVCustomization for handling the conversion of +// data sizes (minsize) expressed as strings to uint64. +func (lv *LVCustomization) UnmarshalJSON(data []byte) error { + var lvAnySize struct { + Name string `json:"name,omitempty" toml:"name,omitempty"` + MinSize any `json:"minsize,omitempty" toml:"minsize,omitempty"` + FilesystemTypedCustomization + } + if err := json.Unmarshal(data, &lvAnySize); err != nil { + return err + } + + lv.Name = lvAnySize.Name + lv.FilesystemTypedCustomization = lvAnySize.FilesystemTypedCustomization + + if lvAnySize.MinSize != nil { + size, err := decodeSize(lvAnySize.MinSize) + if err != nil { + return err + } + lv.MinSize = size + } + return nil +} + +// A btrfs volume consisting of one or more subvolumes. +type BtrfsVolumeCustomization struct { + Subvolumes []BtrfsSubvolumeCustomization +} + +type BtrfsSubvolumeCustomization struct { + // The name of the subvolume, which defines the location (path) on the + // root volume (required). + // See https://btrfs.readthedocs.io/en/latest/Subvolumes.html + Name string `json:"name" toml:"name"` + + // Mountpoint for the subvolume. + Mountpoint string `json:"mountpoint" toml:"mountpoint"` +} + +// Custom JSON unmarshaller that first reads the value of the "type" field and +// then deserialises the whole object into a struct that only contains the +// fields valid for that partition type. This ensures that no fields are set +// for the substructure of a different type than the one defined in the "type" +// fields. +func (v *PartitionCustomization) UnmarshalJSON(data []byte) error { + errPrefix := "JSON unmarshal:" + var typeSniffer struct { + Type string `json:"type"` + MinSize any `json:"minsize"` + } + if err := json.Unmarshal(data, &typeSniffer); err != nil { + return fmt.Errorf("%s %w", errPrefix, err) + } + + partType := "plain" + if typeSniffer.Type != "" { + partType = typeSniffer.Type + } + + switch partType { + case "plain": + if err := decodePlain(v, data); err != nil { + return fmt.Errorf("%s %w", errPrefix, err) + } + case "btrfs": + if err := decodeBtrfs(v, data); err != nil { + return fmt.Errorf("%s %w", errPrefix, err) + } + case "lvm": + if err := decodeLVM(v, data); err != nil { + return fmt.Errorf("%s %w", errPrefix, err) + } + default: + return fmt.Errorf("%s unknown partition type: %s", errPrefix, partType) + } + + v.Type = partType + + if typeSniffer.MinSize != nil { + minsize, err := decodeSize(typeSniffer.MinSize) + if err != nil { + return fmt.Errorf("%s error decoding minsize for partition: %w", errPrefix, err) + } + v.MinSize = minsize + } + + return nil +} + +// decodePlain decodes the data into a struct that only embeds the +// FilesystemCustomization with DisallowUnknownFields. This ensures that when +// the type is "plain", none of the fields for btrfs or lvm are used. +func decodePlain(v *PartitionCustomization, data []byte) error { + var plain struct { + // Type and minsize are handled by the caller. These are added here to + // satisfy "DisallowUnknownFields" when decoding. + Type string `json:"type"` + MinSize any `json:"minsize"` + FilesystemTypedCustomization + } + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err := decoder.Decode(&plain) + if err != nil { + return fmt.Errorf("error decoding partition with type \"plain\": %w", err) + } + + v.FilesystemTypedCustomization = plain.FilesystemTypedCustomization + return nil +} + +// descodeBtrfs decodes the data into a struct that only embeds the +// BtrfsVolumeCustomization with DisallowUnknownFields. This ensures that when +// the type is btrfs, none of the fields for plain or lvm are used. +func decodeBtrfs(v *PartitionCustomization, data []byte) error { + var btrfs struct { + // Type and minsize are handled by the caller. These are added here to + // satisfy "DisallowUnknownFields" when decoding. + Type string `json:"type"` + MinSize any `json:"minsize"` + BtrfsVolumeCustomization + } + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err := decoder.Decode(&btrfs) + if err != nil { + return fmt.Errorf("error decoding partition with type \"btrfs\": %w", err) + } + + v.BtrfsVolumeCustomization = btrfs.BtrfsVolumeCustomization + return nil +} + +// decodeLVM decodes the data into a struct that only embeds the +// VGCustomization with DisallowUnknownFields. This ensures that when the type +// is lvm, none of the fields for plain or btrfs are used. +func decodeLVM(v *PartitionCustomization, data []byte) error { + var vg struct { + // Type and minsize are handled by the caller. These are added here to + // satisfy "DisallowUnknownFields" when decoding. + Type string `json:"type"` + MinSize any `json:"minsize"` + VGCustomization + } + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&vg); err != nil { + return fmt.Errorf("error decoding partition with type \"lvm\": %w", err) + } + + v.VGCustomization = vg.VGCustomization + return nil +} From b9ad80d82a13c3bb40b76b0e7b3675ab169dd6fc Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 13 Nov 2024 16:47:18 +0100 Subject: [PATCH 03/13] blueprint: add custom TOML unmarshaller for PartitionCustomization The unmarshaller decodes "type" and "minsize" manually and reuses the decode* functions from the JSON unmarshaller for each subtype based on the value of "type". --- pkg/blueprint/partitioning_customizations.go | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/pkg/blueprint/partitioning_customizations.go b/pkg/blueprint/partitioning_customizations.go index b53cac3e91..578d71ca98 100644 --- a/pkg/blueprint/partitioning_customizations.go +++ b/pkg/blueprint/partitioning_customizations.go @@ -227,3 +227,59 @@ func decodeLVM(v *PartitionCustomization, data []byte) error { v.VGCustomization = vg.VGCustomization return nil } + +// Custom TOML unmarshaller that first reads the value of the "type" field and +// then deserialises the whole object into a struct that only contains the +// fields valid for that partition type. This ensures that no fields are set +// for the substructure of a different type than the one defined in the "type" +// fields. +func (v *PartitionCustomization) UnmarshalTOML(data any) error { + errPrefix := "TOML unmarshal:" + d, ok := data.(map[string]any) + if !ok { + return fmt.Errorf("%s customizations.partition is not an object", errPrefix) + } + + partType := "plain" + if typeField, ok := d["type"]; ok { + typeStr, ok := typeField.(string) + if !ok { + return fmt.Errorf("%s type must be a string, got \"%v\" of type %T", errPrefix, typeField, typeField) + } + partType = typeStr + } + + // serialise the data to JSON and reuse the subobject decoders + dataJSON, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("%s error while decoding partition customization: %w", errPrefix, err) + } + switch partType { + case "plain": + if err := decodePlain(v, dataJSON); err != nil { + return fmt.Errorf("%s %w", errPrefix, err) + } + case "btrfs": + if err := decodeBtrfs(v, dataJSON); err != nil { + return fmt.Errorf("%s %w", errPrefix, err) + } + case "lvm": + if err := decodeLVM(v, dataJSON); err != nil { + return fmt.Errorf("%s %w", errPrefix, err) + } + default: + return fmt.Errorf("%s unknown partition type: %s", errPrefix, partType) + } + + v.Type = partType + + if minsizeField, ok := d["minsize"]; ok { + minsize, err := decodeSize(minsizeField) + if err != nil { + return fmt.Errorf("%s error decoding minsize for partition: %w", errPrefix, err) + } + v.MinSize = minsize + } + + return nil +} From 5c0fe887d85b35cdcec84c32f44012c6b945b703 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 13 Nov 2024 17:03:03 +0100 Subject: [PATCH 04/13] blueprint: tests for PartitionCustomization json and toml decoding --- .../partitioning_customizations_test.go | 630 ++++++++++++++++++ 1 file changed, 630 insertions(+) create mode 100644 pkg/blueprint/partitioning_customizations_test.go diff --git a/pkg/blueprint/partitioning_customizations_test.go b/pkg/blueprint/partitioning_customizations_test.go new file mode 100644 index 0000000000..8022f49619 --- /dev/null +++ b/pkg/blueprint/partitioning_customizations_test.go @@ -0,0 +1,630 @@ +package blueprint_test + +import ( + "encoding/json" + "testing" + + "github.com/BurntSushi/toml" + "github.com/osbuild/images/pkg/blueprint" + "github.com/osbuild/images/pkg/datasizes" + "github.com/stretchr/testify/assert" +) + +func TestPartitionCustomizationUnmarshalJSON(t *testing.T) { + type testCase struct { + input string + expected *blueprint.PartitionCustomization + errorMsg string + } + + testCases := map[string]testCase{ + "nothing": { + input: "{}", + expected: &blueprint.PartitionCustomization{ + Type: "plain", + MinSize: 0, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "", + Label: "", + FSType: "", + }, + }, + }, + "plain": { + input: `{ + "type": "plain", + "minsize": "1 GiB", + "mountpoint": "/", + "label": "root", + "fs_type": "xfs" + }`, + expected: &blueprint.PartitionCustomization{ + Type: "plain", + MinSize: 1 * datasizes.GiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/", + Label: "root", + FSType: "xfs", + }, + }, + }, + "plain-with-int": { + input: `{ + "type": "plain", + "minsize": 1073741824, + "mountpoint": "/", + "label": "root", + "fs_type": "xfs" + }`, + expected: &blueprint.PartitionCustomization{ + Type: "plain", + MinSize: 1 * datasizes.GiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/", + Label: "root", + FSType: "xfs", + }, + }, + }, + "btrfs": { + input: `{ + "type": "btrfs", + "minsize": "10 GiB", + "subvolumes": [ + { + "name": "subvols/root", + "mountpoint": "/" + }, + { + "name": "subvols/data", + "mountpoint": "/data" + } + ] + }`, + expected: &blueprint.PartitionCustomization{ + Type: "btrfs", + MinSize: 10 * datasizes.GiB, + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "subvols/root", + Mountpoint: "/", + }, + { + Name: "subvols/data", + Mountpoint: "/data", + }, + }, + }, + }, + }, + "btrfs-with-int": { + input: `{ + "type": "btrfs", + "minsize": 10737418240, + "subvolumes": [ + { + "name": "subvols/root", + "mountpoint": "/" + }, + { + "name": "subvols/data", + "mountpoint": "/data" + } + ] + }`, + expected: &blueprint.PartitionCustomization{ + Type: "btrfs", + MinSize: 10 * datasizes.GiB, + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "subvols/root", + Mountpoint: "/", + }, + { + Name: "subvols/data", + Mountpoint: "/data", + }, + }, + }, + }, + }, + "lvm": { + input: `{ + "type": "lvm", + "name": "myvg", + "minsize": "99 GiB", + "logical_volumes": [ + { + "name": "homelv", + "mountpoint": "/home", + "label": "home", + "fs_type": "ext4", + "minsize": "2 GiB" + }, + { + "name": "loglv", + "mountpoint": "/var/log", + "label": "log", + "fs_type": "xfs", + "minsize": "3 GiB" + } + ] + }`, + expected: &blueprint.PartitionCustomization{ + Type: "lvm", + MinSize: 99 * datasizes.GiB, + VGCustomization: blueprint.VGCustomization{ + Name: "myvg", + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "homelv", + MinSize: 2 * datasizes.GiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/home", + Label: "home", + FSType: "ext4", + }, + }, + { + Name: "loglv", + MinSize: 3 * datasizes.GiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/var/log", + Label: "log", + FSType: "xfs", + }, + }, + }, + }, + }, + }, + "lvm-with-int": { + input: `{ + "type": "lvm", + "name": "myvg", + "minsize": 106300440576, + "logical_volumes": [ + { + "name": "homelv", + "mountpoint": "/home", + "label": "home", + "fs_type": "ext4", + "minsize": 2147483648 + }, + { + "name": "loglv", + "mountpoint": "/var/log", + "label": "log", + "fs_type": "xfs", + "minsize": 3221225472 + } + ] + }`, + expected: &blueprint.PartitionCustomization{ + Type: "lvm", + MinSize: 99 * datasizes.GiB, + VGCustomization: blueprint.VGCustomization{ + Name: "myvg", + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "homelv", + MinSize: 2 * datasizes.GiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/home", + Label: "home", + FSType: "ext4", + }, + }, + { + Name: "loglv", + MinSize: 3 * datasizes.GiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/var/log", + Label: "log", + FSType: "xfs", + }, + }, + }, + }, + }, + }, + "bad-type": { + input: `{"type":"not-a-partition-type"}`, + errorMsg: "JSON unmarshal: unknown partition type: not-a-partition-type", + }, + "number": { + input: `{"type":5}`, + errorMsg: "JSON unmarshal: json: cannot unmarshal number into Go struct field .type of type string", + }, + "negative-size": { + input: `{ + "minsize": -10, + "mountpoint": "/", + "fs_type": "xfs" + }`, + errorMsg: "JSON unmarshal: error decoding minsize for partition: cannot be negative", + }, + "wrong-type/btrfs-with-lvm": { + input: `{ + "type": "btrfs", + "name": "myvg", + "logical_volumes": [ + { + "name": "homelv", + "mountpoint": "/home", + "label": "home", + "fs_type": "ext4" + }, + { + "name": "loglv", + "mountpoint": "/var/log", + "label": "log", + "fs_type": "xfs" + } + ] + }`, + errorMsg: `JSON unmarshal: error decoding partition with type "btrfs": json: unknown field "name"`, + }, + "wrong-type/plain-with-lvm": { + input: `{ + "type": "plain", + "name": "myvg", + "logical_volumes": [ + { + "name": "loglv", + "mountpoint": "/var/log", + "label": "log", + "fs_type": "xfs" + } + ] + }`, + errorMsg: `JSON unmarshal: error decoding partition with type "plain": json: unknown field "name"`, + }, + "wrong-type/lvm-with-btrfs": { + input: `{ + "type": "lvm", + "minsize": "10 GiB", + "subvolumes": [ + { + "name": "subvols/data", + "mountpoint": "/data" + } + ] + }`, + errorMsg: `JSON unmarshal: error decoding partition with type "lvm": json: unknown field "subvolumes"`, + }, + "wrong-type/plain-with-btrfs": { + input: `{ + "type": "plain", + "minsize": "10 GiB", + "subvolumes": [ + { + "name": "subvols/data", + "mountpoint": "/data" + } + ] + }`, + errorMsg: `JSON unmarshal: error decoding partition with type "plain": json: unknown field "subvolumes"`, + }, + } + + for name := range testCases { + tc := testCases[name] + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + var pc blueprint.PartitionCustomization + + err := json.Unmarshal([]byte(tc.input), &pc) + if tc.errorMsg == "" { + assert.NoError(err) + assert.Equal(tc.expected, &pc) + } else { + assert.EqualError(err, tc.errorMsg) + } + }) + } +} + +func TestPartitionCustomizationUnmarshalTOML(t *testing.T) { + type testCase struct { + input string + expected *blueprint.PartitionCustomization + errorMsg string + } + + testCases := map[string]testCase{ + "nothing": { + input: "", + expected: &blueprint.PartitionCustomization{ + Type: "plain", + MinSize: 0, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "", + Label: "", + FSType: "", + }, + }, + }, + "plain": { + input: `type = "plain" + minsize = "1 GiB" + mountpoint = "/" + label = "root" + fs_type = "xfs"`, + expected: &blueprint.PartitionCustomization{ + Type: "plain", + MinSize: 1 * datasizes.GiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/", + Label: "root", + FSType: "xfs", + }, + }, + }, + "plain-with-int": { + input: `type = "plain" + minsize = 1073741824 + mountpoint = "/" + label = "root" + fs_type = "xfs"`, + expected: &blueprint.PartitionCustomization{ + Type: "plain", + MinSize: 1 * datasizes.GiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/", + Label: "root", + FSType: "xfs", + }, + }, + }, + "btrfs": { + input: `type = "btrfs" + minsize = "10 GiB" + + [[subvolumes]] + name = "subvols/root" + mountpoint = "/" + + [[subvolumes]] + name = "subvols/data" + mountpoint = "/data" + `, + expected: &blueprint.PartitionCustomization{ + Type: "btrfs", + MinSize: 10 * datasizes.GiB, + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "subvols/root", + Mountpoint: "/", + }, + { + Name: "subvols/data", + Mountpoint: "/data", + }, + }, + }, + }, + }, + "btrfs-with-int": { + input: `type = "btrfs" + minsize = 10737418240 + + [[subvolumes]] + name = "subvols/root" + mountpoint = "/" + + [[subvolumes]] + name = "subvols/data" + mountpoint = "/data" + `, + expected: &blueprint.PartitionCustomization{ + Type: "btrfs", + MinSize: 10 * datasizes.GiB, + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "subvols/root", + Mountpoint: "/", + }, + { + Name: "subvols/data", + Mountpoint: "/data", + }, + }, + }, + }, + }, + "lvm": { + input: `type = "lvm" + name = "myvg" + minsize = "99 GiB" + + [[logical_volumes]] + name = "homelv" + mountpoint = "/home" + label = "home" + fs_type = "ext4" + minsize = "2 GiB" + + [[logical_volumes]] + name = "loglv" + mountpoint = "/var/log" + label = "log" + fs_type = "xfs" + minsize = "3 GiB" + `, + expected: &blueprint.PartitionCustomization{ + Type: "lvm", + MinSize: 99 * datasizes.GiB, + VGCustomization: blueprint.VGCustomization{ + Name: "myvg", + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "homelv", + MinSize: 2 * datasizes.GiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/home", + Label: "home", + FSType: "ext4", + }, + }, + { + Name: "loglv", + MinSize: 3 * datasizes.GiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/var/log", + Label: "log", + FSType: "xfs", + }, + }, + }, + }, + }, + }, + "lvm-with-int": { + input: `type = "lvm" + name = "myvg" + minsize = 106300440576 + + [[logical_volumes]] + name = "homelv" + mountpoint = "/home" + label = "home" + fs_type = "ext4" + minsize = 2147483648 + + [[logical_volumes]] + name = "loglv" + mountpoint = "/var/log" + label = "log" + fs_type = "xfs" + minsize = 3221225472 + `, + expected: &blueprint.PartitionCustomization{ + Type: "lvm", + MinSize: 99 * datasizes.GiB, + VGCustomization: blueprint.VGCustomization{ + Name: "myvg", + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "homelv", + MinSize: 2 * datasizes.GiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/home", + Label: "home", + FSType: "ext4", + }, + }, + { + Name: "loglv", + MinSize: 3 * datasizes.GiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/var/log", + Label: "log", + FSType: "xfs", + }, + }, + }, + }, + }, + }, + "bad-type": { + input: `type = "not-a-partition-type"`, + errorMsg: "toml: line 0: TOML unmarshal: unknown partition type: not-a-partition-type", + }, + "number": { + input: `type = 5`, + errorMsg: `toml: line 0: TOML unmarshal: type must be a string, got "5" of type int64`, + }, + "negative-size": { + input: `minsize = -10 + mountpoint = "/" + fs_type = "xfs" + `, + errorMsg: "toml: line 0: TOML unmarshal: error decoding minsize for partition: cannot be negative", + }, + "wrong-type/btrfs-with-lvm": { + input: `type = "btrfs" + name = "myvg" + + [[logical_volumes]] + name = "homelv" + mountpoint = "/home" + label = "home" + fs_type = "ext4" + + [[logical_volumes]] + name = "loglv" + mountpoint = "/var/log" + label = "log" + fs_type = "xfs" + `, + errorMsg: `toml: line 0: TOML unmarshal: error decoding partition with type "btrfs": json: unknown field "logical_volumes"`, + }, + "wrong-type/plain-with-lvm": { + input: `type = "plain" + name = "myvg" + + [[logical_volumes]] + name = "homelv" + mountpoint = "/home" + label = "home" + fs_type = "ext4" + + [[logical_volumes]] + name = "loglv" + mountpoint = "/var/log" + label = "log" + fs_type = "xfs" + `, + errorMsg: `toml: line 0: TOML unmarshal: error decoding partition with type "plain": json: unknown field "logical_volumes"`, + }, + "wrong-type/lvm-with-btrfs": { + input: `type = "lvm" + minsize = "10 GiB" + + [[subvolumes]] + name = "subvols/root" + mountpoint = "/" + + [[subvolumes]] + name = "subvols/data" + mountpoint = "/data" + `, + errorMsg: `toml: line 0: TOML unmarshal: error decoding partition with type "lvm": json: unknown field "subvolumes"`, + }, + "wrong-type/plain-with-btrfs": { + input: `type = "plain" + minsize = "10 GiB" + + [[subvolumes]] + name = "subvols/root" + mountpoint = "/" + + [[subvolumes]] + name = "subvols/data" + mountpoint = "/data" + `, + errorMsg: `toml: line 0: TOML unmarshal: error decoding partition with type "plain": json: unknown field "subvolumes"`, + }, + } + + for name := range testCases { + tc := testCases[name] + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + var pc blueprint.PartitionCustomization + + err := toml.Unmarshal([]byte(tc.input), &pc) + if tc.errorMsg == "" { + assert.NoError(err) + assert.Equal(tc.expected, &pc) + } else { + assert.EqualError(err, tc.errorMsg) + } + }) + } +} From 9d72478cee46b41c0eb517f04d4380c29a5ae5cb Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 13 Nov 2024 18:43:35 +0100 Subject: [PATCH 05/13] blueprint: validate DiskCustomization Add validation functions for DiskCustomization. Three types of validation are added: - Validate(): checks for functionally invalid configurations (bad mountpoint names or invalid structures, etc). The docstring lists all properties that are validated. - ValidateLayoutConstraints(): checks for limitations we imposed ourselves as a matter of policy and support on the structure of the partition table. This includes disallowing, for example, multiple LVM volume groups, multiple btrfs volumes, or combinations of the two. While these layouts are technically valid for a partitioned disk, we currently do not support them. - CheckDiskMountpointsPolicy(): Checks that none of the mountpoints are disallowed by the given PathPolicy. Tests added for all validators. --- pkg/blueprint/partitioning_customizations.go | 260 +++++ .../partitioning_customizations_test.go | 1020 +++++++++++++++++ 2 files changed, 1280 insertions(+) diff --git a/pkg/blueprint/partitioning_customizations.go b/pkg/blueprint/partitioning_customizations.go index 578d71ca98..800063a2b9 100644 --- a/pkg/blueprint/partitioning_customizations.go +++ b/pkg/blueprint/partitioning_customizations.go @@ -3,7 +3,13 @@ package blueprint import ( "bytes" "encoding/json" + "errors" "fmt" + "path/filepath" + "slices" + "strings" + + "github.com/osbuild/images/pkg/pathpolicy" ) type DiskCustomization struct { @@ -283,3 +289,257 @@ func (v *PartitionCustomization) UnmarshalTOML(data any) error { return nil } + +// Validate checks for customization combinations that are generally not +// supported or can create conflicts, regardless of specific distro or image +// type policies. The validator ensures all of the following properties: +// - All mountpoints are valid +// - All mountpoints are unique +// - All LVM volume group names are unique +// - All LVM logical volume names are unique within a given volume group +// - All btrfs subvolume names are unique within a given btrfs volume +// - All btrfs subvolume names are valid and non-empty +// - All filesystems are valid for their mountpoints (e.g. xfs or ext4 for /boot) +// - No LVM logical volume has an invalid mountpoint (/boot or /boot/efi) +// - 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") +func (p *DiskCustomization) Validate() error { + if p == nil { + return nil + } + + mountpoints := make(map[string]bool) + vgnames := make(map[string]bool) + var errs []error + for _, part := range p.Partitions { + switch part.Type { + case "lvm": + errs = append(errs, part.validateLVM(mountpoints, vgnames)) + case "btrfs": + errs = append(errs, part.validateBtrfs(mountpoints)) + case "plain", "": + errs = append(errs, part.validatePlain(mountpoints)) + default: + errs = append(errs, fmt.Errorf("unknown partition type: %s", part.Type)) + } + } + + // will discard all nil errors + if err := errors.Join(errs...); err != nil { + return fmt.Errorf("invalid partitioning customizations:\n%w", err) + } + return nil +} + +func validateMountpoint(path string) error { + if path == "" { + return fmt.Errorf("mountpoint is empty") + } + + if !strings.HasPrefix(path, "/") { + return fmt.Errorf("mountpoint %q is not an absolute path", path) + } + + if cleanPath := filepath.Clean(path); path != cleanPath { + return fmt.Errorf("mountpoint %q is not a canonical path (did you mean %q?)", path, cleanPath) + } + + return nil +} + +// ValidateLayoutConstraints checks that at most one LVM Volume Group or btrfs +// volume is defined. Returns an error if both LVM and btrfs are set and if +// either has more than one element. +func (p *DiskCustomization) ValidateLayoutConstraints() error { + if p == nil { + return nil + } + + var btrfsVols, lvmVGs uint + for _, part := range p.Partitions { + switch part.Type { + case "lvm": + lvmVGs++ + case "btrfs": + btrfsVols++ + } + if lvmVGs > 0 && btrfsVols > 0 { + return fmt.Errorf("btrfs and lvm partitioning cannot be combined") + } + } + + if btrfsVols > 1 { + return fmt.Errorf("multiple btrfs volumes are not yet supported") + } + + if lvmVGs > 1 { + return fmt.Errorf("multiple LVM volume groups are not yet supported") + } + + return nil +} + +// Check that the fs type is valid for the mountpoint. +func validateFilesystemType(path, fstype string) error { + badfsMsg := "unsupported filesystem type for %q: %s" + switch path { + case "/boot": + switch fstype { + case "xfs", "ext4": + default: + return fmt.Errorf(badfsMsg, path, fstype) + } + case "/boot/efi": + switch fstype { + case "vfat": + default: + return fmt.Errorf(badfsMsg, path, fstype) + } + } + return nil +} + +// These mountpoints must be on a plain partition (i.e. not on LVM or btrfs). +var plainOnlyMountpoints = []string{ + "/boot", + "/boot/efi", // not allowed by our global policies, but that might change +} + +var validPlainFSTypes = []string{ + "ext4", + "vfat", + "xfs", +} + +func (p *PartitionCustomization) validatePlain(mountpoints map[string]bool) error { + if err := validateMountpoint(p.Mountpoint); err != nil { + return err + } + if mountpoints[p.Mountpoint] { + return fmt.Errorf("duplicate mountpoint %q in partitioning customizations", p.Mountpoint) + } + // TODO: allow empty fstype with default from distro + if !slices.Contains(validPlainFSTypes, p.FSType) { + return fmt.Errorf("unknown or invalid filesystem type for mountpoint %q: %s", p.Mountpoint, p.FSType) + } + if err := validateFilesystemType(p.Mountpoint, p.FSType); err != nil { + return err + } + + mountpoints[p.Mountpoint] = true + return nil +} + +func (p *PartitionCustomization) validateLVM(mountpoints, vgnames map[string]bool) error { + if p.Name != "" && vgnames[p.Name] { // VGs with no name get autogenerated names + return fmt.Errorf("duplicate LVM volume group name %q in partitioning customizations", p.Name) + } + + // check for invalid property usage + if len(p.Subvolumes) > 0 { + return fmt.Errorf("subvolumes defined for LVM volume group (partition type \"lvm\")") + } + + if p.Label != "" { + return fmt.Errorf("label %q defined for LVM volume group (partition type \"lvm\")", p.Label) + } + + vgnames[p.Name] = true + lvnames := make(map[string]bool) + for _, lv := range p.LogicalVolumes { + if lv.Name != "" && lvnames[lv.Name] { // LVs with no name get autogenerated names + return fmt.Errorf("duplicate LVM logical volume name %q in volume group %q in partitioning customizations", lv.Name, p.Name) + } + lvnames[lv.Name] = true + + if err := validateMountpoint(lv.Mountpoint); err != nil { + return fmt.Errorf("invalid logical volume customization: %w", err) + } + if mountpoints[lv.Mountpoint] { + return fmt.Errorf("duplicate mountpoint %q in partitioning customizations", lv.Mountpoint) + } + mountpoints[lv.Mountpoint] = true + + if slices.Contains(plainOnlyMountpoints, lv.Mountpoint) { + return fmt.Errorf("invalid mountpoint %q for logical volume", lv.Mountpoint) + } + + // TODO: allow empty fstype with default from distro + if !slices.Contains(validPlainFSTypes, lv.FSType) { + return fmt.Errorf("unknown or invalid filesystem type for logical volume with mountpoint %q: %s", lv.Mountpoint, lv.FSType) + } + } + return nil +} + +func (p *PartitionCustomization) validateBtrfs(mountpoints map[string]bool) error { + if p.Mountpoint != "" { + return fmt.Errorf(`"mountpoint" is not supported for btrfs volumes (only subvolumes can have mountpoints)`) + } + + if len(p.Subvolumes) == 0 { + return fmt.Errorf("btrfs volume requires subvolumes") + } + + if len(p.LogicalVolumes) > 0 { + return fmt.Errorf("LVM logical volumes defined for btrfs volume (partition type \"btrfs\")") + } + + subvolnames := make(map[string]bool) + for _, subvol := range p.Subvolumes { + if subvol.Name == "" { + return fmt.Errorf("btrfs subvolume with empty name in partitioning customizations") + } + if subvolnames[subvol.Name] { + return fmt.Errorf("duplicate btrfs subvolume name %q in partitioning customizations", subvol.Name) + } + subvolnames[subvol.Name] = true + + if err := validateMountpoint(subvol.Mountpoint); err != nil { + return fmt.Errorf("invalid btrfs subvolume customization: %w", err) + } + if mountpoints[subvol.Mountpoint] { + return fmt.Errorf("duplicate mountpoint %q in partitioning customizations", subvol.Mountpoint) + } + if slices.Contains(plainOnlyMountpoints, subvol.Mountpoint) { + return fmt.Errorf("invalid mountpoint %q for btrfs subvolume", subvol.Mountpoint) + } + mountpoints[subvol.Mountpoint] = true + } + return nil +} + +// CheckDiskMountpointsPolicy checks if the mountpoints under a [DiskCustomization] are allowed by the policy. +func CheckDiskMountpointsPolicy(partitioning *DiskCustomization, mountpointAllowList *pathpolicy.PathPolicies) error { + if partitioning == nil { + return nil + } + + // collect all mountpoints + var mountpoints []string + for _, part := range partitioning.Partitions { + if part.Mountpoint != "" { + mountpoints = append(mountpoints, part.Mountpoint) + } + for _, lv := range part.LogicalVolumes { + mountpoints = append(mountpoints, lv.Mountpoint) + } + for _, subvol := range part.Subvolumes { + mountpoints = append(mountpoints, subvol.Mountpoint) + } + } + + var errs []error + for _, mp := range mountpoints { + if err := mountpointAllowList.Check(mp); err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return fmt.Errorf("The following errors occurred while setting up custom mountpoints:\n%w", errors.Join(errs...)) + } + + return nil +} diff --git a/pkg/blueprint/partitioning_customizations_test.go b/pkg/blueprint/partitioning_customizations_test.go index 8022f49619..980190015b 100644 --- a/pkg/blueprint/partitioning_customizations_test.go +++ b/pkg/blueprint/partitioning_customizations_test.go @@ -7,9 +7,1029 @@ import ( "github.com/BurntSushi/toml" "github.com/osbuild/images/pkg/blueprint" "github.com/osbuild/images/pkg/datasizes" + "github.com/osbuild/images/pkg/pathpolicy" "github.com/stretchr/testify/assert" ) +func TestPartitioningValidation(t *testing.T) { + type testCase struct { + partitioning *blueprint.DiskCustomization + expectedMsg string + } + + testCases := map[string]testCase{ + "null": { + partitioning: nil, + expectedMsg: "", + }, + "happy-plain": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "xfs", + Mountpoint: "/data", + }, + }, + }, + }, + expectedMsg: "", + }, + "happy-plain+btrfs": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "xfs", + Mountpoint: "/data", + }, + }, + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "root", + Mountpoint: "/", + }, + }, + }, + }, + }, + }, + expectedMsg: "", + }, + "happy-plain+lvm": { + 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: "/", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "", + }, + "happy-plain-with-boot-and-efi": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "xfs", + Mountpoint: "/data", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/home", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/boot", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "vfat", + Mountpoint: "/boot/efi", + }, + }, + }, + }, + expectedMsg: "", + }, + "unhappy-plain-dupes": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "xfs", + Mountpoint: "/data", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/home", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "xfs", + Mountpoint: "/data", + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nduplicate mountpoint \"/data\" in partitioning customizations", + }, + "unhappy-plain-badfstype": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ntfs", + Mountpoint: "/home", + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nunknown or invalid filesystem type for mountpoint \"/home\": ntfs", + }, + "unhappy-plain-badfstype-boot": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "xfs", + Mountpoint: "/data", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/home", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "zfs", + Mountpoint: "/boot", + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nunknown or invalid filesystem type for mountpoint \"/boot\": zfs", + }, + "unhappy-plain-badfstype-efi": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/home", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "vfat", + Mountpoint: "/boot", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/boot/efi", + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nunsupported filesystem type for \"/boot\": vfat\nunsupported filesystem type for \"/boot/efi\": ext4", + }, + "unhappy-plain-btrfstype": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "btrfs", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/", + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\n\"mountpoint\" is not supported for btrfs volumes (only subvolumes can have mountpoints)", + }, + "unhappy-plain+btrfs-dupes": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "plain", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/data", + FSType: "xfs", + }, + }, + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "root", + Mountpoint: "/", + }, + { + Name: "home", + Mountpoint: "/home", + }, + { + Name: "data", + Mountpoint: "/data", + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nduplicate mountpoint \"/data\" in partitioning customizations", + }, + "unhappy-plain+lvm-dupes": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "plain", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/dupydupe", + FSType: "ext4", + }, + }, + { + Type: "plain", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/data", + FSType: "ext4", + }, + }, + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/home", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/dupydupe", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nduplicate mountpoint \"/dupydupe\" in partitioning customizations", + }, + "unhappy-emptymp": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nmountpoint is empty", + }, + "unhappy-relativemountpoint": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "xfs", + Mountpoint: "i-am-not-absolute", + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nmountpoint \"i-am-not-absolute\" is not an absolute path", + }, + "unhappy-badmp": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "vfat", + Mountpoint: "/home/../root", + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nmountpoint \"/home/../root\" is not a canonical path (did you mean \"/root\"?)", + }, + "unhappy-emptymp-btrfs": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "test", + Mountpoint: "/test", + }, + { + Name: "test2", + Mountpoint: "", + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\ninvalid btrfs subvolume customization: mountpoint is empty", + }, + "unhappy-relativemountpoint-btrfs": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "blorps", + Mountpoint: "blorpsmp", + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\ninvalid btrfs subvolume customization: mountpoint \"blorpsmp\" is not an absolute path", + }, + "unhappy-badmp-btrfs": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "borkage", + Mountpoint: "/home//bork", + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\ninvalid btrfs subvolume customization: mountpoint \"/home//bork\" is not a canonical path (did you mean \"/home/bork\"?)", + }, + "unhappy-emptymp-lvm": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "testlv", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + + FSType: "ext4", + Mountpoint: "/stuff", + }, + }, + { + Name: "testlv2", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\ninvalid logical volume customization: mountpoint is empty", + }, + "unhappy-relativemountpoint-lvm": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "testlv", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "xfs", + Mountpoint: "/stuff", + }, + }, + { + Name: "testlv2", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "i/like/relative/paths", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\ninvalid logical volume customization: mountpoint \"i/like/relative/paths\" is not an absolute path", + }, + "unhappy-badmp-lvm": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "testlv", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/../../../what/", + }, + }, + { + Name: "testlv2", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/test", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\ninvalid logical volume customization: mountpoint \"/../../../what/\" is not a canonical path (did you mean \"/what\"?)", + }, + "unhappy-dupesubvolname": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "root", + Mountpoint: "/", + }, + { + Name: "root", + Mountpoint: "/root", + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nduplicate btrfs subvolume name \"root\" in partitioning customizations", + }, + "unhappy-dupelvname": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "testlv", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/stuff", + }, + }, + { + Name: "testlv", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/stuff2", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nduplicate LVM logical volume name \"testlv\" in volume group \"\" in partitioning customizations", + }, + "unhappy-vg-with-subvols": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{{}}, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nsubvolumes defined for LVM volume group (partition type \"lvm\")", + }, + "unhappy-vg-with-label": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Label: "volume-group", + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nlabel \"volume-group\" defined for LVM volume group (partition type \"lvm\")", + }, + "unhappy-dupevgname": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + Name: "testvg", + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "testlv", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/test", + }, + }, + }, + }, + }, + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + Name: "testvg", + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "testlv", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "ext4", + Mountpoint: "/stuff", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nduplicate LVM volume group name \"testvg\" in partitioning customizations", + }, + "unhappy-emptyname-btrfs": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "test", + Mountpoint: "/test", + }, + { + Name: "", + Mountpoint: "/test2", + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nbtrfs subvolume with empty name in partitioning customizations", + }, + "unhappy-emptysubvols-btrfs": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{}, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nbtrfs volume requires subvolumes", + }, + "unhappy-btrfs-with-lvs": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "test", + Mountpoint: "/test2", + }, + }, + }, + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{{}}, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nLVM logical volumes defined for btrfs volume (partition type \"btrfs\")", + }, + "boot-on-lvm": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "bewt", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/boot", + }, + }, + { + Name: "testlv", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/stuff2", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\ninvalid mountpoint \"/boot\" for logical volume", + }, + "bootefi-on-lvm": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "bewtefi", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/boot/efi", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\ninvalid mountpoint \"/boot/efi\" for logical volume", + }, + "boot-on-btrfs": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "test", + Mountpoint: "/test", + }, + { + Name: "bootbootboot", + Mountpoint: "/boot", + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\ninvalid mountpoint \"/boot\" for btrfs subvolume", + }, + "bootefi-on-btrfs": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "test", + Mountpoint: "/test", + }, + { + Name: "esp", + Mountpoint: "/boot/efi", + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\ninvalid mountpoint \"/boot/efi\" for btrfs subvolume", + }, + "unhappy-btrfs-on-lvm": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + FSType: "btrfs", + Mountpoint: "/var/log", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nunknown or invalid filesystem type for logical volume with mountpoint \"/var/log\": btrfs", + }, + "unhappy-lv-notype": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/var/log", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nunknown or invalid filesystem type for logical volume with mountpoint \"/var/log\": ", + }, + "unhappy-bad-part-type": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "what", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/var/log", + }, + }, + }, + }, + }, + }, + }, + expectedMsg: "invalid partitioning customizations:\nunknown partition type: what", + }, + } + + for name := range testCases { + tc := testCases[name] + t.Run(name, func(t *testing.T) { + err := tc.partitioning.Validate() + if tc.expectedMsg != "" { + assert.EqualError(t, err, tc.expectedMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestPartitioningLayoutConstraints(t *testing.T) { + type testCase struct { + partitioning *blueprint.DiskCustomization + expectedMsg string + } + + testCases := map[string]testCase{ + "unhappy-btrfs+lvm": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "ext4", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/data", + }, + }, + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Mountpoint: "/backup", + }, + }, + }, + }, + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{Mountpoint: "/"}, + }, + }, + }, + }, + }, + }, + expectedMsg: `btrfs and lvm partitioning cannot be combined`, + }, + "unhappy-multibtrfs": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "xfs", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/data", + }, + }, + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "root", + Mountpoint: "/", + }, + }, + }, + }, + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "home", + Mountpoint: "/home", + }, + }, + }, + }, + }, + }, + expectedMsg: `multiple btrfs volumes are not yet supported`, + }, + "unhappy-multivg": { + partitioning: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "xfs", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/data", + }, + }, + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{Mountpoint: "/"}, + }, + }, + }, + }, + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{Mountpoint: "/var/log"}, + }, + }, + }, + }, + }, + }, + expectedMsg: `multiple LVM volume groups are not yet supported`, + }, + } + + for name := range testCases { + tc := testCases[name] + t.Run(name, func(t *testing.T) { + err := tc.partitioning.ValidateLayoutConstraints() + if tc.expectedMsg != "" { + assert.EqualError(t, err, tc.expectedMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestCheckDiskMountpointsPolicy(t *testing.T) { + strict := pathpolicy.NewPathPolicies(map[string]pathpolicy.PathPolicy{ + "/": {Exact: true}, + }) + + noEtc := pathpolicy.NewPathPolicies(map[string]pathpolicy.PathPolicy{ + "/etc": {Deny: true}, + }) + + disk := blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/some/stuff", + }, + }, + { + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Mountpoint: "/data/", + }, + { + Mountpoint: "/scratch", + }, + }, + }, + }, + { + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/logicalvolumes/a", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/logicalvolumes/b", + }, + }, + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/etc", + }, + }, + }, + }, + }, + }, + } + + strictErr := `The following errors occurred while setting up custom mountpoints: +path "/some/stuff" is not allowed +path "/data/" must be canonical +path "/scratch" is not allowed +path "/logicalvolumes/a" is not allowed +path "/logicalvolumes/b" is not allowed +path "/etc" is not allowed` + err := blueprint.CheckDiskMountpointsPolicy(&disk, strict) + assert.EqualError(t, err, strictErr) + + noEtcErr := `The following errors occurred while setting up custom mountpoints: +path "/data/" must be canonical +path "/etc" is not allowed` + err = blueprint.CheckDiskMountpointsPolicy(&disk, noEtc) + assert.EqualError(t, err, noEtcErr) +} + func TestPartitionCustomizationUnmarshalJSON(t *testing.T) { type testCase struct { input string From da345e6e1a54ae3783147145ccb5e18e18f46b0c Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Tue, 12 Nov 2024 18:21:32 +0100 Subject: [PATCH 06/13] disk: extract partition type ID selection into a function Define a nested map: [partition table type] -> [partition type name] -> [partition type ID] and use it from a private function getPartitionTypeIDfor() for convenient partition type ID selection. --- pkg/disk/disk.go | 30 +++++++++++++++++++ pkg/disk/partition_table.go | 51 ++++++++++---------------------- pkg/disk/partition_table_test.go | 2 +- 3 files changed, 46 insertions(+), 37 deletions(-) diff --git a/pkg/disk/disk.go b/pkg/disk/disk.go index a47f5409ab..fdfd027cba 100644 --- a/pkg/disk/disk.go +++ b/pkg/disk/disk.go @@ -72,6 +72,36 @@ const ( DosESPID = "ef00" ) +// pt type -> type -> ID mapping for convenience +var idMap = map[PartitionTableType]map[string]string{ + PT_DOS: { + "bios": DosBIOSBootID, + "boot": DosLinuxTypeID, + "data": DosLinuxTypeID, + "esp": DosESPID, + "lvm": DosLinuxTypeID, + }, + PT_GPT: { + "bios": BIOSBootPartitionGUID, + "boot": XBootLDRPartitionGUID, + "data": FilesystemDataGUID, + "esp": EFISystemPartitionGUID, + "lvm": LVMPartitionGUID, + }, +} + +func getPartitionTypeIDfor(ptType PartitionTableType, partTypeName string) (string, error) { + ptMap, ok := idMap[ptType] + if !ok { + return "", fmt.Errorf("unknown or unsupported partition table enum: %d", ptType) + } + id, ok := ptMap[partTypeName] + if !ok { + return "", fmt.Errorf("unknown or unsupported partition type name: %s", partTypeName) + } + return id, nil +} + // FSType is the filesystem type enum. // // There should always be one value for each filesystem type supported by diff --git a/pkg/disk/partition_table.go b/pkg/disk/partition_table.go index 9930f8666b..313c4b9d21 100644 --- a/pkg/disk/partition_table.go +++ b/pkg/disk/partition_table.go @@ -793,10 +793,9 @@ func (pt *PartitionTable) ensureBtrfs() error { // reset the btrfs partition size - it will be grown later part.Size = 0 - if pt.Type == PT_GPT { - part.Type = FilesystemDataGUID - } else { - part.Type = DosLinuxTypeID + part.Type, err = getPartitionTypeIDfor(pt.Type, "data") + if err != nil { + return fmt.Errorf("error converting partition table to btrfs: %w", err) } } else { @@ -979,14 +978,9 @@ func EnsureRootFilesystem(pt *PartitionTable, defaultFsType FSType) error { return fmt.Errorf("error creating root partition: %w", err) } - var partType string - switch pt.Type { - case PT_DOS: - partType = DosLinuxTypeID - case PT_GPT: - partType = FilesystemDataGUID - default: - return fmt.Errorf("error creating root partition: unknown or unsupported partition table type: %s", pt.Type) + partType, err := getPartitionTypeIDfor(pt.Type, "data") + if err != nil { + return fmt.Errorf("error creating root partition: %w", err) } rootpart := Partition{ Type: partType, @@ -1033,14 +1027,9 @@ func EnsureBootPartition(pt *PartitionTable, bootFsType FSType) error { return fmt.Errorf("error creating boot partition: %w", err) } - var partType string - switch pt.Type { - case PT_DOS: - partType = DosLinuxTypeID - case PT_GPT: - partType = XBootLDRPartitionGUID - default: - return fmt.Errorf("error creating boot partition: unknown or unsupported partition table type: %s", pt.Type) + partType, err := getPartitionTypeIDfor(pt.Type, "boot") + if err != nil { + return fmt.Errorf("error creating boot partition: %w", err) } bootPart := Partition{ Type: partType, @@ -1102,14 +1091,9 @@ func AddPartitionsForBootMode(pt *PartitionTable, bootMode platform.BootMode) er } func mkBIOSBoot(ptType PartitionTableType) (Partition, error) { - var partType string - switch ptType { - case PT_DOS: - partType = DosBIOSBootID - case PT_GPT: - partType = BIOSBootPartitionGUID - default: - return Partition{}, fmt.Errorf("error creating BIOS boot partition: unknown or unsupported partition table enum: %d", ptType) + partType, err := getPartitionTypeIDfor(ptType, "bios") + if err != nil { + return Partition{}, fmt.Errorf("error creating BIOS boot partition: %w", err) } return Partition{ Size: 1 * datasizes.MiB, @@ -1120,14 +1104,9 @@ func mkBIOSBoot(ptType PartitionTableType) (Partition, error) { } func mkESP(size uint64, ptType PartitionTableType) (Partition, error) { - var partType string - switch ptType { - case PT_DOS: - partType = DosESPID - case PT_GPT: - partType = EFISystemPartitionGUID - default: - return Partition{}, fmt.Errorf("error creating EFI system partition: unknown or unsupported partition table enum: %d", ptType) + partType, err := getPartitionTypeIDfor(ptType, "esp") + if err != nil { + return Partition{}, fmt.Errorf("error creating EFI system partition: %w", err) } return Partition{ Size: size, diff --git a/pkg/disk/partition_table_test.go b/pkg/disk/partition_table_test.go index 1f3fcb75bf..cc05922ee0 100644 --- a/pkg/disk/partition_table_test.go +++ b/pkg/disk/partition_table_test.go @@ -661,7 +661,7 @@ func TestEnsureRootFilesystemErrors(t *testing.T) { "err-no-pt-type": { pt: disk.PartitionTable{}, defaultFsType: disk.FS_EXT4, - errmsg: "error creating root partition: unknown or unsupported partition table type: ", + errmsg: "error creating root partition: unknown or unsupported partition table enum: 0", }, "err-plain": { pt: disk.PartitionTable{ From e3241025c35759014cd896be348de0f563983e16 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 13 Nov 2024 19:56:24 +0100 Subject: [PATCH 07/13] disk: NewCustomPartitionTable() function New function that creates a partition table from scratch based on the new disk customization. First, it creates any necessary partitions for the selected boot mode (BIOS boot, ESP, or both). Then it determines if a /boot partition is needed based on the partitions and mountpoints defined in the disk customizations. Then, it iterates through partitions and creates each one in the order its defined. Finally, it creates a root filesystem if one wasn't already defined, resizes any necessary volumes, relayouts the table (setting all partition starts and offsets), and generates UUIDs all entities. Because of the new way the boot partition is handled, the EnsureBootPartition() function was changed to always add a boot partition and was renamed to AddBootPartition() to match the new behaviour. The tests have been updated accordingly. --- pkg/disk/partition_table.go | 302 +++++++++++++++++++++++++++++-- pkg/disk/partition_table_test.go | 106 +---------- 2 files changed, 289 insertions(+), 119 deletions(-) diff --git a/pkg/disk/partition_table.go b/pkg/disk/partition_table.go index 313c4b9d21..9bed8eb611 100644 --- a/pkg/disk/partition_table.go +++ b/pkg/disk/partition_table.go @@ -996,27 +996,16 @@ func EnsureRootFilesystem(pt *PartitionTable, defaultFsType FSType) error { return nil } -// EnsureBootPartition creates a boot partition if one does not already exist. -// The function will append the boot partition to the end of the existing -// partition table therefore it is best to call this function early to put boot -// near the front (as is conventional). -func EnsureBootPartition(pt *PartitionTable, bootFsType FSType) error { +// AddBootPartition creates a boot partition. The function will append the boot +// partition to the end of the existing partition table therefore it is best to +// call this function early to put boot near the front (as is conventional). +func AddBootPartition(pt *PartitionTable, bootFsType FSType) error { // collect all labels to avoid conflicts labels := make(map[string]bool) - var foundBoot bool _ = pt.ForEachMountable(func(mnt Mountable, path []Entity) error { - if mnt.GetMountpoint() == "/boot" { - foundBoot = true - return nil - } - labels[mnt.GetFSSpec().Label] = true return nil }) - if foundBoot { - // nothing to do - return nil - } if bootFsType == FS_NONE { return fmt.Errorf("error creating boot partition: no filesystem type") @@ -1123,3 +1112,286 @@ func mkESP(size uint64, ptType PartitionTableType) (Partition, error) { }, }, nil } + +type CustomPartitionTableOptions struct { + // PartitionTableType must be either "dos" or "gpt". Defaults to "gpt". + PartitionTableType PartitionTableType + + // BootMode determines the types of boot-related partitions that are + // automatically added, BIOS boot (legacy), ESP (UEFI), or both (hybrid). + // If none, no boot-related partitions are created. + BootMode platform.BootMode + + // DefaultFSType determines the filesystem type for automatically created + // filesystems and custom mountpoints that don't specify a type. + // None is only valid if no partitions are created and all mountpoints + // partitions specify a type. + // The default type is also used for the automatically created /boot + // filesystem if it is a supported type for that fileystem. If it is not, + // xfs is used as a fallback. + DefaultFSType FSType + + // RequiredMinSizes defines a map of minimum sizes for specific + // directories. These indirectly control the minimum sizes of partitions. A + // directory with a required size will set the minimum size of the + // partition with the mountpoint that contains the directory. Additional + // directory requirements are additive, meaning the minimum size for a + // mountpoint's partition is the sum of all the required directory sizes it + // will contain. + RequiredMinSizes map[string]uint64 +} + +// Returns the default filesystem type if the fstype is empty. If both are +// empty/none, returns an error. +func (options *CustomPartitionTableOptions) getfstype(fstype string) (string, error) { + if fstype != "" { + return fstype, nil + } + + if options.DefaultFSType == FS_NONE { + return "", fmt.Errorf("no filesystem type defined and no default set") + } + + return options.DefaultFSType.String(), nil +} + +// NewCustomPartitionTable creates a partition table based almost entirely on the disk customizations from a blueprint. +func NewCustomPartitionTable(customizations *blueprint.DiskCustomization, options *CustomPartitionTableOptions, rng *rand.Rand) (*PartitionTable, error) { + if options == nil { + // init options with defaults + options = &CustomPartitionTableOptions{ + PartitionTableType: PT_GPT, + } + } + + if customizations == nil { + customizations = &blueprint.DiskCustomization{} + } + + errPrefix := "error generating partition table:" + + // validate the partitioning customizations before using them + if err := customizations.Validate(); err != nil { + return nil, fmt.Errorf("%s %w", errPrefix, err) + } + + pt := &PartitionTable{} + + // TODO: Handle partition table type in customizations + switch options.PartitionTableType { + case PT_GPT, PT_DOS: + pt.Type = options.PartitionTableType + case PT_NONE: + // default to "gpt" + pt.Type = PT_GPT + default: + return nil, fmt.Errorf("%s invalid partition table type enum value: %d", errPrefix, options.PartitionTableType) + } + + // TODO: switch to ensure ESP in case customizations already include it + if err := AddPartitionsForBootMode(pt, options.BootMode); err != nil { + return nil, fmt.Errorf("%s %w", errPrefix, err) + } + + // The boot type will be the default only if it's a supported filesystem + // type for /boot (ext4 or xfs). Otherwise, we default to xfs. + // FS_NONE also falls back to xfs. + var bootFsType FSType + switch options.DefaultFSType { + case FS_EXT4, FS_XFS: + bootFsType = options.DefaultFSType + default: + bootFsType = FS_XFS + } + + if needsBoot(customizations) { + // we need a /boot partition to boot LVM or Btrfs, create boot + // partition if it does not already exist + if err := AddBootPartition(pt, bootFsType); err != nil { + return nil, fmt.Errorf("%s %w", errPrefix, err) + } + } + + for _, part := range customizations.Partitions { + switch part.Type { + case "plain", "": + if err := addPlainPartition(pt, part, options); err != nil { + return nil, fmt.Errorf("%s %w", errPrefix, err) + } + case "lvm": + if err := addLVMPartition(pt, part, options); err != nil { + return nil, fmt.Errorf("%s %w", errPrefix, err) + } + case "btrfs": + addBtrfsPartition(pt, part) + default: + return nil, fmt.Errorf("%s invalid partition type: %s", errPrefix, part.Type) + } + } + + if err := EnsureRootFilesystem(pt, options.DefaultFSType); err != nil { + return nil, fmt.Errorf("%s %w", errPrefix, err) + } + + if len(options.RequiredMinSizes) != 0 { + pt.EnsureDirectorySizes(options.RequiredMinSizes) + } + + pt.relayout(customizations.MinSize) + pt.GenerateUUIDs(rng) + + return pt, nil +} + +func addPlainPartition(pt *PartitionTable, partition blueprint.PartitionCustomization, options *CustomPartitionTableOptions) error { + fstype, err := options.getfstype(partition.FSType) + 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" { + typeName = "boot" + } + partType, err := getPartitionTypeIDfor(pt.Type, typeName) + if err != nil { + return fmt.Errorf("error creating root partition: %w", err) + } + newpart := Partition{ + Type: partType, + Bootable: false, + Size: partition.MinSize, + Payload: &Filesystem{ + Type: fstype, + Label: partition.Label, + Mountpoint: partition.Mountpoint, + FSTabOptions: "defaults", // TODO: add customization + }, + } + pt.Partitions = append(pt.Partitions, newpart) + return nil +} + +func addLVMPartition(pt *PartitionTable, partition blueprint.PartitionCustomization, options *CustomPartitionTableOptions) error { + vgname := partition.Name + if vgname == "" { + // count existing volume groups and generate unique name + existing := make(map[string]bool) + for _, part := range pt.Partitions { + vg, ok := part.Payload.(*LVMVolumeGroup) + if !ok { + continue + } + existing[vg.Name] = true + } + // unlike other unique name generation cases, here we want the first + // name to have the 00 suffix, so we add the base to the existing set + base := "vg" + existing[base] = true + uniqueName, err := genUniqueString(base, existing) + if err != nil { + return fmt.Errorf("error creating volume group: %w", err) + } + vgname = uniqueName + } + + newvg := &LVMVolumeGroup{ + Name: vgname, + Description: "created via lvm2 and osbuild", + } + for _, lv := range partition.LogicalVolumes { + fstype, err := options.getfstype(lv.FSType) + 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 + } + 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) + } + } + + // create partition for volume group + newpart := Partition{ + Type: LVMPartitionGUID, + Size: partition.MinSize, + Bootable: false, + Payload: newvg, + } + pt.Partitions = append(pt.Partitions, newpart) + return nil +} + +func addBtrfsPartition(pt *PartitionTable, partition blueprint.PartitionCustomization) { + subvols := make([]BtrfsSubvolume, len(partition.Subvolumes)) + for idx, subvol := range partition.Subvolumes { + newsubvol := BtrfsSubvolume{ + Name: subvol.Name, + Mountpoint: subvol.Mountpoint, + } + subvols[idx] = newsubvol + } + + newvol := &Btrfs{ + Subvolumes: subvols, + } + + // create partition for btrfs volume + newpart := Partition{ + Type: FilesystemDataGUID, + Bootable: false, + Payload: newvol, + Size: partition.MinSize, + } + + pt.Partitions = append(pt.Partitions, newpart) +} + +// Determine if a boot partition is needed based on the customizations. A boot +// partition is needed if any of the following conditions apply: +// - / is on LVM or btrfs and /boot is not defined. +// - / is not defined and btrfs or lvm volumes are defined. +// +// In the second case, a root partition will be created automatically on either +// btrfs or lvm. +func needsBoot(disk *blueprint.DiskCustomization) bool { + if disk == nil { + return false + } + + var foundBtrfsOrLVM bool + for _, part := range disk.Partitions { + switch part.Type { + case "plain", "": + if part.Mountpoint == "/" { + return false + } + if part.Mountpoint == "/boot" { + return false + } + case "lvm": + foundBtrfsOrLVM = true + // check if any of the LVs is root + for _, lv := range part.LogicalVolumes { + if lv.Mountpoint == "/" { + return true + } + } + case "btrfs": + foundBtrfsOrLVM = true + // check if any of the subvols is root + for _, subvol := range part.Subvolumes { + if subvol.Mountpoint == "/" { + return true + } + } + default: + // NOTE: invalid types should be validated elsewhere + } + } + return foundBtrfsOrLVM +} diff --git a/pkg/disk/partition_table_test.go b/pkg/disk/partition_table_test.go index cc05922ee0..4558b01b2a 100644 --- a/pkg/disk/partition_table_test.go +++ b/pkg/disk/partition_table_test.go @@ -722,7 +722,7 @@ func TestEnsureRootFilesystemErrors(t *testing.T) { } } -func TestEnsureBootPartition(t *testing.T) { +func TestAddBootPartition(t *testing.T) { type testCase struct { pt disk.PartitionTable expected disk.PartitionTable @@ -859,108 +859,6 @@ func TestEnsureBootPartition(t *testing.T) { }, }, }, - "noop": { - pt: disk.PartitionTable{ - Partitions: []disk.Partition{ - { - Start: 0, - Size: 0, - Type: disk.XBootLDRPartitionGUID, - Bootable: false, - UUID: "", - Payload: &disk.Filesystem{ - Type: "ext4", - Label: "boot", - Mountpoint: "/boot", - FSTabOptions: "defaults", - }, - }, - { - Payload: &disk.LVMVolumeGroup{ - Name: "testvg", - LogicalVolumes: []disk.LVMLogicalVolume{ - { - Name: "varloglv", - Payload: &disk.Filesystem{ - Label: "var-log", - Type: "xfs", - Mountpoint: "/var/log", - }, - }, - { - Name: "datalv", - Payload: &disk.Filesystem{ - Label: "data", - Type: "ext4", - Mountpoint: "/data", - FSTabOptions: "defaults", - }, - }, - { - Name: "rootlv", - Payload: &disk.Filesystem{ - Label: "root", - Type: "ext4", - Mountpoint: "/", - FSTabOptions: "defaults", - }, - }, - }, - }, - }, - }, - }, - expected: disk.PartitionTable{ - Partitions: []disk.Partition{ - { - Start: 0, - Size: 0, - Type: disk.XBootLDRPartitionGUID, - Bootable: false, - UUID: "", - Payload: &disk.Filesystem{ - Type: "ext4", - Label: "boot", - Mountpoint: "/boot", - FSTabOptions: "defaults", - }, - }, - { - Payload: &disk.LVMVolumeGroup{ - Name: "testvg", - LogicalVolumes: []disk.LVMLogicalVolume{ - { - Name: "varloglv", - Payload: &disk.Filesystem{ - Label: "var-log", - Type: "xfs", - Mountpoint: "/var/log", - }, - }, - { - Name: "datalv", - Payload: &disk.Filesystem{ - Label: "data", - Type: "ext4", - Mountpoint: "/data", - FSTabOptions: "defaults", - }, - }, - { - Name: "rootlv", - Payload: &disk.Filesystem{ - Label: "root", - Type: "ext4", - Mountpoint: "/", - FSTabOptions: "defaults", - }, - }, - }, - }, - }, - }, - }, - }, "label-collision": { pt: disk.PartitionTable{ Type: disk.PT_GPT, @@ -1014,7 +912,7 @@ func TestEnsureBootPartition(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) pt := tc.pt - err := disk.EnsureBootPartition(&pt, tc.fsType) + err := disk.AddBootPartition(&pt, tc.fsType) if tc.errmsg == "" { assert.NoError(err) assert.Equal(tc.expected, pt) From 29542a19ccec42778fcd3d06393d8606e96f5cb1 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 13 Nov 2024 20:12:01 +0100 Subject: [PATCH 08/13] disk: add tests for NewCustomPartitionTable() --- pkg/disk/partition_table_test.go | 1011 ++++++++++++++++++++++++++++++ 1 file changed, 1011 insertions(+) diff --git a/pkg/disk/partition_table_test.go b/pkg/disk/partition_table_test.go index 4558b01b2a..2344ccbd17 100644 --- a/pkg/disk/partition_table_test.go +++ b/pkg/disk/partition_table_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/osbuild/images/internal/testdisk" + "github.com/osbuild/images/pkg/blueprint" "github.com/osbuild/images/pkg/datasizes" "github.com/osbuild/images/pkg/disk" "github.com/osbuild/images/pkg/platform" @@ -1119,3 +1120,1013 @@ func TestAddPartitionsForBootMode(t *testing.T) { }) } } + +func TestNewCustomPartitionTable(t *testing.T) { + type testCase struct { + customizations *blueprint.DiskCustomization + options *disk.CustomPartitionTableOptions + expected *disk.PartitionTable + } + + testCases := map[string]testCase{ + "dos-hybrid": { + customizations: nil, + options: &disk.CustomPartitionTableOptions{ + DefaultFSType: disk.FS_XFS, + BootMode: platform.BOOT_HYBRID, + PartitionTableType: disk.PT_DOS, + }, + expected: &disk.PartitionTable{ + Type: disk.PT_DOS, + Size: 202 * datasizes.MiB, + UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", + Partitions: []disk.Partition{ + { + Start: 1 * datasizes.MiB, // header + Bootable: true, + Size: 1 * datasizes.MiB, + 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: 0, + Type: disk.DosLinuxTypeID, + Bootable: false, + Payload: &disk.Filesystem{ + Type: "xfs", + Label: "root", + Mountpoint: "/", + FSTabOptions: "defaults", + UUID: "6e4ff95f-f662-45ee-a82a-bdf44a2d0b75", + }, + }, + }, + }, + }, + "plain": { + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + MinSize: 20 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/data", + Label: "data", + FSType: "ext4", + }, + }, + }, + }, + options: &disk.CustomPartitionTableOptions{ + DefaultFSType: disk.FS_XFS, + BootMode: platform.BOOT_HYBRID, + PartitionTableType: disk.PT_DOS, + }, + expected: &disk.PartitionTable{ + Type: disk.PT_DOS, + Size: 222 * 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: 0, + Type: disk.DosLinuxTypeID, + UUID: "", // partitions on dos PTs don't have UUIDs + Bootable: false, + Payload: &disk.Filesystem{ + Type: "xfs", + Label: "root", + Mountpoint: "/", + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + }, + }, + }, + "plain-legacy": { + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + MinSize: 20 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/data", + Label: "data", + FSType: "ext4", + }, + }, + }, + }, + options: &disk.CustomPartitionTableOptions{ + DefaultFSType: disk.FS_XFS, + BootMode: platform.BOOT_LEGACY, + PartitionTableType: disk.PT_DOS, + }, + expected: &disk.PartitionTable{ + Type: disk.PT_DOS, + Size: 22 * 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: 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: 22 * 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: "fb180daf-48a7-4ee0-b10d-394651850fd4", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + }, + }, + }, + "plain-uefi": { + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + MinSize: 20 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/data", + Label: "data", + FSType: "ext4", + }, + }, + }, + }, + options: &disk.CustomPartitionTableOptions{ + DefaultFSType: disk.FS_XFS, + BootMode: platform.BOOT_UEFI, + PartitionTableType: disk.PT_DOS, + }, + expected: &disk.PartitionTable{ + Type: disk.PT_DOS, + Size: 221 * datasizes.MiB, + UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", + Partitions: []disk.Partition{ + { + Start: 1 * 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: 201 * 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: 221 * 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: "fb180daf-48a7-4ee0-b10d-394651850fd4", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + }, + }, + }, + "plain-reqsizes": { + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + MinSize: 20 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/data", + Label: "data", + FSType: "ext4", + }, + }, + }, + }, + options: &disk.CustomPartitionTableOptions{ + DefaultFSType: disk.FS_XFS, + BootMode: platform.BOOT_HYBRID, + RequiredMinSizes: map[string]uint64{"/": 1 * datasizes.GiB, "/usr": 2 * datasizes.GiB}, // the default for our distro definitions + PartitionTableType: disk.PT_DOS, + }, + expected: &disk.PartitionTable{ + Type: disk.PT_DOS, + Size: 222*datasizes.MiB + 3*datasizes.GiB, + 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: 3 * datasizes.GiB, + Type: disk.DosLinuxTypeID, + UUID: "", // partitions on dos PTs don't have UUIDs + Bootable: false, + Payload: &disk.Filesystem{ + Type: "xfs", + Label: "root", + Mountpoint: "/", + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + }, + }, + }, + "plain+": { + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + MinSize: 50 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/", + Label: "root", + FSType: "xfs", + }, + }, + { + MinSize: 20 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/home", + Label: "home", + FSType: "ext4", + }, + }, + }, + }, + options: &disk.CustomPartitionTableOptions{ + DefaultFSType: disk.FS_EXT4, + BootMode: platform.BOOT_HYBRID, + PartitionTableType: disk.PT_GPT, + RequiredMinSizes: map[string]uint64{"/": 3 * datasizes.GiB}, + }, + expected: &disk.PartitionTable{ + Type: disk.PT_GPT, + Size: 222*datasizes.MiB + 3*datasizes.GiB + datasizes.MiB, // start + size of last partition + footer + + UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", + Partitions: []disk.Partition{ + { + Start: 1 * datasizes.MiB, // header + Size: 1 * datasizes.MiB, + Bootable: true, + Type: disk.BIOSBootPartitionGUID, + UUID: disk.BIOSBootPartitionUUID, + }, + { + Start: 2 * datasizes.MiB, + Size: 200 * datasizes.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, + }, + }, + // root is aligned to the end but not reindexed + { + Start: 222 * 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", + Bootable: false, + Payload: &disk.Filesystem{ + Type: "xfs", + Label: "root", + Mountpoint: "/", + FSTabOptions: "defaults", + UUID: "6e4ff95f-f662-45ee-a82a-bdf44a2d0b75", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + Start: 202 * datasizes.MiB, + Size: 20 * datasizes.MiB, + Type: disk.FilesystemDataGUID, + UUID: "e2d3d0d0-de6b-48f9-b44c-e85ff044c6b1", + Bootable: false, + Payload: &disk.Filesystem{ + Type: "ext4", + Label: "home", + Mountpoint: "/home", + FSTabOptions: "defaults", + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + }, + }, + }, + "lvm": { + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + MinSize: 100 * datasizes.MiB, + VGCustomization: blueprint.VGCustomization{ + Name: "testvg", + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "varloglv", + MinSize: 10 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/var/log", + Label: "var-log", + FSType: "xfs", + }, + }, + { + Name: "rootlv", + MinSize: 50 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/", + Label: "root", + FSType: "xfs", + }, + }, + { // unnamed + untyped logical volume + MinSize: 100 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/data", + Label: "data", + FSType: "ext4", // TODO: remove when we reintroduce the default fs + }, + }, + }, + }, + }, + }, + }, + options: &disk.CustomPartitionTableOptions{ + DefaultFSType: disk.FS_EXT4, + BootMode: platform.BOOT_HYBRID, + }, + 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 + Partitions: []disk.Partition{ + { + Start: 1 * datasizes.MiB, // header + Size: 1 * datasizes.MiB, + Bootable: true, + Type: disk.BIOSBootPartitionGUID, + UUID: disk.BIOSBootPartitionUUID, + }, + { + Start: 2 * datasizes.MiB, + Size: 200 * datasizes.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, + }, + }, + { + Start: 202 * datasizes.MiB, + Size: 512 * datasizes.MiB, + Type: disk.XBootLDRPartitionGUID, + UUID: "f83b8e88-3bbf-457a-ab99-c5b252c7429c", + Bootable: false, + Payload: &disk.Filesystem{ + Type: "ext4", + Label: "boot", + Mountpoint: "/boot", + FSTabOptions: "defaults", + UUID: "6e4ff95f-f662-45ee-a82a-bdf44a2d0b75", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + 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 + Type: disk.LVMPartitionGUID, + UUID: "32f3a8ae-b79e-4856-b659-c18f0dcecc77", + Bootable: false, + Payload: &disk.LVMVolumeGroup{ + Name: "testvg", + Description: "created via lvm2 and osbuild", + LogicalVolumes: []disk.LVMLogicalVolume{ + { + Name: "varloglv", + Size: 12 * datasizes.MiB, // rounded up to next extent (4 MiB) + Payload: &disk.Filesystem{ + Label: "var-log", + Type: "xfs", + Mountpoint: "/var/log", + FSTabOptions: "defaults", + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", + }, + }, + { + Name: "rootlv", + Size: 52 * datasizes.MiB, // rounded up to the next extent (4 MiB) + Payload: &disk.Filesystem{ + Label: "root", + Type: "xfs", + Mountpoint: "/", + FSTabOptions: "defaults", + UUID: "a178892e-e285-4ce1-9114-55780875d64e", + }, + }, + { + Name: "datalv", + Size: 100 * datasizes.MiB, + Payload: &disk.Filesystem{ + Label: "data", + Type: "ext4", // the defaultType + Mountpoint: "/data", + FSTabOptions: "defaults", + UUID: "e2d3d0d0-de6b-48f9-b44c-e85ff044c6b1", + }, + }, + }, + }, + }, + }, + }, + }, + "lvm-multivg": { + // two volume groups, both unnamed, and no root lv defined + // NOTE: this is currently not supported by customizations but the + // PT creation function can handle it + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + MinSize: 100 * datasizes.MiB, + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "varloglv", + MinSize: 10 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/var/log", + Label: "var-log", + FSType: "xfs", + }, + }, + }, + }, + }, + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + LogicalVolumes: []blueprint.LVCustomization{ + { // unnamed + untyped logical volume + MinSize: 100 * datasizes.MiB, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/data", + Label: "data", + FSType: "ext4", // TODO: remove when we reintroduce the default fs + }, + }, + }, + }, + }, + }, + }, + options: &disk.CustomPartitionTableOptions{ + DefaultFSType: disk.FS_EXT4, + BootMode: platform.BOOT_HYBRID, + RequiredMinSizes: map[string]uint64{"/": 3 * datasizes.GiB}, + }, + expected: &disk.PartitionTable{ + Type: disk.PT_GPT, // default when unspecified + UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", + Size: 818*datasizes.MiB + 16*datasizes.MiB + 3*datasizes.GiB + datasizes.MiB, // start + size of last partition (VG) + footer + Partitions: []disk.Partition{ + { + Start: 1 * datasizes.MiB, // header + Size: 1 * datasizes.MiB, + Bootable: true, + Type: disk.BIOSBootPartitionGUID, + UUID: disk.BIOSBootPartitionUUID, + }, + { + Start: 2 * datasizes.MiB, + Size: 200 * datasizes.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, + }, + }, + { + Start: 202 * datasizes.MiB, + Size: 512 * datasizes.MiB, + Type: disk.XBootLDRPartitionGUID, + UUID: "f83b8e88-3bbf-457a-ab99-c5b252c7429c", + Bootable: false, + Payload: &disk.Filesystem{ + Type: "ext4", + Label: "boot", + Mountpoint: "/boot", + FSTabOptions: "defaults", + UUID: "6e4ff95f-f662-45ee-a82a-bdf44a2d0b75", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + Start: 818 * datasizes.MiB, // the root vg is moved to the end of the partition table by relayout() + Size: 3*datasizes.GiB + 16*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", + Bootable: false, + Payload: &disk.LVMVolumeGroup{ + Name: "vg00", + Description: "created via lvm2 and osbuild", + LogicalVolumes: []disk.LVMLogicalVolume{ + { + Name: "varloglv", + Size: 12 * datasizes.MiB, // rounded up to next extent (4 MiB) + Payload: &disk.Filesystem{ + Label: "var-log", + Type: "xfs", + Mountpoint: "/var/log", + FSTabOptions: "defaults", + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", + }, + }, + { + Name: "rootlv", + Size: 3 * datasizes.GiB, + Payload: &disk.Filesystem{ + Label: "root", + Type: "ext4", // the defaultType + Mountpoint: "/", + FSTabOptions: "defaults", + UUID: "a178892e-e285-4ce1-9114-55780875d64e", + }, + }, + }, + }, + }, + { + Start: 714 * datasizes.MiB, + Size: 104 * datasizes.MiB, // 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: "c75e7a81-bfde-475f-a7cf-e242cf3cc354", + Bootable: false, + Payload: &disk.LVMVolumeGroup{ + Name: "vg01", + Description: "created via lvm2 and osbuild", + LogicalVolumes: []disk.LVMLogicalVolume{ + { + Name: "datalv", + Size: 100 * datasizes.MiB, + Payload: &disk.Filesystem{ + Label: "data", + Type: "ext4", // the defaultType + Mountpoint: "/data", + FSTabOptions: "defaults", + UUID: "e2d3d0d0-de6b-48f9-b44c-e85ff044c6b1", + }, + }, + }, + }, + }, + }, + }, + }, + "btrfs": { + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "btrfs", + MinSize: 230 * datasizes.MiB, + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "subvol/root", + Mountpoint: "/", + }, + { + Name: "subvol/home", + Mountpoint: "/home", + }, + { + Name: "subvol/varlog", + Mountpoint: "/var/log", + }, + }, + }, + }, + }, + }, + options: &disk.CustomPartitionTableOptions{ + DefaultFSType: disk.FS_EXT4, + BootMode: platform.BOOT_HYBRID, + PartitionTableType: disk.PT_GPT, + }, + expected: &disk.PartitionTable{ + Type: disk.PT_GPT, + Size: 714*datasizes.MiB + 230*datasizes.MiB + datasizes.MiB, // start + size of last partition + footer + UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", + Partitions: []disk.Partition{ + { + Start: 1 * datasizes.MiB, // header + Size: 1 * datasizes.MiB, + Bootable: true, + Type: disk.BIOSBootPartitionGUID, + UUID: disk.BIOSBootPartitionUUID, + }, + { + Start: 2 * datasizes.MiB, // header + Size: 200 * datasizes.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, + }, + }, + { + Start: 202 * datasizes.MiB, + Size: 512 * datasizes.MiB, + Type: disk.XBootLDRPartitionGUID, + UUID: "a178892e-e285-4ce1-9114-55780875d64e", + Bootable: false, + Payload: &disk.Filesystem{ + Type: "ext4", + Label: "boot", + Mountpoint: "/boot", + UUID: "6e4ff95f-f662-45ee-a82a-bdf44a2d0b75", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + Start: 714 * 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", + Bootable: false, + Payload: &disk.Btrfs{ + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", + Subvolumes: []disk.BtrfsSubvolume{ + { + Name: "subvol/root", + Mountpoint: "/", + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", // same as volume UUID + }, + { + Name: "subvol/home", + Mountpoint: "/home", + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", // same as volume UUID + }, + { + Name: "subvol/varlog", + Mountpoint: "/var/log", + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", // same as volume UUID + }, + }, + }, + }, + }, + }, + }, + "autorootbtrfs": { + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "btrfs", + BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ + Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ + { + Name: "data", + Mountpoint: "/data", + }, + }, + }, + }, + }, + }, + options: nil, + expected: &disk.PartitionTable{ + Type: disk.PT_GPT, + Size: 514 * datasizes.MiB, + UUID: "0194fdc2-fa2f-4cc0-81d3-ff12045b73c8", + Partitions: []disk.Partition{ + { + Start: 1 * datasizes.MiB, + Size: 512 * datasizes.MiB, + Type: disk.XBootLDRPartitionGUID, + UUID: "a178892e-e285-4ce1-9114-55780875d64e", + Bootable: false, + Payload: &disk.Filesystem{ + Type: "xfs", + Label: "boot", + Mountpoint: "/boot", + UUID: "6e4ff95f-f662-45ee-a82a-bdf44a2d0b75", + FSTabOptions: "defaults", + FSTabFreq: 0, + FSTabPassNo: 0, + }, + }, + { + Start: 513 * datasizes.MiB, + Size: 1*datasizes.MiB - (disk.DefaultSectorSize + (128 * 128)), + + Type: disk.FilesystemDataGUID, + UUID: "e2d3d0d0-de6b-48f9-b44c-e85ff044c6b1", + Bootable: false, + Payload: &disk.Btrfs{ + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", + Subvolumes: []disk.BtrfsSubvolume{ + { + Name: "data", + Mountpoint: "/data", + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", + }, + { + Name: "root", + Mountpoint: "/", + UUID: "fb180daf-48a7-4ee0-b10d-394651850fd4", + }, + }, + }, + }, + }, + }, + }, + } + + for name := range testCases { + tc := testCases[name] + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + // Initialise rng for each test separately, otherwise test run + // order will affect results + /* #nosec G404 */ + rnd := rand.New(rand.NewSource(0)) + pt, err := disk.NewCustomPartitionTable(tc.customizations, tc.options, rnd) + + assert.NoError(err) + assert.Equal(tc.expected, pt) + }) + } + +} + +func TestNewCustomPartitionTableErrors(t *testing.T) { + type testCase struct { + customizations *blueprint.DiskCustomization + options *disk.CustomPartitionTableOptions + errmsg string + } + + testCases := map[string]testCase{ + "autoroot-notype": { + customizations: nil, + options: nil, + errmsg: "error generating partition table: error creating root partition: no default filesystem type", + }, + "autorootlv-notype": { + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + Name: "vg-without-root", + }, + }, + }, + }, + options: nil, + errmsg: "error generating partition table: error creating root logical volume: no default filesystem type", + }, + "notype-nodefault": { + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/", + }, + }, + }, + }, + options: nil, + // NOTE: this error message will change when we allow empty fs_type + // in customizations but with a requirement to define a default + errmsg: "error generating partition table: invalid partitioning customizations:\nunknown or invalid filesystem type for mountpoint \"/\": ", + }, + "lvm-notype-nodefault": { + customizations: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + Type: "lvm", + VGCustomization: blueprint.VGCustomization{ + Name: "rootvg", + LogicalVolumes: []blueprint.LVCustomization{ + { + Name: "rootlv", + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/", + }, + }, + }, + }, + }, + }, + }, + options: nil, + // NOTE: this error message will change when we allow empty fs_type + // in customizations but with a requirement to define a default + errmsg: "error generating partition table: invalid partitioning customizations:\nunknown or invalid filesystem type for logical volume with mountpoint \"/\": ", + }, + "bad-pt-type": { + options: &disk.CustomPartitionTableOptions{ + PartitionTableType: 100, + }, + errmsg: `error generating partition table: invalid partition table type enum value: 100`, + }, + } + + // we don't care about the rng for error tests + /* #nosec G404 */ + rnd := rand.New(rand.NewSource(0)) + + for name := range testCases { + tc := testCases[name] + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + _, err := disk.NewCustomPartitionTable(tc.customizations, tc.options, rnd) + assert.EqualError(err, tc.errmsg) + }) + } +} From 21fc1417741cc93f9ee2cb547a482325aefecac0 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 11 Sep 2024 18:27:31 +0200 Subject: [PATCH 09/13] distro/fedora: pass all customizations to getPartitionTable() Preparing the function to use the new PartitioningCustomization when set. --- pkg/distro/fedora/images.go | 6 +++--- pkg/distro/fedora/imagetype.go | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/distro/fedora/images.go b/pkg/distro/fedora/images.go index c40c42e943..51e117cb95 100644 --- a/pkg/distro/fedora/images.go +++ b/pkg/distro/fedora/images.go @@ -329,7 +329,7 @@ func diskImage(workload workload.Workload, img.InstallWeakDeps = common.ToPtr(false) } // TODO: move generation into LiveImage - pt, err := t.getPartitionTable(bp.Customizations.GetFilesystems(), options, rng) + pt, err := t.getPartitionTable(bp.Customizations, options, rng) if err != nil { return nil, err } @@ -700,7 +700,7 @@ func iotImage(workload workload.Workload, img.OSName = "fedora-iot" // TODO: move generation into LiveImage - pt, err := t.getPartitionTable(customizations.GetFilesystems(), options, rng) + pt, err := t.getPartitionTable(customizations, options, rng) if err != nil { return nil, err } @@ -741,7 +741,7 @@ func iotSimplifiedInstallerImage(workload workload.Workload, rawImg.OSName = "fedora" // TODO: move generation into LiveImage - pt, err := t.getPartitionTable(customizations.GetFilesystems(), options, rng) + pt, err := t.getPartitionTable(customizations, options, rng) if err != nil { return nil, err } diff --git a/pkg/distro/fedora/imagetype.go b/pkg/distro/fedora/imagetype.go index 8978e063ba..3f08be3963 100644 --- a/pkg/distro/fedora/imagetype.go +++ b/pkg/distro/fedora/imagetype.go @@ -138,7 +138,7 @@ func (t *imageType) BootMode() platform.BootMode { } func (t *imageType) getPartitionTable( - mountpoints []blueprint.FilesystemCustomization, + customizations *blueprint.Customizations, options distro.ImageOptions, rng *rand.Rand, ) (*disk.PartitionTable, error) { @@ -160,6 +160,7 @@ func (t *imageType) getPartitionTable( partitioningMode = disk.AutoLVMPartitioningMode } + mountpoints := customizations.GetFilesystems() return disk.NewPartitionTable(&basePartitionTable, mountpoints, imageSize, partitioningMode, t.requiredPartitionSizes, rng) } From 58944b14d984261d582f8e4a77d04d9e8d8c0489 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 11 Sep 2024 18:32:59 +0200 Subject: [PATCH 10/13] distro/fedora: enable the new partitioning functionality --- pkg/distro/fedora/imagetype.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/distro/fedora/imagetype.go b/pkg/distro/fedora/imagetype.go index 3f08be3963..1abddad678 100644 --- a/pkg/distro/fedora/imagetype.go +++ b/pkg/distro/fedora/imagetype.go @@ -148,6 +148,24 @@ func (t *imageType) getPartitionTable( } imageSize := t.Size(options.Size) + partitioning := customizations.GetPartitioning() + if partitioning != nil { + // Use the new custom partition table to create a PT fully based on the user's customizations. + // This overrides FilesystemCustomizations, but we should never have both defined. + if options.Size > 0 { + // user specified a size on the command line, so let's override the + // customization with the calculated/rounded imageSize + partitioning.MinSize = imageSize + } + + partOptions := &disk.CustomPartitionTableOptions{ + PartitionTableType: basePartitionTable.Type, // PT type is not customizable, it is determined by the base PT for an image type or architecture + BootMode: t.BootMode(), + DefaultFSType: disk.FS_EXT4, // default fs type for Fedora + RequiredMinSizes: t.requiredPartitionSizes, + } + return disk.NewCustomPartitionTable(partitioning, partOptions, rng) + } partitioningMode := options.PartitioningMode if t.rpmOstree { From 2f8124551bfc81c27ac74c2302db5a4137b2ed5e Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 18 Sep 2024 21:31:10 +0200 Subject: [PATCH 11/13] distro/fedora: add minimum directory sizes to image types For all image types that don't specify a requiredPartitionSizes, add the defaults that are used in the NewPartitionTable() so that NewCustomPartitionTable() has similar behaviour. The only image type that explicitly doesn't specify minimum sizes is the iot-raw-image, which has a pre-existing empty map. --- pkg/distro/fedora/distro.go | 220 ++++++++++++++++++++---------------- 1 file changed, 120 insertions(+), 100 deletions(-) diff --git a/pkg/distro/fedora/distro.go b/pkg/distro/fedora/distro.go index 24e20c31e7..37660d3d64 100644 --- a/pkg/distro/fedora/distro.go +++ b/pkg/distro/fedora/distro.go @@ -51,6 +51,12 @@ var ( oscap.Standard, } + // Default directory size minimums for all image types. + requiredDirectorySizes = map[string]uint64{ + "/": 1 * datasizes.GiB, + "/usr": 2 * datasizes.GiB, + } + // Services iotServices = []string{ "NetworkManager.service", @@ -92,10 +98,11 @@ var ( rpmOstree: false, image: imageInstallerImage, // We don't know the variant of the OS pipeline being installed - isoLabel: getISOLabelFunc("Unknown"), - buildPipelines: []string{"build"}, - payloadPipelines: []string{"anaconda-tree", "rootfs-image", "efiboot-tree", "os", "bootiso-tree", "bootiso"}, - exports: []string{"bootiso"}, + isoLabel: getISOLabelFunc("Unknown"), + buildPipelines: []string{"build"}, + payloadPipelines: []string{"anaconda-tree", "rootfs-image", "efiboot-tree", "os", "bootiso-tree", "bootiso"}, + exports: []string{"bootiso"}, + requiredPartitionSizes: requiredDirectorySizes, } liveInstallerImgType = imageType{ @@ -106,14 +113,15 @@ var ( packageSets: map[string]packageSetFunc{ installerPkgsKey: liveInstallerPackageSet, }, - bootable: true, - bootISO: true, - rpmOstree: false, - image: liveInstallerImage, - isoLabel: getISOLabelFunc("Workstation"), - buildPipelines: []string{"build"}, - payloadPipelines: []string{"anaconda-tree", "rootfs-image", "efiboot-tree", "bootiso-tree", "bootiso"}, - exports: []string{"bootiso"}, + bootable: true, + bootISO: true, + rpmOstree: false, + image: liveInstallerImage, + isoLabel: getISOLabelFunc("Workstation"), + buildPipelines: []string{"build"}, + payloadPipelines: []string{"anaconda-tree", "rootfs-image", "efiboot-tree", "bootiso-tree", "bootiso"}, + exports: []string{"bootiso"}, + requiredPartitionSizes: requiredDirectorySizes, } iotCommitImgType = imageType{ @@ -128,11 +136,12 @@ var ( EnabledServices: iotServices, DracutConf: []*osbuild.DracutConfStageOptions{osbuild.FIPSDracutConfStageOptions}, }, - rpmOstree: true, - image: iotCommitImage, - buildPipelines: []string{"build"}, - payloadPipelines: []string{"os", "ostree-commit", "commit-archive"}, - exports: []string{"commit-archive"}, + rpmOstree: true, + image: iotCommitImage, + buildPipelines: []string{"build"}, + payloadPipelines: []string{"os", "ostree-commit", "commit-archive"}, + exports: []string{"commit-archive"}, + requiredPartitionSizes: requiredDirectorySizes, } iotBootableContainer = imageType{ @@ -142,11 +151,12 @@ var ( packageSets: map[string]packageSetFunc{ osPkgsKey: bootableContainerPackageSet, }, - rpmOstree: true, - image: bootableContainerImage, - buildPipelines: []string{"build"}, - payloadPipelines: []string{"os", "ostree-commit", "ostree-encapsulate"}, - exports: []string{"ostree-encapsulate"}, + rpmOstree: true, + image: bootableContainerImage, + buildPipelines: []string{"build"}, + payloadPipelines: []string{"os", "ostree-commit", "ostree-encapsulate"}, + exports: []string{"ostree-encapsulate"}, + requiredPartitionSizes: requiredDirectorySizes, } iotOCIImgType = imageType{ @@ -164,12 +174,13 @@ var ( EnabledServices: iotServices, DracutConf: []*osbuild.DracutConfStageOptions{osbuild.FIPSDracutConfStageOptions}, }, - rpmOstree: true, - bootISO: false, - image: iotContainerImage, - buildPipelines: []string{"build"}, - payloadPipelines: []string{"os", "ostree-commit", "container-tree", "container"}, - exports: []string{"container"}, + rpmOstree: true, + bootISO: false, + image: iotContainerImage, + buildPipelines: []string{"build"}, + payloadPipelines: []string{"os", "ostree-commit", "container-tree", "container"}, + exports: []string{"container"}, + requiredPartitionSizes: requiredDirectorySizes, } iotInstallerImgType = imageType{ @@ -184,13 +195,14 @@ var ( Locale: common.ToPtr("en_US.UTF-8"), EnabledServices: iotServices, }, - rpmOstree: true, - bootISO: true, - image: iotInstallerImage, - isoLabel: getISOLabelFunc("IoT"), - buildPipelines: []string{"build"}, - payloadPipelines: []string{"anaconda-tree", "rootfs-image", "efiboot-tree", "bootiso-tree", "bootiso"}, - exports: []string{"bootiso"}, + rpmOstree: true, + bootISO: true, + image: iotInstallerImage, + isoLabel: getISOLabelFunc("IoT"), + buildPipelines: []string{"build"}, + payloadPipelines: []string{"anaconda-tree", "rootfs-image", "efiboot-tree", "bootiso-tree", "bootiso"}, + exports: []string{"bootiso"}, + requiredPartitionSizes: requiredDirectorySizes, } iotSimplifiedInstallerImgType = imageType{ @@ -210,17 +222,18 @@ var ( LockRootUser: common.ToPtr(true), IgnitionPlatform: common.ToPtr("metal"), }, - defaultSize: 10 * datasizes.GibiByte, - rpmOstree: true, - bootable: true, - bootISO: true, - image: iotSimplifiedInstallerImage, - isoLabel: getISOLabelFunc("IoT"), - buildPipelines: []string{"build"}, - payloadPipelines: []string{"ostree-deployment", "image", "xz", "coi-tree", "efiboot-tree", "bootiso-tree", "bootiso"}, - exports: []string{"bootiso"}, - basePartitionTables: iotSimplifiedInstallerPartitionTables, - kernelOptions: ostreeDeploymentKernelOptions, + defaultSize: 10 * datasizes.GibiByte, + rpmOstree: true, + bootable: true, + bootISO: true, + image: iotSimplifiedInstallerImage, + isoLabel: getISOLabelFunc("IoT"), + buildPipelines: []string{"build"}, + payloadPipelines: []string{"ostree-deployment", "image", "xz", "coi-tree", "efiboot-tree", "bootiso-tree", "bootiso"}, + exports: []string{"bootiso"}, + basePartitionTables: iotSimplifiedInstallerPartitionTables, + kernelOptions: ostreeDeploymentKernelOptions, + requiredPartitionSizes: requiredDirectorySizes, } iotRawImgType = imageType{ @@ -269,15 +282,16 @@ var ( LockRootUser: common.ToPtr(true), IgnitionPlatform: common.ToPtr("qemu"), }, - defaultSize: 10 * datasizes.GibiByte, - rpmOstree: true, - bootable: true, - image: iotImage, - buildPipelines: []string{"build"}, - payloadPipelines: []string{"ostree-deployment", "image", "qcow2"}, - exports: []string{"qcow2"}, - basePartitionTables: iotBasePartitionTables, - kernelOptions: ostreeDeploymentKernelOptions, + defaultSize: 10 * datasizes.GibiByte, + rpmOstree: true, + bootable: true, + image: iotImage, + buildPipelines: []string{"build"}, + payloadPipelines: []string{"ostree-deployment", "image", "qcow2"}, + exports: []string{"qcow2"}, + basePartitionTables: iotBasePartitionTables, + kernelOptions: ostreeDeploymentKernelOptions, + requiredPartitionSizes: requiredDirectorySizes, } qcow2ImgType = imageType{ @@ -291,14 +305,15 @@ var ( defaultImageConfig: &distro.ImageConfig{ DefaultTarget: common.ToPtr("multi-user.target"), }, - kernelOptions: cloudKernelOptions, - bootable: true, - defaultSize: 5 * datasizes.GibiByte, - image: diskImage, - buildPipelines: []string{"build"}, - payloadPipelines: []string{"os", "image", "qcow2"}, - exports: []string{"qcow2"}, - basePartitionTables: defaultBasePartitionTables, + kernelOptions: cloudKernelOptions, + bootable: true, + defaultSize: 5 * datasizes.GibiByte, + image: diskImage, + buildPipelines: []string{"build"}, + payloadPipelines: []string{"os", "image", "qcow2"}, + exports: []string{"qcow2"}, + basePartitionTables: defaultBasePartitionTables, + requiredPartitionSizes: requiredDirectorySizes, } vmdkDefaultImageConfig = &distro.ImageConfig{ @@ -318,15 +333,16 @@ var ( packageSets: map[string]packageSetFunc{ osPkgsKey: vmdkCommonPackageSet, }, - defaultImageConfig: vmdkDefaultImageConfig, - kernelOptions: cloudKernelOptions, - bootable: true, - defaultSize: 2 * datasizes.GibiByte, - image: diskImage, - buildPipelines: []string{"build"}, - payloadPipelines: []string{"os", "image", "vmdk"}, - exports: []string{"vmdk"}, - basePartitionTables: defaultBasePartitionTables, + defaultImageConfig: vmdkDefaultImageConfig, + kernelOptions: cloudKernelOptions, + bootable: true, + defaultSize: 2 * datasizes.GibiByte, + image: diskImage, + buildPipelines: []string{"build"}, + payloadPipelines: []string{"os", "image", "vmdk"}, + exports: []string{"vmdk"}, + basePartitionTables: defaultBasePartitionTables, + requiredPartitionSizes: requiredDirectorySizes, } ovaImgType = imageType{ @@ -336,15 +352,16 @@ var ( packageSets: map[string]packageSetFunc{ osPkgsKey: vmdkCommonPackageSet, }, - defaultImageConfig: vmdkDefaultImageConfig, - kernelOptions: cloudKernelOptions, - bootable: true, - defaultSize: 2 * datasizes.GibiByte, - image: diskImage, - buildPipelines: []string{"build"}, - payloadPipelines: []string{"os", "image", "vmdk", "ovf", "archive"}, - exports: []string{"archive"}, - basePartitionTables: defaultBasePartitionTables, + defaultImageConfig: vmdkDefaultImageConfig, + kernelOptions: cloudKernelOptions, + bootable: true, + defaultSize: 2 * datasizes.GibiByte, + image: diskImage, + buildPipelines: []string{"build"}, + payloadPipelines: []string{"os", "image", "vmdk", "ovf", "archive"}, + exports: []string{"archive"}, + basePartitionTables: defaultBasePartitionTables, + requiredPartitionSizes: requiredDirectorySizes, } containerImgType = imageType{ @@ -360,11 +377,12 @@ var ( Locale: common.ToPtr("C.UTF-8"), Timezone: common.ToPtr("Etc/UTC"), }, - image: containerImage, - bootable: false, - buildPipelines: []string{"build"}, - payloadPipelines: []string{"os", "container"}, - exports: []string{"container"}, + image: containerImage, + bootable: false, + buildPipelines: []string{"build"}, + payloadPipelines: []string{"os", "container"}, + exports: []string{"container"}, + requiredPartitionSizes: requiredDirectorySizes, } wslImgType = imageType{ @@ -385,11 +403,12 @@ var ( }, }, }, - image: containerImage, - bootable: false, - buildPipelines: []string{"build"}, - payloadPipelines: []string{"os", "container"}, - exports: []string{"container"}, + image: containerImage, + bootable: false, + buildPipelines: []string{"build"}, + payloadPipelines: []string{"os", "container"}, + exports: []string{"container"}, + requiredPartitionSizes: requiredDirectorySizes, } minimalrawImgType = imageType{ @@ -410,15 +429,16 @@ var ( Timeout: 5, }, }, - rpmOstree: false, - kernelOptions: defaultKernelOptions, - bootable: true, - defaultSize: 2 * datasizes.GibiByte, - image: diskImage, - buildPipelines: []string{"build"}, - payloadPipelines: []string{"os", "image", "xz"}, - exports: []string{"xz"}, - basePartitionTables: minimalrawPartitionTables, + rpmOstree: false, + kernelOptions: defaultKernelOptions, + bootable: true, + defaultSize: 2 * datasizes.GibiByte, + image: diskImage, + buildPipelines: []string{"build"}, + payloadPipelines: []string{"os", "image", "xz"}, + exports: []string{"xz"}, + basePartitionTables: minimalrawPartitionTables, + requiredPartitionSizes: requiredDirectorySizes, } ) From a72afb6f26ca2b5cdcd6d222cda52c4098793048 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 11 Sep 2024 18:48:57 +0200 Subject: [PATCH 12/13] distro/fedora: check partitioning customizations Add checks for Partitioning customizations in Fedora's checkOptions() function. The function now returns an error if: - Partitioning is defined at the same time as Filesystem - Partitioning is defined for ostree image types (commit and container). - The mountpoints in Partitioning violate the mountpoint policies for the image type. --- pkg/distro/fedora/distro_test.go | 51 ++++++++++++++++++++++++++++++-- pkg/distro/fedora/imagetype.go | 17 +++++++---- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/pkg/distro/fedora/distro_test.go b/pkg/distro/fedora/distro_test.go index cd50693408..e75deea029 100644 --- a/pkg/distro/fedora/distro_test.go +++ b/pkg/distro/fedora/distro_test.go @@ -740,7 +740,7 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) { imgType, _ := arch.GetImageType(imgTypeName) _, _, err := imgType.Manifest(&bp, distro.ImageOptions{}, nil, 0) if imgTypeName == "iot-commit" || imgTypeName == "iot-container" || imgTypeName == "iot-bootable-container" { - assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types") + assert.EqualError(t, err, "Custom mountpoints and partitioning are not supported for ostree types") } else if imgTypeName == "iot-raw-image" || imgTypeName == "iot-qcow2-image" { assert.EqualError(t, err, fmt.Sprintf(distro.UnsupportedCustomizationError, imgTypeName, "User, Group, Directories, Files, Services, FIPS")) } else if imgTypeName == "iot-installer" || imgTypeName == "iot-simplified-installer" || imgTypeName == "image-installer" { @@ -774,7 +774,7 @@ func TestDistro_TestRootMountPoint(t *testing.T) { imgType, _ := arch.GetImageType(imgTypeName) _, _, err := imgType.Manifest(&bp, distro.ImageOptions{}, nil, 0) if imgTypeName == "iot-commit" || imgTypeName == "iot-container" || imgTypeName == "iot-bootable-container" { - assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types") + assert.EqualError(t, err, "Custom mountpoints and partitioning are not supported for ostree types") } else if imgTypeName == "iot-raw-image" || imgTypeName == "iot-qcow2-image" { assert.EqualError(t, err, fmt.Sprintf(distro.UnsupportedCustomizationError, imgTypeName, "User, Group, Directories, Files, Services, FIPS")) } else if imgTypeName == "iot-installer" || imgTypeName == "iot-simplified-installer" || imgTypeName == "image-installer" { @@ -922,7 +922,7 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) { imgType, _ := arch.GetImageType(imgTypeName) _, _, err := imgType.Manifest(&bp, distro.ImageOptions{}, nil, 0) if imgTypeName == "iot-commit" || imgTypeName == "iot-container" || imgTypeName == "iot-bootable-container" { - assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types") + assert.EqualError(t, err, "Custom mountpoints and partitioning are not supported for ostree types") } else if imgTypeName == "iot-raw-image" || imgTypeName == "iot-qcow2-image" { assert.EqualError(t, err, fmt.Sprintf(distro.UnsupportedCustomizationError, imgTypeName, "User, Group, Directories, Files, Services, FIPS")) } else if imgTypeName == "iot-installer" || imgTypeName == "iot-simplified-installer" || imgTypeName == "image-installer" { @@ -937,6 +937,51 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) { } } +func TestDistro_PartitioningConflict(t *testing.T) { + bp := blueprint.Blueprint{ + Customizations: &blueprint.Customizations{ + Filesystem: []blueprint.FilesystemCustomization{ + { + MinSize: 1024, + Mountpoint: "/", + }, + }, + Disk: &blueprint.DiskCustomization{ + Partitions: []blueprint.PartitionCustomization{ + { + MinSize: 19, + FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ + Mountpoint: "/home", + }, + }, + }, + }, + }, + } + for _, dist := range fedoraFamilyDistros { + fedoraDistro := dist.distro + for _, archName := range fedoraDistro.ListArches() { + arch, _ := fedoraDistro.GetArch(archName) + for _, imgTypeName := range arch.ListImageTypes() { + imgType, _ := arch.GetImageType(imgTypeName) + _, _, err := imgType.Manifest(&bp, distro.ImageOptions{}, nil, 0) + if imgTypeName == "iot-commit" || imgTypeName == "iot-container" || imgTypeName == "iot-bootable-container" { + assert.EqualError(t, err, "Custom mountpoints and partitioning are not supported for ostree types") + } else if imgTypeName == "iot-raw-image" || imgTypeName == "iot-qcow2-image" { + assert.EqualError(t, err, fmt.Sprintf(distro.UnsupportedCustomizationError, imgTypeName, "User, Group, Directories, Files, Services, FIPS")) + } else if imgTypeName == "iot-installer" || imgTypeName == "iot-simplified-installer" || imgTypeName == "image-installer" { + continue + } else if imgTypeName == "live-installer" { + assert.EqualError(t, err, fmt.Sprintf(distro.NoCustomizationsAllowedError, imgTypeName)) + } else { + assert.EqualError(t, err, "partitioning customizations cannot be used with custom filesystems (mountpoints)") + } + } + } + } + +} + func TestDistroFactory(t *testing.T) { type testCase struct { strID string diff --git a/pkg/distro/fedora/imagetype.go b/pkg/distro/fedora/imagetype.go index 1abddad678..60aea17cf7 100644 --- a/pkg/distro/fedora/imagetype.go +++ b/pkg/distro/fedora/imagetype.go @@ -374,13 +374,18 @@ func (t *imageType) checkOptions(bp *blueprint.Blueprint, options distro.ImageOp } mountpoints := customizations.GetFilesystems() - - if mountpoints != nil && t.rpmOstree { - return nil, fmt.Errorf("Custom mountpoints are not supported for ostree types") + partitioning := customizations.GetPartitioning() + if (len(mountpoints) > 0 || partitioning != nil) && t.rpmOstree { + return nil, fmt.Errorf("Custom mountpoints and partitioning are not supported for ostree types") + } + if len(mountpoints) > 0 && partitioning != nil { + return nil, fmt.Errorf("partitioning customizations cannot be used with custom filesystems (mountpoints)") } - err := blueprint.CheckMountpointsPolicy(mountpoints, policies.MountpointPolicies) - if err != nil { + if err := blueprint.CheckMountpointsPolicy(mountpoints, policies.MountpointPolicies); err != nil { + return nil, err + } + if err := blueprint.CheckDiskMountpointsPolicy(partitioning, policies.MountpointPolicies); err != nil { return nil, err } @@ -401,7 +406,7 @@ func (t *imageType) checkOptions(bp *blueprint.Blueprint, options distro.ImageOp dc := customizations.GetDirectories() fc := customizations.GetFiles() - err = blueprint.ValidateDirFileCustomizations(dc, fc) + err := blueprint.ValidateDirFileCustomizations(dc, fc) if err != nil { return nil, err } From 6ba8daddec2b055306737ec5f753f0bad1957f1a Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Wed, 11 Sep 2024 20:06:21 +0200 Subject: [PATCH 13/13] test: add partitioning build configs One for each variant: - Plain - Plain + btrfs - Plain + lvm --- test/config-map.json | 24 +++++++++ test/configs/partitioning-btrfs.json | 55 +++++++++++++++++++++ test/configs/partitioning-lvm.json | 73 ++++++++++++++++++++++++++++ test/configs/partitioning-plain.json | 57 ++++++++++++++++++++++ 4 files changed, 209 insertions(+) create mode 100644 test/configs/partitioning-btrfs.json create mode 100644 test/configs/partitioning-lvm.json create mode 100644 test/configs/partitioning-plain.json diff --git a/test/config-map.json b/test/config-map.json index 1637edd9c8..fa828e32d8 100644 --- a/test/config-map.json +++ b/test/config-map.json @@ -331,5 +331,29 @@ "rhel-9.5", "rhel-10.0" ] + }, + "./configs/partitioning-plain.json": { + "image-types": [ + "ami" + ], + "distros": [ + "fedora*" + ] + }, + "./configs/partitioning-btrfs.json": { + "image-types": [ + "ami" + ], + "distros": [ + "fedora*" + ] + }, + "./configs/partitioning-lvm.json": { + "image-types": [ + "ami" + ], + "distros": [ + "fedora*" + ] } } diff --git a/test/configs/partitioning-btrfs.json b/test/configs/partitioning-btrfs.json new file mode 100644 index 0000000000..3f7079908a --- /dev/null +++ b/test/configs/partitioning-btrfs.json @@ -0,0 +1,55 @@ +{ + "name": "partitioning-btrfs", + "blueprint": { + "customizations": { + "disk": { + "partitions": [ + { + "type": "plain", + "mountpoint": "/data", + "minsize": 1073741824, + "fs_type": "xfs" + }, + { + "type": "btrfs", + "minsize": "10 GiB", + "subvolumes": [ + { + "name": "subvol-home", + "mountpoint": "/home" + }, + { + "name": "subvol-shadowman", + "mountpoint": "/home/shadowman" + }, + { + "name": "subvol-foo", + "mountpoint": "/foo" + }, + { + "name": "subvol-usr", + "mountpoint": "/usr" + }, + { + "name": "subvol-opt", + "mountpoint": "/opt" + }, + { + "name": "subvol-media", + "mountpoint": "/media" + }, + { + "name": "subvol-root", + "mountpoint": "/root" + }, + { + "name": "subvol-srv", + "mountpoint": "/srv" + } + ] + } + ] + } + } + } +} diff --git a/test/configs/partitioning-lvm.json b/test/configs/partitioning-lvm.json new file mode 100644 index 0000000000..960ec93550 --- /dev/null +++ b/test/configs/partitioning-lvm.json @@ -0,0 +1,73 @@ +{ + "name": "partitioning-lvm", + "blueprint": { + "customizations": { + "disk": { + "partitions": [ + { + "mountpoint": "/data", + "minsize": "1 GiB", + "label": "data", + "fs_type": "ext4" + }, + { + "type": "lvm", + "name": "testvg", + "minsize": 10737418240, + "logical_volumes": [ + { + "name": "homelv", + "mountpoint": "/home", + "label": "home", + "fs_type": "ext4", + "minsize": "2 GiB" + }, + { + "name": "shadowmanlv", + "mountpoint": "/home/shadowman", + "fs_type": "ext4", + "minsize": "5 GiB" + }, + { + "name": "foolv", + "mountpoint": "/foo", + "fs_type": "ext4", + "minsize": "1 GiB" + }, + { + "name": "usrlv", + "mountpoint": "/usr", + "fs_type": "ext4", + "minsize": "4 GiB" + }, + { + "name": "optlv", + "mountpoint": "/opt", + "fs_type": "ext4", + "minsize": 1073741824 + }, + { + "name": "medialv", + "mountpoint": "/media", + "fs_type": "ext4", + "minsize": 1073741824 + }, + { + "name": "roothomelv", + "mountpoint": "/root", + "fs_type": "ext4", + "minsize": "1 GiB" + }, + { + "name": "srvlv", + "mountpoint": "/srv", + "fs_type": "ext4", + "minsize": 1073741824 + } + ] + } + ] + } + } + } +} diff --git a/test/configs/partitioning-plain.json b/test/configs/partitioning-plain.json new file mode 100644 index 0000000000..6c2e06ae33 --- /dev/null +++ b/test/configs/partitioning-plain.json @@ -0,0 +1,57 @@ +{ + "name": "partitioning-plain", + "blueprint": { + "customizations": { + "disk": { + "partitions": [ + { + "mountpoint": "/data", + "fs_type": "ext4", + "minsize": 1073741824 + }, + { + "mountpoint": "/home", + "label": "home", + "fs_type": "ext4", + "minsize": 2147483648 + }, + { + "mountpoint": "/home/shadowman", + "fs_type": "ext4", + "minsize": 524288000 + }, + { + "mountpoint": "/foo", + "fs_type": "ext4", + "minsize": 1073741824 + }, + { + "mountpoint": "/var", + "fs_type": "xfs", + "minsize": 4294967296 + }, + { + "mountpoint": "/opt", + "fs_type": "ext4", + "minsize": 1073741824 + }, + { + "mountpoint": "/media", + "fs_type": "ext4", + "minsize": 1073741824 + }, + { + "mountpoint": "/root", + "fs_type": "ext4", + "minsize": 1073741824 + }, + { + "mountpoint": "/srv", + "fs_type": "xfs", + "minsize": 1073741824 + } + ] + } + } + } +}