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:
@@ -3,6 +3,7 @@ package reconcile
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -169,10 +170,21 @@ func (e *Engine) runScratchTest(ctx context.Context, vmid int, spec RestoreTestS
|
|||||||
var mountOverrides map[string]string
|
var mountOverrides map[string]string
|
||||||
if srcVMID, ok := archiveVMID(spec.Archive); ok {
|
if srcVMID, ok := archiveVMID(spec.Archive); ok {
|
||||||
if srcCfg, cerr := e.api.GuestConfig(ctx, srcVMID); cerr == nil {
|
if srcCfg, cerr := e.api.GuestConfig(ctx, srcVMID); cerr == nil {
|
||||||
mountOverrides = bindMountOverrides(srcCfg.MountPoints(), spec.RestoreStorage)
|
if binds := bindMountOverrides(srcCfg.MountPoints(), spec.RestoreStorage); len(binds) > 0 {
|
||||||
if len(mountOverrides) > 0 {
|
// PVE refuses a restore that carries mountpoint params unless `rootfs` is also set
|
||||||
e.logger.Info("restore-test: neutralizing source bind-mount mountpoints for scratch restore",
|
// ("mount points configured, but 'rootfs' not set"). Size the rootfs override from the
|
||||||
"source_vmid", srcVMID, "scratch", vmid, "count", len(mountOverrides))
|
// 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 {
|
} else {
|
||||||
e.logger.Warn("restore-test: could not read source config for mp overrides (restoring as-is)",
|
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 ""
|
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.
|
// 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.
|
// 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) {
|
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")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user