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