Improve visuals and code structure

This commit is contained in:
Melora Hugues 2024-09-22 10:27:05 +02:00
parent 25adf0f374
commit a5084c57f0
8 changed files with 485 additions and 184 deletions

115
internal/cache/datacache.go vendored Normal file
View file

@ -0,0 +1,115 @@
package cache
import (
"sync"
"git.faercol.me/faercol/topology-map/internal/models"
)
func NewDataCache() *DataCache {
return &DataCache{
lock: sync.Mutex{},
devices: make(map[int]*models.Device),
interfaces: make(map[int]*models.Interface),
cables: make(map[int]*models.Cable),
vms: make(map[int]*models.VM),
}
}
type DataCache struct {
lock sync.Mutex
devices map[int]*models.Device
interfaces map[int]*models.Interface
cables map[int]*models.Cable
vms map[int]*models.VM
}
func (d *DataCache) AddDevice(device models.Device) {
d.lock.Lock()
defer d.lock.Unlock()
d.devices[device.ID] = &device
}
func (d *DataCache) GetDevices() []*models.Device {
d.lock.Lock()
defer d.lock.Unlock()
var res []*models.Device
for _, dev := range d.devices {
res = append(res, dev)
}
return res
}
func (d *DataCache) AddInterface(iface models.Interface) {
d.lock.Lock()
defer d.lock.Unlock()
d.interfaces[iface.ID] = &iface
}
func (d *DataCache) GetInterfaces() []*models.Interface {
d.lock.Lock()
defer d.lock.Unlock()
var res []*models.Interface
for _, iface := range d.interfaces {
res = append(res, iface)
}
return res
}
func (d *DataCache) AddCable(cable models.Cable) {
d.lock.Lock()
defer d.lock.Unlock()
d.cables[cable.ID] = &cable
}
func (d *DataCache) GetCables() []*models.Cable {
d.lock.Lock()
defer d.lock.Unlock()
var res []*models.Cable
for _, cable := range d.cables {
res = append(res, cable)
}
return res
}
func (d *DataCache) AddVM(vm models.VM) {
d.lock.Lock()
defer d.lock.Unlock()
d.vms[vm.ID] = &vm
}
func (d *DataCache) GetVMs() []*models.VM {
d.lock.Lock()
defer d.lock.Unlock()
var res []*models.VM
for _, vm := range d.vms {
res = append(res, vm)
}
return res
}
func (d *DataCache) ReconcileData() {
d.lock.Lock()
defer d.lock.Unlock()
for id, cable := range d.cables {
ifaceA, ok := d.interfaces[cable.ATerminations[0].ObjectID]
if !ok {
continue
}
cable.ATerminations[0].Object.Interface = *ifaceA
ifaceB, ok := d.interfaces[cable.BTerminations[0].ObjectID]
if !ok {
continue
}
cable.BTerminations[0].Object.Interface = *ifaceB
d.cables[id] = cable
}
}

View file

@ -0,0 +1,14 @@
package models
type ElementData struct {
ID string `json:"id"`
Label string `json:"label"`
Source string `json:"source,omitempty"`
Target string `json:"target,omitempty"`
Parent string `json:"parent,omitempty"`
}
type Element struct {
Data ElementData `json:"data"`
Classes []string `json:"classes,omitempty"`
}

145
internal/models/netbox.go Normal file
View file

