GL-5: DR restore passes the FULL archive layout (live finding #2)
The live scratch DR exposed the second half of PVE's all-or-nothing explicit-params restore: mountpoints NOT named in the params are silently DROPPED - the DR guest came up without its mp0/mp1 data volumes (boot passed; the customer's world did not ride along). drRestoreOverrides now derives the COMPLETE param set from the archive's extracted config: explicit rootfs, every storage-backed mpN passed through (size + in-guest path + backup flag preserved so vzrestore extracts its content), the two structural binds replaced by 4d-swapped throwaways; unknown bind mpN or unparseable size refuses loudly. Snapshot sections never shadow the current config. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -978,19 +978,54 @@ func TestRunBringUp_DRUnusedDeleteFailureWarns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// GL-5: the archive-config rootfs parse — current-config line wins, snapshot sections never shadow
|
||||
// it, absent/unparseable → 0 (which makes the DR bring-up refuse rather than guess a size).
|
||||
func TestArchiveRootfsSizeGB(t *testing.T) {
|
||||
full := "hostname: demo\nrootfs: local-lvm:vm-9201-disk-0,size=32G\nmp0: local-lvm:vm-9201-disk-1,mp=/var/lib/docker,size=200G\n"
|
||||
if got := archiveRootfsSizeGB(full); got != 32 {
|
||||
t.Errorf("full config: got %d, want 32", got)
|
||||
// GL-5: drRestoreOverrides builds the COMPLETE explicit-params restore set from the archive's
|
||||
// extracted config — the live validation proved PVE drops any mountpoint NOT named in the params,
|
||||
// so mp0/mp1 pass-through is what keeps a DR guest's data volumes. Snapshot sections never shadow;
|
||||
// unknown binds and unparseable sizes refuse.
|
||||
func TestDRRestoreOverrides(t *testing.T) {
|
||||
raw := `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
|
||||
[snap1]
|
||||
rootfs: local-lvm:vm-9201-disk-9,size=99G
|
||||
`
|
||||
ov, err := drRestoreOverrides(raw, "local-lvm")
|
||||
if err != nil {
|
||||
t.Fatalf("drRestoreOverrides: %v", err)
|
||||
}
|
||||
snap := "hostname: demo\n\n[before-upgrade]\nrootfs: local-lvm:vm-9201-disk-9,size=99G\n"
|
||||
if got := archiveRootfsSizeGB(snap); got != 0 {
|
||||
t.Errorf("a snapshot section's rootfs must NOT shadow an absent current one: got %d, want 0", got)
|
||||
want := map[string]string{
|
||||
"rootfs": "local-lvm:32",
|
||||
"mp0": "local-lvm:200,mp=/var/lib/docker,backup=1",
|
||||
"mp1": "local-lvm:50,mp=/mnt/sys_drive,backup=1",
|
||||
"mp8": "local-lvm:1,mp=/mnt/felhom-drives,backup=0",
|
||||
"mp9": "local-lvm:1,mp=/etc/felhom-bootstrap,backup=0",
|
||||
}
|
||||
if got := archiveRootfsSizeGB("hostname: demo\n"); got != 0 {
|
||||
t.Errorf("no rootfs line: got %d, want 0", got)
|
||||
if len(ov) != len(want) {
|
||||
t.Fatalf("overrides = %+v, want %+v", ov, want)
|
||||
}
|
||||
for k, v := range want {
|
||||
if ov[k] != v {
|
||||
t.Errorf("override[%s] = %q, want %q", k, ov[k], v)
|
||||
}
|
||||
}
|
||||
|
||||
// an UNKNOWN bind mpN = unknown topology → refuse (never restore a guest missing a mount)
|
||||
if _, err := drRestoreOverrides("rootfs: l:d,size=8G\nmp3: /srv/other,mp=/data\n", "local-lvm"); err == nil ||
|
||||
!strings.Contains(err.Error(), "unknown bind mountpoint") {
|
||||
t.Errorf("unknown bind must refuse, got %v", err)
|
||||
}
|
||||
// no parseable rootfs → refuse
|
||||
if _, err := drRestoreOverrides("hostname: x\n", "local-lvm"); err == nil ||
|
||||
!strings.Contains(err.Error(), "rootfs size") {
|
||||
t.Errorf("missing rootfs must refuse, got %v", err)
|
||||
}
|
||||
// a storage mpN without a size → refuse (cannot pass it through)
|
||||
if _, err := drRestoreOverrides("rootfs: l:d,size=8G\nmp0: l:d1,mp=/x\n", "local-lvm"); err == nil ||
|
||||
!strings.Contains(err.Error(), "no parseable size") {
|
||||
t.Errorf("sizeless mpN must refuse, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user