diff --git a/collector/collector.go b/collector/collector.go index e5dae381..dd5a21e8 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -562,6 +562,18 @@ func parseDateAndTimeWithPattern(metric *config.Metric, pdu *gosnmp.SnmpPDU, met return float64(t.Unix()), nil } +func parseNtpTimestamp(pdu *gosnmp.SnmpPDU) (float64, error) { + var data []byte + data = pdu.Value.([]byte) + + // 2208988800 is the Unix Epoch in NTP Seconds (1970 - 1900) + secs := int64(binary.BigEndian.Uint32(data[:4])) - 2208988800 + nanos := (int64(binary.BigEndian.Uint32(data[4:])) * 1e9) >> 32 + + t := time.Unix(secs, nanos) + return float64(t.Unix()), nil +} + func pduToSamples(indexOids []int, pdu *gosnmp.SnmpPDU, metric *config.Metric, oidToPdu map[string]gosnmp.SnmpPDU, logger *slog.Logger, metrics Metrics) []prometheus.Metric { var err error // The part of the OID that is the indexes. @@ -598,6 +610,13 @@ func pduToSamples(indexOids []int, pdu *gosnmp.SnmpPDU, metric *config.Metric, o logger.Debug("Error parsing ParseDateAndTime", "err", err) return []prometheus.Metric{} } + case "NTPTimeStamp": + t = prometheus.GaugeValue + value, err = parseNtpTimestamp(pdu) + if err != nil { + logger.Debug("Error parsing NTPTimeStamp", "err", err) + return []prometheus.Metric{} + } case "EnumAsInfo": return enumAsInfo(metric, int(value), labelnames, labelvalues) case "EnumAsStateSet": diff --git a/collector/collector_test.go b/collector/collector_test.go index b8ba8254..e645b232 100644 --- a/collector/collector_test.go +++ b/collector/collector_test.go @@ -644,6 +644,29 @@ func TestGetPduLargeValue(t *testing.T) { } } +func TestNtpTimestamp(t *testing.T) { + cases := []struct { + pdu *gosnmp.SnmpPDU + result float64 + err error + }{ + { + pdu: &gosnmp.SnmpPDU{Value: []byte{235, 6, 119, 246, 48, 209, 11, 59}}, + result: 1.734080886e+09, + err: nil, + }, + } + for _, c := range cases { + got, err := parseNtpTimestamp(c.pdu) + if !reflect.DeepEqual(err, c.err) { + t.Errorf("parseNtpTimestamp(%v) error: got %v, want %v", c.pdu, err, c.err) + } + if !reflect.DeepEqual(got, c.result) { + t.Errorf("parseNtpTimestamp(%v) result: got %v, want %v", c.pdu, got, c.result) + } + } +} + func TestOidToList(t *testing.T) { cases := []struct { oid string diff --git a/generator/README.md b/generator/README.md index 126f252b..37d8f775 100644 --- a/generator/README.md +++ b/generator/README.md @@ -175,6 +175,7 @@ modules: # 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. # ParseDateAndTime: Parse a DisplayString and return the timestamp. See datetime_pattern config option + # NTPTimeStamp: Parse the NTP timestamp (RFC-1305, March 1992, Section 3.1) and return Unix timestamp as float. # 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 05c011b1..9611af21 100644 --- a/generator/tree.go +++ b/generator/tree.go @@ -136,6 +136,9 @@ func prepareTree(nodes *Node, logger *slog.Logger) map[string]*Node { if n.TextualConvention == "ParseDateAndTime" { n.Type = "ParseDateAndTime" } + if n.TextualConvention == "NTPTimeStamp" { + n.Type = "NTPTimeStamp" + } // Convert RFC 4001 InetAddress types textual convention to type. if n.TextualConvention == "InetAddressIPv4" || n.TextualConvention == "InetAddressIPv6" || n.TextualConvention == "InetAddress" { n.Type = n.TextualConvention @@ -170,6 +173,8 @@ func metricType(t string) (string, bool) { return t, true case "ParseDateAndTime": return t, true + case "NTPTimeStamp": + return t, true case "EnumAsInfo", "EnumAsStateSet": return t, true default: diff --git a/generator/tree_test.go b/generator/tree_test.go index b22e0315..2a6d57a2 100644 --- a/generator/tree_test.go +++ b/generator/tree_test.go @@ -148,6 +148,11 @@ func TestTreePrepare(t *testing.T) { in: &Node{Oid: "1", Type: "OctectString", TextualConvention: "InetAddress"}, out: &Node{Oid: "1", Type: "InetAddress", TextualConvention: "InetAddress"}, }, + // NTPTimeStamp + { + in: &Node{Oid: "1", Type: "OctectString", TextualConvention: "NTPTimeStamp"}, + out: &Node{Oid: "1", Type: "NTPTimeStamp", TextualConvention: "NTPTimeStamp"}, + }, } for i, c := range cases { // Indexes always end up initialized. @@ -346,6 +351,7 @@ func TestGenerateConfigModule(t *testing.T) { {Oid: "1.203", Access: "ACCESS_READONLY", Label: "InetAddressIPv4", Type: "OCTETSTR", TextualConvention: "InetAddressIPv4"}, {Oid: "1.204", Access: "ACCESS_READONLY", Label: "InetAddressIPv6", Type: "OCTETSTR", TextualConvention: "InetAddressIPv6"}, {Oid: "1.205", Access: "ACCESS_READONLY", Label: "ParseDateAndTime", Type: "DisplayString", TextualConvention: "ParseDateAndTime"}, + {Oid: "1.206", Access: "ACCESS_READONLY", Label: "NTPTimeStamp", Type: "NTPTimeStamp", TextualConvention: "NTPTimeStamp"}, }}, cfg: &ModuleConfig{ Walk: []string{"root", "1.3"}, @@ -473,6 +479,12 @@ func TestGenerateConfigModule(t *testing.T) { Type: "ParseDateAndTime", Help: " - 1.205", }, + { + Name: "NTPTimeStamp", + Oid: "1.206", + Type: "NTPTimeStamp", + Help: " - 1.206", + }, }, }, },