Merge lp:~michael.nelson/charm-helpers/add-declarative-support into lp:charm-helpers

Proposed by Michael Nelson
Status: Merged
Merged at revision: 28
Proposed branch: lp:~michael.nelson/charm-helpers/add-declarative-support
Merge into: lp:charm-helpers
Diff against target: 309 lines (+284/-0)
3 files modified
charmhelpers/contrib/saltstack/__init__.py (+124/-0)
setup.py (+2/-0)
tests/contrib/saltstates/test_saltstates.py (+158/-0)
To merge this branch: bzr merge lp:~michael.nelson/charm-helpers/add-declarative-support
Reviewer Review Type Date Requested Status
Matthew Wedgwood (community) Approve
Review via email: mp+168961@code.launchpad.net

Commit message

Add declarative machine state support.

Description of the change

This branch adds the ability to declare the machine state, rather than writing procedural code with fragile tests that need to be updated whenever the procedural code for machine state is updated.

See the docstring below for more details.

I'm using this on a private charm at the moment. If you've access, you can see how simple the hooks are at:

https://bazaar.launchpad.net/~michael.nelson/canonical-marshal/click-index-new-helpers/view/head:/hooks/hooks.py

The machine_states and templates directories might also be relevant to look at there. I'm testing that private charm using a deploy setup with an apache front end, cache lb, cache, the app lb + apps. If/when this branch lands, I'll create a bootstrap public charm + blog post showing how it can be used.

To post a comment you must log in.
Revision history for this message
Matthew Wedgwood (mew) wrote :

Per our discussion in IRC, this looks like a good candidate for charmhelpers.contrib.saltstack, rather than to be the more generally-named "declarative." This naming will make it clear which config format is in play and helps keep the namespace clean for future puppet, chef, etc. modules.

31. By Michael Nelson

Move declarative->saltstates.

Revision history for this message
Michael Nelson (michael.nelson) wrote :

On Thu, Jun 13, 2013 at 1:03 AM, Matthew Wedgwood <
<email address hidden>> wrote:

> Per our discussion in IRC, this looks like a good candidate for
> charmhelpers.contrib.saltstack, rather than to be the more generally-named
> "declarative." This naming will make it clear which config format is in
> play and helps keep the namespace clean for future puppet, chef, etc.
> modules.
>
>
Great - thanks Matthew. I've ended up moving it to
charmhelpers.contrib.saltstates so that it's clearer that we're just using
saltstacks support of local machine states, rather than the full saltstack
support (ie. master+slave). If you'd prefer saltstack, just let me know and
I'll switch it.

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

Looks good, although, be warned salt has close to zero testing. Ansible is
a nice alternative.

On Thu, Jun 13, 2013 at 5:28 AM, Michael Nelson <
<email address hidden>> wrote:

> On Thu, Jun 13, 2013 at 1:03 AM, Matthew Wedgwood <
> <email address hidden>> wrote:
>
> > Per our discussion in IRC, this looks like a good candidate for
> > charmhelpers.contrib.saltstack, rather than to be the more
> generally-named
> > "declarative." This naming will make it clear which config format is in
> > play and helps keep the namespace clean for future puppet, chef, etc.
> > modules.
> >
> >
> Great - thanks Matthew. I've ended up moving it to
> charmhelpers.contrib.saltstates so that it's clearer that we're just using
> saltstacks support of local machine states, rather than the full saltstack
> support (ie. master+slave). If you'd prefer saltstack, just let me know and
> I'll switch it.
>
> --
>
> https://code.launchpad.net/~michael.nelson/charm-helpers/add-declarative-support/+merge/168961
> Your team Charm Helper Maintainers is requested to review the proposed
> merge of lp:~michael.nelson/charm-helpers/add-declarative-support into
> lp:charm-helpers.
>

Revision history for this message
Michael Nelson (michael.nelson) wrote :

Thanks Kapil. Ansible does look nice too - if I get time I'll look at
adding helpers for it to contrib too.

On Thu, Jun 13, 2013 at 2:55 PM, Kapil Thangavelu <
<email address hidden>> wrote:

> Looks good, although, be warned salt has close to zero testing. Ansible is
> a nice alternative.
>
>
> On Thu, Jun 13, 2013 at 5:28 AM, Michael Nelson <
> <email address hidden>> wrote:
>
> > On Thu, Jun 13, 2013 at 1:03 AM, Matthew Wedgwood <
> > <email address hidden>> wrote:
> >
> > > Per our discussion in IRC, this looks like a good candidate for
> > > charmhelpers.contrib.saltstack, rather than to be the more
> > generally-named
> > > "declarative." This naming will make it clear which config format is in
> > > play and helps keep the namespace clean for future puppet, chef, etc.
> > > modules.
> > >
> > >
> > Great - thanks Matthew. I've ended up moving it to
> > charmhelpers.contrib.saltstates so that it's clearer that we're just
> using
> > saltstacks support of local machine states, rather than the full
> saltstack
> > support (ie. master+slave). If you'd prefer saltstack, just let me know
> and
> > I'll switch it.
> >
> > --
> >
> >
> https://code.launchpad.net/~michael.nelson/charm-helpers/add-declarative-support/+merge/168961
> > Your team Charm Helper Maintainers is requested to review the proposed
> > merge of lp:~michael.nelson/charm-helpers/add-declarative-support into
> > lp:charm-helpers.
> >
>
> --
>
> https://code.launchpad.net/~michael.nelson/charm-helpers/add-declarative-support/+merge/168961
> You are the owner of
> lp:~michael.nelson/charm-helpers/add-declarative-support.
>

Revision history for this message
Matthew Wedgwood (mew) wrote :

> Great - thanks Matthew. I've ended up moving it to
> charmhelpers.contrib.saltstates so that it's clearer that we're just using
> saltstacks support of local machine states, rather than the full saltstack
> support (ie. master+slave). If you'd prefer saltstack, just let me know and
> I'll switch it.

I think it's fine calling this saltstack. A lot of Puppet sites run masterless, so I wouldn't say that the lack of master/slave support makes this less "full." If someone were to add additional support for saltstack, it should go in this module.

Thanks again! Sorry to be so picky.

32. By Michael Nelson

s/saltstates/saltstack

Revision history for this message
Michael Nelson (michael.nelson) wrote :

On Thu, Jun 13, 2013 at 6:06 PM, Matthew Wedgwood <
<email address hidden>> wrote:

> I think it's fine calling this saltstack. A lot of Puppet sites run
> masterless, so I wouldn't say that the lack of master/slave support makes
> this less "full." If someone were to add additional support for saltstack,
> it should go in this module.
>
> Thanks again! Sorry to be so picky.
>

No problem - that makes sense and it's an easy s/saltstates/saltstack :)
Pushed with r32.

Revision history for this message
Matthew Wedgwood (mew) wrote :

