Package entropy :: Package client :: Package interfaces :: Package package :: Package actions :: Module _manage

Source Code for Module entropy.client.interfaces.package.actions._manage

  1  # -*- coding: utf-8 -*- 
  2  """ 
  3   
  4      @author: Fabio Erculiani <[email protected]> 
  5      @contact: [email protected] 
  6      @copyright: Fabio Erculiani 
  7      @license: GPL-2 
  8   
  9      B{Entropy Package Manager Client Package Interface}. 
 10   
 11  """ 
 12  import errno 
 13  import os 
 14   
 15  from entropy.const import etpConst, const_convert_to_unicode, \ 
 16      const_convert_to_rawstring, const_is_python3 
 17  from entropy.i18n import _ 
 18  from entropy.output import red, purple, teal, brown, darkred, blue, darkgreen 
 19   
 20  import entropy.tools 
 21   
 22  from .. import _content as Content 
 23   
 24  from .action import PackageAction 
 25   
 26   
27 -class _PackageInstallRemoveAction(PackageAction):
28 """ 29 Abstract class that exposes shared functions between install 30 and remove PackageAction classes. 31 """ 32 33 _preserved_libs_enabled = True 34 if os.getenv("ETP_DISABLE_PRESERVED_LIBS"): 35 _preserved_libs_enabled = False 36 37 PRESERVED_LIBS_ENABLED = _preserved_libs_enabled 38
39 - def __init__(self, entropy_client, package_match, opts = None):
40 super(_PackageInstallRemoveAction, self).__init__( 41 entropy_client, package_match, opts = opts) 42 self._meta = None
43
44 - def metadata(self):
45 """ 46 Return the package metadata dict object for manipulation. 47 """ 48 return self._meta
49
50 - def setup(self):
51 """ 52 Overridden from PackageAction. 53 """ 54 raise NotImplementedError()
55
56 - def _run(self):
57 """ 58 Overridden from PackageAction. 59 """ 60 raise NotImplementedError()
61
62 - def _handle_preserved_lib(self, path, atom, preserved_mgr):
63 """ 64 Preserve libraries that would be removed but are still needed by 65 installed packages. This is a safety measure for accidental removals. 66 Proper library dependency ordering should be done during dependencies 67 calculation. 68 """ 69 solved = preserved_mgr.resolve(path) 70 if solved is None: 71 return None 72 73 paths = preserved_mgr.determine(path) 74 75 if paths: 76 77 self._entropy.output( 78 "%s: %s, %s" % ( 79 darkgreen(_("Protecting")), 80 teal(path), 81 darkgreen(_("library needed by:")), 82 ), 83 importance = 1, 84 level = "warning", 85 header = red(" ## ") 86 ) 87 88 library, elfclass, s_path = solved 89 preserved_mgr.register(library, elfclass, s_path, atom) 90 91 installed_package_ids = preserved_mgr.needed(path) 92 installed_repository = preserved_mgr.installed_repository() 93 94 for installed_package_id in installed_package_ids: 95 atom = installed_repository.retrieveAtom(installed_package_id) 96 self._entropy.output( 97 brown(atom), 98 importance = 0, 99 level = "warning", 100 header = darkgreen(" :: ") 101 ) 102 self._entropy.logger.log( 103 "[Package]", 104 etpConst['logging']['normal_loglevel_id'], 105 "Protecting library %s, due to package: %s" % ( 106 path, atom,) 107 ) 108 109 return paths
110
111 - def _garbage_collect_preserved_libs(self, preserved_mgr):
112 """ 113 Garbage collect (and remove) libraries preserved on the system 114 no longer available or no longer needed by any installed package. 115 """ 116 preserved_libs = preserved_mgr.collect() 117 inst_repo = preserved_mgr.installed_repository() 118 119 for library, elfclass, path in preserved_libs: 120 121 self._entropy.output( 122 "%s: %s [%s, %s]" % ( 123 brown(_("Removing library")), 124 darkgreen(path), 125 purple(library), 126 teal(const_convert_to_unicode("%s" % (elfclass,))), 127 ), 128 importance = 0, 129 level = "warning", 130 header = darkgreen(" :: ") 131 ) 132 133 self._entropy.logger.log( 134 "[Package]", 135 etpConst['logging']['normal_loglevel_id'], 136 "%s %s [%s:%s]" % ( 137 const_convert_to_unicode("Removing library"), 138 path, library, elfclass,) 139 ) 140 141 # This will also check if path and it's destinations (in case of 142 # symlink) is owned by other packages. 143 # If this is the case, removal will fail for that specific path. 144 # This may be the case for packages like vmware-workstation, 145 # containing symlinks pointing to system libraries. 146 # See Sabayon bug #5182. 147 remove_failed = preserved_mgr.remove(path) 148 for failed_path, err in remove_failed: 149 self._entropy.output( 150 "%s: %s, %s" % ( 151 purple(_("Failed to remove the library")), 152 darkred(failed_path), 153 err, 154 ), 155 importance = 1, 156 level = "warning", 157 header = brown(" ## ") 158 ) 159 self._entropy.logger.log( 160 "[Package]", 161 etpConst['logging']['normal_loglevel_id'], 162 "Error during %s removal: %s" % (failed_path, err) 163 ) 164 165 preserved_mgr.unregister(library, elfclass, path) 166 167 # commit changes to repository if collected 168 if preserved_libs: 169 inst_repo.commit()
170
171 - def _get_system_root(self, metadata):
172 """ 173 Return the path to the system root directory. 174 """ 175 return metadata.get('unittest_root', "") + etpConst['systemroot']
176
177 - def _get_remove_trigger_data(self, inst_repo, installed_package_id):
178 """ 179 Get the metadata used during removal phases by Triggers. 180 """ 181 data = {} 182 data.update(inst_repo.getTriggerData(installed_package_id)) 183 184 splitdebug_metadata = self._get_splitdebug_metadata() 185 data.update(splitdebug_metadata) 186 187 data['affected_directories'] = self._meta['affected_directories'] 188 data['affected_infofiles'] = self._meta['affected_infofiles'] 189 data['spm_repository'] = inst_repo.retrieveSpmRepository( 190 installed_package_id) 191 192 data['accept_license'] = self._get_licenses( 193 inst_repo, installed_package_id) 194 195 return data
196
197 - def _get_config_protect_skip(self):
198 """ 199 Return the configuration protection path set. 200 """ 201 misc_settings = self._entropy.ClientSettings()['misc'] 202 protectskip = misc_settings['configprotectskip'] 203 204 if not const_is_python3(): 205 protectskip = set(( 206 const_convert_to_rawstring( 207 x, from_enctype = etpConst['conf_encoding']) for x in 208 misc_settings['configprotectskip'])) 209 210 return protectskip
211
212 - def _get_config_protect(self, entropy_repository, package_id, mask = False, 213 _metadata = None):
214 """ 215 Return configuration protection (or mask) metadata for the given 216 package. 217 This method should not be used as source for storing metadata into 218 repositories since the returned objects may not be decoded in utf-8. 219 Data returned by this method is expected to be used only by internal 220 functions. 221 """ 222 misc_data = self._entropy.ClientSettings()['misc'] 223 224 if mask: 225 paths = entropy_repository.retrieveProtectMask(package_id).split() 226 misc_key = "configprotectmask" 227 else: 228 paths = entropy_repository.retrieveProtect(package_id).split() 229 misc_key = "configprotect" 230 231 if _metadata is None: 232 _metadata = self._meta 233 root = self._get_system_root(_metadata) 234 config = set(("%s%s" % (root, path) for path in paths)) 235 config.update(misc_data[misc_key]) 236 237 # os.* methods in Python 2.x do not expect unicode strings 238 # This set of data is only used by _handle_config_protect atm. 239 if not const_is_python3(): 240 config = set((const_convert_to_rawstring(x) for x in config)) 241 242 return config
243
244 - def _get_config_protect_metadata(self, installed_repository, 245 installed_package_id, 246 _metadata=None):
247 """ 248 Get the config_protect+mask metadata object. 249 Make sure to call this before the package goes away from the 250 repository. 251 """ 252 protect = self._get_config_protect( 253 installed_repository, installed_package_id, 254 _metadata = _metadata) 255 mask = self._get_config_protect( 256 installed_repository, installed_package_id, mask = True, 257 _metadata = _metadata) 258 259 metadata = { 260 'config_protect+mask': (protect, mask) 261 } 262 return metadata
263
264 - def _handle_config_protect(self, protect, mask, protectskip, 265 fromfile, tofile, 266 do_allocation_check = True, 267 do_quiet = False):
268 """ 269 Handle configuration file protection. This method contains the logic 270 for determining if a file should be protected from overwrite. 271 """ 272 protected = False 273 do_continue = False 274 in_mask = False 275 276 tofile_os = tofile 277 fromfile_os = fromfile 278 if not const_is_python3(): 279 tofile_os = const_convert_to_rawstring(tofile) 280 fromfile_os = const_convert_to_rawstring(fromfile) 281 282 if tofile in protect: 283 protected = True 284 in_mask = True 285 286 elif os.path.dirname(tofile) in protect: 287 protected = True 288 in_mask = True 289 290 else: 291 tofile_testdir = os.path.dirname(tofile) 292 old_tofile_testdir = None 293 while tofile_testdir != old_tofile_testdir: 294 if tofile_testdir in protect: 295 protected = True 296 in_mask = True 297 break 298 old_tofile_testdir = tofile_testdir 299 tofile_testdir = os.path.dirname(tofile_testdir) 300 301 if protected: # check if perhaps, file is masked, so unprotected 302 303 if tofile in mask: 304 protected = False 305 in_mask = False 306 307 elif os.path.dirname(tofile) in mask: 308 protected = False 309 in_mask = False 310 311 else: 312 tofile_testdir = os.path.dirname(tofile) 313 old_tofile_testdir = None 314 while tofile_testdir != old_tofile_testdir: 315 if tofile_testdir in mask: 316 protected = False 317 in_mask = False 318 break 319 old_tofile_testdir = tofile_testdir 320 tofile_testdir = os.path.dirname(tofile_testdir) 321 322 if not os.path.lexists(tofile_os): 323 protected = False # file doesn't exist 324 325 # check if it's a text file 326 if protected: 327 protected = entropy.tools.istextfile(tofile) 328 in_mask = protected 329 330 if fromfile is not None: 331 if protected and os.path.lexists(fromfile_os) and ( 332 not os.path.exists(fromfile_os)) and ( 333 os.path.islink(fromfile_os)): 334 # broken symlink, don't protect 335 self._entropy.logger.log( 336 "[Package]", 337 etpConst['logging']['normal_loglevel_id'], 338 "WARNING!!! Failed to handle file protection for: " \ 339 "%s, broken symlink in package" % ( 340 tofile, 341 ) 342 ) 343 msg = _("Cannot protect broken symlink") 344 mytxt = "%s:" % ( 345 purple(msg), 346 ) 347 self._entropy.output( 348 mytxt, 349 importance = 1, 350 level = "warning", 351 header = brown(" ## ") 352 ) 353 self._entropy.output( 354 tofile, 355 level = "warning", 356 header = brown(" ## ") 357 ) 358 protected = False 359 360 if not protected: 361 return in_mask, protected, tofile, do_continue 362 363 ## ## 364 # file is protected # 365 ##__________________## 366 367 # check if protection is disabled for this element 368 if tofile in protectskip: 369 self._entropy.logger.log( 370 "[Package]", 371 etpConst['logging']['normal_loglevel_id'], 372 "Skipping config file installation/removal, " \ 373 "as stated in client.conf: %s" % (tofile,) 374 ) 375 if not do_quiet: 376 mytxt = "%s: %s" % ( 377 _("Skipping file installation/removal"), 378 tofile, 379 ) 380 self._entropy.output( 381 mytxt, 382 importance = 1, 383 level = "warning", 384 header = darkred(" ## ") 385 ) 386 do_continue = True 387 return in_mask, protected, tofile, do_continue 388 389 ## ## 390 # file is protected (2) # 391 ##______________________## 392 393 prot_status = True 394 if do_allocation_check: 395 spm_class = self._entropy.Spm_class() 396 tofile, prot_status = spm_class.allocate_protected_file(fromfile, 397 tofile) 398 399 if not prot_status: 400 # a protected file with the same content 401 # is already in place, so not going to protect 402 # the same file twice 403 protected = False 404 return in_mask, protected, tofile, do_continue 405 406 ## ## 407 # file is protected (3) # 408 ##______________________## 409 410 oldtofile = tofile 411 if oldtofile.find("._cfg") != -1: 412 oldtofile = os.path.join(os.path.dirname(oldtofile), 413 os.path.basename(oldtofile)[10:]) 414 415 if not do_quiet: 416 self._entropy.logger.log( 417 "[Package]", 418 etpConst['logging']['normal_loglevel_id'], 419 "Protecting config file: %s" % (oldtofile,) 420 ) 421 mytxt = red("%s: %s") % (_("Protecting config file"), oldtofile,) 422 self._entropy.output( 423 mytxt, 424 importance = 1, 425 level = "warning", 426 header = darkred(" ## ") 427 ) 428 429 return in_mask, protected, tofile, do_continue
430
431 - def _remove_content_from_system_loop(self, inst_repo, remove_atom, 432 remove_content, remove_config, 433 affected_directories, 434 affected_infofiles, 435 directories, directories_cache, 436 preserved_mgr, 437 not_removed_due_to_collisions, 438 colliding_path_messages, 439 automerge_metadata, col_protect, 440 protect, mask, protectskip, 441 sys_root):
442 """ 443 Body of the _remove_content_from_system() method. 444 """ 445 info_dirs = self._get_info_directories() 446 447 # collect all the library paths to be preserved 448 # in the final removal loop. 449 preserved_lib_paths = set() 450 451 if self.PRESERVED_LIBS_ENABLED: 452 for _pkg_id, item, _ftype in remove_content: 453 454 # determine without sys_root 455 paths = self._handle_preserved_lib( 456 item, remove_atom, preserved_mgr) 457 if paths is not None: 458 preserved_lib_paths.update(paths) 459 460 for _pkg_id, item, _ftype in remove_content: 461 462 if not item: 463 continue # empty element?? 464 465 sys_root_item = sys_root + item 466 sys_root_item_encoded = sys_root_item 467 if not const_is_python3(): 468 # this is coming from the db, and it's pure utf-8. 469 # sometimes we need to deal with broken filesystem 470 # or path encodings, so we should try both paths, really. 471 proposed_sys_root_item_encoded = const_convert_to_rawstring( 472 sys_root_item, 473 from_enctype = etpConst['conf_raw_encoding']) 474 proposed_lexists = os.path.lexists(proposed_sys_root_item_encoded) 475 old_lexists = os.path.lexists(sys_root_item_encoded) 476 if proposed_lexists and not old_lexists: 477 sys_root_item_encoded = proposed_sys_root_item_encoded 478 elif not proposed_lexists and old_lexists: 479 # The rawstring version of the path does not exist, while the 480 # original version does. 481 # Do not replace sys_root_item_encoded then. 482 self._entropy.logger.log( 483 "[Package]", 484 etpConst['logging']['verbose_loglevel_id'], 485 "[remove] Package %s contains hostile unicode file paths." % ( 486 remove_atom,)) 487 488 # collision check 489 if col_protect > 0: 490 491 if inst_repo.isFileAvailable(item) \ 492 and os.path.isfile(sys_root_item_encoded): 493 494 # in this way we filter out directories 495 colliding_path_messages.add(sys_root_item) 496 not_removed_due_to_collisions.add(item) 497 continue 498 499 protected = False 500 in_mask = False 501 502 if not remove_config: 503 504 protected_item_test = sys_root_item 505 (in_mask, protected, _x, 506 do_continue) = self._handle_config_protect( 507 protect, mask, protectskip, None, protected_item_test, 508 do_allocation_check = False, do_quiet = True 509 ) 510 511 if do_continue: 512 protected = True 513 514 # when files have not been modified by the user 515 # and they are inside a config protect directory 516 # we could even remove them directly 517 if in_mask: 518 519 oldprot_md5 = automerge_metadata.get(item) 520 if oldprot_md5: 521 522 try: 523 in_system_md5 = entropy.tools.md5sum( 524 protected_item_test) 525 except (OSError, IOError) as err: 526 if err.errno != errno.ENOENT: 527 raise 528 in_system_md5 = "?" 529 530 if oldprot_md5 == in_system_md5: 531 prot_msg = _("Removing config file, never modified") 532 mytxt = "%s: %s" % ( 533 darkgreen(prot_msg), 534 blue(item), 535 ) 536 self._entropy.output( 537 mytxt, 538 importance = 1, 539 level = "info", 540 header = red(" ## ") 541 ) 542 protected = False 543 do_continue = False 544 545 # Is file or directory a protected item? 546 if protected: 547 self._entropy.logger.log( 548 "[Package]", 549 etpConst['logging']['verbose_loglevel_id'], 550 "[remove] Protecting config file: %s" % (sys_root_item,) 551 ) 552 mytxt = "[%s] %s: %s" % ( 553 red(_("remove")), 554 brown(_("Protecting config file")), 555 sys_root_item, 556 ) 557 self._entropy.output( 558 mytxt, 559 importance = 1, 560 level = "warning", 561 header = red(" ## ") 562 ) 563 continue 564 565 try: 566 os.lstat(sys_root_item_encoded) 567 except OSError as err: 568 if err.errno in (errno.ENOENT, errno.ENOTDIR): 569 continue # skip file, does not exist 570 raise 571 572 except UnicodeEncodeError: 573 msg = _("This package contains a badly encoded file !!!") 574 mytxt = brown(msg) 575 self._entropy.output( 576 red("QA: ")+mytxt, 577 importance = 1, 578 level = "warning", 579 header = darkred(" ## ") 580 ) 581 continue # file has a really bad encoding 582 583 if os.path.isdir(sys_root_item_encoded) and \ 584 os.path.islink(sys_root_item_encoded): 585 # S_ISDIR returns False for directory symlinks, 586 # so using os.path.isdir valid directory symlink 587 if sys_root_item not in directories_cache: 588 # collect for Trigger 589 affected_directories.add(item) 590 directories.add((sys_root_item, "link")) 591 directories_cache.add(sys_root_item) 592 continue 593 594 if os.path.isdir(sys_root_item_encoded): 595 # plain directory 596 if sys_root_item not in directories_cache: 597 # collect for Trigger 598 affected_directories.add(item) 599 directories.add((sys_root_item, "dir")) 600 directories_cache.add(sys_root_item) 601 continue 602 603 # files, symlinks or not 604 # just a file or symlink or broken 605 # directory symlink (remove now) 606 607 # skip file removal if item is a preserved library. 608 if item in preserved_lib_paths: 609 self._entropy.logger.log( 610 "[Package]", 611 etpConst['logging']['normal_loglevel_id'], 612 "[remove] skipping removal of: %s" % (sys_root_item,) 613 ) 614 continue 615 616 try: 617 os.remove(sys_root_item_encoded) 618 except OSError as err: 619 self._entropy.logger.log( 620 "[Package]", 621 etpConst['logging']['normal_loglevel_id'], 622 "[remove] Unable to remove %s, error: %s" % ( 623 sys_root_item, err,) 624 ) 625 continue 626 627 # collect for Trigger 628 dir_name = os.path.dirname(item) 629 affected_directories.add(dir_name) 630 631 # account for info files, if any 632 if dir_name in info_dirs: 633 for _ext in self._INFO_EXTS: 634 if item.endswith(_ext): 635 affected_infofiles.add(item) 636 break 637 638 # add its parent directory 639 dirobj = const_convert_to_unicode( 640 os.path.dirname(sys_root_item_encoded)) 641 if dirobj not in directories_cache: 642 if os.path.isdir(dirobj) and os.path.islink(dirobj): 643 directories.add((dirobj, "link")) 644 elif os.path.isdir(dirobj): 645 directories.add((dirobj, "dir")) 646 647 directories_cache.add(dirobj)
648
649 - def _remove_content_from_system(self, installed_repository, 650 remove_atom, remove_config, sys_root, 651 protect_mask, removecontent_file, 652 automerge_metadata, 653 affected_directories, affected_infofiles, 654 preserved_mgr):
655 """ 656 Remove installed package content (files/directories) from live system. 657 658 @keyword automerge_metadata: Entropy "automerge metadata" 659 @type automerge_metadata: dict 660 """ 661 # load CONFIG_PROTECT and CONFIG_PROTECT_MASK 662 misc_settings = self._entropy.ClientSettings()['misc'] 663 col_protect = misc_settings['collisionprotect'] 664 665 # remove files from system 666 directories = set() 667 directories_cache = set() 668 not_removed_due_to_collisions = set() 669 colliding_path_messages = set() 670 671 if protect_mask is not None: 672 protect, mask = protect_mask 673 else: 674 protect, mask = set(), set() 675 protectskip = self._get_config_protect_skip() 676 677 remove_content = None 678 try: 679 # simulate a removecontent list/set object 680 remove_content = [] 681 if removecontent_file is not None: 682 remove_content = Content.FileContentReader( 683 removecontent_file) 684 685 self._remove_content_from_system_loop( 686 installed_repository, remove_atom, 687 remove_content, remove_config, 688 affected_directories, 689 affected_infofiles, 690 directories, directories_cache, 691 preserved_mgr, 692 not_removed_due_to_collisions, colliding_path_messages, 693 automerge_metadata, col_protect, protect, mask, protectskip, 694 sys_root) 695 696 finally: 697 if hasattr(remove_content, "close"): 698 remove_content.close() 699 700 if colliding_path_messages: 701 self._entropy.output( 702 "%s:" % (_("Collision found during removal of"),), 703 importance = 1, 704 level = "warning", 705 header = red(" ## ") 706 ) 707 708 for path in sorted(colliding_path_messages): 709 self._entropy.output( 710 purple(path), 711 importance = 0, 712 level = "warning", 713 header = red(" ## ") 714 ) 715 self._entropy.logger.log( 716 "[Package]", etpConst['logging']['normal_loglevel_id'], 717 "Collision found during removal of %s - cannot overwrite" % ( 718 path,) 719 ) 720 721 # removing files not removed from removecontent. 722 # it happened that boot services not removed due to 723 # collisions got removed from their belonging runlevels 724 # by postremove step. 725 # since this is a set, it is a mapped type, so every 726 # other instance around will feature this update 727 if not_removed_due_to_collisions: 728 def _filter(_path): 729 return _path not in not_removed_due_to_collisions
730 Content.filter_content_file( 731 removecontent_file, _filter) 732 733 # now handle directories 734 directories = sorted(directories, reverse = True) 735 while True: 736 taint = False 737 for directory, dirtype in directories: 738 mydir = "%s%s" % (sys_root, directory,) 739 if dirtype == "link": 740 try: 741 mylist = os.listdir(mydir) 742 if not mylist: 743 try: 744 os.remove(mydir) 745 taint = True 746 except OSError: 747 pass 748 except OSError: 749 pass 750 elif dirtype == "dir": 751 try: 752 mylist = os.listdir(mydir) 753 if not mylist: 754 try: 755 os.rmdir(mydir) 756 taint = True 757 except OSError: 758 pass 759 except OSError: 760 pass 761 762 if not taint: 763 break
764
765 - def _spm_remove_package(self, atom, metadata):
766 """ 767 Call Source Package Manager interface and tell it to remove our 768 just removed package. 769 770 @return: execution status 771 @rtype: int 772 """ 773 spm = self._entropy.Spm() 774 self._entropy.logger.log( 775 "[Package]", 776 etpConst['logging']['normal_loglevel_id'], 777 "Removing from SPM: %s" % (atom,) 778 ) 779 return spm.remove_installed_package(atom, metadata)
780