depot.py revision 814
409N/A#!/usr/bin/python2.4
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#
814N/A# Copyright 2009 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
382N/A# The default authority for the depot.
382N/AAUTH_DEFAULT = "opensolaris.org"
382N/A# The default repository path.
382N/AREPO_PATH_DEFAULT = "/var/pkg/repo"
589N/A# The default path for static and other web content.
589N/ACONTENT_PATH_DEFAULT = "/usr/share/lib/pkg"
812N/A# The default port(s) to serve data from.
382N/APORT_DEFAULT = 80
812N/ASSL_PORT_DEFAULT = 443
382N/A# The minimum number of threads allowed.
382N/ATHREADS_MIN = 1
382N/A# The default number of threads to start.
382N/ATHREADS_DEFAULT = 10
382N/A# The maximum number of threads that can be started.
382N/ATHREADS_MAX = 100
382N/A# The default server socket timeout in seconds. We want this to be longer than
382N/A# the normal default of 10 seconds to accommodate clients with poor quality
382N/A# connections.
382N/ASOCKET_TIMEOUT_DEFAULT = 60
382N/A# Whether modify operations should be allowed.
382N/AREADONLY_DEFAULT = False
382N/A# Whether the repository catalog should be rebuilt on startup.
382N/AREBUILD_DEFAULT = False
429N/A# Whether the indexes should be rebuilt
429N/AREINDEX_DEFAULT = False
461N/A# Not in mirror mode by default
461N/AMIRROR_DEFAULT = False
382N/A
26N/Aimport getopt
689N/Aimport gettext
689N/Aimport locale
466N/Aimport logging
0N/Aimport os
468N/Aimport os.path
812N/Aimport OpenSSL.crypto as crypto
812N/Aimport OpenSSL.SSL as ssl
812N/Aimport pkg.portable.util as os_util
812N/Aimport subprocess
52N/Aimport sys
812N/Aimport tempfile
451N/Aimport urlparse
0N/A
382N/Atry:
382N/A import cherrypy
382N/A version = cherrypy.__version__.split('.')
452N/A if map(int, version) < [3, 1, 0]:
382N/A raise ImportError
452N/A elif map(int, version) >= [3, 2, 0]:
382N/A raise ImportError
382N/Aexcept ImportError:
751N/A print >> sys.stderr, """cherrypy 3.1.0 or greater (but less than """ \
751N/A """3.2.0) is required to use this program."""
382N/A sys.exit(2)
22N/A
814N/Aimport pkg.catalog as catalog
812N/Afrom pkg.misc import port_available, msg, emsg, setlocale
812N/Aimport pkg.search_errors as search_errors
26N/Aimport pkg.server.config as config
382N/Aimport pkg.server.depot as depot
382N/Aimport pkg.server.repository as repo
428N/Aimport pkg.server.repositoryconfig as rc
466N/A
466N/Aclass LogSink(object):
466N/A """This is a dummy object that we can use to discard log entries
466N/A without relying on non-portable interfaces such as /dev/null."""
23N/A
466N/A def write(self, *args, **kwargs):
466N/A """Discard the bits."""
466N/A pass
466N/A
466N/A def flush(self, *args, **kwargs):
466N/A """Discard the bits."""
466N/A pass
466N/A
466N/Adef usage(text):
466N/A if text:
466N/A emsg(text)
466N/A
26N/A print """\
589N/AUsage: /usr/lib/pkg.depotd [-d repo_dir] [-p port] [-s threads]
812N/A [-t socket_timeout] [--cfg-file] [--content-root] [--log-access dest]
589N/A [--log-errors dest] [--mirror] [--proxy-base url] [--readonly]
812N/A [--rebuild] [--ssl-cert-file] [--ssl-dialog] [--ssl-key-file]
382N/A
812N/A --cfg-file The pathname of the file from which to read and to
812N/A write configuration information.
589N/A --content-root The file system path to the directory containing the
589N/A the static and other web content used by the depot's
589N/A browser user interface. The default value is
589N/A '/usr/share/lib/pkg'.
466N/A --log-access The destination for any access related information
466N/A logged by the depot process. Possible values are:
466N/A stderr, stdout, none, or an absolute pathname. The
466N/A default value is stdout if stdout is a tty; otherwise
466N/A the default value is none.
466N/A --log-errors The destination for any errors or other information
466N/A logged by the depot process. Possible values are:
466N/A stderr, stdout, none, or an absolute pathname. The
466N/A default value is stderr.
589N/A --mirror Package mirror mode; publishing and metadata operations
589N/A disallowed. Cannot be used with --readonly or
589N/A --rebuild.
589N/A --proxy-base The url to use as the base for generating internal
589N/A redirects and content.
589N/A --readonly Read-only operation; modifying operations disallowed.
589N/A Cannot be used with --mirror or --rebuild.
589N/A --rebuild Re-build the catalog from pkgs in depot. Cannot be
589N/A used with --mirror or --readonly.
812N/A --ssl-cert-file The absolute pathname to a PEM-encoded Certificate file.
812N/A This option must be used with --ssl-key-file. Usage of
812N/A this option will cause the depot to only respond to SSL
812N/A requests on the provided port.
812N/A --ssl-dialog Specifies what method should be used to obtain the
812N/A passphrase needed to decrypt the file specified by
812N/A --ssl-key-file. Supported values are: builtin,
812N/A exec:/path/to/program, or smf:fmri. The default value
812N/A is builtin.
812N/A --ssl-key-file The absolute pathname to a PEM-encoded Private Key file.
812N/A This option must be used with --ssl-cert-file. Usage of
812N/A this option will cause the depot to only respond to SSL
812N/A requests on the provided port.
26N/A"""
135N/A sys.exit(2)
14N/A
382N/Aclass OptionError(Exception):
429N/A """Option exception. """
14N/A
404N/A def __init__(self, *args):
404N/A Exception.__init__(self, *args)
30N/A
382N/Aif __name__ == "__main__":
30N/A
791N/A setlocale(locale.LC_ALL, "")
689N/A gettext.install("pkg", "/usr/share/locale")
689N/A
382N/A port = PORT_DEFAULT
812N/A port_provided = False
382N/A threads = THREADS_DEFAULT
382N/A socket_timeout = SOCKET_TIMEOUT_DEFAULT
382N/A readonly = READONLY_DEFAULT
382N/A rebuild = REBUILD_DEFAULT
429N/A reindex = REINDEX_DEFAULT
451N/A proxy_base = None
461N/A mirror = MIRROR_DEFAULT
797N/A repo_config_file = None
812N/A ssl_cert_file = None
812N/A ssl_key_file = None
812N/A ssl_dialog = "builtin"
258N/A
382N/A if "PKG_REPO" in os.environ:
382N/A repo_path = os.environ["PKG_REPO"]
382N/A else:
382N/A repo_path = REPO_PATH_DEFAULT
30N/A
589N/A try:
589N/A content_root = os.environ["PKG_DEPOT_CONTENT"]
589N/A except KeyError:
589N/A try:
589N/A content_root = os.path.join(os.environ['PKG_HOME'],
589N/A 'share/lib/pkg')
589N/A except KeyError:
589N/A content_root = CONTENT_PATH_DEFAULT
589N/A
466N/A # By default, if the destination for a particular log type is not
466N/A # specified, this is where we will send the output.
466N/A log_routes = {
466N/A "access": "none",
466N/A "errors": "stderr"
466N/A }
466N/A log_opts = ["--log-%s" % log_type for log_type in log_routes]
466N/A
466N/A # If stdout is a tty, then send access output there by default instead
466N/A # of discarding it.
466N/A if os.isatty(sys.stdout.fileno()):
466N/A log_routes["access"] = "stdout"
466N/A
466N/A opt = None
54N/A try:
812N/A long_opts = ["cfg-file", "content-root=", "mirror",
812N/A "proxy-base=", "readonly", "rebuild", "refresh-index",
812N/A "ssl-cert-file=", "ssl-dialog=", "ssl-key-file="]
466N/A for opt in log_opts:
466N/A long_opts.append("%s=" % opt.lstrip('--'))
382N/A opts, pargs = getopt.getopt(sys.argv[1:], "d:np:s:t:",
466N/A long_opts)
135N/A for opt, arg in opts:
135N/A if opt == "-n":
135N/A sys.exit(0)
135N/A elif opt == "-d":
382N/A repo_path = arg
135N/A elif opt == "-p":
135N/A port = int(arg)
812N/A port_provided = True
382N/A elif opt == "-s":
382N/A threads = int(arg)
382N/A if threads < THREADS_MIN:
382N/A raise OptionError, \
382N/A "minimum value is %d" % THREADS_MIN
382N/A if threads > THREADS_MAX:
382N/A raise OptionError, \
382N/A "maximum value is %d" % THREADS_MAX
382N/A elif opt == "-t":
382N/A socket_timeout = int(arg)
812N/A elif opt == "--cfg-file":
812N/A repo_config_file = os.path.abspath(arg)
589N/A elif opt == "--content-root":
589N/A if arg == "":
589N/A raise OptionError, "You must specify " \
589N/A "a directory path."
589N/A content_root = arg
466N/A elif opt in log_opts:
466N/A if arg is None or arg == "":
466N/A raise OptionError, \
466N/A "You must specify a log " \
466N/A "destination."
466N/A log_routes[opt.lstrip("--log-")] = arg
589N/A elif opt == "--mirror":
589N/A mirror = True
589N/A elif opt == "--proxy-base":
589N/A # Attempt to decompose the url provided into
589N/A # its base parts. This is done so we can
589N/A # remove any scheme information since we
589N/A # don't need it.
589N/A scheme, netloc, path, params, query, \
589N/A fragment = urlparse.urlparse(arg,
765N/A "http", allow_fragments=0)
765N/A
765N/A if not netloc:
765N/A raise OptionError, "Unable to " \
765N/A "determine the hostname from " \
765N/A "the provided URL; please use a " \
765N/A "fully qualified URL."
589N/A
765N/A scheme = scheme.lower()
765N/A if scheme not in ("http", "https"):
765N/A raise OptionError, "Invalid URL; http " \
765N/A "and https are the only supported " \
765N/A "schemes."
765N/A
765N/A # Rebuild the url with the sanitized components.
765N/A proxy_base = urlparse.urlunparse((scheme, netloc,
589N/A path, params, query, fragment)
765N/A )
135N/A elif opt == "--readonly":
382N/A readonly = True
157N/A elif opt == "--rebuild":
382N/A rebuild = True
429N/A elif opt == "--refresh-index":
429N/A # Note: This argument is for internal use
429N/A # only. It's used when pkg.depotd is reexecing
429N/A # itself and needs to know that's the case.
429N/A # This flag is purposefully omitted in usage.
429N/A # The supported way to forcefully reindex is to
429N/A # kill any pkg.depot using that directory,
429N/A # remove the index directory, and restart the
429N/A # pkg.depot process. The index will be rebuilt
429N/A # automatically on startup.
429N/A reindex = True
812N/A elif opt == "--ssl-cert-file":
812N/A if arg == "none":
812N/A continue
812N/A
812N/A ssl_cert_file = arg
812N/A if not os.path.isabs(ssl_cert_file):
812N/A raise OptionError, "The path to " \
812N/A "the Certificate file must be " \
812N/A "absolute."
812N/A elif not os.path.exists(ssl_cert_file):
812N/A raise OptionError, "The specified " \
812N/A "file does not exist."
812N/A elif not os.path.isfile(ssl_cert_file):
812N/A raise OptionError, "The specified " \
812N/A "pathname is not a file."
812N/A elif opt == "--ssl-key-file":
812N/A if arg == "none":
812N/A continue
812N/A
812N/A ssl_key_file = arg
812N/A if not os.path.isabs(ssl_key_file):
812N/A raise OptionError, "The path to " \
812N/A "the Private Key file must be " \
812N/A "absolute."
812N/A elif not os.path.exists(ssl_key_file):
812N/A raise OptionError, "The specified " \
812N/A "file does not exist."
812N/A elif not os.path.isfile(ssl_key_file):
812N/A raise OptionError, "The specified " \
812N/A "pathname is not a file."
812N/A elif opt == "--ssl-dialog":
812N/A if arg != "builtin" and not \
812N/A arg.startswith("exec:/") and not \
812N/A arg.startswith("smf:"):
812N/A raise OptionError, "Invalid value " \
812N/A "specified. Expected: builtin, " \
812N/A "exec:/path/to/program, or " \
812N/A "smf:fmri."
812N/A
812N/A f = arg
812N/A if f.startswith("exec:"):
812N/A if os_util.get_canonical_os_type() != \
812N/A "unix":
812N/A # Don't allow a somewhat insecure
812N/A # authentication method on some
812N/A # platforms.
812N/A raise OptionError, "exec is not " \
812N/A "a supported dialog type for " \
812N/A "this operating system."
812N/A
812N/A f = os.path.abspath(f.split(
812N/A "exec:")[1])
812N/A
812N/A if not os.path.isfile(f):
812N/A raise OptionError, "Invalid " \
812N/A "file path specified for " \
812N/A "exec."
812N/A
812N/A f = "exec:%s" % f
812N/A
812N/A ssl_dialog = f
135N/A except getopt.GetoptError, e:
466N/A usage("pkg.depotd: %s" % e.msg)
382N/A except OptionError, e:
466N/A usage("pkg.depotd: option: %s -- %s" % (opt, e))
382N/A except (ArithmeticError, ValueError):
466N/A usage("pkg.depotd: illegal option value: %s specified " \
466N/A "for option: %s" % (arg, opt))
451N/A
445N/A if rebuild and reindex:
466N/A usage("--refresh-index cannot be used with --rebuild")
461N/A if rebuild and (readonly or mirror):
466N/A usage("--readonly and --mirror cannot be used with --rebuild")
461N/A if reindex and (readonly or mirror):
466N/A usage("--readonly and --mirror cannot be used with " \
466N/A "--refresh-index")
451N/A
812N/A if (ssl_cert_file and not ssl_key_file) or (ssl_key_file and not
812N/A ssl_cert_file):
812N/A usage("The --ssl-cert-file and --ssl-key-file options must "
812N/A "must both be provided when using either option.")
812N/A elif ssl_cert_file and ssl_key_file and not port_provided:
812N/A # If they didn't already specify a particular port, use the
812N/A # default SSL port instead.
812N/A port = SSL_PORT_DEFAULT
812N/A
429N/A # If the program is going to reindex, the port is irrelevant since
429N/A # the program will not bind to a port.
429N/A if not reindex:
429N/A available, msg = port_available(None, port)
429N/A if not available:
429N/A print "pkg.depotd: unable to bind to the specified " \
429N/A "port: %d. Reason: %s" % (port, msg)
429N/A sys.exit(1)
612N/A else:
612N/A # Not applicable for reindexing operations.
612N/A content_root = None
386N/A
589N/A scfg = config.SvrConfig(repo_path, content_root, AUTH_DEFAULT)
382N/A
382N/A if rebuild:
382N/A scfg.destroy_catalog()
382N/A
382N/A if readonly:
382N/A scfg.set_read_only()
382N/A
461N/A if mirror:
461N/A scfg.set_mirror()
461N/A
382N/A try:
382N/A scfg.init_dirs()
589N/A except (RuntimeError, EnvironmentError), e:
382N/A print "pkg.depotd: an error occurred while trying to " \
382N/A "initialize the depot repository directory " \
382N/A "structures:\n%s" % e
382N/A sys.exit(1)
382N/A
812N/A key_data = None
812N/A if not reindex and ssl_cert_file and ssl_key_file and \
812N/A ssl_dialog != "builtin":
812N/A cmdline = None
812N/A def get_ssl_passphrase(*ignored):
812N/A p = None
812N/A try:
812N/A p = subprocess.Popen(cmdline, shell=True,
812N/A stdout=subprocess.PIPE,
812N/A stderr=None)
812N/A p.wait()
812N/A except Exception, e:
812N/A print "pkg.depotd: an error occurred while " \
812N/A "executing [%s]; unable to obtain the " \
812N/A "passphrase needed to decrypt the SSL" \
812N/A "private key file: %s" (cmd, e)
812N/A sys.exit(1)
812N/A return p.stdout.read().strip("\n")
812N/A
812N/A if ssl_dialog.startswith("exec:"):
812N/A cmdline = "%s %s %d" % (ssl_dialog.split("exec:")[1],
812N/A "''", port)
812N/A elif ssl_dialog.startswith("smf:"):
812N/A cmdline = "/usr/bin/svcprop -p " \
812N/A "pkg_secure/ssl_key_passphrase %s" % (
812N/A ssl_dialog.split("smf:")[1])
812N/A
812N/A # The key file requires decryption, but the user has requested
812N/A # exec-based authentication, so it will have to be decoded first
812N/A # to an un-named temporary file.
812N/A try:
812N/A key_file = file(ssl_key_file, "rb")
812N/A pkey = crypto.load_privatekey(crypto.FILETYPE_PEM,
812N/A key_file.read(), get_ssl_passphrase)
812N/A
812N/A key_data = tempfile.TemporaryFile()
812N/A key_data.write(crypto.dump_privatekey(
812N/A crypto.FILETYPE_PEM, pkey))
812N/A key_data.seek(0)
812N/A except EnvironmentError, e:
812N/A print "pkg.depotd: unable to read the SSL private " \
812N/A "key file: %s" % e
812N/A sys.exit(1)
812N/A except crypto.Error, e:
812N/A print "pkg.depotd: authentication or cryptography " \
812N/A "failure while attempting to decode\nthe SSL " \
812N/A "private key file: %s" % e
812N/A sys.exit(1)
812N/A else:
812N/A # Redirect the server to the decrypted key file.
812N/A ssl_key_file = "/dev/fd/%d" % key_data.fileno()
812N/A
452N/A # Setup our global configuration.
466N/A gconf = {
382N/A "environment": "production",
382N/A "checker.on": True,
466N/A "log.screen": False,
452N/A "server.socket_host": "0.0.0.0",
382N/A "server.socket_port": port,
382N/A "server.thread_pool": threads,
466N/A "server.socket_timeout": socket_timeout,
775N/A "server.shutdown_timeout": 0,
742N/A "tools.log_headers.on": True,
812N/A "tools.encode.on": True,
812N/A "server.ssl_certificate": ssl_cert_file,
812N/A "server.ssl_private_key": ssl_key_file
466N/A }
466N/A
466N/A log_type_map = {
466N/A "errors": {
466N/A "param": "log.error_file",
466N/A "attr": "error_log"
466N/A },
466N/A "access": {
466N/A "param": "log.access_file",
466N/A "attr": "access_log"
466N/A }
466N/A }
466N/A
466N/A for log_type in log_type_map:
466N/A dest = log_routes[log_type]
466N/A if dest in ("stdout", "stderr", "none"):
466N/A if dest == "none":
466N/A h = logging.StreamHandler(LogSink())
466N/A else:
466N/A h = logging.StreamHandler(eval("sys.%s" % \
466N/A dest))
466N/A
466N/A h.setLevel(logging.DEBUG)
466N/A h.setFormatter(cherrypy._cplogging.logfmt)
466N/A log_obj = eval("cherrypy.log.%s" % \
466N/A log_type_map[log_type]["attr"])
466N/A log_obj.addHandler(h)
466N/A # Since we've replaced cherrypy's log handler with our
466N/A # own, we don't want the output directed to a file.
466N/A dest = ""
466N/A
466N/A gconf[log_type_map[log_type]["param"]] = dest
382N/A
612N/A cherrypy.config.update(gconf)
612N/A
612N/A # Now that our logging, etc. has been setup, it's safe to perform any
612N/A # remaining preparation.
612N/A if reindex:
612N/A scfg.acquire_catalog(rebuild=False)
617N/A try:
617N/A scfg.catalog.run_update_index()
617N/A except search_errors.IndexingException, e:
617N/A cherrypy.log(str(e), "INDEX")
617N/A sys.exit(1)
612N/A sys.exit(0)
612N/A
451N/A # Now build our site configuration.
382N/A conf = {
452N/A "/": {
452N/A # We have to override cherrypy's default response_class so that
452N/A # we have access to the write() callable to stream data
452N/A # directly to the client.
452N/A "wsgi.response_class": depot.DepotResponse,
452N/A },
382N/A "/robots.txt": {
382N/A "tools.staticfile.on": True,
742N/A "tools.staticfile.filename": os.path.join(scfg.web_root,
382N/A "robots.txt")
382N/A },
382N/A }
145N/A
451N/A if proxy_base:
451N/A # This changes the base URL for our server, and is primarily
451N/A # intended to allow our depot process to operate behind Apache
451N/A # or some other webserver process.
451N/A #
451N/A # Visit the following URL for more information:
451N/A # http://cherrypy.org/wiki/BuiltinTools#tools.proxy
451N/A proxy_conf = {
451N/A "tools.proxy.on": True,
451N/A "tools.proxy.local": "",
451N/A "tools.proxy.base": proxy_base
451N/A }
451N/A
451N/A # Now merge or add our proxy configuration information into the
451N/A # existing configuration.
451N/A for entry in proxy_conf:
451N/A conf["/"][entry] = proxy_conf[entry]
451N/A
466N/A scfg.acquire_in_flight()
814N/A try:
814N/A scfg.acquire_catalog()
814N/A except catalog.CatalogPermissionsException, e:
814N/A emsg("pkg.depotd: %s" % e)
814N/A sys.exit(1)
466N/A
217N/A try:
797N/A root = cherrypy.Application(repo.Repository(scfg,
797N/A repo_config_file))
466N/A except rc.InvalidAttributeValueError, e:
466N/A emsg("pkg.depotd: repository.conf error: %s" % e)
466N/A sys.exit(1)
466N/A
466N/A try:
466N/A cherrypy.quickstart(root, config=conf)
812N/A except Exception, e:
812N/A emsg("pkg.depotd: unknown error starting depot server, " \
812N/A "illegal option value specified?")
812N/A sys.exit(1)
217N/A