Browse Source

partial support for calling metrics with action argument

pull/1/head
sberk42 4 years ago
parent
commit
fa9c9084e1
  1. 33
      fritzbox_upnp/service.go
  2. 248
      main.go

33
fritzbox_upnp/service.go

@ -100,7 +100,7 @@ type Action struct {
// An Inüut Argument to pass to an action // An Inüut Argument to pass to an action
type ActionArgument struct { type ActionArgument struct {
Name string Name string
Value string Value interface{}
} }
// structs to unmarshal SOAP faults // structs to unmarshal SOAP faults
@ -267,12 +267,13 @@ const SoapActionXML = `<?xml version="1.0" encoding="utf-8"?>` +
const SoapActionParamXML = `<%s>%s</%s>` const SoapActionParamXML = `<%s>%s</%s>`
func (a *Action) createCallHttpRequest(actionArgs []ActionArgument) (*http.Request, error) { func (a *Action) createCallHttpRequest(actionArg *ActionArgument) (*http.Request, error) {
argsString := "" argsString := ""
for _, aa := range actionArgs{ if actionArg != nil {
var buf bytes.Buffer var buf bytes.Buffer
xml.EscapeText(&buf, []byte(aa.Value)) sValue := fmt.Sprintf("%v", actionArg.Value)
argsString += fmt.Sprintf(SoapActionParamXML, aa.Name, buf.String(), aa.Name) 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) 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; return req, nil;
} }
// Call an action. // store auth header for reuse
func (a *Action) Call() (Result, error) { var authHeader = ""
return a.CallWithArguments([]ActionArgument{});
} // Call an action with argument if given
// Currently only actions without input arguments are supported. func (a *Action) Call(actionArg *ActionArgument) (Result, error) {
func (a *Action) CallWithArguments(actionArgs []ActionArgument) (Result, error) { req, err := a.createCallHttpRequest(actionArg)
req, err := a.createCallHttpRequest(actionArgs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// reuse prior authHeader, to avoid unnecessary authentication
if authHeader != "" {
req.Header.Set("Authorization", authHeader)
}
// first try call without auth header // first try call without auth header
resp, err := http.DefaultClient.Do(req) 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 != "" { 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 // 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 { if err != nil {
return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error)) 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 { if err != nil {
return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error)) return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error))
} }

248
main.go

