Files
tendrils/types.go
2026-01-28 23:06:26 -08:00

635 lines
14 KiB
Go

package tendrils
import (
"context"
"encoding/json"
"fmt"
"net"
"sort"
"strings"
"time"
"github.com/fvbommel/sortorder"
"go.jetify.com/typeid"
)
func newID(prefix string) string {
tid, _ := typeid.WithPrefix(prefix)
return tid.String()
}
type ArtNetUniverse int
func (u ArtNetUniverse) Net() int {
return (int(u) >> 8) & 0x7f
}
func (u ArtNetUniverse) Subnet() int {
return (int(u) >> 4) & 0x0f
}
func (u ArtNetUniverse) Universe() int {
return int(u) & 0x0f
}
func (u ArtNetUniverse) String() string {
return fmt.Sprintf("%d/%d/%d", u.Net(), u.Subnet(), u.Universe())
}
type ArtNetUniverseSet map[ArtNetUniverse]time.Time
func (s ArtNetUniverseSet) Add(u ArtNetUniverse) {
s[u] = time.Now()
}
func (s ArtNetUniverseSet) Universes() []ArtNetUniverse {
result := make([]ArtNetUniverse, 0, len(s))
for u := range s {
result = append(result, u)
}
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
return result
}
func (s ArtNetUniverseSet) Expire(maxAge time.Duration) {
expireTime := time.Now().Add(-maxAge)
for u, lastSeen := range s {
if lastSeen.Before(expireTime) {
delete(s, u)
}
}
}
func (s ArtNetUniverseSet) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Universes())
}
type SACNUniverse int
func (u SACNUniverse) String() string {
return fmt.Sprintf("%d", u)
}
type SACNUniverseSet map[SACNUniverse]time.Time
func (s SACNUniverseSet) Add(u SACNUniverse) {
s[u] = time.Now()
}
func (s SACNUniverseSet) Universes() []SACNUniverse {
result := make([]SACNUniverse, 0, len(s))
for u := range s {
result = append(result, u)
}
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
return result
}
func (s SACNUniverseSet) Expire(maxAge time.Duration) {
expireTime := time.Now().Add(-maxAge)
for u, lastSeen := range s {
if lastSeen.Before(expireTime) {
delete(s, u)
}
}
}
func (s SACNUniverseSet) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Universes())
}
type MulticastGroupID int
const (
MulticastUnknown MulticastGroupID = iota
MulticastMDNS
MulticastPTP
MulticastPTPAnnounce
MulticastPTPSync
MulticastPTPDelay
MulticastSAP
MulticastShureSLP
MulticastSSDP
MulticastSLP
MulticastAdminScopedBroadcast
)
type MulticastGroup struct {
ID MulticastGroupID
SACNUniverse SACNUniverse
DanteFlow int
DanteAV int
RawIP string
}
func (g MulticastGroup) String() string {
switch g.ID {
case MulticastMDNS:
return "mdns"
case MulticastPTP:
return "ptp"
case MulticastPTPAnnounce:
return "ptp-announce"
case MulticastPTPSync:
return "ptp-sync"
case MulticastPTPDelay:
return "ptp-delay"
case MulticastSAP:
return "sap"
case MulticastShureSLP:
return "shure-slp"
case MulticastSSDP:
return "ssdp"
case MulticastSLP:
return "slp"
case MulticastAdminScopedBroadcast:
return "admin-scoped-broadcast"
}
if g.SACNUniverse > 0 {
return fmt.Sprintf("sacn:%d", g.SACNUniverse)
}
if g.DanteFlow > 0 {
return fmt.Sprintf("dante-mcast:%d", g.DanteFlow)
}
if g.DanteAV > 0 {
return fmt.Sprintf("dante-av:%d", g.DanteAV)
}
return g.RawIP
}
func (g MulticastGroup) MarshalJSON() ([]byte, error) {
return json.Marshal(g.String())
}
func (g MulticastGroup) IsDante() bool {
return g.DanteFlow > 0 || g.DanteAV > 0
}
func (g MulticastGroup) IsSACN() bool {
return g.SACNUniverse > 0
}
func ParseMulticastGroup(ip net.IP) MulticastGroup {
ip4 := ip.To4()
if ip4 == nil {
return MulticastGroup{RawIP: ip.String()}
}
switch ip.String() {
case "224.0.0.251":
return MulticastGroup{ID: MulticastMDNS}
case "224.0.1.129":
return MulticastGroup{ID: MulticastPTP}
case "224.0.1.130":
return MulticastGroup{ID: MulticastPTPAnnounce}
case "224.0.1.131":
return MulticastGroup{ID: MulticastPTPSync}
case "224.0.1.132":
return MulticastGroup{ID: MulticastPTPDelay}
case "224.2.127.254":
return MulticastGroup{ID: MulticastSAP}
case "239.255.254.253":
return MulticastGroup{ID: MulticastShureSLP}
case "239.255.255.250":
return MulticastGroup{ID: MulticastSSDP}
case "239.255.255.253":
return MulticastGroup{ID: MulticastSLP}
case "239.255.255.255":
return MulticastGroup{ID: MulticastAdminScopedBroadcast}
}
if ip4[0] == 239 && ip4[1] == 255 {
universe := int(ip4[2])*256 + int(ip4[3])
if universe >= 1 && universe <= 63999 {
return MulticastGroup{SACNUniverse: SACNUniverse(universe)}
}
}
if ip4[0] == 239 && ip4[1] >= 69 && ip4[1] <= 71 {
flowID := (int(ip4[1]-69) << 16) | (int(ip4[2]) << 8) | int(ip4[3])
return MulticastGroup{DanteFlow: flowID}
}
if ip4[0] == 239 && ip4[1] == 253 {
flowID := (int(ip4[2]) << 8) | int(ip4[3])
return MulticastGroup{DanteAV: flowID}
}
return MulticastGroup{RawIP: ip.String()}
}
type MulticastMembershipSet map[MulticastGroup]time.Time
func (s MulticastMembershipSet) Add(group MulticastGroup) {
s[group] = time.Now()
}
func (s MulticastMembershipSet) Remove(group MulticastGroup) {
delete(s, group)
}
func (s MulticastMembershipSet) Groups() []MulticastGroup {
result := make([]MulticastGroup, 0, len(s))
for g := range s {
result = append(result, g)
}
sort.Slice(result, func(i, j int) bool {
return result[i].String() < result[j].String()
})
return result
}
func (s MulticastMembershipSet) SACNInputs() []SACNUniverse {
var result []SACNUniverse
for g := range s {
if g.IsSACN() {
result = append(result, g.SACNUniverse)
}
}
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
return result
}
func (s MulticastMembershipSet) Expire(maxAge time.Duration) {
expireTime := time.Now().Add(-maxAge)
for g, lastSeen := range s {
if lastSeen.Before(expireTime) {
delete(s, g)
}
}
}
func (s MulticastMembershipSet) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Groups())
}
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 sortNamesByLength(names []string) {
sort.Slice(names, func(i, j int) bool {
if len(names[i]) != len(names[j]) {
return len(names[i]) < len(names[j])
}
return names[i] < names[j]
})
}
func (s NameSet) MarshalJSON() ([]byte, error) {
names := make([]string, 0, len(s))
for name := range s {
names = append(names, name)
}
sortNamesByLength(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 `json:"name,omitempty"`
MAC MAC `json:"mac"`
IPs IPSet `json:"ips"`
Stats *InterfaceStats `json:"stats,omitempty"`
}
type InterfaceStats struct {
Speed uint64 `json:"speed,omitempty"`
InErrors uint64 `json:"in_errors,omitempty"`
OutErrors uint64 `json:"out_errors,omitempty"`
InPktsRate float64 `json:"in_pkts_rate,omitempty"`
OutPktsRate float64 `json:"out_pkts_rate,omitempty"`
InBytesRate float64 `json:"in_bytes_rate,omitempty"`
OutBytesRate float64 `json:"out_bytes_rate,omitempty"`
PoE *PoEStats `json:"poe,omitempty"`
}
type PoEStats struct {
Power float64 `json:"power"`
MaxPower float64 `json:"max_power"`
}
type PoEBudget struct {
Power float64 `json:"power"`
MaxPower float64 `json:"max_power"`
}
type Node struct {
ID string `json:"id"`
Names NameSet `json:"names"`
Interfaces InterfaceMap `json:"interfaces"`
MACTable map[string]string `json:"-"`
PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"`
DanteTxChannels string `json:"dante_tx_channels,omitempty"`
MulticastGroups MulticastMembershipSet `json:"multicast_groups,omitempty"`
ArtNetInputs ArtNetUniverseSet `json:"artnet_inputs,omitempty"`
ArtNetOutputs ArtNetUniverseSet `json:"artnet_outputs,omitempty"`
SACNOutputs SACNUniverseSet `json:"sacn_outputs,omitempty"`
DanteTx []*DantePeer `json:"dante_tx,omitempty"`
DanteRx []*DantePeer `json:"dante_rx,omitempty"`
Unreachable bool `json:"unreachable,omitempty"`
errors *ErrorTracker
pollTrigger chan struct{}
cancelFunc context.CancelFunc
danteLastSeen time.Time
}
func (n *Node) SetUnreachable(unreachable bool) bool {
if n.Unreachable == unreachable {
return false
}
n.Unreachable = unreachable
if n.errors != nil {
if unreachable {
n.errors.AddUnreachable(n)
} else {
n.errors.RemoveUnreachable(n)
}
}
return true
}
func (n *Node) SetInterfaceStats(portName string, stats *InterfaceStats) {
iface := n.Interfaces[portName]
if iface == nil {
return
}
oldStats := iface.Stats
iface.Stats = stats
if n.errors == nil {
return
}
var inDelta, outDelta uint64
if oldStats != nil {
if stats.InErrors > oldStats.InErrors {
inDelta = stats.InErrors - oldStats.InErrors
}
if stats.OutErrors > oldStats.OutErrors {
outDelta = stats.OutErrors - oldStats.OutErrors
}
}
if inDelta > 0 || outDelta > 0 {
n.errors.AddPortError(n, portName, stats, inDelta, outDelta)
}
if stats.Speed > 0 {
maxBytesRate := stats.InBytesRate
if stats.OutBytesRate > maxBytesRate {
maxBytesRate = stats.OutBytesRate
}
speedBytes := float64(stats.Speed) / 8.0
utilization := (maxBytesRate / speedBytes) * 100.0
var oldUtilization float64
if oldStats != nil && oldStats.Speed > 0 {
oldMax := oldStats.InBytesRate
if oldStats.OutBytesRate > oldMax {
oldMax = oldStats.OutBytesRate
}
oldUtilization = (oldMax / (float64(oldStats.Speed) / 8.0)) * 100.0
}
if oldUtilization < 70.0 && utilization >= 70.0 {
n.errors.AddUtilizationError(n, portName, utilization)
}
}
}
func (n *Node) MACTableSize() int {
return len(n.MACTable)
}
func (n *Node) SACNInputs() []SACNUniverse {
if n.MulticastGroups == nil {
return nil
}
return n.MulticastGroups.SACNInputs()
}
type DantePeer struct {
Node *Node `json:"node"`
Channels []string `json:"channels,omitempty"`
Status map[string]string `json:"status,omitempty"`
}
func (p *DantePeer) MarshalJSON() ([]byte, error) {
type peerJSON struct {
Node *Node `json:"node"`
Channels []string `json:"channels,omitempty"`
Status map[string]string `json:"status,omitempty"`
}
nodeRef := &Node{
ID: p.Node.ID,
Names: p.Node.Names,
Interfaces: p.Node.Interfaces,
}
return json.Marshal(peerJSON{
Node: nodeRef,
Channels: p.Channels,
Status: p.Status,
})
}
func (n *Node) WithInterface(ifaceKey string) *Node {
if ifaceKey == "" {
return n
}
iface, exists := n.Interfaces[ifaceKey]
if !exists {
return n
}
return &Node{
ID: n.ID,
Names: n.Names,
Interfaces: InterfaceMap{ifaceKey: iface},
MACTable: n.MACTable,
PoEBudget: n.PoEBudget,
IsDanteClockMaster: n.IsDanteClockMaster,
DanteTxChannels: n.DanteTxChannels,
}
}
func (i *Interface) String() string {
var parts []string
parts = append(parts, string(i.MAC))
if i.Name != "" {
parts = append(parts, fmt.Sprintf("(%s)", i.Name))
}
if len(i.IPs) > 0 {
parts = append(parts, fmt.Sprintf("%v", i.IPs.Slice()))
}
if i.Stats != nil {
parts = append(parts, i.Stats.String())
}
result := parts[0]
for _, p := range parts[1:] {
result += " " + p
}
return result
}
func (s *InterfaceStats) String() string {
var parts []string
if s.Speed > 0 {
if s.Speed >= 1000000000 {
parts = append(parts, fmt.Sprintf("%dG", s.Speed/1000000000))
} else if s.Speed >= 1000000 {
parts = append(parts, fmt.Sprintf("%dM", s.Speed/1000000))
} else {
parts = append(parts, fmt.Sprintf("%d", s.Speed))
}
}
if s.InErrors > 0 || s.OutErrors > 0 {
parts = append(parts, fmt.Sprintf("err:%d/%d", s.InErrors, s.OutErrors))
}
if s.InBytesRate > 0 || s.OutBytesRate > 0 {
parts = append(parts, fmt.Sprintf("%.0f/%.0fB/s", s.InBytesRate, s.OutBytesRate))
}
if s.PoE != nil {
if s.PoE.MaxPower > 0 {
parts = append(parts, fmt.Sprintf("poe:%.1f/%.1fW", s.PoE.Power, s.PoE.MaxPower))
} else {
parts = append(parts, fmt.Sprintf("poe:%.1fW", s.PoE.Power))
}
}
return "[" + strings.Join(parts, " ") + "]"
}
func (n *Node) String() string {
name := n.DisplayName()
if name == "" {
name = "??"
}
var parts []string
parts = append(parts, name)
if n.PoEBudget != nil {
parts = append(parts, fmt.Sprintf("[poe:%.0f/%.0fW]", n.PoEBudget.Power, n.PoEBudget.MaxPower))
}
var ifaces []string
for _, iface := range n.Interfaces {
ifaces = append(ifaces, iface.String())
}
sort.Slice(ifaces, func(i, j int) bool { return sortorder.NaturalLess(ifaces[i], ifaces[j]) })
parts = append(parts, fmt.Sprintf("{%v}", ifaces))
return strings.Join(parts, " ")
}
func (n *Node) DisplayName() string {
if len(n.Names) > 0 {
var names []string
for name := range n.Names {
names = append(names, name)
}
sortNamesByLength(names)
return strings.Join(names, "/")
}
for _, iface := range n.Interfaces {
for ip := range iface.IPs {
return ip
}
}
for _, iface := range n.Interfaces {
if iface.MAC != "" {
return string(iface.MAC)
}
}
return ""
}
func (n *Node) FirstMAC() string {
for _, iface := range n.Interfaces {
if iface.MAC != "" {
return string(iface.MAC)
}
}
return ""
}
func (n *Node) FirstIP() string {
for _, iface := range n.Interfaces {
for ip := range iface.IPs {
return ip
}
}
return ""
}