Blob Blame History Raw
#!/usr/bin/python3 -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.

PROGNAME = "abrt-action-install-debuginfo"

import sys
import os
import errno
import getopt
import reportclient
from subprocess import Popen, PIPE
from reportclient import verbose, log_warning, log1, log2, set_verbosity, error_msg_and_die, error_msg
import time
from reportclient.debuginfo import DebugInfoDownload, filter_installed_debuginfos, build_ids_to_path, clean_up
import problem

# everything was ok
RETURN_OK = 0
# serious problem, should be logged somewhere
RETURN_FAILURE = 2
# path to tmp directory has to be global because of clean_up()
TMPDIR = None

GETTEXT_PROGNAME = "abrt"
import locale
import gettext

_ = lambda x: gettext.gettext(x)

def init_gettext():
    try:
        locale.setlocale(locale.LC_ALL, "")
    except locale.Error:
        os.environ['LC_ALL'] = 'C'
        locale.setlocale(locale.LC_ALL, "")
    # Defeat "AttributeError: 'module' object has no attribute 'nl_langinfo'"
    try:
        gettext.bind_textdomain_codeset(GETTEXT_PROGNAME, locale.nl_langinfo(locale.CODESET))
    except AttributeError:
        pass
    gettext.bindtextdomain(GETTEXT_PROGNAME, '/usr/share/locale')
    gettext.textdomain(GETTEXT_PROGNAME)

def sigterm_handler(signum, frame):
    clean_up(TMPDIR, silent=True)
    exit(RETURN_OK)

