]>
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
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 upload
= uploads
.Upload
.create(self
.backend
, filename
, filesize
,
155 filehash
, user
=self
.user
, builder
=self
.builder
)
157 self
.finish(upload
.uuid
)
160 class UploadsSendChunkHandler(BaseHandler
):
161 @tornado.web
.authenticated
162 def post(self
, upload_id
):
163 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
165 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
167 if not upload
.builder
== self
.builder
:
168 raise tornado
.web
.HTTPError(403, "Uploading an other host's file.")
170 chksum
= self
.get_argument("chksum")
171 data
= self
.get_argument("data")
174 data
= base64
.b64decode(data
)
176 # Calculate hash and compare.
177 h
= hashlib
.new("sha512")
180 if not chksum
== h
.hexdigest():
181 raise tornado
.web
.HTTPError(400, "Checksum mismatch")
183 # Append the data to file.
187 class UploadsFinishedHandler(BaseHandler
):
188 @tornado.web
.authenticated
189 def get(self
, upload_id
):
190 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
192 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
194 if not upload
.builder
== self
.builder
:
195 raise tornado
.web
.HTTPError(403, "Uploading an other host's file.")
197 # Validate the uploaded data to its hash.
198 ret
= upload
.validate()
200 # If the validation was successfull, we mark the upload
201 # as finished and send True to the client.
208 # In case the download was corrupted or incomplete, we delete it
209 # and tell the client to start over.
212 self
.finish("ERROR: CORRUPTED OR INCOMPLETE FILE")
215 class UploadsDestroyHandler(BaseHandler
):
216 @tornado.web
.authenticated
217 def get(self
, upload_id
):
218 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
220 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
222 if not upload
.builder
== self
.builder
:
223 raise tornado
.web
.HTTPError(403, "Removing an other host's file.")
225 # Remove the upload from the database and trash the data.
231 class BuildsCreateHandler(BaseHandler
):
232 @tornado.web
.authenticated
234 # Get the upload ID of the package file.
235 upload_id
= self
.get_argument("upload_id")
237 # Get the identifier of the distribution we build for.
238 distro_ident
= self
.get_argument("distro")
240 # Get a list of arches to build for.
241 arches
= self
.get_argument("arches", None)
245 # Process build type.
246 build_type
= self
.get_argument("build_type")
247 if build_type
== "release":
248 check_for_duplicates
= True
249 elif build_type
== "scratch":
250 check_for_duplicates
= False
252 raise tornado
.web
.HTTPError(400, "Invalid build type")
254 ## Check if the user has permission to create a build.
255 # Users only have the permission to create scratch builds.
256 if self
.user
and not build_type
== "scratch":
257 raise tornado
.web
.HTTPError(403, "Users are only allowed to upload scratch builds")
259 # Get previously uploaded file to create this build from.
260 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
262 raise tornado
.web
.HTTPError(400, "Upload does not exist: %s" % upload_id
)
264 # Check if the uploaded file belongs to this user/builder.
265 if self
.user
and not upload
.user
== self
.user
:
266 raise tornado
.web
.HTTPError(400, "Upload does not belong to this user.")
268 elif self
.builder
and not upload
.builder
== self
.builder
:
269 raise tornado
.web
.HTTPError(400, "Upload does not belong to this builder.")
271 # Get distribution this package should be built for.
272 distro
= self
.backend
.distros
.get_by_ident(distro_ident
)
274 distro
= self
.backend
.distros
.get_default()
276 # Open the package that was uploaded earlier and add it to
277 # the database. Create a new build object from the uploaded package.
280 "check_for_duplicates" : check_for_duplicates
,
285 args
["owner"] = self
.user
288 pkg
, build
= builds
.import_from_package(self
.backend
, upload
.path
, **args
)
291 # Raise any exception.
295 # Creating the build will move the file to the build directory,
296 # so we can safely remove the uploaded file.
299 # Send the build ID back to the user.
300 self
.finish(build
.uuid
)
303 class BuildsGetHandler(BaseHandler
):
304 def get(self
, build_uuid
):
305 build
= self
.backend
.builds
.get_by_uuid(build_uuid
)
307 raise tornado
.web
.HTTPError(404, "Could not find build: %s" % build_uuid
)
310 "distro" : build
.distro
.identifier
,
311 "jobs" : [j
.uuid
for j
in build
.jobs
],
313 "package" : build
.pkg
.uuid
,
314 "priority" : build
.priority
,
315 "score" : build
.credits
,
316 "severity" : build
.severity
,
317 "state" : build
.state
,
318 "sup_arches" : build
.supported_arches
,
319 "time_created" : build
.created
.isoformat(),
324 # If the build is in a repository, update that bit.
326 ret
["repo"] = build
.repo
.identifier
333 class JobsBaseHandler(BaseHandler
):
334 def job2json(self
, job
):
337 "build" : job
.build
.uuid
,
338 "duration" : job
.duration
,
340 "packages" : [p
.uuid
for p
in job
.packages
],
342 "time_created" : job
.time_created
.isoformat(),
343 "type" : "test" if job
.test
else "release",
348 ret
["builder"] = job
.builder
.hostname
351 ret
["time_started"] = job
.time_started
.isoformat()
353 if job
.time_finished
:
354 ret
["time_finished"] = job
.time_finished
.isoformat()
359 class JobsGetActiveHandler(JobsBaseHandler
):
361 # Get list of all active jobs.
362 jobs
= self
.backend
.jobs
.get_active()
365 "jobs" : [self
.job2json(j
) for j
in jobs
],
371 class JobsGetLatestHandler(JobsBaseHandler
):
373 limit
= self
.get_argument_int("limit", 5)
375 # Get the latest jobs.
376 jobs
= self
.backend
.jobs
.get_latest(age
="24 HOUR", limit
=limit
)
379 "jobs" : [self
.job2json(j
) for j
in jobs
],
385 class JobsGetQueueHandler(JobsBaseHandler
):
387 limit
= self
.get_argument_int("limit", 5)
391 for job
in self
.backend
.jobqueue
:
398 "jobs" : [self
.job2json(j
) for j
in jobs
],
404 class JobsGetHandler(JobsBaseHandler
):
405 def get(self
, job_uuid
):
406 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
408 raise tornado
.web
.HTTPError(404, "Could not find job: %s" % job_uuid
)
410 # Check if user is allowed to view this job.
411 if job
.build
.public
== False:
413 raise tornado
.web
.HTTPError(401)
415 # Check if an authenticated user has permission to see this build.
416 if not job
.build
.has_perm(self
.user
):
417 raise tornado
.web
.HTTPError(403)
419 ret
= self
.job2json(job
)
425 class PackagesGetHandler(BaseHandler
):
426 def get(self
, package_uuid
):
427 pkg
= self
.backend
.packages
.get_by_uuid(package_uuid
)
429 raise tornado
.web
.HTTPError(404, "Could not find package: %s" % package_uuid
)
433 "build_id" : pkg
.build_id
,
434 "build_host" : pkg
.build_host
,
435 "build_time" : pkg
.build_time
.isoformat(),
436 "description" : pkg
.description
,
438 "filesize" : pkg
.filesize
,
439 "friendly_name" : pkg
.friendly_name
,
440 "friendly_version" : pkg
.friendly_version
,
441 "groups" : pkg
.groups
,
442 "hash_sha512" : pkg
.hash_sha512
,
443 "license" : pkg
.license
,
445 "release" : pkg
.release
,
447 "summary" : pkg
.summary
,
451 "version" : pkg
.version
,
454 "prerequires" : pkg
.prerequires
,
455 "requires" : pkg
.requires
,
456 "provides" : pkg
.provides
,
457 "obsoletes" : pkg
.obsoletes
,
458 "conflicts" : pkg
.conflicts
,
461 if pkg
.type == "source":
462 ret
["supported_arches"] = pkg
.supported_arches
464 if isinstance(pkg
.maintainer
, users
.User
):
465 ret
["maintainer"] = "%s <%s>" % (pkg
.maintainer
.realname
, pkg
.maintainer
.email
)
467 ret
["maintainer"] = pkg
.maintainer
470 ret
["distro"] = pkg
.distro
.identifier
477 class BuildersBaseHandler(BaseHandler
):
479 # The request must come from an authenticated buider.
481 raise tornado
.web
.HTTPError(403)
484 class BuildersInfoHandler(BuildersBaseHandler
):
485 @tornado.web
.authenticated
489 "cpu_model" : self
.get_argument("cpu_model", None),
490 "cpu_count" : self
.get_argument("cpu_count", None),
491 "cpu_arch" : self
.get_argument("cpu_arch", None),
492 "cpu_bogomips" : self
.get_argument("cpu_bogomips", None),
495 "pakfire_version" : self
.get_argument("pakfire_version", None),
496 "host_key" : self
.get_argument("host_key", None),
499 "os_name" : self
.get_argument("os_name", None),
501 self
.builder
.update_info(**args
)
504 class BuildersKeepaliveHandler(BuildersBaseHandler
):
505 @tornado.web
.authenticated
509 "loadavg1" : self
.get_argument_float("loadavg1", None),
510 "loadavg5" : self
.get_argument_float("loadavg5", None),
511 "loadavg15" : self
.get_argument_float("loadavg15", None),
514 "mem_total" : self
.get_argument_int("mem_total", None),
515 "mem_free" : self
.get_argument_int("mem_free", None),
518 "swap_total" : self
.get_argument_int("swap_total", None),
519 "swap_free" : self
.get_argument_int("swap_free", None),
522 "space_free" : self
.get_argument_int("space_free", None),
524 self
.builder
.update_keepalive(**args
)
529 class BuildersJobsQueueHandler(BuildersBaseHandler
):
530 @tornado.web
.asynchronous
531 @tornado.web
.authenticated
536 # Break if the connection has been closed in the mean time.
537 if self
.connection_closed():
538 logging
.warning("Connection closed")
541 # Check if there is a job for us.
542 job
= self
.builder
.get_next_job()
544 # Got no job, wait and try again.
546 # Check if we have been running for too long.
547 if self
.runtime
>= self
.max_runtime
:
548 logging
.debug("Exceeded max. runtime. Finishing request.")
551 # Try again in a jiffy.
552 self
.add_timeout(self
.heartbeat
, self
.callback
)
556 # Set job to dispatching state.
557 job
.state
= "dispatching"
559 # Set our build host.
560 job
.builder
= self
.builder
565 "source_url" : job
.build
.source_download
,
566 "source_hash_sha512" : job
.build
.source_hash_sha512
,
567 "type" : "test" if job
.test
else "release",
568 "config" : job
.get_config(),
571 # Send build information to the builder.
574 # If anything went wrong, we reset the state.
575 job
.state
= "pending"
580 return 15 # 15 seconds
583 def max_runtime(self
):
584 timeout
= self
.get_argument_int("timeout", None)
586 return timeout
- self
.heartbeat
591 class BuildersJobsStateHandler(BuildersBaseHandler
):
592 @tornado.web
.authenticated
593 def post(self
, job_uuid
, state
):
594 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
596 raise tornado
.web
.HTTPError(404, "Invalid job id.")
598 if not job
.builder
== self
.builder
:
599 raise tornado
.web
.HTTPError(403, "Altering another builder's build.")
601 # Save information to database.
604 message
= self
.get_argument("message", None)
605 job
.update_message(message
)
610 class BuildersJobsBuildrootHandler(BuildersBaseHandler
):
611 @tornado.web
.authenticated
612 def post(self
, job_uuid
):
613 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
615 raise tornado
.web
.HTTPError(404, "Invalid job id.")
617 if not job
.builder
== self
.builder
:
618 raise tornado
.web
.HTTPError(403, "Altering another builder's build.")
621 buildroot
= self
.get_argument_json("buildroot", None)
623 job
.save_buildroot(buildroot
)
628 class BuildersJobsAddFileHandler(BuildersBaseHandler
):
629 @tornado.web
.authenticated
630 def post(self
, job_uuid
, upload_id
):
631 type = self
.get_argument("type")
632 assert type in ("package", "log")
634 # Fetch job we are working on and check if it is actually ours.
635 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
637 raise tornado
.web
.HTTPError(404, "Invalid job id.")
639 if not job
.builder
== self
.builder
:
640 raise tornado
.web
.HTTPError(403, "Altering another builder's job.")
642 # Fetch uploaded file object and check we uploaded it ourself.
643 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
645 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
647 if not upload
.builder
== self
.builder
:
648 raise tornado
.web
.HTTPError(403, "Using an other host's file.")
650 # Remove all files that have to be deleted, first.
651 self
.backend
.cleanup_files()
654 job
.add_file(upload
.path
)
657 # Finally, remove the uploaded file.