5049db212eff1dbc075e22d80a3aa1a85c6cad70
[monitor.git] / monitor_policy.py
1 import config
2 import database
3 import time
4 import mailer
5 from unified_model import cmpCategoryVal
6 import sys
7 import emailTxt
8 import string
9 from monitor.wrapper import plccache
10
11 from rt import is_host_in_rt_tickets
12 import plc
13
14 def get_ticket_id(record):
15         if 'ticket_id' in record and record['ticket_id'] is not "" and record['ticket_id'] is not None:
16                 return record['ticket_id']
17         elif            'found_rt_ticket' in record and \
18                  record['found_rt_ticket'] is not "" and \
19                  record['found_rt_ticket'] is not None:
20                 return record['found_rt_ticket']
21         else:
22                 return None
23
24 # Time to enforce policy
25 POLSLEEP = 7200
26
27 # Where to email the summary
28 SUMTO = "soltesz@cs.princeton.edu"
29 TECHEMAIL="tech-%s@sites.planet-lab.org"
30 PIEMAIL="pi-%s@sites.planet-lab.org"
31 SLICEMAIL="%s@slices.planet-lab.org"
32 PLCEMAIL="support@planet-lab.org"
33
34 #Thresholds (DAYS)
35 SPERMIN = 60
36 SPERHOUR = 60*60
37 SPERDAY = 86400
38 PITHRESH = 7 * SPERDAY
39 SLICETHRESH = 7 * SPERDAY
40 # Days before attempting rins again
41 RINSTHRESH = 5 * SPERDAY
42
43 # Days before calling the node dead.
44 DEADTHRESH = 30 * SPERDAY
45 # Minimum number of nodes up before squeezing
46 MINUP = 2
47
48 TECH=1
49 PI=2
50 USER=4
51 ADMIN=8
52
53 from unified_model import *
54
55 class Merge:
56         def __init__(self, l_merge):
57                 self.merge_list = l_merge
58
59                 # the hostname to loginbase mapping
60                 self.plcdb_hn2lb = plccache.plcdb_hn2lb
61
62                 # Previous actions taken on nodes.
63                 self.act_all = database.if_cached_else(1, "act_all", lambda : {})
64                 self.findbad = database.if_cached_else(1, "findbad", lambda : {})
65
66                 self.cache_all = database.if_cached_else(1, "act_all", lambda : {})
67                 self.sickdb = {}
68                 self.mergedb = {}
69
70         def run(self):
71                 # populate sickdb
72                 self.accumSickSites()
73                 # read data from findbad and act_all
74                 self.mergeActionsAndBadDB()
75                 # pass node_records to RT
76                 return self.getRecordList()
77
78         def accumSickSites(self):
79                 """
80                 Take all nodes, from l_diagnose, look them up in the act_all database, 
81                 and insert them into sickdb[] as:
82
83                         sickdb[loginbase][nodename] = fb_record
84                 """
85                 # look at all problems reported by findbad
86                 l_nodes = self.findbad['nodes'].keys()
87                 count = 0
88                 for nodename in l_nodes:
89                         if nodename not in self.merge_list:
90                                 continue                # skip this node, since it's not wanted
91
92                         count += 1
93                         loginbase = self.plcdb_hn2lb[nodename]
94                         values = self.findbad['nodes'][nodename]['values']
95
96                         fb_record = {}
97                         fb_record['nodename'] = nodename
98                         try:
99                                 fb_record['category'] = values['category']
100                         except:
101                                 print values
102                                 print nodename
103                                 print self.findbad['nodes'][nodename]
104                                 count -= 1
105                                 continue
106                         fb_record['state'] = values['state']
107                         fb_record['comonstats'] = values['comonstats']
108                         fb_record['plcnode'] = values['plcnode']
109                         fb_record['kernel'] = self.getKernel(values['kernel'])
110                         fb_record['stage'] = "findbad"
111                         fb_record['message'] = None
112                         fb_record['bootcd'] = values['bootcd']
113                         fb_record['args'] = None
114                         fb_record['info'] = None
115                         fb_record['time'] = time.time()
116                         fb_record['date_created'] = time.time()
117
118                         if loginbase not in self.sickdb:
119                                 self.sickdb[loginbase] = {}
120
121                         self.sickdb[loginbase][nodename] = fb_record
122
123                 print "Found %d nodes" % count
124
125         def getKernel(self, unamestr):
126                 s = unamestr.split()
127                 if len(s) > 2:
128                         return s[2]
129                 else:
130                         return ""
131
132         def mergeActionsAndBadDB(self): 
133                 """
134                 - Look at the sick node_records as reported in findbad, 
135                 - Then look at the node_records in act_all.  
136
137                 There are four cases:
138                 1) Problem in findbad, no problem in act_all
139                         this ok, b/c it just means it's a new problem
140                 2) Problem in findbad, problem in act_all
141                         -Did the problem get better or worse?  
142                                 -If Same, or Worse, then continue looking for open tickets.
143                                 -If Better, or No problem, then "back-off" penalties.
144                                         This judgement may need to wait until 'Diagnose()'
145
146                 3) No problem in findbad, problem in act_all
147                         The the node is operational again according to Findbad()
148
149                 4) No problem in findbad, no problem in act_all
150                         There won't be a record in either db, so there's no code.
151                 """
152
153                 sorted_sites = self.sickdb.keys()
154                 sorted_sites.sort()
155                 # look at all problems reported by findbad
156                 for loginbase in sorted_sites:
157                         d_fb_nodes = self.sickdb[loginbase]
158                         sorted_nodes = d_fb_nodes.keys()
159                         sorted_nodes.sort()
160                         for nodename in sorted_nodes:
161                                 fb_record = self.sickdb[loginbase][nodename]
162                                 x = fb_record
163                                 if loginbase not in self.mergedb:
164                                         self.mergedb[loginbase] = {}
165
166                                 # take the info either from act_all or fb-record.
167                                 # if node not in act_all
168                                 #       then take it from fbrecord, obviously.
169                                 # else node in act_all
170                                 #   if act_all == 0 length (no previous records)
171                                 #               then take it from fbrecord.
172                                 #   else
173                                 #           take it from act_all.
174                                 #   
175
176                                 # We must compare findbad state with act_all state
177                                 if nodename not in self.act_all:
178                                         # 1) ok, b/c it's a new problem. set ticket_id to null
179                                         self.mergedb[loginbase][nodename] = {} 
180                                         self.mergedb[loginbase][nodename].update(x)
181                                         self.mergedb[loginbase][nodename]['ticket_id'] = ""
182                                         self.mergedb[loginbase][nodename]['prev_category'] = "NORECORD" 
183                                 else: 
184                                         if len(self.act_all[nodename]) == 0:
185                                                 self.mergedb[loginbase][nodename] = {} 
186                                                 self.mergedb[loginbase][nodename].update(x)
187                                                 self.mergedb[loginbase][nodename]['ticket_id'] = ""
188                                                 self.mergedb[loginbase][nodename]['prev_category'] = "NORECORD" 
189                                         else:
190                                                 y = self.act_all[nodename][0]
191                                                 y['prev_category'] = y['category']
192
193                                                 self.mergedb[loginbase][nodename] = {}
194                                                 self.mergedb[loginbase][nodename].update(y)
195                                                 self.mergedb[loginbase][nodename]['comonstats'] = x['comonstats']
196                                                 self.mergedb[loginbase][nodename]['category']   = x['category']
197                                                 self.mergedb[loginbase][nodename]['state'] = x['state']
198                                                 self.mergedb[loginbase][nodename]['kernel']=x['kernel']
199                                                 self.mergedb[loginbase][nodename]['bootcd']=x['bootcd']
200                                                 self.mergedb[loginbase][nodename]['plcnode']=x['plcnode']
201                                                 ticket = get_ticket_id(self.mergedb[loginbase][nodename])
202                                                 self.mergedb[loginbase][nodename]['rt'] = mailer.getTicketStatus(ticket)
203
204                                         # delete the entry from cache_all to keep it out of case 3)
205                                         del self.cache_all[nodename]
206
207                 # 3) nodes that remin in cache_all were not identified by findbad.
208                 #        Do we keep them or not?
209                 #   NOTE: i think that since the categories are performed before this
210                 #               step now, and by a monitor-controlled agent.
211
212                 return
213
214         def getRecordList(self):
215                 sorted_sites = self.mergedb.keys()
216                 sorted_sites.sort()
217                 ret_list = []
218
219                 # look at all problems reported by merge
220                 for loginbase in sorted_sites:
221                         d_merge_nodes = self.mergedb[loginbase]
222                         for nodename in d_merge_nodes.keys():
223                                 record = self.mergedb[loginbase][nodename]
224                                 ret_list.append(record)
225
226                 return ret_list
227
228 class RT:
229         def __init__(self, record_list, dbTickets, l_ticket_blacklist, target = None): 
230                 # Time of last update of ticket DB
231                 self.record_list = record_list
232                 self.dbTickets = dbTickets
233                 self.lastupdated = 0
234                 self.l_ticket_blacklist = l_ticket_blacklist
235                 self.tickets = {}
236
237         def run(self):
238                 self.count = 0
239                 ret_list = []
240                 for diag_node in self.record_list:
241                         if diag_node != None: 
242                                 host = diag_node['nodename']
243                                 (b_host_inticket, r_ticket) = is_host_in_rt_tickets(host, \
244                                                                                                         self.l_ticket_blacklist, \
245                                                                                                         self.dbTickets)
246                                 diag_node['found_rt_ticket'] = None
247                                 if b_host_inticket:
248                                         #logger.debug("RT: found tickets for %s" %host)
249                                         diag_node['found_rt_ticket'] = r_ticket['ticket_id']
250
251                                 else:
252                                         if r_ticket is not None:
253                                                 print "Ignoring ticket %s" % r_ticket['ticket_id']
254                                                 # TODO: why do i return the ticket id for a
255                                                 #               blacklisted ticket id?
256                                                 #diag_node['found_rt_ticket'] = r_ticket['ticket_id']
257                                         self.count = self.count + 1
258
259                                 ret_list.append(diag_node)
260
261                 #print "RT processed %d nodes with noticket" % self.count
262                 #logger.debug("RT filtered %d noticket nodes" % self.count)
263                 return ret_list
264
265 class Diagnose:
266         def __init__(self, record_list):
267                 self.record_list = record_list
268                 self.plcdb_hn2lb = plccache.plcdb_hn2lb
269                 self.findbad = database.if_cached_else(1, "findbad", lambda : {})
270
271                 self.diagnose_in = {}
272                 self.diagnose_out = {}
273
274         def run(self):
275                 self.accumSickSites()
276
277                 #logger.debug("Accumulated %d sick sites" % len(self.diagnose_in.keys()))
278
279                 try:
280                         stats = self.diagnoseAll()
281                 except Exception, err:
282                         print "----------------"
283                         import traceback
284                         print traceback.print_exc()
285                         print err
286                         #if config.policysavedb:
287                         sys.exit(1)
288
289                 #print_stats("sites_observed", stats)
290                 #print_stats("sites_diagnosed", stats)
291                 #print_stats("nodes_diagnosed", stats)
292
293                 return self.diagnose_out
294
295         def accumSickSites(self):
296                 """
297                 Take all nodes, from l_diagnose, look them up in the diagnose_out database, 
298                 and insert them into diagnose_in[] as:
299
300                         diagnose_in[loginbase] = [diag_node1, diag_node2, ...]
301                 """
302                 for node_record in self.record_list:
303
304                         nodename = node_record['nodename']
305                         loginbase = self.plcdb_hn2lb[nodename]
306
307                         if loginbase not in self.diagnose_in:
308                                 self.diagnose_in[loginbase] = {}
309
310                         self.diagnose_in[loginbase][nodename] = node_record
311
312                 return
313
314         def diagnoseAll(self):
315                 i_sites_observed = 0
316                 i_sites_diagnosed = 0
317                 i_nodes_diagnosed = 0
318                 i_nodes_actedon = 0
319                 i_sites_emailed = 0
320                 l_allsites = []
321
322                 sorted_sites = self.diagnose_in.keys()
323                 sorted_sites.sort()
324                 self.diagnose_out= {}
325                 for loginbase in sorted_sites:
326                         l_allsites += [loginbase]
327
328                         d_diag_nodes = self.diagnose_in[loginbase]
329                         d_act_records = self.__diagnoseSite(loginbase, d_diag_nodes)
330                         # store records in diagnose_out, for saving later.
331                         self.diagnose_out.update(d_act_records)
332                         
333                         if len(d_act_records[loginbase]['nodes'].keys()) > 0:
334                                 i_nodes_diagnosed += (len(d_act_records[loginbase]['nodes'].keys()))
335                                 i_sites_diagnosed += 1
336                         i_sites_observed += 1
337
338                 return {'sites_observed': i_sites_observed, 
339                                 'sites_diagnosed': i_sites_diagnosed, 
340                                 'nodes_diagnosed': i_nodes_diagnosed, 
341                                 'allsites':l_allsites}
342
343                 pass
344                 
345         def __getDaysDown(self, diag_record, nodename):
346                 daysdown = -1
347                 if diag_record['comonstats']['sshstatus'] != "null":
348                         daysdown = int(diag_record['comonstats']['sshstatus']) // (60*60*24)
349                 elif diag_record['comonstats']['lastcotop'] != "null":
350                         daysdown = int(diag_record['comonstats']['lastcotop']) // (60*60*24)
351                 else:
352                         now = time.time()
353                         last_contact = diag_record['plcnode']['last_contact']
354                         if last_contact == None:
355                                 # the node has never been up, so give it a break
356                                 daysdown = -1
357                         else:
358                                 diff = now - last_contact
359                                 daysdown = diff // (60*60*24)
360                 return daysdown
361
362         def __getStrDaysDown(self, diag_record, nodename):
363                 daysdown = self.__getDaysDown(diag_record, nodename)
364                 if daysdown > 0:
365                         return "(%d days down)"%daysdown
366                 else:
367                         return "Unknown number of days"
368
369         def __getCDVersion(self, diag_record, nodename):
370                 cdversion = ""
371                 #print "Getting kernel for: %s" % diag_record['nodename']
372                 cdversion = diag_record['kernel']
373                 return cdversion
374
375         def __diagnoseSite(self, loginbase, d_diag_nodes):
376                 """
377                 d_diag_nodes are diagnose_in entries.
378                 """
379                 d_diag_site = {loginbase : { 'config' : 
380                                                                                                 {'squeeze': False,
381                                                                                                  'email': False
382                                                                                                 }, 
383                                                                         'nodes': {}
384                                                                         }
385                                            }
386                 sorted_nodes = d_diag_nodes.keys()
387                 sorted_nodes.sort()
388                 for nodename in sorted_nodes:
389                         node_record = d_diag_nodes[nodename]
390                         diag_record = self.__diagnoseNode(loginbase, node_record)
391
392                         if diag_record != None:
393                                 d_diag_site[loginbase]['nodes'][nodename] = diag_record
394
395                                 # NOTE: improvement means, we need to act/squeeze and email.
396                                 #print "DIAG_RECORD", diag_record
397                                 if 'monitor-end-record' in diag_record['stage'] or \
398                                    'nmreset' in diag_record['stage']:
399                                 #       print "resetting loginbase!" 
400                                         d_diag_site[loginbase]['config']['squeeze'] = True
401                                         d_diag_site[loginbase]['config']['email'] = True
402                                 #else:
403                                 #       print "NO IMPROVEMENT!!!!"
404                         else:
405                                 pass # there is nothing to do for this node.
406
407                 # NOTE: these settings can be overridden by command line arguments,
408                 #       or the state of a record, i.e. if already in RT's Support Queue.
409                 pf = PersistFlags(loginbase, 1, db='site_persistflags')
410                 nodes_up = pf.nodes_up
411                 if nodes_up < MINUP:
412                         d_diag_site[loginbase]['config']['squeeze'] = True
413
414                 max_slices = self.getMaxSlices(loginbase)
415                 num_nodes = pf.nodes_total #self.getNumNodes(loginbase)
416                 # NOTE: when max_slices == 0, this is either a new site (the old way)
417                 #       or an old disabled site from previous monitor (before site['enabled'])
418                 if nodes_up < num_nodes and max_slices != 0:
419                         d_diag_site[loginbase]['config']['email'] = True
420
421                 if len(d_diag_site[loginbase]['nodes'].keys()) > 0:
422                         print "SITE: %20s : %d nodes up, at most" % (loginbase, nodes_up)
423
424                 return d_diag_site
425
426         def diagRecordByCategory(self, node_record):
427                 nodename = node_record['nodename']
428                 category = node_record['category']
429                 state    = node_record['state']
430                 loginbase = self.plcdb_hn2lb[nodename]
431                 diag_record = None
432
433                 if  "ERROR" in category:        # i.e. "DOWN"
434                         diag_record = {}
435                         diag_record.update(node_record)
436                         daysdown = self.__getDaysDown(diag_record, nodename) 
437                         #if daysdown < 7:
438                         #       format = "DIAG: %20s : %-40s Down only %s days  NOTHING DONE"
439                         #       print format % (loginbase, nodename, daysdown)
440                         #       return None
441
442                         s_daysdown = self.__getStrDaysDown(diag_record, nodename)
443                         diag_record['message'] = emailTxt.mailtxt.newdown
444                         diag_record['args'] = {'nodename': nodename}
445                         diag_record['info'] = (nodename, s_daysdown, "")
446
447                         #if 'reboot_node_failed' in node_record:
448                         #       # there was a previous attempt to use the PCU.
449                         #       if node_record['reboot_node_failed'] == False:
450                         #               # then the last attempt apparently, succeeded.
451                         #               # But, the category is still 'ERROR'.  Therefore, the
452                         #               # PCU-to-Node mapping is broken.
453                         #               #print "Setting message for ERROR node to PCU2NodeMapping: %s" % nodename
454                         #               diag_record['message'] = emailTxt.mailtxt.pcutonodemapping
455                         #               diag_record['email_pcu'] = True
456
457                         if diag_record['ticket_id'] == "":
458                                 diag_record['log'] = "DOWN: %20s : %-40s == %20s %s" % \
459                                         (loginbase, nodename, diag_record['info'][1:], diag_record['found_rt_ticket'])
460                         else:
461                                 diag_record['log'] = "DOWN: %20s : %-40s == %20s %s" % \
462                                         (loginbase, nodename, diag_record['info'][1:], diag_record['ticket_id'])
463
464                 elif "OLDBOOTCD" in category:
465                         # V2 boot cds as determined by findbad
466                         s_daysdown = self.__getStrDaysDown(node_record, nodename)
467                         s_cdversion = self.__getCDVersion(node_record, nodename)
468                         diag_record = {}
469                         diag_record.update(node_record)
470                         #if "2.4" in diag_record['kernel'] or "v2" in diag_record['bootcd']:
471                         diag_record['message'] = emailTxt.mailtxt.newbootcd
472                         diag_record['args'] = {'nodename': nodename}
473                         diag_record['info'] = (nodename, s_daysdown, s_cdversion)
474                         if diag_record['ticket_id'] == "":
475                                 diag_record['log'] = "BTCD: %20s : %-40s == %20s %20s %s" % \
476                                                                         (loginbase, nodename, diag_record['kernel'], 
477                                                                          diag_record['bootcd'], diag_record['found_rt_ticket'])
478                         else:
479                                 diag_record['log'] = "BTCD: %20s : %-40s == %20s %20s %s" % \
480                                                                         (loginbase, nodename, diag_record['kernel'], 
481                                                                          diag_record['bootcd'], diag_record['ticket_id'])
482
483                 elif "PROD" in category:
484                         if "DEBUG" in state:
485                                 # Not sure what to do with these yet.  Probably need to
486                                 # reboot, and email.
487                                 print "DEBG: %20s : %-40s  NOTHING DONE" % (loginbase, nodename)
488                                 return None
489                         elif "BOOT" in state:
490                                 # no action needed.
491                                 # TODO: remove penalties, if any are applied.
492                                 now = time.time()
493                                 last_contact = node_record['plcnode']['last_contact']
494                                 if last_contact == None:
495                                         time_diff = 0
496                                 else:
497                                         time_diff = now - last_contact;
498
499                                 if 'improvement' in node_record['stage']:
500                                         # then we need to pass this on to 'action'
501                                         diag_record = {}
502                                         diag_record.update(node_record)
503                                         diag_record['message'] = emailTxt.mailtxt.newthankyou
504                                         diag_record['args'] = {'nodename': nodename}
505                                         diag_record['info'] = (nodename, node_record['prev_category'], 
506                                                                                                          node_record['category'])
507                                         #if 'email_pcu' in diag_record:
508                                         #       if diag_record['email_pcu']:
509                                         #               # previously, the pcu failed to reboot, so send
510                                         #               # email. Now, reset these values to try the reboot
511                                         #               # again.
512                                         #               diag_record['email_pcu'] = False
513                                         #               del diag_record['reboot_node_failed']
514
515                                         if diag_record['ticket_id'] == "":
516                                                 diag_record['log'] = "IMPR: %20s : %-40s == %20s %20s %s %s" % \
517                                                                         (loginbase, nodename, diag_record['stage'], 
518                                                                          state, category, diag_record['found_rt_ticket'])
519                                         else:
520                                                 diag_record['log'] = "IMPR: %20s : %-40s == %20s %20s %s %s" % \
521                                                                         (loginbase, nodename, diag_record['stage'], 
522                                                                          state, category, diag_record['ticket_id'])
523                                         return diag_record
524                                 #elif time_diff >= 6*SPERHOUR:
525                                 #       # heartbeat is older than 30 min.
526                                 #       # then reset NM.
527                                 #       #print "Possible NM problem!! %s - %s = %s" % (now, last_contact, time_diff)
528                                 #       diag_record = {}
529                                 #       diag_record.update(node_record)
530                                 #       diag_record['message'] = emailTxt.mailtxt.NMReset
531                                 #       diag_record['args'] = {'nodename': nodename}
532                                 #       diag_record['stage'] = "nmreset"
533                                 #       diag_record['info'] = (nodename, 
534                                 #                                                       node_record['prev_category'], 
535                                 #                                                       node_record['category'])
536                                 #       if diag_record['ticket_id'] == "":
537                                 #               diag_record['log'] = "NM  : %20s : %-40s == %20s %20s %s %s" % \
538                                 #                                       (loginbase, nodename, diag_record['stage'], 
539                                 #                                        state, category, diag_record['found_rt_ticket'])
540                                 #       else:
541                                 #               diag_record['log'] = "NM  : %20s : %-40s == %20s" % \
542                                 #                                       (loginbase, nodename, diag_record['stage'])
543 #
544 #                                       return diag_record
545                                 else:
546                                         return None
547                         else:
548                                 # unknown
549                                 pass
550                 elif "ALPHA"    in category:
551                         pass
552                 elif "clock_drift" in category:
553                         pass
554                 elif "dns"    in category:
555                         pass
556                 elif "filerw"    in category:
557                         pass
558                 else:
559                         print "Unknown category!!!! %s" % category
560                         sys.exit(1)
561
562                 return diag_record
563
564         def __diagnoseNode(self, loginbase, node_record):
565                 # TODO: change the format of the hostname in this 
566                 #               record to something more natural.
567                 nodename                = node_record['nodename']
568                 category                = node_record['category']
569                 prev_category   = node_record['prev_category']
570                 state                   = node_record['state']
571                 #if 'prev_category' in node_record:
572                 #       prev_category = node_record['prev_category']
573                 #else:
574                 #       prev_category = "ERROR"
575                 if node_record['prev_category'] != "NORECORD":
576                 
577                         val = cmpCategoryVal(category, prev_category)
578                         print "%s went from %s -> %s" % (nodename, prev_category, category)
579                         if prev_category == "UNKNOWN" and category == "PROD":
580                                 # sending too many thank you notes to people that don't
581                                 # deserve them.
582                                 # TODO: not sure what effect this will have on the node
583                                 # status, though...
584                                 return None
585
586                         if val == 1:
587                                 # improved
588                                 if node_record['ticket_id'] == "" or node_record['ticket_id'] == None:
589                                         print "closing record with no ticket: ", node_record['nodename']
590                                         node_record['action'] = ['close_rt']
591                                         node_record['message'] = None
592                                         node_record['stage'] = 'monitor-end-record'
593                                         return node_record
594                                 else:
595                                         node_record['stage'] = 'improvement'
596
597                                 #if 'monitor-end-record' in node_record['stage']:
598                                 #       # just ignore it if it's already ended.
599                                 #       # otherwise, the status should be worse, and we won't get
600                                 #       # here.
601                                 #       print "monitor-end-record: ignoring ", node_record['nodename']
602                                 #       return None
603 #
604 #                                       #return None
605                         elif val == -1:
606                                 # current category is worse than previous, carry on
607                                 pass
608                         else:
609                                 #values are equal, carry on.
610                                 #print "why are we here?"
611                                 pass
612
613                 if 'rt' in node_record and 'Status' in node_record['rt']:
614                         if node_record['stage'] == 'ticket_waitforever':
615                                 if 'resolved' in node_record['rt']['Status']:
616                                         print "ending waitforever record for: ", node_record['nodename']
617                                         node_record['action'] = ['noop']
618                                         node_record['message'] = None
619                                         node_record['stage'] = 'monitor-end-record'
620                                         print "oldlog: %s" % node_record['log'],
621                                         print "%15s" % node_record['action']
622                                         return node_record
623                                 if 'new' in node_record['rt']['Status'] and \
624                                         'Queue' in node_record['rt'] and \
625                                         'Monitor' in node_record['rt']['Queue']:
626
627                                         print "RESETTING stage to findbad"
628                                         node_record['stage'] = 'findbad'
629                         
630                 #### COMPARE category and prev_category
631                 # if not_equal
632                 #       then assign a stage based on relative priorities
633                 # else equal
634                 #       then check category for stats.
635                 diag_record = self.diagRecordByCategory(node_record)
636                 if diag_record == None:
637                         #print "diag_record == None"
638                         return None
639
640                 #### found_RT_ticket
641                 # TODO: need to record time found, and maybe add a stage for acting on it...
642                 # NOTE: after found, if the support ticket is resolved, the block is
643                 #               not removed. How to remove the block on this?
644
645                 #if 'found_rt_ticket' in diag_record and \
646                 #       diag_record['found_rt_ticket'] is not None:
647                 #       if diag_record['stage'] is not 'improvement':
648                 #               diag_record['stage'] = 'ticket_waitforever'
649                                 
650                 current_time = time.time()
651                 # take off four days, for the delay that database caused.
652                 # TODO: generalize delays at PLC, and prevent enforcement when there
653                 #               have been no emails.
654                 # NOTE: 7*SPERDAY exists to offset the 'bad week'
655                 #delta = current_time - diag_record['time'] - 7*SPERDAY
656                 delta = current_time - diag_record['time']
657
658                 message = diag_record['message']
659                 act_record = {}
660                 act_record.update(diag_record)
661
662                 #### DIAGNOSE STAGES 
663                 if   'findbad' in diag_record['stage']:
664                         # The node is bad, and there's no previous record of it.
665                         act_record['email'] = TECH
666                         act_record['action'] = ['noop']
667                         act_record['message'] = message[0]
668                         act_record['stage'] = 'stage_actinoneweek'
669
670                 elif 'nmreset' in diag_record['stage']:
671                         act_record['email']  = ADMIN 
672                         act_record['action'] = ['reset_nodemanager']
673                         act_record['message'] = message[0]
674                         act_record['stage']  = 'nmreset'
675                         return None
676
677                 elif 'reboot_node' in diag_record['stage']:
678                         act_record['email'] = TECH
679                         act_record['action'] = ['noop']
680                         act_record['message'] = message[0]
681                         act_record['stage'] = 'stage_actinoneweek'
682                         
683                 elif 'improvement' in diag_record['stage']:
684                         # - backoff previous squeeze actions (slice suspend, nocreate)
685                         # TODO: add a backoff_squeeze section... Needs to runthrough
686                         print "backing off of %s" % nodename
687                         act_record['action'] = ['close_rt']
688                         act_record['message'] = message[0]
689                         act_record['stage'] = 'monitor-end-record'
690
691                 elif 'actinoneweek' in diag_record['stage']:
692                         if delta >= 7 * SPERDAY: 
693                                 act_record['email'] = TECH | PI
694                                 act_record['stage'] = 'stage_actintwoweeks'
695                                 act_record['message'] = message[1]
696                                 act_record['action'] = ['nocreate' ]
697                                 act_record['time'] = current_time               # reset clock for waitforever
698                         elif delta >= 3* SPERDAY and not 'second-mail-at-oneweek' in act_record:
699                                 act_record['email'] = TECH 
700                                 act_record['message'] = message[0]
701                                 act_record['action'] = ['sendmailagain-waitforoneweekaction' ]
702                                 act_record['second-mail-at-oneweek'] = True
703                         else:
704                                 act_record['message'] = None
705                                 act_record['action'] = ['waitforoneweekaction' ]
706                                 print "ignoring this record for: %s" % act_record['nodename']
707                                 return None                     # don't send if there's no action
708
709                 elif 'actintwoweeks' in diag_record['stage']:
710                         if delta >= 7 * SPERDAY:
711                                 act_record['email'] = TECH | PI | USER
712                                 act_record['stage'] = 'stage_waitforever'
713                                 act_record['message'] = message[2]
714                                 act_record['action'] = ['suspendslices']
715                                 act_record['time'] = current_time               # reset clock for waitforever
716                         elif delta >= 3* SPERDAY and not 'second-mail-at-twoweeks' in act_record:
717                                 act_record['email'] = TECH | PI
718                                 act_record['message'] = message[1]
719                                 act_record['action'] = ['sendmailagain-waitfortwoweeksaction' ]
720                                 act_record['second-mail-at-twoweeks'] = True
721                         else:
722                                 act_record['message'] = None
723                                 act_record['action'] = ['waitfortwoweeksaction']
724                                 return None                     # don't send if there's no action
725
726                 elif 'ticket_waitforever' in diag_record['stage']:
727                         act_record['email'] = TECH
728                         if 'first-found' not in act_record:
729                                 act_record['first-found'] = True
730                                 act_record['log'] += " firstfound"
731                                 act_record['action'] = ['ticket_waitforever']
732                                 act_record['message'] = None
733                                 act_record['time'] = current_time
734                         else:
735                                 if delta >= 7*SPERDAY:
736                                         act_record['action'] = ['ticket_waitforever']
737                                         act_record['message'] = None
738                                         act_record['time'] = current_time               # reset clock
739                                 else:
740                                         act_record['action'] = ['ticket_waitforever']
741                                         act_record['message'] = None
742                                         return None
743
744                 elif 'waitforever' in diag_record['stage']:
745                         # more than 3 days since last action
746                         # TODO: send only on weekdays.
747                         # NOTE: expects that 'time' has been reset before entering waitforever stage
748                         if delta >= 3*SPERDAY:
749                                 act_record['action'] = ['email-againwaitforever']
750                                 act_record['message'] = message[2]
751                                 act_record['time'] = current_time               # reset clock
752                         else:
753                                 act_record['action'] = ['waitforever']
754                                 act_record['message'] = None
755                                 return None                     # don't send if there's no action
756
757                 else:
758                         # There is no action to be taken, possibly b/c the stage has
759                         # already been performed, but diagnose picked it up again.
760                         # two cases, 
761                         #       1. stage is unknown, or 
762                         #       2. delta is not big enough to bump it to the next stage.
763                         # TODO: figure out which. for now assume 2.
764                         print "UNKNOWN stage for %s; nothing done" % nodename
765                         act_record['action'] = ['unknown']
766                         act_record['message'] = message[0]
767
768                         act_record['email'] = TECH
769                         act_record['action'] = ['noop']
770                         act_record['message'] = message[0]
771                         act_record['stage'] = 'stage_actinoneweek'
772                         act_record['time'] = current_time               # reset clock
773                         #print "Exiting..."
774                         #return None
775                         #sys.exit(1)
776
777                 print "%s" % act_record['log'],
778                 print "%15s" % act_record['action']
779                 return act_record
780
781         def getMaxSlices(self, loginbase):
782                 # if sickdb has a loginbase, then it will have at least one node.
783                 site_stats = None
784
785                 for nodename in self.diagnose_in[loginbase].keys():
786                         if nodename in self.findbad['nodes']:
787                                 site_stats = self.findbad['nodes'][nodename]['values']['plcsite']
788                                 break
789
790                 if site_stats == None:
791                         raise Exception, "loginbase with no nodes in findbad"
792                 else:
793                         return site_stats['max_slices']
794
795         def getNumNodes(self, loginbase):
796                 # if sickdb has a loginbase, then it will have at least one node.
797                 site_stats = None
798
799                 for nodename in self.diagnose_in[loginbase].keys():
800                         if nodename in self.findbad['nodes']:
801                                 site_stats = self.findbad['nodes'][nodename]['values']['plcsite']
802                                 break
803
804                 if site_stats == None:
805                         raise Exception, "loginbase with no nodes in findbad"
806                 else:
807                         return site_stats['num_nodes']
808
809         """
810         Returns number of up nodes as the total number *NOT* in act_all with a
811         stage other than 'steady-state' .
812         """
813         def getUpAtSite(self, loginbase, d_diag_site):
814                 # TODO: THIS DOESN"T WORK!!! it misses all the 'debug' state nodes
815                 #               that aren't recorded yet.
816
817                 numnodes = self.getNumNodes(loginbase)
818                 # NOTE: assume nodes we have no record of are ok. (too conservative)
819                 # TODO: make the 'up' value more representative
820                 up = numnodes
821                 for nodename in d_diag_site[loginbase]['nodes'].keys():
822
823                         rec = d_diag_site[loginbase]['nodes'][nodename]
824                         if rec['stage'] != 'monitor-end-record':
825                                 up -= 1
826                         else:
827                                 pass # the node is assumed to be up.
828
829                 #if up != numnodes:
830                 #       print "ERROR: %s total nodes up and down != %d" % (loginbase, numnodes)
831
832                 return up
833
834 def close_rt_backoff(args):
835         if 'ticket_id' in args and (args['ticket_id'] != "" and args['ticket_id'] != None):
836                 mailer.closeTicketViaRT(args['ticket_id'], 
837                                                                 "Ticket CLOSED automatically by SiteAssist.")
838                 plc.enableSlices(args['hostname'])
839                 plc.enableSliceCreation(args['hostname'])
840         return
841
842 def reboot_node(args):
843         host = args['hostname']
844         return reboot.reboot_policy(host, True, config.debug)
845
846 class Action:
847         def __init__(self, diagnose_out):
848                 # the hostname to loginbase mapping
849                 self.plcdb_hn2lb = plccache.plcdb_hn2lb
850
851                 # Actions to take.
852                 self.diagnose_db = diagnose_out
853                 # Actions taken.
854                 self.act_all   = database.if_cached_else(1, "act_all", lambda : {})
855
856                 # A dict of actions to specific functions. PICKLE doesnt' like lambdas.
857                 self.actions = {}
858                 self.actions['suspendslices'] = lambda args: plc.suspendSlices(args['hostname'])
859                 self.actions['nocreate'] = lambda args: plc.removeSliceCreation(args['hostname'])
860                 self.actions['close_rt'] = lambda args: close_rt_backoff(args)
861                 self.actions['rins'] = lambda args: plc.nodeBootState(args['hostname'], "rins") 
862                 self.actions['noop'] = lambda args: args
863                 self.actions['reboot_node'] = lambda args: reboot_node(args)
864                 self.actions['reset_nodemanager'] = lambda args: args # reset_nodemanager(args)
865
866                 self.actions['ticket_waitforever'] = lambda args: args
867                 self.actions['waitforever'] = lambda args: args
868                 self.actions['unknown'] = lambda args: args
869                 self.actions['waitforoneweekaction'] = lambda args: args
870                 self.actions['waitfortwoweeksaction'] = lambda args: args
871                 self.actions['sendmailagain-waitforoneweekaction'] = lambda args: args
872                 self.actions['sendmailagain-waitfortwoweeksaction'] = lambda args: args
873                 self.actions['email-againwaitforever'] = lambda args: args
874                 self.actions['email-againticket_waitforever'] = lambda args: args
875                                 
876                 self.sickdb = {}
877
878         def run(self):
879                 self.accumSites()
880                 #logger.debug("Accumulated %d sick sites" % len(self.sickdb.keys()))
881
882                 try:
883                         stats = self.analyseSites()
884                 except Exception, err:
885                         print "----------------"
886                         import traceback
887                         print traceback.print_exc()
888                         print err
889                         if config.policysavedb:
890                                 print "Saving Databases... act_all"
891                                 database.dbDump("act_all", self.act_all)
892                                 database.dbDump("diagnose_out", self.diagnose_db)
893                         sys.exit(1)
894
895                 #print_stats("sites_observed", stats)
896                 #print_stats("sites_diagnosed", stats)
897                 #print_stats("nodes_diagnosed", stats)
898                 self.print_stats("sites_emailed", stats)
899                 #print_stats("nodes_actedon", stats)
900                 print string.join(stats['allsites'], ",")
901
902                 if config.policysavedb:
903                         print "Saving Databases... act_all"
904                         #database.dbDump("policy.eventlog", self.eventlog)
905                         # TODO: remove 'diagnose_out', 
906                         #       or at least the entries that were acted on.
907                         database.dbDump("act_all", self.act_all)
908                         database.dbDump("diagnose_out", self.diagnose_db)
909
910         def accumSites(self):
911                 """
912                 Take all nodes, from l_action, look them up in the diagnose_db database, 
913                 and insert them into sickdb[] as:
914
915                 This way only the given l_action nodes will be acted on regardless
916                 of how many from diagnose_db are available.
917
918                         sickdb[loginbase][nodename] = diag_record
919                 """
920                 self.sickdb = self.diagnose_db
921
922         def __emailSite(self, loginbase, roles, message, args):
923                 """
924                 loginbase is the unique site abbreviation, prepended to slice names.
925                 roles contains TECH, PI, USER roles, and derive email aliases.
926                 record contains {'message': [<subj>,<body>], 'args': {...}} 
927                 """
928                 ticket_id = 0
929                 args.update({'loginbase':loginbase})
930
931                 if not config.mail and not config.debug and config.bcc:
932                         roles = ADMIN
933                 if config.mail and config.debug:
934                         roles = ADMIN
935
936                 # build targets
937                 contacts = []
938                 if ADMIN & roles:
939                         contacts += [config.email]
940                 if TECH & roles:
941                         #contacts += [TECHEMAIL % loginbase]
942                         contacts += plc.getTechEmails(loginbase)
943                 if PI & roles:
944                         #contacts += [PIEMAIL % loginbase]
945                         contacts += plc.getPIEmails(loginbase)
946                 if USER & roles:
947                         contacts += plc.getSliceUserEmails(loginbase)
948                         slices = plc.slices(loginbase)
949                         if len(slices) >= 1:
950                                 print "SLIC: %20s : %d slices" % (loginbase, len(slices))
951                         else:
952                                 print "SLIC: %20s : 0 slices" % loginbase
953
954                 unique_contacts = set(contacts)
955                 contacts = [ c for c in unique_contacts ]       # convert back into list
956
957                 try:
958                         subject = message[0] % args
959                         body = message[1] % args
960                         if ADMIN & roles:
961                                 # send only to admin
962                                 if 'ticket_id' in args:
963                                         subj = "Re: [PL #%s] %s" % (args['ticket_id'], subject)
964                                 else:
965                                         subj = "Re: [PL noticket] %s" % subject
966                                 mailer.email(subj, body, contacts)
967                                 ticket_id = args['ticket_id']
968                         else:
969                                 ticket_id = mailer.emailViaRT(subject, body, contacts, args['ticket_id'])
970                 except Exception, err:
971                         print "exception on message:"
972                         import traceback
973                         print traceback.print_exc()
974                         print message
975
976                 return ticket_id
977
978
979         def _format_diaginfo(self, diag_node):
980                 info = diag_node['info']
981                 if diag_node['stage'] == 'monitor-end-record':
982                         hlist = "    %s went from '%s' to '%s'\n" % (info[0], info[1], info[2]) 
983                 else:
984                         hlist = "    %s %s - %s\n" % (info[0], info[2], info[1]) #(node,ver,daysdn)
985                 return hlist
986
987
988         def get_email_args(self, act_recordlist, loginbase=None):
989
990                 email_args = {}
991                 email_args['hostname_list'] = ""
992                 email_args['url_list'] = ""
993
994                 for act_record in act_recordlist:
995                         email_args['hostname_list'] += act_record['msg_format']
996                         email_args['hostname'] = act_record['nodename']
997                         email_args['url_list'] += "\thttp://boot2.planet-lab.org/premade-bootcd-alpha/iso/%s.iso\n"
998                         email_args['url_list'] += "\thttp://boot2.planet-lab.org/premade-bootcd-alpha/usb/%s.usb\n"
999                         email_args['url_list'] += "\n"
1000                         if  'plcnode' in act_record and \
1001                                 'pcu_ids' in act_record['plcnode'] and \
1002                                 len(act_record['plcnode']['pcu_ids']) > 0:
1003                                 print "setting 'pcu_id' for email_args %s"%email_args['hostname']
1004                                 email_args['pcu_id'] = act_record['plcnode']['pcu_ids'][0]
1005                         else:
1006                                 email_args['pcu_id'] = "-1"
1007                                         
1008                         if 'ticket_id' in act_record:
1009                                 if act_record['ticket_id'] == 0 or act_record['ticket_id'] == '0':
1010                                         print "Enter the ticket_id for %s @ %s" % (loginbase, act_record['nodename'])
1011                                         sys.stdout.flush()
1012                                         line = sys.stdin.readline()
1013                                         try:
1014                                                 ticket_id = int(line)
1015                                         except:
1016                                                 print "could not get ticket_id from stdin..."
1017                                                 os._exit(1)
1018                                 else:
1019                                         ticket_id = act_record['ticket_id']
1020                                         
1021                                 email_args['ticket_id'] = ticket_id
1022
1023                 return email_args
1024
1025         def get_unique_issues(self, act_recordlist):
1026                 # NOTE: only send one email per site, per problem...
1027                 unique_issues = {}
1028                 for act_record in act_recordlist:
1029                         act_key = act_record['action'][0]
1030                         if act_key not in unique_issues:
1031                                 unique_issues[act_key] = []
1032                                 
1033                         unique_issues[act_key] += [act_record]
1034                         
1035                 return unique_issues
1036                         
1037
1038         def __actOnSite(self, loginbase, site_record):
1039                 i_nodes_actedon = 0
1040                 i_nodes_emailed = 0
1041
1042                 act_recordlist = []
1043
1044                 for nodename in site_record['nodes'].keys():
1045                         diag_record = site_record['nodes'][nodename]
1046                         act_record  = self.__actOnNode(diag_record)
1047                         #print "nodename: %s %s" % (nodename, act_record)
1048                         if act_record is not None:
1049                                 act_recordlist += [act_record]
1050
1051                 unique_issues = self.get_unique_issues(act_recordlist)
1052
1053                 for issue in unique_issues.keys():
1054                         print "\tworking on issue: %s" % issue
1055                         issue_record_list = unique_issues[issue]
1056                         email_args = self.get_email_args(issue_record_list, loginbase)
1057
1058                         # for each record.
1059                         #for act_record in issue_record_list:
1060                         #       # if there's a pcu record and email config is set
1061                         #       if 'email_pcu' in act_record:
1062                         #               if act_record['message'] != None and act_record['email_pcu'] and site_record['config']['email']:
1063                         #                       # and 'reboot_node' in act_record['stage']:
1064
1065                         #                       email_args['hostname'] = act_record['nodename']
1066                         #                       ticket_id = self.__emailSite(loginbase, 
1067                         #                                                               act_record['email'], 
1068                         #                                                               emailTxt.mailtxt.pcudown[0],
1069                         #                                                               email_args)
1070                         #                       if ticket_id == 0:
1071                         #                               # error.
1072                         #                               print "got a ticket_id == 0!!!! %s" % act_record['nodename']
1073                         #                               os._exit(1)
1074                         #                               pass
1075                         #                       email_args['ticket_id'] = ticket_id
1076
1077                         
1078                         act_record = issue_record_list[0]
1079                         # send message before squeezing
1080                         print "\t\tconfig.email: %s and %s" % (act_record['message'] != None, 
1081                                                                                                 site_record['config']['email'])
1082                         if act_record['message'] != None and site_record['config']['email']:
1083                                 ticket_id = self.__emailSite(loginbase, act_record['email'], 
1084                                                                                          act_record['message'], email_args)
1085
1086                                 if ticket_id == 0:
1087                                         # error.
1088                                         print "ticket_id == 0 for %s %s" % (loginbase, act_record['nodename'])
1089                                         import os
1090                                         os._exit(1)
1091                                         pass
1092
1093                                 # Add ticket_id to ALL nodenames
1094                                 for act_record in issue_record_list:
1095                                         nodename = act_record['nodename']
1096                                         # update node record with RT ticket_id
1097                                         if nodename in self.act_all:
1098                                                 self.act_all[nodename][0]['ticket_id'] = "%s" % ticket_id
1099                                                 # if the ticket was previously resolved, reset it to new.
1100                                                 if 'rt' in act_record and \
1101                                                         'Status' in act_record['rt'] and \
1102                                                         act_record['rt']['Status'] == 'resolved':
1103                                                         mailer.setTicketStatus(ticket_id, "new")
1104                                                 status = mailer.getTicketStatus(ticket_id)
1105                                                 self.act_all[nodename][0]['rt'] = status
1106                                         if config.mail: i_nodes_emailed += 1
1107
1108                         print "\t\tconfig.squeeze: %s and %s" % (config.squeeze,
1109                                                                                                         site_record['config']['squeeze'])
1110                         if config.squeeze and site_record['config']['squeeze']:
1111                                 for act_key in act_record['action']:
1112                                         self.actions[act_key](email_args)
1113                                 i_nodes_actedon += 1
1114                 
1115                 if config.policysavedb:
1116                         #print "Saving Databases... act_all, diagnose_out"
1117                         #database.dbDump("act_all", self.act_all)
1118                         # remove site record from diagnose_out, it's in act_all as done.
1119                         del self.diagnose_db[loginbase]
1120                         #database.dbDump("diagnose_out", self.diagnose_db)
1121
1122                 print "sleeping for 1 sec"
1123                 time.sleep(1)
1124                 #print "Hit enter to continue..."
1125                 #sys.stdout.flush()
1126                 #line = sys.stdin.readline()
1127
1128                 return (i_nodes_actedon, i_nodes_emailed)
1129
1130         def __actOnNode(self, diag_record):
1131                 nodename = diag_record['nodename']
1132                 message = diag_record['message']
1133
1134                 act_record = {}
1135                 act_record.update(diag_record)
1136                 act_record['nodename'] = nodename
1137                 act_record['msg_format'] = self._format_diaginfo(diag_record)
1138                 print "act_record['stage'] == %s " % act_record['stage']
1139
1140                 # avoid end records, and nmreset records                                        
1141                 # reboot_node_failed, is set below, so don't reboot repeatedly.
1142
1143                 #if 'monitor-end-record' not in act_record['stage'] and \
1144                 #   'nmreset' not in act_record['stage'] and \
1145                 #   'reboot_node_failed' not in act_record:
1146
1147                 #       if "DOWN" in act_record['log'] and \
1148                 #                       'pcu_ids' in act_record['plcnode'] and \
1149                 #                       len(act_record['plcnode']['pcu_ids']) > 0:
1150 #
1151 #                               print "%s" % act_record['log'],
1152 #                               print "%15s" % (['reboot_node'],)
1153 #                               # Set node to re-install
1154 #                               plc.nodeBootState(act_record['nodename'], "rins")       
1155 #                               try:
1156 #                                       ret = reboot_node({'hostname': act_record['nodename']})
1157 #                               except Exception, exc:
1158 #                                       print "exception on reboot_node:"
1159 #                                       import traceback
1160 #                                       print traceback.print_exc()
1161 #                                       ret = False
1162 #
1163 #                               if ret: # and ( 'reboot_node_failed' not in act_record or act_record['reboot_node_failed'] == False):
1164 #                                       # Reboot Succeeded
1165 #                                       print "reboot succeeded for %s" % act_record['nodename']
1166 #                                       act_record2 = {}
1167 #                                       act_record2.update(act_record)
1168 #                                       act_record2['action'] = ['reboot_node']
1169 #                                       act_record2['stage'] = "reboot_node"
1170 #                                       act_record2['reboot_node_failed'] = False
1171 #                                       act_record2['email_pcu'] = False
1172 #
1173 #                                       if nodename not in self.act_all: 
1174 #                                               self.act_all[nodename] = []
1175 #                                       print "inserting 'reboot_node' record into act_all"
1176 #                                       self.act_all[nodename].insert(0,act_record2)
1177 #
1178 #                                       # return None to avoid further action
1179 #                                       print "Taking no further action"
1180 #                                       return None
1181 #                               else:
1182 #                                       print "reboot failed for %s" % act_record['nodename']
1183 #                                       # set email_pcu to also send pcu notice for this record.
1184 #                                       act_record['reboot_node_failed'] = True
1185 #                                       act_record['email_pcu'] = True
1186 #
1187 #                       print "%s" % act_record['log'],
1188 #                       print "%15s" % act_record['action']
1189
1190                 if act_record['stage'] is not 'monitor-end-record' and \
1191                    act_record['stage'] is not 'nmreset':
1192                         if nodename not in self.act_all: 
1193                                 self.act_all[nodename] = []
1194
1195                         self.act_all[nodename].insert(0,act_record)
1196                 else:
1197                         print "Not recording %s in act_all" % nodename
1198
1199                 return act_record
1200
1201         def analyseSites(self):
1202                 i_sites_observed = 0
1203                 i_sites_diagnosed = 0
1204                 i_nodes_diagnosed = 0
1205                 i_nodes_actedon = 0
1206                 i_sites_emailed = 0
1207                 l_allsites = []
1208
1209                 sorted_sites = self.sickdb.keys()
1210                 sorted_sites.sort()
1211                 for loginbase in sorted_sites:
1212                         site_record = self.sickdb[loginbase]
1213                         print "sites: %s" % loginbase
1214                         
1215                         i_nodes_diagnosed += len(site_record.keys())
1216                         i_sites_diagnosed += 1
1217
1218                         (na,ne) = self.__actOnSite(loginbase, site_record)
1219
1220                         i_sites_observed += 1
1221                         i_nodes_actedon += na
1222                         i_sites_emailed += ne
1223
1224                         l_allsites += [loginbase]
1225
1226                 return {'sites_observed': i_sites_observed, 
1227                                 'sites_diagnosed': i_sites_diagnosed, 
1228                                 'nodes_diagnosed': i_nodes_diagnosed, 
1229                                 'sites_emailed': i_sites_emailed, 
1230                                 'nodes_actedon': i_nodes_actedon, 
1231                                 'allsites':l_allsites}
1232
1233         def print_stats(self, key, stats):
1234                 print "%20s : %d" % (key, stats[key])
1235