Back to index

apport  2.4
packaging_rpm.py
Go to the documentation of this file.
00001 '''A partial apport.PackageInfo class implementation for RPM.
00002 
00003 Used as a base class for Fedora, RHEL, openSUSE, SUSE Linux, and many other
00004 distributions.
00005 '''
00006 
00007 # Copyright (C) 2007 Red Hat Inc.
00008 # Copyright (C) 2008 Nikolay Derkach
00009 # Author: Will Woods <wwoods@redhat.com>, Nikolay Derkach <nderkach@gmail.com>
00010 #
00011 # This program is free software; you can redistribute it and/or modify it
00012 # under the terms of the GNU General Public License as published by the
00013 # Free Software Foundation; either version 2 of the License, or (at your
00014 # option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
00015 # the full text of the license.
00016 
00017 # N.B. There's some distro-specific bits in here (e.g. is_distro_package()).
00018 # So this is actually an abstract base class (or a template, if you like) for
00019 # RPM-based distributions.
00020 # A proper implementation needs to (at least) set official_keylist to a list
00021 # of GPG keyids used by official packages. You might have to extend
00022 # is_distro_package() as well, if you don't sign all your official packages
00023 # (cough cough Fedora rawhide cough)
00024 
00025 # It'd be convenient to use rpmUtils from yum, but I'm trying to keep this
00026 # class distro-agnostic.
00027 import rpm, hashlib, os, stat, subprocess
00028 
00029 
00030 class RPMPackageInfo:
00031     '''Partial apport.PackageInfo class implementation for RPM, as
00032     found in Fedora, RHEL, CentOS, etc.'''
00033 
00034     # Empty keylist. Should contain a list of key ids (8 lowercase hex digits).
00035     # e.g. official_keylist = ('30c9ecf8','4f2a6fd2','897da07a','1ac70ce6')
00036     official_keylist = ()
00037 
00038     def __init__(self):
00039         self.ts = rpm.TransactionSet()  # connect to the rpmdb
00040         self._mirror = None
00041 
00042     def get_version(self, package):
00043         '''Return the installed version of a package.'''
00044         hdr = self._get_header(package)
00045         if hdr is None:
00046             raise ValueError
00047         # Note - "version" here seems to refer to the full EVR, so..
00048         if not hdr['e']:
00049             return hdr['v'] + '-' + hdr['r']
00050         if not hdr['v'] or not hdr['r']:
00051             return None
00052         else:
00053             return hdr['e'] + ':' + hdr['v'] + '-' + hdr['r']
00054 
00055     def get_available_version(self, package):
00056         '''Return the latest available version of a package.'''
00057         # used in report.py, which is used by the frontends
00058         raise NotImplementedError('method must be implemented by distro-specific RPMPackageInfo subclass')
00059 
00060     def get_dependencies(self, package):
00061         '''Return a list of packages a package depends on.'''
00062         hdr = self._get_header(package)
00063         # parse this package's Requires
00064         reqs = []
00065         for r in hdr['requires']:
00066             if r.startswith('rpmlib') or r.startswith('uname('):
00067                 continue  # we've got rpmlib, thanks
00068             if r[0] == '/':  # file requires
00069                 req_heads = self._get_headers_by_tag('basenames', r)
00070             else:           # other requires
00071                 req_heads = self._get_headers_by_tag('provides', r)
00072             for rh in req_heads:
00073                 rh_envra = self._make_envra_from_header(rh)
00074                 if rh_envra not in reqs:
00075                     reqs.append(rh_envra)
00076         return reqs
00077 
00078     def get_source(self, package):
00079         '''Return the source package name for a package.'''
00080         hdr = self._get_header(package)
00081         return hdr['sourcerpm']
00082 
00083     def get_architecture(self, package):
00084         '''Return the architecture of a package.
00085 
00086         This might differ on multiarch architectures (e. g.  an i386 Firefox
00087         package on a x86_64 system)'''
00088         # Yeah, this is kind of redundant, as package is ENVRA, but I want
00089         # to do this the right way (in case we change what 'package' is)
00090         hdr = self._get_header(package)
00091         return hdr['arch']
00092 
00093     def get_files(self, package):
00094         '''Return list of files shipped by a package.'''
00095         hdr = self._get_header(package)
00096         files = []
00097         for (f, mode) in zip(hdr['filenames'], hdr['filemodes']):
00098             if not stat.S_ISDIR(mode):
00099                 files.append(f)
00100         return files
00101 
00102     def get_modified_files(self, package):
00103         '''Return list of all modified files of a package.'''
00104         hdr = self._get_header(package)
00105 
00106         files = hdr['filenames']
00107         mtimes = hdr['filemtimes']
00108         md5s = hdr['filemd5s']
00109 
00110         modified = []
00111         for i in xrange(len(files)):
00112             # Skip files we're not tracking md5s for
00113             if not md5s[i]:
00114                 continue
00115             # Skip files we can't read
00116             if not os.access(files[i], os.R_OK):
00117                 continue
00118             # Skip things that aren't real files
00119             s = os.stat(files[i])
00120             if not stat.S_ISREG(s.st_mode):
00121                 continue
00122             # Skip things that haven't been modified
00123             if mtimes[i] == s.st_mtime:
00124                 continue
00125             # Oh boy, an actual possibly-modified file. Check the md5sum!
00126             if not self._checkmd5(files[i], md5s[i]):
00127                 modified.append(files[i])
00128 
00129         return modified
00130 
00131     def get_file_package(self, file):
00132         '''Return the package a file belongs to, or None if the file is not
00133         shipped by any package.
00134 
00135         Under normal use, the 'file' argument will always be the executable
00136         that crashed.
00137         '''
00138         # The policy for handling files which belong to multiple packages depends on the distro
00139         raise NotImplementedError('method must be implemented by distro-specific RPMPackageInfo subclass')
00140 
00141     def get_system_architecture(self):
00142         '''Return the architecture of the system, in the notation used by the
00143         particular distribution.'''
00144         rpmarch = subprocess.Popen(['rpm', '--eval', '%_target_cpu'],
00145                                    stdout=subprocess.PIPE)
00146         arch = rpmarch.communicate()[0].strip()
00147         return arch
00148 
00149     def is_distro_package(self, package):
00150         '''Check if a package is a genuine distro package (True) or comes from
00151         a third-party source.'''
00152         # This is a list of official keys, set by the concrete subclass
00153         if not self.official_keylist:
00154             raise Exception('Subclass the RPM implementation for your distro!')
00155         hdr = self._get_header(package)
00156         if not hdr:
00157             return False
00158         # Check the GPG sig and key ID to see if this package was signed
00159         # with an official key.
00160         if hdr['siggpg']:
00161             # Package is signed
00162             keyid = hdr['siggpg'][13:17].encode('hex')
00163             if keyid in self.official_keylist:
00164                 return True
00165         return False
00166 
00167     def set_mirror(self, url):
00168         '''Explicitly set a distribution mirror URL for operations that need to
00169         fetch distribution files/packages from the network.
00170 
00171         By default, the mirror will be read from the system configuration
00172         files.'''
00173         # FIXME C&P from apt-dpkg implementation, might move to subclass
00174         self._mirror = url
00175 
00176     def get_source_tree(self, srcpackage, dir, version=None):
00177         '''Download given source package and unpack it into dir (which should
00178         be empty).
00179 
00180         This also has to care about applying patches etc., so that dir will
00181         eventually contain the actually compiled source.
00182 
00183         If version is given, this particular version will be retrieved.
00184         Otherwise this will fetch the latest available version.
00185 
00186         Return the directory that contains the actual source root directory
00187         (which might be a subdirectory of dir). Return None if the source is
00188         not available.'''
00189         # Used only by apport-retrace.
00190         raise NotImplementedError('method must be implemented by distro-specific RPMPackageInfo subclass')
00191 
00192     def compare_versions(self, ver1, ver2):
00193         '''Compare two package versions.
00194 
00195         Return -1 for ver < ver2, 0 for ver1 == ver2, and 1 for ver1 > ver2.'''
00196         # Used by crashdb.py (i.e. the frontends)
00197         # I could duplicate stringToVersion/compareEVR from rpmUtils.misc,
00198         # but I hate duplicating code. So if you don't want to require rpmUtils
00199         # you can implement this function yourself. Probably you've got
00200         # equivalent code in whatever your distro uses instead of yum anyway.
00201         raise NotImplementedError('method must be implemented by distro-specific RPMPackageInfo subclass')
00202 
00203     def package_name_glob(self, glob):
00204         '''Return known package names which match given glob.'''
00205 
00206         raise NotImplementedError('TODO')
00207 
00208     #
00209     # Internal helper methods. These are only single-underscore, so you can use
00210     # use them in extending/overriding the methods above in your subclasses
00211     #
00212 
00213     def _get_headers_by_tag(self, tag, arg):
00214         '''Get a list of RPM headers by doing dbMatch on the given tag and
00215         argument.'''
00216         matches = self.ts.dbMatch(tag, arg)
00217         if matches.count() == 0:
00218             raise ValueError('Could not find package with %s: %s' % (tag, arg))
00219         return [m for m in matches]
00220 
00221     def _get_header(self, envra):
00222         '''Get the RPM header that matches the given ENVRA.'''
00223 
00224         querystr = envra
00225         qlen = len(envra)
00226         while qlen > 0:
00227             mi = impl.ts.dbMatch('name', querystr)
00228             hdrs = [m for m in mi]
00229             if len(hdrs) > 0:
00230                 # yay! we found something
00231                 # Unless there's some rpmdb breakage, you should have one header
00232                 # here. If you do manage to have two rpms with the same ENVRA,
00233                 # who cares which one you get?
00234                 h = hdrs[0]
00235                 break
00236 
00237             # remove the last char of querystr and retry the search
00238             querystr = querystr[0:len(querystr) - 1]
00239             qlen = qlen - 1
00240 
00241         if qlen == 0:
00242             raise ValueError('No headers found for this envra: %s' % envra)
00243         return h
00244 
00245     def _make_envra_from_header(self, h):
00246         '''Generate an ENVRA string from an rpm header'''
00247 
00248         nvra = "%s-%s-%s.%s" % (h['n'], h['v'], h['r'], h['arch'])
00249         if h['e']:
00250             envra = "%s:%s" % (h['e'], nvra)
00251         else:
00252             envra = nvra
00253         return envra
00254 
00255     def _checkmd5(self, filename, filemd5):
00256         '''Internal function to check a file's md5sum'''
00257 
00258         m = hashlib.md5()
00259         f = open(filename)
00260         data = f.read()
00261         f.close()
00262         m.update(data)
00263         return (filemd5 == m.hexdigest())
00264 
00265 impl = RPMPackageInfo()