Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sgdisk: Run partx after partition changes #1717

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ nav_order: 9

### Features

- Support partitioning disk with mounted partitions

### Changes

- The Dracut module now installs partx

### Bug fixes

- Fix Akamai Ignition base64 decoding on padded payloads
Expand Down
1 change: 1 addition & 0 deletions dracut/30ignition/module-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ install() {
mkfs.fat \
mkfs.xfs \
mkswap \
partx \
pothos marked this conversation as resolved.
Show resolved Hide resolved
sgdisk \
useradd \
userdel \
Expand Down
2 changes: 2 additions & 0 deletions internal/distro/distro.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var (
groupdelCmd = "groupdel"
mdadmCmd = "mdadm"
mountCmd = "mount"
partxCmd = "partx"
pothos marked this conversation as resolved.
Show resolved Hide resolved
sgdiskCmd = "sgdisk"
modprobeCmd = "modprobe"
udevadmCmd = "udevadm"
Expand Down Expand Up @@ -92,6 +93,7 @@ func GroupaddCmd() string { return groupaddCmd }
func GroupdelCmd() string { return groupdelCmd }
func MdadmCmd() string { return mdadmCmd }
func MountCmd() string { return mountCmd }
func PartxCmd() string { return partxCmd }
pothos marked this conversation as resolved.
Show resolved Hide resolved
func SgdiskCmd() string { return sgdiskCmd }
func ModprobeCmd() string { return modprobeCmd }
func UdevadmCmd() string { return udevadmCmd }
Expand Down
166 changes: 166 additions & 0 deletions internal/exec/stages/disks/partitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,23 @@
package disks

import (
"bufio"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"

cutil "github.com/coreos/ignition/v2/config/util"
"github.com/coreos/ignition/v2/config/v3_5_experimental/types"
"github.com/coreos/ignition/v2/internal/distro"
"github.com/coreos/ignition/v2/internal/exec/util"
"github.com/coreos/ignition/v2/internal/sgdisk"
iutil "github.com/coreos/ignition/v2/internal/util"
)

var (
Expand Down Expand Up @@ -317,11 +323,126 @@ func (p PartitionList) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}

// Expects a /dev/xyz path
func blockDevHeld(blockDevResolved string) (bool, error) {
_, blockDevNode := filepath.Split(blockDevResolved)

holdersDir := fmt.Sprintf("/sys/class/block/%s/holders/", blockDevNode)
entries, err := os.ReadDir(holdersDir)
if err != nil {
return false, fmt.Errorf("failed to retrieve holders of %q: %v", blockDevResolved, err)
}
return len(entries) > 0, nil
}

// Expects a /dev/xyz path
func blockDevMounted(blockDevResolved string) (bool, error) {
mounts, err := os.Open("/proc/mounts")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit heavy to parse /proc/mounts on every block device. Ideally, we'd do this once. In practice, I don't think it matters too much.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there shouldn't be millions of mount points in the initrd - if that ever is the case it can be cached.

if err != nil {
return false, fmt.Errorf("failed to open /proc/mounts: %v", err)
}
scanner := bufio.NewScanner(mounts)
prestist marked this conversation as resolved.
Show resolved Hide resolved
for scanner.Scan() {
mountSource := strings.Split(scanner.Text(), " ")[0]
if strings.HasPrefix(mountSource, "/") {
mountSourceResolved, err := filepath.EvalSymlinks(mountSource)
if err != nil {
return false, fmt.Errorf("failed to resolve %q: %v", mountSource, err)
}
if mountSourceResolved == blockDevResolved {
return true, nil
}
}
}
if err := scanner.Err(); err != nil {
return false, fmt.Errorf("failed to check mounts for %q: %v", blockDevResolved, err)
}
return false, nil
}

// Expects a /dev/xyz path
func blockDevPartitions(blockDevResolved string) ([]string, error) {
_, blockDevNode := filepath.Split(blockDevResolved)

// This also works for extended MBR partitions
sysDir := fmt.Sprintf("/sys/class/block/%s/", blockDevNode)
entries, err := os.ReadDir(sysDir)
if err != nil {
return nil, fmt.Errorf("failed to retrieve sysfs entries of %q: %v", blockDevResolved, err)
}
var partitions []string
for _, entry := range entries {
if strings.HasPrefix(entry.Name(), blockDevNode) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is that the canonical way to get disk partitions? I wonder how libblkid does it. Which actually, we do link to it in Ignition.

So another way to get this info is to use e.g. getPartitionMap() in this file which calls out to libblkid. But I think that function assumes that the passed device is a disk so it wouldn't work when recursing into the partition in blockDevInUse(). But we could also restructure blockDevInUse() so that the held and mounted checks are their own functions and then not recurse (which anyway feels a bit like a weird thing to do to avoid even looking for partitions on partitions).

Not a blocker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't have time to investigate this idea and will leave it as is

partitions = append(partitions, "/dev/"+entry.Name())
}
}

return partitions, nil
}

// Expects a /dev/xyz path
func blockDevInUse(blockDevResolved string, skipPartitionCheck bool) (bool, []string, error) {
// Note: This ignores swap and LVM usage
inUse := false
held, err := blockDevHeld(blockDevResolved)
if err != nil {
return false, nil, fmt.Errorf("failed to check if %q is held: %v", blockDevResolved, err)
}
mounted, err := blockDevMounted(blockDevResolved)
if err != nil {
return false, nil, fmt.Errorf("failed to check if %q is mounted: %v", blockDevResolved, err)
}
inUse = held || mounted
if skipPartitionCheck {
return inUse, nil, nil
}
partitions, err := blockDevPartitions(blockDevResolved)
if err != nil {
return false, nil, fmt.Errorf("failed to retrieve partitions of %q: %v", blockDevResolved, err)
}
var activePartitions []string
for _, partition := range partitions {
partInUse, _, err := blockDevInUse(partition, true)
if err != nil {
return false, nil, fmt.Errorf("failed to check if partition %q is in use: %v", partition, err)
}
if partInUse {
activePartitions = append(activePartitions, partition)
inUse = true
}
}
return inUse, activePartitions, nil
}

// Expects a /dev/xyz path
func partitionNumberPrefix(blockDevResolved string) string {
lastChar := blockDevResolved[len(blockDevResolved)-1]
if '0' <= lastChar && lastChar <= '9' {
return "p"
}
return ""
}

// partitionDisk partitions devAlias according to the spec given by dev
func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
blockDevResolved, err := filepath.EvalSymlinks(devAlias)
if err != nil {
return fmt.Errorf("failed to resolve %q: %v", devAlias, err)
}

inUse, activeParts, err := blockDevInUse(blockDevResolved, false)
if err != nil {
return fmt.Errorf("failed usage check on %q: %v", devAlias, err)
}
if inUse && len(activeParts) == 0 {
return fmt.Errorf("refusing to operate on directly active disk %q", devAlias)
}
if cutil.IsTrue(dev.WipeTable) {
op := sgdisk.Begin(s.Logger, devAlias)
s.Logger.Info("wiping partition table requested on %q", devAlias)
if len(activeParts) > 0 {
return fmt.Errorf("refusing to wipe active disk %q", devAlias)
}
op.WipeTable(true)
if err := op.Commit(); err != nil {
// `sgdisk --zap-all` will exit code 2 if the table was corrupted; retry it
Expand All @@ -343,6 +464,8 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
return err
}

prefix := partitionNumberPrefix(blockDevResolved)

// get a list of parititions that have size and start 0 replaced with the real sizes
// that would be used if all specified partitions were to be created anew.
// Also calculate sectors for all of the start/size values.
Expand All @@ -351,6 +474,10 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
return err
}

var partxAdd []uint64
var partxDelete []uint64
var partxUpdate []uint64

for _, part := range resolvedPartitions {
shouldExist := partitionShouldExist(part)
info, exists := diskInfo.GetPartition(part.Number)
Expand All @@ -360,17 +487,24 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
}
matches := exists && matchErr == nil
wipeEntry := cutil.IsTrue(part.WipePartitionEntry)
partInUse := iutil.StrSliceContains(activeParts, fmt.Sprintf("%s%s%d", blockDevResolved, prefix, part.Number))

var modification bool

// This is a translation of the matrix in the operator notes.
switch {
case !exists && !shouldExist:
s.Logger.Info("partition %d specified as nonexistant and no partition was found. Success.", part.Number)
case !exists && shouldExist:
op.CreatePartition(part)
modification = true
partxAdd = append(partxAdd, uint64(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))
case exists && shouldExist && matches:
s.Logger.Info("partition %d found with correct specifications", part.Number)
case exists && shouldExist && !wipeEntry && !matches:
Expand All @@ -383,23 +517,55 @@ func (s stage) partitionDisk(dev types.Disk, devAlias string) error {
part.Label = &info.Label
part.StartSector = &info.StartSector
op.CreatePartition(part)
modification = true
partxUpdate = append(partxUpdate, uint64(part.Number))
} else {
return fmt.Errorf("Partition %d didn't match: %v", part.Number, matchErr)
}
case exists && shouldExist && wipeEntry && !matches:
s.Logger.Info("partition %d did not meet specifications, wiping partition entry and recreating", part.Number)
op.DeletePartition(part.Number)
op.CreatePartition(part)
modification = true
partxUpdate = append(partxUpdate, uint64(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)
}

if partInUse && modification {
return fmt.Errorf("refusing to modify active partition %d on %q", part.Number, devAlias)
}
}

if err := op.Commit(); err != nil {
return fmt.Errorf("commit failure: %v", err)
}

// 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.Logger.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
}
}

// It's best to wait here for the /dev/ABC entries to be
// (re)created, not only for other parts of the initramfs but
// also because s.waitOnDevices() can still race with udev's
Expand Down
Loading