Compare commits

...

24 Commits

Author SHA1 Message Date
42dbbded3d Simplify debian dependencies, cut release 1.0.3-1 2025-06-19 15:57:53 +02:00
f16a2b41ea Remove toolchain 2025-06-19 15:48:43 +02:00
5533ab00de Shorten Makefile 2025-06-17 01:26:23 +02:00
1cbca296c4 Refactor README into a terse synopsis and a larger DETAILS.md doc 2025-06-17 01:06:37 +02:00
a73c7cbf91 Update README 2025-06-17 00:59:57 +02:00
1ddc77ec73 Cut a new release with envfile 2025-06-17 00:57:24 +02:00
6063db7311 Move to an envfile for the debian package 2025-06-17 00:55:45 +02:00
7f81b51c1f Move Go code to src/ 2025-06-17 00:47:08 +02:00
c0bcdd5449 Make a simple change, bump version to 1.0.1 2025-06-17 00:40:36 +02:00
fa437ddaf1 Simplify service file, no longer require permissions on /run/vpp, and set optimal perms on agentx 2025-06-17 00:37:15 +02:00
0b4ff36130 Add manpage 2025-06-17 00:27:35 +02:00
82db92f344 Add an initial 'make pkg-deb' and debian/ control directory 2025-06-17 00:22:01 +02:00
adf033318a Add systemd service file 2025-06-16 23:54:55 +02:00
6969e609c0 Fix golangci-lint issues 2025-06-16 23:36:36 +02:00
4fdd0769a5 go mod tidy 2025-06-11 00:27:06 +02:00
d408ec2867 move to untimestamped logs, tag the lots INFO and DEBUG. Update tests 2025-06-11 00:22:16 +02:00
0a0e3e7055 Add tests. They are quite basic ... 2025-06-11 00:02:04 +02:00
cb8acc4c13 Add LICENSE, add copyright to all files 2025-06-10 23:52:49 +02:00
cc08a0218a Simplify - only use queryInterfaceStats() and always do liveness check within it 2025-06-10 14:39:15 +02:00
8d9aef2f99 Add flag -vppstats.api.addr and rename -vppstats.addr to -vppstats.stats.addr; Update README 2025-06-10 14:16:05 +02:00
87327658b2 Add a VPP API liveness check before reading the stats segment. 2025-06-10 14:12:55 +02:00
478168584d Rename from govpp-snmp-exmaple to govpp-snmp-agentx; Also rename the binary + README 2025-06-10 13:41:14 +02:00
467975b9d6 move agentx into its own directory, simplify main.go 2025-06-10 13:28:30 +02:00
458168e308 Add -vppcfg flag to set ifAlias from the 'description' fields in vppcfg.yaml 2025-06-09 19:12:05 +02:00
76 changed files with 1780 additions and 424 deletions

14
.gitignore vendored
View File

@ -1,2 +1,12 @@
vpp-snmp-agent
govpp-snmp-example
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

165
LICENSE Normal file
View File

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

22
Makefile Normal file
View File

@ -0,0 +1,22 @@
PROG = govpp-snmp-agentx
.PHONY: build test clean pkg-deb
# 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
# Build Debian package
pkg-deb:
fakeroot dpkg-buildpackage -us -uc -b

260
README.md
View File

