basic image and package plans
authorStephen Hahn <sch@sun.com>
Thu, 21 Jun 2007 23:43:12 -0700
changeset 50 bfcb1661f019
parent 49 c3a70bdc4527
child 51 d04d55e16d0b
basic image and package plans various annotations
.hgignore
doc/TODO
doc/rfes.txt
src/Makefile
src/client.py
src/depot.py
src/modules/bundle/SolarisPackageDatastreamBundle.py
src/modules/bundle/TarBundle.py
src/modules/catalog.py
src/modules/client/image.py
src/modules/client/imageplan.py
src/modules/client/pkgplan.py
src/modules/client/retrieve.py
src/modules/fmri.py
src/modules/manifest.py
src/modules/server/config.py
src/modules/server/transaction.py
src/modules/sysvpkg.py
--- a/.hgignore	Wed Jun 20 10:40:03 2007 -0700
+++ b/.hgignore	Thu Jun 21 23:43:12 2007 -0700
@@ -1,9 +1,10 @@
 # Ignore compiled byte code, lint output, object files, shared objects,
-# and the entire proto area.
+# VIM swap files, and the entire proto area.
 \.pyc
 \.ln
 \.o
 \.so
+\.sw.
 ^proto
 # Specific build products
 ^src/pkg$
--- a/doc/TODO	Wed Jun 20 10:40:03 2007 -0700
+++ b/doc/TODO	Thu Jun 21 23:43:12 2007 -0700
@@ -5,6 +5,10 @@
 
 Code
 - need to outline ELF inspection module
+	- ali has constructed a simple C example for extracting run
+	paths
+	http://blogs.sun.com/ali/entry/changing_elf_runpaths
+
 - need to start constructing placeholder classes/methods for various
   pieces
 	- list of pieces
--- a/doc/rfes.txt	Wed Jun 20 10:40:03 2007 -0700
+++ b/doc/rfes.txt	Thu Jun 21 23:43:12 2007 -0700
@@ -28,3 +28,9 @@
     could be treated as choking, or we could always examine intermediate
     manifests.
 
+9.  [psa]  Take a snapshot [of each affected filesystem] between every
+    package update operation in a larger image transaction, as opposed
+    to at the image transaction boundaries only.
+
+10.  [sch]  Examine use of alternative, HTTP 1.1-friendly URL loading
+     modules.  (Example:  Duke's urlgrabber.)
--- a/src/Makefile	Wed Jun 20 10:40:03 2007 -0700
+++ b/src/Makefile	Thu Jun 21 23:43:12 2007 -0700
@@ -116,7 +116,10 @@
 	ln -s $(PWD)/modules /usr/lib/python2.4/vendor-packages/pkg
 
 # Invoke all known modules with tests.
+# XXX Invoke the bundle tests.
 test:
+	# $(PYTHON) $(LINKPYTHONPKG)/bundle/__init__.py a_sysv_pkg
+	# $(PYTHON) $(LINKPYTHONPKG)/bundle/__init__.py a_tarball
 	$(PYTHON) $(LINKPYTHONPKG)/misc.py
 	$(PYTHON) $(LINKPYTHONPKG)/version.py
 	$(PYTHON) $(LINKPYTHONPKG)/fmri.py
--- a/src/client.py	Wed Jun 20 10:40:03 2007 -0700
+++ b/src/client.py	Thu Jun 21 23:43:12 2007 -0700
@@ -62,6 +62,7 @@
 import pkg.version as version
 
 import pkg.client.image as image
+import pkg.client.imageplan as imageplan
 
 def usage():
         print """\
@@ -128,66 +129,16 @@
         image.reload_catalogs()
         image.display_catalogs()
 
-def pattern_install(config, image, pattern, strict):
-        # check catalogs for this pattern; None is the representation of the
-        # freezes
-        matches = image.get_matching_pkgs(pattern)
-
-        if len(matches) == 0:
-                raise NameError, "pattern '%s' has no matching packages" % \
-                    pattern
-
-        matches.sort()
-
-        # If there is more than one package in the list, then we've got an
-        # ambiguous outcome, and need to exit.
-
-        l = matches[0]
-        for p in matches:
-                if p.is_same_pkg(l):
-                        l = p
-                        continue
-
-                raise KeyError, "pattern %s led to multiple packages" % pattern
-
-
-        # pick appropriate version, based on request and freezes
-        p = max(matches)
-        #   XXX can we do this with the is_successor()/version() and a map?
-        #   warn on dependencies; exit if --strict
-
-        image.retrieve_manifest(None, p)
-
-        # do we have this manifest?
-        #   get it if not
-        # request manifest
-        # examine manifest for dependencies
-        #   if satisfied by inventory, continue
-        #   if satisfied by pending transaction, continue
-        #   if unsatisfied, then
-        #     if optional, then evaluate client's optional policy to either
-        #     treat-as-required, treat-as-required-unless-pinned, ignore
-        #     skip if ignoring
-        #     if pinned
-        #       ignore if treat-as-required-unless-pinned
-        #     else
-        #       **evaluation of incorporations**
-        #     pursue installation of this package
-        # examine manifest for files
-        #
-        # request files
-
-        return # a client operation
-
-
 
 def install(config, image, args):
-        """Attempt to take package specified to INSTALLED state."""
-        strict = False
-        oplist = []
+        """Attempt to take package specified to INSTALLED state.  The operands
+        are interpreted as glob patterns.
 
+        XXX Authority-catalog issues."""
         opts = None
         pargs = None
+        error = 0
+
         if len(args) > 0:
                 opts, pargs = getopt.getopt(args, "S")
 
@@ -197,22 +148,38 @@
 
         image.reload_catalogs()
 
+        ip = imageplan.ImagePlan(image)
+
         for ppat in pargs:
-                ops = None
+                rpat = re.sub("\*", ".*", ppat)
+                rpat = re.sub("\?", ".", rpat)
 
                 try:
-                        ops = pattern_install(config, image, ppat, strict)
+                        matches = image.get_regex_matching_fmris(rpat)
                 except KeyError:
-                        # okay skip this argument
-                        print "pkg: ambiguous package pattern '%s'" % ppat
-                except NameError:
-                        print "pkg: unknown package pattern '%s'" % ppat
+                        print """\
+pkg: no package matching '%s' could be found in current catalog
+     suggest relaxing pattern, refreshing and/or examining catalogs""" % ppat
+                        error = 1
+                        continue
+
+                pnames = {}
+                for m in matches:
+                        pnames[m[1].get_pkg_stem()] = 1
 
-                if ops != None:
-                        oplist.append(ops)
+                if len(pnames.keys()) > 1:
+                        print "pkg: '%s' matches multiple packages" % ppat
+                        for k in pnames.keys():
+                                print "\t%s" % k
+                        continue
 
+                ip.propose_fmri(m[1])
 
-        # perform update transaction as given in oplist
+        if error != 0:
+                sys.exit(error)
+
+        ip.evaluate()
+        ip.execute()
 
         return
 
@@ -259,12 +226,9 @@
 
 # XXX need an Image configuration by default
 icfg = image.Image()
-icfg.find_parent()
 pcfg = config.ParentRepo("http://localhost:10000", ["http://localhost:10000"])
 
 if __name__ == "__main__":
-
-
         opts = None
         pargs = None
         try:
@@ -282,6 +246,12 @@
 
         # Handle PKG_IMAGE and PKG_SERVER environment variables.
 
+        if subcommand == "image":
+                create_image(pcfg, pargs)
+                sys.exit(0)
+
+        icfg.find_parent()
+
         if subcommand == "refresh":
                 catalog_refresh(pcfg, icfg, pargs)
         elif subcommand == "catalog":
@@ -294,8 +264,6 @@
                 freeze(pcfg, icfg, pargs)
         elif subcommand == "unfreeze":
                 unfreeze(pcfg, icfg, pargs)
-        elif subcommand == "image":
-                create_image(pcfg, pargs)
         else:
                 print "pkg: unknown subcommand '%s'" % subcommand
                 usage()
--- a/src/depot.py	Wed Jun 20 10:40:03 2007 -0700
+++ b/src/depot.py	Thu Jun 21 23:43:12 2007 -0700
@@ -25,6 +25,22 @@
 
 # pkg.depotd - package repository daemon
 
+# XXX The prototype pkg.depotd combines both the version management server that
+# answers to pkgsend(1) sessions and the HTTP file server that answers to the
+# various GET operations that a pkg(1) client makes.  This split is expected to
+# be made more explicit, by constraining the pkg(1) operations such that they
+# can be served as a typical HTTP/HTTPS session.  Thus, pkg.depotd will reduce
+# to a special purpose HTTP/HTTPS server explicitly for the version management
+# operations, and must manipulate the various state files--catalogs, in
+# particular--such that the pkg(1) pull client can operately accurately with
+# only a basic HTTP/HTTPS server in place.
+
+# XXX We should support simple "last-modified" operations via HEAD queries.
+
+# XXX Although we pushed the evaluation of next-version, etc. to the pull
+# client, we should probably provide a query API to do same on the server, for
+# dumb clients (like a notification service).
+
 import BaseHTTPServer
 import getopt
 import os
@@ -50,7 +66,7 @@
 Usage: /usr/lib/pkg.depotd [-n]
 """
 
-def catalog(scfg, request):
+def catalog_get(scfg, request):
         scfg.inc_catalog()
 
         request.send_response(200)
@@ -58,64 +74,22 @@
         request.end_headers()
         request.wfile.write("%s" % scfg.catalog)
 
-def manifest(scfg, request):
+def manifest_get(scfg, request):
         """The request is an encoded pkg FMRI.  If the version is specified
-        incompletely, we return the latest matching manifest.  The matched
-        version is returned in the headers.  If an incomplete FMRI is received,
-        the client is expected to have provided a build release in the request
-        headers.
-
-        Legitimate requests are
-
-        /manifest/[URL]
-        /manifest/branch/[URL]
-        /manifest/release/[URL]
-
-        allowing the request of the next matching version, based on client
-        constraints."""
+        incompletely, we return an error, as the client is expected to form
+        correct requests, based on its interpretation of the catalog and its
+        image policies."""
 
         scfg.inc_manifest()
 
-        constraint = None
-
         # Parse request into FMRI component and decode.
         m = re.match("^/manifest/(.*)", request.path)
         pfmri = urllib.unquote(m.group(1))
 
-        # Look for exact match.
-        # XXX XXX
-
-        # Determine closest version.
-        try:
-                vs = scfg.catalog.get_matching_pkgs(pfmri, constraint)
-        except KeyError, e:
-                msg = "manifest request failed: '%s'" % pfmri
-                request.log_message(msg)
-
-                request.send_response(500)
-                request.send_header('Content-type', 'text/plain')
-                request.end_headers()
-
-                return
-
-        msg = "manifest request '%s': " % pfmri
-        for v in vs:
-                msg = msg + "%s " % v
-        request.log_message(msg)
-
-#       XXX XXX
-#        if len(vs) > 1:
-#                request.log_message("multiple manifest result ambiguous")
-#                request.send_response(400)
-#                request.send_header('Content-type', 'text/plain')
-#                request.end_headers()
-#
-#                return
-
-        p = vs[0]
+        f = fmri.PkgFmri(pfmri, None)
 
         # Open manifest and send.
-        file = open("%s/%s" % (scfg.pkg_root, p.get_dir_path()), "r")
+        file = open("%s/%s" % (scfg.pkg_root, f.get_dir_path()), "r")
         data = file.read()
 
         request.send_response(200)
@@ -123,7 +97,7 @@
         request.end_headers()
         request.wfile.write(data)
 
-def get_file(scfg, request):
+def file_get_single(scfg, request):
         """The request is the SHA-1 hash name for the file."""
         scfg.inc_file()
 
@@ -194,11 +168,11 @@
         def do_GET(self):
                 # Client APIs
                 if re.match("^/catalog$", self.path):
-                        catalog(scfg, self)
+                        catalog_get(scfg, self)
                 elif re.match("^/manifest/.*$", self.path):
-                        manifest(scfg, self)
+                        manifest_get(scfg, self)
                 elif re.match("^/file/.*$", self.path):
-                        get_file(scfg, self)
+                        file_get_single(scfg, self)
 
                 # Publisher APIs
                 elif re.match("^/open/(.*)$", self.path):
--- a/src/modules/bundle/SolarisPackageDatastreamBundle.py	Wed Jun 20 10:40:03 2007 -0700
+++ b/src/modules/bundle/SolarisPackageDatastreamBundle.py	Thu Jun 21 23:43:12 2007 -0700
@@ -43,6 +43,7 @@
 }
 
 class SolarisPackageDatastreamBundle(object):
+        """XXX Need a class comment."""
 
         def __init__(self, filename):
                 self.pkg = SolarisPackage(filename)
--- a/src/modules/bundle/TarBundle.py	Wed Jun 20 10:40:03 2007 -0700
+++ b/src/modules/bundle/TarBundle.py	Thu Jun 21 23:43:12 2007 -0700
@@ -36,7 +36,8 @@
 
         def __init__(self, filename):
                 self.tf = tarfile.open(filename)
-                # XXX This could be more intelligent.  Or get user input.
+                # XXX This could be more intelligent.  Or get user input.  Or
+                # extend API to take FMRI.
                 self.pkgname = os.path.basename(filename)
 
         def __del__(self):
--- a/src/modules/catalog.py	Wed Jun 20 10:40:03 2007 -0700
+++ b/src/modules/catalog.py	Thu Jun 21 23:43:12 2007 -0700
@@ -42,12 +42,17 @@
         incorporation relationships between packages.  This latter section
         allows the graph to be topologically sorted by the client.
 
+        S Last-Modified: [timespec]
+
         XXX A authority mirror-uri ...
         XXX ...
 
         V fmri
         V fmri
         ...
+        C fmri
+        C fmri
+        ...
         I fmri fmri
         I fmri fmri
         ...
@@ -60,14 +65,24 @@
 
         XXX self.pkgs should be a dictionary, accessed by fmri string (or
         package name).  Current code is O(N_packages) O(M_versions), should be
-        O(1) O(M_versions), and possibly O(1) O(1).
+        O(1) O(M_versions), and possibly O(1) O(1).  Likely a similar need for
+        critical_pkgs.
+
+        XXX Initial estimates suggest that the Catalog could be composed of 1e5
+        - 1e7 lines.  Catalogs across these magnitudes will need to be spread
+        out into chunks, and may require a delta-oriented update interface.
         """
 
         def __init__(self):
                 self.catalog_root = ""
 
+                self.attrs = {}
+                self.attrs["Last-Modified"] = time.time()
+
                 self.authorities = {}
                 self.pkgs = []
+                self.critical_pkgs = []
+
                 self.relns = {}
 
         def add_authority(self, authority, urls):
@@ -87,11 +102,19 @@
                 for entry in centries:
                         # each V line is an fmri
                         m = re.match("^V (pkg:[^ ]*)", entry)
-                        if m == None:
-                                continue
+                        if m != None:
+                                pname = m.group(1)
+                                self.add_fmri(fmri.PkgFmri(pname, None))
 
-                        pname = m.group(1)
-                        self.add_fmri(fmri.PkgFmri(pname, None))
+                        m = re.match("^S ([^:]*): (.*)", entry)
+                        if m != None:
+                                self.attrs[m.group(1)] = m.group(2)
+
+                        m = re.match("^C (pkg:[^ ]*)", entry)
+                        if m != None:
+                                pname = m.group(1)
+                                self.critical_pkgs.append(fmri.PkgFmri(pname,
+                                    None))
 
         def add_fmri(self, pkgfmri):
                 name = pkgfmri.get_pkg_stem()
@@ -106,7 +129,7 @@
                 pkg.add_version(pkgfmri)
                 self.pkgs.append(pkg)
 
-        def add_pkg(self, pkg):
+        def add_pkg(self, pkg, critical = False):
                 for opkg in self.pkgs:
                         if pkg.fmri == opkg.fmri:
                                 #
@@ -118,30 +141,47 @@
                                 return
 
                 self.pkgs.append(pkg)
-
-        def add_package_fmri(self, pkg_fmri):
-                return
-
-        def delete_package_fmri(self, pkg_fmri):
-                return
+                if critical:
+                        self.critical_pkgs.append(pkg)
 
         def get_matching_pkgs(self, pfmri, constraint):
-                """Iterate through the catalog's, looking for an fmri match."""
+                """Iterate through the catalogs, looking for an exact fmri
+                match.  XXX Returns a list of Package objects."""
 
-                # XXX FMRI-based implementation doesn't do pattern matching, but
-                # exact matches only.
                 pf = fmri.PkgFmri(pfmri, None)
-
                 for pkg in self.pkgs:
                         if pkg.fmri.is_similar(pf):
                                 return pkg.matching_versions(pfmri, constraint)
 
-                raise KeyError, "%s not found in catalog" % pfmri
+                raise KeyError, "FMRI '%s' not found in catalog" % pfmri
+
+        def get_regex_matching_fmris(self, regex):
+                """Iterate through the catalogs, looking for a regular
+                expression match.  Returns an unsorted list of PkgFmri
+                objects."""
+
+                ret = []
+
+                for p in self.pkgs:
+                        for pv in p.pversions:
+                                fn = "%s@%s" % (p.fmri, pv.version)
+                                if re.search(regex, fn):
+                                        ret.append(fmri.PkgFmri(fn, None))
+
+                if ret == []:
+                        raise KeyError, "pattern '%s' not found in catalog" \
+                            % regex
+
+                return ret
 
         def __str__(self):
                 s = ""
+                for a in self.attrs.keys():
+                        s = s + "S %s: %s\n" % (a, self.attrs[a])
                 for p in self.pkgs:
                         s = s + p.get_catalog_entry()
+                for c in self.critical_pkgs:
+                        s = s + "C %s\n" % p
                 for r in self.relns:
                         s = s + "I %s\n" % r
                 return s
@@ -186,3 +226,11 @@
 
                 for p in c.get_matching_pkgs(tp, None):
                         print "  ", p
+
+        for p in c.get_regex_matching_fmris("test"):
+                print p
+
+        try:
+            l = c.get_regex_matching_fmris("flob")
+        except KeyError:
+            print "correctly determined no match for 'flob'"
--- a/src/modules/client/image.py	Wed Jun 20 10:40:03 2007 -0700
+++ b/src/modules/client/image.py	Thu Jun 21 23:43:12 2007 -0700
@@ -28,6 +28,7 @@
 import urllib
 
 import pkg.catalog as catalog
+import pkg.manifest as manifest
 
 IMG_ENTIRE = 0
 IMG_PARTIAL = 1
@@ -86,6 +87,11 @@
                 self.catalogs = {}
                 self.authorities = {}
 
+                self.attrs = {}
+
+                self.attrs["Policy-Require-Optional"] = False
+                self.attrs["Policy-Pursue-Latest"] = True
+
         def find_parent(self):
                 # Ascend from current working directory to find first
                 # encountered image.
@@ -150,56 +156,54 @@
                 for c in self.catalogs.values():
                         c.display()
 
-        def get_matching_pkgs(self, pattern):
-                """The pattern is a glob pattern, which we translate to an RE
-                pattern.
-
-                XXX This is going to need to return (catalog, fmri) pairs."""
+        def get_matching_pkgs(self, pfmri):
+                """Exact matches to the given FMRI.  Returns a list of (catalog,
+                pkg) pairs."""
 
                 m = []
                 for c in self.catalogs.values():
-                        m.extend(c.get_matching_pkgs(pattern, None))
+                        m.extend([(c, p) \
+                            for p in c.get_matching_pkgs(pfmri, None)])
 
                 return m
 
-        def retrieve_manifest(self, catalog, fmri):
-                """Turn FMRI to Image's path to manifest.  If present, return.
-                If not present, calculate URI and retrieve.
+        def get_regex_matching_fmris(self, regex):
+                """FMRIs matching the given regular expression.  Returns of a
+                list of (catalog, PkgFmri) pairs."""
 
-                XXX Which catalog did we fetch this fmri from?"""
+                m = []
+                for c in self.catalogs.values():
+                        m.extend([(c, p) \
+                            for p in c.get_regex_matching_fmris(regex)])
 
-                authority, pkg_name, version = fmri.tuple()
+                return m
 
+        def has_manifest(self, fmri):
                 mpath = fmri.get_dir_path()
 
                 local_mpath = "%s/pkg/%s/manifest" % (self.imgdir, mpath)
 
                 if (os.path.exists(local_mpath)):
-                        print "short circuit retrieve"
-                        return local_mpath # or return object?
+                        return True
+
+                return False
 
-                # XXX convert authority reference to server
-                if authority == None:
-                        authority = "localhost:10000"
-                url_mpath = "http://%s/manifest/%s" % (authority,
-                                fmri.get_url_path())
+        def get_manifest(self, fmri):
+                m = manifest.Manifest()
+                mcontent = file("%s/pkg/%s/manifest" % 
+                    (self.imgdir, fmri.get_dir_path())).read()
+                m.set_content(mcontent)
+                return m
 
-                print url_mpath
-                try:
-                        m = urllib.urlopen(url_mpath)
-                except:
-                        raise NameError, "could not open %s" % url_mpath
+        def get_version_installed(self, fmri):
+                istate = "%s/pkg/%s/installed" % (self.imgdir, fmri.get_dir_path(True))
+                if not os.path.exists(istate):
+                        raise LookupError, "no installed version of '%s'" % fmri
 
-                data = m.read()
-                print local_mpath
-                try:
-                        mfile = file(local_mpath, "w")
-                        print >>mfile, data
-                except IOError, e:
-                        os.makedirs(os.path.dirname(local_mpath))
-                        mfile = file(local_mpath, "w")
-                        print >>mfile, data
+                vs = os.readlink(istate)
+
+                return fmri.PkgFmri("%s/%s" % (fmri, vs))
 
-                return local_mpath
-
-
+if __name__ == "__main__":
+        # XXX Need to construct a trivial image and catalog.
+        pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/modules/client/imageplan.py	Thu Jun 21 23:43:12 2007 -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 2007 Sun Microsystems, Inc.  All rights reserved.
+# Use is subject to license terms.
+
+import os
+import re
+import urllib
+
+import pkg.catalog as catalog
+import pkg.fmri as fmri
+
+import pkg.client.pkgplan as pkgplan
+import pkg.client.retrieve as retrieve # XXX inventory??
+
+UNEVALUATED = 0
+EVALUATED_OK = 1
+EVALUATED_ERROR = 2
+EXECUTED_OK = 3
+EXECUTED_ERROR = 4
+
+class ImagePlan(object):
+        """An image plan takes a list of requested packages, an Image (and its
+        policy restrictions), and returns the set of package operations needed
+        to transform the Image to the list of requested packages.
+
+        Use of an ImagePlan involves the identification of the Image, the
+        Catalogs (implicitly), and a set of complete or partial package FMRIs.
+        The Image's policy, which is derived from its type and configuration
+        will cause the formulation of the plan or an exception state.
+
+        XXX In the current formulation, an ImagePlan can handle [null ->
+        PkgFmri] and [PkgFmri@Version1 -> PkgFmri@Version2], for a set of
+        PkgFmri objects.  With a correct Action object definition, deletion
+        should be able to be represented as [PkgFmri@V1 -> null].
+
+        XXX Should we allow downgrades?  There's an "arrow of time" associated
+        with the smf(5) configuration method, so it's better to direct
+        manipulators to snapshot-based rollback, but if people are going to do
+        "pkg delete fmri; pkg install fmri@v(n - 1)", then we'd better have a
+        plan to identify when this operation is safe or unsafe."""
+
+        def __init__(self, image):
+                self.image = image
+                self.goal_fmris = []
+                self.state = UNEVALUATED
+
+                self.target_fmris = []
+                self.pkg_plans = []
+
+        def __str__(self):
+                if self.state == UNEVALUATED:
+                        s = "UNEVALUATED: "
+                        for t in self.target_fmris:
+                                s = s + "%s\n" % t
+                        return s
+
+                for pp in self.pkg_plans:
+                        s = s + "%s\n" % pp
+                return s
+
+        def set_goal_pkg_fmris(self, pflist):
+                self.goal_pkg_fmris = pflist
+
+        def propose_fmri(self, fmri):
+                # is a version of fmri.stem in the inventory?
+                #   is there a freeze or incorporation statement?
+                #   do any of them eliminate this fmri version?
+                #     discard
+                # is a version of fmri in our target_fmris?
+                n = range(len(self.target_fmris))
+                if n == []:
+                        self.target_fmris.append(fmri)
+                        return
+
+                for i in n:
+                        p = self.target_fmris[i]
+                        if fmri.is_same_pkg(p):
+                                if fmri > p:
+                                        self.target_fmris[i] = fmri
+                                        break
+
+                return
+
+        def evaluate_fmri(self, fmri):
+                # [image] do we have this manifest?
+                if not self.image.has_manifest(fmri):
+                        retrieve.get_manifest(self.image, fmri)
+
+                m = self.image.get_manifest(fmri)
+
+                # [manifest] examine manifest for dependencies
+                #   [image] if satisfied by inventory, continue
+                #   [imageplan] if satisfied by pending transaction, continue
+                #   if unsatisfied, then
+                #     if optional, then evaluate client's optional policy to either
+                #     treat-as-required, treat-as-required-unless-pinned, ignore
+                #     skip if ignoring
+                #     if pinned
+                #       ignore if treat-as-required-unless-pinned
+                #     else
+                #       **evaluation of incorporations**
+                #     [imageplan] pursue installation of this package -->
+                #     backtrack or reset??
+
+                pp = pkgplan.PkgPlan(self.image)
+
+                pp.propose_destination(fmri, m)
+
+                pp.evaluate()
+
+                self.pkg_plans.append(pp)
+
+        def evaluate(self):
+                assert self.state == UNEVALUATED
+
+                for f in self.target_fmris:
+                        self.evaluate_fmri(f)
+
+                self.state = EVALUATED_OK
+
+        def execute(self):
+                """Invoke the evaluated image plan, constructing each package
+                plan."""
+                assert self.state == EVALUATED_OK
+
+                # image related operations, like a snapshot
+
+                for p in self.pkg_plans:
+                        p.preexecute()
+
+                for p in self.pkg_plans:
+                        # per-package image operations (further snapshots)
+                        p.execute()
+
+                for p in self.pkg_plans:
+                        p.postexecute()
+
+                self.state = EXECUTED_OK
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/modules/client/pkgplan.py	Thu Jun 21 23:43:12 2007 -0700
@@ -0,0 +1,108 @@
+#!/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 2007 Sun Microsystems, Inc.  All rights reserved.
+# Use is subject to license terms.
+
+import os
+import re
+import urllib
+
+import pkg.catalog as catalog
+
+class PkgPlan(object):
+        """A package plan takes two package FMRIs and an Image, and produces the
+        set of actions required to take the Image from the origin FMRI to the
+        destination FMRI."""
+
+        def __init__(self, image):
+                self.origin_fmri = None
+                self.destination_fmri = None
+                self.origin_mfst = None
+                self.destination_mfst = None
+
+                self.image = image
+
+                self.actions = []
+
+        def set_origin(self, fmri):
+                self.origin_fmri = fmri
+                self.origin_mfst = manifest.retrieve(fmri)
+
+        def propose_destination(self, fmri, manifest):
+                self.destination_fmri = fmri
+                self.destination_mfst = manifest
+
+        def is_valid(self):
+                if self.origin_fmri == None:
+                        return True
+
+                if not self.origin_fmri.is_same_pkg(self.destination_fmri):
+                        return False
+
+                if self.origin_fmri > self.destination_fmri:
+                        return False
+
+                return True
+
+        def get_actions(self):
+                return []
+
+        def evaluate(self):
+                # if origin unset, determine if we're dealing with an previously
+                # installed version or if we're dealing with the null package
+                f = None
+                if self.origin_fmri == None:
+                        try:
+                                f = self.image.get_version_installed(self.destination_fmri)
+                        except LookupError:
+                                pass
+
+                # if null package, then our plan is the set of actions for the
+                # destination version
+                if f == None:
+                        self.actions = self.destination_mfst.actions
+                else:
+                        # if a previous package, then our plan is derived from the
+                        # set differences between the previous manifest's actions and
+                        # the union of the destination manifest's actions with the
+                        # critical actions of the critical versions in the version
+                        # interval between origin and destination.
+                        self.actions = self.destination_mfst.difference(
+                            self.origin_mfst)
+                return
+
+        def preexecute(self):
+                # retrieval step
+                return
+
+        def execute(self):
+                # record that we are in an intermediate state
+                # mv step
+                # XXX
+                for a in self.actions:
+                        print a
+                return
+
+        def postexecute(self):
+                # record that package states are consistent
+                return
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/modules/client/retrieve.py	Thu Jun 21 23:43:12 2007 -0700
@@ -0,0 +1,101 @@
+#!/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 2007 Sun Microsystems, Inc.  All rights reserved.
+# Use is subject to license terms.
+#
+
+import getopt
+import httplib
+import os
+import re
+import sys
+import urllib
+import urllib2
+import urlparse
+
+# client/retrieve.py - collected methods for retrieval of pkg components
+# from repositories
+
+def url_catalog(config, image, args):
+        """XXX will need to show available content series for each package"""
+        croot = image.imgdir
+
+        if len(args) != 0:
+                print "pkg: catalog subcommand takes no arguments"
+                usage()
+
+        # Ensure Image directory structure is valid.
+        if not os.path.isdir("%s/catalog" % croot):
+                image.mkdirs()
+
+        # GET /catalog
+        for repo in pcfg.repo_uris:
+                # Ignore http_proxy for localhost case, by overriding default
+                # proxy behaviour of urlopen().
+                proxy_uri = None
+                netloc = urlparse.urlparse(repo)[1]
+                if urllib.splitport(netloc)[0] == "localhost":
+                        proxy_uri = {}
+
+                uri = urlparse.urljoin(repo, "catalog")
+
+                c = urllib.urlopen(uri, proxies=proxy_uri)
+
+                # compare headers
+                data = c.read()
+                fname = urllib.quote(c.geturl(), "")
+
+                # Filename should be reduced to host\:port
+                cfile = file("%s/catalog/%s" % (croot, fname), "w")
+                print >>cfile, data
+
+def get_manifest(image, fmri):
+        """Calculate URI and retrieve.
+        XXX Authority-catalog issues."""
+
+        authority, pkg_name, version = fmri.tuple()
+
+        # XXX convert authority reference to server
+        if authority == None:
+                authority = "localhost:10000"
+
+        url_mpath = "http://%s/manifest/%s" % (authority,
+            fmri.get_url_path())
+
+        try:
+                m = urllib.urlopen(url_mpath)
+        except:
+                raise NameError, "could not open %s" % url_mpath
+
+        data = m.read()
+        local_mpath = "%s/pkg/%s/manifest" % (image.imgdir, fmri.get_dir_path())
+
+        try:
+                mfile = file(local_mpath, "w")
+                print >>mfile, data
+        except IOError, e:
+                os.makedirs(os.path.dirname(local_mpath))
+                mfile = file(local_mpath, "w")
+                print >>mfile, data
+
+def url_file(uri, dest):
+        return
--- a/src/modules/fmri.py	Wed Jun 20 10:40:03 2007 -0700
+++ b/src/modules/fmri.py	Thu Jun 21 23:43:12 2007 -0700
@@ -95,12 +95,6 @@
 
                         return
 
-        def get_authority(self):
-                return self.authority
-
-        def set_authority(self, authority):
-                self.authority = authority
-
         def __str__(self):
                 """Return as specific an FMRI representation as possible."""
                 if self.authority == None:
@@ -115,6 +109,18 @@
                 return "pkg://%s/%s@%s" % (self.authority, self.pkg_name,
                                 self.version)
 
+        def get_authority(self):
+                return self.authority
+
+        def set_authority(self, authority):
+                self.authority = authority
+
+        def set_timestamp(self, new_ts):
+                self.version.set_timestamp(new_ts)
+
+        def get_timestamp(self, new_ts):
+                return self.version.get_timestamp()
+
         def get_pkg_stem(self):
                 """Return a string representation of the FMRI without a specific
                 version."""
@@ -123,9 +129,12 @@
 
                 return "pkg://%s/%s" % (self.authority, self.pkg_name)
 
-        def get_dir_path(self):
-                """Return the escaped directory path fragment for this FMRI.
-                Requires a version to be defined."""
+        def get_dir_path(self, stemonly = False):
+                """Return the escaped directory path fragment for this FMRI."""
+
+                if stemonly:
+                        return "%s" % (urllib.quote(self.pkg_name, ""))
+
                 assert self.version != None
 
                 return "%s/%s" % (urllib.quote(self.pkg_name, ""),
@@ -184,12 +193,6 @@
 
                 return True
 
-        def set_timestamp(self, new_ts):
-                self.version.set_timestamp(new_ts)
-
-        def get_timestamp(self, new_ts):
-                return self.version.get_timestamp()
-
 if __name__ == "__main__":
         n1 = PkgFmri("pkg://pion/sunos/coreutils", "5.9")
         n2 = PkgFmri("sunos/coreutils", "5.10")
--- a/src/modules/manifest.py	Wed Jun 20 10:40:03 2007 -0700
+++ b/src/modules/manifest.py	Thu Jun 21 23:43:12 2007 -0700
@@ -306,6 +306,8 @@
                 """str is the text representation of the manifest"""
 
                 for l in str.splitlines():
+                        if re.match("^\s*$", l):
+                                continue
                         if re.match("^set ", l):
                                 self.add_attribute_line(l)
                         elif re.match("^file ", l):
--- a/src/modules/server/config.py	Wed Jun 20 10:40:03 2007 -0700
+++ b/src/modules/server/config.py	Thu Jun 21 23:43:12 2007 -0700
@@ -53,6 +53,10 @@
                 self.catalog = catalog.Catalog()
                 self.in_flight_trans = {}
 
+                # XXX naive:  change to
+                # catalog_requests = [ (IP-addr, time), ... ]
+                # manifest_requests = { fmri : (IP-addr, time), ... }
+                # file requests = [ (IP-addr, time), ... ]
                 self.catalog_requests = 0
                 self.manifest_requests = 0
                 self.file_requests = 0
--- a/src/modules/server/transaction.py	Wed Jun 20 10:40:03 2007 -0700
+++ b/src/modules/server/transaction.py	Thu Jun 21 23:43:12 2007 -0700
@@ -46,12 +46,17 @@
                 self.open_time = -1
                 self.pkg_name = ""
                 self.esc_pkg_name = ""
+                self.critical = False
                 self.cfg = None
                 self.client_release = ""
                 self.fmri = None
                 self.dir = ""
                 return
 
+        def get_basename(self):
+                return "%d_%s" % (self.open_time,
+                    urllib.quote("%s" % self.fmri, ""))
+
         def open(self, cfg, request):
                 self.cfg = cfg
 
@@ -136,7 +141,6 @@
                         # XXX Build a response from our lists of unsatisfied
                         # dependencies.
 
-
                 try:
                         shutil.rmtree("%s/%s" % (self.cfg.trans_root, trans_id))
                 except:
@@ -187,6 +191,10 @@
                         rfile = request.rfile
                 action = pkg.actions.types[type](rfile, **attrs)
 
+                # XXX Once actions are labelled with critical nature.
+                # if type in critical_actions:
+                #         self.critical = True
+
                 # XXX Need to handle multiple streams
                 fnames = ()
                 for opener in action.data.values():
@@ -214,8 +222,10 @@
 
                 tfile = file("%s/%s/manifest" %
                     (self.cfg.trans_root, trans_id), "a")
-                print >>tfile, ("%s" + " %s" * (len(action.attrs) + len(action.data))) % \
-                    ((type,) + tuple(action.attrs[k] for k in action.attributes()) + fnames)
+                print >>tfile, \
+                    ("%s" + " %s" * (len(action.attrs) + len(action.data))) % \
+                    ((type,) + tuple(action.attrs[k]
+                            for k in action.attributes()) + fnames)
 
                 request.send_response(200)
 
@@ -245,7 +255,9 @@
                 p.update(self.cfg, self)
 
                 # add entry to catalog
-                self.cfg.catalog.add_pkg(p)
+                # XXX Is self.cfg.catalog a copy or a reference??
+                # XXX Could just write new catalog file...
+                self.cfg.catalog.add_pkg(p, self.critical)
 
                 return ("%s" % self.fmri, "PUBLISHED")
 
@@ -254,7 +266,3 @@
                 Make appropriate catalog entries."""
                 return
 
-        def get_basename(self):
-                return "%d_%s" % (self.open_time,
-                    urllib.quote("%s" % self.fmri, ""))
-
--- a/src/modules/sysvpkg.py	Wed Jun 20 10:40:03 2007 -0700
+++ b/src/modules/sysvpkg.py	Thu Jun 21 23:43:12 2007 -0700
@@ -53,7 +53,8 @@
 
 class PkgMapLine(object):
 	"""A class that represents a single line of a SysV package's pkgmap.
-	This class should probably disappear once pkg.Content is a bit more
+
+	XXX This class should probably disappear once pkg.Content is a bit more
 	fleshed out.
 	"""
 
@@ -162,7 +163,7 @@
                                 elif ci.name.endswith("/pkgmap"):
                                         self._pkgmap = self.datastream.extractfile(ci).readlines()
 
-                        # Here we allow for only one package.  :(
+                        # XXX Here we allow for only one package.  :(
                         self.datastream = self.datastream.get_next_archive()
 
                 else: