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

This commit is contained in:
Thomas Casteleyn 2024-10-02 17:33:15 +02:00 committed by GitHub
parent 52d30f9a3a
commit ddd6023cee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 169 additions and 45 deletions

View File

@ -12,6 +12,9 @@ import (
"unicode/utf8" "unicode/utf8"
"github.com/gosnmp/gosnmp" "github.com/gosnmp/gosnmp"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
) )
// Field holds the configuration for a Field to look up. // 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. // "hwaddr" will convert a 6-byte string to a MAC address.
// "ipaddr" will convert the value to an IPv4 or IPv6 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) // "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 Conversion string
// Translate tells if the value of the field should be snmptranslated // Translate tells if the value of the field should be snmptranslated
Translate bool Translate bool
@ -78,7 +82,6 @@ func (f *Field) Init(tr Translator) error {
if f.Conversion == "" { if f.Conversion == "" {
f.Conversion = conversion f.Conversion = conversion
} }
// TODO use textual convention conversion from the MIB
} }
if f.SecondaryIndexTable && f.SecondaryIndexUse { 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") 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 f.initialized = true
return nil return nil
} }
// fieldConvert converts from any type according to the conv specification // fieldConvert converts from any type according to the conv specification
func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) { func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
v := ent.Value
// snmptranslate table field value here // snmptranslate table field value here
if f.Translate { if f.Translate {
if entOid, ok := ent.Value.(string); ok { if entOid, ok := v.(string); ok {
_, _, oidText, _, err := f.translator.SnmpTranslate(entOid) _, _, oidText, _, err := f.translator.SnmpTranslate(entOid)
if err == nil { if err == nil {
// If no error translating, the original value for ent.Value should be replaced // If no error translating, the original value should be replaced
ent.Value = oidText v = oidText
} }
} }
} }
if f.Conversion == "" { if f.Conversion == "" {
// OctetStrings may contain hex data that needs its own conversion // OctetStrings may contain hex data that needs its own conversion
if ent.Type == gosnmp.OctetString && !utf8.Valid(ent.Value.([]byte)[:]) { if ent.Type == gosnmp.OctetString && !utf8.Valid(v.([]byte)[:]) {
return hex.EncodeToString(ent.Value.([]byte)), nil return hex.EncodeToString(v.([]byte)), nil
} }
if bs, ok := ent.Value.([]byte); ok { if bs, ok := v.([]byte); ok {
return string(bs), nil return string(bs), nil
} }
return ent.Value, nil return v, nil
} }
var v interface{}
var d int var d int
if _, err := fmt.Sscanf(f.Conversion, "float(%d)", &d); err == nil || f.Conversion == "float" { if _, err := fmt.Sscanf(f.Conversion, "float(%d)", &d); err == nil || f.Conversion == "float" {
v = ent.Value
switch vt := v.(type) { switch vt := v.(type) {
case float32: case float32:
v = float64(vt) / math.Pow10(d) v = float64(vt) / math.Pow10(d)
@ -163,7 +174,6 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
} }
if f.Conversion == "int" { if f.Conversion == "int" {
v = ent.Value
var err error var err error
switch vt := v.(type) { switch vt := v.(type) {
case float32: case float32:
@ -198,8 +208,9 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
return v, err return v, err
} }
// Deprecated: Use displayhint instead
if f.Conversion == "hwaddr" { if f.Conversion == "hwaddr" {
switch vt := ent.Value.(type) { switch vt := v.(type) {
case string: case string:
v = net.HardwareAddr(vt).String() v = net.HardwareAddr(vt).String()
case []byte: case []byte:
@ -211,7 +222,7 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
} }
if f.Conversion == "hex" { if f.Conversion == "hex" {
switch vt := ent.Value.(type) { switch vt := v.(type) {
case string: case string:
switch ent.Type { switch ent.Type {
case gosnmp.IPAddress: case gosnmp.IPAddress:
@ -237,9 +248,9 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
endian := split[1] endian := split[1]
bit := split[2] bit := split[2]
bv, ok := ent.Value.([]byte) bv, ok := v.([]byte)
if !ok { if !ok {
return ent.Value, nil return v, nil
} }
switch endian { switch endian {
@ -275,7 +286,7 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
if f.Conversion == "ipaddr" { if f.Conversion == "ipaddr" {
var ipbs []byte var ipbs []byte
switch vt := ent.Value.(type) { switch vt := v.(type) {
case string: case string:
ipbs = []byte(vt) ipbs = []byte(vt)
case []byte: case []byte:
@ -298,9 +309,14 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) {
return f.translator.SnmpFormatEnum(ent.Name, ent.Value, false) return f.translator.SnmpFormatEnum(ent.Name, ent.Value, false)
} }
// Deprecated: Use displayhint instead
if f.Conversion == "enum(1)" { if f.Conversion == "enum(1)" {
return f.translator.SnmpFormatEnum(ent.Name, ent.Value, true) 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) return nil, fmt.Errorf("invalid conversion type %q", f.Conversion)
} }

View File

@ -14,6 +14,39 @@ TestMIB MODULE-IDENTITY
" "
::= { iso 1 } ::= { 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 } testingObjects OBJECT IDENTIFIER ::= { iso 0 }
testObjects OBJECT IDENTIFIER ::= { testingObjects 0 } testObjects OBJECT IDENTIFIER ::= { testingObjects 0 }
hostnameone OBJECT IDENTIFIER ::= {testObjects 1 } hostnameone OBJECT IDENTIFIER ::= {testObjects 1 }
@ -54,4 +87,12 @@ description OBJECT-TYPE
"server mib for testing" "server mib for testing"
::= { testMIBObjects 4 } ::= { testMIBObjects 4 }
END dateAndTime OBJECT-TYPE
SYNTAX DateAndTime
ACCESS read-only
STATUS current
DESCRIPTION
"A date-time specification."
::= { testMIBObjects 5 }
END

View File

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

View File

@ -21,4 +21,9 @@ type Translator interface {
formatted string, formatted string,
err error, err error,
) )
SnmpFormatDisplayHint(oid string, value interface{}) (
formatted string,
err error,
)
} }

View File

@ -74,6 +74,22 @@ func (g *gosmiTranslator) SnmpFormatEnum(oid string, value interface{}, full boo
return v.Formatted, nil 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{}) { func getIndex(mibPrefix string, node gosmi.SmiNode) (col []string, tagOids map[string]struct{}) {
// first attempt to get the table's tags // first attempt to get the table's tags
tagOids = map[string]struct{}{} tagOids = map[string]struct{}{}
@ -154,18 +170,20 @@ func snmpTranslateCall(oid string) (mibName string, oidNum string, oidText strin
} }
tc := out.GetSubtree() tc := out.GetSubtree()
for i := range tc { for i := range tc {
// case where the mib doesn't have a conversion so Type struct will be nil // case where the mib doesn't have a conversion so Type struct will be nil
// prevents seg fault // prevents seg fault
if tc[i].Type == nil { if tc[i].Type == nil {
break break
} }
switch tc[i].Type.Name {
case "MacAddress", "PhysAddress": if tc[i].Type.Format != "" {
conversion = "hwaddr" conversion = "displayhint"
case "InetAddressIPv4", "InetAddressIPv6", "InetAddress", "IPSIpAddress": } else {
conversion = "ipaddr" switch tc[i].Type.Name {
case "InetAddress", "IPSIpAddress":
conversion = "ipaddr"
}
} }
} }

View File

@ -42,9 +42,10 @@ func TestFieldInitGosmi(t *testing.T) {
{".1.2.3", "foo", "", ".1.2.3", "foo", ""}, {".1.2.3", "foo", "", ".1.2.3", "foo", ""},
{".iso.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", "", "", ".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"}, {"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"}, {"TCP-MIB::tcpConnectionLocalAddress.1", "", "", ".1.3.6.1.2.1.6.19.1.2.1", "tcpConnectionLocalAddress.1", "ipaddr"},
{".999", "", "", ".999", ".999", ""}, {".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, ".1.3.6.1.2.1.3.1.1.2", tbl.Fields[2].Oid)
require.Equal(t, "atPhysAddress", tbl.Fields[2].Name) require.Equal(t, "atPhysAddress", tbl.Fields[2].Name)
require.False(t, tbl.Fields[2].IsTag) 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, ".1.3.6.1.2.1.3.1.1.3", tbl.Fields[4].Oid)
require.Equal(t, "atNetAddress", tbl.Fields[4].Name) 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) { func TestTableJoin_walkGosmi(t *testing.T) {
tbl := Table{ tbl := Table{
Name: "mytable", Name: "mytable",

View File

@ -266,3 +266,7 @@ func (n *netsnmpTranslator) snmpTranslateCall(oid string) (mibName string, oidNu
func (n *netsnmpTranslator) SnmpFormatEnum(_ string, _ interface{}, _ bool) (string, error) { func (n *netsnmpTranslator) SnmpFormatEnum(_ string, _ interface{}, _ bool) (string, error) {
return "", errors.New("not implemented in netsnmp translator") return "", errors.New("not implemented in netsnmp translator")
} }
func (n *netsnmpTranslator) SnmpFormatDisplayHint(_ string, _ interface{}) (string, error) {
return "", errors.New("not implemented in netsnmp translator")
}

View File

@ -169,7 +169,6 @@ option operate similar to the `snmpget` utility.
## float: Convert the value into a float with no adjustment. Same ## float: Convert the value into a float with no adjustment. Same
## as `float(0)`. ## as `float(0)`.
## int: Convert the value into an integer. ## int: Convert the value into an integer.
## hwaddr: Convert the value to a MAC address.
## ipaddr: Convert the value to an IP address. ## ipaddr: Convert the value to an IP address.
## hex: Convert bytes to a hex string. ## hex: Convert bytes to a hex string.
## hextoint:X:Y Convert bytes to integer, where X is the endian and Y the ## 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 ## hextoint:BigEndian:uint32. Valid options for the endian
## are: BigEndian and LittleEndian. For the bit size: ## are: BigEndian and LittleEndian. For the bit size:
## uint16, uint32 and uint64. ## 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. ## enum: Convert the value according to its syntax in the MIB.
## (Only supported with gosmi translator) ## (Only supported with gosmi translator)
## displayhint: Format the value according to the textual convention in the MIB.
## (Only supported with gosmi translator)
## ##
# conversion = "" # conversion = ""
``` ```

View File

@ -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, ".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.Equal(t, "atPhysAddress", s.Tables[0].Fields[1].Name)
require.False(t, s.Tables[0].Fields[1].IsTag) 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, ".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.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, ".1.3.6.1.2.1.3.1.1.2", s.Fields[0].Oid)
require.Equal(t, "atPhysAddress", s.Fields[0].Name) require.Equal(t, "atPhysAddress", s.Fields[0].Name)
require.False(t, s.Fields[0].IsTag) 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) { func TestSnmpInit_noTranslateGosmi(t *testing.T) {

View File

@ -97,10 +97,9 @@ to use them.
# name = "" # name = ""
## Apply one of the following conversions to the variable value: ## 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.
## ipaddr: Convert the value to an IP address. ## enum: Convert the value according to its syntax in the MIB.
## enum(1): Convert the value according to its syntax in the MIB (full). ## displayhint: Format the value according to the textual convention in the MIB.
## enum: Convert the value according to its syntax in the MIB.
## ##
# conversion = "" # conversion = ""
``` ```

View File

@ -70,9 +70,8 @@
# name = "" # name = ""
## Apply one of the following conversions to the variable value: ## 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.
## ipaddr: Convert the value to an IP address. ## enum: Convert the value according to its syntax in the MIB.
## enum(1): Convert the value according to its syntax in the MIB (full). ## displayhint: Format the value according to the textual convention in the MIB.
## enum: Convert the value according to its syntax in the MIB.
## ##
# conversion = "" # conversion = ""