diff --git a/run_helpers.go b/run_helpers.go index fe8f331..0fc0eac 100644 --- a/run_helpers.go +++ b/run_helpers.go @@ -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" @@ -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) @@ -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/// +// 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) +} diff --git a/run_helpers_test.go b/run_helpers_test.go index eca3971..dcfe40f 100644 --- a/run_helpers_test.go +++ b/run_helpers_test.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io/fs" + "os" "path/filepath" "strings" "testing" @@ -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") + } + }) +}