def sigint_handler(signum, frame):
    clean_up(TMPDIR)
    print("\n{0}".format(_("Exiting on user command")))
    sys.stdout.flush()
    # ??! without "sys.", I am getting segv!
    sys.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)
    fbuild_ids = "build_ids"
    cachedirs = []
    size_mb = 4096
    keeprpms = False
    noninteractive = False
    b_ids = []
    exact_fls = False
    missing = None
    repo_pattern = "*debug*"
    pkgmgr = None
    releasever = None

    # localization
    init_gettext()

    ABRT_VERBOSE = os.getenv("ABRT_VERBOSE")
    if (ABRT_VERBOSE):
        try:
            verbose = int(ABRT_VERBOSE)
        except:
            pass

    PROGNAME = os.path.basename(sys.argv[0])
    # ____________________________________________________________________________________ 7
    # ______01234567890123456789012345678901234567890123456789012345678901234567890123456789
    help_text = _(
            "Usage: %s [-vy] [--ids=BUILD_IDS_FILE] [--pkgmgr=(yum|dnf)]\n"
            "       [--tmpdir=TMPDIR] [--cache=CACHEDIR[:DEBUGINFODIR1:DEBUGINFODIR2...]] [--size_mb=SIZE]\n"
            "       [-e, --exact=PATH[:PATH]...]\n"
            "       [--releasever=RELEASEVER]\n"
            "\n"
            "Installs debuginfos for all build-ids listed in BUILD_IDS_FILE\n"
            "to CACHEDIR, using TMPDIR as temporary staging area.\n"
            "Old files in CACHEDIR are deleted until it is smaller than SIZE.\n"
            "\n"
            "Reads configuration from /etc/abrt/plugins/CCpp.conf\n"
            "\n"
            "    -v          Be verbose\n"
            "    -y          Noninteractive, assume 'Yes' to all questions\n"
            "    --ids       Default: build_ids\n"
            "    --tmpdir    Default: @LARGE_DATA_TMP_DIR@/abrt-tmp-debuginfo-RANDOM_SUFFIX\n"
            "    --cache     Colon separated list of directories. The first one is used for\n"
            "                saving installed debuginfos.\n"
            "                Default: /var/cache/abrt-dir\n"
            "    --size_mb   Default: 4096\n"
            "    --pkgmgr   Default: PackageManager from CCpp.conf or 'dnf'\n"
            "    -e,--exact  Download only specified files\n"
            "    --repo      Pattern to use when searching for repos.\n"
            "                Default: *debug*\n"
            "    --releasever RELEASEVER\n"
            "                Pass this OS version to package managers.\n"
            # --keeprpms is not documented yet because it's a NOP so far
    ) % PROGNAME

    try:
        opts, args = getopt.getopt(sys.argv[1:], "vyhe",
                ["help", "ids=", "cache=", "size_mb=", "tmpdir=", "keeprpms",
                 "exact=", "repo=", "pkgmgr=", "releasever="])
    except getopt.GetoptError as err:
        print(err) # prints something like "option -a not recognized"
        exit(RETURN_FAILURE)

    for opt, arg in opts:
        if opt in ("-h", "--help"):
            print(help_text)
            exit(0)
        elif opt == "-v":
            verbose += 1
        elif opt == "-y":
            noninteractive = True
        elif opt == "--ids":
            fbuild_ids = arg
        elif opt == "--cache":
            cachedirs = arg.split(':')
        elif opt == "--size_mb":
            try:
                size_mb = int(arg)
            except:
                pass
        elif opt == "--tmpdir":
            TMPDIR = arg
        elif opt == "--keeprpms":
            keeprpms = True
        # --exact takes precedence over --ids
        elif opt in ("-e", "--exact"):
            missing = arg.split(':')
            exact_fls = True
        elif opt == "--repo":
            repo_pattern = arg
        elif opt == "--pkgmgr":
            pkgmgr = arg
        elif opt == "--releasever":
            releasever = arg

    set_verbosity(verbose)

    if not cachedirs:
        cachedirs = ["/var/cache/abrt-di"]
        try:
            conf = problem.load_plugin_conf_file("CCpp.conf")
        except OSError as ex:
            print(ex)
        else:
            cachedirs = conf.get("DebuginfoLocation", cachedirs[0]).split(":")

    if not TMPDIR:
        # security people prefer temp subdirs in app's private dir, like /var/run/abrt
        # and we switched to /tmp but Fedora feature tmp-on-tmpfs appeared, hence we must
        # not use /tmp for potential big data anymore
        TMPDIR = "@LARGE_DATA_TMP_DIR@/abrt-tmp-debuginfo-%s.%u" % (time.strftime("%Y-%m-%d-%H:%M:%S"), os.getpid())


    if missing == None:
        fin = sys.stdin
        if fbuild_ids != "-":
            try:
                fin = open(fbuild_ids, "r")
            except IOError as ex:
                error_msg_and_die(_("Can't open {0}: {1}").format(fbuild_ids, ex))
        for line in fin.readlines():
            b_ids.append(line.strip('\n'))

        if not b_ids:
            exit(RETURN_FAILURE)

        # Delete oldest/biggest files from cachedir.
        # (Note that we need to do it before we check for missing debuginfos)
        #
        # We can do it as a separate step in report_event.conf, but this
        # would require setuid'ing abrt-action-trim-files to abrt:abrt.
        # Since we (via abrt-action-install-debuginfo-to-abrt-cache)
        # are already running setuid,
        # it makes sense to NOT setuid abrt-action-trim-files too,
        # but instead run it as our child:
        sys.stdout.flush()
        try:
            pid = os.fork()
            if pid == 0:
                argv = ["abrt-action-trim-files", "-f", "%um:%s" % (size_mb, cachedirs[0]), "--"]
                argv.extend(build_ids_to_path(cachedirs[0], b_ids))
                log2("abrt-action-trim-files %s", argv);
                os.execvp("abrt-action-trim-files", argv);
                error_msg_and_die("Can't execute '%s'", "abrt-action-trim-files");
            if pid > 0:
                os.waitpid(pid, 0);
        except Exception as e:
            error_msg("Can't execute abrt-action-trim-files: %s", e);

        missing = filter_installed_debuginfos(b_ids, cachedirs)

    exact_file_missing = False
    result = RETURN_OK
    if missing:
        log2("%s", missing)
        if len(b_ids) > 0:
            print(_("Coredump references {0} debuginfo files, {1} of them are not installed").format(len(b_ids), len(missing)))
        else:
            # Only --exact FILE[:FILE2]... was specified
            print(_("{0} of debuginfo files are not installed").format(len(missing)))

        if pkgmgr is None:
            try:
                conf = problem.load_plugin_conf_file("CCpp.conf")
            except OSError as ex:
                sys.stderr.write("{0}".format(str(ex)))
                sys.exit(RETURN_FAILURE)

            pkgmgr = conf.get("PackageManager", "dnf").lower()

        download_class = None
        if pkgmgr == "dnf":
            from reportclient.dnfdebuginfo import DNFDebugInfoDownload
            download_class = DNFDebugInfoDownload
        elif pkgmgr == "yum":
            from reportclient.yumdebuginfo import YumDebugInfoDownload
            download_class = YumDebugInfoDownload
        else:
            sys.stderr.write(_("Invalid configuration of CCpp addon, unsupported Package manager: '%s'") % (pkgmgr))
            sys.exit(RETURN_FAILURE)

        # TODO: should we pass keep_rpms=keeprpms to DebugInfoDownload here??
        try:
            downloader = download_class(cache=cachedirs[0], tmp=TMPDIR,
                                    noninteractive=noninteractive,
                                    repo_pattern=repo_pattern,
                                    releasever=releasever)
            result = downloader.download(missing, download_exact_files=exact_fls)

            # make sure that all downloaded directories are writeable by abrt group
            for root, dirs, files in os.walk(cachedirs[0]):
                for walked_dir in dirs:
                    os.chmod(os.path.join(root, walked_dir), 0o775)

        except OSError as ex:
            if ex.errno == errno.EPIPE:
                clean_up(TMPDIR, silent=True)
                exit(RETURN_FAILURE)
            error_msg_and_die("Can't download debuginfos: %s", ex)

        if exact_fls:
            for bid in missing:
                if not os.path.isfile(bid):
                    print(_("Missing requested file: {0}").format(bid))
                    exact_file_missing = True

        missing = filter_installed_debuginfos(b_ids, cachedirs)
        for bid in missing:
            print(_("Missing debuginfo file: {0}").format(bid))

    if not missing and not exact_file_missing:
        print(_("All debuginfo files are available"))

    if exact_file_missing:
        result = RETURN_FAILURE

    exit(result)