controller: F8 share-row stub fusion + F4 mapped_uid range validation (WIP, pre-build)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/logx"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
)
|
||||
|
||||
// NAS network storage (Part A2). The controller is a thin proxy over the agent's /netstorage/* (A1) +
|
||||
@@ -23,6 +24,19 @@ import (
|
||||
// agent applies the +100000 host offset; this is the in-guest id the share is mapped to.
|
||||
const defaultMediaUID = 1000
|
||||
|
||||
// mappedIDMin/Max bound a valid CONTAINER uid/gid (F4). 65534 is `nobody`; a host-side mapped value
|
||||
// (e.g. 101000 = 1000+100000) must never be entered as the app uid.
|
||||
const (
|
||||
mappedIDMin = 1
|
||||
mappedIDMax = 65533
|
||||
)
|
||||
|
||||
// validMappedID reports whether id is a valid container uid/gid (F4 range check).
|
||||
func validMappedID(id int) bool { return id >= mappedIDMin && id <= mappedIDMax }
|
||||
|
||||
// netAddUIDRangeMsg is the friendly F4 refusal for an out-of-range mapped uid/gid.
|
||||
const netAddUIDRangeMsg = "Az alkalmazás felhasználói azonosítója (uid) érvénytelen. Adjon meg 1 és 65533 közötti értéket — a legtöbb médiaalkalmazás az 1000-est használja."
|
||||
|
||||
// netAddOutdatedMsg is the sync add-time refusal (machine code "agent_outdated") when the agent
|
||||
// predates the coupled verify-before-commit add semantics (pre-v0.81.0).
|
||||
const netAddOutdatedMsg = "Az ügynök frissítése szükséges ehhez a funkcióhoz — a frissítés megérkezése után próbáld újra."
|
||||
@@ -50,8 +64,8 @@ type networkStorageItem struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Server string `json:"server"`
|
||||
Export string `json:"export"`
|
||||
Path string `json:"path"` // the in-guest path apps point HDD_PATH at
|
||||
Health string `json:"health"` // ok | idle | unreachable | unknown
|
||||
Path string `json:"path"` // the in-guest path apps point HDD_PATH at
|
||||
Health string `json:"health"` // ok | idle | unreachable | stub | unknown
|
||||
Reachable bool `json:"reachable"`
|
||||
Mounted bool `json:"mounted"`
|
||||
Configured bool `json:"configured"`
|
||||
@@ -108,6 +122,16 @@ func (s *Server) handleNetStorageAdd(w http.ResponseWriter, r *http.Request) {
|
||||
if gid <= 0 {
|
||||
gid = defaultMediaUID
|
||||
}
|
||||
// F4 (CAMPAIGN-3): validate the CONTAINER uid/gid range at the door. The guest maps <uid> to
|
||||
// <uid>+100000 on the host, so a valid app uid is 1..65533 (65534 = nobody; a host-side mapped
|
||||
// value like 101000 must NOT be entered as the app uid). Out of range previously slipped past the
|
||||
// controller and failed only at the agent with a raw `agent_error` (the campaign's 101000). Refuse
|
||||
// here with a friendly Hungarian 400 — nothing is installed.
|
||||
if !validMappedID(uid) || !validMappedID(gid) {
|
||||
logx.Debugf(s.logger, "[web] netstorage add %q refused by validation: uid/gid out of range (uid=%d gid=%d)", name, uid, gid)
|
||||
writeDiskJSON(w, http.StatusBadRequest, false, netAddUIDRangeMsg, nil)
|
||||
return
|
||||
}
|
||||
|
||||
agent, err := s.netAgentForAdd()
|
||||
if err != nil {
|
||||
@@ -191,6 +215,12 @@ func (s *Server) networkStorageItems(ctx context.Context) []networkStorageItem {
|
||||
it.Mounted = m.Mounted
|
||||
it.Configured = m.Configured
|
||||
}
|
||||
// F8 (CAMPAIGN-3): fuse the consuming-namespace classification so the SHARE ROW tells the same
|
||||
// truth as the stacks/dashboard stub badge — both now read `classifyFSPath`, so they can never
|
||||
// contradict. The agent's `health` derives from a SERVER-LEVEL TCP dial that stays green when a
|
||||
// single export is `exportfs -u`'d (the server still answers on 2049/445); the namespace verdict
|
||||
// is the only thing that sees the export-level outage. See fuseNetHealth.
|
||||
it.Health = s.fuseNetHealth(it.Health, it.Path)
|
||||
items = append(items, it)
|
||||
}
|
||||
// Orphans (Scenario F's visible closure): an agent-configured share with NO registry entry is a
|
||||
@@ -219,6 +249,35 @@ func (s *Server) networkStorageItems(ctx context.Context) []networkStorageItem {
|
||||
return items
|
||||
}
|
||||
|
||||
// Net-health values used in the fusion (the agent supplies ok/idle/unreachable/unknown; the
|
||||
// controller ADDS stub — configured + server reachable, but the consuming namespace does NOT see the
|
||||
// network fs at Where, so app data would hit local disk).
|
||||
const (
|
||||
netHealthUnreachable = "unreachable"
|
||||
netHealthStub = "stub"
|
||||
)
|
||||
|
||||
// fuseNetHealth reconciles the agent-reported health with the controller's consuming-namespace
|
||||
// classification (F8, CAMPAIGN-3). Precedence:
|
||||
// - `unreachable` (agent TCP dial failed — a whole-server outage) is the most actionable and WINS;
|
||||
// the classifier is not allowed to override it (the row must say "server down", not "stub").
|
||||
// - otherwise a `stub` classification at Where (the namespace sees local disk / an empty dir, not
|
||||
// the NAS) OVERRIDES a benign idle/ok — this is the exact F8 contradiction resolved.
|
||||
// - autofs-healthy (idle trigger), a real network fs, or an inconclusive `unknown`/fail-open read
|
||||
// leave the agent-derived health untouched — never manufacture a fault, never force-mount.
|
||||
func (s *Server) fuseNetHealth(agentHealth, where string) string {
|
||||
if agentHealth == netHealthUnreachable {
|
||||
return agentHealth // a whole-server outage is the more actionable truth
|
||||
}
|
||||
if s.classifyFSPath == nil || where == "" {
|
||||
return agentHealth
|
||||
}
|
||||
if s.classifyFSPath(where) == system.FSClassStub {
|
||||
return netHealthStub
|
||||
}
|
||||
return agentHealth
|
||||
}
|
||||
|
||||
// handleNetStorageList returns the registered network shares merged with the agent's live per-share health.
|
||||
func (s *Server) handleNetStorageList(w http.ResponseWriter, r *http.Request) {
|
||||
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"network_storage": s.networkStorageItems(r.Context())})
|
||||
|
||||
Reference in New Issue
Block a user