merge of '6bfc098b36170550230dc57120c75b31480dc6b3'
[openembedded.git] / classes / seppuku.bbclass
1 #
2 # Small event handler to automatically open URLs and file
3 # bug reports at a bugzilla of your choiche
4 #
5 # This class requires python2.4 because of the urllib2 usage
6 #
7
8 def seppuku_spliturl(url):
9     """
10     Split GET URL to return the host base and the query
11     as a param dictionary
12     """
13     import urllib
14     (uri,query)  = urllib.splitquery(url)
15     param = {}
16     for par in query.split("&"):
17         (key,value) = urllib.splitvalue(par)
18         if not key or len(key) == 0 or not value:
19             continue
20         key = urllib.unquote(key)
21         value = urllib.unquote(value)
22         param[key] = value
23
24     return (uri,param)
25
26
27
28 def seppuku_login(opener, login, user, password):
29     """
30     We need to post to query.cgi with the parameters
31     Bugzilla_login and Bugzilla_password and will scan
32     the resulting page then
33
34     @param opened = cookie enabled urllib2 opener
35     @param login = http://bugs.openembedded.org/query.cgi?
36     @param user  = Your username
37     @param password  = Your password
38     """
39     import urllib
40     param = urllib.urlencode( {"GoAheadAndLogIn" : 1, "Bugzilla_login" : user, "Bugzilla_password" : password } )
41     result = opener.open(login + param)
42
43     if result.code != 200:
44         return False
45     txt = result.read()
46     if not '<a href="relogin.cgi">Log&nbsp;out</a>' in txt:
47         return False
48
49     return True
50
51 def seppuku_find_bug_report_old():
52     from HTMLParser import HTMLParser
53
54     class BugQueryExtractor(HTMLParser):
55         STATE_NONE             = 0
56         STATE_FOUND_TR         = 1
57         STATE_FOUND_NUMBER     = 2
58         STATE_FOUND_PRIO       = 3
59         STATE_FOUND_PRIO2      = 4
60         STATE_FOUND_NAME       = 5
61         STATE_FOUND_PLATFORM   = 6
62         STATE_FOUND_STATUS     = 7
63         STATE_FOUND_WHATEVER   = 8 # I don't know this field
64         STATE_FOUND_DESCRIPTION =9
65
66         def __init__(self):
67             HTMLParser.__init__(self)
68             self.state = self.STATE_NONE
69             self.bugs = []
70             self.bug  = None
71
72         def handle_starttag(self, tag, attr):
73             if self.state == self.STATE_NONE and tag.lower() == "tr":
74                 if len(attr) == 1 and attr[0][0] == 'class' and \
75                     ('bz_normal' in attr[0][1] or 'bz_blocker' in attr[0][1] or 'bz_enhancement' in attr[0][1] or 'bz_major' in attr[0][1] or 'bz_minor' in attr[0][1] or 'bz_trivial' in attr[0][1] or 'bz_critical' in attr[0][1] or 'bz_wishlist' in attr[0][1]) \
76                     and 'bz_P' in attr[0][1]:
77                     self.state = self.STATE_FOUND_TR
78             elif self.state == self.STATE_FOUND_TR and tag.lower() == "td":
79                 self.state += 1
80
81         def handle_endtag(self, tag):
82             if tag.lower() == "tr":
83                 if self.state != self.STATE_NONE:
84                     self.bugs.append( (self.bug,self.status) )
85                 self.state = self.STATE_NONE
86                 self.bug  = None
87             if self.state > 1 and tag.lower() == "td":
88                 self.state += 1
89
90         def handle_data(self,data):
91             data = data.strip()
92
93             # skip garbage
94             if len(data) == 0:
95                 return
96
97             if self.state == self.STATE_FOUND_NUMBER:
98                 """
99                 #1995 in bugs.oe.org has [SEC] additionally to the number and we want to ignore it
100                 """
101                 if not self.bug:
102                     self.bug = data
103             elif self.state == self.STATE_FOUND_STATUS:
104                 self.status = data
105
106         def result(self):
107             return self.bugs
108
109     return BugQueryExtractor()
110
111
112
113 def seppuku_find_bug_report(debug_file, opener, query, product, component, bugname):
114     """
115     Find a bug report with the sane name and return the bug id
116     and the status.
117
118     @param opener = urllib2 opener
119     @param query  = e.g. http://bugs.openembedded.org/query.cgi?
120     @param product = search for this product
121     @param component = search for this component
122     @param bugname = the bug to search for
123
124     http://bugs.openembedded.org/buglist.cgi?short_desc_type=substring&short_desc=manual+test+bug&product=Openembedded&emailreporter2=1&emailtype2=substring&email2=freyther%40yahoo.com
125     but it does not support ctype=csv...
126     """
127     import urllib
128     product   = urllib.quote(product)
129     component = urllib.quote(component)
130     bugname   = urllib.quote(bugname)
131
132     file = "%(query)sproduct=%(product)s&component=%(component)s&short_desc_type=substring&short_desc=%(bugname)s" % vars()
133     print >> debug_file, "Trying %s" % file
134     result = opener.open(file)
135     if result.code != 200:
136         raise "Can not query the bugzilla at all"
137     txt = result.read()
138     scanner = seppuku_find_bug_report_old()
139     scanner.feed(txt)
140     if len(scanner.result()) == 0:
141         print >> debug_file, "Scanner failed to scan the html site"
142         print >> debug_file, "%(query)sproduct=%(product)s&component=%(component)s&short_desc_type=substring&short_desc=%(bugname)s" % vars()
143         #print >> debug_file, txt
144         return (False,None)
145     else: # silently pick the first result
146         print >> debug_file, "Result of bug search is "
147         #print >> debug_file, txt
148         (number,status) = scanner.result()[0]
149         return (not status in ["CLOS", "RESO", "VERI"],number)
150
151 def seppuku_reopen_bug(poster, file, product, component, bug_number, bugname, text):
152     """
153     Reopen a bug report and append to the comment
154
155     Same as with opening a new report, some bits need to be inside the url
156
157     http://bugs.openembedded.org/process_bug.cgi?id=239&bug_file_loc=http%3A%2F%2F&version=Angstrom&longdesclength=2&product=Openembedded&component=Build&comment=bla&priority=P2&bug_severity=normal&op_sys=Linux&rep_platform=Other&knob=reopen&short_desc=foo
158     """
159
160     import urllib2
161     (uri, param) = seppuku_spliturl( file )
162
163     # Prepare the post
164     param["product"]        = product
165     param["component"]      = component
166     param["longdesclength"] = 2
167     param["short_desc"]     = bugname
168     param["knob"]           = "reopen"
169     param["id"]             = bug_number
170     param["comment"]        = text
171
172     try:
173         result = poster.open( uri, param )
174     except urllib2.HTTPError, e:
175         print e.geturl()
176         print e.info()
177         return False
178     except Exception, e:
179         print e
180         return False
181
182     if result.code != 200:
183         return False
184     else:
185         return True
186
187 def seppuku_file_bug(poster, file, product, component, bugname, text):
188     """
189     Create a completely new bug report
190
191
192     http://bugs.openembedded.org/post_bug.cgi?bug_file_loc=http%3A%2F%2F&version=Angstrom&product=Openembedded&component=Build&short_desc=foo&comment=bla&priority=P2&bug_severity=normal&op_sys=Linux&rep_platform=Other
193
194     You are forced to add some default values to the bugzilla query and stop with '&'
195
196     @param opener  urllib2 opener
197     @param file    The url used to file a bug report
198     @param product Product
199     @param component Component
200     @param bugname  Name of the to be created bug
201     @param text Text
202     """
203
204     import urllib2
205     (uri, param) = seppuku_spliturl( file )
206     param["product"]    = product
207     param["component"]  = component
208     param["short_desc"] = bugname
209     param["comment"]    = text
210
211     try:
212         result = poster.open( uri, param )
213     except urllib2.HTTPError, e:
214         print e.geturl()
215         print e.info()
216         return False
217     except Exception, e:
218         print e
219         return False
220
221     # scan the result for a bug number
222     # it will look like 
223     # '<title>Bug 2742 Submitted</title>'
224     import re
225     res = re.findall(("\>Bug (?P<int>\d+) Submitted"), result.read() )
226     if result.code != 200 or len(res) != 1:
227         return None 
228     else:
229         return res[0] 
230
231 def seppuku_create_attachment(debug, poster, attach_query, product, component, bug_number, text, file):
232     """
233
234     Create a new attachment for the failed report
235     """
236
237     if not bug_number:
238         import bb
239         bb.note("Can't create an attachment, no bugnumber passed to method")
240         print >> debug, "Can't create an attachment, no bugnumber passed to method"
241         return False
242
243     if not attach_query:
244         import bb
245         bb.note("Can't create an attachment, no attach_query passed to method")
246         print >> debug, "Can't create an attachment, no attach_query passed to method"
247         return False
248
249
250     import urllib2
251     param = { "bugid" : bug_number, "action" : "insert", "data" : file, "description" : "Build log", "ispatch" : "0", "contenttypemethod" : "list", "contenttypeselection" : "text/plain", "comment" : text }
252
253     try:
254         result = poster.open( attach_query, param )
255     except urllib2.HTTPError, e:
256         print e.geturl()
257         print e.info()
258         return False
259     except Exception, e:
260         print e
261         print >> debug, "Got exception in poster.open( attach_query, param )"
262         print >> debug, "attach_query: %s  param: %s" % (attach_query, param )
263         return False
264
265     txt = result.read()
266     if result.code != 200:
267         print >> debug, "Got bad return code (%s)" % result.code
268         return False
269     else:
270         print >> debug, "Got good return code (200)" 
271         return True
272
273
274 addhandler seppuku_eventhandler
275 python seppuku_eventhandler() {
276     """
277     Report task failures to the bugzilla
278     and succeeded builds to the box
279     """
280     from bb.event import NotHandled, getName
281     from bb import data, mkdirhier, build
282     import bb, os, glob
283
284     event = e
285     data = e.data
286     name = getName(event)
287     if name == "MsgNote":
288        # avoid recursion
289        return NotHandled
290
291     # Try to load our exotic libraries
292     try:
293         import MultipartPostHandler
294     except:
295         bb.note("You need to put the MultipartPostHandler into your PYTHONPATH. Download it from http://pipe.scs.fsu.edu/PostHandler/MultipartPostHandler.py")
296         return NotHandled
297
298     try:
299         import urllib2, cookielib
300     except:
301         bb.note("Failed to import the cookielib and urllib2, make sure to use python2.4")
302         return NotHandled
303
304     if name == "PkgFailed":
305         if not bb.data.getVar('SEPPUKU_AUTOBUILD', data, True) == "0":
306             build.exec_func('do_clean', data)
307     elif name == "TaskFailed":
308         cj = cookielib.CookieJar()
309         opener  = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
310         poster  = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj),MultipartPostHandler.MultipartPostHandler)
311         login   = bb.data.getVar("SEPPUKU_LOGIN", data, True)
312         query   = bb.data.getVar("SEPPUKU_QUERY", data, True)
313         newbug  = bb.data.getVar("SEPPUKU_NEWREPORT",  data, True)
314         reopen  = bb.data.getVar("SEPPUKU_ADDCOMMENT",  data, True)
315         attach  = bb.data.getVar("SEPPUKU_ATTACHMENT", data, True)
316         user    = bb.data.getVar("SEPPUKU_USER",  data, True)
317         passw   = bb.data.getVar("SEPPUKU_PASS",  data, True)
318         product = bb.data.getVar("SEPPUKU_PRODUCT", data, True)
319         component = bb.data.getVar("SEPPUKU_COMPONENT", data, True)
320         # evil hack to figure out what is going on
321         debug_file = open(os.path.join(bb.data.getVar("TMPDIR", data, True),"..","seppuku-log"),"a")
322
323         if not seppuku_login(opener, login, user, passw):
324             bb.note("Login to bugzilla failed")
325             print >> debug_file, "Login to bugzilla failed"
326             return NotHandled
327         else:
328             print >> debug_file, "Logged into the box"
329
330         file = None
331         if name == "TaskFailed":
332             bugname = "%(package)s-%(pv)s-%(pr)s-%(task)s" % { "package" : bb.data.getVar("PN", data, True),
333                                                                "pv"      : bb.data.getVar("PV", data, True),
334                                                                "pr"      : bb.data.getVar("PR", data, True),
335                                                                "task"    : e.task }
336             log_file = glob.glob("%s/log.%s.*" % (bb.data.getVar('T', event.data, True), event.task))
337             text     = "The package failed to build at %s for machine %s" % (bb.data.getVar('DATETIME', data, True), bb.data.getVar( 'MACHINE', data, True ) )
338             if len(log_file) != 0:
339                 print >> debug_file, "Adding log file %s" % log_file[0]
340                 file = open(log_file[0], 'r')
341             else:
342                 print >> debug_file, "No log file found for the glob"
343         else:
344             print >> debug_file, "Unknown name '%s'" % name
345             assert False
346
347         (bug_open, bug_number) = seppuku_find_bug_report(debug_file, opener, query, product, component, bugname)
348         print >> debug_file, "Bug is open: %s and bug number: %s" % (bug_open, bug_number)
349
350         # The bug is present and still open, attach an error log
351         if bug_number and bug_open:
352             print >> debug_file, "The bug is known as '%s'" % bug_number
353             if file:
354                 if not seppuku_create_attachment(debug_file, poster, attach, product, component, bug_number, text, file):
355                      print >> debug_file, "Failed to attach the build log for bug #%s" % bug_number
356                 else:
357                      print >> debug_file, "Created an attachment for '%s' '%s' '%s'" % (product, component, bug_number)
358             else:
359                      print >> debug_file, "Not trying to create an attachment for bug #%s" % bug_number
360             return NotHandled
361
362         if bug_number and not bug_open:
363             if not seppuku_reopen_bug(poster, reopen, product, component, bug_number, bugname, text):
364                 print >> debug_file, "Failed to reopen the bug #%s" % bug_number
365             else:
366                 print >> debug_file, "Reopened the bug #%s" % bug_number
367         else:   
368             bug_number = seppuku_file_bug(poster, newbug, product, component, bugname, text)
369             if not bug_number:
370                 print >> debug_file, "Couldn't acquire a new bug_numer, filing a bugreport failed"
371             else:
372                 print >> debug_file, "The new bug_number: '%s'" % bug_number
373
374         if bug_number and file:
375             if not seppuku_create_attachment(debug_file, poster, attach, product, component, bug_number, text, file):
376                 print >> debug_file, "Failed to attach the build log for bug #%s" % bug_number
377             else:
378                 print >> debug_file, "Created an attachment for '%s' '%s' '%s'" % (product, component, bug_number)
379         else:
380             print >> debug_file, "Not trying to create an attachment for bug #%s" % bug_number
381
382     return NotHandled
383 }