Merge lp:~michael.nelson/charm-helpers/add-declarative-support into lp:charm-helpers
- add-declarative-support
- Merge into devel
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 |
Related bugs: |
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:
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.
Matthew Wedgwood (mew) wrote : | # |
- 31. By Michael Nelson
-
Move declarative-
>saltstates.
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.
> "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.
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.
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.
> 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.
> 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:/
> 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.
>
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.
> > 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.
> 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:/
> > 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:/
> You are the owner of
> lp:~michael.nelson/charm-helpers/add-declarative-support.
>
Matthew Wedgwood (mew) wrote : | # |
> Great - thanks Matthew. I've ended up moving it to
> charmhelpers.
> 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
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/
Pushed with r32.
Preview Diff
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) |
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.