depot.py revision 145
1516N/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#
2339N/A# Copyright 2007 Sun Microsystems, Inc. All rights reserved.
395N/A# Use is subject to license terms.
290N/A#
883N/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.
849N/A
1516N/A# XXX We should support simple "last-modified" operations via HEAD queries.
290N/A
849N/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
290N/A# dumb clients (like a notification service).
290N/A
290N/Aimport BaseHTTPServer
383N/Aimport SocketServer
290N/Aimport errno
290N/Aimport getopt
2339N/Aimport os
290N/Aimport re
290N/Aimport sha
290N/Aimport shutil
290N/Aimport sys
290N/Aimport time
290N/Aimport urllib
1660N/Aimport tarfile
1660N/Aimport cgi
1660N/Aimport traceback
1660N/A
1660N/Aimport pkg.catalog as catalog
1660N/Aimport pkg.dependency as dependency
1660N/Aimport pkg.fmri as fmri
1660N/Aimport pkg.misc as misc
1660N/Aimport pkg.package as package
1660N/Aimport pkg.version as version
1660N/A
1660N/Aimport pkg.server.face as face
1660N/Aimport pkg.server.config as config
1660N/Aimport pkg.server.transaction as trans
1660N/A
1660N/Adef usage():
1660N/A print """\
1660N/AUsage: /usr/lib/pkg.depotd [--readonly] [-d repo_dir] [-p port]
448N/A --readonly Read-only operation; modifying operations disallowed
448N/A"""
534N/A sys.exit(2)
534N/A
534N/Adef versions_0(scfg, request):
534N/A request.send_response(200)
534N/A request.send_header('Content-type', 'text/plain')
534N/A request.end_headers()
534N/A versions = "\n".join(
290N/A "%s %s" % (op, " ".join(vers))
290N/A for op, vers in vops.iteritems()
954N/A ) + "\n"
954N/A request.wfile.write(versions)
954N/A
954N/Adef catalog_0(scfg, request):
534N/A scfg.inc_catalog()
1099N/A
290N/A request.send_response(200)
1516N/A request.send_header('Content-type', 'text/plain')
290N/A request.end_headers()
290N/A request.wfile.write("%s" % scfg.catalog)
290N/A
661N/Adef manifest_0(scfg, request):
290N/A """The request is an encoded pkg FMRI. If the version is specified
2494N/A incompletely, we return an error, as the client is expected to form
2494N/A correct requests, based on its interpretation of the catalog and its
2494N/A image policies."""
290N/A
2390N/A scfg.inc_manifest()
1498N/A
1498N/A # Parse request into FMRI component and decode.
2310N/A m = re.match("^/manifest/\d+/(.*)", request.path)
2310N/A pfmri = urllib.unquote(m.group(1))
2310N/A
2310N/A f = fmri.PkgFmri(pfmri, None)
290N/A
1674N/A # Open manifest and send.
1674N/A file = open("%s/%s" % (scfg.pkg_root, f.get_dir_path()), "r")
2262N/A data = file.read()
1674N/A
395N/A request.send_response(200)
430N/A request.send_header('Content-type', 'text/plain')
395N/A request.end_headers()
1544N/A request.wfile.write(data)
1968N/A
1557N/Adef filelist_0(scfg, request):
1903N/A """Request data contains application/x-www-form-urlencoded entries
2046N/A with the requested filenames."""
2240N/A hdrs = request.headers
1506N/A # If the sender doesn't specify the content length, reject this request.
395N/A # Calling read() with no size specified will force the server to block
395N/A # until the client sends EOF, an undesireable situation
2026N/A size = int(hdrs.getheader("Content-Length"))
424N/A if size == 0:
1024N/A request.send_response(411)
395N/A return
395N/A
395N/A rfile = request.rfile
2078N/A data_dict = cgi.parse_qs(rfile.read(size))
578N/A
1172N/A scfg.inc_flist()
2310N/A
395N/A request.send_response(200)
661N/A request.send_header("Content-type", "application/data")
1099N/A request.end_headers()
1902N/A
2310N/A tar_stream = tarfile.open(mode = "w|", fileobj = request.wfile)
661N/A
395N/A for v in data_dict.values():
849N/A filepath = os.path.normpath(os.path.join(
290N/A scfg.file_root, misc.hash_file_name(v[0])))
395N/A
395N/A tar_stream.add(filepath, v[0], False)
1968N/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 m = re.match("^/file/\d+/(.*)", request.path)
395N/A fhash = m.group(1)
395N/A
395N/A try:
290N/A file = open(os.path.normpath(os.path.join(
290N/A scfg.file_root, misc.hash_file_name(fhash))))
395N/A except IOError, e:
395N/A if e.errno == errno.ENOENT:
1231N/A request.send_response(404)
1557N/A else:
1903N/A request.send_response(500)
1557N/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")
395N/A request.end_headers()
395N/A request.wfile.write(data)
395N/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).
290N/A if scfg.is_read_only():
290N/A request.send_error(403, "Read-only server")
430N/A return
395N/A
395N/A t = trans.Transaction()
395N/A
395N/A ret = t.open(scfg, request)
1302N/A if ret == 200:
395N/A scfg.in_flight_trans[t.get_basename()] = t
395N/A
290N/A request.send_response(200)
395N/A request.send_header('Content-type', 'text/plain')
1024N/A request.send_header('Transaction-ID', t.get_basename())
413N/A request.end_headers()
1544N/A elif ret == 400:
1557N/A request.send_response(400)
1903N/A else:
2046N/A request.send_response(500)
2240N/A
1506N/A
413N/Adef close_0(scfg, request):
2026N/A if scfg.is_read_only():
413N/A request.send_error(403, "Read-only server")
1978N/A return
1024N/A
395N/A # Pull transaction ID from headers.
395N/A m = re.match("^/close/\d+/(.*)", request.path)
2310N/A trans_id = m.group(1)
2310N/A
395N/A # XXX KeyError?
395N/A t = scfg.in_flight_trans[trans_id]
413N/A t.close(request)
395N/A del scfg.in_flight_trans[trans_id]
395N/A
395N/Adef abandon_0(scfg, request):
395N/A if scfg.is_read_only():
395N/A request.send_error(403, "Read-only server")
395N/A return
2339N/A
1191N/A # Pull transaction ID from headers.
1452N/A m = re.match("^/abandon/\d+/(.*)", request.path)
1231N/A trans_id = m.group(1)
2046N/A
395N/A t = scfg.in_flight_trans[trans_id]
395N/A t.abandon(request)
424N/A del scfg.in_flight_trans[trans_id]
395N/A
742N/Adef add_0(scfg, request):
2339N/A if scfg.is_read_only():
2339N/A request.send_error(403, "Read-only server")
2339N/A return
2339N/A
2339N/A m = re.match("^/add/\d+/([^/]*)/(.*)", request.path)
2339N/A trans_id = m.group(1)
742N/A type = m.group(2)
742N/A
742N/A t = scfg.in_flight_trans[trans_id]
742N/A t.add_content(request, type)
742N/A
742N/Aif "PKG_REPO" in os.environ:
742N/A scfg = config.SvrConfig(os.environ["PKG_REPO"], "pkg.sun.com")
742N/Aelse:
742N/A scfg = config.SvrConfig("/var/pkg/repo", "pkg.sun.com")
742N/A
2310N/Adef set_ops():
1902N/A vops = {}
1099N/A for name in globals():
2390N/A m = re.match("(.*)_(\d+)", name)
2335N/A
2338N/A if not m:
2338N/A continue
2310N/A
2046N/A op = m.group(1)
2223N/A ver = m.group(2)
2046N/A
2046N/A if op in vops:
2310N/A vops[op].append(ver)
2310N/A else:
2310N/A vops[op] = [ ver ]
2310N/A
2310N/A return vops
2310N/A
2310N/Aclass pkgHandler(BaseHTTPServer.BaseHTTPRequestHandler):
2310N/A
2345N/A def do_GET(self):
2345N/A reqarr = self.path.lstrip("/").split("/")
1498N/A operation = reqarr[0]
2339N/A
2339N/A if operation not in vops:
2339N/A if face.match(self):
691N/A face.respond(scfg, self)
691N/A else:
691N/A face.unknown(scfg, self)
395N/A return
395N/A
395N/A # Make sure that we have a integer protocol version
395N/A try:
395N/A version = int(reqarr[1])
290N/A except IndexError, e:
395N/A self.send_response(400)
395N/A self.send_header("Content-type", "text/plain")
591N/A self.end_headers()
591N/A self.wfile.write("Missing version\n")
591N/A return
1505N/A except ValueError, e:
1505N/A self.send_response(400)
1505N/A self.send_header("Content-type", "text/plain")
1505N/A self.end_headers()
1632N/A self.wfile.write("Non-integer version\n")
1632N/A return
1632N/A
1632N/A op_method = "%s_%s" % (operation, version)
2339N/A if op_method not in globals():
2339N/A # If we get here, we know that 'operation' is supported.
2339N/A # Assume 'version' is not supported for that operation.
2339N/A self.send_response(404, "Version not supported")
2339N/A self.send_header("Content-type", "text/plain")
2339N/A self.end_headers()
2339N/A
2339N/A vns = "Version '%s' not supported for operation '%s'\n"
2339N/A self.wfile.write(vns % (version, operation))
2339N/A return
2339N/A
2339N/A op_call = op_method + "(scfg, self)"
2339N/A
2339N/A try:
2339N/A exec op_call
2339N/A except:
2364N/A traceback.print_exc()
2339N/A self.send_response(500)
2339N/A
2339N/A def do_POST(self):
2339N/A self.do_GET()
2339N/A
2339N/A def do_PUT(self):
2339N/A self.send_response(200)
2339N/A self.send_header('Content-type', 'text/plain')
2339N/A self.end_headers()
2339N/A self.wfile.write('''PUT URI %s ; headers %s''' %
2339N/A (self.path, self.headers))
2339N/A
2339N/A def do_DELETE(self):
2339N/A self.send_response(200)
2339N/A self.send_header('Content-type', 'text/plain')
2339N/A self.end_headers()
2339N/A self.wfile.write('''URI %s ; headers %s''' %
2339N/A (self.path, self.headers))
2339N/A
2339N/Aclass ThreadingHTTPServer(SocketServer.ThreadingMixIn,
2364N/A BaseHTTPServer.HTTPServer):
2364N/A pass
2364N/A
2364N/Avops = {}
2364N/A
2364N/Aif __name__ == "__main__":
2364N/A port = 10000
2364N/A
2364N/A try:
2364N/A opts, pargs = getopt.getopt(sys.argv[1:], "d:np:", ["readonly"])
2364N/A for opt, arg in opts:
2364N/A if opt == "-n":
2364N/A sys.exit(0)
2339N/A elif opt == "-d":
395N/A scfg.set_repo_root(arg)
395N/A elif opt == "-p":
290N/A port = int(arg)
290N/A elif opt == "--readonly":
2339N/A scfg.set_read_only()
2339N/A except getopt.GetoptError, e:
290N/A print "pkg.depotd: unknown option '%s'" % e.opt
290N/A usage()
290N/A
290N/A scfg.init_dirs()
290N/A scfg.acquire_in_flight()
290N/A scfg.acquire_catalog()
290N/A
290N/A vops = set_ops()
290N/A
290N/A server = ThreadingHTTPServer(('', port), pkgHandler)
395N/A server.serve_forever()
395N/A