Move Go code to src/

This commit is contained in:
Pim van Pelt
2025-06-17 00:47:08 +02:00
parent c0bcdd5449
commit 7f81b51c1f
61 changed files with 3 additions and 3 deletions

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

@ -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)
}
}