From 772311dc6b78bd9b9003a366658dc16d14f45cee Mon Sep 17 00:00:00 2001 From: Nils Decker Date: Mon, 29 Feb 2016 23:51:14 +0100 Subject: [PATCH] implement more metrics --- fritzbox_upnp/service.go | 281 +++++++++++++++++++++++++++++++++++++++ fritzbox_upnp/upnp.go | 124 ----------------- fritzbox_upnp/values.go | 67 ---------- main.go | 249 ++++++++++++++++++++++++++-------- 4 files changed, 478 insertions(+), 243 deletions(-) create mode 100644 fritzbox_upnp/service.go delete mode 100644 fritzbox_upnp/upnp.go delete mode 100644 fritzbox_upnp/values.go diff --git a/fritzbox_upnp/service.go b/fritzbox_upnp/service.go new file mode 100644 index 0000000..1e2c5cb --- /dev/null +++ b/fritzbox_upnp/service.go @@ -0,0 +1,281 @@ +package fritzbox_upnp + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "strings" +) + +// curl http://fritz.box:49000/igddesc.xml +// curl http://fritz.box:49000/any.xml +// curl http://fritz.box:49000/igdconnSCPD.xml +// curl http://fritz.box:49000/igdicfgSCPD.xml +// curl http://fritz.box:49000/igddslSCPD.xml +// curl http://fritz.box:49000/igd2ipv6fwcSCPD.xml + +const text_xml = `text/xml; charset="utf-8"` + +var ErrResultWithoutChardata = errors.New("result without chardata") + +type Root struct { + BaseUrl string + Device UpnpDevice `xml:"device"` + Services map[string]*UpnpService +} + +type UpnpDevice struct { + root *Root + + DeviceType string `xml:"deviceType"` + FriendlyName string `xml:"friendlyName"` + Manufacturer string `xml:"manufacturer"` + ManufacturerUrl string `xml:"manufacturerURL"` + ModelDescription string `xml:"modelDescription"` + ModelName string `xml:"modelName"` + ModelNumber string `xml:"modelNumber"` + ModelUrl string `xml:"modelURL"` + UDN string `xml:"UDN"` + + Services []*UpnpService `xml:"serviceList>service"` + Devices []*UpnpDevice `xml:"deviceList>device"` + + PresentationUrl string `xml:"presentationURL"` +} + +type UpnpService struct { + Device *UpnpDevice + + ServiceType string `xml:"serviceType"` + ServiceId string `xml:"serviceId"` + ControlUrl string `xml:"controlURL"` + EventSubUrl string `xml:"eventSubURL"` + SCPDUrl string `xml:"SCPDURL"` + + Actions map[string]*UpnpAction + StateVariables []*UpnpStateVariable +} + +type upnpScpd struct { + Actions []*UpnpAction `xml:"actionList>action"` + StateVariables []*UpnpStateVariable `xml:"serviceStateTable>stateVariable"` +} + +type UpnpAction struct { + service *UpnpService + + Name string `xml:"name"` + Arguments []*Argument `xml:"argumentList>argument"` + ArgumentMap map[string]*Argument +} + +func (a *UpnpAction) IsGetOnly() bool { + for _, a := range a.Arguments { + if a.Direction == "in" { + return false + } + } + return len(a.Arguments) > 0 +} + +type Argument struct { + Name string `xml:"name"` + Direction string `xml:"direction"` + RelatedStateVariable string `xml:"relatedStateVariable"` + StateVariable *UpnpStateVariable +} + +type UpnpStateVariable struct { + Name string `xml:"name"` + DataType string `xml:"dataType"` + DefaultValue string `xml:"defaultValue"` +} + +type Result map[string]interface{} + +func (r *Root) load() error { + igddesc, err := http.Get( + fmt.Sprintf("%s/igddesc.xml", r.BaseUrl), + ) + + if err != nil { + return err + } + + dec := xml.NewDecoder(igddesc.Body) + + err = dec.Decode(r) + if err != nil { + return err + } + + r.Services = make(map[string]*UpnpService) + return r.Device.fillServices(r) +} + +func (d *UpnpDevice) fillServices(r *Root) error { + d.root = r + + for _, s := range d.Services { + s.Device = d + + response, err := http.Get(r.BaseUrl + s.SCPDUrl) + if err != nil { + return err + } + + var scpd upnpScpd + + dec := xml.NewDecoder(response.Body) + err = dec.Decode(&scpd) + if err != nil { + return err + } + + s.Actions = make(map[string]*UpnpAction) + for _, a := range scpd.Actions { + s.Actions[a.Name] = a + } + s.StateVariables = scpd.StateVariables + + for _, a := range s.Actions { + a.service = s + a.ArgumentMap = make(map[string]*Argument) + + for _, arg := range a.Arguments { + for _, svar := range s.StateVariables { + if arg.RelatedStateVariable == svar.Name { + arg.StateVariable = svar + } + } + + a.ArgumentMap[arg.Name] = arg + } + } + + r.Services[s.ServiceType] = s + } + for _, d2 := range d.Devices { + err := d2.fillServices(r) + if err != nil { + return err + } + } + return nil +} + +func (a *UpnpAction) Call() (Result, error) { + bodystr := fmt.Sprintf(` + + + + + + + `, a.Name, a.service.ServiceType) + + url := a.service.Device.root.BaseUrl + a.service.ControlUrl + body := strings.NewReader(bodystr) + + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + + action := fmt.Sprintf("%s#%s", a.service.ServiceType, a.Name) + + req.Header["Content-Type"] = []string{text_xml} + req.Header["SoapAction"] = []string{action} + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + data := new(bytes.Buffer) + data.ReadFrom(resp.Body) + + // fmt.Printf(data.String()) + return a.parseSoapResponse(data) + +} + +func (a *UpnpAction) parseSoapResponse(r io.Reader) (Result, error) { + res := make(Result) + dec := xml.NewDecoder(r) + + for { + t, err := dec.Token() + if err == io.EOF { + return res, nil + } + + if err != nil { + return nil, err + } + + if se, ok := t.(xml.StartElement); ok { + arg, ok := a.ArgumentMap[se.Name.Local] + + if ok { + t2, err := dec.Token() + if err != nil { + return nil, err + } + + var val string + switch element := t2.(type) { + case xml.EndElement: + val = "" + case xml.CharData: + val = string(element) + default: + return nil, ErrResultWithoutChardata + } + + converted, err := convertResult(val, arg) + if err != nil { + return nil, err + } + res[arg.StateVariable.Name] = converted + } + } + + } +} + +func convertResult(val string, arg *Argument) (interface{}, error) { + switch arg.StateVariable.DataType { + case "string": + return val, nil + case "boolean": + return bool(val == "1"), nil + + case "ui1", "ui2", "ui4": + res, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return nil, err + } + return uint32(res), nil + default: + return nil, fmt.Errorf("unknown datatype: %s", arg.StateVariable.DataType) + + } +} + +func LoadServices(device string, port uint16) (*Root, error) { + var root = &Root{ + BaseUrl: fmt.Sprintf("http://%s:%d", device, port), + } + + err := root.load() + if err != nil { + return nil, err + } + + return root, nil +} diff --git a/fritzbox_upnp/upnp.go b/fritzbox_upnp/upnp.go deleted file mode 100644 index d015d00..0000000 --- a/fritzbox_upnp/upnp.go +++ /dev/null @@ -1,124 +0,0 @@ -package fritzbox_upnp - -import ( - "bytes" - "encoding/xml" - "errors" - "fmt" - "io" - "net/http" - "strconv" - "strings" -) - -const text_xml = `text/xml; charset="utf-8"` - -var ( - ErrResultNotFound = errors.New("result not found") - ErrResultWithoutChardata = errors.New("result without chardata") -) - -// curl "http://fritz.box:49000/igdupnp/control/WANIPConn1" -// -H "Content-Type: text/xml; charset="utf-8"" -// -H "SoapAction:urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress" -// -d " -// -// -// -// " - -type UpnpValue struct { - Path string - Service string - Method string - RetTag string - - ShortName string - Help string -} - -func (v *UpnpValue) query(device string, port uint16) (string, error) { - url := fmt.Sprintf("http://%s:%d%s", device, port, v.Path) - - bodystr := fmt.Sprintf(` - - - - - - - `, v.Method, v.Service) - - body := strings.NewReader(bodystr) - - req, err := http.NewRequest("POST", url, body) - if err != nil { - return "", err - } - - action := fmt.Sprintf("urn:schemas-upnp-org:service:%s#%s", v.Service, v.Method) - - req.Header["Content-Type"] = []string{text_xml} - req.Header["SoapAction"] = []string{action} - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - - data := new(bytes.Buffer) - data.ReadFrom(resp.Body) - - // fmt.Printf(data.String()) - - dec := xml.NewDecoder(data) - - for { - t, err := dec.Token() - if err == io.EOF { - return "", ErrResultNotFound - } - - if err != nil { - return "", err - } - - if se, ok := t.(xml.StartElement); ok { - if se.Name.Local == v.RetTag { - t2, err := dec.Token() - if err != nil { - return "", err - } - - data, ok := t2.(xml.CharData) - if !ok { - return "", ErrResultWithoutChardata - } - return string(data), nil - } - } - - } -} - -type UpnpValueString struct{ UpnpValue } - -func (v *UpnpValueString) Query(device string, port uint16) (string, error) { - return v.query(device, port) -} - -type UpnpValueUint struct{ UpnpValue } - -func (v *UpnpValueUint) Query(device string, port uint16) (uint64, error) { - strval, err := v.query(device, port) - if err != nil { - return 0, err - } - - val, err := strconv.ParseUint(strval, 10, 64) - if err != nil { - return 0, err - } - - return val, nil -} diff --git a/fritzbox_upnp/values.go b/fritzbox_upnp/values.go deleted file mode 100644 index 656cb89..0000000 --- a/fritzbox_upnp/values.go +++ /dev/null @@ -1,67 +0,0 @@ -package fritzbox_upnp - -// curl http://fritz.box:49000/igddesc.xml -// curl http://fritz.box:49000/any.xml -// curl http://fritz.box:49000/igdconnSCPD.xml -// curl http://fritz.box:49000/igdicfgSCPD.xml -// curl http://fritz.box:49000/igddslSCPD.xml -// curl http://fritz.box:49000/igd2ipv6fwcSCPD.xml - -var ( - WAN_IP = UpnpValueString{UpnpValue{ - Path: "/igdupnp/control/WANIPConn1", - Service: "WANIPConnection:1", - Method: "GetExternalIPAddress", - RetTag: "NewExternalIPAddress", - - ShortName: "wan_ip", - Help: "WAN IP Adress", - }} - - WAN_Packets_Received = UpnpValueUint{UpnpValue{ - Path: "/igdupnp/control/WANCommonIFC1", - Service: "WANCommonInterfaceConfig:1", - Method: "GetTotalPacketsReceived", - RetTag: "NewTotalPacketsReceived", - - ShortName: "packets_received", - Help: "packets received on gateway WAN interface", - }} - - WAN_Packets_Sent = UpnpValueUint{UpnpValue{ - Path: "/igdupnp/control/WANCommonIFC1", - Service: "WANCommonInterfaceConfig:1", - Method: "GetTotalPacketsSent", - RetTag: "NewTotalPacketsSent", - - ShortName: "packets_sent", - Help: "packets sent on gateway WAN interface", - }} - - WAN_Bytes_Received = UpnpValueUint{UpnpValue{ - Path: "/igdupnp/control/WANCommonIFC1", - Service: "WANCommonInterfaceConfig:1", - Method: "GetAddonInfos", - RetTag: "NewTotalBytesReceived", - - ShortName: "bytes_received", - Help: "bytes received on gateway WAN interface", - }} - - WAN_Bytes_Sent = UpnpValueUint{UpnpValue{ - Path: "/igdupnp/control/WANCommonIFC1", - Service: "WANCommonInterfaceConfig:1", - Method: "GetAddonInfos", - RetTag: "NewTotalBytesSent", - - ShortName: "bytes_sent", - Help: "bytes sent on gateway WAN interface", - }} -) - -var Values = []UpnpValueUint{ - WAN_Packets_Received, - WAN_Packets_Sent, - WAN_Bytes_Received, - WAN_Bytes_Sent, -} diff --git a/main.go b/main.go index 8339172..7f0dfb9 100644 --- a/main.go +++ b/main.go @@ -25,87 +25,232 @@ var ( }) ) -type UpnpMetric struct { - upnp.UpnpValueUint - *prometheus.Desc -} +type Metric struct { + Service string + Action string + Result string + OkValue string -func (m UpnpMetric) Describe(ch chan<- *prometheus.Desc) { - ch <- m.Desc + Desc *prometheus.Desc } -func (m UpnpMetric) Collect(gateway string, port uint16, ch chan<- prometheus.Metric) error { - val, err := m.Query(gateway, port) - if err != nil { - return err - } - - ch <- prometheus.MustNewConstMetric( - m.Desc, - prometheus.CounterValue, - float64(val), - gateway, - ) - return nil -} - -func NewUpnpMetric(v upnp.UpnpValueUint) UpnpMetric { - return UpnpMetric{ - v, - prometheus.NewDesc( - prometheus.BuildFQName("gateway", "wan", v.ShortName), - v.Help, +var metrics = []*Metric{ + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetTotalPacketsReceived", + Result: "TotalPacketsReceived", + Desc: prometheus.NewDesc( + "gateway_wan_packets_received", + "packets received on gateway WAN interface", []string{"gateway"}, nil, ), - } + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetTotalPacketsSent", + Result: "TotalPacketsSent", + Desc: prometheus.NewDesc( + "gateway_wan_packets_sent", + "packets sent on gateway WAN interface", + []string{"gateway"}, + nil, + ), + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetAddonInfos", + Result: "TotalBytesReceived", + Desc: prometheus.NewDesc( + "gateway_wan_bytes_received", + "bytes received on gateway WAN interface", + []string{"gateway"}, + nil, + ), + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetAddonInfos", + Result: "TotalBytesSent", + Desc: prometheus.NewDesc( + "gateway_wan_bytes_sent", + "bytes sent on gateway WAN interface", + []string{"gateway"}, + nil, + ), + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetCommonLinkProperties", + Result: "Layer1UpstreamMaxBitRate", + Desc: prometheus.NewDesc( + "gateway_wan_layer1_upstream_max_bitrate", + "Layer1 upstream max bitrate", + []string{"gateway"}, + nil, + ), + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetCommonLinkProperties", + Result: "Layer1DownstreamMaxBitRate", + Desc: prometheus.NewDesc( + "gateway_wan_layer1_downstream_max_bitrate", + "Layer1 downstream max bitrate", + []string{"gateway"}, + nil, + ), + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetCommonLinkProperties", + Result: "PhysicalLinkStatus", + OkValue: "Up", + Desc: prometheus.NewDesc( + "gateway_wan_layer1_link_status", + "Status of physical link (Up = 1)", + []string{"gateway"}, + nil, + ), + }, + { + Service: "urn:schemas-upnp-org:service:WANIPConnection:1", + Action: "GetStatusInfo", + Result: "ConnectionStatus", + OkValue: "Connected", + Desc: prometheus.NewDesc( + "gateway_wan_connection_status", + "WAN connection status (Connected = 1)", + []string{"gateway"}, + nil, + ), + }, + { + Service: "urn:schemas-upnp-org:service:WANIPConnection:1", + Action: "GetStatusInfo", + Result: "Uptime", + Desc: prometheus.NewDesc( + "gateway_wan_connection_uptime_seconds", + "WAN connection uptime", + []string{"gateway"}, + nil, + ), + }, } type FritzboxCollector struct { - gateway string - port uint16 - metrics []UpnpMetric + Root *upnp.Root + Gateway string } func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) { - for _, m := range fc.metrics { - m.Describe(ch) + for _, m := range metrics { + ch <- m.Desc } } func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { - for _, m := range fc.metrics { - err := m.Collect(fc.gateway, fc.port, ch) - if err != nil { - collect_errors.Inc() + var err error + var last_service string + var last_method string + var last_result upnp.Result + + for _, m := range metrics { + if m.Service != last_service || m.Action != last_method { + service, ok := fc.Root.Services[m.Service] + if !ok { + // TODO + fmt.Println("cannot find service", m.Service) + fmt.Println(fc.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 + } + } + + 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) { + case uint32: + 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() + continue + } + + ch <- prometheus.MustNewConstMetric( + m.Desc, + prometheus.CounterValue, // TODO + floatval, + fc.Gateway, + ) + } } func main() { flag.Parse() + root, err := upnp.LoadServices(*flag_gateway_address, uint16(*flag_gateway_port)) + if err != nil { + panic(err) + } + if *flag_test { - for _, v := range upnp.Values { - res, err := v.Query(*flag_gateway_address, uint16(*flag_gateway_port)) - if err != nil { - panic(err) + for _, s := range root.Services { + fmt.Printf("%s: %s\n", s.Device.FriendlyName, s.ServiceType) + for _, a := range s.Actions { + if !a.IsGetOnly() { + continue + } + + res, err := a.Call() + if err != nil { + panic(err) + } + + fmt.Printf(" %s\n", a.Name) + for _, arg := range a.Arguments { + fmt.Printf(" %s - %s: %v\n", arg.RelatedStateVariable, arg.StateVariable.DataType, res[arg.StateVariable.Name]) + } } - fmt.Printf("%s: %d\n", v.ShortName, res) } - return - } - metrics := make([]UpnpMetric, len(upnp.Values)) - for _, v := range upnp.Values { - metrics = append(metrics, NewUpnpMetric(v)) + return } - prometheus.MustRegister(&FritzboxCollector{ - *flag_gateway_address, - uint16(*flag_gateway_port), - metrics, - }) + prometheus.MustRegister(&FritzboxCollector{root, *flag_gateway_address}) // Since we are dealing with custom Collector implementations, it might // be a good idea to enable the collect checks in the registry. prometheus.EnableCollectChecks(true)