1#!/usr/bin/python3 2""" 3Software package management library. 4 5This is an abstraction layer on top of the existing distributions high level 6package managers. It supports package operations useful for testing purposes, 7and multiple high level package managers (here called backends). If you want 8to make this lib to support your particular package manager/distro, please 9implement the given backend class. 10 11@author: Higor Vieira Alves ([email protected]) 12@author: Lucas Meneghel Rodrigues ([email protected]) 13@author: Ramon de Carvalho Valle ([email protected]) 14 15@copyright: IBM 2008-2009 16@copyright: Red Hat 2009-2010 17""" 18import os, re, logging, optparse, random, string 19try: 20 import yum 21except: 22 pass 23import common 24 25from autotest_lib.client.bin import os_dep, utils 26from autotest_lib.client.common_lib import error 27from autotest_lib.client.common_lib import logging_config, logging_manager 28from autotest_lib.client.common_lib import seven 29 30 31def generate_random_string(length): 32 """ 33 Return a random string using alphanumeric characters. 34 35 @length: Length of the string that will be generated. 36 """ 37 r = random.SystemRandom() 38 str = "" 39 chars = string.letters + string.digits 40 while length > 0: 41 str += r.choice(chars) 42 length -= 1 43 return str 44 45 46class SoftwareManagerLoggingConfig(logging_config.LoggingConfig): 47 """ 48 Used with the sole purpose of providing convenient logging setup 49 for the KVM test auxiliary programs. 50 """ 51 def configure_logging(self, results_dir=None, verbose=False): 52 super(SoftwareManagerLoggingConfig, self).configure_logging( 53 use_console=True, 54 verbose=verbose) 55 56 57class SystemInspector(object): 58 """ 59 System inspector class. 60 61 This may grow up to include more complete reports of operating system and 62 machine properties. 63 """ 64 def __init__(self): 65 """ 66 Probe system, and save information for future reference. 67 """ 68 self.distro = utils.get_os_vendor() 69 self.high_level_pms = ['apt-get', 'yum', 'zypper'] 70 71 72 def get_package_management(self): 73 """ 74 Determine the supported package management systems present on the 75 system. If more than one package management system installed, try 76 to find the best supported system. 77 """ 78 list_supported = [] 79 for high_level_pm in self.high_level_pms: 80 try: 81 os_dep.command(high_level_pm) 82 list_supported.append(high_level_pm) 83 except: 84 pass 85 86 pm_supported = None 87 if len(list_supported) == 0: 88 pm_supported = None 89 if len(list_supported) == 1: 90 pm_supported = list_supported[0] 91 elif len(list_supported) > 1: 92 if 'apt-get' in list_supported and self.distro in ['Debian', 'Ubuntu']: 93 pm_supported = 'apt-get' 94 elif 'yum' in list_supported and self.distro == 'Fedora': 95 pm_supported = 'yum' 96 else: 97 pm_supported = list_supported[0] 98 99 logging.debug('Package Manager backend: %s' % pm_supported) 100 return pm_supported 101 102 103class SoftwareManager(object): 104 """ 105 Package management abstraction layer. 106 107 It supports a set of common package operations for testing purposes, and it 108 uses the concept of a backend, a helper class that implements the set of 109 operations of a given package management tool. 110 """ 111 def __init__(self): 112 """ 113 Class constructor. 114 115 Determines the best supported package management system for the given 116 operating system running and initializes the appropriate backend. 117 """ 118 inspector = SystemInspector() 119 backend_type = inspector.get_package_management() 120 if backend_type == 'yum': 121 self.backend = YumBackend() 122 elif backend_type == 'zypper': 123 self.backend = ZypperBackend() 124 elif backend_type == 'apt-get': 125 self.backend = AptBackend() 126 else: 127 raise NotImplementedError('Unimplemented package management ' 128 'system: %s.' % backend_type) 129 130 131 def check_installed(self, name, version=None, arch=None): 132 """ 133 Check whether a package is installed on this system. 134 135 @param name: Package name. 136 @param version: Package version. 137 @param arch: Package architecture. 138 """ 139 return self.backend.check_installed(name, version, arch) 140 141 142 def list_all(self): 143 """ 144 List all installed packages. 145 """ 146 return self.backend.list_all() 147 148 149 def list_files(self, name): 150 """ 151 Get a list of all files installed by package [name]. 152 153 @param name: Package name. 154 """ 155 return self.backend.list_files(name) 156 157 158 def install(self, name): 159 """ 160 Install package [name]. 161 162 @param name: Package name. 163 """ 164 return self.backend.install(name) 165 166 167 def remove(self, name): 168 """ 169 Remove package [name]. 170 171 @param name: Package name. 172 """ 173 return self.backend.remove(name) 174 175 176 def add_repo(self, url): 177 """ 178 Add package repo described by [url]. 179 180 @param name: URL of the package repo. 181 """ 182 return self.backend.add_repo(url) 183 184 185 def remove_repo(self, url): 186 """ 187 Remove package repo described by [url]. 188 189 @param url: URL of the package repo. 190 """ 191 return self.backend.remove_repo(url) 192 193 194 def upgrade(self): 195 """ 196 Upgrade all packages available. 197 """ 198 return self.backend.upgrade() 199 200 201 def provides(self, file): 202 """ 203 Returns a list of packages that provides a given capability to the 204 system (be it a binary, a library). 205 206 @param file: Path to the file. 207 """ 208 return self.backend.provides(file) 209 210 211 def install_what_provides(self, file): 212 """ 213 Installs package that provides [file]. 214 215 @param file: Path to file. 216 """ 217 provides = self.provides(file) 218 if provides is not None: 219 self.install(provides) 220 else: 221 logging.warning('No package seems to provide %s', file) 222 223 224class RpmBackend(object): 225 """ 226 This class implements operations executed with the rpm package manager. 227 228 rpm is a lower level package manager, used by higher level managers such 229 as yum and zypper. 230 """ 231 def __init__(self): 232 self.lowlevel_base_cmd = os_dep.command('rpm') 233 234 235 def _check_installed_version(self, name, version): 236 """ 237 Helper for the check_installed public method. 238 239 @param name: Package name. 240 @param version: Package version. 241 """ 242 cmd = (self.lowlevel_base_cmd + ' -q --qf %{VERSION} ' + name + 243 ' 2> /dev/null') 244 inst_version = utils.system_output(cmd) 245 246 if inst_version >= version: 247 return True 248 else: 249 return False 250 251 252 def check_installed(self, name, version=None, arch=None): 253 """ 254 Check if package [name] is installed. 255 256 @param name: Package name. 257 @param version: Package version. 258 @param arch: Package architecture. 259 """ 260 if arch: 261 cmd = (self.lowlevel_base_cmd + ' -q --qf %{ARCH} ' + name + 262 ' 2> /dev/null') 263 inst_archs = utils.system_output(cmd) 264 inst_archs = inst_archs.split('\n') 265 266 for inst_arch in inst_archs: 267 if inst_arch == arch: 268 return self._check_installed_version(name, version) 269 return False 270 271 elif version: 272 return self._check_installed_version(name, version) 273 else: 274 cmd = 'rpm -q ' + name + ' 2> /dev/null' 275 return (os.system(cmd) == 0) 276 277 278 def list_all(self): 279 """ 280 List all installed packages. 281 """ 282 installed_packages = utils.system_output('rpm -qa').splitlines() 283 return installed_packages 284 285 286 def list_files(self, name): 287 """ 288 List files installed on the system by package [name]. 289 290 @param name: Package name. 291 """ 292 path = os.path.abspath(name) 293 if os.path.isfile(path): 294 option = '-qlp' 295 name = path 296 else: 297 option = '-ql' 298 299 l_cmd = 'rpm' + ' ' + option + ' ' + name + ' 2> /dev/null' 300 301 try: 302 result = utils.system_output(l_cmd) 303 list_files = result.split('\n') 304 return list_files 305 except error.CmdError: 306 return [] 307 308 309class DpkgBackend(object): 310 """ 311 This class implements operations executed with the dpkg package manager. 312 313 dpkg is a lower level package manager, used by higher level managers such 314 as apt and aptitude. 315 """ 316 def __init__(self): 317 self.lowlevel_base_cmd = os_dep.command('dpkg') 318 319 320 def check_installed(self, name): 321 if os.path.isfile(name): 322 n_cmd = (self.lowlevel_base_cmd + ' -f ' + name + 323 ' Package 2>/dev/null') 324 name = utils.system_output(n_cmd) 325 i_cmd = self.lowlevel_base_cmd + ' -s ' + name + ' 2>/dev/null' 326 # Checking if package is installed 327 package_status = utils.system_output(i_cmd, ignore_status=True) 328 not_inst_pattern = re.compile('not-installed', re.IGNORECASE) 329 dpkg_not_installed = re.search(not_inst_pattern, package_status) 330 if dpkg_not_installed: 331 return False 332 return True 333 334 335 def list_all(self): 336 """ 337 List all packages available in the system. 338 """ 339 installed_packages = [] 340 raw_list = utils.system_output('dpkg -l').splitlines()[5:] 341 for line in raw_list: 342 parts = line.split() 343 if parts[0] == "ii": # only grab "installed" packages 344 installed_packages.append("%s-%s" % (parts[1], parts[2])) 345 346 347 def list_files(self, package): 348 """ 349 List files installed by package [package]. 350 351 @param package: Package name. 352 @return: List of paths installed by package. 353 """ 354 if os.path.isfile(package): 355 l_cmd = self.lowlevel_base_cmd + ' -c ' + package 356 else: 357 l_cmd = self.lowlevel_base_cmd + ' -l ' + package 358 return utils.system_output(l_cmd).split('\n') 359 360 361class YumBackend(RpmBackend): 362 """ 363 Implements the yum backend for software manager. 364 365 Set of operations for the yum package manager, commonly found on Yellow Dog 366 Linux and Red Hat based distributions, such as Fedora and Red Hat 367 Enterprise Linux. 368 """ 369 def __init__(self): 370 """ 371 Initializes the base command and the yum package repository. 372 """ 373 super(YumBackend, self).__init__() 374 executable = os_dep.command('yum') 375 base_arguments = '-y' 376 self.base_command = executable + ' ' + base_arguments 377 self.repo_file_path = '/etc/yum.repos.d/autotest.repo' 378 self.cfgparser = seven.config_parser() 379 self.cfgparser.read(self.repo_file_path) 380 y_cmd = executable + ' --version | head -1' 381 self.yum_version = utils.system_output(y_cmd, ignore_status=True) 382 logging.debug('Yum backend initialized') 383 logging.debug('Yum version: %s' % self.yum_version) 384 self.yum_base = yum.YumBase() 385 386 387 def _cleanup(self): 388 """ 389 Clean up the yum cache so new package information can be downloaded. 390 """ 391 utils.system("yum clean all") 392 393 394 def install(self, name): 395 """ 396 Installs package [name]. Handles local installs. 397 """ 398 if os.path.isfile(name): 399 name = os.path.abspath(name) 400 command = 'localinstall' 401 else: 402 command = 'install' 403 404 i_cmd = self.base_command + ' ' + command + ' ' + name 405 406 try: 407 utils.system(i_cmd) 408 return True 409 except: 410 return False 411 412 413 def remove(self, name): 414 """ 415 Removes package [name]. 416 417 @param name: Package name (eg. 'ipython'). 418 """ 419 r_cmd = self.base_command + ' ' + 'erase' + ' ' + name 420 try: 421 utils.system(r_cmd) 422 return True 423 except: 424 return False 425 426 427 def add_repo(self, url): 428 """ 429 Adds package repository located on [url]. 430 431 @param url: Universal Resource Locator of the repository. 432 """ 433 # Check if we URL is already set 434 for section in self.cfgparser.sections(): 435 for option, value in self.cfgparser.items(section): 436 if option == 'url' and value == url: 437 return True 438 439 # Didn't find it, let's set it up 440 while True: 441 section_name = 'software_manager' + '_' + generate_random_string(4) 442 if not self.cfgparser.has_section(section_name): 443 break 444 self.cfgparser.add_section(section_name) 445 self.cfgparser.set(section_name, 'name', 446 'Repository added by the autotest software manager.') 447 self.cfgparser.set(section_name, 'url', url) 448 self.cfgparser.set(section_name, 'enabled', 1) 449 self.cfgparser.set(section_name, 'gpgcheck', 0) 450 self.cfgparser.write(self.repo_file_path) 451 452 453 def remove_repo(self, url): 454 """ 455 Removes package repository located on [url]. 456 457 @param url: Universal Resource Locator of the repository. 458 """ 459 for section in self.cfgparser.sections(): 460 for option, value in self.cfgparser.items(section): 461 if option == 'url' and value == url: 462 self.cfgparser.remove_section(section) 463 self.cfgparser.write(self.repo_file_path) 464 465 466 def upgrade(self): 467 """ 468 Upgrade all available packages. 469 """ 470 r_cmd = self.base_command + ' ' + 'update' 471 try: 472 utils.system(r_cmd) 473 return True 474 except: 475 return False 476 477 478 def provides(self, name): 479 """ 480 Returns a list of packages that provides a given capability. 481 482 @param name: Capability name (eg, 'foo'). 483 """ 484 d_provides = self.yum_base.searchPackageProvides(args=[name]) 485 provides_list = [key for key in d_provides] 486 if provides_list: 487 logging.info("Package %s provides %s", provides_list[0], name) 488 return str(provides_list[0]) 489 else: 490 return None 491 492 493class ZypperBackend(RpmBackend): 494 """ 495 Implements the zypper backend for software manager. 496 497 Set of operations for the zypper package manager, found on SUSE Linux. 498 """ 499 def __init__(self): 500 """ 501 Initializes the base command and the yum package repository. 502 """ 503 super(ZypperBackend, self).__init__() 504 self.base_command = os_dep.command('zypper') + ' -n' 505 z_cmd = self.base_command + ' --version' 506 self.zypper_version = utils.system_output(z_cmd, ignore_status=True) 507 logging.debug('Zypper backend initialized') 508 logging.debug('Zypper version: %s' % self.zypper_version) 509 510 511 def install(self, name): 512 """ 513 Installs package [name]. Handles local installs. 514 515 @param name: Package Name. 516 """ 517 path = os.path.abspath(name) 518 i_cmd = self.base_command + ' install -l ' + name 519 try: 520 utils.system(i_cmd) 521 return True 522 except: 523 return False 524 525 526 def add_repo(self, url): 527 """ 528 Adds repository [url]. 529 530 @param url: URL for the package repository. 531 """ 532 ar_cmd = self.base_command + ' addrepo ' + url 533 try: 534 utils.system(ar_cmd) 535 return True 536 except: 537 return False 538 539 540 def remove_repo(self, url): 541 """ 542 Removes repository [url]. 543 544 @param url: URL for the package repository. 545 """ 546 rr_cmd = self.base_command + ' removerepo ' + url 547 try: 548 utils.system(rr_cmd) 549 return True 550 except: 551 return False 552 553 554 def remove(self, name): 555 """ 556 Removes package [name]. 557 """ 558 r_cmd = self.base_command + ' ' + 'erase' + ' ' + name 559 560 try: 561 utils.system(r_cmd) 562 return True 563 except: 564 return False 565 566 567 def upgrade(self): 568 """ 569 Upgrades all packages of the system. 570 """ 571 u_cmd = self.base_command + ' update -l' 572 573 try: 574 utils.system(u_cmd) 575 return True 576 except: 577 return False 578 579 580 def provides(self, name): 581 """ 582 Searches for what provides a given file. 583 584 @param name: File path. 585 """ 586 p_cmd = self.base_command + ' what-provides ' + name 587 list_provides = [] 588 try: 589 p_output = utils.system_output(p_cmd).split('\n')[4:] 590 for line in p_output: 591 line = [a.strip() for a in line.split('|')] 592 try: 593 state, pname, type, version, arch, repository = line 594 if pname not in list_provides: 595 list_provides.append(pname) 596 except IndexError: 597 pass 598 if len(list_provides) > 1: 599 logging.warning('More than one package found, ' 600 'opting by the first queue result') 601 if list_provides: 602 logging.info("Package %s provides %s", list_provides[0], name) 603 return list_provides[0] 604 return None 605 except: 606 return None 607 608 609class AptBackend(DpkgBackend): 610 """ 611 Implements the apt backend for software manager. 612 613 Set of operations for the apt package manager, commonly found on Debian and 614 Debian based distributions, such as Ubuntu Linux. 615 """ 616 def __init__(self): 617 """ 618 Initializes the base command and the debian package repository. 619 """ 620 super(AptBackend, self).__init__() 621 executable = os_dep.command('apt-get') 622 self.base_command = executable + ' -y' 623 self.repo_file_path = '/etc/apt/sources.list.d/autotest' 624 self.apt_version = utils.system_output('apt-get -v | head -1', 625 ignore_status=True) 626 logging.debug('Apt backend initialized') 627 logging.debug('apt version: %s' % self.apt_version) 628 629 630 def install(self, name): 631 """ 632 Installs package [name]. 633 634 @param name: Package name. 635 """ 636 command = 'install' 637 i_cmd = self.base_command + ' ' + command + ' ' + name 638 639 try: 640 utils.system(i_cmd) 641 return True 642 except: 643 return False 644 645 646 def remove(self, name): 647 """ 648 Remove package [name]. 649 650 @param name: Package name. 651 """ 652 command = 'remove' 653 flag = '--purge' 654 r_cmd = self.base_command + ' ' + command + ' ' + flag + ' ' + name 655 656 try: 657 utils.system(r_cmd) 658 return True 659 except: 660 return False 661 662 663 def add_repo(self, repo): 664 """ 665 Add an apt repository. 666 667 @param repo: Repository string. Example: 668 'deb http://archive.ubuntu.com/ubuntu/ maverick universe' 669 """ 670 repo_file = open(self.repo_file_path, 'a') 671 repo_file_contents = repo_file.read() 672 if repo not in repo_file_contents: 673 repo_file.write(repo) 674 675 676 def remove_repo(self, repo): 677 """ 678 Remove an apt repository. 679 680 @param repo: Repository string. Example: 681 'deb http://archive.ubuntu.com/ubuntu/ maverick universe' 682 """ 683 repo_file = open(self.repo_file_path, 'r') 684 new_file_contents = [] 685 for line in repo_file.readlines: 686 if not line == repo: 687 new_file_contents.append(line) 688 repo_file.close() 689 new_file_contents = "\n".join(new_file_contents) 690 repo_file.open(self.repo_file_path, 'w') 691 repo_file.write(new_file_contents) 692 repo_file.close() 693 694 695 def upgrade(self): 696 """ 697 Upgrade all packages of the system with eventual new versions. 698 """ 699 ud_command = 'update' 700 ud_cmd = self.base_command + ' ' + ud_command 701 try: 702 utils.system(ud_cmd) 703 except: 704 logging.error("Apt package update failed") 705 up_command = 'upgrade' 706 up_cmd = self.base_command + ' ' + up_command 707 try: 708 utils.system(up_cmd) 709 return True 710 except: 711 return False 712 713 714 def provides(self, file): 715 """ 716 Return a list of packages that provide [file]. 717 718 @param file: File path. 719 """ 720 if not self.check_installed('apt-file'): 721 self.install('apt-file') 722 command = os_dep.command('apt-file') 723 cache_update_cmd = command + ' update' 724 try: 725 utils.system(cache_update_cmd, ignore_status=True) 726 except: 727 logging.error("Apt file cache update failed") 728 fu_cmd = command + ' search ' + file 729 try: 730 provides = utils.system_output(fu_cmd).split('\n') 731 list_provides = [] 732 for line in provides: 733 if line: 734 try: 735 line = line.split(':') 736 package = line[0].strip() 737 path = line[1].strip() 738 if path == file and package not in list_provides: 739 list_provides.append(package) 740 except IndexError: 741 pass 742 if len(list_provides) > 1: 743 logging.warning('More than one package found, ' 744 'opting by the first queue result') 745 if list_provides: 746 logging.info("Package %s provides %s", list_provides[0], file) 747 return list_provides[0] 748 return None 749 except: 750 return None 751 752 753if __name__ == '__main__': 754 parser = optparse.OptionParser( 755 "usage: %prog [install|remove|list-all|list-files|add-repo|remove-repo|" 756 "upgrade|what-provides|install-what-provides] arguments") 757 parser.add_option('--verbose', dest="debug", action='store_true', 758 help='include debug messages in console output') 759 760 options, args = parser.parse_args() 761 debug = options.debug 762 logging_manager.configure_logging(SoftwareManagerLoggingConfig(), 763 verbose=debug) 764 software_manager = SoftwareManager() 765 if args: 766 action = args[0] 767 args = " ".join(args[1:]) 768 else: 769 action = 'show-help' 770 771 if action == 'install': 772 software_manager.install(args) 773 elif action == 'remove': 774 software_manager.remove(args) 775 if action == 'list-all': 776 software_manager.list_all() 777 elif action == 'list-files': 778 software_manager.list_files(args) 779 elif action == 'add-repo': 780 software_manager.add_repo(args) 781 elif action == 'remove-repo': 782 software_manager.remove_repo(args) 783 elif action == 'upgrade': 784 software_manager.upgrade() 785 elif action == 'what-provides': 786 software_manager.provides(args) 787 elif action == 'install-what-provides': 788 software_manager.install_what_provides(args) 789 elif action == 'show-help': 790 parser.print_help() 791