| #!/usr/bin/env python |
| # |
| # Copyright (C) 2017 Igalia S.L. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # * Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # |
| # * Redistributions in binary form must reproduce the above |
| # copyright notice, this list of conditions and the following disclaimer |
| # in the documentation and/or other materials provided with the |
| # distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| # |
| import sys |
| import signal |
| import os |
| import argparse |
| import subprocess |
| import tempfile |
| import shutil |
| import socket |
| import json |
| import traceback |
| import multiprocessing |
| from time import sleep |
| |
| test_buildbot_master_tac = """ |
| import os |
| from twisted.application import service |
| try: |
| from buildbot.master.bot import BuildMaster |
| except: |
| from buildbot.master import BuildMaster |
| |
| basedir = os.path.dirname(os.path.realpath(__file__)) |
| configfile = r'master.cfg' |
| |
| application = service.Application('buildmaster') |
| BuildMaster(basedir, configfile).setServiceParent(application) |
| """ |
| |
| worker_buildbot_master_tac = """ |
| import os |
| from twisted.application import service |
| from buildslave.bot import BuildSlave |
| |
| basedir = os.path.dirname(os.path.realpath(__file__)) |
| buildmaster_host = 'localhost' |
| port = 17000 |
| slavename = '%(worker)s' |
| passwd = '1234' |
| keepalive = 600 |
| usepty = 1 |
| |
| application = service.Application('buildslave') |
| BuildSlave(buildmaster_host, port, slavename, passwd, basedir, keepalive, usepty).setServiceParent(application) |
| """ |
| |
| |
| def check_tcp_port_open(address, port): |
| s = socket.socket() |
| try: |
| s.connect((address, port)) |
| return True |
| except: |
| return False |
| |
| |
| def upgrade_db_needed(log): |
| try: |
| with open(log) as f: |
| for l in f: |
| if 'upgrade the database' in l: |
| return True |
| except: |
| return False |
| return False |
| |
| |
| def create_tempdir(tmpdir=None): |
| if tmpdir is not None: |
| if not os.path.isdir(tmpdir): |
| raise ValueError('%s is not a directory' % tmpdir) |
| return tempfile.mkdtemp(prefix=os.path.join(os.path.abspath(tmpdir), 'tmp')) |
| return tempfile.mkdtemp() |
| |
| |
| def print_if_error_stdout_stderr(cmd, retcode, stdout=None, stderr=None, extramsg=None): |
| if retcode != 0: |
| if type(cmd) == type([]): |
| cmd = ' '.join(cmd) |
| print('WARNING: "%s" returned %s status code' % (cmd, retcode)) |
| if stdout is not None: |
| print(stdout) |
| if stderr is not None: |
| print(stderr) |
| if extramsg is not None: |
| print(extramsg) |
| |
| |
| def setup_master_workdir(configdir, base_workdir): |
| master_workdir = os.path.join(base_workdir, 'master') |
| print('Copying files from %s to %s ...' % (configdir, master_workdir)) |
| shutil.copytree(configdir, master_workdir) |
| print('Generating buildbot files at %s ...' % master_workdir) |
| with open(os.path.join(master_workdir, 'buildbot.tac'), 'w') as f: |
| f.write(test_buildbot_master_tac) |
| mkpwd_cmd = ['./make_passwords_json.py'] |
| mkpwd_process = subprocess.Popen(mkpwd_cmd, cwd=master_workdir, |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| stdout, stderr = mkpwd_process.communicate() |
| print_if_error_stdout_stderr(mkpwd_cmd, mkpwd_process.returncode, stdout, stderr) |
| return master_workdir |
| |
| |
| def wait_for_master_ready(master_workdir): |
| master_ready_check_counter = 0 |
| while True: |
| if os.path.isfile(os.path.join(master_workdir, '.master-is-ready')): |
| return |
| if master_ready_check_counter > 60: |
| raise RuntimeError('ERROR: Aborting after waiting 60 seconds for the master to start.') |
| sleep(1) |
| master_ready_check_counter += 1 |
| |
| |
| def start_master(master_workdir): |
| # This is started via multiprocessing. We set a new process group here |
| # to be able to reliably kill this subprocess and all of its child on clean. |
| os.setsid() |
| buildmasterlog = os.path.join(master_workdir, 'buildmaster.log') |
| dbupgraded = False |
| retry = True |
| if check_tcp_port_open('localhost', 8710): |
| print('ERROR: There is some process already listening in port 8170') |
| return 1 |
| while retry: |
| retry = False |
| print('Starting the twistd process ...') |
| twistd_cmd = ['twistd', '-l', buildmasterlog, '-noy', 'buildbot.tac'] |
| twistd_process = subprocess.Popen(twistd_cmd, cwd=master_workdir, |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| while twistd_process.poll() is None: |
| if check_tcp_port_open('localhost', 8710): |
| print('Test buildmaster ready!.\n\n' |
| + ' - See buildmaster log:\n' |
| + ' tail -f %s\n' % buildmasterlog |
| + ' - Open a browser to:\n' |
| + ' http://localhost:8710\n' |
| + ' - Credentials for triggering manual builds:\n' |
| + ' login: committer@webkit.org\n' |
| + ' password: committerpassword\n') |
| with open(os.path.join(master_workdir, '.master-is-ready'), 'w') as f: |
| f.write('ready') |
| twistd_process.wait() |
| return 0 |
| sleep(1) |
| stdout, stderr = twistd_process.communicate() |
| if twistd_process.returncode == 0 and upgrade_db_needed(buildmasterlog) and not dbupgraded: |
| retry = True |
| dbupgraded = True |
| print('Upgrading the database ...') |
| upgrade_cmd = ['buildbot', 'upgrade-master', master_workdir] |
| upgrade_process = subprocess.Popen(upgrade_cmd, cwd=master_workdir, |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| stdout, stderr = upgrade_process.communicate() |
| print_if_error_stdout_stderr(upgrade_cmd, upgrade_process.returncode, stdout, stderr) |
| else: |
| print_if_error_stdout_stderr(twistd_cmd, twistd_process.returncode, stdout, stderr, |
| 'Check the log at %s' % buildmasterlog) |
| return 0 |
| |
| |
| def get_list_workers(master_workdir): |
| password_list = os.path.join(master_workdir, 'passwords.json') |
| with open(password_list) as f: |
| passwords = json.load(f) |
| list_workers = [] |
| for worker in passwords.keys(): |
| list_workers.append(str(worker)) |
| return list_workers |
| |
| |
| def start_worker(base_workdir, worker): |
| # This is started via multiprocessing. We set a new process group here |
| # to be able to reliably kill this subprocess and all of its child on clean. |
| os.setsid() |
| worker_workdir = os.path.join(base_workdir, worker) |
| os.mkdir(worker_workdir) |
| with open(os.path.join(worker_workdir, 'buildbot.tac'), 'w') as f: |
| f.write(worker_buildbot_master_tac % {'worker': worker}) |
| twistd_cmd = ['twistd', '-l', 'worker.log', '-noy', 'buildbot.tac'] |
| twistd_worker_process = subprocess.Popen(twistd_cmd, cwd=worker_workdir, |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| try: |
| stdout, stderr = twistd_worker_process.communicate() |
| except: |
| twistd_worker_process.kill() |
| return |
| print_if_error_stdout_stderr(twistd_cmd, twistd_worker_process.returncode, stdout, stderr, |
| 'Check the log at %s' % os.path.join(worker_workdir, 'worker.log')) |
| |
| |
| def clean(temp_dir): |
| if os.path.isdir(temp_dir): |
| print('\n\nCleaning %s ... \n' % (temp_dir)) |
| # shutil.rmtree can fail if we hold an open file descriptor on temp_dir |
| # (which is very likely when cleaning) or if temp_dir is a NFS mount. |
| # Use rm instead that always works. |
| rm = subprocess.Popen(['rm', '-fr', temp_dir]) |
| rm.wait() |
| |
| |
| def cmd_exists(cmd): |
| return any(os.access(os.path.join(path, cmd), os.X_OK) |
| for path in os.environ['PATH'].split(os.pathsep)) |
| |
| |
| def check_buildbot_installed(): |
| if cmd_exists('twistd') and cmd_exists('buildbot'): |
| return |
| raise RuntimeError('Buildbot is not installed.') |
| |
| |
| def setup_virtualenv(base_workdir_temp): |
| if cmd_exists('virtualenv'): |
| print('Setting up virtualenv at %s ... ' % base_workdir_temp) |
| virtualenv_cmd = ['virtualenv', '-p', 'python2', 'venv'] |
| virtualenv_process = subprocess.Popen(virtualenv_cmd, cwd=base_workdir_temp, |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| stdout, stderr = virtualenv_process.communicate() |
| print_if_error_stdout_stderr(virtualenv_cmd, virtualenv_process.returncode, stdout, stderr) |
| virtualenv_bindir = os.path.join(base_workdir_temp, 'venv', 'bin') |
| virtualenv_pip = os.path.join(virtualenv_bindir, 'pip') |
| if not os.access(virtualenv_pip, os.X_OK): |
| print('Something went wrong setting up virtualenv' |
| 'Trying to continue using the system version of buildbot') |
| return |
| print('Setting up buildbot dependencies on the virtualenv ... ') |
| # The idea is to install the very same version of buildbot and its |
| # dependencies than the ones used for running https://build.webkit.org/about |
| pip_cmd = [virtualenv_pip, 'install', |
| 'buildbot==0.8.6p1', |
| 'buildbot-slave==0.8.6p1', |
| 'twisted==12.1.0', |
| 'jinja2==2.6', |
| 'sqlalchemy==0.7.8', |
| 'sqlalchemy-migrate==0.12.0'] |
| pip_process = subprocess.Popen(pip_cmd, cwd=base_workdir_temp, |
| stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| stdout, stderr = pip_process.communicate() |
| print_if_error_stdout_stderr(pip_cmd, pip_process.returncode, stdout, stderr) |
| os.environ['PATH'] = virtualenv_bindir + ':' + os.environ['PATH'] |
| return |
| print('WARNING: virtualenv not installed. ' |
| 'Trying to continue using the system version of buildbot') |
| |
| |
| def configdir_is_valid(configdir): |
| return(os.path.isdir(configdir) and |
| os.path.isfile(os.path.join(configdir, 'config.json')) and |
| os.path.isfile(os.path.join(configdir, 'master.cfg')) and |
| os.access(os.path.join(configdir, 'make_passwords_json.py'), os.X_OK)) |
| |
| |
| def main(configdir, basetempdir=None, no_clean=False, no_workers=False, use_system_version=False): |
| configdir = os.path.abspath(os.path.realpath(configdir)) |
| if not configdir_is_valid(configdir): |
| raise ValueError('The configdir %s dont contains the buildmaster files expected by this script' % configdir) |
| base_workdir_temp = os.path.abspath(os.path.realpath(create_tempdir(basetempdir))) |
| if base_workdir_temp.startswith(configdir): |
| raise ValueError('The temporal working directory %s cant be located inside configdir %s' % (base_workdir_temp, configdir)) |
| try: |
| if not use_system_version: |
| setup_virtualenv(base_workdir_temp) |
| check_buildbot_installed() |
| master_workdir = setup_master_workdir(configdir, base_workdir_temp) |
| master_runner = multiprocessing.Process(target=start_master, args=(master_workdir,)) |
| master_runner.start() |
| wait_for_master_ready(master_workdir) |
| if no_workers: |
| print(' - To manually attach a build worker use this info:\n' |
| + ' TCP port for the worker-to-master connection: 17000\n' |
| + ' worker-id: the one defined at %s\n' % os.path.join(master_workdir, 'passwords.json') |
| + ' password: 1234\n') |
| else: |
| worker_runners = [] |
| for worker in get_list_workers(master_workdir): |
| worker_runner = multiprocessing.Process(target=start_worker, args=(base_workdir_temp, worker,)) |
| worker_runner.start() |
| worker_runners.append(worker_runner) |
| print(' - Workers started!.\n' |
| + ' Check the log for each one at %s/${worker-name-id}/worker.log\n' % base_workdir_temp |
| + ' tail -f %s/*/worker.log\n' % base_workdir_temp) |
| for worker_runner in worker_runners: |
| worker_runner.join() |
| master_runner.join() |
| except: |
| traceback.print_exc() |
| finally: |
| try: |
| # The children may exit between the check and the kill call. |
| # Ignore any exception raised here. |
| for c in multiprocessing.active_children(): |
| # Send the signal to the whole process group. |
| # Otherwise some twistd sub-childs can remain alive. |
| os.killpg(os.getpgid(c.pid), signal.SIGKILL) |
| except: |
| pass |
| if not no_clean: |
| clean(base_workdir_temp) |
| sys.exit(0) |
| |
| |
| if __name__ == '__main__': |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--config-dir', help='Path to the directory of the build master config files. ' |
| 'Defauls to the directory where this script is located.', |
| dest='configdir', type=str, |
| default=os.path.dirname(__file__)) |
| parser.add_argument('--base-temp-dir', help='Path where the temporal working directory will be created. ' |
| 'Note: To trigger test builds with the test workers you need enough free space on that path.', |
| dest='basetempdir', default=None, type=str) |
| parser.add_argument('--no-clean', help='Do not clean the temporal working dir on exit.', |
| dest='no_clean', action='store_true') |
| parser.add_argument('--no-workers', help='Do not start the test workers.', |
| dest='no_workers', action='store_true') |
| parser.add_argument('--use-system-version', help='Instead of setting up a virtualenv with the buildbot version ' |
| 'used by build.webkit.org, use the buildbot version installed on this system.', |
| dest='use_system_version', action='store_true') |
| args = parser.parse_args() |
| main(args.configdir, args.basetempdir, args.no_clean, args.no_workers, args.use_system_version) |