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