refactor to per-node snmp polling and immediate arp dump

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-22 23:09:54 -08:00
parent d2a09250d0
commit c3ed8e30e3
4 changed files with 94 additions and 53 deletions

5
arp.go
View File

@@ -15,8 +15,6 @@ func (t *Tendrils) pollARP(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second) ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop() defer ticker.Stop()
t.readARPTable()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -35,7 +33,6 @@ type arpEntry struct {
func (t *Tendrils) readARPTable() { func (t *Tendrils) readARPTable() {
entries := t.parseARPTable() entries := t.parseARPTable()
localNode := t.getLocalNode() localNode := t.getLocalNode()
for _, entry := range entries { for _, entry := range entries {
@@ -65,7 +62,7 @@ func (t *Tendrils) parseARPTable() []arpEntry {
} }
func (t *Tendrils) parseARPDarwin() []arpEntry { func (t *Tendrils) parseARPDarwin() []arpEntry {
cmd := exec.Command("arp", "-a") cmd := exec.Command("arp", "-an")
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {
return nil return nil

View File

@@ -1,11 +1,13 @@
package tendrils package tendrils
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"net" "net"
"sort" "sort"
"sync" "sync"
"time"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
) )
@@ -104,6 +106,7 @@ type Node struct {
Interfaces map[string]*Interface Interfaces map[string]*Interface
MACTable map[string]string // peer MAC -> local interface name MACTable map[string]string // peer MAC -> local interface name
PoEBudget *PoEBudget PoEBudget *PoEBudget
pollTrigger chan struct{}
} }
func (n *Node) String() string { func (n *Node) String() string {
@@ -135,20 +138,31 @@ type Nodes struct {
nodes map[int]*Node nodes map[int]*Node
ipIndex map[string]int ipIndex map[string]int
macIndex map[string]int macIndex map[string]int
nodeCancel map[int]context.CancelFunc
nextID int nextID int
t *Tendrils t *Tendrils
ctx context.Context
cancelAll context.CancelFunc
} }
func NewNodes(t *Tendrils) *Nodes { func NewNodes(t *Tendrils) *Nodes {
ctx, cancel := context.WithCancel(context.Background())
return &Nodes{ return &Nodes{
nodes: map[int]*Node{}, nodes: map[int]*Node{},
ipIndex: map[string]int{}, ipIndex: map[string]int{},
macIndex: map[string]int{}, macIndex: map[string]int{},
nodeCancel: map[int]context.CancelFunc{},
nextID: 1, nextID: 1,
t: t, t: t,
ctx: ctx,
cancelAll: cancel,
} }
} }
func (n *Nodes) Shutdown() {
n.cancelAll()
}
func (n *Nodes) Update(target *Node, mac net.HardwareAddr, ips []net.IP, ifaceName, nodeName, source string) { func (n *Nodes) Update(target *Node, mac net.HardwareAddr, ips []net.IP, ifaceName, nodeName, source string) {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
@@ -184,18 +198,22 @@ func (n *Nodes) Update(target *Node, mac net.HardwareAddr, ips []net.IP, ifaceNa
} }
} }
var node *Node
if targetID == -1 { if targetID == -1 {
targetID = n.nextID targetID = n.nextID
n.nextID++ n.nextID++
n.nodes[targetID] = &Node{ node = &Node{
Interfaces: map[string]*Interface{}, Interfaces: map[string]*Interface{},
MACTable: map[string]string{}, MACTable: map[string]string{},
pollTrigger: make(chan struct{}, 1),
} }
n.nodes[targetID] = node
isNew = true isNew = true
n.startNodePoller(targetID, node)
} else {
node = n.nodes[targetID]
} }
node := n.nodes[targetID]
var added []string var added []string
if mac != nil { if mac != nil {
added = n.updateNodeInterface(node, targetID, mac, ips, ifaceName) added = n.updateNodeInterface(node, targetID, mac, ips, ifaceName)
@@ -206,6 +224,14 @@ func (n *Nodes) Update(target *Node, mac net.HardwareAddr, ips []net.IP, ifaceNa
added = append(added, "name="+nodeName) added = append(added, "name="+nodeName)
} }
hasNewIP := false
for _, a := range added {
if len(a) > 3 && a[:3] == "ip=" {
hasNewIP = true
break
}
}
if len(added) > 0 { if len(added) > 0 {
if n.t.LogEvents { if n.t.LogEvents {
if isNew { if isNew {
@@ -218,6 +244,38 @@ func (n *Nodes) Update(target *Node, mac net.HardwareAddr, ips []net.IP, ifaceNa
n.logNode(node) n.logNode(node)
} }
} }
if hasNewIP {
n.triggerPoll(node)
}
}
func (n *Nodes) startNodePoller(nodeID int, node *Node) {
ctx, cancel := context.WithCancel(n.ctx)
n.nodeCancel[nodeID] = cancel
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-node.pollTrigger:
n.t.pollNode(node)
case <-ticker.C:
n.t.pollNode(node)
}
}
}()
}
func (n *Nodes) triggerPoll(node *Node) {
select {
case node.pollTrigger <- struct{}{}:
default:
}
} }
func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr, ips []net.IP, ifaceName string) []string { func (n *Nodes) updateNodeInterface(node *Node, nodeID int, mac net.HardwareAddr, ips []net.IP, ifaceName string) []string {

34
snmp.go
View File

@@ -1,7 +1,6 @@
package tendrils package tendrils
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"net" "net"
@@ -80,35 +79,24 @@ func (t *Tendrils) connectSNMP(ip net.IP) (*gosnmp.GoSNMP, error) {
return snmp, nil return snmp, nil
} }
func (t *Tendrils) pollSNMP(ctx context.Context) { func (t *Tendrils) pollNode(node *Node) {
ticker := time.NewTicker(10 * time.Second) if t.DisableSNMP {
defer ticker.Stop()
t.querySwitches()
for {
select {
case <-ctx.Done():
return return
case <-ticker.C:
t.querySwitches()
}
}
} }
func (t *Tendrils) querySwitches() { t.nodes.mu.RLock()
nodes := t.nodes.All() var ips []net.IP
for _, node := range nodes {
for _, iface := range node.Interfaces { for _, iface := range node.Interfaces {
for _, ip := range iface.IPs { for _, ip := range iface.IPs {
if ip.To4() == nil { if ip.To4() != nil {
continue ips = append(ips, ip)
} }
}
}
t.nodes.mu.RUnlock()
go t.querySNMPDevice(node, ip) for _, ip := range ips {
} t.querySNMPDevice(node, ip)
}
} }
} }

View File

@@ -48,11 +48,9 @@ func (t *Tendrils) Run() {
t.populateLocalAddresses() t.populateLocalAddresses()
if !t.DisableARP { if !t.DisableARP {
t.readARPTable()
go t.pollARP(ctx) go t.pollARP(ctx)
} }
if !t.DisableSNMP {
go t.pollSNMP(ctx)
}
ticker := time.NewTicker(1 * time.Second) ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() defer ticker.Stop()