Merge lp:~thedac/charm-helpers/openstack-workload-status into lp:charm-helpers

Proposed by David Ames
Status: Work in progress
Proposed branch: lp:~thedac/charm-helpers/openstack-workload-status
Merge into: lp:charm-helpers
Diff against target: 439 lines (+328/-27)
4 files modified
charmhelpers/contrib/openstack/utils.py (+62/-1)
charmhelpers/core/hookenv.py (+84/-16)
tests/contrib/openstack/test_openstack_utils.py (+43/-0)
tests/core/test_hookenv.py (+139/-10)
To merge this branch: bzr merge lp:~thedac/charm-helpers/openstack-workload-status
Reviewer Review Type Date Requested Status
James Page Needs Fixing
Review via email: mp+264353@code.launchpad.net

Commit message

Workload status setting decorator and functions based on openstack contexts for openstack charms

Description of the change

Workload status setting decorator and functions based on openstack contexts for openstack charms

To post a comment you must log in.
402. By David Ames

Use functools wraps, which fixes scoping problems with multiple decorators

403. By David Ames

Adapt status_get to allow for --include-data and service
Add status_compound to "add" together statuses
Tests for new features

404. By David Ames

status_get should always return a dictionary and include-data
Log status change on status set

Revision history for this message
James Page (james-page) wrote :

David

I think we need to differentiate between blocked and waiting to provide granualarity to end users - some suggestions in the diff.

Cheers

James

review: Needs Fixing

Unmerged revisions

404. By David Ames

status_get should always return a dictionary and include-data
Log status change on status set

403. By David Ames

