Mercurial > ~darius > hgwebdir.cgi > adslstats
annotate speedcheck.py @ 40:947fa4062b01 default tip
Don't blow up if ping fails.
author | Daniel O'Connor <darius@dons.net.au> |
---|---|
date | Tue, 29 Nov 2022 11:53:06 +1030 |
parents | 1a87c79cf103 |
children |
rev | line source |
---|---|
35
815e6b61d76e
Decode to string to work with Python 3.
Daniel O'Connor <darius@dons.net.au>
parents:
32
diff
changeset
|
1 #!/usr/bin/env python3 |
28 | 2 |
32
1af6865189ce
Update to work with Python 3.
Daniel O'Connor <darius@dons.net.au>
parents:
29
diff
changeset
|
3 import configparser |
28 | 4 import optparse |
5 import os | |
6 import re | |
7 import rrdtool | |
8 import subprocess | |
9 import time | |
10 | |
11 def main(): | |
32
1af6865189ce
Update to work with Python 3.
Daniel O'Connor <darius@dons.net.au>
parents:
29
diff
changeset
|
12 conf = configparser.ConfigParser() |
28 | 13 |
14 conflist = [] | |
15 if ('HOME' in os.environ): | |
16 conflist.append(os.path.expanduser('~/.speedcheck.ini')) | |
17 conf.read(conflist) | |
18 | |
19 usage = '''%prog [options]''' | |
20 parser = optparse.OptionParser(usage) | |
21 parser.add_option('-v', '--verbose', action="store_true", default=False, | |
22 help="Enable debug output") | |
23 parser.add_option('-r', '--rrd', action="store", help="Path to RRD") | |
24 parser.add_option('-g', '--graphdir', action="store", help="Directory for graphs") | |
25 | |
26 (opts, args) = parser.parse_args() | |
27 if opts.rrd == None: | |
28 if conf.has_option('global', 'rrd'): | |
29 opts.rrd = conf.get('global', 'rrd') | |
30 else: | |
31 parser.error('Path to RRD must be specified in either the ini or on the command line') | |
32 | |
33 if opts.graphdir == None: | |
34 if conf.has_option('global', 'graphdir'): | |
35 opts.graphdir = conf.get('global', 'graphdir') | |
36 else: | |
37 parser.error('Graph directory must be specified in either the ini or on the command line') | |
38 | |
39 if opts.verbose: | |
32
1af6865189ce
Update to work with Python 3.
Daniel O'Connor <darius@dons.net.au>
parents:
29
diff
changeset
|
40 print('Fetching stats...') |
28 | 41 stats = fetchstats(conf) |
42 if opts.verbose: | |
32
1af6865189ce
Update to work with Python 3.
Daniel O'Connor <darius@dons.net.au>
parents:
29
diff
changeset
|
43 print(stats) |
28 | 44 if opts.verbose: |
32
1af6865189ce
Update to work with Python 3.
Daniel O'Connor <darius@dons.net.au>
parents:
29
diff
changeset
|
45 print('Updating RRD') |
28 | 46 updaterrd(opts.rrd, stats) |
47 if opts.verbose: | |
32
1af6865189ce
Update to work with Python 3.
Daniel O'Connor <darius@dons.net.au>
parents:
29
diff
changeset
|
48 print('Updating graph') |
28 | 49 graphrrd(opts.rrd, opts.graphdir) |
50 | |
51 def fetchstats(conf): | |
52 stats = {} | |
53 if conf.has_option('global', 'neardl'): | |
54 stats['neardl'] = testdl(conf.get('global', 'neardl')) | |
55 if conf.has_option('global', 'nearul'): | |
56 stats['nearul'] = testul(conf.get('global', 'nearul')) | |
57 if conf.has_option('global', 'nearping'): | |
58 stats['nearpl'], stats['nearlat'] = testping(conf.get('global', 'nearping')) | |
59 if conf.has_option('global', 'fardl'): | |
60 stats['fardl'] = testdl(conf.get('global', 'fardl')) | |
61 if conf.has_option('global', 'farul'): | |
62 stats['farul'] = testul(conf.get('global', 'farul')) | |
63 if conf.has_option('global', 'farping'): | |
64 stats['farpl'], stats['farlat'] = testping(conf.get('global', 'farping')) | |
65 | |
66 return stats | |
67 | |
68 def testdl(url): | |
69 p = subprocess.Popen(['curl', '-w', '%{speed_download}', '-so', '/dev/null', url], stdout = subprocess.PIPE) | |
70 speed, xxx = p.communicate() | |
71 if p.returncode != 0: | |
32
1af6865189ce
Update to work with Python 3.
Daniel O'Connor <darius@dons.net.au>
parents:
29
diff
changeset
|
72 print('Error %d fetching \'%s\'' % (p.returncode, url)) |
28 | 73 return None |
74 return float(speed) * 8.0 / 1024.0 # convert to kbit/sec | |
75 | |
76 def testping(host): | |
77 p = subprocess.Popen(['ping', '-c', '5', '-t', '8', '-q', host], stdout = subprocess.PIPE) | |
78 stdout, stderr = p.communicate() | |
35
815e6b61d76e
Decode to string to work with Python 3.
Daniel O'Connor <darius@dons.net.au>
parents:
32
diff
changeset
|
79 l = stdout.decode('ascii', 'ignore').split('\n') |
28 | 80 if len(l) != 6: |
32
1af6865189ce
Update to work with Python 3.
Daniel O'Connor <darius@dons.net.au>
parents:
29
diff
changeset
|
81 print('Unable to parse ping line:', l) |
40
947fa4062b01
Don't blow up if ping fails.
Daniel O'Connor <darius@dons.net.au>
parents:
39
diff
changeset
|
82 return float('nan'), float('nan') |
28 | 83 xx, xx, xx, plossline, latline, xx = l |
39
1a87c79cf103
Handle new ping which does IPv6 (and spells stddev as std-dev!)
Daniel O'Connor <darius@dons.net.au>
parents:
35
diff
changeset
|
84 ploss = re.match('.* received, ([0-9.]+)% packet loss', plossline) |
1a87c79cf103
Handle new ping which does IPv6 (and spells stddev as std-dev!)
Daniel O'Connor <darius@dons.net.au>
parents:
35
diff
changeset
|
85 if ploss is not None: |
1a87c79cf103
Handle new ping which does IPv6 (and spells stddev as std-dev!)
Daniel O'Connor <darius@dons.net.au>
parents:
35
diff
changeset
|
86 ploss = float(ploss.groups()[0]) |
1a87c79cf103
Handle new ping which does IPv6 (and spells stddev as std-dev!)
Daniel O'Connor <darius@dons.net.au>
parents:
35
diff
changeset
|
87 latency = re.match('.*std-?dev = [0-9.]+/([0-9.]+)/.* ms', latline) |
1a87c79cf103
Handle new ping which does IPv6 (and spells stddev as std-dev!)
Daniel O'Connor <darius@dons.net.au>
parents:
35
diff
changeset
|
88 if latency is not None: |
1a87c79cf103
Handle new ping which does IPv6 (and spells stddev as std-dev!)
Daniel O'Connor <darius@dons.net.au>
parents:
35
diff
changeset
|
89 latency = float(latency.groups()[0]) |
28 | 90 return ploss, latency |
91 | |
92 def createrrd(rrdname): | |
93 # Create RRD for upstream/downstream speed, packet loss and | |
94 # latency for near and far site | |
95 # Do a test every half and hour | |
96 # Average 2 for hourly stats (keep 168 - a weeks worth) | |
97 # Average 48 for hourly stats (keep 1825 - 5 years worth) | |
98 # Detemine minimum & maximum for an hour and keep a weeks worth. | |
99 rrdtool.create(rrdname, | |
100 '--step', '300', | |
101 'DS:neardl:GAUGE:3600:0:U', | |
102 'DS:nearul:GAUGE:3600:0:U', | |
103 'DS:nearpl:GAUGE:3600:0:100', | |
104 'DS:nearlat:GAUGE:3600:0:U', | |
105 'DS:fardl:GAUGE:3600:0:U', | |
106 'DS:farul:GAUGE:3600:0:U', | |
107 'DS:farpl:GAUGE:3600:0:100', | |
108 'DS:farlat:GAUGE:3600:0:U', | |
109 'RRA:AVERAGE:0.1:2:168', | |
110 'RRA:AVERAGE:0.1:48:1825', | |
111 'RRA:MIN:0.1:2:168', | |
112 'RRA:MAX:0.1:2:168', | |
113 ) | |
114 | |
115 def updaterrd(rrdname, stats): | |
116 try: | |
117 os.stat(rrdname) | |
32
1af6865189ce
Update to work with Python 3.
Daniel O'Connor <darius@dons.net.au>
parents:
29
diff
changeset
|
118 except OSError as e: |
28 | 119 if e.errno == 2: |
32
1af6865189ce
Update to work with Python 3.
Daniel O'Connor <darius@dons.net.au>
parents:
29
diff
changeset
|
120 print('Creating RRD...') |
28 | 121 createrrd(rrdname) |
122 s = '%d:' % (int(time.time())) | |
123 for a in ['neardl', 'nearul', 'nearpl', 'nearlat', 'fardl', 'farul', 'farpl', 'farlat']: | |
124 if a in stats: | |
125 s += '%f:' % (stats[a]) | |
126 else: | |
127 s += 'U:' | |
128 s = s[0:-1] | |
129 rrdtool.update(rrdname, s) | |
130 | |
131 def graphrrd(rrdname, graphdir): | |
132 latencyargs = ( | |
133 '-a', 'SVG', | |
134 '--vertical-label', 'milliseconds', | |
135 | |
136 'DEF:nearlat=%s:nearlat:AVERAGE' % rrdname, | |
137 'DEF:nearlatmin=%s:nearlat:MIN' % rrdname, | |
138 'DEF:nearlatmax=%s:nearlat:MAX' % rrdname, | |
139 'CDEF:nearlatdif=nearlatmax,nearlatmin,-', | |
140 | |
141 'LINE0.001:nearlatmin#000000:', | |
142 'AREA:nearlatdif#00dc76::STACK', | |
143 'LINE1:nearlatmax#00ff00:Near latency', | |
144 | |
145 'DEF:farlat=%s:farlat:AVERAGE' % rrdname, | |
146 'DEF:farlatmin=%s:farlat:MIN' % rrdname, | |
147 'DEF:farlatmax=%s:farlat:MAX' % rrdname, | |
148 'CDEF:farlatdif=farlatmax,farlatmin,-', | |
149 | |
150 'LINE0.001:farlatmin#000000:', | |
151 'AREA:farlatdif#dc0076::STACK', | |
152 'LINE1:farlatmax#ff0000:Far packetloss', | |
153 | |
154 'DEF:nearpl=%s:nearpl:AVERAGE' % rrdname, | |
155 'LINE1:nearpl#0000ff:Near packet loss (%)', | |
156 | |
157 'DEF:farpl=%s:farpl:AVERAGE' % rrdname, | |
158 'LINE1:nearpl#ffff00:Far packet loss (%)', | |
159 ) | |
160 rrdtool.graph('%s/latency-hour-link.svg' % (graphdir), | |
161 '--width', '768', | |
162 '--height', '256', | |
163 '--start', 'end - 7d', | |
164 '--end', 'now', | |
165 *latencyargs) | |
166 rrdtool.graph('%s/latency-daily-link.svg' % (graphdir), | |
167 '--width', '768', | |
168 '--height', '256', | |
169 '--start', 'end - 365d', | |
170 '--end', 'now', | |
171 *latencyargs) | |
172 | |
173 bwargs = ( | |
174 '-a', 'SVG', | |
175 '-X', '0', | |
176 '--vertical-label', 'kbit/sec', | |
177 | |
178 'DEF:neardl=%s:neardl:AVERAGE' % rrdname, | |
179 'DEF:neardlmin=%s:neardl:MIN' % rrdname, | |
180 'DEF:neardlmax=%s:neardl:MAX' % rrdname, | |
181 'CDEF:neardldif=neardlmax,neardlmin,-', | |
182 | |
183 'LINE0.001:neardlmin#000000:', | |
184 'AREA:neardldif#00dc76::STACK', | |
185 'LINE1:neardlmax#00ff00:Near download', | |
186 | |
187 'DEF:fardl=%s:fardl:AVERAGE' % rrdname, | |
188 'DEF:fardlmin=%s:fardl:MIN' % rrdname, | |
189 'DEF:fardlmax=%s:fardl:MAX' % rrdname, | |
190 'CDEF:fardldif=fardlmax,fardlmin,-', | |
191 | |
192 'LINE0.001:fardlmin#000000:', | |
193 'AREA:fardldif#dc0076::STACK', | |
194 'LINE1:fardlmax#ff0000:Far download', | |
195 ) | |
196 rrdtool.graph('%s/bw-hour-link.svg' % (graphdir), | |
197 '--width', '768', | |
198 '--height', '256', | |
199 '--start', 'end - 1d', | |
200 '--end', 'now', | |
201 *bwargs) | |
202 rrdtool.graph('%s/bw-daily-link.svg' % (graphdir), | |
203 '--width', '768', | |
204 '--height', '256', | |
205 '--start', 'end - 7d', | |
206 '--end', 'now', | |
207 *bwargs) | |
208 | |
209 if __name__ == '__main__': | |
210 main() |