src/modules/client/api.py
changeset 1537 00a5b4d54eb8
parent 1516 8c950a3b4171
child 1538 78ac66abc186
--- a/src/modules/client/api.py	Wed Dec 02 15:13:59 2009 -0800
+++ b/src/modules/client/api.py	Wed Dec 02 19:21:44 2009 -0600
@@ -26,6 +26,7 @@
 #
 
 import copy
+import fnmatch
 import os
 import StringIO
 import sys
@@ -41,14 +42,15 @@
 import pkg.client.query_parser as query_p
 import pkg.fmri as fmri
 import pkg.misc as misc
+import pkg.nrlock
 import pkg.p5i as p5i
 import pkg.search_errors as search_errors
-import pkg.nrlock
+import pkg.version
 
 from pkg.client.imageplan import EXECUTED_OK
 from pkg.client import global_settings
 
-CURRENT_API_VERSION = 24
+CURRENT_API_VERSION = 25
 CURRENT_P5I_VERSION = 1
 
 logger = global_settings.logger
@@ -71,6 +73,12 @@
         INFO_MULTI_MATCH = 2
         INFO_ILLEGALS = 3
 
+        LIST_ALL = 0
+        LIST_INSTALLED = 1
+        LIST_INSTALLED_NEWEST = 2
+        LIST_NEWEST = 3
+        LIST_UPGRADABLE = 4
+
         # Private constants used for tracking which type of plan was made.
         __INSTALL = 1
         __UNINSTALL = 2
@@ -728,6 +736,591 @@
                         license_lst.append(LicenseInfo(text))
                 return license_lst
 
