From fa9c9084e1046e67743bada5e33fa571e8ae4c6f Mon Sep 17 00:00:00 2001 From: sberk42 Date: Thu, 27 Aug 2020 19:49:25 +0200 Subject: [PATCH] partial support for calling metrics with action argument --- fritzbox_upnp/service.go | 33 +++--- main.go | 248 ++++++++++++++++++++++++++++----------- 2 files changed, 197 insertions(+), 84 deletions(-) diff --git a/fritzbox_upnp/service.go b/fritzbox_upnp/service.go index f447716..1cca2e9 100644 --- a/fritzbox_upnp/service.go +++ b/fritzbox_upnp/service.go @@ -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 = `` + const SoapActionParamXML = `<%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)) } diff --git a/main.go b/main.go index 83d7f5c..6634fe3 100644 --- a/main.go +++ b/main.go @@ -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,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) { fc.Lock() root := fc.Root @@ -123,73 +222,57 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { return } - var err error + // create a map for caching results 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 + 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 + } } - last_result, err = action.Call() - if err != nil { - fmt.Println(err) - collect_errors.Inc() - continue + if aa.IsIndex { + // TODO handle index iterations + } else { + actArg = &upnp.ActionArgument{Name: aa.Name, Value: value } } - - result_map[m_key]=last_result - } + } + + result, err := fc.GetActionResult(result_map, m.Service, m.Action, actArg) - val, ok := last_result[m.Result] - if !ok { - fmt.Println("result not found", m.Result) + if err != nil { + fmt.Println(err.Error()) collect_errors.Inc() - continue + continue } - - 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", val) + + 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 - } - ch <- prometheus.MustNewConstMetric( - m.Desc, - m.MetricType, - floatval, - fc.Gateway, - ) + 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 - 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) } - - fmt.Printf(" %s\n", a.Name) - res, err := a.Call() - if err != nil { - fmt.Printf(" FAILED:%s\n", err.Error()) + + 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 { - fmt.Printf(" %s: %v\n", arg.RelatedStateVariable, res[arg.StateVariable.Name]) - // 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)) }