depot.py revision 146
409N/A#!/usr/bin/python
290N/A#
290N/A# CDDL HEADER START
290N/A#
290N/A# The contents of this file are subject to the terms of the
290N/A# Common Development and Distribution License (the "License").
290N/A# You may not use this file except in compliance with the License.
290N/A#
290N/A# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
290N/A# or http://www.opensolaris.org/os/licensing.
290N/A# See the License for the specific language governing permissions
290N/A# and limitations under the License.
290N/A#
290N/A# When distributing Covered Code, include this CDDL HEADER in each
290N/A# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
290N/A# If applicable, add the following below this CDDL HEADER, with the
290N/A# fields enclosed by brackets "[]" replaced with your own identifying
290N/A# information: Portions Copyright [yyyy] [name of copyright owner]
290N/A#
290N/A# CDDL HEADER END
290N/A#
395N/A# Copyright 2007 Sun Microsystems, Inc. All rights reserved.
395N/A# Use is subject to license terms.
395N/A#
290N/A
454N/A# pkg.depotd - package repository daemon
290N/A
448N/A# XXX The prototype pkg.depotd combines both the version management server that
290N/A# answers to pkgsend(1) sessions and the HTTP file server that answers to the
290N/A# various GET operations that a pkg(1) client makes. This split is expected to
290N/A# be made more explicit, by constraining the pkg(1) operations such that they
383N/A# can be served as a typical HTTP/HTTPS session. Thus, pkg.depotd will reduce
290N/A# to a special purpose HTTP/HTTPS server explicitly for the version management
395N/A# operations, and must manipulate the various state files--catalogs, in
290N/A# particular--such that the pkg(1) pull client can operately accurately with
395N/A# only a basic HTTP/HTTPS server in place.
290N/A
290N/A# XXX We should support simple "last-modified" operations via HEAD queries.
290N/A
290N/A# XXX Although we pushed the evaluation of next-version, etc. to the pull
290N/A# client, we should probably provide a query API to do same on the server, for
383N/A# dumb clients (like a notification service).
290N/A
290N/Aimport BaseHTTPServer
290N/Aimport SocketServer
290N/Aimport errno
290N/Aimport getopt
290N/Aimport os
290N/Aimport re
290N/Aimport sha
465N/Aimport shutil
465N/Aimport sys
465N/Aimport time
801N/Aimport urllib
465N/Aimport tarfile
465N/Aimport cgi
465N/Aimport traceback
465N/A
465N/Aimport pkg.catalog as catalog
465N/Aimport pkg.dependency as dependency
465N/Aimport pkg.fmri as fmri
465N/Aimport pkg.misc as misc
465N/Aimport pkg.package as package
465N/Aimport pkg.version as version
465N/A
708N/Aimport pkg.server.face as face
708N/Aimport pkg.server.config as config
708N/Aimport pkg.server.transaction as trans
708N/A
708N/Adef usage():
708N/A print """\
708N/AUsage: /usr/lib/pkg.depotd [--readonly] [-d repo_dir] [-p port]
742N/A --readonly Read-only operation; modifying operations disallowed
742N/A"""
742N/A sys.exit(2)
742N/A
742N/Adef versions_0(scfg, request):
742N/A request.send_response(200)
742N/A request.send_header('Content-type', 'text/plain')
290N/A request.end_headers()
290N/A versions = "\n".join(
290N/A "%s %s" % (op, " ".join(vers))
395N/A for op, vers in vops.iteritems()
395N/A ) + "\n"
290N/A request.wfile.write(versions)
395N/A
395N/Adef catalog_0(scfg, request):
290N/A scfg.inc_catalog()
395N/A
395N/A request.send_response(200)
290N/A request.send_header('Content-type', 'text/plain')
395N/A request.end_headers()
395N/A request.wfile.write("%s" % scfg.catalog)
290N/A
448N/Adef manifest_0(scfg, request):
448N/A """The request is an encoded pkg FMRI. If the version is specified
534N/A incompletely, we return an error, as the client is expected to form
534N/A correct requests, based on its interpretation of the catalog and its
534N/A image policies."""
534N/A
534N/A scfg.inc_manifest()
534N/A
534N/A # Parse request into FMRI component and decode.
290N/A pfmri = urllib.unquote(request.path.split("/", 3)[-1])
290N/A
290N/A f = fmri.PkgFmri(pfmri, None)
534N/A
290N/A # Open manifest and send.
290N/A file = open("%s/%s" % (scfg.pkg_root, f.get_dir_path()), "r")
290N/A data = file.read()
290N/A
290N/A request.send_response(200)
661N/A request.send_header('Content-type', 'text/plain')
290N/A request.end_headers()
290N/A request.wfile.write(data)
290N/A
395N/Adef filelist_0(scfg, request):
290N/A """Request data contains application/x-www-form-urlencoded entries
290N/A with the requested filenames."""
290N/A hdrs = request.headers
290N/A # If the sender doesn't specify the content length, reject this request.
290N/A # Calling read() with no size specified will force the server to block
395N/A # until the client sends EOF, an undesireable situation
430N/A size = int(hdrs.getheader("Content-Length"))
395N/A if size == 0:
395N/A request.send_response(411)
395N/A return
424N/A
578N/A rfile = request.rfile
395N/A data_dict = cgi.parse_qs(rfile.read(size))
395N/A
395N/A scfg.inc_flist()
578N/A
395N/A request.send_response(200)
661N/A request.send_header("Content-type", "application/data")
661N/A request.end_headers()
661N/A
395N/A tar_stream = tarfile.open(mode = "w|", fileobj = request.wfile)
290N/A
290N/A for v in data_dict.values():
395N/A filepath = os.path.normpath(os.path.join(
395N/A scfg.file_root, misc.hash_file_name(v[0])))
395N/A
395N/A tar_stream.add(filepath, v[0], False)
395N/A scfg.inc_flist_files()
395N/A
395N/A tar_stream.close()
395N/A
395N/Adef file_0(scfg, request):
395N/A """The request is the SHA-1 hash name for the file."""
395N/A scfg.inc_file()
395N/A
395N/A fhash = request.path.split("/", 3)[-1]
290N/A
290N/A try:
395N/A file = open(os.path.normpath(os.path.join(
395N/A scfg.file_root, misc.hash_file_name(fhash))))
395N/A except IOError, e:
395N/A if e.errno == errno.ENOENT:
395N/A request.send_response(404)
395N/A else:
395N/A request.send_response(500)
395N/A return
395N/A
395N/A data = file.read()
395N/A
395N/A request.send_response(200)
395N/A request.send_header("Content-type", "application/data")
290N/A request.end_headers()
290N/A request.wfile.write(data)
430N/A
395N/Adef open_0(scfg, request):
395N/A # XXX Authentication will be handled by virtue of possessing a signed
395N/A # certificate (or a more elaborate system).
395N/A if scfg.is_read_only():
395N/A request.send_error(403, "Read-only server")
395N/A return
290N/A
395N/A t = trans.Transaction()
413N/A
413N/A ret = t.open(scfg, request)
413N/A if ret == 200:
395N/A scfg.in_flight_trans[t.get_basename()] = t
395N/A
413N/A request.send_response(200)
395N/A request.send_header('Content-type', 'text/plain')
395N/A request.send_header('Transaction-ID', t.get_basename())
413N/A request.end_headers()
395N/A elif ret == 400:
395N/A request.send_response(400)
395N/A else:
395N/A request.send_response(500)
395N/A
395N/A
395N/Adef close_0(scfg, request):
395N/A if scfg.is_read_only():
424N/A request.send_error(403, "Read-only server")
395N/A return
742N/A
742N/A # Pull transaction ID from headers.
742N/A trans_id = request.path.split("/", 3)[-1]
742N/A
742N/A # XXX KeyError?
742N/A t = scfg.in_flight_trans[trans_id]
742N/A t.close(request)
742N/A del scfg.in_flight_trans[trans_id]
742N/A
742N/Adef abandon_0(scfg, request):
742N/A if scfg.is_read_only():
395N/A request.send_error(403, "Read-only server")
395N/A return
395N/A
395N/A # Pull transaction ID from headers.
395N/A trans_id = request.path.split("/", 3)[-1]
395N/A
395N/A t = scfg.in_flight_trans[trans_id]
395N/A t.abandon(request)
395N/A del scfg.in_flight_trans[trans_id]
395N/A
578N/Adef add_0(scfg, request):
395N/A if scfg.is_read_only():
691N/A request.send_error(403, "Read-only server")
691N/A return
691N/A
395N/A trans_id, type = request.path.split("/", 4)[-2:]
395N/A
395N/A t = scfg.in_flight_trans[trans_id]
395N/A t.add_content(request, type)
395N/A
290N/Aif "PKG_REPO" in os.environ:
395N/A scfg = config.SvrConfig(os.environ["PKG_REPO"], "pkg.sun.com")
395N/Aelse:
591N/A scfg = config.SvrConfig("/var/pkg/repo", "pkg.sun.com")
591N/A
591N/Adef set_ops():
395N/A vops = {}
395N/A for name in globals():
290N/A m = re.match("(.*)_(\d+)", name)
708N/A
708N/A if not m:
708N/A continue
708N/A
708N/A op = m.group(1)
708N/A ver = m.group(2)
708N/A
708N/A if op in vops:
708N/A vops[op].append(ver)
708N/A else:
708N/A vops[op] = [ ver ]
708N/A
708N/A return vops
708N/A
708N/Aclass pkgHandler(BaseHTTPServer.BaseHTTPRequestHandler):
708N/A
708N/A def do_GET(self):
708N/A reqarr = self.path.lstrip("/").split("/")
708N/A operation = reqarr[0]
708N/A
708N/A if operation not in vops:
708N/A if face.match(self):
708N/A face.respond(scfg, self)
708N/A else:
290N/A face.unknown(scfg, self)
290N/A return
290N/A
290N/A # Make sure that we have a integer protocol version
290N/A try:
290N/A version = int(reqarr[1])
290N/A except IndexError, e:
290N/A self.send_response(400)
290N/A self.send_header("Content-type", "text/plain")
290N/A self.end_headers()
290N/A self.wfile.write("Missing version\n")
290N/A return
290N/A except ValueError, e:
395N/A self.send_response(400)
395N/A self.send_header("Content-type", "text/plain")
290N/A self.end_headers()
290N/A self.wfile.write("Non-integer version\n")
290N/A return
290N/A
290N/A op_method = "%s_%s" % (operation, version)
395N/A if op_method not in globals():
395N/A # If we get here, we know that 'operation' is supported.
395N/A # Assume 'version' is not supported for that operation.
290N/A self.send_response(404, "Version not supported")
395N/A self.send_header("Content-type", "text/plain")
395N/A self.end_headers()
395N/A
395N/A vns = "Version '%s' not supported for operation '%s'\n"
591N/A self.wfile.write(vns % (version, operation))
591N/A return
591N/A
591N/A op_call = op_method + "(scfg, self)"
691N/A
691N/A try:
691N/A exec op_call
691N/A except:
290N/A traceback.print_exc()
290N/A self.send_response(500)
290N/A
290N/A def do_POST(self):
290N/A self.do_GET()
591N/A
591N/A def do_PUT(self):
691N/A self.send_response(200)
691N/A self.send_header('Content-type', 'text/plain')
290N/A self.end_headers()
395N/A self.wfile.write('''PUT URI %s ; headers %s''' %
395N/A (self.path, self.headers))
290N/A
395N/A def do_DELETE(self):
395N/A self.send_response(200)
395N/A self.send_header('Content-type', 'text/plain')
395N/A self.end_headers()
290N/A self.wfile.write('''URI %s ; headers %s''' %
290N/A (self.path, self.headers))
290N/A
395N/Aclass ThreadingHTTPServer(SocketServer.ThreadingMixIn,
395N/A BaseHTTPServer.HTTPServer):
395N/A pass
395N/A
395N/Avops = {}
395N/A
290N/Aif __name__ == "__main__":
290N/A port = 10000
290N/A
290N/A try:
290N/A opts, pargs = getopt.getopt(sys.argv[1:], "d:np:", ["readonly"])
430N/A for opt, arg in opts:
290N/A if opt == "-n":
290N/A sys.exit(0)
395N/A elif opt == "-d":
290N/A scfg.set_repo_root(arg)
395N/A elif opt == "-p":
506N/A port = int(arg)
506N/A elif opt == "--readonly":
506N/A scfg.set_read_only()
506N/A except getopt.GetoptError, e:
506N/A print "pkg.depotd: unknown option '%s'" % e.opt
506N/A usage()
395N/A
506N/A scfg.init_dirs()
506N/A scfg.acquire_in_flight()
513N/A scfg.acquire_catalog()
506N/A
506N/A vops = set_ops()
506N/A
513N/A server = ThreadingHTTPServer(('', port), pkgHandler)
506N/A server.serve_forever()
506N/A