Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
86 lines
3.7 KiB
Go
86 lines
3.7 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
|
|
"github.com/v10networks/nmon/internal/discovery"
|
|
"github.com/v10networks/nmon/internal/store"
|
|
)
|
|
|
|
// TableView says how one discovery table went.
|
|
type TableView struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status" doc:"ok, partial (row limit or timeout: previous rows kept), error or skipped."`
|
|
Rows int `json:"rows"`
|
|
Detail string `json:"detail,omitempty"`
|
|
}
|
|
|
|
// DiscoverView summarises one discovery run.
|
|
type DiscoverView struct {
|
|
Profile string `json:"profile"`
|
|
ProfileMissing string `json:"profile_missing,omitempty" doc:"The profile the device asked for when it no longer exists; generic was used."` //nolint:lll // struct tag text is the OpenAPI doc; cannot wrap
|
|
Partial bool `json:"partial" doc:"A table overran its row limit or timeout; its previous result was kept."`
|
|
Tables []TableView `json:"tables"`
|
|
InterfacesMatched int `json:"interfaces_matched"`
|
|
InterfacesNew int `json:"interfaces_new"`
|
|
InterfacesRetired int `json:"interfaces_retired"`
|
|
InterfacesSplit int `json:"interfaces_split" doc:"Interfaces that could not be matched one-to-one; both rows are kept and an identity review is open."` //nolint:lll // struct tag text is the OpenAPI doc; cannot wrap
|
|
Unretired int `json:"interfaces_unretired"`
|
|
IdentityReviews int `json:"identity_reviews_opened"`
|
|
HardwareReplaced bool `json:"hardware_replaced"`
|
|
Warnings []string `json:"warnings"`
|
|
}
|
|
|
|
func (s *server) registerOps(api huma.API) {
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "discover-device", Method: "POST", Path: "/api/v1/devices/{slug}/discover", Tags: []string{"devices"},
|
|
Summary: "Run discovery now",
|
|
Description: "Walks the device synchronously (bounded by the discovery timeout), applies the identity matcher and " +
|
|
"returns what changed. Admin-only once authentication arrives (M6).",
|
|
Errors: []int{http.StatusNotFound, http.StatusConflict, http.StatusBadGateway},
|
|
}, s.discoverDevice)
|
|
}
|
|
|
|
func (s *server) discoverDevice(ctx context.Context, in *struct {
|
|
Slug string `path:"slug" doc:"Device slug."`
|
|
}) (*struct{ Body DiscoverView }, error) {
|
|
dev, err := s.Store.GetDeviceBySlug(ctx, in.Slug)
|
|
if err != nil {
|
|
return nil, s.storeErr(err)
|
|
}
|
|
if dev.PurgedAt != nil {
|
|
return nil, huma.Error404NotFound("not found")
|
|
}
|
|
if dev.RetiredAt != nil || dev.Kind != store.KindNetwork || dev.SNMPCredentialID == nil {
|
|
return nil, huma.Error409Conflict("device is retired or has no SNMP credential; nothing to discover")
|
|
}
|
|
dctx, cancel := context.WithTimeout(ctx, s.DiscoveryTimeout)
|
|
defer cancel()
|
|
deps := discovery.Deps{Store: s.Store, Profiles: s.Profiles, Clock: s.Clock, SNMPPort: s.SNMPPort, Logger: s.Logger}
|
|
rep, err := discovery.Discover(dctx, deps, dev)
|
|
if err != nil {
|
|
return nil, huma.Error502BadGateway("discovery of " + in.Slug + " failed: " + err.Error())
|
|
}
|
|
if s.AfterDiscover != nil {
|
|
s.AfterDiscover(dev.ID)
|
|
}
|
|
return &struct{ Body DiscoverView }{DiscoverViewOf(rep)}, nil
|
|
}
|
|
|
|
// DiscoverViewOf converts a discovery report.
|
|
func DiscoverViewOf(r *discovery.Report) DiscoverView {
|
|
v := DiscoverView{
|
|
Profile: r.Profile, ProfileMissing: r.ProfileMissing, Partial: r.Partial, Tables: []TableView{},
|
|
InterfacesMatched: r.Matched, InterfacesNew: r.New, InterfacesRetired: r.Retired, InterfacesSplit: r.Split,
|
|
Unretired: r.Unretired, IdentityReviews: len(r.Reviews), HardwareReplaced: r.HardwareReplaced,
|
|
Warnings: nonNil(r.Warnings),
|
|
}
|
|
for _, t := range r.Tables {
|
|
v.Tables = append(v.Tables, TableView{Name: t.Name, Status: t.Status, Rows: t.Rows, Detail: t.Detail})
|
|
}
|
|
return v
|
|
}
|