Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(inputs.snmp): Add displayhint conversion #15935

Merged
merged 8 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading