Compare commits

...

19 Commits

Author SHA1 Message Date
ead795674c lint fix: staticcheck and null derefs 2025-07-03 07:13:42 +02:00
dce4750b0f Bump version v1.1.5-1 2025-07-02 23:16:20 +02:00
d65e055710 Fix test 2025-07-02 22:31:23 +02:00
8ed14834f5 tighten up logging 2025-06-24 07:51:37 +02:00
3401c96112 Make statsmanager and interfacemanager independent. Add reconnect logic for EventMonitoring 2025-06-24 07:36:10 +02:00
1889934a9c Release 1.1.4-1 2025-06-24 07:06:09 +02:00
e93156324d Add tests 2025-06-24 07:03:34 +02:00
bdaa2e366b Refactor vpp.go to have the connection mgmt and vpp_*.go to have one Manager each 2025-06-24 07:00:52 +02:00
96b9dd501d Update paths 2025-06-24 06:40:40 +02:00
70cb134dcf Add a note about stats-collect-rx and stats-collect-tx 2025-06-24 06:37:30 +02:00
15216782d1 Debian package 1.1.3-1 2025-06-24 06:28:18 +02:00
067e324cca Make a compromise: Use Rx.Packets and Tx.Packets if the *Unicast.Packets is empty. Add a comment in the code to explain why 2025-06-24 06:25:46 +02:00
0d19d50d62 Add make sync-version to keep changelog and main.go Version the same. Update docs. Cut 1.1.2-1 2025-06-23 20:56:59 +02:00
686bbe46b0 Add IF-MIB::ifHighSpeed for >2.5Gbps interfaces
Cut a Debian release.
2025-06-23 20:47:25 +02:00
ccc2b5ad4d Cut v1.1.0-1 2025-06-23 19:16:02 +02:00
4f368e625d Use interface details to populate the ifmib, on startup and after each event 2025-06-23 19:11:28 +02:00
35165b0464 Add watchInterfaceEvents() listener 2025-06-23 19:02:02 +02:00
42dbbded3d Simplify debian dependencies, cut release 1.0.3-1 2025-06-19 15:57:53 +02:00
f16a2b41ea Remove toolchain 2025-06-19 15:48:43 +02:00
17 changed files with 1574 additions and 473 deletions

@ -1,6 +1,6 @@
PROG = govpp-snmp-agentx PROG = govpp-snmp-agentx
.PHONY: build test clean pkg-deb .PHONY: build test clean pkg-deb sync-version
# Build the binary # Build the binary
build: build:
@ -17,6 +17,13 @@ clean:
rm -rf debian/.debhelper debian/.gocache debian/go debian/$(PROG) debian/files debian/*.substvars debian/debhelper-build-stamp rm -rf debian/.debhelper debian/.gocache debian/go debian/$(PROG) debian/files debian/*.substvars debian/debhelper-build-stamp
rm -f ../$(PROG)_*.deb ../$(PROG)_*.changes ../$(PROG)_*.buildinfo rm -f ../$(PROG)_*.deb ../$(PROG)_*.changes ../$(PROG)_*.buildinfo
# Sync version from debian/changelog to main.go
sync-version:
@echo "Syncing version from debian/changelog to main.go..."
@VERSION=$$(head -1 debian/changelog | sed -n 's/.*(\([^)]*\)).*/\1/p'); \
sed -i 's/^const Version = ".*"/const Version = "'"$$VERSION"'"/' src/main.go; \
echo "Updated Version const to: $$VERSION"
# Build Debian package # Build Debian package
pkg-deb: pkg-deb: sync-version
fakeroot dpkg-buildpackage -us -uc -b fakeroot dpkg-buildpackage -us -uc -b

68
debian/changelog vendored

