Make nmon easier to install and run without editing YAML by hand: - Settings: point-and-click forms over config.yaml (GET/PUT /api/v1/settings/form); the YAML editor moves to /settings/yaml. - First install: with no sign-in configured nmon starts in install mode, serves only the setup wizard (sign-in, basics, devices, alerting) and closes it for good once sign-in is saved. A self-signed certificate is generated when no TLS source is given. poller.interval becomes a config setting. - Reverse proxies: opt-in --http-listen plain-HTTP listener beside HTTPS; sign-in stays required. - Webhooks: Generic, Teams Adaptive Card, Slack and Discord presets, template preview and test send. - Help: the operations, coverage and profile docs served at /help. - Polling: per-device "Poll now", a dry-run test poll returning an SNMP trace, and time-limited in-memory SNMP debug recording, in the UI and as `nmon device poll`. - Ansible: deploys into install mode when no sign-in is set, and no longer overwrites the config.yaml the wizard wrote. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017TuKgyKve2XLFtrrXWMi2w
146 lines
6.0 KiB
Go
146 lines
6.0 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/v10networks/nmon/internal/auth"
|
|
"github.com/v10networks/nmon/internal/config"
|
|
"github.com/v10networks/nmon/internal/diskguard"
|
|
"github.com/v10networks/nmon/internal/notify"
|
|
"github.com/v10networks/nmon/internal/proxmox"
|
|
"github.com/v10networks/nmon/internal/store"
|
|
)
|
|
|
|
// durString renders a duration for the form; zero is "unset" and shows as empty.
|
|
func durString(d time.Duration) string {
|
|
if d == 0 {
|
|
return ""
|
|
}
|
|
s := config.FormatDuration(d)
|
|
// Go prints 15m as "15m0s" and 1h as "1h0m0s"; the form shows the short spelling.
|
|
if strings.HasSuffix(s, "m0s") {
|
|
s = strings.TrimSuffix(s, "0s")
|
|
}
|
|
if strings.HasSuffix(s, "h0m") {
|
|
s = strings.TrimSuffix(s, "0m")
|
|
}
|
|
return s
|
|
}
|
|
|
|
// formView converts the configuration into the typed form.
|
|
func formView(c config.Config) (SettingsFormView, error) {
|
|
recv, err := receiverViews(c.Alerting)
|
|
if err != nil {
|
|
return SettingsFormView{}, err
|
|
}
|
|
v := SettingsFormView{
|
|
Retention: RetentionForm{Hot: durString(c.Retention.Hot.D()), FiveMinute: durString(c.Retention.FiveMinute.D()),
|
|
Long: durString(c.Retention.Long.D())},
|
|
Timezone: c.Timezone,
|
|
RetiredPurgeAfter: durString(c.RetiredPurgeAfter.D()),
|
|
PollerInterval: durString(c.Poller.Interval.D()),
|
|
Sites: siteForms(c.Sites),
|
|
Alerting: AlertingForm{OutboundAllow: nonNil(c.Alerting.OutboundAllow),
|
|
EvaluationInterval: durString(c.Alerting.EvaluationInterval.D())},
|
|
Receivers: recv,
|
|
Auth: authForm(c.Auth),
|
|
Backup: BackupForm{At: c.Backup.At, Dir: c.Backup.Dir, Keep: c.Backup.Keep},
|
|
Disk: DiskForm{AlertUsed: c.Disk.AlertUsed, AdmitUsed: c.Disk.AdmitUsed, ReserveFree: c.Disk.ReserveFree},
|
|
L2: L2Form{Interval: durString(c.L2.Interval.D()), Keep: durString(c.L2.Keep.D()), EdgeMaxMACs: c.L2.EdgeMaxMACs,
|
|
Concurrency: c.L2.Concurrency, DeviceTimeout: durString(c.L2.DeviceTimeout.D())},
|
|
OUI: OUIForm{Fetch: c.OUI.FetchEnabled(), Refresh: durString(c.OUI.Refresh.D())},
|
|
Proxmox: proxmoxForm(c.Proxmox),
|
|
Checks: ChecksForm{Workers: c.Checks.Workers, Timeout: durString(c.Checks.Timeout), Tick: durString(c.Checks.Tick)},
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func siteForms(sites map[string]config.Site) []SiteForm {
|
|
out := make([]SiteForm, 0, len(sites))
|
|
for name, s := range sites {
|
|
out = append(out, SiteForm{Name: name, Timezone: s.Timezone})
|
|
}
|
|
slices.SortFunc(out, func(a, b SiteForm) int { return compareStrings(a.Name, b.Name) })
|
|
return out
|
|
}
|
|
|
|
func compareStrings(a, b string) int {
|
|
switch {
|
|
case a < b:
|
|
return -1
|
|
case a > b:
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func receiverViews(a config.Alerting) ([]ReceiverView, error) {
|
|
out := make([]ReceiverView, 0, len(a.Receivers))
|
|
for i := range a.Receivers {
|
|
var r notify.Receiver
|
|
if err := a.Receivers[i].Decode(&r); err != nil {
|
|
return nil, fmt.Errorf("alerting.receivers[%d]: %w", i, err)
|
|
}
|
|
headers := make(map[string]string, len(r.Headers)) // names only: the values may be credentials
|
|
for k := range r.Headers {
|
|
headers[k] = ""
|
|
}
|
|
out = append(out, ReceiverView{Name: r.Name, Kind: r.Kind, URL: r.URL, Headers: headers, Template: r.Template,
|
|
SMTP: SMTPView{Host: r.SMTP.Host, Port: r.SMTP.Port, From: r.SMTP.From, To: nonNil(r.SMTP.To),
|
|
Username: r.SMTP.Username, LoginStored: r.SMTP.Password != "", StartTLS: r.SMTP.StartTLS}})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func authForm(a config.Auth) AuthForm {
|
|
f := AuthForm{BreakGlassFile: a.BreakGlassFile, BreakGlassUser: a.BreakGlassUser, TrustedProxies: nonNil(a.TrustedProxies),
|
|
Session: SessionForm{Idle: durString(a.Session.Idle.D()), Absolute: durString(a.Session.Absolute.D())}}
|
|
if o := a.OIDC; o != nil {
|
|
f.OIDC = &OIDCForm{Issuer: o.Issuer, ClientID: o.ClientID, ClientSecretName: o.ClientSecretName, RedirectURL: o.RedirectURL,
|
|
GroupsClaim: o.GroupsClaim, AdminGroup: o.AdminGroup}
|
|
}
|
|
return f
|
|
}
|
|
|
|
func proxmoxForm(p proxmox.Config) ProxmoxForm {
|
|
clusters := make([]ProxmoxClusterForm, 0, len(p.Clusters))
|
|
for _, c := range p.Clusters {
|
|
clusters = append(clusters, ProxmoxClusterForm{Name: c.Name, URL: c.URL, TokenID: c.TokenID, SecretName: c.SecretName,
|
|
CAFile: c.CAFile, InsecureSkipVerify: c.InsecureSkipVerify})
|
|
}
|
|
return ProxmoxForm{Clusters: clusters, Interval: durString(p.Interval), Timeout: durString(p.Timeout),
|
|
RetireGrace: durString(p.RetireGrace), Workers: p.Workers}
|
|
}
|
|
|
|
// formDefaults is what each unset value means; the UI shows it as a placeholder.
|
|
func formDefaults() SettingsFormView {
|
|
d := config.Default()
|
|
return SettingsFormView{
|
|
Retention: RetentionForm{Hot: durString(d.Retention.Hot.D()), FiveMinute: durString(d.Retention.FiveMinute.D()),
|
|
Long: durString(d.Retention.Long.D())},
|
|
Timezone: d.Timezone,
|
|
RetiredPurgeAfter: durString(config.DefaultRetiredPurgeAfter),
|
|
PollerInterval: durString(store.DefaultPollInterval),
|
|
Sites: []SiteForm{},
|
|
Alerting: AlertingForm{OutboundAllow: []string{}, EvaluationInterval: "30s"},
|
|
Receivers: []ReceiverView{},
|
|
Auth: AuthForm{BreakGlassUser: "breakglass", TrustedProxies: []string{}, Session: SessionForm{
|
|
Idle: durString(auth.DefaultIdleTimeout), Absolute: durString(auth.DefaultAbsoluteTimeout)}},
|
|
Backup: BackupForm{Keep: config.DefaultBackupKeep},
|
|
Disk: DiskForm{AlertUsed: diskguard.DefaultAlertUsed, AdmitUsed: diskguard.DefaultAdmitUsed,
|
|
ReserveFree: diskguard.DefaultReserveFree},
|
|
L2: L2Form{Interval: durString(config.DefaultL2Interval), Keep: durString(config.DefaultL2Keep),
|
|
EdgeMaxMACs: config.DefaultL2EdgeMaxMACs, Concurrency: config.DefaultL2Concurrency,
|
|
DeviceTimeout: durString(config.DefaultL2DeviceTimeout)},
|
|
OUI: OUIForm{Fetch: true, Refresh: durString(config.DefaultOUIRefresh)},
|
|
Proxmox: ProxmoxForm{Clusters: []ProxmoxClusterForm{}, Interval: durString(proxmox.DefaultInterval),
|
|
Timeout: durString(proxmox.DefaultTimeout), RetireGrace: durString(proxmox.DefaultRetireGrace),
|
|
Workers: proxmox.DefaultWorkers},
|
|
// The checks runner's defaults are literals in internal/checks (Workers 32, Timeout 10s, Tick 5s).
|
|
Checks: ChecksForm{Workers: 32, Timeout: "10s", Tick: "5s"},
|
|
}
|
|
}
|