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 (
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user