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