feat: MAC lookup in scan FDB (API mac-locations, UI table, index)
Made-with: Cursor
This commit is contained in:
@@ -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 == "" {
|
||||
|
||||
Reference in New Issue
Block a user