Merge lp:~thomir-deactivatedaccount/adt-continuous-deployer/trunk-config-on-disk into lp:adt-continuous-deployer

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
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-proposed-results-checker service. It is backwards compatible - if no config is found in the configs directory, then the fallback option of generating config is used instead.

To post a comment you must log in.
67. By Thomi Richards

Don't count non-committed files when checking for config on disk.

Revision history for this message
Paul Larson (pwlars) wrote :

A couple of small suggestions, take a look and let me know what you think.

review: Needs Fixing
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.:

Subscribers

People subscribed via source and target branches

to all changes: