diff options
author | Stanislav Ochotnicky <sochotnicky@gmail.com> | 2009-08-14 15:42:31 +0200 |
---|---|---|
committer | Stanislav Ochotnicky <sochotnicky@gmail.com> | 2009-08-14 15:42:31 +0200 |
commit | 965d0f69395bcc3f8d092f86cd07875eac004a38 (patch) | |
tree | 66f6a212380f8ca370196a71d06c767e9f8a4175 /src/collagen/tinderbox/__init__.py | |
parent | Add docstrings to most functions (diff) | |
download | collagen-965d0f69395bcc3f8d092f86cd07875eac004a38.tar.gz collagen-965d0f69395bcc3f8d092f86cd07875eac004a38.tar.bz2 collagen-965d0f69395bcc3f8d092f86cd07875eac004a38.zip |
Moved files around to simplify deployment
Diffstat (limited to 'src/collagen/tinderbox/__init__.py')
-rw-r--r-- | src/collagen/tinderbox/__init__.py | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/src/collagen/tinderbox/__init__.py b/src/collagen/tinderbox/__init__.py new file mode 100644 index 0000000..c5d235d --- /dev/null +++ b/src/collagen/tinderbox/__init__.py @@ -0,0 +1,550 @@ +import sys + +import socket +try: + import cPickle as pickle +except ImportError: + import pickle +from time import sleep +import os +import errno +import subprocess +from subprocess import CalledProcessError +import re +import string +from string import atoi +from traceback import print_exc, format_exc +from logger import log, init_logging + +import portage +try: + import portage._sets as psets +except ImportError: + import portage.sets as psets + +import collagen.protocol as protocol +from collagen.util import WritableObject, flatten_deps +from collagen.common.exceptions import ChrootPreparationException +import tinderbox_config as config + + + + + +class Tinderbox(object): + """ + Class for basic worker object called Tinderbox. Tinderbox connects to Matchbox + and asks for package(s) to compile. Tinderbox tries to compile given packages + and responds to Matchbox either with package contents or error messages + """ + + NOMERGE_PKGS=['sys-apps/portage'] + """These packages will never be (re)merged""" + + def __init__(self): + self.hostname = config.MATCHBOX_HOST + self.port = config.MATCHBOX_PORT + self.sock = None + + self.settings = portage.config(clone=portage.settings) + self.trees = portage.create_trees() + self.settings["PORTAGE_VERBOSE"]="1" + self.settings.backup_changes("PORTAGE_VERBOSE") + self.setconf = psets.SetConfig([], self.settings, self.trees) + init_logging('/tmp') + + + def start_tinderbox(self): + """ + This function starts tinderbox process (connects to Matchbox + server) and then starts compiling packages requested by + Matchbox. + """ + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((self.hostname,self.port)) + + while 1: + msg = protocol.GetNextPackage() + msg_pickled = pickle.dumps(msg) + self.sock.sendall(msg_pickled) + reply = self.sock.recv(1024) + reply_unpickled = pickle.loads(reply) + + if type(reply_unpickled) is protocol.GetNextPackageReply: + gnp = reply_unpickled + print "going to compile: %s\nuse flags: %s" %\ + (gnp.package_name,gnp.use_flags) + package = Package(gnp.package_name, gnp.version, gnp.use_flags) + sleep(5) + try: + self.emerge_package(package) + except Exception, e: + log.error("Fatal error when emerging package %s, see backtrace" % package.name) + log.error(format_exc()) + else: + print "Unknown reply: %s" % reply_unpickled + + + + def emerge_package(self, package): + """ + Top level emerge function for compiling packages + + @param package: package to be compiled, optionally with + version/useflag information + @type package: tinderbox Package + @return: None + """ + log.debug("emerge_package starting for %s" % package.name) + settings = self.settings + + porttree = self.trees[portage.root]['porttree'] + portdb = porttree.dbapi + if not package.version: + # we are compiling ALL versions of package + allversions = portdb.xmatch('match-all', package.name) + else: + # we were told exact version to compile + allversions = ["%s-%s" % (package.name, package.version)] + + for pkg in allversions: + archs = portdb.aux_get(pkg, ["KEYWORDS"])[0] + archs = archs.split() + if settings["ARCH"] not in archs and "~%s" % settings["ARCH"] not in archs: + log.warning("Was asked to compile %s but it doesn't have our arch in KEYWORDS" % pkg) + continue + deps = portdb.aux_get(pkg,["DEPEND"]) + + deps = portage.dep.paren_reduce(deps[0]) + + settings.setcpv(pkg, mydb=portdb) + use_enabled = set(settings["PORTAGE_USE"].split()) + iuse = set(settings["IUSE"].split()) + + # only count deps enabled by USE flags + use_deps = portage.dep.use_reduce(deps, list(use_enabled & iuse)) + use_deps = self._normalize_dependencies(use_deps) + + use_deps = flatten_deps(use_deps) + + log.debug("calling create_dep_groups for pkg %s use_deps %s" % (pkg, use_deps)) + dep_groups = self.create_dep_groups(use_deps) + + # prepare chroot & fork & do work + try: + subprocess.check_call([config.MK_CHROOT_SCRIPT,"-s",config.STAGE_TARBALL, + config.BASE_CHROOT, config.WORK_CHROOT]) + except CalledProcessError, cpe: + raise ChrootPreparationException("Chroot preparation for %s failed with error code: %d" + % (pkg, cpe.returncode)) + except OSError, ose: + raise ChrootPreparationException("Chroot preparation for %s failed with error(%d): %s\n\ + Check your settings" + % (pkg, ose.errno, ose.strerror)) + + self.child_pid = os.fork() + childpid = self.child_pid + if 0 == childpid: + # we are the child! + try: + # setup logging! + os.chroot(config.WORK_CHROOT) + os.chdir("/") + init_logging(config.CHROOT_LOGS) + pkgname, pkgver, pkgrev = portage.pkgsplit(pkg) + + if pkgrev is "r0": + package.version = pkgver + else: + package.version = "%s-%s" % (pkgver, pkgrev) + package.reinit() + self._emerge_package_subprocess(pkg, dep_groups, package) + + sys.exit(0) + except Exception, e: + print_exc() + print "Something went really bad and we need logging, stat!" + log.error("Unrecoverable error in tinderbox slave, see backtrace for possible solutions:") + log.error(format_exc()) + sys.exit(1) + + + (retpid, status) = os.waitpid(childpid, 0) + if 0 != status: + # something went really wrong, grab all the info we can + print "Something went really bad and we need logging, stat!" + log.error("Emerge of package %s failed with error code: %d" % (pkg, status)) + + try: + package_infos = self._load_info('package_infos') + except IOError, e: + log.error("Loading package_infos file failed, something has gone wrong in subprocess apparently") + package.attachments["error"] = "Loading package_infos failed, package was not compiled with any dependency combination" + package.version = 'all' + package_infos = [package.get_info()] + + msg = protocol.AddPackageInfo(package_infos) + self.sock.sendall(pickle.dumps(msg)) + + + def _emerge_package_subprocess(self, pkg, dep_groups, package): + """ + This functions is running inside chrooted environment. It's + purpose is to try and emerge packages group-by-group and then + package specified. All information is stored inside + package_infos to be retrieved later by calling function + + @param pkg: cpv string (category/package-version) + @type pkg: string + @param dep_groups: dependency groups as returned by create_dep_groups function + @type dep_groups: list of lists of dependency cpvs + @param package: package class for filling out information + @type package: tinderbox.Package + + @return None + """ + # We are chrooted inside WORK_CHROOT remember! + porttree = self.trees[portage.root]['porttree'] + portdb = porttree.dbapi + vartree = self.trees[portage.root]["vartree"] + ebuild = portdb.findname(pkg) + + package_infos = [] + + settings = self.settings + for group in dep_groups: + dep_failed = None + deps_processed = [] + for dep in group: + + try: + # this will need to change since it's only a quick hack so that + # we don't have to do dep resolution ourselves + import _emerge as emerge + dep_emergepid = os.fork() + # we need to run emerge_main() in child process since a lot of stuff in there + # likes to call sys.exit() and we don't want that do we? + if 0 == dep_emergepid: + try: + extra_use = dep[0] + if extra_use: + os.environ["USE"]=" ".join(extra_use) + os.environ["FEATURES"] = "-strict" + sys.argv = ["emerge","--verbose", "--usepkg", "--buildpkg" ,"=%s" % dep[1]] + exit_code = emerge.emerge_main() + sys.exit(exit_code) + except Exception, e: + print_exc() + log.error(format_exc()) + sys.exit(1) + ret = os.waitpid(dep_emergepid, 0) + if 0 != ret[1]: + raise Exception("emerge_main() failed with error code %d" % ret[1]) + except Exception, e: + log.error(format_exc()) + log.error("Unable to merge dependency %s for package %s" % (dep, pkg)) + dep_failed = dep[1] + + deps_processed.append(dep) + + settings.setcpv(dep[1], mydb=portdb) + dep_use_enabled = set(settings["PORTAGE_USE"].split()) + dep_iuse = set(settings["IUSE"].split()) + dep_name, dep_ver, dep_rev = portage.pkgsplit(dep[1]) + + real_use_enabled = list(dep_use_enabled & dep_iuse) + if dep[0]: + for useflag in dep[0]: + if useflag.startswith('-'): + if 0 is not l.count(useflag[1:]): + real_use_enabled.remove(useflag[1:]) + elif 0 == real_use_enabled.count(useflag): + real_use_enabled.append(useflag) + + if dep_rev is'r0': + dep_ver_full = dep_ver + else: + dep_ver_full = "%s-%s" % (dep_ver, dep_rev) + dep_pkg = Package(dep_name, dep_ver_full, real_use_enabled) + if dep_failed == dep[1]: + build_dir = self.get_build_dir(dep_failed) + self._add_attachment(dep_pkg, "%s/temp/build.log" % build_dir) + self._add_attachment(dep_pkg, "%s/temp/environment" % build_dir) + package.attachments['emerge_info']=self.get_emerge_info() + package_infos.append(dep_pkg.get_info()) + + + if dep_failed: + log.error("Unable to emerge package %s with deps %s" % (pkg, group)) + # TODO unmerge succeeded deps + self._add_attachment(package, "/var/log/emerge.log") + self._add_attachment(package, "%s/tinderbox.log" % config.CHROOT_LOGS) + package.attachments['emerge_info']=self.get_emerge_info() + package.depends = [x[1] for x in group] + package_infos.append(package.get_info()) + continue + settings.setcpv(pkg, mydb=portdb) + + ret = portage.doebuild(ebuild, "merge", portage.root, settings, debug = False, tree="porttree") + if 0 != ret: + # error installing, grab logs + self._add_attachment(package, "%s/build.log" % settings["T"]) + self._add_attachment(package, "%s/environment" % settings["T"]) + self._add_attachment(package, "%s/tinderbox.log" % config.CHROOT_LOGS) + self._add_attachment(package, "/var/log/emerge.log") + package.attachments['emerge_info']=self.get_emerge_info() + + + package.depends = [x[1] for x in deps_processed] + package_infos.append(package.get_info()) + + for dep in group: + if dep[1] in self.setconf.getSetAtoms("system"): + pass + dep_cat, dep_pv = portage.catsplit(dep[1]) + + ret = portage.unmerge(dep_cat, dep_pv, portage.root, settings, True, vartree=vartree) + if 0 != ret: + log.error("Unable to unmerge dep %s" % dep) + + pkg_cat, pkg_pv = portage.catsplit(pkg) + ret = portage.unmerge(pkg_cat, pkg_pv, portage.root, settings, True, vartree=vartree) + if ret != 0: + log.error("Unable to unmerge package %s" % dep) + + self._save_info('package_infos', package_infos) + + def _add_attachment(self, pkg, path): + """ + Adds file content with given path to package pkg as attachment + + @param pkg: Package that will get embedded data + @type pkg: tinderbox.Package + @param path: Path to attachment file + @type path: string + + @return None + """ + try: + attfile = open(path,"r") + except IOError, (errno, strerror): + print "Unable to read file %s: %d: %s" % (path, errno, strerror) + log.warning("Unable to read file (%s) %d: %s" % (path, errno, strerror)) + pkg.attachments[os.path.basename(path)] = "Unable to read file %s" % path + else: + pkg.attachments[os.path.basename(path)] = attfile.read() + attfile.close() + del attfile + + def _save_info(self, key, data): + """ + Save data inside CHROOT_LOGS directory, under name 'key'. This + function is called from within _emerge_package_subprocess to + save data for parent process + + + @param key: key used to identify data inside CHROOT_LOGS directory + @type key: string + @param data: data to be saved + @rtype: None + """ + outfile = open("%s/%s" % (config.CHROOT_LOGS, key), "w") + pickle.dump(data, outfile) + outfile.close() + + def _load_info(self, key): + """ + Load data from CHROOT_LOGS directory (under WORK_CHROOT) with filename 'key' + + + @param key: key used to identify data inside directory + @type key: string + @rtype: depends on key + @returns: data loaded from filename, usually blob + """ + infile = open("%s/%s/%s" % (config.WORK_CHROOT, config.CHROOT_LOGS, + key),"r") + return pickle.load(infile) + + + + def get_emerge_info(self): + """ + @rtype: string + @returns: emerge --info output + + """ + infoout = WritableObject() + sys.stdout = infoout + + cfg, trees, mtimedb = emerge.load_emerge_config() + emerge.action_info(cfg, trees, [], []) + # REDO withouth emerge.action_info + ret = sys.stdout.content + sys.stdout = sys.__stdout__ + + return string.join(infoout.content,sep='') + + def _normalize_dependencies(self, deps): + """ + Normalizes dependencies by replacing '||' by with alternative + + @param deps: depedencies (list of package atoms) + @type deps: List + @rtype: List + @returns: Normalized list of package atoms with replaced || groups + + Example: + >>> from tinderbox import Tinderbox as tb + >>> t = tb() + >>> t._normalize_dependencies(['||', ['=virtual/jdk-1.5*', '=virtual/jdk-1.4*'], 'app-arch/unzip']) + ['=virtual/jdk-1.5*', 'app-arch/unzip'] + >>> + """ + log.debug("normalize_dependencies called with deps: %s" % deps) + new_deps = [] + for i in range(len(deps)): + if '||' == deps[i]: + next = deps[i+1] + if type(next) is list and len(next) > i: + deps[i+1] = next[0] + elif type(deps[i]) == list: + new_deps.extend(self._normalize_dependencies(deps[i])) + else: + new_deps.append(deps[i]) + return new_deps + + def create_dep_groups(self, deps): + """ + Create dependency groups from package dependencies + + Every valid version of dep (according to input spec) has to be at least in + one output dependency group + + @param deps: dependencies of package (atoms) + @type deps: List + @rtype: List of Lists + @returns: List of dependency groups (list of dependency versions) together with special use flags needed + + Example: + Input: ['=dev-libs/glib-2*', '>=net-fs/samba-3.0.0', 'x11-libs/libX11','dev-util/subversion[-dso]'] + Output: + [[(None,'net-fs/samba-3.2.11'),(None, 'dev-libs/glib-2.18.4-r1'),(None, 'x11-libs/libX11-1.1.2-r1'), (['-dso'],'dev-util/subversion-1.5.5'], + [(None,'net-fs/samba-3.0.32'),(None, 'dev-libs/glib-2.18.4-r1'),(None, 'x11-libs/libX11-1.1.3-r1'), (['-dso'],'dev-util/subversion-1.5.5'], + [(None,'net-fs/samba-3.0.32'),(None, 'dev-libs/glib-2.20.3'),(None, 'x11-libs/libX11-1.1.3'), (['-dso'],'dev-util/subversion-1.5.5'], + """ + log.debug("create_dep_groups called with deps %s" % deps) + result = None + porttree = self.trees[portage.root]['porttree'] + portdb = porttree.dbapi + deps_expanded = [] + max_dep_versions = 0 + for dep in deps: + if dep[0].startswith('!') or dep in self.NOMERGE_PKGS: + continue + dep_useflag = list(portage.dep.dep_getusedeps(dep)) + if 0 == len(dep_useflag): + dep_useflag = None + if not portage.dep.isvalidatom(dep): + log.error("%s is not valid atom in %s" % (dep, str(deps))) + continue + depversions = portdb.xmatch('match-all',dep) + depversions = [(dep_useflag,x) for x in depversions] + deps_expanded.append(depversions) + if len(depversions) > max_dep_versions: + max_dep_versions = len(depversions) + + for dep in deps_expanded: + while len(dep) != 0 and len(dep) < max_dep_versions: + dep.append(dep[0]) + + result = [] + for i in range(max_dep_versions): + group = [] + for dep in deps_expanded: + if 0 == len(dep): + continue + group.append(dep.pop()) + result.append(group) + return result + + def get_build_dir(self, cpv): + settings = self.settings + bldprefix = settings["PORTAGE_TMPDIR"]+"/portage" + return os.path.join(bldprefix, cpv) + + + + +class Package(object): + + def __init__(self, name, version, use_flags): + """ + @param name: category/name of given package (excluding version/release) + @type name: string + @param version: version part of CPV e.g. 1.2.0-r4 + @type version: string + @param use_flags: list of enabled use flags for given package + @type use_flags: list + + """ + self.name = name + self.version = version + self.use_flags = use_flags + self.reinit() + + def reinit(self): + self.content = None + self.attachments = {} + self.depends = None + + + def get_info(self): + """Returns protocol.PackageInfo with information about this package + + @returns: PackageInfo for sending across network + @rtype: protocol.PackageInfo + """ + pi = protocol.PackageInfo() + pi.name = self.name + pi.version = self.version + pi.use_flags = self.use_flags + if self.content is None: + self.content = self.get_package_contents() + pi.content = self.content + pi.attachments = self.attachments + if 0 < len(self.attachments): + pi.error = 1 + pi.depends = self.depends + pi.profile = self._get_current_profile() + return pi + + def _get_current_profile(self): + ret = None + try: + profpath = os.readlink("%s/etc/make.profile" % portage.root) + ret = profpath[profpath.rindex("profiles")+len("profiles")+1:] + except Exception, e: + print e + return ret + + + def get_package_contents(self): + """Returns package contents as dict with paths as keys + + data values are tuples of information (such as hash and size) + + @returns: package contents (or {} if it's not installed) + @rtype: dict + """ + vartree = portage.db[portage.root]["vartree"] + cpv = "%s-%s" % (self.name, self.version) + if not vartree.dbapi.cpv_exists(cpv): + # maybe raise exception? instead + return {} + + cat, pkg = portage.catsplit(cpv) + dblink = portage.dblink(cat, pkg, portage.root, vartree.settings, + treetype="vartree", vartree=vartree) + + return dblink.getcontents() |