restore-test: add rootfs override alongside bind-mount overrides

PVE refuses a restore carrying mountpoint params unless rootfs is also set ("mount
points configured, but 'rootfs' not set"). Size the rootfs override from the source
rootfs (rootfsSizeGB/sizeToGB, round up). Validated manually on the host: restore of
bind-mounted 9201 with --rootfs + --mp0/--mp9 overrides boots + tears down cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 20:54:46 +02:00
parent 7fb7ef5c3d
commit b66c137aa7
2 changed files with 73 additions and 4 deletions
+57 -4
View File
@@ -3,6 +3,7 @@ package reconcile
import (
"context"
"fmt"
"math"
"regexp"
"strconv"
"strings"
@@ -169,10 +170,21 @@ func (e *Engine) runScratchTest(ctx context.Context, vmid int, spec RestoreTestS
var mountOverrides map[string]string
if srcVMID, ok := archiveVMID(spec.Archive); ok {
if srcCfg, cerr := e.api.GuestConfig(ctx, srcVMID); cerr == nil {
mountOverrides = bindMountOverrides(srcCfg.MountPoints(), spec.RestoreStorage)
if len(mountOverrides) > 0 {
e.logger.Info("restore-test: neutralizing source bind-mount mountpoints for scratch restore",
"source_vmid", srcVMID, "scratch", vmid, "count", len(mountOverrides))
if binds := bindMountOverrides(srcCfg.MountPoints(), spec.RestoreStorage); len(binds) > 0 {
// PVE refuses a restore that carries mountpoint params unless `rootfs` is also set
// ("mount points configured, but 'rootfs' not set"). Size the rootfs override from the
// SOURCE rootfs (restore needs target >= the archive's volume). Without a parseable
// size we can't safely override, so restore as-is (the bind-mount restore then fails
// in the verdict rather than risking a wrong rootfs size).
if sz := rootfsSizeGB(srcCfg.RootFS); sz > 0 {
binds["rootfs"] = fmt.Sprintf("%s:%d", spec.RestoreStorage, sz)
mountOverrides = binds
e.logger.Info("restore-test: neutralizing source bind-mount mountpoints for scratch restore",
"source_vmid", srcVMID, "scratch", vmid, "bind_mounts", len(binds)-1, "rootfs_gb", sz)
} else {
e.logger.Warn("restore-test: source has bind mounts but rootfs size unparseable — restoring as-is",
"source_vmid", srcVMID, "rootfs", srcCfg.RootFS)
}
}
} else {
e.logger.Warn("restore-test: could not read source config for mp overrides (restoring as-is)",
@@ -299,6 +311,47 @@ func mountPathOf(opts string) string {
return ""
}
// rootfsSizeGB parses the GB size from a volume spec's "size=<N><unit>" field (e.g.
// "local-lvm:vm-9201-disk-0,size=8G"), rounding UP to whole GB — a restore needs the target volume
// >= the archive's. Returns 0 when no size field is present (caller then skips the override).
func rootfsSizeGB(spec string) int {
for _, kv := range strings.Split(spec, ",") {
if v, ok := strings.CutPrefix(kv, "size="); ok {
return sizeToGB(v)
}
}
return 0
}
// sizeToGB converts a PVE size string ("8G", "512M", "1T", "8192K") to whole GB, rounding up (min 1
// when positive). Returns 0 on a malformed value.
func sizeToGB(s string) int {
if s == "" {
return 0
}
mult := 1.0 // default GB if no recognized unit suffix
num := s
switch s[len(s)-1] {
case 'T', 't':
mult, num = 1024, s[:len(s)-1]
case 'G', 'g':
mult, num = 1, s[:len(s)-1]
case 'M', 'm':
mult, num = 1.0/1024, s[:len(s)-1]
case 'K', 'k':
mult, num = 1.0/(1024*1024), s[:len(s)-1]
}
f, err := strconv.ParseFloat(num, 64)
if err != nil || f <= 0 {
return 0
}
gb := int(math.Ceil(f * mult))
if gb < 1 {
gb = 1
}
return gb
}
// teardownScratch destroys the scratch guest (benign, gated) and records the entry terminal.
// On any teardown failure it leaves the entry in-flight so Recover reaps the guest later.
func (e *Engine) teardownScratch(ctx context.Context, base JournalEntry) {
@@ -46,3 +46,19 @@ func TestBindMountOverrides(t *testing.T) {
t.Error("expected nil when there are no bind mounts")
}
}
func TestRootfsSizeGB(t *testing.T) {
cases := map[string]int{
"local-lvm:vm-9201-disk-0,size=8G": 8,
"local-lvm:base,size=512M": 1, // rounds up
"local-lvm:base,size=1T": 1024,
"local-lvm:base,size=0": 0,
"local-lvm:base": 0, // no size field
"local-lvm:base,size=8": 8, // unit-less → GB
}
for spec, want := range cases {
if got := rootfsSizeGB(spec); got != want {
t.Errorf("rootfsSizeGB(%q) = %d, want %d", spec, got, want)
}
}
}