diff --git a/README.md b/README.md index b911657..adda92d 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ share management from the UI. * Service: `systemctl status nasmon` / `journalctl -u nasmon -f` * JSON API: `/api/status`, `/api/history?hours=24`, `/api/disks//history?days=30` -Integrates with RepoISO on the same host: shares of other services -(`/etc/samba/smb.conf.d`, `/etc/exports.d`) are included and kept running, -and RepoISO health is shown on the dashboard and alerted on. +Shares of other software on the same host (`/etc/samba/smb.conf.d/*.conf`, +`/etc/exports.d/*.exports`) are included into the generated config, listed +read-only and keep smbd / nfs-server running. No authentication — designed for a LAN-only NAS (the `/shares` page can create SMB/NFS users and exports, so keep it off the public internet). diff --git a/internal/alert/repoiso_test.go b/internal/alert/repoiso_test.go deleted file mode 100644 index 9e77cf5..0000000 --- a/internal/alert/repoiso_test.go +++ /dev/null @@ -1,35 +0,0 @@ -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)) - } -} diff --git a/internal/alert/rules.go b/internal/alert/rules.go index 4228ae5..05092ce 100644 --- a/internal/alert/rules.go +++ b/internal/alert/rules.go @@ -40,8 +40,6 @@ 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 @@ -196,36 +194,10 @@ 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 { diff --git a/internal/collect/repoiso.go b/internal/collect/repoiso.go deleted file mode 100644 index 144e9ee..0000000 --- a/internal/collect/repoiso.go +++ /dev/null @@ -1,54 +0,0 @@ -package collect - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - "time" -) - -// RepoISO is the health summary of a RepoISO portal running on the same host -// (GET /api/integration/status). Reachable=false means the API did not answer. -type RepoISO struct { - URL string `json:"url"` - Reachable bool `json:"reachable"` - Error string `json:"error,omitempty"` - CheckedAt time.Time `json:"checked_at"` - Version string `json:"version"` - WorkerAlive bool `json:"worker_alive"` - WorkerHeartbeat string `json:"worker_heartbeat"` - Images int `json:"images"` - ImagesBytes uint64 `json:"images_bytes"` - ImagesPending int `json:"images_pending"` - ImagesError int `json:"images_error"` - Publications int `json:"publications"` - PublicationsError int `json:"publications_error"` - JobsActive int `json:"jobs_active"` - JobsFailed24h int `json:"jobs_failed_24h"` - Exports int `json:"exports"` -} - -var repoisoClient = &http.Client{Timeout: 5 * time.Second} - -// FetchRepoISO queries the RepoISO status endpoint; errors are reported in the result. -func FetchRepoISO(base string) RepoISO { - base = strings.TrimRight(base, "/") - r := RepoISO{URL: base, CheckedAt: time.Now()} - resp, err := repoisoClient.Get(base + "/api/integration/status") - if err != nil { - r.Error = err.Error() - return r - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - r.Error = fmt.Sprintf("HTTP %d", resp.StatusCode) - return r - } - if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { - r.Error = "bad response: " + err.Error() - return r - } - r.URL, r.Reachable, r.Error = base, true, "" - return r -} diff --git a/internal/collect/repoiso_test.go b/internal/collect/repoiso_test.go deleted file mode 100644 index f9c8449..0000000 --- a/internal/collect/repoiso_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package collect - -import ( - "net/http" - "net/http/httptest" - "testing" -) - -func TestFetchRepoISO(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/integration/status" { - http.NotFound(w, r) - return - } - _, _ = w.Write([]byte(`{"version":"0.1.3","worker_alive":true,"images":5,"images_bytes":1024,"jobs_failed_24h":1}`)) - })) - defer srv.Close() - r := FetchRepoISO(srv.URL + "/") - if !r.Reachable || r.Version != "0.1.3" || !r.WorkerAlive || r.Images != 5 || r.JobsFailed24h != 1 || r.URL != srv.URL { - t.Fatalf("unexpected %+v", r) - } - if r := FetchRepoISO(srv.URL + "/missing"); r.Reachable || r.Error != "HTTP 404" { - t.Fatalf("404 not reported: %+v", r) - } - srv.Close() - if r := FetchRepoISO(srv.URL); r.Reachable || r.Error == "" { - t.Fatalf("closed server reported reachable: %+v", r) - } -} diff --git a/internal/config/config.go b/internal/config/config.go index e287d15..78b3bae 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,23 +40,15 @@ type SelfTests struct { Hour int `json:"hour"` // local hour of day when tests may be started } -// Integrations links nasmon with other services on the same host. -type Integrations struct { - // RepoISOURL is the RepoISO portal base URL nasmon polls for health - // (e.g. http://127.0.0.1); empty disables the integration. - RepoISOURL string `json:"repoiso_url"` -} - type Settings struct { - SMTP SMTP `json:"smtp"` - AlertsEnabled bool `json:"alerts_enabled"` - NotifyResolved bool `json:"notify_resolved"` - RemindHours int `json:"remind_hours"` - SmartIntervalMin int `json:"smart_interval_min"` - SmartSkipStandby bool `json:"smart_skip_standby"` - Thresholds Thresholds `json:"thresholds"` - SelfTests SelfTests `json:"self_tests"` - Integrations Integrations `json:"integrations"` + SMTP SMTP `json:"smtp"` + AlertsEnabled bool `json:"alerts_enabled"` + NotifyResolved bool `json:"notify_resolved"` + RemindHours int `json:"remind_hours"` + SmartIntervalMin int `json:"smart_interval_min"` + SmartSkipStandby bool `json:"smart_skip_standby"` + Thresholds Thresholds `json:"thresholds"` + SelfTests SelfTests `json:"self_tests"` } func Defaults() Settings { @@ -110,11 +102,6 @@ func (s *Settings) Validate() error { if s.SelfTests.ShortDays < 0 || s.SelfTests.LongDays < 0 { return errors.New("self-test periods must be >= 0") } - u := strings.TrimRight(strings.TrimSpace(s.Integrations.RepoISOURL), "/") - if u != "" && !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") { - return errors.New("RepoISO URL must start with http:// or https://") - } - s.Integrations.RepoISOURL = u return nil } diff --git a/internal/sched/sched.go b/internal/sched/sched.go index e335411..1f4de26 100644 --- a/internal/sched/sched.go +++ b/internal/sched/sched.go @@ -54,7 +54,6 @@ type Status struct { NextSmartCheck time.Time `json:"next_smart_check"` SmartRunning bool `json:"smart_running"` LastMailErr string `json:"last_mail_err,omitempty"` - RepoISO *collect.RepoISO `json:"repoiso,omitempty"` } type Scheduler struct { @@ -244,11 +243,6 @@ func (s *Scheduler) collectStorage() { root, _ := collect.ReadFS("/") alerts, _ := s.st.ActiveAlerts() services := collect.ReadShareServices() - var repoiso *collect.RepoISO - if url := s.cfg.Get().Integrations.RepoISOURL; url != "" { - r := collect.FetchRepoISO(url) - repoiso = &r - } s.mu.Lock() defer s.mu.Unlock() @@ -270,7 +264,6 @@ func (s *Scheduler) collectStorage() { s.status.RootFS = root s.status.Alerts = alerts s.status.Services = services - s.status.RepoISO = repoiso } // buildViews merges lsblk disks with cached SMART data; caller holds s.mu. @@ -426,8 +419,7 @@ func (s *Scheduler) evaluate() { Disks: nil, Pools: st.Pools, PoolsOK: st.PoolsErr == "", CPUTemp: st.CPUTemp, HasCPU: st.HasCPUTemp, RootFS: st.RootFS, RootOK: st.RootFS.Total > 0, - Prev: map[string]*collect.Smart{}, - RepoISO: st.RepoISO, + Prev: map[string]*collect.Smart{}, } for _, v := range st.Disks { in.Disks = append(in.Disks, v.Disk) diff --git a/internal/shares/external.go b/internal/shares/external.go index 34f6c9d..3f2c36b 100644 --- a/internal/shares/external.go +++ b/internal/shares/external.go @@ -7,7 +7,7 @@ import ( "strings" ) -// Other services on the host (e.g. RepoISO) publish their own shares through +// Other services on the host publish their own shares through // drop-in files. nasmon includes the SMB drop-ins into the smb.conf it // generates and keeps smbd / nfs-server running while any of them define a // share, even when nasmon itself has none. @@ -19,7 +19,7 @@ const ( // ExternalShare is a share defined outside nasmon (read-only in the UI). type ExternalShare struct { Kind string // smb | nfs - Source string // drop-in file name without extension, e.g. "repoiso" + Source string // drop-in file name without extension Name string // SMB share name (empty for NFS) Path string Clients string // NFS clients with options, as written in the file diff --git a/internal/web/external_test.go b/internal/web/external_test.go new file mode 100644 index 0000000..9f6a479 --- /dev/null +++ b/internal/web/external_test.go @@ -0,0 +1,32 @@ +package web + +import ( + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "nasmon/internal/shares" +) + +func TestSharesPageListsExternal(t *testing.T) { + srv, _, _ := newTestServer(t) + f := &shareRunner{} + withShareRunner(srv, f) + dir := t.TempDir() + smbDir, nfsDir := filepath.Join(dir, "smb.conf.d"), filepath.Join(dir, "exports.d") + _ = os.MkdirAll(smbDir, 0o755) + _ = os.MkdirAll(nfsDir, 0o755) + _ = os.WriteFile(filepath.Join(smbDir, "repoiso.conf"), []byte("[iso-pve]\n path = /tank/iso/export/pve\n"), 0o644) + _ = os.WriteFile(filepath.Join(nfsDir, "repoiso.exports"), []byte("\"/tank/iso/export/pve\" 10.10.0.0/24(ro)\n"), 0o644) + srv.shm.IncludeDir, srv.shm.ExportsDir, srv.shm.Exports = smbDir, nfsDir, filepath.Join(nfsDir, shares.DefaultExports) + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, httptest.NewRequest("GET", "/shares", nil)) + body := rec.Body.String() + for _, want := range []string{`id="external"`, "iso-pve", "/tank/iso/export/pve", "10.10.0.0/24(ro)", ">repoiso<"} { + if !strings.Contains(body, want) { + t.Errorf("shares page lacks %q", want) + } + } +} diff --git a/internal/web/handlers.go b/internal/web/handlers.go index 4cbeaf4..9eacca0 100644 --- a/internal/web/handlers.go +++ b/internal/web/handlers.go @@ -465,7 +465,6 @@ func formToSettings(r *http.Request, old config.Settings) config.Settings { c.SelfTests.ShortDays = num("st_short_days", old.SelfTests.ShortDays) c.SelfTests.LongDays = num("st_long_days", old.SelfTests.LongDays) c.SelfTests.Hour = num("st_hour", old.SelfTests.Hour) - c.Integrations.RepoISOURL = strings.TrimSpace(r.FormValue("repoiso_url")) return c } diff --git a/internal/web/integration_test.go b/internal/web/integration_test.go deleted file mode 100644 index e25ed7f..0000000 --- a/internal/web/integration_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package web - -import ( - "net/http/httptest" - "net/url" - "os" - "path/filepath" - "strings" - "testing" - - "nasmon/internal/collect" - "nasmon/internal/shares" -) - -func TestBrowserURL(t *testing.T) { - cases := []struct{ cfg, host, want string }{ - {"", "nas:8090", ""}, - {"http://127.0.0.1", "10.10.0.129:8090", "http://10.10.0.129"}, - {"http://localhost:8080/", "nas.lan:8090", "http://nas.lan:8080/"}, - {"https://repo.example.com", "10.0.0.1:8090", "https://repo.example.com"}, - {"http://127.0.0.1", "[fe80::1]:8090", "http://[fe80::1]"}, - } - for _, c := range cases { - if got := browserURL(c.cfg, c.host); got != c.want { - t.Errorf("browserURL(%q, %q) = %q, want %q", c.cfg, c.host, got, c.want) - } - } -} - -func TestRepoISOIntegrationUI(t *testing.T) { - srv, sc, _ := newTestServer(t) - // settings: save the URL, menu link appears - rec := postForm(srv, "/settings", url.Values{"smtp_port": {"587"}, "smtp_security": {"starttls"}, - "smart_interval": {"30"}, "repoiso_url": {"http://127.0.0.1/"}}) - if rec.Code != 200 || srv.cfg.Get().Integrations.RepoISOURL != "http://127.0.0.1" { - t.Fatalf("settings not saved: %d %q", rec.Code, srv.cfg.Get().Integrations.RepoISOURL) - } - rec = postForm(srv, "/settings", url.Values{"smtp_port": {"587"}, "smtp_security": {"starttls"}, - "smart_interval": {"30"}, "repoiso_url": {"ftp://x"}}) - if !strings.Contains(rec.Body.String(), "RepoISO URL must start") { - t.Errorf("bad URL accepted") - } - - st := sc.Status() - st.RepoISO = &collect.RepoISO{URL: "http://127.0.0.1", Reachable: true, WorkerAlive: true, Version: "0.1.3", - Images: 61, ImagesBytes: 350 << 30, Publications: 2, JobsFailed24h: 1, Exports: 1} - sc.SetStatus(st) - rec = httptest.NewRecorder() - req := httptest.NewRequest("GET", "/", nil) - req.Host = "10.10.0.129:8090" - srv.ServeHTTP(rec, req) - body := rec.Body.String() - for _, want := range []string{`href="http://10.10.0.129"`, "RepoISO ↗", `id="repoiso"`, "v0.1.3", "1 failed in 24 h"} { - if !strings.Contains(body, want) { - t.Errorf("dashboard lacks %q", want) - } - } - - st.RepoISO = &collect.RepoISO{URL: "http://127.0.0.1", Error: "connection refused"} - sc.SetStatus(st) - rec = httptest.NewRecorder() - srv.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil)) - if !strings.Contains(rec.Body.String(), "not responding") { - t.Errorf("unreachable RepoISO not shown") - } -} - -func TestSharesPageListsExternal(t *testing.T) { - srv, _, _ := newTestServer(t) - f := &shareRunner{} - withShareRunner(srv, f) - dir := t.TempDir() - smbDir, nfsDir := filepath.Join(dir, "smb.conf.d"), filepath.Join(dir, "exports.d") - _ = os.MkdirAll(smbDir, 0o755) - _ = os.MkdirAll(nfsDir, 0o755) - _ = os.WriteFile(filepath.Join(smbDir, "repoiso.conf"), []byte("[iso-pve]\n path = /tank/iso/export/pve\n"), 0o644) - _ = os.WriteFile(filepath.Join(nfsDir, "repoiso.exports"), []byte("\"/tank/iso/export/pve\" 10.10.0.0/24(ro)\n"), 0o644) - srv.shm.IncludeDir, srv.shm.ExportsDir, srv.shm.Exports = smbDir, nfsDir, filepath.Join(nfsDir, shares.DefaultExports) - rec := httptest.NewRecorder() - srv.ServeHTTP(rec, httptest.NewRequest("GET", "/shares", nil)) - body := rec.Body.String() - for _, want := range []string{`id="external"`, "iso-pve", "/tank/iso/export/pve", "10.10.0.0/24(ro)", "RepoISO"} { - if !strings.Contains(body, want) { - t.Errorf("shares page lacks %q", want) - } - } -} diff --git a/internal/web/server.go b/internal/web/server.go index 9a197d4..2671e69 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -8,9 +8,7 @@ import ( "io/fs" "log" "math" - "net" "net/http" - "net/url" "path/filepath" "strings" "sync" @@ -107,7 +105,6 @@ type pageData struct { Data any Flash string Error string - RepoISO string // browser URL of the RepoISO portal ("" = integration off) } // render writes a full page, or only the "content" block when ?partial=1 is @@ -115,7 +112,6 @@ type pageData struct { func (s *Server) render(w http.ResponseWriter, r *http.Request, name string, pd pageData) { pd.Page = name pd.Partial = r.URL.Query().Get("partial") == "1" - pd.RepoISO = browserURL(s.cfg.Get().Integrations.RepoISOURL, r.Host) if pd.Alerts == 0 { pd.Alerts = len(s.sch.Status().Alerts) } @@ -292,33 +288,3 @@ func sparkline(vals []float64, max float64, class string) template.HTML { ``, class, int(w), int(h), int(h), p, int(w), int(h), p)) } - -// browserURL turns the configured RepoISO address into one the user's browser -// can open: a loopback host is replaced by the host the browser used for nasmon. -func browserURL(cfg, reqHost string) string { - if cfg == "" { - return "" - } - u, err := url.Parse(cfg) - if err != nil || u.Host == "" { - return cfg - } - switch u.Hostname() { - case "127.0.0.1", "localhost", "::1": - host := reqHost - if h, _, err := net.SplitHostPort(reqHost); err == nil { - host = h - } - if host == "" { - return cfg - } - if strings.Contains(host, ":") { - host = "[" + host + "]" - } - if p := u.Port(); p != "" { - host += ":" + p - } - u.Host = host - } - return u.String() -} diff --git a/internal/web/templates/dashboard.html b/internal/web/templates/dashboard.html index 9407818..3080ec7 100644 --- a/internal/web/templates/dashboard.html +++ b/internal/web/templates/dashboard.html @@ -104,26 +104,6 @@ -{{with .S.RepoISO}} -
-

