Compare commits

..

13 Commits

68 changed files with 743 additions and 531 deletions

View File

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

321
README.md
View File

@ -1,315 +1,46 @@
# VPP SNMP AgentX Bridge # VPP SNMP AgentX Bridge
A Go application that bridges VPP (Vector Packet Processing) interface statistics to SNMP using the SNMP AgentX daemon that exposes VPP interface statistics via standard IF-MIB.
AgentX protocol. It queries VPP interface counters and exposes them via the standard IF-MIB
for SNMP monitoring.
## Features ## Quick Start
- **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
```bash ```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 ```bash
# Linux static binary # Edit service configuration
CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o govpp-snmp-agentx . sudo nano /etc/default/govpp-snmp-agentx
# Cross-compile for different architectures # Start service
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags '-extldflags "-static"' -o govpp-snmp-agentx-linux-amd64 . sudo systemctl start govpp-snmp-agentx
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -ldflags '-extldflags "-static"' -o govpp-snmp-agentx-linux-arm64 .
``` ```
### Release Build with Version Info Default configuration:
```
```bash GOVPP_SNMP_AGENTX_FLAGS="-agentx.addr /var/agentx/master -vppcfg /etc/vpp/vppcfg.yaml -vppstats.period 10"
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 .
``` ```
## Usage ## Documentation
### Basic Usage - **Manual page**: `man govpp-snmp-agentx` (after package installation)
- **Detailed documentation**: [docs/DETAILS.md](docs/DETAILS.md)
```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
```
## License ## License
This project uses the LGPL 3.0 licensed go-agentx library. It has been modified due to a bug, LGPL 3.0 (due to modified go-agentx dependency)
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

47
debian/changelog vendored
View File

@ -1,3 +1,50 @@
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 govpp-snmp-agentx (1.0.1-1) bookworm; urgency=medium
* Add manual page for govpp-snmp-agentx(1) * Add manual page for govpp-snmp-agentx(1)

4
debian/control vendored
View File

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

3
debian/rules vendored
View File

@ -10,11 +10,12 @@ export GOPATH = $(CURDIR)/debian/go
dh $@ dh $@
override_dh_auto_build: override_dh_auto_build:
go build -v -ldflags="-s -w" -o govpp-snmp-agentx . cd src && go build -v -ldflags="-s -w" -o ../govpp-snmp-agentx .
override_dh_auto_install: override_dh_auto_install:
install -D -m 0755 govpp-snmp-agentx debian/govpp-snmp-agentx/usr/sbin/govpp-snmp-agentx 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.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 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: override_dh_auto_configure:

322
docs/DETAILS.md Normal file
View File

@ -0,0 +1,322 @@
# 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/vppstats/`): 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
### 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
## 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

15
govpp-snmp-agentx.default Normal file
View 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"

View File

@ -4,9 +4,10 @@ After=network.target vpp.service
[Service] [Service]
Type=simple Type=simple
EnvironmentFile=-/etc/default/govpp-snmp-agentx
ExecStartPre=-+/usr/bin/chmod 770 /var/agentx /var/agentx/master ExecStartPre=-+/usr/bin/chmod 770 /var/agentx /var/agentx/master
ExecStartPre=-+/usr/bin/chown Debian-snmp:vpp /var/agentx /var/agentx/master ExecStartPre=-+/usr/bin/chown Debian-snmp:vpp /var/agentx /var/agentx/master
ExecStart=/usr/sbin/govpp-snmp-agentx -agentx.addr /var/agentx/master -vppcfg /etc/vpp/vppcfg.yaml -vppstats.period 10 ExecStart=/usr/sbin/govpp-snmp-agentx $GOVPP_SNMP_AGENTX_FLAGS
User=Debian-snmp User=Debian-snmp
Group=vpp Group=vpp
ExecReload=/bin/kill -HUP $MAINPID ExecReload=/bin/kill -HUP $MAINPID

View File

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

View File

View File

