Home | History | Annotate | Download | only in common_lib
      1 #!/usr/bin/python
      2 """
      3 Software package management library.
      4 
      5 This is an abstraction layer on top of the existing distributions high level
      6 package managers. It supports package operations useful for testing purposes,
      7 and multiple high level package managers (here called backends). If you want
      8 to make this lib to support your particular package manager/distro, please
      9 implement the given backend class.
     10 
     11 @author: Higor Vieira Alves (halves (at] br.ibm.com)
     12 @author: Lucas Meneghel Rodrigues (lmr (at] redhat.com)
     13 @author: Ramon de Carvalho Valle (rcvalle (at] br.ibm.com)
     14 
     15 @copyright: IBM 2008-2009
     16 @copyright: Red Hat 2009-2010
     17 """
     18 import os, re, logging, ConfigParser, optparse, random, string
     19 try:
     20     import yum
     21 except:
     22     pass
     23 import common
     24 from autotest_lib.client.bin import os_dep, utils
     25 from autotest_lib.client.common_lib import error
     26 from autotest_lib.client.common_lib import logging_config, logging_manager
     27 
     28 
     29 def 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 
     44 class 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 
     55 class 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 
    101 class 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 
    222 class 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 
    307 class 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 
    359 class 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 
    491 class 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 
    607 class 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 
    751 if __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