src/client.py
author Shawn Walker <shawn.walker@sun.com>
Tue, 30 Sep 2008 19:37:17 -0500
changeset 556 1c3526ca7b9e
parent 551 233f0eeddd02
child 561 be1ad23e7704
permissions -rwxr-xr-x
2022 client should provide operational intent to server 3565 client should provide version information when providing intent to server

#!/usr/bin/python2.4
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
# Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#
# pkg - package system client utility
#
# We use urllib2 for GET and POST operations, but httplib for PUT and DELETE
# operations.
#
# The client is going to maintain an on-disk cache of its state, so that
# startup assembly of the graph is reduced.
#
# Client graph is of the entire local catalog.  As operations progress, package
# states will change.
#
# Deduction operation allows the compilation of the local component of the
# catalog, only if an authoritative repository can identify critical files.
#
# Environment variables
#
# PKG_IMAGE - root path of target image
# PKG_IMAGE_TYPE [entire, partial, user] - type of image
#       XXX or is this in the Image configuration?

import getopt
import gettext
import itertools
import os
import re
import socket
import sys
import traceback
import urllib2
import urlparse
import datetime
import time
import calendar

import OpenSSL.crypto

import pkg.client.image as image
import pkg.client.imageplan as imageplan
import pkg.client.filelist as filelist
import pkg.client.progress as progress
import pkg.client.bootenv as bootenv
import pkg.client.history as history
import pkg.search_errors as search_errors
import pkg.fmri as fmri
import pkg.misc as misc
from pkg.misc import msg, emsg, PipeError
import pkg.version
import pkg.Uuid25
import pkg

def error(text):
        """Emit an error message prefixed by the command name """

        # If we get passed something like an Exception, we can convert it
        # down to a string.
        text = str(text)
        # If the message starts with whitespace, assume that it should come
        # *before* the command-name prefix.
        text_nows = text.lstrip()
        ws = text[:len(text) - len(text_nows)]

        # This has to be a constant value as we can't reliably get our actual
        # program name on all platforms.
        emsg(ws + "pkg: " + text_nows)

def usage(usage_error = None):
        """Emit a usage message and optionally prefix it with a more
            specific error message.  Causes program to exit. """

        if usage_error:
                error(usage_error)

        emsg(_("""\
Usage:
        pkg [options] command [cmd_options] [operands]

Basic subcommands:
        pkg install [-nvq] package...
        pkg uninstall [-nrvq] package...
        pkg list [-aHsuv] [package...]
        pkg image-update [-nvq]
        pkg refresh [--full]
        pkg version
        pkg help

Advanced subcommands:
        pkg info [-lr] [--license] [pkg_fmri_pattern ...]
        pkg search [-lrI] [-s server] token
        pkg verify [-fHqv] [pkg_fmri_pattern ...]
        pkg contents [-Hmr] [-o attribute ...] [-s sort_key] [-t action_type ... ]
            pkg_fmri_pattern [...]
        pkg image-create [-FPUz] [--full|--partial|--user] [--zone]
            [-k ssl_key] [-c ssl_cert] -a <prefix>=<url> dir

        pkg set-property propname propvalue
        pkg unset-property propname ...
        pkg property [-H] [propname ...]

        pkg set-authority [-P] [-k ssl_key] [-c ssl_cert] [--reset-uuid]
            [-O origin_url] [-m mirror to add | --add-mirror=mirror to add]
            [-M mirror to remove | --remove-mirror=mirror to remove] authority
        pkg unset-authority authority ...
        pkg authority [-HP] [authname]
        pkg history [-Hl]
        pkg purge-history
        pkg rebuild-index

Options:
        -R dir

Environment:
        PKG_IMAGE"""))
        sys.exit(2)

# XXX Subcommands to implement:
#        pkg image-set name value
#        pkg image-unset name
#        pkg image-get [name ...]

INCONSISTENT_INDEX_ERROR_MESSAGE = "The search index appears corrupted.  " + \
    "Please rebuild the index with 'pkg rebuild-index'."

PROBLEMATIC_PERMISSIONS_ERROR_MESSAGE = " (Failure of consistent use " + \
    "of pfexec when running pkg commands is often a source of this problem.)"

def get_partial_indexing_error_message(text):
        return "Result of partial indexing found.\n" + \
            "Could not make: " + \
            text + "\nbecause it already exists. " + \
            "Please use 'pkg rebuild-index' " + \
            "to fix this problem."

def check_fmri_args(args):
        """ Convenience routine to check that input args are valid fmris. """
        ret = True
        for x in args:
                try:
                        #
                        # Pass a bogus build release-- needed to satisfy
                        # fmri's checks in the common case that a version but
                        # no build release was specified by the user.
                        #
                        fmri.MatchingPkgFmri(x, build_release="1.0")
                except fmri.IllegalFmri, e:
                        error(e)
                        ret = False
        return ret

def list_inventory(img, args):
        all_known = False
        display_headers = True
        upgradable_only = False
        verbose = False
        summary = False

        opts, pargs = getopt.getopt(args, "aHsuv")

        for opt, arg in opts:
                if opt == "-a":
                        all_known = True
                elif opt == "-H":
                        display_headers = False
                elif opt == "-s":
                        summary = True
                elif opt == "-u":
                        upgradable_only = True
                elif opt == "-v":
                        verbose = True

        if summary and verbose:
                usage(_("-s and -v may not be combined"))

        if verbose:
                fmt_str = "%-64s %-10s %s"
        elif summary:
                fmt_str = "%-30s %s"
        else:
                fmt_str = "%-45s %-15s %-10s %s"

        if not check_fmri_args(pargs):
                return 1

        img.history.operation_name = "list"
        img.load_catalogs(progress.NullProgressTracker())

        seen_one_pkg = False
        found = False
        try:
                for pfmri, state in img.inventory(pargs, all_known):
                        seen_one_pkg = True
                        if upgradable_only and not state["upgradable"]:
                                continue

                        if not found:
                                if display_headers:
                                        if verbose:
                                                msg(fmt_str % \
                                                    ("FMRI", "STATE", "UFIX"))
                                        elif summary:
                                                msg(fmt_str % \
                                                    ("NAME (AUTHORITY)",
                                                    "SUMMARY"))
                                        else:
                                                msg(fmt_str % \
                                                    ("NAME (AUTHORITY)",
                                                    "VERSION", "STATE",
                                                    "UFIX"))
                                found = True

                        ufix = "%c%c%c%c" % \
                            (state["upgradable"] and "u" or "-",
                            state["frozen"] and "f" or "-",
                            state["incorporated"] and "i" or "-",
                            state["excludes"] and "x" or "-")

                        if pfmri.preferred_authority():
                                auth = ""
                        else:
                                auth = " (" + pfmri.get_authority() + ")"

                        if verbose:
                                pf = pfmri.get_fmri(
                                    img.get_default_authority())
                                msg("%-64s %-10s %s" % (pf, state["state"],
                                    ufix))
                        elif summary:
                                pf = pfmri.get_name() + auth

                                m = img.get_manifest(pfmri, filtered=True)
                                msg(fmt_str % (pf, m.get("description", "")))

                        else:
                                pf = pfmri.get_name() + auth
                                msg(fmt_str % (pf, pfmri.get_version(),
                                    state["state"], ufix))

                if not found:
                        if not seen_one_pkg and not all_known:
                                emsg(_("no packages installed"))
                                img.history.operation_result = \
                                    history.RESULT_NOTHING_TO_DO
                                return 1

                        if upgradable_only:
                                if pargs:
                                        emsg(_("No specified packages have " \
                                            "available updates"))
                                else:
                                        emsg(_("No installed packages have " \
                                            "available updates"))
                                img.history.operation_result = \
                                    history.RESULT_NOTHING_TO_DO
                                return 1

                        img.history.operation_result = \
                            history.RESULT_NOTHING_TO_DO
                        return 1

                img.history.operation_result = history.RESULT_SUCCEEDED
                return 0

        except image.InventoryException, e:
                if e.illegal:
                        for i in e.illegal:
                                error(i)
                        img.history.operation_result = \
                            history.RESULT_FAILED_BAD_REQUEST
                        return 1

                if all_known:
                        state = image.PKG_STATE_KNOWN
                else:
                        state = image.PKG_STATE_INSTALLED
                for pat in e.notfound:
                        error(_("no packages matching '%s' %s") % (pat, state))
                img.history.operation_result = history.RESULT_NOTHING_TO_DO
                return 1

def get_tracker(quiet = False):
        if quiet:
                progresstracker = progress.QuietProgressTracker()
        else:
                try:
                        progresstracker = \
                            progress.FancyUNIXProgressTracker()
                except progress.ProgressTrackerException:
                        progresstracker = progress.CommandLineProgressTracker()
        return progresstracker



def installed_fmris_from_args(img, args):
        """Helper function to translate client command line arguments
            into a list of installed fmris.  Used by info, contents, verify.

            XXX consider moving into image class
        """
        found = []
        notfound = []
        illegals = []
        try:
                for m in img.inventory(args):
                        found.append(m[0])
        except image.InventoryException, e:
                illegals = e.illegal
                notfound = e.notfound

        return found, notfound, illegals

def verify_image(img, args):
        opts, pargs = getopt.getopt(args, "vfqH")

        quiet = verbose = False
        # for now, always check contents of files
        forever = display_headers = True

        for opt, arg in opts:
                if opt == "-H":
                        display_headers = False
                if opt == "-v":
                        verbose = True
                elif opt == "-f":
                        forever = True
                elif opt == "-q":
                        quiet = True
                        display_headers = False

        if verbose and quiet:
                usage(_("verify: -v and -q may not be combined"))

        progresstracker = get_tracker(quiet)

        if not check_fmri_args(pargs):
                return 1

        img.load_catalogs(progresstracker)

        fmris, notfound, illegals = installed_fmris_from_args(img, pargs)

        if illegals:
                for i in illegals:
                        emsg(str(i))
                return 1

        any_errors = False

        header = False
        for f in fmris:
                pkgerr = False
                for err in img.verify(f, progresstracker,
                    verbose=verbose, forever=forever):
                        #
                        # Eventually this code should probably
                        # move into the progresstracker
                        #
                        if not pkgerr:
                                if display_headers and not header:
                                        msg("%-50s %7s" % ("PACKAGE",
                                            "STATUS"))
                                        header = True

                                if not quiet:
                                        msg("%-50s %7s" % (f.get_pkg_stem(),
                                            "ERROR"))
                                pkgerr = True

                        if not quiet:
                                msg("\t%s" % err[0])
                                for x in err[1]:
                                        msg("\t\t%s" % x)
                if verbose and not pkgerr:
                        if display_headers and not header:
                                msg("%-50s %7s" % ("PACKAGE", "STATUS"))
                                header = True
                        msg("%-50s %7s" % (f.get_pkg_stem(), "OK"))

                any_errors = any_errors or pkgerr

        if fmris:
                progresstracker.verify_done()

        if notfound:
                if fmris:
                        emsg()
                emsg(_("""\
pkg: no packages matching the following patterns you specified are
installed on the system.\n"""))
                for p in notfound:
                        emsg("        %s" % p)
                if fmris:
                        if any_errors:
                                msg2 = "See above for\nverification failures."
                        else:
                                msg2 = "No packages failed\nverification."
                        emsg(_("\nAll other patterns matched installed "
                            "packages.  %s" % msg2))
                any_errors = True

        if any_errors:
                return 1
        return 0

def ipkg_is_up_to_date(img):
        """ Test whether SUNWipkg is updated to the latest version
            known to be available for this image """
        #
        # This routine makes the distinction between the "target image"--
        # which we're going to alter, and the "running image", which is to
        # say whatever image appears to contain the version of the pkg
        # command we're running.
        #

        #
        # There are two relevant cases here:
        #     1) Packaging code and image we're updating are the same
        #        image.  (i.e. 'pkg image-update')
        #
        #     2) Packaging code's image and the image we're updating are
        #        different (i.e. 'pkg image-update -R')
        #
        # In general, we care about getting the user to run the
        # most recent packaging code available for their build.  So,
        # if we're not in the liveroot case, we create a new image
        # which represents "/" on the system.
        #

        # always quiet for this part of the code.
        progresstracker = get_tracker(True)

        if not img.is_liveroot():
                newimg = image.Image()
                cmdpath = os.path.join(os.getcwd(), sys.argv[0])
                cmdpath = os.path.realpath(cmdpath)
                cmddir = os.path.dirname(os.path.realpath(cmdpath))
                try:
                        #
                        # Find the path to ourselves, and use that
                        # as a way to locate the image we're in.  It's
                        # not perfect-- we could be in a developer's
                        # workspace, for example.
                        #
                        newimg.find_root(cmddir)
                except ValueError:
                        # We can't answer in this case, so we return True to
                        # let installation proceed.
                        msg(_("No image corresponding to '%s' was located. " \
                            "Proceeding.") % cmdpath)
                        return True
                newimg.load_config()

                # Refresh the catalog, so that we can discover if a new
                # SUNWipkg is available.
                try:
                        newimg.retrieve_catalogs()
                except image.CatalogRefreshException, cre:
                        display_catalog_failures(cre)
                        error(_("SUNWipkg update check failed."))
                        return False

                # Load catalog.
                newimg.load_catalogs(progresstracker)
                img = newimg

        msg(_("Checking that SUNWipkg (in '%s') is up to date... ") % \
            img.get_root())

        try:
                img.make_install_plan(["SUNWipkg"], progresstracker,
                    filters=[], noexecute=True)
        except RuntimeError:
                return True

        if img.imageplan.nothingtodo():
                return True

        msg(_("WARNING: pkg(5) appears to be out of date, and should be " \
            "updated before\nrunning image-update.\n"))
        msg(_("Please update pkg(5) using 'pfexec pkg install SUNWipkg' " \
            "and then retry\nthe image-update."))
        return False


def image_update(img, args):
        """Attempt to take all installed packages specified to latest
        version."""

        # XXX Authority-catalog issues.
        # XXX Are filters appropriate for an image update?
        # XXX Leaf package refinements.

        # Verify validity of certificates before attempting network operations
        if not img.check_cert_validity():
                return 1

        ret_code = 0

        opts, pargs = getopt.getopt(args, "b:fnvq", ["no-refresh"])

        force = quiet = noexecute = verbose = False
        refresh_catalogs = True
        for opt, arg in opts:
                if opt == "-n":
                        noexecute = True
                elif opt == "-v":
                        verbose = True
                elif opt == "-b":
                        filelist.FileList.maxbytes_default = int(arg)
                elif opt == "-q":
                        quiet = True
                elif opt == "-f":
                        force = True
                elif opt == "--no-refresh":
                        refresh_catalogs = False

        if verbose and quiet:
                usage(_("image-update: -v and -q may not be combined"))

        if pargs:
                usage(_("image-update: command does not take operands " \
                    "('%s')") % " ".join(pargs))

        img.history.operation_name = "image-update"
        progresstracker = get_tracker(quiet)
        img.load_catalogs(progresstracker)

        if refresh_catalogs:
                try:
                        img.retrieve_catalogs()
                except image.CatalogRefreshException, cre:
                        if display_catalog_failures(cre) == 0:
                                if not noexecute:
                                        return 1
                        else:
                                ret_code = 3

                # Reload catalog. This picks up the update from
                # retrieve_catalogs.
                img.load_catalogs(progresstracker)

        #
        # If we can find SUNWipkg and SUNWcs in the target image, then
        # we assume this is a valid opensolaris image, and activate some
        # special case behaviors.
        #
        opensolaris_image = True
        fmris, notfound, illegals = \
            installed_fmris_from_args(img, ["SUNWipkg", "SUNWcs"])
        assert(len(illegals) == 0)
        if notfound:
                opensolaris_image = False

        if opensolaris_image and not force:
                if not ipkg_is_up_to_date(img):
                        img.history.operation_result = \
                            history.RESULT_FAILED_CONSTRAINED
                        return 1

        pkg_list = [ ipkg.get_pkg_stem() for ipkg in img.gen_installed_pkgs() ]

        try:
                img.make_install_plan(pkg_list, progresstracker,
                    verbose=verbose, noexecute=noexecute)
        except RuntimeError, e:
                img.history.operation_result = history.RESULT_FAILED_UNKNOWN
                error(_("image-update failed: %s") % e)
                return 1

        assert img.imageplan

        if img.imageplan.nothingtodo():
                img.history.operation_result = history.RESULT_NOTHING_TO_DO
                msg(_("No updates available for this image."))
                return ret_code

        if noexecute:
                img.history.operation_result = history.RESULT_NOTHING_TO_DO
                return ret_code

        try:
                img.imageplan.preexecute()
        except Exception, e:
                img.history.operation_result = history.RESULT_FAILED_UNKNOWN
                error(_("\nAn unexpected error happened during " \
                    "image-update:"))
                img.cleanup_downloads()
                raise
        try:
                be = bootenv.BootEnv(img.get_root())
        except RuntimeError:
                be = bootenv.BootEnvNull(img.get_root())

        be.init_image_recovery(img)

        if img.is_liveroot():
                img.history.operation_result = \
                    history.RESULT_FAILED_BAD_REQUEST
                error(_("image-update cannot be done on live image"))
                return 1

        try:
                img.imageplan.execute()
                be.activate_image()
        except RuntimeError, e:
                img.history.operation_result = history.RESULT_FAILED_UNKNOWN
                error(_("image-update failed: %s") % e)
                be.restore_image()
                ret_code = 1
        except search_errors.InconsistentIndexException, e:
                img.history.operation_result = history.RESULT_FAILED_SEARCH
                error(INCONSISTENT_INDEX_ERROR_MESSAGE)
                ret_code = 1
        except search_errors.PartialIndexingException, e:
                img.history.operation_result = history.RESULT_FAILED_SEARCH
                error(get_partial_indexing_error_message(e.cause))
                ret_code = 1
        except search_errors.ProblematicPermissionsIndexException, e:
                img.history.operation_result = history.RESULT_FAILED_STORAGE
                error(str(e) + PROBLEMATIC_PERMISSIONS_ERROR_MESSAGE)
                ret_code = 1
        except Exception, e:
                img.history.operation_result = history.RESULT_FAILED_UNKNOWN
                error(_("\nAn unexpected error happened during " \
                    "image-update: %s") % e)
                be.restore_image()
                img.cleanup_downloads()
                raise

        img.cleanup_downloads()
        if ret_code == 0:
                img.cleanup_cached_content()
                img.history.operation_result = history.RESULT_SUCCEEDED

                if opensolaris_image:
                        msg("\n" + "-" * 75)
                        msg(_("NOTE: Please review release notes posted at:\n"
                            "   http://opensolaris.org/os/project/indiana/"
                            "resources/rn3/"))
                        msg("-" * 75 + "\n")

        return ret_code


def install(img, args):
        """Attempt to take package specified to INSTALLED state.  The operands
        are interpreted as glob patterns."""

        # XXX Authority-catalog issues.

        # Verify validity of certificates before attempting network operations
        if not img.check_cert_validity():
                return 1

        ret_code = 0
        
        opts, pargs = getopt.getopt(args, "nvb:f:q", ["no-refresh"])

        quiet = noexecute = verbose = False
        refresh_catalogs = True
        filters = []
        for opt, arg in opts:
                if opt == "-n":
                        noexecute = True
                elif opt == "-v":
                        verbose = True
                elif opt == "-b":
                        filelist.FileList.maxbytes_default = int(arg)
                elif opt == "-f":
                        filters += [ arg ]
                elif opt == "-q":
                        quiet = True
                elif opt == "--no-refresh":
                        refresh_catalogs = False

        if not pargs:
                usage(_("install: at least one package name required"))

        if verbose and quiet:
                usage(_("install: -v and -q may not be combined"))

        img.history.operation_name = "install"
        progresstracker = get_tracker(quiet)

        if not check_fmri_args(pargs):
                return 1

        img.load_catalogs(progresstracker)

        if refresh_catalogs:
        
                try:
                        img.retrieve_catalogs()
                except image.CatalogRefreshException, cre:
                        if not display_catalog_failures(cre):
                                if not noexecute:
                                        return 1
                        else:
                                ret_code = 3

                # Reload catalog. This picks up the update from
                # retrieve_catalogs.
                img.load_catalogs(progresstracker)

        pkg_list = [ pat.replace("*", ".*").replace("?", ".")
            for pat in pargs ]

        try:
                img.make_install_plan(pkg_list, progresstracker,
                    filters=filters, verbose=verbose, noexecute=noexecute)
        except RuntimeError, e:
                img.history.operation_result = history.RESULT_FAILED_UNKNOWN
                error(_("install failed (runtime error): %s") % e)
                return 1
        except image.InventoryException, e:
                img.history.operation_result = \
                    history.RESULT_FAILED_UNKNOWN
                error(_("install failed (inventory exception): %s") % e)
                return 1
        except fmri.IllegalFmri, e:
                img.history.operation_result = \
                    history.RESULT_FAILED_BAD_REQUEST
                error(e)
                return 1

        assert img.imageplan

        #
        # The result of make_install_plan is that an imageplan is now filled
        # out for the image.
        #
        if img.imageplan.nothingtodo():
                img.history.operation_result = history.RESULT_NOTHING_TO_DO
                msg(_("Nothing to install in this image (is this package " \
                    "already installed?)"))
                return ret_code

        if noexecute:
                img.history.operation_result = history.RESULT_NOTHING_TO_DO
                return ret_code

        try:
                img.imageplan.preexecute()
        except Exception, e:
                img.history.operation_result = history.RESULT_FAILED_UNKNOWN
                error(_("\nAn unexpected error happened during " \
                    "install:"))
                img.cleanup_downloads()
                raise

        try:
                be = bootenv.BootEnv(img.get_root())
        except RuntimeError:
                be = bootenv.BootEnvNull(img.get_root())

        try:
                img.imageplan.execute()
                be.activate_install_uninstall()
        except RuntimeError, e:
                img.history.operation_result = history.RESULT_FAILED_UNKNOWN
                error(_("installation failed: %s") % e)
                be.restore_install_uninstall()
                ret_code = 1
        except search_errors.InconsistentIndexException, e:
                img.history.operation_result = history.RESULT_FAILED_SEARCH
                error(INCONSISTENT_INDEX_ERROR_MESSAGE)
                ret_code = 1
        except search_errors.PartialIndexingException, e:
                img.history.operation_result = history.RESULT_FAILED_SEARCH
                error(get_partial_indexing_error_message(e.cause))
                ret_code = 1
        except search_errors.ProblematicPermissionsIndexException, e:
                img.history.operation_result = history.RESULT_FAILED_STORAGE
                error(str(e) + PROBLEMATIC_PERMISSIONS_ERROR_MESSAGE)
                ret_code = 1
        except Exception, e:
                img.history.operation_result = history.RESULT_FAILED_UNKNOWN
                error(_("An unexpected error happened during " \
                    "installation: %s") % e)
                be.restore_install_uninstall()
                img.cleanup_downloads()
                raise

        img.cleanup_downloads()
        if ret_code == 0:
                img.cleanup_cached_content()
                img.history.operation_result = history.RESULT_SUCCEEDED

        return ret_code


def uninstall(img, args):
        """Attempt to take package specified to DELETED state."""

        opts, pargs = getopt.getopt(args, "nrvq")

        quiet = noexecute = recursive_removal = verbose = False
        for opt, arg in opts:
                if opt == "-n":
                        noexecute = True
                elif opt == "-r":
                        recursive_removal = True
                elif opt == "-v":
                        verbose = True
                elif opt == "-q":
                        quiet = True

        if not pargs:
                usage(_("uninstall: at least one package name required"))

        if verbose and quiet:
                usage(_("uninstall: -v and -q may not be combined"))

        img.history.operation_name = "uninstall"
        progresstracker = get_tracker(quiet)

        if not check_fmri_args(pargs):
                return 1

        img.load_catalogs(progresstracker)

        ip = imageplan.ImagePlan(img, progresstracker, recursive_removal,
            noexecute)

        err = 0

        for ppat in pargs:
                rpat = re.sub("\*", ".*", ppat)
                rpat = re.sub("\?", ".", rpat)

                try:
                        matches = list(img.inventory([ rpat ]))
                except RuntimeError:
                        error(_("'%s' not even in catalog!") % ppat)
                        err = 1
                        continue
                except image.InventoryException, e:
                        error(e)
                        err = 1
                        continue

                if len(matches) > 1:
                        error(_("'%s' matches multiple packages") % ppat)
                        for k in matches:
                                msg("\t%s" % k[0])
                        err = 1
                        continue

                if len(matches) < 1:
                        error(_("'%s' matches no installed packages") % \
                            ppat)
                        err = 1
                        continue

                # Propose the removal of the first (and only!) match.
                ip.propose_fmri_removal(matches[0][0])

        if err == 1:
                img.history.operation_result = \
                    history.RESULT_FAILED_BAD_REQUEST
                return err

        if verbose:
                msg(_("Before evaluation:"))
                msg(ip)

        try:
                img.history.operation_start_state = ip.get_plan()
                ip.evaluate()
                img.history.operation_end_state = ip.get_plan(full=False)
        except imageplan.NonLeafPackageException, e:
                img.history.operation_result = \
                    history.RESULT_FAILED_CONSTRAINED
                error("""Cannot remove '%s' due to the following packages """
                    """that depend on it:""" % e[0])
                for d in e[1]:
                        emsg("  %s" % d)
                return 1

        img.imageplan = ip

        if verbose:
                msg(_("After evaluation:"))
                ip.display()

        assert not ip.nothingtodo()

        if noexecute:
                img.history.operation_result = history.RESULT_NOTHING_TO_DO
                return 0

        try:
                img.imageplan.preexecute()
        except Exception, e:
                img.history.operation_result = history.RESULT_FAILED_UNKNOWN
                error(_("\nAn unexpected error happened during " \
                    "uninstall"))
                raise

        try:
                be = bootenv.BootEnv(img.get_root())
        except RuntimeError:
                be = bootenv.BootEnvNull(img.get_root())

        try:
                ip.execute()
        except RuntimeError, e:
                img.history.operation_result = history.RESULT_FAILED_UNKNOWN
                error(_("installation failed: %s") % e)
                be.restore_install_uninstall()
                err = 1
        except search_errors.InconsistentIndexException, e:
                img.history.operation_result = history.RESULT_FAILED_SEARCH
                error(INCONSISTENT_INDEX_ERROR_MESSAGE)
                err = 1
        except search_errors.PartialIndexingException, e:
                img.history.operation_result = history.RESULT_FAILED_SEARCH
                error(get_partial_indexing_error_message(e.cause))
                err = 1
        except search_errors.ProblematicPermissionsIndexException, e:
                img.history.operation_result = history.RESULT_FAILED_STORAGE
                error(str(e) + PROBLEMATIC_PERMISSIONS_ERROR_MESSAGE)
                err = 1
        except Exception, e:
                img.history.operation_result = history.RESULT_FAILED_UNKNOWN
                error(_("An unexpected error happened during " \
                    "uninstallation: %s") % e)
                be.restore_install_uninstall()
                raise

        if ip.state == imageplan.EXECUTED_OK:
                be.activate_install_uninstall()
                img.history.operation_result = history.RESULT_SUCCEEDED
        else:
                be.restore_install_uninstall()

        return err

def freeze(img, args):
        """Attempt to take package specified to FROZEN state, with given
        restrictions.  Package must have been in the INSTALLED state."""
        return 0

def unfreeze(img, args):
        """Attempt to return package specified to INSTALLED state from FROZEN
        state."""
        return 0

def search(img, args):
        """Search through the reverse index databases for the given token."""

        # Verify validity of certificates before attempting network operations
        if not img.check_cert_validity():
                return 1

        opts, pargs = getopt.getopt(args, "lrs:I")

        local = remote = case_sensitive = False
        servers = []
        for opt, arg in opts:
                if opt == "-l":
                        local = True
                elif opt == "-r":
                        remote = True
                elif opt == "-s":
                        if not arg.startswith("http://") and \
                            not arg.startswith("https://"):
                                arg = "http://" + arg
                        remote = True
                        servers.append({"origin": arg})
                elif opt == "-I":
                        case_sensitive = True

        if not local and not remote:
                local = True

        if remote and case_sensitive:
                emsg("Case sensitive remote search not currently supported.")
                usage()

        if not pargs:
                usage()

        searches = []
        if local:
                try:
                        searches.append(img.local_search(pargs,
                            case_sensitive))
                except search_errors.NoIndexException, nie:
                        error(str(nie) +
                            "\nPlease try 'pkg rebuild-index' to recreate " +
                            "the index.")
                        return 1
                except (search_errors.InconsistentIndexException,
                           search_errors.IncorrectIndexFileHash):
                        error("The search index appears corrupted.  Please "
                            "rebuild the index with 'pkg rebuild-index'.")
                        return 1

        if remote:
                searches.append(img.remote_search(pargs, servers))

        # By default assume we don't find anything.
        retcode = 1

        try:
                first = True
                for index, mfmri, action, value in itertools.chain(*searches):
                        retcode = 0
                        if first:
                                if action and value:
                                        msg("%-10s %-9s %-25s %s" % ("INDEX",
                                            "ACTION", "VALUE", "PACKAGE"))
                                else:
                                        msg("%-10s %s" % ("INDEX", "PACKAGE"))
                                first = False
                        if action and value:
                                msg("%-10s %-9s %-25s %s" % (index, action,
                                    value, fmri.PkgFmri(str(mfmri)
                                    ).get_short_fmri()))
                        else:
                                msg("%-10s %s" % (index, mfmri))

        except RuntimeError, failed:
                emsg("Some servers failed to respond:")
                for auth, err in failed.args[0]:
                        if isinstance(err, urllib2.HTTPError):
                                emsg("    %s: %s (%d)" % \
                                    (auth["origin"], err.msg, err.code))
                        elif isinstance(err, urllib2.URLError):
                                if isinstance(err.args[0], socket.timeout):
                                        emsg("    %s: %s" % \
                                            (auth["origin"], "timeout"))
                                else:
                                        emsg("    %s: %s" % \
                                            (auth["origin"], err.args[0][1]))

                retcode = 4

        return retcode

def info_license(img, mfst, remote):
        for i, lic in enumerate(mfst.gen_actions_by_type("license")):
                if i > 0:
                        msg("")

                if remote:
                        misc.gunzip_from_stream(
                            lic.get_remote_opener(img, mfst.fmri)(),
                                sys.stdout)
                else:
                        msg(lic.get_local_opener(img, mfst.fmri)().read()[:-1])

def info(img, args):
        """Display information about a package or packages.
        """

        display_license = False
        info_local = False
        info_remote = False

        opts, pargs = getopt.getopt(args, "lr", ["license"])
        for opt, arg in opts:
                if opt == "-l":
                        info_local = True
                elif opt == "-r":
                        info_remote = True
                elif opt == "--license":
                        display_license = True

        if not info_local and not info_remote:
                info_local = True
        elif info_local and info_remote:
                usage(_("info: -l and -r may not be combined"))

        if info_remote and not pargs:
                usage(_("info: must request remote info for specific "
                    "packages"))

        if not check_fmri_args(pargs):
                return 1

        img.history.operation_name = "info"
        img.load_catalogs(progress.NullProgressTracker())

        err = 0

        if info_local:
                fmris, notfound, illegals = \
                    installed_fmris_from_args(img, pargs)
                if illegals:
                        for i in illegals:
                                emsg(str(i))
                        img.history.operation_result = \
                            history.RESULT_FAILED_BAD_REQUEST
                        return 1

                if not fmris and not notfound:
                        error(_("no packages installed"))
                        img.history.operation_result = \
                            history.RESULT_NOTHING_TO_DO
                        return 1
        elif info_remote:
                # Verify validity of certificates before attempting network
                # operations
                if not img.check_cert_validity():
                        img.history.operation_result = \
                            history.RESULT_FAILED_TRANSPORT
                        return 1

                fmris = []
                notfound = []

                # XXX This loop really needs not to be copied from
                # Image.make_install_plan()!
                for p in pargs:
                        try:
                                matches = list(img.inventory([ p ],
                                    all_known = True))
                        except image.InventoryException, e:
                                assert(len(e.notfound) == 1)
                                notfound.append(e.notfound[0])
                                err = 1
                                continue

                        pnames = {}
                        pmatch = []
                        npnames = {}
                        npmatch = []
                        for m, state in matches:
                                if m.preferred_authority():
                                        pnames[m.get_pkg_stem()] = 1
                                        pmatch.append(m)
                                else:
                                        npnames[m.get_pkg_stem()] = 1
                                        npmatch.append(m)

                        if len(pnames.keys()) > 1:
                                msg(_("pkg: '%s' matches multiple "
                                    "packages") % p)
                                for k in pnames.keys():
                                        msg("\t%s" % k)
                                continue
                        elif len(pnames.keys()) < 1 and \
                            len(npnames.keys()) > 1:
                                msg(_("pkg: '%s' matches multiple "
                                    "packages") % p)
                                for k in npnames.keys():
                                        msg("\t%s" % k)
                                continue

                        # matches is a list reverse sorted by version, so take
                        # the first; i.e., the latest.
                        if len(pmatch) > 0:
                                fmris.append(pmatch[0])
                        else:
                                fmris.append(npmatch[0])

        manifests = ( img.get_manifest(f, filtered=True) for f in fmris )

        for i, m in enumerate(manifests):
                if i > 0:
                        msg("")

                if display_license:
                        info_license(img, m, info_remote)
                        continue

                authority, name, version = m.fmri.tuple()
                authority = fmri.strip_auth_pfx(authority)
                summary = m.get("description", "")
                if m.fmri.preferred_authority():
                        authority += _(" (preferred)")
                if img.is_installed(m.fmri):
                        state = _("Installed")
                else:
                        state = _("Not installed")

                msg("          Name:", name)
                msg("       Summary:", summary)
                msg("         State:", state)

                # XXX even more info on the authority would be nice?
                msg("     Authority:", authority)
                msg("       Version:", version.release)
                msg(" Build Release:", version.build_release)
                msg("        Branch:", version.branch)
                msg("Packaging Date:", version.get_timestamp().ctime())
                if m.size > (1024 * 1024):
                        msg("          Size: %.1f MB" % \
                            (m.size / float(1024 * 1024)))
                elif m.size > 1024:
                        msg("          Size: %d kB" % (m.size / 1024))
                else:
                        msg("          Size: %d B" % m.size)
                msg("          FMRI:", m.fmri)
                # XXX need to properly humanize the manifest.size
                # XXX add license/copyright info here?

        if notfound:
                err = 1
                if fmris:
                        emsg()
                if info_local:
                        emsg(_("""\
pkg: no packages matching the following patterns you specified are
installed on the system.  Try specifying -r to query remotely:"""))
                elif info_remote:
                        emsg(_("""\
pkg: no packages matching the following patterns you specified were
found in the catalog.  Try relaxing the patterns, refreshing, and/or
examining the catalogs:"""))
                emsg()
                for p in notfound:
                        emsg("        %s" % p)
                img.history.operation_result = history.RESULT_NOTHING_TO_DO
        else:
                img.history.operation_result = history.RESULT_SUCCEEDED
        return err

