feat: MAC lookup in scan FDB (API mac-locations, UI table, index)

Made-with: Cursor
This commit is contained in:
PTah
2026-04-13 12:41:48 +10:00
parent 886300b6c0
commit d85ec9ff61
9 changed files with 332 additions and 41 deletions
+85
View File
@@ -50,6 +50,19 @@ type ScanDiffResponse struct {
MissingLinks []string `json:"missing_links"`
}
// MacLocationRow — где в FDB найден MAC: коммутатор (ip), порт, клиентский IP из ARP.
type MacLocationRow struct {
SwitchIP string `json:"switch_ip"`
SwitchSysName string `json:"switch_sys_name,omitempty"`
IfIndex int `json:"if_index"`
BridgePort int `json:"bridge_port,omitempty"`
Vlan int `json:"vlan"`
MAC string `json:"mac"`
ClientIP string `json:"client_ip,omitempty"`
ClientName string `json:"client_name,omitempty"`
CheckedAt time.Time `json:"checked_at"`
}
func NewHandler(store scans.Store, run *scans.Runner, resetDBSecret string) *Handler {
return &Handler{store: store, run: run, resetDBSecret: strings.TrimSpace(resetDBSecret)}
}
@@ -70,6 +83,7 @@ func (h *Handler) Routes() http.Handler {
mux.HandleFunc("GET /api/scans/{id}/lldp", h.getScanLLDP)
mux.HandleFunc("GET /api/scans/{id}/interfaces", h.getScanInterfaces)
mux.HandleFunc("GET /api/scans/{id}/port-devices", h.getScanPortDevices)
mux.HandleFunc("GET /api/scans/{id}/mac-locations", h.getScanMacLocations)
mux.HandleFunc("GET /api/scans/{id}/links", h.getScanLinks)
mux.HandleFunc("GET /api/scans/{id}/topology", h.getScanTopology)
mux.HandleFunc("POST /api/admin/purge-scans", h.postAdminPurgeScans)
@@ -391,6 +405,77 @@ func (h *Handler) getScanPortDevices(w http.ResponseWriter, r *http.Request) {
})
}
func (h *Handler) getScanMacLocations(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
writeError(w, http.StatusBadRequest, "scan id is required")
return
}
if _, ok := h.store.GetScan(id); !ok {
writeError(w, http.StatusNotFound, "scan not found")
return
}
raw := strings.TrimSpace(r.URL.Query().Get("mac"))
if raw == "" {
writeError(w, http.StatusBadRequest, "query parameter mac is required")
return
}
norm := scans.NormalizeMAC(raw)
if norm == "" {
writeError(w, http.StatusBadRequest, "invalid mac address")
return
}
limit := 2000
if lim := strings.TrimSpace(r.URL.Query().Get("limit")); lim != "" {
n, err := strconv.Atoi(lim)
if err != nil || n <= 0 || n > 10000 {
writeError(w, http.StatusBadRequest, "limit must be between 1 and 10000")
return
}
limit = n
}
snmpItems := h.store.ListSNMPResults(id, 200000)
ipToName := make(map[string]string, len(snmpItems))
for _, s := range snmpItems {
if !s.Success {
continue
}
name := strings.TrimSpace(s.SysName)
if name == "" {
continue
}
if _, ok := ipToName[s.IP]; !ok {
ipToName[s.IP] = name
}
}
items := h.store.ListPortDevicesByMac(id, norm, limit)
out := make([]MacLocationRow, 0, len(items))
for _, r := range items {
row := MacLocationRow{
SwitchIP: r.IP,
IfIndex: r.IfIndex,
BridgePort: r.BridgePort,
Vlan: r.Vlan,
MAC: r.MAC,
ClientIP: strings.TrimSpace(r.LearnedIP),
CheckedAt: r.CheckedAt,
}
if nm, ok := ipToName[r.IP]; ok {
row.SwitchSysName = nm
}
if row.ClientIP != "" {
if nm, ok := ipToName[row.ClientIP]; ok {
row.ClientName = nm
}
}
out = append(out, row)
}
writeJSON(w, http.StatusOK, map[string]any{
"mac": norm,
"items": out,
})
}
func (h *Handler) getScanLinks(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {