diff --git a/api.go b/api.go index 8fb9080..99543b4 100644 --- a/api.go +++ b/api.go @@ -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"` @@ -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 } diff --git a/api_test.go b/api_test.go index 7b247a1..507b5bc 100644 --- a/api_test.go +++ b/api_test.go @@ -7,6 +7,59 @@ import ( "github.com/stretchr/testify/require" ) +// Allowed +func TestCMSystemInfo_UnmarshalXML(t *testing.T) { + t.Run("valid xml", func(t *testing.T) { + data := `` + + `` + + `DOCSIS 3.0` + + `5.01` + + `00:00:00:00:00:00` + + `AAAAAAAAAAAA` + + `10day(s)20h:15m:30s` + + `Allowed` + + `` + + 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 := `` + + `` + + `DOCSIS 3.0` + + `5.01` + + `00:00:00:00:00:00` + + `AAAAAAAAAAAA` + + `hello, world` + + `Allowed` + + `` + + 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 := `` + + 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 := `` + diff --git a/collector.go b/collector.go index a9fcddc..1f0f963 100644 --- a/collector.go +++ b/collector.go @@ -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,