Skip to content
Open
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
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ type KernelConfig struct {
type PartitionInfo struct {
Name string `yaml:"name"` // Name: label for the partition
ID string `yaml:"id"` // ID: unique identifier for the partition; can be used as a key
Index *int `yaml:"index"` // Index: index for the partition sdx (x = 1, 2, 3, 4, ...)
Flags []string `yaml:"flags"` // Flags: optional flags for the partition (e.g., "boot", "hidden")
Type string `yaml:"type"` // Type: partition type (e.g., "esp", "linux-root-amd64")
TypeGUID string `yaml:"typeUUID"` // TypeGUID: GPT type GUID for the partition (e.g., "8300" for Linux filesystem)
Expand Down
17 changes: 17 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/open-edge-platform/os-image-composer/internal/config/validate"
)

func intPtr(v int) *int { return &v }

func TestMergeStringSlices(t *testing.T) {
defaultSlice := []string{"a", "b", "c"}
userSlice := []string{"c", "d", "e"}
Expand Down Expand Up @@ -530,6 +532,7 @@ func TestDiskAndSystemConfigGetters(t *testing.T) {
Partitions: []PartitionInfo{
{
ID: "root",
Index: intPtr(1),
FsType: "ext4",
Start: "1MiB",
End: "0",
Expand Down Expand Up @@ -809,6 +812,7 @@ func TestDiskConfigValidation(t *testing.T) {
Partitions: []PartitionInfo{
{
ID: "boot",
Index: intPtr(1),
Name: "EFI Boot",
Type: "esp",
FsType: "fat32",
Expand All @@ -819,6 +823,7 @@ func TestDiskConfigValidation(t *testing.T) {
},
{
ID: "root",
Index: intPtr(2),
Name: "Root",
Type: "linux-root-amd64",
FsType: "ext4",
Expand Down Expand Up @@ -852,6 +857,7 @@ func TestPartitionInfoFields(t *testing.T) {
Partitions: []PartitionInfo{
{
ID: "efi",
Index: intPtr(1),
Name: "EFI System",
Type: "esp",
TypeGUID: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
Expand All @@ -864,6 +870,7 @@ func TestPartitionInfoFields(t *testing.T) {
},
{
ID: "swap",
Index: intPtr(2),
Name: "Swap",
Type: "swap",
TypeGUID: "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F",
Expand All @@ -873,6 +880,7 @@ func TestPartitionInfoFields(t *testing.T) {
},
{
ID: "root",
Index: intPtr(3),
Name: "Root",
Type: "linux-root-amd64",
TypeGUID: "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709",
Expand All @@ -897,6 +905,9 @@ func TestPartitionInfoFields(t *testing.T) {
if efiPartition.ID != "efi" {
t.Errorf("expected EFI partition ID 'efi', got '%s'", efiPartition.ID)
}
if *efiPartition.Index != 1 {
t.Errorf("expected index 1 for EFI partition, got %d", *efiPartition.Index)
}
if len(efiPartition.Flags) != 2 {
t.Errorf("expected 2 flags for EFI partition, got %d", len(efiPartition.Flags))
}
Expand All @@ -918,6 +929,9 @@ func TestPartitionInfoFields(t *testing.T) {
if swapPartition.FsType != "swap" {
t.Errorf("expected swap filesystem type, got '%s'", swapPartition.FsType)
}
if *swapPartition.Index != 2 {
t.Errorf("expected index 2 for swap, got '%d'", *swapPartition.Index)
}
if swapPartition.MountPoint != "" {
t.Errorf("expected empty mount point for swap, got '%s'", swapPartition.MountPoint)
}
Expand All @@ -933,6 +947,9 @@ func TestPartitionInfoFields(t *testing.T) {
if rootPartition.MountPoint != "/" {
t.Errorf("expected root mount point '/', got '%s'", rootPartition.MountPoint)
}
if *rootPartition.Index != 3 {
t.Errorf("expected index 3 for root, got '%d'", *rootPartition.Index)
}
if rootPartition.Start != "2GiB" {
t.Errorf("expected root start '2GiB', got '%s'", rootPartition.Start)
}
Expand Down
1 change: 1 addition & 0 deletions internal/config/schema/os-image-template.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"type": "object",
"properties": {
"id": { "type": "string", "description": "Partition identifier" },
"index": {"type": "integer", "description": "Partition index"},
"name": { "type": "string", "description": "Partition name/label" },
"type": { "type": "string", "description": "Partition type" },
"typeUUID": { "type": "string", "description": "Partition type UUID" },
Expand Down
124 changes: 86 additions & 38 deletions internal/image/imagedisc/imagedisc.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ func diskPartitionCreate(
log.Errorf("Failed to get disk name from path %s: %v", diskPath, err)
return "", fmt.Errorf("failed to get disk name from path: %s", diskPath)
}

startSector, _ := getSectorOffsetFromSize(diskName, startSizeStr)
var endSector uint64
if partitionInfo.End == "0" {
Expand All @@ -505,60 +506,82 @@ func diskPartitionCreate(
log.Infof("Input partition end: " + endSizeStr + ", aligned end sector: " + endSectorStr)

// Create partition
var sfdiskScript strings.Builder
sfdiskScript.WriteString(fmt.Sprintf("start=%d ", startSector))
if endSector != 0 {
size := endSector - startSector
sfdiskScript.WriteString(fmt.Sprintf("size=%d ", size))
}

// Set partition type
// GPT with sgdisk & MBR with sfdisk
if partitionTableType == "gpt" {
// For GPT, use GUID
typeGUID := partitionInfo.TypeGUID
if typeGUID == "" && partitionInfo.Type != "" {
typeGUID, _ = PartitionTypeStrToGUID(partitionInfo.Type)
}

startArg := fmt.Sprintf("%d", startSector)
var endArg string
if endSector == 0 {
endArg = "0"
} else {
endArg = fmt.Sprintf("%d", endSector)
}

// Build sgdisk command: -n (new), -t (type), -c (name)
var parts []string
parts = append(parts, fmt.Sprintf("-n %d:%s:%s", partitionNum, startArg, endArg))
if typeGUID != "" {
sfdiskScript.WriteString(fmt.Sprintf("type=%s ", typeGUID))
parts = append(parts, fmt.Sprintf("-t %d:%s", partitionNum, typeGUID))
}
// Set partition name if provided
if partitionName != "" {
sfdiskScript.WriteString(fmt.Sprintf("name=\"%s\" ", partitionName))
safeName := strings.ReplaceAll(partitionName, "\"", "\\\"")
parts = append(parts, fmt.Sprintf("-c %d:\"%s\"", partitionNum, safeName))
}

cmdStr := fmt.Sprintf("sudo sgdisk %s %s", strings.Join(parts, " "), diskPath)
_, err = shell.ExecCmd(cmdStr, false, shell.HostPath, nil)
if err != nil {
log.Errorf("Failed to create GPT partition %d on disk %s: %v", partitionNum, diskPath, err)
return "", fmt.Errorf("failed to create GPT partition %d on disk %s: %w", partitionNum, diskPath, err)
}

} else {
// For MBR, use hex type code
var typeCode string
switch {
case partitionType == "extended":
typeCode = "5"
case partitionInfo.FsType == "linux-swap":
typeCode = "82"
default:
typeCode = "83" // Linux
var sfdiskScript strings.Builder
sfdiskScript.WriteString(fmt.Sprintf("start=%d ", startSector))
if endSector != 0 {
size := endSector - startSector
sfdiskScript.WriteString(fmt.Sprintf("size=%d ", size))
}

// Set partition type
if partitionTableType == "mbr" {
// For MBR, use hex type code
var typeCode string
switch {
case partitionType == "extended":
typeCode = "5"
case partitionInfo.FsType == "linux-swap":
typeCode = "82"
default:
typeCode = "83" // Linux
}
sfdiskScript.WriteString(fmt.Sprintf("type=%s ", typeCode))
}
sfdiskScript.WriteString(fmt.Sprintf("type=%s ", typeCode))
}

// Handle boot flag
for _, flag := range partitionInfo.Flags {
if flag == "boot" {
sfdiskScript.WriteString("bootable ")
break
// Handle boot flag
for _, flag := range partitionInfo.Flags {
if flag == "boot" {
sfdiskScript.WriteString("bootable ")
break
}
}
}

// Create the partition using sfdisk
cmdStr := fmt.Sprintf("echo '%s' | sudo sfdisk --no-reread --append %s",
sfdiskScript.String(), diskPath)
_, err = shell.ExecCmd(cmdStr, false, shell.HostPath, nil)
if err != nil {
log.Errorf("Failed to create partition %d on disk %s: %v", partitionNum, diskPath, err)
return "", fmt.Errorf("failed to create partition %d on disk %s: %w", partitionNum, diskPath, err)
// Create the partition using sfdisk
cmdStr := fmt.Sprintf("echo '%s' | sudo sfdisk --no-reread --append %s",
sfdiskScript.String(), diskPath)
_, err = shell.ExecCmd(cmdStr, false, shell.HostPath, nil)
if err != nil {
log.Errorf("Failed to create partition %d on disk %s: %v", partitionNum, diskPath, err)
return "", fmt.Errorf("failed to create partition %d on disk %s: %w", partitionNum, diskPath, err)
}
}

// Refresh partition table using partx
cmdStr = fmt.Sprintf("partx -u %s", diskPath)
cmdStr := fmt.Sprintf("partx -u %s", diskPath)
_, err = shell.ExecCmd(cmdStr, true, shell.HostPath, nil)
if err != nil {
log.Errorf("Failed to refresh partition table after creating partition %d: %v", partitionNum, err)
Expand Down Expand Up @@ -698,8 +721,33 @@ func DiskPartitionsCreate(diskPath string, partitionsList []config.PartitionInfo
return nil, fmt.Errorf("failed to create GPT partition table on disk %s: %w", diskPath, err)
}

indexPlaceholder := map[int]string{}
for _, p := range partitionsList {
if p.Index != nil {
if *p.Index <= 0 {
return nil, fmt.Errorf("partition %q: index must be > 0 (got %d)", p.ID, *p.Index)
}
if prev, ok := indexPlaceholder[*p.Index]; ok {
return nil, fmt.Errorf("duplicate partition index %d used by %q and %q", *p.Index, prev, p.ID)
}
indexPlaceholder[*p.Index] = p.ID
}
}

var partitionNum int
for i, partitionInfo := range partitionsList {
partitionNum := i + 1
if partitionInfo.Index != nil {
partitionNum = *partitionInfo.Index
} else {
assignedIndex := i + 1
for {
if _, used := indexPlaceholder[assignedIndex]; !used {
break
}
assignedIndex++
}
partitionNum = assignedIndex
}
diskPartDev, err := diskPartitionCreate(diskPath, partitionNum, partitionInfo, partitionTableType, "primary")
if err != nil {
for i := 1; i < partitionNum; i++ {
Expand Down
36 changes: 31 additions & 5 deletions internal/image/imagedisc/imagedisc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/open-edge-platform/os-image-composer/internal/utils/shell"
)

func intPtr(v int) *int { return &v }

func TestIsDigit(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -785,15 +787,41 @@ func TestDiskPartitionsCreate(t *testing.T) {
End: "100MiB",
FsType: "ext4",
Type: "linux",
Index: intPtr(1),
},
},
partitionTableType: "gpt",
mockCommands: []shell.MockCommand{
{Pattern: "fdisk -l /dev/sda", Output: "Disk /dev/sda: 1 GiB", Error: nil},
{Pattern: "echo 'label: gpt'", Output: "", Error: nil},
{Pattern: "cat /sys/block/sda/queue/hw_sector_size", Output: "512", Error: nil},
{Pattern: "cat /sys/block/sda/queue/physical_block_size", Output: "4096", Error: nil},
{Pattern: "echo", Output: "", Error: nil},
{Pattern: "sudo sgdisk -n:", Output: "", Error: nil},
{Pattern: "partx -u /dev/sda", Output: "", Error: nil},
{Pattern: "mkfs", Output: "", Error: nil},
},
expectError: false,
expectedDevices: 1,
},
{
name: "gpt_partition_index",
diskPath: "/dev/sda",
partitionsList: []config.PartitionInfo{
{
ID: "root",
Name: "root",
Start: "1MiB",
End: "100MiB",
FsType: "ext4",
Type: "linux",
Index: intPtr(14),
},
},
partitionTableType: "gpt",
mockCommands: []shell.MockCommand{
{Pattern: "fdisk -l /dev/sda", Output: "Disk /dev/sda: 1 GiB", Error: nil},
{Pattern: "cat /sys/block/sda/queue/hw_sector_size", Output: "512", Error: nil},
{Pattern: "cat /sys/block/sda/queue/physical_block_size", Output: "4096", Error: nil},
{Pattern: "sudo sgdisk -n:", Output: "", Error: nil},
{Pattern: "partx -u /dev/sda", Output: "", Error: nil},
{Pattern: "mkfs", Output: "", Error: nil},
},
Expand Down Expand Up @@ -838,11 +866,9 @@ func TestDiskPartitionsCreate(t *testing.T) {
},
partitionTableType: "gpt",
mockCommands: []shell.MockCommand{
{Pattern: "fdisk -l /dev/sda", Output: "Disk /dev/sda: 1 GiB", Error: nil},
{Pattern: "echo 'label: gpt'", Output: "", Error: nil},
{Pattern: "cat /sys/block/sda/queue/hw_sector_size", Output: "512", Error: nil},
{Pattern: "cat /sys/block/sda/queue/physical_block_size", Output: "4096", Error: nil},
{Pattern: "echo", Output: "", Error: fmt.Errorf("sfdisk failed")},
{Pattern: "sudo sgdisk", Output: "", Error: fmt.Errorf("sgdisk failed")},
},
expectError: true,
errorMsg: "failed to create partition",
Expand Down
Loading