Implement basic vodafone station exporter

This commit is contained in:
fluepke 2020-12-04 09:12:02 +01:00
parent 499736e5bf
commit fb130d2dc9
No known key found for this signature in database
GPG key ID: 37E30BD2FBE7746A
3 changed files with 340 additions and 20 deletions

View file

@ -9,6 +9,8 @@ import (
"io/ioutil"
"net/http"
"net/http/cookiejar"
"net/url"
"strconv"
"strings"
"time"
)
@ -19,49 +21,188 @@ type VodafoneStation struct {
client *http.Client
}
type LoginResponse struct {
type LoginResponseSalts struct {
Error string `json:"error"`
Salt string `json:"salt"`
SaltWebUI string `json:"saltwebui"`
}
func NewVodafoneStation(url, password string) *VodafoneStation {
type LoginResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Data *LoginResponseData `json:"data"`
}
type LoginResponseData struct {
Interface string `json:"intf"`
User string `json:"user"`
Uid string `json:"uid"`
DefaultPassword string `json:"Dpd"`
RemoteAddress string `json:"remoteAddr"`
UserAgent string `json:"userAgent"`
HttpReferer string `json:"httpReferer"`
}
type LogoutResponse struct {
Error string `json:"error"`
Message string `json:"message"`
}
type DocsisStatusResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Data *DocsisStatusData `json:"data"`
}
type DocsisStatusData struct {
OfdmDownstreamData []*OfdmDownstreamData `json:"ofdm_downstream"`
Downstream []*DocsisDownstreamChannel `json:"downstream"`
Upstream []*DocsisUpstreamChannel `json:"upstream"`
}
type OfdmDownstreamData struct {
Id string `json:"__id"`
ChannelIdOfdm string `json:"channelid_ofdm"`
StartFrequency string `json:"start_frequency"`
EndFrequency string `json:"end_frequency"`
CentralFrequencyOfdm string `json:"CentralFrequency_ofdm"`
Bandwidth string `json:"bandwidth"`
PowerOfdm string `json:"power_ofdm"`
SnrOfdm string `json:"SNR_ofdm"`
FftOfdm string `json:"FFT_ofdm"`
LockedOfdm string `json:"locked_ofdm"`
ChannelType string `json:"ChannelType"`
}
type DocsisDownstreamChannel struct {
Id string `json:"__id"`
ChannelId string `json:"channelid"`
CentralFrequency string `json:"CentralFrequency"`
Power string `json:"power"`
Snr string `json:"SNR"`
Fft string `json:"FFT"`
Locked string `json:"locked"`
ChannelType string `json:"ChannelType"`
}
type DocsisUpstreamChannel struct {
Id string `json:"__id"`
ChannelIdUp string `json:"channelidup"`
CentralFrequency string `json:"CentralFrequency"`
Power string `json:"power"`
ChannelType string `json:"ChannelType"`
Fft string `json:"FFT"`
RangingStatus string `json:"RangingStatus"`
}
func NewVodafoneStation(stationUrl, password string) *VodafoneStation {
cookieJar, err := cookiejar.New(nil)
parsedUrl, err := url.Parse(stationUrl)
cookieJar.SetCookies(parsedUrl, []*http.Cookie{
&http.Cookie{
Name: "Cwd",
Value: "No",
},
})
if err != nil {
panic(err)
}
return &VodafoneStation{
URL: url,
URL: stationUrl,
Password: password,
client: &http.Client{
Jar: cookieJar,
Timeout: time.Second * 2,
Timeout: time.Second * 10,
},
}
}
func (v *VodafoneStation) getLoginSalts() (*LoginResponse, error) {
requestBody := strings.NewReader("username=admin&password=seeksalthash")
response, err := v.client.Post(v.URL+"/api/v1/session/login", "application/x-www-form-urlencoded", requestBody)
func (v *VodafoneStation) Login() (*LoginResponse, error) {
_, err := v.doRequest("GET", v.URL, "")
if err != nil {
return nil, err
}
loginResponseSalts, err := v.getLoginSalts()
if err != nil {
return nil, err
}
derivedPassword := GetLoginPassword(v.Password, loginResponseSalts.Salt, loginResponseSalts.SaltWebUI)
responseBody, err := v.doRequest("POST", v.URL+"/api/v1/session/login", "username=admin&password="+derivedPassword)
if err != nil {
return nil, err
}
loginResponse := &LoginResponse{}
err = json.Unmarshal(responseBody, loginResponse)
if loginResponse.Error != "ok" {
return nil, fmt.Errorf("Got non error=ok message from vodafone station")
}
return loginResponse, nil
}
func (v *VodafoneStation) Logout() (*LogoutResponse, error) {
responseBody, err := v.doRequest("POST", v.URL+"/api/v1/session/logout", "")
if err != nil {
return nil, err
}
logoutResponse := &LogoutResponse{}
err = json.Unmarshal(responseBody, logoutResponse)
if err != nil {
return nil, err
}
if logoutResponse.Error != "ok" {
return nil, fmt.Errorf("Got non error=ok message from vodafone station")
}
return logoutResponse, nil
}
func (v *VodafoneStation) GetDocsisStatus() (*DocsisStatusResponse, error) {
responseBody, err := v.doRequest("GET", v.URL+"/api/v1/sta_docsis_status?_="+strconv.FormatInt(makeTimestamp(), 10), "")
if err != nil {
return nil, err
}
docsisStatusResponse := &DocsisStatusResponse{}
return docsisStatusResponse, json.Unmarshal(responseBody, docsisStatusResponse)
}
func makeTimestamp() int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}
func (v *VodafoneStation) getLoginSalts() (*LoginResponseSalts, error) {
responseBody, err := v.doRequest("POST", v.URL+"/api/v1/session/login", "username=admin&password=seeksalthash")
if err != nil {
return nil, err
}
loginResponseSalts := &LoginResponseSalts{}
err = json.Unmarshal(responseBody, loginResponseSalts)
if err != nil {
return nil, err
}
if loginResponseSalts.Error != "ok" {
return nil, fmt.Errorf("Got non error=ok message from vodafone station")
}
return loginResponseSalts, nil
}
func (v *VodafoneStation) doRequest(method, url, body string) ([]byte, error) {
requestBody := strings.NewReader(body)
request, err := http.NewRequest(method, url, requestBody)
if err != nil {
return nil, err
}
if method == "POST" {
request.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
}
request.Header.Set("Referer", "http://192.168.100.1")
request.Header.Set("X-Requested-With", "XMLHttpRequest")
response, err := v.client.Do(request)
if err != nil {
return nil, err
}
if response.Body != nil {
defer response.Body.Close()
}
responseBody, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
loginResponse := &LoginResponse{}
err = json.Unmarshal(responseBody, loginResponse)
if err != nil {
return nil, err
}
if loginResponse.Error != "ok" {
return nil, fmt.Errorf("Got non error=ok message from vodafone station")
}
return loginResponse, nil
return ioutil.ReadAll(response.Body)
}
// GetLoginPassword derives the password using the given salts

