extras-buildsys/builder Builder.py, NONE, 1.1 BuilderMock.py, NONE, 1.1 main.py, NONE, 1.1 Config.py, 1.2, 1.3 Makefile, 1.4, 1.5 builder.py, 1.54, NONE

Daniel Williams (dcbw) fedora-extras-commits at redhat.com
Fri Apr 28 03:17:42 UTC 2006


Author: dcbw

Update of /cvs/fedora/extras-buildsys/builder
In directory cvs-int.fedora.redhat.com:/tmp/cvs-serv5292/builder

Modified Files:
	Config.py Makefile 
Added Files:
	Builder.py BuilderMock.py main.py 
Removed Files:
	builder.py 
Log Message:
2006-04-27  Dan Williams  <dcbw at redhat.com>

    Commit partial rework of builder<->server communcation.




--- NEW FILE Builder.py ---
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# Copyright 2006 Dan Williams <dcbw at redhat.com> and Red Hat, Inc.


import os
import sys
import socket
import time
import threading
import sha
import exceptions
import xmlrpclib
import OpenSSL
from plague import Commands
from plague import AuthedXMLRPCServer
from plague import HTTPServer
from plague import XMLRPCServerProxy

import Config
import BuilderMock


class BuilderInitException(Exception): pass

def get_hostname(cfg, bind_all):
    cfg_hostname = cfg.get_str("Passive", "hostname")
    if cfg_hostname and len(cfg_hostname):
        return cfg_hostname
    elif bind_all:
        return ''
    return socket.gethostname()

def determine_max_jobs(cfg):
    """ Simple max job calculator based on number of CPUs """

    import commands
    max_jobs = 1
    cmd = "/usr/bin/getconf _NPROCESSORS_ONLN"
    (s, o) = commands.getstatusoutput(cmd)
    if s == 0:
        try:
            max_jobs = int(o)
        except ValueError:
            pass
    return max_jobs

def log(string):
    sys.stdout.write(string + "\n")
    sys.stdout.flush()


class PassiveBuilderRequestHandler:
    def __init__(self, cfg, builder):
        self._builder = builder
        self._all_jobs = {} # unique id => awclass instance
        self._building_jobs_lock = threading.Lock()
        self._building_jobs = []
        self._cfg = cfg

    def _log(self, string):
        if self._cfg.get_bool("General", "debug"):
            print string

    def notify_job_done(self, archjob):
        self._building_jobs_lock.acquire()
        if archjob in self._building_jobs:
            self._building_jobs.remove(archjob)
        self._building_jobs_lock.release()

    def die(self, uniqid):
        try:
            job = self._all_jobs[uniqid]
            job.die()
        except KeyError:
            pass
        return 0

    def files(self, uniqid):
        try:
            job = self._all_jobs[uniqid]
            return job.files()
        except KeyError:
            pass
        return []
    
    def repo_unlocked(self, uniqid):
        try:
            job = self._all_jobs[uniqid]
            job.repo_unlocked()
        except KeyError:
            pass
        return 0

    def building_jobs(self):
        jobs = {}
        self._building_jobs_lock.acquire()
        building = 0
        for job in self._building_jobs:
            jobs[job.uniqid()] = job.status()
            building = building + 1
        free = self._max_jobs - building
        self._building_jobs_lock.release()
        return (jobs, free)

    def num_slots(self):
        (free_slots, max_slots) = self._builder.slots()
        return max_slots

    def job_status(self, uniqid):
        try:
            job = self._all_jobs[uniqid]
            return job.status()
        except KeyError:
            pass
        return ''

    def supported_targets(self):
        return self._builder.supported_targets()
            

