depot.py revision 617
1516N/A#!/usr/bin/python2.4
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#
1902N/A# Copyright 2008 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/A# The default authority for the depot.
383N/AAUTH_DEFAULT = "opensolaris.org"
290N/A# The default repository path.
290N/AREPO_PATH_DEFAULT = "/var/pkg/repo"
290N/A# The default path for static and other web content.
290N/ACONTENT_PATH_DEFAULT = "/usr/share/lib/pkg"
290N/A# The default port to serve data from.
290N/APORT_DEFAULT = 80
290N/A# The minimum number of threads allowed.
290N/ATHREADS_MIN = 1
1660N/A# The default number of threads to start.
1660N/ATHREADS_DEFAULT = 10
1660N/A# The maximum number of threads that can be started.
1660N/ATHREADS_MAX = 100
1660N/A# The default server socket timeout in seconds. We want this to be longer than
1660N/A# the normal default of 10 seconds to accommodate clients with poor quality
1660N/A# connections.
1660N/ASOCKET_TIMEOUT_DEFAULT = 60
1660N/A# Whether modify operations should be allowed.
1660N/AREADONLY_DEFAULT = False
1660N/A# Whether the repository catalog should be rebuilt on startup.
1660N/AREBUILD_DEFAULT = False
1660N/A# Whether the indexes should be rebuilt
1660N/AREINDEX_DEFAULT = False
1660N/A# Not in mirror mode by default
1660N/AMIRROR_DEFAULT = False
1660N/A
1660N/Aimport getopt
465N/Aimport logging
465N/Aimport os
465N/Aimport os.path
1516N/Aimport sys
465N/Aimport urlparse
465N/A
465N/Atry:
1516N/A import cherrypy
465N/A version = cherrypy.__version__.split('.')
465N/A if map(int, version) < [3, 1, 0]:
465N/A raise ImportError
465N/A elif map(int, version) >= [3, 2, 0]:
465N/A raise ImportError
465N/Aexcept ImportError:
465N/A print """cherrypy 3.1.0 or greater (but less than 3.2.0) is """ \
1099N/A """required to use this program."""
465N/A sys.exit(2)
1513N/A
1513N/Aimport pkg.server.face as face
1514N/Aimport pkg.server.config as config
1513N/Aimport pkg.server.depot as depot
1513N/Aimport pkg.server.repository as repo
1513N/Aimport pkg.server.repositoryconfig as rc
1099N/Aimport pkg.search_errors as search_errors
1513N/Afrom pkg.misc import port_available, msg, emsg
708N/A
1391N/Aclass LogSink(object):
1391N/A """This is a dummy object that we can use to discard log entries
1391N/A without relying on non-portable interfaces such as /dev/null."""
1391N/A
1391N/A def write(self, *args, **kwargs):
1391N/A """Discard the bits."""
1391N/A pass
1391N/A
1391N/A def flush(self, *args, **kwargs):
1391N/A """Discard the bits."""
1391N/A pass
742N/A
742N/Adef usage(text):
742N/A if text:
742N/A emsg(text)
742N/A
742N/A print """\
1099N/AUsage: /usr/lib/pkg.depotd [-d repo_dir] [-p port] [-s threads]
742N/A [-t socket_timeout] [--content-root] [--log-access dest]
941N/A [--log-errors dest] [--mirror] [--proxy-base url] [--readonly]
941N/A [--rebuild]
941N/A
941N/A --content-root The file system path to the directory containing the
941N/A the static and other web content used by the depot's
941N/A browser user interface. The default value is
1099N/A '/usr/share/lib/pkg'.
941N/A --log-access The destination for any access related information
1191N/A logged by the depot process. Possible values are:
1513N/A stderr, stdout, none, or an absolute pathname. The
1191N/A default value is stdout if stdout is a tty; otherwise
1191N/A the default value is none.
1191N/A --log-errors The destination for any errors or other information
1191N/A logged by the depot process. Possible values are:
1191N/A stderr, stdout, none, or an absolute pathname. The
1660N/A default value is stderr.
1660N/A --mirror Package mirror mode; publishing and metadata operations
1660N/A disallowed. Cannot be used with --readonly or
290N/A --rebuild.
448N/A --proxy-base The url to use as the base for generating internal
448N/A redirects and content.
534N/A --readonly Read-only operation; modifying operations disallowed.
534N/A Cannot be used with --mirror or --rebuild.
534N/A --rebuild Re-build the catalog from pkgs in depot. Cannot be
534N/A used with --mirror or --readonly.
534N/A"""
534N/A sys.exit(2)
534N/A
290N/Aclass OptionError(Exception):
290N/A """Option exception. """
954N/A
954N/A def __init__(self, *args):
954N/A Exception.__init__(self, *args)
954N/A
534N/Aif __name__ == "__main__":
1099N/A
290N/A port = PORT_DEFAULT
1191N/A threads = THREADS_DEFAULT
1191N/A socket_timeout = SOCKET_TIMEOUT_DEFAULT
1191N/A readonly = READONLY_DEFAULT
1516N/A rebuild = REBUILD_DEFAULT
290N/A reindex = REINDEX_DEFAULT
290N/A proxy_base = None
290N/A mirror = MIRROR_DEFAULT
661N/A
290N/A if "PKG_REPO" in os.environ:
290N/A repo_path = os.environ["PKG_REPO"]
290N/A else:
395N/A repo_path = REPO_PATH_DEFAULT
290N/A
290N/A try:
290N/A content_root = os.environ["PKG_DEPOT_CONTENT"]
1483N/A except KeyError:
290N/A try:
1498N/A content_root = os.path.join(os.environ['PKG_HOME'],
1498N/A 'share/lib/pkg')
290N/A except KeyError:
1674N/A content_root = CONTENT_PATH_DEFAULT
1674N/A
1674N/A # By default, if the destination for a particular log type is not
1674N/A # specified, this is where we will send the output.
1674N/A log_routes = {
1674N/A "access": "none",
1674N/A "errors": "stderr"
395N/A }
430N/A log_opts = ["--log-%s" % log_type for log_type in log_routes]
395N/A
1544N/A # If stdout is a tty, then send access output there by default instead
1557N/A # of discarding it.
1506N/A if os.isatty(sys.stdout.fileno()):
395N/A log_routes["access"] = "stdout"
395N/A
424N/A opt = None
1024N/A try:
395N/A long_opts = ["content-root=", "mirror", "proxy-base=",
395N/A "readonly", "rebuild", "refresh-index"]
395N/A for opt in log_opts:
578N/A long_opts.append("%s=" % opt.lstrip('--'))
1228N/A opts, pargs = getopt.getopt(sys.argv[1:], "d:np:s:t:",
1172N/A long_opts)
395N/A for opt, arg in opts:
661N/A if opt == "-n":
1099N/A sys.exit(0)
1902N/A elif opt == "-d":
661N/A repo_path = arg
395N/A elif opt == "-p":
849N/A port = int(arg)
290N/A elif opt == "-s":
395N/A threads = int(arg)
395N/A if threads < THREADS_MIN:
395N/A raise OptionError, \
395N/A "minimum value is %d" % THREADS_MIN
395N/A if threads > THREADS_MAX:
395N/A raise OptionError, \
395N/A "maximum value is %d" % THREADS_MAX
395N/A elif opt == "-t":
395N/A socket_timeout = int(arg)
395N/A elif opt == "--content-root":
395N/A if arg == "":
395N/A raise OptionError, "You must specify " \
395N/A "a directory path."
290N/A content_root = arg
290N/A elif opt in log_opts:
395N/A if arg is None or arg == "":
395N/A raise OptionError, \
1231N/A "You must specify a log " \
1557N/A "destination."
1557N/A log_routes[opt.lstrip("--log-")] = arg
395N/A elif opt == "--mirror":
395N/A mirror = True
395N/A elif opt == "--proxy-base":
395N/A # Attempt to decompose the url provided into
395N/A # its base parts. This is done so we can
395N/A # remove any scheme information since we
395N/A # don't need it.
395N/A scheme, netloc, path, params, query, \
395N/A fragment = urlparse.urlparse(arg,
395N/A allow_fragments=0)
395N/A
290N/A # Rebuild the url without the scheme and
290N/A # remove the leading // urlunparse adds.
430N/A proxy_base = urlparse.urlunparse(("", netloc,
395N/A path, params, query, fragment)
395N/A ).lstrip("//")
395N/A elif opt == "--readonly":
395N/A readonly = True
1302N/A elif opt == "--rebuild":
395N/A rebuild = True
395N/A elif opt == "--refresh-index":
290N/A # Note: This argument is for internal use
395N/A # only. It's used when pkg.depotd is reexecing
1024N/A # itself and needs to know that's the case.
413N/A # This flag is purposefully omitted in usage.
1544N/A # The supported way to forcefully reindex is to
1557N/A # kill any pkg.depot using that directory,
1506N/A # remove the index directory, and restart the
413N/A # pkg.depot process. The index will be rebuilt
413N/A # automatically on startup.
1024N/A reindex = True
395N/A except getopt.GetoptError, e:
395N/A usage("pkg.depotd: %s" % e.msg)
413N/A except OptionError, e:
395N/A usage("pkg.depotd: option: %s -- %s" % (opt, e))
395N/A except (ArithmeticError, ValueError):
413N/A usage("pkg.depotd: illegal option value: %s specified " \
395N/A "for option: %s" % (arg, opt))
395N/A
395N/A if rebuild and reindex:
395N/A usage("--refresh-index cannot be used with --rebuild")
395N/A if rebuild and (readonly or mirror):
395N/A usage("--readonly and --mirror cannot be used with --rebuild")
1191N/A if reindex and (readonly or mirror):
1452N/A usage("--readonly and --mirror cannot be used with " \
1231N/A "--refresh-index")
395N/A
395N/A # If the program is going to reindex, the port is irrelevant since
424N/A # the program will not bind to a port.
395N/A if not reindex:
742N/A available, msg = port_available(None, port)
742N/A if not available:
742N/A print "pkg.depotd: unable to bind to the specified " \
742N/A "port: %d. Reason: %s" % (port, msg)
742N/A sys.exit(1)
742N/A else:
742N/A # Not applicable for reindexing operations.
742N/A content_root = None
742N/A
742N/A scfg = config.SvrConfig(repo_path, content_root, AUTH_DEFAULT)
742N/A
395N/A if rebuild:
395N/A scfg.destroy_catalog()
395N/A
395N/A if readonly:
395N/A scfg.set_read_only()
954N/A
954N/A if mirror:
954N/A scfg.set_mirror()
954N/A
954N/A try:
954N/A scfg.init_dirs()
954N/A except (RuntimeError, EnvironmentError), e:
395N/A print "pkg.depotd: an error occurred while trying to " \
1483N/A "initialize the depot repository directory " \
1483N/A "structures:\n%s" % e
1483N/A sys.exit(1)
1483N/A
395N/A # Setup our global configuration.
1902N/A # Global cherrypy configuration
1099N/A gconf = {
1099N/A "environment": "production",
395N/A "checker.on": True,
1498N/A "log.screen": False,
1498N/A "server.socket_host": "0.0.0.0",
691N/A "server.socket_port": port,
691N/A "server.thread_pool": threads,
691N/A "server.socket_timeout": socket_timeout,
395N/A "tools.log_headers.on": True
395N/A }
395N/A
395N/A log_type_map = {
395N/A "errors": {
290N/A "param": "log.error_file",
395N/A "attr": "error_log"
395N/A },
591N/A "access": {
591N/A "param": "log.access_file",
591N/A "attr": "access_log"
1505N/A }
1505N/A }
1505N/A
1505N/A for log_type in log_type_map:
1632N/A dest = log_routes[log_type]
1632N/A if dest in ("stdout", "stderr", "none"):
1632N/A if dest == "none":
1632N/A h = logging.StreamHandler(LogSink())
395N/A else:
395N/A h = logging.StreamHandler(eval("sys.%s" % \
290N/A dest))
290N/A
290N/A h.setLevel(logging.DEBUG)
290N/A h.setFormatter(cherrypy._cplogging.logfmt)
290N/A log_obj = eval("cherrypy.log.%s" % \
290N/A log_type_map[log_type]["attr"])
290N/A log_obj.addHandler(h)
290N/A # Since we've replaced cherrypy's log handler with our
290N/A # own, we don't want the output directed to a file.
290N/A dest = ""
290N/A
290N/A gconf[log_type_map[log_type]["param"]] = dest
290N/A
290N/A cherrypy.config.update(gconf)
395N/A
395N/A # Now that our logging, etc. has been setup, it's safe to perform any
290N/A # remaining preparation.
290N/A if reindex:
290N/A scfg.acquire_catalog(rebuild=False)
290N/A try:
290N/A scfg.catalog.run_update_index()
395N/A except search_errors.IndexingException, e:
395N/A cherrypy.log(str(e), "INDEX")
395N/A sys.exit(1)
290N/A sys.exit(0)
395N/A
395N/A # Now build our site configuration.
395N/A conf = {
395N/A "/": {
591N/A # We have to override cherrypy's default response_class so that
591N/A # we have access to the write() callable to stream data
591N/A # directly to the client.
591N/A "wsgi.response_class": depot.DepotResponse,
691N/A },
691N/A "/robots.txt": {
691N/A "tools.staticfile.on": True,
691N/A "tools.staticfile.filename": os.path.join(scfg.web_static_root,
290N/A "robots.txt")
290N/A },
290N/A "/static": {
290N/A "tools.staticdir.on": True,
290N/A "tools.staticdir.root": scfg.web_static_root,
591N/A "tools.staticdir.dir": ""
591N/A }
691N/A }
691N/A
290N/A if proxy_base:
395N/A # This changes the base URL for our server, and is primarily
395N/A # intended to allow our depot process to operate behind Apache
290N/A # or some other webserver process.
395N/A #
395N/A # Visit the following URL for more information:
395N/A # http://cherrypy.org/wiki/BuiltinTools#tools.proxy
395N/A proxy_conf = {
290N/A "tools.proxy.on": True,
290N/A "tools.proxy.local": "",
290N/A "tools.proxy.base": proxy_base
395N/A }
395N/A
395N/A if "/" not in conf:
395N/A conf["/"] = {}
395N/A
395N/A # Now merge or add our proxy configuration information into the
290N/A # existing configuration.
290N/A for entry in proxy_conf:
290N/A conf["/"][entry] = proxy_conf[entry]
290N/A
290N/A scfg.acquire_in_flight()
430N/A scfg.acquire_catalog()
290N/A
290N/A try:
395N/A root = cherrypy.Application(repo.Repository(scfg))
290N/A except rc.InvalidAttributeValueError, e:
395N/A emsg("pkg.depotd: repository.conf error: %s" % e)
506N/A sys.exit(1)
506N/A
506N/A try:
506N/A cherrypy.quickstart(root, config=conf)
506N/A except:
506N/A usage("pkg.depotd: unknown error starting depot, illegal " \
506N/A "option value specified?")
506N/A
834N/A