Get data from ARP cache

This commit is contained in:
Melora Hugues 2024-09-23 13:45:30 +02:00
parent a5084c57f0
commit 22017abc1b
9 changed files with 197 additions and 8 deletions

1
.gitignore vendored
View file

@ -8,6 +8,7 @@
*.dll *.dll
*.so *.so
*.dylib *.dylib
build/*
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test

View file

@ -22,6 +22,7 @@ type DataCache struct {
interfaces map[int]*models.Interface interfaces map[int]*models.Interface
cables map[int]*models.Cable cables map[int]*models.Cable
vms map[int]*models.VM vms map[int]*models.VM
arpRecords []*models.ARPRecord
} }
func (d *DataCache) AddDevice(device models.Device) { func (d *DataCache) AddDevice(device models.Device) {
@ -92,6 +93,13 @@ func (d *DataCache) GetVMs() []*models.VM {
return res return res
} }
func (d *DataCache) AddARPRecords(records []*models.ARPRecord) {
d.lock.Lock()
defer d.lock.Unlock()
d.arpRecords = records
}
func (d *DataCache) ReconcileData() { func (d *DataCache) ReconcileData() {
d.lock.Lock() d.lock.Lock()
defer d.lock.Unlock() defer d.lock.Unlock()
@ -110,6 +118,25 @@ func (d *DataCache) ReconcileData() {
cable.BTerminations[0].Object.Interface = *ifaceB cable.BTerminations[0].Object.Interface = *ifaceB
d.cables[id] = cable d.cables[id] = cable
}
for _, r := range d.arpRecords {
for _, iif := range d.interfaces {
if r.MacAddress == iif.MacAddress {
r.Device = &iif.Device
}
}
} }
} }
func (d *DataCache) GetUnmonitoredMachines() []models.UnmonitoredDevice {
var res []models.UnmonitoredDevice
for _, r := range d.arpRecords {
if r.Device == nil {
res = append(res, models.UnmonitoredDevice{Address: r.Address, MacAddress: r.MacAddress})
}
}
return res
}

36
internal/config/config.go Normal file
View file

@ -0,0 +1,36 @@
package config
import (
"encoding/json"
"fmt"
"os"
)
type NetboxConfig struct {
BaseURL string `json:"baseURL"`
APIKey string `json:"apiKey"`
}
type MikrotikConfig struct {
IPAddress string `json:"ipAddress"`
User string `json:"user"`
Password string `json:"password"`
}
type AppConfig struct {
Netbox NetboxConfig `json:"netbox"`
Mikrotik []MikrotikConfig `json:"mikrotik"`
}
func New(configPath string) (*AppConfig, error) {
content, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}
var conf AppConfig
if err := json.Unmarshal(content, &conf); err != nil {
return nil, fmt.Errorf("invalid config content: %w", err)
}
return &conf, nil
}

View file

@ -0,0 +1,77 @@
package mikrotik
import (
"encoding/json"
"fmt"
"io"
"net/http"
"git.faercol.me/faercol/topology-map/internal/config"
"git.faercol.me/faercol/topology-map/internal/models"
)
const arpRoute = "/rest/ip/arp"
func buildQueryURL(targetIP, route string) string {
return "http://" + targetIP + route
}
type MikrotikClient struct {
httpClt *http.Client
targets []config.MikrotikConfig
}
func NewMikrotikClient(targets []config.MikrotikConfig) *MikrotikClient {
return &MikrotikClient{
httpClt: http.DefaultClient,
targets: targets,
}
}
func (c *MikrotikClient) doQuery(target config.MikrotikConfig, route string) ([]byte, error) {
query, err := http.NewRequest(http.MethodGet, buildQueryURL(target.IPAddress, route), nil)
if err != nil {
return nil, err
}
query.SetBasicAuth(target.User, target.Password)
resp, err := c.httpClt.Do(query)
if err != nil {
return nil, err
}
return io.ReadAll(resp.Body)
}
func (c *MikrotikClient) getARPCacheForTarget(target config.MikrotikConfig) ([]models.ARPRecord, error) {
data, err := c.doQuery(target, arpRoute)
if err != nil {
return nil, err
}
var resp []models.ARPRecord
if err := json.Unmarshal(data, &resp); err != nil {
return nil, err
}
return resp, nil
}
func (c *MikrotikClient) GetARPRecords() ([]*models.ARPRecord, error) {
arpCache := map[string]models.ARPRecord{}
for _, t := range c.targets {
records, err := c.getARPCacheForTarget(t)
if err != nil {
return nil, fmt.Errorf("failed to get ARP data for device %s: %w", t.IPAddress, err)
}
for _, r := range records {
arpCache[r.MacAddress] = r
}
}
var res []*models.ARPRecord
for _, r := range arpCache {
res = append(res, &r)
}
return res, nil
}

View file

@ -0,0 +1,22 @@
package models
type ARPRecord struct {
Device *Device `json:"-"`
Address string `json:"address"`
MacAddress string `json:"mac-address"`
}
type UnmonitoredDevice struct {
Address string
MacAddress string
}
func (d UnmonitoredDevice) Element() Element {
return Element{
Classes: []string{"unmonitored"},
Data: ElementData{
ID: "unmonitored-" + d.MacAddress,
Label: d.Address,
},
}
}

View file

@ -9,10 +9,10 @@ import (
) )
const ( const (
devicesRoute = "dcim/devices" devicesRoute = "/api/dcim/devices"
cablesRoute = "dcim/cables" cablesRoute = "/api/dcim/cables"
vmsRoute = "virtualization/virtual-machines" vmsRoute = "/api/virtualization/virtual-machines"
interfaceRoute = "dcim/interfaces" interfaceRoute = "/api/dcim/interfaces"
) )
type vmsResponse struct { type vmsResponse struct {

23
main.go
View file

@ -7,18 +7,23 @@ import (
"net/http" "net/http"
"git.faercol.me/faercol/topology-map/internal/cache" "git.faercol.me/faercol/topology-map/internal/cache"
"git.faercol.me/faercol/topology-map/internal/config"
"git.faercol.me/faercol/topology-map/internal/mikrotik"
"git.faercol.me/faercol/topology-map/internal/models" "git.faercol.me/faercol/topology-map/internal/models"
"git.faercol.me/faercol/topology-map/internal/netbox" "git.faercol.me/faercol/topology-map/internal/netbox"
) )
const apiKey = "4e75b8927940adc29e2e1eac042bf92bcddd57fe"
const netboxBaseURL = "https://netbox.internal.faercol.me/api/"
func main() { func main() {
config, err := config.New("build/config.json")
if err != nil {
panic(err)
}
fmt.Println("Initializing data") fmt.Println("Initializing data")
dataCache := cache.NewDataCache() dataCache := cache.NewDataCache()
netboxClt := netbox.NewClient(netboxBaseURL, apiKey) netboxClt := netbox.NewClient(config.Netbox.BaseURL, config.Netbox.APIKey)
mikrotikClt := mikrotik.NewMikrotikClient(config.Mikrotik)
fmt.Println("Getting devices") fmt.Println("Getting devices")
devices, err := netboxClt.GetDevices() devices, err := netboxClt.GetDevices()
@ -56,6 +61,13 @@ func main() {
dataCache.AddInterface(i) dataCache.AddInterface(i)
} }
fmt.Println("Getting data from ARP cache")
arpCache, err := mikrotikClt.GetARPRecords()
if err != nil {
panic(err)
}
dataCache.AddARPRecords(arpCache)
fmt.Println("Reconciling data") fmt.Println("Reconciling data")
dataCache.ReconcileData() dataCache.ReconcileData()
fmt.Println("Done") fmt.Println("Done")
@ -80,6 +92,9 @@ func main() {
for _, v := range dataCache.GetVMs() { for _, v := range dataCache.GetVMs() {
resp = append(resp, v.Elements()...) resp = append(resp, v.Elements()...)
} }
for _, unm := range dataCache.GetUnmonitoredMachines() {
resp = append(resp, unm.Element())
}
respBody, err := json.Marshal(resp) respBody, err := json.Marshal(resp)
if err != nil { if err != nil {

View file

@ -65,12 +65,23 @@ function setupGraph() {
"line-color": "#FFC107", "line-color": "#FFC107",
}, },
}, },
{
selector: ".unmonitored",
style: {
"background-color": "#E57373",
"border-color": "#C62828"
}
}
], ],
layout: { layout: {
name: 'cose', name: 'cose',
nodeDimensionsIncludeLabels: true, nodeDimensionsIncludeLabels: true,
} }
// layout: {
// name: "cola",
// nodeDimensionsIncludeLabels: true,
// }
}); });
}); });

Binary file not shown.