- nm_net parameters are now in bps
[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.4 2006/06/02 04:00:00 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', -1), ('nm_net_max_exempt_rate', -1)])
115         if new_maxrate == -1:
116             new_maxrate = default_maxrate
117         if new_maxexemptrate == -1:
118             new_maxexemptrate = default_maxexemptrate
119
120         if new_maxrate != maxrate or new_maxexemptrate != maxexemptrate:
121             print "%s reset to %s/%s" % \
122                   (self.name,
123                    bwlimit.format_tc_rate(new_maxrate),
124                    bwlimit.format_tc_rate(new_maxexemptrate))
125             bwlimit.set(xid = self.xid, maxrate = new_maxrate, maxexemptrate = new_maxexemptrate)
126
127     def update(self, maxrate, maxexemptrate, bytes, exemptbytes):
128         """
129         Update byte counts and check if average rates have been
130         exceeded. In the worst case (instantaneous usage of the entire
131         average daily byte limit at the beginning of the recording
132         period), the slice will be immediately capped and will get to
133         send twice the average daily byte limit. In the common case,
134         it will get to send slightly more than the average daily byte
135         limit.
136         """
137
138         # Query Node Manager for max average rate overrides
139         (self.avgrate, self.avgexemptrate) = nm.query(self.name, [('nm_net_avg_rate', -1), ('nm_net_avg_exempt_rate', -1)])
140         if self.avgrate == -1:
141             self.avgrate = default_avgrate
142         if self.avgexemptrate == -1:
143             self.avgexemptrate = default_avgexemptrate
144
145         # Prepare message parameters from the template
146         message = ""
147         params = {'slice': self.name, 'hostname': socket.gethostname(),
148                   'since': time.asctime(time.gmtime(self.time)) + " GMT",
149                   'until': time.asctime(time.gmtime(self.time + period)) + " GMT",
150                   'date': time.asctime(time.gmtime()) + " GMT",
151                   'period': format_period(period)} 
152
153         bytelimit = self.avgrate * period / bits_per_byte
154         if bytes >= (self.bytes + bytelimit) and \
155            maxrate > self.avgrate:
156             new_maxrate = self.avgrate
157         else:
158             new_maxrate = maxrate
159
160         # Format template parameters for low bandwidth message
161         params['class'] = "low bandwidth"
162         params['bytes'] = format_bytes(bytes - self.bytes)
163         params['maxrate'] = bwlimit.format_tc_rate(maxrate)
164         params['limit'] = format_bytes(bytelimit)
165         params['avgrate'] = bwlimit.format_tc_rate(self.avgrate)
166
167         if verbose:
168             print "%(slice)s %(class)s " \
169                   "%(bytes)s/%(limit)s (%(maxrate)s/%(avgrate)s)" % \
170                   params
171
172         # Cap low bandwidth burst rate
173         if new_maxrate != maxrate:
174             message += template % params
175             print "%(slice)s %(class)s capped at %(avgrate)s (%(bytes)s/%(limit)s)" % params
176
177         exemptbytelimit = self.avgexemptrate * period / bits_per_byte
178         if exemptbytes >= (self.exemptbytes + exemptbytelimit) and \
179            maxexemptrate > self.avgexemptrate:
180             new_maxexemptrate = self.avgexemptrate
181         else:
182             new_maxexemptrate = maxexemptrate
183
184         # Format template parameters for high bandwidth message
185         params['class'] = "high bandwidth"
186         params['bytes'] = format_bytes(exemptbytes - self.exemptbytes)
187         params['maxrate'] = bwlimit.format_tc_rate(maxexemptrate)
188         params['limit'] = format_bytes(exemptbytelimit)
189         params['avgrate'] = bwlimit.format_tc_rate(self.avgexemptrate)
190
191         if verbose:
192             print "%(slice)s %(class)s " \
193                   "%(bytes)s/%(limit)s (%(maxrate)s/%(avgrate)s)" % \
194                   params
195
196         # Cap high bandwidth burst rate
197         if new_maxexemptrate != maxexemptrate:
198             message += template % params
199             print "%(slice)s %(class)s capped at %(avgrate)s (%(bytes)s/%(limit)s)" % params
200
201         # Apply parameters
202         if new_maxrate != maxrate or new_maxexemptrate != maxexemptrate:
203             bwlimit.set(xid = self.xid, maxrate = new_maxrate, maxexemptrate = new_maxexemptrate)
204
205         # Notify slice
206         if message:
207             subject = "pl_mom capped bandwidth of slice %(slice)s on %(hostname)s" % params
208             if debug:
209                 print subject
210                 print message + (footer % params)
211             else:
212                 slicemail(self.name, subject, message + (footer % params))
213
214 def usage():
215     print """
216 Usage: %s [OPTIONS]...
217
218 Options:
219         -d, --debug             Enable debugging (default: %s)
220         -v, --verbose           Increase verbosity level (default: %d)
221         -f, --file=FILE         Data file (default: %s)
222         -s, --slice=SLICE       Constrain monitoring to these slices (default: all)
223         -p, --period=SECONDS    Interval in seconds over which to enforce average byte limits (default: %s)
224         -h, --help              This message
225 """.lstrip() % (sys.argv[0], debug, verbose, datafile, format_period(period))
226
227 def main():
228     # Defaults
229     global debug, verbose, datafile, period, nm
230     # All slices
231     names = []
232
233     try:
234         longopts = ["debug", "verbose", "file=", "slice=", "period=", "help"]
235         (opts, argv) = getopt.getopt(sys.argv[1:], "dvf:s:p:h", longopts)
236     except getopt.GetoptError, err:
237         print "Error: " + err.msg
238         usage()
239         sys.exit(1)
240
241     for (opt, optval) in opts:
242         if opt == "-d" or opt == "--debug":
243             debug = True
244         elif opt == "-v" or opt == "--verbose":
245             verbose += 1
246             bwlimit.verbose = verbose - 1
247         elif opt == "-f" or opt == "--file":
248             datafile = optval
249         elif opt == "-s" or opt == "--slice":
250             names.append(optval)
251         elif opt == "-p" or opt == "--period":
252             period = int(optval)
253         else:
254             usage()
255             sys.exit(0)
256
257     # Check if we are already running
258     writepid("bwmon")
259
260     if not debug:
261         # Redirect stdout and stderr to syslog
262         syslog.openlog("bwmon")
263         sys.stdout = sys.stderr = Logger()
264
265     try:
266         f = open(datafile, "r+")
267         if verbose:
268             print "Loading %s" % datafile
269         (version, slices) = pickle.load(f)
270         f.close()
271         # Check version of data file
272         if version != "$Id: bwmon.py,v 1.4 2006/06/02 04:00:00 mlhuang Exp $":
273             print "Not using old version '%s' data file %s" % (version, datafile)
274             raise Exception
275     except Exception:
276         version = "$Id: bwmon.py,v 1.4 2006/06/02 04:00:00 mlhuang Exp $"
277         slices = {}
278
279     # Get special slice IDs
280     root_xid = bwlimit.get_xid("root")
281     default_xid = bwlimit.get_xid("default")
282
283     # Open connection to Node Manager
284     nm = NM()
285
286     live = []
287     for params in bwlimit.get():
288         (xid, share,
289          minrate, maxrate,
290          minexemptrate, maxexemptrate,
291          bytes, exemptbytes) = params
292         live.append(xid)
293
294         # Ignore root and default buckets
295         if xid == root_xid or xid == default_xid:
296             continue
297
298         name = bwlimit.get_slice(xid)
299         if name is None:
300             # Orphaned (not associated with a slice) class
301             name = "%d?" % xid
302
303         # Monitor only the specified slices
304         if names and name not in names:
305             continue
306
307         if slices.has_key(xid):
308             slice = slices[xid]
309             if time.time() >= (slice.time + period) or \
310                bytes < slice.bytes or exemptbytes < slice.exemptbytes:
311                 # Reset to defaults every 24 hours or if it appears
312                 # that the byte counters have overflowed (or, more
313                 # likely, the node was restarted or the HTB buckets
314                 # were re-initialized).
315                 slice.reset(maxrate, maxexemptrate, bytes, exemptbytes)
316             else:
317                 # Update byte counts
318                 slice.update(maxrate, maxexemptrate, bytes, exemptbytes)
319         else:
320             # New slice, initialize state
321             slice = slices[xid] = Slice(xid, name, maxrate, maxexemptrate, bytes, exemptbytes)
322
323     # Delete dead slices
324     dead = Set(slices.keys()) - Set(live)
325     for xid in dead:
326         del slices[xid]
327
328     if verbose:
329         print "Saving %s" % datafile
330     f = open(datafile, "w")
331     pickle.dump((version, slices), f)
332     f.close()
333
334     removepid("bwmon")
335
336 if __name__ == '__main__':
337     main()