expects the 'interfaces' key in GetSlivers - review logs to always mention module
[nodemanager.git] / bwmon.py
1 #!/usr/bin/python
2 #
3 # $Id$
4 # $URL$
5 #
6 # Average bandwidth monitoring script. Run periodically via NM db.sync to
7 # enforce a soft limit on daily bandwidth usage for each slice. If a
8 # slice is found to have transmitted 80% of its daily byte limit usage,
9 # its instantaneous rate will be capped at the bytes remaning in the limit
10 # over the time remaining in the recording period.
11 #
12 # Two separate limits are enforced, one for destinations exempt from
13 # the node bandwidth cap (i.e. Internet2), and the other for all other destinations.
14 #
15 # Mark Huang <mlhuang@cs.princeton.edu>
16 # Andy Bavier <acb@cs.princeton.edu>
17 # Faiyaz Ahmed <faiyaza@cs.princeton.edu>
18 # Copyright (C) 2004-2008 The Trustees of Princeton University
19 #
20
21 import os
22 import sys
23 import time
24 import pickle
25 import socket
26 import copy
27 import threading
28
29 import logger
30 import tools
31 import bwlimit
32 import database
33
34 from sets import Set
35
36 # Defaults
37 # Set DEBUG to True if you don't want to send emails
38 DEBUG = False
39 # Set ENABLE to False to setup buckets, but not limit.
40 ENABLE = True
41
42 datafile = "/var/lib/misc/bwmon.dat"
43
44 try:
45     sys.path.append("/etc/planetlab")
46     from plc_config import *
47 except:
48     DEBUG = True
49     logger.log("bwmon:  Warning: Configuration file /etc/planetlab/plc_config.py not found", 2)
50     logger.log("bwmon:  Running in DEBUG mode.  Logging to file and not emailing.", 1)
51
52 # Constants
53 seconds_per_day = 24 * 60 * 60
54 bits_per_byte = 8
55
56 dev_default = tools.get_default_if()
57 # Burst to line rate (or node cap).  Set by NM. in KBit/s
58 default_MaxRate = int(bwlimit.get_bwcap(dev_default) / 1000)
59 default_Maxi2Rate = int(bwlimit.bwmax / 1000)
60 # 5.4 Gbyte per day. 5.4 * 1024 k * 1024M * 1024G 
61 # 5.4 Gbyte per day max allowed transfered per recording period
62 # 5.4 Gbytes per day is aprox 512k/s for 24hrs (approx because original math was wrong
63 # but its better to keep a higher byte total and keep people happy than correct
64 # the problem and piss people off.
65 # default_MaxKByte = 5662310
66
67 # -- 6/1/09 
68 # llp wants to double these, so we use the following
69 # 1mbit * 24hrs * 60mins * 60secs = bits/day
70 # 1000000 * 24 * 60 * 60 / (1024 * 8)
71 default_MaxKByte = 10546875
72
73 # 16.4 Gbyte per day max allowed transfered per recording period to I2
74 # default_Maxi2KByte = 17196646
75
76 # -- 6/1/09
77 # 3Mb/s for 24hrs a day (30.17 gigs)
78 default_Maxi2KByte = 31640625
79
80 # Default share quanta
81 default_Share = 1
82
83 # Average over 1 day
84 period = 1 * seconds_per_day
85
86 # Message template
87 template = \
88 """
89 The slice %(slice)s has transmitted more than %(bytes)s from
90 %(hostname)s to %(class)s destinations
91 since %(since)s.
92
93 Its maximum %(class)s burst rate will be capped at %(new_maxrate)s/s
94 until %(until)s.
95
96 Please reduce the average %(class)s transmission rate
97 of the slice to %(limit)s per %(period)s.
98
99 """.lstrip()
100
101 footer = \
102 """
103 %(date)s %(hostname)s bwcap %(slice)s
104 """.lstrip()
105
106 def format_bytes(bytes, si = True):
107     """
108     Formats bytes into a string
109     """
110     if si:
111         kilo = 1000.
112     else:
113         # Officially, a kibibyte
114         kilo = 1024.
115
116     if bytes >= (kilo * kilo * kilo):
117         return "%.1f GB" % (bytes / (kilo * kilo * kilo))
118     elif bytes >= 1000000:
119         return "%.1f MB" % (bytes / (kilo * kilo))
120     elif bytes >= 1000:
121         return "%.1f KB" % (bytes / kilo)
122     else:
123         return "%.0f bytes" % bytes
124
125 def format_period(seconds):
126     """
127     Formats a period in seconds into a string
128     """
129
130     if seconds == (24 * 60 * 60):
131         return "day"
132     elif seconds == (60 * 60):
133         return "hour"
134     elif seconds > (24 * 60 * 60):
135         return "%.1f days" % (seconds / 24. / 60. / 60.)
136     elif seconds > (60 * 60):
137         return "%.1f hours" % (seconds / 60. / 60.)
138     elif seconds > (60):
139         return "%.1f minutes" % (seconds / 60.)
140     else:
141         return "%.0f seconds" % seconds
142
143 def slicemail(slice, subject, body):
144     '''
145     Front end to sendmail.  Sends email to slice alias with given subject and body.
146     '''
147
148     sendmail = os.popen("/usr/sbin/sendmail -N never -t -f%s" % PLC_MAIL_SUPPORT_ADDRESS, "w")
149
150     # Parsed from MyPLC config
151     to = [PLC_MAIL_MOM_LIST_ADDRESS]
152
153     if slice is not None and slice != "root":
154         to.append(PLC_MAIL_SLICE_ADDRESS.replace("SLICE", slice))
155
156     header = {'from': "%s Support <%s>" % (PLC_NAME, PLC_MAIL_SUPPORT_ADDRESS),
157               'to': ", ".join(to),
158               'version': sys.version.split(" ")[0],
159               'subject': subject}
160
161     # Write headers
162     sendmail.write(
163 """
164 Content-type: text/plain
165 From: %(from)s
166 Reply-To: %(from)s
167 To: %(to)s
168 X-Mailer: Python/%(version)s
169 Subject: %(subject)s
170
171 """.lstrip() % header)
172
173     # Write body
174     sendmail.write(body)
175     # Done
176     sendmail.close()
177
178
179 class Slice:
180     """
181     Stores the last recorded bandwidth parameters of a slice.
182
183     xid - slice context/VServer ID
184     name - slice name
185     time - beginning of recording period in UNIX seconds
186     bytes - low bandwidth bytes transmitted at the beginning of the recording period
187     i2bytes - high bandwidth bytes transmitted at the beginning of the recording period (for I2 -F)
188     MaxKByte - total volume of data allowed
189     ThreshKbyte - After thresh, cap node to (maxkbyte - bytes)/(time left in period)
190     Maxi2KByte - same as MaxKByte, but for i2 
191     Threshi2Kbyte - same as Threshi2KByte, but for i2 
192     MaxRate - max_rate slice attribute. 
193     Maxi2Rate - max_exempt_rate slice attribute.
194     Share - Used by Sirius to loan min rates
195     Sharei2 - Used by Sirius to loan min rates for i2
196     self.emailed - did slice recv email during this recording period
197
198     """
199
200     def __init__(self, xid, name, rspec):
201         self.xid = xid
202         self.name = name
203         self.time = 0
204         self.bytes = 0
205         self.i2bytes = 0
206         self.MaxRate = default_MaxRate
207         self.MinRate = bwlimit.bwmin / 1000
208         self.Maxi2Rate = default_Maxi2Rate
209         self.Mini2Rate = bwlimit.bwmin / 1000
210         self.MaxKByte = default_MaxKByte
211         self.ThreshKByte = int(.8 * self.MaxKByte)
212         self.Maxi2KByte = default_Maxi2KByte
213         self.Threshi2KByte = int(.8 * self.Maxi2KByte)
214         self.Share = default_Share
215         self.Sharei2 = default_Share
216         self.emailed = False
217         self.capped = False
218
219         self.updateSliceTags(rspec)
220         bwlimit.set(xid = self.xid, 
221                 minrate = self.MinRate * 1000, 
222                 maxrate = self.MaxRate * 1000, 
223                 maxexemptrate = self.Maxi2Rate * 1000,
224                 minexemptrate = self.Mini2Rate * 1000,
225                 share = self.Share)
226
227     def __repr__(self):
228         return self.name
229
230     def updateSliceTags(self, rspec):
231         '''
232         Use respects from GetSlivers to PLC to populate slice object.  Also
233         do some sanity checking.
234         '''
235
236         # Sanity check plus policy decision for MinRate:
237         # Minrate cant be greater than 25% of MaxRate or NodeCap.
238         MinRate = int(rspec.get("net_min_rate", bwlimit.bwmin / 1000))
239         if MinRate > int(.25 * default_MaxRate):
240             MinRate = int(.25 * default_MaxRate)
241         if MinRate != self.MinRate:
242             self.MinRate = MinRate
243             logger.log("bwmon:  Updating %s: Min Rate = %s" %(self.name, self.MinRate))
244
245         MaxRate = int(rspec.get('net_max_rate', default_MaxRate))
246         if MaxRate != self.MaxRate:
247             self.MaxRate = MaxRate
248             logger.log("bwmon:  Updating %s: Max Rate = %s" %(self.name, self.MaxRate))
249
250         Mini2Rate = int(rspec.get('net_i2_min_rate', bwlimit.bwmin / 1000))
251         if Mini2Rate != self.Mini2Rate:
252             self.Mini2Rate = Mini2Rate 
253             logger.log("bwmon:  Updating %s: Min i2 Rate = %s" %(self.name, self.Mini2Rate))
254
255         Maxi2Rate = int(rspec.get('net_i2_max_rate', default_Maxi2Rate))
256         if Maxi2Rate != self.Maxi2Rate:
257             self.Maxi2Rate = Maxi2Rate
258             logger.log("bwmon:  Updating %s: Max i2 Rate = %s" %(self.name, self.Maxi2Rate))
259                           
260         MaxKByte = int(rspec.get('net_max_kbyte', default_MaxKByte))
261         if MaxKByte != self.MaxKByte:
262             self.MaxKByte = MaxKByte
263             logger.log("bwmon:  Updating %s: Max KByte lim = %s" %(self.name, self.MaxKByte))
264                           
265         Maxi2KByte = int(rspec.get('net_i2_max_kbyte', default_Maxi2KByte))
266         if Maxi2KByte != self.Maxi2KByte:
267             self.Maxi2KByte = Maxi2KByte
268             logger.log("bwmon:  Updating %s: Max i2 KByte = %s" %(self.name, self.Maxi2KByte))
269                           
270         ThreshKByte = int(rspec.get('net_thresh_kbyte', (MaxKByte * .8)))
271         if ThreshKByte != self.ThreshKByte:
272             self.ThreshKByte = ThreshKByte
273             logger.log("bwmon:  Updating %s: Thresh KByte = %s" %(self.name, self.ThreshKByte))
274                           
275         Threshi2KByte = int(rspec.get('net_i2_thresh_kbyte', (Maxi2KByte * .8)))
276         if Threshi2KByte != self.Threshi2KByte:    
277             self.Threshi2KByte = Threshi2KByte
278             logger.log("bwmon:  Updating %s: i2 Thresh KByte = %s" %(self.name, self.Threshi2KByte))
279  
280         Share = int(rspec.get('net_share', default_Share))
281         if Share != self.Share:
282             self.Share = Share
283             logger.log("bwmon:  Updating %s: Net Share = %s" %(self.name, self.Share))
284
285         Sharei2 = int(rspec.get('net_i2_share', default_Share))
286         if Sharei2 != self.Sharei2:
287             self.Sharei2 = Sharei2 
288             logger.log("bwmon:  Updating %s: Net i2 Share = %s" %(self.name, self.i2Share))
289
290
291     def reset(self, runningrates, rspec):
292         """
293         Begin a new recording period. Remove caps by restoring limits
294         to their default values.
295         """
296         # Cache share for later comparison
297         self.Share = runningrates.get('share', 1)
298
299         # Query Node Manager for max rate overrides
300         self.updateSliceTags(rspec)    
301
302         # Reset baseline time
303         self.time = time.time()
304
305         # Reset baseline byte coutns
306         self.bytes = runningrates.get('usedbytes', 0)
307         self.i2bytes = runningrates.get('usedi2bytes', 0)
308
309         # Reset email 
310         self.emailed = False
311         # Reset flag
312         self.capped = False
313         # Reset rates.
314         maxrate = self.MaxRate * 1000 
315         minrate = self.MinRate * 1000 
316         maxi2rate = self.Maxi2Rate * 1000
317         mini2rate = self.Mini2Rate * 1000
318
319         if (maxrate != runningrates.get('maxrate', 0)) or \
320          (minrate != runningrates.get('maxrate', 0)) or \
321          (maxi2rate != runningrates.get('maxexemptrate', 0)) or \
322          (mini2rate != runningrates.get('minexemptrate', 0)) or \
323          (self.Share != runningrates.get('share', 0)):
324             logger.log("bwmon:  %s reset to %s/%s" % \
325                   (self.name,
326                    bwlimit.format_tc_rate(maxrate),
327                    bwlimit.format_tc_rate(maxi2rate)), 1)
328             bwlimit.set(xid = self.xid, dev = dev_default,
329                 minrate = self.MinRate * 1000, 
330                 maxrate = self.MaxRate * 1000, 
331                 maxexemptrate = self.Maxi2Rate * 1000,
332                 minexemptrate = self.Mini2Rate * 1000,
333                 share = self.Share)
334
335     def notify(self, new_maxrate, new_maxexemptrate, usedbytes, usedi2bytes):
336         """
337         Notify the slice it's being capped.
338         """
339          # Prepare message parameters from the template
340         message = ""
341         params = {'slice': self.name, 'hostname': socket.gethostname(),
342                   'since': time.asctime(time.gmtime(self.time)) + " GMT",
343                   'until': time.asctime(time.gmtime(self.time + period)) + " GMT",
344                   'date': time.asctime(time.gmtime()) + " GMT",
345                   'period': format_period(period)}
346
347         if new_maxrate != (self.MaxRate * 1000):
348             # Format template parameters for low bandwidth message
349             params['class'] = "low bandwidth"
350             params['bytes'] = format_bytes(usedbytes - self.bytes)
351             params['limit'] = format_bytes(self.MaxKByte * 1024)
352             params['new_maxrate'] = bwlimit.format_tc_rate(new_maxrate)
353
354             # Cap low bandwidth burst rate
355             message += template % params
356             logger.log("bwmon:   ** %(slice)s %(class)s capped at %(new_maxrate)s/s " % params)
357
358         if new_maxexemptrate != (self.Maxi2Rate * 1000):
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_maxrate'] = bwlimit.format_tc_rate(new_maxexemptrate)
364  
365             message += template % params
366             logger.log("bwmon:   ** %(slice)s %(class)s capped at %(new_maxrate)s/s " % params)
367        
368         # Notify slice
369         if self.emailed == False:
370             subject = "pl_mom capped bandwidth of slice %(slice)s on %(hostname)s" % params
371             if DEBUG:
372                 logger.log("bwmon:  "+ subject)
373                 logger.log("bwmon:  "+ message + (footer % params))
374             else:
375                 self.emailed = True
376                 logger.log("bwmon:  Emailing %s" % self.name)
377                 slicemail(self.name, subject, message + (footer % params))
378
379
380     def update(self, runningrates, rspec):
381         """
382         Update byte counts and check if byte thresholds have been
383         exceeded. If exceeded, cap to remaining bytes in limit over remaining time in period.  
384         Recalculate every time module runs.
385         """
386         # cache share for later comparison
387         runningrates['share'] = self.Share
388
389         # Query Node Manager for max rate overrides
390         self.updateSliceTags(rspec)    
391
392         usedbytes = runningrates['usedbytes']
393         usedi2bytes = runningrates['usedi2bytes']
394
395         # Check limits.
396         if usedbytes >= (self.bytes + (self.ThreshKByte * 1024)):
397             sum = self.bytes + (self.ThreshKByte * 1024)
398             maxbyte = self.MaxKByte * 1024
399             bytesused = usedbytes - self.bytes
400             timeused = int(time.time() - self.time)
401             # Calcuate new rate. in bit/s
402             new_maxrate = int(((maxbyte - bytesused) * 8)/(period - timeused))
403             # Never go under MinRate
404             if new_maxrate < (self.MinRate * 1000):
405                 new_maxrate = self.MinRate * 1000
406             # State information.  I'm capped.
407             self.capped += True
408         else:
409             # Sanity Check
410             new_maxrate = self.MaxRate * 1000
411             self.capped += False
412  
413         if usedi2bytes >= (self.i2bytes + (self.Threshi2KByte * 1024)):
414             maxi2byte = self.Maxi2KByte * 1024
415             i2bytesused = usedi2bytes - self.i2bytes
416             timeused = int(time.time() - self.time)
417             # Calcuate New Rate.
418             new_maxi2rate = int(((maxi2byte - i2bytesused) * 8)/(period - timeused))
419             # Never go under MinRate
420             if new_maxi2rate < (self.Mini2Rate * 1000):
421                 new_maxi2rate = self.Mini2Rate * 1000
422             # State information.  I'm capped.
423             self.capped += True
424         else:
425             # Sanity
426             new_maxi2rate = self.Maxi2Rate * 1000
427             self.capped += False
428
429         # Check running values against newly calculated values so as not to run tc
430         # unnecessarily
431         if (runningrates['maxrate'] != new_maxrate) or \
432         (runningrates['minrate'] != self.MinRate * 1000) or \
433         (runningrates['maxexemptrate'] != new_maxi2rate) or \
434         (runningrates['minexemptrate'] != self.Mini2Rate * 1000) or \
435         (runningrates['share'] != self.Share):
436             # Apply parameters
437             bwlimit.set(xid = self.xid, 
438                 minrate = self.MinRate * 1000, 
439                 maxrate = new_maxrate,
440                 minexemptrate = self.Mini2Rate * 1000,
441                 maxexemptrate = new_maxi2rate,
442                 share = self.Share)
443
444         # Notify slice
445         if self.capped == True:
446             self.notify(new_maxrate, new_maxi2rate, usedbytes, usedi2bytes)
447
448
449 def gethtbs(root_xid, default_xid):
450     """
451     Return dict {xid: {*rates}} of running htbs as reported by tc that have names.
452     Turn off HTBs without names.
453     """
454     livehtbs = {}
455     for params in bwlimit.get():
456         (xid, share,
457          minrate, maxrate,
458          minexemptrate, maxexemptrate,
459          usedbytes, usedi2bytes) = params
460         
461         name = bwlimit.get_slice(xid)
462
463         if (name is None) \
464         and (xid != root_xid) \
465         and (xid != default_xid):
466             # Orphaned (not associated with a slice) class
467             name = "%d?" % xid
468             logger.log("bwmon:  Found orphaned HTB %s. Removing." %name, 1)
469             bwlimit.off(xid)
470
471         livehtbs[xid] = {'share': share,
472             'minrate': minrate,
473             'maxrate': maxrate,
474             'maxexemptrate': maxexemptrate,
475             'minexemptrate': minexemptrate,
476             'usedbytes': usedbytes,
477             'name': name, 
478             'usedi2bytes': usedi2bytes}
479
480     return livehtbs
481
482 def sync(nmdbcopy):
483     """
484     Syncs tc, db, and bwmon.dat.  Then, starts new slices, kills old ones, and updates byte accounts for each running slice.  Sends emails and caps those that went over their limit.
485     """
486     # Defaults
487     global datafile, \
488         period, \
489         default_MaxRate, \
490         default_Maxi2Rate, \
491         default_MaxKByte,\
492         default_Maxi2KByte,\
493         default_Share
494
495     # All slices
496     names = []
497     # Incase the limits have changed. 
498     default_MaxRate = int(bwlimit.get_bwcap() / 1000)
499     default_Maxi2Rate = int(bwlimit.bwmax / 1000)
500
501     # Incase default isn't set yet.
502     if default_MaxRate == -1:
503         default_MaxRate = 1000000
504
505     try:
506         f = open(datafile, "r+")
507         logger.log("bwmon:  Loading %s" % datafile, 2)
508         (version, slices, deaddb) = pickle.load(f)
509         f.close()
510         # Check version of data file
511         if version != "$Id$":
512             logger.log("bwmon:  Not using old version '%s' data file %s" % (version, datafile))
513             raise Exception
514     except Exception:
515         version = "$Id$"
516         slices = {}
517         deaddb = {}
518
519     # Get/set special slice IDs
520     root_xid = bwlimit.get_xid("root")
521     default_xid = bwlimit.get_xid("default")
522
523     # Since root is required for sanity, its not in the API/plc database, so pass {} 
524     # to use defaults.
525     if root_xid not in slices.keys():
526         slices[root_xid] = Slice(root_xid, "root", {})
527         slices[root_xid].reset({}, {})
528     
529     # Used by bwlimit.  pass {} since there is no rspec (like above).
530     if default_xid not in slices.keys():
531         slices[default_xid] = Slice(default_xid, "default", {})
532         slices[default_xid].reset({}, {})
533
534     live = {}
535     # Get running slivers that should be on this node (from plc). {xid: name}
536     # db keys on name, bwmon keys on xid.  db doesnt have xid either.
537     for plcSliver in nmdbcopy.keys():
538         live[bwlimit.get_xid(plcSliver)] = nmdbcopy[plcSliver]
539
540     logger.log("bwmon:  Found %s instantiated slices" % live.keys().__len__(), 2)
541     logger.log("bwmon:  Found %s slices in dat file" % slices.values().__len__(), 2)
542
543     # Get actual running values from tc.
544     # Update slice totals and bandwidth. {xid: {values}}
545     kernelhtbs = gethtbs(root_xid, default_xid)
546     logger.log("bwmon:  Found %s running HTBs" % kernelhtbs.keys().__len__(), 2)
547
548     # The dat file has HTBs for slices, but the HTBs aren't running
549     nohtbslices =  Set(slices.keys()) - Set(kernelhtbs.keys())
550     logger.log( "bwmon:  Found %s slices in dat but not running." % nohtbslices.__len__(), 2)
551     # Reset tc counts.
552     for nohtbslice in nohtbslices:
553         if live.has_key(nohtbslice): 
554             slices[nohtbslice].reset( {}, live[nohtbslice]['_rspec'] )
555         else:
556             logger.log("bwmon:  Removing abondoned slice %s from dat." % nohtbslice)
557             del slices[nohtbslice]
558
559     # The dat file doesnt have HTB for the slice but kern has HTB
560     slicesnodat = Set(kernelhtbs.keys()) - Set(slices.keys())
561     logger.log( "bwmon:  Found %s slices with HTBs but not in dat" % slicesnodat.__len__(), 2)
562     for slicenodat in slicesnodat:
563         # But slice is running 
564         if live.has_key(slicenodat): 
565             # init the slice.  which means start accounting over since kernel
566             # htb was already there.
567             slices[slicenodat] = Slice(slicenodat, 
568                 live[slicenodat]['name'], 
569                 live[slicenodat]['_rspec'])
570
571     # Get new slices.
572     # Slices in GetSlivers but not running HTBs
573     newslicesxids = Set(live.keys()) - Set(kernelhtbs.keys())
574     logger.log("bwmon:  Found %s new slices" % newslicesxids.__len__(), 2)
575        
576     # Setup new slices
577     for newslice in newslicesxids:
578         # Delegated slices dont have xids (which are uids) since they haven't been
579         # instantiated yet.
580         if newslice != None and live[newslice].has_key('_rspec') == True:
581             # Check to see if we recently deleted this slice.
582             if live[newslice]['name'] not in deaddb.keys():
583                 logger.log( "bwmon: new slice %s" % live[newslice]['name'] )
584                 # _rspec is the computed rspec:  NM retrieved data from PLC, computed loans
585                 # and made a dict of computed values.
586                 slices[newslice] = Slice(newslice, live[newslice]['name'], live[newslice]['_rspec'])
587                 slices[newslice].reset( {}, live[newslice]['_rspec'] )
588             # Double check time for dead slice in deaddb is within 24hr recording period.
589             elif (time.time() <= (deaddb[live[newslice]['name']]['slice'].time + period)):
590                 deadslice = deaddb[live[newslice]['name']]
591                 logger.log("bwmon: Reinstantiating deleted slice %s" % live[newslice]['name'])
592                 slices[newslice] = deadslice['slice']
593                 slices[newslice].xid = newslice
594                 # Start the HTB
595                 newvals = {"maxrate": deadslice['slice'].MaxRate * 1000,
596                             "minrate": deadslice['slice'].MinRate * 1000,
597                             "maxexemptrate": deadslice['slice'].Maxi2Rate * 1000,
598                             "usedbytes": deadslice['htb']['usedbytes'] * 1000,
599                             "usedi2bytes": deadslice['htb']['usedi2bytes'],
600                             "share":deadslice['htb']['share']} 
601                 slices[newslice].reset(newvals, live[newslice]['_rspec'])
602                 # Bring up to date
603                 slices[newslice].update(newvals, live[newslice]['_rspec'])
604                 # Since the slice has been reinitialed, remove from dead database.
605                 del deaddb[deadslice['slice'].name]
606                 del newvals
607         else:
608             logger.log("bwmon:  Slice %s doesn't have xid.  Skipping." % live[newslice]['name'])
609
610     # Move dead slices that exist in the pickle file, but
611     # aren't instantiated by PLC into the dead dict until
612     # recording period is over.  This is to avoid the case where a slice is dynamically created
613     # and destroyed then recreated to get around byte limits.
614     deadxids = Set(slices.keys()) - Set(live.keys())
615     logger.log("bwmon:  Found %s dead slices" % (deadxids.__len__() - 2), 2)
616     for deadxid in deadxids:
617         if deadxid == root_xid or deadxid == default_xid:
618             continue
619         logger.log("bwmon:  removing dead slice %s " % deadxid)
620         if slices.has_key(deadxid) and kernelhtbs.has_key(deadxid):
621             # add slice (by name) to deaddb
622             logger.log("bwmon:  Saving bandwidth totals for %s." % slices[deadxid].name)
623             deaddb[slices[deadxid].name] = {'slice': slices[deadxid], 'htb': kernelhtbs[deadxid]}
624             del slices[deadxid]
625         if kernelhtbs.has_key(deadxid): 
626             logger.log("bwmon:  Removing HTB for %s." % deadxid, 2)
627             bwlimit.off(deadxid)
628     
629     # Clean up deaddb
630     for deadslice in deaddb.keys():
631         if (time.time() >= (deaddb[deadslice]['slice'].time + period)):
632             logger.log("bwmon:  Removing dead slice %s from dat." \
633                         % deaddb[deadslice]['slice'].name)
634             del deaddb[deadslice]
635
636     # Get actual running values from tc since we've added and removed buckets.
637     # Update slice totals and bandwidth. {xid: {values}}
638     kernelhtbs = gethtbs(root_xid, default_xid)
639     logger.log("bwmon:  now %s running HTBs" % kernelhtbs.keys().__len__(), 2)
640
641     # Update all byte limites on all slices
642     for (xid, slice) in slices.iteritems():
643         # Monitor only the specified slices
644         if xid == root_xid or xid == default_xid: continue
645         if names and name not in names:
646             continue
647  
648         if (time.time() >= (slice.time + period)) or \
649             (kernelhtbs[xid]['usedbytes'] < slice.bytes) or \
650             (kernelhtbs[xid]['usedi2bytes'] < slice.i2bytes):
651             # Reset to defaults every 24 hours or if it appears
652             # that the byte counters have overflowed (or, more
653             # likely, the node was restarted or the HTB buckets
654             # were re-initialized).
655             slice.reset(kernelhtbs[xid], live[xid]['_rspec'])
656         elif ENABLE:
657             logger.log("bwmon:  Updating slice %s" % slice.name, 2)
658             # Update byte counts
659             slice.update(kernelhtbs[xid], live[xid]['_rspec'])
660
661     logger.log("bwmon:  Saving %s slices in %s" % (slices.keys().__len__(),datafile), 2)
662     f = open(datafile, "w")
663     pickle.dump((version, slices, deaddb), f)
664     f.close()
665
666 # doesnt use generic default interface because this runs as its own thread.
667 # changing the config variable will not have an effect since GetSlivers: pass
668 def getDefaults(nmdbcopy):
669     '''
670     Get defaults from default slice's slice attributes.
671     '''
672     status = True
673     # default slice
674     dfltslice = nmdbcopy.get(PLC_SLICE_PREFIX+"_default")
675     if dfltslice: 
676         if dfltslice['rspec']['net_max_rate'] == -1:
677             allOff()
678             status = False
679     return status
680
681
682 def allOff():
683     """
684     Turn off all slice HTBs
685     """
686     # Get/set special slice IDs
687     root_xid = bwlimit.get_xid("root")
688     default_xid = bwlimit.get_xid("default")
689     kernelhtbs = gethtbs(root_xid, default_xid)
690     if len(kernelhtbs):
691         logger.log("bwmon:  Disabling all running HTBs.")
692         for htb in kernelhtbs.keys(): bwlimit.off(htb) 
693
694
695 lock = threading.Event()
696 def run():
697     """
698     When run as a thread, wait for event, lock db, deep copy it, release it, 
699     run bwmon.GetSlivers(), then go back to waiting.
700     """
701     logger.log("bwmon:  Thread started", 2)
702     while True:
703         lock.wait()
704         logger.log("bwmon:  Event received.  Running.", 2)
705         database.db_lock.acquire()
706         nmdbcopy = copy.deepcopy(database.db)
707         database.db_lock.release()
708         try:  
709             if getDefaults(nmdbcopy) and len(bwlimit.tc("class show dev %s" % dev_default)) > 0:
710                 # class show to check if net:InitNodeLimit:bwlimit.init has run.
711                 sync(nmdbcopy)
712             else: logger.log("bwmon:  BW limits DISABLED.")
713         except: logger.log_exc("bwmon failed")
714         lock.clear()
715
716 def start(*args):
717     tools.as_daemon_thread(run)
718
719 def GetSlivers(*args):
720     logger.verbose ("bwmon: triggering dummy GetSlivers") 
721     pass