Shares of other services and RepoISO integration
build / build (push) Successful in 1m43s

- include SMB drop-ins from /etc/samba/smb.conf.d into the generated smb.conf
- keep smbd/nfs-server running while other services (/etc/exports.d, drop-ins) share,
  instead of disabling them when nasmon has no shares of its own
- list those shares read-only on /shares
- settings: RepoISO URL; poll its /api/integration/status, dashboard card, menu link,
  alerts for portal down, stale worker, failed jobs, unmounted publications

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 15:06:17 +03:00
co-authored by Claude Opus 5.5
parent 661a862f8d
commit 30dbb00dd1
22 changed files with 656 additions and 19 deletions
+35
View File
@@ -0,0 +1,35 @@
package alert
import (
"testing"
"time"
"nasmon/internal/collect"
)
func TestRepoISORules(t *testing.T) {
base := Input{Now: time.Now()}
if ev := Evaluate(base); len(ev) != 0 {
t.Fatalf("integration off must not alert: %v", keys(ev))
}
in := base
in.RepoISO = &collect.RepoISO{URL: "http://127.0.0.1", Error: "connection refused"}
ev := Evaluate(in)
if !has(ev, "repoiso:api:down") || len(ev) != 1 || ev[0].Level != LevelCrit {
t.Fatalf("down: %v", keys(ev))
}
in.RepoISO = &collect.RepoISO{Reachable: true, WorkerAlive: false, JobsFailed24h: 2, PublicationsError: 1}
ev = Evaluate(in)
for _, k := range []string{"repoiso:worker:stale", "repoiso:jobs:failed", "repoiso:publications:error"} {
if !has(ev, k) {
t.Errorf("missing %s in %v", k, keys(ev))
}
}
in.RepoISO = &collect.RepoISO{Reachable: true, WorkerAlive: true}
if ev = Evaluate(in); len(ev) != 0 {
t.Fatalf("healthy RepoISO alerted: %v", keys(ev))
}
}
+28
View File
@@ -40,6 +40,8 @@ type Input struct {
HasCPU bool
RootFS collect.FSUsage
RootOK bool
// RepoISO is the polled RepoISO health (nil when the integration is off).
RepoISO *collect.RepoISO
// Frozen lists key prefixes whose open alerts must be left untouched
// because no fresh data was available for them in this cycle.
Frozen []string
@@ -194,10 +196,36 @@ func Evaluate(in Input) []Event {
}
}
if r := in.RepoISO; r != nil {
switch {
case !r.Reachable:
add("repoiso:api:down", LevelCrit, "RepoISO is not responding",
fmt.Sprintf("%s/api/integration/status: %s. Check: systemctl status repoiso-api nginx", r.URL, r.Error))
case !r.WorkerAlive:
add("repoiso:worker:stale", LevelWarn, "RepoISO worker is not running",
fmt.Sprintf("Last worker heartbeat: %s. Downloads, mounts and Proxmox exports are not processed. Check: systemctl status repoiso-worker", orNever(r.WorkerHeartbeat)))
}
if r.Reachable && r.JobsFailed24h > 0 {
add("repoiso:jobs:failed", LevelWarn, fmt.Sprintf("RepoISO: %d failed job(s) in 24 h", r.JobsFailed24h),
"See the Jobs page of RepoISO for the errors.")
}
if r.Reachable && r.PublicationsError > 0 {
add("repoiso:publications:error", LevelWarn, fmt.Sprintf("RepoISO: %d publication(s) not mounted", r.PublicationsError),
"See the Publications page of RepoISO; remount or unpublish the image.")
}
}
sort.Slice(ev, func(i, j int) bool { return ev[i].Key < ev[j].Key })
return ev
}
func orNever(s string) string {
if s == "" {
return "never"
}
return s
}
func humanBytes(b uint64) string {
const unit = 1024
if b < unit {