2022 client should provide operational intent to server
authorShawn Walker <shawn.walker@sun.com>
Tue, 30 Sep 2008 19:37:17 -0500
changeset 556 1c3526ca7b9e
parent 555 87c86da72fd9
child 557 74cc924a59be
2022 client should provide operational intent to server 3565 client should provide version information when providing intent to server
src/client.py
src/gui/modules/installupdate.py
src/modules/client/history.py
src/modules/client/image.py
src/modules/client/imageplan.py
src/modules/client/imagestate.py
src/modules/client/retrieve.py
src/modules/misc.py
src/packagemanager.py
src/tests/api/t_history.py
src/tests/baseline.txt
--- a/src/client.py	Tue Sep 30 17:59:56 2008 -0500
+++ b/src/client.py	Tue Sep 30 19:37:17 2008 -0500
@@ -204,6 +204,7 @@
         if not check_fmri_args(pargs):
                 return 1
 
+        img.history.operation_name = "list"
         img.load_catalogs(progress.NullProgressTracker())
 
         seen_one_pkg = False
@@ -260,6 +261,8 @@
                 if not found:
                         if not seen_one_pkg and not all_known:
                                 emsg(_("no packages installed"))
+                                img.history.operation_result = \
+                                    history.RESULT_NOTHING_TO_DO
                                 return 1
 
                         if upgradable_only:
@@ -269,14 +272,23 @@
                                 else:
                                         emsg(_("No installed packages have " \
                                             "available updates"))
+                                img.history.operation_result = \
+                                    history.RESULT_NOTHING_TO_DO
                                 return 1
+
+                        img.history.operation_result = \
+                            history.RESULT_NOTHING_TO_DO
                         return 1
+
+                img.history.operation_result = history.RESULT_SUCCEEDED
                 return 0
 
         except image.InventoryException, e:
                 if e.illegal:
                         for i in e.illegal:
                                 error(i)
+                        img.history.operation_result = \
+                            history.RESULT_FAILED_BAD_REQUEST
                         return 1
 
                 if all_known:
@@ -285,6 +297,7 @@
                         state = image.PKG_STATE_INSTALLED
                 for pat in e.notfound:
                         error(_("no packages matching '%s' %s") % (pat, state))
+                img.history.operation_result = history.RESULT_NOTHING_TO_DO
                 return 1
 
 def get_tracker(quiet = False):
@@ -828,7 +841,8 @@
 
         img.load_catalogs(progresstracker)
 
-        ip = imageplan.ImagePlan(img, progresstracker, recursive_removal)
+        ip = imageplan.ImagePlan(img, progresstracker, recursive_removal,
+            noexecute)
 
         err = 0
 
@@ -1088,6 +1102,7 @@
         if not check_fmri_args(pargs):
                 return 1
 
+        img.history.operation_name = "info"
         img.load_catalogs(progress.NullProgressTracker())
 
         err = 0
@@ -1098,15 +1113,21 @@
                 if illegals:
                         for i in illegals:
                                 emsg(str(i))
+                        img.history.operation_result = \
+                            history.RESULT_FAILED_BAD_REQUEST
                         return 1
 
                 if not fmris and not notfound:
                         error(_("no packages installed"))
+                        img.history.operation_result = \
+                            history.RESULT_NOTHING_TO_DO
                         return 1
         elif info_remote:
                 # Verify validity of certificates before attempting network
                 # operations
                 if not img.check_cert_validity():
+                        img.history.operation_result = \
+                            history.RESULT_FAILED_TRANSPORT
                         return 1
 
                 fmris = []
@@ -1214,7 +1235,9 @@
                 emsg()
                 for p in notfound:
                         emsg("        %s" % p)
-
+                img.history.operation_result = history.RESULT_NOTHING_TO_DO
+        else:
+                img.history.operation_result = history.RESULT_SUCCEEDED
         return err
 
 def display_contents_results(actionlist, attrs, sort_attrs, action_types,
@@ -1396,6 +1419,7 @@
                 if a.startswith("pkg.") and not a in valid_special_attrs:
                         usage(_("Invalid attribute '%s'") % a)
 
+        img.history.operation_name = "contents"
         img.load_catalogs(progress.NullProgressTracker())
 
         err = 0
@@ -1407,15 +1431,21 @@
                 if illegals:
                         for i in illegals:
                                 emsg(i)
+                        img.history.operation_result = \
+                            history.RESULT_FAILED_BAD_REQUEST
                         return 1
 
                 if not fmris and not notfound:
                         error(_("no packages installed"))
+                        img.history.operation_result = \
+                            history.RESULT_NOTHING_TO_DO
                         return 1
         elif remote:
                 # Verify validity of certificates before attempting network
                 # operations
                 if not img.check_cert_validity():
+                        img.history.operation_result = \
+                            history.RESULT_FAILED_TRANSPORT
                         return 1
 
                 fmris = []
@@ -1512,7 +1542,9 @@
                 emsg()
                 for p in notfound:
                         emsg("        %s" % p)
-
+                img.history.operation_result = history.RESULT_NOTHING_TO_DO
+        else:
+                img.history.operation_result = history.RESULT_SUCCEEDED
         return err
 
 def display_catalog_failures(cre):
@@ -1672,9 +1704,9 @@
                 error(_("set-authority failed: %s") % e)
                 return 1
         except image.CatalogRefreshException, cre:
-                msg = "Could not refresh the catalog for %s" % \
+                text = "Could not refresh the catalog for %s" % \
                     auth
-                error(_(msg))
+                error(_(text))
                 ret_code = 1
 
         if preferred:
--- a/src/gui/modules/installupdate.py	Tue Sep 30 17:59:56 2008 -0500
+++ b/src/gui/modules/installupdate.py	Tue Sep 30 19:37:17 2008 -0500
@@ -42,6 +42,7 @@
 import pkg.client.bootenv as bootenv
 import pkg.client.history as history
 import pkg.client.imageplan as imageplan
+import pkg.client.imagestate as imagestate
 import pkg.client.progress as progress
 import pkg.fmri as fmri
 import pkg.client.indexer as indexer
@@ -480,6 +481,7 @@
                     self.parent._("Evaluating: %s\n") % pfmri.get_fmri())
 
                 self.ip.progtrack.evaluate_progress()
+                self.ip.image.state.set_target(pfmri, imagestate.INTENT_PROCESS)
                 m = image.get_manifest(pfmri)
 
                 # [manifest] examine manifest for dependencies
@@ -532,6 +534,7 @@
                                 continue
 
                         if excluded:
+                                self.ip.image.state.set_target()
                                 raise RuntimeError, "excluded by '%s'" % f
 
                         # treat-as-required, treat-as-required-unless-pinned,
@@ -559,6 +562,8 @@
                         self.ip.propose_fmri(cf)
                         self.__evaluate_fmri(cf, image)
 
+                self.ip.image.state.set_target()
+
         def __download_stage(self, rebuild=False):
                 '''Parts of the code duplicated from install and image-update from pkg(1) 
                 and pkg.client.ImagePlan.preexecute()'''
--- a/src/modules/client/history.py	Tue Sep 30 17:59:56 2008 -0500
+++ b/src/modules/client/history.py	Tue Sep 30 19:37:17 2008 -0500
@@ -56,6 +56,9 @@
 # Indicates that a transport error caused the operation to fail.
 RESULT_FAILED_TRANSPORT = ["Failed", "Transport"]
 
+# Operations that are discarded, not saved, when recorded by history.
+DISCARDED_OPERATIONS = ["contents", "info", "list"]
+
 class _HistoryOperation(object):
         """A _HistoryOperation object is a representation of data about an
         operation that a pkg(5) client has performed.  This class is private
@@ -192,10 +195,17 @@
                 elif name == "operation_result":
                         # Record when the operation ended.
                         op.end_time = misc.time_to_timestamp(None)
-                        # Write current history and last operation to a file.
-                        if self.__save():
-                                # Discard it now that it is no longer needed.
-                                ops.pop()
+
+                        # Some operations shouldn't be saved -- they're merely
+                        # included in the stack for completeness or to support
+                        # client functionality.
+                        if op.name not in DISCARDED_OPERATIONS:
+                                # Write current history and last operation to a
+                                # file.
+                                self.__save()
+
+                        # Discard it now that it is no longer needed.
+                        ops.pop()
 
         def __init__(self, root_dir=".", filename=None):
                 """'root_dir' should be the path of the directory where the
--- a/src/modules/client/image.py	Tue Sep 30 17:59:56 2008 -0500
+++ b/src/modules/client/image.py	Tue Sep 30 19:37:17 2008 -0500
@@ -52,6 +52,7 @@
 import pkg.client.history as history
 import pkg.client.imageconfig as imageconfig
 import pkg.client.imageplan as imageplan
+import pkg.client.imagestate as imagestate
 import pkg.client.retrieve as retrieve
 import pkg.portable as portable
 import pkg.client.query_engine as query_e
@@ -192,10 +193,13 @@
                 self.dl_cache_dir = None
                 self.dl_cache_incoming = None
                 self.is_user_cache_dir = False
+                self.state = imagestate.ImageState()
+                self.attrs = {
+                    "Policy-Require-Optional": False,
+                    "Policy-Pursue-Latest": True
+                }
 
-                self.attrs = {}
-
-                self.imageplan = None # valid after evaluation succceds
+                self.imageplan = None # valid after evaluation succeeds
 
                 # contains a dictionary w/ key = pkgname, value is miminum
                 # frmi.XXX  Needs rewrite using graph follower
@@ -203,7 +207,11 @@
 
                 # a place to keep info about saved_files; needed by file action
                 self.saved_files = {}
- 
+
+                # A place to keep track of which manifests (based on fmri and
+                # operation) have already provided intent information.
+                self.__touched_manifests = {}
+
         def find_root(self, d):
 
                 def check_subdirs(sub_d, prefix):