+        def get_pkg_categories(self, installed=False, pubs=misc.EmptyI):
+                """Returns an order list of tuples of the form (scheme,
+                category) containing the names of all categories in use by
+                the last version of each unique package in the catalog on a
+                per-publisher basis.
+
+                'installed' is an optional boolean value indicating whether
+                only the categories used by currently installed packages
+                should be returned.  If False, the categories used by the
+                latest vesion of every known package will be returned
+                instead.
+
+                'pubs' is an optional list of publisher prefixes to restrict
+                the results to."""
+
+                if installed:
+                        img_cat = self.__img.get_catalog(
+                            self.__img.IMG_CATALOG_INSTALLED)
+                        excludes = misc.EmptyI
+                else:
+                        img_cat = self.__img.get_catalog(
+                            self.__img.IMG_CATALOG_KNOWN)
+                        excludes = self.__img.list_excludes()
+                return sorted(img_cat.categories(excludes=excludes, pubs=pubs))
+
+        @staticmethod
+        def __get_pkg_cat_data(img_cat, info_needed, actions=None,
+            excludes=misc.EmptyI, pfmri=None):
+                # XXX this doesn't handle locale.
+                get_summ = summ = desc = cat_info = deps = None
+                cat_data = []
+                get_summ = PackageInfo.SUMMARY in info_needed
+                if PackageInfo.CATEGORIES in info_needed:
+                        cat_info = []
+                if PackageInfo.DEPENDENCIES in info_needed:
+                        cat_data.append(img_cat.DEPENDENCY)
+                        deps = []
+
+                if deps is None or len(info_needed) != 1:
+                        # Anything other than dependency data
+                        # requires summary data.
+                        cat_data.append(img_cat.SUMMARY)
+
+                if actions is None:
+                        actions = img_cat.get_entry_actions(pfmri, cat_data,
+                            excludes=excludes)
+
+                for a in actions:
+                        if deps is not None and a.name == "depend":
+                                deps.append(a.attrs.get(a.key_attr))
+                                continue
+                        elif a.name != "set":
+                                continue
+
+                        attr_name = a.attrs["name"]
+                        if attr_name == "pkg.summary":
+                                if get_summ:
+                                        summ = a.attrs["value"]
+                        elif attr_name in ("description", "pkg.description"):
+                                desc = a.attrs["value"]
+                        elif cat_info != None and a.has_category_info():
+                                cat_info.extend(a.parse_category_info())
+
+                if get_summ and summ is None:
+                        if desc is None:
+                                summ = ""
+                        else:
+                                summ = desc
+                if not PackageInfo.DESCRIPTION in info_needed:
+                        desc = None
+                return summ, desc, cat_info, deps
+
+        def get_pkg_list(self, pkg_list, cats=None, patterns=misc.EmptyI,
+            pubs=misc.EmptyI, variants=False):
+                """A generator function that produces tuples of the form ((pub,
+                stem, version), summary, categories, states).  Where 'pub' is
+                the publisher of the package, 'stem' is the name of the package,
+                'version' is a string for the package version, 'summary' is the
+                package summary, 'categories' is a list of tuples of the form
+                (scheme, category), and 'states' is a list of PackageInfo states
+                for the package.  Results are always sorted by stem, publisher,
+                and then in descending version order.
+
+                'pkg_list' is one of the following constant values indicating
+                what base set of package data should be used for results:
+
+                        LIST_ALL
+                                All known packages.
+
+                        LIST_INSTALLED
+                                Installed packages.
+
+                        LIST_INSTALLED_NEWEST
+                                Installed packages and the newest
+                                versions of packages not installed.
+                                Renamed packages that are listed in
+                                an installed incorporation will be
+                                excluded unless they are installed.
+
+                        LIST_NEWEST
+                                The newest versions of all known packages.
+
+                        LIST_UPGRADABLE
+                                Packages that are installed and upgradable.
+
+                'cats' is an optional list of package category tuples of the
+                form (scheme, cat) to restrict the results to.  If a package
+                is assigned to any of the given categories, it will be
+                returned.  A value of [] will return packages not assigned
+                to any package category.  A value of None indicates that no
+                package category filtering should be applied.
+
+                'patterns' is an optional list of FMRI wildcard strings to
+                filter results by.
+
+                'pubs' is an optional list of publisher prefixes to restrict
+                the results to.
+
+                'variants' is an optional boolean value that indicates that
+                packages that are for arch or zone variants not applicable to
+                this image should be returned.
+
+                Please note that this function may invoke network operations
+                to retrieve the requested package information."""
+
+                all = installed = inst_newest = newest = upgradable = False
+                if pkg_list == self.LIST_ALL:
+                        all = True
+                elif pkg_list == self.LIST_INSTALLED:
+                        installed = True
+                elif pkg_list == self.LIST_INSTALLED_NEWEST:
+                        inst_newest = True
+                elif pkg_list == self.LIST_NEWEST:
+                        newest = True
+                elif pkg_list == self.LIST_UPGRADABLE:
+                        upgradable = True
+
+                inc_vers = {}
+                brelease = self.__img.attrs["Build-Release"]
+
+                # Each pattern in patterns can be a partial or full FMRI, so
+                # extract the individual components for use in filtering.
+                illegals = []
+                pat_tuples = {}
+                MATCH_EXACT = 0
+                MATCH_FMRI = 1
+                MATCH_GLOB = 2
+                for pat in patterns:
+                        try:
+                                if "*" in pat or "?" in pat:
+                                        matcher = MATCH_GLOB
+
+                                        # XXX By default, matching FMRIs
+                                        # currently do not also use
+                                        # MatchingVersion.  If that changes,
+                                        # this should change too.
+                                        parts = pat.split("@", 1)
+                                        if len(parts) == 1:
+                                                npat = pkg.fmri.MatchingPkgFmri(
+                                                    pat, brelease)
+                                        else:
+                                                npat = pkg.fmri.MatchingPkgFmri(
+                                                    parts[0], brelease)
+                                                npat.version = \
+                                                    pkg.version.MatchingVersion(
+                                                    str(parts[1]), brelease)
+                                elif pat.startswith("pkg:/"):
+                                        matcher = MATCH_EXACT
+                                        npat = pkg.fmri.PkgFmri(pat,
+                                            brelease)
+                                else:
+                                        matcher = MATCH_FMRI
+                                        npat = pkg.fmri.PkgFmri(pat,
+                                            brelease)
+                                pat_tuples[pat] = (npat.tuple(), matcher)
+                        except (pkg.fmri.FmriError, pkg.version.VersionError):
+                                illegals.append(pat)
+
+                if illegals:
+                        raise api_errors.InventoryException(illegal=illegals)
+
+                # For LIST_INSTALLED_NEWEST, installed packages need to be
+                # determined and incorporation and publisher relationships
+                # mapped.
+                inst_stems = {}
+                ren_inst_stems = {}
+                ren_stems = {}
+
+                pub_ranks = {}
+                inc_stems = {}
+                if inst_newest:
+                        img_cat = self.__img.get_catalog(
+                            self.__img.IMG_CATALOG_INSTALLED)
+                        cat_info = frozenset([img_cat.DEPENDENCY])
+
+                        pub_ranks = self.__img.get_publisher_ranks()
+
+                        # The incorporation list should include all installed,
+                        # incorporated packages from all publishers.
+                        for t in img_cat.entry_actions(cat_info):
+                                (pub, stem, ver), entry, actions = t
+
+                                inst_stems[stem] = ver
+                                pkgr = False
+                                targets = set()
+                                for a in actions:
+                                        if a.name == "set" and \
+                                            a.attrs["name"] == "pkg.renamed":
+                                                pkgr = True
+                                                continue
+                                        elif a.name != "depend":
+                                                continue
+
+                                        if a.attrs["type"] == "require":
+                                                # Because the actions are not
+                                                # returned in a guaranteed
+                                                # order, the dependencies will
+                                                # have to be recorded for
+                                                # evaluation later.
+                                                targets.add(a.attrs["fmri"])
+                                        elif a.attrs["type"] == "incorporate":
+                                                # Record incorporated packages.
+                                                tgt = fmri.PkgFmri(
+                                                    a.attrs["fmri"], brelease)
+                                                tver = tgt.version
+                                                over = inc_vers.get(
+                                                    tgt.pkg_name, None)
+
+                                                # In case this package has been
+                                                # incorporated more than once,
+                                                # use the newest version.
+                                                if over is not None and \
+                                                    over > tver:
+                                                        continue
+                                                inc_vers[tgt.pkg_name] = tver
+
+                                if pkgr:
+                                        for f in targets:
+                                                tgt = fmri.PkgFmri(f, brelease)
+                                                ren_stems[tgt.pkg_name] = stem
+                                                ren_inst_stems.setdefault(stem,
+                                                    set())
+                                                ren_inst_stems[stem].add(
+                                                    tgt.pkg_name)
+
+                        def check_stem(t, entry):
+                                pub, stem, ver = t
+                                if stem in inst_stems:
+                                        iver = inst_stems[stem]
+                                        if stem in ren_inst_stems or \
+                                            ver == iver:
+                                                # The package has been renamed
+                                                # or the entry is for the same
+                                                # version as that which is
+                                                # installed, so doesn't need
+                                                # to be checked.
+                                                return False
+                                        # The package may have been renamed in
+                                        # a newer version, so must be checked.
+                                        return True
+                                elif stem in inc_vers:
+                                        # Package is incorporated, but not
+                                        # installed, so should be checked.
+                                        return True
+
+                                tgt = ren_stems.get(stem, None)
+                                while tgt is not None:
+                                        # This seems counter-intuitive, but
+                                        # for performance and other reasons,
+                                        # this stem should only be checked
+                                        # for a rename if it is incorporated
+                                        # or installed using a previous name.
+                                        if tgt in inst_stems or \
+                                            tgt in inc_vers:
+                                                return True
+                                        tgt = ren_stems.get(tgt, None)
+
+                                # Package should not be checked.
+                                return False
+
+                        img_cat = self.__img.get_catalog(
+                            self.__img.IMG_CATALOG_KNOWN)
+
+                        # Find terminal rename entry for all known packages not
+                        # rejected by check_stem().
+                        for t, entry, actions in img_cat.entry_actions(cat_info,
+                            cb=check_stem, last=True):
+                                pkgr = False
+                                targets = set()
+                                for a in actions:
+                                        if a.name == "set" and \
+                                            a.attrs["name"] == "pkg.renamed":
+                                                pkgr = True
+                                                continue
+
+                                        if a.name != "depend":
+                                                continue
+
+                                        if a.attrs["type"] != "require":
+                                                continue
+
+                                        # Because the actions are not
+                                        # returned in a guaranteed
+                                        # order, the dependencies will
+                                        # have to be recorded for
+                                        # evaluation later.
+                                        targets.add(a.attrs["fmri"])
+
+                                if pkgr:
+                                        pub, stem, ver = t
+                                        for f in targets:
+                                                tgt = fmri.PkgFmri(f, brelease)
+                                                ren_stems[tgt.pkg_name] = stem
+
+                        # Determine highest ranked publisher for package stems
+                        # listed in installed incorporations.
+                        def pub_order(a, b):
+                                return cmp(pub_ranks[a][0], pub_ranks[b][0])
+
+                        for p in sorted(pub_ranks, cmp=pub_order):
+                                if pubs and p not in pubs:
+                                        continue
+                                for stem in img_cat.names(pubs=[p]):
+                                        if stem in inc_vers:
+                                                inc_stems.setdefault(stem, p)
+
+                if installed or upgradable:
+                        img_cat = self.__img.get_catalog(
+                            self.__img.IMG_CATALOG_INSTALLED)
+
+                        # Don't need to perform variant filtering if only
+                        # listing installed packages.
+                        variants = True
+                else:
+                        img_cat = self.__img.get_catalog(
+                            self.__img.IMG_CATALOG_KNOWN)
+
+                cat_info = frozenset([img_cat.DEPENDENCY, img_cat.SUMMARY])
+                api_info = frozenset([PackageInfo.SUMMARY,
+                    PackageInfo.CATEGORIES])
+
+                # Keep track of when the newest version has been found for
+                # each incorporated stem.
+                slist = set()
+
+                # Keep track of listed stems for all other packages on a
+                # per-publisher basis.
+                nlist = set()
+
+                def check_state(t, entry):
+                        states = entry["metadata"]["states"]
+                        pkgi = self.__img.PKG_STATE_INSTALLED in states
+                        pkgu = self.__img.PKG_STATE_UPGRADABLE in states
+                        pub, stem, ver = t
+
+                        if upgradable:
+                                # If package is marked upgradable, return it.
+                                return pkgu
+                        elif pkgi:
+                                # Nothing more to do here.
+                                return True
+                        elif stem in inst_stems:
+                                # Some other version of this package is
+                                # installed, so this one should not be
+                                # returned.
+                                return False
+
+                        # Attempt to determine if this package is installed
+                        # under a different name or constrained under a
+                        # different name.
+                        tgt = ren_stems.get(stem, None)
+                        while tgt is not None:
+                                if tgt in inc_vers:
+                                        # Package is incorporated under a
+                                        # different name, so allow this
+                                        # to fallthrough to the incoporation
+                                        # evaluation.
+                                        break
+                                elif tgt in inst_stems:
+                                        # Package is installed under a
+                                        # different name, so skip it.
+                                        return False
+                                tgt = ren_stems.get(tgt, None)
+
+                        # Attempt to find a suitable version to return.
+                        if stem in inc_vers:
+                                # For package stems that are incorporated, only
+                                # return the newest successor version  based on
+                                # publisher rank.
+                                if stem in slist:
+                                        # Newest version already returned.
+                                        return False
+
+                                if stem in inc_stems and \
+                                    pub != inc_stems[stem]:
+                                        # This entry is for a lower-ranked
+                                        # publisher.
+                                        return False
+
+                                # XXX version should not require build release.
+                                ever = pkg.version.Version(ver, brelease)
+
+                                # If the entry's version is a successor to
+                                # the incorporated version, then this is the
+                                # 'newest' version of this package since
+                                # entries are processed in descending version
+                                # order.
+                                iver = inc_vers[stem]
+                                if ever.is_successor(iver,
+                                    pkg.version.CONSTRAINT_AUTO):
+                                        slist.add(stem)
+                                        return True
+                                return False
+
+                        pkg_stem = pub + "!" + stem
+                        if pkg_stem in nlist:
+                                # A newer version has already been listed for
+                                # this stem and publisher.
+                                return False
+                        return True
+
+                filter_cb = None
+                if inst_newest or upgradable:
+                        # Filtering needs to be applied.
+                        filter_cb = check_state
+
+                arch = self.__img.get_arch()
+                excludes = self.__img.list_excludes()
+                is_zone = self.__img.is_zone()
+
+                for t, entry, actions in img_cat.entry_actions(cat_info,
+                    cb=filter_cb, excludes=excludes, last=newest,
+                    ordered=True, pubs=pubs):
+                        pub, stem, ver = t
+
+                        # Perform image arch and zone variant filtering so
+                        # that only packages appropriate for this image are
+                        # returned, but only do this for packages that are
+                        # not installed.
+                        pcats = []
+                        pkgr = False
+                        omit_package = False
+                        summ = None
+                        targets = set()
+
+                        states = entry["metadata"]["states"]
+                        pkgi = self.__img.PKG_STATE_INSTALLED in states
+                        for a in actions:
+                                if a.name == "depend" and \
+                                    a.attrs["type"] == "require":
+                                        targets.add(a.attrs["fmri"])
+                                        continue
+                                if a.name != "set":
+                                        continue
+
+                                atname = a.attrs["name"]
+                                atvalue = a.attrs["value"]
+                                if atname == "pkg.summary":
+                                        summ = atvalue
+                                        continue
+
+                                if atname in ("description",
+                                    "pkg.description"):
+                                        if summ is None:
+                                                summ = atvalue
+                                        continue
+
+                                if atname == "info.classification":
+                                        pcats.extend(
+                                            a.parse_category_info())
+
+                                if pkgi:
+                                        # No filtering for installed packages.
+                                        continue
+
+                                # Rename filtering should only be performed for
+                                # incorporated packages at this point.
+                                if atname == "pkg.renamed":
+                                        if stem in inc_vers:
+                                                pkgr = True
+                                        continue
+
+                                if variants:
+                                        # No variant filtering.
+                                        continue
+
+                                is_list = type(atvalue) == list
+                                if atname == "variant.arch":
+                                        if (is_list and arch not in atvalue) or \
+                                           (not is_list and arch != atvalue):
+                                                # Package is not for the
+                                                # image's architecture.
+                                                omit_package = True
+                                                continue
+
+                                if atname == "variant.opensolaris.zone":
+                                        if (is_zone and is_list and 
+                                            "nonglobal" not in atvalue) or \
+                                           (is_zone and not is_list and
+                                            atvalue != "nonglobal"):
+                                                # Package is for zones only.
+                                                omit_package = True
+
+                        if filter_cb is not None:
+                                pkg_stem = pub + "!" + stem
+                                nlist.add(pkg_stem)
+
+                        if not pkgi and pkgr and stem in inc_vers:
+                                # If the package is not installed, but this is
+                                # the terminal version entry for the stem and
+                                # it is an incorporated package, then omit the
+                                # package if it has been installed or is
+                                # incorporated using one of the new names.
+                                for e in targets:
+                                        tgt = e
+                                        while tgt is not None:
+                                                if tgt in ren_inst_stems or \
+                                                    tgt in inc_vers:
+                                                        omit_package = True
+                                                        break
+                                                tgt = ren_stems.get(tgt, None)
+
+                        # Pattern filtering has to be applied last so that
+                        # renames, incorporations, and everything else is
+                        # handled correctly.
+                        if not omit_package:
+                                for pat in patterns:
+                                        (pat_pub, pat_stem, pat_ver), matcher = \
+                                            pat_tuples[pat]
+
+                                        if pat_pub is not None and \
+                                            pub != pat_pub:
+                                                # Publisher doesn't match.
+                                                omit_package = True
+                                                continue
+
+                                        if matcher == MATCH_EXACT:
+                                                if pat_stem != stem:
+                                                        # Stem doesn't match.
+                                                        omit_package = True
+                                                        continue
+                                        elif matcher == MATCH_FMRI:
+                                                if not ("/" + stem).endswith(
+                                                    "/" + pat_stem):
+                                                        # Stem doesn't match.
+                                                        omit_package = True
+                                                        continue
+                                        elif matcher == MATCH_GLOB:
+                                                if not fnmatch.fnmatchcase(stem,
+                                                    pat_stem):
+                                                        # Stem doesn't match.
+                                                        omit_package = True
+                                                        continue
+
+                                        if pat_ver is not None:
+                                                ever = pkg.version.Version(ver,
+                                                    brelease)
+                                                if not ever.is_successor(pat_ver,
+                                                    pkg.version.CONSTRAINT_AUTO):
+                                                        omit_package = True
+                                                        continue
+
+                                        # If this entry matched at least one
+                                        # pattern, then ensure it is returned.
+                                        omit_package = False
+                                        break
+
+                        if omit_package:
+                                continue
+
+                        if cats is not None:
+                                if not cats:
+                                        if pcats:
+                                                # Only want packages with no
+                                                # categories.
+                                                continue
+                                elif not [sc for sc in cats if sc in pcats]:
+                                        # Package doesn't match specified
+                                        # category criteria.
+                                        continue
+
+                        # Return the requested package data.
+                        yield (t, summ, pcats,
+                            frozenset(entry["metadata"]["states"]))
+
         def info(self, fmri_strings, local, info_needed):
                 """Gathers information about fmris.  fmri_strings is a list
                 of fmri_names for which information is desired.  local
@@ -798,10 +1391,10 @@
                                 for m, state in matches:
                                         if m.get_publisher() == ppub:
                                                 pnames[m.get_pkg_stem()] = 1
-                                                pmatch.append(m)
+                                                pmatch.append((m, state))
                                         else:
                                                 npnames[m.get_pkg_stem()] = 1
-                                                npmatch.append(m)
+                                                npmatch.append((m, state))
 
                                 if len(pnames.keys()) > 1:
                                         multiple_matches.append(
@@ -830,55 +1423,18 @@
                             self.__img.IMG_CATALOG_KNOWN)
                 excludes = self.__img.list_excludes()
 
-                # Set of summary-related options that are in catalog data.
-                summ_opts = frozenset([PackageInfo.SUMMARY,
-                    PackageInfo.CATEGORIES, PackageInfo.DESCRIPTION])
-
-                # Set of all options that are in catalog data.
-                cat_opts = summ_opts | frozenset([PackageInfo.DEPENDENCIES])
+                # Set of options that can use catalog data.
+                cat_opts = frozenset([PackageInfo.SUMMARY,
+                    PackageInfo.CATEGORIES, PackageInfo.DESCRIPTION,
+                    PackageInfo.DEPENDENCIES])
 
                 # Set of options that require manifest retrieval.
                 act_opts = PackageInfo.ACTION_OPTIONS - \
                     frozenset([PackageInfo.DEPENDENCIES])
 
-                def get_pkg_cat_data(f):
-                        # XXX this doesn't handle locale.
-                        get_summ = summ = desc = cat_info = deps = None
-                        cat_data = []
-                        if summ_opts & info_needed:
-                                cat_data.append(img_cat.SUMMARY)
-                                get_summ = PackageInfo.SUMMARY in info_needed
-                        if PackageInfo.CATEGORIES in info_needed:
-                                cat_info = []
-                        if PackageInfo.DEPENDENCIES in info_needed:
-                                cat_data.append(img_cat.DEPENDENCY)
-                                deps = []
-
-                        for a in img_cat.get_entry_actions(f, cat_data,
-                            excludes=excludes):
-                                if a.name == "depend":
-                                        deps.append(a.attrs.get(a.key_attr))
-                                elif a.attrs["name"] == "pkg.summary":
-                                        if get_summ:
-                                                summ = a.attrs["value"]
-                                elif a.attrs["name"] in ("description",
-                                    "pkg.description"):
-                                        desc = a.attrs["value"]
-                                elif cat_info != None and a.has_category_info():
-                                        cat_info.extend(
-                                            PackageCategory(scheme, cat)
-                                            for scheme, cat
-                                            in a.parse_category_info())
-
-                        if get_summ and summ == None:
-                                summ = desc
-                        if not PackageInfo.DESCRIPTION in info_needed:
-                                desc = None
-                        return summ, desc, cat_info, deps
-
                 pis = []
-                for f in fmris:
-                        pub = name = version = release = states = None
+                for f, fstate in fmris:
+                        pub = name = version = release = None
                         build_release = branch = packaging_date = None
                         if PackageInfo.IDENTITY in info_needed:
                                 pub, name, version = f.tuple()
@@ -888,22 +1444,41 @@
                                 branch = version.branch
                                 packaging_date = \
                                     version.get_timestamp().strftime("%c")
+
                         pref_pub = None
                         if PackageInfo.PREF_PUBLISHER in info_needed:
-                                pref_pub = f.get_publisher() == ppub
-                        state = None
+                                pref_pub = (f.get_publisher() == ppub)
+
+                        # XXX gross; info needs to switch to using get_pkg_list
+                        # routines at a later date once matching is figured
+                        # out.
+                        states = set()
                         if PackageInfo.STATE in info_needed:
-                                states = self.__img.get_pkg_state(f)
+                                if fstate["state"] == \
+                                    self.__img.PKG_STATE_INSTALLED:
+                                        states.add(PackageInfo.INSTALLED)
+                                if fstate["in_catalog"]:
+                                        states.add(PackageInfo.KNOWN)
+                                if fstate["upgradable"]:
+                                        states.add(PackageInfo.UPGRADABLE)
+                                if fstate["obsolete"]:
+                                        states.add(PackageInfo.OBSOLETE)
+                                if fstate["renamed"]:
+                                        states.add(PackageInfo.RENAMED)
+
                         links = hardlinks = files = dirs = dependencies = None
                         summary = size = licenses = cat_info = description = \
                             None
 
-                        if frozenset([PackageInfo.SUMMARY,
-                            PackageInfo.CATEGORIES,
-                            PackageInfo.DESCRIPTION,
-                            PackageInfo.DEPENDENCIES]) & info_needed:
+                        if cat_opts & info_needed:
                                 summary, description, cat_info, dependencies = \
-                                    get_pkg_cat_data(f)
+                                    self.__get_pkg_cat_data(img_cat,
+                                        info_needed, excludes=excludes, pfmri=f)
+                                if cat_info is not None:
+                                        cat_info = [ 
+                                            PackageCategory(scheme, cat)
+                                            for scheme, cat in cat_info
+                                        ]
 
                         if (frozenset([PackageInfo.SIZE,
                             PackageInfo.LICENSES]) | act_opts) & info_needed:
@@ -1683,11 +2258,17 @@
         could need. The fmri is guaranteed to be set. All other values may
         be None, depending on how the PackageInfo instance was created."""
 
