16419868 pkgrepo verify subcommand needed s11-update s11u2b11
authorthejaswini.k@oracle.COM
Thu, 07 Mar 2013 10:46:12 +0530
branchs11-update
changeset 2877 6019cb562993
parent 2855 ea6a1ad661b8
child 2883 22702a36675d
16419868 pkgrepo verify subcommand needed 16419877 pkgrepo fix subcommand needed
src/modules/actions/__init__.py
src/modules/client/progress.py
src/modules/file_layout/file_manager.py
src/modules/manifest.py
src/modules/server/repository.py
src/pkgrepo.py
src/tests/cli/t_pkgrepo.py
src/tests/pkg5unittest.py
--- a/src/modules/actions/__init__.py	Wed Feb 06 13:01:46 2013 -0800
+++ b/src/modules/actions/__init__.py	Thu Mar 07 10:46:12 2013 +0530
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2007, 2012, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2007, 2013, Oracle and/or its affiliates. All rights reserved.
 #
 
 """
@@ -32,8 +32,12 @@
 package directory and they'll just be picked up.  The current package contents
 can be seen in the section "PACKAGE CONTENTS", below.
 
-This package has one data member: "types".  This is a dictionary which maps the
-action names to the classes that represent them.
+This package has two data members:
+  "types", a dictionary which maps the action names to the classes that
+  represent them.
+
+  "payload_types", a dictionary which maps action names that deliver payload
+  to the classes that represent them.
 
 This package also has one function: "fromstr", which creates an action instance
 based on a str() representation of an action.
@@ -53,6 +57,12 @@
 # A dictionary of all the types in this package, mapping to the classes that
 # define them.
 types = {}
+
+# A dictionary of the action names in this package that deliver payload,
+# according to the 'has_payload' member in each class. The dictionary is keyed
+# by the action name, and has the action class as its values.
+payload_types = {}
+
 for modname in __all__:
         module = __import__("%s.%s" % (__name__, modname),
             globals(), locals(), [modname])
@@ -69,6 +79,8 @@
         for cls in classes:
                 if hasattr(cls, "name"):
                         types[cls.name] = cls
+                if hasattr(cls, "has_payload") and cls.has_payload:
+                        payload_types[cls.name] = cls
 
 # Clean up after ourselves
 del f, modname, module, nvlist, classes, c, cls
--- a/src/modules/client/progress.py	Wed Feb 06 13:01:46 2013 -0800
+++ b/src/modules/client/progress.py	Thu Mar 07 10:46:12 2013 +0530
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2008, 2012, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved.
 #
 
 #
@@ -576,6 +576,26 @@
         def _ver_output_done(self): pass
 
         @pt_abstract
+        def _repo_ver_output(self, outspec, repository_scan=False): pass
+
+        @pt_abstract
+        def _repo_ver_output_error(self, errors): pass
+
+        @pt_abstract
+        def _repo_ver_output_done(self): pass
+
+        def _repo_fix_output(self, outspec): pass
+
+        @pt_abstract
+        def _repo_fix_output_error(self, errors): pass
+
+        @pt_abstract
+        def _repo_fix_output_info(self, errors): pass
+
+        @pt_abstract
+        def _repo_fix_output_done(self): pass
+
+        @pt_abstract
         def _archive_output(self, outspec): pass
 
         @pt_abstract
@@ -661,6 +681,8 @@
         JOB_REPO_ANALYZE_REPO = 310
         JOB_REPO_RM_MFST = 311
         JOB_REPO_RM_FILES = 312
+        JOB_REPO_VERIFY_REPO = 313
+        JOB_REPO_FIX_REPO = 314
 
         # Operation purpose.  This set of modes is used by callers to indicate
         # to the progress tracker what's going on at a high level.  This allows
@@ -819,6 +841,47 @@
         @pt_abstract
         def verify_done(self): pass
 
+        # verifying the content of a repository
+        @pt_abstract
+        def repo_verify_start(self, npkgs): pass
+
+        @pt_abstract
+        def repo_verify_start_pkg(self, pkgfmri, repository_scan=False): pass
+
+        @pt_abstract
+        def repo_verify_add_progress(self, pkgfmri): pass
+
+        @pt_abstract
+        def repo_verify_yield_error(self, pkgfmri, errors): pass
+
+        @pt_abstract
+        def repo_verify_yield_warning(self, pkgfmri, warnings): pass
+
+        @pt_abstract
+        def repo_verify_yield_info(self, pkgfmri, info): pass
+
+        @pt_abstract
+        def repo_verify_end_pkg(self, pkgfmri): pass
+
+        @pt_abstract
+        def repo_verify_done(self): pass
+
+        # fixing the content of a repository
+        @pt_abstract
+        def repo_fix_start(self, npkgs): pass
+
+        @pt_abstract
+        def repo_fix_add_progress(self, pkgfmri): pass
+
+        @pt_abstract
+        def repo_fix_yield_error(self, pkgfmri, errors): pass
+
+        @pt_abstract
+        def repo_fix_yield_info(self, pkgfmri, info): pass
+
+        @pt_abstract
+        def repo_fix_done(self): pass
+
         # archiving to .p5p files
         @pt_abstract
         def archive_set_goal(self, arcname, nitems, nbytes): pass
@@ -997,6 +1060,9 @@
                 self.mfst_commit = GoalTrackerItem(_("Committed Manifests"))
 
                 self.ver_pkgs = GoalTrackerItem(_("Verify Packages"))
+                self.repo_ver_pkgs = GoalTrackerItem(
+                    _("Verify Repository Content"))
+                self.repo_fix = GoalTrackerItem(_("Fix Repository Content"))
 
                 # archiving support
                 self.archive_items = GoalTrackerItem(_("Archived items"))
@@ -1066,7 +1132,12 @@
                         self.JOB_REPO_RM_MFST:
                             GoalTrackerItem(_("Removing package manifests")),
                         self.JOB_REPO_RM_FILES:
-                            GoalTrackerItem(_("Removing package files"))
+                            GoalTrackerItem(_("Removing package files")),
+                        self.JOB_REPO_VERIFY_REPO:
+                            GoalTrackerItem(_("Verifying repository content")),
+                        self.JOB_REPO_FIX_REPO:
+                            GoalTrackerItem(_("Fixing repository content"))
+
                 }
 
                 self.reset_download()
@@ -1258,6 +1329,46 @@
         def verify_done(self):
                 self.ver_pkgs.done()
 
+        def repo_verify_start(self, npkgs):
+                self.repo_ver_pkgs.reset()
+                self.repo_ver_pkgs.goalitems = npkgs
+
+        def repo_verify_start_pkg(self, pkgfmri, repository_scan=False):
+                if pkgfmri != self.repo_ver_pkgs.curinfo:
+                        self.repo_ver_pkgs.items += 1
+                        self.repo_ver_pkgs.curinfo = pkgfmri
+                self._repo_ver_output(OutSpec(changed=["startpkg"]),
+                    repository_scan=repository_scan)
+
+        def repo_verify_add_progress(self, pkgfmri):
+                self._repo_ver_output(OutSpec())
+
+        def repo_verify_yield_error(self, pkgfmri, errors):
+                self._repo_ver_output_error(errors)
+
+        def repo_verify_end_pkg(self, pkgfmri):
+                self._repo_ver_output(OutSpec(changed=["endpkg"]))
+                self.repo_ver_pkgs.curinfo = None
+
+        def repo_verify_done(self):
+                self.repo_ver_pkgs.done()
+
+        def repo_fix_start(self, nitems):
+                self.repo_fix.reset()
+                self.repo_fix.goalitems = nitems
+
+        def repo_fix_add_progress(self, pkgfmri):
+                self._repo_fix_output(OutSpec())
+
+        def repo_fix_yield_error(self, pkgfmri, errors):
+                self._repo_fix_output_error(errors)
+
+        def repo_fix_yield_info(self, pkgfmri, info):
+                self._repo_fix_output_info(info)
+
+        def repo_fix_done(self):
+                self.repo_fix.done()
+
         def archive_set_goal(self, arcname, nitems, nbytes):
                 self._archive_name = arcname # pylint: disable=W0201
                 self.archive_items.goalitems = nitems
@@ -1891,6 +2002,33 @@
         def _ver_output_done(self):
                 pass
 
+        def _repo_ver_output(self, outspec, repository_scan=False):
+                pass
+
+        def _repo_ver_output_error(self, errors):
+                self._pe.cprint(errors)
+
+        def _repo_ver_output_warning(self, warnings):
+                pass
+
+        def _repo_ver_output_info(self, info):
+                pass
+
+        def _repo_ver_output_done(self):
+                pass
+
+        def _repo_fix_output(self, outspec):
+                pass
+
+        def _repo_fix_output_error(self, errors):
+                self._pe.cprint(errors)
+
+        def _repo_fix_output_info(self, info):
+                self._pe.cprint(info)
+
+        def _repo_fix_output_done(self):
+                pass
+
         def _dl_output(self, outspec):
                 if not self._ptimer.time_to_print() and not outspec.first and \
                     not outspec.last:
@@ -2284,6 +2422,46 @@
                 # We just erase the "Verifying" progress line.
                 self._pe.cprint("", end='', erase=True)
 
+        def _repo_ver_output(self, outspec, repository_scan=False):
+                """If 'repository_scan' is set and we have no FRMRI set, we emit
+                a message saying that we're performing a scan if the repository.
+                If we have no FMRI, we emit a message saying we don't know what
+                package we're looking at.
+
+                """
+                if not self.repo_ver_pkgs.curinfo:
+                        if repository_scan:
+                                pkg_stem = _("Scanning repository "
+                                    "(this could take some time)")
+                        else:
+                                pkg_stem = _("Unknown package")
+                else:
+                        pkg_stem = self.repo_ver_pkgs.curinfo.get_pkg_stem()
+                if not self._ptimer.time_to_print() and not outspec:
+                        return
+                if "endpkg" in outspec.changed:
+                        self._pe.cprint("", end='', erase=True)
+                        return
+                s = "%-64s %s %c" % \
+                    (pkg_stem, self.repo_ver_pkgs.pair(), self._spinner())
+                self._pe.cprint(s, end='', erase=True)
+
+        def _repo_ver_output_error(self, errors):
+                self._output_flush()
+                self._pe.cprint(errors)
+
+        def _repo_fix_output(self, outspec):
+                s = "%c" %  self._spinner()
+                self._pe.cprint(s, end='', erase=True)
+
+        def _repo_fix_output_error(self, errors):
+                self._output_flush()
+                self._pe.cprint(errors)
+
+        def _repo_fix_output_info(self, info):
+                self._output_flush()
+                self._pe.cprint(info)
+
         def _archive_output(self, outspec):
                 if not self._ptimer.time_to_print() and not outspec:
                         return
--- a/src/modules/file_layout/file_manager.py	Wed Feb 06 13:01:46 2013 -0800
+++ b/src/modules/file_layout/file_manager.py	Thu Mar 07 10:46:12 2013 +0530
@@ -19,7 +19,7 @@
 #
 # CDDL HEADER END
 #
-# Copyright (c) 2009, 2012, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved.
 
 """centralized object for insert, lookup, and removal of files.
 
