]>
git.ipfire.org Git - people/jschlag/pbs.git/blob - src/hub/handlers.py
11 from .. import builders
12 from .. import uploads
15 class LongPollMixin(object):
17 self
._start
_time
= time
.time()
19 def add_timeout(self
, timeout
, callback
):
20 deadline
= time
.time() + timeout
22 return self
.application
.ioloop
.add_timeout(deadline
, callback
)
24 def on_connection_close(self
):
25 logging
.debug("Connection closed unexpectedly")
27 def connection_closed(self
):
28 return self
.request
.connection
.stream
.closed()
32 return time
.time() - self
._start
_time
35 class BaseHandler(LongPollMixin
, tornado
.web
.RequestHandler
):
39 Shortcut handler to pakfire instance
41 return self
.application
.backend
45 return self
.backend
.db
47 def get_basic_auth_credentials(self
):
49 This handles HTTP Basic authentication.
51 auth_header
= self
.request
.headers
.get("Authorization", None)
53 # If no authentication information was provided, we stop here.
57 # No basic auth? We cannot handle that.
58 if not auth_header
.startswith("Basic "):
59 raise tornado
.web
.HTTPError(400, "Can only handle Basic auth.")
62 # Decode the authentication information.
63 auth_header
= base64
.decodestring(auth_header
[6:])
65 name
, password
= auth_header
.split(":", 1)
67 raise tornado
.web
.HTTPError(400, "Authorization data was malformed")
71 def get_current_user(self
):
72 name
, password
= self
.get_basic_auth_credentials()
76 builder
= self
.backend
.builders
.auth(name
, password
)
80 user
= self
.backend
.users
.auth(name
, password
)
86 if isinstance(self
.current_user
, builders
.Builder
):
87 return self
.current_user
91 if isinstance(self
.current_user
, users
.User
):
92 return self
.current_user
94 def get_argument_int(self
, *args
, **kwargs
):
95 arg
= self
.get_argument(*args
, **kwargs
)
99 except (TypeError, ValueError):
102 def get_argument_float(self
, *args
, **kwargs
):
103 arg
= self
.get_argument(*args
, **kwargs
)
107 except (TypeError, ValueError):
110 def get_argument_json(self
, *args
, **kwargs
):
111 arg
= self
.get_argument(*args
, **kwargs
)
114 return json
.loads(arg
)
117 class NoopHandler(BaseHandler
):
120 self
.write("Welcome to the Pakfire hub, %s!" % self
.builder
.hostname
)
122 self
.write("Welcome to the Pakfire hub, %s!" % self
.user
.name
)
124 self
.write("Welcome to the Pakfire hub!")
127 class ErrorTestHandler(BaseHandler
):
128 def get(self
, error_code
=200):
130 For testing a client.
132 This just returns a HTTP response with the given code.
135 error_code
= int(error_code
)
139 raise tornado
.web
.HTTPError(error_code
)
144 class UploadsCreateHandler(BaseHandler
):
146 Create a new upload object in the database and return a unique ID
150 @tornado.web
.authenticated
152 # XXX Check permissions
154 filename
= self
.get_argument("filename")
155 filesize
= self
.get_argument_int("filesize")
156 filehash
= self
.get_argument("hash")
158 with self
.db
.transaction():
159 upload
= self
.backend
.uploads
.create(filename
, filesize
,
160 filehash
, user
=self
.user
, builder
=self
.builder
)
162 self
.finish(upload
.uuid
)
165 class UploadsSendChunkHandler(BaseHandler
):
166 @tornado.web
.authenticated
167 def post(self
, upload_id
):
168 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
170 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
172 if not upload
.builder
== self
.builder
:
173 raise tornado
.web
.HTTPError(403, "Uploading an other host's file.")
175 chksum
= self
.get_argument("chksum")
176 data
= self
.get_argument("data")
179 data
= base64
.b64decode(data
)
181 # Calculate hash and compare.
182 h
= hashlib
.new("sha512")
185 if not chksum
== h
.hexdigest():
186 raise tornado
.web
.HTTPError(400, "Checksum mismatch")
188 # Append the data to file.
189 with self
.db
.transaction():
193 class UploadsFinishedHandler(BaseHandler
):
194 @tornado.web
.authenticated
195 def get(self
, upload_id
):
196 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
198 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
200 if not upload
.builder
== self
.builder
:
201 raise tornado
.web
.HTTPError(403, "Uploading an other host's file.")
203 # Validate the uploaded data to its hash.
204 ret
= upload
.validate()
206 # If the validation was successfull, we mark the upload
207 # as finished and send True to the client.
214 # In case the download was corrupted or incomplete, we delete it
215 # and tell the client to start over.
216 with self
.db
.transaction():
219 self
.finish("ERROR: CORRUPTED OR INCOMPLETE FILE")
222 class UploadsDestroyHandler(BaseHandler
):
223 @tornado.web
.authenticated
224 def get(self
, upload_id
):
225 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
227 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
229 if not upload
.builder
== self
.builder
:
230 raise tornado
.web
.HTTPError(403, "Removing an other host's file.")
232 # Remove the upload from the database and trash the data.
233 with self
.db
.transaction():
239 class BuildsCreateHandler(BaseHandler
):
240 @tornado.web
.authenticated
242 # Get the upload ID of the package file.
243 upload_id
= self
.get_argument("upload_id")
245 # Get the identifier of the distribution we build for.
246 distro_ident
= self
.get_argument("distro")
248 # Get a list of arches to build for.
249 arches
= self
.get_argument("arches", None)
253 # Get previously uploaded file to create this build from.
254 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
256 raise tornado
.web
.HTTPError(400, "Upload does not exist: %s" % upload_id
)
258 # Check if the uploaded file belongs to this user/builder.
259 if self
.user
and not upload
.user
== self
.user
:
260 raise tornado
.web
.HTTPError(400, "Upload does not belong to this user")
262 elif self
.builder
and not upload
.builder
== self
.builder
:
263 raise tornado
.web
.HTTPError(400, "Upload does not belong to this builder")
265 # Get distribution this package should be built for.
266 distro
= self
.backend
.distros
.get_by_ident(distro_ident
)
268 distro
= self
.backend
.distros
.get_default()
270 # Open the package that was uploaded earlier and add it to
271 # the database. Create a new build object from the uploaded package.
273 build
= self
.backend
.builds
.create_from_source_package(upload
.path
, distro
=distro
,
274 type="scratch", arches
=arches
, owner
=self
.user
)
277 # Raise any exception.
281 # Creating the build will move the file to the build directory,
282 # so we can safely remove the uploaded file.
285 # Send the build ID back to the user.
286 self
.finish(build
.uuid
)
289 class BuildsGetHandler(BaseHandler
):
290 def get(self
, build_uuid
):
291 build
= self
.backend
.builds
.get_by_uuid(build_uuid
)
293 raise tornado
.web
.HTTPError(404, "Could not find build: %s" % build_uuid
)
296 "distro" : build
.distro
.identifier
,
297 "jobs" : [j
.uuid
for j
in build
.jobs
],
299 "package" : build
.pkg
.uuid
,
300 "priority" : build
.priority
,
301 "score" : build
.score
,
302 "severity" : build
.severity
,
303 "state" : build
.state
,
304 "sup_arches" : build
.supported_arches
,
305 "time_created" : build
.created
.isoformat(),
310 # If the build is in a repository, update that bit.
312 ret
["repo"] = build
.repo
.identifier
319 class JobsBaseHandler(BaseHandler
):
320 def job2json(self
, job
):
323 "build" : job
.build
.uuid
,
324 "duration" : job
.duration
,
326 "packages" : [p
.uuid
for p
in job
.packages
],
328 "time_created" : job
.time_created
.isoformat(),
329 "type" : "test" if job
.test
else "release",
334 ret
["builder"] = job
.builder
.hostname
337 ret
["time_started"] = job
.time_started
.isoformat()
339 if job
.time_finished
:
340 ret
["time_finished"] = job
.time_finished
.isoformat()
345 class JobsGetActiveHandler(JobsBaseHandler
):
347 # Get list of all active jobs.
348 jobs
= self
.backend
.jobs
.get_active()
351 "jobs" : [self
.job2json(j
) for j
in jobs
],
357 class JobsGetLatestHandler(JobsBaseHandler
):
359 limit
= self
.get_argument_int("limit", 5)
361 # Get the latest jobs.
362 jobs
= self
.backend
.jobs
.get_latest(age
="24 HOUR", limit
=limit
)
365 "jobs" : [self
.job2json(j
) for j
in jobs
],
371 class JobsGetQueueHandler(JobsBaseHandler
):
373 limit
= self
.get_argument_int("limit", 5)
377 for job
in self
.backend
.jobqueue
:
384 "jobs" : [self
.job2json(j
) for j
in jobs
],
390 class JobsGetHandler(JobsBaseHandler
):
391 def get(self
, job_uuid
):
392 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
394 raise tornado
.web
.HTTPError(404, "Could not find job: %s" % job_uuid
)
396 ret
= self
.job2json(job
)
402 class PackagesGetHandler(BaseHandler
):
403 def get(self
, package_uuid
):
404 pkg
= self
.backend
.packages
.get_by_uuid(package_uuid
)
406 raise tornado
.web
.HTTPError(404, "Could not find package: %s" % package_uuid
)
410 "build_id" : pkg
.build_id
,
411 "build_host" : pkg
.build_host
,
412 "build_time" : pkg
.build_time
.isoformat(),
413 "description" : pkg
.description
,
415 "filesize" : pkg
.filesize
,
416 "friendly_name" : pkg
.friendly_name
,
417 "friendly_version" : pkg
.friendly_version
,
418 "groups" : pkg
.groups
,
419 "hash_sha512" : pkg
.hash_sha512
,
420 "license" : pkg
.license
,
422 "release" : pkg
.release
,
424 "summary" : pkg
.summary
,
428 "version" : pkg
.version
,
431 "prerequires" : pkg
.prerequires
,
432 "requires" : pkg
.requires
,
433 "provides" : pkg
.provides
,
434 "obsoletes" : pkg
.obsoletes
,
435 "conflicts" : pkg
.conflicts
,
438 if pkg
.type == "source":
439 ret
["supported_arches"] = pkg
.supported_arches
441 if isinstance(pkg
.maintainer
, users
.User
):
442 ret
["maintainer"] = "%s <%s>" % (pkg
.maintainer
.realname
, pkg
.maintainer
.email
)
444 ret
["maintainer"] = pkg
.maintainer
447 ret
["distro"] = pkg
.distro
.identifier
454 class BuildersBaseHandler(BaseHandler
):
456 # The request must come from an authenticated buider.
458 raise tornado
.web
.HTTPError(403)
461 class BuildersInfoHandler(BuildersBaseHandler
):
462 @tornado.web
.authenticated
466 "cpu_model" : self
.get_argument("cpu_model", None),
467 "cpu_count" : self
.get_argument("cpu_count", None),
468 "cpu_arch" : self
.get_argument("cpu_arch", None),
469 "cpu_bogomips" : self
.get_argument("cpu_bogomips", None),
472 "pakfire_version" : self
.get_argument("pakfire_version", None),
473 "host_key" : self
.get_argument("host_key", None),
476 "os_name" : self
.get_argument("os_name", None),
478 self
.builder
.update_info(**args
)
481 class BuildersKeepaliveHandler(BuildersBaseHandler
):
482 @tornado.web
.authenticated
486 "loadavg1" : self
.get_argument_float("loadavg1", None),
487 "loadavg5" : self
.get_argument_float("loadavg5", None),
488 "loadavg15" : self
.get_argument_float("loadavg15", None),
491 "mem_total" : self
.get_argument_int("mem_total", None),
492 "mem_free" : self
.get_argument_int("mem_free", None),
495 "swap_total" : self
.get_argument_int("swap_total", None),
496 "swap_free" : self
.get_argument_int("swap_free", None),
499 "space_free" : self
.get_argument_int("space_free", None),
501 self
.builder
.update_keepalive(**args
)
506 class BuildersJobsQueueHandler(BuildersBaseHandler
):
507 @tornado.web
.asynchronous
508 @tornado.web
.authenticated
513 # Break if the connection has been closed in the mean time.
514 if self
.connection_closed():
515 logging
.warning("Connection closed")
518 # Check if there is a job for us.
519 job
= self
.builder
.get_next_job()
521 # Got no job, wait and try again.
523 # Check if we have been running for too long.
524 if self
.runtime
>= self
.max_runtime
:
525 logging
.debug("Exceeded max. runtime. Finishing request.")
528 # Try again in a jiffy.
529 self
.add_timeout(self
.heartbeat
, self
.callback
)
533 # Set job to dispatching state.
534 job
.state
= "dispatching"
536 # Set our build host.
537 job
.builder
= self
.builder
542 "source_url" : job
.build
.source_download
,
543 "source_hash_sha512" : job
.build
.source_hash_sha512
,
544 "type" : "test" if job
.test
else "release",
545 "config" : job
.get_config(),
548 # Send build information to the builder.
551 # If anything went wrong, we reset the state.
552 job
.state
= "pending"
557 return 15 # 15 seconds
560 def max_runtime(self
):
561 timeout
= self
.get_argument_int("timeout", None)
563 return timeout
- self
.heartbeat
568 class BuildersJobsStateHandler(BuildersBaseHandler
):
569 @tornado.web
.authenticated
570 def post(self
, job_uuid
, state
):
571 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
573 raise tornado
.web
.HTTPError(404, "Invalid job id.")
575 if not job
.builder
== self
.builder
:
576 raise tornado
.web
.HTTPError(403, "Altering another builder's build.")
578 # Save information to database.
581 message
= self
.get_argument("message", None)
582 job
.update_message(message
)
587 class BuildersJobsBuildrootHandler(BuildersBaseHandler
):
588 @tornado.web
.authenticated
589 def post(self
, job_uuid
):
590 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
592 raise tornado
.web
.HTTPError(404, "Invalid job id.")
594 if not job
.builder
== self
.builder
:
595 raise tornado
.web
.HTTPError(403, "Altering another builder's build.")
598 buildroot
= self
.get_argument_json("buildroot", None)
600 job
.save_buildroot(buildroot
)
605 class BuildersJobsAddFileHandler(BuildersBaseHandler
):
606 @tornado.web
.authenticated
607 def post(self
, job_uuid
, upload_id
):
608 type = self
.get_argument("type")
609 assert type in ("package", "log")
611 # Fetch job we are working on and check if it is actually ours.
612 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
614 raise tornado
.web
.HTTPError(404, "Invalid job id.")
616 if not job
.builder
== self
.builder
:
617 raise tornado
.web
.HTTPError(403, "Altering another builder's job.")
619 # Fetch uploaded file object and check we uploaded it ourself.
620 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
622 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
624 if not upload
.builder
== self
.builder
:
625 raise tornado
.web
.HTTPError(403, "Using an other host's file.")
627 # Remove all files that have to be deleted, first.
628 self
.backend
.cleanup_files()
631 job
.add_file(upload
.path
)
634 # Finally, remove the uploaded file.