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