Compare commits

...

3 Commits

Author SHA1 Message Date
Pim van Pelt
ccc2b5ad4d Cut v1.1.0-1 2025-06-23 19:16:02 +02:00
Pim van Pelt
4f368e625d Use interface details to populate the ifmib, on startup and after each event 2025-06-23 19:11:28 +02:00
Pim van Pelt
35165b0464 Add watchInterfaceEvents() listener 2025-06-23 19:02:02 +02:00
8 changed files with 298 additions and 220 deletions

11
debian/changelog vendored
View File

@@ -1,3 +1,14 @@
govpp-snmp-agentx (1.1.0-1) bookworm; urgency=medium
* Add interface event monitoring with VPP API integration
* Populate IF-MIB with real interface details (MAC address, speed, status)
* Consolidate VPP-related modules into unified vpp package
* Implement real-time interface state updates via event-driven callbacks
* Retrieve and expose actual interface properties: MTU, admin/oper status
* Add comprehensive interface details caching and management
-- Pim van Pelt <pim@ipng.ch> Sun, 23 Jun 2025 00:00:00 +0000
govpp-snmp-agentx (1.0.3-1) bookworm; urgency=medium
* Remove unnecessary toolchain configuration

View File

@@ -13,9 +13,9 @@ import (
"github.com/posteo/go-agentx/value"
"go.fd.io/govpp/api"
"gopkg.in/yaml.v3"
"govpp-snmp-agentx/logger"
"govpp-snmp-agentx/vppstats"
"govpp-snmp-agentx/vpp"
)
// IF-MIB OID bases:
@@ -72,24 +72,26 @@ type VPPConfig struct {
}
type VPPInterface struct {
Description string `yaml:"description"`
SubInterfaces map[string]VPPInterface `yaml:"sub-interfaces"`
Description string `yaml:"description"`
SubInterfaces map[string]VPPInterface `yaml:"sub-interfaces"`
}
type InterfaceMIB struct {
mutex sync.RWMutex
handler *agentx.ListHandler
ifEntrySession *agentx.Session
ifXTableSession *agentx.Session
stats map[uint32]*api.InterfaceCounters // indexed by interface index
descriptions map[string]string // interface name -> description mapping
mutex sync.RWMutex
handler *agentx.ListHandler
ifEntrySession *agentx.Session
ifXTableSession *agentx.Session
stats map[uint32]*api.InterfaceCounters // indexed by interface index
descriptions map[string]string // interface name -> description mapping
interfaceDetails map[uint32]*vpp.InterfaceDetails // indexed by interface index
}
func NewInterfaceMIB() *InterfaceMIB {
return &InterfaceMIB{
handler: &agentx.ListHandler{},
stats: make(map[uint32]*api.InterfaceCounters),
descriptions: make(map[string]string),
handler: &agentx.ListHandler{},
stats: make(map[uint32]*api.InterfaceCounters),
descriptions: make(map[string]string),
interfaceDetails: make(map[uint32]*vpp.InterfaceDetails),
}
}
@@ -142,6 +144,22 @@ func (m *InterfaceMIB) LoadVPPConfig(configPath string) error {
return nil
}
func (m *InterfaceMIB) UpdateInterfaceDetails(details []vpp.InterfaceDetails) {
m.mutex.Lock()
defer m.mutex.Unlock()
logger.Debugf("Updating interface details for %d interfaces", len(details))
// Update interface details map
for _, detail := range details {
m.interfaceDetails[uint32(detail.SwIfIndex)] = &detail
logger.Debugf("Updated details for interface %d (%s): MAC=%x, Speed=%d",
detail.SwIfIndex, detail.InterfaceName, detail.MacAddress, detail.Speed)
}
logger.Debugf("Interface details updated for %d interfaces", len(details))
}
func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
m.mutex.Lock()
defer m.mutex.Unlock()
@@ -172,7 +190,7 @@ func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
}
func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) {
idx := int(iface.InterfaceIndex) + *vppstats.IfIndexOffset
idx := int(iface.InterfaceIndex) + *vpp.IfIndexOffset
// Add ifEntry (classic interface table) entries
m.addIfEntry(iface, idx)
@@ -186,6 +204,9 @@ func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) {
func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
var item *agentx.ListItem
// Get interface details if available
details := m.interfaceDetails[iface.InterfaceIndex]
// ifIndex (.1)
item = m.handler.Add(fmt.Sprintf("%s.1.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeInteger
@@ -201,30 +222,58 @@ func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
item.Type = pdu.VariableTypeInteger
item.Value = int32(6)
// ifMtu (.4) - Default MTU 1500
// ifMtu (.4) - Use real MTU if available, otherwise default to 1500
mtu := int32(1500)
if details != nil {
mtu = int32(details.MTU)
}
item = m.handler.Add(fmt.Sprintf("%s.4.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeInteger
item.Value = int32(1500)
item.Value = mtu
// ifSpeed (.5) - Default to 1Gbps (1000000000 bits/sec)
// ifSpeed (.5) - Use real speed if available, otherwise default to 1Gbps
speed := uint32(1000000000)
if details != nil && details.Speed > 0 {
speed = uint32(details.Speed)
}
item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeGauge32
item.Value = uint32(1000000000)
item.Value = speed
// ifPhysAddress (.6) - Empty for now
// ifPhysAddress (.6) - Use real MAC address if available
macAddr := ""
if details != nil && len(details.MacAddress) > 0 {
macAddr = string(details.MacAddress)
}
item = m.handler.Add(fmt.Sprintf("%s.6.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeOctetString
item.Value = ""
item.Value = macAddr
// ifAdminStatus (.7) - up(1)
// ifAdminStatus (.7) - Use real admin status if available
adminStatus := int32(1) // default up
if details != nil {
if details.AdminStatus {
adminStatus = 1 // up
} else {
adminStatus = 2 // down
}
}
item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeInteger
item.Value = int32(1)
item.Value = adminStatus
// ifOperStatus (.8) - up(1)
// ifOperStatus (.8) - Use real operational status if available
operStatus := int32(1) // default up
if details != nil {
if details.OperStatus {
operStatus = 1 // up
} else {
operStatus = 2 // down
}
}
item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeInteger
item.Value = int32(1)
item.Value = operStatus
// ifLastChange (.9) - 0 (unknown)
item = m.handler.Add(fmt.Sprintf("%s.9.%d", ifEntryOID, idx))

View File

@@ -11,27 +11,27 @@ import (
func TestNewInterfaceMIB(t *testing.T) {
mib := NewInterfaceMIB()
if mib == nil {
t.Fatal("NewInterfaceMIB returned nil")
}
if mib.handler == nil {
t.Error("Expected handler to be initialized")
}
if mib.stats == nil {
t.Error("Expected stats map to be initialized")
}
if mib.descriptions == nil {
t.Error("Expected descriptions map to be initialized")
}
if len(mib.stats) != 0 {
t.Errorf("Expected stats map to be empty, got %d entries", len(mib.stats))
}
if len(mib.descriptions) != 0 {
t.Errorf("Expected descriptions map to be empty, got %d entries", len(mib.descriptions))
}
@@ -40,11 +40,11 @@ func TestNewInterfaceMIB(t *testing.T) {
func TestGetHandler(t *testing.T) {
mib := NewInterfaceMIB()
handler := mib.GetHandler()
if handler == nil {
t.Error("GetHandler returned nil")
}
if handler != mib.handler {
t.Error("GetHandler returned different handler than expected")
}
@@ -52,7 +52,7 @@ func TestGetHandler(t *testing.T) {
func TestLoadVPPConfigValidYAML(t *testing.T) {
mib := NewInterfaceMIB()
// Create a temporary YAML file
yamlContent := `interfaces:
GigabitEthernet0/0/0:
@@ -64,39 +64,39 @@ loopbacks:
loop0:
description: 'Test: Loopback'
`
tmpfile, err := os.CreateTemp("", "test_*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write([]byte(yamlContent)); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
// Test loading the config
err = mib.LoadVPPConfig(tmpfile.Name())
if err != nil {
t.Fatalf("LoadVPPConfig failed: %v", err)
}
// Check that descriptions were loaded
if len(mib.descriptions) != 3 {
t.Errorf("Expected 3 descriptions, got %d", len(mib.descriptions))
}
if mib.descriptions["GigabitEthernet0/0/0"] != "Test: Interface" {
t.Errorf("Unexpected interface description: %s", mib.descriptions["GigabitEthernet0/0/0"])
}
if mib.descriptions["GigabitEthernet0/0/0.100"] != "Test: Sub-interface" {
t.Errorf("Unexpected sub-interface description: %s", mib.descriptions["GigabitEthernet0/0/0.100"])
}
if mib.descriptions["loop0"] != "Test: Loopback" {
t.Errorf("Unexpected loopback description: %s", mib.descriptions["loop0"])
}
@@ -104,7 +104,7 @@ loopbacks:
func TestLoadVPPConfigNonExistentFile(t *testing.T) {
mib := NewInterfaceMIB()
err := mib.LoadVPPConfig("/nonexistent/file.yaml")
if err == nil {
t.Error("Expected error for non-existent file")
@@ -113,25 +113,25 @@ func TestLoadVPPConfigNonExistentFile(t *testing.T) {
func TestLoadVPPConfigInvalidYAML(t *testing.T) {
mib := NewInterfaceMIB()
// Create a temporary file with invalid YAML
invalidYAML := `interfaces:
test: [
`
tmpfile, err := os.CreateTemp("", "invalid_*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write([]byte(invalidYAML)); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
err = mib.LoadVPPConfig(tmpfile.Name())
if err == nil {
t.Error("Expected error for invalid YAML")
@@ -140,7 +140,7 @@ func TestLoadVPPConfigInvalidYAML(t *testing.T) {
func TestUpdateStatsBasic(t *testing.T) {
mib := NewInterfaceMIB()
// Create mock interface stats
stats := &api.InterfaceStats{
Interfaces: []api.InterfaceCounters{
@@ -158,15 +158,15 @@ func TestUpdateStatsBasic(t *testing.T) {
},
},
}
// Call UpdateStats (this will test the basic flow without AgentX sessions)
mib.UpdateStats(stats)
// Check that stats were stored
if len(mib.stats) != 1 {
t.Errorf("Expected 1 interface in stats, got %d", len(mib.stats))
}
if storedStats, exists := mib.stats[0]; !exists {
t.Error("Expected interface 0 to be stored in stats")
} else {
@@ -177,4 +177,4 @@ func TestUpdateStatsBasic(t *testing.T) {
t.Errorf("Expected RX packets 100, got %d", storedStats.Rx.Packets)
}
}
}
}

View File

@@ -13,7 +13,7 @@ import (
"govpp-snmp-agentx/config"
"govpp-snmp-agentx/ifmib"
"govpp-snmp-agentx/logger"
"govpp-snmp-agentx/vppstats"
"govpp-snmp-agentx/vpp"
)
func main() {
@@ -40,8 +40,11 @@ func main() {
log.Fatalf("Failed to start AgentX: %v", err)
}
// Set up interface event callback to update interface details
vpp.SetInterfaceEventCallback(interfaceMIB.UpdateInterfaceDetails)
// Start VPP stats routine with callback to update MIB
vppstats.StartStatsRoutine(interfaceMIB.UpdateStats)
vpp.StartStatsRoutine(interfaceMIB.UpdateStats)
// Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
@@ -50,7 +53,7 @@ func main() {
// Wait for shutdown signal
<-sigChan
logger.Printf("Shutting down...")
// Flush any buffered log entries
logger.Sync()
}

View File

@@ -14,7 +14,7 @@ func TestMainCompiles(t *testing.T) {
// This would run main(), but we skip it in tests
return
}
// Just test that we can access main package
t.Log("Main package compiles successfully")
}
}

145
src/vpp/vpp_iface.go Normal file
View File

@@ -0,0 +1,145 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package vpp
import (
"os"
"go.fd.io/govpp/api"
interfaces "go.fd.io/govpp/binapi/interface"
"go.fd.io/govpp/binapi/interface_types"
"govpp-snmp-agentx/logger"
)
// InterfaceDetails holds detailed information about a VPP interface
type InterfaceDetails struct {
SwIfIndex interface_types.InterfaceIndex
InterfaceName string
MacAddress []byte
Speed uint64
AdminStatus bool
OperStatus bool
MTU uint32
}
// InterfaceEventCallback is called when interface events occur
type InterfaceEventCallback func(details []InterfaceDetails)
// GetAllInterfaceDetails retrieves detailed information for all interfaces
func GetAllInterfaceDetails(ch api.Channel) ([]InterfaceDetails, error) {
logger.Debugf("Retrieving all interface details from VPP")
// Get all interfaces
reqCtx := ch.SendMultiRequest(&interfaces.SwInterfaceDump{
SwIfIndex: ^interface_types.InterfaceIndex(0), // All interfaces
})
var details []InterfaceDetails
for {
iface := &interfaces.SwInterfaceDetails{}
stop, err := reqCtx.ReceiveReply(iface)
if stop {
break
}
if err != nil {
logger.Debugf("Error retrieving interface details: %v", err)
return nil, err
}
// Convert VPP interface flags to admin/oper status
adminUp := (iface.Flags & interface_types.IF_STATUS_API_FLAG_ADMIN_UP) != 0
operUp := (iface.Flags & interface_types.IF_STATUS_API_FLAG_LINK_UP) != 0
detail := InterfaceDetails{
SwIfIndex: iface.SwIfIndex,
InterfaceName: string(iface.InterfaceName),
MacAddress: iface.L2Address[:],
Speed: uint64(iface.LinkSpeed) * 1000, // Convert Kbps to bps
AdminStatus: adminUp,
OperStatus: operUp,
MTU: uint32(iface.LinkMtu),
}
details = append(details, detail)
logger.Debugf("Interface %d (%s): MAC=%x, Speed=%d, Admin=%t, Oper=%t, MTU=%d",
detail.SwIfIndex, detail.InterfaceName, detail.MacAddress,
detail.Speed, detail.AdminStatus, detail.OperStatus, detail.MTU)
}
logger.Debugf("Retrieved details for %d interfaces", len(details))
return details, nil
}
func WatchInterfaceEvents(ch api.Channel, callback InterfaceEventCallback) error {
logger.Debugf("WatchInterfaceEvents() called - starting interface event monitoring")
notifChan := make(chan api.Message, 100)
// subscribe for specific event message
logger.Debugf("Subscribing to interface events...")
sub, err := ch.SubscribeNotification(notifChan, &interfaces.SwInterfaceEvent{})
if err != nil {
logger.Debugf("error subscribing to interface events: %v", err)
return err
}
logger.Debugf("Successfully subscribed to interface events")
// enable interface events in VPP
logger.Debugf("Enabling interface events in VPP...")
err = ch.SendRequest(&interfaces.WantInterfaceEvents{
PID: uint32(os.Getpid()),
EnableDisable: 1,
}).ReceiveReply(&interfaces.WantInterfaceEventsReply{})
if err != nil {
logger.Debugf("error enabling interface events: %v", err)
return err
}
logger.Debugf("Interface events enabled in VPP, starting event listener goroutine")
// receive notifications
go func() {
logger.Debugf("Interface event listener goroutine started")
defer func() {
logger.Debugf("Interface event listener goroutine shutting down")
// disable interface events in VPP
err = ch.SendRequest(&interfaces.WantInterfaceEvents{
PID: uint32(os.Getpid()),
EnableDisable: 0,
}).ReceiveReply(&interfaces.WantInterfaceEventsReply{})
if err != nil {
logger.Debugf("error disabling interface events: %v", err)
}
// unsubscribe from receiving events
err = sub.Unsubscribe()
if err != nil {
logger.Debugf("error unsubscribing from interface events: %v", err)
}
}()
logger.Debugf("Interface event listener waiting for events...")
for notif := range notifChan {
e := notif.(*interfaces.SwInterfaceEvent)
logger.Debugf("interface event: SwIfIndex=%d, Flags=%d, Deleted=%t",
e.SwIfIndex, e.Flags, e.Deleted)
// When an interface event occurs, retrieve all interface details and call callback
if callback != nil {
details, err := GetAllInterfaceDetails(ch)
if err != nil {
logger.Debugf("Failed to retrieve interface details after event: %v", err)
} else {
logger.Debugf("Calling interface event callback with %d interfaces", len(details))
callback(details)
}
}
}
logger.Debugf("Interface event listener goroutine ended")
}()
return nil
}

View File

@@ -1,6 +1,6 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package vppstats
package vpp
import (
"flag"
@@ -17,6 +17,9 @@ import (
type StatsCallback func(*api.InterfaceStats)
// Global callback for interface events
var interfaceEventCallback InterfaceEventCallback
var (
// Flags for VPP stats configuration
ApiAddr = flag.String("vppstats.api.addr", "/var/run/vpp/api.sock", "VPP API socket path")
@@ -25,6 +28,11 @@ var (
Period = flag.Int("vppstats.period", 10, "Interval in seconds for querying VPP interface stats")
)
// SetInterfaceEventCallback sets the callback for interface events
func SetInterfaceEventCallback(callback InterfaceEventCallback) {
interfaceEventCallback = callback
}
// StartStatsRoutine starts a goroutine that queries VPP interface stats at the configured interval
func StartStatsRoutine(callback StatsCallback) {
period := time.Duration(*Period) * time.Second
@@ -131,6 +139,32 @@ func statsRoutine(period time.Duration, callback StatsCallback) {
logger.Printf("Connected to VPP (API: %s, Stats: %s)", *ApiAddr, *StatsAddr)
connected = true
wasConnected = true
// Start watching interface events
logger.Debugf("Creating API channel for interface events...")
ch, err := conn.NewAPIChannel()
if err != nil {
logger.Debugf("Failed to create API channel for interface events: %v", err)
} else {
logger.Debugf("API channel created successfully, calling WatchInterfaceEvents...")
if err := WatchInterfaceEvents(ch, interfaceEventCallback); err != nil {
logger.Debugf("Failed to start interface event watching: %v", err)
ch.Close()
} else {
logger.Printf("Interface event watching started successfully")
// Do initial retrieval of interface details
if interfaceEventCallback != nil {
details, err := GetAllInterfaceDetails(ch)
if err != nil {
logger.Debugf("Failed to get initial interface details: %v", err)
} else {
logger.Debugf("Retrieved initial interface details for %d interfaces", len(details))
interfaceEventCallback(details)
}
}
}
}
}
// Query stats if connected
@@ -231,4 +265,3 @@ func checkVPPLiveness(conn *core.Connection) bool {
logger.Debugf("VPP liveness check passed (version: %s)", string(reply.Version))
return true
}

View File

@@ -1,163 +0,0 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package vppstats
import (
"flag"
"fmt"
"testing"
"time"
"go.fd.io/govpp/api"
)
func TestVPPStatsFlags(t *testing.T) {
// Test default values
if *ApiAddr != "/var/run/vpp/api.sock" {
t.Errorf("Expected default API address to be '/var/run/vpp/api.sock', got '%s'", *ApiAddr)
}
if *StatsAddr != "/var/run/vpp/stats.sock" {
t.Errorf("Expected default stats address to be '/var/run/vpp/stats.sock', got '%s'", *StatsAddr)
}
if *IfIndexOffset != 1000 {
t.Errorf("Expected default interface index offset to be 1000, got %d", *IfIndexOffset)
}
if *Period != 10 {
t.Errorf("Expected default period to be 10, got %d", *Period)
}
}
func TestFlagRegistrations(t *testing.T) {
tests := []struct {
name string
flagName string
defValue string
}{
{"API address", "vppstats.api.addr", "/var/run/vpp/api.sock"},
{"Stats address", "vppstats.stats.addr", "/var/run/vpp/stats.sock"},
{"Index offset", "vppstats.ifindex-offset", "1000"},
{"Period", "vppstats.period", "10"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := flag.Lookup(tt.flagName)
if f == nil {
t.Errorf("Expected %s flag to be registered", tt.flagName)
return
}
if f.DefValue != tt.defValue {
t.Errorf("Expected %s flag default value to be '%s', got '%s'",
tt.flagName, tt.defValue, f.DefValue)
}
})
}
}
func TestStatsCallbackType(t *testing.T) {
// Test that we can create a valid callback function
var called bool
var receivedStats *api.InterfaceStats
callback := func(stats *api.InterfaceStats) {
called = true
receivedStats = stats
}
// Create mock stats
mockStats := &api.InterfaceStats{
Interfaces: []api.InterfaceCounters{
{
InterfaceIndex: 1,
InterfaceName: "test",
},
},
}
// Call the callback
callback(mockStats)
if !called {
t.Error("Expected callback to be called")
}
if receivedStats != mockStats {
t.Error("Expected callback to receive the same stats object")
}
if len(receivedStats.Interfaces) != 1 {
t.Errorf("Expected 1 interface, got %d", len(receivedStats.Interfaces))
}
if receivedStats.Interfaces[0].InterfaceName != "test" {
t.Errorf("Expected interface name 'test', got '%s'", receivedStats.Interfaces[0].InterfaceName)
}
}
func TestPeriodConversion(t *testing.T) {
// Test that period conversion works correctly
originalPeriod := *Period
defer func() { *Period = originalPeriod }()
testPeriods := []struct {
input int
expected time.Duration
}{
{1, time.Second},
{5, 5 * time.Second},
{10, 10 * time.Second},
{60, time.Minute},
}
for _, tt := range testPeriods {
t.Run(fmt.Sprintf("period_%d", tt.input), func(t *testing.T) {
*Period = tt.input
result := time.Duration(*Period) * time.Second
if result != tt.expected {
t.Errorf("Expected period %v, got %v", tt.expected, result)
}
})
}
}
func TestFlagValues(t *testing.T) {
// Save original flag values
originalApiAddr := *ApiAddr
originalStatsAddr := *StatsAddr
originalOffset := *IfIndexOffset
originalPeriod := *Period
defer func() {
*ApiAddr = originalApiAddr
*StatsAddr = originalStatsAddr
*IfIndexOffset = originalOffset
*Period = originalPeriod
}()
// Test setting flag values
*ApiAddr = "/custom/api.sock"
*StatsAddr = "/custom/stats.sock"
*IfIndexOffset = 2000
*Period = 30
if *ApiAddr != "/custom/api.sock" {
t.Errorf("Expected API address to be '/custom/api.sock', got '%s'", *ApiAddr)
}
if *StatsAddr != "/custom/stats.sock" {
t.Errorf("Expected stats address to be '/custom/stats.sock', got '%s'", *StatsAddr)
}
if *IfIndexOffset != 2000 {
t.Errorf("Expected interface index offset to be 2000, got %d", *IfIndexOffset)
}
if *Period != 30 {
t.Errorf("Expected period to be 30, got %d", *Period)
}
}