107 lines
4.5 KiB
Go
107 lines
4.5 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"sort"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
|
)
|
|
|
|
// Agent-backed host metrics (slice 9).
|
|
//
|
|
// The de-privileged controller (slice 8C) sees only its own cgroup, so it cannot read host
|
|
// health itself. This thin proxy forwards GET /api/host-metrics to the agent's GET /host/metrics
|
|
// and returns the host-wide view (cpu%/mem/load/uptime/cpu-temp + per-storage capacity) for the
|
|
// monitoring page. It reuses the same pinned agentapi.Client + {ok,data,error} envelope as the
|
|
// disk proxy (agent_disk_handlers.go). Read-only; no CSRF mutation.
|
|
|
|
// ServeHostMetricsAPI proxies GET /api/host-metrics → agent GET /host/metrics.
|
|
// Wired in main.go behind RequireAuth.
|
|
func (s *Server) ServeHostMetricsAPI(w http.ResponseWriter, r *http.Request) {
|
|
if s.isDebug() {
|
|
s.logger.Printf("[DEBUG] [web] ServeHostMetricsAPI: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
|
|
}
|
|
if r.Method != http.MethodGet {
|
|
writeDiskJSON(w, http.StatusMethodNotAllowed, false, "method not allowed", nil)
|
|
return
|
|
}
|
|
client, err := s.agentClient()
|
|
if err != nil {
|
|
// Unprovisioned guest / no local API configured — the UI shows "host metrics unavailable".
|
|
writeDiskJSON(w, http.StatusServiceUnavailable, false, s.errText(r, err), nil)
|
|
return
|
|
}
|
|
resp, err := client.HostMetrics(r.Context())
|
|
if err != nil {
|
|
s.logger.Printf("[ERROR] [web] host metrics via agent failed: %v", err)
|
|
writeDiskJSON(w, http.StatusBadGateway, false, s.errText(r, err), nil)
|
|
return
|
|
}
|
|
// The agent enumerates storages via `pvesm` in a non-deterministic order, so #host-storage-bars
|
|
// reordered on every poll (item 2). Stabilise the order Go-side and attach friendly labels + a
|
|
// one-line purpose per entry in the reader's language (W26, rehearsal 2026-10-20: they were
|
|
// Hungarian on the English dashboard) — display-only; we NEVER rename the PVE storage ids.
|
|
lang := s.langFor(r)
|
|
enrichHostStorageTargets(resp.StorageTargets, func(key string) string { return s.msgLang(lang, key) })
|
|
writeDiskJSON(w, http.StatusOK, true, "", resp)
|
|
}
|
|
|
|
// enrichHostStorageTargets sorts the host's storage targets into a stable, customer-meaningful
|
|
// order (user-data → system+apps → backup → other; alphabetical by id within a tier) and fills in
|
|
// a friendly label + purpose per entry. Mirrors the disk-overview's sortDisksForView contract:
|
|
// a Go-side ordering beats relying on the agent's enumeration order or template JS.
|
|
// msg turns a message key into the reader's text.
|
|
func enrichHostStorageTargets(targets []agentapi.StorageTarget, msg func(key string) string) {
|
|
sort.SliceStable(targets, func(i, j int) bool {
|
|
if ri, rj := storageTypeRank(targets[i].Type), storageTypeRank(targets[j].Type); ri != rj {
|
|
return ri < rj
|
|
}
|
|
return targets[i].Name < targets[j].Name
|
|
})
|
|
for i := range targets {
|
|
label, purpose := storageLabelAndPurpose(targets[i], msg)
|
|
targets[i].Label = label
|
|
targets[i].Purpose = purpose
|
|
}
|
|
}
|
|
|
|
// storageTypeRank orders storage by what the customer cares about: where their app data lives
|
|
// first, then the system/app disk, then backup targets. Lower sorts first.
|
|
func storageTypeRank(typ string) int {
|
|
switch typ {
|
|
case "usb", "local-dir":
|
|
return 0 // external user-data drives (where browsable app data lives)
|
|
case "lvmthin", "lvm":
|
|
return 1 // the internal SSD: OS + the guest/app volumes
|
|
case "local":
|
|
return 2 // builtin dir: templates + local vzdump backups
|
|
case "pbs", "nfs", "cifs":
|
|
return 3 // backup targets (offsite / network)
|
|
default:
|
|
return 4
|
|
}
|
|
}
|
|
|
|
// storageLabelAndPurpose maps a storage target to a friendly label + one-line purpose (message keys
|
|
// host_storage.*). Falls back to the raw id for unrecognised types. The raw id stays in Name (rendered muted).
|
|
func storageLabelAndPurpose(t agentapi.StorageTarget, msg func(key string) string) (string, string) {
|
|
switch t.Type {
|
|
case "usb":
|
|
return msg("host_storage.usb.label"), msg("host_storage.userdata.purpose")
|
|
case "local-dir":
|
|
return msg("host_storage.local_dir.label"), msg("host_storage.userdata.purpose")
|
|
case "lvmthin", "lvm":
|
|
return msg("host_storage.system.label"), msg("host_storage.system.purpose")
|
|
case "local":
|
|
return msg("host_storage.local.label"), msg("host_storage.local.purpose")
|
|
case "pbs":
|
|
return msg("host_storage.pbs.label"), msg("host_storage.pbs.purpose")
|
|
case "nfs":
|
|
return msg("host_storage.nfs.label"), msg("host_storage.network.purpose")
|
|
case "cifs":
|
|
return msg("host_storage.cifs.label"), msg("host_storage.network.purpose")
|
|
default:
|
|
return t.Name, ""
|
|
}
|
|
}
|