From d01836326122989006060e2408cdba7de29e9719 Mon Sep 17 00:00:00 2001 From: Thomas Casteleyn Date: Wed, 21 Feb 2024 16:55:01 +0100 Subject: [PATCH] chore(snmp): Refactor SNMP translation and data-structures (#14830) --- internal/snmp/field.go | 259 ++++++ internal/snmp/mib_loader.go | 140 ++++ internal/snmp/mib_loader_test.go | 87 ++ internal/snmp/table.go | 315 +++++++ internal/snmp/table_test.go | 246 ++++++ .../snmp/testdata/gosmi}/bridgeMib | 0 .../snmp/testdata/gosmi}/bridgeMibImports | 0 .../snmp/testdata/gosmi}/foo | 0 .../snmp/testdata/gosmi}/fooImports | 0 .../snmp/testdata/gosmi}/ifPhysAddress | 0 .../snmp/testdata/gosmi}/ifPhysAddressImports | 0 .../snmp/testdata/gosmi}/server | 0 .../snmp/testdata/gosmi}/serverImports | 0 .../snmp/testdata/gosmi}/tableBuild | 0 .../snmp/testdata/gosmi}/tableMib | 0 .../snmp/testdata/gosmi}/tableMibImports | 0 .../snmp/testdata/gosmi}/tcpMib | 0 .../snmp/testdata/gosmi}/tcpMibImports | 0 internal/snmp/translate.go | 281 ------- internal/snmp/translate_test.go | 153 ---- internal/snmp/translator.go | 19 + internal/snmp/translator_gosmi.go | 208 +++++ .../snmp/translator_gosmi_test.go | 507 +++-------- .../snmp/translator_netsnmp.go | 32 +- .../snmp/translator_netsnmp_mocks_generate.go | 2 +- .../snmp/translator_netsnmp_mocks_test.go | 0 internal/snmp/translator_netsnmp_test.go | 352 ++++++++ internal/snmp/wrapper.go | 10 + internal/snmp/wrapper_test.go | 89 ++ plugins/inputs/snmp/gosmi.go | 143 ---- plugins/inputs/snmp/snmp.go | 612 +------------- plugins/inputs/snmp/snmp_test.go | 787 +++++------------- plugins/processors/ifname/ifname.go | 26 +- plugins/processors/ifname/ifname_test.go | 8 +- 34 files changed, 2085 insertions(+), 2191 deletions(-) create mode 100644 internal/snmp/field.go create mode 100644 internal/snmp/mib_loader.go create mode 100644 internal/snmp/mib_loader_test.go create mode 100644 internal/snmp/table.go create mode 100644 internal/snmp/table_test.go rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/bridgeMib (100%) rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/bridgeMibImports (100%) rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/foo (100%) rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/fooImports (100%) rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/ifPhysAddress (100%) rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/ifPhysAddressImports (100%) rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/server (100%) rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/serverImports (100%) rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/tableBuild (100%) rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/tableMib (100%) rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/tableMibImports (100%) rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/tcpMib (100%) rename {plugins/inputs/snmp/testdata => internal/snmp/testdata/gosmi}/tcpMibImports (100%) delete mode 100644 internal/snmp/translate.go delete mode 100644 internal/snmp/translate_test.go create mode 100644 internal/snmp/translator_gosmi.go rename plugins/inputs/snmp/gosmi_test.go => internal/snmp/translator_gosmi_test.go (53%) rename plugins/inputs/snmp/netsnmp.go => internal/snmp/translator_netsnmp.go (88%) rename plugins/inputs/snmp/snmp_mocks_generate.go => internal/snmp/translator_netsnmp_mocks_generate.go (97%) rename plugins/inputs/snmp/snmp_mocks_test.go => internal/snmp/translator_netsnmp_mocks_test.go (100%) create mode 100644 internal/snmp/translator_netsnmp_test.go create mode 100644 internal/snmp/wrapper_test.go delete mode 100644 plugins/inputs/snmp/gosmi.go diff --git a/internal/snmp/field.go b/internal/snmp/field.go new file mode 100644 index 000000000..603897ca4 --- /dev/null +++ b/internal/snmp/field.go @@ -0,0 +1,259 @@ +package snmp + +import ( + "encoding/binary" + "errors" + "fmt" + "math" + "net" + "strconv" + "strings" + + "github.com/gosnmp/gosnmp" +) + +// Field holds the configuration for a Field to look up. +type Field struct { + // Name will be the name of the field. + Name string + // OID is prefix for this field. The plugin will perform a walk through all + // OIDs with this as their parent. For each value found, the plugin will strip + // off the OID prefix, and use the remainder as the index. For multiple fields + // to show up in the same row, they must share the same index. + Oid string + // OidIndexSuffix is the trailing sub-identifier on a table record OID that will be stripped off to get the record's index. + OidIndexSuffix string + // OidIndexLength specifies the length of the index in OID path segments. It can be used to remove sub-identifiers that vary in content or length. + OidIndexLength int + // IsTag controls whether this OID is output as a tag or a value. + IsTag bool + // Conversion controls any type conversion that is done on the value. + // "float"/"float(0)" will convert the value into a float. + // "float(X)" will convert the value into a float, and then move the decimal before Xth right-most digit. + // "int" will convert the value into an integer. + // "hwaddr" will convert a 6-byte string to a MAC address. + // "ipaddr" will convert the value to an IPv4 or IPv6 address. + // "enum"/"enum(1)" will convert the value according to its syntax. (Only supported with gosmi translator) + Conversion string + // Translate tells if the value of the field should be snmptranslated + Translate bool + // Secondary index table allows to merge data from two tables with different index + // that this filed will be used to join them. There can be only one secondary index table. + SecondaryIndexTable bool + // This field is using secondary index, and will be later merged with primary index + // using SecondaryIndexTable. SecondaryIndexTable and SecondaryIndexUse are exclusive. + SecondaryIndexUse bool + // Controls if entries from secondary table should be added or not if joining + // index is present or not. I set to true, means that join is outer, and + // index is prepended with "Secondary." for missing values to avoid overlapping + // indexes from both tables. + // Can be set per field or globally with SecondaryIndexTable, global true overrides + // per field false. + SecondaryOuterJoin bool + + initialized bool + translator Translator +} + +// init() converts OID names to numbers, and sets the .Name attribute if unset. +func (f *Field) Init(tr Translator) error { + if f.initialized { + return nil + } + + f.translator = tr + + // check if oid needs translation or name is not set + if strings.ContainsAny(f.Oid, ":abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") || f.Name == "" { + _, oidNum, oidText, conversion, err := f.translator.SnmpTranslate(f.Oid) + if err != nil { + return fmt.Errorf("translating: %w", err) + } + f.Oid = oidNum + if f.Name == "" { + f.Name = oidText + } + if f.Conversion == "" { + f.Conversion = conversion + } + //TODO use textual convention conversion from the MIB + } + + if f.SecondaryIndexTable && f.SecondaryIndexUse { + return errors.New("SecondaryIndexTable and UseSecondaryIndex are exclusive") + } + + if !f.SecondaryIndexTable && !f.SecondaryIndexUse && f.SecondaryOuterJoin { + return errors.New("SecondaryOuterJoin set to true, but field is not being used in join") + } + + f.initialized = true + return nil +} + +// fieldConvert converts from any type according to the conv specification +func (f *Field) Convert(ent gosnmp.SnmpPDU) (v interface{}, err error) { + if f.Conversion == "" { + if bs, ok := ent.Value.([]byte); ok { + return string(bs), nil + } + return ent.Value, nil + } + + var d int + if _, err := fmt.Sscanf(f.Conversion, "float(%d)", &d); err == nil || f.Conversion == "float" { + v = ent.Value + switch vt := v.(type) { + case float32: + v = float64(vt) / math.Pow10(d) + case float64: + v = vt / math.Pow10(d) + case int: + v = float64(vt) / math.Pow10(d) + case int8: + v = float64(vt) / math.Pow10(d) + case int16: + v = float64(vt) / math.Pow10(d) + case int32: + v = float64(vt) / math.Pow10(d) + case int64: + v = float64(vt) / math.Pow10(d) + case uint: + v = float64(vt) / math.Pow10(d) + case uint8: + v = float64(vt) / math.Pow10(d) + case uint16: + v = float64(vt) / math.Pow10(d) + case uint32: + v = float64(vt) / math.Pow10(d) + case uint64: + v = float64(vt) / math.Pow10(d) + case []byte: + vf, _ := strconv.ParseFloat(string(vt), 64) + v = vf / math.Pow10(d) + case string: + vf, _ := strconv.ParseFloat(vt, 64) + v = vf / math.Pow10(d) + } + return v, nil + } + + if f.Conversion == "int" { + v = ent.Value + switch vt := v.(type) { + case float32: + v = int64(vt) + case float64: + v = int64(vt) + case int: + v = int64(vt) + case int8: + v = int64(vt) + case int16: + v = int64(vt) + case int32: + v = int64(vt) + case int64: + v = vt + case uint: + v = int64(vt) + case uint8: + v = int64(vt) + case uint16: + v = int64(vt) + case uint32: + v = int64(vt) + case uint64: + v = int64(vt) + case []byte: + v, _ = strconv.ParseInt(string(vt), 10, 64) + case string: + v, _ = strconv.ParseInt(vt, 10, 64) + } + return v, nil + } + + if f.Conversion == "hwaddr" { + switch vt := ent.Value.(type) { + case string: + v = net.HardwareAddr(vt).String() + case []byte: + v = net.HardwareAddr(vt).String() + default: + return nil, fmt.Errorf("invalid type (%T) for hwaddr conversion", v) + } + return v, nil + } + + split := strings.Split(f.Conversion, ":") + if split[0] == "hextoint" && len(split) == 3 { + endian := split[1] + bit := split[2] + + bv, ok := ent.Value.([]byte) + if !ok { + return ent.Value, nil + } + + switch endian { + case "LittleEndian": + switch bit { + case "uint64": + v = binary.LittleEndian.Uint64(bv) + case "uint32": + v = binary.LittleEndian.Uint32(bv) + case "uint16": + v = binary.LittleEndian.Uint16(bv) + default: + return nil, fmt.Errorf("invalid bit value (%s) for hex to int conversion", bit) + } + case "BigEndian": + switch bit { + case "uint64": + v = binary.BigEndian.Uint64(bv) + case "uint32": + v = binary.BigEndian.Uint32(bv) + case "uint16": + v = binary.BigEndian.Uint16(bv) + default: + return nil, fmt.Errorf("invalid bit value (%s) for hex to int conversion", bit) + } + default: + return nil, fmt.Errorf("invalid Endian value (%s) for hex to int conversion", endian) + } + + return v, nil + } + + if f.Conversion == "ipaddr" { + var ipbs []byte + + switch vt := ent.Value.(type) { + case string: + ipbs = []byte(vt) + case []byte: + ipbs = vt + default: + return nil, fmt.Errorf("invalid type (%T) for ipaddr conversion", v) + } + + switch len(ipbs) { + case 4, 16: + v = net.IP(ipbs).String() + default: + return nil, fmt.Errorf("invalid length (%d) for ipaddr conversion", len(ipbs)) + } + + return v, nil + } + + if f.Conversion == "enum" { + return f.translator.SnmpFormatEnum(ent.Name, ent.Value, false) + } + + if f.Conversion == "enum(1)" { + return f.translator.SnmpFormatEnum(ent.Name, ent.Value, true) + } + + return nil, fmt.Errorf("invalid conversion type %q", f.Conversion) +} diff --git a/internal/snmp/mib_loader.go b/internal/snmp/mib_loader.go new file mode 100644 index 000000000..e3bdc4ee0 --- /dev/null +++ b/internal/snmp/mib_loader.go @@ -0,0 +1,140 @@ +package snmp + +import ( + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/sleepinggenius2/gosmi" + + "github.com/influxdata/telegraf" +) + +// must init, append path for each directory, load module for every file +// or gosmi will fail without saying why +var m sync.Mutex +var once sync.Once +var cache = make(map[string]bool) + +type MibLoader interface { + // appendPath takes the path of a directory + appendPath(path string) + + // loadModule takes the name of a file in one of the + // directories. Basename only, no relative or absolute path + loadModule(path string) error +} + +type GosmiMibLoader struct{} + +func (*GosmiMibLoader) appendPath(path string) { + m.Lock() + defer m.Unlock() + + gosmi.AppendPath(path) +} + +func (*GosmiMibLoader) loadModule(path string) error { + m.Lock() + defer m.Unlock() + + _, err := gosmi.LoadModule(path) + return err +} + +// will give all found folders to gosmi and load in all modules found in the folders +func LoadMibsFromPath(paths []string, log telegraf.Logger, loader MibLoader) error { + folders, err := walkPaths(paths, log) + if err != nil { + return err + } + for _, path := range folders { + loader.appendPath(path) + modules, err := os.ReadDir(path) + if err != nil { + log.Warnf("Can't read directory %v", modules) + continue + } + + for _, entry := range modules { + info, err := entry.Info() + if err != nil { + log.Warnf("Couldn't get info for %v: %v", entry.Name(), err) + continue + } + if info.Mode()&os.ModeSymlink != 0 { + symlink := filepath.Join(path, info.Name()) + target, err := filepath.EvalSymlinks(symlink) + if err != nil { + log.Warnf("Couldn't evaluate symbolic links for %v: %v", symlink, err) + continue + } + //replace symlink's info with the target's info + info, err = os.Lstat(target) + if err != nil { + log.Warnf("Couldn't stat target %v: %v", target, err) + continue + } + } + if info.Mode().IsRegular() { + err := loader.loadModule(info.Name()) + if err != nil { + log.Warnf("Couldn't load module %v: %v", info.Name(), err) + continue + } + } + } + } + return nil +} + +// should walk the paths given and find all folders +func walkPaths(paths []string, log telegraf.Logger) ([]string, error) { + once.Do(gosmi.Init) + folders := []string{} + + for _, mibPath := range paths { + // Check if we loaded that path already and skip it if so + m.Lock() + cached := cache[mibPath] + cache[mibPath] = true + m.Unlock() + if cached { + continue + } + + err := filepath.Walk(mibPath, func(path string, info os.FileInfo, err error) error { + if info == nil { + log.Warnf("No mibs found") + if os.IsNotExist(err) { + log.Warnf("MIB path doesn't exist: %q", mibPath) + } else if err != nil { + return err + } + return nil + } + + if info.Mode()&os.ModeSymlink != 0 { + target, err := filepath.EvalSymlinks(path) + if err != nil { + log.Warnf("Couldn't evaluate symbolic links for %v: %v", path, err) + } + info, err = os.Lstat(target) + if err != nil { + log.Warnf("Couldn't stat target %v: %v", target, err) + } + path = target + } + if info.IsDir() { + folders = append(folders, path) + } + + return nil + }) + if err != nil { + return folders, fmt.Errorf("couldn't walk path %q: %w", mibPath, err) + } + } + return folders, nil +} diff --git a/internal/snmp/mib_loader_test.go b/internal/snmp/mib_loader_test.go new file mode 100644 index 000000000..88ff48e92 --- /dev/null +++ b/internal/snmp/mib_loader_test.go @@ -0,0 +1,87 @@ +package snmp + +import ( + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/influxdata/telegraf/testutil" +) + +type TestingMibLoader struct { + folders []string + files []string +} + +func (t *TestingMibLoader) appendPath(path string) { + t.folders = append(t.folders, path) +} + +func (t *TestingMibLoader) loadModule(path string) error { + t.files = append(t.files, path) + return nil +} +func TestFolderLookup(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping on windows") + } + + tests := []struct { + name string + mibPath [][]string + paths [][]string + files []string + }{ + { + name: "loading folders", + mibPath: [][]string{{"testdata", "loadMibsFromPath", "root"}}, + paths: [][]string{ + {"testdata", "loadMibsFromPath", "root"}, + {"testdata", "loadMibsFromPath", "root", "dirOne"}, + {"testdata", "loadMibsFromPath", "root", "dirOne", "dirTwo"}, + {"testdata", "loadMibsFromPath", "linkTarget"}, + }, + files: []string{"empty", "emptyFile"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + loader := TestingMibLoader{} + + var givenPath []string + for _, paths := range tt.mibPath { + rootPath := filepath.Join(paths...) + givenPath = append(givenPath, rootPath) + } + + err := LoadMibsFromPath(givenPath, testutil.Logger{}, &loader) + require.NoError(t, err) + + var folders []string + for _, pathSlice := range tt.paths { + path := filepath.Join(pathSlice...) + folders = append(folders, path) + } + require.Equal(t, folders, loader.folders) + + require.Equal(t, tt.files, loader.files) + }) + } +} + +func TestMissingMibPath(t *testing.T) { + log := testutil.Logger{} + path := []string{"non-existing-directory"} + require.NoError(t, LoadMibsFromPath(path, log, &GosmiMibLoader{})) +} + +func BenchmarkMibLoading(b *testing.B) { + log := testutil.Logger{} + path := []string{"testdata/gosmi"} + for i := 0; i < b.N; i++ { + require.NoError(b, LoadMibsFromPath(path, log, &GosmiMibLoader{})) + } +} diff --git a/internal/snmp/table.go b/internal/snmp/table.go new file mode 100644 index 000000000..6bc712e09 --- /dev/null +++ b/internal/snmp/table.go @@ -0,0 +1,315 @@ +package snmp + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/gosnmp/gosnmp" +) + +// Table holds the configuration for a SNMP table. +type Table struct { + // Name will be the name of the measurement. + Name string + + // Which tags to inherit from the top-level config. + InheritTags []string + + // Adds each row's table index as a tag. + IndexAsTag bool + + // Fields is the tags and values to look up. + Fields []Field `toml:"field"` + + // OID for automatic field population. + // If provided, init() will populate Fields with all the table columns of the + // given OID. + Oid string + + initialized bool + translator Translator +} + +// RTable is the resulting table built from a Table. +type RTable struct { + // Name is the name of the field, copied from Table.Name. + Name string + // Time is the time the table was built. + Time time.Time + // Rows are the rows that were found, one row for each table OID index found. + Rows []RTableRow +} + +// RTableRow is the resulting row containing all the OID values which shared +// the same index. +type RTableRow struct { + // Tags are all the Field values which had IsTag=true. + Tags map[string]string + // Fields are all the Field values which had IsTag=false. + Fields map[string]interface{} +} + +// Init() builds & initializes the nested fields. +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 == "" { + return errors.New("SNMP table in config file is not named. One or both of the oid and name settings must be set") + } + + if t.initialized { + return nil + } + + t.translator = tr + if err := t.initBuild(); err != nil { + return err + } + + secondaryIndexTablePresent := false + // initialize all the nested fields + for i := range t.Fields { + if err := t.Fields[i].Init(t.translator); err != nil { + return fmt.Errorf("initializing field %s: %w", t.Fields[i].Name, err) + } + if t.Fields[i].SecondaryIndexTable { + if secondaryIndexTablePresent { + return errors.New("only one field can be SecondaryIndexTable") + } + secondaryIndexTablePresent = true + } + } + + t.initialized = true + return nil +} + +// initBuild initializes the table if it has an OID configured. If so, the +// net-snmp tools will be used to look up the OID and auto-populate the table's +// fields. +func (t *Table) initBuild() error { + if t.Oid == "" { + return nil + } + + _, _, oidText, fields, err := t.translator.SnmpTable(t.Oid) + if err != nil { + return err + } + + if t.Name == "" { + t.Name = oidText + } + + knownOIDs := map[string]bool{} + for _, f := range t.Fields { + knownOIDs[f.Oid] = true + } + for _, f := range fields { + if !knownOIDs[f.Oid] { + t.Fields = append(t.Fields, f) + } + } + + return nil +} + +// Build retrieves all the fields specified in the table and constructs the RTable. +func (t Table) Build(gs Connection, walk bool) (*RTable, error) { + rows := map[string]RTableRow{} + + //translation table for secondary index (when preforming join on two tables) + secIdxTab := make(map[string]string) + secGlobalOuterJoin := false + for i, f := range t.Fields { + if f.SecondaryIndexTable { + secGlobalOuterJoin = f.SecondaryOuterJoin + if i != 0 { + t.Fields[0], t.Fields[i] = t.Fields[i], t.Fields[0] + } + break + } + } + + tagCount := 0 + for _, f := range t.Fields { + if f.IsTag { + tagCount++ + } + + if len(f.Oid) == 0 { + return nil, fmt.Errorf("cannot have empty OID on field %s", f.Name) + } + var oid string + if f.Oid[0] == '.' { + oid = f.Oid + } else { + // make sure OID has "." because the BulkWalkAll results do, and the prefix needs to match + oid = "." + f.Oid + } + + // ifv contains a mapping of table OID index to field value + ifv := map[string]interface{}{} + + if !walk { + // This is used when fetching non-table fields. Fields configured a the top + // scope of the plugin. + // We fetch the fields directly, and add them to ifv as if the index were an + // empty string. This results in all the non-table fields sharing the same + // index, and being added on the same row. + if pkt, err := gs.Get([]string{oid}); err != nil { + if errors.Is(err, gosnmp.ErrUnknownSecurityLevel) { + return nil, errors.New("unknown security level (sec_level)") + } else if errors.Is(err, gosnmp.ErrUnknownUsername) { + return nil, errors.New("unknown username (sec_name)") + } else if errors.Is(err, gosnmp.ErrWrongDigest) { + return nil, errors.New("wrong digest (auth_protocol, auth_password)") + } else if errors.Is(err, gosnmp.ErrDecryption) { + return nil, errors.New("decryption error (priv_protocol, priv_password)") + } + return nil, fmt.Errorf("performing get on field %s: %w", f.Name, err) + } else if pkt != nil && len(pkt.Variables) > 0 && pkt.Variables[0].Type != gosnmp.NoSuchObject && pkt.Variables[0].Type != gosnmp.NoSuchInstance { + ent := pkt.Variables[0] + fv, err := f.Convert(ent) + if err != nil { + return nil, fmt.Errorf("converting %q (OID %s) for field %s: %w", ent.Value, ent.Name, f.Name, err) + } + ifv[""] = fv + } + } else { + err := gs.Walk(oid, func(ent gosnmp.SnmpPDU) error { + if len(ent.Name) <= len(oid) || ent.Name[:len(oid)+1] != oid+"." { + return &walkError{} // break the walk + } + + idx := ent.Name[len(oid):] + if f.OidIndexSuffix != "" { + if !strings.HasSuffix(idx, f.OidIndexSuffix) { + // this entry doesn't match our OidIndexSuffix. skip it + return nil + } + idx = idx[:len(idx)-len(f.OidIndexSuffix)] + } + if f.OidIndexLength != 0 { + i := f.OidIndexLength + 1 // leading separator + idx = strings.Map(func(r rune) rune { + if r == '.' { + i-- + } + if i < 1 { + return -1 + } + return r + }, idx) + } + + // snmptranslate table field value here + if f.Translate { + if entOid, ok := ent.Value.(string); ok { + _, _, oidText, _, err := t.translator.SnmpTranslate(entOid) + if err == nil { + // If no error translating, the original value for ent.Value should be replaced + ent.Value = oidText + } + } + } + + fv, err := f.Convert(ent) + if err != nil { + return &walkError{ + msg: fmt.Sprintf("converting %q (OID %s) for field %s", ent.Value, ent.Name, f.Name), + err: err, + } + } + ifv[idx] = fv + return nil + }) + if err != nil { + // Our callback always wraps errors in a walkError. + // If this error isn't a walkError, we know it's not + // from the callback + var walkErr *walkError + if !errors.As(err, &walkErr) { + return nil, fmt.Errorf("performing bulk walk for field %s: %w", f.Name, err) + } + } + } + + for idx, v := range ifv { + if f.SecondaryIndexUse { + if newidx, ok := secIdxTab[idx]; ok { + idx = newidx + } else { + if !secGlobalOuterJoin && !f.SecondaryOuterJoin { + continue + } + idx = ".Secondary" + idx + } + } + rtr, ok := rows[idx] + if !ok { + rtr = RTableRow{} + rtr.Tags = map[string]string{} + rtr.Fields = map[string]interface{}{} + rows[idx] = rtr + } + if t.IndexAsTag && idx != "" { + if idx[0] == '.' { + idx = idx[1:] + } + rtr.Tags["index"] = idx + } + // don't add an empty string + if vs, ok := v.(string); !ok || vs != "" { + if f.IsTag { + if ok { + rtr.Tags[f.Name] = vs + } else { + rtr.Tags[f.Name] = fmt.Sprintf("%v", v) + } + } else { + rtr.Fields[f.Name] = v + } + if f.SecondaryIndexTable { + //indexes are stored here with prepending "." so we need to add them if needed + var vss string + if ok { + vss = "." + vs + } else { + vss = fmt.Sprintf(".%v", v) + } + if idx[0] == '.' { + secIdxTab[vss] = idx + } else { + secIdxTab[vss] = "." + idx + } + } + } + } + } + + rt := RTable{ + Name: t.Name, + Time: time.Now(), //TODO record time at start + Rows: make([]RTableRow, 0, len(rows)), + } + for _, r := range rows { + rt.Rows = append(rt.Rows, r) + } + return &rt, nil +} + +type walkError struct { + msg string + err error +} + +func (e *walkError) Error() string { + return e.msg +} + +func (e *walkError) Unwrap() error { + return e.err +} diff --git a/internal/snmp/table_test.go b/internal/snmp/table_test.go new file mode 100644 index 000000000..4731465ba --- /dev/null +++ b/internal/snmp/table_test.go @@ -0,0 +1,246 @@ +package snmp + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTableJoin_walk(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, + }, + }, + } + + tb, err := tbl.Build(tsc, true) + require.NoError(t, err) + + require.Equal(t, "mytable", tb.Name) + 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, + }, + } + 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 TestTableOuterJoin_walk(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, + }, + }, + } + + tb, err := tbl.Build(tsc, true) + require.NoError(t, err) + + require.Equal(t, "mytable", tb.Name) + 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, + }, + } + 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 TestTableJoinNoIndexAsTag_walk(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, + }, + }, + } + + tb, err := tbl.Build(tsc, true) + require.NoError(t, err) + + require.Equal(t, "mytable", tb.Name) + 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, + }, + } + require.Len(t, tb.Rows, 3) + require.Contains(t, tb.Rows, rtr1) + require.Contains(t, tb.Rows, rtr2) + require.Contains(t, tb.Rows, rtr3) +} diff --git a/plugins/inputs/snmp/testdata/bridgeMib b/internal/snmp/testdata/gosmi/bridgeMib similarity index 100% rename from plugins/inputs/snmp/testdata/bridgeMib rename to internal/snmp/testdata/gosmi/bridgeMib diff --git a/plugins/inputs/snmp/testdata/bridgeMibImports b/internal/snmp/testdata/gosmi/bridgeMibImports similarity index 100% rename from plugins/inputs/snmp/testdata/bridgeMibImports rename to internal/snmp/testdata/gosmi/bridgeMibImports diff --git a/plugins/inputs/snmp/testdata/foo b/internal/snmp/testdata/gosmi/foo similarity index 100% rename from plugins/inputs/snmp/testdata/foo rename to internal/snmp/testdata/gosmi/foo diff --git a/plugins/inputs/snmp/testdata/fooImports b/internal/snmp/testdata/gosmi/fooImports similarity index 100% rename from plugins/inputs/snmp/testdata/fooImports rename to internal/snmp/testdata/gosmi/fooImports diff --git a/plugins/inputs/snmp/testdata/ifPhysAddress b/internal/snmp/testdata/gosmi/ifPhysAddress similarity index 100% rename from plugins/inputs/snmp/testdata/ifPhysAddress rename to internal/snmp/testdata/gosmi/ifPhysAddress diff --git a/plugins/inputs/snmp/testdata/ifPhysAddressImports b/internal/snmp/testdata/gosmi/ifPhysAddressImports similarity index 100% rename from plugins/inputs/snmp/testdata/ifPhysAddressImports rename to internal/snmp/testdata/gosmi/ifPhysAddressImports diff --git a/plugins/inputs/snmp/testdata/server b/internal/snmp/testdata/gosmi/server similarity index 100% rename from plugins/inputs/snmp/testdata/server rename to internal/snmp/testdata/gosmi/server diff --git a/plugins/inputs/snmp/testdata/serverImports b/internal/snmp/testdata/gosmi/serverImports similarity index 100% rename from plugins/inputs/snmp/testdata/serverImports rename to internal/snmp/testdata/gosmi/serverImports diff --git a/plugins/inputs/snmp/testdata/tableBuild b/internal/snmp/testdata/gosmi/tableBuild similarity index 100% rename from plugins/inputs/snmp/testdata/tableBuild rename to internal/snmp/testdata/gosmi/tableBuild diff --git a/plugins/inputs/snmp/testdata/tableMib b/internal/snmp/testdata/gosmi/tableMib similarity index 100% rename from plugins/inputs/snmp/testdata/tableMib rename to internal/snmp/testdata/gosmi/tableMib diff --git a/plugins/inputs/snmp/testdata/tableMibImports b/internal/snmp/testdata/gosmi/tableMibImports similarity index 100% rename from plugins/inputs/snmp/testdata/tableMibImports rename to internal/snmp/testdata/gosmi/tableMibImports diff --git a/plugins/inputs/snmp/testdata/tcpMib b/internal/snmp/testdata/gosmi/tcpMib similarity index 100% rename from plugins/inputs/snmp/testdata/tcpMib rename to internal/snmp/testdata/gosmi/tcpMib diff --git a/plugins/inputs/snmp/testdata/tcpMibImports b/internal/snmp/testdata/gosmi/tcpMibImports similarity index 100% rename from plugins/inputs/snmp/testdata/tcpMibImports rename to internal/snmp/testdata/gosmi/tcpMibImports diff --git a/internal/snmp/translate.go b/internal/snmp/translate.go deleted file mode 100644 index dc3798254..000000000 --- a/internal/snmp/translate.go +++ /dev/null @@ -1,281 +0,0 @@ -package snmp - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/sleepinggenius2/gosmi" - "github.com/sleepinggenius2/gosmi/types" - - "github.com/influxdata/telegraf" -) - -// must init, append path for each directory, load module for every file -// or gosmi will fail without saying why -var m sync.Mutex -var once sync.Once -var cache = make(map[string]bool) - -type MibLoader interface { - // appendPath takes the path of a directory - appendPath(path string) - - // loadModule takes the name of a file in one of the - // directories. Basename only, no relative or absolute path - loadModule(path string) error -} - -type GosmiMibLoader struct{} - -func (*GosmiMibLoader) appendPath(path string) { - m.Lock() - defer m.Unlock() - - gosmi.AppendPath(path) -} - -func (*GosmiMibLoader) loadModule(path string) error { - m.Lock() - defer m.Unlock() - - _, err := gosmi.LoadModule(path) - return err -} - -// will give all found folders to gosmi and load in all modules found in the folders -func LoadMibsFromPath(paths []string, log telegraf.Logger, loader MibLoader) error { - folders, err := walkPaths(paths, log) - if err != nil { - return err - } - for _, path := range folders { - loader.appendPath(path) - modules, err := os.ReadDir(path) - if err != nil { - log.Warnf("Can't read directory %v", modules) - continue - } - - for _, entry := range modules { - info, err := entry.Info() - if err != nil { - log.Warnf("Couldn't get info for %v: %v", entry.Name(), err) - continue - } - if info.Mode()&os.ModeSymlink != 0 { - symlink := filepath.Join(path, info.Name()) - target, err := filepath.EvalSymlinks(symlink) - if err != nil { - log.Warnf("Couldn't evaluate symbolic links for %v: %v", symlink, err) - continue - } - //replace symlink's info with the target's info - info, err = os.Lstat(target) - if err != nil { - log.Warnf("Couldn't stat target %v: %v", target, err) - continue - } - } - if info.Mode().IsRegular() { - err := loader.loadModule(info.Name()) - if err != nil { - log.Warnf("Couldn't load module %v: %v", info.Name(), err) - continue - } - } - } - } - return nil -} - -// should walk the paths given and find all folders -func walkPaths(paths []string, log telegraf.Logger) ([]string, error) { - once.Do(gosmi.Init) - folders := []string{} - - for _, mibPath := range paths { - // Check if we loaded that path already and skip it if so - m.Lock() - cached := cache[mibPath] - cache[mibPath] = true - m.Unlock() - if cached { - continue - } - - err := filepath.Walk(mibPath, func(path string, info os.FileInfo, err error) error { - if info == nil { - log.Warnf("No mibs found") - if os.IsNotExist(err) { - log.Warnf("MIB path doesn't exist: %q", mibPath) - } else if err != nil { - return err - } - return nil - } - - if info.Mode()&os.ModeSymlink != 0 { - target, err := filepath.EvalSymlinks(path) - if err != nil { - log.Warnf("Couldn't evaluate symbolic links for %v: %v", path, err) - } - info, err = os.Lstat(target) - if err != nil { - log.Warnf("Couldn't stat target %v: %v", target, err) - } - path = target - } - if info.IsDir() { - folders = append(folders, path) - } - - return nil - }) - if err != nil { - return folders, fmt.Errorf("couldn't walk path %q: %w", mibPath, err) - } - } - return folders, nil -} - -// The following is for snmp_trap -type MibEntry struct { - MibName string - OidText string -} - -func TrapLookup(oid string) (e MibEntry, err error) { - var givenOid types.Oid - if givenOid, err = types.OidFromString(oid); err != nil { - return e, fmt.Errorf("could not convert OID %s: %w", oid, err) - } - - // Get node name - var node gosmi.SmiNode - if node, err = gosmi.GetNodeByOID(givenOid); err != nil { - return e, err - } - e.OidText = node.Name - - // Add not found OID part - if !givenOid.Equals(node.Oid) { - e.OidText += "." + givenOid[len(node.Oid):].String() - } - - // Get module name - module := node.GetModule() - if module.Name != "" { - e.MibName = module.Name - } - - return e, nil -} - -// The following is for snmp - -func GetIndex(mibPrefix string, node gosmi.SmiNode) (col []string, tagOids map[string]struct{}) { - // first attempt to get the table's tags - tagOids = map[string]struct{}{} - - // mimcks grabbing INDEX {} that is returned from snmptranslate -Td MibName - for _, index := range node.GetIndex() { - tagOids[mibPrefix+index.Name] = struct{}{} - } - - // grabs all columns from the table - // mimmicks grabbing everything returned from snmptable -Ch -Cl -c public 127.0.0.1 oidFullName - _, col = node.GetColumns() - - return col, tagOids -} - -//nolint:revive //Too many return variable but necessary -func SnmpTranslateCall(oid string) (mibName string, oidNum string, oidText string, conversion string, node gosmi.SmiNode, err error) { - var out gosmi.SmiNode - var end string - if strings.ContainsAny(oid, "::") { - // split given oid - // for example RFC1213-MIB::sysUpTime.0 - s := strings.SplitN(oid, "::", 2) - // moduleName becomes RFC1213 - moduleName := s[0] - module, err := gosmi.GetModule(moduleName) - if err != nil { - return oid, oid, oid, oid, gosmi.SmiNode{}, err - } - if s[1] == "" { - return "", oid, oid, oid, gosmi.SmiNode{}, fmt.Errorf("cannot parse %v", oid) - } - // node becomes sysUpTime.0 - node := s[1] - if strings.ContainsAny(node, ".") { - s = strings.SplitN(node, ".", 2) - // node becomes sysUpTime - node = s[0] - end = "." + s[1] - } - - out, err = module.GetNode(node) - if err != nil { - return oid, oid, oid, oid, out, err - } - - if oidNum = out.RenderNumeric(); oidNum == "" { - return oid, oid, oid, oid, out, fmt.Errorf("cannot translate %v into a numeric OID, please ensure all imported MIBs are in the path", oid) - } - - oidNum = "." + oidNum + end - } else if strings.ContainsAny(oid, "abcdefghijklnmopqrstuvwxyz") { - //handle mixed oid ex. .iso.2.3 - s := strings.Split(oid, ".") - for i := range s { - if strings.ContainsAny(s[i], "abcdefghijklmnopqrstuvwxyz") { - out, err = gosmi.GetNode(s[i]) - if err != nil { - return oid, oid, oid, oid, out, err - } - s[i] = out.RenderNumeric() - } - } - oidNum = strings.Join(s, ".") - out, _ = gosmi.GetNodeByOID(types.OidMustFromString(oidNum)) - } else { - out, err = gosmi.GetNodeByOID(types.OidMustFromString(oid)) - oidNum = oid - // ensure modules are loaded or node will be empty (might not error) - //nolint:nilerr // do not return the err as the oid is numeric and telegraf can continue - if err != nil || out.Name == "iso" { - return oid, oid, oid, oid, out, nil - } - } - - tc := out.GetSubtree() - - for i := range tc { - // case where the mib doesn't have a conversion so Type struct will be nil - // prevents seg fault - if tc[i].Type == nil { - break - } - switch tc[i].Type.Name { - case "MacAddress", "PhysAddress": - conversion = "hwaddr" - case "InetAddressIPv4", "InetAddressIPv6", "InetAddress", "IPSIpAddress": - conversion = "ipaddr" - } - } - - oidText = out.RenderQualified() - i := strings.Index(oidText, "::") - if i == -1 { - return "", oid, oid, oid, out, errors.New("not found") - } - mibName = oidText[:i] - oidText = oidText[i+2:] + end - - return mibName, oidNum, oidText, conversion, out, nil -} diff --git a/internal/snmp/translate_test.go b/internal/snmp/translate_test.go deleted file mode 100644 index 9f22947e8..000000000 --- a/internal/snmp/translate_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package snmp - -import ( - "path/filepath" - "runtime" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/influxdata/telegraf/testutil" -) - -func TestTrapLookup(t *testing.T) { - tests := []struct { - name string - oid string - expected MibEntry - }{ - { - name: "Known trap OID", - oid: ".1.3.6.1.6.3.1.1.5.1", - expected: MibEntry{ - MibName: "TGTEST-MIB", - OidText: "coldStart", - }, - }, - { - name: "Known trap value OID", - oid: ".1.3.6.1.2.1.1.3.0", - expected: MibEntry{ - MibName: "TGTEST-MIB", - OidText: "sysUpTimeInstance", - }, - }, - { - name: "Unknown enterprise sub-OID", - oid: ".1.3.6.1.4.1.0.1.2.3", - expected: MibEntry{ - MibName: "TGTEST-MIB", - OidText: "enterprises.0.1.2.3", - }, - }, - { - name: "Unknown MIB", - oid: ".1.2.3", - expected: MibEntry{OidText: "iso.2.3"}, - }, - } - - // Load the MIBs - require.NoError(t, LoadMibsFromPath([]string{"testdata/mibs"}, testutil.Logger{}, &GosmiMibLoader{})) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Run the actual test - actual, err := TrapLookup(tt.oid) - require.NoError(t, err) - require.Equal(t, tt.expected, actual) - }) - } -} - -func TestTrapLookupFail(t *testing.T) { - tests := []struct { - name string - oid string - expected string - }{ - { - name: "New top level OID", - oid: ".3.6.1.3.0", - expected: "Could not find node for OID 3.6.1.3.0", - }, - { - name: "Malformed OID", - oid: ".1.3.dod.1.3.0", - expected: "could not convert OID .1.3.dod.1.3.0: strconv.ParseUint: parsing \"dod\": invalid syntax", - }, - } - - // Load the MIBs - require.NoError(t, LoadMibsFromPath([]string{"testdata/mibs"}, testutil.Logger{}, &GosmiMibLoader{})) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Run the actual test - _, err := TrapLookup(tt.oid) - require.EqualError(t, err, tt.expected) - }) - } -} - -type TestingMibLoader struct { - folders []string - files []string -} - -func (t *TestingMibLoader) appendPath(path string) { - t.folders = append(t.folders, path) -} - -func (t *TestingMibLoader) loadModule(path string) error { - t.files = append(t.files, path) - return nil -} -func TestFolderLookup(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("Skipping on windows") - } - - tests := []struct { - name string - mibPath [][]string - paths [][]string - files []string - }{ - { - name: "loading folders", - mibPath: [][]string{{"testdata", "loadMibsFromPath", "root"}}, - paths: [][]string{ - {"testdata", "loadMibsFromPath", "root"}, - {"testdata", "loadMibsFromPath", "root", "dirOne"}, - {"testdata", "loadMibsFromPath", "root", "dirOne", "dirTwo"}, - {"testdata", "loadMibsFromPath", "linkTarget"}, - }, - files: []string{"empty", "emptyFile"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - loader := TestingMibLoader{} - - var givenPath []string - for _, paths := range tt.mibPath { - rootPath := filepath.Join(paths...) - givenPath = append(givenPath, rootPath) - } - - err := LoadMibsFromPath(givenPath, testutil.Logger{}, &loader) - require.NoError(t, err) - - var folders []string - for _, pathSlice := range tt.paths { - path := filepath.Join(pathSlice...) - folders = append(folders, path) - } - require.Equal(t, folders, loader.folders) - - require.Equal(t, tt.files, loader.files) - }) - } -} diff --git a/internal/snmp/translator.go b/internal/snmp/translator.go index 6a0993a6d..720ee4f15 100644 --- a/internal/snmp/translator.go +++ b/internal/snmp/translator.go @@ -3,3 +3,22 @@ package snmp type TranslatorPlugin interface { SetTranslator(name string) // Agent calls this on inputs before Init } + +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, + ) + + SnmpFormatEnum(oid string, value interface{}, full bool) ( + formatted string, + err error, + ) +} diff --git a/internal/snmp/translator_gosmi.go b/internal/snmp/translator_gosmi.go new file mode 100644 index 000000000..10509c62e --- /dev/null +++ b/internal/snmp/translator_gosmi.go @@ -0,0 +1,208 @@ +package snmp + +import ( + "errors" + "fmt" + "strings" + + "github.com/sleepinggenius2/gosmi" + "github.com/sleepinggenius2/gosmi/models" + "github.com/sleepinggenius2/gosmi/types" + + "github.com/influxdata/telegraf" +) + +type gosmiTranslator struct { +} + +func NewGosmiTranslator(paths []string, log telegraf.Logger) (*gosmiTranslator, error) { + err := LoadMibsFromPath(paths, log, &GosmiMibLoader{}) + if err == nil { + return &gosmiTranslator{}, nil + } + return nil, err +} + +//nolint:revive //function-result-limit conditionally 5 return results allowed +func (g *gosmiTranslator) SnmpTranslate(oid string) (mibName string, oidNum string, oidText string, conversion string, err error) { + mibName, oidNum, oidText, conversion, _, err = snmpTranslateCall(oid) + return mibName, oidNum, oidText, conversion, err +} + +// 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) { + mibName, oidNum, oidText, _, node, err := snmpTranslateCall(oid) + if err != nil { + return "", "", "", nil, fmt.Errorf("translating: %w", err) + } + + mibPrefix := mibName + "::" + + col, tagOids := getIndex(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, nil +} + +func (g *gosmiTranslator) SnmpFormatEnum(oid string, value interface{}, full bool) (string, error) { + //nolint:dogsled // only need to get the node + _, _, _, _, node, err := snmpTranslateCall(oid) + + if err != nil { + return "", err + } + + var v models.Value + if full { + v = node.FormatValue(value, models.FormatEnumName, models.FormatEnumValue) + } else { + v = node.FormatValue(value, models.FormatEnumName) + } + + return v.Formatted, nil +} + +func getIndex(mibPrefix string, node gosmi.SmiNode) (col []string, tagOids map[string]struct{}) { + // first attempt to get the table's tags + tagOids = map[string]struct{}{} + + // mimcks grabbing INDEX {} that is returned from snmptranslate -Td MibName + for _, index := range node.GetIndex() { + tagOids[mibPrefix+index.Name] = struct{}{} + } + + // grabs all columns from the table + // mimmicks grabbing everything returned from snmptable -Ch -Cl -c public 127.0.0.1 oidFullName + _, col = node.GetColumns() + + return col, tagOids +} + +//nolint:revive //Too many return variable but necessary +func snmpTranslateCall(oid string) (mibName string, oidNum string, oidText string, conversion string, node gosmi.SmiNode, err error) { + var out gosmi.SmiNode + var end string + if strings.ContainsAny(oid, "::") { + // split given oid + // for example RFC1213-MIB::sysUpTime.0 + s := strings.SplitN(oid, "::", 2) + // moduleName becomes RFC1213 + moduleName := s[0] + module, err := gosmi.GetModule(moduleName) + if err != nil { + return oid, oid, oid, "", gosmi.SmiNode{}, err + } + if s[1] == "" { + return "", oid, oid, "", gosmi.SmiNode{}, fmt.Errorf("cannot parse %v", oid) + } + // node becomes sysUpTime.0 + node := s[1] + if strings.ContainsAny(node, ".") { + s = strings.SplitN(node, ".", 2) + // node becomes sysUpTime + node = s[0] + end = "." + s[1] + } + + out, err = module.GetNode(node) + if err != nil { + return oid, oid, oid, "", out, err + } + + if oidNum = out.RenderNumeric(); oidNum == "" { + return oid, oid, oid, "", out, fmt.Errorf("cannot translate %v into a numeric OID, please ensure all imported MIBs are in the path", oid) + } + + oidNum = "." + oidNum + end + } else if strings.ContainsAny(oid, "abcdefghijklnmopqrstuvwxyz") { + //handle mixed oid ex. .iso.2.3 + s := strings.Split(oid, ".") + for i := range s { + if strings.ContainsAny(s[i], "abcdefghijklmnopqrstuvwxyz") { + out, err = gosmi.GetNode(s[i]) + if err != nil { + return oid, oid, oid, "", out, err + } + s[i] = out.RenderNumeric() + } + } + oidNum = strings.Join(s, ".") + out, _ = gosmi.GetNodeByOID(types.OidMustFromString(oidNum)) + } else { + out, err = gosmi.GetNodeByOID(types.OidMustFromString(oid)) + oidNum = oid + // ensure modules are loaded or node will be empty (might not error) + //nolint:nilerr // do not return the err as the oid is numeric and telegraf can continue + if err != nil || out.Name == "iso" { + return oid, oid, oid, "", out, nil + } + } + + tc := out.GetSubtree() + + for i := range tc { + // case where the mib doesn't have a conversion so Type struct will be nil + // prevents seg fault + if tc[i].Type == nil { + break + } + switch tc[i].Type.Name { + case "MacAddress", "PhysAddress": + conversion = "hwaddr" + case "InetAddressIPv4", "InetAddressIPv6", "InetAddress", "IPSIpAddress": + conversion = "ipaddr" + } + } + + oidText = out.RenderQualified() + i := strings.Index(oidText, "::") + if i == -1 { + return "", oid, oid, "", out, errors.New("not found") + } + mibName = oidText[:i] + oidText = oidText[i+2:] + end + + return mibName, oidNum, oidText, conversion, out, nil +} + +// The following is for snmp_trap +type MibEntry struct { + MibName string + OidText string +} + +func TrapLookup(oid string) (e MibEntry, err error) { + var givenOid types.Oid + if givenOid, err = types.OidFromString(oid); err != nil { + return e, fmt.Errorf("could not convert OID %s: %w", oid, err) + } + + // Get node name + var node gosmi.SmiNode + if node, err = gosmi.GetNodeByOID(givenOid); err != nil { + return e, err + } + e.OidText = node.Name + + // Add not found OID part + if !givenOid.Equals(node.Oid) { + e.OidText += "." + givenOid[len(node.Oid):].String() + } + + // Get module name + module := node.GetModule() + if module.Name != "" { + e.MibName = module.Name + } + + return e, nil +} diff --git a/plugins/inputs/snmp/gosmi_test.go b/internal/snmp/translator_gosmi_test.go similarity index 53% rename from plugins/inputs/snmp/gosmi_test.go rename to internal/snmp/translator_gosmi_test.go index 3e247739a..a777ce931 100644 --- a/plugins/inputs/snmp/gosmi_test.go +++ b/internal/snmp/translator_gosmi_test.go @@ -1,20 +1,17 @@ package snmp import ( - "errors" "path/filepath" "testing" - "time" "github.com/gosnmp/gosnmp" "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") + testDataPath, err := filepath.Abs("./testdata/gosmi") require.NoError(t, err) tr, err := NewGosmiTranslator([]string{testDataPath}, testutil.Logger{}) @@ -31,56 +28,8 @@ func TestGosmiTranslator(t *testing.T) { 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) + tr := getGosmiTr(t) translations := []struct { inputOid string @@ -102,124 +51,50 @@ func TestFieldInitGosmi(t *testing.T) { for _, txl := range translations { f := Field{Oid: txl.inputOid, Name: txl.inputName, Conversion: txl.inputConversion} - err := f.init(tr) - require.NoError(t, err, "inputOid=%q inputName=%q", txl.inputOid, txl.inputName) + require.NoError(t, f.Init(tr), "inputOid=%q inputName=%q", txl.inputOid, txl.inputName) require.Equal(t, txl.expectedOid, f.Oid, "inputOid=%q inputName=%q inputConversion=%q", txl.inputOid, txl.inputName, txl.inputConversion) require.Equal(t, txl.expectedName, f.Name, "inputOid=%q inputName=%q inputConversion=%q", txl.inputOid, txl.inputName, txl.inputConversion) + require.Equal(t, txl.expectedConversion, f.Conversion, "inputOid=%q inputName=%q inputConversion=%q", 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) - - require.Equal(t, "atTable", s.Tables[0].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}) -} - -func TestSnmpInitGosmi(t *testing.T) { - testDataPath, err := filepath.Abs("./testdata") - require.NoError(t, err) - - s := &Snmp{ - Tables: []Table{ - {Oid: "RFC1213-MIB::atTable"}, - }, + tbl := Table{ + Oid: ".1.3.6.1.2.1.3.1", Fields: []Field{ - {Oid: "RFC1213-MIB::atPhysAddress"}, - }, - ClientConfig: snmp.ClientConfig{ - Path: []string{testDataPath}, - Translator: "gosmi", + {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) + tr := getGosmiTr(t) + require.NoError(t, tbl.Init(tr)) - 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}) + require.Equal(t, "atTable", tbl.Name) - require.Equal(t, Field{ - Oid: ".1.3.6.1.2.1.3.1.1.2", - Name: "atPhysAddress", - Conversion: "hwaddr", - initialized: true, - }, s.Fields[0]) -} + require.Len(t, tbl.Fields, 5) -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", - }, - } + require.Equal(t, ".999", tbl.Fields[0].Oid) + require.Equal(t, "foo", tbl.Fields[0].Name) + require.False(t, tbl.Fields[0].IsTag) + require.Empty(t, tbl.Fields[0].Conversion) - err := s.Init() - require.NoError(t, err) + require.Equal(t, ".1.3.6.1.2.1.3.1.1.1", tbl.Fields[1].Oid) + require.Equal(t, "atIfIndex", tbl.Fields[1].Name) + require.True(t, tbl.Fields[1].IsTag) + require.Empty(t, tbl.Fields[1].Conversion) - require.Equal(t, ".9.1.1.1.1", s.Fields[0].Oid) - require.Equal(t, "one", s.Fields[0].Name) - require.True(t, s.Fields[0].IsTag) + require.Equal(t, ".1.3.6.1.2.1.3.1.1.2", tbl.Fields[2].Oid) + require.Equal(t, "atPhysAddress", tbl.Fields[2].Name) + require.False(t, tbl.Fields[2].IsTag) + require.Equal(t, "hwaddr", tbl.Fields[2].Conversion) - require.Equal(t, ".9.1.1.1.2", s.Fields[1].Oid) - require.Equal(t, "two", s.Fields[1].Name) - require.False(t, 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.False(t, 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.True(t, 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.False(t, 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.False(t, s.Tables[0].Fields[2].IsTag) + require.Equal(t, ".1.3.6.1.2.1.3.1.1.3", tbl.Fields[4].Oid) + require.Equal(t, "atNetAddress", tbl.Fields[4].Name) + require.True(t, tbl.Fields[4].IsTag) + require.Empty(t, tbl.Fields[4].Conversion) } // TestTableBuild_walk in snmp_test.go is split into two tests here, @@ -259,13 +134,7 @@ func TestTableBuild_walk_noTranslate(t *testing.T) { }, } - 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) + tb, err := tbl.Build(tsc, true) require.NoError(t, err) require.Equal(t, "mytable", tb.Name) rtr1 := RTableRow{ @@ -317,12 +186,6 @@ func TestTableBuild_walk_noTranslate(t *testing.T) { } 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, @@ -345,9 +208,8 @@ func TestTableBuild_walk_Translate(t *testing.T) { }, } - err = tbl.Init(tr) - require.NoError(t, err) - tb, err := tbl.Build(gosmiTsc, true, tr) + require.NoError(t, tbl.Init(getGosmiTr(t))) + tb, err := tbl.Build(tsc, true) require.NoError(t, err) require.Equal(t, "atTable", tb.Name) @@ -387,12 +249,6 @@ func TestTableBuild_walk_Translate(t *testing.T) { } 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{ @@ -421,7 +277,7 @@ func TestTableBuild_noWalkGosmi(t *testing.T) { }, } - tb, err := tbl.Build(gosmiTsc, false, tr) + tb, err := tbl.Build(tsc, false) require.NoError(t, err) rtr := RTableRow{ @@ -432,103 +288,6 @@ func TestTableBuild_noWalkGosmi(t *testing.T) { require.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] - 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)) - - 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"]) -} - -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] - require.Equal(t, "baz", m.Tags["host"]) -} - func TestFieldConvertGosmi(t *testing.T) { testTable := []struct { input interface{} @@ -585,77 +344,18 @@ func TestFieldConvertGosmi(t *testing.T) { } for _, tc := range testTable { - act, err := fieldConvert(getGosmiTr(t), tc.conv, gosnmp.SnmpPDU{Name: ".1.3.6.1.2.1.2.2.1.8", Value: tc.input}) + f := Field{ + Name: "test", + Conversion: tc.conv, + } + require.NoError(t, f.Init(getGosmiTr(t))) + + act, err := f.Convert(gosnmp.SnmpPDU{Name: ".1.3.6.1.2.1.2.2.1.8", Value: 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) } } -func TestSnmpTranslateCache_missGosmi(t *testing.T) { - gosmiSnmpTranslateCaches = nil - oid := "IF-MIB::ifPhysAddress.1" - mibName, oidNum, oidText, conversion, err := getGosmiTr(t).SnmpTranslate(oid) - require.Len(t, gosmiSnmpTranslateCaches, 1) - stc := gosmiSnmpTranslateCaches[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) -} - -func TestSnmpTranslateCache_hitGosmi(t *testing.T) { - gosmiSnmpTranslateCaches = map[string]gosmiSnmpTranslateCache{ - "foo": { - mibName: "a", - oidNum: "b", - oidText: "c", - conversion: "d", - err: errors.New("e"), - }, - } - mibName, oidNum, oidText, conversion, err := getGosmiTr(t).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, errors.New("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) - require.Len(t, gosmiSnmpTableCaches, 1) - stc := gosmiSnmpTableCaches[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) -} - -func TestSnmpTableCache_hitGosmi(t *testing.T) { - gosmiSnmpTableCaches = map[string]gosmiSnmpTableCache{ - "foo": { - mibName: "a", - oidNum: "b", - oidText: "c", - fields: []Field{{Name: "d"}}, - err: errors.New("e"), - }, - } - mibName, oidNum, oidText, fields, err := getGosmiTr(t).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, errors.New("e"), err) -} - func TestTableJoin_walkGosmi(t *testing.T) { tbl := Table{ Name: "mytable", @@ -689,13 +389,8 @@ func TestTableJoin_walkGosmi(t *testing.T) { }, } - 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, tbl.Init(getGosmiTr(t))) + tb, err := tbl.Build(tsc, true) require.NoError(t, err) require.Equal(t, "mytable", tb.Name) @@ -772,13 +467,7 @@ func TestTableOuterJoin_walkGosmi(t *testing.T) { }, } - 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) + tb, err := tbl.Build(tsc, true) require.NoError(t, err) require.Equal(t, "mytable", tb.Name) @@ -864,13 +553,7 @@ func TestTableJoinNoIndexAsTag_walkGosmi(t *testing.T) { }, } - 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) + tb, err := tbl.Build(tsc, true) require.NoError(t, err) require.Equal(t, "mytable", tb.Name) @@ -913,33 +596,91 @@ func TestTableJoinNoIndexAsTag_walkGosmi(t *testing.T) { 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::"}, + tr := getGosmiTr(t) + f := Field{ + Oid: "RFC1213-MIB::", + } + + require.Error(t, f.Init(tr)) +} + +func TestTrapLookup(t *testing.T) { + tests := []struct { + name string + oid string + expected MibEntry + }{ + { + name: "Known trap OID", + oid: ".1.3.6.1.6.3.1.1.5.1", + expected: MibEntry{ + MibName: "TGTEST-MIB", + OidText: "coldStart", + }, }, - ClientConfig: snmp.ClientConfig{ - Path: []string{"testdata"}, - Translator: "gosmi", + { + name: "Known trap value OID", + oid: ".1.3.6.1.2.1.1.3.0", + expected: MibEntry{ + MibName: "TGTEST-MIB", + OidText: "sysUpTimeInstance", + }, + }, + { + name: "Unknown enterprise sub-OID", + oid: ".1.3.6.1.4.1.0.1.2.3", + expected: MibEntry{ + MibName: "SNMPv2-SMI", + OidText: "enterprises.0.1.2.3", + }, + }, + { + name: "Unknown MIB", + oid: ".1.999", + expected: MibEntry{OidText: "iso.999"}, }, } - err := s.Init() - require.Error(t, err) + // Load the MIBs + getGosmiTr(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Run the actual test + actual, err := TrapLookup(tt.oid) + require.NoError(t, err) + require.Equal(t, tt.expected, actual) + }) + } } -func TestMissingMibPath(t *testing.T) { - log := testutil.Logger{} - path := []string{"non-existing-directory"} - err := snmp.LoadMibsFromPath(path, log, &snmp.GosmiMibLoader{}) - require.NoError(t, err) +func TestTrapLookupFail(t *testing.T) { + tests := []struct { + name string + oid string + expected string + }{ + { + name: "New top level OID", + oid: ".3.6.1.3.0", + expected: "Could not find node for OID 3.6.1.3.0", + }, + { + name: "Malformed OID", + oid: ".1.3.dod.1.3.0", + expected: "could not convert OID .1.3.dod.1.3.0: strconv.ParseUint: parsing \"dod\": invalid syntax", + }, + } + + // Load the MIBs + getGosmiTr(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Run the actual test + _, err := TrapLookup(tt.oid) + require.EqualError(t, err, tt.expected) + }) + } } diff --git a/plugins/inputs/snmp/netsnmp.go b/internal/snmp/translator_netsnmp.go similarity index 88% rename from plugins/inputs/snmp/netsnmp.go rename to internal/snmp/translator_netsnmp.go index 6943fc511..66f2e463d 100644 --- a/plugins/inputs/snmp/netsnmp.go +++ b/internal/snmp/translator_netsnmp.go @@ -5,21 +5,21 @@ import ( "bytes" "errors" "fmt" - "log" //nolint:depguard // Allow exceptional but valid use of log here. "os/exec" "strings" "sync" - "github.com/influxdata/wlog" + "github.com/influxdata/telegraf" ) // struct that implements the translator interface. This calls existing // code to exec netsnmp's snmptranslate program type netsnmpTranslator struct { + log telegraf.Logger } -func NewNetsnmpTranslator() *netsnmpTranslator { - return &netsnmpTranslator{} +func NewNetsnmpTranslator(log telegraf.Logger) *netsnmpTranslator { + return &netsnmpTranslator{log: log} } type snmpTableCache struct { @@ -35,14 +35,12 @@ 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, " ")) +func (n *netsnmpTranslator) execCmd(arg0 string, args ...string) ([]byte, error) { + quoted := make([]string, 0, len(args)) + for _, arg := range args { + quoted = append(quoted, fmt.Sprintf("%q", arg)) } + n.log.Debugf("executing %q %s", arg0, strings.Join(quoted, " ")) out, err := execCommand(arg0, args...).Output() if err != nil { @@ -98,7 +96,7 @@ func (n *netsnmpTranslator) snmpTableCall(oid string) ( // 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 { + if out, err := n.execCmd("snmptranslate", "-Td", oidFullName+".1"); err == nil { scanner := bufio.NewScanner(bytes.NewBuffer(out)) for scanner.Scan() { line := scanner.Text() @@ -124,7 +122,7 @@ func (n *netsnmpTranslator) snmpTableCall(oid string) ( } // 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) + out, err := n.execCmd("snmptable", "-Ch", "-Cl", "-c", "public", "127.0.0.1", oidFullName) if err != nil { return "", "", "", nil, fmt.Errorf("getting table columns: %w", err) } @@ -179,7 +177,7 @@ func (n *netsnmpTranslator) SnmpTranslate(oid string) ( // 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) + stc.mibName, stc.oidNum, stc.oidText, stc.conversion, stc.err = n.snmpTranslateCall(oid) snmpTranslateCaches[oid] = stc } @@ -189,12 +187,12 @@ func (n *netsnmpTranslator) SnmpTranslate(oid string) ( } //nolint:revive //function-result-limit conditionally 5 return results allowed -func snmpTranslateCall(oid string) (mibName string, oidNum string, oidText string, conversion string, err error) { +func (n *netsnmpTranslator) 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) + out, err = n.execCmd("snmptranslate", "-Td", "-Ob", oid) } else { - out, err = execCmd("snmptranslate", "-Td", "-Ob", "-m", "all", oid) + out, err = n.execCmd("snmptranslate", "-Td", "-Ob", "-m", "all", oid) var execErr *exec.Error if errors.As(err, &execErr) && errors.Is(execErr, exec.ErrNotFound) { // Silently discard error if snmptranslate not found and we have a numeric OID. diff --git a/plugins/inputs/snmp/snmp_mocks_generate.go b/internal/snmp/translator_netsnmp_mocks_generate.go similarity index 97% rename from plugins/inputs/snmp/snmp_mocks_generate.go rename to internal/snmp/translator_netsnmp_mocks_generate.go index f77e56901..7ca46ff29 100644 --- a/plugins/inputs/snmp/snmp_mocks_generate.go +++ b/internal/snmp/translator_netsnmp_mocks_generate.go @@ -98,5 +98,5 @@ func generate() error { f.Write([]byte("}\n")) f.Close() - return exec.Command("gofmt", "-w", "snmp_mocks_test.go").Run() + return exec.Command("gofmt", "-w", "translator_netsnmp_mocks_test.go").Run() } diff --git a/plugins/inputs/snmp/snmp_mocks_test.go b/internal/snmp/translator_netsnmp_mocks_test.go similarity index 100% rename from plugins/inputs/snmp/snmp_mocks_test.go rename to internal/snmp/translator_netsnmp_mocks_test.go diff --git a/internal/snmp/translator_netsnmp_test.go b/internal/snmp/translator_netsnmp_test.go new file mode 100644 index 000000000..a5c88132b --- /dev/null +++ b/internal/snmp/translator_netsnmp_test.go @@ -0,0 +1,352 @@ +//go:generate go run -tags generate translator_netsnmp_mocks_generate.go +package snmp + +import ( + "testing" + + "github.com/gosnmp/gosnmp" + "github.com/stretchr/testify/require" + + "github.com/influxdata/telegraf/testutil" +) + +func TestFieldInit(t *testing.T) { + 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", ""}, + {".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"}, + } + + tr := NewNetsnmpTranslator(testutil.Logger{}) + for _, txl := range translations { + f := Field{Oid: txl.inputOid, Name: txl.inputName, Conversion: txl.inputConversion} + err := f.Init(tr) + require.NoError(t, err, "inputOid=%q inputName=%q", txl.inputOid, txl.inputName) + require.Equal(t, txl.expectedOid, f.Oid, "inputOid=%q inputName=%q inputConversion=%q", txl.inputOid, txl.inputName, txl.inputConversion) + require.Equal(t, txl.expectedName, f.Name, "inputOid=%q inputName=%q inputConversion=%q", txl.inputOid, txl.inputName, txl.inputConversion) + } +} + +func TestTableInit(t *testing.T) { + tbl := Table{ + Oid: ".1.0.0.0", + Fields: []Field{ + {Oid: ".999", Name: "foo"}, + {Oid: "TEST::description", Name: "description", IsTag: true}, + }, + } + err := tbl.Init(NewNetsnmpTranslator(testutil.Logger{})) + require.NoError(t, err) + + require.Equal(t, "testTable", tbl.Name) + + require.Len(t, tbl.Fields, 5) + + require.Equal(t, ".999", tbl.Fields[0].Oid) + require.Equal(t, "foo", tbl.Fields[0].Name) + require.False(t, tbl.Fields[0].IsTag) + require.Empty(t, tbl.Fields[0].Conversion) + + require.Equal(t, ".1.0.0.0.1.1", tbl.Fields[2].Oid) + require.Equal(t, "server", tbl.Fields[2].Name) + require.True(t, tbl.Fields[1].IsTag) + require.Empty(t, tbl.Fields[1].Conversion) + + require.Equal(t, ".1.0.0.0.1.2", tbl.Fields[3].Oid) + require.Equal(t, "connections", tbl.Fields[3].Name) + require.False(t, tbl.Fields[3].IsTag) + require.Empty(t, tbl.Fields[3].Conversion) + + require.Equal(t, ".1.0.0.0.1.3", tbl.Fields[4].Oid) + require.Equal(t, "latency", tbl.Fields[4].Name) + require.False(t, tbl.Fields[4].IsTag) + require.Empty(t, tbl.Fields[4].Conversion) + + require.Equal(t, ".1.0.0.0.1.4", tbl.Fields[1].Oid) + require.Equal(t, "description", tbl.Fields[1].Name) + require.True(t, tbl.Fields[1].IsTag) + require.Empty(t, tbl.Fields[1].Conversion) +} + +func TestTableBuild_walk(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, + }, + { + Name: "myfield6", + Oid: ".1.0.0.0.1.6", + Translate: true, + }, + { + Name: "myfield7", + Oid: ".1.0.0.0.1.6", + Translate: false, + }, + }, + } + + require.NoError(t, tbl.Init(NewNetsnmpTranslator(testutil.Logger{}))) + + tb, err := tbl.Build(tsc, true) + require.NoError(t, err) + + require.Equal(t, "mytable", tb.Name) + rtr1 := RTableRow{ + Tags: map[string]string{ + "myfield1": "foo", + "index": "0", + }, + Fields: map[string]interface{}{ + "myfield2": 1, + "myfield3": float64(0.123), + "myfield4": 11, + "myfield5": 11, + "myfield6": "testTableEntry.7", + "myfield7": ".1.0.0.0.1.7", + }, + } + 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), + }, + } + 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_noWalk(t *testing.T) { + tbl := Table{ + Name: "mytable", + Fields: []Field{ + { + Name: "myfield1", + Oid: ".1.0.0.1.1", + IsTag: true, + }, + { + Name: "myfield2", + Oid: ".1.0.0.1.2", + }, + { + Name: "myfield3", + Oid: ".1.0.0.1.2", + IsTag: true, + }, + { + Name: "empty", + Oid: ".1.0.0.0.1.1.2", + }, + { + Name: "noexist", + Oid: ".1.2.3.4.5", + }, + }, + } + + tb, err := tbl.Build(tsc, false) + require.NoError(t, err) + + rtr := RTableRow{ + Tags: map[string]string{"myfield1": "baz", "myfield3": "234"}, + Fields: map[string]interface{}{"myfield2": 234}, + } + require.Len(t, tb.Rows, 1) + require.Contains(t, tb.Rows, rtr) +} + +func TestFieldConvert(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)}, + {int(123), "int", int64(123)}, + {int8(123), "int", int64(123)}, + {int16(123), "int", int64(123)}, + {int32(123), "int", int64(123)}, + {int64(123), "int", int64(123)}, + {uint(123), "int", int64(123)}, + {uint8(123), "int", int64(123)}, + {uint16(123), "int", int64(123)}, + {uint32(123), "int", int64(123)}, + {uint64(123), "int", int64(123)}, + {[]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 { + f := Field{ + Name: "test", + Conversion: tc.conv, + } + require.NoError(t, f.Init(NewNetsnmpTranslator(testutil.Logger{}))) + + act, err := f.Convert(gosnmp.SnmpPDU{Value: 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) + } +} + +func TestSnmpTranslateCache_miss(t *testing.T) { + snmpTranslateCaches = nil + oid := "IF-MIB::ifPhysAddress.1" + mibName, oidNum, oidText, conversion, err := NewNetsnmpTranslator(testutil.Logger{}).SnmpTranslate(oid) + require.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) +} + +func TestSnmpTranslateCache_hit(t *testing.T) { + snmpTranslateCaches = map[string]snmpTranslateCache{ + "foo": { + mibName: "a", + oidNum: "b", + oidText: "c", + conversion: "d", + }, + } + mibName, oidNum, oidText, conversion, err := NewNetsnmpTranslator(testutil.Logger{}).SnmpTranslate("foo") + require.Equal(t, "a", mibName) + require.Equal(t, "b", oidNum) + require.Equal(t, "c", oidText) + require.Equal(t, "d", conversion) + require.NoError(t, err) + snmpTranslateCaches = nil +} + +func TestSnmpTableCache_miss(t *testing.T) { + snmpTableCaches = nil + oid := ".1.0.0.0" + mibName, oidNum, oidText, fields, err := NewNetsnmpTranslator(testutil.Logger{}).SnmpTable(oid) + require.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) +} + +func TestSnmpTableCache_hit(t *testing.T) { + snmpTableCaches = map[string]snmpTableCache{ + "foo": { + mibName: "a", + oidNum: "b", + oidText: "c", + fields: []Field{{Name: "d"}}, + }, + } + mibName, oidNum, oidText, fields, err := NewNetsnmpTranslator(testutil.Logger{}).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.NoError(t, err) +} diff --git a/internal/snmp/wrapper.go b/internal/snmp/wrapper.go index a6082e463..8cf306110 100644 --- a/internal/snmp/wrapper.go +++ b/internal/snmp/wrapper.go @@ -11,6 +11,16 @@ import ( "github.com/gosnmp/gosnmp" ) +// Connection is an interface which wraps a *gosnmp.GoSNMP object. +// We interact through an interface so we can mock it out in tests. +type Connection interface { + Host() string + //BulkWalkAll(string) ([]gosnmp.SnmpPDU, error) + Walk(string, gosnmp.WalkFunc) error + Get(oids []string) (*gosnmp.SnmpPacket, error) + Reconnect() error +} + // GosnmpWrapper wraps a *gosnmp.GoSNMP object so we can use it as a snmpConnection. type GosnmpWrapper struct { *gosnmp.GoSNMP diff --git a/internal/snmp/wrapper_test.go b/internal/snmp/wrapper_test.go new file mode 100644 index 000000000..10a4896a1 --- /dev/null +++ b/internal/snmp/wrapper_test.go @@ -0,0 +1,89 @@ +package snmp + +import "github.com/gosnmp/gosnmp" + +type testSNMPConnection struct { + host string + values map[string]interface{} +} + +func (tsc *testSNMPConnection) Host() string { + return tsc.host +} + +func (tsc *testSNMPConnection) Get(oids []string) (*gosnmp.SnmpPacket, error) { + sp := &gosnmp.SnmpPacket{} + for _, oid := range oids { + v, ok := tsc.values[oid] + if !ok { + sp.Variables = append(sp.Variables, gosnmp.SnmpPDU{ + Name: oid, + Type: gosnmp.NoSuchObject, + }) + continue + } + sp.Variables = append(sp.Variables, gosnmp.SnmpPDU{ + Name: oid, + Value: v, + }) + } + return sp, nil +} +func (tsc *testSNMPConnection) Walk(oid string, wf gosnmp.WalkFunc) error { + for void, v := range tsc.values { + if void == oid || (len(void) > len(oid) && void[:len(oid)+1] == oid+".") { + if err := wf(gosnmp.SnmpPDU{ + Name: void, + Value: v, + }); err != nil { + return err + } + } + } + return nil +} +func (tsc *testSNMPConnection) Reconnect() error { + return nil +} + +var tsc = &testSNMPConnection{ + host: "tsc", + values: map[string]interface{}{ + ".1.0.0.0.1.1.0": "foo", + ".1.0.0.0.1.1.1": []byte("bar"), + ".1.0.0.0.1.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.0.1.6.0": ".1.0.0.0.1.7", + ".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.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.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, + }, +} diff --git a/plugins/inputs/snmp/gosmi.go b/plugins/inputs/snmp/gosmi.go deleted file mode 100644 index 50d2a84b9..000000000 --- a/plugins/inputs/snmp/gosmi.go +++ /dev/null @@ -1,143 +0,0 @@ -package snmp - -import ( - "fmt" - "sync" - - "github.com/sleepinggenius2/gosmi" - "github.com/sleepinggenius2/gosmi/models" - - "github.com/influxdata/telegraf" - "github.com/influxdata/telegraf/internal/snmp" -) - -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 //function-result-limit conditionally 5 return results allowed -func (g *gosmiTranslator) SnmpTranslate(oid string) (mibName string, oidNum string, oidText string, conversion string, err error) { - mibName, oidNum, oidText, conversion, _, err = g.SnmpTranslateFull(oid) - return mibName, oidNum, oidText, conversion, err -} - -//nolint:revive //function-result-limit conditionally 6 return results allowed -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 := snmp.GetIndex(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, nil -} - -func (g *gosmiTranslator) SnmpFormatEnum(oid string, value interface{}, full bool) (string, error) { - //nolint:dogsled // only need to get the node - _, _, _, _, node, err := g.SnmpTranslateFull(oid) - - if err != nil { - return "", err - } - - var v models.Value - if full { - v = node.FormatValue(value, models.FormatEnumName, models.FormatEnumValue) - } else { - v = node.FormatValue(value, models.FormatEnumName) - } - - return v.Formatted, nil -} diff --git a/plugins/inputs/snmp/snmp.go b/plugins/inputs/snmp/snmp.go index 4a571bc9a..8f9df2626 100644 --- a/plugins/inputs/snmp/snmp.go +++ b/plugins/inputs/snmp/snmp.go @@ -3,18 +3,11 @@ package snmp import ( _ "embed" - "encoding/binary" "errors" "fmt" - "math" - "net" - "strconv" - "strings" "sync" "time" - "github.com/gosnmp/gosnmp" - "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/internal/snmp" @@ -25,25 +18,6 @@ import ( //go:embed sample.conf var sampleConfig string -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, - ) - - SnmpFormatEnum(oid string, value interface{}, full bool) ( - formatted string, - err error, - ) -} - // Snmp holds the configuration for the plugin. type Snmp struct { // The SNMP agent to query. Format is [SCHEME://]ADDR[:PORT] (e.g. @@ -55,19 +29,19 @@ type Snmp struct { snmp.ClientConfig - Tables []Table `toml:"table"` + Tables []snmp.Table `toml:"table"` // Name & Fields are the elements of a Table. // Telegraf chokes if we try to embed a Table. So instead we have to embed the // fields of a Table, and construct a Table during runtime. - Name string `toml:"name"` - Fields []Field `toml:"field"` + Name string `toml:"name"` + Fields []snmp.Field `toml:"field"` - connectionCache []snmpConnection + connectionCache []snmp.Connection Log telegraf.Logger `toml:"-"` - translator Translator + translator snmp.Translator } func (s *Snmp) SetTranslator(name string) { @@ -82,17 +56,17 @@ func (s *Snmp) Init() error { var err error switch s.Translator { case "gosmi": - s.translator, err = NewGosmiTranslator(s.Path, s.Log) + s.translator, err = snmp.NewGosmiTranslator(s.Path, s.Log) if err != nil { return err } case "netsnmp": - s.translator = NewNetsnmpTranslator() + s.translator = snmp.NewNetsnmpTranslator(s.Log) default: return errors.New("invalid translator value") } - s.connectionCache = make([]snmpConnection, len(s.Agents)) + s.connectionCache = make([]snmp.Connection, len(s.Agents)) for i := range s.Tables { if err := s.Tables[i].Init(s.translator); err != nil { @@ -101,7 +75,7 @@ func (s *Snmp) Init() error { } for i := range s.Fields { - if err := s.Fields[i].init(s.translator); err != nil { + if err := s.Fields[i].Init(s.translator); err != nil { return fmt.Errorf("initializing field %s: %w", s.Fields[i].Name, err) } } @@ -119,200 +93,6 @@ func (s *Snmp) Init() error { return nil } -// Table holds the configuration for a SNMP table. -type Table struct { - // Name will be the name of the measurement. - Name string - - // Which tags to inherit from the top-level config. - InheritTags []string - - // Adds each row's table index as a tag. - IndexAsTag bool - - // Fields is the tags and values to look up. - Fields []Field `toml:"field"` - - // OID for automatic field population. - // If provided, init() will populate Fields with all the table columns of the - // given OID. - Oid string - - initialized bool -} - -// Init() builds & initializes the nested fields. -func (t *Table) Init(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 == "" { - return errors.New("SNMP table in config file is not named. One or both of the oid and name settings must be set") - } - - if t.initialized { - return nil - } - - if err := t.initBuild(tr); err != nil { - return err - } - - secondaryIndexTablePresent := false - // initialize all the nested fields - for i := range t.Fields { - 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 { - if secondaryIndexTablePresent { - return errors.New("only one field can be SecondaryIndexTable") - } - secondaryIndexTablePresent = true - } - } - - t.initialized = true - return nil -} - -// initBuild initializes the table if it has an OID configured. If so, the -// net-snmp tools will be used to look up the OID and auto-populate the table's -// fields. -func (t *Table) initBuild(tr Translator) error { - if t.Oid == "" { - return nil - } - - _, _, oidText, fields, err := tr.SnmpTable(t.Oid) - if err != nil { - return err - } - - if t.Name == "" { - t.Name = oidText - } - - knownOIDs := map[string]bool{} - for _, f := range t.Fields { - knownOIDs[f.Oid] = true - } - for _, f := range fields { - if !knownOIDs[f.Oid] { - t.Fields = append(t.Fields, f) - } - } - - return nil -} - -// Field holds the configuration for a Field to look up. -type Field struct { - // Name will be the name of the field. - Name string - // OID is prefix for this field. The plugin will perform a walk through all - // OIDs with this as their parent. For each value found, the plugin will strip - // off the OID prefix, and use the remainder as the index. For multiple fields - // to show up in the same row, they must share the same index. - Oid string - // OidIndexSuffix is the trailing sub-identifier on a table record OID that will be stripped off to get the record's index. - OidIndexSuffix string - // OidIndexLength specifies the length of the index in OID path segments. It can be used to remove sub-identifiers that vary in content or length. - OidIndexLength int - // IsTag controls whether this OID is output as a tag or a value. - IsTag bool - // Conversion controls any type conversion that is done on the value. - // "float"/"float(0)" will convert the value into a float. - // "float(X)" will convert the value into a float, and then move the decimal before Xth right-most digit. - // "int" will convert the value into an integer. - // "hwaddr" will convert a 6-byte string to a MAC address. - // "ipaddr" will convert the value to an IPv4 or IPv6 address. - // "enum"/"enum(1)" will convert the value according to its syntax. (Only supported with gosmi translator) - Conversion string - // Translate tells if the value of the field should be snmptranslated - Translate bool - // Secondary index table allows to merge data from two tables with different index - // that this filed will be used to join them. There can be only one secondary index table. - SecondaryIndexTable bool - // This field is using secondary index, and will be later merged with primary index - // using SecondaryIndexTable. SecondaryIndexTable and SecondaryIndexUse are exclusive. - SecondaryIndexUse bool - // Controls if entries from secondary table should be added or not if joining - // index is present or not. I set to true, means that join is outer, and - // index is prepended with "Secondary." for missing values to avoid overlapping - // indexes from both tables. - // Can be set per field or globally with SecondaryIndexTable, global true overrides - // per field false. - SecondaryOuterJoin bool - - initialized bool -} - -// init() converts OID names to numbers, and sets the .Name attribute if unset. -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 := tr.SnmpTranslate(f.Oid) - if err != nil { - return fmt.Errorf("translating: %w", err) - } - f.Oid = oidNum - if f.Name == "" { - f.Name = oidText - } - if f.Conversion == "" { - f.Conversion = conversion - } - //TODO use textual convention conversion from the MIB - } - - if f.SecondaryIndexTable && f.SecondaryIndexUse { - return errors.New("SecondaryIndexTable and UseSecondaryIndex are exclusive") - } - - if !f.SecondaryIndexTable && !f.SecondaryIndexUse && f.SecondaryOuterJoin { - return errors.New("SecondaryOuterJoin set to true, but field is not being used in join") - } - - f.initialized = true - return nil -} - -// RTable is the resulting table built from a Table. -type RTable struct { - // Name is the name of the field, copied from Table.Name. - Name string - // Time is the time the table was built. - Time time.Time - // Rows are the rows that were found, one row for each table OID index found. - Rows []RTableRow -} - -// RTableRow is the resulting row containing all the OID values which shared -// the same index. -type RTableRow struct { - // Tags are all the Field values which had IsTag=true. - Tags map[string]string - // Fields are all the Field values which had IsTag=false. - Fields map[string]interface{} -} - -type walkError struct { - msg string - err error -} - -func (e *walkError) Error() string { - return e.msg -} - -func (e *walkError) Unwrap() error { - return e.err -} - // Gather retrieves all the configured fields and tables. // Any error encountered does not halt the process. The errors are accumulated // and returned at the end. @@ -329,7 +109,7 @@ func (s *Snmp) Gather(acc telegraf.Accumulator) error { } // First is the top-level fields. We treat the fields as table prefixes with an empty index. - t := Table{ + t := snmp.Table{ Name: s.Name, Fields: s.Fields, } @@ -351,8 +131,8 @@ func (s *Snmp) Gather(acc telegraf.Accumulator) error { return nil } -func (s *Snmp) gatherTable(acc telegraf.Accumulator, gs snmpConnection, t Table, topTags map[string]string, walk bool) error { - rt, err := t.Build(gs, walk, s.translator) +func (s *Snmp) gatherTable(acc telegraf.Accumulator, gs snmp.Connection, t snmp.Table, topTags map[string]string, walk bool) error { + rt, err := t.Build(gs, walk) if err != nil { return err } @@ -380,206 +160,11 @@ func (s *Snmp) gatherTable(acc telegraf.Accumulator, gs snmpConnection, t Table, return nil } -// Build retrieves all the fields specified in the table and constructs the RTable. -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) - secIdxTab := make(map[string]string) - secGlobalOuterJoin := false - for i, f := range t.Fields { - if f.SecondaryIndexTable { - secGlobalOuterJoin = f.SecondaryOuterJoin - if i != 0 { - t.Fields[0], t.Fields[i] = t.Fields[i], t.Fields[0] - } - break - } - } - - tagCount := 0 - for _, f := range t.Fields { - if f.IsTag { - tagCount++ - } - - if len(f.Oid) == 0 { - return nil, fmt.Errorf("cannot have empty OID on field %s", f.Name) - } - var oid string - if f.Oid[0] == '.' { - oid = f.Oid - } else { - // make sure OID has "." because the BulkWalkAll results do, and the prefix needs to match - oid = "." + f.Oid - } - - // ifv contains a mapping of table OID index to field value - ifv := map[string]interface{}{} - - if !walk { - // This is used when fetching non-table fields. Fields configured a the top - // scope of the plugin. - // We fetch the fields directly, and add them to ifv as if the index were an - // empty string. This results in all the non-table fields sharing the same - // index, and being added on the same row. - if pkt, err := gs.Get([]string{oid}); err != nil { - if errors.Is(err, gosnmp.ErrUnknownSecurityLevel) { - return nil, errors.New("unknown security level (sec_level)") - } else if errors.Is(err, gosnmp.ErrUnknownUsername) { - return nil, errors.New("unknown username (sec_name)") - } else if errors.Is(err, gosnmp.ErrWrongDigest) { - return nil, errors.New("wrong digest (auth_protocol, auth_password)") - } else if errors.Is(err, gosnmp.ErrDecryption) { - return nil, errors.New("decryption error (priv_protocol, priv_password)") - } - return nil, fmt.Errorf("performing get on field %s: %w", f.Name, err) - } else if pkt != nil && len(pkt.Variables) > 0 && pkt.Variables[0].Type != gosnmp.NoSuchObject && pkt.Variables[0].Type != gosnmp.NoSuchInstance { - ent := pkt.Variables[0] - fv, err := fieldConvert(tr, f.Conversion, ent) - if err != nil { - return nil, fmt.Errorf("converting %q (OID %s) for field %s: %w", ent.Value, ent.Name, f.Name, err) - } - ifv[""] = fv - } - } else { - err := gs.Walk(oid, func(ent gosnmp.SnmpPDU) error { - if len(ent.Name) <= len(oid) || ent.Name[:len(oid)+1] != oid+"." { - return &walkError{} // break the walk - } - - idx := ent.Name[len(oid):] - if f.OidIndexSuffix != "" { - if !strings.HasSuffix(idx, f.OidIndexSuffix) { - // this entry doesn't match our OidIndexSuffix. skip it - return nil - } - idx = idx[:len(idx)-len(f.OidIndexSuffix)] - } - if f.OidIndexLength != 0 { - i := f.OidIndexLength + 1 // leading separator - idx = strings.Map(func(r rune) rune { - if r == '.' { - i-- - } - if i < 1 { - return -1 - } - return r - }, idx) - } - - // snmptranslate table field value here - if f.Translate { - if entOid, ok := ent.Value.(string); ok { - _, _, oidText, _, err := tr.SnmpTranslate(entOid) - if err == nil { - // If no error translating, the original value for ent.Value should be replaced - ent.Value = oidText - } - } - } - - fv, err := fieldConvert(tr, f.Conversion, ent) - if err != nil { - return &walkError{ - msg: fmt.Sprintf("converting %q (OID %s) for field %s", ent.Value, ent.Name, f.Name), - err: err, - } - } - ifv[idx] = fv - return nil - }) - if err != nil { - // Our callback always wraps errors in a walkError. - // If this error isn't a walkError, we know it's not - // from the callback - var walkErr *walkError - if !errors.As(err, &walkErr) { - return nil, fmt.Errorf("performing bulk walk for field %s: %w", f.Name, err) - } - } - } - - for idx, v := range ifv { - if f.SecondaryIndexUse { - if newidx, ok := secIdxTab[idx]; ok { - idx = newidx - } else { - if !secGlobalOuterJoin && !f.SecondaryOuterJoin { - continue - } - idx = ".Secondary" + idx - } - } - rtr, ok := rows[idx] - if !ok { - rtr = RTableRow{} - rtr.Tags = map[string]string{} - rtr.Fields = map[string]interface{}{} - rows[idx] = rtr - } - if t.IndexAsTag && idx != "" { - if idx[0] == '.' { - idx = idx[1:] - } - rtr.Tags["index"] = idx - } - // don't add an empty string - if vs, ok := v.(string); !ok || vs != "" { - if f.IsTag { - if ok { - rtr.Tags[f.Name] = vs - } else { - rtr.Tags[f.Name] = fmt.Sprintf("%v", v) - } - } else { - rtr.Fields[f.Name] = v - } - if f.SecondaryIndexTable { - //indexes are stored here with prepending "." so we need to add them if needed - var vss string - if ok { - vss = "." + vs - } else { - vss = fmt.Sprintf(".%v", v) - } - if idx[0] == '.' { - secIdxTab[vss] = idx - } else { - secIdxTab[vss] = "." + idx - } - } - } - } - } - - rt := RTable{ - Name: t.Name, - Time: time.Now(), //TODO record time at start - Rows: make([]RTableRow, 0, len(rows)), - } - for _, r := range rows { - rt.Rows = append(rt.Rows, r) - } - return &rt, nil -} - -// snmpConnection is an interface which wraps a *gosnmp.GoSNMP object. -// We interact through an interface so we can mock it out in tests. -type snmpConnection interface { - Host() string - //BulkWalkAll(string) ([]gosnmp.SnmpPDU, error) - Walk(string, gosnmp.WalkFunc) error - Get(oids []string) (*gosnmp.SnmpPacket, error) - Reconnect() error -} - // getConnection creates a snmpConnection (*gosnmp.GoSNMP) object and caches the // result using `agentIndex` as the cache key. This is done to allow multiple // connections to a single address. It is an error to use a connection in // more than one goroutine. -func (s *Snmp) getConnection(idx int) (snmpConnection, error) { +func (s *Snmp) getConnection(idx int) (snmp.Connection, error) { if gs := s.connectionCache[idx]; gs != nil { if err := gs.Reconnect(); err != nil { return gs, fmt.Errorf("reconnecting: %w", err) @@ -590,9 +175,7 @@ func (s *Snmp) getConnection(idx int) (snmpConnection, error) { agent := s.Agents[idx] - var err error - var gs snmp.GosnmpWrapper - gs, err = snmp.NewWrapper(s.ClientConfig) + gs, err := snmp.NewWrapper(s.ClientConfig) if err != nil { return nil, err } @@ -611,173 +194,6 @@ func (s *Snmp) getConnection(idx int) (snmpConnection, error) { return gs, nil } -// fieldConvert converts from any type according to the conv specification -func fieldConvert(tr Translator, conv string, ent gosnmp.SnmpPDU) (v interface{}, err error) { - if conv == "" { - if bs, ok := ent.Value.([]byte); ok { - return string(bs), nil - } - return ent.Value, nil - } - - var d int - if _, err := fmt.Sscanf(conv, "float(%d)", &d); err == nil || conv == "float" { - v = ent.Value - switch vt := v.(type) { - case float32: - v = float64(vt) / math.Pow10(d) - case float64: - v = vt / math.Pow10(d) - case int: - v = float64(vt) / math.Pow10(d) - case int8: - v = float64(vt) / math.Pow10(d) - case int16: - v = float64(vt) / math.Pow10(d) - case int32: - v = float64(vt) / math.Pow10(d) - case int64: - v = float64(vt) / math.Pow10(d) - case uint: - v = float64(vt) / math.Pow10(d) - case uint8: - v = float64(vt) / math.Pow10(d) - case uint16: - v = float64(vt) / math.Pow10(d) - case uint32: - v = float64(vt) / math.Pow10(d) - case uint64: - v = float64(vt) / math.Pow10(d) - case []byte: - vf, _ := strconv.ParseFloat(string(vt), 64) - v = vf / math.Pow10(d) - case string: - vf, _ := strconv.ParseFloat(vt, 64) - v = vf / math.Pow10(d) - } - return v, nil - } - - if conv == "int" { - v = ent.Value - switch vt := v.(type) { - case float32: - v = int64(vt) - case float64: - v = int64(vt) - case int: - v = int64(vt) - case int8: - v = int64(vt) - case int16: - v = int64(vt) - case int32: - v = int64(vt) - case int64: - v = vt - case uint: - v = int64(vt) - case uint8: - v = int64(vt) - case uint16: - v = int64(vt) - case uint32: - v = int64(vt) - case uint64: - v = int64(vt) - case []byte: - v, _ = strconv.ParseInt(string(vt), 10, 64) - case string: - v, _ = strconv.ParseInt(vt, 10, 64) - } - return v, nil - } - - if conv == "hwaddr" { - switch vt := ent.Value.(type) { - case string: - v = net.HardwareAddr(vt).String() - case []byte: - v = net.HardwareAddr(vt).String() - default: - return nil, fmt.Errorf("invalid type (%T) for hwaddr conversion", v) - } - return v, nil - } - - split := strings.Split(conv, ":") - if split[0] == "hextoint" && len(split) == 3 { - endian := split[1] - bit := split[2] - - bv, ok := ent.Value.([]byte) - if !ok { - return ent.Value, nil - } - - switch endian { - case "LittleEndian": - switch bit { - case "uint64": - v = binary.LittleEndian.Uint64(bv) - case "uint32": - v = binary.LittleEndian.Uint32(bv) - case "uint16": - v = binary.LittleEndian.Uint16(bv) - default: - return nil, fmt.Errorf("invalid bit value (%s) for hex to int conversion", bit) - } - case "BigEndian": - switch bit { - case "uint64": - v = binary.BigEndian.Uint64(bv) - case "uint32": - v = binary.BigEndian.Uint32(bv) - case "uint16": - v = binary.BigEndian.Uint16(bv) - default: - return nil, fmt.Errorf("invalid bit value (%s) for hex to int conversion", bit) - } - default: - return nil, fmt.Errorf("invalid Endian value (%s) for hex to int conversion", endian) - } - - return v, nil - } - - if conv == "ipaddr" { - var ipbs []byte - - switch vt := ent.Value.(type) { - case string: - ipbs = []byte(vt) - case []byte: - ipbs = vt - default: - return nil, fmt.Errorf("invalid type (%T) for ipaddr conversion", v) - } - - switch len(ipbs) { - case 4, 16: - v = net.IP(ipbs).String() - default: - return nil, fmt.Errorf("invalid length (%d) for ipaddr conversion", len(ipbs)) - } - - return v, nil - } - - if conv == "enum" { - return tr.SnmpFormatEnum(ent.Name, ent.Value, false) - } - - if conv == "enum(1)" { - return tr.SnmpFormatEnum(ent.Name, ent.Value, true) - } - - return nil, fmt.Errorf("invalid conversion type %q", conv) -} - func init() { inputs.Add("snmp", func() telegraf.Input { return &Snmp{ diff --git a/plugins/inputs/snmp/snmp_test.go b/plugins/inputs/snmp/snmp_test.go index e94aae868..48abaf365 100644 --- a/plugins/inputs/snmp/snmp_test.go +++ b/plugins/inputs/snmp/snmp_test.go @@ -1,11 +1,9 @@ -//go:generate go run -tags generate snmp_mocks_generate.go package snmp import ( - "errors" "fmt" "net" - "os/exec" + "path/filepath" "sync" "testing" "time" @@ -65,135 +63,65 @@ func (tsc *testSNMPConnection) Reconnect() error { var tsc = &testSNMPConnection{ host: "tsc", values: map[string]interface{}{ - ".1.0.0.0.1.1.0": "foo", - ".1.0.0.0.1.1.1": []byte("bar"), - ".1.0.0.0.1.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.0.1.6.0": ".1.0.0.0.1.7", + ".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.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.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, }, } -func TestFieldInit(t *testing.T) { - 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", ""}, - {".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"}, - } - - tr := NewNetsnmpTranslator() - for _, txl := range translations { - f := Field{Oid: txl.inputOid, Name: txl.inputName, Conversion: txl.inputConversion} - err := f.init(tr) - require.NoError(t, err, "inputOid=%q inputName=%q", txl.inputOid, txl.inputName) - require.Equal(t, txl.expectedOid, f.Oid, "inputOid=%q inputName=%q inputConversion=%q", txl.inputOid, txl.inputName, txl.inputConversion) - require.Equal(t, txl.expectedName, f.Name, "inputOid=%q inputName=%q inputConversion=%q", txl.inputOid, txl.inputName, txl.inputConversion) - } -} - -func TestTableInit(t *testing.T) { - tbl := Table{ - Oid: ".1.0.0.0", - Fields: []Field{ - {Oid: ".999", Name: "foo"}, - {Oid: "TEST::description", Name: "description", IsTag: true}, - }, - } - err := tbl.Init(NewNetsnmpTranslator()) - require.NoError(t, err) - - require.Equal(t, "testTable", tbl.Name) - - require.Len(t, tbl.Fields, 5) - require.Contains(t, tbl.Fields, Field{Oid: ".999", Name: "foo", initialized: true}) - require.Contains(t, tbl.Fields, Field{Oid: ".1.0.0.0.1.1", Name: "server", IsTag: true, initialized: true}) - require.Contains(t, tbl.Fields, Field{Oid: ".1.0.0.0.1.2", Name: "connections", initialized: true}) - require.Contains(t, tbl.Fields, Field{Oid: ".1.0.0.0.1.3", Name: "latency", initialized: true}) - require.Contains(t, tbl.Fields, Field{Oid: ".1.0.0.0.1.4", Name: "description", IsTag: true, initialized: true}) -} - func TestSnmpInit(t *testing.T) { s := &Snmp{ - Tables: []Table{ - {Oid: "TEST::testTable"}, - }, - Fields: []Field{ - {Oid: "TEST::hostname"}, - }, ClientConfig: snmp.ClientConfig{ Translator: "netsnmp", }, } - err := s.Init() - require.NoError(t, err) - - require.Len(t, s.Tables[0].Fields, 4) - require.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.0.0.0.1.1", Name: "server", IsTag: true, initialized: true}) - require.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.0.0.0.1.2", Name: "connections", initialized: true}) - require.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.0.0.0.1.3", Name: "latency", initialized: true}) - require.Contains(t, s.Tables[0].Fields, Field{Oid: ".1.0.0.0.1.4", Name: "description", initialized: true}) - - require.Equal(t, Field{ - Oid: ".1.0.0.1.1", - Name: "hostname", - initialized: true, - }, s.Fields[0]) + require.NoError(t, s.Init()) } 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{ + Fields: []snmp.Field{ {Oid: ".1.1.1.1", Name: "one", IsTag: true}, {Oid: ".1.1.1.2", Name: "two"}, {Oid: ".1.1.1.3"}, }, - Tables: []Table{ + Tables: []snmp.Table{ {Name: "testing", - Fields: []Field{ + Fields: []snmp.Field{ {Oid: ".1.1.1.4", Name: "four", IsTag: true}, {Oid: ".1.1.1.5", Name: "five"}, {Oid: ".1.1.1.6"}, @@ -202,6 +130,7 @@ func TestSnmpInit_noTranslate(t *testing.T) { ClientConfig: snmp.ClientConfig{ Translator: "netsnmp", }, + Log: testutil.Logger{Name: "inputs.snmp"}, } err := s.Init() @@ -234,8 +163,8 @@ func TestSnmpInit_noTranslate(t *testing.T) { func TestSnmpInit_noName_noOid(t *testing.T) { s := &Snmp{ - Tables: []Table{ - {Fields: []Field{ + Tables: []snmp.Table{ + {Fields: []snmp.Field{ {Oid: ".1.1.1.4", Name: "four", IsTag: true}, {Oid: ".1.1.1.5", Name: "five"}, {Oid: ".1.1.1.6"}, @@ -243,8 +172,7 @@ func TestSnmpInit_noName_noOid(t *testing.T) { }, } - err := s.Init() - require.Error(t, err) + require.Error(t, s.Init()) } func TestGetSNMPConnection_v2(t *testing.T) { @@ -258,8 +186,7 @@ func TestGetSNMPConnection_v2(t *testing.T) { Translator: "netsnmp", }, } - err := s.Init() - require.NoError(t, err) + require.NoError(t, s.Init()) gsc, err := s.getConnection(0) require.NoError(t, err) @@ -600,147 +527,11 @@ func TestGosnmpWrapper_get_retry(t *testing.T) { require.Equal(t, (gs.Retries+1)*2, reqCount) } -func TestTableBuild_walk(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, - }, - { - 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, NewNetsnmpTranslator()) - require.NoError(t, err) - - require.Equal(t, "mytable", tb.Name) - rtr1 := RTableRow{ - Tags: map[string]string{ - "myfield1": "foo", - "index": "0", - }, - Fields: map[string]interface{}{ - "myfield2": 1, - "myfield3": float64(0.123), - "myfield4": 11, - "myfield5": 11, - "myfield6": "testTableEntry.7", - "myfield7": ".1.0.0.0.1.7", - }, - } - 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), - }, - } - 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_noWalk(t *testing.T) { - tbl := Table{ - Name: "mytable", - Fields: []Field{ - { - Name: "myfield1", - Oid: ".1.0.0.1.1", - IsTag: true, - }, - { - Name: "myfield2", - Oid: ".1.0.0.1.2", - }, - { - Name: "myfield3", - Oid: ".1.0.0.1.2", - IsTag: true, - }, - { - Name: "empty", - Oid: ".1.0.0.0.1.1.2", - }, - { - Name: "noexist", - Oid: ".1.2.3.4.5", - }, - }, - } - - 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) -} - func TestGather(t *testing.T) { s := &Snmp{ Agents: []string{"TestGather"}, Name: "mytable", - Fields: []Field{ + Fields: []snmp.Field{ { Name: "myfield1", Oid: ".1.0.0.1.1", @@ -755,11 +546,11 @@ func TestGather(t *testing.T) { Oid: "1.0.0.1.1", }, }, - Tables: []Table{ + Tables: []snmp.Table{ { Name: "myOtherTable", InheritTags: []string{"myfield1"}, - Fields: []Field{ + Fields: []snmp.Field{ { Name: "myOtherField", Oid: ".1.0.0.0.1.5", @@ -768,7 +559,7 @@ func TestGather(t *testing.T) { }, }, - connectionCache: []snmpConnection{ + connectionCache: []snmp.Connection{ tsc, }, } @@ -787,8 +578,7 @@ func TestGather(t *testing.T) { 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)) + require.WithinRange(t, m.Time, tstart, tstop) m2 := acc.Metrics[1] require.Equal(t, "myOtherTable", m2.Measurement) @@ -802,7 +592,7 @@ func TestGather_host(t *testing.T) { s := &Snmp{ Agents: []string{"TestGather"}, Name: "mytable", - Fields: []Field{ + Fields: []snmp.Field{ { Name: "host", Oid: ".1.0.0.1.1", @@ -814,7 +604,7 @@ func TestGather_host(t *testing.T) { }, }, - connectionCache: []snmpConnection{ + connectionCache: []snmp.Connection{ tsc, }, } @@ -828,366 +618,183 @@ func TestGather_host(t *testing.T) { require.Equal(t, "baz", m.Tags["host"]) } -func TestFieldConvert(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)}, - {int(123), "int", int64(123)}, - {int8(123), "int", int64(123)}, - {int16(123), "int", int64(123)}, - {int32(123), "int", int64(123)}, - {int64(123), "int", int64(123)}, - {uint(123), "int", int64(123)}, - {uint8(123), "int", int64(123)}, - {uint16(123), "int", int64(123)}, - {uint32(123), "int", int64(123)}, - {uint64(123), "int", int64(123)}, - {[]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)}, - } +func TestSnmpInitGosmi(t *testing.T) { + testDataPath, err := filepath.Abs("../../../internal/snmp/testdata/gosmi") + require.NoError(t, err) - for _, tc := range testTable { - act, err := fieldConvert(NewNetsnmpTranslator(), tc.conv, gosnmp.SnmpPDU{Value: 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) - } -} - -func TestSnmpTranslateCache_miss(t *testing.T) { - snmpTranslateCaches = nil - oid := "IF-MIB::ifPhysAddress.1" - mibName, oidNum, oidText, conversion, err := NewNetsnmpTranslator().SnmpTranslate(oid) - require.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) -} - -func TestSnmpTranslateCache_hit(t *testing.T) { - snmpTranslateCaches = map[string]snmpTranslateCache{ - "foo": { - mibName: "a", - oidNum: "b", - oidText: "c", - conversion: "d", - err: errors.New("e"), + s := &Snmp{ + Tables: []snmp.Table{ + {Oid: "RFC1213-MIB::atTable"}, + }, + Fields: []snmp.Field{ + {Oid: "RFC1213-MIB::atPhysAddress"}, + }, + ClientConfig: snmp.ClientConfig{ + Path: []string{testDataPath}, + Translator: "gosmi", }, } - mibName, oidNum, oidText, conversion, err := NewNetsnmpTranslator().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, errors.New("e"), err) - snmpTranslateCaches = nil + + require.NoError(t, s.Init()) + + require.Len(t, s.Tables[0].Fields, 3) + + require.Equal(t, ".1.3.6.1.2.1.3.1.1.1", s.Tables[0].Fields[0].Oid) + require.Equal(t, "atIfIndex", s.Tables[0].Fields[0].Name) + require.True(t, s.Tables[0].Fields[0].IsTag) + require.Empty(t, s.Tables[0].Fields[0].Conversion) + + require.Equal(t, ".1.3.6.1.2.1.3.1.1.2", s.Tables[0].Fields[1].Oid) + require.Equal(t, "atPhysAddress", s.Tables[0].Fields[1].Name) + require.False(t, s.Tables[0].Fields[1].IsTag) + require.Equal(t, "hwaddr", s.Tables[0].Fields[1].Conversion) + + require.Equal(t, ".1.3.6.1.2.1.3.1.1.3", s.Tables[0].Fields[2].Oid) + require.Equal(t, "atNetAddress", s.Tables[0].Fields[2].Name) + require.True(t, s.Tables[0].Fields[2].IsTag) + require.Empty(t, s.Tables[0].Fields[2].Conversion) + + require.Equal(t, ".1.3.6.1.2.1.3.1.1.2", s.Fields[0].Oid) + require.Equal(t, "atPhysAddress", s.Fields[0].Name) + require.False(t, s.Fields[0].IsTag) + require.Equal(t, "hwaddr", s.Fields[0].Conversion) } -func TestSnmpTableCache_miss(t *testing.T) { - snmpTableCaches = nil - oid := ".1.0.0.0" - mibName, oidNum, oidText, fields, err := NewNetsnmpTranslator().SnmpTable(oid) - require.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) -} - -func TestSnmpTableCache_hit(t *testing.T) { - snmpTableCaches = map[string]snmpTableCache{ - "foo": { - mibName: "a", - oidNum: "b", - oidText: "c", - fields: []Field{{Name: "d"}}, - err: errors.New("e"), +func TestSnmpInit_noTranslateGosmi(t *testing.T) { + s := &Snmp{ + Fields: []snmp.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: []snmp.Table{ + {Name: "testing", + Fields: []snmp.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", }, } - mibName, oidNum, oidText, fields, err := NewNetsnmpTranslator().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, errors.New("e"), err) + + require.NoError(t, s.Init()) + + require.Equal(t, ".9.1.1.1.1", s.Fields[0].Oid) + require.Equal(t, "one", s.Fields[0].Name) + require.True(t, 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.False(t, 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.False(t, 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.True(t, 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.False(t, 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.False(t, s.Tables[0].Fields[2].IsTag) } -func TestTableJoin_walk(t *testing.T) { - tbl := Table{ - Name: "mytable", - IndexAsTag: true, - Fields: []Field{ +func TestGatherGosmi(t *testing.T) { + s := &Snmp{ + Agents: []string{"TestGather"}, + Name: "mytable", + Fields: []snmp.Field{ { Name: "myfield1", - Oid: ".1.0.0.3.1.1", + Oid: ".1.0.0.1.1", IsTag: true, }, { Name: "myfield2", - Oid: ".1.0.0.3.1.2", + Oid: ".1.0.0.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, + Name: "myfield3", + Oid: "1.0.0.1.1", + }, + }, + Tables: []snmp.Table{ + { + Name: "myOtherTable", + InheritTags: []string{"myfield1"}, + Fields: []snmp.Field{ + { + Name: "myOtherField", + Oid: ".1.0.0.0.1.5", + }, + }, }, }, - } - tb, err := tbl.Build(tsc, true, NewNetsnmpTranslator()) - require.NoError(t, err) + connectionCache: []snmp.Connection{tsc}, - require.Equal(t, "mytable", tb.Name) - rtr1 := RTableRow{ - Tags: map[string]string{ - "myfield1": "instance", - "myfield4": "bar", - "index": "10", - }, - Fields: map[string]interface{}{ - "myfield2": 10, - "myfield3": 1, - "myfield5": 2, + ClientConfig: snmp.ClientConfig{ + Translator: "gosmi", }, } - 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, - }, - } - require.Len(t, tb.Rows, 3) - require.Contains(t, tb.Rows, rtr1) - require.Contains(t, tb.Rows, rtr2) - require.Contains(t, tb.Rows, rtr3) + 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] + 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.WithinRange(t, m.Time, tstart, tstop) + + 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"]) } -func TestTableOuterJoin_walk(t *testing.T) { - tbl := Table{ - Name: "mytable", - IndexAsTag: true, - Fields: []Field{ +func TestGather_hostGosmi(t *testing.T) { + s := &Snmp{ + Agents: []string{"TestGather"}, + Name: "mytable", + Fields: []snmp.Field{ { - Name: "myfield1", - Oid: ".1.0.0.3.1.1", + Name: "host", + Oid: ".1.0.0.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, + Oid: ".1.0.0.1.2", }, }, + + connectionCache: []snmp.Connection{tsc}, } - tb, err := tbl.Build(tsc, true, NewNetsnmpTranslator()) - require.NoError(t, err) + acc := &testutil.Accumulator{} - require.Equal(t, "mytable", tb.Name) - 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, - }, - } - 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 TestTableJoinNoIndexAsTag_walk(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, - }, - }, - } - - tb, err := tbl.Build(tsc, true, NewNetsnmpTranslator()) - require.NoError(t, err) - - require.Equal(t, "mytable", tb.Name) - 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, - }, - } - require.Len(t, tb.Rows, 3) - require.Contains(t, tb.Rows, rtr1) - require.Contains(t, tb.Rows, rtr2) - require.Contains(t, tb.Rows, rtr3) + require.NoError(t, s.Gather(acc)) + + require.Len(t, acc.Metrics, 1) + m := acc.Metrics[0] + require.Equal(t, "baz", m.Tags["host"]) } diff --git a/plugins/processors/ifname/ifname.go b/plugins/processors/ifname/ifname.go index 52eb31dda..f4b961bfd 100644 --- a/plugins/processors/ifname/ifname.go +++ b/plugins/processors/ifname/ifname.go @@ -13,7 +13,6 @@ import ( "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/internal/snmp" "github.com/influxdata/telegraf/plugins/common/parallel" - si "github.com/influxdata/telegraf/plugins/inputs/snmp" "github.com/influxdata/telegraf/plugins/processors" ) @@ -25,7 +24,6 @@ type keyType = string type valType = nameMap type mapFunc func(agent string) (nameMap, error) -type makeTableFunc func(string) (*si.Table, error) type sigMap map[string]chan struct{} @@ -43,8 +41,8 @@ type IfName struct { Log telegraf.Logger `toml:"-"` - ifTable *si.Table - ifXTable *si.Table + ifTable *snmp.Table + ifXTable *snmp.Table cache *TTLCache lock sync.Mutex @@ -52,9 +50,6 @@ type IfName struct { sigs sigMap getMapRemote mapFunc - makeTable makeTableFunc - - translator si.Translator } const minRetry = 5 * time.Minute @@ -65,7 +60,6 @@ func (*IfName) SampleConfig() string { func (d *IfName) Init() error { d.getMapRemote = d.getMapRemoteNoMock - d.makeTable = d.makeTableNoMock c := NewTTLCache(time.Duration(d.CacheTTL), d.CacheSize) d.cache = &c @@ -76,10 +70,6 @@ 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 } @@ -287,17 +277,17 @@ func init() { }) } -func (d *IfName) makeTableNoMock(oid string) (*si.Table, error) { +func (d *IfName) makeTable(oid string) (*snmp.Table, error) { var err error - tab := si.Table{ + tab := snmp.Table{ Name: "ifTable", IndexAsTag: true, - Fields: []si.Field{ + Fields: []snmp.Field{ {Oid: oid, Name: "ifName"}, }, } - err = tab.Init(d.translator) + err = tab.Init(nil) if err != nil { //Init already wraps return nil, err @@ -306,10 +296,10 @@ func (d *IfName) makeTableNoMock(oid string) (*si.Table, error) { return &tab, nil } -func (d *IfName) buildMap(gs snmp.GosnmpWrapper, tab *si.Table) (nameMap, error) { +func (d *IfName) buildMap(gs snmp.GosnmpWrapper, tab *snmp.Table) (nameMap, error) { var err error - rtab, err := tab.Build(gs, true, d.translator) + rtab, err := tab.Build(gs, true) 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 654e40bbf..43cf6118a 100644 --- a/plugins/processors/ifname/ifname_test.go +++ b/plugins/processors/ifname/ifname_test.go @@ -12,7 +12,6 @@ import ( "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/internal/snmp" "github.com/influxdata/telegraf/metric" - si "github.com/influxdata/telegraf/plugins/inputs/snmp" "github.com/influxdata/telegraf/testutil" ) @@ -105,12 +104,7 @@ func TestGetMap(t *testing.T) { CacheTTL: config.Duration(10 * time.Second), } - // Don't run net-snmp commands to look up table names. - d.makeTable = func(string) (*si.Table, error) { - return &si.Table{}, nil - } - err := d.Init() - require.NoError(t, err) + require.NoError(t, d.Init()) expected := nameMap{ 1: "ifname1",