Skip to content
Closed
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
97 changes: 82 additions & 15 deletions pkg/blueprint/disk_customizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"regexp"
"slices"
"strings"
"unicode/utf16"

"github.com/google/uuid"
"github.com/osbuild/images/pkg/datasizes"
Expand Down Expand Up @@ -72,6 +73,14 @@ type PartitionCustomization struct {
// or the payload type.
PartType string `json:"part_type,omitempty" toml:"part_type,omitempty"`

// The partition label for GPT partitions, not supported for dos partitions.
// Note: This is not the same as the label, which can be set in "Label"
PartLabel string `json:"part_label,omitempty" toml:"part_label,omitempty"`

// The partition GUID for GPT partitions, not supported for dos partitions.
// Note: This is the unique uuid, not the type guid, that is PartType
PartUUID string `json:"part_uuid,omitempty" toml:"part_uuid,omitempty"`

BtrfsVolumeCustomization

VGCustomization
Expand Down Expand Up @@ -162,9 +171,11 @@ type BtrfsSubvolumeCustomization struct {
func (v *PartitionCustomization) UnmarshalJSON(data []byte) error {
errPrefix := "JSON unmarshal:"
var typeSniffer struct {
Type string `json:"type"`
MinSize any `json:"minsize"`
PartType string `json:"part_type"`
Type string `json:"type"`
MinSize any `json:"minsize"`
PartType string `json:"part_type"`
PartLabel string `json:"part_label"`
PartUUID string `json:"part_uuid"`
}
if err := json.Unmarshal(data, &typeSniffer); err != nil {
return fmt.Errorf("%s %w", errPrefix, err)
Expand Down Expand Up @@ -194,6 +205,8 @@ func (v *PartitionCustomization) UnmarshalJSON(data []byte) error {

v.Type = partType
v.PartType = typeSniffer.PartType
v.PartLabel = typeSniffer.PartLabel
v.PartUUID = typeSniffer.PartUUID

if typeSniffer.MinSize == nil {
return fmt.Errorf("minsize is required")
Expand All @@ -213,11 +226,13 @@ 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, minsize, and part_type are handled by the caller. These are added here to
// Type, minsize, and part_* are handled by the caller. These are added here to
// satisfy "DisallowUnknownFields" when decoding.
Type string `json:"type"`
MinSize any `json:"minsize"`
PartType string `json:"part_type"`
Type string `json:"type"`
MinSize any `json:"minsize"`
PartType string `json:"part_type"`
PartLabel string `json:"part_label"`
PartUUID string `json:"part_uuid"`
FilesystemTypedCustomization
}

Expand All @@ -237,11 +252,13 @@ 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, minsize, and part_type are handled by the caller. These are added here to
// Type, minsize, and part_* are handled by the caller. These are added here to
// satisfy "DisallowUnknownFields" when decoding.
Type string `json:"type"`
MinSize any `json:"minsize"`
PartType string `json:"part_type"`
Type string `json:"type"`
MinSize any `json:"minsize"`
PartType string `json:"part_type"`
PartLabel string `json:"part_label"`
PartUUID string `json:"part_uuid"`
BtrfsVolumeCustomization
}

Expand All @@ -261,11 +278,13 @@ 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, minsize, and part_type are handled by the caller. These are added here to
// Type, minsize, and part_* are handled by the caller. These are added here to
// satisfy "DisallowUnknownFields" when decoding.
Type string `json:"type"`
MinSize any `json:"minsize"`
PartType string `json:"part_type"`
Type string `json:"type"`
MinSize any `json:"minsize"`
PartType string `json:"part_type"`
PartLabel string `json:"part_label"`
PartUUID string `json:"part_uuid"`
VGCustomization
}

Expand Down Expand Up @@ -383,6 +402,12 @@ func (p *DiskCustomization) Validate() error {
if err := part.ValidatePartitionTypeID(p.Type); err != nil {
errs = append(errs, err)
}
if err := part.ValidatePartitionID(p.Type); err != nil {
errs = append(errs, err)
}
if err := part.ValidatePartitionLabel(p.Type); err != nil {
errs = append(errs, err)
}
switch part.Type {
case "plain", "":
errs = append(errs, part.validatePlain(mountpoints))
Expand Down Expand Up @@ -525,6 +550,48 @@ func (p *PartitionCustomization) ValidatePartitionTypeID(ptType string) error {
return nil
}

// ValidatePartitionID returns an error if the partition ID is not
// valid given the partition table type. If the partition table type is an
// empty string, the function returns an error only if the partition type ID is
// invalid for both gpt and dos partition tables.
func (p *PartitionCustomization) ValidatePartitionID(ptType string) error {
// Empty PartUUID is fine, it will be selected automatically if needed
if p.PartUUID == "" {
return nil
}

if ptType == "dos" {
return fmt.Errorf("part_type is not supported for dos partition tables")
}

_, uuidErr := uuid.Parse(p.PartUUID)
if uuidErr != nil {
return fmt.Errorf("invalid partition part_uuid %q (must be a valid UUID): %w", p.PartUUID, uuidErr)
}

return nil
}

// ValidatePartitionID returns an error if the partition ID is not
// valid given the partition table type.
func (p *PartitionCustomization) ValidatePartitionLabel(ptType string) error {
// Empty PartLabel is fine
if p.PartLabel == "" {
return nil
}

if ptType == "dos" {
return fmt.Errorf("part_label is not supported for dos partition tables")
}

// GPT Labels are up to 36 utf-16 chars
if len(utf16.Encode([]rune(p.PartLabel))) > 36 {
return fmt.Errorf("part_label is not a valid GPT label, it is too long")
}

return nil
}

func (p *PartitionCustomization) validatePlain(mountpoints map[string]bool) error {
if p.FSType == "swap" {
// make sure the mountpoint is empty and return
Expand Down
97 changes: 97 additions & 0 deletions pkg/blueprint/disk_customizations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,103 @@ func TestPartitioningValidation(t *testing.T) {
},
expectedMsg: "invalid partitioning customizations:\ninvalid partition part_type \"93a9549d-cae1-4024-b95c-e09d77b34c60\" for partition table type \"dos\" (must be a 2-digit hex number)",
},
"happy-partition-part_uuid": {
partitioning: &blueprint.DiskCustomization{
Partitions: []blueprint.PartitionCustomization{
{
PartUUID: "12345678-1234-1234-1234-1234567890ab",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
FSType: "ext4",
Mountpoint: "/",
},
},
},
},
},
"unhappy-partition-part_uuid-dos": {
partitioning: &blueprint.DiskCustomization{
Type: "dos",
Partitions: []blueprint.PartitionCustomization{
{
PartUUID: "12345678-1234-1234-1234-1234567890ab",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
FSType: "ext4",
Mountpoint: "/",
},
},
},
},
expectedMsg: "invalid partitioning customizations:\npart_type is not supported for dos partition tables",
},
"unhappy-partition-part_": {
partitioning: &blueprint.DiskCustomization{
Partitions: []blueprint.PartitionCustomization{
{
PartUUID: "12345678-",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
FSType: "ext4",
Mountpoint: "/",
},
},
},
},
expectedMsg: "invalid partitioning customizations:\ninvalid partition part_uuid \"12345678-\" (must be a valid UUID): invalid UUID length: 9",
},
"happy-partition-part_label": {
partitioning: &blueprint.DiskCustomization{
Partitions: []blueprint.PartitionCustomization{
{
PartLabel: "TheLabel",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
FSType: "ext4",
Mountpoint: "/",
},
},
},
},
},
"unhappy-partition-part_label-dos": {
partitioning: &blueprint.DiskCustomization{
Type: "dos",
Partitions: []blueprint.PartitionCustomization{
{
PartLabel: "TheLabel",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
FSType: "ext4",
Mountpoint: "/",
},
},
},
},
expectedMsg: "invalid partitioning customizations:\npart_label is not supported for dos partition tables",
},
"happy-partition-part_label-long": {
partitioning: &blueprint.DiskCustomization{
Partitions: []blueprint.PartitionCustomization{
{
PartLabel: "123456789012345678901234567890123456",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
FSType: "ext4",
Mountpoint: "/",
},
},
},
},
},
"unhappy-partition-part_label-long": {
partitioning: &blueprint.DiskCustomization{
Partitions: []blueprint.PartitionCustomization{
{
PartLabel: "1234567890123456789012345678901234567",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
FSType: "ext4",
Mountpoint: "/",
},
},
},
},
expectedMsg: "invalid partitioning customizations:\npart_label is not a valid GPT label, it is too long",
},
}

