]> git.ipfire.org Git - people/jschlag/pbs.git/blame - backend/build.py
Initial import.
[people/jschlag/pbs.git] / backend / build.py
CommitLineData
9137135a
MT
1#!/usr/bin/python
2
3import datetime
4import logging
5import uuid
6import time
7import tornado.locale
8
9import base
10import packages
11
12from constants import *
13
14def Build(pakfire, id):
15 """
16 Proxy function that returns the right object depending on the type
17 of the build.
18 """
19 build = pakfire.db.get("SELECT type FROM builds WHERE id = %s", id)
20 if not build:
21 raise Exception, "Build not found"
22
23 for cls in (BinaryBuild, SourceBuild):
24 if not build.type == cls.type:
25 continue
26
27 return cls(pakfire, id)
28
29 raise Exception, "Unknown type: %s" % build.type
30
31
32class Builds(base.Object):
33 """
34 Object that represents all builds.
35 """
36
37 def get_by_id(self, id):
38 return Build(self.pakfire, id)
39
40 def get_by_uuid(self, uuid):
41 build = self.db.get("SELECT id FROM builds WHERE uuid = %s LIMIT 1", uuid)
42
43 if build:
44 return Build(self.pakfire, build.id)
45
46 def get_latest(self, state=None, builder=None, limit=10, type=None):
47 query = "SELECT id FROM builds"
48
49 where = []
50 if builder:
51 where.append("host = '%s'" % builder)
52
53 if state:
54 where.append("state = '%s'" % state)
55
56 if type:
57 where.append("type = '%s'" % type)
58
59 if where:
60 query += " WHERE %s" % " AND ".join(where)
61
62 query += " ORDER BY updated DESC LIMIT %s"
63
64 builds = self.db.query(query, limit)
65
66 return [Build(self.pakfire, b.id) for b in builds]
67
68 def get_by_pkgid(self, pkg_id):
69 builds = self.db.query("""SELECT builds.id as id FROM builds
70 LEFT JOIN builds_binary ON builds_binary.id = builds.build_id
71 WHERE builds_binary.pkg_id = %s AND type = 'binary'""", pkg_id)
72
73 return [Build(self.pakfire, b.id) for b in builds]
74
75 def get_by_host(self, host_id):
76 builds = self.db.query("SELECT id FROM builds WHERE host = %s", host_id)
77
78 return [Build(self.pakfire, b.id) for b in builds]
79
80 def get_by_source(self, source_id):
81 builds = self.db.query("""SELECT builds.id as id FROM builds
82 LEFT JOIN builds_source ON builds_source.id = builds.build_id
83 WHERE builds_source.source_id = %s""", source_id)
84
85 return [Build(self.pakfire, b.id) for b in builds]
86
87 def get_active(self, type=None, host_id=None):
88 running_states = ("dispatching", "running", "uploading",)
89
90 query = "SELECT id FROM builds WHERE (%s)" % \
91 " OR ".join(["state = '%s'" % s for s in running_states])
92
93 if type:
94 query += " AND type = '%s'" % type
95
96 if host_id:
97 query += " AND host = %s" % host_id
98
99 builds = self.db.query(query)
100
101 return [Build(self.pakfire, b.id) for b in builds]
102
103 def get_all_but_finished(self):
104 builds = self.db.query("SELECT id FROM builds WHERE"
105 " NOT state = 'finished' AND NOT state = 'permanently_failed'")
106
107 return [Build(self.pakfire, b.id) for b in builds]
108
109 def get_next(self, type=None, arches=None, limit=None, offset=None):
110 query = "SELECT builds.id as id, build_id FROM builds"
111
112 wheres = ["state = 'pending'", "start_not_before <= NOW()",]
113
114 if type:
115 wheres.append("type = '%s'" % type)
116
117 if type == "binary" and arches:
118 query += " LEFT JOIN builds_binary ON builds.build_id = builds_binary.id"
119 arches = ["builds_binary.arch='%s'" % a for a in arches]
120
121 wheres.append("(%s)" % " OR ".join(arches))
122 elif arches:
123 raise Exception, "Cannot use arches when type is not 'binary'"
124
125 if wheres:
126 query += " WHERE %s" % " AND ".join(wheres)
127
128 # Choose the oldest one at first.
129 query += " ORDER BY priority DESC, time_added ASC"
130
131 if limit:
132 if offset:
133 query += " LIMIT %s,%s" % (limit, offset)
134 else:
135 query += " LIMIT %s" % limit
136
137 builds = [Build(self.pakfire, b.id) for b in self.db.query(query)]
138
139 if limit == 1 and builds:
140 return builds[0]
141
142 return builds
143
144 def count(self, state=None):
145 query = "SELECT COUNT(*) as c FROM builds"
146
147 wheres = []
148 if state:
149 wheres.append("state = '%s'" % state)
150
151 if wheres:
152 query += " WHERE %s" % " AND ".join(wheres)
153
154 result = self.db.get(query)
155
156 return result.c
157
158 def average_build_time(self):
159 result = self.db.get("SELECT AVG(time_finished - time_started) as average"
160 " FROM builds WHERE type = 'binary' AND time_started IS NOT NULL"
161 " AND time_finished IS NOT NULL")
162
163 return result.average or 0
164
165
166class _Build(base.Object):
167 STATE2LOG = {
168 "pending" : LOG_BUILD_STATE_PENDING,
169 "dispatching" : LOG_BUILD_STATE_DISPATCHING,
170 "running" : LOG_BUILD_STATE_RUNNING,
171 "failed" : LOG_BUILD_STATE_FAILED,
172 "permanently_failed" : LOG_BUILD_STATE_PERM_FAILED,
173 "dependency_error" : LOG_BUILD_STATE_DEP_ERROR,
174 "waiting" : LOG_BUILD_STATE_WAITING,
175 "finished" : LOG_BUILD_STATE_FINISHED,
176 "unknown" : LOG_BUILD_STATE_UNKNOWN,
177 "uploading" : LOG_BUILD_STATE_UPLOADING,
178 }
179
180 def __init__(self, pakfire, id):
181 base.Object.__init__(self, pakfire)
182 self.id = id
183
184 self._data = self.db.get("SELECT * FROM builds WHERE id = %s LIMIT 1", self.id)
185
186 def set(self, key, value):
187 self.db.execute("UPDATE builds SET %s = %%s WHERE id = %%s" % key, value, self.id)
188 self._data[key] = value
189
190 @property
191 def build_id(self):
192 return self._data.build_id
193
194 def get_state(self):
195 return self._data.state
196
197 def set_state(self, state):
198 try:
199 log = self.STATE2LOG[state]
200 except KeyErrror:
201 raise Exception, "Trying to set an invalid build state: %s" % state
202
203 # Setting state.
204 self.set("state", state)
205
206 # Inform everybody what happened to the build job.
207 if state == "finished":
208 self.db.execute("UPDATE builds SET time_finished = NOW()"
209 " WHERE id = %s", self.id)
210
211 self.send_finished_message()
212
213 elif state == "failed":
214 self.send_failed_message()
215
216 self.db.execute("UPDATE builds SET time_started = NULL,"
217 " time_finished = NULL WHERE id = %s LIMIT 1", self.id)
218
219 elif state == "pending":
220 self.retries += 1
221
222 elif state in ("dispatching", "running",):
223 self.db.execute("UPDATE builds SET time_started = NOW(),"
224 " time_finished = NULL WHERE id = %s LIMIT 1", self.id)
225
226 # Log the state change.
227 self.logger(log)
228
229 state = property(get_state, set_state)
230
231 @property
232 def finished(self):
233 return self.state == "finished"
234
235 def get_message(self):
236 return self._data.message
237
238 def set_message(self, message):
239 self.set("message", message)
240
241 message = property(get_message, set_message)
242
243 @property
244 def uuid(self):
245 return self._data.uuid
246
247 def get_host(self):
248 return self.pakfire.builders.get_by_id(self._data.host)
249
250 def set_host(self, host):
251 builder = self.pakfire.builders.get_by_name(host)
252
253 self.set("host", builder.id)
254
255 host = property(get_host, set_host)
256
257 def get_retries(self):
258 return self._data.retries
259
260 def set_retries(self, retries):
261 self.set("retries", retries)
262
263 retries = property(get_retries, set_retries)
264
265 @property
266 def time_added(self):
267 return self._data.time_added
268
269 @property
270 def time_started(self):
271 return self._data.time_started
272
273 @property
274 def time_finished(self):
275 return self._data.time_finished
276
277 @property
278 def duration(self):
279 if not self.time_finished or not self.time_started:
280 return
281
282 return self.time_finished - self.time_started
283
284 def get_priority(self):
285 return self._data.priority
286
287 def set_priority(self, value):
288 self.set("priority", value)
289
290 priority = property(get_priority, set_priority)
291
292 @property
293 def log(self):
294 return self.db.query("SELECT * FROM log WHERE build_id = %s ORDER BY time DESC, id DESC", self.id)
295
296 def logger(self, message, text=""):
297 self.pakfire.logger(message, text, build=self)
298
299 @property
300 def source(self):
301 return self.pkg.source
302
303 @property
304 def files(self):
305 files = []
306
307 for p in self.db.query("SELECT id, type FROM package_files WHERE build_id = %s", self.uuid):
308 for p_class in (packages.SourcePackageFile, packages.BinaryPackageFile, packages.LogFile):
309 if p.type == p_class.type:
310 p = p_class(self.pakfire, p.id)
311 break
312 else:
313 continue
314
315 files.append(p)
316
317 return sorted(files)
318
319 @property
320 def packagefiles(self):
321 return [f for f in self.files if isinstance(f, packages.PackageFile)]
322
323 @property
324 def logfiles(self):
325 return [f for f in self.files if isinstance(f, packages.LogFile)]
326
327 @property
328 def recipients(self):
329 return []
330
331 def send_finished_message(self):
332 info = {
333 "build_name" : self.name,
334 "build_host" : self.host.name,
335 "build_uuid" : self.uuid,
336 }
337
338 self.pakfire.messages.send_to_all(self.recipients, MSG_BUILD_FINISHED_SUBJECT,
339 MSG_BUILD_FINISHED, info)
340
341 def send_failed_message(self):
342 build_host = "--"
343 if self.host:
344 build_host = self.host.name
345
346 info = {
347 "build_name" : self.name,
348 "build_host" : build_host,
349 "build_uuid" : self.uuid,
350 }
351
352 self.pakfire.messages.send_to_all(self.recipients, MSG_BUILD_FAILED_SUBJECT,
353 MSG_BUILD_FAILED, info)
354
355 def keepalive(self):
356 """
357 This function is used to prevent build jobs from getting stuck on
358 something.
359 """
360
361 # Get the seconds since we are running.
362 try:
363 time_running = datetime.datetime.utcnow() - self.time_started
364 time_running = time_running.total_seconds()
365 except:
366 time_running = 0
367
368 if self.state == "dispatching":
369 # If the dispatching is running more than 15 minutes, we set the
370 # build to be failed.
371 if time_running >= 900:
372 self.state = "failed"
373
374 elif self.state in ("running", "uploading"):
375 # If the build is running/uploading more than 24 hours, we kill it.
376 if time_running >= 3600 * 24:
377 self.state = "failed"
378
379 elif self.state == "dependency_error":
380 # Resubmit job when it has waited for twelve hours.
381 if time_running >= 3600 * 12:
382 self.state = "pending"
383
384 elif self.state == "failed":
385 # Automatically resubmit jobs that failed after one day.
386 if time_running >= 3600 * 24:
387 self.state = "pending"
388
389 def schedule_rebuild(self, offset):
390 # You cannot do this if the build job has already finished.
391 if self.finished:
392 return
393
394 self.db.execute("UPDATE builds SET start_not_before = NOW() + %s"
395 " WHERE id = %s LIMIT 1", offset, self.id)
396 self.state = "pending"
397
398
399class BinaryBuild(_Build):
400 type = "binary"
401
402 def __init__(self, *args, **kwargs):
403 _Build.__init__(self, *args, **kwargs)
404
405 _data = self.db.get("SELECT * FROM builds_binary WHERE id = %s", self.build_id)
406 del _data["id"]
407 self._data.update(_data)
408
409 self.pkg = self.pakfire.packages.get_by_id(self.pkg_id)
410
411 @classmethod
412 def new(cls, pakfire, pkg, arch):
413 now = datetime.datetime.utcnow()
414
415 build_id = pakfire.db.execute("INSERT INTO builds_binary(pkg_id, arch)"
416 " VALUES(%s, %s)", pkg.id, arch)
417
418 id = pakfire.db.execute("INSERT INTO builds(uuid, build_id, time_added)"
419 " VALUES(%s, %s, %s)", uuid.uuid4(), build_id, now)
420
421 build = cls(pakfire, id)
422 build.logger(LOG_BUILD_CREATED)
423
424 return build
425
426 @property
427 def name(self):
428 return "%s.%s" % (self.pkg.friendly_name, self.arch)
429
430 @property
431 def arch(self):
432 return self._data.arch
433
434 @property
435 def distro(self):
436 return self.pkg.distro
437
438 @property
439 def pkg_id(self):
440 return self._data.pkg_id
441
442 @property
443 def source_build(self):
444 return self.pkg.source_build
445
446 @property
447 def recipients(self):
448 l = set()
449
450 # Get all recipients from the source build (like committer and author).
451 for r in self.source_build.recipients:
452 l.add(r)
453
454 # Add the package maintainer.
455 l.add(self.pkg.maintainer)
456
457 return l
458
459 def add_log(self, filename):
460 self.pkg.add_log(filename, self)
461
462 def schedule_test(self, offset):
463 pass # XXX TBD
464
465
466class SourceBuild(_Build):
467 type = "source"
468
469 def __init__(self, *args, **kwargs):
470 _Build.__init__(self, *args, **kwargs)
471
472 _data = self.db.get("SELECT * FROM builds_source WHERE id = %s", self.build_id)
473 del _data["id"]
474 self._data.update(_data)
475
476 @classmethod
477 def new(cls, pakfire, source_id, revision, author, committer, subject, body, date):
478 now = datetime.datetime.utcnow()
479
480 # Check if the revision does already exist. If so, just return.
481 if pakfire.db.query("SELECT id FROM builds_source WHERE revision = %s", revision):
482 logging.warning("There is already a source build job for rev %s" % revision)
483 return
484
485 build_id = pakfire.db.execute("INSERT INTO builds_source(source_id,"
486 " revision, author, committer, subject, body, date) VALUES(%s, %s, %s, %s,"
487 " %s, %s, %s)", source_id, revision, author, committer, subject, body, date)
488
489 id = pakfire.db.execute("INSERT INTO builds(uuid, type, build_id, time_added)"
490 " VALUES(%s, %s, %s, %s)", uuid.uuid4(), "source", build_id, now)
491
492 build = cls(pakfire, id)
493 build.logger(LOG_BUILD_CREATED)
494
495 # Source builds are immediately pending.
496 build.state = "pending"
497
498 return build
499
500 @property
501 def arch(self):
502 return "src"
503
504 @property
505 def name(self):
506 s = "%s:" % self.source.name
507
508 if self.commit_subject:
509 s += " %s" % self.commit_subject[:60]
510 if len(self.commit_subject) > 60:
511 s += "..."
512 else:
513 s += self.revision[:7]
514
515 return s
516
517 @property
518 def source(self):
519 return self.pakfire.sources.get_by_id(self._data.source_id)
520
521 @property
522 def revision(self):
523 return self._data.revision
524
525 @property
526 def commit_author(self):
527 return self._data.author
528
529 @property
530 def commit_committer(self):
531 return self._data.committer
532
533 @property
534 def commit_subject(self):
535 return self._data.subject
536
537 @property
538 def commit_body(self):
539 return self._data.body
540
541 @property
542 def commit_date(self):
543 return self._data.date
544
545 @property
546 def recipients(self):
547 l = [self.commit_author, self.commit_committer,]
548
549 return set(l)
550
551 def send_finished_message(self):
552 # We do not send finish messages on source build jobs.
553 pass