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
=== added directory 'charmhelpers/contrib/saltstack'
=== added file 'charmhelpers/contrib/saltstack/__init__.py'
--- charmhelpers/contrib/saltstack/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/saltstack/__init__.py 2013-06-14 07:11:26 +0000
@@ -0,0 +1,124 @@
1"""Charm Helpers saltstack - declare the state of your machines.
2
3This helper enables you to declare your machine state, rather than
4program it procedurally (and have to test each change to your procedures).
5Your install hook can be as simple as:
6
7{{{
8from charmhelpers.contrib.saltstack import (
9 install_salt_support,
10 update_machine_state,
11)
12
13
14def install():
15 install_salt_support()
16 update_machine_state('machine_states/dependencies.yaml')
17 update_machine_state('machine_states/installed.yaml')
18}}}
19
20and won't need to change (nor will its tests) when you change the machine
21state.
22
23It's using a python package called salt-minion which allows various formats for
24specifying resources, such as:
25
26{{{
27/srv/{{ basedir }}:
28 file.directory:
29 - group: ubunet
30 - user: ubunet
31 - require:
32 - user: ubunet
33 - recurse:
34 - user
35 - group
36
37ubunet:
38 group.present:
39 - gid: 1500
40 user.present:
41 - uid: 1500
42 - gid: 1500
43 - createhome: False
44 - require:
45 - group: ubunet
46}}}
47
48The docs for all the different state definitions are at:
49 http://docs.saltstack.com/ref/states/all/
50
51
52TODO:
53 * Add test helpers which will ensure that machine state definitions
54 are functionally (but not necessarily logically) correct (ie. getting
55 salt to parse all state defs.
56 * Add a link to a public bootstrap charm example / blogpost.
57 * Find a way to obviate the need to use the grains['charm_dir'] syntax
58 in templates.
59"""
60# Copyright 2013 Canonical Ltd.
61#
62# Authors:
63# Charm Helpers Developers <juju@lists.ubuntu.com>
64import os
65import subprocess
66import yaml
67
68import charmhelpers.core.host
69import charmhelpers.core.hookenv
70
71
72charm_dir = os.environ.get('CHARM_DIR', '')
73salt_grains_path = '/etc/salt/grains'
74
75
76def install_salt_support(from_ppa=True):
77 """Installs the salt-minion helper for machine state.
78
79 By default the salt-minion package is installed from
80 the saltstack PPA. If from_ppa is False you must ensure
81 that the salt-minion package is available in the apt cache.
82 """
83 if from_ppa:
84 subprocess.check_call([
85 '/usr/bin/add-apt-repository',
86 '--yes',
87 'ppa:saltstack/salt',
88 ])
89 subprocess.check_call(['/usr/bin/apt-get', 'update'])
90 charmhelpers.core.host.apt_install('salt-minion')
91
92
93def update_machine_state(state_path):
94 """Update the machine state using the provided state declaration."""
95 juju_config_2_grains()
96 subprocess.check_call([
97 'salt-call',
98 '--local',
99 'state.template',
100 state_path,
101 ])
102
103
104def juju_config_2_grains():
105 """Insert the juju config as salt grains for use in state templates.
106
107 This includes any current relation-get data, and the charm
108 directory.
109 """
110 config = charmhelpers.core.hookenv.config()
111
112 # Add the charm_dir which we will need to refer to charm
113 # file resources etc.
114 config['charm_dir'] = charm_dir
115 config['local_unit'] = charmhelpers.core.hookenv.local_unit()
116 config.update(charmhelpers.core.hookenv.relation_get())
117
118 # Don't use non-standard tags for unicode which will not
119 # work when salt uses yaml.load_safe.
120 yaml.add_representer(unicode, lambda dumper,
121 value: dumper.represent_scalar(
122 u'tag:yaml.org,2002:str', value))
123 with open(salt_grains_path, "w+") as fp:
124 fp.write(config.yaml())
0125
=== modified file 'setup.py'
--- setup.py 2013-05-31 10:34:33 +0000
+++ setup.py 2013-06-14 07:11:26 +0000
@@ -19,8 +19,10 @@
19 "charmhelpers.core",19 "charmhelpers.core",
20 "charmhelpers.fetch",20 "charmhelpers.fetch",
21 "charmhelpers.payload",21 "charmhelpers.payload",
22 "charmhelpers.contrib",
22 "charmhelpers.contrib.charmhelpers",23 "charmhelpers.contrib.charmhelpers",
23 "charmhelpers.contrib.charmsupport",24 "charmhelpers.contrib.charmsupport",
25 "charmhelpers.contrib.saltstack",
24 "charmhelpers.contrib.hahelpers",26 "charmhelpers.contrib.hahelpers",
25 "charmhelpers.contrib.jujugui",27 "charmhelpers.contrib.jujugui",
26 ],28 ],
2729
=== added directory 'tests/contrib/saltstates'
=== added file 'tests/contrib/saltstates/__init__.py'
=== added file 'tests/contrib/saltstates/test_saltstates.py'
--- tests/contrib/saltstates/test_saltstates.py 1970-01-01 00:00:00 +0000
+++ tests/contrib/saltstates/test_saltstates.py 2013-06-14 07:11:26 +0000
@@ -0,0 +1,158 @@
1# Copyright 2013 Canonical Ltd.
2#
3# Authors:
4# Charm Helpers Developers <juju@lists.ubuntu.com>
5import json
6import mock
7import tempfile
8import unittest
9import yaml
10
11import charmhelpers.core.hookenv
12import charmhelpers.contrib.saltstack
13
14
15class InstallSaltSupportTestCase(unittest.TestCase):
16
17 def setUp(self):
18 super(InstallSaltSupportTestCase, self).setUp()
19
20 patcher = mock.patch('charmhelpers.contrib.saltstack.subprocess')
21 self.mock_subprocess = patcher.start()
22 self.addCleanup(patcher.stop)
23
24 patcher = mock.patch('charmhelpers.core')
25 self.mock_charmhelpers_core = patcher.start()
26 self.addCleanup(patcher.stop)
27
28 def test_adds_ppa_by_default(self):
29 charmhelpers.contrib.saltstack.install_salt_support()
30
31 self.assertEqual(self.mock_subprocess.check_call.call_count, 2)
32 self.assertEqual([(([
33 '/usr/bin/add-apt-repository',
34 '--yes',
35 'ppa:saltstack/salt',
36 ],), {}),
37 (([
38 '/usr/bin/apt-get',
39 'update',
40 ],), {})
41 ], self.mock_subprocess.check_call.call_args_list)
42 self.mock_charmhelpers_core.host.apt_install.assert_called_once_with(
43 'salt-minion')
44
45 def test_no_ppa(self):
46 charmhelpers.contrib.saltstack.install_salt_support(
47 from_ppa=False)
48
49 self.assertEqual(self.mock_subprocess.check_call.call_count, 0)
50 self.mock_charmhelpers_core.host.apt_install.assert_called_once_with(
51 'salt-minion')
52
53
54class UpdateMachineStateTestCase(unittest.TestCase):
55
56 def setUp(self):
57 super(UpdateMachineStateTestCase, self).setUp()
58
59 patcher = mock.patch('charmhelpers.contrib.saltstack.subprocess')
60 self.mock_subprocess = patcher.start()
61 self.addCleanup(patcher.stop)
62
63 patcher = mock.patch('charmhelpers.contrib.saltstack.'
64 'juju_config_2_grains')
65 self.mock_config_2_grains = patcher.start()
66 self.addCleanup(patcher.stop)
67
68 def test_calls_local_salt_template(self):
69 charmhelpers.contrib.saltstack.update_machine_state(
70 'states/install.yaml')
71
72 self.mock_subprocess.check_call.assert_called_once_with([
73 'salt-call',
74 '--local',
75 'state.template',
76 'states/install.yaml',
77 ])
78
79 def test_updates_grains(self):
80 charmhelpers.contrib.saltstack.update_machine_state(
81 'states/install.yaml')
82
83 self.mock_config_2_grains.assert_called_once_with()
84
85
86class JujuConfig2GrainsTestCase(unittest.TestCase):
87 def setUp(self):
88 super(JujuConfig2GrainsTestCase, self).setUp()
89
90 # Hookenv patches (a single patch to hookenv doesn't work):
91 patcher = mock.patch('charmhelpers.core.hookenv.config')
92 self.mock_config = patcher.start()
93 self.addCleanup(patcher.stop)
94 patcher = mock.patch('charmhelpers.core.hookenv.relation_get')
95 self.mock_relation_get = patcher.start()
96 self.mock_relation_get.return_value = {}
97 self.addCleanup(patcher.stop)
98 patcher = mock.patch('charmhelpers.core.hookenv.local_unit')
99 self.mock_local_unit = patcher.start()
100 self.addCleanup(patcher.stop)
101
102 # patches specific to this test class.
103 grain_file = tempfile.NamedTemporaryFile()
104 self.grain_path = grain_file.name
105 self.addCleanup(grain_file.close)
106
107 patcher = mock.patch.object(charmhelpers.contrib.saltstack,
108 'salt_grains_path', self.grain_path)
109 patcher.start()
110 self.addCleanup(patcher.stop)
111
112 patcher = mock.patch.object(charmhelpers.contrib.saltstack,
113 'charm_dir', '/tmp/charm_dir')
114 patcher.start()
115 self.addCleanup(patcher.stop)
116
117
118 def test_output_without_relation(self):
119 self.mock_config.return_value = charmhelpers.core.hookenv.Serializable({
120 'group_code_owner': 'webops_deploy',
121 'user_code_runner': 'ubunet',
122 })
123 self.mock_local_unit.return_value = "click-index/3"
124
125 charmhelpers.contrib.saltstack.juju_config_2_grains()
126
127 with open(self.grain_path, 'r') as grain_file:
128 result = yaml.load(grain_file.read())
129 self.assertEqual({
130 "charm_dir": "/tmp/charm_dir",
131 "group_code_owner": "webops_deploy",
132 "user_code_runner": "ubunet",
133 "local_unit": "click-index/3",
134 }, result)
135
136 def test_output_with_relation(self):
137 self.mock_config.return_value = charmhelpers.core.hookenv.Serializable({
138 'group_code_owner': 'webops_deploy',
139 'user_code_runner': 'ubunet',
140 })
141 self.mock_relation_get.return_value = {
142 'relation_key1': 'relation_value1',
143 'relation_key2': 'relation_value2',
144 }
145 self.mock_local_unit.return_value = "click-index/3"
146
147 charmhelpers.contrib.saltstack.juju_config_2_grains()
148
149 with open(self.grain_path, 'r') as grain_file:
150 result = yaml.load(grain_file.read())
151 self.assertEqual({
152 "charm_dir": "/tmp/charm_dir",
153 "group_code_owner": "webops_deploy",
154 "user_code_runner": "ubunet",
155 "relation_key1": "relation_value1",
156 "relation_key2": "relation_value2",
157 "local_unit": "click-index/3",
158 }, result)

Subscribers

People subscribed via source and target branches