Implement basic vodafone station exporter
This commit is contained in:
parent
499736e5bf
commit
fb130d2dc9
3 changed files with 340 additions and 20 deletions
181
collector/api.go
181
collector/api.go
|
|
@ -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
115
collector/collector.go
Normal 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
64
main.go
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue