Merge lp:~thedac/charm-helpers/openstack-workload-status into lp:charm-helpers
- openstack-workload-status
- Merge into devel
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Page | Needs Fixing | ||
Review via email:
|
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
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') |
David
I think we need to differentiate between blocked and waiting to provide granualarity to end users - some suggestions in the diff.
Cheers
James