Merge lp:~bac/charms/oneiric/buildbot-master/history-s3 into lp:~yellow/charms/oneiric/buildbot-master/trunk
- Oneiric (11.10)
- history-s3
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Gary Poster |
Approved revision: | 34 |
Merged at revision: | 33 |
Proposed branch: | lp:~bac/charms/oneiric/buildbot-master/history-s3 |
Merge into: | lp:~yellow/charms/oneiric/buildbot-master/trunk |
Diff against target: |
590 lines (+372/-15) 9 files modified
HACKING.txt (+3/-0) config.yaml (+22/-0) examples/pyflakes.yaml (+1/-1) hooks/config-changed (+22/-7) hooks/install (+1/-1) hooks/local.py (+121/-0) hooks/start (+0/-1) hooks/stop (+5/-2) hooks/tests.py (+197/-3) |
To merge this branch: | bzr merge lp:~bac/charms/oneiric/buildbot-master/history-s3 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gary Poster (community) | Approve | ||
Yellow Squad | code | Pending | |
Review via email: mp+94262@code.launchpad.net |
Commit message
Description of the change
Add ability to store buildbot data files to S3 and retrieve them the next time the master is started.
The configuration settings for access-key and secret-key need to be passed, like so:
juju set buildbot-master access-key='<your AWS key>' secret-key='<your AWS secret key>'
Unfortunately the 'stop' hook is not being called (see bug 872264) so automatically saving the history when bringing down the juju service does not work. As a work-around a new config value is provided which causes the files to get pushed. Setting that config variable to a different value will cause it to save again, e.g.
juju set buildbot-master save-history-
- 34. By Brad Crittenden
-
Added unit tests of history management functions
Gary Poster (gary) wrote : | # |
Hi Brad. That looks good.
You asked about the amount of code needed to stub boto. I am very happy we have it, and I wonder if it ought to be put somewhere else to make it reusable--later, perhaps.
For now, though, the only idea I have is that it might be nice to put the boto setup/teardown stuff in a separate file, just to make the test file easier to read. Just an idea.
Meanwhile though, I approve it again!
Preview Diff
1 | === modified file 'HACKING.txt' | |||
2 | --- HACKING.txt 2012-02-07 17:20:47 +0000 | |||
3 | +++ HACKING.txt 2012-02-23 16:49:43 +0000 | |||
4 | @@ -21,6 +21,9 @@ | |||
5 | 21 | 4) Run the tests: RESOLVE_TEST_CHARMS=1 tests/buildbot-master.test | 21 | 4) Run the tests: RESOLVE_TEST_CHARMS=1 tests/buildbot-master.test |
6 | 22 | 22 | ||
7 | 23 | 23 | ||
8 | 24 | XXX bac 2012-02-23: The tests do not run locally using precise. Set | ||
9 | 25 | default-series: oneiric to get them to pass. | ||
10 | 26 | |||
11 | 24 | Running the charm helper tests | 27 | Running the charm helper tests |
12 | 25 | ============================== | 28 | ============================== |
13 | 26 | 29 | ||
14 | 27 | 30 | ||
15 | === modified file 'config.yaml' | |||
16 | --- config.yaml 2012-02-10 21:19:58 +0000 | |||
17 | +++ config.yaml 2012-02-23 16:49:43 +0000 | |||
18 | @@ -52,3 +52,25 @@ | |||
19 | 52 | install the newly specified packages while leaving the previous | 52 | install the newly specified packages while leaving the previous |
20 | 53 | ones installed. | 53 | ones installed. |
21 | 54 | type: string | 54 | type: string |
22 | 55 | access-key: | ||
23 | 56 | description: | | ||
24 | 57 | Access key for EC2. | ||
25 | 58 | type: string | ||
26 | 59 | secret-key: | ||
27 | 60 | description: | | ||
28 | 61 | Secret key for EC2. | ||
29 | 62 | type: string | ||
30 | 63 | bucket-name: | ||
31 | 64 | description: | | ||
32 | 65 | The bucket used to store buildbot history. If not provided a | ||
33 | 66 | default based on the access-key will be used. | ||
34 | 67 | type: string | ||
35 | 68 | save-history-now: | ||
36 | 69 | description: | | ||
37 | 70 | Configuration hack to fire off the saving of the buildbot master | ||
38 | 71 | history. Normally this would be done in the stop hook but due to | ||
39 | 72 | Bug 872264 that hook is not firing properly. The value of the | ||
40 | 73 | setting is not important but it must change between invocations | ||
41 | 74 | or the event will not be recogized. Monotonically increasing | ||
42 | 75 | integer values would be a good choice. Or a time string. | ||
43 | 76 | type: string | ||
44 | 55 | 77 | ||
45 | === modified file 'examples/pyflakes.yaml' | |||
46 | --- examples/pyflakes.yaml 2012-02-10 21:19:58 +0000 | |||
47 | +++ examples/pyflakes.yaml 2012-02-23 16:49:43 +0000 | |||
48 | @@ -1,5 +1,5 @@ | |||
49 | 1 | buildbot-master: | 1 | buildbot-master: |
51 | 2 | extra-packages: git | 2 | extra-packages: git python-sqlalchemy python-migrate |
52 | 3 | installdir: /tmp/buildbot | 3 | installdir: /tmp/buildbot |
53 | 4 | config-file: | | 4 | config-file: | |
54 | 5 | # -*- python -*- | 5 | # -*- python -*- |
55 | 6 | 6 | ||
56 | === modified file 'hooks/config-changed' | |||
57 | --- hooks/config-changed 2012-02-14 15:51:49 +0000 | |||
58 | +++ hooks/config-changed 2012-02-23 16:49:43 +0000 | |||
59 | @@ -3,14 +3,17 @@ | |||
60 | 3 | # Copyright 2012 Canonical Ltd. This software is licensed under the | 3 | # Copyright 2012 Canonical Ltd. This software is licensed under the |
61 | 4 | # GNU Affero General Public License version 3 (see the file LICENSE). | 4 | # GNU Affero General Public License version 3 (see the file LICENSE). |
62 | 5 | 5 | ||
63 | 6 | import base64 | ||
64 | 6 | import json | 7 | import json |
65 | 7 | import os | 8 | import os |
66 | 8 | import os.path | 9 | import os.path |
67 | 9 | import shutil | 10 | import shutil |
68 | 11 | import subprocess | ||
69 | 10 | import sys | 12 | import sys |
70 | 11 | 13 | ||
71 | 12 | from helpers import ( | 14 | from helpers import ( |
72 | 13 | apt_get_install, | 15 | apt_get_install, |
73 | 16 | cd, | ||
74 | 14 | command, | 17 | command, |
75 | 15 | DictDiffer, | 18 | DictDiffer, |
76 | 16 | get_config, | 19 | get_config, |
77 | @@ -26,7 +29,11 @@ | |||
78 | 26 | buildbot_create, | 29 | buildbot_create, |
79 | 27 | buildbot_reconfig, | 30 | buildbot_reconfig, |
80 | 28 | config_json, | 31 | config_json, |
81 | 32 | fetch_history, | ||
82 | 29 | generate_string, | 33 | generate_string, |
83 | 34 | get_bucket, | ||
84 | 35 | get_key, | ||
85 | 36 | put_history, | ||
86 | 30 | slave_json, | 37 | slave_json, |
87 | 31 | ) | 38 | ) |
88 | 32 | 39 | ||
89 | @@ -36,7 +43,6 @@ | |||
90 | 36 | ] | 43 | ] |
91 | 37 | SUPPORTED_TRANSPORTS = ['bzr'] | 44 | SUPPORTED_TRANSPORTS = ['bzr'] |
92 | 38 | 45 | ||
93 | 39 | |||
94 | 40 | bzr = command('bzr') | 46 | bzr = command('bzr') |
95 | 41 | 47 | ||
96 | 42 | 48 | ||
97 | @@ -57,8 +63,6 @@ | |||
98 | 57 | 63 | ||
99 | 58 | def handle_config_changes(config, diff): | 64 | def handle_config_changes(config, diff): |
100 | 59 | log('Updating buildbot configuration.') | 65 | log('Updating buildbot configuration.') |
101 | 60 | log('Configuration changes seen:') | ||
102 | 61 | log(str(diff)) | ||
103 | 62 | 66 | ||
104 | 63 | buildbot_pkg = config.get('buildbot-pkg') | 67 | buildbot_pkg = config.get('buildbot-pkg') |
105 | 64 | extra_repo = config.get('extra-repository') | 68 | extra_repo = config.get('extra-repository') |
106 | @@ -91,7 +95,6 @@ | |||
107 | 91 | 95 | ||
108 | 92 | # Write the buildbot config to disk (fetching it if necessary). | 96 | # Write the buildbot config to disk (fetching it if necessary). |
109 | 93 | added_or_changed = diff.added_or_changed | 97 | added_or_changed = diff.added_or_changed |
110 | 94 | log("CONFIG FILE: {}".format(config_file)) | ||
111 | 95 | log("ADDED OR CHANGED: {}".format(added_or_changed)) | 98 | log("ADDED OR CHANGED: {}".format(added_or_changed)) |
112 | 96 | if config_file and 'config-file' in added_or_changed: | 99 | if config_file and 'config-file' in added_or_changed: |
113 | 97 | buildbot_create(buildbot_dir) | 100 | buildbot_create(buildbot_dir) |
114 | @@ -108,7 +111,7 @@ | |||
115 | 108 | # gpg-agent needs to send the key over and the bzr launchpad-login | 111 | # gpg-agent needs to send the key over and the bzr launchpad-login |
116 | 109 | # needs to be set. | 112 | # needs to be set. |
117 | 110 | with su('buildbot'): | 113 | with su('buildbot'): |
119 | 111 | lp_id = config.get('config-usr') | 114 | lp_id = config.get('config-user') |
120 | 112 | if lp_id: | 115 | if lp_id: |
121 | 113 | bzr('launchpad-login', lp_id) | 116 | bzr('launchpad-login', lp_id) |
122 | 114 | private_key = config.get('config-private-key') | 117 | private_key = config.get('config-private-key') |
123 | @@ -153,12 +156,21 @@ | |||
124 | 153 | if not os.path.exists(placeholder_path): | 156 | if not os.path.exists(placeholder_path): |
125 | 154 | with open(placeholder_path, 'w') as f: | 157 | with open(placeholder_path, 'w') as f: |
126 | 155 | json.dump(generate_string("temporary-placeholder-"), f) | 158 | json.dump(generate_string("temporary-placeholder-"), f) |
128 | 156 | buildbot_reconfig() | 159 | try: |
129 | 160 | buildbot_reconfig() | ||
130 | 161 | except subprocess.CalledProcessError as e: | ||
131 | 162 | print e | ||
132 | 163 | print e.output | ||
133 | 164 | raise | ||
134 | 165 | |||
135 | 166 | |||
136 | 167 | def conditionally_save_history(config, diff): | ||
137 | 168 | if 'save-history-now' in diff.added_or_changed: | ||
138 | 169 | put_history(config) | ||
139 | 157 | 170 | ||
140 | 158 | 171 | ||
141 | 159 | def main(): | 172 | def main(): |
142 | 160 | config = get_config() | 173 | config = get_config() |
143 | 161 | log(str(config)) | ||
144 | 162 | if not check_config(config): | 174 | if not check_config(config): |
145 | 163 | log("Configuration not valid.") | 175 | log("Configuration not valid.") |
146 | 164 | sys.exit(1) | 176 | sys.exit(1) |
147 | @@ -178,6 +190,7 @@ | |||
148 | 178 | os.makedirs(buildbot_dir) | 190 | os.makedirs(buildbot_dir) |
149 | 179 | 191 | ||
150 | 180 | restart_required |= configure_buildbot(config, diff) | 192 | restart_required |= configure_buildbot(config, diff) |
151 | 193 | restart_required |= fetch_history(config, diff) | ||
152 | 181 | 194 | ||
153 | 182 | master_cfg_path = os.path.join(buildbot_dir, 'master.cfg') | 195 | master_cfg_path = os.path.join(buildbot_dir, 'master.cfg') |
154 | 183 | if restart_required and os.path.exists(master_cfg_path): | 196 | if restart_required and os.path.exists(master_cfg_path): |
155 | @@ -185,6 +198,8 @@ | |||
156 | 185 | else: | 198 | else: |
157 | 186 | log("Configuration changed but didn't require restarting.") | 199 | log("Configuration changed but didn't require restarting.") |
158 | 187 | 200 | ||
159 | 201 | conditionally_save_history(config, diff) | ||
160 | 202 | |||
161 | 188 | config_json.set(config) | 203 | config_json.set(config) |
162 | 189 | 204 | ||
163 | 190 | 205 | ||
164 | 191 | 206 | ||
165 | === modified file 'hooks/install' | |||
166 | --- hooks/install 2012-02-14 16:54:21 +0000 | |||
167 | +++ hooks/install 2012-02-23 16:49:43 +0000 | |||
168 | @@ -22,7 +22,7 @@ | |||
169 | 22 | 22 | ||
170 | 23 | 23 | ||
171 | 24 | def cleanup(buildbot_dir): | 24 | def cleanup(buildbot_dir): |
173 | 25 | apt_get_install('bzr') | 25 | apt_get_install('bzr', 'python-boto') |
174 | 26 | # Since we may be installing into a pre-existing service, ensure the | 26 | # Since we may be installing into a pre-existing service, ensure the |
175 | 27 | # buildbot directory is removed. | 27 | # buildbot directory is removed. |
176 | 28 | if os.path.exists(buildbot_dir): | 28 | if os.path.exists(buildbot_dir): |
177 | 29 | 29 | ||
178 | === modified file 'hooks/local.py' | |||
179 | --- hooks/local.py 2012-02-10 01:05:09 +0000 | |||
180 | +++ hooks/local.py 2012-02-23 16:49:43 +0000 | |||
181 | @@ -10,16 +10,22 @@ | |||
182 | 10 | 'buildslave_stop', | 10 | 'buildslave_stop', |
183 | 11 | 'config_json', | 11 | 'config_json', |
184 | 12 | 'create_slave', | 12 | 'create_slave', |
185 | 13 | 'fetch_history', | ||
186 | 13 | 'generate_string', | 14 | 'generate_string', |
187 | 15 | 'get_bucket', | ||
188 | 16 | 'get_key', | ||
189 | 14 | 'HTTP_PORT_PROTOCOL', | 17 | 'HTTP_PORT_PROTOCOL', |
190 | 18 | 'put_history', | ||
191 | 15 | 'slave_json', | 19 | 'slave_json', |
192 | 16 | ] | 20 | ] |
193 | 17 | 21 | ||
194 | 18 | import os | 22 | import os |
195 | 23 | import re | ||
196 | 19 | import subprocess | 24 | import subprocess |
197 | 20 | import uuid | 25 | import uuid |
198 | 21 | 26 | ||
199 | 22 | from helpers import ( | 27 | from helpers import ( |
200 | 28 | cd, | ||
201 | 23 | get_config, | 29 | get_config, |
202 | 24 | log, | 30 | log, |
203 | 25 | run, | 31 | run, |
204 | @@ -157,3 +163,118 @@ | |||
205 | 157 | 163 | ||
206 | 158 | slave_json = Serializer('/tmp/slave_info.json') | 164 | slave_json = Serializer('/tmp/slave_info.json') |
207 | 159 | config_json = Serializer('/tmp/config.json') | 165 | config_json = Serializer('/tmp/config.json') |
208 | 166 | |||
209 | 167 | |||
210 | 168 | def get_bucket(config, bucket_name=None): | ||
211 | 169 | """Return an S3 bucket or None.""" | ||
212 | 170 | # Late import to ensure python-boto package has been installed. | ||
213 | 171 | import boto | ||
214 | 172 | access_key = config.get('access-key') | ||
215 | 173 | secret_key = config.get('secret-key') | ||
216 | 174 | bucket = None | ||
217 | 175 | if access_key and secret_key: | ||
218 | 176 | if bucket_name is None: | ||
219 | 177 | bucket_name = str(access_key + '-buildbot-history').lower() | ||
220 | 178 | conn = boto.connect_s3(access_key, secret_key) | ||
221 | 179 | bucket = conn.create_bucket(bucket_name) | ||
222 | 180 | log("Using bucket: " + bucket.name) | ||
223 | 181 | return bucket | ||
224 | 182 | |||
225 | 183 | |||
226 | 184 | def get_key(bucket): | ||
227 | 185 | """Return an S3 key for the bucket.""" | ||
228 | 186 | # Late import to ensure python-boto package has been installed. | ||
229 | 187 | from boto.s3.key import Key | ||
230 | 188 | key = Key(bucket) | ||
231 | 189 | value = os.environ['JUJU_UNIT_NAME'] | ||
232 | 190 | key.key = value | ||
233 | 191 | log("Using key: " + key.key) | ||
234 | 192 | return key | ||
235 | 193 | |||
236 | 194 | |||
237 | 195 | VERSION_TO_STORE = { | ||
238 | 196 | '0.7': "*/builder", | ||
239 | 197 | '0.8': "state.sqlite", | ||
240 | 198 | } | ||
241 | 199 | |||
242 | 200 | |||
243 | 201 | def get_buildbot_version(): | ||
244 | 202 | """Get the major version (x.y) of buildbot and return as a string. | ||
245 | 203 | |||
246 | 204 | Return None if the output from buildbot cannot be parsed. | ||
247 | 205 | """ | ||
248 | 206 | version = None | ||
249 | 207 | output = run('buildbot', '--version') | ||
250 | 208 | match = re.search('Buildbot version: (\d+\.\d+)(\.\d+)*\n', output) | ||
251 | 209 | if match and len(match.groups()) > 0: | ||
252 | 210 | version = match.group(1) | ||
253 | 211 | return version | ||
254 | 212 | |||
255 | 213 | |||
256 | 214 | def put_history(config): | ||
257 | 215 | """Put the buildbot history to an external store, if set up.""" | ||
258 | 216 | log("put_history called") | ||
259 | 217 | bucket_name = config.get('bucket-name') | ||
260 | 218 | bucket = get_bucket(config, bucket_name) | ||
261 | 219 | success = False | ||
262 | 220 | if bucket: | ||
263 | 221 | key = get_key(bucket) | ||
264 | 222 | target = '/tmp/history-put.tgz' | ||
265 | 223 | version = get_buildbot_version() | ||
266 | 224 | store_pattern = VERSION_TO_STORE.get(version) | ||
267 | 225 | assert store_pattern is not None, ( | ||
268 | 226 | "Buildbot version not supported: {}".format(version)) | ||
269 | 227 | with cd(config['installdir']): | ||
270 | 228 | with su('buildbot'): | ||
271 | 229 | try: | ||
272 | 230 | run('tar', 'czf', target, store_pattern) | ||
273 | 231 | key.set_contents_from_filename(target) | ||
274 | 232 | # If would be natural to just log the success here, but we | ||
275 | 233 | # are su-ed to the buildbot user and that causes permission | ||
276 | 234 | # problems, so instead set a flag. | ||
277 | 235 | success = True | ||
278 | 236 | except subprocess.CalledProcessError as e: | ||
279 | 237 | print e | ||
280 | 238 | print e.output | ||
281 | 239 | raise | ||
282 | 240 | os.unlink(target) | ||
283 | 241 | else: | ||
284 | 242 | log("Bucket not found: " + bucket_name) | ||
285 | 243 | if success: | ||
286 | 244 | log("History stored to S3.") | ||
287 | 245 | |||
288 | 246 | |||
289 | 247 | def fetch_history(config, diff): | ||
290 | 248 | """Fetch the buildbot history from an external store.""" | ||
291 | 249 | # Currently only S3 is supported. | ||
292 | 250 | restart_required = False | ||
293 | 251 | |||
294 | 252 | log("fetching history") | ||
295 | 253 | if 'secret-key' not in diff.added_or_changed: | ||
296 | 254 | log("skipping fetch of history") | ||
297 | 255 | return restart_required | ||
298 | 256 | |||
299 | 257 | bucket_name = config.get('bucket-name') | ||
300 | 258 | bucket = get_bucket(config, bucket_name) | ||
301 | 259 | |||
302 | 260 | if bucket: | ||
303 | 261 | key = get_key(bucket) | ||
304 | 262 | if key.exists(): | ||
305 | 263 | target = '/tmp/history-fetched.tgz' | ||
306 | 264 | key.get_contents_to_filename(target) | ||
307 | 265 | with cd(config['installdir']): | ||
308 | 266 | with su('buildbot'): | ||
309 | 267 | try: | ||
310 | 268 | run('tar', 'xzf', target) | ||
311 | 269 | except subprocess.CalledProcessError as e: | ||
312 | 270 | print e | ||
313 | 271 | print e.output | ||
314 | 272 | raise | ||
315 | 273 | os.unlink(target) | ||
316 | 274 | log("History fetched from S3.") | ||
317 | 275 | restart_required = True | ||
318 | 276 | else: | ||
319 | 277 | log("Key does not exist: " + key.key) | ||
320 | 278 | else: | ||
321 | 279 | log("Bucket not found: " + bucket_name) | ||
322 | 280 | return restart_required | ||
323 | 160 | 281 | ||
324 | === modified file 'hooks/start' | |||
325 | --- hooks/start 2012-02-08 14:26:58 +0000 | |||
326 | +++ hooks/start 2012-02-23 16:49:43 +0000 | |||
327 | @@ -4,7 +4,6 @@ | |||
328 | 4 | # GNU Affero General Public License version 3 (see the file LICENSE). | 4 | # GNU Affero General Public License version 3 (see the file LICENSE). |
329 | 5 | 5 | ||
330 | 6 | from helpers import ( | 6 | from helpers import ( |
331 | 7 | log, | ||
332 | 8 | log_entry, | 7 | log_entry, |
333 | 9 | log_exit, | 8 | log_exit, |
334 | 10 | run, | 9 | run, |
335 | 11 | 10 | ||
336 | === modified file 'hooks/stop' | |||
337 | --- hooks/stop 2012-02-08 14:26:58 +0000 | |||
338 | +++ hooks/stop 2012-02-23 16:49:43 +0000 | |||
339 | @@ -10,14 +10,17 @@ | |||
340 | 10 | log_exit, | 10 | log_exit, |
341 | 11 | run, | 11 | run, |
342 | 12 | ) | 12 | ) |
344 | 13 | from local import HTTP_PORT_PROTOCOL | 13 | from local import ( |
345 | 14 | HTTP_PORT_PROTOCOL, | ||
346 | 15 | put_history, | ||
347 | 16 | ) | ||
348 | 14 | 17 | ||
349 | 15 | 18 | ||
350 | 16 | def main(): | 19 | def main(): |
351 | 17 | config = get_config() | 20 | config = get_config() |
352 | 18 | log('Stopping buildbot in {}'.format(config['installdir'])) | 21 | log('Stopping buildbot in {}'.format(config['installdir'])) |
353 | 22 | put_history(config) | ||
354 | 19 | run('close-port', HTTP_PORT_PROTOCOL) | 23 | run('close-port', HTTP_PORT_PROTOCOL) |
355 | 20 | |||
356 | 21 | log('Finished stopping buildbot') | 24 | log('Finished stopping buildbot') |
357 | 22 | 25 | ||
358 | 23 | 26 | ||
359 | 24 | 27 | ||
360 | === modified file 'hooks/tests.py' | |||
361 | --- hooks/tests.py 2012-02-10 19:43:46 +0000 | |||
362 | +++ hooks/tests.py 2012-02-23 16:49:43 +0000 | |||
363 | @@ -1,16 +1,32 @@ | |||
364 | 1 | # Copyright 2012 Canonical Ltd. This software is licensed under the | ||
365 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
366 | 3 | |||
367 | 4 | """Helper functions for writing hooks in python.""" | ||
368 | 5 | |||
369 | 6 | __metaclass__ = type | ||
370 | 7 | |||
371 | 8 | |||
372 | 9 | from contextlib import contextmanager | ||
373 | 1 | import os | 10 | import os |
374 | 2 | from subprocess import CalledProcessError | 11 | from subprocess import CalledProcessError |
375 | 12 | import tempfile | ||
376 | 3 | import unittest | 13 | import unittest |
377 | 4 | 14 | ||
378 | 5 | from helpers import ( | 15 | from helpers import ( |
379 | 16 | cd, | ||
380 | 17 | command, | ||
381 | 6 | DictDiffer, | 18 | DictDiffer, |
382 | 7 | command, | ||
383 | 8 | run, | 19 | run, |
384 | 9 | su, | 20 | su, |
385 | 10 | unit_info, | 21 | unit_info, |
386 | 11 | ) | 22 | ) |
387 | 23 | from local import ( | ||
388 | 24 | get_bucket, | ||
389 | 25 | get_key, | ||
390 | 26 | fetch_history, | ||
391 | 27 | put_history, | ||
392 | 28 | ) | ||
393 | 12 | 29 | ||
394 | 13 | from subprocess import CalledProcessError | ||
395 | 14 | 30 | ||
396 | 15 | class TestRun(unittest.TestCase): | 31 | class TestRun(unittest.TestCase): |
397 | 16 | 32 | ||
398 | @@ -24,7 +40,7 @@ | |||
399 | 24 | # produces a string. | 40 | # produces a string. |
400 | 25 | self.assertIn('Usage:', run('/bin/ls', '--help')) | 41 | self.assertIn('Usage:', run('/bin/ls', '--help')) |
401 | 26 | 42 | ||
403 | 27 | def testSimpleCommand(self): | 43 | def testCalledProcessErrorRaised(self): |
404 | 28 | # If an error occurs a CalledProcessError is raised with the return | 44 | # If an error occurs a CalledProcessError is raised with the return |
405 | 29 | # code, command executed, and the output of the command. | 45 | # code, command executed, and the output of the command. |
406 | 30 | with self.assertRaises(CalledProcessError) as info: | 46 | with self.assertRaises(CalledProcessError) as info: |
407 | @@ -205,5 +221,183 @@ | |||
408 | 205 | self.assertEqual(current_home, os.environ['HOME']) | 221 | self.assertEqual(current_home, os.environ['HOME']) |
409 | 206 | 222 | ||
410 | 207 | 223 | ||
411 | 224 | class TestCdContextManager(unittest.TestCase): | ||
412 | 225 | def test_cd(self): | ||
413 | 226 | curdir = os.getcwd() | ||
414 | 227 | self.assertNotEqual('/var', curdir) | ||
415 | 228 | with cd('/var'): | ||
416 | 229 | self.assertEqual('/var', os.getcwd()) | ||
417 | 230 | self.assertEqual(curdir, os.getcwd()) | ||
418 | 231 | |||
419 | 232 | |||
420 | 233 | class StubBucket: | ||
421 | 234 | """Stub S3 Bucket.""" | ||
422 | 235 | def __init__(self, name): | ||
423 | 236 | self.name = name | ||
424 | 237 | self.keys = {} | ||
425 | 238 | |||
426 | 239 | |||
427 | 240 | class StubKey: | ||
428 | 241 | """Stub S3 Key.""" | ||
429 | 242 | def __init__(self, bucket): | ||
430 | 243 | self.bucket = bucket | ||
431 | 244 | self._name = None | ||
432 | 245 | |||
433 | 246 | def _get_key(self): | ||
434 | 247 | return self._name | ||
435 | 248 | def _set_key(self, name): | ||
436 | 249 | if name not in self.bucket.keys: | ||
437 | 250 | self.bucket.keys[name] = None | ||
438 | 251 | self._name = name | ||
439 | 252 | key = property(_get_key, _set_key) | ||
440 | 253 | |||
441 | 254 | def _get_value(self): | ||
442 | 255 | return self.bucket.keys.get(self._name) | ||
443 | 256 | def _set_value(self, v): | ||
444 | 257 | self.bucket.keys[self._name] = v | ||
445 | 258 | value = property(_get_value, _set_value) | ||
446 | 259 | |||
447 | 260 | def exists(self): | ||
448 | 261 | return self.value is not None | ||
449 | 262 | |||
450 | 263 | def get_contents_to_filename(self, fn): | ||
451 | 264 | with open(fn, 'w') as f: | ||
452 | 265 | f.write(self.value) | ||
453 | 266 | |||
454 | 267 | def set_contents_from_filename(self, fn): | ||
455 | 268 | with open(fn, 'r') as f: | ||
456 | 269 | self.value = f.read() | ||
457 | 270 | |||
458 | 271 | |||
459 | 272 | class StubConnection: | ||
460 | 273 | """A stub S3 connection.""" | ||
461 | 274 | def __init__(self, access_key, secret_key): | ||
462 | 275 | self.buckets = {} | ||
463 | 276 | |||
464 | 277 | def create_bucket(self, name): | ||
465 | 278 | if name in self.buckets: | ||
466 | 279 | bucket = self.buckets[name] | ||
467 | 280 | else: | ||
468 | 281 | bucket = StubBucket(name) | ||
469 | 282 | self.buckets[name] = bucket | ||
470 | 283 | return bucket | ||
471 | 284 | |||
472 | 285 | |||
473 | 286 | connection = None | ||
474 | 287 | |||
475 | 288 | |||
476 | 289 | def stub_connect_s3(access_key, secret_key): | ||
477 | 290 | # A stub boto connection factory. | ||
478 | 291 | global connection | ||
479 | 292 | if connection is None: | ||
480 | 293 | connection = StubConnection(access_key, secret_key) | ||
481 | 294 | return connection | ||
482 | 295 | |||
483 | 296 | |||
484 | 297 | def stub_log(msg): | ||
485 | 298 | pass | ||
486 | 299 | |||
487 | 300 | |||
488 | 301 | @contextmanager | ||
489 | 302 | def stub_su(user): | ||
490 | 303 | yield | ||
491 | 304 | |||
492 | 305 | |||
493 | 306 | class TestHistoryManagement(unittest.TestCase): | ||
494 | 307 | |||
495 | 308 | def setUp(self): | ||
496 | 309 | self.config = {'access-key': 'fake-access-key', | ||
497 | 310 | 'secret-key': 'fake-secret-key', | ||
498 | 311 | 'bucket-name': 'fake-bucket-name', | ||
499 | 312 | 'installdir': '/tmp', | ||
500 | 313 | } | ||
501 | 314 | import local | ||
502 | 315 | import boto | ||
503 | 316 | import boto.s3.key | ||
504 | 317 | self.connect_s3 = boto.connect_s3 | ||
505 | 318 | self.Key = boto.s3.key.Key | ||
506 | 319 | self.log = local.log | ||
507 | 320 | self.get_buildbot_version = local.get_buildbot_version | ||
508 | 321 | self.su = local.su | ||
509 | 322 | boto.connect_s3 = stub_connect_s3 | ||
510 | 323 | boto.s3.key.Key = StubKey | ||
511 | 324 | local.log = stub_log | ||
512 | 325 | local.get_buildbot_version = lambda: '0.8' | ||
513 | 326 | local.su = stub_su | ||
514 | 327 | self.tempfile = tempfile.NamedTemporaryFile(delete=False) | ||
515 | 328 | self.tempfilename = self.tempfile.name | ||
516 | 329 | self.tempfile.close() | ||
517 | 330 | os.environ['JUJU_UNIT_NAME'] = 'fake-unit-name' | ||
518 | 331 | |||
519 | 332 | def tearDown(self): | ||
520 | 333 | import local | ||
521 | 334 | import boto | ||
522 | 335 | import boto.s3.key | ||
523 | 336 | boto.connect_s3 = self.connect_s3 | ||
524 | 337 | boto.s3.key.Key = self.Key | ||
525 | 338 | local.log = self.log | ||
526 | 339 | local.get_buildbot_version = self.get_buildbot_version | ||
527 | 340 | local.su = self.su | ||
528 | 341 | os.unlink(self.tempfilename) | ||
529 | 342 | |||
530 | 343 | def test_get_bucket(self): | ||
531 | 344 | bucket = get_bucket(self.config, 'fake-bucket') | ||
532 | 345 | self.assertEqual('fake-bucket', bucket.name) | ||
533 | 346 | |||
534 | 347 | def test_get_key(self): | ||
535 | 348 | bucket = get_bucket(self.config, 'fake-bucket') | ||
536 | 349 | key = get_key(bucket) | ||
537 | 350 | self.assertEqual('fake-bucket', key.bucket.name) | ||
538 | 351 | self.assertEqual('fake-unit-name', key.key) | ||
539 | 352 | self.assertFalse(key.exists()) | ||
540 | 353 | |||
541 | 354 | def test_key_content_file(self): | ||
542 | 355 | contents = "Fake file contents, yo." | ||
543 | 356 | with open(self.tempfilename, 'w') as f: | ||
544 | 357 | f.write(contents) | ||
545 | 358 | bucket = get_bucket(self.config, 'fake-bucket') | ||
546 | 359 | key = get_key(bucket) | ||
547 | 360 | key.set_contents_from_filename(self.tempfilename) | ||
548 | 361 | self.assertTrue(key.exists()) | ||
549 | 362 | self.assertEqual(contents, key.value) | ||
550 | 363 | os.unlink(self.tempfilename) | ||
551 | 364 | key.get_contents_to_filename(self.tempfilename) | ||
552 | 365 | with open(self.tempfilename, 'r') as f: | ||
553 | 366 | value = f.read() | ||
554 | 367 | self.assertEqual(contents, value) | ||
555 | 368 | |||
556 | 369 | def test_put_fetch_history(self): | ||
557 | 370 | # Tests putting the history file into the fake bucket and then | ||
558 | 371 | # retrieving it. | ||
559 | 372 | fake_contents = "Fake state.sqlite" | ||
560 | 373 | history_fn = os.path.join(self.config['installdir'], 'state.sqlite') | ||
561 | 374 | with open(history_fn, 'w') as f: | ||
562 | 375 | f.write(fake_contents) | ||
563 | 376 | put_history(self.config) | ||
564 | 377 | os.unlink(history_fn) | ||
565 | 378 | diff = DictDiffer(self.config, {}) | ||
566 | 379 | restart = fetch_history(self.config, diff) | ||
567 | 380 | self.assertTrue(restart) | ||
568 | 381 | with open(history_fn, 'r') as f: | ||
569 | 382 | contents = f.read() | ||
570 | 383 | self.assertEqual(fake_contents, contents) | ||
571 | 384 | |||
572 | 385 | def test_fetch_history_no_restart(self): | ||
573 | 386 | diff = DictDiffer(self.config, self.config) | ||
574 | 387 | restart = fetch_history(self.config, diff) | ||
575 | 388 | self.assertFalse(restart) | ||
576 | 389 | |||
577 | 390 | def test_fetch_history_no_credentials(self): | ||
578 | 391 | self.config['access-key'] = None | ||
579 | 392 | diff = DictDiffer(self.config, {}) | ||
580 | 393 | restart = fetch_history(self.config, diff) | ||
581 | 394 | self.assertFalse(restart) | ||
582 | 395 | |||
583 | 396 | def test_fetch_history_no_key(self): | ||
584 | 397 | diff = DictDiffer(self.config, {}) | ||
585 | 398 | restart = fetch_history(self.config, diff) | ||
586 | 399 | self.assertFalse(restart) | ||
587 | 400 | |||
588 | 401 | |||
589 | 208 | if __name__ == '__main__': | 402 | if __name__ == '__main__': |
590 | 209 | unittest.main() | 403 | unittest.main() |
Brad, this looks great! Nicely done, and nice to have helpers that can do this for us with future charms.
As I mentioned on IRC, please clean up the log messages in handle_ config_ changes one way or another, so that we no longer have a colon and two messages when one would do.
Making a chdir context manager would be nice. We have one or two hanging around. You mentioned you had found one in setuplxc.
I asked whether tar xvf will have the expanded file or the existing file win: you said the expanded one, which is good.
You pointed out there are no tests. If there's a sane way to write them then that would be great.
Thank you!
Gary