Files
nmon/internal/api/ops.go
jleung f85c3ef781
Some checks failed
ci / lint (push) Has been cancelled
ci / test (ubuntu-24.04-arm) (push) Has been cancelled
ci / test (ubuntu-latest) (push) Has been cancelled
ci / postgres (push) Has been cancelled
ci / openapi (push) Has been cancelled
M3: Go API per UI contract, embedded UI, openapi compat check, nmon-devtools, live gate + screenshots; M4 WP1 reachability and dependency suppression
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-22 20:34:14 +00:00

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
}