R-542: the disk candidates no longer offer a registered, in-use drive
gates / gates (push) Successful in 1m2s
gates / gates (push) Successful in 1m2s
The format wizard rendered the agent's initialize list as-is, and the agent deliberately allows re-initialising Felhom's own drives, so a registered data drive was offered for formatting. The controller proxy now drops every candidate that backs a registered storage path (joined through the guest mount table) from both lists; an unreadable mount table empties initialize. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -5,7 +5,9 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
||||
)
|
||||
@@ -126,9 +128,10 @@ func (s *Server) agentDisksListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// agentDiskCandidatesHandler proxies GET /api/disks/candidates → agent GET /disks/candidates (Impl-2b):
|
||||
// the raw-device scan (Impl-2a) that feeds the enrollment wizards. The agent's unclaimed-disk filter
|
||||
// already excludes claimed/OS/enrolled disks (fail-safe), so `initialize` passes through UNTOUCHED —
|
||||
// no controller-side filtering, and the system/backup drives it hides from the format wizard stay
|
||||
// hidden.
|
||||
// already excludes claimed/OS disks (fail-safe), and the system/backup drives it hides from the format
|
||||
// wizard stay hidden. It deliberately does NOT exclude Felhom's own drives, so the controller removes
|
||||
// every drive backing a REGISTERED storage path from both lists (R-542, dropRegisteredDrives) — the
|
||||
// one filter the controller adds; it only ever removes entries.
|
||||
//
|
||||
// R-280: `attach` additionally carries the controller's own mounted-but-unregistered filesystems.
|
||||
// The agent's scan alone left a rebuilt box with an empty picker under a sentence promising „két
|
||||
@@ -150,9 +153,76 @@ func (s *Server) agentDiskCandidatesHandler(w http.ResponseWriter, r *http.Reque
|
||||
writeDiskJSON(w, http.StatusBadGateway, false, s.errText(r, err), nil)
|
||||
return
|
||||
}
|
||||
mounts := readMountTable()
|
||||
resp = dropRegisteredDrives(resp, mounts, s.registeredStoragePaths())
|
||||
writeDiskJSON(w, http.StatusOK, true, "", mergeAttachCandidates(resp, s.attachableStores()))
|
||||
}
|
||||
|
||||
// dropRegisteredDrives (R-542) removes from BOTH lists every agent candidate that backs a REGISTERED
|
||||
// storage path. The agent deliberately lets Felhom re-initialise its own drives (a mount under
|
||||
// /mnt/felhom-drives is not a foreign claim — felhom-agent internal/storage/claim.go), so a drive the
|
||||
// household registered and uses came back under `initialize` — the format wizard (storage_init.html)
|
||||
// renders that list as-is. Measured 2026-09-16 on a fresh box: the registered default data drive was
|
||||
// offered for formatting and again under `attach`.
|
||||
//
|
||||
// The join is the guest's own mount table: a registered path's mount SOURCE is the host device
|
||||
// (measured on demo-hp 2026-10-06: `/dev/nvme0n1 /mnt/felhom-drives/hdd_1`), which is the candidate's
|
||||
// Device (whole disk) or MountSource (partition).
|
||||
//
|
||||
// ⚠ FAIL-SAFE: an unreadable mount table cannot prove any candidate is NOT in use, so `initialize`
|
||||
// comes back EMPTY (the wizard then says no drive is available) — never the unfiltered list. `attach`
|
||||
// is non-destructive and is left as the agent sent it in that case.
|
||||
func dropRegisteredDrives(resp agentapi.CandidatesResult, mountsText string, registered map[string]bool) agentapi.CandidatesResult {
|
||||
if strings.TrimSpace(mountsText) == "" {
|
||||
resp.Initialize = []agentapi.DiskCandidate{}
|
||||
return resp
|
||||
}
|
||||
inUse := map[string]bool{}
|
||||
for _, row := range parseMountTable(mountsText) {
|
||||
if registered[path.Clean(row[1])] && strings.HasPrefix(row[0], "/dev/") {
|
||||
inUse[row[0]] = true
|
||||
}
|
||||
}
|
||||
if len(inUse) == 0 {
|
||||
return resp
|
||||
}
|
||||
keep := func(in []agentapi.DiskCandidate) []agentapi.DiskCandidate {
|
||||
out := make([]agentapi.DiskCandidate, 0, len(in))
|
||||
for _, c := range in {
|
||||
if candidateBacksInUse(c, inUse) {
|
||||
continue
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out
|
||||
}
|
||||
resp.Initialize = keep(resp.Initialize)
|
||||
resp.Attach = keep(resp.Attach)
|
||||
return resp
|
||||
}
|
||||
|
||||
// candidateBacksInUse reports whether a candidate's disk is a source in inUse: the whole disk itself,
|
||||
// its mountable partition, or any partition of it (/dev/sdb1, /dev/nvme0n1p1 under /dev/sdb, /dev/nvme0n1).
|
||||
func candidateBacksInUse(c agentapi.DiskCandidate, inUse map[string]bool) bool {
|
||||
if inUse[c.Device] || (c.MountSource != "" && inUse[c.MountSource]) {
|
||||
return true
|
||||
}
|
||||
if c.Device == "" {
|
||||
return false
|
||||
}
|
||||
for src := range inUse {
|
||||
rest, ok := strings.CutPrefix(src, c.Device)
|
||||
if !ok || rest == "" {
|
||||
continue
|
||||
}
|
||||
rest = strings.TrimPrefix(rest, "p")
|
||||
if rest != "" && strings.Trim(rest, "0123456789") == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// mergeAttachCandidates adds the mounted-but-unregistered stores to `attach` and returns the result.
|
||||
// `initialize` is passed through untouched — the ONE line that keeps the format wizard's protection
|
||||
// intact, and the reason this is a separate function rather than two appends at the call site: it can
|
||||
|
||||
Reference in New Issue
Block a user