diff --git a/internal/cache/datacache.go b/internal/cache/datacache.go new file mode 100644 index 0000000..7e0f3c7 --- /dev/null +++ b/internal/cache/datacache.go @@ -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 + + } +} diff --git a/internal/models/cytoscape.go b/internal/models/cytoscape.go new file mode 100644 index 0000000..d7821e8 --- /dev/null +++ b/internal/models/cytoscape.go @@ -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"` +} diff --git a/internal/models/netbox.go b/internal/models/netbox.go new file mode 100644 index 0000000..7dd7564 --- /dev/null +++ b/internal/models/netbox.go @@ -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"}, + }, + } +} diff --git a/internal/netbox/netbox.go b/internal/netbox/netbox.go new file mode 100644 index 0000000..9db42eb --- /dev/null +++ b/internal/netbox/netbox.go @@ -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, + } +} diff --git a/main.go b/main.go index 2603bc4..14fc9b6 100644 --- a/main.go +++ b/main.go @@ -4,142 +4,62 @@ import ( "encoding/json" "fmt" "html/template" - "io" "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 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() { + + 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.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) { 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() - if err != nil { - fmt.Printf("Failed to get cables: %s\n", err) - w.WriteHeader(500) - return + resp := []models.Element{} + for _, d := range dataCache.GetDevices() { + resp = append(resp, d.Element()) } - - vms, err := GetVMs() - if err != nil { - fmt.Printf("Failed to get VMs: %s\n", err) - w.WriteHeader(500) - return + for _, c := range dataCache.GetCables() { + resp = append(resp, c.Element()) } - - resp := []Element{} - 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"}}) + for _, v := range dataCache.GetVMs() { + resp = append(resp, v.Elements()...) } respBody, err := json.Marshal(resp) @@ -188,6 +88,7 @@ func main() { return } + w.Header().Add("Content-Type", "application/json") w.Write(respBody) }) 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) } } diff --git a/static/scripts/index.js b/static/scripts/index.js index 2c08c13..6939cfa 100644 --- a/static/scripts/index.js +++ b/static/scripts/index.js @@ -21,28 +21,17 @@ function setupGraph() { .then(function (elements) { var cy = window.cy = cytoscape({ - - container: document.getElementById('map-container'), // container to render in - + container: document.getElementById('map-container'), 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 { selector: 'node', style: { - 'background-color': '#666', - 'label': 'data(id)' + 'background-color': '#E1F5FE', + 'border-color': '#03A9F4', + 'border-width': 0.5, + 'label': 'data(label)' } }, @@ -50,23 +39,37 @@ function setupGraph() { selector: 'edge', style: { 'width': 1, - 'line-color': '#1E88E5', + 'line-color': '#42A5F5', 'target-arrow-color': '#1E88E5', 'target-arrow-shape': 'none', 'curve-style': 'bezier' } }, { - selector: ".virtual-edge", + selector: ".link-virtual", style: { "line-color": "#BDBDBD", "line-style": "dashed", - } - } + }, + }, + { + selector: ".link-fast", + style: { + "width": 1.5, + "line-color": "#1565C0", + }, + }, + { + selector: ".link-slow", + style: { + "line-color": "#FFC107", + }, + }, ], layout: { - name: 'cola', + name: 'cose', + nodeDimensionsIncludeLabels: true, } }); diff --git a/static/style/main.css b/static/style/main.css index 731f286..f7c93cb 100644 --- a/static/style/main.css +++ b/static/style/main.css @@ -1,9 +1,8 @@ #map-container { - width: 80%; - height: 600px; + width: 95%; + height: 1200px; + margin: auto; display: block; + background-color: #F5F5F5; + border-color: #BDBDBD; } - -.edge { - color: pink; -} \ No newline at end of file diff --git a/topology-map b/topology-map index 060453a..053699c 100755 Binary files a/topology-map and b/topology-map differ