115
collector/collector.go Normal file
View file

@ -0,0 +1,115 @@
package collector
import (
"github.com/prometheus/client_golang/prometheus"
"regexp"
"strconv"
)
type Collector struct {
Station *VodafoneStation
}
var (
loginSuccessDesc *prometheus.Desc
loginMessageDesc *prometheus.Desc
userDesc *prometheus.Desc
uidDesc *prometheus.Desc
defaultPasswordDesc *prometheus.Desc
centralFrequencyDesc *prometheus.Desc
powerDesc *prometheus.Desc
snrDesc *prometheus.Desc
lockedDesc *prometheus.Desc
logoutSuccessDesc *prometheus.Desc
logoutMessageDesc *prometheus.Desc
)
const prefix = "vodafone_station_"
func init() {
loginSuccessDesc = prometheus.NewDesc(prefix+"login_success_bool", "1 if the login was successfull", nil, nil)
loginMessageDesc = prometheus.NewDesc(prefix+"login_message_info", "Login message returned by the web interface", []string{"message"}, nil)
userDesc = prometheus.NewDesc(prefix+"user_info", "User name as returned by the web interface", []string{"username"}, nil)
uidDesc = prometheus.NewDesc(prefix+"uid_info", "User id as returned by the web interface", []string{"uid"}, nil)
defaultPasswordDesc = prometheus.NewDesc(prefix+"default_password_bool", "1 if the default password is in use", nil, nil)
channelLabels := []string{"id", "channel_id", "fft", "channel_type"}
centralFrequencyDesc = prometheus.NewDesc(prefix+"central_frequency_hertz", "Central frequency in hertz", channelLabels, nil)
powerDesc = prometheus.NewDesc(prefix+"power_dBmV", "Power in dBmV", channelLabels, nil)
snrDesc = prometheus.NewDesc(prefix+"snr_dB", "SNR in dB", channelLabels, nil)
lockedDesc = prometheus.NewDesc(prefix+"locked_bool", "Locking status", channelLabels, nil)
logoutSuccessDesc = prometheus.NewDesc(prefix+"logout_success_bool", "1 if the logout was successfull", nil, nil)
logoutMessageDesc = prometheus.NewDesc(prefix+"logout_message_info", "Logout message returned by the web interface", []string{"message"}, nil)
}
// Describe implements prometheus.Collector interface's Describe function
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
ch <- loginSuccessDesc
ch <- loginMessageDesc
ch <- userDesc
ch <- uidDesc
ch <- defaultPasswordDesc
ch <- centralFrequencyDesc
ch <- powerDesc
ch <- snrDesc
ch <- snrDesc
ch <- logoutSuccessDesc
ch <- logoutMessageDesc
}
// Collect implements prometheus.Collector interface's Collect function
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
loginresponse, err := c.Station.Login()
if loginresponse != nil {
ch <- prometheus.MustNewConstMetric(loginMessageDesc, prometheus.GaugeValue, 1, loginresponse.Message)
}
if err != nil {
ch <- prometheus.MustNewConstMetric(loginSuccessDesc, prometheus.GaugeValue, 0)
ch <- prometheus.MustNewConstMetric(logoutSuccessDesc, prometheus.GaugeValue, 0)
return
}
ch <- prometheus.MustNewConstMetric(loginSuccessDesc, prometheus.GaugeValue, 1)
ch <- prometheus.MustNewConstMetric(userDesc, prometheus.GaugeValue, 1, loginresponse.Data.User)
ch <- prometheus.MustNewConstMetric(uidDesc, prometheus.GaugeValue, 1, loginresponse.Data.Uid)
ch <- prometheus.MustNewConstMetric(defaultPasswordDesc, prometheus.GaugeValue, bool2float64(loginresponse.Data.DefaultPassword == "Yes"))
docsisStatusResponse, err := c.Station.GetDocsisStatus()
if err == nil && docsisStatusResponse.Data != nil {
for _, downstreamChannel := range docsisStatusResponse.Data.Downstream {
labels := []string{downstreamChannel.Id, downstreamChannel.ChannelId, downstreamChannel.Fft, downstreamChannel.ChannelType}
ch <- prometheus.MustNewConstMetric(centralFrequencyDesc, prometheus.GaugeValue, parse2float(downstreamChannel.CentralFrequency), labels...)
ch <- prometheus.MustNewConstMetric(powerDesc, prometheus.GaugeValue, parse2float(downstreamChannel.Power), labels...)
ch <- prometheus.MustNewConstMetric(snrDesc, prometheus.GaugeValue, parse2float(downstreamChannel.Snr), labels...)
ch <- prometheus.MustNewConstMetric(lockedDesc, prometheus.GaugeValue, bool2float64(downstreamChannel.Locked == "Locked"), labels...)
}
}
logoutresponse, err := c.Station.Logout()
if logoutresponse != nil {
ch <- prometheus.MustNewConstMetric(logoutMessageDesc, prometheus.GaugeValue, 1, logoutresponse.Message)
}
if err != nil {
ch <- prometheus.MustNewConstMetric(logoutSuccessDesc, prometheus.GaugeValue, 0)
}
ch <- prometheus.MustNewConstMetric(logoutSuccessDesc, prometheus.GaugeValue, 1)
}
func parse2float(str string) float64 {
reg := regexp.MustCompile(`[^\.0-9]+`)
processedString := reg.ReplaceAllString(str, "")
value, err := strconv.ParseFloat(processedString, 64)
if err != nil {
return 0
}
return value
}
func bool2float64(b bool) float64 {
if b {
return 1
}
return 0
}

