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