diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go
index 781e04e60928b..39cd35ed91b3c 100644
--- a/plugins/inputs/all/all.go
+++ b/plugins/inputs/all/all.go
@@ -166,6 +166,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/sflow"
_ "github.com/influxdata/telegraf/plugins/inputs/smart"
_ "github.com/influxdata/telegraf/plugins/inputs/snmp"
+ _ "github.com/influxdata/telegraf/plugins/inputs/snmp_heartbeat"
_ "github.com/influxdata/telegraf/plugins/inputs/snmp_legacy"
_ "github.com/influxdata/telegraf/plugins/inputs/snmp_trap"
_ "github.com/influxdata/telegraf/plugins/inputs/socket_listener"
diff --git a/plugins/inputs/snmp/README.md b/plugins/inputs/snmp/README.md
index 0d52881a72f04..9099fd63d8392 100644
--- a/plugins/inputs/snmp/README.md
+++ b/plugins/inputs/snmp/README.md
@@ -49,6 +49,9 @@ information.
## The GETBULK max-repetitions parameter.
# max_repetitions = 10
+ ##Include period & timestamp in each document
+ #include_time_info = true
+
## SNMPv3 authentication and encryption options.
##
## Security Name.
diff --git a/plugins/inputs/snmp/snmp.go b/plugins/inputs/snmp/snmp.go
index 7f2df6b689eac..0b7ad96240ca5 100644
--- a/plugins/inputs/snmp/snmp.go
+++ b/plugins/inputs/snmp/snmp.go
@@ -10,6 +10,7 @@ import (
"math"
"net"
"os/exec"
+ "regexp"
"strconv"
"strings"
"sync"
@@ -74,6 +75,28 @@ const sampleConfig = `
## example collects the system uptime and interface variables. Reference the
## full plugin documentation for configuration details.
`
+const (
+ SYSTEM_NAME_OID = ".1.3.6.1.2.1.1.5"
+ IP_ADDRESS_OID = ".1.3.6.1.2.1.4.20.1.2"
+ SYSTEM_CONTACT_OID = ".1.3.6.1.2.1.1.4"
+ SYSTEM_LOCATION_OID = ".1.3.6.1.2.1.1.6"
+ IF_ADMIN_STATUS = ".1.3.6.1.2.1.2.2.1.7"
+ INTERFACE_STATUS = ".1.3.6.1.2.1.2.2.1.8"
+ IP_ADDRESS_REGEX = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\.|$)){4}$"
+ VERSION_REGEX = `(Cisco IOS Software),\s+(.*)\s+Software\s+(.*)`
+ HUAWEI_VERSION_REGEX = `Version ([0-9.]+)[^\r\n]*(V[0-9A-Z]+)`
+ EMPTY_STRING = ""
+ SYSTEM_DESCRIPTION = ".1.3.6.1.2.1.1.1"
+)
+const (
+ STATUS_UP = "UP"
+ STATUS_TEST = "TEST"
+ STATUS_DOWN = "DOWN"
+)
+const (
+ STATUS_UP_NUM = 1
+ STATUS_TEST_NUM = 3
+)
// execCommand is so tests can mock out exec.Command usage.
var execCommand = exec.Command
@@ -110,8 +133,10 @@ type Snmp struct {
snmp.ClientConfig
- Tables []Table `toml:"table"`
-
+ Tables []Table `toml:"table"`
+ Include_time_info bool `toml:"include_time_info"`
+ Period string `toml:"interval"`
+ Packets int64 `toml:"packets"`
// 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.
@@ -130,6 +155,7 @@ func (s *Snmp) init() error {
s.connectionCache = make([]snmpConnection, len(s.Agents))
for i := range s.Tables {
+
if err := s.Tables[i].Init(); err != nil {
return fmt.Errorf("initializing table %s: %w", s.Tables[i].Name, err)
}
@@ -189,11 +215,12 @@ func (t *Table) Init() error {
// initialize all the nested fields
for i := range t.Fields {
+
if err := t.Fields[i].init(); err != nil {
+
return fmt.Errorf("initializing field %s: %w", t.Fields[i].Name, err)
}
}
-
t.initialized = true
return nil
}
@@ -205,8 +232,8 @@ func (t *Table) initBuild() error {
if t.Oid == "" {
return nil
}
-
_, _, oidText, fields, err := snmpTable(t.Oid)
+
if err != nil {
return err
}
@@ -216,6 +243,7 @@ func (t *Table) initBuild() error {
}
knownOIDs := map[string]bool{}
+
for _, f := range t.Fields {
knownOIDs[f.Oid] = true
}
@@ -223,8 +251,8 @@ func (t *Table) initBuild() error {
if !knownOIDs[f.Oid] {
t.Fields = append(t.Fields, f)
}
- }
+ }
return nil
}
@@ -258,26 +286,29 @@ type Field struct {
// init() converts OID names to numbers, and sets the .Name attribute if unset.
func (f *Field) init() error {
+
if f.initialized {
return nil
}
// check if oid needs translation or name is not set
if strings.ContainsAny(f.Oid, ":abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") || f.Name == "" {
+
_, oidNum, oidText, conversion, err := SnmpTranslate(f.Oid)
if err != nil {
return fmt.Errorf("translating: %w", err)
}
+
f.Oid = oidNum
if f.Name == "" {
f.Name = oidText
}
+
if f.Conversion == "" {
f.Conversion = conversion
}
//TODO use textual convention conversion from the MIB
}
-
f.initialized = true
return nil
}
@@ -363,9 +394,39 @@ func (s *Snmp) Gather(acc telegraf.Accumulator) error {
Name: s.Name,
Fields: s.Fields,
}
+
topTags := map[string]string{}
if err := s.gatherTable(acc, gs, t, topTags, false); err != nil {
- acc.AddError(fmt.Errorf("agent %s: %w", agent, err))
+ if s.Include_time_info {
+ rt := RTable{
+ Name: t.Name,
+ Time: time.Now(), //TODO record time at start
+ Rows: make([]RTableRow, 0, 1),
+ }
+ rtr := RTableRow{}
+ rtr.Tags = map[string]string{}
+ rtr.Fields = map[string]interface{}{}
+ if _, ok := rtr.Tags[s.AgentHostTag]; !ok {
+ rtr.Tags[s.AgentHostTag] = gs.Host()
+ }
+ rtr.Fields["@timestamp"] = 0
+ if strings.Contains(s.Period, "m") {
+ period_str := strings.Trim(s.Period, "m")
+ period, err := strconv.Atoi(period_str)
+ if err == nil {
+ rtr.Fields["period"] = period * 60
+ }
+ } else {
+ period_str := strings.Trim(s.Period, "s")
+ rtr.Fields["period"], err = strconv.Atoi(period_str)
+ }
+
+ rt.Rows = append(rt.Rows, rtr)
+
+ acc.AddFields(rt.Name, rtr.Fields, rtr.Tags, rt.Time)
+ } else {
+ acc.AddError(fmt.Errorf("agent %s: %w", agent, err))
+ }
}
// Now is the real tables.
@@ -377,11 +438,11 @@ func (s *Snmp) Gather(acc telegraf.Accumulator) error {
}(i, agent)
}
wg.Wait()
-
return nil
}
func (s *Snmp) gatherTable(acc telegraf.Accumulator, gs snmpConnection, t Table, topTags map[string]string, walk bool) error {
+
rt, err := t.Build(gs, walk)
if err != nil {
return err
@@ -393,26 +454,41 @@ func (s *Snmp) gatherTable(acc telegraf.Accumulator, gs snmpConnection, t Table,
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 s.Include_time_info {
+ t := time.Now().UTC()
+ tr.Fields["@timestamp"] = t.Format("2006-01-02T15:04:05.000Z")
+ if strings.Contains(s.Period, "m") {
+ period, err := strconv.Atoi(strings.Trim(s.Period, "m"))
+ if err == nil {
+ tr.Fields["period"] = period * 60
+ }
+ } else {
+ tr.Fields["period"], err = strconv.Atoi(strings.Trim(s.Period, "s"))
+ }
+ }
if _, ok := tr.Tags[s.AgentHostTag]; !ok {
tr.Tags[s.AgentHostTag] = 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{}
+ var index_arr []string
tagCount := 0
for _, f := range t.Fields {
@@ -430,7 +506,6 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
// 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{}{}
@@ -468,11 +543,13 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
idx := ent.Name[len(oid):]
if f.OidIndexSuffix != "" {
+
if !strings.HasSuffix(idx, f.OidIndexSuffix) {
// this entry doesn't match our OidIndexSuffix. skip it
return nil
}
- idx = idx[:len(idx)-len(f.OidIndexSuffix)]
+ //idx = idx[:len(idx)-len(f.OidIndexSuffix)]
+ idx = f.OidIndexSuffix
}
if f.OidIndexLength != 0 {
i := f.OidIndexLength + 1 // leading separator
@@ -486,7 +563,9 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
return r
}, idx)
}
-
+ //log.Printf("Value of IDX is %s",idx)
+ //index_arr := []string{}
+ index_arr = append(index_arr, idx)
// snmptranslate table field value here
if f.Translate {
if entOid, ok := ent.Value.(string); ok {
@@ -505,7 +584,23 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
err: err,
}
}
- ifv[idx] = fv
+
+ if f.Oid == SYSTEM_NAME_OID && idx == ".0" {
+ //log.Printf("Array_index is : %+v length is: %d",index_arr,len(index_arr))
+ addSystemName(index_arr, ifv, fv)
+ } else if f.Oid == IP_ADDRESS_OID {
+ addIPAddr(ifv, fv, idx)
+ } else if f.Oid == SYSTEM_CONTACT_OID || f.Oid == SYSTEM_LOCATION_OID {
+ replaceEmptyString(ifv, fv, idx)
+ } else if f.Name == "software_version" && f.Oid == SYSTEM_DESCRIPTION {
+ extractSoftwareVersion(ifv, fv, t.Name)
+ } else if f.Name == "hardware_version" && t.Name == "cisco_hardware_software_data" && f.Oid == SYSTEM_DESCRIPTION {
+ extractHardwareVersion(ifv, fv)
+ } else if f.Oid == IF_ADMIN_STATUS || f.Oid == INTERFACE_STATUS {
+ statusConversion(ifv, fv, idx)
+ } else {
+ ifv[idx] = fv
+ }
return nil
})
if err != nil {
@@ -517,9 +612,10 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
}
}
}
-
for idx, v := range ifv {
+
rtr, ok := rows[idx]
+
if !ok {
rtr = RTableRow{}
rtr.Tags = map[string]string{}
@@ -552,11 +648,81 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
Time: time.Now(), //TODO record time at start
Rows: make([]RTableRow, 0, len(rows)),
}
+
for _, r := range rows {
rt.Rows = append(rt.Rows, r)
}
return &rt, nil
}
+func addSystemName(index_arr []string, ifv map[string]interface{}, fv interface{}) {
+ for i := 0; i <= len(index_arr)-2; i++ {
+ ifv[index_arr[i]] = fv
+ }
+}
+func addIPAddr(ifv map[string]interface{}, fv interface{}, idx string) {
+ match, _ := regexp.MatchString(IP_ADDRESS_REGEX, idx[1:])
+ if match {
+ ifv[fmt.Sprint(".", strconv.Itoa(fv.(int)))] = idx[1:]
+ }
+}
+
+func extractSoftwareVersion(ifv map[string]interface{}, fv interface{}, tableName string) {
+ if tableName == "cisco_hardware_software_data" {
+ str := fv.(string)
+ //r := regexp.MustCompile(`(Cisco IOS Software),\s+(.*)\s+Software\s+(.*)`)
+ r := regexp.MustCompile(VERSION_REGEX)
+ groups := r.FindAllStringSubmatch(str, -1)
+ if len(groups) == 0 {
+ ifv[".0"] = fmt.Sprint(".")
+ } else {
+ str_1 := groups[0][1]
+ str_2 := groups[0][2]
+ str_3 := groups[0][3]
+ ifv[".0"] = fmt.Sprint(str_1, " ", str_2, " ", str_3)
+ }
+ } else if tableName == "huawei_hardware_software_data" {
+ str := fv.(string)
+ //r := regexp.MustCompile(`Version ([0-9.]+)[^\r\n]*(V[0-9A-Z]+)`)
+ r := regexp.MustCompile(HUAWEI_VERSION_REGEX)
+ groups := r.FindAllStringSubmatch(str, -1)
+ if len(groups) == 0 {
+ ifv[".0"] = fmt.Sprint(".")
+ } else {
+ str_1 := groups[0][0]
+ ifv[".0"] = fmt.Sprint(str_1)
+ }
+ }
+
+}
+
+func extractHardwareVersion(ifv map[string]interface{}, fv interface{}) {
+ str := fv.(string)
+ //r := regexp.MustCompile(`(Cisco IOS Software),\s+(.*)\s+Software\s+(.*)`)
+ r := regexp.MustCompile(VERSION_REGEX)
+ groups := r.FindAllStringSubmatch(str, -1)
+ if len(groups) == 0 {
+ ifv[".0"] = fmt.Sprint(".")
+ } else {
+ str_2 := groups[0][2]
+ ifv[".0"] = fmt.Sprint(str_2)
+ }
+}
+func statusConversion(ifv map[string]interface{}, fv interface{}, idx string) {
+ if fv == STATUS_UP_NUM {
+ ifv[idx] = STATUS_UP
+ } else if fv == STATUS_TEST_NUM {
+ ifv[idx] = STATUS_TEST
+ } else {
+ ifv[idx] = STATUS_DOWN
+ }
+}
+func replaceEmptyString(ifv map[string]interface{}, fv interface{}, idx string) {
+ if fv == EMPTY_STRING {
+ ifv[idx] = "-"
+ } else {
+ ifv[idx] = fv
+ }
+}
// snmpConnection is an interface which wraps a *gosnmp.GoSNMP object.
// We interact through an interface so we can mock it out in tests.
@@ -575,9 +741,7 @@ func (s *Snmp) getConnection(idx int) (snmpConnection, error) {
if gs := s.connectionCache[idx]; gs != nil {
return gs, nil
}
-
agent := s.Agents[idx]
-
var err error
var gs snmp.GosnmpWrapper
gs, err = snmp.NewWrapper(s.ClientConfig)
diff --git a/plugins/inputs/snmp_heartbeat/README.md b/plugins/inputs/snmp_heartbeat/README.md
new file mode 100644
index 0000000000000..bab960aad05b8
--- /dev/null
+++ b/plugins/inputs/snmp_heartbeat/README.md
@@ -0,0 +1,240 @@
+# SNMP_HEARTBEAT Input Plugin
+
+The `snmp_healthbeat` input plugin uses polling to gather metrics to check the state
+of the snmp agent.
+Support for gathering individual OIDs as well as complete SNMP tables is
+included.
+
+### Prerequisites
+
+This plugin uses the `snmptable` and `snmptranslate` programs from the
+[net-snmp][] project. These tools will need to be installed into the `PATH` in
+order to be located. Other utilities from the net-snmp project may be useful
+for troubleshooting, but are not directly used by the plugin.
+
+These programs will load available MIBs on the system. Typically the default
+directory for MIBs is `/usr/share/snmp/mibs`, but if your MIBs are in a
+different location you may need to make the paths known to net-snmp. The
+location of these files can be configured in the `snmp.conf` or via the
+`MIBDIRS` environment variable. See [`man 1 snmpcmd`][man snmpcmd] for more
+information.
+
+### Configuration
+```toml
+[[inputs.snmp_heartbeat]]
+ ## Agent addresses to retrieve values from.
+ ## format: agents = [":"]
+ ## scheme: optional, either udp, udp4, udp6, tcp, tcp4, tcp6.
+ ## default is udp
+ ## port: optional
+ ## example: agents = ["udp://127.0.0.1:161"]
+ ## agents = ["tcp://127.0.0.1:161"]
+ ## agents = ["udp4://v4only-snmp-agent"]
+ agents = ["udp://127.0.0.1:161"]
+
+ ## Timeout for each request.
+ # timeout = "5s"
+
+ ## SNMP version; can be 1, 2, or 3.
+ # version = 2
+
+ ## The number of request made before gathering details.
+ packets = 3
+
+ ## Whether this should operate on probe mode. In probe mode state
+ ## field will be present in output based on whether response is
+ ## recieved or not.
+ probe = true
+
+ ## SNMP community string.
+ # community = "public"
+
+ ## Agent host tag
+ # agent_host_tag = "agent_host"
+
+ ## Number of retries to attempt.
+ # retries = 0
+
+ ## The GETBULK max-repetitions parameter.
+ # max_repetitions = 10
+
+ ## SNMPv3 authentication and encryption options.
+ ##
+ ## Security Name.
+ # sec_name = "myuser"
+ ## Authentication protocol; one of "MD5", "SHA", "SHA224", "SHA256", "SHA384", "SHA512" or "".
+ # auth_protocol = "MD5"
+ ## Authentication password.
+ # auth_password = "pass"
+ ## Security Level; one of "noAuthNoPriv", "authNoPriv", or "authPriv".
+ # sec_level = "authNoPriv"
+ ## Context Name.
+ # context_name = ""
+ ## Privacy protocol used for encrypted messages; one of "DES", "AES", "AES192", "AES192C", "AES256", "AES256C", or "".
+ ### Protocols "AES192", "AES192", "AES256", and "AES256C" require the underlying net-snmp tools
+ ### to be compiled with --enable-blumenthal-aes (http://www.net-snmp.org/docs/INSTALL.html)
+ # priv_protocol = ""
+ ## Privacy password used for encrypted messages.
+ # priv_password = ""
+
+ ## Add fields and tables defining the variables you wish to collect. This
+ ## example collects the system uptime and interface variables. Reference the
+ ## full plugin documentation for configuration details.
+ [[inputs.snmp_heartbeat.field]]
+ oid = "RFC1213-MIB::sysName.0"
+ name = "sysName"
+ # Plugin tags.
+ [inputs.snmp_heartbeat.tags]
+ type="snmp-heartbeat"
+ # Additional fields
+ field1="20"
+```
+
+#### Configure SNMP Requests
+
+This plugin provides two methods for configuring the SNMP requests: `fields`
+and `tables`. Use the `field` option to gather single ad-hoc variables.
+To collect SNMP tables, use the `table` option.
+
+##### Field
+
+Use a `field` to collect a variable by OID. Requests specified with this
+option operate similar to the `snmpget` utility.
+
+```toml
+[[inputs.snmp_heartbeat]]
+ # ... snip ...
+
+ [[inputs.snmp_heartbeat.field]]
+ ## Object identifier of the variable as a numeric or textual OID.
+ oid = "RFC1213-MIB::sysName.0"
+
+ ## Name of the field or tag to create. If not specified, it defaults to
+ ## the value of 'oid'. If 'oid' is numeric, an attempt to translate the
+ ## numeric OID into a textual OID will be made.
+ # name = ""
+
+ ## If true the variable will be added as a tag, otherwise a field will be
+ ## created.
+ # is_tag = false
+
+ ## Apply one of the following conversions to the variable value:
+ ## float(X): Convert the input value into a float and divides by the
+ ## Xth power of 10. Effectively just moves the decimal left
+ ## X places. For example a value of `123` with `float(2)`
+ ## will result in `1.23`.
+ ## float: Convert the value into a float with no adjustment. Same
+ ## as `float(0)`.
+ ## int: Convert the value into an integer.
+ ## hwaddr: Convert the value to a MAC address.
+ ## ipaddr: Convert the value to an IP address.
+ ## hextoint:X:Y Convert a hex string value to integer. Where X is the Endian
+ ## and Y the bit size. For example: hextoint:LittleEndian:uint64
+ ## or hextoint:BigEndian:uint32. Valid options for the Endian are:
+ ## BigEndian and LittleEndian. For the bit size: uint16, uint32
+ ## and uint64.
+ ##
+ # conversion = ""
+```
+
+##### Table
+
+Use a `table` to configure the collection of a SNMP table. SNMP requests
+formed with this option operate similarly way to the `snmptable` command.
+
+Control the handling of specific table columns using a nested `field`. These
+nested fields are specified similarly to a top-level `field`.
+
+By default all columns of the SNMP table will be collected - it is not required
+to add a nested field for each column, only those which you wish to modify. To
+*only* collect certain columns, omit the `oid` from the `table` section and only
+include `oid` settings in `field` sections. For more complex include/exclude
+cases for columns use [metric filtering][].
+
+One [metric][] is created for each row of the SNMP table.
+
+```toml
+[[inputs.snmp_heartbeat]]
+ # ... snip ...
+
+ [[inputs.snmp_heartbeat.table]]
+ ## Object identifier of the SNMP table as a numeric or textual OID.
+ oid = "IF-MIB::ifTable"
+
+ ## Name of the field or tag to create. If not specified, it defaults to
+ ## the value of 'oid'. If 'oid' is numeric an attempt to translate the
+ ## numeric OID into a textual OID will be made.
+ # name = ""
+
+ ## Which tags to inherit from the top-level config and to use in the output
+ ## of this table's measurement.
+ ## example: inherit_tags = ["source"]
+ # inherit_tags = []
+
+ ## Add an 'index' tag with the table row number. Use this if the table has
+ ## no indexes or if you are excluding them. This option is normally not
+ ## required as any index columns are automatically added as tags.
+ # index_as_tag = false
+
+ [[inputs.snmp_heartbeat.table.field]]
+ ## OID to get. May be a numeric or textual module-qualified OID.
+ oid = "IF-MIB::ifDescr"
+
+ ## Name of the field or tag to create. If not specified, it defaults to
+ ## the value of 'oid'. If 'oid' is numeric an attempt to translate the
+ ## numeric OID into a textual OID will be made.
+ # name = ""
+
+ ## Output this field as a tag.
+ # is_tag = false
+
+ ## The OID sub-identifier to strip off so that the index can be matched
+ ## against other fields in the table.
+ # oid_index_suffix = ""
+
+ ## Specifies the length of the index after the supplied table OID (in OID
+ ## path segments). Truncates the index after this point to remove non-fixed
+ ## value or length index suffixes.
+ # oid_index_length = 0
+
+ ## Specifies if the value of given field should be snmptranslated
+ ## by default no field values are translated
+ # translate = true
+```
+
+### Troubleshooting
+
+Check that a numeric field can be translated to a textual field:
+```
+$ snmptranslate .1.3.6.1.2.1.1.3.0
+DISMAN-EVENT-MIB::sysUpTimeInstance
+```
+
+Request a top-level field:
+```
+$ snmpget -v2c -c public 127.0.0.1 sysUpTime.0
+```
+
+Request a table:
+```
+$ snmptable -v2c -c public 127.0.0.1 ifTable
+```
+
+To collect a packet capture, run this command in the background while running
+Telegraf or one of the above commands. Adjust the interface, host and port as
+needed:
+```
+$ sudo tcpdump -s 0 -i eth0 -w telegraf-snmp.pcap host 127.0.0.1 and port 161
+```
+
+### Example Output
+
+```
+{"fields":{"average_rtt":37,"jitter":54,"maximum_rtt":110,"minimum_rtt":1,"period":0,"pkt_loss_pct":0,"sysName":"vostro","target_state":"Up"},"name":"snmp_heartbeat","tags":{"agent_host":"127.0.0.1","field1":"20","host":"vostro","type":"snmp-heartbeat"},"timestamp":163153132200}
+
+```
+
+[net-snmp]: http://www.net-snmp.org/
+[man snmpcmd]: http://net-snmp.sourceforge.net/docs/man/snmpcmd.html#lbAK
+[metric filtering]: /docs/CONFIGURATION.md#metric-filtering
+[metric]: /docs/METRICS.md
\ No newline at end of file
diff --git a/plugins/inputs/snmp_heartbeat/snmp_heartbeat.go b/plugins/inputs/snmp_heartbeat/snmp_heartbeat.go
new file mode 100644
index 0000000000000..b8cabe5815c3c
--- /dev/null
+++ b/plugins/inputs/snmp_heartbeat/snmp_heartbeat.go
@@ -0,0 +1,1072 @@
+package snmp_heartbeat
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "log"
+ "math"
+ "net"
+ "os/exec"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gosnmp/gosnmp"
+ "github.com/influxdata/telegraf"
+ "github.com/influxdata/telegraf/config"
+ "github.com/influxdata/telegraf/internal/snmp"
+ "github.com/influxdata/telegraf/plugins/inputs"
+ "github.com/influxdata/wlog"
+)
+
+const description = `Retrieves SNMP values from remote agents`
+const sampleConfig = `
+ ## Agent addresses to retrieve values from.
+ ## format: agents = [":"]
+ ## scheme: optional, either udp, udp4, udp6, tcp, tcp4, tcp6.
+ ## default is udp
+ ## port: optional
+ ## example: agents = ["udp://127.0.0.1:161"]
+ ## agents = ["tcp://127.0.0.1:161"]
+ ## agents = ["udp4://v4only-snmp-agent"]
+ agents = ["udp://127.0.0.1:161"]
+
+ ## Timeout for each request.
+ # timeout = "5s"
+
+ ## SNMP version; can be 1, 2, or 3.
+ # version = 2
+
+ ## Agent host tag; the tag used to reference the source host
+ # agent_host_tag = "agent_host"
+
+ # probe = false
+ # Period = "30s"
+
+ ## SNMP community string.
+ # community = "public"
+
+ ## Number of retries to attempt.
+ # retries = 3
+
+ ## The GETBULK max-repetitions parameter.
+ # max_repetitions = 10
+
+ ## SNMPv3 authentication and encryption options.
+ ##
+ ## Security Name.
+ # sec_name = "myuser"
+ ## Authentication protocol; one of "MD5", "SHA", "SHA224", "SHA256", "SHA384", "SHA512" or "".
+ # auth_protocol = "MD5"
+ ## Authentication password.
+ # auth_password = "pass"
+ ## Security Level; one of "noAuthNoPriv", "authNoPriv", or "authPriv".
+ # sec_level = "authNoPriv"
+ ## Context Name.
+ # context_name = ""
+ ## Privacy protocol used for encrypted messages; one of "DES", "AES" or "".
+ # priv_protocol = ""
+ ## Privacy password used for encrypted messages.
+ # priv_password = ""
+
+ ## Add fields and tables defining the variables you wish to collect. This
+ ## example collects the system uptime and interface variables. Reference the
+ ## full plugin documentation for configuration details.
+`
+
+// execCommand is so tests can mock out exec.Command usage.
+var execCommand = exec.Command
+
+// execCmd executes the specified command, returning the STDOUT content.
+// If command exits with error status, the output is captured into the returned error.
+func execCmd(arg0 string, args ...string) ([]byte, error) {
+ if wlog.LogLevel() == wlog.DEBUG {
+ quoted := make([]string, 0, len(args))
+ for _, arg := range args {
+ quoted = append(quoted, fmt.Sprintf("%q", arg))
+ }
+ log.Printf("D! [inputs.snmp] executing %q %s", arg0, strings.Join(quoted, " "))
+ }
+
+ out, err := execCommand(arg0, args...).Output()
+ if err != nil {
+ if err, ok := err.(*exec.ExitError); ok {
+ return nil, fmt.Errorf("%s: %w", bytes.TrimRight(err.Stderr, "\r\n"), err)
+ }
+ return nil, err
+ }
+ return out, nil
+}
+
+// Snmp holds the configuration for the plugin.
+type Snmp struct {
+ // The SNMP agent to query. Format is [SCHEME://]ADDR[:PORT] (e.g.
+ // udp://1.2.3.4:161). If the scheme is not specified then "udp" is used.
+ Agents []string `toml:"agents"`
+
+ // The tag used to name the agent host
+ AgentHostTag string `toml:"agent_host_tag"`
+
+ snmp.ClientConfig
+
+ Tables []Table `toml:"table"`
+
+ Probe bool `toml:"probe"`
+ Period string `toml:"interval"`
+ Packets int64 `toml:"packets"`
+ Alias string `toml:"alias"`
+ // Name & Fields are the elements of a Table.
+ // Telegraf chokes if we try to embed a Table. So instead we have to embed the
+ // fields of a Table, and construct a Table during runtime.
+ Name string // deprecated in 1.14; use name_override
+ Fields []Field `toml:"field"`
+
+ connectionCache []snmpConnection
+ initialized bool
+}
+
+func (s *Snmp) init() error {
+ if s.initialized {
+ return nil
+ }
+
+ s.connectionCache = make([]snmpConnection, len(s.Agents))
+
+ for i := range s.Tables {
+ if err := s.Tables[i].Init(); err != nil {
+ return fmt.Errorf("initializing table %s: %w", s.Tables[i].Name, err)
+ }
+ }
+
+ for i := range s.Fields {
+ if err := s.Fields[i].init(); err != nil {
+ return fmt.Errorf("initializing field %s: %w", s.Fields[i].Name, err)
+ }
+ }
+
+ if len(s.AgentHostTag) == 0 {
+ s.AgentHostTag = "agent_host"
+ }
+
+ s.initialized = true
+ return nil
+}
+
+// Table holds the configuration for a SNMP table.
+type Table struct {
+ // Name will be the name of the measurement.
+ Name string
+
+ // Which tags to inherit from the top-level config.
+ InheritTags []string
+
+ // Adds each row's table index as a tag.
+ IndexAsTag bool
+
+ // Fields is the tags and values to look up.
+ Fields []Field `toml:"field"`
+
+ // OID for automatic field population.
+ // If provided, init() will populate Fields with all the table columns of the
+ // given OID.
+ Oid string
+
+ initialized bool
+}
+
+// Init() builds & initializes the nested fields.
+func (t *Table) Init() error {
+ //makes sure oid or name is set in config file
+ //otherwise snmp will produce metrics with an empty name
+ if t.Oid == "" && t.Name == "" {
+ return fmt.Errorf("SNMP table in config file is not named. One or both of the oid and name settings must be set")
+ }
+
+ if t.initialized {
+ return nil
+ }
+
+ if err := t.initBuild(); err != nil {
+ return err
+ }
+
+ // initialize all the nested fields
+ for i := range t.Fields {
+ if err := t.Fields[i].init(); err != nil {
+ return fmt.Errorf("initializing field %s: %w", t.Fields[i].Name, err)
+ }
+ }
+
+ t.initialized = true
+ return nil
+}
+
+// initBuild initializes the table if it has an OID configured. If so, the
+// net-snmp tools will be used to look up the OID and auto-populate the table's
+// fields.
+func (t *Table) initBuild() error {
+ if t.Oid == "" {
+ return nil
+ }
+
+ _, _, oidText, fields, err := snmpTable(t.Oid)
+ if err != nil {
+ return err
+ }
+
+ if t.Name == "" {
+ t.Name = oidText
+ }
+
+ knownOIDs := map[string]bool{}
+ for _, f := range t.Fields {
+ knownOIDs[f.Oid] = true
+ }
+ for _, f := range fields {
+ if !knownOIDs[f.Oid] {
+ t.Fields = append(t.Fields, f)
+ }
+ }
+
+ return nil
+}
+
+// Field holds the configuration for a Field to look up.
+type Field struct {
+ // Name will be the name of the field.
+ Name string
+ // OID is prefix for this field. The plugin will perform a walk through all
+ // OIDs with this as their parent. For each value found, the plugin will strip
+ // off the OID prefix, and use the remainder as the index. For multiple fields
+ // to show up in the same row, they must share the same index.
+ Oid string
+ // OidIndexSuffix is the trailing sub-identifier on a table record OID that will be stripped off to get the record's index.
+ OidIndexSuffix string
+ // OidIndexLength specifies the length of the index in OID path segments. It can be used to remove sub-identifiers that vary in content or length.
+ OidIndexLength int
+ // IsTag controls whether this OID is output as a tag or a value.
+ IsTag bool
+ // Conversion controls any type conversion that is done on the value.
+ // "float"/"float(0)" will convert the value into a float.
+ // "float(X)" will convert the value into a float, and then move the decimal before Xth right-most digit.
+ // "int" will conver the value into an integer.
+ // "hwaddr" will convert a 6-byte string to a MAC address.
+ // "ipaddr" will convert the value to an IPv4 or IPv6 address.
+ Conversion string
+ // Translate tells if the value of the field should be snmptranslated
+ Translate bool
+
+ 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
+ }
+
+ // check if oid needs translation or name is not set
+ if strings.ContainsAny(f.Oid, ":abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") || f.Name == "" {
+ _, oidNum, oidText, conversion, err := SnmpTranslate(f.Oid)
+ if err != nil {
+ return fmt.Errorf("translating: %w", err)
+ }
+ f.Oid = oidNum
+ if f.Name == "" {
+ f.Name = oidText
+ }
+ if f.Conversion == "" {
+ f.Conversion = conversion
+ }
+ //TODO use textual convention conversion from the MIB
+ }
+
+ f.initialized = true
+ return nil
+}
+
+// RTable is the resulting table built from a Table.
+type RTable struct {
+ // Name is the name of the field, copied from Table.Name.
+ Name string
+ // Time is the time the table was built.
+ Time time.Time
+ // Rows are the rows that were found, one row for each table OID index found.
+ Rows []RTableRow
+}
+
+// RTableRow is the resulting row containing all the OID values which shared
+// the same index.
+type RTableRow struct {
+ // Tags are all the Field values which had IsTag=true.
+ Tags map[string]string
+ // Fields are all the Field values which had IsTag=false.
+ Fields map[string]interface{}
+}
+
+type walkError struct {
+ msg string
+ err error
+}
+
+func (e *walkError) Error() string {
+ return e.msg
+}
+
+func (e *walkError) Unwrap() error {
+ return e.err
+}
+
+func init() {
+ inputs.Add("snmp_heartbeat", func() telegraf.Input {
+ return &Snmp{
+ Name: "snmp_heartbeat",
+ ClientConfig: snmp.ClientConfig{
+ Retries: 3,
+ MaxRepetitions: 10,
+ Timeout: config.Duration(5 * time.Second),
+ Version: 2,
+ Community: "public",
+ },
+ Packets: 3,
+ Alias: "",
+ }
+ })
+}
+
+// SampleConfig returns the default configuration of the input.
+func (s *Snmp) SampleConfig() string {
+ return sampleConfig
+}
+
+// Description returns a one-sentence description on the input.
+func (s *Snmp) Description() string {
+ return description
+}
+
+// Gather retrieves all the configured fields and tables.
+// Any error encountered does not halt the process. The errors are accumulated
+// and returned at the end.
+func (s *Snmp) Gather(acc telegraf.Accumulator) error {
+
+ if err := s.init(); err != nil {
+ return err
+ }
+
+ var wg sync.WaitGroup
+ for i, agent := range s.Agents {
+ wg.Add(1)
+ go func(i int, agent string) {
+ defer wg.Done()
+ gs, err := s.getConnection(i)
+ if err != nil {
+ acc.AddError(fmt.Errorf("agent %s: %w", agent, err))
+ return
+ }
+
+ // First is the top-level fields. We treat the fields as table prefixes with an empty index.
+ t := Table{
+ Name: s.Name,
+ Fields: s.Fields,
+ }
+ topTags := map[string]string{}
+ if err := s.gatherTable(acc, gs, t, topTags, false); err != nil {
+ if s.Probe {
+ rt := RTable{
+ Name: t.Name,
+ Time: time.Now(), //TODO record time at start
+ Rows: make([]RTableRow, 0, 1),
+ }
+ rtr := RTableRow{}
+ rtr.Tags = map[string]string{}
+ rtr.Fields = map[string]interface{}{}
+ if _, ok := rtr.Tags[s.AgentHostTag]; !ok {
+ rtr.Tags[s.AgentHostTag] = gs.Host()
+ }
+ rtr.Fields["target_state"] = "Down"
+ if strings.Contains(s.Period, "m") {
+ period_str := strings.Trim(s.Period, "m")
+ period, err := strconv.Atoi(period_str)
+ if err == nil {
+ rtr.Fields["period"] = period * 60
+ }
+ } else {
+ period_str := strings.Trim(s.Period, "s")
+ rtr.Fields["period"], err = strconv.Atoi(period_str)
+ }
+ rtr.Fields["average_rtt"] = 0
+ rtr.Fields["minimum_rtt"] = 0
+ rtr.Fields["maximum_rtt"] = 0
+ rtr.Fields["jitter"] = 0
+ rtr.Fields["pkt_loss_pct"] = 100
+
+ rt.Rows = append(rt.Rows, rtr)
+
+ acc.AddFields(rt.Name, rtr.Fields, rtr.Tags, rt.Time)
+ } else {
+ acc.AddError(fmt.Errorf("agent %s: %w", agent, err))
+ }
+ }
+
+ // Now is the real tables.
+ for _, t := range s.Tables {
+ if err := s.gatherTable(acc, gs, t, topTags, true); err != nil {
+ acc.AddError(fmt.Errorf("agent %s: gathering table %s: %w", agent, t.Name, err))
+ }
+ }
+ }(i, agent)
+ }
+ wg.Wait()
+
+ return nil
+}
+
+func (s *Snmp) gatherTable(acc telegraf.Accumulator, gs snmpConnection, t Table, topTags map[string]string, walk bool) error {
+ var rt *RTable
+ var err error
+ var avg_rtt = int64(0)
+ var rtt = int64(0)
+ var max_rtt = int64(0)
+ var min_rtt = int64(0)
+ var jitter = int64(0)
+ var jitter_value = int64(0)
+ var pkt_loss_pct = float64(0)
+ var round_time []int64
+
+ if s.Probe {
+ if s.Packets < 3 {
+ //Min required packets is 3.
+ s.Packets = 3
+ }
+ for snmp_call := int64(1); snmp_call <= s.Packets; snmp_call++ {
+ var out_rt *RTable
+ start := time.Now()
+ out_rt, err = t.Build(gs, walk)
+ time_taken := time.Since(start).Milliseconds()
+ if time_taken == 0 {
+ time_taken = 1
+ }
+ if err != nil {
+ log.Printf("Unable to fetch info for %s %v", gs.Host(), err)
+ } else {
+ rt = out_rt
+ round_time = append(round_time, time_taken)
+ }
+ }
+ // Return if none of the packets go through.
+ if len(round_time) == 0 {
+ return err
+ }
+ max_rtt = round_time[0]
+ min_rtt = round_time[0]
+ for index, ele := range round_time {
+ if max_rtt < ele {
+ max_rtt = ele
+ }
+ if min_rtt > ele {
+ min_rtt = ele
+ }
+ rtt = rtt + ele
+ // Jitter is calculated by adding difference of consecutive rtt.
+ if index+1 < len(round_time) {
+ jitter_value = jitter_value + (ele - round_time[index+1])
+ }
+ avg_rtt = rtt / s.Packets
+ jitter = jitter_value / (s.Packets - 1)
+ if jitter < 0 {
+ jitter = -jitter
+ }
+ }
+ pkt_loss_pct = ((float64(s.Packets) - float64(len(round_time))) / float64(s.Packets)) * 100
+ } else {
+ 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 s.Probe {
+ tr.Fields["target_state"] = "Up"
+ if strings.Contains(s.Period, "m") {
+ period, err := strconv.Atoi(strings.Trim(s.Period, "m"))
+ if err == nil {
+ tr.Fields["period"] = period * 60
+ }
+ } else {
+ tr.Fields["period"], err = strconv.Atoi(strings.Trim(s.Period, "s"))
+ }
+
+ tr.Fields["average_rtt"] = avg_rtt
+ tr.Fields["minimum_rtt"] = min_rtt
+ tr.Fields["maximum_rtt"] = max_rtt
+ tr.Fields["jitter"] = jitter
+ tr.Fields["pkt_loss_pct"] = pkt_loss_pct
+ }
+ if _, ok := tr.Tags[s.AgentHostTag]; !ok {
+ tr.Tags[s.AgentHostTag] = gs.Host()
+ }
+ acc.AddFields(rt.Name, tr.Fields, tr.Tags, rt.Time)
+ }
+
+ return nil
+}
+
+// Build retrieves all the fields specified in the table and constructs the RTable.
+func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) {
+ rows := map[string]RTableRow{}
+ tagCount := 0
+ for _, f := range t.Fields {
+ if f.IsTag {
+ tagCount++
+ }
+
+ if len(f.Oid) == 0 {
+ return nil, fmt.Errorf("cannot have empty OID on field %s", f.Name)
+ }
+ var oid string
+ if f.Oid[0] == '.' {
+ oid = f.Oid
+ } else {
+ // make sure OID has "." because the BulkWalkAll results do, and the prefix needs to match
+ oid = "." + f.Oid
+ }
+
+ // ifv contains a mapping of table OID index to field value
+ ifv := map[string]interface{}{}
+
+ if !walk {
+
+ // This is used when fetching non-table fields. Fields configured a the top
+ // scope of the plugin.
+ // We fetch the fields directly, and add them to ifv as if the index were an
+ // empty string. This results in all the non-table fields sharing the same
+ // index, and being added on the same row.
+ if pkt, err := gs.Get([]string{oid}); err != nil {
+ if errors.Is(err, gosnmp.ErrUnknownSecurityLevel) {
+ return nil, fmt.Errorf("unknown security level (sec_level)")
+ } else if errors.Is(err, gosnmp.ErrUnknownUsername) {
+ return nil, fmt.Errorf("unknown username (sec_name)")
+ } else if errors.Is(err, gosnmp.ErrWrongDigest) {
+ return nil, fmt.Errorf("wrong digest (auth_protocol, auth_password)")
+ } else if errors.Is(err, gosnmp.ErrDecryption) {
+ return nil, fmt.Errorf("decryption error (priv_protocol, priv_password)")
+ } else {
+ return nil, fmt.Errorf("performing get on field %s: %w", f.Name, err)
+ }
+ } else if pkt != nil && len(pkt.Variables) > 0 && pkt.Variables[0].Type != gosnmp.NoSuchObject && pkt.Variables[0].Type != gosnmp.NoSuchInstance {
+ ent := pkt.Variables[0]
+ fv, err := fieldConvert(f.Conversion, ent.Value)
+ if err != nil {
+ return nil, fmt.Errorf("converting %q (OID %s) for field %s: %w", ent.Value, ent.Name, f.Name, err)
+ }
+ ifv[""] = fv
+ }
+ } else {
+ err := gs.Walk(oid, func(ent gosnmp.SnmpPDU) error {
+ if len(ent.Name) <= len(oid) || ent.Name[:len(oid)+1] != oid+"." {
+ return &walkError{} // break the walk
+ }
+
+ idx := ent.Name[len(oid):]
+ if f.OidIndexSuffix != "" {
+ if !strings.HasSuffix(idx, f.OidIndexSuffix) {
+ // this entry doesn't match our OidIndexSuffix. skip it
+ return nil
+ }
+ idx = idx[:len(idx)-len(f.OidIndexSuffix)]
+ }
+ if f.OidIndexLength != 0 {
+ i := f.OidIndexLength + 1 // leading separator
+ idx = strings.Map(func(r rune) rune {
+ if r == '.' {
+ i--
+ }
+ if i < 1 {
+ return -1
+ }
+ return r
+ }, idx)
+ }
+
+ // snmptranslate table field value here
+ if f.Translate {
+ if entOid, ok := ent.Value.(string); ok {
+ _, _, oidText, _, err := SnmpTranslate(entOid)
+ if err == nil {
+ // If no error translating, the original value for ent.Value should be replaced
+ ent.Value = oidText
+ }
+ }
+ }
+
+ fv, err := fieldConvert(f.Conversion, ent.Value)
+ if err != nil {
+ return &walkError{
+ msg: fmt.Sprintf("converting %q (OID %s) for field %s", ent.Value, ent.Name, f.Name),
+ err: err,
+ }
+ }
+ ifv[idx] = fv
+ return nil
+ })
+ if err != nil {
+ // Our callback always wraps errors in a walkError.
+ // If this error isn't a walkError, we know it's not
+ // from the callback
+ if _, ok := err.(*walkError); !ok {
+ return nil, fmt.Errorf("performing bulk walk for field %s: %w", f.Name, err)
+ }
+ }
+ }
+
+ for idx, v := range ifv {
+ rtr, ok := rows[idx]
+ if !ok {
+ rtr = RTableRow{}
+ rtr.Tags = map[string]string{}
+ rtr.Fields = map[string]interface{}{}
+ rows[idx] = rtr
+ }
+ if t.IndexAsTag && idx != "" {
+ if idx[0] == '.' {
+ idx = idx[1:]
+ }
+ rtr.Tags["index"] = idx
+ }
+ // don't add an empty string
+ if vs, ok := v.(string); !ok || vs != "" {
+ if f.IsTag {
+ if ok {
+ rtr.Tags[f.Name] = vs
+ } else {
+ rtr.Tags[f.Name] = fmt.Sprintf("%v", v)
+ }
+ } else {
+ rtr.Fields[f.Name] = v
+ }
+ }
+ }
+ }
+
+ rt := RTable{
+ Name: t.Name,
+ Time: time.Now(), //TODO record time at start
+ Rows: make([]RTableRow, 0, len(rows)),
+ }
+ for _, r := range rows {
+ rt.Rows = append(rt.Rows, r)
+ }
+ return &rt, nil
+}
+
+// snmpConnection is an interface which wraps a *gosnmp.GoSNMP object.
+// We interact through an interface so we can mock it out in tests.
+type snmpConnection interface {
+ Host() string
+ //BulkWalkAll(string) ([]gosnmp.SnmpPDU, error)
+ Walk(string, gosnmp.WalkFunc) error
+ Get(oids []string) (*gosnmp.SnmpPacket, error)
+}
+
+// getConnection creates a snmpConnection (*gosnmp.GoSNMP) object and caches the
+// result using `agentIndex` as the cache key. This is done to allow multiple
+// connections to a single address. It is an error to use a connection in
+// more than one goroutine.
+func (s *Snmp) getConnection(idx int) (snmpConnection, error) {
+ if gs := s.connectionCache[idx]; gs != nil {
+ return gs, nil
+ }
+
+ agent := s.Agents[idx]
+
+ var err error
+ var gs snmp.GosnmpWrapper
+ gs, err = snmp.NewWrapper(s.ClientConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ err = gs.SetAgent(agent)
+ if err != nil {
+ return nil, err
+ }
+
+ s.connectionCache[idx] = gs
+
+ if err := gs.Connect(); err != nil {
+ return nil, fmt.Errorf("setting up connection: %w", err)
+ }
+
+ return gs, nil
+}
+
+// fieldConvert converts from any type according to the conv specification
+func fieldConvert(conv string, v interface{}) (interface{}, error) {
+ if conv == "" {
+ if bs, ok := v.([]byte); ok {
+ return string(bs), nil
+ }
+ return v, nil
+ }
+
+ var d int
+ if _, err := fmt.Sscanf(conv, "float(%d)", &d); err == nil || conv == "float" {
+ switch vt := v.(type) {
+ case float32:
+ v = float64(vt) / math.Pow10(d)
+ case float64:
+ v = float64(vt) / math.Pow10(d)
+ case int:
+ v = float64(vt) / math.Pow10(d)
+ case int8:
+ v = float64(vt) / math.Pow10(d)
+ case int16:
+ v = float64(vt) / math.Pow10(d)
+ case int32:
+ v = float64(vt) / math.Pow10(d)
+ case int64:
+ v = float64(vt) / math.Pow10(d)
+ case uint:
+ v = float64(vt) / math.Pow10(d)
+ case uint8:
+ v = float64(vt) / math.Pow10(d)
+ case uint16:
+ v = float64(vt) / math.Pow10(d)
+ case uint32:
+ v = float64(vt) / math.Pow10(d)
+ case uint64:
+ v = float64(vt) / math.Pow10(d)
+ case []byte:
+ vf, _ := strconv.ParseFloat(string(vt), 64)
+ v = vf / math.Pow10(d)
+ case string:
+ vf, _ := strconv.ParseFloat(vt, 64)
+ v = vf / math.Pow10(d)
+ }
+ return v, nil
+ }
+
+ if conv == "int" {
+ switch vt := v.(type) {
+ case float32:
+ v = int64(vt)
+ case float64:
+ v = int64(vt)
+ case int:
+ v = int64(vt)
+ case int8:
+ v = int64(vt)
+ case int16:
+ v = int64(vt)
+ case int32:
+ v = int64(vt)
+ case int64:
+ v = 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.ParseInt(string(vt), 10, 64)
+ case string:
+ v, _ = strconv.ParseInt(vt, 10, 64)
+ }
+ return v, nil
+ }
+
+ if conv == "hwaddr" {
+ switch vt := v.(type) {
+ case string:
+ v = net.HardwareAddr(vt).String()
+ case []byte:
+ v = net.HardwareAddr(vt).String()
+ default:
+ return nil, fmt.Errorf("invalid type (%T) for hwaddr conversion", v)
+ }
+ return v, nil
+ }
+
+ split := strings.Split(conv, ":")
+ if split[0] == "hextoint" && len(split) == 3 {
+ endian := split[1]
+ bit := split[2]
+
+ bv, ok := v.([]byte)
+ if !ok {
+ return v, nil
+ }
+
+ if endian == "LittleEndian" {
+ switch bit {
+ case "uint64":
+ v = binary.LittleEndian.Uint64(bv)
+ case "uint32":
+ v = binary.LittleEndian.Uint32(bv)
+ case "uint16":
+ v = binary.LittleEndian.Uint16(bv)
+ default:
+ return nil, fmt.Errorf("invalid bit value (%s) for hex to int conversion", bit)
+ }
+ } else if endian == "BigEndian" {
+ switch bit {
+ case "uint64":
+ v = binary.BigEndian.Uint64(bv)
+ case "uint32":
+ v = binary.BigEndian.Uint32(bv)
+ case "uint16":
+ v = binary.BigEndian.Uint16(bv)
+ default:
+ return nil, fmt.Errorf("invalid bit value (%s) for hex to int conversion", bit)
+ }
+ } else {
+ return nil, fmt.Errorf("invalid Endian value (%s) for hex to int conversion", endian)
+ }
+
+ return v, nil
+ }
+
+ if conv == "ipaddr" {
+ var ipbs []byte
+
+ switch vt := v.(type) {
+ case string:
+ ipbs = []byte(vt)
+ case []byte:
+ ipbs = vt
+ default:
+ return nil, fmt.Errorf("invalid type (%T) for ipaddr conversion", v)
+ }
+
+ switch len(ipbs) {
+ case 4, 16:
+ v = net.IP(ipbs).String()
+ default:
+ return nil, fmt.Errorf("invalid length (%d) for ipaddr conversion", len(ipbs))
+ }
+
+ return v, nil
+ }
+
+ return nil, fmt.Errorf("invalid conversion type '%s'", conv)
+}
+
+type snmpTableCache struct {
+ mibName string
+ oidNum string
+ oidText string
+ fields []Field
+ err error
+}
+
+var snmpTableCaches map[string]snmpTableCache
+var snmpTableCachesLock sync.Mutex
+
+// snmpTable resolves the given OID as a table, providing information about the
+// table and fields within.
+func snmpTable(oid string) (mibName string, oidNum string, oidText string, fields []Field, err error) {
+ snmpTableCachesLock.Lock()
+ if snmpTableCaches == nil {
+ snmpTableCaches = map[string]snmpTableCache{}
+ }
+
+ var stc snmpTableCache
+ var ok bool
+ if stc, ok = snmpTableCaches[oid]; !ok {
+ stc.mibName, stc.oidNum, stc.oidText, stc.fields, stc.err = snmpTableCall(oid)
+ snmpTableCaches[oid] = stc
+ }
+
+ snmpTableCachesLock.Unlock()
+ return stc.mibName, stc.oidNum, stc.oidText, stc.fields, stc.err
+}
+
+func snmpTableCall(oid string) (mibName string, oidNum string, oidText string, fields []Field, err error) {
+ mibName, oidNum, oidText, _, err = SnmpTranslate(oid)
+ if err != nil {
+ return "", "", "", nil, fmt.Errorf("translating: %w", err)
+ }
+
+ mibPrefix := mibName + "::"
+ oidFullName := mibPrefix + oidText
+
+ // first attempt to get the table's tags
+ tagOids := map[string]struct{}{}
+ // We have to guess that the "entry" oid is `oid+".1"`. snmptable and snmptranslate don't seem to have a way to provide the info.
+ if out, err := execCmd("snmptranslate", "-Td", oidFullName+".1"); err == nil {
+ scanner := bufio.NewScanner(bytes.NewBuffer(out))
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ if !strings.HasPrefix(line, " INDEX") {
+ continue
+ }
+
+ i := strings.Index(line, "{ ")
+ if i == -1 { // parse error
+ continue
+ }
+ line = line[i+2:]
+ i = strings.Index(line, " }")
+ if i == -1 { // parse error
+ continue
+ }
+ line = line[:i]
+ for _, col := range strings.Split(line, ", ") {
+ tagOids[mibPrefix+col] = struct{}{}
+ }
+ }
+ }
+
+ // this won't actually try to run a query. The `-Ch` will just cause it to dump headers.
+ out, err := execCmd("snmptable", "-Ch", "-Cl", "-c", "public", "127.0.0.1", oidFullName)
+ if err != nil {
+ return "", "", "", nil, fmt.Errorf("getting table columns: %w", err)
+ }
+ scanner := bufio.NewScanner(bytes.NewBuffer(out))
+ scanner.Scan()
+ cols := scanner.Text()
+ if len(cols) == 0 {
+ return "", "", "", nil, fmt.Errorf("could not find any columns in table")
+ }
+ for _, col := range strings.Split(cols, " ") {
+ if len(col) == 0 {
+ continue
+ }
+ _, isTag := tagOids[mibPrefix+col]
+ fields = append(fields, Field{Name: col, Oid: mibPrefix + col, IsTag: isTag})
+ }
+
+ return mibName, oidNum, oidText, fields, err
+}
+
+type snmpTranslateCache struct {
+ mibName string
+ oidNum string
+ oidText string
+ conversion string
+ err error
+}
+
+var snmpTranslateCachesLock sync.Mutex
+var snmpTranslateCaches map[string]snmpTranslateCache
+
+// snmpTranslate resolves the given OID.
+func SnmpTranslate(oid string) (mibName string, oidNum string, oidText string, conversion string, err error) {
+ snmpTranslateCachesLock.Lock()
+ if snmpTranslateCaches == nil {
+ snmpTranslateCaches = map[string]snmpTranslateCache{}
+ }
+
+ var stc snmpTranslateCache
+ var ok bool
+ if stc, ok = snmpTranslateCaches[oid]; !ok {
+ // This will result in only one call to snmptranslate running at a time.
+ // We could speed it up by putting a lock in snmpTranslateCache and then
+ // returning it immediately, and multiple callers would then release the
+ // snmpTranslateCachesLock and instead wait on the individual
+ // snmpTranslation.Lock to release. But I don't know that the extra complexity
+ // is worth it. Especially when it would slam the system pretty hard if lots
+ // of lookups are being performed.
+
+ stc.mibName, stc.oidNum, stc.oidText, stc.conversion, stc.err = snmpTranslateCall(oid)
+ snmpTranslateCaches[oid] = stc
+ }
+
+ snmpTranslateCachesLock.Unlock()
+
+ return stc.mibName, stc.oidNum, stc.oidText, stc.conversion, stc.err
+}
+
+func snmpTranslateCall(oid string) (mibName string, oidNum string, oidText string, conversion string, err error) {
+ var out []byte
+ if strings.ContainsAny(oid, ":abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") {
+ out, err = execCmd("snmptranslate", "-Td", "-Ob", oid)
+ } else {
+ out, err = execCmd("snmptranslate", "-Td", "-Ob", "-m", "all", oid)
+ if err, ok := err.(*exec.Error); ok && err.Err == exec.ErrNotFound {
+ // Silently discard error if snmptranslate not found and we have a numeric OID.
+ // Meaning we can get by without the lookup.
+ return "", oid, oid, "", nil
+ }
+ }
+ if err != nil {
+ return "", "", "", "", err
+ }
+
+ scanner := bufio.NewScanner(bytes.NewBuffer(out))
+ ok := scanner.Scan()
+ if !ok && scanner.Err() != nil {
+ return "", "", "", "", fmt.Errorf("getting OID text: %w", scanner.Err())
+ }
+
+ oidText = scanner.Text()
+
+ i := strings.Index(oidText, "::")
+ if i == -1 {
+ // was not found in MIB.
+ if bytes.Contains(out, []byte("[TRUNCATED]")) {
+ return "", oid, oid, "", nil
+ }
+ // not truncated, but not fully found. We still need to parse out numeric OID, so keep going
+ oidText = oid
+ } else {
+ mibName = oidText[:i]
+ oidText = oidText[i+2:]
+ }
+
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ if strings.HasPrefix(line, " -- TEXTUAL CONVENTION ") {
+ tc := strings.TrimPrefix(line, " -- TEXTUAL CONVENTION ")
+ switch tc {
+ case "MacAddress", "PhysAddress":
+ conversion = "hwaddr"
+ case "InetAddressIPv4", "InetAddressIPv6", "InetAddress", "IPSIpAddress":
+ conversion = "ipaddr"
+ }
+ } else if strings.HasPrefix(line, "::= { ") {
+ objs := strings.TrimPrefix(line, "::= { ")
+ objs = strings.TrimSuffix(objs, " }")
+
+ for _, obj := range strings.Split(objs, " ") {
+ if len(obj) == 0 {
+ continue
+ }
+ if i := strings.Index(obj, "("); i != -1 {
+ obj = obj[i+1:]
+ oidNum += "." + obj[:strings.Index(obj, ")")]
+ } else {
+ oidNum += "." + obj
+ }
+ }
+ break
+ }
+ }
+
+ return mibName, oidNum, oidText, conversion, nil
+}
diff --git a/plugins/inputs/snmp_trap/snmp_trap.go b/plugins/inputs/snmp_trap/snmp_trap.go
index 9fffd8968d593..0d5812586df94 100644
--- a/plugins/inputs/snmp_trap/snmp_trap.go
+++ b/plugins/inputs/snmp_trap/snmp_trap.go
@@ -1,35 +1,39 @@
package snmp_trap
import (
+ "bufio"
+ "bytes"
"fmt"
"net"
- "os"
- "path/filepath"
+ "os/exec"
+ "regexp"
"strconv"
"strings"
+ "sync"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
+ "github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/plugins/inputs"
- "github.com/sleepinggenius2/gosmi"
- "github.com/sleepinggenius2/gosmi/types"
"github.com/gosnmp/gosnmp"
)
var defaultTimeout = config.Duration(time.Second * 5)
+type execer func(config.Duration, string, ...string) ([]byte, error)
+
type mibEntry struct {
mibName string
oidText string
+ enumMap map[int]string
}
type SnmpTrap struct {
ServiceAddress string `toml:"service_address"`
Timeout config.Duration `toml:"timeout"`
Version string `toml:"version"`
- Path []string `toml:"path"`
// Settings for version 3
// Values: "noAuthNoPriv", "authNoPriv", "authPriv"
@@ -42,15 +46,19 @@ type SnmpTrap struct {
PrivProtocol string `toml:"priv_protocol"`
PrivPassword string `toml:"priv_password"`
- acc telegraf.Accumulator
- listener *gosnmp.TrapListener
- timeFunc func() time.Time
- lookupFunc func(string) (mibEntry, error)
- errCh chan error
+ acc telegraf.Accumulator
+ listener *gosnmp.TrapListener
+ timeFunc func() time.Time
+ errCh chan error
makeHandlerWrapper func(gosnmp.TrapHandlerFunc) gosnmp.TrapHandlerFunc
Log telegraf.Logger `toml:"-"`
+
+ cacheLock sync.Mutex
+ cache map[string]mibEntry
+
+ execCmd execer
}
var sampleConfig = `
@@ -62,10 +70,6 @@ var sampleConfig = `
## 1024. See README.md for details
##
# service_address = "udp://:162"
- ##
- ## Path to mib files
- # path = ["/usr/share/snmp/mibs"]
- ##
## Timeout running snmptranslate command
# timeout = "5s"
## Snmp version, defaults to 2c
@@ -102,7 +106,6 @@ func init() {
inputs.Add("snmp_trap", func() telegraf.Input {
return &SnmpTrap{
timeFunc: time.Now,
- lookupFunc: lookup,
ServiceAddress: "udp://:162",
Timeout: defaultTimeout,
Version: "2c",
@@ -110,50 +113,20 @@ func init() {
})
}
-func (s *SnmpTrap) Init() error {
- // must init, append path for each directory, load module for every file
- // or gosmi will fail without saying why
- gosmi.Init()
- err := s.getMibsPath()
+func realExecCmd(timeout config.Duration, arg0 string, args ...string) ([]byte, error) {
+ cmd := exec.Command(arg0, args...)
+ var out bytes.Buffer
+ cmd.Stdout = &out
+ err := internal.RunTimeout(cmd, time.Duration(timeout))
if err != nil {
- s.Log.Errorf("Could not get path %v", err)
+ return nil, err
}
- return nil
+ return out.Bytes(), nil
}
-func (s *SnmpTrap) getMibsPath() error {
- var folders []string
- for _, mibPath := range s.Path {
- gosmi.AppendPath(mibPath)
- folders = append(folders, mibPath)
- err := filepath.Walk(mibPath, func(path string, info os.FileInfo, err error) error {
- if info.Mode()&os.ModeSymlink != 0 {
- s, _ := os.Readlink(path)
- folders = append(folders, s)
- }
- return nil
- })
- if err != nil {
- s.Log.Errorf("Filepath could not be walked %v", err)
- }
- for _, folder := range folders {
- err := filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
- if info.IsDir() {
- gosmi.AppendPath(path)
- } else if info.Mode()&os.ModeSymlink == 0 {
- _, err := gosmi.LoadModule(info.Name())
- if err != nil {
- s.Log.Errorf("Module could not be loaded %v", err)
- }
- }
- return nil
- })
- if err != nil {
- s.Log.Errorf("Filepath could not be walked %v", err)
- }
- }
- folders = []string{}
- }
+func (s *SnmpTrap) Init() error {
+ s.cache = map[string]mibEntry{}
+ s.execCmd = realExecCmd
return nil
}
@@ -277,7 +250,6 @@ func (s *SnmpTrap) Start(acc telegraf.Accumulator) error {
func (s *SnmpTrap) Stop() {
s.listener.Close()
- defer gosmi.Exit()
err := <-s.errCh
if nil != err {
s.Log.Errorf("Error stopping trap listener %v", err)
@@ -298,7 +270,6 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc {
tags["version"] = packet.Version.String()
tags["source"] = addr.IP.String()
-
if packet.Version == gosnmp.Version1 {
// Follow the procedure described in RFC 2576 3.1 to
// translate a v1 trap to v2.
@@ -311,7 +282,7 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc {
}
if trapOid != "" {
- e, err := s.lookupFunc(trapOid)
+ e, err := s.lookup(trapOid)
if err != nil {
s.Log.Errorf("Error resolving V1 OID, oid=%s, source=%s: %v", trapOid, tags["source"], err)
return
@@ -339,6 +310,14 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc {
// only handles textual convention for ip and mac
// addresses
+ e, err := s.lookup(v.Name)
+ if nil != err {
+ s.Log.Errorf("Error resolving OID oid=%s, source=%s: %v", v.Name, tags["source"], err)
+ return
+ }
+
+ name := e.oidText
+
switch v.Type {
case gosnmp.ObjectIdentifier:
val, ok := v.Value.(string)
@@ -349,7 +328,7 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc {
var e mibEntry
var err error
- e, err = s.lookupFunc(val)
+ e, err = s.lookup(val)
if nil != err {
s.Log.Errorf("Error resolving value OID, oid=%s, source=%s: %v", val, tags["source"], err)
return
@@ -363,18 +342,16 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc {
setTrapOid(tags, val, e)
continue
}
+
+ case gosnmp.Integer:
+ if val, ok := e.enumMap[v.Value.(int)]; ok {
+ value = val
+ }
+
default:
value = v.Value
}
- e, err := s.lookupFunc(v.Name)
- if nil != err {
- s.Log.Errorf("Error resolving OID oid=%s, source=%s: %v", v.Name, tags["source"], err)
- return
- }
-
- name := e.oidText
-
fields[name] = value
}
@@ -396,16 +373,50 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc {
}
}
-func lookup(oid string) (e mibEntry, err error) {
- var node gosmi.SmiNode
- node, err = gosmi.GetNodeByOID(types.OidMustFromString(oid))
+func (s *SnmpTrap) lookup(oid string) (e mibEntry, err error) {
+ s.cacheLock.Lock()
+ defer s.cacheLock.Unlock()
+ var ok bool
+ if e, ok = s.cache[oid]; !ok {
+ // cache miss. exec snmptranslate
+ e, err = s.snmptranslate(oid)
+ if err == nil {
+ s.cache[oid] = e
+ }
+ return e, err
+ }
+ return e, nil
+}
+
+func (s *SnmpTrap) clear() {
+ s.cacheLock.Lock()
+ defer s.cacheLock.Unlock()
+ s.cache = map[string]mibEntry{}
+}
+
+func (s *SnmpTrap) load(oid string, e mibEntry) {
+ s.cacheLock.Lock()
+ defer s.cacheLock.Unlock()
+ s.cache[oid] = e
+}
+
+func (s *SnmpTrap) snmptranslate(oid string) (e mibEntry, err error) {
+ var out []byte
+ out, err = s.execCmd(s.Timeout, "snmptranslate", "-Td", "-Ob", "-m", "all", oid)
- // ensure modules are loaded or node will be empty (might not error)
if err != nil {
return e, err
}
- e.oidText = node.RenderQualified()
+ scanner := bufio.NewScanner(bytes.NewBuffer(out))
+
+ // This reads first line
+ ok := scanner.Scan()
+ if err = scanner.Err(); !ok && err != nil {
+ return e, err
+ }
+
+ e.oidText = scanner.Text()
i := strings.Index(e.oidText, "::")
if i == -1 {
@@ -413,5 +424,35 @@ func lookup(oid string) (e mibEntry, err error) {
}
e.mibName = e.oidText[:i]
e.oidText = e.oidText[i+2:]
+
+ // read other lines here
+ e.enumMap = make(map[int]string)
+ syntaxRE := regexp.MustCompile(`\{(.*?)\}`)
+ enumComponentRE := regexp.MustCompile(`\((.*?)\)`)
+ for scanner.Scan() {
+ if err = scanner.Err(); err != nil {
+ return e, err
+ }
+ trapLine := strings.TrimSpace(scanner.Text())
+ if strings.HasPrefix(trapLine, "SYNTAX") {
+ enumStringMatches := syntaxRE.FindStringSubmatch(trapLine)
+ if len(enumStringMatches) > 0 {
+ enumString := strings.TrimLeft(enumStringMatches[0], "{")
+ enumStringTrimmed := strings.TrimRight(enumString, "}")
+ enumStringList := strings.Split(enumStringTrimmed, ",")
+ for _, s := range enumStringList {
+ enumComponentString := strings.TrimSpace(s)
+ enumComponent := enumComponentRE.FindStringSubmatch(enumComponentString)
+ intVal, err := strconv.Atoi(enumComponent[1])
+ if err != nil {
+ // handle error
+ return e, err
+ }
+ e.enumMap[intVal] = enumComponentString
+ }
+ break
+ }
+ }
+ }
return e, nil
}