def display_contents_results(actionlist, attrs, sort_attrs, action_types,
    display_headers):
        """Print results of a "list" operation """

        # widths is a list of tuples of column width and justification.  Start
        # with the widths of the column headers.
        JUST_UNKN = 0
        JUST_LEFT = -1
        JUST_RIGHT = 1
        widths = [ (len(attr) - attr.find(".") - 1, JUST_UNKN)
            for attr in attrs ]
        lines = []

        for manifest, action in actionlist:
                if action_types and action.name not in action_types:
                        continue
                line = []
                for i, attr in enumerate(attrs):
                        just = JUST_UNKN
                        # As a first approximation, numeric attributes
                        # are right justified, non-numerics left.
                        try:
                                int(action.attrs[attr])
                                just = JUST_RIGHT
                        # attribute is non-numeric or is something like
                        # a list.
                        except (ValueError, TypeError):
                                just = JUST_LEFT
                        # attribute isn't in the list, so we don't know
                        # what it might be
                        except KeyError:
                                pass

                        if attr in action.attrs:
                                a = action.attrs[attr]
                        elif attr == "action.name":
                                a = action.name
                                just = JUST_LEFT
                        elif attr == "action.key":
                                a = action.attrs[action.key_attr]
                                just = JUST_LEFT
                        elif attr == "action.raw":
                                a = action
                                just = JUST_LEFT
                        elif attr == "pkg.name":
                                a = manifest.fmri.get_name()
                                just = JUST_LEFT
                        elif attr == "pkg.fmri":
                                a = manifest.fmri
                                just = JUST_LEFT
                        elif attr == "pkg.shortfmri":
                                a = manifest.fmri.get_short_fmri()
                                just = JUST_LEFT
                        elif attr == "pkg.authority":
                                a = manifest.fmri.get_authority()
                                just = JUST_LEFT
                        else:
                                a = ""

                        line.append(a)

                        # XXX What to do when a column's justification
                        # changes?
                        if just != JUST_UNKN:
                                widths[i] = \
                                    (max(widths[i][0], len(str(a))), just)

                if line and [l for l in line if str(l) != ""]:
                        lines.append(line)

        sortidx = 0
        for i, attr in enumerate(attrs):
                if attr == sort_attrs[0]:
                        sortidx = i
                        break

        # Sort numeric columns numerically.
        if widths[sortidx][1] == JUST_RIGHT:
                def key_extract(x):
                        try:
                                return int(x[sortidx])
                        except (ValueError, TypeError):
                                return 0
        else:
                key_extract = lambda x: x[sortidx]

        if display_headers:
                headers = []
                for i, attr in enumerate(attrs):
                        headers.append(str(attr.upper()))
                        widths[i] = \
                            (max(widths[i][0], len(attr)), widths[i][1])

                # Now that we know all the widths, multiply them by the
                # justification values to get positive or negative numbers to
                # pass to the %-expander.
                widths = [ e[0] * e[1] for e in widths ]
                fmt = ("%%%ss " * len(widths)) % tuple(widths)

                msg((fmt % tuple(headers)).rstrip())
        else:
                fmt = "%s\t" * len(widths)
                fmt.rstrip("\t")

        for line in sorted(lines, key=key_extract):
                msg((fmt % tuple(line)).rstrip())

def list_contents(img, args):
        """List package contents.

        If no arguments are given, display for all locally installed packages.
        With -H omit headers and use a tab-delimited format; with -o select
        attributes to display; with -s, specify attributes to sort on; with -t,
        specify which action types to list."""

        # XXX Need remote-info option, to request equivalent information
        # from repository.

        opts, pargs = getopt.getopt(args, "Ho:s:t:mfr")

        valid_special_attrs = [ "action.name", "action.key", "action.raw",
            "pkg.name", "pkg.fmri", "pkg.shortfmri", "pkg.authority",
            "pkg.size" ]

        display_headers = True
        display_raw = False
        display_nofilters = False
        remote = False
        local = False
        attrs = []
        sort_attrs = []
        action_types = []
        for opt, arg in opts:
                if opt == "-H":
                        display_headers = False
                elif opt == "-o":
                        attrs.extend(arg.split(","))
                elif opt == "-s":
                        sort_attrs.append(arg)
                elif opt == "-t":
                        action_types.extend(arg.split(","))
                elif opt == "-r":
                        remote = True
                elif opt == "-m":
                        display_raw = True
                elif opt == "-f":
                        # Undocumented, for now.
                        display_nofilters = True

        if not remote and not local:
                local = True
        elif local and remote:
                usage(_("contents: -l and -r may not be combined"))

        if remote and not pargs:
                usage(_("contents: must request remote contents for specific "
                   "packages"))

        if not check_fmri_args(pargs):
                return 1

        if display_raw:
                display_headers = False
                attrs = [ "action.raw" ]

                invalid = set(("-H", "-o", "-t")). \
                    intersection(set([x[0] for x in opts]))

                if len(invalid) > 0:
                        usage(_("contents: -m and %s may not be specified " \
                            "at the same time") % invalid.pop())

        for a in attrs:
                if a.startswith("action.") and not a in valid_special_attrs:
                        usage(_("Invalid attribute '%s'") % a)

                if a.startswith("pkg.") and not a in valid_special_attrs:
                        usage(_("Invalid attribute '%s'") % a)

        img.history.operation_name = "contents"
        img.load_catalogs(progress.NullProgressTracker())

        err = 0

        if local:
                fmris, notfound, illegals = \
                    installed_fmris_from_args(img, pargs)

                if illegals:
                        for i in illegals:
                                emsg(i)
                        img.history.operation_result = \
                            history.RESULT_FAILED_BAD_REQUEST
                        return 1

                if not fmris and not notfound:
                        error(_("no packages installed"))
                        img.history.operation_result = \
                            history.RESULT_NOTHING_TO_DO
                        return 1
        elif remote:
                # Verify validity of certificates before attempting network
                # operations
                if not img.check_cert_validity():
                        img.history.operation_result = \
                            history.RESULT_FAILED_TRANSPORT
                        return 1

                fmris = []
                notfound = []

                # XXX This loop really needs not to be copied from
                # Image.make_install_plan()!
                for p in pargs:
                        try:
                                matches = list(img.inventory([ p ],
                                    all_known = True))
                        except image.InventoryException, e:
                                assert(len(e.notfound) == 1)
                                notfound.append(e.notfound[0])
                                continue

                        pnames = {}
                        pmatch = []
                        npnames = {}
                        npmatch = []
                        for m, state in matches:
                                if m.preferred_authority():
                                        pnames[m.get_pkg_stem()] = 1
                                        pmatch.append(m)
                                else:
                                        npnames[m.get_pkg_stem()] = 1
                                        npmatch.append(m)

                        if len(pnames.keys()) > 1:
                                msg(_("pkg: '%s' matches multiple "
                                    "packages") % p)
                                for k in pnames.keys():
                                        msg("\t%s" % k)
                                continue
                        elif len(pnames.keys()) < 1 and \
                            len(npnames.keys()) > 1:
                                msg(_("pkg: '%s' matches multiple "
                                    "packages") % p)
                                for k in npnames.keys():
                                        msg("\t%s" % k)
                                continue

                        # matches is a list reverse sorted by version, so take
                        # the first; i.e., the latest.
                        if len(pmatch) > 0:
                                fmris.append(pmatch[0])
                        else:
                                fmris.append(npmatch[0])

        #
        # If the user specifies no specific attrs, and no specific
        # sort order, then we fill in some defaults.
        #
        if not attrs:
                # XXX Possibly have multiple exclusive attributes per column?
                # If listing dependencies and files, you could have a path/fmri
                # column which would list paths for files and fmris for
                # dependencies.
                attrs = [ "path" ]

        if not sort_attrs:
                # XXX reverse sorting
                # Most likely want to sort by path, so don't force people to
                # make it explicit
                if "path" in attrs:
                        sort_attrs = [ "path" ]
                else:
                        sort_attrs = attrs[:1]

        filt = not display_nofilters
        manifests = ( img.get_manifest(f, filtered=filt) for f in fmris )

        actionlist = [ (m, a)
                    for m in manifests
                    for a in m.actions ]

        if fmris:
                display_contents_results(actionlist, attrs, sort_attrs,
                    action_types, display_headers)

        if notfound:
                err = 1
                if fmris:
                        emsg()
                if local:
                        emsg(_("""\
pkg: no packages matching the following patterns you specified are
installed on the system.  Try specifying -r to query remotely:"""))
                elif remote:
                        emsg(_("""\
pkg: no packages matching the following patterns you specified were
found in the catalog.  Try relaxing the patterns, refreshing, and/or
examining the catalogs:"""))
                emsg()
                for p in notfound:
                        emsg("        %s" % p)
                img.history.operation_result = history.RESULT_NOTHING_TO_DO
        else:
                img.history.operation_result = history.RESULT_SUCCEEDED
        return err

