telegraf/plugins/inputs/snmp_trap/snmp_trap.go

271 lines
6.0 KiB
Go

package snmp_trap
import (
"bufio"
"bytes"
"fmt"
"net"
"os/exec"
"strings"
"sync"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs"
"github.com/soniah/gosnmp"
)
var defaultTimeout = internal.Duration{Duration: time.Second * 5}
type handler func(*gosnmp.SnmpPacket, *net.UDPAddr)
type execer func(internal.Duration, string, ...string) ([]byte, error)
type mibEntry struct {
mibName string
oidText string
}
type SnmpTrap struct {
ServiceAddress string `toml:"service_address"`
Timeout internal.Duration `toml:"timeout"`
acc telegraf.Accumulator
listener *gosnmp.TrapListener
timeFunc func() time.Time
errCh chan error
makeHandlerWrapper func(handler) handler
Log telegraf.Logger `toml:"-"`
cacheLock sync.Mutex
cache map[string]mibEntry
execCmd execer
}
var sampleConfig = `
## Transport, local address, and port to listen on. Transport must
## be "udp://". Omit local address to listen on all interfaces.
## example: "udp://127.0.0.1:1234"
##
## Special permissions may be required to listen on a port less than
## 1024. See README.md for details
##
# service_address = "udp://:162"
## Timeout running snmptranslate command
# timeout = "5s"
`
func (s *SnmpTrap) SampleConfig() string {
return sampleConfig
}
func (s *SnmpTrap) Description() string {
return "Receive SNMP traps"
}
func (s *SnmpTrap) Gather(_ telegraf.Accumulator) error {
return nil
}
func init() {
inputs.Add("snmp_trap", func() telegraf.Input {
return &SnmpTrap{
timeFunc: time.Now,
ServiceAddress: "udp://:162",
Timeout: defaultTimeout,
}
})
}
func realExecCmd(Timeout internal.Duration, arg0 string, args ...string) ([]byte, error) {
cmd := exec.Command(arg0, args...)
var out bytes.Buffer
cmd.Stdout = &out
err := internal.RunTimeout(cmd, Timeout.Duration)
if err != nil {
return nil, err
}
return out.Bytes(), nil
}
func (s *SnmpTrap) Init() error {
s.cache = map[string]mibEntry{}
s.execCmd = realExecCmd
return nil
}
func (s *SnmpTrap) Start(acc telegraf.Accumulator) error {
s.acc = acc
s.listener = gosnmp.NewTrapListener()
s.listener.OnNewTrap = makeTrapHandler(s)
s.listener.Params = gosnmp.Default
// wrap the handler, used in unit tests
if nil != s.makeHandlerWrapper {
s.listener.OnNewTrap = s.makeHandlerWrapper(s.listener.OnNewTrap)
}
split := strings.SplitN(s.ServiceAddress, "://", 2)
if len(split) != 2 {
return fmt.Errorf("invalid service address: %s", s.ServiceAddress)
}
protocol := split[0]
addr := split[1]
// gosnmp.TrapListener currently supports udp only. For forward
// compatibility, require udp in the service address
if protocol != "udp" {
return fmt.Errorf("unknown protocol '%s' in '%s'", protocol, s.ServiceAddress)
}
// If (*TrapListener).Listen immediately returns an error we need
// to return it from this function. Use a channel to get it here
// from the goroutine. Buffer one in case Listen returns after
// Listening but before our Close is called.
s.errCh = make(chan error, 1)
go func() {
s.errCh <- s.listener.Listen(addr)
}()
select {
case <-s.listener.Listening():
s.Log.Infof("Listening on %s", s.ServiceAddress)
case err := <-s.errCh:
return err
}
return nil
}
func (s *SnmpTrap) Stop() {
s.listener.Close()
err := <-s.errCh
if nil != err {
s.Log.Errorf("Error stopping trap listener %v", err)
}
}
func makeTrapHandler(s *SnmpTrap) handler {
return func(packet *gosnmp.SnmpPacket, addr *net.UDPAddr) {
tm := s.timeFunc()
fields := map[string]interface{}{}
tags := map[string]string{}
tags["version"] = packet.Version.String()
tags["source"] = addr.IP.String()
for _, v := range packet.Variables {
// Use system mibs to resolve oids. Don't fall back to
// numeric oid because it's not useful enough to the end
// user and can be difficult to translate or remove from
// the database later.
var value interface{}
// todo: format the pdu value based on its snmp type and
// the mib's textual convention. The snmp input plugin
// only handles textual convention for ip and mac
// addresses
switch v.Type {
case gosnmp.ObjectIdentifier:
val, ok := v.Value.(string)
if !ok {
s.Log.Errorf("Error getting value OID")
return
}
var e mibEntry
var err error
e, err = s.lookup(val)
if nil != err {
s.Log.Errorf("Error resolving value OID: %v", err)
return
}
value = e.oidText
// 1.3.6.1.6.3.1.1.4.1.0 is SNMPv2-MIB::snmpTrapOID.0.
// If v.Name is this oid, set a tag of the trap name.
if v.Name == ".1.3.6.1.6.3.1.1.4.1.0" {
tags["oid"] = val
tags["name"] = e.oidText
tags["mib"] = e.mibName
continue
}
default:
value = v.Value
}
e, err := s.lookup(v.Name)
if nil != err {
s.Log.Errorf("Error resolving OID: %v", err)
return
}
name := e.oidText
fields[name] = value
}
s.acc.AddFields("snmp_trap", fields, tags, tm)
}
}
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 snmptranlate
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)
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()
i := strings.Index(e.oidText, "::")
if i == -1 {
return e, fmt.Errorf("not found")
}
e.mibName = e.oidText[:i]
e.oidText = e.oidText[i+2:]
return e, nil
}