Merge lp:~michael.nelson/charm-helpers/ansible-detect-hooks into lp:charm-helpers

Proposed by Michael Nelson
Status: Work in progress
Proposed branch: lp:~michael.nelson/charm-helpers/ansible-detect-hooks
Merge into: lp:charm-helpers
Diff against target: 308 lines (+167/-18)
2 files modified
charmhelpers/contrib/ansible/__init__.py (+68/-15)
tests/contrib/ansible/test_ansible.py (+99/-3)
To merge this branch: bzr merge lp:~michael.nelson/charm-helpers/ansible-detect-hooks
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+216142@code.launchpad.net

Commit message

Detect whether a hook has any corresponding tasks tagged in the playbook before attempting to run them.

Description of the change

Also then makes it possible to do:
https://code.launchpad.net/~michael.nelson/charm-helpers/create-hook-symlinks/+merge/216143

(Edit: actually, it doesn't yet make that possible)

To post a comment you must log in.
146. By Michael Nelson

Merge latest trunk.

Unmerged revisions

146. By Michael Nelson

Merge latest trunk.

145. By Michael Nelson

Deprecation warning.

144. By Michael Nelson

Hook-up with get_tags_for_playbook.

143. By Michael Nelson

Add get_tags_for_playbook.

142. By Michael Nelson

Add available tags to module - need to populate during install.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmhelpers/contrib/ansible/__init__.py'
2--- charmhelpers/contrib/ansible/__init__.py 2014-05-28 12:48:23 +0000
3+++ charmhelpers/contrib/ansible/__init__.py 2014-06-10 10:22:01 +0000
4@@ -11,34 +11,33 @@
5 {{{
6 import charmhelpers.contrib.ansible
7
8+hooks = charmhelpers.contrib.ansible.AnsibleHooks(
9+ playbook_path="playbook.yaml")
10
11+@hooks.hook('install', 'upgrade-charm')
12 def install():
13 charmhelpers.contrib.ansible.install_ansible_support()
14- charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
15 }}}
16
17 and won't need to change (nor will its tests) when you change the machine
18 state.
19
20 All of your juju config and relation-data are available as template
21-variables within your playbooks and templates. An install playbook looks
22+variables within your playbooks and templates.
23+
24+An install playbook looks
25 something like:
26
27 {{{
28 ---
29 - hosts: localhost
30- user: root
31
32 tasks:
33- - name: Add private repositories.
34- template:
35- src: ../templates/private-repositories.list.jinja2
36- dest: /etc/apt/sources.list.d/private.list
37-
38- - name: Update the cache.
39- apt: update_cache=yes
40
41 - name: Install dependencies.
42+ tags:
43+ - install
44+ - config-changed
45 apt: pkg={{ item }}
46 with_items:
47 - python-mimeparse
48@@ -46,6 +45,8 @@
49 - sunburnt
50
51 - name: Setup groups.
52+ - install
53+ - config-changed
54 group: name={{ item.name }} gid={{ item.gid }}
55 with_items:
56 - { name: 'deploy_user', gid: 1800 }
57@@ -54,6 +55,12 @@
58 ...
59 }}}
60
61+
62+Alternatively, you can apply individual playbooks with:
63+{{{
64+charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
65+}}}
66+
67 Read more online about playbooks[1] and standard ansible modules[2].
68
69 [1] http://www.ansibleworks.com/docs/playbooks.html
70@@ -61,6 +68,7 @@
71 """
72 import os
73 import subprocess
74+import warnings
75
76 import charmhelpers.contrib.templating.contexts
77 import charmhelpers.core.host
78@@ -73,6 +81,7 @@
79 # Ansible will automatically include any vars in the following
80 # file in its inventory when run locally.
81 ansible_vars_path = '/etc/ansible/host_vars/localhost'
82+available_tags = set([])
83
84
85 def install_ansible_support(from_ppa=True, ppa_location='ppa:rquillo/ansible'):
86@@ -112,6 +121,33 @@
87 subprocess.check_call(call)
88
89
90+def get_tags_for_playbook(playbook_path):
91+ """Return all tags within a playbook.
92+
93+ The charmhelpers lib should not depend on ansible, hence the
94+ inline imports here.
95+
96+ Discussion whether --list-tags should be a feature of ansible at
97+ http://goo.gl/6gXd50
98+ """
99+ import ansible.utils
100+ import ansible.callbacks
101+ import ansible.playbook
102+ stats = ansible.callbacks.AggregateStats()
103+ callbacks = ansible.callbacks.PlaybookRunnerCallbacks(stats)
104+ runner_callbacks = ansible.callbacks.PlaybookRunnerCallbacks(stats)
105+ playbook = ansible.playbook.PlayBook(playbook=playbook_path,
106+ callbacks=callbacks,
107+ runner_callbacks=runner_callbacks,
108+ stats=stats)
109+ myplay = ansible.playbook.Play(playbook, ds=playbook.playbook[0],
110+ basedir=os.path.dirname(playbook_path))
111+
112+ _, playbook_tags = myplay.compare_tags([])
113+ playbook_tags.remove('all')
114+ return playbook_tags
115+
116+
117 class AnsibleHooks(charmhelpers.core.hookenv.Hooks):
118 """Run a playbook with the hook-name as the tag.
119
120@@ -147,19 +183,36 @@
121 """
122
123 def __init__(self, playbook_path, default_hooks=None):
124- """Register any hooks handled by ansible."""
125+ """Register any hooks handled by ansible.
126+
127+ default_hooks is now deprecated, as we use ansible to
128+ determine the supported hooks from the playbook.
129+ """
130 super(AnsibleHooks, self).__init__()
131
132 self.playbook_path = playbook_path
133
134- default_hooks = default_hooks or []
135+ # The hooks decorator is created at module load time, which on the
136+ # first run, will be before ansible is itself installed.
137+ try:
138+ available_tags.update(get_tags_for_playbook(playbook_path))
139+ except ImportError:
140+ available_tags.add('install')
141+
142+ if default_hooks is not None:
143+ warnings.warn(
144+ "The use of default_hooks is deprecated. Ansible is now "
145+ "used to query your playbook for available tags.",
146+ DeprecationWarning)
147 noop = lambda *args, **kwargs: None
148- for hook in default_hooks:
149+ for hook in available_tags:
150 self.register(hook, noop)
151
152 def execute(self, args):
153 """Execute the hook followed by the playbook using the hook as tag."""
154 super(AnsibleHooks, self).execute(args)
155 hook_name = os.path.basename(args[0])
156- charmhelpers.contrib.ansible.apply_playbook(
157- self.playbook_path, tags=[hook_name])
158+
159+ if hook_name in available_tags:
160+ charmhelpers.contrib.ansible.apply_playbook(
161+ self.playbook_path, tags=[hook_name])
162
163=== modified file 'tests/contrib/ansible/test_ansible.py'
164--- tests/contrib/ansible/test_ansible.py 2014-04-08 14:53:56 +0000
165+++ tests/contrib/ansible/test_ansible.py 2014-06-10 10:22:01 +0000
166@@ -5,8 +5,10 @@
167 import mock
168 import os
169 import shutil
170+import sys
171 import tempfile
172 import unittest
173+import warnings
174 import yaml
175
176
177@@ -125,6 +127,12 @@
178 patcher.start()
179 self.addCleanup(patcher.stop)
180
181+ patcher = mock.patch(
182+ 'charmhelpers.contrib.ansible.get_tags_for_playbook')
183+ self.mock_get_tags_for_playbook = patcher.start()
184+ self.addCleanup(patcher.stop)
185+ self.mock_get_tags_for_playbook.return_value = []
186+
187 def test_calls_ansible_playbook(self):
188 charmhelpers.contrib.ansible.apply_playbook(
189 'playbooks/dependencies.yaml')
190@@ -134,7 +142,8 @@
191
192 def test_writes_vars_file(self):
193 self.assertFalse(os.path.exists(self.vars_path))
194- self.mock_config.return_value = charmhelpers.core.hookenv.Serializable({
195+ Serializable = charmhelpers.core.hookenv.Serializable
196+ self.mock_config.return_value = Serializable({
197 'group_code_owner': 'webops_deploy',
198 'user_code_runner': 'ubunet',
199 'private-address': '10.10.10.10',
200@@ -182,6 +191,7 @@
201 '--tags', 'install,somethingelse'])
202
203 def test_hooks_executes_playbook_with_tag(self):
204+ self.mock_get_tags_for_playbook.return_value = ['foo']
205 hooks = charmhelpers.contrib.ansible.AnsibleHooks('my/playbook.yaml')
206 foo = mock.MagicMock()
207 hooks.register('foo', foo)
208@@ -193,12 +203,98 @@
209 'ansible-playbook', '-c', 'local', 'my/playbook.yaml',
210 '--tags', 'foo'])
211
212+ def test_hooks_doesnt_execute_nonexistant_tag(self):
213+ hooks = charmhelpers.contrib.ansible.AnsibleHooks('my/playbook.yaml')
214+ foo = mock.MagicMock()
215+ hooks.register('foo', foo)
216+
217+ hooks.execute(['foo'])
218+
219+ self.assertEqual(foo.call_count, 1)
220+ self.assertEqual(self.mock_subprocess.check_call.call_count, 0)
221+
222 def test_specifying_ansible_handled_hooks(self):
223- hooks = charmhelpers.contrib.ansible.AnsibleHooks(
224- 'my/playbook.yaml', default_hooks=['start', 'stop'])
225+ self.mock_get_tags_for_playbook.return_value = ['start', 'stop']
226+ hooks = charmhelpers.contrib.ansible.AnsibleHooks('my/playbook.yaml')
227
228 hooks.execute(['start'])
229
230 self.mock_subprocess.check_call.assert_called_once_with([
231 'ansible-playbook', '-c', 'local', 'my/playbook.yaml',
232 '--tags', 'start'])
233+
234+ def test_install_hook(self):
235+ """Install hook is run even though ansible wasn't available
236+ when trying to query for available tags."""
237+ self.mock_get_tags_for_playbook.side_effect = ImportError
238+ hooks = charmhelpers.contrib.ansible.AnsibleHooks('my/playbook.yaml')
239+
240+ hooks.execute(['install'])
241+
242+ self.mock_subprocess.check_call.assert_called_once_with([
243+ 'ansible-playbook', '-c', 'local', 'my/playbook.yaml',
244+ '--tags', 'install'])
245+
246+ def test_register_overrides_existing_hook(self):
247+ self.mock_get_tags_for_playbook.side_effect = ImportError
248+ hooks = charmhelpers.contrib.ansible.AnsibleHooks('my/playbook.yaml')
249+ install_hook = mock.Mock()
250+ hooks.register('install', install_hook)
251+
252+ hooks.execute(['install'])
253+
254+ self.assertEqual(install_hook.call_count, 1)
255+ self.mock_subprocess.check_call.assert_called_once_with([
256+ 'ansible-playbook', '-c', 'local', 'my/playbook.yaml',
257+ '--tags', 'install'])
258+
259+ def test_using_default_hooks_raises_deprecation_warning(self):
260+ with warnings.catch_warnings(record=True) as warns:
261+ warnings.simplefilter("always")
262+ hooks = charmhelpers.contrib.ansible.AnsibleHooks(
263+ 'my/playbook.yaml', default_hooks=['foo'])
264+
265+ self.assertEqual(len(warns), 1)
266+ warning = warns[0]
267+ self.assertEqual(warning.category, DeprecationWarning)
268+ self.assertIn("default_hooks is deprecated",
269+ unicode(warning.message))
270+
271+
272+class GetTagsForPlaybookTestCases(unittest.TestCase):
273+ """Verify that get_tags_for_playbook follows the current ansible contract.
274+
275+ This only verifies that the code works with the ansible api
276+ at the time of writing. Use of the function with ansible installed
277+ is required for real testing, but charmhelpers shouldn't require
278+ ansible.
279+
280+ We could alternately add ansible to test_requirements.txt, but it doesn't
281+ seem to be used, so would just cause others pain.
282+ """
283+
284+ def setUp(self):
285+ super(GetTagsForPlaybookTestCases, self).setUp()
286+ self.mock_ansible = mock.Mock()
287+ self.mocked_modules = {
288+ 'ansible': self.mock_ansible,
289+ 'ansible.utils': mock.Mock(),
290+ 'ansible.callbacks': mock.Mock(),
291+ 'ansible.playbook': mock.Mock(),
292+ }
293+ sys.modules.update(self.mocked_modules)
294+
295+ self.mock_ansible.playbook.PlayBook.return_value.playbook = [1]
296+ mock_Play = self.mock_ansible.playbook.Play
297+ compare_tags = mock_Play.return_value.compare_tags
298+ compare_tags.return_value = ([], ['all', 'install', 'config-changed'])
299+
300+ def tearDown(self):
301+ for key in self.mocked_modules:
302+ del(sys.modules[key])
303+
304+ def test_get_tags(self):
305+ tags = charmhelpers.contrib.ansible.get_tags_for_playbook(
306+ 'my/playbook.yaml')
307+
308+ self.assertEqual(['install', 'config-changed'], tags)

Subscribers

People subscribed via source and target branches