RepoISO {{if $.RepoISO}}open ↗{{end}} - {{if not .Reachable}}not responding - {{else if not .WorkerAlive}}worker stopped - {{else}}running{{end}} - {{if .Version}}v{{.Version}}{{end}}

- {{if not .Reachable}}
{{.URL}}/api/integration/status: {{.Error}}
{{else}} -
- - - - - - - -
Images{{.Images}}{{bytes .ImagesBytes}}{{if .ImagesPending}} · {{.ImagesPending}} being checked{{end}}{{if .ImagesError}} · {{.ImagesError}} with errors{{end}}
Publications{{.Publications}}{{if .PublicationsError}}{{.PublicationsError}} not mounted{{else}}HTTP repositories / file trees{{end}}
Jobs{{.JobsActive}}{{if .JobsActive}}active{{else}}idle{{end}}{{if .JobsFailed24h}} · {{.JobsFailed24h}} failed in 24 h{{end}}
Proxmox exports{{.Exports}}NFS / SMB shares — listed on Shares
Worker{{if .WorkerHeartbeat}}last heartbeat {{.WorkerHeartbeat}}{{else}}no heartbeat yet{{end}}
{{end}} -
-{{end}} -

Datasets

diff --git a/internal/web/templates/layout.html b/internal/web/templates/layout.html index 871efc6..d243504 100644 --- a/internal/web/templates/layout.html +++ b/internal/web/templates/layout.html @@ -16,7 +16,6 @@ SharesAlerts{{if .Alerts}} {{.Alerts}}{{end}}Settings - {{if .RepoISO}}RepoISO ↗{{end}} diff --git a/internal/web/templates/settings.html b/internal/web/templates/settings.html index b2ad58f..2898a02 100644 --- a/internal/web/templates/settings.html +++ b/internal/web/templates/settings.html @@ -67,17 +67,6 @@

