Merge lp:~benji/charms/oneiric/buildbot-master/buildbot-master-lpbuildbot into lp:~yellow/charms/oneiric/buildbot-master/trunk

Proposed by Benji York
Status: Merged
Approved by: Brad Crittenden
Approved revision: 16
Merged at revision: 9
Proposed branch: lp:~benji/charms/oneiric/buildbot-master/buildbot-master-lpbuildbot
Merge into: lp:~yellow/charms/oneiric/buildbot-master/trunk
Diff against target: 441 lines (+302/-73)
10 files modified
.bzrignore (+5/-0)
HACKING.txt (+23/-0)
hooks/config-changed (+59/-40)
hooks/helpers.py (+35/-0)
hooks/install (+37/-20)
hooks/start (+16/-12)
hooks/tests.py (+83/-0)
juju_wrapper (+19/-0)
revision (+0/-1)
tests/buildbot-master.test (+25/-0)
To merge this branch: bzr merge lp:~benji/charms/oneiric/buildbot-master/buildbot-master-lpbuildbot
Reviewer Review Type Date Requested Status
Brad Crittenden (community) Approve
Review via email: mp+91323@code.launchpad.net

Description of the change

This branch translates the hooks from bash into Python, adds some hook/test helpers (with tests) and adds the first charm test.

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Benji,

The Pythonization is great and overdue! And thanks for setting up a testing structure.

typo: s/about to being/about to begin/ (Several occurrences.)

line 119 is missing a comma, as I found out the hard way.

In my branch I've moved the definition of 'log' into the helpers.py. DRY.

The sleep at 431 seems awful aggressive given the long time it takes to deploy.

A lot of install and config-changed hooks I've already modified, so they were not reviewed too closely.

review: Approve
17. By Benji York

ignore revision

Revision history for this message
Benji York (benji) wrote :

> typo: s/about to being/about to begin/ (Several occurrences.)

Thanks. Fixed.

> line 119 is missing a comma, as I found out the hard way.

Ooh! So it is. Automatic string concatenation isn't one of my favorite
Python features. Fixed.

> In my branch I've moved the definition of 'log' into the helpers.py.
> DRY.

Sounds good. I'll leave that for your branch so as not to increase the
already great chance of merge conflicts.

> The sleep at 431 seems awful aggressive given the long time it takes
> to deploy.

My reasoning was more focused on getting out of the loop faster than
worrying too much about how many times we go through the loop. I
believe that doing 10 "juju status" commands a second is so cheap as to
be negligible.

Thanks for the good review.

18. By Benji York

s/being/begin/g

19. By Benji York

add missing comma

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file '.bzrignore'
--- .bzrignore 1970-01-01 00:00:00 +0000
+++ .bzrignore 2012-02-02 20:27:18 +0000
@@ -0,0 +1,5 @@
1.emacs*
2Session.vim
3tags
4TAGS
5revision
06
=== added file 'HACKING.txt'
--- HACKING.txt 1970-01-01 00:00:00 +0000
+++ HACKING.txt 2012-02-02 20:27:18 +0000
@@ -0,0 +1,23 @@
1Running the charm tests
2=======================
3
41) Establish a charm repository if you do not already have one. A charm
5 repository is a directory with subdirectories for each Ubuntu version being
6 used. Inside those per-Ubuntu-version directories are the charm
7 directories. For example, to make a charm repository for this charm under
8 Oneiric follow these steps:
9
10 a) mkdir -p ~/juju-charms/oneiric
11 b) ln -s `pwd` ~/juju-charms/oneiric/buildbot-master
12
132) Copy the juju_wrapper file into some place earlier in your PATH than the
14 real juju executable, naming it "juju". Edit the CHARM_TEST_REPO variable
15 therein to reflect the location of the charm repo from step 1.
16
173) Run the tests: RESOLVE_TEST_CHARMS=1 tests/buildbot-master.test
18
19
20Running the charm helper tests
21==============================
22
23Just run "python hooks/tests.py".
024
=== modified file 'hooks/config-changed' (properties changed: +x to -x)
--- hooks/config-changed 2012-01-30 14:43:09 +0000
+++ hooks/config-changed 2012-02-02 20:27:18 +0000
@@ -1,51 +1,70 @@
1#!/bin/bash1#!/usr/bin/python
2# Hook for handling config changes.2
3set -eux # -x for verbose logging to juju debug-log3from helpers import command, Config, run
4from subprocess import CalledProcessError
5import base64
6import os
7import os.path
8
49
5# config_file is changed via juju like:10# config_file is changed via juju like:
6# juju set buildbot-master config-file=`uuencode master.cfg`11# juju set buildbot-master config-file=`uuencode master.cfg`
712
8BUILDBOT_DIR=`config-get installdir`13log = command('juju-log')
9juju-log "--> config-changed"14bzr = command('bzr')
1015
11juju-log "Updating buildbot configuration."16# Log the fact that we're about to begin the install step.
12CONFIG_FILE=`config-get config-file`17log('--> config-changed')
13CONFIG_TRANSPORT=`config-get config-transport`18
14CONFIG_URL=`config-get config-url`19config = Config()
1520buildbot_dir = config['installdir']
16#21config_file = config['config-file']
17if [[ -n $CONFIG_FILE ]]; then22config_transport = config['config-transport']
18 echo "$CONFIG_FILE" | uudecode -o "$BUILDBOT_DIR"/master.cfg23config_url = config['config-url']
19 juju-log "Config decoded and written."24
20elif [ "$CONFIG_TRANSPORT" == "bzr" ] && [[ -n $CONFIG_URL ]]; then25log('Updating buildbot configuration.')
26# Write the buildbot config to disk (fetching it if necessary).
27if config_file:
28 with open(os.path.join(buildbot_dir, 'master.cfg', 'w')) as f:
29 f.write(base64.decode(config_file))
30 log('Config decoded and written.')
31elif config_transport == 'bzr' and config_url:
21 # If the branch is private then more setup needs to be done. The32 # If the branch is private then more setup needs to be done. The
22 # gpg-agent needs to send the key over and the bzr launchpad-login33 # gpg-agent needs to send the key over and the bzr launchpad-login
23 # needs to be set. 34 # needs to be set.
24 LP_ID=`config-get config-user`35 lp_id = config['config-user']
25 if [[ -n $LP_ID ]]; then36 if lp_id:
26 bzr launchpad-login $LP_ID37 bzr('launchpad-login', lp_id)
27 fi38
28 PKEY=`config-get config-private-key`39 private_key = config['config-private-key']
29 if [[ -n $PKEY ]]; then40 if private_key:
30 # Set up the .ssh directory.41 # Set up the .ssh directory.
31 mkdir ~/.ssh42 ssh_dir = os.expanduser('~/.ssh')
32 chmod 700 ~/.ssh43 os.mkdir(ssh_dir)
33 echo "$PKEY" | uudecode -o ~/.ssh/lp_key44 os.chmod(ssh_dir, 0700)
34 fi45 with open(os.path.join(ssh_dir, 'lp_key', w)) as f:
35 bzr branch --use-existing-dir $CONFIG_URL "$BUILDBOT_DIR"46 f.write(base64.decode(private_key))
36 chown -R ubuntu:ubuntu "$BUILDBOT_DIR"47
37fi48 bzr('branch', '--use-existing-dir', config_url, buildbot_dir)
49 run('chown', '-R', 'ubuntu:ubuntu', buildbot_dir)
50else:
51 # XXX Is it an error to get to this point?
52 pass
3853
39# Restart buildbot if it is running.54# Restart buildbot if it is running.
40PIDFILE="$BUILDBOT_DIR"/twistd.pid55pidfile = os.path.join(buildbot_dir, 'twistd.pid')
41if [ -f $PIDFILE ]; then56if os.path.exists(pidfile):
42 BUILDBOT_PID=`cat $PIDFILE`57 buildbot_pid = open(pidfile).read().strip()
43 if kill -0 $BUILDBOT_PID; then58 try:
44 # Buildbot is running, reconfigure it.59 # Is buildbot running?
45 juju-log "Reconfiguring buildbot"60 run('kill', '-0', buildbot_pid)
46 buildbot reconfig "$BUILDBOT_DIR"61 except CalledProcessError:
47 juju-log "Buildbot reconfigured"62 # Buildbot isn't running, so no need to reconfigure it.
48 fi63 pass
49fi64 else:
65 # Buildbot is running, reconfigure it.
66 log('Reconfiguring buildbot')
67 run('buildbot', 'reconfig', buildbot_dir)
68 log('Buildbot reconfigured')
5069
51juju-log "<-- config-changed"70log('<-- config-changed')
5271
=== added file 'hooks/helpers.py'
--- hooks/helpers.py 1970-01-01 00:00:00 +0000
+++ hooks/helpers.py 2012-02-02 20:27:18 +0000
@@ -0,0 +1,35 @@
1from collections import defaultdict
2import subprocess
3import yaml
4
5def command(*base_args):
6 def callable_command(*args):
7 return subprocess.check_output(base_args+args, shell=False)
8
9 return callable_command
10
11
12def run(*args):
13 return subprocess.check_output(args, shell=False)
14
15
16def unit_info(service_name, item_name, data=None):
17 if data is None:
18 data = yaml.safe_load(run('juju', 'status'))
19 services = data['services'][service_name]
20 units = services['units']
21 item = units.items()[0][1][item_name]
22 return item
23
24
25class Config(defaultdict):
26
27 def __init__(self):
28 super(defaultdict, self).__init__()
29 self.config_get = command('config-get')
30
31 def __missing__(self, key):
32 return self.config_get(key).strip()
33
34 def __setitem__(self, key, value):
35 raise RuntimeError('configuration is read-only')
036
=== modified file 'hooks/install' (properties changed: +x to -x)
--- hooks/install 2012-01-30 14:43:09 +0000
+++ hooks/install 2012-02-02 20:27:18 +0000
@@ -1,22 +1,39 @@
1#!/bin/bash1#!/usr/bin/python
2# Here do anything needed to install the service2
3# i.e. apt-get install -y foo or bzr branch http://myserver/mycode /srv/webroot3from helpers import command, Config, run
4set -eux # -x for verbose logging to juju debug-log4from subprocess import CalledProcessError
55import os
6juju-log "--> install"6import os.path
7BUILDBOT_DIR=`config-get installdir`7import shutil
8apt-get install -y buildbot sharutils8
9# Needed only for the demo site.9log = command('juju-log')
10apt-get install -y git10
1111# Log the fact that we're about to begin the install step.
12juju-log "Creating master in '$BUILDBOT_DIR'."12log('--> install')
13
14config = Config()
15
16buildbot_dir = config['installdir']
17run('apt-get', 'install', '-y', 'sharutils', 'bzr')
18
19# Get the lucid version of buildbot.
20run('apt-add-repository',
21 'deb http://us.archive.ubuntu.com/ubuntu/ lucid main universe')
22run('apt-get', 'update')
23run('apt-get', 'install', '-y', 'buildbot/lucid')
24
25log('Creating master in {}.'.format(buildbot_dir))
13# Since we may be installing into a pre-existing service, ensure the26# Since we may be installing into a pre-existing service, ensure the
14# buildbot directory is removed.27# buildbot directory is removed.
15if [ -e "$BUILDBOT_DIR" ]; then28if os.path.exists(buildbot_dir):
16 buildbot stop "$BUILDBOT_DIR"29 try:
17 rm -rf "$BUILDBOT_DIR"30 run('buildbot', 'stop', buildbot_dir)
18fi31 except CalledProcessError:
19mkdir -p "$BUILDBOT_DIR"32 # It probably wasn't running; just ignore the error.
20buildbot create-master "$BUILDBOT_DIR"33 pass
21mv "$BUILDBOT_DIR"/master.cfg.sample "$BUILDBOT_DIR"/master.cfg34 shutil.rmtree(buildbot_dir)
22juju-log "<-- install"35lpbuildbot_checkout = os.path.join(os.environ['CHARM_DIR'], 'lpbuildbot')
36shutil.copytree(lpbuildbot_checkout, buildbot_dir)
37
38# Log the fact that the install step is done.
39log('<-- install')
2340
=== modified file 'hooks/start' (properties changed: +x to -x)
--- hooks/start 2012-01-30 17:03:24 +0000
+++ hooks/start 2012-02-02 20:27:18 +0000
@@ -1,12 +1,16 @@
1#!/bin/bash1#!/usr/bin/python
2# Here put anything that is needed to start the service.2
3# Note that currently this is run directly after install3from helpers import command, Config, run
4# i.e. 'service apache2 start'4
5set -eux # -x for verbose logging to juju debug-log5log = command('juju-log')
66
7BUILDBOT_DIR=`config-get installdir`7# Log the fact that we're about to begin the start step.
88log('--> start')
9juju-log "--> start"9
10buildbot start "$BUILDBOT_DIR"10config = Config()
11open-port 8010/TCP11buildbot_dir = config['installdir']
12juju-log "<-- start"12run('buildbot', 'start', buildbot_dir)
13run('open-port', '8010/TCP')
14
15# Log the fact that the start step is done.
16log('<-- start')
1317
=== added file 'hooks/tests.py'
--- hooks/tests.py 1970-01-01 00:00:00 +0000
+++ hooks/tests.py 2012-02-02 20:27:18 +0000
@@ -0,0 +1,83 @@
1import unittest
2from subprocess import CalledProcessError
3from helpers import command, unit_info
4
5
6class testCommand(unittest.TestCase):
7
8 def testSimpleCommand(self):
9 # Creating a simple command (ls) works and running the command
10 # produces a string.
11 ls = command('/bin/ls')
12 self.assertIsInstance(ls(), str)
13
14 def testArguments(self):
15 # Arguments can be passed to commands.
16 ls = command('/bin/ls')
17 self.assertIn('Usage:', ls('--help'))
18
19 def testMissingExecutable(self):
20 # If the command does not exist, an OSError (No such file or
21 # directory) is raised.
22 bad = command('this command does not exist')
23 with self.assertRaises(OSError) as info:
24 bad()
25 self.assertEqual(2, info.exception.errno)
26
27 def testError(self):
28 # If the command returns a non-zero exit code, an exception is raised.
29 ls = command('/bin/ls')
30 with self.assertRaises(CalledProcessError):
31 ls('--not a valid switch')
32
33 def testBakedInArguments(self):
34 # Arguments can be passed when creating the command as well as when
35 # executing it.
36 ll = command('/bin/ls', '-l')
37 self.assertIn('rw', ll()) # Assumes a file is r/w in the pwd.
38 self.assertIn('Usage:', ll('--help'))
39
40 def testQuoting(self):
41 # There is no need to quote special shell characters in commands.
42 ls = command('/bin/ls')
43 ls('--help', '>')
44
45
46class testUnit_info(unittest.TestCase):
47
48 def make_data(self, state='started'):
49 return {
50 'machines': {0: {
51 'dns-name': 'localhost',
52 'instance-id': 'local',
53 'instance-state': 'running',
54 'state': 'running'}},
55 'services': {'foo-service': {
56 'charm': 'local:oneiric/foo-service-77',
57 'relations': {},
58 'units': {'foo-unit/29': {
59 'machine': 0,
60 'public-address': '192.168.122.160',
61 'relations': {},
62 'state': state}}}}}
63
64 def testDataParameter(self):
65 # The unit_info function can take a data parameter (primarily for
66 # testing) that provides the juju service information to be queried.
67 # If not provided the "juju status" command is run and its results
68 # parsed.
69 unit_info('foo-service', 'state', data=self.make_data())
70
71 def testStateFetching(self):
72 # The most common data to fetch about a unit is its state.
73 state = unit_info('foo-service', 'state', data=self.make_data())
74 self.assertEqual('started', state)
75
76 def testFailedState(self):
77 state = unit_info(
78 'foo-service', 'state', data=self.make_data(state='bad'))
79 self.assertNotEqual('started', state)
80
81
82if __name__ == '__main__':
83 unittest.main()
084
=== added file 'juju_wrapper'
--- juju_wrapper 1970-01-01 00:00:00 +0000
+++ juju_wrapper 2012-02-02 20:27:18 +0000
@@ -0,0 +1,19 @@
1#!/bin/sh
2
3[ -n "$RESOLVE_TEST_CHARMS" ] || exec /usr/bin/juju $@
4#set -x
5
6CHARM_TEST_REPO=~/juju-charms # <---- Change this.
7
8cmd=$1
9case $cmd in
10deploy)
11 shift
12 charm=$1
13 shift
14 exec /usr/bin/juju deploy --repository $CHARM_TEST_REPO local:$charm $@
15 ;;
16*)
17 exec /usr/bin/juju $@
18 ;;
19esac
020
=== removed file 'revision'
--- revision 2012-01-30 16:43:59 +0000
+++ revision 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
148
20
=== added directory 'tests'
=== added file 'tests/buildbot-master.test'
--- tests/buildbot-master.test 1970-01-01 00:00:00 +0000
+++ tests/buildbot-master.test 2012-02-02 20:27:18 +0000
@@ -0,0 +1,25 @@
1#!/usr/bin/python
2
3from helpers import command, run, unit_info
4import time
5import unittest
6
7juju = command('juju')
8
9class testCharm(unittest.TestCase):
10
11 def testDeploy(self):
12 try:
13 juju('deploy', 'buildbot-master')
14 while True:
15 status = unit_info('buildbot-master', 'state')
16 if 'error' in status or status == 'started':
17 break
18 time.sleep(0.1)
19 self.assertEqual(unit_info('buildbot-master', 'state'), 'started')
20 finally:
21 juju('destroy-service', 'buildbot-master')
22
23
24if __name__ == '__main__':
25 unittest.main()
026
=== added symlink 'tests/helpers.py'
=== target is u'../hooks/helpers.py'

Subscribers

People subscribed via source and target branches

to all changes: