fixes to make them more stand-alone and general.
[monitor.git] / rt.py
1 #!/usr/bin/python2
2
3 import os, sys, shutil
4 import MySQLdb
5 import string
6 import logging
7 import Queue
8 import time 
9 import re
10 import comon
11 import soltesz
12 from threading import *
13
14 # TODO: merge the RT mailer from mailer.py into this file.
15
16 # RT database access constants file
17 RT_DB_CONSTANTS_PATH='rt_db'
18
19 #Logging
20 logger = logging.getLogger("monitor")
21
22 # seconds between ticket update
23 RTSLEEP = 7200
24
25 def stripQuotes( str ):
26         quotes= ["'",'"']
27         if str[0] in quotes:
28                 str= str[1:]
29         if str[len(str)-1] in quotes:
30                 str= str[:len(str)-1]
31         return str
32
33
34 def readConstantsFile( file_path ):
35         """
36         read a file consisting of lines of
37         NAME=VALUE
38         NAME='VALUE'
39         NAME="VALUE"
40         and return a dictionary of the values.
41
42         blank lines, and lines starting with # (comments) are skipped
43         """
44
45         contents= {}
46
47         try:
48                 input_file= file(file_path,"r")
49         except IOError, err:
50                 return None
51
52         for line in input_file:
53                 if line[0] == "#":
54                         continue
55                 line= string.strip(line)
56                 if len(line) == 0:
57                         continue
58
59                 parts= string.split(line,"=",)
60                 if len(parts) <> 2:
61                         continue
62
63                 contents[parts[0]]= stripQuotes(parts[1])
64
65         return contents
66
67
68
69 def open_rt_db():
70
71         # read plc database passwords and connect
72         rt_db_constants= readConstantsFile(RT_DB_CONSTANTS_PATH)
73         if rt_db_constants is None:
74                 print "Unable to read database access constants from %s" % \
75                           RT_DB_CONSTANTS_PATH
76                 return -1
77
78         try:
79                 rt_db = MySQLdb.connect(host=rt_db_constants['RT_DB_HOST'],
80                                 user=rt_db_constants['RT_DB_USER'],
81                                 passwd=rt_db_constants['RT_DB_PASSWORD'],
82                                 db=rt_db_constants['RT_DB_NAME'])
83         except Exception, err:
84                 print "Failed to connect to RT database: %s" %err
85                 return -1
86
87         return rt_db
88
89
90
91 def fetch_from_db(db, sql):
92         try:
93                 # create a 'cursor' (required by MySQLdb)
94                 c = db.cursor()
95                 c.execute(sql)
96         except Exception, err:
97                 print "Could not execute RT query %s" %err
98                 return -1
99
100         # fetch all rows (list of lists)
101         raw = c.fetchall()
102         return raw
103         
104
105 def rt_tickets():
106         db = open_rt_db()
107         if db == -1:
108                 return ""
109 #       sql = """SELECT distinct Tk.id, Tk.Status, Tk.Subject
110 #                        FROM Tickets AS Tk
111 #                        JOIN Transactions AS Tr ON Tk.id=Tr.ObjectId
112 #                        JOIN Attachments AS At ON Tr.id=At.TransactionID
113 #                        WHERE (At.Content LIKE '%%%s%%' OR
114 #                               At.Subject LIKE '%%%s%%') AND
115 #                               (Tk.Status = 'new' OR Tk.Status = 'open') AND
116 #                               Tk.Queue = 3 OR Tk.Queue = 19 
117 #                        ORDER BY Tk.Status, Tk.LastUpdated DESC""" \
118 #                        % (hostname,hostname)
119 #       sql = """SELECT distinct Tk.id, Tk.Status, Tk.Subject
120 #                        FROM Tickets AS Tk
121 #                        JOIN Transactions AS Tr ON Tk.id=Tr.ObjectId
122 #                        JOIN Attachments AS At ON Tr.id=At.TransactionID
123 #                        WHERE (At.Content LIKE '%%%s%%' OR
124 #                               At.Subject LIKE '%%%s%%') AND
125 #                               (Tk.Status = 'new' OR Tk.Status = 'open')
126 #                        ORDER BY Tk.Status, Tk.LastUpdated DESC""" \
127 #                        % (hostname,hostname)
128
129         # Queue == 10 is the spam Queue in RT.
130         sql = """SELECT distinct Tk.id, Tk.Status, Tk.Subject, At.Content
131                          FROM Tickets AS Tk, Attachments AS At 
132                          JOIN Transactions AS Tr ON Tk.id=Tr.ObjectId  
133                          WHERE Tk.Queue != 10 AND Tk.id > 10000 AND 
134                                    Tr.id=At.TransactionID AND Tk.Status = 'open'"""
135                                    #Tr.id=At.TransactionID AND (Tk.Status = 'new' OR Tk.Status = 'open')"""
136         #sqlall = """SELECT distinct Tk.id, Tk.Status, Tk.Subject, At.Content
137 #FROM Tickets AS Tk, Attachments AS At 
138 #JOIN Transactions AS Tr ON Tk.id=Tr.ObjectId  
139 #WHERE Tk.Queue != 10 AND Tk.id > 10000 AND 
140 #Tr.id=At.TransactionID AND ( Tk.Status = 'open' OR
141 #Tk.Status = 'new') """
142         sqlall = """SELECT distinct Tk.id, Tk.Status, Tk.Subject, At.Content, Us.EmailAddress FROM Tickets AS Tk, Attachments AS At, Users as Us JOIN Transactions AS Tr ON Tk.id=Tr.ObjectId WHERE (Tk.Queue=3 OR Tk.Queue=22) AND Tk.id > 10000 AND Tr.id=At.TransactionID AND ( Tk.Status = 'open' OR Tk.Status = 'new') AND Us.id=Tk.LastUpdatedBy """
143
144
145         raw = fetch_from_db(db, sql)
146         if raw == -1:
147                 return raw
148         tickets = map(lambda x: {"ticket_id":str(x[0]),
149                                 "status":x[1],
150                                 "subj":str(x[2]),
151                                 "content":str(x[3])},
152                                 raw)
153
154         raw = fetch_from_db(db,sqlall)
155         if raw == -1:
156                 return raw
157         tickets_all = map(lambda x: {"ticket_id":str(x[0]),
158                                 "status":x[1],
159                                 "subj":str(x[2]),
160                                 "content":str(x[3]),
161                                 "email":str(x[4]) },
162                                 raw)
163
164         db.close()
165
166         idTickets = {}
167         for t in tickets_all:
168                 idTickets[t['ticket_id']] = t
169         soltesz.dbDump("idTickets", idTickets)
170
171         return tickets
172
173 def is_host_in_rt_tickets(host, ticket_blacklist, ad_rt_tickets):
174         # ad_rt_tickets is an array of dicts, defined above.
175         if len(ad_rt_tickets) == 0:
176                 return (False, None)
177         
178         d_ticket = ad_rt_tickets[0]
179         if not ('ticket_id' in d_ticket and 'status' in d_ticket and 
180                         'subj' in d_ticket and 'content' in d_ticket):
181                 logger.debug("RT_tickets array has wrong fields!!!")
182                 return (False, None)
183
184         #logger.debug("Searching all tickets for %s" % host)
185         def search_tickets(host, ad_rt_tickets):
186                 # compile once for more efficiency
187                 re_host = re.compile(host)
188                 for x in ad_rt_tickets:
189                         if re_host.search(x['subj'], re.MULTILINE|re.IGNORECASE) or \
190                            re_host.search(x['content'], re.MULTILINE|re.IGNORECASE):
191                                 logger.debug("\t ticket %s has %s" % (x['ticket_id'], host))
192                                 print "\t ticket %s has %s" % (x['ticket_id'], host)
193                                 if x['ticket_id'] in ticket_blacklist:
194                                         return (False, x)
195                                 else:
196                                         return (True, x)
197                 print "\t noticket -- has %s" % host
198                 #logger.debug("\t noticket -- has %s" % host)
199                 return (False, None)
200
201         # This search, while O(tickets), takes less than a millisecond, 05-25-07
202         #t = soltesz.MyTimer()
203         ret = search_tickets(host, ad_rt_tickets)
204         #del t
205
206         return ret
207
208
209 '''
210 Finds tickets associated with hostnames.
211 The idea is if you give it an array of host names,
212 presumeably from comon's list of bad nodes, it starts
213 a few threads to query RT.  RT takes a while to return.
214
215 This is turning into a reinvention of DB design, which I dont believe in.
216 In an effort to keep things minimal, here's the basic algo:
217
218 Give list of hostnames to RT()
219 Finds tickets associate with new hostnames (not in dict(tickets)).
220 Remove nodes that have come backup. Don't care of ticket is closed after first query.
221 Another thread refresh tickets of nodes already in dict and remove nodes that have come up. 
222 '''
223 class RT(Thread):
224         def __init__(self, dbTickets, q_toRT, q_fromRT, l_ticket_blacklist, target = None): 
225                 # Time of last update of ticket DB
226                 self.dbTickets = dbTickets
227                 self.lastupdated = 0
228                 self.l_ticket_blacklist = l_ticket_blacklist
229                 self.q_toRT = q_toRT
230                 self.q_fromRT = q_fromRT 
231                 self.tickets = {}
232                 Thread.__init__(self,target = self.getTickets)
233
234         # Takes node from q_toRT, gets tickets.  
235         # Thread that actually gets the tickets.
236         def getTickets(self):
237                 self.count = 0
238                 while 1:
239                         diag_node = self.q_toRT.get(block = True)
240                         if diag_node != None: 
241                                 host = diag_node['nodename']
242                                 (b_host_inticket, r_ticket) = is_host_in_rt_tickets(host, \
243                                                                                                         self.l_ticket_blacklist, \
244                                                                                                         self.dbTickets)
245                                 diag_node['found_rt_ticket'] = None
246                                 if b_host_inticket:
247                                         logger.debug("RT: found tickets for %s" %host)
248                                         diag_node['found_rt_ticket'] = r_ticket['ticket_id']
249
250                                 else:
251                                         if r_ticket is not None:
252                                                 print "Ignoring ticket %s" % r_ticket['ticket_id']
253                                                 # TODO: why do i return the ticket id for a
254                                                 #               blacklisted ticket id?
255                                                 #diag_node['found_rt_ticket'] = r_ticket['ticket_id']
256                                         self.count = self.count + 1
257
258                                 self.q_fromRT.put(diag_node) 
259                         else:
260                                 print "RT processed %d nodes with noticket" % self.count
261                                 logger.debug("RT filtered %d noticket nodes" % self.count)
262                                 self.q_fromRT.put(None)
263
264                                 break
265
266         # Removes hosts that are no longer down.
267         def remTickets(self):
268                 logger.debug("Removing stale entries from DB.") 
269                 prevdown = self.tickets.keys()
270
271                 currdown = []
272                 ##BEGIN HACK.  This should be outside of this file. passed to class.
273                 #cmn = comon.Comon(None, None)
274         #       cmn.updatebkts()
275                 #for bucket in cmn.comonbkts.keys():
276                 #       for host in getattr(cmn,bucket):
277                 #               if host not in currdown: currdown.append(host)
278                 ##END HACK
279
280                 # Actually do the comparison
281                 #for host in prevdown:
282                 #       if host not in currdown:
283                 #               del self.tickets[host]
284                 #               logger.info("RT: %s no longer down." % host)
285
286         # Update Tickets
287         def updateTickets(self):
288                 logger.info("Refreshing DB.")
289                 for host in self.tickets.keys():
290                         # Put back in Q to refresh
291                         self.q_toRT.put(host)
292
293         def cleanTickets(self):
294                 while 1:
295                         self.remTickets()
296                         self.updateTickets()
297                         time.sleep(RTSLEEP)
298         
299 def main():
300         logger.setLevel(logging.DEBUG)
301         ch = logging.StreamHandler()
302         ch.setLevel(logging.DEBUG)
303         formatter = logging.Formatter('%(message)s')
304         ch.setFormatter(formatter)
305         logger.addHandler(ch)
306
307         tickets = rt_tickets()
308         soltesz.dbDump("ad_dbTickets", tickets)
309
310
311 if __name__ == '__main__':
312     main()