Skip to content

Commit

Permalink
Image Customizer: Allow omitting disk maxSize and partition start.
Browse files Browse the repository at this point in the history
Allow the partition start to be inferred from the previous partition's
end. Also, allow the disk's maxSize to be inferred from the size/end of
the last partition.

In addition, since the partition start can now be omitted, require the
partitions to be specified in order. Fortunately, most users do this
anyway.
  • Loading branch information
cwize1 committed Sep 6, 2024
1 parent 9c978f0 commit 02124e3
Show file tree
Hide file tree
Showing 11 changed files with 351 additions and 142 deletions.
35 changes: 18 additions & 17 deletions toolkit/tools/imagecustomizerapi/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/microsoft/azurelinux/toolkit/tools/imagegen/diskutils"
"github.com/microsoft/azurelinux/toolkit/tools/internal/ptrutils"
"github.com/stretchr/testify/assert"
)

Expand All @@ -15,11 +16,11 @@ func TestConfigIsValid(t *testing.T) {
Storage: &Storage{
Disks: []Disk{{
PartitionTableType: "gpt",
MaxSize: 3 * diskutils.MiB,
MaxSize: ptrutils.PtrTo(DiskSize(3 * diskutils.MiB)),
Partitions: []Partition{
{
Id: "esp",
Start: 1 * diskutils.MiB,
Start: ptrutils.PtrTo(DiskSize(1 * diskutils.MiB)),
Type: PartitionTypeESP,
},
},
Expand Down Expand Up @@ -52,11 +53,11 @@ func TestConfigIsValidLegacy(t *testing.T) {
Storage: &Storage{
Disks: []Disk{{
PartitionTableType: "gpt",
MaxSize: 3 * diskutils.MiB,
MaxSize: ptrutils.PtrTo(DiskSize(3 * diskutils.MiB)),
Partitions: []Partition{
{
Id: "boot",
Start: 1 * diskutils.MiB,
Start: ptrutils.PtrTo(DiskSize(1 * diskutils.MiB)),
Type: PartitionTypeBiosGrub,
},
},
Expand Down Expand Up @@ -84,11 +85,11 @@ func TestConfigIsValidNoBootType(t *testing.T) {
Storage: &Storage{
Disks: []Disk{{
PartitionTableType: "gpt",
MaxSize: 2 * diskutils.MiB,
MaxSize: ptrutils.PtrTo(DiskSize(2 * diskutils.MiB)),
Partitions: []Partition{
{
Id: "a",
Start: 1 * diskutils.MiB,
Start: ptrutils.PtrTo(DiskSize(1 * diskutils.MiB)),
},
},
}},
Expand All @@ -109,11 +110,11 @@ func TestConfigIsValidMissingBootLoaderReset(t *testing.T) {
Storage: &Storage{
Disks: []Disk{{
PartitionTableType: "gpt",
MaxSize: 3 * diskutils.MiB,
MaxSize: ptrutils.PtrTo(DiskSize(3 * diskutils.MiB)),
Partitions: []Partition{
{
Id: "esp",
Start: 1 * diskutils.MiB,
Start: ptrutils.PtrTo(DiskSize(1 * diskutils.MiB)),
Type: PartitionTypeESP,
},
},
Expand Down Expand Up @@ -145,11 +146,11 @@ func TestConfigIsValidMultipleDisks(t *testing.T) {
Disks: []Disk{
{
PartitionTableType: "gpt",
MaxSize: 1 * diskutils.MiB,
MaxSize: ptrutils.PtrTo(DiskSize(1 * diskutils.MiB)),
},
{
PartitionTableType: "gpt",
MaxSize: 1 * diskutils.MiB,
MaxSize: ptrutils.PtrTo(DiskSize(1 * diskutils.MiB)),
},
},
BootType: "legacy",
Expand Down Expand Up @@ -199,7 +200,7 @@ func TestConfigIsValidBadDisk(t *testing.T) {
BootType: BootTypeEfi,
Disks: []Disk{{
PartitionTableType: PartitionTableTypeGpt,
MaxSize: 0,
MaxSize: ptrutils.PtrTo(DiskSize(0)),
}},
},
OS: &OS{
Expand All @@ -218,7 +219,7 @@ func TestConfigIsValidMissingEsp(t *testing.T) {
Storage: &Storage{
Disks: []Disk{{
PartitionTableType: "gpt",
MaxSize: 2 * diskutils.MiB,
MaxSize: ptrutils.PtrTo(DiskSize(2 * diskutils.MiB)),
Partitions: []Partition{},
}},
BootType: "efi",
Expand All @@ -239,7 +240,7 @@ func TestConfigIsValidMissingBiosBoot(t *testing.T) {
Storage: &Storage{
Disks: []Disk{{
PartitionTableType: "gpt",
MaxSize: 2 * diskutils.MiB,
MaxSize: ptrutils.PtrTo(DiskSize(2 * diskutils.MiB)),
Partitions: []Partition{},
}},
BootType: "legacy",
Expand All @@ -260,11 +261,11 @@ func TestConfigIsValidInvalidMountPoint(t *testing.T) {
Storage: &Storage{
Disks: []Disk{{
PartitionTableType: "gpt",
MaxSize: 3 * diskutils.MiB,
MaxSize: ptrutils.PtrTo(DiskSize(3 * diskutils.MiB)),
Partitions: []Partition{
{
Id: "esp",
Start: 1 * diskutils.MiB,
Start: ptrutils.PtrTo(DiskSize(1 * diskutils.MiB)),
Type: PartitionTypeESP,
},
},
Expand Down Expand Up @@ -298,11 +299,11 @@ func TestConfigIsValidKernelCLI(t *testing.T) {
Storage: &Storage{
Disks: []Disk{{
PartitionTableType: "gpt",
MaxSize: 3 * diskutils.MiB,
MaxSize: ptrutils.PtrTo(DiskSize(3 * diskutils.MiB)),
Partitions: []Partition{
{
Id: "esp",
Start: 1 * diskutils.MiB,
Start: ptrutils.PtrTo(DiskSize(1 * diskutils.MiB)),
Type: PartitionTypeESP,
},
},
Expand Down
99 changes: 68 additions & 31 deletions toolkit/tools/imagecustomizerapi/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ package imagecustomizerapi

import (
"fmt"
"sort"

"github.com/microsoft/azurelinux/toolkit/tools/imagegen/diskutils"
"github.com/microsoft/azurelinux/toolkit/tools/internal/ptrutils"
)

const (
Expand All @@ -29,7 +29,8 @@ type Disk struct {
PartitionTableType PartitionTableType `yaml:"partitionTableType"`

// The virtual size of the disk.
MaxSize DiskSize `yaml:"maxSize"`
// Note: This value is filled in by IsValid().
MaxSize *DiskSize `yaml:"maxSize"`

// The partitions to allocate on the disk.
Partitions []Partition `yaml:"partitions"`
Expand All @@ -41,8 +42,10 @@ func (d *Disk) IsValid() error {
return err
}

if d.MaxSize <= 0 {
return fmt.Errorf("a disk's maxSize value (%d) must be a positive non-zero number", d.MaxSize)
if d.MaxSize != nil {
if *d.MaxSize <= 0 {
return fmt.Errorf("a disk's maxSize value (%d) must be a positive non-zero number", *d.MaxSize)
}
}

for i, partition := range d.Partitions {
Expand All @@ -55,23 +58,41 @@ func (d *Disk) IsValid() error {
gptHeaderSize := DiskSize(roundUp(GptHeaderSectorNum*DefaultSectorSize, DefaultPartitionAlignment))
gptFooterSize := DiskSize(roundUp(GptFooterSectorNum*DefaultSectorSize, DefaultPartitionAlignment))

// Check for overlapping partitions.
// First, sort partitions by start index.
sortedPartitions := append([]Partition(nil), d.Partitions...)
sort.Slice(sortedPartitions, func(i, j int) bool {
return sortedPartitions[i].Start < sortedPartitions[j].Start
})
// Auto-fill the start value from the previous partition's end value.
for i := range d.Partitions {
partition := &d.Partitions[i]

if partition.Start == nil {
if i == 0 {
partition.Start = ptrutils.PtrTo(DiskSize(DefaultPartitionAlignment))
} else {
prev := d.Partitions[i-1]
prevEnd, prevHasEnd := prev.GetEnd()
if !prevHasEnd {
return fmt.Errorf("partition (%s) omitted start value but previous partition (%s) has no size or end value",
partition.Id, prev.Id)
}
partition.Start = &prevEnd
}
}

if partition.IsBiosBoot() {
if *partition.Start != diskutils.MiB {
return fmt.Errorf("BIOS boot partition must start at 1 MiB")
}
}
}

// Then, confirm each partition ends before the next starts.
for i := 0; i < len(sortedPartitions)-1; i++ {
a := &sortedPartitions[i]
b := &sortedPartitions[i+1]
// Confirm each partition ends before the next starts.
for i := 0; i < len(d.Partitions)-1; i++ {
a := d.Partitions[i]
b := d.Partitions[i+1]

aEnd, aHasEnd := a.GetEnd()
if !aHasEnd {
return fmt.Errorf("partition (%s) is not last partition but size is set to \"grow\"", a.Id)
}
if aEnd > b.Start {
if aEnd > *b.Start {
bEnd, bHasEnd := b.GetEnd()
bEndStr := ""
if bHasEnd {
Expand All @@ -82,31 +103,47 @@ func (d *Disk) IsValid() error {
}
}

if len(sortedPartitions) > 0 {
if d.MaxSize == nil && len(d.Partitions) <= 0 {
return fmt.Errorf("either disk must specify maxSize or last partition must have an end or size value")
}

if len(d.Partitions) > 0 {
// Make sure the first block isn't used.
firstPartition := sortedPartitions[0]
if firstPartition.Start < gptHeaderSize {
firstPartition := d.Partitions[0]
if *firstPartition.Start < gptHeaderSize {
return fmt.Errorf("invalid partition (%s) start:\nfirst %s of disk is reserved for the GPT header",
firstPartition.Id, gptHeaderSize.HumanReadable())
}

// Check that the disk is big enough for the partition layout.
lastPartition := sortedPartitions[len(sortedPartitions)-1]

// Verify MaxSize value.
lastPartition := d.Partitions[len(d.Partitions)-1]
lastPartitionEnd, lastPartitionHasEnd := lastPartition.GetEnd()

var requiredSize DiskSize
if !lastPartitionHasEnd {
requiredSize = lastPartition.Start + DefaultPartitionAlignment
} else {
requiredSize = lastPartitionEnd
}
switch {
case !lastPartitionHasEnd && d.MaxSize == nil:
return fmt.Errorf("either disk must specify maxSize or last partition (%s) must have an end or size value",
lastPartition.Id)

case d.MaxSize == nil:
// Fill in the disk's size.
diskSize := lastPartitionEnd + gptFooterSize
d.MaxSize = &diskSize

default:
// Check that the disk is big enough for the partition layout.
var requiredSize DiskSize
if !lastPartitionHasEnd {
requiredSize = *lastPartition.Start + DefaultPartitionAlignment
} else {
requiredSize = lastPartitionEnd
}

requiredSize += gptFooterSize
requiredSize += gptFooterSize

if requiredSize > d.MaxSize {
return fmt.Errorf("disk's partitions need %s but maxSize is only %s:\nGPT footer size is %s",
requiredSize.HumanReadable(), d.MaxSize.HumanReadable(), gptFooterSize.HumanReadable())
if requiredSize > *d.MaxSize {
return fmt.Errorf("disk's partitions need %s but maxSize is only %s:\nGPT footer size is %s",
requiredSize.HumanReadable(), d.MaxSize.HumanReadable(), gptFooterSize.HumanReadable())
}
}
}

Expand Down
Loading

0 comments on commit 02124e3

Please sign in to comment.