Skip to content

Commit

Permalink
blueprint: support specifying partition GUIDs
Browse files Browse the repository at this point in the history
People need to customize partitition types/GUIDs, so this commit adds
this option to the blueprint.

It's technically a partition type for DOS partitions, but the key type
is already taken, so let's go with guid both for DOS and GPT.
  • Loading branch information
ondrejbudai committed Dec 20, 2024
1 parent 8dde738 commit 58c9b33
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 3 deletions.
51 changes: 48 additions & 3 deletions pkg/blueprint/disk_customizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import (
"errors"
"fmt"
"path/filepath"
"regexp"
"slices"
"strings"

"github.com/google/uuid"
"github.com/osbuild/images/pkg/datasizes"
"github.com/osbuild/images/pkg/pathpolicy"
)
Expand Down Expand Up @@ -64,6 +66,15 @@ type PartitionCustomization struct {
// (optional, defaults depend on payload and mountpoints).
MinSize uint64 `json:"minsize" toml:"minsize"`

// The partition type GUID for GPT partitions. For DOS partitions, this
// field can be used to set the partition type (e.g. "swap").
// If not set, the code will make the best guess based on the mountpoint or
// the payload type.
// Examples:
// 3B8F8425-20E0-4F3B-907F-1A25A76F98E8 (/srv on GPT)
// 06 (FAT16 on DOS)
GUID string `json:"guid,omitempty" toml:"guid,omitempty"`

BtrfsVolumeCustomization

VGCustomization
Expand Down Expand Up @@ -156,6 +167,7 @@ func (v *PartitionCustomization) UnmarshalJSON(data []byte) error {
var typeSniffer struct {
Type string `json:"type"`
MinSize any `json:"minsize"`
GUID string `json:"guid"`
}
if err := json.Unmarshal(data, &typeSniffer); err != nil {
return fmt.Errorf("%s %w", errPrefix, err)
Expand Down Expand Up @@ -184,6 +196,7 @@ func (v *PartitionCustomization) UnmarshalJSON(data []byte) error {
}

v.Type = partType
v.GUID = typeSniffer.GUID

if typeSniffer.MinSize == nil {
return fmt.Errorf("minsize is required")
Expand All @@ -203,10 +216,11 @@ func (v *PartitionCustomization) UnmarshalJSON(data []byte) error {
// 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
// Type, minsize and guid are handled by the caller. These are added here to
// satisfy "DisallowUnknownFields" when decoding.
Type string `json:"type"`
MinSize any `json:"minsize"`
GUID string `json:"guid"`
FilesystemTypedCustomization
}

Expand All @@ -226,10 +240,11 @@ func decodePlain(v *PartitionCustomization, data []byte) error {
// 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
// Type, minsize and guid are handled by the caller. These are added here to
// satisfy "DisallowUnknownFields" when decoding.
Type string `json:"type"`
MinSize any `json:"minsize"`
GUID string `json:"guid"`
BtrfsVolumeCustomization
}

Expand All @@ -249,10 +264,11 @@ func decodeBtrfs(v *PartitionCustomization, data []byte) error {
// 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
// Type, minsize and guid are handled by the caller. These are added here to
// satisfy "DisallowUnknownFields" when decoding.
Type string `json:"type"`
MinSize any `json:"minsize"`
GUID string `json:"guid"`
VGCustomization
}

Expand Down Expand Up @@ -367,6 +383,9 @@ func (p *DiskCustomization) Validate() error {
vgnames := make(map[string]bool)
var errs []error
for _, part := range p.Partitions {
if err := part.validateGeneric(); err != nil {
errs = append(errs, err)
}
switch part.Type {
case "plain", "":
errs = append(errs, part.validatePlain(mountpoints))
Expand Down Expand Up @@ -471,6 +490,32 @@ var validPlainFSTypes = []string{
"xfs",
}

// exactly 2 hex digits
var validDosPartitionType = regexp.MustCompile(`^[0-9a-fA-F]{2}$`)

// validateGeneric checks the partition validity regardless of its type.
// Currently, it only checks the GUID field.
func (p *PartitionCustomization) validateGeneric() error {
// Empty GUID is fine, the code will auto-generate it later.
if p.GUID == "" {
return nil
}

// We don't know the partition table type yet, so check:

// 1) the partition GUID is either a valid UUID (for GPT)
if _, err := uuid.Parse(p.GUID); err == nil {
return nil
}

// 2) or a valid DOS partition type (for MBR)
if validDosPartitionType.MatchString(p.GUID) {
return nil
}

return fmt.Errorf("invalid partition GUID: %q (use UUIDs for GPT partition tables, or 2-digit hex numbers for DOS partition tables)", p.GUID)
}

func (p *PartitionCustomization) validatePlain(mountpoints map[string]bool) error {
if p.FSType == "swap" {
// make sure the mountpoint is empty and return
Expand Down
56 changes: 56 additions & 0 deletions pkg/blueprint/disk_customizations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,48 @@ func TestPartitioningValidation(t *testing.T) {
},
expectedMsg: `invalid partitioning customizations: "dos" partition table type only supports up to 4 partitions: got 6`,
},
"happy-partition-guids": {
partitioning: &blueprint.DiskCustomization{
Partitions: []blueprint.PartitionCustomization{
{
GUID: "12345678-1234-1234-1234-1234567890ab",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
FSType: "ext4",
Mountpoint: "/gpt",
},
},
{
GUID: "ef",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
FSType: "ext4",
Mountpoint: "/dos",
},
},
},
},
},
"unhappy-partition-guids": {
partitioning: &blueprint.DiskCustomization{
Partitions: []blueprint.PartitionCustomization{

{
GUID: "12345678-uuid-1234-1234-1234567890ab",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
FSType: "ext4",
Mountpoint: "/gpt",
},
},
{
GUID: "0x52",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
FSType: "ext4",
Mountpoint: "/dos",
},
},
},
},
expectedMsg: "invalid partitioning customizations:\ninvalid partition GUID: \"12345678-uuid-1234-1234-1234567890ab\" (use UUIDs for GPT partition tables, or 2-digit hex numbers for DOS partition tables)\ninvalid partition GUID: \"0x52\" (use UUIDs for GPT partition tables, or 2-digit hex numbers for DOS partition tables)",
},
}

for name := range testCases {
Expand Down Expand Up @@ -1187,13 +1229,15 @@ func TestPartitionCustomizationUnmarshalJSON(t *testing.T) {
input: `{
"type": "plain",
"minsize": "1 GiB",
"guid": "12345678-1234-1234-1234-1234567890ab",
"mountpoint": "/",
"label": "root",
"fs_type": "xfs"
}`,
expected: &blueprint.PartitionCustomization{
Type: "plain",
MinSize: 1 * datasizes.GiB,
GUID: "12345678-1234-1234-1234-1234567890ab",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
Mountpoint: "/",
Label: "root",
Expand Down Expand Up @@ -1223,6 +1267,7 @@ func TestPartitionCustomizationUnmarshalJSON(t *testing.T) {
input: `{
"type": "btrfs",
"minsize": "10 GiB",
"guid": "12345678-1234-1234-1234-1234567890ab",
"subvolumes": [
{
"name": "subvols/root",
Expand All @@ -1237,6 +1282,7 @@ func TestPartitionCustomizationUnmarshalJSON(t *testing.T) {
expected: &blueprint.PartitionCustomization{
Type: "btrfs",
MinSize: 10 * datasizes.GiB,
GUID: "12345678-1234-1234-1234-1234567890ab",
BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{
Subvolumes: []blueprint.BtrfsSubvolumeCustomization{
{
Expand Down Expand Up @@ -1288,6 +1334,7 @@ func TestPartitionCustomizationUnmarshalJSON(t *testing.T) {
"type": "lvm",
"name": "myvg",
"minsize": "99 GiB",
"guid": "12345678-1234-1234-1234-1234567890ab",
"logical_volumes": [
{
"name": "homelv",
Expand All @@ -1308,6 +1355,7 @@ func TestPartitionCustomizationUnmarshalJSON(t *testing.T) {
expected: &blueprint.PartitionCustomization{
Type: "lvm",
MinSize: 99 * datasizes.GiB,
GUID: "12345678-1234-1234-1234-1234567890ab",
VGCustomization: blueprint.VGCustomization{
Name: "myvg",
LogicalVolumes: []blueprint.LVCustomization{
Expand Down Expand Up @@ -1399,6 +1447,14 @@ func TestPartitionCustomizationUnmarshalJSON(t *testing.T) {
}`,
errorMsg: "JSON unmarshal: error decoding minsize for partition: cannot be negative",
},
"guid-not-string": {
input: `{
"minsize": "10 GiB",
"mountpoint": "/",
"guid": 12345678
}`,
errorMsg: "JSON unmarshal: json: cannot unmarshal number into Go struct field .guid of type string",
},
"wrong-type/btrfs-with-lvm": {
input: `{
"type": "btrfs",
Expand Down

0 comments on commit 58c9b33

Please sign in to comment.