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
55 changes: 54 additions & 1 deletion run_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"

"github.com/diskfs/go-diskfs/disk"
"github.com/diskfs/go-diskfs/partition/gpt"
Expand Down Expand Up @@ -66,7 +69,15 @@ func resizeFilesystem(
}
switch deviceType {
case disk.DeviceTypeBlockDevice:
return execResize2fs(device, newSizeMB, fixErrors)
// resize2fs takes the *partition* device, not the whole-disk
// device, so we resolve "/dev/sda" + partition number 9 to
// "/dev/sda9" (or whatever the kernel calls that slot —
// "/dev/nvme0n1p9", "/dev/mmcblk0p9", etc.) via sysfs.
partDevice, err := partitionDevicePath(device, filesystemData.number, "")
if err != nil {
return fmt.Errorf("cannot find partition device for %s partition %d: %w", device, filesystemData.number, err)
}
return execResize2fs(partDevice, newSizeMB, fixErrors)
case disk.DeviceTypeFile:
// copy the partition, then resize it, then copy it back into the original disk image
tmpFile, err2 := os.CreateTemp("", partTmpFilename)
Expand Down Expand Up @@ -158,3 +169,45 @@ func planResizes(
// recalculate resizes with shrinking
return calculateResizes(d.Size, table.Partitions, prTargetsWithShrink)
}

// partitionDevicePath maps a whole-disk path (e.g. "/dev/sda") and a
// partition number to the partition's device path (e.g. "/dev/sda9",
// "/dev/nvme0n1p9", "/dev/mmcblk0p9").
//
// Naming conventions for partition device nodes differ by disk type,
// so we look up the kernel partition name via sysfs rather than
// hardcoding the convention: each /sys/class/block/<disk>/<part>/
// directory holds a "partition" file containing the partition number
// and a directory named after the kernel partition name.
//
// If syspath is empty, /sys is used. Returns an error if no matching
// partition is found under sysfs.
func partitionDevicePath(diskPath string, partNumber int, syspath string) (string, error) {
if syspath == "" {
syspath = sysDefaultPath
}
diskBase := filepath.Base(diskPath)
diskSysDir := filepath.Join(syspath, "class", "block", diskBase)
entries, err := os.ReadDir(diskSysDir)
if err != nil {
return "", fmt.Errorf("read sysfs dir %s: %w", diskSysDir, err)
}
for _, e := range entries {
if !e.IsDir() {
continue
}
partFile := filepath.Join(diskSysDir, e.Name(), "partition")
raw, err := os.ReadFile(partFile)
if err != nil {
continue
}
n, err := strconv.Atoi(strings.TrimSpace(string(raw)))
if err != nil {
continue
}
if n == partNumber {
return filepath.Join("/dev", e.Name()), nil
}
}
return "", fmt.Errorf("partition %d not found under %s", partNumber, diskSysDir)
}
83 changes: 83 additions & 0 deletions run_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
Expand Down Expand Up @@ -194,3 +195,85 @@ func TestPlanResizes(t *testing.T) {
})
})
}

// TestPartitionDevicePath verifies that partitionDevicePath resolves
// a whole-disk path + partition number to the kernel-named partition
// device path via a sysfs lookup. Two fake-sysfs trees cover the
// common naming conventions:
//
// - sda → sda9 (the traditional convention used by most SATA/SCSI
// devices, where the partition number is appended directly).
// - nvme0n1 → nvme0n1p9 ("p" prefix before the number, used by
// NVMe, eMMC, and other devices whose name already ends in a
// digit so a bare "9" would be ambiguous).
//
// Hardcoding the convention based on the disk path is the wrong
// approach (it gets mmcblk, nvme, virtblk, loop, and similar wrong),
// which is why we use a sysfs lookup instead.
func TestPartitionDevicePath(t *testing.T) {
tmp := t.TempDir()
sysClassBlock := filepath.Join(tmp, "class", "block")

// Set up fake sda with partitions sda1 and sda9.
for _, p := range []struct {
name string
num string
}{
{"sda1", "1"},
{"sda9", "9"},
} {
if err := os.MkdirAll(filepath.Join(sysClassBlock, "sda", p.name), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sysClassBlock, "sda", p.name, "partition"), []byte(p.num+"\n"), 0o644); err != nil {
t.Fatal(err)
}
}

// Set up fake nvme0n1 with partitions nvme0n1p1 and nvme0n1p9.
for _, p := range []struct {
name string
num string
}{
{"nvme0n1p1", "1"},
{"nvme0n1p9", "9"},
} {
if err := os.MkdirAll(filepath.Join(sysClassBlock, "nvme0n1", p.name), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sysClassBlock, "nvme0n1", p.name, "partition"), []byte(p.num+"\n"), 0o644); err != nil {
t.Fatal(err)
}
}

t.Run("sda partition 9", func(t *testing.T) {
got, err := partitionDevicePath("/dev/sda", 9, tmp)
if err != nil {
t.Fatalf("partitionDevicePath: %v", err)
}
if got != "/dev/sda9" {
t.Errorf("partitionDevicePath = %q, want /dev/sda9", got)
}
})
t.Run("nvme0n1 partition 9", func(t *testing.T) {
got, err := partitionDevicePath("/dev/nvme0n1", 9, tmp)
if err != nil {
t.Fatalf("partitionDevicePath: %v", err)
}
if got != "/dev/nvme0n1p9" {
t.Errorf("partitionDevicePath = %q, want /dev/nvme0n1p9", got)
}
})
t.Run("partition not found", func(t *testing.T) {
_, err := partitionDevicePath("/dev/sda", 42, tmp)
if err == nil {
t.Fatal("expected error for non-existent partition number")
}
})
t.Run("disk not found", func(t *testing.T) {
_, err := partitionDevicePath("/dev/sdz", 1, tmp)
if err == nil {
t.Fatal("expected error for non-existent disk")
}
})
}