Adapt status_get to allow for --include-data and service
Add status_compound to "add" together statuses
Tests for new features

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmhelpers/contrib/openstack/utils.py'
2--- charmhelpers/contrib/openstack/utils.py 2015-06-30 15:28:49 +0000
3+++ charmhelpers/contrib/openstack/utils.py 2015-07-15 14:48:13 +0000
4@@ -40,7 +40,8 @@
5 charm_dir,
6 INFO,
7 relation_ids,
8- relation_set
9+ relation_set,
10+ status_set
11 )
12
13 from charmhelpers.contrib.storage.linux.lvm import (
14@@ -703,3 +704,63 @@
15 return projects[key]
16
17 return None
18+
19+
20+def context_status(configs, required_interfaces):
21+ """
22+ Decorator to set workload status based on complete contexts
23+ after contexts have been acted on
24+ """
25+ def wrap(f):
26+ @wraps(f)
27+ def wrapped_f(*args, **kwargs):
28+ # Run the original function first
29+ f(*args, **kwargs)
30+ # Set workload status now that contexts have been
31+ # acted on
32+ set_context_status(configs, required_interfaces)
33+ return wrapped_f
34+ return wrap
35+
36+
37+def set_context_status(configs, required_interfaces):
38+ """
39+ Set workload status based on complete contexts
40+ Left outside of the decorator, context_status,
41+ to allow manual use
42+ """
43+ incomplete_ctxts = incomplete_contexts(configs, required_interfaces)
44+ if incomplete_ctxts:
45+ status_set(
46+ 'blocked',
47+ '{} contexts are absent or incomplete'
48+ ''.format(', '.join(incomplete_ctxts))
49+ )
50+ else:
51+ status_set('active', 'Contexts are present and complete')
52+
53+
54+def incomplete_contexts(configs, required_interfaces):
55+ """
56+ Check complete contexts against required_interfaces
57+ Return list of incomplete contexts
58+
59+ configs is an OSConfigRenderer object with configs registered
60+
61+ required_interfaces is a dictionary of required general interfaces
62+ with list values of possible specific interfaces.
63+ The interface is said to be satisfied if anyone of the interfaces in the
64+ list has a complete context.
65+ Example:
66+ required_interfaces = {'database': ['shared-db', 'pgsql-db']}
67+ """
68+ complete_ctxts = configs.complete_contexts()
69+ incomplete_contexts = []
70+ for svc_type in required_interfaces.keys():
71+ found_ctxt = False
72+ for interface in required_interfaces[svc_type]:
73+ if interface in complete_ctxts:
74+ found_ctxt = True
75+ if not found_ctxt:
76+ incomplete_contexts.append(svc_type)
77+ return incomplete_contexts
78
79=== modified file 'charmhelpers/core/hookenv.py'
80--- charmhelpers/core/hookenv.py 2015-06-30 15:28:49 +0000
81+++ charmhelpers/core/hookenv.py 2015-07-15 14:48:13 +0000
82@@ -660,35 +660,103 @@
83 )
84 cmd = ['status-set', workload_state, message]
85 try:
86- ret = subprocess.call(cmd)
87- if ret == 0:
88+ subprocess.call(cmd)
89+ except OSError as e:
90+ if e.errno == errno.ENOENT:
91+ log_message = 'status-set failed: {} {}'.format(workload_state,
92+ message)
93+ log(log_message, level='INFO')
94 return
95- except OSError as e:
96- if e.errno != errno.ENOENT:
97+ else:
98 raise
99- log_message = 'status-set failed: {} {}'.format(workload_state,
100- message)
101- log(log_message, level='INFO')
102-
103-
104-def status_get():
105- """Retrieve the previously set juju workload state
106+ # Log the status change
107+ if workload_state == 'blocked' or workload_state == 'waiting':
108+ lvl = 'WARN'
109+ else:
110+ lvl = 'INFO'
111+ log('{}: {}'.format(workload_state, message), lvl)
112+
113+
114+def status_get(include_data=True, service=False):
115+ """Retrieve the previously set juju workload state and return as a
116+ dictionary.
117+
118+ If service is set return status for all units of this service if this unit
119+ is the leader
120
121 If the status-set command is not found then assume this is juju < 1.23 and
122 return 'unknown'
123 """
124- cmd = ['status-get']
125+ cmd = ['status-get', '--format', 'json']
126+ if include_data:
127+ cmd.append('--include-data')
128+ if service:
129+ cmd.append('--service')
130 try:
131- raw_status = subprocess.check_output(cmd, universal_newlines=True)
132- status = raw_status.rstrip()
133- return status
134+ return json.loads(subprocess.check_output(cmd,
135+ universal_newlines=True))
136 except OSError as e:
137 if e.errno == errno.ENOENT:
138- return 'unknown'
139+ return {'status': 'unknown'}
140 else:
141 raise
142
143
144+def status_compound(current_status, workload_state, message):
145+ """Compound statuses: Take the current status as a dictionary
146+ and new workload state and message then status_set with the highest
147+ severity workload state and a compound message
148+ Example:
149+ status_compound(status_get(include-data=True),
150+ 'waiting',
151+ 'waiting on relations data')
152+ """
153+ hierarchy = {'unknown': -1,
154+ 'active': 0,
155+ 'maintenance': 1,
156+ 'waiting': 2,
157+ 'blocked': 3,
158+ }
159+ current_workload_state = current_status.get('status')
160+ current_message = current_status.get('message')
161+
162+ # Do not set unknown or other invalid state
163+ if hierarchy.get(workload_state) is None:
164+ return {}
165+ elif hierarchy.get(workload_state) < 0:
166+ return {}
167+ if hierarchy.get(current_workload_state) is None:
168+ current_workload_state = 'unknown'
169+ current_message = None
170+ elif hierarchy.get(current_workload_state) < 0:
171+ current_message = None
172+
173+ # New state of 'active' overrides all others
174+ if workload_state == 'active':
175+ hierarchy['active'] = 10
176+ current_message = None
177+
178+ # Set workload_state based on hierarchy of statuses
179+ if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
180+ workload_state = current_workload_state
181+ else:
182+ if current_workload_state == 'active':
183+ current_message = None
184+
185+ # Compound the message
186+ if current_message and message:
187+ message = message + "; " + current_message
188+ elif current_message and not message:
189+ message = current_message
190+
191+ # Set new status
192+ if workload_state:
193+ status_set(workload_state, message)
194+
195+ # Return new status
196+ return {'status': workload_state, 'message': message}
197+
198+
199 def translate_exc(from_exc, to_exc):
200 def inner_translate_exc1(f):
201 def inner_translate_exc2(*args, **kwargs):
202
203=== modified file 'tests/contrib/openstack/test_openstack_utils.py'
204--- tests/contrib/openstack/test_openstack_utils.py 2015-07-14 18:12:04 +0000
205+++ tests/contrib/openstack/test_openstack_utils.py 2015-07-15 14:48:13 +0000
206@@ -824,5 +824,48 @@
207 openstack.git_src_dir(openstack_origin_git, 'keystone')
208 join.assert_called_with('/mnt/openstack-git', 'keystone')
209
210+ def test_incomplete_contexts(self):
211+ configs = MagicMock()
212+ configs.complete_contexts.return_value = ['pgsql-db', 'amqp']
213+ required_interfaces = {
214+ 'database': ['shared-db', 'pgsql-db'],
215+ 'message': ['amqp', 'zeromq-configuration'],
216+ 'identity': ['identity-service']}
217+ expected_result = ['identity']
218+
219+ self.assertEquals(openstack.incomplete_contexts(configs,
220+ required_interfaces),
221+ expected_result)
222+
223+ @patch('charmhelpers.contrib.openstack.utils.status_set')
224+ def test_set_context_status_complete(self, status_set):
225+ configs = MagicMock()
226+ configs.complete_contexts.return_value = ['shared-db',
227+ 'amqp',
228+ 'identity-service']
229+ required_interfaces = {
230+ 'database': ['shared-db', 'pgsql-db'],
231+ 'message': ['amqp', 'zeromq-configuration'],
232+ 'identity': ['identity-service']}
233+
234+ openstack.set_context_status(configs, required_interfaces)
235+ status_set.assert_called_with('active',
236+ 'Contexts are present and complete')
237+
238+ @patch('charmhelpers.contrib.openstack.utils.status_set')
239+ def test_set_context_status_incomplete(self, status_set):
240+ configs = MagicMock()
241+ configs.complete_contexts.return_value = ['shared-db', 'amqp']
242+ required_interfaces = {
243+ 'database': ['shared-db', 'pgsql-db'],
244+ 'message': ['amqp', 'zeromq-configuration'],
245+ 'identity': ['identity-service']}
246+
247+ openstack.set_context_status(configs, required_interfaces)
248+ status_set.assert_called_with('blocked',
249+ 'identity contexts are absent '
250+ 'or incomplete')
251+
252+
253 if __name__ == '__main__':
254 unittest.main()
255
256=== modified file 'tests/core/test_hookenv.py'
257--- tests/core/test_hookenv.py 2015-06-26 14:17:43 +0000
258+++ tests/core/test_hookenv.py 2015-07-15 14:48:13 +0000
259@@ -1293,17 +1293,21 @@
260 self.assertRaises(ValueError, hookenv.status_set, 'random', 'message')
261
262 @patch('subprocess.call')
263- def test_status(self, call):
264- call.return_value = 0
265+ def test_status(self, _call):
266+ _call.return_value = 0
267 hookenv.status_set('active', 'Everything is Awesome!')
268- call.assert_called_with(['status-set', 'active', 'Everything is Awesome!'])
269+ calls = [call(['status-set', 'active', 'Everything is Awesome!']),
270+ call(['juju-log', '-l', 'INFO',
271+ 'active: Everything is Awesome!'])]
272+ _call.assert_has_calls(calls, any_order=True)
273
274 @patch('subprocess.call')
275 @patch.object(hookenv, 'log')
276 def test_status_enoent(self, log, call):
277 call.side_effect = OSError(2, 'fail')
278 hookenv.status_set('active', 'Everything is Awesome!')
279- log.assert_called_with('status-set failed: active Everything is Awesome!', level='INFO')
280+ log.assert_called_with('status-set failed: active Everything is Awesome!',
281+ level='INFO')
282
283 @patch('subprocess.call')
284 @patch.object(hookenv, 'log')
285@@ -1314,23 +1318,148 @@
286
287 @patch('subprocess.check_output')
288 def test_status_get(self, check_output):
289- check_output.return_value = 'active\n'
290+ check_output.return_value = json.dumps({'status': 'active'})
291 result = hookenv.status_get()
292- self.assertEqual(result, 'active')
293- check_output.assert_called_with(['status-get'], universal_newlines=True)
294+ self.assertEqual(result, {'status': 'active'})
295+ check_output.assert_called_with(['status-get',
296+ '--format',
297+ 'json',
298+ '--include-data'],
299+ universal_newlines=True)
300+
301+ @patch('subprocess.check_output')
302+ def test_status_get_include_data_false(self, check_output):
303+ return_dict = {"status": "active"}
304+ check_output.return_value = json.dumps(return_dict)
305+ result = hookenv.status_get(include_data=False)
306+ self.assertEqual(return_dict, result)
307+ check_output.assert_called_with(['status-get',
308+ '--format', 'json'],
309+ universal_newlines=True)
310+
311+ @patch('subprocess.check_output')
312+ def test_status_get_service(self, check_output):
313+ return_dict = {"service-status":
314+ {"message": "all good",
315+ "status": "active",
316+ "status-data": {},
317+ "units": {"test/0":
318+ {"message": "all good",
319+ "status": "active",
320+ "status-data": {}
321+ }
322+ }
323+ }
324+ }
325+
326+ check_output.return_value = json.dumps(return_dict)
327+ result = hookenv.status_get(service=True)
328+ self.assertEqual(return_dict, result)
329+ check_output.assert_called_with(['status-get',
330+ '--format', 'json',
331+ '--include-data',
332+ '--service'],
333+ universal_newlines=True)
334
335 @patch('subprocess.check_output')
336 def test_status_get_nostatus(self, check_output):
337 check_output.side_effect = OSError(2, 'fail')
338 result = hookenv.status_get()
339- self.assertEqual(result, 'unknown')
340- check_output.assert_called_with(['status-get'], universal_newlines=True)
341+ self.assertEqual(result, {'status': 'unknown'})
342+ check_output.assert_called_with(['status-get',
343+ '--format', 'json',
344+ '--include-data'],
345+ universal_newlines=True)
346
347 @patch('subprocess.check_output')
348 def test_status_get_status_error(self, check_output):
349 check_output.side_effect = OSError(3, 'fail')
350 self.assertRaises(OSError, hookenv.status_get)
351- check_output.assert_called_with(['status-get'], universal_newlines=True)
352+ check_output.assert_called_with(['status-get',
353+ '--format', 'json',
354+ '--include-data'],
355+ universal_newlines=True)
356+
357+ @patch('charmhelpers.core.hookenv.status_set')
358+ def test_status_compound_active_to_worse(self, status_set):
359+ current_status = {"status": "active", "message": "all good"}
360+ new_state = "blocked"
361+ new_message = "blocked on bad things"
362+ hookenv.status_compound(current_status, new_state, new_message)
363+ status_set.assert_called_with(new_state, new_message)
364+
365+ @patch('charmhelpers.core.hookenv.status_set')
366+ def test_status_compound_bad_to_active(self, status_set):
367+ current_status = {"status": "blocked", "message": "blocked on bad things"}
368+ new_state = "active"
369+ new_message = "all good"
370+ hookenv.status_compound(current_status, new_state, new_message)
371+ status_set.assert_called_with(new_state, new_message)
372+
373+ @patch('charmhelpers.core.hookenv.status_set')
374+ def test_status_compound_bad_to_worse(self, status_set):
375+ current_status = {"status": "waiting", "message": "waiting on value"}
376+ new_state = "blocked"
377+ new_message = "blocked on bad things"
378+ hookenv.status_compound(current_status, new_state, new_message)
379+ status_set.assert_called_with(new_state,
380+ "{}; {}"
381+ "".format(new_message,
382+ current_status['message']))
383+
384+ @patch('charmhelpers.core.hookenv.status_set')
385+ def test_status_compound_bad_to_better(self, status_set):
386+ current_status = {"status": "waiting", "message": "waiting on value"}
387+ new_state = "maintenance"
388+ new_message = "maintaing things"
389+ hookenv.status_compound(current_status, new_state, new_message)
390+ status_set.assert_called_with(current_status["status"],
391+ "{}; {}"
392+ "".format(new_message,
393+ current_status['message']))
394+
395+ @patch('charmhelpers.core.hookenv.status_set')
396+ def test_status_compound_equal(self, status_set):
397+ current_status = {"status": "waiting", "message": "waiting on value"}
398+ new_state = "waiting"
399+ new_message = "waiting on new things"
400+ hookenv.status_compound(current_status, new_state, new_message)
401+ status_set.assert_called_with(new_state,
402+ "{}; {}"
403+ "".format(new_message,
404+ current_status['message']))
405+
406+ @patch('charmhelpers.core.hookenv.status_set')
407+ def test_status_compound_unknown_to_any(self, status_set):
408+ current_status = {"status": "unknown", "message": "Bogus message"}
409+ new_state = "waiting"
410+ new_message = "waiting on things"
411+ hookenv.status_compound(current_status, new_state, new_message)
412+ status_set.assert_called_with(new_state, new_message)
413+
414+ @patch('charmhelpers.core.hookenv.status_set')
415+ def test_status_compound_any_to_unknown(self, status_set):
416+ current_status = {"status": "waiting", "message": "waiting on value"}
417+ new_state = "unknown"
418+ new_message = "Bogus message"
419+ hookenv.status_compound(current_status, new_state, new_message)
420+ assert not status_set.called
421+
422+ @patch('charmhelpers.core.hookenv.status_set')
423+ def test_status_compound_invalid(self, status_set):
424+ current_status = {"status": "waiting", "message": "waiting on value"}
425+ new_state = "invalid"
426+ new_message = "Bogus message"
427+ hookenv.status_compound(current_status, new_state, new_message)
428+ assert not status_set.called
429+
430+ @patch('charmhelpers.core.hookenv.status_set')
431+ def test_status_compound_none_to_any(self, status_set):
432+ current_status = {}
433+ new_state = "waiting"
434+ new_message = "waiting on things"
435+ hookenv.status_compound(current_status, new_state, new_message)
436+ status_set.assert_called_with(new_state, new_message)
437
438 @patch('subprocess.check_output')
439 @patch('glob.glob')

Subscribers

People subscribed via source and target branches