Tests start only for disks that are awake. A long test on a large HDD takes many hours; disk stays usable.

-
-

Integrations

-
- -
-

RepoISO (ISO library, apt/dnf repositories, Proxmox ISO exports) on this host. nasmon polls - <url>/api/integration/status every minute, shows a dashboard card, alerts when the portal - or its worker is down, and adds a menu link. Use the local address (http://127.0.0.1) — the - status endpoint only answers on loopback. Empty disables the integration.

-
-
diff --git a/internal/web/templates/shares.html b/internal/web/templates/shares.html index a9ca464..6a0808c 100644 --- a/internal/web/templates/shares.html +++ b/internal/web/templates/shares.html @@ -152,7 +152,7 @@
{{range .External}} - + diff --git a/packaging/README.md b/packaging/README.md index 0036075..9394794 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -61,15 +61,3 @@ nasmon lists these shares read-only on /shares and keeps smbd / nfs-server enabled while any of them exist, even with no nasmon shares (previously it disabled the daemons and took such shares down). When SMB is switched off in nasmon, only nasmon's own shares are removed. - -## RepoISO integration - -Settings → Integrations → RepoISO URL (e.g. http://127.0.0.1). nasmon then -polls /api/integration/status every minute and - -* shows a RepoISO card on the dashboard (images, publications, jobs, Proxmox - exports, worker heartbeat) and a "RepoISO" menu link; -* alerts: crit when the portal does not respond, warn when its worker has no - heartbeat, when jobs failed in the last 24 h or publications are not mounted. - -The status endpoint answers only on loopback, so use the local address.
{{if eq .Kind "smb"}}SMB{{else}}NFS{{end}}{{if eq .Source "repoiso"}}{{if $.RepoISO}}RepoISO{{else}}RepoISO{{end}}{{else}}{{.Source}}{{end}}{{.Source}} {{if .Name}}\\{{$.Data.Host}}\{{.Name}}{{else}}—{{end}} {{.Path}} {{.Clients}}