@@ -159,7 +159,7 @@
                                 return cur_full_path, dest_full_path
                 return None, dest_full_path
 
-        def lookup(self, hashval, opener=False):
+        def lookup(self, hashval, opener=False, check_existence=True):
                 """Find the file for hashval.
 
                 The "hashval" parameter contains the name of the file to be
@@ -169,7 +169,7 @@
                 return a path or an open file handle."""
 
                 cur_full_path, dest_full_path = self.__select_path(hashval,
-                    True)
+                    check_existence)
                 if not cur_full_path:
                         return None
 
--- a/src/modules/manifest.py	Wed Feb 06 13:01:46 2013 -0800
+++ b/src/modules/manifest.py	Thu Mar 07 10:46:12 2013 +0530
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2007, 2012, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2007, 2013, Oracle and/or its affiliates. All rights reserved.
 #
 
 from collections import namedtuple, defaultdict
@@ -489,7 +489,7 @@
                                 yield a
 
         def gen_key_attribute_value_by_type(self, atype, excludes=EmptyI):
-                """Generate the value of the key atrribute for each action
+                """Generate the value of the key attribute for each action
                 of type "type" in the manifest."""
 
                 return (
--- a/src/modules/server/repository.py	Wed Feb 06 13:01:46 2013 -0800
+++ b/src/modules/server/repository.py	Thu Mar 07 10:46:12 2013 +0530
@@ -19,21 +19,24 @@
 #
 # CDDL HEADER END
 #
-# Copyright (c) 2008, 2012, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved.
 
 import cStringIO
 import codecs
 import datetime
 import errno
+import hashlib
 import logging
 import os
 import os.path
-import platform
 import shutil
 import stat
 import sys
 import tempfile
 import urllib
+import zlib
+
+import M2Crypto as m2
 
 import pkg.actions as actions
 import pkg.catalog as catalog
@@ -59,6 +62,22 @@
 import pkg.version
 
 CURRENT_REPO_VERSION = 4
+
+REPO_QUARANTINE_DIR = "pkg5-quarantine"
+
+REPO_VERIFY_BADHASH = 0
+REPO_VERIFY_BADMANIFEST = 1
+REPO_VERIFY_BADGZIP = 2
+REPO_VERIFY_NOFILE = 3
+REPO_VERIFY_PERM = 4
+REPO_VERIFY_MFPERM = 5
+REPO_VERIFY_BADSIG = 6
+REPO_VERIFY_WARN_OPENPERMS = 7
+REPO_VERIFY_UNKNOWN = 99
+
+REPO_FIX_ITEM = 0
+REPO_FIX_FAILED = 1
+
 from pkg.pkggzip import PkgGzipFile
 
 class RepositoryError(Exception):
@@ -237,18 +256,37 @@
 
 
 class RepositoryVersionError(RepositoryError):
-        """Raised when the repository specified uses an unsupported format
-        (version).
+        """Raised when the repository specified uses a format greater than the
+        current format (version).
         """
 
-        def __init__(self, location, version):
+        def __init__(self, location, version, current_version):
                 RepositoryError.__init__(self)
                 self.location = location
                 self.version = version
+                self.current_version = current_version
 
         def __str__(self):
                 return("The repository at '%(location)s' is version "
-                    "'%(version)s'; only versions up to are supported.") % \
+                    "'%(version)s'; only versions up to %(current_version)s are"
+                    " supported.") %  self.__dict__
+
+
+class RepositoryInvalidVersionError(RepositoryError):
+        """Raised when the repository specified uses an unsupported format.
+        (version).
+        """
+
+        def __init__(self, location, version, supported):
+                RepositoryError.__init__(self)
+                self.location = location
+                self.version = version
+                self.supported = supported
+
+        def __str__(self):
+                return("The repository at '%(location)s' is version "
+                    "'%(version)s'; only version %(supported)s repositories are"
+                    " supported.") % \
                     self.__dict__
 
 
@@ -261,6 +299,22 @@
                 return("Operation not supported for this configuration.")
 
 
+class RepositoryQuarantinedPathExistsError(RepositoryError):
+        """Raised when the repository is unable to quarantine a file because
+        a file of that name is already in quarantine.
+        """
+
+        def __str__(self):
+                return _("Quarantined path already exists.")
+
+
+class RepositorySigNoTrustAnchorDirError(RepositoryError):
+        """Raised when the repository trust anchor directory could not be found
+        while performing repository verification."""
+
+        def __str__(self):
+                return _("Unable to find trust anchor directory %s") % self.data
+
 class _RepoStore(object):
         """The _RepoStore object provides an interface for performing operations
         on a set of package data contained within a repository.  This class is
@@ -1802,6 +1856,597 @@
                         if fn and os.path.exists(fn):
                                 os.unlink(fn)
 
+        def __build_verify_error(self, error, path, reason):
+                """Given an integer error code, a path within the repository,
+                and a 'reason' dictionary containing more information about
+                the error, return a tuple of the form:
+
+                (error_code, message, path, reason)
+                """
+
+                # if we're looking at a path to a file hash, and we have a pfmri
+                # we'd like to print the 'path' attribute as well as its
+                # location within the repository.
+                hsh = reason.get("hash")
+                pfmri = reason.get("pkg")
+                if hsh and pfmri:
+                        m = self._get_manifest(pfmri)
+                        for ac in m.gen_actions_by_types(
+                            actions.payload_types.keys()):
+                                if ac.hash == hsh:
+                                        fpath = ac.attrs.get("path")
+                                        if fpath:
+                                                reason["fpath"] = fpath
+
+                message = _("Unknown error")
+                if error == REPO_VERIFY_BADHASH:
+                        message = _("Invalid file hash: %s") % reason["hash"]
+                        del reason["hash"]
+                elif error == REPO_VERIFY_BADMANIFEST:
+                        message = _("Corrupt manifest.")
+                        reason["err"] = _("Use pkglint(1) for more details.")
+                elif error == REPO_VERIFY_NOFILE:
+                        message = _("Missing file: %s") % reason["hash"]
+                        del reason["hash"]
+                elif error == REPO_VERIFY_BADGZIP:
+                        message = _("Corrupted gzip file.")
+                elif error in [REPO_VERIFY_PERM, REPO_VERIFY_MFPERM]:
+                        message = _("Verification failure: %s") % reason["err"]
+                        del reason["err"]
+                elif error == REPO_VERIFY_UNKNOWN:
+                        message = _("Bad manifest.")
+                elif error == REPO_VERIFY_BADSIG:
+                        message = _("Bad signature.")
+                elif error == REPO_VERIFY_WARN_OPENPERMS:
+                        # This message constitutes a warning rather than
+                        # an error. No attempt is made by 'pkgrepo fix'
+                        # to rectify this error, as it is potentially
+                        # outside our responsibility to do so.
+                        message = _("Restrictive permissions.")
+                        reason["err"] = \
+                            _("Some repository content for publisher '%s' "
+                            "or paths leading to the repository were not "
+                            "world-readable or were not readable by "
+                            "'pkg5srv:pkg5srv', which can cause access errors "
+                            "if the repository contents are served by the "
+                            "following services:\n"
+                            " svc:/application/pkg/server\n"
+                            " svc:/application/pkg/system-repository.\n"
+                            "Only the first path found with restrictive "
+                            "permissions is shown.") % reason["pub"]
+                        del reason["pub"]
+                else:
+                        raise Exception(
+                            "Unknown repository verify error code: %s" % error)
+
+                return error, path, message, reason
+
+        def __get_hashes(self, path, pfmri):
+                """Given an PkgFmri, return a set containing all of the
+                hashes of the files its manifest references."""
+
+                hashes = set()
+                errors = []
+                try:
+                        m = self._get_manifest(pfmri)
+                        for a in m.gen_actions():
+                                if not a.has_payload or not a.hash:
+                                        continue
+
+                                # Action payload.
+                                hashes.add(a.hash)
+
+                                # Signature actions have additional payloads
+                                if a.name == "signature":
+                                        hashes.update(
+                                            a.attrs.get("chain", "").split())
+                except apx.PermissionsException:
+                        errors.append((REPO_VERIFY_MFPERM, path,
+                            {"err": _("Permission denied.")}))
+                return hashes, errors
+
+        def __verify_manifest(self, path, pfmri):
+                """Verify that a manifest can be found for this pfmri"""
+                try:
+                        m = self._get_manifest(pfmri)
+                except apx.InvalidPackageErrors:
+                        return (REPO_VERIFY_BADMANIFEST, path, {})
+                except apx.PermissionsException, e:
+                        return (REPO_VERIFY_PERM, path, {"err": str(e),
+                            "pkg": pfmri})
+
+        def __verify_hash(self, path, pfmri, h):
+                """Perform hash verification on the given gzip file."""
+
+                gzf = None
+                hash = os.path.basename(path)
+                try:
+                        gzf = PkgGzipFile(fileobj=open(path, "rb"))
+                        fhash = hashlib.sha1()
+                        fhash.update(gzf.read())
+                        actual = fhash.hexdigest()
+                        if actual != h:
+                                return (REPO_VERIFY_BADHASH, path,
+                                    {"actual": actual, "hash": hash,
+                                    "pkg": pfmri})
+                except (ValueError, zlib.error), e:
+                        return (REPO_VERIFY_BADGZIP, path,
+                            {"hash": hash, "pkg": pfmri})
+                except IOError, e:
+                        if e.errno in [errno.EACCES, errno.EPERM]:
+                                return (REPO_VERIFY_PERM, path,
+                                    {"err": str(e), "hash": hash,
+                                    "pkg": pfmri})
+                        else:
+                                return (REPO_VERIFY_BADGZIP, path,
+                                    {"hash": hash, "pkg": pfmri})
+                finally:
+                        if gzf:
+                                gzf.close()
+
+        def __verify_perm(self, path, pfmri, h):
+                """Check that we don't get any permissions errors when
+                trying to stat the given path."""
+                try:
+                        st = os.stat(path)
+                        # if it's a directory, we'll try to list it
+                        if stat.S_ISDIR(st.st_mode):
+                                os.listdir(path)
+                except OSError, e:
+                        if e.errno in [errno.EPERM, errno.EACCES]:
+                                if not pfmri:
+                                        return (REPO_VERIFY_MFPERM, path,
+                                            {"err": str(e)})
+                                return (REPO_VERIFY_PERM, path,
+                                    {"hash": h, "err": str(e), "pkg": pfmri})
+                        else:
+                                return (REPO_VERIFY_NOFILE, path,
+                                    {"hash": h, "err": str(e), "pkg": pfmri})
+
+        def __verify_signature(self, path, pfmri, pub, trust_anchors,
+            sig_required_names, use_crls):
+                """Verify signatures on a given FMRI."""
+                m = self._get_manifest(pfmri)
+                errors = []
+                for sig in m.gen_actions_by_type("signature"):
+                        try:
+                                sig.verify_sig(m.gen_actions(), pub,
+                                    trust_anchors, use_crls,
+                                    required_names=sig_required_names)
+                        except apx.UnverifiedSignature, e:
+                                errors.append((REPO_VERIFY_BADSIG, path,
+                                    {"err": str(e), "pkg": pfmri}))
+                        except apx.CertificateException, e:
+                                errors.append((REPO_VERIFY_BADSIG, path,
+                                    {"err": str(e), "pkg": pfmri}))
+                return errors
+
+        def __verify_permissions(self):
+                """Determine if any of the files or directories in the
+                repository are not readable by either the pkg5srv user or group,
+                or are not world readable.  We return as soon as we find one
+                inaccessible file, returning the name of that file or directory.
+
+                We also walk up the directory path from the repository to '/'
+                checking that the repository would be accessible.
+
+                We only return the first file found because for large
+                repositories with many files affected, we'd be flooding the user
+                with information.
+
+                This check exists as well as verify_perm() since it causes a
+                warning to be emitted if non-world readable files are found,
+                whereas verify_perm() will emit an error.
+                """
+
+                pkg5srv_uid = \
+                    pkg.portable.get_user_by_name("pkg5srv", "/etc", None)
+                pkg5srv_gid = \
+                    pkg.portable.get_group_by_name("pkg5srv", "/etc", None)
+
+                def pkg5srv_readable(st):
+                        """Returns true if the pkg5srv user can read
+                        this file/dir"""
+                        if stat.S_ISDIR(st.st_mode):
+                                if (st.st_uid == pkg5srv_uid and
+                                    st.st_mode & stat.S_IXUSR and
+                                    st.st_mode & stat.S_IRUSR):
+                                        return True
+                                if (st.st_gid == pkg5srv_gid and
+                                    st.st_mode & stat.S_IXGRP and
+                                    st.st_mode & stat.S_IRGRP):
+                                        return True
+                        else:
+                                if (st.st_uid == pkg5srv_uid and
+                                    st.st_mode & stat.S_IRUSR):
+                                        return True
+                                if (st.st_gid == pkg5srv_gid and
+                                    st.st_mode & stat.S_IRGRP):
+                                        return True
+                        return False
+
+                def world_readable(st):
+                        """Returns true if anyone can read this
+                        file/dir."""
+                        if stat.S_ISDIR(st.st_mode):
+                                if (st.st_mode & stat.S_IROTH and
+                                    st.st_mode & stat.S_IXOTH):
+                                        return True
+                        else:
+                                if st.st_mode & stat.S_IROTH:
+                                        return True
+                        return False
+
+                # Scan directories checking file permissions.  First we
+                # walk up the directory path from self.__root
+                components = self.__root.split("/")
+                path = ""
+                for comp in components:
+                        if not comp:
+                                continue
+                        path = "%s/%s" % (path, comp)
+                        st = os.stat(path)
+                        if not (pkg5srv_readable(st) or
+                            world_readable(st)):
+                                return False, path
+
+                for path, dirnames, filenames in os.walk(self.__root):
+                        # we don't care about anything in our quarantine
+                        # directory.
+                        if REPO_QUARANTINE_DIR in path:
+                                continue
+                        st = os.stat(path)
+                        if not (pkg5srv_readable(st) or
+                            world_readable(st)):
+                                return False, path
+                        for fname in filenames:
+                                pth = os.path.join(path, fname)
+                                st = os.stat(pth)
+                                if not (pkg5srv_readable(st) or
+                                    world_readable(st)):
+                                        return False, pth
+                return True, None
+
+        def __gen_verify(self, progtrack, pub, trust_anchors,
+            sig_required_names, use_crls):
+                """A generator that produces verify errors, each a tuple
+                of the form (error_code, path, message, details)"""
+                # We may not have a manifest_root directory if no
+                # packages have ever been published for this publisher.
+                if not os.path.exists(self.manifest_root):
+                        return
+
+                err = self.__verify_perm(self.manifest_root, None, None)
+                if err:
+                        yield self.__build_verify_error(*err)
+                        return
+
+                # Build a list of all of the manifests that must be
+                # verified.
+                mflist = os.listdir(self.manifest_root)
+                goal = len(mflist)
+
+                # If there is more than one version in the manifest dir,
+                # then we add each one to the goal.
+                for name in mflist:
+                        try:
+                                mfdir = os.path.join(self.manifest_root, name)
+                                vers = len(os.listdir(mfdir))
+                                if vers > 1:
+                                        goal += vers - 1
+                        except OSError, e:
+                                # being unable to read the manifest dir is bad,
+                                # but we'll deal with it later
+                                continue
+
+                # Add the repo permissions error check to the number of items
+                # goal.
+                goal +=1
+                progtrack.repo_verify_start(goal)
+
+                # Scan the entire repository for bad file permissions
+                progtrack.repo_verify_start_pkg(None, repository_scan=True)
+                valid_perms, path = self.__verify_permissions()
+                if not valid_perms:
+                        # We intentionally don't use the path as the first
+                        # argument to __build_verify_error(..) as we do not want
+                        # 'pkgrepo fix' to attempt to fix this particular error,
+                        # since it can include paths outside the repository.
+                        yield self.__build_verify_error(
+                            REPO_VERIFY_WARN_OPENPERMS, None,
+                            {"permissionspath": path, "pub": pub.prefix})
+                progtrack.repo_verify_end_pkg(None)
+
+                for name in mflist:
+                        pdir = os.path.join(self.manifest_root, name)
+                        err = self.__verify_perm(pdir, None, None)
+                        if err:
+                                yield self.__build_verify_error(*err)
+                                continue
+
+                        # Stem must be decoded before use.
+                        try:
+                                pname = urllib.unquote(name)
+                        except Exception:
+                                # Assume error is result of an
+                                # unexpected file in the directory. We
+                                # don't know the FMRI here, so use None.
+                                progtrack.repo_verify_start_pkg(None)
+                                progtrack.repo_verify_add_progress(None)
+                                yield self.__build_verify_error(
+                                    REPO_VERIFY_UNKNOWN, pdir, {"err": str(e)})
+                                progtrack.repo_verify_end_pkg(None)
+                                continue
+
+                        for ver in os.listdir(pdir):
+                                path = os.path.join(pdir, ver)
+                                # Version must be decoded before
+                                # use.
+                                pver = urllib.unquote(ver)
+                                try:
+                                        pfmri = fmri.PkgFmri("@".join((pname,
+                                            pver)),
+                                            publisher=self.publisher)
+                                        if not os.path.isfile(path):
+                                                raise Exception(
+                                                    "%s is not a file" % path)
+                                except Exception, e:
+                                        # Assume the error is result of an
+                                        # unexpected file in the directory. We
+                                        # don't know the FMRI here, so use None.
+                                        progtrack.repo_verify_start_pkg(None)
+                                        progtrack.repo_verify_add_progress(None)
+                                        yield self.__build_verify_error(
+                                            REPO_VERIFY_UNKNOWN, path,
+                                            {"err": str(e)})
+                                        progtrack.repo_verify_end_pkg(None)
+                                        continue
+
+                                progtrack.repo_verify_start_pkg(pfmri)
+                                err = self.__verify_manifest(path, pfmri)
+                                if err:
+                                        # with a bad manifest, we can go no
+                                        # further
+                                        yield self.__build_verify_error(*err)
+                                        progtrack.repo_verify_end_pkg(None)
+                                        continue
+
+                                hashes, errors = self.__get_hashes(path, pfmri)
+                                for err in errors:
+                                        yield self.__build_verify_error(*err)
+
+                                # verify manifest signatures
+                                errs = self.__verify_signature(path, pfmri, pub,
+                                    trust_anchors, sig_required_names, use_crls)
+                                for err in errs:
+                                        yield self.__build_verify_error(*err)
+
+                                # verify payload delivered by this pkg
+                                errors = []
+                                for h in hashes:
+                                        try:
+                                                path = self.cache_store.lookup(
+                                                     h, check_existence=False)
+                                        except apx.PermissionsException, e:
+                                                # if we can't even get the path
+                                                # within the repository, then
+                                                # we'll do the best we can to
+                                                # report the problem.
+                                                errors.append((REPO_VERIFY_PERM,
+                                                    pfmri, {"hash": h,
+                                                    "err": _("Permission "
+                                                    "denied.", "path", h)}))
+                                                continue
+
+                                        err = self.__verify_perm(path, pfmri, h)
+                                        if err:
+                                                errors.append(err)
+                                                continue
+                                        err = self.__verify_hash(path, pfmri, h)
+                                        if err:
+                                                errors.append(err)
+                                for err in errors:
+                                        yield self.__build_verify_error(*err)
+
+                                progtrack.repo_verify_end_pkg(fmri)
+                progtrack.job_done(progtrack.JOB_REPO_VERIFY_REPO)
+
+        def verify(self, pub=None, progtrack=None,
+            trust_anchor_dir=None, sig_required_names=None, use_crls=False):
+                """A generator which verifies the contents of the repository
+                store, checking for several different types of errors.
+                No modifying operations may be performed until complete.
+
+                'progtrack' is an optional ProgressTracker object.
+
+                'trust_anchor_dir' is set in the repository configuration and
+                corresponds to the image property of the same name.
+
+                'sig_required_names' is set in the repository configuration and
+                corresponds to the image property of the same name.
+
+                'use_crls' is set in the repository configuration and
+                corresponds to the image property of the same name.
+
+                The generator yields tuples of the form:
+
+                (error_code, path, message, reason) where
+
+                'error_code'  an integer error, correponding to REPO_VERIFY_*
+                'path'        the path to the broken file in the repository
+                'message'     a human-readable summary of the error
+                'reason'      a dictionary of strings containing more detail
+                              about the nature of the error.
+                """
+
+                if not self.catalog_root or self.catalog_version < 1:
+                        raise RepositoryUnsupportedOperationError()
+                if not self.manifest_root:
+                        raise RepositoryUnsupportedOperationError()
+
+                if not progtrack:
+                        progtrack = progtrack.NullProgressTracker()
+
+                # For signature verification, we need to setup a publisher
+                # meta_root, and build a dictionary of trust-anchors.
+                tmp_metaroot = tempfile.mkdtemp(prefix="pkgrepo-verify.")
+                pub.meta_root = tmp_metaroot
+                trust_anchors = {}
+
+                if not os.path.isdir(trust_anchor_dir):
+                        raise RepositorySigNoTrustAnchorDirError(
+                            trust_anchor_dir)
+
+                for fn in os.listdir(trust_anchor_dir):
+                        pth = os.path.join(trust_anchor_dir, fn)
+                        if os.path.islink(pth):
+                                continue
+                        trusted_ca = m2.X509.load_cert(pth)
+                        # M2Crypto's subject hash doesn't match
+                        # openssl's subject hash so recompute it so all
+                        # hashes are in the same universe.
+                        s = trusted_ca.get_subject().as_hash()
+                        trust_anchors.setdefault(s, [])
+                        trust_anchors[s].append(trusted_ca)
+
+                self.__lock_rstore()
+                try:
+                        for err in self.__gen_verify(progtrack, pub,
+                            trust_anchors, sig_required_names, use_crls):
+                                yield err
+                except (Exception, EnvironmentError), e:
+                        import traceback
+                        traceback.print_exc(e)
+                        raise apx._convert_error(e)
+                finally:
+                        self.__unlock_rstore()
+                        shutil.rmtree(tmp_metaroot)
+
+        def fix(self,  pub=None, progtrack=None, verify_callback=None,
+            trust_anchor_dir=None, sig_required_names=None, use_crls=False):
+                """Verify, then quarantine any packages in the repository that
+                were found to be faulty, according to self.verify(..).
+
+                This method yields tuples of the form:
+
+                (status_code, fmri, message, reason) where
+
+                'status_code'  an int status code, corresponding to REPO_FIX_*
+                'path'         the path that was fixed
+                'message'      a summary of the operation performed
+                'reason'       a dictionary of strings describing the operation
+
+                Note, the 'fmri' value may not be a valid FMRI if the manifest
+                being fixed was corrupt, in which case a path to the corrupted
+                manifest in the repository is used instead.
+
+                If any object referred to by a manifest is quarantined, then
+                the manifest for that package is also quarantined, however other
+                files referenced by the manifest are not moved to quarantine
+                in case they are referenced by other packages.
+                """
+
+                if self.read_only:
+                        raise RepositoryReadOnlyError()
+
+                if not progtrack:
+                        progtrack = progress.NullProgressTracker()
+
+                broken_items = set()
+                for error, path, message, reason in self.verify(pub=pub,
+                    progtrack=progtrack, trust_anchor_dir=trust_anchor_dir,
+                    sig_required_names=sig_required_names, use_crls=use_crls):
+                        if verify_callback:
+                                verify_callback(progtrack, (error, path,
+                                    message, reason))
+                        # we don't attempt to fix this error, since it can
+                        # involve paths outside the repository.
+                        if error == REPO_VERIFY_WARN_OPENPERMS:
+                                continue
+                        fmri = reason.get("pkg")
+                        broken_items.add((path, fmri))
+
+                quarantine_root = None
+
+                def _make_quarantine_root():
+                        """Make a directory where we can quarantine content."""
+                        quarantine_base = os.path.join(self.root,
+                            REPO_QUARANTINE_DIR)
+                        if not os.path.exists(quarantine_base):
+                                os.mkdir(quarantine_base)
+                        qroot = tempfile.mkdtemp(prefix="fix.",
+                            dir=quarantine_base)
+                        return qroot
+
+                # look for broken package content where the bad file doesn't
+                # match the path to the manifest (eg. file content) and add
+                # the manifest path to the list of broken items, so that we
+                # move the manifest to quarantine as well.
+                for path, fmri in broken_items.copy():
+                        if not fmri:
+                                continue
+                        mpath = self.manifest(fmri)
+                        if path == mpath:
+                                continue
+                        broken_items.add((mpath, fmri))
+
+                progtrack.job_start(progtrack.JOB_REPO_FIX_REPO,
+		    goal=len(broken_items))
+
+                # keep a set of all the paths we've applied fixes to
+                fixed_paths = set()
+
+                for path, fmri in broken_items:
+                        progtrack.job_add_progress(progtrack.JOB_REPO_FIX_REPO)
+
+                        # we've already applied a fix to this path
+                        if path in fixed_paths:
+                                continue
+
+                        # we can't do anything about missing files
+                        if not os.path.exists(path):
+                                yield (REPO_FIX_ITEM, path,
+                                    _("Missing file %s must be fixed by "
+                                    "republishing the package.") % path,
+                                    {"pkg": fmri})
+                                continue
+
+                        basename = os.path.basename(path)
+                        dir = os.path.dirname(path)
+                        dir = dir.replace(self.root, "", 1).lstrip("/")
+                        if not quarantine_root:
+                                quarantine_root = _make_quarantine_root()
+                        qdir = os.path.join(quarantine_root, dir)
+                        try:
+                                os.makedirs(qdir)
+                        except OSError, e:
+                                if e.errno != errno.EEXIST:
+                                        raise
+                        dest = os.path.join(qdir, basename)
+                        if os.path.exists(dest):
+                                # this should never happen, since we have a
+                                # unique quarantine root per fix(..) call
+                                raise RepositoryQuarantinedPathExistsError()
+
+                        message = _("Moving %(src)s to %(dest)s") % {
+                            "src": path, "dest": dest}
+                        status = REPO_FIX_ITEM
+                        reason = {"dest": dest, "pkg": fmri}
+                        try:
+                                shutil.move(path, qdir)
+                                fixed_paths.add(path)
+                        except Exception, e:
+                                status = REPO_FIX_FAILED
+                                message = _("Unable to quarantine %(path)s: "
+                                    "%(err)s") % {"path": path, "err": e}
+                        finally:
+                                yield(status, path, message, reason)
+
+                progtrack.job_done(progtrack.JOB_REPO_FIX_REPO)
+
+                if broken_items:
+                        self.rebuild()
+
         def valid_new_fmri(self, pfmri):
                 """Check that the FMRI supplied as an argument would be valid
                 to add to the repository catalog.  This checks to make sure
@@ -1990,7 +2635,7 @@
 
                 if self.version > CURRENT_REPO_VERSION:
                         raise RepositoryVersionError(self.root,
-                            self.version)
+                            self.version, CURRENT_REPO_VERSION)
                 if self.version == 4:
                         if self.root and not self.pub_root:
                                 # Don't create the publisher root at this point,
@@ -2849,6 +3494,82 @@
                 # Update the publisher's configuration.
                 rstore.update_publisher(pub)
 
+        def verify(self, progtrack=None, pub=None):
+                """A generator that verifies that repository content matches
+                expected state for all or specified publishers.
+
+                'progtrack' is an optional ProgressTracker object.
+
+                'pub' is an optional publisher prefix to limit the operation to.
+
+                The generator yields tuples of the form:
+
+                (error_code, path, message, details) where
+
+                'error_code'  an integer error, correponding to REPO_VERIFY_*
+                'path'        the path to the broken file in the repository
+                'message'     a summary of the error
+                'details'     a dictionary of strings containing more detail
+                              about the nature of the error.
+                """
+
+                if self.cfg.get_property("repository", "version") != 4:
+                        raise RepositoryInvalidVersionError(self.root,
+                            self.cfg.get_property("repository", "version"), 4)
+
+                rstore = self.get_pub_rstore(pub.prefix)
+
+                trust_anchor_dir = self.cfg.get_property("repository",
+                    "trust-anchor-directory")
+                sig_required_names = set(self.cfg.get_property("repository",
+                    "signature-required-names"))
+                use_crls = self.cfg.get_property("repository",
+                    "check-certificate-revocation")
+
+                return rstore.verify(progtrack=progtrack, pub=pub,
+                    trust_anchor_dir=trust_anchor_dir,
+                    sig_required_names=sig_required_names, use_crls=use_crls)
+
+        def fix(self, progtrack=None, pub=None, verify_callback=None):
+                """A generator that corrects any problems in the repository.
+
+                'progtrack' is an optional ProgressTracker object.
+
+                'pub' is an optional publisher prefix to limit the operation to.
+
+                During the operation, we emit progress, printing the details
+                using 'verify_callback', a method which requires the following
+                arguments,  progtrack, error_code, message, reason, which
+                correspond exactly to the tuple generated by self.verify(..)
+
+                This method yields tuples of the form:
+
+                (status_code, message, details) where
+
+                'status_code'  an integerstatus code, corresponding to REPO_FIX*
+                'message'      a summary of the operation performed
+                'details'      a dictionary of strings describing the operation
+
+                """
+
+                if self.cfg.get_property("repository", "version") != 4:
+                        raise RepositoryInvalidVersionError(self.root,
+                            self.cfg.get_property("repository", "version"), 4)
+
+                rstore = self.get_pub_rstore(pub.prefix)
+
+                trust_anchor_dir = self.cfg.get_property("repository",
+                    "trust-anchor-directory")
+                sig_required_names = set(self.cfg.get_property("repository",
+                    "signature-required-names"))
+                use_crls = self.cfg.get_property("repository",
+                    "check-certificate-revocation")
+
+                return rstore.fix(progtrack=progtrack, pub=pub,
+                    verify_callback=verify_callback,
+                    trust_anchor_dir=trust_anchor_dir,
+                    sig_required_names=sig_required_names, use_crls=use_crls)
+
         def valid_new_fmri(self, pfmri):
                 """Check that the FMRI supplied as an argument would be valid
                 to add to the repository catalog.  This checks to make sure
@@ -2978,6 +3699,10 @@
                 ]),
                 cfg.PropertySection("repository", [
                     cfg.PropInt("version"),
+                    cfg.Property("trust-anchor-directory",
+                        default="/etc/certs/CA/"),
+                    cfg.PropList("signature-required-names"),
+                    cfg.PropBool("check-certificate-revocation", default=False)
                 ]),
             ],
         }
--- a/src/pkgrepo.py	Wed Feb 06 13:01:46 2013 -0800
+++ b/src/pkgrepo.py	Thu Mar 07 10:46:12 2013 +0530
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 #
 
 PKG_CLIENT_NAME = "pkgrepo"
@@ -50,6 +50,7 @@
 import shutil
 import sys
 import tempfile
+import textwrap
 import traceback
 import warnings
 
@@ -155,6 +156,10 @@
          section/property[+|-]=[value] ... or
          section/property[+|-]=([value]) ...
 
+     pkgrepo verify [-p publisher ...] -s repo_uri_or_path
+
+     pkgrepo fix [-v] [-p publisher ...] -s repo_uri_or_path
+
      pkgrepo help
      pkgrepo version
 
@@ -256,7 +261,7 @@
         return sr.Repository(read_only=read_only, root=path)
 
 
-def setup_transport(conf, subcommand=None):
+def setup_transport(conf, subcommand=None, verbose=False):
         repo_uri = conf.get("repo_uri", None)
         if not repo_uri:
                 usage(_("No repository location specified."), cmd=subcommand)
@@ -1193,6 +1198,250 @@
         return EXIT_OK
 
 
+verify_error_header = None
+verify_warning_header = None
+verify_reason_headers = None
+
+def __load_verify_msgs():
+        """Since our gettext isn't loaded we need to ensure our globals have
+        correct content by calling this method.  These values are used by both
+        fix when in verbose mode, and verify"""
+
+        global verify_error_header
+        global verify_warning_header
+        global verify_reason_headers
+
+        # A map of error detail types to the human-readable description of each
+        # type.  These correspond to keys in the dictionary returned by
+        # sr.Repository.verify(..)
+        verify_reason_headers = {
+            "path": _("Repository path"),
+            "actual": _("Computed hash"),
+            "fpath": _("Path"),
+            "permissionspath": _("Path"),
+            "pkg": _("Package"),
+            "err": _("Detail")
+        }
+
+        verify_error_header = _("ERROR")
+        verify_warning_header = _("WARNING")
+
+
+def __fmt_verify(verify_tuple):
+        """Format a verify_tuple, of the form (error, path, message, reason)
+        returning a formatted error message, and an FMRI indicating what
+        packages within the repository are affected. Note that the returned FMRI
+        may not be valid, in which case a path to the broken manifest in the
+        repository is returned instead."""
+
+        error, path, message, reason = verify_tuple
+
+        formatted_message = "%(error_type)16s: %(message)s\n" % \
+            {"error_type": verify_error_header, "message": message}
+        reason["path"] = path
+
+        if error == sr.REPO_VERIFY_BADMANIFEST:
+                reason_keys = ["path", "err"]
+        elif error in [sr.REPO_VERIFY_PERM, sr.REPO_VERIFY_MFPERM]:
+                reason_keys = ["pkg", "path"]
+        elif error == sr.REPO_VERIFY_BADHASH:
+                reason_keys = ["pkg", "path", "actual", "fpath"]
+        elif error == sr.REPO_VERIFY_UNKNOWN:
+                reason_keys = ["path", "err"]
+        elif error == sr.REPO_VERIFY_BADSIG:
+                reason_keys = ["pkg", "path", "err"]
+        elif error == sr.REPO_VERIFY_WARN_OPENPERMS:
+                formatted_message = \
+                    "%(error_type)16s: %(message)s\n" % \
+                    {"error_type": verify_warning_header, "message": message}
+                reason_keys = ["permissionspath", "err"]
+        else:
+                # A list of the details we provide.  Some error codes
+                # have different details associated with them.
+                reason_keys = ["pkg", "path", "fpath"]
+
+
+        # the detailed error message can be long, so we'll wrap it.  If what we
+        # have fits on a single line, use it, otherwise begin displaying the
+        # message on the next line.
+        if "err" in reason_keys:
+                err_str = ""
+                lines = textwrap.wrap(reason["err"])
+                if len(lines) != 1:
+                        for line in lines:
+                                err_str += "%18s\n" % line
+                        reason["err"] = "\n" + err_str.rstrip()
+                else:
+                        reason["err"] = lines[0]
+
+        for key in reason_keys:
+                # sometimes we don't have the key we want, for example we may
+                # not have a file path from the package if the error is a
+                # missing repository file for a 'license' action (which don't
+                # have 'path' attributes, hence no 'fpath' dictionary entry)
+                if key not in reason:
+                        continue
+                formatted_message += "%(key)16s: %(value)s\n" % \
+                    {"key": verify_reason_headers[key], "value": reason[key]}
+
+        formatted_message += "\n"
+
+        if error == sr.REPO_VERIFY_WARN_OPENPERMS:
+                return formatted_message, None
+        elif "pkg" in reason:
+                return formatted_message, reason["pkg"]
+        return formatted_message, reason["path"]
+
+
+def subcmd_verify(conf, args):
+        """Verify the repository content (file and manifest content only)."""
+
+        subcommand = "verify"
+        __load_verify_msgs()
+
+        opts, pargs = getopt.getopt(args, "p:s:")
+        pubs = set()
+        for opt, arg in opts:
+                if opt == "-s":
+                        conf["repo_uri"] = parse_uri(arg)
+                if opt == "-p":
+                        if not misc.valid_pub_prefix(arg):
+                                error(_("Invalid publisher prefix '%s'") % arg,
+                                    cmd=subcommand)
+                        pubs.add(arg)
+
+        if pargs:
+                usage(_("command does not take operands"), cmd=subcommand)
+
+        repo_uri = conf.get("repo_uri", None)
+        if not repo_uri:
+                usage(_("A package repository location must be provided "
+                    "using -s."), cmd=subcommand)
+
+        if repo_uri.scheme != "file":
+                usage(_("Network repositories are not currently supported "
+                    "for this operation."), cmd=subcommand)
+
+        xport, xpub, tmp_dir = setup_transport(conf, subcommand=subcommand)
+        rval, found, pub_data = _get_matching_pubs(subcommand, pubs, xport,
+            xpub)
+        if rval == EXIT_OOPS:
+                return rval
+
+        logger.info("Initiating repository verification.")
+        bad_fmris = set()
+        for pfx in found:
+                progtrack = get_tracker()
+                repo = sr.Repository(root=repo_uri.get_pathname())
+                xpub.prefix = pfx
+                xpub.transport = xport
+                for verify_tuple in repo.verify(pub=xpub, progtrack=progtrack):
+                        message, bad_fmri = __fmt_verify(verify_tuple)
+                        if bad_fmri:
+                                bad_fmris.add(bad_fmri)
+                        progtrack.repo_verify_yield_error(bad_fmri, message)
+        if bad_fmris:
+                return EXIT_OOPS
+        return EXIT_OK
+
+
+def subcmd_fix(conf, args):
+        """Fix the repository content (file and manifest content only)
+        For index and catalog content corruption, a rebuild should be
+        performed."""
+
+        subcommand = "fix"
+        __load_verify_msgs()
+        verbose = False
+
+        opts, pargs = getopt.getopt(args, "vp:s:")
+        pubs = set()
+        for opt, arg in opts:
+                if opt == "-s":
+                        conf["repo_uri"] = parse_uri(arg)
+                if opt == "-v":
+                        verbose = True
+                if opt == "-p":
+                        if not misc.valid_pub_prefix(arg):
+                                error(_("Invalid publisher prefix '%s'") % arg,
+                                    cmd=subcommand)
+                        pubs.add(arg)
+
+        if pargs:
+                usage(_("command does not take operands"), cmd=subcommand)
+
+        repo_uri = conf.get("repo_uri", None)
+        if not repo_uri:
+                usage(_("A package repository location must be provided "
+                    "using -s."), cmd=subcommand)
+
+        if repo_uri.scheme != "file":
+                usage(_("Network repositories are not currently supported "
+                    "for this operation."), cmd=subcommand)
+
+        xport, xpub, tmp_dir = setup_transport(conf, subcommand=subcommand)
+        rval, found, pub_data = _get_matching_pubs(subcommand, pubs, xport,
+            xpub)
+        if rval == EXIT_OOPS:
+                return rval
+
+        logger.info("Initiating repository fix.")
+        progtrack = get_tracker()
+
+        def verify_cb(tracker, verify_tuple):
+                """A method passed to sr.Repository.fix(..) to emit verify
+                messages if verbose mode is enabled."""
+                if not verbose:
+                        return
+                formatted_message, bad_fmri = __fmt_verify(verify_tuple)
+                tracker.repo_verify_yield_error(bad_fmri, formatted_message)
+
+        repo = sr.Repository(root=repo_uri.get_pathname())
+        broken_fmris = set()
+        failed_fix_paths = set()
+        for pfx in found:
+                xpub.prefix = pfx
+                xpub.transport = xport
+                progtrack = get_tracker()
+                for status_code, path, message, reason in \
+                    repo.fix(pub=xpub, progtrack=progtrack,
+                        verify_callback=verify_cb):
+                        if status_code == sr.REPO_FIX_ITEM:
+                                # When we can't get the FMRI, eg. in the case
+                                # of a corrupt manifest, use the path instead.
+                                fmri = reason["pkg"]
+                                if not fmri:
+                                        fmri = path
+                                broken_fmris.add(fmri)
+                                if verbose:
+                                        progtrack.repo_fix_yield_info(fmri,
+                                            message)
+                        else:
+                                failed_fix_paths.add(path)
+
+        progtrack.flush()
+        logger.info("")
+
+        if broken_fmris:
+                logger.info(_("Use pkgsend(1) or pkgrecv(1) to republish the\n"
+                    "following packages or paths which were quarantined:\n\n\t"
+                    "%s") % \
+                    "\n\t".join([str(f) for f in broken_fmris]))
+        if failed_fix_paths:
+                logger.info(_("\npkgrepo could not repair the following paths "
+                    "in the repository:\n\n\t%s") %
+                    "\n\t".join([p for p in failed_fix_paths]))
+
+        if not (broken_fmris or failed_fix_paths):
+                logger.info(_("No repository fixes required."))
+        else:
+                logger.info(_("Repository repairs completed."))
+
+        if failed_fix_paths:
+                return EXIT_OOPS
+        return EXIT_OK
+
+
 def main_func():
         global_settings.client_name = PKG_CLIENT_NAME
 
--- a/src/tests/cli/t_pkgrepo.py	Wed Feb 06 13:01:46 2013 -0800
+++ b/src/tests/cli/t_pkgrepo.py	Thu Mar 07 10:46:12 2013 +0530
@@ -21,7 +21,7 @@
 #
 
 #
-# Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 #
 
 import testutils
@@ -35,6 +35,7 @@
 import pkg.catalog
 import pkg.depotcontroller as dc
 import pkg.fmri as fmri
+import pkg.pkggzip
 import pkg.misc as misc
 import pkg.server.repository as sr
 import shutil
@@ -118,7 +119,8 @@
 
         def setUp(self):
                 pkg5unittest.SingleDepotTestCase.setUp(self)
-                self.make_misc_files(["tmp/empty", "tmp/truck1", "tmp/truck2"])
+                self.make_misc_files(["tmp/empty", "tmp/truck1", "tmp/truck2",
+                    "tmp/other"])
 
         def test_00_base(self):
                 """Verify pkgrepo handles basic option and subcommand parsing
@@ -1632,6 +1634,532 @@
                 self.pkgrepo("list -s %s -H -F tsv nosuchpackage" % repo_path,
                     exit=1)
 
+        def __get_mf_path(self, fmri_str):
+                """Given an FMRI, return the path to its manifest in our
+                repository."""
+
+                path_comps = [self.dc.get_repodir(), "publisher",
+                    "test", "pkg"]
+                pfmri = pkg.fmri.PkgFmri(fmri_str)
+                path_comps.append(pfmri.get_name())
+                path_comps.append(pfmri.get_link_path().split("@")[1])
+                return os.path.sep.join(path_comps)
+
+        def __get_file_path(self, path):
+                """Returns the path to a file in the repository. The path name
+                must be present in self.fhashes."""
+
+                fpath = os.path.sep.join([self.dc.get_repodir(), "publisher",
+                    "test", "file"])
+                fhash = self.fhashes[path]
+                return os.path.sep.join([fpath, fhash[0:2], fhash])
+
+        def __inject_badhash(self, path, valid_gzip=True):
+                """Corrupt a file in the repository with the given path, where
+                that path is a key in self.fhashes, returning the repository
+                path of the file that was corrupted.  If valid_gzip is set,
+                we write a gzip file into the repository, otherwise the file
+                contains plaintext."""
+
+                corrupt_path = self.__get_file_path(path)
+                other_path = os.path.join(self.test_root, "tmp/other")
+
+                with open(other_path, "rb") as other:
+                        with open(corrupt_path, "wb") as corrupt:
+                                if valid_gzip:
+                                        gz = pkg.pkggzip.PkgGzipFile(
+                                            fileobj=corrupt)
+                                        gz.write(other.read())
+                                        gz.close()
+                                else:
+                                        corrupt.write(other.read())
+                return corrupt_path
+
+        def __repair_badhash(self, path):
+                """Fix a file in the repository that corresponds to this path.
+                """
+                fpath = self.__get_file_path(path)
+                fixed_path = os.path.join(self.test_root, path)
+                with open(fixed_path, "rb") as fixed:
+                        with open(fpath, "wb") as broken:
+                                gz = pkg.pkggzip.PkgGzipFile(
+                                    fileobj=broken)
+                                gz.write(fixed.read())
+                                gz.close()
+
+        def __inject_badmanifest(self, fmri_str):
+                """Corrupt a manifest in the repository for the given fmri,
+                returning the path to that FMRI."""
+                mpath = self.__get_mf_path(fmri_str)
+                other_path = os.path.join(self.test_root, "tmp/other")
+
+                with open(other_path, "r") as other:
+                        with open(mpath, "a") as mf:
+                                mf.write(other.read())
+                return mpath
+
+        def __inject_dir_for_manifest(self, fmri_str):
+                """Put a dir in place of a manifest."""
+                mpath = self.__get_mf_path(fmri_str)
+                os.remove(mpath)
+                os.mkdir(mpath)
+                return mpath
+
+        def __inject_nofile(self, path):
+                fpath = self.__get_file_path(path)
+                os.remove(fpath)
+                return fpath
+
+        def __inject_perm(self, path=None, fmri_str=None, parent=False,
+            chown=False):
+                """Set restrictive file permissions on the given path or fmri in
+                the repository. If 'parent' is set to True, we change
+                permissions on the parent directory of the file or fmri.
+                If chown is set, we chown the file and its parent dir to root.
+                """
+                if path:
+                        fpath = self.__get_file_path(path)
+                        if parent:
+                                fpath = os.path.dirname(fpath)
+                        os.chmod(fpath, 0000)
+                        if chown:
+                                os.chown(fpath, 0, 0)
+                                os.chown(os.path.dirname(mpath), 0, 0)
+                        return fpath
+                elif fmri_str:
+                        mpath = self.__get_mf_path(fmri_str)
+                        if parent:
+                                mpath = os.path.dirname(mpath)
+                        os.chmod(mpath, 0000)
+                        if chown:
+                                os.chown(mpath, 0, 0)
+                                os.chown(os.path.dirname(mpath), 0, 0)
+                        return mpath
+                else:
+                        assert_(False, "Invalid use of __inject_perm(..)!")
+
+        def __repair_perm(self, path, parent=False):
+                """Repair errors introduced by __inject_perm."""
+                if parent:
+                        fpath = os.path.dirname(path)
+                else:
+                        fpath = path
+                if os.path.isdir(fpath):
+                        os.chmod(fpath, misc.PKG_DIR_MODE)
+                elif os.path.isfile(fpath):
+                        os.chmod(fpath, misc.PKG_FILE_MODE)
+
+        def __inject_badsig(self, fmri_str):
+                mpath = self.__get_mf_path(fmri_str)
+                with open(mpath, "ab+") as mf:
+                        mf.write("set name=pkg.t_pkgrepo.bad_sig value=foo")
+                return mpath
+
+        def __repair_badsig(self, path):
+                mpath_new = path + ".new"
+                with open(path, "rb") as mf:
+                        with open(mpath_new, "wb") as mf_new:
+                                for line in mf.readlines():
+                                        if not "pkg.t_pkgrepo.bad_sig" in line:
+                                                mf_new.write(line)
+                os.rename(mpath_new, path)
+
+        def __inject_unknown(self):
+                pass
+
+        def test_11_verify_badhash(self):
+                """Test that verify finds bad hashes and invalid gzip files."""
+
+                repo_path = self.dc.get_repodir()
+                repo_uri = self.dc.get_repo_url()
+
+                # publish a single package and make sure the repository is clean
+                fmris = self.pkgsend_bulk(repo_path, (self.tree10))
+                self.pkgrepo("-s %s verify" % repo_path, exit=0)
+
+                # break a file in the repository and ensure we spot it
+                bad_hash_path = self.__inject_badhash("tmp/truck1")
+
+                self.pkgrepo("-s %s verify" % repo_path, exit=1)
+                self.assert_(
+                    self.output.count("ERROR: Invalid file hash") == 1)
+                self.assert_(bad_hash_path in self.output)
+                self.assert_(fmris[0] in self.output)
+
+                # fix the file in the repository, and publish another package
+                self.__repair_badhash("tmp/truck1")
+                fmris += self.pkgsend_bulk(repo_path, (self.truck10))
+                self.pkgrepo("-s %s verify" % repo_path, exit=0)
+                self.assert_(bad_hash_path not in self.output)
+                bad_hash_path = self.__inject_badhash("tmp/truck1")
+
+                # verify we now get two errors when verifying the repository
+                self.pkgrepo("-s %s verify" % repo_path, exit=1)
+                self.assert_(
+                    self.output.count("ERROR: Invalid file hash") == 2)
+                for fmri in fmris:
+                        self.assert_(fmri in self.output)
+                self.assert_(bad_hash_path in self.output)
+                # check we also print paths which deliver that corrupted file
+                self.assert_("etc/truck1" in self.output)
+                self.assert_("etc/trailer" in self.output)
+
+                # finally, corrupt another file to see that we can also spot
+                # files that aren't gzipped.
+                fmris += self.pkgsend_bulk(repo_path, (self.truck20))
+                bad_gzip_path = self.__inject_badhash("tmp/truck2",
+                    valid_gzip=False)
+                self.pkgrepo("-s %s verify" % repo_path, exit=1)
+                # we should get 3 bad file hashes now, since we've added truck20
+                # which also references etc/truck1, which is bad.
+                self.assert_(
+                    self.output.count("ERROR: Invalid file hash") == 3)
+                self.assert_(
+                    self.output.count("ERROR: Corrupted gzip file") == 1)
+                self.assert_(bad_gzip_path in self.output)
+
+
+        def test_12_verify_badmanifest(self):
+                """Test that verify finds bad manifests."""
+                repo_path = self.dc.get_repodir()
+
+                # publish a single package
+                fmris = self.pkgsend_bulk(repo_path, (self.tree10))
+
+                # corrupt a manifest and make sure pkglint agrees
+                bad_mf = self.__inject_badmanifest(fmris[0])
+                self.pkglint(bad_mf, exit=1)
+
+                self.pkgrepo("-s %s verify" % repo_path, exit=1)
+                self.assert_(bad_mf in self.output)
+                self.assert_("Corrupt manifest." in self.output)
+
+                # publish more packages, and verify we still get the one error
+                fmris += self.pkgsend_bulk(repo_path, (self.truck10,
+                    self.amber10))
+                self.pkgrepo("-s %s verify" % repo_path, exit=1)
+                self.assert_(
+                    self.output.count("ERROR: Corrupt manifest.") == 1)
+
+                # break another manifest, and check we get two errors
+                another_bad_mf = self.__inject_badmanifest(fmris[-1])
+                self.pkgrepo("-s %s verify" % repo_path, exit=1)
+                self.assert_(another_bad_mf in self.output)
+                self.assert_(bad_mf in self.output)
+                self.assert_(
+                    self.output.count("Corrupt manifest.") == 2)
+
+        def test_13_verify_nofile(self):
+                """Test that verify finds missing files."""
+
+                repo_path = self.dc.get_repodir()
+
+                # publish a single package and break it
+                fmris = self.pkgsend_bulk(repo_path, (self.tree10))
+                missing_file = self.__inject_nofile("tmp/truck1")
+
+                self.pkgrepo("-s %s verify" % repo_path, exit=1)
+                self.assert_(missing_file in self.output)
+                self.assert_("ERROR: Missing file: %s" %
+                    self.fhashes["tmp/truck1"] in self.output)
+                self.assert_(fmris[0] in self.output)
+
+                # publish another package that also delivers the file
+                # and inject the error again, checking that both manifests
+                # appear in the output
+                fmris += self.pkgsend_bulk(repo_path, (self.truck10))
+                self.__inject_nofile("tmp/truck1")
+
+                self.pkgrepo("-s %s verify" % repo_path, exit=1)
+                self.assert_(missing_file in self.output)
+                self.assert_(self.output.count("ERROR: Missing file: %s" %
+                    self.fhashes["tmp/truck1"]) == 2)
+                for f in fmris:
+                        self.assert_(f in self.output)
+
+        def test_14_verify_permissions(self):
+                """Check that we can find files and manifests in the
+                repository that have invalid permissions."""
+
+                repo_path = self.dc.get_repodir()
+
+                shutil.rmtree(repo_path)
+                os.mkdir(repo_path)
+                os.chmod(repo_path, 0777)
+                self.pkgrepo("create %s" % repo_path, su_wrap=True)
+                self.pkgrepo("set -s %s publisher/prefix=test" % repo_path,
+                    su_wrap=True)
+                # publish a single package and break it
+                fmris = self.pkgsend_bulk(repo_path, (self.truck10),
+                    su_wrap=True)
+                bad_path = self.__inject_perm(path="tmp/truck1")
+                self.pkgrepo("-s %s verify" % repo_path, exit=1, su_wrap=True)
+                self.assert_(bad_path in self.output)
+                self.assert_("ERROR: Verification failure" in self.output)
+                self.assert_(fmris[0] in self.output)
+                self.__repair_perm(bad_path)
+
+                # Just break the parent directory, we should still report the
+                # hash file as unreadable
+                bad_parent = self.__inject_perm(path="tmp/truck1", parent=True)
+                self.pkgrepo("-s %s verify" % repo_path, exit=1, su_wrap=True)
+                self.assert_(bad_path in self.output)
+                self.assert_("ERROR: Verification failure" in self.output)
+                self.assert_(fmris[0] in self.output)
+                self.__repair_perm(bad_parent)
+                # break some manifests
+                fmris = self.pkgsend_bulk(repo_path, (self.truck20),
+                    su_wrap=True)
+
+                bad_mf_path = self.__inject_perm(fmri_str=fmris[0])
+                self.pkgrepo("-s %s verify" % repo_path, exit=1, su_wrap=True)
+                self.assert_(bad_mf_path in self.output)
+                self.assert_("ERROR: Verification failure" in self.output)
+
+                # this should cause both manifests to report errors
+                bad_mf_path = self.__inject_perm(fmri_str=fmris[0], parent=True)
+                self.pkgrepo("-s %s verify" % repo_path, exit=1, su_wrap=True)
+                self.assert_(bad_mf_path in self.output)
+                self.assert_("ERROR: Verification failure" in self.output)
+                self.__repair_perm(bad_mf_path)
+
+        def test_15_verify_badsig(self):
+                repo_path = self.dc.get_repodir()
+
+                # publish a single package and break it
+                fmris = self.pkgsend_bulk(repo_path, (self.truck10))
+                self.pkgsign(repo_path, "\*")
+                self.pkgrepo("-s %s verify" % repo_path)
+                bad_path = self.__inject_badsig(fmris[0])
+                self.pkgrepo("-s %s verify" % repo_path, exit=1)
+                self.assert_(bad_path in self.output)
+                self.assert_("ERROR: Bad signature." in self.output)
+                self.__repair_badsig(bad_path)
+                self.pkgrepo("-s %s verify" % repo_path, exit=0)
+
+                # now sign with a key, cert and chain cert and check we fail
+                # to verify
+                self.pkgsign_simple(repo_path, "\*")
+                self.pkgrepo("-s %s verify" % repo_path, exit=1)
+                self.assert_("ERROR: Bad signature." in self.output)
+
+                # now set a trust anchor directory, and expect that pkgrepo
+                # verify will now pass
+                ta_dir = os.path.join(self.test_root,
+                    "ro_data/signing_certs/produced/ta3")
+                self.pkgrepo("-s %s set repository/trust-anchor-directory=%s" %
+                    (repo_path, ta_dir))
+                self.pkgrepo("-s %s verify" % repo_path, exit=0)
+
+        def test_16_verify_warn_openperms(self):
+                """Test that we emit a warning message when the repository is
+                not world-readable."""
+
+                repo_path = self.dc.get_repodir()
+                fmris = self.pkgsend_bulk(repo_path, (self.truck10))
+                os.chmod(self.test_root, 0700)
+                self.pkgrepo("-s %s verify" % repo_path, exit=0)
+                self.assert_("WARNING: " in self.output)
+                self.assert_("svc:/application/pkg/system-repository" \
+                    in self.output)
+                self.assert_("ERROR: " not in self.output)
+                os.chmod(self.test_root, 0755)
+                self.pkgrepo("-s %s verify" % repo_path, exit=0)
+                self.assert_("WARNING: " not in self.output)
+
+        def test_17_verify_empty_pub(self):
+                """Test that we can verify a repository that contains a
+                publisher with no packages."""
+                repo_path = self.dc.get_repodir()
+                fmris = self.pkgsend_bulk(repo_path, (self.truck10))
+                self.pkgrepo("-s %s add-publisher empty" % repo_path)
+                self.pkgrepo("-s %s verify -p empty" % repo_path)
+
+        def test_18_verify_invalid_repos(self):
+                """Test that we exit with a usage message for v3 repos and
+                network repositories."""
+                repo_path = self.dc.get_repodir()
+                fmris = self.pkgsend_bulk(repo_path, (self.truck10))
+                depot_uri = self.dc.get_depot_url()
+                self.dc.start()
+                self.pkgrepo("-s %s verify" % depot_uri, exit=2)
+                self.dc.stop()
+                shutil.rmtree(repo_path)
+                self.pkgrepo("create --version=3 %s" % repo_path)
+                self.pkgrepo("set -s %s publisher/prefix=test" % repo_path)
+                fmris = self.pkgsend_bulk(repo_path, (self.truck10))
+                self.pkgrepo("-s %s verify" % repo_path, exit=1)
+
+        def __get_fhashes(self, repodir, pub):
+                """Returns a list of file hashes for the publisher
+                pub in a given repository."""
+                fhashes = []
+                files_dir = os.path.sep.join(["publisher", pub, "file"])
+                for dirpath, dirs, files in os.walk(os.path.join(repodir,
+                    files_dir)):
+                        fhashes.extend(files)
+                return fhashes
+
+        def test_19_fix_brokenmanifest(self):
+                """Test that fix operations correct a bad manifest in a file
+                repo."""
+
+                repo_path = self.dc.get_repodir()
+                fmris = self.pkgsend_bulk(repo_path, (self.truck10))
+                self.pkgsign(repo_path, "\*")
+
+                self.pkgrepo("-s %s fix" % repo_path)
+
+                valid_hashes = self.__get_fhashes(repo_path, "test")
+                self.debug(valid_hashes)
+                bad_path = self.__inject_badsig(fmris[0])
+                self.pkgrepo("-s %s fix" % repo_path)
+                self.assert_(fmris[0] in self.output)
+                self.assert_(not os.path.exists(bad_path))
+
+                # check our quarantine dir has been created
+                q_dir = os.path.sep.join([repo_path, "publisher", "test",
+                    "pkg5-quarantine"])
+                self.assert_(os.path.exists(q_dir))
+
+                # check the broken manifest is in the quarantine dir
+                mf_path_sub = bad_path.replace(
+                    os.path.sep.join([repo_path, "publisher", "test"]), "")
+                # quarantined items are stored in a new tmpdir per-session
+                q_dir_tmp = os.listdir(q_dir)[0]
+                q_mf_path = os.path.sep.join([q_dir, q_dir_tmp, mf_path_sub])
+                self.assert_(os.path.exists(q_mf_path))
+
+                # make sure the package no longer appears in the catalog
+                self.pkgrepo("-s %s list -F tsv" % repo_path)
+                self.assert_(fmris[0] not in self.output)
+
+                # ensure that only the manifest was quarantined - file hashes
+                # were left alone.
+                remaining_hashes = self.__get_fhashes(repo_path, "test")
+                self.assert_(set(valid_hashes) == set(remaining_hashes))
+
+                # finally, ensure we can republish this package
+                fmris = self.pkgsend_bulk(repo_path, (self.truck10))
+                self.pkgrepo("-s %s list -F tsv" % repo_path)
+                self.assert_(fmris[0] in self.output)
+
+        def test_20_fix_brokenfile(self):
+                """Test that operations that cause us to fix a file shared
+                by several packages cause all of those packages to be
+                quarantined.
+
+                This also tests the -v option of pkg fix, which prints the
+                pkgrepo verify output and prints details of which files are
+                being quarantined.
+                """
+
+                repo_path = self.dc.get_repodir()
+                fmris = self.pkgsend_bulk(repo_path, (self.tree10,
+                    self.truck10))
+                self.pkgrepo("-s %s fix" % repo_path)
+                bad_file = self.__inject_badhash("tmp/truck1")
+
+                old_hashes = self.__get_fhashes(repo_path, "test1")
+
+                self.pkgrepo("-s %s fix -v" % repo_path)
+
+                # since the file was shared by two manifests, we should get
+                # the manifest name printed twice: once when we encounter the
+                # broken file, then again when we print the summary of which
+                # packages need to be republished.
+                self.assert_(self.output.count(fmris[0]) == 2)
+                self.assert_(self.output.count(fmris[1]) == 2)
+                self.assert_(self.output.count("ERROR: Invalid file hash") == 2)
+
+                # the bad file name gets printed 3 times, once for each time
+                # a manifest references it and verify discovered the error,
+                # and once when we report where the file was moved to.
+                self.assert_(self.output.count(bad_file) == 3)
+
+                # check the broken file is in the quarantine dir and that
+                # we printed where it moved to
+                bad_file_sub = bad_file.replace(
+                    os.path.sep.join([repo_path, "publisher", "test"]), "")
+                # quarantined items are stored in a new tmpdir per-session
+                q_dir = os.path.sep.join([repo_path, "publisher", "test",
+                    "pkg5-quarantine"])
+                q_dir_tmp = os.listdir(q_dir)[0]
+                q_file_path = os.path.normpath(
+                    os.path.sep.join([q_dir, q_dir_tmp, bad_file_sub]))
+                self.assert_(os.path.exists(q_file_path))
+                self.debug(q_file_path)
+                self.assert_(q_file_path in self.output)
+
+                remaining_hashes = self.__get_fhashes(repo_path, "test1")
+                # check that we only quarantined the bad hash file
+                self.assert_(set(remaining_hashes) == \
+                    set(old_hashes) - set(os.path.basename(bad_file)))
+
+                # Make sure the repository is now clean, and remains so even
+                # after we republish the packages, and that all file content is
+                # replaced.
+                self.pkgrepo("-s %s fix" % repo_path)
+                fmris = self.pkgsend_bulk(repo_path, (self.tree10,
+                    self.truck10))
+                new_hashes = self.__get_fhashes(repo_path, "test1")
+                self.assert_(set(new_hashes) == set(old_hashes))
+                self.pkgrepo("-s %s fix" % repo_path)
+
+        def test_21_fix_brokenperm(self):
+                """Tests that when running fix as an unpriviliged user that we
+                fail to fix the repository."""
+
+                repo_path = self.dc.get_repodir()
+
+                shutil.rmtree(repo_path)
+                os.mkdir(repo_path)
+                os.chmod(repo_path, 0777)
+                self.pkgrepo("create %s" % repo_path, su_wrap=True)
+                self.pkgrepo("set -s %s publisher/prefix=test" % repo_path,
+                    su_wrap=True)
+                # publish a single package and break it
+                fmris = self.pkgsend_bulk(repo_path, (self.truck10),
+                    su_wrap=True)
+                # this breaks the permissions of one of the manifests and
+                # chowns is such that we shouldn't be able to quarantine it.
+                bad_path = self.__inject_perm(fmri_str=fmris[0], chown=True)
+
+                self.pkgrepo("-s %s fix" % repo_path, exit=1, stderr=True,
+                    su_wrap=True)
+                self.assert_(bad_path in self.errout)
+                # the fix should succeed now, but not emit any output, because
+                # it's running as root, and the permissions are fine according
+                # to a root user
+                self.pkgrepo("-s %s fix" % repo_path)
+                # but we should still be able to warn about the bad path for
+                # pkg5srv access.
+                self.pkgrepo("-s %s verify" % repo_path)
+                self.assert_("WARNING: " in self.output)
+
+        def test_22_fix_unsupported_repo(self):
+                """Tests that when running fix on a v3 repo fails"""
+
+                repo_path = self.dc.get_repodir()
+                shutil.rmtree(repo_path)
+                self.pkgrepo("create --version=3 %s" % repo_path)
+                self.pkgrepo("-s %s set publisher/prefix=test" % repo_path)
+                fmris = self.pkgsend_bulk(repo_path, (self.truck10))
+                self.pkgrepo("-s %s fix" % repo_path, exit=1)
+                self.assert_("only version 4 repositories are supported." in
+                    self.errout)
+
+        def test_23_fix_empty_missing_pub(self):
+                """Test that we can attempt to fix a repository that contains a
+                publisher with no packages, and that we fail on missing pubs"""
+
+                repo_path = self.dc.get_repodir()
+                fmris = self.pkgsend_bulk(repo_path, (self.truck10))
+                self.pkgrepo("-s %s add-publisher empty" % repo_path)
+                self.pkgrepo("-s %s fix -p test" % repo_path)
+                self.pkgrepo("-s %s fix -p missing" % repo_path, exit=1)
+                self.assert_("no matching publishers" in self.errout)
+
 
 if __name__ == "__main__":
         unittest.main()
