Skip to content

Commit a853acf

Browse files
committed
Added support to visualize inventory devices
Netmap now supports visualizing inventory devices by recursive lookup of LLDP information.
0 parents  commit a853acf

12 files changed

Lines changed: 459 additions & 0 deletions

File tree

.DS_Store

6 KB
Binary file not shown.

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Netmap
2+
3+
Netmap stands for Network Mapper, a visualizer for your inventory of network devices. Netmap starts collecting LLDP information with a single device credential and recursively collects neighbor of neighbors information. Built with love by Roopesh and friends in Go.
4+
5+
## Usage
6+
7+
```
8+
roopesh:~/ $ netmap create --help
9+
Create topology diagram
10+
11+
Usage:
12+
netmap create
13+
14+
Flags:
15+
-h, --help help for create
16+
-n, --hostname string hostname to connect
17+
-p, --password string password to connect to the host
18+
-u, --username string username to connect to the host
19+
roopesh:~/ $ netmap create -n ok270 -u admin -p password
20+
```

cmd/create.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package cmd
2+
3+
import (
4+
"opennetworktools/netmap/internal"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
var Hostname, Username, Password string
10+
11+
func init() {
12+
rootCmd.AddCommand(createCmd)
13+
createCmd.Flags().StringVarP(&Hostname, "hostname", "n", "", "hostname to connect")
14+
createCmd.Flags().StringVarP(&Username, "username", "u", "", "username to connect to the host")
15+
createCmd.Flags().StringVarP(&Password, "password", "p", "", "password to connect to the host")
16+
}
17+
18+
var createCmd = &cobra.Command{
19+
Use: "create",
20+
Short: "create command is used to create a topology diagram",
21+
Long: `create command is used to create a topology diagram by passing flags`,
22+
DisableFlagsInUseLine: true,
23+
Run: func(cmd *cobra.Command, args []string) {
24+
host, _ := cmd.Flags().GetString("hostname")
25+
username, _ := cmd.Flags().GetString("username")
26+
password, _ := cmd.Flags().GetString("password")
27+
internal.Traverse(host, username, password)
28+
},
29+
}

cmd/root.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var rootCmd = &cobra.Command{
11+
Use: "netmap",
12+
Short: "Netmap stands for Network Mapper, a visualizer for your inventory of network devices.",
13+
Long: `Netmap uses LLDP information to map devices. Built with love by Roopesh and friends in Go.
14+
Complete documentation is available at https://github.com/opennetworktools/netmap`,
15+
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
16+
}
17+
18+
func Execute() {
19+
if err := rootCmd.Execute(); err != nil {
20+
fmt.Println(err)
21+
os.Exit(1)
22+
}
23+
}

cmd/version.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
func init() {
10+
rootCmd.AddCommand(versionCmd)
11+
}
12+
13+
var versionCmd = &cobra.Command{
14+
Use: "version",
15+
Short: "Print the version number of Netmap",
16+
Long: `All software has versions. This is Netmap's`,
17+
Run: func(cmd *cobra.Command, args []string) {
18+
fmt.Println("netmap v0.0.1 -- HEAD")
19+
},
20+
}

go.mod

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module opennetworktools/netmap
2+
3+
go 1.23.1
4+
5+
require (
6+
github.com/aristanetworks/goeapi v1.0.0
7+
github.com/goccy/go-graphviz v0.2.9
8+
github.com/spf13/cobra v1.9.1
9+
)
10+
11+
require (
12+
github.com/disintegration/imaging v1.6.2 // indirect
13+
github.com/flopp/go-findfont v0.1.0 // indirect
14+
github.com/fogleman/gg v1.3.0 // indirect
15+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
16+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
17+
github.com/mitchellh/mapstructure v1.5.0 // indirect
18+
github.com/spf13/pflag v1.0.6 // indirect
19+
github.com/tetratelabs/wazero v1.8.1 // indirect
20+
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec // indirect
21+
golang.org/x/image v0.21.0 // indirect
22+
golang.org/x/text v0.19.0 // indirect
23+
)

go.sum

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
github.com/aristanetworks/goeapi v1.0.0 h1:FjckkjOY32SkmKrqDyBqYu6hN7DaIJuxcii9LLdZqtQ=
2+
github.com/aristanetworks/goeapi v1.0.0/go.mod h1:DcgIvssM+qcRRVICDky/ecT/Gqpx40UQDTYY8Lu/iJ0=
3+
github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI=
4+
github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
5+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
6+
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
7+
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
8+
github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU=
9+
github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw=
10+
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
11+
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
12+
github.com/goccy/go-graphviz v0.2.9 h1:4yD2MIMpxNt+sOEARDh5jTE2S/jeAKi92w72B83mWGg=
13+
github.com/goccy/go-graphviz v0.2.9/go.mod h1:hssjl/qbvUXGmloY81BwXt2nqoApKo7DFgDj5dLJGb8=
14+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
15+
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
16+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
17+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
18+
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
19+
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
20+
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
21+
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
22+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
23+
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
24+
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
25+
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
26+
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
27+
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
28+
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
29+
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec h1:DGmKwyZwEB8dI7tbLt/I/gQuP559o/0FrAkHKlQM/Ks=
30+
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw=
31+
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
32+
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
33+
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
34+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
35+
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
36+
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
37+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
38+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/arista/eapi.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package arista
2+
3+
import (
4+
"opennetworktools/netmap/internal/utils"
5+
6+
"github.com/aristanetworks/goeapi"
7+
"github.com/aristanetworks/goeapi/module"
8+
)
9+
10+
func GetNeighbors(hostname, username, password, enablePasswd string, port int) (*module.ShowLLDPNeighbors, error) {
11+
lldp := &module.ShowLLDPNeighbors{}
12+
13+
node, err := goeapi.Connect("http", hostname, username, password, port)
14+
if err != nil {
15+
return lldp, err
16+
}
17+
node.EnableAuthentication(enablePasswd)
18+
19+
handle, err := node.GetHandle("json")
20+
if err != nil {
21+
return lldp, err
22+
}
23+
handle.AddCommand(lldp)
24+
err = handle.Call()
25+
if err != nil {
26+
return lldp, err
27+
}
28+
29+
err = utils.SaveStructAsJson(hostname, lldp)
30+
if err != nil {
31+
return lldp, err
32+
}
33+
34+
return lldp, nil
35+
}

internal/traverse.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package internal
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"opennetworktools/netmap/internal/arista"
7+
"opennetworktools/netmap/internal/utils"
8+
"opennetworktools/netmap/internal/visualizer"
9+
)
10+
11+
func Traverse(hostname, username, password string) {
12+
ctx := context.Background()
13+
14+
// hostname := "roi460"
15+
// username := "admin"
16+
// password := ""
17+
enablePasswd := ""
18+
port := 80
19+
20+
networkMap := &visualizer.NetworkMap{
21+
Devices: make(map[string][]visualizer.Edge),
22+
}
23+
24+
// Queue for traversal
25+
pendingDevices := []string{hostname}
26+
visited := make(map[string]bool)
27+
28+
for len(pendingDevices) > 0 {
29+
device := pendingDevices[0]
30+
pendingDevices = pendingDevices[1:]
31+
32+
// Skip if already visited
33+
if visited[device] {
34+
continue
35+
}
36+
visited[device] = true
37+
38+
fmt.Printf("\nDiscovering neighbors for %s...", device)
39+
40+
// Get LLDP neighbors
41+
neighbors, err := arista.GetNeighbors(device, username, password, enablePasswd, port)
42+
if err != nil {
43+
fmt.Printf("\n%s", err.Error())
44+
continue
45+
}
46+
47+
// Store device in topology (ensure it's initialized)
48+
if _, exists := networkMap.Devices[device]; !exists {
49+
networkMap.Devices[device] = []visualizer.Edge{}
50+
}
51+
52+
fmt.Printf(" Found %d for %s!", len(neighbors.LLDPNeighbors), device)
53+
54+
// Process each neighbor
55+
for _, obj := range neighbors.LLDPNeighbors {
56+
neighborDevice := obj.NeighborDevice
57+
// fmt.Printf("\n - Found neighbor: %s", neighborDevice)
58+
59+
// Store the connection in the network map
60+
networkMap.Devices[device] = append(networkMap.Devices[device], visualizer.Edge{
61+
LocalPort: obj.Port,
62+
Neighbor: neighborDevice,
63+
NeighborPort: obj.NeighborPort,
64+
})
65+
66+
// Add neighbor to queue if not visited
67+
if !visited[neighborDevice] {
68+
pendingDevices = append(pendingDevices, neighborDevice)
69+
}
70+
}
71+
}
72+
73+
// Exporting networkMap to a JSON file
74+
err := utils.SaveStructAsJson("networkMap", networkMap)
75+
if err != nil {
76+
fmt.Println("Error writing the file: ", err)
77+
return
78+
}
79+
80+
// Creating a graph and exporting it to a PNG file using go-graphviz
81+
err = visualizer.SaveTopologyWithGraphviz(ctx, networkMap)
82+
if err != nil {
83+
fmt.Println("Error rendering the topology: ", err)
84+
return
85+
}
86+
}

internal/utils/utils.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package utils
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
)
9+
10+
func SaveStructAsJson(filename string, structData any) error {
11+
jsonRes, err := json.Marshal(structData)
12+
if err != nil {
13+
return err
14+
}
15+
16+
appDir, err := CreateDirectoryToSaveOutput()
17+
if err != nil {
18+
return err
19+
}
20+
21+
err = os.WriteFile(appDir+filename+".json", jsonRes, 0644)
22+
if err != nil {
23+
fmt.Println("Error writing file:", err)
24+
return err
25+
}
26+
27+
return nil
28+
}
29+
30+
func CreateDirectoryToSaveOutput() (string, error) {
31+
configDir, err := os.UserConfigDir()
32+
if err != nil {
33+
return "", err
34+
}
35+
36+
appDir := filepath.Join(configDir, "netmap")
37+
os.MkdirAll(appDir, 0755) // Ensure the directory exists
38+
39+
return appDir, nil
40+
}
41+
42+
func TruncateString(s string, n int) string {
43+
if n >= len(s) {
44+
return s
45+
}
46+
return s[:n] + "..."
47+
}

0 commit comments

Comments
 (0)