- stop old version of pl_mom before upgrading
[mom.git] / bwmon.py
1 #!/usr/bin/python
2 #
3 # Average bandwidth monitoring script. Run periodically via cron(8) to
4 # enforce a soft limit on daily bandwidth usage for each slice. If a
5 # slice is found to have exceeded its daily bandwidth usage when the
6 # script is run, its instantaneous rate will be capped at the desired
7 # average rate. Thus, in the worst case, a slice will only be able to
8 # send a little more than twice its average daily limit.
9 #
10 # Two separate limits are enforced, one for destinations exempt from
11 # the node bandwidth cap, and the other for all other destinations.
12 #
13 # Mark Huang <mlhuang@cs.princeton.edu>
14 # Andy Bavier <acb@cs.princeton.edu>
15 # Copyright (C) 2004-2006 The Trustees of Princeton University
16 #
17 # $Id: bwmon.py,v 1.2 2006/04/28 20:25:19 mlhuang Exp $
18 #
19
20 import syslog
21 import os
22 import sys
23 import getopt
24 import time
25 import pickle
26
27 import socket
28 import xmlrpclib
29 import bwlimit
30
31 from sets import Set
32
33 # Utility functions
34 from pl_mom import *
35
36 # Constants
37 seconds_per_day = 24 * 60 * 60
38 bits_per_byte = 8
39
40 # Defaults
41 debug = False
42 verbose = 0
43 datafile = "/var/lib/misc/bwmon.dat"
44 nm = None
45
46 default_maxrate = bwlimit.get_bwcap()
47
48 default_maxexemptrate = bwlimit.bwmax
49
50 # 500 Kbit or 5.4 GB per day
51 default_avgrate = 500000
52
53 # 1.5 Mbit or 16.4 GB per day
54 default_avgexemptrate = 1500000
55
56 # Average over 1 day
57 period = 1 * seconds_per_day
58
59 # Message template
60 template = \
61 """
62 The slice %(slice)s has transmitted more than %(bytes)s from
63 %(hostname)s to %(class)s destinations
64 since %(since)s.
65
66 Its maximum %(class)s burst rate will be capped at %(avgrate)s
67 until %(until)s.
68
69 Please reduce the average %(class)s transmission rate
70 of the slice to %(avgrate)s, or %(limit)s per %(period)s.
71
72 """.lstrip()
73
74 footer = \
75 """
76 %(date)s %(hostname)s bwcap %(slice)s
77 """.lstrip()
78
79 class Slice:
80     """
81     Stores the last recorded bandwidth parameters of a slice.
82
83     xid - slice context/VServer ID
84     name - slice name
85     time - beginning of recording period in UNIX seconds
86     bytes - low bandwidth bytes transmitted at the beginning of the recording period
87     exemptbytes - high bandwidth bytes transmitted at the beginning of the recording period
88     avgrate - average low bandwidth rate to enforce over the recording period
89     avgexemptrate - average high bandwidth rate to enforce over the recording period 
90     """
91
92     def __init__(self, xid, name, maxrate, maxexemptrate, bytes, exemptbytes):
93         self.xid = xid
94         self.name = name
95         self.reset(maxrate, maxexemptrate, bytes, exemptbytes)
96
97     def __repr__(self):
98         return self.name
99
100     def reset(self, maxrate, maxexemptrate, bytes, exemptbytes):
101         """
102         Begin a new recording period. Remove caps by restoring limits
103         to their default values.
104         """
105
106         # Reset baseline time
107         self.time = time.time()
108
109         # Reset baseline byte coutns
110         self.bytes = bytes
111         self.exemptbytes = exemptbytes
112
113         # Query Node Manager for max rate overrides
114         (new_maxrate, new_maxexemptrate) = nm.query(self.name, ['nm_net_max_rate', 'nm_net_max_exempt_rate'])
115         if new_maxrate is not None:
116             new_maxrate *= 1000
117         else:
118             new_maxrate = default_maxrate
119         if new_maxexemptrate is not None:
120             new_maxexemptrate *= 1000
121         else:
122             new_maxexemptrate = default_maxexemptrate
123
124         if new_maxrate != maxrate or new_maxexemptrate != maxexemptrate:
125             print "%s reset to %s/%s" % \
126                   (self.name,
127                    bwlimit.format_tc_rate(new_maxrate),
128                    bwlimit.format_tc_rate(new_maxexemptrate))
129             bwlimit.set(xid = self.xid, maxrate = new_maxrate, maxexemptrate = new_maxexemptrate)
130
131     def update(self, maxrate, maxexemptrate, bytes, exemptbytes):
132         """
133         Update byte counts and check if average rates have been
134         exceeded. In the worst case (instantaneous usage of the entire
135         average daily byte limit at the beginning of the recording
136         period), the slice will be immediately capped and will get to
137         send twice the average daily byte limit. In the common case,
138         it will get to send slightly more than the average daily byte
139         limit.
140         """
141
142         # Query Node Manager for max average rate overrides
143         (self.avgrate, self.avgexemptrate) = nm.query(self.name, ['nm_net_avg_rate', 'nm_net_avg_exempt_rate'])
144         if self.avgrate is None:
145             self.avgrate = default_avgrate
146         if self.avgexemptrate is None:
147             self.avgexemptrate = default_avgexemptrate
148
149         # Prepare message parameters from the template
150         message = ""
151         params = {'slice': self.name, 'hostname': socket.gethostname(),
152                   'since': time.asctime(time.gmtime(self.time)) + " GMT",
153                   'until': time.asctime(time.gmtime(self.time + period)) + " GMT",
154                   'date': time.asctime(time.gmtime()) + " GMT",
155                   'period': format_period(period)} 
156
157         bytelimit = self.avgrate * period / bits_per_byte
158         if bytes >= (self.bytes + bytelimit) and \
159            maxrate > self.avgrate:
160             new_maxrate = self.avgrate
161         else:
162             new_maxrate = maxrate
163
164         # Format template parameters for low bandwidth message
165         params['class'] = "low bandwidth"
166         params['bytes'] = format_bytes(bytes - self.bytes)
167         params['maxrate'] = bwlimit.format_tc_rate(maxrate)
168         params['limit'] = format_bytes(bytelimit)
169         params['avgrate'] = bwlimit.format_tc_rate(self.avgrate)
170
171         if verbose:
172             print "%(slice)s %(class)s " \
173                   "%(bytes)s/%(limit)s (%(maxrate)s/%(avgrate)s)" % \
174                   params
175
176         # Cap low bandwidth burst rate
177         if new_maxrate != maxrate:
178             message += template % params
179             print "%(slice)s %(class)s capped at %(avgrate)s (%(bytes)s/%(limit)s)" % params
180
181         exemptbytelimit = self.avgexemptrate * period / bits_per_byte
182         if exemptbytes >= (self.exemptbytes + exemptbytelimit) and \
183            maxexemptrate > self.avgexemptrate:
184             new_maxexemptrate = self.avgexemptrate
185         else:
186             new_maxexemptrate = maxexemptrate
187
188         # Format template parameters for high bandwidth message
189         params['class'] = "high bandwidth"
190         params['bytes'] = format_bytes(exemptbytes - self.exemptbytes)
191         params['maxrate'] = bwlimit.format_tc_rate(maxexemptrate)
192         params['limit'] = format_bytes(exemptbytelimit)
193         params['avgrate'] = bwlimit.format_tc_rate(self.avgexemptrate)
194
195         if verbose:
196             print "%(slice)s %(class)s " \
197                   "%(bytes)s/%(limit)s (%(maxrate)s/%(avgrate)s)" % \
198                   params
199
200         # Cap high bandwidth burst rate
201         if new_maxexemptrate != maxexemptrate:
202             message += template % params
203             print "%(slice)s %(class)s capped at %(avgrate)s (%(bytes)s/%(limit)s)" % params
204
205         # Apply parameters
206         if new_maxrate != maxrate or new_maxexemptrate != maxexemptrate:
207             bwlimit.set(xid = self.xid, maxrate = new_maxrate, maxexemptrate = new_maxexemptrate)
208
209         # Notify slice
210         if message:
211             subject = "pl_mom capped bandwidth of slice %(slice)s on %(hostname)s" % params
212             if debug:
213                 print subject
214                 print message + (footer % params)
215             else:
216                 slicemail(self.name, subject, message + (footer % params))
217
218 def usage():
219     print """
220 Usage: %s [OPTIONS]...
221
222 Options:
223         -d, --debug             Enable debugging (default: %s)
224         -v, --verbose           Increase verbosity level (default: %d)
225         -f, --file=FILE         Data file (default: %s)
226         -s, --slice=SLICE       Constrain monitoring to these slices (default: all)
227         -p, --period=SECONDS    Interval in seconds over which to enforce average byte limits (default: %s)
228         -h, --help              This message
229 """.lstrip() % (sys.argv[0], debug, verbose, datafile, format_period(period))
230
231 def main():
232     # Defaults
233     global debug, verbose, datafile, period, nm
234     # All slices
235     names = []
236
237     try:
238         longopts = ["debug", "verbose", "file=", "slice=", "period=", "help"]
239         (opts, argv) = getopt.getopt(sys.argv[1:], "dvf:s:p:h", longopts)
240     except getopt.GetoptError, err:
241         print "Error: " + err.msg
242         usage()
243         sys.exit(1)
244
245     for (opt, optval) in opts:
246         if opt == "-d" or opt == "--debug":
247             debug = True
248         elif opt == "-v" or opt == "--verbose":
249             verbose += 1
250             bwlimit.verbose = verbose - 1
251         elif opt == "-f" or opt == "--file":
252             datafile = optval
253         elif opt == "-s" or opt == "--slice":
254             names.append(optval)
255         elif opt == "-p" or opt == "--period":
256             period = int(optval)
257         else:
258             usage()
259             sys.exit(0)
260
261     # Check if we are already running
262     writepid("bwmon")
263
264     if not debug:
265         # Redirect stdout and stderr to syslog
266         syslog.openlog("bwmon")
267         sys.stdout = sys.stderr = Logger()
268
269     try:
270         f = open(datafile, "r+")
271         if verbose:
272             print "Loading %s" % datafile
273         (version, slices) = pickle.load(f)
274         f.close()
275         # Check version of data file
276         if version != "$Id: bwmon.py,v 1.2 2006/04/28 20:25:19 mlhuang Exp $":
277             print "Not using old version '%s' data file %s" % (version, datafile)
278             raise Exception
279     except Exception:
280         version = "$Id: bwmon.py,v 1.2 2006/04/28 20:25:19 mlhuang Exp $"
281         slices = {}
282
283     # Get special slice IDs
284     root_xid = bwlimit.get_xid("root")
285     default_xid = bwlimit.get_xid("default")
286
287     # Open connection to Node Manager
288     nm = NM()
289
290     live = []
291     for params in bwlimit.get():
292         (xid, share,
293          minrate, maxrate,
294          minexemptrate, maxexemptrate,
295          bytes, exemptbytes) = params
296         live.append(xid)
297
298         # Ignore root and default buckets
299         if xid == root_xid or xid == default_xid:
300             continue
301
302         name = bwlimit.get_slice(xid)
303         if name is None:
304             # Orphaned (not associated with a slice) class
305             name = "%d?" % xid
306
307         # Monitor only the specified slices
308         if names and name not in names:
309             continue
310
311         if slices.has_key(xid):
312             slice = slices[xid]
313             if time.time() >= (slice.time + period) or \
314                bytes < slice.bytes or exemptbytes < slice.exemptbytes:
315                 # Reset to defaults every 24 hours or if it appears
316                 # that the byte counters have overflowed (or, more
317                 # likely, the node was restarted or the HTB buckets
318                 # were re-initialized).
319                 slice.reset(maxrate, maxexemptrate, bytes, exemptbytes)
320             else:
321                 # Update byte counts
322                 slice.update(maxrate, maxexemptrate, bytes, exemptbytes)
323         else:
324             # New slice, initialize state
325             slice = slices[xid] = Slice(xid, name, maxrate, maxexemptrate, bytes, exemptbytes)
326
327     # Delete dead slices
328     dead = Set(slices.keys()) - Set(live)
329     for xid in dead:
330         del slices[xid]
331
332     if verbose:
333         print "Saving %s" % datafile
334     f = open(datafile, "w")
335     pickle.dump((version, slices), f)
336     f.close()
337
338     removepid("bwmon")
339
340 if __name__ == '__main__':
341     main()