Compare commits
26 Commits
4fdd0769a5
...
v1.1.4-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1889934a9c | ||
|
|
e93156324d | ||
|
|
bdaa2e366b | ||
|
|
96b9dd501d | ||
|
|
70cb134dcf | ||
|
|
15216782d1 | ||
|
|
067e324cca | ||
|
|
0d19d50d62 | ||
|
|
686bbe46b0 | ||
|
|
ccc2b5ad4d | ||
|
|
4f368e625d | ||
|
|
35165b0464 | ||
|
|
42dbbded3d | ||
|
|
f16a2b41ea | ||
|
|
5533ab00de | ||
|
|
1cbca296c4 | ||
|
|
a73c7cbf91 | ||
|
|
1ddc77ec73 | ||
|
|
6063db7311 | ||
|
|
7f81b51c1f | ||
|
|
c0bcdd5449 | ||
|
|
fa437ddaf1 | ||
|
|
0b4ff36130 | ||
|
|
82db92f344 | ||
|
|
adf033318a | ||
|
|
6969e609c0 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,2 +1,12 @@
|
||||
govpp-snmp-agentx
|
||||
vppcfg.yaml
|
||||
|
||||
# Debian packaging artifacts
|
||||
debian/.debhelper/
|
||||
debian/.gocache/
|
||||
debian/go/
|
||||
debian/govpp-snmp-agentx/
|
||||
debian/files
|
||||
debian/*.substvars
|
||||
debian/debhelper-build-stamp
|
||||
debian/*.debhelper
|
||||
|
||||
29
Makefile
Normal file
29
Makefile
Normal file
@@ -0,0 +1,29 @@
|
||||
PROG = govpp-snmp-agentx
|
||||
|
||||
.PHONY: build test clean pkg-deb sync-version
|
||||
|
||||
# Build the binary
|
||||
build:
|
||||
cd src && go build -o ../$(PROG) .
|
||||
|
||||
# Run all tests
|
||||
test:
|
||||
cd src && go test ./...
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -f $(PROG)
|
||||
[ -d debian/go ] && chmod -R +w debian/go || true
|
||||
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
|
||||
|
||||
# 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
|
||||
pkg-deb: sync-version
|
||||
fakeroot dpkg-buildpackage -us -uc -b
|
||||
321
README.md
321
README.md
@@ -1,315 +1,46 @@
|
||||
# VPP SNMP AgentX Bridge
|
||||
|
||||
A Go application that bridges VPP (Vector Packet Processing) interface statistics to SNMP using the
|
||||
AgentX protocol. It queries VPP interface counters and exposes them via the standard IF-MIB
|
||||
for SNMP monitoring.
|
||||
SNMP AgentX daemon that exposes VPP interface statistics via standard IF-MIB.
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time VPP interface statistics** via SNMP
|
||||
- **Standard IF-MIB compliance** (ifXTable)
|
||||
- **AgentX protocol support** (TCP and Unix sockets)
|
||||
- **Configurable interface index offset** to avoid conflicts
|
||||
- **Configurable polling intervals**
|
||||
- **Thread-safe operation** with proper synchronization
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
VPP Stats Socket → VPP Stats Client → Interface MIB → AgentX → SNMPd
|
||||
```
|
||||
|
||||
The application consists of four main components:
|
||||
|
||||
1. **VPP Stats Client** (`vppstats/`): Connects to VPP stats socket and retrieves interface counters
|
||||
2. **Interface MIB** (`ifmib/`): Maps VPP statistics to SNMP IF-MIB structure
|
||||
3. **AgentX Client** (`agentx/`): Handles AgentX protocol connection and MIB registration
|
||||
4. **Main Application**: Orchestrates the components and handles configuration
|
||||
|
||||
## Build Instructions
|
||||
|
||||
### Development Build
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
go build -o govpp-snmp-agentx .
|
||||
# Build
|
||||
make build
|
||||
|
||||
# Test
|
||||
make test
|
||||
|
||||
# Create Debian package
|
||||
make pkg-deb
|
||||
|
||||
# Install package
|
||||
sudo dpkg -i ../govpp-snmp-agentx_*.deb
|
||||
```
|
||||
|
||||
### Static Binary Build
|
||||
## Configuration
|
||||
|
||||
For deployment without Go runtime dependencies:
|
||||
The Debian package installs a systemd service that reads configuration from `/etc/default/govpp-snmp-agentx`:
|
||||
|
||||
```bash
|
||||
# Linux static binary
|
||||
CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o govpp-snmp-agentx .
|
||||
# Edit service configuration
|
||||
sudo nano /etc/default/govpp-snmp-agentx
|
||||
|
||||
# Cross-compile for different architectures
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags '-extldflags "-static"' -o govpp-snmp-agentx-linux-amd64 .
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -ldflags '-extldflags "-static"' -o govpp-snmp-agentx-linux-arm64 .
|
||||
# Start service
|
||||
sudo systemctl start govpp-snmp-agentx
|
||||
```
|
||||
|
||||
### Release Build with Version Info
|
||||
|
||||
```bash
|
||||
VERSION=$(git describe --tags --always --dirty)
|
||||
BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
|
||||
CGO_ENABLED=0 go build -ldflags "-X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" -o govpp-snmp-agentx .
|
||||
Default configuration:
|
||||
```
|
||||
GOVPP_SNMP_AGENTX_FLAGS="-agentx.addr /var/agentx/master -vppcfg /etc/vpp/vppcfg.yaml -vppstats.period 10"
|
||||
```
|
||||
|
||||
## Usage
|
||||
## Documentation
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# Run with default settings
|
||||
./govpp-snmp-agentx
|
||||
|
||||
# Run with custom AgentX address
|
||||
./govpp-snmp-agentx -agentx.addr 127.0.0.1:705
|
||||
|
||||
# Run with Unix socket AgentX connection
|
||||
./govpp-snmp-agentx -agentx.addr /var/agentx/master
|
||||
```
|
||||
|
||||
### Command Line Flags
|
||||
|
||||
#### General Application Flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `-debug` | `false` | Enable debug logging |
|
||||
| `-vppcfg` | `""` | VPP configuration YAML file to read interface descriptions from |
|
||||
|
||||
#### AgentX Module Flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `-agentx.addr` | `localhost:705` | AgentX master agent address (hostname:port or Unix socket path) |
|
||||
|
||||
#### VPP Statistics Module Flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `-vppstats.api.addr` | `/var/run/vpp/api.sock` | VPP API socket path |
|
||||
| `-vppstats.stats.addr` | `/var/run/vpp/stats.sock` | VPP statistics socket path |
|
||||
| `-vppstats.period` | `10` | Interval in seconds for querying VPP interface stats |
|
||||
| `-vppstats.ifindex-offset` | `1000` | Offset to add to VPP interface indices for SNMP |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Enable debug logging
|
||||
./govpp-snmp-agentx -debug
|
||||
|
||||
# Custom polling interval (5 seconds)
|
||||
./govpp-snmp-agentx -vppstats.period 5
|
||||
|
||||
# Custom VPP stats socket
|
||||
./govpp-snmp-agentx -vppstats.stats.addr /custom/path/stats.sock
|
||||
|
||||
# Custom VPP API socket
|
||||
./govpp-snmp-agentx -vppstats.api.addr /custom/path/api.sock
|
||||
|
||||
# Custom interface index offset (start at 2000)
|
||||
./govpp-snmp-agentx -vppstats.ifindex-offset 2000
|
||||
|
||||
# With VPP configuration file for interface descriptions
|
||||
./govpp-snmp-agentx -vppcfg /etc/vpp/vppcfg.yaml
|
||||
|
||||
# Full configuration
|
||||
./govpp-snmp-agentx \
|
||||
-agentx.addr /var/agentx/master \
|
||||
-debug \
|
||||
-vppcfg /etc/vpp/vppcfg.yaml \
|
||||
-vppstats.api.addr /var/run/vpp/api.sock \
|
||||
-vppstats.stats.addr /var/run/vpp/stats.sock \
|
||||
-vppstats.period 5 \
|
||||
-vppstats.ifindex-offset 1000
|
||||
```
|
||||
|
||||
## VPP Configuration File
|
||||
|
||||
The `-vppcfg` flag accepts a YAML configuration file that describes VPP interfaces and their descriptions. This file is used to populate the `ifAlias` (.18) field in the ifXTable with meaningful interface descriptions.
|
||||
|
||||
### YAML Format Example
|
||||
|
||||
```yaml
|
||||
interfaces:
|
||||
GigabitEthernet82/0/0:
|
||||
description: 'Infra: Management interface'
|
||||
TenGigabitEthernet1/0/2:
|
||||
description: 'Infra: Core uplink'
|
||||
sub-interfaces:
|
||||
100:
|
||||
description: 'Cust: Customer VLAN 100'
|
||||
200:
|
||||
description: 'Transit: Provider VLAN 200'
|
||||
loopbacks:
|
||||
loop0:
|
||||
description: 'Core: Router loopback'
|
||||
```
|
||||
|
||||
### Description Mapping
|
||||
|
||||
- **Main interfaces**: Use the `description` field directly
|
||||
- **Sub-interfaces**: Use the `description` field from the `sub-interfaces` section
|
||||
- **Loopbacks**: Use the `description` field from the `loopbacks` section
|
||||
- **Fallback**: If no description is found, the interface name is used as the alias
|
||||
|
||||
## SNMP Interface Mapping
|
||||
|
||||
VPP interfaces are mapped to SNMP indices with a configurable offset (default 1000):
|
||||
|
||||
- **VPP Interface 0** → **SNMP Index 1000**
|
||||
- **VPP Interface 1** → **SNMP Index 1001**
|
||||
- **VPP Interface N** → **SNMP Index (N + offset)**
|
||||
|
||||
## Supported MIB Objects
|
||||
|
||||
The application implements the ifXTable (1.3.6.1.2.1.31.1.1.1) with the following objects:
|
||||
|
||||
| OID | Object | Type | Description |
|
||||
|-----|--------|------|-------------|
|
||||
| `.1.{index}` | ifName | DisplayString | Interface name |
|
||||
| `.2.{index}` | ifInMulticastPkts | Counter32 | RX multicast packets |
|
||||
| `.3.{index}` | ifInBroadcastPkts | Counter32 | RX broadcast packets |
|
||||
| `.4.{index}` | ifOutMulticastPkts | Counter32 | TX multicast packets |
|
||||
| `.5.{index}` | ifOutBroadcastPkts | Counter32 | TX broadcast packets |
|
||||
| `.6.{index}` | ifHCInOctets | Counter64 | RX bytes (high capacity) |
|
||||
| `.7.{index}` | ifHCInUcastPkts | Counter64 | RX unicast packets (high capacity) |
|
||||
| `.8.{index}` | ifHCInMulticastPkts | Counter64 | RX multicast packets (high capacity) |
|
||||
| `.9.{index}` | ifHCInBroadcastPkts | Counter64 | RX broadcast packets (high capacity) |
|
||||
| `.10.{index}` | ifHCOutOctets | Counter64 | TX bytes (high capacity) |
|
||||
| `.11.{index}` | ifHCOutUcastPkts | Counter64 | TX unicast packets (high capacity) |
|
||||
| `.12.{index}` | ifHCOutMulticastPkts | Counter64 | TX multicast packets (high capacity) |
|
||||
| `.13.{index}` | ifHCOutBroadcastPkts | Counter64 | TX broadcast packets (high capacity) |
|
||||
| `.18.{index}` | ifAlias | DisplayString | Interface description/alias (from VPP config or interface name) |
|
||||
|
||||
## SNMP Query Examples
|
||||
|
||||
### Query Interface Names
|
||||
|
||||
```bash
|
||||
# Get all interface names
|
||||
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.1
|
||||
|
||||
# Get specific interface name (interface 0 with default offset)
|
||||
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.1.1000
|
||||
```
|
||||
|
||||
### Query Interface Descriptions
|
||||
|
||||
```bash
|
||||
# Get all interface descriptions/aliases
|
||||
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.18
|
||||
|
||||
# Get specific interface description (interface 0 with default offset)
|
||||
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.18.1000
|
||||
```
|
||||
|
||||
### Query Interface Counters
|
||||
|
||||
```bash
|
||||
# Get RX bytes for interface 0
|
||||
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.6.1000
|
||||
|
||||
# Get TX bytes for interface 1
|
||||
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.10.1001
|
||||
|
||||
# Walk all interface counters
|
||||
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1
|
||||
```
|
||||
|
||||
### Query with Custom Offset
|
||||
|
||||
If running with `-vppstats.ifindex-offset 2000`:
|
||||
|
||||
```bash
|
||||
# Interface 0 counters at index 2000
|
||||
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.6.2000
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### VPP Requirements
|
||||
|
||||
- VPP must be running with stats socket enabled
|
||||
- Stats socket accessible at `/var/run/vpp/stats.sock` (or custom path)
|
||||
- Application must have read permissions on the stats socket
|
||||
|
||||
### SNMP Requirements
|
||||
|
||||
- SNMP master agent running (net-snmp's snmpd)
|
||||
- AgentX protocol enabled in snmpd configuration
|
||||
- AgentX socket accessible (TCP port 705 or Unix socket)
|
||||
|
||||
### SNMP Agent Configuration
|
||||
|
||||
Add to `/etc/snmp/snmpd.conf`:
|
||||
|
||||
```
|
||||
# Enable AgentX
|
||||
master agentx
|
||||
agentXSocket tcp:localhost:705
|
||||
# or for Unix socket:
|
||||
# agentXSocket /var/agentx/master
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"Failed to connect to VPP stats"**
|
||||
- Check VPP is running: `vppctl show version`
|
||||
- Verify stats socket exists: `ls -la /var/run/vpp/stats.sock`
|
||||
- Check permissions on stats socket
|
||||
|
||||
2. **"Failed to dial AgentX"**
|
||||
- Verify SNMP agent is running: `systemctl status snmpd`
|
||||
- Check AgentX is enabled in snmpd.conf
|
||||
- Test AgentX socket: `netstat -ln | grep 705`
|
||||
|
||||
3. **"Failed to register IF-MIB: ErrorDuplicateRegistration"**
|
||||
- Another agent is already serving the IF-MIB
|
||||
- Check with: `snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1`
|
||||
|
||||
4. **"Retrieved stats for 0 interfaces"**
|
||||
- VPP may have no interfaces configured
|
||||
- Check VPP interfaces: `vppctl show interface`
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check VPP interfaces
|
||||
vppctl show interface
|
||||
|
||||
# Check VPP stats
|
||||
vppctl show stats
|
||||
|
||||
# Test SNMP master agent
|
||||
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.1
|
||||
|
||||
# Check AgentX registration
|
||||
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1
|
||||
```
|
||||
- **Manual page**: `man govpp-snmp-agentx` (after package installation)
|
||||
- **Detailed documentation**: [docs/DETAILS.md](docs/DETAILS.md)
|
||||
|
||||
## License
|
||||
|
||||
This project uses the LGPL 3.0 licensed go-agentx library. It has been modified due to a bug,
|
||||
see details in [[GitHub PR#7](https://github.com/posteo/go-agentx/pull/11)], and as such is
|
||||
licensed also LGPL 3.0. The go-agentx source code in this project will be removed once the
|
||||
upstream PR is merged.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests if applicable
|
||||
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
|
||||
LGPL 3.0 (due to modified go-agentx dependency)
|
||||
|
||||
|
||||
84
debian/changelog
vendored
Normal file
84
debian/changelog
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
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
|
||||
|
||||
* Reorganize source code into src/ subdirectory for cleaner project structure
|
||||
* Add environment file support (/etc/default/govpp-snmp-agentx)
|
||||
* Move service configuration to environment variables
|
||||
* Improve systemd service configurability
|
||||
|
||||
-- Pim van Pelt <pim@ipng.ch> Tue, 17 Jun 2025 00:55:00 +0000
|
||||
|
||||
govpp-snmp-agentx (1.0.1-1) bookworm; urgency=medium
|
||||
|
||||
* Add manual page for govpp-snmp-agentx(1)
|
||||
* Make VPP config file optional - log warning and continue if missing
|
||||
* Fix Debian package build reproducibility issues
|
||||
* Improve build system with proper cleanup targets
|
||||
|
||||
-- Pim van Pelt <pim@ipng.ch> Tue, 17 Jun 2025 00:35:00 +0000
|
||||
|
||||
govpp-snmp-agentx (1.0.0-1) bookworm; urgency=medium
|
||||
|
||||
* Initial release
|
||||
* SNMP AgentX daemon for VPP statistics
|
||||
* Interface MIB support
|
||||
* Systemd service integration
|
||||
|
||||
-- Pim van Pelt <pim@ipng.ch> Mon, 16 Jun 2025 00:00:00 +0000
|
||||
23
debian/control
vendored
Normal file
23
debian/control
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
Source: govpp-snmp-agentx
|
||||
Section: net
|
||||
Priority: optional
|
||||
Maintainer: Pim van Pelt <pim@ipng.ch>
|
||||
Build-Depends: debhelper-compat (= 13), golang-go (>= 1.23.8)
|
||||
Standards-Version: 4.6.2
|
||||
Homepage: https://git.ipng.ch/ipng/govpp-agentx-snmp
|
||||
Vcs-Git: https://git.ipng.ch/ipng/govpp-agentx-snmp
|
||||
Vcs-Browser: https://git.ipng.ch/ipng/govpp-agentx-snmp
|
||||
|
||||
Package: govpp-snmp-agentx
|
||||
Architecture: any
|
||||
Depends: ${misc:Depends}, ${shlibs:Depends}, snmpd
|
||||
Description: GoVPP SNMP AgentX Daemon
|
||||
A SNMP AgentX daemon that provides SNMP access to VPP (Vector Packet Processing)
|
||||
statistics and interface information. This daemon acts as a subagent that
|
||||
connects to the main SNMP daemon via the AgentX protocol.
|
||||
.
|
||||
Features:
|
||||
- Interface MIB support
|
||||
- VPP statistics exposure
|
||||
- AgentX protocol implementation
|
||||
- Systemd integration
|
||||
12
debian/govpp-snmp-agentx.postrm.debhelper
vendored
Normal file
12
debian/govpp-snmp-agentx.postrm.debhelper
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Automatically added by dh_installsystemd/13.11.4
|
||||
if [ "$1" = remove ] && [ -d /run/systemd/system ] ; then
|
||||
systemctl --system daemon-reload >/dev/null || true
|
||||
fi
|
||||
# End automatically added section
|
||||
# Automatically added by dh_installsystemd/13.11.4
|
||||
if [ "$1" = "purge" ]; then
|
||||
if [ -x "/usr/bin/deb-systemd-helper" ]; then
|
||||
deb-systemd-helper purge 'govpp-snmp-agentx.service' >/dev/null || true
|
||||
fi
|
||||
fi
|
||||
# End automatically added section
|
||||
23
debian/postinst
vendored
Executable file
23
debian/postinst
vendored
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
# Enable and start the service
|
||||
systemctl daemon-reload
|
||||
systemctl enable govpp-snmp-agentx.service
|
||||
;;
|
||||
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "postinst called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
22
debian/prerm
vendored
Executable file
22
debian/prerm
vendored
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
case "$1" in
|
||||
remove|upgrade|deconfigure)
|
||||
systemctl stop govpp-snmp-agentx.service || true
|
||||
systemctl disable govpp-snmp-agentx.service || true
|
||||
;;
|
||||
|
||||
failed-upgrade)
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "prerm called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
30
debian/rules
vendored
Executable file
30
debian/rules
vendored
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
export DH_VERBOSE = 1
|
||||
export GO111MODULE = on
|
||||
export GOPROXY = direct
|
||||
export GOCACHE = $(CURDIR)/debian/.gocache
|
||||
export GOPATH = $(CURDIR)/debian/go
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_auto_build:
|
||||
cd src && go build -v -ldflags="-s -w" -o ../govpp-snmp-agentx .
|
||||
|
||||
override_dh_auto_install:
|
||||
install -D -m 0755 govpp-snmp-agentx debian/govpp-snmp-agentx/usr/sbin/govpp-snmp-agentx
|
||||
install -D -m 0644 govpp-snmp-agentx.service debian/govpp-snmp-agentx/lib/systemd/system/govpp-snmp-agentx.service
|
||||
install -D -m 0644 govpp-snmp-agentx.default debian/govpp-snmp-agentx/etc/default/govpp-snmp-agentx
|
||||
install -D -m 0644 docs/govpp-snmp-agentx.1 debian/govpp-snmp-agentx/usr/share/man/man1/govpp-snmp-agentx.1
|
||||
|
||||
override_dh_auto_configure:
|
||||
# Skip auto configure
|
||||
|
||||
override_dh_auto_test:
|
||||
# Skip tests during packaging
|
||||
|
||||
override_dh_auto_clean:
|
||||
rm -f govpp-snmp-agentx
|
||||
[ -d debian/go ] && chmod -R +w debian/go || true
|
||||
rm -rf debian/.gocache debian/go obj-*
|
||||
342
docs/DETAILS.md
Normal file
342
docs/DETAILS.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# VPP SNMP AgentX Bridge - Detailed Documentation
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time VPP interface statistics** via SNMP
|
||||
- **Standard IF-MIB compliance** (ifXTable)
|
||||
- **AgentX protocol support** (TCP and Unix sockets)
|
||||
- **Configurable interface index offset** to avoid conflicts
|
||||
- **Configurable polling intervals**
|
||||
- **Thread-safe operation** with proper synchronization
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
VPP Stats Socket → VPP Stats Client → Interface MIB → AgentX → SNMPd
|
||||
```
|
||||
|
||||
The application consists of four main components:
|
||||
|
||||
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
|
||||
3. **AgentX Client** (`src/agentx/`): Handles AgentX protocol connection and MIB registration
|
||||
4. **Main Application** (`src/main.go`): Orchestrates the components and handles configuration
|
||||
|
||||
## Command Line Flags
|
||||
|
||||
### General Application Flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `-debug` | `false` | Enable debug logging |
|
||||
| `-vppcfg` | `""` | VPP configuration YAML file to read interface descriptions from |
|
||||
|
||||
### AgentX Module Flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `-agentx.addr` | `localhost:705` | AgentX master agent address (hostname:port or Unix socket path) |
|
||||
|
||||
### VPP Statistics Module Flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `-vppstats.api.addr` | `/var/run/vpp/api.sock` | VPP API socket path |
|
||||
| `-vppstats.stats.addr` | `/var/run/vpp/stats.sock` | VPP statistics socket path |
|
||||
| `-vppstats.period` | `10` | Interval in seconds for querying VPP interface stats |
|
||||
| `-vppstats.ifindex-offset` | `1000` | Offset to add to VPP interface indices for SNMP |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```bash
|
||||
# Enable debug logging
|
||||
./govpp-snmp-agentx -debug
|
||||
|
||||
# Custom polling interval (5 seconds)
|
||||
./govpp-snmp-agentx -vppstats.period 5
|
||||
|
||||
# Custom VPP stats socket
|
||||
./govpp-snmp-agentx -vppstats.stats.addr /custom/path/stats.sock
|
||||
|
||||
# Custom VPP API socket
|
||||
./govpp-snmp-agentx -vppstats.api.addr /custom/path/api.sock
|
||||
|
||||
# Custom interface index offset (start at 2000)
|
||||
./govpp-snmp-agentx -vppstats.ifindex-offset 2000
|
||||
|
||||
# With VPP configuration file for interface descriptions
|
||||
./govpp-snmp-agentx -vppcfg /etc/vpp/vppcfg.yaml
|
||||
|
||||
# Full configuration
|
||||
./govpp-snmp-agentx \
|
||||
-agentx.addr /var/agentx/master \
|
||||
-debug \
|
||||
-vppcfg /etc/vpp/vppcfg.yaml \
|
||||
-vppstats.api.addr /var/run/vpp/api.sock \
|
||||
-vppstats.stats.addr /var/run/vpp/stats.sock \
|
||||
-vppstats.period 5 \
|
||||
-vppstats.ifindex-offset 1000
|
||||
```
|
||||
|
||||
## VPP Configuration File
|
||||
|
||||
The `-vppcfg` flag accepts a YAML configuration file that describes VPP interfaces and their descriptions. This file is used to populate the `ifAlias` (.18) field in the ifXTable with meaningful interface descriptions.
|
||||
|
||||
### YAML Format Example
|
||||
|
||||
```yaml
|
||||
interfaces:
|
||||
GigabitEthernet82/0/0:
|
||||
description: 'Infra: Management interface'
|
||||
TenGigabitEthernet1/0/2:
|
||||
description: 'Infra: Core uplink'
|
||||
sub-interfaces:
|
||||
100:
|
||||
description: 'Cust: Customer VLAN 100'
|
||||
200:
|
||||
description: 'Transit: Provider VLAN 200'
|
||||
loopbacks:
|
||||
loop0:
|
||||
description: 'Core: Router loopback'
|
||||
```
|
||||
|
||||
### Description Mapping
|
||||
|
||||
- **Main interfaces**: Use the `description` field directly
|
||||
- **Sub-interfaces**: Use the `description` field from the `sub-interfaces` section
|
||||
- **Loopbacks**: Use the `description` field from the `loopbacks` section
|
||||
- **Fallback**: If no description is found, the interface name is used as the alias
|
||||
|
||||
## SNMP Interface Mapping
|
||||
|
||||
VPP interfaces are mapped to SNMP indices with a configurable offset (default 1000):
|
||||
|
||||
- **VPP Interface 0** → **SNMP Index 1000**
|
||||
- **VPP Interface 1** → **SNMP Index 1001**
|
||||
- **VPP Interface N** → **SNMP Index (N + offset)**
|
||||
|
||||
## Supported MIB Objects
|
||||
|
||||
The application implements the ifXTable (1.3.6.1.2.1.31.1.1.1) with the following objects:
|
||||
|
||||
| OID | Object | Type | Description |
|
||||
|-----|--------|------|-------------|
|
||||
| `.1.{index}` | ifName | DisplayString | Interface name |
|
||||
| `.2.{index}` | ifInMulticastPkts | Counter32 | RX multicast packets |
|
||||
| `.3.{index}` | ifInBroadcastPkts | Counter32 | RX broadcast packets |
|
||||
| `.4.{index}` | ifOutMulticastPkts | Counter32 | TX multicast packets |
|
||||
| `.5.{index}` | ifOutBroadcastPkts | Counter32 | TX broadcast packets |
|
||||
| `.6.{index}` | ifHCInOctets | Counter64 | RX bytes (high capacity) |
|
||||
| `.7.{index}` | ifHCInUcastPkts | Counter64 | RX unicast packets (high capacity) |
|
||||
| `.8.{index}` | ifHCInMulticastPkts | Counter64 | RX multicast packets (high capacity) |
|
||||
| `.9.{index}` | ifHCInBroadcastPkts | Counter64 | RX broadcast packets (high capacity) |
|
||||
| `.10.{index}` | ifHCOutOctets | Counter64 | TX bytes (high capacity) |
|
||||
| `.11.{index}` | ifHCOutUcastPkts | Counter64 | TX unicast packets (high capacity) |
|
||||
| `.12.{index}` | ifHCOutMulticastPkts | Counter64 | TX multicast packets (high capacity) |
|
||||
| `.13.{index}` | ifHCOutBroadcastPkts | Counter64 | TX broadcast packets (high capacity) |
|
||||
| `.18.{index}` | ifAlias | DisplayString | Interface description/alias (from VPP config or interface name) |
|
||||
|
||||
## SNMP Query Examples
|
||||
|
||||
### Query Interface Names
|
||||
|
||||
```bash
|
||||
# Get all interface names
|
||||
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.1
|
||||
|
||||
# Get specific interface name (interface 0 with default offset)
|
||||
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.1.1000
|
||||
```
|
||||
|
||||
### Query Interface Descriptions
|
||||
|
||||
```bash
|
||||
# Get all interface descriptions/aliases
|
||||
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.18
|
||||
|
||||
# Get specific interface description (interface 0 with default offset)
|
||||
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.18.1000
|
||||
```
|
||||
|
||||
### Query Interface Counters
|
||||
|
||||
```bash
|
||||
# Get RX bytes for interface 0
|
||||
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.6.1000
|
||||
|
||||
# Get TX bytes for interface 1
|
||||
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.10.1001
|
||||
|
||||
# Walk all interface counters
|
||||
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1
|
||||
```
|
||||
|
||||
### Query with Custom Offset
|
||||
|
||||
If running with `-vppstats.ifindex-offset 2000`:
|
||||
|
||||
```bash
|
||||
# Interface 0 counters at index 2000
|
||||
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.6.2000
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### VPP Requirements
|
||||
|
||||
- VPP must be running with stats socket enabled
|
||||
- Stats socket accessible at `/var/run/vpp/stats.sock` (or custom path)
|
||||
- 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 master agent running (net-snmp's snmpd)
|
||||
- AgentX protocol enabled in snmpd configuration
|
||||
- AgentX socket accessible (TCP port 705 or Unix socket)
|
||||
|
||||
### SNMP Agent Configuration
|
||||
|
||||
Add to `/etc/snmp/snmpd.conf`:
|
||||
|
||||
```
|
||||
# Enable AgentX
|
||||
master agentx
|
||||
agentXSocket tcp:localhost:705
|
||||
# or for Unix socket:
|
||||
# agentXSocket /var/agentx/master
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"Failed to connect to VPP stats"**
|
||||
- Check VPP is running: `vppctl show version`
|
||||
- Verify stats socket exists: `ls -la /var/run/vpp/stats.sock`
|
||||
- Check permissions on stats socket
|
||||
|
||||
2. **"Failed to dial AgentX"**
|
||||
- Verify SNMP agent is running: `systemctl status snmpd`
|
||||
- Check AgentX is enabled in snmpd.conf
|
||||
- Test AgentX socket: `netstat -ln | grep 705`
|
||||
|
||||
3. **"Failed to register IF-MIB: ErrorDuplicateRegistration"**
|
||||
- Another agent is already serving the IF-MIB
|
||||
- Check with: `snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1`
|
||||
|
||||
4. **"Retrieved stats for 0 interfaces"**
|
||||
- VPP may have no interfaces configured
|
||||
- Check VPP interfaces: `vppctl show interface`
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check VPP interfaces
|
||||
vppctl show interface
|
||||
|
||||
# Check VPP stats
|
||||
vppctl show stats
|
||||
|
||||
# Test SNMP master agent
|
||||
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.1
|
||||
|
||||
# Check AgentX registration
|
||||
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
|
||||
|
||||
This project uses the LGPL 3.0 licensed go-agentx library. It has been modified due to a bug,
|
||||
see details in [[GitHub PR#7](https://github.com/posteo/go-agentx/pull/11)], and as such is
|
||||
licensed also LGPL 3.0. The go-agentx source code in this project will be removed once the
|
||||
upstream PR is merged.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests if applicable
|
||||
5. Submit a pull request
|
||||
72
docs/govpp-snmp-agentx.1
Normal file
72
docs/govpp-snmp-agentx.1
Normal file
@@ -0,0 +1,72 @@
|
||||
.TH GOVPP-SNMP-AGENTX 1 "June 2025" "version 1.0.0" "User Commands"
|
||||
.SH NAME
|
||||
govpp-snmp-agentx \- VPP SNMP AgentX daemon for interface statistics
|
||||
.SH SYNOPSIS
|
||||
.B govpp-snmp-agentx
|
||||
[\fIOPTION\fR]...
|
||||
.SH DESCRIPTION
|
||||
.B govpp-snmp-agentx
|
||||
is an SNMP AgentX subagent that provides SNMP access to VPP (Vector Packet Processing) interface statistics and information. It connects to a master SNMP daemon via the AgentX protocol and populates standard IF-MIB tables with real-time VPP interface data.
|
||||
.PP
|
||||
The daemon implements two MIB tables:
|
||||
.TP
|
||||
.B ifEntry
|
||||
Classic interface table (1.3.6.1.2.1.2.2.1) with basic interface statistics including counters for packets, bytes, errors, and discards.
|
||||
.TP
|
||||
.B ifXTable
|
||||
Extended interface table (1.3.6.1.2.1.31.1.1.1) with high-capacity 64-bit counters and additional interface information.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.BR \-agentx.addr " " \fIADDRESS\fR
|
||||
AgentX master agent address. Can be a TCP address (hostname:port) or Unix socket path.
|
||||
Default: localhost:705
|
||||
.TP
|
||||
.BR \-debug
|
||||
Enable debug logging to show detailed operational information.
|
||||
.TP
|
||||
.BR \-vppcfg " " \fIFILE\fR
|
||||
VPP configuration YAML file to read interface descriptions from.
|
||||
.TP
|
||||
.BR \-vppstats.api.addr " " \fIPATH\fR
|
||||
VPP API socket path for interface enumeration.
|
||||
Default: /var/run/vpp/api.sock
|
||||
.TP
|
||||
.BR \-vppstats.stats.addr " " \fIPATH\fR
|
||||
VPP statistics socket path for interface counters.
|
||||
Default: /var/run/vpp/stats.sock
|
||||
.TP
|
||||
.BR \-vppstats.ifindex-offset " " \fINUMBER\fR
|
||||
Offset added to VPP interface indices for SNMP interface numbering.
|
||||
Default: 1000
|
||||
.TP
|
||||
.BR \-vppstats.period " " \fISECONDS\fR
|
||||
Interval in seconds for querying VPP interface statistics.
|
||||
Default: 10
|
||||
.SH EXAMPLES
|
||||
.TP
|
||||
Connect to AgentX master via TCP:
|
||||
.B govpp-snmp-agentx -agentx.addr snmp.example.com:705
|
||||
.TP
|
||||
Connect via Unix socket with debug logging:
|
||||
.B govpp-snmp-agentx -agentx.addr /var/agentx/master -debug
|
||||
.TP
|
||||
Use custom VPP sockets and config:
|
||||
.B govpp-snmp-agentx -vppstats.api.addr /opt/vpp/api.sock -vppcfg /etc/vpp/vppcfg.yaml
|
||||
.SH FILES
|
||||
.TP
|
||||
.I /var/run/vpp/api.sock
|
||||
Default VPP API socket
|
||||
.TP
|
||||
.I /var/run/vpp/stats.sock
|
||||
Default VPP statistics socket
|
||||
.TP
|
||||
.I /var/agentx/master
|
||||
Common AgentX Unix socket path
|
||||
.SH SEE ALSO
|
||||
.BR snmpd (8),
|
||||
.BR snmpwalk (1),
|
||||
.BR vpp (8)
|
||||
.SH AUTHOR
|
||||
Pim van Pelt <pim@ipng.ch>
|
||||
.SH COPYRIGHT
|
||||
Copyright 2025, IPng Networks GmbH
|
||||
15
govpp-snmp-agentx.default
Normal file
15
govpp-snmp-agentx.default
Normal file
@@ -0,0 +1,15 @@
|
||||
# Default configuration for govpp-snmp-agentx
|
||||
# This file contains environment variables for the GoVPP SNMP AgentX daemon
|
||||
#
|
||||
# Command line flags for govpp-snmp-agentx
|
||||
GOVPP_SNMP_AGENTX_FLAGS="-agentx.addr /var/agentx/master -vppcfg /etc/vpp/vppcfg.yaml -vppstats.period 10"
|
||||
|
||||
# Additional options that can be added to GOVPP_SNMP_AGENTX_FLAGS:
|
||||
# -debug Enable debug logging
|
||||
# -agentx.addr host:port SNMPd Agentx address (example: localhost:705)
|
||||
# -vppstats.api.addr PATH VPP API socket path (default: /var/run/vpp/api.sock)
|
||||
# -vppstats.stats.addr PATH VPP stats socket path (default: /var/run/vpp/stats.sock)
|
||||
# -vppstats.ifindex-offset NUM Interface index offset (default: 1000)
|
||||
#
|
||||
# Example with debug logging:
|
||||
# GOVPP_SNMP_AGENTX_FLAGS="-agentx.addr localhost:705 -vppcfg /etc/vpp/vppcfg.yaml -vppstats.period 10 -debug"
|
||||
18
govpp-snmp-agentx.service
Normal file
18
govpp-snmp-agentx.service
Normal file
@@ -0,0 +1,18 @@
|
||||
[Unit]
|
||||
Description=GoVPP SNMP AgentX Daemon
|
||||
After=network.target vpp.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=-/etc/default/govpp-snmp-agentx
|
||||
ExecStartPre=-+/usr/bin/chmod 770 /var/agentx /var/agentx/master
|
||||
ExecStartPre=-+/usr/bin/chown Debian-snmp:vpp /var/agentx /var/agentx/master
|
||||
ExecStart=/usr/sbin/govpp-snmp-agentx $GOVPP_SNMP_AGENTX_FLAGS
|
||||
User=Debian-snmp
|
||||
Group=vpp
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -41,6 +41,7 @@ func TestFlagRegistration(t *testing.T) {
|
||||
f := flag.Lookup("agentx.addr")
|
||||
if f == nil {
|
||||
t.Error("Expected agentx.addr flag to be registered")
|
||||
return
|
||||
}
|
||||
|
||||
if f.DefValue != "localhost:705" {
|
||||
@@ -2,8 +2,6 @@ module govpp-snmp-agentx
|
||||
|
||||
go 1.23.8
|
||||
|
||||
toolchain go1.23.10
|
||||
|
||||
require (
|
||||
github.com/posteo/go-agentx v0.2.1
|
||||
go.fd.io/govpp v0.12.0
|
||||
@@ -4,7 +4,7 @@ package ifmib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"govpp-snmp-agentx/logger"
|
||||
"govpp-snmp-agentx/vppstats"
|
||||
"govpp-snmp-agentx/vpp"
|
||||
)
|
||||
|
||||
// IF-MIB OID bases:
|
||||
@@ -60,6 +60,7 @@ import (
|
||||
// ifHCOutUcastPkts .11 - Counter64
|
||||
// ifHCOutMulticastPkts .12 - Counter64
|
||||
// ifHCOutBroadcastPkts .13 - Counter64
|
||||
// ifHighSpeed .15 - Gauge32 (interface speed in Mbps)
|
||||
// ifAlias .18 - DisplayString
|
||||
|
||||
const ifEntryOID = "1.3.6.1.2.1.2.2.1"
|
||||
@@ -72,24 +73,26 @@ type VPPConfig struct {
|
||||
}
|
||||
|
||||
type VPPInterface struct {
|
||||
Description string `yaml:"description"`
|
||||
SubInterfaces map[string]VPPInterface `yaml:"sub-interfaces"`
|
||||
Description string `yaml:"description"`
|
||||
SubInterfaces map[string]VPPInterface `yaml:"sub-interfaces"`
|
||||
}
|
||||
|
||||
type InterfaceMIB struct {
|
||||
mutex sync.RWMutex
|
||||
handler *agentx.ListHandler
|
||||
ifEntrySession *agentx.Session
|
||||
ifXTableSession *agentx.Session
|
||||
stats map[uint32]*api.InterfaceCounters // indexed by interface index
|
||||
descriptions map[string]string // interface name -> description mapping
|
||||
mutex sync.RWMutex
|
||||
handler *agentx.ListHandler
|
||||
ifEntrySession *agentx.Session
|
||||
ifXTableSession *agentx.Session
|
||||
stats map[uint32]*api.InterfaceCounters // indexed by interface index
|
||||
descriptions map[string]string // interface name -> description mapping
|
||||
interfaceDetails map[uint32]*vpp.InterfaceDetails // indexed by interface index
|
||||
}
|
||||
|
||||
func NewInterfaceMIB() *InterfaceMIB {
|
||||
return &InterfaceMIB{
|
||||
handler: &agentx.ListHandler{},
|
||||
stats: make(map[uint32]*api.InterfaceCounters),
|
||||
descriptions: make(map[string]string),
|
||||
handler: &agentx.ListHandler{},
|
||||
stats: make(map[uint32]*api.InterfaceCounters),
|
||||
descriptions: make(map[string]string),
|
||||
interfaceDetails: make(map[uint32]*vpp.InterfaceDetails),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +105,7 @@ func (m *InterfaceMIB) LoadVPPConfig(configPath string) error {
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// Read YAML file
|
||||
data, err := ioutil.ReadFile(configPath)
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read VPP config file: %v", err)
|
||||
}
|
||||
@@ -142,6 +145,22 @@ func (m *InterfaceMIB) LoadVPPConfig(configPath string) error {
|
||||
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) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
@@ -172,7 +191,7 @@ func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
|
||||
}
|
||||
|
||||
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
|
||||
m.addIfEntry(iface, idx)
|
||||
@@ -186,6 +205,9 @@ func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) {
|
||||
func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
|
||||
var item *agentx.ListItem
|
||||
|
||||
// Get interface details if available
|
||||
details := m.interfaceDetails[iface.InterfaceIndex]
|
||||
|
||||
// ifIndex (.1)
|
||||
item = m.handler.Add(fmt.Sprintf("%s.1.%d", ifEntryOID, idx))
|
||||
item.Type = pdu.VariableTypeInteger
|
||||
@@ -201,30 +223,63 @@ func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
|
||||
item.Type = pdu.VariableTypeInteger
|
||||
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.Type = pdu.VariableTypeInteger
|
||||
item.Value = int32(1500)
|
||||
item.Value = mtu
|
||||
|
||||
// ifSpeed (.5) - Default to 1Gbps (1000000000 bits/sec)
|
||||
item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifEntryOID, idx))
|
||||
item.Type = pdu.VariableTypeGauge32
|
||||
item.Value = uint32(1000000000)
|
||||
// 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.Type = pdu.VariableTypeGauge32
|
||||
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.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.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.Type = pdu.VariableTypeInteger
|
||||
item.Value = int32(1)
|
||||
item.Value = operStatus
|
||||
|
||||
// ifLastChange (.9) - 0 (unknown)
|
||||
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)
|
||||
item = m.handler.Add(fmt.Sprintf("%s.11.%d", ifEntryOID, idx))
|
||||
item.Type = pdu.VariableTypeCounter32
|
||||
item.Value = uint32(iface.RxUnicast.Packets)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// ifInNUcastPkts (.12) - multicast + broadcast
|
||||
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)
|
||||
item = m.handler.Add(fmt.Sprintf("%s.17.%d", ifEntryOID, idx))
|
||||
item.Type = pdu.VariableTypeCounter32
|
||||
item.Value = uint32(iface.TxUnicast.Packets)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// ifOutNUcastPkts (.18) - multicast + broadcast
|
||||
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)
|
||||
item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifXTableOID, idx))
|
||||
item.Type = pdu.VariableTypeCounter64
|
||||
item.Value = iface.RxUnicast.Packets
|
||||
if iface.RxUnicast.Packets == 0 {
|
||||
item.Value = iface.Rx.Packets
|
||||
} else {
|
||||
item.Value = iface.RxUnicast.Packets
|
||||
}
|
||||
|
||||
// ifHCInMulticastPkts (.8)
|
||||
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)
|
||||
item = m.handler.Add(fmt.Sprintf("%s.11.%d", ifXTableOID, idx))
|
||||
item.Type = pdu.VariableTypeCounter64
|
||||
item.Value = iface.TxUnicast.Packets
|
||||
if iface.TxUnicast.Packets == 0 {
|
||||
item.Value = iface.Tx.Packets
|
||||
} else {
|
||||
item.Value = iface.TxUnicast.Packets
|
||||
}
|
||||
|
||||
// ifHCOutMulticastPkts (.12)
|
||||
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.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
|
||||
item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifXTableOID, idx))
|
||||
item.Type = pdu.VariableTypeOctetString
|
||||
@@ -26,7 +26,7 @@ func TestPrintf(t *testing.T) {
|
||||
|
||||
// Read captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
_, _ = io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Check output format: "INFO file.go:function message"
|
||||
@@ -64,7 +64,7 @@ func TestDebugfWithDebugEnabled(t *testing.T) {
|
||||
|
||||
// Read captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
_, _ = io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Check output format: "DEBUG file.go:function message"
|
||||
@@ -98,7 +98,7 @@ func TestDebugfWithDebugDisabled(t *testing.T) {
|
||||
|
||||
// Read captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
_, _ = io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Should be empty when debug is disabled
|
||||
@@ -13,9 +13,11 @@ import (
|
||||
"govpp-snmp-agentx/config"
|
||||
"govpp-snmp-agentx/ifmib"
|
||||
"govpp-snmp-agentx/logger"
|
||||
"govpp-snmp-agentx/vppstats"
|
||||
"govpp-snmp-agentx/vpp"
|
||||
)
|
||||
|
||||
const Version = "1.1.4-1"
|
||||
|
||||
func main() {
|
||||
debug := flag.Bool("debug", false, "Enable debug logging")
|
||||
vppcfg := flag.String("vppcfg", "", "VPP configuration YAML file to read interface descriptions from")
|
||||
@@ -24,13 +26,17 @@ func main() {
|
||||
// Set global debug flag
|
||||
config.Debug = *debug
|
||||
|
||||
// Log startup message with version
|
||||
logger.Printf("Starting govpp-snmp-agentx version %s", Version)
|
||||
|
||||
// Create the interface MIB
|
||||
interfaceMIB := ifmib.NewInterfaceMIB()
|
||||
|
||||
// Load VPP config if specified
|
||||
if *vppcfg != "" {
|
||||
if err := interfaceMIB.LoadVPPConfig(*vppcfg); err != nil {
|
||||
log.Fatalf("Failed to load VPP config: %v", err)
|
||||
logger.Printf("Warning: Failed to load VPP config from %s: %v", *vppcfg, err)
|
||||
logger.Printf("Continuing without VPP config file...")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +45,19 @@ func main() {
|
||||
log.Fatalf("Failed to start AgentX: %v", err)
|
||||
}
|
||||
|
||||
// Start VPP stats routine with callback to update MIB
|
||||
vppstats.StartStatsRoutine(interfaceMIB.UpdateStats)
|
||||
// Create VPP client and managers
|
||||
vppClient := vpp.NewVPPClient()
|
||||
interfaceManager := vpp.NewInterfaceManager(vppClient)
|
||||
statsManager := vpp.NewStatsManager(vppClient, interfaceManager)
|
||||
|
||||
// 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()
|
||||
|
||||
// Set up signal handling for graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
@@ -50,6 +67,10 @@ func main() {
|
||||
<-sigChan
|
||||
logger.Printf("Shutting down...")
|
||||
|
||||
// Stop stats routine and disconnect
|
||||
statsManager.StopStatsRoutine()
|
||||
vppClient.Disconnect()
|
||||
|
||||
// Flush any buffered log entries
|
||||
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
|
||||
}
|
||||
200
src/vpp/vpp_iface.go
Normal file
200
src/vpp/vpp_iface.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||
|
||||
package vpp
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.Debugf("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
|
||||
}
|
||||
190
src/vpp/vpp_iface_test.go
Normal file
190
src/vpp/vpp_iface_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||
|
||||
package vpp
|
||||
|
||||
import (
|
||||
"go.fd.io/govpp/binapi/interface_types"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewInterfaceManager(t *testing.T) {
|
||||
client := NewVPPClient()
|
||||
manager := NewInterfaceManager(client)
|
||||
|
||||
if manager == nil {
|
||||
t.Fatal("NewInterfaceManager() returned nil")
|
||||
}
|
||||
|
||||
if manager.client != client {
|
||||
t.Error("InterfaceManager should store the provided client")
|
||||
}
|
||||
|
||||
if manager.eventCallback != nil {
|
||||
t.Error("InterfaceManager should have nil callback 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 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)
|
||||
}
|
||||
}
|
||||
180
src/vpp/vpp_stats.go
Normal file
180
src/vpp/vpp_stats.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// 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
|
||||
interfaceManager *InterfaceManager
|
||||
statsCallback StatsCallback
|
||||
period time.Duration
|
||||
running bool
|
||||
}
|
||||
|
||||
// NewStatsManager creates a new stats manager
|
||||
func NewStatsManager(client *VPPClient, interfaceManager *InterfaceManager) *StatsManager {
|
||||
return &StatsManager{
|
||||
client: client,
|
||||
interfaceManager: interfaceManager,
|
||||
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.Debugf("VPP not connected, attempting connection...")
|
||||
}
|
||||
|
||||
if err := sm.client.Connect(); err != nil {
|
||||
logger.Debugf("Failed to connect to VPP: %v", err)
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Printf("VPP connection established")
|
||||
wasConnected = true
|
||||
|
||||
// Initialize interface event watching
|
||||
if sm.interfaceManager != nil {
|
||||
if err := sm.interfaceManager.StartEventWatcher(); err != nil {
|
||||
logger.Debugf("Failed to start interface event watching: %v", err)
|
||||
} else {
|
||||
logger.Debugf("Interface event watching started")
|
||||
|
||||
// Get initial interface details
|
||||
if details, err := sm.interfaceManager.GetAllInterfaceDetails(); err != nil {
|
||||
logger.Debugf("Failed to get initial interface details: %v", err)
|
||||
} else {
|
||||
logger.Debugf("Retrieved initial interface details for %d interfaces", len(details))
|
||||
if sm.interfaceManager.eventCallback != nil {
|
||||
sm.interfaceManager.eventCallback(details)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 sm.statsCallback != nil {
|
||||
sm.statsCallback(stats)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
250
src/vpp/vpp_stats_test.go
Normal file
250
src/vpp/vpp_stats_test.go
Normal file
@@ -0,0 +1,250 @@
|
||||
// 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()
|
||||
interfaceManager := NewInterfaceManager(client)
|
||||
manager := NewStatsManager(client, interfaceManager)
|
||||
|
||||
if manager == nil {
|
||||
t.Fatal("NewStatsManager() returned nil")
|
||||
}
|
||||
|
||||
if manager.client != client {
|
||||
t.Error("StatsManager should store the provided client")
|
||||
}
|
||||
|
||||
if manager.interfaceManager != interfaceManager {
|
||||
t.Error("StatsManager should store the provided interface manager")
|
||||
}
|
||||
|
||||
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()
|
||||
interfaceManager := NewInterfaceManager(client)
|
||||
manager := NewStatsManager(client, interfaceManager)
|
||||
|
||||
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()
|
||||
interfaceManager := NewInterfaceManager(client)
|
||||
manager := NewStatsManager(client, interfaceManager)
|
||||
|
||||
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()
|
||||
interfaceManager := NewInterfaceManager(client)
|
||||
manager := NewStatsManager(client, interfaceManager)
|
||||
|
||||
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()
|
||||
interfaceManager := NewInterfaceManager(client)
|
||||
manager := NewStatsManager(client, interfaceManager)
|
||||
|
||||
_, 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()
|
||||
interfaceManager := NewInterfaceManager(client)
|
||||
manager := NewStatsManager(client, interfaceManager)
|
||||
|
||||
// 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()
|
||||
interfaceManager := NewInterfaceManager(client)
|
||||
manager := NewStatsManager(client, interfaceManager)
|
||||
|
||||
// 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,237 +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
|
||||
select {
|
||||
case <-ticker.C:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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