Move Go code to src/

This commit is contained in:
Pim van Pelt
2025-06-17 00:47:08 +02:00
parent c0bcdd5449
commit 7f81b51c1f
61 changed files with 3 additions and 3 deletions

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

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

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

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

8
src/config/config.go Normal file
View 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
View File

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

4
src/go-agentx/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
agentx
Makefile
*.txt
vendor

12
src/go-agentx/AUTHORS Normal file
View File

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

191
src/go-agentx/LICENSE Normal file
View File

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

110
src/go-agentx/README.md Normal file
View File

@ -0,0 +1,110 @@
# AgentX
[![Documentation](https://godoc.org/github.com/posteo/go-agentx?status.svg)](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).

217
src/go-agentx/client.go Normal file
View File

@ -0,0 +1,217 @@
// 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
}

View File

@ -0,0 +1,52 @@
// 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())
},
}
}

5
src/go-agentx/go.mod Normal file
View File

@ -0,0 +1,5 @@
module github.com/posteo/go-agentx
go 1.13
require github.com/stretchr/testify v1.6.1

12
src/go-agentx/go.sum Normal file
View File

@ -0,0 +1,12 @@
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=

17
src/go-agentx/handler.go Normal file
View File

@ -0,0 +1,17 @@
// 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)
}

View File

@ -0,0 +1,31 @@
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))
}

View File

@ -0,0 +1,66 @@
// 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)
}

View File

@ -0,0 +1,63 @@
// 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))
})
}

View File

@ -0,0 +1,13 @@
// 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{}
}

View File

@ -0,0 +1,33 @@
// 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
}

View File

@ -0,0 +1,29 @@
// 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
}

View File

@ -0,0 +1,26 @@
// 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
}

View File

@ -0,0 +1,29 @@
// 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
}

View File

@ -0,0 +1,62 @@
// 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)
}

View File

@ -0,0 +1,45 @@
// 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, " | "))
}

40
src/go-agentx/pdu/get.go Normal file
View File

@ -0,0 +1,40 @@
// 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
}

View File

@ -0,0 +1,28 @@
// 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
}

View File

@ -0,0 +1,61 @@
// 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() + ")"
}

View File

@ -0,0 +1,38 @@
// 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)
}

View File

@ -0,0 +1,96 @@
// 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()
}

View File

@ -0,0 +1,43 @@
// 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
}

38
src/go-agentx/pdu/open.go Normal file
View File

@ -0,0 +1,38 @@
// 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
}

View File

@ -0,0 +1,14 @@
// 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
}

View File

@ -0,0 +1,53 @@
// 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
}

View File

@ -0,0 +1,27 @@
// 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
}

View File

@ -0,0 +1,38 @@
// 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)
}

View File

@ -0,0 +1,37 @@
// 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
}

View File

@ -0,0 +1,68 @@
// 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() + ")"
}

View File

@ -0,0 +1,29 @@
// 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()
}

77
src/go-agentx/pdu/type.go Normal file
View File

@ -0,0 +1,77 @@
// 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"
}

View File

@ -0,0 +1,37 @@
// 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
}

View File

@ -0,0 +1,185 @@
// 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)
}

View File

@ -0,0 +1,59 @@
// 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)
}

View File

@ -0,0 +1,56 @@
// 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, ", ") + "]"
}

16
src/go-agentx/request.go Normal file
View File

@ -0,0 +1,16 @@
// 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() + ")"
}

185
src/go-agentx/session.go Normal file
View File

@ -0,0 +1,185 @@
// 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())
}

View File

@ -0,0 +1,48 @@
// 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))
})
}

7
src/go-agentx/shell.nix Normal file
View File

@ -0,0 +1,7 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
name = "dev-environment";
buildInputs = [
pkgs.net-snmp
];
}

7
src/go-agentx/snmpd.conf Normal file
View File

@ -0,0 +1,7 @@
agentaddress udp:127.0.0.1:30161
rocommunity public
master agentx
agentXSocket tcp:127.0.0.1:30705

103
src/go-agentx/value/oid.go Normal file
View File

@ -0,0 +1,103 @@
// 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, ".")
}

View File

@ -0,0 +1,70 @@
// 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)
}

22
src/go.mod Normal file
View File

