Title: [225336] trunk/Tools
Revision
225336
Author
clo...@igalia.com
Date
2017-11-30 11:29:54 -0800 (Thu, 30 Nov 2017)

Log Message

Add a script for automatically running the buildbot configurations on a test environment.
https://bugs.webkit.org/show_bug.cgi?id=169084

Reviewed by Michael Catanzaro.

This new script does the following:
- Creates a temporal workingdir.
- Installs buildbot and its dependencies there (very same version used by the webkit.org buildbot master https://build.webkit.org/about ).
  This is important as there are problems that that may not be detected if you don't test with the same version that will be used on production.
- Copies the buildmaster config files to the temporal workingdir.
- Performs the required steps to start a buildmaster.
- Creates the required config for each slave and starts all of them.

The test buildmaster should be accessible at http://localhost:8710/ allowing to trigger manual builds.
This allows to test any configuration changes on this test environment that is pretty much identical to the production one.

* BuildSlaveSupport/build.webkit.org-config/run-buildbot-test.py: Added.
(check_tcp_port_open):
(upgrade_db_needed):
(create_tempdir):
(print_if_error_stdout_stderr):
(setup_master_workdir):
(wait_for_master_ready):
(start_master):
(get_list_workers):
(start_worker):
(clean):
(cmd_exists):
(check_buildbot_installed):
(setup_virtualenv):
(configdir_is_valid):
(main):

Modified Paths

Added Paths

Diff

Added: trunk/Tools/BuildSlaveSupport/build.webkit.org-config/run-buildbot-test.py (0 => 225336)


--- trunk/Tools/BuildSlaveSupport/build.webkit.org-config/run-buildbot-test.py	                        (rev 0)
+++ trunk/Tools/BuildSlaveSupport/build.webkit.org-config/run-buildbot-test.py	2017-11-30 19:29:54 UTC (rev 225336)
@@ -0,0 +1,347 @@
+#!/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 > 15:
+            raise RuntimeError('ERROR: Aborting after waiting 15 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:     commit...@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.7.2']
+        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='')
+    parser.add_argument('--no-workers', help='Do not start the test workers.',
+                        dest='no_workers', action='')
+    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='')
+    args = parser.parse_args()
+    main(args.configdir, args.basetempdir, args.no_clean, args.no_workers, args.use_system_version)
Property changes on: trunk/Tools/BuildSlaveSupport/build.webkit.org-config/run-buildbot-test.py
___________________________________________________________________

Added: svn:executable

+* \ No newline at end of property

Modified: trunk/Tools/ChangeLog (225335 => 225336)


--- trunk/Tools/ChangeLog	2017-11-30 19:17:55 UTC (rev 225335)
+++ trunk/Tools/ChangeLog	2017-11-30 19:29:54 UTC (rev 225336)
@@ -1,3 +1,38 @@
+2017-11-30  Carlos Alberto Lopez Perez  <clo...@igalia.com>
+
+        Add a script for automatically running the buildbot configurations on a test environment.
+        https://bugs.webkit.org/show_bug.cgi?id=169084
+
+        Reviewed by Michael Catanzaro.
+
+        This new script does the following:
+        - Creates a temporal workingdir.
+        - Installs buildbot and its dependencies there (very same version used by the webkit.org buildbot master https://build.webkit.org/about ).
+          This is important as there are problems that that may not be detected if you don't test with the same version that will be used on production.
+        - Copies the buildmaster config files to the temporal workingdir.
+        - Performs the required steps to start a buildmaster.
+        - Creates the required config for each slave and starts all of them.
+
+        The test buildmaster should be accessible at http://localhost:8710/ allowing to trigger manual builds.
+        This allows to test any configuration changes on this test environment that is pretty much identical to the production one.
+
+        * BuildSlaveSupport/build.webkit.org-config/run-buildbot-test.py: Added.
+        (check_tcp_port_open):
+        (upgrade_db_needed):
+        (create_tempdir):
+        (print_if_error_stdout_stderr):
+        (setup_master_workdir):
+        (wait_for_master_ready):
+        (start_master):
+        (get_list_workers):
+        (start_worker):
+        (clean):
+        (cmd_exists):
+        (check_buildbot_installed):
+        (setup_virtualenv):
+        (configdir_is_valid):
+        (main):
+
 2017-11-30  Aakash Jain  <aakash_j...@apple.com>
 
         [build.webkit.org] Move python code from master.cfg in separate files
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to