* Added fix for > 25% min bandwidth shares. The idea is if someone wants a min rate...
[nodemanager.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.9 2007/02/26 17:52:08 faiyaza Exp $
19 #
20
21 import os
22 import sys
23 import time
24 import pickle
25
26 import socket
27 #import xmlrpclib
28 import bwlimit
29 import logger
30
31 from sets import Set
32 try:
33     sys.path.append("/etc/planetlab")
34     from plc_config import *
35 except:
36     logger.log("bwmon:  Warning: Configuration file /etc/planetlab/plc_config.py not found")
37     PLC_NAME = "PlanetLab"
38     PLC_SLICE_PREFIX = "pl"
39     PLC_MAIL_SUPPORT_ADDRESS = "support@planet-lab.org"
40     PLC_MAIL_SLICE_ADDRESS = "SLICE@slices.planet-lab.org"
41
42
43 # Utility functions
44 #from pl_mom import *
45
46 # Constants
47 seconds_per_day = 24 * 60 * 60
48 bits_per_byte = 8
49
50 # Defaults
51 debug = False 
52 verbose = False
53 datafile = "/var/lib/misc/bwmon.dat"
54 #nm = None
55
56 # Burst to line rate (or node cap).  Set by NM. in KBit/s
57 default_MaxRate = int(bwlimit.get_bwcap() / 1000)
58 default_Maxi2Rate = int(bwlimit.bwmax / 1000)
59 # Min rate 8 bits/s 
60 default_MinRate = 0
61 default_Mini2Rate = 0
62 # 5.4 Gbyte per day. 5.4 * 1024 k * 1024M * 1024G 
63 # 5.4 Gbyte per day max allowed transfered per recording period
64 default_MaxKByte = 5662310
65 default_ThreshKByte = int(.8 * default_MaxKByte) 
66 # 16.4 Gbyte per day max allowed transfered per recording period to I2
67 default_Maxi2KByte = 17196646
68 default_Threshi2KByte = int(.8 * default_Maxi2KByte) 
69 # Default share quanta
70 default_Share = 1
71
72 # Average over 1 day
73 period = 1 * seconds_per_day
74
75 # Message template
76 template = \
77 """
78 The slice %(slice)s has transmitted more than %(bytes)s from
79 %(hostname)s to %(class)s destinations
80 since %(since)s.
81
82 Its maximum %(class)s burst rate will be capped at %(new_maxrate)s/s
83 until %(until)s.
84
85 Please reduce the average %(class)s transmission rate
86 of the slice to %(limit)s per %(period)s.
87
88 """.lstrip()
89
90 footer = \
91 """
92 %(date)s %(hostname)s bwcap %(slice)s
93 """.lstrip()
94
95 def format_bytes(bytes, si = True):
96     """
97     Formats bytes into a string
98     """
99     if si:
100         kilo = 1000.
101     else:
102         # Officially, a kibibyte
103         kilo = 1024.
104
105     if bytes >= (kilo * kilo * kilo):
106         return "%.1f GB" % (bytes / (kilo * kilo * kilo))
107     elif bytes >= 1000000:
108         return "%.1f MB" % (bytes / (kilo * kilo))
109     elif bytes >= 1000:
110         return "%.1f KB" % (bytes / kilo)
111     else:
112         return "%.0f bytes" % bytes
113
114 def format_period(seconds):
115     """
116     Formats a period in seconds into a string
117     """
118
119     if seconds == (24 * 60 * 60):
120         return "day"
121     elif seconds == (60 * 60):
122         return "hour"
123     elif seconds > (24 * 60 * 60):
124         return "%.1f days" % (seconds / 24. / 60. / 60.)
125     elif seconds > (60 * 60):
126         return "%.1f hours" % (seconds / 60. / 60.)
127     elif seconds > (60):
128         return "%.1f minutes" % (seconds / 60.)
129     else:
130         return "%.0f seconds" % seconds
131
132 def slicemail(slice, subject, body):
133     sendmail = os.popen("/usr/sbin/sendmail -N never -t -f%s" % PLC_MAIL_SUPPORT_ADDRESS, "w")
134
135     # PLC has a separate list for pl_mom messages
136     if PLC_MAIL_SUPPORT_ADDRESS == "support@planet-lab.org":
137         to = ["pl-mom@planet-lab.org"]
138     else:
139         to = [PLC_MAIL_SUPPORT_ADDRESS]
140
141     if slice is not None and slice != "root":
142         to.append(PLC_MAIL_SLICE_ADDRESS.replace("SLICE", slice))
143
144     header = {'from': "%s Support <%s>" % (PLC_NAME, PLC_MAIL_SUPPORT_ADDRESS),
145               'to': ", ".join(to),
146               'version': sys.version.split(" ")[0],
147               'subject': subject}
148
149     # Write headers
150     sendmail.write(
151 """
152 Content-type: text/plain
153 From: %(from)s
154 Reply-To: %(from)s
155 To: %(to)s
156 X-Mailer: Python/%(version)s
157 Subject: %(subject)s
158
159 """.lstrip() % header)
160
161     # Write body
162     sendmail.write(body)
163     # Done
164     sendmail.close()
165
166
167 class Slice:
168     """
169     Stores the last recorded bandwidth parameters of a slice.
170
171     xid - slice context/VServer ID
172     name - slice name
173     time - beginning of recording period in UNIX seconds
174     bytes - low bandwidth bytes transmitted at the beginning of the recording period
175     i2bytes - high bandwidth bytes transmitted at the beginning of the recording period (for I2 -F)
176     ByteMax - total volume of data allowed
177     ByteThresh - After thresh, cap node to (maxbyte - bytes)/(time left in period)
178     ExemptByteMax - Same as above, but for i2.
179     ExemptByteThresh - i2 ByteThresh
180     maxrate - max_rate slice attribute. 
181     maxexemptrate - max_exempt_rate slice attribute.
182     self.emailed = did we email during this recording period
183
184     """
185
186     def __init__(self, xid, name, data):
187         self.xid = xid
188         self.name = name
189         self.time = 0
190         self.bytes = 0
191         self.i2bytes = 0
192         self.MaxRate = default_MaxRate
193         self.MinRate = default_MinRate
194         self.Maxi2Rate = default_Maxi2Rate
195         self.Mini2Rate = default_Mini2Rate
196         self.MaxKByte = default_MaxKByte
197         self.ThreshKByte = default_ThreshKByte
198         self.Maxi2KByte = default_Maxi2KByte
199         self.Threshi2KByte = default_Threshi2KByte
200         self.Share = default_Share
201         self.emailed = False
202
203         self.updateSliceAttributes(data)
204         bwlimit.set(xid = self.xid, 
205                 minrate = self.MinRate, 
206                 maxrate = self.MaxRate, 
207                 maxexemptrate = self.Maxi2Rate,
208                 minexemptrate = self.Mini2Rate,
209                 share = self.Share)
210
211
212     def __repr__(self):
213         return self.name
214
215     def updateSliceAttributes(self, data):
216         for sliver in data['slivers']:
217             if sliver['name'] == self.name: 
218                 for attribute in sliver['attributes']:
219                     if attribute['name'] == 'net_min_rate':     
220                         logger.log("bwmon:  Updating %s. Min Rate = %s" \
221                           %(self.name, self.MinRate))
222                         # To ensure min does not go above 25% of nodecap.
223                         if int(attribute['value']) > int(.25 * default_MaxRate):
224                             self.MinRate = int(.25 * default_MaxRate)
225                         else:    
226                             self.MinRate = int(attribute['value'])
227                     elif attribute['name'] == 'net_max_rate':       
228                         self.MaxRate = int(attribute['value'])
229                         logger.log("bwmon:  Updating %s. Max Rate = %s" \
230                           %(self.name, self.MaxRate))
231                     elif attribute['name'] == 'net_i2_min_rate':
232                         self.Mini2Rate = int(attribute['value'])
233                         logger.log("bwmon:  Updating %s. Min i2 Rate = %s" \
234                           %(self.name, self.Mini2Rate))
235                     elif attribute['name'] == 'net_i2_max_rate':        
236                         self.Maxi2Rate = int(attribute['value'])
237                         logger.log("bwmon:  Updating %s. Max i2 Rate = %s" \
238                           %(self.name, self.Maxi2Rate))
239                     elif attribute['name'] == 'net_max_kbyte':      
240                         self.MaxKByte = int(attribute['value'])
241                         logger.log("bwmon:  Updating %s. Max KByte lim = %s" \
242                           %(self.name, self.MaxKByte))
243                     elif attribute['name'] == 'net_i2_max_kbyte':   
244                         self.Maxi2KByte = int(attribute['value'])
245                         logger.log("bwmon:  Updating %s. Max i2 KByte = %s" \
246                           %(self.name, self.Maxi2KByte))
247                     elif attribute['name'] == 'net_thresh_kbyte':   
248                         self.ThreshKByte = int(attribute['value'])
249                         logger.log("bwmon:  Updating %s. Thresh KByte = %s" \
250                           %(self.name, self.ThreshKByte))
251                     elif attribute['name'] == 'net_i2_thresh_kbyte':    
252                         self.Threshi2KByte = int(attribute['value'])
253                         logger.log("bwmon:  Updating %s. i2 Thresh KByte = %s" \
254                           %(self.name, self.Threshi2KByte))
255                     elif attribute['name'] == 'net_share':  
256                         self.Share = int(attribute['value'])
257                         logger.log("bwmon:  Updating %s. Net Share = %s" \
258                           %(self.name, self.Share))
259                     elif attribute['name'] == 'net_i2_share':   
260                         self.Sharei2 = int(attribute['value'])
261                         logger.log("bwmon:  Updating %s. Net i2 Share = %s" \
262                           %(self.name, self.i2Share))
263
264
265     def reset(self, runningmaxrate, runningmaxi2rate, usedbytes, usedi2bytes, data):
266         """
267         Begin a new recording period. Remove caps by restoring limits
268         to their default values.
269         """
270         
271         # Query Node Manager for max rate overrides
272         self.updateSliceAttributes(data)    
273
274         # Reset baseline time
275         self.time = time.time()
276
277         # Reset baseline byte coutns
278         self.bytes = usedbytes
279         self.i2bytes = usedi2bytes
280
281         # Reset email 
282         self.emailed = False
283         maxrate = self.MaxRate * 1000 
284         maxi2rate = self.Maxi2Rate * 1000 
285         # Reset rates.
286         if (self.MaxRate != runningmaxrate) or (self.Maxi2Rate != runningmaxi2rate):
287             logger.log("bwmon:  %s reset to %s/%s" % \
288                   (self.name,
289                    bwlimit.format_tc_rate(maxrate),
290                    bwlimit.format_tc_rate(maxi2rate)))
291             bwlimit.set(xid = self.xid, 
292                 minrate = self.MinRate * 1000, 
293                 maxrate = self.MaxRate * 1000, 
294                 maxexemptrate = self.Maxi2Rate * 1000,
295                 minexemptrate = self.Mini2Rate * 1000,
296                 share = self.Share)
297
298     def update(self, runningmaxrate, runningmaxi2rate, usedbytes, usedi2bytes, data):
299         """
300         Update byte counts and check if byte limits have been
301         exceeded. 
302         """
303     
304         # Query Node Manager for max rate overrides
305         self.updateSliceAttributes(data)    
306      
307         # Prepare message parameters from the template
308         message = ""
309         params = {'slice': self.name, 'hostname': socket.gethostname(),
310                   'since': time.asctime(time.gmtime(self.time)) + " GMT",
311                   'until': time.asctime(time.gmtime(self.time + period)) + " GMT",
312                   'date': time.asctime(time.gmtime()) + " GMT",
313                   'period': format_period(period)} 
314
315         if usedbytes >= (self.bytes + (self.ThreshKByte * 1024)):
316             maxbyte = self.MaxKByte * 1024
317             bytesused = usedbytes - self.bytes
318             timeused = int(time.time() - self.time)
319             new_maxrate = int(((maxbyte - bytesused) * 8)/(period - timeused))
320             if new_maxrate < (self.MinRate * 1000):
321                 new_maxrate = self.MinRate * 1000
322         else:
323             new_maxrate = self.MaxRate * 1000 
324
325         # Format template parameters for low bandwidth message
326         params['class'] = "low bandwidth"
327         params['bytes'] = format_bytes(usedbytes - self.bytes)
328         params['limit'] = format_bytes(self.MaxKByte * 1024)
329         params['new_maxrate'] = bwlimit.format_tc_rate(new_maxrate)
330
331         if verbose:
332             logger.log("bwmon:  %(slice)s %(class)s " \
333                   "%(bytes)s of %(limit)s (%(new_maxrate)s/s maxrate)" % \
334                   params)
335
336         # Cap low bandwidth burst rate
337         if new_maxrate != runningmaxrate:
338             message += template % params
339             logger.log("bwmon:   ** %(slice)s %(class)s capped at %(new_maxrate)s/s " % params)
340     
341         if usedi2bytes >= (self.i2bytes + (self.Threshi2KByte * 1024)):
342             maxi2byte = self.Maxi2KByte * 1024
343             i2bytesused = usedi2bytes - self.i2bytes
344             timeused = int(time.time() - self.time)
345             new_maxi2rate = int(((maxi2byte - i2bytesused) * 8)/(period - timeused))
346             if new_maxi2rate < (self.Mini2Rate * 1000):
347                 new_maxi2rate = self.Mini2Rate * 1000
348         else:
349             new_maxi2rate = self.Maxi2Rate * 1000
350
351         # Format template parameters for high bandwidth message
352         params['class'] = "high bandwidth"
353         params['bytes'] = format_bytes(usedi2bytes - self.i2bytes)
354         params['limit'] = format_bytes(self.Maxi2KByte * 1024)
355         params['new_maxexemptrate'] = bwlimit.format_tc_rate(new_maxi2rate)
356
357         if verbose:
358             logger.log("bwmon:  %(slice)s %(class)s " \
359                   "%(bytes)s of %(limit)s (%(new_maxrate)s/s maxrate)" % params)
360
361         # Cap high bandwidth burst rate
362         if new_maxi2rate != runningmaxi2rate:
363             message += template % params
364             logger.log("bwmon:  %(slice)s %(class)s capped at %(new_maxexemptrate)s/s" % params)
365
366         # Apply parameters
367         if new_maxrate != runningmaxrate or new_maxi2rate != runningmaxi2rate:
368             bwlimit.set(xid = self.xid, maxrate = new_maxrate, maxexemptrate = new_maxi2rate)
369
370         # Notify slice
371         if message and self.emailed == False:
372             subject = "pl_mom capped bandwidth of slice %(slice)s on %(hostname)s" % params
373             if debug:
374                 logger.log("bwmon:  "+ subject)
375                 logger.log("bwmon:  "+ message + (footer % params))
376             else:
377                 self.emailed = True
378                 slicemail(self.name, subject, message + (footer % params))
379
380 def GetSlivers(data):
381     # Defaults
382     global datafile, \
383         period, \
384         default_MaxRate, \
385         default_Maxi2Rate, \
386         default_MinRate, \
387         default_MaxKByte,\
388         default_ThreshKByte,\
389         default_Maxi2KByte,\
390         default_Threshi2KByte,\
391         default_Share,\
392         verbose
393
394     # All slices
395     names = []
396
397     try:
398         f = open(datafile, "r+")
399         logger.log("bwmon:  Loading %s" % datafile)
400         (version, slices) = pickle.load(f)
401         f.close()
402         # Check version of data file
403         if version != "$Id: bwmon.py,v 1.9 2007/02/26 17:52:08 faiyaza Exp $":
404             logger.log("bwmon:  Not using old version '%s' data file %s" % (version, datafile))
405             raise Exception
406     except Exception:
407         version = "$Id: bwmon.py,v 1.9 2007/02/26 17:52:08 faiyaza Exp $"
408         slices = {}
409
410     # Get/set special slice IDs
411     root_xid = bwlimit.get_xid("root")
412     default_xid = bwlimit.get_xid("default")
413
414     if root_xid not in slices.keys():
415         slices[root_xid] = Slice(root_xid, "root", data)
416         slices[root_xid].reset(0, 0, 0, 0, data)
417
418     if default_xid not in slices.keys():
419         slices[default_xid] = Slice(default_xid, "default", data)
420         slices[default_xid].reset(0, 0, 0, 0, data)
421
422     live = {}
423     # Get running slivers. {xid: name}
424     for sliver in data['slivers']:
425         live[bwlimit.get_xid(sliver['name'])] = sliver['name']
426
427     # Setup new slices.
428     # live.xids - runing.xids = new.xids
429     newslicesxids = Set(live.keys()) - Set(slices.keys())
430     for newslicexid in newslicesxids:
431         if newslicexid != None:
432             logger.log("bwmon: New Slice %s" % live[newslicexid])
433             slices[newslicexid] = Slice(newslicexid, live[newslicexid], data)
434             slices[newslicexid].reset(0, 0, 0, 0, data)
435         else:
436             logger.log("bwmon  Slice %s doesn't have xid.  Must be delegated.  Skipping." % live[newslicexid])
437     # Get actual running values from tc.
438     # Update slice totals and bandwidth.
439     for params in bwlimit.get():
440         (xid, share,
441          minrate, maxrate,
442          minexemptrate, maxexemptrate,
443          usedbytes, usedi2bytes) = params
444         
445         # Ignore root and default buckets
446         if xid == root_xid or xid == default_xid:
447             continue
448
449         name = bwlimit.get_slice(xid)
450         if name is None:
451             # Orphaned (not associated with a slice) class
452             name = "%d?" % xid
453             bwlimit.off(xid)
454
455         # Monitor only the specified slices
456         if names and name not in names:
457             continue
458         #slices is populated from the pickle file
459         #xid is populated from bwlimit (read from /etc/passwd) 
460         if slices.has_key(xid):
461             slice = slices[xid]
462             if time.time() >= (slice.time + period) or \
463                usedbytes < slice.bytes or usedi2bytes < slice.i2bytes:
464                 # Reset to defaults every 24 hours or if it appears
465                 # that the byte counters have overflowed (or, more
466                 # likely, the node was restarted or the HTB buckets
467                 # were re-initialized).
468                 slice.reset(maxrate, maxexemptrate, usedbytes, usedi2bytes, data)
469             else:
470                 # Update byte counts
471                 slice.update(maxrate, maxexemptrate, usedbytes, usedi2bytes, data)
472         else:
473             # Just in case.  Probably (hopefully) this will never happen.
474             # New slice, initialize state
475             logger.log("bwmon: New Slice %s" % name)
476             slice = slices[xid] = Slice(xid, name, data)
477             slice.reset(maxrate, maxexemptrate, usedbytes, usedi2bytes, data)
478
479     # Delete dead slices
480     dead = Set(slices.keys()) - Set(live.keys())
481     for xid in dead:
482         if xid == root_xid or xid == default_xid:
483             continue
484         del slices[xid]
485         bwlimit.off(xid)
486
487     logger.log("bwmon:  Saving %s" % datafile)
488     f = open(datafile, "w")
489     pickle.dump((version, slices), f)
490     f.close()
491
492
493 #def GetSlivers(data):
494 #   for sliver in data['slivers']:
495 #       if sliver.has_key('attributes'):
496 #          print sliver
497 #           for attribute in sliver['attributes']:
498 #               if attribute['name'] == "KByteThresh": print attribute['value']
499
500 def start(options, config):
501     pass
502