v0.84.0: ReassertNetworkMounts — NAS automount survives guest reboots (RCA fix 1)
Storage §8 decision table (stop + enable --now on idle triggers; active mounts untouched), daemon leg at startup with per-running-guest visibility verify, guest-hook post-start leg (root, direct systemctl, non-fatal). Red-proofs: always-rearm table FAIL; unwired hook FAIL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
|
||||
)
|
||||
|
||||
// fakeNetOpsReassert is fakeNetOps plus the type-asserted reassert capability.
|
||||
type fakeNetOpsReassert struct {
|
||||
fakeNetOps
|
||||
reassertN int
|
||||
results []storage.NetReassertResult
|
||||
}
|
||||
|
||||
func (f *fakeNetOpsReassert) ReassertNetworkAutomounts(context.Context) []storage.NetReassertResult {
|
||||
f.reassertN++
|
||||
return f.results
|
||||
}
|
||||
|
||||
// seeingAttacher records GuestSeesMount calls and controls per-vmid running state.
|
||||
type seeingAttacher struct {
|
||||
fakeGuestAttacher
|
||||
running map[int]bool
|
||||
seen []struct {
|
||||
vmid int
|
||||
path string
|
||||
}
|
||||
sees bool
|
||||
}
|
||||
|
||||
func (s *seeingAttacher) GuestBootID(_ context.Context, vmid int) string {
|
||||
if s.running[vmid] {
|
||||
return "boot-1"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *seeingAttacher) GuestSeesMount(_ context.Context, vmid int, path string) bool {
|
||||
s.seen = append(s.seen, struct {
|
||||
vmid int
|
||||
path string
|
||||
}{vmid, path})
|
||||
return s.sees
|
||||
}
|
||||
|
||||
// The daemon leg: reassert runs once, then visibility is verified per RUNNING guest per acted
|
||||
// share; stopped guests are skipped entirely.
|
||||
func TestReassertNetworkMounts_VerifiesRunningGuestsOnly(t *testing.T) {
|
||||
n := &fakeNetOpsReassert{results: []storage.NetReassertResult{
|
||||
{Name: "media", Where: "/mnt/felhom-drives/media", Action: storage.NetReassertRearmed},
|
||||
}}
|
||||
srv := newNetServer(t, n, t.TempDir())
|
||||
ga := &seeingAttacher{running: map[int]bool{8200: true, 9300: false}, sees: true}
|
||||
srv.guestAttach = ga
|
||||
gb := tempBindStore(t)
|
||||
if err := gb.Record(8200, "uuid:aaa"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gb.Record(9300, "uuid:bbb"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv.guestBinds = gb
|
||||
|
||||
srv.ReassertNetworkMounts(context.Background())
|
||||
|
||||
if n.reassertN != 1 {
|
||||
t.Fatalf("host-side reassert must run exactly once (host-global), ran %d", n.reassertN)
|
||||
}
|
||||
if len(ga.seen) != 1 || ga.seen[0].vmid != 8200 || ga.seen[0].path != "/mnt/felhom-drives/media" {
|
||||
t.Fatalf("visibility verify must cover the RUNNING guest only, got %+v", ga.seen)
|
||||
}
|
||||
}
|
||||
|
||||
// A guest that does NOT see the share after reassert is a WARN, never an error — the method
|
||||
// returns normally (non-fatal proof) and the pass still ran.
|
||||
func TestReassertNetworkMounts_InvisibleShareNonFatal(t *testing.T) {
|
||||
n := &fakeNetOpsReassert{results: []storage.NetReassertResult{
|
||||
{Name: "media", Where: "/mnt/felhom-drives/media", Action: storage.NetReassertRearmed},
|
||||
}}
|
||||
srv := newNetServer(t, n, t.TempDir())
|
||||
ga := &seeingAttacher{running: map[int]bool{8200: true}, sees: false}
|
||||
srv.guestAttach = ga
|
||||
gb := tempBindStore(t)
|
||||
if err := gb.Record(8200, "uuid:aaa"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv.guestBinds = gb
|
||||
|
||||
srv.ReassertNetworkMounts(context.Background()) // must not panic / abort
|
||||
|
||||
if n.reassertN != 1 || len(ga.seen) != 1 {
|
||||
t.Fatalf("pass must complete despite invisible share: reassert=%d seen=%+v", n.reassertN, ga.seen)
|
||||
}
|
||||
}
|
||||
|
||||
// A netStorage surface WITHOUT the reassert capability (the lean interface, e.g. plain fakes) is a
|
||||
// clean no-op — the type-assert gate.
|
||||
func TestReassertNetworkMounts_NoCapabilityNoOp(t *testing.T) {
|
||||
srv := newNetServer(t, &fakeNetOps{}, t.TempDir())
|
||||
srv.ReassertNetworkMounts(context.Background()) // must not panic
|
||||
}
|
||||
Reference in New Issue
Block a user