diff --git a/changelog/bugfixes/2026-06-08-ignition-part-0.md b/changelog/bugfixes/2026-06-08-ignition-part-0.md new file mode 100644 index 00000000000..b0d23f65659 --- /dev/null +++ b/changelog/bugfixes/2026-06-08-ignition-part-0.md @@ -0,0 +1 @@ +- Fixed using Ignition to create new partitions with number 0 to get the next available slot. ([ignition#2234](https://github.com/coreos/ignition/pull/2234)) diff --git a/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/files/0019-stages-disks-Fix-giving-partition-number-0-to-get-th.patch b/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/files/0019-stages-disks-Fix-giving-partition-number-0-to-get-th.patch new file mode 100644 index 00000000000..53be8a51ea7 --- /dev/null +++ b/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/files/0019-stages-disks-Fix-giving-partition-number-0-to-get-th.patch @@ -0,0 +1,128 @@ +From 22c97cdf614e3db9b2d059dd7758ddf7dd5a282e Mon Sep 17 00:00:00 2001 +From: James Le Cuirot +Date: Tue, 2 Jun 2026 15:12:37 +0100 +Subject: [PATCH 19/20] stages/disks: Fix giving partition number 0 to get the + next available + +This was broken since partx was used in commit c2cc56cd. Passing 0 to +partx causes it to try and add all the partitions, which will almost +always fail because the kernel will usually already know about at least +some of them. + +This changes getRealStartAndSize() to also determine and return the +resulting partition numbers so that subsequent operations use these +instead of 0. + +sgdisk does support --new=0, but it has no way to report which partition +number it actually used. + +Signed-off-by: James Le Cuirot +--- a/internal/exec/stages/disks/partitions.go ++++ b/internal/exec/stages/disks/partitions.go +@@ -112,15 +112,6 @@ func partitionMatchesCommon(existing util.PartitionInfo, spec sgdisk.Partition) + return nil + } + +-// partitionShouldBeInspected returns if the partition has zeroes that need to be resolved to sectors. +-func partitionShouldBeInspected(part sgdisk.Partition) bool { +- if part.Number == 0 { +- return false +- } +- return (part.StartSector != nil && *part.StartSector == 0) || +- (part.SizeInSectors != nil && *part.SizeInSectors == 0) +-} +- + func convertMiBToSectors(mib *int, sectorSize int) *int64 { + if mib != nil { + v := int64(*mib) * (1024 * 1024 / int64(sectorSize)) +@@ -130,10 +121,17 @@ func convertMiBToSectors(mib *int, sectorSize int) *int64 { + } + } + +-// getRealStartAndSize returns a map of partition numbers to a struct that contains what their real start +-// and end sector should be. It runs sgdisk --pretend to determine what the partitions would look like if +-// everything specified were to be (re)created. ++// getRealStartAndSize returns a copy of the given partition configuration with the real partition ++// numbers, start sectors, and end sectors filled in. It runs sgdisk --pretend to determine what the ++// partitions would look like if everything specified were to be (re)created. + func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo util.DiskInfo) ([]sgdisk.Partition, error) { ++ used := map[int]bool{} ++ ++ // Determine which partition numbers are already used. ++ for _, part := range diskInfo.Partitions { ++ used[part.Number] = true ++ } ++ + partitions := []sgdisk.Partition{} + for _, cpart := range dev.Partitions { + partitions = append(partitions, sgdisk.Partition{ +@@ -156,6 +154,10 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti + part.SizeInSectors = &info.SizeInSectors + } + } ++ if part.Number > 0 { ++ // Mark the partition number as used or not. ++ used[part.Number] = partitionShouldExist(part) ++ } + if partitionShouldExist(part) { + // Clear the label. sgdisk doesn't escape control characters. This makes parsing easier + part.Label = nil +@@ -163,12 +165,25 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti + } + } + +- // We only care to examine partitions that have start or size 0. ++ free := 1 + partitionsToInspect := []int{} +- for _, part := range partitions { +- if partitionShouldBeInspected(part) { +- op.Info(part.Number) +- partitionsToInspect = append(partitionsToInspect, part.Number) ++ for i := range partitions { ++ part := &partitions[i] ++ if partitionShouldExist(*part) { ++ // Find the next free partition number and use it. ++ if part.Number == 0 { ++ for used[free] { ++ free++ ++ } ++ part.Number = free ++ free++ ++ } ++ // We only care to examine partitions that have start or size 0. ++ if part.StartSector == nil || *part.StartSector == 0 || ++ part.SizeInSectors == nil || *part.SizeInSectors == 0 { ++ op.Info(part.Number) ++ partitionsToInspect = append(partitionsToInspect, part.Number) ++ } + } + } + +@@ -182,19 +197,14 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti + return nil, err + } + +- result := []sgdisk.Partition{} +- for _, part := range partitions { ++ for i := range partitions { ++ part := &partitions[i] + if dims, ok := realDimensions[part.Number]; ok { +- if part.StartSector != nil { +- part.StartSector = &dims.start +- } +- if part.SizeInSectors != nil { +- part.SizeInSectors = &dims.size +- } ++ part.StartSector = &dims.start ++ part.SizeInSectors = &dims.size + } +- result = append(result, part) + } +- return result, nil ++ return partitions, nil + } + + type sgdiskOutput struct { +-- +2.54.0 + diff --git a/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/files/0019-stages-disks-Make-getRealStartAndSize-return-a-map-l.patch b/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/files/0019-stages-disks-Make-getRealStartAndSize-return-a-map-l.patch deleted file mode 100644 index 16c9ad20ef7..00000000000 --- a/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/files/0019-stages-disks-Make-getRealStartAndSize-return-a-map-l.patch +++ /dev/null @@ -1,108 +0,0 @@ -From a14bde1c582771b17dfe378efcc213c854cabc45 Mon Sep 17 00:00:00 2001 -From: James Le Cuirot -Date: Mon, 11 May 2026 12:25:38 +0100 -Subject: [PATCH 19/20] stages/disks: Make getRealStartAndSize return a map - like it says it does - -This is useful for the code I'm about to add. Use int rather than uint64 -because that's what sgdisk.Partition.Number uses. That should be more -than big enough! - -Signed-off-by: James Le Cuirot ---- - internal/exec/stages/disks/partitions.go | 24 ++++++++++++------------ - 1 file changed, 12 insertions(+), 12 deletions(-) - -diff --git a/internal/exec/stages/disks/partitions.go b/internal/exec/stages/disks/partitions.go -index dd6acdac..327c7efd 100644 ---- a/internal/exec/stages/disks/partitions.go -+++ b/internal/exec/stages/disks/partitions.go -@@ -133,7 +133,7 @@ func convertMiBToSectors(mib *int, sectorSize int) *int64 { - // getRealStartAndSize returns a map of partition numbers to a struct that contains what their real start - // and end sector should be. It runs sgdisk --pretend to determine what the partitions would look like if - // everything specified were to be (re)created. --func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo util.DiskInfo) ([]sgdisk.Partition, error) { -+func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo util.DiskInfo) (map[int]sgdisk.Partition, error) { - partitions := []sgdisk.Partition{} - for _, cpart := range dev.Partitions { - partitions = append(partitions, sgdisk.Partition{ -@@ -182,7 +182,7 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti - return nil, err - } - -- result := []sgdisk.Partition{} -+ result := map[int]sgdisk.Partition{} - for _, part := range partitions { - if dims, ok := realDimensions[part.Number]; ok { - if part.StartSector != nil { -@@ -192,7 +192,7 @@ func (s stage) getRealStartAndSize(dev types.Disk, devAlias string, diskInfo uti - part.SizeInSectors = &dims.size - } - } -- result = append(result, part) -+ result[part.Number] = part - } - return result, nil - } -@@ -486,9 +486,9 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { - return err - } - -- var partxAdd []uint64 -- var partxDelete []uint64 -- var partxUpdate []uint64 -+ var partxAdd []int -+ var partxDelete []int -+ var partxUpdate []int - - for _, part := range resolvedPartitions { - shouldExist := partitionShouldExist(part) -@@ -510,13 +510,13 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { - case !exists && shouldExist: - op.CreatePartition(part) - modification = true -- partxAdd = append(partxAdd, uint64(part.Number)) -+ partxAdd = append(partxAdd, part.Number) - case exists && !shouldExist && !wipeEntry: - return fmt.Errorf("partition %d exists but is specified as nonexistant and wipePartitionEntry is false", part.Number) - case exists && !shouldExist && wipeEntry: - op.DeletePartition(part.Number) - modification = true -- partxDelete = append(partxDelete, uint64(part.Number)) -+ partxDelete = append(partxDelete, part.Number) - case exists && shouldExist && matches: - s.Info("partition %d found with correct specifications", part.Number) - case exists && shouldExist && !wipeEntry && !matches: -@@ -530,7 +530,7 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { - part.StartSector = &info.StartSector - op.CreatePartition(part) - modification = true -- partxUpdate = append(partxUpdate, uint64(part.Number)) -+ partxUpdate = append(partxUpdate, part.Number) - } else { - return fmt.Errorf("partition %d didn't match: %v", part.Number, matchErr) - } -@@ -539,7 +539,7 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { - op.DeletePartition(part.Number) - op.CreatePartition(part) - modification = true -- partxUpdate = append(partxUpdate, uint64(part.Number)) -+ partxUpdate = append(partxUpdate, part.Number) - default: - // unfortunatey, golang doesn't check that all cases are handled exhaustively - return fmt.Errorf("unreachable code reached when processing partition %d. golang--", part.Number) -@@ -558,9 +558,9 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { - // kernel partition table with BLKPG but only uses BLKRRPART which fails - // as soon as one partition of the disk is mounted - if len(activeParts) > 0 { -- runPartxCommand := func(op string, partitions []uint64) error { -+ runPartxCommand := func(op string, partitions []int) error { - for _, partNr := range partitions { -- cmd := exec.Command(distro.PartxCmd(), "--"+op, "--nr", strconv.FormatUint(partNr, 10), blockDevResolved) -+ cmd := exec.Command(distro.PartxCmd(), "--"+op, "--nr", fmt.Sprint(partNr), blockDevResolved) - if _, err := s.LogCmd(cmd, "triggering partition %d %s on %q", partNr, op, devAlias); err != nil { - return fmt.Errorf("partition %s failed: %v", op, err) - } --- -2.53.0 - diff --git a/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/files/0020-stages-disks-Allow-partx-to-fail-then-check-the-stat.patch b/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/files/0020-stages-disks-Allow-partx-to-fail-then-check-the-stat.patch index d360bbbe2cc..7ddc633622c 100644 --- a/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/files/0020-stages-disks-Allow-partx-to-fail-then-check-the-stat.patch +++ b/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/files/0020-stages-disks-Allow-partx-to-fail-then-check-the-stat.patch @@ -1,4 +1,4 @@ -From e9d0f43924886744830a0599880916aef643c66d Mon Sep 17 00:00:00 2001 +From 665ed65bcdcec1a6815a0ea537f46ea647e5c2e9 Mon Sep 17 00:00:00 2001 From: James Le Cuirot Date: Fri, 8 May 2026 17:58:38 +0100 Subject: [PATCH 20/20] stages/disks: Allow partx to fail then check the state @@ -13,18 +13,92 @@ the right start sector and size and that deleted partitions are absent once udev has settled. Signed-off-by: James Le Cuirot ---- - internal/exec/stages/disks/partitions.go | 62 ++++++++++++++++++++---- - 1 file changed, 53 insertions(+), 9 deletions(-) - -diff --git a/internal/exec/stages/disks/partitions.go b/internal/exec/stages/disks/partitions.go -index 327c7efd..86e4a4b8 100644 --- a/internal/exec/stages/disks/partitions.go +++ b/internal/exec/stages/disks/partitions.go -@@ -567,15 +567,9 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { +@@ -22,10 +22,12 @@ import ( + "bufio" + "errors" + "fmt" ++ "iter" + "os" + "os/exec" + "path/filepath" + "regexp" ++ "slices" + "sort" + "strconv" + "strings" +@@ -496,9 +498,9 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { + return err + } + +- var partxAdd []uint64 +- var partxDelete []uint64 +- var partxUpdate []uint64 ++ var partxAdd []int ++ var partxDelete []int ++ var partxUpdate []int + + for _, part := range resolvedPartitions { + shouldExist := partitionShouldExist(part) +@@ -520,13 +522,13 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { + case !exists && shouldExist: + op.CreatePartition(part) + modification = true +- partxAdd = append(partxAdd, uint64(part.Number)) ++ partxAdd = append(partxAdd, part.Number) + case exists && !shouldExist && !wipeEntry: + return fmt.Errorf("partition %d exists but is specified as nonexistant and wipePartitionEntry is false", part.Number) + case exists && !shouldExist && wipeEntry: + op.DeletePartition(part.Number) + modification = true +- partxDelete = append(partxDelete, uint64(part.Number)) ++ partxDelete = append(partxDelete, part.Number) + case exists && shouldExist && matches: + s.Info("partition %d found with correct specifications", part.Number) + case exists && shouldExist && !wipeEntry && !matches: +@@ -540,7 +542,7 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { + part.StartSector = &info.StartSector + op.CreatePartition(part) + modification = true +- partxUpdate = append(partxUpdate, uint64(part.Number)) ++ partxUpdate = append(partxUpdate, part.Number) + } else { + return fmt.Errorf("partition %d didn't match: %v", part.Number, matchErr) } - return nil - } +@@ -549,7 +551,7 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { + op.DeletePartition(part.Number) + op.CreatePartition(part) + modification = true +- partxUpdate = append(partxUpdate, uint64(part.Number)) ++ partxUpdate = append(partxUpdate, part.Number) + default: + // unfortunatey, golang doesn't check that all cases are handled exhaustively + return fmt.Errorf("unreachable code reached when processing partition %d. golang--", part.Number) +@@ -567,26 +569,22 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { + // In contrast to similar tools, sgdisk does not trigger the update of the + // kernel partition table with BLKPG but only uses BLKRRPART which fails + // as soon as one partition of the disk is mounted +- if len(activeParts) > 0 { +- runPartxCommand := func(op string, partitions []uint64) error { +- for _, partNr := range partitions { +- cmd := exec.Command(distro.PartxCmd(), "--"+op, "--nr", strconv.FormatUint(partNr, 10), blockDevResolved) +- if _, err := s.LogCmd(cmd, "triggering partition %d %s on %q", partNr, op, devAlias); err != nil { +- return fmt.Errorf("partition %s failed: %v", op, err) +- } ++ runPartxCommand := func(op string, partitions iter.Seq[int]) { ++ for partNr := range partitions { ++ // Don't use LogCmd here because we don't want to treat failure as ++ // critical and this command will never produce anything on Stdout. ++ cmd := exec.Command(distro.PartxCmd(), "--"+op, "--nr", fmt.Sprint(partNr), blockDevResolved) ++ s.Info("triggering partition %d %s on %q", partNr, op, devAlias) ++ s.Debug("executing: %q", cmd.Args) ++ _, err := cmd.Output() ++ if err, ok := err.(*exec.ExitError); ok { ++ s.Err("%v: Cmd: %q Stderr: %q", err, cmd.Args, err.Stderr) + } +- return nil +- } - if err := runPartxCommand("delete", partxDelete); err != nil { - return err - } @@ -33,69 +107,69 @@ index 327c7efd..86e4a4b8 100644 - } - if err := runPartxCommand("add", partxAdd); err != nil { - return err -- } -+ runPartxCommand("delete", partxDelete) -+ runPartxCommand("update", partxUpdate) -+ runPartxCommand("add", partxAdd) + } } ++ runPartxCommand("delete", slices.Values(partxDelete)) ++ runPartxCommand("update", slices.Values(partxUpdate)) ++ runPartxCommand("add", slices.Values(partxAdd)) // It's best to wait here for the /dev/ABC entries to be -@@ -586,5 +580,55 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { + // (re)created, not only for other parts of the initramfs but +@@ -596,5 +594,54 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error { return fmt.Errorf("failed to wait for udev on %q after partitioning: %v", devAlias, err) } -+ for _, partNum := range append(append([]int{}, partxAdd...), partxUpdate...) { -+ part := resolvedPartitions[partNum] -+ partDev := fmt.Sprintf("%s%s%d", blockDevResolved, prefix, partNum) -+ sysBlockDir := fmt.Sprintf("/sys/class/block/%s/", filepath.Base(partDev)) ++ for _, part := range resolvedPartitions { ++ partDev := fmt.Sprintf("%s%s%d", blockDevResolved, prefix, part.Number) + -+ // sysfs always reports in 512-byte sectors; convert our expected -+ // values from logical sectors to 512-byte sectors for comparison -+ logicalTo512 := int64(diskInfo.LogicalSectorSize) / 512 -+ -+ startStr, err := os.ReadFile(sysBlockDir + "start") -+ if err != nil { -+ return fmt.Errorf("failed to read start of %q from sysfs: %v", partDev, err) -+ } -+ kernelStart, err := strconv.ParseInt(strings.TrimSpace(string(startStr)), 10, 64) -+ if err != nil { -+ return fmt.Errorf("failed to parse start of %q from sysfs: %v", partDev, err) -+ } -+ if part.StartSector != nil { -+ expectedStart := *part.StartSector * logicalTo512 -+ if kernelStart != expectedStart { -+ return fmt.Errorf("kernel partition start for %q does not match expected (%d != %d 512-byte sectors)", partDev, kernelStart, expectedStart) ++ if slices.Contains(partxDelete, part.Number) { ++ _, err := os.Stat(partDev) ++ if err == nil { ++ return fmt.Errorf("%q unexpectedly exists after partitioning", partDev) ++ } else if !os.IsNotExist(err) { ++ return fmt.Errorf("failed to stat %q after partitioning: %v", partDev, err) + } -+ } ++ } else if slices.Contains(partxAdd, part.Number) || slices.Contains(partxUpdate, part.Number) { ++ sysBlockDir := fmt.Sprintf("/sys/class/block/%s/", filepath.Base(partDev)) + -+ sizeStr, err := os.ReadFile(sysBlockDir + "size") -+ if err != nil { -+ return fmt.Errorf("failed to read size of %q from sysfs: %v", partDev, err) -+ } -+ kernelSize, err := strconv.ParseInt(strings.TrimSpace(string(sizeStr)), 10, 64) -+ if err != nil { -+ return fmt.Errorf("failed to parse size of %q from sysfs: %v", partDev, err) -+ } -+ if part.SizeInSectors != nil { -+ expectedSize := *part.SizeInSectors * logicalTo512 -+ if kernelSize != expectedSize { -+ return fmt.Errorf("kernel partition size for %q does not match expected (%d != %d 512-byte sectors)", partDev, kernelSize, expectedSize) ++ // sysfs always reports in 512-byte sectors; convert our expected ++ // values from logical sectors to 512-byte sectors for comparison ++ logicalTo512 := int64(diskInfo.LogicalSectorSize) / 512 ++ ++ startStr, err := os.ReadFile(sysBlockDir + "start") ++ if err != nil { ++ return fmt.Errorf("failed to read start of %q from sysfs: %v", partDev, err) ++ } ++ kernelStart, err := strconv.ParseInt(strings.TrimSpace(string(startStr)), 10, 64) ++ if err != nil { ++ return fmt.Errorf("failed to parse start of %q from sysfs: %v", partDev, err) ++ } ++ if part.StartSector != nil { ++ expectedStart := *part.StartSector * logicalTo512 ++ if kernelStart != expectedStart { ++ return fmt.Errorf("kernel partition start for %q does not match expected (%d != %d 512-byte sectors)", partDev, kernelStart, expectedStart) ++ } + } -+ } -+ } + -+ for _, partNum := range partxDelete { -+ partDev := fmt.Sprintf("%s%s%d", blockDevResolved, prefix, partNum) -+ _, err := os.Stat(partDev) -+ if err == nil { -+ return fmt.Errorf("%q unexpectedly exists after partitioning", partDev) -+ } else if !os.IsNotExist(err) { -+ return fmt.Errorf("failed to stat %q after partitioning: %v", partDev, err) ++ sizeStr, err := os.ReadFile(sysBlockDir + "size") ++ if err != nil { ++ return fmt.Errorf("failed to read size of %q from sysfs: %v", partDev, err) ++ } ++ kernelSize, err := strconv.ParseInt(strings.TrimSpace(string(sizeStr)), 10, 64) ++ if err != nil { ++ return fmt.Errorf("failed to parse size of %q from sysfs: %v", partDev, err) ++ } ++ if part.SizeInSectors != nil { ++ expectedSize := *part.SizeInSectors * logicalTo512 ++ if kernelSize != expectedSize { ++ return fmt.Errorf("kernel partition size for %q does not match expected (%d != %d 512-byte sectors)", partDev, kernelSize, expectedSize) ++ } ++ } + } + } + return nil } -- -2.53.0 +2.54.0 diff --git a/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/ignition-2.24.0-r3.ebuild b/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/ignition-2.24.0-r4.ebuild similarity index 100% rename from sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/ignition-2.24.0-r3.ebuild rename to sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/ignition-2.24.0-r4.ebuild diff --git a/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/ignition-9999.ebuild b/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/ignition-9999.ebuild index a5c521a1a6b..14f7919c935 100644 --- a/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/ignition-9999.ebuild +++ b/sdk_container/src/third_party/coreos-overlay/sys-apps/ignition/ignition-9999.ebuild @@ -34,7 +34,7 @@ else "${FILESDIR}/0016-docs-Add-re-added-platforms-to-docs-to-pass-tests.patch" "${FILESDIR}/0017-usr-share-oem-oem.patch" "${FILESDIR}/0018-internal-exec-stages-mount-Mount-oem.patch" - "${FILESDIR}/0019-stages-disks-Make-getRealStartAndSize-return-a-map-l.patch" + "${FILESDIR}/0019-stages-disks-Fix-giving-partition-number-0-to-get-th.patch" "${FILESDIR}/0020-stages-disks-Allow-partx-to-fail-then-check-the-stat.patch" ) fi