64
main.go Normal file
View file

@ -0,0 +1,64 @@
package main
import (
"flag"
"fmt"
"github.com/fluepke/vodafone-station-exporter/collector"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/log"
"net/http"
"os"
)
const version = "0.0.1"
var (
showVersion = flag.Bool("version", false, "Print version and exit")
listenAddress = flag.String("web.listen-address", "[::]:9420", "Address to listen on")
metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics")
vodafoneStationUrl = flag.String("vodafone.station-url", "http://192.168.0.1", "Vodafone station URL. For bridge mode this is 192.168.100.1 (note: Configure a route if using bridge mode)")
vodafoneStationPassword = flag.String("vodafone.station-password", "How is the default password calculated? mhmm", "Password for logging into the Vodafone station")
)
func main() {
flag.Parse()
if *showVersion {
fmt.Println("vodafone-station-exporter")
fmt.Printf("Version: %s\n", version)
fmt.Println("Author: @fluepke")
fmt.Println("Prometheus Exporter for the Vodafone Station (CGA4233DE)")
os.Exit(0)
}
startServer()
}
func startServer() {
log.Infof("Starting vodafone-station-exporter (version %s)", version)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`<html>
<head><title>vodafone-station-exporter (Version ` + version + `)</title></head>
<body>
<h1>vodafone-station-exporter</h1>
<a href="/metrics">metrics</a>
</body>
</html>`))
})
http.HandleFunc(*metricsPath, handleMetricsRequest)
log.Infof("Listening on %s", *listenAddress)
log.Fatal(http.ListenAndServe(*listenAddress, nil))
}
func handleMetricsRequest(w http.ResponseWriter, request *http.Request) {
registry := prometheus.NewRegistry()
registry.MustRegister(&collector.Collector{
Station: collector.NewVodafoneStation(*vodafoneStationUrl, *vodafoneStationPassword),
})
promhttp.HandlerFor(registry, promhttp.HandlerOpts{
ErrorLog: log.NewErrorLogger(),
ErrorHandling: promhttp.ContinueOnError,
}).ServeHTTP(w, request)
}