Disks tab: lsblk devices with partitions/usage/live IO, SMART via smartctl (standby-aware, self-tests, install button), ZFS pools/datasets/snapshots with scrub and snapshot-now
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
724f30c02b
commit
c704e87065
+86
-2
@@ -22,6 +22,7 @@ import (
|
||||
"hamlab/internal/cron"
|
||||
"hamlab/internal/metrics"
|
||||
"hamlab/internal/services"
|
||||
"hamlab/internal/storage"
|
||||
"hamlab/internal/store"
|
||||
"hamlab/internal/tasks"
|
||||
"hamlab/internal/term"
|
||||
@@ -41,6 +42,7 @@ type Server struct {
|
||||
cron *cron.Manager
|
||||
tasks *tasks.Manager
|
||||
updates *updates.Manager
|
||||
storage *storage.Manager
|
||||
version string
|
||||
started time.Time
|
||||
user string
|
||||
@@ -49,9 +51,9 @@ type Server struct {
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *metrics.Collector, st *store.Store, svc *services.Manager, cr *cron.Manager, tm *tasks.Manager, up *updates.Manager, ui fs.FS, version string) *Server {
|
||||
func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *metrics.Collector, st *store.Store, svc *services.Manager, cr *cron.Manager, tm *tasks.Manager, up *updates.Manager, sto *storage.Manager, ui fs.FS, version string) *Server {
|
||||
host, _ := os.Hostname()
|
||||
s := &Server{cfg: cfg, sessions: sessions, mgr: mgr, metrics: col, store: st, svc: svc, cron: cr, tasks: tm, updates: up, ui: ui, version: version,
|
||||
s := &Server{cfg: cfg, sessions: sessions, mgr: mgr, metrics: col, store: st, svc: svc, cron: cr, tasks: tm, updates: up, storage: sto, ui: ui, version: version,
|
||||
started: time.Now(), host: host, user: os.Getenv("USER")}
|
||||
if u, err := user.Current(); err == nil && u.Username != "" {
|
||||
s.user = u.Username
|
||||
@@ -99,6 +101,13 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
|
||||
m.HandleFunc("POST /api/updates/upgrade", s.auth(s.updatesUpgrade))
|
||||
m.HandleFunc("GET /api/updates/changelog", s.auth(s.updatesChangelog))
|
||||
m.HandleFunc("POST /api/power/{action}", s.auth(s.power))
|
||||
m.HandleFunc("POST /api/updates/install", s.auth(s.updatesInstall))
|
||||
m.HandleFunc("GET /api/storage", s.auth(s.storageOverview))
|
||||
m.HandleFunc("GET /api/storage/smart/{dev}", s.auth(s.storageSmart))
|
||||
m.HandleFunc("POST /api/storage/smart/{dev}/test/{kind}", s.auth(s.storageSmartTest))
|
||||
m.HandleFunc("GET /api/storage/zfs/snapshots", s.auth(s.zfsSnapshots))
|
||||
m.HandleFunc("POST /api/storage/zfs/scrub/{pool}/{action}", s.auth(s.zfsScrub))
|
||||
m.HandleFunc("POST /api/storage/zfs/snapshot", s.auth(s.zfsSnapshot))
|
||||
m.HandleFunc("GET /api/prefs/dashboard", s.auth(s.getDashPrefs))
|
||||
m.HandleFunc("PUT /api/prefs/dashboard", s.auth(s.putDashPrefs))
|
||||
m.HandleFunc("/api/", func(w http.ResponseWriter, _ *http.Request) { jsonErr(w, 404, "not found") })
|
||||
@@ -588,6 +597,81 @@ func (s *Server) power(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
// ---- storage (Disks tab) ----
|
||||
|
||||
func (s *Server) updatesInstall(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Packages []string `json:"packages"`
|
||||
}
|
||||
_ = json.NewDecoder(io.LimitReader(r.Body, 64<<10)).Decode(&body)
|
||||
t, err := s.updates.Install(body.Packages)
|
||||
if err != nil {
|
||||
jsonErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("updates: %s started as task %s (from %s)", t.Title, t.ID, clientIP(r))
|
||||
jsonOK(w, 202, t.Snapshot(false))
|
||||
}
|
||||
|
||||
func (s *Server) storageOverview(w http.ResponseWriter, r *http.Request) {
|
||||
devs, err := storage.Devices(r.Context())
|
||||
if err != nil {
|
||||
jsonErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, 200, map[string]any{"devices": devs, "zfs": s.storage.ZFS(r.Context()), "smartctl": s.storage.HasSmartctl()})
|
||||
}
|
||||
|
||||
func (s *Server) storageSmart(w http.ResponseWriter, r *http.Request) {
|
||||
sm := s.storage.Smart(r.Context(), r.PathValue("dev"), r.URL.Query().Get("force") == "1")
|
||||
if sm.Error == "unknown device" {
|
||||
jsonErr(w, 404, sm.Error)
|
||||
return
|
||||
}
|
||||
jsonOK(w, 200, sm)
|
||||
}
|
||||
|
||||
func (s *Server) storageSmartTest(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.storage.SelfTest(r.Context(), r.PathValue("dev"), r.PathValue("kind")); err != nil {
|
||||
jsonErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("smart: %s test on %s (from %s)", r.PathValue("kind"), r.PathValue("dev"), clientIP(r))
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
func (s *Server) zfsSnapshots(w http.ResponseWriter, r *http.Request) {
|
||||
snaps, err := s.storage.Snapshots(r.Context(), r.URL.Query().Get("dataset"))
|
||||
if err != nil {
|
||||
jsonErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, 200, snaps)
|
||||
}
|
||||
|
||||
func (s *Server) zfsScrub(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.storage.Scrub(r.Context(), r.PathValue("pool"), r.PathValue("action")); err != nil {
|
||||
jsonErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("zfs: scrub %s %s (from %s)", r.PathValue("action"), r.PathValue("pool"), clientIP(r))
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
func (s *Server) zfsSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Dataset string `json:"dataset"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
_ = json.NewDecoder(io.LimitReader(r.Body, 4096)).Decode(&body)
|
||||
if err := s.storage.CreateSnapshot(r.Context(), body.Dataset, body.Name); err != nil {
|
||||
jsonErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("zfs: snapshot %s@%s (from %s)", body.Dataset, body.Name, clientIP(r))
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
|
||||
// ---- dashboard layout prefs (opaque JSON object, owned by the UI) ----
|
||||
|
||||
func (s *Server) getDashPrefs(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user