Perfect. +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'charmhelpers/contrib/saltstack'
2=== added file 'charmhelpers/contrib/saltstack/__init__.py'
3--- charmhelpers/contrib/saltstack/__init__.py 1970-01-01 00:00:00 +0000
4+++ charmhelpers/contrib/saltstack/__init__.py 2013-06-14 07:11:26 +0000
5@@ -0,0 +1,124 @@
6+"""Charm Helpers saltstack - declare the state of your machines.
7+
8+This helper enables you to declare your machine state, rather than
9+program it procedurally (and have to test each change to your procedures).
10+Your install hook can be as simple as:
11+
12+{{{
13+from charmhelpers.contrib.saltstack import (
14+ install_salt_support,
15+ update_machine_state,
16+)
17+
18+
19+def install():
20+ install_salt_support()
21+ update_machine_state('machine_states/dependencies.yaml')
22+ update_machine_state('machine_states/installed.yaml')
23+}}}
24+
25+and won't need to change (nor will its tests) when you change the machine
26+state.
27+
28+It's using a python package called salt-minion which allows various formats for
29+specifying resources, such as:
30+
31+{{{
32+/srv/{{ basedir }}:
33+ file.directory:
34+ - group: ubunet
35+ - user: ubunet
36+ - require:
37+ - user: ubunet
38+ - recurse:
39+ - user
40+ - group
41+
42+ubunet:
43+ group.present:
44+ - gid: 1500
45+ user.present:
46+ - uid: 1500
47+ - gid: 1500
48+ - createhome: False
49+ - require:
50+ - group: ubunet
51+}}}
52+
53+The docs for all the different state definitions are at:
54+ http://docs.saltstack.com/ref/states/all/
55+
56+
57+TODO:
58+ * Add test helpers which will ensure that machine state definitions
59+ are functionally (but not necessarily logically) correct (ie. getting
60+ salt to parse all state defs.
61+ * Add a link to a public bootstrap charm example / blogpost.
62+ * Find a way to obviate the need to use the grains['charm_dir'] syntax
63+ in templates.
64+"""
65+# Copyright 2013 Canonical Ltd.
66+#
67+# Authors:
68+# Charm Helpers Developers <juju@lists.ubuntu.com>
69+import os
70+import subprocess
71+import yaml
72+
73+import charmhelpers.core.host
74+import charmhelpers.core.hookenv
75+
76+
77+charm_dir = os.environ.get('CHARM_DIR', '')
78+salt_grains_path = '/etc/salt/grains'
79+
80+
81+def install_salt_support(from_ppa=True):
82+ """Installs the salt-minion helper for machine state.
83+
84+ By default the salt-minion package is installed from
85+ the saltstack PPA. If from_ppa is False you must ensure
86+ that the salt-minion package is available in the apt cache.
87+ """
88+ if from_ppa:
89+ subprocess.check_call([
90+ '/usr/bin/add-apt-repository',
91+ '--yes',
92+ 'ppa:saltstack/salt',
93+ ])
94+ subprocess.check_call(['/usr/bin/apt-get', 'update'])
95+ charmhelpers.core.host.apt_install('salt-minion')
96+
97+
98+def update_machine_state(state_path):
99+ """Update the machine state using the provided state declaration."""
100+ juju_config_2_grains()
101+ subprocess.check_call([
102+ 'salt-call',
103+ '--local',
104+ 'state.template',
105+ state_path,
106+ ])
107+
108+
109+def juju_config_2_grains():
110+ """Insert the juju config as salt grains for use in state templates.
111+
112+ This includes any current relation-get data, and the charm
113+ directory.
114+ """
115+ config = charmhelpers.core.hookenv.config()
116+
117+ # Add the charm_dir which we will need to refer to charm
118+ # file resources etc.
119+ config['charm_dir'] = charm_dir
120+ config['local_unit'] = charmhelpers.core.hookenv.local_unit()
121+ config.update(charmhelpers.core.hookenv.relation_get())
122+
123+ # Don't use non-standard tags for unicode which will not
124+ # work when salt uses yaml.load_safe.
125+ yaml.add_representer(unicode, lambda dumper,
126+ value: dumper.represent_scalar(
127+ u'tag:yaml.org,2002:str', value))
128+ with open(salt_grains_path, "w+") as fp:
129+ fp.write(config.yaml())
130
131=== modified file 'setup.py'
132--- setup.py 2013-05-31 10:34:33 +0000
133+++ setup.py 2013-06-14 07:11:26 +0000
134@@ -19,8 +19,10 @@
135 "charmhelpers.core",
136 "charmhelpers.fetch",
137 "charmhelpers.payload",
138+ "charmhelpers.contrib",
139 "charmhelpers.contrib.charmhelpers",
140 "charmhelpers.contrib.charmsupport",
141+ "charmhelpers.contrib.saltstack",
142 "charmhelpers.contrib.hahelpers",
143 "charmhelpers.contrib.jujugui",
144 ],
145
146=== added directory 'tests/contrib/saltstates'
147=== added file 'tests/contrib/saltstates/__init__.py'
148=== added file 'tests/contrib/saltstates/test_saltstates.py'
149--- tests/contrib/saltstates/test_saltstates.py 1970-01-01 00:00:00 +0000
150+++ tests/contrib/saltstates/test_saltstates.py 2013-06-14 07:11:26 +0000
151@@ -0,0 +1,158 @@
152+# Copyright 2013 Canonical Ltd.
153+#
154+# Authors:
155+# Charm Helpers Developers <juju@lists.ubuntu.com>
156+import json
157+import mock
158+import tempfile
159+import unittest
160+import yaml
161+
162+import charmhelpers.core.hookenv
163+import charmhelpers.contrib.saltstack
164+
165+
166+class InstallSaltSupportTestCase(unittest.TestCase):
167+
168+ def setUp(self):
169+ super(InstallSaltSupportTestCase, self).setUp()
170+
171+ patcher = mock.patch('charmhelpers.contrib.saltstack.subprocess')
172+ self.mock_subprocess = patcher.start()
173+ self.addCleanup(patcher.stop)
174+
175+ patcher = mock.patch('charmhelpers.core')
176+ self.mock_charmhelpers_core = patcher.start()
177+ self.addCleanup(patcher.stop)
178+
179+ def test_adds_ppa_by_default(self):
180+ charmhelpers.contrib.saltstack.install_salt_support()
181+
182+ self.assertEqual(self.mock_subprocess.check_call.call_count, 2)
183+ self.assertEqual([(([
184+ '/usr/bin/add-apt-repository',
185+ '--yes',
186+ 'ppa:saltstack/salt',
187+ ],), {}),
188+ (([
189+ '/usr/bin/apt-get',
190+ 'update',
191+ ],), {})
192+ ], self.mock_subprocess.check_call.call_args_list)
193+ self.mock_charmhelpers_core.host.apt_install.assert_called_once_with(
194+ 'salt-minion')
195+
196+ def test_no_ppa(self):
197+ charmhelpers.contrib.saltstack.install_salt_support(
198+ from_ppa=False)
199+
200+ self.assertEqual(self.mock_subprocess.check_call.call_count, 0)
201+ self.mock_charmhelpers_core.host.apt_install.assert_called_once_with(
202+ 'salt-minion')
203+
204+
205+class UpdateMachineStateTestCase(unittest.TestCase):
206+
207+ def setUp(self):
208+ super(UpdateMachineStateTestCase, self).setUp()
209+
210+ patcher = mock.patch('charmhelpers.contrib.saltstack.subprocess')
211+ self.mock_subprocess = patcher.start()
212+ self.addCleanup(patcher.stop)
213+
214+ patcher = mock.patch('charmhelpers.contrib.saltstack.'
215+ 'juju_config_2_grains')
216+ self.mock_config_2_grains = patcher.start()
217+ self.addCleanup(patcher.stop)
218+
219+ def test_calls_local_salt_template(self):
220+ charmhelpers.contrib.saltstack.update_machine_state(
221+ 'states/install.yaml')
222+
223+ self.mock_subprocess.check_call.assert_called_once_with([
224+ 'salt-call',
225+ '--local',
226+ 'state.template',
227+ 'states/install.yaml',
228+ ])
229+
230+ def test_updates_grains(self):
231+ charmhelpers.contrib.saltstack.update_machine_state(
232+ 'states/install.yaml')
233+
234+ self.mock_config_2_grains.assert_called_once_with()
235+
236+
237+class JujuConfig2GrainsTestCase(unittest.TestCase):
238+ def setUp(self):
239+ super(JujuConfig2GrainsTestCase, self).setUp()
240+
241+ # Hookenv patches (a single patch to hookenv doesn't work):
242+ patcher = mock.patch('charmhelpers.core.hookenv.config')
243+ self.mock_config = patcher.start()
244+ self.addCleanup(patcher.stop)
245+ patcher = mock.patch('charmhelpers.core.hookenv.relation_get')
246+ self.mock_relation_get = patcher.start()
247+ self.mock_relation_get.return_value = {}
248+ self.addCleanup(patcher.stop)
249+ patcher = mock.patch('charmhelpers.core.hookenv.local_unit')
250+ self.mock_local_unit = patcher.start()
251+ self.addCleanup(patcher.stop)
252+
253+ # patches specific to this test class.
254+ grain_file = tempfile.NamedTemporaryFile()
255+ self.grain_path = grain_file.name
256+ self.addCleanup(grain_file.close)
257+
258+ patcher = mock.patch.object(charmhelpers.contrib.saltstack,
259+ 'salt_grains_path', self.grain_path)
260+ patcher.start()
261+ self.addCleanup(patcher.stop)
262+
263+ patcher = mock.patch.object(charmhelpers.contrib.saltstack,
264+ 'charm_dir', '/tmp/charm_dir')
265+ patcher.start()
266+ self.addCleanup(patcher.stop)
267+
268+
269+ def test_output_without_relation(self):
270+ self.mock_config.return_value = charmhelpers.core.hookenv.Serializable({
271+ 'group_code_owner': 'webops_deploy',
272+ 'user_code_runner': 'ubunet',
273+ })
274+ self.mock_local_unit.return_value = "click-index/3"
275+
276+ charmhelpers.contrib.saltstack.juju_config_2_grains()
277+
278+ with open(self.grain_path, 'r') as grain_file:
279+ result = yaml.load(grain_file.read())
280+ self.assertEqual({
281+ "charm_dir": "/tmp/charm_dir",
282+ "group_code_owner": "webops_deploy",
283+ "user_code_runner": "ubunet",
284+ "local_unit": "click-index/3",
285+ }, result)
286+
287+ def test_output_with_relation(self):
288+ self.mock_config.return_value = charmhelpers.core.hookenv.Serializable({
289+ 'group_code_owner': 'webops_deploy',
290+ 'user_code_runner': 'ubunet',
291+ })
292+ self.mock_relation_get.return_value = {
293+ 'relation_key1': 'relation_value1',
294+ 'relation_key2': 'relation_value2',
295+ }
296+ self.mock_local_unit.return_value = "click-index/3"
297+
298+ charmhelpers.contrib.saltstack.juju_config_2_grains()
299+
300+ with open(self.grain_path, 'r') as grain_file:
301+ result = yaml.load(grain_file.read())
302+ self.assertEqual({
303+ "charm_dir": "/tmp/charm_dir",
304+ "group_code_owner": "webops_deploy",
305+ "user_code_runner": "ubunet",
306+ "relation_key1": "relation_value1",
307+ "relation_key2": "relation_value2",
308+ "local_unit": "click-index/3",
309+ }, result)

Subscribers

People subscribed via source and target branches