@@ -402,7 +410,7 @@
                         res = cmp(a.errors, b.errors)
                         if res == 0:
                                 return cmp(a.good_tx, b.good_tx)
-                        return res                        
+                        return res
 
                 slst.sort(cmp = cmp_depotstatus)
 
@@ -477,7 +485,7 @@
                                             " not found") % pfx)
                                         emsg(_("File was supposed to exist at" \
                                            "  path %s") % ssl_cert)
-                                        return False 
+                                        return False
                                 else:
                                         raise
                         # OpenSSL.crypto.Error
@@ -508,7 +516,7 @@
                                 emsg(_("Certificate effective date is in" \
                                     " the future"))
                                 return False
-                        
+
                         na = cert.get_notAfter()
                         t = time.strptime(na, "%Y%m%d%H%M%SZ")
                         nadt = datetime.datetime.utcfromtimestamp(
@@ -624,7 +632,7 @@
                         refresh_needed = True
 
                 self.save_config()
-                
+
                 if refresh_needed and refresh_allowed:
                         self.destroy_catalog(auth_name)
                         self.destroy_catalog_cache()
@@ -717,8 +725,8 @@
 
                 return False
 
-        def _fetch_manifest_with_retries(self, fmri):
-                """Wrapper function around _fetch_manifest to handle some
+        def __fetch_manifest_with_retries(self, fmri):
+                """Wrapper function around __fetch_manifest to handle some
                 exceptions and keep track of additional state."""
 
                 m = None
@@ -726,15 +734,99 @@
 
                 while not m:
                         try:
-                                m = self._fetch_manifest(fmri)
+                                m = self.__fetch_manifest(fmri)
                         except TransferTimedOutException:
                                 retry_count -= 1
 
                                 if retry_count <= 0:
                                         raise
+
                 return m
 
-        def _fetch_manifest(self, fmri):
+        def __get_touched_manifest(self, fmri):
+                """Returns whether intent information has been provided for the
+                given fmri."""
+
+                op = self.history.operation_name
+                if not op:
+                        # The client may not have provided the name of the
+                        # operation it is performing.
+                        op = "unknown"
+
+                if op not in self.__touched_manifests:
+                        # No intent information has been provided for fmris
+                        # for the current operation.
+                        return False
+
+                f = str(fmri)
+                if f not in self.__touched_manifests[op]:
+                        # No intent information has been provided for this
+                        # fmri for the current operation.
+                        return False
+
+                return True
+
+        def __set_touched_manifest(self, fmri):
+                """Records that intent information has been provided for the
+                given fmri's manifest."""
+
+                op = self.history.operation_name
+                if not op:
+                        # The client may not have provided the name of the
+                        # operation it is performing.
+                        op = "unknown"
+
+                if op not in self.__touched_manifests:
+                        # No intent information has yet been provided for fmris
+                        # for the current operation.
+                        self.__touched_manifests[op] = {}
+
+                f = str(fmri)
+                if f not in self.__touched_manifests[op]:
+                        # No intent information has yet been provided for this
+                        # fmri for the current operation.
+                        self.__touched_manifests[op][f] = None
+
+        def __touch_manifest(self, fmri):
+                """Perform steps necessary to 'touch' a manifest to provide
+                intent information.  Ignores most exceptions as this operation
+                is only for informational purposes."""
+
+                if not self.__get_touched_manifest(fmri):
+                        # If the manifest for this fmri hasn't been "seen"
+                        # before, determine if intent information needs to be
+                        # provided.
+
+                        # What is the client currently processing?
+                        target, intent = self.state.get_target()
+
+                        if target and intent != imagestate.INTENT_EVALUATE:
+                                # If the client is currently performing an
+                                # image-modifying operation, not just an
+                                # an evaluation, then perform further checks.
+
+                                # Ignore the authority for comparison.
+                                na_target = target.get_fmri(anarchy=True)
+                                na_fmri = target.get_fmri(anarchy=True)
+
+                                if na_target == na_fmri:
+                                        # If the client is currently processing
+                                        # the given fmri (for an install, etc.)
+                                        # then intent information is needed.
+                                        try:
+                                                retrieve.touch_manifest(self,
+                                                    fmri)
+                                        except NameError:
+                                                pass
+
+                                        # Manifests should only be marked as
+                                        # processed if the touch is actually
+                                        # performed since multiple retrievals
+                                        # may occur during the same operation
+                                        # for different reasons.
+                                        self.__set_touched_manifest(fmri)
+
+        def __fetch_manifest(self, fmri):
                 """Perform steps necessary to get manifest from remote host
                 and write resulting contents to disk.  Helper routine for
                 get_manifest.  Does not filter the results, caller must do
@@ -771,6 +863,8 @@
                         if e.errno not in (errno.EROFS, errno.EACCES):
                                 raise
 
+                self.__set_touched_manifest(fmri)
+
                 return m
 
         def _valid_manifest(self, fmri, manifest):
@@ -796,39 +890,50 @@
                     fmri.get_dir_path(), "manifest")
                 return mpath
 
+        def __get_manifest(self, fmri):
+                """Find on-disk manifest and create in-memory Manifest
+                object."""
+
+                m = None
+                mpath = os.path.join(self.imgdir, "pkg", fmri.get_dir_path(),
+                    "manifest")
+                if os.path.exists(mpath):
+                        # If the manifest already exists, load it from storage.
+                        m = manifest.Manifest()
+                        mcontent = file(mpath).read()
+                        m.set_fmri(self, fmri)
+                        m.set_content(mcontent)
+
+                try:
+                        # If the manifest didn't already exist, or isn't from
+                        # the correct authority, or no authority is attached
+                        # to the manifest, attempt to download a new one.
+                        if not m or not self._valid_manifest(fmri, m):
+                                m = self.__fetch_manifest_with_retries(fmri)
+                except NameError:
+                        # In this case, the client has failed to download a new
+                        # manifest or re-download an existing one with the same
+                        # name.
+                        if not m:
+                                # Since an older copy doesn't exist, give up.
+                                raise
+
+                        # Since the old manifest exists, keep it, and drive on.
+
+                return m
+
         def get_manifest(self, fmri, filtered = False):
                 """Find on-disk manifest and create in-memory Manifest
                 object, applying appropriate filters as needed."""
 
-                m = manifest.Manifest()
-
-                fmri_dir_path = os.path.join(self.imgdir, "pkg",
-                    fmri.get_dir_path())
-                mpath = os.path.join(fmri_dir_path, "manifest")
-
-                # If the manifest isn't there, download.
-                if not os.path.exists(mpath):
-                        m = self._fetch_manifest_with_retries(fmri)
-                else:
-                        mcontent = file(mpath).read()
-                        m.set_fmri(self, fmri)
-                        m.set_content(mcontent)
-
-                # If the manifest isn't from the correct authority, or
-                # no authority is attached to the manifest, download a new one.
-                if not self._valid_manifest(fmri, m):
-                        try:
-                                m = self._fetch_manifest_with_retries(fmri)
-                        except NameError:
-                                # In thise case, the client has failed to
-                                # download a new manifest with the same name.
-                                # We can either give up or drive on.  It makes
-                                # the most sense to do the best we can with what
-                                # we have.  Keep the old manifest and drive on.
-                                pass
+                m = self.__get_manifest(fmri)
+                self.__touch_manifest(fmri)
 
                 # XXX perhaps all of the below should live in Manifest.filter()?
                 if filtered:
+                        fmri_dir_path = os.path.join(self.imgdir, "pkg",
+                            fmri.get_dir_path())
+
                         filters = []
                         try:
                                 f = file("%s/filters" % fmri_dir_path, "r")
@@ -996,6 +1101,15 @@
                                 raise
                         shutil.rmtree(tmpdir)
 
+        def get_version_installed(self, pfmri):
+                """Returns an fmri of the installed package matching the
+                package stem of the given fmri or None if no match is found."""
+                target = pfmri.get_pkg_stem()
+                for f in self.gen_installed_pkgs():
+                        if f.get_pkg_stem() == target:
+                                return f
+                return None
+
         def _get_version_installed(self, pfmri):
                 pd = pfmri.get_pkg_stem()
                 pdir = "%s/pkg/%s" % (self.imgdir,
@@ -1185,7 +1299,7 @@
                         total += 1
 
                         full_refresh_this_auth = False
-                        
+
                         if auth["prefix"] in self.catalogs:
                                 cat = self.catalogs[auth["prefix"]]
                                 ts = cat.last_modified()
@@ -1240,7 +1354,7 @@
                                 succeeded += 1
 
                 self.cache_catalogs()
-                
+
                 if failed:
                         raise CatalogRefreshException(failed, total, succeeded)
 
@@ -1341,7 +1455,7 @@
                 except OSError, e:
                         if e.errno != errno.ENOENT:
                                 raise
-                        
+
         def destroy_catalog(self, auth_name):
                 try:
                         shutil.rmtree("%s/catalog/%s" %
@@ -1538,7 +1652,6 @@
         def load_optional_dependencies(self):
                 for fmri in self.gen_installed_pkgs():
                         mfst = self.get_manifest(fmri, filtered = True)
-
                         for dep in mfst.gen_actions_by_type("depend"):
                                 required, min_fmri, max_fmri = dep.parse(self)
                                 if required == False:
@@ -1822,7 +1935,7 @@
                         return pkg.fmri.PkgFmri(urllib.unquote(os.path.dirname(
                             index[len(idxdir) + 1:]).replace(os.path.sep, "@")),
                             None)
-                
+
                 res = []
 
                 for fmri, mfst in self.get_fmri_manifest_pairs():
@@ -1845,7 +1958,7 @@
                                                     action, keyval))
                 return res
 
-                
+
         def local_search(self, args, case_sensitive):
                 """Search the image for the token in args[0]."""
                 assert args[0]
@@ -1985,7 +2098,8 @@
                 to name the package."""
 
                 error = 0
-                ip = imageplan.ImagePlan(self, progress, filters = filters)
+                ip = imageplan.ImagePlan(self, progress, filters = filters,
+                    noexecute = noexecute)
 
                 self.load_optional_dependencies()
 
@@ -2062,8 +2176,8 @@
                         msg(ip.display())
 
         def rebuild_search_index(self, progtracker):
-                """Rebuilds the search indexes.  Removes all 
-                existing indexes and replaces them from scratch rather than 
+                """Rebuilds the search indexes.  Removes all
+                existing indexes and replaces them from scratch rather than
                 performing the incremental update which is usually used."""
                 self.update_index_dir()
                 if not os.path.isdir(self.index_dir):
--- a/src/modules/client/imageplan.py	Tue Sep 30 17:59:56 2008 -0500
+++ b/src/modules/client/imageplan.py	Tue Sep 30 19:37:17 2008 -0500
@@ -25,9 +25,8 @@
 
 import os
 import pkg.fmri as fmri
+import pkg.client.imagestate as imagestate
 import pkg.client.pkgplan as pkgplan
-import pkg.client.retrieve as retrieve # XXX inventory??
-import pkg.version as version
 import pkg.client.indexer as indexer
 import pkg.search_errors as se
 from pkg.client.filter import compile_filter
@@ -78,12 +77,19 @@
         "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, progtrack, recursive_removal = False, filters = []):
+        def __init__(self, image, progtrack, recursive_removal = False,
+            noexecute = False, filters = []):
                 self.image = image
                 self.state = UNEVALUATED
                 self.recursive_removal = recursive_removal
                 self.progtrack = progtrack
 
+                self.noexecute = noexecute
+                if noexecute:
+                        self.__intent = imagestate.INTENT_EVALUATE
+                else:
+                        self.__intent = imagestate.INTENT_PROCESS
+
                 self.target_fmris = []
                 self.target_rem_fmris = []
                 self.pkg_plans = []
@@ -240,6 +246,7 @@
         def evaluate_fmri(self, pfmri):
 
                 self.progtrack.evaluate_progress()
+                self.image.state.set_target(pfmri, self.__intent)
                 m = self.image.get_manifest(pfmri)
 
                 # [manifest] examine manifest for dependencies
@@ -292,6 +299,7 @@
                                 continue
 
                         if excluded:
+                                self.image.state.set_target()
                                 raise RuntimeError, "excluded by '%s'" % f
 
                         # treat-as-required, treat-as-required-unless-pinned,
@@ -319,6 +327,8 @@
                         self.propose_fmri(cf)
                         self.evaluate_fmri(cf)
 
+                self.image.state.set_target()
+
         def add_pkg_plan(self, pfmri):
                 """add a pkg plan to imageplan for fully evaluated frmi"""
                 m = self.image.get_manifest(pfmri)
@@ -351,13 +361,15 @@
                 if dependents and not self.recursive_removal:
                         raise NonLeafPackageException(pfmri, dependents)
 
-                m = self.image.get_manifest(pfmri)
+                pp = pkgplan.PkgPlan(self.image, self.progtrack)
 
-                pp = pkgplan.PkgPlan(self.image, self.progtrack)
+                self.image.state.set_target(pfmri, self.__intent)
+                m = self.image.get_manifest(pfmri)
 
                 try:
                         pp.propose_removal(pfmri, m)
                 except RuntimeError:
+                        self.image.state.set_target()
                         msg("pkg %s not installed" % pfmri)
                         return
 
@@ -376,6 +388,7 @@
                 # dependency graphs.  Cycles need to be arbitrarily broken, and
                 # are done so in the loop above.
                 self.pkg_plans.append(pp)
+                self.image.state.set_target()
 
         def evaluate(self):
                 assert self.state == UNEVALUATED
@@ -390,8 +403,8 @@
                         try:
                                 self.evaluate_fmri(f)
                         except KeyError, e:
-                                outstring += "Attemping to install %s causes:\n\t%s\n" % \
-                                    (f.get_name(), e)
+                                outstring += "Attempting to install %s " \
+                                    "causes:\n\t%s\n" % (f.get_name(), e)
 
                 if outstring:
                         raise RuntimeError("No packages were installed because "
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/modules/client/imagestate.py	Tue Sep 30 19:37:17 2008 -0500
@@ -0,0 +1,77 @@
+#!/usr/bin/python2.4
+#
+# 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 2008 Sun Microsystems, Inc.  All rights reserved.
+# Use is subject to license terms.
+
+# Indicates that the fmri is being used strictly for information.
+INTENT_INFO = "info"
+
+# Indicates that the fmri is being used to perform a dry-run evaluation of an
+# image-modifying operation.
+INTENT_EVALUATE = "evaluate"
+
+# Indicates that the fmri is being processed as part of an image-modifying
+# operation.
+INTENT_PROCESS = "process"
+
+class ImageState(object):
+        """An ImageState object provides a temporary place to store information
+        about operations that are being performed on an image (e.g. fmris of
+        packages that are being installed, uninstalled, etc.).
+        """
+
+        def __init__(self):
+                self.__fmri_intent_stack = []
+
+        def __str__(self):
+                return "%s" % self.__fmri_intent_stack
+
+        def set_target(self, fmri=None, intent=INTENT_INFO):
+                """Indicates that the given fmri is currently being evaluated
+                or manipulated for an image operation.  A value of None for
+                fmri will clear the current target.
+                """
+                if fmri:
+                        self.__fmri_intent_stack.append((fmri, intent))
+                else:
+                        del self.__fmri_intent_stack[-1]
+
+        def get_target(self):
+                """Returns a tuple of the format (fmri, intent) representing an
+                fmri currently being evaluated or manipulated for an image
+                operation.  A tuple containing (None, None) will be returned if
+                no target has been set.
+                """
+                try:
+                        return self.__fmri_intent_stack[-1]
+                except IndexError:
+                        return (None, None)
+
+        def get_targets(self):
+                """Returns a list of tuples of the format (fmri, intent)
+                representing fmris currently being evaluated or manipulated for
+                an image operation.  An empty list is returned if there are no
+                targets.
+                """
+                return self.__fmri_intent_stack[:]
+
--- a/src/modules/client/retrieve.py	Tue Sep 30 17:59:56 2008 -0500
+++ b/src/modules/client/retrieve.py	Tue Sep 30 19:37:17 2008 -0500
@@ -25,58 +25,148 @@
 
 import socket
 import urllib2
-import httplib
 
 import pkg.fmri
+import pkg.client.imagestate as imagestate
 from pkg.misc import versioned_urlopen
 from pkg.misc import TransferTimedOutException
 from pkg.misc import retryable_http_errors
 
 # client/retrieve.py - collected methods for retrieval of pkg components
 # from repositories
+def __get_intent_str(img, fmri):
+        """Returns a string representing the intent of the client in retrieving
+        information based on the operation information provided by the image
+        history object.
+        """
 
-def get_datastream(img, fmri, hash):
-        """Retrieve a file handle based on a package fmri and a file hash."""
+        op = img.history.operation_name
+        if not op:
+                # The client hasn't indicated what operation is executing.
+                op = "unknown"
+
+        reason = imagestate.INTENT_INFO
+        initial_pkg = ""
+        parent_pkg = ""
+        try:
+                targets = img.state.get_targets()
+                # Attempt to determine why the client is retrieving the
+                # manifest for this fmri and what its current target is.
+                target, reason = targets[-1]
+
+                na_current = fmri.get_fmri(anarchy=True)
+                na_target = target.get_fmri(anarchy=True)
+                if na_target == na_current:
+                        # If the fmri for the manifest being retrieved does not
+                        # match the fmri of the target, then this manifest is
+                        # being retrieved for information purposes only, so don't
+                        # provide this information.
+
+                        # The fmri responsible for the current one being processed
+                        # should immediately precede the current one in the target
+                        # list.
+                        parent = targets[-2][0]
+                        parent_pkg = parent.get_fmri(anarchy=True)[len("pkg:/"):]
 
-        authority, pkg_name, version = fmri.tuple()
+                        if len(targets) > 2:
+                                # If there are more than two targets in the list, then
+                                # the very first fmri is the one that caused the
+                                # current and parent fmris to be retrieved.
+                                initial = targets[0][0]
+                                initial_pkg = initial.get_fmri(
+                                    anarchy=True)[len("pkg:/"):]
+                        else:
+                                initial_pkg = parent_pkg
+                                parent_pkg = ""
+        except IndexError:
+                # Any part of the target information may not be available.
+                # Ignore it, and move on.
+                pass
+
+        version = ""
+        if reason != imagestate.INTENT_INFO:
+                # Only provide version information for non-informational
+                # operations.
+                version = "none"
+                try:
+                        version = "%s" % img.get_version_installed(fmri).version
+                except AttributeError:
+                        # We didn't get a match back, drive on.
+                        pass
+
+        info = {
+            "operation": op,
+            "version": version,
+            "reason": reason,
+            "initial_target": initial_pkg,
+            "parent_target": parent_pkg,
+        }
+
+        # op/installed_version/reason/initial_target/immediate_parent/
+        return "(%s)" % ";".join([
+            "%s=%s" % (key, info[key]) for key in info.keys()
+            if info[key] != ""
+        ])
+
+def get_datastream(img, fmri, fhash):
+        """Retrieve a file handle based on a package fmri and a file hash.
+        """
+
+        authority = fmri.get_authority_str()
         authority = pkg.fmri.strip_auth_pfx(authority)
         url_prefix = img.get_url_by_authority(authority)
         ssl_tuple = img.get_ssl_credentials(authority)
         uuid = img.get_uuid(authority)
 
         try:
-                f, v = versioned_urlopen(url_prefix, "file", [0], hash,
-                    ssl_creds=ssl_tuple, imgtype=img.type, 
-                    uuid=uuid)
+                f = versioned_urlopen(url_prefix, "file", [0], fhash,
+                    ssl_creds=ssl_tuple, imgtype=img.type, uuid=uuid)[0]
         except urllib2.HTTPError, e:
                 raise NameError, "could not retrieve file '%s' from '%s'" % \
-                    (hash, url_prefix)
+                    (fhash, url_prefix)
         except urllib2.URLError, e:
                 if len(e.args) == 1 and isinstance(e.args[0], socket.sslerror):
                         raise RuntimeError, e
 
                 raise NameError, "could not retrieve file '%s' from '%s'" % \
-                    (hash, url_prefix)
+                    (fhash, url_prefix)
         except:
                 raise NameError, "could not retrieve file '%s' from '%s'" % \
-                    (hash, url_prefix)
+                    (fhash, url_prefix)
 
         return f
 
-def get_manifest(img, fmri):
-        """ Calculate URI and retrieve manifest.  Return it as a buffer to
-            the caller. """
+def __get_manifest(img, fmri, method):
+        """Given an image object, fmri, and http method; return a file object
+        for the related manifest and send intent information.
+        """
 
-        authority, pkg_name, version = fmri.tuple()
+        authority = fmri.get_authority_str()
         authority = pkg.fmri.strip_auth_pfx(authority)
         url_prefix = img.get_url_by_authority(authority)
         ssl_tuple = img.get_ssl_credentials(authority)
         uuid = img.get_uuid(authority)
 
+        # Tell the server why this resource is being requested.
+        headers = {
+            "X-IPkg-Intent": __get_intent_str(img, fmri)
+        }
+
+        return versioned_urlopen(url_prefix, "manifest", [0],
+            fmri.get_url_path(), ssl_creds=ssl_tuple, imgtype=img.type,
+            method=method, headers=headers, uuid=uuid)[0]
+
+def get_manifest(img, fmri):
+        """Retrieve the manifest for the given fmri.  Return it as a buffer to
+        the caller.
+        """
+
+        authority = fmri.tuple()[0]
+        authority = pkg.fmri.strip_auth_pfx(authority)
+        url_prefix = img.get_url_by_authority(authority)
+
         try:
-                m, v = versioned_urlopen(url_prefix, "manifest", [0],
-                    fmri.get_url_path(), ssl_creds=ssl_tuple,
-                    imgtype=img.type, uuid=uuid)
+                m = __get_manifest(img, fmri, "GET")
         except urllib2.HTTPError, e:
                 if e.code in retryable_http_errors:
                         raise TransferTimedOutException
@@ -86,7 +176,8 @@
         except urllib2.URLError, e:
                 if len(e.args) == 1 and isinstance(e.args[0], socket.sslerror):
                         raise RuntimeError, e
-                elif len(e.args) == 1 and isinstance(e.args[0], socket.timeout):
+                elif len(e.args) == 1 and isinstance(e.args[0],
+                    socket.timeout):
                         raise TransferTimedOutException
 
                 raise NameError, "could not retrieve manifest '%s' from '%s'" % \
@@ -96,3 +187,18 @@
                     (fmri.get_url_path(), url_prefix)
 
         return m.read()
+
+def touch_manifest(img, fmri):
+        """Perform a HEAD operation on the manifest for the given fmri.
+        """
+
+        authority = fmri.get_authority_str()
+        authority = pkg.fmri.strip_auth_pfx(authority)
+        url_prefix = img.get_url_by_authority(authority)
+
+        try:
+                __get_manifest(img, fmri, "HEAD")
+        except:
+                raise NameError, "could not 'touch' manifest '%s' at '%s'" % \
+                    (fmri.get_url_path(), url_prefix)
+
--- a/src/modules/misc.py	Tue Sep 30 17:59:56 2008 -0500
+++ b/src/modules/misc.py	Tue Sep 30 19:37:17 2008 -0500
@@ -32,7 +32,6 @@
 import socket
 import urllib
 import urllib2
-import httplib
 import urlparse
 import sys
 import zlib
@@ -81,9 +80,9 @@
     (VERSION, portable.util.get_canonical_os_name(), platform.machine(),
     portable.util.get_os_release(), platform.version())
 
-def versioned_urlopen(base_uri, operation, versions = [], tail = None,
+def versioned_urlopen(base_uri, operation, versions = None, tail = None,
     data = None, headers = None, ssl_creds = None, imgtype = IMG_NONE,
-    uuid = None):
+    method = "GET", uuid = None):
         """Open the best URI for an operation given a set of versions.
 
         Both the client and the server may support multiple versions of
@@ -115,6 +114,9 @@
         else:
                 url_opener = urllib2.urlopen
 
+        if not versions:
+                versions = []
+
         if not headers:
                 headers = {}
 
@@ -134,7 +136,11 @@
                 if uuid:
                         headers["X-IPkg-UUID"] = uuid
                 req = urllib2.Request(url = uri, headers = headers)
-                if data is not None:
+                if method == "HEAD":
+                        # Must override urllib2's get_method since it doesn't
+                        # natively support this operation.
+                        req.get_method = lambda: "HEAD"
+                elif data is not None:
                         req.add_data(data)
 
                 try:
@@ -432,9 +438,9 @@
                 self.hashval = hashval
 
         def __str__(self):
-                str = "Action with path %s should have hash %s. Computed hash %s instead." % \
-                    (self.action.attrs["path"], self.action.attrs["chash"], 
-                    self.hashval)
+                str = "Action with path %s should have hash %s. Computed " \
+                    "hash %s instead." % (self.action.attrs["path"],
+                    self.action.attrs["chash"], self.hashval)
                 return str
 
 # Default maximum memory useage during indexing
--- a/src/packagemanager.py	Tue Sep 30 17:59:56 2008 -0500
+++ b/src/packagemanager.py	Tue Sep 30 19:37:17 2008 -0500
@@ -917,10 +917,17 @@
                         gobject.idle_add(self.__update_package_info, pkg, icon,
                             True, None)
                 man = None
+                img.history.operation_name = "info"
                 try:
                         man = img.get_manifest(pkg, filtered = True)
                 except IOError:
                         man = "NotAvailable"
+                        img.history.operation_result = \
+                            history.RESULT_FAILED_STORAGE
+                except:
+                        img.history.operation_result = \
+                            history.RESULT_FAILED_UNKNOWN
+
                 if cmp(self.pkginfo_thread, pkg) == 0:
                         if not pkg:
                                 gobject.idle_add(self.__update_package_info, pkg, icon, \
@@ -928,7 +935,11 @@
                         else:
                                 gobject.idle_add(self.__update_package_info, pkg, icon, \
                                     True, man)
+                        img.history.operation_result = \
+                            history.RESULT_SUCCEEDED
                 else:
+                        img.history.operation_result = \
+                            history.RESULT_SUCCEEDED
                         return
 
         # This function is ported from pkg.actions.generic.distinguished_name()
@@ -1672,6 +1683,8 @@
                 for the particular version (local operation only), if the package is 
                 not installed than the newest one'''
                 self.description_thread_running = True
+                img = self.image_o
+                img.history.operation_name = "info"
                 for pkg in self.application_list:
                         if self.cancelled:
                                 self.description_thread_running = False
@@ -1698,6 +1711,7 @@
                         # XXX workaround, this should be done nicer
                         gobject.idle_add(self.update_desc, info, pkg, package)
                         time.sleep(0.01)
+                img.history.operation_result = history.RESULT_SUCCEEDED
                 self.description_thread_running = False
                 
         def update_statusbar(self):
--- a/src/tests/api/t_history.py	Tue Sep 30 17:59:56 2008 -0500
+++ b/src/tests/api/t_history.py	Tue Sep 30 19:37:17 2008 -0500
@@ -209,6 +209,30 @@
                         loaded_data = loaded_ops[op_name]
                         self.assertEqual(op_data, loaded_data)
 
+        def test_5_discarded_operations(self):
+                """Verify that discarded operations are not saved."""
+
+                h = self.__h
+                h.client_name = "pkg-test"
+
+                for op_name in sorted(history.DISCARDED_OPERATIONS):
+                        h.operation_name = op_name
+                        h.operation_result = history.RESULT_NOTHING_TO_DO
+
+                # Now load all operation data that's been saved during testing
+                # for comparison.
+                loaded_ops = []
+                for entry in sorted(os.listdir(h.path)):
+                        # Load the history entry.
+                        he = history.History(root_dir=h.root_dir,
+                            filename=entry)
+                        loaded_ops.append(he.operation_name)
+
+                # Now verify that none of the saved operations are one that
+                # should have been discarded.
+                for op_name in sorted(history.DISCARDED_OPERATIONS):
+                        self.assert_(op_name not in loaded_ops)
+
 if __name__ == "__main__":
         unittest.main()
 
--- a/src/tests/baseline.txt	Tue Sep 30 17:59:56 2008 -0500
+++ b/src/tests/baseline.txt	Tue Sep 30 19:37:17 2008 -0500
@@ -95,6 +95,7 @@
 api.t_history.py TestHistory.test_2_clear|pass
 api.t_history.py TestHistory.test_3_client_load|pass
 api.t_history.py TestHistory.test_4_stacked_operations|pass
+api.t_history.py TestHistory.test_5_discarded_operations|pass
 api.t_imageconfig.py TestImageConfig.test_missing_conffile|pass
 api.t_imageconfig.py TestImageConfig.test_read|pass
 api.t_imageconfig.py TestImageConfig.test_unicode|pass