1 #!/usr/bin/python2.4
   2 #
   3 # CDDL HEADER START
   4 #
   5 # The contents of this file are subject to the terms of the
   6 # Common Development and Distribution License (the "License").
   7 # You may not use this file except in compliance with the License.
   8 #
   9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
  10 # or http://www.opensolaris.org/os/licensing.
  11 # See the License for the specific language governing permissions
  12 # and limitations under the License.
  13 #
  14 # When distributing Covered Code, include this CDDL HEADER in each
  15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
  16 # If applicable, add the following below this CDDL HEADER, with the
  17 # fields enclosed by brackets "[]" replaced with your own identifying
  18 # information: Portions Copyright [yyyy] [name of copyright owner]
  19 #
  20 # CDDL HEADER END
  21 #
  22 # Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
  23 # Use is subject to license terms.
  24 #
  25 # pkg - package system client utility
  26 #
  27 # We use urllib2 for GET and POST operations, but httplib for PUT and DELETE
  28 # operations.
  29 #
  30 # The client is going to maintain an on-disk cache of its state, so that
  31 # startup assembly of the graph is reduced.
  32 #
  33 # Client graph is of the entire local catalog.  As operations progress, package
  34 # states will change.
  35 #
  36 # Deduction operation allows the compilation of the local component of the
  37 # catalog, only if an authoritative repository can identify critical files.
  38 #
  39 # Environment variables
  40 #
  41 # PKG_IMAGE - root path of target image
  42 # PKG_IMAGE_TYPE [entire, partial, user] - type of image
  43 #       XXX or is this in the Image configuration?
  44 
  45 import calendar
  46 import datetime
  47 import errno
  48 import getopt
  49 import gettext
  50 import itertools
  51 import locale
  52 import os
  53 import socket
  54 import sys
  55 import time
  56 import traceback
  57 import urllib2
  58 import urlparse
  59 
  60 import pkg
  61 import pkg.actions as actions
  62 import pkg.client.api as api
  63 import pkg.client.api_errors as api_errors
  64 import pkg.client.bootenv as bootenv
  65 import pkg.client.history as history
  66 import pkg.client.image as image
  67 import pkg.client.imagetypes as imgtypes
  68 import pkg.client.progress as progress
  69 import pkg.client.publisher as publisher
  70 import pkg.fmri as fmri
  71 import pkg.misc as misc
  72 
  73 from pkg.client import global_settings
  74 from pkg.client.debugvalues import DebugValues
  75 from pkg.client.history import (RESULT_CANCELED, RESULT_FAILED_BAD_REQUEST,
  76     RESULT_FAILED_CONFIGURATION, RESULT_FAILED_TRANSPORT, RESULT_FAILED_UNKNOWN,
  77     RESULT_FAILED_OUTOFMEMORY)
  78 from pkg.client.filelist import FileListRetrievalError
  79 from pkg.client.retrieve import (CatalogRetrievalError,
  80     DatastreamRetrievalError, ManifestRetrievalError)
  81 from pkg.misc import EmptyI, msg, emsg, PipeError
  82 
  83 CLIENT_API_VERSION = 15
  84 PKG_CLIENT_NAME = "pkg"
  85 
  86 def error(text, cmd=None):
  87         """Emit an error message prefixed by the command name """
  88 
  89         if cmd:
  90                 text = "%s: %s" % (cmd, text)
  91         else:
  92                 # If we get passed something like an Exception, we can convert
  93                 # it down to a string.
  94                 text = str(text)
  95 
  96         # If the message starts with whitespace, assume that it should come
  97         # *before* the command-name prefix.
  98         text_nows = text.lstrip()
  99         ws = text[:len(text) - len(text_nows)]
 100 
 101         # This has to be a constant value as we can't reliably get our actual
 102         # program name on all platforms.
 103         emsg(ws + "pkg: " + text_nows)
 104 
 105 def usage(usage_error=None, cmd=None, retcode=2):
 106         """Emit a usage message and optionally prefix it with a more
 107             specific error message.  Causes program to exit. """
 108 
 109         if usage_error:
 110                 error(usage_error, cmd=cmd)
 111 
 112         emsg(_("""\
 113 Usage:
 114         pkg [options] command [cmd_options] [operands]
 115 
 116 Basic subcommands:
 117         pkg install [-nvq] [--no-refresh] [--no-index] package...
 118         pkg uninstall [-nrvq] [--no-index] package...
 119         pkg list [-Hafsuv] [--no-refresh] [package...]
 120         pkg image-update [-fnvq] [--be-name name] [--no-refresh] [--no-index]
 121         pkg refresh [--full] [publisher ...]
 122         pkg version
 123 
 124 Advanced subcommands:
 125         pkg info [-lr] [--license] [pkg_fmri_pattern ...]
 126         pkg search [-alprI] [-s server] query
 127         pkg verify [-Hqv] [pkg_fmri_pattern ...]
 128         pkg fix [pkg_fmri_pattern ...]
 129         pkg contents [-Hmr] [-o attribute ...] [-s sort_key]
 130             [-t action_type ... ] [pkg_fmri_pattern ...]
 131         pkg image-create [-fFPUz] [--force] [--full|--partial|--user] [--zone]
 132             [-k ssl_key] [-c ssl_cert] [--no-refresh] 
 133             [--variant <variant_spec>=<instance>]
 134             (-p|--publisher) name=uri dir
 135 
 136         pkg set-property propname propvalue
 137         pkg unset-property propname ...
 138         pkg property [-H] [propname ...]
 139 
 140         pkg set-publisher [-Ped] [-k ssl_key] [-c ssl_cert]
 141             [-O origin_uri] [-m mirror_to_add | --add-mirror=mirror_to_add]
 142             [-M mirror_to_remove | --remove-mirror=mirror_to_remove]
 143             [--enable] [--disable] [--no-refresh] [--reset-uuid] publisher
 144         pkg unset-publisher publisher ...
 145         pkg publisher [-HPa] [publisher ...]
 146         pkg history [-Hl]
 147         pkg purge-history
 148         pkg rebuild-index
 149 
 150 Options:
 151         -R dir
 152         --help or -?
 153 
 154 Environment:
 155         PKG_IMAGE"""))
 156         sys.exit(retcode)
 157 
 158 # XXX Subcommands to implement:
 159 #        pkg image-set name value
 160 #        pkg image-unset name
 161 #        pkg image-get [name ...]
 162 
 163 INCONSISTENT_INDEX_ERROR_MESSAGE = "The search index appears corrupted.  " + \
 164     "Please rebuild the index with 'pkg rebuild-index'."
 165 
 166 PROBLEMATIC_PERMISSIONS_ERROR_MESSAGE = "\n(Failure of consistent use " + \
 167     "of pfexec when executing pkg commands is often a\nsource of this problem.)"
 168 
 169 def check_fmri_args(args):
 170         """ Convenience routine to check that input args are valid fmris. """
 171         ret = True
 172         for x in args:
 173                 try:
 174                         #
 175                         # Pass a bogus build release-- needed to satisfy
 176                         # fmri's checks in the common case that a version but
 177                         # no build release was specified by the user.
 178                         #
 179                         fmri.MatchingPkgFmri(x, build_release="1.0")
 180                 except fmri.IllegalFmri, e:
 181                         error(e)
 182                         ret = False
 183         return ret
 184 
 185 def list_inventory(img, args):
 186         """List packages."""
 187 
 188         all_known = False
 189         all_versions = True
 190         display_headers = True
 191         refresh_catalogs = True
 192         summary = False
 193         upgradable_only = False
 194         verbose = False
 195 
 196         opts, pargs = getopt.getopt(args, "Hafsuv", ["no-refresh"])
 197 
 198         for opt, arg in opts:
 199                 if opt == "-a":
 200                         all_known = True
 201                 elif opt == "-H":
 202                         display_headers = False
 203                 elif opt == "-s":
 204                         summary = True
 205                 elif opt == "-u":
 206                         upgradable_only = True
 207                 elif opt == "-v":
 208                         verbose = True
 209                 elif opt == "-f":
 210                         all_versions = False
 211                 elif opt == "--no-refresh":
 212                         refresh_catalogs = False
 213 
 214         if summary and verbose:
 215                 usage(_("-s and -v may not be combined"))
 216 
 217         if verbose:
 218                 fmt_str = "%-64s %-10s %s"
 219         elif summary:
 220                 fmt_str = "%-30s %s"
 221         else:
 222                 fmt_str = "%-45s %-15s %-10s %s"
 223 
 224         if not check_fmri_args(pargs):
 225                 return 1
 226 
 227         img.history.operation_name = "list"
 228         img.load_catalogs(progress.NullProgressTracker())
 229 
 230         api_inst = api.ImageInterface(img.get_root(), CLIENT_API_VERSION,
 231             get_tracker(quiet=True), None, PKG_CLIENT_NAME)
 232         info_needed = frozenset([api.PackageInfo.SUMMARY])
 233 
 234         seen_one_pkg = False
 235         found = False
 236         try:
 237                 if all_known and refresh_catalogs:
 238                         # If the user requested all known packages, ensure that
 239                         # a publisher metadata refresh is performed if needed
 240                         # since the catalog may be out of date or invalid as
 241                         # a result of publisher information changing (such as
 242                         # an origin uri, etc.).
 243                         tracker = get_tracker(quiet=not display_headers)
 244                         try:
 245                                 img.refresh_publishers(progtrack=tracker)
 246                         except KeyboardInterrupt:
 247                                 raise
 248                         except:
 249                                 # Ignore the above error and just use what
 250                                 # already exists.
 251                                 pass
 252 
 253                 res = misc.get_inventory_list(img, pargs,
 254                     all_known, all_versions)
 255                 prev_pfmri_str = ""
 256                 prev_state = None
 257                 for pfmri, state in res:
 258                         if all_versions and prev_pfmri_str and \
 259                             prev_pfmri_str == pfmri.get_short_fmri() and \
 260                             prev_state == state:
 261                                 continue
 262                         prev_pfmri_str = pfmri.get_short_fmri()
 263                         prev_state = state
 264                         seen_one_pkg = True
 265                         if upgradable_only and not state["upgradable"]:
 266                                 continue
 267 
 268                         if not found:
 269                                 if display_headers:
 270                                         if verbose:
 271                                                 msg(fmt_str % \
 272                                                     ("FMRI", "STATE", "UFIX"))
 273                                         elif summary:
 274                                                 msg(fmt_str % \
 275                                                     ("NAME (PUBLISHER)",
 276                                                     "SUMMARY"))
 277                                         else:
 278                                                 msg(fmt_str % \
 279                                                     ("NAME (PUBLISHER)",
 280                                                     "VERSION", "STATE", "UFIX"))
 281                                 found = True
 282                         ufix = "%c%c%c%c" % \
 283                             (state["upgradable"] and "u" or "-",
 284                             state["frozen"] and "f" or "-",
 285                             state["incorporated"] and "i" or "-",
 286                             state["excludes"] and "x" or "-")
 287 
 288                         if pfmri.preferred_publisher():
 289                                 pub = ""
 290                         else:
 291                                 pub = " (" + pfmri.get_publisher() + ")"
 292 
 293                         if verbose:
 294                                 pf = pfmri.get_fmri(
 295                                     img.get_preferred_publisher())
 296                                 msg("%-64s %-10s %s" % (pf, state["state"],
 297                                     ufix))
 298                         elif summary:
 299                                 pf = pfmri.get_name() + pub
 300 
 301                                 try:
 302                                         ret = api_inst.info([pfmri], False,
 303                                             info_needed)
 304                                         pis = ret[api.ImageInterface.INFO_FOUND]
 305                                 except api_errors.ApiException, e:
 306                                         error(e)
 307                                         return 1
 308 
 309                                 msg(fmt_str % (pf, pis[0].summary)) 
 310 
 311                         else:
 312                                 pf = pfmri.get_name() + pub
 313                                 msg(fmt_str % (pf, pfmri.get_version(),
 314                                     state["state"], ufix))
 315 
 316                 if not found:
 317                         if not seen_one_pkg and not all_known:
 318                                 emsg(_("no packages installed"))
 319                                 img.history.operation_result = \
 320                                     history.RESULT_NOTHING_TO_DO
 321                                 return 1
 322 
 323                         if upgradable_only:
 324                                 if pargs:
 325                                         emsg(_("No specified packages have " \
 326                                             "available updates"))
 327                                 else:
 328                                         emsg(_("No installed packages have " \
 329                                             "available updates"))
 330                                 img.history.operation_result = \
 331                                     history.RESULT_NOTHING_TO_DO
 332                                 return 1
 333 
 334                         img.history.operation_result = \
 335                             history.RESULT_NOTHING_TO_DO
 336                         return 1
 337 
 338                 img.history.operation_result = history.RESULT_SUCCEEDED
 339                 return 0
 340 
 341         except api_errors.InventoryException, e:
 342                 if e.illegal:
 343                         for i in e.illegal:
 344                                 error(i)
 345                         img.history.operation_result = \
 346                             history.RESULT_FAILED_BAD_REQUEST
 347                         return 1
 348 
 349                 if all_known:
 350                         state = image.PKG_STATE_KNOWN
 351                 else:
 352                         state = image.PKG_STATE_INSTALLED
 353                 for pat in e.notfound:
 354                         error(_("no packages matching "
 355                             "'%(pattern)s' %(state)s") %
 356                             { "pattern": pat, "state": state })
 357                 img.history.operation_result = history.RESULT_NOTHING_TO_DO
 358                 return 1
 359 
 360 def get_tracker(quiet=False):
 361         if quiet:
 362                 progresstracker = progress.QuietProgressTracker()
 363         else:
 364                 try:
 365                         progresstracker = \
 366                             progress.FancyUNIXProgressTracker()
 367                 except progress.ProgressTrackerException:
 368                         progresstracker = progress.CommandLineProgressTracker()
 369         return progresstracker
 370 
 371 def fix_image(img, args):
 372         progresstracker = get_tracker(False)
 373         img.load_catalogs(progresstracker)
 374         fmris, notfound, illegals = img.installed_fmris_from_args(args)
 375 
 376         any_errors = False
 377         repairs = []
 378         for f in fmris:
 379                 failed_actions = []
 380                 for err in img.verify(f, progresstracker,
 381                     verbose=True, forever=True):
 382                         if not failed_actions:
 383                                 msg("Verifying: %-50s %7s" %
 384                                     (f.get_pkg_stem(), "ERROR"))
 385                         act = err[0]
 386                         failed_actions.append(act)
 387                         msg("\t%s" % act.distinguished_name())
 388                         for x in err[1]:
 389                                 msg("\t\t%s" % x)
 390                 if failed_actions:
 391                         repairs.append((f, failed_actions))
 392 
 393         # Repair anything we failed to verify
 394         if repairs:
 395                 # Create a snapshot in case they want to roll back
 396                 try:
 397                         be = bootenv.BootEnv(img.get_root())
 398                         if be.exists():
 399                                 msg(_("Created ZFS snapshot: %s" % 
 400                                     be.snapshot_name))
 401                 except RuntimeError:
 402                         pass # Error is printed by the BootEnv call.
 403                 success = img.repair(repairs, progresstracker)
 404                 if not success:
 405                         progresstracker.verify_done()
 406                         return 1
 407         progresstracker.verify_done()
 408         return 0
 409 
 410 def verify_image(img, args):
 411         opts, pargs = getopt.getopt(args, "vfqH")
 412 
 413         quiet = verbose = False
 414         # for now, always check contents of files
 415         forever = display_headers = True
 416 
 417         for opt, arg in opts:
 418                 if opt == "-H":
 419                         display_headers = False
 420                 if opt == "-v":
 421                         verbose = True
 422                 elif opt == "-f":
 423                         forever = True
 424                 elif opt == "-q":
 425                         quiet = True
 426                         display_headers = False
 427 
 428         if verbose and quiet:
 429                 usage(_("verify: -v and -q may not be combined"))
 430 
 431         progresstracker = get_tracker(quiet)
 432 
 433         if not check_fmri_args(pargs):
 434                 return 1
 435 
 436         img.load_catalogs(progresstracker)
 437 
 438         fmris, notfound, illegals = img.installed_fmris_from_args(pargs)
 439 
 440         if illegals:
 441                 for i in illegals:
 442                         emsg(str(i))
 443                 return 1
 444 
 445         any_errors = False
 446 
 447         header = False
 448         for f in fmris:
 449                 pkgerr = False
 450                 for err in img.verify(f, progresstracker,
 451                     verbose=verbose, forever=forever):
 452                         #
 453                         # Eventually this code should probably
 454                         # move into the progresstracker
 455                         #
 456                         if not pkgerr:
 457                                 if display_headers and not header:
 458                                         msg("%-50s %7s" % ("PACKAGE", "STATUS"))
 459                                         header = True
 460 
 461                                 if not quiet:
 462                                         msg("%-50s %7s" % (f.get_pkg_stem(),
 463                                             "ERROR"))
 464                                 pkgerr = True
 465 
 466                         if not quiet:
 467                                 msg("\t%s" % err[0].distinguished_name())
 468                                 for x in err[1]:
 469                                         msg("\t\t%s" % x)
 470                 if verbose and not pkgerr:
 471                         if display_headers and not header:
 472                                 msg("%-50s %7s" % ("PACKAGE", "STATUS"))
 473                                 header = True
 474                         msg("%-50s %7s" % (f.get_pkg_stem(), "OK"))
 475 
 476                 any_errors = any_errors or pkgerr
 477 
 478         if fmris:
 479                 progresstracker.verify_done()
 480 
 481         if notfound:
 482                 if fmris:
 483                         emsg()
 484                 emsg(_("""\
 485 pkg: no packages matching the following patterns you specified are
 486 installed on the system.\n"""))
 487                 for p in notfound:
 488                         emsg("        %s" % p)
 489                 if fmris:
 490                         if any_errors:
 491                                 msg2 = "See above for\nverification failures."
 492                         else:
 493                                 msg2 = "No packages failed\nverification."
 494                         emsg(_("\nAll other patterns matched installed "
 495                             "packages.  %s" % msg2))
 496                 any_errors = True
 497 
 498         if any_errors:
 499                 return 1
 500         return 0
 501 
 502 def image_update(img_dir, args):
 503         """Attempt to take all installed packages specified to latest
 504         version."""
 505 
 506         # XXX Publisher-catalog issues.
 507         # XXX Are filters appropriate for an image update?
 508         # XXX Leaf package refinements.
 509 
 510         opts, pargs = getopt.getopt(args, "fnvq", ["be-name=", "no-refresh",
 511             "no-index"])
 512 
 513         force = quiet = noexecute = verbose = False
 514         refresh_catalogs = update_index = True
 515         be_name = None
 516         for opt, arg in opts:
 517                 if opt == "-n":
 518                         noexecute = True
 519                 elif opt == "-v":
 520                         verbose = True
 521                 elif opt == "-q":
 522                         quiet = True
 523                 elif opt == "-f":
 524                         force = True
 525                 elif opt == "--no-refresh":
 526                         refresh_catalogs = False
 527                 elif opt == "--no-index":
 528                         update_index = False
 529                 elif opt == "--be-name":
 530                         be_name = arg
 531 
 532         if verbose and quiet:
 533                 usage(_("image-update: -v and -q may not be combined"))
 534 
 535         if pargs:
 536                 usage(_("image-update: command does not take operands " \
 537                     "('%s')") % " ".join(pargs))
 538 
 539         progresstracker = get_tracker(quiet)
 540 
 541         try:
 542                 api_inst = api.ImageInterface(img_dir, CLIENT_API_VERSION,
 543                     progresstracker, cancel_state_callable=None,
 544                     pkg_client_name=PKG_CLIENT_NAME)
 545         except api_errors.ImageNotFoundException, e:
 546                 error(_("No image rooted at '%s'") % e.user_dir)
 547                 return 1
 548 
 549         try:
 550                 # cre is either None or a catalog refresh exception which was
 551                 # caught while planning.
 552                 stuff_to_do, opensolaris_image, cre = \
 553                     api_inst.plan_update_all(sys.argv[0], refresh_catalogs,
 554                         noexecute, force=force, verbose=verbose,
 555                         update_index=update_index, be_name=be_name)
 556                 if cre and not display_catalog_failures(cre):
 557                         raise RuntimeError("Catalog refresh failed during"
 558                             " image-update.")
 559                 if not stuff_to_do:
 560                         msg(_("No updates available for this image."))
 561                         return 0
 562         except api_errors.InventoryException, e:
 563                 error(_("image-update failed (inventory exception):\n%s") % e)
 564                 return 1
 565         except api_errors.CatalogRefreshException, e:
 566                 if display_catalog_failures(e) == 0:
 567                         if not noexecute:
 568                                 return 1
 569                 else:
 570                         raise RuntimeError("Catalog refresh failed during"
 571                             " image-update.")
 572         except api_errors.BEException, e:
 573                 error(_(e))
 574                 return 1
 575         except (api_errors.CertificateError,
 576             api_errors.PlanCreationException,
 577             api_errors.NetworkUnavailableException,
 578             api_errors.PermissionsException), e:
 579                 # Prepend a newline because otherwise the exception will
 580                 # be printed on the same line as the spinner.
 581                 error("\n" + str(e))
 582                 return 1
 583         except api_errors.IpkgOutOfDateException:
 584                 msg(_("WARNING: pkg(5) appears to be out of date, and should " \
 585                     "be updated before\nrunning image-update.\n"))
 586                 msg(_("Please update pkg(5) using 'pfexec pkg install " \
 587                     "SUNWipkg' and then retry\nthe image-update."))
 588                 return 1
 589         except api_errors.ImageNotFoundException, e:
 590                 error(_("No image rooted at '%s'") % e.user_dir)
 591                 return 1
 592         if noexecute:
 593                 return 0
 594 
 595         ret_code = 0
 596 
 597         # Exceptions which happen here are printed in the above level, with
 598         # or without some extra decoration done here.
 599         # XXX would be nice to kick the progress tracker.
 600         try:
 601                 api_inst.prepare()
 602         except misc.TransportException:
 603                 # move past the progress tracker line.
 604                 msg("\n")
 605                 raise
 606         except KeyboardInterrupt:
 607                 raise
 608         except api_errors.PermissionsException, e:
 609                 # Prepend a newline because otherwise the exception will
 610                 # be printed on the same line as the spinner.
 611                 error("\n" + str(e))
 612                 return 1
 613         except:
 614                 error(_("\nAn unexpected error happened while preparing for " \
 615                     "image-update:"))
 616                 raise
 617 
 618         try:
 619                 api_inst.execute_plan()
 620         except RuntimeError, e:
 621                 error(_("image-update failed: %s") % e)
 622                 ret_code = 1
 623         except api_errors.ImageUpdateOnLiveImageException:
 624                 error(_("image-update cannot be done on live image"))
 625                 ret_code = 1
 626         except api_errors.CorruptedIndexException, e:
 627                 error(INCONSISTENT_INDEX_ERROR_MESSAGE)
 628                 ret_code = 1
 629         except api_errors.ProblematicPermissionsIndexException, e:
 630                 error(str(e) + PROBLEMATIC_PERMISSIONS_ERROR_MESSAGE)
 631                 ret_code = 1
 632         except api_errors.PermissionsException, e:
 633                 # Prepend a newline because otherwise the exception will
 634                 # be printed on the same line as the spinner.
 635                 error("\n" + str(e))
 636                 ret_code = 1
 637         except api_errors.MainDictParsingException, e:
 638                 error(str(e))
 639                 ret_code = 1
 640         except api_errors.BEException, e:
 641                 error(_(e))
 642                 return 1
 643         except KeyboardInterrupt:
 644                 raise
 645         except Exception, e:
 646                 error(_("\nAn unexpected error happened during " \
 647                     "image-update: %s") % e)
 648                 raise
 649 
 650         if ret_code == 0 and opensolaris_image:
 651                 msg("\n" + "-" * 75)
 652                 msg(_("NOTE: Please review release notes posted at:\n" ))
 653                 msg(misc.get_release_notes_url())
 654                 msg("-" * 75 + "\n")
 655 
 656         if bool(os.environ.get("PKG_MIRROR_STATS", False)):
 657                 print_mirror_stats(api_inst)
 658 
 659         return ret_code
 660 
 661 def print_mirror_stats(api_inst):
 662         """Given an api_inst object, print depot status information."""
 663 
 664         status_fmt = "%-10s %-35s %10s %10s"
 665         print status_fmt % ("Publisher", "URI", "Success", "Failure")
 666 
 667         for ds in api_inst.img.gen_depot_status():
 668                 print status_fmt % (ds.prefix, ds.url, ds.good_tx, ds.errors)
 669 
 670 def install(img_dir, args):
 671         """Attempt to take package specified to INSTALLED state.  The operands
 672         are interpreted as glob patterns."""
 673 
 674         # XXX Publisher-catalog issues.
 675 
 676         opts, pargs = getopt.getopt(args, "nvf:q", ["no-refresh", "no-index"])
 677 
 678         quiet = noexecute = verbose = False
 679         refresh_catalogs = update_index = True
 680         filters = []
 681         for opt, arg in opts:
 682                 if opt == "-n":
 683                         noexecute = True
 684                 elif opt == "-v":
 685                         verbose = True
 686                 elif opt == "-f":
 687                         filters += [ arg ]
 688                 elif opt == "-q":
 689                         quiet = True
 690                 elif opt == "--no-refresh":
 691                         refresh_catalogs = False
 692                 elif opt == "--no-index":
 693                         update_index = False
 694 
 695         if not pargs:
 696                 usage(_("install: at least one package name required"))
 697 
 698         if verbose and quiet:
 699                 usage(_("install: -v and -q may not be combined"))
 700 
 701         progresstracker = get_tracker(quiet)
 702 
 703         if not check_fmri_args(pargs):
 704                 return 1
 705 
 706         # XXX not sure where this should live
 707         pkg_list = [ pat.replace("*", ".*").replace("?", ".")
 708             for pat in pargs ]
 709 
 710         try:
 711                 api_inst = api.ImageInterface(img_dir, CLIENT_API_VERSION,
 712                     progresstracker, None, PKG_CLIENT_NAME)
 713         except api_errors.ImageNotFoundException, e:
 714                 error(_("'%s' is not an install image") % e.user_dir)
 715                 return 1
 716 
 717         try:
 718                 # cre is either None or a catalog refresh exception which was
 719                 # caught while planning.
 720                 stuff_to_do, cre = api_inst.plan_install(pkg_list, filters,
 721                     refresh_catalogs, noexecute, verbose=verbose,
 722                     update_index=update_index)
 723                 if cre and not display_catalog_failures(cre):
 724                         raise RuntimeError("Catalog refresh failed during"
 725                             " install.")
 726                 if not stuff_to_do:
 727                         msg(_("No updates available for this image."))
 728                         return 0
 729         except api_errors.CatalogRefreshException, e:
 730                 if display_catalog_failures(e) == 0:
 731                         if not noexecute:
 732                                 return 1
 733                 else:
 734                         error(_("Catalog refresh failed during install."),
 735                             cmd="install")
 736                         return 1
 737         except (api_errors.CertificateError,
 738             api_errors.PlanCreationException,
 739             api_errors.NetworkUnavailableException,
 740             api_errors.PermissionsException), e:
 741                 # Prepend a newline because otherwise the exception will
 742                 # be printed on the same line as the spinner.
 743                 error("\n" + str(e), cmd="install")
 744                 return 1
 745         except api_errors.InventoryException, e:
 746                 error(_("install failed (inventory exception):\n%s") % e,
 747                     cmd="install")
 748                 return 1
 749         except fmri.IllegalFmri, e:
 750                 error(e, cmd="install")
 751                 return 1
 752 
 753         if noexecute:
 754                 return 0
 755 
 756         # Exceptions which happen here are printed in the above level, with
 757         # or without some extra decoration done here.
 758         # XXX would be nice to kick the progress tracker.
 759         try:
 760                 api_inst.prepare()
 761         except misc.TransportException:
 762                 # move past the progress tracker line.
 763                 msg("\n")
 764                 raise
 765         except KeyboardInterrupt:
 766                 raise
 767         except api_errors.PermissionsException, e:
 768                 # Prepend a newline because otherwise the exception will
 769                 # be printed on the same line as the spinner.
 770                 error("\n" + str(e))
 771                 return 1
 772         except:
 773                 error(_("\nAn unexpected error happened while preparing for " \
 774                     "install:"))
 775                 raise
 776 
 777         ret_code = 0
 778 
 779         try:
 780                 api_inst.execute_plan()
 781         except RuntimeError, e:
 782                 error(_("installation failed: %s") % e)
 783                 ret_code = 1
 784         except api_errors.CorruptedIndexException, e:
 785                 error(INCONSISTENT_INDEX_ERROR_MESSAGE)
 786                 ret_code = 1
 787         except api_errors.ProblematicPermissionsIndexException, e:
 788                 error(str(e) + PROBLEMATIC_PERMISSIONS_ERROR_MESSAGE)
 789                 ret_code = 1
 790         except api_errors.PermissionsException, e:
 791                 # Prepend a newline because otherwise the exception will
 792                 # be printed on the same line as the spinner.
 793                 error("\n" + str(e))
 794                 ret_code = 1
 795         except api_errors.MainDictParsingException, e:
 796                 error(str(e))
 797                 ret_code = 1
 798         except KeyboardInterrupt:
 799                 raise
 800         except api_errors.ActionExecutionError:
 801                 ret_code = 1
 802         except Exception, e:
 803                 error(_("An unexpected error happened during " \
 804                     "installation: %s") % e)
 805                 raise
 806 
 807         if bool(os.environ.get("PKG_MIRROR_STATS", False)):
 808                 print_mirror_stats(api_inst)
 809 
 810         return ret_code
 811 
 812 
 813 def uninstall(img_dir, args):
 814         """Attempt to take package specified to DELETED state."""
 815 
 816         opts, pargs = getopt.getopt(args, "nrvq", ["no-index"])
 817 
 818         quiet = noexecute = recursive_removal = verbose = False
 819         update_index = True
 820         for opt, arg in opts:
 821                 if opt == "-n":
 822                         noexecute = True
 823                 elif opt == "-r":
 824                         recursive_removal = True
 825                 elif opt == "-v":
 826                         verbose = True
 827                 elif opt == "-q":
 828                         quiet = True
 829                 elif opt == "--no-index":
 830                         update_index = False
 831 
 832         if not pargs:
 833                 usage(_("uninstall: at least one package name required"))
 834 
 835         if verbose and quiet:
 836                 usage(_("uninstall: -v and -q may not be combined"))
 837 
 838         progresstracker = get_tracker(quiet)
 839 
 840         if not check_fmri_args(pargs):
 841                 return 1
 842 
 843         # XXX not sure where this should live
 844         pkg_list = [ pat.replace("*", ".*").replace("?", ".")
 845             for pat in pargs ]
 846 
 847         try:
 848                 api_inst = api.ImageInterface(img_dir, CLIENT_API_VERSION,
 849                     progresstracker, None, PKG_CLIENT_NAME)
 850         except api_errors.ImageNotFoundException, e:
 851                 error(_("'%s' is not an install image") % e.user_dir)
 852                 return 1
 853 
 854         try:
 855                 if not api_inst.plan_uninstall(pkg_list, recursive_removal,
 856                     noexecute, verbose=verbose, update_index=update_index):
 857                         assert 0
 858         except api_errors.InventoryException, e:
 859                 error(_("uninstall failed (inventory exception):\n%s") % e)
 860                 return 1
 861         except api_errors.NonLeafPackageException, e:
 862                 error("""Cannot remove '%s' due to
 863 the following packages that depend on it:""" % e[0])
 864                 for d in e[1]:
 865                         emsg("  %s" % d)
 866                 return 1
 867         except (api_errors.PlanCreationException,
 868             api_errors.PermissionsException), e:
 869                 # Prepend a newline because otherwise the exception will
 870                 # be printed on the same line as the spinner.
 871                 error("\n" + str(e))
 872                 return 1
 873 
 874         if noexecute:
 875                 return 0
 876 
 877         # Exceptions which happen here are printed in the above level, with
 878         # or without some extra decoration done here.
 879         # XXX would be nice to kick the progress tracker.
 880         try:
 881                 api_inst.prepare()
 882         except misc.TransportException:
 883                 # move past the progress tracker line.
 884                 msg("\n")
 885                 raise
 886         except api_errors.FileInUseException, e:
 887                 error("\n" + str(e))
 888                 return 1
 889         except KeyboardInterrupt:
 890                 raise
 891         except:
 892                 error(_("\nAn unexpected error happened while preparing for " \
 893                     "install:"))
 894                 raise
 895 
 896         ret_code = 0
 897 
 898         try:
 899                 api_inst.execute_plan()
 900         except RuntimeError, e:
 901                 error(_("uninstallation failed: %s") % e)
 902                 ret_code = 1
 903         except api_errors.CorruptedIndexException, e:
 904                 error(INCONSISTENT_INDEX_ERROR_MESSAGE)
 905                 ret_code = 1
 906         except api_errors.ProblematicPermissionsIndexException, e:
 907                 error(str(e) + PROBLEMATIC_PERMISSIONS_ERROR_MESSAGE)
 908                 ret_code = 1
 909         except api_errors.PermissionsException, e:
 910                 # Prepend a newline because otherwise the exception will
 911                 # be printed on the same line as the spinner.
 912                 error("\n" + str(e))
 913                 ret_code = 1
 914         except api_errors.MainDictParsingException, e:
 915                 error(str(e))
 916                 ret_code = 1
 917         except KeyboardInterrupt:
 918                 raise
 919         except Exception, e:
 920                 error(_("An unexpected error happened during " \
 921                     "uninstallation: %s") % e)
 922                 raise
 923 
 924         return ret_code
 925 
 926 def freeze(img, args):
 927         """Attempt to take package specified to FROZEN state, with given
 928         restrictions.  Package must have been in the INSTALLED state."""
 929         return 0
 930 
 931 def unfreeze(img, args):
 932         """Attempt to return package specified to INSTALLED state from FROZEN
 933         state."""
 934         return 0
 935 
 936 def process_v_0_search(tup, first):
 937         """Transforms the tuples returned by search v1 into the four column
 938         output format.
 939 
 940         The "tup" parameter is a four tuple with the each entry corresponding
 941         to a column of the output.
 942         
 943         The "first" parameter is a boolean stating whether this is the first
 944         time this function has been called.  This controls the printing of the
 945         header information."""
 946 
 947         try:
 948                 index, mfmri, action, value = tup
 949         except ValueError:
 950                 error(_("The server returned a malformed result.\n"
 951                     "The problematic structure: %r") % (tup,))
 952                 return False
 953         if first:
 954                 msg("%-10s %-9s %-25s %s" %
 955                     ("INDEX", "ACTION", "VALUE", "PACKAGE"))
 956         msg("%-10s %-9s %-25s %s" % (index, action, value,
 957             fmri.PkgFmri(str(mfmri)).get_short_fmri()))
 958         return True
 959 
 960 def __convert_output(a_str, match):
 961         """Converts a string to a three tuple with the information to fill
 962         the INDEX, ACTION, and VALUE columns.
 963 
 964         The "a_str" parameter is the string representation of an action.
 965         
 966         The "match" parameter is a string whose precise interpretation is given
 967         below.
 968 
 969         For most action types, match defines which attribute the query matched
 970         with.  For example, it states whether the basename or path attribute of
 971         a file action matched the query.  Attribute (set) actions are treated
 972         differently because they only have one attribute, and many values
 973         associated with that attribute.  For those actions, the match parameter
 974         states which value matched the query."""
 975         
 976         a = actions.fromstr(a_str.rstrip())
 977         if isinstance(a, actions.attribute.AttributeAction):
 978                 return a.attrs.get(a.key_attr), a.name, match
 979         return match, a.name, a.attrs.get(a.key_attr)
 980 
 981 def process_v_1_search(tup, first, return_type, pub):
 982         """Transforms the tuples returned by search v1 into the four column
 983         output format.
 984 
 985         The "first" parameter is a boolean stating whether this is the first
 986         time this function has been called.  This controls the printing of the
 987         header information.
 988 
 989         The "return_type" parameter is an enumeration that describes the type
 990         of the information that will be converted.
 991 
 992         The type of the "tup" parameter depends on the value of "return_type".
 993         If "return_type" is action information, "tup" is a three-tuple of the fmri
 994         name, the match, and a string representation of the action.  In the case
 995         where "return_type" is package information, "tup" is a one-tuple containing
 996         the fmri name."""
 997 
 998         if return_type == api.Query.RETURN_ACTIONS:
 999                 try:
1000                         pfmri, match, action = tup
1001                 except ValueError:
1002                         error(_("The server returned a malformed result.\n"
1003                             "The problematic structure:%r") % (tup,))
1004                         return False
1005                 if first:
1006                         msg("%-10s %-9s %-25s %s" %
1007                             ("INDEX", "ACTION", "VALUE", "PACKAGE"))
1008                 try:
1009                         out1, out2, out3 = __convert_output(action, match)
1010                 except (actions.UnknownActionError,
1011                     actions.MalformedActionError), e:
1012                         error(_("The server returned a malformed action.\n%s") %
1013                             e)
1014                         return False
1015                 msg("%-10s %-9s %-25s %s" %
1016                     (out1, out2, out3,
1017                     fmri.PkgFmri(str(pfmri)).get_short_fmri()))
1018         else:
1019                 pfmri = tup
1020                 if first:
1021                         msg("%s" % ("PACKAGE"))
1022                 pub_name = ''
1023                 if pub is not None and "prefix" in pub:
1024                         pub_name = " (%s)" % pub.prefix
1025                 msg("%s%s" %
1026                     (fmri.PkgFmri(str(pfmri)).get_short_fmri(), pub_name))
1027         return True
1028 
1029 def search(img_dir, args):
1030         """Search for the given query."""
1031 
1032         opts, pargs = getopt.getopt(args, "alprs:I")
1033 
1034         local = remote = case_sensitive = False
1035         servers = []
1036         return_actions = True
1037         for opt, arg in opts:
1038                 if opt == "-a":
1039                         return_actions = True
1040                 elif opt == "-l":
1041                         local = True
1042                 elif opt == "-p":
1043                         return_actions = False
1044                 elif opt == "-r":
1045                         remote = True
1046                 elif opt == "-s":
1047                         if not misc.valid_pub_url(arg):
1048                                 orig_arg = arg
1049                                 arg = "http://" + arg
1050                                 if not misc.valid_pub_url(arg):
1051                                         error(_("%s is not a valid "
1052                                             "server URL.") % orig_arg)
1053                                         return 1
1054                         remote = True
1055                         servers.append({"origin": arg})
1056                 elif opt == "-I":
1057                         case_sensitive = True
1058 
1059         if not local and not remote:
1060                 remote = True
1061 
1062         if not pargs:
1063                 usage()
1064 
1065         searches = []
1066 
1067         try:
1068                 api_inst = api.ImageInterface(img_dir, CLIENT_API_VERSION,
1069                     get_tracker(), None, PKG_CLIENT_NAME)
1070         except api_errors.ImageNotFoundException, e:
1071                 error(_("'%s' is not an install image") % e.user_dir)
1072                 return 1
1073         try:
1074                 query = [api.Query(" ".join(pargs), case_sensitive,
1075                     return_actions)]
1076         except api_errors.BooleanQueryException, e:
1077                 error(e)
1078                 return 1
1079 
1080         first = True
1081         good_res = False
1082         bad_res = False
1083         
1084         try:
1085                 if local:
1086                         searches.append(api_inst.local_search(query))
1087                 if remote:
1088                         searches.append(api_inst.remote_search(query,
1089                             servers=servers))
1090 
1091                 # By default assume we don't find anything.
1092                 retcode = 1
1093 
1094                 for raw_value in itertools.chain(*searches):
1095                         try:
1096                                 query_num, pub, (v, return_type, tmp) = \
1097                                     raw_value
1098                         except ValueError, e:
1099                                 error(_("The server returned a malformed "
1100                                     "result:%r") % (raw_value,))
1101                                 bad_res = True
1102                                 continue
1103                         if v == 0:
1104                                 ret = process_v_0_search(tmp, first)
1105                         else:
1106                                 ret = process_v_1_search(tmp, first,
1107                                     return_type, pub)
1108                         good_res |= ret
1109                         bad_res |= not ret
1110                         first = False
1111         except (api_errors.IncorrectIndexFileHash,
1112             api_errors.InconsistentIndexException):
1113                 error(_("The search index appears corrupted.  Please "
1114                     "rebuild the index with 'pkg rebuild-index'."))
1115                 return 1
1116         except api_errors.ProblematicSearchServers, e:
1117                 error(e)
1118                 bad_res = True
1119         except api_errors.SlowSearchUsed, e:
1120                 error(e)
1121         except (api_errors.IncorrectIndexFileHash,
1122             api_errors.InconsistentIndexException):
1123                 error(_("The search index appears corrupted.  Please "
1124                     "rebuild the index with 'pkg rebuild-index'."))
1125                 return 1
1126         except api_errors.ApiException, e:
1127                 error(e)
1128                 return 1
1129         if good_res and bad_res:
1130                 retcode = 4
1131         elif bad_res:
1132                 retcode = 1
1133         elif not first:
1134                 retcode = 0      
1135         return retcode
1136 
1137 def info(img_dir, args):
1138         """Display information about a package or packages.
1139         """
1140 
1141         display_license = False
1142         info_local = False
1143         info_remote = False
1144 
1145         opts, pargs = getopt.getopt(args, "lr", ["license"])
1146         for opt, arg in opts:
1147                 if opt == "-l":
1148                         info_local = True
1149                 elif opt == "-r":
1150                         info_remote = True
1151                 elif opt == "--license":
1152                         display_license = True
1153 
1154         if not info_local and not info_remote:
1155                 info_local = True
1156         elif info_local and info_remote:
1157                 usage(_("info: -l and -r may not be combined"))
1158 
1159         if info_remote and not pargs:
1160                 usage(_("info: must request remote info for specific packages"))
1161 
1162         if not check_fmri_args(pargs):
1163                 return 1
1164 
1165         err = 0
1166 
1167         api_inst = api.ImageInterface(img_dir, CLIENT_API_VERSION,
1168             get_tracker(quiet=True), None, PKG_CLIENT_NAME)
1169 
1170         try:
1171                 info_needed = api.PackageInfo.ALL_OPTIONS
1172                 if not display_license:
1173                         info_needed = api.PackageInfo.ALL_OPTIONS - \
1174                             frozenset([api.PackageInfo.LICENSES])
1175                 try:
1176                         ret = api_inst.info(pargs, info_local, info_needed)
1177                 except api_errors.UnrecognizedOptionsToInfo, e:
1178                         error(e)
1179                         return 1
1180                 pis = ret[api.ImageInterface.INFO_FOUND]
1181                 notfound = ret[api.ImageInterface.INFO_MISSING]
1182                 illegals = ret[api.ImageInterface.INFO_ILLEGALS]
1183                 multi_match = ret[api.ImageInterface.INFO_MULTI_MATCH]
1184 
1185         except api_errors.PermissionsException, e:
1186                 error(e)
1187                 return 1
1188         except api_errors.NoPackagesInstalledException:
1189                 error(_("no packages installed"))
1190                 return 1
1191 
1192         no_licenses = []
1193         for i, pi in enumerate(pis):
1194                 if i > 0:
1195                         msg("")
1196 
1197                 if display_license:
1198                         if not pi.licenses:
1199                                 no_licenses.append(pi.fmri)
1200                         else:
1201                                 for lic in pi.licenses:
1202                                         msg(lic)
1203                         continue
1204 
1205                 if pi.state == api.PackageInfo.INSTALLED:
1206                         state = _("Installed")
1207                 elif pi.state == api.PackageInfo.NOT_INSTALLED:
1208                         state = _("Not installed")
1209                 else:
1210                         raise RuntimeError("Encountered unknown package "
1211                             "information state: %d" % pi.state )
1212                 name_str = _("          Name:")
1213                 msg(name_str, pi.pkg_stem)
1214                 msg(_("       Summary:"), pi.summary)
1215                 if pi.category_info_list:
1216                         verbose = len(pi.category_info_list) > 1
1217                         msg(_("      Category:"),
1218                             pi.category_info_list[0].__str__(verbose))
1219                         if len(pi.category_info_list) > 1:
1220                                 for ci in pi.category_info_list[1:]:
1221                                         msg(" " * len(name_str),
1222                                             ci.__str__(verbose))
1223 
1224                 msg(_("         State:"), state)
1225 
1226                 # XXX even more info on the publisher would be nice?
1227                 msg(_("     Publisher:"), pi.publisher)
1228                 msg(_("       Version:"), pi.version)
1229                 msg(_(" Build Release:"), pi.build_release)
1230                 msg(_("        Branch:"), pi.branch)
1231                 msg(_("Packaging Date:"), pi.packaging_date)
1232                 msg(_("          Size:"), misc.bytes_to_str(pi.size))
1233                 msg(_("          FMRI:"), pi.fmri)
1234                 # XXX add license/copyright info here?
1235 
1236         if notfound:
1237                 err = 1
1238                 if pis:
1239                         emsg()
1240                 if info_local:
1241                         emsg(_("""\
1242 pkg: no packages matching the following patterns you specified are
1243 installed on the system.  Try specifying -r to query remotely:"""))
1244                 elif info_remote:
1245                         emsg(_("""\
1246 pkg: no packages matching the following patterns you specified were
1247 found in the catalog.  Try relaxing the patterns, refreshing, and/or
1248 examining the catalogs:"""))
1249                 emsg()
1250                 for p in notfound:
1251                         emsg("        %s" % p)
1252 
1253         if illegals:
1254                 err = 1
1255                 for i in illegals:
1256                         emsg(str(i))
1257 
1258         if multi_match:
1259                 err = 1
1260                 for pfmri, matches in multi_match:
1261                         error(_("'%s' matches multiple packages") % pfmri)
1262                         for k in matches:
1263                                 emsg("\t%s" % k)
1264 
1265         if no_licenses:
1266                 err = 1
1267                 error(_("no license information could be found for the "
1268                     "following packages:"))
1269                 for pfmri in no_licenses:
1270                         emsg("\t%s" % pfmri)
1271 
1272         return err
1273 
1274 def display_contents_results(actionlist, attrs, sort_attrs, action_types,
1275     display_headers):
1276         """Print results of a "list" operation """
1277 
1278         # widths is a list of tuples of column width and justification.  Start
1279         # with the widths of the column headers.
1280         JUST_UNKN = 0
1281         JUST_LEFT = -1
1282         JUST_RIGHT = 1
1283         widths = [ (len(attr) - attr.find(".") - 1, JUST_UNKN)
1284             for attr in attrs ]
1285         lines = []
1286 
1287         for manifest, action in actionlist:
1288                 if action_types and action.name not in action_types:
1289                         continue
1290                 line = []
1291                 for i, attr in enumerate(attrs):
1292                         just = JUST_UNKN
1293                         # As a first approximation, numeric attributes
1294                         # are right justified, non-numerics left.
1295                         try:
1296                                 int(action.attrs[attr])
1297                                 just = JUST_RIGHT
1298                         # attribute is non-numeric or is something like
1299                         # a list.
1300                         except (ValueError, TypeError):
1301                                 just = JUST_LEFT
1302                         # attribute isn't in the list, so we don't know
1303                         # what it might be
1304                         except KeyError:
1305                                 pass
1306 
1307                         if attr in action.attrs:
1308                                 a = action.attrs[attr]
1309                         elif attr == "action.name":
1310                                 a = action.name
1311                                 just = JUST_LEFT
1312                         elif attr == "action.key":
1313                                 a = action.attrs[action.key_attr]
1314                                 just = JUST_LEFT
1315                         elif attr == "action.raw":
1316                                 a = action
1317                                 just = JUST_LEFT
1318                         elif attr == "pkg.name":
1319                                 a = manifest.fmri.get_name()
1320                                 just = JUST_LEFT
1321                         elif attr == "pkg.fmri":
1322                                 a = manifest.fmri
1323                                 just = JUST_LEFT
1324                         elif attr == "pkg.shortfmri":
1325                                 a = manifest.fmri.get_short_fmri()
1326                                 just = JUST_LEFT
1327                         elif attr == "pkg.publisher":
1328                                 a = manifest.fmri.get_publisher()
1329                                 just = JUST_LEFT
1330                         else:
1331                                 a = ""
1332 
1333                         line.append(a)
1334 
1335                         # XXX What to do when a column's justification
1336                         # changes?
1337                         if just != JUST_UNKN:
1338                                 widths[i] = \
1339                                     (max(widths[i][0], len(str(a))), just)
1340 
1341                 if line and [l for l in line if str(l) != ""]:
1342                         lines.append(line)
1343 
1344         sortidx = 0
1345         for i, attr in enumerate(attrs):
1346                 if attr == sort_attrs[0]:
1347                         sortidx = i
1348                         break
1349 
1350         # Sort numeric columns numerically.
1351         if widths[sortidx][1] == JUST_RIGHT:
1352                 def key_extract(x):
1353                         try:
1354                                 return int(x[sortidx])
1355                         except (ValueError, TypeError):
1356                                 return 0
1357         else:
1358                 key_extract = lambda x: x[sortidx]
1359 
1360         if display_headers:
1361                 headers = []
1362                 for i, attr in enumerate(attrs):
1363                         headers.append(str(attr.upper()))
1364                         widths[i] = \
1365                             (max(widths[i][0], len(attr)), widths[i][1])
1366 
1367                 # Now that we know all the widths, multiply them by the
1368                 # justification values to get positive or negative numbers to
1369                 # pass to the %-expander.
1370                 widths = [ e[0] * e[1] for e in widths ]
1371                 fmt = ("%%%ss " * len(widths)) % tuple(widths)
1372 
1373                 msg((fmt % tuple(headers)).rstrip())
1374         else:
1375                 fmt = "%s\t" * len(widths)
1376                 fmt.rstrip("\t")
1377 
1378         for line in sorted(lines, key=key_extract):
1379                 msg((fmt % tuple(line)).rstrip())
1380 
1381 def list_contents(img, args):
1382         """List package contents.
1383 
1384         If no arguments are given, display for all locally installed packages.
1385         With -H omit headers and use a tab-delimited format; with -o select
1386         attributes to display; with -s, specify attributes to sort on; with -t,
1387         specify which action types to list."""
1388 
1389         # XXX Need remote-info option, to request equivalent information
1390         # from repository.
1391 
1392         opts, pargs = getopt.getopt(args, "Ho:s:t:mfr")
1393 
1394         valid_special_attrs = [ "action.name", "action.key", "action.raw",
1395             "pkg.name", "pkg.fmri", "pkg.shortfmri", "pkg.publisher",
1396             "pkg.size", "pkg.csize" ]
1397 
1398         display_headers = True
1399         display_raw = False
1400         remote = False
1401         local = False
1402         attrs = []
1403         sort_attrs = []
1404         action_types = []
1405         for opt, arg in opts:
1406                 if opt == "-H":
1407                         display_headers = False
1408                 elif opt == "-o":
1409                         attrs.extend(arg.split(","))
1410                 elif opt == "-s":
1411                         sort_attrs.append(arg)
1412                 elif opt == "-t":
1413                         action_types.extend(arg.split(","))
1414                 elif opt == "-r":
1415                         remote = True
1416                 elif opt == "-m":
1417                         display_raw = True
1418 
1419         if not remote and not local:
1420                 local = True
1421         elif local and remote:
1422                 usage(_("contents: -l and -r may not be combined"))
1423 
1424         if remote and not pargs:
1425                 usage(_("contents: must request remote contents for specific "
1426                    "packages"))
1427 
1428         if not check_fmri_args(pargs):
1429                 return 1
1430 
1431         if display_raw:
1432                 display_headers = False
1433                 attrs = [ "action.raw" ]
1434 
1435                 invalid = set(("-H", "-o", "-t")). \
1436                     intersection(set([x[0] for x in opts]))
1437 
1438                 if len(invalid) > 0:
1439                         usage(_("contents: -m and %s may not be specified " \
1440                             "at the same time") % invalid.pop())
1441 
1442         for a in attrs:
1443                 if a.startswith("action.") and not a in valid_special_attrs:
1444                         usage(_("Invalid attribute '%s'") % a)
1445 
1446                 if a.startswith("pkg.") and not a in valid_special_attrs:
1447                         usage(_("Invalid attribute '%s'") % a)
1448 
1449         img.history.operation_name = "contents"
1450         img.load_catalogs(progress.QuietProgressTracker())
1451 
1452         err = 0
1453 
1454         if local:
1455                 fmris, notfound, illegals = \
1456                     img.installed_fmris_from_args(pargs)
1457 
1458                 if illegals:
1459                         for i in illegals:
1460                                 emsg(i)
1461                         img.history.operation_result = \
1462                             history.RESULT_FAILED_BAD_REQUEST
1463                         return 1
1464 
1465                 if not fmris and not notfound:
1466                         error(_("no packages installed"))
1467                         img.history.operation_result = \
1468                             history.RESULT_NOTHING_TO_DO
1469                         return 1
1470         elif remote:
1471                 # Verify validity of certificates before attempting network
1472                 # operations
1473                 try:
1474                         img.check_cert_validity()
1475                 except (api_errors.CertificateError,
1476                     api_errors.PermissionsException), e:
1477                         img.history.log_operation_end(error=e)
1478                         return 1
1479 
1480                 fmris = []
1481                 notfound = []
1482 
1483                 # XXX This loop really needs not to be copied from
1484                 # Image.make_install_plan()!
1485                 for p in pargs:
1486                         try:
1487                                 matches = list(img.inventory([ p ],
1488                                     all_known = True))
1489                         except api_errors.InventoryException, e:
1490                                 assert(len(e.notfound) == 1)
1491                                 notfound.append(e.notfound[0])
1492                                 continue
1493 
1494                         pnames = {}
1495                         pmatch = []
1496                         npnames = {}
1497                         npmatch = []
1498                         for m, state in matches:
1499                                 if m.preferred_publisher():
1500                                         pnames[m.get_pkg_stem()] = 1
1501                                         pmatch.append(m)
1502                                 else:
1503                                         npnames[m.get_pkg_stem()] = 1
1504                                         npmatch.append(m)
1505 
1506                         if len(pnames.keys()) > 1:
1507                                 msg(_("pkg: '%s' matches multiple packages") % \
1508                                     p)
1509                                 for k in pnames.keys():
1510                                         msg("\t%s" % k)
1511                                 continue
1512                         elif len(pnames.keys()) < 1 and len(npnames.keys()) > 1:
1513                                 msg(_("pkg: '%s' matches multiple packages") % \
1514                                     p)
1515                                 for k in npnames.keys():
1516                                         msg("\t%s" % k)
1517                                 continue
1518 
1519                         # matches is a list reverse sorted by version, so take
1520                         # the first; i.e., the latest.
1521                         if len(pmatch) > 0:
1522                                 fmris.append(pmatch[0])
1523                         else:
1524                                 fmris.append(npmatch[0])
1525 
1526         #
1527         # If the user specifies no specific attrs, and no specific
1528         # sort order, then we fill in some defaults.
1529         #
1530         if not attrs:
1531                 # XXX Possibly have multiple exclusive attributes per column?
1532                 # If listing dependencies and files, you could have a path/fmri
1533                 # column which would list paths for files and fmris for
1534                 # dependencies.
1535                 attrs = [ "path" ]
1536 
1537         if not sort_attrs:
1538                 # XXX reverse sorting
1539                 # Most likely want to sort by path, so don't force people to
1540                 # make it explicit
1541                 if "path" in attrs:
1542                         sort_attrs = [ "path" ]
1543                 else:
1544                         sort_attrs = attrs[:1]
1545 
1546         # if we want a raw display (contents -m), disable the automatic
1547         # variant filtering that normally limits working set.
1548 
1549         if display_raw:
1550                 excludes = EmptyI
1551         else:
1552                 excludes = img.list_excludes()
1553 
1554         manifests = ( img.get_manifest(f, all_arch=display_raw) for f in fmris )
1555 
1556         actionlist = [ 
1557             (m, a)
1558             for m in manifests
1559             for a in m.gen_actions(excludes) 
1560         ]
1561 
1562         if fmris:
1563                 display_contents_results(actionlist, attrs, sort_attrs,
1564                     action_types, display_headers)
1565 
1566         if notfound:
1567                 err = 1
1568                 if fmris:
1569                         emsg()
1570                 if local:
1571                         emsg(_("""\
1572 pkg: no packages matching the following patterns you specified are
1573 installed on the system.  Try specifying -r to query remotely:"""))
1574                 elif remote:
1575                         emsg(_("""\
1576 pkg: no packages matching the following patterns you specified were
1577 found in the catalog.  Try relaxing the patterns, refreshing, and/or
1578 examining the catalogs:"""))
1579                 emsg()
1580                 for p in notfound:
1581                         emsg("        %s" % p)
1582                 img.history.operation_result = history.RESULT_NOTHING_TO_DO
1583         else:
1584                 img.history.operation_result = history.RESULT_SUCCEEDED
1585         return err
1586 
1587 def display_catalog_failures(cre):
1588         total = cre.total
1589         succeeded = cre.succeeded
1590 
1591         txt = _("pkg: %s/%s catalogs successfully updated:") % (succeeded,
1592             total)
1593         if cre.failed:
1594                 # This ensures that the text gets printed before the errors.
1595                 emsg(txt)
1596         else:
1597                 msg(txt)
1598 
1599         for pub, err in cre.failed:
1600                 if isinstance(err, urllib2.HTTPError):
1601                         emsg("   %s: %s - %s" % \
1602                             (err.filename, err.code, err.msg))
1603                 elif isinstance(err, urllib2.URLError):
1604                         if err.args[0][0] == 8:
1605                                 emsg("    %s: %s" % \
1606                                     (urlparse.urlsplit(
1607                                         pub["origin"])[1].split(":")[0],
1608                                     err.args[0][1]))
1609                         else:
1610                                 if isinstance(err.args[0], socket.timeout):
1611                                         emsg("    %s: %s" % \
1612                                             (pub["origin"], "timeout"))
1613                                 else:
1614                                         emsg("    %s: %s" % \
1615                                             (pub["origin"], err.args[0][1]))
1616                 elif isinstance(err, CatalogRetrievalError) and \
1617                     isinstance(err.exc, EnvironmentError) and \
1618                     err.exc.errno == errno.EACCES:
1619                         if err.prefix:
1620                                 emsg("   ", _("Could not update catalog "
1621                                      "for '%s' due to insufficient "
1622                                      "permissions.") % err.prefix)
1623                         else:
1624                                 emsg("   ", _("Could not update a catalog "
1625                                      "due to insufficient permissions."))
1626 
1627                         emsg("   ", _("Please try the command again "
1628                         "using pfexec, or otherwise increase \n    your "
1629                         "permissions."))
1630                 else:
1631                         emsg("   ", err)
1632 
1633         if cre.message:
1634                 emsg(cre.message)
1635 
1636         return succeeded
1637 
1638 def publisher_refresh(img_dir, args):
1639         """Update metadata for the image's publishers."""
1640 
1641         # XXX will need to show available content series for each package
1642         full_refresh = False
1643         opts, pargs = getopt.getopt(args, "", ["full"])
1644         for opt, arg in opts:
1645                 if opt == "--full":
1646                         full_refresh = True
1647 
1648         try:
1649                 api_inst = api.ImageInterface(img_dir, CLIENT_API_VERSION,
1650                     get_tracker(), None, PKG_CLIENT_NAME)
1651         except api_errors.ImageNotFoundException, e:
1652                 error(_("'%s' is not an install image") % e.user_dir)
1653                 return 1
1654 
1655         try:
1656                 # The user explicitly requested this refresh, so set the
1657                 # refresh to occur immediately.
1658                 api_inst.refresh(full_refresh=full_refresh, immediate=True,
1659                     pubs=pargs)
1660         except api_errors.PublisherError, e:
1661                 error(e)
1662                 error(_("'pkg publisher' will show a list of publishers."))
1663                 return 1
1664         except (api_errors.PermissionsException,
1665             api_errors.NetworkUnavailableException), e:
1666                 # Prepend a newline because otherwise the exception will
1667                 # be printed on the same line as the spinner.
1668                 error("\n" + str(e))
1669                 return 1
1670         except api_errors.CatalogRefreshException, e:
1671                 if display_catalog_failures(e) == 0:
1672                         return 1
1673                 else:
1674                         return 3
1675         else:
1676                 return 0
1677 
1678 def publisher_set(img, img_dir, args):
1679         """pkg set-publisher [-Ped] [-k ssl_key] [-c ssl_cert] [--reset-uuid]
1680             [-O origin_url] [-m mirror to add] [-M mirror to remove]
1681             [--enable] [--disable] [--no-refresh] publisher"""
1682 
1683         preferred = False
1684         ssl_key = None
1685         ssl_cert = None
1686         origin_url = None
1687         reset_uuid = False
1688         add_mirror = None
1689         remove_mirror = None
1690         refresh_catalogs = True
1691         disable = None
1692 
1693         opts, pargs = getopt.getopt(args, "Pedk:c:O:M:m:",
1694             ["add-mirror=", "remove-mirror=", "no-refresh", "reset-uuid",
1695             "enable", "disable"])
1696 
1697         for opt, arg in opts:
1698                 if opt == "-P":
1699                         preferred = True
1700                 if opt == "-k":
1701                         ssl_key = arg
1702                 if opt == "-c":
1703                         ssl_cert = arg
1704                 if opt == "-O":
1705                         origin_url = arg
1706                 if opt == "-m" or opt == "--add-mirror":
1707                         add_mirror = arg
1708                 if opt == "-M" or opt == "--remove-mirror":
1709                         remove_mirror = arg
1710                 if opt == "--no-refresh":
1711                         refresh_catalogs = False
1712                 if opt == "--reset-uuid":
1713                         reset_uuid = True
1714                 if opt == "-e" or opt == "--enable":
1715                         disable = False
1716                 if opt == "-d" or opt == "--disable":
1717                         disable = True
1718 
1719         if len(pargs) == 0:
1720                 usage(_("requires a publisher name"), cmd="set-publisher")
1721         elif len(pargs) > 1:
1722                 usage( _("only one publisher name may be specified"),
1723                     cmd="set-publisher",)
1724 
1725         name = pargs[0]
1726 
1727         if preferred and disable:
1728                 usage(_("the -p and -d options may not be combined"),
1729                     cmd="set-publisher")
1730 
1731         try:
1732                 api_inst = api.ImageInterface(img_dir, CLIENT_API_VERSION,
1733                     get_tracker(), None, PKG_CLIENT_NAME)
1734         except api_errors.ImageNotFoundException, e:
1735                 error(_("'%s' is not an install image") % e.user_dir,
1736                     cmd="set-publisher")
1737                 return 1
1738 
1739         new_pub = False
1740         try:
1741                 pub = api_inst.get_publisher(prefix=name, alias=name,
1742                     duplicate=True)
1743                 if reset_uuid:
1744                         pub.reset_client_uuid()
1745                 repo = pub.selected_repository
1746         except api_errors.PermissionsException, e:
1747                 error(e, cmd="set-publisher")
1748                 return 1
1749         except api_errors.UnknownPublisher:
1750                 if not origin_url:
1751                         error(_("publisher does not exist. Use -O to define "
1752                             "origin URI for new publisher."),
1753                             cmd="set-publisher")
1754                         return 1
1755                 # No pre-existing, so create a new one.
1756                 repo = publisher.Repository()
1757                 pub = publisher.Publisher(name, repositories=[repo])
1758                 new_pub = True
1759 
1760         if disable is not None:
1761                 # Set disabled property only if provided.
1762                 pub.disabled = disable
1763 
1764         if origin_url:
1765                 try:
1766                         if not repo.origins:
1767                                 # New publisher case.
1768                                 repo.add_origin(origin_url)
1769                                 origin = repo.origins[0]
1770                         else:
1771                                 origin = repo.origins[0]
1772                                 origin.uri = origin_url
1773 
1774                         # XXX once image configuration supports storing this
1775                         # information at the uri level, ssl info should be set
1776                         # here.
1777                 except api_errors.PublisherError, e:
1778                         error(e, cmd="set-publisher")
1779                         return 1
1780 
1781         if add_mirror:
1782                 try:
1783                         # XXX once image configuration supports storing this
1784                         # information at the uri level, ssl info should be set
1785                         # here.
1786                         repo.add_mirror(add_mirror)
1787                 except (api_errors.PublisherError,
1788                     api_errors.CertificateError), e:
1789                         error(e, cmd="set-publisher")
1790                         return 1
1791 
1792         if remove_mirror:
1793                 try:
1794                         repo.remove_mirror(remove_mirror)
1795                 except api_errors.PublisherError, e:
1796                         error(e, cmd="set-publisher")
1797                         return 1
1798 
1799         # None is checked for here so that a client can unset a ssl_cert or
1800         # ssl_key by using -k "" or -c "".
1801         if ssl_cert is not None or ssl_key is not None:
1802                 #
1803                 # In the case of zones, the ssl cert given is assumed to
1804                 # be relative to the root of the image, not truly absolute.
1805                 #
1806                 if img.is_zone():
1807                         if ssl_cert is not None:
1808                                 ssl_cert = os.path.abspath(
1809                                     img.get_root() + os.sep + ssl_cert)
1810                         if ssl_key is not None:
1811                                 ssl_key = os.path.abspath(
1812                                     img.get_root() + os.sep + ssl_key)
1813 
1814                 # Assume the user wanted to update the ssl_cert or ssl_key
1815                 # information for *all* of the currently selected
1816                 # repository's origins and mirrors.
1817                 try:
1818                         for uri in repo.origins:
1819                                 if ssl_cert is not None:
1820                                         uri.ssl_cert = ssl_cert
1821                                 if ssl_key is not None:
1822                                         uri.ssl_key = ssl_key
1823                         for uri in repo.mirrors:
1824                                 if ssl_cert is not None:
1825                                         uri.ssl_cert = ssl_cert
1826                                 if ssl_key is not None:
1827                                         uri.ssl_key = ssl_key
1828                 except (api_errors.PublisherError,
1829                     api_errors.CertificateError), e:
1830                         error(e, cmd="set-publisher")
1831                         return 1
1832 
1833         try:
1834                 if new_pub:
1835                         api_inst.add_publisher(pub,
1836                             refresh_allowed=refresh_catalogs)
1837                 else:
1838                         api_inst.update_publisher(pub,
1839                             refresh_allowed=refresh_catalogs)
1840         except api_errors.CatalogRefreshException, e:
1841                 text = "Could not refresh the catalog for %s"
1842                 error(_(text) % pub)
1843                 return 1
1844         except api_errors.InvalidDepotResponseException, e:
1845                 error(_("The origin URIs for '%(pubname)s' do not appear to "
1846                     "point to a valid pkg server.\nPlease check the server's "
1847                     "address and client's network configuration."
1848                     "\nAdditional details:\n\n%(details)s") %
1849                     { "pubname": pub.prefix, "details": e })
1850                 return 1
1851         except api_errors.PermissionsException, e:
1852                 # Prepend a newline because otherwise the exception will
1853                 # be printed on the same line as the spinner.
1854                 error("\n" + str(e))
1855                 return 1
1856 
1857         if preferred:
1858                 api_inst.set_preferred_publisher(prefix=pub.prefix)
1859 
1860         return 0
1861 
1862 def publisher_unset(img_dir, args):
1863         """pkg unset-publisher publisher ..."""
1864 
1865         if len(args) == 0:
1866                 usage()
1867 
1868         try:
1869                 api_inst = api.ImageInterface(img_dir, CLIENT_API_VERSION,
1870                     get_tracker(), None, PKG_CLIENT_NAME)
1871         except api_errors.ImageNotFoundException, e:
1872                 error(_("'%s' is not an install image") % e.user_dir)
1873                 return 1
1874 
1875         errors = []
1876         for name in args:
1877                 try:
1878                         api_inst.remove_publisher(prefix=name, alias=name)
1879                 except (api_errors.PermissionsException,
1880                     api_errors.PublisherError), e:
1881                         errors.append((name, e))
1882 
1883         retcode = 0
1884         if errors:
1885                 if len(errors) == len(args):
1886                         # If the operation failed for every provided publisher
1887                         # prefix or alias, complete failure occurred.
1888                         retcode = 1
1889                 else:
1890                         # If the operation failed for only some of the provided
1891                         # publisher prefixes or aliases, then partial failure
1892                         # occurred.
1893                         retcode = 3
1894 
1895                 txt = ""
1896                 for name, err in errors:
1897                         txt += "\n"
1898                         txt += _("Removal failed for '%(pub)s': %(msg)s") % {
1899                             "pub": name, "msg": err }
1900                         txt += "\n"
1901                 error(txt, cmd="unset-publisher")
1902 
1903         return retcode
1904 
1905 def publisher_list(img_dir, args):
1906         """pkg publishers"""
1907         omit_headers = False
1908         preferred_only = False
1909         inc_disabled = False
1910 
1911         opts, pargs = getopt.getopt(args, "HPa")
1912         for opt, arg in opts:
1913                 if opt == "-H":
1914                         omit_headers = True
1915                 if opt == "-P":
1916                         preferred_only = True
1917                 if opt == "-a":
1918                         inc_disabled = True
1919 
1920         progresstracker = get_tracker(True)
1921 
1922         try:
1923                 api_inst = api.ImageInterface(img_dir, CLIENT_API_VERSION,
1924                     progresstracker, None, PKG_CLIENT_NAME)
1925         except api_errors.ImageNotFoundException, e:
1926                 error(_("'%s' is not an install image") % e.user_dir)
1927                 return 1
1928 
1929         cert_cache = {}
1930         def get_cert_info(ssl_cert):
1931                 if not ssl_cert:
1932                         return None
1933                 if ssl_cert not in cert_cache:
1934                         c = cert_cache[ssl_cert] = {}
1935                         errors = c["errors"] = []
1936                         times = c["info"] = {
1937                             "effective": "",
1938                             "expiration": "",
1939                         }
1940 
1941                         try:
1942                                 cert = misc.validate_ssl_cert(ssl_cert)
1943                         except (EnvironmentError,
1944                             api_errors.CertificateError,
1945                             api_errors.PermissionsException), e:
1946                                 # If the cert information can't be retrieved,
1947                                 # add the errors to a list and continue on.
1948                                 errors.append(e)
1949                                 c["valid"] = False
1950                         else:
1951                                 nb = cert.get_notBefore()
1952                                 t = time.strptime(nb, "%Y%m%d%H%M%SZ")
1953                                 nb = datetime.datetime.utcfromtimestamp(
1954                                     calendar.timegm(t))
1955                                 times["effective"] = nb.strftime("%c")
1956 
1957                                 na = cert.get_notAfter()
1958                                 t = time.strptime(na, "%Y%m%d%H%M%SZ")
1959                                 na = datetime.datetime.utcfromtimestamp(
1960                                     calendar.timegm(t))
1961                                 times["expiration"] = na.strftime("%c")
1962                                 c["valid"] = True
1963 
1964                 return cert_cache[ssl_cert]
1965 
1966         retcode = 0
1967         if len(pargs) == 0:
1968                 fmt = "%-24s %-12s %-8s %-8s %s"
1969                 if not omit_headers:
1970                         msg(fmt % (_("PUBLISHER"), "", _("TYPE"), _("STATUS"),
1971                             _("URI")))
1972 
1973                 pref_pub = api_inst.get_preferred_publisher()
1974                 if preferred_only:
1975                         pubs = [pref_pub]
1976                 else:
1977                         pubs = [
1978                             p for p in api_inst.get_publishers()
1979                             if inc_disabled or not p.disabled
1980                         ]
1981 
1982                 for p in pubs:
1983                         pfx = p.prefix
1984                         pstatus = ""
1985                         if not preferred_only and p == pref_pub:
1986                                 pstatus = _("(preferred)")
1987                         if p.disabled:
1988                                 pstatus = _("(disabled)")
1989 
1990                         # Only show the selected repository's information in
1991                         # summary view.
1992                         r = p.selected_repository
1993                         for uri in r.origins:
1994                                 # XXX get the real origin status
1995                                 msg(fmt % (pfx, pstatus, _("origin"), "online",
1996                                     uri))
1997                         for uri in r.mirrors:
1998                                 # XXX get the real mirror status
1999                                 msg(fmt % (pfx, pstatus, _("mirror"), "online",
2000                                     uri))
2001         else:
2002                 def display_ssl_info(uri):
2003                         retcode = 0
2004                         c = get_cert_info(uri.ssl_cert)
2005                         msg(_("              SSL Key:"), uri.ssl_key)
2006                         msg(_("             SSL Cert:"), uri.ssl_cert)
2007 
2008                         if not c:
2009                                 return retcode
2010 
2011                         if c["errors"]:
2012                                 retcode = 1
2013 
2014                         for e in c["errors"]:
2015                                 emsg("\n" + str(e) + "\n")
2016 
2017                         if c["valid"]:
2018                                 msg(_(" Cert. Effective Date:"),
2019                                     c["info"]["effective"])
2020                                 msg(_("Cert. Expiration Date:"),
2021                                     c["info"]["expiration"])
2022                         return retcode
2023 
2024                 def display_repository(r):
2025                         retcode = 0
2026                         for uri in r.origins:
2027                                 msg(_("           Origin URI:"), uri)
2028                                 rval = display_ssl_info(uri)
2029                                 if rval == 1:
2030                                         retcode = 3
2031 
2032                         for uri in r.mirrors:
2033                                 msg(_("           Mirror URI:"), uri)
2034                                 rval = display_ssl_info(uri)
2035                                 if rval == 1:
2036                                         retcode = 3
2037                         return retcode
2038 
2039                 for name in pargs:
2040                         # detailed print
2041                         pub = api_inst.get_publisher(prefix=name, alias=name)
2042                         dt = api_inst.get_publisher_last_update_time(pub.prefix)
2043                         if dt:
2044                                 dt = dt.strftime("%c")
2045 
2046                         msg("")
2047                         msg(_("            Publisher:"), pub.prefix)
2048                         msg(_("                Alias:"), pub.alias)
2049 
2050                         for r in pub.repositories:
2051                                 rval = display_repository(r)
2052                                 if rval != 0:
2053                                         # There was an error in displaying some
2054                                         # of the information about a repository.
2055                                         # However, continue on.
2056                                         retcode = rval
2057 
2058                         msg(_("          Client UUID:"), pub.client_uuid)
2059                         msg(_("      Catalog Updated:"), dt)
2060                         if pub.disabled:
2061                                 msg(_("              Enabled:"), _("No"))
2062                         else:
2063                                 msg(_("              Enabled:"), _("Yes"))
2064         return retcode
2065 
2066 def property_set(img, args):
2067         """pkg set-property propname propvalue"""
2068 
2069         # ensure no options are passed in
2070         opts, pargs = getopt.getopt(args, "")
2071         try:
2072                 propname, propvalue = pargs
2073         except ValueError:
2074                 usage(_("set-property: requires a property name and value"))
2075 
2076         if propname == "preferred-publisher":
2077                 error(_("set-property: set-publisher must be used to change "
2078                         "the preferred publisher"))
2079                 return 1
2080 
2081         try:
2082                 img.set_property(propname, propvalue)
2083         except api_errors.PermissionsException, e:
2084                 # Prepend a newline because otherwise the exception will
2085                 # be printed on the same line as the spinner.
2086                 error("\nset-property failed:\n" + str(e))
2087                 return 1
2088 
2089         return 0
2090 
2091 def property_unset(img, args):
2092         """pkg unset-property propname ..."""
2093 
2094         # is this an existing property in our image?
2095         # if so, delete it
2096         # if not, error
2097 
2098         # ensure no options are passed in
2099         opts, pargs = getopt.getopt(args, "")
2100         if not pargs:
2101                 usage(_("unset-property: requires at least one property name"))
2102 
2103         for p in pargs:
2104                 if p == "preferred-publisher":
2105                         error(_("unset-property: set-publisher must be used to "
2106                             "change the preferred publisher"))
2107                         return 1
2108 
2109                 try:
2110                         img.delete_property(p)
2111                 except KeyError:
2112                         error(_("unset-property: no such property: %s") % p)
2113                         return 1
2114                 except api_errors.PermissionsException, e:
2115                         # Prepend a newline because otherwise the exception
2116                         # will be printed on the same line as the spinner.
2117                         error("\n" + str(e))
2118                         return 1
2119 
2120         return 0
2121 
2122 def property_list(img, args):
2123         """pkg property [-H] [propname ...]"""
2124         omit_headers = False
2125 
2126         opts, pargs = getopt.getopt(args, "H")
2127         for opt, arg in opts:
2128                 if opt == "-H":
2129                         omit_headers = True
2130 
2131         for p in pargs:
2132                 if not img.has_property(p):
2133                         error(_("property: no such property: %s") % p)
2134                         return 1
2135 
2136         if not pargs:
2137                 pargs = list(img.properties())
2138 
2139         width = max(max([len(p) for p in pargs]), 8)
2140         fmt = "%%-%ss %%s" % width
2141         if not omit_headers:
2142                 msg(fmt % ("PROPERTY", "VALUE"))
2143 
2144         for p in pargs:
2145                 msg(fmt % (p, img.get_property(p)))
2146 
2147         return 0
2148 
2149 def image_create(img, args):
2150         """Create an image of the requested kind, at the given path.  Load
2151         catalog for initial publisher for convenience.
2152 
2153         At present, it is legitimate for a user image to specify that it will be
2154         deployed in a zone.  An easy example would be a program with an optional
2155         component that consumes global zone-only information, such as various
2156         kernel statistics or device information."""
2157 
2158         imgtype = image.IMG_USER
2159         is_zone = False
2160         ssl_key = None
2161         ssl_cert = None
2162         pub_name = None
2163         pub_url = None
2164         refresh_catalogs = True
2165         force = False
2166         variants = {}
2167 
2168         opts, pargs = getopt.getopt(args, "fFPUza:p:k:c:",
2169             ["force", "full", "partial", "user", "zone", "authority=",
2170                 "publisher=", "no-refresh", "variant="])
2171 
2172         for opt, arg in opts:
2173                 if opt == "-f" or opt == "--force":
2174                         force = True
2175                 if opt == "-F" or opt == "--full":
2176                         imgtype = imgtypes.IMG_ENTIRE
2177                 if opt == "-P" or opt == "--partial":
2178                         imgtype = imgtypes.IMG_PARTIAL
2179                 if opt == "-U" or opt == "--user":
2180                         imgtype = imgtypes.IMG_USER
2181                 if opt == "-z" or opt == "--zone":
2182                         is_zone = True
2183                         imgtype = image.IMG_ENTIRE
2184                 if opt == "--no-refresh":
2185                         refresh_catalogs = False
2186                 if opt == "-k":
2187                         ssl_key = arg
2188                 if opt == "-c":
2189                         ssl_cert = arg
2190 
2191                 # -a is deprecated and will be removed at a future date.
2192                 if opt in ("-a", "-p", "--publisher"):
2193                         try:
2194                                 pub_name, pub_url = arg.split("=", 1)
2195                         except ValueError:
2196                                 usage(_("image-create requires publisher "
2197                                     "argument to be of the form "
2198                                     "'<prefix>=<url>'."))
2199                 if opt == "--variant":
2200                         try:
2201                                 v_name, v_value = arg.split("=", 1)
2202                                 if not v_name.startswith("variant."):
2203                                         v_name = "variant.%s" % v_name 
2204                         except ValueError:
2205                                 usage(_("image-create requires variant "
2206                                     "arguments to be of the form "
2207                                     "'<name>=<value>'."))
2208                         variants[v_name] = v_value
2209 
2210         if len(pargs) != 1:
2211                 usage(_("image-create requires a single image directory path"))
2212         image_dir = pargs[0]
2213 
2214         if ssl_key:
2215                 # When creating zones, the path is image-root-relative.
2216                 if is_zone:
2217                         ssl_key = os.path.normpath(image_dir + os.sep + \
2218                             ssl_key)
2219                 else:
2220                         ssl_key = os.path.abspath(ssl_key)
2221                 if not os.path.exists(ssl_key):
2222                         msg(_("pkg: set-publisher: SSL key file '%s' does " \
2223                             "not exist") % ssl_key)
2224                         return 1
2225 
2226         if ssl_cert:
2227                 # When creating zones, the path is image-root-relative.
2228                 if is_zone:
2229                         ssl_cert = os.path.normpath(image_dir + os.sep + \
2230                             ssl_cert)
2231                 else:
2232                         ssl_cert = os.path.abspath(ssl_cert)
2233                 if not os.path.exists(ssl_cert):
2234                         msg(_("pkg: set-publisher: SSL key cert '%s' does " \
2235                             "not exist") % ssl_cert)
2236                         return 1
2237 
2238         if not pub_name and not pub_url:
2239                 usage(_("image-create requires a publisher argument"))
2240 
2241         if not pub_name or not pub_url:
2242                 usage(_("image-create requires publisher argument to be of "
2243                     "the form '<prefix>=<url>'."))
2244 
2245         if pub_name.startswith(fmri.PREF_PUB_PFX):
2246                 error(_("image-create requires that a prefix not match: %s"
2247                         % fmri.PREF_PUB_PFX))
2248                 return 1
2249 
2250         if not misc.valid_pub_prefix(pub_name):
2251                 error(_("image-create: publisher prefix has invalid " \
2252                     "characters"))
2253                 return 1
2254 
2255         # Bail if there is already an image there
2256         if img.image_type(image_dir) != None and not force:
2257                 error(_("there is already an image at: %s") % image_dir)
2258                 error(_("To override, use the -f (force) option."))
2259                 return 1
2260 
2261         # Bail if the directory exists but isn't empty
2262         if os.path.exists(image_dir) and \
2263             len(os.listdir(image_dir)) > 0 and not force:
2264                 error(_("Non-empty directory: %s") % image_dir)
2265                 error(_("To override, use the -f (force) option."))
2266                 return 1
2267 
2268         try:
2269                 img.set_attrs(imgtype, image_dir, is_zone, pub_name, pub_url,
2270                     ssl_key=ssl_key, ssl_cert=ssl_cert, variants=variants,
2271                     refresh_allowed=refresh_catalogs, progtrack=get_tracker())
2272         except OSError, e:
2273                 # Ensure messages are displayed after the spinner.
2274                 emsg("\n")
2275                 error(_("cannot create image at %(image_dir)s: %(reason)s") %
2276                     { "image_dir": image_dir, "reason": e.args[1] })
2277                 return 1
2278         except api_errors.PermissionsException, e:
2279                 # Ensure messages are displayed after the spinner.
2280                 emsg("")
2281                 error(e, cmd="image-create")
2282                 return 1
2283         except api_errors.InvalidDepotResponseException, e:
2284                 # Ensure messages are displayed after the spinner.
2285                 emsg("\n")
2286                 error(_("The URI '%(pub_url)s' does not appear to point to a "
2287                     "valid pkg server.\nPlease check the server's "
2288                     "address and client's network configuration."
2289                     "\nAdditional details:\n\n%(error)s") %
2290                     { "pub_url": pub_url, "error": e },
2291                     cmd="image-create")
2292                 return 1
2293         except api_errors.CatalogRefreshException, cre:
2294                 # Ensure messages are displayed after the spinner.
2295                 error("", cmd="image-create")
2296                 if display_catalog_failures(cre) == 0:
2297                         return 1
2298                 else:
2299                         return 3
2300         return 0
2301 
2302 
2303 def rebuild_index(img_dir, pargs):
2304         """pkg rebuild-index
2305 
2306         Forcibly rebuild the search indexes. Will remove existing indexes
2307         and build new ones from scratch."""
2308         quiet = False
2309 
2310         if pargs:
2311                 usage(_("rebuild-index: command does not take operands " \
2312                     "('%s')") % " ".join(pargs))
2313         try:
2314                 api_inst = api.ImageInterface(img_dir, CLIENT_API_VERSION,
2315                     get_tracker(quiet), None, PKG_CLIENT_NAME)
2316         except api_errors.ImageNotFoundException, e:
2317                 error(_("'%s' is not an install image") % e.user_dir)
2318                 return 1
2319 
2320         try:
2321                 api_inst.rebuild_search_index()
2322         except api_errors.CorruptedIndexException:
2323                 error(INCONSISTENT_INDEX_ERROR_MESSAGE)
2324                 return 1
2325         except api_errors.ProblematicPermissionsIndexException, e:
2326                 error(str(e) + PROBLEMATIC_PERMISSIONS_ERROR_MESSAGE)
2327                 return 1
2328         except api_errors.MainDictParsingException, e:
2329                 error(str(e))
2330                 return 1
2331         else:
2332                 return 0
2333 
2334 def history_list(img, args):
2335         """Display history about the current image.
2336         """
2337 
2338         omit_headers = False
2339         long_format = False
2340 
2341         opts, pargs = getopt.getopt(args, "Hl")
2342         for opt, arg in opts:
2343                 if opt == "-H":
2344                         omit_headers = True
2345                 elif opt == "-l":
2346                         long_format = True
2347 
2348         if omit_headers and long_format:
2349                 usage(_("history: -H and -l may not be combined"))
2350 
2351         if not long_format:
2352                 if not omit_headers:
2353                         msg("%-19s %-25s %-15s %s" % (_("TIME"),
2354                             _("OPERATION"), _("CLIENT"), _("OUTCOME")))
2355 
2356         if not os.path.exists(img.history.path):
2357                 # Nothing to display.
2358                 return 0
2359 
2360         for entry in sorted(os.listdir(img.history.path)):
2361                 # Load the history entry.
2362                 try:
2363                         he = history.History(root_dir=img.history.root_dir,
2364                             filename=entry)
2365                 except api_errors.PermissionsException, e:
2366                         error(e, cmd="history")
2367                         return 1
2368                 except history.HistoryLoadException, e:
2369                         if e.parse_failure:
2370                                 # Ignore corrupt entries.
2371                                 continue
2372                         raise
2373 
2374                 # Retrieve and format some of the data shared between each
2375                 # output format.
2376                 start_time = misc.timestamp_to_time(
2377                     he.operation_start_time)
2378                 start_time = datetime.datetime.fromtimestamp(
2379                     start_time).isoformat()
2380 
2381                 res = he.operation_result
2382                 if len(res) > 1:
2383                         outcome = "%s (%s)" % (_(res[0]), _(res[1]))
2384                 else:
2385                         outcome = _(res[0])
2386 
2387                 if long_format:
2388                         data = []
2389                         data.append(("Operation", he.operation_name))
2390 
2391                         data.append(("Outcome", outcome))
2392                         data.append(("Client", he.client_name))
2393                         data.append(("Version", he.client_version))
2394 
2395                         data.append(("User", "%s (%s)" % \
2396                             (he.operation_username, he.operation_userid)))
2397 
2398                         data.append(("Start Time", start_time))
2399 
2400                         end_time = misc.timestamp_to_time(
2401                             he.operation_end_time)
2402                         end_time = datetime.datetime.fromtimestamp(
2403                             end_time).isoformat()
2404                         data.append(("End Time", end_time))
2405 
2406                         data.append(("Command", " ".join(he.client_args)))
2407 
2408                         state = he.operation_start_state
2409                         if state:
2410                                 data.append(("Start State", "\n" + state))
2411 
2412                         state = he.operation_end_state
2413                         if state:
2414                                 data.append(("End State", "\n" + state))
2415 
2416                         errors = "\n".join(he.operation_errors)
2417                         if errors:
2418                                 data.append(("Errors", "\n" + errors))
2419 
2420                         for field, value in data:
2421                                 msg("%15s: %s" % (_(field), value))
2422 
2423                         # Separate log entries with a blank line.
2424                         msg("")
2425                 else:
2426                         msg("%-19s %-25s %-15s %s" % (start_time,
2427                             he.operation_name, he.client_name, outcome))
2428 
2429         return 0
2430 
2431 # To allow exception handler access to the image.
2432 __img = None
2433 
2434 def main_func():
2435         global_settings.client_name = PKG_CLIENT_NAME
2436 
2437         global __img
2438         __img = img = image.Image()
2439 
2440         misc.setlocale(locale.LC_ALL, "", error)
2441         gettext.install("pkg", "/usr/share/locale")
2442 
2443         try:
2444                 opts, pargs = getopt.getopt(sys.argv[1:], "R:D:?",
2445                     ["debug=", "help"])
2446         except getopt.GetoptError, e:
2447                 usage(_("illegal global option -- %s") % e.opt)
2448 
2449         show_usage = False
2450         for opt, arg in opts:
2451                 if opt == "-D" or opt == "--debug":
2452                         try:
2453                                 key, value = arg.split("=", 1)
2454                         except (AttributeError, ValueError):
2455                                 usage(_("%(opt)s takes argument of form "
2456                                     "name=value, not %(arg)s") % { "opt":  opt,
2457                                     "arg": arg })
2458                         DebugValues.set_value(key, value)
2459                 elif opt == "-R":
2460                         mydir = arg
2461                 elif opt in ("--help", "-?"):
2462                         show_usage = True
2463 
2464         subcommand = None
2465         if pargs:
2466                 subcommand = pargs.pop(0)
2467                 if subcommand == "help":
2468                         show_usage = True
2469 
2470         if show_usage:
2471                 usage(retcode=0)
2472         elif not subcommand:
2473                 usage()
2474 
2475         socket.setdefaulttimeout(
2476             int(os.environ.get("PKG_CLIENT_TIMEOUT", "30"))) # in seconds
2477 
2478         # Override default PKG_TIMEOUT_MAX if a value has been specified
2479         # in the environment.
2480         global_settings.PKG_TIMEOUT_MAX = int(os.environ.get("PKG_TIMEOUT_MAX",
2481             global_settings.PKG_TIMEOUT_MAX))
2482 
2483         if subcommand == "image-create":
2484                 if "mydir" in locals():
2485                         usage(_("-R not allowed for %s subcommand") %
2486                               subcommand)
2487                 try:
2488                         ret = image_create(img, pargs)
2489                 except getopt.GetoptError, e:
2490                         usage(_("illegal %s option -- %s") % \
2491                             (subcommand, e.opt))
2492                 return ret
2493         elif subcommand == "version":
2494                 if "mydir" in locals():
2495                         usage(_("-R not allowed for %s subcommand") %
2496                               subcommand)
2497                 if pargs:
2498                         usage(_("version: command does not take operands " \
2499                             "('%s')") % " ".join(pargs))
2500                 msg(pkg.VERSION)
2501                 return 0
2502 
2503         provided_image_dir = True
2504         pkg_image_used = False
2505 
2506         if "mydir" not in locals():
2507                 try:
2508                         mydir = os.environ["PKG_IMAGE"]
2509                         pkg_image_used = True
2510                 except KeyError:
2511                         try:
2512                                 provided_image_dir = False
2513                                 mydir = os.getcwd()
2514                         except OSError, e:
2515                                 try:
2516                                         mydir = os.environ["PWD"]
2517                                         if not mydir or mydir[0] != "/":
2518                                                 mydir = None
2519                                 except KeyError:
2520                                         mydir = None
2521 
2522         if mydir == None:
2523                 error(_("Could not find image.  Use the -R option or set "
2524                     "$PKG_IMAGE to point\nto an image, or change the working "
2525                     "directory to one inside the image."))
2526                 return 1
2527 
2528         try:
2529                 img.find_root(mydir, provided_image_dir)
2530         except api_errors.ImageNotFoundException, e:
2531                 if e.user_specified:
2532                         m = "No image rooted at '%s'"
2533                         if pkg_image_used:
2534                                 m += " (set by $PKG_IMAGE)"
2535                         error(_(m) % e.user_dir)
2536                 else:
2537                         error(_("No image found."))
2538                 return 1
2539 
2540         try:
2541                 img.load_config()
2542         except api_errors.ApiException, e:
2543                 error(_("client configuration error: %s") % e)
2544                 return 1
2545 
2546         try:
2547                 if subcommand == "refresh":
2548                         return publisher_refresh(mydir, pargs)
2549                 elif subcommand == "list":
2550                         return list_inventory(img, pargs)
2551                 elif subcommand == "image-update":
2552                         return image_update(mydir, pargs)
2553                 elif subcommand == "install":
2554                         return install(mydir, pargs)
2555                 elif subcommand == "uninstall":
2556                         return uninstall(mydir, pargs)
2557                 elif subcommand == "freeze":
2558                         return freeze(img, pargs)
2559                 elif subcommand == "unfreeze":
2560                         return unfreeze(img, pargs)
2561                 elif subcommand == "search":
2562                         return search(mydir, pargs)
2563                 elif subcommand == "info":
2564                         return info(mydir, pargs)
2565                 elif subcommand == "contents":
2566                         return list_contents(img, pargs)
2567                 elif subcommand == "fix":
2568                         return fix_image(img, pargs)
2569                 elif subcommand == "verify":
2570                         return verify_image(img, pargs)
2571                 elif subcommand in ("set-authority", "set-publisher"):
2572                         return publisher_set(img, mydir, pargs)
2573                 elif subcommand in ("unset-authority", "unset-publisher"):
2574                         return publisher_unset(mydir, pargs)
2575                 elif subcommand in ("authority", "publisher"):
2576                         return publisher_list(mydir, pargs)
2577                 elif subcommand == "set-property":
2578                         return property_set(img, pargs)
2579                 elif subcommand == "unset-property":
2580                         return property_unset(img, pargs)
2581                 elif subcommand == "property":
2582                         return property_list(img, pargs)
2583                 elif subcommand == "history":
2584                         return history_list(img, pargs)
2585                 elif subcommand == "purge-history":
2586                         ret_code = img.history.purge()
2587                         if ret_code == 0:
2588                                 msg(_("History purged."))
2589                         return ret_code
2590                 elif subcommand == "rebuild-index":
2591                         return rebuild_index(mydir, pargs)
2592                 else:
2593                         usage(_("unknown subcommand '%s'") % subcommand)
2594 
2595         except getopt.GetoptError, e:
2596                 usage(_("illegal %(cmd)s option -- %(error)s") %
2597                     { "cmd": subcommand, "error": e.opt })
2598 
2599 
2600 #
2601 # Establish a specific exit status which means: "python barfed an exception"
2602 # so that we can more easily detect these in testing of the CLI commands.
2603 #
2604 if __name__ == "__main__":
2605         try:
2606                 # Out of memory errors can be raised as EnvironmentErrors with
2607                 # an errno of ENOMEM, so in order to handle those exceptions
2608                 # with other errnos, we nest this try block and have the outer
2609                 # one handle the other instances.
2610                 try:
2611                         __ret = main_func()
2612                 except (MemoryError, EnvironmentError), __e:
2613                         if isinstance(__e, EnvironmentError) and \
2614                             __e.errno != errno.ENOMEM:
2615                                 raise
2616                         if __img:
2617                                 __img.history.abort(RESULT_FAILED_OUTOFMEMORY)
2618                         error("\n" + misc.out_of_memory())
2619                         __ret = 1
2620         except SystemExit, __e:
2621                 if __img:
2622                         __img.history.abort(RESULT_FAILED_UNKNOWN)
2623                 raise __e
2624         except (PipeError, KeyboardInterrupt):
2625                 if __img:
2626                         __img.history.abort(RESULT_CANCELED)
2627                 # We don't want to display any messages here to prevent
2628                 # possible further broken pipe (EPIPE) errors.
2629                 __ret = 1
2630         except api_errors.CertificateError, __e:
2631                 if __img:
2632                         __img.history.abort(RESULT_FAILED_CONFIGURATION)
2633                 error(__e)
2634                 __ret = 1
2635         except api_errors.PublisherError, __e:
2636                 if __img:
2637                         __img.history.abort(RESULT_FAILED_BAD_REQUEST)
2638                 error(__e)
2639                 __ret = 1
2640         except misc.TransportException, __e:
2641                 if __img:
2642                         __img.history.abort(RESULT_FAILED_TRANSPORT)
2643                 error(_("\nMaximum number of network retries exceeded during "
2644                     "download. Details follow:\n%s") % __e)
2645                 __ret = 1
2646         except (ManifestRetrievalError,
2647             DatastreamRetrievalError, FileListRetrievalError), __e:
2648                 if __img:
2649                         __img.history.abort(RESULT_FAILED_TRANSPORT)
2650                 error(_("An error was encountered while attempting to retrieve"
2651                     " package or file data for the requested operation."))
2652                 error(__e)
2653                 __ret = 1
2654         except api_errors.InvalidDepotResponseException, __e:
2655                 if __img:
2656                         __img.history.abort(RESULT_FAILED_TRANSPORT)
2657                 error(_("\nUnable to contact a valid package depot. "
2658                     "This may be due to a problem with the server, "
2659                     "network misconfiguration, or an incorrect pkg client "
2660                     "configuration.  Please check your network settings and "
2661                     "attempt to contact the server using a web browser."))
2662                 error(_("\nAdditional details:\n\n%s") % __e)
2663                 __ret = 1 
2664         except history.HistoryLoadException, __e:
2665                 # Since a history related error occurred, discard all
2666                 # information about the current operation(s) in progress.
2667                 if __img:
2668                         __img.history.clear()
2669                 error(_("An error was encountered while attempting to load "
2670                     "history information\nabout past client operations."))
2671                 error(__e)
2672                 __ret = 1
2673         except history.HistoryStoreException, __e:
2674                 # Since a history related error occurred, discard all
2675                 # information about the current operation(s) in progress.
2676                 if __img:
2677                         __img.history.clear()
2678                 error(_("An error was encountered while attempting to store "
2679                     "information about the\ncurrent operation in client "
2680                     "history."))
2681                 error(__e)
2682                 __ret = 1
2683         except history.HistoryPurgeException, __e:
2684                 # Since a history related error occurred, discard all
2685                 # information about the current operation(s) in progress.
2686                 if __img:
2687                         __img.history.clear()
2688                 error(_("An error was encountered while attempting to purge "
2689                     "client history."))
2690                 error(__e)
2691                 __ret = 1
2692         except api_errors.VersionException, __e:
2693                 if __img:
2694                         __img.history.abort(RESULT_FAILED_UNKNOWN)
2695                 error(_("The pkg command appears out of sync with the "
2696                     "libraries provided \nby SUNWipkg. The client version is "
2697                     "%(client)s while the library API version is %(api)s") %
2698                     {'client': __e.received_version,
2699                      'api': __e.expected_version
2700                     })
2701                 __ret = 1
2702         except:
2703                 if __img:
2704                         __img.history.abort(RESULT_FAILED_UNKNOWN)
2705                 traceback.print_exc()
2706                 error(
2707                     _("\n\nThis is an internal error.  Please let the "
2708                     "developers know about this\nproblem by filing a bug at "
2709                     "http://defect.opensolaris.org and including the\nabove "
2710                     "traceback and this message.  The version of pkg(5) is "
2711                     "'%s'.") % pkg.VERSION)
2712                 __ret = 99
2713 
2714         sys.exit(__ret)