--- a/src/tests/pkg5unittest.py	Wed Feb 06 13:01:46 2013 -0800
+++ b/src/tests/pkg5unittest.py	Thu Mar 07 10:46:12 2013 +0530
@@ -18,7 +18,7 @@
 # CDDL HEADER END
 #
 
-# Copyright (c) 2008, 2012, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved.
 
 #
 # Define the basic classes that all test cases are inherited from.
@@ -54,6 +54,7 @@
 import unittest
 import urllib2
 import urlparse
+import operator
 import platform
 import pty
 import pwd
@@ -2124,15 +2125,19 @@
                 return res
 
 
-def get_su_wrap_user():
+def get_su_wrap_user(uid_gid=False):
         for u in ["noaccess", "nobody"]:
                 try:
-                        pwd.getpwnam(u)
+                        pw = pwd.getpwnam(u)
+                        if uid_gid:
+                                return operator.attrgetter(
+                                    'pw_uid', 'pw_gid')(pw)
                         return u
                 except (KeyError, NameError):
                         pass
         raise RuntimeError("Unable to determine user for su.")
 
+
 class DepotTracebackException(Pkg5CommonException):
         def __init__(self, logfile, output):
                 Pkg5CommonException.__init__(self)
@@ -2416,10 +2421,11 @@
                 return self.cmdline_run(cmdline, comment=comment, exit=exit,
                     su_wrap=su_wrap)
 
