Mercurial > ~darius > hgwebdir.cgi > adslstats
comparison adslstats.py @ 14:2debc3fb4372
Update for VDSL modem (TP-Link W9970).
Log maximum as well as current rates.
Remove trailing whitespace.
Keep 5 years of data.
author | Daniel O'Connor <darius@dons.net.au> |
---|---|
date | Fri, 20 May 2016 15:06:43 +0930 |
parents | bf46efd061d7 |
children | 7dbe86981f6b |
comparison
equal
deleted
inserted
replaced
13:bf46efd061d7 | 14:2debc3fb4372 |
---|---|
1 #!/usr/bin/env python2 | 1 #!/usr/bin/env python2 |
2 ############################################################################ | 2 ############################################################################ |
3 # | 3 # |
4 # Parse ADSL link stats for Billion 7300G & generate RRD archives & graphs | 4 # Parse ADSL link stats for TP-Link W9970 & generate RRD archives & graphs |
5 # | 5 # |
6 ############################################################################ | 6 ############################################################################ |
7 # | 7 # |
8 # Copyright (C) 2015 Daniel O'Connor. All rights reserved. | 8 # Copyright (C) 2015 Daniel O'Connor. All rights reserved. |
9 # | 9 # |
28 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | 28 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF |
29 # SUCH DAMAGE. | 29 # SUCH DAMAGE. |
30 # | 30 # |
31 ############################################################################ | 31 ############################################################################ |
32 | 32 |
33 import base64 | |
33 import ConfigParser | 34 import ConfigParser |
34 import optparse | 35 import optparse |
35 import os | 36 import os |
36 import re | 37 import re |
38 import requests | |
37 import rrdtool | 39 import rrdtool |
38 import sys | 40 import sys |
39 import time | 41 import time |
40 import urllib | 42 import urllib |
41 from bs4 import BeautifulSoup | 43 from bs4 import BeautifulSoup |
42 | 44 |
43 conf = ConfigParser.ConfigParser() | 45 conf = ConfigParser.ConfigParser() |
44 conf.add_section('global') | 46 conf.add_section('global') |
45 conf.set('global', 'username', 'admin') | 47 conf.set('global', 'username', 'admin') |
46 conf.set('global', 'password', 'admin') | 48 conf.set('global', 'password', 'admin') |
47 conf.set('global', 'name', 'dsl.dons.net.au') | 49 conf.set('global', 'name', '10.0.2.13') |
48 | 50 |
49 conflist = ['adslstats.ini'] | 51 conflist = ['adslstats.ini'] |
50 if ('HOME' in os.environ): | 52 if ('HOME' in os.environ): |
51 conflist.append(os.path.expanduser('~/.adslstats.ini')) | 53 conflist.append(os.path.expanduser('~/.adslstats.ini')) |
52 conf.read(conflist) | 54 conf.read(conflist) |
84 14 : 'Rate (Kbps):', | 86 14 : 'Rate (Kbps):', |
85 } | 87 } |
86 | 88 |
87 class ADSLStats(object): | 89 class ADSLStats(object): |
88 def __str__(self): | 90 def __str__(self): |
89 return """Line Rate - Up: %d kbits, Down %d kbits | 91 s = "Line Rate - Up: %d kbits, Down %d kbits\n" % (self.upstream, self.downstream) |
90 Noise Margin - Up: %.1f dB, Down %.1f dB | 92 if hasattr(self, 'upstreammax'): |
91 Attenuation - Up: %.1f dB, Down %.1f dB""" % (self.upstream, self.downstream, | 93 s += "Maximum Rate - Up: %d kbit, Down %s kbit\n" % (self.upstreammax, self.downstreammax) |
92 self.nmup, self.nmdown, | 94 s += """Noise Margin - Up: %.1f dB, Down %.1f dB |
95 Attenuation - Up: %.1f dB, Down %.1f dB""" % (self.nmup, self.nmdown, | |
93 self.attenup, self.attendown) | 96 self.attenup, self.attendown) |
94 def getstats(f): | 97 return s |
95 s = BeautifulSoup(f) | 98 |
96 a = s.findAll('tr') | 99 def getstats(): |
97 | |
98 for i in statsdict: | |
99 assert a[i].td.contents[0] == statsdict[i] | |
100 | |
101 stats = ADSLStats() | 100 stats = ADSLStats() |
102 | 101 parser = ConfigParser.ConfigParser() |
103 # Check if the modem is offline | 102 base = 'http://%s' % (conf.get('global', 'name')) |
104 if a[3].td.findNext('td').contents[0] != 'Up': | 103 # Gunk extracted from Chrome (what the page is requesting). Note it's sensitive to line ending type... |
105 return None | 104 # We could get more data, eg error rates.. |
106 | 105 data = '[WAN_DSL_INTF_CFG#1,0,0,0,0,0#0,0,0,0,0,0]0,12\r\nstatus\r\nmodulationType\r\nX_TP_AdslModulationCfg\r\nupstreamCurrRate\r\ndownstreamCurrRate\r\nX_TP_AnnexType\r\nupstreamMaxRate\r\ndownstreamMaxRate\r\nupstreamNoiseMargin\r\ndownstreamNoiseMargin\r\nupstreamAttenuation\r\ndownstreamAttenuation\r\n[WAN_DSL_INTF_STATS_TOTAL#1,0,0,0,0,0#0,0,0,0,0,0]1,8\r\nATUCCRCErrors\r\nCRCErrors\r\nATUCFECErrors\r\nFECErrors\r\nSeverelyErroredSecs\r\nX_TP_US_SeverelyErroredSecs\r\nerroredSecs\r\nX_TP_US_ErroredSecs\r\n' |
107 # dB | 106 cookies = {'Authorization' : 'Basic ' + base64.standard_b64encode(conf.get('global', 'username') + ':' + conf.get('global', 'password'))} |
108 stats.nmdown = float(a[8].td.findNext('td').contents[0]) / 10.0 | 107 headers = {'Referer' : base} |
109 stats.nmup = float(a[8].td.findNext('td').findNext('td').contents[0]) / 10.0 | 108 r = requests.post(base + '/cgi?1&5' , data = data, headers = headers, cookies = cookies, stream = True) |
110 stats.attendown = float(a[9].td.findNext('td').contents[0]) / 10.0 | 109 parser.readfp(r.raw) |
111 stats.attenup = float(a[9].td.findNext('td').findNext('td').contents[0]) / 10.0 | 110 res = {} |
112 # kBit | 111 tmp = '1,0,0,0,0,0' |
113 stats.downstream = float(a[14].td.findNext('td').contents[0]) | 112 if parser.get(tmp, 'status') == 'Up': |
114 stats.upstream = float(a[14].td.findNext('td').findNext('td').contents[0]) | 113 stats.linkup = True |
114 else: | |
115 stats.linkup = False | |
116 stats.upstream = float(parser.get(tmp, 'upstreamCurrRate')) | |
117 stats.downstream = float(parser.get(tmp, 'downstreamCurrRate')) | |
118 stats.upstreammax = float(parser.get(tmp, 'upstreamMaxRate')) | |
119 stats.downstreammax = float(parser.get(tmp, 'downstreamMaxRate')) | |
120 stats.nmup = float(parser.get(tmp, 'upstreamNoiseMargin')) / 10.0 | |
121 stats.nmdown = float(parser.get(tmp, 'downstreamNoiseMargin')) / 10.0 | |
122 stats.attenup = float(parser.get(tmp, 'upstreamAttenuation')) / 10.0 | |
123 stats.attendown = float(parser.get(tmp, 'downstreamAttenuation')) / 10.0 | |
115 | 124 |
116 return stats | 125 return stats |
117 | 126 |
118 # Setup RRD | 127 # Setup RRD |
119 # We expect data to be logged every 5 minutes | 128 # We expect data to be logged every 5 minutes |
120 # Average 12 5 minute points -> hourly stats (keep 168 - a weeks worth) | 129 # Average 12 5 minute points -> hourly stats (keep 168 - a weeks worth) |
121 # Average 288 5 minute points -> daily stats (keep 365 - a years worth) | 130 # Average 288 5 minute points -> daily stats (keep 1825 - 5 years worth) |
122 # Detemine minimum & maximum for an hour and keep a weeks worth. | 131 # Detemine minimum & maximum for an hour and keep a weeks worth. |
123 def makerrd(filename): | 132 def makerrd(filename): |
124 rrdtool.create(filename, | 133 rrdtool.create(filename, |
125 '--step', '300', | 134 '--step', '300', |
126 'DS:upstream:GAUGE:3600:32:25000', # Upstream (kbits) - 24mbit is ADSL2+ max | 135 'DS:upstream:GAUGE:3600:32:150000', # Upstream (kbits) |
127 'DS:downstream:GAUGE:3600:32:25000', # Downstream (kbits) | 136 'DS:downstream:GAUGE:3600:32:150000', # Downstream (kbits) |
137 'DS:upstreammax:GAUGE:3600:32:150000', # Upstream maximum (kbits) | |
138 'DS:downstreammax:GAUGE:3600:32:150000', # Downstream maximum (kbits) | |
128 'DS:nmup:GAUGE:3600:0:100', # Upstream Noise margin (dB) | 139 'DS:nmup:GAUGE:3600:0:100', # Upstream Noise margin (dB) |
129 'DS:nmdown:GAUGE:3600:0:100', # Downstream Noise margin (dB) | 140 'DS:nmdown:GAUGE:3600:0:100', # Downstream Noise margin (dB) |
130 'DS:attenup:GAUGE:3600:0:100', # Upstream Attenuation (dB) | 141 'DS:attenup:GAUGE:3600:0:100', # Upstream Attenuation (dB) |
131 'DS:attendown:GAUGE:3600:0:100', # Downstream Attenuation (dB) | 142 'DS:attendown:GAUGE:3600:0:100', # Downstream Attenuation (dB) |
132 'RRA:AVERAGE:0.1:12:168', | 143 'RRA:AVERAGE:0.1:12:168', |
133 'RRA:AVERAGE:0.1:288:365', | 144 'RRA:AVERAGE:0.1:288:1825', |
134 'RRA:MIN:0.1:12:168', | 145 'RRA:MIN:0.1:12:168', |
135 'RRA:MAX:0.1:12:168') | 146 'RRA:MAX:0.1:12:168') |
136 | 147 |
137 # Update the RRD (format stats as expected) | 148 # Update the RRD (format stats as expected) |
138 def updaterrd(filename, tstamp, stats): | 149 def updaterrd(filename, tstamp, stats): |
139 rrdtool.update(filename, | 150 rrdtool.update(filename, |
140 '%d:%d:%d:%f:%f:%f:%f' % (tstamp, | 151 '%d:%d:%d:%d:%d:%f:%f:%f:%f' % (tstamp, |
141 stats.upstream, | 152 stats.upstream, |
142 stats.downstream, | 153 stats.downstream, |
154 stats.upstreammax, | |
155 stats.downstreammax, | |
143 stats.nmup, | 156 stats.nmup, |
144 stats.nmdown, | 157 stats.nmdown, |
145 stats.attenup, | 158 stats.attenup, |
146 stats.attendown)) | 159 stats.attendown)) |
147 | 160 |
148 # Open the URL and call the parser | 161 # Open the URL and call the parser |
149 def getdata(): | 162 def getdata(): |
150 opener = urllib.FancyURLopener() | 163 stats = getstats() |
151 opener.prompt_user_passwd = lambda host, realm: (options.authname, options.password) | |
152 f = opener.open(statsurl) | |
153 #f = open("adsl.html") | |
154 stats = getstats(f) | |
155 if stats == None: | |
156 return None | |
157 return stats | 164 return stats |
158 | 165 |
159 # Generate a graph | 166 # Generate a graph |
160 def gengraph(): | 167 def gengraph(): |
161 | 168 |
162 linkargs = ( | 169 linkargs = ( |
163 '-a', 'SVG', | 170 '-a', 'SVG', |
164 '-X', '0', | 171 '-X', '0', |
172 '-l', '0', | |
165 '--vertical-label', 'kbit/sec', | 173 '--vertical-label', 'kbit/sec', |
166 '--slope-mode', | 174 '--slope-mode', |
167 | 175 |
168 'DEF:upstream=%s:upstream:AVERAGE' % rrdname, | 176 'DEF:upstream=%s:upstream:AVERAGE' % rrdname, |
169 'DEF:upstreammin=%s:upstream:MIN' % rrdname, | 177 'DEF:upstreammin=%s:upstream:MIN' % rrdname, |
170 'DEF:upstreammax=%s:upstream:MAX' % rrdname, | 178 'DEF:upstreammax=%s:upstream:MAX' % rrdname, |
171 | 179 |
172 'DEF:downstream=%s:downstream:AVERAGE' % rrdname, | 180 'DEF:downstream=%s:downstream:AVERAGE' % rrdname, |
173 'DEF:downstreammin=%s:downstream:MIN' % rrdname, | 181 'DEF:downstreammin=%s:downstream:MIN' % rrdname, |
174 'DEF:downstreammax=%s:downstream:MAX' % rrdname, | 182 'DEF:downstreammax=%s:downstream:MAX' % rrdname, |
175 | 183 |
176 'CDEF:upstreamdif=upstreammax,upstreammin,-', | 184 'CDEF:upstreamdif=upstreammax,upstreammin,-', |
177 'CDEF:downstreamdif=downstreammax,downstreammin,-', | 185 'CDEF:downstreamdif=downstreammax,downstreammin,-', |
178 | 186 |
187 'DEF:maxupstream=%s:upstreammax:AVERAGE' % rrdname, | |
188 'DEF:maxdownstream=%s:downstreammax:AVERAGE' % rrdname, | |
189 | |
179 'LINE0:upstreammin#000000:', | 190 'LINE0:upstreammin#000000:', |
180 'AREA:upstreamdif#00dc76::STACK', | 191 'AREA:upstreamdif#00dc76::STACK', |
181 'LINE1:upstream#00ff00:Upstream', | 192 'LINE1:upstream#00ff00:Upstream', |
182 | 193 |
183 'LINE0:downstreammin#000000:', | 194 'LINE0:downstreammin#000000:', |
184 'AREA:downstreamdif#ff8686::STACK', | 195 'AREA:downstreamdif#ff8686::STACK', |
185 'LINE1:downstream#ff0000:Downstream') | 196 'LINE1:downstream#ff0000:Downstream', |
197 | |
198 'LINE1:maxupstream#0000ff:Upstream (maximum)', | |
199 'LINE1:maxdownstream#000000:Downstream (maximum)' | |
200 ) | |
186 | 201 |
187 signalargs = ( | 202 signalargs = ( |
188 '-a', 'SVG', | 203 '-a', 'SVG', |
189 '--vertical-label', 'dB', | 204 '--vertical-label', 'dB', |
190 '--slope-mode', | 205 '--slope-mode', |
191 | 206 |
192 'DEF:upstream=%s:upstream:AVERAGE' % rrdname, | 207 'DEF:upstream=%s:upstream:AVERAGE' % rrdname, |
193 'DEF:downstream=%s:downstream:AVERAGE' % rrdname, | 208 'DEF:downstream=%s:downstream:AVERAGE' % rrdname, |
194 | 209 |
195 'DEF:nmup_=%s:nmup:AVERAGE' % rrdname, | 210 'DEF:nmup_=%s:nmup:AVERAGE' % rrdname, |
196 'DEF:nmupmin_=%s:nmup:MIN' % rrdname, | 211 'DEF:nmupmin_=%s:nmup:MIN' % rrdname, |
197 'DEF:nmupmax_=%s:nmup:MAX' % rrdname, | 212 'DEF:nmupmax_=%s:nmup:MAX' % rrdname, |
198 | 213 |
199 'DEF:nmdown_=%s:nmdown:AVERAGE' % rrdname, | 214 'DEF:nmdown_=%s:nmdown:AVERAGE' % rrdname, |
200 'DEF:nmdownmin_=%s:nmdown:MIN' % rrdname, | 215 'DEF:nmdownmin_=%s:nmdown:MIN' % rrdname, |
201 'DEF:nmdownmax_=%s:nmdown:MAX' % rrdname, | 216 'DEF:nmdownmax_=%s:nmdown:MAX' % rrdname, |
202 | 217 |
203 'DEF:attenup=%s:attenup:AVERAGE' % rrdname, | 218 'DEF:attenup=%s:attenup:AVERAGE' % rrdname, |
204 'DEF:attenupmin=%s:attenup:MIN' % rrdname, | 219 'DEF:attenupmin=%s:attenup:MIN' % rrdname, |
205 'DEF:attenupmax=%s:attenup:MAX' % rrdname, | 220 'DEF:attenupmax=%s:attenup:MAX' % rrdname, |
206 | 221 |
207 'DEF:attendown=%s:attendown:AVERAGE' % rrdname, | 222 'DEF:attendown=%s:attendown:AVERAGE' % rrdname, |
208 'DEF:attendownmin=%s:attendown:MIN' % rrdname, | 223 'DEF:attendownmin=%s:attendown:MIN' % rrdname, |
209 'DEF:attendownmax=%s:attendown:MAX' % rrdname, | 224 'DEF:attendownmax=%s:attendown:MAX' % rrdname, |
210 | 225 |
211 'CDEF:nmup=nmup_,10,*', | 226 'CDEF:nmup=nmup_,10,*', |
212 'CDEF:nmupmin=nmupmin_,10,*', | 227 'CDEF:nmupmin=nmupmin_,10,*', |
213 'CDEF:nmupmax=nmupmax_,10,*', | 228 'CDEF:nmupmax=nmupmax_,10,*', |
214 'CDEF:nmupdif=nmupmax,nmupmin,-', | 229 'CDEF:nmupdif=nmupmax,nmupmin,-', |
215 | 230 |
216 'CDEF:nmdown=nmdown_,10,*', | 231 'CDEF:nmdown=nmdown_,10,*', |
217 'CDEF:nmdownmin=nmdownmin_,10,*', | 232 'CDEF:nmdownmin=nmdownmin_,10,*', |
218 'CDEF:nmdownmax=nmdownmax_,10,*', | 233 'CDEF:nmdownmax=nmdownmax_,10,*', |
219 'CDEF:nmdowndif=nmdownmax,nmdownmin,-', | 234 'CDEF:nmdowndif=nmdownmax,nmdownmin,-', |
220 | 235 |
221 'CDEF:attenupdif=attenupmax,attenupmin,-', | 236 'CDEF:attenupdif=attenupmax,attenupmin,-', |
222 | 237 |
223 'CDEF:attendowndif=attendownmax,attendownmin,-', | 238 'CDEF:attendowndif=attendownmax,attendownmin,-', |
224 | 239 |
225 'LINE0:nmupmin#000000:', | 240 'LINE0:nmupmin#000000:', |
226 'AREA:nmupdif#5c5cff::STACK', | 241 'AREA:nmupdif#5c5cff::STACK', |
227 'LINE1:nmup#0000ff:Noise Margin - Up (1/10 dB)', | 242 'LINE1:nmup#0000ff:Noise Margin - Up (1/10 dB)', |
228 | 243 |
229 'LINE0:nmdownmin#000000:', | 244 'LINE0:nmdownmin#000000:', |
230 'AREA:nmdowndif#009a00::STACK', | 245 'AREA:nmdowndif#009a00::STACK', |
231 'LINE1:nmdown#00ff00:Noise Margin - Down (1/10 dB)', | 246 'LINE1:nmdown#00ff00:Noise Margin - Down (1/10 dB)', |
232 | 247 |
233 'LINE0:attenupmin#000000:', | 248 'LINE0:attenupmin#000000:', |
234 'AREA:attenupdif#f98100::STACK', | 249 'AREA:attenupdif#f98100::STACK', |
235 'LINE1:attenup#ff0000:Attenuation - Up', | 250 'LINE1:attenup#ff0000:Attenuation - Up', |
236 | 251 |
237 'LINE0:attendownmin#000000:', | 252 'LINE0:attendownmin#000000:', |
238 'AREA:attendowndif#aaaaaa::STACK', | 253 'AREA:attendowndif#aaaaaa::STACK', |
239 'LINE1:attendown#000000:Attenuation - Down') | 254 'LINE1:attendown#000000:Attenuation - Down') |
240 | 255 |
241 rrdtool.graph("%s-hour-link.svg" % (graphbasename), | 256 rrdtool.graph("%s-hour-link.svg" % (graphbasename), |
242 '--width', '768', | 257 '--width', '768', |
243 '--height', '256', | 258 '--height', '256', |
244 '--start', 'end - 7d', | 259 '--start', 'end - 7d', |
245 '--end', 'now', | 260 '--end', 'now', |
304 down.max 24000 | 319 down.max 24000 |
305 down.min 0''' | 320 down.min 0''' |
306 sys.exit(0) | 321 sys.exit(0) |
307 if options.update or options.munin: | 322 if options.update or options.munin: |
308 stats = getdata() | 323 stats = getdata() |
309 if stats == None: | 324 if options.verbose: |
310 if options.verbose: | 325 if stats == None: |
311 print "Modem is offline" | 326 print "Modem is offline" |
312 | 327 else: |
328 print stats | |
313 if (options.update or options.munin != None) and stats != None: | 329 if (options.update or options.munin != None) and stats != None: |
314 if options.update: | 330 if options.update: |
315 try: | 331 try: |
316 os.stat(rrdname) | 332 os.stat(rrdname) |
317 except OSError, e: | 333 except OSError, e: |