-        # Possible package installation states
-        INSTALLED = 1
-        NOT_INSTALLED = 2
-        OBSOLETE = 3
-        RENAMED = 4
+        # Possible package installation states; these constants should match
+        # the values used by the Image class.  Constants with negative values
+        # are not currently available.
+        FROZEN = -1
+        INCORPORATED = -2
+        EXCLUDES = -3
+        KNOWN = image.Image.PKG_STATE_KNOWN
+        INSTALLED = image.Image.PKG_STATE_INSTALLED
+        UPGRADABLE = image.Image.PKG_STATE_UPGRADABLE
+        OBSOLETE = image.Image.PKG_STATE_OBSOLETE
+        RENAMED = image.Image.PKG_STATE_RENAMED
 
         __NUM_PROPS = 13
         IDENTITY, SUMMARY, CATEGORIES, STATE, PREF_PUBLISHER, SIZE, LICENSES, \
@@ -1708,6 +2289,7 @@
                 if category_info_list is None:
                         category_info_list = []
                 self.category_info_list = category_info_list
+                self.states = states
                 self.publisher = publisher
                 self.preferred_publisher = preferred_publisher
                 self.version = version
@@ -1723,32 +2305,10 @@
                 self.dirs = dirs
                 self.dependencies = dependencies
                 self.description = description
-                self.states = self.__map_states(states)
 
         def __str__(self):
                 return self.fmri
 
-        @classmethod
-        def __map_states(cls, states):
-                d = {
-                    0: cls.NOT_INSTALLED,
-                    2: cls.INSTALLED,
-                    8: cls.OBSOLETE,
-                    9: cls.RENAMED
-                }
-
-                if not states:
-                        return []
-
-                return [
-                    t
-                    for t in (
-                        d.get(s, -1)
-                        for s in states
-                    )
-                    if t != -1
-                ]
-
         @staticmethod
         def build_from_fmri(f):
                 if not f: