From 44f90c0a76fbdba336559263ef40ad306941e3a9 Mon Sep 17 00:00:00 2001 From: Thomas Casteleyn Date: Wed, 2 Oct 2024 17:33:15 +0200 Subject: [PATCH] feat(inputs.snmp): Add displayhint conversion (#15935) --- internal/snmp/field.go | 48 ++++++++++++++------- internal/snmp/testdata/gosmi/server | 43 ++++++++++++++++++- internal/snmp/testdata/gosmi/tableMib | 12 +++--- internal/snmp/translator.go | 5 +++ internal/snmp/translator_gosmi.go | 30 ++++++++++--- internal/snmp/translator_gosmi_test.go | 49 ++++++++++++++++++++-- internal/snmp/translator_netsnmp.go | 4 ++ plugins/inputs/snmp/README.md | 5 +-- plugins/inputs/snmp/snmp_test.go | 4 +- plugins/processors/snmp_lookup/README.md | 7 ++-- plugins/processors/snmp_lookup/sample.conf | 7 ++-- 11 files changed, 169 insertions(+), 45 deletions(-) diff --git a/internal/snmp/field.go b/internal/snmp/field.go index 6050acd3ebedb..b2ed67b6e9f2e 100644 --- a/internal/snmp/field.go +++ b/internal/snmp/field.go @@ -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. @@ -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 @@ -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 { @@ -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) @@ -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: @@ -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: @@ -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: @@ -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 { @@ -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: @@ -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) } diff --git a/internal/snmp/testdata/gosmi/server b/internal/snmp/testdata/gosmi/server index 4f97618d62ef3..a091d7a7311e4 100644 --- a/internal/snmp/testdata/gosmi/server +++ b/internal/snmp/testdata/gosmi/server @@ -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 } @@ -54,4 +87,12 @@ description OBJECT-TYPE "server mib for testing" ::= { testMIBObjects 4 } -END \ No newline at end of file +dateAndTime OBJECT-TYPE + SYNTAX DateAndTime + ACCESS read-only + STATUS current + DESCRIPTION + "A date-time specification." + ::= { testMIBObjects 5 } + +END diff --git a/internal/snmp/testdata/gosmi/tableMib b/internal/snmp/testdata/gosmi/tableMib index 3faf92761596b..af6cc52f213ab 100644 --- a/internal/snmp/testdata/gosmi/tableMib +++ b/internal/snmp/testdata/gosmi/tableMib @@ -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 diff --git a/internal/snmp/translator.go b/internal/snmp/translator.go index 720ee4f15dcde..3bf22575b0e40 100644 --- a/internal/snmp/translator.go +++ b/internal/snmp/translator.go @@ -21,4 +21,9 @@ type Translator interface { formatted string, err error, ) + + SnmpFormatDisplayHint(oid string, value interface{}) ( + formatted string, + err error, + ) } diff --git a/internal/snmp/translator_gosmi.go b/internal/snmp/translator_gosmi.go index d6bfba91a0393..88e4c2ae9d566 100644 --- a/internal/snmp/translator_gosmi.go +++ b/internal/snmp/translator_gosmi.go @@ -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{}{} @@ -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" + } } } diff --git a/internal/snmp/translator_gosmi_test.go b/internal/snmp/translator_gosmi_test.go index 26f907c2d974d..123ca116b5dd2 100644 --- a/internal/snmp/translator_gosmi_test.go +++ b/internal/snmp/translator_gosmi_test.go @@ -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", ""}, } @@ -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) @@ -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", diff --git a/internal/snmp/translator_netsnmp.go b/internal/snmp/translator_netsnmp.go index 66f2e463daf49..e75d81b1d8e7d 100644 --- a/internal/snmp/translator_netsnmp.go +++ b/internal/snmp/translator_netsnmp.go @@ -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") +} diff --git a/plugins/inputs/snmp/README.md b/plugins/inputs/snmp/README.md index 491e1de134d0e..8e4b585fa5d3b 100644 --- a/plugins/inputs/snmp/README.md +++ b/plugins/inputs/snmp/README.md @@ -169,7 +169,6 @@ 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 @@ -177,10 +176,10 @@ option operate similar to the `snmpget` utility. ## 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 = "" ``` diff --git a/plugins/inputs/snmp/snmp_test.go b/plugins/inputs/snmp/snmp_test.go index 0637d276821ef..cb9a4622bed70 100644 --- a/plugins/inputs/snmp/snmp_test.go +++ b/plugins/inputs/snmp/snmp_test.go @@ -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) @@ -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) { diff --git a/plugins/processors/snmp_lookup/README.md b/plugins/processors/snmp_lookup/README.md index e617a4ee4d9ef..e5be1d68c9ffe 100644 --- a/plugins/processors/snmp_lookup/README.md +++ b/plugins/processors/snmp_lookup/README.md @@ -97,10 +97,9 @@ to use them. # name = "" ## Apply one of the following conversions to the variable value: - ## hwaddr: Convert the value to a MAC address. - ## ipaddr: Convert the value to an IP address. - ## enum(1): Convert the value according to its syntax in the MIB (full). - ## enum: Convert the value according to its syntax in the MIB. + ## ipaddr: Convert the value to an IP address. + ## enum: Convert the value according to its syntax in the MIB. + ## displayhint: Format the value according to the textual convention in the MIB. ## # conversion = "" ``` diff --git a/plugins/processors/snmp_lookup/sample.conf b/plugins/processors/snmp_lookup/sample.conf index 24b8d63e18b87..46df18069079c 100644 --- a/plugins/processors/snmp_lookup/sample.conf +++ b/plugins/processors/snmp_lookup/sample.conf @@ -70,9 +70,8 @@ # name = "" ## Apply one of the following conversions to the variable value: - ## hwaddr: Convert the value to a MAC address. - ## ipaddr: Convert the value to an IP address. - ## enum(1): Convert the value according to its syntax in the MIB (full). - ## enum: Convert the value according to its syntax in the MIB. + ## ipaddr: Convert the value to an IP address. + ## enum: Convert the value according to its syntax in the MIB. + ## displayhint: Format the value according to the textual convention in the MIB. ## # conversion = ""