Cope with files that already exist.
[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
75 class StormHost(Host):
76     __storm_table__ = "host"
77
78     id = Int(primary=True)
79     name = RawStr()
80     owner_name = Unicode(name="owner")
81     owner_email = Unicode()
82     password = Unicode()
83     ssh_access = Bool()
84     fqdn = RawStr()
85     platform = Unicode()
86     permission = Unicode()
87     last_dead_mail = Int()
88     join_time = Int()
89
90     def _set_owner(self, value):
91         if value is None:
92             self.owner_name = None
93             self.owner_email = None
94         else:
95             (self.owner_name, self.owner_email) = value
96
97     def _get_owner(self):
98         if self.owner_name is None:
99             return None
100         else:
101             return (self.owner_name, self.owner_email)
102
103     owner = property(_get_owner, _set_owner)
104
105
106 class StormHostDatabase(HostDatabase):
107
108     def __init__(self, store=None):
109         if store is None:
110             self.store = memory_store()
111         else:
112             self.store = store
113
114     def createhost(self, name, platform=None, owner=None, owner_email=None,
115             password=None, permission=None):
116         """See `HostDatabase.createhost`."""
117         newhost = StormHost(name, owner=owner, owner_email=owner_email,
118                 password=password, permission=permission, platform=platform)
119         try:
120             self.store.add(newhost)
121             self.store.flush()
122         except sqlite3.IntegrityError:
123             raise HostAlreadyExists(name)
124         return newhost
125
126     def deletehost(self, name):
127         """Remove a host."""
128         self.store.remove(self[name])
129
130     def hosts(self):
131         """Retrieve an iterable over all hosts."""
132         return self.store.find(StormHost).order_by(StormHost.name)
133
134     def __getitem__(self, name):
135         ret = self.store.find(StormHost, StormHost.name==name).one()
136         if ret is None:
137             raise NoSuchHost(name)
138         return ret
139
140     def commit(self):
141         self.store.commit()
142
143
144 class StormCachingBuildResultStore(BuildResultStore):
145
146     def __init__(self, basedir, store=None):
147         super(StormCachingBuildResultStore, self).__init__(basedir)
148
149         if store is None:
150             store = memory_store()
151
152         self.store = store
153
154     def __contains__(self, build):
155         return (self._get_by_checksum(build) is not None)
156
157     def get_previous_revision(self, tree, host, compiler, revision):
158         result = self.store.find(StormBuild,
159             StormBuild.tree == tree,
160             StormBuild.host == host,
161             StormBuild.compiler == compiler,
162             StormBuild.revision == revision)
163         cur_build = result.any()
164         if cur_build is None:
165             raise NoSuchBuildError(tree, host, compiler, revision)
166
167         result = self.store.find(StormBuild,
168             StormBuild.tree == tree,
169             StormBuild.host == host,
170             StormBuild.compiler == compiler,
171             StormBuild.revision != revision,
172             StormBuild.id < cur_build.id)
173         result = result.order_by(Desc(StormBuild.id))
174         prev_build = result.first()
175         if prev_build is None:
176             raise NoSuchBuildError(tree, host, compiler, revision)
177         return prev_build.revision
178
179     def get_latest_revision(self, tree, host, compiler):
180         result = self.store.find(StormBuild,
181             StormBuild.tree == tree,
182             StormBuild.host == host,
183             StormBuild.compiler == compiler)
184         result = result.order_by(Desc(StormBuild.id))
185         build = result.first()
186         if build is None:
187             raise NoSuchBuildError(tree, host, compiler)
188         return build.revision
189
190     def _get_by_checksum(self, build):
191         result = self.store.find(StormBuild,
192             StormBuild.checksum == build.log_checksum())
193         return result.one()
194
195     def upload_build(self, build):
196         existing_build = self._get_by_checksum(build)
197         if existing_build is not None:
198             # Already present
199             assert build.tree == existing_build.tree
200             assert build.host == existing_build.host
201             assert build.compiler == existing_build.compiler
202             return existing_build
203         rev, timestamp = build.revision_details()
204         super(StormCachingBuildResultStore, self).upload_build(build)
205         new_basename = self.build_fname(build.tree, build.host, build.compiler, rev)
206         new_build = StormBuild(new_basename, build.tree, build.host,
207             build.compiler, rev)
208         new_build.checksum = build.log_checksum()
209         new_build.upload_time = build.upload_time
210         new_build.status_str = build.status().__serialize__()
211         new_build.basename = new_basename
212         self.store.add(new_build)
213         return new_build
214
215     def get_old_revs(self, tree, host, compiler):
216         return self.store.find(StormBuild,
217             StormBuild.tree == tree,
218             StormBuild.host == host,
219             StormBuild.compiler == compiler).order_by(Desc(StormBuild.upload_time))
220
221     def get_build(self, tree, host, compiler, revision=None, checksum=None):
222         expr = [
223             StormBuild.tree == tree,
224             StormBuild.host == host,
225             StormBuild.compiler == compiler,
226             ]
227         if revision is not None:
228             expr.append(StormBuild.revision == revision)
229         if checksum is not None:
230             expr.append(StormBuild.checksum == checksum)
231         result = self.store.find(StormBuild, *expr).order_by(Desc(StormBuild.upload_time))
232         ret = result.first()
233         if ret is None:
234             raise NoSuchBuildError(tree, host, compiler, revision)
235         return ret
236
237
238 class StormCachingBuildFarm(BuildFarm):
239
240     def __init__(self, path=None, store=None, timeout=0.5):
241         self.timeout = timeout
242         self.store = store
243         super(StormCachingBuildFarm, self).__init__(path)
244
245     def _get_store(self):
246         if self.store is not None:
247             return self.store
248         db_path = os.path.join(self.path, "db", "hostdb.sqlite")
249         db = create_database("sqlite:%s?timeout=%f" % (db_path, self.timeout))
250         self.store = Store(db)
251         setup_schema(self.store)
252         return self.store
253
254     def _open_hostdb(self):
255         return StormHostDatabase(self._get_store())
256
257     def _open_build_results(self):
258         return StormCachingBuildResultStore(os.path.join(self.path, "data", "oldrevs"),
259             self._get_store())
260
261     def get_host_builds(self, host):
262         return self._get_store().find(StormBuild,
263             StormBuild.host==host).group_by(StormBuild.compiler, StormBuild.tree)
264
265     def get_tree_builds(self, tree):
266         result = self._get_store().find(StormBuild, StormBuild.tree == tree)
267         return result.order_by(Desc(StormBuild.upload_time))
268
269     def get_last_builds(self):
270         result = self._get_store().find(StormBuild)
271         return result.group_by(
272             StormBuild.tree, StormBuild.compiler, StormBuild.host).order_by(
273                 Desc(StormBuild.upload_time))
274
275     def commit(self):
276         self.store.commit()
277
278
279 class StormTree(Tree):
280     __storm_table__ = "tree"
281
282     id = Int(primary=True)
283     name = RawStr()
284     scm = Int()
285     branch = RawStr()
286     subdir = RawStr()
287     repo = RawStr()
288     scm = RawStr()
289
290
291 def setup_schema(db):
292     db.execute("PRAGMA foreign_keys = 1;", noresult=True)
293     db.execute("""
294 CREATE TABLE IF NOT EXISTS host (
295     id integer primary key autoincrement,
296     name blob not null,
297     owner text,
298     owner_email text,
299     password text,
300     ssh_access int,
301     fqdn text,
302     platform text,
303     permission text,
304     last_dead_mail int,
305     join_time int
306 );""", noresult=True)
307     db.execute("CREATE UNIQUE INDEX IF NOT EXISTS unique_hostname ON host (name);", noresult=True)
308     db.execute("""
309 CREATE TABLE IF NOT EXISTS build (
310     id integer primary key autoincrement,
311     tree blob not null,
312     revision blob,
313     host blob not null,
314     host_id integer,
315     compiler blob not null,
316     checksum blob,
317     age int,
318     status blob,
319     basename blob,
320     FOREIGN KEY (host_id) REFERENCES host (id)
321 );""", noresult=True)
322     db.execute("CREATE UNIQUE INDEX IF NOT EXISTS unique_checksum ON build (checksum);", noresult=True)
323     db.execute("""
324 CREATE TABLE IF NOT EXISTS tree (
325     id integer primary key autoincrement,
326     name blob not null,
327     scm int,
328     branch blob,
329     subdir blob,
330     repo blob
331     );""", noresult=True)
332
333
334 def memory_store():
335     db = create_database("sqlite:")
336     store = Store(db)
337     setup_schema(store)
338     return store