-        def pkgrepo(self, command, comment="", exit=0, su_wrap=False):
+        def pkgrepo(self, command, comment="", exit=0, su_wrap=False,
+            stderr=False):
                 cmdline = "%s/usr/bin/pkgrepo %s" % (g_proto_area, command)
                 return self.cmdline_run(cmdline, comment=comment, exit=exit,
-                    su_wrap=su_wrap)
+                    su_wrap=su_wrap, stderr=stderr)
 
         def pkgsign(self, depot_url, command, exit=0, comment=""):
                 args = []
@@ -2445,7 +2451,7 @@
                 return self.pkgsign(depot_url, sign_args, exit=exit)
 
         def pkgsend(self, depot_url="", command="", exit=0, comment="",
-            allow_timestamp=False):
+            allow_timestamp=False, su_wrap=False):
                 args = []
                 if allow_timestamp:
                         args.append("-D allow-timestamp")
@@ -2460,7 +2466,8 @@
                     " ".join(args))
 
                 retcode, out = self.cmdline_run(cmdline, comment=comment,
-                    exit=exit, out=True, prefix=prefix, raise_error=False)
+                    exit=exit, out=True, prefix=prefix, raise_error=False,
+                    su_wrap=su_wrap)
                 errout = self.errout
 
                 cmdop = command.split(' ')[0]