@ -25,7 +25,8 @@ import (
"io/ioutil" "io/ioutil"
"sort" "sort"
"bytes" "bytes"
"errors"
"github.com/namsral/flag" "github.com/namsral/flag"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
@ -37,6 +38,7 @@ const serviceLoadRetryTime = 1 * time.Minute
var ( var (
flag_test = flag.Bool("test", false, "print all available metrics to stdout") 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_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.") 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"` 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 { type Metric struct {
// initialized loading JSON // initialized loading JSON
Service string `json:"service"` Service string `json:"service"`
Action string `json:"action"` Action string `json:"action"`
ActionArgument *ActionArg `json:"actionArgument"`
Result string `json:"result"` Result string `json:"result"`
OkValue string `json:"okValue"` OkValue string `json:"okValue"`
PromDesc JSON_PromDesc `json:"promDesc"` PromDesc JSON_PromDesc `json:"promDesc"`
@ -87,6 +97,29 @@ type FritzboxCollector struct {
Root *upnp.Root 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. // LoadServices tries to load the service information. Retries until success.
func (fc *FritzboxCollector) LoadServices() { func (fc *FritzboxCollector) LoadServices() {
for { for {
@ -113,6 +146,72 @@ func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) {
} }
} }
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:
if tval {
floatval = 1
} else {
floatval = 0
}
case string:
if tval == m.OkValue {
floatval = 1
} else {
floatval = 0
}
default:
fmt.Println("unknown type", val)
collect_errors.Inc()
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))
}
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) { func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
fc.Lock() fc.Lock()
root := fc.Root root := fc.Root
@ -123,73 +222,57 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
return return
} }
var err error // create a map for caching results
var result_map = make(map[string]upnp.Result) var result_map = make(map[string]upnp.Result)
for _, m := range metrics { for _, m := range metrics {
m_key := m.Service+"|"+m.Action var actArg *upnp.ActionArgument
last_result := result_map[m_key]; if m.ActionArgument != nil {
if last_result == nil { aa := m.ActionArgument
service, ok := root.Services[m.Service] var value interface {}
if !ok { value = aa.Value
// TODO
fmt.Println("cannot find service", m.Service) if aa.ProviderAction != "" {
fmt.Println(root.Services) provRes, err := fc.GetActionResult(result_map, m.Service, aa.ProviderAction, nil)
continue
} if err != nil {
action, ok := service.Actions[m.Action] fmt.Printf("Error getting provider action %s result for %s.%s: %s\n", aa.ProviderAction, m.Service, m.Action, err.Error())
if !ok { collect_errors.Inc()
// TODO continue
fmt.Println("cannot find action", m.Action) }
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
}
} }
last_result, err = action.Call() if aa.IsIndex {
if err != nil { // TODO handle index iterations
fmt.Println(err) } else {
collect_errors.Inc() actArg = &upnp.ActionArgument{Name: aa.Name, Value: value }
continue
} }
}
result_map[m_key]=last_result
} result, err := fc.GetActionResult(result_map, m.Service, m.Action, actArg)
val, ok := last_result[m.Result] if err != nil {
if !ok { fmt.Println(err.Error())
fmt.Println("result not found", m.Result)
collect_errors.Inc() collect_errors.Inc()
continue continue
} }
var floatval float64 val, ok := result[m.Result]
switch tval := val.(type) { if !ok {
case uint64: fmt.Printf("%s.%s has no result %s", m.Service, m.Action, m.Result)
floatval = float64(tval)
case bool:
if tval {
floatval = 1
} else {
floatval = 0
}
case string:
if tval == m.OkValue {
floatval = 1
} else {
floatval = 0
}
default:
fmt.Println("unknown", val)
collect_errors.Inc() collect_errors.Inc()
continue continue
} }
ch <- prometheus.MustNewConstMetric( fc.ReportMetric(ch, m, val)
m.Desc,
m.MetricType,
floatval,
fc.Gateway,
)
} }
} }
@ -219,26 +302,20 @@ func test() {
sort.Strings(actionKeys) sort.Strings(actionKeys)
for _, l := range actionKeys { for _, l := range actionKeys {
a := s.Actions[l] a := s.Actions[l]
fmt.Printf(" %s - arguments: variable [direction] (soap name, soap type)\n", a.Name)
if !a.IsGetOnly() { for _, arg := range a.Arguments {
fmt.Printf(" %s - not calling - arguments: variable [direction] (soap name, soap type\n", a.Name) sv := arg.StateVariable
for _, arg := range a.Arguments { fmt.Printf(" %s [%s] (%s, %s)\n", arg.RelatedStateVariable, arg.Direction, arg.Name, sv.DataType)
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) if !a.IsGetOnly() {
res, err := a.Call() fmt.Printf(" %s - not calling, since arguments required or no output\n", a.Name)
if err != nil {
fmt.Printf(" FAILED:%s\n", err.Error())
continue continue
} }
// only create JSON for Get
// TODO also create JSON templates for input actionParams
for _, arg := range a.Arguments { for _, arg := range a.Arguments {
fmt.Printf(" %s: %v\n", arg.RelatedStateVariable, res[arg.StateVariable.Name])
// create new json entry // create new json entry
if(newEntry) { if(newEntry) {
json.WriteString(",\n") json.WriteString(",\n")
@ -254,6 +331,18 @@ func test() {
json.WriteString(arg.RelatedStateVariable) json.WriteString(arg.RelatedStateVariable)
json.WriteString("\"\n\t}") 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, Username: *flag_gateway_username,
Password: *flag_gateway_password, 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() go collector.LoadServices()
prometheus.MustRegister(collector) prometheus.MustRegister(collector)
@ -328,5 +435,6 @@ func main() {
http.Handle("/metrics", promhttp.Handler()) http.Handle("/metrics", promhttp.Handler())
fmt.Printf("metrics available at http://%s/metrics\n", *flag_addr) fmt.Printf("metrics available at http://%s/metrics\n", *flag_addr)
log.Fatal(http.ListenAndServe(*flag_addr, nil)) log.Fatal(http.ListenAndServe(*flag_addr, nil))
} }

Loading…
Cancel
Save