@ -1,254 +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 (1.3.6.1.2.1.31.1.1.1) 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 → SNMP Master Agent
```
The application consists of three 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**: Registers with SNMP master agent to serve the MIB data
## Build Instructions
### Development Build
## Quick Start
```bash
go build -o vpp-snmp-agent .
# 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 vpp-snmp-agent .
# 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 vpp-snmp-agent-linux-amd64 .
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -ldflags '-extldflags "-static"' -o vpp-snmp-agent-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 vpp-snmp-agent .
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
./vpp-snmp-agent
# Run with custom AgentX address
./vpp-snmp-agent -agentx-addr 127.0.0.1:705
# Run with Unix socket AgentX connection
./vpp-snmp-agent -agentx-addr /var/agentx/master
```
### Command Line Flags
#### General Application Flags
| Flag | Default | Description |
|------|---------|-------------|
| `-agentx-addr` | `localhost:705` | AgentX master agent address (hostname:port or Unix socket path) |
| `-debug` | `false` | Enable debug logging |
#### VPP Statistics Module Flags
| Flag | Default | Description |
|------|---------|-------------|
| `-vppstats.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
./vpp-snmp-agent -debug
# Custom polling interval (5 seconds)
./vpp-snmp-agent -vppstats.period 5
# Custom VPP stats socket
./vpp-snmp-agent -vppstats.addr /custom/path/stats.sock
# Custom interface index offset (start at 2000)
./vpp-snmp-agent -vppstats.ifindex-offset 2000
# Full configuration
./vpp-snmp-agent \
-agentx-addr /var/agentx/master \
-debug \
-vppstats.addr /var/run/vpp/stats.sock \
-vppstats.period 5 \
-vppstats.ifindex-offset 1000
```
## 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) |
## 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 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.
## 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)

35
debian/changelog vendored Normal file
View File

@ -0,0 +1,35 @@
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
View 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

View 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
View 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
View 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
View 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-*

266
docs/DETAILS.md Normal file
View File

@ -0,0 +1,266 @@
# 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
```
## 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

72
docs/govpp-snmp-agentx.1 Normal file
View 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
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"

18
govpp-snmp-agentx.service Normal file
View 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

View File

@ -1,44 +0,0 @@
package logger
import (
"fmt"
"log"
"path/filepath"
"runtime"
"govpp-snmp-example/config"
)
// logf logs a message with automatic caller information (file:function)
func logf(format string, args ...interface{}) {
pc, file, _, ok := runtime.Caller(2)
if !ok {
log.Printf(format, args...)
return
}
fn := runtime.FuncForPC(pc)
if fn == nil {
log.Printf(format, args...)
return
}
funcName := filepath.Base(fn.Name())
fileName := filepath.Base(file)
prefix := fmt.Sprintf("%s:%s", fileName, funcName)
message := fmt.Sprintf(format, args...)
log.Printf("%s %s", prefix, message)
}
// Printf logs a message with caller information
func Printf(format string, args ...interface{}) {
logf(format, args...)
}
// Debugf logs a debug message with caller information if global debug is enabled
func Debugf(format string, args ...interface{}) {
if config.Debug {
logf(format, args...)
}
}

54
main.go
View File

@ -1,54 +0,0 @@
package main
import (
"flag"
"log"
"strings"
"time"
"github.com/posteo/go-agentx"
"govpp-snmp-example/config"
"govpp-snmp-example/ifmib"
"govpp-snmp-example/vppstats"
)
func main() {
addr := flag.String("agentx-addr", "localhost:705", "Address to connect to (hostname:port or Unix socket path)")
debug := flag.Bool("debug", false, "Enable debug logging")
flag.Parse()
// Set global debug flag
config.Debug = *debug
var network, address string
if strings.HasPrefix(*addr, "/") {
network = "unix"
address = *addr
} else {
network = "tcp"
address = *addr
}
client, err := agentx.Dial(network, address)
if err != nil {
log.Fatalf("Failed to dial %s %s: %v", network, address, err)
}
client.Timeout = 1 * time.Minute
client.ReconnectInterval = 1 * time.Second
// Create the interface MIB
interfaceMIB := ifmib.NewInterfaceMIB()
// Register the interface MIB with the AgentX client
if err := interfaceMIB.RegisterWithClient(client); err != nil {
log.Fatalf("Failed to register interface MIB: %v", err)
}
// Start VPP stats routine with callback to update MIB
vppstats.StartStatsRoutine(interfaceMIB.UpdateStats)
for {
time.Sleep(100 * time.Millisecond)
}
}

48
src/agentx/agentx.go Normal file
View File

