chore(snmp): Refactor SNMP translation and data-structures (#14830)

This commit is contained in:
Thomas Casteleyn 2024-02-21 16:55:01 +01:00 committed by GitHub
parent 4111cee421
commit d018363261
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 2085 additions and 2191 deletions

259
internal/snmp/field.go Normal file
View File

@ -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)
}

140
internal/snmp/mib_loader.go Normal file
View File

@ -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
}

View File

@ -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{}))
}
}

315
internal/snmp/table.go Normal file
View File

@ -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
}

246
internal/snmp/table_test.go Normal file
View File

@ -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)
}

View File

@ -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 != "<well-known>" {
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
}

View File

@ -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)
})
}
}

View File

@ -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,
)
}

View File

@ -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 != "<well-known>" {
e.MibName = module.Name
}
return e, nil
}

View File

@ -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)
})
}
}

View File

@ -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.

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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

View File

@ -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,
},
}

View File

@ -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
}

View File

@ -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{

View File

@ -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"])
}

View File

@ -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

View File

@ -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",