First commit

master
marcel 8 months ago
parent fe8119d660
commit b2de356334
  1. 106
      README.md
  2. 2
      config.yml
  3. 304
      fritz-api.sh
  4. 104
      fritzbox_exporter.py

@ -1,3 +1,103 @@
# fritzbox_exporter
Exports statistics from FritzBox to Prometheus
# fritzbox_exporter
Collects data from FRITZ!Box via its SOAP api and exports it to Prometheus via http.
The program is split up into two parts:
## Bash script
Collects various information from your FRITZ!Box via its SOAP api. It outputs the data to stdout in JSON format.
It can also be used from the command line. Script can output:
- link uptime
- connection status
- maximum upstream sync speed on current connection
- maximum downstream sync speed on current connection
- Current downstream bandwidth usage
- Current upstream bandwidth usage
- Total download usage on current connection
- Total upload usage on current connection
This bash script is taken from https://github.com/mrwhale/fritzbox-api
### Requirement
The Universal Plug & Play (UPnP) service must be enabled.
You can do that in the settings: Home Network » Network » Network Settings
Enable "Transmit status information over UPnP"
### Usage
```text
usage: fritz-api.sh [-f <function>] [-h hostname] [-b rate] [-j] [-d]
-f: function to be executed [Default: bandwidthdown]
-h: hostname or IP of the FRITZ!Box [Default: fritz.box]
-b: rate to display. b, k, m. all in bytes
-j: JSON output
Does not accept any functions.
Will display all output in JSON format.
Useful for running in cron and ingesting into another program
-d: enable debug output
functions:
linkuptime connection time in seconds
connection connection status
downstream maximum downstream on current connection (Downstream Sync)
upstream maximum upstream on current connection (Upstream Sync)
bandwidthdown current bandwidth down
bandwidthup current bandwidth up
totalbwdown total downloads
totalbwup total uploads
Example: fritz-api.sh -f downstream -h 192.168.100.1 -b m
```
### Dependancies
- curl
- bc
## Python program
This is the actual exporter. It takes the output from the bash script, processes it and exposes it via http (default port 9000) to Prometheus.
### Usage
Configure prometheus.yml:
```text
- job_name: 'FritzBox'
static_configs:
- targets: ['localhost:9001']
```
Start program as background process:
```text
python3 ./fritzbox_exporter.py &
```
Use Graphana to display the data.
### Configuration
It can be configured via the file config.yml.
```text
# Set port (default: 9000)
port: 9001
# Set scrape frequency (default: 15 seconds)
scrape_frequency: 60
```
### Dependancies
- Python 3
- subprocess
- json
- pyyaml
- prometheus_client

@ -0,0 +1,2 @@
port: 9001
scrape_frequency: 60

@ -0,0 +1,304 @@
#!/usr/bin/bash
RC_OK=0
RC_WARN=1
RC_CRIT=2
RC_UNKNOWN=3
HOSTNAME="fritz.box"
CHECK="bandwidthdown"
MY_SCRIPT_NAME=$(basename "$0")
# Duration we wait for curl response.
MY_CURL_TIMEOUT="5"
usage(){
echo "usage: $MY_SCRIPT_NAME [-f <function>] [-h hostname] [-b rate] [-j] [-d]"
echo " -f: function to be executed [Default: ${CHECK}]"
echo " -h: hostname or IP of the FRITZ!Box [Default: ${HOSTNAME}]"
echo " -b: rate to display. b, k, m. all in bytes"
echo " -j: JSON output"
echo " Does not accept any functions."
echo " Will display all output in JSON format."
echo " Useful for running in cron and ingesting into another program"
echo " -d: enable debug output"
echo
echo "functions:"
echo " linkuptime connection time in seconds"
echo " connection connection status"
echo " downstream maximum downstream on current connection (Downstream Sync)"
echo " upstream maximum upstream on current connection (Upstream Sync)"
echo " bandwidthdown current bandwidth down"
echo " bandwidthup current bandwidth up"
echo " totalbwdown total downloads"
echo " totalbwup total uploads"
echo
echo "Example: $MY_SCRIPT_NAME -f downstream -h 192.168.100.1 -b m"
exit ${RC_UNKNOWN}
}
require_number()
{
VAR=$1
MSG=$2
if [[ ! "${VAR}" =~ ^[0-9]+$ ]] ; then
echo "ERROR - ${MSG} (${VAR})"
exit ${RC_UNKNOWN}
fi
}
find_xml_value()
{
XML=$1
VAL=$2
echo "${XML}" | grep "${VAL}" | sed "s/<${VAL}>\([^<]*\)<\/${VAL}>/\1/"
}
check_greater()
{
VAL=$1
WARN=$2
CRIT=$3
MSG=$4
if [ "${VAL}" -gt "${WARN}" ] || [ "${WARN}" -eq 0 ]; then
echo "OK - ${MSG}"
exit ${RC_OK}
elif [ "${VAL}" -gt "${CRIT}" ] || [ "${CRIT}" -eq 0 ]; then
echo "WARNING - ${MSG}"
exit ${RC_WARN}
else
echo "CRITICAL - ${MSG}"
exit ${RC_CRIT}
fi
}
print_json(){
VERB1=GetStatusInfo
URL1=WANIPConn1
NS1=WANIPConnection
VERB2=GetCommonLinkProperties
URL2=WANCommonIFC1
NS2=WANCommonInterfaceConfig
VERB3=GetAddonInfos
URL3=WANCommonIFC1
NS3=WANCommonInterfaceConfig
STATUS1=$(curl --max-time "${MY_CURL_TIMEOUT}" "http://${HOSTNAME}:${PORT}/igdupnp/control/${URL1}" \
-H "Content-Type: text/xml; charset=\"utf-8\"" \
-H "SoapAction:urn:schemas-upnp-org:service:${NS1}:1#${VERB1}" \
-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:${VERB1} xmlns:u=\"urn:schemas-upnp-org:service:${NS1}:1\" /> </s:Body> </s:Envelope>" \
-s)
if [ "$?" -ne "0" ]; then
printf '{"Connection":"ERROR - Could not retrieve status from FRITZ!Box"}'
exit ${RC_CRIT}
fi
STATUS2=$(curl --max-time "${MY_CURL_TIMEOUT}" "http://${HOSTNAME}:${PORT}/igdupnp/control/${URL2}" \
-H "Content-Type: text/xml; charset=\"utf-8\"" \
-H "SoapAction:urn:schemas-upnp-org:service:${NS2}:1#${VERB2}" \
-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:${VERB2} xmlns:u=\"urn:schemas-upnp-org:service:${NS2}:1\" /> </s:Body> </s:Envelope>" \
-s)
if [ "$?" -ne "0" ]; then
printf '{"Connection":"ERROR - Could not retrieve status from FRITZ!Box"}'
exit ${RC_CRIT}
fi
STATUS3=$(curl --max-time "${MY_CURL_TIMEOUT}" "http://${HOSTNAME}:${PORT}/igdupnp/control/${URL3}" \
-H "Content-Type: text/xml; charset=\"utf-8\"" \
-H "SoapAction:urn:schemas-upnp-org:service:${NS3}:1#${VERB3}" \
-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:${VERB3} xmlns:u=\"urn:schemas-upnp-org:service:${NS3}:1\" /> </s:Body> </s:Envelope>" \
-s)
if [ "$?" -ne "0" ]; then
printf '{"Connection":"ERROR - Could not retrieve status from FRITZ!Box"}'
exit ${RC_CRIT}
fi
CONNECTIONSTATUS=$(find_xml_value "${STATUS1}" NewConnectionStatus)
UPTIME=$(find_xml_value "${STATUS1}" NewUptime)
DOWNSTREAM=$(find_xml_value "${STATUS2}" NewLayer1DownstreamMaxBitRate)
UPSTREAM=$(find_xml_value "${STATUS2}" NewLayer1UpstreamMaxBitRate)
BANDWIDTHDOWNBYTES=$(find_xml_value "${STATUS3}" NewByteReceiveRate)
BANDWIDTHUPBYTES=$(find_xml_value "${STATUS3}" NewByteSendRate)
TOTALBWDOWNBYTES=$(find_xml_value "${STATUS3}" NewTotalBytesReceived)
TOTALBWUPBYTES=$(find_xml_value "${STATUS3}" NewTotalBytesSent)
if [ "${DEBUG}" -eq 1 ]; then
echo "DEBUG - Status:"
echo "$CONNECTIONSTATUS"
echo "$UPTIME"
echo "$DOWNSTREAM"
echo "$UPSTREAM"
echo "$BANDWIDTHDOWNBYTES"
echo "$BANDWIDTHUPBYTES"
echo "$TOTALBWDOWNBYTES"
echo "$TOTALBWUPBYTES"
fi
printf '{"Connection":"%s","Uptime":%d,"UpstreamSync":%d,"DownstreamSync":%d,"UploadBW":%d,"DownloadBW":%d,"TotalUploads":%d,"TotalDownloads":%d}\n' "$CONNECTIONSTATUS" "$UPTIME" "$UPSTREAM" "$DOWNSTREAM" "$BANDWIDTHUPBYTES" "$BANDWIDTHDOWNBYTES" "$TOTALBWUPBYTES" "$TOTALBWDOWNBYTES"
exit #exit so we dont get unknown service check error
}
# Check Commands
command -v curl >/dev/null 2>&1 || { echo >&2 "ERROR: 'curl' is needed. Please install 'curl'. More details can be found at https://curl.haxx.se/"; exit 1; }
command -v bc >/dev/null 2>&1 || { echo >&2 "ERROR: 'bc' is needed. Please install 'bc'."; exit 1; }
PORT=49000
DEBUG=0
WARN=0
CRIT=0
RATE=1
PRE=
while getopts h:jf:db: OPTNAME; do
case "${OPTNAME}" in
h)
HOSTNAME="${OPTARG}"
;;
j)
CHECK=""
print_json
;;
f)
CHECK="${OPTARG}"
;;
d)
DEBUG=1
;;
b)
case "${OPTARG}" in
b)
RATE=1
PRE=
;;
k)
RATE=1000
PRE=kilo
;;
m)
RATE=1000000
PRE=mega
;;
*)
echo "Wrong prefix"
;;
esac
;;
*)
echo "$OPTNAME"
usage
;;
esac
done
case ${CHECK} in
linkuptime|connection)
VERB=GetStatusInfo
URL=WANIPConn1
NS=WANIPConnection
;;
downstream|upstream)
VERB=GetCommonLinkProperties
URL=WANCommonIFC1
NS=WANCommonInterfaceConfig
;;
bandwidthup|bandwidthdown|totalbwup|totalbwdown)
VERB=GetAddonInfos
URL=WANCommonIFC1
NS=WANCommonInterfaceConfig
;;
*)
echo "ERROR - Unknown service check ${CHECK}"
exit ${RC_UNKNOWN}
;;
esac
STATUS=$(curl --max-time "${MY_CURL_TIMEOUT}" "http://${HOSTNAME}:${PORT}/igdupnp/control/${URL}" \
-H "Content-Type: text/xml; charset=\"utf-8\"" \
-H "SoapAction:urn:schemas-upnp-org:service:${NS}:1#${VERB}" \
-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:${VERB} xmlns:u=\"urn:schemas-upnp-org:service:${NS}:1\" /> </s:Body> </s:Envelope>" \
-s)
if [ "$?" -ne "0" ]; then
echo "ERROR - Could not retrieve status from FRITZ!Box"
exit ${RC_CRIT}
fi
if [ ${DEBUG} -eq 1 ]; then
echo "DEBUG - Status:"
echo "${STATUS}"
fi
case ${CHECK} in
linkuptime)
UPTIME=$(find_xml_value "${STATUS}" NewUptime)
require_number "${UPTIME}" "Could not parse uptime"
HOURS=$((${UPTIME}/3600))
MINUTES=$(((${UPTIME}-(${HOURS}*3600))/60))
SECONDS=$((${UPTIME}-(${HOURS}*3600)-(${MINUTES}*60)))
RESULT="Link uptime ${UPTIME} seconds [${HOURS}h ${MINUTES}m ${SECONDS}s]"
echo "${RESULT}"
;;
upstream)
UPSTREAMBITS=$(find_xml_value "${STATUS}" NewLayer1UpstreamMaxBitRate)
require_number "${UPSTREAMBITS}" "Could not parse upstream"
UPSTREAM=$(echo "scale=3;$UPSTREAMBITS/$RATE" | bc)
RESULT="Upstream ${UPSTREAM} ${PRE}bits per second"
echo "${RESULT}"
;;
downstream)
DOWNSTREAMBITS=$(find_xml_value "${STATUS}" NewLayer1DownstreamMaxBitRate)
require_number "${DOWNSTREAMBITS}" "Could not parse downstream"
DOWNSTREAM=$(echo "scale=3;$DOWNSTREAMBITS/$RATE" | bc)
RESULT="Downstream ${DOWNSTREAM} ${PRE}bits per second"
echo "${RESULT}"
;;
bandwidthdown)
BANDWIDTHDOWNBYTES=$(find_xml_value "${STATUS}" NewByteReceiveRate)
BANDWIDTHDOWN=$(echo "scale=3;$BANDWIDTHDOWNBYTES/$RATE" | bc)
RESULT="Current download ${BANDWIDTHDOWN} ${PRE}bytes per second"
echo "${RESULT}"
;;
bandwidthup)
BANDWIDTHUPBYTES=$(find_xml_value "${STATUS}" NewByteSendRate)
BANDWIDTHUP=$(echo "scale=3;$BANDWIDTHUPBYTES/$RATE" | bc)
RESULT="Current upload ${BANDWIDTHUP} ${PRE}bytes per second"
echo "${RESULT}"
;;
totalbwdown)
TOTALBWDOWNBYTES=$(find_xml_value "${STATUS}" NewTotalBytesReceived)
TOTALBWDOWN=$(echo "scale=3;$TOTALBWDOWNBYTES/$RATE" | bc)
RESULT="total download ${TOTALBWDOWN} ${PRE}bytes"
echo "$RESULT"
;;
totalbwup)
TOTALBWUPBYTES=$(find_xml_value "${STATUS}" NewTotalBytesSent)
TOTALBWUP=$(echo "scale=3;$TOTALBWUPBYTES/$RATE" | bc)
RESULT="total uploads ${TOTALBWUP} ${PRE}bytes"
echo "$RESULT"
;;
connection)
STATE=$(find_xml_value "${STATUS}" NewConnectionStatus)
case ${STATE} in
Connected)
echo "OK - Connected"
exit ${RC_OK}
;;
Connecting | Disconnected)
echo "WARNING - Connection lost"
exit ${RC_WARN}
;;
*)
echo "ERROR - Unknown connection state ${STATE}"
exit ${RC_UNKNOWN}
;;
esac
;;
*)
echo "ERROR - Unknown service check ${CHECK}"
exit ${RC_UNKNOWN}
esac

