merge of '86dacb8c8e579fd5d42ab61a0c0f344bcf74146d'
[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.net/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.net/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.net/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.net/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.net/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(data, 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     import bb
250     logdescription = "Build log for machine %s" % (bb.data.getVar('MACHINE', data, True))
251
252     import urllib2
253     param = { "bugid" : bug_number, "action" : "insert", "data" : file, "description" : logdescription, "ispatch" : "0", "contenttypemethod" : "list", "contenttypeselection" : "text/plain", "comment" : text }
254
255     try:
256         result = poster.open( attach_query, param )
257     except urllib2.HTTPError, e:
258         print e.geturl()
259         print e.info()
260         return False
261     except Exception, e:
262         print e
263         print >> debug, "Got exception in poster.open( attach_query, param )"
264         print >> debug, "attach_query: %s  param: %s" % (attach_query, param )
265         return False
266
267     txt = result.read()
268     if result.code != 200:
269         print >> debug, "Got bad return code (%s)" % result.code
270         return False
271     else:
272         print >> debug, "Got good return code (200)" 
273         return True
274
275
276 addhandler seppuku_eventhandler
277 python seppuku_eventhandler() {
278     """
279     Report task failures to the bugzilla
280     and succeeded builds to the box
281     """
282     from bb.event import NotHandled, getName
283     from bb import data, mkdirhier, build
284     import bb, os, glob
285
286     event = e
287     data = e.data
288     name = getName(event)
289     if name == "MsgNote":
290        # avoid recursion
291        return NotHandled
292
293     # Try to load our exotic libraries
294     try:
295         import MultipartPostHandler
296     except:
297         bb.note("You need to put the MultipartPostHandler into your PYTHONPATH. Download it from http://pipe.scs.fsu.edu/PostHandler/MultipartPostHandler.py")
298         return NotHandled
299
300     try:
301         import urllib2, cookielib
302     except:
303         bb.note("Failed to import the cookielib and urllib2, make sure to use python2.4")
304         return NotHandled
305
306     if name == "PkgFailed":
307         if not bb.data.getVar('SEPPUKU_AUTOBUILD', data, True) == "0":
308             build.exec_func('do_clean', data)
309     elif name == "TaskFailed":
310         cj = cookielib.CookieJar()
311         opener  = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
312         poster  = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj),MultipartPostHandler.MultipartPostHandler)
313         login   = bb.data.getVar("SEPPUKU_LOGIN", data, True)
314         query   = bb.data.getVar("SEPPUKU_QUERY", data, True)
315         newbug  = bb.data.getVar("SEPPUKU_NEWREPORT",  data, True)
316         reopen  = bb.data.getVar("SEPPUKU_ADDCOMMENT",  data, True)
317         attach  = bb.data.getVar("SEPPUKU_ATTACHMENT", data, True)
318         user    = bb.data.getVar("SEPPUKU_USER",  data, True)
319         passw   = bb.data.getVar("SEPPUKU_PASS",  data, True)
320         product = bb.data.getVar("SEPPUKU_PRODUCT", data, True)
321         component = bb.data.getVar("SEPPUKU_COMPONENT", data, True)
322         # evil hack to figure out what is going on
323         debug_file = open(os.path.join(bb.data.getVar("TMPDIR", data, True),"..","seppuku-log"),"a")
324
325         if not seppuku_login(opener, login, user, passw):
326             bb.note("Login to bugzilla failed")
327             print >> debug_file, "Login to bugzilla failed"
328             return NotHandled
329         else:
330             print >> debug_file, "Logged into the box"
331
332         file = None
333         if name == "TaskFailed":
334             bugname = "%(package)s-%(pv)s-autobuild" % { "package" : bb.data.getVar("PN", data, True),
335                                                                "pv"      : bb.data.getVar("PV", data, True),
336                                                                }  
337             log_file = glob.glob("%s/log.%s.*" % (bb.data.getVar('T', event.data, True), event.task))
338             text     = "The %s step in %s failed at %s for machine %s" % (e.task, bb.data.getVar("PN", data, True), bb.data.getVar('DATETIME', data, True), bb.data.getVar( 'MACHINE', data, True ) )
339             if len(log_file) != 0:
340                 print >> debug_file, "Adding log file %s" % log_file[0]
341                 file = open(log_file[0], 'r')
342             else:
343                 print >> debug_file, "No log file found for the glob"
344         else:
345             print >> debug_file, "Unknown name '%s'" % name
346             assert False
347
348         (bug_open, bug_number) = seppuku_find_bug_report(debug_file, opener, query, product, component, bugname)
349         print >> debug_file, "Bug is open: %s and bug number: %s" % (bug_open, bug_number)
350
351         # The bug is present and still open, attach an error log
352         if bug_number and bug_open:
353             print >> debug_file, "The bug is known as '%s'" % bug_number
354             if file:
355                 if not seppuku_create_attachment(data, debug_file, poster, attach, product, component, bug_number, text, file):
356                      print >> debug_file, "Failed to attach the build log for bug #%s" % bug_number
357                 else:
358                      print >> debug_file, "Created an attachment for '%s' '%s' '%s'" % (product, component, bug_number)
359             else:
360                      print >> debug_file, "Not trying to create an attachment for bug #%s" % bug_number
361             return NotHandled
362
363         if bug_number and not bug_open:
364             if not seppuku_reopen_bug(poster, reopen, product, component, bug_number, bugname, text):
365                 print >> debug_file, "Failed to reopen the bug #%s" % bug_number
366             else:
367                 print >> debug_file, "Reopened the bug #%s" % bug_number
368         else:   
369             bug_number = seppuku_file_bug(poster, newbug, product, component, bugname, text)
370             if not bug_number:
371                 print >> debug_file, "Couldn't acquire a new bug_numer, filing a bugreport failed"
372             else:
373                 print >> debug_file, "The new bug_number: '%s'" % bug_number
374
375         if bug_number and file:
376             if not seppuku_create_attachment(data, debug_file, poster, attach, product, component, bug_number, text, file):
377                 print >> debug_file, "Failed to attach the build log for bug #%s" % bug_number
378             else:
379                 print >> debug_file, "Created an attachment for '%s' '%s' '%s'" % (product, component, bug_number)
380         else:
381             print >> debug_file, "Not trying to create an attachment for bug #%s" % bug_number
382
383     return NotHandled
384 }