Files
felhom-agent/internal/dr/plan_test.go
T
admin bd4bced771 dr: recovered WG-key install + host_loss directive→restore-PLAN (S5 safe halves)
wgtunnel.InstallRecoveredKey: write an escrow-recovered WG private key (create-
only, refuse-overwrite) so the tunnel re-establishes with the same identity/pubkey
(same /32), no keygen. Wired into identity-consume -install-wg-key (opt-in;
pre-S3 blob → logged fresh-keygen fallback). Value never logged.

internal/dr (new): consume the host_loss restore_directive (was logged-ignored)
into an inspectable RestorePlan via the AddConsumer raw seam — per guest
{vmid,archive,target,sizing} + per drive {durable_id→mount} + offsite PBS coord.
DERIVE-AND-SURFACE only; the Consumer has no restore/destroy dependency (execute-
nothing is structural). guest_loss/absent → no plan.

Tests + red-proofs (WG create-only overwrite; plan mode-gate). No secrets on
argv/stdout/logs. The destructive in-place restore is a separate operator-present
STOP-gated drill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-04 21:06:31 +02:00

93 lines
3.9 KiB
Go

package dr
import (
"context"
"testing"
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
)
func sampleRecipe() *hub.DRRecipeHostHalf {
return &hub.DRRecipeHostHalf{
RecipeVersion: 1,
Guests: []hub.DRGuest{{VMID: 9201, Cores: 2, MemoryBytes: 12 << 30, DiskBytes: 32 << 30}},
PBS: &hub.DRPBSCoord{RepoID: "felhom-offsite", Namespace: "demo-felhom-01", LatestSnapshotID: "9201"},
Drives: []hub.DRDrive{{DurableID: "uuid:abc", MountPath: "/mnt/felhom-drives/photos", Intent: "enrolled", TotalBytes: 500 << 30}},
}
}
// TestBuildRestorePlan_HostLoss: a host_loss directive + recipe yields per-guest {vmid, archive,
// target, sizing} + per-drive {durable_id → mount} + the offsite PBS coord.
func TestBuildRestorePlan_HostLoss(t *testing.T) {
dir := &hub.WireRestoreDirective{Mode: "host_loss", VMID: 9201, Archive: "felhom-offsite:backup/ct/9201/2026-07-04T14:55:44Z"}
plan, ok := BuildRestorePlan(dir, sampleRecipe(), "local-lvm")
if !ok || plan == nil {
t.Fatal("host_loss must yield a plan")
}
if plan.Mode != "host_loss" || len(plan.Guests) != 1 || len(plan.Drives) != 1 {
t.Fatalf("plan shape = %+v", plan)
}
g := plan.Guests[0]
if g.VMID != 9201 || g.TargetStorage != "local-lvm" || g.Cores != 2 || g.DiskBytes != 32<<30 {
t.Errorf("planned guest = %+v", g)
}
if g.Archive != dir.Archive {
t.Errorf("planned guest archive = %q, want the directive's %q", g.Archive, dir.Archive)
}
d := plan.Drives[0]
if d.DurableID != "uuid:abc" || d.ExpectedMount != "/mnt/felhom-drives/photos" {
t.Errorf("planned drive (durable_id→mount) = %+v", d)
}
if plan.PBS == nil || plan.PBS.RepoID != "felhom-offsite" {
t.Errorf("plan must carry the offsite PBS coord, got %+v", plan.PBS)
}
}
// TestBuildRestorePlan_NoPlanCases is the red-proof anchor: guest_loss / absent / nil-recipe yield
// NO plan (execute-nothing on the wrong mode). Relaxing the mode gate → the guest_loss case fails.
func TestBuildRestorePlan_NoPlanCases(t *testing.T) {
if _, ok := BuildRestorePlan(&hub.WireRestoreDirective{Mode: "guest_loss", VMID: 9201}, sampleRecipe(), "local-lvm"); ok {
t.Error("guest_loss must NOT yield a host-loss plan")
}
if _, ok := BuildRestorePlan(nil, sampleRecipe(), "local-lvm"); ok {
t.Error("absent directive must NOT yield a plan")
}
if _, ok := BuildRestorePlan(&hub.WireRestoreDirective{Mode: "host_loss"}, nil, "local-lvm"); ok {
t.Error("nil recipe must NOT yield a plan")
}
}
// TestConsumer_SurfacesPlanNeverExecutes: the consumer surfaces the plan on host_loss, consults the
// recipe only then, and clears it otherwise. It has NO restore/destroy dependency (execute-nothing
// is structural — the type literally cannot call a restore).
func TestConsumer_SurfacesPlanNeverExecutes(t *testing.T) {
recipeCalls := 0
c := NewConsumer(func(context.Context) *hub.DRRecipeHostHalf { recipeCalls++; return sampleRecipe() }, "local-lvm", nil)
ds := func(d *hub.WireRestoreDirective) *hub.DesiredStateResponse {
return &hub.DesiredStateResponse{DesiredState: hub.WireDesiredState{RestoreDirective: d}}
}
// non-host_loss → no plan, recipe NOT consulted.
c.OnDesiredState(context.Background(), ds(&hub.WireRestoreDirective{Mode: "guest_loss"}))
if c.LastPlan() != nil {
t.Error("guest_loss set a plan")
}
if recipeCalls != 0 {
t.Errorf("recipe consulted on a non-host_loss directive (%d calls)", recipeCalls)
}
// host_loss → plan surfaced, recipe consulted once.
c.OnDesiredState(context.Background(), ds(&hub.WireRestoreDirective{Mode: "host_loss", VMID: 9201}))
p := c.LastPlan()
if p == nil || len(p.Guests) != 1 || p.Guests[0].VMID != 9201 {
t.Fatalf("host_loss plan = %+v", p)
}
if recipeCalls != 1 {
t.Errorf("recipe calls = %d, want 1", recipeCalls)
}
// absent directive clears the plan.
c.OnDesiredState(context.Background(), ds(nil))
if c.LastPlan() != nil {
t.Error("absent directive did not clear the plan")
}
}