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: