]>
Commit | Line | Data |
---|---|---|
f6e6ff79 MT |
1 | #!/usr/bin/python |
2 | ||
3 | import base64 | |
4 | import hashlib | |
c2902b29 | 5 | import json |
f6e6ff79 | 6 | import logging |
c2902b29 | 7 | import time |
f6e6ff79 | 8 | import tornado.web |
f6e6ff79 | 9 | |
2c909128 MT |
10 | from .. import builds |
11 | from .. import builders | |
12 | from .. import uploads | |
13 | from .. import users | |
14 | ||
c2902b29 MT |
15 | class LongPollMixin(object): |
16 | def initialize(self): | |
17 | self._start_time = time.time() | |
f6e6ff79 | 18 | |
c2902b29 MT |
19 | def add_timeout(self, timeout, callback): |
20 | deadline = time.time() + timeout | |
f6e6ff79 | 21 | |
c2902b29 | 22 | return self.application.ioloop.add_timeout(deadline, callback) |
f6e6ff79 | 23 | |
c2902b29 MT |
24 | def on_connection_close(self): |
25 | logging.debug("Connection closed unexpectedly") | |
f6e6ff79 | 26 | |
c2902b29 MT |
27 | def connection_closed(self): |
28 | return self.request.connection.stream.closed() | |
f6e6ff79 | 29 | |
c2902b29 MT |
30 | @property |
31 | def runtime(self): | |
32 | return time.time() - self._start_time | |
f6e6ff79 | 33 | |
f6e6ff79 | 34 | |
c2902b29 MT |
35 | class BaseHandler(LongPollMixin, tornado.web.RequestHandler): |
36 | @property | |
37 | def backend(self): | |
f6e6ff79 | 38 | """ |
4b9167ef | 39 | Shortcut handler to pakfire instance |
f6e6ff79 | 40 | """ |
4b9167ef | 41 | return self.application.backend |
f6e6ff79 | 42 | |
c2902b29 | 43 | def get_basic_auth_credentials(self): |
f6e6ff79 MT |
44 | """ |
45 | This handles HTTP Basic authentication. | |
46 | """ | |
47 | auth_header = self.request.headers.get("Authorization", None) | |
48 | ||
49 | # If no authentication information was provided, we stop here. | |
50 | if not auth_header: | |
c2902b29 | 51 | return None, None |
f6e6ff79 MT |
52 | |
53 | # No basic auth? We cannot handle that. | |
54 | if not auth_header.startswith("Basic "): | |
55 | raise tornado.web.HTTPError(400, "Can only handle Basic auth.") | |
56 | ||
f6e6ff79 | 57 | try: |
c2902b29 MT |
58 | # Decode the authentication information. |
59 | auth_header = base64.decodestring(auth_header[6:]) | |
60 | ||
f6e6ff79 MT |
61 | name, password = auth_header.split(":", 1) |
62 | except: | |
63 | raise tornado.web.HTTPError(400, "Authorization data was malformed") | |
64 | ||
c2902b29 | 65 | return name, password |
f6e6ff79 | 66 | |
c2902b29 MT |
67 | def get_current_user(self): |
68 | name, password = self.get_basic_auth_credentials() | |
69 | if name is None: | |
70 | return | |
f6e6ff79 | 71 | |
c2902b29 MT |
72 | builder = self.backend.builders.auth(name, password) |
73 | if builder: | |
74 | return builder | |
f6e6ff79 | 75 | |
c2902b29 MT |
76 | user = self.backend.users.auth(name, password) |
77 | if user: | |
78 | return user | |
f6e6ff79 MT |
79 | |
80 | @property | |
81 | def builder(self): | |
2c909128 | 82 | if isinstance(self.current_user, builders.Builder): |
c2902b29 | 83 | return self.current_user |
f6e6ff79 | 84 | |
c2902b29 MT |
85 | @property |
86 | def user(self): | |
2c909128 | 87 | if isinstance(self.current_user, users.User): |
c2902b29 | 88 | return self.current_user |
f6e6ff79 | 89 | |
c2902b29 MT |
90 | def get_argument_int(self, *args, **kwargs): |
91 | arg = self.get_argument(*args, **kwargs) | |
f6e6ff79 | 92 | |
c2902b29 MT |
93 | try: |
94 | return int(arg) | |
95 | except (TypeError, ValueError): | |
96 | return None | |
f6e6ff79 | 97 | |
c2902b29 MT |
98 | def get_argument_float(self, *args, **kwargs): |
99 | arg = self.get_argument(*args, **kwargs) | |
f6e6ff79 | 100 | |
c2902b29 MT |
101 | try: |
102 | return float(arg) | |
103 | except (TypeError, ValueError): | |
104 | return None | |
f6e6ff79 | 105 | |
c2902b29 MT |
106 | def get_argument_json(self, *args, **kwargs): |
107 | arg = self.get_argument(*args, **kwargs) | |
f6e6ff79 | 108 | |
c2902b29 MT |
109 | if arg: |
110 | return json.loads(arg) | |
f6e6ff79 | 111 | |
c2902b29 MT |
112 | |
113 | class NoopHandler(BaseHandler): | |
114 | def get(self): | |
115 | if self.builder: | |
116 | self.write("Welcome to the Pakfire hub, %s!" % self.builder.hostname) | |
117 | elif self.user: | |
118 | self.write("Welcome to the Pakfire hub, %s!" % self.user.name) | |
f6e6ff79 | 119 | else: |
c2902b29 | 120 | self.write("Welcome to the Pakfire hub!") |
f6e6ff79 | 121 | |
f6e6ff79 | 122 | |
c2902b29 MT |
123 | class ErrorTestHandler(BaseHandler): |
124 | def get(self, error_code=200): | |
125 | """ | |
126 | For testing a client. | |
f6e6ff79 | 127 | |
c2902b29 MT |
128 | This just returns a HTTP response with the given code. |
129 | """ | |
130 | try: | |
131 | error_code = int(error_code) | |
132 | except ValueError: | |
133 | error_code = 200 | |
f6e6ff79 | 134 | |
c2902b29 | 135 | raise tornado.web.HTTPError(error_code) |
f6e6ff79 | 136 | |
f6e6ff79 | 137 | |
6efed544 MT |
138 | class StatsBuildsTypesHandler(BaseHandler): |
139 | def get(self): | |
140 | ret = {} | |
141 | ||
142 | types = self.backend.builds.get_types_stats() | |
143 | for type, count in types.items(): | |
144 | ret["builds_type_%s" % type] = count | |
145 | ||
146 | self.write(ret) | |
147 | ||
148 | ||
149 | class StatsJobsHandler(BaseHandler): | |
150 | def get(self): | |
151 | ret = {} | |
152 | ||
153 | self.write(ret) | |
154 | ||
155 | ||
156 | class StatsJobsDurationsHandler(BaseHandler): | |
157 | def get(self): | |
158 | durations = self.backend.jobs.get_build_durations() | |
159 | ||
160 | ret = {} | |
161 | for platform, d in durations.items(): | |
162 | ret.update({ | |
163 | "jobs_durations_%s_minimum" % platform : d.get("minimum", None), | |
164 | "jobs_durations_%s_maximum" % platform : d.get("maximum", None), | |
165 | "jobs_durations_%s_average" % platform : d.get("average", None), | |
166 | "jobs_durations_%s_stddev" % platform : d.get("stddev", None), | |
167 | }) | |
168 | ||
169 | self.write(ret) | |
170 | ||
171 | class StatsJobsStatesHandler(BaseHandler): | |
172 | def get(self): | |
173 | ret = {} | |
174 | ||
175 | states = self.backend.jobs.get_state_stats() | |
176 | for state, count in states.items(): | |
177 | ret["jobs_state_%s" % state] = count | |
178 | ||
179 | self.write(ret) | |
180 | ||
181 | ||
c2902b29 MT |
182 | class StatsJobsQueueHandler(BaseHandler): |
183 | def get(self): | |
6efed544 MT |
184 | ret = {} |
185 | ||
186 | # Queue length(s). | |
fd43d5e1 | 187 | ret["job_queue_length"] = len(self.backend.jobqueue) |
6efed544 MT |
188 | |
189 | # Average waiting time. | |
fd43d5e1 | 190 | ret["job_queue_avg_wait_time"] = self.backend.jobqueue.average_waiting_time |
f6e6ff79 | 191 | |
c2902b29 | 192 | self.write(ret) |
f6e6ff79 | 193 | |
f6e6ff79 | 194 | |
c2902b29 | 195 | # Uploads |
f6e6ff79 | 196 | |
c2902b29 MT |
197 | class UploadsCreateHandler(BaseHandler): |
198 | """ | |
199 | Create a new upload object in the database and return a unique ID | |
200 | to the uploader. | |
201 | """ | |
f6e6ff79 MT |
202 | |
203 | @tornado.web.authenticated | |
c2902b29 MT |
204 | def get(self): |
205 | # XXX Check permissions | |
206 | ||
207 | filename = self.get_argument("filename") | |
208 | filesize = self.get_argument_int("filesize") | |
209 | filehash = self.get_argument("hash") | |
210 | ||
2c909128 | 211 | upload = uploads.Upload.create(self.backend, filename, filesize, |
c2902b29 | 212 | filehash, user=self.user, builder=self.builder) |
f6e6ff79 | 213 | |
c2902b29 | 214 | self.finish(upload.uuid) |
f6e6ff79 | 215 | |
c2902b29 MT |
216 | |
217 | class UploadsSendChunkHandler(BaseHandler): | |
f6e6ff79 | 218 | @tornado.web.authenticated |
c2902b29 MT |
219 | def post(self, upload_id): |
220 | upload = self.backend.uploads.get_by_uuid(upload_id) | |
f6e6ff79 MT |
221 | if not upload: |
222 | raise tornado.web.HTTPError(404, "Invalid upload id.") | |
223 | ||
224 | if not upload.builder == self.builder: | |
225 | raise tornado.web.HTTPError(403, "Uploading an other host's file.") | |
226 | ||
c2902b29 MT |
227 | chksum = self.get_argument("chksum") |
228 | data = self.get_argument("data") | |
229 | ||
230 | # Decode data. | |
231 | data = base64.b64decode(data) | |
232 | ||
233 | # Calculate hash and compare. | |
234 | h = hashlib.new("sha512") | |
235 | h.update(data) | |
236 | ||
237 | if not chksum == h.hexdigest(): | |
238 | raise tornado.web.HTTPError(400, "Checksum mismatch") | |
239 | ||
240 | # Append the data to file. | |
241 | upload.append(data) | |
242 | ||
f6e6ff79 | 243 | |
c2902b29 | 244 | class UploadsFinishedHandler(BaseHandler): |
f6e6ff79 | 245 | @tornado.web.authenticated |
c2902b29 MT |
246 | def get(self, upload_id): |
247 | upload = self.backend.uploads.get_by_uuid(upload_id) | |
f6e6ff79 MT |
248 | if not upload: |
249 | raise tornado.web.HTTPError(404, "Invalid upload id.") | |
250 | ||
251 | if not upload.builder == self.builder: | |
252 | raise tornado.web.HTTPError(403, "Uploading an other host's file.") | |
253 | ||
254 | # Validate the uploaded data to its hash. | |
255 | ret = upload.validate() | |
256 | ||
257 | # If the validation was successfull, we mark the upload | |
258 | # as finished and send True to the client. | |
259 | if ret: | |
260 | upload.finished() | |
c2902b29 MT |
261 | self.finish("OK") |
262 | ||
263 | return | |
f6e6ff79 MT |
264 | |
265 | # In case the download was corrupted or incomplete, we delete it | |
266 | # and tell the client to start over. | |
267 | upload.remove() | |
f6e6ff79 | 268 | |
c2902b29 MT |
269 | self.finish("ERROR: CORRUPTED OR INCOMPLETE FILE") |
270 | ||
271 | ||
272 | class UploadsDestroyHandler(BaseHandler): | |
f6e6ff79 | 273 | @tornado.web.authenticated |
c2902b29 MT |
274 | def get(self, upload_id): |
275 | upload = self.backend.uploads.get_by_uuid(upload_id) | |
f6e6ff79 MT |
276 | if not upload: |
277 | raise tornado.web.HTTPError(404, "Invalid upload id.") | |
278 | ||
279 | if not upload.builder == self.builder: | |
280 | raise tornado.web.HTTPError(403, "Removing an other host's file.") | |
281 | ||
282 | # Remove the upload from the database and trash the data. | |
283 | upload.remove() | |
284 | ||
285 | ||
c2902b29 | 286 | # Builds |
f6e6ff79 | 287 | |
c2902b29 MT |
288 | class BuildsCreateHandler(BaseHandler): |
289 | @tornado.web.authenticated | |
290 | def get(self): | |
291 | # Get the upload ID of the package file. | |
292 | upload_id = self.get_argument("upload_id") | |
f6e6ff79 | 293 | |
c2902b29 MT |
294 | # Get the identifier of the distribution we build for. |
295 | distro_ident = self.get_argument("distro") | |
f6e6ff79 | 296 | |
c2902b29 MT |
297 | # Get a list of arches to build for. |
298 | arches = self.get_argument("arches", None) | |
299 | if arches == "": | |
300 | arches = None | |
f6e6ff79 | 301 | |
c2902b29 MT |
302 | # Process build type. |
303 | build_type = self.get_argument("build_type") | |
304 | if build_type == "release": | |
305 | check_for_duplicates = True | |
306 | elif build_type == "scratch": | |
307 | check_for_duplicates = False | |
308 | else: | |
309 | raise tornado.web.HTTPError(400, "Invalid build type") | |
f6e6ff79 | 310 | |
c2902b29 MT |
311 | ## Check if the user has permission to create a build. |
312 | # Users only have the permission to create scratch builds. | |
313 | if self.user and not build_type == "scratch": | |
314 | raise tornado.web.HTTPError(403, "Users are only allowed to upload scratch builds") | |
f6e6ff79 | 315 | |
c2902b29 MT |
316 | # Get previously uploaded file to create this build from. |
317 | upload = self.backend.uploads.get_by_uuid(upload_id) | |
318 | if not upload: | |
319 | raise tornado.web.HTTPError(400, "Upload does not exist: %s" % upload_id) | |
f6e6ff79 | 320 | |
c2902b29 MT |
321 | # Check if the uploaded file belongs to this user/builder. |
322 | if self.user and not upload.user == self.user: | |
323 | raise tornado.web.HTTPError(400, "Upload does not belong to this user.") | |
f6e6ff79 | 324 | |
c2902b29 MT |
325 | elif self.builder and not upload.builder == self.builder: |
326 | raise tornado.web.HTTPError(400, "Upload does not belong to this builder.") | |
f6e6ff79 | 327 | |
c2902b29 MT |
328 | # Get distribution this package should be built for. |
329 | distro = self.backend.distros.get_by_ident(distro_ident) | |
330 | if not distro: | |
331 | distro = self.backend.distros.get_default() | |
f6e6ff79 | 332 | |
c2902b29 MT |
333 | # Open the package that was uploaded earlier and add it to |
334 | # the database. Create a new build object from the uploaded package. | |
335 | args = { | |
336 | "arches" : arches, | |
337 | "check_for_duplicates" : check_for_duplicates, | |
338 | "distro" : distro, | |
339 | "type" : build_type, | |
340 | } | |
341 | if self.user: | |
342 | args["owner"] = self.user | |
f6e6ff79 | 343 | |
c2902b29 | 344 | try: |
2c909128 | 345 | pkg, build = builds.import_from_package(self.backend, upload.path, **args) |
f6e6ff79 | 346 | |
c2902b29 MT |
347 | except: |
348 | # Raise any exception. | |
349 | raise | |
f6e6ff79 | 350 | |
c2902b29 MT |
351 | else: |
352 | # Creating the build will move the file to the build directory, | |
353 | # so we can safely remove the uploaded file. | |
354 | upload.remove() | |
f6e6ff79 | 355 | |
c2902b29 MT |
356 | # Send the build ID back to the user. |
357 | self.finish(build.uuid) | |
f6e6ff79 | 358 | |
f6e6ff79 | 359 | |
c2902b29 MT |
360 | class BuildsGetHandler(BaseHandler): |
361 | def get(self, build_uuid): | |
362 | build = self.backend.builds.get_by_uuid(build_uuid) | |
363 | if not build: | |
364 | raise tornado.web.HTTPError(404, "Could not find build: %s" % build_uuid) | |
f6e6ff79 | 365 | |
c2902b29 MT |
366 | ret = { |
367 | "distro" : build.distro.identifier, | |
368 | "jobs" : [j.uuid for j in build.jobs], | |
369 | "name" : build.name, | |
370 | "package" : build.pkg.uuid, | |
371 | "priority" : build.priority, | |
f6e6ff79 | 372 | "score" : build.credits, |
c2902b29 MT |
373 | "severity" : build.severity, |
374 | "state" : build.state, | |
375 | "sup_arches" : build.supported_arches, | |
376 | "time_created" : build.created.isoformat(), | |
377 | "type" : build.type, | |
378 | "uuid" : build.uuid, | |
f6e6ff79 MT |
379 | } |
380 | ||
381 | # If the build is in a repository, update that bit. | |
382 | if build.repo: | |
c2902b29 | 383 | ret["repo"] = build.repo.identifier |
f6e6ff79 | 384 | |
c2902b29 | 385 | self.finish(ret) |
f6e6ff79 | 386 | |
f6e6ff79 | 387 | |
c2902b29 | 388 | # Jobs |
f6e6ff79 | 389 | |
ba9f9092 MT |
390 | class JobsBaseHandler(BaseHandler): |
391 | def job2json(self, job): | |
f6e6ff79 | 392 | ret = { |
22b715d7 | 393 | "arch" : job.arch, |
c2902b29 | 394 | "build" : job.build.uuid, |
c2902b29 MT |
395 | "duration" : job.duration, |
396 | "name" : job.name, | |
397 | "packages" : [p.uuid for p in job.packages], | |
398 | "state" : job.state, | |
399 | "time_created" : job.time_created.isoformat(), | |
400 | "type" : job.type, | |
401 | "uuid" : job.uuid, | |
f6e6ff79 MT |
402 | } |
403 | ||
ba9f9092 MT |
404 | if job.builder: |
405 | ret["builder"] = job.builder.hostname | |
406 | ||
c2902b29 MT |
407 | if job.time_started: |
408 | ret["time_started"] = job.time_started.isoformat() | |
f6e6ff79 | 409 | |
c2902b29 MT |
410 | if job.time_finished: |
411 | ret["time_finished"] = job.time_finished.isoformat() | |
f6e6ff79 | 412 | |
ba9f9092 MT |
413 | return ret |
414 | ||
415 | ||
416 | class JobsGetActiveHandler(JobsBaseHandler): | |
417 | def get(self): | |
418 | # Get list of all active jobs. | |
419 | jobs = self.backend.jobs.get_active() | |
420 | ||
421 | args = { | |
422 | "jobs" : [self.job2json(j) for j in jobs], | |
423 | } | |
424 | ||
425 | self.finish(args) | |
426 | ||
427 | ||
428 | class JobsGetLatestHandler(JobsBaseHandler): | |
429 | def get(self): | |
430 | limit = self.get_argument_int("limit", 5) | |
431 | ||
432 | # Get the latest jobs. | |
433 | jobs = self.backend.jobs.get_latest(age="24 HOUR", limit=limit) | |
434 | ||
435 | args = { | |
436 | "jobs" : [self.job2json(j) for j in jobs], | |
437 | } | |
438 | ||
439 | self.finish(args) | |
440 | ||
441 | ||
442 | class JobsGetQueueHandler(JobsBaseHandler): | |
443 | def get(self): | |
444 | limit = self.get_argument_int("limit", 5) | |
445 | ||
446 | # Get the job queue. | |
fd43d5e1 MT |
447 | jobs = [] |
448 | for job in self.backend.jobqueue: | |
449 | jobs.append(job) | |
450 | ||
451 | limit -= 1 | |
452 | if not limit: break | |
ba9f9092 MT |
453 | |
454 | args = { | |
455 | "jobs" : [self.job2json(j) for j in jobs], | |
456 | } | |
457 | ||
458 | self.finish(args) | |
459 | ||
460 | ||
461 | class JobsGetHandler(JobsBaseHandler): | |
462 | def get(self, job_uuid): | |
463 | job = self.backend.jobs.get_by_uuid(job_uuid) | |
464 | if not job: | |
465 | raise tornado.web.HTTPError(404, "Could not find job: %s" % job_uuid) | |
466 | ||
467 | # Check if user is allowed to view this job. | |
468 | if job.build.public == False: | |
469 | if not self.user: | |
470 | raise tornado.web.HTTPError(401) | |
471 | ||
472 | # Check if an authenticated user has permission to see this build. | |
473 | if not job.build.has_perm(self.user): | |
474 | raise tornado.web.HTTPError(403) | |
475 | ||
476 | ret = self.job2json(job) | |
c2902b29 | 477 | self.finish(ret) |
f6e6ff79 | 478 | |
f6e6ff79 | 479 | |
c2902b29 | 480 | # Packages |
f6e6ff79 | 481 | |
c2902b29 MT |
482 | class PackagesGetHandler(BaseHandler): |
483 | def get(self, package_uuid): | |
484 | pkg = self.backend.packages.get_by_uuid(package_uuid) | |
f6e6ff79 | 485 | if not pkg: |
c2902b29 | 486 | raise tornado.web.HTTPError(404, "Could not find package: %s" % package_uuid) |
f6e6ff79 MT |
487 | |
488 | ret = { | |
22b715d7 | 489 | "arch" : pkg.arch, |
c2902b29 MT |
490 | "build_id" : pkg.build_id, |
491 | "build_host" : pkg.build_host, | |
492 | "build_time" : pkg.build_time.isoformat(), | |
493 | "description" : pkg.description, | |
494 | "epoch" : pkg.epoch, | |
495 | "filesize" : pkg.filesize, | |
f6e6ff79 MT |
496 | "friendly_name" : pkg.friendly_name, |
497 | "friendly_version" : pkg.friendly_version, | |
498 | "groups" : pkg.groups, | |
c2902b29 | 499 | "hash_sha512" : pkg.hash_sha512, |
f6e6ff79 | 500 | "license" : pkg.license, |
c2902b29 MT |
501 | "name" : pkg.name, |
502 | "release" : pkg.release, | |
f6e6ff79 | 503 | "size" : pkg.size, |
c2902b29 MT |
504 | "summary" : pkg.summary, |
505 | "type" : pkg.type, | |
506 | "url" : pkg.url, | |
507 | "uuid" : pkg.uuid, | |
508 | "version" : pkg.version, | |
f6e6ff79 MT |
509 | |
510 | # Dependencies. | |
511 | "prerequires" : pkg.prerequires, | |
512 | "requires" : pkg.requires, | |
513 | "provides" : pkg.provides, | |
514 | "obsoletes" : pkg.obsoletes, | |
515 | "conflicts" : pkg.conflicts, | |
f6e6ff79 MT |
516 | } |
517 | ||
c2902b29 MT |
518 | if pkg.type == "source": |
519 | ret["supported_arches"] = pkg.supported_arches | |
520 | ||
2c909128 | 521 | if isinstance(pkg.maintainer, users.User): |
f6e6ff79 MT |
522 | ret["maintainer"] = "%s <%s>" % (pkg.maintainer.realname, pkg.maintainer.email) |
523 | elif pkg.maintainer: | |
524 | ret["maintainer"] = pkg.maintainer | |
525 | ||
526 | if pkg.distro: | |
c2902b29 | 527 | ret["distro"] = pkg.distro.identifier |
f6e6ff79 | 528 | |
c2902b29 | 529 | self.finish(ret) |
f6e6ff79 MT |
530 | |
531 | ||
c2902b29 | 532 | # Builders |
f6e6ff79 | 533 | |
c2902b29 MT |
534 | class BuildersBaseHandler(BaseHandler): |
535 | def prepare(self): | |
536 | # The request must come from an authenticated buider. | |
537 | if not self.builder: | |
538 | raise tornado.web.HTTPError(403) | |
f6e6ff79 | 539 | |
f6e6ff79 | 540 | |
c2902b29 | 541 | class BuildersInfoHandler(BuildersBaseHandler): |
f6e6ff79 | 542 | @tornado.web.authenticated |
c2902b29 MT |
543 | def post(self): |
544 | args = { | |
545 | # CPU info | |
546 | "cpu_model" : self.get_argument("cpu_model", None), | |
547 | "cpu_count" : self.get_argument("cpu_count", None), | |
548 | "cpu_arch" : self.get_argument("cpu_arch", None), | |
549 | "cpu_bogomips" : self.get_argument("cpu_bogomips", None), | |
550 | ||
551 | # Pakfire | |
552 | "pakfire_version" : self.get_argument("pakfire_version", None), | |
553 | "host_key" : self.get_argument("host_key", None), | |
554 | ||
555 | # OS | |
556 | "os_name" : self.get_argument("os_name", None), | |
557 | } | |
558 | self.builder.update_info(**args) | |
f6e6ff79 | 559 | |
f6e6ff79 | 560 | |
c2902b29 MT |
561 | class BuildersKeepaliveHandler(BuildersBaseHandler): |
562 | @tornado.web.authenticated | |
563 | def post(self): | |
564 | args = { | |
565 | # Load average | |
566 | "loadavg1" : self.get_argument_float("loadavg1", None), | |
567 | "loadavg5" : self.get_argument_float("loadavg5", None), | |
568 | "loadavg15" : self.get_argument_float("loadavg15", None), | |
569 | ||
570 | # Memory | |
571 | "mem_total" : self.get_argument_int("mem_total", None), | |
572 | "mem_free" : self.get_argument_int("mem_free", None), | |
573 | ||
574 | # swap | |
575 | "swap_total" : self.get_argument_int("swap_total", None), | |
576 | "swap_free" : self.get_argument_int("swap_free", None), | |
577 | ||
578 | # Disk space | |
579 | "space_free" : self.get_argument_int("space_free", None), | |
580 | } | |
581 | self.builder.update_keepalive(**args) | |
f6e6ff79 | 582 | |
c2902b29 | 583 | self.finish("OK") |
f6e6ff79 | 584 | |
f6e6ff79 | 585 | |
c2902b29 MT |
586 | class BuildersJobsQueueHandler(BuildersBaseHandler): |
587 | @tornado.web.asynchronous | |
588 | @tornado.web.authenticated | |
589 | def get(self): | |
590 | self.callback() | |
f6e6ff79 | 591 | |
c2902b29 MT |
592 | def callback(self): |
593 | # Break if the connection has been closed in the mean time. | |
594 | if self.connection_closed(): | |
595 | logging.warning("Connection closed") | |
f6e6ff79 MT |
596 | return |
597 | ||
c2902b29 MT |
598 | # Check if there is a job for us. |
599 | job = self.builder.get_next_job() | |
600 | ||
601 | # Got no job, wait and try again. | |
f6e6ff79 | 602 | if not job: |
c2902b29 MT |
603 | # Check if we have been running for too long. |
604 | if self.runtime >= self.max_runtime: | |
605 | logging.debug("Exceeded max. runtime. Finishing request.") | |
606 | return self.finish() | |
f6e6ff79 | 607 | |
c2902b29 MT |
608 | # Try again in a jiffy. |
609 | self.add_timeout(self.heartbeat, self.callback) | |
610 | return | |
f6e6ff79 MT |
611 | |
612 | try: | |
613 | # Set job to dispatching state. | |
614 | job.state = "dispatching" | |
615 | ||
616 | # Set our build host. | |
617 | job.builder = self.builder | |
618 | ||
619 | ret = { | |
620 | "id" : job.uuid, | |
22b715d7 | 621 | "arch" : job.arch, |
c2902b29 MT |
622 | "source_url" : job.build.source_download, |
623 | "source_hash_sha512" : job.build.source_hash_sha512, | |
f6e6ff79 MT |
624 | "type" : job.type, |
625 | "config" : job.get_config(), | |
626 | } | |
627 | ||
628 | # Send build information to the builder. | |
c2902b29 | 629 | self.finish(ret) |
f6e6ff79 MT |
630 | except: |
631 | # If anything went wrong, we reset the state. | |
632 | job.state = "pending" | |
633 | raise | |
634 | ||
c2902b29 MT |
635 | @property |
636 | def heartbeat(self): | |
637 | return 15 # 15 seconds | |
638 | ||
639 | @property | |
640 | def max_runtime(self): | |
641 | timeout = self.get_argument_int("timeout", None) | |
642 | if timeout: | |
643 | return timeout - self.heartbeat | |
644 | ||
645 | return 300 # 5 min | |
646 | ||
647 | ||
648 | class BuildersJobsStateHandler(BuildersBaseHandler): | |
649 | @tornado.web.authenticated | |
650 | def post(self, job_uuid, state): | |
651 | job = self.backend.jobs.get_by_uuid(job_uuid) | |
f6e6ff79 MT |
652 | if not job: |
653 | raise tornado.web.HTTPError(404, "Invalid job id.") | |
654 | ||
655 | if not job.builder == self.builder: | |
656 | raise tornado.web.HTTPError(403, "Altering another builder's build.") | |
657 | ||
658 | # Save information to database. | |
659 | job.state = state | |
c2902b29 MT |
660 | |
661 | message = self.get_argument("message", None) | |
f6e6ff79 MT |
662 | job.update_message(message) |
663 | ||
c2902b29 MT |
664 | self.finish("OK") |
665 | ||
666 | ||
667 | class BuildersJobsBuildrootHandler(BuildersBaseHandler): | |
668 | @tornado.web.authenticated | |
669 | def post(self, job_uuid): | |
670 | job = self.backend.jobs.get_by_uuid(job_uuid) | |
671 | if not job: | |
672 | raise tornado.web.HTTPError(404, "Invalid job id.") | |
673 | ||
674 | if not job.builder == self.builder: | |
675 | raise tornado.web.HTTPError(403, "Altering another builder's build.") | |
f6e6ff79 | 676 | |
c2902b29 MT |
677 | # Get buildroot. |
678 | buildroot = self.get_argument_json("buildroot", None) | |
679 | if buildroot: | |
680 | job.save_buildroot(buildroot) | |
681 | ||
682 | self.finish("OK") | |
683 | ||
684 | ||
685 | class BuildersJobsAddFileHandler(BuildersBaseHandler): | |
686 | @tornado.web.authenticated | |
687 | def post(self, job_uuid, upload_id): | |
688 | type = self.get_argument("type") | |
f6e6ff79 MT |
689 | assert type in ("package", "log") |
690 | ||
691 | # Fetch job we are working on and check if it is actually ours. | |
c2902b29 | 692 | job = self.backend.jobs.get_by_uuid(job_uuid) |
f6e6ff79 MT |
693 | if not job: |
694 | raise tornado.web.HTTPError(404, "Invalid job id.") | |
695 | ||
696 | if not job.builder == self.builder: | |
697 | raise tornado.web.HTTPError(403, "Altering another builder's job.") | |
698 | ||
699 | # Fetch uploaded file object and check we uploaded it ourself. | |
c2902b29 | 700 | upload = self.backend.uploads.get_by_uuid(upload_id) |
f6e6ff79 MT |
701 | if not upload: |
702 | raise tornado.web.HTTPError(404, "Invalid upload id.") | |
703 | ||
704 | if not upload.builder == self.builder: | |
705 | raise tornado.web.HTTPError(403, "Using an other host's file.") | |
706 | ||
707 | # Remove all files that have to be deleted, first. | |
c2902b29 | 708 | self.backend.cleanup_files() |
f6e6ff79 MT |
709 | |
710 | try: | |
711 | job.add_file(upload.path) | |
712 | ||
713 | finally: | |
714 | # Finally, remove the uploaded file. | |
715 | upload.remove() | |
716 | ||
c2902b29 | 717 | self.finish("OK") |