* Fixed output of -s -v and -d flags.
[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
105     """
106
107         def __init__(self, xid, name, maxrate, maxexemptrate, bytes, exemptbytes):
108                 self.xid = xid
109                 self.name = name
110                 self.time = 0
111                 self.bytes = 0
112                 self.exemptbytes = 0
113                 self.ByteMax = default_ByteMax
114                 self.ByteThresh = default_ByteThresh
115                 self.ExemptByteMax = default_ExemptByteMax
116                 self.ExemptByteThresh = default_ExemptByteThresh
117                 self.maxrate = default_maxrate
118                 self.maxexemptrate = default_maxexemptrate
119
120                 # Get real values where applicable
121                 self.reset(maxrate, maxexemptrate, bytes, exemptbytes)
122
123         def __repr__(self):
124                 return self.name
125
126         def updateSliceAttributes(self):
127                 # Query Node Manager for max rate overrides
128                 try:
129                         vals = nm.query(self.name, 
130                                 [('nm_net_max_rate', self.maxrate),
131                                 ('nm_net_max_exempt_rate', self.maxexemptrate),
132                                 ("nm_net_max_byte", int(self.ByteMax / 1024)),
133                                 ("nm_net_max_exempt_byte", int(self.ExemptByteMax / 1024)),
134                                 ("nm_net_max_thresh_byte", int( .8 * self.ByteMax / 1024)),
135                                 ("nm_net_max_thresh_exempt_byte", int(.8 * self.ExemptByteMax / 1024)),
136                                 ("nm_net_avg_rate", 0),
137                                 ("nm_net_avg_exempt_rate", 0)])
138
139                         (self.maxrate,
140                                 self.maxexemptrate,
141                                 ByteMax,
142                                 ExemptByteMax,
143                                 ByteThresh,
144                                 ExemptByteThresh,
145                                 avgrate,
146                                 avgexemptrate) = vals
147                         
148                         # The shitty bit.  Gotta bias the limits so as not to overflow xmlrpc
149                         self.ByteMax = ByteMax * 1024 
150                         self.ByteThresh = ByteMax * 1024 
151                         self.ExemptByteMax = ExemptByteMax * 1024 
152                         self.ExemptByteThresh = ExemptByteThresh * 1024 
153
154                         # The hack here is that when i pass 0 to the xmlrpc request to NM, 
155                         # for rate limits and it comes back non zero, then screw the byte limits.
156                         # Mult by the period and recompute the byte limits.  The thought is
157                         # If/when PLC switches to byte limits, the avgrates wont be used as
158                         # slice attributes and will return as 0
159                         if (avgrate != 0):
160                                 self.ByteMax = avgrate * period 
161                                 self.ByteThresh = int(self.ByteMax * .8)
162
163                         if (avgexemptrate != 0):
164                                 self.ExemptByteMax = avgexemptrate * period
165                                 self.ExemptByteThresh = int(self.ExemptByteMax * .8)
166
167                 except Exception, err:
168                         print "Warning: Exception received while querying NM:", err
169
170         def reset(self, maxrate, maxexemptrate, bytes, exemptbytes):
171                 """
172                 Begin a new recording period. Remove caps by restoring limits
173                 to their default values.
174                 """
175                 
176                 # Query Node Manager for max rate overrides
177                 self.updateSliceAttributes()    
178
179                 # Reset baseline time
180                 self.time = time.time()
181
182                 # Reset baseline byte coutns
183                 self.bytes = bytes
184                 self.exemptbytes = exemptbytes
185
186                 if (self.maxrate != maxrate) or (self.maxexemptrate != maxexemptrate):
187                         print "%s reset to %s/%s" % \
188                                   (self.name,
189                                    #bwlimit.format_tc_rate(self.maxrate),
190                                    self.maxrate,
191                                    #bwlimit.format_tc_rate(self.maxexemptrate))
192                                    self.maxexemptrate)
193                         bwlimit.set(xid = self.xid, maxrate = self.maxrate, maxexemptrate = self.maxexemptrate)
194
195         def update(self, maxrate, maxexemptrate, bytes, exemptbytes):
196                 """
197                 Update byte counts and check if byte limits have been
198                 exceeded. 
199                 """
200         
201                 # Query Node Manager for max rate overrides
202                 self.updateSliceAttributes()    
203         
204                 if verbose:
205                         print("\n%s slice attributes "\
206                             "maxrate %s, maxexemptrate %s" % \
207                                 (self.name, 
208                                 bwlimit.format_tc_rate(maxrate), 
209                                 bwlimit.format_tc_rate(maxexemptrate)))
210
211                 # Prepare message parameters from the template
212                 message = ""
213                 params = {'slice': self.name, 'hostname': socket.gethostname(),
214                                   'since': time.asctime(time.gmtime(self.time)) + " GMT",
215                                   'until': time.asctime(time.gmtime(self.time + period)) + " GMT",
216                                   'date': time.asctime(time.gmtime()) + " GMT",
217                                   'period': format_period(period)} 
218
219                 if bytes >= (self.bytes + self.ByteThresh):
220                         new_maxrate = \
221                         int((self.ByteMax - self.bytes + bytes)/(period - time.time() - self.time))
222                 else:
223                         new_maxrate = maxrate
224
225                 # Format template parameters for low bandwidth message
226                 params['class'] = "low bandwidth"
227                 params['bytes'] = format_bytes(bytes - self.bytes)
228                 params['maxrate'] = bwlimit.format_tc_rate(maxrate)
229                 params['limit'] = format_bytes(self.ByteMax)
230                 params['new_maxrate'] = bwlimit.format_tc_rate(new_maxrate)
231
232                 if verbose:
233                         print "%(slice)s %(class)s " \
234                                   "%(bytes)s of %(limit)s (%(new_maxrate)s maxrate)" % \
235                                   params
236
237                 # Cap low bandwidth burst rate
238                 if new_maxrate != maxrate:
239                         message += template % params
240                         print "%(slice)s %(class)s capped at %(new_maxrate)s " % params
241         
242                 if exemptbytes >= (self.exemptbytes + self.ExemptByteThresh):
243                         new_maxexemptrate = \
244                         int((self.ExemptByteMax - (self.bytes + bytes))/(period - (time.time() - self.time)))
245                 else:
246                         new_maxexemptrate = maxexemptrate
247
248                 # Format template parameters for high bandwidth message
249                 params['class'] = "high bandwidth"
250                 params['bytes'] = format_bytes(exemptbytes - self.exemptbytes)
251                 params['maxrate'] = bwlimit.format_tc_rate(maxexemptrate)
252                 params['limit'] = format_bytes(self.ExemptByteMax)
253                 params['new_maxexemptrate'] = bwlimit.format_tc_rate(new_maxexemptrate)
254
255                 if verbose:
256                         print "%(slice)s %(class)s " \
257                                   "%(bytes)s of %(limit)s (%(new_maxrate)s maxrate)" % params
258
259                 # Cap high bandwidth burst rate
260                 if new_maxexemptrate != maxexemptrate:
261                         message += template % params
262                         print "%(slice)s %(class)s capped at %(new_maxexemptrate)s" % params
263
264                 # Apply parameters
265                 if new_maxrate != maxrate or new_maxexemptrate != maxexemptrate:
266                         bwlimit.set(xid = self.xid, maxrate = new_maxrate, maxexemptrate = new_maxexemptrate)
267
268                 # Notify slice
269                 #if message:
270                 #       subject = "pl_mom capped bandwidth of slice %(slice)s on %(hostname)s" % params
271                 #       if debug:
272                 #               print subject
273                 #               print message + (footer % params)
274                 #       else:
275                 #               slicemail(self.name, subject, message + (footer % params))
276
277
278
279 def usage():
280         print """
281 Usage: %s [OPTIONS]...
282
283 Options:
284                 -d, --debug                     Enable debugging (default: %s)
285                 -v, --verbose               Increase verbosity level (default: %d)
286                 -f, --file=FILE             Data file (default: %s)
287                 -s, --slice=SLICE           Constrain monitoring to these slices (default: all)
288                 -p, --period=SECONDS    Interval in seconds over which to enforce average byte limits (default: %s)
289                 -h, --help                          This message
290 """.lstrip() % (sys.argv[0], debug, verbose, datafile, format_period(period))
291
292 def main():
293         # Defaults
294         global debug, verbose, datafile, period, nm
295         # All slices
296         names = []
297
298         try:
299                 longopts = ["debug", "verbose", "file=", "slice=", "period=", "help"]
300                 (opts, argv) = getopt.getopt(sys.argv[1:], "dvf:s:p:h", longopts)
301         except getopt.GetoptError, err:
302                 print "Error: " + err.msg
303                 usage()
304                 sys.exit(1)
305
306         for (opt, optval) in opts:
307                 if opt == "-d" or opt == "--debug":
308                         debug = True
309                 elif opt == "-v" or opt == "--verbose":
310                         verbose += 1
311                         bwlimit.verbose = verbose - 1
312                 elif opt == "-f" or opt == "--file":
313                         datafile = optval
314                 elif opt == "-s" or opt == "--slice":
315                         names.append(optval)
316                 elif opt == "-p" or opt == "--period":
317                         period = int(optval)
318                 else:
319                         usage()
320                         sys.exit(0)
321
322         # Check if we are already running
323         writepid("bwmon")
324
325         if not debug:
326                 # Redirect stdout and stderr to syslog
327                 syslog.openlog("bwmon")
328                 sys.stdout = sys.stderr = Logger()
329
330         try:
331                 f = open(datafile, "r+")
332                 if verbose:
333                         print "Loading %s" % datafile
334                 (version, slices) = pickle.load(f)
335                 f.close()
336                 # Check version of data file
337                 if version != "$Id: bwmon.py,v 1.5.2.2 2006/08/21 21:27:35 mlhuang Exp $":
338                         print "Not using old version '%s' data file %s" % (version, datafile)
339                         raise Exception
340         except Exception:
341                 version = "$Id: bwmon.py,v 1.5.2.2 2006/08/21 21:27:35 mlhuang Exp $"
342                 slices = {}
343
344         # Get special slice IDs
345         root_xid = bwlimit.get_xid("root")
346         default_xid = bwlimit.get_xid("default")
347
348         #Open connection to Node Manager. Global.
349         nm = NM()
350
351         live = []
352         # Get actuall running values from tc.
353         for params in bwlimit.get():
354                 (xid, share,
355                  minrate, maxrate,
356                  minexemptrate, maxexemptrate,
357                  bytes, exemptbytes) = params
358                 live.append(xid)
359
360                 # Ignore root and default buckets
361                 if xid == root_xid or xid == default_xid:
362                         continue
363
364                 name = bwlimit.get_slice(xid)
365                 if name is None:
366                         # Orphaned (not associated with a slice) class
367                         name = "%d?" % xid
368
369                 # Monitor only the specified slices
370                 if names and name not in names:
371                         continue
372                 #slices is populated from the pickle file
373                 #xid is populated from bwlimit (read from /etc/passwd) 
374                 if slices.has_key(xid):
375                         slice = slices[xid]
376                         if time.time() >= (slice.time + period) or \
377                            bytes < slice.bytes or exemptbytes < slice.exemptbytes:
378                                 # Reset to defaults every 24 hours or if it appears
379                                 # that the byte counters have overflowed (or, more
380                                 # likely, the node was restarted or the HTB buckets
381                                 # were re-initialized).
382                                 slice.reset(maxrate, maxexemptrate, bytes, exemptbytes)
383                         else:
384                                 # Update byte counts
385                                 slice.update(maxrate, maxexemptrate, bytes, exemptbytes)
386                 else:
387                         # New slice, initialize state
388                         slice = slices[xid] = Slice(xid, name, maxrate, maxexemptrate, bytes, exemptbytes)
389
390         # Delete dead slices
391         dead = Set(slices.keys()) - Set(live)
392         for xid in dead:
393                 del slices[xid]
394
395         if verbose:
396                 print "Saving %s" % datafile
397         f = open(datafile, "w")
398         pickle.dump((version, slices), f)
399         f.close()
400
401         removepid("bwmon")
402
403 if __name__ == '__main__':
404         main()