6957 mDNS support for depot
authorjohansen <johansen@opensolaris.org>
Tue, 18 May 2010 10:51:50 -0700
changeset 1902 59b7214232cb
parent 1901 fb369f67a414
child 1903 460ef45c8ebd
6957 mDNS support for depot 11178 remove defunct policies from imageconfig 15371 repository property defaults opensolaris.org-specific 15760 Client should detect mirrors advertised by mDNS 15973 Depot should not have default for repo_dir
.hgignore
src/depot.py
src/man/pkg.1.txt
src/man/pkg.depotd.1m.txt
src/modules/client/imageconfig.py
src/modules/client/transport/exception.py
src/modules/client/transport/mdetect.py
src/modules/client/transport/transport.py
src/modules/depotcontroller.py
src/modules/server/depot.py
src/modules/server/feed.py
src/modules/server/repository.py
src/modules/server/repositoryconfig.py
src/pkgdefs/SUNWipkg/prototype
src/setup.py
src/svc/pkg-mdns.xml
src/svc/pkg-server.xml
src/svc/svc-pkg-depot
src/svc/svc-pkg-mdns
src/tests/api/t_api_search.py
src/tests/cli/t_pkg_depotd.py
src/tests/cli/t_pkg_install.py
src/util/distro-import/140/common/package:pkg
--- a/.hgignore	Tue May 18 11:11:28 2010 +0100
+++ b/.hgignore	Tue May 18 10:51:50 2010 -0700
@@ -22,16 +22,9 @@
 ^src/gui/data/packagemanager.desktop$
 ^src/gui/help/.*/package-manager.xml$
 ^src/gui/data/packagemanager-info.xml$
-^src/man/packagemanager.1$
-^src/man/pkg.1$
-^src/man/pkg.5$
-^src/man/pkg.depotd.1m$
-^src/man/pkgdepend.1$
-^src/man/pkgdiff.1$
-^src/man/pkgmogrify.1$
-^src/man/pkgrecv.1$
-^src/man/pkgsend.1$
-^src/man/pm-updatemanager.1$
+^src/man/.*\.1$
+^src/man/.*\.1m$
+^src/man/.*\.5$
 ^src/pkg$
 ^src/pkg.depotd$
 ^src/pkgdefs/pkgproto$
--- a/src/depot.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/depot.py	Tue May 18 10:51:50 2010 -0700
@@ -19,8 +19,7 @@
 #
 # CDDL HEADER END
 #
-# Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
-# Use is subject to license terms.
+# Copyright (c) 2007, 2010 Oracle and/or its affiliates.  All rights reserved.
 #
 
 # pkg.depotd - package repository daemon
@@ -39,8 +38,6 @@
 # client, we should probably provide a query API to do same on the server, for
 # dumb clients (like a notification service).
 
-# The default repository path.
-REPO_PATH_DEFAULT = "/var/pkg/repo"
 # The default path for static and other web content.
 CONTENT_PATH_DEFAULT = "/usr/share/lib/pkg"
 # cherrypy has a max_request_body_size parameter that determines whether the
@@ -71,6 +68,8 @@
 REINDEX_DEFAULT = False
 # Not in mirror mode by default
 MIRROR_DEFAULT = False
+# Not in link-local mirror mode my default
+LL_MIRROR_DEFAULT = False
 
 import getopt
 import gettext
@@ -139,8 +138,8 @@
 Usage: /usr/lib/pkg.depotd [-d repo_dir] [-p port] [-s threads]
            [-t socket_timeout] [--cfg-file] [--content-root]
            [--disable-ops op[/1][,...]] [--debug feature_list]
-           [--log-access dest] [--log-errors dest] [--mirror] [--nasty]
-           [--set-property <section.property>=<value>]
+           [--file-root dir] [--log-access dest] [--log-errors dest]
+           [--mirror] [--nasty] [--set-property <section.property>=<value>]
            [--proxy-base url] [--readonly] [--rebuild] [--ssl-cert-file]
            [--ssl-dialog] [--ssl-key-file] [--sort-file-max-size size]
            [--writable-root dir]
@@ -165,6 +164,9 @@
         --exit-ready    Perform startup processing (including rebuilding
                         catalog or indices, if requested) and exit when
                         ready to start serving packages.
+        --file-root     The path to the root of the file content for a given
+                        repository.  This is used to override the default,
+                        <repo_root>/file.
         --log-access    The destination for any access related information
                         logged by the depot process.  Possible values are:
                         stderr, stdout, none, or an absolute pathname.  The
@@ -213,7 +215,6 @@
                         access.  Used with --readonly to allow server to
                         create needed files, such as search indices, without
                         needing write access to the package information.
-
 Options:
         --help or -?
 
@@ -247,9 +248,12 @@
         reindex = REINDEX_DEFAULT
         proxy_base = None
         mirror = MIRROR_DEFAULT
+        ll_mirror = LL_MIRROR_DEFAULT
+        file_root = None
         nasty = False
         nasty_value = 0
         repo_config_file = None
+        repo_path = None
         sort_file_max_size = indexer.SORT_FILE_MAX_SIZE
         ssl_cert_file = None
         ssl_key_file = None
@@ -260,8 +264,6 @@
 
         if "PKG_REPO" in os.environ:
                 repo_path = os.environ["PKG_REPO"]
-        else:
-                repo_path = REPO_PATH_DEFAULT
 
         try:
                 content_root = os.environ["PKG_DEPOT_CONTENT"]
@@ -289,10 +291,11 @@
         repo_props = {}
         try:
                 long_opts = ["add-content", "cfg-file=", "content-root=",
-                    "debug=", "disable-ops=", "exit-ready", "help", "mirror",
-                    "nasty=", "set-property=", "proxy-base=", "readonly",
-                    "rebuild", "refresh-index", "ssl-cert-file=", "ssl-dialog=",
-                    "ssl-key-file=", "sort-file-max-size=", "writable-root="]
+                    "debug=", "disable-ops=", "exit-ready", "file-root=",
+                    "help", "llmirror", "mirror", "nasty=", "proxy-base=",
+                    "readonly", "rebuild", "refresh-index", "set-property=",
+                    "ssl-cert-file=", "ssl-dialog=", "ssl-key-file=",
+                    "sort-file-max-size=", "writable-root="]
 
                 for opt in log_opts:
                         long_opts.append("%s=" % opt.lstrip('--'))
@@ -327,6 +330,11 @@
                                         raise OptionError, "You must specify " \
                                             "a directory path."
                                 content_root = arg
+                        elif opt == "--file-root":
+                                if arg == "":
+                                        raise OptionError, "You must specify " \
+                                            "a directory path."
+                                file_root = arg
                         elif opt == "--debug":
                                 if arg is None or arg == "":
                                         raise OptionError, \
@@ -378,6 +386,10 @@
                                 show_usage = True
                         elif opt == "--mirror":
                                 mirror = True
+                        elif opt == "--llmirror":
+                                mirror = True
+                                ll_mirror = True
+                                readonly = True
                         elif opt == "--nasty":
                                 value_err = None
                                 try:
@@ -536,6 +548,10 @@
                 usage("--readonly can only be used with --refresh-index if "
                     "--writable-root is used")
 
+        if not repo_path and not file_root:
+                usage("At least one of PKG_REPO, -d, or --file-root" 
+                    " must be provided")
+
         if (ssl_cert_file and not ssl_key_file) or (ssl_key_file and not
             ssl_cert_file):
                 usage("The --ssl-cert-file and --ssl-key-file options must "
@@ -678,10 +694,11 @@
         fork_allowed = not reindex and not exit_ready  
         try:
                 repo = sr.Repository(auto_create=not readonly,
-                    cfgpathname=repo_config_file, fork_allowed=fork_allowed,
-                    log_obj=cherrypy, mirror=mirror, properties=repo_props,
-                    read_only=readonly, refresh_index=not add_content, 
-                    repo_root=repo_path, sort_file_max_size=sort_file_max_size,
+                    cfgpathname=repo_config_file,  file_root=file_root,
+                    fork_allowed=fork_allowed, log_obj=cherrypy,
+                    mirror=mirror, properties=repo_props, read_only=readonly,
+                    refresh_index=not add_content, repo_root=repo_path,
+                    sort_file_max_size=sort_file_max_size,
                     writable_root=writable_root)
         except (RuntimeError, sr.RepositoryError), _e:
                 emsg("pkg.depotd: %s" % _e)
@@ -779,6 +796,9 @@
                 for entry in proxy_conf:
                         conf["/"][entry] = proxy_conf[entry]
 
+        if ll_mirror:
+                ds.DNSSD_Plugin(cherrypy.engine, conf, gconf).subscribe()
+
         try:
                 root = cherrypy.Application(depot)
                 cherrypy.quickstart(root, config=conf)
--- a/src/man/pkg.1.txt	Tue May 18 11:11:28 2010 +0100
+++ b/src/man/pkg.1.txt	Tue May 18 10:51:50 2010 -0700
@@ -681,6 +681,43 @@
           string is not guaranteed to be comparable in any fashion between
           versions.
 
+IMAGE PROPERTIES
+     The following properties are part of the image and may be set using
+     the set-property subcommand.  The values of these properties are
+     viewable with the property subcommand.
+
+     flush-content-cache-on-success
+          (boolean) If this is set to True, the package client will
+	  remove the files in its content-cache when install or
+	  image-update operations complete.  For image-update
+	  operations, the content is removed only from the source BE.
+	  When a packaging operation next occurs in the destination BE,
+	  it will flush its content cache, provided this option has not
+	  been changed.
+	  
+	  This property may be used to keep the content-cache small on
+	  systems with limited disk space, but it may cause operations
+	  to take longer to complete.
+
+	  Default value: False
+
+     mirror-discovery
+          (boolean)  Mirror-discovery tells the client to discover
+	  link-local content mirrors using mDNS and DNS-SD.  If this is
+	  set to True, the client will attempt to download package content
+	  from mirrors it dynamically discovers.  To run a mirror that
+	  advertises its content via mDNS, see pkg.mdnsd(1m).
+
+	  Default value: False
+
+     send-uuid
+          (boolean)  Send the image's Universally Unique Identifier
+	  (UUID) when performing network operations.  Although users may
+	  disable this option, some network repositories may refuse to talk
+	  to clients that do not supply a UUID.
+
+	  Default value: True
+
 EXAMPLES
      Example 1:  Create a new, full image, with publisher example.com,
      stored at /aux0/example_root.
@@ -859,7 +896,7 @@
     |_____________________________|_____________________________|
 
 SEE ALSO
-     pkgsend(1), pkg.depotd(1M), glob(3C), attributes(5), pkg(5)
+     pkgsend(1), pkg.depotd(1M), pkg.mdnsd(1M), glob(3C), attributes(5), pkg(5)
 
 NOTES
      The image packaging system is an under-development feature.
--- a/src/man/pkg.depotd.1m.txt	Tue May 18 11:11:28 2010 +0100
+++ b/src/man/pkg.depotd.1m.txt	Tue May 18 10:51:50 2010 -0700
@@ -48,6 +48,10 @@
                                         Logs the headers of every request to
                                         the error log.
 
+     pkg/file_root		(astring) The root of the file content
+     				for this repository.  If undefined, this
+				defaults to <repo_root>/file.
+
      pkg/inst_root              (astring) The file system path at which
                                 the instance should find its repository
                                 data.  The default value is
@@ -148,6 +152,26 @@
                                         value is read-authorization
                                         protected.
 
+     The pkg depot is also able to act as a mirror server for local
+     client images from pkg(5).  This allows clients that share a subnet
+     on a LAN to mirror their file caches.  Clients may download files
+     from one another, thereby reducing load on the package depot
+     server.  This functionality is available as an alternate depot
+     service configured by smf(5).  It uses mDNS and dns-sd for service
+     discovery.
+
+     The mDNS mirror is generally configured via the smf(5) properties
+     associated with its service.  The following properties are recognized:
+
+     pkg/file_root              (astring) The file system path at which
+                                the instance should find its file
+				content.  The default value is
+                                /var/pkg/download.
+
+     pkg/port                   (count) The port number on which the
+                                instance should listen for incoming
+                                package requests.  The default value is
+ 
 OPTIONS
      The following options alter the default behavior, if present, and
      will override the settings from the service instance when managed
@@ -189,6 +213,9 @@
                                 sure search indices are built, new content
                                 cataloged, etc.
 
+     --file-root path		Overrides pkg/file_root with value given
+     				by path.
+
      --log-access dest          Overrides pkg/log_access with the value
                                 given by dest.
 
@@ -249,6 +276,17 @@
      # svcadm refresh application/pkg/server
      # svcadm restart application/pkg/server
 
+     Example 3:  Enabling the mirror.
+
+     # svcadm enable application/pkg/dynamic-mirror
+
+     Example 4:  Changing the listening port of the server.
+
+     # svccfg -s application/pkg/server setprop pkg/port = 20000
+     # svcadm refresh application/pkg/server
+     # svcadm restart application/pkg/server
+
+
 ENVIRONMENT VARIABLES
      PKG_DEPOT_CONTENT          Specifies the directory containing static
                                 content served by the depot.  The files
@@ -379,7 +417,7 @@
     |_____________________________|_____________________________|
 
 SEE ALSO
-     pkg(1), pkgsend(1), syslogd(1M), attributes(5), smf(5)
+     dns-sd(1), mdnsd(1), pkg(1), pkgsend(1), syslogd(1M), attributes(5), smf(5)
 
 NOTES
      The image packaging system is an under-development feature.
@@ -394,9 +432,14 @@
 
           svc:/application/pkg/server
 
-     Because the depot server expects to be run by an smf(5) restarter,
-     it does not daemonize.  Error messages are generally sent to
-     standard output, or to the syslogd(1M) system.
+     The mDNS mirror service is managed by the service management
+     facility, smf(5), under the service identifier:
+
+          svc:/application/pkg/dynamic-mirror
+
+     Because the depot server and mirror expect to be run by an smf(5)
+     restarter, they do not daemonize.  Error messages are generally
+     sent to standard output, or to the syslogd(1M) system.
 
      When publishing individual package files that are greater than 128MB
      in size, filesystem-based publication must be used due to publication
--- a/src/modules/client/imageconfig.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/modules/client/imageconfig.py	Tue May 18 10:51:50 2010 -0700
@@ -20,8 +20,9 @@
 # CDDL HEADER END
 #
 
-# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
-# Use is subject to license terms.
+#
+# Copyright (c) 2007, 2010 Oracle and/or its affiliates.  All rights reserved.
+#
 
 import ConfigParser
 import errno
@@ -44,14 +45,12 @@
 # pkg(5) and their default values. Calls to the ImageConfig.get_policy method
 # should use the constants defined here.
 
-PURSUE_LATEST = "pursue-latest"
-DISPLAY_COPYRIGHTS = "display-copyrights"
 FLUSH_CONTENT_CACHE = "flush-content-cache-on-success"
+MIRROR_DISCOVERY = "mirror-discovery"
 SEND_UUID = "send-uuid"
 default_policies = {
-    PURSUE_LATEST: True,
-    DISPLAY_COPYRIGHTS: True,
     FLUSH_CONTENT_CACHE: False,
+    MIRROR_DISCOVERY: False,
     SEND_UUID: True
 }
 
--- a/src/modules/client/transport/exception.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/modules/client/transport/exception.py	Tue May 18 10:51:50 2010 -0700
@@ -340,3 +340,13 @@
                 if r != 0:
                         return r
                 return cmp(self.count, other.count)
+
+class mDNSException(TransportException):
+        """Used when mDNS operations fail."""
+
+        def __init__(self, errstr):
+                TransportException.__init__(self)
+                self.err = errstr
+
+        def __str__(self):
+                return self.err
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/modules/client/transport/mdetect.py	Tue May 18 10:51:50 2010 -0700
@@ -0,0 +1,160 @@
+#!/usr/bin/python
+#
+# CDDL HEADER START
+#
+# The contents of this file are subject to the terms of the
+# Common Development and Distribution License (the "License").
+# You may not use this file except in compliance with the License.
+#
+# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+# or http://www.opensolaris.org/os/licensing.
+# See the License for the specific language governing permissions
+# and limitations under the License.
+#
+# When distributing Covered Code, include this CDDL HEADER in each
+# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+# If applicable, add the following below this CDDL HEADER, with the
+# fields enclosed by brackets "[]" replaced with your own identifying
+# information: Portions Copyright [yyyy] [name of copyright owner]
+#
+# CDDL HEADER END
+#
+
+#
+# Copyright (c) 2010 Oracle and/or its affiliates.  All rights reserved.
+#
+
+import random
+
+import pkg.misc as misc
+import pkg.client.publisher as pub
+import pkg.client.transport.exception as tx
+
+try:
+        import pybonjour
+except (OSError, ImportError):
+        pass
+else:
+        import select
+
+
+class MirrorDetector(object):
+        """This class uses mDNS and DNS-SD to find link-local content
+        mirrors that may be present on the client's subnet."""
+
+        def __init__(self):
+                self._mirrors = []
+                self.__timeout = 1
+                self.__service = "_pkg5._tcp"
+
+        def __contains__(self, key):
+                return key in self._mirrors
+
+        def __getitem__(self, pos):
+                return self._mirrors[pos]
+
+        def __iter__(self):
+                """Each time iterator is invoked, randomly select up to
+                five mirrors from the list of available mirrors."""
+
+                listlen = len(self._mirrors)
+                iterlst = random.sample(xrange(listlen), min(listlen, 5))
+
+                for v in iterlst:
+                        yield self._mirrors[v]
+
+        def locate(self):
+                """When invoked, this populates the MirrorDetector object with
+                URLs that name dynamically discovered content mirrors."""
+
+                # Clear the list of mirrors.  It will be repopulated later.
+                self._mirrors = []      
+
+                if not "pybonjour" in globals():
+                        return
+
+                timedout = False
+                tval = self.__timeout
+
+                def browse_cb(sd_hdl, flags, interface_idx, error_code,
+                    service_name, regtype, reply_domain):
+
+                        if error_code != pybonjour.kDNSServiceErr_NoError:
+                                return
+
+                        if not (flags & pybonjour.kDNSServiceFlagsAdd):
+                                return
+
+                        self._resolve_server(interface_idx, error_code,
+                            service_name, regtype, reply_domain)
+
+                try:
+                        sd_hdl = pybonjour.DNSServiceBrowse(
+                            regtype=self.__service, callBack=browse_cb)
+                except pybonjour.BonjourError, e:
+                        errstr = "mDNS Service Browse Failed: %s\n" % e[0][1]
+                        raise tx.mDNSException(errstr)
+
+                try:
+                        while not timedout:
+                                avail = select.select([sd_hdl], [], [], tval)
+                                if sd_hdl in avail[0]:
+                                        pybonjour.DNSServiceProcessResult(
+                                            sd_hdl)
+                                        tval = 0
+                                else:
+                                        timedout = True
+                except select.error, e:
+                        errstr = "Select failed: %s\n" % e[1]
+                        raise tx.mDNSException(errstr)
+                except pybonjour.BonjourError, e:
+                        errstr = "mDNS Process Result failed: %s\n" % e[0][1]
+                        raise tx.mDNSException(errstr)
+                finally:
+                        sd_hdl.close()
+
+        def _resolve_server(self, if_idx, ec, service_name, regtype,
+            reply_domain):
+                """Invoked to resolve mDNS information about a service that
+                was discovered by a Browse call."""
+
+                timedout = False
+                tval = self.__timeout
+
+                def resolve_cb(sd_hdl, flags, interface_idx, error_code,
+                    full_name, host_target, port, txt_record):
+
+                        if error_code != pybonjour.kDNSServiceErr_NoError:
+                                return
+
+                        tr = pybonjour.TXTRecord.parse(txt_record)
+                        if "url" in tr:
+                                url = tr["url"]
+                                if not misc.valid_pub_url(url):
+                                        return
+                                self._mirrors.append(pub.RepositoryURI(url))
+
+                try:
+                        sd_hdl =  pybonjour.DNSServiceResolve(0, if_idx,
+                            service_name, regtype, reply_domain, resolve_cb)
+                except pybonjour.BonjourError, e:
+                        errstr = "mDNS Service Resolve Failed: %s\n" % e[0][1]
+                        raise tx.mDNSException(errstr)
+
+                try:
+                        while not timedout:
+                                avail = select.select([sd_hdl], [], [], tval)
+                                if sd_hdl in avail[0]:
+                                        pybonjour.DNSServiceProcessResult(
+                                            sd_hdl)
+                                        tval = 0
+                                else:
+                                        timedout = True
+                except select.error, e:
+                        errstr = "Select failed; %s\n" % e[1]
+                        raise tx.mDNSException(errstr)
+                except pybonjour.BonjourError, e:
+                        errstr = "mDNS Process Result Failed: %s\n" % e[0][1]
+                        raise tx.mDNSException(errstr)
+                finally:
+                        sd_hdl.close()
--- a/src/modules/client/transport/transport.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/modules/client/transport/transport.py	Tue May 18 10:51:50 2010 -0700
@@ -39,6 +39,7 @@
 import pkg.client.publisher as publisher
 import pkg.client.transport.engine as engine
 import pkg.client.transport.exception as tx
+import pkg.client.transport.mdetect as mdetect
 import pkg.client.transport.repo as trepo
 import pkg.client.transport.stats as tstats
 import pkg.file_layout.file_manager as fm
@@ -70,6 +71,7 @@
                 self.__cadir = None
                 self.__portal_test_executed = False
                 self.__repo_cache = None
+                self.__dynamic_mirrors = []
                 self.__lock = nrlock.NRLock()
                 self._caches = None
                 self.stats = tstats.RepoChooser()
@@ -84,6 +86,16 @@
 
                 self.__repo_cache = trepo.RepoCache(self.__engine)
 
+                cc = self.__img.cfg_cache
+                if cc and cc.get_policy(imageconfig.MIRROR_DISCOVERY):
+                        self.__dynamic_mirrors = mdetect.MirrorDetector()
+                        try:
+                                self.__dynamic_mirrors.locate()
+                        except tx.mDNSException:
+                                # Not fatal.  Suppress.
+                                pass
+
+
         def _reset_caches(self):
                 # For now, transport write caches all publisher data in one
                 # place regardless of publisher source.
@@ -135,6 +147,12 @@
                         self.__engine.reset()
                         self.__repo_cache.clear_cache()
                         self._reset_caches()
+                        if self.__dynamic_mirrors:
+                                try:
+                                        self.__dynamic_mirrors.locate()
+                                except tx.mDNSException:
+                                        # Not fatal. Suppress.
+                                        pass
                 finally:
                         self.__lock.release()
 
@@ -154,6 +172,7 @@
                                 self.__repo_cache.clear_cache()
                         self.__repo_cache = None
                         self._caches = None
+                        self.__dynamic_mirrors = []
                 finally:
                         self.__lock.release()
 
@@ -1568,6 +1587,7 @@
                         repo = pub.selected_repository
                         repolist = repo.mirrors[:]
                         repolist.extend(repo.origins)
+                        repolist.extend(self.__dynamic_mirrors)
                         rslist = self.stats.get_repostats(repolist,
                             repo.origins)
                         for rs, ruri in rslist:
--- a/src/modules/depotcontroller.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/modules/depotcontroller.py	Tue May 18 10:51:50 2010 -0700
@@ -57,6 +57,7 @@
                 self.__dir = None
                 self.__disable_ops = None
                 self.__exit_ready = False
+                self.__file_root = None
                 self.__logpath = "/tmp/depot.log"
                 self.__mirror = False
                 self.__output = None
@@ -118,6 +119,12 @@
         def get_property(self, section, prop):
                 return self.__props.get(section, {}).get(prop)
 
+        def set_file_root(self, f_root):
+                self.__file_root = f_root
+
+        def get_file_root(self):
+                return self.__file_root
+
         def set_repodir(self, repodir):
                 self.__dir = repodir
 
@@ -260,6 +267,8 @@
                 if self.__dir != None:
                         args.append("-d")
                         args.append(self.__dir)
+                if self.__file_root != None:
+                        args.append("--file-root=%s" % self.__file_root)
                 if self.__readonly:
                         args.append("--readonly")
                 if self.__rebuild:
--- a/src/modules/server/depot.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/modules/server/depot.py	Tue May 18 10:51:50 2010 -0700
@@ -27,6 +27,14 @@
 import cherrypy
 from cherrypy.lib.static import serve_file
 from email.utils import formatdate
+from cherrypy.process.plugins import SimplePlugin
+
+try:
+        import pybonjour
+except (OSError, ImportError):
+        pass
+else:
+        import select
 
 import cStringIO
 import errno
@@ -39,6 +47,7 @@
 import socket
 import tarfile
 import time
+import urlparse
 # Without the below statements, tarfile will trigger calls to getpwuid and
 # getgrgid for every file downloaded.  This in turn leads to nscd usage which
 # limits the throughput of the depot process.  Setting these attributes to
@@ -132,8 +141,10 @@
                 # Store any possible configuration changes.
                 repo.write_config()
 
-                if repo.mirror:
-                        self.ops_list = self.REPO_OPS_MIRROR
+                if repo.mirror or not repo.repo_root:
+                        self.ops_list = self.REPO_OPS_MIRROR[:]
+                        if not repo.cfg.get_property("publisher", "prefix"):
+                                self.ops_list.remove("publisher")
                 elif repo.read_only:
                         self.ops_list = self.REPO_OPS_READONLY
                 else:
@@ -1476,3 +1487,94 @@
                         response.body = nfile.read(filesz)
 
                 return response.body
+
+class DNSSD_Plugin(SimplePlugin):
+        """Allow a depot to configure DNS-SD through mDNS."""
+
+        def __init__(self, bus, scfg, gconf):
+                """Bus is cherrypy engine, scfg is SvrConfig, and gconf
+                is dictionary containing the CherryPy configuration."""
+
+                SimplePlugin.__init__(self, bus)
+
+                if "pybonjour" not in globals():
+                        self.start = lambda: None
+                        self.exit = lambda: None
+                        return
+
+                self.name = "pkg(5) mirror on %s" % socket.gethostname()
+                self.wanted_name = self.name
+                self.regtype = "_pkg5._tcp"
+                self.port = gconf["server.socket_port"]
+                self.sd_hdl = None
+                self.reg_ok = False
+
+                if gconf["server.ssl_certificate"] and \
+                    gconf["server.ssl_private_key"]:
+                        proto = "https"
+                else:
+                        proto = "http"
+
+                netloc = "%s:%s" % (socket.getfqdn(), self.port)
+                self.url = urlparse.urlunsplit((proto, netloc, '', '', ''))
+
+        def reg_cb(self, sd_hdl, flags, error_code, name, regtype, domain):
+                """Callback invoked by service register function.  Arguments
+                are determined by the pybonjour framework, and must not
+                be changed."""
+
+                if error_code != pybonjour.kDNSServiceErr_NoError:
+                        self.bus.log("Error in DNS-SD registration: %s" %
+                            pybonjour.BonjourError(error_code))
+
+                self.reg_ok = True
+                self.name = name
+                if self.name != self.wanted_name:
+                        self.bus.log("DNS-SD service name changed to: %s" %
+                            self.name)
+
+        def start(self):
+                self.bus.log("Starting DNS-SD registration.")
+
+                txt_r = pybonjour.TXTRecord()
+                txt_r["url"] = self.url
+                txt_r["type"] = "mirror"
+
+                to_val = 10
+                timedout = False
+
+                try:
+                        self.sd_hdl = pybonjour.DNSServiceRegister(
+                            name=self.name, regtype=self.regtype,
+                            port=self.port, txtRecord=txt_r,
+                            callBack=self.reg_cb)
+                except pybonjour.BonjourError, e:
+                        self.bus.log("DNS-SD service registration failed: %s" %
+                            e)
+                        return
+
+                try:
+                        while not timedout:
+                                avail = select.select([self.sd_hdl], [], [],
+                                    to_val)
+                                if self.sd_hdl in avail[0]:
+                                        pybonjour.DNSServiceProcessResult(
+                                            self.sd_hdl)
+                                        to_val = 0
+                                else:
+                                        timedout = True
+                except pybonjour.BonjourError, e:
+                        self.bus.log("DNS-SD service registration failed: %s" %
+                            e)
+
+                if not self.reg_ok:
+                        self.bus.log("DNS-SD registration timed out.")
+                        return
+                self.bus.log("Finished DNS-SD registration.")
+
+        def exit(self):
+                self.bus.log("DNS-SD plugin exited")
+                if self.sd_hdl:
+                        self.sd_hdl.close()
+                self.sd_hdl = None
+                self.bus.log("Service unregistration for DNS-SD complete.")
--- a/src/modules/server/feed.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/modules/server/feed.py	Tue May 18 10:51:50 2010 -0700
@@ -73,7 +73,8 @@
         for feeds to work correctly.
         """
 
-        if not (repo.read_only and not repo.writable_root):
+        if repo.feed_cache_root and not \
+            (repo.read_only and not repo.writable_root):
                 # RSS/Atom feeds require a unique identifier, so
                 # generate one if isn't defined already.  This
                 # needs to be a persistent value, so we only
--- a/src/modules/server/repository.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/modules/server/repository.py	Tue May 18 10:51:50 2010 -0700
@@ -144,6 +144,13 @@
                 return _("Search functionality is temporarily unavailable.")
 
 
+class RepositoryUnsupportedOperationError(RepositoryError):
+        """Raised when the repository is unable to support an operation,
+        based upon its current configuration."""
+
+        def __str__(self):
+                return("Operation not supported for this configuration.")
+
 class RepositoryUpgradeError(RepositoryError):
         """Used to indicate that the specified repository root cannot be used
         as the catalog or format of it is an older version that needs to be
@@ -167,7 +174,7 @@
         def __init__(self, auto_create=False, catalog_root=None,
             cfgpathname=None, fork_allowed=False, index_root=None, log_obj=None,
             mirror=False, pkg_root=None, properties=EmptyDict, read_only=False,
-            repo_root=None, trans_root=None, refresh_index=True,
+            repo_root=None, trans_root=None, file_root=None, refresh_index=True,
             sort_file_max_size=indexer.SORT_FILE_MAX_SIZE, writable_root=None):
                 """Prepare the repository for use."""
 
@@ -184,8 +191,16 @@
                 self.read_only = read_only
                 self.__sort_file_max_size = sort_file_max_size
                 self.__tmp_root = None
+                self.__file_root = None
 
-                # Must be set before other roots.
+                # Set before repo root, since it's possible to have
+                # the file root in an entirely different location.  Repo
+                # root will govern file_root, if an argument to file_root
+                # is not supplied in __init__.
+                if file_root:
+                        self.file_root = file_root
+
+                # Must be set before most other roots.
                 self.repo_root = repo_root
 
                 # These are all overrides for the default values that setting
@@ -202,8 +217,11 @@
                         self.trans_root = trans_root
 
                 # Must be set before writable_root.
-                self.__required_dirs = [self.trans_root, self.pkg_root,
-                    self.catalog_root]
+                if self.mirror:
+                        self.__required_dirs = [self.file_root]
+                else:
+                        self.__required_dirs = [self.trans_root, self.pkg_root,
+                            self.catalog_root, self.file_root]
 
                 # Ideally, callers would just specify overrides for the feed
                 # cache root, index_root, etc.  But this must be set after all
@@ -314,6 +332,9 @@
                 """Create a temp directory under repository directory for
                 various purposes."""
 
+                if not self.repo_root:
+                        return
+
                 root = self.repo_root
                 if self.writable_root:
                         root = self.writable_root
@@ -344,6 +365,9 @@
                                 raise
                         return datetime.datetime.utcfromtimestamp(mod_time)
 
+                if not self.catalog_root:
+                        return
+
                 # To determine if an upgrade is needed, first check for a v0
                 # catalog attrs file.
                 need_upgrade = False
@@ -424,7 +448,7 @@
                     "take some time."))
                 self.__rebuild(lm=v0_lm)
 
-                if not self.read_only:
+                if not self.read_only and self.repo_root:
                         v0_cat = os.path.join(self.repo_root, "catalog",
                             "catalog")
                         for f in v0_attrs, v0_cat:
@@ -466,7 +490,7 @@
                 """Destroy the catalog."""
 
                 self.__catalog = None
-                if os.path.exists(self.catalog_root):
+                if self.catalog_root and os.path.exists(self.catalog_root):
                         shutil.rmtree(self.catalog_root)
 
         @staticmethod
@@ -535,10 +559,23 @@
                                 self.cfg.set_property(section, prop, value)
 
                 # Verify that all required configuration information is set.
-                self.cfg.validate()
+                try:
+                        self.cfg.validate()
+                except rc.RequiredPropertyValueError, ex:
+                        # In mirror mode, the repository may be configured
+                        # with a file store that contains content from multiple
+                        # publishers.  In this case, eschew the requirement that
+                        # the publisher prefix must be set.
+                        if self.mirror and ex.section == "publisher" and \
+                            ex.prop == "prefix":
+                                pass
+                        else:
+                                raise
 
         def __init_dirs(self):
                 """Verify and instantiate repository directory structure."""
+                if not self.repo_root:
+                        return
                 emsg = _("repository directories incomplete")
                 for d in self.__required_dirs + self.__optional_dirs:
                         if self.auto_create or (self.writable_root and
@@ -573,6 +610,10 @@
                 """Load stored configuration data and configure the repository
                 appropriately."""
 
+                if not self.repo_root:
+                        self.cfg = rc.RepositoryConfig(pathname=None)
+                        return
+
                 default_cfg_path = False
 
                 # Now load our repository configuration / metadata.
@@ -602,6 +643,9 @@
                         # Mirrors don't permit publication.
                         return
 
+                if not self.trans_root:
+                        return
+
                 self.__in_flight_trans = {}
                 for txn in os.walk(self.trans_root):
                         if txn[0] == self.trans_root:
@@ -626,6 +670,9 @@
                 """Private version; caller responsible for repository
                 locking."""
 
+                if not self.pkg_root:
+                        return
+
                 default_pub = self.cfg.get_property("publisher", "prefix")
 
                 if self.read_only:
@@ -683,6 +730,9 @@
                 """Private version; caller responsible for repository
                 locking."""
 
+                if not self.index_root:
+                        return
+
                 self.__searchdb_update_handle_lock.acquire()
 
                 if self.__searchdb_update_handle:
@@ -774,8 +824,8 @@
                 # to load it.
                 self.__upgrade()
 
-                if self.mirror:
-                        # In mirror-mode, nothing else to do.
+                if self.mirror or not self.repo_root:
+                        # In mirror-mode, or no repo_root, nothing to do.
                         return
 
                 # If no catalog exists on-disk yet, ensure an empty one does
@@ -893,34 +943,54 @@
                         self.catalog.meta_root = root
 
         def __set_repo_root(self, root):
-                assert root is not None
+                if root:
+                        root = os.path.abspath(root)
+                        self.__repo_root = root
+                        self.__tmp_root = os.path.join(root, "tmp")
+                        self.catalog_root = os.path.join(root, "catalog")
+                        self.feed_cache_root = root
+                        self.index_root = os.path.join(root, "index")
+                        self.pkg_root = os.path.join(root, "pkg")
+                        self.trans_root = os.path.join(root, "trans")
+                        if not self.file_root:
+                                self.file_root = os.path.join(root, "file")
+                else:
+                        self.__repo_root = None
+                        self.catalog_root = None
+                        self.feed_cache_root = None
+                        self.index_root = None
+                        self.pkg_root = None
+                        self.trans_root = None
+ 
+        def __get_file_root(self):
+                return self.__file_root
 
-                root = os.path.abspath(root)
-                self.__repo_root = root
-                self.__tmp_root = os.path.join(root, "tmp")
-                self.catalog_root = os.path.join(root, "catalog")
-                self.feed_cache_root = root
+        def __set_file_root(self, root):
+                self.__file_root = root
                 try:
-                        self.cache_store = file_manager.FileManager(
-                            os.path.join(root, "file"), self.read_only)
+                        self.cache_store = file_manager.FileManager(root,
+                            self.read_only)
                 except file_manager.NeedToModifyReadOnlyFileManager:
-                        try:
-                                fs = os.lstat(self.repo_root)
-                        except OSError, e:
-                                # If the stat failed due to this, then assume
-                                # the repository is possibly valid but that
-                                # there is a permissions issue.
-                                if e.errno == EACCES:
-                                        raise api_errors.PermissionsException(
-                                            e.filename)
-                                raise
-                        # If the stat succeeded, then regardless of whether
-                        # repo_root is really a directory, the repository is
-                        # invalid.
-                        raise RepositoryInvalidError(self.repo_root)
-                self.index_root = os.path.join(root, "index")
-                self.pkg_root = os.path.join(root, "pkg")
-                self.trans_root = os.path.join(root, "trans")
+                        if self.repo_root:
+                                try:
+                                        fs = os.lstat(self.repo_root)
+                                except OSError, e:
+                                        # If the stat failed due to this, then
+                                        # assume the repository is possibly
+                                        # valid but that there is a permissions
+                                        # issue.
+                                        if e.errno == EACCES:
+                                                raise api_errors.\
+                                                    PermissionsException(
+                                                    e.filename)
+                                        raise
+                                # If the stat succeeded, then regardless of
+                                # whether repo_root is really a directory, the
+                                # repository is invalid.
+                                raise RepositoryInvalidError(self.repo_root)
+                        # If repository root hasn't been specified yet,
+                        # just raise the error with the path that is available.
+                        raise RepositoryInvalidError(root)
 
         def __set_writable_root(self, root):
                 if root is not None:
@@ -928,10 +998,14 @@
                         self.__tmp_root = os.path.join(root, "tmp")
                         self.feed_cache_root = root
                         self.index_root = os.path.join(root, "index")
-                else:
+                elif self.repo_root:
                         self.__tmp_root = os.path.join(self.repo_root, "tmp")
                         self.feed_cache_root = self.repo_root
                         self.index_root = os.path.join(self.repo_root, "index")
+                else:
+                        self.__tmp_root = None
+                        self.feed_cache_root = None
+                        self.index_root = None
                 self.__writable_root = root
 
         def __unlock_repository(self):
@@ -993,6 +1067,8 @@
                         raise RepositoryMirrorError()
                 if self.read_only:
                         raise RepositoryReadOnlyError()
+                if not self.repo_root:
+                        raise RepositoryUnsupportedOperationError()
 
                 self.__lock_repository()
                 try:
@@ -1019,6 +1095,8 @@
                         raise RepositoryMirrorError()
                 if self.read_only:
                         raise RepositoryReadOnlyError()
+                if not self.repo_root:
+                        raise RepositoryUnsupportedOperationError()
 
                 self.__lock_repository()
                 try:
@@ -1042,6 +1120,8 @@
                         raise RepositoryMirrorError()
                 if self.read_only:
                         raise RepositoryReadOnlyError()
+                if not self.repo_root:
+                        raise RepositoryUnsupportedOperationError()
 
                 self.__lock_repository()
                 try:
@@ -1071,6 +1151,9 @@
                 as the v0 updatelog does not support renames, obsoletion,
                 package removal, etc."""
 
+                if not self.catalog_root:
+                        raise RepositoryUnsupportedOperationError()
+
                 c = self.catalog
                 self.inc_catalog()
 
@@ -1092,6 +1175,8 @@
 
                 if self.mirror:
                         raise RepositoryMirrorError()
+                if not self.catalog_root:
+                        raise RepositoryUnsupportedOperationError()
 
                 assert name
                 self.inc_catalog()
@@ -1106,6 +1191,8 @@
 
                 if self.mirror:
                         raise RepositoryMirrorError()
+                if not self.repo_root:
+                        raise RepositoryUnsupportedOperationError()
 
                 try:
                         t = self.__in_flight_trans[trans_id]
@@ -1163,6 +1250,8 @@
 
                 if self.mirror:
                         raise RepositoryMirrorError()
+                if not self.repo_root:
+                        raise RepositoryUnsupportedOperationError()
 
                 self.inc_manifest()
 
@@ -1183,6 +1272,8 @@
                         raise RepositoryMirrorError()
                 if self.read_only:
                         raise RepositoryReadOnlyError()
+                if not self.repo_root:
+                        raise RepositoryUnsupportedOperationError()
 
                 self.__lock_repository()
                 try:
@@ -1203,6 +1294,8 @@
 
                 if self.mirror:
                         raise RepositoryMirrorError()
+                if not self.repo_root:
+                        raise RepositoryUnsupportedOperationError()
 
                 self.__lock_repository()
                 try:
@@ -1215,6 +1308,8 @@
                 in the catalog and adds them in"""
                 if self.mirror:
                         raise RepositoryMirrorError()
+                if not self.repo_root:
+                        raise RepositoryUnsupportedOperationError()
 
                 self.__lock_repository()
                 try:
@@ -1230,6 +1325,8 @@
 
                 if self.mirror:
                         raise RepositoryMirrorError()
+                if not self.repo_root:
+                        raise RepositoryUnsupportedOperationError()
 
                 self.__lock_repository()
                 try:
@@ -1261,6 +1358,8 @@
 
                 if self.mirror:
                         raise RepositoryMirrorError()
+                if not self.index_root:
+                        raise RepositoryUnsupportedOperationError()
 
                 c = self.catalog
                 fmris_to_index = indexer.Indexer.check_for_updates(
@@ -1283,6 +1382,8 @@
 
                 if self.mirror:
                         raise RepositoryMirrorError()
+                if not self.index_root:
+                        raise RepositoryUnsupportedOperationError()
 
                 def _search(q):
                         assert self.index_root
@@ -1322,6 +1423,8 @@
 
                 if self.mirror:
                         raise RepositoryMirrorError()
+                if not self.repo_root:
+                        raise RepositoryUnsupportedOperationError()
                 if not fmri.is_valid_pkg_name(pfmri.get_name()):
                         return False
                 if not pfmri.version:
@@ -1341,6 +1444,7 @@
                         self.__unlock_repository()
 
         catalog_root = property(__get_catalog_root, __set_catalog_root)
+        file_root = property(__get_file_root, __set_file_root)
         repo_root = property(__get_repo_root, __set_repo_root)
         writable_root = property(__get_writable_root, __set_writable_root)
 
--- a/src/modules/server/repositoryconfig.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/modules/server/repositoryconfig.py	Tue May 18 10:51:50 2010 -0700
@@ -47,27 +47,52 @@
 class PropertyError(Exception):
         """Base exception class for property errors."""
 
-        def __init__(self, *args):
-                Exception.__init__(self, *args)
+        def __init__(self, section, prop):
+                self.section = section
+                self.prop = prop
 
+        def __str__(self):
+                raise NotImplementedError()
 
 class InvalidPropertyError(PropertyError):
         """Exception class used to indicate an invalid property."""
 
+        def __str__(self):
+                return "Invalid property %s.%s" % (self.section, self.prop)
+
+class InvalidSectionError(InvalidPropertyError):
+        """Exception class used to indicate an invalid section."""
+
+        def __init__(self, section):
+                InvalidPropertyError.__init__(self, section, None)
+
+        def __str__(self):
+                return "Invalid Property. Unknown section: %s." % self.section
 
 class InvalidPropertyValueError(PropertyError):
         """Exception class used to indicate an invalid property value."""
 
+        def __init__(self, section, prop, value):
+                PropertyError.__init__(self, section, prop)
+                self.value = value
+
+        def __str__(self):
+                return "Invalid value '%s' for %s.%s." % (self.value,
+                    self.section, self.prop)
 
 class RequiredPropertyValueError(PropertyError):
         """Exception class used to indicate a required property value is
         missing."""
 
+        def __str__(self):
+                return "%s.%s is required." % (self.section, self.prop)
 
 class ReadOnlyPropertyError(PropertyError):
         """Exception class used to indicate when an attempt to set a read-only
         value was made."""
 
+        def __str__(self):
+                return "%s.%s is read-only." % (self.section, self.prop)
 
 class RepositoryConfig(object):
         """A RepositoryConfig object is a collection of configuration
@@ -93,18 +118,15 @@
                 "description": {},
                 "detailed_url": {
                     "type": PROP_TYPE_URI,
-                    "default": "http://www.opensolaris.com"
                 },
                 "legal_uris": {
                     "type": PROP_TYPE_URI_LIST
                 },
                 "maintainer": {
-                    "default":
-                        "Project Indiana <[email protected]>"
+                    "type": PROP_TYPE_STR
                 },
                 "maintainer_url": {
                     "type": PROP_TYPE_URI,
-                    "default": "http://www.opensolaris.org/os/project/indiana/"
                 },
                 "mirrors": {
                     "type": PROP_TYPE_URI_LIST
@@ -132,7 +154,7 @@
                     "readonly": True,
                 },
                 "name": {
-                    "default": "opensolaris.org repository feed"
+                    "default": "package repository feed"
                 },
                 "description": {},
                 "icon": {
@@ -160,7 +182,7 @@
                 self.__dirty = False
                 self.__pathname = pathname
 
-                if os.path.exists(pathname):
+                if pathname and os.path.exists(pathname):
                         # If a pathname was provided, read the data in.
                         self.__read(overrides=properties)
                 else:
@@ -207,16 +229,12 @@
                 """
                 if section not in cls._props:
                         if raise_error:
-                                raise InvalidPropertyError("Invalid "
-                                    " property. Unknown section: %s." % \
-                                    (section))
+                                raise InvalidSectionError(section)
                         else:
                                 return False
                 if prop not in cls._props[section]:
                         if raise_error:
-                                raise InvalidPropertyError("Invalid "
-                                    "property %s.%s." % \
-                                    (section, prop))
+                                raise InvalidPropertyError(section, prop)
                         else:
                                 return False
                 return True
@@ -322,11 +340,9 @@
                                 if raise_error:
                                         if value in (None, ""):
                                                 raise RequiredPropertyValueError(
-                                                    "%s.%s is required." % \
-                                                    (section, prop))
+                                                    section, prop)
                                         raise InvalidPropertyValueError(
-                                            "Invalid value '%s' for %s.%s." % \
-                                            (value, section, prop))
+                                            section, prop, value)
                                 else:
                                         return False
                 else:
@@ -395,8 +411,7 @@
                 if not self.is_readonly_property(section, prop):
                         return self._set_property(section, prop, value)
                 else:
-                        raise ReadOnlyPropertyError("%s.%s is read-only." % \
-                            (prop, section))
+                        raise ReadOnlyPropertyError(section, prop)
 
         def __read(self, overrides=misc.EmptyDict):
                 """Reads the specified pathname and populates the configuration
@@ -404,6 +419,9 @@
                 expected to be in a ConfigParser-compatible format.
                 """
 
+                if not self.__pathname:
+                        return
+
                 # Reset to initial state to ensure we only have default values
                 # so that any values not overwritten by the saved configuration
                 # will be correct.
@@ -470,7 +488,10 @@
                 """Saves the current configuration object to the specified
                 pathname using ConfigParser.
                 """
-                if os.path.exists(self.__pathname) and not self.__dirty:
+
+                if not self.__pathname:
+                        return
+                elif os.path.exists(self.__pathname) and not self.__dirty:
                         return
 
                 cp = ConfigParser.SafeConfigParser()
--- a/src/pkgdefs/SUNWipkg/prototype	Tue May 18 11:11:28 2010 +0100
+++ b/src/pkgdefs/SUNWipkg/prototype	Tue May 18 10:51:50 2010 -0700
@@ -10,6 +10,7 @@
 d none lib/svc 755 root bin
 d none lib/svc/method 755 root bin
 f none lib/svc/method/svc-pkg-depot 755 root bin
+f none lib/svc/method/svc-pkg-mdns 755 root bin
 d none usr 755 root sys
 d none usr/bin 755 root bin
 f none usr/bin/pkg 755 root bin
@@ -122,6 +123,8 @@
 f none usr/lib/python2.6/vendor-packages/pkg/client/transport/exception.pyc 444 root bin
 f none usr/lib/python2.6/vendor-packages/pkg/client/transport/fileobj.py 444 root bin
 f none usr/lib/python2.6/vendor-packages/pkg/client/transport/fileobj.pyc 444 root bin
+f none usr/lib/python2.6/vendor-packages/pkg/client/transport/mdetect.py 444 root bin
+f none usr/lib/python2.6/vendor-packages/pkg/client/transport/mdetect.pyc 444 root bin
 f none usr/lib/python2.6/vendor-packages/pkg/client/transport/repo.py 444 root bin
 f none usr/lib/python2.6/vendor-packages/pkg/client/transport/repo.pyc 444 root bin
 f none usr/lib/python2.6/vendor-packages/pkg/client/transport/stats.py 444 root bin
@@ -322,3 +325,4 @@
 d none var/svc/manifest 755 root sys
 d none var/svc/manifest/application 755 root sys
 f none var/svc/manifest/application/pkg-server.xml 444 root sys
+f none var/svc/manifest/application/pkg-mdns.xml 444 root sys
--- a/src/setup.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/setup.py	Tue May 18 10:51:50 2010 -0700
@@ -19,8 +19,7 @@
 #
 # CDDL HEADER END
 #
-# Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
-# Use is subject to license terms.
+# Copyright (c) 2008, 2010 Oracle and/or its affiliates.  All rights reserved.
 #
 
 import errno
@@ -198,6 +197,7 @@
                 ],
         svc_method_dir: [
                 ['svc/svc-pkg-depot', 'svc-pkg-depot'],
+                ['svc/svc-pkg-mdns', 'svc-pkg-mdns'],
                 ],
         }
 
@@ -301,6 +301,7 @@
         'brand/smf_disable.conf',
         ]
 smf_files = [
+        'svc/pkg-mdns.xml',
         'svc/pkg-server.xml',
         'svc/pkg-update.xml',
         ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/svc/pkg-mdns.xml	Tue May 18 10:51:50 2010 -0700
@@ -0,0 +1,121 @@
+<?xml version="1.0"?>
+<!--
+	CDDL HEADER START
+
+	The contents of this file are subject to the terms of the
+	Common Development and Distribution License (the "License").
+	You may not use this file except in compliance with the License.
+
+	You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+	or http://www.opensolaris.org/os/licensing.
+	See the License for the specific language governing permissions
+	and limitations under the License.
+
+	When distributing Covered Code, include this CDDL HEADER in each
+	file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+	If applicable, add the following below this CDDL HEADER, with the
+	fields enclosed by brackets "[]" replaced with your own identifying
+	information: Portions Copyright [yyyy] [name of copyright owner]
+
+	CDDL HEADER END
+
+	Copyright (c) 2010 Oracle and/or its affiliates.  All rights reserved.
+
+	NOTE:  This service manifest is not editable; its contents will
+	be overwritten by package or patch operations, including
+	operating system upgrade.  Make customizations in a different
+	file.
+-->
+
+<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
+
+<service_bundle type='manifest' name=':pkg-mdns'>
+
+<service
+	name='application/pkg/dynamic-mirror'
+	type='service'
+	version='1'>
+
+	<create_default_instance enabled='false' />
+
+	<dependency
+		name='fs'
+		grouping='require_all'
+		restart_on='none'
+		type='service'>
+		<service_fmri value='svc:/system/filesystem/local' />
+	</dependency>
+
+	<!--
+	    If we're homed on an autofs mount point, then we should
+	    delay until our path becomes available.
+	-->
+	<dependency
+		name='autofs'
+		grouping='optional_all'
+		restart_on='none'
+		type='service'>
+		<service_fmri value='svc:/system/filesystem/autofs' />
+	</dependency>
+
+	<!--
+	    To ensure that the sequence IDs between two repositories are
+	    sensible, we should delay startup until we can issue correct
+	    timestamps.
+	-->
+	<dependency
+		name='ntp'
+		grouping='optional_all'
+		restart_on='none'
+		type='service'>
+		<service_fmri value='svc:/network/ntp' />
+	</dependency>
+
+	<dependency
+		name='network'
+		grouping='require_all'
+		restart_on='none'
+		type='service'>
+		<service_fmri value='svc:/milestone/network' />
+	</dependency>
+
+	<exec_method
+		type='method'
+		name='start'
+		exec='%{pkg/pkg_root}/lib/svc/method/svc-pkg-mdns %m'
+		timeout_seconds='0'>
+		<method_context>
+			<method_credential user='pkg5srv' group='pkg5srv' />
+		</method_context>
+	</exec_method>
+
+	<exec_method
+		type='method'
+		name='stop'
+		exec='%{pkg/pkg_root}/lib/svc/method/svc-pkg-mdns %m %{restarter/contract}'
+		timeout_seconds='30'>
+	</exec_method>
+
+	<property_group name='startd' type='framework'>
+		<propval name='duration' type='astring' value='child' />
+	</property_group>
+
+	<property_group name='pkg' type='application'>
+		<propval name='file_root' type='astring'
+			value='/var/pkg/download' />
+		<propval name='pkg_root' type='astring' value='/' />
+		<propval name='port' type='count' value='10000' />
+	</property_group>
+
+	<stability value='Unstable' />
+
+	<template>
+		<common_name>
+			<loctext xml:lang='C'>
+image packaging repository
+			</loctext>
+		</common_name>
+	</template>
+</service>
+
+</service_bundle>
--- a/src/svc/pkg-server.xml	Tue May 18 11:11:28 2010 +0100
+++ b/src/svc/pkg-server.xml	Tue May 18 10:51:50 2010 -0700
@@ -19,8 +19,7 @@
 
 	CDDL HEADER END
 
-	Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
-	Use is subject to license terms.
+	Copyright (c) 2009, 2010 Oracle and/or its affiliates.  All rights reserved.
 
 	NOTE:  This service manifest is not editable; its contents will
 	be overwritten by package or patch operations, including
@@ -120,6 +119,7 @@
 		<propval name='ssl_key_file' type='astring' value='none' />
 		<propval name='writable_root' type='astring' value=''/>
 		<propval name='sort_file_max_size' type='astring' value=''/>
+		<propval name='file_root' type='astring' value='' />
 	</property_group>
 
 	<property_group name='pkg_secure' type='application'>
--- a/src/svc/svc-pkg-depot	Tue May 18 11:11:28 2010 +0100
+++ b/src/svc/svc-pkg-depot	Tue May 18 10:51:50 2010 -0700
@@ -19,8 +19,8 @@
 #
 # CDDL HEADER END
 #
-# Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
-# Use is subject to license terms.
+# Copyright (c) 2009, 2010 Oracle and/or its affiliates.  All rights reserved.
+#
 
 # Load SMF constants and functions
 . /lib/svc/share/smf_include.sh
@@ -46,13 +46,14 @@
 
 	# short_option_props are properties which are communicated to the depot
 	# via a long option flag which takes an argument.
-	long_option_props="cfg_file content_root debug log_access log_errors \
-	    proxy_base sort_file_max_size ssl_cert_file ssl_dialog ssl_key_file \
-	    writable_root"
+	long_option_props="cfg_file content_root debug file_root log_access \
+	    log_errors proxy_base sort_file_max_size ssl_cert_file \
+	    ssl_dialog ssl_key_file writable_root"
 
 	set -A long_option_cmd_line "cfg-file" "content-root" "debug" \
-	    "log-access" "log-errors" "proxy-base" "sort-file-max-size" \
-	    "ssl-cert-file" "ssl-dialog" "ssl-key-file" "writable-root"
+	    "file-root" "log-access" "log-errors" "proxy-base" \
+	    "sort-file-max-size" "ssl-cert-file" "ssl-dialog" "ssl-key-file" \
+	    "writable-root"
 
 	bool_ops=""
 	option_props=""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/svc/svc-pkg-mdns	Tue May 18 10:51:50 2010 -0700
@@ -0,0 +1,176 @@
+#!/usr/bin/ksh -p
+#
+# CDDL HEADER START
+#
+# The contents of this file are subject to the terms of the
+# Common Development and Distribution License (the "License").
+# You may not use this file except in compliance with the License.
+#
+# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+# or http://www.opensolaris.org/os/licensing.
+# See the License for the specific language governing permissions
+# and limitations under the License.
+#
+# When distributing Covered Code, include this CDDL HEADER in each
+# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+# If applicable, add the following below this CDDL HEADER, with the
+# fields enclosed by brackets "[]" replaced with your own identifying
+# information: Portions Copyright [yyyy] [name of copyright owner]
+#
+# CDDL HEADER END
+#
+# Copyright (c) 2010 Oracle and/or its affiliates.  All rights reserved.
+#
+
+# Load SMF constants and functions
+. /lib/svc/share/smf_include.sh
+
+if [[ -z "$SMF_FMRI" ]]; then
+	echo "this script can only be invoked by smf(5)"
+	exit $SMF_EXIT_ERR_NOSMF
+fi
+
+case "$1" in
+'start')
+	# Handles mDNS depot startup
+
+	# short_option_props are properties which are communicated to the depot
+	# via a single character flag which takes an argument.
+	short_option_props="port"
+	set -A short_option_cmd_line "p"
+
+	long_option_props="file_root"
+
+	set -A long_option_cmd_line "file-root"
+
+	# retrieve the pkg_root property. If the variable is left empty
+	# pkg_root is /
+	pkg_root=$(svcprop -p pkg/pkg_root $SMF_FMRI)
+	if [[ $? -ne 0 ]]; then
+		echo "service property pkg/pkg_root not defined for" \
+		    "service: $SMF_FMRI"
+		exit $SMF_EXIT_ERR_CONFIG
+	fi
+
+	# make sure pkg_root ends with a /
+	echo $pkg_root | grep /$ >/dev/null
+	if [[ $? -ne 0 ]]; then
+		pkg_root="${pkg_root}/"
+	fi
+
+	# adjust the PYTHONPATH to point to the current environment
+	# we need to make sure to adjust the PYTHONPATH accordingly
+	# to a Python 2.4 or 2.6 environment
+	python_ver=$(head -1 ${pkg_root}usr/lib/pkg.depotd 2>/dev/null |
+	    awk -F/ '{print $NF}')
+	if [[ $python_ver != *python* ]]; then
+		echo "invalid python version $python_ver found in"
+		echo "${pkg_root}usr/lib/pkg.depotd"
+		exit $SMF_EXIT_ERR_FATAL
+	fi
+
+	PYTHONPATH=${pkg_root}usr/lib/${python_ver}/vendor-packages/:$PYTHONPATH
+
+	export PYTHONPATH
+
+	# Go through each property in short_option_props and, if its value is
+	# set to something other than "", add the appropriate command line
+	# flag and argument to the string.
+	cnt=0
+	for o in $short_option_props; do
+		val=$(svcprop -p pkg/$o $SMF_FMRI)
+		if [[ $? -ne 0 ]]; then
+			echo "service property pkg/$o not defined for" \
+		       	    "service: $SMF_FMRI"
+			exit $SMF_EXIT_ERR_CONFIG
+		fi
+		# If the SMF property is set to something other than 'none', add
+		# the flag and its argument to the command.
+		if [[ $val != '""' ]]; then
+			option_ops="$option_ops -${short_option_cmd_line[$cnt]} $val"
+		fi
+		cnt=$(($cnt + 1))
+	done
+
+	# Go through each property in long_option_props and, if its value is
+	# set to something other than "", add the appropriate command line
+	# flag and argument to the string.
+	cnt=0
+	for o in $long_option_props; do
+		val=$(svcprop -p pkg/$o $SMF_FMRI)
+		if [[ $? -ne 0 ]]; then
+			echo "service property pkg/$o not defined for" \
+		       	    "service: $SMF_FMRI"
+			exit $SMF_EXIT_ERR_CONFIG
+		fi
+		if [[ $val != '""' ]]; then
+			option_ops="$option_ops --${long_option_cmd_line[$cnt]}=$val"
+		fi
+		cnt=$(($cnt + 1))
+	done
+
+	# In order to run in mdns mode, we need to append the --llmirror
+	# flag to the list of command options.  Do that last, here.
+	option_ops="$option_ops --llmirror"
+
+	# Build the command to start pkg.depotd with the specified options.
+	cmd="${pkg_root}usr/lib/pkg.depotd $option_ops"
+	# Echo the command so that the log contains the command used to start
+	# the depot.
+	echo $cmd
+
+	exec $cmd
+
+	;;
+
+'stop')
+	#
+	# Strategy: First, try shutting down depot using polite kill.  Use up
+	# as much as possible of the allotted timeout period waiting for polite
+	# kill to take effect.  As time runs out, try a more aggressive kill.
+	#
+	SVC_TIMEOUT=`svcprop -p stop/timeout_seconds $SMF_FMRI`
+	if [[ $? -ne 0 ]]; then
+		echo "service property stop/timeout_seconds not defined" \
+		    "for service: $SMF_FMRI"
+		exit $SMF_EXIT_ERR_CONFIG
+	fi
+
+	#
+	# Note that we're working around an oddity in smf_kill_contract: it
+	# waits in 5 second chunks and can overshoot the specified timeout
+	# by as many as 4 seconds.  Example: a specified wait of 6 will result
+	# in a wait of 10 seconds in reality.  Since we may potentially do a
+	# first kill and then a second, we must ensure that at least 8 seconds
+	# of slop is left in reserve.  To be paranoid, we go for 10.
+	#
+	((POLITE=$SVC_TIMEOUT - 10))
+	if [[ $POLITE -gt 0 ]]; then
+		smf_kill_contract $2 TERM 1 $POLITE
+		ret=$?
+		# '2' indicates timeout with non-empty contract.
+		if [[ $ret -eq 2 ]]; then
+			echo "Gentle contract kill timed out after"
+		       	    "$POLITE seconds, trying SIGKILL." >&2
+			#
+			# Again, despite the specified timeout, this will
+			# take a minimum of 5 seconds to complete.
+			#
+			smf_kill_contract $2 KILL 1 1
+			if [[ $ret -ne 0 ]]; then
+				exit $SMF_EXIT_ERR_FATAL
+			fi
+		fi
+	else
+		# If the timeout is too short, we just try once, politely.
+		smf_kill_contract $2 TERM
+	fi
+	;;
+
+*)
+	echo "Usage: $0 { start | stop }"
+	exit $SMF_EXIT_ERR_CONFIG
+	;;
+
+esac
+exit $SMF_EXIT_OK
--- a/src/tests/api/t_api_search.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/tests/api/t_api_search.py	Tue May 18 10:51:50 2010 -0700
@@ -1523,6 +1523,7 @@
                 self._search_op(api_obj, remote, 'unique_dir',
                     self.res_space_unique)
                 remote = True
+                time.sleep(1)
                 self._search_op(api_obj, remote, 'with', set())
                 self._search_op(api_obj, remote, 'with*',
                     self.res_space_with_star)
@@ -1762,7 +1763,7 @@
                 tok_file = os.path.join(ind_dir, ss.BYTE_OFFSET_FILE)
                 main_file = os.path.join(ind_dir, ss.MAIN_FILE)
                 self.pkgsend_bulk(durl, self.example_pkg10)
-                time.sleep(1)
+                time.sleep(2)
                 fh = open(tok_file)
                 tok_1 = fh.readlines()
                 tok_len = len(tok_1)
@@ -1771,7 +1772,7 @@
                 main_1 = fh.readlines()
                 main_len = len(main_1)
                 self.pkgsend_bulk(durl, self.example_pkg10, optional=False)
-                time.sleep(1)
+                time.sleep(2)
                 fh = open(tok_file)
                 tok_2 = fh.readlines()
                 new_tok_len = len(tok_2)
@@ -2117,7 +2118,7 @@
                 self._run_remote_empty_tests(api_obj)
                 os.rmdir(tmp_dir)
                 self.pkgsend_bulk(durl, self.example_pkg10)
-                time.sleep(1)
+                time.sleep(2)
                 self._run_remote_tests(api_obj)
                 self._search_op(api_obj, True, "unique_dir",
                     self.res_space_unique)
--- a/src/tests/cli/t_pkg_depotd.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/tests/cli/t_pkg_depotd.py	Tue May 18 10:51:50 2010 -0700
@@ -322,6 +322,38 @@
                 shutil.rmtree(dpath)
                 self.dc.set_repodir(opath)
 
+        def test_repo_file_only(self):
+                """Test that if a depot is created with only --file-root
+                supplied, it comes up in mirror mode, with only file content
+                available."""
+
+                if self.dc.is_alive():
+                        self.dc.stop()
+
+                fpath = "/var/pkg/download"
+                opath = self.dc.get_repodir()
+                self.dc.set_repodir(None)
+                self.dc.set_file_root(fpath)
+                self.dc.set_readwrite()
+                self.dc.start()
+                self.assert_(self.dc.is_alive())
+
+                durl = self.dc.get_depot_url()
+                verdata = urllib2.urlopen("%s/versions/0/" % durl)
+                verlines = verdata.readlines()
+                verdict = dict(
+                    s.split(None, 1)
+                    for s in (l.strip() for l in verlines)
+                )
+
+                self.assert_("file" in verdict)
+                self.assert_("catalog" not in verdict)
+                self.assert_("manifest" not in verdict)
+
+                self.dc.stop()
+                self.dc.set_repodir(opath)
+                self.dc.set_file_root(None)
+
 
 class TestDepotController(pkg5unittest.CliTestCase):
 
--- a/src/tests/cli/t_pkg_install.py	Tue May 18 11:11:28 2010 +0100
+++ b/src/tests/cli/t_pkg_install.py	Tue May 18 10:51:50 2010 -0700
@@ -298,6 +298,24 @@
                 self.pkg("list")
                 self.pkg("list [email protected] [email protected] [email protected]")
 
+        def test_basics_mdns(self):
+                """ Send empty package [email protected], install and uninstall """
+
+                self.pkgsend_bulk(self.rurl, self.foo11)
+                self.image_create(self.rurl)
+
+                self.pkg("set-property mirror-discovery True")
+                self.pkg("list -a")
+                self.pkg("list", exit=1)
+
+                self.pkg("install foo")
+
+                self.pkg("list")
+                self.pkg("verify")
+
+                self.pkg("uninstall foo")
+                self.pkg("verify")
+
         def test_image_upgrade(self):
                 """ Send package [email protected], dependent on [email protected].  Install
                     [email protected].  List all packages.  Upgrade image. """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/util/distro-import/140/common/package:pkg	Tue May 18 10:51:50 2010 -0700
@@ -0,0 +1,19 @@
+package package/pkg
+consolidation "ips"
+classification "System/Packaging"
+import SUNWipkg
+add group groupname=pkg5srv gid=97
+add user username=pkg5srv uid=97 group=pkg5srv gcos-field="pkg(5) server UID"
+depend library/python-2/[email protected]
+depend library/python-2/[email protected]
+depend library/python-2/[email protected]
+depend library/python-2/[email protected]
+depend library/python-2/[email protected]
+depend library/python-2/simplejson-26
+end package
+
+package SUNWipkg
+consolidation "ips"
+renamed 133
+depend package/pkg
+end package