D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
proc
/
self
/
root
/
lib64
/
python2.7
/
site-packages
/
reportclient
/
Filename :
debuginfo.py
back
Copy
""" This module provides classes and functions used to download and manage debuginfos. """ import sys import os import time import errno import shutil from subprocess import Popen from yum import _, YumBase from yum.callbacks import DownloadBaseCallback from yum.Errors import YumBaseError from reportclient import (_, log1, log2, RETURN_OK, RETURN_FAILURE, RETURN_CANCEL_BY_USER, verbose, ask_yes_no, error_msg) TMPDIR="" def ensure_abrt_uid(fn): """ Ensures that the function is called using abrt's uid and gid Returns: Either an unchanged function object or a wrapper function object for the function. """ import pwd current_uid = os.getuid() current_gid = os.getgid() abrt = pwd.getpwnam("abrt") # if we're are already running as abrt, don't do anything if abrt.pw_uid == current_uid and abrt.pw_gid == current_gid: return fn def wrapped(*args, **kwargs): """ Wrapper function around the called function. Sets up uid and gid to match abrt's and after the function finishes rolls its uid and gid back. Returns: Return value of the wrapped function. """ # switch to abrt os.setegid(abrt.pw_gid) os.seteuid(abrt.pw_uid) # extract the files as abrt:abrt retval = fn(*args, **kwargs) # switch back to whatever we were os.seteuid(current_uid) os.setegid(current_gid) return retval return wrapped # TODO: unpack just required debuginfo and not entire rpm? # ..that can lead to: foo.c No such file and directory # files is not used... @ensure_abrt_uid def unpack_rpm(package_file_name, files, tmp_dir, destdir, keeprpm, exact_files=False): """ Unpacks a single rpm located in tmp_dir into destdir. Arguments: package_file_name - name of the rpm file files - files to extract from the rpm tmp_dir - temporary directory where the rpm file is located destdir - destination directory for the rpm package extraction keeprpm - check if the user wants to delete rpms from the tmp directory exact_files - extract only specified files Returns: RETURN_FAILURE in case of a serious problem """ package_full_path = tmp_dir + "/" + package_file_name log1("Extracting %s to %s", package_full_path, destdir) log2("%s", files) print _("Extracting cpio from {0}").format(package_full_path) unpacked_cpio_path = tmp_dir + "/unpacked.cpio" try: unpacked_cpio = open(unpacked_cpio_path, 'wb') except IOError, ex: print _("Can't write to '{0}': {1}").format(unpacked_cpio_path, ex) return RETURN_FAILURE rpm2cpio = Popen(["rpm2cpio", package_full_path], stdout = unpacked_cpio, bufsize = -1) retcode = rpm2cpio.wait() if retcode == 0: log1("cpio written OK") if not keeprpm: log1("keeprpms = False, removing %s", package_full_path) #print _("Removing temporary rpm file") os.unlink(package_full_path) else: unpacked_cpio.close() print _("Can't extract package '{0}'").format(package_full_path) return RETURN_FAILURE # close the file unpacked_cpio.close() # and open it for reading unpacked_cpio = open(unpacked_cpio_path, 'rb') print _("Caching files from {0} made from {1}").format("unpacked.cpio", package_file_name) file_patterns = "" cpio_args = ["cpio", "-idu"] if exact_files: for filename in files: file_patterns += "." + filename + " " cpio_args = ["cpio", "-idu", file_patterns.strip()] with open("/dev/null", "w") as null: cpio = Popen(cpio_args, cwd=destdir, bufsize=-1, stdin=unpacked_cpio, stdout=null, stderr=null) retcode = cpio.wait() if retcode == 0: log1("files extracted OK") #print _("Removing temporary cpio file") os.unlink(unpacked_cpio_path) else: print _("Can't extract files from '{0}'").format(unpacked_cpio_path) return RETURN_FAILURE def clean_up(): """ Removes the temporary directory. """ if TMPDIR: try: shutil.rmtree(TMPDIR) except OSError, ex: if ex.errno != errno.ENOENT: error_msg(_("Can't remove '{0}': {1}").format(TMPDIR, ex)) class MyDownloadCallback(DownloadBaseCallback): """ This class serves as a download progress handler for yum's progress bar. """ def __init__(self, total_pkgs): """ Sets up instance variables Arguments: total_pkgs - number of packages to download """ self.total_pkgs = total_pkgs self.downloaded_pkgs = 0 self.last_pct = 0 self.last_time = 0 DownloadBaseCallback.__init__(self) def updateProgress(self, name, frac, fread, ftime): """ A method used to update the progress Arguments: name - filename frac - progress fracment (0 -> 1) fread - formated string containing BytesRead ftime - formated string containing remaining or elapsed time """ pct = int(frac * 100) if pct == self.last_pct: log2("percentage is the same, not updating progress") return self.last_pct = pct # if run from terminal we can have fancy output if sys.stdout.isatty(): sys.stdout.write("\033[sDownloading (%i of %i) %s: %3u%%\033[u" % (self.downloaded_pkgs + 1, self.total_pkgs, name, pct) ) if pct == 100: #print (_("Downloading (%i of %i) %s: %3u%%") # % (self.downloaded_pkgs + 1, self.total_pkgs, name, pct) #) print (_("Downloading ({0} of {1}) {2}: {3:3}%").format( self.downloaded_pkgs + 1, self.total_pkgs, name, pct ) ) # but we want machine friendly output when spawned from abrt-server else: t = time.time() if self.last_time == 0: self.last_time = t # update only every 5 seconds if pct == 100 or self.last_time > t or t - self.last_time >= 5: print (_("Downloading ({0} of {1}) {2}: {3:3}%").format( self.downloaded_pkgs + 1, self.total_pkgs, name, pct ) ) self.last_time = t if pct == 100: self.last_time = 0 sys.stdout.flush() def downloadErrorCallback(callBackObj): """ A callback function for mirror errors. """ print _("Problem '{0!s}' occured while downloading from mirror: '{1!s}'. Trying next one").format( callBackObj.exception, callBackObj.mirror) # explanation of the return value can be found here: # /usr/lib/python2.7/site-packages/urlgrabber/mirror.py return {'fail':0} class DebugInfoDownload(YumBase): """ This class is used to manage download of debuginfos. """ def __init__(self, cache, tmp, repo_pattern="*debug*", keep_rpms=False, noninteractive=True): self.old_stdout = -1 self.cachedir = cache self.tmpdir = tmp global TMPDIR TMPDIR = tmp self.keeprpms = keep_rpms self.noninteractive = noninteractive self.repo_pattern=repo_pattern YumBase.__init__(self) self.mute_stdout() #self.conf.cache = os.geteuid() != 0 # Setup yum (Ts, RPM db, Repo & Sack) # doConfigSetup() takes some time, let user know what we are doing print _("Initializing yum") try: # Saw this exception here: # cannot open Packages index using db3 - Permission denied (13) # yum.Errors.YumBaseError: Error: rpmdb open failed self.doConfigSetup() except YumBaseError, ex: self.unmute_stdout() print _("Error initializing yum (YumBase.doConfigSetup): '{0!s}'").format(ex) #return 1 - can't do this in constructor exit(1) self.unmute_stdout() def mute_stdout(self): """ Links sys.stdout with /dev/null and saves the old stdout """ if verbose < 2: self.old_stdout = sys.stdout sys.stdout = open("/dev/null", "w") def unmute_stdout(self): """ Replaces sys.stdout by stdout saved using mute """ if verbose < 2: if self.old_stdout != -1: sys.stdout = self.old_stdout else: print "ERR: unmute called without mute?" @ensure_abrt_uid def setup_tmp_dirs(self): if not os.path.exists(self.tmpdir): try: os.makedirs(self.tmpdir) except OSError, ex: print "Can't create tmpdir: %s" % ex return RETURN_FAILURE if not os.path.exists(self.cachedir): try: os.makedirs(self.cachedir) except OSError, ex: print "Can't create cachedir: %s" % ex return RETURN_FAILURE return RETURN_OK # return value will be used as exitcode. So 0 = ok, !0 - error def download(self, files, download_exact_files=False): """ Downloads rpms into a temporary directory Arguments: package_files_dict - a dict containing {pkg: file list} entries total_pkgs - total number of packages to download download_exact_files - extract only specified files Returns: RETURN_OK if all goes well. RETURN_FAILURE in case it cannot set up either of the directories. """ installed_size = 0 total_pkgs = 0 todownload_size = 0 downloaded_pkgs = 0 # nothing to download? if not files: return RETURN_FAILURE #if verbose == 0: # # this suppress yum messages about setting up repositories # mute_stdout() # make yumdownloader work as non root user if not self.setCacheDir(): print _("Error: can't make cachedir, exiting") return RETURN_FAILURE # disable all not needed for repo in self.repos.listEnabled(): try: repo.close() self.repos.disableRepo(repo.id) except YumBaseError, ex: print _("Can't disable repository '{0!s}': {1!s}").format(repo.id, str(ex)) # This takes some time, let user know what we are doing print _("Setting up yum repositories") # setting-up repos one-by-one, so we can skip the broken ones... # this helps when users are using 3rd party repos like rpmfusion # in rawhide it results in: Can't find valid base url... for r in self.repos.findRepos(pattern=self.repo_pattern): try: rid = self.repos.enableRepo(r.id) self.repos.doSetup(thisrepo=str(r.id)) log1("enabled repo %s", rid) setattr(r, "skip_if_unavailable", True) # yes, we want async download, otherwise our progressCallback # is not called and the internal yum's one is used, # which causes artifacts on output try: setattr(r, "_async", False) except (NameError, AttributeError), ex: print ex print _("Can't disable async download, the output might contain artifacts!") except YumBaseError, ex: print _("Can't setup {0}: {1}, disabling").format(r.id, ex) self.repos.disableRepo(r.id) # This is somewhat "magic", it unpacks the metadata making it usable. # Looks like this is the moment when yum talks to remote servers, # which takes time (sometimes minutes), let user know why # we have "paused": print _("Looking for needed packages in repositories") try: self.repos.populateSack(mdtype='metadata', cacheonly=1) except YumBaseError, ex: print _("Error retrieving metadata: '{0!s}'").format(ex) #we don't want to die here, some metadata might be already retrieved # so there is a chance we already have what we need #return 1 try: # Saw this exception here: # raise Errors.NoMoreMirrorsRepoError, errstr # NoMoreMirrorsRepoError: failure: # repodata/7e6632b82c91a2e88a66ad848e231f14c48259cbf3a1c3e992a77b1fc0e9d2f6-filelists.sqlite.bz2 # from fedora-debuginfo: [Errno 256] No more mirrors to try. self.repos.populateSack(mdtype='filelists', cacheonly=1) except YumBaseError, ex: print _("Error retrieving filelists: '{0!s}'").format(ex) # we don't want to die here, some repos might be already processed # so there is a chance we already have what we need #return 1 #if verbose == 0: # # re-enable the output to stdout # unmute_stdout() not_found = [] package_files_dict = {} for debuginfo_path in files: log2("yum whatprovides %s", debuginfo_path) pkg = self.pkgSack.searchFiles(debuginfo_path) # sometimes one file is provided by more rpms, we can use either of # them, so let's use the first match if pkg: if pkg[0] in package_files_dict.keys(): package_files_dict[pkg[0]].append(debuginfo_path) else: package_files_dict[pkg[0]] = [debuginfo_path] todownload_size += float(pkg[0].size) installed_size += float(pkg[0].installedsize) total_pkgs += 1 log2("found pkg for %s: %s", debuginfo_path, pkg[0]) else: log2("not found pkg for %s", debuginfo_path) not_found.append(debuginfo_path) # connect our progress update callback dnlcb = MyDownloadCallback(total_pkgs) self.repos.setProgressBar(dnlcb) self.repos.setMirrorFailureCallback(downloadErrorCallback) if verbose != 0 or len(not_found) != 0: print _("Can't find packages for {0} debuginfo files").format(len(not_found)) if verbose != 0 or total_pkgs != 0: print _("Packages to download: {0}").format(total_pkgs) question = _("Downloading {0:.2f}Mb, installed size: {1:.2f}Mb. Continue?").format( todownload_size / (1024*1024), installed_size / (1024*1024) ) if self.noninteractive == False and not ask_yes_no(question): print _("Download cancelled by user") return RETURN_CANCEL_BY_USER # set up tmp and cache dirs so that we can check free space in both retval = self.setup_tmp_dirs() if retval != RETURN_OK: return retval # check if there is enough free space in both tmp and cache res = os.statvfs(self.tmpdir) tmp_space = float(res.f_bsize * res.f_bavail) / (1024*1024) if (todownload_size / (1024*1024)) > tmp_space: question = _("Warning: Not enough free space in tmp dir '{0}'" " ({1:.2f}Mb left). Continue?").format( self.tmpdir, tmp_space) if not self.noninteractive and not ask_yes_no(question): print _("Download cancelled by user") return RETURN_CANCEL_BY_USER res = os.statvfs(self.cachedir) cache_space = float(res.f_bsize * res.f_bavail) / (1024*1024) if (installed_size / (1024*1024)) > cache_space: question = _("Warning: Not enough free space in cache dir " "'{0}' ({1:.2f}Mb left). Continue?").format( self.cachedir, cache_space) if not self.noninteractive and not ask_yes_no(question): print _("Download cancelled by user") return RETURN_CANCEL_BY_USER for pkg, files in package_files_dict.iteritems(): dnlcb.downloaded_pkgs = downloaded_pkgs repo.cache = 0 remote = pkg.returnSimple('relativepath') local = os.path.basename(remote) retval = self.setup_tmp_dirs() # continue only if the tmp dirs are ok if retval != RETURN_OK: return retval remote_path = pkg.returnSimple('remote_url') # check if the pkg is in a local repo and copy it if it is err = None if remote_path.startswith('file:///'): pkg_path = remote_path[7:] log2("copying from local repo: %s", remote) try: shutil.copy(pkg_path, self.tmpdir) except OSError, ex: print _("Cannot copy file '{0}': {1}").format(pkg_path, ex) continue else: # pkg is in a remote repo, we need to download it to tmpdir local = os.path.join(self.tmpdir, local) pkg.localpath = local # Hack: to set the localpath we want err = self.downloadPkgs(pkglist=[pkg]) # normalize the name # just str(pkg) doesn't work because it can have epoch pkg_nvra = pkg.name + "-" + pkg.version + "-" + pkg.release + "." + pkg.arch package_file_name = pkg_nvra + ".rpm" if err: # I observed a zero-length file left on error, # which prevents cleanup later. Fix it: try: os.unlink(self.tmpdir + "/" + package_file_name) except OSError: pass print (_("Downloading package {0} failed").format(pkg)) else: unpack_result = unpack_rpm(package_file_name, files, self.tmpdir, self.cachedir, self.keeprpms, exact_files=download_exact_files) if unpack_result == RETURN_FAILURE: # recursively delete the temp dir on failure print _("Unpacking failed, aborting download...") clean_up() return RETURN_FAILURE downloaded_pkgs += 1 if not self.keeprpms and os.path.exists(self.tmpdir): # Was: "All downloaded packages have been extracted, removing..." # but it was appearing even if no packages were in fact extracted # (say, when there was one package, and it has download error). print (_("Removing {0}").format(self.tmpdir)) try: os.rmdir(self.tmpdir) except OSError: error_msg(_("Can't remove %s, probably contains an error log").format(self.tmpdir)) return RETURN_OK def build_ids_to_path(pfx, build_ids): """ Transforms build ids into a path. build_id1=${build_id:0:2} build_id2=${build_id:2} file="usr/lib/debug/.build-id/$build_id1/$build_id2.debug" """ return ["%s/usr/lib/debug/.build-id/%s/%s.debug" % (pfx, b_id[:2], b_id[2:]) for b_id in build_ids] # beware this finds only missing libraries, but not the executable itself .. def filter_installed_debuginfos(build_ids, cache_dirs): """ Checks for installed debuginfos. Arguments: build_ids - string containing build ids cache_dirs - list of cache directories Returns: List of missing debuginfo files. """ files = build_ids_to_path("", build_ids) missing = [] # 1st pass -> search in /usr/lib for debuginfo_path in files: log2("looking: %s", debuginfo_path) if os.path.exists(debuginfo_path): log2("found: %s", debuginfo_path) continue log2("not found: %s", debuginfo_path) missing.append(debuginfo_path) if missing: files = missing missing = [] else: # nothing is missing, we can stop looking return missing for cache_dir in cache_dirs: log2("looking in %s" % cache_dir) for debuginfo_path in files: cache_debuginfo_path = cache_dir + debuginfo_path log2("looking: %s", cache_debuginfo_path) if os.path.exists(cache_debuginfo_path): log2("found: %s", cache_debuginfo_path) continue log2("not found: %s", debuginfo_path) missing.append(debuginfo_path) # in next iteration look only for files missing # from previous iterations if missing: files = missing missing = [] else: # nothing is missing, we can stop looking return missing return files