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

Proposed by Michael Nelson
Status: Merged
Approved by: Michael Nelson
Approved revision: 100
Merged at revision: 98
Proposed branch: lp:~michael.nelson/charm-helpers/ansible-tags
Merge into: lp:charm-helpers
Diff against target: 627 lines (+332/-208)
7 files modified
charmhelpers/contrib/ansible/__init__.py (+66/-3)
charmhelpers/contrib/saltstack/__init__.py (+3/-50)
charmhelpers/contrib/templating/contexts.py (+64/-0)
setup.py (+1/-0)
tests/contrib/ansible/test_ansible.py (+30/-0)
tests/contrib/saltstack/test_saltstates.py (+1/-155)
tests/contrib/templating/test_contexts.py (+167/-0)
To merge this branch: bzr merge lp:~michael.nelson/charm-helpers/ansible-tags
Reviewer Review Type Date Requested Status
James Page Approve
Review via email: mp+194324@code.launchpad.net

Commit message

Add tags to the ansible support and provide AnsibleHooks() helper.

Description of the change

This branch:
 * adds the ability to specify tags when playing an ansible playbook
 * adds an AnsibleHooks helper which can be used instead of the hookenv.Hooks helper, which works just like the hookenv.Hooks but additionally plays the configured playbook using the hook name as a tag, like this:

{{{
# Create the hooks helper, passing a list of hooks which will be
# handled by default by running all the sections of the playbook
# tagged with the hook name.
hooks = charmhelpers.contrib.ansible.AnsibleHooks(
    playbook_path='playbooks/site.yaml',
    default_hooks=[
        'start',
        'stop',
        'config-changed',
        'solr-relation-changed',
    ])

# All the tasks within the specified playbook tagged with 'install'
# will be run automatically after the call to install_ansible_support.
@hooks.hook()
def install():
    charmhelpers.contrib.ansible.install_ansible_support(from_ppa=True)
}}}

minimizing the amount of procedural code in hooks.py to be tested and maintained.

 * then a big chunk is just moving the juju_state_to_yaml helper and its tests out of saltstack (recommendation from last MP, as it's now used by both the saltstack and ansible helpers).

See line 534 of the (private, sorry) MP diff here for an example of the ansible hooks being used [1]

[1] https://code.launchpad.net/~michael.nelson/canonical-is-charms/bootstrap-7d-utils/+merge/191808

To post a comment you must log in.
Revision history for this message
James Page (james-page) wrote :

LGTM

review: Approve

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 2013-08-21 09:10:58 +0000
3+++ charmhelpers/contrib/ansible/__init__.py 2013-11-07 14:38:44 +0000
4@@ -95,7 +95,70 @@
5 hosts_file.write('localhost ansible_connection=local')
6
7
8-def apply_playbook(playbook):
9- charmhelpers.contrib.saltstack.juju_state_to_yaml(
10+def apply_playbook(playbook, tags=None):
11+ tags = tags or []
12+ tags = ",".join(tags)
13+ charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
14 ansible_vars_path, namespace_separator='__')
15- subprocess.check_call(['ansible-playbook', '-c', 'local', playbook])
16+ call = [
17+ 'ansible-playbook',
18+ '-c',
19+ 'local',
20+ playbook,
21+ ]
22+ if tags:
23+ call.extend(['--tags', '{}'.format(tags)])
24+ subprocess.check_call(call)
25+
26+
27+class AnsibleHooks(charmhelpers.core.hookenv.Hooks):
28+ """Run a playbook with the hook-name as the tag.
29+
30+ This helper builds on the standard hookenv.Hooks helper,
31+ but additionally runs the playbook with the hook-name specified
32+ using --tags (ie. running all the tasks tagged with the hook-name).
33+
34+ Example:
35+ hooks = AnsibleHooks(playbook_path='playbooks/my_machine_state.yaml')
36+
37+ # All the tasks within my_machine_state.yaml tagged with 'install'
38+ # will be run automatically after do_custom_work()
39+ @hooks.hook()
40+ def install():
41+ do_custom_work()
42+
43+ # For most of your hooks, you won't need to do anything other
44+ # than run the tagged tasks for the hook:
45+ @hooks.hook('config-changed', 'start', 'stop')
46+ def just_use_playbook():
47+ pass
48+
49+ # As a convenience, you can avoid the above noop function by specifying
50+ # the hooks which are handled by ansible-only and they'll be registered
51+ # for you:
52+ # hooks = AnsibleHooks(
53+ # 'playbooks/my_machine_state.yaml',
54+ # default_hooks=['config-changed', 'start', 'stop'])
55+
56+ if __name__ == "__main__":
57+ # execute a hook based on the name the program is called by
58+ hooks.execute(sys.argv)
59+ """
60+
61+ def __init__(self, playbook_path, default_hooks=None):
62+ """Register any hooks handled by ansible."""
63+ super(AnsibleHooks, self).__init__()
64+
65+ self.playbook_path = playbook_path
66+
67+ default_hooks = default_hooks or []
68+ noop = lambda *args, **kwargs: None
69+ for hook in default_hooks:
70+ self.register(hook, noop)
71+
72+ def execute(self, args):
73+ """Execute the hook followed by the playbook using the hook as tag."""
74+ super(AnsibleHooks, self).execute(args)
75+ hook_name = os.path.basename(args[0])
76+ charmhelpers.contrib.ansible.apply_playbook(
77+ self.playbook_path, tags=[hook_name])
78
79=== modified file 'charmhelpers/contrib/saltstack/__init__.py'
80--- charmhelpers/contrib/saltstack/__init__.py 2013-08-07 23:14:14 +0000
81+++ charmhelpers/contrib/saltstack/__init__.py 2013-11-07 14:38:44 +0000
82@@ -61,15 +61,13 @@
83 #
84 # Authors:
85 # Charm Helpers Developers <juju@lists.ubuntu.com>
86-import os
87 import subprocess
88-import yaml
89
90+import charmhelpers.contrib.templating.contexts
91 import charmhelpers.core.host
92 import charmhelpers.core.hookenv
93
94
95-charm_dir = os.environ.get('CHARM_DIR', '')
96 salt_grains_path = '/etc/salt/grains'
97
98
99@@ -94,56 +92,11 @@
100
101 def update_machine_state(state_path):
102 """Update the machine state using the provided state declaration."""
103- juju_state_to_yaml(salt_grains_path)
104+ charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
105+ salt_grains_path)
106 subprocess.check_call([
107 'salt-call',
108 '--local',
109 'state.template',
110 state_path,
111 ])
112-
113-
114-def juju_state_to_yaml(yaml_path, namespace_separator=':'):
115- """Update the juju config and state in a yaml file.
116-
117- This includes any current relation-get data, and the charm
118- directory.
119- """
120- config = charmhelpers.core.hookenv.config()
121-
122- # Add the charm_dir which we will need to refer to charm
123- # file resources etc.
124- config['charm_dir'] = charm_dir
125- config['local_unit'] = charmhelpers.core.hookenv.local_unit()
126-
127- # Add any relation data prefixed with the relation type.
128- relation_type = charmhelpers.core.hookenv.relation_type()
129- if relation_type is not None:
130- relation_data = charmhelpers.core.hookenv.relation_get()
131- relation_data = dict(
132- ("{relation_type}{namespace_separator}{key}".format(
133- relation_type=relation_type.replace('-', '_'),
134- key=key,
135- namespace_separator=namespace_separator), val)
136- for key, val in relation_data.items())
137- config.update(relation_data)
138-
139- # Don't use non-standard tags for unicode which will not
140- # work when salt uses yaml.load_safe.
141- yaml.add_representer(unicode, lambda dumper,
142- value: dumper.represent_scalar(
143- u'tag:yaml.org,2002:str', value))
144-
145- yaml_dir = os.path.dirname(yaml_path)
146- if not os.path.exists(yaml_dir):
147- os.makedirs(yaml_dir)
148-
149- if os.path.exists(yaml_path):
150- with open(yaml_path, "r") as existing_vars_file:
151- existing_vars = yaml.load(existing_vars_file.read())
152- else:
153- existing_vars = {}
154-
155- existing_vars.update(config)
156- with open(yaml_path, "w+") as fp:
157- fp.write(yaml.dump(existing_vars))
158
159=== added file 'charmhelpers/contrib/templating/contexts.py'
160--- charmhelpers/contrib/templating/contexts.py 1970-01-01 00:00:00 +0000
161+++ charmhelpers/contrib/templating/contexts.py 2013-11-07 14:38:44 +0000
162@@ -0,0 +1,64 @@
163+# Copyright 2013 Canonical Ltd.
164+#
165+# Authors:
166+# Charm Helpers Developers <juju@lists.ubuntu.com>
167+"""A helper to create a yaml cache of config with namespaced relation data."""
168+import os
169+import yaml
170+
171+import charmhelpers.core.hookenv
172+
173+
174+charm_dir = os.environ.get('CHARM_DIR', '')
175+
176+
177+def juju_state_to_yaml(yaml_path, namespace_separator=':'):
178+ """Update the juju config and state in a yaml file.
179+
180+ This includes any current relation-get data, and the charm
181+ directory.
182+
183+ This function was created for the ansible and saltstack
184+ support, as those libraries can use a yaml file to supply
185+ context to templates, but it may be useful generally to
186+ create and update an on-disk cache of all the config, including
187+ previous relation data.
188+ """
189+ config = charmhelpers.core.hookenv.config()
190+
191+ # Add the charm_dir which we will need to refer to charm
192+ # file resources etc.
193+ config['charm_dir'] = charm_dir
194+ config['local_unit'] = charmhelpers.core.hookenv.local_unit()
195+
196+ # Add any relation data prefixed with the relation type.
197+ relation_type = charmhelpers.core.hookenv.relation_type()
198+ if relation_type is not None:
199+ relation_data = charmhelpers.core.hookenv.relation_get()
200+ relation_data = dict(
201+ ("{relation_type}{namespace_separator}{key}".format(
202+ relation_type=relation_type.replace('-', '_'),
203+ key=key,
204+ namespace_separator=namespace_separator), val)
205+ for key, val in relation_data.items())
206+ config.update(relation_data)
207+
208+ # Don't use non-standard tags for unicode which will not
209+ # work when salt uses yaml.load_safe.
210+ yaml.add_representer(unicode, lambda dumper,
211+ value: dumper.represent_scalar(
212+ u'tag:yaml.org,2002:str', value))
213+
214+ yaml_dir = os.path.dirname(yaml_path)
215+ if not os.path.exists(yaml_dir):
216+ os.makedirs(yaml_dir)
217+
218+ if os.path.exists(yaml_path):
219+ with open(yaml_path, "r") as existing_vars_file:
220+ existing_vars = yaml.load(existing_vars_file.read())
221+ else:
222+ existing_vars = {}
223+
224+ existing_vars.update(config)
225+ with open(yaml_path, "w+") as fp:
226+ fp.write(yaml.dump(existing_vars))
227
228=== modified file 'setup.py'
229--- setup.py 2013-08-01 01:40:14 +0000
230+++ setup.py 2013-11-07 14:38:44 +0000
231@@ -27,6 +27,7 @@
232 "charmhelpers.contrib.saltstack",
233 "charmhelpers.contrib.hahelpers",
234 "charmhelpers.contrib.jujugui",
235+ "charmhelpers.contrib.templating",
236 ],
237 'scripts': [
238 "bin/chlp",
239
240=== modified file 'tests/contrib/ansible/test_ansible.py'
241--- tests/contrib/ansible/test_ansible.py 2013-08-21 09:10:58 +0000
242+++ tests/contrib/ansible/test_ansible.py 2013-11-07 14:38:44 +0000
243@@ -132,3 +132,33 @@
244 "wsgi_file__relation_key1": "relation_value1",
245 "wsgi_file__relation_key2": "relation_value2",
246 }, result)
247+
248+ def test_calls_with_tags(self):
249+ charmhelpers.contrib.ansible.apply_playbook(
250+ 'playbooks/complete-state.yaml', tags=['install', 'somethingelse'])
251+
252+ self.mock_subprocess.check_call.assert_called_once_with([
253+ 'ansible-playbook', '-c', 'local', 'playbooks/complete-state.yaml',
254+ '--tags', 'install,somethingelse' ])
255+
256+ def test_hooks_executes_playbook_with_tag(self):
257+ hooks = charmhelpers.contrib.ansible.AnsibleHooks('my/playbook.yaml')
258+ foo = mock.MagicMock()
259+ hooks.register('foo', foo)
260+
261+ hooks.execute(['foo'])
262+
263+ self.assertEqual(foo.call_count, 1)
264+ self.mock_subprocess.check_call.assert_called_once_with([
265+ 'ansible-playbook', '-c', 'local', 'my/playbook.yaml',
266+ '--tags', 'foo' ])
267+
268+ def test_specifying_ansible_handled_hooks(self):
269+ hooks = charmhelpers.contrib.ansible.AnsibleHooks(
270+ 'my/playbook.yaml', default_hooks=['start', 'stop'])
271+
272+ hooks.execute(['start'])
273+
274+ self.mock_subprocess.check_call.assert_called_once_with([
275+ 'ansible-playbook', '-c', 'local', 'my/playbook.yaml',
276+ '--tags', 'start' ])
277
278=== modified file 'tests/contrib/saltstack/test_saltstates.py'
279--- tests/contrib/saltstack/test_saltstates.py 2013-08-21 09:10:58 +0000
280+++ tests/contrib/saltstack/test_saltstates.py 2013-11-07 14:38:44 +0000
281@@ -3,11 +3,7 @@
282 # Authors:
283 # Charm Helpers Developers <juju@lists.ubuntu.com>
284 import mock
285-import os
286-import shutil
287-import tempfile
288 import unittest
289-import yaml
290
291 import charmhelpers.contrib.saltstack
292
293@@ -56,7 +52,7 @@
294 self.mock_subprocess = patcher.start()
295 self.addCleanup(patcher.stop)
296
297- patcher = mock.patch('charmhelpers.contrib.saltstack.'
298+ patcher = mock.patch('charmhelpers.contrib.templating.contexts.'
299 'juju_state_to_yaml')
300 self.mock_config_2_grains = patcher.start()
301 self.addCleanup(patcher.stop)
302@@ -77,153 +73,3 @@
303 'states/install.yaml')
304
305 self.mock_config_2_grains.assert_called_once_with('/etc/salt/grains')
306-
307-
308-class JujuConfig2GrainsTestCase(unittest.TestCase):
309- def setUp(self):
310- super(JujuConfig2GrainsTestCase, self).setUp()
311-
312- # Hookenv patches (a single patch to hookenv doesn't work):
313- patcher = mock.patch('charmhelpers.core.hookenv.config')
314- self.mock_config = patcher.start()
315- self.addCleanup(patcher.stop)
316- patcher = mock.patch('charmhelpers.core.hookenv.relation_get')
317- self.mock_relation_get = patcher.start()
318- self.mock_relation_get.return_value = {}
319- self.addCleanup(patcher.stop)
320- patcher = mock.patch('charmhelpers.core.hookenv.relation_type')
321- self.mock_relation_type = patcher.start()
322- self.mock_relation_type.return_value = None
323- self.addCleanup(patcher.stop)
324- patcher = mock.patch('charmhelpers.core.hookenv.local_unit')
325- self.mock_local_unit = patcher.start()
326- self.addCleanup(patcher.stop)
327-
328- # patches specific to this test class.
329- etc_dir = tempfile.mkdtemp()
330- self.addCleanup(shutil.rmtree, etc_dir)
331- self.grain_path = os.path.join(etc_dir, 'salt', 'grains')
332-
333- patcher = mock.patch.object(charmhelpers.contrib.saltstack,
334- 'charm_dir', '/tmp/charm_dir')
335- patcher.start()
336- self.addCleanup(patcher.stop)
337-
338- def test_output_with_empty_relation(self):
339- self.mock_config.return_value = {
340- 'group_code_owner': 'webops_deploy',
341- 'user_code_runner': 'ubunet',
342- }
343- self.mock_local_unit.return_value = "click-index/3"
344-
345- charmhelpers.contrib.saltstack.juju_state_to_yaml(self.grain_path)
346-
347- with open(self.grain_path, 'r') as grain_file:
348- result = yaml.load(grain_file.read())
349- self.assertEqual({
350- "charm_dir": "/tmp/charm_dir",
351- "group_code_owner": "webops_deploy",
352- "user_code_runner": "ubunet",
353- "local_unit": "click-index/3",
354- }, result)
355-
356- def test_output_with_no_relation(self):
357- self.mock_config.return_value = {
358- 'group_code_owner': 'webops_deploy',
359- 'user_code_runner': 'ubunet',
360- }
361- self.mock_local_unit.return_value = "click-index/3"
362- self.mock_relation_get.return_value = None
363-
364- charmhelpers.contrib.saltstack.juju_state_to_yaml(self.grain_path)
365-
366- with open(self.grain_path, 'r') as grain_file:
367- result = yaml.load(grain_file.read())
368- self.assertEqual({
369- "charm_dir": "/tmp/charm_dir",
370- "group_code_owner": "webops_deploy",
371- "user_code_runner": "ubunet",
372- "local_unit": "click-index/3",
373- }, result)
374-
375- def test_output_with_relation(self):
376- self.mock_config.return_value = {
377- 'group_code_owner': 'webops_deploy',
378- 'user_code_runner': 'ubunet',
379- }
380- self.mock_relation_type.return_value = 'wsgi-file'
381- self.mock_relation_get.return_value = {
382- 'relation_key1': 'relation_value1',
383- 'relation_key2': 'relation_value2',
384- }
385- self.mock_local_unit.return_value = "click-index/3"
386-
387- charmhelpers.contrib.saltstack.juju_state_to_yaml(self.grain_path)
388-
389- with open(self.grain_path, 'r') as grain_file:
390- result = yaml.load(grain_file.read())
391- self.assertEqual({
392- "charm_dir": "/tmp/charm_dir",
393- "group_code_owner": "webops_deploy",
394- "user_code_runner": "ubunet",
395- "wsgi_file:relation_key1": "relation_value1",
396- "wsgi_file:relation_key2": "relation_value2",
397- "local_unit": "click-index/3",
398- }, result)
399-
400- def test_relation_with_separator(self):
401- self.mock_config.return_value = {
402- 'group_code_owner': 'webops_deploy',
403- 'user_code_runner': 'ubunet',
404- }
405- self.mock_relation_type.return_value = 'wsgi-file'
406- self.mock_relation_get.return_value = {
407- 'relation_key1': 'relation_value1',
408- 'relation_key2': 'relation_value2',
409- }
410- self.mock_local_unit.return_value = "click-index/3"
411-
412- charmhelpers.contrib.saltstack.juju_state_to_yaml(
413- self.grain_path, namespace_separator='__')
414-
415- with open(self.grain_path, 'r') as grain_file:
416- result = yaml.load(grain_file.read())
417- self.assertEqual({
418- "charm_dir": "/tmp/charm_dir",
419- "group_code_owner": "webops_deploy",
420- "user_code_runner": "ubunet",
421- "wsgi_file__relation_key1": "relation_value1",
422- "wsgi_file__relation_key2": "relation_value2",
423- "local_unit": "click-index/3",
424- }, result)
425-
426- def test_updates_existing_values(self):
427- """Data stored in grains is retained.
428-
429- This may be helpful so that templates can access information
430- from relations outside the current context.
431- """
432- os.makedirs(os.path.dirname(self.grain_path))
433- with open(self.grain_path, 'w+') as grain_file:
434- grain_file.write(yaml.dump({
435- 'solr:hostname': 'example.com',
436- 'user_code_runner': 'oldvalue',
437- }))
438-
439- self.mock_config.return_value = charmhelpers.core.hookenv.Serializable({
440- 'group_code_owner': 'webops_deploy',
441- 'user_code_runner': 'newvalue',
442- })
443- self.mock_local_unit.return_value = "click-index/3"
444-
445- charmhelpers.contrib.saltstack.juju_state_to_yaml(self.grain_path)
446-
447- with open(self.grain_path, 'r') as grain_file:
448- result = yaml.load(grain_file.read())
449- self.assertEqual({
450- "charm_dir": "/tmp/charm_dir",
451- "group_code_owner": "webops_deploy",
452- "user_code_runner": "newvalue",
453- "local_unit": "click-index/3",
454- "solr:hostname": "example.com",
455- }, result)
456
457=== added file 'tests/contrib/templating/test_contexts.py'
458--- tests/contrib/templating/test_contexts.py 1970-01-01 00:00:00 +0000
459+++ tests/contrib/templating/test_contexts.py 2013-11-07 14:38:44 +0000
460@@ -0,0 +1,167 @@
461+# Copyright 2013 Canonical Ltd.
462+#
463+# Authors:
464+# Charm Helpers Developers <juju@lists.ubuntu.com>
465+import mock
466+import os
467+import shutil
468+import tempfile
469+import unittest
470+import yaml
471+
472+import charmhelpers.contrib.templating
473+
474+
475+class JujuState2YamlTestCase(unittest.TestCase):
476+ def setUp(self):
477+ super(JujuState2YamlTestCase, self).setUp()
478+
479+ # Hookenv patches (a single patch to hookenv doesn't work):
480+ patcher = mock.patch('charmhelpers.core.hookenv.config')
481+ self.mock_config = patcher.start()
482+ self.addCleanup(patcher.stop)
483+ patcher = mock.patch('charmhelpers.core.hookenv.relation_get')
484+ self.mock_relation_get = patcher.start()
485+ self.mock_relation_get.return_value = {}
486+ self.addCleanup(patcher.stop)
487+ patcher = mock.patch('charmhelpers.core.hookenv.relation_type')
488+ self.mock_relation_type = patcher.start()
489+ self.mock_relation_type.return_value = None
490+ self.addCleanup(patcher.stop)
491+ patcher = mock.patch('charmhelpers.core.hookenv.local_unit')
492+ self.mock_local_unit = patcher.start()
493+ self.addCleanup(patcher.stop)
494+
495+ # patches specific to this test class.
496+ etc_dir = tempfile.mkdtemp()
497+ self.addCleanup(shutil.rmtree, etc_dir)
498+ self.context_path = os.path.join(etc_dir, 'some', 'context')
499+
500+ patcher = mock.patch.object(charmhelpers.contrib.templating.contexts,
501+ 'charm_dir', '/tmp/charm_dir')
502+ patcher.start()
503+ self.addCleanup(patcher.stop)
504+
505+ def test_output_with_empty_relation(self):
506+ self.mock_config.return_value = {
507+ 'group_code_owner': 'webops_deploy',
508+ 'user_code_runner': 'ubunet',
509+ }
510+ self.mock_local_unit.return_value = "click-index/3"
511+
512+ charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
513+ self.context_path)
514+
515+ with open(self.context_path, 'r') as context_file:
516+ result = yaml.load(context_file.read())
517+ self.assertEqual({
518+ "charm_dir": "/tmp/charm_dir",
519+ "group_code_owner": "webops_deploy",
520+ "user_code_runner": "ubunet",
521+ "local_unit": "click-index/3",
522+ }, result)
523+
524+ def test_output_with_no_relation(self):
525+ self.mock_config.return_value = {
526+ 'group_code_owner': 'webops_deploy',
527+ 'user_code_runner': 'ubunet',
528+ }
529+ self.mock_local_unit.return_value = "click-index/3"
530+ self.mock_relation_get.return_value = None
531+
532+ charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
533+ self.context_path)
534+
535+ with open(self.context_path, 'r') as context_file:
536+ result = yaml.load(context_file.read())
537+ self.assertEqual({
538+ "charm_dir": "/tmp/charm_dir",
539+ "group_code_owner": "webops_deploy",
540+ "user_code_runner": "ubunet",
541+ "local_unit": "click-index/3",
542+ }, result)
543+
544+ def test_output_with_relation(self):
545+ self.mock_config.return_value = {
546+ 'group_code_owner': 'webops_deploy',
547+ 'user_code_runner': 'ubunet',
548+ }
549+ self.mock_relation_type.return_value = 'wsgi-file'
550+ self.mock_relation_get.return_value = {
551+ 'relation_key1': 'relation_value1',
552+ 'relation_key2': 'relation_value2',
553+ }
554+ self.mock_local_unit.return_value = "click-index/3"
555+
556+ charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
557+ self.context_path)
558+
559+ with open(self.context_path, 'r') as context_file:
560+ result = yaml.load(context_file.read())
561+ self.assertEqual({
562+ "charm_dir": "/tmp/charm_dir",
563+ "group_code_owner": "webops_deploy",
564+ "user_code_runner": "ubunet",
565+ "wsgi_file:relation_key1": "relation_value1",
566+ "wsgi_file:relation_key2": "relation_value2",
567+ "local_unit": "click-index/3",
568+ }, result)
569+
570+ def test_relation_with_separator(self):
571+ self.mock_config.return_value = {
572+ 'group_code_owner': 'webops_deploy',
573+ 'user_code_runner': 'ubunet',
574+ }
575+ self.mock_relation_type.return_value = 'wsgi-file'
576+ self.mock_relation_get.return_value = {
577+ 'relation_key1': 'relation_value1',
578+ 'relation_key2': 'relation_value2',
579+ }
580+ self.mock_local_unit.return_value = "click-index/3"
581+
582+ charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
583+ self.context_path, namespace_separator='__')
584+
585+ with open(self.context_path, 'r') as context_file:
586+ result = yaml.load(context_file.read())
587+ self.assertEqual({
588+ "charm_dir": "/tmp/charm_dir",
589+ "group_code_owner": "webops_deploy",
590+ "user_code_runner": "ubunet",
591+ "wsgi_file__relation_key1": "relation_value1",
592+ "wsgi_file__relation_key2": "relation_value2",
593+ "local_unit": "click-index/3",
594+ }, result)
595+
596+ def test_updates_existing_values(self):
597+ """Data stored in the context file is retained.
598+
599+ This may be helpful so that templates can access information
600+ from relations outside the current execution environment.
601+ """
602+ os.makedirs(os.path.dirname(self.context_path))
603+ with open(self.context_path, 'w+') as context_file:
604+ context_file.write(yaml.dump({
605+ 'solr:hostname': 'example.com',
606+ 'user_code_runner': 'oldvalue',
607+ }))
608+
609+ self.mock_config.return_value = charmhelpers.core.hookenv.Serializable({
610+ 'group_code_owner': 'webops_deploy',
611+ 'user_code_runner': 'newvalue',
612+ })
613+ self.mock_local_unit.return_value = "click-index/3"
614+
615+ charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
616+ self.context_path)
617+
618+ with open(self.context_path, 'r') as context_file:
619+ result = yaml.load(context_file.read())
620+ self.assertEqual({
621+ "charm_dir": "/tmp/charm_dir",
622+ "group_code_owner": "webops_deploy",
623+ "user_code_runner": "newvalue",
624+ "local_unit": "click-index/3",
625+ "solr:hostname": "example.com",
626+ }, result)
627+

Subscribers

People subscribed via source and target branches