package localapi import ( "context" "io" "log/slog" "os" "path/filepath" "strings" "testing" ) // R-117 §2.2 — WHAT AttachDrive DOES with each verdict, which is a RULING and not a detail. // // The two dead states have DIFFERENT repairs, and the existing self-heal only fits one: // // - STALE DEVICE (the detach/return case). The raw mount has healed onto the returning device via its // fs-UUID-keyed unit, so umount + re-bind lands the namespace on a HEALTHY superblock. Repair is // correct, and it happens live with no guest restart (proven on hardware, spike §8.1). It MUST run: // three call sites already invoke it — the 20 s reconcile ticker, agent startup, and the controller's // Return branch BEFORE it restarts the apps (spike §8.2) — and before v0.117.0 all three were // short-circuited by `if n == 1 && GuestSeesMount(...)` declaring the dead namespace "fully live". // // - ABORTED FILESYSTEM (the steady-state case, R-117a). The raw mount is the SAME aborted superblock, // so a re-bind produces a fresh bind to a still-dead filesystem. It MUST NOT run: AttachDrive is // called every 20 s, so re-binding here is an infinite silent retry — the exact silence R-117a // found, with more CPU — and it would mask the state instead of surfacing it. The truth travels in // BoundUnderParent (now false), so the drive gate stops the apps and alarms. Clearing an aborted // filesystem needs a remount or a fsck; that is an operator decision, never an automatic one. // // These tests assert the CONSEQUENCE — which privileged commands were issued — not that a verdict was // computed. RED-PROOF: make the BindAborted arm fall through to the re-bind instead of returning, and // TestAttachDrive_AbortedFilesystem_DoesNotRebind fails on the recorded umount/mount calls. // attachRecorder is a proxmox.Runner that records every privileged call and answers `lxc-info -p` with a // fixed PID, so the REAL GuestSeesMount runs against a captured guest mount table. type attachRecorder struct { calls [][]string pid string } func (r *attachRecorder) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) { r.calls = append(r.calls, append([]string{name}, args...)) if name == "lxc-info" { return []byte(r.pid + "\n"), nil, nil } return nil, nil, nil } func (r *attachRecorder) RunStdin(ctx context.Context, _ io.Reader, name string, args ...string) ([]byte, []byte, error) { return r.Run(ctx, name, args...) } // mountOps returns just the mount-table-mutating calls — the ones that constitute "a repair ran". func (r *attachRecorder) mountOps() []string { var out []string for _, c := range r.calls { switch c[0] { case "umount", "mount": out = append(out, strings.Join(c, " ")) } } return out } // attachFixture points BOTH mount tables at fixtures: the host one (procSelfMountinfo, which // countHostMounts and bindLiveness read) and the guest one (procGuestMountinfo, which the REAL // GuestSeesMount reads). Only the data is injected — every predicate runs for real. func attachFixture(t *testing.T, hostBody, guestBody string) *attachRecorder { t.Helper() dir := t.TempDir() hp := filepath.Join(dir, "host-mountinfo") gp := filepath.Join(dir, "guest-mountinfo") if err := os.WriteFile(hp, []byte(hostBody), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(gp, []byte(guestBody), 0o600); err != nil { t.Fatal(err) } prevHost, prevGuest := procSelfMountinfo, procGuestMountinfo procSelfMountinfo = hp procGuestMountinfo = func(string) string { return gp } t.Cleanup(func() { procSelfMountinfo, procGuestMountinfo = prevHost, prevGuest }) return &attachRecorder{pid: "9301"} } // guestSeesStale / guestSeesHealthy / guestSeesAborted are the GUEST-side captures — note `master:450`, // the slave-of-the-shared-parent tag that proves propagation was wired (spike §3.1). The guest carries the // same devno and super options as the host bind, because it IS the same mount. const guestSeesStale = `759 1176 8:16 /felhom-data /mnt/felhom-drives/r117sd rw,relatime shared:459 master:450 - ext4 /dev/sdb rw,stripe=512,shutdown ` const guestSeesHealthy = `759 1176 8:16 /felhom-data /mnt/felhom-drives/r117sd rw,relatime shared:459 master:450 - ext4 /dev/sdb rw,stripe=512 ` const guestSeesAborted = `759 1176 8:16 /felhom-data /mnt/felhom-drives/r117sd rw,relatime shared:459 master:450 - ext4 /dev/sdb rw,stripe=512,emergency_ro ` func attachBinder(rec *attachRecorder) *GuestBinder { return NewGuestBinder(rec, slog.New(slog.NewTextHandler(io.Discard, nil))) } // TestAttachDrive_StaleBind_Rebinds is Q6's consequence: the repair that was short-circuited for three // releases now runs. Exactly the state measured on hardware — bind on 8:16, raw healed onto 8:32. func TestAttachDrive_StaleBind_Rebinds(t *testing.T) { rec := attachFixture(t, mountinfoStaleBind, guestSeesStale) b := attachBinder(rec) got, err := b.AttachDrive(context.Background(), 9301, "/mnt/r117sd") if err != nil { t.Fatalf("AttachDrive: %v", err) } if got != "/mnt/felhom-drives/r117sd" { t.Errorf("stable path = %q", got) } ops := rec.mountOps() if len(ops) == 0 { t.Fatal("NO repair ran over a stale bind — this is the R-117 short-circuit: `n == 1 && " + "GuestSeesMount` declared an EIO namespace \"fully live\", so the 20 s ticker, agent startup " + "and the controller's pre-restart re-attach all did nothing") } var sawUmount, sawBind bool for _, o := range ops { if strings.HasPrefix(o, "umount /mnt/felhom-drives/r117sd") { sawUmount = true } if o == "mount --bind /mnt/r117sd/felhom-data /mnt/felhom-drives/r117sd" { sawBind = true } } if !sawUmount || !sawBind { t.Errorf("repair did not umount-then-rebind; ops=%v", ops) } } // TestAttachDrive_AbortedFilesystem_DoesNotRebind is the §2.2 ruling. A re-bind here cannot repair // anything (the raw mount is the same aborted superblock) and AttachDrive runs every 20 s, so re-binding // would be an infinite silent retry that also masks the state. func TestAttachDrive_AbortedFilesystem_DoesNotRebind(t *testing.T) { rec := attachFixture(t, mountinfoAborted, guestSeesAborted) b := attachBinder(rec) got, err := b.AttachDrive(context.Background(), 9301, "/mnt/r117sd") if err != nil { t.Fatalf("AttachDrive returned an error for an aborted filesystem: %v — it must be a quiet no-op; "+ "an error here would log `reconcile: AttachDrive failed` every 20 s", err) } if got != "/mnt/felhom-drives/r117sd" { t.Errorf("stable path = %q", got) } if ops := rec.mountOps(); len(ops) != 0 { t.Errorf("AttachDrive re-bound an ABORTED filesystem: %v\n"+ "A re-bind lands on the SAME dead superblock, and this runs every 20 s — an infinite silent "+ "retry, which is R-117a's silence with more CPU. The state must SURFACE via "+ "BoundUnderParent=false so the gate stops the apps and alarms.", ops) } } // TestAttachDrive_Healthy_IsStillANoOp — the idempotency the reconcile ticker depends on. If this // regressed, every tick would umount and re-bind a working drive, re-firing propagation into the guest // 4320 times a day. func TestAttachDrive_Healthy_IsStillANoOp(t *testing.T) { rec := attachFixture(t, mountinfoHealthy, guestSeesHealthy) b := attachBinder(rec) if _, err := b.AttachDrive(context.Background(), 9301, "/mnt/r117sd"); err != nil { t.Fatalf("AttachDrive: %v", err) } if ops := rec.mountOps(); len(ops) != 0 { t.Errorf("a healthy bind was disturbed: %v — the 20 s reconcile must stay a no-op", ops) } } // TestAttachDrive_UnknownLiveness_IsANoOp — cannot-tell must not trigger churn either. An unreadable // mount table making the agent umount and re-bind every 20 s would be a self-inflicted outage. func TestAttachDrive_UnknownLiveness_IsANoOp(t *testing.T) { // Host table healthy (so n == 1) but on a filesystem whose abort vocabulary we cannot read. rec := attachFixture(t, mountinfoUnknownFS, `759 1176 8:16 /felhom-data /mnt/felhom-drives/r117sd rw,relatime shared:459 master:450 - btrfs /dev/sdb rw `) b := attachBinder(rec) if _, err := b.AttachDrive(context.Background(), 9301, "/mnt/r117sd"); err != nil { t.Fatalf("AttachDrive: %v", err) } if ops := rec.mountOps(); len(ops) != 0 { t.Errorf("an UNKNOWN verdict caused a re-bind: %v — cannot-tell must never churn a live mount", ops) } }