diff --git a/.gitignore b/.gitignore index 5b90e79..9e880eb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.dll *.so *.dylib +build/* # Test binary, built with `go test -c` *.test diff --git a/internal/cache/datacache.go b/internal/cache/datacache.go index 7e0f3c7..e6e730c 100644 --- a/internal/cache/datacache.go +++ b/internal/cache/datacache.go @@ -22,6 +22,7 @@ type DataCache struct { interfaces map[int]*models.Interface cables map[int]*models.Cable vms map[int]*models.VM + arpRecords []*models.ARPRecord } func (d *DataCache) AddDevice(device models.Device) { @@ -92,6 +93,13 @@ func (d *DataCache) GetVMs() []*models.VM { return res } +func (d *DataCache) AddARPRecords(records []*models.ARPRecord) { + d.lock.Lock() + defer d.lock.Unlock() + + d.arpRecords = records +} + func (d *DataCache) ReconcileData() { d.lock.Lock() defer d.lock.Unlock() @@ -110,6 +118,25 @@ func (d *DataCache) ReconcileData() { cable.BTerminations[0].Object.Interface = *ifaceB 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 +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..83fdd60 --- /dev/null +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/mikrotik/mikrotik.go b/internal/mikrotik/mikrotik.go new file mode 100644 index 0000000..b0aa2f3 --- /dev/null +++ b/internal/mikrotik/mikrotik.go @@ -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 +} diff --git a/internal/models/mikrotik.go b/internal/models/mikrotik.go new file mode 100644 index 0000000..8a306b9 --- /dev/null +++ b/internal/models/mikrotik.go @@ -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, + }, + } +} diff --git a/internal/netbox/netbox.go b/internal/netbox/netbox.go index 9db42eb..1bd7f61 100644 --- a/internal/netbox/netbox.go +++ b/internal/netbox/netbox.go @@ -9,10 +9,10 @@ import ( ) const ( - devicesRoute = "dcim/devices" - cablesRoute = "dcim/cables" - vmsRoute = "virtualization/virtual-machines" - interfaceRoute = "dcim/interfaces" + devicesRoute = "/api/dcim/devices" + cablesRoute = "/api/dcim/cables" + vmsRoute = "/api/virtualization/virtual-machines" + interfaceRoute = "/api/dcim/interfaces" ) type vmsResponse struct { diff --git a/main.go b/main.go index 14fc9b6..fe6b46a 100644 --- a/main.go +++ b/main.go @@ -7,18 +7,23 @@ import ( "net/http" "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/netbox" ) -const apiKey = "4e75b8927940adc29e2e1eac042bf92bcddd57fe" -const netboxBaseURL = "https://netbox.internal.faercol.me/api/" - func main() { + config, err := config.New("build/config.json") + if err != nil { + panic(err) + } + fmt.Println("Initializing data") 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") devices, err := netboxClt.GetDevices() @@ -56,6 +61,13 @@ func main() { 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") dataCache.ReconcileData() fmt.Println("Done") @@ -80,6 +92,9 @@ func main() { for _, v := range dataCache.GetVMs() { resp = append(resp, v.Elements()...) } + for _, unm := range dataCache.GetUnmonitoredMachines() { + resp = append(resp, unm.Element()) + } respBody, err := json.Marshal(resp) if err != nil { diff --git a/static/scripts/index.js b/static/scripts/index.js index 6939cfa..5a3249f 100644 --- a/static/scripts/index.js +++ b/static/scripts/index.js @@ -65,12 +65,23 @@ function setupGraph() { "line-color": "#FFC107", }, }, + { + selector: ".unmonitored", + style: { + "background-color": "#E57373", + "border-color": "#C62828" + } + } ], layout: { name: 'cose', nodeDimensionsIncludeLabels: true, } + // layout: { + // name: "cola", + // nodeDimensionsIncludeLabels: true, + // } }); }); diff --git a/topology-map b/topology-map deleted file mode 100755 index 053699c..0000000 Binary files a/topology-map and /dev/null differ