From fb52104af74bbf6eeda394880666df40b4354aba Mon Sep 17 00:00:00 2001 From: Karel Klic Date: Tue, 1 Mar 2011 11:32:49 +0100 Subject: moved retrace server code to src/retrace --- src/retrace/Makefile.am | 27 ++++ src/retrace/abrt-retrace-cleanup.py | 43 ++++++ src/retrace/abrt-retrace-reposync | 58 +++++++ src/retrace/backtrace.wsgi | 42 +++++ src/retrace/coredump2packages.py | 293 +++++++++++++++++++++++++++++++++++ src/retrace/create.wsgi | 96 ++++++++++++ src/retrace/install.sh | 244 +++++++++++++++++++++++++++++ src/retrace/log.wsgi | 42 +++++ src/retrace/retrace-local.repo | 143 +++++++++++++++++ src/retrace/retrace.conf | 33 ++++ src/retrace/retrace.py | 232 ++++++++++++++++++++++++++++ src/retrace/retrace.repo | 199 ++++++++++++++++++++++++ src/retrace/retrace_httpd.conf | 4 + src/retrace/status.wsgi | 38 +++++ src/retrace/test-uploader.py | 139 +++++++++++++++++ src/retrace/worker.c | 51 ++++++ src/retrace/worker.py | 298 ++++++++++++++++++++++++++++++++++++ 17 files changed, 1982 insertions(+) create mode 100644 src/retrace/Makefile.am create mode 100755 src/retrace/abrt-retrace-cleanup.py create mode 100755 src/retrace/abrt-retrace-reposync create mode 100644 src/retrace/backtrace.wsgi create mode 100755 src/retrace/coredump2packages.py create mode 100644 src/retrace/create.wsgi create mode 100755 src/retrace/install.sh create mode 100644 src/retrace/log.wsgi create mode 100644 src/retrace/retrace-local.repo create mode 100644 src/retrace/retrace.conf create mode 100644 src/retrace/retrace.py create mode 100644 src/retrace/retrace.repo create mode 100644 src/retrace/retrace_httpd.conf create mode 100644 src/retrace/status.wsgi create mode 100755 src/retrace/test-uploader.py create mode 100644 src/retrace/worker.c create mode 100755 src/retrace/worker.py (limited to 'src/retrace') diff --git a/src/retrace/Makefile.am b/src/retrace/Makefile.am new file mode 100644 index 00000000..887466eb --- /dev/null +++ b/src/retrace/Makefile.am @@ -0,0 +1,27 @@ +bin_PROGRAMS = abrt-retrace-worker +abrt_retrace_worker_SOURCES = worker.c + +dist_bin_SCRIPTS=abrt-retrace-reposync + +retrace_PYTHON = retrace.py +retracedir = $(datadir)/abrt-retrace + +interface_PYTHON = backtrace.wsgi create.wsgi log.wsgi status.wsgi +# interfacedir should probably be $$(pkgdatadir)/retrace +interfacedir = $(datadir)/abrt-retrace + +repo_DATA = retrace.repo retrace-local.repo +repodir = ${sysconfdir}/yum.repos.d + +retraceconf_DATA = retrace.conf +retraceconfdir = ${sysconfdir}/abrt + +httpdconf_DATA = retrace_httpd.conf +httpdconfdir = ${sysconfdir}/httpd/conf.d + +EXTRA_DIST = retrace.conf retrace_httpd.conf retrace.repo retrace-local.repo + +# Apache config files can be owned by root, httpd just needs read +# access. +#install-data-hook: +# chown apache:apache $(DESTDIR)$(httpdconfdir)/retrace_httpd.conf diff --git a/src/retrace/abrt-retrace-cleanup.py b/src/retrace/abrt-retrace-cleanup.py new file mode 100755 index 00000000..9286889f --- /dev/null +++ b/src/retrace/abrt-retrace-cleanup.py @@ -0,0 +1,43 @@ +#!/usr/bin/python + +import os +import shutil +import sys +import time +from retrace import * + +if __name__ == "__main__": + now = int(time.time()) + + logfile = "%s/cleanup.log" % CONFIG["LogDir"] + + try: + log = open(logfile, "a") + log.write(time.strftime("[%Y-%m-%d %H:%M:%S] Running cleanup\n")) + + files = os.listdir(CONFIG["WorkDir"]) + except IOError, ex: + print "Error opening log file: %s" % ex + sys.exit(1) + except OSError, ex: + log.write("Unable to list work directory: %s" % ex) + sys.exit(2) + + for filename in files: + filepath = "%s/%s" % (CONFIG["WorkDir"], filename) + if os.path.isdir(filepath): + try: + if (now - os.path.getctime(filepath)) / 3600 >= CONFIG["DeleteTaskAfter"]: + log.write("Deleting directory '%s'\n" % filepath) + shutil.rmtree(filepath) + except OSError, ex: + log.write("Error deleting directory: %s\n" % (filepath, ex)) + except IOError, ex: + print "Unable to write to log file: %s" % ex + sys.exit(3) + + try: + log.close() + except IOError, ex: + print "Error closing log file: %s" % ex + sys.exit(4) diff --git a/src/retrace/abrt-retrace-reposync b/src/retrace/abrt-retrace-reposync new file mode 100755 index 00000000..0e75a7e1 --- /dev/null +++ b/src/retrace/abrt-retrace-reposync @@ -0,0 +1,58 @@ +#!/bin/bash + +if [ $3 ] && [ ! $4 ] \ + && [ $1 = "fedora" ] \ + && ( [ $3 = "i686" ] || [ $3 = "x86_64" ] ) +then + LOG="/var/log/abrt-retrace/$1-$2-$3.log" + LOCK="/var/lock/subsys/abrt-retrace-$1-$2-$3" + REPODIR="/var/cache/abrt-retrace/" + + REPODIR_CUSTOM=`cat "/etc/abrt/retrace.conf" | grep "RepoDir" | sed "s/^ *RepoDir *= *//"` + if [ -d $REPODIR_CUSTOM ] || mkdir -p $REPODIR_CUSTOM > /dev/null 2>&1 + then + REPODIR="$REPODIR_CUSTOM" + fi + + date >> "$LOG" + + if [ -f "$LOCK" ] + then + echo "The repository synchronization is running at the moment." >> "$LOG" + exit 2 + fi + + if ! touch "$LOCK" + then + echo "Unable to set lock." + exit 3 + fi + + cd "$REPODIR" + + reposync -a $3 \ + --repoid="$1-$2-$3-updates" \ + --repoid="$1-$2-$3-updates-debuginfo" \ + --repoid="$1-$2-$3-updates-testing" \ + --repoid="$1-$2-$3-updates-testing-debuginfo" \ + | grep "Downloading" \ + | sed -e "s/^\[\([^:]*\).*\] Downloading /\1 /" \ + | sed -e "s/Packages\///" \ + >> "$LOG" + + createrepo "$1-$2-$3-updates" > /dev/null + createrepo "$1-$2-$3-updates-debuginfo" > /dev/null + createrepo "$1-$2-$3-updates-testing" > /dev/null + createrepo "$1-$2-$3-updates-testing-debuginfo" > /dev/null + + rm -f "$LOCK" +else + echo "Usage: $0 distribution version architecture" + echo + echo "where" + echo "distributuon = [fedora]" + echo "version = release version" + echo "architecture = [i686|x86_64]" + + exit 1 +fi diff --git a/src/retrace/backtrace.wsgi b/src/retrace/backtrace.wsgi new file mode 100644 index 00000000..d6a8686a --- /dev/null +++ b/src/retrace/backtrace.wsgi @@ -0,0 +1,42 @@ +#!/usr/bin/python + +import sys +sys.path = ["/usr/share/abrt-retrace"] + sys.path + +from retrace import * + +def application(environ, start_response): + request = Request(environ) + + match = URL_PARSER.match(request.script_name) + if not match: + return response(start_response, "404 Not Found") + + taskdir = "%s/%s" % (CONFIG["SaveDir"], match.group(1)) + + if not os.path.isdir(taskdir): + return response(start_response, "404 Not Found") + + pwdpath = "%s/password" % taskdir + try: + pwdfile = open(pwdpath, "r") + pwd = pwdfile.read() + pwdfile.close() + except: + return response(start_response, "500 Internal Server Error", "Unable to verify password") + + if not "X-Task-Password" in request.headers or request.headers["X-Task-Password"] != pwd: + return response(start_response, "403 Forbidden") + + btpath = "%s/retrace_backtrace" % taskdir + if not os.path.isfile(btpath): + return response(start_response, "404 Not Found") + + try: + btfile = open(btpath, "r") + output = btfile.read() + btfile.close() + except: + return response(start_response, "500 Internal Server Error", "Unable to read backtrace file at server") + + return response(start_response, "200 OK", output) diff --git a/src/retrace/coredump2packages.py b/src/retrace/coredump2packages.py new file mode 100755 index 00000000..ac2db9f2 --- /dev/null +++ b/src/retrace/coredump2packages.py @@ -0,0 +1,293 @@ +#! /usr/bin/python +# -*- coding:utf-8;mode:python -*- +# Gets list of packages necessary for processing of a coredump. +# Uses eu-unstrip and yum. + +import subprocess +import yum +import sys +import argparse + +parser = argparse.ArgumentParser(description='Get packages for coredump processing.') +parser.add_argument('--repos', default='*', metavar='WILDCARD', + help='Yum repository wildcard to be enabled') +parser.add_argument('coredump', help='Coredump') +parser.add_argument('--log', metavar='FILENAME', + help='Store debug output to a file') +args = parser.parse_args() + +if args.log: + log = open(args.log, "w") +else: + log = open("/dev/null", "w") + +# +# Initialize yum, enable only repositories specified via command line +# --repos option. +# +stdout = sys.stdout +sys.stdout = log +yumbase = yum.YumBase() +yumbase.doConfigSetup() +if not yumbase.setCacheDir(): + exit(2) +log.write("Closing all enabled repositories...\n") +for repo in yumbase.repos.listEnabled(): + log.write(" - {0}\n".format(repo.name)) + repo.close() + yumbase.repos.disableRepo(repo.id) +log.write("Enabling repositories matching \'{0}\'...\n".format(args.repos)) +for repo in yumbase.repos.findRepos(args.repos): + log.write(" - {0}\n".format(repo.name)) + repo.enable() + repo.skip_if_unavailable = True +yumbase.repos.doSetup() +yumbase.repos.populateSack(mdtype='metadata', cacheonly=1) +yumbase.repos.populateSack(mdtype='filelists', cacheonly=1) +sys.stdout = stdout + +# +# Get eu-unstrip output, which contains build-ids and binary object +# paths +# +log.write("Running eu-unstrip...\n") +unstrip_args = ['eu-unstrip', '--core={0}'.format(args.coredump), '-n'] +unstrip_proc = subprocess.Popen(unstrip_args, stdout=subprocess.PIPE) +unstrip = unstrip_proc.communicate()[0] +log.write("{0}\n".format(unstrip)) +if not unstrip: + exit(1) + +def binary_packages_from_debuginfo_package(debuginfo_package, binobj_path): + """ + Returns a list of packages corresponding to the provided debuginfo + package. One of the packages in the list contains the binary + specified in binobj_path; this is a list because if binobj_patch + is not specified (and sometimes it is not, binobj_path might + contain just '-'), we do not know which package contains the + binary, we know only packages from the same SRPM as the debuginfo + package. + """ + package_list = [] + if binobj_path == '-': # [exe] without binary name + log.write(" Yum search for [exe] without binary name, " + "packages with NVR {0}:{1}-{2}.{3}...\n".format(debuginfo_package.epoch, + debuginfo_package.ver, + debuginfo_package.rel, + debuginfo_package.arch)) + # Append all packages with the same base package name. + # Other possibility is to download the debuginfo RPM, + # unpack it, and get the name of the binary from the + # /usr/lib/debug/.build-id/xx/yyyyyy symlink. + evra_list = yumbase.pkgSack.searchNevra(epoch=debuginfo_package.epoch, + ver=debuginfo_package.ver, + rel=debuginfo_package.rel, + arch=debuginfo_package.arch) + for package in evra_list: + log.write(" - {0}: base name \"{1}\"\n".format(str(package), package.base_package_name)) + if package.base_package_name != debuginfo_package.base_package_name: + continue + package_list.append(package) + else: + log.write(" Yum search for {0}...\n".format(binobj_path)) + binobj_package_list = yumbase.pkgSack.searchFiles(binobj_path) + for binobj_package in binobj_package_list: + log.write(" - {0}".format(str(binobj_package))) + if 0 != binobj_package.returnEVR().compare(debuginfo_package.returnEVR()): + log.write(": NVR doesn't match\n") + continue + log.write(": NVR matches\n") + package_list.append(binobj_package) + return package_list + +def process_unstrip_entry(build_id, binobj_path): + """ + Returns a tuple of two items. + + First item is a list of packages which we found to be associated + with the unstrip entry defined by build_id and binobj_path. + + Second item is a list of package versions (same package name, + different epoch-version-release), which contain the binary object + (an executable or shared library) corresponding to this unstrip + entry. If this method failed to find an unique package name (with + only different versions), this list contains the list of base + package names. This item can be used to associate a coredump with + some crashing package. + """ + package_list = [] + coredump_package_list = [] + coredump_base_package_list = [] + # Ask for a known path from debuginfo package. + debuginfo_path = "/usr/lib/debug/.build-id/{0}/{1}.debug".format(build_id[:2], build_id[2:]) + log.write("Yum search for {0}...\n".format(debuginfo_path)) + debuginfo_package_list = yumbase.pkgSack.searchFiles(debuginfo_path) + + # A problem here is that some libraries lack debuginfo. Either + # they were stripped during build, or they were not stripped by + # /usr/lib/rpm/find-debuginfo.sh because of wrong permissions or + # something. The proper solution is to detect such libraries and + # fix the packages. + for debuginfo_package in debuginfo_package_list: + log.write(" - {0}\n".format(str(debuginfo_package))) + package_list.append(debuginfo_package) + binary_packages = binary_packages_from_debuginfo_package(debuginfo_package, binobj_path) + coredump_base_package_list.append(debuginfo_package.base_package_name) + if len(binary_packages) == 1: + coredump_package_list.append(str(binary_packages[0])) + package_list.extend(binary_packages) + if len(coredump_package_list) == len(coredump_base_package_list): + return package_list, coredump_package_list + else: + return package_list, coredump_base_package_list + + +def process_unstrip_output(): + """ + Parse the eu-unstrip output, and search for packages via yum. + + Returns a tuple containing three items: + - a list of package objects + - a list of missing buildid entries + - a list of coredump package adepts + """ + # List of packages found in yum repositories and matching the + # coredump. + package_list = [] + # List of pairs (library/executable path, build id) which were not + # found via yum. + missing_buildid_list = [] + # coredump package adepts + coredump_package_list = [] + first_entry = True + for line in unstrip.split('\n'): + parts = line.split() + if not parts or len(parts) < 3: + continue + build_id = parts[1].split('@')[0] + binobj_path = parts[2] + if binobj_path[0] != '/' and parts[4] != '[exe]': + continue + entry_package_list, entry_coredump_package_list = process_unstrip_entry(build_id, binobj_path) + if first_entry: + coredump_package_list = entry_coredump_package_list + first_entry = False + if len(entry_package_list) == 0: + missing_buildid_list.append([binobj_path, build_id]) + else: + for entry_package in entry_package_list: + found = False + for package in package_list: + if str(entry_package) == str(package): + found = True + break + if not found: + package_list.append(entry_package) + return package_list, missing_buildid_list, coredump_package_list + +package_list, missing_buildid_list, coredump_package_list = process_unstrip_output() + +# +# The package list might contain multiple packages with the same name, +# but different version. This happens because some binary had the same +# build id over multiple package releases. +# +def find_duplicates(package_list): + for p1 in range(0, len(package_list) - 1): + package1 = package_list[p1] + for p2 in range(p1 + 1, len(package_list)): + package2 = package_list[p2] + if package1.name == package2.name: + return package1, package2 + return None, None + +def count_removals(package_list, base_package_name, epoch, ver, rel, arch): + count = 0 + for package in package_list: + if package.base_package_name != base_package_name: + continue + if package.epoch != epoch or package.ver != ver or package.rel != rel or package.arch != arch: + continue + count += 1 + return count + +log.write("Checking for duplicates...\n") +while True: + package1, package2 = find_duplicates(package_list) + if package1 is None: + break + p1removals = count_removals(package_list, + package1.base_package_name, + package1.epoch, + package1.ver, + package1.rel, + package1.arch) + p2removals = count_removals(package_list, + package2.base_package_name, + package2.epoch, + package2.ver, + package2.rel, + package2.arch) + + log.write(" - {0}".format(package1.base_package_name)) + if package1.base_package_name != package2.base_package_name: + log.write(" {0}\n".format(package2.base_package_name)) + else: + log.write("\n") + log.write(" - {0}:{1}-{2}.{3} ({4} dependent packages)\n".format(package1.epoch, + package1.ver, + package1.rel, + package1.arch, + p1removals)) + log.write(" - {0}:{1}-{2}.{3} ({4} dependent packages)\n".format(package2.epoch, + package2.ver, + package2.rel, + package2.arch, + p2removals)) + + removal_candidate = package1 + if p1removals == p2removals: + # Remove older if we can choose + if package1.returnEVR().compare(package2.returnEVR()) > 0: + removal_candidate = package2 + log.write(" - decided to remove {0}:{1}-{2}.{3} because it's older\n".format(removal_candidate.epoch, + removal_candidate.ver, + removal_candidate.rel, + removal_candidate.arch)) + else: + if p1removals > p2removals: + removal_candidate = package2 + log.write(" - decided to remove {0}:{1}-{2}.{3} because has fewer dependencies\n".format(removal_candidate.epoch, + removal_candidate.ver, + removal_candidate.rel, + removal_candidate.arch)) + # Remove the removal_candidate packages from the package list + for package in package_list[:]: + if package.base_package_name == removal_candidate.base_package_name and \ + 0 == package.returnEVR().compare(removal_candidate.returnEVR()): + package_list.remove(package) + +# Clean coredump_package_list: +for coredump_package in coredump_package_list[:]: + found = False + for package in package_list: + if str(package) == coredump_package or package.base_package_name == coredump_package: + found = True + break + if not found: + coredump_package_list.remove(coredump_package) + +# +# Print names of found packages first, then a newline separator, and +# then objects for which the packages were not found. +# +if len(coredump_package_list) == 1: + print coredump_package_list[0] +else: + print "-" +print +for package in sorted(package_list): + print str(package) +print +for path, build_id in missing_buildid_list: + print "{0} {1}".format(path, build_id) diff --git a/src/retrace/create.wsgi b/src/retrace/create.wsgi new file mode 100644 index 00000000..1c560d45 --- /dev/null +++ b/src/retrace/create.wsgi @@ -0,0 +1,96 @@ +#!/usr/bin/python + +import sys +sys.path = ["/usr/share/abrt-retrace"] + sys.path + +from retrace import * +from tempfile import * + +def application(environ, start_response): + request = Request(environ) + + if request.scheme != "https": + return response(start_response, "403 Forbidden", "You must use HTTPS.") + + if len(get_active_tasks()) >= CONFIG["MaxParallelTasks"]: + return response(start_response, "503 Service Unavailable") + + if request.method != "POST": + return response(start_response, "405 Method Not Allowed") + + if not request.content_type in ["application/x-xz", "application/x-xz-compressed-tar", "application/x-gzip", "application/x-tar"]: + return response(start_response, "415 Unsupported Media Type") + + if not request.content_length: + return response(start_response, "411 Length Required") + + if request.content_length > CONFIG["MaxPackedSize"] * 1048576: + return response(start_response, "413 Request Entity Too Large") + + if CONFIG["UseWorkDir"]: + workdir = CONFIG["WorkDir"] + else: + workdir = CONFIG["SaveDir"] + + if not os.path.isdir(workdir): + try: + os.makedirs(workdir) + except: + return response(start_response, "500 Internal Server Error", "Unable to create working directory") + + space = free_space(workdir) + + if not space: + return response(start_response, "500 Internal Server Error", "Unable to obtain disk free space") + + if space - request.content_length < CONFIG["MinStorageLeft"] * 1048576: + return response(start_response, "507 Insufficient Storage") + + try: + archive = NamedTemporaryFile(mode="wb", delete=False, suffix=".tar.xz") + archive.write(request.body) + archive.close() + except: + return response(start_response, "500 Internal Server Error", "Unable to save archive") + + size = unpacked_size(archive.name) + if not size: + os.unlink(archive.name) + return response(start_response, "500 Internal Server Error", "Unable to obtain unpacked size") + + if size > CONFIG["MaxUnpackedSize"] * 1048576: + os.unlink(archive.name) + return response(start_response, "413 Request Entity Too Large") + + if space - size < CONFIG["MinStorageLeft"] * 1048576: + os.unlink(archive.name) + return response(start_response, "507 Insufficient Storage") + + taskid, taskpass, taskdir = new_task() + if not taskid or not taskpass or not taskdir: + return response(start_response, "500 Internal Server Error", "Unable to create new task") + + try: + os.mkdir("%s/crash/" % taskdir) + os.chdir("%s/crash/" % taskdir) + unpack_retcode = unpack(archive.name) + os.unlink(archive.name) + + if unpack_retcode != 0: + raise Exception + except: + os.chdir("/") + Popen(["rm", "-rf", taskdir]) + return response(start_response, "500 Internal Server Error", "Unable to unpack archive") + + files = os.listdir(".") + + for required_file in REQUIRED_FILES: + if not required_file in files: + os.chdir("/") + Popen(["rm", "-rf", taskdir]) + return response(start_response, "403 Forbidden") + + Popen(["/usr/sbin/abrt-retrace-worker", "%d" % taskid]) + + return response(start_response, "201 Created", "", [("X-Task-Id", "%d" % taskid), ("X-Task-Password", taskpass)]) diff --git a/src/retrace/install.sh b/src/retrace/install.sh new file mode 100755 index 00000000..ca88a1ec --- /dev/null +++ b/src/retrace/install.sh @@ -0,0 +1,244 @@ +#!/bin/bash + +ABRTDIR="/etc/abrt" +LOGDIR="/var/log/abrt-retrace" +REPODIR="/var/cache/abrt-retrace" +SCRIPTDIR="/usr/share/abrt-retrace" +SRCDIR="." +WORKDIR="/var/spool/abrt-retrace" + +INTFILES="$SRCDIR/interface/create.wsgi $SRCDIR/interface/status.wsgi \ + $SRCDIR/interface/log.wsgi $SRCDIR/interface/backtrace.wsgi" +LIBFILES="$SRCDIR/lib/retrace.py" +REPOSYNCFILES="$SRCDIR/reposync/abrt-retrace-reposync" +WORKERFILES="$SRCDIR/worker/worker.py $SRCDIR/worker/coredump2packages.py" + +if [ ! $EUID = "0" ] +then + echo "You must run '$0' with root permissions." + exit 1 +fi + +if ! rpm -q httpd > /dev/null 2>&1 +then + echo "httpd package is required to install Retrace Server." + exit 2 +fi + +if ! rpm -q mod_wsgi > /dev/null 2>&1 +then + echo "mod_wsgi package is required to install Retrace Server" + exit 3 +fi + +if ! rpm -q mod_ssl > /dev/null 2>&1 +then + echo "mod_ssl package is required to install Retrace Server" + exit 4 +fi + +if ! rpm -q python-webob > /dev/null 2>&1 +then + echo "python-webob package is required to install Retrace Server" + exit 5 +fi + +if ! rpm -q yum-utils > /dev/null 2>&1 +then + echo "yum-utils package is required to install Retrace Server" + exit 6 +fi + +if ! rpm -q createrepo > /dev/null 2>&1 +then + echo "createrepo package is required to install Retrace Server" + exit 7 +fi + +if ! rpm -q mock > /dev/null 2>&1 +then + echo "mock package is required to install Retrace Server" + exit 8 +fi + +if ! rpm -q xz > /dev/null 2>&1 +then + echo "xz package is required to install Retrace Server" + exit 9 +fi + +if ! rpm -q gcc > /dev/null 2>&1 +then + echo "gcc package is required to install Retrace Server" + exit 10 +fi + +if usermod -G mock root +then + echo "User 'root' added to 'mock' group" +else + echo "Unable to add user 'root' to group 'mock'" + exit 11 +fi + +if [ ! -d "$ABRTDIR" ] +then + if mkdir "$ABRTDIR" + then + echo "Created directory '$ABRTDIR'" + else + echo "Error creating directory '$ABRTDIR'" + exit 12 + fi +fi + +if [ ! -d "$SCRIPTDIR" ] +then + if mkdir "$SCRIPTDIR" + then + echo "Created directory '$SCRIPTDIR'" + else + echo "Error creating directory '$SCRIPTDIR'" + exit 13 + fi +fi + +if [ ! -d "$WORKDIR" ] +then + if mkdir "$WORKDIR" + then + echo "Created directory '$WORKDIR'" + if chown apache "$WORKDIR" && chgrp apache "$WORKDIR" + then + echo "$WORKDIR owner and group changed to 'apache'" + else + echo "$WORKDIR unable to change owner or group" + exit 14 + fi + else + echo "Error creating directory '$WORKDIR'" + exit 15 + fi +fi + +if [ ! -d "$REPODIR" ] +then + if mkdir "$REPODIR" + then + echo "Created directory '$REPODIR'" + else + echo "Error creating directory '$REPODIR'" + exit 16 + fi +fi + +if [ ! -d "$LOGDIR" ] +then + if mkdir "$LOGDIR" + then + echo "Created directory '$LOGDIR'" + else + echo "Error creating directory '$LOGDIR'" + exit 17 + fi +fi + +if ! gcc -pedantic -Wall -Wextra -Werror -o "/usr/sbin/abrt-retrace-worker" "$SRCDIR/worker/worker.c" \ + || ! chmod u+s "/usr/sbin/abrt-retrace-worker" +then + echo "Error compiling abrt-retrace-worker" + exit 18 +fi + +echo "abrt-retrace-worker compiled" + +for FILE in $LIBFILES +do + if cp "$FILE" "$SCRIPTDIR" + then + echo "Installed '$FILE'" + else + echo "Error installing '$FILE'" + exit 19 + fi +done + +for FILE in $INTFILES +do + if cp "$FILE" "$SCRIPTDIR" + then + echo "Installed '$FILE'" + else + echo "Error installing '$FILE'" + exit 20 + fi +done + +for FILE in $WORKERFILES +do + if cp "$FILE" "$SCRIPTDIR" + then + echo "Installed '$FILE'" + else + echo "Error installing '$FILE'" + exit 21 + fi +done + +for FILE in $REPOSYNCFILES +do + if cp "$FILE" "$SCRIPTDIR" + then + echo "Installed '$FILE'" + else + echo "Error installing '$FILE'" + exit 22 + fi +done + +if cp "$SRCDIR/config/retrace.conf" "/etc/abrt/retrace.conf" +then + echo "Copied '$SRCDIR/config/retrace.conf' to '/etc/abrt/retrace.conf'" +else + echo "Error copying '$SRCDIR/config/retrace.conf'" + exit 23 +fi + +if cp "$SRCDIR/config/retrace.repo" "/etc/yum.repos.d/retrace.repo" \ + && cp "$SRCDIR/config/retrace-local.repo" "/etc/yum.repos.d/retrace-local.repo" +then + echo "Copied '$SRCDIR/config/retrace.repo' to '/etc/yum.repos.d/retrace.repo'" + echo "Running initial repository download. This will take some time." + "$SCRIPTDIR/abrt-retrace-reposync" fedora 14 i686 + createrepo "$REPODIR/fedora-14-i686" > /dev/null + createrepo "$REPODIR/fedora-14-i686-debuginfo" > /dev/null + "$SCRIPTDIR/abrt-retrace-reposync" fedora 14 x86_64 + createrepo "$REPODIR/fedora-14-x86_64" > /dev/null + createrepo "$REPODIR/fedora-14-x86_64-debuginfo" > /dev/null +# "$SCRIPTDIR/abrt-retrace-reposync" fedora 15 i686 +# createrepo "$REPODIR/fedora-15-i686" +# createrepo "$REPODIR/fedora-15-i686-debuginfo" +# "$SCRIPTDIR/abrt-retrace-reposync" fedora 15 x86_64 +# createrepo "$REPODIR/fedora-15-x86_64" +# createrepo "$REPODIR/fedora-15-x86_64-debuginfo" +else + echo "Error copying '$SRCDIR/config/retrace.repo'" + exit 24 +fi + +if cp "$SRCDIR/config/retrace_httpd.conf" "/etc/httpd/conf.d/retrace.conf" +then + echo "Copied '$SRCDIR/config/retrace_httpd.conf' to '/etc/httpd/conf.d/retrace.conf'" + service httpd restart +else + echo "Error copying '$SRCDIR/config/retrace_httpd.conf'" + exit 25 +fi + +echo +echo "Retrace Server setup OK." +echo "You should set up cron to periodically synchronize local repositories. The recommended configuration is:" +echo "0 0,8,16 * * * $SCRIPTDIR/abrt-retrace-reposync fedora 14 i686" +echo "0 2,10,18 * * * $SCRIPTDIR/abrt-retrace-reposync fedora 14 x86_64" +#echo "0 4,12,20 * * * $SCRIPTDIR/abrt-retrace-reposync fedora 15 i686" +#echo "0 6,14,22 * * * $SCRIPTDIR/abrt-retrace-reposync fedora 15 x86_64" diff --git a/src/retrace/log.wsgi b/src/retrace/log.wsgi new file mode 100644 index 00000000..bc20a98b --- /dev/null +++ b/src/retrace/log.wsgi @@ -0,0 +1,42 @@ +#!/usr/bin/python + +import sys +sys.path = ["/usr/share/abrt-retrace"] + sys.path + +from retrace import * + +def application(environ, start_response): + request = Request(environ) + + match = URL_PARSER.match(request.script_name) + if not match: + return response(start_response, "404 Not Found") + + taskdir = "%s/%s" % (CONFIG["SaveDir"], match.group(1)) + + if not os.path.isdir(taskdir): + return response(start_response, "404 Not Found") + + pwdpath = "%s/password" % taskdir + try: + pwdfile = open(pwdpath, "r") + pwd = pwdfile.read() + pwdfile.close() + except: + return response(start_response, "500 Internal Server Error", "Unable to verify password") + + if not "X-Task-Password" in request.headers or request.headers["X-Task-Password"] != pwd: + return response(start_response, "403 Forbidden") + + logpath = "%s/retrace_log" % taskdir + if not os.path.isfile(logpath): + return response(start_response, "404 Not Found") + + try: + logfile = open(logpath, "r") + output = logfile.read() + logfile.close() + except: + return response(start_response, "500 Internal Server Error", "Unable to read log file at server") + + return response(start_response, "200 OK", output) diff --git a/src/retrace/retrace-local.repo b/src/retrace/retrace-local.repo new file mode 100644 index 00000000..fb079ca5 --- /dev/null +++ b/src/retrace/retrace-local.repo @@ -0,0 +1,143 @@ +[retrace-fedora-13-i686] +name=Fedora 13 - i686 +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-13-i686/ +enabled=0 + +[retrace-fedora-13-i686-debuginfo] +name=Fedora 13 - i686 - Debug +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-13-i686-debuginfo/ +enabled=0 + +[retrace-fedora-13-i686-updates] +name=Fedora 13 - i686 - Updates +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-13-i686-updates/ +enabled=0 + +[retrace-fedora-13-i686-updates-debuginfo] +name=Fedora 13 - i686 - Updates - Debug +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-13-i686-updates-debuginfo/ +enabled=0 + +[retrace-fedora-13-i686-updates-testing] +name=Fedora 13 - i686 - Test Updates +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-13-i686-updates-testing/ +enabled=0 + +[retrace-fedora-13-i686-updates-testing-debuginfo] +name=Fedora 13 - i686 - Test Updates Debug +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-13-i686-updates-testing-debuginfo/ +enabled=0 + +[retrace-fedora-13-x86_64] +name=Fedora 13 - x86_64 +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-13-x86_64/ +enabled=0 + +[retrace-fedora-13-x86_64-debuginfo] +name=Fedora 13 - x86_64 - Debug +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-13-x86_64-debuginfo/ +enabled=0 + +[retrace-fedora-13-x86_64-updates] +name=Fedora 13 - x86_64 - Updates +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-13-x86_64-updates/ +enabled=0 + +[retrace-fedora-13-x86_64-updates-debuginfo] +name=Fedora 13 - x86_64 - Updates - Debug +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-13-x86_64-updates-debuginfo/ +enabled=0 + +[retrace-fedora-13-x86_64-updates-testing] +name=Fedora 13 - x86_64 - Test Updates +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-13-x86_64-updates-testing/ +enabled=0 + +[retrace-fedora-13-x86_64-updates-testing-debuginfo] +name=Fedora 13 - x86_64 - Test Updates Debug +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-13-x86_64-updates-testing-debuginfo/ +enabled=0 + +[retrace-fedora-14-i686] +name=Fedora 14 - i686 +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-14-i686/ +enabled=0 + +[retrace-fedora-14-i686-debuginfo] +name=Fedora 14 - i686 - Debug +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-14-i686-debuginfo/ +enabled=0 + +[retrace-fedora-14-i686-updates] +name=Fedora 14 - i686 - Updates +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-14-i686-updates/ +enabled=0 + +[retrace-fedora-14-i686-updates-debuginfo] +name=Fedora 14 - i686 - Updates - Debug +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-14-i686-updates-debuginfo/ +enabled=0 + +[retrace-fedora-14-i686-updates-testing] +name=Fedora 14 - i686 - Test Updates +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-14-i686-updates-testing/ +enabled=0 + +[retrace-fedora-14-i686-updates-testing-debuginfo] +name=Fedora 14 - i686 - Test Updates Debug +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-14-i686-updates-testing-debuginfo/ +enabled=0 + +[retrace-fedora-14-x86_64] +name=Fedora 14 - x86_64 +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-14-x86_64/ +enabled=0 + +[retrace-fedora-14-x86_64-debuginfo] +name=Fedora 14 - x86_64 - Debug +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-14-x86_64-debuginfo/ +enabled=0 + +[retrace-fedora-14-x86_64-updates] +name=Fedora 14 - x86_64 - Updates +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-14-x86_64-updates/ +enabled=0 + +[retrace-fedora-14-x86_64-updates-debuginfo] +name=Fedora 14 - x86_64 - Updates - Debug +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-14-x86_64-updates-debuginfo/ +enabled=0 + +[retrace-fedora-14-x86_64-updates-testing] +name=Fedora 14 - x86_64 - Test Updates +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-14-x86_64-updates-testing/ +enabled=0 + +[retrace-fedora-14-x86_64-updates-testing-debuginfo] +name=Fedora 14 - x86_64 - Test Updates Debug +failovermethod=priority +baseurl=file:///var/cache/abrt-retrace/fedora-14-x86_64-updates-testing-debuginfo/ +enabled=0 diff --git a/src/retrace/retrace.conf b/src/retrace/retrace.conf new file mode 100644 index 00000000..e97e7cfc --- /dev/null +++ b/src/retrace/retrace.conf @@ -0,0 +1,33 @@ +[retrace] +# Maximum tasks running at one moment +MaxParallelTasks = 5 + +# Maximum size of archive uploaded by user (MB) +MaxPackedSize = 50 + +# Maximum size of archive contents (MB) +MaxUnpackedSize = 1024 + +# Minimal storage left on WorkDir FS after unpacking archive (MB) +MinStorageLeft = 1024 + +# Delete old tasks after (hours) +DeleteTaskAfter = 120 + +# SQLite statistics DB filename +DBFile = stats.db + +# Log directory +LogDir = /var/log/abrt-retrace + +# Local repos directory +RepoDir = /var/cache/abrt-retrace + +# Directory where the crashes and results are saved +SaveDir = /var/spool/abrt-retrace + +# Whether to use explicit working directory, otherwise SaveDir is used +UseWorkDir = 0 + +# Working directory +WorkDir = /tmp/abrt-retrace diff --git a/src/retrace/retrace.py b/src/retrace/retrace.py new file mode 100644 index 00000000..1c741ec7 --- /dev/null +++ b/src/retrace/retrace.py @@ -0,0 +1,232 @@ +#!/usr/bin/python + +import os +import re +import ConfigParser +import random +import sqlite3 +from webob import Request +from subprocess import * + +REQUIRED_FILES = ["architecture", "coredump", "release"] + +DF_BIN = "/bin/df" +DU_BIN = "/usr/bin/du" +TAR_BIN = "/bin/tar" +XZ_BIN = "/usr/bin/xz" + +TASKID_PARSER = re.compile("^.*/([0-9]+)/*$") +PACKAGE_PARSER = re.compile("^(.+)-([0-9]+(\.[0-9]+)*-[0-9]+)\.([^-]+)$") +DF_OUTPUT_PARSER = re.compile("^([^ ^\t]*)[ \t]+([0-9]+)[ \t]+([0-9]+)[ \t]+([0-9]+)[ \t]+([0-9]+%)[ \t]+(.*)$") +DU_OUTPUT_PARSER = re.compile("^([0-9]+)") +XZ_OUTPUT_PARSER = re.compile("^totals[ \t]+([0-9]+)[ \t]+([0-9]+)[ \t]+([0-9]+)[ \t]+([0-9]+)[ \t]+([0-9]+\.[0-9]+)[ \t]+([^ ^\t]+)[ \t]+([0-9]+)") +URL_PARSER = re.compile("^/([0-9]+)/?") +RELEASE_PARSERS = { + "fedora": re.compile("^Fedora[^0-9]+([0-9]+)[^\(]\(([^\)]+)\)$"), +} + +TASKPASS_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +CONFIG_FILE = "/etc/abrt/retrace.conf" +CONFIG = { + "TaskIdLength": 9, + "TaskPassLength": 32, + "MaxParallelTasks": 10, + "MaxPackedSize": 30, + "MaxUnpackedSize": 600, + "MinStorageLeft": 10240, + "DeleteTaskAfter": 120, + "LogDir": "/var/log/abrt-retrace", + "RepoDir": "/var/cache/abrt-retrace", + "SaveDir": "/var/spool/abrt-retrace", + "WorkDir": "/tmp/abrt-retrace", + "UseWorkDir": False, + "DBFile": "stats.db", +} + +def read_config(): + parser = ConfigParser.ConfigParser() + parser.read(CONFIG_FILE) + for key in CONFIG.keys(): + vartype = type(CONFIG[key]) + if vartype is int: + get = parser.getint + elif vartype is bool: + get = parser.getboolean + elif vartype is float: + get = parser.getfloat + else: + get = parser.get + + try: + CONFIG[key] = get("retrace", key) + except: + pass + +def free_space(path): + pipe = Popen([DF_BIN, path], stdout=PIPE).stdout + for line in pipe.readlines(): + match = DF_OUTPUT_PARSER.match(line) + if match: + pipe.close() + return 1024 * int(match.group(4)) + + pipe.close() + return None + +def dir_size(path): + pipe = Popen([DU_BIN, "-s", path], stdout=PIPE).stdout + for line in pipe.readlines(): + match = DU_OUTPUT_PARSER.match(line) + if match: + pipe.close() + return 1024 * int(match.group(1)) + + pipe.close() + return 0 + +def unpacked_size(archive): + pipe = Popen([XZ_BIN, "--list", "--robot", archive], stdout=PIPE).stdout + for line in pipe.readlines(): + match = XZ_OUTPUT_PARSER.match(line) + if match: + pipe.close() + return int(match.group(4)) + + pipe.close() + return None + +def gen_task_password(taskdir): + generator = random.SystemRandom() + taskpass = "" + for j in xrange(CONFIG["TaskPassLength"]): + taskpass += generator.choice(TASKPASS_ALPHABET) + + try: + passfile = open("%s/password" % taskdir, "w") + passfile.write(taskpass) + passfile.close() + except: + return None + + return taskpass + +def get_task_est_time(taskdir): + return 180 + +def new_task(): + i = 0 + newdir = CONFIG["SaveDir"] + while os.path.exists(newdir) and i < 50: + i += 1 + taskid = random.randint(pow(10, CONFIG["TaskIdLength"] - 1), pow(10, CONFIG["TaskIdLength"]) - 1) + newdir = "%s/%d" % (CONFIG["SaveDir"], taskid) + + try: + os.mkdir(newdir) + taskpass = gen_task_password(newdir) + if not taskpass: + Popen(["rm", "-rf", newdir]) + raise Exception + + return taskid, taskpass, newdir + except: + return None, None, None + +def unpack(archive): + pipe = Popen([TAR_BIN, "xJf", archive]) + pipe.wait() + return pipe.returncode + +def response(start_response, status, body="", extra_headers=[]): + start_response(status, [("Content-Type", "text/plain"), ("Content-Length", "%d" % len(body))] + extra_headers) + return [body] + +def get_active_tasks(): + tasks = [] + if CONFIG["UseWorkDir"]: + tasksdir = CONFIG["WorkDir"] + else: + tasksdir = CONFIG["SaveDir"] + + for filename in os.listdir(tasksdir): + if len(filename) != CONFIG["TaskIdLength"]: + continue + + try: + taskid = int(filename) + except: + continue + + path = "%s/%s" % (tasksdir, filename) + if os.path.isdir(path) and not os.path.isfile("%s/retrace_log" % path): + tasks.append(taskid) + + return tasks + +def init_crashstats_db(): + try: + con = sqlite3.connect("%s/%s" % (CONFIG["SaveDir"], CONFIG["DBFile"])) + query = con.cursor() + query.execute(""" + CREATE TABLE IF NOT EXISTS + retracestats( + taskid INT NOT NULL, + package VARCHAR(255) NOT NULL, + version VARCHAR(16) NOT NULL, + release VARCHAR(16) NOT NULL, + arch VARCHAR(8) NOT NULL, + starttime INT NOT NULL, + duration INT NOT NULL, + prerunning TINYINT NOT NULL, + postrunning TINYINT NOT NULL, + chrootsize BIGINT NOT NULL + ) + """) + con.commit() + con.close() + + return True + except: + return False + +def save_crashstats(crashstats): + try: + con = sqlite3.connect("%s/%s" % (CONFIG["SaveDir"], CONFIG["DBFile"])) + query = con.cursor() + query.execute(""" + INSERT INTO retracestats(taskid, package, version, release, arch, + starttime, duration, prerunning, postrunning, chrootsize) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + (crashstats["taskid"], crashstats["package"], crashstats["version"], + crashstats["release"], crashstats["arch"], crashstats["starttime"], + crashstats["duration"], crashstats["prerunning"], + crashstats["postrunning"], crashstats["chrootsize"]) + ) + con.commit() + con.close() + + return True + except: + return False + +class logger(): + def __init__(self, taskid): + "Starts logging into savedir." + self._logfile = open("%s/%s/log" % (CONFIG["SaveDir"], taskid), "w") + + def write(self, msg): + "Writes msg into log file." + if not self._logfile.closed: + self._logfile.write(msg) + self._logfile.flush() + + def close(self): + "Finishes logging and renames file to retrace_log." + if not self._logfile.closed: + self._logfile.close() + os.rename(self._logfile.name, self._logfile.name.replace("/log", "/retrace_log")) + +### read config on import ### +read_config() diff --git a/src/retrace/retrace.repo b/src/retrace/retrace.repo new file mode 100644 index 00000000..19c409e2 --- /dev/null +++ b/src/retrace/retrace.repo @@ -0,0 +1,199 @@ +[fedora-13-i686] +name=Fedora 13 - i686 +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=fedora-13&arch=i386 +enabled=0 +metadata_expire=6h +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-i386 + +[fedora-13-i686-debuginfo] +name=Fedora 13 - i686 - Debug +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=fedora-debug-13&arch=i386 +enabled=0 +metadata_expire=6h +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-i386 + +[fedora-13-i686-updates] +name=Fedora 13 - i686 - Updates +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-released-f13&arch=i386 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-i386 + +[fedora-13-i686-updates-debuginfo] +name=Fedora 13 - i686 - Updates - Debug +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-released-debug-f13&arch=i386 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-i386 + +[fedora-13-i686-updates-testing] +name=Fedora 13 - i686 - Test Updates +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-testing-f13&arch=i386 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-i386 + +[fedora-13-i686-updates-testing-debuginfo] +name=Fedora 13 - i686 - Test Updates Debug +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-testing-debug-f13&arch=i386 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-i386 + +[fedora-13-x86_64] +name=Fedora 13 - x86_64 +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=fedora-13&arch=x86_64 +enabled=0 +metadata_expire=6h +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-x86_64 + +[fedora-13-x86_64-debuginfo] +name=Fedora 13 - x86_64 - Debug +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=fedora-debug-13&arch=x86_64 +enabled=0 +metadata_expire=6h +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-x86_64 + +[fedora-13-x86_64-updates] +name=Fedora 13 - x86_64 - Updates +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-released-f13&arch=x86_64 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-x86_64 + +[fedora-13-x86_64-updates-debuginfo] +name=Fedora 13 - x86_64 - Updates - Debug +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-released-debug-f13&arch=x86_64 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-x86_64 + +[fedora-13-x86_64-updates-testing] +name=Fedora 13 - x86_64 - Test Updates +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-testing-f13&arch=x86_64 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-x86_64 + +[fedora-13-x86_64-updates-testing-debuginfo] +name=Fedora 13 - x86_64 - Test Updates Debug +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-testing-debug-f13&arch=x86_64 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-x86_64 + +[fedora-14-i686] +name=Fedora 14 - i686 +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=fedora-14&arch=i386 +enabled=0 +metadata_expire=6h +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-i386 + +[fedora-14-i686-debuginfo] +name=Fedora 14 - i686 - Debug +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=fedora-debug-14&arch=i386 +enabled=0 +metadata_expire=6h +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-i386 + +[fedora-14-i686-updates] +name=Fedora 14 - i686 - Updates +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-released-f14&arch=i386 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-i386 + +[fedora-14-i686-updates-debuginfo] +name=Fedora 14 - i686 - Updates - Debug +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-released-debug-f14&arch=i386 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-i386 + +[fedora-14-i686-updates-testing] +name=Fedora 14 - i686 - Test Updates +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-testing-f14&arch=i386 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-i386 + +[fedora-14-i686-updates-testing-debuginfo] +name=Fedora 14 - i686 - Test Updates Debug +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-testing-debug-f14&arch=i386 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-i386 + +[fedora-14-x86_64] +name=Fedora 14 - x86_64 +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=fedora-14&arch=x86_64 +enabled=0 +metadata_expire=6h +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-x86_64 + +[fedora-14-x86_64-debuginfo] +name=Fedora 14 - x86_64 - Debug +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=fedora-debug-14&arch=x86_64 +enabled=0 +metadata_expire=6h +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-x86_64 + +[fedora-14-x86_64-updates] +name=Fedora 14 - x86_64 - Updates +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-released-f14&arch=x86_64 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-x86_64 + +[fedora-14-x86_64-updates-debuginfo] +name=Fedora 14 - x86_64 - Updates - Debug +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-released-debug-f14&arch=x86_64 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-x86_64 + +[fedora-14-x86_64-updates-testing] +name=Fedora 14 - x86_64 - Test Updates +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-testing-f14&arch=x86_64 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-x86_64 + +[fedora-14-x86_64-updates-testing-debuginfo] +name=Fedora 14 - x86_64 - Test Updates Debug +failovermethod=priority +mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=updates-testing-debug-f14&arch=x86_64 +enabled=0 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-x86_64 diff --git a/src/retrace/retrace_httpd.conf b/src/retrace/retrace_httpd.conf new file mode 100644 index 00000000..b896f86f --- /dev/null +++ b/src/retrace/retrace_httpd.conf @@ -0,0 +1,4 @@ +WSGIScriptAliasMatch ^/create$ /usr/share/abrt-retrace/create.wsgi +WSGIScriptAliasMatch ^/[0-9]+/?$ /usr/share/abrt-retrace/status.wsgi +WSGIScriptAliasMatch ^/[0-9]+/log$ /usr/share/abrt-retrace/log.wsgi +WSGIScriptAliasMatch ^/[0-9]+/backtrace$ /usr/share/abrt-retrace/backtrace.wsgi diff --git a/src/retrace/status.wsgi b/src/retrace/status.wsgi new file mode 100644 index 00000000..eb153b05 --- /dev/null +++ b/src/retrace/status.wsgi @@ -0,0 +1,38 @@ +#!/usr/bin/python + +import sys +sys.path = ["/usr/share/abrt-retrace"] + sys.path + +from retrace import * + +def application(environ, start_response): + request = Request(environ) + + match = URL_PARSER.match(request.script_name) + if not match: + return response(start_response, "404 Not Found") + + taskdir = "%s/%s" % (CONFIG["SaveDir"], match.group(1)) + + if not os.path.isdir(taskdir): + return response(start_response, "404 Not Found") + + pwdpath = "%s/password" % taskdir + try: + pwdfile = open(pwdpath, "r") + pwd = pwdfile.read() + pwdfile.close() + except: + return response(start_response, "500 Internal Server Error", "Unable to verify password") + + if not "X-Task-Password" in request.headers or request.headers["X-Task-Password"] != pwd: + return response(start_response, "403 Forbidden") + + status = "PENDING" + if os.path.isfile("%s/retrace_log" % taskdir): + if os.path.isfile("%s/retrace_backtrace" % taskdir): + status = "FINISHED_SUCCESS" + else: + status = "FINISHED_FAILURE" + + return response(start_response, "200 OK", status, [("X-Task-Status", status)]) diff --git a/src/retrace/test-uploader.py b/src/retrace/test-uploader.py new file mode 100755 index 00000000..db75da16 --- /dev/null +++ b/src/retrace/test-uploader.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +import os +import socket +import ssl +import sys +import tempfile + +from subprocess import * + +REQUIRED_FILES = ["analyzer", "architecture", "coredump", "executable", "package", "release", "uid"] +CONTENT_TYPE = "application/x-xz-compressed-tar" + +if __name__ == "__main__": + argc = len(sys.argv) + if argc < 2 or argc > 3: + print "ABRT Retrace Uploader" + print "For test purposes only" + print "Usage: '" + sys.argv[0] + " crash_directory server_address'" + print " Crash directory is the directory created by ABRT (default /var/spool/abrt/crash_directory/)." + print " Crash directory must contain analyzer, architecture, coredump, executable, package, release and uid files." + print " If no server address is specified, default testing machine is used." + print " Only binary crashes (caught by CCpp) need retrace." + print " The script shows raw HTTP output including X-Task-Id and X-Task-Password headers." + print " The tester is supposed to know what he's uploading and should handle task id and password on his own." + sys.exit(1) + + crashdir = sys.argv[1] + if argc == 2: + server_addr = "retrace01.fedoraproject.org" + else: + server_addr = sys.argv[2] + + print "Checking crash directory...", + sys.stdout.flush() + + for required_file in REQUIRED_FILES: + if not os.path.isfile(crashdir + "/" + required_file): + print "Error" + sys.exit(3) + + print "OK" + print "Checking analyzer...", + sys.stdout.flush() + + try: + anfile = open(crashdir + "/analyzer", "r") + an = anfile.read() + anfile.close() + if an != "CCpp": + raise + except: + print "Error" + sys.exit(4) + + print "OK" + print "Checking architecture...", + sys.stdout.flush() + + print "OK" + + print "Compressing crash into .tar.xz archive...", + sys.stdout.flush() + + archive = tempfile.NamedTemporaryFile(delete = False, suffix = ".tar.xz") + archive.close() + + os.chdir(crashdir) + compress = Popen(["tar", "cJf", archive.name] + REQUIRED_FILES) + compress.wait() + if compress.returncode != 0: + print "Error" + sys.exit(7) + + print "OK" + print "Building request...", + sys.stdout.flush() + + try: + f = open(archive.name, "rb") + data = f.read() + f.close() + request = "POST /create HTTP/1.1\r\n" \ + + "Host: " + server_addr + "\r\n" \ + + "Content-Type: " + CONTENT_TYPE + "\r\n" \ + + "Content-Length: " + str(len(data)) + "\r\n" \ + + "Connection: close\r\n" \ + + "\r\n" + + print "OK" + print "---------" + print request + "[raw data]" + print "---------" + except: + print "Error" + sys.exit(8) + + print "Connecting to retrace server @ ssl://" + server_addr + ":443...", + sys.stdout.flush() + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sslsock = ssl.wrap_socket(sock) + try: + sslsock.connect((server_addr, 443)) + print "OK" + print "Sending request...", + sys.stdout.flush() + + sslsock.write(request + data) + + print "OK" + print "Receiving response...", + sys.stdout.flush() + + response = "" + block = sslsock.read() + while (block): + response += block + block = sslsock.read() + + sslsock.close() + + print "OK" + print "----------" + print response, + print "----------" + except: + print "Error" + sys.exit(9) + + print "Cleanup...", + sys.stdout.flush() + + try: + os.unlink(archive.name) + except: + print "Error" + sys.exit(10) + + print "OK" diff --git a/src/retrace/worker.c b/src/retrace/worker.c new file mode 100644 index 00000000..2020627d --- /dev/null +++ b/src/retrace/worker.c @@ -0,0 +1,51 @@ +#include +#include +#include +#include + +/* + Launches Retrace Server worker (worker.py) with root permissions. + Binary needs to be owned by root and needs to set SUID bit. +*/ + +int main(int argc, char **argv) +{ + char command[256]; + FILE *pipe; + int i; + + if (argc != 2) + { + fprintf(stderr, "Usage: %s task_id\n", argv[0]); + return 1; + } + + if (setuid(0) != 0) + { + fprintf(stderr, "You must run %s with root permissions.\n", argv[0]); + return 2; + } + + for (i = 0; argv[1][i]; ++i) + if (!isdigit(argv[1][i])) + { + fputs("Task ID may only contain digits.", stderr); + return 3; + } + + /* needs to be set to make mock work properly */ + setenv("SUDO_USER", "root", 1); + setenv("SUDO_UID", "0", 1); + setenv("SUDO_GID", "0", 1); + + /* launch worker.py */ + sprintf(command, "/usr/bin/python /usr/share/abrt-retrace/worker.py \"%s\"", argv[1]); + pipe = popen(command, "r"); + if (pipe == NULL) + { + fputs("Unable to run 'worker.py'.", stderr); + return 4; + } + + return pclose(pipe) >> 8; +} diff --git a/src/retrace/worker.py b/src/retrace/worker.py new file mode 100755 index 00000000..4b56c992 --- /dev/null +++ b/src/retrace/worker.py @@ -0,0 +1,298 @@ +#!/usr/bin/python + +import sys +import time +from retrace import * + +LOG = None + +def retrace_run(errorcode, cmd): + "Runs cmd using subprocess.Popen and kills script with errorcode on failure" + try: + process = Popen(cmd, stdout=PIPE, stderr=STDOUT) + process.wait() + output = process.stdout.read() + process.stdout.close() + except Exception as ex: + process = None + output = "An unhandled exception occured: %s" % ex + + if not process or process.returncode != 0: + LOG.write("Error %d:\n=== OUTPUT ===\n%s\n" % (errorcode, output)) + LOG.close() + sys.exit(errorcode) + + return output + +if __name__ == "__main__": + starttime = time.time() + + if len(sys.argv) != 2: + sys.stderr.write("Usage: %s task_id\n" % sys.argv[0]) + sys.exit(11) + + taskid = sys.argv[1] + try: + int(taskid) + except: + sys.stderr.write("Task ID may only contain digits.\n") + sys.exit(12) + + savedir = workdir = "%s/%s" % (CONFIG["SaveDir"], taskid) + + if CONFIG["UseWorkDir"]: + workdir = "%s/%s" % (CONFIG["WorkDir"], taskid) + + if not os.path.isdir(savedir): + sys.stderr.write("Task '%s' does not exist.\n" % workdir) + sys.exit(13) + + try: + LOG = logger(taskid) + except Exception as ex: + sys.stderr.write("Unable to start logging for task '%s': %s.\n" % (taskid, ex)) + sys.exit(14) + + # check the crash directory for required files + for required_file in REQUIRED_FILES: + if not os.path.isfile("%s/crash/%s" % (savedir, required_file)): + LOG.write("Crash directory does not contain required file '%s'.\n" % required_file) + LOG.close() + sys.exit(15) + + # read architecture file + try: + arch_file = open("%s/crash/architecture" % savedir, "r") + arch = repoarch = arch_file.read() + arch_file.close() + except Exception as ex: + LOG.write("Unable to read architecture from 'architecture' file: %s.\n" % ex) + LOG.close() + sys.exit(16) + + # required hack for public repos + if arch == "i686": + repoarch = "i386" + + # read release, distribution and version from release file + try: + release_file = open("%s/crash/release" % savedir, "r") + release = release_file.read() + release_file.close() + except Exception as ex: + LOG.write("Unable to read distribution and version from 'release' file: %s.\n" % ex) + LOG.close() + sys.exit(17) + + version = distribution = None + for distro in RELEASE_PARSERS.keys(): + match = RELEASE_PARSERS[distro].match(release) + if match: + version = match.group(1) + distribution = distro + break + + if not version or not distribution: + LOG.write("Release '%s' is not supported.\n" % release) + LOG.close() + sys.exit(18) + + # read package file + try: + package_file = open("%s/crash/package" % savedir, "r") + crash_package = package_file.read() + package_file.close() + except Exception as ex: + LOG.write("Unable to read crash package from 'package' file: %s.\n" % ex) + LOG.close() + sys.exit(19) + + # read required packages from coredump + packages = "%s.%s" % (crash_package, arch) + try: + # ToDo: deal with not found build-ids + pipe = Popen(["/usr/share/abrt-retrace/coredump2packages.py", "%s/crash/coredump" % savedir, "--repos=retrace-%s-%s-%s*" % (distribution, version, arch)], stdout=PIPE).stdout + section = 0 + crash_package_or_component = None + for line in pipe.readlines(): + if line == "\n": + section += 1 + continue + elif 0 == section: + crash_package_or_component = line.strip() + elif 1 == section: + packages += " %s" % line.rstrip("\n") + elif 2 == section: + # Missing build ids + pass + pipe.close() + except Exception as ex: + LOG.write("Unable to obtain packages from 'coredump' file: %s.\n" % ex) + LOG.close() + sys.exit(20) + + # create mock config file + try: + mockcfg = open("%s/mock.cfg" % savedir, "w") + mockcfg.write("config_opts['root'] = 'chroot'\n") + mockcfg.write("config_opts['target_arch'] = '%s'\n" % arch) + mockcfg.write("config_opts['chroot_setup_cmd'] = 'install %s shadow-utils abrt-addon-ccpp gdb'\n" % packages) + mockcfg.write("config_opts['basedir'] = '%s'\n" % workdir) + mockcfg.write("config_opts['plugin_conf']['ccache_enable'] = False\n") + mockcfg.write("config_opts['plugin_conf']['yum_cache_enable'] = False\n") + mockcfg.write("config_opts['plugin_conf']['root_cache_enable'] = False\n") + mockcfg.write("\n") + mockcfg.write("config_opts['yum.conf'] = \"\"\"\n") + mockcfg.write("[main]\n") + mockcfg.write("cachedir=/var/cache/yum\n") + mockcfg.write("debuglevel=1\n") + mockcfg.write("reposdir=/dev/null\n") + mockcfg.write("logfile=/var/log/yum.log\n") + mockcfg.write("retries=20\n") + mockcfg.write("obsoletes=1\n") + mockcfg.write("gpgcheck=0\n") + mockcfg.write("assumeyes=1\n") + mockcfg.write("syslog_ident=mock\n") + mockcfg.write("syslog_device=\n") + mockcfg.write("\n") + mockcfg.write("#repos\n") + mockcfg.write("\n") + mockcfg.write("[fedora]\n") + mockcfg.write("name=fedora\n") + mockcfg.write("baseurl=file://%s/%s-%s-%s/\n" % (CONFIG["RepoDir"], distribution, version, arch)) + mockcfg.write("failovermethod=priority\n") + mockcfg.write("\n") + mockcfg.write("[fedora-debuginfo]\n") + mockcfg.write("name=fedora-debuginfo\n") + mockcfg.write("baseurl=file://%s/%s-%s-%s-debuginfo/\n" % (CONFIG["RepoDir"], distribution, version, arch)) + mockcfg.write("failovermethod=priority\n") + mockcfg.write("\n") + mockcfg.write("[updates]\n") + mockcfg.write("name=updates\n") + mockcfg.write("baseurl=file://%s/%s-%s-%s-updates/\n" % (CONFIG["RepoDir"], distribution, version, arch)) + mockcfg.write("failovermethod=priority\n") + mockcfg.write("\n") + mockcfg.write("[updates-debuginfo]\n") + mockcfg.write("name=updates-debuginfo\n") + mockcfg.write("baseurl=file://%s/%s-%s-%s-updates-debuginfo/\n" % (CONFIG["RepoDir"], distribution, version, arch)) + mockcfg.write("failovermethod=priority\n") + mockcfg.write("\n") + mockcfg.write("[updates-testing]\n") + mockcfg.write("name=updates-testing\n") + mockcfg.write("baseurl=file://%s/%s-%s-%s-updates-testing/\n" % (CONFIG["RepoDir"], distribution, version, arch)) + mockcfg.write("failovermethod=priority\n") + mockcfg.write("\n") + mockcfg.write("[updates-testing-debuginfo]\n") + mockcfg.write("name=updates-testing-debuginfo\n") + mockcfg.write("baseurl=file://%s/%s-%s-%s-updates-testing-debuginfo/\n" % (CONFIG["RepoDir"], distribution, version, arch)) + mockcfg.write("failovermethod=priority\n") + mockcfg.write("\n") + # custom ABRT repo with ABRT 2.0 binaries - obsolete after release of ABRT 2.0 + mockcfg.write("[abrt]\n") + mockcfg.write("name=abrt\n") + mockcfg.write("baseurl=http://repos.fedorapeople.org/repos/mtoman/abrt20/%s-%s/%s/\n" % (distribution, version, repoarch)) + mockcfg.write("failovermethod=priority\n") + mockcfg.write("\n") + mockcfg.write("\"\"\"\n") + mockcfg.close() + except Exception as ex: + LOG.write("Unable to create mock config file: %s.\n" % ex) + LOG.close() + sys.exit(21) + + # get count of tasks running before starting + prerunning = len(get_active_tasks()) - 1 + + # run retrace + mockr = "../../%s/mock" % savedir + + LOG.write("Initializing virtual root... ") + + retrace_run(25, ["mock", "init", "-r", mockr]) + retrace_run(26, ["mock", "-r", mockr, "--copyin", "%s/crash" % savedir, "/var/spool/abrt/crash"]) + retrace_run(27, ["touch", "%s/chroot/root/var/spool/abrt/crash/time" % workdir]) + + LOG.write("OK\n") + + try: + rootfile = open("%s/chroot/result/root.log" % workdir, "r") + rootlog = rootfile.read() + rootfile.close() + except Exception as ex: + LOG.write("Error reading root log: %s.\n" % ex) + rootlog = "Not found" + + # generate backtrace + LOG.write("Generating backtrace... ") + + retrace_run(28, ["mock", "shell", "-r", mockr, "--", "/usr/bin/abrt-action-generate-backtrace", "-d", "/var/spool/abrt/crash/"]) + retrace_run(29, ["mock", "-r", mockr, "--copyout", "/var/spool/abrt/crash/backtrace", savedir]) + retrace_run(30, ["chmod", "a+r", "%s/backtrace" % savedir]) + + LOG.write("OK\n") + + chroot_size = dir_size("%s/chroot/root" % workdir) + + # clean up temporary data + LOG.write("Cleaning up... ") + + retrace_run(31, ["mock", "-r", mockr, "--scrub=all"]) + retrace_run(32, ["rm", "-rf", "%s/mock.cfg" % savedir, "%s/crash" % savedir]) + + # ignore error: workdir = savedir => workdir is not empty + if CONFIG["UseWorkDir"]: + try: + os.rmdir(workdir) + except: + pass + + LOG.write("OK\n") + + # save crash statistics + LOG.write("Saving crash statistics... ") + + duration = int(time.time() - starttime) + + package_match = PACKAGE_PARSER.match(crash_package) + if not package_match: + package = crash_package + version = "unknown" + release = "unknown" + else: + package = package_match.group(1) + version = package_match.group(2) + release = package_match.group(4) + + crashstats = { + "taskid": int(taskid), + "package": package, + "version": version, + "release": release, + "arch": arch, + "starttime": int(starttime), + "duration": duration, + "prerunning": prerunning, + "postrunning": len(get_active_tasks()) - 1, + "chrootsize": chroot_size + } + + if not init_crashstats_db() or not save_crashstats(crashstats): + LOG.write("Error: %s\n" % crashstats) + else: + LOG.write("OK\n") + + # publish bactkrace and log + LOG.write("Finishing task... ") + + try: + os.rename("%s/backtrace" % savedir, "%s/retrace_backtrace" % savedir) + except Exception as ex: + LOG.write("Error: %s\n" % ex) + LOG.close() + sys.exit(35) + + LOG.write("OK\n") + LOG.write("Retrace took %d seconds.\n" % duration) + + LOG.write("\n=== ROOT LOG ===\n%s\n" % rootlog) + LOG.close() -- cgit