]> git.ipfire.org Git - people/jschlag/pbs.git/blame - backend/builders.py
Remove FULLTEXT index from users table.
[people/jschlag/pbs.git] / backend / builders.py
CommitLineData
9137135a
MT
1#!/usr/bin/python
2
f96eb5ed
MT
3from __future__ import division
4
9137135a
MT
5import datetime
6import hashlib
7import logging
8import random
9import string
10import time
11
12import base
f6e6ff79
MT
13import logs
14
15from users import generate_password_hash, check_password_hash, generate_random_string
9137135a
MT
16
17class Builders(base.Object):
f6e6ff79
MT
18 def auth(self, name, passphrase):
19 # If either name or passphrase is None, we don't check at all.
20 if None in (name, passphrase):
21 return
22
23 # Search for the hostname in the database.
24 # The builder must not be deleted.
25 builder = self.db.get("SELECT id FROM builders WHERE name = %s AND \
26 NOT status = 'deleted'", name)
27
28 if not builder:
29 return
30
31 # Get the whole Builder object from the database.
32 builder = self.get_by_id(builder.id)
33
34 # If the builder was not found or the passphrase does not match,
35 # you have bad luck.
36 if not builder or not builder.validate_passphrase(passphrase):
37 return
38
39 # Otherwise we return the Builder object.
40 return builder
41
9137135a 42 def get_all(self):
9fa1787c 43 builders = self.db.query("SELECT * FROM builders WHERE NOT status = 'deleted' ORDER BY name")
9137135a 44
9fa1787c 45 return [Builder(self.pakfire, b.id, b) for b in builders]
9137135a
MT
46
47 def get_by_id(self, id):
48 if not id:
49 return
50
51 return Builder(self.pakfire, id)
52
53 def get_by_name(self, name):
9fa1787c 54 builder = self.db.get("SELECT * FROM builders WHERE name = %s LIMIT 1", name)
9137135a
MT
55
56 if builder:
9fa1787c 57 return Builder(self.pakfire, builder.id, builder)
9137135a
MT
58
59 def get_all_arches(self):
60 arches = set()
61
62 for result in self.db.query("SELECT DISTINCT arches FROM builders"):
63 if not result.arches:
64 continue
65
66 _arches = result.arches.split()
67
68 for arch in _arches:
69 arches.add(arch)
70
71 return sorted(arches)
72
f6e6ff79 73 def get_load(self):
f96eb5ed
MT
74 res1 = self.db.get("SELECT SUM(max_jobs) AS max_jobs FROM builders \
75 WHERE status = 'enabled'")
f6e6ff79 76
f96eb5ed
MT
77 res2 = self.db.get("SELECT COUNT(*) AS count FROM jobs \
78 WHERE state = 'dispatching' OR state = 'running' OR state = 'uploading'")
f6e6ff79 79
33857fed
MT
80 try:
81 return (res2.count * 100 / res1.max_jobs)
82 except:
83 return 0
f6e6ff79
MT
84
85 def get_history(self, limit=None, offset=None, builder=None, user=None):
86 query = "SELECT * FROM builders_history"
87 args = []
88
89 conditions = []
90
91 if builder:
92 conditions.append("builder_id = %s")
93 args.append(builder.id)
94
95 if user:
96 conditions.append("user_id = %s")
97 args.append(user.id)
98
99 if conditions:
100 query += " WHERE %s" % " AND ".join(conditions)
101
102 query += " ORDER BY time DESC"
103
104 if limit:
105 if offset:
106 query += " LIMIT %s,%s"
107 args += [offset, limit,]
108 else:
109 query += " LIMIT %s"
110 args += [limit,]
111
112 entries = []
113 for entry in self.db.query(query, *args):
114 entry = logs.BuilderLogEntry(self.pakfire, entry)
115 entries.append(entry)
116
117 return entries
118
9137135a
MT
119
120class Builder(base.Object):
9fa1787c 121 def __init__(self, pakfire, id, data=None):
9137135a
MT
122 base.Object.__init__(self, pakfire)
123
124 self.id = id
125
f6e6ff79 126 # Cache.
9fa1787c 127 self._data = data
f6e6ff79
MT
128 self._active_jobs = None
129 self._arches = None
130 self._disabled_arches = None
9137135a
MT
131
132 def __cmp__(self, other):
318a6dfd
MT
133 if other is None:
134 return -1
135
9137135a
MT
136 return cmp(self.id, other.id)
137
f6e6ff79
MT
138 @property
139 def data(self):
140 if self._data is None:
f96eb5ed
MT
141 self._data = self.db.get("SELECT * FROM builders WHERE id = %s", self.id)
142 assert self._data
f6e6ff79
MT
143
144 return self._data
145
9137135a 146 @classmethod
f6e6ff79
MT
147 def create(cls, pakfire, name, user=None, log=True):
148 """
149 Creates a new builder.
150 """
151 builder_id = pakfire.db.execute("INSERT INTO builders(name, time_created) \
152 VALUES(%s, NOW())", name)
9137135a 153
f6e6ff79
MT
154 # Create Builder object.
155 builder = cls(pakfire, builder_id)
9137135a 156
f6e6ff79
MT
157 # Generate a new passphrase.
158 passphrase = builder.regenerate_passphrase()
159
160 # Log what we have done.
161 if log:
162 builder.log("created", user=user)
163
164 # The Builder object and the passphrase are returned.
165 return builder, passphrase
166
167 def log(self, action, user=None):
168 user_id = None
169 if user:
170 user_id = user.id
171
172 self.db.execute("INSERT INTO builders_history(builder_id, action, user_id, time) \
173 VALUES(%s, %s, %s, NOW())", self.id, action, user_id)
9137135a
MT
174
175 def set(self, key, value):
176 self.db.execute("UPDATE builders SET %s = %%s WHERE id = %%s LIMIT 1" % key,
177 value, self.id)
178 self.data[key] = value
179
9137135a 180 def regenerate_passphrase(self):
f6e6ff79
MT
181 """
182 Generates a new random passphrase and stores it as a salted hash
183 to the database.
184
185 The new passphrase is returned to be sent to the user (once).
186 """
187 # Generate a random string with 20 chars.
188 passphrase = generate_random_string(length=20)
189
190 # Create salted hash.
191 passphrase_hash = generate_password_hash(passphrase)
192
193 # Store the hash in the database.
194 self.db.execute("UPDATE builders SET passphrase = %s WHERE id = %s",
195 passphrase_hash, self.id)
9137135a 196
f6e6ff79
MT
197 # Return the clear-text passphrase.
198 return passphrase
9137135a
MT
199
200 def validate_passphrase(self, passphrase):
f6e6ff79
MT
201 """
202 Compare the given passphrase with the one stored in the database.
203 """
204 return check_password_hash(passphrase, self.data.passphrase)
205
206 @property
207 def description(self):
208 return self.data.description or ""
209
210 @property
211 def status(self):
212 return self.data.status
213
214 def update_description(self, description):
215 self.db.execute("UPDATE builders SET description = %s, time_updated = NOW() \
216 WHERE id = %s", description, self.id)
217
218 if self._data:
219 self._data["description"] = description
220
221 @property
222 def keepalive(self):
223 """
224 Returns time of last keepalive message from this host.
225 """
226 return self.data.time_keepalive
227
228 def update_keepalive(self, loadavg, free_space):
229 """
230 Update the keepalive timestamp of this machine.
231 """
232 if free_space is None:
233 free_space = 0
234
235 self.db.execute("UPDATE builders SET time_keepalive = NOW(), loadavg = %s, \
236 free_space = %s WHERE id = %s", loadavg, free_space, self.id)
237
238 logging.debug("Builder %s updated it keepalive status: %s" \
239 % (self.name, loadavg))
240
241 def needs_update(self):
242 query = self.db.get("SELECT time_updated, NOW() - time_updated \
243 AS seconds FROM builders WHERE id = %s", self.id)
244
245 # If there has been no update at all, we will need a new one.
246 if query.time_updated is None:
247 return True
248
249 # Require an update after the data is older than 24 hours.
250 return query.seconds >= 24*3600
251
252 def update_info(self, arches, cpu_model, cpu_count, memory, pakfire_version=None, host_key_id=None):
253 # Update architecture information.
254 self.update_arches(arches)
255
256 # Update all the rest.
257 self.db.execute("UPDATE builders SET time_updated = NOW(), \
258 pakfire_version = %s, cpu_model = %s, cpu_count = %s, memory = %s, \
259 host_key_id = %s \
260 WHERE id = %s", pakfire_version or "", cpu_model, cpu_count, memory,
261 host_key_id, self.id)
262
263 def update_arches(self, arches):
264 # Get all arches this builder does currently support.
265 supported_arches = [a.name for a in self.get_arches()]
266
267 # Noarch is always supported.
268 if not "noarch" in arches:
269 arches.append("noarch")
270
271 arches_add = []
272 for arch in arches:
273 if arch in supported_arches:
274 supported_arches.remove(arch)
275 continue
276
277 arches_add.append(arch)
278 arches_rem = supported_arches
279
280 for arch_name in arches_add:
281 arch = self.pakfire.arches.get_by_name(arch_name)
282 if not arch:
283 logging.info("Client sent unknown architecture: %s" % arch_name)
284 continue
285
286 self.db.execute("INSERT INTO builders_arches(builder_id, arch_id) \
287 VALUES(%s, %s)", self.id, arch.id)
288
289 for arch_name in arches_rem:
290 arch = self.pakfire.arches.get_by_name(arch_name)
291 assert arch
292
293 self.db.execute("DELETE FROM builders_arches WHERE builder_id = %s \
294 AND arch_id = %s", self.id, arch.id)
9137135a 295
f6e6ff79
MT
296 def update_overload(self, overload):
297 if overload:
298 overload = "Y"
299 else:
300 overload = "N"
301
302 self.db.execute("UPDATE builders SET overload = %s WHERE id = %s",
303 overload, self.id)
304 self._data["overload"] = overload
305
306 logging.debug("Builder %s updated it overload status to %s" % \
307 (self.name, self.overload))
9137135a
MT
308
309 def get_enabled(self):
f6e6ff79 310 return self.status == "enabled"
9137135a
MT
311
312 def set_enabled(self, value):
f6e6ff79
MT
313 # XXX deprecated
314
9137135a 315 if value:
f6e6ff79 316 value = "enabled"
9137135a 317 else:
f6e6ff79 318 value = "disabled"
9137135a 319
f6e6ff79 320 self.set_status(value)
9137135a
MT
321
322 enabled = property(get_enabled, set_enabled)
323
324 @property
325 def disabled(self):
f6e6ff79
MT
326 return not self.enabled
327
328 def set_status(self, status, user=None, log=True):
329 assert status in ("created", "enabled", "disabled", "deleted")
330
331 if self.status == status:
332 return
333
334 self.db.execute("UPDATE builders SET status = %s WHERE id = %s",
335 status, self.id)
336
337 if self._data:
338 self._data["status"] = status
339
340 if log:
341 self.log(status, user=user)
342
343 def get_arches(self, enabled=None):
344 """
345 A list of architectures that are supported by this builder.
346 """
347 if enabled is True:
348 enabled = "Y"
349 elif enabled is False:
350 enabled = "N"
351 else:
352 enabled = None
353
354 query = "SELECT arch_id AS id FROM builders_arches WHERE builder_id = %s"
355 args = [self.id,]
356
357 if enabled:
358 query += " AND enabled = %s"
359 args.append(enabled)
360
361 # Get all other arches from the database.
362 arches = []
363 for arch in self.db.query(query, *args):
364 arch = self.pakfire.arches.get_by_id(arch.id)
365 arches.append(arch)
366
367 # Save a sorted list of supported architectures.
368 arches.sort()
369
370 return arches
9137135a
MT
371
372 @property
373 def arches(self):
f6e6ff79
MT
374 if self._arches is None:
375 self._arches = self.get_arches(enabled=True)
9137135a 376
f6e6ff79 377 return self._arches
9137135a 378
f6e6ff79
MT
379 @property
380 def disabled_arches(self):
381 if self._disabled_arches is None:
382 self._disabled_arches = self.get_arches(enabled=False)
9137135a 383
f6e6ff79 384 return self._disabled_arches
9137135a 385
f6e6ff79
MT
386 def set_arch_status(self, arch, enabled):
387 if enabled:
388 enabled = "Y"
389 else:
390 enabled = "N"
391
392 self.db.execute("UPDATE builders_arches SET enabled = %s \
393 WHERE builder_id = %s AND arch_id = %s", enabled, self.id, arch.id)
394
395 # Reset the arch cache.
396 self._arches = None
9137135a 397
f6e6ff79
MT
398 def get_build_release(self):
399 return self.data.build_release == "Y"
400
401 def set_build_release(self, value):
9137135a
MT
402 if value:
403 value = "Y"
404 else:
405 value = "N"
406
f6e6ff79
MT
407 self.db.execute("UPDATE builders SET build_release = %s WHERE id = %s",
408 value, self.id)
409
410 # Update the cache.
411 if self._data:
412 self._data["build_release"] = value
9137135a 413
f6e6ff79 414 build_release = property(get_build_release, set_build_release)
9137135a 415
f6e6ff79
MT
416 def get_build_scratch(self):
417 return self.data.build_scratch == "Y"
9137135a 418
f6e6ff79 419 def set_build_scratch(self, value):
9137135a
MT
420 if value:
421 value = "Y"
422 else:
423 value = "N"
424
f6e6ff79
MT
425 self.db.execute("UPDATE builders SET build_scratch = %s WHERE id = %s",
426 value, self.id)
427
428 # Update the cache.
429 if self._data:
430 self._data["build_scratch"] = value
9137135a 431
f6e6ff79 432 build_scratch = property(get_build_scratch, set_build_scratch)
9137135a
MT
433
434 def get_build_test(self):
435 return self.data.build_test == "Y"
436
437 def set_build_test(self, value):
438 if value:
439 value = "Y"
440 else:
441 value = "N"
442
f6e6ff79
MT
443 self.db.execute("UPDATE builders SET build_test = %s WHERE id = %s",
444 value, self.id)
445
446 # Update the cache.
447 if self._data:
448 self._data["build_test"] = value
9137135a
MT
449
450 build_test = property(get_build_test, set_build_test)
451
f6e6ff79
MT
452 @property
453 def build_types(self):
454 ret = []
455
456 if self.build_release:
457 ret.append("release")
458
459 if self.build_scratch:
460 ret.append("scratch")
461
462 if self.build_test:
463 ret.append("test")
464
465 return ret
466
9137135a
MT
467 def get_max_jobs(self):
468 return self.data.max_jobs
469
470 def set_max_jobs(self, value):
471 self.set("max_jobs", value)
472
473 max_jobs = property(get_max_jobs, set_max_jobs)
474
475 @property
476 def name(self):
477 return self.data.name
478
479 @property
480 def hostname(self):
481 return self.name
482
483 @property
484 def passphrase(self):
485 return self.data.passphrase
486
487 @property
488 def loadavg(self):
f6e6ff79
MT
489 if self.state == "online":
490 return self.data.loadavg
9137135a 491
f6e6ff79
MT
492 @property
493 def load1(self):
494 try:
495 load1, load5, load15 = self.loadavg.split(", ")
496 except:
497 return None
498
499 return load1
500
501 @property
502 def pakfire_version(self):
503 return self.data.pakfire_version or ""
9137135a
MT
504
505 @property
506 def cpu_model(self):
f6e6ff79
MT
507 return self.data.cpu_model or ""
508
509 @property
510 def cpu_count(self):
511 return self.data.cpu_count
9137135a
MT
512
513 @property
514 def memory(self):
f6e6ff79 515 return self.data.memory
9137135a
MT
516
517 @property
f6e6ff79
MT
518 def free_space(self):
519 return self.data.free_space or 0
520
521 @property
522 def overload(self):
523 return self.data.overload == "Y"
524
525 @property
526 def host_key_id(self):
527 return self.data.host_key_id
528
529 @property
530 def state(self):
9137135a 531 if self.disabled:
f6e6ff79 532 return "disabled"
9137135a 533
f6e6ff79
MT
534 if self.data.time_keepalive is None:
535 return "offline"
9137135a 536
f96eb5ed
MT
537 #if self.data.updated >= 5*60:
538 # return "offline"
9137135a 539
f6e6ff79 540 return "online"
9137135a 541
163d9d8b
MT
542 def get_active_jobs(self, *args, **kwargs):
543 if self._active_jobs is None:
544 self._active_jobs = self.pakfire.jobs.get_active(builder=self, *args, **kwargs)
545
546 return self._active_jobs
547
548 def count_active_jobs(self):
549 return len(self.get_active_jobs())
550
551 @property
552 def too_many_jobs(self):
553 """
554 Tell if this host is already running enough or too many jobs.
555 """
556 return self.count_active_jobs() >= self.max_jobs
9e8a20d7 557
163d9d8b
MT
558 def get_next_jobs(self, arches=None, limit=None):
559 if arches is None:
560 arches = self.get_arches()
9e8a20d7 561
163d9d8b
MT
562 return self.pakfire.jobs.get_next(arches=arches, builder=self,
563 state="pending", limit=limit)
9e8a20d7 564
163d9d8b
MT
565 def get_next_job(self, *args, **kwargs):
566 kwargs["limit"] = 1
9e8a20d7 567
163d9d8b 568 jobs = self.get_next_jobs(*args, **kwargs)
9137135a 569
163d9d8b
MT
570 if jobs:
571 return jobs[0]
f6e6ff79
MT
572
573 def get_history(self, *args, **kwargs):
574 kwargs["builder"] = self
575
576 return self.pakfire.builders.get_history(*args, **kwargs)