@ -1,3 +1,71 @@
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
* Implement automatic version synchronization between debian/changelog and main.go
* Add make sync-version target for manual version syncing
* Ensure version consistency across package and application code
-- Pim van Pelt <pim@ipng.ch> Sun, 23 Jun 2025 00:20:00 +0000
govpp-snmp-agentx (1.1.1-1) bookworm; urgency=medium
* Add IF-MIB::ifHighSpeed field (OID 1.3.6.1.2.1.31.1.1.1.15)
* Populate ifHighSpeed with interface speed in Megabits per second
* Implement conditional ifSpeed population (skip for speeds > 2.5Gbps)
* Improve SNMP compliance for high-speed interface reporting
-- Pim van Pelt <pim@ipng.ch> Sun, 23 Jun 2025 00:10:00 +0000
govpp-snmp-agentx (1.1.0-1) bookworm; urgency=medium
* Add interface event monitoring with VPP API integration
* Populate IF-MIB with real interface details (MAC address, speed, status)
* Consolidate VPP-related modules into unified vpp package
* Implement real-time interface state updates via event-driven callbacks
* Retrieve and expose actual interface properties: MTU, admin/oper status
* Add comprehensive interface details caching and management
-- Pim van Pelt <pim@ipng.ch> Sun, 23 Jun 2025 00:00:00 +0000
govpp-snmp-agentx (1.0.3-1) bookworm; urgency=medium
* Remove unnecessary toolchain configuration
* Simplify and shorten Makefile
* Refactor documentation structure with detailed DETAILS.md
* Update and improve README documentation
-- Pim van Pelt <pim@ipng.ch> Thu, 19 Jun 2025 00:00:00 +0000
govpp-snmp-agentx (1.0.2-1) bookworm; urgency=medium govpp-snmp-agentx (1.0.2-1) bookworm; urgency=medium
* Reorganize source code into src/ subdirectory for cleaner project structure * Reorganize source code into src/ subdirectory for cleaner project structure

4
debian/control vendored

@ -2,7 +2,7 @@ Source: govpp-snmp-agentx
Section: net Section: net
Priority: optional Priority: optional
Maintainer: Pim van Pelt <pim@ipng.ch> Maintainer: Pim van Pelt <pim@ipng.ch>
Build-Depends: debhelper-compat (= 13), golang-go (>= 1.21) Build-Depends: debhelper-compat (= 13), golang-go (>= 1.23.8)
Standards-Version: 4.6.2 Standards-Version: 4.6.2
Homepage: https://git.ipng.ch/ipng/govpp-agentx-snmp Homepage: https://git.ipng.ch/ipng/govpp-agentx-snmp
Vcs-Git: https://git.ipng.ch/ipng/govpp-agentx-snmp Vcs-Git: https://git.ipng.ch/ipng/govpp-agentx-snmp
@ -10,7 +10,7 @@ Vcs-Browser: https://git.ipng.ch/ipng/govpp-agentx-snmp
Package: govpp-snmp-agentx Package: govpp-snmp-agentx
Architecture: any Architecture: any
Depends: ${misc:Depends}, ${shlibs:Depends}, snmp, snmpd, adduser Depends: ${misc:Depends}, ${shlibs:Depends}, snmpd
Description: GoVPP SNMP AgentX Daemon Description: GoVPP SNMP AgentX Daemon
A SNMP AgentX daemon that provides SNMP access to VPP (Vector Packet Processing) A SNMP AgentX daemon that provides SNMP access to VPP (Vector Packet Processing)
statistics and interface information. This daemon acts as a subagent that statistics and interface information. This daemon acts as a subagent that

@ -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)
@ -244,6 +270,62 @@ snmpwalk -v2c -c public localhost 1.3.6.1.2.1.1
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1 snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1
``` ```
## Building and Releasing
### Build Targets
The project uses a Makefile with the following targets:
```bash
# Build the binary
make build
# Run tests
make test
# Clean build artifacts
make clean
# Sync version from debian/changelog to main.go
make sync-version
# Build Debian package (includes automatic version sync)
make pkg-deb
```
### Release Process
To cut a new release, follow these steps in order:
1. **Update debian/changelog** with the new version and changelog entries:
```bash
# Edit debian/changelog manually
vim debian/changelog
```
2. **Sync version to main.go**:
```bash
make sync-version
```
3. **Build the package**:
```bash
make pkg-deb
```
4. **Commit all changes together**:
```bash
git add debian/changelog src/main.go
git commit -m "Cut release X.Y.Z-A"
git tag vX.Y.Z-A
```
### Version Synchronization
The build system automatically ensures that the version in `debian/changelog` matches the version constant in `src/main.go`. The `make pkg-deb` target automatically calls `make sync-version` before building to maintain consistency.
**Important**: Always update `debian/changelog` first, as this is the authoritative source for the version number. The `make sync-version` target extracts the version from the first line of the changelog and updates the `Version` constant in `src/main.go`.
## License ## License
This project uses the LGPL 3.0 licensed go-agentx library. It has been modified due to a bug, This project uses the LGPL 3.0 licensed go-agentx library. It has been modified due to a bug,
@ -258,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

