Merge lp:~benji/charms/oneiric/buildbot-master/buildbot-master-lpbuildbot into lp:~yellow/charms/oneiric/buildbot-master/trunk
- Oneiric (11.10)
- buildbot-master-lpbuildbot
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Crittenden (community) | Approve | ||
Review via email: mp+91323@code.launchpad.net |
Commit message
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.
- 17. By Benji York
-
ignore revision
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
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' |
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.