diff --git a/README.md b/README.md index c7e3d37..b3099eb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,103 @@ -# fritzbox_exporter - -Exports statistics from FritzBox to Prometheus \ No newline at end of file +# 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 ] [-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 + diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..4dbb355 --- /dev/null +++ b/config.yml @@ -0,0 +1,2 @@ +port: 9001 +scrape_frequency: 60 diff --git a/fritz-api.sh b/fritz-api.sh new file mode 100755 index 0000000..0f22bb2 --- /dev/null +++ b/fritz-api.sh @@ -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 ] [-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 " " \ + -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 " " \ + -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 " " \ + -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 " " \ + -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 diff --git a/fritzbox_exporter.py b/fritzbox_exporter.py new file mode 100644 index 0000000..2913597 --- /dev/null +++ b/fritzbox_exporter.py @@ -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 . # +# # +################################################################################## + +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)