* Fixed syntax error with low bw thresholds.
[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 # Faiyaz Ahmed <faiyaza@cs.princeton.edu>
16 # Copyright (C) 2004-2006 The Trustees of Princeton University
17 #
18 # $Id: bwmon.py,v 1.5.2.2 2006/08/21 21:27:35 mlhuang Exp $
19 #
20
21 import syslog
22 import os
23 import sys
24 import getopt
25 import time
26 import pickle
27
28 import socket
29 import xmlrpclib
30 import bwlimit
31
32 from sets import Set
33
34 # Utility functions
35 from pl_mom import *
36
37 # Constants
38 seconds_per_day = 24 * 60 * 60
39 bits_per_byte = 8
40
41 # Defaults
42 debug = False
43 verbose = 0
44 datafile = "/var/lib/misc/bwmon.dat"
45 nm = None
46
47 # Burst to line rate (or node cap).  Set by NM.
48 default_maxrate = bwlimit.get_bwcap()
49 default_maxexemptrate = bwlimit.bwmax
50
51 # What we cap to when slices break the rules.
52 # 500 Kbit or 5.4 GB per day
53 #default_avgrate = 500000
54 # 1.5 Mbit or 16.4 GB per day
55 #default_avgexemptrate = 1500000
56
57 # 5.4 Gbyte per day. 5.4 * 1024 k * 1024M * 1024G 
58 # 5.4 Gbyte per day max allowed transfered per recording period
59 default_ByteMax = 5798205850
60 default_ByteThresh = int(.8 * default_ByteMax) 
61 # 16.4 Gbyte per day max allowed transfered per recording period to I2
62 default_ExemptByteMax = 17609365914 
63 default_ExemptByteThresh = int(.8 * default_ExemptByteMax) 
64
65
66 # Average over 1 day
67 period = 1 * seconds_per_day
68
69 # Message template
70 template = \
71 """
72 The slice %(slice)s has transmitted more than %(bytes)s from
73 %(hostname)s to %(class)s destinations
74 since %(since)s.
75
76 Its maximum %(class)s burst rate will be capped at %(new_maxrate)s
77 until %(until)s.
78
79 Please reduce the average %(class)s transmission rate
80 of the slice %(limit)s per %(period)s.
81
82 """.lstrip()
83
84 footer = \
85 """
86 %(date)s %(hostname)s bwcap %(slice)s
87 """.lstrip()
88
89 class Slice:
90     """
91     Stores the last recorded bandwidth parameters of a slice.
92
93     xid - slice context/VServer ID
94     name - slice name
95     time - beginning of recording period in UNIX seconds
96     bytes - low bandwidth bytes transmitted at the beginning of the recording period
97     exemptbytes - high bandwidth bytes transmitted at the beginning of the recording period (for I2 -F)
98     ByteMax - total volume of data allowed
99     ByteThresh - After thresh, cap node to (maxbyte - bytes)/(time left in period)
100     ExemptByteMax - Same as above, but for i2.
101     ExemptByteThresh - i2 ByteThresh
102     maxrate - max_rate slice attribute. 
103     maxexemptrate - max_exempt_rate slice attribute.
104     self.emailed = did we email during this recording period
105
106     """
107
108     def __init__(self, xid, name, maxrate, maxexemptrate, bytes, exemptbytes):
109         self.xid = xid
110         self.name = name
111         self.time = 0
112         self.bytes = 0
113         self.exemptbytes = 0
114         self.ByteMax = default_ByteMax
115         self.ByteThresh = default_ByteThresh
116         self.ExemptByteMax = default_ExemptByteMax
117         self.ExemptByteThresh = default_ExemptByteThresh
118         self.maxrate = default_maxrate
119         self.maxexemptrate = default_maxexemptrate
120         self.emailed = False
121
122         # Get real values where applicable
123         self.reset(maxrate, maxexemptrate, bytes, exemptbytes)
124
125     def __repr__(self):
126         return self.name
127
128     def updateSliceAttributes(self):
129         # Query Node Manager for max rate overrides
130         try:
131             vals = nm.query(self.name, 
132                 [('nm_net_max_rate', self.maxrate),
133                 ('nm_net_max_exempt_rate', self.maxexemptrate),
134                 ("nm_net_max_byte", int(self.ByteMax / 1024)),
135                 ("nm_net_max_exempt_byte", int(self.ExemptByteMax / 1024)),
136                 ("nm_net_max_thresh_byte", int( .8 * self.ByteMax / 1024)),
137                 ("nm_net_max_thresh_exempt_byte", int(.8 * self.ExemptByteMax / 1024)),
138                 ("nm_net_avg_rate", 0),
139                 ("nm_net_avg_exempt_rate", 0)])
140
141             (self.maxrate,
142                 self.maxexemptrate,
143                 ByteMax,
144                 ExemptByteMax,
145                 ByteThresh,
146                 ExemptByteThresh,
147                 avgrate,
148                 avgexemptrate) = vals
149             
150             # The shitty bit.  Gotta bias the limits so as not to overflow xmlrpc
151             self.ByteMax = ByteMax * 1024 
152             self.ByteThresh = ByteThresh * 1024 
153             self.ExemptByteMax = ExemptByteMax * 1024 
154             self.ExemptByteThresh = ExemptByteThresh * 1024 
155
156             # The hack here is that when i pass 0 to the xmlrpc request to NM, 
157             # for rate limits and it comes back non zero, then screw the byte limits.
158             # Mult by the period and recompute the byte limits.  The thought is
159             # If/when PLC switches to byte limits, the avgrates wont be used as
160             # slice attributes and will return as 0
161             if (avgrate != 0):
162                 self.ByteMax = avgrate * period 
163                 self.ByteThresh = int(self.ByteMax * .8)
164
165             if (avgexemptrate != 0):
166                 self.ExemptByteMax = avgexemptrate * period
167                 self.ExemptByteThresh = int(self.ExemptByteMax * .8)
168
169         except Exception, err:
170             print "Warning: Exception received while querying NM:", err
171
172     def reset(self, maxrate, maxexemptrate, bytes, exemptbytes):
173         """
174         Begin a new recording period. Remove caps by restoring limits
175         to their default values.
176         """
177         
178         # Query Node Manager for max rate overrides
179         self.updateSliceAttributes()    
180
181         # Reset baseline time
182         self.time = time.time()
183
184         # Reset baseline byte coutns
185         self.bytes = bytes
186         self.exemptbytes = exemptbytes
187
188         # Reset email 
189         self.emailed = False
190
191         if (self.maxrate != maxrate) or (self.maxexemptrate != maxexemptrate):
192             print "%s reset to %s/%s" % \
193                   (self.name,
194                    bwlimit.format_tc_rate(self.maxrate),
195                    bwlimit.format_tc_rate(self.maxexemptrate))
196             bwlimit.set(xid = self.xid, maxrate = self.maxrate, maxexemptrate = self.maxexemptrate)
197
198     def update(self, maxrate, maxexemptrate, bytes, exemptbytes):
199         """
200         Update byte counts and check if byte limits have been
201         exceeded. 
202         """
203     
204         # Query Node Manager for max rate overrides
205         self.updateSliceAttributes()    
206      
207         if verbose:
208             print("\n%s slice attributes "\
209                 "maxrate %s, maxexemptrate %s" % \
210                 (self.name, 
211                 bwlimit.format_tc_rate(maxrate), 
212                 bwlimit.format_tc_rate(maxexemptrate)))
213
214         # Prepare message parameters from the template
215         message = ""
216         params = {'slice': self.name, 'hostname': socket.gethostname(),
217                   'since': time.asctime(time.gmtime(self.time)) + " GMT",
218                   'until': time.asctime(time.gmtime(self.time + period)) + " GMT",
219                   'date': time.asctime(time.gmtime()) + " GMT",
220                   'period': format_period(period)} 
221
222         if bytes >= (self.bytes + self.ByteThresh):
223             new_maxrate = \
224             int((self.ByteMax - self.bytes + bytes)/(period - time.time() - self.time))
225         else:
226             new_maxrate = maxrate
227
228         # Format template parameters for low bandwidth message
229         params['class'] = "low bandwidth"
230         params['bytes'] = format_bytes(bytes - self.bytes)
231         params['maxrate'] = bwlimit.format_tc_rate(maxrate)
232         params['limit'] = format_bytes(self.ByteMax)
233         params['new_maxrate'] = bwlimit.format_tc_rate(new_maxrate)
234
235         if verbose:
236             print "%(slice)s %(class)s " \
237                   "%(bytes)s of %(limit)s (%(new_maxrate)s maxrate)" % \
238                   params
239
240         # Cap low bandwidth burst rate
241         if new_maxrate != maxrate:
242             message += template % params
243             print "%(slice)s %(class)s capped at %(new_maxrate)s " % params
244     
245         if exemptbytes >= (self.exemptbytes + self.ExemptByteThresh):
246             new_maxexemptrate = \
247             int((self.ExemptByteMax - (self.bytes + bytes))/(period - (time.time() - self.time)))
248         else:
249             new_maxexemptrate = maxexemptrate
250
251         # Format template parameters for high bandwidth message
252         params['class'] = "high bandwidth"
253         params['bytes'] = format_bytes(exemptbytes - self.exemptbytes)
254         params['maxrate'] = bwlimit.format_tc_rate(maxexemptrate)
255         params['limit'] = format_bytes(self.ExemptByteMax)
256         params['new_maxexemptrate'] = bwlimit.format_tc_rate(new_maxexemptrate)
257
258         if verbose:
259             print "%(slice)s %(class)s " \
260                   "%(bytes)s of %(limit)s (%(new_maxrate)s maxrate)" % params
261
262         # Cap high bandwidth burst rate
263         if new_maxexemptrate != maxexemptrate:
264             message += template % params
265             print "%(slice)s %(class)s capped at %(new_maxexemptrate)s" % params
266
267         # Apply parameters
268         if new_maxrate != maxrate or new_maxexemptrate != maxexemptrate:
269             bwlimit.set(xid = self.xid, maxrate = new_maxrate, maxexemptrate = new_maxexemptrate)
270
271         # Notify slice
272         if message and self.emailed == False:
273             subject = "pl_mom capped bandwidth of slice %(slice)s on %(hostname)s" % params
274             if debug:
275                 print subject
276                 print message + (footer % params)
277             else:
278                 self.emailed = True
279                 slicemail(self.name, subject, message + (footer % params))
280
281
282
283 def usage():
284     print """
285 Usage: %s [OPTIONS]...
286
287 Options:
288         -d, --debug            Enable debugging (default: %s)
289         -v, --verbose            Increase verbosity level (default: %d)
290         -f, --file=FILE            Data file (default: %s)
291         -s, --slice=SLICE        Constrain monitoring to these slices (default: all)
292         -p, --period=SECONDS    Interval in seconds over which to enforce average byte limits (default: %s)
293         -h, --help                This message
294 """.lstrip() % (sys.argv[0], debug, verbose, datafile, format_period(period))
295
296 def main():
297     # Defaults
298     global debug, verbose, datafile, period, nm
299     # All slices
300     names = []
301
302     try:
303         longopts = ["debug", "verbose", "file=", "slice=", "period=", "help"]
304         (opts, argv) = getopt.getopt(sys.argv[1:], "dvf:s:p:h", longopts)
305     except getopt.GetoptError, err:
306         print "Error: " + err.msg
307         usage()
308         sys.exit(1)
309
310     for (opt, optval) in opts:
311         if opt == "-d" or opt == "--debug":
312             debug = True
313         elif opt == "-v" or opt == "--verbose":
314             verbose += 1
315             bwlimit.verbose = verbose - 1
316         elif opt == "-f" or opt == "--file":
317             datafile = optval
318         elif opt == "-s" or opt == "--slice":
319             names.append(optval)
320         elif opt == "-p" or opt == "--period":
321             period = int(optval)
322         else:
323             usage()
324             sys.exit(0)
325
326     # Check if we are already running
327     writepid("bwmon")
328
329     if not debug:
330         # Redirect stdout and stderr to syslog
331         syslog.openlog("bwmon")
332         sys.stdout = sys.stderr = Logger()
333
334     try:
335         f = open(datafile, "r+")
336         if verbose:
337             print "Loading %s" % datafile
338         (version, slices) = pickle.load(f)
339         f.close()
340         # Check version of data file
341         if version != "$Id: bwmon.py,v 1.5.2.2 2006/08/21 21:27:35 mlhuang Exp $":
342             print "Not using old version '%s' data file %s" % (version, datafile)
343             raise Exception
344     except Exception:
345         version = "$Id: bwmon.py,v 1.5.2.2 2006/08/21 21:27:35 mlhuang Exp $"
346         slices = {}
347
348     # Get special slice IDs
349     root_xid = bwlimit.get_xid("root")
350     default_xid = bwlimit.get_xid("default")
351
352     #Open connection to Node Manager. Global.
353     nm = NM()
354
355     live = []
356     # Get actuall running values from tc.
357     for params in bwlimit.get():
358         (xid, share,
359          minrate, maxrate,
360          minexemptrate, maxexemptrate,
361          bytes, exemptbytes) = params
362         live.append(xid)
363
364         # Ignore root and default buckets
365         if xid == root_xid or xid == default_xid:
366             continue
367
368         name = bwlimit.get_slice(xid)
369         if name is None:
370             # Orphaned (not associated with a slice) class
371             name = "%d?" % xid
372
373         # Monitor only the specified slices
374         if names and name not in names:
375             continue
376         #slices is populated from the pickle file
377         #xid is populated from bwlimit (read from /etc/passwd) 
378         if slices.has_key(xid):
379             slice = slices[xid]
380             if time.time() >= (slice.time + period) or \
381                bytes < slice.bytes or exemptbytes < slice.exemptbytes:
382                 # Reset to defaults every 24 hours or if it appears
383                 # that the byte counters have overflowed (or, more
384                 # likely, the node was restarted or the HTB buckets
385                 # were re-initialized).
386                 slice.reset(maxrate, maxexemptrate, bytes, exemptbytes)
387             else:
388                 # Update byte counts
389                 slice.update(maxrate, maxexemptrate, bytes, exemptbytes)
390         else:
391             # New slice, initialize state
392             slice = slices[xid] = Slice(xid, name, maxrate, maxexemptrate, bytes, exemptbytes)
393
394     # Delete dead slices
395     dead = Set(slices.keys()) - Set(live)
396     for xid in dead:
397         del slices[xid]
398
399     if verbose:
400         print "Saving %s" % datafile
401     f = open(datafile, "w")
402     pickle.dump((version, slices), f)
403     f.close()
404
405     removepid("bwmon")
406
407 if __name__ == '__main__':
408     main()