]>
git.ipfire.org Git - 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", None)
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 @tornado.web
.stream_request_body
166 class UploadsStreamHandler(BaseHandler
):
167 @tornado.web
.authenticated
172 upload_uuid
= self
.get_argument("id")
174 # Fetch upload object from database
175 self
.upload
= self
.backend
.uploads
.get_by_uuid(upload_uuid
)
177 raise tornado
.web
.HTTPError(404)
179 def data_received(self
, data
):
180 logging
.debug("Received chunk of %s bytes" % len(data
))
181 self
.size
+= len(data
)
183 # Write the received chunk to disk
184 with self
.db
.transaction():
185 self
.upload
.append(data
)
188 logging
.info("Received entire file (%s bytes)" % self
.size
)
190 with self
.db
.transaction():
191 self
.upload
.finished()
196 class UploadsSendChunkHandler(BaseHandler
):
197 @tornado.web
.authenticated
198 def post(self
, upload_id
):
199 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
201 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
203 if not upload
.builder
== self
.builder
:
204 raise tornado
.web
.HTTPError(403, "Uploading an other host's file.")
206 chksum
= self
.get_argument("chksum")
207 data
= self
.get_argument("data")
210 data
= base64
.b64decode(data
)
212 # Calculate hash and compare.
213 h
= hashlib
.new("sha512")
216 if not chksum
== h
.hexdigest():
217 raise tornado
.web
.HTTPError(400, "Checksum mismatch")
219 # Append the data to file.
220 with self
.db
.transaction():
224 class UploadsFinishedHandler(BaseHandler
):
225 @tornado.web
.authenticated
226 def get(self
, upload_id
):
227 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
229 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
231 if not upload
.builder
== self
.builder
:
232 raise tornado
.web
.HTTPError(403, "Uploading an other host's file.")
234 # Validate the uploaded data to its hash.
235 ret
= upload
.validate()
237 # If the validation was successfull, we mark the upload
238 # as finished and send True to the client.
245 # In case the download was corrupted or incomplete, we delete it
246 # and tell the client to start over.
247 with self
.db
.transaction():
250 self
.finish("ERROR: CORRUPTED OR INCOMPLETE FILE")
253 class UploadsDestroyHandler(BaseHandler
):
254 @tornado.web
.authenticated
255 def get(self
, upload_id
):
256 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
258 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
260 if not upload
.builder
== self
.builder
:
261 raise tornado
.web
.HTTPError(403, "Removing an other host's file.")
263 # Remove the upload from the database and trash the data.
264 with self
.db
.transaction():
270 class BuildsCreateHandler(BaseHandler
):
271 @tornado.web
.authenticated
273 # Get the upload ID of the package file.
274 upload_id
= self
.get_argument("upload_id")
276 # Get the identifier of the distribution we build for.
277 distro_ident
= self
.get_argument("distro")
279 # Get a list of arches to build for.
280 arches
= self
.get_argument("arches", None)
284 # Get previously uploaded file to create this build from.
285 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
287 raise tornado
.web
.HTTPError(400, "Upload does not exist: %s" % upload_id
)
289 # Check if the uploaded file belongs to this user/builder.
290 if self
.user
and not upload
.user
== self
.user
:
291 raise tornado
.web
.HTTPError(400, "Upload does not belong to this user")
293 elif self
.builder
and not upload
.builder
== self
.builder
:
294 raise tornado
.web
.HTTPError(400, "Upload does not belong to this builder")
296 # Get distribution this package should be built for.
297 distro
= self
.backend
.distros
.get_by_ident(distro_ident
)
299 distro
= self
.backend
.distros
.get_default()
301 # Open the package that was uploaded earlier and add it to
302 # the database. Create a new build object from the uploaded package.
304 build
= self
.backend
.builds
.create_from_source_package(upload
.path
, distro
=distro
,
305 type="scratch", arches
=arches
, owner
=self
.user
)
308 # Raise any exception.
312 # Creating the build will move the file to the build directory,
313 # so we can safely remove the uploaded file.
316 # Send the build ID back to the user.
317 self
.finish(build
.uuid
)
320 class BuildsGetHandler(BaseHandler
):
321 def get(self
, build_uuid
):
322 build
= self
.backend
.builds
.get_by_uuid(build_uuid
)
324 raise tornado
.web
.HTTPError(404, "Could not find build: %s" % build_uuid
)
327 "distro" : build
.distro
.identifier
,
328 "jobs" : [j
.uuid
for j
in build
.jobs
],
330 "package" : build
.pkg
.uuid
,
331 "priority" : build
.priority
,
332 "score" : build
.score
,
333 "severity" : build
.severity
,
334 "state" : build
.state
,
335 "sup_arches" : build
.supported_arches
,
336 "time_created" : build
.created
.isoformat(),
341 # If the build is in a repository, update that bit.
343 ret
["repo"] = build
.repo
.identifier
350 class JobsBaseHandler(BaseHandler
):
351 def job2json(self
, job
):
354 "build" : job
.build
.uuid
,
355 "duration" : job
.duration
,
357 "packages" : [p
.uuid
for p
in job
.packages
],
359 "time_created" : job
.time_created
.isoformat(),
360 "type" : "test" if job
.test
else "release",
365 ret
["builder"] = job
.builder
.hostname
368 ret
["time_started"] = job
.time_started
.isoformat()
370 if job
.time_finished
:
371 ret
["time_finished"] = job
.time_finished
.isoformat()
376 class JobsGetActiveHandler(JobsBaseHandler
):
378 # Get list of all active jobs.
379 jobs
= self
.backend
.jobs
.get_active()
382 "jobs" : [self
.job2json(j
) for j
in jobs
],
388 class JobsGetLatestHandler(JobsBaseHandler
):
390 limit
= self
.get_argument_int("limit", 5)
392 # Get the latest jobs.
393 jobs
= self
.backend
.jobs
.get_recently_ended(limit
=limit
)
396 "jobs" : [self
.job2json(j
) for j
in jobs
],
402 class JobsGetQueueHandler(JobsBaseHandler
):
404 limit
= self
.get_argument_int("limit", 5)
408 for job
in self
.backend
.jobqueue
:
415 "jobs" : [self
.job2json(j
) for j
in jobs
],
421 class JobsGetHandler(JobsBaseHandler
):
422 def get(self
, job_uuid
):
423 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
425 raise tornado
.web
.HTTPError(404, "Could not find job: %s" % job_uuid
)
427 ret
= self
.job2json(job
)
433 class PackagesGetHandler(BaseHandler
):
434 def get(self
, package_uuid
):
435 pkg
= self
.backend
.packages
.get_by_uuid(package_uuid
)
437 raise tornado
.web
.HTTPError(404, "Could not find package: %s" % package_uuid
)
441 "build_id" : pkg
.build_id
,
442 "build_host" : pkg
.build_host
,
443 "build_time" : pkg
.build_time
.isoformat(),
444 "description" : pkg
.description
,
446 "filesize" : pkg
.filesize
,
447 "friendly_name" : pkg
.friendly_name
,
448 "friendly_version" : pkg
.friendly_version
,
449 "groups" : pkg
.groups
,
450 "hash_sha512" : pkg
.hash_sha512
,
451 "license" : pkg
.license
,
453 "release" : pkg
.release
,
455 "summary" : pkg
.summary
,
459 "version" : pkg
.version
,
462 "prerequires" : pkg
.prerequires
,
463 "requires" : pkg
.requires
,
464 "provides" : pkg
.provides
,
465 "obsoletes" : pkg
.obsoletes
,
466 "conflicts" : pkg
.conflicts
,
469 if pkg
.type == "source":
470 ret
["supported_arches"] = pkg
.supported_arches
472 if isinstance(pkg
.maintainer
, users
.User
):
473 ret
["maintainer"] = "%s <%s>" % (pkg
.maintainer
.realname
, pkg
.maintainer
.email
)
475 ret
["maintainer"] = pkg
.maintainer
478 ret
["distro"] = pkg
.distro
.identifier
485 class BuildersBaseHandler(BaseHandler
):
487 # The request must come from an authenticated buider.
489 raise tornado
.web
.HTTPError(403)
492 class BuildersInfoHandler(BuildersBaseHandler
):
493 @tornado.web
.authenticated
497 "cpu_model" : self
.get_argument("cpu_model", None),
498 "cpu_count" : self
.get_argument("cpu_count", None),
499 "cpu_arch" : self
.get_argument("cpu_arch", None),
500 "cpu_bogomips" : self
.get_argument("cpu_bogomips", None),
503 "pakfire_version" : self
.get_argument("pakfire_version", None),
504 "host_key" : self
.get_argument("host_key", None),
507 "os_name" : self
.get_argument("os_name", None),
509 self
.builder
.update_info(**args
)
512 class BuildersKeepaliveHandler(BuildersBaseHandler
):
513 @tornado.web
.authenticated
517 "loadavg1" : self
.get_argument_float("loadavg1", None),
518 "loadavg5" : self
.get_argument_float("loadavg5", None),
519 "loadavg15" : self
.get_argument_float("loadavg15", None),
522 "mem_total" : self
.get_argument_int("mem_total", None),
523 "mem_free" : self
.get_argument_int("mem_free", None),
526 "swap_total" : self
.get_argument_int("swap_total", None),
527 "swap_free" : self
.get_argument_int("swap_free", None),
530 "space_free" : self
.get_argument_int("space_free", None),
532 self
.builder
.update_keepalive(**args
)
537 class BuildersGetNextJobHandler(BuildersBaseHandler
):
538 @tornado.web
.authenticated
542 # If the builder is disabled, we don't need to do anything
543 # but will ask it to return after 5 min
544 if not self
.builder
.enabled
:
545 self
.set_header("Retry-After", "300")
548 # If the builder has too many jobs running,
549 # we will tell it to return after 1 min
550 if self
.builder
.too_many_jobs
:
551 self
.set_header("Retry-After", "60")
554 # Okay, we are ready for the next job
555 job
= self
.builder
.get_next_job()
557 # If we got no job, we will ask the builder
558 # to return after 30 seconds
560 self
.set_header("Retry-After", "30")
563 # If we got a job, we will serialise it
564 # and send it to the builder
565 with self
.db
.transaction():
566 job
.start(builder
=self
.builder
)
571 "source_url" : job
.build
.source_download
,
572 "source_hash_sha512" : job
.build
.source_hash_sha512
,
573 "type" : "test" if job
.test
else "release",
574 "config" : job
.get_config(),
579 class BuildersJobsQueueHandler(BuildersBaseHandler
):
580 @tornado.web
.asynchronous
581 @tornado.web
.authenticated
586 # Break if the connection has been closed in the mean time.
587 if self
.connection_closed():
588 logging
.warning("Connection closed")
591 with self
.db
.transaction():
592 # Check if there is a job for us.
593 job
= self
.builder
.get_next_job()
595 # Got no job, wait and try again.
597 return self
.add_timeout(self
.heartbeat
, self
.callback
)
600 job
.start(builder
=self
.builder
)
605 "source_url" : job
.build
.source_download
,
606 "source_hash_sha512" : job
.build
.source_hash_sha512
,
607 "type" : "test" if job
.test
else "release",
608 "config" : job
.get_config(),
611 # Send build information to the builder.
616 return 15 # 15 seconds
619 def max_runtime(self
):
620 timeout
= self
.get_argument_int("timeout", None)
622 return timeout
- self
.heartbeat
627 class BuildersJobsStateHandler(BuildersBaseHandler
):
628 @tornado.web
.authenticated
629 def post(self
, job_uuid
, state
):
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 build.")
637 message
= self
.get_argument("message", None)
639 # Save information to database.
640 with self
.db
.transaction():
641 if state
== "running":
643 elif state
== "failed":
645 elif state
== "finished":
653 class BuildersJobsBuildrootHandler(BuildersBaseHandler
):
654 @tornado.web
.authenticated
655 def post(self
, job_uuid
):
656 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
658 raise tornado
.web
.HTTPError(404, "Invalid job id.")
660 if not job
.builder
== self
.builder
:
661 raise tornado
.web
.HTTPError(403, "Altering another builder's build.")
664 buildroot
= self
.get_argument_json("buildroot", None)
666 job
.save_buildroot(buildroot
)
671 class BuildersJobsAddFileHandler(BuildersBaseHandler
):
672 @tornado.web
.authenticated
673 def post(self
, job_uuid
, upload_id
):
674 type = self
.get_argument("type")
675 assert type in ("package", "log")
677 # Fetch job we are working on and check if it is actually ours.
678 job
= self
.backend
.jobs
.get_by_uuid(job_uuid
)
680 raise tornado
.web
.HTTPError(404, "Invalid job id.")
682 if not job
.builder
== self
.builder
:
683 raise tornado
.web
.HTTPError(403, "Altering another builder's job.")
685 # Fetch uploaded file object and check we uploaded it ourself.
686 upload
= self
.backend
.uploads
.get_by_uuid(upload_id
)
688 raise tornado
.web
.HTTPError(404, "Invalid upload id.")
690 if not upload
.builder
== self
.builder
:
691 raise tornado
.web
.HTTPError(403, "Using an other host's file.")
694 job
.add_file(upload
.path
)
697 # Finally, remove the uploaded file.