seppuku: add support for working behind the proxy
[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         proxy   = bb.data.getVar('HTTP_PROXY', data, True )
323         if (proxy):
324                 phl = urllib2.ProxyHandler({'http' : proxy})
325                 poster.add_handler(phl)
326                 opener.add_handler(phl)
327
328         # evil hack to figure out what is going on
329         debug_file = open(os.path.join(bb.data.getVar("TMPDIR", data, True),"..","seppuku-log"),"a")
330
331         if not seppuku_login(opener, login, user, passw):
332             bb.note("Login to bugzilla failed")
333             print >> debug_file, "Login to bugzilla failed"
334             return NotHandled
335         else:
336             print >> debug_file, "Logged into the box"
337
338         file = None
339         if name == "TaskFailed":
340             bugname = "%(package)s-%(pv)s-autobuild" % { "package" : bb.data.getVar("PN", data, True),
341                                                                "pv"      : bb.data.getVar("PV", data, True),
342                                                                }  
343             log_file = glob.glob("%s/log.%s.*" % (bb.data.getVar('T', event.data, True), event.task))
344             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 ) )
345             if len(log_file) != 0:
346                 print >> debug_file, "Adding log file %s" % log_file[0]
347                 file = open(log_file[0], 'r')
348             else:
349                 print >> debug_file, "No log file found for the glob"
350         else:
351             print >> debug_file, "Unknown name '%s'" % name
352             assert False
353
354         (bug_open, bug_number) = seppuku_find_bug_report(debug_file, opener, query, product, component, bugname)
355         print >> debug_file, "Bug is open: %s and bug number: %s" % (bug_open, bug_number)
356
357         # The bug is present and still open, attach an error log
358         if bug_number and bug_open:
359             print >> debug_file, "The bug is known as '%s'" % bug_number
360             if file:
361                 if not seppuku_create_attachment(data, debug_file, poster, attach, product, component, bug_number, text, file):
362                      print >> debug_file, "Failed to attach the build log for bug #%s" % bug_number
363                 else:
364                      print >> debug_file, "Created an attachment for '%s' '%s' '%s'" % (product, component, bug_number)
365             else:
366                      print >> debug_file, "Not trying to create an attachment for bug #%s" % bug_number
367             return NotHandled
368
369         if bug_number and not bug_open:
370             if not seppuku_reopen_bug(poster, reopen, product, component, bug_number, bugname, text):
371                 print >> debug_file, "Failed to reopen the bug #%s" % bug_number
372             else:
373                 print >> debug_file, "Reopened the bug #%s" % bug_number
374         else:   
375             bug_number = seppuku_file_bug(poster, newbug, product, component, bugname, text)
376             if not bug_number:
377                 print >> debug_file, "Couldn't acquire a new bug_numer, filing a bugreport failed"
378             else:
379                 print >> debug_file, "The new bug_number: '%s'" % bug_number
380
381         if bug_number and file:
382             if not seppuku_create_attachment(data, debug_file, poster, attach, product, component, bug_number, text, file):
383                 print >> debug_file, "Failed to attach the build log for bug #%s" % bug_number
384             else:
385                 print >> debug_file, "Created an attachment for '%s' '%s' '%s'" % (product, component, bug_number)
386         else:
387             print >> debug_file, "Not trying to create an attachment for bug #%s" % bug_number
388
389         # store bug number for oestats-client
390         if bug_number:
391             bb.data.setVar('OESTATS_BUG_NUMBER', bug_number, event.data)
392             bb.data.setVar('OESTATS_BUG_TRACKER', "http://bugs.openembedded.net/", event.data)
393
394     return NotHandled
395 }