class Builder(object):
    """ Abstract builder base object """
    def __init__(self, cfg):
        self._cfg = cfg
        self._certs = None
        self._max_slots = determine_max_jobs(cfg)
        self._seq_gen = Commands.SequenceGenerator()

        self._building_jobs_lock = threading.Lock()
        self._building_jobs = []
        self._all_jobs = {}

        if cfg.get_bool("SSL", "use_ssl"):
            hostname = get_hostname(self._cfg, False)
            key_file = os.path.join(cfg.get_str("SSL", "builder_key_and_cert_dir"), "%s.pem" % hostname)
            self._certs = {}
            self._certs['key_and_cert'] = key_file
            self._certs['ca_cert'] = cfg.get_str("SSL", "ca_cert")
            self._certs['peer_ca_cert'] = self._certs['ca_cert']

    def _log(self, string):
        if self._cfg.get_bool("General", "debug"):
            log(string)

    def new_builder(cfg, btype):
        if btype == 'passive':
            return PassiveBuilder(cfg)
        elif btype == 'active':
            return ActiveBuilder(cfg)
        else:
            return None
    new_builder = staticmethod(new_builder)

    def stop(self):
        pass

    def cleanup(self):
        return
        (building_jobs, free) = bcs.building_jobs()
        for jobid in building_jobs.keys():
            bcs.die(jobid)

        # wait for the jobs to clean up before quitting
        log("Waiting for running jobs to stop...")
        while True:
            (building_jobs, free) = bcs.building_jobs()
            if len(building_jobs.keys()) == 0:
                break
            try:
                log(".")
                time.sleep(0.5)
            except KeyboardInterrupt:
                break

    def slots(self):
        self._building_jobs_lock.acquire()
        free_slots = self._max_slots - len(self._building_jobs)
        self._building_jobs_lock.release()
        return (free_slots, self._max_slots)

    def supported_targets(self):
        targets = []
        for t in self._cfg.targets():
            td = t.target_dict()
            td['supported_arches'] = t.arches()
            targets.append(td)
        return targets

    def _get_target_cfg(self, target_dict):
        target_cfg = None

        # First try to find a target for buildarch specifically
        try:
            target_cfg = self._cfg.get_target(target_dict)
        except Config.InvalidTargetException:
            pass

        if not target_cfg:
            # If that doesn't work, just get a target that can build the arch
            try:
                target_cfg = self._cfg.get_target(target_dict, True)
            except Config.InvalidTargetException:
                pass

        return target_cfg

    def _new_job_for_arch(self, target_cfg, buildarch, srpm_url):
        """Creates a new mock build job given a particular build architecture."""

        if buildarch != 'noarch' and not BuilderMock.BuilderClassDict.has_key(buildarch):
            # we know nothing about the architecture 'buildarch'
            return None

        builder_class = None
        if buildarch == 'noarch':
            # Just grab the first available architecture from the ones we support
            builder_class = BuilderMock.BuilderClassDict[target_cfg.arches()[0]]
        elif buildarch in target_cfg.arches():
            builder_class = BuilderMock.BuilderClassDict[buildarch]

        # We'll throw a TypeError here if there's no available builder_class for this arch
        return builder_class(self, target_cfg, buildarch, srpm_url)

    def _start_new_job(self, target_dict, srpm_url):
        target_str = Config.make_target_string(target_dict['distro'], target_dict['target'], target_dict['arch'], target_dict['repo'])

        uniqid = -1
        msg = "Success"
        (free, max) = self.slots()
        if free <= 0:
            msg = "Error: Tried to build '%s' on target %s when already building" \
                        " maximum (%d) jobs" % (srpm_url, target_str, max)
            self._log(msg)
            return (uniqid, msg)

        target_cfg = self._get_target_cfg(target_dict)
        if not target_cfg:
            msg = "Error: Tried to build '%s' on target %s which isn't supported" % (srpm_url, target_str)
            self._log(msg)
            return (uniqid, msg)

        archjob = None
        try:
            archjob = self._new_job_for_arch(target_cfg, target_dict['arch'], srpm_url)
            uniqid = archjob.uniqid()
            self._all_jobs[uniqid] = archjob
            self._building_jobs_lock.acquire()
            self._building_jobs.append(archjob)
            self._building_jobs_lock.release()
            filename = os.path.basename(srpm_url)
            msg = "%s: started %s on %s arch %s at time %d" % (uniqid, filename,
                        target_str, target_dict['arch'], archjob.starttime())
#            job.start()
        except (OSError, TypeError), err:
            msg = "Failed request for %s on %s: '%s'" % (srpm_url,
                    target_str, err)

        self._log(msg)
        return (uniqid, msg)


class PassiveBuilder(Builder):
    """
    Passive builders initiate no communication of their own.  They wait
    for the build server to contact them, and therefore may not be used
    behind a firewall without holes being punched through it.
    """
    def __init__(self, cfg):
        Builder.__init__(self, cfg)
        self._http_server = None
        self._xmlrpc_server = None

    def _start_servers(self):
        # Start up the HTTP server thread which the build server
        # pulls completed RPMs from
        hostname = get_hostname(self._cfg, True)
        port = cfg.get_int("Passive", "fileserver_port")
        self._http_server = HTTPServer.PlgHTTPServerManager((hostname, port), work_dir, self._certs)
        self._http_server.start()

        log("Binding to address '%s' with arches: [%s]\n" % (hostname, string.join(build_arches, ",")))
        xmlrpc_port = cfg.get_int("Passive", "xmlrpc_port")
        try:
            if cfg.get_bool("SSL", "use_ssl") == True:
                self._xmlrpc_server = AuthedXMLRPCServer.AuthedSSLXMLRPCServer((hostname, xmlrpc_port), None, self._certs)
            else:
                self._xmlrpc_server = AuthedXMLRPCServer.AuthedXMLRPCServer((hostname, xmlrpc_port), None)
        except socket.error, e:
            if e[0] == 98:
                raise BuilderInitException("Error: couldn't bind to address '%s:%s'.  "  \
                           "Is the builder already running?\n" % (hostname, xmlrpc_port))

        brh = PassiveBuilderRequestHandler(cfg, self)
        self._xmlrpc_server.register_instance(brh)

    def work(self):
        self._start_servers()
        try:
            self._xmlrpc_server.serve_forever()
        except KeyboardInterrupt:
            pass

    def _stop_servers(self):
        self._http_server.stop()
        self._xmlrpc_server.stop()
        try:
            time.sleep(1)
        except KeyboardInterrupt:
            pass
        self._xmlrpc_server.server_close()

    def stop(self):
        Builder.stop(self)
        self._stop_servers()



# HACK: This class is a hack to work around SSL hanging issues,
# which cause the whole server to grind to a halt
class ActiveBuilderRequest(threading.Thread):
    def __init__(self, server, address, cmds):
        self._server = server
        self._address = address
        self._cmds = cmds
        self.done = False
        self.failed = False
        self.response = None
        threading.Thread.__init__(self)

    def run(self):
        self.setName("ActiveBuilderRequest: %s" % self._address)
        try:
            cmd_stream = Commands.serialize_to_command_stream(self._cmds)
            self.response = self._server.request(cmd_stream)
        except (socket.error, socket.timeout, OpenSSL.SSL.SysCallError, OpenSSL.SSL.Error, xmlrpclib.ProtocolError):
            self.failed = True
        except xmlrpclib.Fault, e:
            print "Builder Error (%s) in request(): server replied '%s'" % (self._address, e)
            self.failed = True
        self.done = True

class ActiveBuilder(Builder, threading.Thread):
    """
    Active builders initiate all communication to the builder server, and
    therefore may be used from behind a firewall.
    """
    _SERVER_CONTACT_INTERVAL = 20

    def __init__(self, cfg):
        Builder.__init__(self, cfg)
        self._stop = False
        self._last_comm = time.time() - self._SERVER_CONTACT_INTERVAL - 1
        self._queued_cmds = []
        self._xmlrpc_address = self._get_server_address(cfg.get_str("Active", "xmlrpc_port"))
        self._server = XMLRPCServerProxy.PlgXMLRPCServerProxy(self._xmlrpc_address, self._certs, timeout=20)
        threading.Thread.__init__(self)

    def _get_server_address(self, port):
        addr = self._cfg.get_str("Active", "server")
        if addr.startswith("http://"):
            addr = addr[7:]
        elif addr.startswith("https://"):
            addr = addr[8:]
        if self._cfg.get_bool("SSL", "use_ssl"):
            addr = "https://" + addr
        else:
            addr = "http://" + addr
        return addr + ":" + port

    def work(self):
        self.start()
        try:
            while not self._stop:
                time.sleep(60)
        except KeyboardInterrupt:
            pass

    def _get_default_commands(self):
        """Return a python list of serialized commands that the builder
        sends to the server every time it contacts the server."""

        defcmds = []

        # always send a target list
        next_seq = self._seq_gen.next()
        cmd = Commands.PlgCommandTargets(self.supported_targets(), next_seq)
        defcmds.append(cmd)

        # always send free & max slots
        next_seq = self._seq_gen.next()
        (free, max) = self.slots()
        cmd = Commands.PlgCommandSlots(free, max, next_seq)
        defcmds.append(cmd)

        return defcmds

    def _send_commands(self):
        """Send default commands, and any commands that we've queued up
        since the last time we sent commands to the server."""

        cmds = self._get_default_commands()
        cmds = cmds + self._queued_cmds

        # The actual XML-RPC request runs in a different thread because SSL
        # calls sometimes hang 
        req = ActiveBuilderRequest(self._server, self._xmlrpc_address, cmds)
        curtime = time.time()
        req.start()

        # Give the request 10s, otherwise forget about it
        while time.time() - curtime < 10:
            if req.done:
                break
            time.sleep(0.5)

        if req.done and not req.failed:
            self.queued_cmds = []
            return req.response
        return None

    def _dispatch_server_command(self, cmd):
        """Process a single command from the server."""

        if isinstance(cmd, Commands.PlgCommandNewJobReq):
            (uniqid, msg) = self._start_new_job(cmd.target_dict(), cmd.srpm_url())
            ack = Commands.PlgCommandNewJobAck(uniqid, msg, cmd.seq(), self._seq_gen.next())
            self._queued_cmds.append(ack)

    def _process_server_response(self, response):
        """Process the server's response."""

        if not response:
            # Something went wrong...
            return

        cmds = Commands.deserialize_command_stream(response)
        for cmd in cmds:
            self._dispatch_server_command(cmd)

    def run(self):
        """Main builder loop, send commands to and receive commands from
        the server every so often."""

        while not self._stop:
            if self._last_comm < time.time() - self._SERVER_CONTACT_INTERVAL:
                self._last_comm = time.time()
                resp = self._send_commands()
                self._process_server_response(resp)
            time.sleep(1)

    def stop(self):
        self._stop = True


--- NEW FILE BuilderMock.py ---
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# Copyright 2005 Dan Williams <dcbw at redhat.com> and Red Hat, Inc.


import threading
import sys
import os


import socket
import shutil
import string
import fcntl
import urllib
import errno
import exceptions
import threading
import sha
import time

from plague import ExecUtils
from plague import FileDownloader


def log(string):
    sys.stdout.write(string)
    sys.stdout.flush()

def get_url_for_file(cfg, file_path):
    """ Return a URL pointing to a particular file in our work dir """

    # Ensure the file we're turning into a URL lives in our builder work dir
    work_dir = cfg.get_str("Directories", "builder_work_dir")
    if not file_path.startswith(work_dir):
        return None
    file_part = file_path[len(work_dir) + 1:]
    port = "%s" % cfg.get_int("Network", "fileserver_port")
    if cfg.get_bool("SSL", "use_ssl"):
        method = "https://"
    else:
        method = "http://"
    hostname = get_hostname(cfg, False)
    full_url = "%s%s:%s/%s" % (method, hostname, port, file_part)
    return urllib.quote(full_url)

def _generate_uniqid(target_str, srpm_url):
    sum = sha.new()
    sum.update('%d %s %s' % (time.time(), target_str, srpm_url))
    return sum.hexdigest()


class BuilderMock(threading.Thread):
    """puts things together for an arch - baseclass for handling builds for 
       other arches"""

    def __init__(self, controller, target_cfg, buildarch, srpm_url):
        self._controller = controller
        self.buildarch = buildarch
        self._starttime = time.time()
        self._endtime = 0
        self._mockstarttime = 0
        self._uniqid = _generate_uniqid(str(target_cfg), srpm_url)
        self._status = 'init'
        self._die = False
        self._repo_locked = True
        self._repo_wait_start = 0
        self._files = []
        self._childpid = 0
        self._target_cfg = target_cfg
        self._builder_cfg = target_cfg.parent_cfg()
        self._srpm_url = srpm_url
        self._srpm_tries = 0
        self._log_fd = None
        self._mock_config = None
        self._done_status = ''
        self._mock_log = None
        self.buildroot = self._target_cfg.mock_config()

        self._work_dir = self._builder_cfg.get_str("Directories", "builder_work_dir")
        self._result_dir = os.path.join(self._work_dir, self._uniqid, "result")
        if not os.path.exists(self._result_dir):
            os.makedirs(self._result_dir)

        self._state_dir = os.path.join(self._work_dir, self._uniqid, "mock-state")
        if not os.path.exists(self._state_dir):
            os.makedirs(self._state_dir)

        logfile = os.path.join(self._result_dir, "job.log")
        self._log_fd = open(logfile, "w+")

        threading.Thread.__init__(self)

    def starttime(self):
        return self._starttime

    def endtime(self):
        return self._endtime

    def die(self):
        if self.is_done_status() or self._done_status == 'killed':
            return True
        self._die = True
        return True

    def _handle_death(self):
        self._die = False
        self._done_status = 'killed'
        self._log("Killing build process...\n")

        # Don't try to kill a running cleanup process
        if self._status != 'cleanup':
            # Kill a running non-cleanup mock process, if any
            if self._childpid:
                child_pgroup = 0 - self._childpid
                try:
                    # Kill all members of the child's process group
                    os.kill(child_pgroup, 9)
                except OSError, e:
                    self._log("ERROR: Couldn't kill child process group %d: %s\n" % (child_pgroup, e))
                else:
                    # Ensure child process is reaped
                    self._log("Waiting for mock process %d to exit...\n" % self._childpid)
                    try:
                        (pid, status) = os.waitpid(self._childpid, 0)
                    except OSError, e:
                        pass
                    self._log("Mock process %d exited.\n" % self._childpid)
                self._childpid = 0

            # Start cleanup up the job
            self._start_cleanup()

        self._log("Killed.\n");

    def _log(self, string):
        if string and self._log_fd:
            self._log_fd.write(string)
            self._log_fd.flush()
            os.fsync(self._log_fd.fileno())
            if self._builder_cfg.get_bool("General", "debug"):
                s = "%s: " % self._uniqid
                sys.stdout.write(s + string)
                sys.stdout.flush()

    def dl_callback(self, dl_status, cb_data, err_msg):
        url = cb_data
        if dl_status == 'done':
            self._status = 'downloaded'
            self._log("Retrieved %s.\n" % url)
        elif dl_status == 'failed':
            # If job was cancelled, just return
            if self.is_done_status():
                return

            # Retry up to 5 times
            self._srpm_tries = self._srpm_tries + 1
            if self._srpm_tries >= 5:
                self._status = 'failed'
                self._log("ERROR: Failed to retrieve %s.\n" % url)
            else:
                # retry the download
                self._status = 'init'
                self._log("ERROR: Failed to retrieve %s on attempt %d (%s).  Trying again...\n" % (url, self._srpm_tries, err_msg))

    def _copy_mock_output_to_log(self):
        if self._mock_log and os.path.exists(self._mock_log):
            ml = open(self._mock_log, "r")
            line = "foo"
            while len(line):
                line = ml.readline()
                if len(line):
                    self._log_fd.write(line)
            ml.close()
            os.remove(self._mock_log)
            self._mock_log = None

    def _start_build(self):
        self._log("Starting step 'building' with command:\n")
        if not os.path.exists(self._result_dir):
            os.makedirs(self._result_dir)
        if not os.path.exists(self._result_dir):
            os.makedirs(self._result_dir)

        # Set up build process arguments
        args = []
        builder_cmd = os.path.abspath(self._builder_cfg.get_str("General", "builder_cmd"))
        cmd = builder_cmd
        if self.arch_command and len(self.arch_command):
            arg_list = self.arch_command.split()
            for arg in arg_list:
                args.append(arg)
            cmd = os.path.abspath(arg_list[0])
        args.append(builder_cmd)
        args.append("-r")
        args.append(self.buildroot)
        args.append("--arch")
        args.append(self.buildarch)
        args.append("--resultdir=%s" % self._result_dir)
        args.append("--statedir=%s" % self._state_dir)
        args.append("--uniqueext=%s" % self._uniqid)
        args.append(self._srpm_path)
        self._log("   %s\n" % string.join(args))

        self._mock_log = os.path.join(self._result_dir, "mock-output.log")
        self._childpid = ExecUtils.exec_with_redirect(cmd, args, None, self._mock_log, self._mock_log)
        self._mockstarttime = time.time()
        self._status = 'prepping'

    def _start_cleanup(self):
        self._log("Cleaning up the buildroot...\n")

        args = []
        builder_cmd = os.path.abspath(self._builder_cfg.get_str("General", "builder_cmd"))
        cmd = builder_cmd
        if self.arch_command and len(self.arch_command):
            arg_list = self.arch_command.split()
            for arg in arg_list:
                args.append(arg)
            cmd = os.path.abspath(arg_list[0])
        args.append(builder_cmd)
        args.append("clean")
        args.append("--uniqueext=%s" % self._uniqid)
        args.append("-r")
        args.append(self.buildroot)

        self._log("   %s\n" % string.join(args))
        self._childpid = ExecUtils.exec_with_redirect(cmd, args, None, None, None)
        self._status = 'cleanup'

    def _mock_is_prepping(self):
        mock_status = self._get_mock_status()
        if mock_status:
            if mock_status == 'prep':
                return True
            elif mock_status == 'setu':
                return True
        return False

    def _mock_using_repo(self):
        mock_status = self._get_mock_status()
        if mock_status:
            if mock_status == 'init':
                return True
            elif mock_status == 'clea':
                return True
            elif mock_status == 'prep':
                return True
            elif mock_status == 'setu':
                return True
        return False

    def _mock_is_closed(self):
        mock_status = self._get_mock_status()
        if mock_status and mock_status == "done":
            return True
        return False

    def _get_mock_status(self):
        mockstatusfile = os.path.join(self._state_dir, 'status')
        if not os.path.exists(mockstatusfile):
            return None

        f = open(mockstatusfile, "r")
        fcntl.fcntl(f.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
        
        while True:
            try:
                f.seek(0, 0)
                string = f.read(4)
            except OSError, e:
                if e.errno == errno.EAGAIN:
                    try:
                        time.sleep(0.25)
                    except KeyboardInterrupt:
                        pass
                    continue
            else:
                if len(string) < 4:
                    continue
                break
        f.close()
        string = string.lower()
        return string

    def _read_mock_config(self):
        mockconfigfile = os.path.join(self._result_dir, 'mockconfig.log')
        if not os.path.exists(mockconfigfile):
            return None

        f = open(mockconfigfile, "r")
        contents = {}
        for line in f:
            (item, loc) = line.split('=')
            item = item.strip()
            loc = loc.strip()
            contents[item] = loc
        f.close()
        return contents

    def _status_init(self):
        self._log("Starting download of %s.\n" % self._srpm_url)
        self._status = 'downloading'
        target_dir = os.path.dirname(self._srpm_path)
        try:
            dl_thread = FileDownloader.FileDownloader(self.dl_callback, self._srpm_url, self._srpm_url,
                        target_dir, ['.src.rpm'], certs)
            dl_thread.start()
        except FileDownloader.FileNameException, e:
            self._status = 'failed'
            self._log("ERROR: Failed to begin SRPM download.  Error: '%s'   URL: %s\n" % (e, self._srpm_url))
            
    def _status_downloading(self):
        pass

    def _status_downloaded(self):
        # We can't start doing anything with yum until the build
        # server tells us the repo is unlocked.
        if not self._repo_locked:
            self._start_build()
        else:
            # Only show this message once
            if self._repo_wait_start <= 0:
                self._log("Waiting for repository to unlock before starting the build...\n")
                self._repo_wait_start = time.time()

            # Kill a job in 'downloaded' state after 30 minutes because
            # it's likely orphaned
            if time.time() > (self._repo_wait_start + (60 * 30)):
                self._log("Job waited too long for repo to unlock. Killing it...\n")
                self.die()

    def _watch_mock(self, good_exit, bad_exit):
        (aux_pid, status) = os.waitpid(self._childpid, os.WNOHANG)
        status = os.WEXITSTATUS(status)
        if aux_pid:
            self._childpid = 0
            if status == 0:
                self._done_status = good_exit
            elif status > 0:
                self._done_status = bad_exit

            self._copy_mock_output_to_log()
            self._start_cleanup()

    def _status_prepping(self):
        # Mock shouldn't exit at all during the prepping stage, if it does
        # something is wrong
        self._watch_mock('failed', 'failed')
        if self._status != 'prepping':
               return

        # We need to make sure that mock has dumped the status file withing a certain
        # amount of time, otherwise we can't tell what it's doing
        mockstatusfile = os.path.join(self._state_dir, 'status')
        if not os.path.exists(mockstatusfile):
            # something is wrong if mock takes more than 15s to write the status file
            if time.time() > self._mockstarttime + 15:
                self._mockstarttime = 0
                self._log("ERROR: Timed out waiting for the mock status file!  %s\n" % mockstatusfile)
                self.die()
        else:
            if not self._mock_config and self._mock_is_prepping():
                self._mock_config = self._read_mock_config()
            if not self._mock_using_repo():
                self._status = 'building'

    def _status_building(self):
        self._watch_mock('done', 'failed')

    def _status_cleanup(self):
        (aux_pid, status) = os.waitpid(self._childpid, os.WNOHANG)
        if aux_pid:
            self._childpid = 0
            # Mock exited
            if self._mock_config:
                if self._mock_config.has_key('rootdir'):
                    mock_root_dir = os.path.abspath(os.path.join(self._mock_config['rootdir'], "../"))
                    # Ensure we're actually deleteing the job's rootdir
                    if mock_root_dir.endswith(self._uniqid):
                        shutil.rmtree(mock_root_dir, ignore_errors=True)

                if self._mock_config.has_key('statedir'):
                    shutil.rmtree(self._mock_config['statedir'], ignore_errors=True)

                source_dir = os.path.abspath(os.path.join(self._mock_config['rootdir'], "../source"))
                # Ensure we're actually deleteing the job's sourcedir
                if source_dir.endswith(os.path.join(self._uniqid, "source")):
                    shutil.rmtree(source_dir, ignore_errors=True)

        # Ensure child process is reaped, if any
        if self._childpid:
            try:
                self._log("Waiting for child process %d to exit.\n" % self._childpid)
                (pid, status) = os.waitpid(self._childpid, 0)
            except OSError, e:
                self._childpid = 0
                pass

        self._copy_mock_output_to_log()

        self._files = self._find_files()
        self._status = self._done_status

    def _job_done(self):
        self._log("-----------------------\n")
        if self._status == 'done':
            self._log("Job completed successfully.\n")
        elif self._status == 'failed':
            self._log("Job failed due to build errors!  Please see build logs.\n")
        elif self._status == 'killed':
            self._log("Job failed because it was killed.\n")
        self._log("\n\n")

        if self._log_fd:
            self._log_fd.close()
            self._log_fd = None

    def run(self):
        # Print out a nice message at the start of the job
        target_dict = self._target_cfg.target_dict()
        target_str = "%s-%s-%s-%s" % (target_dict['distro'], target_dict['target'], target_dict['arch'], target_dict['repo'])
        self._log("""Starting job:
   Time: %s
   Target: %s
   UID: %s
   Architecture: %s
   SRPM: %s\n\n""" % (time.asctime(time.localtime(self._starttime)), target_str, self._uniqid, self.buildarch, self._srpm_url))

        try:
            srpm_filename = FileDownloader.get_base_filename_from_url(self._srpm_url, ['.src.rpm'])
            self._srpm_path = os.path.join(self._work_dir, self._uniqid, "source", srpm_filename)
        except FileDownloader.FileNameException, e:
            self._log("ERROR: SRPM file name was invalid.  Message: '%s'\n" % e)
            self._status = 'failed'

        # Main build job work loop
        while not self.is_done_status():
            if self._die:
                self._handle_death()

            # Execute operations for our current status
            try:
                func = getattr(self, "_status_%s" % self._status)
            except AttributeError:
                self._log("ERROR: internal builder inconsistency, didn't recognize status '%s'.\n" % self._status)
                self._status = 'failed'
            else:
                func()
            time.sleep(3)

        self._job_done()
        self._endtime = time.time()
        if self._childpid:
            self._log("ERROR: childpid was !NULL  (%d)" % self._childpid)
        self._controller.notify_job_done(self)

    def _find_files(self):
        # Grab the list of files in our job's result dir and URL encode them
        files_in_dir = os.listdir(self._result_dir)
        file_list = []
        self._log("\n")
        self._log("Output File List:\n")
        self._log("-----------------\n")
        for f in files_in_dir:
            file_url = get_url_for_file(self._builder_cfg, os.path.join(self._result_dir, f))
            if file_url:
                file_list.append(file_url)
                self._log("  Output File: %s\n" % urllib.unquote(file_url))
            else:
                self._log("  Error: Couldn't get file URL for file %s\n" % f)
        self._log("-----------------\n")
        return file_list

    def status(self):
        return self._status

    def uniqid(self):
        return self._uniqid

    def files(self):
        return self._files

    def repo_unlocked(self):
        self._repo_locked = False

    def is_done_status(self):
        if (self._status is 'done') or (self._status is 'killed') or (self._status is 'failed'):
            return True
        return False


class InvalidTargetError(exceptions.Exception): pass

class i386Arch(BuilderMock):
    def __init__(self, controller, target_cfg, buildarch, srpm_url):
        self.arch_command = '/usr/bin/setarch i686'
        BuilderMock.__init__(self, controller, target_cfg, buildarch, srpm_url)

class x86_64Arch(BuilderMock):
    def __init__(self, controller, target_cfg, buildarch, srpm_url):
        self.arch_command = ''
        BuilderMock.__init__(self, controller, target_cfg, buildarch, srpm_url)

class PPCArch(BuilderMock):
    def __init__(self, controller, target_cfg, buildarch, srpm_url):
        self.arch_command = '/usr/bin/setarch ppc32'
        BuilderMock.__init__(self, controller, target_cfg, buildarch, srpm_url)

class PPC64Arch(BuilderMock):
    def __init__(self, controller, target_cfg, buildarch, srpm_url):
        self.arch_command = ''
        BuilderMock.__init__(self, controller, target_cfg, buildarch, srpm_url)

class SparcArch(BuilderMock):
    def __init__(self, controller, target_cfg, buildarch, srpm_url):
        self.arch_command = '/usr/bin/sparc32'
        BuilderMock.__init__(self, controller, target_cfg, buildarch, srpm_url)

class Sparc64Arch(BuilderMock):
    def __init__(self, controller, target_cfg, buildarch, srpm_url):
        self.arch_command = '/usr/bin/sparc64'
        BuilderMock.__init__(self, controller, target_cfg, buildarch, srpm_url)


BuilderClassDict = {
                'i386':     i386Arch,
                'i486':     i386Arch,
                'i586':     i386Arch,
                'i686':     i386Arch,
                'athlon':   i386Arch,
                'x86_64':   x86_64Arch,
                'amd64':    x86_64Arch,
                'ia32e':    x86_64Arch,
                'ppc':      PPCArch,
                'ppc32':    PPCArch,
                'ppc64':    PPC64Arch,
                'sparc':    SparcArch,
                'sparcv8':  SparcArch,
                'sparcv9':  SparcArch,
                'sparc64':  Sparc64Arch
               }


--- NEW FILE main.py ---
#!/usr/bin/python -t
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# Copyright 2005 Dan Williams <dcbw at redhat.com> and Red Hat, Inc.


import os
import time
import sys
import signal
from plague import ArchUtils
from plague import daemonize
from optparse import OptionParser


sys.path.append('/usr/share/plague/builder')

import Config
import Builder
import BuilderMock

def log(string):
    sys.stdout.write(string)
    sys.stdout.flush()


def drop_privs(user):
    """
    We can't and shouldn't run mock as root, so we drop privs.
    We have to run the HTTP server as root though so it can chroot
    to the fileserver directory.
    """

    if os.getuid() != 0:
        return

    import pwd
    import grp

    eu = user
    try:
        uid = int(eu)
    except ValueError:
        try:
            pwrec = pwd.getpwnam(eu)
        except KeyError:
            print "Username '%s' does not exist." % eu
            return -1
        uid = pwrec[2]
    else:
        try:
            pwrec = pwd.getpwuid(uid)
        except KeyError:
            print "User ID %d doesn't exist." % uid
            return -1
    gid = pwrec[3]

    if uid == 0:
        print "You cannot use the superuser as the 'builder_user' option."
        return -1

    # Make ourself members of the mock group build_user's group
    try:
        mock_req = grp.getgrnam('mock')
    except KeyError:
        print "Mock group doesn't exist."
        return -1
    groups = [mock_req[2], gid]
    os.setgroups(groups)

    try:
        os.setgid(gid)
    except OSError:
        print "Could drop group privileges. Error: '%s'" % sys.exc_info()
        return -1

    os.setuid(uid)
    return 0


def determine_build_arches(cfg):
    """
    Attempt to autodetect what architectures this machine can build for,
    based on the kernel's uname.  If that fails, fall back to options in
    the config file.
    """

    machine_arch = os.uname()[4]
    arches = []
    try:
        arches = ArchUtils.supported_arches[machine_arch]
    except KeyError:
        print "Unknown machine type.  Please update plague's ArchUtils.py file."

    # Ok, grab from config file if we can't autodetermine
    if not len(arches):
        arches = cfg.get_list("General", "build_arches")

    for arch in arches:
        if not arch in BuilderMock.BuilderClassDict.keys():
            print "Unknown arch '%s' is not supported." % arch
            sys.exit(1)

    return arches


builder = None

def exit_handler(signum, frame):
    global builder
    log("Received SIGTERM, quitting...\n")
    builder.stop()

def main():
    global builder

    usage = "Usage: %s  [-p <pidfile>] [-l <logfile>] [-d] -c <configfile>" % sys.argv[0]
    parser = OptionParser(usage=usage)
    parser.add_option("-p", "--pidfile", default=None,
        help='file to write the PID to')
    parser.add_option("-l", "--logfile", default=None,
        help="location of file to write log output to")
    parser.add_option("-d", "--daemon", default=False, action="store_true",
        help="daemonize (i.e., detach from the terminal)")
    parser.add_option("-c", "--configfile", default=None,
        help="location of the builder config file")
    (opts, args) = parser.parse_args()

    if not opts.configfile:
        log("Must specify a config file.\n")
        sys.exit(1)

    # Load in the config
    cfg = Config.BuilderConfig(opts.configfile)
    btype = cfg.get_str("General", "comm_type")
    if btype != 'passive' and btype != 'active':
        log("Builder communication type must be 'active' or 'passive', not '%s'.  Exiting...\n" % btype)
        sys.exit(1)

    build_arches = determine_build_arches(cfg)
    if not len(build_arches):
        log("Cannot determine buildable arches for this builder.  Exiting...\n")
        sys.exit(1)

    cfg.load_target_configs(build_arches)
    if len(cfg.targets()) == 0:
        log("No useable mock buildroots configured.  Exiting...\n")
        sys.exit(1)

    if opts.daemon:
        ret=daemonize.createDaemon()
        if ret:
            log("Daemonizing failed!\n")
            sys.exit(2)

    if opts.pidfile:
        f = open(opts.pidfile, 'w', 1)
        f.write('%d\n' % os.getpid())
        f.flush()
        f.close()

    if opts.logfile:
        logf=open(opts.logfile, 'a')
        sys.stdout=logf
        sys.stderr=logf

    work_dir = cfg.get_str("Directories", "builder_work_dir")
    if not os.path.exists(work_dir) or not os.access(work_dir, os.R_OK):
        log("%s does not exist or is not readable.\n" % work_dir)
        os._exit(1)

    # Stop running as root
    if drop_privs(cfg.get_str("General", "builder_user")) == -1:
        builder.cleanup()
        os._exit(1)

    # Set up our termination handler
    signal.signal(signal.SIGTERM, exit_handler)

    builder = Builder.Builder.new_builder(cfg, btype)

    # Start doing stuff
    builder.work()

    log("Shutting down...\n")
    builder.stop()
    builder.cleanup()
    time.sleep(2)
    log(" done.\n");

    sys.stdout.flush()
    os._exit(0)


if __name__ == '__main__':
    main()


Index: Config.py
===================================================================
RCS file: /cvs/fedora/extras-buildsys/builder/Config.py,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -r1.2 -r1.3
--- Config.py	31 Aug 2005 14:13:02 -0000	1.2
+++ Config.py	28 Apr 2006 03:17:35 -0000	1.3
@@ -22,6 +22,10 @@
 
 class InvalidTargetException(Exception): pass
 
+def make_target_string(distro, target, arch, repo):
+    return "%s-%s-%s-%s" % (distro, target, arch, repo)
+
+
 class BuilderConfig(BaseConfig.BaseConfig):
     def __init__(self, filename):
         BaseConfig.BaseConfig.__init__(self, filename)
@@ -77,15 +81,21 @@
         self.set_option("General", "debug", "yes")
         self.set_option("General", "builder_cmd", "/usr/bin/mock")
         self.set_option("General", "builder_user", "plague-builder")
+        self.set_option("General", "comm_type", "active")
 
         self.add_section("Directories")
         self.set_option("Directories", "builder_work_dir", "/tmp/builder_work")
         self.set_option("Directories", "target_configs_dir", "/etc/plague/builder/targets")
 
-        self.add_section("Network")
-        self.set_option("Network", "fileserver_port", "8889")
-        self.set_option("Network", "xmlrpc_port", "8888")
-        self.set_option("Network", "hostname", "")
+        self.add_section("Active")
+        self.set_option("Active", "server", "")
+        self.set_option("Active", "xmlrpc_port", "8886")
+        self.set_option("Active", "fileserver_port", "8887")
+
+        self.add_section("Passive")
+        self.set_option("Passive", "hostname", "")
+        self.set_option("Passive", "xmlrpc_port", "8888")
+        self.set_option("Passive", "fileserver_port", "8889")
 
         self.add_section("SSL")
         self.set_option("SSL", "use_ssl", "yes")
@@ -159,6 +169,10 @@
     def parent_cfg(self):
         return self._parent_cfg
 
+    def __repr__(self):
+        return make_target_string(self._distro, self._target, self._basearch, self._repo)
+    __str__ = __repr__
+
     def save_default_config(self, filename=None):
         self.add_section("General")
         self.set_option("General", "distro", "fedora")


Index: Makefile
===================================================================
RCS file: /cvs/fedora/extras-buildsys/builder/Makefile,v
retrieving revision 1.4
retrieving revision 1.5
diff -u -r1.4 -r1.5
--- Makefile	25 Aug 2005 18:15:13 -0000	1.4
+++ Makefile	28 Apr 2006 03:17:35 -0000	1.5
@@ -14,11 +14,13 @@
 CONFIGDIR=$(DESTDIR)$(ETCDIR)/$(PKGNAME)/builder
 
 FILES = \
-	Config.py
+	Config.py \
+	Builder.py \
+	BuilderMock.py
 
 install:
 	$(MKDIR) -p $(DESTDIR)$(BINDIR)
-	$(INSTALL) -m 755 builder.py $(DESTDIR)/$(BINDIR)/$(PKGNAME)-builder
+	$(INSTALL) -m 755 main.py $(DESTDIR)/$(BINDIR)/$(PKGNAME)-builder
 	$(MKDIR) -p $(OTHERINSTDIR)
 	for file in $(FILES); do $(INSTALL) -m 644 $$file $(OTHERINSTDIR)/$$file; done
 	$(MKDIR) -p $(CONFIGDIR)


--- builder.py DELETED ---




More information about the fedora-extras-commits mailing list