From 8c6195f0af2e52b8fbc85759d49edde77e6ea6ba Mon Sep 17 00:00:00 2001 From: Ben Kochie Date: Wed, 15 Aug 2018 11:44:38 +0200 Subject: [PATCH] Add support for RFC 2579 DateAndTime (#322) Extract a UNIX timestamp from RFC 2579 DateAndTime textual conversions. * Add support to the generator. * Add support to the collector. * Update snmp.yml with new DateAndTime metrics. * Refactor type walkNode in generator/tree.go. Signed-off-by: Ben Kochie --- collector.go | 56 ++++++++++++++++++++++++++++++++++++++++++ collector_test.go | 36 +++++++++++++++++++++++++++ generator/README.md | 1 + generator/tree.go | 14 ++++++++--- generator/tree_test.go | 12 +++++++++ snmp.yml | 6 ++--- 6 files changed, 118 insertions(+), 7 deletions(-) diff --git a/collector.go b/collector.go index 87dc805d..0d16adac 100644 --- a/collector.go +++ b/collector.go @@ -14,6 +14,7 @@ package main import ( + "encoding/binary" "fmt" "net" "strconv" @@ -234,7 +235,55 @@ func getPduValue(pdu *gosnmp.SnmpPDU) float64 { } } +// parseDateAndTime extracts a UNIX timestamp from an RFC 2579 DateAndTime. +func parseDateAndTime(pdu *gosnmp.SnmpPDU) (float64, error) { + var ( + v []byte + tz *time.Location + err error + ) + // DateAndTime should be a slice of bytes. + switch pduType := pdu.Value.(type) { + case []byte: + v = pdu.Value.([]byte) + default: + return 0, fmt.Errorf("invalid DateAndTime type %v", pduType) + } + pduLength := len(v) + // DateAndTime can be 8 or 11 bytes depending if the time zone is included. + switch pduLength { + case 8: + // No time zone included, assume UTC. + tz = time.UTC + case 11: + // Extract the timezone from the last 3 bytes. + locString := fmt.Sprintf("%s%02d%02d", string(v[8]), uint8(v[9]), uint8(v[10])) + loc, err := time.Parse("-0700", locString) + if err != nil { + return 0, fmt.Errorf("error parsing location string: %q, error: %s", locString, err) + } + tz = loc.Location() + default: + return 0, fmt.Errorf("invalid DateAndTime length %v", pduLength) + } + if err != nil { + return 0, fmt.Errorf("unable to parse DateAndTime %q, error: %s", v, err) + } + // Build the date from the various fields and time zone. + t := time.Date( + int(binary.BigEndian.Uint16(v[0:2])), + time.Month(uint8(v[2])), + int(uint8(v[3])), + int(uint8(v[4])), + int(uint8(v[5])), + int(uint8(v[6])), + int(uint8(v[7]))*1e+8, + tz) + return float64(t.Unix()), nil +} + func pduToSamples(indexOids []int, pdu *gosnmp.SnmpPDU, metric *config.Metric, oidToPdu map[string]gosnmp.SnmpPDU) []prometheus.Metric { + var err error // The part of the OID that is the indexes. labels := indexesToLabels(indexOids, metric, oidToPdu) @@ -255,6 +304,13 @@ func pduToSamples(indexOids []int, pdu *gosnmp.SnmpPDU, metric *config.Metric, o t = prometheus.GaugeValue case "Float", "Double": t = prometheus.GaugeValue + case "DateAndTime": + t = prometheus.GaugeValue + value, err = parseDateAndTime(pdu) + if err != nil { + log.Debugf("error parsing DateAndTime: %s", err) + return []prometheus.Metric{} + } default: // It's some form of string. t = prometheus.GaugeValue diff --git a/collector_test.go b/collector_test.go index 40249624..d9a3b8ec 100644 --- a/collector_test.go +++ b/collector_test.go @@ -14,6 +14,7 @@ package main import ( + "errors" "reflect" "regexp" "testing" @@ -497,6 +498,41 @@ func TestPduValueAsString(t *testing.T) { } } +func TestParseDateAndTime(t *testing.T) { + cases := []struct { + pdu *gosnmp.SnmpPDU + result float64 + err error + }{ + // No timezone, use UTC + { + pdu: &gosnmp.SnmpPDU{Value: []byte{7, 226, 8, 15, 8, 1, 15, 0}}, + result: 1534320075, + err: nil, + }, + // +0200 + { + pdu: &gosnmp.SnmpPDU{Value: []byte{7, 226, 8, 15, 8, 1, 15, 0, 43, 2, 0}}, + result: 1534312875, + err: nil, + }, + { + pdu: &gosnmp.SnmpPDU{Value: []byte{0}}, + result: 0, + err: errors.New("invalid DateAndTime length 1"), + }, + } + for _, c := range cases { + got, err := parseDateAndTime(c.pdu) + if !reflect.DeepEqual(err, c.err) { + t.Errorf("parseDateAndTime(%v) error: got %v, want %v", c.pdu, err, c.err) + } + if !reflect.DeepEqual(got, c.result) { + t.Errorf("parseDateAndTime(%v) result: got %v, want %v", c.pdu, got, c.result) + } + } +} + func TestIndexesToLabels(t *testing.T) { cases := []struct { oid []int diff --git a/generator/README.md b/generator/README.md index 9c59929c..dd8176a7 100644 --- a/generator/README.md +++ b/generator/README.md @@ -105,6 +105,7 @@ modules: # gauge: An integer with type gauge. # counter: An integer with type counter. # OctetString: A bit string, rendered as 0xff34. + # DateAndTime: An RFC 2579 DateAndTime byte sequence. If the device has no time zone data, UTC is used. # DisplayString: An ASCII or UTF-8 string. # PhysAddress48: A 48 bit MAC address, rendered as 00:01:02:03:04:ff. # Float: A 32 bit floating-point value with type gauge. diff --git a/generator/tree.go b/generator/tree.go index 5e381bf7..3785e9f2 100644 --- a/generator/tree.go +++ b/generator/tree.go @@ -90,8 +90,9 @@ func prepareTree(nodes *Node) map[string]*Node { // is technically only ASCII. displayStringRe := regexp.MustCompile(`^\d+[at]$`) - // Set type on MAC addresses and strings. + // Apply various tweaks to the types. walkNode(nodes, func(n *Node) { + // Set type on MAC addresses and strings. // RFC 2579 switch n.Hint { case "1x:": @@ -106,13 +107,16 @@ func prepareTree(nodes *Node) map[string]*Node { if n.TextualConvention == "DisplayString" { n.Type = "DisplayString" } - }) - // Promote Opaque Float/Double textual convention to type. - walkNode(nodes, func(n *Node) { + // Promote Opaque Float/Double textual convention to type. if n.TextualConvention == "Float" || n.TextualConvention == "Double" { n.Type = n.TextualConvention } + + // Convert RFC 2579 DateAndTime textual conversion to type. + if n.TextualConvention == "DateAndTime" { + n.Type = "DateAndTime" + } }) return nameToNode @@ -130,6 +134,8 @@ func metricType(t string) (string, bool) { return "IpAddr", true case "PhysAddress48", "DisplayString", "Float", "Double": return t, true + case "DateAndTime": + return t, true default: // Unsupported type. return "", false diff --git a/generator/tree_test.go b/generator/tree_test.go index bdbabcf4..be91a564 100644 --- a/generator/tree_test.go +++ b/generator/tree_test.go @@ -119,6 +119,11 @@ func TestTreePrepare(t *testing.T) { in: &Node{Oid: "1", Type: "OPAQUE", TextualConvention: "Double"}, out: &Node{Oid: "1", Type: "Double", TextualConvention: "Double"}, }, + // RFC 2579 DateAndTime. + { + in: &Node{Oid: "1", Type: "DisplayString", TextualConvention: "DateAndTime"}, + out: &Node{Oid: "1", Type: "DateAndTime", TextualConvention: "DateAndTime"}, + }, } for i, c := range cases { // Indexes always end up initilized. @@ -312,6 +317,7 @@ func TestGenerateConfigModule(t *testing.T) { {Oid: "1.100", Access: "ACCESS_READONLY", Label: "MacAddress", Type: "OCTETSTR", Hint: "1x:"}, {Oid: "1.200", Access: "ACCESS_READONLY", Label: "Float", Type: "OPAQUE", TextualConvention: "Float"}, {Oid: "1.201", Access: "ACCESS_READONLY", Label: "Double", Type: "OPAQUE", TextualConvention: "Double"}, + {Oid: "1.202", Access: "ACCESS_READONLY", Label: "DateAndTime", Type: "DisplayString", TextualConvention: "DateAndTime"}, }}, cfg: &ModuleConfig{ Walk: []string{"root", "1.3"}, @@ -409,6 +415,12 @@ func TestGenerateConfigModule(t *testing.T) { Type: "Double", Help: " - 1.201", }, + { + Name: "DateAndTime", + Oid: "1.202", + Type: "DateAndTime", + Help: " - 1.202", + }, }, }, }, diff --git a/snmp.yml b/snmp.yml index 4ffd05d8..77c0c489 100644 --- a/snmp.yml +++ b/snmp.yml @@ -5788,7 +5788,7 @@ paloalto_fw: help: The amount of time since this host was last initialized - 1.3.6.1.2.1.25.1.1 - name: hrSystemDate oid: 1.3.6.1.2.1.25.1.2 - type: DisplayString + type: DateAndTime help: The host's notion of the local date and time of day. - 1.3.6.1.2.1.25.1.2 - name: hrSystemInitialLoadDevice oid: 1.3.6.1.2.1.25.1.3 @@ -6047,7 +6047,7 @@ paloalto_fw: type: gauge - name: hrFSLastFullBackupDate oid: 1.3.6.1.2.1.25.3.8.1.8 - type: DisplayString + type: DateAndTime help: The last date at which this complete file system was copied to another storage device for backup - 1.3.6.1.2.1.25.3.8.1.8 indexes: @@ -6055,7 +6055,7 @@ paloalto_fw: type: gauge - name: hrFSLastPartialBackupDate oid: 1.3.6.1.2.1.25.3.8.1.9 - type: DisplayString + type: DateAndTime help: The last date at which a portion of this file system was copied to another storage device for backup - 1.3.6.1.2.1.25.3.8.1.9 indexes: