mirror of
https://github.com/sberk42/fritzbox_exporter.git
synced 2026-08-26 23:57:16 +00:00
partial support for calling metrics with action argument
This commit is contained in:
+19
-14
@@ -100,7 +100,7 @@ type Action struct {
|
||||
// An Inüut Argument to pass to an action
|
||||
type ActionArgument struct {
|
||||
Name string
|
||||
Value string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// structs to unmarshal SOAP faults
|
||||
@@ -267,12 +267,13 @@ const SoapActionXML = `<?xml version="1.0" encoding="utf-8"?>` +
|
||||
|
||||
const SoapActionParamXML = `<%s>%s</%s>`
|
||||
|
||||
func (a *Action) createCallHttpRequest(actionArgs []ActionArgument) (*http.Request, error) {
|
||||
func (a *Action) createCallHttpRequest(actionArg *ActionArgument) (*http.Request, error) {
|
||||
argsString := ""
|
||||
for _, aa := range actionArgs{
|
||||
if actionArg != nil {
|
||||
var buf bytes.Buffer
|
||||
xml.EscapeText(&buf, []byte(aa.Value))
|
||||
argsString += fmt.Sprintf(SoapActionParamXML, aa.Name, buf.String(), aa.Name)
|
||||
sValue := fmt.Sprintf("%v", actionArg.Value)
|
||||
xml.EscapeText(&buf, []byte(sValue))
|
||||
argsString += fmt.Sprintf(SoapActionParamXML, actionArg.Name, buf.String(), actionArg.Name)
|
||||
}
|
||||
bodystr := fmt.Sprintf(SoapActionXML, a.Name, a.service.ServiceType, argsString, a.Name, a.service.ServiceType)
|
||||
|
||||
@@ -292,17 +293,21 @@ func (a *Action) createCallHttpRequest(actionArgs []ActionArgument) (*http.Reque
|
||||
return req, nil;
|
||||
}
|
||||
|
||||
// Call an action.
|
||||
func (a *Action) Call() (Result, error) {
|
||||
return a.CallWithArguments([]ActionArgument{});
|
||||
}
|
||||
// Currently only actions without input arguments are supported.
|
||||
func (a *Action) CallWithArguments(actionArgs []ActionArgument) (Result, error) {
|
||||
req, err := a.createCallHttpRequest(actionArgs)
|
||||
// store auth header for reuse
|
||||
var authHeader = ""
|
||||
|
||||
// Call an action with argument if given
|
||||
func (a *Action) Call(actionArg *ActionArgument) (Result, error) {
|
||||
req, err := a.createCallHttpRequest(actionArg)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// reuse prior authHeader, to avoid unnecessary authentication
|
||||
if authHeader != "" {
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
}
|
||||
|
||||
// first try call without auth header
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
@@ -317,12 +322,12 @@ func (a *Action) CallWithArguments(actionArgs []ActionArgument) (Result, error)
|
||||
|
||||
if wwwAuth != "" && a.service.Device.root.Username != "" && a.service.Device.root.Password != "" {
|
||||
// call failed, but we have a password so calculate header and try again
|
||||
authHeader, err := a.getDigestAuthHeader(wwwAuth, a.service.Device.root.Username, a.service.Device.root.Password)
|
||||
authHeader, err = a.getDigestAuthHeader(wwwAuth, a.service.Device.root.Username, a.service.Device.root.Password)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error))
|
||||
}
|
||||
|
||||
req, err = a.createCallHttpRequest(actionArgs)
|
||||
req, err = a.createCallHttpRequest(actionArg)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error))
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ import (
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"bytes"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/namsral/flag"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
@@ -37,6 +38,7 @@ const serviceLoadRetryTime = 1 * time.Minute
|
||||
|
||||
var (
|
||||
flag_test = flag.Bool("test", false, "print all available metrics to stdout")
|
||||
flag_collect = flag.Bool("collect", false, "print configured metrics to stdout and exit")
|
||||
flag_jsonout = flag.String("json-out", "", "store metrics also to JSON file when running test")
|
||||
|
||||
flag_addr = flag.String("listen-address", "127.0.0.1:9042", "The address to listen on for HTTP requests.")
|
||||
@@ -61,10 +63,18 @@ type JSON_PromDesc struct {
|
||||
VarLabels []string `json:"varLabels"`
|
||||
}
|
||||
|
||||
type ActionArg struct {
|
||||
Name string `json:"Name"`
|
||||
IsIndex bool `json:"IsIndex"`
|
||||
ProviderAction string `json:"ProviderAction"`
|
||||
Value string `json:"Value"`
|
||||
}
|
||||
|
||||
type Metric struct {
|
||||
// initialized loading JSON
|
||||
Service string `json:"service"`
|
||||
Action string `json:"action"`
|
||||
ActionArgument *ActionArg `json:"actionArgument"`
|
||||
Result string `json:"result"`
|
||||
OkValue string `json:"okValue"`
|
||||
PromDesc JSON_PromDesc `json:"promDesc"`
|
||||
@@ -87,6 +97,29 @@ type FritzboxCollector struct {
|
||||
Root *upnp.Root
|
||||
}
|
||||
|
||||
// simple ResponseWriter to collect output
|
||||
type TestResponseWriter struct {
|
||||
header http.Header
|
||||
statusCode int
|
||||
body bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *TestResponseWriter) Header() http.Header {
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *TestResponseWriter) Write(b []byte) (int, error) {
|
||||
return w.body.Write(b)
|
||||
}
|
||||
|
||||
func (w *TestResponseWriter) WriteHeader(statusCode int) {
|
||||
w.statusCode = statusCode
|
||||
}
|
||||
|
||||
func (w *TestResponseWriter) String() string {
|
||||
return w.body.String()
|
||||
}
|
||||
|
||||
// LoadServices tries to load the service information. Retries until success.
|
||||
func (fc *FritzboxCollector) LoadServices() {
|
||||
for {
|
||||
@@ -113,56 +146,9 @@ func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
}
|
||||
}
|
||||
|
||||
func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
fc.Lock()
|
||||
root := fc.Root
|
||||
fc.Unlock()
|
||||
|
||||
if root == nil {
|
||||
// Services not loaded yet
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
var result_map = make(map[string]upnp.Result)
|
||||
|
||||
for _, m := range metrics {
|
||||
m_key := m.Service+"|"+m.Action
|
||||
last_result := result_map[m_key];
|
||||
if last_result == nil {
|
||||
service, ok := root.Services[m.Service]
|
||||
if !ok {
|
||||
// TODO
|
||||
fmt.Println("cannot find service", m.Service)
|
||||
fmt.Println(root.Services)
|
||||
continue
|
||||
}
|
||||
action, ok := service.Actions[m.Action]
|
||||
if !ok {
|
||||
// TODO
|
||||
fmt.Println("cannot find action", m.Action)
|
||||
continue
|
||||
}
|
||||
|
||||
last_result, err = action.Call()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
collect_errors.Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
result_map[m_key]=last_result
|
||||
}
|
||||
|
||||
val, ok := last_result[m.Result]
|
||||
if !ok {
|
||||
fmt.Println("result not found", m.Result)
|
||||
collect_errors.Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
var floatval float64
|
||||
switch tval := val.(type) {
|
||||
func (fc *FritzboxCollector) ReportMetric(ch chan<- prometheus.Metric, m *Metric, val interface{}) {
|
||||
var floatval float64
|
||||
switch tval := val.(type) {
|
||||
case uint64:
|
||||
floatval = float64(tval)
|
||||
case bool:
|
||||
@@ -178,18 +164,115 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
floatval = 0
|
||||
}
|
||||
default:
|
||||
fmt.Println("unknown", val)
|
||||
fmt.Println("unknown type", val)
|
||||
collect_errors.Inc()
|
||||
continue
|
||||
return
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
m.Desc,
|
||||
m.MetricType,
|
||||
floatval,
|
||||
fc.Gateway,
|
||||
)
|
||||
}
|
||||
|
||||
func (fc *FritzboxCollector) GetActionResult(result_map map[string]upnp.Result, serviceType string, actionName string, actionArg *upnp.ActionArgument) (upnp.Result, error) {
|
||||
|
||||
m_key := serviceType+"|"+actionName
|
||||
|
||||
// for calls with argument also add arguement name and value to key
|
||||
if actionArg != nil {
|
||||
|
||||
m_key += "|"+actionArg.Name+"|"+fmt.Sprintf("%v", actionArg.Value)
|
||||
}
|
||||
|
||||
last_result := result_map[m_key];
|
||||
if last_result == nil {
|
||||
service, ok := fc.Root.Services[serviceType]
|
||||
if !ok {
|
||||
return nil, errors.New(fmt.Sprintf("service %s not found", serviceType))
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
m.Desc,
|
||||
m.MetricType,
|
||||
floatval,
|
||||
fc.Gateway,
|
||||
)
|
||||
action, ok := service.Actions[actionName]
|
||||
if !ok {
|
||||
return nil, errors.New(fmt.Sprintf("action %s not found in service %s", actionName, serviceType))
|
||||
}
|
||||
|
||||
var err error
|
||||
last_result, err = action.Call(actionArg);
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result_map[m_key]=last_result
|
||||
}
|
||||
|
||||
return last_result, nil
|
||||
}
|
||||
|
||||
func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
fc.Lock()
|
||||
root := fc.Root
|
||||
fc.Unlock()
|
||||
|
||||
if root == nil {
|
||||
// Services not loaded yet
|
||||
return
|
||||
}
|
||||
|
||||
// create a map for caching results
|
||||
var result_map = make(map[string]upnp.Result)
|
||||
|
||||
for _, m := range metrics {
|
||||
var actArg *upnp.ActionArgument
|
||||
if m.ActionArgument != nil {
|
||||
aa := m.ActionArgument
|
||||
var value interface {}
|
||||
value = aa.Value
|
||||
|
||||
if aa.ProviderAction != "" {
|
||||
provRes, err := fc.GetActionResult(result_map, m.Service, aa.ProviderAction, nil)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting provider action %s result for %s.%s: %s\n", aa.ProviderAction, m.Service, m.Action, err.Error())
|
||||
collect_errors.Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
var ok bool
|
||||
value, ok = provRes[aa.Value] // Value contains the result name for provider actions
|
||||
if !ok {
|
||||
fmt.Printf("provider action %s for %s.%s has no result %s", m.Service, m.Action, aa.Value)
|
||||
collect_errors.Inc()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if aa.IsIndex {
|
||||
// TODO handle index iterations
|
||||
} else {
|
||||
actArg = &upnp.ActionArgument{Name: aa.Name, Value: value }
|
||||
}
|
||||
}
|
||||
|
||||
result, err := fc.GetActionResult(result_map, m.Service, m.Action, actArg)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
collect_errors.Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
val, ok := result[m.Result]
|
||||
if !ok {
|
||||
fmt.Printf("%s.%s has no result %s", m.Service, m.Action, m.Result)
|
||||
collect_errors.Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
fc.ReportMetric(ch, m, val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,26 +302,20 @@ func test() {
|
||||
sort.Strings(actionKeys)
|
||||
for _, l := range actionKeys {
|
||||
a := s.Actions[l]
|
||||
|
||||
if !a.IsGetOnly() {
|
||||
fmt.Printf(" %s - not calling - arguments: variable [direction] (soap name, soap type\n", a.Name)
|
||||
for _, arg := range a.Arguments {
|
||||
sv := arg.StateVariable
|
||||
fmt.Printf(" %s [%s] (%s, %s)\n", arg.RelatedStateVariable, arg.Direction, arg.Name, sv.DataType)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" %s\n", a.Name)
|
||||
res, err := a.Call()
|
||||
if err != nil {
|
||||
fmt.Printf(" FAILED:%s\n", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" %s - arguments: variable [direction] (soap name, soap type)\n", a.Name)
|
||||
for _, arg := range a.Arguments {
|
||||
fmt.Printf(" %s: %v\n", arg.RelatedStateVariable, res[arg.StateVariable.Name])
|
||||
sv := arg.StateVariable
|
||||
fmt.Printf(" %s [%s] (%s, %s)\n", arg.RelatedStateVariable, arg.Direction, arg.Name, sv.DataType)
|
||||
}
|
||||
|
||||
if !a.IsGetOnly() {
|
||||
fmt.Printf(" %s - not calling, since arguments required or no output\n", a.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// only create JSON for Get
|
||||
// TODO also create JSON templates for input actionParams
|
||||
for _, arg := range a.Arguments {
|
||||
// create new json entry
|
||||
if(newEntry) {
|
||||
json.WriteString(",\n")
|
||||
@@ -254,6 +331,18 @@ func test() {
|
||||
json.WriteString(arg.RelatedStateVariable)
|
||||
json.WriteString("\"\n\t}")
|
||||
}
|
||||
|
||||
fmt.Printf(" %s - calling - results: variable: value\n", a.Name)
|
||||
res, err := a.Call(nil)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf(" FAILED:%s\n", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
for _, arg := range a.Arguments {
|
||||
fmt.Printf(" %s: %v\n", arg.RelatedStateVariable, res[arg.StateVariable.Name])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +409,25 @@ func main() {
|
||||
Username: *flag_gateway_username,
|
||||
Password: *flag_gateway_password,
|
||||
}
|
||||
|
||||
if *flag_collect {
|
||||
collector.LoadServices()
|
||||
|
||||
prometheus.MustRegister(collector)
|
||||
prometheus.MustRegister(collect_errors)
|
||||
|
||||
fmt.Println("collecting metrics via http")
|
||||
|
||||
// simulate HTTP request without starting actual http server
|
||||
writer := TestResponseWriter{header: http.Header{}}
|
||||
request := http.Request{}
|
||||
promhttp.Handler().ServeHTTP(&writer, &request)
|
||||
|
||||
fmt.Println(writer.String())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
go collector.LoadServices()
|
||||
|
||||
prometheus.MustRegister(collector)
|
||||
@@ -328,5 +435,6 @@ func main() {
|
||||
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
fmt.Printf("metrics available at http://%s/metrics\n", *flag_addr)
|
||||
|
||||
log.Fatal(http.ListenAndServe(*flag_addr, nil))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user