Compare commits
10 Commits
15216782d1
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
ead795674c | ||
|
dce4750b0f | ||
|
d65e055710 | ||
|
8ed14834f5 | ||
|
3401c96112 | ||
|
1889934a9c | ||
|
e93156324d | ||
|
bdaa2e366b | ||
|
96b9dd501d | ||
|
70cb134dcf |
21
debian/changelog
vendored
21
debian/changelog
vendored
@@ -1,3 +1,24 @@
|
|||||||
|
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
|
govpp-snmp-agentx (1.1.3-1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Use fallback packet counters when VPP unicast stats are unavailable
|
* Use fallback packet counters when VPP unicast stats are unavailable
|
||||||
|
@@ -17,7 +17,7 @@ VPP Stats Socket → VPP Stats Client → Interface MIB → AgentX → SNMPd
|
|||||||
|
|
||||||
The application consists of four main components:
|
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
|
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
|
3. **AgentX Client** (`src/agentx/`): Handles AgentX protocol connection and MIB registration
|
||||||
4. **Main Application** (`src/main.go`): Orchestrates the components and handles configuration
|
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)
|
- Stats socket accessible at `/var/run/vpp/stats.sock` (or custom path)
|
||||||
- Application must have read permissions on the stats socket
|
- 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 Requirements
|
||||||
|
|
||||||
- SNMP master agent running (net-snmp's snmpd)
|
- SNMP master agent running (net-snmp's snmpd)
|
||||||
@@ -314,9 +340,3 @@ upstream PR is merged.
|
|||||||
3. Make your changes
|
3. Make your changes
|
||||||
4. Add tests if applicable
|
4. Add tests if applicable
|
||||||
5. Submit a pull request
|
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 {
|
if m.ifXTableSession != nil {
|
||||||
m.ifXTableSession.Handler = m.handler
|
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))
|
logger.Debugf("IF-MIB now contains %d interfaces", len(m.stats))
|
||||||
|
@@ -14,6 +14,7 @@ func TestNewInterfaceMIB(t *testing.T) {
|
|||||||
|
|
||||||
if mib == nil {
|
if mib == nil {
|
||||||
t.Fatal("NewInterfaceMIB returned nil")
|
t.Fatal("NewInterfaceMIB returned nil")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if mib.handler == nil {
|
if mib.handler == nil {
|
||||||
|
26
src/main.go
26
src/main.go
@@ -16,7 +16,7 @@ import (
|
|||||||
"govpp-snmp-agentx/vpp"
|
"govpp-snmp-agentx/vpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "1.1.3-1"
|
const Version = "1.1.5-1"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
debug := flag.Bool("debug", false, "Enable debug logging")
|
debug := flag.Bool("debug", false, "Enable debug logging")
|
||||||
@@ -45,11 +45,22 @@ func main() {
|
|||||||
log.Fatalf("Failed to start AgentX: %v", err)
|
log.Fatalf("Failed to start AgentX: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up interface event callback to update interface details
|
// Create VPP client and managers
|
||||||
vpp.SetInterfaceEventCallback(interfaceMIB.UpdateInterfaceDetails)
|
vppClient := vpp.NewVPPClient()
|
||||||
|
interfaceManager := vpp.NewInterfaceManager(vppClient)
|
||||||
|
statsManager := vpp.NewStatsManager(vppClient)
|
||||||
|
|
||||||
// Start VPP stats routine with callback to update MIB
|
// Set up interface event callback to update interface details
|
||||||
vpp.StartStatsRoutine(interfaceMIB.UpdateStats)
|
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
|
// Set up signal handling for graceful shutdown
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
@@ -59,6 +70,11 @@ func main() {
|
|||||||
<-sigChan
|
<-sigChan
|
||||||
logger.Printf("Shutting down...")
|
logger.Printf("Shutting down...")
|
||||||
|
|
||||||
|
// Stop stats routine and interface monitoring, then disconnect
|
||||||
|
statsManager.StopStatsRoutine()
|
||||||
|
interfaceManager.StopEventMonitoring()
|
||||||
|
vppClient.Disconnect()
|
||||||
|
|
||||||
// Flush any buffered log entries
|
// Flush any buffered log entries
|
||||||
logger.Sync()
|
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 (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go.fd.io/govpp/api"
|
"go.fd.io/govpp/api"
|
||||||
interfaces "go.fd.io/govpp/binapi/interface"
|
interfaces "go.fd.io/govpp/binapi/interface"
|
||||||
@@ -26,8 +27,146 @@ type InterfaceDetails struct {
|
|||||||
// InterfaceEventCallback is called when interface events occur
|
// InterfaceEventCallback is called when interface events occur
|
||||||
type InterfaceEventCallback func(details []InterfaceDetails)
|
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
|
// 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")
|
logger.Debugf("Retrieving all interface details from VPP")
|
||||||
|
|
||||||
// Get all interfaces
|
// Get all interfaces
|
||||||
@@ -73,7 +212,8 @@ func GetAllInterfaceDetails(ch api.Channel) ([]InterfaceDetails, error) {
|
|||||||
return details, nil
|
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")
|
logger.Debugf("WatchInterfaceEvents() called - starting interface event monitoring")
|
||||||
|
|
||||||
notifChan := make(chan api.Message, 100)
|
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...")
|
logger.Debugf("Interface event listener waiting for events...")
|
||||||
for notif := range notifChan {
|
for notif := range notifChan {
|
||||||
e := notif.(*interfaces.SwInterfaceEvent)
|
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)
|
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 {
|
if callback != nil {
|
||||||
details, err := GetAllInterfaceDetails(ch)
|
callback()
|
||||||
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")
|
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
|
package vpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.fd.io/govpp/adapter/socketclient"
|
|
||||||
"go.fd.io/govpp/adapter/statsclient"
|
|
||||||
"go.fd.io/govpp/api"
|
"go.fd.io/govpp/api"
|
||||||
"go.fd.io/govpp/binapi/vpe"
|
|
||||||
"go.fd.io/govpp/core"
|
|
||||||
|
|
||||||
"govpp-snmp-agentx/logger"
|
"govpp-snmp-agentx/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// StatsCallback is called when interface stats are retrieved
|
||||||
type StatsCallback func(*api.InterfaceStats)
|
type StatsCallback func(*api.InterfaceStats)
|
||||||
|
|
||||||
// Global callback for interface events
|
// StatsManager handles VPP statistics operations
|
||||||
var interfaceEventCallback InterfaceEventCallback
|
type StatsManager struct {
|
||||||
|
client *VPPClient
|
||||||
var (
|
statsCallback StatsCallback
|
||||||
// Flags for VPP stats configuration
|
period time.Duration
|
||||||
ApiAddr = flag.String("vppstats.api.addr", "/var/run/vpp/api.sock", "VPP API socket path")
|
running bool
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartStatsRoutine starts a goroutine that queries VPP interface stats at the configured interval
|
// NewStatsManager creates a new stats manager
|
||||||
func StartStatsRoutine(callback StatsCallback) {
|
func NewStatsManager(client *VPPClient) *StatsManager {
|
||||||
period := time.Duration(*Period) * time.Second
|
return &StatsManager{
|
||||||
go statsRoutine(period, callback)
|
client: client,
|
||||||
|
period: time.Duration(*Period) * time.Second,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func statsRoutine(period time.Duration, callback StatsCallback) {
|
// SetStatsCallback sets the callback for stats updates
|
||||||
logger.Debugf("Starting VPP stats routine with API: %s, Stats: %s, period: %v", *ApiAddr, *StatsAddr, period)
|
func (sm *StatsManager) SetStatsCallback(callback StatsCallback) {
|
||||||
|
sm.statsCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
var conn *core.Connection
|
// SetPeriod sets the polling period for stats
|
||||||
var statsConn *core.StatsConnection
|
func (sm *StatsManager) SetPeriod(period time.Duration) {
|
||||||
var connected = false
|
sm.period = period
|
||||||
var wasConnected = false
|
}
|
||||||
|
|
||||||
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 ticker.Stop()
|
||||||
|
|
||||||
defer func() {
|
var wasConnected = false
|
||||||
// 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()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
if !sm.running {
|
||||||
|
logger.Debugf("Stats routine stopping")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we need to connect/reconnect
|
// Check if we need to connect/reconnect
|
||||||
if !connected {
|
if !sm.client.IsConnected() {
|
||||||
// Clean up existing connections
|
if wasConnected {
|
||||||
if conn != nil {
|
logger.Printf("VPP connection lost, attempting reconnect...")
|
||||||
func() {
|
wasConnected = false
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
logger.Debugf("API channel created successfully, calling WatchInterfaceEvents...")
|
logger.Printf("VPP not connected, attempting connection...")
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Query stats if connected
|
||||||
if connected {
|
if sm.client.IsConnected() {
|
||||||
if !queryInterfaceStats(conn, statsConn, callback) {
|
if !sm.queryAndReportStats() {
|
||||||
connected = false
|
logger.Printf("Stats query failed, marking connection as lost")
|
||||||
|
sm.client.Disconnect()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,26 +120,27 @@ func statsRoutine(period time.Duration, callback StatsCallback) {
|
|||||||
// Wait for next tick
|
// Wait for next tick
|
||||||
<-ticker.C
|
<-ticker.C
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Stats routine ended")
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryInterfaceStats(conn *core.Connection, statsConn *core.StatsConnection, callback StatsCallback) bool {
|
// queryAndReportStats queries stats and calls the callback
|
||||||
// Check VPP liveness using API call
|
func (sm *StatsManager) queryAndReportStats() bool {
|
||||||
if !checkVPPLiveness(conn) {
|
// Check VPP liveness first
|
||||||
logger.Printf("VPP liveness check failed")
|
if !sm.client.CheckLiveness() {
|
||||||
|
logger.Debugf("VPP liveness check failed")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the proper struct for interface stats
|
// Get interface stats
|
||||||
stats := new(api.InterfaceStats)
|
stats, err := sm.GetInterfaceStats()
|
||||||
|
if err != nil {
|
||||||
// Use the GetInterfaceStats method - this is the correct approach
|
|
||||||
if err := statsConn.GetInterfaceStats(stats); err != nil {
|
|
||||||
logger.Printf("Failed to get interface stats: %v", err)
|
logger.Printf("Failed to get interface stats: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always log basic info
|
// Debug log basic info
|
||||||
logger.Printf("Retrieved stats for %d interfaces", len(stats.Interfaces))
|
logger.Debugf("Retrieved stats for %d interfaces", len(stats.Interfaces))
|
||||||
|
|
||||||
// Debug logging for individual interfaces
|
// Debug logging for individual interfaces
|
||||||
for _, iface := range stats.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
|
// Call the callback to update the MIB
|
||||||
if callback != nil {
|
if sm.statsCallback != nil {
|
||||||
callback(stats)
|
sm.statsCallback(stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
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