Alternate SNMP plugin (#1389)
* Add a new and improved snmp plugin * update gosnmp for duplicate packet fix https://github.com/soniah/gosnmp/issues/68 https://github.com/soniah/gosnmp/pull/69
This commit is contained in:
parent
b022b5567d
commit
7fac74919c
|
@ -89,6 +89,7 @@ consistent with the behavior of `collection_jitter`.
|
||||||
- [#1213](https://github.com/influxdata/telegraf/issues/1213): Add inactive & active memory to mem plugin.
|
- [#1213](https://github.com/influxdata/telegraf/issues/1213): Add inactive & active memory to mem plugin.
|
||||||
- [#1543](https://github.com/influxdata/telegraf/pull/1543): Official Windows service.
|
- [#1543](https://github.com/influxdata/telegraf/pull/1543): Official Windows service.
|
||||||
- [#1414](https://github.com/influxdata/telegraf/pull/1414): Forking sensors command to remove C package dependency.
|
- [#1414](https://github.com/influxdata/telegraf/pull/1414): Forking sensors command to remove C package dependency.
|
||||||
|
- [#1389](https://github.com/influxdata/telegraf/pull/1389): Add a new SNMP plugin.
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
|
|
||||||
|
|
2
Godeps
2
Godeps
|
@ -47,7 +47,7 @@ github.com/prometheus/common e8eabff8812b05acf522b45fdcd725a785188e37
|
||||||
github.com/prometheus/procfs 406e5b7bfd8201a36e2bb5f7bdae0b03380c2ce8
|
github.com/prometheus/procfs 406e5b7bfd8201a36e2bb5f7bdae0b03380c2ce8
|
||||||
github.com/samuel/go-zookeeper 218e9c81c0dd8b3b18172b2bbfad92cc7d6db55f
|
github.com/samuel/go-zookeeper 218e9c81c0dd8b3b18172b2bbfad92cc7d6db55f
|
||||||
github.com/shirou/gopsutil 4d0c402af66c78735c5ccf820dc2ca7de5e4ff08
|
github.com/shirou/gopsutil 4d0c402af66c78735c5ccf820dc2ca7de5e4ff08
|
||||||
github.com/soniah/gosnmp b1b4f885b12c5dcbd021c5cee1c904110de6db7d
|
github.com/soniah/gosnmp eb32571c2410868d85849ad67d1e51d01273eb84
|
||||||
github.com/sparrc/aerospike-client-go d4bb42d2c2d39dae68e054116f4538af189e05d5
|
github.com/sparrc/aerospike-client-go d4bb42d2c2d39dae68e054116f4538af189e05d5
|
||||||
github.com/streadway/amqp b4f3ceab0337f013208d31348b578d83c0064744
|
github.com/streadway/amqp b4f3ceab0337f013208d31348b578d83c0064744
|
||||||
github.com/stretchr/testify 1f4a1643a57e798696635ea4c126e9127adb7d3c
|
github.com/stretchr/testify 1f4a1643a57e798696635ea4c126e9127adb7d3c
|
||||||
|
|
|
@ -190,6 +190,7 @@ Currently implemented sources:
|
||||||
* [riak](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/riak)
|
* [riak](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/riak)
|
||||||
* [sensors](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/sensors)
|
* [sensors](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/sensors)
|
||||||
* [snmp](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/snmp)
|
* [snmp](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/snmp)
|
||||||
|
* [snmp_legacy](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/snmp_legacy)
|
||||||
* [sql server](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/sqlserver) (microsoft)
|
* [sql server](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/sqlserver) (microsoft)
|
||||||
* [twemproxy](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/twemproxy)
|
* [twemproxy](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/twemproxy)
|
||||||
* [varnish](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/varnish)
|
* [varnish](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/varnish)
|
||||||
|
|
|
@ -60,6 +60,7 @@ import (
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/rethinkdb"
|
_ "github.com/influxdata/telegraf/plugins/inputs/rethinkdb"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/riak"
|
_ "github.com/influxdata/telegraf/plugins/inputs/riak"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/sensors"
|
_ "github.com/influxdata/telegraf/plugins/inputs/sensors"
|
||||||
|
_ "github.com/influxdata/telegraf/plugins/inputs/snmp"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/snmp_legacy"
|
_ "github.com/influxdata/telegraf/plugins/inputs/snmp_legacy"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/sqlserver"
|
_ "github.com/influxdata/telegraf/plugins/inputs/sqlserver"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/statsd"
|
_ "github.com/influxdata/telegraf/plugins/inputs/statsd"
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
# SNMP Plugin
|
||||||
|
|
||||||
|
The SNMP input plugin gathers metrics from SNMP agents.
|
||||||
|
|
||||||
|
## Configuration:
|
||||||
|
|
||||||
|
### Example:
|
||||||
|
|
||||||
|
SNMP data:
|
||||||
|
```
|
||||||
|
.1.0.0.0.1.1.0 octet_str "foo"
|
||||||
|
.1.0.0.0.1.1.1 octet_str "bar"
|
||||||
|
.1.0.0.0.1.102 octet_str "bad"
|
||||||
|
.1.0.0.0.1.2.0 integer 1
|
||||||
|
.1.0.0.0.1.2.1 integer 2
|
||||||
|
.1.0.0.0.1.3.0 octet_str "0.123"
|
||||||
|
.1.0.0.0.1.3.1 octet_str "0.456"
|
||||||
|
.1.0.0.0.1.3.2 octet_str "9.999"
|
||||||
|
.1.0.0.1.1 octet_str "baz"
|
||||||
|
.1.0.0.1.2 uinteger 54321
|
||||||
|
.1.0.0.1.3 uinteger 234
|
||||||
|
```
|
||||||
|
|
||||||
|
Telegraf config:
|
||||||
|
```toml
|
||||||
|
[[inputs.snmp]]
|
||||||
|
agents = [ "127.0.0.1:161" ]
|
||||||
|
version = 2
|
||||||
|
community = "public"
|
||||||
|
|
||||||
|
name = "system"
|
||||||
|
[[inputs.snmp.field]]
|
||||||
|
name = "hostname"
|
||||||
|
oid = ".1.0.0.1.1"
|
||||||
|
is_tag = true
|
||||||
|
[[inputs.snmp.field]]
|
||||||
|
name = "uptime"
|
||||||
|
oid = ".1.0.0.1.2"
|
||||||
|
[[inputs.snmp.field]]
|
||||||
|
name = "loadavg"
|
||||||
|
oid = ".1.0.0.1.3"
|
||||||
|
conversion = "float(2)"
|
||||||
|
|
||||||
|
[[inputs.snmp.table]]
|
||||||
|
name = "remote_servers"
|
||||||
|
inherit_tags = [ "hostname" ]
|
||||||
|
[[inputs.snmp.table.field]]
|
||||||
|
name = "server"
|
||||||
|
oid = ".1.0.0.0.1.1"
|
||||||
|
is_tag = true
|
||||||
|
[[inputs.snmp.table.field]]
|
||||||
|
name = "connections"
|
||||||
|
oid = ".1.0.0.0.1.2"
|
||||||
|
[[inputs.snmp.table.field]]
|
||||||
|
name = "latency"
|
||||||
|
oid = ".1.0.0.0.1.3"
|
||||||
|
conversion = "float"
|
||||||
|
```
|
||||||
|
|
||||||
|
Resulting output:
|
||||||
|
```
|
||||||
|
* Plugin: snmp, 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
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configuration via MIB:
|
||||||
|
|
||||||
|
This example uses the SNMP data above, but is configured via the MIB.
|
||||||
|
The example MIB file can be found in the `testdata` directory. See the [MIB lookups](#mib-lookups) section for more information.
|
||||||
|
|
||||||
|
Telegraf config:
|
||||||
|
```toml
|
||||||
|
[[inputs.snmp]]
|
||||||
|
agents = [ "127.0.0.1:161" ]
|
||||||
|
version = 2
|
||||||
|
community = "public"
|
||||||
|
|
||||||
|
[[inputs.snmp.field]]
|
||||||
|
oid = "TEST::hostname"
|
||||||
|
is_tag = true
|
||||||
|
|
||||||
|
[[inputs.snmp.table]]
|
||||||
|
oid = "TEST::testTable"
|
||||||
|
inherit_tags = "hostname"
|
||||||
|
```
|
||||||
|
|
||||||
|
Resulting output:
|
||||||
|
```
|
||||||
|
* Plugin: snmp, Collection 1
|
||||||
|
> testTable,agent_host=127.0.0.1,host=mylocalhost,hostname=baz,server=foo connections=1i,latency="0.123" 1468953135000000000
|
||||||
|
> testTable,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:
|
||||||
|
* `oid`:
|
||||||
|
OID to get. May be a numeric or textual OID.
|
||||||
|
|
||||||
|
* `name`:
|
||||||
|
Output field/tag name.
|
||||||
|
If not specified, it defaults to the value of `oid`. If `oid` is numeric, an attempt to translate the numeric OID into a texual OID will be made.
|
||||||
|
|
||||||
|
* `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:
|
||||||
|
* `oid`:
|
||||||
|
Automatically populates the table's fields using data from the MIB.
|
||||||
|
|
||||||
|
* `name`:
|
||||||
|
Output measurement name.
|
||||||
|
If not specified, it defaults to the value of `oid`. If `oid` is numeric, an attempt to translate the numeric OID into a texual OID will be made.
|
||||||
|
|
||||||
|
* `inherit_tags`:
|
||||||
|
Which tags to inherit from the top-level config and to use in the output of this table's measurement.
|
||||||
|
|
||||||
|
### MIB lookups
|
||||||
|
If the plugin is configured such that it needs to perform lookups from the MIB, it will use the net-snmp utilities `snmptranslate` and `snmptable`.
|
||||||
|
|
||||||
|
When performing the lookups, the plugin will load all available MIBs. If your MIB files are in a custom path, you may add the path using the `MIBDIRS` environment variable. See [`man 1 snmpcmd`](http://net-snmp.sourceforge.net/docs/man/snmpcmd.html#lbAK) for more information on the variable.
|
|
@ -0,0 +1,791 @@
|
||||||
|
package snmp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/influxdata/telegraf"
|
||||||
|
"github.com/influxdata/telegraf/internal"
|
||||||
|
"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" ]
|
||||||
|
timeout = "5s"
|
||||||
|
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.snmp.field]]
|
||||||
|
name = "hostname"
|
||||||
|
oid = ".1.0.0.1.1"
|
||||||
|
[[inputs.snmp.field]]
|
||||||
|
name = "uptime"
|
||||||
|
oid = ".1.0.0.1.2"
|
||||||
|
[[inputs.snmp.field]]
|
||||||
|
name = "load"
|
||||||
|
oid = ".1.0.0.1.3"
|
||||||
|
[[inputs.snmp.field]]
|
||||||
|
oid = "HOST-RESOURCES-MIB::hrMemorySize"
|
||||||
|
|
||||||
|
[[inputs.snmp.table]]
|
||||||
|
# measurement name
|
||||||
|
name = "remote_servers"
|
||||||
|
inherit_tags = [ "hostname" ]
|
||||||
|
[[inputs.snmp.table.field]]
|
||||||
|
name = "server"
|
||||||
|
oid = ".1.0.0.0.1.0"
|
||||||
|
is_tag = true
|
||||||
|
[[inputs.snmp.table.field]]
|
||||||
|
name = "connections"
|
||||||
|
oid = ".1.0.0.0.1.1"
|
||||||
|
[[inputs.snmp.table.field]]
|
||||||
|
name = "latency"
|
||||||
|
oid = ".1.0.0.0.1.2"
|
||||||
|
|
||||||
|
[[inputs.snmp.table]]
|
||||||
|
# auto populate table's fields using the MIB
|
||||||
|
oid = "HOST-RESOURCES-MIB::hrNetworkTable"
|
||||||
|
`
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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, "\n")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snmp holds the configuration for the plugin.
|
||||||
|
type Snmp 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.
|
||||||
|
Timeout internal.Duration
|
||||||
|
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
|
||||||
|
initialized bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Snmp) init() error {
|
||||||
|
if s.initialized {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range s.Tables {
|
||||||
|
if err := s.Tables[i].init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range s.Fields {
|
||||||
|
if err := s.Fields[i].init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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() populates Fields if a table OID is provided.
|
||||||
|
func (t *Table) init() error {
|
||||||
|
if t.initialized {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if t.Oid == "" {
|
||||||
|
t.initialized = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mibPrefix := ""
|
||||||
|
if err := snmpTranslate(&mibPrefix, &t.Oid, &t.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// first attempt to get the table's tags
|
||||||
|
tagOids := map[string]struct{}{}
|
||||||
|
// We have to guess that the "entry" oid is `t.Oid+".1"`. snmptable and snmptranslate don't seem to have a way to provide the info.
|
||||||
|
if out, err := execCmd("snmptranslate", "-m", "all", "-Td", t.Oid+".1"); err == nil {
|
||||||
|
lines := bytes.Split(out, []byte{'\n'})
|
||||||
|
// get the MIB name if we didn't get it above
|
||||||
|
if mibPrefix == "" {
|
||||||
|
if i := bytes.Index(lines[0], []byte("::")); i != -1 {
|
||||||
|
mibPrefix = string(lines[0][:i+2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
if !bytes.HasPrefix(line, []byte(" INDEX")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
i := bytes.Index(line, []byte("{ "))
|
||||||
|
if i == -1 { // parse error
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line = line[i+2:]
|
||||||
|
i = bytes.Index(line, []byte(" }"))
|
||||||
|
if i == -1 { // parse error
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line = line[:i]
|
||||||
|
for _, col := range bytes.Split(line, []byte(", ")) {
|
||||||
|
tagOids[mibPrefix+string(col)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this won't actually try to run a query. The `-Ch` will just cause it to dump headers.
|
||||||
|
out, err := execCmd("snmptable", "-m", "all", "-Ch", "-Cl", "-c", "public", "127.0.0.1", t.Oid)
|
||||||
|
if err != nil {
|
||||||
|
return Errorf(err, "getting table columns for %s", t.Oid)
|
||||||
|
}
|
||||||
|
cols := bytes.SplitN(out, []byte{'\n'}, 2)[0]
|
||||||
|
if len(cols) == 0 {
|
||||||
|
return fmt.Errorf("unable to get columns for table %s", t.Oid)
|
||||||
|
}
|
||||||
|
for _, col := range bytes.Split(cols, []byte{' '}) {
|
||||||
|
if len(col) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
col := string(col)
|
||||||
|
_, isTag := tagOids[mibPrefix+col]
|
||||||
|
t.Fields = append(t.Fields, Field{Name: col, Oid: mibPrefix + col, IsTag: isTag})
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize all the nested fields
|
||||||
|
for i := range t.Fields {
|
||||||
|
if err := t.Fields[i].init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.initialized = true
|
||||||
|
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
|
||||||
|
// 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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := snmpTranslate(nil, &f.Oid, &f.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//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{
|
||||||
|
Retries: 5,
|
||||||
|
MaxRepetitions: 50,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, agent := range s.Agents {
|
||||||
|
gs, err := s.getConnection(agent)
|
||||||
|
if err != nil {
|
||||||
|
acc.AddError(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 {
|
||||||
|
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", agent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
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 *Snmp) 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)
|
||||||
|
|
||||||
|
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 = 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// snmpTranslate resolves the given OID.
|
||||||
|
// The contents of the oid parameter will be replaced with the numeric oid value.
|
||||||
|
// If name is empty, the textual OID value is stored in it. If the textual OID cannot be translated, the numeric OID is stored instead.
|
||||||
|
// If mibPrefix is non-nil, the MIB in which the OID was found is stored, with a suffix of "::".
|
||||||
|
func snmpTranslate(mibPrefix *string, oid *string, name *string) error {
|
||||||
|
if strings.ContainsAny(*oid, ":abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") {
|
||||||
|
out, err := execCmd("snmptranslate", "-m", "all", "-On", *oid)
|
||||||
|
if err != nil {
|
||||||
|
return Errorf(err, "translating %s", *oid)
|
||||||
|
}
|
||||||
|
*oid = string(bytes.TrimSuffix(out, []byte{'\n'}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if *name == "" {
|
||||||
|
out, err := execCmd("snmptranslate", "-m", "all", *oid)
|
||||||
|
if err != nil {
|
||||||
|
//TODO debug message
|
||||||
|
*name = *oid
|
||||||
|
} else {
|
||||||
|
if i := bytes.Index(out, []byte("::")); i != -1 {
|
||||||
|
if mibPrefix != nil {
|
||||||
|
*mibPrefix = string(out[:i+2])
|
||||||
|
}
|
||||||
|
out = out[i+2:]
|
||||||
|
}
|
||||||
|
*name = string(bytes.TrimSuffix(out, []byte{'\n'}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,641 @@
|
||||||
|
package snmp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/influxdata/telegraf/internal"
|
||||||
|
"github.com/influxdata/telegraf/testutil"
|
||||||
|
"github.com/influxdata/toml"
|
||||||
|
"github.com/soniah/gosnmp"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mockExecCommand(arg0 string, args ...string) *exec.Cmd {
|
||||||
|
args = append([]string{"-test.run=TestMockExecCommand", "--", arg0}, args...)
|
||||||
|
cmd := exec.Command(os.Args[0], args...)
|
||||||
|
cmd.Stderr = os.Stderr // so the test output shows errors
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
func TestMockExecCommand(t *testing.T) {
|
||||||
|
var cmd []string
|
||||||
|
for _, arg := range os.Args {
|
||||||
|
if string(arg) == "--" {
|
||||||
|
cmd = []string{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cmd == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmd = append(cmd, string(arg))
|
||||||
|
}
|
||||||
|
if cmd == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// will not properly handle args with spaces, but it's good enough
|
||||||
|
cmdStr := strings.Join(cmd, " ")
|
||||||
|
switch cmdStr {
|
||||||
|
case "snmptranslate -m all .1.0.0.0":
|
||||||
|
fmt.Printf("TEST::testTable\n")
|
||||||
|
case "snmptranslate -m all .1.0.0.0.1.1":
|
||||||
|
fmt.Printf("server\n")
|
||||||
|
case "snmptranslate -m all .1.0.0.0.1.1.0":
|
||||||
|
fmt.Printf("server.0\n")
|
||||||
|
case "snmptranslate -m all .1.0.0.1.1":
|
||||||
|
fmt.Printf("hostname\n")
|
||||||
|
case "snmptranslate -m all .999":
|
||||||
|
fmt.Printf(".999\n")
|
||||||
|
case "snmptranslate -m all -On TEST::testTable":
|
||||||
|
fmt.Printf(".1.0.0.0\n")
|
||||||
|
case "snmptranslate -m all -On TEST::hostname":
|
||||||
|
fmt.Printf(".1.0.0.1.1\n")
|
||||||
|
case "snmptranslate -m all -On TEST::server":
|
||||||
|
fmt.Printf(".1.0.0.0.1.1\n")
|
||||||
|
case "snmptranslate -m all -On TEST::connections":
|
||||||
|
fmt.Printf(".1.0.0.0.1.2\n")
|
||||||
|
case "snmptranslate -m all -On TEST::latency":
|
||||||
|
fmt.Printf(".1.0.0.0.1.3\n")
|
||||||
|
case "snmptranslate -m all -On TEST::server.0":
|
||||||
|
fmt.Printf(".1.0.0.0.1.1.0\n")
|
||||||
|
case "snmptranslate -m all -Td .1.0.0.0.1":
|
||||||
|
fmt.Printf(`TEST::testTableEntry
|
||||||
|
testTableEntry OBJECT-TYPE
|
||||||
|
-- FROM TEST
|
||||||
|
MAX-ACCESS not-accessible
|
||||||
|
STATUS current
|
||||||
|
INDEX { server }
|
||||||
|
::= { iso(1) 2 testOID(3) testTable(0) 1 }
|
||||||
|
`)
|
||||||
|
case "snmptable -m all -Ch -Cl -c public 127.0.0.1 .1.0.0.0":
|
||||||
|
fmt.Printf(`server connections latency
|
||||||
|
TEST::testTable: No entries
|
||||||
|
`)
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "Command not mocked: `%s`\n", cmdStr)
|
||||||
|
// you get the expected output by running the missing command with `-M testdata` in the plugin directory.
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
func init() {
|
||||||
|
execCommand = mockExecCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
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.0.0.0.1.1.0": "foo",
|
||||||
|
".1.0.0.0.1.1.1": []byte("bar"),
|
||||||
|
".1.0.0.0.1.102": "bad",
|
||||||
|
".1.0.0.0.1.2.0": 1,
|
||||||
|
".1.0.0.0.1.2.1": 2,
|
||||||
|
".1.0.0.0.1.3.0": "0.123",
|
||||||
|
".1.0.0.0.1.3.1": "0.456",
|
||||||
|
".1.0.0.0.1.3.2": "9.999",
|
||||||
|
".1.0.0.0.1.4.0": 123456,
|
||||||
|
".1.0.0.1.1": "baz",
|
||||||
|
".1.0.0.1.2": 234,
|
||||||
|
".1.0.0.1.3": []byte("byte slice"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSampleConfig(t *testing.T) {
|
||||||
|
conf := struct {
|
||||||
|
Inputs struct {
|
||||||
|
Snmp []*Snmp
|
||||||
|
}
|
||||||
|
}{}
|
||||||
|
err := toml.Unmarshal([]byte("[[inputs.snmp]]\n"+(*Snmp)(nil).SampleConfig()), &conf)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
s := Snmp{
|
||||||
|
Agents: []string{"127.0.0.1:161"},
|
||||||
|
Timeout: internal.Duration{Duration: 5 * time.Second},
|
||||||
|
Version: 2,
|
||||||
|
Community: "public",
|
||||||
|
MaxRepetitions: 50,
|
||||||
|
|
||||||
|
Name: "system",
|
||||||
|
Fields: []Field{
|
||||||
|
{Name: "hostname", Oid: ".1.0.0.1.1"},
|
||||||
|
{Name: "uptime", Oid: ".1.0.0.1.2"},
|
||||||
|
{Name: "load", Oid: ".1.0.0.1.3"},
|
||||||
|
{Oid: "HOST-RESOURCES-MIB::hrMemorySize"},
|
||||||
|
},
|
||||||
|
Tables: []Table{
|
||||||
|
{
|
||||||
|
Name: "remote_servers",
|
||||||
|
InheritTags: []string{"hostname"},
|
||||||
|
Fields: []Field{
|
||||||
|
{Name: "server", Oid: ".1.0.0.0.1.0", IsTag: true},
|
||||||
|
{Name: "connections", Oid: ".1.0.0.0.1.1"},
|
||||||
|
{Name: "latency", Oid: ".1.0.0.0.1.2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Oid: "HOST-RESOURCES-MIB::hrNetworkTable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, s, *conf.Inputs.Snmp[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldInit(t *testing.T) {
|
||||||
|
translations := []struct {
|
||||||
|
inputOid string
|
||||||
|
inputName string
|
||||||
|
expectedOid string
|
||||||
|
expectedName string
|
||||||
|
}{
|
||||||
|
{".1.0.0.0.1.1", "", ".1.0.0.0.1.1", "server"},
|
||||||
|
{".1.0.0.0.1.1.0", "", ".1.0.0.0.1.1.0", "server.0"},
|
||||||
|
{".999", "", ".999", ".999"},
|
||||||
|
{"TEST::server", "", ".1.0.0.0.1.1", "server"},
|
||||||
|
{"TEST::server.0", "", ".1.0.0.0.1.1.0", "server.0"},
|
||||||
|
{"TEST::server", "foo", ".1.0.0.0.1.1", "foo"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, txl := range translations {
|
||||||
|
f := Field{Oid: txl.inputOid, Name: txl.inputName}
|
||||||
|
err := f.init()
|
||||||
|
if !assert.NoError(t, err, "inputOid='%s' inputName='%s'", txl.inputOid, txl.inputName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assert.Equal(t, txl.expectedOid, f.Oid, "inputOid='%s' inputName='%s'", txl.inputOid, txl.inputName)
|
||||||
|
assert.Equal(t, txl.expectedName, f.Name, "inputOid='%s' inputName='%s'", txl.inputOid, txl.inputName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTableInit(t *testing.T) {
|
||||||
|
tbl := Table{
|
||||||
|
Oid: ".1.0.0.0",
|
||||||
|
Fields: []Field{{Oid: ".999", Name: "foo"}},
|
||||||
|
}
|
||||||
|
err := tbl.init()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "testTable", tbl.Name)
|
||||||
|
|
||||||
|
assert.Len(t, tbl.Fields, 4)
|
||||||
|
assert.Contains(t, tbl.Fields, Field{Oid: ".999", Name: "foo", initialized: true})
|
||||||
|
assert.Contains(t, tbl.Fields, Field{Oid: ".1.0.0.0.1.1", Name: "server", IsTag: true, initialized: true})
|
||||||
|
assert.Contains(t, tbl.Fields, Field{Oid: ".1.0.0.0.1.2", Name: "connections", initialized: true})
|
||||||
|
assert.Contains(t, tbl.Fields, Field{Oid: ".1.0.0.0.1.3", Name: "latency", initialized: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSnmpInit(t *testing.T) {
|
||||||
|
s := &Snmp{
|
||||||
|
Tables: []Table{
|
||||||
|
{Oid: "TEST::testTable"},
|
||||||
|
},
|
||||||
|
Fields: []Field{
|
||||||
|
{Oid: "TEST::hostname"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.init()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, s.Tables[0].Fields, 3)
|
||||||
|
assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.0.0.0.1.1", Name: "server", IsTag: true, initialized: true})
|
||||||
|
assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.0.0.0.1.2", Name: "connections", initialized: true})
|
||||||
|
assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.0.0.0.1.3", Name: "latency", initialized: true})
|
||||||
|
|
||||||
|
assert.Equal(t, Field{
|
||||||
|
Oid: ".1.0.0.1.1",
|
||||||
|
Name: "hostname",
|
||||||
|
initialized: true,
|
||||||
|
}, s.Fields[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSNMPConnection_v2(t *testing.T) {
|
||||||
|
s := &Snmp{
|
||||||
|
Timeout: internal.Duration{Duration: 3 * time.Second},
|
||||||
|
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 := &Snmp{
|
||||||
|
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 := &Snmp{}
|
||||||
|
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.0.0", 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.0.0"})
|
||||||
|
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.0.0.0.1.1",
|
||||||
|
IsTag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "myfield2",
|
||||||
|
Oid: ".1.0.0.0.1.2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "myfield3",
|
||||||
|
Oid: ".1.0.0.0.1.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.0.0.1.1",
|
||||||
|
IsTag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "myfield2",
|
||||||
|
Oid: ".1.0.0.1.2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "myfield3",
|
||||||
|
Oid: ".1.0.0.1.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 := &Snmp{
|
||||||
|
Agents: []string{"TestGather"},
|
||||||
|
Name: "mytable",
|
||||||
|
Fields: []Field{
|
||||||
|
{
|
||||||
|
Name: "myfield1",
|
||||||
|
Oid: ".1.0.0.1.1",
|
||||||
|
IsTag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "myfield2",
|
||||||
|
Oid: ".1.0.0.1.2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "myfield3",
|
||||||
|
Oid: "1.0.0.1.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tables: []Table{
|
||||||
|
{
|
||||||
|
Name: "myOtherTable",
|
||||||
|
InheritTags: []string{"myfield1"},
|
||||||
|
Fields: []Field{
|
||||||
|
{
|
||||||
|
Name: "myOtherField",
|
||||||
|
Oid: ".1.0.0.0.1.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 := &Snmp{
|
||||||
|
Agents: []string{"TestGather"},
|
||||||
|
Name: "mytable",
|
||||||
|
Fields: []Field{
|
||||||
|
{
|
||||||
|
Name: "host",
|
||||||
|
Oid: ".1.0.0.1.1",
|
||||||
|
IsTag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "myfield2",
|
||||||
|
Oid: ".1.0.0.1.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")
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
# This config provides the data represented in the plugin documentation
|
||||||
|
# Requires net-snmp >= 5.7
|
||||||
|
|
||||||
|
#agentaddress UDP:127.0.0.1:1161
|
||||||
|
rocommunity public
|
||||||
|
|
||||||
|
override .1.0.0.0.1.1.0 octet_str "foo"
|
||||||
|
override .1.0.0.0.1.1.1 octet_str "bar"
|
||||||
|
override .1.0.0.0.1.102 octet_str "bad"
|
||||||
|
override .1.0.0.0.1.2.0 integer 1
|
||||||
|
override .1.0.0.0.1.2.1 integer 2
|
||||||
|
override .1.0.0.0.1.3.0 octet_str "0.123"
|
||||||
|
override .1.0.0.0.1.3.1 octet_str "0.456"
|
||||||
|
override .1.0.0.0.1.3.2 octet_str "9.999"
|
||||||
|
override .1.0.0.1.1 octet_str "baz"
|
||||||
|
override .1.0.0.1.2 uinteger 54321
|
||||||
|
override .1.0.0.1.3 uinteger 234
|
|
@ -0,0 +1,51 @@
|
||||||
|
TEST DEFINITIONS ::= BEGIN
|
||||||
|
|
||||||
|
testOID ::= { 1 0 0 }
|
||||||
|
|
||||||
|
testTable OBJECT-TYPE
|
||||||
|
SYNTAX SEQUENCE OF testTableEntry
|
||||||
|
MAX-ACCESS not-accessible
|
||||||
|
STATUS current
|
||||||
|
::= { testOID 0 }
|
||||||
|
|
||||||
|
testTableEntry OBJECT-TYPE
|
||||||
|
SYNTAX TestTableEntry
|
||||||
|
MAX-ACCESS not-accessible
|
||||||
|
STATUS current
|
||||||
|
INDEX {
|
||||||
|
server
|
||||||
|
}
|
||||||
|
::= { testTable 1 }
|
||||||
|
|
||||||
|
TestTableEntry ::=
|
||||||
|
SEQUENCE {
|
||||||
|
server OCTET STRING,
|
||||||
|
connections INTEGER,
|
||||||
|
latency OCTET STRING,
|
||||||
|
}
|
||||||
|
|
||||||
|
server OBJECT-TYPE
|
||||||
|
SYNTAX OCTET STRING
|
||||||
|
MAX-ACCESS read-only
|
||||||
|
STATUS current
|
||||||
|
::= { testTableEntry 1 }
|
||||||
|
|
||||||
|
connections OBJECT-TYPE
|
||||||
|
SYNTAX INTEGER
|
||||||
|
MAX-ACCESS read-only
|
||||||
|
STATUS current
|
||||||
|
::= { testTableEntry 2 }
|
||||||
|
|
||||||
|
latency OBJECT-TYPE
|
||||||
|
SYNTAX OCTET STRING
|
||||||
|
MAX-ACCESS read-only
|
||||||
|
STATUS current
|
||||||
|
::= { testTableEntry 3 }
|
||||||
|
|
||||||
|
hostname OBJECT-TYPE
|
||||||
|
SYNTAX OCTET STRING
|
||||||
|
MAX-ACCESS read-only
|
||||||
|
STATUS current
|
||||||
|
::= { testOID 1 1 }
|
||||||
|
|
||||||
|
END
|
Loading…
Reference in New Issue