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
140 lines
4.3 KiB
Go
140 lines
4.3 KiB
Go
package poller
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/v10networks/nmon/internal/snmp"
|
|
)
|
|
|
|
// Debug limits.
|
|
const (
|
|
// MaxDebugMinutes is the longest a device's SNMP debug recording may be switched on for.
|
|
MaxDebugMinutes = 60
|
|
// DebugCycles is how many recorded cycles a device keeps; the oldest is dropped first.
|
|
DebugCycles = 20
|
|
// debugRowCap bounds the per-OID rows of one recorded cycle (failures first), so the ring stays small.
|
|
debugRowCap = 500
|
|
)
|
|
|
|
// DebugState is one device's SNMP debug recording.
|
|
type DebugState struct {
|
|
Enabled bool `json:"enabled"`
|
|
Until *time.Time `json:"until,omitempty" doc:"When recording stops by itself."`
|
|
Target string `json:"target,omitempty" doc:"Address and port polled, as of the latest recorded cycle."`
|
|
SNMPVersion string `json:"snmp_version,omitempty"`
|
|
Profile string `json:"profile,omitempty"`
|
|
MaxCycles int `json:"max_cycles"`
|
|
Cycles []GroupTrace `json:"cycles" doc:"Recorded cycles, newest first. In memory only: gone on restart."`
|
|
}
|
|
|
|
type debugRing struct {
|
|
until time.Time
|
|
meta DebugState
|
|
cycles []GroupTrace // oldest first
|
|
}
|
|
|
|
// debugStore holds the recordings by device ID. It is in memory only, by design.
|
|
type debugStore struct {
|
|
mu sync.Mutex
|
|
m map[int64]*debugRing
|
|
}
|
|
|
|
func newDebugStore() *debugStore { return &debugStore{m: map[int64]*debugRing{}} }
|
|
|
|
// set switches recording on until the given time, dropping an earlier recording, or off when until is zero (the
|
|
// recording so far stays readable).
|
|
func (d *debugStore) set(id int64, until time.Time) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
r := d.m[id]
|
|
switch {
|
|
case until.IsZero() && r != nil:
|
|
r.until = time.Time{}
|
|
case !until.IsZero():
|
|
d.m[id] = &debugRing{until: until}
|
|
}
|
|
}
|
|
|
|
func (d *debugStore) active(id int64, now time.Time) bool {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
r := d.m[id]
|
|
return r != nil && now.Before(r.until)
|
|
}
|
|
|
|
func (d *debugStore) add(id int64, meta DebugState, c GroupTrace) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
r := d.m[id]
|
|
if r == nil {
|
|
return
|
|
}
|
|
r.meta = meta
|
|
r.cycles = append(r.cycles, c)
|
|
if len(r.cycles) > DebugCycles {
|
|
r.cycles = append([]GroupTrace(nil), r.cycles[len(r.cycles)-DebugCycles:]...)
|
|
}
|
|
}
|
|
|
|
func (d *debugStore) drop(id int64) {
|
|
d.mu.Lock()
|
|
delete(d.m, id)
|
|
d.mu.Unlock()
|
|
}
|
|
|
|
func (d *debugStore) snapshot(id int64, now time.Time) DebugState {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
out := DebugState{MaxCycles: DebugCycles, Cycles: []GroupTrace{}}
|
|
r := d.m[id]
|
|
if r == nil {
|
|
return out
|
|
}
|
|
out.Target, out.SNMPVersion, out.Profile = r.meta.Target, r.meta.SNMPVersion, r.meta.Profile
|
|
if now.Before(r.until) {
|
|
u := r.until
|
|
out.Enabled, out.Until = true, &u
|
|
}
|
|
for i := len(r.cycles) - 1; i >= 0; i-- {
|
|
out.Cycles = append(out.Cycles, r.cycles[i])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// SetDebug records every scheduled cycle of device id for the next minutes (0 turns it off, more than
|
|
// MaxDebugMinutes is capped) and returns the state. Recordings live in memory only.
|
|
func (p *Poller) SetDebug(id int64, minutes int) DebugState {
|
|
minutes = max(0, min(minutes, MaxDebugMinutes))
|
|
var until time.Time
|
|
if minutes > 0 {
|
|
until = p.clk.Now().Add(time.Duration(minutes) * time.Minute)
|
|
}
|
|
p.dbg.set(id, until)
|
|
return p.dbg.snapshot(id, p.clk.Now())
|
|
}
|
|
|
|
// DebugState returns device id's recording state and recorded cycles.
|
|
func (p *Poller) DebugState(id int64) DebugState { return p.dbg.snapshot(id, p.clk.Now()) }
|
|
|
|
// debugRecorder returns a recorder when device debug is on, else nil.
|
|
func (l *devLoop) debugRecorder(t *target) *traceRec {
|
|
if !l.p.dbg.active(l.id, l.p.clk.Now()) {
|
|
return nil
|
|
}
|
|
return newTraceRec(l.p.clk, t.cred)
|
|
}
|
|
|
|
// recordDebug files a finished fetch in the device's ring and logs a one-line summary. rec nil: not recording.
|
|
func (l *devLoop) recordDebug(rec *traceRec, t *target, g *group, vbs []snmp.Varbind, got []bool, err error) {
|
|
if rec == nil {
|
|
return
|
|
}
|
|
gt := rec.groupTrace(g, vbs, got, err, debugRowCap)
|
|
meta := DebugState{Target: t.addr.String(), SNMPVersion: trimV(t.cred.Version), Profile: t.profile}
|
|
l.p.dbg.add(l.id, meta, gt)
|
|
l.p.log.Info("snmp debug cycle", "device", l.slug, "group_s", gt.IntervalSeconds, "requests", len(gt.Requests),
|
|
"requested", gt.Requested, "returned", gt.Returned, "missing", gt.Missing, "skipped", gt.Skipped,
|
|
"duration_ms", gt.DurationMS, "err", gt.Error)
|
|
}
|