Compare commits
12 Commits
0d19d50d62
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
ead795674c | ||
|
dce4750b0f | ||
|
d65e055710 | ||
|
8ed14834f5 | ||
|
3401c96112 | ||
|
1889934a9c | ||
|
e93156324d | ||
|
bdaa2e366b | ||
|
96b9dd501d | ||
|
70cb134dcf | ||
|
15216782d1 | ||
|
067e324cca |
30
debian/changelog
vendored
30
debian/changelog
vendored
@@ -1,3 +1,33 @@
|
||||
govpp-snmp-agentx (1.1.5-1) bookworm; urgency=medium
|
||||
|
||||
* Implement automatic interface deletion handling in IF-MIB
|
||||
* Simplify interface event processing by removing separate delete callbacks
|
||||
* Remove unused functions and clean up codebase (RemoveInterface, rebuildMIB)
|
||||
* Improve interface state synchronization between VPP and SNMP MIB
|
||||
* Automatically detect and remove deleted interfaces during updates
|
||||
|
||||
-- Pim van Pelt <pim@ipng.ch> Wed, 02 Jul 2025 00:00:00 +0000
|
||||
|
||||
govpp-snmp-agentx (1.1.4-1) bookworm; urgency=medium
|
||||
|
||||
* Major VPP module refactoring with improved separation of concerns
|
||||
* Replace legacy global functions with structured VPPClient, InterfaceManager, and StatsManager
|
||||
* Fix stats polling timing bug - now properly respects vppstats.period setting
|
||||
* Add comprehensive test suite with 64.6% code coverage
|
||||
* Improve connection management and error handling
|
||||
* Remove legacy compatibility functions for cleaner API
|
||||
|
||||
-- Pim van Pelt <pim@ipng.ch> Mon, 24 Jun 2025 01:00:00 +0000
|
||||
|
||||
govpp-snmp-agentx (1.1.3-1) bookworm; urgency=medium
|
||||
|
||||
* Use fallback packet counters when VPP unicast stats are unavailable
|
||||
* Fix unicast packet reporting for interfaces without detailed stats collection
|
||||
* Add VPP configuration comments for stats-collect feature requirements
|
||||
* Improve packet counter accuracy across different VPP configurations
|
||||
|
||||
-- Pim van Pelt <pim@ipng.ch> Mon, 24 Jun 2025 00:00:00 +0000
|
||||
|
||||
govpp-snmp-agentx (1.1.2-1) bookworm; urgency=medium
|
||||
|
||||
* Add startup version logging to INFO log level
|
||||
|
@@ -17,7 +17,7 @@ VPP Stats Socket → VPP Stats Client → Interface MIB → AgentX → SNMPd
|
||||
|
||||
The application consists of four main components:
|
||||
|
||||
1. **VPP Stats Client** (`src/vppstats/`): Connects to VPP stats socket and retrieves interface counters
|
||||
1. **VPP Stats Client** (`src/vpp/`): Connects to VPP stats socket and retrieves interface counters
|
||||
2. **Interface MIB** (`src/ifmib/`): Maps VPP statistics to SNMP IF-MIB structure
|
||||
3. **AgentX Client** (`src/agentx/`): Handles AgentX protocol connection and MIB registration
|
||||
4. **Main Application** (`src/main.go`): Orchestrates the components and handles configuration
|
||||
@@ -188,6 +188,32 @@ snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.6.2000
|
||||
- Stats socket accessible at `/var/run/vpp/stats.sock` (or custom path)
|
||||
- Application must have read permissions on the stats socket
|
||||
|
||||
### VPP Packet Counter Configuration
|
||||
|
||||
For accurate unicast, multicast, and broadcast packet counters, VPP requires specific feature arc configurations:
|
||||
|
||||
#### Receive Packet Counters
|
||||
To enable detailed RX packet counters (RxUnicast, RxMulticast, RxBroadcast), configure:
|
||||
```
|
||||
set interface feature <interface> stats-collect-rx arc device-input
|
||||
```
|
||||
|
||||
#### Transmit Packet Counters
|
||||
To enable detailed TX packet counters (TxUnicast, TxMulticast, TxBroadcast), configure:
|
||||
```
|
||||
set interface feature <interface> stats-collect-tx arc interface-output
|
||||
```
|
||||
|
||||
#### Fallback Behavior
|
||||
If these features are not enabled, the detailed packet counters will be zero. The SNMP agent automatically falls back to using the total packet counters (Rx.Packets and Tx.Packets) for unicast packet reporting to maintain SNMP compatibility.
|
||||
|
||||
**Example Configuration:**
|
||||
```bash
|
||||
# Enable detailed packet counters for GigabitEthernet0/8/0
|
||||
vppctl set interface feature GigabitEthernet0/8/0 stats-collect-rx arc device-input
|
||||
vppctl set interface feature GigabitEthernet0/8/0 stats-collect-tx arc interface-output
|
||||
```
|
||||
|
||||
### SNMP Requirements
|
||||
|
||||
- SNMP master agent running (net-snmp's snmpd)
|
||||
@@ -314,9 +340,3 @@ upstream PR is merged.
|
||||
3. Make your changes
|
||||
4. Add tests if applicable
|
||||
5. Submit a pull request
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.0.0**: Initial release with IF-MIB support
|
||||
- **v1.1.0**: Added configurable interface index offset
|
||||
- **v1.2.0**: Added Unix socket support for AgentX
|
@@ -184,7 +184,7 @@ func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
|
||||
}
|
||||
if m.ifXTableSession != nil {
|
||||
m.ifXTableSession.Handler = m.handler
|
||||
logger.Printf("Updated session handlers with new IF-MIB data")
|
||||
logger.Printf("Updated session handlers with new IF-MIB data for %d interfaces", len(m.stats))
|
||||
}
|
||||
|
||||
logger.Debugf("IF-MIB now contains %d interfaces", len(m.stats))
|
||||
@@ -294,7 +294,12 @@ func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
|
||||
// ifInUcastPkts (.11)
|
||||
item = m.handler.Add(fmt.Sprintf("%s.11.%d", ifEntryOID, idx))
|
||||
item.Type = pdu.VariableTypeCounter32
|
||||
item.Value = uint32(iface.RxUnicast.Packets)
|
||||
// iface.Rx*cast.Packets is only set if "set interface feature X stats-collect-rx arc device-input" is configured
|
||||
if iface.RxUnicast.Packets == 0 {
|
||||
item.Value = uint32(iface.Rx.Packets)
|
||||
} else {
|
||||
item.Value = uint32(iface.RxUnicast.Packets)
|
||||
}
|
||||
|
||||
// ifInNUcastPkts (.12) - multicast + broadcast
|
||||
item = m.handler.Add(fmt.Sprintf("%s.12.%d", ifEntryOID, idx))
|
||||
@@ -324,7 +329,12 @@ func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
|
||||
// ifOutUcastPkts (.17)
|
||||
item = m.handler.Add(fmt.Sprintf("%s.17.%d", ifEntryOID, idx))
|
||||
item.Type = pdu.VariableTypeCounter32
|
||||
item.Value = uint32(iface.TxUnicast.Packets)
|
||||
// iface.Tx*cast.Packets is only set if "set interface feature X stats-collect-tx arc interface-output" is configured
|
||||
if iface.TxUnicast.Packets == 0 {
|
||||
item.Value = uint32(iface.Tx.Packets)
|
||||
} else {
|
||||
item.Value = uint32(iface.TxUnicast.Packets)
|
||||
}
|
||||
|
||||
// ifOutNUcastPkts (.18) - multicast + broadcast
|
||||
item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifEntryOID, idx))
|
||||
@@ -385,7 +395,11 @@ func (m *InterfaceMIB) addIfXTable(iface *api.InterfaceCounters, idx int) {
|
||||
// ifHCInUcastPkts (.7)
|
||||
item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifXTableOID, idx))
|
||||
item.Type = pdu.VariableTypeCounter64
|
||||
item.Value = iface.RxUnicast.Packets
|
||||
if iface.RxUnicast.Packets == 0 {
|
||||
item.Value = iface.Rx.Packets
|
||||
} else {
|
||||
item.Value = iface.RxUnicast.Packets
|
||||
}
|
||||
|
||||
// ifHCInMulticastPkts (.8)
|
||||
item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifXTableOID, idx))
|
||||
@@ -405,7 +419,11 @@ func (m *InterfaceMIB) addIfXTable(iface *api.InterfaceCounters, idx int) {
|
||||
// ifHCOutUcastPkts (.11)
|
||||
item = m.handler.Add(fmt.Sprintf("%s.11.%d", ifXTableOID, idx))
|
||||
item.Type = pdu.VariableTypeCounter64
|
||||
item.Value = iface.TxUnicast.Packets
|
||||
if iface.TxUnicast.Packets == 0 {
|
||||
item.Value = iface.Tx.Packets
|
||||
} else {
|
||||
item.Value = iface.TxUnicast.Packets
|
||||
}
|
||||
|
||||
// ifHCOutMulticastPkts (.12)
|
||||
item = m.handler.Add(fmt.Sprintf("%s.12.%d", ifXTableOID, idx))
|
||||
|
@@ -14,6 +14,7 @@ func TestNewInterfaceMIB(t *testing.T) {
|
||||
|
||||
if mib == nil {
|
||||
t.Fatal("NewInterfaceMIB returned nil")
|
||||
return
|
||||
}
|
||||
|
||||
if mib.handler == nil {
|
||||
|
26
src/main.go
26
src/main.go
@@ -16,7 +16,7 @@ import (
|
||||
"govpp-snmp-agentx/vpp"
|
||||
)
|
||||
|
||||
const Version = "1.1.2-1"
|
||||
const Version = "1.1.5-1"
|
||||
|
||||
func main() {
|
||||
debug := flag.Bool("debug", false, "Enable debug logging")
|
||||
@@ -45,11 +45,22 @@ func main() {
|
||||
log.Fatalf("Failed to start AgentX: %v", err)
|
||||
}
|
||||
|
||||
// Set up interface event callback to update interface details
|
||||
vpp.SetInterfaceEventCallback(interfaceMIB.UpdateInterfaceDetails)
|
||||
// Create VPP client and managers
|
||||
vppClient := vpp.NewVPPClient()
|
||||
interfaceManager := vpp.NewInterfaceManager(vppClient)
|
||||
statsManager := vpp.NewStatsManager(vppClient)
|
||||
|
||||
// Start VPP stats routine with callback to update MIB
|
||||
vpp.StartStatsRoutine(interfaceMIB.UpdateStats)
|
||||
// Set up interface event callback to update interface details
|
||||
interfaceManager.SetEventCallback(interfaceMIB.UpdateInterfaceDetails)
|
||||
|
||||
// Set up stats callback to update MIB
|
||||
statsManager.SetStatsCallback(interfaceMIB.UpdateStats)
|
||||
|
||||
// Start VPP stats routine
|
||||
statsManager.StartStatsRoutine()
|
||||
|
||||
// Start interface event monitoring (handles reconnections automatically)
|
||||
interfaceManager.StartEventMonitoring()
|
||||
|
||||
// Set up signal handling for graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
@@ -59,6 +70,11 @@ func main() {
|
||||
<-sigChan
|
||||
logger.Printf("Shutting down...")
|
||||
|
||||
// Stop stats routine and interface monitoring, then disconnect
|
||||
statsManager.StopStatsRoutine()
|
||||
interfaceManager.StopEventMonitoring()
|
||||
vppClient.Disconnect()
|
||||
|
||||
// Flush any buffered log entries
|
||||
logger.Sync()
|
||||
}
|
||||
|
178
src/vpp/vpp.go
Normal file
178
src/vpp/vpp.go
Normal file
@@ -0,0 +1,178 @@
|
||||
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||
|
||||
package vpp
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"go.fd.io/govpp/adapter/socketclient"
|
||||
"go.fd.io/govpp/adapter/statsclient"
|
||||
"go.fd.io/govpp/api"
|
||||
"go.fd.io/govpp/binapi/vpe"
|
||||
"go.fd.io/govpp/core"
|
||||
|
||||
"govpp-snmp-agentx/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
// Flags for VPP configuration
|
||||
ApiAddr = flag.String("vppstats.api.addr", "/var/run/vpp/api.sock", "VPP API socket path")
|
||||
StatsAddr = flag.String("vppstats.stats.addr", "/var/run/vpp/stats.sock", "VPP stats socket path")
|
||||
IfIndexOffset = flag.Int("vppstats.ifindex-offset", 1000, "Offset to add to VPP interface indices for SNMP")
|
||||
Period = flag.Int("vppstats.period", 10, "Interval in seconds for querying VPP interface stats")
|
||||
)
|
||||
|
||||
// VPPClient manages VPP connections and provides a unified interface
|
||||
type VPPClient struct {
|
||||
apiConn *core.Connection
|
||||
statsConn *core.StatsConnection
|
||||
connected bool
|
||||
}
|
||||
|
||||
// NewVPPClient creates a new VPP client instance
|
||||
func NewVPPClient() *VPPClient {
|
||||
return &VPPClient{}
|
||||
}
|
||||
|
||||
// Connect establishes connections to both VPP API and Stats sockets
|
||||
func (c *VPPClient) Connect() error {
|
||||
logger.Debugf("Connecting to VPP (API: %s, Stats: %s)", *ApiAddr, *StatsAddr)
|
||||
|
||||
// Connect to API socket
|
||||
apiConn, err := core.Connect(socketclient.NewVppClient(*ApiAddr))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Connect to stats socket
|
||||
statsClient := statsclient.NewStatsClient(*StatsAddr)
|
||||
statsConn, err := core.ConnectStats(statsClient)
|
||||
if err != nil {
|
||||
// Clean up API connection on stats failure
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debugf("Recovered from API disconnect during stats error: %v", r)
|
||||
}
|
||||
}()
|
||||
apiConn.Disconnect()
|
||||
}()
|
||||
return err
|
||||
}
|
||||
|
||||
c.apiConn = apiConn
|
||||
c.statsConn = statsConn
|
||||
c.connected = true
|
||||
|
||||
logger.Printf("Connected to VPP (API: %s, Stats: %s)", *ApiAddr, *StatsAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect closes all VPP connections safely
|
||||
func (c *VPPClient) Disconnect() {
|
||||
if c.apiConn != nil {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debugf("Recovered from API disconnect panic: %v", r)
|
||||
}
|
||||
}()
|
||||
c.apiConn.Disconnect()
|
||||
}()
|
||||
c.apiConn = nil
|
||||
}
|
||||
|
||||
if c.statsConn != nil {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debugf("Recovered from stats disconnect panic: %v", r)
|
||||
}
|
||||
}()
|
||||
c.statsConn.Disconnect()
|
||||
}()
|
||||
c.statsConn = nil
|
||||
}
|
||||
|
||||
c.connected = false
|
||||
}
|
||||
|
||||
// IsConnected returns true if both API and Stats connections are active
|
||||
func (c *VPPClient) IsConnected() bool {
|
||||
return c.connected && c.apiConn != nil && c.statsConn != nil
|
||||
}
|
||||
|
||||
// GetAPIConnection returns the API connection for direct use
|
||||
func (c *VPPClient) GetAPIConnection() *core.Connection {
|
||||
return c.apiConn
|
||||
}
|
||||
|
||||
// GetStatsConnection returns the stats connection for direct use
|
||||
func (c *VPPClient) GetStatsConnection() *core.StatsConnection {
|
||||
return c.statsConn
|
||||
}
|
||||
|
||||
// NewAPIChannel creates a new API channel from the connection
|
||||
func (c *VPPClient) NewAPIChannel() (api.Channel, error) {
|
||||
if c.apiConn == nil {
|
||||
return nil, &VPPError{Message: "API connection not established"}
|
||||
}
|
||||
return c.apiConn.NewAPIChannel()
|
||||
}
|
||||
|
||||
// CheckLiveness performs a VPP liveness check using ShowVersion API call
|
||||
func (c *VPPClient) CheckLiveness() bool {
|
||||
if !c.IsConnected() {
|
||||
return false
|
||||
}
|
||||
|
||||
ch, err := c.NewAPIChannel()
|
||||
if err != nil {
|
||||
logger.Debugf("Failed to create API channel for liveness check: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
var channelClosed bool
|
||||
defer func() {
|
||||
if !channelClosed {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debugf("Recovered from channel close panic: %v", r)
|
||||
}
|
||||
}()
|
||||
ch.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
req := &vpe.ShowVersion{}
|
||||
reply := &vpe.ShowVersionReply{}
|
||||
|
||||
if err := ch.SendRequest(req).ReceiveReply(reply); err != nil {
|
||||
logger.Debugf("VPP ShowVersion failed: %v", err)
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debugf("Channel already closed during error handling")
|
||||
}
|
||||
}()
|
||||
ch.Close()
|
||||
channelClosed = true
|
||||
}()
|
||||
return false
|
||||
}
|
||||
|
||||
ch.Close()
|
||||
channelClosed = true
|
||||
|
||||
logger.Debugf("VPP liveness check passed (version: %s)", string(reply.Version))
|
||||
return true
|
||||
}
|
||||
|
||||
// VPPError represents a VPP-specific error
|
||||
type VPPError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *VPPError) Error() string {
|
||||
return e.Message
|
||||
}
|
@@ -4,6 +4,7 @@ package vpp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go.fd.io/govpp/api"
|
||||
interfaces "go.fd.io/govpp/binapi/interface"
|
||||
@@ -26,8 +27,146 @@ type InterfaceDetails struct {
|
||||
// InterfaceEventCallback is called when interface events occur
|
||||
type InterfaceEventCallback func(details []InterfaceDetails)
|
||||
|
||||
// InterfaceManager handles interface-related VPP operations
|
||||
type InterfaceManager struct {
|
||||
client *VPPClient
|
||||
eventCallback InterfaceEventCallback
|
||||
running bool
|
||||
watchingEvents bool
|
||||
}
|
||||
|
||||
// NewInterfaceManager creates a new interface manager
|
||||
func NewInterfaceManager(client *VPPClient) *InterfaceManager {
|
||||
return &InterfaceManager{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// SetEventCallback sets the callback for interface events
|
||||
func (im *InterfaceManager) SetEventCallback(callback InterfaceEventCallback) {
|
||||
im.eventCallback = callback
|
||||
}
|
||||
|
||||
// InitializeEventWatching starts event watching and retrieves initial interface details
|
||||
func (im *InterfaceManager) InitializeEventWatching() error {
|
||||
if !im.client.IsConnected() {
|
||||
return &VPPError{Message: "VPP client not connected"}
|
||||
}
|
||||
|
||||
// Start watching interface events
|
||||
if err := im.StartEventWatcher(); err != nil {
|
||||
logger.Debugf("Failed to start interface event watching: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debugf("Interface event watching started")
|
||||
|
||||
// Get initial interface details
|
||||
if details, err := im.GetAllInterfaceDetails(); err != nil {
|
||||
logger.Debugf("Failed to get initial interface details: %v", err)
|
||||
return err
|
||||
} else {
|
||||
logger.Debugf("Retrieved initial interface details for %d interfaces", len(details))
|
||||
if im.eventCallback != nil {
|
||||
im.eventCallback(details)
|
||||
}
|
||||
}
|
||||
|
||||
im.watchingEvents = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartEventMonitoring starts continuous monitoring for VPP connection and restarts event watching as needed
|
||||
func (im *InterfaceManager) StartEventMonitoring() {
|
||||
if im.running {
|
||||
logger.Debugf("Interface event monitoring already running")
|
||||
return
|
||||
}
|
||||
|
||||
im.running = true
|
||||
go im.eventMonitoringRoutine()
|
||||
}
|
||||
|
||||
// StopEventMonitoring stops the event monitoring routine
|
||||
func (im *InterfaceManager) StopEventMonitoring() {
|
||||
im.running = false
|
||||
}
|
||||
|
||||
// eventMonitoringRoutine continuously monitors VPP connection and manages event watching
|
||||
func (im *InterfaceManager) eventMonitoringRoutine() {
|
||||
logger.Debugf("Starting interface event monitoring routine")
|
||||
|
||||
for {
|
||||
if !im.running {
|
||||
logger.Debugf("Interface event monitoring routine stopping")
|
||||
break
|
||||
}
|
||||
|
||||
if im.client.IsConnected() {
|
||||
if !im.watchingEvents {
|
||||
if err := im.InitializeEventWatching(); err != nil {
|
||||
logger.Printf("Failed to initialize interface event watching: %v", err)
|
||||
} else {
|
||||
logger.Printf("Interface event watching started")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if im.watchingEvents {
|
||||
logger.Printf("VPP connection lost, interface event watching will restart on reconnection")
|
||||
im.watchingEvents = false
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
logger.Debugf("Interface event monitoring routine ended")
|
||||
}
|
||||
|
||||
// GetAllInterfaceDetails retrieves detailed information for all interfaces
|
||||
func GetAllInterfaceDetails(ch api.Channel) ([]InterfaceDetails, error) {
|
||||
func (im *InterfaceManager) GetAllInterfaceDetails() ([]InterfaceDetails, error) {
|
||||
if !im.client.IsConnected() {
|
||||
return nil, &VPPError{Message: "VPP client not connected"}
|
||||
}
|
||||
|
||||
ch, err := im.client.NewAPIChannel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ch.Close()
|
||||
|
||||
return getAllInterfaceDetails(ch)
|
||||
}
|
||||
|
||||
// StartEventWatcher starts watching for interface events
|
||||
func (im *InterfaceManager) StartEventWatcher() error {
|
||||
if !im.client.IsConnected() {
|
||||
return &VPPError{Message: "VPP client not connected"}
|
||||
}
|
||||
|
||||
ch, err := im.client.NewAPIChannel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return watchInterfaceEvents(ch, im.handleInterfaceEvent)
|
||||
}
|
||||
|
||||
// handleInterfaceEvent handles interface events and calls the callback
|
||||
func (im *InterfaceManager) handleInterfaceEvent() {
|
||||
if im.eventCallback != nil {
|
||||
details, err := im.GetAllInterfaceDetails()
|
||||
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))
|
||||
im.eventCallback(details)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getAllInterfaceDetails retrieves detailed information for all interfaces (internal function)
|
||||
func getAllInterfaceDetails(ch api.Channel) ([]InterfaceDetails, error) {
|
||||
logger.Debugf("Retrieving all interface details from VPP")
|
||||
|
||||
// Get all interfaces
|
||||
@@ -73,7 +212,8 @@ func GetAllInterfaceDetails(ch api.Channel) ([]InterfaceDetails, error) {
|
||||
return details, nil
|
||||
}
|
||||
|
||||
func WatchInterfaceEvents(ch api.Channel, callback InterfaceEventCallback) error {
|
||||
// watchInterfaceEvents watches for VPP interface events (internal function)
|
||||
func watchInterfaceEvents(ch api.Channel, callback func()) error {
|
||||
logger.Debugf("WatchInterfaceEvents() called - starting interface event monitoring")
|
||||
|
||||
notifChan := make(chan api.Message, 100)
|
||||
@@ -124,18 +264,12 @@ func WatchInterfaceEvents(ch api.Channel, callback InterfaceEventCallback) error
|
||||
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",
|
||||
logger.Printf("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
|
||||
// When an interface event occurs, call the 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)
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
logger.Debugf("Interface event listener goroutine ended")
|
||||
|
285
src/vpp/vpp_iface_test.go
Normal file
285
src/vpp/vpp_iface_test.go
Normal file
@@ -0,0 +1,285 @@
|
||||
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||
|
||||
package vpp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.fd.io/govpp/binapi/interface_types"
|
||||
)
|
||||
|
||||
func TestNewInterfaceManager(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewInterfaceManager(client)
|
||||
|
||||
if manager == nil {
|
||||
t.Fatal("NewInterfaceManager() returned nil")
|
||||
return
|
||||
}
|
||||
|
||||
if manager.client != client {
|
||||
t.Error("InterfaceManager should store the provided client")
|
||||
}
|
||||
|
||||
if manager.eventCallback != nil {
|
||||
t.Error("InterfaceManager should have nil callback initially")
|
||||
}
|
||||
|
||||
if manager.running {
|
||||
t.Error("InterfaceManager should not be running initially")
|
||||
}
|
||||
|
||||
if manager.watchingEvents {
|
||||
t.Error("InterfaceManager should not be watching events initially")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceManagerSetEventCallback(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewInterfaceManager(client)
|
||||
|
||||
var callbackCalled bool
|
||||
var receivedDetails []InterfaceDetails
|
||||
|
||||
callback := func(details []InterfaceDetails) {
|
||||
callbackCalled = true
|
||||
receivedDetails = details
|
||||
}
|
||||
|
||||
manager.SetEventCallback(callback)
|
||||
|
||||
if manager.eventCallback == nil {
|
||||
t.Error("SetEventCallback() should store the callback")
|
||||
}
|
||||
|
||||
// Test callback execution
|
||||
testDetails := []InterfaceDetails{
|
||||
{
|
||||
SwIfIndex: 1,
|
||||
InterfaceName: "test-interface",
|
||||
MacAddress: []byte{0xde, 0xad, 0xbe, 0xef, 0x00, 0x01},
|
||||
Speed: 1000000000,
|
||||
AdminStatus: true,
|
||||
OperStatus: true,
|
||||
MTU: 1500,
|
||||
},
|
||||
}
|
||||
|
||||
manager.eventCallback(testDetails)
|
||||
|
||||
if !callbackCalled {
|
||||
t.Error("Callback should have been called")
|
||||
}
|
||||
|
||||
if len(receivedDetails) != 1 {
|
||||
t.Errorf("Expected 1 interface detail, got %d", len(receivedDetails))
|
||||
}
|
||||
|
||||
if receivedDetails[0].InterfaceName != "test-interface" {
|
||||
t.Errorf("Expected interface name 'test-interface', got %q", receivedDetails[0].InterfaceName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceManagerGetAllInterfaceDetailsWithoutConnection(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewInterfaceManager(client)
|
||||
|
||||
_, err := manager.GetAllInterfaceDetails()
|
||||
if err == nil {
|
||||
t.Error("GetAllInterfaceDetails() should return error when not connected")
|
||||
}
|
||||
|
||||
vppErr, ok := err.(*VPPError)
|
||||
if !ok {
|
||||
t.Errorf("Expected VPPError, got %T", err)
|
||||
}
|
||||
|
||||
if vppErr.Message != "VPP client not connected" {
|
||||
t.Errorf("Expected specific error message, got: %s", vppErr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceManagerStartEventWatcherWithoutConnection(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewInterfaceManager(client)
|
||||
|
||||
err := manager.StartEventWatcher()
|
||||
if err == nil {
|
||||
t.Error("StartEventWatcher() should return error when not connected")
|
||||
}
|
||||
|
||||
vppErr, ok := err.(*VPPError)
|
||||
if !ok {
|
||||
t.Errorf("Expected VPPError, got %T", err)
|
||||
}
|
||||
|
||||
if vppErr.Message != "VPP client not connected" {
|
||||
t.Errorf("Expected specific error message, got: %s", vppErr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceManagerHandleInterfaceEventWithoutCallback(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewInterfaceManager(client)
|
||||
|
||||
// Should not panic when callback is nil
|
||||
manager.handleInterfaceEvent()
|
||||
}
|
||||
|
||||
func TestInterfaceManagerInitializeEventWatchingWithoutConnection(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewInterfaceManager(client)
|
||||
|
||||
err := manager.InitializeEventWatching()
|
||||
if err == nil {
|
||||
t.Error("InitializeEventWatching() should return error when not connected")
|
||||
}
|
||||
|
||||
vppErr, ok := err.(*VPPError)
|
||||
if !ok {
|
||||
t.Errorf("Expected VPPError, got %T", err)
|
||||
}
|
||||
|
||||
if vppErr.Message != "VPP client not connected" {
|
||||
t.Errorf("Expected specific error message, got: %s", vppErr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceDetails(t *testing.T) {
|
||||
details := InterfaceDetails{
|
||||
SwIfIndex: interface_types.InterfaceIndex(42),
|
||||
InterfaceName: "GigabitEthernet0/8/0",
|
||||
MacAddress: []byte{0x02, 0xfe, 0x3c, 0x4d, 0x5e, 0x6f},
|
||||
Speed: 10000000000, // 10 Gbps
|
||||
AdminStatus: true,
|
||||
OperStatus: false,
|
||||
MTU: 9000,
|
||||
}
|
||||
|
||||
if details.SwIfIndex != 42 {
|
||||
t.Errorf("Expected SwIfIndex 42, got %d", details.SwIfIndex)
|
||||
}
|
||||
|
||||
if details.InterfaceName != "GigabitEthernet0/8/0" {
|
||||
t.Errorf("Expected interface name 'GigabitEthernet0/8/0', got %q", details.InterfaceName)
|
||||
}
|
||||
|
||||
if len(details.MacAddress) != 6 {
|
||||
t.Errorf("Expected MAC address length 6, got %d", len(details.MacAddress))
|
||||
}
|
||||
|
||||
if details.Speed != 10000000000 {
|
||||
t.Errorf("Expected speed 10000000000, got %d", details.Speed)
|
||||
}
|
||||
|
||||
if !details.AdminStatus {
|
||||
t.Error("Expected AdminStatus true")
|
||||
}
|
||||
|
||||
if details.OperStatus {
|
||||
t.Error("Expected OperStatus false")
|
||||
}
|
||||
|
||||
if details.MTU != 9000 {
|
||||
t.Errorf("Expected MTU 9000, got %d", details.MTU)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceEventCallback(t *testing.T) {
|
||||
var callbackInvoked bool
|
||||
var callbackDetails []InterfaceDetails
|
||||
|
||||
callback := InterfaceEventCallback(func(details []InterfaceDetails) {
|
||||
callbackInvoked = true
|
||||
callbackDetails = details
|
||||
})
|
||||
|
||||
testDetails := []InterfaceDetails{
|
||||
{SwIfIndex: 1, InterfaceName: "test1"},
|
||||
{SwIfIndex: 2, InterfaceName: "test2"},
|
||||
}
|
||||
|
||||
callback(testDetails)
|
||||
|
||||
if !callbackInvoked {
|
||||
t.Error("Callback should have been invoked")
|
||||
}
|
||||
|
||||
if len(callbackDetails) != 2 {
|
||||
t.Errorf("Expected 2 interface details, got %d", len(callbackDetails))
|
||||
}
|
||||
|
||||
if callbackDetails[0].InterfaceName != "test1" {
|
||||
t.Errorf("Expected first interface 'test1', got %q", callbackDetails[0].InterfaceName)
|
||||
}
|
||||
|
||||
if callbackDetails[1].InterfaceName != "test2" {
|
||||
t.Errorf("Expected second interface 'test2', got %q", callbackDetails[1].InterfaceName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceManagerStartStopEventMonitoring(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewInterfaceManager(client)
|
||||
|
||||
if manager.running {
|
||||
t.Error("InterfaceManager should not be running initially")
|
||||
}
|
||||
|
||||
manager.StartEventMonitoring()
|
||||
|
||||
if !manager.running {
|
||||
t.Error("InterfaceManager should be running after StartEventMonitoring()")
|
||||
}
|
||||
|
||||
// Test starting again (should be safe)
|
||||
manager.StartEventMonitoring()
|
||||
|
||||
if !manager.running {
|
||||
t.Error("InterfaceManager should still be running after second StartEventMonitoring()")
|
||||
}
|
||||
|
||||
manager.StopEventMonitoring()
|
||||
|
||||
if manager.running {
|
||||
t.Error("InterfaceManager should not be running after StopEventMonitoring()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterfaceManagerEventMonitoringWithConnectionChanges(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewInterfaceManager(client)
|
||||
|
||||
// Set a callback to track calls
|
||||
var callbackCount int
|
||||
manager.SetEventCallback(func(details []InterfaceDetails) {
|
||||
callbackCount++
|
||||
})
|
||||
|
||||
manager.StartEventMonitoring()
|
||||
|
||||
// Let it run briefly
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Simulate VPP connection and disconnection by checking state changes
|
||||
initialWatchingState := manager.watchingEvents
|
||||
|
||||
// Stop monitoring
|
||||
manager.StopEventMonitoring()
|
||||
|
||||
// Verify it stopped
|
||||
if manager.running {
|
||||
t.Error("Event monitoring should have stopped")
|
||||
}
|
||||
|
||||
// The watching state should reflect the connection state
|
||||
if !client.IsConnected() && manager.watchingEvents {
|
||||
t.Error("Should not be watching events when disconnected")
|
||||
}
|
||||
|
||||
// Initial state should be false since we're not connected to VPP in tests
|
||||
if initialWatchingState {
|
||||
t.Error("Should not be watching events initially when VPP is not connected")
|
||||
}
|
||||
}
|
@@ -3,174 +3,116 @@
|
||||
package vpp
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"time"
|
||||
|
||||
"go.fd.io/govpp/adapter/socketclient"
|
||||
"go.fd.io/govpp/adapter/statsclient"
|
||||
"go.fd.io/govpp/api"
|
||||
"go.fd.io/govpp/binapi/vpe"
|
||||
"go.fd.io/govpp/core"
|
||||
|
||||
"govpp-snmp-agentx/logger"
|
||||
)
|
||||
|
||||
// StatsCallback is called when interface stats are retrieved
|
||||
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")
|
||||
StatsAddr = flag.String("vppstats.stats.addr", "/var/run/vpp/stats.sock", "VPP stats socket path")
|
||||
IfIndexOffset = flag.Int("vppstats.ifindex-offset", 1000, "Offset to add to VPP interface indices for SNMP")
|
||||
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
|
||||
// StatsManager handles VPP statistics operations
|
||||
type StatsManager struct {
|
||||
client *VPPClient
|
||||
statsCallback StatsCallback
|
||||
period time.Duration
|
||||
running bool
|
||||
}
|
||||
|
||||
// StartStatsRoutine starts a goroutine that queries VPP interface stats at the configured interval
|
||||
func StartStatsRoutine(callback StatsCallback) {
|
||||
period := time.Duration(*Period) * time.Second
|
||||
go statsRoutine(period, callback)
|
||||
// NewStatsManager creates a new stats manager
|
||||
func NewStatsManager(client *VPPClient) *StatsManager {
|
||||
return &StatsManager{
|
||||
client: client,
|
||||
period: time.Duration(*Period) * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func statsRoutine(period time.Duration, callback StatsCallback) {
|
||||
logger.Debugf("Starting VPP stats routine with API: %s, Stats: %s, period: %v", *ApiAddr, *StatsAddr, period)
|
||||
// SetStatsCallback sets the callback for stats updates
|
||||
func (sm *StatsManager) SetStatsCallback(callback StatsCallback) {
|
||||
sm.statsCallback = callback
|
||||
}
|
||||
|
||||
var conn *core.Connection
|
||||
var statsConn *core.StatsConnection
|
||||
var connected = false
|
||||
var wasConnected = false
|
||||
// SetPeriod sets the polling period for stats
|
||||
func (sm *StatsManager) SetPeriod(period time.Duration) {
|
||||
sm.period = period
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(period)
|
||||
// StartStatsRoutine starts the stats polling routine
|
||||
func (sm *StatsManager) StartStatsRoutine() {
|
||||
if sm.running {
|
||||
logger.Debugf("Stats routine already running")
|
||||
return
|
||||
}
|
||||
|
||||
sm.running = true
|
||||
go sm.statsRoutine()
|
||||
}
|
||||
|
||||
// StopStatsRoutine stops the stats polling routine
|
||||
func (sm *StatsManager) StopStatsRoutine() {
|
||||
sm.running = false
|
||||
}
|
||||
|
||||
// GetInterfaceStats retrieves current interface statistics
|
||||
func (sm *StatsManager) GetInterfaceStats() (*api.InterfaceStats, error) {
|
||||
if !sm.client.IsConnected() {
|
||||
return nil, &VPPError{Message: "VPP client not connected"}
|
||||
}
|
||||
|
||||
statsConn := sm.client.GetStatsConnection()
|
||||
if statsConn == nil {
|
||||
return nil, &VPPError{Message: "Stats connection not available"}
|
||||
}
|
||||
|
||||
stats := new(api.InterfaceStats)
|
||||
if err := statsConn.GetInterfaceStats(stats); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// statsRoutine is the main stats polling loop
|
||||
func (sm *StatsManager) statsRoutine() {
|
||||
logger.Debugf("Starting VPP stats routine with period: %v", sm.period)
|
||||
|
||||
ticker := time.NewTicker(sm.period)
|
||||
defer ticker.Stop()
|
||||
|
||||
defer func() {
|
||||
// Safely disconnect connections with panic recovery
|
||||
if conn != nil {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debugf("Recovered from conn.Disconnect panic: %v", r)
|
||||
}
|
||||
}()
|
||||
conn.Disconnect()
|
||||
}()
|
||||
}
|
||||
if statsConn != nil {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debugf("Recovered from statsConn.Disconnect panic: %v", r)
|
||||
}
|
||||
}()
|
||||
statsConn.Disconnect()
|
||||
}()
|
||||
}
|
||||
}()
|
||||
var wasConnected = false
|
||||
|
||||
for {
|
||||
if !sm.running {
|
||||
logger.Debugf("Stats routine stopping")
|
||||
break
|
||||
}
|
||||
|
||||
// Check if we need to connect/reconnect
|
||||
if !connected {
|
||||
// Clean up existing connections
|
||||
if conn != nil {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debugf("Recovered from conn.Disconnect during reconnect: %v", r)
|
||||
}
|
||||
}()
|
||||
conn.Disconnect()
|
||||
}()
|
||||
conn = nil
|
||||
}
|
||||
if statsConn != nil {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debugf("Recovered from statsConn.Disconnect during reconnect: %v", r)
|
||||
}
|
||||
}()
|
||||
statsConn.Disconnect()
|
||||
}()
|
||||
statsConn = nil
|
||||
}
|
||||
|
||||
// Create API connection first - only proceed if this succeeds
|
||||
var err error
|
||||
conn, err = core.Connect(socketclient.NewVppClient(*ApiAddr))
|
||||
if err != nil {
|
||||
if wasConnected {
|
||||
logger.Printf("VPP API connection lost: %v", err)
|
||||
wasConnected = false
|
||||
} else {
|
||||
logger.Debugf("Failed to connect to VPP API: %v", err)
|
||||
}
|
||||
connected = false
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// Only try stats connection if API connection succeeded
|
||||
statsClient := statsclient.NewStatsClient(*StatsAddr)
|
||||
statsConn, err = core.ConnectStats(statsClient)
|
||||
if err != nil {
|
||||
logger.Printf("VPP stats connection failed: %v", err)
|
||||
// Close the API connection since we can't get stats
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debugf("Recovered from conn.Disconnect during stats error: %v", r)
|
||||
}
|
||||
}()
|
||||
conn.Disconnect()
|
||||
}()
|
||||
conn = nil
|
||||
connected = false
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
if !sm.client.IsConnected() {
|
||||
if wasConnected {
|
||||
logger.Printf("VPP connection lost, attempting reconnect...")
|
||||
wasConnected = false
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.Printf("VPP not connected, attempting connection...")
|
||||
}
|
||||
|
||||
if err := sm.client.Connect(); err != nil {
|
||||
logger.Printf("Failed to connect to VPP: %v", err)
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Printf("VPP connection established")
|
||||
wasConnected = true
|
||||
}
|
||||
|
||||
// Query stats if connected
|
||||
if connected {
|
||||
if !queryInterfaceStats(conn, statsConn, callback) {
|
||||
connected = false
|
||||
if sm.client.IsConnected() {
|
||||
if !sm.queryAndReportStats() {
|
||||
logger.Printf("Stats query failed, marking connection as lost")
|
||||
sm.client.Disconnect()
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -178,26 +120,27 @@ func statsRoutine(period time.Duration, callback StatsCallback) {
|
||||
// Wait for next tick
|
||||
<-ticker.C
|
||||
}
|
||||
|
||||
logger.Debugf("Stats routine ended")
|
||||
}
|
||||
|
||||
func queryInterfaceStats(conn *core.Connection, statsConn *core.StatsConnection, callback StatsCallback) bool {
|
||||
// Check VPP liveness using API call
|
||||
if !checkVPPLiveness(conn) {
|
||||
logger.Printf("VPP liveness check failed")
|
||||
// queryAndReportStats queries stats and calls the callback
|
||||
func (sm *StatsManager) queryAndReportStats() bool {
|
||||
// Check VPP liveness first
|
||||
if !sm.client.CheckLiveness() {
|
||||
logger.Debugf("VPP liveness check failed")
|
||||
return false
|
||||
}
|
||||
|
||||
// Create the proper struct for interface stats
|
||||
stats := new(api.InterfaceStats)
|
||||
|
||||
// Use the GetInterfaceStats method - this is the correct approach
|
||||
if err := statsConn.GetInterfaceStats(stats); err != nil {
|
||||
// Get interface stats
|
||||
stats, err := sm.GetInterfaceStats()
|
||||
if err != nil {
|
||||
logger.Printf("Failed to get interface stats: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Always log basic info
|
||||
logger.Printf("Retrieved stats for %d interfaces", len(stats.Interfaces))
|
||||
// Debug log basic info
|
||||
logger.Debugf("Retrieved stats for %d interfaces", len(stats.Interfaces))
|
||||
|
||||
// Debug logging for individual interfaces
|
||||
for _, iface := range stats.Interfaces {
|
||||
@@ -208,60 +151,9 @@ func queryInterfaceStats(conn *core.Connection, statsConn *core.StatsConnection,
|
||||
}
|
||||
|
||||
// Call the callback to update the MIB
|
||||
if callback != nil {
|
||||
callback(stats)
|
||||
if sm.statsCallback != nil {
|
||||
sm.statsCallback(stats)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func checkVPPLiveness(conn *core.Connection) bool {
|
||||
// Create a channel for the API call
|
||||
ch, err := conn.NewAPIChannel()
|
||||
if err != nil {
|
||||
logger.Debugf("Failed to create API channel: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Use a flag to track if channel was closed successfully
|
||||
var channelClosed bool
|
||||
defer func() {
|
||||
if !channelClosed {
|
||||
// Recover from potential panic when closing already closed channel
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debugf("Recovered from channel close panic: %v", r)
|
||||
}
|
||||
}()
|
||||
ch.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Create ShowVersion request
|
||||
req := &vpe.ShowVersion{}
|
||||
reply := &vpe.ShowVersionReply{}
|
||||
|
||||
// Send the request with timeout
|
||||
if err := ch.SendRequest(req).ReceiveReply(reply); err != nil {
|
||||
logger.Debugf("VPP ShowVersion failed: %v", err)
|
||||
// Try to close the channel properly on error
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Debugf("Channel already closed during error handling")
|
||||
}
|
||||
}()
|
||||
ch.Close()
|
||||
channelClosed = true
|
||||
}()
|
||||
return false
|
||||
}
|
||||
|
||||
// Close channel successfully
|
||||
ch.Close()
|
||||
channelClosed = true
|
||||
|
||||
// If we got here, VPP is responsive
|
||||
logger.Debugf("VPP liveness check passed (version: %s)", string(reply.Version))
|
||||
return true
|
||||
}
|
||||
|
240
src/vpp/vpp_stats_test.go
Normal file
240
src/vpp/vpp_stats_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||
|
||||
package vpp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.fd.io/govpp/api"
|
||||
)
|
||||
|
||||
func TestNewStatsManager(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewStatsManager(client)
|
||||
|
||||
if manager == nil {
|
||||
t.Fatal("NewStatsManager() returned nil")
|
||||
return
|
||||
}
|
||||
|
||||
if manager.client != client {
|
||||
t.Error("StatsManager should store the provided client")
|
||||
}
|
||||
|
||||
if manager.period != time.Duration(*Period)*time.Second {
|
||||
t.Errorf("Expected period %v, got %v", time.Duration(*Period)*time.Second, manager.period)
|
||||
}
|
||||
|
||||
if manager.running {
|
||||
t.Error("StatsManager should not be running initially")
|
||||
}
|
||||
|
||||
if manager.statsCallback != nil {
|
||||
t.Error("StatsManager should have nil callback initially")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsManagerSetStatsCallback(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewStatsManager(client)
|
||||
|
||||
var callbackCalled bool
|
||||
var receivedStats *api.InterfaceStats
|
||||
|
||||
callback := func(stats *api.InterfaceStats) {
|
||||
callbackCalled = true
|
||||
receivedStats = stats
|
||||
}
|
||||
|
||||
manager.SetStatsCallback(callback)
|
||||
|
||||
if manager.statsCallback == nil {
|
||||
t.Error("SetStatsCallback() should store the callback")
|
||||
}
|
||||
|
||||
// Test callback execution
|
||||
testStats := &api.InterfaceStats{
|
||||
Interfaces: []api.InterfaceCounters{
|
||||
{
|
||||
InterfaceIndex: 1,
|
||||
InterfaceName: "test-interface",
|
||||
Rx: api.InterfaceCounterCombined{Packets: 100, Bytes: 1500},
|
||||
Tx: api.InterfaceCounterCombined{Packets: 50, Bytes: 750},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
manager.statsCallback(testStats)
|
||||
|
||||
if !callbackCalled {
|
||||
t.Error("Callback should have been called")
|
||||
}
|
||||
|
||||
if receivedStats != testStats {
|
||||
t.Error("Callback should 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-interface" {
|
||||
t.Errorf("Expected interface name 'test-interface', got %q", receivedStats.Interfaces[0].InterfaceName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsManagerSetPeriod(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewStatsManager(client)
|
||||
|
||||
newPeriod := 5 * time.Second
|
||||
manager.SetPeriod(newPeriod)
|
||||
|
||||
if manager.period != newPeriod {
|
||||
t.Errorf("Expected period %v, got %v", newPeriod, manager.period)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsManagerStartStopStatsRoutine(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewStatsManager(client)
|
||||
|
||||
if manager.running {
|
||||
t.Error("StatsManager should not be running initially")
|
||||
}
|
||||
|
||||
manager.StartStatsRoutine()
|
||||
|
||||
if !manager.running {
|
||||
t.Error("StatsManager should be running after StartStatsRoutine()")
|
||||
}
|
||||
|
||||
// Test starting again (should be safe)
|
||||
manager.StartStatsRoutine()
|
||||
|
||||
if !manager.running {
|
||||
t.Error("StatsManager should still be running after second StartStatsRoutine()")
|
||||
}
|
||||
|
||||
manager.StopStatsRoutine()
|
||||
|
||||
if manager.running {
|
||||
t.Error("StatsManager should not be running after StopStatsRoutine()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsManagerGetInterfaceStatsWithoutConnection(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewStatsManager(client)
|
||||
|
||||
_, err := manager.GetInterfaceStats()
|
||||
if err == nil {
|
||||
t.Error("GetInterfaceStats() should return error when not connected")
|
||||
}
|
||||
|
||||
vppErr, ok := err.(*VPPError)
|
||||
if !ok {
|
||||
t.Errorf("Expected VPPError, got %T", err)
|
||||
}
|
||||
|
||||
if vppErr.Message != "VPP client not connected" {
|
||||
t.Errorf("Expected specific error message, got: %s", vppErr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsCallback(t *testing.T) {
|
||||
var callbackInvoked bool
|
||||
var callbackStats *api.InterfaceStats
|
||||
|
||||
callback := StatsCallback(func(stats *api.InterfaceStats) {
|
||||
callbackInvoked = true
|
||||
callbackStats = stats
|
||||
})
|
||||
|
||||
testStats := &api.InterfaceStats{
|
||||
Interfaces: []api.InterfaceCounters{
|
||||
{
|
||||
InterfaceIndex: 42,
|
||||
InterfaceName: "test-callback-interface",
|
||||
Rx: api.InterfaceCounterCombined{Packets: 200, Bytes: 3000},
|
||||
Tx: api.InterfaceCounterCombined{Packets: 100, Bytes: 1500},
|
||||
RxUnicast: api.InterfaceCounterCombined{Packets: 180, Bytes: 2700},
|
||||
TxUnicast: api.InterfaceCounterCombined{Packets: 90, Bytes: 1350},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
callback(testStats)
|
||||
|
||||
if !callbackInvoked {
|
||||
t.Error("Callback should have been invoked")
|
||||
}
|
||||
|
||||
if callbackStats != testStats {
|
||||
t.Error("Callback should receive the same stats object")
|
||||
}
|
||||
|
||||
if len(callbackStats.Interfaces) != 1 {
|
||||
t.Errorf("Expected 1 interface, got %d", len(callbackStats.Interfaces))
|
||||
}
|
||||
|
||||
iface := callbackStats.Interfaces[0]
|
||||
if iface.InterfaceIndex != 42 {
|
||||
t.Errorf("Expected interface index 42, got %d", iface.InterfaceIndex)
|
||||
}
|
||||
|
||||
if iface.InterfaceName != "test-callback-interface" {
|
||||
t.Errorf("Expected interface name 'test-callback-interface', got %q", iface.InterfaceName)
|
||||
}
|
||||
|
||||
if iface.Rx.Packets != 200 {
|
||||
t.Errorf("Expected RX packets 200, got %d", iface.Rx.Packets)
|
||||
}
|
||||
|
||||
if iface.Tx.Bytes != 1500 {
|
||||
t.Errorf("Expected TX bytes 1500, got %d", iface.Tx.Bytes)
|
||||
}
|
||||
|
||||
if iface.RxUnicast.Packets != 180 {
|
||||
t.Errorf("Expected RX unicast packets 180, got %d", iface.RxUnicast.Packets)
|
||||
}
|
||||
|
||||
if iface.TxUnicast.Bytes != 1350 {
|
||||
t.Errorf("Expected TX unicast bytes 1350, got %d", iface.TxUnicast.Bytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsManagerQueryAndReportStatsWithoutConnection(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewStatsManager(client)
|
||||
|
||||
// Should return false when not connected
|
||||
if manager.queryAndReportStats() {
|
||||
t.Error("queryAndReportStats() should return false when not connected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsManagerWithShortPeriod(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewStatsManager(client)
|
||||
|
||||
// Set a very short period for testing
|
||||
manager.SetPeriod(10 * time.Millisecond)
|
||||
|
||||
if manager.period != 10*time.Millisecond {
|
||||
t.Errorf("Expected period 10ms, got %v", manager.period)
|
||||
}
|
||||
|
||||
manager.StartStatsRoutine()
|
||||
|
||||
// Let it run briefly
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
manager.StopStatsRoutine()
|
||||
|
||||
// Should stop gracefully
|
||||
if manager.running {
|
||||
t.Error("StatsManager should have stopped")
|
||||
}
|
||||
}
|
100
src/vpp/vpp_test.go
Normal file
100
src/vpp/vpp_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||
|
||||
package vpp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewVPPClient(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
|
||||
if client == nil {
|
||||
t.Fatal("NewVPPClient() returned nil")
|
||||
}
|
||||
|
||||
if client.IsConnected() {
|
||||
t.Error("NewVPPClient() should return disconnected client")
|
||||
}
|
||||
|
||||
if client.GetAPIConnection() != nil {
|
||||
t.Error("NewVPPClient() should have nil API connection initially")
|
||||
}
|
||||
|
||||
if client.GetStatsConnection() != nil {
|
||||
t.Error("NewVPPClient() should have nil stats connection initially")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVPPClientDisconnect(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
|
||||
// Should be safe to call disconnect on unconnected client
|
||||
client.Disconnect()
|
||||
|
||||
if client.IsConnected() {
|
||||
t.Error("Client should not be connected after Disconnect()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVPPClientNewAPIChannelWithoutConnection(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
|
||||
_, err := client.NewAPIChannel()
|
||||
if err == nil {
|
||||
t.Error("NewAPIChannel() should return error when not connected")
|
||||
}
|
||||
|
||||
vppErr, ok := err.(*VPPError)
|
||||
if !ok {
|
||||
t.Errorf("Expected VPPError, got %T", err)
|
||||
}
|
||||
|
||||
if vppErr.Message != "API connection not established" {
|
||||
t.Errorf("Expected specific error message, got: %s", vppErr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVPPClientCheckLivenessWithoutConnection(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
|
||||
if client.CheckLiveness() {
|
||||
t.Error("CheckLiveness() should return false when not connected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVPPError(t *testing.T) {
|
||||
err := &VPPError{Message: "test error"}
|
||||
|
||||
if err.Error() != "test error" {
|
||||
t.Errorf("VPPError.Error() returned %q, expected %q", err.Error(), "test error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVPPClientConnectWithInvalidPaths(t *testing.T) {
|
||||
// Save original values
|
||||
origApiAddr := *ApiAddr
|
||||
origStatsAddr := *StatsAddr
|
||||
|
||||
// Set invalid paths
|
||||
*ApiAddr = "/tmp/nonexistent_api.sock"
|
||||
*StatsAddr = "/tmp/nonexistent_stats.sock"
|
||||
|
||||
// Restore original values after test
|
||||
defer func() {
|
||||
*ApiAddr = origApiAddr
|
||||
*StatsAddr = origStatsAddr
|
||||
}()
|
||||
|
||||
client := NewVPPClient()
|
||||
err := client.Connect()
|
||||
|
||||
if err == nil {
|
||||
t.Error("Connect() should fail with invalid socket paths")
|
||||
client.Disconnect() // Clean up if somehow it connected
|
||||
}
|
||||
|
||||
if client.IsConnected() {
|
||||
t.Error("Client should not be connected after failed Connect()")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user