GL-5: DR bring-up structural bind overrides + 4d real-bind swap

ModeDRGuestLoss now passes restore-time MountOverrides for the two
platform-constant structural binds (mp8 parent, mp9 bootstrap) via the
shared throwaway-volume format helper - without them a customer-archive
restore under the privsep token fails outright ("restoring 'mp8' to bind
mount is only possible for root"). New post-restore step 4d swaps the real
binds in via the host runner (root pct set, one slot per call), deletes the
displaced unusedN volumes (API config PUT; a scoped-token refusal logs the
residue loudly instead of widening privileges), and respects the
committed/launched rollback envelope. Provision passes nil overrides -
byte-identical behavior (regression contract test).

Engine grows an optional HostRunner + StateDir seam (DR refuses up front
without a runner); selftest bring-up wires the ExecRunner + cleans the
scratch mp9 host dir on teardown; proxmox.GuestConfig.Unused() added.
6 new tests incl. C2 mid-swap rollback + C3 older-archive + 403-warn paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-08 08:48:11 +02:00
parent 4c40846769
commit c12b512316
6 changed files with 441 additions and 16 deletions
+10 -1
View File
@@ -342,7 +342,7 @@ func bindMountOverrides(mps map[string]string, restoreStorage string) map[string
if mp == "" {
mp = volPart // fall back to the host path if no explicit in-guest mp=
}
out[key] = fmt.Sprintf("%s:1,mp=%s,backup=0", restoreStorage, mp)
out[key] = throwawayVolumeOverride(restoreStorage, mp)
}
if len(out) == 0 {
return nil
@@ -350,6 +350,15 @@ func bindMountOverrides(mps map[string]string, restoreStorage string) map[string
return out
}
// throwawayVolumeOverride is the ONE source of the restore-override value format: a throwaway 1G
// volume on restoreStorage at the given in-guest path, excluded from backup. Used by the
// restore-test's source-config-derived overrides (above) and by the DR bring-up's synthesized
// structural-bind overrides (GL-5, bringup.go — its mpN are platform constants, so it skips this
// file's is-a-bind filtering and calls the format directly).
func throwawayVolumeOverride(restoreStorage, guestPath string) string {
return fmt.Sprintf("%s:1,mp=%s,backup=0", restoreStorage, guestPath)
}
// mountPathOf returns the mp= field from an mpN value's trailing options ("" if absent).
func mountPathOf(opts string) string {
for _, kv := range strings.Split(opts, ",") {