Browse Source

implement more metrics

pull/1/head
Nils Decker 9 years ago
parent
commit
772311dc6b
  1. 281
      fritzbox_upnp/service.go
  2. 124
      fritzbox_upnp/upnp.go
  3. 67
      fritzbox_upnp/values.go
  4. 249
      main.go

281
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(`
<?xml version='1.0' encoding='utf-8'?>
<s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'>
<s:Body>
<u:%s xmlns:u='%s' />
</s:Body>
</s:Envelope>
`, 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
}

124
fritzbox_upnp/upnp.go

@ -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 "<?xml version='1.0' encoding='utf-8'?>
// <s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'>
// <s:Body>
// <u:GetExternalIPAddress xmlns:u='urn:schemas-upnp-org:service:WANIPConnection:1' />
// </s:Body> </s:Envelope>"
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(`
<?xml version='1.0' encoding='utf-8'?>
<s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'>
<s:Body>
<u:%s xmlns:u='urn:schemas-upnp-org:service:%s' />
</s:Body>
</s:Envelope>
`, 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
}

67
fritzbox_upnp/values.go

@ -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,
}

249
main.go

@ -25,87 +25,232 @@ var (
}) })
) )
type UpnpMetric struct { type Metric struct {
upnp.UpnpValueUint Service string
*prometheus.Desc Action string
} Result string
OkValue string
func (m UpnpMetric) Describe(ch chan<- *prometheus.Desc) { Desc *prometheus.Desc
ch <- m.Desc
} }
func (m UpnpMetric) Collect(gateway string, port uint16, ch chan<- prometheus.Metric) error { var metrics = []*Metric{
val, err := m.Query(gateway, port) {
if err != nil { Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1",
return err Action: "GetTotalPacketsReceived",
} Result: "TotalPacketsReceived",
Desc: prometheus.NewDesc(
ch <- prometheus.MustNewConstMetric( "gateway_wan_packets_received",
m.Desc, "packets received on gateway WAN interface",
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,
[]string{"gateway"}, []string{"gateway"},
nil, 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 { type FritzboxCollector struct {
gateway string Root *upnp.Root
port uint16 Gateway string
metrics []UpnpMetric
} }
func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) { func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) {
for _, m := range fc.metrics { for _, m := range metrics {
m.Describe(ch) ch <- m.Desc
} }
} }
func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
for _, m := range fc.metrics { var err error
err := m.Collect(fc.gateway, fc.port, ch) var last_service string
if err != nil { var last_method string
collect_errors.Inc() 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() { func main() {
flag.Parse() flag.Parse()
root, err := upnp.LoadServices(*flag_gateway_address, uint16(*flag_gateway_port))
if err != nil {
panic(err)
}
if *flag_test { if *flag_test {
for _, v := range upnp.Values { for _, s := range root.Services {
res, err := v.Query(*flag_gateway_address, uint16(*flag_gateway_port)) fmt.Printf("%s: %s\n", s.Device.FriendlyName, s.ServiceType)
if err != nil { for _, a := range s.Actions {
panic(err) 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)) return
for _, v := range upnp.Values {
metrics = append(metrics, NewUpnpMetric(v))
} }
prometheus.MustRegister(&FritzboxCollector{ prometheus.MustRegister(&FritzboxCollector{root, *flag_gateway_address})
*flag_gateway_address,
uint16(*flag_gateway_port),
metrics,
})
// Since we are dealing with custom Collector implementations, it might // Since we are dealing with custom Collector implementations, it might
// be a good idea to enable the collect checks in the registry. // be a good idea to enable the collect checks in the registry.
prometheus.EnableCollectChecks(true) prometheus.EnableCollectChecks(true)

Loading…
Cancel
Save