GL-5b: restore-test full-fidelity verification (v0.76.0)
The restore-test had GL-5 finding #2's mirror image: its live-source-config bind-override path tripped PVE's drop-unlisted-mountpoints rule, so scratch guests boot-verified WITHOUT their storage mpN - weaker verification than claimed. Params now derive from the ARCHIVE's own embedded config via ExtractArchiveConfig + drRestoreOverrides (the object under test; full layout, content genuinely extracted - the added runtime IS the verification); unreadable/unknown-topology archives refuse up front. NEW mount-parity assert (2b, pre-start): restored mpN set vs the archive's - missing/mispathed/undersized/extra mpN fail the test naming the delta, so constraint (b) can never regress into a green light. MountParity + MountInventory ride the result + hub wire record (additive). Dead bindMountOverrides/archiveVMID path deleted with its tests (no reachable lookalike). DR bring-up untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -1,51 +1,13 @@
|
||||
package reconcile
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArchiveVMID(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
volid string
|
||||
want int
|
||||
ok bool
|
||||
}{
|
||||
"pbs ct": {"felhom-pbs:backup/ct/9201/2026-06-12T18:29:58Z", 9201, true},
|
||||
"pbs vm": {"felhom-pbs:backup/vm/142/2026-06-12T18:29:58Z", 142, true},
|
||||
"vzdump lxc": {"local:backup/vzdump-lxc-9201-2026_06_12-18_29_58.tar.zst", 9201, true},
|
||||
"vzdump qemu": {"local:backup/vzdump-qemu-100-2026_06_12.vma.zst", 100, true},
|
||||
"none": {"local:iso/whatever.iso", 0, false},
|
||||
}
|
||||
for name, c := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, ok := archiveVMID(c.volid)
|
||||
if ok != c.ok || got != c.want {
|
||||
t.Errorf("archiveVMID(%q) = (%d,%v), want (%d,%v)", c.volid, got, ok, c.want, c.ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindMountOverrides(t *testing.T) {
|
||||
mps := map[string]string{
|
||||
"mp0": "/mnt/felhom-usb/felhom-data,mp=/mnt/felhom-usb", // bind mount → override
|
||||
"mp1": "local-lvm:8,mp=/data,backup=0", // real volume → left alone
|
||||
"mp2": "/srv/extra", // bind mount, no explicit mp= → use host path
|
||||
}
|
||||
out := bindMountOverrides(mps, "local-lvm")
|
||||
if _, ok := out["mp1"]; ok {
|
||||
t.Error("mp1 is a storage volume and must NOT be overridden")
|
||||
}
|
||||
if got, want := out["mp0"], "local-lvm:1,mp=/mnt/felhom-usb,backup=0"; got != want {
|
||||
t.Errorf("mp0 override = %q, want %q", got, want)
|
||||
}
|
||||
if got, want := out["mp2"], "local-lvm:1,mp=/srv/extra,backup=0"; got != want {
|
||||
t.Errorf("mp2 override = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
// No bind mounts → nil (restore proceeds unchanged).
|
||||
if bindMountOverrides(map[string]string{"mp0": "local-lvm:8,mp=/data"}, "local-lvm") != nil {
|
||||
t.Error("expected nil when there are no bind mounts")
|
||||
}
|
||||
}
|
||||
// (TestArchiveVMID + TestBindMountOverrides were deleted with their subjects in v0.76.0/GL-5b —
|
||||
// the restore-test now derives its params from the ARCHIVE's embedded config via
|
||||
// drRestoreOverrides; the live-source-config path was the pre-fix shape.)
|
||||
|
||||
func TestRootfsSizeGB(t *testing.T) {
|
||||
cases := map[string]int{
|
||||
@@ -62,3 +24,77 @@ func TestRootfsSizeGB(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GL-5b: the mount-parity comparator — the assert that keeps PVE's drop-unlisted-mountpoints rule
|
||||
// from regressing into a green light. Pure-function cases; the engine-level Scenario A/B live in
|
||||
// restoretest_test.go / the fake-driven tests.
|
||||
func TestMountParity(t *testing.T) {
|
||||
archive := map[string]string{
|
||||
"hostname": "demo",
|
||||
"rootfs": "local-lvm:vm-9201-disk-0,size=32G",
|
||||
"mp0": "local-lvm:vm-9201-disk-1,mp=/var/lib/docker,backup=1,size=200G",
|
||||
"mp1": "local-lvm:vm-9201-disk-2,mp=/mnt/sys_drive,backup=1,size=50G",
|
||||
"mp8": "/mnt/felhom-drives,mp=/mnt/felhom-drives",
|
||||
"mp9": "/var/lib/felhom-agent/guests/9201/bootstrap,mp=/etc/felhom-bootstrap,ro=1",
|
||||
}
|
||||
|
||||
// full parity: storage mpN at archived path+size, binds as throwaway stand-ins
|
||||
restored := map[string]string{
|
||||
"mp0": "local-lvm:vm-990000-disk-1,mp=/var/lib/docker,backup=1,size=200G",
|
||||
"mp1": "local-lvm:vm-990000-disk-2,mp=/mnt/sys_drive,backup=1,size=50G",
|
||||
"mp8": "local-lvm:vm-990000-disk-3,mp=/mnt/felhom-drives,backup=0,size=1G",
|
||||
"mp9": "local-lvm:vm-990000-disk-4,mp=/etc/felhom-bootstrap,backup=0,size=1G",
|
||||
}
|
||||
inv, delta := mountParity(archive, restored)
|
||||
if len(delta) != 0 {
|
||||
t.Fatalf("full parity expected, got delta: %v", delta)
|
||||
}
|
||||
if len(inv) != 4 {
|
||||
t.Fatalf("inventory must carry all 4 verified mpN, got %v", inv)
|
||||
}
|
||||
|
||||
// the GL-5 constraint-(b) shape: a storage mpN silently dropped → NAMED delta (Scenario B core)
|
||||
dropped := map[string]string{
|
||||
"mp1": restored["mp1"], "mp8": restored["mp8"], "mp9": restored["mp9"],
|
||||
}
|
||||
_, delta = mountParity(archive, dropped)
|
||||
if len(delta) != 1 || !strings.Contains(delta[0], "mp0 MISSING") {
|
||||
t.Fatalf("dropped mp0 must be a named delta, got %v", delta)
|
||||
}
|
||||
|
||||
// wrong path
|
||||
wrongPath := map[string]string{
|
||||
"mp0": "local-lvm:vm-990000-disk-1,mp=/elsewhere,size=200G",
|
||||
"mp1": restored["mp1"], "mp8": restored["mp8"], "mp9": restored["mp9"],
|
||||
}
|
||||
_, delta = mountParity(archive, wrongPath)
|
||||
if len(delta) != 1 || !strings.Contains(delta[0], `mp0 path "/elsewhere"`) {
|
||||
t.Fatalf("wrong path must be a named delta, got %v", delta)
|
||||
}
|
||||
|
||||
// undersized volume (a 1G stand-in where 200G of data should be = the dropped-content shape)
|
||||
small := map[string]string{
|
||||
"mp0": "local-lvm:vm-990000-disk-1,mp=/var/lib/docker,size=1G",
|
||||
"mp1": restored["mp1"], "mp8": restored["mp8"], "mp9": restored["mp9"],
|
||||
}
|
||||
_, delta = mountParity(archive, small)
|
||||
if len(delta) != 1 || !strings.Contains(delta[0], "mp0 size 1G < archive 200G") {
|
||||
t.Fatalf("undersized mpN must be a named delta, got %v", delta)
|
||||
}
|
||||
|
||||
// an extra restored mpN the archive never had is a delta too
|
||||
extra := map[string]string{
|
||||
"mp0": restored["mp0"], "mp1": restored["mp1"], "mp8": restored["mp8"], "mp9": restored["mp9"],
|
||||
"mp3": "local-lvm:vm-990000-disk-9,mp=/stray,size=1G",
|
||||
}
|
||||
_, delta = mountParity(archive, extra)
|
||||
if len(delta) != 1 || !strings.Contains(delta[0], "mp3 present on the restored guest but not in the archive") {
|
||||
t.Fatalf("extra mpN must be a named delta, got %v", delta)
|
||||
}
|
||||
|
||||
// no mpN anywhere = trivially parity (the golden-shaped archive)
|
||||
inv, delta = mountParity(map[string]string{"rootfs": "l:d,size=8G"}, map[string]string{})
|
||||
if len(delta) != 0 || len(inv) != 0 {
|
||||
t.Fatalf("mpN-less archive must be trivial parity, got inv=%v delta=%v", inv, delta)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user