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
126 lines
5.3 KiB
Go
126 lines
5.3 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
|
|
"github.com/v10networks/nmon/internal/notify"
|
|
)
|
|
|
|
// NotifyTester sends one immediate test notification through the running outbound policy. *notify.Queue fits.
|
|
type NotifyTester interface {
|
|
SendTest(ctx context.Context, r notify.Receiver, state string) (notify.TestResult, error)
|
|
}
|
|
|
|
// NotifyRequest is a receiver as the settings form holds it plus the sample state to render.
|
|
type NotifyRequest struct {
|
|
Receiver ReceiverForm `json:"receiver" doc:"The receiver as edited. Blank credentials resolve to the stored receiver of the same name."`
|
|
State string `json:"state,omitempty" enum:"firing,resolved,flapping,stale,suppressed_summary" doc:"Sample state; default firing."`
|
|
}
|
|
|
|
// NotifyPreview is a rendered sample payload, or why it could not be rendered.
|
|
type NotifyPreview struct {
|
|
Output string `json:"output" doc:"The payload that would be sent, redacted. Empty on error."`
|
|
Error string `json:"error" doc:"Template, JSON or receiver validation problem. Empty on success."`
|
|
}
|
|
|
|
// NotifyTestResult is the outcome of one test send; the receiver's own failures are reported here with a 200.
|
|
type NotifyTestResult struct {
|
|
Payload string `json:"payload" doc:"What was sent, redacted."`
|
|
StatusCode int `json:"status_code" doc:"HTTP status of a webhook; 0 for SMTP or when no response arrived."`
|
|
ResponseBody string `json:"response_body" doc:"First 4 KB of the response, redacted."`
|
|
DurationMS int64 `json:"duration_ms"`
|
|
Error string `json:"error" doc:"Why the send failed. Empty on success."`
|
|
OK bool `json:"ok"`
|
|
}
|
|
|
|
// NotifyPreset is a built-in webhook template.
|
|
type NotifyPreset struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Template string `json:"template"`
|
|
URLHint string `json:"url_hint" doc:"Shape of the service's webhook URL, for a placeholder."`
|
|
ContentType string `json:"content_type"`
|
|
}
|
|
|
|
func (s *server) registerNotify(api huma.API) {
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "list-notify-presets", Method: http.MethodGet, Path: "/api/v1/notify/presets", Tags: []string{"notify"},
|
|
Summary: "Built-in webhook templates", Description: "Teams (Workflows), Slack, Discord and generic JSON.",
|
|
}, s.listNotifyPresets)
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "preview-notify", Method: http.MethodPost, Path: "/api/v1/notify/preview", Tags: []string{"notify"},
|
|
Summary: "Render a receiver's template", Description: "Admin only. Renders the template against a sample " +
|
|
"notification with the delivery code path. Nothing is sent or stored.",
|
|
}, s.previewNotify)
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "test-notify", Method: http.MethodPost, Path: "/api/v1/notify/test", Tags: []string{"notify"},
|
|
Summary: "Send one test notification",
|
|
Description: "Admin only. Sends a sample notification to the receiver once, now, through the outbound allow-list, " +
|
|
"without the retry queue. Only an audit row is stored. At most two run at a time.",
|
|
Errors: []int{http.StatusTooManyRequests, http.StatusServiceUnavailable},
|
|
}, s.testNotify)
|
|
}
|
|
|
|
func (s *server) listNotifyPresets(context.Context, *struct{}) (*struct{ Body []NotifyPreset }, error) {
|
|
ps := notify.Presets()
|
|
out := make([]NotifyPreset, len(ps))
|
|
for i, p := range ps {
|
|
out[i] = NotifyPreset(p)
|
|
}
|
|
return &struct{ Body []NotifyPreset }{out}, nil
|
|
}
|
|
|
|
// stateOrDefault is the requested sample state, firing when unset.
|
|
func stateOrDefault(st string) string {
|
|
if st == "" {
|
|
return notify.StateFiring
|
|
}
|
|
return st
|
|
}
|
|
|
|
// resolveReceiver builds the receiver to use from the form, filling blank credentials from the stored receiver
|
|
// of the same name, exactly like PUT /settings/form.
|
|
func (s *server) resolveReceiver(f ReceiverForm) notify.Receiver {
|
|
var old notify.Receiver
|
|
if a, ok := s.Settings.(SettingsConfigAdmin); ok {
|
|
cfg, _ := a.CurrentConfig()
|
|
old = storedReceivers(cfg.Alerting)[strings.TrimSpace(f.Name)]
|
|
}
|
|
return receiverFrom(f, old)
|
|
}
|
|
|
|
func (s *server) previewNotify(_ context.Context, in *struct{ Body NotifyRequest }) (*struct{ Body NotifyPreview }, error) {
|
|
r := receiverFrom(in.Body.Receiver, notify.Receiver{})
|
|
out, err := notify.Preview(r, stateOrDefault(in.Body.State), s.Clock.Now())
|
|
p := NotifyPreview{Output: out}
|
|
if err != nil {
|
|
p.Error = err.Error()
|
|
}
|
|
return &struct{ Body NotifyPreview }{p}, nil
|
|
}
|
|
|
|
func (s *server) testNotify(ctx context.Context, in *struct{ Body NotifyRequest }) (*struct{ Body NotifyTestResult }, error) {
|
|
if s.Notify == nil {
|
|
return nil, errNotConfigured("notifications")
|
|
}
|
|
r, state := s.resolveReceiver(in.Body.Receiver), stateOrDefault(in.Body.State)
|
|
res, err := s.Notify.SendTest(ctx, r, state)
|
|
if errors.Is(err, notify.ErrTestBusy) {
|
|
return nil, huma.Error429TooManyRequests("another test send is in progress; try again in a moment")
|
|
}
|
|
if err != nil {
|
|
return nil, huma.Error422UnprocessableEntity(err.Error())
|
|
}
|
|
detail := map[string]any{"kind": r.Kind, "state": state, "ok": res.OK, "status_code": res.StatusCode}
|
|
if aerr := s.Store.AppendAudit(ctx, actorOf(ctx), "notify.test", "receiver:"+r.Name, nil, detail); aerr != nil {
|
|
s.Logger.Warn("audit notify.test", "err", aerr)
|
|
}
|
|
return &struct{ Body NotifyTestResult }{NotifyTestResult(res)}, nil
|
|
}
|