Skip to content

Commit

Permalink
Add cmsysteminfo metrics.
Browse files Browse the repository at this point in the history
  • Loading branch information
Denis Krivak committed Sep 6, 2023
1 parent ab96914 commit c26158e
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 6 deletions.
78 changes: 76 additions & 2 deletions api.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,64 @@
package main

import "encoding/xml"
import (
"encoding/xml"
"fmt"
"regexp"
"strconv"
"time"
)

// List of XML RPC getter function codes.
const (
FnLogin = "15"
FnLogout = "16"
)

// List of XML RPC setter function codes.
const (
FnCMSystemInfo = "2"
FnCMState = "136"
)

// List of string constants from the XML API responses.
const (
OperStateOK = "OPERATIONAL"
OperStateOK = "OPERATIONAL"
NetworkAccessAllowed = "NetworkAccess"
)

// CMSystemInfo shows cable modem system info.
type CMSystemInfo struct {
DocsisMode string `xml:"cm_docsis_mode"`
HardwareVersion string `xml:"cm_hardware_version"`
MacAddr string `xml:"cm_mac_addr"`
SerialNumber string `xml:"cm_serial_number"`
SystemUptime int `xml:"cm_system_uptime"`
NetworkAccess string `xml:"cm_network_access"`
}

// UnmarshalXML is a standard unmarshaller + string to seconds convertor.
func (c *CMSystemInfo) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type Alias CMSystemInfo
aux := &struct {
*Alias
SystemUptime string `xml:"cm_system_uptime"`
}{
Alias: (*Alias)(c),
}

if err := d.DecodeElement(&aux, &start); err != nil {
return err //nolint:wrapcheck
}

dur, err := parseDuration(aux.SystemUptime)
if err != nil {
return err //nolint:wrapcheck
}
c.SystemUptime = int(dur.Seconds())

return nil
}

// CMState shows cable modem state.
type CMState struct {
TunnerTemperature int `xml:"TunnerTemperature"`
Expand Down Expand Up @@ -34,6 +86,28 @@ func (c *CMState) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return nil
}

var durationRegexp = regexp.MustCompile(`(?:(\d+)day\(s\))?(\d+)h:(\d+)m:(\d+)s`)

// Input format: "1day(s)2h:34m:56s".
func parseDuration(s string) (time.Duration, error) {
matches := durationRegexp.FindStringSubmatch(s)
if len(matches) != 5 {
return 0, fmt.Errorf("invalid duration string")
}

days, _ := strconv.Atoi(matches[1])
hours, _ := strconv.Atoi(matches[2])
minutes, _ := strconv.Atoi(matches[3])
seconds, _ := strconv.Atoi(matches[4])

dur := time.Duration(days)*24*time.Hour +
time.Duration(hours)*time.Hour +
time.Duration(minutes)*time.Minute +
time.Duration(seconds)*time.Second

return dur, nil
}

func fahrenheitToCelsius(f int) int {
return (f - 32) * 5.0 / 9
}
53 changes: 53 additions & 0 deletions api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,59 @@ import (
"github.com/stretchr/testify/require"
)

