Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42cfb43e2e |
@@ -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/<serial>/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).
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
`<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))
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
@@ -104,26 +104,6 @@
|
||||
</table></div>
|
||||
</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">
|
||||
<h2>Datasets</h2>
|
||||
<div class="tablewrap"><table>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<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="/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>
|
||||
<span class="refresh" id="refresh-state" title="auto-refresh"></span>
|
||||
</header>
|
||||
|
||||
@@ -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>
|
||||
</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"><url>/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">
|
||||
<button type="submit" name="action" value="save">Save</button>
|
||||
<button type="submit" name="action" value="test" class="secondary">Save & send test e-mail</button>
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
<tbody>
|
||||
{{range .External}}<tr>
|
||||
<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 small">{{.Path}}</td>
|
||||
<td class="mono small">{{.Clients}}</td>
|
||||
|
||||
@@ -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 <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.
|
||||
|
||||
Reference in New Issue
Block a user