def display_catalog_failures(cre):
        total = cre.total
        succeeded = cre.succeeded
        msg(_("pkg: %s/%s catalogs successfully updated:") % (succeeded,
            total))

        for auth, err in cre.failed:
                if isinstance(err, urllib2.HTTPError):
                        emsg("   %s: %s - %s" % \
                            (err.filename, err.code, err.msg))
                elif isinstance(err, urllib2.URLError):
                        if err.args[0][0] == 8:
                                emsg("    %s: %s" % \
                                    (urlparse.urlsplit(
                                        auth["origin"])[1].split(":")[0],
                                    err.args[0][1]))
                        else:
                                if isinstance(err.args[0], socket.timeout):
                                        emsg("    %s: %s" % \
                                            (auth["origin"], "timeout"))
                                else:
                                        emsg("    %s: %s" % \
                                            (auth["origin"], err.args[0][1]))
                else:
                        emsg("   ", err)

        return succeeded

def catalog_refresh(img, args):
        """Update image's catalogs."""

        # Verify validity of certificates before attempting network operations
        if not img.check_cert_validity():
                return 1

        # XXX will need to show available content series for each package
        full_refresh = False
        opts, pargs = getopt.getopt(args, "", ["full"])
        for opt, arg in opts:
                if opt == "--full":
                        full_refresh = True

        auths_to_refresh = []
        for parg in pargs:
                try:
                        auth = img.get_authority(parg)
                except KeyError:
                        tmp = "%s is not a recognized authority to " \
                            "refresh. \n'pkg authority' will show a" \
                            " list of authorities."
                        tmp = (_(tmp))
                        tmp = tmp % parg
                        error(tmp)
                        return 1
                auths_to_refresh.append(auth)

        # Ensure Image directory structure is valid.
        if not os.path.isdir("%s/catalog" % img.imgdir):
                img.mkdirs()

        # Loading catalogs allows us to perform incremental update
        img.load_catalogs(get_tracker())

        try:
                img.retrieve_catalogs(full_refresh, auths_to_refresh)
        except image.CatalogRefreshException, cre:
                if display_catalog_failures(cre) == 0:
                        return 1
                else:
                        return 3
        else:
                return 0

def authority_set(img, args):
        """pkg set-authority [-P] [-k ssl_key] [-c ssl_cert] [--reset-uuid]
            [-O origin_url] [-m mirror to add] [-M mirror to remove] 
            [--no-refresh] authority"""

        preferred = False
        ssl_key = None
        ssl_cert = None
        origin_url = None
        reset_uuid = False
        add_mirror = None
        remove_mirror = None
        ret_code = 0
        refresh_catalogs = True

        opts, pargs = getopt.getopt(args, "Pk:c:O:M:m:",
            ["add-mirror=", "remove-mirror=", "no-refresh", "reset-uuid"])

        for opt, arg in opts:
                if opt == "-P":
                        preferred = True
                if opt == "-k":
                        ssl_key = arg
                if opt == "-c":
                        ssl_cert = arg
                if opt == "-O":
                        origin_url = arg
                if opt == "-m" or opt == "--add-mirror":
                        add_mirror = arg
                if opt == "-M" or opt == "--remove-mirror":
                        remove_mirror = arg
                if opt == "--no-refresh":
                        refresh_catalogs = False
                if opt == "--reset-uuid":
                        reset_uuid = True

        if len(pargs) != 1:
                usage(
                    _("pkg: set-authority: one and only one authority " \
                        "may be set"))

        auth = pargs[0]

        if ssl_key:
                ssl_key = os.path.abspath(ssl_key)
                if not os.path.exists(ssl_key):
                        error(_("set-authority: SSL key file '%s' does not " \
                            "exist") % ssl_key)
                        return 1

        if ssl_cert:
                ssl_cert = os.path.abspath(ssl_cert)
                if not os.path.exists(ssl_cert):
                        error(_("set-authority: SSL key cert '%s' does not " \
                            "exist") % ssl_cert)
                        return 1


        if not img.has_authority(auth) and origin_url == None:
                error(_("set-authority: authority does not exist. Use " \
                    "-O to define origin URL for new authority"))
                return 1

        elif not img.has_authority(auth) and not misc.valid_auth_prefix(auth):
                error(_("set-authority: authority name has invalid "
                    "characters"))
                return 1

        if origin_url and not misc.valid_auth_url(origin_url):
                error(_("set-authority: authority URL is invalid"))
                return 1

        uuid = None
        if reset_uuid:
                uuid = pkg.Uuid25.uuid1()

        try:
                img.set_authority(auth, origin_url=origin_url,
                    ssl_key=ssl_key, ssl_cert=ssl_cert,
                    refresh_allowed=refresh_catalogs, uuid=uuid)
        except RuntimeError, e:
                error(_("set-authority failed: %s") % e)
                return 1
        except image.CatalogRefreshException, cre:
                text = "Could not refresh the catalog for %s" % \
                    auth
                error(_(text))
                ret_code = 1

        if preferred:
                img.set_preferred_authority(auth)


        if add_mirror:

                if not misc.valid_auth_url(add_mirror):
                        error(_("set-authority: added mirror's URL is "
                            "invalid"))
                        return 1

                if img.has_mirror(auth, add_mirror):
                        error(_("set-authority: mirror already exists"))
                        return 1

                img.add_mirror(auth, add_mirror)

        if remove_mirror:

                if not misc.valid_auth_url(remove_mirror):
                        error(_("set-authority: removed mirror has bad URL"))
                        return 1

                if not img.has_mirror(auth, remove_mirror):
                        error(_("set-authority: mirror does not exist"))
                        return 1


                img.del_mirror(auth, remove_mirror)


        return ret_code

