Merge lp:~ajkavanagh/charm-helpers/support-maintenance-mode into lp:charm-helpers
- support-maintenance-mode
- Merge into devel
Proposed by
Alex Kavanagh
Status: | Merged |
---|---|
Merged at revision: | 539 |
Proposed branch: | lp:~ajkavanagh/charm-helpers/support-maintenance-mode |
Merge into: | lp:charm-helpers |
Diff against target: |
1459 lines (+1050/-191) 3 files modified
charmhelpers/contrib/openstack/utils.py (+539/-118) charmhelpers/core/host.py (+36/-14) tests/contrib/openstack/test_openstack_utils.py (+475/-59) |
To merge this branch: | bzr merge lp:~ajkavanagh/charm-helpers/support-maintenance-mode |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Liam Young (community) | Approve | ||
Review via email: mp+287907@code.launchpad.net |
Commit message
Description of the change
Support pause/resume functionality for OpenStack charms. Pause and resume are baked into set_os_
Also includes a @pausable_
To post a comment you must log in.
Revision history for this message
Alex Kavanagh (ajkavanagh) wrote : | # |
Thanks for looking through it; I know it's a big patch. I'll simplify the list comprehensions, and I've commented inline/responses.
- 542. By Alex Kavanagh
-
Simplify the list comprehensions around message generation as per gnuoy's
suggestions. They were a little unreadable.
Revision history for this message
Liam Young (gnuoy) wrote : | # |
Looks good to me, thanks for the mp!
review:
Approve
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 2016-02-17 16:29:13 +0000 |
3 | +++ charmhelpers/contrib/openstack/utils.py 2016-03-07 16:45:36 +0000 |
4 | @@ -24,6 +24,7 @@ |
5 | import sys |
6 | import re |
7 | import itertools |
8 | +import functools |
9 | |
10 | import six |
11 | import tempfile |
12 | @@ -69,7 +70,15 @@ |
13 | pip_install, |
14 | ) |
15 | |
16 | -from charmhelpers.core.host import lsb_release, mounts, umount, service_running |
17 | +from charmhelpers.core.host import ( |
18 | + lsb_release, |
19 | + mounts, |
20 | + umount, |
21 | + service_running, |
22 | + service_pause, |
23 | + service_resume, |
24 | + restart_on_change_helper, |
25 | +) |
26 | from charmhelpers.fetch import apt_install, apt_cache, install_remote |
27 | from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk |
28 | from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device |
29 | @@ -862,66 +871,155 @@ |
30 | return wrap |
31 | |
32 | |
33 | -def set_os_workload_status(configs, required_interfaces, charm_func=None, services=None, ports=None): |
34 | - """ |
35 | - Set workload status based on complete contexts. |
36 | - status-set missing or incomplete contexts |
37 | - and juju-log details of missing required data. |
38 | - charm_func is a charm specific function to run checking |
39 | - for charm specific requirements such as a VIP setting. |
40 | - |
41 | - This function also checks for whether the services defined are ACTUALLY |
42 | - running and that the ports they advertise are open and being listened to. |
43 | - |
44 | - @param services - OPTIONAL: a [{'service': <string>, 'ports': [<int>]] |
45 | - The ports are optional. |
46 | - If services is a [<string>] then ports are ignored. |
47 | - @param ports - OPTIONAL: an [<int>] representing ports that shoudl be |
48 | - open. |
49 | - @returns None |
50 | - """ |
51 | - incomplete_rel_data = incomplete_relation_data(configs, required_interfaces) |
52 | - state = 'active' |
53 | - missing_relations = [] |
54 | - incomplete_relations = [] |
55 | +def set_os_workload_status(configs, required_interfaces, charm_func=None, |
56 | + services=None, ports=None): |
57 | + """Set the state of the workload status for the charm. |
58 | + |
59 | + This calls _determine_os_workload_status() to get the new state, message |
60 | + and sets the status using status_set() |
61 | + |
62 | + @param configs: a templating.OSConfigRenderer() object |
63 | + @param required_interfaces: {generic: [specific, specific2, ...]} |
64 | + @param charm_func: a callable function that returns state, message. The |
65 | + signature is charm_func(configs) -> (state, message) |
66 | + @param services: list of strings OR dictionary specifying services/ports |
67 | + @param ports: OPTIONAL list of port numbers. |
68 | + @returns state, message: the new workload status, user message |
69 | + """ |
70 | + state, message = _determine_os_workload_status( |
71 | + configs, required_interfaces, charm_func, services, ports) |
72 | + status_set(state, message) |
73 | + |
74 | + |
75 | +def _determine_os_workload_status( |
76 | + configs, required_interfaces, charm_func=None, |
77 | + services=None, ports=None): |
78 | + """Determine the state of the workload status for the charm. |
79 | + |
80 | + This function returns the new workload status for the charm based |
81 | + on the state of the interfaces, the paused state and whether the |
82 | + services are actually running and any specified ports are open. |
83 | + |
84 | + This checks: |
85 | + |
86 | + 1. if the unit should be paused, that it is actually paused. If so the |
87 | + state is 'maintenance' + message, else 'broken'. |
88 | + 2. that the interfaces/relations are complete. If they are not then |
89 | + it sets the state to either 'broken' or 'waiting' and an appropriate |
90 | + message. |
91 | + 3. If all the relation data is set, then it checks that the actual |
92 | + services really are running. If not it sets the state to 'broken'. |
93 | + |
94 | + If everything is okay then the state returns 'active'. |
95 | + |
96 | + @param configs: a templating.OSConfigRenderer() object |
97 | + @param required_interfaces: {generic: [specific, specific2, ...]} |
98 | + @param charm_func: a callable function that returns state, message. The |
99 | + signature is charm_func(configs) -> (state, message) |
100 | + @param services: list of strings OR dictionary specifying services/ports |
101 | + @param ports: OPTIONAL list of port numbers. |
102 | + @returns state, message: the new workload status, user message |
103 | + """ |
104 | + state, message = _ows_check_if_paused(services, ports) |
105 | + |
106 | + if state is None: |
107 | + state, message = _ows_check_generic_interfaces( |
108 | + configs, required_interfaces) |
109 | + |
110 | + if state != 'maintenance' and charm_func: |
111 | + # _ows_check_charm_func() may modify the state, message |
112 | + state, message = _ows_check_charm_func( |
113 | + state, message, lambda: charm_func(configs)) |
114 | + |
115 | + if state is None: |
116 | + state, message = _ows_check_services_running(services, ports) |
117 | + |
118 | + if state is None: |
119 | + state = 'active' |
120 | + message = "Unit is ready" |
121 | + juju_log(message, 'INFO') |
122 | + |
123 | + return state, message |
124 | + |
125 | + |
126 | +def _ows_check_if_paused(services=None, ports=None): |
127 | + """Check if the unit is supposed to be paused, and if so check that the |
128 | + services/ports (if passed) are actually stopped/not being listened to. |
129 | + |
130 | + if the unit isn't supposed to be paused, just return None, None |
131 | + |
132 | + @param services: OPTIONAL services spec or list of service names. |
133 | + @param ports: OPTIONAL list of port numbers. |
134 | + @returns state, message or None, None |
135 | + """ |
136 | + if is_unit_paused_set(): |
137 | + state, message = check_actually_paused(services=services, |
138 | + ports=ports) |
139 | + if state is None: |
140 | + # we're paused okay, so set maintenance and return |
141 | + state = "maintenance" |
142 | + message = "Paused. Use 'resume' action to resume normal service." |
143 | + return state, message |
144 | + return None, None |
145 | + |
146 | + |
147 | +def _ows_check_generic_interfaces(configs, required_interfaces): |
148 | + """Check the complete contexts to determine the workload status. |
149 | + |
150 | + - Checks for missing or incomplete contexts |
151 | + - juju log details of missing required data. |
152 | + - determines the correct workload status |
153 | + - creates an appropriate message for status_set(...) |
154 | + |
155 | + if there are no problems then the function returns None, None |
156 | + |
157 | + @param configs: a templating.OSConfigRenderer() object |
158 | + @params required_interfaces: {generic_interface: [specific_interface], } |
159 | + @returns state, message or None, None |
160 | + """ |
161 | + incomplete_rel_data = incomplete_relation_data(configs, |
162 | + required_interfaces) |
163 | + state = None |
164 | message = None |
165 | - charm_state = None |
166 | - charm_message = None |
167 | + missing_relations = set() |
168 | + incomplete_relations = set() |
169 | |
170 | - for generic_interface in incomplete_rel_data.keys(): |
171 | + for generic_interface, relations_states in incomplete_rel_data.items(): |
172 | related_interface = None |
173 | missing_data = {} |
174 | # Related or not? |
175 | - for interface in incomplete_rel_data[generic_interface]: |
176 | - if incomplete_rel_data[generic_interface][interface].get('related'): |
177 | + for interface, relation_state in relations_states.items(): |
178 | + if relation_state.get('related'): |
179 | related_interface = interface |
180 | - missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data') |
181 | - # No relation ID for the generic_interface |
182 | + missing_data = relation_state.get('missing_data') |
183 | + break |
184 | + # No relation ID for the generic_interface? |
185 | if not related_interface: |
186 | juju_log("{} relation is missing and must be related for " |
187 | "functionality. ".format(generic_interface), 'WARN') |
188 | state = 'blocked' |
189 | - if generic_interface not in missing_relations: |
190 | - missing_relations.append(generic_interface) |
191 | + missing_relations.add(generic_interface) |
192 | else: |
193 | - # Relation ID exists but no related unit |
194 | + # Relation ID eists but no related unit |
195 | if not missing_data: |
196 | - # Edge case relation ID exists but departing |
197 | - if ('departed' in hook_name() or 'broken' in hook_name()) \ |
198 | - and related_interface in hook_name(): |
199 | + # Edge case - relation ID exists but departings |
200 | + _hook_name = hook_name() |
201 | + if (('departed' in _hook_name or 'broken' in _hook_name) and |
202 | + related_interface in _hook_name): |
203 | state = 'blocked' |
204 | - if generic_interface not in missing_relations: |
205 | - missing_relations.append(generic_interface) |
206 | + missing_relations.add(generic_interface) |
207 | juju_log("{} relation's interface, {}, " |
208 | "relationship is departed or broken " |
209 | "and is required for functionality." |
210 | - "".format(generic_interface, related_interface), "WARN") |
211 | + "".format(generic_interface, related_interface), |
212 | + "WARN") |
213 | # Normal case relation ID exists but no related unit |
214 | # (joining) |
215 | else: |
216 | juju_log("{} relations's interface, {}, is related but has " |
217 | "no units in the relation." |
218 | - "".format(generic_interface, related_interface), "INFO") |
219 | + "".format(generic_interface, related_interface), |
220 | + "INFO") |
221 | # Related unit exists and data missing on the relation |
222 | else: |
223 | juju_log("{} relation's interface, {}, is related awaiting " |
224 | @@ -930,9 +1028,8 @@ |
225 | ", ".join(missing_data)), "INFO") |
226 | if state != 'blocked': |
227 | state = 'waiting' |
228 | - if generic_interface not in incomplete_relations \ |
229 | - and generic_interface not in missing_relations: |
230 | - incomplete_relations.append(generic_interface) |
231 | + if generic_interface not in missing_relations: |
232 | + incomplete_relations.add(generic_interface) |
233 | |
234 | if missing_relations: |
235 | message = "Missing relations: {}".format(", ".join(missing_relations)) |
236 | @@ -945,9 +1042,22 @@ |
237 | "".format(", ".join(incomplete_relations)) |
238 | state = 'waiting' |
239 | |
240 | - # Run charm specific checks |
241 | - if charm_func: |
242 | - charm_state, charm_message = charm_func(configs) |
243 | + return state, message |
244 | + |
245 | + |
246 | +def _ows_check_charm_func(state, message, charm_func_with_configs): |
247 | + """Run a custom check function for the charm to see if it wants to |
248 | + change the state. This is only run if not in 'maintenance' and |
249 | + tests to see if the new state is more important that the previous |
250 | + one determined by the interfaces/relations check. |
251 | + |
252 | + @param state: the previously determined state so far. |
253 | + @param message: the user orientated message so far. |
254 | + @param charm_func: a callable function that returns state, message |
255 | + @returns state, message strings. |
256 | + """ |
257 | + if charm_func_with_configs: |
258 | + charm_state, charm_message = charm_func_with_configs() |
259 | if charm_state != 'active' and charm_state != 'unknown': |
260 | state = workload_state_compare(state, charm_state) |
261 | if message: |
262 | @@ -956,72 +1066,151 @@ |
263 | message = "{}, {}".format(message, charm_message) |
264 | else: |
265 | message = charm_message |
266 | - |
267 | - # If the charm thinks the unit is active, check that the actual services |
268 | - # really are active. |
269 | - if services is not None and state == 'active': |
270 | - # if we're passed the dict() then just grab the values as a list. |
271 | - if isinstance(services, dict): |
272 | - services = services.values() |
273 | - # either extract the list of services from the dictionary, or if |
274 | - # it is a simple string, use that. i.e. works with mixed lists. |
275 | - _s = [] |
276 | - for s in services: |
277 | - if isinstance(s, dict) and 'service' in s: |
278 | - _s.append(s['service']) |
279 | - if isinstance(s, str): |
280 | - _s.append(s) |
281 | - services_running = [service_running(s) for s in _s] |
282 | - if not all(services_running): |
283 | - not_running = [s for s, running in zip(_s, services_running) |
284 | - if not running] |
285 | - message = ("Services not running that should be: {}" |
286 | - .format(", ".join(not_running))) |
287 | + return state, message |
288 | + |
289 | + |
290 | +def _ows_check_services_running(services, ports): |
291 | + """Check that the services that should be running are actually running |
292 | + and that any ports specified are being listened to. |
293 | + |
294 | + @param services: list of strings OR dictionary specifying services/ports |
295 | + @param ports: list of ports |
296 | + @returns state, message: strings or None, None |
297 | + """ |
298 | + messages = [] |
299 | + state = None |
300 | + if services is not None: |
301 | + services = _extract_services_list_helper(services) |
302 | + services_running, running = _check_running_services(services) |
303 | + if not all(running): |
304 | + messages.append( |
305 | + "Services not running that should be: {}" |
306 | + .format(", ".join(_filter_tuples(services_running, False)))) |
307 | state = 'blocked' |
308 | # also verify that the ports that should be open are open |
309 | # NB, that ServiceManager objects only OPTIONALLY have ports |
310 | - port_map = OrderedDict([(s['service'], s['ports']) |
311 | - for s in services if 'ports' in s]) |
312 | - if state == 'active' and port_map: |
313 | - all_ports = list(itertools.chain(*port_map.values())) |
314 | - ports_open = [port_has_listener('0.0.0.0', p) |
315 | - for p in all_ports] |
316 | - if not all(ports_open): |
317 | - not_opened = [p for p, opened in zip(all_ports, ports_open) |
318 | - if not opened] |
319 | - map_not_open = OrderedDict() |
320 | - for service, ports in port_map.items(): |
321 | - closed_ports = set(ports).intersection(not_opened) |
322 | - if closed_ports: |
323 | - map_not_open[service] = closed_ports |
324 | - # find which service has missing ports. They are in service |
325 | - # order which makes it a bit easier. |
326 | - message = ( |
327 | - "Services with ports not open that should be: {}" |
328 | - .format( |
329 | - ", ".join([ |
330 | - "{}: [{}]".format( |
331 | - service, |
332 | - ", ".join([str(v) for v in ports])) |
333 | - for service, ports in map_not_open.items()]))) |
334 | - state = 'blocked' |
335 | + map_not_open, ports_open = ( |
336 | + _check_listening_on_services_ports(services)) |
337 | + if not all(ports_open): |
338 | + # find which service has missing ports. They are in service |
339 | + # order which makes it a bit easier. |
340 | + message_parts = {service: ", ".join([str(v) for v in open_ports]) |
341 | + for service, open_ports in map_not_open.items()} |
342 | + message = ", ".join( |
343 | + ["{}: [{}]".format(s, sp) for s, sp in message_parts.items()]) |
344 | + messages.append( |
345 | + "Services with ports not open that should be: {}" |
346 | + .format(message)) |
347 | + state = 'blocked' |
348 | |
349 | - if ports is not None and state == 'active': |
350 | + if ports is not None: |
351 | # and we can also check ports which we don't know the service for |
352 | - ports_open = [port_has_listener('0.0.0.0', p) for p in ports] |
353 | - if not all(ports_open): |
354 | - message = ( |
355 | + ports_open, ports_open_bools = _check_listening_on_ports_list(ports) |
356 | + if not all(ports_open_bools): |
357 | + messages.append( |
358 | "Ports which should be open, but are not: {}" |
359 | - .format(", ".join([str(p) for p, v in zip(ports, ports_open) |
360 | + .format(", ".join([str(p) for p, v in ports_open |
361 | if not v]))) |
362 | state = 'blocked' |
363 | |
364 | - # Set to active if all requirements have been met |
365 | - if state == 'active': |
366 | - message = "Unit is ready" |
367 | - juju_log(message, "INFO") |
368 | - |
369 | - status_set(state, message) |
370 | + if state is not None: |
371 | + message = "; ".join(messages) |
372 | + return state, message |
373 | + |
374 | + return None, None |
375 | + |
376 | + |
377 | +def _extract_services_list_helper(services): |
378 | + """Extract a OrderedDict of {service: [ports]} of the supplied services |
379 | + for use by the other functions. |
380 | + |
381 | + The services object can either be: |
382 | + - None : no services were passed (an empty dict is returned) |
383 | + - a list of strings |
384 | + - A dictionary (optionally OrderedDict) {service_name: {'service': ..}} |
385 | + - An array of [{'service': service_name, ...}, ...] |
386 | + |
387 | + @param services: see above |
388 | + @returns OrderedDict(service: [ports], ...) |
389 | + """ |
390 | + if services is None: |
391 | + return {} |
392 | + if isinstance(services, dict): |
393 | + services = services.values() |
394 | + # either extract the list of services from the dictionary, or if |
395 | + # it is a simple string, use that. i.e. works with mixed lists. |
396 | + _s = OrderedDict() |
397 | + for s in services: |
398 | + if isinstance(s, dict) and 'service' in s: |
399 | + _s[s['service']] = s.get('ports', []) |
400 | + if isinstance(s, str): |
401 | + _s[s] = [] |
402 | + return _s |
403 | + |
404 | + |
405 | +def _check_running_services(services): |
406 | + """Check that the services dict provided is actually running and provide |
407 | + a list of (service, boolean) tuples for each service. |
408 | + |
409 | + Returns both a zipped list of (service, boolean) and a list of booleans |
410 | + in the same order as the services. |
411 | + |
412 | + @param services: OrderedDict of strings: [ports], one for each service to |
413 | + check. |
414 | + @returns [(service, boolean), ...], : results for checks |
415 | + [boolean] : just the result of the service checks |
416 | + """ |
417 | + services_running = [service_running(s) for s in services] |
418 | + return list(zip(services, services_running)), services_running |
419 | + |
420 | + |
421 | +def _check_listening_on_services_ports(services, test=False): |
422 | + """Check that the unit is actually listening (has the port open) on the |
423 | + ports that the service specifies are open. If test is True then the |
424 | + function returns the services with ports that are open rather than |
425 | + closed. |
426 | + |
427 | + Returns an OrderedDict of service: ports and a list of booleans |
428 | + |
429 | + @param services: OrderedDict(service: [port, ...], ...) |
430 | + @param test: default=False, if False, test for closed, otherwise open. |
431 | + @returns OrderedDict(service: [port-not-open, ...]...), [boolean] |
432 | + """ |
433 | + test = not(not(test)) # ensure test is True or False |
434 | + all_ports = list(itertools.chain(*services.values())) |
435 | + ports_states = [port_has_listener('0.0.0.0', p) for p in all_ports] |
436 | + map_ports = OrderedDict() |
437 | + matched_ports = [p for p, opened in zip(all_ports, ports_states) |
438 | + if opened == test] # essentially opened xor test |
439 | + for service, ports in services.items(): |
440 | + set_ports = set(ports).intersection(matched_ports) |
441 | + if set_ports: |
442 | + map_ports[service] = set_ports |
443 | + return map_ports, ports_states |
444 | + |
445 | + |
446 | +def _check_listening_on_ports_list(ports): |
447 | + """Check that the ports list given are being listened to |
448 | + |
449 | + Returns a list of ports being listened to and a list of the |
450 | + booleans. |
451 | + |
452 | + @param ports: LIST or port numbers. |
453 | + @returns [(port_num, boolean), ...], [boolean] |
454 | + """ |
455 | + ports_open = [port_has_listener('0.0.0.0', p) for p in ports] |
456 | + return zip(ports, ports_open), ports_open |
457 | + |
458 | + |
459 | +def _filter_tuples(services_states, state): |
460 | + """Return a simple list from a list of tuples according to the condition |
461 | + |
462 | + @param services_states: LIST of (string, boolean): service and running |
463 | + state. |
464 | + @param state: Boolean to match the tuple against. |
465 | + @returns [LIST of strings] that matched the tuple RHS. |
466 | + """ |
467 | + return [s for s, b in services_states if b == state] |
468 | |
469 | |
470 | def workload_state_compare(current_workload_state, workload_state): |
471 | @@ -1046,8 +1235,7 @@ |
472 | |
473 | |
474 | def incomplete_relation_data(configs, required_interfaces): |
475 | - """ |
476 | - Check complete contexts against required_interfaces |
477 | + """Check complete contexts against required_interfaces |
478 | Return dictionary of incomplete relation data. |
479 | |
480 | configs is an OSConfigRenderer object with configs registered |
481 | @@ -1072,19 +1260,13 @@ |
482 | 'shared-db': {'related': True}}} |
483 | """ |
484 | complete_ctxts = configs.complete_contexts() |
485 | - incomplete_relations = [] |
486 | - for svc_type in required_interfaces.keys(): |
487 | - # Avoid duplicates |
488 | - found_ctxt = False |
489 | - for interface in required_interfaces[svc_type]: |
490 | - if interface in complete_ctxts: |
491 | - found_ctxt = True |
492 | - if not found_ctxt: |
493 | - incomplete_relations.append(svc_type) |
494 | - incomplete_context_data = {} |
495 | - for i in incomplete_relations: |
496 | - incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i]) |
497 | - return incomplete_context_data |
498 | + incomplete_relations = [ |
499 | + svc_type |
500 | + for svc_type, interfaces in required_interfaces.items() |
501 | + if not set(interfaces).intersection(complete_ctxts)] |
502 | + return { |
503 | + i: configs.get_incomplete_context_data(required_interfaces[i]) |
504 | + for i in incomplete_relations} |
505 | |
506 | |
507 | def do_action_openstack_upgrade(package, upgrade_callback, configs): |
508 | @@ -1145,3 +1327,242 @@ |
509 | relation_set(relation_id=rid, |
510 | relation_settings=trigger, |
511 | ) |
512 | + |
513 | + |
514 | +def check_actually_paused(services=None, ports=None): |
515 | + """Check that services listed in the services object and and ports |
516 | + are actually closed (not listened to), to verify that the unit is |
517 | + properly paused. |
518 | + |
519 | + @param services: See _extract_services_list_helper |
520 | + @returns status, : string for status (None if okay) |
521 | + message : string for problem for status_set |
522 | + """ |
523 | + state = None |
524 | + message = None |
525 | + messages = [] |
526 | + if services is not None: |
527 | + services = _extract_services_list_helper(services) |
528 | + services_running, services_states = _check_running_services(services) |
529 | + if any(services_states): |
530 | + # there shouldn't be any running so this is a problem |
531 | + messages.append("these services running: {}" |
532 | + .format(", ".join( |
533 | + _filter_tuples(services_running, True)))) |
534 | + state = "blocked" |
535 | + ports_open, ports_open_bools = ( |
536 | + _check_listening_on_services_ports(services, True)) |
537 | + if any(ports_open_bools): |
538 | + message_parts = {service: ", ".join([str(v) for v in open_ports]) |
539 | + for service, open_ports in ports_open.items()} |
540 | + message = ", ".join( |
541 | + ["{}: [{}]".format(s, sp) for s, sp in message_parts.items()]) |
542 | + messages.append( |
543 | + "these service:ports are open: {}".format(message)) |
544 | + state = 'blocked' |
545 | + if ports is not None: |
546 | + ports_open, bools = _check_listening_on_ports_list(ports) |
547 | + if any(bools): |
548 | + messages.append( |
549 | + "these ports which should be closed, but are open: {}" |
550 | + .format(", ".join([str(p) for p, v in ports_open if v]))) |
551 | + state = 'blocked' |
552 | + if messages: |
553 | + message = ("Services should be paused but {}" |
554 | + .format(", ".join(messages))) |
555 | + return state, message |
556 | + |
557 | + |
558 | +def set_unit_paused(): |
559 | + """Set the unit to a paused state in the local kv() store. |
560 | + This does NOT actually pause the unit |
561 | + """ |
562 | + with unitdata.HookData()() as kv: |
563 | + kv.set('unit-paused', True) |
564 | + |
565 | + |
566 | +def clear_unit_paused(): |
567 | + """Clear the unit from a paused state in the local kv() store |
568 | + This does NOT actually restart any services - it only clears the |
569 | + local state. |
570 | + """ |
571 | + with unitdata.HookData()() as kv: |
572 | + kv.set('unit-paused', False) |
573 | + |
574 | + |
575 | +def is_unit_paused_set(): |
576 | + """Return the state of the kv().get('unit-paused'). |
577 | + This does NOT verify that the unit really is paused. |
578 | + |
579 | + To help with units that don't have HookData() (testing) |
580 | + if it excepts, return False |
581 | + """ |
582 | + try: |
583 | + with unitdata.HookData()() as kv: |
584 | + # transform something truth-y into a Boolean. |
585 | + return not(not(kv.get('unit-paused'))) |
586 | + except: |
587 | + return False |
588 | + |
589 | + |
590 | +def pause_unit(assess_status_func, services=None, ports=None, |
591 | + charm_func=None): |
592 | + """Pause a unit by stopping the services and setting 'unit-paused' |
593 | + in the local kv() store. |
594 | + |
595 | + Also checks that the services have stopped and ports are no longer |
596 | + being listened to. |
597 | + |
598 | + An optional charm_func() can be called that can either raise an |
599 | + Exception or return non None, None to indicate that the unit |
600 | + didn't pause cleanly. |
601 | + |
602 | + The signature for charm_func is: |
603 | + charm_func() -> message: string |
604 | + |
605 | + charm_func() is executed after any services are stopped, if supplied. |
606 | + |
607 | + The services object can either be: |
608 | + - None : no services were passed (an empty dict is returned) |
609 | + - a list of strings |
610 | + - A dictionary (optionally OrderedDict) {service_name: {'service': ..}} |
611 | + - An array of [{'service': service_name, ...}, ...] |
612 | + |
613 | + @param assess_status_func: (f() -> message: string | None) or None |
614 | + @param services: OPTIONAL see above |
615 | + @param ports: OPTIONAL list of port |
616 | + @param charm_func: function to run for custom charm pausing. |
617 | + @returns None |
618 | + @raises Exception(message) on an error for action_fail(). |
619 | + """ |
620 | + services = _extract_services_list_helper(services) |
621 | + messages = [] |
622 | + if services: |
623 | + for service in services.keys(): |
624 | + stopped = service_pause(service) |
625 | + if not stopped: |
626 | + messages.append("{} didn't stop cleanly.".format(service)) |
627 | + if charm_func: |
628 | + try: |
629 | + message = charm_func() |
630 | + if message: |
631 | + messages.append(message) |
632 | + except Exception as e: |
633 | + message.append(str(e)) |
634 | + set_unit_paused() |
635 | + if assess_status_func: |
636 | + message = assess_status_func() |
637 | + if message: |
638 | + messages.append(message) |
639 | + if messages: |
640 | + raise Exception("Couldn't pause: {}".format("; ".join(messages))) |
641 | + |
642 | + |
643 | +def resume_unit(assess_status_func, services=None, ports=None, |
644 | + charm_func=None): |
645 | + """Resume a unit by starting the services and clearning 'unit-paused' |
646 | + in the local kv() store. |
647 | + |
648 | + Also checks that the services have started and ports are being listened to. |
649 | + |
650 | + An optional charm_func() can be called that can either raise an |
651 | + Exception or return non None to indicate that the unit |
652 | + didn't resume cleanly. |
653 | + |
654 | + The signature for charm_func is: |
655 | + charm_func() -> message: string |
656 | + |
657 | + charm_func() is executed after any services are started, if supplied. |
658 | + |
659 | + The services object can either be: |
660 | + - None : no services were passed (an empty dict is returned) |
661 | + - a list of strings |
662 | + - A dictionary (optionally OrderedDict) {service_name: {'service': ..}} |
663 | + - An array of [{'service': service_name, ...}, ...] |
664 | + |
665 | + @param assess_status_func: (f() -> message: string | None) or None |
666 | + @param services: OPTIONAL see above |
667 | + @param ports: OPTIONAL list of port |
668 | + @param charm_func: function to run for custom charm resuming. |
669 | + @returns None |
670 | + @raises Exception(message) on an error for action_fail(). |
671 | + """ |
672 | + services = _extract_services_list_helper(services) |
673 | + messages = [] |
674 | + if services: |
675 | + for service in services.keys(): |
676 | + started = service_resume(service) |
677 | + if not started: |
678 | + messages.append("{} didn't start cleanly.".format(service)) |
679 | + if charm_func: |
680 | + try: |
681 | + message = charm_func() |
682 | + if message: |
683 | + messages.append(message) |
684 | + except Exception as e: |
685 | + message.append(str(e)) |
686 | + clear_unit_paused() |
687 | + if assess_status_func: |
688 | + message = assess_status_func() |
689 | + if message: |
690 | + messages.append(message) |
691 | + if messages: |
692 | + raise Exception("Couldn't resume: {}".format("; ".join(messages))) |
693 | + |
694 | + |
695 | +def make_assess_status_func(*args, **kwargs): |
696 | + """Creates an assess_status_func() suitable for handing to pause_unit() |
697 | + and resume_unit(). |
698 | + |
699 | + This uses the _determine_os_workload_status(...) function to determine |
700 | + what the workload_status should be for the unit. If the unit is |
701 | + not in maintenance or active states, then the message is returned to |
702 | + the caller. This is so an action that doesn't result in either a |
703 | + complete pause or complete resume can signal failure with an action_fail() |
704 | + """ |
705 | + def _assess_status_func(): |
706 | + state, message = _determine_os_workload_status(*args, **kwargs) |
707 | + status_set(state, message) |
708 | + if state not in ['maintenance', 'active']: |
709 | + return message |
710 | + return None |
711 | + |
712 | + return _assess_status_func |
713 | + |
714 | + |
715 | +def pausable_restart_on_change(restart_map, stopstart=False): |
716 | + """A restart_on_change decorator that checks to see if the unit is |
717 | + paused. If it is paused then the decorated function doesn't fire. |
718 | + |
719 | + This is provided as a helper, as the @restart_on_change(...) decorator |
720 | + is in core.host, yet the openstack specific helpers are in this file |
721 | + (contrib.openstack.utils). Thus, this needs to be an optional feature |
722 | + for openstack charms (or charms that wish to use the openstack |
723 | + pause/resume type features). |
724 | + |
725 | + It is used as follows: |
726 | + |
727 | + from contrib.openstack.utils import ( |
728 | + pausable_restart_on_change as restart_on_change) |
729 | + |
730 | + @restart_on_change(restart_map, stopstart=<boolean>) |
731 | + def some_hook(...): |
732 | + pass |
733 | + |
734 | + see core.utils.restart_on_change() for more details. |
735 | + |
736 | + @param f: the function to decorate |
737 | + @param restart_map: the restart map {conf_file: [services]} |
738 | + @param stopstart: DEFAULT false; whether to stop, start or just restart |
739 | + @returns decorator to use a restart_on_change with pausability |
740 | + """ |
741 | + def wrap(f): |
742 | + @functools.wraps(f) |
743 | + def wrapped_f(*args, **kwargs): |
744 | + if is_unit_paused_set(): |
745 | + return f(*args, **kwargs) |
746 | + # otherwise, normal restart_on_change functionality |
747 | + return restart_on_change_helper( |
748 | + (lambda: f(*args, **kwargs)), restart_map, stopstart) |
749 | + return wrapped_f |
750 | + return wrap |
751 | |
752 | === modified file 'charmhelpers/core/host.py' |
753 | --- charmhelpers/core/host.py 2016-01-19 21:53:13 +0000 |
754 | +++ charmhelpers/core/host.py 2016-03-07 16:45:36 +0000 |
755 | @@ -30,6 +30,8 @@ |
756 | import string |
757 | import subprocess |
758 | import hashlib |
759 | +import functools |
760 | +import itertools |
761 | from contextlib import contextmanager |
762 | from collections import OrderedDict |
763 | |
764 | @@ -428,27 +430,47 @@ |
765 | restarted if any file matching the pattern got changed, created |
766 | or removed. Standard wildcards are supported, see documentation |
767 | for the 'glob' module for more information. |
768 | + |
769 | + @param restart_map: {path_file_name: [service_name, ...] |
770 | + @param stopstart: DEFAULT false; whether to stop, start OR restart |
771 | + @returns result from decorated function |
772 | """ |
773 | def wrap(f): |
774 | + @functools.wraps(f) |
775 | def wrapped_f(*args, **kwargs): |
776 | - checksums = {path: path_hash(path) for path in restart_map} |
777 | - f(*args, **kwargs) |
778 | - restarts = [] |
779 | - for path in restart_map: |
780 | - if path_hash(path) != checksums[path]: |
781 | - restarts += restart_map[path] |
782 | - services_list = list(OrderedDict.fromkeys(restarts)) |
783 | - if not stopstart: |
784 | - for service_name in services_list: |
785 | - service('restart', service_name) |
786 | - else: |
787 | - for action in ['stop', 'start']: |
788 | - for service_name in services_list: |
789 | - service(action, service_name) |
790 | + return restart_on_change_helper( |
791 | + (lambda: f(*args, **kwargs)), restart_map, stopstart) |
792 | return wrapped_f |
793 | return wrap |
794 | |
795 | |
796 | +def restart_on_change_helper(lambda_f, restart_map, stopstart=False): |
797 | + """Helper function to perform the restart_on_change function. |
798 | + |
799 | + This is provided for decorators to restart services if files described |
800 | + in the restart_map have changed after an invocation of lambda_f(). |
801 | + |
802 | + @param lambda_f: function to call. |
803 | + @param restart_map: {file: [service, ...]} |
804 | + @param stopstart: whether to stop, start or restart a service |
805 | + @returns result of lambda_f() |
806 | + """ |
807 | + checksums = {path: path_hash(path) for path in restart_map} |
808 | + r = lambda_f() |
809 | + # create a list of lists of the services to restart |
810 | + restarts = [restart_map[path] |
811 | + for path in restart_map |
812 | + if path_hash(path) != checksums[path]] |
813 | + # create a flat list of ordered services without duplicates from lists |
814 | + services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts))) |
815 | + if services_list: |
816 | + actions = ('stop', 'start') if stopstart else ('restart',) |
817 | + for action in actions: |
818 | + for service_name in services_list: |
819 | + service(action, service_name) |
820 | + return r |
821 | + |
822 | + |
823 | def lsb_release(): |
824 | """Return /etc/lsb-release in a dict""" |
825 | d = {} |
826 | |
827 | === modified file 'tests/contrib/openstack/test_openstack_utils.py' |
828 | --- tests/contrib/openstack/test_openstack_utils.py 2016-02-17 16:29:13 +0000 |
829 | +++ tests/contrib/openstack/test_openstack_utils.py 2016-03-07 16:45:36 +0000 |
830 | @@ -919,7 +919,10 @@ |
831 | |
832 | @patch.object(openstack, 'juju_log') |
833 | @patch('charmhelpers.contrib.openstack.utils.status_set') |
834 | - def test_set_os_workload_status_complete(self, status_set, log): |
835 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
836 | + return_value=False) |
837 | + def test_set_os_workload_status_complete( |
838 | + self, is_unit_paused_set, status_set, log): |
839 | configs = MagicMock() |
840 | configs.complete_contexts.return_value = ['shared-db', |
841 | 'amqp', |
842 | @@ -936,9 +939,11 @@ |
843 | @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data', |
844 | return_value={'identity': {'identity-service': {'related': True}}}) |
845 | @patch('charmhelpers.contrib.openstack.utils.status_set') |
846 | - def test_set_os_workload_status_related_incomplete(self, status_set, |
847 | - incomplete_relation_data, |
848 | - log): |
849 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
850 | + return_value=False) |
851 | + def test_set_os_workload_status_related_incomplete( |
852 | + self, is_unit_paused_set, status_set, |
853 | + incomplete_relation_data, log): |
854 | configs = MagicMock() |
855 | configs.complete_contexts.return_value = ['shared-db', 'amqp'] |
856 | required_interfaces = { |
857 | @@ -954,8 +959,11 @@ |
858 | @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data', |
859 | return_value={'identity': {'identity-service': {'related': False}}}) |
860 | @patch('charmhelpers.contrib.openstack.utils.status_set') |
861 | - def test_set_os_workload_status_absent(self, status_set, |
862 | - incomplete_relation_data, log): |
863 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
864 | + return_value=False) |
865 | + def test_set_os_workload_status_absent( |
866 | + self, is_unit_paused_set, status_set, |
867 | + incomplete_relation_data, log): |
868 | configs = MagicMock() |
869 | configs.complete_contexts.return_value = ['shared-db', 'amqp'] |
870 | required_interfaces = { |
871 | @@ -973,9 +981,11 @@ |
872 | @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data', |
873 | return_value={'identity': {'identity-service': {'related': True}}}) |
874 | @patch('charmhelpers.contrib.openstack.utils.status_set') |
875 | - def test_set_os_workload_status_related_broken(self, status_set, |
876 | - incomplete_relation_data, |
877 | - hook_name, log): |
878 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
879 | + return_value=False) |
880 | + def test_set_os_workload_status_related_broken( |
881 | + self, is_unit_paused_set, status_set, |
882 | + incomplete_relation_data, hook_name, log): |
883 | configs = MagicMock() |
884 | configs.complete_contexts.return_value = ['shared-db', 'amqp'] |
885 | required_interfaces = { |
886 | @@ -1000,8 +1010,11 @@ |
887 | {'shared-db': {'related': False}} |
888 | }) |
889 | @patch('charmhelpers.contrib.openstack.utils.status_set') |
890 | - def test_set_os_workload_status_mixed(self, status_set, incomplete_relation_data, |
891 | - log): |
892 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
893 | + return_value=False) |
894 | + def test_set_os_workload_status_mixed( |
895 | + self, is_unit_paused_set, status_set, |
896 | + incomplete_relation_data, log): |
897 | configs = MagicMock() |
898 | configs.complete_contexts.return_value = ['shared-db', 'amqp'] |
899 | required_interfaces = { |
900 | @@ -1025,16 +1038,14 @@ |
901 | @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
902 | @patch.object(openstack, 'juju_log') |
903 | @patch('charmhelpers.contrib.openstack.utils.status_set') |
904 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
905 | + return_value=False) |
906 | def test_set_os_workload_status_complete_with_services_list( |
907 | - self, status_set, log, port_has_listener, service_running): |
908 | + self, is_unit_paused_set, status_set, log, |
909 | + port_has_listener, service_running): |
910 | configs = MagicMock() |
911 | - configs.complete_contexts.return_value = ['shared-db', |
912 | - 'amqp', |
913 | - 'identity-service'] |
914 | - required_interfaces = { |
915 | - 'database': ['shared-db', 'pgsql-db'], |
916 | - 'message': ['amqp', 'zeromq-configuration'], |
917 | - 'identity': ['identity-service']} |
918 | + configs.complete_contexts.return_value = [] |
919 | + required_interfaces = {} |
920 | |
921 | services = ['database', 'identity'] |
922 | # Assume that the service and ports are open. |
923 | @@ -1049,16 +1060,14 @@ |
924 | @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
925 | @patch.object(openstack, 'juju_log') |
926 | @patch('charmhelpers.contrib.openstack.utils.status_set') |
927 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
928 | + return_value=False) |
929 | def test_set_os_workload_status_complete_services_list_not_running( |
930 | - self, status_set, log, port_has_listener, service_running): |
931 | + self, is_unit_paused_set, status_set, log, |
932 | + port_has_listener, service_running): |
933 | configs = MagicMock() |
934 | - configs.complete_contexts.return_value = ['shared-db', |
935 | - 'amqp', |
936 | - 'identity-service'] |
937 | - required_interfaces = { |
938 | - 'database': ['shared-db', 'pgsql-db'], |
939 | - 'message': ['amqp', 'zeromq-configuration'], |
940 | - 'identity': ['identity-service']} |
941 | + configs.complete_contexts.return_value = [] |
942 | + required_interfaces = {} |
943 | |
944 | services = ['database', 'identity'] |
945 | port_has_listener.return_value = True |
946 | @@ -1075,16 +1084,14 @@ |
947 | @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
948 | @patch.object(openstack, 'juju_log') |
949 | @patch('charmhelpers.contrib.openstack.utils.status_set') |
950 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
951 | + return_value=False) |
952 | def test_set_os_workload_status_complete_with_services( |
953 | - self, status_set, log, port_has_listener, service_running): |
954 | + self, is_unit_paused_set, status_set, log, |
955 | + port_has_listener, service_running): |
956 | configs = MagicMock() |
957 | - configs.complete_contexts.return_value = ['shared-db', |
958 | - 'amqp', |
959 | - 'identity-service'] |
960 | - required_interfaces = { |
961 | - 'database': ['shared-db', 'pgsql-db'], |
962 | - 'message': ['amqp', 'zeromq-configuration'], |
963 | - 'identity': ['identity-service']} |
964 | + configs.complete_contexts.return_value = [] |
965 | + required_interfaces = {} |
966 | |
967 | services = [ |
968 | {'service': 'database', 'ports': [10, 20]}, |
969 | @@ -1102,16 +1109,14 @@ |
970 | @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
971 | @patch.object(openstack, 'juju_log') |
972 | @patch('charmhelpers.contrib.openstack.utils.status_set') |
973 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
974 | + return_value=False) |
975 | def test_set_os_workload_status_complete_service_not_running( |
976 | - self, status_set, log, port_has_listener, service_running): |
977 | + self, is_unit_paused_set, status_set, log, |
978 | + port_has_listener, service_running): |
979 | configs = MagicMock() |
980 | - configs.complete_contexts.return_value = ['shared-db', |
981 | - 'amqp', |
982 | - 'identity-service'] |
983 | - required_interfaces = { |
984 | - 'database': ['shared-db', 'pgsql-db'], |
985 | - 'message': ['amqp', 'zeromq-configuration'], |
986 | - 'identity': ['identity-service']} |
987 | + configs.complete_contexts.return_value = [] |
988 | + required_interfaces = {} |
989 | |
990 | services = [ |
991 | {'service': 'database', 'ports': [10, 20]}, |
992 | @@ -1131,16 +1136,14 @@ |
993 | @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
994 | @patch.object(openstack, 'juju_log') |
995 | @patch('charmhelpers.contrib.openstack.utils.status_set') |
996 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
997 | + return_value=False) |
998 | def test_set_os_workload_status_complete_port_not_open( |
999 | - self, status_set, log, port_has_listener, service_running): |
1000 | + self, is_unit_paused_set, status_set, log, |
1001 | + port_has_listener, service_running): |
1002 | configs = MagicMock() |
1003 | - configs.complete_contexts.return_value = ['shared-db', |
1004 | - 'amqp', |
1005 | - 'identity-service'] |
1006 | - required_interfaces = { |
1007 | - 'database': ['shared-db', 'pgsql-db'], |
1008 | - 'message': ['amqp', 'zeromq-configuration'], |
1009 | - 'identity': ['identity-service']} |
1010 | + configs.complete_contexts.return_value = [] |
1011 | + required_interfaces = {} |
1012 | |
1013 | services = [ |
1014 | {'service': 'database', 'ports': [10, 20]}, |
1015 | @@ -1160,16 +1163,13 @@ |
1016 | @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1017 | @patch.object(openstack, 'juju_log') |
1018 | @patch('charmhelpers.contrib.openstack.utils.status_set') |
1019 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
1020 | + return_value=False) |
1021 | def test_set_os_workload_status_complete_ports_not_open( |
1022 | - self, status_set, log, port_has_listener): |
1023 | + self, is_unit_paused_set, status_set, log, port_has_listener): |
1024 | configs = MagicMock() |
1025 | - configs.complete_contexts.return_value = ['shared-db', |
1026 | - 'amqp', |
1027 | - 'identity-service'] |
1028 | - required_interfaces = { |
1029 | - 'database': ['shared-db', 'pgsql-db'], |
1030 | - 'message': ['amqp', 'zeromq-configuration'], |
1031 | - 'identity': ['identity-service']} |
1032 | + configs.complete_contexts.return_value = [] |
1033 | + required_interfaces = {} |
1034 | |
1035 | ports = [50, 60, 70] |
1036 | port_has_listener.side_effect = [True, False, True] |
1037 | @@ -1181,6 +1181,422 @@ |
1038 | 'Ports which should be open, but are not: 60') |
1039 | |
1040 | @patch.object(openstack, 'juju_log') |
1041 | + @patch('charmhelpers.contrib.openstack.utils.status_set') |
1042 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
1043 | + return_value=True) |
1044 | + def test_set_os_workload_status_paused_simple( |
1045 | + self, is_unit_paused_set, status_set, log): |
1046 | + configs = MagicMock() |
1047 | + configs.complete_contexts.return_value = [] |
1048 | + required_interfaces = {} |
1049 | + |
1050 | + openstack.set_os_workload_status(configs, required_interfaces) |
1051 | + status_set.assert_called_with( |
1052 | + 'maintenance', |
1053 | + "Paused. Use 'resume' action to resume normal service.") |
1054 | + |
1055 | + @patch('charmhelpers.contrib.openstack.utils.service_running') |
1056 | + @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1057 | + @patch.object(openstack, 'juju_log') |
1058 | + @patch('charmhelpers.contrib.openstack.utils.status_set') |
1059 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
1060 | + return_value=True) |
1061 | + def test_set_os_workload_status_paused_services_check( |
1062 | + self, is_unit_paused_set, status_set, log, |
1063 | + port_has_listener, service_running): |
1064 | + configs = MagicMock() |
1065 | + configs.complete_contexts.return_value = [] |
1066 | + required_interfaces = {} |
1067 | + |
1068 | + services = [ |
1069 | + {'service': 'database', 'ports': [10, 20]}, |
1070 | + {'service': 'identity', 'ports': [30]}, |
1071 | + ] |
1072 | + port_has_listener.return_value = False |
1073 | + service_running.side_effect = [False, False] |
1074 | + |
1075 | + openstack.set_os_workload_status( |
1076 | + configs, required_interfaces, services=services) |
1077 | + status_set.assert_called_with( |
1078 | + 'maintenance', |
1079 | + "Paused. Use 'resume' action to resume normal service.") |
1080 | + |
1081 | + @patch('charmhelpers.contrib.openstack.utils.service_running') |
1082 | + @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1083 | + @patch.object(openstack, 'juju_log') |
1084 | + @patch('charmhelpers.contrib.openstack.utils.status_set') |
1085 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
1086 | + return_value=True) |
1087 | + def test_set_os_workload_status_paused_services_fail( |
1088 | + self, is_unit_paused_set, status_set, log, |
1089 | + port_has_listener, service_running): |
1090 | + configs = MagicMock() |
1091 | + configs.complete_contexts.return_value = [] |
1092 | + required_interfaces = {} |
1093 | + |
1094 | + services = [ |
1095 | + {'service': 'database', 'ports': [10, 20]}, |
1096 | + {'service': 'identity', 'ports': [30]}, |
1097 | + ] |
1098 | + port_has_listener.return_value = False |
1099 | + # Fail the identity service |
1100 | + service_running.side_effect = [False, True] |
1101 | + |
1102 | + openstack.set_os_workload_status( |
1103 | + configs, required_interfaces, services=services) |
1104 | + status_set.assert_called_with( |
1105 | + 'blocked', |
1106 | + "Services should be paused but these services running: identity") |
1107 | + |
1108 | + @patch('charmhelpers.contrib.openstack.utils.service_running') |
1109 | + @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1110 | + @patch.object(openstack, 'juju_log') |
1111 | + @patch('charmhelpers.contrib.openstack.utils.status_set') |
1112 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
1113 | + return_value=True) |
1114 | + def test_set_os_workload_status_paused_services_ports_fail( |
1115 | + self, is_unit_paused_set, status_set, log, |
1116 | + port_has_listener, service_running): |
1117 | + configs = MagicMock() |
1118 | + configs.complete_contexts.return_value = [] |
1119 | + required_interfaces = {} |
1120 | + |
1121 | + services = [ |
1122 | + {'service': 'database', 'ports': [10, 20]}, |
1123 | + {'service': 'identity', 'ports': [30]}, |
1124 | + ] |
1125 | + # make the service 20 port be still listening. |
1126 | + port_has_listener.side_effect = [False, True, False] |
1127 | + service_running.return_value = False |
1128 | + |
1129 | + openstack.set_os_workload_status( |
1130 | + configs, required_interfaces, services=services) |
1131 | + status_set.assert_called_with( |
1132 | + 'blocked', |
1133 | + "Services should be paused but these service:ports are open:" |
1134 | + " database: [20]") |
1135 | + |
1136 | + @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1137 | + @patch.object(openstack, 'juju_log') |
1138 | + @patch('charmhelpers.contrib.openstack.utils.status_set') |
1139 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
1140 | + return_value=True) |
1141 | + def test_set_os_workload_status_paused_ports_check( |
1142 | + self, is_unit_paused_set, status_set, log, |
1143 | + port_has_listener): |
1144 | + configs = MagicMock() |
1145 | + configs.complete_contexts.return_value = [] |
1146 | + required_interfaces = {} |
1147 | + |
1148 | + ports = [50, 60, 70] |
1149 | + port_has_listener.side_effect = [False, False, False] |
1150 | + |
1151 | + openstack.set_os_workload_status( |
1152 | + configs, required_interfaces, ports=ports) |
1153 | + status_set.assert_called_with( |
1154 | + 'maintenance', |
1155 | + "Paused. Use 'resume' action to resume normal service.") |
1156 | + |
1157 | + @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1158 | + @patch.object(openstack, 'juju_log') |
1159 | + @patch('charmhelpers.contrib.openstack.utils.status_set') |
1160 | + @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set', |
1161 | + return_value=True) |
1162 | + def test_set_os_workload_status_paused_ports_fail( |
1163 | + self, is_unit_paused_set, status_set, log, |
1164 | + port_has_listener): |
1165 | + configs = MagicMock() |
1166 | + configs.complete_contexts.return_value = [] |
1167 | + required_interfaces = {} |
1168 | + |
1169 | + # fail port 70 to make it seem to be running |
1170 | + ports = [50, 60, 70] |
1171 | + port_has_listener.side_effect = [False, False, True] |
1172 | + |
1173 | + openstack.set_os_workload_status( |
1174 | + configs, required_interfaces, ports=ports) |
1175 | + status_set.assert_called_with( |
1176 | + 'blocked', |
1177 | + "Services should be paused but " |
1178 | + "these ports which should be closed, but are open: 70") |
1179 | + |
1180 | + @patch('charmhelpers.contrib.openstack.utils.service_running') |
1181 | + @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1182 | + def test_check_actually_paused_simple_services( |
1183 | + self, port_has_listener, service_running): |
1184 | + services = ['database', 'identity'] |
1185 | + port_has_listener.return_value = False |
1186 | + service_running.return_value = False |
1187 | + |
1188 | + state, message = openstack.check_actually_paused( |
1189 | + services) |
1190 | + self.assertEquals(state, None) |
1191 | + self.assertEquals(message, None) |
1192 | + |
1193 | + @patch('charmhelpers.contrib.openstack.utils.service_running') |
1194 | + @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1195 | + def test_check_actually_paused_simple_services_fail( |
1196 | + self, port_has_listener, service_running): |
1197 | + services = ['database', 'identity'] |
1198 | + port_has_listener.return_value = False |
1199 | + service_running.side_effect = [False, True] |
1200 | + |
1201 | + state, message = openstack.check_actually_paused( |
1202 | + services) |
1203 | + self.assertEquals(state, 'blocked') |
1204 | + self.assertEquals( |
1205 | + message, |
1206 | + "Services should be paused but these services running: identity") |
1207 | + |
1208 | + @patch('charmhelpers.contrib.openstack.utils.service_running') |
1209 | + @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1210 | + def test_check_actually_paused_services_dict( |
1211 | + self, port_has_listener, service_running): |
1212 | + services = [ |
1213 | + {'service': 'database', 'ports': [10, 20]}, |
1214 | + {'service': 'identity', 'ports': [30]}, |
1215 | + ] |
1216 | + # Assume that the service and ports are open. |
1217 | + port_has_listener.return_value = False |
1218 | + service_running.return_value = False |
1219 | + |
1220 | + state, message = openstack.check_actually_paused( |
1221 | + services) |
1222 | + self.assertEquals(state, None) |
1223 | + self.assertEquals(message, None) |
1224 | + |
1225 | + @patch('charmhelpers.contrib.openstack.utils.service_running') |
1226 | + @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1227 | + def test_check_actually_paused_services_dict_fail( |
1228 | + self, port_has_listener, service_running): |
1229 | + services = [ |
1230 | + {'service': 'database', 'ports': [10, 20]}, |
1231 | + {'service': 'identity', 'ports': [30]}, |
1232 | + ] |
1233 | + # Assume that the service and ports are open. |
1234 | + port_has_listener.return_value = False |
1235 | + service_running.side_effect = [False, True] |
1236 | + |
1237 | + state, message = openstack.check_actually_paused( |
1238 | + services) |
1239 | + self.assertEquals(state, 'blocked') |
1240 | + self.assertEquals( |
1241 | + message, |
1242 | + "Services should be paused but these services running: identity") |
1243 | + |
1244 | + @patch('charmhelpers.contrib.openstack.utils.service_running') |
1245 | + @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1246 | + def test_check_actually_paused_services_dict_ports_fail( |
1247 | + self, port_has_listener, service_running): |
1248 | + services = [ |
1249 | + {'service': 'database', 'ports': [10, 20]}, |
1250 | + {'service': 'identity', 'ports': [30]}, |
1251 | + ] |
1252 | + # Assume that the service and ports are open. |
1253 | + port_has_listener.side_effect = [False, True, False] |
1254 | + service_running.return_value = False |
1255 | + |
1256 | + state, message = openstack.check_actually_paused( |
1257 | + services) |
1258 | + self.assertEquals(state, 'blocked') |
1259 | + self.assertEquals(message, |
1260 | + 'Services should be paused but these service:ports' |
1261 | + ' are open: database: [20]') |
1262 | + |
1263 | + @patch('charmhelpers.contrib.openstack.utils.service_running') |
1264 | + @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1265 | + def test_check_actually_paused_ports_okay( |
1266 | + self, port_has_listener, service_running): |
1267 | + port_has_listener.side_effect = [False, False, False] |
1268 | + service_running.return_value = False |
1269 | + ports = [50, 60, 70] |
1270 | + |
1271 | + state, message = openstack.check_actually_paused( |
1272 | + ports=ports) |
1273 | + self.assertEquals(state, None) |
1274 | + self.assertEquals(state, None) |
1275 | + |
1276 | + @patch('charmhelpers.contrib.openstack.utils.service_running') |
1277 | + @patch('charmhelpers.contrib.openstack.utils.port_has_listener') |
1278 | + def test_check_actually_paused_ports_fail( |
1279 | + self, port_has_listener, service_running): |
1280 | + port_has_listener.side_effect = [False, True, False] |
1281 | + service_running.return_value = False |
1282 | + ports = [50, 60, 70] |
1283 | + |
1284 | + state, message = openstack.check_actually_paused( |
1285 | + ports=ports) |
1286 | + self.assertEquals(state, 'blocked') |
1287 | + self.assertEquals(message, |
1288 | + 'Services should be paused but these ports ' |
1289 | + 'which should be closed, but are open: 60') |
1290 | + |
1291 | + @patch('charmhelpers.contrib.openstack.utils.service_pause') |
1292 | + @patch('charmhelpers.contrib.openstack.utils.set_unit_paused') |
1293 | + def test_pause_unit_okay(self, set_unit_paused, service_pause): |
1294 | + services = ['service1', 'service2'] |
1295 | + service_pause.side_effect = [True, True] |
1296 | + openstack.pause_unit(None, services=services) |
1297 | + set_unit_paused.assert_called_once_with() |
1298 | + self.assertEquals(service_pause.call_count, 2) |
1299 | + |
1300 | + @patch('charmhelpers.contrib.openstack.utils.service_pause') |
1301 | + @patch('charmhelpers.contrib.openstack.utils.set_unit_paused') |
1302 | + def test_pause_unit_service_fails(self, set_unit_paused, service_pause): |
1303 | + services = ['service1', 'service2'] |
1304 | + service_pause.side_effect = [True, True] |
1305 | + openstack.pause_unit(None, services=services) |
1306 | + set_unit_paused.assert_called_once_with() |
1307 | + self.assertEquals(service_pause.call_count, 2) |
1308 | + # Fail the 2nd service |
1309 | + service_pause.side_effect = [True, False] |
1310 | + try: |
1311 | + openstack.pause_unit(None, services=services) |
1312 | + raise Exception("pause_unit should have raised Exception") |
1313 | + except Exception as e: |
1314 | + self.assertEquals(e.args[0], |
1315 | + "Couldn't pause: service2 didn't stop cleanly.") |
1316 | + |
1317 | + @patch('charmhelpers.contrib.openstack.utils.service_pause') |
1318 | + @patch('charmhelpers.contrib.openstack.utils.set_unit_paused') |
1319 | + def test_pausee_unit_service_charm_func( |
1320 | + self, set_unit_paused, service_pause): |
1321 | + services = ['service1', 'service2'] |
1322 | + service_pause.return_value = True |
1323 | + charm_func = MagicMock() |
1324 | + charm_func.return_value = None |
1325 | + openstack.pause_unit(None, services=services, charm_func=charm_func) |
1326 | + charm_func.assert_called_once_with() |
1327 | + # fail the charm_func |
1328 | + charm_func.return_value = "Custom charm failed" |
1329 | + try: |
1330 | + openstack.pause_unit( |
1331 | + None, services=services, charm_func=charm_func) |
1332 | + raise Exception("pause_unit should have raised Exception") |
1333 | + except Exception as e: |
1334 | + self.assertEquals(e.args[0], |
1335 | + "Couldn't pause: Custom charm failed") |
1336 | + |
1337 | + @patch('charmhelpers.contrib.openstack.utils.service_pause') |
1338 | + @patch('charmhelpers.contrib.openstack.utils.set_unit_paused') |
1339 | + def test_pause_unit_assess_status_func( |
1340 | + self, set_unit_paused, service_pause): |
1341 | + services = ['service1', 'service2'] |
1342 | + service_pause.return_value = True |
1343 | + assess_status_func = MagicMock() |
1344 | + assess_status_func.return_value = None |
1345 | + openstack.pause_unit(assess_status_func, services=services) |
1346 | + assess_status_func.assert_called_once_with() |
1347 | + # fail the assess_status_func |
1348 | + assess_status_func.return_value = "assess_status_func failed" |
1349 | + try: |
1350 | + openstack.pause_unit(assess_status_func, services=services) |
1351 | + raise Exception("pause_unit should have raised Exception") |
1352 | + except Exception as e: |
1353 | + self.assertEquals(e.args[0], |
1354 | + "Couldn't pause: assess_status_func failed") |
1355 | + |
1356 | + @patch('charmhelpers.contrib.openstack.utils.service_resume') |
1357 | + @patch('charmhelpers.contrib.openstack.utils.clear_unit_paused') |
1358 | + def test_resume_unit_okay(self, clear_unit_paused, service_resume): |
1359 | + services = ['service1', 'service2'] |
1360 | + service_resume.side_effect = [True, True] |
1361 | + openstack.resume_unit(None, services=services) |
1362 | + clear_unit_paused.assert_called_once_with() |
1363 | + self.assertEquals(service_resume.call_count, 2) |
1364 | + |
1365 | + @patch('charmhelpers.contrib.openstack.utils.service_resume') |
1366 | + @patch('charmhelpers.contrib.openstack.utils.clear_unit_paused') |
1367 | + def test_resume_unit_service_fails(self, clear_unit_paused, service_resume): |
1368 | + services = ['service1', 'service2'] |
1369 | + service_resume.side_effect = [True, True] |
1370 | + openstack.resume_unit(None, services=services) |
1371 | + clear_unit_paused.assert_called_once_with() |
1372 | + self.assertEquals(service_resume.call_count, 2) |
1373 | + # Fail the 2nd service |
1374 | + service_resume.side_effect = [True, False] |
1375 | + try: |
1376 | + openstack.resume_unit(None, services=services) |
1377 | + raise Exception("resume_unit should have raised Exception") |
1378 | + except Exception as e: |
1379 | + self.assertEquals(e.args[0], |
1380 | + "Couldn't resume: service2 didn't start cleanly.") |
1381 | + |
1382 | + @patch('charmhelpers.contrib.openstack.utils.service_resume') |
1383 | + @patch('charmhelpers.contrib.openstack.utils.clear_unit_paused') |
1384 | + def test_resume_unit_service_charm_func( |
1385 | + self, clear_unit_paused, service_resume): |
1386 | + services = ['service1', 'service2'] |
1387 | + service_resume.return_value = True |
1388 | + charm_func = MagicMock() |
1389 | + charm_func.return_value = None |
1390 | + openstack.resume_unit(None, services=services, charm_func=charm_func) |
1391 | + charm_func.assert_called_once_with() |
1392 | + # fail the charm_func |
1393 | + charm_func.return_value = "Custom charm failed" |
1394 | + try: |
1395 | + openstack.resume_unit( |
1396 | + None, services=services, charm_func=charm_func) |
1397 | + raise Exception("resume_unit should have raised Exception") |
1398 | + except Exception as e: |
1399 | + self.assertEquals(e.args[0], |
1400 | + "Couldn't resume: Custom charm failed") |
1401 | + |
1402 | + @patch('charmhelpers.contrib.openstack.utils.service_resume') |
1403 | + @patch('charmhelpers.contrib.openstack.utils.clear_unit_paused') |
1404 | + def test_resume_unit_assess_status_func( |
1405 | + self, clear_unit_paused, service_resume): |
1406 | + services = ['service1', 'service2'] |
1407 | + service_resume.return_value = True |
1408 | + assess_status_func = MagicMock() |
1409 | + assess_status_func.return_value = None |
1410 | + openstack.resume_unit(assess_status_func, services=services) |
1411 | + assess_status_func.assert_called_once_with() |
1412 | + # fail the assess_status_func |
1413 | + assess_status_func.return_value = "assess_status_func failed" |
1414 | + try: |
1415 | + openstack.resume_unit(assess_status_func, services=services) |
1416 | + raise Exception("resume_unit should have raised Exception") |
1417 | + except Exception as e: |
1418 | + self.assertEquals(e.args[0], |
1419 | + "Couldn't resume: assess_status_func failed") |
1420 | + |
1421 | + @patch('charmhelpers.contrib.openstack.utils.status_set') |
1422 | + @patch('charmhelpers.contrib.openstack.utils.' |
1423 | + '_determine_os_workload_status') |
1424 | + def test_make_assess_status_func(self, _determine_os_workload_status, |
1425 | + status_set): |
1426 | + _determine_os_workload_status.return_value = ('active', 'fine') |
1427 | + f = openstack.make_assess_status_func('one', 'two', three='three') |
1428 | + r = f() |
1429 | + self.assertEquals(r, None) |
1430 | + _determine_os_workload_status.assert_called_once_with( |
1431 | + 'one', 'two', three='three') |
1432 | + status_set.assert_called_once_with('active', 'fine') |
1433 | + # return something other than 'active' or 'maintenance' |
1434 | + _determine_os_workload_status.return_value = ('broken', 'damaged') |
1435 | + r = f() |
1436 | + self.assertEquals(r, 'damaged') |
1437 | + |
1438 | + @patch.object(openstack, 'restart_on_change_helper') |
1439 | + @patch.object(openstack, 'is_unit_paused_set') |
1440 | + def test_pausable_restart_on_change( |
1441 | + self, is_unit_paused_set, restart_on_change_helper): |
1442 | + @openstack.pausable_restart_on_change({}) |
1443 | + def test_func(): |
1444 | + pass |
1445 | + |
1446 | + # test with pause: restart_on_change_helper should not be called. |
1447 | + is_unit_paused_set.return_value = True |
1448 | + test_func() |
1449 | + self.assertEquals(restart_on_change_helper.call_count, 0) |
1450 | + |
1451 | + # test without pause: restart_on_change_helper should be called. |
1452 | + is_unit_paused_set.return_value = False |
1453 | + test_func() |
1454 | + self.assertEquals(restart_on_change_helper.call_count, 1) |
1455 | + |
1456 | + @patch.object(openstack, 'juju_log') |
1457 | @patch.object(openstack, 'action_set') |
1458 | @patch.object(openstack, 'action_fail') |
1459 | @patch.object(openstack, 'openstack_upgrade_available') |
It looks good to me, I like the rejig. A minor nitpick but could you expand some of the list comprehensions, I find them hard to parse tbh. I've pointed them out in line.