Compare commits
53 Commits
069b1b6fc2
...
v1.2.3-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06a1f4401d | ||
|
|
b450e02b8d | ||
|
|
5e36d5c926 | ||
|
|
16bebb0ece | ||
|
|
cdc8765a9e | ||
|
|
9596e16887 | ||
|
|
4935f5a8ef | ||
|
|
b6d2e3b629 | ||
|
|
a0d5c61643 | ||
|
|
27c7a5bcae | ||
|
|
9e9103c184 | ||
|
|
b358cc2443 | ||
|
|
ead795674c | ||
|
|
dce4750b0f | ||
|
|
d65e055710 | ||
|
|
8ed14834f5 | ||
|
|
3401c96112 | ||
|
|
1889934a9c | ||
|
|
e93156324d | ||
|
|
bdaa2e366b | ||
|
|
96b9dd501d | ||
|
|
70cb134dcf | ||
|
|
15216782d1 | ||
|
|
067e324cca | ||
|
|
0d19d50d62 | ||
|
|
686bbe46b0 | ||
|
|
ccc2b5ad4d | ||
|
|
4f368e625d | ||
|
|
35165b0464 | ||
|
|
42dbbded3d | ||
|
|
f16a2b41ea | ||
|
|
5533ab00de | ||
|
|
1cbca296c4 | ||
|
|
a73c7cbf91 | ||
|
|
1ddc77ec73 | ||
|
|
6063db7311 | ||
|
|
7f81b51c1f | ||
|
|
c0bcdd5449 | ||
|
|
fa437ddaf1 | ||
|
|
0b4ff36130 | ||
|
|
82db92f344 | ||
|
|
adf033318a | ||
|
|
6969e609c0 | ||
|
|
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
|
||||||
|
|||||||
@@ -1,33 +1,7 @@
|
|||||||
All files in this repository are licensed as follows. If you contribute
|
|
||||||
to this repository, it is assumed that you license your contribution
|
|
||||||
under the same license unless you state otherwise.
|
|
||||||
|
|
||||||
All files Copyright (C) 2015 Canonical Ltd. unless otherwise specified in the file.
|
|
||||||
|
|
||||||
This software is licensed under the LGPLv3, included below.
|
|
||||||
|
|
||||||
As a special exception to the GNU Lesser General Public License version 3
|
|
||||||
("LGPL3"), the copyright holders of this Library give you permission to
|
|
||||||
convey to a third party a Combined Work that links statically or dynamically
|
|
||||||
to this Library without providing any Minimal Corresponding Source or
|
|
||||||
Minimal Application Code as set out in 4d or providing the installation
|
|
||||||
information set out in section 4e, provided that you comply with the other
|
|
||||||
provisions of LGPL3 and provided that you meet, for the Application the
|
|
||||||
terms and conditions of the license(s) which apply to the Application.
|
|
||||||
|
|
||||||
Except as stated in this special exception, the provisions of LGPL3 will
|
|
||||||
continue to comply in full to this Library. If you modify this Library, you
|
|
||||||
may apply this exception to your version of this Library, but you are not
|
|
||||||
obliged to do so. If you do not wish to do so, delete this exception
|
|
||||||
statement from your version. This exception does not (and cannot) modify any
|
|
||||||
license terms which apply to the Application, with which you must still
|
|
||||||
comply.
|
|
||||||
|
|
||||||
|
|
||||||
GNU LESSER GENERAL PUBLIC LICENSE
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
// Global configuration variables
|
|
||||||
var (
|
|
||||||
Debug bool
|
|
||||||
)
|
|
||||||
154
debian/changelog
vendored
Normal file
154
debian/changelog
vendored
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
govpp-snmp-agentx (1.2.3-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Fix VPP stats filtering to exclude deleted interfaces
|
||||||
|
* Add interface manager integration to stats manager
|
||||||
|
* Filter out stale interface statistics from deleted interfaces
|
||||||
|
* Improve stats reliability by cross-referencing with current interface list
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Sat, 23 Nov 2024 00:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.2.2-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Simplify ifmib integration by removing reflection usage
|
||||||
|
* Move to direct counter updates for improved performance
|
||||||
|
* Refactor interface tracking with simplified state management
|
||||||
|
* Remove dependency on reflection for handler manipulation
|
||||||
|
* Fix test suite to match refactored ifmib implementation
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Sat, 23 Nov 2024 00:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.2.1-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Fix OID visibility bug after go-agentx 0.3.0 upgrade
|
||||||
|
* Use reflection to clear handler contents instead of creating new handlers
|
||||||
|
* Simplify AgentX session creation and registration logic
|
||||||
|
* Add proper session cleanup method (Close())
|
||||||
|
* Improve error handling in AgentX interactions
|
||||||
|
* Code cleanup and maintainability improvements
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Fri, 22 Nov 2024 00:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.2.0-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Update go-agentx dependency from 0.2.1 to 0.3.0 to fix compilation issues
|
||||||
|
* Adapt to breaking changes in go-agentx Session API (now requires nameOID, name, handler)
|
||||||
|
* Update Client configuration to use dial options (WithTimeout, WithReconnectInterval)
|
||||||
|
* Remove access to unexported Session.Handler field (API change)
|
||||||
|
* NOTE: This version fixes compilation broken in 1.1.6-1 and 1.1.7-1
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Wed, 20 Nov 2024 00:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.1.7-1) bookworm; urgency=critical
|
||||||
|
|
||||||
|
* Refactor VPPClient constructor to use idiomatic Go patterns
|
||||||
|
* Remove redundant NewVPPClient() constructor function
|
||||||
|
* Update all code to use direct struct initialization (&VPPClient{})
|
||||||
|
* Improve code maintainability and follow Go best practices
|
||||||
|
* WARNING: This version is BROKEN due to go-agentx 0.2.1 incompatibility
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Fri, 15 Nov 2024 00:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.1.6-1) bookworm; urgency=critical
|
||||||
|
|
||||||
|
* Replace forked go-agentx dependency with upstream github.com/posteo/go-agentx
|
||||||
|
* Remove local src/go-agentx/ directory and use official upstream package
|
||||||
|
* Upgrade to go-agentx v0.2.1 from official GitHub repository
|
||||||
|
* Improve dependency management and reduce maintenance overhead
|
||||||
|
* WARNING: This version is BROKEN due to go-agentx 0.2.1 incompatibility
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Fri, 08 Nov 2024 00:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.1.5-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Implement automatic interface deletion handling in IF-MIB
|
||||||
|
* Simplify interface event processing by removing separate delete callbacks
|
||||||
|
* Remove unused functions and clean up codebase (RemoveInterface, rebuildMIB)
|
||||||
|
* Improve interface state synchronization between VPP and SNMP MIB
|
||||||
|
* Automatically detect and remove deleted interfaces during updates
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Wed, 02 Jul 2025 00:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.1.4-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Major VPP module refactoring with improved separation of concerns
|
||||||
|
* Replace legacy global functions with structured VPPClient, InterfaceManager, and StatsManager
|
||||||
|
* Fix stats polling timing bug - now properly respects vppstats.period setting
|
||||||
|
* Add comprehensive test suite with 64.6% code coverage
|
||||||
|
* Improve connection management and error handling
|
||||||
|
* Remove legacy compatibility functions for cleaner API
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Mon, 24 Jun 2025 01:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.1.3-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Use fallback packet counters when VPP unicast stats are unavailable
|
||||||
|
* Fix unicast packet reporting for interfaces without detailed stats collection
|
||||||
|
* Add VPP configuration comments for stats-collect feature requirements
|
||||||
|
* Improve packet counter accuracy across different VPP configurations
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Mon, 24 Jun 2025 00:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.1.2-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Add startup version logging to INFO log level
|
||||||
|
* Implement automatic version synchronization between debian/changelog and main.go
|
||||||
|
* Add make sync-version target for manual version syncing
|
||||||
|
* Ensure version consistency across package and application code
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Sun, 23 Jun 2025 00:20:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.1.1-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Add IF-MIB::ifHighSpeed field (OID 1.3.6.1.2.1.31.1.1.1.15)
|
||||||
|
* Populate ifHighSpeed with interface speed in Megabits per second
|
||||||
|
* Implement conditional ifSpeed population (skip for speeds > 2.5Gbps)
|
||||||
|
* Improve SNMP compliance for high-speed interface reporting
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Sun, 23 Jun 2025 00:10:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.1.0-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Add interface event monitoring with VPP API integration
|
||||||
|
* Populate IF-MIB with real interface details (MAC address, speed, status)
|
||||||
|
* Consolidate VPP-related modules into unified vpp package
|
||||||
|
* Implement real-time interface state updates via event-driven callbacks
|
||||||
|
* Retrieve and expose actual interface properties: MTU, admin/oper status
|
||||||
|
* Add comprehensive interface details caching and management
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Sun, 23 Jun 2025 00:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.0.3-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Remove unnecessary toolchain configuration
|
||||||
|
* Simplify and shorten Makefile
|
||||||
|
* Refactor documentation structure with detailed DETAILS.md
|
||||||
|
* Update and improve README documentation
|
||||||
|
|
||||||
|
-- Pim van Pelt <pim@ipng.ch> Thu, 19 Jun 2025 00:00:00 +0000
|
||||||
|
|
||||||
|
govpp-snmp-agentx (1.0.2-1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* 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.24.2
|
||||||
|
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.24.2
|
||||||
|
if [ "$1" = "purge" ]; then
|
||||||
|
if [ -x "/usr/bin/deb-systemd-helper" ]; then
|
||||||
|
deb-systemd-helper purge 'govpp-snmp-agentx.service' >/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# End automatically added section
|
||||||
23
debian/postinst
vendored
Executable file
23
debian/postinst
vendored
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
configure)
|
||||||
|
# Enable and start the service
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable govpp-snmp-agentx.service
|
||||||
|
;;
|
||||||
|
|
||||||
|
abort-upgrade|abort-remove|abort-deconfigure)
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "postinst called with unknown argument \`$1'" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
#DEBHELPER#
|
||||||
|
|
||||||
|
exit 0
|
||||||
22
debian/prerm
vendored
Executable file
22
debian/prerm
vendored
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
remove|upgrade|deconfigure)
|
||||||
|
systemctl stop govpp-snmp-agentx.service || true
|
||||||
|
systemctl disable govpp-snmp-agentx.service || true
|
||||||
|
;;
|
||||||
|
|
||||||
|
failed-upgrade)
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "prerm called with unknown argument \`$1'" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
#DEBHELPER#
|
||||||
|
|
||||||
|
exit 0
|
||||||
30
debian/rules
vendored
Executable file
30
debian/rules
vendored
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
export DH_VERBOSE = 1
|
||||||
|
export GO111MODULE = on
|
||||||
|
export GOPROXY = direct
|
||||||
|
export GOCACHE = $(CURDIR)/debian/.gocache
|
||||||
|
export GOPATH = $(CURDIR)/debian/go
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@
|
||||||
|
|
||||||
|
override_dh_auto_build:
|
||||||
|
cd src && go build -v -ldflags="-s -w" -o ../govpp-snmp-agentx .
|
||||||
|
|
||||||
|
override_dh_auto_install:
|
||||||
|
install -D -m 0755 govpp-snmp-agentx debian/govpp-snmp-agentx/usr/sbin/govpp-snmp-agentx
|
||||||
|
install -D -m 0644 govpp-snmp-agentx.service debian/govpp-snmp-agentx/lib/systemd/system/govpp-snmp-agentx.service
|
||||||
|
install -D -m 0644 govpp-snmp-agentx.default debian/govpp-snmp-agentx/etc/default/govpp-snmp-agentx
|
||||||
|
install -D -m 0644 docs/govpp-snmp-agentx.1 debian/govpp-snmp-agentx/usr/share/man/man1/govpp-snmp-agentx.1
|
||||||
|
|
||||||
|
override_dh_auto_configure:
|
||||||
|
# Skip auto configure
|
||||||
|
|
||||||
|
override_dh_auto_test:
|
||||||
|
# Skip tests during packaging
|
||||||
|
|
||||||
|
override_dh_auto_clean:
|
||||||
|
rm -f govpp-snmp-agentx
|
||||||
|
[ -d debian/go ] && chmod -R +w debian/go || true
|
||||||
|
rm -rf debian/.gocache debian/go obj-*
|
||||||
342
docs/DETAILS.md
Normal file
342
docs/DETAILS.md
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
# VPP SNMP AgentX Bridge - Detailed Documentation
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Real-time VPP interface statistics** via SNMP
|
||||||
|
- **Standard IF-MIB compliance** (ifXTable)
|
||||||
|
- **AgentX protocol support** (TCP and Unix sockets)
|
||||||
|
- **Configurable interface index offset** to avoid conflicts
|
||||||
|
- **Configurable polling intervals**
|
||||||
|
- **Thread-safe operation** with proper synchronization
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
VPP Stats Socket → VPP Stats Client → Interface MIB → AgentX → SNMPd
|
||||||
|
```
|
||||||
|
|
||||||
|
The application consists of four main components:
|
||||||
|
|
||||||
|
1. **VPP Stats Client** (`src/vpp/`): Connects to VPP stats socket and retrieves interface counters
|
||||||
|
2. **Interface MIB** (`src/ifmib/`): Maps VPP statistics to SNMP IF-MIB structure
|
||||||
|
3. **AgentX Client** (`src/agentx/`): Handles AgentX protocol connection and MIB registration
|
||||||
|
4. **Main Application** (`src/main.go`): Orchestrates the components and handles configuration
|
||||||
|
|
||||||
|
## Command Line Flags
|
||||||
|
|
||||||
|
### General Application Flags
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `-debug` | `false` | Enable debug logging |
|
||||||
|
| `-vppcfg` | `""` | VPP configuration YAML file to read interface descriptions from |
|
||||||
|
|
||||||
|
### AgentX Module Flags
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `-agentx.addr` | `localhost:705` | AgentX master agent address (hostname:port or Unix socket path) |
|
||||||
|
|
||||||
|
### VPP Statistics Module Flags
|
||||||
|
|
||||||
|
| Flag | Default | Description |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| `-vppstats.api.addr` | `/var/run/vpp/api.sock` | VPP API socket path |
|
||||||
|
| `-vppstats.stats.addr` | `/var/run/vpp/stats.sock` | VPP statistics socket path |
|
||||||
|
| `-vppstats.period` | `10` | Interval in seconds for querying VPP interface stats |
|
||||||
|
| `-vppstats.ifindex-offset` | `1000` | Offset to add to VPP interface indices for SNMP |
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable debug logging
|
||||||
|
./govpp-snmp-agentx -debug
|
||||||
|
|
||||||
|
# Custom polling interval (5 seconds)
|
||||||
|
./govpp-snmp-agentx -vppstats.period 5
|
||||||
|
|
||||||
|
# Custom VPP stats socket
|
||||||
|
./govpp-snmp-agentx -vppstats.stats.addr /custom/path/stats.sock
|
||||||
|
|
||||||
|
# Custom VPP API socket
|
||||||
|
./govpp-snmp-agentx -vppstats.api.addr /custom/path/api.sock
|
||||||
|
|
||||||
|
# Custom interface index offset (start at 2000)
|
||||||
|
./govpp-snmp-agentx -vppstats.ifindex-offset 2000
|
||||||
|
|
||||||
|
# With VPP configuration file for interface descriptions
|
||||||
|
./govpp-snmp-agentx -vppcfg /etc/vpp/vppcfg.yaml
|
||||||
|
|
||||||
|
# Full configuration
|
||||||
|
./govpp-snmp-agentx \
|
||||||
|
-agentx.addr /var/agentx/master \
|
||||||
|
-debug \
|
||||||
|
-vppcfg /etc/vpp/vppcfg.yaml \
|
||||||
|
-vppstats.api.addr /var/run/vpp/api.sock \
|
||||||
|
-vppstats.stats.addr /var/run/vpp/stats.sock \
|
||||||
|
-vppstats.period 5 \
|
||||||
|
-vppstats.ifindex-offset 1000
|
||||||
|
```
|
||||||
|
|
||||||
|
## VPP Configuration File
|
||||||
|
|
||||||
|
The `-vppcfg` flag accepts a YAML configuration file that describes VPP interfaces and their descriptions. This file is used to populate the `ifAlias` (.18) field in the ifXTable with meaningful interface descriptions.
|
||||||
|
|
||||||
|
### YAML Format Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
interfaces:
|
||||||
|
GigabitEthernet82/0/0:
|
||||||
|
description: 'Infra: Management interface'
|
||||||
|
TenGigabitEthernet1/0/2:
|
||||||
|
description: 'Infra: Core uplink'
|
||||||
|
sub-interfaces:
|
||||||
|
100:
|
||||||
|
description: 'Cust: Customer VLAN 100'
|
||||||
|
200:
|
||||||
|
description: 'Transit: Provider VLAN 200'
|
||||||
|
loopbacks:
|
||||||
|
loop0:
|
||||||
|
description: 'Core: Router loopback'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Description Mapping
|
||||||
|
|
||||||
|
- **Main interfaces**: Use the `description` field directly
|
||||||
|
- **Sub-interfaces**: Use the `description` field from the `sub-interfaces` section
|
||||||
|
- **Loopbacks**: Use the `description` field from the `loopbacks` section
|
||||||
|
- **Fallback**: If no description is found, the interface name is used as the alias
|
||||||
|
|
||||||
|
## SNMP Interface Mapping
|
||||||
|
|
||||||
|
VPP interfaces are mapped to SNMP indices with a configurable offset (default 1000):
|
||||||
|
|
||||||
|
- **VPP Interface 0** → **SNMP Index 1000**
|
||||||
|
- **VPP Interface 1** → **SNMP Index 1001**
|
||||||
|
- **VPP Interface N** → **SNMP Index (N + offset)**
|
||||||
|
|
||||||
|
## Supported MIB Objects
|
||||||
|
|
||||||
|
The application implements the ifXTable (1.3.6.1.2.1.31.1.1.1) with the following objects:
|
||||||
|
|
||||||
|
| OID | Object | Type | Description |
|
||||||
|
|-----|--------|------|-------------|
|
||||||
|
| `.1.{index}` | ifName | DisplayString | Interface name |
|
||||||
|
| `.2.{index}` | ifInMulticastPkts | Counter32 | RX multicast packets |
|
||||||
|
| `.3.{index}` | ifInBroadcastPkts | Counter32 | RX broadcast packets |
|
||||||
|
| `.4.{index}` | ifOutMulticastPkts | Counter32 | TX multicast packets |
|
||||||
|
| `.5.{index}` | ifOutBroadcastPkts | Counter32 | TX broadcast packets |
|
||||||
|
| `.6.{index}` | ifHCInOctets | Counter64 | RX bytes (high capacity) |
|
||||||
|
| `.7.{index}` | ifHCInUcastPkts | Counter64 | RX unicast packets (high capacity) |
|
||||||
|
| `.8.{index}` | ifHCInMulticastPkts | Counter64 | RX multicast packets (high capacity) |
|
||||||
|
| `.9.{index}` | ifHCInBroadcastPkts | Counter64 | RX broadcast packets (high capacity) |
|
||||||
|
| `.10.{index}` | ifHCOutOctets | Counter64 | TX bytes (high capacity) |
|
||||||
|
| `.11.{index}` | ifHCOutUcastPkts | Counter64 | TX unicast packets (high capacity) |
|
||||||
|
| `.12.{index}` | ifHCOutMulticastPkts | Counter64 | TX multicast packets (high capacity) |
|
||||||
|
| `.13.{index}` | ifHCOutBroadcastPkts | Counter64 | TX broadcast packets (high capacity) |
|
||||||
|
| `.18.{index}` | ifAlias | DisplayString | Interface description/alias (from VPP config or interface name) |
|
||||||
|
|
||||||
|
## SNMP Query Examples
|
||||||
|
|
||||||
|
### Query Interface Names
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get all interface names
|
||||||
|
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.1
|
||||||
|
|
||||||
|
# Get specific interface name (interface 0 with default offset)
|
||||||
|
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.1.1000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Interface Descriptions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get all interface descriptions/aliases
|
||||||
|
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.18
|
||||||
|
|
||||||
|
# Get specific interface description (interface 0 with default offset)
|
||||||
|
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.18.1000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Interface Counters
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get RX bytes for interface 0
|
||||||
|
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.6.1000
|
||||||
|
|
||||||
|
# Get TX bytes for interface 1
|
||||||
|
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.10.1001
|
||||||
|
|
||||||
|
# Walk all interface counters
|
||||||
|
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query with Custom Offset
|
||||||
|
|
||||||
|
If running with `-vppstats.ifindex-offset 2000`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interface 0 counters at index 2000
|
||||||
|
snmpget -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1.6.2000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### VPP Requirements
|
||||||
|
|
||||||
|
- VPP must be running with stats socket enabled
|
||||||
|
- Stats socket accessible at `/var/run/vpp/stats.sock` (or custom path)
|
||||||
|
- Application must have read permissions on the stats socket
|
||||||
|
|
||||||
|
### VPP Packet Counter Configuration
|
||||||
|
|
||||||
|
For accurate unicast, multicast, and broadcast packet counters, VPP requires specific feature arc configurations:
|
||||||
|
|
||||||
|
#### Receive Packet Counters
|
||||||
|
To enable detailed RX packet counters (RxUnicast, RxMulticast, RxBroadcast), configure:
|
||||||
|
```
|
||||||
|
set interface feature <interface> stats-collect-rx arc device-input
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Transmit Packet Counters
|
||||||
|
To enable detailed TX packet counters (TxUnicast, TxMulticast, TxBroadcast), configure:
|
||||||
|
```
|
||||||
|
set interface feature <interface> stats-collect-tx arc interface-output
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fallback Behavior
|
||||||
|
If these features are not enabled, the detailed packet counters will be zero. The SNMP agent automatically falls back to using the total packet counters (Rx.Packets and Tx.Packets) for unicast packet reporting to maintain SNMP compatibility.
|
||||||
|
|
||||||
|
**Example Configuration:**
|
||||||
|
```bash
|
||||||
|
# Enable detailed packet counters for GigabitEthernet0/8/0
|
||||||
|
vppctl set interface feature GigabitEthernet0/8/0 stats-collect-rx arc device-input
|
||||||
|
vppctl set interface feature GigabitEthernet0/8/0 stats-collect-tx arc interface-output
|
||||||
|
```
|
||||||
|
|
||||||
|
### SNMP Requirements
|
||||||
|
|
||||||
|
- SNMP master agent running (net-snmp's snmpd)
|
||||||
|
- AgentX protocol enabled in snmpd configuration
|
||||||
|
- AgentX socket accessible (TCP port 705 or Unix socket)
|
||||||
|
|
||||||
|
### SNMP Agent Configuration
|
||||||
|
|
||||||
|
Add to `/etc/snmp/snmpd.conf`:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Enable AgentX
|
||||||
|
master agentx
|
||||||
|
agentXSocket tcp:localhost:705
|
||||||
|
# or for Unix socket:
|
||||||
|
# agentXSocket /var/agentx/master
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **"Failed to connect to VPP stats"**
|
||||||
|
- Check VPP is running: `vppctl show version`
|
||||||
|
- Verify stats socket exists: `ls -la /var/run/vpp/stats.sock`
|
||||||
|
- Check permissions on stats socket
|
||||||
|
|
||||||
|
2. **"Failed to dial AgentX"**
|
||||||
|
- Verify SNMP agent is running: `systemctl status snmpd`
|
||||||
|
- Check AgentX is enabled in snmpd.conf
|
||||||
|
- Test AgentX socket: `netstat -ln | grep 705`
|
||||||
|
|
||||||
|
3. **"Failed to register IF-MIB: ErrorDuplicateRegistration"**
|
||||||
|
- Another agent is already serving the IF-MIB
|
||||||
|
- Check with: `snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1`
|
||||||
|
|
||||||
|
4. **"Retrieved stats for 0 interfaces"**
|
||||||
|
- VPP may have no interfaces configured
|
||||||
|
- Check VPP interfaces: `vppctl show interface`
|
||||||
|
|
||||||
|
### Debug Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check VPP interfaces
|
||||||
|
vppctl show interface
|
||||||
|
|
||||||
|
# Check VPP stats
|
||||||
|
vppctl show stats
|
||||||
|
|
||||||
|
# Test SNMP master agent
|
||||||
|
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.1
|
||||||
|
|
||||||
|
# Check AgentX registration
|
||||||
|
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building and Releasing
|
||||||
|
|
||||||
|
### Build Targets
|
||||||
|
|
||||||
|
The project uses a Makefile with the following targets:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the binary
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
make clean
|
||||||
|
|
||||||
|
# Sync version from debian/changelog to main.go
|
||||||
|
make sync-version
|
||||||
|
|
||||||
|
# Build Debian package (includes automatic version sync)
|
||||||
|
make pkg-deb
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release Process
|
||||||
|
|
||||||
|
To cut a new release, follow these steps in order:
|
||||||
|
|
||||||
|
1. **Update debian/changelog** with the new version and changelog entries:
|
||||||
|
```bash
|
||||||
|
# Edit debian/changelog manually
|
||||||
|
vim debian/changelog
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Sync version to main.go**:
|
||||||
|
```bash
|
||||||
|
make sync-version
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Build the package**:
|
||||||
|
```bash
|
||||||
|
make pkg-deb
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Commit all changes together**:
|
||||||
|
```bash
|
||||||
|
git add debian/changelog src/main.go
|
||||||
|
git commit -m "Cut release X.Y.Z-A"
|
||||||
|
git tag vX.Y.Z-A
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Synchronization
|
||||||
|
|
||||||
|
The build system automatically ensures that the version in `debian/changelog` matches the version constant in `src/main.go`. The `make pkg-deb` target automatically calls `make sync-version` before building to maintain consistency.
|
||||||
|
|
||||||
|
**Important**: Always update `debian/changelog` first, as this is the authoritative source for the version number. The `make sync-version` target extracts the version from the first line of the changelog and updates the `Version` constant in `src/main.go`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project uses the LGPL 3.0 licensed go-agentx library. It has been modified due to a bug,
|
||||||
|
see details in [[GitHub PR#7](https://github.com/posteo/go-agentx/pull/11)], and as such is
|
||||||
|
licensed also LGPL 3.0. The go-agentx source code in this project will be removed once the
|
||||||
|
upstream PR is merged.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Add tests if applicable
|
||||||
|
5. Submit a pull request
|
||||||
72
docs/govpp-snmp-agentx.1
Normal file
72
docs/govpp-snmp-agentx.1
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
.TH GOVPP-SNMP-AGENTX 1 "June 2025" "version 1.0.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
govpp-snmp-agentx \- VPP SNMP AgentX daemon for interface statistics
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B govpp-snmp-agentx
|
||||||
|
[\fIOPTION\fR]...
|
||||||
|
.SH DESCRIPTION
|
||||||
|
.B govpp-snmp-agentx
|
||||||
|
is an SNMP AgentX subagent that provides SNMP access to VPP (Vector Packet Processing) interface statistics and information. It connects to a master SNMP daemon via the AgentX protocol and populates standard IF-MIB tables with real-time VPP interface data.
|
||||||
|
.PP
|
||||||
|
The daemon implements two MIB tables:
|
||||||
|
.TP
|
||||||
|
.B ifEntry
|
||||||
|
Classic interface table (1.3.6.1.2.1.2.2.1) with basic interface statistics including counters for packets, bytes, errors, and discards.
|
||||||
|
.TP
|
||||||
|
.B ifXTable
|
||||||
|
Extended interface table (1.3.6.1.2.1.31.1.1.1) with high-capacity 64-bit counters and additional interface information.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
.BR \-agentx.addr " " \fIADDRESS\fR
|
||||||
|
AgentX master agent address. Can be a TCP address (hostname:port) or Unix socket path.
|
||||||
|
Default: localhost:705
|
||||||
|
.TP
|
||||||
|
.BR \-debug
|
||||||
|
Enable debug logging to show detailed operational information.
|
||||||
|
.TP
|
||||||
|
.BR \-vppcfg " " \fIFILE\fR
|
||||||
|
VPP configuration YAML file to read interface descriptions from.
|
||||||
|
.TP
|
||||||
|
.BR \-vppstats.api.addr " " \fIPATH\fR
|
||||||
|
VPP API socket path for interface enumeration.
|
||||||
|
Default: /var/run/vpp/api.sock
|
||||||
|
.TP
|
||||||
|
.BR \-vppstats.stats.addr " " \fIPATH\fR
|
||||||
|
VPP statistics socket path for interface counters.
|
||||||
|
Default: /var/run/vpp/stats.sock
|
||||||
|
.TP
|
||||||
|
.BR \-vppstats.ifindex-offset " " \fINUMBER\fR
|
||||||
|
Offset added to VPP interface indices for SNMP interface numbering.
|
||||||
|
Default: 1000
|
||||||
|
.TP
|
||||||
|
.BR \-vppstats.period " " \fISECONDS\fR
|
||||||
|
Interval in seconds for querying VPP interface statistics.
|
||||||
|
Default: 10
|
||||||
|
.SH EXAMPLES
|
||||||
|
.TP
|
||||||
|
Connect to AgentX master via TCP:
|
||||||
|
.B govpp-snmp-agentx -agentx.addr snmp.example.com:705
|
||||||
|
.TP
|
||||||
|
Connect via Unix socket with debug logging:
|
||||||
|
.B govpp-snmp-agentx -agentx.addr /var/agentx/master -debug
|
||||||
|
.TP
|
||||||
|
Use custom VPP sockets and config:
|
||||||
|
.B govpp-snmp-agentx -vppstats.api.addr /opt/vpp/api.sock -vppcfg /etc/vpp/vppcfg.yaml
|
||||||
|
.SH FILES
|
||||||
|
.TP
|
||||||
|
.I /var/run/vpp/api.sock
|
||||||
|
Default VPP API socket
|
||||||
|
.TP
|
||||||
|
.I /var/run/vpp/stats.sock
|
||||||
|
Default VPP statistics socket
|
||||||
|
.TP
|
||||||
|
.I /var/agentx/master
|
||||||
|
Common AgentX Unix socket path
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR snmpd (8),
|
||||||
|
.BR snmpwalk (1),
|
||||||
|
.BR vpp (8)
|
||||||
|
.SH AUTHOR
|
||||||
|
Pim van Pelt <pim@ipng.ch>
|
||||||
|
.SH COPYRIGHT
|
||||||
|
Copyright 2025, IPng Networks GmbH
|
||||||
4
go-agentx/.gitignore
vendored
4
go-agentx/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
agentx
|
|
||||||
Makefile
|
|
||||||
*.txt
|
|
||||||
vendor
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# This is the official list of agentx authors for copyright purposes.
|
|
||||||
# This file is distinct from the CONTRIBUTORS files.
|
|
||||||
# See the latter for an explanation.
|
|
||||||
|
|
||||||
# Names should be added to this file as
|
|
||||||
# Name or Organization <email address>
|
|
||||||
# The email address is not required for organizations.
|
|
||||||
|
|
||||||
# Please keep the list sorted.
|
|
||||||
|
|
||||||
Philipp Brüll <pb@simia.tech>
|
|
||||||
Posteo e.K. <opensource@posteo.de>
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
# AgentX
|
|
||||||
|
|
||||||
[](http://godoc.org/github.com/posteo/go-agentx)
|
|
||||||
|
|
||||||
A library with a pure Go implementation of the [AgentX-Protocol](http://tools.ietf.org/html/rfc2741). The library is not yet feature-complete, but should be far enough to used in a production environment.
|
|
||||||
|
|
||||||
The AgentX-Protocol can be used to extend a snmp-daemon such that it dispatches the requests to an OID-subtree to your Go application. Those requests are than handled by this library and can be replied with metrics about your applications state.
|
|
||||||
|
|
||||||
## State
|
|
||||||
|
|
||||||
The library implements all variable types (Integer, OctetString, Null, ObjectIdentifier, IPAddress, Counter32, Gauge32, TimeTicks, Opaque, Counter64, NoSuchObject, NoSuchInstance, EndOfMIBView), but only some of the requests (Get, GetNext, GetBulk). Set-requests and Traps are not implemented yet.
|
|
||||||
|
|
||||||
## Helper
|
|
||||||
|
|
||||||
In order to provided metrics, your have to implement the `agentx.Handler` interface. For convenience, you can use the `agentx.ListHandler` implementation, which takes a list of OIDs and values and serves them if requested. An example is listed below.
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/posteo/go-agentx"
|
|
||||||
"github.com/posteo/go-agentx/pdu"
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
client, err := agentx.Dial("tcp", "localhost:705")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(err)
|
|
||||||
}
|
|
||||||
client.Timeout = 1 * time.Minute
|
|
||||||
client.ReconnectInterval = 1 * time.Second
|
|
||||||
|
|
||||||
session, err := client.Session()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
listHandler := &agentx.ListHandler{}
|
|
||||||
|
|
||||||
item := listHandler.Add("1.3.6.1.4.1.45995.3.1")
|
|
||||||
item.Type = pdu.VariableTypeInteger
|
|
||||||
item.Value = int32(-123)
|
|
||||||
|
|
||||||
item = listHandler.Add("1.3.6.1.4.1.45995.3.2")
|
|
||||||
item.Type = pdu.VariableTypeOctetString
|
|
||||||
item.Value = "echo test"
|
|
||||||
|
|
||||||
item = listHandler.Add("1.3.6.1.4.1.45995.3.3")
|
|
||||||
item.Type = pdu.VariableTypeNull
|
|
||||||
item.Value = nil
|
|
||||||
|
|
||||||
item = listHandler.Add("1.3.6.1.4.1.45995.3.4")
|
|
||||||
item.Type = pdu.VariableTypeObjectIdentifier
|
|
||||||
item.Value = "1.3.6.1.4.1.45995.1.5"
|
|
||||||
|
|
||||||
item = listHandler.Add("1.3.6.1.4.1.45995.3.5")
|
|
||||||
item.Type = pdu.VariableTypeIPAddress
|
|
||||||
item.Value = net.IP{10, 10, 10, 10}
|
|
||||||
|
|
||||||
item = listHandler.Add("1.3.6.1.4.1.45995.3.6")
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(123)
|
|
||||||
|
|
||||||
item = listHandler.Add("1.3.6.1.4.1.45995.3.7")
|
|
||||||
item.Type = pdu.VariableTypeGauge32
|
|
||||||
item.Value = uint32(123)
|
|
||||||
|
|
||||||
item = listHandler.Add("1.3.6.1.4.1.45995.3.8")
|
|
||||||
item.Type = pdu.VariableTypeTimeTicks
|
|
||||||
item.Value = 123 * time.Second
|
|
||||||
|
|
||||||
item = listHandler.Add("1.3.6.1.4.1.45995.3.9")
|
|
||||||
item.Type = pdu.VariableTypeOpaque
|
|
||||||
item.Value = []byte{1, 2, 3}
|
|
||||||
|
|
||||||
item = listHandler.Add("1.3.6.1.4.1.45995.3.10")
|
|
||||||
item.Type = pdu.VariableTypeCounter64
|
|
||||||
item.Value = uint64(12345678901234567890)
|
|
||||||
|
|
||||||
session.Handler = listHandler
|
|
||||||
|
|
||||||
if err := session.Register(127, value.MustParseOID("1.3.6.1.4.1.45995.3")); err != nil {
|
|
||||||
log.Fatalf(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Connection lost
|
|
||||||
|
|
||||||
If the connection to the snmp-daemon is lost, the client tries to reconnect. Therefor the property `ReconnectInterval` has be set. It specifies a duration that is waited before a re-connect is tried.
|
|
||||||
If the client has open session or registrations, the client try to re-establish both on a successful re-connect.
|
|
||||||
|
|
||||||
## Project
|
|
||||||
|
|
||||||
The implementation was provided by [simia.tech (haftungsbeschränkt)](https://simia.tech).
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
The project is licensed under LGPL 3.0 (see LICENSE file).
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package agentx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/posteo/go-agentx/pdu"
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client defines an agentx client.
|
|
||||||
type Client struct {
|
|
||||||
Timeout time.Duration
|
|
||||||
ReconnectInterval time.Duration
|
|
||||||
NameOID value.OID
|
|
||||||
Name string
|
|
||||||
|
|
||||||
network string
|
|
||||||
address string
|
|
||||||
conn net.Conn
|
|
||||||
requestChan chan *request
|
|
||||||
sessions map[uint32]*Session
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial connects to the provided agentX endpoint.
|
|
||||||
func Dial(network, address string) (*Client, error) {
|
|
||||||
conn, err := net.Dial(network, address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("dial %s %s: %w", network, address, err)
|
|
||||||
}
|
|
||||||
c := &Client{
|
|
||||||
network: network,
|
|
||||||
address: address,
|
|
||||||
conn: conn,
|
|
||||||
requestChan: make(chan *request),
|
|
||||||
sessions: make(map[uint32]*Session),
|
|
||||||
}
|
|
||||||
tx := c.runTransmitter()
|
|
||||||
rx := c.runReceiver()
|
|
||||||
c.runDispatcher(tx, rx)
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close tears down the client.
|
|
||||||
func (c *Client) Close() error {
|
|
||||||
if err := c.conn.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close connection: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session sets up a new session.
|
|
||||||
func (c *Client) Session() (*Session, error) {
|
|
||||||
s := &Session{
|
|
||||||
client: c,
|
|
||||||
timeout: c.Timeout,
|
|
||||||
}
|
|
||||||
if err := s.open(c.NameOID, c.Name); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.sessions[s.ID()] = s
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) runTransmitter() chan *pdu.HeaderPacket {
|
|
||||||
tx := make(chan *pdu.HeaderPacket)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for headerPacket := range tx {
|
|
||||||
headerPacketBytes, err := headerPacket.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("marshal error: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
writer := bufio.NewWriter(c.conn)
|
|
||||||
if _, err := writer.Write(headerPacketBytes); err != nil {
|
|
||||||
log.Printf("write error: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := writer.Flush(); err != nil {
|
|
||||||
log.Printf("flush error: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return tx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) runReceiver() chan *pdu.HeaderPacket {
|
|
||||||
rx := make(chan *pdu.HeaderPacket)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
mainLoop:
|
|
||||||
for {
|
|
||||||
reader := bufio.NewReader(c.conn)
|
|
||||||
headerBytes := make([]byte, pdu.HeaderSize)
|
|
||||||
if _, err := reader.Read(headerBytes); err != nil {
|
|
||||||
if opErr, ok := err.(*net.OpError); ok && strings.HasSuffix(opErr.Error(), "use of closed network connection") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err == io.EOF {
|
|
||||||
log.Printf("lost connection - try to re-connect ...")
|
|
||||||
reopenLoop:
|
|
||||||
for {
|
|
||||||
time.Sleep(c.ReconnectInterval)
|
|
||||||
conn, err := net.Dial(c.network, c.address)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("try to reconnect: %v", err)
|
|
||||||
continue reopenLoop
|
|
||||||
}
|
|
||||||
c.conn = conn
|
|
||||||
go func() {
|
|
||||||
for _, session := range c.sessions {
|
|
||||||
delete(c.sessions, session.ID())
|
|
||||||
if err := session.reopen(); err != nil {
|
|
||||||
log.Printf("error during reopen session: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.sessions[session.ID()] = session
|
|
||||||
log.Printf("successful re-connected")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
continue mainLoop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
header := &pdu.Header{}
|
|
||||||
if err := header.UnmarshalBinary(headerBytes); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var packet pdu.Packet
|
|
||||||
switch header.Type {
|
|
||||||
case pdu.TypeResponse:
|
|
||||||
packet = &pdu.Response{}
|
|
||||||
case pdu.TypeGet:
|
|
||||||
packet = &pdu.Get{}
|
|
||||||
case pdu.TypeGetNext:
|
|
||||||
packet = &pdu.GetNext{}
|
|
||||||
default:
|
|
||||||
log.Printf("unhandled packet of type %s", header.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
packetBytes := make([]byte, header.PayloadLength)
|
|
||||||
if _, err := reader.Read(packetBytes); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := packet.UnmarshalBinary(packetBytes); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rx <- &pdu.HeaderPacket{Header: header, Packet: packet}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return rx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) runDispatcher(tx, rx chan *pdu.HeaderPacket) {
|
|
||||||
go func() {
|
|
||||||
currentPacketID := uint32(0)
|
|
||||||
responseChans := make(map[uint32]chan *pdu.HeaderPacket)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case request := <-c.requestChan:
|
|
||||||
// log.Printf(">: %v", request)
|
|
||||||
request.headerPacket.Header.PacketID = currentPacketID
|
|
||||||
responseChans[currentPacketID] = request.responseChan
|
|
||||||
currentPacketID++
|
|
||||||
|
|
||||||
tx <- request.headerPacket
|
|
||||||
case headerPacket := <-rx:
|
|
||||||
// log.Printf("<: %v", headerPacket)
|
|
||||||
packetID := headerPacket.Header.PacketID
|
|
||||||
responseChan, ok := responseChans[packetID]
|
|
||||||
if ok {
|
|
||||||
responseChan <- headerPacket
|
|
||||||
delete(responseChans, packetID)
|
|
||||||
} else {
|
|
||||||
session, ok := c.sessions[headerPacket.Header.SessionID]
|
|
||||||
if ok {
|
|
||||||
tx <- session.handle(headerPacket)
|
|
||||||
} else {
|
|
||||||
log.Printf("got without session: %v", headerPacket)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) request(hp *pdu.HeaderPacket) *pdu.HeaderPacket {
|
|
||||||
responseChan := make(chan *pdu.HeaderPacket)
|
|
||||||
request := &request{
|
|
||||||
headerPacket: hp,
|
|
||||||
responseChan: responseChan,
|
|
||||||
}
|
|
||||||
c.requestChan <- request
|
|
||||||
headerPacket := <-responseChan
|
|
||||||
return headerPacket
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package agentx_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/posteo/go-agentx"
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
)
|
|
||||||
|
|
||||||
type environment struct {
|
|
||||||
client *agentx.Client
|
|
||||||
tearDown func()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setUpTestEnvironment(tb testing.TB) *environment {
|
|
||||||
cmd := exec.Command("snmpd", "-Lo", "-f", "-c", "snmpd.conf")
|
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
require.NoError(tb, err)
|
|
||||||
go func() {
|
|
||||||
io.Copy(os.Stdout, stdout)
|
|
||||||
}()
|
|
||||||
|
|
||||||
log.Printf("run: %s", cmd)
|
|
||||||
require.NoError(tb, cmd.Start())
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
|
|
||||||
client, err := agentx.Dial("tcp", "127.0.0.1:30705")
|
|
||||||
require.NoError(tb, err)
|
|
||||||
client.Timeout = 60 * time.Second
|
|
||||||
client.NameOID = value.MustParseOID("1.3.6.1.4.1.45995")
|
|
||||||
client.Name = "test client"
|
|
||||||
|
|
||||||
return &environment{
|
|
||||||
client: client,
|
|
||||||
tearDown: func() {
|
|
||||||
require.NoError(tb, client.Close())
|
|
||||||
require.NoError(tb, cmd.Process.Kill())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module github.com/posteo/go-agentx
|
|
||||||
|
|
||||||
go 1.13
|
|
||||||
|
|
||||||
require github.com/stretchr/testify v1.6.1
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package agentx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/posteo/go-agentx/pdu"
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handler defines an interface for a handler of events that
|
|
||||||
// might occure during a session.
|
|
||||||
type Handler interface {
|
|
||||||
Get(value.OID) (value.OID, pdu.VariableType, interface{}, error)
|
|
||||||
GetNext(value.OID, bool, value.OID) (value.OID, pdu.VariableType, interface{}, error)
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package agentx_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SNMPGet(tb testing.TB, oid string) string {
|
|
||||||
cmd := exec.Command("snmpget", "-v2c", "-cpublic", "-On", "127.0.0.1:30161", oid)
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
require.NoError(tb, err)
|
|
||||||
return strings.TrimSpace(string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
func SNMPGetNext(tb testing.TB, oid string) string {
|
|
||||||
cmd := exec.Command("snmpgetnext", "-v2c", "-cpublic", "-On", "127.0.0.1:30161", oid)
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
require.NoError(tb, err)
|
|
||||||
return strings.TrimSpace(string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
func SNMPGetBulk(tb testing.TB, oid string, nonRepeaters, maxRepetitions int) string {
|
|
||||||
cmd := exec.Command("snmpbulkget", "-v2c", "-cpublic", "-On", fmt.Sprintf("-Cn%d", nonRepeaters), fmt.Sprintf("-Cr%d", maxRepetitions), "127.0.0.1:30161", oid)
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
require.NoError(tb, err)
|
|
||||||
return strings.TrimSpace(string(output))
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package agentx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/posteo/go-agentx/pdu"
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ListHandler is a helper that takes a list of oids and implements
|
|
||||||
// a default behaviour for that list.
|
|
||||||
type ListHandler struct {
|
|
||||||
oids []value.OID
|
|
||||||
items map[string]*ListItem
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a list item for the provided oid and returns it.
|
|
||||||
func (l *ListHandler) Add(oid string) *ListItem {
|
|
||||||
if l.items == nil {
|
|
||||||
l.items = make(map[string]*ListItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedOID := value.MustParseOID(oid)
|
|
||||||
l.oids = append(l.oids, parsedOID)
|
|
||||||
value.SortOIDs(l.oids)
|
|
||||||
item := &ListItem{}
|
|
||||||
l.items[oid] = item
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tries to find the provided oid and returns the corresponding value.
|
|
||||||
func (l *ListHandler) Get(oid value.OID) (value.OID, pdu.VariableType, interface{}, error) {
|
|
||||||
if l.items == nil {
|
|
||||||
return nil, pdu.VariableTypeNoSuchObject, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
item, ok := l.items[oid.String()]
|
|
||||||
if ok {
|
|
||||||
return oid, item.Type, item.Value, nil
|
|
||||||
}
|
|
||||||
return nil, pdu.VariableTypeNoSuchObject, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNext tries to find the value that follows the provided oid and returns it.
|
|
||||||
func (l *ListHandler) GetNext(from value.OID, includeFrom bool, to value.OID) (value.OID, pdu.VariableType, interface{}, error) {
|
|
||||||
if l.items == nil {
|
|
||||||
return nil, pdu.VariableTypeNoSuchObject, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, oid := range l.oids {
|
|
||||||
if oidWithin(oid, from, includeFrom, to) {
|
|
||||||
return l.Get(oid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, pdu.VariableTypeNoSuchObject, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func oidWithin(oid value.OID, from value.OID, includeFrom bool, to value.OID) bool {
|
|
||||||
fromCompare := value.CompareOIDs(from, oid)
|
|
||||||
toCompare := value.CompareOIDs(to, oid)
|
|
||||||
|
|
||||||
return (fromCompare == -1 || (fromCompare == 0 && includeFrom)) && (toCompare == 1)
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package agentx_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/posteo/go-agentx"
|
|
||||||
"github.com/posteo/go-agentx/pdu"
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestListHandler(t *testing.T) {
|
|
||||||
e := setUpTestEnvironment(t)
|
|
||||||
defer e.tearDown()
|
|
||||||
|
|
||||||
session, err := e.client.Session()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer session.Close()
|
|
||||||
|
|
||||||
lh := &agentx.ListHandler{}
|
|
||||||
i := lh.Add("1.3.6.1.4.1.45995.3.1")
|
|
||||||
i.Type = pdu.VariableTypeOctetString
|
|
||||||
i.Value = "test"
|
|
||||||
session.Handler = lh
|
|
||||||
|
|
||||||
baseOID := value.MustParseOID("1.3.6.1.4.1.45995")
|
|
||||||
|
|
||||||
require.NoError(t, session.Register(127, baseOID))
|
|
||||||
defer session.Unregister(127, baseOID)
|
|
||||||
|
|
||||||
t.Run("Get", func(t *testing.T) {
|
|
||||||
assert.Equal(t,
|
|
||||||
".1.3.6.1.4.1.45995.3.1 = STRING: \"test\"",
|
|
||||||
SNMPGet(t, "1.3.6.1.4.1.45995.3.1"))
|
|
||||||
|
|
||||||
assert.Equal(t,
|
|
||||||
".1.3.6.1.4.1.45995.3.2 = No Such Object available on this agent at this OID",
|
|
||||||
SNMPGet(t, "1.3.6.1.4.1.45995.3.2"))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("GetNext", func(t *testing.T) {
|
|
||||||
assert.Equal(t,
|
|
||||||
".1.3.6.1.4.1.45995.3.1 = STRING: \"test\"",
|
|
||||||
SNMPGetNext(t, "1.3.6.1.4.1.45995.3.0"))
|
|
||||||
|
|
||||||
assert.Equal(t,
|
|
||||||
".1.3.6.1.4.1.45995.3.1 = STRING: \"test\"",
|
|
||||||
SNMPGetNext(t, "1.3.6.1.4.1.45995.3"))
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("GetBulk", func(t *testing.T) {
|
|
||||||
assert.Equal(t,
|
|
||||||
".1.3.6.1.4.1.45995.3.1 = STRING: \"test\"",
|
|
||||||
SNMPGetBulk(t, "1.3.6.1.4.1.45995.3.0", 0, 1))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package agentx
|
|
||||||
|
|
||||||
import "github.com/posteo/go-agentx/pdu"
|
|
||||||
|
|
||||||
// ListItem defines an item of the list handler.
|
|
||||||
type ListItem struct {
|
|
||||||
Type pdu.VariableType
|
|
||||||
Value interface{}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package marshaler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Multi defines a binary marshaler that marshals all child marshalers
|
|
||||||
// and concatinate the results.
|
|
||||||
type Multi []encoding.BinaryMarshaler
|
|
||||||
|
|
||||||
// NewMulti returns a new instance of MultiBinaryMarshaler.
|
|
||||||
func NewMulti(marshalers ...encoding.BinaryMarshaler) Multi {
|
|
||||||
return Multi(marshalers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary marshals all the binary marshalers and concatinates the results.
|
|
||||||
func (m Multi) MarshalBinary() ([]byte, error) {
|
|
||||||
result := []byte{}
|
|
||||||
|
|
||||||
for _, marshaler := range m {
|
|
||||||
data, err := marshaler.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result = append(result, data...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
// AllocateIndex defiens the pdu allocate index packet.
|
|
||||||
type AllocateIndex struct {
|
|
||||||
Variables Variables
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns the pdu packet type.
|
|
||||||
func (ai *AllocateIndex) Type() Type {
|
|
||||||
return TypeIndexAllocate
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (ai *AllocateIndex) MarshalBinary() ([]byte, error) {
|
|
||||||
data, err := ai.Variables.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (ai *AllocateIndex) UnmarshalBinary(data []byte) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
// Close defines the pdu close packet.
|
|
||||||
type Close struct {
|
|
||||||
Reason Reason
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns the pdu packet type.
|
|
||||||
func (c *Close) Type() Type {
|
|
||||||
return TypeClose
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (c *Close) MarshalBinary() ([]byte, error) {
|
|
||||||
return []byte{byte(c.Reason), 0x00, 0x00, 0x00}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (c *Close) UnmarshalBinary(data []byte) error {
|
|
||||||
c.Reason = Reason(data[0])
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
// DeallocateIndex defiens the pdu deallocate index packet.
|
|
||||||
type DeallocateIndex struct {
|
|
||||||
Variables Variables
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns the pdu packet type.
|
|
||||||
func (di *DeallocateIndex) Type() Type {
|
|
||||||
return TypeIndexDeallocate
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (di *DeallocateIndex) MarshalBinary() ([]byte, error) {
|
|
||||||
data, err := di.Variables.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (di *DeallocateIndex) UnmarshalBinary(data []byte) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// The various pdu packet errors.
|
|
||||||
const (
|
|
||||||
ErrorNone Error = 0
|
|
||||||
ErrorOpenFailed Error = 256
|
|
||||||
ErrorNotOpen Error = 257
|
|
||||||
ErrorIndexWrongType Error = 258
|
|
||||||
ErrorIndexAlreadyAllocated Error = 259
|
|
||||||
ErrorIndexNoneAvailable Error = 260
|
|
||||||
ErrorIndexNotAllocated Error = 261
|
|
||||||
ErrorUnsupportedContext Error = 262
|
|
||||||
ErrorDuplicateRegistration Error = 263
|
|
||||||
ErrorUnknownRegistration Error = 264
|
|
||||||
ErrorUnknownAgentCaps Error = 265
|
|
||||||
ErrorParse Error = 266
|
|
||||||
ErrorRequestDenied Error = 267
|
|
||||||
ErrorProcessing Error = 268
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error defines a pdu packet error.
|
|
||||||
type Error uint16
|
|
||||||
|
|
||||||
func (e Error) String() string {
|
|
||||||
switch e {
|
|
||||||
case ErrorNone:
|
|
||||||
return "ErrorNone"
|
|
||||||
case ErrorOpenFailed:
|
|
||||||
return "ErrorOpenFailed"
|
|
||||||
case ErrorNotOpen:
|
|
||||||
return "ErrorNotOpen"
|
|
||||||
case ErrorIndexWrongType:
|
|
||||||
return "ErrorIndexWrongType"
|
|
||||||
case ErrorIndexAlreadyAllocated:
|
|
||||||
return "ErrorIndexAlreadyAllocated"
|
|
||||||
case ErrorIndexNoneAvailable:
|
|
||||||
return "ErrorIndexNoneAvailable"
|
|
||||||
case ErrorIndexNotAllocated:
|
|
||||||
return "ErrorIndexNotAllocated"
|
|
||||||
case ErrorUnsupportedContext:
|
|
||||||
return "ErrorUnsupportedContext"
|
|
||||||
case ErrorDuplicateRegistration:
|
|
||||||
return "ErrorDuplicateRegistration"
|
|
||||||
case ErrorUnknownRegistration:
|
|
||||||
return "ErrorUnknownRegistration"
|
|
||||||
case ErrorUnknownAgentCaps:
|
|
||||||
return "ErrorUnknownAgentCaps"
|
|
||||||
case ErrorParse:
|
|
||||||
return "ErrorParse"
|
|
||||||
case ErrorRequestDenied:
|
|
||||||
return "ErrorRequestDenied"
|
|
||||||
case ErrorProcessing:
|
|
||||||
return "ErrorProcessing"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("ErrorUnknown (%d)", e)
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The various pdu packet flags.
|
|
||||||
const (
|
|
||||||
FlagInstanceRegistration Flags = 1 << 0
|
|
||||||
FlagNewIndex Flags = 1 << 1
|
|
||||||
FlagAnyIndex Flags = 1 << 2
|
|
||||||
FlagNonDefaultContext Flags = 1 << 3
|
|
||||||
FlagNetworkByteOrder Flags = 1 << 4
|
|
||||||
)
|
|
||||||
|
|
||||||
// Flags defines pdu packet flags.
|
|
||||||
type Flags byte
|
|
||||||
|
|
||||||
func (f Flags) String() string {
|
|
||||||
result := []string{}
|
|
||||||
if f&FlagInstanceRegistration != 0 {
|
|
||||||
result = append(result, "FlagInstanceRegistration")
|
|
||||||
}
|
|
||||||
if f&FlagNewIndex != 0 {
|
|
||||||
result = append(result, "FlagNewIndex")
|
|
||||||
}
|
|
||||||
if f&FlagAnyIndex != 0 {
|
|
||||||
result = append(result, "FlagAnyIndex")
|
|
||||||
}
|
|
||||||
if f&FlagNonDefaultContext != 0 {
|
|
||||||
result = append(result, "FlagNonDefaultContext")
|
|
||||||
}
|
|
||||||
if f&FlagNetworkByteOrder != 0 {
|
|
||||||
result = append(result, "FlagNetworkByteOrder")
|
|
||||||
}
|
|
||||||
if len(result) == 0 {
|
|
||||||
return "(FlagNone)"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("(%s)", strings.Join(result, " | "))
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import "github.com/posteo/go-agentx/value"
|
|
||||||
|
|
||||||
// Get defines the pdu get packet.
|
|
||||||
type Get struct {
|
|
||||||
SearchRange Range
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOID returns the oid.
|
|
||||||
func (g *Get) GetOID() value.OID {
|
|
||||||
return g.SearchRange.From.GetIdentifier()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOID sets the provided oid.
|
|
||||||
func (g *Get) SetOID(oid value.OID) {
|
|
||||||
g.SearchRange.From.SetIdentifier(oid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns the pdu packet type.
|
|
||||||
func (g *Get) Type() Type {
|
|
||||||
return TypeGet
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (g *Get) MarshalBinary() ([]byte, error) {
|
|
||||||
return []byte{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (g *Get) UnmarshalBinary(data []byte) error {
|
|
||||||
if err := g.SearchRange.UnmarshalBinary(data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
// GetNext defines the pdu get next packet.
|
|
||||||
type GetNext struct {
|
|
||||||
SearchRanges Ranges
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns the pdu packet type.
|
|
||||||
func (g *GetNext) Type() Type {
|
|
||||||
return TypeGetNext
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (g *GetNext) MarshalBinary() ([]byte, error) {
|
|
||||||
return []byte{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (g *GetNext) UnmarshalBinary(data []byte) error {
|
|
||||||
if err := g.SearchRanges.UnmarshalBinary(data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// HeaderSize defines the total size of a header packet.
|
|
||||||
HeaderSize = 20
|
|
||||||
)
|
|
||||||
|
|
||||||
// Header defines a pdu packet header
|
|
||||||
type Header struct {
|
|
||||||
Version byte
|
|
||||||
Type Type
|
|
||||||
Flags Flags
|
|
||||||
SessionID uint32
|
|
||||||
TransactionID uint32
|
|
||||||
PacketID uint32
|
|
||||||
PayloadLength uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu header as a slice of bytes.
|
|
||||||
func (h *Header) MarshalBinary() ([]byte, error) {
|
|
||||||
buffer := bytes.NewBuffer([]byte{h.Version, byte(h.Type), byte(h.Flags), 0x00})
|
|
||||||
|
|
||||||
binary.Write(buffer, binary.LittleEndian, h.SessionID)
|
|
||||||
binary.Write(buffer, binary.LittleEndian, h.TransactionID)
|
|
||||||
binary.Write(buffer, binary.LittleEndian, h.PacketID)
|
|
||||||
binary.Write(buffer, binary.LittleEndian, h.PayloadLength)
|
|
||||||
|
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the header structure from the provided slice of bytes.
|
|
||||||
func (h *Header) UnmarshalBinary(data []byte) error {
|
|
||||||
if len(data) < HeaderSize {
|
|
||||||
return fmt.Errorf("not enough bytes (%d) to unmarshal the header (%d)", len(data), HeaderSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
h.Version, h.Type, h.Flags = data[0], Type(data[1]), Flags(data[2])
|
|
||||||
|
|
||||||
buffer := bytes.NewBuffer(data[4:])
|
|
||||||
|
|
||||||
binary.Read(buffer, binary.LittleEndian, &h.SessionID)
|
|
||||||
binary.Read(buffer, binary.LittleEndian, &h.TransactionID)
|
|
||||||
binary.Read(buffer, binary.LittleEndian, &h.PacketID)
|
|
||||||
binary.Read(buffer, binary.LittleEndian, &h.PayloadLength)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Header) String() string {
|
|
||||||
return "(header " + h.Type.String() + ")"
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HeaderPacket defines a container structure for a header and a packet.
|
|
||||||
type HeaderPacket struct {
|
|
||||||
Header *Header
|
|
||||||
Packet Packet
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (hp *HeaderPacket) MarshalBinary() ([]byte, error) {
|
|
||||||
payloadBytes, err := hp.Packet.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hp.Header.Version = 1
|
|
||||||
hp.Header.Type = hp.Packet.Type()
|
|
||||||
hp.Header.PayloadLength = uint32(len(payloadBytes))
|
|
||||||
|
|
||||||
result, err := hp.Header.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(result, payloadBytes...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hp *HeaderPacket) String() string {
|
|
||||||
return fmt.Sprintf("[head %v, body %v]", hp.Header, hp.Packet)
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ObjectIdentifier defines the pdu object identifier packet.
|
|
||||||
type ObjectIdentifier struct {
|
|
||||||
Prefix uint8
|
|
||||||
Include byte
|
|
||||||
Subidentifiers []uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetInclude sets the include field.
|
|
||||||
func (o *ObjectIdentifier) SetInclude(value bool) {
|
|
||||||
if value {
|
|
||||||
o.Include = 0x01
|
|
||||||
} else {
|
|
||||||
o.Include = 0x00
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInclude returns true if the include field ist set, false otherwise.
|
|
||||||
func (o *ObjectIdentifier) GetInclude() bool {
|
|
||||||
if o.Include == 0x00 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetIdentifier set the subidentifiers by the provided oid string.
|
|
||||||
func (o *ObjectIdentifier) SetIdentifier(oid value.OID) {
|
|
||||||
o.Subidentifiers = make([]uint32, 0)
|
|
||||||
|
|
||||||
if len(oid) > 4 && oid[0] == 1 && oid[1] == 3 && oid[2] == 6 && oid[3] == 1 {
|
|
||||||
o.Subidentifiers = append(o.Subidentifiers, uint32(1), uint32(3), uint32(6), uint32(1), uint32(oid[4]))
|
|
||||||
oid = oid[5:]
|
|
||||||
}
|
|
||||||
|
|
||||||
o.Subidentifiers = append(o.Subidentifiers, oid...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIdentifier returns the identifier as an oid string.
|
|
||||||
func (o *ObjectIdentifier) GetIdentifier() value.OID {
|
|
||||||
var oid value.OID
|
|
||||||
if o.Prefix != 0 {
|
|
||||||
oid = append(oid, 1, 3, 6, 1, uint32(o.Prefix))
|
|
||||||
}
|
|
||||||
return append(oid, o.Subidentifiers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByteSize returns the number of bytes, the binding would need in the encoded version.
|
|
||||||
func (o *ObjectIdentifier) ByteSize() int {
|
|
||||||
return 4 + len(o.Subidentifiers)*4
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (o *ObjectIdentifier) MarshalBinary() ([]byte, error) {
|
|
||||||
buffer := bytes.NewBuffer([]byte{byte(len(o.Subidentifiers)), o.Prefix, o.Include, 0x00})
|
|
||||||
|
|
||||||
for _, subidentifier := range o.Subidentifiers {
|
|
||||||
binary.Write(buffer, binary.LittleEndian, &subidentifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (o *ObjectIdentifier) UnmarshalBinary(data []byte) error {
|
|
||||||
count := data[0]
|
|
||||||
o.Prefix = data[1]
|
|
||||||
o.Include = data[2]
|
|
||||||
|
|
||||||
o.Subidentifiers = make([]uint32, 0)
|
|
||||||
buffer := bytes.NewBuffer(data[4:])
|
|
||||||
for index := byte(0); index < count; index++ {
|
|
||||||
var subidentifier uint32
|
|
||||||
if err := binary.Read(buffer, binary.LittleEndian, &subidentifier); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
o.Subidentifiers = append(o.Subidentifiers, subidentifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o ObjectIdentifier) String() string {
|
|
||||||
return o.GetIdentifier().String()
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OctetString defines the pdu description packet.
|
|
||||||
type OctetString struct {
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (o *OctetString) MarshalBinary() ([]byte, error) {
|
|
||||||
buffer := &bytes.Buffer{}
|
|
||||||
|
|
||||||
binary.Write(buffer, binary.LittleEndian, uint32(len(o.Text)))
|
|
||||||
buffer.WriteString(o.Text)
|
|
||||||
|
|
||||||
for buffer.Len()%4 > 0 {
|
|
||||||
buffer.WriteByte(0x00)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (o *OctetString) UnmarshalBinary(data []byte) error {
|
|
||||||
buffer := bytes.NewBuffer(data)
|
|
||||||
|
|
||||||
length := uint32(0)
|
|
||||||
if err := binary.Read(buffer, binary.LittleEndian, &length); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
o.Text = string(data[4 : 4+length])
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/posteo/go-agentx/marshaler"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Open defines a pdu open packet.
|
|
||||||
type Open struct {
|
|
||||||
Timeout Timeout
|
|
||||||
ID ObjectIdentifier
|
|
||||||
Description OctetString
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns the pdu packet type.
|
|
||||||
func (o *Open) Type() Type {
|
|
||||||
return TypeOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (o *Open) MarshalBinary() ([]byte, error) {
|
|
||||||
combined := marshaler.NewMulti(&o.Timeout, &o.ID, &o.Description)
|
|
||||||
|
|
||||||
combinedBytes, err := combined.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return combinedBytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (o *Open) UnmarshalBinary(data []byte) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import "encoding"
|
|
||||||
|
|
||||||
// Packet defines a general interface for a pdu packet.
|
|
||||||
type Packet interface {
|
|
||||||
TypeOwner
|
|
||||||
encoding.BinaryMarshaler
|
|
||||||
encoding.BinaryUnmarshaler
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Range defines the pdu search range packet.
|
|
||||||
type Range struct {
|
|
||||||
From ObjectIdentifier
|
|
||||||
To ObjectIdentifier
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByteSize returns the number of bytes, the binding would need in the encoded version.
|
|
||||||
func (r *Range) ByteSize() int {
|
|
||||||
return r.From.ByteSize() + r.To.ByteSize()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (r *Range) MarshalBinary() ([]byte, error) {
|
|
||||||
r.To.SetInclude(false)
|
|
||||||
return []byte{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (r *Range) UnmarshalBinary(data []byte) error {
|
|
||||||
if err := r.From.UnmarshalBinary(data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := r.To.UnmarshalBinary(data[r.From.ByteSize():]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Range) String() string {
|
|
||||||
result := ""
|
|
||||||
if r.From.GetInclude() {
|
|
||||||
result += "["
|
|
||||||
} else {
|
|
||||||
result += "("
|
|
||||||
}
|
|
||||||
result += fmt.Sprintf("%v, %v", r.From, r.To)
|
|
||||||
if r.To.GetInclude() {
|
|
||||||
result += "]"
|
|
||||||
} else {
|
|
||||||
result += ")"
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
// Ranges defines the pdu search range list packet.
|
|
||||||
type Ranges []Range
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (r *Ranges) MarshalBinary() ([]byte, error) {
|
|
||||||
return []byte{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (r *Ranges) UnmarshalBinary(data []byte) error {
|
|
||||||
*r = make([]Range, 0)
|
|
||||||
for offset := 0; offset < len(data); {
|
|
||||||
rng := Range{}
|
|
||||||
if err := rng.UnmarshalBinary(data[offset:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*r = append(*r, rng)
|
|
||||||
offset += rng.ByteSize()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// The various pdu packet reasons.
|
|
||||||
const (
|
|
||||||
ReasonOther Reason = 1
|
|
||||||
ReasonParseError Reason = 2
|
|
||||||
ReasonProtocolError Reason = 3
|
|
||||||
ReasonTimeouts Reason = 4
|
|
||||||
ReasonShutdown Reason = 5
|
|
||||||
ReasonByManager Reason = 6
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reason defines a reason.
|
|
||||||
type Reason byte
|
|
||||||
|
|
||||||
func (r Reason) String() string {
|
|
||||||
switch r {
|
|
||||||
case ReasonOther:
|
|
||||||
return "ReasonOther"
|
|
||||||
case ReasonParseError:
|
|
||||||
return "ReasonParseError"
|
|
||||||
case ReasonProtocolError:
|
|
||||||
return "ReasonProtocolError"
|
|
||||||
case ReasonTimeouts:
|
|
||||||
return "ReasonTimeouts"
|
|
||||||
case ReasonShutdown:
|
|
||||||
return "ReasonShutdown"
|
|
||||||
case ReasonByManager:
|
|
||||||
return "ReasonByManager"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("ReasonUnknown (%d)", r)
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/posteo/go-agentx/marshaler"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register defines the pdu register packet.
|
|
||||||
type Register struct {
|
|
||||||
Timeout Timeout
|
|
||||||
Subtree ObjectIdentifier
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns the pdu packet type.
|
|
||||||
func (r *Register) Type() Type {
|
|
||||||
return TypeRegister
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (r *Register) MarshalBinary() ([]byte, error) {
|
|
||||||
combined := marshaler.NewMulti(&r.Timeout, &r.Subtree)
|
|
||||||
|
|
||||||
combinedBytes, err := combined.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return combinedBytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (r *Register) UnmarshalBinary(data []byte) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Response defines the pdu response packet.
|
|
||||||
type Response struct {
|
|
||||||
UpTime time.Duration
|
|
||||||
Error Error
|
|
||||||
Index uint16
|
|
||||||
Variables Variables
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns the pdu packet type.
|
|
||||||
func (r *Response) Type() Type {
|
|
||||||
return TypeResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (r *Response) MarshalBinary() ([]byte, error) {
|
|
||||||
buffer := &bytes.Buffer{}
|
|
||||||
|
|
||||||
upTime := uint32(r.UpTime.Seconds() / 100)
|
|
||||||
binary.Write(buffer, binary.LittleEndian, &upTime)
|
|
||||||
binary.Write(buffer, binary.LittleEndian, &r.Error)
|
|
||||||
binary.Write(buffer, binary.LittleEndian, &r.Index)
|
|
||||||
|
|
||||||
vBytes, err := r.Variables.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buffer.Write(vBytes)
|
|
||||||
|
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (r *Response) UnmarshalBinary(data []byte) error {
|
|
||||||
buffer := bytes.NewBuffer(data)
|
|
||||||
|
|
||||||
upTime := uint32(0)
|
|
||||||
if err := binary.Read(buffer, binary.LittleEndian, &upTime); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
r.UpTime = time.Second * time.Duration(upTime*100)
|
|
||||||
if err := binary.Read(buffer, binary.LittleEndian, &r.Error); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := binary.Read(buffer, binary.LittleEndian, &r.Index); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := r.Variables.UnmarshalBinary(data[8:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Response) String() string {
|
|
||||||
return "(response " + r.Variables.String() + ")"
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// Timeout defines the pdu timeout packet.
|
|
||||||
type Timeout struct {
|
|
||||||
Duration time.Duration
|
|
||||||
Priority byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (t *Timeout) MarshalBinary() ([]byte, error) {
|
|
||||||
return []byte{byte(t.Duration.Seconds()), t.Priority, 0x00, 0x00}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (t *Timeout) UnmarshalBinary(data []byte) error {
|
|
||||||
t.Duration = time.Duration(data[0]) * time.Second
|
|
||||||
t.Priority = data[1]
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t Timeout) String() string {
|
|
||||||
return t.Duration.String()
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
// The various pdu packet types.
|
|
||||||
const (
|
|
||||||
TypeOpen Type = 1
|
|
||||||
TypeClose Type = 2
|
|
||||||
TypeRegister Type = 3
|
|
||||||
TypeUnregister Type = 4
|
|
||||||
TypeGet Type = 5
|
|
||||||
TypeGetNext Type = 6
|
|
||||||
TypeGetBulk Type = 7
|
|
||||||
TypeTestSet Type = 8
|
|
||||||
TypeCommitSet Type = 9
|
|
||||||
TypeUndoSet Type = 10
|
|
||||||
TypeCleanupSet Type = 11
|
|
||||||
TypeNotify Type = 12
|
|
||||||
TypePing Type = 13
|
|
||||||
TypeIndexAllocate Type = 14
|
|
||||||
TypeIndexDeallocate Type = 15
|
|
||||||
TypeAddAgentCaps Type = 16
|
|
||||||
TypeRemoveAgentCaps Type = 17
|
|
||||||
TypeResponse Type = 18
|
|
||||||
)
|
|
||||||
|
|
||||||
// Type defines the pdu packet type.
|
|
||||||
type Type byte
|
|
||||||
|
|
||||||
// TypeOwner defines the interface for an object that provides a type.
|
|
||||||
type TypeOwner interface {
|
|
||||||
Type() Type
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t Type) String() string {
|
|
||||||
switch t {
|
|
||||||
case TypeOpen:
|
|
||||||
return "TypeOpen"
|
|
||||||
case TypeClose:
|
|
||||||
return "TypeClose"
|
|
||||||
case TypeRegister:
|
|
||||||
return "TypeRegister"
|
|
||||||
case TypeUnregister:
|
|
||||||
return "TypeUnregister"
|
|
||||||
case TypeGet:
|
|
||||||
return "TypeGet"
|
|
||||||
case TypeGetNext:
|
|
||||||
return "TypeGetNext"
|
|
||||||
case TypeGetBulk:
|
|
||||||
return "TypeGetBulk"
|
|
||||||
case TypeTestSet:
|
|
||||||
return "TypeTestSet"
|
|
||||||
case TypeCommitSet:
|
|
||||||
return "TypeCommitSet"
|
|
||||||
case TypeUndoSet:
|
|
||||||
return "TypeUndoSet"
|
|
||||||
case TypeCleanupSet:
|
|
||||||
return "TypeCleanupSet"
|
|
||||||
case TypeNotify:
|
|
||||||
return "TypeNotify"
|
|
||||||
case TypePing:
|
|
||||||
return "TypePing"
|
|
||||||
case TypeIndexAllocate:
|
|
||||||
return "TypeIndexAllocate"
|
|
||||||
case TypeIndexDeallocate:
|
|
||||||
return "TypeIndexDeallocate"
|
|
||||||
case TypeAddAgentCaps:
|
|
||||||
return "TypeAddAgentCaps"
|
|
||||||
case TypeRemoveAgentCaps:
|
|
||||||
return "TypeRemoveAgentCaps"
|
|
||||||
case TypeResponse:
|
|
||||||
return "TypeResponse"
|
|
||||||
}
|
|
||||||
return "TypeUnknown"
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/posteo/go-agentx/marshaler"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Unregister defines the pdu unregister packet.
|
|
||||||
type Unregister struct {
|
|
||||||
Timeout Timeout
|
|
||||||
Subtree ObjectIdentifier
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns the pdu packet type.
|
|
||||||
func (u *Unregister) Type() Type {
|
|
||||||
return TypeUnregister
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (u *Unregister) MarshalBinary() ([]byte, error) {
|
|
||||||
combined := marshaler.NewMulti(&u.Timeout, &u.Subtree)
|
|
||||||
|
|
||||||
combinedBytes, err := combined.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return combinedBytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (u *Unregister) UnmarshalBinary(data []byte) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Variable defines the pdu varbind packet.
|
|
||||||
type Variable struct {
|
|
||||||
Type VariableType
|
|
||||||
Name ObjectIdentifier
|
|
||||||
Value interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set sets the variable.
|
|
||||||
func (v *Variable) Set(oid value.OID, t VariableType, value interface{}) {
|
|
||||||
v.Name.SetIdentifier(oid)
|
|
||||||
v.Type = t
|
|
||||||
v.Value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByteSize returns the number of bytes, the binding would need in the encoded version.
|
|
||||||
func (v *Variable) ByteSize() int {
|
|
||||||
bytes, err := v.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return len(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (v *Variable) MarshalBinary() ([]byte, error) {
|
|
||||||
buffer := &bytes.Buffer{}
|
|
||||||
|
|
||||||
binary.Write(buffer, binary.LittleEndian, &v.Type)
|
|
||||||
buffer.WriteByte(0x00)
|
|
||||||
buffer.WriteByte(0x00)
|
|
||||||
|
|
||||||
nameBytes, err := v.Name.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buffer.Write(nameBytes)
|
|
||||||
|
|
||||||
switch v.Type {
|
|
||||||
case VariableTypeInteger:
|
|
||||||
value := v.Value.(int32)
|
|
||||||
binary.Write(buffer, binary.LittleEndian, &value)
|
|
||||||
case VariableTypeOctetString:
|
|
||||||
octetString := &OctetString{Text: v.Value.(string)}
|
|
||||||
octetStringBytes, err := octetString.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buffer.Write(octetStringBytes)
|
|
||||||
case VariableTypeNull, VariableTypeNoSuchObject, VariableTypeNoSuchInstance, VariableTypeEndOfMIBView:
|
|
||||||
break
|
|
||||||
case VariableTypeObjectIdentifier:
|
|
||||||
targetOID, err := value.ParseOID(v.Value.(string))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
oi := &ObjectIdentifier{}
|
|
||||||
oi.SetIdentifier(targetOID)
|
|
||||||
oiBytes, err := oi.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buffer.Write(oiBytes)
|
|
||||||
case VariableTypeIPAddress:
|
|
||||||
ip := v.Value.(net.IP)
|
|
||||||
octetString := &OctetString{Text: string(ip)}
|
|
||||||
octetStringBytes, err := octetString.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buffer.Write(octetStringBytes)
|
|
||||||
case VariableTypeCounter32, VariableTypeGauge32:
|
|
||||||
value := v.Value.(uint32)
|
|
||||||
binary.Write(buffer, binary.LittleEndian, &value)
|
|
||||||
case VariableTypeTimeTicks:
|
|
||||||
value := uint32(v.Value.(time.Duration).Seconds() * 100)
|
|
||||||
binary.Write(buffer, binary.LittleEndian, &value)
|
|
||||||
case VariableTypeOpaque:
|
|
||||||
octetString := &OctetString{Text: string(v.Value.([]byte))}
|
|
||||||
octetStringBytes, err := octetString.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buffer.Write(octetStringBytes)
|
|
||||||
case VariableTypeCounter64:
|
|
||||||
value := v.Value.(uint64)
|
|
||||||
binary.Write(buffer, binary.LittleEndian, &value)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unhandled variable type %s", v.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (v *Variable) UnmarshalBinary(data []byte) error {
|
|
||||||
buffer := bytes.NewBuffer(data)
|
|
||||||
|
|
||||||
if err := binary.Read(buffer, binary.LittleEndian, &v.Type); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
offset := 4
|
|
||||||
|
|
||||||
if err := v.Name.UnmarshalBinary(data[offset:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
offset += v.Name.ByteSize()
|
|
||||||
|
|
||||||
switch v.Type {
|
|
||||||
case VariableTypeInteger:
|
|
||||||
value := int32(0)
|
|
||||||
if err := binary.Read(buffer, binary.LittleEndian, &value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Value = value
|
|
||||||
case VariableTypeOctetString:
|
|
||||||
octetString := &OctetString{}
|
|
||||||
if err := octetString.UnmarshalBinary(data[offset:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Value = octetString.Text
|
|
||||||
case VariableTypeNull, VariableTypeNoSuchObject, VariableTypeNoSuchInstance, VariableTypeEndOfMIBView:
|
|
||||||
v.Value = nil
|
|
||||||
case VariableTypeObjectIdentifier:
|
|
||||||
oid := &ObjectIdentifier{}
|
|
||||||
if err := oid.UnmarshalBinary(data[offset:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Value = oid.GetIdentifier()
|
|
||||||
case VariableTypeIPAddress:
|
|
||||||
octetString := &OctetString{}
|
|
||||||
if err := octetString.UnmarshalBinary(data[offset:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Value = net.IP(octetString.Text)
|
|
||||||
case VariableTypeCounter32, VariableTypeGauge32:
|
|
||||||
value := uint32(0)
|
|
||||||
if err := binary.Read(buffer, binary.LittleEndian, &value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Value = value
|
|
||||||
case VariableTypeTimeTicks:
|
|
||||||
value := uint32(0)
|
|
||||||
if err := binary.Read(buffer, binary.LittleEndian, &value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Value = time.Duration(value) * time.Second / 100
|
|
||||||
case VariableTypeOpaque:
|
|
||||||
octetString := &OctetString{}
|
|
||||||
if err := octetString.UnmarshalBinary(data[offset:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Value = []byte(octetString.Text)
|
|
||||||
case VariableTypeCounter64:
|
|
||||||
value := uint64(0)
|
|
||||||
if err := binary.Read(buffer, binary.LittleEndian, &value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
v.Value = value
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unhandled variable type %s", v.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Variable) String() string {
|
|
||||||
return fmt.Sprintf("(variable %s = %v)", v.Type, v.Value)
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// The various variable types.
|
|
||||||
const (
|
|
||||||
VariableTypeInteger VariableType = 2
|
|
||||||
VariableTypeOctetString VariableType = 4
|
|
||||||
VariableTypeNull VariableType = 5
|
|
||||||
VariableTypeObjectIdentifier VariableType = 6
|
|
||||||
VariableTypeIPAddress VariableType = 64
|
|
||||||
VariableTypeCounter32 VariableType = 65
|
|
||||||
VariableTypeGauge32 VariableType = 66
|
|
||||||
VariableTypeTimeTicks VariableType = 67
|
|
||||||
VariableTypeOpaque VariableType = 68
|
|
||||||
VariableTypeCounter64 VariableType = 70
|
|
||||||
VariableTypeNoSuchObject VariableType = 128
|
|
||||||
VariableTypeNoSuchInstance VariableType = 129
|
|
||||||
VariableTypeEndOfMIBView VariableType = 130
|
|
||||||
)
|
|
||||||
|
|
||||||
// VariableType defines the type of a variable.
|
|
||||||
type VariableType uint16
|
|
||||||
|
|
||||||
func (v VariableType) String() string {
|
|
||||||
switch v {
|
|
||||||
case VariableTypeInteger:
|
|
||||||
return "VariableTypeInteger"
|
|
||||||
case VariableTypeOctetString:
|
|
||||||
return "VariableTypeOctetString"
|
|
||||||
case VariableTypeNull:
|
|
||||||
return "VariableTypeNull"
|
|
||||||
case VariableTypeObjectIdentifier:
|
|
||||||
return "VariableTypeObjectIdentifier"
|
|
||||||
case VariableTypeIPAddress:
|
|
||||||
return "VariableTypeIPAddress"
|
|
||||||
case VariableTypeCounter32:
|
|
||||||
return "VariableTypeCounter32"
|
|
||||||
case VariableTypeGauge32:
|
|
||||||
return "VariableTypeGauge32"
|
|
||||||
case VariableTypeTimeTicks:
|
|
||||||
return "VariableTypeTimeTicks"
|
|
||||||
case VariableTypeOpaque:
|
|
||||||
return "VariableTypeOpaque"
|
|
||||||
case VariableTypeCounter64:
|
|
||||||
return "VariableTypeCounter64"
|
|
||||||
case VariableTypeNoSuchObject:
|
|
||||||
return "VariableTypeNoSuchObject"
|
|
||||||
case VariableTypeNoSuchInstance:
|
|
||||||
return "VariableTypeNoSuchInstance"
|
|
||||||
case VariableTypeEndOfMIBView:
|
|
||||||
return "VariableTypeEndOfMIBView"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("VariableTypeUnknown (%d)", v)
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package pdu
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Variables defines a list of variable bindings.
|
|
||||||
type Variables []Variable
|
|
||||||
|
|
||||||
// Add adds the provided variable.
|
|
||||||
func (v *Variables) Add(oid value.OID, t VariableType, value interface{}) {
|
|
||||||
variable := Variable{}
|
|
||||||
variable.Set(oid, t, value)
|
|
||||||
*v = append(*v, variable)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary returns the pdu packet as a slice of bytes.
|
|
||||||
func (v *Variables) MarshalBinary() ([]byte, error) {
|
|
||||||
result := []byte{}
|
|
||||||
for _, variable := range *v {
|
|
||||||
data, err := variable.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result = append(result, data...)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary sets the packet structure from the provided slice of bytes.
|
|
||||||
func (v *Variables) UnmarshalBinary(data []byte) error {
|
|
||||||
*v = make([]Variable, 0)
|
|
||||||
for offset := 0; offset < len(data); {
|
|
||||||
variable := Variable{}
|
|
||||||
if err := variable.UnmarshalBinary(data[offset:]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = append(*v, variable)
|
|
||||||
offset += variable.ByteSize()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Variables) String() string {
|
|
||||||
parts := make([]string, len(v))
|
|
||||||
for index, va := range v {
|
|
||||||
parts[index] = va.String()
|
|
||||||
}
|
|
||||||
return "[variables " + strings.Join(parts, ", ") + "]"
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package agentx
|
|
||||||
|
|
||||||
import "github.com/posteo/go-agentx/pdu"
|
|
||||||
|
|
||||||
type request struct {
|
|
||||||
headerPacket *pdu.HeaderPacket
|
|
||||||
responseChan chan *pdu.HeaderPacket
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *request) String() string {
|
|
||||||
return "(request " + r.headerPacket.String() + ")"
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package agentx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/posteo/go-agentx/pdu"
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Session defines an agentx session.
|
|
||||||
type Session struct {
|
|
||||||
Handler Handler
|
|
||||||
|
|
||||||
client *Client
|
|
||||||
sessionID uint32
|
|
||||||
timeout time.Duration
|
|
||||||
|
|
||||||
openRequestPacket *pdu.HeaderPacket
|
|
||||||
registerRequestPacket *pdu.HeaderPacket
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns the session id.
|
|
||||||
func (s *Session) ID() uint32 {
|
|
||||||
return s.sessionID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register registers the client under the provided rootID with the provided priority
|
|
||||||
// on the master agent.
|
|
||||||
func (s *Session) Register(priority byte, baseOID value.OID) error {
|
|
||||||
if s.registerRequestPacket != nil {
|
|
||||||
return fmt.Errorf("session is already registered")
|
|
||||||
}
|
|
||||||
|
|
||||||
requestPacket := &pdu.Register{}
|
|
||||||
requestPacket.Timeout.Duration = s.timeout
|
|
||||||
requestPacket.Timeout.Priority = priority
|
|
||||||
requestPacket.Subtree.SetIdentifier(baseOID)
|
|
||||||
request := &pdu.HeaderPacket{Header: &pdu.Header{Type: pdu.TypeRegister}, Packet: requestPacket}
|
|
||||||
|
|
||||||
response := s.request(request)
|
|
||||||
if err := checkError(response); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.registerRequestPacket = request
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unregister removes the registration for the provided subtree.
|
|
||||||
func (s *Session) Unregister(priority byte, baseOID value.OID) error {
|
|
||||||
if s.registerRequestPacket == nil {
|
|
||||||
return fmt.Errorf("session is not registered")
|
|
||||||
}
|
|
||||||
|
|
||||||
requestPacket := &pdu.Unregister{}
|
|
||||||
requestPacket.Timeout.Duration = s.timeout
|
|
||||||
requestPacket.Timeout.Priority = priority
|
|
||||||
requestPacket.Subtree.SetIdentifier(baseOID)
|
|
||||||
request := &pdu.HeaderPacket{Header: &pdu.Header{}, Packet: requestPacket}
|
|
||||||
|
|
||||||
response := s.request(request)
|
|
||||||
if err := checkError(response); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.registerRequestPacket = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close tears down the session with the master agent.
|
|
||||||
func (s *Session) Close() error {
|
|
||||||
requestPacket := &pdu.Close{Reason: pdu.ReasonShutdown}
|
|
||||||
|
|
||||||
response := s.request(&pdu.HeaderPacket{Header: &pdu.Header{}, Packet: requestPacket})
|
|
||||||
if err := checkError(response); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) open(nameOID value.OID, name string) error {
|
|
||||||
requestPacket := &pdu.Open{}
|
|
||||||
requestPacket.Timeout.Duration = s.timeout
|
|
||||||
requestPacket.ID.SetIdentifier(nameOID)
|
|
||||||
requestPacket.Description.Text = name
|
|
||||||
request := &pdu.HeaderPacket{Header: &pdu.Header{Type: pdu.TypeOpen}, Packet: requestPacket}
|
|
||||||
|
|
||||||
response := s.request(request)
|
|
||||||
if err := checkError(response); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.sessionID = response.Header.SessionID
|
|
||||||
s.openRequestPacket = request
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) reopen() error {
|
|
||||||
if s.openRequestPacket != nil {
|
|
||||||
response := s.request(s.openRequestPacket)
|
|
||||||
if err := checkError(response); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.sessionID = response.Header.SessionID
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.registerRequestPacket != nil {
|
|
||||||
response := s.request(s.registerRequestPacket)
|
|
||||||
if err := checkError(response); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) request(hp *pdu.HeaderPacket) *pdu.HeaderPacket {
|
|
||||||
hp.Header.SessionID = s.sessionID
|
|
||||||
return s.client.request(hp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) handle(request *pdu.HeaderPacket) *pdu.HeaderPacket {
|
|
||||||
responseHeader := &pdu.Header{}
|
|
||||||
responseHeader.SessionID = request.Header.SessionID
|
|
||||||
responseHeader.TransactionID = request.Header.TransactionID
|
|
||||||
responseHeader.PacketID = request.Header.PacketID
|
|
||||||
responsePacket := &pdu.Response{}
|
|
||||||
|
|
||||||
switch requestPacket := request.Packet.(type) {
|
|
||||||
case *pdu.Get:
|
|
||||||
if s.Handler == nil {
|
|
||||||
log.Printf("warning: no handler for session specified")
|
|
||||||
responsePacket.Variables.Add(requestPacket.GetOID(), pdu.VariableTypeNull, nil)
|
|
||||||
} else {
|
|
||||||
oid, t, v, err := s.Handler.Get(requestPacket.GetOID())
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error while handling packet: %v", err)
|
|
||||||
responsePacket.Error = pdu.ErrorProcessing
|
|
||||||
}
|
|
||||||
if oid == nil {
|
|
||||||
responsePacket.Variables.Add(requestPacket.GetOID(), pdu.VariableTypeNoSuchObject, nil)
|
|
||||||
} else {
|
|
||||||
responsePacket.Variables.Add(oid, t, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case *pdu.GetNext:
|
|
||||||
if s.Handler == nil {
|
|
||||||
log.Printf("warning: no handler for session specified")
|
|
||||||
} else {
|
|
||||||
for _, sr := range requestPacket.SearchRanges {
|
|
||||||
oid, t, v, err := s.Handler.GetNext(sr.From.GetIdentifier(), (sr.From.Include == 1), sr.To.GetIdentifier())
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error while handling packet: %v", err)
|
|
||||||
responsePacket.Error = pdu.ErrorProcessing
|
|
||||||
}
|
|
||||||
|
|
||||||
if oid == nil {
|
|
||||||
responsePacket.Variables.Add(sr.From.GetIdentifier(), pdu.VariableTypeEndOfMIBView, nil)
|
|
||||||
} else {
|
|
||||||
responsePacket.Variables.Add(oid, t, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Printf("cannot handle unrequested packet: %v", request)
|
|
||||||
responsePacket.Error = pdu.ErrorProcessing
|
|
||||||
}
|
|
||||||
|
|
||||||
return &pdu.HeaderPacket{Header: responseHeader, Packet: responsePacket}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkError(hp *pdu.HeaderPacket) error {
|
|
||||||
response, ok := hp.Packet.(*pdu.Response)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if response.Error == pdu.ErrorNone {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errors.New(response.Error.String())
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package agentx_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSession(t *testing.T) {
|
|
||||||
e := setUpTestEnvironment(t)
|
|
||||||
defer e.tearDown()
|
|
||||||
|
|
||||||
t.Run("Open", func(t *testing.T) {
|
|
||||||
session, err := e.client.Session()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer session.Close()
|
|
||||||
|
|
||||||
assert.NotEqual(t, 0, session.ID())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Close", func(t *testing.T) {
|
|
||||||
session, err := e.client.Session()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NoError(t, session.Close())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Register", func(t *testing.T) {
|
|
||||||
session, err := e.client.Session()
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer session.Close()
|
|
||||||
|
|
||||||
baseOID := value.MustParseOID("1.3.6.1.4.1.45995")
|
|
||||||
|
|
||||||
require.NoError(t,
|
|
||||||
session.Register(127, baseOID))
|
|
||||||
|
|
||||||
require.NoError(t,
|
|
||||||
session.Unregister(127, baseOID))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{ pkgs ? import <nixpkgs> {} }:
|
|
||||||
pkgs.mkShell {
|
|
||||||
name = "dev-environment";
|
|
||||||
buildInputs = [
|
|
||||||
pkgs.net-snmp
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
agentaddress udp:127.0.0.1:30161
|
|
||||||
|
|
||||||
rocommunity public
|
|
||||||
|
|
||||||
master agentx
|
|
||||||
agentXSocket tcp:127.0.0.1:30705
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package value
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OID defines an OID.
|
|
||||||
type OID []uint32
|
|
||||||
|
|
||||||
// ParseOID parses the provided string and returns a valid oid. If one of the
|
|
||||||
// subidentifers canot be parsed to an uint32, the function will panic.
|
|
||||||
func ParseOID(text string) (OID, error) {
|
|
||||||
var result OID
|
|
||||||
|
|
||||||
parts := strings.Split(text, ".")
|
|
||||||
for _, part := range parts {
|
|
||||||
subidentifier, err := strconv.ParseUint(part, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse uint [%s]: %w", part, err)
|
|
||||||
}
|
|
||||||
result = append(result, uint32(subidentifier))
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustParseOID works like ParseOID expect it panics on a parsing error.
|
|
||||||
func MustParseOID(text string) OID {
|
|
||||||
result, err := ParseOID(text)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// First returns the first n subidentifiers as a new oid.
|
|
||||||
func (o OID) First(count int) OID {
|
|
||||||
return o[:count]
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommonPrefix compares the oid with the provided one and
|
|
||||||
// returns a new oid containing all matching prefix subidentifiers.
|
|
||||||
func (o OID) CommonPrefix(other OID) OID {
|
|
||||||
matchCount := 0
|
|
||||||
|
|
||||||
for index, subidentifier := range o {
|
|
||||||
if index >= len(other) || subidentifier != other[index] {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
matchCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
return o[:matchCount]
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompareOIDs returns an integer comparing two OIDs lexicographically.
|
|
||||||
// The result will be 0 if oid1 == oid2, -1 if oid1 < oid2, +1 if oid1 > oid2.
|
|
||||||
func CompareOIDs(oid1, oid2 OID) int {
|
|
||||||
if oid2 != nil {
|
|
||||||
oid1Length := len(oid1)
|
|
||||||
oid2Length := len(oid2)
|
|
||||||
for i := 0; i < oid1Length && i < oid2Length; i++ {
|
|
||||||
if oid1[i] < oid2[i] {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if oid1[i] > oid2[i] {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if oid1Length == oid2Length {
|
|
||||||
return 0
|
|
||||||
} else if oid1Length < oid2Length {
|
|
||||||
return -1
|
|
||||||
} else {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// SortOIDs performs sorting of the OID list.
|
|
||||||
func SortOIDs(oids []OID) {
|
|
||||||
sort.Slice(oids, func(i, j int) bool {
|
|
||||||
return CompareOIDs(oids[i], oids[j]) == -1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o OID) String() string {
|
|
||||||
var parts []string
|
|
||||||
|
|
||||||
for _, subidentifier := range o {
|
|
||||||
parts = append(parts, fmt.Sprintf("%d", subidentifier))
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(parts, ".")
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
// Copyright 2018 The agentx authors
|
|
||||||
// Licensed under the LGPLv3 with static-linking exception.
|
|
||||||
// See LICENCE file for details.
|
|
||||||
|
|
||||||
package value_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCommonPrefix(t *testing.T) {
|
|
||||||
oid := value.MustParseOID("1.3.6.1.2")
|
|
||||||
result := oid.CommonPrefix(value.MustParseOID("1.3.6.1.4"))
|
|
||||||
assert.Equal(t, value.MustParseOID("1.3.6.1"), result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareOIDs_Less(t *testing.T) {
|
|
||||||
oid1 := value.OID{1, 3, 6, 1, 2}
|
|
||||||
oid2 := value.OID{1, 3, 6, 1, 4}
|
|
||||||
|
|
||||||
// oid1 < oid2
|
|
||||||
expected := -1
|
|
||||||
assert.Equal(t, expected, value.CompareOIDs(oid1, oid2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareOIDs_Greater(t *testing.T) {
|
|
||||||
oid1 := value.OID{1, 3, 6, 1, 2}
|
|
||||||
oid2 := value.OID{1, 3, 6, 1, 4}
|
|
||||||
|
|
||||||
// oid2 > oid1
|
|
||||||
expected := 1
|
|
||||||
assert.Equal(t, expected, value.CompareOIDs(oid2, oid1))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareOIDs_Equals(t *testing.T) {
|
|
||||||
oid1 := value.OID{1, 3, 6, 1, 4}
|
|
||||||
oid2 := value.OID{1, 3, 6, 1, 4}
|
|
||||||
|
|
||||||
// oid1 == oid2
|
|
||||||
expected := 0
|
|
||||||
assert.Equal(t, expected, value.CompareOIDs(oid1, oid2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareOIDs_NilValue(t *testing.T) {
|
|
||||||
oid1 := value.OID{1, 3, 6, 1, 4}
|
|
||||||
var oid2 value.OID
|
|
||||||
|
|
||||||
// oid2 is nil, thus oid1 is greater
|
|
||||||
expected := 1
|
|
||||||
assert.Equal(t, expected, value.CompareOIDs(oid1, oid2))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSortOIDs(t *testing.T) {
|
|
||||||
var oidList []value.OID
|
|
||||||
oid1 := value.OID{1, 3, 6, 1}
|
|
||||||
oid2 := value.OID{1, 3, 6, 5, 7}
|
|
||||||
oid3 := value.OID{1, 3, 6, 1, 12}
|
|
||||||
oid4 := value.OID{1, 3, 6, 5}
|
|
||||||
|
|
||||||
oidList = append(oidList, oid1, oid2, oid3, oid4)
|
|
||||||
value.SortOIDs(oidList)
|
|
||||||
|
|
||||||
var expect []value.OID
|
|
||||||
expect = append(expect, oid1, oid3, oid4, oid2)
|
|
||||||
assert.Equal(t, expect, oidList)
|
|
||||||
}
|
|
||||||
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
|
||||||
340
ifmib/ifmib.go
340
ifmib/ifmib.go
@@ -1,340 +0,0 @@
|
|||||||
package ifmib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/posteo/go-agentx"
|
|
||||||
"github.com/posteo/go-agentx/pdu"
|
|
||||||
"github.com/posteo/go-agentx/value"
|
|
||||||
"go.fd.io/govpp/api"
|
|
||||||
|
|
||||||
"govpp-snmp-example/logger"
|
|
||||||
"govpp-snmp-example/vppstats"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IF-MIB OID bases:
|
|
||||||
// ifEntry (classic): 1.3.6.1.2.1.2.2.1
|
|
||||||
// ifXTable (extended): 1.3.6.1.2.1.31.1.1.1
|
|
||||||
|
|
||||||
// ifEntry (1.3.6.1.2.1.2.2.1) - Classic Interface Table:
|
|
||||||
// ifIndex .1 - Integer32
|
|
||||||
// ifDescr .2 - DisplayString
|
|
||||||
// ifType .3 - IANAifType
|
|
||||||
// ifMtu .4 - Integer32
|
|
||||||
// ifSpeed .5 - Gauge32
|
|
||||||
// ifPhysAddress .6 - PhysAddress
|
|
||||||
// ifAdminStatus .7 - INTEGER
|
|
||||||
// ifOperStatus .8 - INTEGER
|
|
||||||
// ifLastChange .9 - TimeTicks
|
|
||||||
// ifInOctets .10 - Counter32
|
|
||||||
// ifInUcastPkts .11 - Counter32
|
|
||||||
// ifInNUcastPkts .12 - Counter32
|
|
||||||
// ifInDiscards .13 - Counter32
|
|
||||||
// ifInErrors .14 - Counter32
|
|
||||||
// ifInUnknownProtos .15 - Counter32
|
|
||||||
// ifOutOctets .16 - Counter32
|
|
||||||
// ifOutUcastPkts .17 - Counter32
|
|
||||||
// ifOutNUcastPkts .18 - Counter32
|
|
||||||
// ifOutDiscards .19 - Counter32
|
|
||||||
// ifOutErrors .20 - Counter32
|
|
||||||
// ifOutQLen .21 - Gauge32
|
|
||||||
// ifSpecific .22 - OBJECT IDENTIFIER
|
|
||||||
|
|
||||||
// ifXTable (1.3.6.1.2.1.31.1.1.1) - Extended Interface Table:
|
|
||||||
// ifName .1 - DisplayString
|
|
||||||
// ifInMulticastPkts .2 - Counter32
|
|
||||||
// ifInBroadcastPkts .3 - Counter32
|
|
||||||
// ifOutMulticastPkts .4 - Counter32
|
|
||||||
// ifOutBroadcastPkts .5 - Counter32
|
|
||||||
// ifHCInOctets .6 - Counter64
|
|
||||||
// ifHCInUcastPkts .7 - Counter64
|
|
||||||
// ifHCInMulticastPkts .8 - Counter64
|
|
||||||
// ifHCInBroadcastPkts .9 - Counter64
|
|
||||||
// ifHCOutOctets .10 - Counter64
|
|
||||||
// ifHCOutUcastPkts .11 - Counter64
|
|
||||||
// ifHCOutMulticastPkts .12 - Counter64
|
|
||||||
// ifHCOutBroadcastPkts .13 - Counter64
|
|
||||||
|
|
||||||
const ifEntryOID = "1.3.6.1.2.1.2.2.1"
|
|
||||||
const ifXTableOID = "1.3.6.1.2.1.31.1.1.1"
|
|
||||||
|
|
||||||
type InterfaceMIB struct {
|
|
||||||
mutex sync.RWMutex
|
|
||||||
handler *agentx.ListHandler
|
|
||||||
ifEntrySession *agentx.Session
|
|
||||||
ifXTableSession *agentx.Session
|
|
||||||
stats map[uint32]*api.InterfaceCounters // indexed by interface index
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInterfaceMIB() *InterfaceMIB {
|
|
||||||
return &InterfaceMIB{
|
|
||||||
handler: &agentx.ListHandler{},
|
|
||||||
stats: make(map[uint32]*api.InterfaceCounters),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *InterfaceMIB) GetHandler() *agentx.ListHandler {
|
|
||||||
return m.handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
logger.Debugf("Updating IF-MIB with %d interfaces", len(interfaceStats.Interfaces))
|
|
||||||
|
|
||||||
// Clear existing entries
|
|
||||||
m.handler = &agentx.ListHandler{}
|
|
||||||
m.stats = make(map[uint32]*api.InterfaceCounters)
|
|
||||||
|
|
||||||
// Add new entries
|
|
||||||
for _, iface := range interfaceStats.Interfaces {
|
|
||||||
logger.Debugf("Processing interface %d (%s)", iface.InterfaceIndex, iface.InterfaceName)
|
|
||||||
m.stats[iface.InterfaceIndex] = &iface
|
|
||||||
m.addInterfaceToMIB(&iface)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update both sessions with the new handler
|
|
||||||
if m.ifEntrySession != nil {
|
|
||||||
m.ifEntrySession.Handler = m.handler
|
|
||||||
}
|
|
||||||
if m.ifXTableSession != nil {
|
|
||||||
m.ifXTableSession.Handler = m.handler
|
|
||||||
logger.Printf("Updated session handlers with new IF-MIB data")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("IF-MIB now contains %d interfaces", len(m.stats))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) {
|
|
||||||
idx := int(iface.InterfaceIndex) + *vppstats.IfIndexOffset
|
|
||||||
|
|
||||||
// Add ifEntry (classic interface table) entries
|
|
||||||
m.addIfEntry(iface, idx)
|
|
||||||
|
|
||||||
// Add ifXTable (extended interface table) entries
|
|
||||||
m.addIfXTable(iface, idx)
|
|
||||||
|
|
||||||
logger.Debugf("Added interface %d (%s) to IF-MIB with SNMP index %d", iface.InterfaceIndex, iface.InterfaceName, idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
|
|
||||||
var item *agentx.ListItem
|
|
||||||
|
|
||||||
// ifIndex (.1)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.1.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeInteger
|
|
||||||
item.Value = int32(idx)
|
|
||||||
|
|
||||||
// ifDescr (.2)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.2.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeOctetString
|
|
||||||
item.Value = iface.InterfaceName
|
|
||||||
|
|
||||||
// ifType (.3) - Using ethernetCsmacd(6) as default
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.3.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeInteger
|
|
||||||
item.Value = int32(6)
|
|
||||||
|
|
||||||
// ifMtu (.4) - Default MTU 1500
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.4.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeInteger
|
|
||||||
item.Value = int32(1500)
|
|
||||||
|
|
||||||
// ifSpeed (.5) - Default to 1Gbps (1000000000 bits/sec)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeGauge32
|
|
||||||
item.Value = uint32(1000000000)
|
|
||||||
|
|
||||||
// ifPhysAddress (.6) - Empty for now
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.6.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeOctetString
|
|
||||||
item.Value = ""
|
|
||||||
|
|
||||||
// ifAdminStatus (.7) - up(1)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeInteger
|
|
||||||
item.Value = int32(1)
|
|
||||||
|
|
||||||
// ifOperStatus (.8) - up(1)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeInteger
|
|
||||||
item.Value = int32(1)
|
|
||||||
|
|
||||||
// ifLastChange (.9) - 0 (unknown)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.9.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeTimeTicks
|
|
||||||
item.Value = 0 * time.Second
|
|
||||||
|
|
||||||
// ifInOctets (.10)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.10.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.Rx.Bytes)
|
|
||||||
|
|
||||||
// ifInUcastPkts (.11)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.11.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.RxUnicast.Packets)
|
|
||||||
|
|
||||||
// ifInNUcastPkts (.12) - multicast + broadcast
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.12.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.RxMulticast.Packets + iface.RxBroadcast.Packets)
|
|
||||||
|
|
||||||
// ifInDiscards (.13) - using drops
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.13.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.Drops)
|
|
||||||
|
|
||||||
// ifInErrors (.14)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.14.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.RxErrors)
|
|
||||||
|
|
||||||
// ifInUnknownProtos (.15) - 0 (not available)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.15.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(0)
|
|
||||||
|
|
||||||
// ifOutOctets (.16)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.16.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.Tx.Bytes)
|
|
||||||
|
|
||||||
// ifOutUcastPkts (.17)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.17.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.TxUnicast.Packets)
|
|
||||||
|
|
||||||
// ifOutNUcastPkts (.18) - multicast + broadcast
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.TxMulticast.Packets + iface.TxBroadcast.Packets)
|
|
||||||
|
|
||||||
// ifOutDiscards (.19) - 0 (not available)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.19.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(0)
|
|
||||||
|
|
||||||
// ifOutErrors (.20)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.20.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.TxErrors)
|
|
||||||
|
|
||||||
// ifOutQLen (.21) - 0 (not available)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.21.%d", ifEntryOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeGauge32
|
|
||||||
item.Value = uint32(0)
|
|
||||||
|
|
||||||
// ifSpecific (.22) - Skip this field as it's optional and causing issues
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *InterfaceMIB) addIfXTable(iface *api.InterfaceCounters, idx int) {
|
|
||||||
var item *agentx.ListItem
|
|
||||||
|
|
||||||
// ifName (.1)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.1.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeOctetString
|
|
||||||
item.Value = iface.InterfaceName
|
|
||||||
|
|
||||||
// ifInMulticastPkts (.2)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.2.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.RxMulticast.Packets)
|
|
||||||
|
|
||||||
// ifInBroadcastPkts (.3)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.3.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.RxBroadcast.Packets)
|
|
||||||
|
|
||||||
// ifOutMulticastPkts (.4)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.4.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.TxMulticast.Packets)
|
|
||||||
|
|
||||||
// ifOutBroadcastPkts (.5)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter32
|
|
||||||
item.Value = uint32(iface.TxBroadcast.Packets)
|
|
||||||
|
|
||||||
// ifHCInOctets (.6)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.6.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter64
|
|
||||||
item.Value = iface.Rx.Bytes
|
|
||||||
|
|
||||||
// ifHCInUcastPkts (.7)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter64
|
|
||||||
item.Value = iface.RxUnicast.Packets
|
|
||||||
|
|
||||||
// ifHCInMulticastPkts (.8)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter64
|
|
||||||
item.Value = iface.RxMulticast.Packets
|
|
||||||
|
|
||||||
// ifHCInBroadcastPkts (.9)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.9.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter64
|
|
||||||
item.Value = iface.RxBroadcast.Packets
|
|
||||||
|
|
||||||
// ifHCOutOctets (.10)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.10.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter64
|
|
||||||
item.Value = iface.Tx.Bytes
|
|
||||||
|
|
||||||
// ifHCOutUcastPkts (.11)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.11.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter64
|
|
||||||
item.Value = iface.TxUnicast.Packets
|
|
||||||
|
|
||||||
// ifHCOutMulticastPkts (.12)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.12.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter64
|
|
||||||
item.Value = iface.TxMulticast.Packets
|
|
||||||
|
|
||||||
// ifHCOutBroadcastPkts (.13)
|
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.13.%d", ifXTableOID, idx))
|
|
||||||
item.Type = pdu.VariableTypeCounter64
|
|
||||||
item.Value = iface.TxBroadcast.Packets
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *InterfaceMIB) RegisterWithClient(client *agentx.Client) error {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
// Create separate sessions for each MIB
|
|
||||||
ifEntrySession, err := client.Session()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create ifEntry session: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ifXTableSession, err := client.Session()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create ifXTable session: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.ifEntrySession = ifEntrySession
|
|
||||||
m.ifXTableSession = ifXTableSession
|
|
||||||
|
|
||||||
// Set handlers for both sessions
|
|
||||||
ifEntrySession.Handler = m.handler
|
|
||||||
ifXTableSession.Handler = m.handler
|
|
||||||
|
|
||||||
// Register the classic ifEntry
|
|
||||||
err = ifEntrySession.Register(127, value.MustParseOID(ifEntryOID))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to register ifEntry: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the extended ifXTable
|
|
||||||
err = ifXTableSession.Register(127, value.MustParseOID(ifXTableOID))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to register ifXTable: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("Registered IF-MIB ifEntry at OID %s", ifEntryOID)
|
|
||||||
logger.Debugf("Registered IF-MIB ifXTable at OID %s", ifXTableOID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
46
src/agentx/agentx.go
Normal file
46
src/agentx/agentx.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// 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 {
|
||||||
|
// Determine network type based on address format
|
||||||
|
network := "tcp"
|
||||||
|
if strings.HasPrefix(*AgentXAddr, "/") {
|
||||||
|
network = "unix"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Connecting to AgentX at %s://%s", network, *AgentXAddr)
|
||||||
|
|
||||||
|
client, err := agentx.Dial(network, *AgentXAddr,
|
||||||
|
agentx.WithTimeout(1*time.Minute),
|
||||||
|
agentx.WithReconnectInterval(1*time.Second),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, *AgentXAddr)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/config/config.go
Normal file
8
src/config/config.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
// Global configuration variables
|
||||||
|
var (
|
||||||
|
Debug bool
|
||||||
|
)
|
||||||
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,20 +1,18 @@
|
|||||||
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.3.0
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/posteo/go-agentx => ./go-agentx
|
|
||||||
@@ -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,16 +8,21 @@ 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=
|
||||||
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/posteo/go-agentx v0.3.0 h1:Mqu0qzPHxbyZF3+fKwN2vjW49t6TPPgivjjplcuouNw=
|
||||||
|
github.com/posteo/go-agentx v0.3.0/go.mod h1:YCWL7bzLlpSNeU9vnfEg1pdlllDs1v2mz+pRcg21CUg=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
go.fd.io/govpp v0.12.0 h1:5HnMzsKHSFdxglsFyEhR0g+CzncWiLYXG2NDYgNUrnE=
|
go.fd.io/govpp v0.12.0 h1:5HnMzsKHSFdxglsFyEhR0g+CzncWiLYXG2NDYgNUrnE=
|
||||||
@@ -29,6 +35,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=
|
||||||
485
src/ifmib/ifmib.go
Normal file
485
src/ifmib/ifmib.go
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
package ifmib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/posteo/go-agentx"
|
||||||
|
"github.com/posteo/go-agentx/pdu"
|
||||||
|
"github.com/posteo/go-agentx/value"
|
||||||
|
"go.fd.io/govpp/api"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"govpp-snmp-agentx/logger"
|
||||||
|
"govpp-snmp-agentx/vpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IF-MIB OID bases
|
||||||
|
const ifEntryOID = "1.3.6.1.2.1.2.2.1"
|
||||||
|
const ifXTableOID = "1.3.6.1.2.1.31.1.1.1"
|
||||||
|
|
||||||
|
// VPP Config structures
|
||||||
|
type VPPConfig struct {
|
||||||
|
Interfaces map[string]VPPInterface `yaml:"interfaces"`
|
||||||
|
Loopbacks map[string]VPPInterface `yaml:"loopbacks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VPPInterface struct {
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
SubInterfaces map[string]VPPInterface `yaml:"sub-interfaces"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceMIB struct {
|
||||||
|
mutex sync.RWMutex
|
||||||
|
client *agentx.Client
|
||||||
|
ifEntrySession *agentx.Session
|
||||||
|
ifXTableSession *agentx.Session
|
||||||
|
|
||||||
|
// Simple approach: track interface set and rebuild when it changes
|
||||||
|
currentInterfaces map[uint32]bool
|
||||||
|
descriptions map[string]string
|
||||||
|
interfaceDetails map[uint32]*vpp.InterfaceDetails
|
||||||
|
|
||||||
|
// Counter items - direct references for fast updates
|
||||||
|
counterItems map[string]*agentx.ListItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterfaceMIB() *InterfaceMIB {
|
||||||
|
return &InterfaceMIB{
|
||||||
|
currentInterfaces: make(map[uint32]bool),
|
||||||
|
descriptions: make(map[string]string),
|
||||||
|
interfaceDetails: make(map[uint32]*vpp.InterfaceDetails),
|
||||||
|
counterItems: make(map[string]*agentx.ListItem),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InterfaceMIB) GetHandler() *agentx.ListHandler {
|
||||||
|
// Always create a new handler - this is the key simplification
|
||||||
|
return &agentx.ListHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InterfaceMIB) LoadVPPConfig(configPath string) error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read VPP config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
for subID, subConfig := range ifConfig.SubInterfaces {
|
||||||
|
if subConfig.Description != "" {
|
||||||
|
subIfName := fmt.Sprintf("%s.%s", ifName, subID)
|
||||||
|
m.descriptions[subIfName] = subConfig.Description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ifName, ifConfig := range config.Loopbacks {
|
||||||
|
if ifConfig.Description != "" {
|
||||||
|
m.descriptions[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()
|
||||||
|
|
||||||
|
for _, detail := range details {
|
||||||
|
m.interfaceDetails[uint32(detail.SwIfIndex)] = &detail
|
||||||
|
}
|
||||||
|
logger.Debugf("Updated interface details for %d interfaces", len(details))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
// Check if interface set changed
|
||||||
|
newInterfaces := make(map[uint32]bool)
|
||||||
|
for _, iface := range interfaceStats.Interfaces {
|
||||||
|
newInterfaces[iface.InterfaceIndex] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple comparison - rebuild if different
|
||||||
|
needsRebuild := len(newInterfaces) != len(m.currentInterfaces)
|
||||||
|
if !needsRebuild {
|
||||||
|
for idx := range newInterfaces {
|
||||||
|
if !m.currentInterfaces[idx] {
|
||||||
|
needsRebuild = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsRebuild {
|
||||||
|
logger.Debugf("Interface set changed, rebuilding MIB")
|
||||||
|
if err := m.rebuildMIB(interfaceStats.Interfaces); err != nil {
|
||||||
|
logger.Printf("Failed to rebuild MIB: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.currentInterfaces = newInterfaces
|
||||||
|
} else {
|
||||||
|
// Fast path: just update counters
|
||||||
|
logger.Debugf("Updating counters for %d interfaces", len(interfaceStats.Interfaces))
|
||||||
|
for _, iface := range interfaceStats.Interfaces {
|
||||||
|
m.updateCounterValues(&iface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Printf("Updated IF-MIB data for %d interfaces", len(interfaceStats.Interfaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InterfaceMIB) rebuildMIB(interfaces []api.InterfaceCounters) error {
|
||||||
|
// If no client is available (e.g., during testing), just track interfaces
|
||||||
|
if m.client == nil {
|
||||||
|
logger.Debugf("No AgentX client available, only tracking interface set")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close old sessions
|
||||||
|
if m.ifEntrySession != nil {
|
||||||
|
m.ifEntrySession.Close()
|
||||||
|
m.ifEntrySession = nil
|
||||||
|
}
|
||||||
|
if m.ifXTableSession != nil {
|
||||||
|
m.ifXTableSession.Close()
|
||||||
|
m.ifXTableSession = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create fresh handler
|
||||||
|
handler := &agentx.ListHandler{}
|
||||||
|
m.counterItems = make(map[string]*agentx.ListItem)
|
||||||
|
|
||||||
|
// Build all MIB entries
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
m.addInterfaceToHandler(handler, &iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register new sessions
|
||||||
|
ifEntrySession, err := m.client.Session(value.MustParseOID(ifEntryOID), "ifEntry", handler)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create ifEntry session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ifEntrySession.Register(127, value.MustParseOID(ifEntryOID))
|
||||||
|
if err != nil {
|
||||||
|
ifEntrySession.Close()
|
||||||
|
return fmt.Errorf("failed to register ifEntry: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ifXTableSession, err := m.client.Session(value.MustParseOID(ifXTableOID), "ifXTable", handler)
|
||||||
|
if err != nil {
|
||||||
|
ifEntrySession.Close()
|
||||||
|
return fmt.Errorf("failed to create ifXTable session: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ifXTableSession.Register(127, value.MustParseOID(ifXTableOID))
|
||||||
|
if err != nil {
|
||||||
|
ifEntrySession.Close()
|
||||||
|
ifXTableSession.Close()
|
||||||
|
return fmt.Errorf("failed to register ifXTable: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.ifEntrySession = ifEntrySession
|
||||||
|
m.ifXTableSession = ifXTableSession
|
||||||
|
|
||||||
|
logger.Debugf("Successfully rebuilt MIB with %d interfaces", len(interfaces))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InterfaceMIB) addInterfaceToHandler(handler *agentx.ListHandler, iface *api.InterfaceCounters) {
|
||||||
|
idx := int(iface.InterfaceIndex) + *vpp.IfIndexOffset
|
||||||
|
details := m.interfaceDetails[iface.InterfaceIndex]
|
||||||
|
|
||||||
|
// Add static fields (these don't change frequently)
|
||||||
|
m.addStaticFields(handler, iface, idx, details)
|
||||||
|
|
||||||
|
// Add counter fields and store references for fast updates
|
||||||
|
m.addCounterFields(handler, iface, idx)
|
||||||
|
|
||||||
|
logger.Debugf("Added interface %d (%s) to IF-MIB with SNMP index %d", iface.InterfaceIndex, iface.InterfaceName, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InterfaceMIB) addStaticFields(handler *agentx.ListHandler, iface *api.InterfaceCounters, idx int, details *vpp.InterfaceDetails) {
|
||||||
|
var item *agentx.ListItem
|
||||||
|
|
||||||
|
// ifIndex (.1)
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.1.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeInteger
|
||||||
|
item.Value = int32(idx)
|
||||||
|
|
||||||
|
// ifDescr (.2)
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.2.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeOctetString
|
||||||
|
item.Value = iface.InterfaceName
|
||||||
|
|
||||||
|
// ifType (.3) - Using ethernetCsmacd(6) as default
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.3.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeInteger
|
||||||
|
item.Value = int32(6)
|
||||||
|
|
||||||
|
// ifMtu (.4)
|
||||||
|
mtu := int32(1500)
|
||||||
|
if details != nil {
|
||||||
|
mtu = int32(details.MTU)
|
||||||
|
}
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.4.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeInteger
|
||||||
|
item.Value = mtu
|
||||||
|
|
||||||
|
// ifSpeed (.5) - Only for speeds <= 2.5Gbps
|
||||||
|
if details != nil && details.Speed > 0 && details.Speed <= 2500000000 {
|
||||||
|
item = 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 {
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.5.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeGauge32
|
||||||
|
item.Value = uint32(1000000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ifPhysAddress (.6)
|
||||||
|
macAddr := ""
|
||||||
|
if details != nil && len(details.MacAddress) > 0 {
|
||||||
|
macAddr = string(details.MacAddress)
|
||||||
|
}
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.6.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeOctetString
|
||||||
|
item.Value = macAddr
|
||||||
|
|
||||||
|
// ifAdminStatus (.7)
|
||||||
|
adminStatus := int32(1)
|
||||||
|
if details != nil && !details.AdminStatus {
|
||||||
|
adminStatus = 2
|
||||||
|
}
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.7.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeInteger
|
||||||
|
item.Value = adminStatus
|
||||||
|
|
||||||
|
// ifOperStatus (.8)
|
||||||
|
operStatus := int32(1)
|
||||||
|
if details != nil && !details.OperStatus {
|
||||||
|
operStatus = 2
|
||||||
|
}
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.8.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeInteger
|
||||||
|
item.Value = operStatus
|
||||||
|
|
||||||
|
// ifLastChange (.9)
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.9.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeTimeTicks
|
||||||
|
item.Value = 0 * time.Second
|
||||||
|
|
||||||
|
// ifInUnknownProtos (.15)
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.15.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeCounter32
|
||||||
|
item.Value = uint32(0)
|
||||||
|
|
||||||
|
// ifOutDiscards (.19)
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.19.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeCounter32
|
||||||
|
item.Value = uint32(0)
|
||||||
|
|
||||||
|
// ifOutQLen (.21)
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.21.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeGauge32
|
||||||
|
item.Value = uint32(0)
|
||||||
|
|
||||||
|
// ifXTable static fields
|
||||||
|
// ifName (.1)
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.1.%d", ifXTableOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeOctetString
|
||||||
|
item.Value = iface.InterfaceName
|
||||||
|
|
||||||
|
// ifHighSpeed (.15)
|
||||||
|
speedMbps := uint32(1000)
|
||||||
|
if details != nil && details.Speed > 0 {
|
||||||
|
speedMbps = uint32(details.Speed / 1000000)
|
||||||
|
}
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.15.%d", ifXTableOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeGauge32
|
||||||
|
item.Value = speedMbps
|
||||||
|
|
||||||
|
// ifAlias (.18)
|
||||||
|
item = handler.Add(fmt.Sprintf("%s.18.%d", ifXTableOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeOctetString
|
||||||
|
if desc, exists := m.descriptions[iface.InterfaceName]; exists {
|
||||||
|
item.Value = desc
|
||||||
|
} else {
|
||||||
|
item.Value = iface.InterfaceName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InterfaceMIB) addCounterFields(handler *agentx.ListHandler, iface *api.InterfaceCounters, idx int) {
|
||||||
|
ifIdx := iface.InterfaceIndex
|
||||||
|
|
||||||
|
// ifEntry counters
|
||||||
|
counters := []struct {
|
||||||
|
oid string
|
||||||
|
key string
|
||||||
|
typ pdu.VariableType
|
||||||
|
}{
|
||||||
|
{fmt.Sprintf("%s.10.%d", ifEntryOID, idx), "ifInOctets", pdu.VariableTypeCounter32},
|
||||||
|
{fmt.Sprintf("%s.11.%d", ifEntryOID, idx), "ifInUcastPkts", pdu.VariableTypeCounter32},
|
||||||
|
{fmt.Sprintf("%s.12.%d", ifEntryOID, idx), "ifInNUcastPkts", pdu.VariableTypeCounter32},
|
||||||
|
{fmt.Sprintf("%s.13.%d", ifEntryOID, idx), "ifInDiscards", pdu.VariableTypeCounter32},
|
||||||
|
{fmt.Sprintf("%s.14.%d", ifEntryOID, idx), "ifInErrors", pdu.VariableTypeCounter32},
|
||||||
|
{fmt.Sprintf("%s.16.%d", ifEntryOID, idx), "ifOutOctets", pdu.VariableTypeCounter32},
|
||||||
|
{fmt.Sprintf("%s.17.%d", ifEntryOID, idx), "ifOutUcastPkts", pdu.VariableTypeCounter32},
|
||||||
|
{fmt.Sprintf("%s.18.%d", ifEntryOID, idx), "ifOutNUcastPkts", pdu.VariableTypeCounter32},
|
||||||
|
{fmt.Sprintf("%s.20.%d", ifEntryOID, idx), "ifOutErrors", pdu.VariableTypeCounter32},
|
||||||
|
|
||||||
|
// ifXTable counters
|
||||||
|
{fmt.Sprintf("%s.2.%d", ifXTableOID, idx), "ifInMulticastPkts", pdu.VariableTypeCounter32},
|
||||||
|
{fmt.Sprintf("%s.3.%d", ifXTableOID, idx), "ifInBroadcastPkts", pdu.VariableTypeCounter32},
|
||||||
|
{fmt.Sprintf("%s.4.%d", ifXTableOID, idx), "ifOutMulticastPkts", pdu.VariableTypeCounter32},
|
||||||
|
{fmt.Sprintf("%s.5.%d", ifXTableOID, idx), "ifOutBroadcastPkts", pdu.VariableTypeCounter32},
|
||||||
|
{fmt.Sprintf("%s.6.%d", ifXTableOID, idx), "ifHCInOctets", pdu.VariableTypeCounter64},
|
||||||
|
{fmt.Sprintf("%s.7.%d", ifXTableOID, idx), "ifHCInUcastPkts", pdu.VariableTypeCounter64},
|
||||||
|
{fmt.Sprintf("%s.8.%d", ifXTableOID, idx), "ifHCInMulticastPkts", pdu.VariableTypeCounter64},
|
||||||
|
{fmt.Sprintf("%s.9.%d", ifXTableOID, idx), "ifHCInBroadcastPkts", pdu.VariableTypeCounter64},
|
||||||
|
{fmt.Sprintf("%s.10.%d", ifXTableOID, idx), "ifHCOutOctets", pdu.VariableTypeCounter64},
|
||||||
|
{fmt.Sprintf("%s.11.%d", ifXTableOID, idx), "ifHCOutUcastPkts", pdu.VariableTypeCounter64},
|
||||||
|
{fmt.Sprintf("%s.12.%d", ifXTableOID, idx), "ifHCOutMulticastPkts", pdu.VariableTypeCounter64},
|
||||||
|
{fmt.Sprintf("%s.13.%d", ifXTableOID, idx), "ifHCOutBroadcastPkts", pdu.VariableTypeCounter64},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, counter := range counters {
|
||||||
|
item := handler.Add(counter.oid)
|
||||||
|
item.Type = counter.typ
|
||||||
|
m.counterItems[fmt.Sprintf("%d_%s", ifIdx, counter.key)] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial values
|
||||||
|
m.updateCounterValues(iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InterfaceMIB) updateCounterValues(iface *api.InterfaceCounters) {
|
||||||
|
ifIdx := iface.InterfaceIndex
|
||||||
|
|
||||||
|
// ifEntry counters
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifInOctets", ifIdx)]; item != nil {
|
||||||
|
item.Value = uint32(iface.Rx.Bytes)
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifInUcastPkts", ifIdx)]; item != nil {
|
||||||
|
if iface.RxUnicast.Packets == 0 {
|
||||||
|
item.Value = uint32(iface.Rx.Packets)
|
||||||
|
} else {
|
||||||
|
item.Value = uint32(iface.RxUnicast.Packets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifInNUcastPkts", ifIdx)]; item != nil {
|
||||||
|
item.Value = uint32(iface.RxMulticast.Packets + iface.RxBroadcast.Packets)
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifInDiscards", ifIdx)]; item != nil {
|
||||||
|
item.Value = uint32(iface.Drops)
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifInErrors", ifIdx)]; item != nil {
|
||||||
|
item.Value = uint32(iface.RxErrors)
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifOutOctets", ifIdx)]; item != nil {
|
||||||
|
item.Value = uint32(iface.Tx.Bytes)
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifOutUcastPkts", ifIdx)]; item != nil {
|
||||||
|
if iface.TxUnicast.Packets == 0 {
|
||||||
|
item.Value = uint32(iface.Tx.Packets)
|
||||||
|
} else {
|
||||||
|
item.Value = uint32(iface.TxUnicast.Packets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifOutNUcastPkts", ifIdx)]; item != nil {
|
||||||
|
item.Value = uint32(iface.TxMulticast.Packets + iface.TxBroadcast.Packets)
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifOutErrors", ifIdx)]; item != nil {
|
||||||
|
item.Value = uint32(iface.TxErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ifXTable counters
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifInMulticastPkts", ifIdx)]; item != nil {
|
||||||
|
item.Value = uint32(iface.RxMulticast.Packets)
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifInBroadcastPkts", ifIdx)]; item != nil {
|
||||||
|
item.Value = uint32(iface.RxBroadcast.Packets)
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifOutMulticastPkts", ifIdx)]; item != nil {
|
||||||
|
item.Value = uint32(iface.TxMulticast.Packets)
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifOutBroadcastPkts", ifIdx)]; item != nil {
|
||||||
|
item.Value = uint32(iface.TxBroadcast.Packets)
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifHCInOctets", ifIdx)]; item != nil {
|
||||||
|
item.Value = iface.Rx.Bytes
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifHCInUcastPkts", ifIdx)]; item != nil {
|
||||||
|
if iface.RxUnicast.Packets == 0 {
|
||||||
|
item.Value = iface.Rx.Packets
|
||||||
|
} else {
|
||||||
|
item.Value = iface.RxUnicast.Packets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifHCInMulticastPkts", ifIdx)]; item != nil {
|
||||||
|
item.Value = iface.RxMulticast.Packets
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifHCInBroadcastPkts", ifIdx)]; item != nil {
|
||||||
|
item.Value = iface.RxBroadcast.Packets
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifHCOutOctets", ifIdx)]; item != nil {
|
||||||
|
item.Value = iface.Tx.Bytes
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifHCOutUcastPkts", ifIdx)]; item != nil {
|
||||||
|
if iface.TxUnicast.Packets == 0 {
|
||||||
|
item.Value = iface.Tx.Packets
|
||||||
|
} else {
|
||||||
|
item.Value = iface.TxUnicast.Packets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifHCOutMulticastPkts", ifIdx)]; item != nil {
|
||||||
|
item.Value = iface.TxMulticast.Packets
|
||||||
|
}
|
||||||
|
if item := m.counterItems[fmt.Sprintf("%d_ifHCOutBroadcastPkts", ifIdx)]; item != nil {
|
||||||
|
item.Value = iface.TxBroadcast.Packets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InterfaceMIB) RegisterWithClient(client *agentx.Client) error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
m.client = client
|
||||||
|
// Don't register anything yet - wait for first UpdateStats call
|
||||||
|
logger.Debugf("Stored AgentX client reference")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *InterfaceMIB) Close() {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
if m.ifEntrySession != nil {
|
||||||
|
m.ifEntrySession.Close()
|
||||||
|
m.ifEntrySession = nil
|
||||||
|
}
|
||||||
|
if m.ifXTableSession != nil {
|
||||||
|
m.ifXTableSession.Close()
|
||||||
|
m.ifXTableSession = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
184
src/ifmib/ifmib_test.go
Normal file
184
src/ifmib/ifmib_test.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
// 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")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mib.descriptions == nil {
|
||||||
|
t.Error("Expected descriptions map to be initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mib.interfaceDetails == nil {
|
||||||
|
t.Error("Expected interfaceDetails map to be initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mib.currentInterfaces == nil {
|
||||||
|
t.Error("Expected currentInterfaces map to be initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mib.counterItems == nil {
|
||||||
|
t.Error("Expected counterItems map to be initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mib.currentInterfaces) != 0 {
|
||||||
|
t.Errorf("Expected currentInterfaces map to be empty, got %d entries", len(mib.currentInterfaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
handler1 := mib.GetHandler()
|
||||||
|
handler2 := mib.GetHandler()
|
||||||
|
|
||||||
|
if handler1 == nil {
|
||||||
|
t.Error("GetHandler returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if handler2 == nil {
|
||||||
|
t.Error("GetHandler returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the new implementation, GetHandler always returns a fresh handler
|
||||||
|
if handler1 == handler2 {
|
||||||
|
t.Error("Expected GetHandler to return fresh handlers, but got the same reference")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 interface was registered
|
||||||
|
if len(mib.currentInterfaces) != 1 {
|
||||||
|
t.Errorf("Expected 1 interface in currentInterfaces, got %d", len(mib.currentInterfaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mib.currentInterfaces[0] {
|
||||||
|
t.Error("Expected interface 0 to be registered in currentInterfaces")
|
||||||
|
}
|
||||||
|
}
|
||||||
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()
|
||||||
|
}
|
||||||
80
src/main.go
Normal file
80
src/main.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// 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.2.3-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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create VPP client and managers
|
||||||
|
vppClient := &vpp.VPPClient{}
|
||||||
|
interfaceManager := vpp.NewInterfaceManager(vppClient)
|
||||||
|
statsManager := vpp.NewStatsManager(vppClient, interfaceManager)
|
||||||
|
|
||||||
|
// Set up interface event callback to update interface details
|
||||||
|
interfaceManager.SetEventCallback(interfaceMIB.UpdateInterfaceDetails)
|
||||||
|
|
||||||
|
// Set up stats callback to update MIB
|
||||||
|
statsManager.SetStatsCallback(interfaceMIB.UpdateStats)
|
||||||
|
|
||||||
|
// Start VPP stats routine
|
||||||
|
statsManager.StartStatsRoutine()
|
||||||
|
|
||||||
|
// Start interface event monitoring (handles reconnections automatically)
|
||||||
|
interfaceManager.StartEventMonitoring()
|
||||||
|
|
||||||
|
// 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...")
|
||||||
|
|
||||||
|
// Stop stats routine and interface monitoring, then disconnect
|
||||||
|
statsManager.StopStatsRoutine()
|
||||||
|
interfaceManager.StopEventMonitoring()
|
||||||
|
vppClient.Disconnect()
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
173
src/vpp/vpp.go
Normal file
173
src/vpp/vpp.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
package vpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
"go.fd.io/govpp/adapter/socketclient"
|
||||||
|
"go.fd.io/govpp/adapter/statsclient"
|
||||||
|
"go.fd.io/govpp/api"
|
||||||
|
"go.fd.io/govpp/binapi/vpe"
|
||||||
|
"go.fd.io/govpp/core"
|
||||||
|
|
||||||
|
"govpp-snmp-agentx/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Flags for VPP configuration
|
||||||
|
ApiAddr = flag.String("vppstats.api.addr", "/var/run/vpp/api.sock", "VPP API socket path")
|
||||||
|
StatsAddr = flag.String("vppstats.stats.addr", "/var/run/vpp/stats.sock", "VPP stats socket path")
|
||||||
|
IfIndexOffset = flag.Int("vppstats.ifindex-offset", 1000, "Offset to add to VPP interface indices for SNMP")
|
||||||
|
Period = flag.Int("vppstats.period", 10, "Interval in seconds for querying VPP interface stats")
|
||||||
|
)
|
||||||
|
|
||||||
|
// VPPClient manages VPP connections and provides a unified interface
|
||||||
|
type VPPClient struct {
|
||||||
|
apiConn *core.Connection
|
||||||
|
statsConn *core.StatsConnection
|
||||||
|
connected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect establishes connections to both VPP API and Stats sockets
|
||||||
|
func (c *VPPClient) Connect() error {
|
||||||
|
logger.Debugf("Connecting to VPP (API: %s, Stats: %s)", *ApiAddr, *StatsAddr)
|
||||||
|
|
||||||
|
// Connect to API socket
|
||||||
|
apiConn, err := core.Connect(socketclient.NewVppClient(*ApiAddr))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to stats socket
|
||||||
|
statsClient := statsclient.NewStatsClient(*StatsAddr)
|
||||||
|
statsConn, err := core.ConnectStats(statsClient)
|
||||||
|
if err != nil {
|
||||||
|
// Clean up API connection on stats failure
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Debugf("Recovered from API disconnect during stats error: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
apiConn.Disconnect()
|
||||||
|
}()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.apiConn = apiConn
|
||||||
|
c.statsConn = statsConn
|
||||||
|
c.connected = true
|
||||||
|
|
||||||
|
logger.Printf("Connected to VPP (API: %s, Stats: %s)", *ApiAddr, *StatsAddr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect closes all VPP connections safely
|
||||||
|
func (c *VPPClient) Disconnect() {
|
||||||
|
if c.apiConn != nil {
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Debugf("Recovered from API disconnect panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
c.apiConn.Disconnect()
|
||||||
|
}()
|
||||||
|
c.apiConn = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.statsConn != nil {
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Debugf("Recovered from stats disconnect panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
c.statsConn.Disconnect()
|
||||||
|
}()
|
||||||
|
c.statsConn = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.connected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsConnected returns true if both API and Stats connections are active
|
||||||
|
func (c *VPPClient) IsConnected() bool {
|
||||||
|
return c.connected && c.apiConn != nil && c.statsConn != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAPIConnection returns the API connection for direct use
|
||||||
|
func (c *VPPClient) GetAPIConnection() *core.Connection {
|
||||||
|
return c.apiConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatsConnection returns the stats connection for direct use
|
||||||
|
func (c *VPPClient) GetStatsConnection() *core.StatsConnection {
|
||||||
|
return c.statsConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAPIChannel creates a new API channel from the connection
|
||||||
|
func (c *VPPClient) NewAPIChannel() (api.Channel, error) {
|
||||||
|
if c.apiConn == nil {
|
||||||
|
return nil, &VPPError{Message: "API connection not established"}
|
||||||
|
}
|
||||||
|
return c.apiConn.NewAPIChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckLiveness performs a VPP liveness check using ShowVersion API call
|
||||||
|
func (c *VPPClient) CheckLiveness() bool {
|
||||||
|
if !c.IsConnected() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ch, err := c.NewAPIChannel()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("Failed to create API channel for liveness check: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var channelClosed bool
|
||||||
|
defer func() {
|
||||||
|
if !channelClosed {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Debugf("Recovered from channel close panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ch.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
req := &vpe.ShowVersion{}
|
||||||
|
reply := &vpe.ShowVersionReply{}
|
||||||
|
|
||||||
|
if err := ch.SendRequest(req).ReceiveReply(reply); err != nil {
|
||||||
|
logger.Debugf("VPP ShowVersion failed: %v", err)
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Debugf("Channel already closed during error handling")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ch.Close()
|
||||||
|
channelClosed = true
|
||||||
|
}()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.Close()
|
||||||
|
channelClosed = true
|
||||||
|
|
||||||
|
logger.Debugf("VPP liveness check passed (version: %s)", string(reply.Version))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// VPPError represents a VPP-specific error
|
||||||
|
type VPPError struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *VPPError) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
279
src/vpp/vpp_iface.go
Normal file
279
src/vpp/vpp_iface.go
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
package vpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.fd.io/govpp/api"
|
||||||
|
interfaces "go.fd.io/govpp/binapi/interface"
|
||||||
|
"go.fd.io/govpp/binapi/interface_types"
|
||||||
|
|
||||||
|
"govpp-snmp-agentx/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InterfaceDetails holds detailed information about a VPP interface
|
||||||
|
type InterfaceDetails struct {
|
||||||
|
SwIfIndex interface_types.InterfaceIndex
|
||||||
|
InterfaceName string
|
||||||
|
MacAddress []byte
|
||||||
|
Speed uint64
|
||||||
|
AdminStatus bool
|
||||||
|
OperStatus bool
|
||||||
|
MTU uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceEventCallback is called when interface events occur
|
||||||
|
type InterfaceEventCallback func(details []InterfaceDetails)
|
||||||
|
|
||||||
|
// InterfaceManager handles interface-related VPP operations
|
||||||
|
type InterfaceManager struct {
|
||||||
|
client *VPPClient
|
||||||
|
eventCallback InterfaceEventCallback
|
||||||
|
running bool
|
||||||
|
watchingEvents bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInterfaceManager creates a new interface manager
|
||||||
|
func NewInterfaceManager(client *VPPClient) *InterfaceManager {
|
||||||
|
return &InterfaceManager{
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEventCallback sets the callback for interface events
|
||||||
|
func (im *InterfaceManager) SetEventCallback(callback InterfaceEventCallback) {
|
||||||
|
im.eventCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitializeEventWatching starts event watching and retrieves initial interface details
|
||||||
|
func (im *InterfaceManager) InitializeEventWatching() error {
|
||||||
|
if !im.client.IsConnected() {
|
||||||
|
return &VPPError{Message: "VPP client not connected"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start watching interface events
|
||||||
|
if err := im.StartEventWatcher(); err != nil {
|
||||||
|
logger.Debugf("Failed to start interface event watching: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Interface event watching started")
|
||||||
|
|
||||||
|
// Get initial interface details
|
||||||
|
if details, err := im.GetAllInterfaceDetails(); err != nil {
|
||||||
|
logger.Debugf("Failed to get initial interface details: %v", err)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
logger.Debugf("Retrieved initial interface details for %d interfaces", len(details))
|
||||||
|
if im.eventCallback != nil {
|
||||||
|
im.eventCallback(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
im.watchingEvents = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartEventMonitoring starts continuous monitoring for VPP connection and restarts event watching as needed
|
||||||
|
func (im *InterfaceManager) StartEventMonitoring() {
|
||||||
|
if im.running {
|
||||||
|
logger.Debugf("Interface event monitoring already running")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
im.running = true
|
||||||
|
go im.eventMonitoringRoutine()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopEventMonitoring stops the event monitoring routine
|
||||||
|
func (im *InterfaceManager) StopEventMonitoring() {
|
||||||
|
im.running = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventMonitoringRoutine continuously monitors VPP connection and manages event watching
|
||||||
|
func (im *InterfaceManager) eventMonitoringRoutine() {
|
||||||
|
logger.Debugf("Starting interface event monitoring routine")
|
||||||
|
|
||||||
|
for {
|
||||||
|
if !im.running {
|
||||||
|
logger.Debugf("Interface event monitoring routine stopping")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if im.client.IsConnected() {
|
||||||
|
if !im.watchingEvents {
|
||||||
|
if err := im.InitializeEventWatching(); err != nil {
|
||||||
|
logger.Printf("Failed to initialize interface event watching: %v", err)
|
||||||
|
} else {
|
||||||
|
logger.Printf("Interface event watching started")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if im.watchingEvents {
|
||||||
|
logger.Printf("VPP connection lost, interface event watching will restart on reconnection")
|
||||||
|
im.watchingEvents = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Interface event monitoring routine ended")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllInterfaceDetails retrieves detailed information for all interfaces
|
||||||
|
func (im *InterfaceManager) GetAllInterfaceDetails() ([]InterfaceDetails, error) {
|
||||||
|
if !im.client.IsConnected() {
|
||||||
|
return nil, &VPPError{Message: "VPP client not connected"}
|
||||||
|
}
|
||||||
|
|
||||||
|
ch, err := im.client.NewAPIChannel()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer ch.Close()
|
||||||
|
|
||||||
|
return getAllInterfaceDetails(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartEventWatcher starts watching for interface events
|
||||||
|
func (im *InterfaceManager) StartEventWatcher() error {
|
||||||
|
if !im.client.IsConnected() {
|
||||||
|
return &VPPError{Message: "VPP client not connected"}
|
||||||
|
}
|
||||||
|
|
||||||
|
ch, err := im.client.NewAPIChannel()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return watchInterfaceEvents(ch, im.handleInterfaceEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleInterfaceEvent handles interface events and calls the callback
|
||||||
|
func (im *InterfaceManager) handleInterfaceEvent() {
|
||||||
|
if im.eventCallback != nil {
|
||||||
|
details, err := im.GetAllInterfaceDetails()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("Failed to retrieve interface details after event: %v", err)
|
||||||
|
} else {
|
||||||
|
logger.Debugf("Calling interface event callback with %d interfaces", len(details))
|
||||||
|
im.eventCallback(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAllInterfaceDetails retrieves detailed information for all interfaces (internal function)
|
||||||
|
func getAllInterfaceDetails(ch api.Channel) ([]InterfaceDetails, error) {
|
||||||
|
logger.Debugf("Retrieving all interface details from VPP")
|
||||||
|
|
||||||
|
// Get all interfaces
|
||||||
|
reqCtx := ch.SendMultiRequest(&interfaces.SwInterfaceDump{
|
||||||
|
SwIfIndex: ^interface_types.InterfaceIndex(0), // All interfaces
|
||||||
|
})
|
||||||
|
|
||||||
|
var details []InterfaceDetails
|
||||||
|
|
||||||
|
for {
|
||||||
|
iface := &interfaces.SwInterfaceDetails{}
|
||||||
|
stop, err := reqCtx.ReceiveReply(iface)
|
||||||
|
if stop {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("Error retrieving interface details: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert VPP interface flags to admin/oper status
|
||||||
|
adminUp := (iface.Flags & interface_types.IF_STATUS_API_FLAG_ADMIN_UP) != 0
|
||||||
|
operUp := (iface.Flags & interface_types.IF_STATUS_API_FLAG_LINK_UP) != 0
|
||||||
|
|
||||||
|
detail := InterfaceDetails{
|
||||||
|
SwIfIndex: iface.SwIfIndex,
|
||||||
|
InterfaceName: string(iface.InterfaceName),
|
||||||
|
MacAddress: iface.L2Address[:],
|
||||||
|
Speed: uint64(iface.LinkSpeed) * 1000, // Convert Kbps to bps
|
||||||
|
AdminStatus: adminUp,
|
||||||
|
OperStatus: operUp,
|
||||||
|
MTU: uint32(iface.LinkMtu),
|
||||||
|
}
|
||||||
|
|
||||||
|
details = append(details, detail)
|
||||||
|
|
||||||
|
logger.Debugf("Interface %d (%s): MAC=%x, Speed=%d, Admin=%t, Oper=%t, MTU=%d",
|
||||||
|
detail.SwIfIndex, detail.InterfaceName, detail.MacAddress,
|
||||||
|
detail.Speed, detail.AdminStatus, detail.OperStatus, detail.MTU)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Retrieved details for %d interfaces", len(details))
|
||||||
|
return details, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchInterfaceEvents watches for VPP interface events (internal function)
|
||||||
|
func watchInterfaceEvents(ch api.Channel, callback func()) error {
|
||||||
|
logger.Debugf("WatchInterfaceEvents() called - starting interface event monitoring")
|
||||||
|
|
||||||
|
notifChan := make(chan api.Message, 100)
|
||||||
|
|
||||||
|
// subscribe for specific event message
|
||||||
|
logger.Debugf("Subscribing to interface events...")
|
||||||
|
sub, err := ch.SubscribeNotification(notifChan, &interfaces.SwInterfaceEvent{})
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("error subscribing to interface events: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Debugf("Successfully subscribed to interface events")
|
||||||
|
|
||||||
|
// enable interface events in VPP
|
||||||
|
logger.Debugf("Enabling interface events in VPP...")
|
||||||
|
err = ch.SendRequest(&interfaces.WantInterfaceEvents{
|
||||||
|
PID: uint32(os.Getpid()),
|
||||||
|
EnableDisable: 1,
|
||||||
|
}).ReceiveReply(&interfaces.WantInterfaceEventsReply{})
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("error enabling interface events: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Interface events enabled in VPP, starting event listener goroutine")
|
||||||
|
|
||||||
|
// receive notifications
|
||||||
|
go func() {
|
||||||
|
logger.Debugf("Interface event listener goroutine started")
|
||||||
|
defer func() {
|
||||||
|
logger.Debugf("Interface event listener goroutine shutting down")
|
||||||
|
// disable interface events in VPP
|
||||||
|
err = ch.SendRequest(&interfaces.WantInterfaceEvents{
|
||||||
|
PID: uint32(os.Getpid()),
|
||||||
|
EnableDisable: 0,
|
||||||
|
}).ReceiveReply(&interfaces.WantInterfaceEventsReply{})
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("error disabling interface events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsubscribe from receiving events
|
||||||
|
err = sub.Unsubscribe()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("error unsubscribing from interface events: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger.Debugf("Interface event listener waiting for events...")
|
||||||
|
for notif := range notifChan {
|
||||||
|
e := notif.(*interfaces.SwInterfaceEvent)
|
||||||
|
logger.Printf("interface event: SwIfIndex=%d, Flags=%d, Deleted=%t",
|
||||||
|
e.SwIfIndex, e.Flags, e.Deleted)
|
||||||
|
|
||||||
|
// When an interface event occurs, call the callback
|
||||||
|
if callback != nil {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Debugf("Interface event listener goroutine ended")
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
285
src/vpp/vpp_iface_test.go
Normal file
285
src/vpp/vpp_iface_test.go
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
package vpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.fd.io/govpp/binapi/interface_types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewInterfaceManager(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
manager := NewInterfaceManager(client)
|
||||||
|
|
||||||
|
if manager == nil {
|
||||||
|
t.Fatal("NewInterfaceManager() returned nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager.client != client {
|
||||||
|
t.Error("InterfaceManager should store the provided client")
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager.eventCallback != nil {
|
||||||
|
t.Error("InterfaceManager should have nil callback initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager.running {
|
||||||
|
t.Error("InterfaceManager should not be running initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager.watchingEvents {
|
||||||
|
t.Error("InterfaceManager should not be watching events initially")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterfaceManagerSetEventCallback(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
manager := NewInterfaceManager(client)
|
||||||
|
|
||||||
|
var callbackCalled bool
|
||||||
|
var receivedDetails []InterfaceDetails
|
||||||
|
|
||||||
|
callback := func(details []InterfaceDetails) {
|
||||||
|
callbackCalled = true
|
||||||
|
receivedDetails = details
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetEventCallback(callback)
|
||||||
|
|
||||||
|
if manager.eventCallback == nil {
|
||||||
|
t.Error("SetEventCallback() should store the callback")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test callback execution
|
||||||
|
testDetails := []InterfaceDetails{
|
||||||
|
{
|
||||||
|
SwIfIndex: 1,
|
||||||
|
InterfaceName: "test-interface",
|
||||||
|
MacAddress: []byte{0xde, 0xad, 0xbe, 0xef, 0x00, 0x01},
|
||||||
|
Speed: 1000000000,
|
||||||
|
AdminStatus: true,
|
||||||
|
OperStatus: true,
|
||||||
|
MTU: 1500,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.eventCallback(testDetails)
|
||||||
|
|
||||||
|
if !callbackCalled {
|
||||||
|
t.Error("Callback should have been called")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(receivedDetails) != 1 {
|
||||||
|
t.Errorf("Expected 1 interface detail, got %d", len(receivedDetails))
|
||||||
|
}
|
||||||
|
|
||||||
|
if receivedDetails[0].InterfaceName != "test-interface" {
|
||||||
|
t.Errorf("Expected interface name 'test-interface', got %q", receivedDetails[0].InterfaceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterfaceManagerGetAllInterfaceDetailsWithoutConnection(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
manager := NewInterfaceManager(client)
|
||||||
|
|
||||||
|
_, err := manager.GetAllInterfaceDetails()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetAllInterfaceDetails() should return error when not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
vppErr, ok := err.(*VPPError)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Expected VPPError, got %T", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vppErr.Message != "VPP client not connected" {
|
||||||
|
t.Errorf("Expected specific error message, got: %s", vppErr.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterfaceManagerStartEventWatcherWithoutConnection(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
manager := NewInterfaceManager(client)
|
||||||
|
|
||||||
|
err := manager.StartEventWatcher()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("StartEventWatcher() should return error when not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
vppErr, ok := err.(*VPPError)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Expected VPPError, got %T", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vppErr.Message != "VPP client not connected" {
|
||||||
|
t.Errorf("Expected specific error message, got: %s", vppErr.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterfaceManagerHandleInterfaceEventWithoutCallback(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
manager := NewInterfaceManager(client)
|
||||||
|
|
||||||
|
// Should not panic when callback is nil
|
||||||
|
manager.handleInterfaceEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterfaceManagerInitializeEventWatchingWithoutConnection(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
manager := NewInterfaceManager(client)
|
||||||
|
|
||||||
|
err := manager.InitializeEventWatching()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("InitializeEventWatching() should return error when not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
vppErr, ok := err.(*VPPError)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Expected VPPError, got %T", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vppErr.Message != "VPP client not connected" {
|
||||||
|
t.Errorf("Expected specific error message, got: %s", vppErr.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterfaceDetails(t *testing.T) {
|
||||||
|
details := InterfaceDetails{
|
||||||
|
SwIfIndex: interface_types.InterfaceIndex(42),
|
||||||
|
InterfaceName: "GigabitEthernet0/8/0",
|
||||||
|
MacAddress: []byte{0x02, 0xfe, 0x3c, 0x4d, 0x5e, 0x6f},
|
||||||
|
Speed: 10000000000, // 10 Gbps
|
||||||
|
AdminStatus: true,
|
||||||
|
OperStatus: false,
|
||||||
|
MTU: 9000,
|
||||||
|
}
|
||||||
|
|
||||||
|
if details.SwIfIndex != 42 {
|
||||||
|
t.Errorf("Expected SwIfIndex 42, got %d", details.SwIfIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if details.InterfaceName != "GigabitEthernet0/8/0" {
|
||||||
|
t.Errorf("Expected interface name 'GigabitEthernet0/8/0', got %q", details.InterfaceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(details.MacAddress) != 6 {
|
||||||
|
t.Errorf("Expected MAC address length 6, got %d", len(details.MacAddress))
|
||||||
|
}
|
||||||
|
|
||||||
|
if details.Speed != 10000000000 {
|
||||||
|
t.Errorf("Expected speed 10000000000, got %d", details.Speed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !details.AdminStatus {
|
||||||
|
t.Error("Expected AdminStatus true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if details.OperStatus {
|
||||||
|
t.Error("Expected OperStatus false")
|
||||||
|
}
|
||||||
|
|
||||||
|
if details.MTU != 9000 {
|
||||||
|
t.Errorf("Expected MTU 9000, got %d", details.MTU)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterfaceEventCallback(t *testing.T) {
|
||||||
|
var callbackInvoked bool
|
||||||
|
var callbackDetails []InterfaceDetails
|
||||||
|
|
||||||
|
callback := InterfaceEventCallback(func(details []InterfaceDetails) {
|
||||||
|
callbackInvoked = true
|
||||||
|
callbackDetails = details
|
||||||
|
})
|
||||||
|
|
||||||
|
testDetails := []InterfaceDetails{
|
||||||
|
{SwIfIndex: 1, InterfaceName: "test1"},
|
||||||
|
{SwIfIndex: 2, InterfaceName: "test2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(testDetails)
|
||||||
|
|
||||||
|
if !callbackInvoked {
|
||||||
|
t.Error("Callback should have been invoked")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(callbackDetails) != 2 {
|
||||||
|
t.Errorf("Expected 2 interface details, got %d", len(callbackDetails))
|
||||||
|
}
|
||||||
|
|
||||||
|
if callbackDetails[0].InterfaceName != "test1" {
|
||||||
|
t.Errorf("Expected first interface 'test1', got %q", callbackDetails[0].InterfaceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if callbackDetails[1].InterfaceName != "test2" {
|
||||||
|
t.Errorf("Expected second interface 'test2', got %q", callbackDetails[1].InterfaceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterfaceManagerStartStopEventMonitoring(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
manager := NewInterfaceManager(client)
|
||||||
|
|
||||||
|
if manager.running {
|
||||||
|
t.Error("InterfaceManager should not be running initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.StartEventMonitoring()
|
||||||
|
|
||||||
|
if !manager.running {
|
||||||
|
t.Error("InterfaceManager should be running after StartEventMonitoring()")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test starting again (should be safe)
|
||||||
|
manager.StartEventMonitoring()
|
||||||
|
|
||||||
|
if !manager.running {
|
||||||
|
t.Error("InterfaceManager should still be running after second StartEventMonitoring()")
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.StopEventMonitoring()
|
||||||
|
|
||||||
|
if manager.running {
|
||||||
|
t.Error("InterfaceManager should not be running after StopEventMonitoring()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterfaceManagerEventMonitoringWithConnectionChanges(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
manager := NewInterfaceManager(client)
|
||||||
|
|
||||||
|
// Set a callback to track calls
|
||||||
|
var callbackCount int
|
||||||
|
manager.SetEventCallback(func(details []InterfaceDetails) {
|
||||||
|
callbackCount++
|
||||||
|
})
|
||||||
|
|
||||||
|
manager.StartEventMonitoring()
|
||||||
|
|
||||||
|
// Let it run briefly
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// Simulate VPP connection and disconnection by checking state changes
|
||||||
|
initialWatchingState := manager.watchingEvents
|
||||||
|
|
||||||
|
// Stop monitoring
|
||||||
|
manager.StopEventMonitoring()
|
||||||
|
|
||||||
|
// Verify it stopped
|
||||||
|
if manager.running {
|
||||||
|
t.Error("Event monitoring should have stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The watching state should reflect the connection state
|
||||||
|
if !client.IsConnected() && manager.watchingEvents {
|
||||||
|
t.Error("Should not be watching events when disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state should be false since we're not connected to VPP in tests
|
||||||
|
if initialWatchingState {
|
||||||
|
t.Error("Should not be watching events initially when VPP is not connected")
|
||||||
|
}
|
||||||
|
}
|
||||||
211
src/vpp/vpp_stats.go
Normal file
211
src/vpp/vpp_stats.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
package vpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.fd.io/govpp/api"
|
||||||
|
|
||||||
|
"govpp-snmp-agentx/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatsCallback is called when interface stats are retrieved
|
||||||
|
type StatsCallback func(*api.InterfaceStats)
|
||||||
|
|
||||||
|
// StatsManager handles VPP statistics operations
|
||||||
|
type StatsManager struct {
|
||||||
|
client *VPPClient
|
||||||
|
interfaceManager *InterfaceManager
|
||||||
|
statsCallback StatsCallback
|
||||||
|
period time.Duration
|
||||||
|
running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatsManager creates a new stats manager
|
||||||
|
func NewStatsManager(client *VPPClient, interfaceManager *InterfaceManager) *StatsManager {
|
||||||
|
return &StatsManager{
|
||||||
|
client: client,
|
||||||
|
interfaceManager: interfaceManager,
|
||||||
|
period: time.Duration(*Period) * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStatsCallback sets the callback for stats updates
|
||||||
|
func (sm *StatsManager) SetStatsCallback(callback StatsCallback) {
|
||||||
|
sm.statsCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPeriod sets the polling period for stats
|
||||||
|
func (sm *StatsManager) SetPeriod(period time.Duration) {
|
||||||
|
sm.period = period
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartStatsRoutine starts the stats polling routine
|
||||||
|
func (sm *StatsManager) StartStatsRoutine() {
|
||||||
|
if sm.running {
|
||||||
|
logger.Debugf("Stats routine already running")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.running = true
|
||||||
|
go sm.statsRoutine()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopStatsRoutine stops the stats polling routine
|
||||||
|
func (sm *StatsManager) StopStatsRoutine() {
|
||||||
|
sm.running = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInterfaceStats retrieves current interface statistics
|
||||||
|
func (sm *StatsManager) GetInterfaceStats() (*api.InterfaceStats, error) {
|
||||||
|
if !sm.client.IsConnected() {
|
||||||
|
return nil, &VPPError{Message: "VPP client not connected"}
|
||||||
|
}
|
||||||
|
|
||||||
|
statsConn := sm.client.GetStatsConnection()
|
||||||
|
if statsConn == nil {
|
||||||
|
return nil, &VPPError{Message: "Stats connection not available"}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := new(api.InterfaceStats)
|
||||||
|
if err := statsConn.GetInterfaceStats(stats); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// statsRoutine is the main stats polling loop
|
||||||
|
func (sm *StatsManager) statsRoutine() {
|
||||||
|
logger.Debugf("Starting VPP stats routine with period: %v", sm.period)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(sm.period)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
var wasConnected = false
|
||||||
|
|
||||||
|
for {
|
||||||
|
if !sm.running {
|
||||||
|
logger.Debugf("Stats routine stopping")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we need to connect/reconnect
|
||||||
|
if !sm.client.IsConnected() {
|
||||||
|
if wasConnected {
|
||||||
|
logger.Printf("VPP connection lost, attempting reconnect...")
|
||||||
|
wasConnected = false
|
||||||
|
} else {
|
||||||
|
logger.Printf("VPP not connected, attempting connection...")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sm.client.Connect(); err != nil {
|
||||||
|
logger.Printf("Failed to connect to VPP: %v", err)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Printf("VPP connection established")
|
||||||
|
wasConnected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query stats if connected
|
||||||
|
if sm.client.IsConnected() {
|
||||||
|
if !sm.queryAndReportStats() {
|
||||||
|
logger.Printf("Stats query failed, marking connection as lost")
|
||||||
|
sm.client.Disconnect()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for next tick
|
||||||
|
<-ticker.C
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Stats routine ended")
|
||||||
|
}
|
||||||
|
|
||||||
|
// queryAndReportStats queries stats and calls the callback
|
||||||
|
func (sm *StatsManager) queryAndReportStats() bool {
|
||||||
|
// Check VPP liveness first
|
||||||
|
if !sm.client.CheckLiveness() {
|
||||||
|
logger.Debugf("VPP liveness check failed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get interface stats
|
||||||
|
stats, err := sm.GetInterfaceStats()
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("Failed to get interface stats: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out deleted interfaces by comparing with current interface list
|
||||||
|
filteredStats, err := sm.filterValidInterfaces(stats)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("Failed to filter interface stats: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug log basic info
|
||||||
|
originalCount := len(stats.Interfaces)
|
||||||
|
filteredCount := len(filteredStats.Interfaces)
|
||||||
|
logger.Debugf("Retrieved stats for %d interfaces, filtered to %d valid interfaces", originalCount, filteredCount)
|
||||||
|
|
||||||
|
if originalCount > filteredCount {
|
||||||
|
logger.Debugf("Filtered out %d deleted interfaces", originalCount-filteredCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging for individual interfaces
|
||||||
|
for _, iface := range filteredStats.Interfaces {
|
||||||
|
logger.Debugf("Interface %d (%s): RX %d pkts/%d bytes, TX %d pkts/%d bytes",
|
||||||
|
iface.InterfaceIndex, iface.InterfaceName,
|
||||||
|
iface.Rx.Packets, iface.Rx.Bytes,
|
||||||
|
iface.Tx.Packets, iface.Tx.Bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the callback to update the MIB
|
||||||
|
if sm.statsCallback != nil {
|
||||||
|
sm.statsCallback(filteredStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterValidInterfaces removes stats for deleted interfaces by comparing with current interface list
|
||||||
|
func (sm *StatsManager) filterValidInterfaces(stats *api.InterfaceStats) (*api.InterfaceStats, error) {
|
||||||
|
if sm.interfaceManager == nil {
|
||||||
|
logger.Debugf("No interface manager available, returning unfiltered stats")
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current interface details
|
||||||
|
currentInterfaces, err := sm.interfaceManager.GetAllInterfaceDetails()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("Failed to get current interface list: %v", err)
|
||||||
|
return stats, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map of valid sw_if_index values
|
||||||
|
validInterfaces := make(map[uint32]bool)
|
||||||
|
for _, iface := range currentInterfaces {
|
||||||
|
validInterfaces[uint32(iface.SwIfIndex)] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter the stats to only include valid interfaces
|
||||||
|
filteredStats := &api.InterfaceStats{
|
||||||
|
Interfaces: make([]api.InterfaceCounters, 0, len(stats.Interfaces)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ifaceStat := range stats.Interfaces {
|
||||||
|
if validInterfaces[ifaceStat.InterfaceIndex] {
|
||||||
|
filteredStats.Interfaces = append(filteredStats.Interfaces, ifaceStat)
|
||||||
|
} else {
|
||||||
|
logger.Debugf("Filtering out stats for deleted interface %d (%s)",
|
||||||
|
ifaceStat.InterfaceIndex, ifaceStat.InterfaceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredStats, nil
|
||||||
|
}
|
||||||
247
src/vpp/vpp_stats_test.go
Normal file
247
src/vpp/vpp_stats_test.go
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
package vpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.fd.io/govpp/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewStatsManager(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
interfaceManager := NewInterfaceManager(client)
|
||||||
|
manager := NewStatsManager(client, interfaceManager)
|
||||||
|
|
||||||
|
if manager == nil {
|
||||||
|
t.Fatal("NewStatsManager() returned nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager.client != client {
|
||||||
|
t.Error("StatsManager should store the provided client")
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager.period != time.Duration(*Period)*time.Second {
|
||||||
|
t.Errorf("Expected period %v, got %v", time.Duration(*Period)*time.Second, manager.period)
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager.running {
|
||||||
|
t.Error("StatsManager should not be running initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager.statsCallback != nil {
|
||||||
|
t.Error("StatsManager should have nil callback initially")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatsManagerSetStatsCallback(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
interfaceManager := NewInterfaceManager(client)
|
||||||
|
manager := NewStatsManager(client, interfaceManager)
|
||||||
|
|
||||||
|
var callbackCalled bool
|
||||||
|
var receivedStats *api.InterfaceStats
|
||||||
|
|
||||||
|
callback := func(stats *api.InterfaceStats) {
|
||||||
|
callbackCalled = true
|
||||||
|
receivedStats = stats
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetStatsCallback(callback)
|
||||||
|
|
||||||
|
if manager.statsCallback == nil {
|
||||||
|
t.Error("SetStatsCallback() should store the callback")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test callback execution
|
||||||
|
testStats := &api.InterfaceStats{
|
||||||
|
Interfaces: []api.InterfaceCounters{
|
||||||
|
{
|
||||||
|
InterfaceIndex: 1,
|
||||||
|
InterfaceName: "test-interface",
|
||||||
|
Rx: api.InterfaceCounterCombined{Packets: 100, Bytes: 1500},
|
||||||
|
Tx: api.InterfaceCounterCombined{Packets: 50, Bytes: 750},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.statsCallback(testStats)
|
||||||
|
|
||||||
|
if !callbackCalled {
|
||||||
|
t.Error("Callback should have been called")
|
||||||
|
}
|
||||||
|
|
||||||
|
if receivedStats != testStats {
|
||||||
|
t.Error("Callback should receive the same stats object")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(receivedStats.Interfaces) != 1 {
|
||||||
|
t.Errorf("Expected 1 interface, got %d", len(receivedStats.Interfaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
if receivedStats.Interfaces[0].InterfaceName != "test-interface" {
|
||||||
|
t.Errorf("Expected interface name 'test-interface', got %q", receivedStats.Interfaces[0].InterfaceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatsManagerSetPeriod(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
interfaceManager := NewInterfaceManager(client)
|
||||||
|
manager := NewStatsManager(client, interfaceManager)
|
||||||
|
|
||||||
|
newPeriod := 5 * time.Second
|
||||||
|
manager.SetPeriod(newPeriod)
|
||||||
|
|
||||||
|
if manager.period != newPeriod {
|
||||||
|
t.Errorf("Expected period %v, got %v", newPeriod, manager.period)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatsManagerStartStopStatsRoutine(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
interfaceManager := NewInterfaceManager(client)
|
||||||
|
manager := NewStatsManager(client, interfaceManager)
|
||||||
|
|
||||||
|
if manager.running {
|
||||||
|
t.Error("StatsManager should not be running initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.StartStatsRoutine()
|
||||||
|
|
||||||
|
if !manager.running {
|
||||||
|
t.Error("StatsManager should be running after StartStatsRoutine()")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test starting again (should be safe)
|
||||||
|
manager.StartStatsRoutine()
|
||||||
|
|
||||||
|
if !manager.running {
|
||||||
|
t.Error("StatsManager should still be running after second StartStatsRoutine()")
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.StopStatsRoutine()
|
||||||
|
|
||||||
|
if manager.running {
|
||||||
|
t.Error("StatsManager should not be running after StopStatsRoutine()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatsManagerGetInterfaceStatsWithoutConnection(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
interfaceManager := NewInterfaceManager(client)
|
||||||
|
manager := NewStatsManager(client, interfaceManager)
|
||||||
|
|
||||||
|
_, err := manager.GetInterfaceStats()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("GetInterfaceStats() should return error when not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
vppErr, ok := err.(*VPPError)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Expected VPPError, got %T", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vppErr.Message != "VPP client not connected" {
|
||||||
|
t.Errorf("Expected specific error message, got: %s", vppErr.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatsCallback(t *testing.T) {
|
||||||
|
var callbackInvoked bool
|
||||||
|
var callbackStats *api.InterfaceStats
|
||||||
|
|
||||||
|
callback := StatsCallback(func(stats *api.InterfaceStats) {
|
||||||
|
callbackInvoked = true
|
||||||
|
callbackStats = stats
|
||||||
|
})
|
||||||
|
|
||||||
|
testStats := &api.InterfaceStats{
|
||||||
|
Interfaces: []api.InterfaceCounters{
|
||||||
|
{
|
||||||
|
InterfaceIndex: 42,
|
||||||
|
InterfaceName: "test-callback-interface",
|
||||||
|
Rx: api.InterfaceCounterCombined{Packets: 200, Bytes: 3000},
|
||||||
|
Tx: api.InterfaceCounterCombined{Packets: 100, Bytes: 1500},
|
||||||
|
RxUnicast: api.InterfaceCounterCombined{Packets: 180, Bytes: 2700},
|
||||||
|
TxUnicast: api.InterfaceCounterCombined{Packets: 90, Bytes: 1350},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(testStats)
|
||||||
|
|
||||||
|
if !callbackInvoked {
|
||||||
|
t.Error("Callback should have been invoked")
|
||||||
|
}
|
||||||
|
|
||||||
|
if callbackStats != testStats {
|
||||||
|
t.Error("Callback should receive the same stats object")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(callbackStats.Interfaces) != 1 {
|
||||||
|
t.Errorf("Expected 1 interface, got %d", len(callbackStats.Interfaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
iface := callbackStats.Interfaces[0]
|
||||||
|
if iface.InterfaceIndex != 42 {
|
||||||
|
t.Errorf("Expected interface index 42, got %d", iface.InterfaceIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if iface.InterfaceName != "test-callback-interface" {
|
||||||
|
t.Errorf("Expected interface name 'test-callback-interface', got %q", iface.InterfaceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if iface.Rx.Packets != 200 {
|
||||||
|
t.Errorf("Expected RX packets 200, got %d", iface.Rx.Packets)
|
||||||
|
}
|
||||||
|
|
||||||
|
if iface.Tx.Bytes != 1500 {
|
||||||
|
t.Errorf("Expected TX bytes 1500, got %d", iface.Tx.Bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if iface.RxUnicast.Packets != 180 {
|
||||||
|
t.Errorf("Expected RX unicast packets 180, got %d", iface.RxUnicast.Packets)
|
||||||
|
}
|
||||||
|
|
||||||
|
if iface.TxUnicast.Bytes != 1350 {
|
||||||
|
t.Errorf("Expected TX unicast bytes 1350, got %d", iface.TxUnicast.Bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatsManagerQueryAndReportStatsWithoutConnection(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
interfaceManager := NewInterfaceManager(client)
|
||||||
|
manager := NewStatsManager(client, interfaceManager)
|
||||||
|
|
||||||
|
// Should return false when not connected
|
||||||
|
if manager.queryAndReportStats() {
|
||||||
|
t.Error("queryAndReportStats() should return false when not connected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatsManagerWithShortPeriod(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
interfaceManager := NewInterfaceManager(client)
|
||||||
|
manager := NewStatsManager(client, interfaceManager)
|
||||||
|
|
||||||
|
// Set a very short period for testing
|
||||||
|
manager.SetPeriod(10 * time.Millisecond)
|
||||||
|
|
||||||
|
if manager.period != 10*time.Millisecond {
|
||||||
|
t.Errorf("Expected period 10ms, got %v", manager.period)
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.StartStatsRoutine()
|
||||||
|
|
||||||
|
// Let it run briefly
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
manager.StopStatsRoutine()
|
||||||
|
|
||||||
|
// Should stop gracefully
|
||||||
|
if manager.running {
|
||||||
|
t.Error("StatsManager should have stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
100
src/vpp/vpp_test.go
Normal file
100
src/vpp/vpp_test.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
package vpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewVPPClient(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
t.Fatal("NewVPPClient() returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.IsConnected() {
|
||||||
|
t.Error("NewVPPClient() should return disconnected client")
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.GetAPIConnection() != nil {
|
||||||
|
t.Error("NewVPPClient() should have nil API connection initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.GetStatsConnection() != nil {
|
||||||
|
t.Error("NewVPPClient() should have nil stats connection initially")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVPPClientDisconnect(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
|
||||||
|
// Should be safe to call disconnect on unconnected client
|
||||||
|
client.Disconnect()
|
||||||
|
|
||||||
|
if client.IsConnected() {
|
||||||
|
t.Error("Client should not be connected after Disconnect()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVPPClientNewAPIChannelWithoutConnection(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
|
||||||
|
_, err := client.NewAPIChannel()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("NewAPIChannel() should return error when not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
vppErr, ok := err.(*VPPError)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Expected VPPError, got %T", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if vppErr.Message != "API connection not established" {
|
||||||
|
t.Errorf("Expected specific error message, got: %s", vppErr.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVPPClientCheckLivenessWithoutConnection(t *testing.T) {
|
||||||
|
client := &VPPClient{}
|
||||||
|
|
||||||
|
if client.CheckLiveness() {
|
||||||
|
t.Error("CheckLiveness() should return false when not connected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVPPError(t *testing.T) {
|
||||||
|
err := &VPPError{Message: "test error"}
|
||||||
|
|
||||||
|
if err.Error() != "test error" {
|
||||||
|
t.Errorf("VPPError.Error() returned %q, expected %q", err.Error(), "test error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVPPClientConnectWithInvalidPaths(t *testing.T) {
|
||||||
|
// Save original values
|
||||||
|
origApiAddr := *ApiAddr
|
||||||
|
origStatsAddr := *StatsAddr
|
||||||
|
|
||||||
|
// Set invalid paths
|
||||||
|
*ApiAddr = "/tmp/nonexistent_api.sock"
|
||||||
|
*StatsAddr = "/tmp/nonexistent_stats.sock"
|
||||||
|
|
||||||
|
// Restore original values after test
|
||||||
|
defer func() {
|
||||||
|
*ApiAddr = origApiAddr
|
||||||
|
*StatsAddr = origStatsAddr
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := &VPPClient{}
|
||||||
|
err := client.Connect()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Connect() should fail with invalid socket paths")
|
||||||
|
client.Disconnect() // Clean up if somehow it connected
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.IsConnected() {
|
||||||
|
t.Error("Client should not be connected after failed Connect()")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,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