Skip to content

Commit

Permalink
Add support for RFC 2579 DateAndTime (#322)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
SuperQ authored and brian-brazil committed Aug 15, 2018
1 parent 5ebe3e9 commit 8c6195f
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 7 deletions.
56 changes: 56 additions & 0 deletions collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package main

import (
"encoding/binary"
"fmt"
"net"
"strconv"
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down
36 changes: 36 additions & 0 deletions collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package main

import (
"errors"
"reflect"
"regexp"
"testing"
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 10 additions & 4 deletions generator/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:":
Expand All @@ -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
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions generator/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -409,6 +415,12 @@ func TestGenerateConfigModule(t *testing.T) {
Type: "Double",
Help: " - 1.201",
},
{
Name: "DateAndTime",
Oid: "1.202",
Type: "DateAndTime",
Help: " - 1.202",
},
},
},
},
Expand Down
6 changes: 3 additions & 3 deletions snmp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -6047,15 +6047,15 @@ 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:
- labelname: hrFSIndex
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:
Expand Down

0 comments on commit 8c6195f

Please sign in to comment.