Skip to content

Commit

Permalink
feat(inputs.snmp): Add displayhint conversion (influxdata#15935)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hipska authored and asaharn committed Oct 16, 2024
1 parent 562145b commit 44f90c0
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 45 deletions.
48 changes: 32 additions & 16 deletions internal/snmp/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (
"unicode/utf8"

"github.com/gosnmp/gosnmp"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
)

// Field holds the configuration for a Field to look up.
Expand All @@ -36,6 +39,7 @@ type Field struct {
// "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)
// "displayhint" will format the value according to the textual convention. (Only supported with gosmi translator)
Conversion string
// Translate tells if the value of the field should be snmptranslated
Translate bool
Expand Down Expand Up @@ -78,7 +82,6 @@ func (f *Field) Init(tr Translator) error {
if f.Conversion == "" {
f.Conversion = conversion
}
// TODO use textual convention conversion from the MIB
}

if f.SecondaryIndexTable && f.SecondaryIndexUse {
Expand All @@ -89,38 +92,46 @@ func (f *Field) Init(tr Translator) error {
return errors.New("SecondaryOuterJoin set to true, but field is not being used in join")
}

switch f.Conversion {
case "hwaddr", "enum(1)":
config.PrintOptionValueDeprecationNotice("inputs.snmp", "field.conversion", f.Conversion, telegraf.DeprecationInfo{
Since: "1.33.0",
Notice: "Use 'displayhint' instead",
})
}

f.initialized = true
return nil
}

// fieldConvert converts from any type according to the conv specification
func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
v := ent.Value

// snmptranslate table field value here
if f.Translate {
if entOid, ok := ent.Value.(string); ok {
if entOid, ok := v.(string); ok {
_, _, oidText, _, err := f.translator.SnmpTranslate(entOid)
if err == nil {
// If no error translating, the original value for ent.Value should be replaced
ent.Value = oidText
// If no error translating, the original value should be replaced
v = oidText
}
}
}

if f.Conversion == "" {
// OctetStrings may contain hex data that needs its own conversion
if ent.Type == gosnmp.OctetString && !utf8.Valid(ent.Value.([]byte)[:]) {
return hex.EncodeToString(ent.Value.([]byte)), nil
if ent.Type == gosnmp.OctetString && !utf8.Valid(v.([]byte)[:]) {
return hex.EncodeToString(v.([]byte)), nil
}
if bs, ok := ent.Value.([]byte); ok {
if bs, ok := v.([]byte); ok {
return string(bs), nil
}
return ent.Value, nil
return v, nil
}

var v interface{}
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)
Expand Down Expand Up @@ -163,7 +174,6 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
}

if f.Conversion == "int" {
v = ent.Value
var err error
switch vt := v.(type) {
case float32:
Expand Down Expand Up @@ -198,8 +208,9 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
return v, err
}

// Deprecated: Use displayhint instead
if f.Conversion == "hwaddr" {
switch vt := ent.Value.(type) {
switch vt := v.(type) {
case string:
v = net.HardwareAddr(vt).String()
case []byte:
Expand All @@ -211,7 +222,7 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
}

if f.Conversion == "hex" {
switch vt := ent.Value.(type) {
switch vt := v.(type) {
case string:
switch ent.Type {
case gosnmp.IPAddress:
Expand All @@ -237,9 +248,9 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
endian := split[1]
bit := split[2]

bv, ok := ent.Value.([]byte)
bv, ok := v.([]byte)
if !ok {
return ent.Value, nil
return v, nil
}

switch endian {
Expand Down Expand Up @@ -275,7 +286,7 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
if f.Conversion == "ipaddr" {
var ipbs []byte

switch vt := ent.Value.(type) {
switch vt := v.(type) {
case string:
ipbs = []byte(vt)
case []byte:
Expand All @@ -298,9 +309,14 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
return f.translator.SnmpFormatEnum(ent.Name, ent.Value, false)
}

// Deprecated: Use displayhint instead
if f.Conversion == "enum(1)" {
return f.translator.SnmpFormatEnum(ent.Name, ent.Value, true)
}

if f.Conversion == "displayhint" {
return f.translator.SnmpFormatDisplayHint(ent.Name, ent.Value)
}

return nil, fmt.Errorf("invalid conversion type %q", f.Conversion)
}
43 changes: 42 additions & 1 deletion internal/snmp/testdata/gosmi/server
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,39 @@ TestMIB MODULE-IDENTITY
"
::= { iso 1 }

DateAndTime ::= TEXTUAL-CONVENTION
DISPLAY-HINT "2d-1d-1d,1d:1d:1d.1d,1a1d:1d"
STATUS current
DESCRIPTION
"A date-time specification.

field octets contents range
----- ------ -------- -----
1 1-2 year* 0..65536
2 3 month 1..12
3 4 day 1..31
4 5 hour 0..23
5 6 minutes 0..59
6 7 seconds 0..60
(use 60 for leap-second)
7 8 deci-seconds 0..9
8 9 direction from UTC '+' / '-'
9 10 hours from UTC* 0..13
10 11 minutes from UTC 0..59

* Notes:
- the value of year is in network-byte order
- daylight saving time in New Zealand is +13

For example, Tuesday May 26, 1992 at 1:30:15 PM EDT would be
displayed as:

1992-5-26,13:30:15.0,-4:0

Note that if only local time is known, then timezone
information (fields 8-10) is not present."
SYNTAX OCTET STRING (SIZE (8 | 11))

testingObjects OBJECT IDENTIFIER ::= { iso 0 }
testObjects OBJECT IDENTIFIER ::= { testingObjects 0 }
hostnameone OBJECT IDENTIFIER ::= {testObjects 1 }
Expand Down Expand Up @@ -54,4 +87,12 @@ description OBJECT-TYPE
"server mib for testing"
::= { testMIBObjects 4 }

END
dateAndTime OBJECT-TYPE
SYNTAX DateAndTime
ACCESS read-only
STATUS current
DESCRIPTION
"A date-time specification."
::= { testMIBObjects 5 }

END
12 changes: 6 additions & 6 deletions internal/snmp/testdata/gosmi/tableMib
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ DisplayString ::=
--
-- SIZE (0..255)

PhysAddress ::=
OCTET STRING
-- This data type is used to model media addresses. For many
-- types of media, this will be in a binary representation.
-- For example, an ethernet address would be represented as
-- a string of 6 octets.
PhysAddress ::= TEXTUAL-CONVENTION
DISPLAY-HINT "1x:"
STATUS current
DESCRIPTION
"Represents media- or physical-level addresses."
SYNTAX OCTET STRING

-- groups in MIB-II

Expand Down
5 changes: 5 additions & 0 deletions internal/snmp/translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ type Translator interface {
formatted string,
err error,
)

SnmpFormatDisplayHint(oid string, value interface{}) (
formatted string,
err error,
)
}
30 changes: 24 additions & 6 deletions internal/snmp/translator_gosmi.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ func (g *gosmiTranslator) SnmpFormatEnum(oid string, value interface{}, full boo
return v.Formatted, nil
}

func (g *gosmiTranslator) SnmpFormatDisplayHint(oid string, value interface{}) (string, error) {
if value == nil {
return "", nil
}

//nolint:dogsled // only need to get the node
_, _, _, _, node, err := snmpTranslateCall(oid)
if err != nil {
return "", err
}

v := node.FormatValue(value)

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{}{}
Expand Down Expand Up @@ -154,18 +170,20 @@ func snmpTranslateCall(oid string) (mibName string, oidNum string, oidText strin
}

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"

if tc[i].Type.Format != "" {
conversion = "displayhint"
} else {
switch tc[i].Type.Name {
case "InetAddress", "IPSIpAddress":
conversion = "ipaddr"
}
}
}

Expand Down
49 changes: 46 additions & 3 deletions internal/snmp/translator_gosmi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ func TestFieldInitGosmi(t *testing.T) {
{".1.2.3", "foo", "", ".1.2.3", "foo", ""},
{".iso.2.3", "foo", "", ".1.2.3", "foo", ""},
{".1.0.0.0.1.1", "", "", ".1.0.0.0.1.1", "server", ""},
{"IF-MIB::ifPhysAddress.1", "", "", ".1.3.6.1.2.1.2.2.1.6.1", "ifPhysAddress.1", "hwaddr"},
{".1.0.0.0.1.5", "", "", ".1.0.0.0.1.5", "dateAndTime", "displayhint"},
{"IF-MIB::ifPhysAddress.1", "", "", ".1.3.6.1.2.1.2.2.1.6.1", "ifPhysAddress.1", "displayhint"},
{"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"},
{"BRIDGE-MIB::dot1dTpFdbAddress.1", "", "", ".1.3.6.1.2.1.17.4.3.1.1.1", "dot1dTpFdbAddress.1", "displayhint"},
{"TCP-MIB::tcpConnectionLocalAddress.1", "", "", ".1.3.6.1.2.1.6.19.1.2.1", "tcpConnectionLocalAddress.1", "ipaddr"},
{".999", "", "", ".999", ".999", ""},
}
Expand Down Expand Up @@ -89,7 +90,7 @@ func TestTableInitGosmi(t *testing.T) {
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, "displayhint", tbl.Fields[2].Conversion)

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)
Expand Down Expand Up @@ -355,6 +356,48 @@ func TestFieldConvertGosmi(t *testing.T) {
}
}

func TestSnmpFormatDisplayHint(t *testing.T) {
tests := []struct {
name string
oid string
input interface{}
expected string
}{
{
name: "ifOperStatus",
oid: ".1.3.6.1.2.1.2.2.1.8",
input: 3,
expected: "testing(3)",
}, {
name: "ifPhysAddress",
oid: ".1.3.6.1.2.1.2.2.1.6",
input: []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
expected: "01:23:45:67:89:ab:cd:ef",
}, {
name: "DateAndTime short",
oid: ".1.0.0.0.1.5",
input: []byte{0x07, 0xe8, 0x09, 0x18, 0x10, 0x24, 0x27, 0x05},
expected: "2024-9-24,16:36:39.5",
}, {
name: "DateAndTime long",
oid: ".1.0.0.0.1.5",
input: []byte{0x07, 0xe8, 0x09, 0x18, 0x10, 0x24, 0x27, 0x05, 0x2b, 0x02, 0x00},
expected: "2024-9-24,16:36:39.5,+2:0",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tr := getGosmiTr(t)

actual, err := tr.SnmpFormatDisplayHint(tt.oid, tt.input)
require.NoError(t, err)

require.Equal(t, tt.expected, actual)
})
}
}

func TestTableJoin_walkGosmi(t *testing.T) {
tbl := Table{
Name: "mytable",
Expand Down
4 changes: 4 additions & 0 deletions internal/snmp/translator_netsnmp.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,7 @@ func (n *netsnmpTranslator) snmpTranslateCall(oid string) (mibName string, oidNu
func (n *netsnmpTranslator) SnmpFormatEnum(_ string, _ interface{}, _ bool) (string, error) {
return "", errors.New("not implemented in netsnmp translator")
}

func (n *netsnmpTranslator) SnmpFormatDisplayHint(_ string, _ interface{}) (string, error) {
return "", errors.New("not implemented in netsnmp translator")
}
5 changes: 2 additions & 3 deletions plugins/inputs/snmp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,18 +169,17 @@ option operate similar to the `snmpget` utility.
## float: Convert the value into a float with no adjustment. Same
## as `float(0)`.
## int: Convert the value into an integer.
## hwaddr: Convert the value to a MAC address.
## ipaddr: Convert the value to an IP address.
## hex: Convert bytes to a hex string.
## hextoint:X:Y Convert bytes to integer, where X is the endian and Y the
## bit size. For example: hextoint:LittleEndian:uint64 or
## hextoint:BigEndian:uint32. Valid options for the endian
## are: BigEndian and LittleEndian. For the bit size:
## uint16, uint32 and uint64.
## enum(1): Convert the value according to its syntax in the MIB (full).
## (Only supported with gosmi translator)
## enum: Convert the value according to its syntax in the MIB.
## (Only supported with gosmi translator)
## displayhint: Format the value according to the textual convention in the MIB.
## (Only supported with gosmi translator)
##
# conversion = ""
```
Expand Down
4 changes: 2 additions & 2 deletions plugins/inputs/snmp/snmp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ func TestSnmpInitGosmi(t *testing.T) {
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, "displayhint", 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)
Expand All @@ -657,7 +657,7 @@ func TestSnmpInitGosmi(t *testing.T) {
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)
require.Equal(t, "displayhint", s.Fields[0].Conversion)
}

func TestSnmpInit_noTranslateGosmi(t *testing.T) {
Expand Down
Loading

0 comments on commit 44f90c0

Please sign in to comment.