Merge lp:~thomir-deactivatedaccount/adt-continuous-deployer/trunk-config-on-disk into lp:adt-continuous-deployer
- trunk-config-on-disk
- Merge into trunk
Proposed by
Thomi Richards
Status: | Merged |
---|---|
Approved by: | Thomi Richards |
Approved revision: | 68 |
Merged at revision: | 60 |
Proposed branch: | lp:~thomir-deactivatedaccount/adt-continuous-deployer/trunk-config-on-disk |
Merge into: | lp:adt-continuous-deployer |
Diff against target: |
821 lines (+654/-82) 4 files modified
README (+137/-44) ci_automation/config.py (+171/-0) ci_automation/tests/test_config.py (+272/-0) mojo.py (+74/-38) |
To merge this branch: | bzr merge lp:~thomir-deactivatedaccount/adt-continuous-deployer/trunk-config-on-disk |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Larson | Approve | ||
Review via email: mp+260669@code.launchpad.net |
Commit message
Add code to read config from disk.
Description of the change
This branch changes the way we handle service config.
Instead of generating it from command line arguments, we read from a bzr branch on disk.
This is now ready to be merged, and has been tested on the bootstack jumphost for the snappy-
To post a comment you must log in.
- 67. By Thomi Richards
-
Don't count non-committed files when checking for config on disk.
- 68. By Thomi Richards
-
Updated README.
Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
I replied to your inline comments...
Revision history for this message
Paul Larson (pwlars) wrote : | # |
Nice! Thanks for updating that, this will be really useful in the future.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'README' |
2 | --- README 2015-04-15 17:26:25 +0000 |
3 | +++ README 2015-06-04 21:46:36 +0000 |
4 | @@ -8,7 +8,7 @@ |
5 | code is supposed to run in, we try to avoid additional dependencies. |
6 | |
7 | Although for supporting remote logging via Logstash, we need a small package |
8 | -that can/should be installed locally:: |
9 | +that can/should be installed locally:: |
10 | |
11 | $ wget https://pypi.python.org/packages/source/p/python-logstash/\ |
12 | python-logstash-0.4.2.tar.gz#md5=30350b384a006c1d1adea6f7df90bf47 |
13 | @@ -19,8 +19,61 @@ |
14 | environment variable, you do not need to install anything. |
15 | |
16 | |
17 | -Usage |
18 | -===== |
19 | +Background |
20 | +=========== |
21 | + |
22 | +Mojo Project Pools |
23 | +------------------ |
24 | + |
25 | +mojo.py accepts a '--project' argument - this tells it which mojo project |
26 | +should be used. If you plan on calling mojo.py directly, you will need to |
27 | +specify the mojo project name. |
28 | + |
29 | +However, cd.py contains a feature where mojo projects are stored in a pool. |
30 | +This prevents the need to have a 1:1 relationship between services and |
31 | +mojo projects, and allows us to use the mojo projects we have in the best |
32 | +possible way. To use the mojo project pool feature: |
33 | + |
34 | +1. Create the pool directory:: |
35 | + |
36 | + mkdir ~/mojo_project_pool |
37 | + |
38 | +2. Create the pool lock directory:: |
39 | + |
40 | + mkdir ~/mojo_project_pool/locks |
41 | + |
42 | +3. For each mojo project that you want to use in the pool, create an empty |
43 | + file in the project pool directory. For example, if you have a mojo |
44 | + project called 'ci-0':: |
45 | + |
46 | + touch ~/mojo_project_pool/ci-0 |
47 | + |
48 | +4. When calling ci.py, pass the `--use-project-pool` argument, and *don't* |
49 | + pass the `--project` option. |
50 | + |
51 | +Service Configuration |
52 | +--------------------- |
53 | + |
54 | +Most mojo specs require configuration and other files to be present in the |
55 | +`/srv/mojo/LOCAL/<mojo_project>/<mojo_stage>/` directory. cd.py will expect |
56 | +all these files to be in a bzr repository. To set up service config: |
57 | + |
58 | +1. Create the config repository:: |
59 | + |
60 | + bzr init ~/service_configs |
61 | + |
62 | +2. Within that repository, create a directory that matches the full mojo |
63 | + stage you're deploying. For example:: |
64 | + |
65 | + cd ~/service_configs |
66 | + mkdir -p ue/mojo-ue-snappy-proposed-selftest-agent/devel |
67 | + |
68 | +3. Add whatever config files, data files, or other secrets you need to that |
69 | + directory. Commit your changes, and cd.py will ensure those files are |
70 | + present when mojo copies them to the spec workspace. |
71 | + |
72 | +Creating new Mojo Projects |
73 | +-------------------------- |
74 | |
75 | If working locally, you will have to create the shared mojo project called |
76 | "mojo-stg-ue-ci-engineering" deployments :: |
77 | @@ -31,7 +84,17 @@ |
78 | mojo-project are managed by IS. You can choose a different project name, |
79 | but that will force you to pass it via '--project' to `mojo.py`. |
80 | |
81 | -Deploy "mojo-ue-ci-rabbit":: |
82 | + |
83 | +Usage |
84 | +===== |
85 | + |
86 | +Deploying services with mojo.py |
87 | +------------------------------- |
88 | + |
89 | +The mojo.py script handles actually deploying services using mojo. On a |
90 | +jumphost, it's rarely called directly, but rather called from cd.py. |
91 | + |
92 | +To deploy the "mojo-ue-ci-rabbit" service:: |
93 | |
94 | $ ./mojo.py \ |
95 | --stage ue/mojo-ue-ci-rabbitmq/devel \ |
96 | @@ -40,42 +103,60 @@ |
97 | --base ~/juju-environments \ |
98 | # '--devel' only if you're deploying outside PS4. for e.g. bootstack |
99 | |
100 | - |
101 | -Get the IP address of the rabbit server using its dedicated environment |
102 | -information:: |
103 | - |
104 | - # NOTE: The SHA1_HASH below will vary according to the deployment contents |
105 | - $ JUJU_HOME=~/juju-environments/mojo-ue-ci-rabbit-{SHA1_HASH} \ |
106 | - juju status | grep public-address |
107 | - |
108 | -Deploy the "adt-cloud-worker":: |
109 | - |
110 | - $ ./mojo.py \ |
111 | - --stage ue/mojo-ue-adt-cloud-worker/devel \ |
112 | - --branch lp:~canonical-ci-engineering/canonical-mojo-specs/trunk \ |
113 | - --network 415a0839-eb05-4e7a-907c-413c657f4bf5 \ |
114 | - --base ~/juju-environments \ |
115 | - --amqp-uris "amqp://guest:guest@${AMQP_IP}:5672//" |
116 | - # '--devel' only if you're deploying outside PS4. for e.g. bootstack |
117 | - |
118 | -Check if "adt-cloud-worker" is correctly configured to access the |
119 | -previously deployed "ci-rabbit":: |
120 | - |
121 | - $ JUJU_HOME=/srv/juju-environments/mojo-ue-adt-cloud-worker-{SHA1_HASH}/ \ |
122 | - juju run --unit adt-cloud-worker/0 \ |
123 | - "tail /srv/adt-cloud-worker/logs/adt-cloud-worker.log" |
124 | - Start from server ... |
125 | - ... |
126 | - Open OK! |
127 | - Connected to amqp://guest:**@<<< GIVEN RABBIT IP ADDR >>>:5672// |
128 | - using channel_id: 1 |
129 | - Channel open |
130 | +The `--stage` environment variable specifies both the mojo spec and the mojo |
131 | +stage. It reads the mojo spec from whatever is specified in the `--branch` |
132 | +argument. |
133 | + |
134 | +The `--network` option is used for auto-generated config, and is being |
135 | +deprecated. |
136 | + |
137 | +Inspecting services with list.py and juju |
138 | +----------------------------------------- |
139 | + |
140 | +The list.py script gives us a list of everything that's been deployed. An |
141 | +example output is shown below:: |
142 | + |
143 | + $ ./adt-continuous-deployer/list.py |
144 | + * mojo-ue-snappy-proposed-image-builder: |
145 | + 27c224e5: Thu Jun 4 21:14:15 2015 (2 units) |
146 | + 705f3930: Thu Jun 4 18:28:16 2015 (2 units) |
147 | + c9ab5b8c: Thu Jun 4 18:14:15 2015 (2 units) |
148 | + deeb15d9: Wed Jun 3 19:35:17 2015 (1 units) |
149 | + a45035f8: Wed Jun 3 19:05:15 2015 (2 units) |
150 | + * mojo-ue-snappy-proposed-image-tester: |
151 | + 58dd9477: Wed Jun 3 18:30:47 2015 (2 units) |
152 | + * mojo-ue-snappy-proposed-result-checker: |
153 | + d021d997: Thu Jun 4 01:04:01 2015 (2 units) |
154 | + * mojo-ue-snappy-proposed-selftest-agent: |
155 | + 35edcc15: Thu Jun 4 20:40:32 2015 (2 units) |
156 | + 8b84a34a: Wed Jun 3 22:20:32 2015 (2 units) |
157 | + |
158 | +The first level of output is the service name. The second level is the |
159 | +deployment hash. The hash is calculated from all the bzr revnos of all the |
160 | +branches involved in the deployment. Here we can see several parallel |
161 | +deployments of the 'mojo-ue-snappy-proposed-image-builder' service. |
162 | + |
163 | +We can combine the service name and a deployment hash to get the juju home |
164 | +directory. This allows us to run arbitrary juju commands:: |
165 | + |
166 | + JUJU_HOME=~/juju-environments/mojo-ue-snappy-proposed-image-builder-27c224e5 \ |
167 | + juju stat |
168 | + |
169 | +This is useful for several tasks: |
170 | + |
171 | + * Inspecting why a service didn't deploy properly. |
172 | + * Finding the public IP address of a server. |
173 | + * Logging in to a deployed service with `juju ssh` |
174 | + * Running arbitrary commands on the deployed service with `juju run` |
175 | + |
176 | +Continuous Delivery with cd.py |
177 | +------------------------------ |
178 | |
179 | Continuous and isolated deployments can be done with `cd.py` which can |
180 | -periodically (cron) inspect a mojo spec and has been updated, it call |
181 | -`mojo.py` to do a new (and parallel) deployment:: |
182 | +periodically (via cron, for example) determine if a mojo spec, or any of the |
183 | +branches it deploys has been updated, and call mojo.py to deploy a the new |
184 | +revision. An example crontab setup is shown below:: |
185 | |
186 | - stg-ue-ci-engineering@wendigo:~$ crontab -l |
187 | NET_ID="79126fa3-b675-4f68-b7ec-c5f4be5dbe0e" |
188 | CI_SPECS_BRANCH="lp:~canonical-ci-engineering/canonical-mojo-specs/trunk" |
189 | |
190 | @@ -94,7 +175,7 @@ |
191 | part of that deployment:: |
192 | |
193 | # TAB-separated lines as: <branch>\t<revno>\n |
194 | - $ cat ~/ci-cd-identifiers/4c29c6aa |
195 | + $ cat ~/ci-cd-identifiers/4c29c6aa |
196 | lp:charms/trusty/haproxy 90 |
197 | lp:charms/trusty/apache2 65 |
198 | lp:~canonical-ci-engineering/adt-cloud-service/pip-cache 4 |
199 | @@ -106,17 +187,29 @@ |
200 | lp:~canonical-sysadmins/basenode/trunk 80 |
201 | lp:~canonical-sysadmins/basenode/trunk 80 |
202 | |
203 | +This information is also written to the nova instance(s) metadata field. |
204 | |
205 | -So, for figuring out what was deployed for a particular juju environment-name |
206 | +To figure out what was deployed for a particular juju environment-name |
207 | / machine-name we have to lookup for it's hash file and inspect its contents. |
208 | |
209 | -`monitor.py` is to monitor the number of deployments to not go out of hands. |
210 | -The default number per each service deployment is now 2 and any more deployments |
211 | -will be destroyed. |
212 | + |
213 | +Monitoring deployed services with monitor.py |
214 | +-------------------------------------------- |
215 | + |
216 | +The `monitor.py` script is used to monitor the number of deployments. Eventually |
217 | +we will autmoatically destroy old deployments, but right now this is not |
218 | +enabled. The policy in monitor.py is to allow two deployments (a current, and an |
219 | +old revision, and to destroy the rest). |
220 | |
221 | A cron job in wendigo employing `monitor.py` looks like the folloging:: |
222 | + |
223 | 8-59/20 * * * * . $HOME/.novarc; export CI_LOGSTASH_HOST=$INTERNAL_LOGSTASH_HOST; ~/adt-continuous-deployer/monitor.py mojo-ue-core-result-checker >> ~/core-result-checker.log 2>&1 |
224 | |
225 | -`reaper.py` is to destroy an deployment that was deployed using `cd.py` along |
226 | -with its environment by using its SHA1 hash value:: |
227 | +Destroying environments with reaper.py |
228 | +-------------------------------------- |
229 | + |
230 | +The `reaper.py` script is used to destroy an deployment that was deployed using |
231 | +`cd.py`. The only thing you need to specify is the deployment hash, as shown |
232 | +from `list.py`:: |
233 | + |
234 | ./reaper.py 4c29c6aa |
235 | |
236 | === added file 'ci_automation/config.py' |
237 | --- ci_automation/config.py 1970-01-01 00:00:00 +0000 |
238 | +++ ci_automation/config.py 2015-06-04 21:46:36 +0000 |
239 | @@ -0,0 +1,171 @@ |
240 | +# |
241 | +# Copyright (C) 2015 Canonical |
242 | +# |
243 | +# This program is free software: you can redistribute it and/or modify |
244 | +# it under the terms of the GNU General Public License as published by |
245 | +# the Free Software Foundation, either version 3 of the License, or |
246 | +# (at your option) any later version. |
247 | +# |
248 | +# This program is distributed in the hope that it will be useful, |
249 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
250 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
251 | +# GNU General Public License for more details. |
252 | +# |
253 | +# You should have received a copy of the GNU General Public License |
254 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
255 | +# |
256 | + |
257 | +"""CI Automation config helpers. |
258 | + |
259 | +We store config files on disk as fully realised files (no templating nonsense) |
260 | +and copy them to the mojo "secrets" folder before calling mojo to deploy the |
261 | +service in question. |
262 | + |
263 | +This gives us several advantages: |
264 | + |
265 | +1. It's super-simple to see what config a service will get deployed with - |
266 | + just look in the config directory and examine the last bzr commit contents. |
267 | + |
268 | +2. We get a log of all changes, who made them, and *why*, since we only get |
269 | + the last bzr commit - unstaged changes are never deployed. |
270 | + |
271 | +3. We can re-deploy a service by changing the config file, since the config |
272 | + folder's bzr revno is considered to be part of the deployment hash. |
273 | + |
274 | +All configs must be stored in `CONFIG_DIR`. By default this is set to |
275 | +~/service_configs. All functions read this variable without caching, so tests |
276 | +can be written by setting this variable to something else for the lifetime of |
277 | +the test. |
278 | + |
279 | +Beneath the config_dir, the folder structure should mimic the |
280 | +canonical-mojo-specs project. For example:: |
281 | + |
282 | + service_configs |
283 | + - ue |
284 | + - mojo-ue-adt-cloud-service |
285 | + - devel |
286 | + - production |
287 | + - mojo-ue-adt-cloud-worker |
288 | + - devel |
289 | + - production |
290 | + |
291 | +...continued for every mojo spec we want to deploy. Within every stage |
292 | +directory ('devel' / 'production' etc), any files present will be present in |
293 | +the mojo secrets dir when deployment begins. |
294 | + |
295 | +""" |
296 | + |
297 | +import os.path |
298 | +import re |
299 | +import shutil |
300 | +import subprocess |
301 | + |
302 | + |
303 | +__all__ = [ |
304 | + 'check_config_dir_exists', |
305 | + 'deployment_has_config', |
306 | + 'export_latest_config_for_deployment', |
307 | + 'get_revno_for_deployment', |
308 | +] |
309 | + |
310 | + |
311 | +CONFIG_DIR = os.path.expanduser("~/service_configs") |
312 | + |
313 | + |
314 | +def check_config_dir_exists(): |
315 | + """Return True if the config directory exists, and is a bzr repository.""" |
316 | + global CONFIG_DIR |
317 | + if not os.path.exists(CONFIG_DIR): |
318 | + return False |
319 | + return subprocess.call( |
320 | + ['bzr', 'info'], |
321 | + cwd=CONFIG_DIR, |
322 | + stdout=subprocess.DEVNULL, |
323 | + stderr=subprocess.DEVNULL, |
324 | + ) == 0 |
325 | + |
326 | + |
327 | +def deployment_has_config(mojo_stage): |
328 | + """Return true if a mojo spec stage has configuration data on disk. |
329 | + |
330 | + 'mojo_stage' should be the name of a mojo stage to deploy, like |
331 | + 'ue/mojo-ue-adt-cloud-service/devel' or |
332 | + 'ue/mojo-ue-adt-cloud-service/production'. |
333 | + |
334 | + """ |
335 | + global CONFIG_DIR |
336 | + full_path = os.path.join( |
337 | + CONFIG_DIR, |
338 | + mojo_stage, |
339 | + ) |
340 | + return ( |
341 | + os.path.exists(full_path) and |
342 | + os.path.isdir(full_path) and |
343 | + get_revno_for_deployment(mojo_stage) is not None |
344 | + ) |
345 | + |
346 | + |
347 | +def get_revno_for_deployment(mojo_stage): |
348 | + """Get the bzr revno for the given mojo spec & stage. |
349 | + |
350 | + 'mojo_stage' should be the name of a mojo stage to deploy, like |
351 | + 'ue/mojo-ue-adt-cloud-service/devel' or |
352 | + 'ue/mojo-ue-adt-cloud-service/production'. |
353 | + |
354 | + """ |
355 | + global CONFIG_DIR |
356 | + try: |
357 | + output = subprocess.check_output( |
358 | + ['bzr', 'log', '-l', '1', '-S', mojo_stage], |
359 | + cwd=CONFIG_DIR, |
360 | + stderr=subprocess.DEVNULL, |
361 | + ).decode() |
362 | + except subprocess.CalledProcessError as e: |
363 | + # returncode 3 is 'unknown path' |
364 | + if e.returncode != 3: |
365 | + raise |
366 | + return None |
367 | + match = re.search(' *([0-9]+)', output) |
368 | + if match: |
369 | + return match.group(1) |
370 | + return None |
371 | + |
372 | + |
373 | +def export_latest_config_for_deployment(mojo_stage, target_dir): |
374 | + """Export configuration for a mojo spec to 'target_dir'. |
375 | + |
376 | + 'mojo_stage' should be the name of a mojo stage to deploy, like |
377 | + 'ue/mojo-ue-adt-cloud-service/devel' or |
378 | + 'ue/mojo-ue-adt-cloud-service/production'. |
379 | + |
380 | + 'target_dir' is the target location to export to. This will probably be |
381 | + set to '/srv/mojo/LOCAL' in production. |
382 | + |
383 | + Directories for the spec and stage will be created in the target directory. |
384 | + |
385 | + :raises: ValueError if the given mojo spec & stage don't have configuration |
386 | + in the config dir. |
387 | + |
388 | + """ |
389 | + global CONFIG_DIR |
390 | + if not deployment_has_config(mojo_stage): |
391 | + raise ValueError( |
392 | + "Mojo stage '{}' have no config on disk.".format(mojo_stage) |
393 | + ) |
394 | + target_dir = os.path.join(target_dir, mojo_stage) |
395 | + config_dir = os.path.join(CONFIG_DIR, mojo_stage) |
396 | + |
397 | + if not os.path.exists(target_dir): |
398 | + os.makedirs(target_dir) |
399 | + else: |
400 | + for entry in os.listdir(target_dir): |
401 | + full_path = os.path.join(target_dir, entry) |
402 | + if os.path.isdir(full_path): |
403 | + shutil.rmtree(full_path) |
404 | + else: |
405 | + os.unlink(full_path) |
406 | + subprocess.check_call( |
407 | + ['bzr', 'export', target_dir, config_dir], |
408 | + stdout=subprocess.DEVNULL, |
409 | + stderr=subprocess.DEVNULL, |
410 | + ) |
411 | |
412 | === added file 'ci_automation/tests/test_config.py' |
413 | --- ci_automation/tests/test_config.py 1970-01-01 00:00:00 +0000 |
414 | +++ ci_automation/tests/test_config.py 2015-06-04 21:46:36 +0000 |
415 | @@ -0,0 +1,272 @@ |
416 | +#!/usr/bin/env python3 |
417 | +# |
418 | +# Copyright (C) 2015 Canonical |
419 | +# |
420 | +# This program is free software: you can redistribute it and/or modify |
421 | +# it under the terms of the GNU General Public License as published by |
422 | +# the Free Software Foundation, either version 3 of the License, or |
423 | +# (at your option) any later version. |
424 | +# |
425 | +# This program is distributed in the hope that it will be useful, |
426 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
427 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
428 | +# GNU General Public License for more details. |
429 | +# |
430 | +# You should have received a copy of the GNU General Public License |
431 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
432 | +# |
433 | + |
434 | +"""CI Automation tests for config helpers.""" |
435 | + |
436 | +import tempfile |
437 | +import unittest |
438 | +import shutil |
439 | +import functools |
440 | +import contextlib |
441 | +import subprocess |
442 | +import os.path |
443 | + |
444 | +from ci_automation import config |
445 | + |
446 | + |
447 | +class TestConfig(unittest.TestCase): |
448 | + |
449 | + def test_config_dir_check_returns_false_if_dir_does_not_exist(self): |
450 | + with tempfile.TemporaryDirectory() as workdir: |
451 | + config_dir = os.path.join(workdir, "doesnotexist") |
452 | + with patch_config_dir(config_dir): |
453 | + self.assertEqual(False, config.check_config_dir_exists()) |
454 | + |
455 | + def test_config_dir_check_returns_false_if_not_a_bzr_repo(self): |
456 | + with tempfile.TemporaryDirectory() as config_dir: |
457 | + with patch_config_dir(config_dir): |
458 | + self.assertEqual(False, config.check_config_dir_exists()) |
459 | + |
460 | + def test_config_dir_returns_true_on_empty_repo(self): |
461 | + with BzrConfigDir() as cm: |
462 | + self.assertEqual(True, config.check_config_dir_exists()) |
463 | + |
464 | + def test_deployment_has_config_returns_false_on_nonexisting_project(self): |
465 | + with BzrConfigDir() as cm: |
466 | + self.assertEqual( |
467 | + False, |
468 | + config.deployment_has_config('ue/nonexist/devel'), |
469 | + ) |
470 | + |
471 | + def test_deployment_has_config_returns_false_on_nonexisting_stage(self): |
472 | + with BzrConfigDir() as cm: |
473 | + os.makedirs(os.path.join(cm.config_dir, 'ue/test')) |
474 | + self.assertEqual( |
475 | + False, |
476 | + config.deployment_has_config('ue/test/devel'), |
477 | + ) |
478 | + |
479 | + def test_deployment_has_config_returns_true_on_existing_stage(self): |
480 | + with BzrConfigDir() as cm: |
481 | + cm.add_mojo_stage('ue/my-test-project/devel') |
482 | + self.assertEqual( |
483 | + True, |
484 | + config.deployment_has_config('ue/my-test-project/devel'), |
485 | + ) |
486 | + |
487 | + def test_get_revno_for_deployment_returns_none_without_bzr_control(self): |
488 | + with BzrConfigDir() as cm: |
489 | + os.makedirs(os.path.join(cm.config_dir, 'ue/project/devel')) |
490 | + self.assertIsNone( |
491 | + config.get_revno_for_deployment('ue/project/devel') |
492 | + ) |
493 | + |
494 | + def test_get_revno_for_deployment_returns_revno(self): |
495 | + with BzrConfigDir() as cm: |
496 | + cm.add_mojo_stage('ue/project/devel') |
497 | + self.assertEqual( |
498 | + '1', |
499 | + config.get_revno_for_deployment('ue/project/devel'), |
500 | + ) |
501 | + |
502 | + def test_adding_file_increases_bzr_revno(self): |
503 | + with BzrConfigDir() as cm: |
504 | + project = cm.add_mojo_stage('ue/project/production') |
505 | + with project.write_new_file('config.ini') as f: |
506 | + f.write('[section]\n') |
507 | + f.write('key = value\n') |
508 | + self.assertEqual( |
509 | + '2', |
510 | + config.get_revno_for_deployment('ue/project/production'), |
511 | + ) |
512 | + |
513 | + def test_projects_dont_increase_each_others_revnos(self): |
514 | + with BzrConfigDir() as cm: |
515 | + project1 = cm.add_mojo_stage('ue/project1/production') |
516 | + project2 = cm.add_mojo_stage('ue/project2/production') |
517 | + self.assertEqual( |
518 | + '1', |
519 | + config.get_revno_for_deployment('ue/project1/production'), |
520 | + ) |
521 | + self.assertEqual( |
522 | + '2', |
523 | + config.get_revno_for_deployment('ue/project2/production'), |
524 | + ) |
525 | + with project1.write_new_file('README') as f: |
526 | + f.write("Some text") |
527 | + self.assertEqual( |
528 | + '3', |
529 | + config.get_revno_for_deployment('ue/project1/production'), |
530 | + ) |
531 | + self.assertEqual( |
532 | + '2', |
533 | + config.get_revno_for_deployment('ue/project2/production'), |
534 | + ) |
535 | + |
536 | + def test_export_raises_ValueError_for_unconfigured_spec(self): |
537 | + with BzrConfigDir() as cm: |
538 | + with self.assertRaises(ValueError): |
539 | + config.export_latest_config_for_deployment( |
540 | + 'project/devel', |
541 | + 'target' |
542 | + ) |
543 | + |
544 | + def test_exports_empty_config_dir(self): |
545 | + with BzrConfigDir() as cm, tempfile.TemporaryDirectory() as target_dir: |
546 | + cm.add_mojo_stage('ue/project/production') |
547 | + target_project_path = os.path.join( |
548 | + target_dir, |
549 | + 'ue/project/production' |
550 | + ) |
551 | + config.export_latest_config_for_deployment( |
552 | + 'ue/project/production', |
553 | + target_dir |
554 | + ) |
555 | + self.assertTrue(os.path.exists(target_project_path)) |
556 | + |
557 | + def test_exports_simple_file(self): |
558 | + with BzrConfigDir() as cm, tempfile.TemporaryDirectory() as target_dir: |
559 | + project = cm.add_mojo_stage('ue/project/production') |
560 | + with project.write_new_file('config.ini') as f: |
561 | + f.write("Test config") |
562 | + config.export_latest_config_for_deployment( |
563 | + 'ue/project/production', |
564 | + target_dir |
565 | + ) |
566 | + target_config_path = os.path.join( |
567 | + target_dir, |
568 | + 'ue/project/production/config.ini' |
569 | + ) |
570 | + self.assertTrue(os.path.exists(target_config_path)) |
571 | + with open(target_config_path) as f: |
572 | + self.assertEqual(f.read(), "Test config") |
573 | + |
574 | + def test_exports_directory_heirarchy(self): |
575 | + with BzrConfigDir() as cm, tempfile.TemporaryDirectory() as target_dir: |
576 | + project = cm.add_mojo_stage('ue/project/production') |
577 | + project.create_directory('foo/bar/baz') |
578 | + with project.write_new_file('foo/bar/baz/config.ini') as f: |
579 | + f.write("Test config") |
580 | + config.export_latest_config_for_deployment( |
581 | + 'ue/project/production', |
582 | + target_dir |
583 | + ) |
584 | + target_config_path = os.path.join( |
585 | + target_dir, |
586 | + 'ue/project/production/foo/bar/baz/config.ini' |
587 | + ) |
588 | + self.assertTrue(os.path.exists(target_config_path)) |
589 | + with open(target_config_path) as f: |
590 | + self.assertEqual(f.read(), "Test config") |
591 | + |
592 | + |
593 | +@contextlib.contextmanager |
594 | +def patch_config_dir(new_dir): |
595 | + """A context manager that patches config.CONFIG_DIR.""" |
596 | + old_dir = config.CONFIG_DIR |
597 | + config.CONFIG_DIR = new_dir |
598 | + try: |
599 | + yield |
600 | + except: |
601 | + config.CONFIG_DIR = old_dir |
602 | + raise |
603 | + |
604 | + |
605 | +class BzrConfigDir(contextlib.ExitStack): |
606 | + """A context manager that sets up a simple config bzr repo.""" |
607 | + |
608 | + def __enter__(self): |
609 | + super().__enter__() |
610 | + self.config_dir = self.enter_context(tempfile.TemporaryDirectory()) |
611 | + self.enter_context(patch_config_dir(self.config_dir)) |
612 | + |
613 | + self._create_config_repo() |
614 | + return self |
615 | + |
616 | + def _create_config_repo(self): |
617 | + subprocess.check_call( |
618 | + ['bzr', 'init'], |
619 | + stdout=subprocess.DEVNULL, |
620 | + stderr=subprocess.DEVNULL, |
621 | + cwd=self.config_dir, |
622 | + ) |
623 | + |
624 | + def add_mojo_stage(self, spec_path): |
625 | + full_path = os.path.join(self.config_dir, spec_path) |
626 | + os.makedirs(full_path) |
627 | + subprocess.check_call( |
628 | + ['bzr', 'add', spec_path], |
629 | + stdout=subprocess.DEVNULL, |
630 | + stderr=subprocess.DEVNULL, |
631 | + cwd=self.config_dir, |
632 | + ) |
633 | + subprocess.check_call( |
634 | + ['bzr', 'commit', '-m', 'adding {}'.format(spec_path)], |
635 | + stdout=subprocess.DEVNULL, |
636 | + stderr=subprocess.DEVNULL, |
637 | + cwd=self.config_dir, |
638 | + ) |
639 | + return ProjectConfigManager(full_path) |
640 | + |
641 | + |
642 | +class ProjectConfigManager(object): |
643 | + |
644 | + """A simple interface to make it easy to write data to a test config |
645 | + dir for a mojo project. |
646 | + """ |
647 | + |
648 | + def __init__(self, path): |
649 | + self.path = path |
650 | + |
651 | + @contextlib.contextmanager |
652 | + def write_new_file(self, path): |
653 | + full_path = os.path.join(self.path, path) |
654 | + with open(full_path, 'wt') as f: |
655 | + yield f |
656 | + subprocess.check_call( |
657 | + ['bzr', 'add', path], |
658 | + cwd=self.path, |
659 | + stdout=subprocess.DEVNULL, |
660 | + stderr=subprocess.DEVNULL, |
661 | + ) |
662 | + subprocess.check_call( |
663 | + ['bzr', 'commit', '-m', 'adding {}'.format(path)], |
664 | + stdout=subprocess.DEVNULL, |
665 | + stderr=subprocess.DEVNULL, |
666 | + cwd=self.path, |
667 | + ) |
668 | + |
669 | + def create_directory(self, dir_path): |
670 | + full_path = os.path.join(self.path, dir_path) |
671 | + os.makedirs(full_path) |
672 | + subprocess.check_call( |
673 | + ['bzr', 'add', dir_path], |
674 | + cwd=self.path, |
675 | + stdout=subprocess.DEVNULL, |
676 | + stderr=subprocess.DEVNULL, |
677 | + ) |
678 | + subprocess.check_call( |
679 | + ['bzr', 'commit', '-m', 'adding {}'.format(dir_path)], |
680 | + stdout=subprocess.DEVNULL, |
681 | + stderr=subprocess.DEVNULL, |
682 | + cwd=self.path, |
683 | + ) |
684 | + |
685 | + |
686 | +if __name__ == '__main__': |
687 | + unittest.main() |
688 | |
689 | === modified file 'mojo.py' |
690 | --- mojo.py 2015-05-27 16:00:59 +0000 |
691 | +++ mojo.py 2015-06-04 21:46:36 +0000 |
692 | @@ -27,6 +27,7 @@ |
693 | |
694 | from ci_automation import ( |
695 | branch, |
696 | + config, |
697 | juju, |
698 | mojo, |
699 | utils, |
700 | @@ -157,48 +158,83 @@ |
701 | if args.devel: |
702 | fix_permissions(args.project) |
703 | |
704 | + # Thomi - we're moving to config files being stored on disk. As a |
705 | + # transitional stage, we use the new system if it has config on disk, |
706 | + # otherwise we fall back to the old system. |
707 | + if config.deployment_has_config(args.stage): |
708 | + # If config is on disk, warn the user if they're also specifying |
709 | + # config on the command line, as they might get confused otherwise. |
710 | + deprecated_args = ( |
711 | + 'network', |
712 | + 'amqp-uris', |
713 | + 'logstash-host', |
714 | + 'swift-public-url', |
715 | + 'image-mapper-url', |
716 | + 'os-username', |
717 | + 'os-tenant-name', |
718 | + 'os-password', |
719 | + 'os-auth-url', |
720 | + 'os-region-name', |
721 | + 'adt-os-username', |
722 | + 'adt-os-tenant-name', |
723 | + 'adt-os-password', |
724 | + 'adt-os-auth-url', |
725 | + 'adt-os-region-name', |
726 | + 'adt-extra-args', |
727 | + 'adt-http-proxy', |
728 | + ) |
729 | + used_deprepcated = [a for a in deprecated_args if a in args] |
730 | + if used_deprepcated: |
731 | + print("Warning: This mojo spec uses config on disk, but you have") |
732 | + print("specified one or more configuration options on the command") |
733 | + print("line. The following options will be ignored:") |
734 | + print(', '.join(used_deprepcated)) |
735 | + config_target = os.path.join('/srv/mojo/LOCAL/', args.project) |
736 | + config.export_latest_config_for_deployment(args.stage, config_target) |
737 | + else: |
738 | + # Generate config from command line options: |
739 | + computed_adt_extra_args = ' '.join([ |
740 | + args.adt_extra_args, |
741 | + '--net-id={}'.format(args.network) if args.devel else '', |
742 | + ]).strip() |
743 | + computed_adt_image_flavor = ( |
744 | + 'cpu2-ram2-disk50' if not args.devel else 'm1.small') |
745 | + computed_adt_archive_mirror = ( |
746 | + 'http://ftpmaster.internal/ubuntu/' if not args.devel else |
747 | + 'http://nova.clouds.archive.ubuntu.com/ubuntu/') |
748 | + computed_adt_http_proxy = ( |
749 | + 'http://squid.internal:3128/' if not args.devel else |
750 | + args.adt_http_proxy |
751 | + ) |
752 | + |
753 | + create_service_conf( |
754 | + args.project, |
755 | + args.stage, |
756 | + network=args.network, |
757 | + amqp_uris=args.amqp_uris, |
758 | + logstash_host=args.logstash_host, |
759 | + swift_public_url=args.swift_public_url, |
760 | + image_mapper_url=args.image_mapper_url, |
761 | + os_username=args.os_username, |
762 | + os_tenant_name=args.os_tenant_name, |
763 | + os_password=args.os_password, |
764 | + os_auth_url=args.os_auth_url, |
765 | + os_region_name=args.os_region_name, |
766 | + adt_os_username=args.adt_os_username, |
767 | + adt_os_tenant_name=args.adt_os_tenant_name, |
768 | + adt_os_password=args.adt_os_password, |
769 | + adt_os_auth_url=args.adt_os_auth_url, |
770 | + adt_os_region_name=args.adt_os_region_name, |
771 | + adt_extra_args=computed_adt_extra_args, |
772 | + adt_image_flavor=computed_adt_image_flavor, |
773 | + adt_archive_mirror=computed_adt_archive_mirror, |
774 | + adt_http_proxy=computed_adt_http_proxy, |
775 | + ) |
776 | + |
777 | # Relies on the canonical-mojo-specs layout, e.g.: |
778 | # ue/mojo-ue-adt-cloud-worker/devel |
779 | spec_name = args.stage.split('/')[1] |
780 | |
781 | - computed_adt_extra_args = ' '.join([ |
782 | - args.adt_extra_args, |
783 | - '--net-id={}'.format(args.network) if args.devel else '', |
784 | - ]).strip() |
785 | - computed_adt_image_flavor = ( |
786 | - 'cpu2-ram2-disk50' if not args.devel else 'm1.small') |
787 | - computed_adt_archive_mirror = ( |
788 | - 'http://ftpmaster.internal/ubuntu/' if not args.devel else |
789 | - 'http://nova.clouds.archive.ubuntu.com/ubuntu/') |
790 | - computed_adt_http_proxy = ( |
791 | - 'http://squid.internal:3128/' if not args.devel else |
792 | - args.adt_http_proxy |
793 | - ) |
794 | - |
795 | - create_service_conf( |
796 | - args.project, |
797 | - args.stage, |
798 | - network=args.network, |
799 | - amqp_uris=args.amqp_uris, |
800 | - logstash_host=args.logstash_host, |
801 | - swift_public_url=args.swift_public_url, |
802 | - image_mapper_url=args.image_mapper_url, |
803 | - os_username=args.os_username, |
804 | - os_tenant_name=args.os_tenant_name, |
805 | - os_password=args.os_password, |
806 | - os_auth_url=args.os_auth_url, |
807 | - os_region_name=args.os_region_name, |
808 | - adt_os_username=args.adt_os_username, |
809 | - adt_os_tenant_name=args.adt_os_tenant_name, |
810 | - adt_os_password=args.adt_os_password, |
811 | - adt_os_auth_url=args.adt_os_auth_url, |
812 | - adt_os_region_name=args.adt_os_region_name, |
813 | - adt_extra_args=computed_adt_extra_args, |
814 | - adt_image_flavor=computed_adt_image_flavor, |
815 | - adt_archive_mirror=computed_adt_archive_mirror, |
816 | - adt_http_proxy=computed_adt_http_proxy, |
817 | - ) |
818 | - |
819 | # Juju environment & mojo workspaces have identical names. Names |
820 | # are unique by appending a spec content identifier including |
821 | # all collected branch URLs and their current revision numbers, e.g.: |
A couple of small suggestions, take a look and let me know what you think.