]>
git.ipfire.org Git - people/jschlag/pbs.git/blob - src/hub/handlers.py
37fb60e9480690dfe008991b5c8679b3def3195a
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
43 def get_basic_auth_credentials(self
):
45 This handles HTTP Basic authentication.
47 auth_header
= self
.request
.headers
.get("Authorization", None)
49 # If no authentication information was provided, we stop here.
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.")
58 # Decode the authentication information.
59 auth_header
= base64
.decodestring(auth_header
[6:])
61 name
, password
= auth_header
.split(":", 1)
63 raise tornado
.web
.HTTPError(400, "Authorization data was malformed")
67 def get_current_user(self
):
68 name
, password
= self
.get_basic_auth_credentials()
72 builder
= self
.backend
.builders
.auth(name
, password
)
76 user
= self
.backend
.users
.auth(name
, password
)
82 if isinstance(self
.current_user
, builders
.Builder
):
83 return self
.current_user
87 if isinstance(self
.current_user
, users
.User
):
88 return self
.current_user
90 def get_argument_int(self
, *args
, **kwargs
):
91 arg
= self
.get_argument(*args
, **kwargs
)
95 except (TypeError, ValueError):
98 def get_argument_float(self
, *args
, **kwargs
):
99 arg
= self
.get_argument(*args
, **kwargs
)
103 except (TypeError, ValueError):
106 def get_argument_json(self
, *args
, **kwargs
):
107 arg
= self
.get_argument(*args
, **kwargs
)
110 return json
.loads(arg
)
113 class NoopHandler(BaseHandler
):
116 self
.write("Welcome to the Pakfire hub, %s!" % self
.builder
.hostname
)
118 self
.write("Welcome to the Pakfire hub, %s!" % self
.user
.name
)
120 self
.write("Welcome to the Pakfire hub!")
123 class ErrorTestHandler(BaseHandler
):
124 def get(self
, error_code
=200):
126 For testing a client.
128 This just returns a HTTP response with the given code.
131 error_code
= int(error_code
)
135 raise tornado
.web
.HTTPError(error_code
)
140 class UploadsCreateHandler(BaseHandler
):
142 Create a new upload object in the database and return a unique ID
146 @tornado.web
.authenticated
148 # XXX Check permissions
150 filename
= self
.get_argument("filename")
151 filesize
= self
.get_argument_int("filesize")
152 filehash
= self
.get_argument("hash")
154 with self
.db
.transaction():
155 upload
= self
.backend
.uploads
.create(filename
, filesize
,
156 filehash
, user
=self
.user
, builder
=self
.builder
)
158 self
.finish(upload
.uuid
)
161 class UploadsSendChunkHandler(BaseHandler
):
162 @tornado.web
.authenticated
163 def post(self
, upload_id
):
164 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
166 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
168 if not upload
.builder
== self
.builder
:
169 raise tornado
.web
.HTTPError(403, "Uploading an other host's file.")
171 chksum
= self
.get_argument("chksum")
172 data
= self
.get_argument("data")
175 data
= base64
.b64decode(data
)
177 # Calculate hash and compare.
178 h
= hashlib
.new("sha512")
181 if not chksum
== h
.hexdigest():
182 raise tornado
.web
.HTTPError(400, "Checksum mismatch")
184 # Append the data to file.
185 with self
.db
.transaction():
189 class UploadsFinishedHandler(BaseHandler
):
190 @tornado.web
.authenticated
191 def get(self
, upload_id
):
192 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
194 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
196 if not upload
.builder
== self
.builder
:
197 raise tornado
.web
.HTTPError(403, "Uploading an other host's file.")
199 # Validate the uploaded data to its hash.
200 ret
= upload
.validate()
202 # If the validation was successfull, we mark the upload
203 # as finished and send True to the client.
210 # In case the download was corrupted or incomplete, we delete it
211 # and tell the client to start over.
212 with self
.db
.transaction():
215 self
.finish("ERROR: CORRUPTED OR INCOMPLETE FILE")
218 class UploadsDestroyHandler(BaseHandler
):
219 @tornado.web
.authenticated
220 def get(self
, upload_id
):
221 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
223 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
225 if not upload
.builder
== self
.builder
:
226 raise tornado
.web
.HTTPError(403, "Removing an other host's file.")
228 # Remove the upload from the database and trash the data.
229 with self
.db
.transaction():
235 class BuildsCreateHandler(BaseHandler
):
236 @tornado.web
.authenticated
238 # Get the upload ID of the package file.
239 upload_id
= self
.get_argument("upload_id")
241 # Get the identifier of the distribution we build for.
242 distro_ident
= self
.get_argument("distro")
244 # Get a list of arches to build for.
245 arches
= self
.get_argument("arches", None)
249 # Process build type.
250 build_type
= self
.get_argument("build_type")
251 if build_type
== "release":
252 check_for_duplicates
= True
253 elif build_type
== "scratch":
254 check_for_duplicates
= False
256 raise tornado
.web
.HTTPError(400, "Invalid build type")
258 ## Check if the user has permission to create a build.
259 # Users only have the permission to create scratch builds.
260 if self
.user
and not build_type
== "scratch":
261 raise tornado
.web
.HTTPError(403, "Users are only allowed to upload scratch builds")
263 # Get previously uploaded file to create this build from.
264 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
266 raise tornado
.web
.HTTPError(400, "Upload does not exist: %s" % upload_id
)
268 # Check if the uploaded file belongs to this user/builder.
269 if self
.user
and not upload
.user
== self
.user
:
270 raise tornado
.web
.HTTPError(400, "Upload does not belong to this user.")
272 elif self
.builder
and not upload
.builder
== self
.builder
:
273 raise tornado
.web
.HTTPError(400, "Upload does not belong to this builder.")
275 # Get distribution this package should be built for.
276 distro
= self
.backend
.distros
.get_by_ident(distro_ident
)
278 distro
= self
.backend
.distros
.get_default()
280 # Open the package that was uploaded earlier and add it to
281 # the database. Create a new build object from the uploaded package.
284 "check_for_duplicates" : check_for_duplicates
,
289 args
["owner"] = self
.user
292 pkg
, build
= builds
.import_from_package(self
.backend
, upload
.path
, **args
)
295 # Raise any exception.
299 # Creating the build will move the file to the build directory,
300 # so we can safely remove the uploaded file.
303 # Send the build ID back to the user.
304 self
.finish(build
.uuid
)
307 class BuildsGetHandler(BaseHandler
):
308 def get(self
, build_uuid
):
309 build
= self
.backend
.builds
.get_by_uuid(build_uuid
)
311 raise tornado
.web
.HTTPError(404, "Could not find build: %s" % build_uuid
)
314 "distro" : build
.distro
.identifier
,
315 "jobs" : [j
.uuid
for j
in build
.jobs
],
317 "package" : build
.pkg
.uuid
,
318 "priority" : build
.priority
,
319 "score" : build
.score
,
320 "severity" : build
.severity
,
321 "state" : build
.state
,
322 "sup_arches" : build
.supported_arches
,
323 "time_created" : build
.created
.isoformat(),
328 # If the build is in a repository, update that bit.
330 ret
["repo"] = build
.repo
.identifier
337 class JobsBaseHandler(BaseHandler
):
338 def job2json(self
, job
):
341 "build" : job
.build
.uuid
,
342 "duration" : job
.duration
,
344 "packages" : [p
.uuid
for p
in job
.packages
],
346 "time_created" : job
.time_created
.isoformat(),
347 "type" : "test" if job
.test
else "release",
352 ret
["builder"] = job
.builder
.hostname
355 ret
["time_started"] = job
.time_started
.isoformat()
357 if job
.time_finished
:
358 ret
["time_finished"] = job
.time_finished
.isoformat()
363 class JobsGetActiveHandler(JobsBaseHandler
):
365 # Get list of all active jobs.
366 jobs
= self
.backend
.jobs
.get_active()
369 "jobs" : [self
.job2json(j
) for j
in jobs
],
375 class JobsGetLatestHandler(JobsBaseHandler
):
377 limit
= self
.get_argument_int("limit", 5)
379 # Get the latest jobs.
380 jobs
= self
.backend
.jobs
.get_latest(age
="24 HOUR", limit
=limit
)
383 "jobs" : [self
.job2json(j
) for j
in jobs
],
389 class JobsGetQueueHandler(JobsBaseHandler
):
391 limit
= self
.get_argument_int("limit", 5)
395 for job
in self
.backend
.jobqueue
:
402 "jobs" : [self
.job2json(j
) for j
in jobs
],
408 class JobsGetHandler(JobsBaseHandler
):
409 def get(self
, job_uuid
):
410 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
412 raise tornado
.web
.HTTPError(404, "Could not find job: %s" % job_uuid
)
414 ret
= self
.job2json(job
)
420 class PackagesGetHandler(BaseHandler
):
421 def get(self
, package_uuid
):
422 pkg
= self
.backend
.packages
.get_by_uuid(package_uuid
)
424 raise tornado
.web
.HTTPError(404, "Could not find package: %s" % package_uuid
)
428 "build_id" : pkg
.build_id
,
429 "build_host" : pkg
.build_host
,
430 "build_time" : pkg
.build_time
.isoformat(),
431 "description" : pkg
.description
,
433 "filesize" : pkg
.filesize
,
434 "friendly_name" : pkg
.friendly_name
,
435 "friendly_version" : pkg
.friendly_version
,
436 "groups" : pkg
.groups
,
437 "hash_sha512" : pkg
.hash_sha512
,
438 "license" : pkg
.license
,
440 "release" : pkg
.release
,
442 "summary" : pkg
.summary
,
446 "version" : pkg
.version
,
449 "prerequires" : pkg
.prerequires
,
450 "requires" : pkg
.requires
,
451 "provides" : pkg
.provides
,
452 "obsoletes" : pkg
.obsoletes
,
453 "conflicts" : pkg
.conflicts
,
456 if pkg
.type == "source":
457 ret
["supported_arches"] = pkg
.supported_arches
459 if isinstance(pkg
.maintainer
, users
.User
):
460 ret
["maintainer"] = "%s <%s>" % (pkg
.maintainer
.realname
, pkg
.maintainer
.email
)
462 ret
["maintainer"] = pkg
.maintainer
465 ret
["distro"] = pkg
.distro
.identifier
472 class BuildersBaseHandler(BaseHandler
):
474 # The request must come from an authenticated buider.
476 raise tornado
.web
.HTTPError(403)
479 class BuildersInfoHandler(BuildersBaseHandler
):
480 @tornado.web
.authenticated
484 "cpu_model" : self
.get_argument("cpu_model", None),
485 "cpu_count" : self
.get_argument("cpu_count", None),
486 "cpu_arch" : self
.get_argument("cpu_arch", None),
487 "cpu_bogomips" : self
.get_argument("cpu_bogomips", None),
490 "pakfire_version" : self
.get_argument("pakfire_version", None),
491 "host_key" : self
.get_argument("host_key", None),
494 "os_name" : self
.get_argument("os_name", None),
496 self
.builder
.update_info(**args
)
499 class BuildersKeepaliveHandler(BuildersBaseHandler
):
500 @tornado.web
.authenticated
504 "loadavg1" : self
.get_argument_float("loadavg1", None),
505 "loadavg5" : self
.get_argument_float("loadavg5", None),
506 "loadavg15" : self
.get_argument_float("loadavg15", None),
509 "mem_total" : self
.get_argument_int("mem_total", None),
510 "mem_free" : self
.get_argument_int("mem_free", None),
513 "swap_total" : self
.get_argument_int("swap_total", None),
514 "swap_free" : self
.get_argument_int("swap_free", None),
517 "space_free" : self
.get_argument_int("space_free", None),
519 self
.builder
.update_keepalive(**args
)
524 class BuildersJobsQueueHandler(BuildersBaseHandler
):
525 @tornado.web
.asynchronous
526 @tornado.web
.authenticated
531 # Break if the connection has been closed in the mean time.
532 if self
.connection_closed():
533 logging
.warning("Connection closed")
536 # Check if there is a job for us.
537 job
= self
.builder
.get_next_job()
539 # Got no job, wait and try again.
541 # Check if we have been running for too long.
542 if self
.runtime
>= self
.max_runtime
:
543 logging
.debug("Exceeded max. runtime. Finishing request.")
546 # Try again in a jiffy.
547 self
.add_timeout(self
.heartbeat
, self
.callback
)
551 # Set job to dispatching state.
552 job
.state
= "dispatching"
554 # Set our build host.
555 job
.builder
= self
.builder
560 "source_url" : job
.build
.source_download
,
561 "source_hash_sha512" : job
.build
.source_hash_sha512
,
562 "type" : "test" if job
.test
else "release",
563 "config" : job
.get_config(),
566 # Send build information to the builder.
569 # If anything went wrong, we reset the state.
570 job
.state
= "pending"
575 return 15 # 15 seconds
578 def max_runtime(self
):
579 timeout
= self
.get_argument_int("timeout", None)
581 return timeout
- self
.heartbeat
586 class BuildersJobsStateHandler(BuildersBaseHandler
):
587 @tornado.web
.authenticated
588 def post(self
, job_uuid
, state
):
589 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
591 raise tornado
.web
.HTTPError(404, "Invalid job id.")
593 if not job
.builder
== self
.builder
:
594 raise tornado
.web
.HTTPError(403, "Altering another builder's build.")
596 # Save information to database.
599 message
= self
.get_argument("message", None)
600 job
.update_message(message
)
605 class BuildersJobsBuildrootHandler(BuildersBaseHandler
):
606 @tornado.web
.authenticated
607 def post(self
, job_uuid
):
608 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
610 raise tornado
.web
.HTTPError(404, "Invalid job id.")
612 if not job
.builder
== self
.builder
:
613 raise tornado
.web
.HTTPError(403, "Altering another builder's build.")
616 buildroot
= self
.get_argument_json("buildroot", None)
618 job
.save_buildroot(buildroot
)
623 class BuildersJobsAddFileHandler(BuildersBaseHandler
):
624 @tornado.web
.authenticated
625 def post(self
, job_uuid
, upload_id
):
626 type = self
.get_argument("type")
627 assert type in ("package", "log")
629 # Fetch job we are working on and check if it is actually ours.
630 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
632 raise tornado
.web
.HTTPError(404, "Invalid job id.")
634 if not job
.builder
== self
.builder
:
635 raise tornado
.web
.HTTPError(403, "Altering another builder's job.")
637 # Fetch uploaded file object and check we uploaded it ourself.
638 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
640 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
642 if not upload
.builder
== self
.builder
:
643 raise tornado
.web
.HTTPError(403, "Using an other host's file.")
645 # Remove all files that have to be deleted, first.
646 self
.backend
.cleanup_files()
649 job
.add_file(upload
.path
)
652 # Finally, remove the uploaded file.