]> git.ipfire.org Git - people/jschlag/pbs.git/blob - src/buildservice/builders.py
Merge branch 'master' of git://git.ipfire.org/pbs
[people/jschlag/pbs.git] / src / buildservice / builders.py
1 #!/usr/bin/python
2
3 from __future__ import absolute_import, division
4
5 import datetime
6 import hashlib
7 import logging
8 import random
9 import string
10 import time
11
12 from . import base
13 from . import logs
14
15 from .decorators import *
16
17 from .users import generate_password_hash, check_password_hash, generate_random_string
18
19 class Builders(base.Object):
20 def _get_builder(self, query, *args):
21 res = self.db.get(query, *args)
22
23 if res:
24 return Builder(self.backend, res.id, data=res)
25
26 def _get_builders(self, query, *args):
27 res = self.db.query(query, *args)
28
29 for row in res:
30 yield Builder(self.backend, row.id, data=row)
31
32 def __iter__(self):
33 builders = self._get_builders("SELECT * FROM builders \
34 WHERE deleted IS FALSE ORDER BY name")
35
36 return iter(builders)
37
38 def create(self, name, user=None, log=True):
39 """
40 Creates a new builder.
41 """
42 builder = self._get_builder("INSERT INTO builders(name) \
43 VALUES(%s) RETURNING *", name)
44
45 # Generate a new passphrase.
46 passphrase = builder.regenerate_passphrase()
47
48 # Log what we have done.
49 if log:
50 builder.log("created", user=user)
51
52 # The Builder object and the passphrase are returned.
53 return builder, passphrase
54
55 def auth(self, name, passphrase):
56 # If either name or passphrase is None, we don't check at all.
57 if None in (name, passphrase):
58 return
59
60 # Search for the hostname in the database.
61 builder = self._get_builder("SELECT * FROM builders \
62 WHERE name = %s AND deleted IS FALSE", name)
63
64 # If the builder was not found or the passphrase does not match,
65 # you have bad luck.
66 if not builder or not builder.validate_passphrase(passphrase):
67 return
68
69 # Otherwise we return the Builder object.
70 return builder
71
72 def get_by_id(self, builder_id):
73 return self._get_builder("SELECT * FROM builders WHERE id = %s", builder_id)
74
75 def get_by_name(self, name):
76 return self._get_builder("SELECT * FROM builders \
77 WHERE name = %s AND deleted IS FALSE", name)
78
79 def get_history(self, limit=None, offset=None, builder=None, user=None):
80 query = "SELECT * FROM builders_history"
81 args = []
82
83 conditions = []
84
85 if builder:
86 conditions.append("builder_id = %s")
87 args.append(builder.id)
88
89 if user:
90 conditions.append("user_id = %s")
91 args.append(user.id)
92
93 if conditions:
94 query += " WHERE %s" % " AND ".join(conditions)
95
96 query += " ORDER BY time DESC"
97
98 if limit:
99 if offset:
100 query += " LIMIT %s,%s"
101 args += [offset, limit,]
102 else:
103 query += " LIMIT %s"
104 args += [limit,]
105
106 entries = []
107 for entry in self.db.query(query, *args):
108 entry = logs.BuilderLogEntry(self.pakfire, entry)
109 entries.append(entry)
110
111 return entries
112
113
114 class Builder(base.DataObject):
115 table = "builders"
116
117 def __eq__(self, other):
118 if isinstance(other, self.__class__):
119 return self.id == other.id
120
121 def __lt__(self, other):
122 if isinstance(other, self.__class__):
123 return self.name < other.name
124
125 def log(self, action, user=None):
126 user_id = None
127 if user:
128 user_id = user.id
129
130 self.db.execute("INSERT INTO builders_history(builder_id, action, user_id, time) \
131 VALUES(%s, %s, %s, NOW())", self.id, action, user_id)
132
133 def regenerate_passphrase(self):
134 """
135 Generates a new random passphrase and stores it as a salted hash
136 to the database.
137
138 The new passphrase is returned to be sent to the user (once).
139 """
140 # Generate a random string with 40 chars.
141 passphrase = generate_random_string(length=40)
142
143 # Create salted hash.
144 passphrase_hash = generate_password_hash(passphrase)
145
146 # Store the hash in the database.
147 self._set_attribute("passphrase", passphrase_hash)
148
149 # Return the clear-text passphrase.
150 return passphrase
151
152 def validate_passphrase(self, passphrase):
153 """
154 Compare the given passphrase with the one stored in the database.
155 """
156 return check_password_hash(passphrase, self.data.passphrase)
157
158 # Description
159
160 def set_description(self, description):
161 self._set_attribute("description", description)
162
163 description = property(lambda s: s.data.description or "", set_description)
164
165 @property
166 def keepalive(self):
167 """
168 Returns time of last keepalive message from this host.
169 """
170 return self.data.time_keepalive
171
172 def update_keepalive(self, loadavg1=None, loadavg5=None, loadavg15=None,
173 mem_total=None, mem_free=None, swap_total=None, swap_free=None,
174 space_free=None):
175 """
176 Update the keepalive timestamp of this machine.
177 """
178 self.db.execute("UPDATE builders SET time_keepalive = NOW(), \
179 loadavg1 = %s, loadavg5 = %s, loadavg15 = %s, space_free = %s, \
180 mem_total = %s, mem_free = %s, swap_total = %s, swap_free = %s \
181 WHERE id = %s", loadavg1, loadavg5, loadavg15, space_free,
182 mem_total, mem_free, swap_total, swap_free, self.id)
183
184 def update_info(self, cpu_model=None, cpu_count=None, cpu_arch=None, cpu_bogomips=None,
185 pakfire_version=None, host_key=None, os_name=None):
186 # Update all the rest.
187 self.db.execute("UPDATE builders SET time_updated = NOW(), \
188 pakfire_version = %s, cpu_model = %s, cpu_count = %s, cpu_arch = %s, \
189 cpu_bogomips = %s, host_key_id = %s, os_name = %s WHERE id = %s",
190 pakfire_version, cpu_model, cpu_count, cpu_arch, cpu_bogomips,
191 host_key, os_name, self.id)
192
193 def set_enabled(self, enabled):
194 self._set_attribute("enabled", enabled)
195
196 enabled = property(lambda s: s.data.enabled, set_enabled)
197
198 @property
199 def disabled(self):
200 return not self.enabled
201
202 @property
203 def native_arch(self):
204 """
205 The native architecture of this builder
206 """
207 return self.cpu_arch
208
209 @lazy_property
210 def supported_arches(self):
211 # Every builder supports noarch
212 arches = ["noarch"]
213
214 # We can always build our native architeture
215 if self.native_arch:
216 arches.append(self.native_arch)
217
218 # Get all compatible architectures
219 res = self.db.query("SELECT build_arch FROM arches_compat \
220 WHERE native_arch = %s", self.native_arch)
221
222 for row in res:
223 if not row.build_arch in arches:
224 arches.append(row.build_arch)
225
226 return sorted(arches)
227
228 def set_testmode(self, testmode):
229 self._set_attribute("testmode", testmode)
230
231 testmode = property(lambda s: s.data.testmode, set_testmode)
232
233 def set_max_jobs(self, value):
234 self._set_attribute("max_jobs", value)
235
236 max_jobs = property(lambda s: s.data.max_jobs, set_max_jobs)
237
238 @property
239 def name(self):
240 return self.data.name
241
242 @property
243 def hostname(self):
244 return self.name
245
246 @property
247 def passphrase(self):
248 return self.data.passphrase
249
250 # Load average
251
252 @property
253 def loadavg(self):
254 return ", ".join(["%.2f" % l for l in (self.loadavg1, self.loadavg5, self.loadavg15)])
255
256 @property
257 def loadavg1(self):
258 return self.data.loadavg1 or 0.0
259
260 @property
261 def loadavg5(self):
262 return self.data.loadavg5 or 0.0
263
264 @property
265 def loadavg15(self):
266 return self.data.loadavg15 or 0.0
267
268 @property
269 def pakfire_version(self):
270 return self.data.pakfire_version or ""
271
272 @property
273 def os_name(self):
274 return self.data.os_name or ""
275
276 @property
277 def cpu_model(self):
278 return self.data.cpu_model or ""
279
280 @property
281 def cpu_count(self):
282 return self.data.cpu_count
283
284 @property
285 def cpu_arch(self):
286 return self.data.cpu_arch
287
288 @property
289 def cpu_bogomips(self):
290 return self.data.cpu_bogomips or 0.0
291
292 @property
293 def mem_percentage(self):
294 if not self.mem_total:
295 return None
296
297 return self.mem_used * 100 / self.mem_total
298
299 @property
300 def mem_total(self):
301 return self.data.mem_total
302
303 @property
304 def mem_used(self):
305 if self.mem_total and self.mem_free:
306 return self.mem_total - self.mem_free
307
308 @property
309 def mem_free(self):
310 return self.data.mem_free
311
312 @property
313 def swap_percentage(self):
314 if not self.swap_total:
315 return None
316
317 return self.swap_used * 100 / self.swap_total
318
319 @property
320 def swap_total(self):
321 return self.data.swap_total
322
323 @property
324 def swap_used(self):
325 if self.swap_total and self.swap_free:
326 return self.swap_total - self.swap_free
327
328 @property
329 def swap_free(self):
330 return self.data.swap_free
331
332 @property
333 def space_free(self):
334 return self.data.space_free
335
336 @property
337 def host_key_id(self):
338 return self.data.host_key_id
339
340 @property
341 def state(self):
342 if self.disabled:
343 return "disabled"
344
345 if self.data.time_keepalive is None:
346 return "offline"
347
348 #if self.data.updated >= 5*60:
349 # return "offline"
350
351 return "online"
352
353 @lazy_property
354 def active_jobs(self, *args, **kwargs):
355 return self.pakfire.jobs.get_active(builder=self, *args, **kwargs)
356
357 @property
358 def too_many_jobs(self):
359 """
360 Tell if this host is already running enough or too many jobs.
361 """
362 return len(self.active_jobs) >= self.max_jobs
363
364 @lazy_property
365 def jobqueue(self):
366 return self.backend.jobqueue.for_arches(self.supported_arches)
367
368 def get_next_job(self):
369 """
370 Returns the next job in line for this builder.
371 """
372 # Don't send any jobs to disabled builders
373 if not self.enabled:
374 return
375
376 # Don't return anything if the builder has already too many jobs running
377 if self.too_many_jobs:
378 return
379
380 for job in self.jobqueue:
381 # Only allow building test jobs in test mode
382 if self.testmode and not job.test:
383 continue
384
385 return job
386
387 def get_history(self, *args, **kwargs):
388 kwargs["builder"] = self
389
390 return self.pakfire.builders.get_history(*args, **kwargs)