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
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2012-02-02 20:27:18 +0000
4@@ -0,0 +1,5 @@
5+.emacs*
6+Session.vim
7+tags
8+TAGS
9+revision
10
11=== added file 'HACKING.txt'
12--- HACKING.txt 1970-01-01 00:00:00 +0000
13+++ HACKING.txt 2012-02-02 20:27:18 +0000
14@@ -0,0 +1,23 @@
15+Running the charm tests
16+=======================
17+
18+1) Establish a charm repository if you do not already have one. A charm
19+ repository is a directory with subdirectories for each Ubuntu version being
20+ used. Inside those per-Ubuntu-version directories are the charm
21+ directories. For example, to make a charm repository for this charm under
22+ Oneiric follow these steps:
23+
24+ a) mkdir -p ~/juju-charms/oneiric
25+ b) ln -s `pwd` ~/juju-charms/oneiric/buildbot-master
26+
27+2) Copy the juju_wrapper file into some place earlier in your PATH than the
28+ real juju executable, naming it "juju". Edit the CHARM_TEST_REPO variable
29+ therein to reflect the location of the charm repo from step 1.
30+
31+3) Run the tests: RESOLVE_TEST_CHARMS=1 tests/buildbot-master.test
32+
33+
34+Running the charm helper tests
35+==============================
36+
37+Just run "python hooks/tests.py".
38
39=== modified file 'hooks/config-changed' (properties changed: +x to -x)
40--- hooks/config-changed 2012-01-30 14:43:09 +0000
41+++ hooks/config-changed 2012-02-02 20:27:18 +0000
42@@ -1,51 +1,70 @@
43-#!/bin/bash
44-# Hook for handling config changes.
45-set -eux # -x for verbose logging to juju debug-log
46+#!/usr/bin/python
47+
48+from helpers import command, Config, run
49+from subprocess import CalledProcessError
50+import base64
51+import os
52+import os.path
53+
54
55 # config_file is changed via juju like:
56 # juju set buildbot-master config-file=`uuencode master.cfg`
57
58-BUILDBOT_DIR=`config-get installdir`
59-juju-log "--> config-changed"
60-
61-juju-log "Updating buildbot configuration."
62-CONFIG_FILE=`config-get config-file`
63-CONFIG_TRANSPORT=`config-get config-transport`
64-CONFIG_URL=`config-get config-url`
65-
66-#
67-if [[ -n $CONFIG_FILE ]]; then
68- echo "$CONFIG_FILE" | uudecode -o "$BUILDBOT_DIR"/master.cfg
69- juju-log "Config decoded and written."
70-elif [ "$CONFIG_TRANSPORT" == "bzr" ] && [[ -n $CONFIG_URL ]]; then
71+log = command('juju-log')
72+bzr = command('bzr')
73+
74+# Log the fact that we're about to begin the install step.
75+log('--> config-changed')
76+
77+config = Config()
78+buildbot_dir = config['installdir']
79+config_file = config['config-file']
80+config_transport = config['config-transport']
81+config_url = config['config-url']
82+
83+log('Updating buildbot configuration.')
84+# Write the buildbot config to disk (fetching it if necessary).
85+if config_file:
86+ with open(os.path.join(buildbot_dir, 'master.cfg', 'w')) as f:
87+ f.write(base64.decode(config_file))
88+ log('Config decoded and written.')
89+elif config_transport == 'bzr' and config_url:
90 # If the branch is private then more setup needs to be done. The
91 # gpg-agent needs to send the key over and the bzr launchpad-login
92 # needs to be set.
93- LP_ID=`config-get config-user`
94- if [[ -n $LP_ID ]]; then
95- bzr launchpad-login $LP_ID
96- fi
97- PKEY=`config-get config-private-key`
98- if [[ -n $PKEY ]]; then
99+ lp_id = config['config-user']
100+ if lp_id:
101+ bzr('launchpad-login', lp_id)
102+
103+ private_key = config['config-private-key']
104+ if private_key:
105 # Set up the .ssh directory.
106- mkdir ~/.ssh
107- chmod 700 ~/.ssh
108- echo "$PKEY" | uudecode -o ~/.ssh/lp_key
109- fi
110- bzr branch --use-existing-dir $CONFIG_URL "$BUILDBOT_DIR"
111- chown -R ubuntu:ubuntu "$BUILDBOT_DIR"
112-fi
113+ ssh_dir = os.expanduser('~/.ssh')
114+ os.mkdir(ssh_dir)
115+ os.chmod(ssh_dir, 0700)
116+ with open(os.path.join(ssh_dir, 'lp_key', w)) as f:
117+ f.write(base64.decode(private_key))
118+
119+ bzr('branch', '--use-existing-dir', config_url, buildbot_dir)
120+ run('chown', '-R', 'ubuntu:ubuntu', buildbot_dir)
121+else:
122+ # XXX Is it an error to get to this point?
123+ pass
124
125 # Restart buildbot if it is running.
126-PIDFILE="$BUILDBOT_DIR"/twistd.pid
127-if [ -f $PIDFILE ]; then
128- BUILDBOT_PID=`cat $PIDFILE`
129- if kill -0 $BUILDBOT_PID; then
130- # Buildbot is running, reconfigure it.
131- juju-log "Reconfiguring buildbot"
132- buildbot reconfig "$BUILDBOT_DIR"
133- juju-log "Buildbot reconfigured"
134- fi
135-fi
136+pidfile = os.path.join(buildbot_dir, 'twistd.pid')
137+if os.path.exists(pidfile):
138+ buildbot_pid = open(pidfile).read().strip()
139+ try:
140+ # Is buildbot running?
141+ run('kill', '-0', buildbot_pid)
142+ except CalledProcessError:
143+ # Buildbot isn't running, so no need to reconfigure it.
144+ pass
145+ else:
146+ # Buildbot is running, reconfigure it.
147+ log('Reconfiguring buildbot')
148+ run('buildbot', 'reconfig', buildbot_dir)
149+ log('Buildbot reconfigured')
150
151-juju-log "<-- config-changed"
152+log('<-- config-changed')
153
154=== added file 'hooks/helpers.py'
155--- hooks/helpers.py 1970-01-01 00:00:00 +0000
156+++ hooks/helpers.py 2012-02-02 20:27:18 +0000
157@@ -0,0 +1,35 @@
158+from collections import defaultdict
159+import subprocess
160+import yaml
161+
162+def command(*base_args):
163+ def callable_command(*args):
164+ return subprocess.check_output(base_args+args, shell=False)
165+
166+ return callable_command
167+
168+
169+def run(*args):
170+ return subprocess.check_output(args, shell=False)
171+
172+
173+def unit_info(service_name, item_name, data=None):
174+ if data is None:
175+ data = yaml.safe_load(run('juju', 'status'))
176+ services = data['services'][service_name]
177+ units = services['units']
178+ item = units.items()[0][1][item_name]
179+ return item
180+
181+
182+class Config(defaultdict):
183+
184+ def __init__(self):
185+ super(defaultdict, self).__init__()
186+ self.config_get = command('config-get')
187+
188+ def __missing__(self, key):
189+ return self.config_get(key).strip()
190+
191+ def __setitem__(self, key, value):
192+ raise RuntimeError('configuration is read-only')
193
194=== modified file 'hooks/install' (properties changed: +x to -x)
195--- hooks/install 2012-01-30 14:43:09 +0000
196+++ hooks/install 2012-02-02 20:27:18 +0000
197@@ -1,22 +1,39 @@
198-#!/bin/bash
199-# Here do anything needed to install the service
200-# i.e. apt-get install -y foo or bzr branch http://myserver/mycode /srv/webroot
201-set -eux # -x for verbose logging to juju debug-log
202-
203-juju-log "--> install"
204-BUILDBOT_DIR=`config-get installdir`
205-apt-get install -y buildbot sharutils
206-# Needed only for the demo site.
207-apt-get install -y git
208-
209-juju-log "Creating master in '$BUILDBOT_DIR'."
210+#!/usr/bin/python
211+
212+from helpers import command, Config, run
213+from subprocess import CalledProcessError
214+import os
215+import os.path
216+import shutil
217+
218+log = command('juju-log')
219+
220+# Log the fact that we're about to begin the install step.
221+log('--> install')
222+
223+config = Config()
224+
225+buildbot_dir = config['installdir']
226+run('apt-get', 'install', '-y', 'sharutils', 'bzr')
227+
228+# Get the lucid version of buildbot.
229+run('apt-add-repository',
230+ 'deb http://us.archive.ubuntu.com/ubuntu/ lucid main universe')
231+run('apt-get', 'update')
232+run('apt-get', 'install', '-y', 'buildbot/lucid')
233+
234+log('Creating master in {}.'.format(buildbot_dir))
235 # Since we may be installing into a pre-existing service, ensure the
236 # buildbot directory is removed.
237-if [ -e "$BUILDBOT_DIR" ]; then
238- buildbot stop "$BUILDBOT_DIR"
239- rm -rf "$BUILDBOT_DIR"
240-fi
241-mkdir -p "$BUILDBOT_DIR"
242-buildbot create-master "$BUILDBOT_DIR"
243-mv "$BUILDBOT_DIR"/master.cfg.sample "$BUILDBOT_DIR"/master.cfg
244-juju-log "<-- install"
245+if os.path.exists(buildbot_dir):
246+ try:
247+ run('buildbot', 'stop', buildbot_dir)
248+ except CalledProcessError:
249+ # It probably wasn't running; just ignore the error.
250+ pass
251+ shutil.rmtree(buildbot_dir)
252+lpbuildbot_checkout = os.path.join(os.environ['CHARM_DIR'], 'lpbuildbot')
253+shutil.copytree(lpbuildbot_checkout, buildbot_dir)
254+
255+# Log the fact that the install step is done.
256+log('<-- install')
257
258=== modified file 'hooks/start' (properties changed: +x to -x)
259--- hooks/start 2012-01-30 17:03:24 +0000
260+++ hooks/start 2012-02-02 20:27:18 +0000
261@@ -1,12 +1,16 @@
262-#!/bin/bash
263-# Here put anything that is needed to start the service.
264-# Note that currently this is run directly after install
265-# i.e. 'service apache2 start'
266-set -eux # -x for verbose logging to juju debug-log
267-
268-BUILDBOT_DIR=`config-get installdir`
269-
270-juju-log "--> start"
271-buildbot start "$BUILDBOT_DIR"
272-open-port 8010/TCP
273-juju-log "<-- start"
274+#!/usr/bin/python
275+
276+from helpers import command, Config, run
277+
278+log = command('juju-log')
279+
280+# Log the fact that we're about to begin the start step.
281+log('--> start')
282+
283+config = Config()
284+buildbot_dir = config['installdir']
285+run('buildbot', 'start', buildbot_dir)
286+run('open-port', '8010/TCP')
287+
288+# Log the fact that the start step is done.
289+log('<-- start')
290
291=== added file 'hooks/tests.py'
292--- hooks/tests.py 1970-01-01 00:00:00 +0000
293+++ hooks/tests.py 2012-02-02 20:27:18 +0000
294@@ -0,0 +1,83 @@
295+import unittest
296+from subprocess import CalledProcessError
297+from helpers import command, unit_info
298+
299+
300+class testCommand(unittest.TestCase):
301+
302+ def testSimpleCommand(self):
303+ # Creating a simple command (ls) works and running the command
304+ # produces a string.
305+ ls = command('/bin/ls')
306+ self.assertIsInstance(ls(), str)
307+
308+ def testArguments(self):
309+ # Arguments can be passed to commands.
310+ ls = command('/bin/ls')
311+ self.assertIn('Usage:', ls('--help'))
312+
313+ def testMissingExecutable(self):
314+ # If the command does not exist, an OSError (No such file or
315+ # directory) is raised.
316+ bad = command('this command does not exist')
317+ with self.assertRaises(OSError) as info:
318+ bad()
319+ self.assertEqual(2, info.exception.errno)
320+
321+ def testError(self):
322+ # If the command returns a non-zero exit code, an exception is raised.
323+ ls = command('/bin/ls')
324+ with self.assertRaises(CalledProcessError):
325+ ls('--not a valid switch')
326+
327+ def testBakedInArguments(self):
328+ # Arguments can be passed when creating the command as well as when
329+ # executing it.
330+ ll = command('/bin/ls', '-l')
331+ self.assertIn('rw', ll()) # Assumes a file is r/w in the pwd.
332+ self.assertIn('Usage:', ll('--help'))
333+
334+ def testQuoting(self):
335+ # There is no need to quote special shell characters in commands.
336+ ls = command('/bin/ls')
337+ ls('--help', '>')
338+
339+
340+class testUnit_info(unittest.TestCase):
341+
342+ def make_data(self, state='started'):
343+ return {
344+ 'machines': {0: {
345+ 'dns-name': 'localhost',
346+ 'instance-id': 'local',
347+ 'instance-state': 'running',
348+ 'state': 'running'}},
349+ 'services': {'foo-service': {
350+ 'charm': 'local:oneiric/foo-service-77',
351+ 'relations': {},
352+ 'units': {'foo-unit/29': {
353+ 'machine': 0,
354+ 'public-address': '192.168.122.160',
355+ 'relations': {},
356+ 'state': state}}}}}
357+
358+ def testDataParameter(self):
359+ # The unit_info function can take a data parameter (primarily for
360+ # testing) that provides the juju service information to be queried.
361+ # If not provided the "juju status" command is run and its results
362+ # parsed.
363+ unit_info('foo-service', 'state', data=self.make_data())
364+
365+ def testStateFetching(self):
366+ # The most common data to fetch about a unit is its state.
367+ state = unit_info('foo-service', 'state', data=self.make_data())
368+ self.assertEqual('started', state)
369+
370+ def testFailedState(self):
371+ state = unit_info(
372+ 'foo-service', 'state', data=self.make_data(state='bad'))
373+ self.assertNotEqual('started', state)
374+
375+
376+if __name__ == '__main__':
377+ unittest.main()
378
379=== added file 'juju_wrapper'
380--- juju_wrapper 1970-01-01 00:00:00 +0000
381+++ juju_wrapper 2012-02-02 20:27:18 +0000
382@@ -0,0 +1,19 @@
383+#!/bin/sh
384+
385+[ -n "$RESOLVE_TEST_CHARMS" ] || exec /usr/bin/juju $@
386+#set -x
387+
388+CHARM_TEST_REPO=~/juju-charms # <---- Change this.
389+
390+cmd=$1
391+case $cmd in
392+deploy)
393+ shift
394+ charm=$1
395+ shift
396+ exec /usr/bin/juju deploy --repository $CHARM_TEST_REPO local:$charm $@
397+ ;;
398+*)
399+ exec /usr/bin/juju $@
400+ ;;
401+esac
402
403=== removed file 'revision'
404--- revision 2012-01-30 16:43:59 +0000
405+++ revision 1970-01-01 00:00:00 +0000
406@@ -1,1 +0,0 @@
407-48
408
409=== added directory 'tests'
410=== added file 'tests/buildbot-master.test'
411--- tests/buildbot-master.test 1970-01-01 00:00:00 +0000
412+++ tests/buildbot-master.test 2012-02-02 20:27:18 +0000
413@@ -0,0 +1,25 @@
414+#!/usr/bin/python
415+
416+from helpers import command, run, unit_info
417+import time
418+import unittest
419+
420+juju = command('juju')
421+
422+class testCharm(unittest.TestCase):
423+
424+ def testDeploy(self):
425+ try:
426+ juju('deploy', 'buildbot-master')
427+ while True:
428+ status = unit_info('buildbot-master', 'state')
429+ if 'error' in status or status == 'started':
430+ break
431+ time.sleep(0.1)
432+ self.assertEqual(unit_info('buildbot-master', 'state'), 'started')
433+ finally:
434+ juju('destroy-service', 'buildbot-master')
435+
436+
437+if __name__ == '__main__':
438+ unittest.main()
439
440=== added symlink 'tests/helpers.py'
441=== target is u'../hooks/helpers.py'

Subscribers

People subscribed via source and target branches

to all changes: