]>
git.ipfire.org Git - pbs.git/blob - src/hub/handlers.py
1aad2a056bb3a7cc39671220c5116dc7e14cee6d
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
)
138 class StatsBuildsTypesHandler(BaseHandler
):
142 types
= self
.backend
.builds
.get_types_stats()
143 for type, count
in types
.items():
144 ret
["builds_type_%s" % type] = count
149 class StatsJobsHandler(BaseHandler
):
156 class StatsJobsDurationsHandler(BaseHandler
):
158 durations
= self
.backend
.jobs
.get_build_durations()
161 for platform
, d
in durations
.items():
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),
171 class StatsJobsStatesHandler(BaseHandler
):
175 states
= self
.backend
.jobs
.get_state_stats()
176 for state
, count
in states
.items():
177 ret
["jobs_state_%s" % state
] = count
182 class StatsJobsQueueHandler(BaseHandler
):
187 ret
["job_queue_length"] = self
.backend
.jobs
.get_queue_length()
188 for state
in ("new", "pending"):
189 ret
["job_queue_length_%s" % state
] = self
.backend
.jobs
.get_queue_length(state
)
191 # Average waiting time.
192 ret
["job_queue_avg_wait_time"] = self
.backend
.jobs
.get_avg_wait_time()
199 class UploadsCreateHandler(BaseHandler
):
201 Create a new upload object in the database and return a unique ID
205 @tornado.web
.authenticated
207 # XXX Check permissions
209 filename
= self
.get_argument("filename")
210 filesize
= self
.get_argument_int("filesize")
211 filehash
= self
.get_argument("hash")
213 upload
= uploads
.Upload
.create(self
.backend
, filename
, filesize
,
214 filehash
, user
=self
.user
, builder
=self
.builder
)
216 self
.finish(upload
.uuid
)
219 class UploadsSendChunkHandler(BaseHandler
):
220 @tornado.web
.authenticated
221 def post(self
, upload_id
):
222 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
224 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
226 if not upload
.builder
== self
.builder
:
227 raise tornado
.web
.HTTPError(403, "Uploading an other host's file.")
229 chksum
= self
.get_argument("chksum")
230 data
= self
.get_argument("data")
233 data
= base64
.b64decode(data
)
235 # Calculate hash and compare.
236 h
= hashlib
.new("sha512")
239 if not chksum
== h
.hexdigest():
240 raise tornado
.web
.HTTPError(400, "Checksum mismatch")
242 # Append the data to file.
246 class UploadsFinishedHandler(BaseHandler
):
247 @tornado.web
.authenticated
248 def get(self
, upload_id
):
249 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
251 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
253 if not upload
.builder
== self
.builder
:
254 raise tornado
.web
.HTTPError(403, "Uploading an other host's file.")
256 # Validate the uploaded data to its hash.
257 ret
= upload
.validate()
259 # If the validation was successfull, we mark the upload
260 # as finished and send True to the client.
267 # In case the download was corrupted or incomplete, we delete it
268 # and tell the client to start over.
271 self
.finish("ERROR: CORRUPTED OR INCOMPLETE FILE")
274 class UploadsDestroyHandler(BaseHandler
):
275 @tornado.web
.authenticated
276 def get(self
, upload_id
):
277 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
279 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
281 if not upload
.builder
== self
.builder
:
282 raise tornado
.web
.HTTPError(403, "Removing an other host's file.")
284 # Remove the upload from the database and trash the data.
290 class BuildsCreateHandler(BaseHandler
):
291 @tornado.web
.authenticated
293 # Get the upload ID of the package file.
294 upload_id
= self
.get_argument("upload_id")
296 # Get the identifier of the distribution we build for.
297 distro_ident
= self
.get_argument("distro")
299 # Get a list of arches to build for.
300 arches
= self
.get_argument("arches", None)
304 # Process build type.
305 build_type
= self
.get_argument("build_type")
306 if build_type
== "release":
307 check_for_duplicates
= True
308 elif build_type
== "scratch":
309 check_for_duplicates
= False
311 raise tornado
.web
.HTTPError(400, "Invalid build type")
313 ## Check if the user has permission to create a build.
314 # Users only have the permission to create scratch builds.
315 if self
.user
and not build_type
== "scratch":
316 raise tornado
.web
.HTTPError(403, "Users are only allowed to upload scratch builds")
318 # Get previously uploaded file to create this build from.
319 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
321 raise tornado
.web
.HTTPError(400, "Upload does not exist: %s" % upload_id
)
323 # Check if the uploaded file belongs to this user/builder.
324 if self
.user
and not upload
.user
== self
.user
:
325 raise tornado
.web
.HTTPError(400, "Upload does not belong to this user.")
327 elif self
.builder
and not upload
.builder
== self
.builder
:
328 raise tornado
.web
.HTTPError(400, "Upload does not belong to this builder.")
330 # Get distribution this package should be built for.
331 distro
= self
.backend
.distros
.get_by_ident(distro_ident
)
333 distro
= self
.backend
.distros
.get_default()
335 # Open the package that was uploaded earlier and add it to
336 # the database. Create a new build object from the uploaded package.
339 "check_for_duplicates" : check_for_duplicates
,
344 args
["owner"] = self
.user
347 pkg
, build
= builds
.import_from_package(self
.backend
, upload
.path
, **args
)
350 # Raise any exception.
354 # Creating the build will move the file to the build directory,
355 # so we can safely remove the uploaded file.
358 # Send the build ID back to the user.
359 self
.finish(build
.uuid
)
362 class BuildsGetHandler(BaseHandler
):
363 def get(self
, build_uuid
):
364 build
= self
.backend
.builds
.get_by_uuid(build_uuid
)
366 raise tornado
.web
.HTTPError(404, "Could not find build: %s" % build_uuid
)
369 "distro" : build
.distro
.identifier
,
370 "jobs" : [j
.uuid
for j
in build
.jobs
],
372 "package" : build
.pkg
.uuid
,
373 "priority" : build
.priority
,
374 "score" : build
.credits
,
375 "severity" : build
.severity
,
376 "state" : build
.state
,
377 "sup_arches" : build
.supported_arches
,
378 "time_created" : build
.created
.isoformat(),
383 # If the build is in a repository, update that bit.
385 ret
["repo"] = build
.repo
.identifier
392 class JobsBaseHandler(BaseHandler
):
393 def job2json(self
, job
):
395 "arch" : job
.arch
.name
,
396 "build" : job
.build
.uuid
,
397 "duration" : job
.duration
,
399 "packages" : [p
.uuid
for p
in job
.packages
],
401 "time_created" : job
.time_created
.isoformat(),
407 ret
["builder"] = job
.builder
.hostname
410 ret
["time_started"] = job
.time_started
.isoformat()
412 if job
.time_finished
:
413 ret
["time_finished"] = job
.time_finished
.isoformat()
418 class JobsGetActiveHandler(JobsBaseHandler
):
420 # Get list of all active jobs.
421 jobs
= self
.backend
.jobs
.get_active()
424 "jobs" : [self
.job2json(j
) for j
in jobs
],
430 class JobsGetLatestHandler(JobsBaseHandler
):
432 limit
= self
.get_argument_int("limit", 5)
434 # Get the latest jobs.
435 jobs
= self
.backend
.jobs
.get_latest(age
="24 HOUR", limit
=limit
)
438 "jobs" : [self
.job2json(j
) for j
in jobs
],
444 class JobsGetQueueHandler(JobsBaseHandler
):
446 limit
= self
.get_argument_int("limit", 5)
449 jobs
= self
.backend
.jobs
.get_next(limit
=limit
)
452 "jobs" : [self
.job2json(j
) for j
in jobs
],
458 class JobsGetHandler(JobsBaseHandler
):
459 def get(self
, job_uuid
):
460 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
462 raise tornado
.web
.HTTPError(404, "Could not find job: %s" % job_uuid
)
464 # Check if user is allowed to view this job.
465 if job
.build
.public
== False:
467 raise tornado
.web
.HTTPError(401)
469 # Check if an authenticated user has permission to see this build.
470 if not job
.build
.has_perm(self
.user
):
471 raise tornado
.web
.HTTPError(403)
473 ret
= self
.job2json(job
)
479 class PackagesGetHandler(BaseHandler
):
480 def get(self
, package_uuid
):
481 pkg
= self
.backend
.packages
.get_by_uuid(package_uuid
)
483 raise tornado
.web
.HTTPError(404, "Could not find package: %s" % package_uuid
)
486 "arch" : pkg
.arch
.name
,
487 "build_id" : pkg
.build_id
,
488 "build_host" : pkg
.build_host
,
489 "build_time" : pkg
.build_time
.isoformat(),
490 "description" : pkg
.description
,
492 "filesize" : pkg
.filesize
,
493 "friendly_name" : pkg
.friendly_name
,
494 "friendly_version" : pkg
.friendly_version
,
495 "groups" : pkg
.groups
,
496 "hash_sha512" : pkg
.hash_sha512
,
497 "license" : pkg
.license
,
499 "release" : pkg
.release
,
501 "summary" : pkg
.summary
,
505 "version" : pkg
.version
,
508 "prerequires" : pkg
.prerequires
,
509 "requires" : pkg
.requires
,
510 "provides" : pkg
.provides
,
511 "obsoletes" : pkg
.obsoletes
,
512 "conflicts" : pkg
.conflicts
,
515 if pkg
.type == "source":
516 ret
["supported_arches"] = pkg
.supported_arches
518 if isinstance(pkg
.maintainer
, users
.User
):
519 ret
["maintainer"] = "%s <%s>" % (pkg
.maintainer
.realname
, pkg
.maintainer
.email
)
521 ret
["maintainer"] = pkg
.maintainer
524 ret
["distro"] = pkg
.distro
.identifier
531 class BuildersBaseHandler(BaseHandler
):
533 # The request must come from an authenticated buider.
535 raise tornado
.web
.HTTPError(403)
538 class BuildersInfoHandler(BuildersBaseHandler
):
539 @tornado.web
.authenticated
543 "cpu_model" : self
.get_argument("cpu_model", None),
544 "cpu_count" : self
.get_argument("cpu_count", None),
545 "cpu_arch" : self
.get_argument("cpu_arch", None),
546 "cpu_bogomips" : self
.get_argument("cpu_bogomips", None),
549 "pakfire_version" : self
.get_argument("pakfire_version", None),
550 "host_key" : self
.get_argument("host_key", None),
553 "os_name" : self
.get_argument("os_name", None),
555 self
.builder
.update_info(**args
)
558 class BuildersKeepaliveHandler(BuildersBaseHandler
):
559 @tornado.web
.authenticated
563 "loadavg1" : self
.get_argument_float("loadavg1", None),
564 "loadavg5" : self
.get_argument_float("loadavg5", None),
565 "loadavg15" : self
.get_argument_float("loadavg15", None),
568 "mem_total" : self
.get_argument_int("mem_total", None),
569 "mem_free" : self
.get_argument_int("mem_free", None),
572 "swap_total" : self
.get_argument_int("swap_total", None),
573 "swap_free" : self
.get_argument_int("swap_free", None),
576 "space_free" : self
.get_argument_int("space_free", None),
578 self
.builder
.update_keepalive(**args
)
583 class BuildersJobsQueueHandler(BuildersBaseHandler
):
584 @tornado.web
.asynchronous
585 @tornado.web
.authenticated
590 # Break if the connection has been closed in the mean time.
591 if self
.connection_closed():
592 logging
.warning("Connection closed")
595 # Check if there is a job for us.
596 job
= self
.builder
.get_next_job()
598 # Got no job, wait and try again.
600 # Check if we have been running for too long.
601 if self
.runtime
>= self
.max_runtime
:
602 logging
.debug("Exceeded max. runtime. Finishing request.")
605 # Try again in a jiffy.
606 self
.add_timeout(self
.heartbeat
, self
.callback
)
610 # Set job to dispatching state.
611 job
.state
= "dispatching"
613 # Set our build host.
614 job
.builder
= self
.builder
618 "arch" : job
.arch
.name
,
619 "source_url" : job
.build
.source_download
,
620 "source_hash_sha512" : job
.build
.source_hash_sha512
,
622 "config" : job
.get_config(),
625 # Send build information to the builder.
628 # If anything went wrong, we reset the state.
629 job
.state
= "pending"
634 return 15 # 15 seconds
637 def max_runtime(self
):
638 timeout
= self
.get_argument_int("timeout", None)
640 return timeout
- self
.heartbeat
645 class BuildersJobsStateHandler(BuildersBaseHandler
):
646 @tornado.web
.authenticated
647 def post(self
, job_uuid
, state
):
648 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
650 raise tornado
.web
.HTTPError(404, "Invalid job id.")
652 if not job
.builder
== self
.builder
:
653 raise tornado
.web
.HTTPError(403, "Altering another builder's build.")
655 # Save information to database.
658 message
= self
.get_argument("message", None)
659 job
.update_message(message
)
664 class BuildersJobsBuildrootHandler(BuildersBaseHandler
):
665 @tornado.web
.authenticated
666 def post(self
, job_uuid
):
667 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
669 raise tornado
.web
.HTTPError(404, "Invalid job id.")
671 if not job
.builder
== self
.builder
:
672 raise tornado
.web
.HTTPError(403, "Altering another builder's build.")
675 buildroot
= self
.get_argument_json("buildroot", None)
677 job
.save_buildroot(buildroot
)
682 class BuildersJobsAddFileHandler(BuildersBaseHandler
):
683 @tornado.web
.authenticated
684 def post(self
, job_uuid
, upload_id
):
685 type = self
.get_argument("type")
686 assert type in ("package", "log")
688 # Fetch job we are working on and check if it is actually ours.
689 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
691 raise tornado
.web
.HTTPError(404, "Invalid job id.")
693 if not job
.builder
== self
.builder
:
694 raise tornado
.web
.HTTPError(403, "Altering another builder's job.")
696 # Fetch uploaded file object and check we uploaded it ourself.
697 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
699 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
701 if not upload
.builder
== self
.builder
:
702 raise tornado
.web
.HTTPError(403, "Using an other host's file.")
704 # Remove all files that have to be deleted, first.
705 self
.backend
.cleanup_files()
708 job
.add_file(upload
.path
)
711 # Finally, remove the uploaded file.