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.
10 # Two separate limits are enforced, one for destinations exempt from
11 # the node bandwidth cap, and the other for all other destinations.
13 # Mark Huang <mlhuang@cs.princeton.edu>
14 # Andy Bavier <acb@cs.princeton.edu>
15 # Copyright (C) 2004-2006 The Trustees of Princeton University
17 # $Id: bwmon.py,v 1.4 2006/06/02 04:00:00 mlhuang Exp $
37 seconds_per_day = 24 * 60 * 60
43 datafile = "/var/lib/misc/bwmon.dat"
46 default_maxrate = bwlimit.get_bwcap()
48 default_maxexemptrate = bwlimit.bwmax
50 # 500 Kbit or 5.4 GB per day
51 default_avgrate = 500000
53 # 1.5 Mbit or 16.4 GB per day
54 default_avgexemptrate = 1500000
57 period = 1 * seconds_per_day
62 The slice %(slice)s has transmitted more than %(bytes)s from
63 %(hostname)s to %(class)s destinations
66 Its maximum %(class)s burst rate will be capped at %(avgrate)s
69 Please reduce the average %(class)s transmission rate
70 of the slice to %(avgrate)s, or %(limit)s per %(period)s.
76 %(date)s %(hostname)s bwcap %(slice)s
81 Stores the last recorded bandwidth parameters of a slice.
83 xid - slice context/VServer ID
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
92 def __init__(self, xid, name, maxrate, maxexemptrate, bytes, exemptbytes):
95 self.reset(maxrate, maxexemptrate, bytes, exemptbytes)
100 def reset(self, maxrate, maxexemptrate, bytes, exemptbytes):
102 Begin a new recording period. Remove caps by restoring limits
103 to their default values.
106 # Reset baseline time
107 self.time = time.time()
109 # Reset baseline byte coutns
111 self.exemptbytes = exemptbytes
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
120 if new_maxrate != maxrate or new_maxexemptrate != maxexemptrate:
121 print "%s reset to %s/%s" % \
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)
127 def update(self, maxrate, maxexemptrate, bytes, exemptbytes):
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
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
145 # Prepare message parameters from the template
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)}
153 bytelimit = self.avgrate * period / bits_per_byte
154 if bytes >= (self.bytes + bytelimit) and \
155 maxrate > self.avgrate:
156 new_maxrate = self.avgrate
158 new_maxrate = maxrate
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)
168 print "%(slice)s %(class)s " \
169 "%(bytes)s/%(limit)s (%(maxrate)s/%(avgrate)s)" % \
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
177 exemptbytelimit = self.avgexemptrate * period / bits_per_byte
178 if exemptbytes >= (self.exemptbytes + exemptbytelimit) and \
179 maxexemptrate > self.avgexemptrate:
180 new_maxexemptrate = self.avgexemptrate
182 new_maxexemptrate = maxexemptrate
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)
192 print "%(slice)s %(class)s " \
193 "%(bytes)s/%(limit)s (%(maxrate)s/%(avgrate)s)" % \
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
202 if new_maxrate != maxrate or new_maxexemptrate != maxexemptrate:
203 bwlimit.set(xid = self.xid, maxrate = new_maxrate, maxexemptrate = new_maxexemptrate)
207 subject = "pl_mom capped bandwidth of slice %(slice)s on %(hostname)s" % params
210 print message + (footer % params)
212 slicemail(self.name, subject, message + (footer % params))
216 Usage: %s [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))
229 global debug, verbose, datafile, period, nm
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
241 for (opt, optval) in opts:
242 if opt == "-d" or opt == "--debug":
244 elif opt == "-v" or opt == "--verbose":
246 bwlimit.verbose = verbose - 1
247 elif opt == "-f" or opt == "--file":
249 elif opt == "-s" or opt == "--slice":
251 elif opt == "-p" or opt == "--period":
257 # Check if we are already running
261 # Redirect stdout and stderr to syslog
262 syslog.openlog("bwmon")
263 sys.stdout = sys.stderr = Logger()
266 f = open(datafile, "r+")
268 print "Loading %s" % datafile
269 (version, slices) = pickle.load(f)
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)
276 version = "$Id: bwmon.py,v 1.4 2006/06/02 04:00:00 mlhuang Exp $"
279 # Get special slice IDs
280 root_xid = bwlimit.get_xid("root")
281 default_xid = bwlimit.get_xid("default")
283 # Open connection to Node Manager
287 for params in bwlimit.get():
290 minexemptrate, maxexemptrate,
291 bytes, exemptbytes) = params
294 # Ignore root and default buckets
295 if xid == root_xid or xid == default_xid:
298 name = bwlimit.get_slice(xid)
300 # Orphaned (not associated with a slice) class
303 # Monitor only the specified slices
304 if names and name not in names:
307 if slices.has_key(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)
318 slice.update(maxrate, maxexemptrate, bytes, exemptbytes)
320 # New slice, initialize state
321 slice = slices[xid] = Slice(xid, name, maxrate, maxexemptrate, bytes, exemptbytes)
324 dead = Set(slices.keys()) - Set(live)
329 print "Saving %s" % datafile
330 f = open(datafile, "w")
331 pickle.dump((version, slices), f)
336 if __name__ == '__main__':