diff --git a/internal/snmp/field.go b/internal/snmp/field.go index 328037003..7406a5d98 100644 --- a/internal/snmp/field.go +++ b/internal/snmp/field.go @@ -2,12 +2,14 @@ package snmp import ( "encoding/binary" + "encoding/hex" "errors" "fmt" "math" "net" "strconv" "strings" + "unicode/utf8" "github.com/gosnmp/gosnmp" ) @@ -94,6 +96,10 @@ func (f *Field) Init(tr Translator) error { // fieldConvert converts from any type according to the conv specification func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) { if f.Conversion == "" { + // OctetStrings may contain hex data that needs its own conversion + if ent.Type == gosnmp.OctetString && !utf8.ValidString(string(ent.Value.([]byte)[:])) { + return hex.EncodeToString(ent.Value.([]byte)), nil + } if bs, ok := ent.Value.([]byte); ok { return string(bs), nil } @@ -188,7 +194,29 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) { case []byte: v = net.HardwareAddr(vt).String() default: - return nil, fmt.Errorf("invalid type (%T) for hwaddr conversion", v) + return nil, fmt.Errorf("invalid type (%T) for hwaddr conversion", vt) + } + return v, nil + } + + if f.Conversion == "hex" { + switch vt := ent.Value.(type) { + case string: + switch ent.Type { + case gosnmp.IPAddress: + ip := net.ParseIP(vt) + if ip4 := ip.To4(); ip4 != nil { + v = hex.EncodeToString(ip4) + } else { + v = hex.EncodeToString(ip) + } + default: + return nil, fmt.Errorf("unsupported Asn1BER (%#v) for hex conversion", ent.Type) + } + case []byte: + v = hex.EncodeToString(vt) + default: + return nil, fmt.Errorf("unsupported type (%T) for hex conversion", vt) } return v, nil } @@ -242,7 +270,7 @@ func (f *Field) Convert(ent gosnmp.SnmpPDU) (interface{}, error) { case []byte: ipbs = vt default: - return nil, fmt.Errorf("invalid type (%T) for ipaddr conversion", v) + return nil, fmt.Errorf("invalid type (%T) for ipaddr conversion", vt) } switch len(ipbs) { diff --git a/internal/snmp/field_test.go b/internal/snmp/field_test.go new file mode 100644 index 000000000..c9cdf0209 --- /dev/null +++ b/internal/snmp/field_test.go @@ -0,0 +1,243 @@ +package snmp + +import ( + "testing" + + "github.com/gosnmp/gosnmp" + "github.com/stretchr/testify/require" +) + +func TestConvertDefault(t *testing.T) { + tests := []struct { + name string + ent gosnmp.SnmpPDU + expected interface{} + errmsg string + }{ + { + name: "integer", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.Integer, + Value: int(2), + }, + expected: 2, + }, + { + name: "octet string with valid bytes", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.OctetString, + Value: []byte{0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64}, + }, + expected: "Hello world", + }, + { + name: "octet string with invalid bytes", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.OctetString, + Value: []byte{0x84, 0xc8, 0x7, 0xff, 0xfd, 0x38, 0x54, 0xc1}, + }, + expected: "84c807fffd3854c1", + }, + } + + f := Field{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, err := f.Convert(tt.ent) + + if tt.errmsg != "" { + require.ErrorContains(t, err, tt.errmsg) + } else { + require.NoError(t, err) + } + + require.Equal(t, tt.expected, actual) + }) + } + + t.Run("invalid", func(t *testing.T) { + f.Conversion = "invalid" + actual, err := f.Convert(gosnmp.SnmpPDU{}) + + require.Nil(t, actual) + require.ErrorContains(t, err, "invalid conversion type") + }) +} + +func TestConvertHex(t *testing.T) { + tests := []struct { + name string + ent gosnmp.SnmpPDU + expected interface{} + errmsg string + }{ + { + name: "octet string with valid bytes", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.OctetString, + Value: []byte{0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64}, + }, + expected: "48656c6c6f20776f726c64", + }, + { + name: "octet string with invalid bytes", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.OctetString, + Value: []byte{0x84, 0xc8, 0x7, 0xff, 0xfd, 0x38, 0x54, 0xc1}, + }, + expected: "84c807fffd3854c1", + }, + { + name: "IPv4", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.IPAddress, + Value: "192.0.2.1", + }, + expected: "c0000201", + }, + { + name: "IPv6", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.IPAddress, + Value: "2001:db8::1", + }, + expected: "20010db8000000000000000000000001", + }, + { + name: "oid", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.ObjectIdentifier, + Value: ".1.2.3", + }, + errmsg: "unsupported Asn1BER (0x6) for hex conversion", + }, + { + name: "integer", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.Integer, + Value: int(2), + }, + errmsg: "unsupported type (int) for hex conversion", + }, + } + + f := Field{Conversion: "hex"} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, err := f.Convert(tt.ent) + + if tt.errmsg != "" { + require.ErrorContains(t, err, tt.errmsg) + } else { + require.NoError(t, err) + } + + require.Equal(t, tt.expected, actual) + }) + } +} + +func TestConvertHextoint(t *testing.T) { + tests := []struct { + name string + conversion string + ent gosnmp.SnmpPDU + expected interface{} + errmsg string + }{ + { + name: "empty", + conversion: "hextoint:BigEndian:uint64", + ent: gosnmp.SnmpPDU{}, + expected: nil, + }, + { + name: "big endian uint64", + conversion: "hextoint:BigEndian:uint64", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.OctetString, + Value: []byte{0x84, 0xc8, 0x7, 0xff, 0xfd, 0x38, 0x54, 0xc1}, + }, + expected: uint64(0x84c807fffd3854c1), + }, + { + name: "big endian uint32", + conversion: "hextoint:BigEndian:uint32", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.OctetString, + Value: []byte{0x84, 0xc8, 0x7, 0xff}, + }, + expected: uint32(0x84c807ff), + }, + { + name: "big endian uint16", + conversion: "hextoint:BigEndian:uint16", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.OctetString, + Value: []byte{0x84, 0xc8}, + }, + expected: uint16(0x84c8), + }, + { + name: "big endian invalid", + conversion: "hextoint:BigEndian:invalid", + ent: gosnmp.SnmpPDU{Type: gosnmp.OctetString, Value: []uint8{}}, + errmsg: "invalid bit value", + }, + { + name: "little endian uint64", + conversion: "hextoint:LittleEndian:uint64", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.OctetString, + Value: []byte{0x84, 0xc8, 0x7, 0xff, 0xfd, 0x38, 0x54, 0xc1}, + }, + expected: uint64(0xc15438fdff07c884), + }, + { + name: "little endian uint32", + conversion: "hextoint:LittleEndian:uint32", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.OctetString, + Value: []byte{0x84, 0xc8, 0x7, 0xff}, + }, + expected: uint32(0xff07c884), + }, + { + name: "little endian uint16", + conversion: "hextoint:LittleEndian:uint16", + ent: gosnmp.SnmpPDU{ + Type: gosnmp.OctetString, + Value: []byte{0x84, 0xc8}, + }, + expected: uint16(0xc884), + }, + { + name: "little endian invalid", + conversion: "hextoint:LittleEndian:invalid", + ent: gosnmp.SnmpPDU{Type: gosnmp.OctetString, Value: []byte{}}, + errmsg: "invalid bit value", + }, + { + name: "invalid", + conversion: "hextoint:invalid:uint64", + ent: gosnmp.SnmpPDU{Type: gosnmp.OctetString, Value: []byte{}}, + errmsg: "invalid Endian value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := Field{Conversion: tt.conversion} + + actual, err := f.Convert(tt.ent) + + if tt.errmsg != "" { + require.ErrorContains(t, err, tt.errmsg) + } else { + require.NoError(t, err) + } + + require.Equal(t, tt.expected, actual) + }) + } +} diff --git a/internal/snmp/translator_gosmi_test.go b/internal/snmp/translator_gosmi_test.go index a777ce931..c89cd28e8 100644 --- a/internal/snmp/translator_gosmi_test.go +++ b/internal/snmp/translator_gosmi_test.go @@ -294,7 +294,6 @@ func TestFieldConvertGosmi(t *testing.T) { 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))}, @@ -333,12 +332,6 @@ func TestFieldConvertGosmi(t *testing.T) { {[]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)}, {3, "enum", "testing"}, {3, "enum(1)", "testing(3)"}, } diff --git a/internal/snmp/translator_netsnmp_test.go b/internal/snmp/translator_netsnmp_test.go index a5c88132b..28797d524 100644 --- a/internal/snmp/translator_netsnmp_test.go +++ b/internal/snmp/translator_netsnmp_test.go @@ -4,7 +4,6 @@ package snmp import ( "testing" - "github.com/gosnmp/gosnmp" "github.com/stretchr/testify/require" "github.com/influxdata/telegraf/testutil" @@ -222,72 +221,6 @@ func TestTableBuild_noWalk(t *testing.T) { 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" diff --git a/plugins/inputs/snmp/README.md b/plugins/inputs/snmp/README.md index 3e8d8f84e..491e1de13 100644 --- a/plugins/inputs/snmp/README.md +++ b/plugins/inputs/snmp/README.md @@ -171,11 +171,12 @@ option operate similar to the `snmpget` utility. ## int: Convert the value into an integer. ## hwaddr: Convert the value to a MAC address. ## ipaddr: Convert the value to an IP address. - ## hextoint:X:Y Convert a hex string value 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. + ## 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.