@ -0,0 +1,22 @@
module govpp-snmp-agentx
go 1.23.8
toolchain go1.23.10
require (
github.com/posteo/go-agentx v0.2.1
go.fd.io/govpp v0.12.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/sys v0.31.0 // indirect
)
replace github.com/posteo/go-agentx => ./go-agentx

41
src/go.sum Normal file
View File

@ -0,0 +1,41 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff h1:zk1wwii7uXmI0znwU+lqg+wFL9G5+vm5I+9rv2let60=
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff/go.mod h1:yUhRXHewUVJ1k89wHKP68xfzk7kwXUx/DV1nx4EBMbw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe h1:ewr1srjRCmcQogPQ/NCx6XCk6LGVmsVCc9Y3vvPZj+Y=
github.com/lunixbochs/struc v0.0.0-20200521075829-a4cb8d33dbbe/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
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/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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.fd.io/govpp v0.12.0 h1:5HnMzsKHSFdxglsFyEhR0g+CzncWiLYXG2NDYgNUrnE=
go.fd.io/govpp v0.12.0/go.mod h1:6qp4J/+jumgXXoowrtVAk13PSXS6+ghPrDG8CyuU/Is=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

413
src/ifmib/ifmib.go Normal file
View File

@ -0,0 +1,413 @@
// 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/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
// ifAlias .18 - DisplayString
const ifEntryOID = "1.3.6.1.2.1.2.2.1"
const ifXTableOID = "1.3.6.1.2.1.31.1.1.1"
// VPP Config YAML structures
type VPPConfig struct {
Interfaces map[string]VPPInterface `yaml:"interfaces"`
Loopbacks map[string]VPPInterface `yaml:"loopbacks"`
}
type VPPInterface struct {
Description string `yaml:"description"`
SubInterfaces map[string]VPPInterface `yaml:"sub-interfaces"`
}
type InterfaceMIB struct {
mutex sync.RWMutex
handler *agentx.ListHandler
ifEntrySession *agentx.Session
ifXTableSession *agentx.Session
stats map[uint32]*api.InterfaceCounters // indexed by interface index
descriptions map[string]string // interface name -> description mapping
}
func NewInterfaceMIB() *InterfaceMIB {
return &InterfaceMIB{
handler: &agentx.ListHandler{},
stats: make(map[uint32]*api.InterfaceCounters),
descriptions: make(map[string]string),
}
}
func (m *InterfaceMIB) GetHandler() *agentx.ListHandler {
return m.handler
}
func (m *InterfaceMIB) LoadVPPConfig(configPath string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
// Read YAML file
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read VPP config file: %v", err)
}
// Parse YAML
var config VPPConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return fmt.Errorf("failed to parse VPP config YAML: %v", err)
}
// Extract interface descriptions
for ifName, ifConfig := range config.Interfaces {
if ifConfig.Description != "" {
m.descriptions[ifName] = ifConfig.Description
logger.Debugf("Loaded description for interface %s: %s", ifName, ifConfig.Description)
}
// Process sub-interfaces
for subID, subConfig := range ifConfig.SubInterfaces {
if subConfig.Description != "" {
subIfName := fmt.Sprintf("%s.%s", ifName, subID)
m.descriptions[subIfName] = subConfig.Description
logger.Debugf("Loaded description for sub-interface %s: %s", subIfName, subConfig.Description)
}
}
}
// Extract loopback descriptions
for ifName, ifConfig := range config.Loopbacks {
if ifConfig.Description != "" {
m.descriptions[ifName] = ifConfig.Description
logger.Debugf("Loaded description for loopback %s: %s", ifName, ifConfig.Description)
}
}
logger.Printf("Loaded %d interface descriptions from VPP config", len(m.descriptions))
return nil
}
func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) {
m.mutex.Lock()
defer m.mutex.Unlock()
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
// ifAlias (.18) - Interface description/alias
item = m.handler.Add(fmt.Sprintf("%s.18.%d", ifXTableOID, idx))
item.Type = pdu.VariableTypeOctetString
// Use description from VPP config if available, otherwise use interface name
if desc, exists := m.descriptions[iface.InterfaceName]; exists {
item.Value = desc
} else {
item.Value = iface.InterfaceName
}
}
func (m *InterfaceMIB) RegisterWithClient(client *agentx.Client) error {
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
}

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

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

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

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

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

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

56
src/main.go Normal file
View File

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

20
src/main_test.go Normal file
View File

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

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

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

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

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