@ -0,0 +1,104 @@
##################################################################################
# #
# Takes the output from fritz-api.sh , processes it and exports it to Promethues #
# via http on default port 9000 (can be configured via config.yml #
# #
# (C)2022 M.T. Konstapel https://meezenest.nl/mees #
# #
# This file is part of fritzbox_exporter. #
# #
# fritzbox_exporter is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# fritzbox_exporter is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with fritzbox_exporter. If not, see <https://www.gnu.org/licenses/>. #
# #
##################################################################################
import time
import random
import subprocess
import json
from os import path
import yaml
from prometheus_client.core import GaugeMetricFamily, REGISTRY, CounterMetricFamily
from prometheus_client import start_http_server
totalRandomNumber = 0
# Collect Fritzbox data for the first time. This way, the json_object is defined globally. This is not the proper way to do it, but it works.
result = subprocess.run(['./fritz-api.sh', '-j'], stdout=subprocess.PIPE)
#convert string to object
json_object = json.loads(result.stdout)
class Fritzbox_collector(object):
def __init__(self):
pass
def collect(self):
# Connection state
if json_object["Connection"] in ['Connected']:
Fritzbox_connection_state = 2
else:
Fritzbox_connection_state = 1
gauge = GaugeMetricFamily("fritz_connect_state", "Fritzbox connection state (1=not connected, 2=connected)", labels=["Fritzbox"])
gauge.add_metric(['1'], Fritzbox_connection_state)
yield gauge
gauge = GaugeMetricFamily("fritz_down_speed", "Fritzbox downlink speed", labels=["Fritzbox"])
gauge.add_metric(['1'], json_object["DownstreamSync"])
yield gauge
gauge = GaugeMetricFamily("fritz_up_speed", "Fritzbox uplink speed", labels=["Fritzbox"])
gauge.add_metric(['1'], json_object["UpstreamSync"])
yield gauge
count = CounterMetricFamily("fritz_uptime", "Fritzbox uptime", labels=['Fritzbox'])
count.add_metric(['1'], json_object["Uptime"])
yield count
count = CounterMetricFamily("fritz_upload_bw", "Fritzbox upload BW", labels=['Fritzbox'])
count.add_metric(['1'], json_object["UploadBW"])
yield count
count = CounterMetricFamily("fritz_download_bw", "Fritzbox download BW", labels=['Fritzbox'])
count.add_metric(['1'], json_object["DownloadBW"])
yield count
count = CounterMetricFamily("fritz_total_uploads", "Fritzbox total uploads", labels=['Fritzbox'])
count.add_metric(['1'], json_object["TotalUploads"])
yield count
count = CounterMetricFamily("fritz_total_downloads", "Fritzbox total downloads", labels=['Fritzbox'])
count.add_metric(['1'], json_object["TotalDownloads"])
yield count
if __name__ == "__main__":
port = 9000
frequency = 15
if path.exists('config.yml'):
with open('config.yml', 'r') as config_file:
try:
config = yaml.safe_load(config_file)
port = int(config['port'])
frequency = config['scrape_frequency']
except yaml.YAMLError as error:
print(error)
start_http_server(port)
REGISTRY.register(Fritzbox_collector())
while True:
# Collect Fritzbox data
result = subprocess.run(['./fritz-api.sh', '-j'], stdout=subprocess.PIPE)
#convert string to json object
json_object = json.loads(result.stdout)
# period between collection
time.sleep(frequency)
Loading…
Cancel
Save