diff --git a/agent/agent.go b/agent/agent.go index 5762e3334f744..880b897d855c8 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -13,6 +13,7 @@ import ( "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/internal" + "github.com/influxdata/telegraf/internal/snmp" "github.com/influxdata/telegraf/models" "github.com/influxdata/telegraf/plugins/serializers/influx" ) @@ -186,6 +187,10 @@ func (a *Agent) Run(ctx context.Context) error { // initPlugins runs the Init function on plugins. func (a *Agent) initPlugins() error { for _, input := range a.Config.Inputs { + // Share the snmp translator setting with plugins that need it. + if tp, ok := input.Input.(snmp.TranslatorPlugin); ok { + tp.SetTranslator(a.Config.Agent.Translator) + } err := input.Init() if err != nil { return fmt.Errorf("could not initialize input %s: %v", diff --git a/config/config.go b/config/config.go index 40caf83d5bbcf..be1a1ab780208 100644 --- a/config/config.go +++ b/config/config.go @@ -225,6 +225,8 @@ type AgentConfig struct { Hostname string OmitHostname bool + + Translator string `toml:"translator"` } // InputNames returns a list of strings of the configured inputs. @@ -418,6 +420,11 @@ var agentConfig = ` hostname = "" ## If set to true, do no set the "host" tag in the telegraf agent. omit_hostname = false + + ## Translator for SNMP OIDs + ## Valid values are "netsnmp" which runs snmptranslate and snmptable, + ## and "gosmi" which uses the gosmi library. + # translator = "netsnmp" ` var outputHeader = ` @@ -855,6 +862,11 @@ func (c *Config) LoadConfigData(data []byte) error { c.Tags["host"] = c.Agent.Hostname } + // Set snmp agent translator default + if c.Agent.Translator == "" { + c.Agent.Translator = "netsnmp" + } + if len(c.UnusedFields) > 0 { return fmt.Errorf("line %d: configuration specified the fields %q, but they weren't used", tbl.Line, keys(c.UnusedFields)) } diff --git a/internal/snmp/config.go b/internal/snmp/config.go index 4ad1d3a0cd3e3..8475c084ab2d6 100644 --- a/internal/snmp/config.go +++ b/internal/snmp/config.go @@ -12,6 +12,8 @@ type ClientConfig struct { Version uint8 `toml:"version"` // Path to mib files Path []string `toml:"path"` + // Translator implementation + Translator string `toml:"-"` // Parameters for Version 1 & 2 Community string `toml:"community"` diff --git a/internal/snmp/translator.go b/internal/snmp/translator.go new file mode 100644 index 0000000000000..6a0993a6d1a04 --- /dev/null +++ b/internal/snmp/translator.go @@ -0,0 +1,5 @@ +package snmp + +type TranslatorPlugin interface { + SetTranslator(name string) // Agent calls this on inputs before Init +} diff --git a/plugins/inputs/snmp/README.md b/plugins/inputs/snmp/README.md index 27158133efe6b..7d4457916f5e2 100644 --- a/plugins/inputs/snmp/README.md +++ b/plugins/inputs/snmp/README.md @@ -30,6 +30,8 @@ path onto the global path variable # version = 2 ## Path to mib files + ## Used by the gosmi translator. + ## To add paths when translating with netsnmp, use the MIBDIRS environment variable # path = ["/usr/share/snmp/mibs"] ## SNMP community string. diff --git a/plugins/inputs/snmp/gosmi.go b/plugins/inputs/snmp/gosmi.go new file mode 100644 index 0000000000000..f2de844ce6fc0 --- /dev/null +++ b/plugins/inputs/snmp/gosmi.go @@ -0,0 +1,123 @@ +package snmp + +import ( + "fmt" + "sync" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/internal/snmp" + "github.com/sleepinggenius2/gosmi" +) + +type gosmiTranslator struct { +} + +func NewGosmiTranslator(paths []string, log telegraf.Logger) (*gosmiTranslator, error) { + err := snmp.LoadMibsFromPath(paths, log, &snmp.GosmiMibLoader{}) + if err == nil { + return &gosmiTranslator{}, nil + } + return nil, err +} + +type gosmiSnmpTranslateCache struct { + mibName string + oidNum string + oidText string + conversion string + node gosmi.SmiNode + err error +} + +var gosmiSnmpTranslateCachesLock sync.Mutex +var gosmiSnmpTranslateCaches map[string]gosmiSnmpTranslateCache + +//nolint:revive +func (g *gosmiTranslator) SnmpTranslate(oid string) (string, string, string, string, error) { + a, b, c, d, _, e := g.SnmpTranslateFull(oid) + return a, b, c, d, e +} + +//nolint:revive +func (g *gosmiTranslator) SnmpTranslateFull(oid string) ( + mibName string, oidNum string, oidText string, + conversion string, + node gosmi.SmiNode, + err error) { + gosmiSnmpTranslateCachesLock.Lock() + if gosmiSnmpTranslateCaches == nil { + gosmiSnmpTranslateCaches = map[string]gosmiSnmpTranslateCache{} + } + + var stc gosmiSnmpTranslateCache + var ok bool + if stc, ok = gosmiSnmpTranslateCaches[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.node, stc.err = snmp.SnmpTranslateCall(oid) + gosmiSnmpTranslateCaches[oid] = stc + } + + gosmiSnmpTranslateCachesLock.Unlock() + + return stc.mibName, stc.oidNum, stc.oidText, stc.conversion, stc.node, stc.err +} + +type gosmiSnmpTableCache struct { + mibName string + oidNum string + oidText string + fields []Field + err error +} + +var gosmiSnmpTableCaches map[string]gosmiSnmpTableCache +var gosmiSnmpTableCachesLock sync.Mutex + +// snmpTable resolves the given OID as a table, providing information about the +// table and fields within. +//nolint:revive //Too many return variable but necessary +func (g *gosmiTranslator) SnmpTable(oid string) ( + mibName string, oidNum string, oidText string, + fields []Field, + err error) { + gosmiSnmpTableCachesLock.Lock() + if gosmiSnmpTableCaches == nil { + gosmiSnmpTableCaches = map[string]gosmiSnmpTableCache{} + } + + var stc gosmiSnmpTableCache + var ok bool + if stc, ok = gosmiSnmpTableCaches[oid]; !ok { + stc.mibName, stc.oidNum, stc.oidText, stc.fields, stc.err = g.SnmpTableCall(oid) + gosmiSnmpTableCaches[oid] = stc + } + + gosmiSnmpTableCachesLock.Unlock() + return stc.mibName, stc.oidNum, stc.oidText, stc.fields, stc.err +} + +//nolint:revive //Too many return variable but necessary +func (g *gosmiTranslator) SnmpTableCall(oid string) (mibName string, oidNum string, oidText string, fields []Field, err error) { + mibName, oidNum, oidText, _, node, err := g.SnmpTranslateFull(oid) + if err != nil { + return "", "", "", nil, fmt.Errorf("translating: %w", err) + } + + mibPrefix := mibName + "::" + + col, tagOids, err := snmp.GetIndex(oidNum, mibPrefix, node) + + for _, c := range col { + _, isTag := tagOids[mibPrefix+c] + fields = append(fields, Field{Name: c, Oid: mibPrefix + c, IsTag: isTag}) + } + + return mibName, oidNum, oidText, fields, err +} diff --git a/plugins/inputs/snmp/gosmi_test.go b/plugins/inputs/snmp/gosmi_test.go new file mode 100644 index 0000000000000..bca48ffa17094 --- /dev/null +++ b/plugins/inputs/snmp/gosmi_test.go @@ -0,0 +1,943 @@ +package snmp + +import ( + "fmt" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/influxdata/telegraf/internal/snmp" + "github.com/influxdata/telegraf/testutil" +) + +func getGosmiTr(t *testing.T) Translator { + testDataPath, err := filepath.Abs("./testdata") + require.NoError(t, err) + + tr, err := NewGosmiTranslator([]string{testDataPath}, testutil.Logger{}) + require.NoError(t, err) + return tr +} + +func TestGosmiTranslator(t *testing.T) { + var tr Translator + var err error + + tr, err = NewGosmiTranslator([]string{"testdata"}, testutil.Logger{}) + require.NoError(t, err) + require.NotNil(t, tr) +} + +//gosmi uses the same connection struct as netsnmp but has a few +//different test cases, so it has its own copy +var gosmiTsc = &testSNMPConnection{ + host: "tsc", + values: map[string]interface{}{ + ".1.3.6.1.2.1.3.1.1.1.0": "foo", + ".1.3.6.1.2.1.3.1.1.1.1": []byte("bar"), + ".1.3.6.1.2.1.3.1.1.1.2": []byte(""), + ".1.3.6.1.2.1.3.1.1.102": "bad", + ".1.3.6.1.2.1.3.1.1.2.0": 1, + ".1.3.6.1.2.1.3.1.1.2.1": 2, + ".1.3.6.1.2.1.3.1.1.2.2": 0, + ".1.3.6.1.2.1.3.1.1.3.0": "1.3.6.1.2.1.3.1.1.3", + ".1.3.6.1.2.1.3.1.1.5.0": 123456, + ".1.0.0.0.1.1.0": "foo", + ".1.0.0.0.1.1.1": []byte("bar"), + ".1.0.0.0.1.1.2": []byte(""), + ".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.2.2": 0, + ".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": "0.000", + ".1.0.0.0.1.3.3": "9.999", + ".1.0.0.0.1.5.0": 123456, + ".1.0.0.1.1": "baz", + ".1.0.0.1.2": 234, + ".1.0.0.1.3": []byte("byte slice"), + ".1.0.0.2.1.5.0.9.9": 11, + ".1.0.0.2.1.5.1.9.9": 22, + ".1.0.0.0.1.6.0": ".1.0.0.0.1.7", + ".1.0.0.3.1.1.10": "instance", + ".1.0.0.3.1.1.11": "instance2", + ".1.0.0.3.1.1.12": "instance3", + ".1.0.0.3.1.2.10": 10, + ".1.0.0.3.1.2.11": 20, + ".1.0.0.3.1.2.12": 20, + ".1.0.0.3.1.3.10": 1, + ".1.0.0.3.1.3.11": 2, + ".1.0.0.3.1.3.12": 3, + }, +} + +func TestFieldInitGosmi(t *testing.T) { + testDataPath, err := filepath.Abs("./testdata") + require.NoError(t, err) + + tr, err := NewGosmiTranslator([]string{testDataPath}, testutil.Logger{}) + require.NoError(t, err) + + translations := []struct { + inputOid string + inputName string + inputConversion string + expectedOid string + expectedName string + expectedConversion string + }{ + {".1.2.3", "foo", "", ".1.2.3", "foo", ""}, + {".iso.2.3", "foo", "", ".1.2.3", "foo", ""}, + {".1.0.0.0.1.1", "", "", ".1.0.0.0.1.1", "server", ""}, + {"IF-MIB::ifPhysAddress.1", "", "", ".1.3.6.1.2.1.2.2.1.6.1", "ifPhysAddress.1", "hwaddr"}, + {"IF-MIB::ifPhysAddress.1", "", "none", ".1.3.6.1.2.1.2.2.1.6.1", "ifPhysAddress.1", "none"}, + {"BRIDGE-MIB::dot1dTpFdbAddress.1", "", "", ".1.3.6.1.2.1.17.4.3.1.1.1", "dot1dTpFdbAddress.1", "hwaddr"}, + {"TCP-MIB::tcpConnectionLocalAddress.1", "", "", ".1.3.6.1.2.1.6.19.1.2.1", "tcpConnectionLocalAddress.1", "ipaddr"}, + {".999", "", "", ".999", ".999", ""}, + } + + for _, txl := range translations { + f := Field{Oid: txl.inputOid, Name: txl.inputName, Conversion: txl.inputConversion} + err := f.init(tr) + require.NoError(t, err, "inputOid='%s' inputName='%s'", txl.inputOid, txl.inputName) + + assert.Equal(t, txl.expectedOid, f.Oid, "inputOid='%s' inputName='%s' inputConversion='%s'", txl.inputOid, txl.inputName, txl.inputConversion) + assert.Equal(t, txl.expectedName, f.Name, "inputOid='%s' inputName='%s' inputConversion='%s'", txl.inputOid, txl.inputName, txl.inputConversion) + } +} + +func TestTableInitGosmi(t *testing.T) { + testDataPath, err := filepath.Abs("./testdata") + require.NoError(t, err) + + s := &Snmp{ + ClientConfig: snmp.ClientConfig{ + Path: []string{testDataPath}, + Translator: "gosmi", + }, + Tables: []Table{ + {Oid: ".1.3.6.1.2.1.3.1", + Fields: []Field{ + {Oid: ".999", Name: "foo"}, + {Oid: ".1.3.6.1.2.1.3.1.1.1", Name: "atIfIndex", IsTag: true}, + {Oid: "RFC1213-MIB::atPhysAddress", Name: "atPhysAddress"}, + }}, + }, + } + err = s.Init() + require.NoError(t, err) + + assert.Equal(t, "atTable", s.Tables[0].Name) + + assert.Len(t, s.Tables[0].Fields, 5) + assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".999", Name: "foo", initialized: true}) + assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.3.6.1.2.1.3.1.1.1", Name: "atIfIndex", initialized: true, IsTag: true}) + assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.3.6.1.2.1.3.1.1.2", Name: "atPhysAddress", initialized: true, Conversion: "hwaddr"}) + assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.3.6.1.2.1.3.1.1.3", Name: "atNetAddress", initialized: true, IsTag: true}) +} + +func TestSnmpInitGosmi(t *testing.T) { + testDataPath, err := filepath.Abs("./testdata") + require.NoError(t, err) + + s := &Snmp{ + Tables: []Table{ + {Oid: "RFC1213-MIB::atTable"}, + }, + Fields: []Field{ + {Oid: "RFC1213-MIB::atPhysAddress"}, + }, + ClientConfig: snmp.ClientConfig{ + Path: []string{testDataPath}, + Translator: "gosmi", + }, + } + + 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.3.6.1.2.1.3.1.1.1", Name: "atIfIndex", IsTag: true, initialized: true}) + assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.3.6.1.2.1.3.1.1.2", Name: "atPhysAddress", initialized: true, Conversion: "hwaddr"}) + assert.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.3.6.1.2.1.3.1.1.3", Name: "atNetAddress", IsTag: true, initialized: true}) + + assert.Equal(t, Field{ + Oid: ".1.3.6.1.2.1.3.1.1.2", + Name: "atPhysAddress", + Conversion: "hwaddr", + initialized: true, + }, s.Fields[0]) +} + +func TestSnmpInit_noTranslateGosmi(t *testing.T) { + s := &Snmp{ + Fields: []Field{ + {Oid: ".9.1.1.1.1", Name: "one", IsTag: true}, + {Oid: ".9.1.1.1.2", Name: "two"}, + {Oid: ".9.1.1.1.3"}, + }, + Tables: []Table{ + {Name: "testing", + Fields: []Field{ + {Oid: ".9.1.1.1.4", Name: "four", IsTag: true}, + {Oid: ".9.1.1.1.5", Name: "five"}, + {Oid: ".9.1.1.1.6"}, + }}, + }, + ClientConfig: snmp.ClientConfig{ + Path: []string{}, + Translator: "gosmi", + }, + } + + err := s.Init() + require.NoError(t, err) + + assert.Equal(t, ".9.1.1.1.1", s.Fields[0].Oid) + assert.Equal(t, "one", s.Fields[0].Name) + assert.Equal(t, true, s.Fields[0].IsTag) + + assert.Equal(t, ".9.1.1.1.2", s.Fields[1].Oid) + assert.Equal(t, "two", s.Fields[1].Name) + assert.Equal(t, false, s.Fields[1].IsTag) + + assert.Equal(t, ".9.1.1.1.3", s.Fields[2].Oid) + assert.Equal(t, ".9.1.1.1.3", s.Fields[2].Name) + assert.Equal(t, false, s.Fields[2].IsTag) + + assert.Equal(t, ".9.1.1.1.4", s.Tables[0].Fields[0].Oid) + assert.Equal(t, "four", s.Tables[0].Fields[0].Name) + assert.Equal(t, true, s.Tables[0].Fields[0].IsTag) + + assert.Equal(t, ".9.1.1.1.5", s.Tables[0].Fields[1].Oid) + assert.Equal(t, "five", s.Tables[0].Fields[1].Name) + assert.Equal(t, false, s.Tables[0].Fields[1].IsTag) + + assert.Equal(t, ".9.1.1.1.6", s.Tables[0].Fields[2].Oid) + assert.Equal(t, ".9.1.1.1.6", s.Tables[0].Fields[2].Name) + assert.Equal(t, false, s.Tables[0].Fields[2].IsTag) +} + +//TestTableBuild_walk in snmp_test.go is split into two tests here, +//noTranslate and Translate. +// +//This is only running with gosmi translator but should be valid with +//netsnmp too. +func TestTableBuild_walk_noTranslate(t *testing.T) { + tbl := Table{ + Name: "mytable", + IndexAsTag: true, + 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", + }, + { + Name: "myfield4", + Oid: ".1.0.0.2.1.5", + OidIndexSuffix: ".9.9", + }, + { + Name: "myfield5", + Oid: ".1.0.0.2.1.5", + OidIndexLength: 1, + }, + }, + } + + testDataPath, err := filepath.Abs("./testdata") + require.NoError(t, err) + + tr, err := NewGosmiTranslator([]string{testDataPath}, testutil.Logger{}) + require.NoError(t, err) + + tb, err := tbl.Build(gosmiTsc, true, tr) + require.NoError(t, err) + assert.Equal(t, tb.Name, "mytable") + rtr1 := RTableRow{ + Tags: map[string]string{ + "myfield1": "foo", + "index": "0", + }, + Fields: map[string]interface{}{ + "myfield2": 1, + "myfield3": float64(0.123), + "myfield4": 11, + "myfield5": 11, + }, + } + rtr2 := RTableRow{ + Tags: map[string]string{ + "myfield1": "bar", + "index": "1", + }, + Fields: map[string]interface{}{ + "myfield2": 2, + "myfield3": float64(0.456), + "myfield4": 22, + "myfield5": 22, + }, + } + rtr3 := RTableRow{ + Tags: map[string]string{ + "index": "2", + }, + Fields: map[string]interface{}{ + "myfield2": 0, + "myfield3": float64(0.0), + }, + } + rtr4 := RTableRow{ + Tags: map[string]string{ + "index": "3", + }, + Fields: map[string]interface{}{ + "myfield3": float64(9.999), + }, + } + assert.Len(t, tb.Rows, 4) + assert.Contains(t, tb.Rows, rtr1) + assert.Contains(t, tb.Rows, rtr2) + assert.Contains(t, tb.Rows, rtr3) + assert.Contains(t, tb.Rows, rtr4) +} + +func TestTableBuild_walk_Translate(t *testing.T) { + testDataPath, err := filepath.Abs("./testdata") + require.NoError(t, err) + + tr, err := NewGosmiTranslator([]string{testDataPath}, testutil.Logger{}) + require.NoError(t, err) + + tbl := Table{ + Name: "atTable", + IndexAsTag: true, + Fields: []Field{ + { + Name: "ifIndex", + Oid: "1.3.6.1.2.1.3.1.1.1", + IsTag: true, + }, + { + Name: "atPhysAddress", + Oid: "1.3.6.1.2.1.3.1.1.2", + Translate: false, + }, + { + Name: "atNetAddress", + Oid: "1.3.6.1.2.1.3.1.1.3", + Translate: true, + }, + }, + } + + err = tbl.Init(tr) + require.NoError(t, err) + tb, err := tbl.Build(gosmiTsc, true, tr) + require.NoError(t, err) + + require.Equal(t, tb.Name, "atTable") + + rtr1 := RTableRow{ + Tags: map[string]string{ + "ifIndex": "foo", + "index": "0", + }, + Fields: map[string]interface{}{ + "atPhysAddress": 1, + "atNetAddress": "atNetAddress", + }, + } + rtr2 := RTableRow{ + Tags: map[string]string{ + "ifIndex": "bar", + "index": "1", + }, + Fields: map[string]interface{}{ + "atPhysAddress": 2, + }, + } + rtr3 := RTableRow{ + Tags: map[string]string{ + "index": "2", + }, + Fields: map[string]interface{}{ + "atPhysAddress": 0, + }, + } + + require.Len(t, tb.Rows, 3) + require.Contains(t, tb.Rows, rtr1) + require.Contains(t, tb.Rows, rtr2) + require.Contains(t, tb.Rows, rtr3) +} + +func TestTableBuild_noWalkGosmi(t *testing.T) { + testDataPath, err := filepath.Abs("./testdata") + require.NoError(t, err) + + tr, err := NewGosmiTranslator([]string{testDataPath}, testutil.Logger{}) + require.NoError(t, err) + + 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, + }, + { + Name: "empty", + Oid: ".1.0.0.0.1.1.2", + }, + { + Name: "noexist", + Oid: ".1.2.3.4.5", + }, + }, + } + + tb, err := tbl.Build(gosmiTsc, false, tr) + 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 TestGatherGosmi(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.5", + }, + }, + }, + }, + + connectionCache: []snmpConnection{ + gosmiTsc, + }, + + ClientConfig: snmp.ClientConfig{ + Path: []string{"testdata"}, + Translator: "gosmi", + }, + } + acc := &testutil.Accumulator{} + + tstart := time.Now() + require.NoError(t, 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[s.AgentHostTag]) + 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.False(t, tstart.After(m.Time)) + assert.False(t, tstop.Before(m.Time)) + + m2 := acc.Metrics[1] + assert.Equal(t, "myOtherTable", m2.Measurement) + assert.Equal(t, "tsc", m2.Tags[s.AgentHostTag]) + assert.Equal(t, "baz", m2.Tags["myfield1"]) + assert.Len(t, m2.Fields, 1) + assert.Equal(t, 123456, m2.Fields["myOtherField"]) +} + +func TestGather_hostGosmi(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: []snmpConnection{ + gosmiTsc, + }, + } + + acc := &testutil.Accumulator{} + + require.NoError(t, s.Gather(acc)) + + require.Len(t, acc.Metrics, 1) + m := acc.Metrics[0] + assert.Equal(t, "baz", m.Tags["host"]) +} + +func TestFieldConvertGosmi(t *testing.T) { + testTable := []struct { + input interface{} + conv string + expected interface{} + }{ + {[]byte("foo"), "", "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)}, + {float64(0.123123123123), "float", float64(0.123123123123)}, + {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)}, + {"123123123123", "int", int64(123123123123)}, + {[]byte("123123123123"), "int", int64(123123123123)}, + {float32(12.3), "int", int64(12)}, + {float64(12.3), "int", int64(12)}, + {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)}, + {[]byte("abcdef"), "hwaddr", "61:62:63:64:65:66"}, + {"abcdef", "hwaddr", "61:62:63:64:65:66"}, + {[]byte("abcd"), "ipaddr", "97.98.99.100"}, + {"abcd", "ipaddr", "97.98.99.100"}, + {[]byte("abcdefghijklmnop"), "ipaddr", "6162:6364:6566:6768:696a:6b6c:6d6e:6f70"}, + {[]byte{0x00, 0x09, 0x3E, 0xE3, 0xF6, 0xD5, 0x3B, 0x60}, "hextoint:BigEndian:uint64", uint64(2602423610063712)}, + {[]byte{0x00, 0x09, 0x3E, 0xE3}, "hextoint:BigEndian:uint32", uint32(605923)}, + {[]byte{0x00, 0x09}, "hextoint:BigEndian:uint16", uint16(9)}, + {[]byte{0x00, 0x09, 0x3E, 0xE3, 0xF6, 0xD5, 0x3B, 0x60}, "hextoint:LittleEndian:uint64", uint64(6934371307618175232)}, + {[]byte{0x00, 0x09, 0x3E, 0xE3}, "hextoint:LittleEndian:uint32", uint32(3812493568)}, + {[]byte{0x00, 0x09}, "hextoint:LittleEndian:uint16", uint16(2304)}, + } + + for _, tc := range testTable { + act, err := fieldConvert(tc.conv, tc.input) + assert.NoError(t, err, "input=%T(%v) conv=%s expected=%T(%v)", tc.input, tc.input, tc.conv, tc.expected, tc.expected) + 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 TestSnmpTranslateCache_missGosmi(t *testing.T) { + gosmiSnmpTranslateCaches = nil + oid := "IF-MIB::ifPhysAddress.1" + mibName, oidNum, oidText, conversion, err := getGosmiTr(t).SnmpTranslate(oid) + assert.Len(t, gosmiSnmpTranslateCaches, 1) + stc := gosmiSnmpTranslateCaches[oid] + assert.NotNil(t, stc) + assert.Equal(t, mibName, stc.mibName) + assert.Equal(t, oidNum, stc.oidNum) + assert.Equal(t, oidText, stc.oidText) + assert.Equal(t, conversion, stc.conversion) + assert.Equal(t, err, stc.err) +} + +func TestSnmpTranslateCache_hitGosmi(t *testing.T) { + gosmiSnmpTranslateCaches = map[string]gosmiSnmpTranslateCache{ + "foo": { + mibName: "a", + oidNum: "b", + oidText: "c", + conversion: "d", + err: fmt.Errorf("e"), + }, + } + mibName, oidNum, oidText, conversion, err := getGosmiTr(t).SnmpTranslate("foo") + assert.Equal(t, "a", mibName) + assert.Equal(t, "b", oidNum) + assert.Equal(t, "c", oidText) + assert.Equal(t, "d", conversion) + assert.Equal(t, fmt.Errorf("e"), err) + gosmiSnmpTranslateCaches = nil +} + +func TestSnmpTableCache_missGosmi(t *testing.T) { + gosmiSnmpTableCaches = nil + oid := ".1.0.0.0" + mibName, oidNum, oidText, fields, err := getGosmiTr(t).SnmpTable(oid) + assert.Len(t, gosmiSnmpTableCaches, 1) + stc := gosmiSnmpTableCaches[oid] + assert.NotNil(t, stc) + assert.Equal(t, mibName, stc.mibName) + assert.Equal(t, oidNum, stc.oidNum) + assert.Equal(t, oidText, stc.oidText) + assert.Equal(t, fields, stc.fields) + assert.Equal(t, err, stc.err) +} + +func TestSnmpTableCache_hitGosmi(t *testing.T) { + gosmiSnmpTableCaches = map[string]gosmiSnmpTableCache{ + "foo": { + mibName: "a", + oidNum: "b", + oidText: "c", + fields: []Field{{Name: "d"}}, + err: fmt.Errorf("e"), + }, + } + mibName, oidNum, oidText, fields, err := getGosmiTr(t).SnmpTable("foo") + assert.Equal(t, "a", mibName) + assert.Equal(t, "b", oidNum) + assert.Equal(t, "c", oidText) + assert.Equal(t, []Field{{Name: "d"}}, fields) + assert.Equal(t, fmt.Errorf("e"), err) +} + +func TestTableJoin_walkGosmi(t *testing.T) { + tbl := Table{ + Name: "mytable", + IndexAsTag: true, + Fields: []Field{ + { + Name: "myfield1", + Oid: ".1.0.0.3.1.1", + IsTag: true, + }, + { + Name: "myfield2", + Oid: ".1.0.0.3.1.2", + }, + { + Name: "myfield3", + Oid: ".1.0.0.3.1.3", + SecondaryIndexTable: true, + }, + { + Name: "myfield4", + Oid: ".1.0.0.0.1.1", + SecondaryIndexUse: true, + IsTag: true, + }, + { + Name: "myfield5", + Oid: ".1.0.0.0.1.2", + SecondaryIndexUse: true, + }, + }, + } + + testDataPath, err := filepath.Abs("./testdata") + require.NoError(t, err) + + tr, err := NewGosmiTranslator([]string{testDataPath}, testutil.Logger{}) + require.NoError(t, err) + + tb, err := tbl.Build(gosmiTsc, true, tr) + require.NoError(t, err) + + assert.Equal(t, tb.Name, "mytable") + rtr1 := RTableRow{ + Tags: map[string]string{ + "myfield1": "instance", + "myfield4": "bar", + "index": "10", + }, + Fields: map[string]interface{}{ + "myfield2": 10, + "myfield3": 1, + "myfield5": 2, + }, + } + rtr2 := RTableRow{ + Tags: map[string]string{ + "myfield1": "instance2", + "index": "11", + }, + Fields: map[string]interface{}{ + "myfield2": 20, + "myfield3": 2, + "myfield5": 0, + }, + } + rtr3 := RTableRow{ + Tags: map[string]string{ + "myfield1": "instance3", + "index": "12", + }, + Fields: map[string]interface{}{ + "myfield2": 20, + "myfield3": 3, + }, + } + assert.Len(t, tb.Rows, 3) + assert.Contains(t, tb.Rows, rtr1) + assert.Contains(t, tb.Rows, rtr2) + assert.Contains(t, tb.Rows, rtr3) +} + +func TestTableOuterJoin_walkGosmi(t *testing.T) { + tbl := Table{ + Name: "mytable", + IndexAsTag: true, + Fields: []Field{ + { + Name: "myfield1", + Oid: ".1.0.0.3.1.1", + IsTag: true, + }, + { + Name: "myfield2", + Oid: ".1.0.0.3.1.2", + }, + { + Name: "myfield3", + Oid: ".1.0.0.3.1.3", + SecondaryIndexTable: true, + SecondaryOuterJoin: true, + }, + { + Name: "myfield4", + Oid: ".1.0.0.0.1.1", + SecondaryIndexUse: true, + IsTag: true, + }, + { + Name: "myfield5", + Oid: ".1.0.0.0.1.2", + SecondaryIndexUse: true, + }, + }, + } + + testDataPath, err := filepath.Abs("./testdata") + require.NoError(t, err) + + tr, err := NewGosmiTranslator([]string{testDataPath}, testutil.Logger{}) + require.NoError(t, err) + + tb, err := tbl.Build(gosmiTsc, true, tr) + require.NoError(t, err) + + assert.Equal(t, tb.Name, "mytable") + rtr1 := RTableRow{ + Tags: map[string]string{ + "myfield1": "instance", + "myfield4": "bar", + "index": "10", + }, + Fields: map[string]interface{}{ + "myfield2": 10, + "myfield3": 1, + "myfield5": 2, + }, + } + rtr2 := RTableRow{ + Tags: map[string]string{ + "myfield1": "instance2", + "index": "11", + }, + Fields: map[string]interface{}{ + "myfield2": 20, + "myfield3": 2, + "myfield5": 0, + }, + } + rtr3 := RTableRow{ + Tags: map[string]string{ + "myfield1": "instance3", + "index": "12", + }, + Fields: map[string]interface{}{ + "myfield2": 20, + "myfield3": 3, + }, + } + rtr4 := RTableRow{ + Tags: map[string]string{ + "index": "Secondary.0", + "myfield4": "foo", + }, + Fields: map[string]interface{}{ + "myfield5": 1, + }, + } + assert.Len(t, tb.Rows, 4) + assert.Contains(t, tb.Rows, rtr1) + assert.Contains(t, tb.Rows, rtr2) + assert.Contains(t, tb.Rows, rtr3) + assert.Contains(t, tb.Rows, rtr4) +} + +func TestTableJoinNoIndexAsTag_walkGosmi(t *testing.T) { + tbl := Table{ + Name: "mytable", + IndexAsTag: false, + Fields: []Field{ + { + Name: "myfield1", + Oid: ".1.0.0.3.1.1", + IsTag: true, + }, + { + Name: "myfield2", + Oid: ".1.0.0.3.1.2", + }, + { + Name: "myfield3", + Oid: ".1.0.0.3.1.3", + SecondaryIndexTable: true, + }, + { + Name: "myfield4", + Oid: ".1.0.0.0.1.1", + SecondaryIndexUse: true, + IsTag: true, + }, + { + Name: "myfield5", + Oid: ".1.0.0.0.1.2", + SecondaryIndexUse: true, + }, + }, + } + + testDataPath, err := filepath.Abs("./testdata") + require.NoError(t, err) + + tr, err := NewGosmiTranslator([]string{testDataPath}, testutil.Logger{}) + require.NoError(t, err) + + tb, err := tbl.Build(gosmiTsc, true, tr) + require.NoError(t, err) + + assert.Equal(t, tb.Name, "mytable") + rtr1 := RTableRow{ + Tags: map[string]string{ + "myfield1": "instance", + "myfield4": "bar", + //"index": "10", + }, + Fields: map[string]interface{}{ + "myfield2": 10, + "myfield3": 1, + "myfield5": 2, + }, + } + rtr2 := RTableRow{ + Tags: map[string]string{ + "myfield1": "instance2", + //"index": "11", + }, + Fields: map[string]interface{}{ + "myfield2": 20, + "myfield3": 2, + "myfield5": 0, + }, + } + rtr3 := RTableRow{ + Tags: map[string]string{ + "myfield1": "instance3", + //"index": "12", + }, + Fields: map[string]interface{}{ + "myfield2": 20, + "myfield3": 3, + }, + } + assert.Len(t, tb.Rows, 3) + assert.Contains(t, tb.Rows, rtr1) + assert.Contains(t, tb.Rows, rtr2) + assert.Contains(t, tb.Rows, rtr3) +} + +func BenchmarkMibLoading(b *testing.B) { + log := testutil.Logger{} + path := []string{"testdata"} + for i := 0; i < b.N; i++ { + err := snmp.LoadMibsFromPath(path, log, &snmp.GosmiMibLoader{}) + require.NoError(b, err) + } +} + +func TestCanNotParse(t *testing.T) { + s := &Snmp{ + Fields: []Field{ + {Oid: "RFC1213-MIB::"}, + }, + ClientConfig: snmp.ClientConfig{ + Path: []string{"testdata"}, + Translator: "gosmi", + }, + } + + err := s.Init() + require.Error(t, err) +} + +func TestMissingMibPath(t *testing.T) { + log := testutil.Logger{} + path := []string{"non-existing-directory"} + err := snmp.LoadMibsFromPath(path, log, &snmp.GosmiMibLoader{}) + require.NoError(t, err) +} diff --git a/plugins/inputs/snmp/netsnmp.go b/plugins/inputs/snmp/netsnmp.go new file mode 100644 index 0000000000000..96339956f5c71 --- /dev/null +++ b/plugins/inputs/snmp/netsnmp.go @@ -0,0 +1,256 @@ +package snmp + +import ( + "bufio" + "bytes" + "fmt" + "log" //nolint:revive + "os/exec" + "strings" + "sync" + + "github.com/influxdata/wlog" +) + +//struct that implements the translator interface. This calls existing +//code to exec netsnmp's snmptranslate program +type netsnmpTranslator struct { +} + +func NewNetsnmpTranslator() *netsnmpTranslator { + return &netsnmpTranslator{} +} + +type snmpTableCache struct { + mibName string + oidNum string + oidText string + fields []Field + err error +} + +// 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 +} + +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. +//nolint:revive +func (n *netsnmpTranslator) 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 = n.snmpTableCall(oid) + snmpTableCaches[oid] = stc + } + + snmpTableCachesLock.Unlock() + return stc.mibName, stc.oidNum, stc.oidText, stc.fields, stc.err +} + +//nolint:revive +func (n *netsnmpTranslator) snmpTableCall(oid string) ( + mibName string, oidNum string, oidText string, + fields []Field, + err error) { + mibName, oidNum, oidText, _, err = n.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. +//nolint:revive +func (n *netsnmpTranslator) 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 +} + +//nolint:revive +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/snmp.go b/plugins/inputs/snmp/snmp.go index 39111af7715b2..23a872ca692c8 100644 --- a/plugins/inputs/snmp/snmp.go +++ b/plugins/inputs/snmp/snmp.go @@ -12,7 +12,6 @@ import ( "time" "github.com/gosnmp/gosnmp" - "github.com/sleepinggenius2/gosmi" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/config" @@ -39,6 +38,8 @@ const sampleConfig = ` # version = 2 ## Path to mib files + ## Used by the gosmi translator. + ## To add paths when translating with netsnmp, use the MIBDIRS environment variable # path = ["/usr/share/snmp/mibs"] ## Agent host tag; the tag used to reference the source host @@ -69,12 +70,26 @@ const sampleConfig = ` # 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. ` +type Translator interface { + SnmpTranslate(oid string) ( + mibName string, oidNum string, oidText string, + conversion string, + err error, + ) + + SnmpTable(oid string) ( + mibName string, oidNum string, oidText string, + fields []Field, + err error, + ) +} + // Snmp holds the configuration for the plugin. type Snmp struct { // The SNMP agent to query. Format is [SCHEME://]ADDR[:PORT] (e.g. @@ -97,24 +112,38 @@ type Snmp struct { connectionCache []snmpConnection Log telegraf.Logger `toml:"-"` + + translator Translator +} + +func (s *Snmp) SetTranslator(name string) { + s.Translator = name } func (s *Snmp) Init() error { - err := snmp.LoadMibsFromPath(s.Path, s.Log, &snmp.GosmiMibLoader{}) - if err != nil { - return err + var err error + switch s.Translator { + case "gosmi": + s.translator, err = NewGosmiTranslator(s.Path, s.Log) + if err != nil { + return err + } + case "netsnmp": + s.translator = NewNetsnmpTranslator() + default: + return fmt.Errorf("invalid translator value") } s.connectionCache = make([]snmpConnection, len(s.Agents)) for i := range s.Tables { - if err := s.Tables[i].Init(); err != nil { + if err := s.Tables[i].Init(s.translator); 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 { + if err := s.Fields[i].init(s.translator); err != nil { return fmt.Errorf("initializing field %s: %w", s.Fields[i].Name, err) } } @@ -149,7 +178,7 @@ type Table struct { } // Init() builds & initializes the nested fields. -func (t *Table) Init() error { +func (t *Table) Init(tr Translator) 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 == "" { @@ -160,14 +189,14 @@ func (t *Table) Init() error { return nil } - if err := t.initBuild(); err != nil { + if err := t.initBuild(tr); err != nil { return err } secondaryIndexTablePresent := false // initialize all the nested fields for i := range t.Fields { - if err := t.Fields[i].init(); err != nil { + if err := t.Fields[i].init(tr); err != nil { return fmt.Errorf("initializing field %s: %w", t.Fields[i].Name, err) } if t.Fields[i].SecondaryIndexTable { @@ -185,12 +214,12 @@ func (t *Table) Init() error { // 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 { +func (t *Table) initBuild(tr Translator) error { if t.Oid == "" { return nil } - _, _, oidText, fields, err := snmpTable(t.Oid) + _, _, oidText, fields, err := tr.SnmpTable(t.Oid) if err != nil { return err } @@ -254,14 +283,14 @@ type Field struct { } // init() converts OID names to numbers, and sets the .Name attribute if unset. -func (f *Field) init() error { +func (f *Field) init(tr Translator) 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) + _, oidNum, oidText, conversion, err := tr.SnmpTranslate(f.Oid) if err != nil { return fmt.Errorf("translating: %w", err) } @@ -384,7 +413,7 @@ func (s *Snmp) Gather(acc telegraf.Accumulator) error { } func (s *Snmp) gatherTable(acc telegraf.Accumulator, gs snmpConnection, t Table, topTags map[string]string, walk bool) error { - rt, err := t.Build(gs, walk) + rt, err := t.Build(gs, walk, s.translator) if err != nil { return err } @@ -413,7 +442,7 @@ func (s *Snmp) gatherTable(acc telegraf.Accumulator, gs snmpConnection, t Table, } // Build retrieves all the fields specified in the table and constructs the RTable. -func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) { +func (t Table) Build(gs snmpConnection, walk bool, tr Translator) (*RTable, error) { rows := map[string]RTableRow{} //translation table for secondary index (when preforming join on two tables) @@ -505,7 +534,7 @@ func (t Table) Build(gs snmpConnection, walk bool) (*RTable, error) { // snmptranslate table field value here if f.Translate { if entOid, ok := ent.Value.(string); ok { - _, _, oidText, _, _, err := SnmpTranslate(entOid) + _, _, oidText, _, err := tr.SnmpTranslate(entOid) if err == nil { // If no error translating, the original value for ent.Value should be replaced ent.Value = oidText @@ -794,93 +823,3 @@ func fieldConvert(conv string, v interface{}) (interface{}, error) { 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. -//nolint:revive //Too many return variable but necessary -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 -} - -//nolint:revive //Too many return variable but necessary -func snmpTableCall(oid string) (mibName string, oidNum string, oidText string, fields []Field, err error) { - mibName, oidNum, oidText, _, node, err := SnmpTranslate(oid) - if err != nil { - return "", "", "", nil, fmt.Errorf("translating: %w", err) - } - - mibPrefix := mibName + "::" - - col, tagOids, err := snmp.GetIndex(oidNum, mibPrefix, node) - - for _, c := range col { - _, isTag := tagOids[mibPrefix+c] - fields = append(fields, Field{Name: c, Oid: mibPrefix + c, IsTag: isTag}) - } - - return mibName, oidNum, oidText, fields, err -} - -type snmpTranslateCache struct { - mibName string - oidNum string - oidText string - conversion string - node gosmi.SmiNode - err error -} - -var snmpTranslateCachesLock sync.Mutex -var snmpTranslateCaches map[string]snmpTranslateCache - -// snmpTranslate resolves the given OID. -//nolint:revive //Too many return variable but necessary -func SnmpTranslate(oid string) (mibName string, oidNum string, oidText string, conversion string, node gosmi.SmiNode, 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.node, stc.err = snmp.SnmpTranslateCall(oid) - snmpTranslateCaches[oid] = stc - } - - snmpTranslateCachesLock.Unlock() - - return stc.mibName, stc.oidNum, stc.oidText, stc.conversion, stc.node, stc.err -} diff --git a/plugins/inputs/snmp/snmp_mocks_generate.go b/plugins/inputs/snmp/snmp_mocks_generate.go new file mode 100644 index 0000000000000..f87f9029b0d06 --- /dev/null +++ b/plugins/inputs/snmp/snmp_mocks_generate.go @@ -0,0 +1,103 @@ +//go:build generate +// +build generate + +package main + +import ( + "bufio" + "bytes" + "fmt" + "os" + "os/exec" + "strings" +) + +// This file is a generator used to generate the mocks for the commands used by the tests. + +// These are the commands to be mocked. +var mockedCommands = [][]string{ + {"snmptranslate", "-Td", "-Ob", "-m", "all", ".1.0.0.0"}, + {"snmptranslate", "-Td", "-Ob", "-m", "all", ".1.0.0.1.1"}, + {"snmptranslate", "-Td", "-Ob", "-m", "all", ".1.0.0.1.2"}, + {"snmptranslate", "-Td", "-Ob", "-m", "all", "1.0.0.1.1"}, + {"snmptranslate", "-Td", "-Ob", "-m", "all", ".1.0.0.0.1.1"}, + {"snmptranslate", "-Td", "-Ob", "-m", "all", ".1.0.0.0.1.1.0"}, + {"snmptranslate", "-Td", "-Ob", "-m", "all", ".1.0.0.0.1.5"}, + {"snmptranslate", "-Td", "-Ob", "-m", "all", ".1.2.3"}, + {"snmptranslate", "-Td", "-Ob", "-m", "all", ".1.0.0.0.1.7"}, + {"snmptranslate", "-Td", "-Ob", ".iso.2.3"}, + {"snmptranslate", "-Td", "-Ob", "-m", "all", ".999"}, + {"snmptranslate", "-Td", "-Ob", "TEST::server"}, + {"snmptranslate", "-Td", "-Ob", "TEST::server.0"}, + {"snmptranslate", "-Td", "-Ob", "TEST::testTable"}, + {"snmptranslate", "-Td", "-Ob", "TEST::connections"}, + {"snmptranslate", "-Td", "-Ob", "TEST::latency"}, + {"snmptranslate", "-Td", "-Ob", "TEST::description"}, + {"snmptranslate", "-Td", "-Ob", "TEST::hostname"}, + {"snmptranslate", "-Td", "-Ob", "IF-MIB::ifPhysAddress.1"}, + {"snmptranslate", "-Td", "-Ob", "BRIDGE-MIB::dot1dTpFdbAddress.1"}, + {"snmptranslate", "-Td", "-Ob", "TCP-MIB::tcpConnectionLocalAddress.1"}, + {"snmptranslate", "-Td", "TEST::testTable.1"}, + {"snmptable", "-Ch", "-Cl", "-c", "public", "127.0.0.1", "TEST::testTable"}, +} + +type mockedCommandResult struct { + stdout string + stderr string + exitError bool +} + +func main() { + if err := generate(); err != nil { + fmt.Fprintf(os.Stderr, "error: %s\n", err) + os.Exit(1) + } +} + +func generate() error { + f, err := os.OpenFile("snmp_mocks_test.go", os.O_RDWR, 0644) + if err != nil { + return err + } + br := bufio.NewReader(f) + var i int64 + for l, err := br.ReadString('\n'); err == nil; l, err = br.ReadString('\n') { + i += int64(len(l)) + if l == "// BEGIN GO GENERATE CONTENT\n" { + break + } + } + f.Truncate(i) + f.Seek(i, 0) + + fmt.Fprintf(f, "var mockedCommandResults = map[string]mockedCommandResult{\n") + + for _, cmd := range mockedCommands { + ec := exec.Command(cmd[0], cmd[1:]...) + out := bytes.NewBuffer(nil) + err := bytes.NewBuffer(nil) + ec.Stdout = out + ec.Stderr = err + ec.Env = []string{ + "MIBDIRS=+./testdata", + } + + var mcr mockedCommandResult + if err := ec.Run(); err != nil { + if err, ok := err.(*exec.ExitError); !ok { + mcr.exitError = true + } else { + return fmt.Errorf("executing %v: %s", cmd, err) + } + } + mcr.stdout = string(out.Bytes()) + mcr.stderr = string(err.Bytes()) + cmd0 := strings.Join(cmd, "\000") + mcrv := fmt.Sprintf("%#v", mcr)[5:] // trim `main.` prefix + fmt.Fprintf(f, "%#v: %s,\n", cmd0, mcrv) + } + f.Write([]byte("}\n")) + f.Close() + + return exec.Command("gofmt", "-w", "snmp_mocks_test.go").Run() +} diff --git a/plugins/inputs/snmp/snmp_mocks_test.go b/plugins/inputs/snmp/snmp_mocks_test.go new file mode 100644 index 0000000000000..850f6b83830bc --- /dev/null +++ b/plugins/inputs/snmp/snmp_mocks_test.go @@ -0,0 +1,93 @@ +package snmp + +import ( + "fmt" + "os" + "os/exec" + "strings" + "testing" +) + +type mockedCommandResult struct { + stdout string + stderr string + exitError bool +} + +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 +} + +// This is not a real test. This is just a way of mocking out commands. +// +// Idea based on https://github.com/golang/go/blob/7c31043/src/os/exec/exec_test.go#L568 +func TestMockExecCommand(_ *testing.T) { + var cmd []string + for _, arg := range os.Args { + if arg == "--" { + cmd = []string{} + continue + } + if cmd == nil { + continue + } + cmd = append(cmd, arg) + } + if cmd == nil { + return + } + + cmd0 := strings.Join(cmd, "\000") + mcr, ok := mockedCommandResults[cmd0] + if !ok { + cv := fmt.Sprintf("%#v", cmd)[8:] // trim `[]string` prefix + //nolint:errcheck,revive + fmt.Fprintf(os.Stderr, "Unmocked command. Please add the following to `mockedCommands` in snmp_mocks_generate.go, and then run `go generate`:\n\t%s,\n", cv) + //nolint:revive // error code is important for this "test" + os.Exit(1) + } + //nolint:errcheck,revive + fmt.Printf("%s", mcr.stdout) + //nolint:errcheck,revive + fmt.Fprintf(os.Stderr, "%s", mcr.stderr) + if mcr.exitError { + //nolint:revive // error code is important for this "test" + os.Exit(1) + } + //nolint:revive // error code is important for this "test" + os.Exit(0) +} + +func init() { + execCommand = mockExecCommand +} + +// BEGIN GO GENERATE CONTENT +var mockedCommandResults = map[string]mockedCommandResult{ + "snmptranslate\x00-Td\x00-Ob\x00-m\x00all\x00.1.0.0.0": {stdout: "TEST::testTable\ntestTable OBJECT-TYPE\n -- FROM\tTEST\n MAX-ACCESS\tnot-accessible\n STATUS\tcurrent\n::= { iso(1) 0 testOID(0) 0 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00-m\x00all\x00.1.0.0.1.1": {stdout: "TEST::hostname\nhostname OBJECT-TYPE\n -- FROM\tTEST\n SYNTAX\tOCTET STRING\n MAX-ACCESS\tread-only\n STATUS\tcurrent\n::= { iso(1) 0 testOID(0) 1 1 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00-m\x00all\x00.1.0.0.1.2": {stdout: "TEST::1.2\nanonymous#1 OBJECT-TYPE\n -- FROM\tTEST\n::= { iso(1) 0 testOID(0) 1 2 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00-m\x00all\x001.0.0.1.1": {stdout: "TEST::hostname\nhostname OBJECT-TYPE\n -- FROM\tTEST\n SYNTAX\tOCTET STRING\n MAX-ACCESS\tread-only\n STATUS\tcurrent\n::= { iso(1) 0 testOID(0) 1 1 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00-m\x00all\x00.1.0.0.0.1.1": {stdout: "TEST::server\nserver OBJECT-TYPE\n -- FROM\tTEST\n SYNTAX\tOCTET STRING\n MAX-ACCESS\tread-only\n STATUS\tcurrent\n::= { iso(1) 0 testOID(0) testTable(0) testTableEntry(1) 1 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00-m\x00all\x00.1.0.0.0.1.1.0": {stdout: "TEST::server.0\nserver OBJECT-TYPE\n -- FROM\tTEST\n SYNTAX\tOCTET STRING\n MAX-ACCESS\tread-only\n STATUS\tcurrent\n::= { iso(1) 0 testOID(0) testTable(0) testTableEntry(1) server(1) 0 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00-m\x00all\x00.1.0.0.0.1.5": {stdout: "TEST::testTableEntry.5\ntestTableEntry OBJECT-TYPE\n -- FROM\tTEST\n MAX-ACCESS\tnot-accessible\n STATUS\tcurrent\n INDEX\t\t{ server }\n::= { iso(1) 0 testOID(0) testTable(0) testTableEntry(1) 5 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00-m\x00all\x00.1.2.3": {stdout: "iso.2.3\niso OBJECT-TYPE\n -- FROM\t#-1\n::= { iso(1) 2 3 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00-m\x00all\x00.1.0.0.0.1.7": {stdout: "TEST::testTableEntry.7\ntestTableEntry OBJECT-TYPE\n -- FROM\tTEST\n MAX-ACCESS\tnot-accessible\n STATUS\tcurrent\n INDEX\t\t{ server }\n::= { iso(1) std(0) testOID(0) testTable(0) testTableEntry(1) 7 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00.iso.2.3": {stdout: "iso.2.3\niso OBJECT-TYPE\n -- FROM\t#-1\n::= { iso(1) 2 3 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00-m\x00all\x00.999": {stdout: ".999\n [TRUNCATED]\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00TEST::server": {stdout: "TEST::server\nserver OBJECT-TYPE\n -- FROM\tTEST\n SYNTAX\tOCTET STRING\n MAX-ACCESS\tread-only\n STATUS\tcurrent\n::= { iso(1) 0 testOID(0) testTable(0) testTableEntry(1) 1 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00TEST::server.0": {stdout: "TEST::server.0\nserver OBJECT-TYPE\n -- FROM\tTEST\n SYNTAX\tOCTET STRING\n MAX-ACCESS\tread-only\n STATUS\tcurrent\n::= { iso(1) 0 testOID(0) testTable(0) testTableEntry(1) server(1) 0 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00TEST::testTable": {stdout: "TEST::testTable\ntestTable OBJECT-TYPE\n -- FROM\tTEST\n MAX-ACCESS\tnot-accessible\n STATUS\tcurrent\n::= { iso(1) 0 testOID(0) 0 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00TEST::connections": {stdout: "TEST::connections\nconnections OBJECT-TYPE\n -- FROM\tTEST\n SYNTAX\tINTEGER\n MAX-ACCESS\tread-only\n STATUS\tcurrent\n::= { iso(1) 0 testOID(0) testTable(0) testTableEntry(1) 2 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00TEST::latency": {stdout: "TEST::latency\nlatency OBJECT-TYPE\n -- FROM\tTEST\n SYNTAX\tOCTET STRING\n MAX-ACCESS\tread-only\n STATUS\tcurrent\n::= { iso(1) 0 testOID(0) testTable(0) testTableEntry(1) 3 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00TEST::description": {stdout: "TEST::description\ndescription OBJECT-TYPE\n -- FROM\tTEST\n SYNTAX\tOCTET STRING\n MAX-ACCESS\tread-only\n STATUS\tcurrent\n::= { iso(1) 0 testOID(0) testTable(0) testTableEntry(1) 4 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00TEST::hostname": {stdout: "TEST::hostname\nhostname OBJECT-TYPE\n -- FROM\tTEST\n SYNTAX\tOCTET STRING\n MAX-ACCESS\tread-only\n STATUS\tcurrent\n::= { iso(1) 0 testOID(0) 1 1 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00IF-MIB::ifPhysAddress.1": {stdout: "IF-MIB::ifPhysAddress.1\nifPhysAddress OBJECT-TYPE\n -- FROM\tIF-MIB\n -- TEXTUAL CONVENTION PhysAddress\n SYNTAX\tOCTET STRING\n DISPLAY-HINT\t\"1x:\"\n MAX-ACCESS\tread-only\n STATUS\tcurrent\n DESCRIPTION\t\"The interface's address at its protocol sub-layer. For\n example, for an 802.x interface, this object normally\n contains a MAC address. The interface's media-specific MIB\n must define the bit and byte ordering and the format of the\n value of this object. For interfaces which do not have such\n an address (e.g., a serial line), this object should contain\n an octet string of zero length.\"\n::= { iso(1) org(3) dod(6) internet(1) mgmt(2) mib-2(1) interfaces(2) ifTable(2) ifEntry(1) ifPhysAddress(6) 1 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00BRIDGE-MIB::dot1dTpFdbAddress.1": {stdout: "BRIDGE-MIB::dot1dTpFdbAddress.1\ndot1dTpFdbAddress OBJECT-TYPE\n -- FROM\tBRIDGE-MIB\n -- TEXTUAL CONVENTION MacAddress\n SYNTAX\tOCTET STRING (6) \n DISPLAY-HINT\t\"1x:\"\n MAX-ACCESS\tread-only\n STATUS\tcurrent\n DESCRIPTION\t\"A unicast MAC address for which the bridge has\n forwarding and/or filtering information.\"\n::= { iso(1) org(3) dod(6) internet(1) mgmt(2) mib-2(1) dot1dBridge(17) dot1dTp(4) dot1dTpFdbTable(3) dot1dTpFdbEntry(1) dot1dTpFdbAddress(1) 1 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00-Ob\x00TCP-MIB::tcpConnectionLocalAddress.1": {stdout: "TCP-MIB::tcpConnectionLocalAddress.1\ntcpConnectionLocalAddress OBJECT-TYPE\n -- FROM\tTCP-MIB\n -- TEXTUAL CONVENTION InetAddress\n SYNTAX\tOCTET STRING (0..255) \n MAX-ACCESS\tnot-accessible\n STATUS\tcurrent\n DESCRIPTION\t\"The local IP address for this TCP connection. The type\n of this address is determined by the value of\n tcpConnectionLocalAddressType.\n\n As this object is used in the index for the\n tcpConnectionTable, implementors should be\n careful not to create entries that would result in OIDs\n with more than 128 subidentifiers; otherwise the information\n cannot be accessed by using SNMPv1, SNMPv2c, or SNMPv3.\"\n::= { iso(1) org(3) dod(6) internet(1) mgmt(2) mib-2(1) tcp(6) tcpConnectionTable(19) tcpConnectionEntry(1) tcpConnectionLocalAddress(2) 1 }\n", stderr: "", exitError: false}, + "snmptranslate\x00-Td\x00TEST::testTable.1": {stdout: "TEST::testTableEntry\ntestTableEntry OBJECT-TYPE\n -- FROM\tTEST\n MAX-ACCESS\tnot-accessible\n STATUS\tcurrent\n INDEX\t\t{ server }\n::= { iso(1) 0 testOID(0) testTable(0) 1 }\n", stderr: "", exitError: false}, + "snmptable\x00-Ch\x00-Cl\x00-c\x00public\x00127.0.0.1\x00TEST::testTable": {stdout: "server connections latency description \nTEST::testTable: No entries\n", stderr: "", exitError: false}, +} diff --git a/plugins/inputs/snmp/snmp_test.go b/plugins/inputs/snmp/snmp_test.go index f69b5e52d0519..65e1e73b055c3 100644 --- a/plugins/inputs/snmp/snmp_test.go +++ b/plugins/inputs/snmp/snmp_test.go @@ -1,21 +1,22 @@ +//go:generate go run -tags generate snmp_mocks_generate.go package snmp import ( "fmt" "net" - "path/filepath" + "os/exec" "sync" "testing" "time" "github.com/gosnmp/gosnmp" - "github.com/influxdata/toml" - "github.com/stretchr/testify/require" - "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/internal/snmp" "github.com/influxdata/telegraf/plugins/inputs" "github.com/influxdata/telegraf/testutil" + "github.com/influxdata/toml" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type testSNMPConnection struct { @@ -62,42 +63,33 @@ func (tsc *testSNMPConnection) Walk(oid string, wf gosnmp.WalkFunc) error { var tsc = &testSNMPConnection{ host: "tsc", values: map[string]interface{}{ - ".1.3.6.1.2.1.3.1.1.1.0": "foo", - ".1.3.6.1.2.1.3.1.1.1.1": []byte("bar"), - ".1.3.6.1.2.1.3.1.1.1.2": []byte(""), - ".1.3.6.1.2.1.3.1.1.102": "bad", - ".1.3.6.1.2.1.3.1.1.2.0": 1, - ".1.3.6.1.2.1.3.1.1.2.1": 2, - ".1.3.6.1.2.1.3.1.1.2.2": 0, - ".1.3.6.1.2.1.3.1.1.3.0": "1.3.6.1.2.1.3.1.1.3", - ".1.3.6.1.2.1.3.1.1.5.0": 123456, - ".1.0.0.0.1.1.0": "foo", - ".1.0.0.0.1.1.1": []byte("bar"), - ".1.0.0.0.1.1.2": []byte(""), - ".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.2.2": 0, - ".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": "0.000", - ".1.0.0.0.1.3.3": "9.999", - ".1.0.0.0.1.5.0": 123456, - ".1.0.0.1.1": "baz", - ".1.0.0.1.2": 234, - ".1.0.0.1.3": []byte("byte slice"), - ".1.0.0.2.1.5.0.9.9": 11, - ".1.0.0.2.1.5.1.9.9": 22, - ".1.0.0.0.1.6.0": ".1.0.0.0.1.7", - ".1.0.0.3.1.1.10": "instance", - ".1.0.0.3.1.1.11": "instance2", - ".1.0.0.3.1.1.12": "instance3", - ".1.0.0.3.1.2.10": 10, - ".1.0.0.3.1.2.11": 20, - ".1.0.0.3.1.2.12": 20, - ".1.0.0.3.1.3.10": 1, - ".1.0.0.3.1.3.11": 2, - ".1.0.0.3.1.3.12": 3, + ".1.0.0.0.1.1.0": "foo", + ".1.0.0.0.1.1.1": []byte("bar"), + ".1.0.0.0.1.1.2": []byte(""), + ".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.2.2": 0, + ".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": "0.000", + ".1.0.0.0.1.3.3": "9.999", + ".1.0.0.0.1.5.0": 123456, + ".1.0.0.1.1": "baz", + ".1.0.0.1.2": 234, + ".1.0.0.1.3": []byte("byte slice"), + ".1.0.0.2.1.5.0.9.9": 11, + ".1.0.0.2.1.5.1.9.9": 22, + ".1.0.0.0.1.6.0": ".1.0.0.0.1.7", + ".1.0.0.3.1.1.10": "instance", + ".1.0.0.3.1.1.11": "instance2", + ".1.0.0.3.1.1.12": "instance3", + ".1.0.0.3.1.2.10": 10, + ".1.0.0.3.1.2.11": 20, + ".1.0.0.3.1.2.12": 20, + ".1.0.0.3.1.3.10": 1, + ".1.0.0.3.1.3.11": 2, + ".1.0.0.3.1.3.12": 3, }, } @@ -123,18 +115,6 @@ func TestSampleConfig(t *testing.T) { } func TestFieldInit(t *testing.T) { - testDataPath, err := filepath.Abs("./testdata") - require.NoError(t, err) - s := &Snmp{ - ClientConfig: snmp.ClientConfig{ - Path: []string{testDataPath}, - }, - Log: &testutil.Logger{}, - } - - err = s.Init() - require.NoError(t, err) - translations := []struct { inputOid string inputName string @@ -146,130 +126,131 @@ func TestFieldInit(t *testing.T) { {".1.2.3", "foo", "", ".1.2.3", "foo", ""}, {".iso.2.3", "foo", "", ".1.2.3", "foo", ""}, {".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", ""}, {"IF-MIB::ifPhysAddress.1", "", "", ".1.3.6.1.2.1.2.2.1.6.1", "ifPhysAddress.1", "hwaddr"}, {"IF-MIB::ifPhysAddress.1", "", "none", ".1.3.6.1.2.1.2.2.1.6.1", "ifPhysAddress.1", "none"}, {"BRIDGE-MIB::dot1dTpFdbAddress.1", "", "", ".1.3.6.1.2.1.17.4.3.1.1.1", "dot1dTpFdbAddress.1", "hwaddr"}, {"TCP-MIB::tcpConnectionLocalAddress.1", "", "", ".1.3.6.1.2.1.6.19.1.2.1", "tcpConnectionLocalAddress.1", "ipaddr"}, - {".999", "", "", ".999", ".999", ""}, } + tr := NewNetsnmpTranslator() for _, txl := range translations { f := Field{Oid: txl.inputOid, Name: txl.inputName, Conversion: txl.inputConversion} - err := f.init() - require.NoError(t, err, "inputOid='%s' inputName='%s'", txl.inputOid, txl.inputName) - - require.Equal(t, txl.expectedOid, f.Oid, "inputOid='%s' inputName='%s' inputConversion='%s'", txl.inputOid, txl.inputName, txl.inputConversion) - require.Equal(t, txl.expectedName, f.Name, "inputOid='%s' inputName='%s' inputConversion='%s'", txl.inputOid, txl.inputName, txl.inputConversion) + err := f.init(tr) + 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' inputConversion='%s'", txl.inputOid, txl.inputName, txl.inputConversion) + assert.Equal(t, txl.expectedName, f.Name, "inputOid='%s' inputName='%s' inputConversion='%s'", txl.inputOid, txl.inputName, txl.inputConversion) } } func TestTableInit(t *testing.T) { - testDataPath, err := filepath.Abs("./testdata") - require.NoError(t, err) - - s := &Snmp{ - ClientConfig: snmp.ClientConfig{ - Path: []string{testDataPath}, - }, - Tables: []Table{ - {Oid: ".1.3.6.1.2.1.3.1", - Fields: []Field{ - {Oid: ".999", Name: "foo"}, - {Oid: ".1.3.6.1.2.1.3.1.1.1", Name: "atIfIndex", IsTag: true}, - {Oid: "RFC1213-MIB::atPhysAddress", Name: "atPhysAddress"}, - }}, + tbl := Table{ + Oid: ".1.0.0.0", + Fields: []Field{ + {Oid: ".999", Name: "foo"}, + {Oid: "TEST::description", Name: "description", IsTag: true}, }, } - err = s.Init() + err := tbl.Init(NewNetsnmpTranslator()) require.NoError(t, err) - require.Equal(t, "atTable", s.Tables[0].Name) + assert.Equal(t, "testTable", tbl.Name) - require.Len(t, s.Tables[0].Fields, 5) - require.Contains(t, s.Tables[0].Fields, Field{Oid: ".999", Name: "foo", initialized: true}) - require.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.3.6.1.2.1.3.1.1.1", Name: "atIfIndex", initialized: true, IsTag: true}) - require.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.3.6.1.2.1.3.1.1.2", Name: "atPhysAddress", initialized: true, Conversion: "hwaddr"}) - require.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.3.6.1.2.1.3.1.1.3", Name: "atNetAddress", initialized: true, IsTag: true}) + assert.Len(t, tbl.Fields, 5) + 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}) + assert.Contains(t, tbl.Fields, Field{Oid: ".1.0.0.0.1.4", Name: "description", IsTag: true, initialized: true}) } func TestSnmpInit(t *testing.T) { - testDataPath, err := filepath.Abs("./testdata") - require.NoError(t, err) - s := &Snmp{ Tables: []Table{ - {Oid: "RFC1213-MIB::atTable"}, + {Oid: "TEST::testTable"}, }, Fields: []Field{ - {Oid: "RFC1213-MIB::atPhysAddress"}, + {Oid: "TEST::hostname"}, }, ClientConfig: snmp.ClientConfig{ - Path: []string{testDataPath}, + Translator: "netsnmp", }, } - err = s.Init() + err := s.Init() require.NoError(t, err) - require.Len(t, s.Tables[0].Fields, 3) - require.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.3.6.1.2.1.3.1.1.1", Name: "atIfIndex", IsTag: true, initialized: true}) - require.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.3.6.1.2.1.3.1.1.2", Name: "atPhysAddress", initialized: true, Conversion: "hwaddr"}) - require.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.3.6.1.2.1.3.1.1.3", Name: "atNetAddress", IsTag: true, initialized: true}) + assert.Len(t, s.Tables[0].Fields, 4) + 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.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.0.0.0.1.4", Name: "description", initialized: true}) - require.Equal(t, Field{ - Oid: ".1.3.6.1.2.1.3.1.1.2", - Name: "atPhysAddress", - Conversion: "hwaddr", + assert.Equal(t, Field{ + Oid: ".1.0.0.1.1", + Name: "hostname", initialized: true, }, s.Fields[0]) } func TestSnmpInit_noTranslate(t *testing.T) { + // override execCommand so it returns exec.ErrNotFound + defer func(ec func(string, ...string) *exec.Cmd) { execCommand = ec }(execCommand) + execCommand = func(_ string, _ ...string) *exec.Cmd { + return exec.Command("snmptranslateExecErrNotFound") + } + s := &Snmp{ Fields: []Field{ - {Oid: ".9.1.1.1.1", Name: "one", IsTag: true}, - {Oid: ".9.1.1.1.2", Name: "two"}, - {Oid: ".9.1.1.1.3"}, + {Oid: ".1.1.1.1", Name: "one", IsTag: true}, + {Oid: ".1.1.1.2", Name: "two"}, + {Oid: ".1.1.1.3"}, }, Tables: []Table{ {Name: "testing", Fields: []Field{ - {Oid: ".9.1.1.1.4", Name: "four", IsTag: true}, - {Oid: ".9.1.1.1.5", Name: "five"}, - {Oid: ".9.1.1.1.6"}, + {Oid: ".1.1.1.4", Name: "four", IsTag: true}, + {Oid: ".1.1.1.5", Name: "five"}, + {Oid: ".1.1.1.6"}, }}, }, ClientConfig: snmp.ClientConfig{ - Path: []string{}, + Translator: "netsnmp", }, } err := s.Init() require.NoError(t, err) - require.Equal(t, ".9.1.1.1.1", s.Fields[0].Oid) - require.Equal(t, "one", s.Fields[0].Name) - require.Equal(t, true, s.Fields[0].IsTag) + assert.Equal(t, ".1.1.1.1", s.Fields[0].Oid) + assert.Equal(t, "one", s.Fields[0].Name) + assert.Equal(t, true, s.Fields[0].IsTag) - require.Equal(t, ".9.1.1.1.2", s.Fields[1].Oid) - require.Equal(t, "two", s.Fields[1].Name) - require.Equal(t, false, s.Fields[1].IsTag) + assert.Equal(t, ".1.1.1.2", s.Fields[1].Oid) + assert.Equal(t, "two", s.Fields[1].Name) + assert.Equal(t, false, s.Fields[1].IsTag) - require.Equal(t, ".9.1.1.1.3", s.Fields[2].Oid) - require.Equal(t, ".9.1.1.1.3", s.Fields[2].Name) - require.Equal(t, false, s.Fields[2].IsTag) + assert.Equal(t, ".1.1.1.3", s.Fields[2].Oid) + assert.Equal(t, ".1.1.1.3", s.Fields[2].Name) + assert.Equal(t, false, s.Fields[2].IsTag) - require.Equal(t, ".9.1.1.1.4", s.Tables[0].Fields[0].Oid) - require.Equal(t, "four", s.Tables[0].Fields[0].Name) - require.Equal(t, true, s.Tables[0].Fields[0].IsTag) + assert.Equal(t, ".1.1.1.4", s.Tables[0].Fields[0].Oid) + assert.Equal(t, "four", s.Tables[0].Fields[0].Name) + assert.Equal(t, true, s.Tables[0].Fields[0].IsTag) - require.Equal(t, ".9.1.1.1.5", s.Tables[0].Fields[1].Oid) - require.Equal(t, "five", s.Tables[0].Fields[1].Name) - require.Equal(t, false, s.Tables[0].Fields[1].IsTag) + assert.Equal(t, ".1.1.1.5", s.Tables[0].Fields[1].Oid) + assert.Equal(t, "five", s.Tables[0].Fields[1].Name) + assert.Equal(t, false, s.Tables[0].Fields[1].IsTag) - require.Equal(t, ".9.1.1.1.6", s.Tables[0].Fields[2].Oid) - require.Equal(t, ".9.1.1.1.6", s.Tables[0].Fields[2].Name) - require.Equal(t, false, s.Tables[0].Fields[2].IsTag) + assert.Equal(t, ".1.1.1.6", s.Tables[0].Fields[2].Oid) + assert.Equal(t, ".1.1.1.6", s.Tables[0].Fields[2].Name) + assert.Equal(t, false, s.Tables[0].Fields[2].IsTag) } func TestSnmpInit_noName_noOid(t *testing.T) { @@ -288,17 +269,14 @@ func TestSnmpInit_noName_noOid(t *testing.T) { } func TestGetSNMPConnection_v2(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - s := &Snmp{ Agents: []string{"1.2.3.4:567", "1.2.3.4", "udp://127.0.0.1"}, ClientConfig: snmp.ClientConfig{ - Timeout: config.Duration(3 * time.Second), - Retries: 4, - Version: 2, - Community: "foo", + Timeout: config.Duration(3 * time.Second), + Retries: 4, + Version: 2, + Community: "foo", + Translator: "netsnmp", }, } err := s.Init() @@ -307,25 +285,25 @@ func TestGetSNMPConnection_v2(t *testing.T) { gsc, err := s.getConnection(0) require.NoError(t, err) gs := gsc.(snmp.GosnmpWrapper) - require.Equal(t, "1.2.3.4", gs.Target) - require.EqualValues(t, 567, gs.Port) - require.Equal(t, gosnmp.Version2c, gs.Version) - require.Equal(t, "foo", gs.Community) - require.Equal(t, "udp", gs.Transport) + 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) + assert.Equal(t, "udp", gs.Transport) gsc, err = s.getConnection(1) require.NoError(t, err) gs = gsc.(snmp.GosnmpWrapper) - require.Equal(t, "1.2.3.4", gs.Target) - require.EqualValues(t, 161, gs.Port) - require.Equal(t, "udp", gs.Transport) + assert.Equal(t, "1.2.3.4", gs.Target) + assert.EqualValues(t, 161, gs.Port) + assert.Equal(t, "udp", gs.Transport) gsc, err = s.getConnection(2) require.NoError(t, err) gs = gsc.(snmp.GosnmpWrapper) - require.Equal(t, "127.0.0.1", gs.Target) - require.EqualValues(t, 161, gs.Port) - require.Equal(t, "udp", gs.Transport) + assert.Equal(t, "127.0.0.1", gs.Target) + assert.EqualValues(t, 161, gs.Port) + assert.Equal(t, "udp", gs.Transport) } func TestGetSNMPConnectionTCP(t *testing.T) { @@ -336,6 +314,9 @@ func TestGetSNMPConnectionTCP(t *testing.T) { s := &Snmp{ Agents: []string{"tcp://127.0.0.1:56789"}, + ClientConfig: snmp.ClientConfig{ + Translator: "netsnmp", + }, } err := s.Init() require.NoError(t, err) @@ -344,9 +325,9 @@ func TestGetSNMPConnectionTCP(t *testing.T) { gsc, err := s.getConnection(0) require.NoError(t, err) gs := gsc.(snmp.GosnmpWrapper) - require.Equal(t, "127.0.0.1", gs.Target) - require.EqualValues(t, 56789, gs.Port) - require.Equal(t, "tcp", gs.Transport) + assert.Equal(t, "127.0.0.1", gs.Target) + assert.EqualValues(t, 56789, gs.Port) + assert.Equal(t, "tcp", gs.Transport) wg.Wait() } @@ -361,10 +342,6 @@ func stubTCPServer(wg *sync.WaitGroup) { } func TestGetSNMPConnection_v3(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - s := &Snmp{ Agents: []string{"1.2.3.4"}, ClientConfig: snmp.ClientConfig{ @@ -380,6 +357,7 @@ func TestGetSNMPConnection_v3(t *testing.T) { EngineID: "myengineid", EngineBoots: 1, EngineTime: 2, + Translator: "netsnmp", }, } err := s.Init() @@ -388,27 +366,23 @@ func TestGetSNMPConnection_v3(t *testing.T) { gsc, err := s.getConnection(0) require.NoError(t, err) gs := gsc.(snmp.GosnmpWrapper) - require.Equal(t, gs.Version, gosnmp.Version3) + assert.Equal(t, gs.Version, gosnmp.Version3) sp := gs.SecurityParameters.(*gosnmp.UsmSecurityParameters) - require.Equal(t, "1.2.3.4", gsc.Host()) - require.EqualValues(t, 20, gs.MaxRepetitions) - require.Equal(t, "mycontext", gs.ContextName) - require.Equal(t, gosnmp.AuthPriv, gs.MsgFlags&gosnmp.AuthPriv) - require.Equal(t, "myuser", sp.UserName) - require.Equal(t, gosnmp.MD5, sp.AuthenticationProtocol) - require.Equal(t, "password123", sp.AuthenticationPassphrase) - require.Equal(t, gosnmp.DES, sp.PrivacyProtocol) - require.Equal(t, "321drowssap", sp.PrivacyPassphrase) - require.Equal(t, "myengineid", sp.AuthoritativeEngineID) - require.EqualValues(t, 1, sp.AuthoritativeEngineBoots) - require.EqualValues(t, 2, sp.AuthoritativeEngineTime) + assert.Equal(t, "1.2.3.4", gsc.Host()) + assert.EqualValues(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_v3_blumenthal(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - testCases := []struct { Name string Algorithm gosnmp.SnmpV3PrivProtocol @@ -432,6 +406,7 @@ func TestGetSNMPConnection_v3_blumenthal(t *testing.T) { EngineID: "myengineid", EngineBoots: 1, EngineTime: 2, + Translator: "netsnmp", }, }, }, @@ -453,6 +428,7 @@ func TestGetSNMPConnection_v3_blumenthal(t *testing.T) { EngineID: "myengineid", EngineBoots: 1, EngineTime: 2, + Translator: "netsnmp", }, }, }, @@ -474,6 +450,7 @@ func TestGetSNMPConnection_v3_blumenthal(t *testing.T) { EngineID: "myengineid", EngineBoots: 1, EngineTime: 2, + Translator: "netsnmp", }, }, }, @@ -495,6 +472,7 @@ func TestGetSNMPConnection_v3_blumenthal(t *testing.T) { EngineID: "myengineid", EngineBoots: 1, EngineTime: 2, + Translator: "netsnmp", }, }, }, @@ -509,31 +487,30 @@ func TestGetSNMPConnection_v3_blumenthal(t *testing.T) { gsc, err := s.getConnection(0) require.NoError(t, err) gs := gsc.(snmp.GosnmpWrapper) - require.Equal(t, gs.Version, gosnmp.Version3) + assert.Equal(t, gs.Version, gosnmp.Version3) sp := gs.SecurityParameters.(*gosnmp.UsmSecurityParameters) - require.Equal(t, "1.2.3.4", gsc.Host()) - require.EqualValues(t, 20, gs.MaxRepetitions) - require.Equal(t, "mycontext", gs.ContextName) - require.Equal(t, gosnmp.AuthPriv, gs.MsgFlags&gosnmp.AuthPriv) - require.Equal(t, "myuser", sp.UserName) - require.Equal(t, gosnmp.MD5, sp.AuthenticationProtocol) - require.Equal(t, "password123", sp.AuthenticationPassphrase) - require.Equal(t, tc.Algorithm, sp.PrivacyProtocol) - require.Equal(t, "password123", sp.PrivacyPassphrase) - require.Equal(t, "myengineid", sp.AuthoritativeEngineID) - require.EqualValues(t, 1, sp.AuthoritativeEngineBoots) - require.EqualValues(t, 2, sp.AuthoritativeEngineTime) + assert.Equal(t, "1.2.3.4", gsc.Host()) + assert.EqualValues(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, tc.Algorithm, sp.PrivacyProtocol) + assert.Equal(t, "password123", 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) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - s := &Snmp{ Agents: []string{"1.2.3.4", "1.2.3.5", "1.2.3.5"}, + ClientConfig: snmp.ClientConfig{ + Translator: "netsnmp", + }, } err := s.Init() require.NoError(t, err) @@ -545,9 +522,9 @@ func TestGetSNMPConnection_caching(t *testing.T) { require.NoError(t, err) gs4, err := s.getConnection(2) require.NoError(t, err) - require.Equal(t, gs1, gs2) - require.NotEqual(t, gs2, gs3) - require.NotEqual(t, gs3, gs4) + assert.Equal(t, gs1, gs2) + assert.NotEqual(t, gs2, gs3) + assert.NotEqual(t, gs3, gs4) } func TestGosnmpWrapper_walk_retry(t *testing.T) { @@ -599,9 +576,9 @@ func TestGosnmpWrapper_walk_retry(t *testing.T) { err = gsw.Walk(".1.0.0", func(_ gosnmp.SnmpPDU) error { return nil }) require.NoError(t, srvr.Close()) wg.Wait() - require.Error(t, err) - require.NotEqual(t, gs.Conn, conn) - require.Equal(t, (gs.Retries+1)*2, reqCount) + assert.Error(t, err) + assert.NotEqual(t, gs.Conn, conn) + assert.Equal(t, (gs.Retries+1)*2, reqCount) } func TestGosnmpWrapper_get_retry(t *testing.T) { @@ -652,12 +629,12 @@ func TestGosnmpWrapper_get_retry(t *testing.T) { _, err = gsw.Get([]string{".1.0.0"}) require.NoError(t, srvr.Close()) wg.Wait() - require.Error(t, err) - require.NotEqual(t, gs.Conn, conn) - require.Equal(t, (gs.Retries+1)*2, reqCount) + assert.Error(t, err) + assert.NotEqual(t, gs.Conn, conn) + assert.Equal(t, (gs.Retries+1)*2, reqCount) } -func TestTableBuild_walk_noTranslate(t *testing.T) { +func TestTableBuild_walk(t *testing.T) { tbl := Table{ Name: "mytable", IndexAsTag: true, @@ -686,12 +663,23 @@ func TestTableBuild_walk_noTranslate(t *testing.T) { Oid: ".1.0.0.2.1.5", OidIndexLength: 1, }, + { + Name: "myfield6", + Oid: ".1.0.0.0.1.6", + Translate: true, + }, + { + Name: "myfield7", + Oid: ".1.0.0.0.1.6", + Translate: false, + }, }, } - tb, err := tbl.Build(tsc, true) + tb, err := tbl.Build(tsc, true, NewNetsnmpTranslator()) require.NoError(t, err) - require.Equal(t, tb.Name, "mytable") + + assert.Equal(t, tb.Name, "mytable") rtr1 := RTableRow{ Tags: map[string]string{ "myfield1": "foo", @@ -702,6 +690,8 @@ func TestTableBuild_walk_noTranslate(t *testing.T) { "myfield3": float64(0.123), "myfield4": 11, "myfield5": 11, + "myfield6": "testTableEntry.7", + "myfield7": ".1.0.0.0.1.7", }, } rtr2 := RTableRow{ @@ -733,85 +723,11 @@ func TestTableBuild_walk_noTranslate(t *testing.T) { "myfield3": float64(9.999), }, } - require.Len(t, tb.Rows, 4) - require.Contains(t, tb.Rows, rtr1) - require.Contains(t, tb.Rows, rtr2) - require.Contains(t, tb.Rows, rtr3) - require.Contains(t, tb.Rows, rtr4) -} - -func TestTableBuild_walk_Translate(t *testing.T) { - testDataPath, err := filepath.Abs("./testdata") - require.NoError(t, err) - s := &Snmp{ - ClientConfig: snmp.ClientConfig{ - Path: []string{testDataPath}, - }, - } - err = s.Init() - require.NoError(t, err) - - tbl := Table{ - Name: "atTable", - IndexAsTag: true, - Fields: []Field{ - { - Name: "ifIndex", - Oid: "1.3.6.1.2.1.3.1.1.1", - IsTag: true, - }, - { - Name: "atPhysAddress", - Oid: "1.3.6.1.2.1.3.1.1.2", - Translate: false, - }, - { - Name: "atNetAddress", - Oid: "1.3.6.1.2.1.3.1.1.3", - Translate: true, - }, - }, - } - - err = tbl.Init() - require.NoError(t, err) - tb, err := tbl.Build(tsc, true) - require.NoError(t, err) - - require.Equal(t, tb.Name, "atTable") - - rtr1 := RTableRow{ - Tags: map[string]string{ - "ifIndex": "foo", - "index": "0", - }, - Fields: map[string]interface{}{ - "atPhysAddress": 1, - "atNetAddress": "atNetAddress", - }, - } - rtr2 := RTableRow{ - Tags: map[string]string{ - "ifIndex": "bar", - "index": "1", - }, - Fields: map[string]interface{}{ - "atPhysAddress": 2, - }, - } - rtr3 := RTableRow{ - Tags: map[string]string{ - "index": "2", - }, - Fields: map[string]interface{}{ - "atPhysAddress": 0, - }, - } - - require.Len(t, tb.Rows, 3) - require.Contains(t, tb.Rows, rtr1) - require.Contains(t, tb.Rows, rtr2) - require.Contains(t, tb.Rows, rtr3) + assert.Len(t, tb.Rows, 4) + assert.Contains(t, tb.Rows, rtr1) + assert.Contains(t, tb.Rows, rtr2) + assert.Contains(t, tb.Rows, rtr3) + assert.Contains(t, tb.Rows, rtr4) } func TestTableBuild_noWalk(t *testing.T) { @@ -843,15 +759,15 @@ func TestTableBuild_noWalk(t *testing.T) { }, } - tb, err := tbl.Build(tsc, false) + tb, err := tbl.Build(tsc, false, NewNetsnmpTranslator()) require.NoError(t, err) rtr := RTableRow{ Tags: map[string]string{"myfield1": "baz", "myfield3": "234"}, Fields: map[string]interface{}{"myfield2": 234}, } - require.Len(t, tb.Rows, 1) - require.Contains(t, tb.Rows, rtr) + assert.Len(t, tb.Rows, 1) + assert.Contains(t, tb.Rows, rtr) } func TestGather(t *testing.T) { @@ -899,21 +815,21 @@ func TestGather(t *testing.T) { require.Len(t, acc.Metrics, 2) m := acc.Metrics[0] - require.Equal(t, "mytable", m.Measurement) - require.Equal(t, "tsc", m.Tags[s.AgentHostTag]) - require.Equal(t, "baz", m.Tags["myfield1"]) - require.Len(t, m.Fields, 2) - require.Equal(t, 234, m.Fields["myfield2"]) - require.Equal(t, "baz", m.Fields["myfield3"]) - require.False(t, tstart.After(m.Time)) - require.False(t, tstop.Before(m.Time)) + assert.Equal(t, "mytable", m.Measurement) + assert.Equal(t, "tsc", m.Tags[s.AgentHostTag]) + 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.After(m.Time)) + assert.True(t, !tstop.Before(m.Time)) m2 := acc.Metrics[1] - require.Equal(t, "myOtherTable", m2.Measurement) - require.Equal(t, "tsc", m2.Tags[s.AgentHostTag]) - require.Equal(t, "baz", m2.Tags["myfield1"]) - require.Len(t, m2.Fields, 1) - require.Equal(t, 123456, m2.Fields["myOtherField"]) + assert.Equal(t, "myOtherTable", m2.Measurement) + assert.Equal(t, "tsc", m2.Tags[s.AgentHostTag]) + 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) { @@ -943,7 +859,7 @@ func TestGather_host(t *testing.T) { require.Len(t, acc.Metrics, 1) m := acc.Metrics[0] - require.Equal(t, "baz", m.Tags["host"]) + assert.Equal(t, "baz", m.Tags["host"]) } func TestFieldConvert(t *testing.T) { @@ -976,7 +892,7 @@ func TestFieldConvert(t *testing.T) { {[]byte("123123123123"), "int", int64(123123123123)}, {float32(12.3), "int", int64(12)}, {float64(12.3), "int", int64(12)}, - {123, "int", int64(123)}, + {int(123), "int", int64(123)}, {int8(123), "int", int64(123)}, {int16(123), "int", int64(123)}, {int32(123), "int", int64(123)}, @@ -1001,23 +917,25 @@ func TestFieldConvert(t *testing.T) { for _, tc := range testTable { act, err := fieldConvert(tc.conv, tc.input) - require.NoError(t, err, "input=%T(%v) conv=%s expected=%T(%v)", tc.input, tc.input, tc.conv, tc.expected, tc.expected) - require.EqualValues(t, tc.expected, act, "input=%T(%v) conv=%s expected=%T(%v)", tc.input, tc.input, tc.conv, tc.expected, tc.expected) + if !assert.NoError(t, err, "input=%T(%v) conv=%s expected=%T(%v)", tc.input, tc.input, tc.conv, tc.expected, tc.expected) { + continue + } + 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 TestSnmpTranslateCache_miss(t *testing.T) { snmpTranslateCaches = nil oid := "IF-MIB::ifPhysAddress.1" - mibName, oidNum, oidText, conversion, _, err := SnmpTranslate(oid) - require.Len(t, snmpTranslateCaches, 1) + mibName, oidNum, oidText, conversion, err := NewNetsnmpTranslator().SnmpTranslate(oid) + assert.Len(t, snmpTranslateCaches, 1) stc := snmpTranslateCaches[oid] require.NotNil(t, stc) - require.Equal(t, mibName, stc.mibName) - require.Equal(t, oidNum, stc.oidNum) - require.Equal(t, oidText, stc.oidText) - require.Equal(t, conversion, stc.conversion) - require.Equal(t, err, stc.err) + assert.Equal(t, mibName, stc.mibName) + assert.Equal(t, oidNum, stc.oidNum) + assert.Equal(t, oidText, stc.oidText) + assert.Equal(t, conversion, stc.conversion) + assert.Equal(t, err, stc.err) } func TestSnmpTranslateCache_hit(t *testing.T) { @@ -1030,27 +948,27 @@ func TestSnmpTranslateCache_hit(t *testing.T) { err: fmt.Errorf("e"), }, } - mibName, oidNum, oidText, conversion, _, err := SnmpTranslate("foo") - require.Equal(t, "a", mibName) - require.Equal(t, "b", oidNum) - require.Equal(t, "c", oidText) - require.Equal(t, "d", conversion) - require.Equal(t, fmt.Errorf("e"), err) + mibName, oidNum, oidText, conversion, err := NewNetsnmpTranslator().SnmpTranslate("foo") + assert.Equal(t, "a", mibName) + assert.Equal(t, "b", oidNum) + assert.Equal(t, "c", oidText) + assert.Equal(t, "d", conversion) + assert.Equal(t, fmt.Errorf("e"), err) snmpTranslateCaches = nil } func TestSnmpTableCache_miss(t *testing.T) { snmpTableCaches = nil oid := ".1.0.0.0" - mibName, oidNum, oidText, fields, err := snmpTable(oid) - require.Len(t, snmpTableCaches, 1) + mibName, oidNum, oidText, fields, err := NewNetsnmpTranslator().SnmpTable(oid) + assert.Len(t, snmpTableCaches, 1) stc := snmpTableCaches[oid] require.NotNil(t, stc) - require.Equal(t, mibName, stc.mibName) - require.Equal(t, oidNum, stc.oidNum) - require.Equal(t, oidText, stc.oidText) - require.Equal(t, fields, stc.fields) - require.Equal(t, err, stc.err) + assert.Equal(t, mibName, stc.mibName) + assert.Equal(t, oidNum, stc.oidNum) + assert.Equal(t, oidText, stc.oidText) + assert.Equal(t, fields, stc.fields) + assert.Equal(t, err, stc.err) } func TestSnmpTableCache_hit(t *testing.T) { @@ -1063,12 +981,12 @@ func TestSnmpTableCache_hit(t *testing.T) { err: fmt.Errorf("e"), }, } - mibName, oidNum, oidText, fields, err := snmpTable("foo") - require.Equal(t, "a", mibName) - require.Equal(t, "b", oidNum) - require.Equal(t, "c", oidText) - require.Equal(t, []Field{{Name: "d"}}, fields) - require.Equal(t, fmt.Errorf("e"), err) + mibName, oidNum, oidText, fields, err := NewNetsnmpTranslator().SnmpTable("foo") + assert.Equal(t, "a", mibName) + assert.Equal(t, "b", oidNum) + assert.Equal(t, "c", oidText) + assert.Equal(t, []Field{{Name: "d"}}, fields) + assert.Equal(t, fmt.Errorf("e"), err) } func TestTableJoin_walk(t *testing.T) { @@ -1104,10 +1022,10 @@ func TestTableJoin_walk(t *testing.T) { }, } - tb, err := tbl.Build(tsc, true) + tb, err := tbl.Build(tsc, true, NewNetsnmpTranslator()) require.NoError(t, err) - require.Equal(t, tb.Name, "mytable") + assert.Equal(t, tb.Name, "mytable") rtr1 := RTableRow{ Tags: map[string]string{ "myfield1": "instance", @@ -1141,10 +1059,10 @@ func TestTableJoin_walk(t *testing.T) { "myfield3": 3, }, } - require.Len(t, tb.Rows, 3) - require.Contains(t, tb.Rows, rtr1) - require.Contains(t, tb.Rows, rtr2) - require.Contains(t, tb.Rows, rtr3) + assert.Len(t, tb.Rows, 3) + assert.Contains(t, tb.Rows, rtr1) + assert.Contains(t, tb.Rows, rtr2) + assert.Contains(t, tb.Rows, rtr3) } func TestTableOuterJoin_walk(t *testing.T) { @@ -1181,10 +1099,10 @@ func TestTableOuterJoin_walk(t *testing.T) { }, } - tb, err := tbl.Build(tsc, true) + tb, err := tbl.Build(tsc, true, NewNetsnmpTranslator()) require.NoError(t, err) - require.Equal(t, tb.Name, "mytable") + assert.Equal(t, tb.Name, "mytable") rtr1 := RTableRow{ Tags: map[string]string{ "myfield1": "instance", @@ -1227,11 +1145,11 @@ func TestTableOuterJoin_walk(t *testing.T) { "myfield5": 1, }, } - require.Len(t, tb.Rows, 4) - require.Contains(t, tb.Rows, rtr1) - require.Contains(t, tb.Rows, rtr2) - require.Contains(t, tb.Rows, rtr3) - require.Contains(t, tb.Rows, rtr4) + assert.Len(t, tb.Rows, 4) + assert.Contains(t, tb.Rows, rtr1) + assert.Contains(t, tb.Rows, rtr2) + assert.Contains(t, tb.Rows, rtr3) + assert.Contains(t, tb.Rows, rtr4) } func TestTableJoinNoIndexAsTag_walk(t *testing.T) { @@ -1267,10 +1185,10 @@ func TestTableJoinNoIndexAsTag_walk(t *testing.T) { }, } - tb, err := tbl.Build(tsc, true) + tb, err := tbl.Build(tsc, true, NewNetsnmpTranslator()) require.NoError(t, err) - require.Equal(t, tb.Name, "mytable") + assert.Equal(t, tb.Name, "mytable") rtr1 := RTableRow{ Tags: map[string]string{ "myfield1": "instance", @@ -1304,35 +1222,8 @@ func TestTableJoinNoIndexAsTag_walk(t *testing.T) { "myfield3": 3, }, } - require.Len(t, tb.Rows, 3) - require.Contains(t, tb.Rows, rtr1) - require.Contains(t, tb.Rows, rtr2) - require.Contains(t, tb.Rows, rtr3) -} - -func BenchmarkMibLoading(b *testing.B) { - log := testutil.Logger{} - path := []string{"testdata"} - for i := 0; i < b.N; i++ { - err := snmp.LoadMibsFromPath(path, log, &snmp.GosmiMibLoader{}) - require.NoError(b, err) - } -} - -func TestCanNotParse(t *testing.T) { - s := &Snmp{ - Fields: []Field{ - {Oid: "RFC1213-MIB::"}, - }, - } - - err := s.Init() - require.Error(t, err) -} - -func TestMissingMibPath(t *testing.T) { - log := testutil.Logger{} - path := []string{"non-existing-directory"} - err := snmp.LoadMibsFromPath(path, log, &snmp.GosmiMibLoader{}) - require.NoError(t, err) + assert.Len(t, tb.Rows, 3) + assert.Contains(t, tb.Rows, rtr1) + assert.Contains(t, tb.Rows, rtr2) + assert.Contains(t, tb.Rows, rtr3) } diff --git a/plugins/inputs/snmp_trap/README.md b/plugins/inputs/snmp_trap/README.md index 04334fa7df847..4a68812e93f0c 100644 --- a/plugins/inputs/snmp_trap/README.md +++ b/plugins/inputs/snmp_trap/README.md @@ -25,6 +25,8 @@ path onto the global path variable # service_address = "udp://:162" ## ## Path to mib files + ## Used by the gosmi translator. + ## To add paths when translating with netsnmp, use the MIBDIRS environment variable # path = ["/usr/share/snmp/mibs"] ## ## Deprecated in 1.20.0; no longer running snmptranslate diff --git a/plugins/inputs/snmp_trap/gosmi.go b/plugins/inputs/snmp_trap/gosmi.go new file mode 100644 index 0000000000000..7acc8201ef866 --- /dev/null +++ b/plugins/inputs/snmp_trap/gosmi.go @@ -0,0 +1,21 @@ +package snmp_trap + +import ( + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/internal/snmp" +) + +type gosmiTranslator struct { +} + +func (t *gosmiTranslator) lookup(oid string) (snmp.MibEntry, error) { + return snmp.TrapLookup(oid) +} + +func newGosmiTranslator(paths []string, log telegraf.Logger) (*gosmiTranslator, error) { + err := snmp.LoadMibsFromPath(paths, log, &snmp.GosmiMibLoader{}) + if err == nil { + return &gosmiTranslator{}, nil + } + return nil, err +} diff --git a/plugins/inputs/snmp_trap/netsnmp.go b/plugins/inputs/snmp_trap/netsnmp.go new file mode 100644 index 0000000000000..25a5ba3e0a3c3 --- /dev/null +++ b/plugins/inputs/snmp_trap/netsnmp.go @@ -0,0 +1,89 @@ +package snmp_trap + +import ( + "bufio" + "bytes" + "fmt" + "os/exec" + "strings" + "sync" + "time" + + "github.com/influxdata/telegraf/config" + "github.com/influxdata/telegraf/internal" + "github.com/influxdata/telegraf/internal/snmp" +) + +type execer func(config.Duration, string, ...string) ([]byte, error) + +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 { + return nil, err + } + return out.Bytes(), nil +} + +type netsnmpTranslator struct { + // Each translator has its own cache and each plugin instance has + // its own translator. This is different than the snmp plugin + // which has one global cache. + // + // We may want to change snmp_trap to + // have a global cache although it's not as important for + // snmp_trap to be global because there is usually only one + // instance, while it's common to configure many snmp instances. + cacheLock sync.Mutex + cache map[string]snmp.MibEntry + execCmd execer + Timeout config.Duration +} + +func (s *netsnmpTranslator) lookup(oid string) (e snmp.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 *netsnmpTranslator) snmptranslate(oid string) (e snmp.MibEntry, err error) { + var out []byte + out, err = s.execCmd(s.Timeout, "snmptranslate", "-Td", "-Ob", "-m", "all", oid) + + if err != nil { + return e, err + } + + scanner := bufio.NewScanner(bytes.NewBuffer(out)) + 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 { + return e, fmt.Errorf("not found") + } + e.MibName = e.OidText[:i] + e.OidText = e.OidText[i+2:] + return e, nil +} + +func newNetsnmpTranslator() *netsnmpTranslator { + return &netsnmpTranslator{ + execCmd: realExecCmd, + } +} diff --git a/plugins/inputs/snmp_trap/snmp_trap.go b/plugins/inputs/snmp_trap/snmp_trap.go index 77c3388d3e53e..a197f757c4f71 100644 --- a/plugins/inputs/snmp_trap/snmp_trap.go +++ b/plugins/inputs/snmp_trap/snmp_trap.go @@ -15,10 +15,15 @@ import ( "github.com/gosnmp/gosnmp" ) +type translator interface { + lookup(oid string) (snmp.MibEntry, error) +} + type SnmpTrap struct { ServiceAddress string `toml:"service_address"` Timeout config.Duration `toml:"timeout" deprecated:"1.20.0;unused option"` Version string `toml:"version"` + Translator string `toml:"-"` Path []string `toml:"path"` // Settings for version 3 @@ -32,15 +37,16 @@ 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) (snmp.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:"-"` + + translator translator //nolint:revive } var sampleConfig = ` @@ -90,7 +96,6 @@ func init() { inputs.Add("snmp_trap", func() telegraf.Input { return &SnmpTrap{ timeFunc: time.Now, - lookupFunc: snmp.TrapLookup, ServiceAddress: "udp://:162", Path: []string{"/usr/share/snmp/mibs"}, Version: "2c", @@ -98,8 +103,24 @@ func init() { }) } +func (s *SnmpTrap) SetTranslator(name string) { + s.Translator = name +} + func (s *SnmpTrap) Init() error { - err := snmp.LoadMibsFromPath(s.Path, s.Log, &snmp.GosmiMibLoader{}) + var err error + switch s.Translator { + case "gosmi": + s.translator, err = newGosmiTranslator(s.Path, s.Log) + if err != nil { + return err + } + case "netsnmp": + s.translator = newNetsnmpTranslator() + default: + return fmt.Errorf("invalid translator value") + } + if err != nil { s.Log.Errorf("Could not get path %v", err) } @@ -259,7 +280,7 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc { } if trapOid != "" { - e, err := s.lookupFunc(trapOid) + e, err := s.translator.lookup(trapOid) if err != nil { s.Log.Errorf("Error resolving V1 OID, oid=%s, source=%s: %v", trapOid, tags["source"], err) return @@ -297,7 +318,7 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc { var e snmp.MibEntry var err error - e, err = s.lookupFunc(val) + e, err = s.translator.lookup(val) if nil != err { s.Log.Errorf("Error resolving value OID, oid=%s, source=%s: %v", val, tags["source"], err) return @@ -315,7 +336,7 @@ func makeTrapHandler(s *SnmpTrap) gosnmp.TrapHandlerFunc { value = v.Value } - e, err := s.lookupFunc(v.Name) + e, err := s.translator.lookup(v.Name) if nil != err { s.Log.Errorf("Error resolving OID oid=%s, source=%s: %v", v.Name, tags["source"], err) return diff --git a/plugins/inputs/snmp_trap/snmp_trap_test.go b/plugins/inputs/snmp_trap/snmp_trap_test.go index 804e5a34c1ff3..a3a16f0de97bb 100644 --- a/plugins/inputs/snmp_trap/snmp_trap_test.go +++ b/plugins/inputs/snmp_trap/snmp_trap_test.go @@ -16,6 +16,28 @@ import ( "github.com/influxdata/telegraf/testutil" ) +type entry struct { + oid string + e snmp.MibEntry +} + +type testTranslator struct { + entries []entry +} + +func (t *testTranslator) lookup(input string) (snmp.MibEntry, error) { + for _, entry := range t.entries { + if input == entry.oid { + return snmp.MibEntry{MibName: entry.e.MibName, OidText: entry.e.OidText}, nil + } + } + return snmp.MibEntry{}, fmt.Errorf("unexpected oid") +} + +func newTestTranslator(entries []entry) *testTranslator { + return &testTranslator{entries: entries} +} + func newMsgFlagsV3(secLevel string) gosnmp.SnmpV3MsgFlags { var msgFlags gosnmp.SnmpV3MsgFlags switch strings.ToLower(secLevel) { @@ -130,11 +152,6 @@ func TestReceiveTrap(t *testing.T) { now := uint32(123123123) fakeTime := time.Unix(456456456, 456) - type entry struct { - oid string - e snmp.MibEntry - } - // If the first pdu isn't type TimeTicks, gosnmp.SendTrap() will // prepend one with time.Now() var tests = []struct { @@ -1260,14 +1277,6 @@ func TestReceiveTrap(t *testing.T) { timeFunc: func() time.Time { return fakeTime }, - lookupFunc: func(input string) (snmp.MibEntry, error) { - for _, entry := range tt.entries { - if input == entry.oid { - return snmp.MibEntry{MibName: entry.e.MibName, OidText: entry.e.OidText}, nil - } - } - return snmp.MibEntry{}, fmt.Errorf("unexpected oid") - }, //if cold start be answer otherwise err Log: testutil.Logger{}, Version: tt.version.String(), @@ -1277,10 +1286,14 @@ func TestReceiveTrap(t *testing.T) { AuthPassword: tt.authPass, PrivProtocol: tt.privProto, PrivPassword: tt.privPass, + Translator: "netsnmp", } require.NoError(t, s.Init()) + //inject test translator + s.translator = newTestTranslator(tt.entries) + var acc testutil.Accumulator require.Nil(t, s.Start(&acc)) defer s.Stop() diff --git a/plugins/processors/ifname/ifname.go b/plugins/processors/ifname/ifname.go index 04a860f956ad6..5fd6989d7a005 100644 --- a/plugins/processors/ifname/ifname.go +++ b/plugins/processors/ifname/ifname.go @@ -107,6 +107,8 @@ type IfName struct { getMapRemote mapFunc makeTable makeTableFunc + + translator si.Translator } const minRetry = 5 * time.Minute @@ -121,7 +123,7 @@ func (d *IfName) Description() string { func (d *IfName) Init() error { d.getMapRemote = d.getMapRemoteNoMock - d.makeTable = makeTableNoMock + d.makeTable = d.makeTableNoMock c := NewTTLCache(time.Duration(d.CacheTTL), d.CacheSize) d.cache = &c @@ -132,6 +134,10 @@ func (d *IfName) Init() error { return fmt.Errorf("parsing SNMP client config: %w", err) } + // Since OIDs in this plugin are always numeric there is no need + // to translate. + d.translator = si.NewNetsnmpTranslator() + return nil } @@ -309,11 +315,11 @@ func (d *IfName) getMapRemoteNoMock(agent string) (nameMap, error) { //try ifXtable and ifName first. if that fails, fall back to //ifTable and ifDescr var m nameMap - if m, err = buildMap(gs, d.ifXTable); err == nil { + if m, err = d.buildMap(gs, d.ifXTable); err == nil { return m, nil } - if m, err = buildMap(gs, d.ifTable); err == nil { + if m, err = d.buildMap(gs, d.ifTable); err == nil { return m, nil } @@ -340,7 +346,7 @@ func init() { }) } -func makeTableNoMock(oid string) (*si.Table, error) { +func (d *IfName) makeTableNoMock(oid string) (*si.Table, error) { var err error tab := si.Table{ Name: "ifTable", @@ -350,7 +356,7 @@ func makeTableNoMock(oid string) (*si.Table, error) { }, } - err = tab.Init() + err = tab.Init(d.translator) if err != nil { //Init already wraps return nil, err @@ -359,10 +365,10 @@ func makeTableNoMock(oid string) (*si.Table, error) { return &tab, nil } -func buildMap(gs snmp.GosnmpWrapper, tab *si.Table) (nameMap, error) { +func (d *IfName) buildMap(gs snmp.GosnmpWrapper, tab *si.Table) (nameMap, error) { var err error - rtab, err := tab.Build(gs, true) + rtab, err := tab.Build(gs, true, d.translator) if err != nil { //Build already wraps return nil, err diff --git a/plugins/processors/ifname/ifname_test.go b/plugins/processors/ifname/ifname_test.go index 4b8b78aea2c6e..c639fc2f21af2 100644 --- a/plugins/processors/ifname/ifname_test.go +++ b/plugins/processors/ifname/ifname_test.go @@ -36,7 +36,7 @@ func TestTable(t *testing.T) { require.NoError(t, err) // Could use ifIndex but oid index is always the same - m, err := buildMap(gs, tab) + m, err := d.buildMap(gs, tab) require.NoError(t, err) require.NotEmpty(t, m) }