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 )