Use gosmi for SNMP traps (#9343)

Use gosmi for snmp_trap plugin

Co-authored-by: Logan McNaughton <logan@bacoosta.com>
Co-authored-by: reimda <reimda@users.noreply.github.com>
This commit is contained in:
Mya 2021-06-16 10:20:33 -06:00 committed by GitHub
parent daec1040c6
commit 28fbdd8fba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 323 additions and 99 deletions

View File

@ -19,6 +19,7 @@ following works:
- github.com/Shopify/sarama [MIT License](https://github.com/Shopify/sarama/blob/master/LICENSE)
- github.com/StackExchange/wmi [MIT License](https://github.com/StackExchange/wmi/blob/master/LICENSE)
- github.com/aerospike/aerospike-client-go [Apache License 2.0](https://github.com/aerospike/aerospike-client-go/blob/master/LICENSE)
- github.com/alecthomas/participle [MIT License](https://github.com/alecthomas/participle/blob/master/COPYING)
- github.com/alecthomas/units [MIT License](https://github.com/alecthomas/units/blob/master/COPYING)
- github.com/aliyun/alibaba-cloud-sdk-go [Apache License 2.0](https://github.com/aliyun/alibaba-cloud-sdk-go/blob/master/LICENSE)
- github.com/amir/raidman [The Unlicense](https://github.com/amir/raidman/blob/master/UNLICENSE)
@ -188,6 +189,7 @@ following works:
- github.com/signalfx/golib [Apache License 2.0](https://github.com/signalfx/golib/blob/master/LICENSE)
- github.com/signalfx/sapm-proto [Apache License 2.0](https://github.com/signalfx/sapm-proto/blob/master/LICENSE)
- github.com/sirupsen/logrus [MIT License](https://github.com/sirupsen/logrus/blob/master/LICENSE)
- github.com/sleepinggenius2/gosmi [MIT License](https://github.com/sleepinggenius2/gosmi/blob/master/LICENSE)
- github.com/snowflakedb/gosnowflake [Apache License 2.0](https://github.com/snowflakedb/gosnowflake/blob/master/LICENSE)
- github.com/streadway/amqp [BSD 2-Clause "Simplified" License](https://github.com/streadway/amqp/blob/master/LICENSE)
- github.com/stretchr/objx [MIT License](https://github.com/stretchr/objx/blob/master/LICENSE)

1
go.mod
View File

@ -112,6 +112,7 @@ require (
github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114 // indirect
github.com/signalfx/golib/v3 v3.3.0
github.com/sirupsen/logrus v1.7.0
github.com/sleepinggenius2/gosmi v0.4.3
github.com/snowflakedb/gosnowflake v1.5.0
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271
github.com/stretchr/testify v1.7.0

8
go.sum
View File

@ -164,6 +164,12 @@ github.com/aerospike/aerospike-client-go v1.27.0/go.mod h1:zj8LBEnWBDOVEIJt8LvaR
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/go-thrift v0.0.0-20170109061633-7914173639b2/go.mod h1:CxCgO+NdpMdi9SsTlGbc0W+/UNxO3I0AabOEJZ3w61w=
github.com/alecthomas/kong v0.2.1/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/participle v0.4.1 h1:P2PJWzwrSpuCWXKnzqvw0b0phSfH1kJo4p2HvLynVsI=
github.com/alecthomas/participle v0.4.1/go.mod h1:T8u4bQOSMwrkTWOSyt8/jSFPEnRtd0FKFMjVfYBlqPs=
github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/alecthomas/repr v0.0.0-20210301060118-828286944d6a/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@ -1370,6 +1376,8 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sleepinggenius2/gosmi v0.4.3 h1:99Zwzy1Cvgsh396sw07oR2G4ab88ILGZFMxSlGWnR6o=
github.com/sleepinggenius2/gosmi v0.4.3/go.mod h1:l8OniPmd3bJzw0MXP2/qh7AhP/e+bTY2CNivIhsnDT0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=

View File

@ -31,6 +31,10 @@ information.
## 1024. See README.md for details
##
# service_address = "udp://:162"
##
## Path to mib files
# path = ["/usr/share/snmp/mibs"]
##
## Timeout running snmptranslate command
# timeout = "5s"
## Snmp version

View File

@ -1,28 +1,25 @@
package snmp_trap
import (
"bufio"
"bytes"
"fmt"
"net"
"os/exec"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs"
"github.com/sleepinggenius2/gosmi"
"github.com/sleepinggenius2/gosmi/types"
"github.com/gosnmp/gosnmp"
)
var defaultTimeout = config.Duration(time.Second * 5)
type execer func(config.Duration, string, ...string) ([]byte, error)
type mibEntry struct {
mibName string
oidText string
@ -32,6 +29,7 @@ type SnmpTrap struct {
ServiceAddress string `toml:"service_address"`
Timeout config.Duration `toml:"timeout"`
Version string `toml:"version"`
Path []string `toml:"path"`
// Settings for version 3
// Values: "noAuthNoPriv", "authNoPriv", "authPriv"
@ -44,19 +42,15 @@ type SnmpTrap struct {
PrivProtocol string `toml:"priv_protocol"`
PrivPassword string `toml:"priv_password"`
acc telegraf.Accumulator
listener *gosnmp.TrapListener
timeFunc func() time.Time
errCh chan error
acc telegraf.Accumulator
listener *gosnmp.TrapListener
timeFunc func() time.Time
lookupFunc func(string) (mibEntry, error)
errCh chan error
makeHandlerWrapper func(gosnmp.TrapHandlerFunc) gosnmp.TrapHandlerFunc
Log telegraf.Logger `toml:"-"`
cacheLock sync.Mutex
cache map[string]mibEntry
execCmd execer
}
var sampleConfig = `
@ -68,6 +62,10 @@ var sampleConfig = `
## 1024. See README.md for details
##
# service_address = "udp://:162"
##
## Path to mib files
# path = ["/usr/share/snmp/mibs"]
##
## Timeout running snmptranslate command
# timeout = "5s"
## Snmp version, defaults to 2c
@ -104,6 +102,7 @@ func init() {
inputs.Add("snmp_trap", func() telegraf.Input {
return &SnmpTrap{
timeFunc: time.Now,
lookupFunc: lookup,
ServiceAddress: "udp://:162",
Timeout: defaultTimeout,
Version: "2c",
@ -111,20 +110,50 @@ func init() {
})
}
func realExecCmd(timeout config.Duration, arg0 string, args ...string) ([]byte, error) {
cmd := exec.Command(arg0, args...)
var out bytes.Buffer
cmd.Stdout = &out
err := internal.RunTimeout(cmd, time.Duration(timeout))
func (s *SnmpTrap) Init() error {
// must init, append path for each directory, load module for every file
// or gosmi will fail without saying why
gosmi.Init()
err := s.getMibsPath()
if err != nil {
return nil, err
s.Log.Errorf("Could not get path %v", err)
}
return out.Bytes(), nil
return nil
}
func (s *SnmpTrap) Init() error {
s.cache = map[string]mibEntry{}
s.execCmd = realExecCmd
func (s *SnmpTrap) getMibsPath() error {
var folders []string
for _, mibPath := range s.Path {
gosmi.AppendPath(mibPath)
folders = append(folders, mibPath)
err := filepath.Walk(mibPath, func(path string, info os.FileInfo, err error) error {
if info.Mode()&os.ModeSymlink != 0 {
s, _ := os.Readlink(path)
folders = append(folders, s)
}
return nil
})
if err != nil {
s.Log.Errorf("Filepath could not be walked %v", err)
}
for _, folder := range folders {
err := filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
gosmi.AppendPath(path)
} else if info.Mode()&os.ModeSymlink == 0 {
_, err := gosmi.LoadModule(info.Name())
if err != nil {
s.Log.Errorf("Module could not be loaded %v", err)
}
}
return nil
})
if err != nil {
s.Log.Errorf("Filepath could not be walked %v", err)
}
}
folders = []string{}
}
return nil
}
@ -248,6 +277,7 @@ func (s *SnmpTrap) Start(acc telegraf.Accumulator) error {
func (s *SnmpTrap) Stop() {
s.listener.Close()
defer gosmi.Exit()
err := <-s.errCh
if nil != err {
s.Log.Errorf("Error stopping trap listener %v", err)
@ -281,7 +311,7 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc {
}
if trapOid != "" {
e, err := s.lookup(trapOid)
e, err := s.lookupFunc(trapOid)
if err != nil {
s.Log.Errorf("Error resolving V1 OID, oid=%s, source=%s: %v", trapOid, tags["source"], err)
return
@ -319,7 +349,7 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc {
var e mibEntry
var err error
e, err = s.lookup(val)
e, err = s.lookupFunc(val)
if nil != err {
s.Log.Errorf("Error resolving value OID, oid=%s, source=%s: %v", val, tags["source"], err)
return
@ -337,7 +367,7 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc {
value = v.Value
}
e, err := s.lookup(v.Name)
e, err := s.lookupFunc(v.Name)
if nil != err {
s.Log.Errorf("Error resolving OID oid=%s, source=%s: %v", v.Name, tags["source"], err)
return
@ -366,48 +396,16 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc {
}
}
func (s *SnmpTrap) lookup(oid string) (e mibEntry, err error) {
s.cacheLock.Lock()
defer s.cacheLock.Unlock()
var ok bool
if e, ok = s.cache[oid]; !ok {
// cache miss. exec snmptranslate
e, err = s.snmptranslate(oid)
if err == nil {
s.cache[oid] = e
}
return e, err
}
return e, nil
}
func (s *SnmpTrap) clear() {
s.cacheLock.Lock()
defer s.cacheLock.Unlock()
s.cache = map[string]mibEntry{}
}
func (s *SnmpTrap) load(oid string, e mibEntry) {
s.cacheLock.Lock()
defer s.cacheLock.Unlock()
s.cache[oid] = e
}
func (s *SnmpTrap) snmptranslate(oid string) (e mibEntry, err error) {
var out []byte
out, err = s.execCmd(s.Timeout, "snmptranslate", "-Td", "-Ob", "-m", "all", oid)
func lookup(oid string) (e mibEntry, err error) {
var node gosmi.SmiNode
node, err = gosmi.GetNodeByOID(types.OidMustFromString(oid))
// ensure modules are loaded or node will be empty (might not error)
if err != nil {
return e, err
}
scanner := bufio.NewScanner(bytes.NewBuffer(out))
ok := scanner.Scan()
if err = scanner.Err(); !ok && err != nil {
return e, err
}
e.oidText = scanner.Text()
e.oidText = node.RenderQualified()
i := strings.Index(e.oidText, "::")
if i == -1 {

View File

@ -3,6 +3,7 @@ package snmp_trap
import (
"fmt"
"net"
"path/filepath"
"strconv"
"strings"
"testing"
@ -11,35 +12,11 @@ import (
"github.com/gosnmp/gosnmp"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/testutil"
"github.com/stretchr/testify/require"
)
func TestLoad(t *testing.T) {
s := &SnmpTrap{}
require.Nil(t, s.Init())
defer s.clear()
s.load(
".1.3.6.1.6.3.1.1.5.1",
mibEntry{
"SNMPv2-MIB",
"coldStart",
},
)
e, err := s.lookup(".1.3.6.1.6.3.1.1.5.1")
require.NoError(t, err)
require.Equal(t, "SNMPv2-MIB", e.mibName)
require.Equal(t, "coldStart", e.oidText)
}
func fakeExecCmd(_ config.Duration, x string, y ...string) ([]byte, error) {
return nil, fmt.Errorf("mock " + x + " " + strings.Join(y, " "))
}
func newMsgFlagsV3(secLevel string) gosnmp.SnmpV3MsgFlags {
var msgFlags gosnmp.SnmpV3MsgFlags
switch strings.ToLower(secLevel) {
@ -1284,6 +1261,15 @@ func TestReceiveTrap(t *testing.T) {
timeFunc: func() time.Time {
return fakeTime
},
lookupFunc: func(input string) (mibEntry, error) {
for _, entry := range tt.entries {
if input == entry.oid {
return mibEntry{entry.e.mibName, entry.e.oidText}, nil
}
}
return mibEntry{}, fmt.Errorf("Unexpected oid")
},
//if cold start be answer otherwise err
Log: testutil.Logger{},
Version: tt.version.String(),
SecName: tt.secName,
@ -1293,19 +1279,13 @@ func TestReceiveTrap(t *testing.T) {
PrivProtocol: tt.privProto,
PrivPassword: tt.privPass,
}
require.Nil(t, s.Init())
// Don't look up oid with snmptranslate.
s.execCmd = fakeExecCmd
require.NoError(t, s.Init())
var acc testutil.Accumulator
require.Nil(t, s.Start(&acc))
defer s.Stop()
// Preload the cache with the oids we'll use in this test
// so snmptranslate and mibs don't need to be installed.
for _, entry := range tt.entries {
s.load(entry.oid, entry.e)
}
var goSNMP gosnmp.GoSNMP
if tt.version == gosnmp.Version3 {
msgFlags := newMsgFlagsV3(tt.secLevel)
@ -1331,4 +1311,98 @@ func TestReceiveTrap(t *testing.T) {
testutil.SortMetrics())
})
}
}
func TestGosmiSingleMib(t *testing.T) {
// We would prefer to specify port 0 and let the network
// stack choose an unused port for us but TrapListener
// doesn't have a way to return the autoselected port.
// Instead, we'll use an unusual port and hope it's
// unused.
const port = 12399
// Hook into the trap handler so the test knows when the
// trap has been received
received := make(chan int)
wrap := func(f gosnmp.TrapHandlerFunc) gosnmp.TrapHandlerFunc {
return func(p *gosnmp.SnmpPacket, a *net.UDPAddr) {
f(p, a)
received <- 0
}
}
fakeTime := time.Unix(456456456, 456)
now := uint32(123123123)
testDataPath, err := filepath.Abs("./testdata")
require.NoError(t, err)
trap := gosnmp.SnmpTrap{
Variables: []gosnmp.SnmpPDU{
{
Name: ".1.3.6.1.2.1.1.3.0",
Type: gosnmp.TimeTicks,
Value: now,
},
{
Name: ".1.3.6.1.6.3.1.1.4.1.0", // SNMPv2-MIB::snmpTrapOID.0
Type: gosnmp.ObjectIdentifier,
Value: ".1.3.6.1.6.3.1.1.5.1", // coldStart
},
},
}
metrics := []telegraf.Metric{
testutil.MustMetric(
"snmp_trap", // name
map[string]string{ // tags
"oid": ".1.3.6.1.6.3.1.1.5.1",
"name": "coldStart",
"mib": "SNMPv2-MIB",
"version": "2c",
"source": "127.0.0.1",
"community": "public",
},
map[string]interface{}{ // fields
"sysUpTimeInstance": now,
},
fakeTime,
),
}
// Set up the service input plugin
s := &SnmpTrap{
ServiceAddress: "udp://:" + strconv.Itoa(port),
makeHandlerWrapper: wrap,
timeFunc: func() time.Time {
return fakeTime
},
lookupFunc: lookup,
Log: testutil.Logger{},
Version: "2c",
Path: []string{testDataPath},
}
require.NoError(t, s.Init())
var acc testutil.Accumulator
require.Nil(t, s.Start(&acc))
defer s.Stop()
goSNMP := newGoSNMP(gosnmp.Version2c, port)
// Send the trap
sendTrap(t, goSNMP, trap)
// Wait for trap to be received
select {
case <-received:
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for trap to be received")
}
// Verify plugin output
testutil.RequireMetricsEqual(t,
metrics, acc.GetTelegrafMetrics(),
testutil.SortMetrics())
}

View File

@ -0,0 +1,40 @@
SNMPv2-MIB DEFINITIONS ::= BEGIN
IMPORTS
NOTIFICATION-TYPE, NOTIFICATION-GROUP
FROM test2;
snmpMIB MODULE-IDENTITY
LAST-UPDATED "2021060900Z"
ORGANIZATION "testing"
CONTACT-INFO
"EMail: testing@emai.com"
DESCRIPTION
"MIB module for testing snmp_trap plugin
for telegraf
"
::={ coldStart 1 }
snmpMIBObjects OBJECT IDENTIFIER ::= { snmpMIB 1 }
system OBJECT IDENTIFIER ::= { sysUpTimeInstance 1 }
coldStart NOTIFICATION-TYPE
STATUS current
DESCRIPTION
"A coldStart trap signifies that the SNMP entity,
supporting a notification originator application, is
reinitializing itself and that its configuration may
have been altered."
::= { snmpTraps 1 }
snmpBasicNotificationsGroup NOTIFICATION-GROUP
NOTIFICATIONS { coldStart, authenticationFailure }
STATUS current
DESCRIPTION
"The basic notifications implemented by an SNMP entity
supporting command responder applications."
::= { snmpMIBGroups 7 }
END

97
plugins/inputs/snmp_trap/testdata/test2 vendored Normal file
View File

@ -0,0 +1,97 @@
SNMPv2-MIB DEFINITIONS ::= BEGIN
org OBJECT IDENTIFIER ::= { iso 3 } -- "iso" = 1
dod OBJECT IDENTIFIER ::= { org 6 }
internet OBJECT IDENTIFIER ::= { dod 1 }
directory OBJECT IDENTIFIER ::= { internet 1 }
mgmt OBJECT IDENTIFIER ::= { internet 2 }
sysUpTimeInstance OBJECT IDENTIFIER ::= { mgmt 1 }
transmission OBJECT IDENTIFIER ::= { sysUpTimeInstance 10 }
experimental OBJECT IDENTIFIER ::= { internet 3 }
private OBJECT IDENTIFIER ::= { internet 4 }
enterprises OBJECT IDENTIFIER ::= { private 1 }
security OBJECT IDENTIFIER ::= { internet 5 }
snmpV2 OBJECT IDENTIFIER ::= { internet 6 }
-- transport domains
snmpDomains OBJECT IDENTIFIER ::= { snmpV2 1 }
-- transport proxies
snmpProxys OBJECT IDENTIFIER ::= { snmpV2 2 }
-- module identities
coldStart OBJECT IDENTIFIER ::= { snmpV2 3 }
NOTIFICATION-TYPE MACRO ::=
BEGIN
TYPE NOTATION ::=
ObjectsPart
"STATUS" Status
"DESCRIPTION" Text
ReferPart
VALUE NOTATION ::=
value(VALUE NotificationName)
ObjectsPart ::=
"OBJECTS" "{" Objects "}"
| empty
Objects ::=
Object
| Objects "," Object
Object ::=
value(ObjectName)
Status ::=
"current"
| "deprecated"
| "obsolete"
ReferPart ::=
"REFERENCE" Text
| empty
-- a character string as defined in section 3.1.1
Text ::= value(IA5String)
END
NOTIFICATION-GROUP MACRO ::=
BEGIN
TYPE NOTATION ::=
NotificationsPart
"STATUS" Status
"DESCRIPTION" Text
ReferPart
VALUE NOTATION ::=
value(VALUE OBJECT IDENTIFIER)
NotificationsPart ::=
"NOTIFICATIONS" "{" Notifications "}"
Notifications ::=
Notification
| Notifications "," Notification
Notification ::=
value(NotificationName)
Status ::=
"current"
| "deprecated"
| "obsolete"
ReferPart ::=
"REFERENCE" Text
| empty
-- a character string as defined in [2]
Text ::= value(IA5String)
END
END