interfaces.alert_down / groups.alert_down (migration 9); interface_down rule OptIn; non-opted ports' open alerts close silently as retired. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
156 lines
5.5 KiB
Go
156 lines
5.5 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
|
|
"github.com/v10networks/nmon/internal/store"
|
|
)
|
|
|
|
// AlertDownState says whether an interface alerts when it goes down. Own is the port's flag; Effective adds
|
|
// membership of a group with the flag on, and Via names the source ("port", or "group:<name>").
|
|
type AlertDownState struct {
|
|
AlertDown bool `json:"alert_down" doc:"The port's own opt-in for interface_down alerts."`
|
|
AlertDownEffective bool `json:"alert_down_effective" doc:"The port alerts when down: own flag, or a group it is in now."`
|
|
AlertDownVia string `json:"alert_down_via" doc:"Why it is effective: port, or group:<name>. Empty when it is not."`
|
|
}
|
|
|
|
func alertDownState(own bool, via string) AlertDownState {
|
|
return AlertDownState{AlertDown: own, AlertDownEffective: via != "", AlertDownVia: via}
|
|
}
|
|
|
|
// InterfaceAlertBody is the body of PUT /interfaces/{id}.
|
|
type InterfaceAlertBody struct {
|
|
AlertDown bool `json:"alert_down" doc:"Opt the port in (true) or out (false) of interface_down alerts."`
|
|
}
|
|
|
|
// DeviceAlertDownBody is the body of POST /devices/{slug}/interfaces/alert-down.
|
|
type DeviceAlertDownBody struct {
|
|
On bool `json:"on" doc:"Opt the ports in (true) or out (false)."`
|
|
Ifaces []string `json:"ifaces,omitempty" doc:"Interface ids of this device. Omitted means every active port of the device."`
|
|
}
|
|
|
|
// AlertDownResult reports how many ports a bulk change touched.
|
|
type AlertDownResult struct {
|
|
Changed int `json:"changed"`
|
|
}
|
|
|
|
func (s *server) registerAlertDown(api huma.API) {
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "set-interface-alert-down", Method: http.MethodPut, Path: "/api/v1/interfaces/{id}", Tags: []string{"interfaces"},
|
|
Summary: "Opt one interface in or out of interface_down alerts",
|
|
Description: "Admin only. interface_down alerts are opt-in: a down port raises nothing until its own flag is on " +
|
|
"or a group it belongs to has alert_down on.",
|
|
Errors: []int{http.StatusNotFound},
|
|
}, s.setInterfaceAlertDown)
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "set-device-alert-down", Method: http.MethodPost, Path: "/api/v1/devices/{slug}/interfaces/alert-down",
|
|
Tags: []string{"devices"},
|
|
Summary: "Opt a device's interfaces in or out of interface_down alerts",
|
|
Description: "Admin only. Without ifaces the change applies to every active port of the device. " +
|
|
"Turning a port off closes its open interface_down alert silently.",
|
|
Errors: []int{http.StatusNotFound, http.StatusUnprocessableEntity},
|
|
}, s.setDeviceAlertDown)
|
|
}
|
|
|
|
func (s *server) alertDownChanged() {
|
|
if s.OnAlertDownChange != nil {
|
|
s.OnAlertDownChange()
|
|
}
|
|
}
|
|
|
|
func (s *server) setInterfaceAlertDown(ctx context.Context, in *struct {
|
|
ID string `path:"id" doc:"Interface ULID."`
|
|
Body InterfaceAlertBody
|
|
}) (*struct{ Body InterfaceView }, error) {
|
|
err := s.Store.WithTx(ctx, func(tx store.Store) error {
|
|
if err := tx.SetInterfaceAlertDown(ctx, []string{in.ID}, in.Body.AlertDown); err != nil {
|
|
return err
|
|
}
|
|
return tx.AppendAudit(ctx, actorOf(ctx), "interface.alert_down", in.ID, nil, in.Body)
|
|
})
|
|
if err != nil {
|
|
return nil, s.storeErr(err)
|
|
}
|
|
s.alertDownChanged()
|
|
i, err := s.Store.GetInterface(ctx, in.ID)
|
|
if err != nil {
|
|
return nil, s.storeErr(err)
|
|
}
|
|
via, err := s.Store.InterfaceAlertDownVia(ctx, []string{in.ID})
|
|
if err != nil {
|
|
return nil, s.storeErr(err)
|
|
}
|
|
v := interfaceView(i, nil)
|
|
v.AlertDownState = alertDownState(i.AlertDown, via[in.ID])
|
|
return &struct{ Body InterfaceView }{v}, nil
|
|
}
|
|
|
|
func (s *server) setDeviceAlertDown(ctx context.Context, in *struct {
|
|
Slug string `path:"slug" doc:"Device slug."`
|
|
Body DeviceAlertDownBody
|
|
}) (*struct{ Body AlertDownResult }, error) {
|
|
d, err := s.Store.GetDeviceBySlug(ctx, in.Slug)
|
|
if err != nil || d.PurgedAt != nil {
|
|
return nil, huma.Error404NotFound("not found")
|
|
}
|
|
ifs, err := s.Store.ListInterfaces(ctx, d.ID, false)
|
|
if err != nil {
|
|
return nil, s.storeErr(err)
|
|
}
|
|
ids, err := alertDownTargets(ifs, in.Body.Ifaces)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = s.Store.WithTx(ctx, func(tx store.Store) error {
|
|
if err := tx.SetInterfaceAlertDown(ctx, ids, in.Body.On); err != nil {
|
|
return err
|
|
}
|
|
return tx.AppendAudit(ctx, actorOf(ctx), "device.interfaces.alert_down", d.Slug, nil,
|
|
map[string]any{"on": in.Body.On, "count": len(ids), "all": in.Body.Ifaces == nil})
|
|
})
|
|
if err != nil {
|
|
return nil, s.storeErr(err)
|
|
}
|
|
s.alertDownChanged()
|
|
return &struct{ Body AlertDownResult }{AlertDownResult{Changed: len(ids)}}, nil
|
|
}
|
|
|
|
// alertDownTargets picks the port ids a bulk change applies to: every active port, or the listed ones, which
|
|
// must all belong to the device.
|
|
func alertDownTargets(ifs []store.Interface, want []string) ([]string, error) {
|
|
have := make(map[string]bool, len(ifs))
|
|
all := make([]string, 0, len(ifs))
|
|
for _, i := range ifs {
|
|
have[i.ID] = true
|
|
all = append(all, i.ID)
|
|
}
|
|
if want == nil {
|
|
return all, nil
|
|
}
|
|
for _, id := range want {
|
|
if !have[id] {
|
|
return nil, huma.Error422UnprocessableEntity("interface " + id + " is not an active port of this device")
|
|
}
|
|
}
|
|
return want, nil
|
|
}
|
|
|
|
// withAlertDown fills the alert_down fields of interface views from one store query.
|
|
func (s *server) withAlertDown(ctx context.Context, ifs []store.Interface, views []InterfaceView) error {
|
|
ids := make([]string, len(ifs))
|
|
for i := range ifs {
|
|
ids[i] = ifs[i].ID
|
|
}
|
|
via, err := s.Store.InterfaceAlertDownVia(ctx, ids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range views {
|
|
views[i].AlertDownState = alertDownState(ifs[i].AlertDown, via[ifs[i].ID])
|
|
}
|
|
return nil
|
|
}
|