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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user