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
90 lines
3.1 KiB
Go
90 lines
3.1 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
|
|
"github.com/v10networks/nmon/internal/store"
|
|
)
|
|
|
|
// InterfaceHit is one interface search result with its device and live rates.
|
|
type InterfaceHit struct {
|
|
ID string `json:"id" doc:"Interface ULID."`
|
|
Device DeviceRef `json:"device"`
|
|
IfName string `json:"ifname"`
|
|
IfAlias string `json:"ifalias"`
|
|
Description string `json:"description" doc:"Operator-owned port description (CSV import); discovery never overwrites it."`
|
|
IfDescr string `json:"ifdescr"`
|
|
OperStatus int64 `json:"oper_status"`
|
|
AdminStatus int64 `json:"admin_status"`
|
|
SpeedBPS int64 `json:"speed_bps" doc:"Link speed in bits per second (ifHighSpeed when set)."`
|
|
InBPS *float64 `json:"in_bps" nullable:"true"`
|
|
OutBPS *float64 `json:"out_bps" nullable:"true"`
|
|
AlertDownState
|
|
}
|
|
|
|
// InterfaceList is a page of InterfaceHit.
|
|
type InterfaceList struct {
|
|
Items []InterfaceHit `json:"items"`
|
|
Page int64 `json:"page"`
|
|
PerPage int64 `json:"per_page"`
|
|
Total int64 `json:"total"`
|
|
}
|
|
|
|
func (s *server) registerInterfaces(api huma.API) {
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "list-interfaces", Method: http.MethodGet, Path: "/api/v1/interfaces", Tags: []string{"interfaces"},
|
|
Summary: "Search interfaces by name or alias",
|
|
Description: "q matches ifname, ifalias and description, case-insensitively, across all devices. " +
|
|
"Rates are null when unknown or unreliable.",
|
|
}, s.listInterfaces)
|
|
}
|
|
|
|
func (s *server) listInterfaces(ctx context.Context, in *struct {
|
|
Q string `query:"q" doc:"Substring of ifname, ifalias or description."`
|
|
PageParams
|
|
}) (*struct{ Body InterfaceList }, error) {
|
|
rows, total, err := s.Store.SearchInterfaceHits(ctx, in.Q, store.Page{Offset: in.offset(), Limit: in.limit()})
|
|
if err != nil {
|
|
return nil, s.storeErr(err)
|
|
}
|
|
out := InterfaceList{Items: make([]InterfaceHit, 0, len(rows)), Page: in.pageNo(), PerPage: in.limit(), Total: total}
|
|
var rates map[string]*ifRates
|
|
if len(rows) > 0 {
|
|
rates, _ = s.liveRates(ctx, `iface=~`+quote(regexAlt(hitIDs(rows))), false)
|
|
}
|
|
via, err := s.Store.InterfaceAlertDownVia(ctx, hitIDs(rows))
|
|
if err != nil {
|
|
return nil, s.storeErr(err)
|
|
}
|
|
for i := range rows {
|
|
r := &rows[i]
|
|
inB, outB := rates[r.ID].bps(&r.Interface)
|
|
out.Items = append(out.Items, InterfaceHit{
|
|
ID: r.ID, Device: DeviceRef{Slug: r.DeviceSlug, Name: r.DeviceName}, IfName: r.IfName, IfAlias: r.IfAlias, Description: r.Description,
|
|
IfDescr: r.IfDescr, OperStatus: r.OperStatus, AdminStatus: r.AdminStatus, SpeedBPS: speedBPS(&r.Interface),
|
|
InBPS: inB, OutBPS: outB, AlertDownState: alertDownState(r.AlertDown, via[r.ID]),
|
|
})
|
|
}
|
|
return &struct{ Body InterfaceList }{out}, nil
|
|
}
|
|
|
|
// speedBPS is the link speed in bits per second: ifHighSpeed (Mbit/s) when the
|
|
// device reports it, since ifSpeed saturates at 4.29 Gbit/s.
|
|
func speedBPS(i *store.Interface) int64 {
|
|
if i.IfHighSpeed > 0 {
|
|
return i.IfHighSpeed * 1_000_000
|
|
}
|
|
return i.IfSpeed
|
|
}
|
|
|
|
func hitIDs(rows []store.InterfaceHit) []string {
|
|
ids := make([]string, len(rows))
|
|
for i := range rows {
|
|
ids[i] = rows[i].ID
|
|
}
|
|
return ids
|
|
}
|