@ -2,8 +2,6 @@ module govpp-snmp-agentx
go 1.23.8 go 1.23.8
toolchain go1.23.10
require ( require (
github.com/posteo/go-agentx v0.2.1 github.com/posteo/go-agentx v0.2.1
go.fd.io/govpp v0.12.0 go.fd.io/govpp v0.12.0

@ -15,7 +15,7 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"govpp-snmp-agentx/logger" "govpp-snmp-agentx/logger"
"govpp-snmp-agentx/vppstats" "govpp-snmp-agentx/vpp"
) )
// IF-MIB OID bases: // IF-MIB OID bases:
@ -60,6 +60,7 @@ import (
// ifHCOutUcastPkts .11 - Counter64 // ifHCOutUcastPkts .11 - Counter64
// ifHCOutMulticastPkts .12 - Counter64 // ifHCOutMulticastPkts .12 - Counter64
// ifHCOutBroadcastPkts .13 - Counter64 // ifHCOutBroadcastPkts .13 - Counter64
// ifHighSpeed .15 - Gauge32 (interface speed in Mbps)
// ifAlias .18 - DisplayString // ifAlias .18 - DisplayString
const ifEntryOID = "1.3.6.1.2.1.2.2.1" const ifEntryOID = "1.3.6.1.2.1.2.2.1"
@ -83,6 +84,7 @@ type InterfaceMIB struct {
ifXTableSession *agentx.Session ifXTableSession *agentx.Session
stats map[uint32]*api.InterfaceCounters // indexed by interface index stats map[uint32]*api.InterfaceCounters // indexed by interface index
descriptions map[string]string // interface name -> description mapping descriptions map[string]string // interface name -> description mapping
interfaceDetails map[uint32]*vpp.InterfaceDetails // indexed by interface index
} }
func NewInterfaceMIB() *InterfaceMIB { func NewInterfaceMIB() *InterfaceMIB {
@ -90,6 +92,7 @@ func NewInterfaceMIB() *InterfaceMIB {
handler: &agentx.ListHandler{}, handler: &agentx.ListHandler{},
stats: make(map[uint32]*api.InterfaceCounters), stats: make(map[uint32]*api.InterfaceCounters),
descriptions: make(map[string]string), descriptions: make(map[string]string),
interfaceDetails: make(map[uint32]*vpp.InterfaceDetails),
} }
} }
@ -142,6 +145,22 @@ func (m *InterfaceMIB) LoadVPPConfig(configPath string) error {
return nil return nil
} }
func (m *InterfaceMIB) UpdateInterfaceDetails(details []vpp.InterfaceDetails) {
m.mutex.Lock()
defer m.mutex.Unlock()
logger.Debugf("Updating interface details for %d interfaces", len(details))
// Update interface details map
for _, detail := range details {
m.interfaceDetails[uint32(detail.SwIfIndex)] = &detail
logger.Debugf("Updated details for interface %d (%s): MAC=%x, Speed=%d",
detail.SwIfIndex, detail.InterfaceName, detail.MacAddress, detail.Speed)
}
logger.Debugf("Interface details updated for %d interfaces", len(details))
}
func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) { func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() defer m.mutex.Unlock()
@ -165,14 +184,14 @@ 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))
} }
func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) { func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) {
idx := int(iface.InterfaceIndex) + *vppstats.IfIndexOffset idx := int(iface.InterfaceIndex) + *vpp.IfIndexOffset
// Add ifEntry (classic interface table) entries // Add ifEntry (classic interface table) entries
m.addIfEntry(iface, idx) m.addIfEntry(iface, idx)
@ -186,6 +205,9 @@ func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) {
func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) { func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
var item *agentx.ListItem var item *agentx.ListItem
// Get interface details if available
details := m.interfaceDetails[iface.InterfaceIndex]
// ifIndex (.1) // ifIndex (.1)
item = m.handler.Add(fmt.Sprintf("%s.1.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.1.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeInteger item.Type = pdu.VariableTypeInteger
@ -201,30 +223,63 @@ func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
item.Type = pdu.VariableTypeInteger item.Type = pdu.VariableTypeInteger
item.Value = int32(6) item.Value = int32(6)
// ifMtu (.4) - Default MTU 1500 // ifMtu (.4) - Use real MTU if available, otherwise default to 1500
mtu := int32(1500)
if details != nil {
mtu = int32(details.MTU)
}
item = m.handler.Add(fmt.Sprintf("%s.4.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.4.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeInteger item.Type = pdu.VariableTypeInteger
item.Value = int32(1500) item.Value = mtu
// ifSpeed (.5) - Default to 1Gbps (1000000000 bits/sec) // ifSpeed (.5) - Only populate for speeds <= 2.5Gbps (legacy field limitation)
if details != nil && details.Speed > 0 && details.Speed <= 2500000000 {
// Use real speed for interfaces <= 2.5Gbps
item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeGauge32
item.Value = uint32(details.Speed)
} else if details == nil || details.Speed == 0 {
// Default to 1Gbps when speed is unknown
item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeGauge32 item.Type = pdu.VariableTypeGauge32
item.Value = uint32(1000000000) item.Value = uint32(1000000000)
}
// For speeds > 2.5Gbps, don't populate ifSpeed field at all
// ifPhysAddress (.6) - Empty for now // ifPhysAddress (.6) - Use real MAC address if available
macAddr := ""
if details != nil && len(details.MacAddress) > 0 {
macAddr = string(details.MacAddress)
}
item = m.handler.Add(fmt.Sprintf("%s.6.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.6.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeOctetString item.Type = pdu.VariableTypeOctetString
item.Value = "" item.Value = macAddr
// ifAdminStatus (.7) - up(1) // ifAdminStatus (.7) - Use real admin status if available
adminStatus := int32(1) // default up
if details != nil {
if details.AdminStatus {
adminStatus = 1 // up
} else {
adminStatus = 2 // down
}
}
item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeInteger item.Type = pdu.VariableTypeInteger
item.Value = int32(1) item.Value = adminStatus
// ifOperStatus (.8) - up(1) // ifOperStatus (.8) - Use real operational status if available
operStatus := int32(1) // default up
if details != nil {
if details.OperStatus {
operStatus = 1 // up
} else {
operStatus = 2 // down
}
}
item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeInteger item.Type = pdu.VariableTypeInteger
item.Value = int32(1) item.Value = operStatus
// ifLastChange (.9) - 0 (unknown) // ifLastChange (.9) - 0 (unknown)
item = m.handler.Add(fmt.Sprintf("%s.9.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.9.%d", ifEntryOID, idx))
@ -239,7 +294,12 @@ func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
// ifInUcastPkts (.11) // ifInUcastPkts (.11)
item = m.handler.Add(fmt.Sprintf("%s.11.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.11.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeCounter32 item.Type = pdu.VariableTypeCounter32
// 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) item.Value = uint32(iface.RxUnicast.Packets)
}
// ifInNUcastPkts (.12) - multicast + broadcast // ifInNUcastPkts (.12) - multicast + broadcast
item = m.handler.Add(fmt.Sprintf("%s.12.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.12.%d", ifEntryOID, idx))
@ -269,7 +329,12 @@ func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
// ifOutUcastPkts (.17) // ifOutUcastPkts (.17)
item = m.handler.Add(fmt.Sprintf("%s.17.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.17.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeCounter32 item.Type = pdu.VariableTypeCounter32
// 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) item.Value = uint32(iface.TxUnicast.Packets)
}
// ifOutNUcastPkts (.18) - multicast + broadcast // ifOutNUcastPkts (.18) - multicast + broadcast
item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifEntryOID, idx))
@ -330,7 +395,11 @@ func (m *InterfaceMIB) addIfXTable(iface *api.InterfaceCounters, idx int) {
// ifHCInUcastPkts (.7) // ifHCInUcastPkts (.7)
item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifXTableOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifXTableOID, idx))
item.Type = pdu.VariableTypeCounter64 item.Type = pdu.VariableTypeCounter64
if iface.RxUnicast.Packets == 0 {
item.Value = iface.Rx.Packets
} else {
item.Value = iface.RxUnicast.Packets item.Value = iface.RxUnicast.Packets
}
// ifHCInMulticastPkts (.8) // ifHCInMulticastPkts (.8)
item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifXTableOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifXTableOID, idx))
@ -350,7 +419,11 @@ func (m *InterfaceMIB) addIfXTable(iface *api.InterfaceCounters, idx int) {
// ifHCOutUcastPkts (.11) // ifHCOutUcastPkts (.11)
item = m.handler.Add(fmt.Sprintf("%s.11.%d", ifXTableOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.11.%d", ifXTableOID, idx))
item.Type = pdu.VariableTypeCounter64 item.Type = pdu.VariableTypeCounter64
if iface.TxUnicast.Packets == 0 {
item.Value = iface.Tx.Packets
} else {
item.Value = iface.TxUnicast.Packets item.Value = iface.TxUnicast.Packets
}
// ifHCOutMulticastPkts (.12) // ifHCOutMulticastPkts (.12)
item = m.handler.Add(fmt.Sprintf("%s.12.%d", ifXTableOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.12.%d", ifXTableOID, idx))
@ -362,6 +435,16 @@ func (m *InterfaceMIB) addIfXTable(iface *api.InterfaceCounters, idx int) {
item.Type = pdu.VariableTypeCounter64 item.Type = pdu.VariableTypeCounter64
item.Value = iface.TxBroadcast.Packets item.Value = iface.TxBroadcast.Packets
// ifHighSpeed (.15) - Interface speed in Megabits per second
details := m.interfaceDetails[iface.InterfaceIndex]
speedMbps := uint32(1000) // default 1 Gbps = 1000 Mbps
if details != nil && details.Speed > 0 {
speedMbps = uint32(details.Speed / 1000000) // Convert bps to Mbps
}
item = m.handler.Add(fmt.Sprintf("%s.15.%d", ifXTableOID, idx))
item.Type = pdu.VariableTypeGauge32
item.Value = speedMbps
// ifAlias (.18) - Interface description/alias // ifAlias (.18) - Interface description/alias
item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifXTableOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifXTableOID, idx))
item.Type = pdu.VariableTypeOctetString item.Type = pdu.VariableTypeOctetString

@ -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 {

@ -13,9 +13,11 @@ import (
"govpp-snmp-agentx/config" "govpp-snmp-agentx/config"
"govpp-snmp-agentx/ifmib" "govpp-snmp-agentx/ifmib"
"govpp-snmp-agentx/logger" "govpp-snmp-agentx/logger"
"govpp-snmp-agentx/vppstats" "govpp-snmp-agentx/vpp"
) )
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")
vppcfg := flag.String("vppcfg", "", "VPP configuration YAML file to read interface descriptions from") vppcfg := flag.String("vppcfg", "", "VPP configuration YAML file to read interface descriptions from")
@ -24,6 +26,9 @@ func main() {
// Set global debug flag // Set global debug flag
config.Debug = *debug config.Debug = *debug
// Log startup message with version
logger.Printf("Starting govpp-snmp-agentx version %s", Version)
// Create the interface MIB // Create the interface MIB
interfaceMIB := ifmib.NewInterfaceMIB() interfaceMIB := ifmib.NewInterfaceMIB()
@ -40,8 +45,22 @@ func main() {
log.Fatalf("Failed to start AgentX: %v", err) log.Fatalf("Failed to start AgentX: %v", err)
} }
// Start VPP stats routine with callback to update MIB // Create VPP client and managers
vppstats.StartStatsRoutine(interfaceMIB.UpdateStats) vppClient := vpp.NewVPPClient()
interfaceManager := vpp.NewInterfaceManager(vppClient)
statsManager := vpp.NewStatsManager(vppClient)
// 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 // Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
@ -51,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

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

279
src/vpp/vpp_iface.go Normal file

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

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

159
src/vpp/vpp_stats.go Normal file

@ -0,0 +1,159 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package vpp
import (
"time"
"go.fd.io/govpp/api"
"govpp-snmp-agentx/logger"
)
// StatsCallback is called when interface stats are retrieved
type StatsCallback func(*api.InterfaceStats)
// StatsManager handles VPP statistics operations
type StatsManager struct {
client *VPPClient
statsCallback StatsCallback
period time.Duration
running bool
}
// NewStatsManager creates a new stats manager
func NewStatsManager(client *VPPClient) *StatsManager {
return &StatsManager{
client: client,
period: time.Duration(*Period) * time.Second,
}
}
// SetStatsCallback sets the callback for stats updates
func (sm *StatsManager) SetStatsCallback(callback StatsCallback) {
sm.statsCallback = callback
}
// SetPeriod sets the polling period for stats
func (sm *StatsManager) SetPeriod(period time.Duration) {
sm.period = 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()
var wasConnected = false
for {
if !sm.running {
logger.Debugf("Stats routine stopping")
break
}
// Check if we need to connect/reconnect
if !sm.client.IsConnected() {
if wasConnected {
logger.Printf("VPP connection lost, attempting reconnect...")
wasConnected = false
} else {
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 sm.client.IsConnected() {
if !sm.queryAndReportStats() {
logger.Printf("Stats query failed, marking connection as lost")
sm.client.Disconnect()
continue
}
}
// Wait for next tick
<-ticker.C
}
logger.Debugf("Stats routine ended")
}
// 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
}
// Get interface stats
stats, err := sm.GetInterfaceStats()
if err != nil {
logger.Printf("Failed to get interface stats: %v", err)
return false
}
// Debug log basic info
logger.Debugf("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 sm.statsCallback != nil {
sm.statsCallback(stats)
}
return true
}

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

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

@ -1,234 +0,0 @@
// 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
}

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