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
|
||||
- Always push after commiting
|
||||
- Use git add -A so you don't miss files when committing
|
||||
- Never use go build -- use go run instead
|
||||
- DO NOT commit unless asked to
|
||||
- Never use go build -- use go vet instead
|
||||
- DO NOT commit unless asked to
|
||||
|
||||
10
artnet.go
10
artnet.go
@@ -23,10 +23,11 @@ const (
|
||||
)
|
||||
|
||||
type ArtNetNode struct {
|
||||
Node *Node
|
||||
Inputs []int
|
||||
Outputs []int
|
||||
LastSeen time.Time
|
||||
TypeID string `json:"typeid"`
|
||||
Node *Node `json:"node"`
|
||||
Inputs []int `json:"inputs,omitempty"`
|
||||
Outputs []int `json:"outputs,omitempty"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
}
|
||||
|
||||
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()
|
||||
} else {
|
||||
a.nodes[node] = &ArtNetNode{
|
||||
TypeID: newTypeID("artnetnode"),
|
||||
Node: node,
|
||||
Inputs: inputs,
|
||||
Outputs: outputs,
|
||||
|
||||
@@ -30,6 +30,7 @@ func main() {
|
||||
debugBMD := flag.Bool("debug-bmd", false, "debug Blackmagic discovery")
|
||||
debugShure := flag.Bool("debug-shure", false, "debug Shure discovery")
|
||||
debugYamaha := flag.Bool("debug-yamaha", false, "debug Yamaha discovery")
|
||||
httpPort := flag.String("http", ":80", "HTTP server port (empty to disable)")
|
||||
flag.Parse()
|
||||
|
||||
t := tendrils.New()
|
||||
@@ -56,5 +57,6 @@ func main() {
|
||||
t.DebugBMD = *debugBMD
|
||||
t.DebugShure = *debugShure
|
||||
t.DebugYamaha = *debugYamaha
|
||||
t.HTTPPort = *httpPort
|
||||
t.Run()
|
||||
}
|
||||
|
||||
36
dante.go
36
dante.go
@@ -3,6 +3,7 @@ package tendrils
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@@ -118,16 +119,30 @@ func (n *Nodes) GetDanteTxDeviceInGroup(groupIP net.IP) string {
|
||||
|
||||
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 {
|
||||
Source *Node
|
||||
Subscribers map[*Node]*DanteFlowSubscriber
|
||||
TypeID string `json:"typeid"`
|
||||
Source *Node `json:"source"`
|
||||
Subscribers DanteSubscriberMap `json:"subscribers"`
|
||||
}
|
||||
|
||||
type DanteFlowSubscriber struct {
|
||||
Node *Node
|
||||
Channels []string
|
||||
ChannelStatus map[string]DanteFlowStatus
|
||||
LastSeen time.Time
|
||||
Node *Node `json:"node"`
|
||||
Channels []string `json:"channels,omitempty"`
|
||||
ChannelStatus map[string]DanteFlowStatus `json:"channel_status,omitempty"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
}
|
||||
|
||||
type DanteFlows struct {
|
||||
@@ -157,8 +172,9 @@ func (d *DanteFlows) Update(source, subscriber *Node, channelInfo string, flowSt
|
||||
flow := d.flows[source]
|
||||
if flow == nil {
|
||||
flow = &DanteFlow{
|
||||
TypeID: newTypeID("danteflow"),
|
||||
Source: source,
|
||||
Subscribers: map[*Node]*DanteFlowSubscriber{},
|
||||
Subscribers: DanteSubscriberMap{},
|
||||
}
|
||||
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 {
|
||||
RxChannel int
|
||||
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)
|
||||
}
|
||||
if sourceName == "" {
|
||||
sourceName = (&MulticastGroup{IP: groupIP}).Name()
|
||||
sourceName = multicastGroupName(groupIP)
|
||||
}
|
||||
sourceNode := t.nodes.GetOrCreateByName(sourceName)
|
||||
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/gosnmp/gosnmp v1.42.1
|
||||
github.com/miekg/dns v1.1.72
|
||||
go.jetify.com/typeid v1.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gofrs/uuid/v5 v5.2.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.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/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/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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
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/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
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/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"
|
||||
|
||||
type Link struct {
|
||||
NodeA *Node
|
||||
InterfaceA string
|
||||
NodeB *Node
|
||||
InterfaceB string
|
||||
TypeID string `json:"typeid"`
|
||||
NodeA *Node `json:"node_a"`
|
||||
InterfaceA string `json:"interface_a,omitempty"`
|
||||
NodeB *Node `json:"node_b"`
|
||||
InterfaceB string `json:"interface_b,omitempty"`
|
||||
}
|
||||
|
||||
func (l *Link) String() string {
|
||||
@@ -33,8 +34,8 @@ func (n *Nodes) getDirectLinks() []*Link {
|
||||
macToNode := map[string]*Node{}
|
||||
for _, node := range n.nodes {
|
||||
for _, iface := range node.Interfaces {
|
||||
if iface.MAC != nil {
|
||||
macToNode[iface.MAC.String()] = node
|
||||
if iface.MAC != "" {
|
||||
macToNode[string(iface.MAC)] = node
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,10 +46,10 @@ func (n *Nodes) getDirectLinks() []*Link {
|
||||
for _, target := range n.nodes {
|
||||
seenMACs := map[string]bool{}
|
||||
for _, iface := range target.Interfaces {
|
||||
if iface.MAC == nil {
|
||||
if iface.MAC == "" {
|
||||
continue
|
||||
}
|
||||
mac := iface.MAC.String()
|
||||
mac := string(iface.MAC)
|
||||
if seenMACs[mac] {
|
||||
continue
|
||||
}
|
||||
@@ -64,6 +65,7 @@ func (n *Nodes) getDirectLinks() []*Link {
|
||||
if !seen[key] {
|
||||
seen[key] = true
|
||||
links = append(links, &Link{
|
||||
TypeID: newTypeID("link"),
|
||||
NodeA: lastHop,
|
||||
InterfaceA: lastPort,
|
||||
NodeB: target,
|
||||
|
||||
112
multicast.go
112
multicast.go
@@ -1,32 +1,74 @@
|
||||
package tendrils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/fvbommel/sortorder"
|
||||
)
|
||||
|
||||
type MulticastGroup struct {
|
||||
IP net.IP
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
type MulticastMembership struct {
|
||||
Node *Node
|
||||
LastSeen time.Time
|
||||
SourceIP string `json:"source_ip"`
|
||||
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 {
|
||||
Group *MulticastGroup
|
||||
Members map[string]*MulticastMembership // source IP -> membership
|
||||
TypeID string `json:"typeid"`
|
||||
Group *MulticastGroup `json:"group"`
|
||||
Members MulticastMembershipMap `json:"members"`
|
||||
}
|
||||
|
||||
func (g *MulticastGroup) Name() string {
|
||||
ip := g.IP.To4()
|
||||
func (g *MulticastGroup) IsDante() bool {
|
||||
ip := net.ParseIP(g.IP).To4()
|
||||
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":
|
||||
return "mdns"
|
||||
case "224.0.1.129":
|
||||
@@ -49,41 +91,24 @@ func (g *MulticastGroup) Name() string {
|
||||
return "admin-scoped-broadcast"
|
||||
}
|
||||
|
||||
// sACN (239.255.x.x, universes 1-63999)
|
||||
if ip[0] == 239 && ip[1] == 255 {
|
||||
universe := int(ip[2])*256 + int(ip[3])
|
||||
if ip4[0] == 239 && ip4[1] == 255 {
|
||||
universe := int(ip4[2])*256 + int(ip4[3])
|
||||
if universe >= 1 && universe <= 63999 {
|
||||
return fmt.Sprintf("sacn:%d", universe)
|
||||
}
|
||||
}
|
||||
|
||||
// Dante audio multicast (239.69-71.x.x)
|
||||
if ip[0] == 239 && ip[1] >= 69 && ip[1] <= 71 {
|
||||
flowID := (int(ip[1]-69) << 16) | (int(ip[2]) << 8) | int(ip[3])
|
||||
if ip4[0] == 239 && ip4[1] >= 69 && ip4[1] <= 71 {
|
||||
flowID := (int(ip4[1]-69) << 16) | (int(ip4[2]) << 8) | int(ip4[3])
|
||||
return fmt.Sprintf("dante-mcast:%d", flowID)
|
||||
}
|
||||
|
||||
// Dante AV multicast (239.253.x.x)
|
||||
if ip[0] == 239 && ip[1] == 253 {
|
||||
flowID := (int(ip[2]) << 8) | int(ip[3])
|
||||
if ip4[0] == 239 && ip4[1] == 253 {
|
||||
flowID := (int(ip4[2]) << 8) | int(ip4[3])
|
||||
return fmt.Sprintf("dante-av:%d", flowID)
|
||||
}
|
||||
|
||||
return g.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
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
|
||||
@@ -98,16 +123,25 @@ func (n *Nodes) UpdateMulticastMembership(sourceIP, groupIP net.IP) {
|
||||
gm := n.multicastGroups[groupKey]
|
||||
if gm == nil {
|
||||
gm = &MulticastGroupMembers{
|
||||
Group: &MulticastGroup{IP: groupIP},
|
||||
Members: map[string]*MulticastMembership{},
|
||||
TypeID: newTypeID("mcastgroup"),
|
||||
Group: &MulticastGroup{
|
||||
Name: multicastGroupName(groupIP),
|
||||
IP: groupKey,
|
||||
},
|
||||
Members: MulticastMembershipMap{},
|
||||
}
|
||||
n.multicastGroups[groupKey] = gm
|
||||
}
|
||||
|
||||
gm.Members[sourceKey] = &MulticastMembership{
|
||||
Node: node,
|
||||
LastSeen: time.Now(),
|
||||
membership := gm.Members[sourceKey]
|
||||
if membership == nil {
|
||||
membership = &MulticastMembership{
|
||||
SourceIP: sourceKey,
|
||||
}
|
||||
gm.Members[sourceKey] = membership
|
||||
}
|
||||
membership.Node = node
|
||||
membership.LastSeen = time.Now()
|
||||
}
|
||||
|
||||
func (n *Nodes) RemoveMulticastMembership(sourceIP, groupIP net.IP) {
|
||||
@@ -137,7 +171,7 @@ func (n *Nodes) GetDanteMulticastGroups(deviceIP net.IP) []net.IP {
|
||||
continue
|
||||
}
|
||||
if _, exists := gm.Members[deviceKey]; exists {
|
||||
groups = append(groups, gm.Group.IP)
|
||||
groups = append(groups, net.ParseIP(gm.Group.IP))
|
||||
}
|
||||
}
|
||||
return groups
|
||||
|
||||
48
nodes.go
48
nodes.go
@@ -152,7 +152,8 @@ func (n *Nodes) createNode() int {
|
||||
targetID := n.nextID
|
||||
n.nextID++
|
||||
node := &Node{
|
||||
Interfaces: map[string]*Interface{},
|
||||
TypeID: newTypeID("node"),
|
||||
Interfaces: InterfaceMap{},
|
||||
MACTable: map[string]string{},
|
||||
pollTrigger: make(chan struct{}, 1),
|
||||
}
|
||||
@@ -172,10 +173,10 @@ func (n *Nodes) applyNodeUpdates(node *Node, nodeID int, mac net.HardwareAddr, i
|
||||
|
||||
if nodeName != "" {
|
||||
if node.Names == nil {
|
||||
node.Names = map[string]bool{}
|
||||
node.Names = NameSet{}
|
||||
}
|
||||
if !node.Names[nodeName] {
|
||||
node.Names[nodeName] = true
|
||||
if !node.Names.Has(nodeName) {
|
||||
node.Names.Add(nodeName)
|
||||
n.nameIndex[nodeName] = nodeID
|
||||
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
|
||||
iface, exists := node.Interfaces[ipKey]
|
||||
if !exists {
|
||||
iface = &Interface{IPs: map[string]net.IP{}}
|
||||
iface = &Interface{IPs: IPSet{}}
|
||||
node.Interfaces[ipKey] = iface
|
||||
}
|
||||
iface.IPs[ipKey] = ip
|
||||
iface.IPs.Add(ip)
|
||||
added = append(added, "ip="+ipKey)
|
||||
go n.t.requestARP(ip)
|
||||
}
|
||||
@@ -273,8 +274,8 @@ func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr
|
||||
if !exists {
|
||||
iface = &Interface{
|
||||
Name: ifaceName,
|
||||
MAC: mac,
|
||||
IPs: map[string]net.IP{},
|
||||
MAC: MACFrom(mac),
|
||||
IPs: IPSet{},
|
||||
}
|
||||
node.Interfaces[ifaceKey] = iface
|
||||
added = append(added, "iface="+ifaceKey)
|
||||
@@ -286,10 +287,10 @@ func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr
|
||||
|
||||
for _, ip := range ips {
|
||||
ipKey := ip.String()
|
||||
if _, exists := iface.IPs[ipKey]; !exists {
|
||||
if !iface.IPs.Has(ipKey) {
|
||||
added = append(added, "ip="+ipKey)
|
||||
}
|
||||
iface.IPs[ipKey] = ip
|
||||
iface.IPs.Add(ip)
|
||||
n.ipIndex[ipKey] = nodeID
|
||||
}
|
||||
|
||||
@@ -300,7 +301,7 @@ func (n *Nodes) findOrCreateInterface(node *Node, macKey, ifaceName, ifaceKey st
|
||||
var added []string
|
||||
|
||||
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
|
||||
delete(node.Interfaces, macKey)
|
||||
node.Interfaces[ifaceKey] = oldIface
|
||||
@@ -308,7 +309,7 @@ func (n *Nodes) findOrCreateInterface(node *Node, macKey, ifaceName, ifaceKey st
|
||||
}
|
||||
} else {
|
||||
for _, existing := range node.Interfaces {
|
||||
if existing.MAC.String() == macKey {
|
||||
if string(existing.MAC) == macKey {
|
||||
return existing, true, added
|
||||
}
|
||||
}
|
||||
@@ -364,20 +365,20 @@ func (n *Nodes) mergeNodes(keepID, mergeID int) {
|
||||
|
||||
for name := range merge.Names {
|
||||
if keep.Names == nil {
|
||||
keep.Names = map[string]bool{}
|
||||
keep.Names = NameSet{}
|
||||
}
|
||||
keep.Names[name] = true
|
||||
keep.Names.Add(name)
|
||||
n.nameIndex[name] = keepID
|
||||
}
|
||||
|
||||
for _, iface := range merge.Interfaces {
|
||||
var ips []net.IP
|
||||
for _, ip := range iface.IPs {
|
||||
ips = append(ips, ip)
|
||||
for ipStr := range iface.IPs {
|
||||
ips = append(ips, net.ParseIP(ipStr))
|
||||
}
|
||||
if iface.MAC != nil {
|
||||
n.updateNodeInterface(keep, keepID, iface.MAC, ips, iface.Name)
|
||||
n.macIndex[iface.MAC.String()] = keepID
|
||||
if iface.MAC != "" {
|
||||
n.updateNodeInterface(keep, keepID, iface.MAC.Parse(), ips, iface.Name)
|
||||
n.macIndex[string(iface.MAC)] = keepID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,8 +452,9 @@ func (n *Nodes) GetOrCreateByName(name string) *Node {
|
||||
targetID := n.nextID
|
||||
n.nextID++
|
||||
node := &Node{
|
||||
Names: map[string]bool{name: true},
|
||||
Interfaces: map[string]*Interface{},
|
||||
TypeID: newTypeID("node"),
|
||||
Names: NameSet{name: true},
|
||||
Interfaces: InterfaceMap{},
|
||||
MACTable: map[string]string{},
|
||||
pollTrigger: make(chan struct{}, 1),
|
||||
}
|
||||
@@ -593,7 +595,7 @@ func (n *Nodes) LogAll() {
|
||||
groups = append(groups, gm)
|
||||
}
|
||||
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))
|
||||
@@ -614,7 +616,7 @@ func (n *Nodes) LogAll() {
|
||||
sort.Slice(memberNames, func(i, j int) bool {
|
||||
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
|
||||
DebugShure bool
|
||||
DebugYamaha bool
|
||||
HTTPPort string
|
||||
}
|
||||
|
||||
func New() *Tendrils {
|
||||
@@ -83,6 +84,7 @@ func (t *Tendrils) Run() {
|
||||
}()
|
||||
|
||||
t.populateLocalAddresses()
|
||||
t.startHTTPServer()
|
||||
|
||||
if !t.DisableARP {
|
||||
t.readARPTable()
|
||||
@@ -239,8 +241,9 @@ func (t *Tendrils) pollNode(node *Node) {
|
||||
t.nodes.mu.RLock()
|
||||
var ips []net.IP
|
||||
for _, iface := range node.Interfaces {
|
||||
for _, ip := range iface.IPs {
|
||||
if ip.To4() != nil {
|
||||
for ipStr := range iface.IPs {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip != nil && ip.To4() != nil {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
|
||||
139
types.go
139
types.go
@@ -1,62 +1,143 @@
|
||||
package tendrils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"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 {
|
||||
Name string
|
||||
MAC net.HardwareAddr
|
||||
IPs map[string]net.IP
|
||||
Stats *InterfaceStats
|
||||
Name string `json:"name,omitempty"`
|
||||
MAC MAC `json:"mac"`
|
||||
IPs IPSet `json:"ips"`
|
||||
Stats *InterfaceStats `json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
type InterfaceStats struct {
|
||||
Speed uint64 // bits per second
|
||||
InErrors uint64
|
||||
OutErrors uint64
|
||||
PoE *PoEStats
|
||||
Speed uint64 `json:"speed,omitempty"`
|
||||
InErrors uint64 `json:"in_errors,omitempty"`
|
||||
OutErrors uint64 `json:"out_errors,omitempty"`
|
||||
PoE *PoEStats `json:"poe,omitempty"`
|
||||
}
|
||||
|
||||
type PoEStats struct {
|
||||
Power float64 // watts in use
|
||||
MaxPower float64 // watts allocated/negotiated
|
||||
Power float64 `json:"power"`
|
||||
MaxPower float64 `json:"max_power"`
|
||||
}
|
||||
|
||||
type PoEBudget struct {
|
||||
Power float64 // watts in use
|
||||
MaxPower float64 // watts total budget
|
||||
Power float64 `json:"power"`
|
||||
MaxPower float64 `json:"max_power"`
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
Names map[string]bool
|
||||
Interfaces map[string]*Interface
|
||||
MACTable map[string]string // peer MAC -> local interface name
|
||||
PoEBudget *PoEBudget
|
||||
IsDanteClockMaster bool
|
||||
DanteTxChannels string
|
||||
TypeID string `json:"typeid"`
|
||||
Names NameSet `json:"names"`
|
||||
Interfaces InterfaceMap `json:"interfaces"`
|
||||
MACTable map[string]string `json:"-"`
|
||||
MACTableSize int `json:"mac_table_size,omitempty"`
|
||||
PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
|
||||
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"`
|
||||
DanteTxChannels string `json:"dante_tx_channels,omitempty"`
|
||||
pollTrigger chan struct{}
|
||||
}
|
||||
|
||||
func (i *Interface) String() string {
|
||||
var ips []string
|
||||
for _, ip := range i.IPs {
|
||||
ips = append(ips, ip.String())
|
||||
}
|
||||
sort.Strings(ips)
|
||||
|
||||
var parts []string
|
||||
parts = append(parts, i.MAC.String())
|
||||
parts = append(parts, string(i.MAC))
|
||||
if i.Name != "" {
|
||||
parts = append(parts, fmt.Sprintf("(%s)", i.Name))
|
||||
}
|
||||
if len(ips) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%v", ips))
|
||||
if len(i.IPs) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%v", i.IPs.Slice()))
|
||||
}
|
||||
if i.Stats != nil {
|
||||
parts = append(parts, i.Stats.String())
|
||||
@@ -135,8 +216,8 @@ func (n *Node) DisplayName() string {
|
||||
|
||||
func (n *Node) FirstMAC() string {
|
||||
for _, iface := range n.Interfaces {
|
||||
if iface.MAC != nil {
|
||||
return iface.MAC.String()
|
||||
if iface.MAC != "" {
|
||||
return string(iface.MAC)
|
||||
}
|
||||
}
|
||||
return "??"
|
||||
|
||||
Reference in New Issue
Block a user