Completely refactor snmp plugin
- use snmpget/snmpwalk terminology - use snmptranslate to get MIBs
This commit is contained in:
parent
aa86a535cb
commit
86a9a44c7e
|
@ -1397,35 +1397,49 @@
|
||||||
# #priv_protocol = "" # Values: "DES", "AES", ""
|
# #priv_protocol = "" # Values: "DES", "AES", ""
|
||||||
# #priv_password = ""
|
# #priv_password = ""
|
||||||
#
|
#
|
||||||
# ## measurement name
|
# ## Each 'tag' is an "snmpget" request. Tags are inherited by snmp 'walk'
|
||||||
# name = "system"
|
# ## and 'get' requests specified below. If a name for the tag is not provided,
|
||||||
# ## SNMP fields are gotten by using an "snmpget" request. If a name is not
|
# ## we will attempt to use snmptranslate on the OID to get the MIB name.
|
||||||
# ## specified, we attempt to use snmptranslate on the OID to get the MIB name.
|
# [[inputs.snmp.tag]]
|
||||||
# [[inputs.snmp.field]]
|
# name = "sys_name" # optional, tag name
|
||||||
# name = "hostname"
|
# oid = ".1.3.6.1.2.1.1.5.0"
|
||||||
# oid = ".1.2.3.0.1.1"
|
# [[inputs.snmp.tag]]
|
||||||
# [[inputs.snmp.field]]
|
# name = "sys_location"
|
||||||
# name = "uptime"
|
# oid = ".1.3.6.1.2.1.1.6.0"
|
||||||
# oid = ".1.2.3.0.1.200"
|
|
||||||
# [[inputs.snmp.field]]
|
|
||||||
# oid = ".1.2.3.0.1.201"
|
|
||||||
#
|
#
|
||||||
# [[inputs.snmp.table]]
|
# ## optional, name of the measurement that each 'get' field will be under.
|
||||||
# ## measurement name
|
# name = "snmp"
|
||||||
# name = "remote_servers"
|
# ## Each 'get' is an "snmpget" request. If a name for the field is not provided,
|
||||||
# inherit_tags = ["hostname"]
|
# ## we will attempt to use snmptranslate on the OID to get the MIB name.
|
||||||
# ## SNMP table fields must be specified individually. If the table field has
|
# [[inputs.snmp.get]]
|
||||||
# ## multiple rows, they will all be gotten.
|
# name = "snmp_in_packets" # optional, field name
|
||||||
# [[inputs.snmp.table.field]]
|
# oid = ".1.3.6.1.2.1.11.1.0"
|
||||||
# name = "server"
|
# [[inputs.snmp.get]]
|
||||||
# oid = ".1.2.3.0.0.0"
|
# oid = ".1.3.6.1.2.1.11.2.0"
|
||||||
# is_tag = true
|
#
|
||||||
# [[inputs.snmp.table.field]]
|
# ## An SNMP walk will do an "snmpwalk" from the given root OID.
|
||||||
# name = "connections"
|
# ## Each OID it encounters will be converted into a field on the measurement.
|
||||||
# oid = ".1.2.3.0.0.1"
|
# ## We will attempt to use snmptranslate on the OID to get the MIB names for
|
||||||
# [[inputs.snmp.table.field]]
|
# ## each field.
|
||||||
# name = "latency"
|
# [[inputs.snmp.walk]]
|
||||||
# oid = ".1.2.3.0.0.2"
|
# ## optional, inherit_tags specifies which top-level tags to inherit.
|
||||||
|
# ## Globs are supported.
|
||||||
|
# inherit_tags = ["sys_*"]
|
||||||
|
# name = "snmp_metrics" # measurement name
|
||||||
|
# root_oid = ".1.3.6.1.2.1.11"
|
||||||
|
#
|
||||||
|
# [[inputs.snmp.walk]]
|
||||||
|
# ## optional, 'include' specifies MIB names to include in the walk.
|
||||||
|
# ## 'exclude' is also available, although they're mutually-exclusive.
|
||||||
|
# ## Globs are supported.
|
||||||
|
# include = ["if*"]
|
||||||
|
# name = "ifTable"
|
||||||
|
# root_oid = ".1.3.6.1.2.1.2.2"
|
||||||
|
#
|
||||||
|
# [[inputs.snmp.walk]]
|
||||||
|
# exclude = ["ifAlias"]
|
||||||
|
# name = "ifXTable"
|
||||||
|
# root_oid = ".1.3.6.1.2.1.31.1.1"
|
||||||
|
|
||||||
|
|
||||||
# # DEPRECATED: This will be removed in a future release.
|
# # DEPRECATED: This will be removed in a future release.
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package snmp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/soniah/gosnmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 fmt.Errorf("reconnecting %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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, fmt.Errorf("reconnecting %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ package snmp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"net"
|
"net"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -10,6 +9,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/influxdata/telegraf"
|
"github.com/influxdata/telegraf"
|
||||||
|
"github.com/influxdata/telegraf/filter"
|
||||||
"github.com/influxdata/telegraf/internal"
|
"github.com/influxdata/telegraf/internal"
|
||||||
"github.com/influxdata/telegraf/plugins/inputs"
|
"github.com/influxdata/telegraf/plugins/inputs"
|
||||||
|
|
||||||
|
@ -36,35 +36,48 @@ const sampleConfig = `
|
||||||
#priv_protocol = "" # Values: "DES", "AES", ""
|
#priv_protocol = "" # Values: "DES", "AES", ""
|
||||||
#priv_password = ""
|
#priv_password = ""
|
||||||
|
|
||||||
## measurement name
|
## Each 'tag' is an "snmpget" request. Tags are inherited by snmp 'walk'
|
||||||
name = "system"
|
## and 'get' requests specified below. If a name for the tag is not provided,
|
||||||
## SNMP fields are gotten by using an "snmpget" request. If a name is not
|
## we will attempt to get the MIB name.
|
||||||
## specified, we attempt to use snmptranslate on the OID to get the MIB name.
|
[[inputs.snmp.tag]]
|
||||||
[[inputs.snmp.field]]
|
name = "sys_name" # optional, tag name
|
||||||
name = "hostname"
|
oid = ".1.3.6.1.2.1.1.5.0"
|
||||||
oid = ".1.2.3.0.1.1"
|
[[inputs.snmp.tag]]
|
||||||
[[inputs.snmp.field]]
|
name = "sys_location"
|
||||||
name = "uptime"
|
oid = ".1.3.6.1.2.1.1.6.0"
|
||||||
oid = ".1.2.3.0.1.200"
|
|
||||||
[[inputs.snmp.field]]
|
|
||||||
oid = ".1.2.3.0.1.201"
|
|
||||||
|
|
||||||
[[inputs.snmp.table]]
|
## optional, name of the measurement that each 'get' field will be under.
|
||||||
## measurement name
|
name = "snmp"
|
||||||
name = "remote_servers"
|
## Each 'get' is an "snmpget" request. If a name for the field is not provided,
|
||||||
inherit_tags = ["hostname"]
|
## we will attempt to get the MIB name.
|
||||||
## SNMP table fields must be specified individually. If the table field has
|
[[inputs.snmp.get]]
|
||||||
## multiple rows, they will all be gotten.
|
name = "snmp_in_packets" # optional, field name
|
||||||
[[inputs.snmp.table.field]]
|
oid = ".1.3.6.1.2.1.11.1.0"
|
||||||
name = "server"
|
[[inputs.snmp.get]]
|
||||||
oid = ".1.2.3.0.0.0"
|
oid = ".1.3.6.1.2.1.11.2.0"
|
||||||
is_tag = true
|
|
||||||
[[inputs.snmp.table.field]]
|
## An SNMP walk will do an "snmpwalk" from the given root OID.
|
||||||
name = "connections"
|
## Each OID it encounters will be converted into a field on the measurement.
|
||||||
oid = ".1.2.3.0.0.1"
|
## We will attempt to get the MIB names for each field.
|
||||||
[[inputs.snmp.table.field]]
|
[[inputs.snmp.walk]]
|
||||||
name = "latency"
|
## optional, inherit_tags specifies which top-level tags to inherit.
|
||||||
oid = ".1.2.3.0.0.2"
|
## Globs are supported.
|
||||||
|
inherit_tags = ["sys_*"]
|
||||||
|
name = "snmp_metrics" # measurement name
|
||||||
|
root_oid = ".1.3.6.1.2.1.11"
|
||||||
|
|
||||||
|
[[inputs.snmp.walk]]
|
||||||
|
## optional, 'include' specifies MIB names to include in the walk.
|
||||||
|
## 'exclude' is also available, although they're mutually-exclusive.
|
||||||
|
## Globs are supported.
|
||||||
|
include = ["if*"]
|
||||||
|
name = "ifTable"
|
||||||
|
root_oid = ".1.3.6.1.2.1.2.2"
|
||||||
|
|
||||||
|
[[inputs.snmp.walk]]
|
||||||
|
exclude = ["ifAlias"]
|
||||||
|
name = "ifXTable"
|
||||||
|
root_oid = ".1.3.6.1.2.1.31.1.1"
|
||||||
`
|
`
|
||||||
|
|
||||||
// Snmp holds the configuration for the plugin.
|
// Snmp holds the configuration for the plugin.
|
||||||
|
@ -94,106 +107,59 @@ type Snmp struct {
|
||||||
// Values: "DES", "AES", "". Default: ""
|
// Values: "DES", "AES", "". Default: ""
|
||||||
PrivProtocol string
|
PrivProtocol string
|
||||||
PrivPassword string
|
PrivPassword string
|
||||||
EngineID string
|
|
||||||
EngineBoots uint32
|
|
||||||
EngineTime uint32
|
|
||||||
|
|
||||||
Tables []Table `toml:"table"`
|
|
||||||
|
|
||||||
// Name & Fields are the elements of a 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
|
// Telegraf chokes if we try to embed a Table. So instead we have to embed
|
||||||
// fields of a Table, and construct a Table during runtime.
|
// the fields of a Table, and construct a Table during runtime.
|
||||||
Name string
|
Name string
|
||||||
Fields []Field `toml:"field"`
|
Gets []Get `toml:"get"`
|
||||||
|
Walks []Walk `toml:"walk"`
|
||||||
|
Tags []Tag `toml:"tag"`
|
||||||
|
|
||||||
|
// oidToMib is a map of OIDs to MIBs.
|
||||||
|
oidToMib map[string]string
|
||||||
|
// translateBin is the location of the 'snmptranslate' binary.
|
||||||
|
translateBin string
|
||||||
|
|
||||||
connectionCache map[string]snmpConnection
|
connectionCache map[string]snmpConnection
|
||||||
|
|
||||||
inited bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table holds the configuration for a SNMP table.
|
// Get holds the configuration for a Get to look up.
|
||||||
type Table struct {
|
type Get struct {
|
||||||
// Name will be the name of the measurement.
|
|
||||||
Name string
|
|
||||||
|
|
||||||
// Which tags to inherit from the top-level config.
|
|
||||||
InheritTags []string
|
|
||||||
|
|
||||||
// Fields is the tags and values to look up.
|
|
||||||
Fields []Field `toml:"field"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field holds the configuration for a Field to look up.
|
|
||||||
type Field struct {
|
|
||||||
// Name will be the name of the field.
|
// Name will be the name of the field.
|
||||||
Name string
|
Name string
|
||||||
|
// OID is prefix for this field.
|
||||||
|
Oid string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walker holds the configuration for a Walker to look up.
|
||||||
|
type Walk struct {
|
||||||
|
// Name will be the name of the measurement.
|
||||||
|
Name string
|
||||||
// OID is prefix for this field. The plugin will perform a walk through all
|
// 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
|
// 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
|
// 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.
|
// to show up in the same row, they must share the same index.
|
||||||
Oid string
|
RootOid string
|
||||||
// IsTag controls whether this OID is output as a tag or a value.
|
// Include is a list of glob filters, only OID/MIBs that match these will
|
||||||
IsTag bool
|
// be included in the resulting output metrics.
|
||||||
// Conversion controls any type conversion that is done on the value.
|
Include []string
|
||||||
// "float"/"float(0)" will convert the value into a float.
|
include filter.Filter
|
||||||
// "float(X)" will convert the value into a float, and then move the decimal before Xth right-most digit.
|
// Exclude is the exact opposite of Include
|
||||||
// "int" will conver the value into an integer.
|
Exclude []string
|
||||||
Conversion string
|
exclude filter.Filter
|
||||||
|
// InheritTags is a list of tags to inherit from the top-level snmp.tag
|
||||||
|
// configuration.
|
||||||
|
InheritTags []string
|
||||||
|
inheritTags filter.Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
// RTable is the resulting table built from a Table.
|
// Tag holds the config for a tag.
|
||||||
type RTable struct {
|
type Tag struct {
|
||||||
// Name is the name of the field, copied from Table.Name.
|
// Name will be the name of the tag.
|
||||||
Name string
|
Name string
|
||||||
// Time is the time the table was built.
|
// OID is prefix for this field.
|
||||||
Time time.Time
|
Oid string
|
||||||
// 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{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Errors is a list of errors accumulated during an interval.
|
|
||||||
type Errors []error
|
|
||||||
|
|
||||||
func (errs Errors) Error() string {
|
|
||||||
s := ""
|
|
||||||
for _, err := range errs {
|
|
||||||
if s == "" {
|
|
||||||
s = err.Error()
|
|
||||||
} else {
|
|
||||||
s = s + ". " + err.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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...),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SampleConfig returns the default configuration of the input.
|
// SampleConfig returns the default configuration of the input.
|
||||||
|
@ -210,258 +176,150 @@ func (s *Snmp) Description() string {
|
||||||
// Any error encountered does not halt the process. The errors are accumulated
|
// Any error encountered does not halt the process. The errors are accumulated
|
||||||
// and returned at the end.
|
// and returned at the end.
|
||||||
func (s *Snmp) Gather(acc telegraf.Accumulator) error {
|
func (s *Snmp) Gather(acc telegraf.Accumulator) error {
|
||||||
if !s.inited {
|
|
||||||
s.initOidNames()
|
|
||||||
}
|
|
||||||
s.inited = true
|
|
||||||
var errs Errors
|
|
||||||
for _, agent := range s.Agents {
|
for _, agent := range s.Agents {
|
||||||
gs, err := s.getConnection(agent)
|
gs, err := s.getConnection(agent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, Errorf(err, "agent %s", agent))
|
acc.AddError(fmt.Errorf("Agent %s, err: %s", agent, err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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{}
|
topTags := map[string]string{}
|
||||||
if err := s.gatherTable(acc, gs, t, topTags, false); err != nil {
|
// Gather all snmp tags
|
||||||
errs = append(errs, Errorf(err, "agent %s", agent))
|
for _, t := range s.Tags {
|
||||||
}
|
tagval, err := get(gs, t.Oid)
|
||||||
|
if err != nil {
|
||||||
// Now is the real tables.
|
acc.AddError(fmt.Errorf("Agent %s, err: %s", agent, err))
|
||||||
for _, t := range s.Tables {
|
|
||||||
if err := s.gatherTable(acc, gs, t, topTags, true); err != nil {
|
|
||||||
errs = append(errs, Errorf(err, "agent %s", agent))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if errs == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
// initOidNames loops through each [[inputs.snmp.field]] defined.
|
|
||||||
// If the field doesn't have a 'name' defined, it will attempt to use
|
|
||||||
// snmptranslate to get a name for the OID. If snmptranslate doesn't return a
|
|
||||||
// name, or snmptranslate is not available, then use the OID as the name.
|
|
||||||
func (s *Snmp) initOidNames() {
|
|
||||||
bin, _ := exec.LookPath("snmptranslate")
|
|
||||||
|
|
||||||
// Lookup names for each OID defined as a "field"
|
|
||||||
for i, field := range s.Fields {
|
|
||||||
if field.Name != "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.Fields[i].Name = lookupOidName(bin, field.Oid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lookup names for each OID defined as a "table.field"
|
|
||||||
for i, table := range s.Tables {
|
|
||||||
for j, field := range table.Fields {
|
|
||||||
if field.Name != "" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.Tables[i].Fields[j].Name = lookupOidName(bin, field.Oid)
|
if tagval == nil {
|
||||||
}
|
continue
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupOidName(bin, oid string) string {
|
|
||||||
name := oid
|
|
||||||
if bin != "" {
|
|
||||||
out, err := internal.CombinedOutputTimeout(
|
|
||||||
exec.Command(bin, "-Os", oid),
|
|
||||||
time.Millisecond*250)
|
|
||||||
if err == nil && len(out) > 0 {
|
|
||||||
name = strings.TrimSpace(string(out))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
name := t.Name
|
||||||
// real table. Inherit any specified tags.
|
if name == "" {
|
||||||
for _, k := range t.InheritTags {
|
name, _ = s.getMibName(t.Oid)
|
||||||
if v, ok := topTags[k]; ok {
|
}
|
||||||
tr.Tags[k] = v
|
if s, ok := tagval.(string); ok {
|
||||||
}
|
topTags[name] = s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, ok := tr.Tags["agent_host"]; !ok {
|
|
||||||
tr.Tags["agent_host"] = gs.Host()
|
// Gather all snmp gets
|
||||||
|
fields := map[string]interface{}{}
|
||||||
|
for _, g := range s.Gets {
|
||||||
|
val, err := get(gs, g.Oid)
|
||||||
|
if err != nil {
|
||||||
|
acc.AddError(fmt.Errorf("Agent %s, err: %s", agent, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if val == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := g.Name
|
||||||
|
if name == "" {
|
||||||
|
name, _ = s.getMibName(g.Oid)
|
||||||
|
}
|
||||||
|
fields[name] = val
|
||||||
|
}
|
||||||
|
if len(fields) > 0 {
|
||||||
|
acc.AddFields(s.Name, fields, topTags, time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather all snmp walks
|
||||||
|
for _, w := range s.Walks {
|
||||||
|
w.compileWalkFilters(acc)
|
||||||
|
allfields := map[string]map[string]interface{}{}
|
||||||
|
now := time.Now()
|
||||||
|
s.walk(gs, allfields, w.RootOid, w.include, w.exclude)
|
||||||
|
for index, wfields := range allfields {
|
||||||
|
tags := copyTags(topTags, w.inheritTags)
|
||||||
|
tags["index"] = index
|
||||||
|
acc.AddFields(w.Name, wfields, tags, now)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
acc.AddFields(rt.Name, tr.Fields, tr.Tags, rt.Time)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build retrieves all the fields specified in the table and constructs the RTable.
|
// walk does a walk and populates the given 'fields' map with whatever it finds.
|
||||||
func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
|
// as it goes, it attempts to lookup the MIB name of each OID it encounters.
|
||||||
rows := map[string]RTableRow{}
|
func (s *Snmp) walk(
|
||||||
|
gs snmpConnection,
|
||||||
tagCount := 0
|
fields map[string]map[string]interface{},
|
||||||
for _, f := range t.Fields {
|
rootOid string,
|
||||||
if f.IsTag {
|
include filter.Filter,
|
||||||
tagCount++
|
exclude filter.Filter,
|
||||||
}
|
) {
|
||||||
|
gs.Walk(rootOid, func(ent gosnmp.SnmpPDU) error {
|
||||||
if len(f.Oid) == 0 {
|
name, index := s.getMibName(ent.Name)
|
||||||
return nil, fmt.Errorf("cannot have empty OID")
|
if include != nil {
|
||||||
}
|
if !include.Match(name) {
|
||||||
var oid string
|
return nil
|
||||||
if f.Oid[0] == '.' {
|
}
|
||||||
oid = f.Oid
|
} else if exclude != nil {
|
||||||
} else {
|
if exclude.Match(name) {
|
||||||
// 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")
|
|
||||||
} else if pkt != nil && len(pkt.Variables) > 0 && pkt.Variables[0].Type != gosnmp.NoSuchObject {
|
|
||||||
ent := pkt.Variables[0]
|
|
||||||
ifv[ent.Name[len(oid):]] = fieldConvert(f.Conversion, ent.Value)
|
|
||||||
}
|
|
||||||
} 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
|
|
||||||
}
|
|
||||||
ifv[ent.Name[len(oid):]] = fieldConvert(f.Conversion, ent.Value)
|
|
||||||
return nil
|
return nil
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(NestedError); !ok {
|
|
||||||
return nil, Errorf(err, "performing bulk walk")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if _, ok := fields[index]; ok {
|
||||||
for i, v := range ifv {
|
fields[index][name] = normalize(ent.Value)
|
||||||
rtr, ok := rows[i]
|
|
||||||
if !ok {
|
|
||||||
rtr = RTableRow{}
|
|
||||||
rtr.Tags = map[string]string{}
|
|
||||||
rtr.Fields = map[string]interface{}{}
|
|
||||||
rows[i] = rtr
|
|
||||||
}
|
|
||||||
if f.IsTag {
|
|
||||||
if vs, ok := v.(string); 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 {
|
|
||||||
if len(r.Tags) < tagCount {
|
|
||||||
// don't add rows which are missing tags, as without tags you can't filter
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
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 {
|
} else {
|
||||||
err = gsw.GoSNMP.BulkWalk(oid, fn)
|
fields[index] = map[string]interface{}{
|
||||||
|
name: normalize(ent.Value),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err == nil {
|
return nil
|
||||||
return nil
|
})
|
||||||
}
|
|
||||||
if err := gsw.GoSNMP.Connect(); err != nil {
|
|
||||||
return Errorf(err, "reconnecting")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get wraps GoSNMP.GET().
|
// get simply gets the given OID and converts it to the given type.
|
||||||
// If any error is encountered, it will just once reconnect and try again.
|
func get(gs snmpConnection, oid string) (interface{}, error) {
|
||||||
func (gsw gosnmpWrapper) Get(oids []string) (*gosnmp.SnmpPacket, error) {
|
pkt, err := gs.Get([]string{oid})
|
||||||
var err error
|
if err != nil {
|
||||||
var pkt *gosnmp.SnmpPacket
|
return nil, fmt.Errorf("Error performing get: %s", err)
|
||||||
for i := 0; i < 2; i++ {
|
}
|
||||||
pkt, err = gsw.GoSNMP.Get(oids)
|
if pkt != nil && len(pkt.Variables) > 0 && pkt.Variables[0].Type != gosnmp.NoSuchObject {
|
||||||
if err == nil {
|
ent := pkt.Variables[0]
|
||||||
return pkt, nil
|
return normalize(ent.Value), nil
|
||||||
}
|
}
|
||||||
if err := gsw.GoSNMP.Connect(); err != nil {
|
return nil, nil
|
||||||
return nil, Errorf(err, "reconnecting")
|
}
|
||||||
|
|
||||||
|
func (s *Snmp) getMibName(oid string) (string, string) {
|
||||||
|
name, ok := s.oidToMib[oid]
|
||||||
|
if !ok {
|
||||||
|
// lookup the mib using snmptranslate
|
||||||
|
name = lookupOidName(s.translateBin, oid)
|
||||||
|
s.oidToMib[oid] = name
|
||||||
|
}
|
||||||
|
return separateIndex(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Walk) compileWalkFilters(acc telegraf.Accumulator) {
|
||||||
|
// if it's the first gather, compile any inherit_tags filter:
|
||||||
|
if len(w.InheritTags) > 0 && w.inheritTags == nil {
|
||||||
|
var err error
|
||||||
|
w.inheritTags, err = filter.CompileFilter(w.InheritTags)
|
||||||
|
if err != nil {
|
||||||
|
acc.AddError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if it's the first gather, compile any include filter:
|
||||||
|
if len(w.Include) > 0 && w.include == nil {
|
||||||
|
var err error
|
||||||
|
w.include, err = filter.CompileFilter(w.Include)
|
||||||
|
if err != nil {
|
||||||
|
acc.AddError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if it's the first gather, compile any exclude filter:
|
||||||
|
if len(w.Exclude) > 0 && w.exclude == nil {
|
||||||
|
var err error
|
||||||
|
w.exclude, err = filter.CompileFilter(w.Exclude)
|
||||||
|
if err != nil {
|
||||||
|
acc.AddError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getConnection creates a snmpConnection (*gosnmp.GoSNMP) object and caches the
|
// getConnection creates a snmpConnection (*gosnmp.GoSNMP) object and caches the
|
||||||
|
@ -479,7 +337,7 @@ func (s *Snmp) getConnection(agent string) (snmpConnection, error) {
|
||||||
host, portStr, err := net.SplitHostPort(agent)
|
host, portStr, err := net.SplitHostPort(agent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err, ok := err.(*net.AddrError); !ok || err.Err != "missing port in address" {
|
if err, ok := err.(*net.AddrError); !ok || err.Err != "missing port in address" {
|
||||||
return nil, Errorf(err, "parsing host")
|
return nil, fmt.Errorf("reconnecting %s", err)
|
||||||
}
|
}
|
||||||
host = agent
|
host = agent
|
||||||
portStr = "161"
|
portStr = "161"
|
||||||
|
@ -488,13 +346,13 @@ func (s *Snmp) getConnection(agent string) (snmpConnection, error) {
|
||||||
|
|
||||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, Errorf(err, "parsing port")
|
return nil, fmt.Errorf("reconnecting %s", err)
|
||||||
}
|
}
|
||||||
gs.Port = uint16(port)
|
gs.Port = uint16(port)
|
||||||
|
|
||||||
if s.Timeout != "" {
|
if s.Timeout != "" {
|
||||||
if gs.Timeout, err = time.ParseDuration(s.Timeout); err != nil {
|
if gs.Timeout, err = time.ParseDuration(s.Timeout); err != nil {
|
||||||
return nil, Errorf(err, "parsing timeout")
|
return nil, fmt.Errorf("reconnecting %s", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
gs.Timeout = time.Second * 1
|
gs.Timeout = time.Second * 1
|
||||||
|
@ -568,112 +426,77 @@ func (s *Snmp) getConnection(agent string) (snmpConnection, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sp.PrivacyPassphrase = s.PrivPassword
|
sp.PrivacyPassphrase = s.PrivPassword
|
||||||
|
|
||||||
sp.AuthoritativeEngineID = s.EngineID
|
|
||||||
|
|
||||||
sp.AuthoritativeEngineBoots = s.EngineBoots
|
|
||||||
|
|
||||||
sp.AuthoritativeEngineTime = s.EngineTime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gs.Connect(); err != nil {
|
if err := gs.Connect(); err != nil {
|
||||||
return nil, Errorf(err, "setting up connection")
|
return nil, fmt.Errorf("setting up connection %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.connectionCache[agent] = gs
|
s.connectionCache[agent] = gs
|
||||||
return gs, nil
|
return gs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fieldConvert converts from any type according to the conv specification
|
// normalize normalizes the given interface for metric storage.
|
||||||
// "float"/"float(0)" will convert the value into a float.
|
func normalize(v interface{}) interface{} {
|
||||||
// "float(X)" will convert the value into a float, and then move the decimal before Xth right-most digit.
|
switch vt := v.(type) {
|
||||||
// "int" will convert the value into an integer.
|
case []byte:
|
||||||
// "" will convert a byte slice into a string.
|
v = string(vt)
|
||||||
// Any other conv will return the input value unchanged.
|
|
||||||
func fieldConvert(conv string, v interface{}) interface{} {
|
|
||||||
if conv == "" {
|
|
||||||
if bs, ok := v.([]byte); ok {
|
|
||||||
return string(bs)
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyTags(in map[string]string, inheritTags filter.Filter) map[string]string {
|
||||||
|
out := map[string]string{}
|
||||||
|
for k, v := range in {
|
||||||
|
if inheritTags != nil {
|
||||||
|
if !inheritTags.Match(k) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupOidName looks up the MIB name of the given OID using the provided
|
||||||
|
// snmptranslate binary. If a name is not found, then we just return the OID.
|
||||||
|
func lookupOidName(bin, oid string) string {
|
||||||
|
name := oid
|
||||||
|
if bin != "" {
|
||||||
|
out, err := internal.CombinedOutputTimeout(
|
||||||
|
exec.Command(bin, "-Os", oid),
|
||||||
|
time.Millisecond*250)
|
||||||
|
if err == nil && len(out) > 0 {
|
||||||
|
name = strings.TrimSpace(string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// separateIndex takes an input string (either a MIB or an OID) and separates
|
||||||
|
// out the index from it, ie:
|
||||||
|
// ifName.1 -> (ifName, 1)
|
||||||
|
// snmpInPkts.0 -> (snmpInPkts, 0)
|
||||||
|
// .1.3.6.4.2.0 -> (.1.3.6.4.2, 0)
|
||||||
|
func separateIndex(in string) (string, string) {
|
||||||
|
items := strings.Split(in, ".")
|
||||||
|
if len(items) == 1 {
|
||||||
|
return in, "0"
|
||||||
|
}
|
||||||
|
index := items[len(items)-1]
|
||||||
|
return strings.Join(items[0:len(items)-1], "."), index
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
bin, _ := exec.LookPath("snmptranslate")
|
||||||
inputs.Add("snmp", func() telegraf.Input {
|
inputs.Add("snmp", func() telegraf.Input {
|
||||||
return &Snmp{
|
return &Snmp{
|
||||||
|
Name: "snmp",
|
||||||
Retries: 5,
|
Retries: 5,
|
||||||
MaxRepetitions: 50,
|
MaxRepetitions: 50,
|
||||||
|
translateBin: bin,
|
||||||
|
oidToMib: make(map[string]string),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue