Merge lp:~ajkavanagh/charm-helpers/support-maintenance-mode into lp:charm-helpers

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
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Review via email: mp+287907@code.launchpad.net

Description of the change

Support pause/resume functionality for OpenStack charms. Pause and resume are baked into set_os_workload_status() and also perform additional checks using service_running() and port listening checks.

Also includes a @pausable_restart_on_change decorator which can be used in-place of @restart_on_change which gates the restart of services if the unit is supposed to be paused.

To post a comment you must log in.
Revision history for this message
Liam Young (gnuoy) wrote :

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.

review: Needs Fixing
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')

Subscribers

People subscribed via source and target branches