forked from extern/smegmesh
Merge pull request #67 from tim-beatham/66-improve-graph-dot-tool
66 improve graph dot tool
This commit is contained in:
commit
311a15363a
@ -6,6 +6,8 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/akamensky/argparse"
|
||||
"github.com/tim-beatham/wgmesh/pkg/ctrlserver"
|
||||
graph "github.com/tim-beatham/wgmesh/pkg/dot"
|
||||
"github.com/tim-beatham/wgmesh/pkg/ipc"
|
||||
logging "github.com/tim-beatham/wgmesh/pkg/log"
|
||||
)
|
||||
@ -91,17 +93,40 @@ func leaveMesh(client *ipcRpc.Client, meshId string) {
|
||||
fmt.Println(reply)
|
||||
}
|
||||
|
||||
func getGraph(client *ipcRpc.Client, meshId string) {
|
||||
var reply string
|
||||
func getGraph(client *ipcRpc.Client) {
|
||||
listMeshesReply := new(ipc.ListMeshReply)
|
||||
|
||||
err := client.Call("IpcHandler.GetDOT", &meshId, &reply)
|
||||
err := client.Call("IpcHandler.ListMeshes", "", &listMeshesReply)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(reply)
|
||||
meshes := make(map[string][]ctrlserver.MeshNode)
|
||||
|
||||
for _, meshId := range listMeshesReply.Meshes {
|
||||
var meshReply ipc.GetMeshReply
|
||||
|
||||
err := client.Call("IpcHandler.GetMesh", &meshId, &meshReply)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
meshes[meshId] = meshReply.Nodes
|
||||
}
|
||||
|
||||
dotGenerator := graph.NewMeshGraphConverter(meshes)
|
||||
dot, err := dotGenerator.Generate()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(dot)
|
||||
}
|
||||
|
||||
func queryMesh(client *ipcRpc.Client, meshId, query string) {
|
||||
@ -258,11 +283,6 @@ func main() {
|
||||
Help: "Advertise ::/0 into the mesh network",
|
||||
})
|
||||
|
||||
var getGraphMeshId *string = getGraphCmd.String("m", "mesh", &argparse.Options{
|
||||
Required: true,
|
||||
Help: "MeshID of the graph to get",
|
||||
})
|
||||
|
||||
var leaveMeshMeshId *string = leaveMeshCmd.String("m", "mesh", &argparse.Options{
|
||||
Required: true,
|
||||
Help: "MeshID of the mesh to leave",
|
||||
@ -351,7 +371,7 @@ func main() {
|
||||
}
|
||||
|
||||
if getGraphCmd.Happened() {
|
||||
getGraph(client, *getGraphMeshId)
|
||||
getGraph(client)
|
||||
}
|
||||
|
||||
if leaveMeshCmd.Happened() {
|
||||
|
214
pkg/crdt/two_phase_map_test.go
Normal file
214
pkg/crdt/two_phase_map_test.go
Normal file
@ -0,0 +1,214 @@
|
||||
package crdt
|
||||
|
||||
import (
|
||||
"hash/fnv"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func NewMap(processId string) *TwoPhaseMap[string, string] {
|
||||
theMap := NewTwoPhaseMap[string, string](processId, func(key string) uint64 {
|
||||
hash := fnv.New64a()
|
||||
hash.Write([]byte(key))
|
||||
return hash.Sum64()
|
||||
}, 1)
|
||||
return theMap
|
||||
}
|
||||
|
||||
func TestTwoPhaseMapEmpty(t *testing.T) {
|
||||
theMap := NewMap("a")
|
||||
|
||||
if theMap.Contains("a") {
|
||||
t.Fatalf(`a should not be present in the map`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoPhaseMapValuePresent(t *testing.T) {
|
||||
theMap := NewMap("a")
|
||||
theMap.Put("a", "")
|
||||
|
||||
if !theMap.Contains("a") {
|
||||
t.Fatalf(`should be present within the map`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoPhaseMapValueNotPresent(t *testing.T) {
|
||||
theMap := NewMap("a")
|
||||
theMap.Put("b", "")
|
||||
|
||||
if theMap.Contains("a") {
|
||||
t.Fatalf(`a should not be present in the map`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoPhaseMapPutThenRemove(t *testing.T) {
|
||||
theMap := NewMap("a")
|
||||
|
||||
theMap.Put("a", "")
|
||||
theMap.Remove("a")
|
||||
|
||||
if theMap.Contains("a") {
|
||||
t.Fatalf(`a should not be present within the map`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoPhaseMapPutThenRemoveThenPut(t *testing.T) {
|
||||
theMap := NewMap("a")
|
||||
|
||||
theMap.Put("a", "")
|
||||
theMap.Remove("a")
|
||||
theMap.Put("a", "")
|
||||
|
||||
if !theMap.Contains("a") {
|
||||
t.Fatalf(`a should be present within the map`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkMarksTheValueIn2PMap(t *testing.T) {
|
||||
theMap := NewMap("a")
|
||||
|
||||
theMap.Put("a", "")
|
||||
theMap.Mark("a")
|
||||
|
||||
if !theMap.IsMarked("a") {
|
||||
t.Fatalf(`a should be marked`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsListReturnsItemsInList(t *testing.T) {
|
||||
theMap := NewMap("a")
|
||||
|
||||
theMap.Put("a", "bob")
|
||||
theMap.Put("b", "dylan")
|
||||
|
||||
keys := theMap.AsList()
|
||||
slices.Sort(keys)
|
||||
|
||||
if !slices.Equal([]string{"bob", "dylan"}, keys) {
|
||||
t.Fatalf(`values should be bob, dylan`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapShotRemoveMapEmpty(t *testing.T) {
|
||||
theMap := NewMap("a")
|
||||
theMap.Put("a", "bob")
|
||||
theMap.Put("b", "dylan")
|
||||
|
||||
snapshot := theMap.Snapshot()
|
||||
|
||||
if len(snapshot.Add) != 2 {
|
||||
t.Fatalf(`add values length should be 2`)
|
||||
}
|
||||
|
||||
if len(snapshot.Remove) != 0 {
|
||||
t.Fatalf(`remove map length should be 0`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapshotMapEmpty(t *testing.T) {
|
||||
theMap := NewMap("a")
|
||||
|
||||
snapshot := theMap.Snapshot()
|
||||
|
||||
if len(snapshot.Add) != 0 || len(snapshot.Remove) != 0 {
|
||||
t.Fatalf(`snapshot length should be 0`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnapShotFromStateReturnsIntersection(t *testing.T) {
|
||||
map1 := NewMap("a")
|
||||
map1.Put("a", "heyy")
|
||||
|
||||
map2 := NewMap("b")
|
||||
map2.Put("b", "hmmm")
|
||||
|
||||
message := map2.GenerateMessage()
|
||||
|
||||
snapShot := map1.SnapShotFromState(message)
|
||||
|
||||
if len(snapShot.Add) != 1 {
|
||||
t.Fatalf(`add length should be 1`)
|
||||
}
|
||||
|
||||
if len(snapShot.Remove) != 0 {
|
||||
t.Fatalf(`remove length should be 0`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHashDifferentOnChange(t *testing.T) {
|
||||
theMap := NewMap("a")
|
||||
|
||||
prevHash := theMap.GetHash()
|
||||
|
||||
theMap.Put("b", "hmmhmhmh")
|
||||
|
||||
if prevHash == theMap.GetHash() {
|
||||
t.Fatalf(`hashes should not be the same`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMessageReturnsClocks(t *testing.T) {
|
||||
theMap := NewMap("a")
|
||||
theMap.Put("a", "hmm")
|
||||
theMap.Put("b", "hmm")
|
||||
theMap.Remove("a")
|
||||
|
||||
message := theMap.GenerateMessage()
|
||||
|
||||
if len(message.AddContents) != 2 {
|
||||
t.Fatalf(`two items added add should be 2`)
|
||||
}
|
||||
|
||||
if len(message.RemoveContents) != 1 {
|
||||
t.Fatalf(`a was removed remove map should be length 1`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDifferenceReturnsDifferenceOfMaps(t *testing.T) {
|
||||
map1 := NewMap("a")
|
||||
map1.Put("a", "ssms")
|
||||
map1.Put("b", "sdmdsmd")
|
||||
|
||||
map2 := NewMap("b")
|
||||
map2.Put("d", "eek")
|
||||
map2.Put("c", "meh")
|
||||
|
||||
message1 := map1.GenerateMessage()
|
||||
message2 := map2.GenerateMessage()
|
||||
|
||||
difference := message1.Difference(0, message2)
|
||||
|
||||
if len(difference.AddContents) != 2 {
|
||||
t.Fatalf(`d and c are not in map1 they should be in add contents`)
|
||||
}
|
||||
|
||||
if len(difference.RemoveContents) != 0 {
|
||||
t.Fatalf(`remove should be empty`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeMergesValuesThatAreGreaterThanCurrentClock(t *testing.T) {
|
||||
map1 := NewMap("a")
|
||||
map1.Put("a", "ssms")
|
||||
map1.Put("b", "sdmdsmd")
|
||||
|
||||
map2 := NewMap("b")
|
||||
map2.Put("d", "eek")
|
||||
map2.Put("c", "meh")
|
||||
|
||||
message1 := map1.GenerateMessage()
|
||||
message2 := map2.GenerateMessage()
|
||||
|
||||
difference := message1.Difference(0, message2)
|
||||
state := map2.SnapShotFromState(difference)
|
||||
|
||||
map1.Merge(*state)
|
||||
|
||||
if !map1.Contains("d") {
|
||||
t.Fatalf(`d should be in the map`)
|
||||
}
|
||||
|
||||
if !map2.Contains("c") {
|
||||
t.Fatalf(`c should be in the map`)
|
||||
}
|
||||
}
|
227
pkg/dot/dot.go
Normal file
227
pkg/dot/dot.go
Normal file
@ -0,0 +1,227 @@
|
||||
// Graph allows the definition of a DOT graph in golang
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"strings"
|
||||
|
||||
"github.com/tim-beatham/wgmesh/pkg/lib"
|
||||
)
|
||||
|
||||
type GraphType string
|
||||
type Shape string
|
||||
|
||||
const (
|
||||
GRAPH GraphType = "graph"
|
||||
DIGRAPH GraphType = "digraph"
|
||||
)
|
||||
|
||||
const (
|
||||
CIRCLE Shape = "circle"
|
||||
STAR Shape = "star"
|
||||
HEXAGON Shape = "hexagon"
|
||||
PARALLELOGRAM Shape = "parallelogram"
|
||||
)
|
||||
|
||||
type Graph interface {
|
||||
Dottable
|
||||
GetType() GraphType
|
||||
}
|
||||
|
||||
type Cluster struct {
|
||||
Type GraphType
|
||||
Name string
|
||||
Label string
|
||||
nodes map[string]*Node
|
||||
edges map[string]Edge
|
||||
}
|
||||
|
||||
type RootGraph struct {
|
||||
Type GraphType
|
||||
Label string
|
||||
nodes map[string]*Node
|
||||
clusters map[string]*Cluster
|
||||
edges map[string]Edge
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
Name string
|
||||
Label string
|
||||
Shape Shape
|
||||
Size int
|
||||
}
|
||||
|
||||
type Edge interface {
|
||||
Dottable
|
||||
}
|
||||
|
||||
type DirectedEdge struct {
|
||||
Name string
|
||||
Label string
|
||||
From string
|
||||
To string
|
||||
}
|
||||
|
||||
type UndirectedEdge struct {
|
||||
Name string
|
||||
Label string
|
||||
From string
|
||||
To string
|
||||
}
|
||||
|
||||
// Dottable means an implementer can convert the struct to DOT representation
|
||||
type Dottable interface {
|
||||
GetDOT() (string, error)
|
||||
}
|
||||
|
||||
func NewGraph(label string, graphType GraphType) *RootGraph {
|
||||
return &RootGraph{Type: graphType, Label: label, clusters: map[string]*Cluster{}, nodes: make(map[string]*Node), edges: make(map[string]Edge)}
|
||||
}
|
||||
|
||||
// PutNode: puts a node in the graph
|
||||
func (g *RootGraph) PutNode(name, label string, size int, shape Shape) error {
|
||||
_, exists := g.nodes[name]
|
||||
|
||||
if exists {
|
||||
// If exists no need to add the ndoe
|
||||
return nil
|
||||
}
|
||||
|
||||
g.nodes[name] = &Node{Name: name, Label: label, Size: size, Shape: shape}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *RootGraph) PutCluster(graph *Cluster) {
|
||||
g.clusters[graph.Label] = graph
|
||||
}
|
||||
|
||||
func writeContituents[D Dottable](result *strings.Builder, elements ...D) error {
|
||||
for _, node := range elements {
|
||||
dot, err := node.GetDOT()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = result.WriteString(dot)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *RootGraph) GetDOT() (string, error) {
|
||||
var result strings.Builder
|
||||
|
||||
result.WriteString(fmt.Sprintf("%s {\n", g.Type))
|
||||
result.WriteString("node [colorscheme=set312];\n")
|
||||
result.WriteString("layout = fdp;\n")
|
||||
nodes := lib.MapValues(g.nodes)
|
||||
edges := lib.MapValues(g.edges)
|
||||
writeContituents(&result, nodes...)
|
||||
writeContituents(&result, edges...)
|
||||
|
||||
for _, cluster := range g.clusters {
|
||||
clusterDOT, err := cluster.GetDOT()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result.WriteString(clusterDOT)
|
||||
}
|
||||
|
||||
result.WriteString("}")
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
// GetType implements Graph.
|
||||
func (r *RootGraph) GetType() GraphType {
|
||||
return r.Type
|
||||
}
|
||||
|
||||
func constructEdge(graph Graph, name, label, from, to string) Edge {
|
||||
switch graph.GetType() {
|
||||
case DIGRAPH:
|
||||
return &DirectedEdge{Name: name, Label: label, From: from, To: to}
|
||||
default:
|
||||
return &UndirectedEdge{Name: name, Label: label, From: from, To: to}
|
||||
}
|
||||
}
|
||||
|
||||
// AddEdge: adds an edge between two nodes in the graph
|
||||
func (g *RootGraph) AddEdge(name string, label string, from string, to string) error {
|
||||
g.edges[name] = constructEdge(g, name, label, from, to)
|
||||
return nil
|
||||
}
|
||||
|
||||
const numColours = 12
|
||||
|
||||
func (n *Node) hash() int {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(n.Name))
|
||||
return (int(h.Sum32()) % numColours) + 1
|
||||
}
|
||||
|
||||
func (n *Node) GetDOT() (string, error) {
|
||||
return fmt.Sprintf("node[label=\"%s\",shape=%s, style=\"filled\", fillcolor=%d, width=%d, height=%d, fixedsize=true] \"%s\";\n",
|
||||
n.Label, n.Shape, n.hash(), n.Size, n.Size, n.Name), nil
|
||||
}
|
||||
|
||||
func (e *DirectedEdge) GetDOT() (string, error) {
|
||||
return fmt.Sprintf("\"%s\" -> \"%s\" [label=\"%s\"];\n", e.From, e.To, e.Label), nil
|
||||
}
|
||||
|
||||
func (e *UndirectedEdge) GetDOT() (string, error) {
|
||||
return fmt.Sprintf("\"%s\" -- \"%s\" [label=\"%s\"];\n", e.From, e.To, e.Label), nil
|
||||
}
|
||||
|
||||
// AddEdge: adds an edge between two nodes in the graph
|
||||
func (g *Cluster) AddEdge(name string, label string, from string, to string) error {
|
||||
g.edges[name] = constructEdge(g, name, label, from, to)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PutNode: puts a node in the graph
|
||||
func (g *Cluster) PutNode(name, label string, size int, shape Shape) error {
|
||||
_, exists := g.nodes[name]
|
||||
|
||||
if exists {
|
||||
// If exists no need to add the ndoe
|
||||
return nil
|
||||
}
|
||||
|
||||
g.nodes[name] = &Node{Name: name, Label: label, Shape: shape, Size: size}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Cluster) GetDOT() (string, error) {
|
||||
var builder strings.Builder
|
||||
|
||||
builder.WriteString(fmt.Sprintf("subgraph \"cluster%s\" {\n", g.Label))
|
||||
builder.WriteString(fmt.Sprintf("label = \"%s\"\n", g.Label))
|
||||
nodes := lib.MapValues(g.nodes)
|
||||
edges := lib.MapValues(g.edges)
|
||||
writeContituents(&builder, nodes...)
|
||||
writeContituents(&builder, edges...)
|
||||
|
||||
builder.WriteString("}\n")
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func (g *Cluster) GetType() GraphType {
|
||||
return g.Type
|
||||
}
|
||||
|
||||
func NewSubGraph(name string, label string, graphType GraphType) *Cluster {
|
||||
return &Cluster{
|
||||
Label: name,
|
||||
Type: graphType,
|
||||
Name: name,
|
||||
nodes: make(map[string]*Node),
|
||||
edges: make(map[string]Edge),
|
||||
}
|
||||
}
|
116
pkg/dot/wg.go
Normal file
116
pkg/dot/wg.go
Normal file
@ -0,0 +1,116 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/tim-beatham/wgmesh/pkg/ctrlserver"
|
||||
)
|
||||
|
||||
// MeshGraphConverter converts a mesh to a graph
|
||||
type MeshGraphConverter interface {
|
||||
// convert the mesh to textual form
|
||||
Generate() (string, error)
|
||||
}
|
||||
|
||||
type MeshDOTConverter struct {
|
||||
meshes map[string][]ctrlserver.MeshNode
|
||||
destinations map[string]interface{}
|
||||
}
|
||||
|
||||
func (c *MeshDOTConverter) Generate() (string, error) {
|
||||
g := NewGraph("Smegmesh", GRAPH)
|
||||
|
||||
for meshId := range c.meshes {
|
||||
err := c.generateMesh(g, meshId)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
for mesh := range c.meshes {
|
||||
g.PutNode(mesh, mesh, 1, CIRCLE)
|
||||
}
|
||||
|
||||
for destination := range c.destinations {
|
||||
g.PutNode(destination, destination, 1, HEXAGON)
|
||||
}
|
||||
|
||||
return g.GetDOT()
|
||||
}
|
||||
|
||||
func (c *MeshDOTConverter) generateMesh(g *RootGraph, meshId string) error {
|
||||
nodes := c.meshes[meshId]
|
||||
|
||||
g.PutNode(meshId, meshId, 1, CIRCLE)
|
||||
|
||||
for _, node := range nodes {
|
||||
c.graphNode(g, node, meshId)
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
g.AddEdge(fmt.Sprintf("%s to %s", node.PublicKey, meshId), "", node.PublicKey, meshId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// graphNode: graphs a node within the mesh
|
||||
func (c *MeshDOTConverter) graphNode(g *RootGraph, node ctrlserver.MeshNode, meshId string) {
|
||||
alias := node.Alias
|
||||
|
||||
if alias == "" {
|
||||
alias = node.WgHost[1:len(node.WgHost)-20] + "\\n" + node.WgHost[len(node.WgHost)-20:len(node.WgHost)]
|
||||
}
|
||||
|
||||
g.PutNode(node.PublicKey, alias, 2, CIRCLE)
|
||||
|
||||
for _, route := range node.Routes {
|
||||
if len(route.Path) == 0 {
|
||||
g.AddEdge(route.Destination, "", node.PublicKey, route.Destination)
|
||||
continue
|
||||
}
|
||||
|
||||
reversedPath := slices.Clone(route.Path)
|
||||
slices.Reverse(reversedPath)
|
||||
|
||||
g.AddEdge(fmt.Sprintf("%s to %s", node.PublicKey, reversedPath[0]), "", node.PublicKey, reversedPath[0])
|
||||
|
||||
for _, mesh := range route.Path {
|
||||
if _, ok := c.meshes[mesh]; !ok {
|
||||
c.destinations[mesh] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for index := range reversedPath[0 : len(reversedPath)-1] {
|
||||
routeID := fmt.Sprintf("%s to %s", reversedPath[index], reversedPath[index+1])
|
||||
g.AddEdge(routeID, "", reversedPath[index], reversedPath[index+1])
|
||||
}
|
||||
|
||||
if route.Destination == "::/0" {
|
||||
c.destinations[route.Destination] = struct{}{}
|
||||
lastMesh := reversedPath[len(reversedPath)-1]
|
||||
routeID := fmt.Sprintf("%s to %s", lastMesh, route.Destination)
|
||||
g.AddEdge(routeID, "", lastMesh, route.Destination)
|
||||
}
|
||||
}
|
||||
|
||||
for service := range node.Services {
|
||||
c.putService(g, service, meshId, node)
|
||||
}
|
||||
}
|
||||
|
||||
// putService: construct a service node and a link between the nodes
|
||||
func (c *MeshDOTConverter) putService(g *RootGraph, key, meshId string, node ctrlserver.MeshNode) {
|
||||
serviceID := fmt.Sprintf("%s%s%s", key, node.PublicKey, meshId)
|
||||
g.PutNode(serviceID, key, 1, PARALLELOGRAM)
|
||||
g.AddEdge(fmt.Sprintf("%s to %s", node.PublicKey, serviceID), "", node.PublicKey, serviceID)
|
||||
}
|
||||
|
||||
func NewMeshGraphConverter(meshes map[string][]ctrlserver.MeshNode) MeshGraphConverter {
|
||||
return &MeshDOTConverter{
|
||||
meshes: meshes,
|
||||
destinations: make(map[string]interface{}),
|
||||
}
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
// Graph allows the definition of a DOT graph in golang
|
||||
package graph
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"strings"
|
||||
|
||||
"github.com/tim-beatham/wgmesh/pkg/lib"
|
||||
)
|
||||
|
||||
type GraphType string
|
||||
type Shape string
|
||||
|
||||
const (
|
||||
GRAPH GraphType = "graph"
|
||||
DIGRAPH = "digraph"
|
||||
)
|
||||
|
||||
const (
|
||||
CIRCLE Shape = "circle"
|
||||
STAR Shape = "star"
|
||||
HEXAGON Shape = "hexagon"
|
||||
)
|
||||
|
||||
type Graph struct {
|
||||
Type GraphType
|
||||
Label string
|
||||
nodes map[string]*Node
|
||||
edges []Edge
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
Name string
|
||||
Shape Shape
|
||||
}
|
||||
|
||||
type Edge interface {
|
||||
Dottable
|
||||
}
|
||||
|
||||
type DirectedEdge struct {
|
||||
Label string
|
||||
From *Node
|
||||
To *Node
|
||||
}
|
||||
|
||||
type UndirectedEdge struct {
|
||||
Label string
|
||||
From *Node
|
||||
To *Node
|
||||
}
|
||||
|
||||
// Dottable means an implementer can convert the struct to DOT representation
|
||||
type Dottable interface {
|
||||
GetDOT() (string, error)
|
||||
}
|
||||
|
||||
func NewGraph(label string, graphType GraphType) *Graph {
|
||||
return &Graph{Type: graphType, Label: label, nodes: make(map[string]*Node), edges: make([]Edge, 0)}
|
||||
}
|
||||
|
||||
// PutNode: puts a node in the graph
|
||||
func (g *Graph) PutNode(label string, shape Shape) error {
|
||||
_, exists := g.nodes[label]
|
||||
|
||||
if exists {
|
||||
// If exists no need to add the ndoe
|
||||
return nil
|
||||
}
|
||||
|
||||
g.nodes[label] = &Node{Name: label, Shape: shape}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeContituents[D Dottable](result *strings.Builder, elements ...D) error {
|
||||
for _, node := range elements {
|
||||
dot, err := node.GetDOT()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = result.WriteString(dot)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Graph) GetDOT() (string, error) {
|
||||
var result strings.Builder
|
||||
|
||||
_, err := result.WriteString(fmt.Sprintf("%s {\n", g.Type))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = result.WriteString("node [colorscheme=set312];\n")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nodes := lib.MapValues(g.nodes)
|
||||
|
||||
err = writeContituents(&result, nodes...)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = writeContituents(&result, g.edges...)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = result.WriteString("}")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func (g *Graph) constructEdge(label string, from *Node, to *Node) Edge {
|
||||
switch g.Type {
|
||||
case DIGRAPH:
|
||||
return &DirectedEdge{Label: label, From: from, To: to}
|
||||
default:
|
||||
return &UndirectedEdge{Label: label, From: from, To: to}
|
||||
}
|
||||
}
|
||||
|
||||
// AddEdge: adds an edge between two nodes in the graph
|
||||
func (g *Graph) AddEdge(label string, from string, to string) error {
|
||||
fromNode, exists := g.nodes[from]
|
||||
|
||||
if !exists {
|
||||
return errors.New(fmt.Sprintf("Node %s does not exist", from))
|
||||
}
|
||||
|
||||
toNode, exists := g.nodes[to]
|
||||
|
||||
if !exists {
|
||||
return errors.New(fmt.Sprintf("Node %s does not exist", to))
|
||||
}
|
||||
|
||||
g.edges = append(g.edges, g.constructEdge(label, fromNode, toNode))
|
||||
return nil
|
||||
}
|
||||
|
||||
const numColours = 12
|
||||
|
||||
func (n *Node) hash() int {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(n.Name))
|
||||
return (int(h.Sum32()) % numColours) + 1
|
||||
}
|
||||
|
||||
func (n *Node) GetDOT() (string, error) {
|
||||
return fmt.Sprintf("node[shape=%s, style=\"filled\", fillcolor=%d] %s;\n",
|
||||
n.Shape, n.hash(), n.Name), nil
|
||||
}
|
||||
|
||||
func (e *DirectedEdge) GetDOT() (string, error) {
|
||||
return fmt.Sprintf("%s -> %s;\n", e.From.Name, e.To.Name), nil
|
||||
}
|
||||
|
||||
func (e *UndirectedEdge) GetDOT() (string, error) {
|
||||
return fmt.Sprintf("%s -- %s;\n", e.From.Name, e.To.Name), nil
|
||||
}
|
@ -68,7 +68,6 @@ type MeshIpc interface {
|
||||
JoinMesh(args JoinMeshArgs, reply *string) error
|
||||
LeaveMesh(meshId string, reply *string) error
|
||||
GetMesh(meshId string, reply *GetMeshReply) error
|
||||
GetDOT(meshId string, reply *string) error
|
||||
Query(query QueryMesh, reply *string) error
|
||||
PutDescription(description string, reply *string) error
|
||||
PutAlias(alias string, reply *string) error
|
||||
|
@ -1,77 +0,0 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/tim-beatham/wgmesh/pkg/graph"
|
||||
"github.com/tim-beatham/wgmesh/pkg/lib"
|
||||
)
|
||||
|
||||
// MeshGraphConverter converts a mesh to a graph
|
||||
type MeshGraphConverter interface {
|
||||
// convert the mesh to textual form
|
||||
Generate(meshId string) (string, error)
|
||||
}
|
||||
|
||||
type MeshDOTConverter struct {
|
||||
manager MeshManager
|
||||
}
|
||||
|
||||
func (c *MeshDOTConverter) Generate(meshId string) (string, error) {
|
||||
mesh := c.manager.GetMesh(meshId)
|
||||
|
||||
if mesh == nil {
|
||||
return "", errors.New("mesh does not exist")
|
||||
}
|
||||
|
||||
g := graph.NewGraph(meshId, graph.GRAPH)
|
||||
|
||||
snapshot, err := mesh.GetMesh()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, node := range snapshot.GetNodes() {
|
||||
c.graphNode(g, node, meshId)
|
||||
}
|
||||
|
||||
nodes := lib.MapValues(snapshot.GetNodes())
|
||||
|
||||
for i, node1 := range nodes[:len(nodes)-1] {
|
||||
for _, node2 := range nodes[i+1:] {
|
||||
if node1.GetWgEndpoint() == node2.GetWgEndpoint() {
|
||||
continue
|
||||
}
|
||||
|
||||
node1Id := fmt.Sprintf("\"%s\"", node1.GetIdentifier())
|
||||
node2Id := fmt.Sprintf("\"%s\"", node2.GetIdentifier())
|
||||
g.AddEdge(fmt.Sprintf("%s to %s", node1Id, node2Id), node1Id, node2Id)
|
||||
}
|
||||
}
|
||||
|
||||
return g.GetDOT()
|
||||
}
|
||||
|
||||
// graphNode: graphs a node within the mesh
|
||||
func (c *MeshDOTConverter) graphNode(g *graph.Graph, node MeshNode, meshId string) {
|
||||
nodeId := fmt.Sprintf("\"%s\"", node.GetIdentifier())
|
||||
g.PutNode(nodeId, graph.CIRCLE)
|
||||
|
||||
self, _ := c.manager.GetSelf(meshId)
|
||||
|
||||
if NodeEquals(self, node) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, route := range node.GetRoutes() {
|
||||
routeId := fmt.Sprintf("\"%s\"", route)
|
||||
g.PutNode(routeId, graph.HEXAGON)
|
||||
g.AddEdge(fmt.Sprintf("%s to %s", nodeId, routeId), nodeId, routeId)
|
||||
}
|
||||
}
|
||||
|
||||
func NewMeshDotConverter(m MeshManager) MeshGraphConverter {
|
||||
return &MeshDOTConverter{manager: m}
|
||||
}
|
@ -182,19 +182,6 @@ func (n *IpcHandler) GetMesh(meshId string, reply *ipc.GetMeshReply) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *IpcHandler) GetDOT(meshId string, reply *string) error {
|
||||
g := mesh.NewMeshDotConverter(n.Server.GetMeshManager())
|
||||
|
||||
result, err := g.Generate(meshId)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*reply = result
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *IpcHandler) Query(params ipc.QueryMesh, reply *string) error {
|
||||
queryResponse, err := n.Server.GetQuerier().Query(params.MeshId, params.Query)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user