* Fixed floating point arith error. tc likes whole numbers.
[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 # 4 Gbyte per day. 4 * 1024K * 1024M * 1024G 
58 default_ByteThresh = 4294967296
59 # 5.4 Gbyte per day max allowed transfered per recording period
60 default_ByteMax = 5798205850 
61 # 14 Gbyte per day
62 default_ExemptByteThresh = 15032385536
63 # 16.4 Gbyte per day max allowed transfered per recording period to I2
64 default_ExemptByteMax = 17609365914 
65
66
67 # Average over 1 day
68 period = 1 * seconds_per_day
69
70 # Message template
71 template = \
72 """
73 The slice %(slice)s has transmitted more than %(bytes)s from
74 %(hostname)s to %(class)s destinations
75 since %(since)s.
76
77 Its maximum %(class)s burst rate will be capped at %(new_maxrate)s
78 until %(until)s.
79
80 Please reduce the average %(class)s transmission rate
81 of the slice %(limit)s per %(period)s.
82
83 """.lstrip()
84
85 footer = \
86 """
87 %(date)s %(hostname)s bwcap %(slice)s
88 """.lstrip()
89
90 class Slice:
91         """
92         Stores the last recorded bandwidth parameters of a slice.
93
94         xid - slice context/VServer ID
95         name - slice name
96         time - beginning of recording period in UNIX seconds
97         bytes - low bandwidth bytes transmitted at the beginning of the recording period
98         exemptbytes - high bandwidth bytes transmitted at the beginning of the recording period (for I2 -F)
99         ByteMax - total volume of data allowed
100         ByteThresh - After thresh, cap node to (maxbyte - bytes)/(time left in period)
101         ExemptByteMax - Same as above, but for i2.
102         ExemptByteThresh - i2 ByteThresh
103
104     """
105
106         def __init__(self, xid, name, maxrate, maxexemptrate, bytes, exemptbytes):
107                 self.xid = xid
108                 self.name = name
109                 self.time = 0
110                 self.bytes = 0
111                 self.exemptbytes = 0
112                 self.ByteMax = default_ByteMax
113                 self.ByteThresh = default_ByteThresh
114                 self.ExemptByteMax = default_ExemptByteMax
115                 self.ExemptByteThresh = default_ExemptByteThresh
116                 self.maxrate = default_maxrate
117                 self.maxexemptrate = default_maxexemptrate
118
119                 # Get real values where applicable
120                 self.reset(maxrate, maxexemptrate, bytes, exemptbytes)
121
122         def __repr__(self):
123                 return self.name
124
125         def updateSliceAttributes(self):
126                 # Query Node Manager for max rate overrides
127                 try:
128                         vals = nm.query(self.name, 
129                                 [('nm_net_max_rate', self.maxrate),
130                                 ('nm_net_max_exempt_rate', self.maxexemptrate)])
131                         (self.maxrate, self.maxexemptrate) = vals
132
133                 except Exception, err:
134                         print "Warning: Exception received while querying NM:", err
135
136         def reset(self, maxrate, maxexemptrate, bytes, exemptbytes):
137                 """
138                 Begin a new recording period. Remove caps by restoring limits
139                 to their default values.
140                 """
141                 
142                 # Query Node Manager for max rate overrides
143                 self.updateSliceAttributes()    
144
145                 # Reset baseline time
146                 self.time = time.time()
147
148                 # Reset baseline byte coutns
149                 self.bytes = bytes
150                 self.exemptbytes = exemptbytes
151
152                 if (self.maxrate != maxrate) or (self.maxexemptrate != maxexemptrate):
153                         print "%s reset to %s/%s" % \
154                                   (self.name,
155                                    bwlimit.format_tc_rate(self.maxrate),
156                                    bwlimit.format_tc_rate(self.maxexemptrate))
157                         bwlimit.set(xid = self.xid, maxrate = self.maxrate, maxexemptrate = self.maxexemptrate)
158
159         def update(self, maxrate, maxexemptrate, bytes, exemptbytes):
160                 """
161                 Update byte counts and check if byte limits have been
162                 exceeded. 
163                 """
164         
165                 # Query Node Manager for max rate overrides
166                 self.updateSliceAttributes()    
167  
168                 # Prepare message parameters from the template
169                 message = ""
170                 params = {'slice': self.name, 'hostname': socket.gethostname(),
171                                   'since': time.asctime(time.gmtime(self.time)) + " GMT",
172                                   'until': time.asctime(time.gmtime(self.time + period)) + " GMT",
173                                   'date': time.asctime(time.gmtime()) + " GMT",
174                                   'period': format_period(period)} 
175
176                 print("byts %s self.bytes %s ByteThresh %s" %(bytes, self.bytes, self.ByteThresh))
177                 if bytes >= (self.bytes + self.ByteThresh):
178                         new_maxrate = int(self.ByteMax - self.ByteThresh)/(period - (time.time() - self.time))
179                 else:
180                         new_maxrate = maxrate
181
182                 # Format template parameters for low bandwidth message
183                 params['class'] = "low bandwidth"
184                 params['bytes'] = format_bytes(bytes - self.bytes)
185                 params['maxrate'] = bwlimit.format_tc_rate(maxrate)
186                 params['limit'] = format_bytes(self.ByteMax)
187                 params['new_maxrate'] = bwlimit.format_tc_rate(new_maxrate)
188
189                 if verbose:
190                         print "%(slice)s %(class)s " \
191                                   "%(bytes)s, %(limit)s (%(new_maxrate)s maxrate)" % \
192                                   params
193
194                 # Cap low bandwidth burst rate
195                 if new_maxrate != maxrate:
196                         message += template % params
197                         print "%(slice)s %(class)s capped at %(new_maxrate)s " % params
198         
199                 if exemptbytes >= (self.exemptbytes + self.ExemptByteThresh):
200                         new_maxexemptrate = \
201                         int((self.ExemptByteMax - self.ExemptByteThresh)/(period - (time.time() - self.time)))
202                 else:
203                         new_maxexemptrate = maxexemptrate
204
205                 # Format template parameters for high bandwidth message
206                 params['class'] = "high bandwidth"
207                 params['bytes'] = format_bytes(exemptbytes - self.exemptbytes)
208                 params['maxrate'] = bwlimit.format_tc_rate(maxexemptrate)
209                 params['limit'] = format_bytes(self.ExemptByteMax)
210                 params['new_maxrate'] = bwlimit.format_tc_rate(new_maxexemptrate)
211
212                 if verbose:
213                         print "%(slice)s %(class)s " \
214                                   "%(bytes)s, %(limit)s (%(new_maxrate)s avg)" % params
215
216                 # Cap high bandwidth burst rate
217                 if new_maxexemptrate != maxexemptrate:
218                         message += template % params
219                         print "%(slice)s %(class)s capped at %(new_maxexemptrate)s" % params
220
221                 # Apply parameters
222                 if new_maxrate != maxrate or new_maxexemptrate != maxexemptrate:
223                         bwlimit.set(xid = self.xid, maxrate = new_maxrate, maxexemptrate = new_maxexemptrate)
224
225                 # Notify slice
226                 if message:
227                         subject = "pl_mom capped bandwidth of slice %(slice)s on %(hostname)s" % params
228                         if debug:
229                                 print subject
230                                 print message + (footer % params)
231                         else:
232                                 slicemail(self.name, subject, message + (footer % params))
233
234
235
236 def usage():
237         print """
238 Usage: %s [OPTIONS]...
239
240 Options:
241                 -d, --debug                     Enable debugging (default: %s)
242                 -v, --verbose               Increase verbosity level (default: %d)
243                 -f, --file=FILE             Data file (default: %s)
244                 -s, --slice=SLICE           Constrain monitoring to these slices (default: all)
245                 -p, --period=SECONDS    Interval in seconds over which to enforce average byte limits (default: %s)
246                 -h, --help                          This message
247 """.lstrip() % (sys.argv[0], debug, verbose, datafile, format_period(period))
248
249 def main():
250         # Defaults
251         global debug, verbose, datafile, period, nm
252         # All slices
253         names = []
254
255         try:
256                 longopts = ["debug", "verbose", "file=", "slice=", "period=", "help"]
257                 (opts, argv) = getopt.getopt(sys.argv[1:], "dvf:s:p:h", longopts)
258         except getopt.GetoptError, err:
259                 print "Error: " + err.msg
260                 usage()
261                 sys.exit(1)
262
263         for (opt, optval) in opts:
264                 if opt == "-d" or opt == "--debug":
265                         debug = True
266                 elif opt == "-v" or opt == "--verbose":
267                         verbose += 1
268                         bwlimit.verbose = verbose - 1
269                 elif opt == "-f" or opt == "--file":
270                         datafile = optval
271                 elif opt == "-s" or opt == "--slice":
272                         names.append(optval)
273                 elif opt == "-p" or opt == "--period":
274                         period = int(optval)
275                 else:
276                         usage()
277                         sys.exit(0)
278
279         # Check if we are already running
280         writepid("bwmon")
281
282         if not debug:
283                 # Redirect stdout and stderr to syslog
284                 syslog.openlog("bwmon")
285                 sys.stdout = sys.stderr = Logger()
286
287         try:
288                 f = open(datafile, "r+")
289                 if verbose:
290                         print "Loading %s" % datafile
291                 (version, slices) = pickle.load(f)
292                 f.close()
293                 # Check version of data file
294                 if version != "$Id: bwmon.py,v 1.5.2.2 2006/08/21 21:27:35 mlhuang Exp $":
295                         print "Not using old version '%s' data file %s" % (version, datafile)
296                         raise Exception
297         except Exception:
298                 version = "$Id: bwmon.py,v 1.5.2.2 2006/08/21 21:27:35 mlhuang Exp $"
299                 slices = {}
300
301         # Get special slice IDs
302         root_xid = bwlimit.get_xid("root")
303         default_xid = bwlimit.get_xid("default")
304
305         #Open connection to Node Manager. Global.
306         nm = NM()
307
308         live = []
309         # Get actuall running values from tc.
310         for params in bwlimit.get():
311                 (xid, share,
312                  minrate, maxrate,
313                  minexemptrate, maxexemptrate,
314                  bytes, exemptbytes) = params
315                 live.append(xid)
316                 
317                 if verbose:
318                         print("\n%s, minrate %s, maxrate %s, minexemptrate %s,"\
319                                 " maxexemptrate %s, bytes %s, exemptbytes %s" % \
320                                 (bwlimit.get_slice(xid), 
321                                 bwlimit.format_tc_rate(minrate),
322                                 bwlimit.format_tc_rate(maxrate), 
323                                 bwlimit.format_tc_rate(minexemptrate),
324                                 bwlimit.format_tc_rate(maxexemptrate), 
325                                 bytes, 
326                                 exemptbytes))
327
328                 # Ignore root and default buckets
329                 if xid == root_xid or xid == default_xid:
330                         continue
331
332                 name = bwlimit.get_slice(xid)
333                 if name is None:
334                         # Orphaned (not associated with a slice) class
335                         name = "%d?" % xid
336
337                 # Monitor only the specified slices
338                 if names and name not in names:
339                         continue
340                 #slices is populated from the pickle file
341                 #xid is populated from bwlimit (read from /etc/passwd) 
342                 if slices.has_key(xid):
343                         slice = slices[xid]
344                         if time.time() >= (slice.time + period) or \
345                            bytes < slice.bytes or exemptbytes < slice.exemptbytes:
346                                 # Reset to defaults every 24 hours or if it appears
347                                 # that the byte counters have overflowed (or, more
348                                 # likely, the node was restarted or the HTB buckets
349                                 # were re-initialized).
350                                 slice.reset(maxrate, maxexemptrate, bytes, exemptbytes)
351                         else:
352                                 # Update byte counts
353                                 slice.update(maxrate, maxexemptrate, bytes, exemptbytes)
354                 else:
355                         # New slice, initialize state
356                         slice = slices[xid] = Slice(xid, name, maxrate, maxexemptrate, bytes, exemptbytes)
357
358         # Delete dead slices
359         dead = Set(slices.keys()) - Set(live)
360         for xid in dead:
361                 del slices[xid]
362
363         if verbose:
364                 print "Saving %s" % datafile
365         f = open(datafile, "w")
366         pickle.dump((version, slices), f)
367         f.close()
368
369         removepid("bwmon")
370
371 if __name__ == '__main__':
372         main()