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
19 changes: 19 additions & 0 deletions pkg/disk/disk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ func TestAlignUp(t *testing.T) {
got := pt.AlignUp(tt.size)
assert.Equal(t, tt.want, got, "Expected %d, got %d", tt.want, got)
}

// custom grain size
customGrain := datasizes.Size(4096)
ptCustom := disk.PartitionTable{GrainSize: customGrain}
customTests := []struct {
size datasizes.Size
want datasizes.Size
}{
{0, 0},
{1, customGrain},
{customGrain - 1, customGrain},
{customGrain, customGrain},
{customGrain + 1, customGrain * 2},
}

for _, tt := range customTests {
got := ptCustom.AlignUp(tt.size)
assert.Equal(t, tt.want, got, "Expected %d, got %d (grain=%d)", tt.want, got, customGrain)
}
}

func TestDynamicallyResizePartitionTable(t *testing.T) {
Expand Down
54 changes: 41 additions & 13 deletions pkg/disk/partition_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,23 @@ type PartitionTable struct {
Type PartitionTableType `json:"type" yaml:"type"`
Partitions []Partition `json:"partitions" yaml:"partitions"`

// Sector size in bytes
// Grain size for partition alignment (in bytes). 0 means DefaultGrainBytes.
GrainSize datasizes.Size `json:"grain_size,omitempty" yaml:"grain_size,omitempty"`
// Sector size in bytes. 0 means DefaultSectorSize.
SectorSize uint64 `json:"sector_size,omitempty" yaml:"sector_size,omitempty"`
// Extra space at the end of the partition table (sectors)
ExtraPadding uint64 `json:"extra_padding,omitempty" yaml:"extra_padding,omitempty"`
// Starting offset of the first partition in the table (in bytes)
// Starting offset of the first partition in the table (in bytes).
// By default this is added to the header size. When
// AbsoluteStartOffset is true, it is treated as a minimum absolute
// position (matching systemd-repart's use of libfdisk's first_lba).
StartOffset Offset `json:"start_offset,omitempty" yaml:"start_offset,omitempty"`
// Treat StartOffset as an absolute minimum start position rather
// than an additive offset on top of the header.
AbsoluteStartOffset bool `json:"absolute_start_offset,omitempty" yaml:"absolute_start_offset,omitempty"`
// Align the GPT footer to the grain size, ensuring the last partition's
// size is also grain-aligned. Matches systemd-repart behavior.
AlignFooter bool `json:"align_footer,omitempty" yaml:"align_footer,omitempty"`

// Dictates if certain bits and bobs are required or not; uses the default
// policy if not set.
Expand Down Expand Up @@ -215,14 +226,17 @@ func (pt *PartitionTable) Clone() Entity {
}

clone := &PartitionTable{
Size: pt.Size,
UUID: pt.UUID,
Type: pt.Type,
Partitions: make([]Partition, len(pt.Partitions)),
SectorSize: pt.SectorSize,
ExtraPadding: pt.ExtraPadding,
StartOffset: pt.StartOffset,
Policy: pt.Policy,
Size: pt.Size,
UUID: pt.UUID,
Type: pt.Type,
Partitions: make([]Partition, len(pt.Partitions)),
SectorSize: pt.SectorSize,
GrainSize: pt.GrainSize,
ExtraPadding: pt.ExtraPadding,
StartOffset: pt.StartOffset,
AbsoluteStartOffset: pt.AbsoluteStartOffset,
AlignFooter: pt.AlignFooter,
Policy: pt.Policy,
}

for idx, partition := range pt.Partitions {
Expand All @@ -243,10 +257,17 @@ func (pt *PartitionTable) Clone() Entity {
return clone
}

// AlignUp will round up the given size value to the default grain if not
func (pt *PartitionTable) grainSize() datasizes.Size {
if pt.GrainSize > 0 {
return pt.GrainSize
}
return DefaultGrainBytes
}

// AlignUp will round up the given size value to the grain size if not
// already aligned.
func (pt *PartitionTable) AlignUp(size datasizes.Size) datasizes.Size {
grain := DefaultGrainBytes
grain := pt.grainSize()
if size%grain == 0 {
// already aligned: return unchanged
return size
Expand Down Expand Up @@ -506,9 +527,16 @@ func (pt *PartitionTable) relayout(size datasizes.Size) uint64 {
// The GPT header is also at the end of the partition table
if pt.Type == PT_GPT {
footer = header
if pt.AlignFooter {
footer = pt.AlignUp(footer)
}
}

start := pt.AlignUp(header + pt.StartOffset).Uint64()
startBase := header + pt.StartOffset
if pt.AbsoluteStartOffset && pt.StartOffset > header {
startBase = pt.StartOffset
}
start := pt.AlignUp(startBase).Uint64()

size = pt.AlignUp(size)

Expand Down
102 changes: 102 additions & 0 deletions pkg/disk/partition_table_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,108 @@ func TestRelayout(t *testing.T) {
},
},
},
"gpt-4k-grain-align-footer": {
pt: &PartitionTable{
Type: PT_GPT,
Size: 200*MiB + 2*GiB,
GrainSize: 4096,
AlignFooter: true,
Partitions: []Partition{
{
Size: 200 * MiB,
Type: EFISystemPartitionGUID,
Payload: &Filesystem{
Type: "vfat",
Mountpoint: "/boot/efi",
},
},
{
Size: 2 * GiB,
Payload: &Filesystem{
Mountpoint: "/",
},
},
},
},
size: 200*MiB + 2*GiB,
expected: &PartitionTable{
Type: PT_GPT,
Size: 200*MiB + 2*GiB + 10*4096, // header (5*4096) + footer (5*4096)
GrainSize: 4096,
AlignFooter: true,
Partitions: []Partition{
{
Start: 5 * 4096, // header (16896) aligned up to 4096 grain
Size: 200 * MiB,
Type: EFISystemPartitionGUID,
Payload: &Filesystem{
Type: "vfat",
Mountpoint: "/boot/efi",
},
},
{
Start: 5*4096 + 200*MiB,
Size: 2 * GiB, // exactly 2 GiB: footer is grain-aligned so root stays aligned
Payload: &Filesystem{
Mountpoint: "/",
},
},
},
},
},
"gpt-4k-grain-repart-compat": {
pt: &PartitionTable{
Type: PT_GPT,
Size: 200*MiB + 2*GiB,
GrainSize: 4096,
StartOffset: 1 * MiB,
AbsoluteStartOffset: true,
AlignFooter: true,
Partitions: []Partition{
{
Size: 200 * MiB,
Type: EFISystemPartitionGUID,
Payload: &Filesystem{
Type: "vfat",
Mountpoint: "/boot/efi",
},
},
{
Size: 2 * GiB,
Payload: &Filesystem{
Mountpoint: "/",
},
},
},
},
size: 200*MiB + 2*GiB,
expected: &PartitionTable{
Type: PT_GPT,
Size: 1*MiB + 200*MiB + 2*GiB + 5*4096, // start (1 MiB) + partitions + aligned footer
GrainSize: 4096,
StartOffset: 1 * MiB,
AbsoluteStartOffset: true,
AlignFooter: true,
Partitions: []Partition{
{
Start: 1 * MiB, // StartOffset treated as absolute minimum, already grain-aligned
Size: 200 * MiB,
Type: EFISystemPartitionGUID,
Payload: &Filesystem{
Type: "vfat",
Mountpoint: "/boot/efi",
},
},
{
Start: 1*MiB + 200*MiB,
Size: 2 * GiB,
Payload: &Filesystem{
Mountpoint: "/",
},
},
},
},
},
}

for name := range testCases {
Expand Down
Loading