Add a new and improved snmp plugin

This commit is contained in:
Patrick Hemmer 2016-06-19 15:34:28 -04:00 committed by Cameron Sparr
parent c991b579d2
commit f3055228f3
6 changed files with 1251 additions and 0 deletions

View File

@ -65,6 +65,8 @@ should now look like:
- [#1434](https://github.com/influxdata/telegraf/pull/1434): Add measurement name arg to logparser plugin.
- [#1479](https://github.com/influxdata/telegraf/pull/1479): logparser: change resp_code from a field to a tag.
- [#1411](https://github.com/influxdata/telegraf/pull/1411): Implement support for fetching hddtemp data
- [#1466](https://github.com/influxdata/telegraf/pull/1466): MongoDB input plugin: adding per DB stats from db.stats()
- [#1389](https://github.com/influxdata/telegraf/pull/1389): Add a new and improved SNMP plugin (snmp2).
### Bugfixes

View File

@ -190,6 +190,7 @@ Currently implemented sources:
* [riak](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/riak)
* [sensors ](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/sensors) (only available if built from source)
* [snmp](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/snmp)
* [snmp2](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/snmp2)
* [sql server](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/sqlserver) (microsoft)
* [twemproxy](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/twemproxy)
* [varnish](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/varnish)

View File

@ -61,6 +61,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/riak"
_ "github.com/influxdata/telegraf/plugins/inputs/sensors"
_ "github.com/influxdata/telegraf/plugins/inputs/snmp"
_ "github.com/influxdata/telegraf/plugins/inputs/snmp2"
_ "github.com/influxdata/telegraf/plugins/inputs/sqlserver"
_ "github.com/influxdata/telegraf/plugins/inputs/statsd"
_ "github.com/influxdata/telegraf/plugins/inputs/sysstat"

View File

@ -0,0 +1,130 @@
# SNMP2 Plugin
The SNMP2 input plugin gathers metrics from SNMP agents.
It is an alternative to the SNMP plugin with significantly different configuration & behavior.
## Configuration:
### Example:
SNMP data:
```
.1.2.3.0.0.1.0 octet_str "foo"
.1.2.3.0.0.1.1 octet_str "bar"
.1.2.3.0.0.102 octet_str "bad"
.1.2.3.0.0.2.0 integer 1
.1.2.3.0.0.2.1 integer 2
.1.2.3.0.0.3.0 octet_str "0.123"
.1.2.3.0.0.3.1 octet_str "0.456"
.1.2.3.0.0.3.2 octet_str "9.999"
.1.2.3.0.1 octet_str "baz"
.1.2.3.0.2 uinteger 54321
.1.2.3.0.3 uinteger 234
```
Telegraf config:
```toml
[[inputs.snmp2]]
agents = [ "127.0.0.1:161" ]
version = 2
community = "public"
name = "system"
[[inputs.snmp2.field]]
name = "hostname"
oid = ".1.2.3.0.1"
is_tag = true
[[inputs.snmp2.field]]
name = "uptime"
oid = ".1.2.3.0.2"
[[inputs.snmp2.field]]
name = "loadavg"
oid = ".1.2.3.0.3"
conversion = "float(2)"
[[inputs.snmp2.table]]
name = "remote_servers"
inherit_tags = [ "hostname" ]
[[inputs.snmp2.table.field]]
name = "server"
oid = ".1.2.3.0.0.1"
is_tag = true
[[inputs.snmp2.table.field]]
name = "connections"
oid = ".1.2.3.0.0.2"
[[inputs.snmp2.table.field]]
name = "latency"
oid = ".1.2.3.0.0.3"
conversion = "float"
```
Resulting output:
```
* Plugin: snmp2, Collection 1
> system,agent_host=127.0.0.1,host=mylocalhost,hostname=baz loadavg=2.34,uptime=54321i 1468953135000000000
> remote_servers,agent_host=127.0.0.1,host=mylocalhost,hostname=baz,server=foo connections=1i,latency=0.123 1468953135000000000
> remote_servers,agent_host=127.0.0.1,host=mylocalhost,hostname=baz,server=bar connections=2i,latency=0.456 1468953135000000000
```
### Config parameters
* `agents`: Default: `[]`
List of SNMP agents to connect to in the form of `IP[:PORT]`. If `:PORT` is unspecified, it defaults to `161`.
* `version`: Default: `2`
SNMP protocol version to use.
* `community`: Default: `"public"`
SNMP community to use.
* `max_repetitions`: Default: `50`
Maximum number of iterations for repeating variables.
* `sec_name`:
Security name for authenticated SNMPv3 requests.
* `auth_protocol`: Values: `"MD5"`,`"SHA"`,`""`. Default: `""`
Authentication protocol for authenticated SNMPv3 requests.
* `auth_password`:
Authentication password for authenticated SNMPv3 requests.
* `sec_level`: Values: `"noAuthNoPriv"`,`"authNoPriv"`,`"authPriv"`. Default: `"noAuthNoPriv"`
Security level used for SNMPv3 messages.
* `context_name`:
Context name used for SNMPv3 requests.
* `priv_protocol`: Values: `"DES"`,`"AES"`,`""`. Default: `""`
Privacy protocol used for encrypted SNMPv3 messages.
* `priv_password`:
Privacy password used for encrypted SNMPv3 messages.
* `name`:
Output measurement name.
#### Field parameters:
* `name`:
Output field/tag name.
* `oid`:
OID to get. Must be in dotted notation, not textual.
* `is_tag`:
Output this field as a tag.
* `conversion`: Values: `"float(X)"`,`"float"`,`"int"`,`""`. Default: `""`
Converts the value according to the given specification.
- `float(X)`: Converts the input value into a float and divides by the Xth power of 10. Efficively just moves the decimal left X places. For example a value of `123` with `float(2)` will result in `1.23`.
- `float`: Converts the value into a float with no adjustment. Same as `float(0)`.
- `int`: Convertes the value into an integer.
#### Table parameters:
* `name`:
Output measurement name.
* `inherit_tags`:
Which tags to inherit from the top-level config and to use in the output of this table's measurement.

View File

@ -0,0 +1,624 @@
package snmp2
import (
"fmt"
"math"
"net"
"strconv"
"strings"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/inputs"
"github.com/soniah/gosnmp"
)
const description = `Retrieves SNMP values from remote agents`
const sampleConfig = `
agents = [ "127.0.0.1:161" ]
version = 2
# SNMPv1 & SNMPv2 parameters
community = "public"
# SNMPv2 & SNMPv3 parameters
max_repetitions = 50
# SNMPv3 parameters
#sec_name = "myuser"
#auth_protocol = "md5" # Values: "MD5", "SHA", ""
#auth_password = "password123"
#sec_level = "authNoPriv" # Values: "noAuthNoPriv", "authNoPriv", "authPriv"
#context_name = ""
#priv_protocol = "" # Values: "DES", "AES", ""
#priv_password = ""
# measurement name
name = "system"
[[inputs.snmp2.field]]
name = "hostname"
oid = ".1.2.3.0.1.1"
[[inputs.snmp2.field]]
name = "uptime"
oid = ".1.2.3.0.1.200"
[[inputs.snmp2.field]]
name = "load"
oid = ".1.2.3.0.1.201"
[[inputs.snmp2.table]]
# measurement name
name = "remote_servers"
inherit_tags = [ "hostname" ]
[[inputs.snmp2.table.field]]
name = "server"
oid = ".1.2.3.0.0.0"
is_tag = true
[[inputs.snmp2.table.field]]
name = "connections"
oid = ".1.2.3.0.0.1"
[[inputs.snmp2.table.field]]
name = "latency"
oid = ".1.2.3.0.0.2"
`
// Snmp2 holds the configuration for the plugin.
type Snmp2 struct {
// The SNMP agent to query. Format is ADDR[:PORT] (e.g. 1.2.3.4:161).
Agents []string
// Timeout to wait for a response. Value is anything accepted by time.ParseDuration().
Timeout string
Retries int
// Values: 1, 2, 3
Version uint8
// Parameters for Version 1 & 2
Community string
// Parameters for Version 2 & 3
MaxRepetitions uint
// Parameters for Version 3
ContextName string
// Values: "noAuthNoPriv", "authNoPriv", "authPriv"
SecLevel string
SecName string
// Values: "MD5", "SHA", "". Default: ""
AuthProtocol string
AuthPassword string
// Values: "DES", "AES", "". Default: ""
PrivProtocol string
PrivPassword string
EngineID string
EngineBoots uint32
EngineTime uint32
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
Fields []Field `toml:"field"`
connectionCache map[string]snmpConnection
}
// 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
// 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 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
// 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.
Conversion string
}
// 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{}
}
// 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...),
}
}
func init() {
inputs.Add("snmp2", func() telegraf.Input {
return &Snmp2{
Retries: 5,
MaxRepetitions: 50,
}
})
}
// SampleConfig returns the default configuration of the input.
func (s *Snmp2) SampleConfig() string {
return sampleConfig
}
// Description returns a one-sentence description on the input.
func (s *Snmp2) 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 *Snmp2) Gather(acc telegraf.Accumulator) error {
var errs Errors
for _, agent := range s.Agents {
gs, err := s.getConnection(agent)
if err != nil {
errs = append(errs, Errorf(err, "agent %s", agent))
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{}
if err := s.gatherTable(acc, gs, t, topTags, false); err != nil {
errs = append(errs, 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 {
errs = append(errs, Errorf(err, "agent %s", agent))
}
}
}
if errs == nil {
return nil
}
return errs
}
func (s *Snmp2) 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")
}
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")
} 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
})
if err != nil {
if _, ok := err.(NestedError); !ok {
return nil, Errorf(err, "performing bulk walk")
}
}
}
for i, v := range ifv {
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 {
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 `agent` as the cache key.
func (s *Snmp2) getConnection(agent string) (snmpConnection, error) {
if s.connectionCache == nil {
s.connectionCache = map[string]snmpConnection{}
}
if gs, ok := s.connectionCache[agent]; ok {
return gs, nil
}
gs := gosnmpWrapper{&gosnmp.GoSNMP{}}
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)
if s.Timeout != "" {
if gs.Timeout, err = time.ParseDuration(s.Timeout); err != nil {
return nil, Errorf(err, "parsing timeout")
}
} else {
gs.Timeout = time.Second * 1
}
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 = int(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")
}
s.connectionCache[agent] = gs
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.
// "" will convert a byte slice into a string.
// 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
}

View File

@ -0,0 +1,493 @@
package snmp2
import (
"fmt"
"net"
"sync"
"testing"
"time"
"github.com/influxdata/telegraf/testutil"
"github.com/influxdata/toml"
"github.com/soniah/gosnmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type testSNMPConnection struct {
host string
values map[string]interface{}
}
func (tsc *testSNMPConnection) Host() string {
return tsc.host
}
func (tsc *testSNMPConnection) Get(oids []string) (*gosnmp.SnmpPacket, error) {
sp := &gosnmp.SnmpPacket{}
for _, oid := range oids {
v, ok := tsc.values[oid]
if !ok {
sp.Variables = append(sp.Variables, gosnmp.SnmpPDU{
Name: oid,
Type: gosnmp.NoSuchObject,
})
continue
}
sp.Variables = append(sp.Variables, gosnmp.SnmpPDU{
Name: oid,
Value: v,
})
}
return sp, nil
}
func (tsc *testSNMPConnection) Walk(oid string, wf gosnmp.WalkFunc) error {
for void, v := range tsc.values {
if void == oid || (len(void) > len(oid) && void[:len(oid)+1] == oid+".") {
if err := wf(gosnmp.SnmpPDU{
Name: void,
Value: v,
}); err != nil {
return err
}
}
}
return nil
}
var tsc = &testSNMPConnection{
host: "tsc",
values: map[string]interface{}{
".1.2.3.0.0.1.0": "foo",
".1.2.3.0.0.1.1": []byte("bar"),
".1.2.3.0.0.102": "bad",
".1.2.3.0.0.2.0": 1,
".1.2.3.0.0.2.1": 2,
".1.2.3.0.0.3.0": "0.123",
".1.2.3.0.0.3.1": "0.456",
".1.2.3.0.0.3.2": "9.999",
".1.2.3.0.0.4.0": 123456,
".1.2.3.0.1": "baz",
".1.2.3.0.2": 234,
".1.2.3.0.3": []byte("byte slice"),
},
}
func TestSampleConfig(t *testing.T) {
conf := struct {
Inputs struct {
Snmp2 []*Snmp2
}
}{}
err := toml.Unmarshal([]byte("[[inputs.snmp2]]\n"+(*Snmp2)(nil).SampleConfig()), &conf)
assert.NoError(t, err)
s := Snmp2{
Agents: []string{"127.0.0.1:161"},
Version: 2,
Community: "public",
MaxRepetitions: 50,
Name: "system",
Fields: []Field{
{Name: "hostname", Oid: ".1.2.3.0.1.1"},
{Name: "uptime", Oid: ".1.2.3.0.1.200"},
{Name: "load", Oid: ".1.2.3.0.1.201"},
},
Tables: []Table{
{
Name: "remote_servers",
InheritTags: []string{"hostname"},
Fields: []Field{
{Name: "server", Oid: ".1.2.3.0.0.0", IsTag: true},
{Name: "connections", Oid: ".1.2.3.0.0.1"},
{Name: "latency", Oid: ".1.2.3.0.0.2"},
},
},
},
}
assert.Equal(t, s, *conf.Inputs.Snmp2[0])
}
func TestGetSNMPConnection_v2(t *testing.T) {
s := &Snmp2{
Timeout: "3s",
Retries: 4,
Version: 2,
Community: "foo",
}
gsc, err := s.getConnection("1.2.3.4:567")
require.NoError(t, err)
gs := gsc.(gosnmpWrapper)
assert.Equal(t, "1.2.3.4", gs.Target)
assert.EqualValues(t, 567, gs.Port)
assert.Equal(t, gosnmp.Version2c, gs.Version)
assert.Equal(t, "foo", gs.Community)
gsc, err = s.getConnection("1.2.3.4")
require.NoError(t, err)
gs = gsc.(gosnmpWrapper)
assert.Equal(t, "1.2.3.4", gs.Target)
assert.EqualValues(t, 161, gs.Port)
}
func TestGetSNMPConnection_v3(t *testing.T) {
s := &Snmp2{
Version: 3,
MaxRepetitions: 20,
ContextName: "mycontext",
SecLevel: "authPriv",
SecName: "myuser",
AuthProtocol: "md5",
AuthPassword: "password123",
PrivProtocol: "des",
PrivPassword: "321drowssap",
EngineID: "myengineid",
EngineBoots: 1,
EngineTime: 2,
}
gsc, err := s.getConnection("1.2.3.4")
require.NoError(t, err)
gs := gsc.(gosnmpWrapper)
assert.Equal(t, gs.Version, gosnmp.Version3)
sp := gs.SecurityParameters.(*gosnmp.UsmSecurityParameters)
assert.Equal(t, "1.2.3.4", gsc.Host())
assert.Equal(t, 20, gs.MaxRepetitions)
assert.Equal(t, "mycontext", gs.ContextName)
assert.Equal(t, gosnmp.AuthPriv, gs.MsgFlags&gosnmp.AuthPriv)
assert.Equal(t, "myuser", sp.UserName)
assert.Equal(t, gosnmp.MD5, sp.AuthenticationProtocol)
assert.Equal(t, "password123", sp.AuthenticationPassphrase)
assert.Equal(t, gosnmp.DES, sp.PrivacyProtocol)
assert.Equal(t, "321drowssap", sp.PrivacyPassphrase)
assert.Equal(t, "myengineid", sp.AuthoritativeEngineID)
assert.EqualValues(t, 1, sp.AuthoritativeEngineBoots)
assert.EqualValues(t, 2, sp.AuthoritativeEngineTime)
}
func TestGetSNMPConnection_caching(t *testing.T) {
s := &Snmp2{}
gs1, err := s.getConnection("1.2.3.4")
require.NoError(t, err)
gs2, err := s.getConnection("1.2.3.4")
require.NoError(t, err)
gs3, err := s.getConnection("1.2.3.5")
require.NoError(t, err)
assert.True(t, gs1 == gs2)
assert.False(t, gs2 == gs3)
}
func TestGosnmpWrapper_walk_retry(t *testing.T) {
srvr, err := net.ListenUDP("udp4", &net.UDPAddr{})
defer srvr.Close()
require.NoError(t, err)
reqCount := 0
// Set up a WaitGroup to wait for the server goroutine to exit and protect
// reqCount.
// Even though simultaneous access is impossible because the server will be
// blocked on ReadFrom, without this the race detector gets unhappy.
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
buf := make([]byte, 256)
for {
_, addr, err := srvr.ReadFrom(buf)
if err != nil {
return
}
reqCount++
srvr.WriteTo([]byte{'X'}, addr) // will cause decoding error
}
}()
gs := &gosnmp.GoSNMP{
Target: srvr.LocalAddr().(*net.UDPAddr).IP.String(),
Port: uint16(srvr.LocalAddr().(*net.UDPAddr).Port),
Version: gosnmp.Version2c,
Community: "public",
Timeout: time.Millisecond * 10,
Retries: 1,
}
err = gs.Connect()
require.NoError(t, err)
conn := gs.Conn
gsw := gosnmpWrapper{gs}
err = gsw.Walk(".1.2.3", func(_ gosnmp.SnmpPDU) error { return nil })
srvr.Close()
wg.Wait()
assert.Error(t, err)
assert.False(t, gs.Conn == conn)
assert.Equal(t, (gs.Retries+1)*2, reqCount)
}
func TestGosnmpWrapper_get_retry(t *testing.T) {
srvr, err := net.ListenUDP("udp4", &net.UDPAddr{})
defer srvr.Close()
require.NoError(t, err)
reqCount := 0
// Set up a WaitGroup to wait for the server goroutine to exit and protect
// reqCount.
// Even though simultaneous access is impossible because the server will be
// blocked on ReadFrom, without this the race detector gets unhappy.
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
buf := make([]byte, 256)
for {
_, addr, err := srvr.ReadFrom(buf)
if err != nil {
return
}
reqCount++
srvr.WriteTo([]byte{'X'}, addr) // will cause decoding error
}
}()
gs := &gosnmp.GoSNMP{
Target: srvr.LocalAddr().(*net.UDPAddr).IP.String(),
Port: uint16(srvr.LocalAddr().(*net.UDPAddr).Port),
Version: gosnmp.Version2c,
Community: "public",
Timeout: time.Millisecond * 10,
Retries: 1,
}
err = gs.Connect()
require.NoError(t, err)
conn := gs.Conn
gsw := gosnmpWrapper{gs}
_, err = gsw.Get([]string{".1.2.3"})
srvr.Close()
wg.Wait()
assert.Error(t, err)
assert.False(t, gs.Conn == conn)
assert.Equal(t, (gs.Retries+1)*2, reqCount)
}
func TestTableBuild_walk(t *testing.T) {
tbl := Table{
Name: "mytable",
Fields: []Field{
{
Name: "myfield1",
Oid: ".1.2.3.0.0.1",
IsTag: true,
},
{
Name: "myfield2",
Oid: ".1.2.3.0.0.2",
},
{
Name: "myfield3",
Oid: ".1.2.3.0.0.3",
Conversion: "float",
},
},
}
tb, err := tbl.Build(tsc, true)
require.NoError(t, err)
assert.Equal(t, tb.Name, "mytable")
rtr1 := RTableRow{
Tags: map[string]string{"myfield1": "foo"},
Fields: map[string]interface{}{"myfield2": 1, "myfield3": float64(0.123)},
}
rtr2 := RTableRow{
Tags: map[string]string{"myfield1": "bar"},
Fields: map[string]interface{}{"myfield2": 2, "myfield3": float64(0.456)},
}
assert.Len(t, tb.Rows, 2)
assert.Contains(t, tb.Rows, rtr1)
assert.Contains(t, tb.Rows, rtr2)
}
func TestTableBuild_noWalk(t *testing.T) {
tbl := Table{
Name: "mytable",
Fields: []Field{
{
Name: "myfield1",
Oid: ".1.2.3.0.1",
IsTag: true,
},
{
Name: "myfield2",
Oid: ".1.2.3.0.2",
},
{
Name: "myfield3",
Oid: ".1.2.3.0.2",
IsTag: true,
},
},
}
tb, err := tbl.Build(tsc, false)
require.NoError(t, err)
rtr := RTableRow{
Tags: map[string]string{"myfield1": "baz", "myfield3": "234"},
Fields: map[string]interface{}{"myfield2": 234},
}
assert.Len(t, tb.Rows, 1)
assert.Contains(t, tb.Rows, rtr)
}
func TestGather(t *testing.T) {
s := &Snmp2{
Agents: []string{"TestGather"},
Name: "mytable",
Fields: []Field{
{
Name: "myfield1",
Oid: ".1.2.3.0.1",
IsTag: true,
},
{
Name: "myfield2",
Oid: ".1.2.3.0.2",
},
{
Name: "myfield3",
Oid: "1.2.3.0.1",
},
},
Tables: []Table{
{
Name: "myOtherTable",
InheritTags: []string{"myfield1"},
Fields: []Field{
{
Name: "myOtherField",
Oid: ".1.2.3.0.0.4",
},
},
},
},
connectionCache: map[string]snmpConnection{
"TestGather": tsc,
},
}
acc := &testutil.Accumulator{}
tstart := time.Now()
s.Gather(acc)
tstop := time.Now()
require.Len(t, acc.Metrics, 2)
m := acc.Metrics[0]
assert.Equal(t, "mytable", m.Measurement)
assert.Equal(t, "tsc", m.Tags["agent_host"])
assert.Equal(t, "baz", m.Tags["myfield1"])
assert.Len(t, m.Fields, 2)
assert.Equal(t, 234, m.Fields["myfield2"])
assert.Equal(t, "baz", m.Fields["myfield3"])
assert.True(t, tstart.Before(m.Time))
assert.True(t, tstop.After(m.Time))
m2 := acc.Metrics[1]
assert.Equal(t, "myOtherTable", m2.Measurement)
assert.Equal(t, "tsc", m2.Tags["agent_host"])
assert.Equal(t, "baz", m2.Tags["myfield1"])
assert.Len(t, m2.Fields, 1)
assert.Equal(t, 123456, m2.Fields["myOtherField"])
}
func TestGather_host(t *testing.T) {
s := &Snmp2{
Agents: []string{"TestGather"},
Name: "mytable",
Fields: []Field{
{
Name: "host",
Oid: ".1.2.3.0.1",
IsTag: true,
},
{
Name: "myfield2",
Oid: ".1.2.3.0.2",
},
},
connectionCache: map[string]snmpConnection{
"TestGather": tsc,
},
}
acc := &testutil.Accumulator{}
s.Gather(acc)
require.Len(t, acc.Metrics, 1)
m := acc.Metrics[0]
assert.Equal(t, "baz", m.Tags["host"])
}
func TestFieldConvert(t *testing.T) {
testTable := []struct {
input interface{}
conv string
expected interface{}
}{
{[]byte("foo"), "", string("foo")},
{"0.123", "float", float64(0.123)},
{[]byte("0.123"), "float", float64(0.123)},
{float32(0.123), "float", float64(float32(0.123))},
{float64(0.123), "float", float64(0.123)},
{123, "float", float64(123)},
{123, "float(0)", float64(123)},
{123, "float(4)", float64(0.0123)},
{int8(123), "float(3)", float64(0.123)},
{int16(123), "float(3)", float64(0.123)},
{int32(123), "float(3)", float64(0.123)},
{int64(123), "float(3)", float64(0.123)},
{uint(123), "float(3)", float64(0.123)},
{uint8(123), "float(3)", float64(0.123)},
{uint16(123), "float(3)", float64(0.123)},
{uint32(123), "float(3)", float64(0.123)},
{uint64(123), "float(3)", float64(0.123)},
{"123", "int", int64(123)},
{[]byte("123"), "int", int64(123)},
{float32(12.3), "int", int64(12)},
{float64(12.3), "int", int64(12)},
{int(123), "int", int64(123)},
{int8(123), "int", int64(123)},
{int16(123), "int", int64(123)},
{int32(123), "int", int64(123)},
{int64(123), "int", int64(123)},
{uint(123), "int", int64(123)},
{uint8(123), "int", int64(123)},
{uint16(123), "int", int64(123)},
{uint32(123), "int", int64(123)},
{uint64(123), "int", int64(123)},
}
for _, tc := range testTable {
act := fieldConvert(tc.conv, tc.input)
assert.EqualValues(t, tc.expected, act, "input=%T(%v) conv=%s expected=%T(%v)", tc.input, tc.input, tc.conv, tc.expected, tc.expected)
}
}
func TestError(t *testing.T) {
e := fmt.Errorf("nested error")
err := Errorf(e, "top error %d", 123)
require.Error(t, err)
ne, ok := err.(NestedError)
require.True(t, ok)
assert.Equal(t, e, ne.NestedErr)
assert.Contains(t, err.Error(), "top error 123")
assert.Contains(t, err.Error(), "nested error")
}