Mercurial > ~darius > hgwebdir.cgi > adslstats
comparison adslstats.py @ 22:a53f90508a06
Switch to TG-1.
Doesn't show FEC errors so skip graphing that.
mysrp.py is a modified version of https://github.com/cocagne/pysrp/blob/master/srp/_pysrp.py
to implement SRP-6 (vs SRP-6a)
author | Daniel O'Connor <darius@dons.net.au> |
---|---|
date | Thu, 15 Jun 2017 15:07:01 +0930 |
parents | 8c44182a2984 |
children | 4b6c811e77df |
comparison
equal
deleted
inserted
replaced
21:8c44182a2984 | 22:a53f90508a06 |
---|---|
1 #!/usr/bin/env python2 | 1 #!/usr/bin/env python2 |
2 ############################################################################ | 2 ############################################################################ |
3 # | 3 # |
4 # Parse DSL link stats for TP-Link W9970 & generate RRD archives & graphs | 4 # Parse DSL link stats for iiNet TG-1 & generate RRD archives & graphs |
5 # | 5 # |
6 ############################################################################ | 6 ############################################################################ |
7 # | 7 # |
8 # Copyright (C) 2015 Daniel O'Connor. All rights reserved. | 8 # Copyright (C) 2017 Daniel O'Connor. All rights reserved. |
9 # | 9 # |
10 # Redistribution and use in source and binary forms, with or without | 10 # Redistribution and use in source and binary forms, with or without |
11 # modification, are permitted provided that the following conditions | 11 # modification, are permitted provided that the following conditions |
12 # are met: | 12 # are met: |
13 # 1. Redistributions of source code must retain the above copyright | 13 # 1. Redistributions of source code must retain the above copyright |
29 # SUCH DAMAGE. | 29 # SUCH DAMAGE. |
30 # | 30 # |
31 ############################################################################ | 31 ############################################################################ |
32 | 32 |
33 import base64 | 33 import base64 |
34 import binascii | |
35 import bs4 | |
34 import ConfigParser | 36 import ConfigParser |
37 import json | |
38 import mechanize | |
39 import mysrp as srp | |
35 import optparse | 40 import optparse |
36 import os | 41 import os |
37 import re | 42 import re |
38 import requests | 43 import requests |
39 import rrdtool | 44 import rrdtool |
40 import sys | 45 import sys |
41 import time | 46 import time |
42 import urllib | 47 import urllib |
43 from bs4 import BeautifulSoup | |
44 | 48 |
45 conf = ConfigParser.ConfigParser() | 49 conf = ConfigParser.ConfigParser() |
46 conf.add_section('global') | 50 conf.add_section('global') |
47 conf.set('global', 'username', 'admin') | 51 conf.set('global', 'username', 'admin') |
48 conf.set('global', 'password', 'admin') | 52 conf.set('global', 'password', 'admin') |
49 conf.set('global', 'name', '10.0.2.13') | 53 conf.set('global', 'name', '10.0.2.14') |
50 | 54 |
51 conflist = ['adslstats.ini'] | 55 conflist = ['adslstats.ini'] |
52 if ('HOME' in os.environ): | 56 if ('HOME' in os.environ): |
53 conflist.append(os.path.expanduser('~/.adslstats.ini')) | 57 conflist.append(os.path.expanduser('~/.adslstats.ini')) |
54 conf.read(conflist) | 58 conf.read(conflist) |
81 def __str__(self): | 85 def __str__(self): |
82 s = '''Line Rate - Up: %d kbits, Down %d kbits | 86 s = '''Line Rate - Up: %d kbits, Down %d kbits |
83 Maximum Rate - Up: %d kbit, Down %s kbit | 87 Maximum Rate - Up: %d kbit, Down %s kbit |
84 Noise Margin - Up: %.1f dB, Down %.1f dB | 88 Noise Margin - Up: %.1f dB, Down %.1f dB |
85 Attenuation - Up: %.1f dB, Down %.1f dB | 89 Attenuation - Up: %.1f dB, Down %.1f dB |
86 Errors - Up: %d, Down %d | |
87 Power - Up: %.1f dBm, Down %.1f dBm | 90 Power - Up: %.1f dBm, Down %.1f dBm |
88 Uptime - %d sec''' % (self.upstream, self.downstream, | 91 Uptime - %d sec''' % (self.upstream, self.downstream, |
89 self.upstreammax, self.downstreammax, | 92 self.upstreammax, self.downstreammax, |
90 self.nmup, self.nmdown, | 93 self.nmup, self.nmdown, |
91 self.attenup, self.attendown, | 94 self.attenup, self.attendown, |
92 self.fecATUC, self.fecATUR, | |
93 self.uppower, self.downpower, | 95 self.uppower, self.downpower, |
94 self.uptime) | 96 self.uptime) |
95 return s | 97 return s |
96 | 98 |
97 def getstats(): | 99 def getstats(): |
98 stats = DSLStats() | 100 stats = DSLStats() |
99 parser = ConfigParser.ConfigParser() | 101 parser = ConfigParser.ConfigParser() |
100 base = 'http://%s' % (conf.get('global', 'name')) | 102 base = 'http://%s' % (conf.get('global', 'name')) |
101 # Gunk extracted from Chrome (what the page is requesting). Note it's sensitive to line ending type... | 103 |
102 # Plus information from http://forum.kitz.co.uk/index.php?topic=15738.0 | 104 # Connect and authenticate |
103 # ATUR = ADSL Termination Unit Remote | 105 br = mechanize.Browser() |
104 # ATUC = ADSL Termination Unit Central office | 106 r = br.open(base) |
105 query = '[WAN_DSL_INTF_CFG#0,0,0,0,0,0#0,0,0,0,0,0]0,0\r\n[WAN_DSL_INTF_STATS_TOTAL#0,0,0,0,0,0#0,0,0,0,0,0]1,0\r\n' | 107 bs = bs4.BeautifulSoup(r) |
106 cookies = {'Authorization' : 'Basic ' + base64.standard_b64encode(conf.get('global', 'username') + ':' + conf.get('global', 'password'))} | 108 token = bs.head.find(lambda tag: tag.has_attr('name') and tag['name'] == 'CSRFtoken')['content'] |
107 headers = {'Referer' : base} | 109 #print('Got CSRF token ' + token) |
108 r = requests.post(base + '/cgi?5&5' , data = query, headers = headers, cookies = cookies, stream = True) | 110 |
109 parser.readfp(r.raw) | 111 usr = srp.User(conf.get('global', 'username'), conf.get('global', 'password'), hash_alg = srp.SHA256, ng_type = srp.NG_2048) |
110 res = {} | 112 uname, A = usr.start_authentication() |
111 tmp = '1,0,0,0,0,0' | 113 |
112 if parser.get(tmp, 'status') == 'Up': | 114 req = mechanize.Request(base + '/authenticate', data = urllib.urlencode({'CSRFtoken' : token, 'I' : uname, 'A' : binascii.hexlify(A)})) |
115 r = br.open(req) | |
116 j = json.decoder.JSONDecoder().decode(r.read()) | |
117 #print('Sent challenge, got ' + str(j)) | |
118 | |
119 M = usr.process_challenge(binascii.unhexlify(j['s']), binascii.unhexlify(j['B'])) | |
120 req = mechanize.Request(base + '/authenticate', data = urllib.urlencode({'CSRFtoken' : token, 'M' : binascii.hexlify(M)})) | |
121 r = br.open(req) | |
122 j = json.decoder.JSONDecoder().decode(r.read()) | |
123 #print('Got response ' + str(j)) | |
124 | |
125 usr.verify_session(binascii.unhexlify(j['M'])) | |
126 if not usr.authenticated(): | |
127 print('Failed to authenticate') | |
128 return None | |
129 | |
130 # Fetch stats and parse | |
131 r = br.open(base + '/modals/broadband-bridge-modal.lp') | |
132 bs = bs4.BeautifulSoup(r) | |
133 | |
134 # Helper function to extract data | |
135 def getvals(bs, text): | |
136 subs = bs.findAll('label', text = text)[0].fetchNextSiblings()[0].strings | |
137 return map(lambda s: float(s.split()[0]), subs) | |
138 | |
139 if map(None, bs.findAll('label', text = 'DSL Status')[0].fetchNextSiblings()[0].strings)[0] == 'Up': | |
113 stats.linkup = True | 140 stats.linkup = True |
114 else: | 141 else: |
115 stats.linkup = False | 142 stats.linkup = False |
116 stats.upstream = float(parser.get(tmp, 'upstreamCurrRate')) | 143 |
117 stats.downstream = float(parser.get(tmp, 'downstreamCurrRate')) | 144 stats.upstreammax, stats.downstreammax = getvals(bs, 'Maximum Line rate') |
118 stats.upstreammax = float(parser.get(tmp, 'upstreamMaxRate')) | 145 stats.upstream, stats.downstream = getvals(bs, 'Line Rate') |
119 stats.downstreammax = float(parser.get(tmp, 'downstreamMaxRate')) | 146 stats.uppower, stats.downpower = getvals(bs, 'Output Power') |
120 stats.nmup = float(parser.get(tmp, 'upstreamNoiseMargin')) / 10.0 | 147 stats.nmup, stats.nmdown = getvals(bs, 'Noise Margin') |
121 stats.nmdown = float(parser.get(tmp, 'downstreamNoiseMargin')) / 10.0 | 148 |
122 stats.attenup = float(parser.get(tmp, 'upstreamAttenuation')) / 10.0 | 149 # Line attenuation returns several values for each direction, parse specially and just take the first one |
123 stats.attendown = float(parser.get(tmp, 'downstreamAttenuation')) / 10.0 | 150 upattens, downattens = map(None, bs.findAll('label', text = 'Line Attenuation')[0].fetchNextSiblings()[0].strings) |
124 stats.fecATUR = int(parser.get(tmp, 'FECErrors')) | 151 stats.attenup = float(re.findall('([0-9.N/A]+)', upattens)[0]) |
125 stats.fecATUC = int(parser.get(tmp, 'ATUCFECErrors')) | 152 stats.attendown = float(re.findall('([0-9.N/A]+)', downattens)[0]) |
126 stats.uppower = float(parser.get(tmp, 'upstreamPower')) / 10.0 # I think it's tenths of a dBm but who knows | 153 |
127 stats.downpower = float(parser.get(tmp, 'downstreamPower')) / 10.0 | 154 # Convert something like '2days 17hours 28min 19sec' into seconds |
128 stats.uptime = int(parser.get(tmp, 'showtimeStart')) | 155 uptime = re.findall('([0-9]+)', map(None, bs.findAll('label', text = 'DSL Uptime')[0].fetchNextSiblings()[0].strings)[0]) |
156 uptime.reverse() # End up with an array of seconds, minutes, hours, etc | |
157 mults = [1, 60, 60 * 60, 24 * 60 * 60] | |
158 if len(uptime) > mults: | |
159 print('Too many uptime elements to work out') | |
160 stats.uptime = None | |
161 else: | |
162 stats.uptime = reduce(lambda a, b: a + b, map(lambda a: int(a[0]) * a[1], zip(uptime, mults))) | |
129 | 163 |
130 return stats | 164 return stats |
131 | 165 |
132 # Setup RRD | 166 # Setup RRD |
133 # We expect data to be logged every 5 minutes | 167 # We expect data to be logged every 5 minutes |
156 'RRA:MAX:0.1:12:168') | 190 'RRA:MAX:0.1:12:168') |
157 | 191 |
158 # Update the RRD (format stats as expected) | 192 # Update the RRD (format stats as expected) |
159 def updaterrd(filename, tstamp, stats): | 193 def updaterrd(filename, tstamp, stats): |
160 rrdtool.update(filename, | 194 rrdtool.update(filename, |
161 '%d:%d:%d:%d:%d:%f:%f:%f:%f:%d:%d:%f:%f:%d' % ( | 195 '%d:%d:%d:%d:%d:%f:%f:%f:%f:U:U:%f:%f:%d' % ( |
162 tstamp, | 196 tstamp, |
163 stats.upstream, | 197 stats.upstream, |
164 stats.downstream, | 198 stats.downstream, |
165 stats.upstreammax, | 199 stats.upstreammax, |
166 stats.downstreammax, | 200 stats.downstreammax, |
167 stats.nmup, | 201 stats.nmup, |
168 stats.nmdown, | 202 stats.nmdown, |
169 stats.attenup, | 203 stats.attenup, |
170 stats.attendown, | 204 stats.attendown, |
171 stats.fecATUC, | |
172 stats.fecATUR, | |
173 stats.uppower, | 205 stats.uppower, |
174 stats.downpower, | 206 stats.downpower, |
175 stats.uptime)) | 207 stats.uptime)) |
176 | 208 |
177 # Open the URL and call the parser | 209 # Open the URL and call the parser |
277 'CDEF:powerdowndif=powerdownmax,powerdownmin,-', | 309 'CDEF:powerdowndif=powerdownmax,powerdownmin,-', |
278 | 310 |
279 'LINE0:powerdownmin#000000:', | 311 'LINE0:powerdownmin#000000:', |
280 'AREA:powerdowndif#604872::STACK', | 312 'AREA:powerdowndif#604872::STACK', |
281 'LINE1:powerdown#c090e5:Power - Down (dBm)', | 313 'LINE1:powerdown#c090e5:Power - Down (dBm)', |
282 | |
283 'DEF:fecATUC=%s:fecATUC:AVERAGE' % rrdname, | |
284 'LINE1:fecATUC#fff384:Upstream errors', | |
285 | |
286 'DEF:fecATUR=%s:fecATUR:AVERAGE' % rrdname, | |
287 'LINE1:fecATUR#45cfc9:Downstream errors', | |
288 | 314 |
289 'DEF:uptime=%s:uptime:AVERAGE' % rrdname, | 315 'DEF:uptime=%s:uptime:AVERAGE' % rrdname, |
290 'CDEF:uptimepct=uptime,10,*', | 316 'CDEF:uptimepct=uptime,10,*', |
291 'LINE1:uptimepct#606060:Uptime (10\'s%)', | 317 'LINE1:uptimepct#606060:Uptime (10\'s%)', |
292 ) | 318 ) |