1058 lines
28 KiB
Go
1058 lines
28 KiB
Go
package snmp
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"net"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/influxdata/telegraf"
|
|
"github.com/influxdata/telegraf/internal"
|
|
"github.com/influxdata/telegraf/plugins/inputs"
|
|
"github.com/influxdata/wlog"
|
|
"github.com/soniah/gosnmp"
|
|
)
|
|
|
|
const description = `Retrieves SNMP values from remote agents`
|
|
const sampleConfig = `
|
|
## Agent addresses to retrieve values from.
|
|
## example: agents = ["udp://127.0.0.1:161"]
|
|
## agents = ["tcp://127.0.0.1:161"]
|
|
agents = ["udp://127.0.0.1:161"]
|
|
|
|
## Timeout for each request.
|
|
# timeout = "5s"
|
|
|
|
## SNMP version; can be 1, 2, or 3.
|
|
# version = 2
|
|
|
|
## SNMP community string.
|
|
# community = "public"
|
|
|
|
## Number of retries to attempt.
|
|
# retries = 3
|
|
|
|
## The GETBULK max-repetitions parameter.
|
|
# max_repetitions = 10
|
|
|
|
## SNMPv3 authentication and encryption options.
|
|
##
|
|
## Security Name.
|
|
# sec_name = "myuser"
|
|
## Authentication protocol; one of "MD5", "SHA", or "".
|
|
# auth_protocol = "MD5"
|
|
## Authentication password.
|
|
# auth_password = "pass"
|
|
## Security Level; one of "noAuthNoPriv", "authNoPriv", or "authPriv".
|
|
# sec_level = "authNoPriv"
|
|
## Context Name.
|
|
# context_name = ""
|
|
## Privacy protocol used for encrypted messages; one of "DES", "AES" or "".
|
|
# priv_protocol = ""
|
|
## Privacy password used for encrypted messages.
|
|
# priv_password = ""
|
|
|
|
## Add fields and tables defining the variables you wish to collect. This
|
|
## example collects the system uptime and interface variables. Reference the
|
|
## full plugin documentation for configuration details.
|
|
`
|
|
|
|
// execCommand is so tests can mock out exec.Command usage.
|
|
var execCommand = exec.Command
|
|
|
|
// execCmd executes the specified command, returning the STDOUT content.
|
|
// If command exits with error status, the output is captured into the returned error.
|
|
func execCmd(arg0 string, args ...string) ([]byte, error) {
|
|
if wlog.LogLevel() == wlog.DEBUG {
|
|
quoted := make([]string, 0, len(args))
|
|
for _, arg := range args {
|
|
quoted = append(quoted, fmt.Sprintf("%q", arg))
|
|
}
|
|
log.Printf("D! [inputs.snmp] executing %q %s", arg0, strings.Join(quoted, " "))
|
|
}
|
|
|
|
out, err := execCommand(arg0, args...).Output()
|
|
if err != nil {
|
|
if err, ok := err.(*exec.ExitError); ok {
|
|
return nil, NestedError{
|
|
Err: err,
|
|
NestedErr: fmt.Errorf("%s", bytes.TrimRight(err.Stderr, "\r\n")),
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Snmp holds the configuration for the plugin.
|
|
type Snmp struct {
|
|
// The SNMP agent to query. Format is [SCHEME://]ADDR[:PORT] (e.g.
|
|
// udp://1.2.3.4:161). If the scheme is not specified then "udp" is used.
|
|
Agents []string `toml:"agents"`
|
|
// Timeout to wait for a response.
|
|
Timeout internal.Duration `toml:"timeout"`
|
|
Retries int `toml:"retries"`
|
|
// Values: 1, 2, 3
|
|
Version uint8 `toml:"version"`
|
|
|
|
// Parameters for Version 1 & 2
|
|
Community string `toml:"community"`
|
|
|
|
// Parameters for Version 2 & 3
|
|
MaxRepetitions uint8 `toml:"max_repetitions"`
|
|
|
|
// Parameters for Version 3
|
|
ContextName string `toml:"context_name"`
|
|
// Values: "noAuthNoPriv", "authNoPriv", "authPriv"
|
|
SecLevel string `toml:"sec_level"`
|
|
SecName string `toml:"sec_name"`
|
|
// Values: "MD5", "SHA", "". Default: ""
|
|
AuthProtocol string `toml:"auth_protocol"`
|
|
AuthPassword string `toml:"auth_password"`
|
|
// Values: "DES", "AES", "". Default: ""
|
|
PrivProtocol string `toml:"priv_protocol"`
|
|
PrivPassword string `toml:"priv_password"`
|
|
EngineID string `toml:"-"`
|
|
EngineBoots uint32 `toml:"-"`
|
|
EngineTime uint32 `toml:"-"`
|
|
|
|
Tables []Table `toml:"table"`
|
|
|
|
// Name & Fields are the elements of a Table.
|
|
// Telegraf chokes if we try to embed a Table. So instead we have to embed the
|
|
// fields of a Table, and construct a Table during runtime.
|
|
Name string // deprecated in 1.14; use name_override
|
|
Fields []Field `toml:"field"`
|
|
|
|
connectionCache []snmpConnection
|
|
initialized bool
|
|
}
|
|
|
|
func (s *Snmp) init() error {
|
|
if s.initialized {
|
|
return nil
|
|
}
|
|
|
|
s.connectionCache = make([]snmpConnection, len(s.Agents))
|
|
|
|
for i := range s.Tables {
|
|
if err := s.Tables[i].init(); err != nil {
|
|
return Errorf(err, "initializing table %s", s.Tables[i].Name)
|
|
}
|
|
}
|
|
|
|
for i := range s.Fields {
|
|
if err := s.Fields[i].init(); err != nil {
|
|
return Errorf(err, "initializing field %s", s.Fields[i].Name)
|
|
}
|
|
}
|
|
|
|
s.initialized = true
|
|
return nil
|
|
}
|
|
|
|
// Table holds the configuration for a SNMP table.
|
|
type Table struct {
|
|
// Name will be the name of the measurement.
|
|
Name string
|
|
|
|
// Which tags to inherit from the top-level config.
|
|
InheritTags []string
|
|
|
|
// Adds each row's table index as a tag.
|
|
IndexAsTag bool
|
|
|
|
// Fields is the tags and values to look up.
|
|
Fields []Field `toml:"field"`
|
|
|
|
// OID for automatic field population.
|
|
// If provided, init() will populate Fields with all the table columns of the
|
|
// given OID.
|
|
Oid string
|
|
|
|
initialized bool
|
|
}
|
|
|
|
// init() builds & initializes the nested fields.
|
|
func (t *Table) init() error {
|
|
if t.initialized {
|
|
return nil
|
|
}
|
|
|
|
if err := t.initBuild(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// initialize all the nested fields
|
|
for i := range t.Fields {
|
|
if err := t.Fields[i].init(); err != nil {
|
|
return Errorf(err, "initializing field %s", t.Fields[i].Name)
|
|
}
|
|
}
|
|
|
|
t.initialized = true
|
|
return nil
|
|
}
|
|
|
|
// initBuild initializes the table if it has an OID configured. If so, the
|
|
// net-snmp tools will be used to look up the OID and auto-populate the table's
|
|
// fields.
|
|
func (t *Table) initBuild() error {
|
|
if t.Oid == "" {
|
|
return nil
|
|
}
|
|
|
|
_, _, oidText, fields, err := snmpTable(t.Oid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if t.Name == "" {
|
|
t.Name = oidText
|
|
}
|
|
|
|
knownOIDs := map[string]bool{}
|
|
for _, f := range t.Fields {
|
|
knownOIDs[f.Oid] = true
|
|
}
|
|
for _, f := range fields {
|
|
if !knownOIDs[f.Oid] {
|
|
t.Fields = append(t.Fields, f)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Field holds the configuration for a Field to look up.
|
|
type Field struct {
|
|
// Name will be the name of the field.
|
|
Name string
|
|
// OID is prefix for this field. The plugin will perform a walk through all
|
|
// OIDs with this as their parent. For each value found, the plugin will strip
|
|
// off the OID prefix, and use the remainder as the index. For multiple fields
|
|
// to show up in the same row, they must share the same index.
|
|
Oid string
|
|
// OidIndexSuffix is the trailing sub-identifier on a table record OID that will be stripped off to get the record's index.
|
|
OidIndexSuffix string
|
|
// OidIndexLength specifies the length of the index in OID path segments. It can be used to remove sub-identifiers that vary in content or length.
|
|
OidIndexLength int
|
|
// IsTag controls whether this OID is output as a tag or a value.
|
|
IsTag bool
|
|
// Conversion controls any type conversion that is done on the value.
|
|
// "float"/"float(0)" will convert the value into a float.
|
|
// "float(X)" will convert the value into a float, and then move the decimal before Xth right-most digit.
|
|
// "int" will conver the value into an integer.
|
|
// "hwaddr" will convert a 6-byte string to a MAC address.
|
|
// "ipaddr" will convert the value to an IPv4 or IPv6 address.
|
|
Conversion string
|
|
|
|
initialized bool
|
|
}
|
|
|
|
// init() converts OID names to numbers, and sets the .Name attribute if unset.
|
|
func (f *Field) init() error {
|
|
if f.initialized {
|
|
return nil
|
|
}
|
|
|
|
_, oidNum, oidText, conversion, err := SnmpTranslate(f.Oid)
|
|
if err != nil {
|
|
return Errorf(err, "translating")
|
|
}
|
|
f.Oid = oidNum
|
|
if f.Name == "" {
|
|
f.Name = oidText
|
|
}
|
|
if f.Conversion == "" {
|
|
f.Conversion = conversion
|
|
}
|
|
|
|
//TODO use textual convention conversion from the MIB
|
|
|
|
f.initialized = true
|
|
return nil
|
|
}
|
|
|
|
// RTable is the resulting table built from a Table.
|
|
type RTable struct {
|
|
// Name is the name of the field, copied from Table.Name.
|
|
Name string
|
|
// Time is the time the table was built.
|
|
Time time.Time
|
|
// Rows are the rows that were found, one row for each table OID index found.
|
|
Rows []RTableRow
|
|
}
|
|
|
|
// RTableRow is the resulting row containing all the OID values which shared
|
|
// the same index.
|
|
type RTableRow struct {
|
|
// Tags are all the Field values which had IsTag=true.
|
|
Tags map[string]string
|
|
// Fields are all the Field values which had IsTag=false.
|
|
Fields map[string]interface{}
|
|
}
|
|
|
|
// NestedError wraps an error returned from deeper in the code.
|
|
type NestedError struct {
|
|
// Err is the error from where the NestedError was constructed.
|
|
Err error
|
|
// NestedError is the error that was passed back from the called function.
|
|
NestedErr error
|
|
}
|
|
|
|
// Error returns a concatenated string of all the nested errors.
|
|
func (ne NestedError) Error() string {
|
|
return ne.Err.Error() + ": " + ne.NestedErr.Error()
|
|
}
|
|
|
|
// Errorf is a convenience function for constructing a NestedError.
|
|
func Errorf(err error, msg string, format ...interface{}) error {
|
|
return NestedError{
|
|
NestedErr: err,
|
|
Err: fmt.Errorf(msg, format...),
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
inputs.Add("snmp", func() telegraf.Input {
|
|
return &Snmp{
|
|
Name: "snmp",
|
|
Retries: 3,
|
|
MaxRepetitions: 10,
|
|
Timeout: internal.Duration{Duration: 5 * time.Second},
|
|
Version: 2,
|
|
Community: "public",
|
|
}
|
|
})
|
|
}
|
|
|
|
// SampleConfig returns the default configuration of the input.
|
|
func (s *Snmp) SampleConfig() string {
|
|
return sampleConfig
|
|
}
|
|
|
|
// Description returns a one-sentence description on the input.
|
|
func (s *Snmp) Description() string {
|
|
return description
|
|
}
|
|
|
|
// Gather retrieves all the configured fields and tables.
|
|
// Any error encountered does not halt the process. The errors are accumulated
|
|
// and returned at the end.
|
|
func (s *Snmp) Gather(acc telegraf.Accumulator) error {
|
|
if err := s.init(); err != nil {
|
|
return err
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for i, agent := range s.Agents {
|
|
wg.Add(1)
|
|
go func(i int, agent string) {
|
|
defer wg.Done()
|
|
gs, err := s.getConnection(i)
|
|
if err != nil {
|
|
acc.AddError(Errorf(err, "agent %s", agent))
|
|
return
|
|
}
|
|
|
|
// First is the top-level fields. We treat the fields as table prefixes with an empty index.
|
|
t := Table{
|
|
Name: s.Name,
|
|
Fields: s.Fields,
|
|
}
|
|
topTags := map[string]string{}
|
|
if err := s.gatherTable(acc, gs, t, topTags, false); err != nil {
|
|
acc.AddError(Errorf(err, "agent %s", agent))
|
|
}
|
|
|
|
// Now is the real tables.
|
|
for _, t := range s.Tables {
|
|
if err := s.gatherTable(acc, gs, t, topTags, true); err != nil {
|
|
acc.AddError(Errorf(err, "agent %s: gathering table %s", agent, t.Name))
|
|
}
|
|
}
|
|
}(i, agent)
|
|
}
|
|
wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Snmp) gatherTable(acc telegraf.Accumulator, gs snmpConnection, t Table, topTags map[string]string, walk bool) error {
|
|
rt, err := t.Build(gs, walk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, tr := range rt.Rows {
|
|
if !walk {
|
|
// top-level table. Add tags to topTags.
|
|
for k, v := range tr.Tags {
|
|
topTags[k] = v
|
|
}
|
|
} else {
|
|
// real table. Inherit any specified tags.
|
|
for _, k := range t.InheritTags {
|
|
if v, ok := topTags[k]; ok {
|
|
tr.Tags[k] = v
|
|
}
|
|
}
|
|
}
|
|
if _, ok := tr.Tags["agent_host"]; !ok {
|
|
tr.Tags["agent_host"] = gs.Host()
|
|
}
|
|
acc.AddFields(rt.Name, tr.Fields, tr.Tags, rt.Time)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Build retrieves all the fields specified in the table and constructs the RTable.
|
|
func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
|
|
rows := map[string]RTableRow{}
|
|
|
|
tagCount := 0
|
|
for _, f := range t.Fields {
|
|
if f.IsTag {
|
|
tagCount++
|
|
}
|
|
|
|
if len(f.Oid) == 0 {
|
|
return nil, fmt.Errorf("cannot have empty OID on field %s", f.Name)
|
|
}
|
|
var oid string
|
|
if f.Oid[0] == '.' {
|
|
oid = f.Oid
|
|
} else {
|
|
// make sure OID has "." because the BulkWalkAll results do, and the prefix needs to match
|
|
oid = "." + f.Oid
|
|
}
|
|
|
|
// ifv contains a mapping of table OID index to field value
|
|
ifv := map[string]interface{}{}
|
|
|
|
if !walk {
|
|
// This is used when fetching non-table fields. Fields configured a the top
|
|
// scope of the plugin.
|
|
// We fetch the fields directly, and add them to ifv as if the index were an
|
|
// empty string. This results in all the non-table fields sharing the same
|
|
// index, and being added on the same row.
|
|
if pkt, err := gs.Get([]string{oid}); err != nil {
|
|
return nil, Errorf(err, "performing get on field %s", f.Name)
|
|
} else if pkt != nil && len(pkt.Variables) > 0 && pkt.Variables[0].Type != gosnmp.NoSuchObject && pkt.Variables[0].Type != gosnmp.NoSuchInstance {
|
|
ent := pkt.Variables[0]
|
|
fv, err := fieldConvert(f.Conversion, ent.Value)
|
|
if err != nil {
|
|
return nil, Errorf(err, "converting %q (OID %s) for field %s", ent.Value, ent.Name, f.Name)
|
|
}
|
|
ifv[""] = fv
|
|
}
|
|
} else {
|
|
err := gs.Walk(oid, func(ent gosnmp.SnmpPDU) error {
|
|
if len(ent.Name) <= len(oid) || ent.Name[:len(oid)+1] != oid+"." {
|
|
return NestedError{} // break the walk
|
|
}
|
|
|
|
idx := ent.Name[len(oid):]
|
|
if f.OidIndexSuffix != "" {
|
|
if !strings.HasSuffix(idx, f.OidIndexSuffix) {
|
|
// this entry doesn't match our OidIndexSuffix. skip it
|
|
return nil
|
|
}
|
|
idx = idx[:len(idx)-len(f.OidIndexSuffix)]
|
|
}
|
|
if f.OidIndexLength != 0 {
|
|
i := f.OidIndexLength + 1 // leading separator
|
|
idx = strings.Map(func(r rune) rune {
|
|
if r == '.' {
|
|
i -= 1
|
|
}
|
|
if i < 1 {
|
|
return -1
|
|
}
|
|
return r
|
|
}, idx)
|
|
}
|
|
|
|
fv, err := fieldConvert(f.Conversion, ent.Value)
|
|
if err != nil {
|
|
return Errorf(err, "converting %q (OID %s) for field %s", ent.Value, ent.Name, f.Name)
|
|
}
|
|
ifv[idx] = fv
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
if _, ok := err.(NestedError); !ok {
|
|
return nil, Errorf(err, "performing bulk walk for field %s", f.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
for idx, v := range ifv {
|
|
rtr, ok := rows[idx]
|
|
if !ok {
|
|
rtr = RTableRow{}
|
|
rtr.Tags = map[string]string{}
|
|
rtr.Fields = map[string]interface{}{}
|
|
rows[idx] = rtr
|
|
}
|
|
if t.IndexAsTag && idx != "" {
|
|
if idx[0] == '.' {
|
|
idx = idx[1:]
|
|
}
|
|
rtr.Tags["index"] = idx
|
|
}
|
|
// don't add an empty string
|
|
if vs, ok := v.(string); !ok || vs != "" {
|
|
if f.IsTag {
|
|
if ok {
|
|
rtr.Tags[f.Name] = vs
|
|
} else {
|
|
rtr.Tags[f.Name] = fmt.Sprintf("%v", v)
|
|
}
|
|
} else {
|
|
rtr.Fields[f.Name] = v
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
rt := RTable{
|
|
Name: t.Name,
|
|
Time: time.Now(), //TODO record time at start
|
|
Rows: make([]RTableRow, 0, len(rows)),
|
|
}
|
|
for _, r := range rows {
|
|
rt.Rows = append(rt.Rows, r)
|
|
}
|
|
return &rt, nil
|
|
}
|
|
|
|
// snmpConnection is an interface which wraps a *gosnmp.GoSNMP object.
|
|
// We interact through an interface so we can mock it out in tests.
|
|
type snmpConnection interface {
|
|
Host() string
|
|
//BulkWalkAll(string) ([]gosnmp.SnmpPDU, error)
|
|
Walk(string, gosnmp.WalkFunc) error
|
|
Get(oids []string) (*gosnmp.SnmpPacket, error)
|
|
}
|
|
|
|
// gosnmpWrapper wraps a *gosnmp.GoSNMP object so we can use it as a snmpConnection.
|
|
type gosnmpWrapper struct {
|
|
*gosnmp.GoSNMP
|
|
}
|
|
|
|
// Host returns the value of GoSNMP.Target.
|
|
func (gsw gosnmpWrapper) Host() string {
|
|
return gsw.Target
|
|
}
|
|
|
|
// Walk wraps GoSNMP.Walk() or GoSNMP.BulkWalk(), depending on whether the
|
|
// connection is using SNMPv1 or newer.
|
|
// Also, if any error is encountered, it will just once reconnect and try again.
|
|
func (gsw gosnmpWrapper) Walk(oid string, fn gosnmp.WalkFunc) error {
|
|
var err error
|
|
// On error, retry once.
|
|
// Unfortunately we can't distinguish between an error returned by gosnmp, and one returned by the walk function.
|
|
for i := 0; i < 2; i++ {
|
|
if gsw.Version == gosnmp.Version1 {
|
|
err = gsw.GoSNMP.Walk(oid, fn)
|
|
} else {
|
|
err = gsw.GoSNMP.BulkWalk(oid, fn)
|
|
}
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if err := gsw.GoSNMP.Connect(); err != nil {
|
|
return Errorf(err, "reconnecting")
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Get wraps GoSNMP.GET().
|
|
// If any error is encountered, it will just once reconnect and try again.
|
|
func (gsw gosnmpWrapper) Get(oids []string) (*gosnmp.SnmpPacket, error) {
|
|
var err error
|
|
var pkt *gosnmp.SnmpPacket
|
|
for i := 0; i < 2; i++ {
|
|
pkt, err = gsw.GoSNMP.Get(oids)
|
|
if err == nil {
|
|
return pkt, nil
|
|
}
|
|
if err := gsw.GoSNMP.Connect(); err != nil {
|
|
return nil, Errorf(err, "reconnecting")
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// getConnection creates a snmpConnection (*gosnmp.GoSNMP) object and caches the
|
|
// result using `agentIndex` as the cache key. This is done to allow multiple
|
|
// connections to a single address. It is an error to use a connection in
|
|
// more than one goroutine.
|
|
func (s *Snmp) getConnection(idx int) (snmpConnection, error) {
|
|
if gs := s.connectionCache[idx]; gs != nil {
|
|
return gs, nil
|
|
}
|
|
|
|
agent := s.Agents[idx]
|
|
|
|
gs := gosnmpWrapper{&gosnmp.GoSNMP{}}
|
|
s.connectionCache[idx] = gs
|
|
|
|
if strings.HasPrefix(agent, "tcp://") {
|
|
agent = strings.TrimPrefix(agent, "tcp://")
|
|
gs.Transport = "tcp"
|
|
}
|
|
host, portStr, err := net.SplitHostPort(agent)
|
|
if err != nil {
|
|
if err, ok := err.(*net.AddrError); !ok || err.Err != "missing port in address" {
|
|
return nil, Errorf(err, "parsing host")
|
|
}
|
|
host = agent
|
|
portStr = "161"
|
|
}
|
|
gs.Target = host
|
|
|
|
port, err := strconv.ParseUint(portStr, 10, 16)
|
|
if err != nil {
|
|
return nil, Errorf(err, "parsing port")
|
|
}
|
|
gs.Port = uint16(port)
|
|
|
|
gs.Timeout = s.Timeout.Duration
|
|
|
|
gs.Retries = s.Retries
|
|
|
|
switch s.Version {
|
|
case 3:
|
|
gs.Version = gosnmp.Version3
|
|
case 2, 0:
|
|
gs.Version = gosnmp.Version2c
|
|
case 1:
|
|
gs.Version = gosnmp.Version1
|
|
default:
|
|
return nil, fmt.Errorf("invalid version")
|
|
}
|
|
|
|
if s.Version < 3 {
|
|
if s.Community == "" {
|
|
gs.Community = "public"
|
|
} else {
|
|
gs.Community = s.Community
|
|
}
|
|
}
|
|
|
|
gs.MaxRepetitions = s.MaxRepetitions
|
|
|
|
if s.Version == 3 {
|
|
gs.ContextName = s.ContextName
|
|
|
|
sp := &gosnmp.UsmSecurityParameters{}
|
|
gs.SecurityParameters = sp
|
|
gs.SecurityModel = gosnmp.UserSecurityModel
|
|
|
|
switch strings.ToLower(s.SecLevel) {
|
|
case "noauthnopriv", "":
|
|
gs.MsgFlags = gosnmp.NoAuthNoPriv
|
|
case "authnopriv":
|
|
gs.MsgFlags = gosnmp.AuthNoPriv
|
|
case "authpriv":
|
|
gs.MsgFlags = gosnmp.AuthPriv
|
|
default:
|
|
return nil, fmt.Errorf("invalid secLevel")
|
|
}
|
|
|
|
sp.UserName = s.SecName
|
|
|
|
switch strings.ToLower(s.AuthProtocol) {
|
|
case "md5":
|
|
sp.AuthenticationProtocol = gosnmp.MD5
|
|
case "sha":
|
|
sp.AuthenticationProtocol = gosnmp.SHA
|
|
case "":
|
|
sp.AuthenticationProtocol = gosnmp.NoAuth
|
|
default:
|
|
return nil, fmt.Errorf("invalid authProtocol")
|
|
}
|
|
|
|
sp.AuthenticationPassphrase = s.AuthPassword
|
|
|
|
switch strings.ToLower(s.PrivProtocol) {
|
|
case "des":
|
|
sp.PrivacyProtocol = gosnmp.DES
|
|
case "aes":
|
|
sp.PrivacyProtocol = gosnmp.AES
|
|
case "":
|
|
sp.PrivacyProtocol = gosnmp.NoPriv
|
|
default:
|
|
return nil, fmt.Errorf("invalid privProtocol")
|
|
}
|
|
|
|
sp.PrivacyPassphrase = s.PrivPassword
|
|
|
|
sp.AuthoritativeEngineID = s.EngineID
|
|
|
|
sp.AuthoritativeEngineBoots = s.EngineBoots
|
|
|
|
sp.AuthoritativeEngineTime = s.EngineTime
|
|
}
|
|
|
|
if err := gs.Connect(); err != nil {
|
|
return nil, Errorf(err, "setting up connection")
|
|
}
|
|
|
|
return gs, nil
|
|
}
|
|
|
|
// fieldConvert converts from any type according to the conv specification
|
|
// "float"/"float(0)" will convert the value into a float.
|
|
// "float(X)" will convert the value into a float, and then move the decimal before Xth right-most digit.
|
|
// "int" will convert the value into an integer.
|
|
// "hwaddr" will convert the value into a MAC address.
|
|
// "ipaddr" will convert the value into into an IP address.
|
|
// "" will convert a byte slice into a string.
|
|
func fieldConvert(conv string, v interface{}) (interface{}, error) {
|
|
if conv == "" {
|
|
if bs, ok := v.([]byte); ok {
|
|
return string(bs), nil
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
var d int
|
|
if _, err := fmt.Sscanf(conv, "float(%d)", &d); err == nil || conv == "float" {
|
|
switch vt := v.(type) {
|
|
case float32:
|
|
v = float64(vt) / math.Pow10(d)
|
|
case float64:
|
|
v = float64(vt) / math.Pow10(d)
|
|
case int:
|
|
v = float64(vt) / math.Pow10(d)
|
|
case int8:
|
|
v = float64(vt) / math.Pow10(d)
|
|
case int16:
|
|
v = float64(vt) / math.Pow10(d)
|
|
case int32:
|
|
v = float64(vt) / math.Pow10(d)
|
|
case int64:
|
|
v = float64(vt) / math.Pow10(d)
|
|
case uint:
|
|
v = float64(vt) / math.Pow10(d)
|
|
case uint8:
|
|
v = float64(vt) / math.Pow10(d)
|
|
case uint16:
|
|
v = float64(vt) / math.Pow10(d)
|
|
case uint32:
|
|
v = float64(vt) / math.Pow10(d)
|
|
case uint64:
|
|
v = float64(vt) / math.Pow10(d)
|
|
case []byte:
|
|
vf, _ := strconv.ParseFloat(string(vt), 64)
|
|
v = vf / math.Pow10(d)
|
|
case string:
|
|
vf, _ := strconv.ParseFloat(vt, 64)
|
|
v = vf / math.Pow10(d)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
if conv == "int" {
|
|
switch vt := v.(type) {
|
|
case float32:
|
|
v = int64(vt)
|
|
case float64:
|
|
v = int64(vt)
|
|
case int:
|
|
v = int64(vt)
|
|
case int8:
|
|
v = int64(vt)
|
|
case int16:
|
|
v = int64(vt)
|
|
case int32:
|
|
v = int64(vt)
|
|
case int64:
|
|
v = int64(vt)
|
|
case uint:
|
|
v = int64(vt)
|
|
case uint8:
|
|
v = int64(vt)
|
|
case uint16:
|
|
v = int64(vt)
|
|
case uint32:
|
|
v = int64(vt)
|
|
case uint64:
|
|
v = int64(vt)
|
|
case []byte:
|
|
v, _ = strconv.Atoi(string(vt))
|
|
case string:
|
|
v, _ = strconv.Atoi(vt)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
if conv == "hwaddr" {
|
|
switch vt := v.(type) {
|
|
case string:
|
|
v = net.HardwareAddr(vt).String()
|
|
case []byte:
|
|
v = net.HardwareAddr(vt).String()
|
|
default:
|
|
return nil, fmt.Errorf("invalid type (%T) for hwaddr conversion", v)
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
if conv == "ipaddr" {
|
|
var ipbs []byte
|
|
|
|
switch vt := v.(type) {
|
|
case string:
|
|
ipbs = []byte(vt)
|
|
case []byte:
|
|
ipbs = vt
|
|
default:
|
|
return nil, fmt.Errorf("invalid type (%T) for ipaddr conversion", v)
|
|
}
|
|
|
|
switch len(ipbs) {
|
|
case 4, 16:
|
|
v = net.IP(ipbs).String()
|
|
default:
|
|
return nil, fmt.Errorf("invalid length (%d) for ipaddr conversion", len(ipbs))
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("invalid conversion type '%s'", conv)
|
|
}
|
|
|
|
type snmpTableCache struct {
|
|
mibName string
|
|
oidNum string
|
|
oidText string
|
|
fields []Field
|
|
err error
|
|
}
|
|
|
|
var snmpTableCaches map[string]snmpTableCache
|
|
var snmpTableCachesLock sync.Mutex
|
|
|
|
// snmpTable resolves the given OID as a table, providing information about the
|
|
// table and fields within.
|
|
func snmpTable(oid string) (mibName string, oidNum string, oidText string, fields []Field, err error) {
|
|
snmpTableCachesLock.Lock()
|
|
if snmpTableCaches == nil {
|
|
snmpTableCaches = map[string]snmpTableCache{}
|
|
}
|
|
|
|
var stc snmpTableCache
|
|
var ok bool
|
|
if stc, ok = snmpTableCaches[oid]; !ok {
|
|
stc.mibName, stc.oidNum, stc.oidText, stc.fields, stc.err = snmpTableCall(oid)
|
|
snmpTableCaches[oid] = stc
|
|
}
|
|
|
|
snmpTableCachesLock.Unlock()
|
|
return stc.mibName, stc.oidNum, stc.oidText, stc.fields, stc.err
|
|
}
|
|
|
|
func snmpTableCall(oid string) (mibName string, oidNum string, oidText string, fields []Field, err error) {
|
|
mibName, oidNum, oidText, _, err = SnmpTranslate(oid)
|
|
if err != nil {
|
|
return "", "", "", nil, Errorf(err, "translating")
|
|
}
|
|
|
|
mibPrefix := mibName + "::"
|
|
oidFullName := mibPrefix + oidText
|
|
|
|
// first attempt to get the table's tags
|
|
tagOids := map[string]struct{}{}
|
|
// We have to guess that the "entry" oid is `oid+".1"`. snmptable and snmptranslate don't seem to have a way to provide the info.
|
|
if out, err := execCmd("snmptranslate", "-Td", oidFullName+".1"); err == nil {
|
|
scanner := bufio.NewScanner(bytes.NewBuffer(out))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
if !strings.HasPrefix(line, " INDEX") {
|
|
continue
|
|
}
|
|
|
|
i := strings.Index(line, "{ ")
|
|
if i == -1 { // parse error
|
|
continue
|
|
}
|
|
line = line[i+2:]
|
|
i = strings.Index(line, " }")
|
|
if i == -1 { // parse error
|
|
continue
|
|
}
|
|
line = line[:i]
|
|
for _, col := range strings.Split(line, ", ") {
|
|
tagOids[mibPrefix+col] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// this won't actually try to run a query. The `-Ch` will just cause it to dump headers.
|
|
out, err := execCmd("snmptable", "-Ch", "-Cl", "-c", "public", "127.0.0.1", oidFullName)
|
|
if err != nil {
|
|
return "", "", "", nil, Errorf(err, "getting table columns")
|
|
}
|
|
scanner := bufio.NewScanner(bytes.NewBuffer(out))
|
|
scanner.Scan()
|
|
cols := scanner.Text()
|
|
if len(cols) == 0 {
|
|
return "", "", "", nil, fmt.Errorf("could not find any columns in table")
|
|
}
|
|
for _, col := range strings.Split(cols, " ") {
|
|
if len(col) == 0 {
|
|
continue
|
|
}
|
|
_, isTag := tagOids[mibPrefix+col]
|
|
fields = append(fields, Field{Name: col, Oid: mibPrefix + col, IsTag: isTag})
|
|
}
|
|
|
|
return mibName, oidNum, oidText, fields, err
|
|
}
|
|
|
|
type snmpTranslateCache struct {
|
|
mibName string
|
|
oidNum string
|
|
oidText string
|
|
conversion string
|
|
err error
|
|
}
|
|
|
|
var snmpTranslateCachesLock sync.Mutex
|
|
var snmpTranslateCaches map[string]snmpTranslateCache
|
|
|
|
// snmpTranslate resolves the given OID.
|
|
func SnmpTranslate(oid string) (mibName string, oidNum string, oidText string, conversion string, err error) {
|
|
snmpTranslateCachesLock.Lock()
|
|
if snmpTranslateCaches == nil {
|
|
snmpTranslateCaches = map[string]snmpTranslateCache{}
|
|
}
|
|
|
|
var stc snmpTranslateCache
|
|
var ok bool
|
|
if stc, ok = snmpTranslateCaches[oid]; !ok {
|
|
// This will result in only one call to snmptranslate running at a time.
|
|
// We could speed it up by putting a lock in snmpTranslateCache and then
|
|
// returning it immediately, and multiple callers would then release the
|
|
// snmpTranslateCachesLock and instead wait on the individual
|
|
// snmpTranlsation.Lock to release. But I don't know that the extra complexity
|
|
// is worth it. Especially when it would slam the system pretty hard if lots
|
|
// of lookups are being perfomed.
|
|
|
|
stc.mibName, stc.oidNum, stc.oidText, stc.conversion, stc.err = snmpTranslateCall(oid)
|
|
snmpTranslateCaches[oid] = stc
|
|
}
|
|
|
|
snmpTranslateCachesLock.Unlock()
|
|
|
|
return stc.mibName, stc.oidNum, stc.oidText, stc.conversion, stc.err
|
|
}
|
|
|
|
func SnmpTranslateForce(oid string, mibName string, oidNum string, oidText string, conversion string) {
|
|
snmpTranslateCachesLock.Lock()
|
|
defer snmpTranslateCachesLock.Unlock()
|
|
if snmpTranslateCaches == nil {
|
|
snmpTranslateCaches = map[string]snmpTranslateCache{}
|
|
}
|
|
|
|
var stc snmpTranslateCache
|
|
stc.mibName = mibName
|
|
stc.oidNum = oidNum
|
|
stc.oidText = oidText
|
|
stc.conversion = conversion
|
|
stc.err = nil
|
|
snmpTranslateCaches[oid] = stc
|
|
}
|
|
|
|
func SnmpTranslateClear() {
|
|
snmpTranslateCachesLock.Lock()
|
|
defer snmpTranslateCachesLock.Unlock()
|
|
snmpTranslateCaches = map[string]snmpTranslateCache{}
|
|
}
|
|
|
|
func snmpTranslateCall(oid string) (mibName string, oidNum string, oidText string, conversion string, err error) {
|
|
var out []byte
|
|
if strings.ContainsAny(oid, ":abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") {
|
|
out, err = execCmd("snmptranslate", "-Td", "-Ob", oid)
|
|
} else {
|
|
out, err = execCmd("snmptranslate", "-Td", "-Ob", "-m", "all", oid)
|
|
if err, ok := err.(*exec.Error); ok && err.Err == exec.ErrNotFound {
|
|
// Silently discard error if snmptranslate not found and we have a numeric OID.
|
|
// Meaning we can get by without the lookup.
|
|
return "", oid, oid, "", nil
|
|
}
|
|
}
|
|
if err != nil {
|
|
return "", "", "", "", err
|
|
}
|
|
|
|
scanner := bufio.NewScanner(bytes.NewBuffer(out))
|
|
ok := scanner.Scan()
|
|
if !ok && scanner.Err() != nil {
|
|
return "", "", "", "", Errorf(scanner.Err(), "getting OID text")
|
|
}
|
|
|
|
oidText = scanner.Text()
|
|
|
|
i := strings.Index(oidText, "::")
|
|
if i == -1 {
|
|
// was not found in MIB.
|
|
if bytes.Contains(out, []byte("[TRUNCATED]")) {
|
|
return "", oid, oid, "", nil
|
|
}
|
|
// not truncated, but not fully found. We still need to parse out numeric OID, so keep going
|
|
oidText = oid
|
|
} else {
|
|
mibName = oidText[:i]
|
|
oidText = oidText[i+2:]
|
|
}
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
if strings.HasPrefix(line, " -- TEXTUAL CONVENTION ") {
|
|
tc := strings.TrimPrefix(line, " -- TEXTUAL CONVENTION ")
|
|
switch tc {
|
|
case "MacAddress", "PhysAddress":
|
|
conversion = "hwaddr"
|
|
case "InetAddressIPv4", "InetAddressIPv6", "InetAddress", "IPSIpAddress":
|
|
conversion = "ipaddr"
|
|
}
|
|
} else if strings.HasPrefix(line, "::= { ") {
|
|
objs := strings.TrimPrefix(line, "::= { ")
|
|
objs = strings.TrimSuffix(objs, " }")
|
|
|
|
for _, obj := range strings.Split(objs, " ") {
|
|
if len(obj) == 0 {
|
|
continue
|
|
}
|
|
if i := strings.Index(obj, "("); i != -1 {
|
|
obj = obj[i+1:]
|
|
oidNum += "." + obj[:strings.Index(obj, ")")]
|
|
} else {
|
|
oidNum += "." + obj
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
return mibName, oidNum, oidText, conversion, nil
|
|
}
|