Compare commits
19 Commits
5533ab00de
...
main
Author | SHA1 | Date | |
---|---|---|---|
ead795674c | |||
dce4750b0f | |||
d65e055710 | |||
8ed14834f5 | |||
3401c96112 | |||
1889934a9c | |||
e93156324d | |||
bdaa2e366b | |||
96b9dd501d | |||
70cb134dcf | |||
15216782d1 | |||
067e324cca | |||
0d19d50d62 | |||
686bbe46b0 | |||
ccc2b5ad4d | |||
4f368e625d | |||
35165b0464 | |||
42dbbded3d | |||
f16a2b41ea |
11
Makefile
11
Makefile
@ -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
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
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 {
|
||||||
|
30
src/main.go
30
src/main.go
@ -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
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
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
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
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
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()")
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user