a907278e1577af367ecce41abba41227c89d9455
[build-farm.git] / buildfarm / data.py
1 #!/usr/bin/python
2 # Simple database query script for the buildfarm
3 #
4 # Copyright (C) Andrew Tridgell <tridge@samba.org>     2001-2005
5 # Copyright (C) Andrew Bartlett <abartlet@samba.org>   2001
6 # Copyright (C) Vance Lankhaar  <vance@samba.org>      2002-2005
7 # Copyright (C) Martin Pool <mbp@samba.org>            2001
8 # Copyright (C) Jelmer Vernooij <jelmer@samba.org>         2007-2010
9 #
10 #   This program is free software; you can redistribute it and/or modify
11 #   it under the terms of the GNU General Public License as published by
12 #   the Free Software Foundation; either version 2 of the License, or
13 #   (at your option) any later version.
14 #
15 #   This program is distributed in the hope that it will be useful,
16 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
17 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 #   GNU General Public License for more details.
19 #
20 #   You should have received a copy of the GNU General Public License
21 #   along with this program; if not, write to the Free Software
22 #   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23
24
25 import os
26 import re
27 import time
28 import util
29
30
31 def span(classname, contents):
32     return "<span class=\"%s\">%s</span>" % (classname, contents)
33
34
35 def check_dir_exists(kind, path):
36     if not os.path.isdir(path):
37         raise Exception("%s directory %s does not exist" % (kind, path))
38
39
40
41 def status_info_cmp(self, s1, s2):
42     a1 = s1["array"]
43     a2 = s2["array"]
44     c1 = 0
45     c2 = 0
46
47     i = 0
48     while True:
49         if i >= len(a1) or i >= len(a2):
50             break
51
52         if c1 != c2:
53             return c2 - c1
54
55         if a1[i] != a2[i]:
56             return a2[i] - a1[i]
57
58     return s2["value"] - s1["value"]
59
60
61 class BuildfarmDatabase(object):
62
63     OLDAGE = 60*60*4,
64     DEADAGE = 60*60*24*4
65     LCOVHOST = "magni"
66
67     def __init__(self, basedir, readonly=False):
68         self.basedir = basedir
69         check_dir_exists("base", self.basedir)
70         self.readonly = readonly
71
72         self.webdir = os.path.join(basedir, "web")
73         check_dir_exists("web", self.webdir)
74
75         self.datadir = os.path.join(basedir, "data")
76         check_dir_exists("data", self.datadir)
77
78         self.cachedir = os.path.join(basedir, "cache")
79         check_dir_exists("cache", self.cachedir)
80
81         self.lcovdir = os.path.join(basedir, "lcov/data")
82         check_dir_exists("lcov", self.lcovdir)
83
84         self.compilers = util.load_list(os.path.join(self.webdir, "compilers.list"))
85         self.hosts = util.load_hash(os.path.join(self.webdir, "hosts.list"))
86
87         self.trees = {
88             'ccache': {
89                 'scm': 'git',
90                 'repo': 'ccache',
91                 'branch': 'master',
92                 'subdir': '',
93                 'srcdir': ''
94             },
95             'ccache-maint': {
96                 'scm': 'git',
97                 'repo': 'ccache',
98                 'branch': 'maint',
99                 'subdir': '',
100                 'srcdir': ''
101             },
102             'ppp': {
103                 'scm': 'git',
104                 'repo': 'ppp',
105                 'branch': 'master',
106                 'subdir': '',
107                 'srcdir': ''
108             },
109             'build_farm': {
110                 'scm': 'svn',
111                 'repo': 'build-farm',
112                 'branch': 'trunk',
113                 'subdir': '',
114                 'srcdir': ''
115             },
116             'samba-web': {
117                 'scm': 'svn',
118                 'repo': 'samba-web',
119                 'branch': 'trunk',
120                 'subdir': '',
121                 'srcdir': ''
122             },
123             'samba-docs': {
124                 'scm': 'svn',
125                 'repo': 'samba-docs',
126                 'branch': 'trunk',
127                 'subdir': '',
128                 'srcdir': ''
129             },
130             'lorikeet': {
131                 'scm': 'svn',
132                 'repo': 'lorikeeet',
133                 'branch': 'trunk',
134                 'subdir': '',
135                 'srcdir': ''
136             },
137             'samba_3_current': {
138                 'scm': 'git',
139                 'repo': 'samba.git',
140                 'branch': 'v3-5-test',
141                 'subdir': '',
142                 'srcdir': 'source'
143             },
144             'samba_3_next': {
145                 'scm': 'git',
146                 'repo': 'samba.git',
147                 'branch': 'v3-6-test',
148                 'subdir': '',
149                 'srcdir': 'source'
150             },
151             'samba_3_master': {
152                 'scm': 'git',
153                 'repo': 'samba.git',
154                 'branch': 'master',
155                 'subdir': '',
156                 'srcdir': 'source'
157             },
158             'samba_4_0_test': {
159                 'scm': 'git',
160                 'repo': 'samba.git',
161                 'branch': 'master',
162                 'subdir': '',
163                 'srcdir': 'source4'
164             },
165             'libreplace': {
166                 'scm': 'git',
167                 'repo': 'samba.git',
168                 'branch': 'master',
169                 'subdir': 'lib/replace/',
170                 'srcdir': ''
171             },
172             'talloc': {
173                 'scm': 'git',
174                 'repo': 'samba.git',
175                 'branch': 'master',
176                 'subdir': 'lib/talloc/',
177                 'srcdir': ''
178             },
179             'tdb': {
180                 'scm': 'git',
181                 'repo': 'samba.git',
182                 'branch': 'master',
183                 'subdir': 'lib/tdb/',
184                 'srcdir': ''
185             },
186             'ldb': {
187                 'scm': 'git',
188                 'repo': 'samba.git',
189                 'branch': 'master',
190                 'subdir': 'lib/ldb/',
191                 'srcdir': ''
192             },
193             'pidl': {
194                 'scm': 'git',
195                 'repo': 'samba.git',
196                 'branch': 'master',
197                 'subdir': 'pidl/',
198                 'srcdir': ''
199             },
200             'rsync': {
201                 'scm': 'git',
202                 'repo': 'rsync.git',
203                 'branch': 'HEAD',
204                 'subdir': '',
205                 'srcdir': ''
206             }
207         }
208
209     def cache_fname(self, tree, host, compiler, rev=None):
210         if rev is not None:
211             return os.path.join(self.cachedir, "build.%s.%s.%s-%s" % (tree,host,compiler,rev))
212         else:
213             return os.path.join(self.cachedir, "build.%s.%s.%s" % (tree,host,compiler))
214
215     def build_fname(self, tree, host, compiler, rev=None):
216         """get the name of the build file"""
217         if rev is not None:
218             return os.path.join(self.datadir, "oldrevs/build.%s.%s.%s-%s" % (tree, host, compiler, rev))
219         return os.path.join(self.datadir, "upload/build.%s.%s.%s" % (tree, host, compiler))
220
221     ###################
222     # the mtime age is used to determine if builds are still happening
223     # on a host.
224     # the ctime age is used to determine when the last real build happened
225
226     ##############################################
227     def build_age_mtime(self, host, tree, compiler, rev):
228         """get the age of build from mtime"""
229         file = self.build_fname(tree, host, compiler, rev)
230
231         try:
232             st = os.stat("%s.log" % file)
233         except OSError:
234             # File does not exist
235             return -1
236         else:
237             return time.time() - st.st_mtime
238
239     def build_age_ctime(self, host, tree, compiler, rev):
240         """get the age of build from ctime"""
241         file = self.build_fname(tree, host, compiler, rev)
242
243         try:
244             st = os.stat("%s.log" % file)
245         except OSError:
246             return -1
247         else:
248             return time.time() - st.st_ctime
249
250     def build_revision_details(self, host, tree, compiler, rev=None):
251         """get the svn revision of build"""
252         file = self.build_fname(tree, host, compiler, rev)
253         cachef = self.cache_fname(tree, host, compiler, rev)
254
255         # don't fast-path for trees with git repository:
256         # we get the timestamp as rev and want the details
257         if rev:
258             if tree not in self.trees:
259                 return rev
260             if self.trees[tree]["scm"] != "git":
261                 return rev
262
263         try:
264             st1 = os.stat("%s.log" % file)
265         except OSError:
266             # File does not exist
267             return "NO SUCH FILE"
268
269         try:
270             st2 = os.stat("%s.revision" % cachef)
271         except OSError:
272             # File does not exist
273             st2 = None
274
275         # the ctime/mtime asymmetry is needed so we don't get fooled by
276         # the mtime update from rsync 
277         if st2 and st1.st_ctime <= st2.st_mtime:
278             return util.FileLoad("%s.revision" % cachef)
279
280         log = util.FileLoad("%s.log" % file)
281
282         m = re.search("BUILD COMMIT REVISION: (.*)", log)
283         if m:
284             ret = m.group(1)
285         else:
286             m = re.search("BUILD REVISION: (.*)", log)
287             if m:
288                 ret = m.group(1)
289             else:
290                 ret = ""
291
292         m = re.search("BUILD COMMIT TIME: (.*)", log)
293         if m:
294             ret += ":" + m.group(1)
295
296         if not self.readonly:
297             util.FileSave("%s.revision" % cachef, ret)
298
299         return ret
300
301     def build_revision(self, host, tree, compiler, rev):
302         r = self.build_revision_details(host, tree, compiler, rev)
303         return r.split(":")[0]
304
305     def build_revision_time(self, host, tree, compiler, rev):
306         r = self.build_revision_details(host, tree, compiler, rev)
307         return r.split(":", 1)[1]
308
309     def build_status_from_logs(self, log, err):
310         """get status of build"""
311         def span_status(st):
312             if st == 0:
313                 return span("status passed", "ok")
314             else:
315                 return span("status failed", st)
316
317         m = re.search("TEST STATUS:(.*)", log)
318         if m:
319             tstatus = span_status(m.group(1))
320         else:
321             m = re.search("ACTION (PASSED|FAILED): test", log)
322             if m:
323                 test_failures = len(re.findall("testsuite-(failure|error): ", log))
324                 test_successes = len(re.findall("testsuite-success: ", log))
325                 if test_successes > 0:
326                     tstatus = span_status(test_failures)
327                 else:
328                     tstatus = span_status(255)
329             else:
330                 tstatus = span("status unknown", "?")
331
332         m = re.search("INSTALL STATUS:(.*)", log)
333         if m:
334             istatus = span_status(m.group(1))
335         else:
336             istatus = span("status unknown", "?")
337
338         m = re.search("BUILD STATUS:(.*)", log)
339         if m:
340             bstatus = span_status(m.group(1))
341         else:
342             bstatus = span("status unknown", "?")
343
344         m = re.search("CONFIGURE STATUS:(.*)", log)
345         if m:
346             cstatus = span_status(m.group(1))
347         else:
348             cstatus = span("status unknown", "?")
349
350         m = re.search("(PANIC|INTERNAL ERROR):.*", log)
351         if m:
352             sstatus = "/"+span("status panic", "PANIC")
353         else:
354             sstatus = ""
355
356         if "No space left on device" in err or "No space left on device" in log:
357             dstatus = "/"+span("status failed", "disk full")
358         else:
359             dstatus = ""
360
361         if "maximum runtime exceeded" in log:
362             tostatus = "/"+span("status failed", "timeout")
363         else:
364             tostatus = ""
365
366         m = re.search("CC_CHECKER STATUS: (.*)", log)
367         if m and int(m.group(1)) > 0:
368             sstatus += "/".span("status checker", m.group(1))
369
370         return "%s/%s/%s/%s%s%s%s" % (
371                 cstatus, bstatus, istatus, tstatus, sstatus, dstatus, tostatus)
372
373     def build_status(self, host, tree, compiler, rev):
374         """get status of build"""
375         file = self.build_fname(tree, host, compiler, rev)
376         cachefile = self.cache_fname(tree, host, compiler, rev)+".status"
377         try:
378             st1 = os.stat("%s.log" % file)
379         except OSError:
380             # No such file
381             return "Unknown Build"
382
383         try:
384             st2 = os.stat(cachefile)
385         except OSError:
386             # No such file
387             st2 = None
388
389         if st2 and st1.st_ctime <= st2.st_mtime:
390             return util.FileLoad(cachefile)
391
392         log = util.FileLoad("%s.log" % file)
393         try:
394             err = util.FileLoad("%s.err" % file)
395         except OSError:
396             # No such file
397             err = ""
398
399         ret = self.build_status_from_logs(log, err)
400
401         if not self.readonly:
402             util.FileSave(cachefile, ret)
403
404         return ret
405
406     def build_status_info_from_string(self, rev_seq, rev, status_raw):
407         """find the build status as an perl object
408
409         the 'value' gets one point for passing each stage"""
410         status_split = status_raw.split("/")
411         status_str = ""
412         status_arr = []
413         status_val = 0
414
415         for r in status_split:
416             r = r.strip()
417
418             if r == "ok":
419                 e = 0
420             elif r.isdigit():
421                 e = int(r)
422                 if e < 0:
423                     e = 1
424             else:
425                 e = 1
426
427             if status_str != "":
428                 status_str += "/"
429             status_str += "%d" % r
430
431             status_val += e
432
433             status_arr.append(e)
434
435         return {
436             "rev": rev,
437             "rev_seq": rev_seq,
438             "array": status_arr,
439             "string": status_str,
440             "value": status_val,
441             }
442
443     def build_status_info_from_html(self, rev_seq, rev, status_html):
444         """find the build status as an perl object
445
446         the 'value' gets one point for passing each stage
447         """
448         status_raw = util.strip_html(status_html)
449         return self.build_status_info_from_string(rev_seq, rev, status_raw)
450
451     def build_status_info(self, host, tree, compiler, rev_seq):
452         """find the build status as an perl object
453
454         the 'value' gets one point for passing each stage
455         """
456         rev = self.build_revision(host, tree, compiler, rev_seq)
457         status_html = self.build_status(host, tree, compiler, rev_seq)
458         return self.build_status_info_from_html(rev_seq, rev, status_html)
459
460     def lcov_status(self, tree):
461         """get status of build"""
462         cachefile = os.path.join(self.cachedir, "lcov.%s.%s.status" % (self.LCOVHOST, tree))
463         file = os.path.join(self.lcovdir, self.LCOVHOST, tree, "index.html")
464         try:
465             st1 = os.stat(file)
466         except OSError:
467             # File does not exist
468             return ""
469         try:
470             st2 = os.stat(cachefile)
471         except OSError:
472             # file does not exist
473             st2 = None
474
475         if st2 and st1.st_ctime <= st2.st_mtime:
476             return util.FileLoad(cachefile)
477
478         lcov_html = util.FileLoad(file)
479         m = re.search('\<td class="headerItem".*?\>Code\&nbsp\;covered\:\<\/td\>.*?\n.*?\<td class="headerValue".*?\>([0-9.]+) \%', lcov_html)
480         if m:
481             ret = "<a href=\"/lcov/data/%s/%s\">%s %%</a>" % (self.LCOVHOST, tree, m.group(1))
482         else:
483             ret = ""
484         if self.readonly:
485             util.FileSave(cachefile, ret)
486         return ret
487
488     def err_count(self, host, tree, compiler, rev):
489         """get status of build"""
490         file = self.build_fname(tree, host, compiler, rev)
491         cachef = self.cache_fname(tree, host, compiler, rev)
492
493         try:
494             st1 = os.stat("%s.err" % file)
495         except OSError:
496             # File does not exist
497             return 0
498         try:
499             st2 = os.stat("%s.errcount" % cachef)
500         except OSError:
501             # File does not exist
502             st2 = None
503
504         if st2 and st1.st_ctime <= st2.st_mtime:
505             return util.FileLoad("%s.errcount" % cachef)
506
507         try:
508             err = util.FileLoad("%s.err" % file)
509         except OSError:
510             # File does not exist
511             return 0
512
513         ret = util.count_lines(err)
514
515         if not self.readonly:
516             util.FileSave("%s.errcount" % cachef, str(ret))
517
518         return ret
519
520     def read_log(self, tree, host, compiler, rev):
521         """read full log file"""
522         return util.FileLoad(self.build_fname(tree, host, compiler, rev)+".log")
523
524     def read_err(self, tree, host, compiler, rev):
525         """read full err file"""
526         return util.FileLoad(self.build_fname(tree, host, compiler, rev)+".err")
527
528     def get_old_revs(self, tree, host, compiler):
529         """get a list of old builds and their status."""
530         directory = os.path.join(self.datadir, "oldrevs")
531         logfiles = [d for d in os.listdir(directory) if d.startswith("build.%s.%s.%s-" % (tree, host, compiler)) and d.endswith(".log")]
532         for l in logfiles:
533             m = re.match(".*-([0-9A-Fa-f]+).log$", l)
534             if m:
535                 rev = m.group(1)
536                 stat = os.stat(os.path.join(directory, l))
537                 # skip the current build
538                 if stat.st_nlink == 2:
539                     continue
540                 r = {
541                     "STATUS": self.build_status(host, tree, compiler, rev),
542                     "REVISION": rev,
543                     "TIMESTAMP": stat.st_ctime
544                     }
545                 ret.append(r)
546
547         ret.sort(lambda a, b: cmp(a["TIMESTAMP"], b["TIMESTAMP"]))
548
549         return ret
550
551     def has_host(self, host):
552         return host in os.listdir(os.path.join(self.datadir, "upload"))