]>
Commit | Line | Data |
---|---|---|
9137135a MT |
1 | #!/usr/bin/python |
2 | ||
3 | import datetime | |
4 | import logging | |
5 | import uuid | |
6 | import time | |
7 | import tornado.locale | |
8 | ||
9 | import base | |
10 | import packages | |
11 | ||
12 | from constants import * | |
13 | ||
14 | def 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 | ||
32 | class 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 | ||
166 | class _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 | ||
399 | class 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 | ||
466 | class 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 |