depot.py revision 258
3177N/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#
3158N/A# Copyright 2007 Sun Microsystems, Inc. All rights reserved.
395N/A# Use is subject to license terms.
290N/A#
3143N/A
883N/A# pkg.depotd - package repository daemon
454N/A
290N/A# XXX The prototype pkg.depotd combines both the version management server that
448N/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
290N/A# can be served as a typical HTTP/HTTPS session. Thus, pkg.depotd will reduce
383N/A# to a special purpose HTTP/HTTPS server explicitly for the version management
290N/A# operations, and must manipulate the various state files--catalogs, in
395N/A# particular--such that the pkg(1) pull client can operately accurately with
290N/A# only a basic HTTP/HTTPS server in place.
395N/A
849N/A# XXX We should support simple "last-modified" operations via HEAD queries.
1516N/A
2508N/A# XXX Although we pushed the evaluation of next-version, etc. to the pull
2826N/A# client, we should probably provide a query API to do same on the server, for
290N/A# dumb clients (like a notification service).
2535N/A
2698N/Aimport BaseHTTPServer
290N/Aimport SocketServer
290N/Aimport socket
2535N/Aimport errno
2561N/Aimport getopt
290N/Aimport os
2508N/Aimport re
383N/Aimport sys
290N/Aimport urllib
290N/Aimport tarfile
2339N/Aimport cgi
2535N/Aimport traceback
290N/A
290N/Aimport pkg.fmri as fmri
2535N/Aimport pkg.misc as misc
2535N/Aimport pkg.catalog as catalog
290N/A
290N/Aimport pkg.server.face as face
2508N/Aimport pkg.server.config as config
2508N/Aimport pkg.server.transaction as trans
290N/A
1660N/Adef usage():
1660N/A print """\
1660N/AUsage: /usr/lib/pkg.depotd [--readonly] [--rebuild] [-d repo_dir] [-p port]
1660N/A --readonly Read-only operation; modifying operations disallowed
1660N/A --rebuild Re-build the catalog from pkgs in depot
1660N/A"""
1660N/A sys.exit(2)
1660N/A
1660N/Adef versions_0(scfg, request):
1660N/A request.send_response(200)
1660N/A request.send_header('Content-type', 'text/plain')
1660N/A request.end_headers()
1660N/A versions = "\n".join(
1660N/A "%s %s" % (op, " ".join(vers))
1660N/A for op, vers in vops.iteritems()
1660N/A ) + "\n"
1660N/A request.wfile.write(versions)
1660N/A
448N/Adef search_0(scfg, request):
448N/A try:
2828N/A token = urllib.unquote(request.path.split("/", 3)[3])
2828N/A except IndexError:
2828N/A request.send_response(400)
534N/A return
534N/A
534N/A if not token:
534N/A request.send_response(400)
534N/A return
534N/A
534N/A if not scfg.search_available():
290N/A request.send_response(503, "Search temporarily unavailable")
290N/A return
954N/A
954N/A try:
954N/A res = scfg.catalog.search(token)
954N/A except KeyError:
534N/A request.send_response(404)
1099N/A return
290N/A
3117N/A request.send_response(200)
3117N/A request.send_header("Content-type", "text/plain")
3183N/A request.end_headers()
3117N/A for l in res:
290N/A request.wfile.write("%s %s\n" % (l[0], l[1]))
290N/A
290N/Adef catalog_0(scfg, request):
661N/A scfg.inc_catalog()
2867N/A
290N/A # Try to guard against a non-existent catalog. The catalog open will
2494N/A # raise an exception, and only the attributes will have been sent. But
2494N/A # because we've sent data already (never mind the response header), we
2494N/A # can't raise an exception here, or a 500 header will get sent as well.
2516N/A try:
2516N/A scfg.updatelog.send(request)
2516N/A except:
2516N/A request.log_error("Internal failure:\n%s",
2516N/A traceback.format_exc())
2516N/A
2516N/Adef manifest_0(scfg, request):
290N/A """The request is an encoded pkg FMRI. If the version is specified
3185N/A incompletely, we return an error, as the client is expected to form
2523N/A correct requests, based on its interpretation of the catalog and its
3138N/A image policies."""
2390N/A
1498N/A scfg.inc_manifest()
1498N/A
2867N/A # Parse request into FMRI component and decode.
2310N/A pfmri = urllib.unquote(request.path.split("/", 3)[-1])
2310N/A
2310N/A f = fmri.PkgFmri(pfmri, None)
2852N/A
2852N/A # Open manifest and send.
2852N/A try:
2852N/A file = open("%s/%s" % (scfg.pkg_root, f.get_dir_path()), "r")
2535N/A except IOError, e:
2867N/A if e.errno == errno.ENOENT:
2867N/A request.send_response(404)
2310N/A else:
290N/A request.log_error("Internal failure:\n%s",
1674N/A traceback.format_exc())
1674N/A request.send_response(500)
2262N/A return
1674N/A data = file.read()
395N/A
430N/A request.send_response(200)
395N/A request.send_header('Content-type', 'text/plain')
1544N/A request.end_headers()
1968N/A request.wfile.write(data)
1557N/A
1903N/Adef filelist_0(scfg, request):
2046N/A """Request data contains application/x-www-form-urlencoded entries
2240N/A with the requested filenames."""
1506N/A # If the sender doesn't specify the content length, reject this request.
2928N/A # Calling read() with no size specified will force the server to block
395N/A # until the client sends EOF, an undesireable situation
395N/A size = int(request.headers.getheader("Content-Length", "0"))
2026N/A if size == 0:
395N/A request.send_response(411)
395N/A return
395N/A
2310N/A rfile = request.rfile
2852N/A data_dict = cgi.parse_qs(rfile.read(size))
395N/A
661N/A scfg.inc_flist()
2867N/A
2867N/A request.send_response(200)
2867N/A request.send_header("Content-type", "application/data")
2867N/A request.end_headers()
2867N/A
2852N/A tar_stream = tarfile.open(mode = "w|", fileobj = request.wfile)
2310N/A
2867N/A for v in data_dict.values():
2867N/A filepath = os.path.normpath(os.path.join(
2867N/A scfg.file_root, misc.hash_file_name(v[0])))
661N/A
3185N/A tar_stream.add(filepath, v[0], False)
3185N/A scfg.inc_flist_files()
3185N/A
395N/A tar_stream.close()
849N/A
290N/Adef rename_0(scfg, request):
395N/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
1968N/A # until the client sends EOF, an undesireable situation
395N/A size = int(request.headers.getheader("Content-Length", "0"))
395N/A if size == 0:
395N/A request.send_response(411)
395N/A return
395N/A
395N/A rfile = request.rfile
395N/A rename_dict = cgi.parse_qs(rfile.read(size))
395N/A
395N/A try:
395N/A src_fmri = fmri.PkgFmri(rename_dict['Src-FMRI'][0], None)
395N/A except KeyError:
290N/A request.send_response(400, "No source FMRI present.")
290N/A return
395N/A except ValueError:
395N/A request.send_response(400, "Invalid source FMRI.")
1231N/A return
1557N/A
1903N/A try:
1557N/A dest_fmri = fmri.PkgFmri(rename_dict['Dest-FMRI'][0], None)
395N/A except KeyError:
395N/A request.send_response(400, "No destination FMRI present.")
395N/A return
395N/A except ValueError:
395N/A request.send_response(400, "Invalid destination FMRI.")
395N/A return
395N/A
395N/A try:
395N/A scfg.updatelog.rename_package(src_fmri.pkg_name,
395N/A str(src_fmri.version), dest_fmri.pkg_name,
3185N/A str(dest_fmri.version))
3185N/A except catalog.CatalogException, e:
3185N/A request.send_response(404, e.args)
395N/A return
290N/A except catalog.RenameException, e:
290N/A request.send_response(404, e.args)
430N/A return
395N/A
395N/A scfg.inc_renamed()
395N/A request.send_response(200)
395N/A
1302N/A
395N/Adef file_0(scfg, request):
395N/A """The request is the SHA-1 hash name for the file."""
290N/A scfg.inc_file()
3139N/A
3139N/A fhash = request.path.split("/", 3)[-1]
395N/A
3139N/A try:
3139N/A file = open(os.path.normpath(os.path.join(
3139N/A scfg.file_root, misc.hash_file_name(fhash))))
3139N/A except IOError, e:
3139N/A if e.errno == errno.ENOENT:
3139N/A request.send_response(404)
3139N/A else:
3139N/A request.log_error("Internal failure:\n%s",
3139N/A traceback.format_exc())
3139N/A request.send_response(500)
3139N/A return
3139N/A
3139N/A data = file.read()
3139N/A
3139N/A request.send_response(200)
3139N/A request.send_header("Content-type", "application/data")
395N/A request.end_headers()
3139N/A request.wfile.write(data)
3139N/A
3139N/Adef open_0(scfg, request):
3139N/A # XXX Authentication will be handled by virtue of possessing a signed
3139N/A # certificate (or a more elaborate system).
3139N/A if scfg.is_read_only():
3139N/A request.send_error(403, "Read-only server")
395N/A return
3139N/A
3139N/A t = trans.Transaction()
3139N/A
3139N/A ret = t.open(scfg, request)
3139N/A if ret == 200:
2516N/A scfg.in_flight_trans[t.get_basename()] = t
2516N/A
3139N/A request.send_response(200)
3139N/A request.send_header('Content-type', 'text/plain')
3139N/A request.send_header('Transaction-ID', t.get_basename())
3139N/A request.end_headers()
3139N/A elif ret == 400:
3139N/A request.send_response(400)
3139N/A else:
3139N/A request.send_response(500)
3139N/A
3139N/A
3139N/Adef close_0(scfg, request):
3139N/A if scfg.is_read_only():
3139N/A request.send_error(403, "Read-only server")
3139N/A return
3139N/A
2516N/A # Pull transaction ID from headers.
3139N/A trans_id = request.path.split("/", 3)[-1]
3139N/A
3139N/A try:
3139N/A t = scfg.in_flight_trans[trans_id]
3139N/A except KeyError:
3139N/A request.send_response(404, "Transaction ID not found")
2516N/A else:
3139N/A t.close(request)
3139N/A del scfg.in_flight_trans[trans_id]
3139N/A
3139N/Adef abandon_0(scfg, request):
3139N/A if scfg.is_read_only():
2516N/A request.send_error(403, "Read-only server")
2516N/A return
3139N/A
3139N/A # Pull transaction ID from headers.
3139N/A trans_id = request.path.split("/", 3)[-1]
3139N/A
3139N/A try:
3139N/A t = scfg.in_flight_trans[trans_id]
3139N/A except KeyError:
3139N/A request.send_response(404, "Transaction ID not found")
3139N/A else:
3139N/A t.abandon(request)
3139N/A del scfg.in_flight_trans[trans_id]
3139N/A
3139N/Adef add_0(scfg, request):
3139N/A if scfg.is_read_only():
3139N/A request.send_error(403, "Read-only server")
2516N/A return
3139N/A
3139N/A trans_id, type = request.path.split("/", 4)[-2:]
3139N/A
3139N/A try:
3139N/A t = scfg.in_flight_trans[trans_id]
3139N/A except KeyError:
2516N/A request.send_response(404, "Transaction ID not found")
3139N/A else:
3139N/A t.add_content(request, type)
3139N/A
3139N/Aif "PKG_REPO" in os.environ:
3139N/A scfg = config.SvrConfig(os.environ["PKG_REPO"], "pkg.sun.com")
2516N/Aelse:
395N/A scfg = config.SvrConfig("/var/pkg/repo", "pkg.sun.com")
395N/A
395N/Adef set_ops():
395N/A vops = {}
395N/A for name in globals():
2339N/A m = re.match("(.*)_(\d+)", name)
1191N/A
1452N/A if not m:
1231N/A continue
2046N/A
395N/A op = m.group(1)
395N/A ver = m.group(2)
424N/A
395N/A if op in vops:
742N/A vops[op].append(ver)
2339N/A else:
2339N/A vops[op] = [ ver ]
2693N/A
2690N/A return vops
2339N/A
3094N/Aclass pkgHandler(BaseHTTPServer.BaseHTTPRequestHandler):
2339N/A
2690N/A def address_string(self):
2690N/A host, port = self.client_address[:2]
2693N/A return host
2693N/A
2690N/A def do_GET(self):
2690N/A reqarr = self.path.lstrip("/").split("/")
2339N/A operation = reqarr[0]
2339N/A
742N/A if operation not in vops:
742N/A if face.match(self):
742N/A face.respond(scfg, self)
742N/A else:
742N/A face.unknown(scfg, self)
742N/A return
742N/A
742N/A # Make sure that we have a integer protocol version
742N/A try:
3136N/A version = int(reqarr[1])
2688N/A except IndexError, e:
2688N/A self.send_response(400)
2688N/A self.send_header("Content-type", "text/plain")
2688N/A self.end_headers()
2688N/A self.wfile.write("Missing version\n")
2688N/A return
2688N/A except ValueError, e:
2688N/A self.send_response(400)
2688N/A self.send_header("Content-type", "text/plain")
742N/A self.end_headers()
2310N/A self.wfile.write("Non-integer version\n")
2852N/A return
1902N/A
2867N/A op_method = "%s_%s" % (operation, version)
2867N/A if op_method not in globals():
1099N/A # If we get here, we know that 'operation' is supported.
2867N/A # Assume 'version' is not supported for that operation.
2338N/A self.send_response(404, "Version not supported")
2338N/A self.send_header("Content-type", "text/plain")
2310N/A self.end_headers()
2046N/A
2223N/A vns = "Version '%s' not supported for operation '%s'\n"
2046N/A self.wfile.write(vns % (version, operation))
2046N/A return
2523N/A
2523N/A op_call = op_method + "(scfg, self)"
2523N/A
2523N/A try:
2523N/A exec op_call
2523N/A except:
2310N/A self.log_error("Internal failure:\n%s",
2677N/A traceback.format_exc())
2310N/A # XXX op_call may already have spit some data out to the
2310N/A # client, in which case this response just corrupts that
2310N/A # datastream. I don't know of any way to tell whether
2310N/A # data has already been sent.
2310N/A self.send_response(500)
2310N/A
2858N/A def do_POST(self):
2310N/A self.do_GET()
2852N/A
2852N/A def do_PUT(self):
2852N/A self.send_response(200)
2852N/A self.send_header('Content-type', 'text/plain')
3136N/A self.end_headers()
2852N/A self.wfile.write('''PUT URI %s ; headers %s''' %
2852N/A (self.path, self.headers))
2852N/A
2852N/A def do_DELETE(self):
2858N/A self.send_response(200)
2852N/A self.send_header('Content-type', 'text/plain')
3138N/A self.end_headers()
3138N/A self.wfile.write('''URI %s ; headers %s''' %
2852N/A (self.path, self.headers))
2852N/A
2852N/Aclass ThreadingHTTPServer(SocketServer.ThreadingMixIn,
2508N/A BaseHTTPServer.HTTPServer):
2508N/A pass
2508N/A
2508N/Avops = {}
2867N/A
2535N/Aif __name__ == "__main__":
2535N/A port = 80
2535N/A unprivport = 10000
3053N/A
3053N/A if "PKG_DEPOT_CONTENT" in os.environ:
3053N/A face.set_content_root(os.environ["PKG_DEPOT_CONTENT"])
3026N/A
3026N/A try:
3026N/A opts, pargs = getopt.getopt(sys.argv[1:], "d:np:",
2339N/A ["readonly", "rebuild"])
2339N/A for opt, arg in opts:
2339N/A if opt == "-n":
691N/A sys.exit(0)
691N/A elif opt == "-d":
691N/A scfg.set_repo_root(arg)
395N/A elif opt == "-p":
395N/A port = int(arg)
395N/A elif opt == "--readonly":
395N/A scfg.set_read_only()
395N/A elif opt == "--rebuild":
290N/A scfg.destroy_catalog()
395N/A except getopt.GetoptError, e:
395N/A print "pkg.depotd: illegal option -- %s" % e.opt
591N/A usage()
591N/A
591N/A scfg.init_dirs()
2639N/A scfg.acquire_in_flight()
2639N/A scfg.acquire_catalog()
2639N/A
2639N/A vops = set_ops()
2639N/A
2639N/A try:
1505N/A server = ThreadingHTTPServer(('', port), pkgHandler)
2516N/A except socket.error, e:
1505N/A if e.args[0] != errno.EACCES:
1505N/A raise
1632N/A
1632N/A server = ThreadingHTTPServer(('', unprivport), pkgHandler)
1632N/A print >> sys.stderr, \
1632N/A "Insufficient privilege to bind to port %d." % port
2339N/A print >> sys.stderr, \
2339N/A "Bound server to port %d instead." % unprivport
2339N/A print >> sys.stderr, \
2339N/A "Use the -p option to pick another port, if desired."
2339N/A
2339N/A server.serve_forever()
2339N/A