def authority_unset(img, args):
        """pkg unset-authority authority ..."""

        # is this an existing authority in our image?
        # if so, delete it
        # if not, error
        preferred_auth = img.get_default_authority()

        if len(args) == 0:
                usage()

        for a in args:
                if not img.has_authority(a):
                        error(_("unset-authority: no such authority: %s") \
                            % a)
                        return 1

                if a == preferred_auth:
                        error(_("unset-authority: removal of preferred " \
                            "authority not allowed."))
                        return 1

                img.delete_authority(a)

        return 0

def authority_list(img, args):
        """pkg authorities"""
        omit_headers = False
        preferred_only = False
        preferred_authority = img.get_default_authority()

        opts, pargs = getopt.getopt(args, "HP")
        for opt, arg in opts:
                if opt == "-H":
                        omit_headers = True
                if opt == "-P":
                        preferred_only = True

        if len(pargs) == 0:
                if not omit_headers:
                        msg("%-35s %s" % ("AUTHORITY", "URL"))

                if preferred_only:
                        auths = [img.get_authority(preferred_authority)]
                else:
                        auths = img.gen_authorities()

                for a in auths:
                        # summary list
                        pfx, url, ssl_key, ssl_cert, dt, mir = \
                            img.split_authority(a)

                        if not preferred_only and pfx == preferred_authority:
                                pfx += " (preferred)"
                        msg("%-35s %s" % (pfx, url))
        else:
                img.load_catalogs(get_tracker())

                for a in pargs:
                        if not img.has_authority(a):
                                error(_("authority: no such authority: %s") \
                                    % a)
                                return 1

                        # detailed print
                        auth = img.get_authority(a)
                        pfx, url, ssl_key, ssl_cert, dt, mir = \
                            img.split_authority(auth)

                        if dt:
                                dt = dt.ctime()

                        if ssl_cert:
                                try:
                                        cert = img.build_cert(ssl_cert)
                                except (IOError, OpenSSL.crypto.Error):
                                        error(_("SSL certificate for %s" \
                                            "is invalid or non-existent.") % \
                                            pfx)
                                        error(_("Please check file at %s") %\
                                            ssl_cert)
                                        continue

                                nb = cert.get_notBefore()
                                t = time.strptime(nb, "%Y%m%d%H%M%SZ")
                                nb = datetime.datetime.utcfromtimestamp(
                                    calendar.timegm(t))

                                na = cert.get_notAfter()
                                t = time.strptime(na, "%Y%m%d%H%M%SZ")
                                na = datetime.datetime.utcfromtimestamp(
                                    calendar.timegm(t))
                        else:
                                cert = None

                        msg("")
                        msg("           Authority:", pfx)
                        msg("          Origin URL:", url)
                        msg("             SSL Key:", ssl_key)
                        msg("            SSL Cert:", ssl_cert)
                        if cert:
                                msg(" Cert Effective Date:", nb.ctime())
                                msg("Cert Expiration Date:", na.ctime())
                        msg("                UUID:", auth["uuid"])
                        msg("     Catalog Updated:", dt)
                        msg("             Mirrors:", mir)

        return 0

def property_set(img, args):
        """pkg set-property propname propvalue"""

        # ensure no options are passed in
        opts, pargs = getopt.getopt(args, "")
        try:
                propname, propvalue = pargs
        except ValueError:
                usage(
                    _("pkg: set-property: requires a property name and value"))

        try:
                img.set_property(propname, propvalue)
        except RuntimeError, e:
                error(_("set-property failed: %s") % e)
                return 1
        return 0

def property_unset(img, args):
        """pkg unset-property propname ..."""

        # is this an existing property in our image?
        # if so, delete it
        # if not, error

        # ensure no options are passed in
        opts, pargs = getopt.getopt(args, "")
        if not pargs:
                usage(
                    _("pkg: unset-property requires at least one property name"))

        for p in pargs:
                try:
                        img.delete_property(p)
                except KeyError:
                        error(_("unset-property: no such property: %s") % p)
                        return 1

        return 0

def property_list(img, args):
        """pkg property [-H] [propname ...]"""
        omit_headers = False

        opts, pargs = getopt.getopt(args, "H")
        for opt, arg in opts:
                if opt == "-H":
                        omit_headers = True

        for p in pargs:
                if not img.has_property(p):
                        error(_("property: no such property: %s") % p)
                        return 1

        if not pargs:
                pargs = img.properties()

        width = max(max([len(p) for p in pargs]), 8)
        fmt = "%%-%ss %%s" % width
        if not omit_headers:
                msg(fmt % ("PROPERTY", "VALUE"))

        for p in pargs:
                msg(fmt % (p, img.get_property(p)))

        return 0

def image_create(img, args):
        """Create an image of the requested kind, at the given path.  Load
        catalog for initial authority for convenience.

        At present, it is legitimate for a user image to specify that it will be
        deployed in a zone.  An easy example would be a program with an optional
        component that consumes global zone-only information, such as various
        kernel statistics or device information."""

        # XXX Long options support

        imgtype = image.IMG_USER
        is_zone = False
        ssl_key = None
        ssl_cert = None
        auth_name = None
        auth_url = None
        refresh_catalogs = True

        opts, pargs = getopt.getopt(args, "FPUza:k:c:",
            ["full", "partial", "user", "zone", "authority=", "no-refresh"])

        for opt, arg in opts:
                if opt == "-F" or opt == "--full":
                        imgtype = image.IMG_ENTIRE
                if opt == "-P" or opt == "--partial":
                        imgtype = image.IMG_PARTIAL
                if opt == "-U" or opt == "--user":
                        imgtype = image.IMG_USER
                if opt == "-z" or opt == "--zone":
                        is_zone = True
                if opt == "--no-refresh":
                        refresh_catalogs = False
                if opt == "-k":
                        ssl_key = arg
                if opt == "-c":
                        ssl_cert = arg
                if opt == "-a" or opt == "--authority":
                        try:
                                auth_name, auth_url = arg.split("=", 1)
                        except ValueError:
                                usage(_("image-create requires authority "
                                    "argument to be of the form "
                                    "'<prefix>=<url>'."))

        if len(pargs) != 1:
                usage(_("image-create requires a single image directory path"))

        if ssl_key:
                ssl_key = os.path.abspath(ssl_key)
                if not os.path.exists(ssl_key):
                        msg(_("pkg: set-authority: SSL key file '%s' does " \
                            "not exist") % ssl_key)
                        return 1

        if ssl_cert:
                ssl_cert = os.path.abspath(ssl_cert)
                if not os.path.exists(ssl_cert):
                        msg(_("pkg: set-authority: SSL key cert '%s' does " \
                            "not exist") % ssl_cert)
                        return 1

        if not auth_name and not auth_url:
                usage("image-create requires an authority argument")

        if not auth_name or not auth_url:
                usage(_("image-create requires authority argument to be of "
                    "the form '<prefix>=<url>'."))

        if auth_name.startswith(fmri.PREF_AUTH_PFX):
                error(_("image-create requires that a prefix not match: %s"
                        % fmri.PREF_AUTH_PFX))
                return 1

        if not misc.valid_auth_prefix(auth_name):
                error(_("image-create: authority prefix has invalid " \
                    "characters"))
                return 1

        if not misc.valid_auth_url(auth_url):
                error(_("image-create: authority URL is invalid"))
                return 1

        try:
                img.set_attrs(imgtype, pargs[0], is_zone, auth_name, auth_url,
                    ssl_key=ssl_key, ssl_cert=ssl_cert)
        except OSError, e:
                error(_("cannot create image at %s: %s") % \
                    (pargs[0], e.args[1]))
                return 1

        if refresh_catalogs:
                try:
                        img.retrieve_catalogs()
                except image.CatalogRefreshException, cre:
                        if display_catalog_failures(cre) == 0:
                                return 1
                        else:
                                return 3
        return 0


def rebuild_index(img, pargs):
        """pkg rebuild-index

        Forcibly rebuild the search indexes. Will remove existing indexes
        and build new ones from scratch."""
        quiet = False

        if pargs:
                usage(_("rebuild-index: command does not take operands " \
                    "('%s')") % " ".join(pargs))

        try:
                img.history.operation_name = "rebuild-index"
                img.rebuild_search_index(get_tracker(quiet))
        except search_errors.InconsistentIndexException:
                img.history.operation_result = history.RESULT_FAILED_SEARCH
                error(INCONSISTENT_INDEX_ERROR_MESSAGE)
                return 1
        except search_errors.ProblematicPermissionsIndexException, ppie:
                img.history.operation_result = history.RESULT_FAILED_STORAGE
                error(str(ppie) + PROBLEMATIC_PERMISSIONS_ERROR_MESSAGE)
                return 1
        else:
                img.history.operation_result = history.RESULT_SUCCEEDED
                return 0

