Files
nmon/internal/api/devices_list.go

137 lines
4.9 KiB
Go

package api
import (
"context"
"net/http"
"time"
"github.com/danielgtaylor/huma/v2"
"github.com/v10networks/nmon/internal/store"
)
// DeviceSummary is one row of the device list.
type DeviceSummary struct {
Slug string `json:"slug"`
Name string `json:"name"`
Kind string `json:"kind" enum:"network,vm,container,check_only"`
Site string `json:"site"`
Role string `json:"role"`
MgmtIP string `json:"mgmt_ip"`
Profile string `json:"profile"`
Reachability string `json:"reachability" enum:"up,down,warning,unknown,maintenance"`
PollState string `json:"poll_state" enum:"ok,degraded,backoff"`
LastSeen *time.Time `json:"last_seen" nullable:"true"`
InterfacesTotal int64 `json:"interfaces_total" doc:"Active interfaces, excluded ones left out."`
InterfacesDown int64 `json:"interfaces_down" doc:"Active interfaces that are admin up and not oper up."`
Warnings []string `json:"warnings"`
}
// DeviceList is a page of DeviceSummary.
type DeviceList struct {
Items []DeviceSummary `json:"items"`
Page int64 `json:"page"`
PerPage int64 `json:"per_page"`
Total int64 `json:"total"`
}
// DeviceFacets are the distinct values the list's filter dropdowns offer.
type DeviceFacets struct {
Sites []string `json:"sites"`
Roles []string `json:"roles"`
Kinds []string `json:"kinds"`
}
func (s *server) registerDeviceList(api huma.API) {
huma.Register(api, huma.Operation{
OperationID: "list-devices", Method: http.MethodGet, Path: "/api/v1/devices", Tags: []string{"devices"},
Summary: "List devices",
Description: "Paged, with reachability and interface tallies. state filters on the derived reachability, " +
"so it is applied after the other filters.",
}, s.listDevices)
huma.Register(api, huma.Operation{
OperationID: "get-device-facets", Method: http.MethodGet, Path: "/api/v1/devices/facets", Tags: []string{"devices"},
Summary: "Distinct site, role and kind values of unretired devices",
}, s.deviceFacets)
}
func (s *server) listDevices(ctx context.Context, in *struct {
Site string `query:"site"`
Role string `query:"role"`
Kind string `query:"kind" enum:"network,vm,container,check_only"`
Q string `query:"q" doc:"Substring of name, slug, management address or sysName."`
State string `query:"state" enum:"up,down,warning,unknown,maintenance" doc:"Reachability."`
PageParams
}) (*struct{ Body DeviceList }, error) {
f := store.DeviceFilter{Site: in.Site, Role: in.Role, Kind: in.Kind, Q: in.Q}
var (
devs []store.Device
total int64
reach map[string]string
err error
)
if in.State == "" {
devs, total, err = s.Store.ListDevices(ctx, f, store.Page{Offset: in.offset(), Limit: in.limit()})
if err == nil {
reach = s.Reachability.Reachability(ctx, devs)
}
} else {
devs, total, reach, err = s.devicesInState(ctx, f, in.State, in.PageParams)
}
if err != nil {
return nil, s.storeErr(err)
}
counts, err := s.Store.InterfaceCounts(ctx)
if err != nil {
return nil, s.storeErr(err)
}
out := DeviceList{Items: make([]DeviceSummary, 0, len(devs)), Page: in.pageNo(), PerPage: in.limit(), Total: total}
for i := range devs {
d := &devs[i]
c := counts[d.ID]
out.Items = append(out.Items, DeviceSummary{
Slug: d.Slug, Name: d.Name, Kind: d.Kind, Site: d.Site, Role: d.Role, MgmtIP: d.MgmtIP, Profile: d.Profile,
Reachability: reach[d.Slug], PollState: pollStateOf(d), LastSeen: utcp(d.LastSeen),
InterfacesTotal: c.Total, InterfacesDown: c.Down, Warnings: s.warningsOf(d),
})
}
return &struct{ Body DeviceList }{out}, nil
}
// devicesInState pages over the devices whose derived reachability is state.
// Reachability is not a column, so every device matching the other filters is
// classified first and the page is cut from the survivors.
func (s *server) devicesInState(
ctx context.Context, f store.DeviceFilter, state string, p PageParams,
) (page []store.Device, total int64, reach map[string]string, err error) {
var all []store.Device
for off := int64(0); ; off += store.MaxLimit {
rows, n, err := s.Store.ListDevices(ctx, f, store.Page{Offset: off, Limit: store.MaxLimit})
if err != nil {
return nil, 0, nil, err
}
all = append(all, rows...)
if off+store.MaxLimit >= n {
break
}
}
reach = s.Reachability.Reachability(ctx, all)
var match []store.Device
for _, d := range all {
if reach[d.Slug] == state {
match = append(match, d)
}
}
lo := min(p.offset(), int64(len(match)))
hi := min(lo+p.limit(), int64(len(match)))
return match[lo:hi], int64(len(match)), reach, nil
}
func (s *server) deviceFacets(ctx context.Context, _ *struct{}) (*struct{ Body DeviceFacets }, error) {
sites, roles, kinds, err := s.Store.DeviceFacets(ctx)
if err != nil {
return nil, s.storeErr(err)
}
return &struct{ Body DeviceFacets }{DeviceFacets{Sites: nonNil(sites), Roles: nonNil(roles), Kinds: nonNil(kinds)}}, nil
}