agent v0.25.0: slice 10 P2 — bind enrolled user-data drives into the guest

POST /disks/guest-attach binds an enrolled drive's felhom-data namespace into
the guest (Model A: felhom-data is the bind source mounted at /mnt/<name>, so
only Felhom's namespace crosses in). GuestBinder does mkdir+chown(100000)+pct set
(RW bind) via the fenced runner. Idempotent, free-slot selection, path-validated.
Spike-proven on 9201. Pairs with controller P2C + golden /mnt:rslave (P2B).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 15:38:55 +02:00
parent d1bd44d2d5
commit c1d04c28c1
6 changed files with 311 additions and 8 deletions
+90
View File
@@ -372,6 +372,96 @@ func TestEject_RoleGated(t *testing.T) {
d4.mu.Unlock()
}
// ---- guest data-drive passthrough (slice 10 P2) -----------------------------------------
type fakeGuestAttacher struct {
mu sync.Mutex
calls []struct {
vmid int
slot, where string
}
}
func (f *fakeGuestAttacher) AttachBind(_ context.Context, vmid int, mountKey, where string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.calls = append(f.calls, struct {
vmid int
slot, where string
}{vmid, mountKey, where})
return nil
}
func (f *fakeGuestAttacher) count() int { f.mu.Lock(); defer f.mu.Unlock(); return len(f.calls) }
func newAttachServer(t *testing.T, ga GuestAttacher, mounts map[int]map[string]string) http.Handler {
t.Helper()
srv, err := NewServer(Options{
ListenAddr: "127.0.0.1:0", Guests: &fakeGuestsCfg{mounts: mounts}, Backups: &fakeBackups{},
Store: &fakeStore{}, Storage: fakeStorage{}, Tokens: staticTokens{"A": 8200, "B": 9300},
GuestAttach: ga, Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
})
if err != nil {
t.Fatal(err)
}
srv.baseCtx = context.Background()
return srv.Handler()
}
// A first attach picks the lowest free slot (mp0; mp9 bootstrap is taken) and calls the binder.
func TestGuestAttach_PicksFreeSlotAndBinds(t *testing.T) {
ga := &fakeGuestAttacher{}
h := newAttachServer(t, ga, map[int]map[string]string{
8200: {"mp9": "/var/lib/.../bootstrap,mp=/etc/felhom-bootstrap,ro=1"},
})
w := do(t, h, "POST", "/disks/guest-attach", "A", `{"where":"/mnt/felhom-usb"}`)
if w.Code != http.StatusOK {
t.Fatalf("attach: got %d want 200 (%s)", w.Code, w.Body.String())
}
if ga.count() != 1 || ga.calls[0].slot != "mp0" || ga.calls[0].where != "/mnt/felhom-usb" || ga.calls[0].vmid != 8200 {
t.Fatalf("AttachBind not called with mp0/where/vmid: %+v", ga.calls)
}
}
// An already-bound drive is idempotent: returns the existing slot, binder NOT called again.
func TestGuestAttach_Idempotent(t *testing.T) {
ga := &fakeGuestAttacher{}
h := newAttachServer(t, ga, map[int]map[string]string{
8200: {"mp0": "/mnt/felhom-usb/felhom-data,mp=/mnt/felhom-usb"},
})
w := do(t, h, "POST", "/disks/guest-attach", "A", `{"where":"/mnt/felhom-usb"}`)
if w.Code != http.StatusOK {
t.Fatalf("idempotent attach: got %d want 200 (%s)", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), `"already":true`) {
t.Fatalf("expected already:true: %s", w.Body.String())
}
if ga.count() != 0 {
t.Fatalf("AttachBind must NOT be called for an already-bound drive: %+v", ga.calls)
}
}
// A hostile/invalid where is refused with no binder call.
func TestGuestAttach_RejectsBadPath(t *testing.T) {
ga := &fakeGuestAttacher{}
h := newAttachServer(t, ga, map[int]map[string]string{8200: {}})
for _, bad := range []string{`{"where":"/mnt/../etc"}`, `{"where":"/etc/passwd"}`, `{"where":"/mnt/a/b"}`, `{"where":""}`} {
if w := do(t, h, "POST", "/disks/guest-attach", "A", bad); w.Code != http.StatusBadRequest {
t.Fatalf("bad where %s: got %d want 400", bad, w.Code)
}
}
if ga.count() != 0 {
t.Fatal("binder called for an invalid path")
}
}
// Not configured (no GuestAttach dep) → 503.
func TestGuestAttach_NotConfigured(t *testing.T) {
h := newAttachServer(t, nil, map[int]map[string]string{8200: {}})
if w := do(t, h, "POST", "/disks/guest-attach", "A", `{"where":"/mnt/felhom-usb"}`); w.Code != http.StatusServiceUnavailable {
t.Fatalf("unconfigured: got %d want 503", w.Code)
}
}
// ---- auth / config ----------------------------------------------------------------------
func TestDisks_CrossGuest403(t *testing.T) {