def history_list(img, args):
        """Display history about the current image.
        """

        omit_headers = False
        long_format = False

        opts, pargs = getopt.getopt(args, "Hl")
        for opt, arg in opts:
                if opt == "-H":
                        omit_headers = True
                elif opt == "-l":
                        long_format = True

        if omit_headers and long_format:
                usage(_("history: -H and -l may not be combined"))

        if not long_format:
                if not omit_headers:
                        msg("%-19s %-25s %-15s %s" % (_("TIME"),
                            _("OPERATION"), _("CLIENT"), _("OUTCOME")))

        for entry in sorted(os.listdir(img.history.path)):
                # Load the history entry.
                he = history.History(root_dir=img.history.root_dir,
                    filename=entry)

                # Retrieve and format some of the data shared between each
                # output format.
                start_time = misc.timestamp_to_time(
                    he.operation_start_time)
                start_time = datetime.datetime.fromtimestamp(
                    start_time).isoformat()

                res = he.operation_result
                if len(res) > 1:
                        outcome = "%s (%s)" % (_(res[0]), _(res[1]))
                else:
                        outcome = _(res[0])

                if long_format:
                        data = []
                        data.append(("Operation", he.operation_name))

                        data.append(("Outcome", outcome))
                        data.append(("Client", he.client_name))
                        data.append(("Version", he.client_version))

                        data.append(("User", "%s (%s)" % \
                            (he.operation_username, he.operation_userid)))

                        data.append(("Start Time", start_time))

                        end_time = misc.timestamp_to_time(
                            he.operation_end_time)
                        end_time = datetime.datetime.fromtimestamp(
                            end_time).isoformat()
                        data.append(("End Time", end_time))

                        data.append(("Command", " ".join(he.client_args)))

                        state = he.operation_start_state
                        if state:
                                data.append(("Start State", "\n" + state))

                        state = he.operation_end_state
                        if state:
                                data.append(("End State", "\n" + state))

                        errors = "\n".join(he.operation_errors)
                        if errors:
                                data.append(("Errors", "\n" + errors))

                        for field, value in data:
                                msg("%15s: %s" % (_(field), value))

                        # Separate log entries with a blank line.
                        msg("")
                else:
                        msg("%-19s %-25s %-15s %s" % (start_time,
                            he.operation_name, he.client_name, outcome))

        return 0

# To allow exception handler access to the image.
__img = None

def main_func():
        global __img
        __img = img = image.Image()
        img.history.client_name = "pkg"

        # XXX /usr/lib/locale is OpenSolaris-specific.
        gettext.install("pkg", "/usr/lib/locale")

        try:
                opts, pargs = getopt.getopt(sys.argv[1:], "R:")
        except getopt.GetoptError, e:
                usage(_("illegal global option -- %s") % e.opt)

        if pargs == None or len(pargs) == 0:
                usage()

        subcommand = pargs[0]
        del pargs[0]

        socket.setdefaulttimeout(
            int(os.environ.get("PKG_CLIENT_TIMEOUT", "30"))) # in seconds

        # Override default MAX_TIMEOUT_COUNT if a value has been specified
        # in the environment.
        timeout_max = misc.MAX_TIMEOUT_COUNT
        misc.MAX_TIMEOUT_COUNT = int(os.environ.get("PKG_TIMEOUT_MAX",
            timeout_max))

        if subcommand == "image-create":
                try:
                        ret = image_create(img, pargs)
                except getopt.GetoptError, e:
                        usage(_("illegal %s option -- %s") % \
                            (subcommand, e.opt))
                return ret
        elif subcommand == "version":
                if pargs:
                        usage(_("version: command does not take operands " \
                            "('%s')") % " ".join(pargs))
                msg(pkg.VERSION)
                return 0
        elif subcommand == "help":
                try:
                        usage()
                except SystemExit:
                        return 0

        for opt, arg in opts:
                if opt == "-R":
                        mydir = arg

        if "mydir" not in locals():
                try:
                        mydir = os.environ["PKG_IMAGE"]
                except KeyError:
                        try:
                                mydir = os.getcwd()
                        except OSError, e:
                                try:
                                        mydir = os.environ["PWD"]
                                        if not mydir or mydir[0] != "/":
                                                mydir = None
                                except KeyError:
                                        mydir = None

        if mydir == None:
                error(_("Could not find image.  Use the -R option or set "
                    "$PKG_IMAGE to point\nto an image, or change the working "
                    "directory to one inside the image."))
                return 1

        try:
                img.find_root(mydir)
        except ValueError:
                error(_("'%s' is not an install image") % mydir)
                return 1

        img.load_config()

        try:
                if subcommand == "refresh":
                        return catalog_refresh(img, pargs)
                elif subcommand == "list":
                        return list_inventory(img, pargs)
                elif subcommand == "image-update":
                        return image_update(img, pargs)
                elif subcommand == "install":
                        return install(img, pargs)
                elif subcommand == "uninstall":
                        return uninstall(img, pargs)
                elif subcommand == "freeze":
                        return freeze(img, pargs)
                elif subcommand == "unfreeze":
                        return unfreeze(img, pargs)
                elif subcommand == "search":
                        return search(img, pargs)
                elif subcommand == "info":
                        return info(img, pargs)
                elif subcommand == "contents":
                        return list_contents(img, pargs)
                elif subcommand == "verify":
                        return verify_image(img, pargs)
                elif subcommand == "set-authority":
                        return authority_set(img, pargs)
                elif subcommand == "unset-authority":
                        return authority_unset(img, pargs)
                elif subcommand == "authority":
                        return authority_list(img, pargs)
                elif subcommand == "set-property":
                        return property_set(img, pargs)
                elif subcommand == "unset-property":
                        return property_unset(img, pargs)
                elif subcommand == "property":
                        return property_list(img, pargs)
                elif subcommand == "history":
                        return history_list(img, pargs)
                elif subcommand == "purge-history":
                        ret_code = img.history.purge()
                        if ret_code == 0:
                                msg(_("History purged."))
                        return ret_code
                elif subcommand == "rebuild-index":
                        return rebuild_index(img, pargs)
                else:
                        usage(_("unknown subcommand '%s'") % subcommand)

        except getopt.GetoptError, e:
                usage(_("illegal %s option -- %s") % (subcommand, e.opt))


#
# Establish a specific exit status which means: "python barfed an exception"
# so that we can more easily detect these in testing of the CLI commands.
#
if __name__ == "__main__":
        try:
                __ret = main_func()
        except SystemExit, __e:
                if __img and __img.history.operation_name:
                        __img.history.operation_result = \
                            history.RESULT_FAILED_UNKNOWN
                raise __e
        except (PipeError, KeyboardInterrupt):
                if __img and __img.history.operation_name:
                        __img.history.operation_result = \
                            history.RESULT_CANCELED
                # We don't want to display any messages here to prevent
                # possible further broken pipe (EPIPE) errors.
                __ret = 1
        except misc.TransferTimedOutException:
                if __img and __img.history.operation_name:
                        __img.history.operation_result = \
                            history.RESULT_FAILED_TRANSPORT
                error(_("maximum number of timeouts exceeded during "
                    "download."))
                __ret = 1
        except misc.InvalidContentException, __e:
                if __img and __img.history.operation_name:
                        __img.history.operation_result = \
                            history.RESULT_FAILED_TRANSPORT
                error(_("One or more hosts providing content for this install"
                    "has provided a file with invalid content."))
                error(str(__e))
                __ret = 1
        except:
                if __img and __img.history.operation_name:
                        __img.history.operation_result = \
                            history.RESULT_FAILED_UNKNOWN
                traceback.print_exc()
                error(
                    _("\n\nThis is an internal error.  Please let the " + \
                    "developers know about this\nproblem by filing " + \
                    "a bug at http://defect.opensolaris.org and including " + \
                    "the\nabove traceback and this message.  The version " + \
                    "of pkg(5) is '%s'.") %
                    pkg.VERSION)
                __ret = 99

        sys.exit(__ret)