summaryrefslogtreecommitdiffstats
path: root/src/plugins/abrt-action-install-debuginfo.py
diff options
context:
space:
mode:
authorJiri Moskovcak <jmoskovc@redhat.com>2010-11-29 14:00:41 +0100
committerJiri Moskovcak <jmoskovc@redhat.com>2010-11-29 14:00:41 +0100
commit72a48e59e5898116b88daccfee6370aea81c764c (patch)
tree3ea2057d74e62644091d6188a2f1d640468d6487 /src/plugins/abrt-action-install-debuginfo.py
parentcf44477e4a1f75ec850192b2cf9c38b07636e8ac (diff)
downloadabrt-72a48e59e5898116b88daccfee6370aea81c764c.tar.gz
abrt-72a48e59e5898116b88daccfee6370aea81c764c.tar.xz
abrt-72a48e59e5898116b88daccfee6370aea81c764c.zip
new debuginfo install script rewritten in python
- using python alows us to use the yum API, so we can read the progress, file sizes, requires disk space, etc.. and seems to be faster the using yum --whatprovides + yumdownloader - it's easier to translate - we can drop dependency on yum-utils
Diffstat (limited to 'src/plugins/abrt-action-install-debuginfo.py')
-rwxr-xr-xsrc/plugins/abrt-action-install-debuginfo.py472
1 files changed, 472 insertions, 0 deletions
diff --git a/src/plugins/abrt-action-install-debuginfo.py b/src/plugins/abrt-action-install-debuginfo.py
new file mode 100755
index 00000000..b16cc3e9
--- /dev/null
+++ b/src/plugins/abrt-action-install-debuginfo.py
@@ -0,0 +1,472 @@
+#! /usr/bin/python -u
+# -*- coding: utf-8 -*-
+
+# WARNING: python -u means unbuffered I/O without it the messages are
+# passed to the parent asynchronously which looks bad in clients..
+from subprocess import Popen, PIPE
+import sys
+import os
+import getopt
+import shutil
+from yum import _, YumBase
+from yum.callbacks import DownloadBaseCallback
+
+# everything was ok
+RETURN_OK = 0
+# serious problem, should be logged somewhere
+RETURN_FAILURE = 2
+
+
+GETTEXT_PROGNAME = "abrt"
+import locale
+import gettext
+
+_ = lambda x: gettext.lgettext(x)
+
+def init_gettext():
+ try:
+ locale.setlocale(locale.LC_ALL, "")
+ except locale.Error:
+ import os
+ os.environ['LC_ALL'] = 'C'
+ locale.setlocale(locale.LC_ALL, "")
+ gettext.bind_textdomain_codeset(GETTEXT_PROGNAME, locale.nl_langinfo(locale.CODESET))
+ gettext.bindtextdomain(GETTEXT_PROGNAME, '/usr/share/locale')
+ gettext.textdomain(GETTEXT_PROGNAME)
+
+
+old_stdout = -1
+def mute_stdout():
+ if verbose < 2:
+ global old_stdout
+ old_stdout = sys.stdout
+ sys.stdout = open("/dev/null", "w")
+
+def unmute_stdout():
+ if verbose < 2:
+ if old_stdout != -1:
+ sys.stdout = old_stdout
+ else:
+ print "ERR: unmute called without mute?"
+
+def ask_yes_no(prompt, retries=4):
+ while True:
+ response = raw_input(prompt)
+ if response in ('y', 'Y'):
+ return True
+ if response in ('n', 'N', ''):
+ return False
+ retries = retries - 1
+ if retries < 0:
+ break
+ return False
+
+# TODO: unpack just required debuginfo and not entire rpm?
+# ..that can lead to: foo.c No such file and directory
+# files is not used...
+def unpack_rpm(package_nevra, files, tmp_dir, destdir, keeprpm):
+ package_file_suffix = ".rpm"
+ package_full_path = tmp_dir + "/" + package_nevra + package_file_suffix
+ log1("Extracting %s to %s" % (package_full_path, destdir))
+ log2(files)
+ print (_("Extracting cpio from %s") % (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:"), (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 the temporary rpm file")
+ os.unlink(package_full_path)
+ else:
+ unpacked_cpio.close()
+ print (_("Can't extract package: %s") % 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 %s made from %s") %
+ (unpacked_cpio_path, package_full_path))
+ cpio = Popen(["cpio","-i", "-d", "--quiet"],
+ stdin=unpacked_cpio, cwd=destdir, bufsize=-1)
+ retcode = cpio.wait()
+
+ if retcode == 0:
+ log1("files extracted OK")
+ print _("Removing the temporary cpio file")
+ os.unlink(unpacked_cpio_path)
+ else:
+ print (_("Can't extract files from: %s") % unpacked_cpio_path)
+ return RETURN_FAILURE
+
+class MyDownloadCallback(DownloadBaseCallback):
+ def __init__(self, total_pkgs):
+ self.total_pkgs = total_pkgs
+ self.downloaded_pkgs = 0
+ self.last_pct = 0
+ DownloadBaseCallback.__init__(self)
+
+ def updateProgress(self, name, frac, fread, ftime):
+ 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 a fancy output
+ if sys.stdout.isatty():
+ sys.stdout.write("\033[sDownloading (%i of %i) %.30s : %.3s %%\033[u"
+ % (self.downloaded_pkgs + 1, self.total_pkgs,
+ name, pct)
+ )
+ if pct == 100:
+ print _("Downloading (%i of %i) %.30s : %.3s %%"
+ % (self.downloaded_pkgs + 1, self.total_pkgs,
+ name, pct)
+ )
+ # but we want machine friendly output when spawned from abrt-server
+ else:
+ print (_("Downloading (%i of %i) %.30s : %.3s %%")
+ % (self.downloaded_pkgs + 1, self.total_pkgs, name, pct)
+ )
+
+ sys.stdout.flush()
+
+class DebugInfoDownload(YumBase):
+ """abrt-debuginfo-install --core=CORE tmpdir cachedir"""
+ def __init__(self, cache, tmp, keep_rpms=False):
+ self.cachedir = cache
+ self.tmpdir = tmp
+ self.keeprpms = keep_rpms
+ YumBase.__init__(self)
+ mute_stdout()
+ #self.conf.cache = os.geteuid() != 0
+ # Setup yum (Ts, RPM db, Repo & Sack)
+ self.doConfigSetup()
+ unmute_stdout()
+
+ def download(self, files):
+ """ @files - """
+ installed_size = 0
+ total_pkgs = 0
+ todownload_size = 0
+ downloaded_pkgs = 0
+ # nothing to download?
+ if not files:
+ return
+
+ print _("Searching the missing debuginfo packages")
+ # this suppress yum messages about setting up repositories
+ mute_stdout()
+
+ # make yumdownloader work as non root user.
+ if not self.setCacheDir():
+ self.logger.error("Error: Could not make cachedir, exiting")
+ sys.exit(50)
+
+ # disable all not needed
+ for repo in self.repos.listEnabled():
+ repo.close()
+ self.repos.disableRepo(repo.id)
+ # enable -debuginfo repos
+ for repo in self.repos.findRepos(pattern="*debuginfo*"):
+ #print repo
+ repo.enable()
+ rid = self.repos.enableRepo(repo.id)
+ log1("enabled repo %s" % rid)
+ setattr(repo, "skip_if_unavailable", True)
+ self.repos.doSetup()
+ # This is somewhat "magic", it unpacks the metadata making it usable.
+ self.repos.populateSack(mdtype='metadata', cacheonly=1)
+ self.repos.populateSack(mdtype='filelists', cacheonly=1)
+
+ # 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" % debuginfo_path)
+ 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 )
+
+ log1("%i files in %i packages" % (len(files), total_pkgs))
+
+ print (_("To download: (%.2f) M / Installed size: %.2f M" %
+ (
+ todownload_size / (1024**2),
+ installed_size / (1024**2))
+ )
+ )
+ #print _("%i debug infos not found" % len(not_found))
+
+ log1("packages: %i\nTo download: %f \nUnpacked size: %f" %
+ (total_pkgs,
+ todownload_size / (1024**2),
+ installed_size / (1024**2)))
+
+ # ask only if we have terminal, because for now we don't have a way
+ # how to pass the question to gui and the response back
+ if noninteractive == False and sys.stdout.isatty():
+ if not ask_yes_no(_("Is this ok? [y/N] ")):
+ return RETURN_OK
+
+ 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)
+ if not os.path.exists(self.tmpdir):
+ os.makedirs(self.tmpdir)
+ if not os.path.exists(self.cachedir):
+ os.makedirs(self.cachedir)
+ local = os.path.join(self.tmpdir, local)
+ pkg.localpath = local # Hack: to set the localpath we want
+ ret = self.downloadPkgs(pkglist=[pkg])
+ # downloadPkgs return an non empty list on succes
+ if ret:
+ print (_("Downloading package %s failed") % pkg)
+ else:
+ # normalize the name
+ # just str(pkg) doesn't work because it can have epoch
+ pkg_nvra = (pkg.name +"-"+ pkg.version +"-"+
+ pkg.release +"."+ pkg.arch)
+
+ unpack_result = unpack_rpm(pkg_nvra, files, self.tmpdir,
+ self.cachedir, keeprpms)
+ 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:
+ print (_("All downloaded packages have been extracted, removing %s")
+ % self.tmpdir)
+ try:
+ os.rmdir(self.tmpdir)
+ except OSError:
+ print _("Can't remove %s, probably contains an error log")
+
+verbose = 0
+def log1(message):
+ """ prints log message if verbosity > 0 """
+ if verbose > 0:
+ print "LOG1:", message
+
+def log2(message):
+ """ prints log message if verbosity > 1 """
+ if verbose > 1:
+ print "LOG2:", message
+
+#eu_unstrip_OUT=`eu-unstrip "--core=$core" -n 2>eu_unstrip.ERR`
+def extract_info_from_core(corefile):
+ """
+ Extracts builds with filenames,
+ Returns a list of tuples (build_id, filename)
+ """
+ #OFFSET = 0
+ BUILD_ID = 1
+ LIBRARY = 2
+ #SEP = 3
+ EXECUTABLE = 4
+
+ print (_("Analyzing corefile: %(corefile_path)s") %
+ {"corefile_path":corefile})
+ eu_unstrip_OUT = Popen(["eu-unstrip","--core=%s" % corefile, "-n"], stdout=PIPE, bufsize=-1).communicate()[0]
+ # parse eu_unstrip_OUT and return the list of build_ids
+
+ # eu_unstrip_OUT = ("0x7f42362ca000+0x204000 c4d35d993598a6242f7525d024b5ec3becf5b447@0x7f42362ca1a0 /usr/lib64/libcanberra-gtk.so.0 - libcanberra-gtk.so.0\n"
+ # "0x3afa400000+0x210000 607308f916c13c3ad9ee503008d31fa671ba73ce@0x3afa4001a0 /usr/lib64/libcanberra.so.0 - libcanberra.so.0\n"
+ # "0x3afa400000+0x210000 607308f916c13c3ad9ee503008d31fa671ba73ce@0x3afa4001a0 /usr/lib64/libcanberra.so.0 - libcanberra.so.0\n"
+ # "0x3bc7000000+0x208000 3be016bb723e85779a23e111a8ab1a520b209422@0x3bc70001a0 /usr/lib64/libvorbisfile.so.3 - libvorbisfile.so.3\n"
+ # "0x7f423609e000+0x22c000 87f9c7d9844f364c73aa2566d6cfc9c5fa36d35d@0x7f423609e1a0 /usr/lib64/libvorbis.so.0 - libvorbis.so.0\n"
+ # "0x7f4235e99000+0x205000 b5bc98c125a11b571cf4f2746268a6d3cfa95b68@0x7f4235e991a0 /usr/lib64/libogg.so.0 - libogg.so.0\n"
+ # "0x7f4235c8b000+0x20e000 f1ff6c8ee30dba27e90ef0c5b013df2833da2889@0x7f4235c8b1a0 /usr/lib64/libtdb.so.1 - libtdb.so.1\n"
+ # "0x3bc3000000+0x209000 8ef56f789fd914e8d0678eb0cdfda1bfebb00b40@0x3bc30001a0 /usr/lib64/libltdl.so.7 - libltdl.so.7\n"
+ # "0x7f4231b64000+0x22b000 3ca5b83798349f78b362b1ea51c8a4bc8114b8b1@0x7f4231b641a0 /usr/lib64/gio/modules/libgvfsdbus.so - libgvfsdbus.so\n"
+ # "0x7f423192a000+0x218000 ad024a01ad132737a8cfc7c95beb7c77733a652d@0x7f423192a1a0 /usr/lib64/libgvfscommon.so.0 - libgvfscommon.so.0\n"
+ # "0x7f423192a000+0x218000 ad024a01ad132737a8cfc7c95beb7c77733a652d@0x7f423192a1a0 /usr/lib64/libgvfscommon.so.0 - libgvfscommon.so.0\n"
+ # "0x3bb8e00000+0x20e000 d240ac5755184a95c783bb98a2d05530e0cf958a@0x3bb8e001a0 /lib64/libudev.so.0 - libudev.so.0")
+ #
+ #print eu_unstrip_OUT
+ # we failed to get build ids from the core -> die
+ if not eu_unstrip_OUT:
+ log1("can't get build ids from the core")
+ return RETURN_FAILURE
+
+ lines = eu_unstrip_OUT.split('\n')
+ # using set ensures the unique values
+ build_ids = set()
+ libraries = set()
+ build_ids = set()
+
+ for line in lines:
+ b_ids_line = line.split()
+ if len(b_ids_line) > 2:
+ # [exe] -> the executable itself
+ # linux-vdso.so.1 -> Virtual Dynamic Shared Object
+ if b_ids_line[EXECUTABLE] not in ["linux-vdso.so.1"]:
+ build_id = b_ids_line[BUILD_ID].split('@')[0]
+ build_ids.add(build_id)
+ library = b_ids_line[LIBRARY]
+ libraries.add(library)
+ build_ids.add(build_id)
+ else:
+ log2("skipping line %s" % line)
+ log1("Found %i build_ids" % len(build_ids))
+ log1("Found %i libs" % len(libraries))
+ return build_ids
+
+def build_ids_to_path(build_ids):
+ """
+ build_id1=${build_id:0:2}
+ build_id2=${build_id:2}
+ file="usr/lib/debug/.build-id/$build_id1/$build_id2.debug"
+ """
+ return ["/usr/lib/debug/.build-id/%s/%s.debug" % (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_dir):
+ # 1st pass -> search in /usr/lib
+ missing_di = []
+ files = build_ids_to_path(build_ids)
+ for debuginfo_path in files:
+ cache_debuginfo_path = cache_dir + debuginfo_path
+ log2("checking path: %s" % debuginfo_path)
+ if os.path.exists(debuginfo_path):
+ log2("found: %s" % debuginfo_path)
+ continue
+ if os.path.exists(cache_debuginfo_path):
+ log2("found: %s" % cache_debuginfo_path)
+ continue
+ log2("not found: %s" % (cache_debuginfo_path))
+ missing_di.append(debuginfo_path)
+ return missing_di
+
+tmpdir = None
+def clean_up():
+ try:
+ shutil.rmtree(tmpdir)
+ except OSError, ex:
+ print (_("Can't remove %(tmpdir_path)s: %(reason)s")
+ % {"tmpdir_path":tmpdir, "reason": ex }
+ )
+
+def sigterm_handler(signum, frame):
+ clean_up()
+ exit(RETURN_OK)
+
+def sigint_handler(signum, frame):
+ clean_up()
+ print "\n", _("Exiting on user Command")
+ exit(RETURN_OK)
+
+import signal
+if __name__ == "__main__":
+ # abrt-server can send SIGTERM to abort the download
+ signal.signal(signal.SIGTERM, sigterm_handler)
+ # ctrl-c
+ signal.signal(signal.SIGINT, sigint_handler)
+ core = None
+ cachedir = None
+ tmpdir = None
+ keeprpms = False
+ result = RETURN_OK
+ noninteractive = False
+
+ # localization
+ init_gettext()
+
+ help_text = _("Usage: %s --core=<COREFILE> "
+ "--tmpdir=<TMPDIR> "
+ "--cachedir=<CACHEDIR>") % sys.argv[0]
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], "vyhc:", ["help", "core=",
+ "cache=", "tmpdir=",
+ "keeprpms"])
+ except getopt.GetoptError, err:
+ print str(err) # prints something like "option -a not recognized"
+ sys.exit(RETURN_FAILURE)
+
+ for opt, arg in opts:
+ if opt == "-v":
+ verbose += 1
+ elif opt == "-y":
+ noninteractive = True
+ elif opt in ("--core","-c"):
+ core = arg
+ elif opt in ("--cache"):
+ cachedir = arg
+ elif opt in ("--tmpdir"):
+ tmpdir = arg
+ elif opt in ("--keeprpms"):
+ keeprpms = True
+ elif opt in ("-h", "--help"):
+ print help_text
+ sys.exit()
+
+ if not core:
+ print _("You have to specify the path to coredump.")
+ print help_text
+ exit(RETURN_FAILURE)
+ if not cachedir:
+ print _("You have to specify the path to cachedir.")
+ print help_text
+ exit(RETURN_FAILURE)
+ if not tmpdir:
+ print _("You have to specify the path to tmpdir.")
+ print help_text
+ exit(RETURN_FAILURE)
+
+ b_ids = extract_info_from_core(core)
+ if b_ids == RETURN_FAILURE:
+ exit(RETURN_FAILURE)
+ missing = filter_installed_debuginfos(b_ids, cachedir)
+ if missing:
+ log2(missing)
+ downloader = DebugInfoDownload(cache=cachedir, tmp=tmpdir)
+ result = downloader.download(missing)
+ else:
+ print _("All debuginfo seems to be available")
+ exit(RETURN_OK)
+
+ missing = filter_installed_debuginfos(b_ids, cachedir)
+ for bid in missing:
+ log1("MISSING:%s" % bid)
+
+ print _("Complete!")
+ exit(result)
+