virt.virt_test_utils: run_autotest - 'tar' needs relative paths to strip the leading '/'
[autotest-zwu.git] / client / common_lib / software_manager.py
blobf67f66741ac527737b4b4ff5dbd6203a0b4da04c
1 #!/usr/bin/python
2 """
3 Software package management library.
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.
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)
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
29 def generate_random_string(length):
30 """
31 Return a random string using alphanumeric characters.
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
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)
55 class SystemInspector(object):
56 """
57 System inspector class.
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']
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
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]
97 logging.debug('Package Manager backend: %s' % pm_supported)
98 return pm_supported
101 class SoftwareManager(object):
103 Package management abstraction layer.
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.
109 def __init__(self):
111 Class constructor.
113 Determines the best supported package management system for the given
114 operating system running and initializes the appropriate backend.
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)
129 def check_installed(self, name, version=None, arch=None):
131 Check whether a package is installed on this system.
133 @param name: Package name.
134 @param version: Package version.
135 @param arch: Package architecture.
137 return self.backend.check_installed(name, version, arch)
140 def list_all(self):
142 List all installed packages.
144 return self.backend.list_all()
147 def list_files(self, name):
149 Get a list of all files installed by package [name].
151 @param name: Package name.
153 return self.backend.list_files(name)
156 def install(self, name):
158 Install package [name].
160 @param name: Package name.
162 return self.backend.install(name)
165 def remove(self, name):
167 Remove package [name].
169 @param name: Package name.
171 return self.backend.remove(name)
174 def add_repo(self, url):
176 Add package repo described by [url].
178 @param name: URL of the package repo.
180 return self.backend.add_repo(url)
183 def remove_repo(self, url):
185 Remove package repo described by [url].
187 @param url: URL of the package repo.
189 return self.backend.remove_repo(url)
192 def upgrade(self):
194 Upgrade all packages available.
196 return self.backend.upgrade()
199 def provides(self, file):
201 Returns a list of packages that provides a given capability to the
202 system (be it a binary, a library).
204 @param file: Path to the file.
206 return self.backend.provides(file)
209 def install_what_provides(self, file):
211 Installs package that provides [file].
213 @param file: Path to file.
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)
222 class RpmBackend(object):
224 This class implements operations executed with the rpm package manager.
226 rpm is a lower level package manager, used by higher level managers such
227 as yum and zypper.
229 def __init__(self):
230 self.lowlevel_base_cmd = os_dep.command('rpm')
233 def _check_installed_version(self, name, version):
235 Helper for the check_installed public method.
237 @param name: Package name.
238 @param version: Package version.
240 cmd = (self.lowlevel_base_cmd + ' -q --qf %{VERSION} ' + name +
241 ' 2> /dev/null')
242 inst_version = utils.system_output(cmd)
244 if inst_version >= version:
245 return True
246 else:
247 return False
250 def check_installed(self, name, version=None, arch=None):
252 Check if package [name] is installed.
254 @param name: Package name.
255 @param version: Package version.
256 @param arch: Package architecture.
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')
264 for inst_arch in inst_archs:
265 if inst_arch == arch:
266 return self._check_installed_version(name, version)
267 return False
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)
276 def list_all(self):
278 List all installed packages.
280 installed_packages = utils.system_output('rpm -qa').splitlines()
281 return installed_packages
284 def list_files(self, name):
286 List files installed on the system by package [name].
288 @param name: Package name.
290 path = os.path.abspath(name)
291 if os.path.isfile(path):
292 option = '-qlp'
293 name = path
294 else:
295 option = '-ql'
297 l_cmd = 'rpm' + ' ' + option + ' ' + name + ' 2> /dev/null'
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 []
307 class DpkgBackend(object):
309 This class implements operations executed with the dpkg package manager.
311 dpkg is a lower level package manager, used by higher level managers such
312 as apt and aptitude.
314 def __init__(self):
315 self.lowlevel_base_cmd = os_dep.command('dpkg')
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
333 def list_all(self):
335 List all packages available in the system.
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]))
345 def list_files(self, package):
347 List files installed by package [package].
349 @param package: Package name.
350 @return: List of paths installed by package.
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')
359 class YumBackend(RpmBackend):
361 Implements the yum backend for software manager.
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.
367 def __init__(self):
369 Initializes the base command and the yum package repository.
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()
385 def _cleanup(self):
387 Clean up the yum cache so new package information can be downloaded.
389 utils.system("yum clean all")
392 def install(self, name):
394 Installs package [name]. Handles local installs.
396 if os.path.isfile(name):
397 name = os.path.abspath(name)
398 command = 'localinstall'
399 else:
400 command = 'install'
402 i_cmd = self.base_command + ' ' + command + ' ' + name
404 try:
405 utils.system(i_cmd)
406 return True
407 except:
408 return False
411 def remove(self, name):
413 Removes package [name].
415 @param name: Package name (eg. 'ipython').
417 r_cmd = self.base_command + ' ' + 'erase' + ' ' + name
418 try:
419 utils.system(r_cmd)
420 return True
421 except:
422 return False
425 def add_repo(self, url):
427 Adds package repository located on [url].
429 @param url: Universal Resource Locator of the repository.
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
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)
451 def remove_repo(self, url):
453 Removes package repository located on [url].
455 @param url: Universal Resource Locator of the repository.
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)
464 def upgrade(self):
466 Upgrade all available packages.
468 r_cmd = self.base_command + ' ' + 'update'
469 try:
470 utils.system(r_cmd)
471 return True
472 except:
473 return False
476 def provides(self, name):
478 Returns a list of packages that provides a given capability.
480 @param name: Capability name (eg, 'foo').
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
491 class ZypperBackend(RpmBackend):
493 Implements the zypper backend for software manager.
495 Set of operations for the zypper package manager, found on SUSE Linux.
497 def __init__(self):
499 Initializes the base command and the yum package repository.
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)
509 def install(self, name):
511 Installs package [name]. Handles local installs.
513 @param name: Package Name.
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
524 def add_repo(self, url):
526 Adds repository [url].
528 @param url: URL for the package repository.
530 ar_cmd = self.base_command + ' addrepo ' + url
531 try:
532 utils.system(ar_cmd)
533 return True
534 except:
535 return False
538 def remove_repo(self, url):
540 Removes repository [url].
542 @param url: URL for the package repository.
544 rr_cmd = self.base_command + ' removerepo ' + url
545 try:
546 utils.system(rr_cmd)
547 return True
548 except:
549 return False
552 def remove(self, name):
554 Removes package [name].
556 r_cmd = self.base_command + ' ' + 'erase' + ' ' + name
558 try:
559 utils.system(r_cmd)
560 return True
561 except:
562 return False
565 def upgrade(self):
567 Upgrades all packages of the system.
569 u_cmd = self.base_command + ' update -l'
571 try:
572 utils.system(u_cmd)
573 return True
574 except:
575 return False
578 def provides(self, name):
580 Searches for what provides a given file.
582 @param name: File path.
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
607 class AptBackend(DpkgBackend):
609 Implements the apt backend for software manager.
611 Set of operations for the apt package manager, commonly found on Debian and
612 Debian based distributions, such as Ubuntu Linux.
614 def __init__(self):
616 Initializes the base command and the debian package repository.
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)
628 def install(self, name):
630 Installs package [name].
632 @param name: Package name.
634 command = 'install'
635 i_cmd = self.base_command + ' ' + command + ' ' + name
637 try:
638 utils.system(i_cmd)
639 return True
640 except:
641 return False
644 def remove(self, name):
646 Remove package [name].
648 @param name: Package name.
650 command = 'remove'
651 flag = '--purge'
652 r_cmd = self.base_command + ' ' + command + ' ' + flag + ' ' + name
654 try:
655 utils.system(r_cmd)
656 return True
657 except:
658 return False
661 def add_repo(self, repo):
663 Add an apt repository.
665 @param repo: Repository string. Example:
666 'deb http://archive.ubuntu.com/ubuntu/ maverick universe'
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)
674 def remove_repo(self, repo):
676 Remove an apt repository.
678 @param repo: Repository string. Example:
679 'deb http://archive.ubuntu.com/ubuntu/ maverick universe'
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()
693 def upgrade(self):
695 Upgrade all packages of the system with eventual new versions.
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
712 def provides(self, file):
714 Return a list of packages that provide [file].
716 @param file: File path.
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
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')
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'
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()