Remove the RepoISO integration; keep generic support for shares of other services

RepoISO now manages the NAS itself, so nasmon no longer polls it (settings field, dashboard card,
menu link and repoiso:* alerts are gone). Drop-in SMB includes, read-only listing and keeping
smbd/nfs-server running for other software's shares stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 19:05:16 +03:00
co-authored by Claude Opus 5.5
parent 30dbb00dd1
commit 4f4ae82e68
17 changed files with 47 additions and 348 deletions
+3 -3
View File
@@ -11,9 +11,9 @@ share management from the UI.
* Service: `systemctl status nasmon` / `journalctl -u nasmon -f` * Service: `systemctl status nasmon` / `journalctl -u nasmon -f`
* JSON API: `/api/status`, `/api/history?hours=24`, `/api/disks/<serial>/history?days=30` * JSON API: `/api/status`, `/api/history?hours=24`, `/api/disks/<serial>/history?days=30`
Integrates with RepoISO on the same host: shares of other services Shares of other software on the same host (`/etc/samba/smb.conf.d/*.conf`,
(`/etc/samba/smb.conf.d`, `/etc/exports.d`) are included and kept running, `/etc/exports.d/*.exports`) are included into the generated config, listed
and RepoISO health is shown on the dashboard and alerted on. read-only and keep smbd / nfs-server running.
No authentication — designed for a LAN-only NAS (the `/shares` page can 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). create SMB/NFS users and exports, so keep it off the public internet).
-35
View File
@@ -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))
}
}
-28
View File
@@ -40,8 +40,6 @@ type Input struct {
HasCPU bool HasCPU bool
RootFS collect.FSUsage RootFS collect.FSUsage
RootOK bool 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 // Frozen lists key prefixes whose open alerts must be left untouched
// because no fresh data was available for them in this cycle. // because no fresh data was available for them in this cycle.
Frozen []string 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 }) sort.Slice(ev, func(i, j int) bool { return ev[i].Key < ev[j].Key })
return ev return ev
} }
func orNever(s string) string {
if s == "" {
return "never"
}
return s
}
func humanBytes(b uint64) string { func humanBytes(b uint64) string {
const unit = 1024 const unit = 1024
if b < unit { if b < unit {
-54
View File
@@ -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 <url>/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
}
-29
View File
@@ -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)
}
}
-13
View File
@@ -40,13 +40,6 @@ type SelfTests struct {
Hour int `json:"hour"` // local hour of day when tests may be started 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 { type Settings struct {
SMTP SMTP `json:"smtp"` SMTP SMTP `json:"smtp"`
AlertsEnabled bool `json:"alerts_enabled"` AlertsEnabled bool `json:"alerts_enabled"`
@@ -56,7 +49,6 @@ type Settings struct {
SmartSkipStandby bool `json:"smart_skip_standby"` SmartSkipStandby bool `json:"smart_skip_standby"`
Thresholds Thresholds `json:"thresholds"` Thresholds Thresholds `json:"thresholds"`
SelfTests SelfTests `json:"self_tests"` SelfTests SelfTests `json:"self_tests"`
Integrations Integrations `json:"integrations"`
} }
func Defaults() Settings { func Defaults() Settings {
@@ -110,11 +102,6 @@ func (s *Settings) Validate() error {
if s.SelfTests.ShortDays < 0 || s.SelfTests.LongDays < 0 { if s.SelfTests.ShortDays < 0 || s.SelfTests.LongDays < 0 {
return errors.New("self-test periods must be >= 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 return nil
} }
-8
View File
@@ -54,7 +54,6 @@ type Status struct {
NextSmartCheck time.Time `json:"next_smart_check"` NextSmartCheck time.Time `json:"next_smart_check"`
SmartRunning bool `json:"smart_running"` SmartRunning bool `json:"smart_running"`
LastMailErr string `json:"last_mail_err,omitempty"` LastMailErr string `json:"last_mail_err,omitempty"`
RepoISO *collect.RepoISO `json:"repoiso,omitempty"`
} }
type Scheduler struct { type Scheduler struct {
@@ -244,11 +243,6 @@ func (s *Scheduler) collectStorage() {
root, _ := collect.ReadFS("/") root, _ := collect.ReadFS("/")
alerts, _ := s.st.ActiveAlerts() alerts, _ := s.st.ActiveAlerts()
services := collect.ReadShareServices() services := collect.ReadShareServices()
var repoiso *collect.RepoISO
if url := s.cfg.Get().Integrations.RepoISOURL; url != "" {
r := collect.FetchRepoISO(url)
repoiso = &r
}
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@@ -270,7 +264,6 @@ func (s *Scheduler) collectStorage() {
s.status.RootFS = root s.status.RootFS = root
s.status.Alerts = alerts s.status.Alerts = alerts
s.status.Services = services s.status.Services = services
s.status.RepoISO = repoiso
} }
// buildViews merges lsblk disks with cached SMART data; caller holds s.mu. // buildViews merges lsblk disks with cached SMART data; caller holds s.mu.
@@ -427,7 +420,6 @@ func (s *Scheduler) evaluate() {
CPUTemp: st.CPUTemp, HasCPU: st.HasCPUTemp, CPUTemp: st.CPUTemp, HasCPU: st.HasCPUTemp,
RootFS: st.RootFS, RootOK: st.RootFS.Total > 0, RootFS: st.RootFS, RootOK: st.RootFS.Total > 0,
Prev: map[string]*collect.Smart{}, Prev: map[string]*collect.Smart{},
RepoISO: st.RepoISO,
} }
for _, v := range st.Disks { for _, v := range st.Disks {
in.Disks = append(in.Disks, v.Disk) in.Disks = append(in.Disks, v.Disk)
+2 -2
View File
@@ -7,7 +7,7 @@ import (
"strings" "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 // 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 // generates and keeps smbd / nfs-server running while any of them define a
// share, even when nasmon itself has none. // share, even when nasmon itself has none.
@@ -19,7 +19,7 @@ const (
// ExternalShare is a share defined outside nasmon (read-only in the UI). // ExternalShare is a share defined outside nasmon (read-only in the UI).
type ExternalShare struct { type ExternalShare struct {
Kind string // smb | nfs 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) Name string // SMB share name (empty for NFS)
Path string Path string
Clients string // NFS clients with options, as written in the file Clients string // NFS clients with options, as written in the file
+32
View File
@@ -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)
}
}
}
-1
View File
@@ -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.ShortDays = num("st_short_days", old.SelfTests.ShortDays)
c.SelfTests.LongDays = num("st_long_days", old.SelfTests.LongDays) c.SelfTests.LongDays = num("st_long_days", old.SelfTests.LongDays)
c.SelfTests.Hour = num("st_hour", old.SelfTests.Hour) c.SelfTests.Hour = num("st_hour", old.SelfTests.Hour)
c.Integrations.RepoISOURL = strings.TrimSpace(r.FormValue("repoiso_url"))
return c return c
} }
-87
View File
@@ -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)
}
}
}
-34
View File
@@ -8,9 +8,7 @@ import (
"io/fs" "io/fs"
"log" "log"
"math" "math"
"net"
"net/http" "net/http"
"net/url"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
@@ -107,7 +105,6 @@ type pageData struct {
Data any Data any
Flash string Flash string
Error 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 // 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) { func (s *Server) render(w http.ResponseWriter, r *http.Request, name string, pd pageData) {
pd.Page = name pd.Page = name
pd.Partial = r.URL.Query().Get("partial") == "1" pd.Partial = r.URL.Query().Get("partial") == "1"
pd.RepoISO = browserURL(s.cfg.Get().Integrations.RepoISOURL, r.Host)
if pd.Alerts == 0 { if pd.Alerts == 0 {
pd.Alerts = len(s.sch.Status().Alerts) pd.Alerts = len(s.sch.Status().Alerts)
} }
@@ -292,33 +288,3 @@ func sparkline(vals []float64, max float64, class string) template.HTML {
`<svg class="spark %s" viewBox="0 0 %d %d" preserveAspectRatio="none"><polygon points="0,%d %s %d,%d"/><polyline points="%s"/></svg>`, `<svg class="spark %s" viewBox="0 0 %d %d" preserveAspectRatio="none"><polygon points="0,%d %s %d,%d"/><polyline points="%s"/></svg>`,
class, int(w), int(h), int(h), p, int(w), int(h), p)) 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()
}
-20
View File
@@ -104,26 +104,6 @@
</table></div> </table></div>
</section> </section>
{{with .S.RepoISO}}
<section class="card" id="repoiso">
<h2>RepoISO {{if $.RepoISO}}<a class="muted small" href="{{$.RepoISO}}" target="_blank" rel="noopener">open ↗</a>{{end}}
{{if not .Reachable}}<span class="badge crit">not responding</span>
{{else if not .WorkerAlive}}<span class="badge warn">worker stopped</span>
{{else}}<span class="badge ok">running</span>{{end}}
{{if .Version}}<span class="muted small">v{{.Version}}</span>{{end}}</h2>
{{if not .Reachable}}<div class="flash crit">{{.URL}}/api/integration/status: {{.Error}}</div>{{else}}
<div class="tablewrap"><table class="compact">
<tbody>
<tr><td class="muted">Images</td><td class="num">{{.Images}}</td><td>{{bytes .ImagesBytes}}{{if .ImagesPending}} · <span class="muted">{{.ImagesPending}} being checked</span>{{end}}{{if .ImagesError}} · <span class="crit">{{.ImagesError}} with errors</span>{{end}}</td></tr>
<tr><td class="muted">Publications</td><td class="num">{{.Publications}}</td><td>{{if .PublicationsError}}<span class="badge warn">{{.PublicationsError}} not mounted</span>{{else}}<span class="muted">HTTP repositories / file trees</span>{{end}}</td></tr>
<tr><td class="muted">Jobs</td><td class="num">{{.JobsActive}}</td><td>{{if .JobsActive}}active{{else}}<span class="muted">idle</span>{{end}}{{if .JobsFailed24h}} · <span class="badge warn">{{.JobsFailed24h}} failed in 24 h</span>{{end}}</td></tr>
<tr><td class="muted">Proxmox exports</td><td class="num">{{.Exports}}</td><td class="muted">NFS / SMB shares — listed on <a href="/shares#external">Shares</a></td></tr>
<tr><td class="muted">Worker</td><td></td><td class="small">{{if .WorkerHeartbeat}}last heartbeat {{.WorkerHeartbeat}}{{else}}no heartbeat yet{{end}}</td></tr>
</tbody>
</table></div>{{end}}
</section>
{{end}}
<section class="card"> <section class="card">
<h2>Datasets</h2> <h2>Datasets</h2>
<div class="tablewrap"><table> <div class="tablewrap"><table>
-1
View File
@@ -16,7 +16,6 @@
<a href="/shares" {{if eq .Page "shares"}}class="on"{{end}}>Shares</a> <a href="/shares" {{if eq .Page "shares"}}class="on"{{end}}>Shares</a>
<a href="/alerts" {{if eq .Page "alerts"}}class="on"{{end}}>Alerts{{if .Alerts}} <span class="badge crit">{{.Alerts}}</span>{{end}}</a> <a href="/alerts" {{if eq .Page "alerts"}}class="on"{{end}}>Alerts{{if .Alerts}} <span class="badge crit">{{.Alerts}}</span>{{end}}</a>
<a href="/settings" {{if eq .Page "settings"}}class="on"{{end}}>Settings</a> <a href="/settings" {{if eq .Page "settings"}}class="on"{{end}}>Settings</a>
{{if .RepoISO}}<a href="{{.RepoISO}}" target="_blank" rel="noopener" title="ISO library, repositories, Proxmox exports">RepoISO ↗</a>{{end}}
</nav> </nav>
<span class="refresh" id="refresh-state" title="auto-refresh"></span> <span class="refresh" id="refresh-state" title="auto-refresh"></span>
</header> </header>
-11
View File
@@ -67,17 +67,6 @@
<p class="muted small">Tests start only for disks that are awake. A long test on a large HDD takes many hours; disk stays usable.</p> <p class="muted small">Tests start only for disks that are awake. A long test on a large HDD takes many hours; disk stays usable.</p>
</section> </section>
<section class="card">
<h2>Integrations</h2>
<div class="form-grid">
<label class="wide">RepoISO URL <input name="repoiso_url" value="{{$c.Integrations.RepoISOURL}}" placeholder="http://127.0.0.1"></label>
</div>
<p class="muted small">RepoISO (ISO library, apt/dnf repositories, Proxmox ISO exports) on this host. nasmon polls
<span class="mono">&lt;url&gt;/api/integration/status</span> every minute, shows a dashboard card, alerts when the portal
or its worker is down, and adds a menu link. Use the local address (<span class="mono">http://127.0.0.1</span>) — the
status endpoint only answers on loopback. Empty disables the integration.</p>
</section>
<div class="actions"> <div class="actions">
<button type="submit" name="action" value="save">Save</button> <button type="submit" name="action" value="save">Save</button>
<button type="submit" name="action" value="test" class="secondary">Save &amp; send test e-mail</button> <button type="submit" name="action" value="test" class="secondary">Save &amp; send test e-mail</button>
+1 -1
View File
@@ -152,7 +152,7 @@
<tbody> <tbody>
{{range .External}}<tr> {{range .External}}<tr>
<td><span class="badge">{{if eq .Kind "smb"}}SMB{{else}}NFS{{end}}</span></td> <td><span class="badge">{{if eq .Kind "smb"}}SMB{{else}}NFS{{end}}</span></td>
<td>{{if eq .Source "repoiso"}}{{if $.RepoISO}}<a href="{{$.RepoISO}}/proxmox" target="_blank" rel="noopener">RepoISO</a>{{else}}RepoISO{{end}}{{else}}{{.Source}}{{end}}</td> <td class="mono">{{.Source}}</td>
<td class="mono">{{if .Name}}\\{{$.Data.Host}}\{{.Name}}{{else}}—{{end}}</td> <td class="mono">{{if .Name}}\\{{$.Data.Host}}\{{.Name}}{{else}}—{{end}}</td>
<td class="mono small">{{.Path}}</td> <td class="mono small">{{.Path}}</td>
<td class="mono small">{{.Clients}}</td> <td class="mono small">{{.Clients}}</td>
-12
View File
@@ -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 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 disabled the daemons and took such shares down). When SMB is switched off in
nasmon, only nasmon's own shares are removed. nasmon, only nasmon's own shares are removed.
## RepoISO integration
Settings → Integrations → RepoISO URL (e.g. http://127.0.0.1). nasmon then
polls <url>/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.