for name := range testCases {
Expand Down
4 changes: 4 additions & 0 deletions pkg/disk/partition.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ type Partition struct {
// is just a string.
UUID string `json:"uuid,omitempty" yaml:"uuid,omitempty"`

// Partition name (not filesystem label), only supported for GPT
Label string `json:"label,omitempty" yaml:"label,omitempty"`

// If nil, the partition is raw; It doesn't contain a payload.
Payload PayloadEntity `json:"payload,omitempty" yaml:"payload,omitempty"`
}
Expand All @@ -36,6 +39,7 @@ func (p *Partition) Clone() Entity {
Type: p.Type,
Bootable: p.Bootable,
UUID: p.UUID,
Label: p.Label,
}

if p.Payload != nil {
Expand Down
14 changes: 13 additions & 1 deletion pkg/disk/partition_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -1302,10 +1302,16 @@ func NewCustomPartitionTable(customizations *blueprint.DiskCustomization, option
// add user customized partitions
for _, part := range customizations.Partitions {
if part.PartType != "" {
// check the partition type now that we also know the partition table type
// check the partition details now that we also know the partition table type
if err := part.ValidatePartitionTypeID(pt.Type.String()); err != nil {
return nil, fmt.Errorf("%s error validating partition type ID for %q: %w", errPrefix, part.Mountpoint, err)
}
if err := part.ValidatePartitionID(pt.Type.String()); err != nil {
return nil, fmt.Errorf("%s error validating partition ID for %q: %w", errPrefix, part.Mountpoint, err)
}
if err := part.ValidatePartitionLabel(pt.Type.String()); err != nil {
return nil, fmt.Errorf("%s error validating partition label for %q: %w", errPrefix, part.Mountpoint, err)
}
}

switch part.Type {
Expand Down Expand Up @@ -1400,6 +1406,8 @@ func addPlainPartition(pt *PartitionTable, partition blueprint.PartitionCustomiz

newpart := Partition{
Type: partType,
UUID: partition.PartUUID,
Label: partition.PartLabel,
Size: partition.MinSize,
Payload: payload,
}
Expand Down Expand Up @@ -1472,6 +1480,8 @@ func addLVMPartition(pt *PartitionTable, partition blueprint.PartitionCustomizat

newpart := Partition{
Type: partType,
UUID: partition.PartUUID,
Label: partition.PartLabel,
Size: partition.MinSize,
Bootable: false,
Payload: newvg,
Expand Down Expand Up @@ -1505,6 +1515,8 @@ func addBtrfsPartition(pt *PartitionTable, partition blueprint.PartitionCustomiz
}
newpart := Partition{
Type: partType,
UUID: partition.PartUUID,
Label: partition.PartLabel,
Bootable: false,
Payload: newvol,
Size: partition.MinSize,
Expand Down
11 changes: 7 additions & 4 deletions pkg/disk/partition_table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1275,8 +1275,10 @@ func TestNewCustomPartitionTable(t *testing.T) {
customizations: &blueprint.DiskCustomization{
Partitions: []blueprint.PartitionCustomization{
{
MinSize: 20 * datasizes.MiB,
PartType: "01234567-89ab-cdef-0123-456789abcdef", // overrides the inferred type
MinSize: 20 * datasizes.MiB,
PartType: "01234567-89ab-cdef-0123-456789abcdef", // overrides the inferred type
PartLabel: "TheLabel",
PartUUID: "76543210-89ab-cdef-0123-456789abcdef", // don't generate uuid
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{
Mountpoint: "/data",
Label: "data",
Expand Down Expand Up @@ -1323,7 +1325,8 @@ func TestNewCustomPartitionTable(t *testing.T) {
Size: 20 * datasizes.MiB,
Type: "01234567-89ab-cdef-0123-456789abcdef",
Bootable: false,
UUID: "a178892e-e285-4ce1-9114-55780875d64e",
UUID: "76543210-89ab-cdef-0123-456789abcdef",
Label: "TheLabel",
Payload: &disk.Filesystem{
Type: "ext4",
Label: "data",
Expand All @@ -1338,7 +1341,7 @@ func TestNewCustomPartitionTable(t *testing.T) {
Start: 222 * datasizes.MiB,
Size: 1*datasizes.MiB - (disk.DefaultSectorSize + (128 * 128)),
Type: disk.RootPartitionX86_64GUID,
UUID: "e2d3d0d0-de6b-48f9-b44c-e85ff044c6b1",
UUID: "a178892e-e285-4ce1-9114-55780875d64e",
Bootable: false,
Payload: &disk.Filesystem{
Type: "xfs",
Expand Down
2 changes: 2 additions & 0 deletions pkg/osbuild/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func sfdiskStageOptions(pt *disk.PartitionTable) *SfdiskStageOptions {
Size: pt.BytesToSectors(p.Size),
Type: p.Type,
UUID: p.UUID,
Name: p.Label,
}
}
stageOptions := &SfdiskStageOptions{
Expand All @@ -40,6 +41,7 @@ func sgdiskStageOptions(pt *disk.PartitionTable) *SgdiskStageOptions {
Start: pt.BytesToSectors(p.Start),
Size: pt.BytesToSectors(p.Size),
Type: p.Type,
Name: p.Label,
}

if p.UUID != "" {
Expand Down
Loading