@ -0,0 +1,145 @@
package models
import "strconv"
const (
Iface2dot5G = "2.5gbase-t"
Iface1G = "1000base-t"
Iface100M = "100base-tx"
)
var speedAssociation map[string]int = map[string]int{
"2.5gbase-t": 2500,
"1000base-t": 1000,
"100base-tx": 100,
}
type DeviceRole struct {
ID int `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
}
type Device struct {
ID int `json:"id"`
Name string `json:"name"`
Role DeviceRole `json:"role"`
}
func (d Device) UniqueID() string {
return "device-" + strconv.FormatInt(int64(d.ID), 10)
}
func (d Device) Element() Element {
return Element{
Data: ElementData{
ID: d.UniqueID(),
Label: d.Name,
},
Classes: nil,
}
}
type InterfaceType struct {
Value string `json:"value"`
Label string `json:"label"`
}
type Interface struct {
ID int `json:"id"`
Name string `json:"name"`
Device Device `json:"device"`
MacAddress string `json:"mac_address"`
Type InterfaceType `json:"type"`
}
func (i Interface) MaxSpeed() int {
speed, ok := speedAssociation[i.Type.Value]
if !ok {
return 0
}
return speed
}
type Object struct {
Device Device `json:"device"`
Interface Interface
}
type CableTermination struct {
ObjectID int `json:"object_id"`
Object Object `json:"object"`
}
type Cable struct {
ID int `json:"id"`
ATerminations []CableTermination `json:"a_terminations"`
BTerminations []CableTermination `json:"b_terminations"`
}
func (c Cable) UniqueID() string {
return "link-" + strconv.FormatInt(int64(c.ID), 10)
}
func (c Cable) MaxSpeed() int {
speedA := c.ATerminations[0].Object.Interface.MaxSpeed()
speedB := c.BTerminations[0].Object.Interface.MaxSpeed()
if speedA > speedB {
return speedB
}
return speedA
}
func (c Cable) Element() Element {
var linkClass string
switch c.MaxSpeed() {
case 2500:
linkClass = "link-fast"
case 1000:
linkClass = "link-normal"
case 100:
linkClass = "link-slow"
default:
linkClass = "link-speed-unknown"
}
return Element{
Data: ElementData{
ID: c.UniqueID(),
Source: c.ATerminations[0].Object.Device.UniqueID(),
Target: c.BTerminations[0].Object.Device.UniqueID(),
},
Classes: []string{"link", linkClass},
}
}
type VM struct {
ID int `json:"id"`
Name string `json:"name"`
Device Device `json:"device"`
}
func (v VM) UniqueID() string {
return "vm-" + strconv.FormatInt(int64(v.ID), 10)
}
func (v VM) Elements() []Element {
return []Element{
{
Data: ElementData{
ID: v.UniqueID(),
Label: v.Name,
},
},
{
Data: ElementData{
ID: "vm-cable" + strconv.FormatInt(int64(v.ID), 10),
Source: v.UniqueID(),
Target: v.Device.UniqueID(),
},
Classes: []string{"link", "link-virtual"},
},
}
}

124
internal/netbox/netbox.go Normal file
View file

@ -0,0 +1,124 @@
package netbox
import (
"encoding/json"
"io"
"net/http"
"git.faercol.me/faercol/topology-map/internal/models"
)
const (
devicesRoute = "dcim/devices"
cablesRoute = "dcim/cables"
vmsRoute = "virtualization/virtual-machines"
interfaceRoute = "dcim/interfaces"
)
type vmsResponse struct {
Results []models.VM `json:"results"`
}
type deviceResponse struct {
Results []models.Device `json:"results"`
}
type cableResponse struct {
Results []models.Cable `json:"results"`
}
type interfaceResponse struct {
Results []models.Interface `json:"results"`
}
type NetboxClient struct {
httpClt *http.Client
netboxBaseURL string
netboxToken string
}
func (c *NetboxClient) queryAPI(route string) ([]byte, error) {
query, err := http.NewRequest("GET", c.netboxBaseURL+route, nil)
if err != nil {
return nil, err
}
query.Header.Set("Authorization", "Token "+c.netboxToken)
resp, err := c.httpClt.Do(query)
if err != nil {
return nil, err
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return respBody, nil
}
func (c *NetboxClient) GetDevices() ([]models.Device, error) {
var res deviceResponse
respBody, err := c.queryAPI(devicesRoute)
if err != nil {
return nil, err
}
if err := json.Unmarshal(respBody, &res); err != nil {
return nil, err
}
return res.Results, nil
}
func (c *NetboxClient) GetInterfaces() ([]models.Interface, error) {
var res interfaceResponse
respBody, err := c.queryAPI(interfaceRoute)
if err != nil {
return nil, err
}
if err := json.Unmarshal(respBody, &res); err != nil {
return nil, err
}
return res.Results, nil
}
func (c *NetboxClient) GetCables() ([]models.Cable, error) {
var res cableResponse
respBody, err := c.queryAPI(cablesRoute)
if err != nil {
return nil, err
}
if err := json.Unmarshal(respBody, &res); err != nil {
return nil, err
}
return res.Results, nil
}
func (c *NetboxClient) GetVMs() ([]models.VM, error) {
var res vmsResponse
respBody, err := c.queryAPI(vmsRoute)
if err != nil {
return nil, err
}
if err := json.Unmarshal(respBody, &res); err != nil {
return nil, err
}
return res.Results, nil
}
func NewClient(baseURL, token string) *NetboxClient {
return &NetboxClient{
httpClt: http.DefaultClient,
netboxBaseURL: baseURL,
netboxToken: token,
}
}

215
main.go
View file

@ -4,142 +4,62 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"io"
"net/http" "net/http"
"strconv"
"git.faercol.me/faercol/topology-map/internal/cache"
"git.faercol.me/faercol/topology-map/internal/models"
"git.faercol.me/faercol/topology-map/internal/netbox"
) )
const apiKey = "4e75b8927940adc29e2e1eac042bf92bcddd57fe" const apiKey = "4e75b8927940adc29e2e1eac042bf92bcddd57fe"
const netboxBaseURL = "https://netbox.internal.faercol.me/api/" const netboxBaseURL = "https://netbox.internal.faercol.me/api/"
type Device struct {
ID int `json:"id"`
Name string `json:"name"`
}
type Object struct {
Device Device `json:"device"`
}
type CableTermination struct {
Object Object `json:"object"`
}
type Cable struct {
ID int `json:"id"`
ATerminations []CableTermination `json:"a_terminations"`
BTerminations []CableTermination `json:"b_terminations"`
}
type VM struct {
ID int `json:"id"`
Name string `json:"name"`
Device Device `json:"device"`
}
type vmsResponse struct {
Results []VM `json:"results"`
}
type deviceResponse struct {
Results []Device `json:"results"`
}
type cableResponse struct {
Results []Cable `json:"results"`
}
type ElementData struct {
ID string `json:"id"`
Source string `json:"source,omitempty"`
Target string `json:"target,omitempty"`
Parent string `json:"parent,omitempty"`
}
type Element struct {
Data ElementData `json:"data"`
Classes []string `json:"classes"`
}
func GetDevices() ([]Device, error) {
query, err := http.NewRequest("GET", netboxBaseURL+"dcim/devices", nil)
if err != nil {
return nil, err
}
query.Header.Set("Authorization", "Token "+apiKey)
resp, err := http.DefaultClient.Do(query)
if err != nil {
return nil, err
}
var res deviceResponse
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err := json.Unmarshal(respBody, &res); err != nil {
return nil, err
}
return res.Results, nil
}
func GetCables() ([]Cable, error) {
query, err := http.NewRequest("GET", netboxBaseURL+"dcim/cables", nil)
if err != nil {
return nil, err
}
query.Header.Set("Authorization", "Token "+apiKey)
resp, err := http.DefaultClient.Do(query)
if err != nil {
return nil, err
}
var res cableResponse
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err := json.Unmarshal(respBody, &res); err != nil {
return nil, err
}
return res.Results, nil
}
func GetVMs() ([]VM, error) {
query, err := http.NewRequest("GET", netboxBaseURL+"virtualization/virtual-machines", nil)
if err != nil {
return nil, err
}
query.Header.Set("Authorization", "Token "+apiKey)
resp, err := http.DefaultClient.Do(query)
if err != nil {
return nil, err
}
var res vmsResponse
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err := json.Unmarshal(respBody, &res); err != nil {
return nil, err
}
return res.Results, nil
}
func main() { func main() {
fmt.Println("Initializing data")
dataCache := cache.NewDataCache()
netboxClt := netbox.NewClient(netboxBaseURL, apiKey)
fmt.Println("Getting devices")
devices, err := netboxClt.GetDevices()
if err != nil {
panic(err)
}
for _, d := range devices {
dataCache.AddDevice(d)
}
fmt.Println("Getting VMs")
vms, err := netboxClt.GetVMs()
if err != nil {
panic(err)
}
for _, v := range vms {
dataCache.AddVM(v)
}
fmt.Println("Getting cables")
cables, err := netboxClt.GetCables()
if err != nil {
panic(err)
}
for _, c := range cables {
dataCache.AddCable(c)
}
fmt.Println("Getting interfaces")
interfaces, err := netboxClt.GetInterfaces()
if err != nil {
panic(err)
}
for _, i := range interfaces {
dataCache.AddInterface(i)
}
fmt.Println("Reconciling data")
dataCache.ReconcileData()
fmt.Println("Done")
srv := http.NewServeMux() srv := http.NewServeMux()
srv.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) { srv.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
@ -149,36 +69,16 @@ func main() {
}) })
srv.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) { srv.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Serving API route") fmt.Println("Serving API route")
devices, err := GetDevices()
if err != nil {
fmt.Printf("Failed to get devices: %s\n", err)
w.WriteHeader(500)
return
}
cables, err := GetCables() resp := []models.Element{}
if err != nil { for _, d := range dataCache.GetDevices() {
fmt.Printf("Failed to get cables: %s\n", err) resp = append(resp, d.Element())
w.WriteHeader(500)
return
} }
for _, c := range dataCache.GetCables() {
vms, err := GetVMs() resp = append(resp, c.Element())
if err != nil {
fmt.Printf("Failed to get VMs: %s\n", err)
w.WriteHeader(500)
return
} }
for _, v := range dataCache.GetVMs() {
resp := []Element{} resp = append(resp, v.Elements()...)
for _, d := range devices {
resp = append(resp, Element{Data: ElementData{ID: d.Name}})
}
for _, c := range cables {
resp = append(resp, Element{Data: ElementData{ID: "link-" + strconv.FormatInt(int64(c.ID), 10), Source: c.ATerminations[0].Object.Device.Name, Target: c.BTerminations[0].Object.Device.Name}})
}
for _, v := range vms {
resp = append(resp, Element{Data: ElementData{ID: v.Name}}, Element{Data: ElementData{ID: "vm-" + strconv.FormatInt(int64(v.ID), 10), Source: v.Name, Target: v.Device.Name}, Classes: []string{"edge", "virtual-edge"}})
} }
respBody, err := json.Marshal(resp) respBody, err := json.Marshal(resp)
@ -188,6 +88,7 @@ func main() {
return return
} }
w.Header().Add("Content-Type", "application/json")
w.Write(respBody) w.Write(respBody)
}) })
srv.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { srv.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
@ -206,7 +107,7 @@ func main() {
} }
}) })
if err := http.ListenAndServe("127.0.0.1:5000", srv); err != nil { if err := http.ListenAndServe("0.0.0.0:5000", srv); err != nil {
panic(err) panic(err)
} }
} }

View file

@ -21,28 +21,17 @@ function setupGraph() {
.then(function (elements) { .then(function (elements) {
var cy = window.cy = cytoscape({ var cy = window.cy = cytoscape({
container: document.getElementById('map-container'),
container: document.getElementById('map-container'), // container to render in
elements: elements, elements: elements,
// elements: [ // list of graph elements to start with
// { // node a
// data: { id: 'a' }
// },
// { // node b
// data: { id: 'b' }
// },
// { // edge ab
// data: { id: 'ab', source: 'a', target: 'b' }
// }
// ],
style: [ // the stylesheet for the graph style: [ // the stylesheet for the graph
{ {
selector: 'node', selector: 'node',
style: { style: {
'background-color': '#666', 'background-color': '#E1F5FE',
'label': 'data(id)' 'border-color': '#03A9F4',
'border-width': 0.5,
'label': 'data(label)'
} }
}, },
@ -50,23 +39,37 @@ function setupGraph() {
selector: 'edge', selector: 'edge',
style: { style: {
'width': 1, 'width': 1,
'line-color': '#1E88E5', 'line-color': '#42A5F5',
'target-arrow-color': '#1E88E5', 'target-arrow-color': '#1E88E5',
'target-arrow-shape': 'none', 'target-arrow-shape': 'none',
'curve-style': 'bezier' 'curve-style': 'bezier'
} }
}, },
{ {
selector: ".virtual-edge", selector: ".link-virtual",
style: { style: {
"line-color": "#BDBDBD", "line-color": "#BDBDBD",
"line-style": "dashed", "line-style": "dashed",
} },
} },
{
selector: ".link-fast",
style: {
"width": 1.5,
"line-color": "#1565C0",
},
},
{
selector: ".link-slow",
style: {
"line-color": "#FFC107",
},
},
], ],
layout: { layout: {
name: 'cola', name: 'cose',
nodeDimensionsIncludeLabels: true,
} }
}); });

View file

@ -1,9 +1,8 @@
#map-container { #map-container {
width: 80%; width: 95%;
height: 600px; height: 1200px;
margin: auto;
display: block; display: block;
} background-color: #F5F5F5;
border-color: #BDBDBD;
.edge {
color: pink;
} }

Binary file not shown.