@ -13,9 +13,9 @@ import (
"github.com/posteo/go-agentx/value" "github.com/posteo/go-agentx/value"
"go.fd.io/govpp/api" "go.fd.io/govpp/api"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"govpp-snmp-agentx/logger" "govpp-snmp-agentx/logger"
"govpp-snmp-agentx/vppstats" "govpp-snmp-agentx/vpp"
) )
// IF-MIB OID bases: // IF-MIB OID bases:
@ -60,6 +60,7 @@ import (
// ifHCOutUcastPkts .11 - Counter64 // ifHCOutUcastPkts .11 - Counter64
// ifHCOutMulticastPkts .12 - Counter64 // ifHCOutMulticastPkts .12 - Counter64
// ifHCOutBroadcastPkts .13 - Counter64 // ifHCOutBroadcastPkts .13 - Counter64
// ifHighSpeed .15 - Gauge32 (interface speed in Mbps)
// ifAlias .18 - DisplayString // ifAlias .18 - DisplayString
const ifEntryOID = "1.3.6.1.2.1.2.2.1" const ifEntryOID = "1.3.6.1.2.1.2.2.1"
@ -72,24 +73,26 @@ type VPPConfig struct {
} }
type VPPInterface struct { type VPPInterface struct {
Description string `yaml:"description"` Description string `yaml:"description"`
SubInterfaces map[string]VPPInterface `yaml:"sub-interfaces"` SubInterfaces map[string]VPPInterface `yaml:"sub-interfaces"`
} }
type InterfaceMIB struct { type InterfaceMIB struct {
mutex sync.RWMutex mutex sync.RWMutex
handler *agentx.ListHandler handler *agentx.ListHandler
ifEntrySession *agentx.Session ifEntrySession *agentx.Session
ifXTableSession *agentx.Session ifXTableSession *agentx.Session
stats map[uint32]*api.InterfaceCounters // indexed by interface index stats map[uint32]*api.InterfaceCounters // indexed by interface index
descriptions map[string]string // interface name -> description mapping descriptions map[string]string // interface name -> description mapping
interfaceDetails map[uint32]*vpp.InterfaceDetails // indexed by interface index
} }
func NewInterfaceMIB() *InterfaceMIB { func NewInterfaceMIB() *InterfaceMIB {
return &InterfaceMIB{ return &InterfaceMIB{
handler: &agentx.ListHandler{}, handler: &agentx.ListHandler{},
stats: make(map[uint32]*api.InterfaceCounters), stats: make(map[uint32]*api.InterfaceCounters),
descriptions: make(map[string]string), descriptions: make(map[string]string),
interfaceDetails: make(map[uint32]*vpp.InterfaceDetails),
} }
} }
@ -142,6 +145,22 @@ func (m *InterfaceMIB) LoadVPPConfig(configPath string) error {
return nil return nil
} }
func (m *InterfaceMIB) UpdateInterfaceDetails(details []vpp.InterfaceDetails) {
m.mutex.Lock()
defer m.mutex.Unlock()
logger.Debugf("Updating interface details for %d interfaces", len(details))
// Update interface details map
for _, detail := range details {
m.interfaceDetails[uint32(detail.SwIfIndex)] = &detail
logger.Debugf("Updated details for interface %d (%s): MAC=%x, Speed=%d",
detail.SwIfIndex, detail.InterfaceName, detail.MacAddress, detail.Speed)
}
logger.Debugf("Interface details updated for %d interfaces", len(details))
}
func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) { func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() defer m.mutex.Unlock()
@ -172,7 +191,7 @@ func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
} }
func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) { func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) {
idx := int(iface.InterfaceIndex) + *vppstats.IfIndexOffset idx := int(iface.InterfaceIndex) + *vpp.IfIndexOffset
// Add ifEntry (classic interface table) entries // Add ifEntry (classic interface table) entries
m.addIfEntry(iface, idx) m.addIfEntry(iface, idx)
@ -186,6 +205,9 @@ func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) {
func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) { func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
var item *agentx.ListItem var item *agentx.ListItem
// Get interface details if available
details := m.interfaceDetails[iface.InterfaceIndex]
// ifIndex (.1) // ifIndex (.1)
item = m.handler.Add(fmt.Sprintf("%s.1.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.1.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeInteger item.Type = pdu.VariableTypeInteger
@ -201,30 +223,63 @@ func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
item.Type = pdu.VariableTypeInteger item.Type = pdu.VariableTypeInteger
item.Value = int32(6) item.Value = int32(6)
// ifMtu (.4) - Default MTU 1500 // ifMtu (.4) - Use real MTU if available, otherwise default to 1500
mtu := int32(1500)
if details != nil {
mtu = int32(details.MTU)
}
item = m.handler.Add(fmt.Sprintf("%s.4.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.4.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeInteger item.Type = pdu.VariableTypeInteger
item.Value = int32(1500) item.Value = mtu
// ifSpeed (.5) - Default to 1Gbps (1000000000 bits/sec) // ifSpeed (.5) - Only populate for speeds <= 2.5Gbps (legacy field limitation)
item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifEntryOID, idx)) if details != nil && details.Speed > 0 && details.Speed <= 2500000000 {
item.Type = pdu.VariableTypeGauge32 // Use real speed for interfaces <= 2.5Gbps
item.Value = uint32(1000000000) 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 = m.handler.Add(fmt.Sprintf("%s.6.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeOctetString item.Type = pdu.VariableTypeOctetString
item.Value = "" item.Value = macAddr
// ifAdminStatus (.7) - up(1) // ifAdminStatus (.7) - Use real admin status if available
adminStatus := int32(1) // default up
if details != nil {
if details.AdminStatus {
adminStatus = 1 // up
} else {
adminStatus = 2 // down
}
}
item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeInteger item.Type = pdu.VariableTypeInteger
item.Value = int32(1) item.Value = adminStatus
// ifOperStatus (.8) - up(1) // ifOperStatus (.8) - Use real operational status if available
operStatus := int32(1) // default up
if details != nil {
if details.OperStatus {
operStatus = 1 // up
} else {
operStatus = 2 // down
}
}
item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifEntryOID, idx))
item.Type = pdu.VariableTypeInteger item.Type = pdu.VariableTypeInteger
item.Value = int32(1) item.Value = operStatus
// ifLastChange (.9) - 0 (unknown) // ifLastChange (.9) - 0 (unknown)
item = m.handler.Add(fmt.Sprintf("%s.9.%d", ifEntryOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.9.%d", ifEntryOID, idx))
@ -362,6 +417,16 @@ func (m *InterfaceMIB) addIfXTable(iface *api.InterfaceCounters, idx int) {
item.Type = pdu.VariableTypeCounter64 item.Type = pdu.VariableTypeCounter64
item.Value = iface.TxBroadcast.Packets item.Value = iface.TxBroadcast.Packets
// ifHighSpeed (.15) - Interface speed in Megabits per second
details := m.interfaceDetails[iface.InterfaceIndex]
speedMbps := uint32(1000) // default 1 Gbps = 1000 Mbps
if details != nil && details.Speed > 0 {
speedMbps = uint32(details.Speed / 1000000) // Convert bps to Mbps
}
item = m.handler.Add(fmt.Sprintf("%s.15.%d", ifXTableOID, idx))
item.Type = pdu.VariableTypeGauge32
item.Value = speedMbps
// ifAlias (.18) - Interface description/alias // ifAlias (.18) - Interface description/alias
item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifXTableOID, idx)) item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifXTableOID, idx))
item.Type = pdu.VariableTypeOctetString item.Type = pdu.VariableTypeOctetString

View File

@ -11,27 +11,27 @@ import (
func TestNewInterfaceMIB(t *testing.T) { func TestNewInterfaceMIB(t *testing.T) {
mib := NewInterfaceMIB() mib := NewInterfaceMIB()
if mib == nil { if mib == nil {
t.Fatal("NewInterfaceMIB returned nil") t.Fatal("NewInterfaceMIB returned nil")
} }
if mib.handler == nil { if mib.handler == nil {
t.Error("Expected handler to be initialized") t.Error("Expected handler to be initialized")
} }
if mib.stats == nil { if mib.stats == nil {
t.Error("Expected stats map to be initialized") t.Error("Expected stats map to be initialized")
} }
if mib.descriptions == nil { if mib.descriptions == nil {
t.Error("Expected descriptions map to be initialized") t.Error("Expected descriptions map to be initialized")
} }
if len(mib.stats) != 0 { if len(mib.stats) != 0 {
t.Errorf("Expected stats map to be empty, got %d entries", len(mib.stats)) t.Errorf("Expected stats map to be empty, got %d entries", len(mib.stats))
} }
if len(mib.descriptions) != 0 { if len(mib.descriptions) != 0 {
t.Errorf("Expected descriptions map to be empty, got %d entries", len(mib.descriptions)) t.Errorf("Expected descriptions map to be empty, got %d entries", len(mib.descriptions))
} }
@ -40,11 +40,11 @@ func TestNewInterfaceMIB(t *testing.T) {
func TestGetHandler(t *testing.T) { func TestGetHandler(t *testing.T) {
mib := NewInterfaceMIB() mib := NewInterfaceMIB()
handler := mib.GetHandler() handler := mib.GetHandler()
if handler == nil { if handler == nil {
t.Error("GetHandler returned nil") t.Error("GetHandler returned nil")
} }
if handler != mib.handler { if handler != mib.handler {
t.Error("GetHandler returned different handler than expected") t.Error("GetHandler returned different handler than expected")
} }
@ -52,7 +52,7 @@ func TestGetHandler(t *testing.T) {
func TestLoadVPPConfigValidYAML(t *testing.T) { func TestLoadVPPConfigValidYAML(t *testing.T) {
mib := NewInterfaceMIB() mib := NewInterfaceMIB()
// Create a temporary YAML file // Create a temporary YAML file
yamlContent := `interfaces: yamlContent := `interfaces:
GigabitEthernet0/0/0: GigabitEthernet0/0/0:
@ -64,39 +64,39 @@ loopbacks:
loop0: loop0:
description: 'Test: Loopback' description: 'Test: Loopback'
` `
tmpfile, err := os.CreateTemp("", "test_*.yaml") tmpfile, err := os.CreateTemp("", "test_*.yaml")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer os.Remove(tmpfile.Name()) defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write([]byte(yamlContent)); err != nil { if _, err := tmpfile.Write([]byte(yamlContent)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := tmpfile.Close(); err != nil { if err := tmpfile.Close(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Test loading the config // Test loading the config
err = mib.LoadVPPConfig(tmpfile.Name()) err = mib.LoadVPPConfig(tmpfile.Name())
if err != nil { if err != nil {
t.Fatalf("LoadVPPConfig failed: %v", err) t.Fatalf("LoadVPPConfig failed: %v", err)
} }
// Check that descriptions were loaded // Check that descriptions were loaded
if len(mib.descriptions) != 3 { if len(mib.descriptions) != 3 {
t.Errorf("Expected 3 descriptions, got %d", len(mib.descriptions)) t.Errorf("Expected 3 descriptions, got %d", len(mib.descriptions))
} }
if mib.descriptions["GigabitEthernet0/0/0"] != "Test: Interface" { if mib.descriptions["GigabitEthernet0/0/0"] != "Test: Interface" {
t.Errorf("Unexpected interface description: %s", mib.descriptions["GigabitEthernet0/0/0"]) t.Errorf("Unexpected interface description: %s", mib.descriptions["GigabitEthernet0/0/0"])
} }
if mib.descriptions["GigabitEthernet0/0/0.100"] != "Test: Sub-interface" { if mib.descriptions["GigabitEthernet0/0/0.100"] != "Test: Sub-interface" {
t.Errorf("Unexpected sub-interface description: %s", mib.descriptions["GigabitEthernet0/0/0.100"]) t.Errorf("Unexpected sub-interface description: %s", mib.descriptions["GigabitEthernet0/0/0.100"])
} }
if mib.descriptions["loop0"] != "Test: Loopback" { if mib.descriptions["loop0"] != "Test: Loopback" {
t.Errorf("Unexpected loopback description: %s", mib.descriptions["loop0"]) t.Errorf("Unexpected loopback description: %s", mib.descriptions["loop0"])
} }
@ -104,7 +104,7 @@ loopbacks:
func TestLoadVPPConfigNonExistentFile(t *testing.T) { func TestLoadVPPConfigNonExistentFile(t *testing.T) {
mib := NewInterfaceMIB() mib := NewInterfaceMIB()
err := mib.LoadVPPConfig("/nonexistent/file.yaml") err := mib.LoadVPPConfig("/nonexistent/file.yaml")
if err == nil { if err == nil {
t.Error("Expected error for non-existent file") t.Error("Expected error for non-existent file")
@ -113,25 +113,25 @@ func TestLoadVPPConfigNonExistentFile(t *testing.T) {
func TestLoadVPPConfigInvalidYAML(t *testing.T) { func TestLoadVPPConfigInvalidYAML(t *testing.T) {
mib := NewInterfaceMIB() mib := NewInterfaceMIB()
// Create a temporary file with invalid YAML // Create a temporary file with invalid YAML
invalidYAML := `interfaces: invalidYAML := `interfaces:
test: [ test: [
` `
tmpfile, err := os.CreateTemp("", "invalid_*.yaml") tmpfile, err := os.CreateTemp("", "invalid_*.yaml")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer os.Remove(tmpfile.Name()) defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write([]byte(invalidYAML)); err != nil { if _, err := tmpfile.Write([]byte(invalidYAML)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := tmpfile.Close(); err != nil { if err := tmpfile.Close(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
err = mib.LoadVPPConfig(tmpfile.Name()) err = mib.LoadVPPConfig(tmpfile.Name())
if err == nil { if err == nil {
t.Error("Expected error for invalid YAML") t.Error("Expected error for invalid YAML")
@ -140,7 +140,7 @@ func TestLoadVPPConfigInvalidYAML(t *testing.T) {
func TestUpdateStatsBasic(t *testing.T) { func TestUpdateStatsBasic(t *testing.T) {
mib := NewInterfaceMIB() mib := NewInterfaceMIB()
// Create mock interface stats // Create mock interface stats
stats := &api.InterfaceStats{ stats := &api.InterfaceStats{
Interfaces: []api.InterfaceCounters{ Interfaces: []api.InterfaceCounters{
@ -158,15 +158,15 @@ func TestUpdateStatsBasic(t *testing.T) {
}, },
}, },
} }
// Call UpdateStats (this will test the basic flow without AgentX sessions) // Call UpdateStats (this will test the basic flow without AgentX sessions)
mib.UpdateStats(stats) mib.UpdateStats(stats)
// Check that stats were stored // Check that stats were stored
if len(mib.stats) != 1 { if len(mib.stats) != 1 {
t.Errorf("Expected 1 interface in stats, got %d", len(mib.stats)) t.Errorf("Expected 1 interface in stats, got %d", len(mib.stats))
} }
if storedStats, exists := mib.stats[0]; !exists { if storedStats, exists := mib.stats[0]; !exists {
t.Error("Expected interface 0 to be stored in stats") t.Error("Expected interface 0 to be stored in stats")
} else { } else {
@ -177,4 +177,4 @@ func TestUpdateStatsBasic(t *testing.T) {
t.Errorf("Expected RX packets 100, got %d", storedStats.Rx.Packets) t.Errorf("Expected RX packets 100, got %d", storedStats.Rx.Packets)
} }
} }
} }

View File

@ -13,9 +13,11 @@ import (
"govpp-snmp-agentx/config" "govpp-snmp-agentx/config"
"govpp-snmp-agentx/ifmib" "govpp-snmp-agentx/ifmib"
"govpp-snmp-agentx/logger" "govpp-snmp-agentx/logger"
"govpp-snmp-agentx/vppstats" "govpp-snmp-agentx/vpp"
) )
const Version = "1.1.2-1"
func main() { func main() {
debug := flag.Bool("debug", false, "Enable debug logging") debug := flag.Bool("debug", false, "Enable debug logging")
vppcfg := flag.String("vppcfg", "", "VPP configuration YAML file to read interface descriptions from") vppcfg := flag.String("vppcfg", "", "VPP configuration YAML file to read interface descriptions from")
@ -24,6 +26,9 @@ func main() {
// Set global debug flag // Set global debug flag
config.Debug = *debug config.Debug = *debug
// Log startup message with version
logger.Printf("Starting govpp-snmp-agentx version %s", Version)
// Create the interface MIB // Create the interface MIB
interfaceMIB := ifmib.NewInterfaceMIB() interfaceMIB := ifmib.NewInterfaceMIB()
@ -40,8 +45,11 @@ func main() {
log.Fatalf("Failed to start AgentX: %v", err) log.Fatalf("Failed to start AgentX: %v", err)
} }
// Set up interface event callback to update interface details
vpp.SetInterfaceEventCallback(interfaceMIB.UpdateInterfaceDetails)
// Start VPP stats routine with callback to update MIB // Start VPP stats routine with callback to update MIB
vppstats.StartStatsRoutine(interfaceMIB.UpdateStats) vpp.StartStatsRoutine(interfaceMIB.UpdateStats)
// Set up signal handling for graceful shutdown // Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
@ -50,7 +58,7 @@ func main() {
// Wait for shutdown signal // Wait for shutdown signal
<-sigChan <-sigChan
logger.Printf("Shutting down...") logger.Printf("Shutting down...")
// Flush any buffered log entries // Flush any buffered log entries
logger.Sync() logger.Sync()
} }

View File

@ -14,7 +14,7 @@ func TestMainCompiles(t *testing.T) {
// This would run main(), but we skip it in tests // This would run main(), but we skip it in tests
return return
} }
// Just test that we can access main package // Just test that we can access main package
t.Log("Main package compiles successfully") t.Log("Main package compiles successfully")
} }

145
src/vpp/vpp_iface.go Normal file
View File

@ -0,0 +1,145 @@
// 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)
// GetAllInterfaceDetails retrieves detailed information for all interfaces
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
}
func WatchInterfaceEvents(ch api.Channel, callback InterfaceEventCallback) 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, retrieve all interface details and call callback
if callback != nil {
details, err := GetAllInterfaceDetails(ch)
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))
callback(details)
}
}
}
logger.Debugf("Interface event listener goroutine ended")
}()
return nil
}

View File

@ -1,6 +1,6 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch> // Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package vppstats package vpp
import ( import (
"flag" "flag"
@ -17,6 +17,9 @@ import (
type StatsCallback func(*api.InterfaceStats) type StatsCallback func(*api.InterfaceStats)
// Global callback for interface events
var interfaceEventCallback InterfaceEventCallback
var ( var (
// Flags for VPP stats configuration // Flags for VPP stats configuration
ApiAddr = flag.String("vppstats.api.addr", "/var/run/vpp/api.sock", "VPP API socket path") ApiAddr = flag.String("vppstats.api.addr", "/var/run/vpp/api.sock", "VPP API socket path")
@ -25,6 +28,11 @@ var (
Period = flag.Int("vppstats.period", 10, "Interval in seconds for querying VPP interface stats") Period = flag.Int("vppstats.period", 10, "Interval in seconds for querying VPP interface stats")
) )
// SetInterfaceEventCallback sets the callback for interface events
func SetInterfaceEventCallback(callback InterfaceEventCallback) {
interfaceEventCallback = callback
}
// StartStatsRoutine starts a goroutine that queries VPP interface stats at the configured interval // StartStatsRoutine starts a goroutine that queries VPP interface stats at the configured interval
func StartStatsRoutine(callback StatsCallback) { func StartStatsRoutine(callback StatsCallback) {
period := time.Duration(*Period) * time.Second period := time.Duration(*Period) * time.Second
@ -131,6 +139,32 @@ func statsRoutine(period time.Duration, callback StatsCallback) {
logger.Printf("Connected to VPP (API: %s, Stats: %s)", *ApiAddr, *StatsAddr) logger.Printf("Connected to VPP (API: %s, Stats: %s)", *ApiAddr, *StatsAddr)
connected = true connected = true
wasConnected = true wasConnected = true
// Start watching interface events
logger.Debugf("Creating API channel for interface events...")
ch, err := conn.NewAPIChannel()
if err != nil {
logger.Debugf("Failed to create API channel for interface events: %v", err)
} else {
logger.Debugf("API channel created successfully, calling WatchInterfaceEvents...")
if err := WatchInterfaceEvents(ch, interfaceEventCallback); err != nil {
logger.Debugf("Failed to start interface event watching: %v", err)
ch.Close()
} else {
logger.Printf("Interface event watching started successfully")
// Do initial retrieval of interface details
if interfaceEventCallback != nil {
details, err := GetAllInterfaceDetails(ch)
if err != nil {
logger.Debugf("Failed to get initial interface details: %v", err)
} else {
logger.Debugf("Retrieved initial interface details for %d interfaces", len(details))
interfaceEventCallback(details)
}
}
}
}
} }
// Query stats if connected // Query stats if connected
@ -231,4 +265,3 @@ func checkVPPLiveness(conn *core.Connection) bool {
logger.Debugf("VPP liveness check passed (version: %s)", string(reply.Version)) logger.Debugf("VPP liveness check passed (version: %s)", string(reply.Version))
return true return true
} }

View File

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