Unset basename when removing logs.
[amitay/build-farm.git] / buildfarm / sqldb.py
1 #!/usr/bin/python
2
3 # Samba.org buildfarm
4 # Copyright (C) 2010 Jelmer Vernooij <jelmer@samba.org>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19
20 from buildfarm import (
21     BuildFarm,
22     Tree,
23     )
24 from buildfarm.data import (
25     Build,
26     BuildResultStore,
27     BuildStatus,
28     NoSuchBuildError,
29     )
30 from buildfarm.hostdb import (
31     Host,
32     HostDatabase,
33     HostAlreadyExists,
34     NoSuchHost,
35     )
36
37 import os
38 try:
39     from pysqlite2 import dbapi2 as sqlite3
40 except ImportError:
41     import sqlite3
42 from storm.database import create_database
43 from storm.locals import Bool, Desc, Int, Unicode, RawStr
44 from storm.store import Store
45
46
47 class StormBuild(Build):
48     __storm_table__ = "build"
49
50     id = Int(primary=True)
51     tree = RawStr()
52     revision = RawStr()
53     host = RawStr()
54     compiler = RawStr()
55     checksum = RawStr()
56     upload_time = Int(name="age")
57     status_str = RawStr(name="status")
58     basename = RawStr()
59     host_id = Int()
60
61     def status(self):
62         return BuildStatus.__deserialize__(self.status_str)
63
64     def revision_details(self):
65         return (self.revision, None)
66
67     def log_checksum(self):
68         return self.checksum
69
70     def remove(self):
71         super(StormBuild, self).remove()
72         Store.of(self).remove(self)
73
74     def remove_logs(self):
75         super(StormBuild, self).remove_logs()
76         self.basename = None
77
78
79 class StormHost(Host):
80     __storm_table__ = "host"
81
82     id = Int(primary=True)
83     name = RawStr()
84     owner_name = Unicode(name="owner")
85     owner_email = Unicode()
86     password = Unicode()
87     ssh_access = Bool()
88     fqdn = RawStr()
89     platform = Unicode()
90     permission = Unicode()
91     last_dead_mail = Int()
92     join_time = Int()
93
94     def _set_owner(self, value):
95         if value is None:
96             self.owner_name = None
97             self.owner_email = None
98         else:
99             (self.owner_name, self.owner_email) = value
100
101     def _get_owner(self):
102         if self.owner_name is None:
103             return None
104         else:
105             return (self.owner_name, self.owner_email)
106
107     owner = property(_get_owner, _set_owner)
108
109
110 class StormHostDatabase(HostDatabase):
111
112     def __init__(self, store=None):
113         if store is None:
114             self.store = memory_store()
115         else:
116             self.store = store
117
118     def createhost(self, name, platform=None, owner=None, owner_email=None,
119             password=None, permission=None):
120         """See `HostDatabase.createhost`."""
121         newhost = StormHost(name, owner=owner, owner_email=owner_email,
122                 password=password, permission=permission, platform=platform)
123         try:
124             self.store.add(newhost)
125             self.store.flush()
126         except sqlite3.IntegrityError:
127             raise HostAlreadyExists(name)
128         return newhost
129
130     def deletehost(self, name):
131         """Remove a host."""
132         self.store.remove(self[name])
133
134     def hosts(self):
135         """Retrieve an iterable over all hosts."""
136         return self.store.find(StormHost).order_by(StormHost.name)
137
138     def __getitem__(self, name):
139         ret = self.store.find(StormHost, StormHost.name==name).one()
140         if ret is None:
141             raise NoSuchHost(name)
142         return ret
143
144     def commit(self):
145         self.store.commit()
146
147
148 class StormCachingBuildResultStore(BuildResultStore):
149
150     def __init__(self, basedir, store=None):
151         super(StormCachingBuildResultStore, self).__init__(basedir)
152
153         if store is None:
154             store = memory_store()
155
156         self.store = store
157
158     def __contains__(self, build):
159         return (self._get_by_checksum(build) is not None)
160
161     def get_previous_revision(self, tree, host, compiler, revision):
162         result = self.store.find(StormBuild,
163             StormBuild.tree == tree,
164             StormBuild.host == host,
165             StormBuild.compiler == compiler,
166             StormBuild.revision == revision)
167         cur_build = result.any()
168         if cur_build is None:
169             raise NoSuchBuildError(tree, host, compiler, revision)
170
171         result = self.store.find(StormBuild,
172             StormBuild.tree == tree,
173             StormBuild.host == host,
174             StormBuild.compiler == compiler,
175             StormBuild.revision != revision,
176             StormBuild.id < cur_build.id)
177         result = result.order_by(Desc(StormBuild.id))
178         prev_build = result.first()
179         if prev_build is None:
180             raise NoSuchBuildError(tree, host, compiler, revision)
181         return prev_build.revision
182
183     def get_latest_revision(self, tree, host, compiler):
184         result = self.store.find(StormBuild,
185             StormBuild.tree == tree,
186             StormBuild.host == host,
187             StormBuild.compiler == compiler)
188         result = result.order_by(Desc(StormBuild.id))
189         build = result.first()
190         if build is None:
191             raise NoSuchBuildError(tree, host, compiler)
192         return build.revision
193
194     def _get_by_checksum(self, build):
195         result = self.store.find(StormBuild,
196             StormBuild.checksum == build.log_checksum())
197         return result.one()
198
199     def upload_build(self, build):
200         existing_build = self._get_by_checksum(build)
201         if existing_build is not None:
202             # Already present
203             assert build.tree == existing_build.tree
204             assert build.host == existing_build.host
205             assert build.compiler == existing_build.compiler
206             return existing_build
207         rev, timestamp = build.revision_details()
208         super(StormCachingBuildResultStore, self).upload_build(build)
209         new_basename = self.build_fname(build.tree, build.host, build.compiler, rev)
210         new_build = StormBuild(new_basename, build.tree, build.host,
211             build.compiler, rev)
212         new_build.checksum = build.log_checksum()
213         new_build.upload_time = build.upload_time
214         new_build.status_str = build.status().__serialize__()
215         new_build.basename = new_basename
216         self.store.add(new_build)
217         return new_build
218
219     def get_old_revs(self, tree, host, compiler):
220         return self.store.find(StormBuild,
221             StormBuild.tree == tree,
222             StormBuild.host == host,
223             StormBuild.compiler == compiler).order_by(Desc(StormBuild.upload_time))
224
225     def get_build(self, tree, host, compiler, revision=None, checksum=None):
226         expr = [
227             StormBuild.tree == tree,
228             StormBuild.host == host,
229             StormBuild.compiler == compiler,
230             ]
231         if revision is not None:
232             expr.append(StormBuild.revision == revision)
233         if checksum is not None:
234             expr.append(StormBuild.checksum == checksum)
235         result = self.store.find(StormBuild, *expr).order_by(Desc(StormBuild.upload_time))
236         ret = result.first()
237         if ret is None:
238             raise NoSuchBuildError(tree, host, compiler, revision)
239         return ret
240
241
242 class StormCachingBuildFarm(BuildFarm):
243
244     def __init__(self, path=None, store=None, timeout=0.5):
245         self.timeout = timeout
246         self.store = store
247         super(StormCachingBuildFarm, self).__init__(path)
248
249     def _get_store(self):
250         if self.store is not None:
251             return self.store
252         db_path = os.path.join(self.path, "db", "hostdb.sqlite")
253         db = create_database("sqlite:%s?timeout=%f" % (db_path, self.timeout))
254         self.store = Store(db)
255         setup_schema(self.store)
256         return self.store
257
258     def _open_hostdb(self):
259         return StormHostDatabase(self._get_store())
260
261     def _open_build_results(self):
262         return StormCachingBuildResultStore(os.path.join(self.path, "data", "oldrevs"),
263             self._get_store())
264
265     def get_host_builds(self, host):
266         return self._get_store().find(StormBuild,
267             StormBuild.host==host).group_by(StormBuild.compiler, StormBuild.tree)
268
269     def get_tree_builds(self, tree):
270         result = self._get_store().find(StormBuild, StormBuild.tree == tree)
271         return result.order_by(Desc(StormBuild.upload_time))
272
273     def get_last_builds(self):
274         result = self._get_store().find(StormBuild)
275         return result.group_by(
276             StormBuild.tree, StormBuild.compiler, StormBuild.host).order_by(
277                 Desc(StormBuild.upload_time))
278
279     def commit(self):
280         self.store.commit()
281
282
283 class StormTree(Tree):
284     __storm_table__ = "tree"
285
286     id = Int(primary=True)
287     name = RawStr()
288     scm = Int()
289     branch = RawStr()
290     subdir = RawStr()
291     repo = RawStr()
292     scm = RawStr()
293
294
295 def setup_schema(db):
296     db.execute("PRAGMA foreign_keys = 1;", noresult=True)
297     db.execute("""
298 CREATE TABLE IF NOT EXISTS host (
299     id integer primary key autoincrement,
300     name blob not null,
301     owner text,
302     owner_email text,
303     password text,
304     ssh_access int,
305     fqdn text,
306     platform text,
307     permission text,
308     last_dead_mail int,
309     join_time int
310 );""", noresult=True)
311     db.execute("CREATE UNIQUE INDEX IF NOT EXISTS unique_hostname ON host (name);", noresult=True)
312     db.execute("""
313 CREATE TABLE IF NOT EXISTS build (
314     id integer primary key autoincrement,
315     tree blob not null,
316     revision blob,
317     host blob not null,
318     host_id integer,
319     compiler blob not null,
320     checksum blob,
321     age int,
322     status blob,
323     basename blob,
324     FOREIGN KEY (host_id) REFERENCES host (id)
325 );""", noresult=True)
326     db.execute("CREATE UNIQUE INDEX IF NOT EXISTS unique_checksum ON build (checksum);", noresult=True)
327     db.execute("""
328 CREATE TABLE IF NOT EXISTS tree (
329     id integer primary key autoincrement,
330     name blob not null,
331     scm int,
332     branch blob,
333     subdir blob,
334     repo blob
335     );""", noresult=True)
336
337
338 def memory_store():
339     db = create_database("sqlite:")
340     store = Store(db)
341     setup_schema(store)
342     return store