Impl-2b: resolve a raw candidate's fs-UUID via /disks/candidates (enroll fix)

runStorageInit/runStorageAttach resolved the fs UUID only via agent.Disks(),
which does NOT include a raw (unenrolled, non-PVE-storage) device — so a raw
candidate could be offered but never enrolled ("no fs identifier"). New
resolveEnrollUUID falls back to the raw-device scan (/disks/candidates), which
reports each free disk's durable_id (uuid:<fs-uuid>). Both enroll paths use it;
legacy re-attach (drive in /disks) still works. Test + red-proof.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 18:19:15 +02:00
parent feab92ccfc
commit bea05ea600
2 changed files with 62 additions and 11 deletions
+32 -11
View File
@@ -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:<fs-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ó)")
}
@@ -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/<name> — the same path runStorageInit/
// runStorageAttach register — NOT the raw /mnt/<name> it receives. Registering the raw path made the