Add HTTP API with JSON status endpoint and typeid support
This commit is contained in:
@@ -5,5 +5,5 @@
|
|||||||
- Don't mention claude in commit messages. Keep them to a single, short, descriptive sentence
|
- Don't mention claude in commit messages. Keep them to a single, short, descriptive sentence
|
||||||
- Always push after commiting
|
- Always push after commiting
|
||||||
- Use git add -A so you don't miss files when committing
|
- Use git add -A so you don't miss files when committing
|
||||||
- Never use go build -- use go run instead
|
- Never use go build -- use go vet instead
|
||||||
- DO NOT commit unless asked to
|
- DO NOT commit unless asked to
|
||||||
10
artnet.go
10
artnet.go
@@ -23,10 +23,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ArtNetNode struct {
|
type ArtNetNode struct {
|
||||||
Node *Node
|
TypeID string `json:"typeid"`
|
||||||
Inputs []int
|
Node *Node `json:"node"`
|
||||||
Outputs []int
|
Inputs []int `json:"inputs,omitempty"`
|
||||||
LastSeen time.Time
|
Outputs []int `json:"outputs,omitempty"`
|
||||||
|
LastSeen time.Time `json:"last_seen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tendrils) listenArtNet(ctx context.Context, iface net.Interface) {
|
func (t *Tendrils) listenArtNet(ctx context.Context, iface net.Interface) {
|
||||||
@@ -217,6 +218,7 @@ func (a *ArtNetNodes) Update(node *Node, inputs, outputs []int) {
|
|||||||
existing.LastSeen = time.Now()
|
existing.LastSeen = time.Now()
|
||||||
} else {
|
} else {
|
||||||
a.nodes[node] = &ArtNetNode{
|
a.nodes[node] = &ArtNetNode{
|
||||||
|
TypeID: newTypeID("artnetnode"),
|
||||||
Node: node,
|
Node: node,
|
||||||
Inputs: inputs,
|
Inputs: inputs,
|
||||||
Outputs: outputs,
|
Outputs: outputs,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ func main() {
|
|||||||
debugBMD := flag.Bool("debug-bmd", false, "debug Blackmagic discovery")
|
debugBMD := flag.Bool("debug-bmd", false, "debug Blackmagic discovery")
|
||||||
debugShure := flag.Bool("debug-shure", false, "debug Shure discovery")
|
debugShure := flag.Bool("debug-shure", false, "debug Shure discovery")
|
||||||
debugYamaha := flag.Bool("debug-yamaha", false, "debug Yamaha discovery")
|
debugYamaha := flag.Bool("debug-yamaha", false, "debug Yamaha discovery")
|
||||||
|
httpPort := flag.String("http", ":80", "HTTP server port (empty to disable)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
t := tendrils.New()
|
t := tendrils.New()
|
||||||
@@ -56,5 +57,6 @@ func main() {
|
|||||||
t.DebugBMD = *debugBMD
|
t.DebugBMD = *debugBMD
|
||||||
t.DebugShure = *debugShure
|
t.DebugShure = *debugShure
|
||||||
t.DebugYamaha = *debugYamaha
|
t.DebugYamaha = *debugYamaha
|
||||||
|
t.HTTPPort = *httpPort
|
||||||
t.Run()
|
t.Run()
|
||||||
}
|
}
|
||||||
|
|||||||
36
dante.go
36
dante.go
@@ -3,6 +3,7 @@ package tendrils
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
@@ -118,16 +119,30 @@ func (n *Nodes) GetDanteTxDeviceInGroup(groupIP net.IP) string {
|
|||||||
|
|
||||||
var danteSeqID uint32
|
var danteSeqID uint32
|
||||||
|
|
||||||
|
type DanteSubscriberMap map[*Node]*DanteFlowSubscriber
|
||||||
|
|
||||||
|
func (m DanteSubscriberMap) MarshalJSON() ([]byte, error) {
|
||||||
|
subs := make([]*DanteFlowSubscriber, 0, len(m))
|
||||||
|
for _, sub := range m {
|
||||||
|
subs = append(subs, sub)
|
||||||
|
}
|
||||||
|
sort.Slice(subs, func(i, j int) bool {
|
||||||
|
return sortorder.NaturalLess(subs[i].Node.DisplayName(), subs[j].Node.DisplayName())
|
||||||
|
})
|
||||||
|
return json.Marshal(subs)
|
||||||
|
}
|
||||||
|
|
||||||
type DanteFlow struct {
|
type DanteFlow struct {
|
||||||
Source *Node
|
TypeID string `json:"typeid"`
|
||||||
Subscribers map[*Node]*DanteFlowSubscriber
|
Source *Node `json:"source"`
|
||||||
|
Subscribers DanteSubscriberMap `json:"subscribers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DanteFlowSubscriber struct {
|
type DanteFlowSubscriber struct {
|
||||||
Node *Node
|
Node *Node `json:"node"`
|
||||||
Channels []string
|
Channels []string `json:"channels,omitempty"`
|
||||||
ChannelStatus map[string]DanteFlowStatus
|
ChannelStatus map[string]DanteFlowStatus `json:"channel_status,omitempty"`
|
||||||
LastSeen time.Time
|
LastSeen time.Time `json:"last_seen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DanteFlows struct {
|
type DanteFlows struct {
|
||||||
@@ -157,8 +172,9 @@ func (d *DanteFlows) Update(source, subscriber *Node, channelInfo string, flowSt
|
|||||||
flow := d.flows[source]
|
flow := d.flows[source]
|
||||||
if flow == nil {
|
if flow == nil {
|
||||||
flow = &DanteFlow{
|
flow = &DanteFlow{
|
||||||
|
TypeID: newTypeID("danteflow"),
|
||||||
Source: source,
|
Source: source,
|
||||||
Subscribers: map[*Node]*DanteFlowSubscriber{},
|
Subscribers: DanteSubscriberMap{},
|
||||||
}
|
}
|
||||||
d.flows[source] = flow
|
d.flows[source] = flow
|
||||||
}
|
}
|
||||||
@@ -437,6 +453,10 @@ func (s DanteFlowStatus) String() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s DanteFlowStatus) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(s.String())
|
||||||
|
}
|
||||||
|
|
||||||
type DanteSubscription struct {
|
type DanteSubscription struct {
|
||||||
RxChannel int
|
RxChannel int
|
||||||
TxDeviceName string
|
TxDeviceName string
|
||||||
@@ -866,7 +886,7 @@ func (t *Tendrils) probeDanteDeviceWithPort(ip net.IP, port int) {
|
|||||||
log.Printf("[dante] %s: multicast group %s -> tx device %q", ip, groupIP, sourceName)
|
log.Printf("[dante] %s: multicast group %s -> tx device %q", ip, groupIP, sourceName)
|
||||||
}
|
}
|
||||||
if sourceName == "" {
|
if sourceName == "" {
|
||||||
sourceName = (&MulticastGroup{IP: groupIP}).Name()
|
sourceName = multicastGroupName(groupIP)
|
||||||
}
|
}
|
||||||
sourceNode := t.nodes.GetOrCreateByName(sourceName)
|
sourceNode := t.nodes.GetOrCreateByName(sourceName)
|
||||||
subscriberNode := t.nodes.GetOrCreateByName(info.Name)
|
subscriberNode := t.nodes.GetOrCreateByName(info.Name)
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -7,9 +7,11 @@ require (
|
|||||||
github.com/google/gopacket v1.1.19
|
github.com/google/gopacket v1.1.19
|
||||||
github.com/gosnmp/gosnmp v1.42.1
|
github.com/gosnmp/gosnmp v1.42.1
|
||||||
github.com/miekg/dns v1.1.72
|
github.com/miekg/dns v1.1.72
|
||||||
|
go.jetify.com/typeid v1.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/gofrs/uuid/v5 v5.2.0 // indirect
|
||||||
golang.org/x/mod v0.31.0 // indirect
|
golang.org/x/mod v0.31.0 // indirect
|
||||||
golang.org/x/net v0.48.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
|
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
|
||||||
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
|
||||||
|
github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM=
|
||||||
|
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
@@ -14,6 +16,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
go.jetify.com/typeid v1.3.0 h1:fuWV7oxO4mSsgpxwhaVpFXgt0IfjogR29p+XAjDCVKY=
|
||||||
|
go.jetify.com/typeid v1.3.0/go.mod h1:CtVGyt2+TSp4Rq5+ARLvGsJqdNypKBAC6INQ9TLPlmk=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
@@ -36,5 +40,7 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
|
|||||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
140
http.go
Normal file
140
http.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package tendrils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/fvbommel/sortorder"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusResponse struct {
|
||||||
|
Nodes []*Node `json:"nodes"`
|
||||||
|
Links []*Link `json:"links"`
|
||||||
|
MulticastGroups []*MulticastGroupMembers `json:"multicast_groups"`
|
||||||
|
ArtNetNodes []*ArtNetNode `json:"artnet_nodes"`
|
||||||
|
DanteFlows []*DanteFlow `json:"dante_flows"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) startHTTPServer() {
|
||||||
|
if t.HTTPPort == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/api/status", t.handleAPIStatus)
|
||||||
|
mux.Handle("/", http.FileServer(http.Dir("static")))
|
||||||
|
|
||||||
|
log.Printf("[http] listening on %s", t.HTTPPort)
|
||||||
|
go func() {
|
||||||
|
if err := http.ListenAndServe(t.HTTPPort, mux); err != nil {
|
||||||
|
log.Printf("[ERROR] http server failed: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) handleAPIStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
status := t.GetStatus()
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) GetStatus() *StatusResponse {
|
||||||
|
return &StatusResponse{
|
||||||
|
Nodes: t.getNodes(),
|
||||||
|
Links: t.getLinks(),
|
||||||
|
MulticastGroups: t.getMulticastGroups(),
|
||||||
|
ArtNetNodes: t.getArtNetNodes(),
|
||||||
|
DanteFlows: t.getDanteFlows(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) getNodes() []*Node {
|
||||||
|
t.nodes.mu.RLock()
|
||||||
|
defer t.nodes.mu.RUnlock()
|
||||||
|
|
||||||
|
nodes := make([]*Node, 0, len(t.nodes.nodes))
|
||||||
|
for _, node := range t.nodes.nodes {
|
||||||
|
nodes = append(nodes, node)
|
||||||
|
}
|
||||||
|
sort.Slice(nodes, func(i, j int) bool {
|
||||||
|
return sortorder.NaturalLess(nodes[i].DisplayName(), nodes[j].DisplayName())
|
||||||
|
})
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) getLinks() []*Link {
|
||||||
|
t.nodes.mu.RLock()
|
||||||
|
defer t.nodes.mu.RUnlock()
|
||||||
|
|
||||||
|
links := t.nodes.getDirectLinks()
|
||||||
|
sort.Slice(links, func(i, j int) bool {
|
||||||
|
if links[i].NodeA.DisplayName() != links[j].NodeA.DisplayName() {
|
||||||
|
return sortorder.NaturalLess(links[i].NodeA.DisplayName(), links[j].NodeA.DisplayName())
|
||||||
|
}
|
||||||
|
if links[i].InterfaceA != links[j].InterfaceA {
|
||||||
|
return sortorder.NaturalLess(links[i].InterfaceA, links[j].InterfaceA)
|
||||||
|
}
|
||||||
|
if links[i].NodeB.DisplayName() != links[j].NodeB.DisplayName() {
|
||||||
|
return sortorder.NaturalLess(links[i].NodeB.DisplayName(), links[j].NodeB.DisplayName())
|
||||||
|
}
|
||||||
|
return sortorder.NaturalLess(links[i].InterfaceB, links[j].InterfaceB)
|
||||||
|
})
|
||||||
|
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) getMulticastGroups() []*MulticastGroupMembers {
|
||||||
|
t.nodes.mu.Lock()
|
||||||
|
t.nodes.expireMulticastMemberships()
|
||||||
|
t.nodes.mu.Unlock()
|
||||||
|
|
||||||
|
t.nodes.mu.RLock()
|
||||||
|
defer t.nodes.mu.RUnlock()
|
||||||
|
|
||||||
|
groups := make([]*MulticastGroupMembers, 0, len(t.nodes.multicastGroups))
|
||||||
|
for _, gm := range t.nodes.multicastGroups {
|
||||||
|
groups = append(groups, gm)
|
||||||
|
}
|
||||||
|
sort.Slice(groups, func(i, j int) bool {
|
||||||
|
return sortorder.NaturalLess(groups[i].Group.Name, groups[j].Group.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) getArtNetNodes() []*ArtNetNode {
|
||||||
|
t.artnet.Expire()
|
||||||
|
|
||||||
|
t.artnet.mu.RLock()
|
||||||
|
defer t.artnet.mu.RUnlock()
|
||||||
|
|
||||||
|
nodes := make([]*ArtNetNode, 0, len(t.artnet.nodes))
|
||||||
|
for _, node := range t.artnet.nodes {
|
||||||
|
nodes = append(nodes, node)
|
||||||
|
}
|
||||||
|
sort.Slice(nodes, func(i, j int) bool {
|
||||||
|
return sortorder.NaturalLess(nodes[i].Node.DisplayName(), nodes[j].Node.DisplayName())
|
||||||
|
})
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) getDanteFlows() []*DanteFlow {
|
||||||
|
t.danteFlows.Expire()
|
||||||
|
|
||||||
|
t.danteFlows.mu.RLock()
|
||||||
|
defer t.danteFlows.mu.RUnlock()
|
||||||
|
|
||||||
|
flows := make([]*DanteFlow, 0, len(t.danteFlows.flows))
|
||||||
|
for _, flow := range t.danteFlows.flows {
|
||||||
|
flows = append(flows, flow)
|
||||||
|
}
|
||||||
|
sort.Slice(flows, func(i, j int) bool {
|
||||||
|
return sortorder.NaturalLess(flows[i].Source.DisplayName(), flows[j].Source.DisplayName())
|
||||||
|
})
|
||||||
|
|
||||||
|
return flows
|
||||||
|
}
|
||||||
18
link.go
18
link.go
@@ -3,10 +3,11 @@ package tendrils
|
|||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
type Link struct {
|
type Link struct {
|
||||||
NodeA *Node
|
TypeID string `json:"typeid"`
|
||||||
InterfaceA string
|
NodeA *Node `json:"node_a"`
|
||||||
NodeB *Node
|
InterfaceA string `json:"interface_a,omitempty"`
|
||||||
InterfaceB string
|
NodeB *Node `json:"node_b"`
|
||||||
|
InterfaceB string `json:"interface_b,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Link) String() string {
|
func (l *Link) String() string {
|
||||||
@@ -33,8 +34,8 @@ func (n *Nodes) getDirectLinks() []*Link {
|
|||||||
macToNode := map[string]*Node{}
|
macToNode := map[string]*Node{}
|
||||||
for _, node := range n.nodes {
|
for _, node := range n.nodes {
|
||||||
for _, iface := range node.Interfaces {
|
for _, iface := range node.Interfaces {
|
||||||
if iface.MAC != nil {
|
if iface.MAC != "" {
|
||||||
macToNode[iface.MAC.String()] = node
|
macToNode[string(iface.MAC)] = node
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,10 +46,10 @@ func (n *Nodes) getDirectLinks() []*Link {
|
|||||||
for _, target := range n.nodes {
|
for _, target := range n.nodes {
|
||||||
seenMACs := map[string]bool{}
|
seenMACs := map[string]bool{}
|
||||||
for _, iface := range target.Interfaces {
|
for _, iface := range target.Interfaces {
|
||||||
if iface.MAC == nil {
|
if iface.MAC == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mac := iface.MAC.String()
|
mac := string(iface.MAC)
|
||||||
if seenMACs[mac] {
|
if seenMACs[mac] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -64,6 +65,7 @@ func (n *Nodes) getDirectLinks() []*Link {
|
|||||||
if !seen[key] {
|
if !seen[key] {
|
||||||
seen[key] = true
|
seen[key] = true
|
||||||
links = append(links, &Link{
|
links = append(links, &Link{
|
||||||
|
TypeID: newTypeID("link"),
|
||||||
NodeA: lastHop,
|
NodeA: lastHop,
|
||||||
InterfaceA: lastPort,
|
InterfaceA: lastPort,
|
||||||
NodeB: target,
|
NodeB: target,
|
||||||
|
|||||||
112
multicast.go
112
multicast.go
@@ -1,32 +1,74 @@
|
|||||||
package tendrils
|
package tendrils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fvbommel/sortorder"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MulticastGroup struct {
|
type MulticastGroup struct {
|
||||||
IP net.IP
|
Name string `json:"name"`
|
||||||
|
IP string `json:"ip"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MulticastMembership struct {
|
type MulticastMembership struct {
|
||||||
Node *Node
|
SourceIP string `json:"source_ip"`
|
||||||
LastSeen time.Time
|
Node *Node `json:"node,omitempty"`
|
||||||
|
LastSeen time.Time `json:"last_seen"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MulticastMembershipMap map[string]*MulticastMembership
|
||||||
|
|
||||||
|
func (m MulticastMembershipMap) MarshalJSON() ([]byte, error) {
|
||||||
|
members := make([]*MulticastMembership, 0, len(m))
|
||||||
|
for _, membership := range m {
|
||||||
|
members = append(members, membership)
|
||||||
|
}
|
||||||
|
sort.Slice(members, func(i, j int) bool {
|
||||||
|
nameI := members[i].SourceIP
|
||||||
|
if members[i].Node != nil && members[i].Node.DisplayName() != "" {
|
||||||
|
nameI = members[i].Node.DisplayName()
|
||||||
|
}
|
||||||
|
nameJ := members[j].SourceIP
|
||||||
|
if members[j].Node != nil && members[j].Node.DisplayName() != "" {
|
||||||
|
nameJ = members[j].Node.DisplayName()
|
||||||
|
}
|
||||||
|
return sortorder.NaturalLess(nameI, nameJ)
|
||||||
|
})
|
||||||
|
return json.Marshal(members)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MulticastGroupMembers struct {
|
type MulticastGroupMembers struct {
|
||||||
Group *MulticastGroup
|
TypeID string `json:"typeid"`
|
||||||
Members map[string]*MulticastMembership // source IP -> membership
|
Group *MulticastGroup `json:"group"`
|
||||||
|
Members MulticastMembershipMap `json:"members"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *MulticastGroup) Name() string {
|
func (g *MulticastGroup) IsDante() bool {
|
||||||
ip := g.IP.To4()
|
ip := net.ParseIP(g.IP).To4()
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
return g.IP.String()
|
return false
|
||||||
|
}
|
||||||
|
if ip[0] == 239 && ip[1] >= 69 && ip[1] <= 71 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if ip[0] == 239 && ip[1] == 253 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func multicastGroupName(ip net.IP) string {
|
||||||
|
ip4 := ip.To4()
|
||||||
|
if ip4 == nil {
|
||||||
|
return ip.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
switch g.IP.String() {
|
switch ip.String() {
|
||||||
case "224.0.0.251":
|
case "224.0.0.251":
|
||||||
return "mdns"
|
return "mdns"
|
||||||
case "224.0.1.129":
|
case "224.0.1.129":
|
||||||
@@ -49,41 +91,24 @@ func (g *MulticastGroup) Name() string {
|
|||||||
return "admin-scoped-broadcast"
|
return "admin-scoped-broadcast"
|
||||||
}
|
}
|
||||||
|
|
||||||
// sACN (239.255.x.x, universes 1-63999)
|
if ip4[0] == 239 && ip4[1] == 255 {
|
||||||
if ip[0] == 239 && ip[1] == 255 {
|
universe := int(ip4[2])*256 + int(ip4[3])
|
||||||
universe := int(ip[2])*256 + int(ip[3])
|
|
||||||
if universe >= 1 && universe <= 63999 {
|
if universe >= 1 && universe <= 63999 {
|
||||||
return fmt.Sprintf("sacn:%d", universe)
|
return fmt.Sprintf("sacn:%d", universe)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dante audio multicast (239.69-71.x.x)
|
if ip4[0] == 239 && ip4[1] >= 69 && ip4[1] <= 71 {
|
||||||
if ip[0] == 239 && ip[1] >= 69 && ip[1] <= 71 {
|
flowID := (int(ip4[1]-69) << 16) | (int(ip4[2]) << 8) | int(ip4[3])
|
||||||
flowID := (int(ip[1]-69) << 16) | (int(ip[2]) << 8) | int(ip[3])
|
|
||||||
return fmt.Sprintf("dante-mcast:%d", flowID)
|
return fmt.Sprintf("dante-mcast:%d", flowID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dante AV multicast (239.253.x.x)
|
if ip4[0] == 239 && ip4[1] == 253 {
|
||||||
if ip[0] == 239 && ip[1] == 253 {
|
flowID := (int(ip4[2]) << 8) | int(ip4[3])
|
||||||
flowID := (int(ip[2]) << 8) | int(ip[3])
|
|
||||||
return fmt.Sprintf("dante-av:%d", flowID)
|
return fmt.Sprintf("dante-av:%d", flowID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return g.IP.String()
|
return ip.String()
|
||||||
}
|
|
||||||
|
|
||||||
func (g *MulticastGroup) IsDante() bool {
|
|
||||||
ip := g.IP.To4()
|
|
||||||
if ip == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if ip[0] == 239 && ip[1] >= 69 && ip[1] <= 71 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if ip[0] == 239 && ip[1] == 253 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
|
func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
|
||||||
@@ -98,16 +123,25 @@ func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
|
|||||||
gm := n.multicastGroups[groupKey]
|
gm := n.multicastGroups[groupKey]
|
||||||
if gm == nil {
|
if gm == nil {
|
||||||
gm = &MulticastGroupMembers{
|
gm = &MulticastGroupMembers{
|
||||||
Group: &MulticastGroup{IP: groupIP},
|
TypeID: newTypeID("mcastgroup"),
|
||||||
Members: map[string]*MulticastMembership{},
|
Group: &MulticastGroup{
|
||||||
|
Name: multicastGroupName(groupIP),
|
||||||
|
IP: groupKey,
|
||||||
|
},
|
||||||
|
Members: MulticastMembershipMap{},
|
||||||
}
|
}
|
||||||
n.multicastGroups[groupKey] = gm
|
n.multicastGroups[groupKey] = gm
|
||||||
}
|
}
|
||||||
|
|
||||||
gm.Members[sourceKey] = &MulticastMembership{
|
membership := gm.Members[sourceKey]
|
||||||
Node: node,
|
if membership == nil {
|
||||||
LastSeen: time.Now(),
|
membership = &MulticastMembership{
|
||||||
|
SourceIP: sourceKey,
|
||||||
|
}
|
||||||
|
gm.Members[sourceKey] = membership
|
||||||
}
|
}
|
||||||
|
membership.Node = node
|
||||||
|
membership.LastSeen = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
|
func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
|
||||||
@@ -137,7 +171,7 @@ func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, exists := gm.Members[deviceKey]; exists {
|
if _, exists := gm.Members[deviceKey]; exists {
|
||||||
groups = append(groups, gm.Group.IP)
|
groups = append(groups, net.ParseIP(gm.Group.IP))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return groups
|
return groups
|
||||||
|
|||||||
48
nodes.go
48
nodes.go
@@ -152,7 +152,8 @@ func (n *Nodes) createNode() int {
|
|||||||
targetID := n.nextID
|
targetID := n.nextID
|
||||||
n.nextID++
|
n.nextID++
|
||||||
node := &Node{
|
node := &Node{
|
||||||
Interfaces: map[string]*Interface{},
|
TypeID: newTypeID("node"),
|
||||||
|
Interfaces: InterfaceMap{},
|
||||||
MACTable: map[string]string{},
|
MACTable: map[string]string{},
|
||||||
pollTrigger: make(chan struct{}, 1),
|
pollTrigger: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
@@ -172,10 +173,10 @@ func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, i
|
|||||||
|
|
||||||
if nodeName != "" {
|
if nodeName != "" {
|
||||||
if node.Names == nil {
|
if node.Names == nil {
|
||||||
node.Names = map[string]bool{}
|
node.Names = NameSet{}
|
||||||
}
|
}
|
||||||
if !node.Names[nodeName] {
|
if !node.Names.Has(nodeName) {
|
||||||
node.Names[nodeName] = true
|
node.Names.Add(nodeName)
|
||||||
n.nameIndex[nodeName] = nodeID
|
n.nameIndex[nodeName] = nodeID
|
||||||
added = append(added, "name="+nodeName)
|
added = append(added, "name="+nodeName)
|
||||||
}
|
}
|
||||||
@@ -194,10 +195,10 @@ func (n *Nodes) updateNodeIPs(node *Node, nodeID int, ips []net.IP) []string {
|
|||||||
n.ipIndex[ipKey] = nodeID
|
n.ipIndex[ipKey] = nodeID
|
||||||
iface, exists := node.Interfaces[ipKey]
|
iface, exists := node.Interfaces[ipKey]
|
||||||
if !exists {
|
if !exists {
|
||||||
iface = &Interface{IPs: map[string]net.IP{}}
|
iface = &Interface{IPs: IPSet{}}
|
||||||
node.Interfaces[ipKey] = iface
|
node.Interfaces[ipKey] = iface
|
||||||
}
|
}
|
||||||
iface.IPs[ipKey] = ip
|
iface.IPs.Add(ip)
|
||||||
added = append(added, "ip="+ipKey)
|
added = append(added, "ip="+ipKey)
|
||||||
go n.t.requestARP(ip)
|
go n.t.requestARP(ip)
|
||||||
}
|
}
|
||||||
@@ -273,8 +274,8 @@ func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr
|
|||||||
if !exists {
|
if !exists {
|
||||||
iface = &Interface{
|
iface = &Interface{
|
||||||
Name: ifaceName,
|
Name: ifaceName,
|
||||||
MAC: mac,
|
MAC: MACFrom(mac),
|
||||||
IPs: map[string]net.IP{},
|
IPs: IPSet{},
|
||||||
}
|
}
|
||||||
node.Interfaces[ifaceKey] = iface
|
node.Interfaces[ifaceKey] = iface
|
||||||
added = append(added, "iface="+ifaceKey)
|
added = append(added, "iface="+ifaceKey)
|
||||||
@@ -286,10 +287,10 @@ func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr
|
|||||||
|
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
ipKey := ip.String()
|
ipKey := ip.String()
|
||||||
if _, exists := iface.IPs[ipKey]; !exists {
|
if !iface.IPs.Has(ipKey) {
|
||||||
added = append(added, "ip="+ipKey)
|
added = append(added, "ip="+ipKey)
|
||||||
}
|
}
|
||||||
iface.IPs[ipKey] = ip
|
iface.IPs.Add(ip)
|
||||||
n.ipIndex[ipKey] = nodeID
|
n.ipIndex[ipKey] = nodeID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +301,7 @@ func (n *Nodes) findOrCreateInterface(node *Node, macKey, ifaceName, ifaceKey st
|
|||||||
var added []string
|
var added []string
|
||||||
|
|
||||||
if ifaceName != "" {
|
if ifaceName != "" {
|
||||||
if oldIface, oldExists := node.Interfaces[macKey]; oldExists && oldIface.MAC.String() == macKey {
|
if oldIface, oldExists := node.Interfaces[macKey]; oldExists && string(oldIface.MAC) == macKey {
|
||||||
oldIface.Name = ifaceName
|
oldIface.Name = ifaceName
|
||||||
delete(node.Interfaces, macKey)
|
delete(node.Interfaces, macKey)
|
||||||
node.Interfaces[ifaceKey] = oldIface
|
node.Interfaces[ifaceKey] = oldIface
|
||||||
@@ -308,7 +309,7 @@ func (n *Nodes) findOrCreateInterface(node *Node, macKey, ifaceName, ifaceKey st
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, existing := range node.Interfaces {
|
for _, existing := range node.Interfaces {
|
||||||
if existing.MAC.String() == macKey {
|
if string(existing.MAC) == macKey {
|
||||||
return existing, true, added
|
return existing, true, added
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,20 +365,20 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
|
|||||||
|
|
||||||
for name := range merge.Names {
|
for name := range merge.Names {
|
||||||
if keep.Names == nil {
|
if keep.Names == nil {
|
||||||
keep.Names = map[string]bool{}
|
keep.Names = NameSet{}
|
||||||
}
|
}
|
||||||
keep.Names[name] = true
|
keep.Names.Add(name)
|
||||||
n.nameIndex[name] = keepID
|
n.nameIndex[name] = keepID
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, iface := range merge.Interfaces {
|
for _, iface := range merge.Interfaces {
|
||||||
var ips []net.IP
|
var ips []net.IP
|
||||||
for _, ip := range iface.IPs {
|
for ipStr := range iface.IPs {
|
||||||
ips = append(ips, ip)
|
ips = append(ips, net.ParseIP(ipStr))
|
||||||
}
|
}
|
||||||
if iface.MAC != nil {
|
if iface.MAC != "" {
|
||||||
n.updateNodeInterface(keep, keepID, iface.MAC, ips, iface.Name)
|
n.updateNodeInterface(keep, keepID, iface.MAC.Parse(), ips, iface.Name)
|
||||||
n.macIndex[iface.MAC.String()] = keepID
|
n.macIndex[string(iface.MAC)] = keepID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,8 +452,9 @@ func (n *Nodes) GetOrCreateByName(name string) *Node {
|
|||||||
targetID := n.nextID
|
targetID := n.nextID
|
||||||
n.nextID++
|
n.nextID++
|
||||||
node := &Node{
|
node := &Node{
|
||||||
Names: map[string]bool{name: true},
|
TypeID: newTypeID("node"),
|
||||||
Interfaces: map[string]*Interface{},
|
Names: NameSet{name: true},
|
||||||
|
Interfaces: InterfaceMap{},
|
||||||
MACTable: map[string]string{},
|
MACTable: map[string]string{},
|
||||||
pollTrigger: make(chan struct{}, 1),
|
pollTrigger: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
@@ -593,7 +595,7 @@ func (n *Nodes) LogAll() {
|
|||||||
groups = append(groups, gm)
|
groups = append(groups, gm)
|
||||||
}
|
}
|
||||||
sort.Slice(groups, func(i, j int) bool {
|
sort.Slice(groups, func(i, j int) bool {
|
||||||
return sortorder.NaturalLess(groups[i].Group.Name(), groups[j].Group.Name())
|
return sortorder.NaturalLess(groups[i].Group.Name, groups[j].Group.Name)
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Printf("[sigusr1] ================ %d multicast groups ================", len(groups))
|
log.Printf("[sigusr1] ================ %d multicast groups ================", len(groups))
|
||||||
@@ -614,7 +616,7 @@ func (n *Nodes) LogAll() {
|
|||||||
sort.Slice(memberNames, func(i, j int) bool {
|
sort.Slice(memberNames, func(i, j int) bool {
|
||||||
return sortorder.NaturalLess(memberNames[i], memberNames[j])
|
return sortorder.NaturalLess(memberNames[i], memberNames[j])
|
||||||
})
|
})
|
||||||
log.Printf("[sigusr1] %s: %s", gm.Group.Name(), strings.Join(memberNames, ", "))
|
log.Printf("[sigusr1] %s: %s", gm.Group.Name, strings.Join(memberNames, ", "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
0
static/.gitkeep
Normal file
0
static/.gitkeep
Normal file
@@ -58,6 +58,7 @@ type Tendrils struct {
|
|||||||
DebugBMD bool
|
DebugBMD bool
|
||||||
DebugShure bool
|
DebugShure bool
|
||||||
DebugYamaha bool
|
DebugYamaha bool
|
||||||
|
HTTPPort string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Tendrils {
|
func New() *Tendrils {
|
||||||
@@ -83,6 +84,7 @@ func (t *Tendrils) Run() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
t.populateLocalAddresses()
|
t.populateLocalAddresses()
|
||||||
|
t.startHTTPServer()
|
||||||
|
|
||||||
if !t.DisableARP {
|
if !t.DisableARP {
|
||||||
t.readARPTable()
|
t.readARPTable()
|
||||||
@@ -239,8 +241,9 @@ func (t *Tendrils) pollNode(node *Node) {
|
|||||||
t.nodes.mu.RLock()
|
t.nodes.mu.RLock()
|
||||||
var ips []net.IP
|
var ips []net.IP
|
||||||
for _, iface := range node.Interfaces {
|
for _, iface := range node.Interfaces {
|
||||||
for _, ip := range iface.IPs {
|
for ipStr := range iface.IPs {
|
||||||
if ip.To4() != nil {
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip != nil && ip.To4() != nil {
|
||||||
ips = append(ips, ip)
|
ips = append(ips, ip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
139
types.go
139
types.go
@@ -1,62 +1,143 @@
|
|||||||
package tendrils
|
package tendrils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fvbommel/sortorder"
|
"github.com/fvbommel/sortorder"
|
||||||
|
"go.jetify.com/typeid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func newTypeID(prefix string) string {
|
||||||
|
tid, _ := typeid.WithPrefix(prefix)
|
||||||
|
return tid.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type MAC string
|
||||||
|
|
||||||
|
func (m MAC) Parse() net.HardwareAddr {
|
||||||
|
mac, _ := net.ParseMAC(string(m))
|
||||||
|
return mac
|
||||||
|
}
|
||||||
|
|
||||||
|
func MACFrom(mac net.HardwareAddr) MAC {
|
||||||
|
if mac == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return MAC(mac.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
type IPSet map[string]bool
|
||||||
|
|
||||||
|
func (s IPSet) MarshalJSON() ([]byte, error) {
|
||||||
|
ips := make([]string, 0, len(s))
|
||||||
|
for ip := range s {
|
||||||
|
ips = append(ips, ip)
|
||||||
|
}
|
||||||
|
sort.Strings(ips)
|
||||||
|
return json.Marshal(ips)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s IPSet) Add(ip net.IP) {
|
||||||
|
s[ip.String()] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s IPSet) Has(ip string) bool {
|
||||||
|
return s[ip]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s IPSet) Slice() []string {
|
||||||
|
ips := make([]string, 0, len(s))
|
||||||
|
for ip := range s {
|
||||||
|
ips = append(ips, ip)
|
||||||
|
}
|
||||||
|
sort.Strings(ips)
|
||||||
|
return ips
|
||||||
|
}
|
||||||
|
|
||||||
|
type NameSet map[string]bool
|
||||||
|
|
||||||
|
func (s NameSet) MarshalJSON() ([]byte, error) {
|
||||||
|
names := make([]string, 0, len(s))
|
||||||
|
for name := range s {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
return json.Marshal(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s NameSet) Add(name string) {
|
||||||
|
s[name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s NameSet) Has(name string) bool {
|
||||||
|
return s[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceMap map[string]*Interface
|
||||||
|
|
||||||
|
func (m InterfaceMap) MarshalJSON() ([]byte, error) {
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
|
return sortorder.NaturalLess(keys[i], keys[j])
|
||||||
|
})
|
||||||
|
ifaces := make([]*Interface, 0, len(m))
|
||||||
|
for _, k := range keys {
|
||||||
|
ifaces = append(ifaces, m[k])
|
||||||
|
}
|
||||||
|
return json.Marshal(ifaces)
|
||||||
|
}
|
||||||
|
|
||||||
type Interface struct {
|
type Interface struct {
|
||||||
Name string
|
Name string `json:"name,omitempty"`
|
||||||
MAC net.HardwareAddr
|
MAC MAC `json:"mac"`
|
||||||
IPs map[string]net.IP
|
IPs IPSet `json:"ips"`
|
||||||
Stats *InterfaceStats
|
Stats *InterfaceStats `json:"stats,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceStats struct {
|
type InterfaceStats struct {
|
||||||
Speed uint64 // bits per second
|
Speed uint64 `json:"speed,omitempty"`
|
||||||
InErrors uint64
|
InErrors uint64 `json:"in_errors,omitempty"`
|
||||||
OutErrors uint64
|
OutErrors uint64 `json:"out_errors,omitempty"`
|
||||||
PoE *PoEStats
|
PoE *PoEStats `json:"poe,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PoEStats struct {
|
type PoEStats struct {
|
||||||
Power float64 // watts in use
|
Power float64 `json:"power"`
|
||||||
MaxPower float64 // watts allocated/negotiated
|
MaxPower float64 `json:"max_power"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PoEBudget struct {
|
type PoEBudget struct {
|
||||||
Power float64 // watts in use
|
Power float64 `json:"power"`
|
||||||
MaxPower float64 // watts total budget
|
MaxPower float64 `json:"max_power"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Names map[string]bool
|
TypeID string `json:"typeid"`
|
||||||
Interfaces map[string]*Interface
|
Names NameSet `json:"names"`
|
||||||
MACTable map[string]string // peer MAC -> local interface name
|
Interfaces InterfaceMap `json:"interfaces"`
|
||||||
PoEBudget *PoEBudget
|
MACTable map[string]string `json:"-"`
|
||||||
IsDanteClockMaster bool
|
MACTableSize int `json:"mac_table_size,omitempty"`
|
||||||
DanteTxChannels string
|
PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
|
||||||
|
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"`
|
||||||
|
DanteTxChannels string `json:"dante_tx_channels,omitempty"`
|
||||||
pollTrigger chan struct{}
|
pollTrigger chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Interface) String() string {
|
func (i *Interface) String() string {
|
||||||
var ips []string
|
|
||||||
for _, ip := range i.IPs {
|
|
||||||
ips = append(ips, ip.String())
|
|
||||||
}
|
|
||||||
sort.Strings(ips)
|
|
||||||
|
|
||||||
var parts []string
|
var parts []string
|
||||||
parts = append(parts, i.MAC.String())
|
parts = append(parts, string(i.MAC))
|
||||||
if i.Name != "" {
|
if i.Name != "" {
|
||||||
parts = append(parts, fmt.Sprintf("(%s)", i.Name))
|
parts = append(parts, fmt.Sprintf("(%s)", i.Name))
|
||||||
}
|
}
|
||||||
if len(ips) > 0 {
|
if len(i.IPs) > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("%v", ips))
|
parts = append(parts, fmt.Sprintf("%v", i.IPs.Slice()))
|
||||||
}
|
}
|
||||||
if i.Stats != nil {
|
if i.Stats != nil {
|
||||||
parts = append(parts, i.Stats.String())
|
parts = append(parts, i.Stats.String())
|
||||||
@@ -135,8 +216,8 @@ func (n *Node) DisplayName() string {
|
|||||||
|
|
||||||
func (n *Node) FirstMAC() string {
|
func (n *Node) FirstMAC() string {
|
||||||
for _, iface := range n.Interfaces {
|
for _, iface := range n.Interfaces {
|
||||||
if iface.MAC != nil {
|
if iface.MAC != "" {
|
||||||
return iface.MAC.String()
|
return string(iface.MAC)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "??"
|
return "??"
|
||||||
|
|||||||
Reference in New Issue
Block a user