Skip to content
Draft
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
3 changes: 3 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ nav_order: 9

### Bug fixes

- Fix giving disk partition number 0 to get the next available slot. This caused the disks stage to fail since version 2.20.0. ([#2234](https://github.com/coreos/ignition/pull/2234))
- Fix disk partitioning race condition where the kernel is already aware of the changes before running `partx`, causing a fatal error. ([#2234](https://github.com/coreos/ignition/pull/2234))


## Ignition 2.26.0 (2026-02-17)

Expand Down
110 changes: 87 additions & 23 deletions internal/exec/stages/disks/partitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
"bufio"
"errors"
"fmt"
"iter"
"os"
"os/exec"
"path/filepath"
"regexp"
"slices"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -130,10 +132,17 @@
}
}

// 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 := []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{
Expand All @@ -156,6 +165,10 @@
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
Expand All @@ -182,9 +195,17 @@
return nil, err
}

free := 1
result := []sgdisk.Partition{}
for _, part := range partitions {
if dims, ok := realDimensions[part.Number]; ok {
if part.Number == 0 {
// Find the next free partition number and use it.
for used[free] {
free++
}
part.Number = free
free++
} else if dims, ok := realDimensions[part.Number]; ok {
if part.StartSector != nil {
part.StartSector = &dims.start
}
Expand Down Expand Up @@ -486,9 +507,9 @@
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)
Expand All @@ -510,13 +531,13 @@
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:
Expand All @@ -530,7 +551,7 @@
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)
}
Expand All @@ -539,7 +560,7 @@
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)
Expand All @@ -558,24 +579,18 @@
// 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)
runPartxCommand := func(op string, partitions iter.Seq[int]) error {
for partNr := range partitions {
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)
}
}
return nil
}
if err := runPartxCommand("delete", partxDelete); err != nil {
return err
}
if err := runPartxCommand("update", partxUpdate); err != nil {
return err
}
if err := runPartxCommand("add", partxAdd); err != nil {
return err
}
runPartxCommand("delete", slices.Values(partxDelete))

Check failure on line 591 in internal/exec/stages/disks/partitions.go

View workflow job for this annotation

GitHub Actions / Test (1.25.x)

Error return value is not checked (errcheck)

Check failure on line 591 in internal/exec/stages/disks/partitions.go

View workflow job for this annotation

GitHub Actions / Test (1.26.x)

Error return value is not checked (errcheck)
runPartxCommand("update", slices.Values(partxUpdate))

Check failure on line 592 in internal/exec/stages/disks/partitions.go

View workflow job for this annotation

GitHub Actions / Test (1.25.x)

Error return value is not checked (errcheck)

Check failure on line 592 in internal/exec/stages/disks/partitions.go

View workflow job for this annotation

GitHub Actions / Test (1.26.x)

Error return value is not checked (errcheck)
runPartxCommand("add", slices.Values(partxAdd))

Check failure on line 593 in internal/exec/stages/disks/partitions.go

View workflow job for this annotation

GitHub Actions / Test (1.25.x)

Error return value is not checked (errcheck)

Check failure on line 593 in internal/exec/stages/disks/partitions.go

View workflow job for this annotation

GitHub Actions / Test (1.26.x)

Error return value is not checked (errcheck)
}

// It's best to wait here for the /dev/ABC entries to be
Expand All @@ -586,5 +601,54 @@
return fmt.Errorf("failed to wait for udev on %q after partitioning: %v", devAlias, err)
}

for _, part := range resolvedPartitions {
partDev := fmt.Sprintf("%s%s%d", blockDevResolved, prefix, part.Number)

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 {
sysBlockDir := fmt.Sprintf("/sys/class/block/%s/", filepath.Base(partDev))

// 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)
}
}

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
}
Loading