Move Go code to src/
This commit is contained in:
Makefile
debian
src
agentx
config
go-agentx
.gitignoreAUTHORSLICENSEREADME.mdclient.goenvironment_test.gogo.modgo.sumhandler.gohelper_test.golist_handler.golist_handler_test.golist_item.go
go.modgo.summarshaler
pdu
allocate_index.goclose.godeallocate_index.goerror.goflags.goget.goget_next.goheader.goheader_packet.goobject_identifier.gooctet_string.goopen.gopacket.gorange.goranges.goreason.goregister.goresponse.gotimeout.gotype.gounregister.govariable.govariable_type.govariables.go
request.gosession.gosession_test.goshell.nixsnmpd.confvalue
ifmib
logger
main.gomain_test.govppstats
234
src/vppstats/stats.go
Normal file
234
src/vppstats/stats.go
Normal file
@ -0,0 +1,234 @@
|
||||
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||
|
||||
package vppstats
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type StatsCallback func(*api.InterfaceStats)
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func statsRoutine(period time.Duration, callback StatsCallback) {
|
||||
logger.Debugf("Starting VPP stats routine with API: %s, Stats: %s, period: %v", *ApiAddr, *StatsAddr, period)
|
||||
|
||||
var conn *core.Connection
|
||||
var statsConn *core.StatsConnection
|
||||
var connected = false
|
||||
var wasConnected = false
|
||||
|
||||
ticker := time.NewTicker(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()
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
// 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
|
||||
}
|
||||
|
||||
// Query stats if connected
|
||||
if connected {
|
||||
if !queryInterfaceStats(conn, statsConn, callback) {
|
||||
connected = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for next tick
|
||||
<-ticker.C
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
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 {
|
||||
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 logging for individual interfaces
|
||||
for _, iface := range stats.Interfaces {
|
||||
logger.Debugf("Interface %d (%s): RX %d pkts/%d bytes, TX %d pkts/%d bytes",
|
||||
iface.InterfaceIndex, iface.InterfaceName,
|
||||
iface.Rx.Packets, iface.Rx.Bytes,
|
||||
iface.Tx.Packets, iface.Tx.Bytes)
|
||||
}
|
||||
|
||||
// Call the callback to update the MIB
|
||||
if callback != nil {
|
||||
callback(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
|
||||
}
|
||||
|
163
src/vppstats/stats_test.go
Normal file
163
src/vppstats/stats_test.go
Normal file
@ -0,0 +1,163 @@
|
||||
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||
|
||||
package vppstats
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.fd.io/govpp/api"
|
||||
)
|
||||
|
||||
func TestVPPStatsFlags(t *testing.T) {
|
||||
// Test default values
|
||||
if *ApiAddr != "/var/run/vpp/api.sock" {
|
||||
t.Errorf("Expected default API address to be '/var/run/vpp/api.sock', got '%s'", *ApiAddr)
|
||||
}
|
||||
|
||||
if *StatsAddr != "/var/run/vpp/stats.sock" {
|
||||
t.Errorf("Expected default stats address to be '/var/run/vpp/stats.sock', got '%s'", *StatsAddr)
|
||||
}
|
||||
|
||||
if *IfIndexOffset != 1000 {
|
||||
t.Errorf("Expected default interface index offset to be 1000, got %d", *IfIndexOffset)
|
||||
}
|
||||
|
||||
if *Period != 10 {
|
||||
t.Errorf("Expected default period to be 10, got %d", *Period)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagRegistrations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flagName string
|
||||
defValue string
|
||||
}{
|
||||
{"API address", "vppstats.api.addr", "/var/run/vpp/api.sock"},
|
||||
{"Stats address", "vppstats.stats.addr", "/var/run/vpp/stats.sock"},
|
||||
{"Index offset", "vppstats.ifindex-offset", "1000"},
|
||||
{"Period", "vppstats.period", "10"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f := flag.Lookup(tt.flagName)
|
||||
if f == nil {
|
||||
t.Errorf("Expected %s flag to be registered", tt.flagName)
|
||||
return
|
||||
}
|
||||
|
||||
if f.DefValue != tt.defValue {
|
||||
t.Errorf("Expected %s flag default value to be '%s', got '%s'",
|
||||
tt.flagName, tt.defValue, f.DefValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsCallbackType(t *testing.T) {
|
||||
// Test that we can create a valid callback function
|
||||
var called bool
|
||||
var receivedStats *api.InterfaceStats
|
||||
|
||||
callback := func(stats *api.InterfaceStats) {
|
||||
called = true
|
||||
receivedStats = stats
|
||||
}
|
||||
|
||||
// Create mock stats
|
||||
mockStats := &api.InterfaceStats{
|
||||
Interfaces: []api.InterfaceCounters{
|
||||
{
|
||||
InterfaceIndex: 1,
|
||||
InterfaceName: "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Call the callback
|
||||
callback(mockStats)
|
||||
|
||||
if !called {
|
||||
t.Error("Expected callback to be called")
|
||||
}
|
||||
|
||||
if receivedStats != mockStats {
|
||||
t.Error("Expected callback to receive the same stats object")
|
||||
}
|
||||
|
||||
if len(receivedStats.Interfaces) != 1 {
|
||||
t.Errorf("Expected 1 interface, got %d", len(receivedStats.Interfaces))
|
||||
}
|
||||
|
||||
if receivedStats.Interfaces[0].InterfaceName != "test" {
|
||||
t.Errorf("Expected interface name 'test', got '%s'", receivedStats.Interfaces[0].InterfaceName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeriodConversion(t *testing.T) {
|
||||
// Test that period conversion works correctly
|
||||
originalPeriod := *Period
|
||||
defer func() { *Period = originalPeriod }()
|
||||
|
||||
testPeriods := []struct {
|
||||
input int
|
||||
expected time.Duration
|
||||
}{
|
||||
{1, time.Second},
|
||||
{5, 5 * time.Second},
|
||||
{10, 10 * time.Second},
|
||||
{60, time.Minute},
|
||||
}
|
||||
|
||||
for _, tt := range testPeriods {
|
||||
t.Run(fmt.Sprintf("period_%d", tt.input), func(t *testing.T) {
|
||||
*Period = tt.input
|
||||
result := time.Duration(*Period) * time.Second
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected period %v, got %v", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagValues(t *testing.T) {
|
||||
// Save original flag values
|
||||
originalApiAddr := *ApiAddr
|
||||
originalStatsAddr := *StatsAddr
|
||||
originalOffset := *IfIndexOffset
|
||||
originalPeriod := *Period
|
||||
|
||||
defer func() {
|
||||
*ApiAddr = originalApiAddr
|
||||
*StatsAddr = originalStatsAddr
|
||||
*IfIndexOffset = originalOffset
|
||||
*Period = originalPeriod
|
||||
}()
|
||||
|
||||
// Test setting flag values
|
||||
*ApiAddr = "/custom/api.sock"
|
||||
*StatsAddr = "/custom/stats.sock"
|
||||
*IfIndexOffset = 2000
|
||||
*Period = 30
|
||||
|
||||
if *ApiAddr != "/custom/api.sock" {
|
||||
t.Errorf("Expected API address to be '/custom/api.sock', got '%s'", *ApiAddr)
|
||||
}
|
||||
|
||||
if *StatsAddr != "/custom/stats.sock" {
|
||||
t.Errorf("Expected stats address to be '/custom/stats.sock', got '%s'", *StatsAddr)
|
||||
}
|
||||
|
||||
if *IfIndexOffset != 2000 {
|
||||
t.Errorf("Expected interface index offset to be 2000, got %d", *IfIndexOffset)
|
||||
}
|
||||
|
||||
if *Period != 30 {
|
||||
t.Errorf("Expected period to be 30, got %d", *Period)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user