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:
@@ -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).
|
||||||
|
|||||||
@@ -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
|
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 {
|
||||||
|
|||||||
@@ -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
|
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"`
|
||||||
NotifyResolved bool `json:"notify_resolved"`
|
NotifyResolved bool `json:"notify_resolved"`
|
||||||
RemindHours int `json:"remind_hours"`
|
RemindHours int `json:"remind_hours"`
|
||||||
SmartIntervalMin int `json:"smart_interval_min"`
|
SmartIntervalMin int `json:"smart_interval_min"`
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -426,8 +419,7 @@ func (s *Scheduler) evaluate() {
|
|||||||
Disks: nil, Pools: st.Pools, PoolsOK: st.PoolsErr == "",
|
Disks: nil, Pools: st.Pools, PoolsOK: st.PoolsErr == "",
|
||||||
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
"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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"><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">
|
<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 & send test e-mail</button>
|
<button type="submit" name="action" value="test" class="secondary">Save & send test e-mail</button>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user