← Back to team overview

yellow team mailing list archive

[Merge] lp:~bac/charms/oneiric/buildbot-master/dynamic-relationship into lp:~yellow/charms/oneiric/buildbot-master/trunk

 

Brad Crittenden has proposed merging lp:~bac/charms/oneiric/buildbot-master/dynamic-relationship into lp:~yellow/charms/oneiric/buildbot-master/trunk.

Requested reviews:
  Launchpad Yellow Squad (yellow): code

For more details, see:
https://code.launchpad.net/~bac/charms/oneiric/buildbot-master/dynamic-relationship/+merge/91737

Cleaned up helpers.py and local.py.  Merged Graham's cleanup branch.
-- 
https://code.launchpad.net/~bac/charms/oneiric/buildbot-master/dynamic-relationship/+merge/91737
Your team Launchpad Yellow Squad is requested to review the proposed merge of lp:~bac/charms/oneiric/buildbot-master/dynamic-relationship into lp:~yellow/charms/oneiric/buildbot-master/trunk.
=== added file 'README.txt'
--- README.txt	1970-01-01 00:00:00 +0000
+++ README.txt	2012-02-06 23:20:22 +0000
@@ -0,0 +1,31 @@
+Demo of a single master.cfg file:
+
+This uses the standard buildbot Pyflakes example.
+
+Start with the buildbot-slave and buildbot-master charm in your charm
+repository.
+
+juju bootstrap
+juju deploy --repository=./charms local:buildbot-master
+juju deploy --repository=./charms local:buildbot-slave
+juju set buildbot-master extra-packages=git
+juju set buildbot-master config-file=`./charms/oneiric/buildbot-master/encode charms/oneiric/buildbot-master/examples/master.cfg`
+juju set buildbot-slave builders=runtests
+juju add-relation buildbot-slave buildbot-master
+
+Demo of a full buildbot master directory:
+
+juju bootstrap
+juju deploy --config=./charms/oneiric/buildbot-master/examples/lpbuildbot.yaml --repository=./charms local:buildbot-master
+juju deploy --config=./charms/oneiric/buildbot-slave/config.setuplxc.yaml --repository=./charms local:buildbot-slave
+juju add-relation buildbot-slave buildbot-master
+
+XXX Running setuplxc takes a long time.  For now, then, replace the
+juju deploy of the slave with these two lines.  This will mean that,
+when it works, the slave will connect to the master but when it tries
+to run tests it will fail (because the slave environment has not been
+set up).
+
+juju deploy --repository=./charms local:buildbot-slave
+juju set buildbot-slave builders=lucid_lp,lucid_db_lp
+

=== modified file 'config.yaml'
--- config.yaml	2012-02-01 15:39:49 +0000
+++ config.yaml	2012-02-06 23:20:22 +0000
@@ -13,10 +13,11 @@
     default: /tmp/buildbot
   config-file:
     description: |
-        A uuencoded master.cfg file.  Use of this configuration is
-        mutually exclusive with the use of config-transport,
-        config-url.  Use it like:
-          juju set buildbot-master config-file `uuencode master.cfg`
+        An encoded master.cfg file.  Use of this configuration is
+        mutually exclusive with the use of config-transport and
+        config-url.  Use it with the `encode` executable in this
+        charm, like this:
+          juju set buildbot-master config-file=`./encode ./examples/master.cfg`
     type: string
   config-transport:
     description: |

=== added file 'encode'
--- encode	1970-01-01 00:00:00 +0000
+++ encode	2012-02-06 23:20:22 +0000
@@ -0,0 +1,15 @@
+#!/usr/bin/python
+
+# encode examples/master.cfg  | python -c 'import sys, base64; base64.decode(sys.stdin, sys.stdout)'
+
+import base64
+import StringIO
+import sys
+
+filename = sys.argv[1]
+tmp = StringIO.StringIO()
+with open(filename) as f:
+     base64.encode(f, tmp)
+tmp.flush()
+tmp.seek(0)
+sys.stdout.write(''.join(l.strip() for l in tmp.readlines()))

=== added file 'examples/master.cfg'
--- examples/master.cfg	1970-01-01 00:00:00 +0000
+++ examples/master.cfg	2012-02-06 23:20:22 +0000
@@ -0,0 +1,118 @@
+# -*- python -*-
+# ex: set syntax=python:
+
+# This is a sample buildmaster config file. It must be installed as
+# 'master.cfg' in your buildmaster's base directory.
+
+# This is the dictionary that the buildmaster pays attention to. We also use
+# a shorter alias to save typing.
+c = BuildmasterConfig = {}
+
+####### BUILDSLAVES
+
+# The 'slaves' list defines the set of recognized buildslaves. Each element is
+# a BuildSlave object, specifying a username and password.  The same username and
+# password must be configured on the slave.
+from buildbot.buildslave import BuildSlave
+c['slaves'] = []
+
+# 'slavePortnum' defines the TCP port to listen on for connections from slaves.
+# This must match the value configured into the buildslaves (with their
+# --master option)
+c['slavePortnum'] = 9989
+
+####### CHANGESOURCES
+
+# the 'change_source' setting tells the buildmaster how it should find out
+# about source code changes.  Here we point to the buildbot clone of pyflakes.
+
+from buildbot.changes.gitpoller import GitPoller
+c['change_source'] = GitPoller(
+        'git://github.com/buildbot/pyflakes.git',
+        branch='master', pollinterval=1200)
+
+####### SCHEDULERS
+
+# Configure the Schedulers, which decide how to react to incoming changes.  In this
+# case, just kick off a 'runtests' build
+
+from buildbot.scheduler import Scheduler
+c['schedulers'] = []
+c['schedulers'].append(Scheduler(name="all", branch=None,
+                                 treeStableTimer=None,
+                                 builderNames=["runtests"]))
+
+####### BUILDERS
+
+# The 'builders' list defines the Builders, which tell Buildbot how to perform a build:
+# what steps, and which slaves can execute them.  Note that any particular build will
+# only take place on one slave.
+
+from buildbot.process.factory import BuildFactory
+from buildbot.steps.source import Git
+from buildbot.steps.shell import ShellCommand
+
+factory = BuildFactory()
+# check out the source
+factory.addStep(Git(repourl='git://github.com/buildbot/pyflakes.git', mode='copy'))
+# run the tests (note that this will require that 'trial' is installed)
+factory.addStep(ShellCommand(command=["trial", "pyflakes"]))
+
+from buildbot.config import BuilderConfig
+
+c['builders'] = [
+    BuilderConfig(name="runtests",
+      # Buildbot enforces that the slavenames list must not be empty. Our
+      # wrapper will remove the empty string and replace it with proper values.
+      slavenames=[''],
+      factory=factory),
+    ]
+
+####### STATUS TARGETS
+
+# 'status' is a list of Status Targets. The results of each build will be
+# pushed to these targets. buildbot/status/*.py has a variety to choose from,
+# including web pages, email senders, and IRC bots.
+
+c['status'] = []
+
+from buildbot.status import html
+from buildbot.status.web import auth, authz
+authz_cfg=authz.Authz(
+    # change any of these to True to enable; see the manual for more
+    # options
+    gracefulShutdown = False,
+    forceBuild = True, # use this to test your slave once it is set up
+    forceAllBuilds = False,
+    pingBuilder = False,
+    stopBuild = False,
+    stopAllBuilds = False,
+    cancelPendingBuild = False,
+)
+c['status'].append(html.WebStatus(http_port=8010, authz=authz_cfg))
+
+####### PROJECT IDENTITY
+
+# the 'projectName' string will be used to describe the project that this
+# buildbot is working on. For example, it is used as the title of the
+# waterfall HTML page. The 'projectURL' string will be used to provide a link
+# from buildbot HTML pages to your project's home page.
+
+c['projectName'] = "Pyflakes"
+c['projectURL'] = "http://divmod.org/trac/wiki/DivmodPyflakes";
+
+# the 'buildbotURL' string should point to the location where the buildbot's
+# internal web server (usually the html.WebStatus page) is visible. This
+# typically uses the port number set in the Waterfall 'status' entry, but
+# with an externally-visible host name which the buildbot cannot figure out
+# without some help.
+
+c['buildbotURL'] = "http://localhost:8010/";
+
+####### DB URL
+
+# This specifies what database buildbot uses to store change and scheduler
+# state.  You can leave this at its default for all but the largest
+# installations.
+c['db_url'] = "sqlite:///state.sqlite"
+

=== added file 'hooks/buildbot-relation-changed'
--- hooks/buildbot-relation-changed	1970-01-01 00:00:00 +0000
+++ hooks/buildbot-relation-changed	2012-02-06 23:20:22 +0000
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+
+import json
+import os
+
+from helpers import (
+    get_config,
+    load_pickle,
+    log,
+    relation_get,
+    relation_set,
+    save_pickle,
+    )
+from local import (
+    buildbot_reconfig,
+    generate_string,
+    slave_json,
+    )
+
+
+def get_or_create(key, prefix=''):
+    log("Retrieving {}.".format(key))
+    value = relation_get(key)
+    if not value:
+        log("Generating {}.".format(key))
+        value = generate_string(prefix)
+    log("{}: {}".format(key, value))
+    return value
+
+
+def update_slave_json(builders, name, passwd):
+    slave_info = slave_json.get()
+    slave_info[name] = (passwd, builders)
+    slave_json.set(slave_info)
+
+
+def main():
+    log("Retrieving builders.")
+    builders = filter(
+        None,
+        (b.strip() for b in relation_get('builders').split(',')))
+    log("builders: {}".format(builders))
+    name = get_or_create('name', prefix='slave-')
+    passwd = get_or_create('passwd')
+    update_slave_json(builders, name, passwd)
+    log("Reconfiguring buildbot.")
+    buildbot_reconfig()
+    log("Sending name and password to the slave.")
+    relation_set(name=name, passwd=passwd)
+
+
+if __name__ == '__main__':
+    log('BUILDBOT-RELATION-CHANGED HOOK:')
+    main()

=== modified file 'hooks/config-changed'
--- hooks/config-changed	2012-02-03 14:21:51 +0000
+++ hooks/config-changed	2012-02-06 23:20:22 +0000
@@ -1,148 +1,168 @@
 #!/usr/bin/python
 
+import base64
+import json
+import os
+import os.path
+import shutil
+import StringIO
+import sys
+
 from helpers import (
+    apt_get_install,
     command,
     DictDiffer,
     get_config,
     install_extra_repository,
-    load_pickle,
-    save_pickle,
+    log,
     run,
     )
-from subprocess import CalledProcessError
-import base64
-import os
-import os.path
-import sys
-
-# config_file is changed via juju like:
-# juju set buildbot-master config-file=`uuencode master.cfg`
-
-CONFIG_PICKLE = "config.pkl"
+from local import (
+    buildbot_create,
+    buildbot_reconfig,
+    config_json,
+    generate_string,
+    slave_json,
+    )
+
 REQUIRED_KEYS = [
     'buildbot-pkg',
     'installdir',
     ]
 SUPPORTED_TRANSPORTS = ['bzr']
 
-restart_required = False
 
-log = command('juju-log')
 bzr = command('bzr')
-apt_get_install = command('apt-get', 'install', '-y', '--force-yes')
-
-# Log the fact that we're about to begin the install step.
-log('--> config-changed')
-
-config = get_config()
-log(str(config))
-
-# If all of the required keys are not present in the configuration then exit
-# with an error.
-if not all(config.get(k) for k in REQUIRED_KEYS):
-    log('All required items not configured: {}'.format(REQUIRED_KEYS))
+
+
+def check_config(config):
+    # If all of the required keys are not present in the configuration
+    # then exit with an error.
+    if not all(config.get(k) for k in REQUIRED_KEYS):
+        log('All required items not configured: {}'.format(REQUIRED_KEYS))
+        return False
+    # If asked to fetch the config file then it must be a transport we support.
+    config_transport = config.get('config-transport')
+    config_url = config.get('config-url')
+    if config_url and config_transport not in SUPPORTED_TRANSPORTS:
+        log("{} is an unsupported transport".format(config_transport))
+        return False
+    return True
+
+
+def handle_config_changes(config, diff):
+    log('Updating buildbot configuration.')
+    log('Configuration changes seen:')
+    log(str(diff))
+
+    buildbot_pkg = config.get('buildbot-pkg')
+    extra_repo = config.get('extra-repository')
+    extra_pkgs = config.get('extra-packages')
+    restart_required = False
+
+    # Add a new repository if it was just added.
+    if extra_repo and 'extra-repository' in diff.added_or_changed:
+        install_extra_repository(extra_repo)
+        restart_required = True
+    if extra_pkgs and 'extra_packages' not in diff.unchanged:
+        apt_get_install(
+            *(pkg.strip() for pkg in extra_pkgs.split()))
+        restart_required = True
+    if 'buildbot-pkg' not in diff.unchanged:
+        apt_get_install(buildbot_pkg)
+        restart_required = True
+    return restart_required
+
+
+def configure_buildbot(config, diff):
+    buildbot_dir =  config.get('installdir')
+    config_file = config.get('config-file')
+    master_cfg_path = os.path.join(buildbot_dir, 'master.cfg')
+    config_transport = config.get('config-transport')
+    config_url = config.get('config-url')
+    restart_required = False
+
+    # Write the buildbot config to disk (fetching it if necessary).
+    log("CONFIG FILE: {}".format(config_file))
+    log("ADDED OR CHANGED: {}".format(diff.added_or_changed))
+    if config_file and 'config-file' in diff.added_or_changed:
+        buildbot_create(buildbot_dir)
+        # This file will be moved to master.cfg.original in
+        # initialize_buildbot().
+        with open(master_cfg_path, 'w') as f:
+            base64.decode(StringIO.StringIO(config_file), f)
+        log('config_file decoded and written.')
+        restart_required = True
+    elif (config_transport == 'bzr' and config_url and
+          'config-transport' not in diff.unchanged and
+          'config-url' not in diff.unchanged):
+        # If the branch is private then more setup needs to be done.  The
+        # gpg-agent needs to send the key over and the bzr launchpad-login
+        # needs to be set.
+        lp_id = config.get('config-user')
+        if lp_id:
+            bzr('launchpad-login', lp_id)
+
+        private_key = config.get('config-private-key')
+        if private_key:
+            # Set up the .ssh directory.
+            ssh_dir = os.expanduser('~/.ssh')
+            os.mkdir(ssh_dir)
+            os.chmod(ssh_dir, 0700)
+            with open(os.path.join(ssh_dir, 'lp_key', w)) as f:
+                f.write(base64.decode(private_key))
+        bzr('branch', '--use-existing-dir', config_url, buildbot_dir)
+        log('configuration fetched from {}'.format(config_url))
+        restart_required = True
+    return restart_required
+
+
+def initialize_buildbot(config):
+    # Initialize the buildbot directory and (re)start buildbot.
+    buildbot_dir =  config.get('installdir')
+    master_cfg_path = os.path.join(buildbot_dir, 'master.cfg')
+    shutil.move(master_cfg_path, master_cfg_path + '.original')
+    shutil.copy(
+        os.path.join(os.path.dirname(__file__), 'master.cfg'), master_cfg_path)
+    placeholder_path = os.path.join(buildbot_dir, 'placeholder.json')
+    if not os.path.exists(placeholder_path):
+        with open(placeholder_path, 'w') as f:
+            json.dump(generate_string("temporary-placeholder-"), f)
+    run('chown', '-R', 'ubuntu:ubuntu', buildbot_dir)
+    buildbot_reconfig()
+
+
+def main():
+    config = get_config()
     log(str(config))
-    sys.exit(1)
-
-prev_config = load_pickle(CONFIG_PICKLE)
-diff = DictDiffer(config, prev_config)
-
-if not diff.modified:
-    log("No configuration changes, exiting.")
-    sys.exit(0)
-
-log('Updating buildbot configuration.')
-log('Configuration changes seen:')
-log(str(diff))
-
-buildbot_pkg = config.get('buildbot-pkg')
-buildbot_dir =  config.get('installdir')
-config_file = config.get('config-file')
-config_transport = config.get('config-transport')
-config_url = config.get('config-url')
-extra_repo = config.get('extra-repository')
-extra_pkgs = config.get('extra-packages')
-
-# Add a new repository if it was just added.
-if extra_repo and 'extra-repository' in diff.added_or_changed:
-    install_extra_repository(extra_repo)
-    restart_required = True
-
-if extra_pkgs and 'extra_packages' not in diff.unchanged:
-    apt_get_install(extra_pkgs)
-    restart_required = True
-
-if 'buildbot-pkg' not in diff.unchanged:
-    apt_get_install(buildbot_pkg)
-    restart_required = True
-
-# Ensure the install directory exists.
-if not os.path.exists(buildbot_dir):
-    os.makedirs(buildbot_dir)
-
-# If asked to fetch the config file then it must be a transport we support.
-if config_url and config_transport not in SUPPORTED_TRANSPORTS:
-    log("{} is an unsupported transport".format(config_transport))
-    sys.exit(1)
-
-# Write the buildbot config to disk (fetching it if necessary).
-if config_file and 'config-file' not in diff.unchanged:
+    if not check_config(config):
+        log("Configuration not valid.")
+        sys.exit(1)
+    prev_config = config_json.get()
+    diff = DictDiffer(config, prev_config)
+
+    if not diff.modified:
+        log("No configuration changes, exiting.")
+        sys.exit(0)
+
+    # Ensure the install directory exists.
+    buildbot_dir = config.get('installdir')
     if not os.path.exists(buildbot_dir):
         os.makedirs(buildbot_dir)
-    with open(os.path.join(buildbot_dir, 'master.cfg', 'w')) as f:
-        f.write(base64.decode(config_file))
-    log('config_file decoded and written.')
-    restart_required = True
-elif (config_transport == 'bzr' and config_url and
-      'config-transport' not in diff.unchanged and
-      'config-url' not in diff.unchanged):
-    # If the branch is private then more setup needs to be done.  The
-    # gpg-agent needs to send the key over and the bzr launchpad-login
-    # needs to be set.
-    lp_id = config.get('config-user')
-    if lp_id:
-        bzr('launchpad-login', lp_id)
-
-    private_key = config.get('config-private-key')
-    if private_key:
-        # Set up the .ssh directory.
-        ssh_dir = os.expanduser('~/.ssh')
-        os.mkdir(ssh_dir)
-        os.chmod(ssh_dir, 0700)
-        with open(os.path.join(ssh_dir, 'lp_key', w)) as f:
-            f.write(base64.decode(private_key))
-    bzr('branch', '--use-existing-dir', config_url, buildbot_dir)
-    run('chown', '-R', 'ubuntu:ubuntu', buildbot_dir)
-    log('configuration fetched from {}'.format(config_url))
-    restart_required = True
-else:
-    # Configuration file specifiers are unchanged or unrecognized.
-    pass
-
-# Restart buildbot if it is running.
-if restart_required:
-    pidfile = os.path.join(buildbot_dir, 'twistd.pid')
-    if os.path.exists(pidfile):
-        buildbot_pid = open(pidfile).read().strip()
-        try:
-            # Is buildbot running?
-            run('kill', '-0', buildbot_pid)
-        except CalledProcessError:
-            # Buildbot isn't running, so no need to reconfigure it.
-            pass
-        else:
-            # Buildbot is running, reconfigure it.
-            log('Reconfiguring buildbot')
-            run('buildbot', 'reconfig', buildbot_dir)
-            log('Buildbot reconfigured')
+
+    restart_required = (
+        handle_config_changes(config, diff) or
+        configure_buildbot(config, diff))
+
+    master_cfg_path = os.path.join(buildbot_dir, 'master.cfg')
+    if restart_required and os.path.exists(master_cfg_path):
+        initialize_buildbot(config)
     else:
-        # Buildbot isn't running so start afresh but only if properly
-        # configured.
-        if os.path.exists(os.path.join(buildbot_dir, 'master.cfg')):
-            run('buildbot', 'start', buildbot_dir)
-else:
-    log("Configuration changed but didn't require restarting.")
-
-save_pickle(config, CONFIG_PICKLE)
-
-log('<-- config-changed')
+        log("Configuration changed but didn't require restarting.")
+
+    config_json.set(config)
+
+
+if __name__ == '__main__':
+    log('CONFIG-CHANGED HOOK:')
+    main()

=== modified file 'hooks/helpers.py'
--- hooks/helpers.py	2012-02-06 14:54:04 +0000
+++ hooks/helpers.py	2012-02-06 23:20:22 +0000
@@ -5,13 +5,19 @@
 
 __metaclass__ = type
 __all__ = [
+    'apt_get_install',
     'command',
     'DictDiffer',
+    'generate_string',
     'get_config',
+    'get_value_from_line',
+    'grep',
     'install_extra_repository',
     'load_pickle',
     'log',
     'run',
+    'relation_get',
+    'relation_set',
     'save_pickle',
     'unit_info',
     ]
@@ -51,6 +57,7 @@
 
 
 log = command('juju-log')
+apt_get_install = command('apt-get', 'install', '-y', '--force-yes')
 
 
 def run(*args):
@@ -81,11 +88,42 @@
     return prev_config
 
 
+def relation_get(*args):
+    cmd = command('relation-get')
+    return cmd(*args).strip()
+
+
+def relation_set(**kwargs):
+    cmd = command('relation-set')
+    args = ['{}={}'.format(k, v) for k, v in kwargs.items()]
+    return cmd(*args)
+
+
 def save_pickle(obj, filepath):
     with open(filepath, 'w') as fd:
         pickle.dump(obj, fd)
 
 
+def unit_info(service_name, item_name, data=None):
+    if data is None:
+        data = yaml.safe_load(run('juju', 'status'))
+    services = data['services'][service_name]
+    units = services['units']
+    item = units.items()[0][1][item_name]
+    return item
+
+
+def grep(content, filename):
+    with open(filename) as f:
+        for line in f:
+            if re.match(content, line):
+                return line.strip()
+
+
+def get_value_from_line(line):
+    return line.split('=')[1].strip('"\' ')
+
+
 class DictDiffer:
     """
     Calculate the difference between two dictionaries as:
@@ -152,10 +190,24 @@
         return s
 
 
-def unit_info(service_name, item_name, data=None):
-    if data is None:
-        data = yaml.safe_load(run('juju', 'status'))
-    services = data['services'][service_name]
-    units = services['units']
-    item = units.items()[0][1][item_name]
-    return item
+class Serializer:
+
+    def __init__(self, path, default=None, serialize=None, deserialize=None):
+        self.path = path
+        log("Serializer path: " + self.path)
+        self.default = default or {}
+        self.serialize = serialize or json.dump
+        self.deserialize = deserialize or json.load
+
+    def exists(self):
+        return os.path.exists(self.path)
+
+    def get(self):
+        if self.exists():
+            with open(self.path) as f:
+                return self.deserialize(f)
+        return self.default
+
+    def set(self, data):
+        with open(self.path, 'w') as f:
+            self.serialize(data, f)

=== modified file 'hooks/install'
--- hooks/install	2012-02-03 14:21:51 +0000
+++ hooks/install	2012-02-06 23:20:22 +0000
@@ -1,34 +1,49 @@
 #!/usr/bin/python
 
-from helpers import command, get_config, run, log
-from subprocess import CalledProcessError
 import os
 import shutil
-
-log = command('juju-log')
-
-# Log the fact that we're about to begin the install step.
-log('--> install')
+from subprocess import CalledProcessError
+
+from helpers import (
+    apt_get_install,
+    get_config,
+    log,
+    run,
+    )
+from local import (
+    config_json,
+    slave_json,
+    )
 
 config = get_config()
-log("config:")
-log(str(config))
-
-buildbot_dir =  config['installdir']
-run('apt-get', 'install', '-y', 'sharutils', 'bzr')
-
-# Install the extra repository
-# Install the initially configured version of buildbot.
-
-# Since we may be installing into a pre-existing service, ensure the
-# buildbot directory is removed.
-if os.path.exists(buildbot_dir):
-    try:
-        run('buildbot', 'stop', buildbot_dir)
-    except CalledProcessError:
-        # It probably wasn't running; just ignore the error.
-        pass
-    shutil.rmtree(buildbot_dir)
-
-# Log the fact that the install step is done.
-log('<-- install')
+
+
+def bootstrap(buildbot_dir):
+    apt_get_install('sharutils', 'bzr')
+    # Since we may be installing into a pre-existing service, ensure the
+    # buildbot directory is removed.
+    if os.path.exists(buildbot_dir):
+        try:
+            run('buildbot', 'stop', buildbot_dir)
+        except (CalledProcessError, OSError):
+            # This usually happens because buildbot hasn't been
+            # installed yet, or that it wasn't running; just ignore the
+            # error.
+            pass
+        shutil.rmtree(buildbot_dir)
+    # Initialize the cached config so that old configs don't hang around
+    # after the service is torn down.
+    config_json.set({})
+    slave_json.set({})
+
+
+def main():
+    config = get_config()
+    log("config:")
+    log(str(config))
+    bootstrap(config['installdir'])
+
+
+if __name__ == '__main__':
+    log('INSTALL HOOK:')
+    main()

=== added file 'hooks/local.py'
--- hooks/local.py	1970-01-01 00:00:00 +0000
+++ hooks/local.py	2012-02-06 23:20:22 +0000
@@ -0,0 +1,132 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Shared functions for the buildbot master and slave"""
+
+__metaclass__ = type
+__all__ = [
+    'buildbot_reconfig',
+    'buildslave_start',
+    'buildslave_stop',
+    'config_json'
+    'create_slave',
+    'slave_json',
+    ]
+
+import os
+import subprocess
+import uuid
+
+from helpers import (
+    get_config,
+    log,
+    run,
+    Serializer,
+    )
+from subprocess import CalledProcessError
+from textwrap import dedent
+
+
+def _get_buildbot_dir():
+    config = get_config()
+    return config.get('installdir')
+
+
+def generate_string(prefix=""):
+    """Generate a unique string and return it."""
+    return prefix + uuid.uuid4().hex
+
+
+def _get_slave_info_path():
+    return os.path.join('/', 'tmp', 'slave_info.json')
+
+
+def buildbot_create(buildbot_dir):
+    """Create a buildbot instance in `buildbot_dir`."""
+    if not os.path.exists(os.path.join(buildbot_dir, 'buildbot.tac')):
+        return run('buildbot', 'create-master', buildbot_dir)
+
+
+def buildbot_running(buildbot_dir):
+    pidfile = os.path.join(buildbot_dir, 'twistd.pid')
+    if os.path.exists(pidfile):
+        buildbot_pid = open(pidfile).read().strip()
+        try:
+            # Is buildbot running?
+            run('kill', '-0', buildbot_pid)
+        except CalledProcessError:
+            return False
+        return True
+    return False
+
+
+def buildbot_stop():
+    buildbot_dir = _get_buildbot_dir()
+    if buildbot_running(buildbot_dir):
+        # Buildbot is running, stop it.
+        log('Stopping buildbot')
+        run('buildbot', 'stop', buildbot_dir)
+
+
+def buildbot_reconfig():
+    buildbot_dir = _get_buildbot_dir()
+    pidfile = os.path.join(buildbot_dir, 'twistd.pid')
+    running = False
+    if os.path.exists(pidfile):
+        buildbot_pid = open(pidfile).read().strip()
+        try:
+            # Is buildbot running?
+            run('kill', '-0', buildbot_pid)
+        except CalledProcessError:
+            # Buildbot isn't running, so no need to reconfigure it.
+            pass
+        else:
+            # Buildbot is running, reconfigure it.
+            log('--> Reconfiguring buildbot')
+            # XXX as root! :-(
+            # reconfig is broken in 0.8.3 (Oneiric)
+            # run('buildbot', 'reconfig', buildbot_dir)
+            run('buildbot', 'stop', buildbot_dir)
+            run('buildbot', 'start', buildbot_dir)
+            log('<-- Reconfiguring buildbot')
+            running = True
+    if not running:
+        # Buildbot isn't running so start afresh.
+        if os.path.exists(os.path.join(buildbot_dir, 'master.cfg')):
+            log('--> Starting buildbot')
+            # XXX as root! :-(
+            run('buildbot', 'start', buildbot_dir)
+            log('<-- Starting buildbot')
+
+
+def _get_tac_filename(buildbot_dir):
+    return os.path.join(buildbot_dir, 'buildbot.tac')
+
+
+def buildslave_stop(buildbot_dir=None):
+    if buildbot_dir is None:
+        buildbot_dir = _get_buildbot_dir()
+    exit_code = subprocess.call(['buildslave', 'stop', buildbot_dir])
+    tac_file = _get_tac_filename(buildbot_dir)
+    if os.path.exists(tac_file):
+        os.remove(tac_file)
+    return exit_code
+
+
+def buildslave_start(buildbot_dir=None):
+    if buildbot_dir is None:
+        buildbot_dir = _get_buildbot_dir()
+    return subprocess.call(['buildslave', 'start', buildbot_dir])
+
+
+def create_slave(name, passwd, host='localhost', buildbot_dir=None):
+    if buildbot_dir is None:
+        buildbot_dir = _get_buildbot_dir()
+    if not os.path.exists(buildbot_dir):
+        os.makedirs(buildbot_dir)
+    return subprocess.call([
+        'buildslave', 'create-slave', buildbot_dir, host, name, passwd])
+
+
+slave_json = Serializer('/tmp/slave_info.json')
+config_json = Serializer('/tmp/config.json')

=== added file 'hooks/master.cfg'
--- hooks/master.cfg	1970-01-01 00:00:00 +0000
+++ hooks/master.cfg	2012-02-06 23:20:22 +0000
@@ -0,0 +1,38 @@
+# -*- python -*-
+# ex: set syntax=python:
+
+with open('master.cfg.original') as f:
+    exec f
+
+import uuid
+import json
+
+# Stability in this name keeps the Buildbot UI stable across live reconfigs.
+with open('placeholder.json') as f:
+    name_of_dummy_slave = json.load(f)
+
+# Now BuildmasterConfig is in the locals, configured as master.cfg.original
+# wanted it.
+c = BuildmasterConfig
+
+# This is a dict.  The keys are the slave names.  The values are a
+# pair of (password, builders).  "builders" is a list.
+with open('slave_info.json') as f:
+    slave_info = json.load(f)
+
+# Add slaves.
+from buildbot.buildslave import BuildSlave
+c['slaves'].append(BuildSlave(name_of_dummy_slave, uuid.uuid4().hex))
+for name, (passwd, builders) in slave_info.items():
+    c['slaves'].append(BuildSlave(name, passwd))
+
+# Add slaves to builders.
+for builder in c['builders']:
+    name = builder.name
+    current_slaves = list(filter(None, builder.slavenames))
+    for slavename, (passwd, builders) in slave_info.items():
+        if name in builders:
+            current_slaves.append(slavename)
+    if not current_slaves:
+        current_slaves.append(name_of_dummy_slave)
+    builder.slavenames = current_slaves

