]>
git.ipfire.org Git - people/jschlag/pbs.git/blob - src/hub/handlers.py
c51d9ad2c6bd4a4f24a81121d6fa4dc396891237
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
.score
,
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 ret
= self
.job2json(job
)
416 class PackagesGetHandler(BaseHandler
):
417 def get(self
, package_uuid
):
418 pkg
= self
.backend
.packages
.get_by_uuid(package_uuid
)
420 raise tornado
.web
.HTTPError(404, "Could not find package: %s" % package_uuid
)
424 "build_id" : pkg
.build_id
,
425 "build_host" : pkg
.build_host
,
426 "build_time" : pkg
.build_time
.isoformat(),
427 "description" : pkg
.description
,
429 "filesize" : pkg
.filesize
,
430 "friendly_name" : pkg
.friendly_name
,
431 "friendly_version" : pkg
.friendly_version
,
432 "groups" : pkg
.groups
,
433 "hash_sha512" : pkg
.hash_sha512
,
434 "license" : pkg
.license
,
436 "release" : pkg
.release
,
438 "summary" : pkg
.summary
,
442 "version" : pkg
.version
,
445 "prerequires" : pkg
.prerequires
,
446 "requires" : pkg
.requires
,
447 "provides" : pkg
.provides
,
448 "obsoletes" : pkg
.obsoletes
,
449 "conflicts" : pkg
.conflicts
,
452 if pkg
.type == "source":
453 ret
["supported_arches"] = pkg
.supported_arches
455 if isinstance(pkg
.maintainer
, users
.User
):
456 ret
["maintainer"] = "%s <%s>" % (pkg
.maintainer
.realname
, pkg
.maintainer
.email
)
458 ret
["maintainer"] = pkg
.maintainer
461 ret
["distro"] = pkg
.distro
.identifier
468 class BuildersBaseHandler(BaseHandler
):
470 # The request must come from an authenticated buider.
472 raise tornado
.web
.HTTPError(403)
475 class BuildersInfoHandler(BuildersBaseHandler
):
476 @tornado.web
.authenticated
480 "cpu_model" : self
.get_argument("cpu_model", None),
481 "cpu_count" : self
.get_argument("cpu_count", None),
482 "cpu_arch" : self
.get_argument("cpu_arch", None),
483 "cpu_bogomips" : self
.get_argument("cpu_bogomips", None),
486 "pakfire_version" : self
.get_argument("pakfire_version", None),
487 "host_key" : self
.get_argument("host_key", None),
490 "os_name" : self
.get_argument("os_name", None),
492 self
.builder
.update_info(**args
)
495 class BuildersKeepaliveHandler(BuildersBaseHandler
):
496 @tornado.web
.authenticated
500 "loadavg1" : self
.get_argument_float("loadavg1", None),
501 "loadavg5" : self
.get_argument_float("loadavg5", None),
502 "loadavg15" : self
.get_argument_float("loadavg15", None),
505 "mem_total" : self
.get_argument_int("mem_total", None),
506 "mem_free" : self
.get_argument_int("mem_free", None),
509 "swap_total" : self
.get_argument_int("swap_total", None),
510 "swap_free" : self
.get_argument_int("swap_free", None),
513 "space_free" : self
.get_argument_int("space_free", None),
515 self
.builder
.update_keepalive(**args
)
520 class BuildersJobsQueueHandler(BuildersBaseHandler
):
521 @tornado.web
.asynchronous
522 @tornado.web
.authenticated
527 # Break if the connection has been closed in the mean time.
528 if self
.connection_closed():
529 logging
.warning("Connection closed")
532 # Check if there is a job for us.
533 job
= self
.builder
.get_next_job()
535 # Got no job, wait and try again.
537 # Check if we have been running for too long.
538 if self
.runtime
>= self
.max_runtime
:
539 logging
.debug("Exceeded max. runtime. Finishing request.")
542 # Try again in a jiffy.
543 self
.add_timeout(self
.heartbeat
, self
.callback
)
547 # Set job to dispatching state.
548 job
.state
= "dispatching"
550 # Set our build host.
551 job
.builder
= self
.builder
556 "source_url" : job
.build
.source_download
,
557 "source_hash_sha512" : job
.build
.source_hash_sha512
,
558 "type" : "test" if job
.test
else "release",
559 "config" : job
.get_config(),
562 # Send build information to the builder.
565 # If anything went wrong, we reset the state.
566 job
.state
= "pending"
571 return 15 # 15 seconds
574 def max_runtime(self
):
575 timeout
= self
.get_argument_int("timeout", None)
577 return timeout
- self
.heartbeat
582 class BuildersJobsStateHandler(BuildersBaseHandler
):
583 @tornado.web
.authenticated
584 def post(self
, job_uuid
, state
):
585 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
587 raise tornado
.web
.HTTPError(404, "Invalid job id.")
589 if not job
.builder
== self
.builder
:
590 raise tornado
.web
.HTTPError(403, "Altering another builder's build.")
592 # Save information to database.
595 message
= self
.get_argument("message", None)
596 job
.update_message(message
)
601 class BuildersJobsBuildrootHandler(BuildersBaseHandler
):
602 @tornado.web
.authenticated
603 def post(self
, job_uuid
):
604 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
606 raise tornado
.web
.HTTPError(404, "Invalid job id.")
608 if not job
.builder
== self
.builder
:
609 raise tornado
.web
.HTTPError(403, "Altering another builder's build.")
612 buildroot
= self
.get_argument_json("buildroot", None)
614 job
.save_buildroot(buildroot
)
619 class BuildersJobsAddFileHandler(BuildersBaseHandler
):
620 @tornado.web
.authenticated
621 def post(self
, job_uuid
, upload_id
):
622 type = self
.get_argument("type")
623 assert type in ("package", "log")
625 # Fetch job we are working on and check if it is actually ours.
626 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
628 raise tornado
.web
.HTTPError(404, "Invalid job id.")
630 if not job
.builder
== self
.builder
:
631 raise tornado
.web
.HTTPError(403, "Altering another builder's job.")
633 # Fetch uploaded file object and check we uploaded it ourself.
634 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
636 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
638 if not upload
.builder
== self
.builder
:
639 raise tornado
.web
.HTTPError(403, "Using an other host's file.")
641 # Remove all files that have to be deleted, first.
642 self
.backend
.cleanup_files()
645 job
.add_file(upload
.path
)
648 # Finally, remove the uploaded file.