1 #!/usr/bin/python 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 23 # 24 # Copyright 2010 Sun Microsystems, Inc. All rights reserved. 25 # Use is subject to license terms. 26 # 27 28 """This module provides the supported, documented interface for clients to 29 interface with the pkg(5) system. 30 31 Refer to pkg.api_common for additional core class documentation.""" 32 33 import copy 34 import errno 35 import fnmatch 36 import os 37 import shutil 38 import sys 39 import threading 40 import urllib 41 42 import pkg.client.actuator as actuator 43 import pkg.client.api_errors as api_errors 44 import pkg.client.bootenv as bootenv 45 import pkg.client.history as history 46 import pkg.client.image as image 47 import pkg.client.imagetypes as imgtypes 48 import pkg.client.indexer as indexer 49 import pkg.client.publisher as publisher 50 import pkg.client.query_parser as query_p 51 import pkg.fmri as fmri 52 import pkg.misc as misc 53 import pkg.nrlock 54 import pkg.p5i as p5i 55 import pkg.search_errors as search_errors 56 import pkg.version 57 58 from pkg.api_common import (PackageInfo, LicenseInfo, PackageCategory, 59 _get_pkg_cat_data) 60 from pkg.client.imageplan import EXECUTED_OK 61 from pkg.client import global_settings 62 63 CURRENT_API_VERSION = 32 64 CURRENT_P5I_VERSION = 1 65 66 # Image type constants. 67 IMG_TYPE_NONE = imgtypes.IMG_NONE # No image. 68 IMG_TYPE_ENTIRE = imgtypes.IMG_ENTIRE # Full image ('/'). 69 IMG_TYPE_PARTIAL = imgtypes.IMG_PARTIAL # Not yet implemented. 70 IMG_TYPE_USER = imgtypes.IMG_USER # Not '/'; some other location. 71 72 logger = global_settings.logger 73 74 class ImageInterface(object): 75 """This class presents an interface to images that clients may use. 76 There is a specific order of methods which must be used to install 77 or uninstall packages, or update an image. First, plan_install, 78 plan_uninstall, plan_update_all or plan_change_variant must be 79 called. After that method completes successfully, describe may be 80 called, and prepare must be called. Finally, execute_plan may be 81 called to implement the previous created plan. The other methods 82 do not have an ordering imposed upon them, and may be used as 83 needed. Cancel may only be invoked while a cancelable method is 84 running.""" 85 86 # Constants used to reference specific values that info can return. 87 INFO_FOUND = 0 88 INFO_MISSING = 1 89 INFO_MULTI_MATCH = 2 90 INFO_ILLEGALS = 3 91 92 LIST_ALL = 0 93 LIST_INSTALLED = 1 94 LIST_INSTALLED_NEWEST = 2 95 LIST_NEWEST = 3 96 LIST_UPGRADABLE = 4 97 98 # Private constants used for tracking which type of plan was made. 99 __INSTALL = 1 100 __UNINSTALL = 2 101 __IMAGE_UPDATE = 3 102 103 def __init__(self, img_path, version_id, progresstracker, 104 cancel_state_callable, pkg_client_name): 105 """Constructs an ImageInterface object. 106 107 'img_path' is the absolute path to an existing image. 108 109 'version_id' indicates the version of the api the client is 110 expecting to use. 111 112 'progresstracker' is the ProgressTracker object the client wants 113 the api to use for UI progress callbacks. 114 115 'cancel_state_callable' is an optional function reference that 116 will be called if the cancellable status of an operation 117 changes. 118 119 'pkg_client_name' is a string containing the name of the client, 120 such as "pkg" or "packagemanager". 121 122 This function can raise VersionException and 123 ImageNotFoundException.""" 124 125 compatible_versions = set([CURRENT_API_VERSION]) 126 127 if version_id not in compatible_versions: 128 raise api_errors.VersionException(CURRENT_API_VERSION, 129 version_id) 130 131 # The image's History object will use client_name from 132 # global_settings, but if the program forgot to set it, 133 # we'll go ahead and do so here. 134 if global_settings.client_name is None: 135 global_settings.client_name = pkg_client_name 136 137 if isinstance(img_path, basestring): 138 # Store this for reset(). 139 self.__img_path = img_path 140 self.__img = image.Image(img_path, 141 progtrack=progresstracker) 142 elif isinstance(img_path, image.Image): 143 # This is a temporary, special case for client.py 144 # until the image api is complete. 145 self.__img = img_path 146 self.__img_path = img_path.get_root() 147 else: 148 # API consumer passed an unknown type for img_path. 149 raise TypeError(_("Unknown img_path type.")) 150 151 self.__progresstracker = progresstracker 152 self.__cancel_state_callable = cancel_state_callable 153 self.__plan_type = None 154 self.__plan_desc = None 155 self.__prepared = False 156 self.__executed = False 157 self.__be_name = None 158 self.__can_be_canceled = False 159 self.__canceling = False 160 self.__activity_lock = pkg.nrlock.NRLock() 161 self.__blocking_locks = False 162 self.__img.blocking_locks = self.__blocking_locks 163 self.__cancel_lock = pkg.nrlock.NRLock() 164 self.__cancel_cv = threading.Condition(self.__cancel_lock) 165 166 def __set_blocking_locks(self, value): 167 self.__activity_lock.acquire() 168 self.__blocking_locks = value 169 self.__img.blocking_locks = value 170 self.__activity_lock.release() 171 172 blocking_locks = property(lambda self: self.__blocking_locks, 173 __set_blocking_locks, doc="A boolean value indicating whether " 174 "the API should wait until the image interface can be locked if " 175 "it is in use by another thread or process. Clients should be " 176 "aware that there is no timeout mechanism in place if blocking is " 177 "enabled, and so should take steps to remain responsive to user " 178 "input or provide a way for users to cancel operations.") 179 180 @property 181 def img(self): 182 """Private; public access to this property will be removed at 183 a later date. Do not use.""" 184 return self.__img 185 186 @property 187 def img_type(self): 188 """Returns the IMG_TYPE constant for the image's type.""" 189 if not self.__img: 190 return None 191 return self.__img.image_type(self.__img.root) 192 193 @property 194 def is_zone(self): 195 """A boolean value indicating whether the image is a zone.""" 196 return self.__img.is_zone() 197 198 @property 199 def root(self): 200 """The absolute pathname of the filesystem root of the image. 201 This property is read-only.""" 202 if not self.__img: 203 return None 204 return self.__img.root 205 206 @staticmethod 207 def check_be_name(be_name): 208 bootenv.BootEnv.check_be_name(be_name) 209 return True 210 211 def __cert_verify(self, log_op_end=None): 212 """Verify validity of certificates. Any 213 api_errors.ExpiringCertificate exceptions are caught 214 here, a message is displayed, and execution continues. 215 All other exceptions will be passed to the calling 216 context. The caller can also set log_op_end to a list 217 of exceptions that should result in a call to 218 self.log_operation_end() before the exception is passed 219 on.""" 220 221 if log_op_end == None: 222 log_op_end = [] 223 224 # we always explicitly handle api_errors.ExpiringCertificate 225 assert api_errors.ExpiringCertificate not in log_op_end 226 227 try: 228 self.__img.check_cert_validity() 229 except api_errors.ExpiringCertificate, e: 230 logger.error(e) 231 except: 232 exc_type, exc_value, exc_traceback = sys.exc_info() 233 if exc_type in log_op_end: 234 self.log_operation_end(error=exc_value) 235 raise 236 237 def __refresh_publishers(self): 238 """Refresh publisher metadata.""" 239 240 # 241 # Verify validity of certificates before possibly 242 # attempting network operations. 243 # 244 self.__cert_verify() 245 self.__img.refresh_publishers(immediate=True, 246 progtrack=self.__progresstracker) 247 248 def __acquire_activity_lock(self): 249 """Private helper method to aqcuire activity lock.""" 250 251 rc = self.__activity_lock.acquire( 252 blocking=self.__blocking_locks) 253 if not rc: 254 raise api_errors.ImageLockedError() 255 256 def __plan_common_start(self, operation, noexecute): 257 """Start planning an operation. Acquire locks and log 258 the start of the operation.""" 259 260 self.__acquire_activity_lock() 261 try: 262 self.__enable_cancel() 263 if self.__plan_type is not None: 264 raise api_errors.PlanExistsException( 265 self.__plan_type) 266 self.__img.lock(allow_unprivileged=noexecute) 267 except: 268 self.__activity_lock.release() 269 raise 270 271 assert self.__activity_lock._is_owned() 272 self.log_operation_start(operation) 273 274 def __plan_common_finish(self): 275 """Finish planning an operation.""" 276 277 assert self.__activity_lock._is_owned() 278 self.__img.cleanup_downloads() 279 self.__img.unlock() 280 self.__activity_lock.release() 281 282 def __plan_common_exception(self, log_op_end=None): 283 """Deal with exceptions that can occur while planning an 284 operation. Any exceptions generated here are passed 285 onto the calling context. By default all exceptions 286 will result in a call to self.log_operation_end() before 287 they are passed onto the calling context. Optionally, 288 the caller can specify the exceptions that should result 289 in a call to self.log_operation_end() by setting 290 log_op_end.""" 291 292 if log_op_end == None: 293 log_op_end = [] 294 295 # we always explicity handle api_errors.PlanCreationException 296 assert api_errors.PlanCreationException not in log_op_end 297 298 exc_type, exc_value, exc_traceback = sys.exc_info() 299 300 if exc_type == api_errors.PlanCreationException: 301 self.__set_history_PlanCreationException(exc_value) 302 elif exc_type == api_errors.CanceledException: 303 self.__cancel_done() 304 elif not log_op_end or exc_type in log_op_end: 305 self.log_operation_end(error=exc_value) 306 307 if exc_type != api_errors.ImageLockedError: 308 # Must be called before reset_unlock, and only if 309 # the exception was not a locked error. 310 self.__img.unlock() 311 312 self.__reset_unlock() 313 self.__activity_lock.release() 314 raise 315 316 def plan_install(self, pkg_list, refresh_catalogs=True, 317 noexecute=False, verbose=False, update_index=True): 318 """Contructs a plan to install the packages provided in 319 pkg_list. pkg_list is a list of packages to install. 320 refresh_catalogs controls whether the catalogs will 321 automatically be refreshed. noexecute determines whether the 322 history will be recorded after planning is finished. verbose 323 controls whether verbose debugging output will be printed to the 324 terminal. Its existence is temporary. It returns a boolean 325 which tells the client whether there is anything to do. It can 326 raise PlanCreationException, PermissionsException and 327 InventoryException. The noexecute argument is included for 328 compatibility with operational history. The hope is it can be 329 removed in the future.""" 330 331 self.__plan_common_start("install", noexecute) 332 try: 333 if refresh_catalogs: 334 self.__refresh_publishers() 335 336 self.__img.make_install_plan(pkg_list, 337 self.__progresstracker, 338 self.__check_cancelation, noexecute, 339 verbose=verbose) 340 341 assert self.__img.imageplan 342 343 self.__disable_cancel() 344 345 if not noexecute: 346 self.__plan_type = self.__INSTALL 347 348 self.__plan_desc = PlanDescription(self.__img) 349 if self.__img.imageplan.nothingtodo() or noexecute: 350 self.log_operation_end( 351 result=history.RESULT_NOTHING_TO_DO) 352 353 self.__img.imageplan.update_index = update_index 354 except: 355 self.__plan_common_exception(log_op_end=[ 356 api_errors.CanceledException, fmri.IllegalFmri, 357 Exception]) 358 # NOTREACHED 359 360 self.__plan_common_finish() 361 res = not self.__img.imageplan.nothingtodo() 362 return res 363 364 def plan_uninstall(self, pkg_list, recursive_removal, noexecute=False, 365 verbose=False, update_index=True): 366 """Contructs a plan to uninstall the packages provided in 367 pkg_list. pkg_list is a list of packages to install. 368 recursive_removal controls whether recursive removal is 369 allowed. noexecute determines whether the history will be 370 recorded after planning is finished. verbose controls whether 371 verbose debugging output will be printed to the terminal. Its 372 existence is temporary. If there are things to do to complete 373 the uninstall, it returns True, otherwise it returns False. It 374 can raise NonLeafPackageException and PlanCreationException.""" 375 376 self.__plan_common_start("uninstall", noexecute) 377 378 try: 379 self.__img.make_uninstall_plan(pkg_list, 380 recursive_removal, self.__progresstracker, 381 self.__check_cancelation, noexecute, 382 verbose=verbose) 383 384 assert self.__img.imageplan 385 386 self.__disable_cancel() 387 388 if not noexecute: 389 self.__plan_type = self.__UNINSTALL 390 391 self.__plan_desc = PlanDescription(self.__img) 392 if noexecute: 393 self.log_operation_end( 394 result=history.RESULT_NOTHING_TO_DO) 395 self.__img.imageplan.update_index = update_index 396 except: 397 self.__plan_common_exception() 398 # NOTREACHED 399 400 self.__plan_common_finish() 401 res = not self.__img.imageplan.nothingtodo() 402 return res 403 404 def plan_update_all(self, actual_cmd, refresh_catalogs=True, 405 noexecute=False, force=False, verbose=False, update_index=True, 406 be_name=None): 407 """Creates a plan to update all packages on the system to the 408 latest known versions. actual_cmd is the command used to start 409 the client. It is used to determine the image to check whether 410 SUNWipkg is up to date. refresh_catalogs controls whether the 411 catalogs will automatically be refreshed. noexecute determines 412 whether the history will be recorded after planning is finished. 413 force controls whether update should proceed even if ipkg is not 414 up to date. verbose controls whether verbose debugging output 415 will be printed to the terminal. Its existence is temporary. It 416 returns a tuple of two things. The first is a boolean which 417 tells the client whether there is anything to do. The second 418 tells whether the image is an opensolaris image. It can raise 419 CatalogRefreshException, IpkgOutOfDateException, 420 PlanCreationException and PermissionsException.""" 421 422 self.__plan_common_start("image-update", noexecute) 423 try: 424 self.check_be_name(be_name) 425 self.__be_name = be_name 426 427 if refresh_catalogs: 428 self.__refresh_publishers() 429 430 # If we can find SUNWipkg and SUNWcs in the 431 # target image, then we assume this is a valid 432 # opensolaris image, and activate some 433 # special case behaviors. 434 opensolaris_image = True 435 fmris, notfound, illegals = \ 436 self.__img.installed_fmris_from_args( 437 ["SUNWipkg", "SUNWcs"]) 438 assert(len(illegals) == 0) 439 if notfound: 440 opensolaris_image = False 441 442 if opensolaris_image and not force: 443 try: 444 if not self.__img.ipkg_is_up_to_date( 445 actual_cmd, 446 self.__check_cancelation, 447 noexecute, 448 refresh_allowed=refresh_catalogs, 449 progtrack=self.__progresstracker): 450 raise api_errors.IpkgOutOfDateException() 451 except api_errors.ImageNotFoundException: 452 # Can't do anything in this 453 # case; so proceed. 454 pass 455 456 self.__img.make_update_plan(self.__progresstracker, 457 self.__check_cancelation, noexecute, 458 verbose=verbose) 459 460 assert self.__img.imageplan 461 462 self.__disable_cancel() 463 464 if not noexecute: 465 self.__plan_type = self.__IMAGE_UPDATE 466 467 self.__plan_desc = PlanDescription(self.__img) 468 469 if self.__img.imageplan.nothingtodo() or noexecute: 470 self.log_operation_end( 471 result=history.RESULT_NOTHING_TO_DO) 472 self.__img.imageplan.update_index = update_index 473 474 except: 475 self.__plan_common_exception( 476 log_op_end=[api_errors.IpkgOutOfDateException]) 477 # NOTREACHED 478 479 self.__plan_common_finish() 480 res = not self.__img.imageplan.nothingtodo() 481 return res, opensolaris_image 482 483 def plan_change_varcets(self, variants=None, facets=None, 484 noexecute=False, verbose=False, be_name=None): 485 """Creates a plan to change the specified variants/facets on an image. 486 verbose controls 487 whether verbose debugging output will be printed to the 488 terminal. This function has two return values. The first is 489 a boolean which tells the client whether there is anything to 490 do. The third is either None, or an exception which indicates 491 partial success. This is currently used to indicate a failure 492 in refreshing catalogs. It can raise CatalogRefreshException, 493 IpkgOutOfDateException, NetworkUnavailableException, 494 PlanCreationException and PermissionsException.""" 495 496 self.__plan_common_start("change-variant", noexecute) 497 if not variants and not facets: 498 raise ValueError, "Nothing to do" 499 try: 500 self.check_be_name(be_name) 501 self.be_name = be_name 502 503 self.__refresh_publishers() 504 505 self.__img.image_change_varcets(variants, 506 facets, 507 self.__progresstracker, 508 self.__check_cancelation, 509 noexecute, verbose=verbose) 510 511 assert self.__img.imageplan 512 513 self.__disable_cancel() 514 515 if not noexecute: 516 self.__plan_type = self.__IMAGE_UPDATE 517 518 self.__plan_desc = PlanDescription(self.__img) 519 520 if self.__img.imageplan.nothingtodo() or noexecute: 521 self.log_operation_end( 522 result=history.RESULT_NOTHING_TO_DO) 523 524 # 525 # We always rebuild the search index after a 526 # variant change 527 # 528 self.__img.imageplan.update_index = True 529 530 except: 531 self.__plan_common_exception() 532 # NOTREACHED 533 534 self.__plan_common_finish() 535 res = not self.__img.imageplan.nothingtodo() 536 return res 537 538 def describe(self): 539 """Returns None if no plan is ready yet, otherwise returns 540 a PlanDescription.""" 541 542 return self.__plan_desc 543 544 def prepare(self): 545 """Takes care of things which must be done before the plan 546 can be executed. This includes downloading the packages to 547 disk and preparing the indexes to be updated during 548 execution. It can raise ProblematicPermissionsIndexException, 549 and PlanMissingException. Should only be called once a 550 plan_X method has been called.""" 551 552 self.__acquire_activity_lock() 553 try: 554 self.__img.lock() 555 except: 556 self.__activity_lock.release() 557 raise 558 559 try: 560 if not self.__img.imageplan: 561 raise api_errors.PlanMissingException() 562 563 if self.__prepared: 564 raise api_errors.AlreadyPreparedException() 565 assert self.__plan_type == self.__INSTALL or \ 566 self.__plan_type == self.__UNINSTALL or \ 567 self.__plan_type == self.__IMAGE_UPDATE 568 569 self.__enable_cancel() 570 571 try: 572 self.__img.imageplan.preexecute() 573 except search_errors.ProblematicPermissionsIndexException, e: 574 raise api_errors.ProblematicPermissionsIndexException(e) 575 except: 576 raise 577 578 self.__disable_cancel() 579 self.__prepared = True 580 except api_errors.CanceledException, e: 581 self.__cancel_done() 582 if self.__img.history.operation_name: 583 # If an operation is in progress, log 584 # the error and mark its end. 585 self.log_operation_end(error=e) 586 raise 587 except Exception, e: 588 self.__cancel_cleanup_exception() 589 if self.__img.history.operation_name: 590 # If an operation is in progress, log 591 # the error and mark its end. 592 self.log_operation_end(error=e) 593 raise 594 except: 595 # Handle exceptions that are not subclasses of 596 # Exception. 597 self.__cancel_cleanup_exception() 598 if self.__img.history.operation_name: 599 # If an operation is in progress, log 600 # the error and mark its end. 601 exc_type, exc_value, exc_traceback = \ 602 sys.exc_info() 603 self.log_operation_end(error=exc_type) 604 raise 605 finally: 606 self.__img.cleanup_downloads() 607 self.__img.unlock() 608 self.__activity_lock.release() 609 610 def execute_plan(self): 611 """Executes the plan. This is uncancelable one it begins. It 612 can raise CorruptedIndexException, ImageLockedError, 613 ProblematicPermissionsIndexException, ImageplanStateException, 614 ImageUpdateOnLiveImageException, and PlanMissingException. 615 Should only be called after the prepare method has been 616 called.""" 617 618 self.__acquire_activity_lock() 619 try: 620 self.__disable_cancel() 621 self.__img.lock() 622 except: 623 self.__activity_lock.release() 624 raise 625 626 try: 627 if not self.__img.imageplan: 628 raise api_errors.PlanMissingException() 629 630 if not self.__prepared: 631 raise api_errors.PrematureExecutionException() 632 633 if self.__executed: 634 raise api_errors.AlreadyExecutedException() 635 636 assert self.__plan_type == self.__INSTALL or \ 637 self.__plan_type == self.__UNINSTALL or \ 638 self.__plan_type == self.__IMAGE_UPDATE 639 640 try: 641 be = bootenv.BootEnv(self.__img.get_root()) 642 except RuntimeError: 643 be = bootenv.BootEnvNull(self.__img.get_root()) 644 self.__img.bootenv = be 645 646 if self.__plan_type is self.__IMAGE_UPDATE: 647 try: 648 be.init_image_recovery(self.__img, 649 self.__be_name) 650 except Exception, e: 651 self.log_operation_end(error=e) 652 raise 653 except: 654 # Handle exceptions that are not 655 # subclasses of Exception. 656 exc_type, exc_value, exc_traceback = \ 657 sys.exc_info() 658 self.log_operation_end(error=exc_type) 659 raise 660 661 if self.__img.is_liveroot(): 662 e = api_errors.ImageUpdateOnLiveImageException() 663 self.log_operation_end(error=e) 664 raise e 665 else: 666 if self.__img.imageplan.reboot_needed() and \ 667 self.__img.is_liveroot(): 668 e = api_errors.RebootNeededOnLiveImageException() 669 self.log_operation_end(error=e) 670 raise e 671 672 try: 673 self.__img.imageplan.execute() 674 except RuntimeError, e: 675 if self.__plan_type is self.__IMAGE_UPDATE: 676 be.restore_image() 677 else: 678 be.restore_install_uninstall() 679 # Must be done after bootenv restore. 680 self.log_operation_end(error=e) 681 raise 682 except search_errors.ProblematicPermissionsIndexException, e: 683 error = api_errors.ProblematicPermissionsIndexException(e) 684 self.log_operation_end(error=error) 685 raise error 686 except (search_errors.InconsistentIndexException, 687 search_errors.PartialIndexingException), e: 688 error = api_errors.CorruptedIndexException(e) 689 self.log_operation_end(error=error) 690 raise error 691 except search_errors.MainDictParsingException, e: 692 error = api_errors.MainDictParsingException(e) 693 self.log_operation_end(error=error) 694 raise error 695 except actuator.NonzeroExitException, e: 696 # Won't happen during image-update 697 be.restore_install_uninstall() 698 error = api_errors.ActuatorException(e) 699 self.log_operation_end(error=error) 700 raise error 701 except api_errors.WrapIndexingException, e: 702 self.__finished_execution(be) 703 raise 704 except Exception, e: 705 if self.__plan_type is self.__IMAGE_UPDATE: 706 be.restore_image() 707 else: 708 be.restore_install_uninstall() 709 # Must be done after bootenv restore. 710 self.log_operation_end(error=e) 711 raise 712 except: 713 # Handle exceptions that are not subclasses of 714 # Exception. 715 exc_type, exc_value, exc_traceback = \ 716 sys.exc_info() 717 718 if self.__plan_type is self.__IMAGE_UPDATE: 719 be.restore_image() 720 else: 721 be.restore_install_uninstall() 722 # Must be done after bootenv restore. 723 self.log_operation_end(error=exc_type) 724 raise 725 726 self.__finished_execution(be) 727 finally: 728 self.__img.cleanup_downloads() 729 self.__img.unlock() 730 self.__activity_lock.release() 731 732 def __finished_execution(self, be): 733 if self.__img.imageplan.state != EXECUTED_OK: 734 if self.__plan_type is self.__IMAGE_UPDATE: 735 be.restore_image() 736 else: 737 be.restore_install_uninstall() 738 739 error = api_errors.ImageplanStateException( 740 self.__img.imageplan.state) 741 # Must be done after bootenv restore. 742 self.log_operation_end(error=error) 743 raise error 744 if self.__plan_type is self.__IMAGE_UPDATE: 745 be.activate_image() 746 else: 747 be.activate_install_uninstall() 748 self.__img.cleanup_cached_content() 749 # If the end of the operation wasn't already logged 750 # by one of the previous operations, then log it as 751 # ending now. 752 if self.__img.history.operation_name: 753 self.log_operation_end() 754 self.__executed = True 755 try: 756 if int(os.environ.get("PKG_DUMP_STATS", 0)) > 0: 757 self.__img.transport.stats.dump() 758 except ValueError: 759 # Don't generate stats if an invalid value 760 # is supplied. 761 pass 762 763 def set_plan_license_status(self, pfmri, plicense, accepted=None, 764 displayed=None): 765 """Sets the license status for the given package FMRI and 766 license entry. 767 768 'accepted' is an optional parameter that can be one of three 769 values: 770 None leaves accepted status unchanged 771 False sets accepted status to False 772 True sets accepted status to True 773 774 'displayed' is an optional parameter that can be one of three 775 values: 776 None leaves displayed status unchanged 777 False sets displayed status to False 778 True sets displayed status to True""" 779 780 self.__acquire_activity_lock() 781 try: 782 try: 783 self.__disable_cancel() 784 except api_errors.CanceledException: 785 self.__cancel_done() 786 raise 787 788 if not self.__img.imageplan: 789 raise api_errors.PlanMissingException() 790 791 for pp in self.__img.imageplan.pkg_plans: 792 if pp.destination_fmri == pfmri: 793 pp.set_license_status(plicense, 794 accepted=accepted, 795 displayed=displayed) 796 break 797 finally: 798 self.__activity_lock.release() 799 800 def refresh(self, full_refresh=False, pubs=None, immediate=False): 801 """Refreshes the metadata (e.g. catalog) for one or more 802 publishers. 803 804 'full_refresh' is an optional boolean value indicating whether 805 a full retrieval of publisher metadata (e.g. catalogs) or only 806 an update to the existing metadata should be performed. When 807 True, 'immediate' is also set to True. 808 809 'pubs' is a list of publisher prefixes or publisher objects 810 to refresh. Passing an empty list or using the default value 811 implies all publishers. 812 813 'immediate' is an optional boolean value indicating whether 814 a refresh should occur now. If False, a publisher's selected 815 repository will only be checked for updates if the update 816 interval period recorded in the image configuration has been 817 exceeded. 818 819 Currently returns an image object, allowing existing code to 820 work while the rest of the API is put into place.""" 821 822 self.__acquire_activity_lock() 823 try: 824 self.__disable_cancel() 825 self.__img.lock() 826 try: 827 self.__refresh(full_refresh=full_refresh, 828 pubs=pubs, immediate=immediate) 829 return self.__img 830 finally: 831 self.__img.unlock() 832 self.__img.cleanup_downloads() 833 except api_errors.CanceledException: 834 self.__cancel_done() 835 raise 836 finally: 837 self.__activity_lock.release() 838 839 def __refresh(self, full_refresh=False, pubs=None, immediate=False): 840 """Private refresh method; caller responsible for locking and 841 cleanup.""" 842 843 self.__img.refresh_publishers(full_refresh=full_refresh, 844 immediate=immediate, pubs=pubs, 845 progtrack=self.__progresstracker) 846 847 def __licenses(self, pfmri, mfst): 848 """Private function. Returns the license info from the 849 manifest mfst.""" 850 license_lst = [] 851 for lic in mfst.gen_actions_by_type("license"): 852 license_lst.append(LicenseInfo(pfmri, lic, 853 img=self.__img)) 854 return license_lst 855 856 def get_pkg_categories(self, installed=False, pubs=misc.EmptyI): 857 """Returns an order list of tuples of the form (scheme, 858 category) containing the names of all categories in use by 859 the last version of each unique package in the catalog on a 860 per-publisher basis. 861 862 'installed' is an optional boolean value indicating whether 863 only the categories used by currently installed packages 864 should be returned. If False, the categories used by the 865 latest vesion of every known package will be returned 866 instead. 867 868 'pubs' is an optional list of publisher prefixes to restrict 869 the results to.""" 870 871 if installed: 872 img_cat = self.__img.get_catalog( 873 self.__img.IMG_CATALOG_INSTALLED) 874 excludes = misc.EmptyI 875 else: 876 img_cat = self.__img.get_catalog( 877 self.__img.IMG_CATALOG_KNOWN) 878 excludes = self.__img.list_excludes() 879 return sorted(img_cat.categories(excludes=excludes, pubs=pubs)) 880 881 def get_pkg_list(self, pkg_list, cats=None, patterns=misc.EmptyI, 882 pubs=misc.EmptyI, variants=False): 883 """A generator function that produces tuples of the form ((pub, 884 stem, version), summary, categories, states). Where 'pub' is 885 the publisher of the package, 'stem' is the name of the package, 886 'version' is a string for the package version, 'summary' is the 887 package summary, 'categories' is a list of tuples of the form 888 (scheme, category), and 'states' is a list of PackageInfo states 889 for the package. Results are always sorted by stem, publisher, 890 and then in descending version order. 891 892 'pkg_list' is one of the following constant values indicating 893 what base set of package data should be used for results: 894 895 LIST_ALL 896 All known packages. 897 898 LIST_INSTALLED 899 Installed packages. 900 901 LIST_INSTALLED_NEWEST 902 Installed packages and the newest 903 versions of packages not installed. 904 Renamed packages that are listed in 905 an installed incorporation will be 906 excluded unless they are installed. 907 908 LIST_NEWEST 909 The newest versions of all known packages. 910 911 LIST_UPGRADABLE 912 Packages that are installed and upgradable. 913 914 'cats' is an optional list of package category tuples of the 915 form (scheme, cat) to restrict the results to. If a package 916 is assigned to any of the given categories, it will be 917 returned. A value of [] will return packages not assigned 918 to any package category. A value of None indicates that no 919 package category filtering should be applied. 920 921 'patterns' is an optional list of FMRI wildcard strings to 922 filter results by. 923 924 'pubs' is an optional list of publisher prefixes to restrict 925 the results to. 926 927 'variants' is an optional boolean value that indicates that 928 packages that are for arch or zone variants not applicable to 929 this image should be returned. 930 931 Please note that this function may invoke network operations 932 to retrieve the requested package information.""" 933 934 all = installed = inst_newest = newest = upgradable = False 935 if pkg_list == self.LIST_ALL: 936 all = True 937 elif pkg_list == self.LIST_INSTALLED: 938 installed = True 939 elif pkg_list == self.LIST_INSTALLED_NEWEST: 940 inst_newest = True 941 elif pkg_list == self.LIST_NEWEST: 942 newest = True 943 elif pkg_list == self.LIST_UPGRADABLE: 944 upgradable = True 945 946 inc_vers = {} 947 brelease = self.__img.attrs["Build-Release"] 948 949 # Each pattern in patterns can be a partial or full FMRI, so 950 # extract the individual components for use in filtering. 951 illegals = [] 952 pat_tuples = {} 953 MATCH_EXACT = 0 954 MATCH_FMRI = 1 955 MATCH_GLOB = 2 956 for pat in patterns: 957 try: 958 if "*" in pat or "?" in pat: 959 matcher = MATCH_GLOB 960 961 # XXX By default, matching FMRIs 962 # currently do not also use 963 # MatchingVersion. If that changes, 964 # this should change too. 965 parts = pat.split("@", 1) 966 if len(parts) == 1: 967 npat = pkg.fmri.MatchingPkgFmri( 968 pat, brelease) 969 else: 970 npat = pkg.fmri.MatchingPkgFmri( 971 parts[0], brelease) 972 npat.version = \ 973 pkg.version.MatchingVersion( 974 str(parts[1]), brelease) 975 elif pat.startswith("pkg:/"): 976 matcher = MATCH_EXACT 977 npat = pkg.fmri.PkgFmri(pat, 978 brelease) 979 else: 980 matcher = MATCH_FMRI 981 npat = pkg.fmri.PkgFmri(pat, 982 brelease) 983 pat_tuples[pat] = (npat.tuple(), matcher) 984 except (pkg.fmri.FmriError, pkg.version.VersionError): 985 illegals.append(pat) 986 987 if illegals: 988 raise api_errors.InventoryException(illegal=illegals) 989 990 # For LIST_INSTALLED_NEWEST, installed packages need to be 991 # determined and incorporation and publisher relationships 992 # mapped. 993 inst_stems = {} 994 ren_inst_stems = {} 995 ren_stems = {} 996 997 pub_ranks = {} 998 inc_stems = {} 999 if inst_newest: 1000 img_cat = self.__img.get_catalog( 1001 self.__img.IMG_CATALOG_INSTALLED) 1002 cat_info = frozenset([img_cat.DEPENDENCY]) 1003 1004 pub_ranks = self.__img.get_publisher_ranks() 1005 1006 # The incorporation list should include all installed, 1007 # incorporated packages from all publishers. 1008 for t in img_cat.entry_actions(cat_info): 1009 (pub, stem, ver), entry, actions = t 1010 1011 inst_stems[stem] = ver 1012 pkgr = False 1013 targets = set() 1014 for a in actions: 1015 if a.name == "set" and \ 1016 a.attrs["name"] == "pkg.renamed": 1017 pkgr = True 1018 continue 1019 elif a.name != "depend": 1020 continue 1021 1022 if a.attrs["type"] == "require": 1023 # Because the actions are not 1024 # returned in a guaranteed 1025 # order, the dependencies will 1026 # have to be recorded for 1027 # evaluation later. 1028 targets.add(a.attrs["fmri"]) 1029 elif a.attrs["type"] == "incorporate": 1030 # Record incorporated packages. 1031 tgt = fmri.PkgFmri( 1032 a.attrs["fmri"], brelease) 1033 tver = tgt.version 1034 over = inc_vers.get( 1035 tgt.pkg_name, None) 1036 1037 # In case this package has been 1038 # incorporated more than once, 1039 # use the newest version. 1040 if over is not None and \ 1041 over > tver: 1042 continue 1043 inc_vers[tgt.pkg_name] = tver 1044 1045 if pkgr: 1046 for f in targets: 1047 tgt = fmri.PkgFmri(f, brelease) 1048 ren_stems[tgt.pkg_name] = stem 1049 ren_inst_stems.setdefault(stem, 1050 set()) 1051 ren_inst_stems[stem].add( 1052 tgt.pkg_name) 1053 1054 def check_stem(t, entry): 1055 pub, stem, ver = t 1056 if stem in inst_stems: 1057 iver = inst_stems[stem] 1058 if stem in ren_inst_stems or \ 1059 ver == iver: 1060 # The package has been renamed 1061 # or the entry is for the same 1062 # version as that which is 1063 # installed, so doesn't need 1064 # to be checked. 1065 return False 1066 # The package may have been renamed in 1067 # a newer version, so must be checked. 1068 return True 1069 elif stem in inc_vers: 1070 # Package is incorporated, but not 1071 # installed, so should be checked. 1072 return True 1073 1074 tgt = ren_stems.get(stem, None) 1075 while tgt is not None: 1076 # This seems counter-intuitive, but 1077 # for performance and other reasons, 1078 # this stem should only be checked 1079 # for a rename if it is incorporated 1080 # or installed using a previous name. 1081 if tgt in inst_stems or \ 1082 tgt in inc_vers: 1083 return True 1084 tgt = ren_stems.get(tgt, None) 1085 1086 # Package should not be checked. 1087 return False 1088 1089 img_cat = self.__img.get_catalog( 1090 self.__img.IMG_CATALOG_KNOWN) 1091 1092 # Find terminal rename entry for all known packages not 1093 # rejected by check_stem(). 1094 for t, entry, actions in img_cat.entry_actions(cat_info, 1095 cb=check_stem, last=True): 1096 pkgr = False 1097 targets = set() 1098 for a in actions: 1099 if a.name == "set" and \ 1100 a.attrs["name"] == "pkg.renamed": 1101 pkgr = True 1102 continue 1103 1104 if a.name != "depend": 1105 continue 1106 1107 if a.attrs["type"] != "require": 1108 continue 1109 1110 # Because the actions are not 1111 # returned in a guaranteed 1112 # order, the dependencies will 1113 # have to be recorded for 1114 # evaluation later. 1115 targets.add(a.attrs["fmri"]) 1116 1117 if pkgr: 1118 pub, stem, ver = t 1119 for f in targets: 1120 tgt = fmri.PkgFmri(f, brelease) 1121 ren_stems[tgt.pkg_name] = stem 1122 1123 # Determine highest ranked publisher for package stems 1124 # listed in installed incorporations. 1125 def pub_order(a, b): 1126 return cmp(pub_ranks[a][0], pub_ranks[b][0]) 1127 1128 for p in sorted(pub_ranks, cmp=pub_order): 1129 if pubs and p not in pubs: 1130 continue 1131 for stem in img_cat.names(pubs=[p]): 1132 if stem in inc_vers: 1133 inc_stems.setdefault(stem, p) 1134 1135 if installed or upgradable: 1136 img_cat = self.__img.get_catalog( 1137 self.__img.IMG_CATALOG_INSTALLED) 1138 1139 # Don't need to perform variant filtering if only 1140 # listing installed packages. 1141 variants = True 1142 else: 1143 img_cat = self.__img.get_catalog( 1144 self.__img.IMG_CATALOG_KNOWN) 1145 1146 cat_info = frozenset([img_cat.DEPENDENCY, img_cat.SUMMARY]) 1147 api_info = frozenset([PackageInfo.SUMMARY, 1148 PackageInfo.CATEGORIES]) 1149 1150 # Keep track of when the newest version has been found for 1151 # each incorporated stem. 1152 slist = set() 1153 1154 # Keep track of listed stems for all other packages on a 1155 # per-publisher basis. 1156 nlist = set() 1157 1158 def check_state(t, entry): 1159 states = entry["metadata"]["states"] 1160 pkgi = self.__img.PKG_STATE_INSTALLED in states 1161 pkgu = self.__img.PKG_STATE_UPGRADABLE in states 1162 pub, stem, ver = t 1163 1164 if upgradable: 1165 # If package is marked upgradable, return it. 1166 return pkgu 1167 elif pkgi: 1168 # Nothing more to do here. 1169 return True 1170 elif stem in inst_stems: 1171 # Some other version of this package is 1172 # installed, so this one should not be 1173 # returned. 1174 return False 1175 1176 # Attempt to determine if this package is installed 1177 # under a different name or constrained under a 1178 # different name. 1179 tgt = ren_stems.get(stem, None) 1180 while tgt is not None: 1181 if tgt in inc_vers: 1182 # Package is incorporated under a 1183 # different name, so allow this 1184 # to fallthrough to the incoporation 1185 # evaluation. 1186 break 1187 elif tgt in inst_stems: 1188 # Package is installed under a 1189 # different name, so skip it. 1190 return False 1191 tgt = ren_stems.get(tgt, None) 1192 1193 # Attempt to find a suitable version to return. 1194 if stem in inc_vers: 1195 # For package stems that are incorporated, only 1196 # return the newest successor version based on 1197 # publisher rank. 1198 if stem in slist: 1199 # Newest version already returned. 1200 return False 1201 1202 if stem in inc_stems and \ 1203 pub != inc_stems[stem]: 1204 # This entry is for a lower-ranked 1205 # publisher. 1206 return False 1207 1208 # XXX version should not require build release. 1209 ever = pkg.version.Version(ver, brelease) 1210 1211 # If the entry's version is a successor to 1212 # the incorporated version, then this is the 1213 # 'newest' version of this package since 1214 # entries are processed in descending version 1215 # order. 1216 iver = inc_vers[stem] 1217 if ever.is_successor(iver, 1218 pkg.version.CONSTRAINT_AUTO): 1219 slist.add(stem) 1220 return True 1221 return False 1222 1223 pkg_stem = pub + "!" + stem 1224 if pkg_stem in nlist: 1225 # A newer version has already been listed for 1226 # this stem and publisher. 1227 return False 1228 return True 1229 1230 filter_cb = None 1231 if inst_newest or upgradable: 1232 # Filtering needs to be applied. 1233 filter_cb = check_state 1234 1235 arch = self.__img.get_arch() 1236 excludes = self.__img.list_excludes() 1237 is_zone = self.__img.is_zone() 1238 1239 for t, entry, actions in img_cat.entry_actions(cat_info, 1240 cb=filter_cb, excludes=excludes, last=newest, 1241 ordered=True, pubs=pubs): 1242 pub, stem, ver = t 1243 1244 # Perform image arch and zone variant filtering so 1245 # that only packages appropriate for this image are 1246 # returned, but only do this for packages that are 1247 # not installed. 1248 pcats = [] 1249 pkgr = False 1250 omit_package = False 1251 summ = None 1252 targets = set() 1253 1254 states = entry["metadata"]["states"] 1255 pkgi = self.__img.PKG_STATE_INSTALLED in states 1256 for a in actions: 1257 if a.name == "depend" and \ 1258 a.attrs["type"] == "require": 1259 targets.add(a.attrs["fmri"]) 1260 continue 1261 if a.name != "set": 1262 continue 1263 1264 atname = a.attrs["name"] 1265 atvalue = a.attrs["value"] 1266 if atname == "pkg.summary": 1267 summ = atvalue 1268 continue 1269 1270 if atname == "description": 1271 if summ is None: 1272 # Historical summary field. 1273 summ = atvalue 1274 continue 1275 1276 if atname == "info.classification": 1277 pcats.extend( 1278 a.parse_category_info()) 1279 1280 if pkgi: 1281 # No filtering for installed packages. 1282 continue 1283 1284 # Rename filtering should only be performed for 1285 # incorporated packages at this point. 1286 if atname == "pkg.renamed": 1287 if stem in inc_vers: 1288 pkgr = True 1289 continue 1290 1291 if variants: 1292 # No variant filtering. 1293 continue 1294 1295 is_list = type(atvalue) == list 1296 if atname == "variant.arch": 1297 if (is_list and arch not in atvalue) or \ 1298 (not is_list and arch != atvalue): 1299 # Package is not for the 1300 # image's architecture. 1301 omit_package = True 1302 continue 1303 1304 if atname == "variant.opensolaris.zone": 1305 if (is_zone and is_list and 1306 "nonglobal" not in atvalue) or \ 1307 (is_zone and not is_list and 1308 atvalue != "nonglobal"): 1309 # Package is for zones only. 1310 omit_package = True 1311 1312 if filter_cb is not None: 1313 pkg_stem = pub + "!" + stem 1314 nlist.add(pkg_stem) 1315 1316 if not pkgi and pkgr and stem in inc_vers: 1317 # If the package is not installed, but this is 1318 # the terminal version entry for the stem and 1319 # it is an incorporated package, then omit the 1320 # package if it has been installed or is 1321 # incorporated using one of the new names. 1322 for e in targets: 1323 tgt = e 1324 while tgt is not None: 1325 if tgt in ren_inst_stems or \ 1326 tgt in inc_vers: 1327 omit_package = True 1328 break 1329 tgt = ren_stems.get(tgt, None) 1330 1331 # Pattern filtering has to be applied last so that 1332 # renames, incorporations, and everything else is 1333 # handled correctly. 1334 if not omit_package: 1335 for pat in patterns: 1336 (pat_pub, pat_stem, pat_ver), matcher = \ 1337 pat_tuples[pat] 1338 1339 if pat_pub is not None and \ 1340 pub != pat_pub: 1341 # Publisher doesn't match. 1342 omit_package = True 1343 continue 1344 1345 if matcher == MATCH_EXACT: 1346 if pat_stem != stem: 1347 # Stem doesn't match. 1348 omit_package = True 1349 continue 1350 elif matcher == MATCH_FMRI: 1351 if not ("/" + stem).endswith( 1352 "/" + pat_stem): 1353 # Stem doesn't match. 1354 omit_package = True 1355 continue 1356 elif matcher == MATCH_GLOB: 1357 if not fnmatch.fnmatchcase(stem, 1358 pat_stem): 1359 # Stem doesn't match. 1360 omit_package = True 1361 continue 1362 1363 if pat_ver is not None: 1364 ever = pkg.version.Version(ver, 1365 brelease) 1366 if not ever.is_successor(pat_ver, 1367 pkg.version.CONSTRAINT_AUTO): 1368 omit_package = True 1369 continue 1370 1371 # If this entry matched at least one 1372 # pattern, then ensure it is returned. 1373 omit_package = False 1374 break 1375 1376 if omit_package: 1377 continue 1378 1379 if cats is not None: 1380 if not cats: 1381 if pcats: 1382 # Only want packages with no 1383 # categories. 1384 continue 1385 elif not [sc for sc in cats if sc in pcats]: 1386 # Package doesn't match specified 1387 # category criteria. 1388 continue 1389 1390 # Return the requested package data. 1391 yield (t, summ, pcats, 1392 frozenset(entry["metadata"]["states"])) 1393 1394 def info(self, fmri_strings, local, info_needed): 1395 """Gathers information about fmris. fmri_strings is a list 1396 of fmri_names for which information is desired. local 1397 determines whether to retrieve the information locally 1398 (if possible). It returns a dictionary of lists. The keys 1399 for the dictionary are the constants specified in the class 1400 definition. The values are lists of PackageInfo objects or 1401 strings.""" 1402 1403 # Currently, this is mostly a wapper for activity locking. 1404 self.__acquire_activity_lock() 1405 try: 1406 i = self._info_op(fmri_strings, local, info_needed) 1407 finally: 1408 self.__img.cleanup_downloads() 1409 self.__activity_lock.release() 1410 1411 return i 1412 1413 def _info_op(self, fmri_strings, local, info_needed): 1414 """Performs the actual info operation. The external 1415 interface to the API's consumers is defined in info().""" 1416 1417 bad_opts = info_needed - PackageInfo.ALL_OPTIONS 1418 if bad_opts: 1419 raise api_errors.UnrecognizedOptionsToInfo(bad_opts) 1420 1421 self.log_operation_start("info") 1422 1423 fmris = [] 1424 notfound = [] 1425 multiple_matches = [] 1426 illegals = [] 1427 1428 ppub = self.__img.get_preferred_publisher() 1429 if local: 1430 fmris, notfound, illegals = \ 1431 self.__img.installed_fmris_from_args(fmri_strings) 1432 if not fmris and not notfound and not illegals: 1433 self.log_operation_end( 1434 result=history.RESULT_NOTHING_TO_DO) 1435 raise api_errors.NoPackagesInstalledException() 1436 else: 1437 # Verify validity of certificates before attempting 1438 # network operations. 1439 self.__cert_verify( 1440 log_op_end=[api_errors.CertificateError]) 1441 1442 # XXX This loop really needs not to be copied from 1443 # Image.make_install_plan()! 1444 for p in fmri_strings: 1445 try: 1446 matches = list(self.__img.inventory([p], 1447 all_known=True, ordered=False)) 1448 except api_errors.InventoryException, e: 1449 assert(len(e.notfound) == 1 or \ 1450 len(e.illegal) == 1) 1451 if e.notfound: 1452 notfound.append(e.notfound[0]) 1453 else: 1454 illegals.append(e.illegal[0]) 1455 err = 1 1456 continue 1457 1458 pnames = {} 1459 pmatch = [] 1460 npnames = {} 1461 npmatch = [] 1462 for m, state in matches: 1463 if m.get_publisher() == ppub: 1464 pnames[m.get_pkg_stem()] = 1 1465 pmatch.append((m, state)) 1466 else: 1467 npnames[m.get_pkg_stem()] = 1 1468 npmatch.append((m, state)) 1469 1470 if len(pnames.keys()) > 1: 1471 multiple_matches.append( 1472 (p, pnames.keys())) 1473 error = 1 1474 continue 1475 elif len(pnames.keys()) < 1 and \ 1476 len(npnames.keys()) > 1: 1477 multiple_matches.append( 1478 (p, pnames.keys())) 1479 error = 1 1480 continue 1481 1482 # matches is a list reverse sorted by version, 1483 # so take the first; i.e., the latest. 1484 if len(pmatch) > 0: 1485 fmris.append(pmatch[0]) 1486 else: 1487 fmris.append(npmatch[0]) 1488 1489 if local: 1490 img_cat = self.__img.get_catalog( 1491 self.__img.IMG_CATALOG_INSTALLED) 1492 else: 1493 img_cat = self.__img.get_catalog( 1494 self.__img.IMG_CATALOG_KNOWN) 1495 excludes = self.__img.list_excludes() 1496 1497 # Set of options that can use catalog data. 1498 cat_opts = frozenset([PackageInfo.SUMMARY, 1499 PackageInfo.CATEGORIES, PackageInfo.DESCRIPTION, 1500 PackageInfo.DEPENDENCIES]) 1501 1502 # Set of options that require manifest retrieval. 1503 act_opts = PackageInfo.ACTION_OPTIONS - \ 1504 frozenset([PackageInfo.DEPENDENCIES]) 1505 1506 pis = [] 1507 for f, fstate in fmris: 1508 pub = name = version = release = None 1509 build_release = branch = packaging_date = None 1510 if PackageInfo.IDENTITY in info_needed: 1511 pub, name, version = f.tuple() 1512 pub = fmri.strip_pub_pfx(pub) 1513 release = version.release 1514 build_release = version.build_release 1515 branch = version.branch 1516 packaging_date = \ 1517 version.get_timestamp().strftime("%c") 1518 1519 pref_pub = None 1520 if PackageInfo.PREF_PUBLISHER in info_needed: 1521 pref_pub = (f.get_publisher() == ppub) 1522 1523 # XXX gross; info needs to switch to using get_pkg_list 1524 # routines at a later date once matching is figured 1525 # out. 1526 states = set() 1527 if PackageInfo.STATE in info_needed: 1528 if fstate["state"] == \ 1529 self.__img.PKG_STATE_INSTALLED: 1530 states.add(PackageInfo.INSTALLED) 1531 if fstate["in_catalog"]: 1532 states.add(PackageInfo.KNOWN) 1533 if fstate["upgradable"]: 1534 states.add(PackageInfo.UPGRADABLE) 1535 if fstate["obsolete"]: 1536 states.add(PackageInfo.OBSOLETE) 1537 if fstate["renamed"]: 1538 states.add(PackageInfo.RENAMED) 1539 1540 links = hardlinks = files = dirs = dependencies = None 1541 summary = size = licenses = cat_info = description = \ 1542 None 1543 1544 if cat_opts & info_needed: 1545 summary, description, cat_info, dependencies = \ 1546 _get_pkg_cat_data(img_cat, info_needed, 1547 excludes=excludes, pfmri=f) 1548 if cat_info is not None: 1549 cat_info = [ 1550 PackageCategory(scheme, cat) 1551 for scheme, cat in cat_info 1552 ] 1553 1554 if (frozenset([PackageInfo.SIZE, 1555 PackageInfo.LICENSES]) | act_opts) & info_needed: 1556 mfst = self.__img.get_manifest(f) 1557 if PackageInfo.LICENSES in info_needed: 1558 licenses = self.__licenses(f, mfst) 1559 1560 if PackageInfo.SIZE in info_needed: 1561 size = mfst.get_size(excludes=excludes) 1562 1563 if act_opts & info_needed: 1564 if PackageInfo.LINKS in info_needed: 1565 links = list( 1566 mfst.gen_key_attribute_value_by_type( 1567 "link", excludes)) 1568 if PackageInfo.HARDLINKS in info_needed: 1569 hardlinks = list( 1570 mfst.gen_key_attribute_value_by_type( 1571 "hardlink", excludes)) 1572 if PackageInfo.FILES in info_needed: 1573 files = list( 1574 mfst.gen_key_attribute_value_by_type( 1575 "file", excludes)) 1576 if PackageInfo.DIRS in info_needed: 1577 dirs = list( 1578 mfst.gen_key_attribute_value_by_type( 1579 "dir", excludes)) 1580 1581 pis.append(PackageInfo(pkg_stem=name, summary=summary, 1582 category_info_list=cat_info, states=states, 1583 publisher=pub, preferred_publisher=pref_pub, 1584 version=release, build_release=build_release, 1585 branch=branch, packaging_date=packaging_date, 1586 size=size, pfmri=str(f), licenses=licenses, 1587 links=links, hardlinks=hardlinks, files=files, 1588 dirs=dirs, dependencies=dependencies, 1589 description=description)) 1590 if pis: 1591 self.log_operation_end() 1592 elif illegals or multiple_matches: 1593 self.log_operation_end( 1594 result=history.RESULT_FAILED_BAD_REQUEST) 1595 else: 1596 self.log_operation_end( 1597 result=history.RESULT_NOTHING_TO_DO) 1598 return { 1599 self.INFO_FOUND: pis, 1600 self.INFO_MISSING: notfound, 1601 self.INFO_MULTI_MATCH: multiple_matches, 1602 self.INFO_ILLEGALS: illegals 1603 } 1604 1605 def can_be_canceled(self): 1606 """Returns true if the API is in a cancelable state.""" 1607 return self.__can_be_canceled 1608 1609 def __disable_cancel(self): 1610 """Sets_can_be_canceled to False in a way that prevents missed 1611 wakeups. This may raise CanceledException, if a 1612 cancellation is pending.""" 1613 1614 self.__cancel_lock.acquire() 1615 if self.__canceling: 1616 self.__cancel_lock.release() 1617 self.__img.transport.reset() 1618 raise api_errors.CanceledException() 1619 else: 1620 self.__set_can_be_canceled(False) 1621 self.__cancel_lock.release() 1622 1623 def __enable_cancel(self): 1624 """Sets can_be_canceled to True while grabbing the cancel 1625 locks. The caller must still hold the activity lock while 1626 calling this function.""" 1627 1628 self.__cancel_lock.acquire() 1629 self.__set_can_be_canceled(True) 1630 self.__cancel_lock.release() 1631 1632 def __set_can_be_canceled(self, status): 1633 """Private method. Handles the details of changing the 1634 cancelable state.""" 1635 assert self.__cancel_lock._is_owned() 1636 1637 # If caller requests a change to current state there is 1638 # nothing to do. 1639 if self.__can_be_canceled == status: 1640 return 1641 1642 if status == True: 1643 # Callers must hold activity lock for operations 1644 # that they will make cancelable. 1645 assert self.__activity_lock._is_owned() 1646 # In any situation where the caller holds the activity 1647 # lock and wants to set cancelable to true, a cancel 1648 # should not already be in progress. This is because 1649 # it should not be possible to invoke cancel until 1650 # this routine has finished. Assert that we're not 1651 # canceling. 1652 assert not self.__canceling 1653 1654 self.__can_be_canceled = status 1655 if self.__cancel_state_callable: 1656 self.__cancel_state_callable(self.__can_be_canceled) 1657 1658 def reset(self): 1659 """Resets the API back the the initial state. Note: 1660 this does not necessarily return the disk to its initial state 1661 since the indexes or download cache may have been changed by 1662 the prepare method.""" 1663 self.__acquire_activity_lock() 1664 self.__reset_unlock() 1665 self.__activity_lock.release() 1666 1667 def __reset_unlock(self): 1668 """Private method. Provides a way to reset without taking the 1669 activity lock. Should only be called by a thread which already 1670 holds the activity lock.""" 1671 1672 assert self.__activity_lock._is_owned() 1673 1674 # This needs to be done first so that find_root can use it. 1675 self.__progresstracker.reset() 1676 1677 self.__img.cleanup_downloads() 1678 self.__img.transport.shutdown() 1679 # Recreate the image object using the path the api 1680 # object was created with instead of the current path. 1681 self.__img = image.Image(self.__img_path, 1682 progtrack=self.__progresstracker) 1683 self.__img.blocking_locks = self.__blocking_locks 1684 1685 self.__plan_desc = None 1686 self.__plan_type = None 1687 self.__prepared = False 1688 self.__executed = False 1689 self.__be_name = None 1690 1691 self.__cancel_cleanup_exception() 1692 1693 def __check_cancelation(self): 1694 """Private method. Provides a callback method for internal 1695 code to use to determine whether the current action has been 1696 canceled.""" 1697 return self.__canceling 1698 1699 def __cancel_cleanup_exception(self): 1700 """A private method that is called from exception handlers. 1701 This is not needed if the method calls reset unlock, 1702 which will call this method too. This catches the case 1703 where a caller might have called cancel and gone to sleep, 1704 but the requested operation failed with an exception before 1705 it could raise a CanceledException.""" 1706 1707 self.__cancel_lock.acquire() 1708 self.__set_can_be_canceled(False) 1709 self.__canceling = False 1710 # Wake up any threads that are waiting on this aborted 1711 # operation. 1712 self.__cancel_cv.notify_all() 1713 self.__cancel_lock.release() 1714 1715 def __cancel_done(self): 1716 """A private method that wakes any threads that have been 1717 sleeping, waiting for a cancellation to finish.""" 1718 1719 self.__cancel_lock.acquire() 1720 if self.__canceling: 1721 self.__canceling = False 1722 self.__cancel_cv.notify_all() 1723 self.__cancel_lock.release() 1724 1725 def cancel(self): 1726 """Used for asynchronous cancelation. It returns the API 1727 to the state it was in prior to the current method being 1728 invoked. Canceling during a plan phase returns the API to 1729 its initial state. Canceling during prepare puts the API 1730 into the state it was in just after planning had completed. 1731 Plan execution cannot be canceled. A call to this method blocks 1732 until the cancellation has happened. Note: this does not 1733 necessarily return the disk to its initial state since the 1734 indexes or download cache may have been changed by the 1735 prepare method.""" 1736 1737 self.__cancel_lock.acquire() 1738 1739 if not self.__can_be_canceled: 1740 self.__cancel_lock.release() 1741 return False 1742 1743 self.__set_can_be_canceled(False) 1744 self.__canceling = True 1745 # Wait until the cancelled operation wakes us up. 1746 self.__cancel_cv.wait() 1747 self.__canceling = False 1748 self.__cancel_lock.release() 1749 return True 1750 1751 def __set_history_PlanCreationException(self, e): 1752 if e.unmatched_fmris or e.multiple_matches or \ 1753 e.missing_matches or e.illegal: 1754 self.log_operation_end(error=e, 1755 result=history.RESULT_FAILED_BAD_REQUEST) 1756 else: 1757 self.log_operation_end(error=e) 1758 1759 def local_search(self, query_lst): 1760 """local_search takes a list of Query objects and performs 1761 each query against the installed packages of the image.""" 1762 1763 l = query_p.QueryLexer() 1764 l.build() 1765 qp = query_p.QueryParser(l) 1766 ssu = None 1767 for i, q in enumerate(query_lst): 1768 try: 1769 query = qp.parse(q.encoded_text()) 1770 query_rr = qp.parse(q.encoded_text()) 1771 if query_rr.remove_root(self.__img.root): 1772 query.add_or(query_rr) 1773 except query_p.BooleanQueryException, e: 1774 raise api_errors.BooleanQueryException(e) 1775 except query_p.ParseError, e: 1776 raise api_errors.ParseError(e) 1777 self.__img.update_index_dir() 1778 assert self.__img.index_dir 1779 try: 1780 query.set_info(num_to_return=q.num_to_return, 1781 start_point=q.start_point, 1782 index_dir=self.__img.index_dir, 1783 get_manifest_path=\ 1784 self.__img.get_manifest_path, 1785 gen_installed_pkg_names=\ 1786 self.__img.gen_installed_pkg_names, 1787 case_sensitive=q.case_sensitive) 1788 res = query.search( 1789 self.__img.gen_installed_pkgs, 1790 self.__img.get_manifest_path, 1791 self.__img.list_excludes()) 1792 except search_errors.InconsistentIndexException, e: 1793 raise api_errors.InconsistentIndexException(e) 1794 # i is being inserted to track which query the results 1795 # are for. None is being inserted since there is no 1796 # publisher being searched against. 1797 try: 1798 for r in res: 1799 yield i, None, r 1800 except api_errors.SlowSearchUsed, e: 1801 ssu = e 1802 if ssu: 1803 raise ssu 1804 1805 @staticmethod 1806 def __parse_v_0(line, pub, v): 1807 """This function parses the string returned by a version 0 1808 search server and puts it into the expected format of 1809 (query_number, publisher, (version, return_type, (results))). 1810 1811 "query_number" in the return value is fixed at 0 since search 1812 v0 servers cannot accept multiple queries in a single HTTP 1813 request.""" 1814 1815 line = line.strip() 1816 fields = line.split(None, 3) 1817 return (0, pub, (v, Query.RETURN_ACTIONS, (fields[:4]))) 1818 1819 @staticmethod 1820 def __parse_v_1(line, pub, v): 1821 """This function parses the string returned by a version 1 1822 search server and puts it into the expected format of 1823 (query_number, publisher, (version, return_type, (results))) 1824 If it receives a line it can't parse, it raises a 1825 ServerReturnError.""" 1826 1827 fields = line.split(None, 2) 1828 if len(fields) != 3: 1829 raise api_errors.ServerReturnError(line) 1830 try: 1831 return_type = int(fields[1]) 1832 query_num = int(fields[0]) 1833 except ValueError: 1834 raise api_errors.ServerReturnError(line) 1835 if return_type == Query.RETURN_ACTIONS: 1836 subfields = fields[2].split(None, 2) 1837 return (query_num, pub, (v, return_type, 1838 (subfields[0], urllib.unquote(subfields[1]), 1839 subfields[2]))) 1840 elif return_type == Query.RETURN_PACKAGES: 1841 return (query_num, pub, (v, return_type, fields[2])) 1842 else: 1843 raise api_errors.ServerReturnError(line) 1844 1845 def remote_search(self, query_str_and_args_lst, servers=None): 1846 """This function takes a list of Query objects, and optionally 1847 a list of servers to search against. It performs each query 1848 against each server and yields the results in turn. If no 1849 servers are provided, the search is conducted against all 1850 active servers known by the image. 1851 1852 The servers argument is a list of servers in two possible 1853 forms: the old deprecated form of a publisher, in a 1854 dictionary, or a Publisher object. 1855 1856 A call to this function returns a generator that holds 1857 API locks. Callers must either iterate through all of the 1858 results, or call close() on the resulting object. Otherwise 1859 it is possible to get deadlocks or NRLock reentrance 1860 exceptions.""" 1861 1862 clean_exit = True 1863 canceled = False 1864 1865 self.__acquire_activity_lock() 1866 self.__enable_cancel() 1867 try: 1868 for r in self._remote_search(query_str_and_args_lst, 1869 servers): 1870 yield r 1871 except GeneratorExit: 1872 return 1873 except api_errors.CanceledException: 1874 canceled = True 1875 raise 1876 except Exception: 1877 clean_exit = False 1878 raise 1879 finally: 1880 if canceled: 1881 self.__cancel_done() 1882 elif clean_exit: 1883 try: 1884 self.__disable_cancel() 1885 except api_errors.CanceledException: 1886 self.__cancel_done() 1887 self.__activity_lock.release() 1888 raise 1889 else: 1890 self.__cancel_cleanup_exception() 1891 self.__activity_lock.release() 1892 1893 def _remote_search(self, query_str_and_args_lst, servers=None): 1894 """This is the implementation of remote_search. The other 1895 function is a wrapper that handles locking and exception 1896 handling. This is a generator function.""" 1897 1898 failed = [] 1899 invalid = [] 1900 unsupported = [] 1901 1902 if not servers: 1903 servers = self.__img.gen_publishers() 1904 1905 new_qs = [] 1906 l = query_p.QueryLexer() 1907 l.build() 1908 qp = query_p.QueryParser(l) 1909 for q in query_str_and_args_lst: 1910 try: 1911 query = qp.parse(q.encoded_text()) 1912 query_rr = qp.parse(q.encoded_text()) 1913 if query_rr.remove_root(self.__img.root): 1914 query.add_or(query_rr) 1915 new_qs.append(query_p.Query(str(query), 1916 q.case_sensitive, q.return_type, 1917 q.num_to_return, q.start_point)) 1918 else: 1919 new_qs.append(q) 1920 except query_p.BooleanQueryException, e: 1921 raise api_errors.BooleanQueryException(e) 1922 except query_p.ParseError, e: 1923 raise api_errors.ParseError(e) 1924 1925 query_str_and_args_lst = new_qs 1926 1927 for pub in servers: 1928 descriptive_name = None 1929 1930 if self.__canceling: 1931 raise api_errors.CanceledException() 1932 1933 if isinstance(pub, dict): 1934 origin = pub["origin"] 1935 try: 1936 pub = self.__img.get_publisher( 1937 origin=origin) 1938 except api_errors.UnknownPublisher: 1939 pub = publisher.RepositoryURI(origin) 1940 descriptive_name = origin 1941 1942 if not descriptive_name: 1943 descriptive_name = pub.prefix 1944 1945 try: 1946 res = self.__img.transport.do_search(pub, 1947 query_str_and_args_lst, 1948 ccancel=self.__check_cancelation) 1949 except api_errors.CanceledException: 1950 raise 1951 except api_errors.NegativeSearchResult: 1952 continue 1953 except api_errors.TransportError, e: 1954 failed.append((descriptive_name, e)) 1955 continue 1956 except api_errors.UnsupportedSearchError, e: 1957 unsupported.append((descriptive_name, e)) 1958 continue 1959 except api_errors.MalformedSearchRequest, e: 1960 ex = self._validate_search( 1961 query_str_and_args_lst) 1962 if ex: 1963 raise ex 1964 failed.append((descriptive_name, e)) 1965 continue 1966 1967 try: 1968 if not self.validate_response(res, 1): 1969 invalid.append(descriptive_name) 1970 continue 1971 for line in res: 1972 yield self.__parse_v_1(line, pub, 1) 1973 except api_errors.CanceledException: 1974 raise 1975 except api_errors.TransportError, e: 1976 failed.append((descriptive_name, e)) 1977 continue 1978 1979 if failed or invalid or unsupported: 1980 raise api_errors.ProblematicSearchServers(failed, 1981 invalid, unsupported) 1982 1983 @staticmethod 1984 def __unconvert_return_type(v): 1985 return v == query_p.Query.RETURN_ACTIONS 1986 1987 def _validate_search(self, query_str_lst): 1988 """Called by remote search if server responds that the 1989 request was invalid. In this case, parse the query on 1990 the client-side and determine what went wrong.""" 1991 1992 for q in query_str_lst: 1993 l = query_p.QueryLexer() 1994 l.build() 1995 qp = query_p.QueryParser(l) 1996 try: 1997 query = qp.parse(q.encoded_text()) 1998 except query_p.BooleanQueryException, e: 1999 return api_errors.BooleanQueryException(e) 2000 except query_p.ParseError, e: 2001 return api_errors.ParseError(e) 2002 2003 return None 2004 2005 def rebuild_search_index(self): 2006 """Rebuilds the search indexes. Removes all 2007 existing indexes and replaces them from scratch rather than 2008 performing the incremental update which is usually used. 2009 This is useful for times when the index for the client has 2010 been corrupted.""" 2011 self.__img.update_index_dir() 2012 self.log_operation_start("rebuild-index") 2013 if not os.path.isdir(self.__img.index_dir): 2014 self.__img.mkdirs() 2015 try: 2016 ind = indexer.Indexer(self.__img, self.__img.get_manifest, 2017 self.__img.get_manifest_path, 2018 self.__progresstracker, self.__img.list_excludes()) 2019 ind.rebuild_index_from_scratch( 2020 self.__img.gen_installed_pkgs()) 2021 except search_errors.ProblematicPermissionsIndexException, e: 2022 error = api_errors.ProblematicPermissionsIndexException(e) 2023 self.log_operation_end(error=error) 2024 raise error 2025 except search_errors.MainDictParsingException, e: 2026 error = api_errors.MainDictParsingException(e) 2027 self.log_operation_end(error=error) 2028 self.__img.cleanup_downloads() 2029 raise error 2030 else: 2031 self.log_operation_end() 2032 2033 def get_manifest(self, pfmri, all_variants=True, intent=None): 2034 return self.__img.get_manifest(pfmri) 2035 2036 @staticmethod 2037 def validate_response(res, v): 2038 """This function is used to determine whether the first 2039 line returned from a server is expected. This ensures that 2040 search is really communicating with a search-enabled server.""" 2041 2042 try: 2043 s = res.next() 2044 return s == Query.VALIDATION_STRING[v] 2045 except StopIteration: 2046 return False 2047 2048 def add_publisher(self, pub, refresh_allowed=True): 2049 """Add the provided publisher object to the image 2050 configuration.""" 2051 try: 2052 self.__img.add_publisher(pub, 2053 refresh_allowed=refresh_allowed, 2054 progtrack=self.__progresstracker) 2055 finally: 2056 self.__img.cleanup_downloads() 2057 2058 def get_pub_search_order(self): 2059 """Return current search order of publishers; includes 2060 disabled publishers""" 2061 return self.__img.cfg_cache.publisher_search_order 2062 2063 def set_pub_search_after(self, being_moved_prefix, staying_put_prefix): 2064 """Change the publisher search order so that being_moved is 2065 searched after staying_put""" 2066 self.__img.pub_search_after(being_moved_prefix, staying_put_prefix) 2067 2068 def set_pub_search_before(self, being_moved_prefix, staying_put_prefix): 2069 """Change the publisher search order so that being_moved is 2070 searched before staying_put""" 2071 self.__img.pub_search_before(being_moved_prefix, staying_put_prefix) 2072 2073 def get_preferred_publisher(self): 2074 """Returns the preferred publisher object for the image.""" 2075 return self.get_publisher( 2076 prefix=self.__img.get_preferred_publisher()) 2077 2078 def get_publisher(self, prefix=None, alias=None, duplicate=False): 2079 """Retrieves a publisher object matching the provided prefix 2080 (name) or alias. 2081 2082 'duplicate' is an optional boolean value indicating whether 2083 a copy of the publisher object should be returned instead 2084 of the original. 2085 """ 2086 pub = self.__img.get_publisher(prefix=prefix, alias=alias) 2087 if duplicate: 2088 # Never return the original so that changes to the 2089 # retrieved object are not reflected until 2090 # update_publisher is called. 2091 return copy.copy(pub) 2092 return pub 2093 2094 def get_publisherdata(self, pub=None, repo=None): 2095 """Attempts to retrieve publisher configuration information from 2096 the specified publisher's repository or the provided repository. 2097 If successful, it will either return an empty list (in the case 2098 that the repository supports the operation, but doesn't offer 2099 configuration information) or a list of Publisher objects. 2100 If this operation is not supported by the publisher or the 2101 specified repository, an UnsupportedRepositoryOperation 2102 exception will be raised. 2103 2104 'pub' is an optional Publisher object. 2105 2106 'repo' is an optional RepositoryURI object. 2107 2108 Either 'pub' or 'repo' must be provided.""" 2109 2110 assert (pub or repo) and not (pub and repo) 2111 2112 # Transport accepts either type of object, but a distinction is 2113 # made in the client API for clarity. 2114 pub = max(pub, repo) 2115 2116 self.__activity_lock.acquire() 2117 try: 2118 self.__enable_cancel() 2119 data = self.__img.transport.get_publisherdata(pub, 2120 ccancel=self.__check_cancelation) 2121 self.__disable_cancel() 2122 return data 2123 except api_errors.CanceledException: 2124 self.__cancel_done() 2125 raise 2126 except: 2127 self.__cancel_cleanup_exception() 2128 raise 2129 finally: 2130 self.__img.cleanup_downloads() 2131 self.__activity_lock.release() 2132 2133 def get_publishers(self, duplicate=False): 2134 """Returns a list of the publisher objects for the current 2135 image. 2136 2137 'duplicate' is an optional boolean value indicating whether 2138 copies of the publisher objects should be returned instead 2139 of the originals. 2140 """ 2141 if duplicate: 2142 # Return a copy so that changes to the retrieved objects 2143 # are not reflected until update_publisher is called. 2144 pubs = [ 2145 copy.copy(p) 2146 for p in self.__img.get_publishers().values() 2147 ] 2148 else: 2149 pubs = self.__img.get_publishers().values() 2150 return misc.get_sorted_publishers(pubs, 2151 preferred=self.__img.get_preferred_publisher()) 2152 2153 def get_publisher_last_update_time(self, prefix=None, alias=None): 2154 """Returns a datetime object representing the last time the 2155 catalog for a publisher was modified or None.""" 2156 2157 if alias: 2158 pub = self.get_publisher(alias=alias) 2159 else: 2160 pub = self.get_publisher(prefix=prefix) 2161 2162 if pub.disabled: 2163 return None 2164 2165 dt = None 2166 self.__acquire_activity_lock() 2167 try: 2168 self.__enable_cancel() 2169 try: 2170 dt = pub.catalog.last_modified 2171 except: 2172 self.__reset_unlock() 2173 raise 2174 try: 2175 self.__disable_cancel() 2176 except api_errors.CanceledException: 2177 self.__cancel_done() 2178 raise 2179 finally: 2180 self.__activity_lock.release() 2181 return dt 2182 2183 def has_publisher(self, prefix=None, alias=None): 2184 """Returns a boolean value indicating whether a publisher using 2185 the given prefix or alias exists.""" 2186 return self.__img.has_publisher(prefix=prefix, alias=alias) 2187 2188 def remove_publisher(self, prefix=None, alias=None): 2189 """Removes a publisher object matching the provided prefix 2190 (name) or alias.""" 2191 self.__img.remove_publisher(prefix=prefix, alias=alias, 2192 progtrack=self.__progresstracker) 2193 2194 def set_preferred_publisher(self, prefix=None, alias=None): 2195 """Sets the preferred publisher for the image.""" 2196 self.__img.set_preferred_publisher(prefix=prefix, alias=alias) 2197 2198 def update_publisher(self, pub, refresh_allowed=True): 2199 """Replaces an existing publisher object with the provided one 2200 using the _source_object_id identifier set during copy. 2201 2202 'refresh_allowed' is an optional boolean value indicating 2203 whether a refresh of publisher metadata (such as its catalog) 2204 should be performed if transport information is changed for a 2205 repository, mirror, or origin. If False, no attempt will be 2206 made to retrieve publisher metadata.""" 2207 2208 self.__acquire_activity_lock() 2209 try: 2210 self.__disable_cancel() 2211 with self.__img.locked_op("update-publisher"): 2212 return self.__update_publisher(pub, 2213 refresh_allowed=refresh_allowed) 2214 except api_errors.CanceledException, e: 2215 self.__cancel_done() 2216 raise 2217 finally: 2218 self.__img.cleanup_downloads() 2219 self.__activity_lock.release() 2220 2221 def __update_publisher(self, pub, refresh_allowed=True): 2222 """Private publisher update method; caller responsible for 2223 locking.""" 2224 2225 if pub.disabled and \ 2226 pub.prefix == self.__img.get_preferred_publisher(): 2227 raise api_errors.SetPreferredPublisherDisabled( 2228 pub.prefix) 2229 2230 def origins_changed(oldr, newr): 2231 old_origins = set([ 2232 (o.uri, o.ssl_cert, 2233 o.ssl_key) 2234 for o in oldr.origins 2235 ]) 2236 new_origins = set([ 2237 (o.uri, o.ssl_cert, 2238 o.ssl_key) 2239 for o in newr.origins 2240 ]) 2241 return new_origins - old_origins 2242 2243 def need_refresh(oldo, newo): 2244 if newo.disabled: 2245 # The publisher is disabled, so no refresh 2246 # should be performed. 2247 return False 2248 2249 if oldo.disabled and not newo.disabled: 2250 # The publisher has been re-enabled, so 2251 # retrieve the catalog. 2252 return True 2253 2254 if len(newo.repositories) != len(oldo.repositories): 2255 # If there are an unequal number of repositories 2256 # then some have been added or removed. 2257 return True 2258 2259 oldr = oldo.selected_repository 2260 newr = newo.selected_repository 2261 if newr._source_object_id != id(oldr): 2262 # Selected repository has changed. 2263 return True 2264 return len(origins_changed(oldr, newr)) != 0 2265 2266 refresh_catalog = False 2267 disable = False 2268 orig_pub = None 2269 publishers = self.__img.get_publishers() 2270 2271 # First, attempt to match the updated publisher object to an 2272 # existing one using the object id that was stored during 2273 # copy(). 2274 for key, old in publishers.iteritems(): 2275 if pub._source_object_id == id(old): 2276 # Store the new publisher's id and the old 2277 # publisher object so it can be restored if the 2278 # update operation fails. 2279 orig_pub = (id(pub), old) 2280 break 2281 2282 if not orig_pub: 2283 # If a matching publisher couldn't be found and 2284 # replaced, something is wrong (client api usage 2285 # error). 2286 raise api_errors.UnknownPublisher(pub) 2287 2288 # Next, be certain that the publisher's prefix and alias 2289 # are not already in use by another publisher. 2290 for key, old in publishers.iteritems(): 2291 if pub._source_object_id == id(old): 2292 # Don't check the object we're replacing. 2293 continue 2294 2295 if pub.prefix == old.prefix or \ 2296 pub.prefix == old.alias or \ 2297 pub.alias and (pub.alias == old.alias or 2298 pub.alias == old.prefix): 2299 raise api_errors.DuplicatePublisher(pub) 2300 2301 # Next, determine what needs updating and add the updated 2302 # publisher. 2303 for key, old in publishers.iteritems(): 2304 if pub._source_object_id == id(old): 2305 old = orig_pub[-1] 2306 if need_refresh(old, pub): 2307 refresh_catalog = True 2308 if not old.disabled and pub.disabled: 2309 disable = True 2310 2311 # Now remove the old publisher object using the 2312 # iterator key if the prefix has changed. 2313 if key != pub.prefix: 2314 del publishers[key] 2315 2316 # Prepare the new publisher object. 2317 pub.meta_root = \ 2318 self.__img._get_publisher_meta_root( 2319 pub.prefix) 2320 pub.transport = self.__img.transport 2321 2322 # Finally, add the new publisher object. 2323 publishers[pub.prefix] = pub 2324 break 2325 2326 def cleanup(): 2327 new_id, old_pub = orig_pub 2328 for new_pfx, new_pub in publishers.iteritems(): 2329 if id(new_pub) == new_id: 2330 del publishers[new_pfx] 2331 publishers[old_pub.prefix] = old_pub 2332 break 2333 2334 repo = pub.selected_repository 2335 if not repo.origins: 2336 raise api_errors.PublisherOriginRequired(pub.prefix) 2337 2338 validate = origins_changed(orig_pub[-1].selected_repository, 2339 pub.selected_repository) 2340 2341 try: 2342 if disable: 2343 # Remove the publisher's metadata (such as 2344 # catalogs, etc.). This only needs to be done 2345 # in the event that a publisher is disabled; in 2346 # any other case (the origin changing, etc.), 2347 # refresh() will do the right thing. 2348 self.__img.remove_publisher_metadata(pub) 2349 elif not pub.disabled and not refresh_catalog: 2350 refresh_catalog = pub.needs_refresh 2351 2352 if refresh_catalog and refresh_allowed: 2353 # One of the publisher's repository origins may 2354 # have changed, so the publisher needs to be 2355 # revalidated. 2356 2357 if validate: 2358 self.__img.transport.valid_publisher_test( 2359 pub) 2360 2361 # Validate all new origins against publisher 2362 # configuration. 2363 for uri, ssl_cert, ssl_key in validate: 2364 repo = publisher.RepositoryURI(uri, 2365 ssl_cert=ssl_cert, ssl_key=ssl_key) 2366 pub.validate_config(repo) 2367 2368 self.__refresh(pubs=[pub], immediate=True) 2369 elif refresh_catalog: 2370 # Something has changed (such as a repository 2371 # origin) for the publisher, so a refresh should 2372 # occur, but isn't currently allowed. As such, 2373 # clear the last_refreshed time so that the next 2374 # time the client checks to see if a refresh is 2375 # needed and is allowed, one will be performed. 2376 pub.last_refreshed = None 2377 except Exception, e: 2378 # If any of the above fails, the original publisher 2379 # information needs to be restored so that state is 2380 # consistent. 2381 cleanup() 2382 raise 2383 except: 2384 # If any of the above fails, the original publisher 2385 # information needs to be restored so that state is 2386 # consistent. 2387 cleanup() 2388 raise 2389 2390 # Successful; so save configuration. 2391 self.__img.save_config() 2392 2393 def log_operation_end(self, error=None, result=None): 2394 """Marks the end of an operation to be recorded in image 2395 history. 2396 2397 'result' should be a pkg.client.history constant value 2398 representing the outcome of an operation. If not provided, 2399 and 'error' is provided, the final result of the operation will 2400 be based on the class of 'error' and 'error' will be recorded 2401 for the current operation. If 'result' and 'error' is not 2402 provided, success is assumed.""" 2403 self.__img.history.log_operation_end(error=error, result=result) 2404 2405 def log_operation_error(self, error): 2406 """Adds an error to the list of errors to be recorded in image 2407 history for the current opreation.""" 2408 self.__img.history.log_operation_error(error) 2409 2410 def log_operation_start(self, name): 2411 """Marks the start of an operation to be recorded in image 2412 history.""" 2413 self.__img.history.log_operation_start(name) 2414 2415 def parse_p5i(self, data=None, fileobj=None, location=None): 2416 """Reads the pkg(5) publisher JSON formatted data at 'location' 2417 or from the provided file-like object 'fileobj' and returns a 2418 list of tuples of the format (publisher object, pkg_names). 2419 pkg_names is a list of strings representing package names or 2420 FMRIs. If any pkg_names not specific to a publisher were 2421 provided, the last tuple returned will be of the format (None, 2422 pkg_names). 2423 2424 'data' is an optional string containing the p5i data. 2425 2426 'fileobj' is an optional file-like object that must support a 2427 'read' method for retrieving data. 2428 2429 'location' is an optional string value that should either start 2430 with a leading slash and be pathname of a file or a URI string. 2431 If it is a URI string, supported protocol schemes are 'file', 2432 'ftp', 'http', and 'https'. 2433 2434 'data' or 'fileobj' or 'location' must be provided.""" 2435 2436 return p5i.parse(data=data, fileobj=fileobj, location=location) 2437 2438 def write_p5i(self, fileobj, pkg_names=None, pubs=None): 2439 """Writes the publisher, repository, and provided package names 2440 to the provided file-like object 'fileobj' in JSON p5i format. 2441 2442 'fileobj' is only required to have a 'write' method that accepts 2443 data to be written as a parameter. 2444 2445 'pkg_names' is a dict of lists, tuples, or sets indexed by 2446 publisher prefix that contain package names, FMRI strings, or 2447 package info objects. A prefix of "" can be used for packages 2448 that are not specific to a publisher. 2449 2450 'pubs' is an optional list of publisher prefixes or Publisher 2451 objects. If not provided, the information for all publishers 2452 (excluding those disabled) will be output.""" 2453 2454 if not pubs: 2455 plist = [ 2456 p for p in self.get_publishers() 2457 if not p.disabled 2458 ] 2459 else: 2460 plist = [] 2461 for p in pubs: 2462 if not isinstance(p, publisher.Publisher): 2463 plist.append(self.__img.get_publisher( 2464 prefix=p, alias=p)) 2465 else: 2466 plist.append(p) 2467 2468 # Transform PackageInfo object entries into PkgFmri entries 2469 # before passing them to the p5i module. 2470 new_pkg_names = {} 2471 for pub in pkg_names: 2472 pkglist = [] 2473 for p in pkg_names[pub]: 2474 if isinstance(p, PackageInfo): 2475 pkglist.append(p.fmri) 2476 else: 2477 pkglist.append(p) 2478 new_pkg_names[pub] = pkglist 2479 p5i.write(fileobj, plist, pkg_names=new_pkg_names) 2480 2481 2482 class Query(query_p.Query): 2483 """This class is the object used to pass queries into the api functions. 2484 It encapsulates the possible options available for a query as well as 2485 the text of the query itself.""" 2486 2487 def __init__(self, text, case_sensitive, return_actions=True, 2488 num_to_return=None, start_point=None): 2489 if return_actions: 2490 return_type = query_p.Query.RETURN_ACTIONS 2491 else: 2492 return_type = query_p.Query.RETURN_PACKAGES 2493 query_p.Query.__init__(self, text, case_sensitive, return_type, 2494 num_to_return, start_point) 2495 2496 2497 class PlanDescription(object): 2498 """A class which describes the changes the plan will make.""" 2499 2500 def __init__(self, img): 2501 self.__plan = img.imageplan 2502 self.__img = img 2503 2504 def get_changes(self): 2505 """A generation function that yields tuples of PackageInfo 2506 objects of the form (src_pi, dest_pi). 2507 2508 If 'src_pi' is None, then 'dest_pi' is the package being 2509 installed. 2510 2511 If 'src_pi' is not None, and 'dest_pi' is None, 'src_pi' 2512 is the package being removed. 2513 2514 If 'src_pi' is not None, and 'dest_pi' is not None, 2515 then 'src_pi' is the original version of the package, 2516 and 'dest_pi' is the new version of the package it is 2517 being upgraded to.""" 2518 2519 for pp in self.__plan.pkg_plans: 2520 yield (PackageInfo.build_from_fmri(pp.origin_fmri), 2521 PackageInfo.build_from_fmri(pp.destination_fmri)) 2522 2523 def get_licenses(self, pfmri=None): 2524 """A generator function that yields information about the 2525 licenses related to the current plan in tuples of the form 2526 (dest_fmri, src, dest, accepted, displayed) for the given 2527 package FMRI or all packages in the plan. This is only 2528 available for licenses that are being installed or updated. 2529 2530 'dest_fmri' is the FMRI of the package being installed. 2531 2532 'src' is a LicenseInfo object if the license of the related 2533 package is being updated; otherwise it is None. 2534 2535 'dest' is the LicenseInfo object for the license that is being 2536 installed. 2537 2538 'accepted' is a boolean value indicating that the license has 2539 been marked as accepted for the current plan. 2540 2541 'displayed' is a boolean value indicating that the license has 2542 been marked as displayed for the current plan.""" 2543 2544 for pp in self.__plan.pkg_plans: 2545 dfmri = pp.destination_fmri 2546 if pfmri and dfmri != pfmri: 2547 continue 2548 2549 for lid, entry in pp.get_licenses(): 2550 src = entry["src"] 2551 src_li = None 2552 if src: 2553 src_li = LicenseInfo(pp.origin_fmri, 2554 src, img=self.__img) 2555 2556 dest = entry["dest"] 2557 dest_li = None 2558 if dest: 2559 dest_li = LicenseInfo( 2560 pp.destination_fmri, dest, 2561 img=self.__img) 2562 2563 yield (pp.destination_fmri, src_li, dest_li, 2564 entry["accepted"], entry["displayed"]) 2565 2566 if pfmri: 2567 break 2568 2569 def image_create(pkg_client_name, version_id, root, imgtype, is_zone, 2570 cancel_state_callable=None, facets=misc.EmptyDict, force=False, 2571 mirrors=misc.EmptyI, origins=misc.EmptyI, prefix=None, refresh_allowed=True, 2572 repo_uri=None, ssl_cert=None, ssl_key=None, user_provided_dir=False, 2573 progtrack=None, variants=misc.EmptyDict): 2574 """Creates an image at the specified location. 2575 2576 'pkg_client_name' is a string containing the name of the client, 2577 such as "pkg" or "packagemanager". 2578 2579 'version_id' indicates the version of the api the client is 2580 expecting to use. 2581 2582 'root' is the absolute path of the directory where the image will 2583 be created. If it does not exist, it will be created. 2584 2585 'imgtype' is an IMG_TYPE constant representing the type of image 2586 to create. 2587 2588 'is_zone' is a boolean value indicating whether the image being 2589 created is for a zone. 2590 2591 'cancel_state_callable' is an optional function reference that will 2592 be called if the cancellable status of an operation changes. 2593 2594 'facets' is a dictionary of facet names and values to set during 2595 the image creation process. 2596 2597 'force' is an optional boolean value indicating that if an image 2598 already exists at the specified 'root' that it should be overwritten. 2599 2600 'mirrors' is an optional list of URI strings that should be added to 2601 all publishers configured during image creation as mirrors. 2602 2603 'origins' is an optional list of URI strings that should be added to 2604 all publishers configured during image creation as origins. 2605 2606 'prefix' is an optional publisher prefix to configure as a publisher 2607 for the new image if origins is provided, or to restrict which publisher 2608 will be configured if 'repo_uri' is provided. If this prefix does not 2609 match the publisher configuration retrieved from the repository, an 2610 UnknownRepositoryPublishers exception will be raised. If not provided, 2611 'refresh_allowed' cannot be False. 2612 2613 'refresh_allowed' is an optional boolean value indicating whether 2614 publisher configuration data and metadata can be retrieved during 2615 image creation. If False, 'repo_uri' cannot be specified and 2616 a 'prefix' must be provided. 2617 2618 'repo_uri' is an optional URI string of a package repository to 2619 retrieve publisher configuration information from. If the target 2620 repository supports this, all publishers found will be added to the 2621 image and any origins or mirrors will be added to all of those 2622 publishers. If the target repository does not support this, and a 2623 prefix was not specified, an UnsupportedRepositoryOperation exception 2624 will be raised. If the target repository supports the operation, but 2625 does not provide complete configuration information, a 2626 RepoPubConfigUnavailable exception will be raised. 2627 2628 'ssl_cert' is an optional pathname of an SSL Certificate file to 2629 configure all publishers with and to use when retrieving publisher 2630 configuration information. If provided, 'ssl_key' must also be 2631 provided. The certificate file must be pem-encoded. 2632 2633 'ssl_key' is an optional pathname of an SSL Key file to configure all 2634 publishers with and to use when retrieving publisher configuration 2635 information. If provided, 'ssl_cert' must also be provided. The 2636 key file must be pem-encoded. 2637 2638 'user_provided_dir' is an optional boolean value indicating that the 2639 provided 'root' was user-supplied and that additional error handling 2640 should be enforced. This primarily affects cases where a relative 2641 root has been provided or the root was based on the current working 2642 directory. 2643 2644 'progtrack' is an optional ProgressTracker object. 2645 2646 'variants' is a dictionary of variant names and values to set during 2647 the image creation process. 2648 2649 Callers must provide one of the following when calling this function: 2650 * a 'prefix' and 'repo_uri' (origins and mirrors are optional) 2651 * no 'prefix' and a 'repo_uri' (origins and mirrors are optional) 2652 * a 'prefix' and 'origins' 2653 """ 2654 2655 # Caller must provide a prefix and repository, or no prefix and a 2656 # repository, or a prefix and origins. 2657 assert (prefix and repo_uri) or (not prefix and repo_uri) or (prefix and 2658 origins) 2659 2660 # If prefix isn't provided, and refresh isn't allowed, then auto-config 2661 # cannot be done. 2662 assert not repo_uri or (repo_uri and refresh_allowed) 2663 2664 destroy_root = False 2665 try: 2666 destroy_root = not os.path.exists(root) 2667 except EnvironmentError, e: 2668 if e.errno == errno.EACCES: 2669 raise api_errors.PermissionsException( 2670 e.filename) 2671 raise 2672 2673 # The image object must be created first since transport may be 2674 # needed to retrieve publisher configuration information. 2675 img = image.Image(root, force=force, imgtype=imgtype, 2676 progtrack=progtrack, should_exist=False, 2677 user_provided_dir=user_provided_dir) 2678 2679 api_inst = ImageInterface(img, version_id, 2680 progtrack, cancel_state_callable, pkg_client_name) 2681 2682 try: 2683 if repo_uri: 2684 # Assume auto configuration. 2685 repo = publisher.RepositoryURI(repo_uri, 2686 ssl_cert=ssl_cert, ssl_key=ssl_key) 2687 2688 pubs = None 2689 try: 2690 pubs = api_inst.get_publisherdata(repo=repo) 2691 except api_errors.UnsupportedRepositoryOperation: 2692 if not prefix: 2693 raise api_errors.RepoPubConfigUnavailable( 2694 location=repo_uri) 2695 # For a v0 repo where a prefix was specified, 2696 # fallback to manual configuration. 2697 if not origins: 2698 origins = [repo_uri] 2699 repo_uri = None 2700 2701 if not prefix and not pubs: 2702 # Empty repository configuration. 2703 raise api_errors.RepoPubConfigUnavailable( 2704 location=repo_uri) 2705 2706 if repo_uri: 2707 for p in pubs: 2708 if p.selected_repository: 2709 continue 2710 # Incomplete repository configuration. 2711 raise api_errors.RepoPubConfigUnavailable( 2712 location=repo_uri) 2713 2714 if prefix and not repo_uri: 2715 # Auto-configuration not possible or not requested. 2716 repo = publisher.Repository() 2717 for o in origins: 2718 repo.add_origin(o) 2719 for m in mirrors: 2720 repo.add_mirror(m) 2721 pub = publisher.Publisher(prefix, 2722 repositories=[repo]) 2723 pubs = [pub] 2724 2725 if prefix and prefix not in pubs: 2726 # If publisher prefix requested isn't found in the list 2727 # of publishers at this point, then configuration isn't 2728 # possible. 2729 known = [p.prefix for p in pubs] 2730 raise api_errors.UnknownRepositoryPublishers( 2731 known=known, unknown=[prefix], location=repo_uri) 2732 elif prefix: 2733 # Filter out any publishers that weren't requested. 2734 pubs = [ 2735 p for p in pubs 2736 if p.prefix == prefix 2737 ] 2738 2739 # Add additional origins and mirrors that weren't found in the 2740 # publisher configuration if provided. 2741 for p in pubs: 2742 pr = p.selected_repository 2743 for o in origins: 2744 if not pr.has_origin(o): 2745 pr.add_origin(o) 2746 for m in mirrors: 2747 if not pr.has_mirror(m): 2748 pr.add_mirror(m) 2749 2750 # Set provided SSL Cert/Key for all configured publishers. 2751 for p in pubs: 2752 repo = p.selected_repository 2753 for o in repo.origins: 2754 o.ssl_cert = ssl_cert 2755 o.ssl_key = ssl_key 2756 for m in repo.mirrors: 2757 m.ssl_cert = ssl_cert 2758 m.ssl_key = ssl_key 2759 2760 img.create(pubs, facets=facets, is_zone=is_zone, 2761 progtrack=progtrack, refresh_allowed=refresh_allowed, 2762 variants=variants) 2763 except EnvironmentError, e: 2764 if e.errno == errno.EACCES: 2765 raise api_errors.PermissionsException( 2766 e.filename) 2767 if e.errno == errno.EROFS: 2768 raise api_errors.ReadOnlyFileSystemException( 2769 e.filename) 2770 raise 2771 except: 2772 # Ensure a defunct image isn't left behind. 2773 img.destroy() 2774 if destroy_root and \ 2775 os.path.abspath(root) != "/" and \ 2776 os.path.exists(root): 2777 # Root didn't exist before create and isn't '/', 2778 # so remove it. 2779 shutil.rmtree(root, True) 2780 raise 2781 2782 img.cleanup_downloads() 2783 2784 return api_inst 2785 2786