Add missing node tracking for config-defined nodes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
53
config.go
53
config.go
@@ -2,6 +2,7 @@ package tendrils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -12,10 +13,54 @@ type Config struct {
|
||||
}
|
||||
|
||||
type Location struct {
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Direction string `yaml:"direction,omitempty" json:"direction,omitempty"`
|
||||
Nodes []string `yaml:"nodes,omitempty" json:"nodes,omitempty"`
|
||||
Children []*Location `yaml:"children,omitempty" json:"children,omitempty"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Direction string `yaml:"direction,omitempty" json:"direction,omitempty"`
|
||||
Nodes []*NodeRef `yaml:"nodes,omitempty" json:"nodes,omitempty"`
|
||||
Children []*Location `yaml:"children,omitempty" json:"children,omitempty"`
|
||||
}
|
||||
|
||||
type NodeRef struct {
|
||||
Name string `yaml:"name,omitempty" json:"name,omitempty"`
|
||||
MAC string `yaml:"mac,omitempty" json:"mac,omitempty"`
|
||||
IP string `yaml:"ip,omitempty" json:"ip,omitempty"`
|
||||
}
|
||||
|
||||
func (r *NodeRef) DisplayName() string {
|
||||
if r.Name != "" {
|
||||
return r.Name
|
||||
}
|
||||
if r.MAC != "" {
|
||||
return r.MAC
|
||||
}
|
||||
return r.IP
|
||||
}
|
||||
|
||||
func (r *NodeRef) ParseMAC() net.HardwareAddr {
|
||||
if r.MAC == "" {
|
||||
return nil
|
||||
}
|
||||
mac, _ := net.ParseMAC(r.MAC)
|
||||
return mac
|
||||
}
|
||||
|
||||
func (r *NodeRef) ParseIP() net.IP {
|
||||
if r.IP == "" {
|
||||
return nil
|
||||
}
|
||||
return net.ParseIP(r.IP)
|
||||
}
|
||||
|
||||
func (c *Config) AllNodeRefs() []*NodeRef {
|
||||
var refs []*NodeRef
|
||||
var collect func([]*Location)
|
||||
collect = func(locs []*Location) {
|
||||
for _, loc := range locs {
|
||||
refs = append(refs, loc.Nodes...)
|
||||
collect(loc.Children)
|
||||
}
|
||||
}
|
||||
collect(c.Locations)
|
||||
return refs
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
|
||||
192
config.yaml
192
config.yaml
@@ -4,168 +4,168 @@ locations:
|
||||
children:
|
||||
- name: LIGHTING-2 Rack
|
||||
nodes:
|
||||
- lighting-2
|
||||
- "48:59:00:41:00:29" # Pixie Driver 8k Port 1
|
||||
- "48:59:00:28:00:27" # Pixie Driver 8k Port 2
|
||||
- "48:59:00:43:00:29" # Pixie Driver 8k Port 3
|
||||
- "48:59:00:42:00:29" # Pixie Driver 8k Port 4
|
||||
- "48:59:00:3c:00:3e" # Pixie Driver 8k Port 5
|
||||
- "48:59:00:3f:00:3e" # Pixie Driver 8k Port 6
|
||||
- "48:59:00:25:00:3e" # Pixie Driver 8k Port 7
|
||||
- "48:59:00:41:00:3e" # Pixie Driver 8k Port 8
|
||||
- ART9 # Cyc
|
||||
- ART10 # Cyc
|
||||
- ART11 # Cyc
|
||||
- ART12 # Cyc
|
||||
- ART13 # Cyc
|
||||
- ART14 # Cyc
|
||||
- name: lighting-2
|
||||
- mac: "48:59:00:41:00:29" # Pixie Driver 8k Port 1
|
||||
- mac: "48:59:00:28:00:27" # Pixie Driver 8k Port 2
|
||||
- mac: "48:59:00:43:00:29" # Pixie Driver 8k Port 3
|
||||
- mac: "48:59:00:42:00:29" # Pixie Driver 8k Port 4
|
||||
- mac: "48:59:00:3c:00:3e" # Pixie Driver 8k Port 5
|
||||
- mac: "48:59:00:3f:00:3e" # Pixie Driver 8k Port 6
|
||||
- mac: "48:59:00:25:00:3e" # Pixie Driver 8k Port 7
|
||||
- mac: "48:59:00:41:00:3e" # Pixie Driver 8k Port 8
|
||||
- name: ART9 # Cyc
|
||||
- name: ART10 # Cyc
|
||||
- name: ART11 # Cyc
|
||||
- name: ART12 # Cyc
|
||||
- name: ART13 # Cyc
|
||||
- name: ART14 # Cyc
|
||||
|
||||
- direction: horizontal
|
||||
children:
|
||||
- nodes:
|
||||
- ART16 # R2X1
|
||||
- name: ART16 # R2X1
|
||||
|
||||
- nodes:
|
||||
- ART20 # R2X2
|
||||
- name: ART20 # R2X2
|
||||
|
||||
- direction: horizontal
|
||||
children:
|
||||
- nodes:
|
||||
- "MON1-A"
|
||||
- "MON1-B"
|
||||
- name: MON1-A
|
||||
- name: MON1-B
|
||||
|
||||
- name: Under Apron
|
||||
direction: horizontal
|
||||
children:
|
||||
- name: AUDIO Rack
|
||||
nodes:
|
||||
- audio
|
||||
- "MICS-A"
|
||||
- "00:0e:dd:a7:29:93" # MICS-A bridge interface
|
||||
- "MICS-B"
|
||||
- "00:0e:dd:a8:3e:b3" # MICS-B bridge interface
|
||||
- "MICS-C"
|
||||
- "00:0e:dd:a7:6f:55" # MICS-C bridge interface
|
||||
- "MICS-D"
|
||||
- "00:0e:dd:64:3d:51" # MICS-D bridge interface
|
||||
- "MICS-E"
|
||||
- "00:0e:dd:ac:fc:7d" # MICS-E bridge interface
|
||||
- name: audio
|
||||
- name: MICS-A
|
||||
- mac: "00:0e:dd:a7:29:93" # MICS-A bridge interface
|
||||
- name: MICS-B
|
||||
- mac: "00:0e:dd:a8:3e:b3" # MICS-B bridge interface
|
||||
- name: MICS-C
|
||||
- mac: "00:0e:dd:a7:6f:55" # MICS-C bridge interface
|
||||
- name: MICS-D
|
||||
- mac: "00:0e:dd:64:3d:51" # MICS-D bridge interface
|
||||
- name: MICS-E
|
||||
- mac: "00:0e:dd:ac:fc:7d" # MICS-E bridge interface
|
||||
|
||||
- name: LIGHTING-1 Rack
|
||||
nodes:
|
||||
- lighting-1
|
||||
- "48:59:00:27:00:27" # Pixie Driver 8k Port 1
|
||||
- "48:59:00:37:00:27" # Pixie Driver 8k Port 2
|
||||
- "48:59:00:3e:00:27" # Pixie Driver 8k Port 3
|
||||
- "48:59:00:3f:00:27" # Pixie Driver 8k Port 4
|
||||
- "48:59:00:47:00:1a" # Pixie Driver 8k Port 5
|
||||
- "48:59:00:44:00:1a" # Pixie Driver 8k Port 6
|
||||
- "48:59:00:42:00:19" # Pixie Driver 8k Port 7
|
||||
- "48:59:00:44:00:19" # Pixie Driver 8k Port 8
|
||||
- name: lighting-1
|
||||
- mac: "48:59:00:27:00:27" # Pixie Driver 8k Port 1
|
||||
- mac: "48:59:00:37:00:27" # Pixie Driver 8k Port 2
|
||||
- mac: "48:59:00:3e:00:27" # Pixie Driver 8k Port 3
|
||||
- mac: "48:59:00:3f:00:27" # Pixie Driver 8k Port 4
|
||||
- mac: "48:59:00:47:00:1a" # Pixie Driver 8k Port 5
|
||||
- mac: "48:59:00:44:00:1a" # Pixie Driver 8k Port 6
|
||||
- mac: "48:59:00:42:00:19" # Pixie Driver 8k Port 7
|
||||
- mac: "48:59:00:44:00:19" # Pixie Driver 8k Port 8
|
||||
|
||||
- name: VIDEO Rack
|
||||
nodes:
|
||||
- video
|
||||
- "ATEM 2 M/E Constellation 4K"
|
||||
- "HyperDeck-Studio-4K-Pro"
|
||||
- RX-QLAB-1
|
||||
- RX-QLAB-2
|
||||
- TX-PROJ-1
|
||||
- TX-PROJ-2
|
||||
- TX-M4
|
||||
- TX-M16
|
||||
- TX-MISC
|
||||
- TX-PREVIEW
|
||||
- name: video
|
||||
- name: "ATEM 2 M/E Constellation 4K"
|
||||
- name: HyperDeck-Studio-4K-Pro
|
||||
- name: RX-QLAB-1
|
||||
- name: RX-QLAB-2
|
||||
- name: TX-PROJ-1
|
||||
- name: TX-PROJ-2
|
||||
- name: TX-M4
|
||||
- name: TX-M16
|
||||
- name: TX-MISC
|
||||
- name: TX-PREVIEW
|
||||
|
||||
- direction: horizontal
|
||||
children:
|
||||
- nodes:
|
||||
- "Y001-MAIN1-L-d1e155"
|
||||
- "ac:44:f2:4e:84:d6" # MAIN1-L bridge interface
|
||||
- name: Y001-MAIN1-L-d1e155
|
||||
- mac: "ac:44:f2:4e:84:d6" # MAIN1-L bridge interface
|
||||
|
||||
- name: Stage 2
|
||||
nodes:
|
||||
- "MON2"
|
||||
- name: MON2
|
||||
|
||||
- name: Stage 3
|
||||
nodes:
|
||||
- "MON3"
|
||||
- name: MON3
|
||||
|
||||
- nodes:
|
||||
- "Y001-MAIN1-R-d1e194"
|
||||
- "ac:44:f2:4e:84:d4" # MAIN1-R bridge interface
|
||||
- name: Y001-MAIN1-R-d1e194
|
||||
- mac: "ac:44:f2:4e:84:d4" # MAIN1-R bridge interface
|
||||
|
||||
- direction: horizontal
|
||||
children:
|
||||
- nodes:
|
||||
- "RX-PROJ-1"
|
||||
- name: RX-PROJ-1
|
||||
|
||||
- nodes:
|
||||
- "RX-PROJ-2"
|
||||
- name: RX-PROJ-2
|
||||
|
||||
- direction: horizontal
|
||||
children:
|
||||
- nodes:
|
||||
- satellite-2
|
||||
- "Y001-MAIN2-L-d1e298"
|
||||
- "ac:44:f2:4e:87:2a" # MAIN2-L bridge interface
|
||||
- ART3 # Wash
|
||||
- ART4 # Wash
|
||||
- ART5 # Wash
|
||||
- ART17 # Focus
|
||||
- name: satellite-2
|
||||
- name: Y001-MAIN2-L-d1e298
|
||||
- mac: "ac:44:f2:4e:87:2a" # MAIN2-L bridge interface
|
||||
- name: ART3 # Wash
|
||||
- name: ART4 # Wash
|
||||
- name: ART5 # Wash
|
||||
- name: ART17 # Focus
|
||||
|
||||
- nodes:
|
||||
- ART15 # R3X
|
||||
- ART18 # Focus
|
||||
- name: ART15 # R3X
|
||||
- name: ART18 # Focus
|
||||
|
||||
- nodes:
|
||||
- satellite-3
|
||||
- "Y001-MAIN2-R-f0dd93"
|
||||
- "ac:44:f2:4e:87:27" # MAIN2-R bridge interface
|
||||
- ART6 # Wash
|
||||
- ART7 # Wash
|
||||
- DMX8 # Wash
|
||||
- ART19 # Focus
|
||||
- name: satellite-3
|
||||
- name: Y001-MAIN2-R-f0dd93
|
||||
- mac: "ac:44:f2:4e:87:27" # MAIN2-R bridge interface
|
||||
- name: ART6 # Wash
|
||||
- name: ART7 # Wash
|
||||
- name: DMX32 # Wash
|
||||
- name: ART19 # Focus
|
||||
|
||||
- name: Booth
|
||||
direction: vertical
|
||||
children:
|
||||
- nodes:
|
||||
- satellite-1
|
||||
- name: satellite-1
|
||||
|
||||
- direction: horizontal
|
||||
children:
|
||||
- name: Lighting Control
|
||||
nodes:
|
||||
- qlab
|
||||
- TX-QLAB-1
|
||||
- TX-QLAB-2
|
||||
- "SK_PTZEXTREMEV2 [457081]"
|
||||
- "SK_RACKPRO2 [452514]"
|
||||
- pigeon
|
||||
- showpi1
|
||||
- d8:3a:dd:e3:5b:db # showpi2/artmap
|
||||
|
||||
- name: qlab
|
||||
- name: TX-QLAB-1
|
||||
- name: TX-QLAB-2
|
||||
- name: "SK_PTZEXTREMEV2 [457081]"
|
||||
- name: "SK_RACKPRO2 [452514]"
|
||||
- name: pigeon
|
||||
- name: showpi1
|
||||
- mac: "d8:3a:dd:e3:5b:db" # showpi2/artmap
|
||||
|
||||
- name: Sound Control
|
||||
nodes:
|
||||
- SQ-7
|
||||
- "00:04:c4:15:07:a4" # SQ-7 bridge port
|
||||
- BT
|
||||
- name: SQ-7
|
||||
- mac: "00:04:c4:15:07:a4" # SQ-7 bridge port
|
||||
- name: BT
|
||||
|
||||
- name: Camera Control
|
||||
nodes:
|
||||
- RX-CC-PREVIEW
|
||||
- RX-CC-M16
|
||||
- "AtemPanel-7c2e0da86d22"
|
||||
- "AtemPanel-7c2e0da86d4c"
|
||||
- name: RX-CC-PREVIEW
|
||||
- name: RX-CC-M16
|
||||
- name: AtemPanel-7c2e0da86d22
|
||||
- name: AtemPanel-7c2e0da86d4c
|
||||
|
||||
- name: Video Control
|
||||
nodes:
|
||||
- RX-VC-M4
|
||||
- RX-VC-M16
|
||||
- "ATEM-2-ME-Advanced-Panel-20"
|
||||
- name: RX-VC-M4
|
||||
- name: RX-VC-M16
|
||||
- name: ATEM-2-ME-Advanced-Panel-20
|
||||
|
||||
- name: Control
|
||||
nodes:
|
||||
- sunset
|
||||
- RX-CONTROL-1
|
||||
- name: sunset
|
||||
- name: RX-CONTROL-1
|
||||
|
||||
57
errors.go
57
errors.go
@@ -9,10 +9,11 @@ import (
|
||||
type PortErrorType string
|
||||
|
||||
const (
|
||||
ErrorTypeStartup PortErrorType = "startup"
|
||||
ErrorTypeNew PortErrorType = "new"
|
||||
ErrorTypeUnreachable PortErrorType = "unreachable"
|
||||
ErrorTypeStartup PortErrorType = "startup"
|
||||
ErrorTypeNew PortErrorType = "new"
|
||||
ErrorTypeUnreachable PortErrorType = "unreachable"
|
||||
ErrorTypeHighUtilization PortErrorType = "high_utilization"
|
||||
ErrorTypeMissing PortErrorType = "missing"
|
||||
)
|
||||
|
||||
type PortError struct {
|
||||
@@ -327,3 +328,53 @@ func (e *ErrorTracker) clearUnreachableLocked(node *Node) (changed bool, becameR
|
||||
}
|
||||
return becameReachable, becameReachable
|
||||
}
|
||||
|
||||
func (e *ErrorTracker) SetMissing(node *Node) {
|
||||
changed := e.setMissingLocked(node)
|
||||
if changed {
|
||||
e.t.NotifyUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ErrorTracker) setMissingLocked(node *Node) bool {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
key := "missing:" + node.TypeID
|
||||
|
||||
if _, exists := e.errors[key]; exists {
|
||||
return false
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
e.nextID++
|
||||
e.errors[key] = &PortError{
|
||||
ID: fmt.Sprintf("err-%d", e.nextID),
|
||||
NodeTypeID: node.TypeID,
|
||||
NodeName: node.DisplayName(),
|
||||
PortName: "",
|
||||
ErrorType: ErrorTypeMissing,
|
||||
FirstSeen: now,
|
||||
LastUpdated: now,
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *ErrorTracker) ClearMissing(node *Node) {
|
||||
changed := e.clearMissingLocked(node)
|
||||
if changed {
|
||||
e.t.NotifyUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ErrorTracker) clearMissingLocked(node *Node) bool {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
key := "missing:" + node.TypeID
|
||||
if _, exists := e.errors[key]; exists {
|
||||
delete(e.errors, key)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
5
nodes.go
5
nodes.go
@@ -67,6 +67,11 @@ func (n *Nodes) updateLocked(target *Node, mac net.HardwareAddr, ips []net.IP, i
|
||||
|
||||
added := n.applyNodeUpdates(node, targetID, mac, ips, ifaceName, nodeName)
|
||||
|
||||
if node.Missing && source != "config" {
|
||||
node.Missing = false
|
||||
n.t.errors.ClearMissing(node)
|
||||
}
|
||||
|
||||
n.logUpdates(node, added, isNew, source)
|
||||
|
||||
if hasNewIP(added) {
|
||||
|
||||
@@ -991,6 +991,9 @@
|
||||
if (node.interfaces) {
|
||||
node.interfaces.forEach(iface => {
|
||||
if (iface.mac) ids.push(iface.mac.toLowerCase());
|
||||
if (iface.ips) {
|
||||
iface.ips.forEach(ip => ids.push(ip.toLowerCase()));
|
||||
}
|
||||
});
|
||||
}
|
||||
return ids;
|
||||
@@ -1147,7 +1150,7 @@
|
||||
name: loc.name || '',
|
||||
anonymous: anonymous,
|
||||
direction: loc.direction || 'horizontal',
|
||||
nodeRefs: (loc.nodes || []).map(n => n.toLowerCase()),
|
||||
nodeRefs: (loc.nodes || []).map(n => (n.name || n.mac || n.ip || '').toLowerCase()),
|
||||
parent: parent,
|
||||
children: []
|
||||
};
|
||||
|
||||
44
tendrils.go
44
tendrils.go
@@ -119,6 +119,48 @@ func (t *Tendrils) unsubscribeSSE(id int) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tendrils) syncConfigNodes() {
|
||||
if t.config == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, ref := range t.config.AllNodeRefs() {
|
||||
var node *Node
|
||||
var created bool
|
||||
|
||||
if ref.Name != "" {
|
||||
node = t.nodes.GetByName(ref.Name)
|
||||
if node == nil {
|
||||
node = t.nodes.GetOrCreateByName(ref.Name)
|
||||
created = true
|
||||
}
|
||||
} else if mac := ref.ParseMAC(); mac != nil {
|
||||
node = t.nodes.GetByMAC(mac)
|
||||
if node == nil {
|
||||
t.nodes.Update(nil, mac, nil, "", "", "config")
|
||||
node = t.nodes.GetByMAC(mac)
|
||||
created = true
|
||||
}
|
||||
} else if ip := ref.ParseIP(); ip != nil {
|
||||
node = t.nodes.GetByIP(ip)
|
||||
if node == nil {
|
||||
t.nodes.Update(nil, nil, []net.IP{ip}, "", "", "config")
|
||||
node = t.nodes.GetByIP(ip)
|
||||
created = true
|
||||
}
|
||||
}
|
||||
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if created {
|
||||
node.Missing = true
|
||||
t.errors.SetMissing(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tendrils) Run() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -141,6 +183,7 @@ func (t *Tendrils) Run() {
|
||||
continue
|
||||
}
|
||||
t.config = cfg
|
||||
t.syncConfigNodes()
|
||||
log.Printf("reloaded config from %s", t.ConfigFile)
|
||||
t.NotifyUpdate()
|
||||
}
|
||||
@@ -153,6 +196,7 @@ func (t *Tendrils) Run() {
|
||||
t.config = cfg
|
||||
|
||||
t.populateLocalAddresses()
|
||||
t.syncConfigNodes()
|
||||
t.startHTTPServer()
|
||||
|
||||
if !t.DisableARP {
|
||||
|
||||
2
types.go
2
types.go
@@ -140,6 +140,7 @@ type Node struct {
|
||||
PoEBudget *PoEBudget `json:"poe_budget,omitempty"`
|
||||
IsDanteClockMaster bool `json:"is_dante_clock_master,omitempty"`
|
||||
DanteTxChannels string `json:"dante_tx_channels,omitempty"`
|
||||
Missing bool `json:"missing,omitempty"`
|
||||
pollTrigger chan struct{}
|
||||
}
|
||||
|
||||
@@ -159,6 +160,7 @@ func (n *Node) WithInterface(ifaceKey string) *Node {
|
||||
PoEBudget: n.PoEBudget,
|
||||
IsDanteClockMaster: n.IsDanteClockMaster,
|
||||
DanteTxChannels: n.DanteTxChannels,
|
||||
Missing: n.Missing,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user