// <cm_network_access>Allowed</cm_network_access></cm_system_info>
func TestCMSystemInfo_UnmarshalXML(t *testing.T) {
t.Run("valid xml", func(t *testing.T) {
data := `<?xml version="1.0" encoding="utf-8"?>` +
`<cmsysteminfo>` +
`<cm_docsis_mode>DOCSIS 3.0</cm_docsis_mode>` +
`<cm_hardware_version>5.01</cm_hardware_version>` +
`<cm_mac_addr>00:00:00:00:00:00</cm_mac_addr>` +
`<cm_serial_number>AAAAAAAAAAAA</cm_serial_number>` +
`<cm_system_uptime>10day(s)20h:15m:30s</cm_system_uptime>` +
`<cm_network_access>Allowed</cm_network_access>` +
`</cmsysteminfo>`

var cminfo CMSystemInfo
err := xml.Unmarshal([]byte(data), &cminfo)
require.NoError(t, err)

expected := CMSystemInfo{
DocsisMode: "DOCSIS 3.0",
HardwareVersion: "5.01",
MacAddr: "00:00:00:00:00:00",
SerialNumber: "AAAAAAAAAAAA",
SystemUptime: 936930,
NetworkAccess: "Allowed",
}
require.Equal(t, expected, cminfo)
})

t.Run("invalid duration", func(t *testing.T) {
data := `<?xml version="1.0" encoding="utf-8"?>` +
`<cmsysteminfo>` +
`<cm_docsis_mode>DOCSIS 3.0</cm_docsis_mode>` +
`<cm_hardware_version>5.01</cm_hardware_version>` +
`<cm_mac_addr>00:00:00:00:00:00</cm_mac_addr>` +
`<cm_serial_number>AAAAAAAAAAAA</cm_serial_number>` +
`<cm_system_uptime>hello, world</cm_system_uptime>` +
`<cm_network_access>Allowed</cm_network_access>` +
`</cmsysteminfo>`

var cminfo CMSystemInfo
err := xml.Unmarshal([]byte(data), &cminfo)
require.ErrorContains(t, err, "invalid duration string")
})

t.Run("invalid xml", func(t *testing.T) {
data := `<?xml version="1.0" encoding="utf-8"?><cmsysteminfo>`

var cminfo CMSystemInfo
err := xml.Unmarshal([]byte(data), &cminfo)
require.ErrorContains(t, err, "XML syntax error")
})
}

func TestCMState_UnmarshalXML(t *testing.T) {
t.Run("valid xml", func(t *testing.T) {
data := `<?xml version="1.0" encoding="utf-8"?>` +
Expand Down
60 changes: 56 additions & 4 deletions collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,67 @@ func (c *Collector) ServeHTTP(w http.ResponseWriter, r *http.Request) {

reg := prometheus.NewRegistry()
c.collectCMState(r.Context(), reg, client)

if err := client.Logout(r.Context()); err != nil {
log.Fatalf("Failed to logout: %v", err)
}
c.collectCMSSystemInfo(r.Context(), reg, client)

h := promhttp.HandlerFor(reg, promhttp.HandlerOpts{})
h.ServeHTTP(w, r)
}

func (c *Collector) collectCMSSystemInfo(
ctx context.Context,
reg *prometheus.Registry,
client *ConnectBox,
) {
cmDocsisModeGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "connect_box_cm_docsis_mode",
Help: "cm_docsis_mode.",
}, []string{"mode"})
cmHardwareVersionGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "connect_box_cm_hardware_version",
Help: "cm_hardware_version.",
}, []string{"version"})
cmMacAddrGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "connect_box_cm_mac_addr",
Help: "cm_mac_addr.",
}, []string{"addr"})
cmSerialNumberGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "connect_box_cm_serial_number",
Help: "cm_serial_number.",
}, []string{"sn"})
cmSystemUptimeGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "connect_box_cm_system_uptime",
Help: "cm_system_uptime.",
}, []string{})
cmNetworkAccessGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "connect_box_cm_network_access",
Help: "cm_network_access.",
}, []string{})

reg.MustRegister(cmDocsisModeGauge)
reg.MustRegister(cmHardwareVersionGauge)
reg.MustRegister(cmMacAddrGauge)
reg.MustRegister(cmSerialNumberGauge)
reg.MustRegister(cmSystemUptimeGauge)
reg.MustRegister(cmNetworkAccessGauge)

var data CMSystemInfo
err := client.GetMetrics(ctx, FnCMSystemInfo, &data)
if err == nil {
cmDocsisModeGauge.WithLabelValues(data.DocsisMode).Set(1)
cmHardwareVersionGauge.WithLabelValues(data.HardwareVersion).Set(1)
cmMacAddrGauge.WithLabelValues(data.MacAddr).Set(1)
cmSerialNumberGauge.WithLabelValues(data.SerialNumber).Set(1)
cmSystemUptimeGauge.WithLabelValues().Set(float64(data.SystemUptime))
var val float64
if data.NetworkAccess == NetworkAccessAllowed {
val = 1
}
cmNetworkAccessGauge.WithLabelValues().Set(val)
} else {
log.Printf("Failed to get CMState: %v", err)
}
}

func (c *Collector) collectCMState(
ctx context.Context,
reg *prometheus.Registry,
Expand Down

0 comments on commit c26158e

Please sign in to comment.