Merge lp:~michael.nelson/charm-helpers/ansible-support into lp:charm-helpers
- ansible-support
- Merge into devel
Proposed by
Michael Nelson
Status: | Merged |
---|---|
Merged at revision: | 65 |
Proposed branch: | lp:~michael.nelson/charm-helpers/ansible-support |
Merge into: | lp:charm-helpers |
Prerequisite: | lp:~michael.nelson/charm-helpers/namespace-relation-data |
Diff against target: |
545 lines (+346/-62) 7 files modified
charmhelpers/contrib/ansible/__init__.py (+101/-0) charmhelpers/contrib/saltstack/__init__.py (+17/-14) charmhelpers/fetch/__init__.py (+1/-1) setup.py (+1/-0) tests/contrib/ansible/test_ansible.py (+136/-0) tests/contrib/saltstack/test_saltstates.py (+87/-46) tests/fetch/test_fetch.py (+3/-1) |
To merge this branch: | bzr merge lp:~michael.nelson/charm-helpers/ansible-support |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Matthew Wedgwood (community) | Approve | ||
Review via email: mp+176973@code.launchpad.net |
Commit message
Add support for using ansible within charms.
Description of the change
Adds ansible support to charm-helpers.
I've converted a (private - sorry) charm to test out this support [1] - all works fine.
The juju config data, as well as any relation data is available to the ansible playbooks and templates using standard jinja2 syntax: {{ instance_type }}, where relation data is namespaced by the relation type, using django-like syntax: {{ solr__hostname }}.
One question I have is whether there's a better place for the juju_state_
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'charmhelpers/contrib/ansible' | |||
2 | === added file 'charmhelpers/contrib/ansible/__init__.py' | |||
3 | --- charmhelpers/contrib/ansible/__init__.py 1970-01-01 00:00:00 +0000 | |||
4 | +++ charmhelpers/contrib/ansible/__init__.py 2013-07-25 15:55:36 +0000 | |||
5 | @@ -0,0 +1,101 @@ | |||
6 | 1 | # Copyright 2013 Canonical Ltd. | ||
7 | 2 | # | ||
8 | 3 | # Authors: | ||
9 | 4 | # Charm Helpers Developers <juju@lists.ubuntu.com> | ||
10 | 5 | """Charm Helpers ansible - declare the state of your machines. | ||
11 | 6 | |||
12 | 7 | This helper enables you to declare your machine state, rather than | ||
13 | 8 | program it procedurally (and have to test each change to your procedures). | ||
14 | 9 | Your install hook can be as simple as: | ||
15 | 10 | |||
16 | 11 | {{{ | ||
17 | 12 | import charmhelpers.contrib.ansible | ||
18 | 13 | |||
19 | 14 | |||
20 | 15 | def install(): | ||
21 | 16 | charmhelpers.contrib.ansible.install_ansible_support() | ||
22 | 17 | charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml') | ||
23 | 18 | }}} | ||
24 | 19 | |||
25 | 20 | and won't need to change (nor will its tests) when you change the machine | ||
26 | 21 | state. | ||
27 | 22 | |||
28 | 23 | All of your juju config and relation-data are available as template | ||
29 | 24 | variables within your playbooks and templates. An install playbook looks | ||
30 | 25 | something like: | ||
31 | 26 | |||
32 | 27 | {{{ | ||
33 | 28 | --- | ||
34 | 29 | - hosts: localhost | ||
35 | 30 | user: root | ||
36 | 31 | |||
37 | 32 | tasks: | ||
38 | 33 | - name: Add private repositories. | ||
39 | 34 | template: | ||
40 | 35 | src: ../templates/private-repositories.list.jinja2 | ||
41 | 36 | dest: /etc/apt/sources.list.d/private.list | ||
42 | 37 | |||
43 | 38 | - name: Update the cache. | ||
44 | 39 | apt: update_cache=yes | ||
45 | 40 | |||
46 | 41 | - name: Install dependencies. | ||
47 | 42 | apt: pkg={{ item }} | ||
48 | 43 | with_items: | ||
49 | 44 | - python-mimeparse | ||
50 | 45 | - python-webob | ||
51 | 46 | - sunburnt | ||
52 | 47 | |||
53 | 48 | - name: Setup groups. | ||
54 | 49 | group: name={{ item.name }} gid={{ item.gid }} | ||
55 | 50 | with_items: | ||
56 | 51 | - { name: 'deploy_user', gid: 1800 } | ||
57 | 52 | - { name: 'service_user', gid: 1500 } | ||
58 | 53 | |||
59 | 54 | ... | ||
60 | 55 | }}} | ||
61 | 56 | |||
62 | 57 | Read more online about playbooks[1] and standard ansible modules[2]. | ||
63 | 58 | |||
64 | 59 | [1] http://www.ansibleworks.com/docs/playbooks.html | ||
65 | 60 | [2] http://www.ansibleworks.com/docs/modules.html | ||
66 | 61 | """ | ||
67 | 62 | import os | ||
68 | 63 | import subprocess | ||
69 | 64 | |||
70 | 65 | import charmhelpers.contrib.saltstack | ||
71 | 66 | import charmhelpers.core.host | ||
72 | 67 | import charmhelpers.core.hookenv | ||
73 | 68 | import charmhelpers.fetch | ||
74 | 69 | |||
75 | 70 | |||
76 | 71 | charm_dir = os.environ.get('CHARM_DIR', '') | ||
77 | 72 | ansible_hosts_path = '/etc/ansible/hosts' | ||
78 | 73 | # Ansible will automatically include any vars in the following | ||
79 | 74 | # file in its inventory when run locally. | ||
80 | 75 | ansible_vars_path = '/etc/ansible/host_vars/localhost' | ||
81 | 76 | |||
82 | 77 | |||
83 | 78 | def install_ansible_support(from_ppa=True): | ||
84 | 79 | """Installs the ansible package. | ||
85 | 80 | |||
86 | 81 | By default it is installed from the PPA [1] linked from | ||
87 | 82 | the ansible website [2]. | ||
88 | 83 | |||
89 | 84 | [1] https://launchpad.net/~rquillo/+archive/ansible | ||
90 | 85 | [2] http://www.ansibleworks.com/docs/gettingstarted.html#ubuntu-and-debian | ||
91 | 86 | |||
92 | 87 | If from_ppa is false, you must ensure that the package is available | ||
93 | 88 | from a configured repository. | ||
94 | 89 | """ | ||
95 | 90 | if from_ppa: | ||
96 | 91 | charmhelpers.fetch.add_source('ppa:rquillo/ansible') | ||
97 | 92 | charmhelpers.core.host.apt_update(fatal=True) | ||
98 | 93 | charmhelpers.core.host.apt_install('ansible') | ||
99 | 94 | with open(ansible_hosts_path, 'w+') as hosts_file: | ||
100 | 95 | hosts_file.write('localhost ansible_connection=local') | ||
101 | 96 | |||
102 | 97 | |||
103 | 98 | def apply_playbook(playbook): | ||
104 | 99 | charmhelpers.contrib.saltstack.juju_state_to_yaml( | ||
105 | 100 | ansible_vars_path, namespace_separator='__') | ||
106 | 101 | subprocess.check_call(['ansible-playbook', '-c', 'local', playbook]) | ||
107 | 0 | 102 | ||
108 | === modified file 'charmhelpers/contrib/saltstack/__init__.py' | |||
109 | --- charmhelpers/contrib/saltstack/__init__.py 2013-07-11 13:38:36 +0000 | |||
110 | +++ charmhelpers/contrib/saltstack/__init__.py 2013-07-25 15:55:36 +0000 | |||
111 | @@ -94,7 +94,7 @@ | |||
112 | 94 | 94 | ||
113 | 95 | def update_machine_state(state_path): | 95 | def update_machine_state(state_path): |
114 | 96 | """Update the machine state using the provided state declaration.""" | 96 | """Update the machine state using the provided state declaration.""" |
116 | 97 | juju_config_2_grains() | 97 | juju_state_to_yaml(salt_grains_path) |
117 | 98 | subprocess.check_call([ | 98 | subprocess.check_call([ |
118 | 99 | 'salt-call', | 99 | 'salt-call', |
119 | 100 | '--local', | 100 | '--local', |
120 | @@ -103,8 +103,8 @@ | |||
121 | 103 | ]) | 103 | ]) |
122 | 104 | 104 | ||
123 | 105 | 105 | ||
126 | 106 | def juju_config_2_grains(): | 106 | def juju_state_to_yaml(yaml_path, namespace_separator=':'): |
127 | 107 | """Insert the juju config as salt grains for use in state templates. | 107 | """Update the juju config and state in a yaml file. |
128 | 108 | 108 | ||
129 | 109 | This includes any current relation-get data, and the charm | 109 | This includes any current relation-get data, and the charm |
130 | 110 | directory. | 110 | directory. |
131 | @@ -121,7 +121,10 @@ | |||
132 | 121 | if relation_type is not None: | 121 | if relation_type is not None: |
133 | 122 | relation_data = charmhelpers.core.hookenv.relation_get() | 122 | relation_data = charmhelpers.core.hookenv.relation_get() |
134 | 123 | relation_data = dict( | 123 | relation_data = dict( |
136 | 124 | ("{}:{}".format(relation_type, key), val) | 124 | ("{relation_type}{namespace_separator}{key}".format( |
137 | 125 | relation_type=relation_type.replace('-', '_'), | ||
138 | 126 | key=key, | ||
139 | 127 | namespace_separator=namespace_separator), val) | ||
140 | 125 | for key, val in relation_data.items()) | 128 | for key, val in relation_data.items()) |
141 | 126 | config.update(relation_data) | 129 | config.update(relation_data) |
142 | 127 | 130 | ||
143 | @@ -131,16 +134,16 @@ | |||
144 | 131 | value: dumper.represent_scalar( | 134 | value: dumper.represent_scalar( |
145 | 132 | u'tag:yaml.org,2002:str', value)) | 135 | u'tag:yaml.org,2002:str', value)) |
146 | 133 | 136 | ||
150 | 134 | grains_dir = os.path.dirname(salt_grains_path) | 137 | yaml_dir = os.path.dirname(yaml_path) |
151 | 135 | if not os.path.exists(grains_dir): | 138 | if not os.path.exists(yaml_dir): |
152 | 136 | os.makedirs(grains_dir) | 139 | os.makedirs(yaml_dir) |
153 | 137 | 140 | ||
157 | 138 | if os.path.exists(salt_grains_path): | 141 | if os.path.exists(yaml_path): |
158 | 139 | with open(salt_grains_path, "r") as grain_file: | 142 | with open(yaml_path, "r") as existing_vars_file: |
159 | 140 | grains = yaml.load(grain_file.read()) | 143 | existing_vars = yaml.load(existing_vars_file.read()) |
160 | 141 | else: | 144 | else: |
162 | 142 | grains = {} | 145 | existing_vars = {} |
163 | 143 | 146 | ||
167 | 144 | grains.update(config) | 147 | existing_vars.update(config) |
168 | 145 | with open(salt_grains_path, "w+") as fp: | 148 | with open(yaml_path, "w+") as fp: |
169 | 146 | fp.write(yaml.dump(grains)) | 149 | fp.write(yaml.dump(existing_vars)) |
170 | 147 | 150 | ||
171 | === modified file 'charmhelpers/fetch/__init__.py' | |||
172 | --- charmhelpers/fetch/__init__.py 2013-07-03 11:42:37 +0000 | |||
173 | +++ charmhelpers/fetch/__init__.py 2013-07-25 15:55:36 +0000 | |||
174 | @@ -27,7 +27,7 @@ | |||
175 | 27 | def add_source(source, key=None): | 27 | def add_source(source, key=None): |
176 | 28 | if ((source.startswith('ppa:') or | 28 | if ((source.startswith('ppa:') or |
177 | 29 | source.startswith('http:'))): | 29 | source.startswith('http:'))): |
179 | 30 | subprocess.check_call(['add-apt-repository', source]) | 30 | subprocess.check_call(['add-apt-repository', '--yes', source]) |
180 | 31 | elif source.startswith('cloud:'): | 31 | elif source.startswith('cloud:'): |
181 | 32 | apt_install(filter_installed_packages(['ubuntu-cloud-keyring']), | 32 | apt_install(filter_installed_packages(['ubuntu-cloud-keyring']), |
182 | 33 | fatal=True) | 33 | fatal=True) |
183 | 34 | 34 | ||
184 | === modified file 'setup.py' | |||
185 | --- setup.py 2013-06-19 09:54:19 +0000 | |||
186 | +++ setup.py 2013-07-25 15:55:36 +0000 | |||
187 | @@ -20,6 +20,7 @@ | |||
188 | 20 | "charmhelpers.fetch", | 20 | "charmhelpers.fetch", |
189 | 21 | "charmhelpers.payload", | 21 | "charmhelpers.payload", |
190 | 22 | "charmhelpers.contrib", | 22 | "charmhelpers.contrib", |
191 | 23 | "charmhelpers.contrib.ansible", | ||
192 | 23 | "charmhelpers.contrib.charmhelpers", | 24 | "charmhelpers.contrib.charmhelpers", |
193 | 24 | "charmhelpers.contrib.charmsupport", | 25 | "charmhelpers.contrib.charmsupport", |
194 | 25 | "charmhelpers.contrib.saltstack", | 26 | "charmhelpers.contrib.saltstack", |
195 | 26 | 27 | ||
196 | === added directory 'tests/contrib/ansible' | |||
197 | === added file 'tests/contrib/ansible/__init__.py' | |||
198 | === added file 'tests/contrib/ansible/test_ansible.py' | |||
199 | --- tests/contrib/ansible/test_ansible.py 1970-01-01 00:00:00 +0000 | |||
200 | +++ tests/contrib/ansible/test_ansible.py 2013-07-25 15:55:36 +0000 | |||
201 | @@ -0,0 +1,136 @@ | |||
202 | 1 | # Copyright 2013 Canonical Ltd. | ||
203 | 2 | # | ||
204 | 3 | # Authors: | ||
205 | 4 | # Charm Helpers Developers <juju@lists.ubuntu.com> | ||
206 | 5 | import mock | ||
207 | 6 | import os | ||
208 | 7 | import shutil | ||
209 | 8 | import tempfile | ||
210 | 9 | import unittest | ||
211 | 10 | import yaml | ||
212 | 11 | |||
213 | 12 | |||
214 | 13 | import charmhelpers.contrib.ansible | ||
215 | 14 | |||
216 | 15 | |||
217 | 16 | class InstallAnsibleSupportTestCase(unittest.TestCase): | ||
218 | 17 | |||
219 | 18 | def setUp(self): | ||
220 | 19 | super(InstallAnsibleSupportTestCase, self).setUp() | ||
221 | 20 | |||
222 | 21 | patcher = mock.patch('charmhelpers.fetch') | ||
223 | 22 | self.mock_fetch = patcher.start() | ||
224 | 23 | self.addCleanup(patcher.stop) | ||
225 | 24 | |||
226 | 25 | patcher = mock.patch('charmhelpers.core') | ||
227 | 26 | self.mock_core = patcher.start() | ||
228 | 27 | self.addCleanup(patcher.stop) | ||
229 | 28 | |||
230 | 29 | |||
231 | 30 | hosts_file = tempfile.NamedTemporaryFile() | ||
232 | 31 | self.ansible_hosts_path = hosts_file.name | ||
233 | 32 | self.addCleanup(hosts_file.close) | ||
234 | 33 | patcher = mock.patch.object(charmhelpers.contrib.ansible, | ||
235 | 34 | 'ansible_hosts_path', | ||
236 | 35 | self.ansible_hosts_path) | ||
237 | 36 | patcher.start() | ||
238 | 37 | self.addCleanup(patcher.stop) | ||
239 | 38 | |||
240 | 39 | def test_adds_ppa_by_default(self): | ||
241 | 40 | charmhelpers.contrib.ansible.install_ansible_support() | ||
242 | 41 | |||
243 | 42 | self.mock_fetch.add_source.assert_called_once_with( | ||
244 | 43 | 'ppa:rquillo/ansible') | ||
245 | 44 | self.mock_core.host.apt_update.assert_called_once_with(fatal=True) | ||
246 | 45 | self.mock_core.host.apt_install.assert_called_once_with( | ||
247 | 46 | 'ansible') | ||
248 | 47 | |||
249 | 48 | def test_no_ppa(self): | ||
250 | 49 | charmhelpers.contrib.ansible.install_ansible_support( | ||
251 | 50 | from_ppa=False) | ||
252 | 51 | |||
253 | 52 | self.assertEqual(self.mock_fetch.add_source.call_count, 0) | ||
254 | 53 | self.mock_core.host.apt_install.assert_called_once_with( | ||
255 | 54 | 'ansible') | ||
256 | 55 | |||
257 | 56 | def test_writes_ansible_hosts(self): | ||
258 | 57 | with open(self.ansible_hosts_path) as hosts_file: | ||
259 | 58 | self.assertEqual(hosts_file.read(), '') | ||
260 | 59 | |||
261 | 60 | charmhelpers.contrib.ansible.install_ansible_support() | ||
262 | 61 | |||
263 | 62 | with open(self.ansible_hosts_path) as hosts_file: | ||
264 | 63 | self.assertEqual(hosts_file.read(), | ||
265 | 64 | 'localhost ansible_connection=local') | ||
266 | 65 | |||
267 | 66 | |||
268 | 67 | class ApplyPlaybookTestCases(unittest.TestCase): | ||
269 | 68 | |||
270 | 69 | def setUp(self): | ||
271 | 70 | super(ApplyPlaybookTestCases, self).setUp() | ||
272 | 71 | |||
273 | 72 | # Hookenv patches (a single patch to hookenv doesn't work): | ||
274 | 73 | patcher = mock.patch('charmhelpers.core.hookenv.config') | ||
275 | 74 | self.mock_config = patcher.start() | ||
276 | 75 | self.addCleanup(patcher.stop) | ||
277 | 76 | Serializable = charmhelpers.core.hookenv.Serializable | ||
278 | 77 | self.mock_config.return_value = Serializable({}) | ||
279 | 78 | patcher = mock.patch('charmhelpers.core.hookenv.relation_get') | ||
280 | 79 | self.mock_relation_get = patcher.start() | ||
281 | 80 | self.mock_relation_get.return_value = {} | ||
282 | 81 | self.addCleanup(patcher.stop) | ||
283 | 82 | patcher = mock.patch('charmhelpers.core.hookenv.relation_type') | ||
284 | 83 | self.mock_relation_type = patcher.start() | ||
285 | 84 | self.mock_relation_type.return_value = None | ||
286 | 85 | self.addCleanup(patcher.stop) | ||
287 | 86 | patcher = mock.patch('charmhelpers.core.hookenv.local_unit') | ||
288 | 87 | self.mock_local_unit = patcher.start() | ||
289 | 88 | self.addCleanup(patcher.stop) | ||
290 | 89 | self.mock_local_unit.return_value = {} | ||
291 | 90 | |||
292 | 91 | patcher = mock.patch('charmhelpers.contrib.ansible.subprocess') | ||
293 | 92 | self.mock_subprocess = patcher.start() | ||
294 | 93 | self.addCleanup(patcher.stop) | ||
295 | 94 | |||
296 | 95 | etc_dir = tempfile.mkdtemp() | ||
297 | 96 | self.addCleanup(shutil.rmtree, etc_dir) | ||
298 | 97 | self.vars_path = os.path.join(etc_dir, 'ansible', 'vars.yaml') | ||
299 | 98 | patcher = mock.patch.object(charmhelpers.contrib.ansible, | ||
300 | 99 | 'ansible_vars_path', self.vars_path) | ||
301 | 100 | patcher.start() | ||
302 | 101 | self.addCleanup(patcher.stop) | ||
303 | 102 | |||
304 | 103 | |||
305 | 104 | def test_calls_ansible_playbook(self): | ||
306 | 105 | charmhelpers.contrib.ansible.apply_playbook( | ||
307 | 106 | 'playbooks/dependencies.yaml') | ||
308 | 107 | |||
309 | 108 | self.mock_subprocess.check_call.assert_called_once_with([ | ||
310 | 109 | 'ansible-playbook', '-c', 'local', 'playbooks/dependencies.yaml']) | ||
311 | 110 | |||
312 | 111 | def test_writes_vars_file(self): | ||
313 | 112 | self.assertFalse(os.path.exists(self.vars_path)) | ||
314 | 113 | self.mock_config.return_value = charmhelpers.core.hookenv.Serializable({ | ||
315 | 114 | 'group_code_owner': 'webops_deploy', | ||
316 | 115 | 'user_code_runner': 'ubunet', | ||
317 | 116 | }) | ||
318 | 117 | self.mock_relation_type.return_value = 'wsgi-file' | ||
319 | 118 | self.mock_relation_get.return_value = { | ||
320 | 119 | 'relation_key1': 'relation_value1', | ||
321 | 120 | 'relation_key2': 'relation_value2', | ||
322 | 121 | } | ||
323 | 122 | |||
324 | 123 | charmhelpers.contrib.ansible.apply_playbook( | ||
325 | 124 | 'playbooks/dependencies.yaml') | ||
326 | 125 | |||
327 | 126 | self.assertTrue(os.path.exists(self.vars_path)) | ||
328 | 127 | with open(self.vars_path, 'r') as vars_file: | ||
329 | 128 | result = yaml.load(vars_file.read()) | ||
330 | 129 | self.assertEqual({ | ||
331 | 130 | "group_code_owner": "webops_deploy", | ||
332 | 131 | "user_code_runner": "ubunet", | ||
333 | 132 | "charm_dir": "", | ||
334 | 133 | "local_unit": {}, | ||
335 | 134 | "wsgi_file__relation_key1": "relation_value1", | ||
336 | 135 | "wsgi_file__relation_key2": "relation_value2", | ||
337 | 136 | }, result) | ||
338 | 0 | 137 | ||
339 | === modified file 'tests/contrib/saltstack/test_saltstates.py' | |||
340 | --- tests/contrib/saltstack/test_saltstates.py 2013-07-11 13:38:36 +0000 | |||
341 | +++ tests/contrib/saltstack/test_saltstates.py 2013-07-25 15:55:36 +0000 | |||
342 | @@ -9,7 +9,6 @@ | |||
343 | 9 | import unittest | 9 | import unittest |
344 | 10 | import yaml | 10 | import yaml |
345 | 11 | 11 | ||
346 | 12 | import charmhelpers.core.hookenv | ||
347 | 13 | import charmhelpers.contrib.saltstack | 12 | import charmhelpers.contrib.saltstack |
348 | 14 | 13 | ||
349 | 15 | 14 | ||
350 | @@ -62,7 +61,7 @@ | |||
351 | 62 | self.addCleanup(patcher.stop) | 61 | self.addCleanup(patcher.stop) |
352 | 63 | 62 | ||
353 | 64 | patcher = mock.patch('charmhelpers.contrib.saltstack.' | 63 | patcher = mock.patch('charmhelpers.contrib.saltstack.' |
355 | 65 | 'juju_config_2_grains') | 64 | 'juju_state_to_yaml') |
356 | 66 | self.mock_config_2_grains = patcher.start() | 65 | self.mock_config_2_grains = patcher.start() |
357 | 67 | self.addCleanup(patcher.stop) | 66 | self.addCleanup(patcher.stop) |
358 | 68 | 67 | ||
359 | @@ -81,7 +80,7 @@ | |||
360 | 81 | charmhelpers.contrib.saltstack.update_machine_state( | 80 | charmhelpers.contrib.saltstack.update_machine_state( |
361 | 82 | 'states/install.yaml') | 81 | 'states/install.yaml') |
362 | 83 | 82 | ||
364 | 84 | self.mock_config_2_grains.assert_called_once_with() | 83 | self.mock_config_2_grains.assert_called_once_with('/etc/salt/grains') |
365 | 85 | 84 | ||
366 | 86 | 85 | ||
367 | 87 | class JujuConfig2GrainsTestCase(unittest.TestCase): | 86 | class JujuConfig2GrainsTestCase(unittest.TestCase): |
368 | @@ -108,56 +107,98 @@ | |||
369 | 108 | etc_dir = tempfile.mkdtemp() | 107 | etc_dir = tempfile.mkdtemp() |
370 | 109 | self.addCleanup(shutil.rmtree, etc_dir) | 108 | self.addCleanup(shutil.rmtree, etc_dir) |
371 | 110 | self.grain_path = os.path.join(etc_dir, 'salt', 'grains') | 109 | self.grain_path = os.path.join(etc_dir, 'salt', 'grains') |
372 | 111 | patcher = mock.patch.object(charmhelpers.contrib.saltstack, | ||
373 | 112 | 'salt_grains_path', self.grain_path) | ||
374 | 113 | patcher.start() | ||
375 | 114 | self.addCleanup(patcher.stop) | ||
376 | 115 | 110 | ||
377 | 116 | patcher = mock.patch.object(charmhelpers.contrib.saltstack, | 111 | patcher = mock.patch.object(charmhelpers.contrib.saltstack, |
378 | 117 | 'charm_dir', '/tmp/charm_dir') | 112 | 'charm_dir', '/tmp/charm_dir') |
379 | 118 | patcher.start() | 113 | patcher.start() |
380 | 119 | self.addCleanup(patcher.stop) | 114 | self.addCleanup(patcher.stop) |
381 | 120 | 115 | ||
399 | 121 | def test_output_without_relation(self): | 116 | def test_output_with_empty_relation(self): |
400 | 122 | self.mock_config.return_value = charmhelpers.core.hookenv.Serializable({ | 117 | self.mock_config.return_value = { |
401 | 123 | 'group_code_owner': 'webops_deploy', | 118 | 'group_code_owner': 'webops_deploy', |
402 | 124 | 'user_code_runner': 'ubunet', | 119 | 'user_code_runner': 'ubunet', |
403 | 125 | }) | 120 | } |
404 | 126 | self.mock_local_unit.return_value = "click-index/3" | 121 | self.mock_local_unit.return_value = "click-index/3" |
405 | 127 | 122 | ||
406 | 128 | charmhelpers.contrib.saltstack.juju_config_2_grains() | 123 | charmhelpers.contrib.saltstack.juju_state_to_yaml(self.grain_path) |
407 | 129 | 124 | ||
408 | 130 | with open(self.grain_path, 'r') as grain_file: | 125 | with open(self.grain_path, 'r') as grain_file: |
409 | 131 | result = yaml.load(grain_file.read()) | 126 | result = yaml.load(grain_file.read()) |
410 | 132 | self.assertEqual({ | 127 | self.assertEqual({ |
411 | 133 | "charm_dir": "/tmp/charm_dir", | 128 | "charm_dir": "/tmp/charm_dir", |
412 | 134 | "group_code_owner": "webops_deploy", | 129 | "group_code_owner": "webops_deploy", |
413 | 135 | "user_code_runner": "ubunet", | 130 | "user_code_runner": "ubunet", |
414 | 136 | "local_unit": "click-index/3", | 131 | "local_unit": "click-index/3", |
415 | 137 | }, result) | 132 | }, result) |
416 | 133 | |||
417 | 134 | def test_output_with_no_relation(self): | ||
418 | 135 | self.mock_config.return_value = { | ||
419 | 136 | 'group_code_owner': 'webops_deploy', | ||
420 | 137 | 'user_code_runner': 'ubunet', | ||
421 | 138 | } | ||
422 | 139 | self.mock_local_unit.return_value = "click-index/3" | ||
423 | 140 | self.mock_relation_get.return_value = None | ||
424 | 141 | |||
425 | 142 | charmhelpers.contrib.saltstack.juju_state_to_yaml(self.grain_path) | ||
426 | 143 | |||
427 | 144 | with open(self.grain_path, 'r') as grain_file: | ||
428 | 145 | result = yaml.load(grain_file.read()) | ||
429 | 146 | self.assertEqual({ | ||
430 | 147 | "charm_dir": "/tmp/charm_dir", | ||
431 | 148 | "group_code_owner": "webops_deploy", | ||
432 | 149 | "user_code_runner": "ubunet", | ||
433 | 150 | "local_unit": "click-index/3", | ||
434 | 151 | }, result) | ||
435 | 152 | |||
436 | 138 | 153 | ||
437 | 139 | def test_output_with_relation(self): | 154 | def test_output_with_relation(self): |
459 | 140 | self.mock_config.return_value = charmhelpers.core.hookenv.Serializable({ | 155 | self.mock_config.return_value = { |
460 | 141 | 'group_code_owner': 'webops_deploy', | 156 | 'group_code_owner': 'webops_deploy', |
461 | 142 | 'user_code_runner': 'ubunet', | 157 | 'user_code_runner': 'ubunet', |
462 | 143 | }) | 158 | } |
463 | 144 | self.mock_relation_type.return_value = 'wsgi-file' | 159 | self.mock_relation_type.return_value = 'wsgi-file' |
464 | 145 | self.mock_relation_get.return_value = { | 160 | self.mock_relation_get.return_value = { |
465 | 146 | 'relation_key1': 'relation_value1', | 161 | 'relation_key1': 'relation_value1', |
466 | 147 | 'relation_key2': 'relation_value2', | 162 | 'relation_key2': 'relation_value2', |
467 | 148 | } | 163 | } |
468 | 149 | self.mock_local_unit.return_value = "click-index/3" | 164 | self.mock_local_unit.return_value = "click-index/3" |
469 | 150 | 165 | ||
470 | 151 | charmhelpers.contrib.saltstack.juju_config_2_grains() | 166 | charmhelpers.contrib.saltstack.juju_state_to_yaml(self.grain_path) |
471 | 152 | 167 | ||
472 | 153 | with open(self.grain_path, 'r') as grain_file: | 168 | with open(self.grain_path, 'r') as grain_file: |
473 | 154 | result = yaml.load(grain_file.read()) | 169 | result = yaml.load(grain_file.read()) |
474 | 155 | self.assertEqual({ | 170 | self.assertEqual({ |
475 | 156 | "charm_dir": "/tmp/charm_dir", | 171 | "charm_dir": "/tmp/charm_dir", |
476 | 157 | "group_code_owner": "webops_deploy", | 172 | "group_code_owner": "webops_deploy", |
477 | 158 | "user_code_runner": "ubunet", | 173 | "user_code_runner": "ubunet", |
478 | 159 | "wsgi-file:relation_key1": "relation_value1", | 174 | "wsgi_file:relation_key1": "relation_value1", |
479 | 160 | "wsgi-file:relation_key2": "relation_value2", | 175 | "wsgi_file:relation_key2": "relation_value2", |
480 | 176 | "local_unit": "click-index/3", | ||
481 | 177 | }, result) | ||
482 | 178 | |||
483 | 179 | def test_relation_with_separator(self): | ||
484 | 180 | self.mock_config.return_value = { | ||
485 | 181 | 'group_code_owner': 'webops_deploy', | ||
486 | 182 | 'user_code_runner': 'ubunet', | ||
487 | 183 | } | ||
488 | 184 | self.mock_relation_type.return_value = 'wsgi-file' | ||
489 | 185 | self.mock_relation_get.return_value = { | ||
490 | 186 | 'relation_key1': 'relation_value1', | ||
491 | 187 | 'relation_key2': 'relation_value2', | ||
492 | 188 | } | ||
493 | 189 | self.mock_local_unit.return_value = "click-index/3" | ||
494 | 190 | |||
495 | 191 | charmhelpers.contrib.saltstack.juju_state_to_yaml( | ||
496 | 192 | self.grain_path, namespace_separator='__') | ||
497 | 193 | |||
498 | 194 | with open(self.grain_path, 'r') as grain_file: | ||
499 | 195 | result = yaml.load(grain_file.read()) | ||
500 | 196 | self.assertEqual({ | ||
501 | 197 | "charm_dir": "/tmp/charm_dir", | ||
502 | 198 | "group_code_owner": "webops_deploy", | ||
503 | 199 | "user_code_runner": "ubunet", | ||
504 | 200 | "wsgi_file__relation_key1": "relation_value1", | ||
505 | 201 | "wsgi_file__relation_key2": "relation_value2", | ||
506 | 161 | "local_unit": "click-index/3", | 202 | "local_unit": "click-index/3", |
507 | 162 | }, result) | 203 | }, result) |
508 | 163 | 204 | ||
509 | @@ -180,7 +221,7 @@ | |||
510 | 180 | }) | 221 | }) |
511 | 181 | self.mock_local_unit.return_value = "click-index/3" | 222 | self.mock_local_unit.return_value = "click-index/3" |
512 | 182 | 223 | ||
514 | 183 | charmhelpers.contrib.saltstack.juju_config_2_grains() | 224 | charmhelpers.contrib.saltstack.juju_state_to_yaml(self.grain_path) |
515 | 184 | 225 | ||
516 | 185 | with open(self.grain_path, 'r') as grain_file: | 226 | with open(self.grain_path, 'r') as grain_file: |
517 | 186 | result = yaml.load(grain_file.read()) | 227 | result = yaml.load(grain_file.read()) |
518 | 187 | 228 | ||
519 | === modified file 'tests/fetch/test_fetch.py' | |||
520 | --- tests/fetch/test_fetch.py 2013-07-03 11:42:37 +0000 | |||
521 | +++ tests/fetch/test_fetch.py 2013-07-25 15:55:36 +0000 | |||
522 | @@ -34,6 +34,7 @@ | |||
523 | 34 | source = "ppa:test-ppa" | 34 | source = "ppa:test-ppa" |
524 | 35 | fetch.add_source(source=source) | 35 | fetch.add_source(source=source) |
525 | 36 | check_call.assert_called_with(['add-apt-repository', | 36 | check_call.assert_called_with(['add-apt-repository', |
526 | 37 | '--yes', | ||
527 | 37 | source]) | 38 | source]) |
528 | 38 | 39 | ||
529 | 39 | @patch('subprocess.check_call') | 40 | @patch('subprocess.check_call') |
530 | @@ -41,6 +42,7 @@ | |||
531 | 41 | source = "http://archive.ubuntu.com/ubuntu raring-backports main" | 42 | source = "http://archive.ubuntu.com/ubuntu raring-backports main" |
532 | 42 | fetch.add_source(source=source) | 43 | fetch.add_source(source=source) |
533 | 43 | check_call.assert_called_with(['add-apt-repository', | 44 | check_call.assert_called_with(['add-apt-repository', |
534 | 45 | '--yes', | ||
535 | 44 | source]) | 46 | source]) |
536 | 45 | 47 | ||
537 | 46 | @patch.object(fetch, 'filter_installed_packages') | 48 | @patch.object(fetch, 'filter_installed_packages') |
538 | @@ -72,7 +74,7 @@ | |||
539 | 72 | key = "akey" | 74 | key = "akey" |
540 | 73 | fetch.add_source(source=source, key=key) | 75 | fetch.add_source(source=source, key=key) |
541 | 74 | check_call.assert_has_calls([ | 76 | check_call.assert_has_calls([ |
543 | 75 | call(['add-apt-repository', source]), | 77 | call(['add-apt-repository', '--yes', source]), |
544 | 76 | call(['apt-key', 'import', key]) | 78 | call(['apt-key', 'import', key]) |
545 | 77 | ]) | 79 | ]) |
546 | 78 | 80 |
Hey, Michael,
This looks awesome, thanks! I can't wait to try it out.
Regarding juju_state_ to_yaml( ), I'd say it belongs in core.hookenv. Bonus points for including a larger chunk of execution_ environment( ).
-Matthew