depot.py revision 114
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
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
34N/Aimport pkg.package as package
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 """\
26N/AUsage: /usr/lib/pkg.depotd [-n]
26N/A"""
14N/A
50N/Adef catalog_get(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()
26N/A request.wfile.write("%s" % scfg.catalog)
26N/A
50N/Adef manifest_get(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.
30N/A m = re.match("^/manifest/(.*)", request.path)
30N/A pfmri = urllib.unquote(m.group(1))
30N/A
50N/A f = fmri.PkgFmri(pfmri, None)
45N/A
30N/A # Open manifest and send.
50N/A file = open("%s/%s" % (scfg.pkg_root, f.get_dir_path()), "r")
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
50N/Adef file_get_single(scfg, request):
30N/A """The request is the SHA-1 hash name for the file."""
30N/A scfg.inc_file()
30N/A
30N/A m = re.match("^/file/(.*)", request.path)
30N/A fhash = m.group(1)
30N/A
54N/A try:
54N/A file = open(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:
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
0N/Adef trans_open(scfg, request):
22N/A # XXX Authentication will be handled by virtue of possessing a signed
22N/A # certificate (or a more elaborate system).
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
0N/Adef trans_close(scfg, request):
0N/A # Pull transaction ID from headers.
0N/A m = re.match("^/close/(.*)", request.path)
0N/A trans_id = m.group(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
22N/Adef trans_abandon(scfg, request):
22N/A # Pull transaction ID from headers.
22N/A m = re.match("^/abandon/(.*)", request.path)
22N/A trans_id = m.group(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
0N/Adef trans_add(scfg, request):
0N/A m = re.match("^/add/([^/]*)/(.*)", request.path)
0N/A trans_id = m.group(1)
0N/A type = m.group(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
0N/Aclass pkgHandler(BaseHTTPServer.BaseHTTPRequestHandler):
0N/A
0N/A def do_GET(self):
26N/A # Client APIs
0N/A if re.match("^/catalog$", self.path):
50N/A catalog_get(scfg, self)
26N/A elif re.match("^/manifest/.*$", self.path):
50N/A manifest_get(scfg, self)
30N/A elif re.match("^/file/.*$", self.path):
50N/A file_get_single(scfg, self)
26N/A
26N/A # Publisher APIs
0N/A elif re.match("^/open/(.*)$", self.path):
0N/A trans_open(scfg, self)
0N/A elif re.match("^/close/(.*)$", self.path):
0N/A trans_close(scfg, self)
22N/A elif re.match("^/abandon/(.*)$", self.path):
22N/A trans_abandon(scfg, self)
0N/A elif re.match("^/add/(.*)$", self.path):
0N/A trans_add(scfg, self)
26N/A
26N/A # Informational APIs
114N/A elif face.match(self):
114N/A face.respond(scfg, self)
0N/A else:
114N/A face.unknown(scfg, self)
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_POST(self):
0N/A if re.match("^/add/(.*)$", self.path):
0N/A trans_add(scfg, self)
0N/A else:
0N/A self.send_response(404)
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
22N/Aif __name__ == "__main__":
22N/A scfg.init_dirs()
26N/A scfg.acquire_in_flight()
26N/A scfg.acquire_catalog()
26N/A
82N/A port = 10000
82N/A
26N/A try:
82N/A opts, pargs = getopt.getopt(sys.argv[1:], "np:")
26N/A for opt, arg in opts:
26N/A if opt == "-n":
26N/A sys.exit(0)
82N/A elif opt == "-p":
82N/A port = int(arg)
52N/A except getopt.GetoptError, e:
52N/A print "pkg.depotd: unknown option '%s'" % e.opt
26N/A usage()
26N/A
82N/A server = ThreadingHTTPServer(('', port), pkgHandler)
22N/A server.serve_forever()