Add basic prometheus freebox exporter
This commit is contained in:
parent
30dfedee67
commit
b14b172649
5 changed files with 931 additions and 0 deletions
57
fbx/cert.go
Normal file
57
fbx/cert.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package fbx
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
)
|
||||
|
||||
const (
|
||||
FreeboxRootCA = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFmjCCA4KgAwIBAgIJAKLyz15lYOrYMA0GCSqGSIb3DQEBCwUAMFoxCzAJBgNV
|
||||
BAYTAkZSMQ8wDQYDVQQIDAZGcmFuY2UxDjAMBgNVBAcMBVBhcmlzMRAwDgYDVQQK
|
||||
DAdGcmVlYm94MRgwFgYDVQQDDA9GcmVlYm94IFJvb3QgQ0EwHhcNMTUwNzMwMTUw
|
||||
OTIwWhcNMzUwNzI1MTUwOTIwWjBaMQswCQYDVQQGEwJGUjEPMA0GA1UECAwGRnJh
|
||||
bmNlMQ4wDAYDVQQHDAVQYXJpczEQMA4GA1UECgwHRnJlZWJveDEYMBYGA1UEAwwP
|
||||
RnJlZWJveCBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
|
||||
xqYIvq8538SH6BJ99jDlOPoyDBrlwKEp879oYplicTC2/p0X66R/ft0en1uSQadC
|
||||
sL/JTyfgyJAgI1Dq2Y5EYVT/7G6GBtVH6Bxa713mM+I/v0JlTGFalgMqamMuIRDQ
|
||||
tdyvqEIs8DcfGB/1l2A8UhKOFbHQsMcigxOe9ZodMhtVNn0mUyG+9Zgu1e/YMhsS
|
||||
iG4Kqap6TGtk80yruS1mMWVSgLOq9F5BGD4rlNlWLo0C3R10mFCpqvsFU+g4kYoA
|
||||
dTxaIpi1pgng3CGLE0FXgwstJz8RBaZObYEslEYKDzmer5zrU1pVHiwkjsgwbnuy
|
||||
WtM1Xry3Jxc7N/i1rxFmN/4l/Tcb1F7x4yVZmrzbQVptKSmyTEvPvpzqzdxVWuYi
|
||||
qIFSe/njl8dX9v5hjbMo4CeLuXIRE4nSq2A7GBm4j9Zb6/l2WIBpnCKtwUVlroKw
|
||||
NBgB6zHg5WI9nWGuy3ozpP4zyxqXhaTgrQcDDIG/SQS1GOXKGdkCcSa+VkJ0jTf5
|
||||
od7PxBn9/TuN0yYdgQK3YDjD9F9+CLp8QZK1bnPdVGywPfL1iztngF9J6JohTyL/
|
||||
VMvpWfS/X6R4Y3p8/eSio4BNuPvm9r0xp6IMpW92V8SYL0N6TQQxzZYgkLV7TbQI
|
||||
Hw6v64yMbbF0YS9VjS0sFpZcFERVQiodRu7nYNC1jy8CAwEAAaNjMGEwHQYDVR0O
|
||||
BBYEFD2erMkECujilR0BuER09FdsYIebMB8GA1UdIwQYMBaAFD2erMkECujilR0B
|
||||
uER09FdsYIebMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqG
|
||||
SIb3DQEBCwUAA4ICAQAZ2Nx8mWIWckNY8X2t/ymmCbcKxGw8Hn3BfTDcUWQ7GLRf
|
||||
MGzTqxGSLBQ5tENaclbtTpNrqPv2k6LY0VjfrKoTSS8JfXkm6+FUtyXpsGK8MrLL
|
||||
hZ/YdADTfbbWOjjD0VaPUoglvo2N4n7rOuRxVYIij11fL/wl3OUZ7GHLgL3qXSz0
|
||||
+RGW+1oZo8HQ7pb6RwLfv42Gf+2gyNBckM7VVh9R19UkLCsHFqhFBbUmqwJgNA2/
|
||||
3twgV6Y26qlyHXXODUfV3arLCwFoNB+IIrde1E/JoOry9oKvF8DZTo/Qm6o2KsdZ
|
||||
dxs/YcIUsCvKX8WCKtH6la/kFCUcXIb8f1u+Y4pjj3PBmKI/1+Rs9GqB0kt1otyx
|
||||
Q6bqxqBSgsrkuhCfRxwjbfBgmXjIZ/a4muY5uMI0gbl9zbMFEJHDojhH6TUB5qd0
|
||||
JJlI61gldaT5Ci1aLbvVcJtdeGhElf7pOE9JrXINpP3NOJJaUSueAvxyj/WWoo0v
|
||||
4KO7njox8F6jCHALNDLdTsX0FTGmUZ/s/QfJry3VNwyjCyWDy1ra4KWoqt6U7SzM
|
||||
d5jENIZChM8TnDXJzqc+mu00cI3icn9bV9flYCXLTIsprB21wVSMh0XeBGylKxeB
|
||||
S27oDfFq04XSox7JM9HdTt2hLK96x1T7FpFrBTnALzb7vHv9MhXqAT90fPR/8A==
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
)
|
||||
|
||||
func NewTlsConfig() *tls.Config {
|
||||
caCertPool := x509.NewCertPool()
|
||||
if caCertPool.AppendCertsFromPEM([]byte(FreeboxRootCA)) == false {
|
||||
panic("Could not add the certificate")
|
||||
}
|
||||
|
||||
// Setup HTTPS client
|
||||
tlsConfig := &tls.Config{
|
||||
RootCAs: caCertPool,
|
||||
}
|
||||
tlsConfig.BuildNameToCertificate()
|
||||
return tlsConfig
|
||||
}
|
373
fbx/connection.go
Normal file
373
fbx/connection.go
Normal file
|
@ -0,0 +1,373 @@
|
|||
package fbx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/trazfr/freebox-exporter/log"
|
||||
)
|
||||
|
||||
type FreeboxAPIVersion struct {
|
||||
APIDomain string `json:"api_domain"`
|
||||
UID string `json:"uid"`
|
||||
HTTPSAvailable bool `json:"https_available"`
|
||||
HTTPSPort uint16 `json:"https_port"`
|
||||
DeviceName string `json:"device_name"`
|
||||
APIVersion string `json:"api_version"`
|
||||
APIBaseURL string `json:"api_base_url"`
|
||||
DeviceType string `json:"device_type"`
|
||||
}
|
||||
|
||||
type FreeboxConnection struct {
|
||||
client *http.Client
|
||||
API FreeboxAPIVersion `json:"api"`
|
||||
AppToken string `json:"app_token"`
|
||||
sessionToken string
|
||||
challenge string
|
||||
passwordSalt string
|
||||
}
|
||||
|
||||
type freeboxAuthorizePostRequest struct {
|
||||
AppID string `json:"app_id"`
|
||||
AppName string `json:"app_name"`
|
||||
AppVersion string `json:"app_version"`
|
||||
DeviceName string `json:"device_name"`
|
||||
}
|
||||
|
||||
type freeboxResponseBase struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"msg"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
}
|
||||
|
||||
type freeboxResponseResultBase struct {
|
||||
Challenge string `json:"challenge"`
|
||||
PasswordSalt string `json:"password_salt"`
|
||||
}
|
||||
|
||||
const (
|
||||
apiVersionURL = "http://mafreebox.freebox.fr/api_version"
|
||||
)
|
||||
|
||||
func getChallengeAuthorizationRequest() freeboxAuthorizePostRequest {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return freeboxAuthorizePostRequest{
|
||||
AppID: "com.github.trazfr.fboxexp",
|
||||
AppName: "prometheus-freebox-exporter",
|
||||
AppVersion: "0.0.1",
|
||||
DeviceName: hostname,
|
||||
}
|
||||
}
|
||||
|
||||
func newHttpClient() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: NewTlsConfig(),
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
DisableCompression: true,
|
||||
},
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func getField(i interface{}, fieldNames ...string) string {
|
||||
log.Debug.Println(i, fieldNames)
|
||||
if s, ok := i.(string); ok && len(fieldNames) == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
fieldName := fieldNames[0]
|
||||
nextFields := fieldNames[1:]
|
||||
|
||||
value := reflect.ValueOf(i)
|
||||
if value.Type().Kind() == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
}
|
||||
field := value.FieldByName(fieldName)
|
||||
if field.IsValid() == false {
|
||||
return ""
|
||||
}
|
||||
return getField(field.Interface(), nextFields...)
|
||||
}
|
||||
|
||||
/*
|
||||
* FreeboxConnection
|
||||
*/
|
||||
|
||||
func (f *FreeboxAPIVersion) isValid() bool {
|
||||
return f.APIDomain != "" &&
|
||||
f.UID != "" &&
|
||||
f.HTTPSAvailable == true &&
|
||||
f.HTTPSPort != 0 &&
|
||||
f.DeviceName != "" &&
|
||||
f.APIVersion != "" &&
|
||||
f.APIBaseURL != "" &&
|
||||
f.DeviceType != ""
|
||||
}
|
||||
|
||||
func (f *FreeboxAPIVersion) getURL(path string, miscPath []interface{}) (string, error) {
|
||||
if f.isValid() == false {
|
||||
return "", errors.New("Invalid FreeboxAPIVersion")
|
||||
}
|
||||
versionSplit := strings.Split(f.APIVersion, ".")
|
||||
if len(versionSplit) != 2 {
|
||||
return "", fmt.Errorf("Could not decode the api version \"%s\"", f.APIVersion)
|
||||
}
|
||||
args := make([]interface{}, len(miscPath)+4)
|
||||
args[0] = f.APIDomain
|
||||
args[1] = f.HTTPSPort
|
||||
args[2] = f.APIBaseURL
|
||||
args[3] = versionSplit[0]
|
||||
if len(miscPath) > 0 {
|
||||
copy(args[4:], miscPath)
|
||||
}
|
||||
return fmt.Sprintf("https://%s:%d%sv%s/"+path, args...), nil
|
||||
}
|
||||
|
||||
func (f *FreeboxAPIVersion) refresh(client *http.Client) error {
|
||||
log.Debug.Printf("Get API version: %s\n", apiVersionURL)
|
||||
// get api version
|
||||
r, err := client.Get(apiVersionURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
if err := json.NewDecoder(r.Body).Decode(f); err != nil {
|
||||
return err
|
||||
}
|
||||
if f.isValid() == false {
|
||||
return errors.New("Could not get the API version")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
* FreeboxConnection
|
||||
*/
|
||||
|
||||
func NewFreeboxConnection() *FreeboxConnection {
|
||||
return &FreeboxConnection{
|
||||
client: newHttpClient(),
|
||||
API: FreeboxAPIVersion{},
|
||||
AppToken: "",
|
||||
challenge: "",
|
||||
passwordSalt: "",
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FreeboxConnection) call(auth bool, method string, path string, pathFmt []interface{}, body interface{}, out interface{}) error {
|
||||
auth = auth && f.sessionToken != ""
|
||||
url, err := f.API.getURL(path, pathFmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug.Println(method, url)
|
||||
|
||||
var bodyReader io.Reader
|
||||
{
|
||||
buffer := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(buffer).Encode(body); err != nil {
|
||||
return err
|
||||
}
|
||||
bodyReader = buffer
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, bodyReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if auth {
|
||||
req.Header.Set("X-Fbx-App-Auth", f.sessionToken)
|
||||
}
|
||||
res, err := f.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
return err
|
||||
}
|
||||
// if we have a result.challenge / result.password_salt, take it
|
||||
needSessionTokenRefresh := false
|
||||
if s := getField(out, "Result", "Challenge"); s != "" && s != f.challenge {
|
||||
f.challenge = s
|
||||
needSessionTokenRefresh = true
|
||||
}
|
||||
if s := getField(out, "Result", "PasswordSalt"); s != "" && s != f.passwordSalt {
|
||||
f.passwordSalt = s
|
||||
needSessionTokenRefresh = true
|
||||
}
|
||||
if needSessionTokenRefresh {
|
||||
if f.refreshSessionToken(false) == false {
|
||||
return errors.New("Could not refresh the session token")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FreeboxConnection) get(out interface{}, path string, pathFmt ...interface{}) error {
|
||||
return f.getInternal(true, out, path, pathFmt...)
|
||||
}
|
||||
|
||||
func (f *FreeboxConnection) getInternal(auth bool, out interface{}, path string, pathFmt ...interface{}) error {
|
||||
return f.call(auth, "GET", path, pathFmt, nil, out)
|
||||
}
|
||||
|
||||
func (f *FreeboxConnection) post(in interface{}, out interface{}, path string, pathFmt ...interface{}) error {
|
||||
return f.call(true, "POST", path, pathFmt, in, out)
|
||||
}
|
||||
|
||||
func (f *FreeboxConnection) test(auth bool) error {
|
||||
r := struct {
|
||||
freeboxResponseBase
|
||||
Result struct {
|
||||
freeboxResponseResultBase
|
||||
LoggedIn bool `json:"logged_in"`
|
||||
} `json:"result"`
|
||||
}{}
|
||||
if err := f.getInternal(auth, &r, "login"); err != nil {
|
||||
return err
|
||||
}
|
||||
if auth && r.Result.LoggedIn != true {
|
||||
return errors.New("Not logged in")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FreeboxConnection) askAuthorization() error {
|
||||
postResponse := struct {
|
||||
freeboxResponseBase
|
||||
Result struct {
|
||||
freeboxResponseResultBase
|
||||
AppToken string `json:"app_token"`
|
||||
TrackID int64 `json:"track_id"`
|
||||
} `json:"result"`
|
||||
}{}
|
||||
if err := f.post(getChallengeAuthorizationRequest(), &postResponse, "login/authorize"); err != nil {
|
||||
return err
|
||||
}
|
||||
if postResponse.Success == false {
|
||||
return fmt.Errorf("The POST to login/authorize/ failed: code=\"%v\" msg=\"%v\"", postResponse.ErrorCode, postResponse.Message)
|
||||
}
|
||||
f.AppToken = postResponse.Result.AppToken
|
||||
|
||||
pending := false
|
||||
accessGranted := false
|
||||
for accessGranted == false {
|
||||
r := struct {
|
||||
freeboxResponseBase
|
||||
Result struct {
|
||||
freeboxResponseResultBase
|
||||
Status string `json:"status"`
|
||||
} `json:"result"`
|
||||
}{}
|
||||
if err := f.get(&r, "login/authorize/%d", postResponse.Result.TrackID); err != nil {
|
||||
return err
|
||||
}
|
||||
if postResponse.Success == false {
|
||||
return errors.New("The GET to login/authorize failed")
|
||||
}
|
||||
switch r.Result.Status {
|
||||
case "pending":
|
||||
if pending == false {
|
||||
fmt.Println("Please accept the login on the Freebox Server")
|
||||
pending = true
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
case "granted":
|
||||
accessGranted = true
|
||||
default:
|
||||
return fmt.Errorf("Access is %s", r.Result.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FreeboxConnection) refreshSessionToken(force bool) (ok bool) {
|
||||
log.Debug.Println("Refresh session token. Force:", force)
|
||||
oldChallenge := f.challenge
|
||||
if force || f.challenge == "" {
|
||||
if err := f.test(false); err != nil {
|
||||
log.Error.Println("Refresh token:", err.Error())
|
||||
return false
|
||||
}
|
||||
}
|
||||
if f.challenge == "" {
|
||||
panic("No challenge...")
|
||||
}
|
||||
|
||||
if f.AppToken != "" && oldChallenge != f.challenge {
|
||||
hash := hmac.New(sha1.New, []byte(f.AppToken))
|
||||
hash.Write([]byte(f.challenge))
|
||||
password := hex.EncodeToString(hash.Sum(nil))
|
||||
|
||||
authorizationRequest := getChallengeAuthorizationRequest()
|
||||
req := struct {
|
||||
AppID string `json:"app_id"`
|
||||
Password string `json:"password"`
|
||||
}{
|
||||
AppID: authorizationRequest.AppID,
|
||||
Password: password,
|
||||
}
|
||||
res := struct {
|
||||
freeboxResponseBase
|
||||
Result struct {
|
||||
freeboxResponseResultBase
|
||||
SessionToken string `json:"session_token"`
|
||||
} `json:"result"`
|
||||
}{}
|
||||
f.sessionToken = ""
|
||||
if err := f.post(&req, &res, "login/session"); err == nil {
|
||||
log.Debug.Println("login result:", res.Success, "token:", res.Result.SessionToken)
|
||||
if res.Success && res.Result.SessionToken != "" {
|
||||
f.sessionToken = res.Result.SessionToken
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return f.AppToken != ""
|
||||
}
|
||||
|
||||
func (f *FreeboxConnection) Login() error {
|
||||
if f.API.isValid() == false {
|
||||
if err := f.API.refresh(f.client); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if f.refreshSessionToken(true) == true {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := f.askAuthorization()
|
||||
if err == nil {
|
||||
if f.refreshSessionToken(false) == false {
|
||||
return errors.New("Could not refresh the session token")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *FreeboxConnection) Logout() error {
|
||||
res := freeboxResponseBase{}
|
||||
return f.post(nil, &res, "logout")
|
||||
}
|
161
fbx/get_metrics.go
Normal file
161
fbx/get_metrics.go
Normal file
|
@ -0,0 +1,161 @@
|
|||
package fbx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MetricsFreeboxSystem https://dev.freebox.fr/sdk/os/system/
|
||||
type MetricsFreeboxSystem struct {
|
||||
FirmwareVersion string `json:"firmware_version"`
|
||||
Mac string `json:"mac"`
|
||||
Serial string `json:"serial"`
|
||||
Uptime string `json:"uptime"`
|
||||
UptimeValue *int64 `json:"uptime_val"`
|
||||
BoardName string `json:"board_name"`
|
||||
TempCPUM *int64 `json:"temp_cpum"`
|
||||
TempSW *int64 `json:"temp_sw"`
|
||||
TempCPUB *int64 `json:"temp_cpub"`
|
||||
FanRpm *int64 `json:"fan_rpm"`
|
||||
BoxAuthenticated *bool `json:"box_authenticated"`
|
||||
DiskStatus string `json:"disk_status"`
|
||||
BoxFlavor string `json:"box_flavor"`
|
||||
UserMainStorage string `json:"user_main_storage"`
|
||||
}
|
||||
|
||||
// MetricsFreeboxConnection https://dev.freebox.fr/sdk/os/connection/
|
||||
type MetricsFreeboxConnection struct {
|
||||
State string `json:"state"`
|
||||
Type string `json:"type"`
|
||||
Media string `json:"media"`
|
||||
IPv4 string `json:"ipv4"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
RateUp *int64 `json:"rate_up"`
|
||||
RateDown *int64 `json:"rate_down"`
|
||||
BandwithUp *int64 `json:"bandwith_up"`
|
||||
BandwithDown *int64 `json:"bandwith_down"`
|
||||
BytesUp *int64 `json:"bytes_up"`
|
||||
BytesDown *int64 `json:"bytes_down"`
|
||||
// ipv4_port_range
|
||||
}
|
||||
|
||||
// MetricsFreeboxConnectionXdslStats https://dev.freebox.fr/sdk/os/connection/#XdslStats
|
||||
type MetricsFreeboxConnectionXdslStats struct {
|
||||
Maxrate *int64 `json:"maxrate"`
|
||||
Rate *int64 `json:"rate"`
|
||||
Snr *int64 `json:"snr"`
|
||||
Attn *int64 `json:"attn"`
|
||||
Snr10 *int64 `json:"snr_10"`
|
||||
Attn10 *int64 `json:"attn_10"`
|
||||
Fec *int64 `json:"fec"`
|
||||
Crc *int64 `json:"crc"`
|
||||
Hec *int64 `json:"hec"`
|
||||
Es *int64 `json:"es"`
|
||||
Ses *int64 `json:"ses"`
|
||||
Phyr *bool `json:"phyr"`
|
||||
Ginp *bool `json:"ginp"`
|
||||
Nitro *bool `json:"nitro"`
|
||||
Rxmt *int64 `json:"rxmt"` // phyr
|
||||
RxmtCorr *int64 `json:"rxmt_corr"` // phyr
|
||||
RxmtUncorr *int64 `json:"rxmt_uncorr"` // phyr
|
||||
RtxTx *int64 `json:"rtx_tx"` // ginp
|
||||
RtxC *int64 `json:"rtx_c"` // ginp
|
||||
RtxUc *int64 `json:"rtx_uc"` // ginp
|
||||
}
|
||||
|
||||
// MetricsFreeboxConnectionXdsl https://dev.freebox.fr/sdk/os/connection/#XdslInfos
|
||||
type MetricsFreeboxConnectionXdsl struct {
|
||||
// https://dev.freebox.fr/sdk/os/connection/#XdslStatus
|
||||
Status *struct {
|
||||
Status string `json:"status"`
|
||||
Protocol string `json:"protocol"`
|
||||
Modulation string `json:"modulation"`
|
||||
Uptime *int64 `json:"uptime"`
|
||||
} `json:"status"`
|
||||
Down *MetricsFreeboxConnectionXdslStats `json:"down"`
|
||||
Up *MetricsFreeboxConnectionXdslStats `json:"up"`
|
||||
}
|
||||
|
||||
// MetricsFreeboxConnectionFtth https://dev.freebox.fr/sdk/os/connection/#FtthStatus
|
||||
type MetricsFreeboxConnectionFtth struct {
|
||||
SfpPresent *bool `json:"sfp_present"`
|
||||
SfpAlimOk *bool `json:"sfp_alim_ok"`
|
||||
SfpHasPowerReport *bool `json:"sfp_has_power_report"`
|
||||
SfpHasSignal *bool `json:"sfp_has_signal"`
|
||||
Link *bool `json:"link"`
|
||||
SfpSerial string `json:"sfp_serial"`
|
||||
SfpModel string `json:"sfp_model"`
|
||||
SfpVendor string `json:"sfp_vendor"`
|
||||
SfpPwrTx *int64 `json:"sfp_pwr_tx"`
|
||||
SfpPwrRx *int64 `json:"sfp_pwr_rx"`
|
||||
}
|
||||
|
||||
// MetricsFreeboxConnectionAll is the result of GetMetricsConnection()
|
||||
type MetricsFreeboxConnectionAll struct {
|
||||
MetricsFreeboxConnection
|
||||
Xdsl *MetricsFreeboxConnectionXdsl
|
||||
Ftth *MetricsFreeboxConnectionFtth
|
||||
}
|
||||
|
||||
// GetMetricsSystem http://mafreebox.freebox.fr/api/v5/system/
|
||||
func (f *FreeboxConnection) GetMetricsSystem() (*MetricsFreeboxSystem, error) {
|
||||
res := new(MetricsFreeboxSystem)
|
||||
callRes := struct {
|
||||
freeboxResponseBase
|
||||
Result *MetricsFreeboxSystem `json:"result"`
|
||||
}{
|
||||
Result: res,
|
||||
}
|
||||
err := f.get(&callRes, "system")
|
||||
if callRes.Success == true || err != nil {
|
||||
return res, err
|
||||
}
|
||||
return nil, fmt.Errorf("GET system: success=false erc=%q msg=%q", callRes.ErrorCode, callRes.Message)
|
||||
}
|
||||
|
||||
// GetMetricsConnection http://mafreebox.freebox.fr/api/v5/connection/
|
||||
func (f *FreeboxConnection) GetMetricsConnection() (*MetricsFreeboxConnectionAll, error) {
|
||||
result := new(MetricsFreeboxConnectionAll)
|
||||
{
|
||||
metricsCnx := struct {
|
||||
freeboxResponseBase
|
||||
Result *MetricsFreeboxConnection `json:"result"`
|
||||
}{
|
||||
Result: &result.MetricsFreeboxConnection,
|
||||
}
|
||||
if err := f.get(&metricsCnx, "connection"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
switch result.Media {
|
||||
case "xdsl":
|
||||
// http://mafreebox.freebox.fr/api/v5/connection/xdsl/
|
||||
// https://dev.freebox.fr/sdk/os/connection/#get-the-current-xdsl-infos
|
||||
result.Xdsl = new(MetricsFreeboxConnectionXdsl)
|
||||
metricsXdsl := struct {
|
||||
freeboxResponseBase
|
||||
Result *MetricsFreeboxConnectionXdsl `json:"result"`
|
||||
}{
|
||||
Result: result.Xdsl,
|
||||
}
|
||||
|
||||
if err := f.get(&metricsXdsl, "connection/xdsl"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "ftth":
|
||||
// http://mafreebox.freebox.fr/api/v5/connection/ftth/
|
||||
// https://dev.freebox.fr/sdk/os/connection/#get-the-current-ftth-status
|
||||
result.Ftth = new(MetricsFreeboxConnectionFtth)
|
||||
metricsFtth := struct {
|
||||
freeboxResponseBase
|
||||
Result *MetricsFreeboxConnectionFtth `json:"result"`
|
||||
}{
|
||||
Result: result.Ftth,
|
||||
}
|
||||
if err := f.get(&metricsFtth, "connection/ftth"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
36
log/log.go
Normal file
36
log/log.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
var (
|
||||
Debug *log.Logger
|
||||
Info *log.Logger
|
||||
Warning *log.Logger
|
||||
Error *log.Logger
|
||||
)
|
||||
|
||||
func Init(
|
||||
debugHandle io.Writer,
|
||||
infoHandle io.Writer,
|
||||
warningHandle io.Writer,
|
||||
errorHandle io.Writer) {
|
||||
|
||||
Debug = log.New(debugHandle,
|
||||
"DEBUG: ",
|
||||
log.Ldate|log.Ltime|log.Lshortfile)
|
||||
|
||||
Info = log.New(infoHandle,
|
||||
"INFO: ",
|
||||
log.Ldate|log.Ltime|log.Lshortfile)
|
||||
|
||||
Warning = log.New(warningHandle,
|
||||
"WARNING: ",
|
||||
log.Ldate|log.Ltime|log.Lshortfile)
|
||||
|
||||
Error = log.New(errorHandle,
|
||||
"ERROR: ",
|
||||
log.Ldate|log.Ltime|log.Lshortfile)
|
||||
}
|
304
main.go
Normal file
304
main.go
Normal file
|
@ -0,0 +1,304 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
"github.com/trazfr/freebox-exporter/fbx"
|
||||
"github.com/trazfr/freebox-exporter/log"
|
||||
)
|
||||
|
||||
const (
|
||||
namespace = "freebox"
|
||||
)
|
||||
|
||||
var (
|
||||
info = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "info",
|
||||
Help: "A constant metric with value=0. Various information about the Freebox",
|
||||
}, []string{"firmware", "mac", "serial", "boardname", "box_flavor", "state", "media", "ipv4", "ipv6"})
|
||||
|
||||
collectorSystemUptimeValue = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "system_uptime",
|
||||
Help: "freebox uptime (in seconds)",
|
||||
})
|
||||
collectorVecSystemTemp = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "system_temp_degrees",
|
||||
Help: "temperature (°C)",
|
||||
}, []string{"probe"})
|
||||
collectorSystemTempCpum = collectorVecSystemTemp.WithLabelValues("cpum")
|
||||
collectorSystemTempCpub = collectorVecSystemTemp.WithLabelValues("cpub")
|
||||
collectorSystemTempSw = collectorVecSystemTemp.WithLabelValues("sw")
|
||||
collectorSystemFanRpm = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "system_fan_rpm",
|
||||
Help: "fan rpm",
|
||||
})
|
||||
collectorVecConnectionRate = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "connection_rate",
|
||||
Help: "current upload/download rate in byte/s",
|
||||
}, []string{"dir"})
|
||||
collectorConnectionRateUp = collectorVecConnectionRate.WithLabelValues("up")
|
||||
collectorConnectionRateDown = collectorVecConnectionRate.WithLabelValues("down")
|
||||
collectorVecConnectionBandwith = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "connection_bandwith",
|
||||
Help: " available upload/download bandwidth in bit/s",
|
||||
}, []string{"dir"})
|
||||
collectorConnectionBandwithUp = collectorVecConnectionBandwith.WithLabelValues("up")
|
||||
collectorConnectionBandwithDown = collectorVecConnectionBandwith.WithLabelValues("down")
|
||||
collectorVecConnectionBytes = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "connection_bytes",
|
||||
Help: "total uploaded/downloaded bytes since last connection",
|
||||
}, []string{"dir"})
|
||||
collectorConnectionBytesUp = collectorVecConnectionBytes.WithLabelValues("up")
|
||||
collectorConnectionBytesDown = collectorVecConnectionBytes.WithLabelValues("down")
|
||||
collectorVecConnectionUptime = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "connection_uptime",
|
||||
Help: "Uptime in seconds",
|
||||
}, []string{"media"})
|
||||
collectorConnectionUptimeXdsl = collectorVecConnectionUptime.WithLabelValues("xdsl")
|
||||
collectorVecConnectionXdslMaxrate = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "connection_xdsl_maxrate_kbps",
|
||||
Help: "ATM max rate in kbit/s",
|
||||
}, []string{"dir"})
|
||||
collectorConnectionXdslMaxrateUp = collectorVecConnectionXdslMaxrate.WithLabelValues("up")
|
||||
collectorConnectionXdslMaxrateDown = collectorVecConnectionXdslMaxrate.WithLabelValues("down")
|
||||
collectorVecConnectionXdslRate = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "connection_xdsl_rate_kbps",
|
||||
Help: "ATM rate in kbit/s",
|
||||
}, []string{"dir"})
|
||||
collectorConnectionXdslRateUp = collectorVecConnectionXdslRate.WithLabelValues("up")
|
||||
collectorConnectionXdslRateDown = collectorVecConnectionXdslRate.WithLabelValues("down")
|
||||
collectorVecConnectionXdslSnr = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "connection_xdsl_snr_db",
|
||||
Help: "in Db",
|
||||
}, []string{"dir"})
|
||||
collectorConnectionXdslSnrUp = collectorVecConnectionXdslSnr.WithLabelValues("up")
|
||||
collectorConnectionXdslSnrDown = collectorVecConnectionXdslSnr.WithLabelValues("down")
|
||||
collectorVecConnectionXdslAttn = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: "connection_xdsl_attn_db",
|
||||
Help: "in Db",
|
||||
}, []string{"dir"})
|
||||
collectorConnectionXdslAttnUp = collectorVecConnectionXdslAttn.WithLabelValues("up")
|
||||
collectorConnectionXdslAttnDown = collectorVecConnectionXdslAttn.WithLabelValues("down")
|
||||
)
|
||||
|
||||
type context struct {
|
||||
client *http.Client
|
||||
freebox *fbx.FreeboxConnection
|
||||
used map[prometheus.Metric]bool
|
||||
}
|
||||
|
||||
func (c *context) Describe(ch chan<- *prometheus.Desc) {
|
||||
log.Debug.Println("Describe")
|
||||
ch2 := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
c.Collect(ch2)
|
||||
close(ch2)
|
||||
}()
|
||||
metrics := make([]prometheus.Metric, 16)
|
||||
for v := range ch2 {
|
||||
metrics = append(metrics, v)
|
||||
ch <- v.Desc()
|
||||
}
|
||||
for _, v := range metrics {
|
||||
c.used[v] = true
|
||||
}
|
||||
}
|
||||
|
||||
func (c *context) Collect(ch chan<- prometheus.Metric) {
|
||||
log.Debug.Println("Collect")
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
var firmwareVersion string
|
||||
var mac string
|
||||
var serial string
|
||||
var boardName string
|
||||
var boxFlavor string
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.Debug.Println("Collect system")
|
||||
if m, err := c.freebox.GetMetricsSystem(); err == nil {
|
||||
firmwareVersion = m.FirmwareVersion
|
||||
mac = m.Mac
|
||||
serial = m.Serial
|
||||
boardName = m.BoardName
|
||||
boxFlavor = m.BoxFlavor
|
||||
|
||||
c.collectGauge(ch, collectorSystemUptimeValue, m.UptimeValue)
|
||||
c.collectGauge(ch, collectorSystemTempCpum, m.TempCPUM)
|
||||
c.collectGauge(ch, collectorSystemTempCpub, m.TempCPUB)
|
||||
c.collectGauge(ch, collectorSystemTempSw, m.TempSW)
|
||||
c.collectGauge(ch, collectorSystemFanRpm, m.FanRpm)
|
||||
} else {
|
||||
log.Info.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
var cnxState string
|
||||
var cnxMedia string
|
||||
var cnxIPv4 string
|
||||
var cnxIPv6 string
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
log.Debug.Println("Collect connection")
|
||||
if m, err := c.freebox.GetMetricsConnection(); err == nil {
|
||||
cnxState = m.State
|
||||
cnxMedia = m.Media
|
||||
cnxIPv4 = m.IPv4
|
||||
cnxIPv6 = m.IPv6
|
||||
|
||||
c.collectGauge(ch, collectorConnectionRateUp, m.RateUp)
|
||||
c.collectGauge(ch, collectorConnectionRateDown, m.RateDown)
|
||||
c.collectGauge(ch, collectorConnectionBandwithUp, m.BandwithUp)
|
||||
c.collectGauge(ch, collectorConnectionBandwithDown, m.BandwithDown)
|
||||
c.collectGauge(ch, collectorConnectionBytesUp, m.BytesUp)
|
||||
c.collectGauge(ch, collectorConnectionBytesDown, m.BytesDown)
|
||||
if m.Xdsl != nil {
|
||||
if m.Xdsl.Status != nil {
|
||||
c.collectGauge(ch, collectorConnectionUptimeXdsl, m.Xdsl.Status.Uptime)
|
||||
}
|
||||
if m.Xdsl.Up != nil {
|
||||
x := m.Xdsl.Up
|
||||
c.collectGauge(ch, collectorConnectionXdslMaxrateUp, x.Maxrate)
|
||||
c.collectGauge(ch, collectorConnectionXdslRateUp, x.Rate)
|
||||
if c.use(collectorConnectionXdslSnrUp) && x.Snr10 != nil {
|
||||
collectorConnectionXdslSnrUp.Set(float64(*x.Snr10) / 10)
|
||||
collectorConnectionXdslSnrUp.Collect(ch)
|
||||
} else {
|
||||
c.collectGauge(ch, collectorConnectionXdslSnrUp, x.Snr)
|
||||
}
|
||||
if c.use(collectorConnectionXdslAttnUp) && x.Attn10 != nil {
|
||||
collectorConnectionXdslAttnUp.Set(float64(*x.Attn10) / 10)
|
||||
collectorConnectionXdslAttnUp.Collect(ch)
|
||||
} else {
|
||||
c.collectGauge(ch, collectorConnectionXdslAttnUp, x.Attn)
|
||||
}
|
||||
}
|
||||
if m.Xdsl.Down != nil {
|
||||
x := m.Xdsl.Down
|
||||
c.collectGauge(ch, collectorConnectionXdslMaxrateDown, x.Maxrate)
|
||||
c.collectGauge(ch, collectorConnectionXdslRateDown, x.Rate)
|
||||
if c.use(collectorConnectionXdslSnrDown) && x.Snr10 != nil {
|
||||
collectorConnectionXdslSnrDown.Set(float64(*x.Snr10) / 10)
|
||||
collectorConnectionXdslSnrDown.Collect(ch)
|
||||
} else {
|
||||
c.collectGauge(ch, collectorConnectionXdslSnrDown, x.Snr)
|
||||
}
|
||||
if c.use(collectorConnectionXdslAttnDown) && x.Attn10 != nil {
|
||||
collectorConnectionXdslAttnDown.Set(float64(*x.Attn10) / 10)
|
||||
collectorConnectionXdslAttnDown.Collect(ch)
|
||||
} else {
|
||||
c.collectGauge(ch, collectorConnectionXdslAttnDown, x.Attn)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Info.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
info.WithLabelValues(firmwareVersion,
|
||||
mac,
|
||||
serial,
|
||||
boardName,
|
||||
boxFlavor,
|
||||
cnxState,
|
||||
cnxMedia,
|
||||
cnxIPv4,
|
||||
cnxIPv6).Collect(ch)
|
||||
}
|
||||
|
||||
func (c *context) collectGauge(ch chan<- prometheus.Metric, i prometheus.Gauge, value *int64) {
|
||||
if c.use(i) && value != nil {
|
||||
i.Set(float64(*value))
|
||||
i.Collect(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *context) use(i prometheus.Metric) bool {
|
||||
found, _ := c.used[i]
|
||||
return found || len(c.used) == 0
|
||||
}
|
||||
|
||||
func decodeFromFile(file io.Reader, fb *fbx.FreeboxConnection) bool {
|
||||
if err := json.NewDecoder(file).Decode(fb); err != nil {
|
||||
return false
|
||||
}
|
||||
if fb.AppToken == "" {
|
||||
return false
|
||||
}
|
||||
if err := fb.Login(); err != nil {
|
||||
log.Debug.Println("Decoding file:", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getContext(filename string) context {
|
||||
connection := fbx.NewFreeboxConnection()
|
||||
tryConnect := true
|
||||
if r, err := os.Open(filename); err == nil {
|
||||
defer r.Close()
|
||||
tryConnect = !decodeFromFile(r, connection)
|
||||
}
|
||||
if tryConnect {
|
||||
if err := connection.Login(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
w, err := os.Create(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer w.Close()
|
||||
if err := json.NewEncoder(w).Encode(connection); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return context{
|
||||
freebox: connection,
|
||||
used: make(map[prometheus.Metric]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) <= 1 {
|
||||
fmt.Fprintf(os.Stderr, "usage: %s [configfile]\n", os.Args[0])
|
||||
os.Exit(-1)
|
||||
}
|
||||
if true {
|
||||
log.Init(ioutil.Discard, os.Stdout, os.Stdout, os.Stderr)
|
||||
} else {
|
||||
log.Init(os.Stdout, os.Stdout, os.Stdout, os.Stderr)
|
||||
}
|
||||
context := getContext(os.Args[1])
|
||||
defer func() { context.freebox.Logout() }()
|
||||
|
||||
prometheus.MustRegister(&context)
|
||||
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
log.Error.Println(http.ListenAndServe(":9091", nil))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue