//go:build linux && rootrunner package host import ( "context" "os" "os/exec" "path/filepath" "strconv" "strings" "testing" "time" "golang.org/x/sys/unix" ) func TestRootRebootServingParkGrownDiskPhase1(t *testing.T) { TestRootRebootServingParkChangedMountPhase1(t) } func TestRootRebootServingParkGrownDiskPhase2(t *testing.T) { TestRootRebootServingParkChangedMountPhase2(t) } // Capture retirement on a small ext4 filesystem, grow it online, then attach it // under a different device number after reboot. Neither identity change is a seam. func servingRunnerGrownDisk(t *testing.T, data string, first bool) func() { t.Helper() command := func(args ...string) string { t.Helper() out, err := exec.CommandContext(t.Context(), args[0], args[1:]...).CombinedOutput() if err != nil { t.Fatalf("grown disk %q: %v %s", args, err, out) } return strings.TrimSpace(string(out)) } image := filepath.Join(servingRebootRoot, "grow.ext4") deviceFile := filepath.Join(servingRebootRoot, "grow.device") uuidFile := filepath.Join(servingRebootRoot, "grow.uuid") create := func(path string, size int64) { t.Helper() f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0600) if err != nil { t.Fatal(err) } err = f.Truncate(size) closeErr := f.Close() if err != nil || closeErr != nil { t.Fatal(err, closeErr) } } detach := func(device string) { t.Cleanup(func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if out, err := exec.CommandContext(ctx, "losetup", "--detach", device).CombinedOutput(); err != nil { t.Errorf("detach %s: %v %s", device, err, out) } }) } if first { if err := os.Mkdir(data, 0700); err != nil { t.Fatal(err) } create(image, 128<<20) command("mkfs.ext4", "-q", "-F", image) } else { old, err := os.ReadFile(deviceFile) if err != nil { t.Fatal(err) } suffix := strings.TrimPrefix(string(old), "/dev/loop") if suffix == string(old) { t.Fatal("invalid loop path") } if _, err := strconv.ParseUint(suffix, 10, 32); err != nil { t.Fatal(err) } blocker := filepath.Join(servingRebootRoot, "blocker") create(blocker, 1<<20) command("losetup", string(old), blocker) detach(string(old)) } device := command("losetup", "--find", "--show", image) detach(device) uuid := command("blkid", "-s", "UUID", "-o", "value", device) if first { if err := os.WriteFile(deviceFile, []byte(device), 0600); err != nil { t.Fatal(err) } if err := os.WriteFile(uuidFile, []byte(uuid), 0600); err != nil { t.Fatal(err) } } else { old, err := os.ReadFile(deviceFile) if err != nil || string(old) == device { t.Fatal("device did not change", err) } saved, err := os.ReadFile(uuidFile) if err != nil || uuid == "" || string(saved) != uuid { t.Fatal("filesystem UUID changed", err) } } if err := unix.Mount(device, data, "ext4", unix.MS_NOSUID|unix.MS_NODEV, ""); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := unix.Unmount(data, 0); err != nil { t.Error(err) } }) var st unix.Statfs_t if err := unix.Statfs(data, &st); err != nil { t.Fatal(err) } t.Logf("grown filesystem: device=%s uuid=%s blocks=%d blocksize=%d", device, uuid, st.Blocks, st.Bsize) if !first { if st.Blocks*uint64(st.Bsize) <= 128<<20 { t.Fatal("grown capacity lost across reboot") } return nil } return func() { t.Helper() if err := os.Truncate(image, 256<<20); err != nil { t.Fatal(err) } command("losetup", "--set-capacity", device) t.Log(command("resize2fs", device)) var grown unix.Statfs_t if err := unix.Statfs(data, &grown); err != nil { t.Fatal(err) } if grown.Blocks <= st.Blocks || grown.Fsid != st.Fsid || command("blkid", "-s", "UUID", "-o", "value", device) != uuid { t.Fatal("grow changed identity or did not increase capacity") } command("sync", "-f", data) t.Logf("grew filesystem: blocks %d -> %d, filesystem identity unchanged", st.Blocks, grown.Blocks) } }