seppuku: misc fixes
[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     result = opener.open("%(query)s?product=%(product)s&component=%(component)s&short_desc_type=substring&short_desc=%(bugname)s" % vars())
133     if result.code != 200:
134         raise "Can not query the bugzilla at all"
135     txt = result.read()
136     scanner = seppuku_find_bug_report_old()
137     scanner.feed(txt)
138     if len(scanner.result()) == 0:
139         print >> debug_file, "Scanner failed to scan the html site"
140         print >> debug_file, "%(query)s?product=%(product)s&component=%(component)s&short_desc_type=substring&short_desc=%(bugname)s" % vars()
141         print >> debug_file, txt
142         return (False,None)
143     else: # silently pick the first result
144         print >> debug_file, "Result of bug search is "
145         print >> debug_file, txt
146         (number,status) = scanner.result()[0]
147         return (not status in ["CLOS", "RESO", "VERI"],number)
148
149 def seppuku_reopen_bug(poster, file, product, component, bug_number, bugname, text):
150     """
151     Reopen a bug report and append to the comment
152
153     Same as with opening a new report, some bits need to be inside the url
154
155     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
156     """
157
158     import urllib2
159     (uri, param) = seppuku_spliturl( file )
160
161     # Prepare the post
162     param["product"]        = product
163     param["component"]      = component
164     param["longdesclength"] = 2
165     param["short_desc"]     = bugname
166     param["knob"]           = "reopen"
167     param["id"]             = bug_number
168     param["comment"]        = text
169
170     try:
171         result = poster.open( uri, param )
172     except urllib2.HTTPError, e:
173         print e.geturl()
174         print e.info()
175         return False
176     except Exception, e:
177         print e
178         return False
179
180     if result.code != 200:
181         return False
182     else:
183         return True
184
185 def seppuku_file_bug(poster, file, product, component, bugname, text):
186     """
187     Create a completely new bug report
188
189
190     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
191
192     You are forced to add some default values to the bugzilla query and stop with '&'
193
194     @param opener  urllib2 opener
195     @param file    The url used to file a bug report
196     @param product Product
197     @param component Component
198     @param bugname  Name of the to be created bug
199     @param text Text
200     """
201
202     import urllib2
203     (uri, param) = seppuku_spliturl( file )
204     param["product"]    = product
205     param["component"]  = component
206     param["short_desc"] = bugname
207     param["comment"]    = text
208
209     try:
210         result = poster.open( uri, param )
211     except urllib2.HTTPError, e:
212         print e.geturl()
213         print e.info()
214         return False
215     except Exception, e:
216         print e
217         return False
218
219     # scan the result for a bug number
220     # it will look like 
221     # '<title>Bug 2742 Submitted</title>'
222     import re
223     res = re.findall(("\>Bug (?P<int>\d+) Submitted"), result.read() )
224     if result.code != 200 or len(res) != 1:
225         return None 
226     else:
227         return res[0] 
228
229 def seppuku_create_attachment(debug, poster, attach_query, product, component, bug_number, text, file):
230     """
231
232     Create a new attachment for the failed report
233     """
234
235     if not bug_number:
236         import bb
237         bb.note("Can't create an attachment, no bugnumber passed to method")
238         return False
239
240     import urllib2
241     param = { "bugid" : bug_number, "action" : "insert", "data" : file, "description" : "Build log", "ispatch" : "0", "contenttypemethod" : "list", "contenttypeselection" : "text/plain", "comment" : text }
242
243     try:
244         result = poster.open( attach_query, param )
245     except urllib2.HTTPError, e:
246         print e.geturl()
247         print e.info()
248         return False
249     except Exception, e:
250         print e
251         return False
252
253     print >> debug, result.read()
254     if result.code != 200:
255         return False
256     else:
257         return True
258
259
260 addhandler seppuku_eventhandler
261 python seppuku_eventhandler() {
262     """
263     Report task failures to the bugzilla
264     and succeeded builds to the box
265     """
266     from bb.event import NotHandled, getName
267     from bb import data, mkdirhier, build
268     import bb, os, glob
269
270     event = e
271     data = e.data
272     name = getName(event)
273     if name == "MsgNote":
274        # avoid recursion
275        return NotHandled
276
277     # Try to load our exotic libraries
278     try:
279         import MultipartPostHandler
280     except:
281         bb.note("You need to put the MultipartPostHandler into your PYTHONPATH. Download it from http://pipe.scs.fsu.edu/PostHandler/MultipartPostHandler.py")
282         return NotHandled
283
284     try:
285         import urllib2, cookielib
286     except:
287         bb.note("Failed to import the cookielib and urllib2, make sure to use python2.4")
288         return NotHandled
289
290     if name == "PkgFailed":
291         if not bb.data.getVar('SEPPUKU_AUTOBUILD', data, True) == "0":
292             build.exec_task('do_clean', data)
293     elif name == "TaskFailed" or name == "NoProvider":
294         cj = cookielib.CookieJar()
295         opener  = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
296         poster  = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj),MultipartPostHandler.MultipartPostHandler)
297         login   = bb.data.getVar("SEPPUKU_LOGIN", data, True)
298         query   = bb.data.getVar("SEPPUKU_QUERY", data, True)
299         newbug  = bb.data.getVar("SEPPUKU_NEWREPORT",  data, True)
300         reopen  = bb.data.getVar("SEPPUKU_ADDCOMMENT",  data, True)
301         attach  = bb.data.getVar("SEPPUKU_ATTACHMENT", data, True)
302         user    = bb.data.getVar("SEPPUKU_USER",  data, True)
303         passw   = bb.data.getVar("SEPPUKU_PASS",  data, True)
304         product = bb.data.getVar("SEPPUKU_PRODUCT", data, True)
305         component = bb.data.getVar("SEPPUKU_COMPONENT", data, True)
306         # evil hack to figure out what is going on
307         debug_file = open(os.path.join(bb.data.getVar("TMPDIR", data, True),"..","seppuku-log"),"a")
308
309         if not seppuku_login(opener, login, user, passw):
310             bb.note("Login to bugzilla failed")
311             print >> debug_file, "Login to bugzilla failed"
312             return NotHandled
313         else:
314             print >> debug_file, "Logged into the box"
315
316         file = None
317         if name == "TaskFailed":
318             bugname = "%(package)s-%(pv)s-%(pr)s-%(task)s" % { "package" : bb.data.getVar("PN", data, True),
319                                                                "pv"      : bb.data.getVar("PV", data, True),
320                                                                "pr"      : bb.data.getVar("PR", data, True),
321                                                                "task"    : e.task }
322             log_file = glob.glob("%s/log.%s.*" % (bb.data.getVar('T', event.data, True), event.task))
323             text     = "The package failed to build at %s for machine %s" % (bb.data.getVar('DATETIME', data, True), bb.data.getVar( 'MACHINE', data, True ) )
324             if len(log_file) != 0:
325                 print >> debug_file, "Adding log file %s" % log_file[0]
326                 file = open(log_file[0], 'r')
327             else:
328                 print >> debug_file, "No log file found for the glob"
329         #elif name == "NoProvider":
330         #    bugname = "noprovider for %s " % (event.getItem)
331         #    text    = "Please fix it"
332         else:
333             print >> debug_file, "Unknown name '%s'" % name
334             assert False
335
336         (bug_open, bug_number) = seppuku_find_bug_report(debug_file, opener, query, product, component, bugname)
337         print >> debug_file, "Bug is open: %s and bug number: %s" % (bug_open, bug_number)
338
339         # The bug is present and still open, no need to attach an error log
340         if bug_number and bug_open:
341             print >> debug_file, "The bug is known as '%s'" % bug_number
342             return NotHandled
343
344         if bug_number and not bug_open:
345             if not seppuku_reopen_bug(poster, reopen, product, component, bug_number, bugname, text):
346                 print >> debug_file, "Failed to reopen the bug report"
347             else:
348                 print >> debug_file, "Reopened the bug report"
349         else:   
350             bug_number = seppuku_file_bug(poster, newbug, product, component, bugname, text)
351             if not bug_number:
352                 print >> debug_file, "Couldn't acquire a new bug_numer, filing a bugreport failed"
353             else:
354                 print >> debug_file, "The new bug_number: '%s'" % bug_number
355
356         if bug_number and file:
357             if not seppuku_create_attachment(debug_file, poster, attach, product, component, bug_number, text, file):
358                 print >> debug_file, "Failed to attach the build log"
359             else:
360                 print >> debug_file, "Created an attachment for '%s' '%s' '%s'" % (product, component, bug_number)
361         else:
362             print >> debug_file, "Not trying to create an attachment"
363
364     return NotHandled
365 }