Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

disk: enable customizing ESP partitions #1116

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like calling this a GUID when it isn't always a GUID. In other places this is called Type, but that's obviously already been used. Maybe PartType?


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
21 changes: 21 additions & 0 deletions pkg/disk/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"io"
"math/rand"
"reflect"
"regexp"
"strings"

"slices"
Expand Down Expand Up @@ -120,6 +121,26 @@ func getPartitionTypeIDfor(ptType PartitionTableType, partTypeName string) (stri
return id, nil
}

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

func validatePartitionTypeID(ptType PartitionTableType, partTypeName string) error {
switch ptType {
case PT_DOS:
if !validDosPartitionType.MatchString(partTypeName) {
return fmt.Errorf("invalid dos partition type ID: %s", partTypeName)
}
case PT_GPT:
if _, err := uuid.Parse(partTypeName); err != nil {
return fmt.Errorf("invalid gpt partition type GUID: %s", partTypeName)
}
default:
return fmt.Errorf("unknown or unsupported partition table enum: %d", ptType)
}

return nil
}

// FSType is the filesystem type enum.
//
// There should always be one value for each filesystem type supported by
Expand Down
Loading
Loading