Compare commits
7 Commits
5533ab00de
...
main
Author | SHA1 | Date | |
---|---|---|---|
0d19d50d62 | |||
686bbe46b0 | |||
ccc2b5ad4d | |||
4f368e625d | |||
35165b0464 | |||
42dbbded3d | |||
f16a2b41ea |
11
Makefile
11
Makefile
@ -1,6 +1,6 @@
|
|||||||
PROG = govpp-snmp-agentx
|
PROG = govpp-snmp-agentx
|
||||||
|
|
||||||
.PHONY: build test clean pkg-deb
|
.PHONY: build test clean pkg-deb sync-version
|
||||||
|
|
||||||
# Build the binary
|
# Build the binary
|
||||||
build:
|
build:
|
||||||
@ -17,6 +17,13 @@ clean:
|
|||||||
rm -rf debian/.debhelper debian/.gocache debian/go debian/$(PROG) debian/files debian/*.substvars debian/debhelper-build-stamp
|
rm -rf debian/.debhelper debian/.gocache debian/go debian/$(PROG) debian/files debian/*.substvars debian/debhelper-build-stamp
|
||||||
rm -f ../$(PROG)_*.deb ../$(PROG)_*.changes ../$(PROG)_*.buildinfo
|
rm -f ../$(PROG)_*.deb ../$(PROG)_*.changes ../$(PROG)_*.buildinfo
|
||||||
|
|
||||||
|
# Sync version from debian/changelog to main.go
|
||||||
|
sync-version:
|
||||||
|
@echo "Syncing version from debian/changelog to main.go..."
|
||||||
|
@VERSION=$$(head -1 debian/changelog | sed -n 's/.*(\([^)]*\)).*/\1/p'); \
|
||||||
|
sed -i 's/^const Version = ".*"/const Version = "'"$$VERSION"'"/' src/main.go; \
|
||||||
|
echo "Updated Version const to: $$VERSION"
|
||||||
|
|
||||||
# Build Debian package
|
# Build Debian package
|
||||||
pkg-deb:
|
pkg-deb: sync-version
|
||||||
fakeroot dpkg-buildpackage -us -uc -b
|
fakeroot dpkg-buildpackage -us -uc -b
|
||||||
|
38
debian/changelog
vendored
38
debian/changelog
vendored
@ -1,3 +1,41 @@
|
|||||||
|
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
|
govpp-snmp-agentx (1.0.2-1) bookworm; urgency=medium
|
||||||
|
|
||||||
* Reorganize source code into src/ subdirectory for cleaner project structure
|
* Reorganize source code into src/ subdirectory for cleaner project structure
|
||||||
|
4
debian/control
vendored
4
debian/control
vendored
@ -2,7 +2,7 @@ Source: govpp-snmp-agentx
|
|||||||
Section: net
|
Section: net
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Maintainer: Pim van Pelt <pim@ipng.ch>
|
Maintainer: Pim van Pelt <pim@ipng.ch>
|
||||||
Build-Depends: debhelper-compat (= 13), golang-go (>= 1.21)
|
Build-Depends: debhelper-compat (= 13), golang-go (>= 1.23.8)
|
||||||
Standards-Version: 4.6.2
|
Standards-Version: 4.6.2
|
||||||
Homepage: https://git.ipng.ch/ipng/govpp-agentx-snmp
|
Homepage: https://git.ipng.ch/ipng/govpp-agentx-snmp
|
||||||
Vcs-Git: https://git.ipng.ch/ipng/govpp-agentx-snmp
|
Vcs-Git: https://git.ipng.ch/ipng/govpp-agentx-snmp
|
||||||
@ -10,7 +10,7 @@ Vcs-Browser: https://git.ipng.ch/ipng/govpp-agentx-snmp
|
|||||||
|
|
||||||
Package: govpp-snmp-agentx
|
Package: govpp-snmp-agentx
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Depends: ${misc:Depends}, ${shlibs:Depends}, snmp, snmpd, adduser
|
Depends: ${misc:Depends}, ${shlibs:Depends}, snmpd
|
||||||
Description: GoVPP SNMP AgentX Daemon
|
Description: GoVPP SNMP AgentX Daemon
|
||||||
A SNMP AgentX daemon that provides SNMP access to VPP (Vector Packet Processing)
|
A SNMP AgentX daemon that provides SNMP access to VPP (Vector Packet Processing)
|
||||||
statistics and interface information. This daemon acts as a subagent that
|
statistics and interface information. This daemon acts as a subagent that
|
||||||
|
@ -244,6 +244,62 @@ snmpwalk -v2c -c public localhost 1.3.6.1.2.1.1
|
|||||||
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.31.1.1.1
|
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
|
## License
|
||||||
|
|
||||||
This project uses the LGPL 3.0 licensed go-agentx library. It has been modified due to a bug,
|
This project uses the LGPL 3.0 licensed go-agentx library. It has been modified due to a bug,
|
||||||
|
@ -2,8 +2,6 @@ module govpp-snmp-agentx
|
|||||||
|
|
||||||
go 1.23.8
|
go 1.23.8
|
||||||
|
|
||||||
toolchain go1.23.10
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/posteo/go-agentx v0.2.1
|
github.com/posteo/go-agentx v0.2.1
|
||||||
go.fd.io/govpp v0.12.0
|
go.fd.io/govpp v0.12.0
|
||||||
|
@ -15,7 +15,7 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"govpp-snmp-agentx/logger"
|
"govpp-snmp-agentx/logger"
|
||||||
"govpp-snmp-agentx/vppstats"
|
"govpp-snmp-agentx/vpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IF-MIB OID bases:
|
// IF-MIB OID bases:
|
||||||
@ -60,6 +60,7 @@ import (
|
|||||||
// ifHCOutUcastPkts .11 - Counter64
|
// ifHCOutUcastPkts .11 - Counter64
|
||||||
// ifHCOutMulticastPkts .12 - Counter64
|
// ifHCOutMulticastPkts .12 - Counter64
|
||||||
// ifHCOutBroadcastPkts .13 - Counter64
|
// ifHCOutBroadcastPkts .13 - Counter64
|
||||||
|
// ifHighSpeed .15 - Gauge32 (interface speed in Mbps)
|
||||||
// ifAlias .18 - DisplayString
|
// ifAlias .18 - DisplayString
|
||||||
|
|
||||||
const ifEntryOID = "1.3.6.1.2.1.2.2.1"
|
const ifEntryOID = "1.3.6.1.2.1.2.2.1"
|
||||||
@ -72,24 +73,26 @@ type VPPConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type VPPInterface struct {
|
type VPPInterface struct {
|
||||||
Description string `yaml:"description"`
|
Description string `yaml:"description"`
|
||||||
SubInterfaces map[string]VPPInterface `yaml:"sub-interfaces"`
|
SubInterfaces map[string]VPPInterface `yaml:"sub-interfaces"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceMIB struct {
|
type InterfaceMIB struct {
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
handler *agentx.ListHandler
|
handler *agentx.ListHandler
|
||||||
ifEntrySession *agentx.Session
|
ifEntrySession *agentx.Session
|
||||||
ifXTableSession *agentx.Session
|
ifXTableSession *agentx.Session
|
||||||
stats map[uint32]*api.InterfaceCounters // indexed by interface index
|
stats map[uint32]*api.InterfaceCounters // indexed by interface index
|
||||||
descriptions map[string]string // interface name -> description mapping
|
descriptions map[string]string // interface name -> description mapping
|
||||||
|
interfaceDetails map[uint32]*vpp.InterfaceDetails // indexed by interface index
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInterfaceMIB() *InterfaceMIB {
|
func NewInterfaceMIB() *InterfaceMIB {
|
||||||
return &InterfaceMIB{
|
return &InterfaceMIB{
|
||||||
handler: &agentx.ListHandler{},
|
handler: &agentx.ListHandler{},
|
||||||
stats: make(map[uint32]*api.InterfaceCounters),
|
stats: make(map[uint32]*api.InterfaceCounters),
|
||||||
descriptions: make(map[string]string),
|
descriptions: make(map[string]string),
|
||||||
|
interfaceDetails: make(map[uint32]*vpp.InterfaceDetails),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +145,22 @@ func (m *InterfaceMIB) LoadVPPConfig(configPath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *InterfaceMIB) UpdateInterfaceDetails(details []vpp.InterfaceDetails) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
logger.Debugf("Updating interface details for %d interfaces", len(details))
|
||||||
|
|
||||||
|
// Update interface details map
|
||||||
|
for _, detail := range details {
|
||||||
|
m.interfaceDetails[uint32(detail.SwIfIndex)] = &detail
|
||||||
|
logger.Debugf("Updated details for interface %d (%s): MAC=%x, Speed=%d",
|
||||||
|
detail.SwIfIndex, detail.InterfaceName, detail.MacAddress, detail.Speed)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Interface details updated for %d interfaces", len(details))
|
||||||
|
}
|
||||||
|
|
||||||
func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
|
func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
@ -172,7 +191,7 @@ func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) {
|
func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) {
|
||||||
idx := int(iface.InterfaceIndex) + *vppstats.IfIndexOffset
|
idx := int(iface.InterfaceIndex) + *vpp.IfIndexOffset
|
||||||
|
|
||||||
// Add ifEntry (classic interface table) entries
|
// Add ifEntry (classic interface table) entries
|
||||||
m.addIfEntry(iface, idx)
|
m.addIfEntry(iface, idx)
|
||||||
@ -186,6 +205,9 @@ func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) {
|
|||||||
func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
|
func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
|
||||||
var item *agentx.ListItem
|
var item *agentx.ListItem
|
||||||
|
|
||||||
|
// Get interface details if available
|
||||||
|
details := m.interfaceDetails[iface.InterfaceIndex]
|
||||||
|
|
||||||
// ifIndex (.1)
|
// ifIndex (.1)
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.1.%d", ifEntryOID, idx))
|
item = m.handler.Add(fmt.Sprintf("%s.1.%d", ifEntryOID, idx))
|
||||||
item.Type = pdu.VariableTypeInteger
|
item.Type = pdu.VariableTypeInteger
|
||||||
@ -201,30 +223,63 @@ func (m *InterfaceMIB) addIfEntry(iface *api.InterfaceCounters, idx int) {
|
|||||||
item.Type = pdu.VariableTypeInteger
|
item.Type = pdu.VariableTypeInteger
|
||||||
item.Value = int32(6)
|
item.Value = int32(6)
|
||||||
|
|
||||||
// ifMtu (.4) - Default MTU 1500
|
// ifMtu (.4) - Use real MTU if available, otherwise default to 1500
|
||||||
|
mtu := int32(1500)
|
||||||
|
if details != nil {
|
||||||
|
mtu = int32(details.MTU)
|
||||||
|
}
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.4.%d", ifEntryOID, idx))
|
item = m.handler.Add(fmt.Sprintf("%s.4.%d", ifEntryOID, idx))
|
||||||
item.Type = pdu.VariableTypeInteger
|
item.Type = pdu.VariableTypeInteger
|
||||||
item.Value = int32(1500)
|
item.Value = mtu
|
||||||
|
|
||||||
// ifSpeed (.5) - Default to 1Gbps (1000000000 bits/sec)
|
// ifSpeed (.5) - Only populate for speeds <= 2.5Gbps (legacy field limitation)
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifEntryOID, idx))
|
if details != nil && details.Speed > 0 && details.Speed <= 2500000000 {
|
||||||
item.Type = pdu.VariableTypeGauge32
|
// Use real speed for interfaces <= 2.5Gbps
|
||||||
item.Value = uint32(1000000000)
|
item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeGauge32
|
||||||
|
item.Value = uint32(details.Speed)
|
||||||
|
} else if details == nil || details.Speed == 0 {
|
||||||
|
// Default to 1Gbps when speed is unknown
|
||||||
|
item = m.handler.Add(fmt.Sprintf("%s.5.%d", ifEntryOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeGauge32
|
||||||
|
item.Value = uint32(1000000000)
|
||||||
|
}
|
||||||
|
// For speeds > 2.5Gbps, don't populate ifSpeed field at all
|
||||||
|
|
||||||
// ifPhysAddress (.6) - Empty for now
|
// ifPhysAddress (.6) - Use real MAC address if available
|
||||||
|
macAddr := ""
|
||||||
|
if details != nil && len(details.MacAddress) > 0 {
|
||||||
|
macAddr = string(details.MacAddress)
|
||||||
|
}
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.6.%d", ifEntryOID, idx))
|
item = m.handler.Add(fmt.Sprintf("%s.6.%d", ifEntryOID, idx))
|
||||||
item.Type = pdu.VariableTypeOctetString
|
item.Type = pdu.VariableTypeOctetString
|
||||||
item.Value = ""
|
item.Value = macAddr
|
||||||
|
|
||||||
// ifAdminStatus (.7) - up(1)
|
// ifAdminStatus (.7) - Use real admin status if available
|
||||||
|
adminStatus := int32(1) // default up
|
||||||
|
if details != nil {
|
||||||
|
if details.AdminStatus {
|
||||||
|
adminStatus = 1 // up
|
||||||
|
} else {
|
||||||
|
adminStatus = 2 // down
|
||||||
|
}
|
||||||
|
}
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifEntryOID, idx))
|
item = m.handler.Add(fmt.Sprintf("%s.7.%d", ifEntryOID, idx))
|
||||||
item.Type = pdu.VariableTypeInteger
|
item.Type = pdu.VariableTypeInteger
|
||||||
item.Value = int32(1)
|
item.Value = adminStatus
|
||||||
|
|
||||||
// ifOperStatus (.8) - up(1)
|
// ifOperStatus (.8) - Use real operational status if available
|
||||||
|
operStatus := int32(1) // default up
|
||||||
|
if details != nil {
|
||||||
|
if details.OperStatus {
|
||||||
|
operStatus = 1 // up
|
||||||
|
} else {
|
||||||
|
operStatus = 2 // down
|
||||||
|
}
|
||||||
|
}
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifEntryOID, idx))
|
item = m.handler.Add(fmt.Sprintf("%s.8.%d", ifEntryOID, idx))
|
||||||
item.Type = pdu.VariableTypeInteger
|
item.Type = pdu.VariableTypeInteger
|
||||||
item.Value = int32(1)
|
item.Value = operStatus
|
||||||
|
|
||||||
// ifLastChange (.9) - 0 (unknown)
|
// ifLastChange (.9) - 0 (unknown)
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.9.%d", ifEntryOID, idx))
|
item = m.handler.Add(fmt.Sprintf("%s.9.%d", ifEntryOID, idx))
|
||||||
@ -362,6 +417,16 @@ func (m *InterfaceMIB) addIfXTable(iface *api.InterfaceCounters, idx int) {
|
|||||||
item.Type = pdu.VariableTypeCounter64
|
item.Type = pdu.VariableTypeCounter64
|
||||||
item.Value = iface.TxBroadcast.Packets
|
item.Value = iface.TxBroadcast.Packets
|
||||||
|
|
||||||
|
// ifHighSpeed (.15) - Interface speed in Megabits per second
|
||||||
|
details := m.interfaceDetails[iface.InterfaceIndex]
|
||||||
|
speedMbps := uint32(1000) // default 1 Gbps = 1000 Mbps
|
||||||
|
if details != nil && details.Speed > 0 {
|
||||||
|
speedMbps = uint32(details.Speed / 1000000) // Convert bps to Mbps
|
||||||
|
}
|
||||||
|
item = m.handler.Add(fmt.Sprintf("%s.15.%d", ifXTableOID, idx))
|
||||||
|
item.Type = pdu.VariableTypeGauge32
|
||||||
|
item.Value = speedMbps
|
||||||
|
|
||||||
// ifAlias (.18) - Interface description/alias
|
// ifAlias (.18) - Interface description/alias
|
||||||
item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifXTableOID, idx))
|
item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifXTableOID, idx))
|
||||||
item.Type = pdu.VariableTypeOctetString
|
item.Type = pdu.VariableTypeOctetString
|
||||||
|
12
src/main.go
12
src/main.go
@ -13,9 +13,11 @@ import (
|
|||||||
"govpp-snmp-agentx/config"
|
"govpp-snmp-agentx/config"
|
||||||
"govpp-snmp-agentx/ifmib"
|
"govpp-snmp-agentx/ifmib"
|
||||||
"govpp-snmp-agentx/logger"
|
"govpp-snmp-agentx/logger"
|
||||||
"govpp-snmp-agentx/vppstats"
|
"govpp-snmp-agentx/vpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const Version = "1.1.2-1"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
debug := flag.Bool("debug", false, "Enable debug logging")
|
debug := flag.Bool("debug", false, "Enable debug logging")
|
||||||
vppcfg := flag.String("vppcfg", "", "VPP configuration YAML file to read interface descriptions from")
|
vppcfg := flag.String("vppcfg", "", "VPP configuration YAML file to read interface descriptions from")
|
||||||
@ -24,6 +26,9 @@ func main() {
|
|||||||
// Set global debug flag
|
// Set global debug flag
|
||||||
config.Debug = *debug
|
config.Debug = *debug
|
||||||
|
|
||||||
|
// Log startup message with version
|
||||||
|
logger.Printf("Starting govpp-snmp-agentx version %s", Version)
|
||||||
|
|
||||||
// Create the interface MIB
|
// Create the interface MIB
|
||||||
interfaceMIB := ifmib.NewInterfaceMIB()
|
interfaceMIB := ifmib.NewInterfaceMIB()
|
||||||
|
|
||||||
@ -40,8 +45,11 @@ func main() {
|
|||||||
log.Fatalf("Failed to start AgentX: %v", err)
|
log.Fatalf("Failed to start AgentX: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up interface event callback to update interface details
|
||||||
|
vpp.SetInterfaceEventCallback(interfaceMIB.UpdateInterfaceDetails)
|
||||||
|
|
||||||
// Start VPP stats routine with callback to update MIB
|
// Start VPP stats routine with callback to update MIB
|
||||||
vppstats.StartStatsRoutine(interfaceMIB.UpdateStats)
|
vpp.StartStatsRoutine(interfaceMIB.UpdateStats)
|
||||||
|
|
||||||
// Set up signal handling for graceful shutdown
|
// Set up signal handling for graceful shutdown
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
145
src/vpp/vpp_iface.go
Normal file
145
src/vpp/vpp_iface.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
|
package vpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"go.fd.io/govpp/api"
|
||||||
|
interfaces "go.fd.io/govpp/binapi/interface"
|
||||||
|
"go.fd.io/govpp/binapi/interface_types"
|
||||||
|
|
||||||
|
"govpp-snmp-agentx/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InterfaceDetails holds detailed information about a VPP interface
|
||||||
|
type InterfaceDetails struct {
|
||||||
|
SwIfIndex interface_types.InterfaceIndex
|
||||||
|
InterfaceName string
|
||||||
|
MacAddress []byte
|
||||||
|
Speed uint64
|
||||||
|
AdminStatus bool
|
||||||
|
OperStatus bool
|
||||||
|
MTU uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceEventCallback is called when interface events occur
|
||||||
|
type InterfaceEventCallback func(details []InterfaceDetails)
|
||||||
|
|
||||||
|
// GetAllInterfaceDetails retrieves detailed information for all interfaces
|
||||||
|
func GetAllInterfaceDetails(ch api.Channel) ([]InterfaceDetails, error) {
|
||||||
|
logger.Debugf("Retrieving all interface details from VPP")
|
||||||
|
|
||||||
|
// Get all interfaces
|
||||||
|
reqCtx := ch.SendMultiRequest(&interfaces.SwInterfaceDump{
|
||||||
|
SwIfIndex: ^interface_types.InterfaceIndex(0), // All interfaces
|
||||||
|
})
|
||||||
|
|
||||||
|
var details []InterfaceDetails
|
||||||
|
|
||||||
|
for {
|
||||||
|
iface := &interfaces.SwInterfaceDetails{}
|
||||||
|
stop, err := reqCtx.ReceiveReply(iface)
|
||||||
|
if stop {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("Error retrieving interface details: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert VPP interface flags to admin/oper status
|
||||||
|
adminUp := (iface.Flags & interface_types.IF_STATUS_API_FLAG_ADMIN_UP) != 0
|
||||||
|
operUp := (iface.Flags & interface_types.IF_STATUS_API_FLAG_LINK_UP) != 0
|
||||||
|
|
||||||
|
detail := InterfaceDetails{
|
||||||
|
SwIfIndex: iface.SwIfIndex,
|
||||||
|
InterfaceName: string(iface.InterfaceName),
|
||||||
|
MacAddress: iface.L2Address[:],
|
||||||
|
Speed: uint64(iface.LinkSpeed) * 1000, // Convert Kbps to bps
|
||||||
|
AdminStatus: adminUp,
|
||||||
|
OperStatus: operUp,
|
||||||
|
MTU: uint32(iface.LinkMtu),
|
||||||
|
}
|
||||||
|
|
||||||
|
details = append(details, detail)
|
||||||
|
|
||||||
|
logger.Debugf("Interface %d (%s): MAC=%x, Speed=%d, Admin=%t, Oper=%t, MTU=%d",
|
||||||
|
detail.SwIfIndex, detail.InterfaceName, detail.MacAddress,
|
||||||
|
detail.Speed, detail.AdminStatus, detail.OperStatus, detail.MTU)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Retrieved details for %d interfaces", len(details))
|
||||||
|
return details, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WatchInterfaceEvents(ch api.Channel, callback InterfaceEventCallback) error {
|
||||||
|
logger.Debugf("WatchInterfaceEvents() called - starting interface event monitoring")
|
||||||
|
|
||||||
|
notifChan := make(chan api.Message, 100)
|
||||||
|
|
||||||
|
// subscribe for specific event message
|
||||||
|
logger.Debugf("Subscribing to interface events...")
|
||||||
|
sub, err := ch.SubscribeNotification(notifChan, &interfaces.SwInterfaceEvent{})
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("error subscribing to interface events: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Debugf("Successfully subscribed to interface events")
|
||||||
|
|
||||||
|
// enable interface events in VPP
|
||||||
|
logger.Debugf("Enabling interface events in VPP...")
|
||||||
|
err = ch.SendRequest(&interfaces.WantInterfaceEvents{
|
||||||
|
PID: uint32(os.Getpid()),
|
||||||
|
EnableDisable: 1,
|
||||||
|
}).ReceiveReply(&interfaces.WantInterfaceEventsReply{})
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("error enabling interface events: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("Interface events enabled in VPP, starting event listener goroutine")
|
||||||
|
|
||||||
|
// receive notifications
|
||||||
|
go func() {
|
||||||
|
logger.Debugf("Interface event listener goroutine started")
|
||||||
|
defer func() {
|
||||||
|
logger.Debugf("Interface event listener goroutine shutting down")
|
||||||
|
// disable interface events in VPP
|
||||||
|
err = ch.SendRequest(&interfaces.WantInterfaceEvents{
|
||||||
|
PID: uint32(os.Getpid()),
|
||||||
|
EnableDisable: 0,
|
||||||
|
}).ReceiveReply(&interfaces.WantInterfaceEventsReply{})
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("error disabling interface events: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsubscribe from receiving events
|
||||||
|
err = sub.Unsubscribe()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("error unsubscribing from interface events: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger.Debugf("Interface event listener waiting for events...")
|
||||||
|
for notif := range notifChan {
|
||||||
|
e := notif.(*interfaces.SwInterfaceEvent)
|
||||||
|
logger.Debugf("interface event: SwIfIndex=%d, Flags=%d, Deleted=%t",
|
||||||
|
e.SwIfIndex, e.Flags, e.Deleted)
|
||||||
|
|
||||||
|
// When an interface event occurs, retrieve all interface details and call callback
|
||||||
|
if callback != nil {
|
||||||
|
details, err := GetAllInterfaceDetails(ch)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("Failed to retrieve interface details after event: %v", err)
|
||||||
|
} else {
|
||||||
|
logger.Debugf("Calling interface event callback with %d interfaces", len(details))
|
||||||
|
callback(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Debugf("Interface event listener goroutine ended")
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
||||||
|
|
||||||
package vppstats
|
package vpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
@ -17,6 +17,9 @@ import (
|
|||||||
|
|
||||||
type StatsCallback func(*api.InterfaceStats)
|
type StatsCallback func(*api.InterfaceStats)
|
||||||
|
|
||||||
|
// Global callback for interface events
|
||||||
|
var interfaceEventCallback InterfaceEventCallback
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Flags for VPP stats configuration
|
// Flags for VPP stats configuration
|
||||||
ApiAddr = flag.String("vppstats.api.addr", "/var/run/vpp/api.sock", "VPP API socket path")
|
ApiAddr = flag.String("vppstats.api.addr", "/var/run/vpp/api.sock", "VPP API socket path")
|
||||||
@ -25,6 +28,11 @@ var (
|
|||||||
Period = flag.Int("vppstats.period", 10, "Interval in seconds for querying VPP interface stats")
|
Period = flag.Int("vppstats.period", 10, "Interval in seconds for querying VPP interface stats")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SetInterfaceEventCallback sets the callback for interface events
|
||||||
|
func SetInterfaceEventCallback(callback InterfaceEventCallback) {
|
||||||
|
interfaceEventCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
// StartStatsRoutine starts a goroutine that queries VPP interface stats at the configured interval
|
// StartStatsRoutine starts a goroutine that queries VPP interface stats at the configured interval
|
||||||
func StartStatsRoutine(callback StatsCallback) {
|
func StartStatsRoutine(callback StatsCallback) {
|
||||||
period := time.Duration(*Period) * time.Second
|
period := time.Duration(*Period) * time.Second
|
||||||
@ -131,6 +139,32 @@ func statsRoutine(period time.Duration, callback StatsCallback) {
|
|||||||
logger.Printf("Connected to VPP (API: %s, Stats: %s)", *ApiAddr, *StatsAddr)
|
logger.Printf("Connected to VPP (API: %s, Stats: %s)", *ApiAddr, *StatsAddr)
|
||||||
connected = true
|
connected = true
|
||||||
wasConnected = true
|
wasConnected = true
|
||||||
|
|
||||||
|
// Start watching interface events
|
||||||
|
logger.Debugf("Creating API channel for interface events...")
|
||||||
|
ch, err := conn.NewAPIChannel()
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("Failed to create API channel for interface events: %v", err)
|
||||||
|
} else {
|
||||||
|
logger.Debugf("API channel created successfully, calling WatchInterfaceEvents...")
|
||||||
|
if err := WatchInterfaceEvents(ch, interfaceEventCallback); err != nil {
|
||||||
|
logger.Debugf("Failed to start interface event watching: %v", err)
|
||||||
|
ch.Close()
|
||||||
|
} else {
|
||||||
|
logger.Printf("Interface event watching started successfully")
|
||||||
|
|
||||||
|
// Do initial retrieval of interface details
|
||||||
|
if interfaceEventCallback != nil {
|
||||||
|
details, err := GetAllInterfaceDetails(ch)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debugf("Failed to get initial interface details: %v", err)
|
||||||
|
} else {
|
||||||
|
logger.Debugf("Retrieved initial interface details for %d interfaces", len(details))
|
||||||
|
interfaceEventCallback(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query stats if connected
|
// Query stats if connected
|
||||||
@ -231,4 +265,3 @@ func checkVPPLiveness(conn *core.Connection) bool {
|
|||||||
logger.Debugf("VPP liveness check passed (version: %s)", string(reply.Version))
|
logger.Debugf("VPP liveness check passed (version: %s)", string(reply.Version))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -1,163 +0,0 @@
|
|||||||
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
|
|
||||||
|
|
||||||
package vppstats
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.fd.io/govpp/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestVPPStatsFlags(t *testing.T) {
|
|
||||||
// Test default values
|
|
||||||
if *ApiAddr != "/var/run/vpp/api.sock" {
|
|
||||||
t.Errorf("Expected default API address to be '/var/run/vpp/api.sock', got '%s'", *ApiAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *StatsAddr != "/var/run/vpp/stats.sock" {
|
|
||||||
t.Errorf("Expected default stats address to be '/var/run/vpp/stats.sock', got '%s'", *StatsAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *IfIndexOffset != 1000 {
|
|
||||||
t.Errorf("Expected default interface index offset to be 1000, got %d", *IfIndexOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *Period != 10 {
|
|
||||||
t.Errorf("Expected default period to be 10, got %d", *Period)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFlagRegistrations(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
flagName string
|
|
||||||
defValue string
|
|
||||||
}{
|
|
||||||
{"API address", "vppstats.api.addr", "/var/run/vpp/api.sock"},
|
|
||||||
{"Stats address", "vppstats.stats.addr", "/var/run/vpp/stats.sock"},
|
|
||||||
{"Index offset", "vppstats.ifindex-offset", "1000"},
|
|
||||||
{"Period", "vppstats.period", "10"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
f := flag.Lookup(tt.flagName)
|
|
||||||
if f == nil {
|
|
||||||
t.Errorf("Expected %s flag to be registered", tt.flagName)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.DefValue != tt.defValue {
|
|
||||||
t.Errorf("Expected %s flag default value to be '%s', got '%s'",
|
|
||||||
tt.flagName, tt.defValue, f.DefValue)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStatsCallbackType(t *testing.T) {
|
|
||||||
// Test that we can create a valid callback function
|
|
||||||
var called bool
|
|
||||||
var receivedStats *api.InterfaceStats
|
|
||||||
|
|
||||||
callback := func(stats *api.InterfaceStats) {
|
|
||||||
called = true
|
|
||||||
receivedStats = stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create mock stats
|
|
||||||
mockStats := &api.InterfaceStats{
|
|
||||||
Interfaces: []api.InterfaceCounters{
|
|
||||||
{
|
|
||||||
InterfaceIndex: 1,
|
|
||||||
InterfaceName: "test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the callback
|
|
||||||
callback(mockStats)
|
|
||||||
|
|
||||||
if !called {
|
|
||||||
t.Error("Expected callback to be called")
|
|
||||||
}
|
|
||||||
|
|
||||||
if receivedStats != mockStats {
|
|
||||||
t.Error("Expected callback to receive the same stats object")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(receivedStats.Interfaces) != 1 {
|
|
||||||
t.Errorf("Expected 1 interface, got %d", len(receivedStats.Interfaces))
|
|
||||||
}
|
|
||||||
|
|
||||||
if receivedStats.Interfaces[0].InterfaceName != "test" {
|
|
||||||
t.Errorf("Expected interface name 'test', got '%s'", receivedStats.Interfaces[0].InterfaceName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPeriodConversion(t *testing.T) {
|
|
||||||
// Test that period conversion works correctly
|
|
||||||
originalPeriod := *Period
|
|
||||||
defer func() { *Period = originalPeriod }()
|
|
||||||
|
|
||||||
testPeriods := []struct {
|
|
||||||
input int
|
|
||||||
expected time.Duration
|
|
||||||
}{
|
|
||||||
{1, time.Second},
|
|
||||||
{5, 5 * time.Second},
|
|
||||||
{10, 10 * time.Second},
|
|
||||||
{60, time.Minute},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range testPeriods {
|
|
||||||
t.Run(fmt.Sprintf("period_%d", tt.input), func(t *testing.T) {
|
|
||||||
*Period = tt.input
|
|
||||||
result := time.Duration(*Period) * time.Second
|
|
||||||
|
|
||||||
if result != tt.expected {
|
|
||||||
t.Errorf("Expected period %v, got %v", tt.expected, result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFlagValues(t *testing.T) {
|
|
||||||
// Save original flag values
|
|
||||||
originalApiAddr := *ApiAddr
|
|
||||||
originalStatsAddr := *StatsAddr
|
|
||||||
originalOffset := *IfIndexOffset
|
|
||||||
originalPeriod := *Period
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
*ApiAddr = originalApiAddr
|
|
||||||
*StatsAddr = originalStatsAddr
|
|
||||||
*IfIndexOffset = originalOffset
|
|
||||||
*Period = originalPeriod
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Test setting flag values
|
|
||||||
*ApiAddr = "/custom/api.sock"
|
|
||||||
*StatsAddr = "/custom/stats.sock"
|
|
||||||
*IfIndexOffset = 2000
|
|
||||||
*Period = 30
|
|
||||||
|
|
||||||
if *ApiAddr != "/custom/api.sock" {
|
|
||||||
t.Errorf("Expected API address to be '/custom/api.sock', got '%s'", *ApiAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *StatsAddr != "/custom/stats.sock" {
|
|
||||||
t.Errorf("Expected stats address to be '/custom/stats.sock', got '%s'", *StatsAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *IfIndexOffset != 2000 {
|
|
||||||
t.Errorf("Expected interface index offset to be 2000, got %d", *IfIndexOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *Period != 30 {
|
|
||||||
t.Errorf("Expected period to be 30, got %d", *Period)
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user