Merge lp:~james-page/charm-tools/extra-python-helpers into lp:~charmers/charm-tools/trunk

Proposed by James Page
Status: Merged
Approved by: Mark Mims
Approved revision: 162
Merged at revision: 162
Proposed branch: lp:~james-page/charm-tools/extra-python-helpers
Merge into: lp:~charmers/charm-tools/trunk
Diff against target: 269 lines (+199/-9)
2 files modified
helpers/python/charmhelpers/__init__.py (+95/-9)
helpers/python/charmhelpers/tests/test_charmhelpers.py (+104/-0)
To merge this branch: bzr merge lp:~james-page/charm-tools/extra-python-helpers
Reviewer Review Type Date Requested Status
Mark Mims (community) Approve
Review via email: mp+127490@code.launchpad.net

Description of the change

This MP proposed a number of new helper functions for the python
charm helper.

This wrap a number of juju commands to help with interacting with relations,
retrieving unit and config attributed and interacting with deployed services.

Plus for a bonus a source configurator which uses config attributed 'source'
(and optionally 'key') to configure a charm to use different, non-archive software
sources.

To post a comment you must log in.
Revision history for this message
Mark Mims (mark-mims) wrote :

lgtm

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'helpers/python/charmhelpers/__init__.py'
2--- helpers/python/charmhelpers/__init__.py 2012-04-11 12:40:22 +0000
3+++ helpers/python/charmhelpers/__init__.py 2012-10-02 13:57:18 +0000
4@@ -11,6 +11,13 @@
5 'log_exit',
6 'relation_get',
7 'relation_set',
8+ 'relation_ids',
9+ 'relation_list',
10+ 'config_get',
11+ 'unit_get',
12+ 'open_port',
13+ 'close_port',
14+ 'service_control',
15 'unit_info',
16 'wait_for_machine',
17 'wait_for_page_contents',
18@@ -29,6 +36,7 @@
19 import time
20 import urllib2
21 import yaml
22+from subprocess import CalledProcessError
23
24
25 SLEEP_AMOUNT = 0.1
26@@ -48,20 +56,98 @@
27
28
29 def get_config():
30- config_get = command('config-get', '--format=json')
31- return json.loads(config_get())
32-
33-
34-def relation_get(*args):
35- cmd = command('relation-get')
36- return cmd(*args).strip()
37+ _config_get = command('config-get', '--format=json')
38+ return json.loads(_config_get())
39+
40+def relation_get():
41+ cmd = command('relation-get')
42+ return cmd().strip()
43+
44+def relation_get(attribute, unit=None, rid=None):
45+ cmd = command('relation-get')
46+ _args = []
47+ if rid:
48+ _args.append('-r')
49+ _args.append(rid)
50+ _args.append(attribute)
51+ if unit:
52+ _args.append(unit)
53+ return cmd(*_args).strip()
54
55
56 def relation_set(**kwargs):
57 cmd = command('relation-set')
58 args = ['{}={}'.format(k, v) for k, v in kwargs.items()]
59- return cmd(*args)
60-
61+ cmd(*args)
62+
63+
64+def relation_ids(relation_name):
65+ cmd = command('relation-ids')
66+ args = [ relation_name ]
67+ return cmd(*args).split()
68+
69+
70+def relation_list(rid=None):
71+ cmd = command('relation-list')
72+ args = []
73+ if rid:
74+ args.append('-r')
75+ args.append(rid)
76+ return cmd(*args).split()
77+
78+
79+def config_get(attribute):
80+ cmd = command('config-get')
81+ args = [ attribute ]
82+ return cmd(*args).strip()
83+
84+
85+def unit_get(attribute):
86+ cmd = command('unit-get')
87+ args = [ attribute ]
88+ return cmd(*args).strip()
89+
90+
91+def open_port(port, protocol="TCP"):
92+ cmd = command('open-port')
93+ args = [ '{}/{}'.format(port, protocol) ]
94+ cmd(*args)
95+
96+
97+def close_port(port, protocol="TCP"):
98+ cmd = command('close-port')
99+ args = [ '{}/{}'.format(port, protocol) ]
100+ cmd(*args)
101+
102+START="start"
103+RESTART="restart"
104+STOP="stop"
105+RELOAD="reload"
106+
107+def service_control(service_name, action):
108+ cmd = command('service')
109+ args = [ service_name, action ]
110+ try:
111+ if action == RESTART:
112+ try:
113+ cmd(*args)
114+ except CalledProcessError:
115+ service_control(service_name, START)
116+ else:
117+ cmd(*args)
118+ except CalledProcessError:
119+ log("Failed to perform {} on service {}".format(action, service_name))
120+
121+def configure_source(update=False):
122+ source = config_get('source')
123+ if (source.startswith('ppa:') or
124+ source.startswith('cloud:') or
125+ source.startswith('http:')):
126+ run('add-apt-repository', source)
127+ if source.startswith("http:"):
128+ run('apt-key', 'import', config_get('key'))
129+ if update:
130+ run('apt-get', 'update')
131
132 def make_charm_config_file(charm_config):
133 charm_config_file = tempfile.NamedTemporaryFile()
134
135=== modified file 'helpers/python/charmhelpers/tests/test_charmhelpers.py'
136--- helpers/python/charmhelpers/tests/test_charmhelpers.py 2012-04-11 13:15:44 +0000
137+++ helpers/python/charmhelpers/tests/test_charmhelpers.py 2012-10-02 13:57:18 +0000
138@@ -14,6 +14,8 @@
139 sys.path.insert(0, 'helpers/python')
140 import charmhelpers
141
142+from subprocess import CalledProcessError
143+
144
145 class CharmHelpersTestCase(TestCase):
146 """A basic test case for Python charm helpers."""
147@@ -91,6 +93,24 @@
148 self._patch_command(lambda: dumps(mock_config))
149 self.assertEqual(mock_config, charmhelpers.get_config())
150
151+ def test_config_get(self):
152+ # config_get is used to retrieve individual configuration elements
153+ mock_config = {'key': 'value'}
154+
155+ # Monkey-patch shelltoolbox.command to avoid having to call out
156+ # to config-get.
157+ self._patch_command(lambda *args: mock_config[args[0]])
158+ self.assertEqual(mock_config['key'], charmhelpers.config_get('key'))
159+
160+ def test_unit_get(self):
161+ # unit_get is used to retrieve individual configuration elements
162+ mock_config = {'key': 'value'}
163+
164+ # Monkey-patch shelltoolbox.command to avoid having to call out
165+ # to unit-get.
166+ self._patch_command(lambda *args: mock_config[args[0]])
167+ self.assertEqual(mock_config['key'], charmhelpers.unit_get('key'))
168+
169 def test_relation_get(self):
170 # relation_get returns the value of a given relation variable,
171 # as returned by relation-get $VAR.
172@@ -102,6 +122,10 @@
173 self.assertEqual('bar', charmhelpers.relation_get('foo'))
174 self.assertEqual('eggs', charmhelpers.relation_get('spam'))
175
176+ self._patch_command(lambda *args: mock_relation_values[args[2]])
177+ self.assertEqual('bar', charmhelpers.relation_get('foo','test','test:1'))
178+ self.assertEqual('eggs', charmhelpers.relation_get('spam','test','test:1'))
179+
180 def test_relation_set(self):
181 # relation_set calls out to relation-set and passes key=value
182 # pairs to it.
183@@ -115,6 +139,86 @@
184 self.assertEqual('bar', items_set.get('foo'))
185 self.assertEqual('eggs', items_set.get('spam'))
186
187+ def test_relation_ids(self):
188+ # relation_ids returns a list of relations id for the given
189+ # named relation
190+ mock_relation_ids = {
191+ 'test' : 'test:1 test:2'
192+ }
193+ self._patch_command(lambda *args: mock_relation_ids[args[0]])
194+ self.assertEqual(mock_relation_ids['test'].split(),
195+ charmhelpers.relation_ids('test'))
196+
197+ def test_relation_list(self):
198+ # relation_list returns a list of unit names either for the current
199+ # context or for the provided relation ID
200+ mock_unit_names = {
201+ 'test:1' : 'test/0 test/1 test/2',
202+ 'test:2' : 'test/3 test/4 test/5'
203+ }
204+
205+ # Patch command for current context use base - context = test:1
206+ self._patch_command(lambda: mock_unit_names['test:1'])
207+ self.assertEqual(mock_unit_names['test:1'].split(),
208+ charmhelpers.relation_list())
209+ # Patch command for provided relation-id
210+ self._patch_command(lambda *args: mock_unit_names[args[1]])
211+ self.assertEqual(mock_unit_names['test:2'].split(),
212+ charmhelpers.relation_list(rid='test:2'))
213+
214+ def test_open_close_port(self):
215+ # expose calls open-port with port/protocol parameters
216+ ports_set = []
217+ def mock_open_port(*args):
218+ for arg in args:
219+ ports_set.append(arg)
220+ def mock_close_port(*args):
221+ if args[0] in ports_set:
222+ ports_set.remove(args[0])
223+ # Monkey patch in the open-port mock
224+ self._patch_command(mock_open_port)
225+ charmhelpers.open_port(80,"TCP")
226+ charmhelpers.open_port(90,"UDP")
227+ charmhelpers.open_port(100)
228+ self.assertTrue("80/TCP" in ports_set)
229+ self.assertTrue("90/UDP" in ports_set)
230+ self.assertTrue("100/TCP" in ports_set)
231+ # Monkey patch in the close-port mock function
232+ self._patch_command(mock_close_port)
233+ charmhelpers.close_port(80,"TCP")
234+ charmhelpers.close_port(90,"UDP")
235+ charmhelpers.close_port(100)
236+ # ports_set should now be empty
237+ self.assertEquals(len(ports_set), 0)
238+
239+ def test_service_control(self):
240+ # Collect commands that have been run
241+ commands_set = {}
242+ def mock_service(*args):
243+ service = args[0]
244+ action = args[1]
245+ if service not in commands_set:
246+ commands_set[service] = []
247+ if (len(commands_set[service]) > 1 and
248+ commands_set[service][-1] == 'stop' and
249+ action == 'restart'):
250+ # Service is stopped - so needs 'start'
251+ # action as restart will fail
252+ commands_set[service].append(action)
253+ raise CalledProcessError(1, repr(args))
254+ else:
255+ commands_set[service].append(action)
256+
257+ result = [ 'start', 'stop', 'restart', 'start' ]
258+
259+ # Monkey patch service command
260+ self._patch_command(mock_service)
261+ charmhelpers.service_control('myservice','start')
262+ charmhelpers.service_control('myservice','stop')
263+ charmhelpers.service_control('myservice','restart')
264+ self.assertEquals(result, commands_set['myservice'])
265+
266+
267 def test_make_charm_config_file(self):
268 # make_charm_config_file() writes the passed configuration to a
269 # temporary file as YAML.

Subscribers

People subscribed via source and target branches