controller v0.48.0: slice 10 P2C — enroll passes the drive into the guest
agentapi GuestAttach(where) → POST /disks/guest-attach; runStorageInit/Attach + handleStorageRegister call attachIntoGuest after register (best-effort, P3 heals). Closes Branch A: enrolled drives become usable in the guest, banner clears. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -323,6 +323,14 @@ func (c *Client) AssignDisk(ctx context.Context, uuid, where, fstype, options st
|
||||
return err
|
||||
}
|
||||
|
||||
// GuestAttach binds an enrolled drive's felhom-data namespace into THIS guest (slice 10 P2, Model A).
|
||||
// The drive must already be host-mounted at `where` (the enroll flow's assign did that). Idempotent on
|
||||
// the agent side (returns the existing slot if already bound). Returns nil on success.
|
||||
func (c *Client) GuestAttach(ctx context.Context, where string) error {
|
||||
_, err := c.post(ctx, "/disks/guest-attach", map[string]string{"where": where})
|
||||
return err
|
||||
}
|
||||
|
||||
// EjectResult mirrors POST /disks/eject (the dependent-guest warning).
|
||||
type EjectResult struct {
|
||||
VMID int `json:"vmid"`
|
||||
|
||||
@@ -28,6 +28,7 @@ type diskAgent interface {
|
||||
FormatDisk(ctx context.Context, device, fstype string, confirmed bool, durableID string) (agentapi.FormatResult, error)
|
||||
AssignDisk(ctx context.Context, uuid, where, fstype, options string) error
|
||||
EjectDisk(ctx context.Context, where string) (agentapi.EjectResult, error)
|
||||
GuestAttach(ctx context.Context, where string) error
|
||||
}
|
||||
|
||||
// mountNameRe is the safe `/mnt/<name>` component (DNS-ish: letters, digits, _ , -).
|
||||
@@ -116,9 +117,21 @@ func (s *Server) runStorageInit(ctx context.Context, agent diskAgent, device, fs
|
||||
if err := s.registerStoragePath(where, label, setDefault); err != nil {
|
||||
return storageInitResult{}, err
|
||||
}
|
||||
s.attachIntoGuest(ctx, agent, where)
|
||||
return storageInitResult{Registered: true, Where: where}, nil
|
||||
}
|
||||
|
||||
// attachIntoGuest passes an enrolled drive INTO the guest (slice 10 P2) so the controller + apps can
|
||||
// use it. Best-effort: the StoragePath registration is the durable intent, so a transient attach
|
||||
// failure is logged (not fatal) — P3 self-heal reconcile will complete it on the next tick.
|
||||
func (s *Server) attachIntoGuest(ctx context.Context, agent diskAgent, where string) {
|
||||
if err := agent.GuestAttach(ctx, where); err != nil {
|
||||
s.logger.Printf("[WARN] [web] enroll: guest-attach %s failed (registered; will be retried): %v", where, err)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] enroll: drive bound into guest: %s", where)
|
||||
}
|
||||
|
||||
// runStorageAttach mounts an existing-filesystem device (non-destructive — never touches the gate)
|
||||
// and registers it. The UUID is resolved server-side from the device.
|
||||
func (s *Server) runStorageAttach(ctx context.Context, agent diskAgent, device, fstype, where, label string, setDefault bool) (storageInitResult, error) {
|
||||
@@ -136,6 +149,7 @@ func (s *Server) runStorageAttach(ctx context.Context, agent diskAgent, device,
|
||||
if err := s.registerStoragePath(where, label, setDefault); err != nil {
|
||||
return storageInitResult{}, err
|
||||
}
|
||||
s.attachIntoGuest(ctx, agent, where)
|
||||
return storageInitResult{Registered: true, Where: where}, nil
|
||||
}
|
||||
|
||||
@@ -353,6 +367,11 @@ func (s *Server) handleStorageRegister(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] storage path registered (existing mount): %s", req.Where)
|
||||
// Pass the drive into the guest too (slice 10 P2) — registering a host-only mount otherwise leaves
|
||||
// it guest-invisible (the exact gap that produced the "nem elérhető" banner).
|
||||
if agent, aerr := s.agentClient(); aerr == nil {
|
||||
s.attachIntoGuest(r.Context(), agent, req.Where)
|
||||
}
|
||||
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"registered": true, "where": req.Where})
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ type mockAgent struct {
|
||||
assignCalls []assignCall
|
||||
disksCalls int
|
||||
formatCalls []formatCall
|
||||
guestAttachCalls []string
|
||||
}
|
||||
|
||||
type assignCall struct{ uuid, where, fstype string }
|
||||
@@ -55,6 +56,10 @@ func (m *mockAgent) AssignDisk(_ context.Context, uuid, where, fstype, _ string)
|
||||
func (m *mockAgent) EjectDisk(_ context.Context, where string) (agentapi.EjectResult, error) {
|
||||
return agentapi.EjectResult{Ejected: where}, nil
|
||||
}
|
||||
func (m *mockAgent) GuestAttach(_ context.Context, where string) error {
|
||||
m.guestAttachCalls = append(m.guestAttachCalls, where)
|
||||
return nil
|
||||
}
|
||||
|
||||
func testServer(t *testing.T) *Server {
|
||||
t.Helper()
|
||||
@@ -166,6 +171,10 @@ func TestRunStorageInit_Success(t *testing.T) {
|
||||
if len(paths) != 1 || paths[0].Path != "/mnt/hdd1" || paths[0].Label != "Külső HDD" || !paths[0].IsDefault || !paths[0].Schedulable {
|
||||
t.Fatalf("StoragePath not registered as expected: %+v", paths)
|
||||
}
|
||||
// P2C: enroll must pass the drive into the guest.
|
||||
if len(agent.guestAttachCalls) != 1 || agent.guestAttachCalls[0] != "/mnt/hdd1" {
|
||||
t.Fatalf("enroll did not guest-attach the drive: %+v", agent.guestAttachCalls)
|
||||
}
|
||||
}
|
||||
|
||||
// Attach is non-destructive: resolve UUID → assign → register (no format).
|
||||
|
||||
Reference in New Issue
Block a user