diff --git a/controller/internal/web/storage_handlers.go b/controller/internal/web/storage_handlers.go index f75ffde..8713564 100644 --- a/controller/internal/web/storage_handlers.go +++ b/controller/internal/web/storage_handlers.go @@ -29,6 +29,7 @@ import ( // without a live agent). *agentapi.Client satisfies it. type diskAgent interface { Disks(ctx context.Context) (agentapi.DisksResponse, error) + ListCandidates(ctx context.Context) (agentapi.CandidatesResult, error) 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) @@ -63,6 +64,31 @@ func fsUUIDForDevice(disks agentapi.DisksResponse, device string) string { return "" } +// resolveEnrollUUID resolves a device's filesystem UUID for enrollment. A RAW candidate (Impl-2b) is +// NOT in the /disks list (it has no PVE storage / isn't enrolled), so fsUUIDForDevice can't see it — +// fall back to the raw-device scan (/disks/candidates), which reports each free disk's durable_id +// (uuid:). Matches the whole disk OR its FS-bearing node (mount_source, e.g. /dev/sdd1). +// Returns "" only if the device truly has no resolvable fs UUID. +func resolveEnrollUUID(ctx context.Context, agent diskAgent, device string) string { + if disks, err := agent.Disks(ctx); err == nil { + if u := fsUUIDForDevice(disks, device); u != "" { + return u + } + } + cands, err := agent.ListCandidates(ctx) + if err != nil { + return "" + } + for _, c := range append(append([]agentapi.DiskCandidate{}, cands.Initialize...), cands.Attach...) { + if c.Device == device || c.MountSource == device { + if rest, ok := strings.CutPrefix(c.DurableID, "uuid:"); ok { + return rest + } + } + } + return "" +} + // storageInitResult is the outcome of an init attempt (JSON-rendered to the wizard). type storageInitResult struct { Registered bool `json:"registered"` @@ -107,12 +133,9 @@ func (s *Server) runStorageInit(ctx context.Context, agent diskAgent, device, fs if !fr.Formatted { return storageInitResult{}, fmt.Errorf("az eszköz nem lett megformázva (%s)", fr.Reason) } - // 2. Resolve the NEW fs UUID (re-list disks; the device's storage now carries a fresh UUID). - disks, err := agent.Disks(ctx) - if err != nil { - return storageInitResult{}, fmt.Errorf("formázás kész, de a meghajtólista nem olvasható: %w", err) - } - uuid := fsUUIDForDevice(disks, device) + // 2. Resolve the NEW fs UUID. A freshly-formatted RAW device isn't in /disks (not enrolled yet), so + // resolve via the raw-device scan too (Impl-2b) — the device now appears there with its new durable_id. + uuid := resolveEnrollUUID(ctx, agent, device) if uuid == "" { return storageInitResult{}, fmt.Errorf("formázás kész, de az új fájlrendszer-azonosító nem feloldható — frissítsen és használja a Csatolás funkciót") } @@ -145,11 +168,9 @@ func (s *Server) attachIntoGuest(ctx context.Context, agent diskAgent, where str // 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) { - disks, err := agent.Disks(ctx) - if err != nil { - return storageInitResult{}, fmt.Errorf("a meghajtólista nem olvasható: %w", err) - } - uuid := fsUUIDForDevice(disks, device) + // Resolve the fs UUID. Works for a legacy re-attach (drive in /disks) AND a raw candidate (Impl-2b: + // not in /disks — resolved via the raw-device scan's durable_id). + uuid := resolveEnrollUUID(ctx, agent, device) if uuid == "" { return storageInitResult{}, fmt.Errorf("a kiválasztott meghajtóhoz nem található fájlrendszer-azonosító (csak fájlrendszerrel rendelkező meghajtó csatolható)") } diff --git a/controller/internal/web/storage_handlers_test.go b/controller/internal/web/storage_handlers_test.go index f9a4f08..d5ec5b0 100644 --- a/controller/internal/web/storage_handlers_test.go +++ b/controller/internal/web/storage_handlers_test.go @@ -42,6 +42,7 @@ type mockAgent struct { decommissionCalls []string guestRebootCalls int guestRebootErr error + candidates agentapi.CandidatesResult } type assignCall struct{ uuid, where, fstype string } @@ -54,6 +55,10 @@ func (m *mockAgent) Disks(context.Context) (agentapi.DisksResponse, error) { m.disksCalls++ return m.disks, nil } + +func (m *mockAgent) ListCandidates(context.Context) (agentapi.CandidatesResult, error) { + return m.candidates, nil +} func (m *mockAgent) FormatDisk(_ context.Context, device, fstype string, confirmed bool, durableID string) (agentapi.FormatResult, error) { m.formatCalls = append(m.formatCalls, formatCall{device, fstype, durableID, confirmed}) return m.formatRes, m.formatErr @@ -219,6 +224,31 @@ func TestRunStorageAttach_Success(t *testing.T) { } } +// Impl-2b: a RAW candidate (not in /disks — no PVE storage) attaches by resolving its fs UUID from the +// raw-device scan's durable_id. RED-PROOF: with the old fsUUIDForDevice(Disks)-only resolution this +// fails ("no fs identifier"), since the raw device is absent from /disks. +func TestRunStorageAttach_RawCandidate(t *testing.T) { + s := testServer(t) + agent := &mockAgent{ + disks: agentapi.DisksResponse{Disks: []agentapi.DiskInfo{}}, // raw device NOT in /disks + candidates: agentapi.CandidatesResult{ + Attach: []agentapi.DiskCandidate{ + {Device: "/dev/sdd", FSType: "ext4", Mountable: true, MountSource: "/dev/sdd", DurableID: "uuid:RAW-99"}, + }, + }, + } + res, err := s.runStorageAttach(context.Background(), agent, "/dev/sdd", "ext4", "/mnt/teszt", "Teszt", false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !res.Registered { + t.Fatal("expected registered") + } + if len(agent.assignCalls) != 1 || agent.assignCalls[0].uuid != "RAW-99" { + t.Fatalf("raw candidate must assign by the scan-derived fs UUID: %+v", agent.assignCalls) + } +} + // handleStorageRegister (the "Regisztrálás" action for an already-mounted, unregistered drive) must // register the STABLE intermediary path /mnt/felhom-drives/ — the same path runStorageInit/ // runStorageAttach register — NOT the raw /mnt/ it receives. Registering the raw path made the