Support for the plc_initscript_id attribute.
[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.1.2.5 2007/02/28 05:26:54 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 * 1000, 
206                 maxrate = self.MaxRate * 1000, 
207                 maxexemptrate = self.Maxi2Rate * 1000,
208                 minexemptrate = self.Mini2Rate * 1000,
209                 share = self.Share)
210
211
212     def __repr__(self):
213         return self.name
214
215     def updateSliceAttributes(self, data):
216         # Incase the limits have changed. 
217         if (self.MaxRate != default_MaxRate) or \
218         (self.Maxi2Rate != default_Maxi2Rate):
219             self.MaxRate = int(bwlimit.get_bwcap() / 1000)
220             self.Maxi2Rate = int(bwlimit.bwmax / 1000)
221
222         # Get attributes
223         for sliver in data['slivers']:
224             if sliver['name'] == self.name: 
225                 for attribute in sliver['attributes']:
226                     if attribute['name'] == 'net_min_rate':     
227                         logger.log("bwmon:  Updating %s. Min Rate = %s" \
228                           %(self.name, self.MinRate))
229                         # To ensure min does not go above 25% of nodecap.
230                         if int(attribute['value']) > int(.25 * default_MaxRate):
231                             self.MinRate = int(.25 * default_MaxRate)
232                         else:    
233                             self.MinRate = int(attribute['value'])
234                     elif attribute['name'] == 'net_max_rate':       
235                         self.MaxRate = int(attribute['value'])
236                         logger.log("bwmon:  Updating %s. Max Rate = %s" \
237                           %(self.name, self.MaxRate))
238                     elif attribute['name'] == 'net_i2_min_rate':
239                         self.Mini2Rate = int(attribute['value'])
240                         logger.log("bwmon:  Updating %s. Min i2 Rate = %s" \
241                           %(self.name, self.Mini2Rate))
242                     elif attribute['name'] == 'net_i2_max_rate':        
243                         self.Maxi2Rate = int(attribute['value'])
244                         logger.log("bwmon:  Updating %s. Max i2 Rate = %s" \
245                           %(self.name, self.Maxi2Rate))
246                     elif attribute['name'] == 'net_max_kbyte':      
247                         self.MaxKByte = int(attribute['value'])
248                         logger.log("bwmon:  Updating %s. Max KByte lim = %s" \
249                           %(self.name, self.MaxKByte))
250                     elif attribute['name'] == 'net_i2_max_kbyte':   
251                         self.Maxi2KByte = int(attribute['value'])
252                         logger.log("bwmon:  Updating %s. Max i2 KByte = %s" \
253                           %(self.name, self.Maxi2KByte))
254                     elif attribute['name'] == 'net_thresh_kbyte':   
255                         self.ThreshKByte = int(attribute['value'])
256                         logger.log("bwmon:  Updating %s. Thresh KByte = %s" \
257                           %(self.name, self.ThreshKByte))
258                     elif attribute['name'] == 'net_i2_thresh_kbyte':    
259                         self.Threshi2KByte = int(attribute['value'])
260                         logger.log("bwmon:  Updating %s. i2 Thresh KByte = %s" \
261                           %(self.name, self.Threshi2KByte))
262                     elif attribute['name'] == 'net_share':  
263                         self.Share = int(attribute['value'])
264                         logger.log("bwmon:  Updating %s. Net Share = %s" \
265                           %(self.name, self.Share))
266                     elif attribute['name'] == 'net_i2_share':   
267                         self.Sharei2 = int(attribute['value'])
268                         logger.log("bwmon:  Updating %s. Net i2 Share = %s" \
269                           %(self.name, self.i2Share))
270
271
272     def reset(self, runningmaxrate, runningmaxi2rate, usedbytes, usedi2bytes, data):
273         """
274         Begin a new recording period. Remove caps by restoring limits
275         to their default values.
276         """
277         
278         # Query Node Manager for max rate overrides
279         self.updateSliceAttributes(data)    
280
281         # Reset baseline time
282         self.time = time.time()
283
284         # Reset baseline byte coutns
285         self.bytes = usedbytes
286         self.i2bytes = usedi2bytes
287
288         # Reset email 
289         self.emailed = False
290         maxrate = self.MaxRate * 1000 
291         maxi2rate = self.Maxi2Rate * 1000 
292         # Reset rates.
293         if (self.MaxRate != runningmaxrate) or (self.Maxi2Rate != runningmaxi2rate):
294             logger.log("bwmon:  %s reset to %s/%s" % \
295                   (self.name,
296                    bwlimit.format_tc_rate(maxrate),
297                    bwlimit.format_tc_rate(maxi2rate)))
298             bwlimit.set(xid = self.xid, 
299                 minrate = self.MinRate * 1000, 
300                 maxrate = self.MaxRate * 1000, 
301                 maxexemptrate = self.Maxi2Rate * 1000,
302                 minexemptrate = self.Mini2Rate * 1000,
303                 share = self.Share)
304
305     def update(self, runningmaxrate, runningmaxi2rate, usedbytes, usedi2bytes, data):
306         """
307         Update byte counts and check if byte limits have been
308         exceeded. 
309         """
310     
311         # Query Node Manager for max rate overrides
312         self.updateSliceAttributes(data)    
313      
314         # Prepare message parameters from the template
315         message = ""
316         params = {'slice': self.name, 'hostname': socket.gethostname(),
317                   'since': time.asctime(time.gmtime(self.time)) + " GMT",
318                   'until': time.asctime(time.gmtime(self.time + period)) + " GMT",
319                   'date': time.asctime(time.gmtime()) + " GMT",
320                   'period': format_period(period)} 
321
322         if usedbytes >= (self.bytes + (self.ThreshKByte * 1024)):
323             sum = self.bytes + (self.ThreshKByte * 1024)
324             maxbyte = self.MaxKByte * 1024
325             bytesused = usedbytes - self.bytes
326             timeused = int(time.time() - self.time)
327             new_maxrate = int(((maxbyte - bytesused) * 8)/(period - timeused))
328             if new_maxrate < (self.MinRate * 1000):
329                 new_maxrate = self.MinRate * 1000
330         else:
331             new_maxrate = self.MaxRate * 1000 
332
333         # Format template parameters for low bandwidth message
334         params['class'] = "low bandwidth"
335         params['bytes'] = format_bytes(usedbytes - self.bytes)
336         params['limit'] = format_bytes(self.MaxKByte * 1024)
337         params['new_maxrate'] = bwlimit.format_tc_rate(new_maxrate)
338
339         if verbose:
340             logger.log("bwmon:  %(slice)s %(class)s " \
341                   "%(bytes)s of %(limit)s (%(new_maxrate)s/s maxrate)" % \
342                   params)
343
344         # Cap low bandwidth burst rate
345         if new_maxrate != runningmaxrate:
346             message += template % params
347             logger.log("bwmon:   ** %(slice)s %(class)s capped at %(new_maxrate)s/s " % params)
348     
349         if usedi2bytes >= (self.i2bytes + (self.Threshi2KByte * 1024)):
350             maxi2byte = self.Maxi2KByte * 1024
351             i2bytesused = usedi2bytes - self.i2bytes
352             timeused = int(time.time() - self.time)
353             new_maxi2rate = int(((maxi2byte - i2bytesused) * 8)/(period - timeused))
354             if new_maxi2rate < (self.Mini2Rate * 1000):
355                 new_maxi2rate = self.Mini2Rate * 1000
356         else:
357             new_maxi2rate = self.Maxi2Rate * 1000
358
359         # Format template parameters for high bandwidth message
360         params['class'] = "high bandwidth"
361         params['bytes'] = format_bytes(usedi2bytes - self.i2bytes)
362         params['limit'] = format_bytes(self.Maxi2KByte * 1024)
363         params['new_maxexemptrate'] = bwlimit.format_tc_rate(new_maxi2rate)
364
365         if verbose:
366             logger.log("bwmon:  %(slice)s %(class)s " \
367                   "%(bytes)s of %(limit)s (%(new_maxrate)s/s maxrate)" % params)
368
369         # Cap high bandwidth burst rate
370         if new_maxi2rate != runningmaxi2rate:
371             message += template % params
372             logger.log("bwmon:  %(slice)s %(class)s capped at %(new_maxexemptrate)s/s" % params)
373
374         # Apply parameters
375         if new_maxrate != runningmaxrate or new_maxi2rate != runningmaxi2rate:
376             bwlimit.set(xid = self.xid, maxrate = new_maxrate, maxexemptrate = new_maxi2rate)
377
378         # Notify slice
379         if message and self.emailed == False:
380             subject = "pl_mom capped bandwidth of slice %(slice)s on %(hostname)s" % params
381             if debug:
382                 logger.log("bwmon:  "+ subject)
383                 logger.log("bwmon:  "+ message + (footer % params))
384             else:
385                 self.emailed = True
386                 slicemail(self.name, subject, message + (footer % params))
387
388 def GetSlivers(data):
389     # Defaults
390     global datafile, \
391         period, \
392         default_MaxRate, \
393         default_Maxi2Rate, \
394         default_MinRate, \
395         default_MaxKByte,\
396         default_ThreshKByte,\
397         default_Maxi2KByte,\
398         default_Threshi2KByte,\
399         default_Share,\
400         verbose
401
402     # All slices
403     names = []
404
405     # Incase the limits have changed. 
406     default_MaxRate = int(bwlimit.get_bwcap() / 1000)
407     default_Maxi2Rate = int(bwlimit.bwmax / 1000)
408
409     # Incase default isn't set yet.
410     if default_MaxRate == -1:
411         default_MaxRate = 1000000
412
413     try:
414         f = open(datafile, "r+")
415         logger.log("bwmon:  Loading %s" % datafile)
416         (version, slices) = pickle.load(f)
417         f.close()
418         # Check version of data file
419         if version != "$Id: bwmon.py,v 1.1.2.5 2007/02/28 05:26:54 faiyaza Exp $":
420             logger.log("bwmon:  Not using old version '%s' data file %s" % (version, datafile))
421             raise Exception
422     except Exception:
423         version = "$Id: bwmon.py,v 1.1.2.5 2007/02/28 05:26:54 faiyaza Exp $"
424         slices = {}
425
426     # Get/set special slice IDs
427     root_xid = bwlimit.get_xid("root")
428     default_xid = bwlimit.get_xid("default")
429
430     if root_xid not in slices.keys():
431         slices[root_xid] = Slice(root_xid, "root", data)
432         slices[root_xid].reset(0, 0, 0, 0, data)
433
434     if default_xid not in slices.keys():
435         slices[default_xid] = Slice(default_xid, "default", data)
436         slices[default_xid].reset(0, 0, 0, 0, data)
437
438     live = {}
439     # Get running slivers. {xid: name}
440     for sliver in data['slivers']:
441         live[bwlimit.get_xid(sliver['name'])] = sliver['name']
442
443     # Setup new slices.
444     # live.xids - runing.xids = new.xids
445     newslicesxids = Set(live.keys()) - Set(slices.keys())
446     for newslicexid in newslicesxids:
447         if newslicexid != None:
448             logger.log("bwmon: New Slice %s" % live[newslicexid])
449             slices[newslicexid] = Slice(newslicexid, live[newslicexid], data)
450             slices[newslicexid].reset(0, 0, 0, 0, data)
451         else:
452             logger.log("bwmon  Slice %s doesn't have xid.  Must be delegated.  Skipping." % live[newslicexid])
453     # Get actual running values from tc.
454     # Update slice totals and bandwidth.
455     for params in bwlimit.get():
456         (xid, share,
457          minrate, maxrate,
458          minexemptrate, maxexemptrate,
459          usedbytes, usedi2bytes) = params
460         
461         # Ignore root and default buckets
462         if xid == root_xid or xid == default_xid:
463             continue
464
465         name = bwlimit.get_slice(xid)
466         if name is None:
467             # Orphaned (not associated with a slice) class
468             name = "%d?" % xid
469             bwlimit.off(xid)
470
471         # Monitor only the specified slices
472         if names and name not in names:
473             continue
474         #slices is populated from the pickle file
475         #xid is populated from bwlimit (read from /etc/passwd) 
476         if slices.has_key(xid):
477             slice = slices[xid]
478             if time.time() >= (slice.time + period) or \
479                usedbytes < slice.bytes or usedi2bytes < slice.i2bytes:
480                 # Reset to defaults every 24 hours or if it appears
481                 # that the byte counters have overflowed (or, more
482                 # likely, the node was restarted or the HTB buckets
483                 # were re-initialized).
484                 slice.reset(maxrate, maxexemptrate, usedbytes, usedi2bytes, data)
485             else:
486                 # Update byte counts
487                 slice.update(maxrate, maxexemptrate, usedbytes, usedi2bytes, data)
488         else:
489             # Just in case.  Probably (hopefully) this will never happen.
490             # New slice, initialize state
491             logger.log("bwmon: New Slice %s" % name)
492             slice = slices[xid] = Slice(xid, name, data)
493             slice.reset(maxrate, maxexemptrate, usedbytes, usedi2bytes, data)
494
495     # Delete dead slices
496     dead = Set(slices.keys()) - Set(live.keys())
497     for xid in dead:
498         if xid == root_xid or xid == default_xid:
499             continue
500         del slices[xid]
501         bwlimit.off(xid)
502
503     logger.log("bwmon:  Saving %s" % datafile)
504     f = open(datafile, "w")
505     pickle.dump((version, slices), f)
506     f.close()
507
508
509 #def GetSlivers(data):
510 #   for sliver in data['slivers']:
511 #       if sliver.has_key('attributes'):
512 #          print sliver
513 #           for attribute in sliver['attributes']:
514 #               if attribute['name'] == "KByteThresh": print attribute['value']
515
516 def start(options, config):
517     pass
518