diff --git a/.gitignore b/.gitignore index 72364f9..84c3713 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,6 @@ ENV/ # Rope project settings .ropeproject + +.credentials +.idea diff --git a/README.md b/README.md index 883e305..99511c1 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ This is what I used, you can of course adapt the collector script to talk to inf First thing to do is to register an app, to generate a specific `freebox_app_token`. -Use the `freebox_register_app.py` script. +Run `python freebox_monitoring.py --register` to do that. *PS: You can modify the app name/versions etc as shown below (Optional)* @@ -45,7 +45,11 @@ Head to your Freebox Server device. Press the `>` to authorize the app registration process. -Be sure to save the `freebox_app_token` and `track_id` somewhere safe, you will need them to authenticate later on. +You can check the saved tokens with `python freebox_monitor.py --register-status`: + +![register-status](freebox_registration_status.png) + +If you need to re-auth you can delete the authorization credentials by removing the file `.credentials` in the directory where `freebox_monitor.py` is. # Step 2: Use the script to display freebox statistics information @@ -55,18 +59,39 @@ Once you have your `freebox_app_token`, the process to authenticate happens in 2 (This avoids sending the token over the network) -Edit the `freebox_monitor.py` script and set your `freebox_app_token` and `track_id` (line 73-74) - -```python - freebox_app_token = "CHANGE_THIS" - track_id = "CHANGE_THIS" -``` - Then execute it, to make sure it connects and displays information. ![freebox monitor](freebox_monitor.png) -# Step 3: Leverage telegraf to call the script and send stats to Graphite +# Step 3: Stats to get and show + +By default it auto adapts beetween FFTH and xDSL, by using a switch indicated (`python freebox_monitor.py 'indicated switch'`) you can get the listed stats. + + * FFTH and xDSL (no switch, default) + * bytes up/down + * rate up/down + * bandwidth up/down + * connection state + + * FTTH + * sfp power rx/tx + + * xDSL (each for up, and down, except uptime) + * uptime + * errors: es, hec, crc, ses, fec + * rate, attenuation, signal noise ratio, max rate + * G.INP status, corrected and uncorrected + + * System infos (-H switch) + * Fan RPM, temp SW, CPU B, CPU M, Box uptime + + * Switch status (-S switch) + * for each switch port: link mode + + * Switch ports status (-P switch) + * for each switch port: rx/tx bytes rate + +# Step 4: Leverage telegraf to call the script and send stats to Graphite Install telegraf on the SexiGraf appliance. @@ -147,4 +172,9 @@ Here is a 2 day view of the download/upload stats. ![dashboard 2days](freebox_2days.png) +Example of the xDSL graphs + +![xdsl_dash_12h_1](freebox_xdsl_12h_1.png) +![xdsl_dash_12h_2](freebox_xdsl_12h_2.png) + Enjoy ! diff --git a/freebox_monitor.py b/freebox_monitor.py index 32bfaf7..508f8c0 100644 --- a/freebox_monitor.py +++ b/freebox_monitor.py @@ -1,32 +1,42 @@ #!/usr/bin/env python # pylint: disable=C0103,C0111,W0621 +from __future__ import print_function +import requests +import os +import json +import hmac +import time +import argparse +import sys +from hashlib import sha1 + +if sys.version_info >= (3, 0): + import configparser as configp +else: + import ConfigParser as configp # # Freebox API SDK / Docs: http://dev.freebox.fr/sdk/os/login/ # -import os -import json -import hmac -import time - -from hashlib import sha1 -import requests +VERSION = "0.4.3" +ENDPOINT = "http://mafreebox.freebox.fr/api/v3" def get_challenge(freebox_app_id): - api_url = 'http://mafreebox.freebox.fr/api/v3/login/authorize/%s' % freebox_app_id + api_url = '%s/login/authorize/%s' % (ENDPOINT, freebox_app_id) r = requests.get(api_url) if r.status_code == 200: return r.json() else: - print 'Failed request: %s\n' % r.text + print("Failed request: %s\n" % r.text) + def open_session(password, freebox_app_id): - api_url = 'http://mafreebox.freebox.fr/api/v3/login/session/' + api_url = '%s/login/session/' % ENDPOINT app_info = { 'app_id': freebox_app_id, @@ -39,46 +49,87 @@ def open_session(password, freebox_app_id): if r.status_code == 200: return r.json() else: - print 'Failed request: %s\n' % r.text - + print("Failed request: %s\n" % r.text) def get_connection_stats(headers): - api_url = 'http://mafreebox.freebox.fr/api/v3/connection/' + api_url = '%s/connection/' % ENDPOINT r = requests.get(api_url, headers=headers) if r.status_code == 200: return r.json() else: - print 'Failed request: %s\n' % r.text - + print("Failed request: %s\n" % r.text) def get_ftth_status(headers): - api_url = 'http://mafreebox.freebox.fr/api/v3/connection/ftth/' + api_url = '%s/connection/ftth/' % ENDPOINT r = requests.get(api_url, headers=headers) if r.status_code == 200: return r.json() else: - print 'Failed request: %s\n' % r.text + print('Failed request: %s\n' % r.text) -# Main -if __name__ == '__main__': +def get_xdsl_status(headers): + api_url = '%s/connection/xdsl/' % ENDPOINT + r = requests.get(api_url, headers=headers) + + if r.status_code == 200: + return r.json() + else: + print('Failed request: %s\n' % r.text) + + +def get_system_config(headers): + api_url = '%s/system/' % ENDPOINT + + r = requests.get(api_url, headers=headers) + + if r.status_code == 200: + return r.json() + else: + print('Failed request: %s\n' % r.text) + + +def get_switch_status(headers): + api_url = '%s/switch/status/' % ENDPOINT + + r = requests.get(api_url, headers=headers) + + if r.status_code == 200: + return r.json() + else: + print('Failed request: %s\n' % r.text) + + +def get_switch_port_stats(headers, port): + api_url = '%s/switch/port/%s/stats' % (ENDPOINT, port) + + r = requests.get(api_url, headers=headers) + + if r.status_code == 200: + return r.json() + else: + print('Failed request: %s\n' % r.text) + + +def get_and_print_metrics(creds, s_switch, s_ports, s_sys): freebox_app_id = "fr.freebox.seximonitor" - freebox_app_token = "CHANGE_THIS" - track_id = "CHANGE_THIS" # Fetch challenge - resp = get_challenge(track_id) + resp = get_challenge(creds['track_id']) challenge = resp['result']['challenge'] # Generate session password - h = hmac.new(freebox_app_token, challenge, sha1) + if sys.version_info >= (3, 0): + h = hmac.new(bytearray(creds['app_token'], 'ASCII'), bytearray(challenge, 'ASCII'), sha1) + else: + h = hmac.new(creds['app_token'], challenge, sha1) password = h.hexdigest() # Fetch session_token @@ -91,34 +142,227 @@ if __name__ == '__main__': } # Setup hashtable for results - myData = {} + my_data = {} # Fetch connection stats - jsonRaw = get_connection_stats(headers) + json_raw = get_connection_stats(headers) - myData['bytes_down'] = jsonRaw['result']['bytes_down'] - myData['bytes_up'] = jsonRaw['result']['bytes_up'] - myData['rate_down'] = jsonRaw['result']['rate_down'] - myData['rate_up'] = jsonRaw['result']['rate_up'] - if jsonRaw['result']['state'] == "up": - myData['state'] = 1 + # Generic datas, same for FFTH or xDSL + my_data['bytes_down'] = json_raw['result']['bytes_down'] # total in bytes since last connection + my_data['bytes_up'] = json_raw['result']['bytes_up'] + + my_data['rate_down'] = json_raw['result']['rate_down'] # current rate in byte/s + my_data['rate_up'] = json_raw['result']['rate_up'] + + my_data['bandwidth_down'] = json_raw['result']['bandwidth_down'] # available bw in bit/s + my_data['bandwidth_up'] = json_raw['result']['bandwidth_up'] + + if json_raw['result']['state'] == "up": + my_data['state'] = 1 else: - myData['state'] = 0 + my_data['state'] = 0 - # Fetch ftth signal stats - jsonRaw = get_ftth_status(headers) + # ffth for FFTH (default) + # xdsl for xDSL + connection_media = json_raw['result']['media'] - myData['sfp_pwr_rx'] = jsonRaw['result']['sfp_pwr_rx'] - myData['sfp_pwr_tx'] = jsonRaw['result']['sfp_pwr_tx'] + ### + # FFTH specific + if connection_media == "ffth": + json_raw = get_ftth_status(headers) + + my_data['sfp_pwr_rx'] = json_raw['result']['sfp_pwr_rx'] # scaled by 100 (in dBm) + my_data['sfp_pwr_tx'] = json_raw['result']['sfp_pwr_tx'] + + ### + # xDSL specific + if connection_media == "xdsl": + json_raw = get_xdsl_status(headers) + + my_data['xdsl_uptime'] = json_raw['result']['status']['uptime'] # in seconds + + if json_raw['result']['status']['status'] == "down": # unsynchronized + my_data['xdsl_status'] = 0 + elif json_raw['result']['status']['status'] == "training": # synchronizing step 1/4 + my_data['xdsl_status'] = 1 + elif json_raw['result']['status']['status'] == "started": # synchronizing step 2/4 + my_data['xdsl_status'] = 2 + elif json_raw['result']['status']['status'] == "chan_analysis": # synchronizing step 3/4 + my_data['xdsl_status'] = 3 + elif json_raw['result']['status']['status'] == "msg_exchange": # synchronizing step 4/4 + my_data['xdsl_status'] = 4 + elif json_raw['result']['status']['status'] == "showtime": # ready + my_data['xdsl_status'] = 5 + elif json_raw['result']['status']['status'] == "disabled": # disabled + my_data['xdsl_status'] = 6 + else: # unknown + my_data['xdsl_status'] = 999 + + my_data['xdsl_down_es'] = json_raw['result']['down']['es'] # increment + my_data['xdsl_down_attn'] = json_raw['result']['down']['attn'] # in dB + my_data['xdsl_down_snr'] = json_raw['result']['down']['snr'] # in dB + my_data['xdsl_down_rate'] = json_raw['result']['down']['rate'] # ATM rate in kbit/s + my_data['xdsl_down_hec'] = json_raw['result']['down']['hec'] # increment + my_data['xdsl_down_crc'] = json_raw['result']['down']['crc'] # increment + my_data['xdsl_down_ses'] = json_raw['result']['down']['ses'] # increment + my_data['xdsl_down_fec'] = json_raw['result']['down']['fec'] # increment + my_data['xdsl_down_maxrate'] = json_raw['result']['down']['maxrate'] # ATM max rate in kbit/s + my_data['xdsl_down_rtx_tx'] = json_raw['result']['down']['rtx_tx'] # G.INP on/off + my_data['xdsl_down_rtx_c'] = json_raw['result']['down']['rtx_c'] # G.INP corrected + my_data['xdsl_down_rtx_uc'] = json_raw['result']['down']['rtx_uc'] # G.INP uncorrected + + my_data['xdsl_up_es'] = json_raw['result']['up']['es'] + my_data['xdsl_up_attn'] = json_raw['result']['up']['attn'] + my_data['xdsl_up_snr'] = json_raw['result']['up']['snr'] + my_data['xdsl_up_rate'] = json_raw['result']['up']['rate'] + my_data['xdsl_up_hec'] = json_raw['result']['up']['hec'] + my_data['xdsl_up_crc'] = json_raw['result']['up']['crc'] + my_data['xdsl_up_ses'] = json_raw['result']['up']['ses'] + my_data['xdsl_up_fec'] = json_raw['result']['up']['fec'] + my_data['xdsl_up_maxrate'] = json_raw['result']['up']['maxrate'] + my_data['xdsl_up_rtx_tx'] = json_raw['result']['up']['rtx_tx'] # G.INP on/off + my_data['xdsl_up_rtx_c'] = json_raw['result']['up']['rtx_c'] # G.INP corrected + my_data['xdsl_up_rtx_uc'] = json_raw['result']['up']['rtx_uc'] # G.INP uncorrected + + ## + # General infos + if s_sys: + sys_json_raw = get_system_config(headers) + my_data['sys_fan_rpm'] = sys_json_raw['result']['fan_rpm'] # rpm + my_data['sys_temp_sw'] = sys_json_raw['result']['temp_sw'] # Temp Switch, degree Celcius + my_data['sys_uptime'] = sys_json_raw['result']['uptime_val'] # Uptime, in seconds + my_data['sys_temp_cpub'] = sys_json_raw['result']['temp_cpub'] # Temp CPU Broadcom, degree Celcius + my_data['sys_temp_cpum'] = sys_json_raw['result']['temp_cpum'] # Temp CPU Marvell, degree Celcius + + ## + # Switch status + if s_switch: + switch_json_raw = get_switch_status(headers) + for i in switch_json_raw['result']: + # 0 down, 1 up + my_data['switch_%s_link' % i['id']] = 0 if i['link'] == "down" else 1 + # 0 auto, 1 10Base-T, 2 100Base-T, 3 1000Base-T + # In fact the duplex is appended like 10BaseT-HD, 1000BaseT-FD, 1000BaseT-FD + # So juse is an "in" because duplex isn't really usefull + if "10BaseT" in i['mode']: + my_data['switch_%s_mode' % i['id']] = 1 + elif "100BaseT" in i['mode']: + my_data['switch_%s_mode' % i['id']] = 2 + elif "1000BaseT" in i['mode']: + my_data['switch_%s_mode' % i['id']] = 3 + else: + my_data['switch_%s_mode' % i['id']] = 0 # auto + + ## + # Switch ports status + if s_ports: + for i in [1, 2, 3, 4]: + switch_port_stats = get_switch_port_stats(headers, i) + my_data['switch_%s_rx_bytes_rate' % i] = switch_port_stats['result']['rx_bytes_rate'] # bytes/s (?) + my_data['switch_%s_tx_bytes_rate' % i] = switch_port_stats['result']['tx_bytes_rate'] # Prepping Graphite Data format timestamp = int(time.time()) # Output the information - print "freebox.bytes_down %s %d" % (myData['bytes_down'], timestamp) - print "freebox.bytes_up %s %d" % (myData['bytes_up'], timestamp) - print "freebox.rate_down %s %d" % (myData['rate_down'], timestamp) - print "freebox.rate_up %s %d" % (myData['rate_up'], timestamp) - print "freebox.state %s %d" % (myData['state'], timestamp) - print "freebox.sfp_pwr_rx %s %d" % (myData['sfp_pwr_rx'], timestamp) - print "freebox.sfp_pwr_tx %s %d" % (myData['sfp_pwr_tx'], timestamp) + for i in my_data: + print("freebox.%s %s %d" % (i, my_data[i], timestamp)) + + +def get_auth(): + script_dir = os.path.dirname(os.path.realpath(__file__)) + cfg_file = os.path.join(script_dir, ".credentials") + + f = configp.RawConfigParser() + f.read(cfg_file) + + try: + _ = f.get("general", "track_id") + _ = f.get("general", "app_token") + except configp.NoSectionError: + print("Config is invalid, auth not done.") + return None + + return {'track_id': f.get('general', 'track_id'), + 'app_token': f.get('general', 'app_token')} + + +def write_auth(auth_infos): + script_dir = os.path.dirname(os.path.realpath(__file__)) + cfg_file = os.path.join(script_dir, ".credentials") + f = configp.RawConfigParser() + f.add_section("general") + f.set("general", "track_id", auth_infos['track_id']) + f.set("general", "app_token", auth_infos["app_token"]) + with open(cfg_file, "wb") as authFile: + f.write(authFile) + + +def do_register(creds): + if creds is not None: + if 'track_id' in creds and 'app_token' in creds: + print("Already registered, exiting") + return + + print("Doing registration") + headers = {'Content-type': 'application/json'} + app_info = { + 'app_id': 'fr.freebox.seximonitor', + 'app_name': 'SexiMonitor', + 'app_version': VERSION, + 'device_name': 'SexiServer' + } + json_payload = json.dumps(app_info) + + r = requests.post('%s/login/authorize/' % ENDPOINT, headers=headers, data=json_payload) + register_infos = None + + if r.status_code == 200: + register_infos = r.json() + else: + print('Failed registration: %s\n' % r.text) + + write_auth(register_infos['result']) + print("Don't forget to accept auth on the Freebox panel !") + + +def register_status(creds): + if not creds: + print("Status: invalid config, auth not done.") + print("Please run `%s --register` to register app." % sys.argv[0]) + return + print("Status: auth already done") + print(" track_id: %s" % creds["track_id"]) + print(" app_token: %s" % creds["app_token"]) + + +# Main +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-r', '--register', action='store_true', help="Register app with Freebox API") + parser.add_argument('-s', '--register-status', dest='status', action='store_true', help="Get register status") + + parser.add_argument('-S', '--status-switch', + dest='status_switch', + action='store_true', + help="Get and show switch status") + + parser.add_argument('-P', '--status-ports', + dest='status_ports', + action='store_true', + help="Get and show switch ports stats") + + parser.add_argument('-H', '--status-sys', + dest='status_sys', + action='store_true', + help="Get and show system status") + args = parser.parse_args() + + auth = get_auth() + + if args.register: + do_register(auth) + elif args.status: + register_status(auth) + else: + get_and_print_metrics(auth, args.status_switch, args.status_ports, args.status_sys) diff --git a/freebox_register_app.py b/freebox_register_app.py deleted file mode 100644 index 478a497..0000000 --- a/freebox_register_app.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python -# pylint: disable=C0103,C0111 - -import json -import requests - -def get_authorization(): - api_url = 'http://mafreebox.freebox.fr/api/v3/login/authorize/' - headers = {'Content-type': 'application/json'} - app_info = { - 'app_id': 'fr.freebox.seximonitor', - 'app_name': 'SexiMonitor', - 'app_version': '0.4.2', - 'device_name': 'SexiServer' - } - json_payload = json.dumps(app_info) - - r = requests.post(api_url, headers=headers, data=json_payload) - - if r.status_code == 200: - return r.json() - else: - print 'Failed registration: %s\n' % r.text - -if __name__ == '__main__': - resp = get_authorization() - - print '[Track ID] {}'.format(resp['result']['track_id']) - print '[App token] {}'.format(resp['result']['app_token']) - print 'Press on the right arrow on the Freebox Server and validate the app registration' diff --git a/freebox_registration.png b/freebox_registration.png index f3ee9e5..aad2f7f 100755 Binary files a/freebox_registration.png and b/freebox_registration.png differ diff --git a/freebox_registration_status.png b/freebox_registration_status.png new file mode 100644 index 0000000..15a723f Binary files /dev/null and b/freebox_registration_status.png differ diff --git a/freebox_xdsl_12h_1.png b/freebox_xdsl_12h_1.png new file mode 100644 index 0000000..29fd372 Binary files /dev/null and b/freebox_xdsl_12h_1.png differ diff --git a/freebox_xdsl_12h_2.png b/freebox_xdsl_12h_2.png new file mode 100644 index 0000000..d5ccd24 Binary files /dev/null and b/freebox_xdsl_12h_2.png differ