xref: /aosp_15_r20/external/autotest/client/common_lib/software_manager.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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