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:
@@ -29,6 +29,7 @@ import (
|
|||||||
// without a live agent). *agentapi.Client satisfies it.
|
// without a live agent). *agentapi.Client satisfies it.
|
||||||
type diskAgent interface {
|
type diskAgent interface {
|
||||||
Disks(ctx context.Context) (agentapi.DisksResponse, error)
|
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)
|
FormatDisk(ctx context.Context, device, fstype string, confirmed bool, durableID string) (agentapi.FormatResult, error)
|
||||||
AssignDisk(ctx context.Context, uuid, where, fstype, options string) error
|
AssignDisk(ctx context.Context, uuid, where, fstype, options string) error
|
||||||
EjectDisk(ctx context.Context, where string) (agentapi.EjectResult, error)
|
EjectDisk(ctx context.Context, where string) (agentapi.EjectResult, error)
|
||||||
@@ -63,6 +64,31 @@ func fsUUIDForDevice(disks agentapi.DisksResponse, device string) string {
|
|||||||
return ""
|
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).
|
// storageInitResult is the outcome of an init attempt (JSON-rendered to the wizard).
|
||||||
type storageInitResult struct {
|
type storageInitResult struct {
|
||||||
Registered bool `json:"registered"`
|
Registered bool `json:"registered"`
|
||||||
@@ -107,12 +133,9 @@ func (s *Server) runStorageInit(ctx context.Context, agent diskAgent, device, fs
|
|||||||
if !fr.Formatted {
|
if !fr.Formatted {
|
||||||
return storageInitResult{}, fmt.Errorf("az eszköz nem lett megformázva (%s)", fr.Reason)
|
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).
|
// 2. Resolve the NEW fs UUID. A freshly-formatted RAW device isn't in /disks (not enrolled yet), so
|
||||||
disks, err := agent.Disks(ctx)
|
// resolve via the raw-device scan too (Impl-2b) — the device now appears there with its new durable_id.
|
||||||
if err != nil {
|
uuid := resolveEnrollUUID(ctx, agent, device)
|
||||||
return storageInitResult{}, fmt.Errorf("formázás kész, de a meghajtólista nem olvasható: %w", err)
|
|
||||||
}
|
|
||||||
uuid := fsUUIDForDevice(disks, device)
|
|
||||||
if uuid == "" {
|
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")
|
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)
|
// runStorageAttach mounts an existing-filesystem device (non-destructive — never touches the gate)
|
||||||
// and registers it. The UUID is resolved server-side from the device.
|
// 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) {
|
func (s *Server) runStorageAttach(ctx context.Context, agent diskAgent, device, fstype, where, label string, setDefault bool) (storageInitResult, error) {
|
||||||
disks, err := agent.Disks(ctx)
|
// Resolve the fs UUID. Works for a legacy re-attach (drive in /disks) AND a raw candidate (Impl-2b:
|
||||||
if err != nil {
|
// not in /disks — resolved via the raw-device scan's durable_id).
|
||||||
return storageInitResult{}, fmt.Errorf("a meghajtólista nem olvasható: %w", err)
|
uuid := resolveEnrollUUID(ctx, agent, device)
|
||||||
}
|
|
||||||
uuid := fsUUIDForDevice(disks, device)
|
|
||||||
if uuid == "" {
|
if uuid == "" {
|
||||||
return storageInitResult{}, fmt.Errorf("a kiválasztott meghajtóhoz nem található fájlrendszer-azonosító (csak fájlrendszerrel rendelkező meghajtó csatolható)")
|
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
|
decommissionCalls []string
|
||||||
guestRebootCalls int
|
guestRebootCalls int
|
||||||
guestRebootErr error
|
guestRebootErr error
|
||||||
|
candidates agentapi.CandidatesResult
|
||||||
}
|
}
|
||||||
|
|
||||||
type assignCall struct{ uuid, where, fstype string }
|
type assignCall struct{ uuid, where, fstype string }
|
||||||
@@ -54,6 +55,10 @@ func (m *mockAgent) Disks(context.Context) (agentapi.DisksResponse, error) {
|
|||||||
m.disksCalls++
|
m.disksCalls++
|
||||||
return m.disks, nil
|
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) {
|
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})
|
m.formatCalls = append(m.formatCalls, formatCall{device, fstype, durableID, confirmed})
|
||||||
return m.formatRes, m.formatErr
|
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
|
// 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/
|
// 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
|
// runStorageAttach register — NOT the raw /mnt/<name> it receives. Registering the raw path made the
|
||||||
|
|||||||
Reference in New Issue
Block a user