Compare commits
29 Commits
069b1b6fc2
...
main
Author | SHA1 | Date | |
---|---|---|---|
0d19d50d62 | |||
686bbe46b0 | |||
ccc2b5ad4d | |||
4f368e625d | |||
35165b0464 | |||
42dbbded3d | |||
f16a2b41ea | |||
5533ab00de | |||
1cbca296c4 | |||
a73c7cbf91 | |||
1ddc77ec73 | |||
6063db7311 | |||
7f81b51c1f | |||
c0bcdd5449 | |||
fa437ddaf1 | |||
0b4ff36130 | |||
82db92f344 | |||
adf033318a | |||
6969e609c0 | |||
4fdd0769a5 | |||
d408ec2867 | |||
0a0e3e7055 | |||
cb8acc4c13 | |||
cc08a0218a | |||
8d9aef2f99 | |||
87327658b2 | |||
478168584d | |||
467975b9d6 | |||
458168e308 |
14
.gitignore
vendored
14
.gitignore
vendored
@ -1,2 +1,12 @@
|
|||||||
vpp-snmp-agent
|
govpp-snmp-agentx
|
||||||
govpp-snmp-example
|
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
165
LICENSE
Normal 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.
|
29
Makefile
Normal file
29
Makefile
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
PROG = govpp-snmp-agentx
|
||||||
|
|
||||||
|
.PHONY: build test clean pkg-deb sync-version
|
||||||
|
|
||||||
|
# Build the binary
|
||||||
|
build:
|
||||||
|
cd src && go build -o ../$(PROG) .
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
test:
|
||||||
|
cd src && go test ./...
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
rm -f $(PROG)
|
||||||
|
[ -d debian/go ] && chmod -R +w debian/go || true
|
||||||
|
rm -rf debian/.debhelper debian/.gocache debian/go debian/$(PROG) debian/files debian/*.substvars debian/debhelper-build-stamp
|
||||||
|
rm -f ../$(PROG)_*.deb ../$(PROG)_*.changes ../$(PROG)_*.buildinfo
|
||||||
|
|
||||||
|
# Sync version from debian/changelog to main.go
|
||||||
|
sync-version:
|
||||||
|
@echo "Syncing version from debian/changelog to main.go..."
|
||||||
|
@VERSION=$$(head -1 debian/changelog | sed -n 's/.*(\([^)]*\)).*/\1/p'); \
|
||||||
|
sed -i 's/^const Version = ".*"/const Version = "'"$$VERSION"'"/' src/main.go; \
|
||||||
|
echo "Updated Version const to: $$VERSION"
|
||||||
|
|
||||||
|
# Build Debian package
|
||||||
|
pkg-deb: sync-version
|
||||||
|
fakeroot dpkg-buildpackage -us -uc -b
|
260
README.md
260
README.md
@ -1,254 +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 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
|
## 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 → 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
|
|
||||||
|
|
||||||
```bash
|
```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
|
```bash
|
||||||
# Linux static binary
|
# Edit service configuration
|
||||||
CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o vpp-snmp-agent .
|
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 vpp-snmp-agent-linux-amd64 .
|
sudo systemctl start govpp-snmp-agentx
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -a -ldflags '-extldflags "-static"' -o vpp-snmp-agent-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 vpp-snmp-agent .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
|
||||||
./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
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project uses the LGPL 3.0 licensed go-agentx library.
|
LGPL 3.0 (due to modified go-agentx dependency)
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
|
64
debian/changelog
vendored
Normal file
64
debian/changelog
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
govpp-snmp-agentx (1.1.2-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Add startup version logging to INFO log level
|
||||||
|
* Implement automatic version synchronization between debian/changelog and main.go
|
||||||
|
* Add make sync-version target for manual version syncing
|
||||||
|
* Ensure version consistency across package and application code
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Sun, 23 Jun 2025 00:20:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.1.1-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Add IF-MIB::ifHighSpeed field (OID 1.3.6.1.2.1.31.1.1.1.15)
|
||||||
|
* Populate ifHighSpeed with interface speed in Megabits per second
|
||||||
|
* Implement conditional ifSpeed population (skip for speeds > 2.5Gbps)
|
||||||
|
* Improve SNMP compliance for high-speed interface reporting
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Sun, 23 Jun 2025 00:10:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.1.0-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Add interface event monitoring with VPP API integration
|
||||||
|
* Populate IF-MIB with real interface details (MAC address, speed, status)
|
||||||
|
* Consolidate VPP-related modules into unified vpp package
|
||||||
|
* Implement real-time interface state updates via event-driven callbacks
|
||||||
|
* Retrieve and expose actual interface properties: MTU, admin/oper status
|
||||||
|
* Add comprehensive interface details caching and management
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Sun, 23 Jun 2025 00:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.0.3-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Remove unnecessary toolchain configuration
|
||||||
|
* Simplify and shorten Makefile
|
||||||
|
* Refactor documentation structure with detailed DETAILS.md
|
||||||
|
* Update and improve README documentation
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Thu, 19 Jun 2025 00:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.0.2-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Reorganize source code into src/ subdirectory for cleaner project structure
|
||||||
|
* Add environment file support (/etc/default/govpp-snmp-agentx)
|
||||||
|
* Move service configuration to environment variables
|
||||||
|
* Improve systemd service configurability
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Tue, 17 Jun 2025 00:55:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.0.1-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Add manual page for govpp-snmp-agentx(1)
|
||||||
|
* Make VPP config file optional - log warning and continue if missing
|
||||||
|
* Fix Debian package build reproducibility issues
|
||||||
|
* Improve build system with proper cleanup targets
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Tue, 17 Jun 2025 00:35:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.0.0-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Initial release
|
||||||
|
* SNMP AgentX daemon for VPP statistics
|
||||||
|
* Interface MIB support
|
||||||
|
* Systemd service integration
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Mon, 16 Jun 2025 00:00:00 +0000
|
23
debian/control
vendored
Normal file
23
debian/control
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
Source: govpp-snmp-agentx
|
||||||
|
Section: net
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: Pim van Pelt <pim@ipng.ch>
|
||||||
|
Build-Depends: debhelper-compat (= 13), golang-go (>= 1.23.8)
|
||||||
|
Standards-Version: 4.6.2
|
||||||
|
Homepage: https://git.ipng.ch/ipng/govpp-agentx-snmp
|
||||||
|
Vcs-Git: https://git.ipng.ch/ipng/govpp-agentx-snmp
|
||||||
|
Vcs-Browser: https://git.ipng.ch/ipng/govpp-agentx-snmp
|
||||||
|
|
||||||
|
Package: govpp-snmp-agentx
|
||||||
|
Architecture: any
|
||||||
|
Depends: ${misc:Depends}, ${shlibs:Depends}, snmpd
|
||||||
|
Description: GoVPP SNMP AgentX Daemon
|
||||||
|
A SNMP AgentX daemon that provides SNMP access to VPP (Vector Packet Processing)
|
||||||
|
statistics and interface information. This daemon acts as a subagent that
|
||||||
|
connects to the main SNMP daemon via the AgentX protocol.
|
||||||
|
.
|
||||||
|
Features:
|
||||||
|
- Interface MIB support
|
||||||
|
- VPP statistics exposure
|
||||||
|
- AgentX protocol implementation
|
||||||
|
- Systemd integration
|
12
debian/govpp-snmp-agentx.postrm.debhelper
vendored
Normal file
12
debian/govpp-snmp-agentx.postrm.debhelper
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Automatically added by dh_installsystemd/13.11.4
|
||||||
|
if [ "$1" = remove ] && [ -d /run/systemd/system ] ; then
|
||||||
|
systemctl --system daemon-reload >/dev/null || true
|
||||||
|
fi
|
||||||
|
# End automatically added section
|
||||||
|
# Automatically added by dh_installsystemd/13.11.4
|
||||||
|
if [ "$1" = "purge" ]; then
|
||||||
|
if [ -x "/usr/bin/deb-systemd-helper" ]; then
|
||||||
|
deb-systemd-helper purge 'govpp-snmp-agentx.service' >/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# End automatically added section
|
23
debian/postinst
vendored
Executable file
23
debian/postinst
vendored
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
configure)
|
||||||
|
# Enable and start the service
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable govpp-snmp-agentx.service
|
||||||
|
;;
|
||||||
|
|
||||||
|
abort-upgrade|abort-remove|abort-deconfigure)
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "postinst called with unknown argument \`$1'" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
#DEBHELPER#
|
||||||
|
|
||||||
|
exit 0
|
22
debian/prerm
vendored
Executable file
22
debian/prerm
vendored
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
remove|upgrade|deconfigure)
|
||||||
|
systemctl stop govpp-snmp-agentx.service || true
|
||||||
|
systemctl disable govpp-snmp-agentx.service || true
|
||||||
|
;;
|
||||||
|
|
||||||
|
failed-upgrade)
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "prerm called with unknown argument \`$1'" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
#DEBHELPER#
|
||||||
|
|
||||||
|
exit 0
|
30
debian/rules
vendored
Executable file
30
debian/rules
vendored
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
export DH_VERBOSE = 1
|
||||||
|
export GO111MODULE = on
|
||||||
|
export GOPROXY = direct
|
||||||
|
export GOCACHE = $(CURDIR)/debian/.gocache
|
||||||
|
export GOPATH = $(CURDIR)/debian/go
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@
|
||||||
|
|
||||||
|
override_dh_auto_build:
|
||||||
|
cd src && go build -v -ldflags="-s -w" -o ../govpp-snmp-agentx .
|
||||||
|
|
||||||
|
override_dh_auto_install:
|
||||||
|
install -D -m 0755 govpp-snmp-agentx debian/govpp-snmp-agentx/usr/sbin/govpp-snmp-agentx
|
||||||
|
install -D -m 0644 govpp-snmp-agentx.service debian/govpp-snmp-agentx/lib/systemd/system/govpp-snmp-agentx.service
|
||||||
|
install -D -m 0644 govpp-snmp-agentx.default debian/govpp-snmp-agentx/etc/default/govpp-snmp-agentx
|
||||||
|
install -D -m 0644 docs/govpp-snmp-agentx.1 debian/govpp-snmp-agentx/usr/share/man/man1/govpp-snmp-agentx.1
|
||||||
|
|
||||||
|
override_dh_auto_configure:
|
||||||
|
# Skip auto configure
|
||||||
|
|
||||||
|
override_dh_auto_test:
|
||||||
|
# Skip tests during packaging
|
||||||
|
|
||||||
|
override_dh_auto_clean:
|
||||||
|
rm -f govpp-snmp-agentx
|
||||||
|
[ -d debian/go ] && chmod -R +w debian/go || true
|
||||||
|
rm -rf debian/.gocache debian/go obj-*
|
322
docs/DETAILS.md
Normal file
322
docs/DETAILS.md
Normal 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
|
72
docs/govpp-snmp-agentx.1
Normal file
72
docs/govpp-snmp-agentx.1
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
.TH GOVPP-SNMP-AGENTX 1 "June 2025" "version 1.0.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
govpp-snmp-agentx \- VPP SNMP AgentX daemon for interface statistics
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B govpp-snmp-agentx
|
||||||
|
[\fIOPTION\fR]...
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B govpp-snmp-agentx
|
||||||
|
is an SNMP AgentX subagent that provides SNMP access to VPP (Vector Packet Processing) interface statistics and information. It connects to a master SNMP daemon via the AgentX protocol and populates standard IF-MIB tables with real-time VPP interface data.
|
||||||
|
.PP
|
||||||
|
The daemon implements two MIB tables:
|
||||||
|
.TP
|
||||||
|
.B ifEntry
|
||||||
|
Classic interface table (1.3.6.1.2.1.2.2.1) with basic interface statistics including counters for packets, bytes, errors, and discards.
|
||||||
|
.TP
|
||||||
|
.B ifXTable
|
||||||
|
Extended interface table (1.3.6.1.2.1.31.1.1.1) with high-capacity 64-bit counters and additional interface information.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BR \-agentx.addr " " \fIADDRESS\fR
|
||||||
|
AgentX master agent address. Can be a TCP address (hostname:port) or Unix socket path.
|
||||||
|
Default: localhost:705
|
||||||
|
.TP
|
||||||
|
.BR \-debug
|
||||||
|
Enable debug logging to show detailed operational information.
|
||||||
|
.TP
|
||||||
|
.BR \-vppcfg " " \fIFILE\fR
|
||||||
|
VPP configuration YAML file to read interface descriptions from.
|
||||||
|
.TP
|
||||||
|
.BR \-vppstats.api.addr " " \fIPATH\fR
|
||||||
|
VPP API socket path for interface enumeration.
|
||||||
|
Default: /var/run/vpp/api.sock
|
||||||
|
.TP
|
||||||
|
.BR \-vppstats.stats.addr " " \fIPATH\fR
|
||||||
|
VPP statistics socket path for interface counters.
|
||||||
|
Default: /var/run/vpp/stats.sock
|
||||||
|
.TP
|
||||||
|
.BR \-vppstats.ifindex-offset " " \fINUMBER\fR
|
||||||
|
Offset added to VPP interface indices for SNMP interface numbering.
|
||||||
|
Default: 1000
|
||||||
|
.TP
|
||||||
|
.BR \-vppstats.period " " \fISECONDS\fR
|
||||||
|
Interval in seconds for querying VPP interface statistics.
|
||||||
|
Default: 10
|
||||||
|
.SH EXAMPLES
|
||||||
|
.TP
|
||||||
|
Connect to AgentX master via TCP:
|
||||||
|
.B govpp-snmp-agentx -agentx.addr snmp.example.com:705
|
||||||
|
.TP
|
||||||
|
Connect via Unix socket with debug logging:
|
||||||
|
.B govpp-snmp-agentx -agentx.addr /var/agentx/master -debug
|
||||||
|
.TP
|
||||||
|
Use custom VPP sockets and config:
|
||||||
|
.B govpp-snmp-agentx -vppstats.api.addr /opt/vpp/api.sock -vppcfg /etc/vpp/vppcfg.yaml
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.I /var/run/vpp/api.sock
|
||||||
|
Default VPP API socket
|
||||||
|
.TP
|
||||||
|
.I /var/run/vpp/stats.sock
|
||||||
|
Default VPP statistics socket
|
||||||
|
.TP
|
||||||
|
.I /var/agentx/master
|
||||||
|
Common AgentX Unix socket path
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR snmpd (8),
|
||||||
|
.BR snmpwalk (1),
|
||||||
|
.BR vpp (8)
|
||||||
|
.SH AUTHOR
|
||||||
|
Pim van Pelt <pim@ipng.ch>
|
||||||
|
.SH COPYRIGHT
|
||||||
|
Copyright 2025, IPng Networks GmbH
|
15
govpp-snmp-agentx.default
Normal file
15
govpp-snmp-agentx.default
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Default configuration for govpp-snmp-agentx
|
||||||
|
# This file contains environment variables for the GoVPP SNMP AgentX daemon
|
||||||
|
#
|
||||||
|
# Command line flags for govpp-snmp-agentx
|
||||||
|
GOVPP_SNMP_AGENTX_FLAGS="-agentx.addr /var/agentx/master -vppcfg /etc/vpp/vppcfg.yaml -vppstats.period 10"
|
||||||
|
|
||||||
|
# Additional options that can be added to GOVPP_SNMP_AGENTX_FLAGS:
|
||||||
|
# -debug Enable debug logging
|
||||||
|
# -agentx.addr host:port SNMPd Agentx address (example: localhost:705)
|
||||||
|
# -vppstats.api.addr PATH VPP API socket path (default: /var/run/vpp/api.sock)
|
||||||
|
# -vppstats.stats.addr PATH VPP stats socket path (default: /var/run/vpp/stats.sock)
|
||||||
|
# -vppstats.ifindex-offset NUM Interface index offset (default: 1000)
|
||||||
|
#
|
||||||
|
# Example with debug logging:
|
||||||
|
# GOVPP_SNMP_AGENTX_FLAGS="-agentx.addr localhost:705 -vppcfg /etc/vpp/vppcfg.yaml -vppstats.period 10 -debug"
|
18
govpp-snmp-agentx.service
Normal file
18
govpp-snmp-agentx.service
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=GoVPP SNMP AgentX Daemon
|
||||||
|
After=network.target vpp.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
EnvironmentFile=-/etc/default/govpp-snmp-agentx
|
||||||
|
ExecStartPre=-+/usr/bin/chmod 770 /var/agentx /var/agentx/master
|
||||||
|
ExecStartPre=-+/usr/bin/chown Debian-snmp:vpp /var/agentx /var/agentx/master
|
||||||
|
ExecStart=/usr/sbin/govpp-snmp-agentx $GOVPP_SNMP_AGENTX_FLAGS
|
||||||
|
User=Debian-snmp
|
||||||
|
Group=vpp
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
@ -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
54
main.go
@ -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
48
src/agentx/agentx.go
Normal 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
54
src/agentx/agentx_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
package config
|
package config
|
||||||
|
|
||||||
// Global configuration variables
|
// Global configuration variables
|
30
src/config/config_test.go
Normal file
30
src/config/config_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,17 @@
|
|||||||
module govpp-snmp-example
|
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
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff // 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/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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 h1:ewr1srjRCmcQogPQ/NCx6XCk6LGVmsVCc9Y3vvPZj+Y=
|
||||||
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
|
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
|
||||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
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 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
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 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@ -1,7 +1,10 @@
|
|||||||
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
package ifmib
|
package ifmib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -9,9 +12,10 @@ import (
|
|||||||
"github.com/posteo/go-agentx/pdu"
|
"github.com/posteo/go-agentx/pdu"
|
||||||
"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"
|
||||||
|
|
||||||
"govpp-snmp-example/logger"
|
"govpp-snmp-agentx/logger"
|
||||||
"govpp-snmp-example/vppstats"
|
"govpp-snmp-agentx/vpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IF-MIB OID bases:
|
// IF-MIB OID bases:
|
||||||
@ -56,22 +60,39 @@ 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
|
||||||
|
|
||||||
const ifEntryOID = "1.3.6.1.2.1.2.2.1"
|
const ifEntryOID = "1.3.6.1.2.1.2.2.1"
|
||||||
const ifXTableOID = "1.3.6.1.2.1.31.1.1.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 {
|
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
|
||||||
|
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),
|
||||||
|
interfaceDetails: make(map[uint32]*vpp.InterfaceDetails),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +100,67 @@ func (m *InterfaceMIB) GetHandler() *agentx.ListHandler {
|
|||||||
return m.handler
|
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) 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()
|
||||||
@ -109,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)
|
||||||
@ -123,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
|
||||||
@ -138,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))
|
||||||
@ -298,6 +416,26 @@ func (m *InterfaceMIB) addIfXTable(iface *api.InterfaceCounters, idx int) {
|
|||||||
item = m.handler.Add(fmt.Sprintf("%s.13.%d", ifXTableOID, idx))
|
item = m.handler.Add(fmt.Sprintf("%s.13.%d", ifXTableOID, idx))
|
||||||
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
|
||||||
|
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 {
|
func (m *InterfaceMIB) RegisterWithClient(client *agentx.Client) error {
|
180
src/ifmib/ifmib_test.go
Normal file
180
src/ifmib/ifmib_test.go
Normal 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
52
src/logger/logger.go
Normal 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
113
src/logger/logger_test.go
Normal 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()
|
||||||
|
}
|
64
src/main.go
Normal file
64
src/main.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// 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/vpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const Version = "1.1.2-1"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// Log startup message with version
|
||||||
|
logger.Printf("Starting govpp-snmp-agentx version %s", Version)
|
||||||
|
|
||||||
|
// Create the interface MIB
|
||||||
|
interfaceMIB := ifmib.NewInterfaceMIB()
|
||||||
|
|
||||||
|
// Load VPP config if specified
|
||||||
|
if *vppcfg != "" {
|
||||||
|
if err := interfaceMIB.LoadVPPConfig(*vppcfg); err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up interface event callback to update interface details
|
||||||
|
vpp.SetInterfaceEventCallback(interfaceMIB.UpdateInterfaceDetails)
|
||||||
|
|
||||||
|
// Start VPP stats routine with callback to update MIB
|
||||||
|
vpp.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
20
src/main_test.go
Normal 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")
|
||||||
|
}
|
145
src/vpp/vpp_iface.go
Normal file
145
src/vpp/vpp_iface.go
Normal 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
|
||||||
|
}
|
267
src/vpp/vpp_stats.go
Normal file
267
src/vpp/vpp_stats.go
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
package vpp
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Global callback for interface events
|
||||||
|
var interfaceEventCallback InterfaceEventCallback
|
||||||
|
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if connected {
|
||||||
|
if !queryInterfaceStats(conn, statsConn, callback) {
|
||||||
|
connected = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for next tick
|
||||||
|
<-ticker.C
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryInterfaceStats(conn *core.Connection, statsConn *core.StatsConnection, callback StatsCallback) bool {
|
||||||
|
// Check VPP liveness using API call
|
||||||
|
if !checkVPPLiveness(conn) {
|
||||||
|
logger.Printf("VPP liveness check failed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the proper struct for interface stats
|
||||||
|
stats := new(api.InterfaceStats)
|
||||||
|
|
||||||
|
// Use the GetInterfaceStats method - this is the correct approach
|
||||||
|
if err := statsConn.GetInterfaceStats(stats); err != nil {
|
||||||
|
logger.Printf("Failed to get interface stats: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always log basic info
|
||||||
|
logger.Printf("Retrieved stats for %d interfaces", len(stats.Interfaces))
|
||||||
|
|
||||||
|
// Debug logging for individual interfaces
|
||||||
|
for _, iface := range stats.Interfaces {
|
||||||
|
logger.Debugf("Interface %d (%s): RX %d pkts/%d bytes, TX %d pkts/%d bytes",
|
||||||
|
iface.InterfaceIndex, iface.InterfaceName,
|
||||||
|
iface.Rx.Packets, iface.Rx.Bytes,
|
||||||
|
iface.Tx.Packets, iface.Tx.Bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the callback to update the MIB
|
||||||
|
if callback != nil {
|
||||||
|
callback(stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkVPPLiveness(conn *core.Connection) bool {
|
||||||
|
// Create a channel for the API call
|
||||||
|
ch, err := conn.NewAPIChannel()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("Failed to create API channel: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a flag to track if channel was closed successfully
|
||||||
|
var channelClosed bool
|
||||||
|
defer func() {
|
||||||
|
if !channelClosed {
|
||||||
|
// Recover from potential panic when closing already closed channel
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Debugf("Recovered from channel close panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ch.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create ShowVersion request
|
||||||
|
req := &vpe.ShowVersion{}
|
||||||
|
reply := &vpe.ShowVersionReply{}
|
||||||
|
|
||||||
|
// Send the request with timeout
|
||||||
|
if err := ch.SendRequest(req).ReceiveReply(reply); err != nil {
|
||||||
|
logger.Debugf("VPP ShowVersion failed: %v", err)
|
||||||
|
// Try to close the channel properly on error
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Debugf("Channel already closed during error handling")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ch.Close()
|
||||||
|
channelClosed = true
|
||||||
|
}()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close channel successfully
|
||||||
|
ch.Close()
|
||||||
|
channelClosed = true
|
||||||
|
|
||||||
|
// If we got here, VPP is responsive
|
||||||
|
logger.Debugf("VPP liveness check passed (version: %s)", string(reply.Version))
|
||||||
|
return true
|
||||||
|
}
|
@ -1,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)
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user