@ -0,0 +1,48 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package agentx
import (
"flag"
"strings"
"time"
"github.com/posteo/go-agentx"
"govpp-snmp-agentx/ifmib"
"govpp-snmp-agentx/logger"
)
var (
// Flags for AgentX configuration
AgentXAddr = flag.String("agentx.addr", "localhost:705", "Address to connect to (hostname:port or Unix socket path)")
)
// StartAgentXRoutine initializes the AgentX client and registers the interface MIB
func StartAgentXRoutine(interfaceMIB *ifmib.InterfaceMIB) error {
var network, address string
if strings.HasPrefix(*AgentXAddr, "/") {
network = "unix"
address = *AgentXAddr
} else {
network = "tcp"
address = *AgentXAddr
}
logger.Debugf("Connecting to AgentX at %s://%s", network, address)
client, err := agentx.Dial(network, address)
if err != nil {
return err
}
client.Timeout = 1 * time.Minute
client.ReconnectInterval = 1 * time.Second
// Register the interface MIB with the AgentX client
if err := interfaceMIB.RegisterWithClient(client); err != nil {
return err
}
logger.Printf("Successfully registered with AgentX at %s://%s", network, address)
return nil
}

54
src/agentx/agentx_test.go Normal file
View File

@ -0,0 +1,54 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package agentx
import (
"flag"
"testing"
)
func TestAgentXAddrFlag(t *testing.T) {
// Test that the flag is registered with correct default
if *AgentXAddr != "localhost:705" {
t.Errorf("Expected default AgentX address to be 'localhost:705', got '%s'", *AgentXAddr)
}
}
func TestAgentXAddrFlagParsing(t *testing.T) {
// Save original flag value
originalAddr := *AgentXAddr
defer func() { *AgentXAddr = originalAddr }()
// Test Unix socket path
testAddr := "/var/run/test.sock"
*AgentXAddr = testAddr
if *AgentXAddr != testAddr {
t.Errorf("Expected AgentX address to be '%s', got '%s'", testAddr, *AgentXAddr)
}
// Test TCP address
testAddr = "192.168.1.1:705"
*AgentXAddr = testAddr
if *AgentXAddr != testAddr {
t.Errorf("Expected AgentX address to be '%s', got '%s'", testAddr, *AgentXAddr)
}
}
func TestFlagRegistration(t *testing.T) {
// Test that our flag is properly registered
f := flag.Lookup("agentx.addr")
if f == nil {
t.Error("Expected agentx.addr flag to be registered")
return
}
if f.DefValue != "localhost:705" {
t.Errorf("Expected flag default value to be 'localhost:705', got '%s'", f.DefValue)
}
if f.Usage != "Address to connect to (hostname:port or Unix socket path)" {
t.Errorf("Unexpected flag usage string: %s", f.Usage)
}
}

View File

@ -1,3 +1,5 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package config
// Global configuration variables

30
src/config/config_test.go Normal file
View File

@ -0,0 +1,30 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package config
import "testing"
func TestDebugFlagDefault(t *testing.T) {
// Test that Debug flag starts as false by default
if Debug != false {
t.Errorf("Expected Debug to be false by default, got %v", Debug)
}
}
func TestDebugFlagSet(t *testing.T) {
// Save original value
original := Debug
defer func() { Debug = original }()
// Test setting Debug to true
Debug = true
if Debug != true {
t.Errorf("Expected Debug to be true after setting, got %v", Debug)
}
// Test setting Debug to false
Debug = false
if Debug != false {
t.Errorf("Expected Debug to be false after setting, got %v", Debug)
}
}

View File

@ -1,17 +1,17 @@
module govpp-snmp-example
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
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/sys v0.31.0 // indirect

View File

@ -1,3 +1,4 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -7,6 +8,10 @@ github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff h1:zk1wwii7uXmI0znwU+
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff/go.mod h1:yUhRXHewUVJ1k89wHKP68xfzk7kwXUx/DV1nx4EBMbw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe h1:ewr1srjRCmcQogPQ/NCx6XCk6LGVmsVCc9Y3vvPZj+Y=
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
@ -29,6 +34,8 @@ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,7 +1,10 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package ifmib
import (
"fmt"
"os"
"sync"
"time"
@ -9,9 +12,10 @@ import (
"github.com/posteo/go-agentx/pdu"
"github.com/posteo/go-agentx/value"
"go.fd.io/govpp/api"
"gopkg.in/yaml.v3"
"govpp-snmp-example/logger"
"govpp-snmp-example/vppstats"
"govpp-snmp-agentx/logger"
"govpp-snmp-agentx/vppstats"
)
// IF-MIB OID bases:
@ -56,22 +60,36 @@ import (
// ifHCOutUcastPkts .11 - Counter64
// ifHCOutMulticastPkts .12 - Counter64
// ifHCOutBroadcastPkts .13 - Counter64
// ifAlias .18 - DisplayString
const ifEntryOID = "1.3.6.1.2.1.2.2.1"
const ifXTableOID = "1.3.6.1.2.1.31.1.1.1"
// VPP Config YAML structures
type VPPConfig struct {
Interfaces map[string]VPPInterface `yaml:"interfaces"`
Loopbacks map[string]VPPInterface `yaml:"loopbacks"`
}
type VPPInterface struct {
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
}
func NewInterfaceMIB() *InterfaceMIB {
return &InterfaceMIB{
handler: &agentx.ListHandler{},
stats: make(map[uint32]*api.InterfaceCounters),
handler: &agentx.ListHandler{},
stats: make(map[uint32]*api.InterfaceCounters),
descriptions: make(map[string]string),
}
}
@ -79,6 +97,51 @@ func (m *InterfaceMIB) GetHandler() *agentx.ListHandler {
return m.handler
}
func (m *InterfaceMIB) LoadVPPConfig(configPath string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
// Read YAML file
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read VPP config file: %v", err)
}
// Parse YAML
var config VPPConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return fmt.Errorf("failed to parse VPP config YAML: %v", err)
}
// Extract interface descriptions
for ifName, ifConfig := range config.Interfaces {
if ifConfig.Description != "" {
m.descriptions[ifName] = ifConfig.Description
logger.Debugf("Loaded description for interface %s: %s", ifName, ifConfig.Description)
}
// Process sub-interfaces
for subID, subConfig := range ifConfig.SubInterfaces {
if subConfig.Description != "" {
subIfName := fmt.Sprintf("%s.%s", ifName, subID)
m.descriptions[subIfName] = subConfig.Description
logger.Debugf("Loaded description for sub-interface %s: %s", subIfName, subConfig.Description)
}
}
}
// Extract loopback descriptions
for ifName, ifConfig := range config.Loopbacks {
if ifConfig.Description != "" {
m.descriptions[ifName] = ifConfig.Description
logger.Debugf("Loaded description for loopback %s: %s", ifName, ifConfig.Description)
}
}
logger.Printf("Loaded %d interface descriptions from VPP config", len(m.descriptions))
return nil
}
func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
m.mutex.Lock()
defer m.mutex.Unlock()
@ -298,6 +361,16 @@ func (m *InterfaceMIB) addIfXTable(iface *api.InterfaceCounters, idx int) {
item = m.handler.Add(fmt.Sprintf("%s.13.%d", ifXTableOID, idx))
item.Type = pdu.VariableTypeCounter64
item.Value = iface.TxBroadcast.Packets
// ifAlias (.18) - Interface description/alias
item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifXTableOID, idx))
item.Type = pdu.VariableTypeOctetString
// Use description from VPP config if available, otherwise use interface name
if desc, exists := m.descriptions[iface.InterfaceName]; exists {
item.Value = desc
} else {
item.Value = iface.InterfaceName
}
}
func (m *InterfaceMIB) RegisterWithClient(client *agentx.Client) error {

180
src/ifmib/ifmib_test.go Normal file
View File

@ -0,0 +1,180 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package ifmib
import (
"os"
"testing"
"go.fd.io/govpp/api"
)
func TestNewInterfaceMIB(t *testing.T) {
mib := NewInterfaceMIB()
if mib == nil {
t.Fatal("NewInterfaceMIB returned nil")
}
if mib.handler == nil {
t.Error("Expected handler to be initialized")
}
if mib.stats == nil {
t.Error("Expected stats map to be initialized")
}
if mib.descriptions == nil {
t.Error("Expected descriptions map to be initialized")
}
if len(mib.stats) != 0 {
t.Errorf("Expected stats map to be empty, got %d entries", len(mib.stats))
}
if len(mib.descriptions) != 0 {
t.Errorf("Expected descriptions map to be empty, got %d entries", len(mib.descriptions))
}
}
func TestGetHandler(t *testing.T) {
mib := NewInterfaceMIB()
handler := mib.GetHandler()
if handler == nil {
t.Error("GetHandler returned nil")
}
if handler != mib.handler {
t.Error("GetHandler returned different handler than expected")
}
}
func TestLoadVPPConfigValidYAML(t *testing.T) {
mib := NewInterfaceMIB()
// Create a temporary YAML file
yamlContent := `interfaces:
GigabitEthernet0/0/0:
description: 'Test: Interface'
sub-interfaces:
100:
description: 'Test: Sub-interface'
loopbacks:
loop0:
description: 'Test: Loopback'
`
tmpfile, err := os.CreateTemp("", "test_*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write([]byte(yamlContent)); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
// Test loading the config
err = mib.LoadVPPConfig(tmpfile.Name())
if err != nil {
t.Fatalf("LoadVPPConfig failed: %v", err)
}
// Check that descriptions were loaded
if len(mib.descriptions) != 3 {
t.Errorf("Expected 3 descriptions, got %d", len(mib.descriptions))
}
if mib.descriptions["GigabitEthernet0/0/0"] != "Test: Interface" {
t.Errorf("Unexpected interface description: %s", mib.descriptions["GigabitEthernet0/0/0"])
}
if mib.descriptions["GigabitEthernet0/0/0.100"] != "Test: Sub-interface" {
t.Errorf("Unexpected sub-interface description: %s", mib.descriptions["GigabitEthernet0/0/0.100"])
}
if mib.descriptions["loop0"] != "Test: Loopback" {
t.Errorf("Unexpected loopback description: %s", mib.descriptions["loop0"])
}
}
func TestLoadVPPConfigNonExistentFile(t *testing.T) {
mib := NewInterfaceMIB()
err := mib.LoadVPPConfig("/nonexistent/file.yaml")
if err == nil {
t.Error("Expected error for non-existent file")
}
}
func TestLoadVPPConfigInvalidYAML(t *testing.T) {
mib := NewInterfaceMIB()
// Create a temporary file with invalid YAML
invalidYAML := `interfaces:
test: [
`
tmpfile, err := os.CreateTemp("", "invalid_*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write([]byte(invalidYAML)); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
err = mib.LoadVPPConfig(tmpfile.Name())
if err == nil {
t.Error("Expected error for invalid YAML")
}
}
func TestUpdateStatsBasic(t *testing.T) {
mib := NewInterfaceMIB()
// Create mock interface stats
stats := &api.InterfaceStats{
Interfaces: []api.InterfaceCounters{
{
InterfaceIndex: 0,
InterfaceName: "test0",
Rx: api.InterfaceCounterCombined{
Packets: 100,
Bytes: 1000,
},
Tx: api.InterfaceCounterCombined{
Packets: 200,
Bytes: 2000,
},
},
},
}
// Call UpdateStats (this will test the basic flow without AgentX sessions)
mib.UpdateStats(stats)
// Check that stats were stored
if len(mib.stats) != 1 {
t.Errorf("Expected 1 interface in stats, got %d", len(mib.stats))
}
if storedStats, exists := mib.stats[0]; !exists {
t.Error("Expected interface 0 to be stored in stats")
} else {
if storedStats.InterfaceName != "test0" {
t.Errorf("Expected interface name 'test0', got '%s'", storedStats.InterfaceName)
}
if storedStats.Rx.Packets != 100 {
t.Errorf("Expected RX packets 100, got %d", storedStats.Rx.Packets)
}
}
}

52
src/logger/logger.go Normal file
View File

@ -0,0 +1,52 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package logger
import (
"fmt"
"path/filepath"
"runtime"
"govpp-snmp-agentx/config"
)
// getCallerInfo returns caller information in the format "file.go:function"
func getCallerInfo() string {
pc, file, _, ok := runtime.Caller(2) // Skip getCallerInfo and Printf/Debugf
if !ok {
return "unknown:unknown"
}
fn := runtime.FuncForPC(pc)
if fn == nil {
return "unknown:unknown"
}
funcName := filepath.Base(fn.Name())
fileName := filepath.Base(file)
return fmt.Sprintf("%s:%s", fileName, funcName)
}
// Printf logs a message with caller information in SYSLOG style
func Printf(format string, args ...interface{}) {
caller := getCallerInfo()
message := fmt.Sprintf(format, args...)
syslogMessage := fmt.Sprintf("INFO %s %s", caller, message)
fmt.Println(syslogMessage)
}
// Debugf logs a debug message with caller information if global debug is enabled
func Debugf(format string, args ...interface{}) {
if config.Debug {
caller := getCallerInfo()
message := fmt.Sprintf(format, args...)
syslogMessage := fmt.Sprintf("DEBUG %s %s", caller, message)
fmt.Println(syslogMessage)
}
}
// Sync flushes any buffered log entries (no-op for fmt.Println)
func Sync() {
// No buffering with fmt.Println, so this is a no-op
}

113
src/logger/logger_test.go Normal file
View File

@ -0,0 +1,113 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package logger
import (
"bytes"
"io"
"os"
"strings"
"testing"
"govpp-snmp-agentx/config"
)
func TestPrintf(t *testing.T) {
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
Printf("test message: %s", "hello")
// Close writer and restore stdout
w.Close()
os.Stdout = oldStdout
// Read captured output
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
// Check output format: "INFO file.go:function message"
if !strings.HasPrefix(output, "INFO ") {
t.Errorf("Expected output to start with 'INFO ', got: %s", output)
}
if !strings.Contains(output, "logger_test.go:logger.TestPrintf") {
t.Errorf("Expected output to contain caller info, got: %s", output)
}
if !strings.Contains(output, "test message: hello") {
t.Errorf("Expected output to contain message, got: %s", output)
}
}
func TestDebugfWithDebugEnabled(t *testing.T) {
// Save original debug state
originalDebug := config.Debug
defer func() { config.Debug = originalDebug }()
// Enable debug
config.Debug = true
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
Debugf("debug message: %s", "test")
// Close writer and restore stdout
w.Close()
os.Stdout = oldStdout
// Read captured output
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
// Check output format: "DEBUG file.go:function message"
if !strings.HasPrefix(output, "DEBUG ") {
t.Errorf("Expected output to start with 'DEBUG ', got: %s", output)
}
if !strings.Contains(output, "debug message: test") {
t.Errorf("Expected output to contain message, got: %s", output)
}
}
func TestDebugfWithDebugDisabled(t *testing.T) {
// Save original debug state
originalDebug := config.Debug
defer func() { config.Debug = originalDebug }()
// Disable debug
config.Debug = false
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
Debugf("debug message: %s", "test")
// Close writer and restore stdout
w.Close()
os.Stdout = oldStdout
// Read captured output
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
// Should be empty when debug is disabled
if output != "" {
t.Errorf("Expected no output when debug is disabled, got: %s", output)
}
}
func TestSync(t *testing.T) {
// Test that Sync doesn't panic (it's a no-op now)
Sync()
}

56
src/main.go Normal file
View File

@ -0,0 +1,56 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
"govpp-snmp-agentx/agentx"
"govpp-snmp-agentx/config"
"govpp-snmp-agentx/ifmib"
"govpp-snmp-agentx/logger"
"govpp-snmp-agentx/vppstats"
)
func main() {
debug := flag.Bool("debug", false, "Enable debug logging")
vppcfg := flag.String("vppcfg", "", "VPP configuration YAML file to read interface descriptions from")
flag.Parse()
// Set global debug flag
config.Debug = *debug
// Create the interface MIB
interfaceMIB := ifmib.NewInterfaceMIB()
// Load VPP config if specified
if *vppcfg != "" {
if err := interfaceMIB.LoadVPPConfig(*vppcfg); err != nil {
logger.Printf("Warning: Failed to load VPP config from %s: %v", *vppcfg, err)
logger.Printf("Continuing without VPP config file...")
}
}
// Start AgentX routine
if err := agentx.StartAgentXRoutine(interfaceMIB); err != nil {
log.Fatalf("Failed to start AgentX: %v", err)
}
// Start VPP stats routine with callback to update MIB
vppstats.StartStatsRoutine(interfaceMIB.UpdateStats)
// Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Wait for shutdown signal
<-sigChan
logger.Printf("Shutting down...")
// Flush any buffered log entries
logger.Sync()
}

20
src/main_test.go Normal file
View File

@ -0,0 +1,20 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package main
import (
"os"
"testing"
)
func TestMainCompiles(t *testing.T) {
// This test simply ensures that main package compiles
// More comprehensive integration tests would require mocking VPP and SNMP
if os.Getenv("BE_MAIN") == "1" {
// This would run main(), but we skip it in tests
return
}
// Just test that we can access main package
t.Log("Main package compiles successfully")
}

234
src/vppstats/stats.go Normal file
View File

@ -0,0 +1,234 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package vppstats
import (
"flag"
"time"
"go.fd.io/govpp/adapter/socketclient"
"go.fd.io/govpp/adapter/statsclient"
"go.fd.io/govpp/api"
"go.fd.io/govpp/binapi/vpe"
"go.fd.io/govpp/core"
"govpp-snmp-agentx/logger"
)
type StatsCallback func(*api.InterfaceStats)
var (
// Flags for VPP stats configuration
ApiAddr = flag.String("vppstats.api.addr", "/var/run/vpp/api.sock", "VPP API socket path")
StatsAddr = flag.String("vppstats.stats.addr", "/var/run/vpp/stats.sock", "VPP stats socket path")
IfIndexOffset = flag.Int("vppstats.ifindex-offset", 1000, "Offset to add to VPP interface indices for SNMP")
Period = flag.Int("vppstats.period", 10, "Interval in seconds for querying VPP interface stats")
)
// StartStatsRoutine starts a goroutine that queries VPP interface stats at the configured interval
func StartStatsRoutine(callback StatsCallback) {
period := time.Duration(*Period) * time.Second
go statsRoutine(period, callback)
}
func statsRoutine(period time.Duration, callback StatsCallback) {
logger.Debugf("Starting VPP stats routine with API: %s, Stats: %s, period: %v", *ApiAddr, *StatsAddr, period)
var conn *core.Connection
var statsConn *core.StatsConnection
var connected = false
var wasConnected = false
ticker := time.NewTicker(period)
defer ticker.Stop()
defer func() {
// Safely disconnect connections with panic recovery
if conn != nil {
func() {
defer func() {
if r := recover(); r != nil {
logger.Debugf("Recovered from conn.Disconnect panic: %v", r)
}
}()
conn.Disconnect()
}()
}
if statsConn != nil {
func() {
defer func() {
if r := recover(); r != nil {
logger.Debugf("Recovered from statsConn.Disconnect panic: %v", r)
}
}()
statsConn.Disconnect()
}()
}
}()
for {
// Check if we need to connect/reconnect
if !connected {
// Clean up existing connections
if conn != nil {
func() {
defer func() {
if r := recover(); r != nil {
logger.Debugf("Recovered from conn.Disconnect during reconnect: %v", r)
}
}()
conn.Disconnect()
}()
conn = nil
}
if statsConn != nil {
func() {
defer func() {
if r := recover(); r != nil {
logger.Debugf("Recovered from statsConn.Disconnect during reconnect: %v", r)
}
}()
statsConn.Disconnect()
}()
statsConn = nil
}
// Create API connection first - only proceed if this succeeds
var err error
conn, err = core.Connect(socketclient.NewVppClient(*ApiAddr))
if err != nil {
if wasConnected {
logger.Printf("VPP API connection lost: %v", err)
wasConnected = false
} else {
logger.Debugf("Failed to connect to VPP API: %v", err)
}
connected = false
time.Sleep(time.Second)
continue
}
// Only try stats connection if API connection succeeded
statsClient := statsclient.NewStatsClient(*StatsAddr)
statsConn, err = core.ConnectStats(statsClient)
if err != nil {
logger.Printf("VPP stats connection failed: %v", err)
// Close the API connection since we can't get stats
func() {
defer func() {
if r := recover(); r != nil {
logger.Debugf("Recovered from conn.Disconnect during stats error: %v", r)
}
}()
conn.Disconnect()
}()
conn = nil
connected = false
time.Sleep(time.Second)
continue
}
logger.Printf("Connected to VPP (API: %s, Stats: %s)", *ApiAddr, *StatsAddr)
connected = true
wasConnected = true
}
// Query stats if connected
if connected {
if !queryInterfaceStats(conn, statsConn, callback) {
connected = false
continue
}
}
// Wait for next tick
<-ticker.C
}
}
func queryInterfaceStats(conn *core.Connection, statsConn *core.StatsConnection, callback StatsCallback) bool {
// Check VPP liveness using API call
if !checkVPPLiveness(conn) {
logger.Printf("VPP liveness check failed")
return false
}
// Create the proper struct for interface stats
stats := new(api.InterfaceStats)
// Use the GetInterfaceStats method - this is the correct approach
if err := statsConn.GetInterfaceStats(stats); err != nil {
logger.Printf("Failed to get interface stats: %v", err)
return false
}
// Always log basic info
logger.Printf("Retrieved stats for %d interfaces", len(stats.Interfaces))
// Debug logging for individual interfaces
for _, iface := range stats.Interfaces {
logger.Debugf("Interface %d (%s): RX %d pkts/%d bytes, TX %d pkts/%d bytes",
iface.InterfaceIndex, iface.InterfaceName,
iface.Rx.Packets, iface.Rx.Bytes,
iface.Tx.Packets, iface.Tx.Bytes)
}
// Call the callback to update the MIB
if callback != nil {
callback(stats)
}
return true
}
func checkVPPLiveness(conn *core.Connection) bool {
// Create a channel for the API call
ch, err := conn.NewAPIChannel()
if err != nil {
logger.Debugf("Failed to create API channel: %v", err)
return false
}
// Use a flag to track if channel was closed successfully
var channelClosed bool
defer func() {
if !channelClosed {
// Recover from potential panic when closing already closed channel
defer func() {
if r := recover(); r != nil {
logger.Debugf("Recovered from channel close panic: %v", r)
}
}()
ch.Close()
}
}()
// Create ShowVersion request
req := &vpe.ShowVersion{}
reply := &vpe.ShowVersionReply{}
// Send the request with timeout
if err := ch.SendRequest(req).ReceiveReply(reply); err != nil {
logger.Debugf("VPP ShowVersion failed: %v", err)
// Try to close the channel properly on error
func() {
defer func() {
if r := recover(); r != nil {
logger.Debugf("Channel already closed during error handling")
}
}()
ch.Close()
channelClosed = true
}()
return false
}
// Close channel successfully
ch.Close()
channelClosed = true
// If we got here, VPP is responsive
logger.Debugf("VPP liveness check passed (version: %s)", string(reply.Version))
return true
}

163
src/vppstats/stats_test.go Normal file
View File

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

View File

@ -1,83 +0,0 @@
package vppstats
import (
"flag"
"log"
"time"
"go.fd.io/govpp/adapter/statsclient"
"go.fd.io/govpp/api"
"go.fd.io/govpp/core"
"govpp-snmp-example/logger"
)
type StatsCallback func(*api.InterfaceStats)
var (
// Flags for VPP stats configuration
StatsAddr = flag.String("vppstats.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 socket: %s, period: %v", *StatsAddr, period)
// Create stats client
client := statsclient.NewStatsClient(*StatsAddr)
// Connect using core.ConnectStats (proper way)
c, err := core.ConnectStats(client)
if err != nil {
log.Printf("Failed to connect to VPP stats: %v", err)
return
}
defer c.Disconnect()
// Query stats immediately on startup
queryInterfaceStats(c, callback)
ticker := time.NewTicker(period)
defer ticker.Stop()
for {
select {
case <-ticker.C:
queryInterfaceStats(c, callback)
}
}
}
func queryInterfaceStats(c *core.StatsConnection, callback StatsCallback) {
// Create the proper struct for interface stats
stats := new(api.InterfaceStats)
// Use the GetInterfaceStats method - this is the correct approach
if err := c.GetInterfaceStats(stats); err != nil {
logger.Printf("Failed to get interface stats: %v", err)
return
}
// 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)
}
}