@@ -2495,7 +2502,7 @@
                 return retcode, published
 
         def pkgsend_bulk(self, depot_url, commands, exit=0, comment="",
-            no_catalog=False, refresh_index=False):
+            no_catalog=False, refresh_index=False, su_wrap=False):
                 """ Send a series of packaging commands; useful  for quickly
                     doing a bulk-load of stuff into the repo.  All commands are
                     expected to work; if not, the transaction is abandoned.  If
@@ -2543,6 +2550,10 @@
                                         for l in accumulate:
                                                 os.write(fd, "%s\n" % l)
                                         os.close(fd)
+                                        if su_wrap:
+                                                os.chown(f_path,
+                                                    *get_su_wrap_user(
+                                                    uid_gid=True))
                                         try:
                                                 cmd = "publish %s -d %s %s" % (
                                                     extra_opts, self.test_root,
@@ -2557,7 +2568,8 @@
                                                 # package behaviour.
                                                 retcode, published = \
                                                     self.pkgsend(depot_url, cmd,
-                                                    allow_timestamp=True)
+                                                    allow_timestamp=True,
+                                                    su_wrap=su_wrap)
                                                 if retcode == 0 and published:
                                                         plist.append(published)
                                         except:
@@ -2575,7 +2587,7 @@
 
                         if exit == 0 and refresh_index:
                                 self.pkgrepo("-s %s refresh --no-catalog" %
-                                    depot_url)
+                                    depot_url, su_wrap=su_wrap)
                 except UnexpectedExitCodeException, e:
                         if e.exitcode != exit:
                                 raise