=== removed file 'hooks/relation-name-relation-broken'
--- hooks/relation-name-relation-broken	2012-01-13 16:21:47 +0000
+++ hooks/relation-name-relation-broken	1970-01-01 00:00:00 +0000
@@ -1,2 +0,0 @@
-#!/bin/sh
-# This hook runs when the full relation is removed (not just a single member)

=== removed file 'hooks/relation-name-relation-changed'
--- hooks/relation-name-relation-changed	2012-01-13 16:21:47 +0000
+++ hooks/relation-name-relation-changed	1970-01-01 00:00:00 +0000
@@ -1,9 +0,0 @@
-#!/bin/bash
-# This must be renamed to the name of the relation. The goal here is to
-# affect any change needed by relationships being formed, modified, or broken
-# This script should be idempotent.
-juju-log $JUJU_REMOTE_UNIT modified its settings
-juju-log Relation settings:
-relation-get
-juju-log Relation members:
-relation-list

=== removed file 'hooks/relation-name-relation-departed'
--- hooks/relation-name-relation-departed	2012-01-13 16:21:47 +0000
+++ hooks/relation-name-relation-departed	1970-01-01 00:00:00 +0000
@@ -1,5 +0,0 @@
-#!/bin/sh
-# This must be renamed to the name of the relation. The goal here is to
-# affect any change needed by the remote unit leaving the relationship.
-# This script should be idempotent.
-juju-log $JUJU_REMOTE_UNIT departed

=== removed file 'hooks/relation-name-relation-joined'
--- hooks/relation-name-relation-joined	2012-01-13 16:21:47 +0000
+++ hooks/relation-name-relation-joined	1970-01-01 00:00:00 +0000
@@ -1,5 +0,0 @@
-#!/bin/sh
-# This must be renamed to the name of the relation. The goal here is to
-# affect any change needed by relationships being formed
-# This script should be idempotent.
-juju-log $JUJU_REMOTE_UNIT joined

=== modified file 'hooks/start'
--- hooks/start	2012-02-03 14:21:51 +0000
+++ hooks/start	2012-02-06 23:20:22 +0000
@@ -1,19 +1,13 @@
 #!/usr/bin/python
 
-from helpers import (
-    command,
-    get_config,
-    log,
-    run,
-    )
-
-# Log the fact that we're about to begin the start step.
-log('--> start')
-
-config = get_config()
-buildbot_dir =  config.get('installdir')
-# Actually starting the buildbot happens in config-changed.
-run('open-port', '8010/TCP')
-
-# Log the fact that the start step is done.
-log('<-- start')
+from helpers import log, run
+
+
+def main():
+    # Actually starting the buildbot happens in config-changed.
+    run('open-port', '8010/TCP')
+
+
+if __name__ == '__main__':
+    log('START HOOK:')
+    main()

=== modified file 'hooks/stop'
--- hooks/stop	2012-01-30 16:43:59 +0000
+++ hooks/stop	2012-02-06 23:20:22 +0000
@@ -1,16 +1,16 @@
-#!/bin/bash
-# This will be run when the service is being torn down, allowing you to disable
-# it in various ways..
-# For example, if your web app uses a text file to signal to the load balancer
-# that it is live... you could remove it and sleep for a bit to allow the load
-# balancer to stop sending traffic.
-# rm /srv/webroot/server-live.txt && sleep 30
-
-BUILDBOT_DIR=`config-get installdir`
-
-juju-log "<-- stop"
-juju-log "Stopping buildbot in $BUILDBOT_DIR"
-close-port 8010/TCP
-buildbot stop $BUILDBOT_DIR
-juju-log "Finished stopping buildbot"
-juju-log "--> stop"
+#!/usr/bin/python
+
+from helpers import get_config, log, run
+
+
+def main():
+    config = get_config()
+    log('Stopping buildbot in {}'.format(config['installdir']))
+    run('close-port', '8010/TCP')
+
+    log('Finished stopping buildbot')
+
+
+if __name__ == '__main__':
+    log('STOP HOOK:')
+    main()


Follow ups