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
=== modified file 'charmhelpers/contrib/openstack/utils.py'
--- charmhelpers/contrib/openstack/utils.py 2016-02-17 16:29:13 +0000
+++ charmhelpers/contrib/openstack/utils.py 2016-03-07 16:45:36 +0000
@@ -24,6 +24,7 @@
24import sys24import sys
25import re25import re
26import itertools26import itertools
27import functools
2728
28import six29import six
29import tempfile30import tempfile
@@ -69,7 +70,15 @@
69 pip_install,70 pip_install,
70)71)
7172
72from charmhelpers.core.host import lsb_release, mounts, umount, service_running73from charmhelpers.core.host import (
74 lsb_release,
75 mounts,
76 umount,
77 service_running,
78 service_pause,
79 service_resume,
80 restart_on_change_helper,
81)
73from charmhelpers.fetch import apt_install, apt_cache, install_remote82from charmhelpers.fetch import apt_install, apt_cache, install_remote
74from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk83from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
75from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device84from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
@@ -862,66 +871,155 @@
862 return wrap871 return wrap
863872
864873
865def set_os_workload_status(configs, required_interfaces, charm_func=None, services=None, ports=None):874def set_os_workload_status(configs, required_interfaces, charm_func=None,
866 """875 services=None, ports=None):
867 Set workload status based on complete contexts.876 """Set the state of the workload status for the charm.
868 status-set missing or incomplete contexts877
869 and juju-log details of missing required data.878 This calls _determine_os_workload_status() to get the new state, message
870 charm_func is a charm specific function to run checking879 and sets the status using status_set()
871 for charm specific requirements such as a VIP setting.880
872881 @param configs: a templating.OSConfigRenderer() object
873 This function also checks for whether the services defined are ACTUALLY882 @param required_interfaces: {generic: [specific, specific2, ...]}
874 running and that the ports they advertise are open and being listened to.883 @param charm_func: a callable function that returns state, message. The
875884 signature is charm_func(configs) -> (state, message)
876 @param services - OPTIONAL: a [{'service': <string>, 'ports': [<int>]]885 @param services: list of strings OR dictionary specifying services/ports
877 The ports are optional.886 @param ports: OPTIONAL list of port numbers.
878 If services is a [<string>] then ports are ignored.887 @returns state, message: the new workload status, user message
879 @param ports - OPTIONAL: an [<int>] representing ports that shoudl be888 """
880 open.889 state, message = _determine_os_workload_status(
881 @returns None890 configs, required_interfaces, charm_func, services, ports)
882 """891 status_set(state, message)
883 incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)892
884 state = 'active'893
885 missing_relations = []894def _determine_os_workload_status(
886 incomplete_relations = []895 configs, required_interfaces, charm_func=None,
896 services=None, ports=None):
897 """Determine the state of the workload status for the charm.
898
899 This function returns the new workload status for the charm based
900 on the state of the interfaces, the paused state and whether the
901 services are actually running and any specified ports are open.
902
903 This checks:
904
905 1. if the unit should be paused, that it is actually paused. If so the
906 state is 'maintenance' + message, else 'broken'.
907 2. that the interfaces/relations are complete. If they are not then
908 it sets the state to either 'broken' or 'waiting' and an appropriate
909 message.
910 3. If all the relation data is set, then it checks that the actual
911 services really are running. If not it sets the state to 'broken'.
912
913 If everything is okay then the state returns 'active'.
914
915 @param configs: a templating.OSConfigRenderer() object
916 @param required_interfaces: {generic: [specific, specific2, ...]}
917 @param charm_func: a callable function that returns state, message. The
918 signature is charm_func(configs) -> (state, message)
919 @param services: list of strings OR dictionary specifying services/ports
920 @param ports: OPTIONAL list of port numbers.
921 @returns state, message: the new workload status, user message
922 """
923 state, message = _ows_check_if_paused(services, ports)
924
925 if state is None:
926 state, message = _ows_check_generic_interfaces(
927 configs, required_interfaces)
928
929 if state != 'maintenance' and charm_func:
930 # _ows_check_charm_func() may modify the state, message
931 state, message = _ows_check_charm_func(
932 state, message, lambda: charm_func(configs))
933
934 if state is None:
935 state, message = _ows_check_services_running(services, ports)
936
937 if state is None:
938 state = 'active'
939 message = "Unit is ready"
940 juju_log(message, 'INFO')
941
942 return state, message
943
944
945def _ows_check_if_paused(services=None, ports=None):
946 """Check if the unit is supposed to be paused, and if so check that the
947 services/ports (if passed) are actually stopped/not being listened to.
948
949 if the unit isn't supposed to be paused, just return None, None
950
951 @param services: OPTIONAL services spec or list of service names.
952 @param ports: OPTIONAL list of port numbers.
953 @returns state, message or None, None
954 """
955 if is_unit_paused_set():
956 state, message = check_actually_paused(services=services,
957 ports=ports)
958 if state is None:
959 # we're paused okay, so set maintenance and return
960 state = "maintenance"
961 message = "Paused. Use 'resume' action to resume normal service."
962 return state, message
963 return None, None
964
965
966def _ows_check_generic_interfaces(configs, required_interfaces):
967 """Check the complete contexts to determine the workload status.
968
969 - Checks for missing or incomplete contexts
970 - juju log details of missing required data.
971 - determines the correct workload status
972 - creates an appropriate message for status_set(...)
973
974 if there are no problems then the function returns None, None
975
976 @param configs: a templating.OSConfigRenderer() object
977 @params required_interfaces: {generic_interface: [specific_interface], }
978 @returns state, message or None, None
979 """
980 incomplete_rel_data = incomplete_relation_data(configs,
981 required_interfaces)
982 state = None
887 message = None983 message = None
888 charm_state = None984 missing_relations = set()
889 charm_message = None985 incomplete_relations = set()
890986
891 for generic_interface in incomplete_rel_data.keys():987 for generic_interface, relations_states in incomplete_rel_data.items():
892 related_interface = None988 related_interface = None
893 missing_data = {}989 missing_data = {}
894 # Related or not?990 # Related or not?
895 for interface in incomplete_rel_data[generic_interface]:991 for interface, relation_state in relations_states.items():
896 if incomplete_rel_data[generic_interface][interface].get('related'):992 if relation_state.get('related'):
897 related_interface = interface993 related_interface = interface
898 missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')994 missing_data = relation_state.get('missing_data')
899 # No relation ID for the generic_interface995 break
996 # No relation ID for the generic_interface?
900 if not related_interface:997 if not related_interface:
901 juju_log("{} relation is missing and must be related for "998 juju_log("{} relation is missing and must be related for "
902 "functionality. ".format(generic_interface), 'WARN')999 "functionality. ".format(generic_interface), 'WARN')
903 state = 'blocked'1000 state = 'blocked'
904 if generic_interface not in missing_relations:1001 missing_relations.add(generic_interface)
905 missing_relations.append(generic_interface)
906 else:1002 else:
907 # Relation ID exists but no related unit1003 # Relation ID eists but no related unit
908 if not missing_data:1004 if not missing_data:
909 # Edge case relation ID exists but departing1005 # Edge case - relation ID exists but departings
910 if ('departed' in hook_name() or 'broken' in hook_name()) \1006 _hook_name = hook_name()
911 and related_interface in hook_name():1007 if (('departed' in _hook_name or 'broken' in _hook_name) and
1008 related_interface in _hook_name):
912 state = 'blocked'1009 state = 'blocked'
913 if generic_interface not in missing_relations:1010 missing_relations.add(generic_interface)
914 missing_relations.append(generic_interface)
915 juju_log("{} relation's interface, {}, "1011 juju_log("{} relation's interface, {}, "
916 "relationship is departed or broken "1012 "relationship is departed or broken "
917 "and is required for functionality."1013 "and is required for functionality."
918 "".format(generic_interface, related_interface), "WARN")1014 "".format(generic_interface, related_interface),
1015 "WARN")
919 # Normal case relation ID exists but no related unit1016 # Normal case relation ID exists but no related unit
920 # (joining)1017 # (joining)
921 else:1018 else:
922 juju_log("{} relations's interface, {}, is related but has "1019 juju_log("{} relations's interface, {}, is related but has "
923 "no units in the relation."1020 "no units in the relation."
924 "".format(generic_interface, related_interface), "INFO")1021 "".format(generic_interface, related_interface),
1022 "INFO")
925 # Related unit exists and data missing on the relation1023 # Related unit exists and data missing on the relation
926 else:1024 else:
927 juju_log("{} relation's interface, {}, is related awaiting "1025 juju_log("{} relation's interface, {}, is related awaiting "
@@ -930,9 +1028,8 @@
930 ", ".join(missing_data)), "INFO")1028 ", ".join(missing_data)), "INFO")
931 if state != 'blocked':1029 if state != 'blocked':
932 state = 'waiting'1030 state = 'waiting'
933 if generic_interface not in incomplete_relations \1031 if generic_interface not in missing_relations:
934 and generic_interface not in missing_relations:1032 incomplete_relations.add(generic_interface)
935 incomplete_relations.append(generic_interface)
9361033
937 if missing_relations:1034 if missing_relations:
938 message = "Missing relations: {}".format(", ".join(missing_relations))1035 message = "Missing relations: {}".format(", ".join(missing_relations))
@@ -945,9 +1042,22 @@
945 "".format(", ".join(incomplete_relations))1042 "".format(", ".join(incomplete_relations))
946 state = 'waiting'1043 state = 'waiting'
9471044
948 # Run charm specific checks1045 return state, message
949 if charm_func:1046
950 charm_state, charm_message = charm_func(configs)1047
1048def _ows_check_charm_func(state, message, charm_func_with_configs):
1049 """Run a custom check function for the charm to see if it wants to
1050 change the state. This is only run if not in 'maintenance' and
1051 tests to see if the new state is more important that the previous
1052 one determined by the interfaces/relations check.
1053
1054 @param state: the previously determined state so far.
1055 @param message: the user orientated message so far.
1056 @param charm_func: a callable function that returns state, message
1057 @returns state, message strings.
1058 """
1059 if charm_func_with_configs:
1060 charm_state, charm_message = charm_func_with_configs()
951 if charm_state != 'active' and charm_state != 'unknown':1061 if charm_state != 'active' and charm_state != 'unknown':
952 state = workload_state_compare(state, charm_state)1062 state = workload_state_compare(state, charm_state)
953 if message:1063 if message:
@@ -956,72 +1066,151 @@
956 message = "{}, {}".format(message, charm_message)1066 message = "{}, {}".format(message, charm_message)
957 else:1067 else:
958 message = charm_message1068 message = charm_message
9591069 return state, message
960 # If the charm thinks the unit is active, check that the actual services1070
961 # really are active.1071
962 if services is not None and state == 'active':1072def _ows_check_services_running(services, ports):
963 # if we're passed the dict() then just grab the values as a list.1073 """Check that the services that should be running are actually running
964 if isinstance(services, dict):1074 and that any ports specified are being listened to.
965 services = services.values()1075
966 # either extract the list of services from the dictionary, or if1076 @param services: list of strings OR dictionary specifying services/ports
967 # it is a simple string, use that. i.e. works with mixed lists.1077 @param ports: list of ports
968 _s = []1078 @returns state, message: strings or None, None
969 for s in services:1079 """
970 if isinstance(s, dict) and 'service' in s:1080 messages = []
971 _s.append(s['service'])1081 state = None
972 if isinstance(s, str):1082 if services is not None:
973 _s.append(s)1083 services = _extract_services_list_helper(services)
974 services_running = [service_running(s) for s in _s]1084 services_running, running = _check_running_services(services)
975 if not all(services_running):1085 if not all(running):
976 not_running = [s for s, running in zip(_s, services_running)1086 messages.append(
977 if not running]1087 "Services not running that should be: {}"
978 message = ("Services not running that should be: {}"1088 .format(", ".join(_filter_tuples(services_running, False))))
979 .format(", ".join(not_running)))
980 state = 'blocked'1089 state = 'blocked'
981 # also verify that the ports that should be open are open1090 # also verify that the ports that should be open are open
982 # NB, that ServiceManager objects only OPTIONALLY have ports1091 # NB, that ServiceManager objects only OPTIONALLY have ports
983 port_map = OrderedDict([(s['service'], s['ports'])1092 map_not_open, ports_open = (
984 for s in services if 'ports' in s])1093 _check_listening_on_services_ports(services))
985 if state == 'active' and port_map:1094 if not all(ports_open):
986 all_ports = list(itertools.chain(*port_map.values()))1095 # find which service has missing ports. They are in service
987 ports_open = [port_has_listener('0.0.0.0', p)1096 # order which makes it a bit easier.
988 for p in all_ports]1097 message_parts = {service: ", ".join([str(v) for v in open_ports])
989 if not all(ports_open):1098 for service, open_ports in map_not_open.items()}
990 not_opened = [p for p, opened in zip(all_ports, ports_open)1099 message = ", ".join(
991 if not opened]1100 ["{}: [{}]".format(s, sp) for s, sp in message_parts.items()])
992 map_not_open = OrderedDict()1101 messages.append(
993 for service, ports in port_map.items():1102 "Services with ports not open that should be: {}"
994 closed_ports = set(ports).intersection(not_opened)1103 .format(message))
995 if closed_ports:1104 state = 'blocked'
996 map_not_open[service] = closed_ports
997 # find which service has missing ports. They are in service
998 # order which makes it a bit easier.
999 message = (
1000 "Services with ports not open that should be: {}"
1001 .format(
1002 ", ".join([
1003 "{}: [{}]".format(
1004 service,
1005 ", ".join([str(v) for v in ports]))
1006 for service, ports in map_not_open.items()])))
1007 state = 'blocked'
10081105
1009 if ports is not None and state == 'active':1106 if ports is not None:
1010 # and we can also check ports which we don't know the service for1107 # and we can also check ports which we don't know the service for
1011 ports_open = [port_has_listener('0.0.0.0', p) for p in ports]1108 ports_open, ports_open_bools = _check_listening_on_ports_list(ports)
1012 if not all(ports_open):1109 if not all(ports_open_bools):
1013 message = (1110 messages.append(
1014 "Ports which should be open, but are not: {}"1111 "Ports which should be open, but are not: {}"
1015 .format(", ".join([str(p) for p, v in zip(ports, ports_open)1112 .format(", ".join([str(p) for p, v in ports_open
1016 if not v])))1113 if not v])))
1017 state = 'blocked'1114 state = 'blocked'
10181115
1019 # Set to active if all requirements have been met1116 if state is not None:
1020 if state == 'active':1117 message = "; ".join(messages)
1021 message = "Unit is ready"1118 return state, message
1022 juju_log(message, "INFO")1119
10231120 return None, None
1024 status_set(state, message)1121
1122
1123def _extract_services_list_helper(services):
1124 """Extract a OrderedDict of {service: [ports]} of the supplied services
1125 for use by the other functions.
1126
1127 The services object can either be:
1128 - None : no services were passed (an empty dict is returned)
1129 - a list of strings
1130 - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1131 - An array of [{'service': service_name, ...}, ...]
1132
1133 @param services: see above
1134 @returns OrderedDict(service: [ports], ...)
1135 """
1136 if services is None:
1137 return {}
1138 if isinstance(services, dict):
1139 services = services.values()
1140 # either extract the list of services from the dictionary, or if
1141 # it is a simple string, use that. i.e. works with mixed lists.
1142 _s = OrderedDict()
1143 for s in services:
1144 if isinstance(s, dict) and 'service' in s:
1145 _s[s['service']] = s.get('ports', [])
1146 if isinstance(s, str):
1147 _s[s] = []
1148 return _s
1149
1150
1151def _check_running_services(services):
1152 """Check that the services dict provided is actually running and provide
1153 a list of (service, boolean) tuples for each service.
1154
1155 Returns both a zipped list of (service, boolean) and a list of booleans
1156 in the same order as the services.
1157
1158 @param services: OrderedDict of strings: [ports], one for each service to
1159 check.
1160 @returns [(service, boolean), ...], : results for checks
1161 [boolean] : just the result of the service checks
1162 """
1163 services_running = [service_running(s) for s in services]
1164 return list(zip(services, services_running)), services_running
1165
1166
1167def _check_listening_on_services_ports(services, test=False):
1168 """Check that the unit is actually listening (has the port open) on the
1169 ports that the service specifies are open. If test is True then the
1170 function returns the services with ports that are open rather than
1171 closed.
1172
1173 Returns an OrderedDict of service: ports and a list of booleans
1174
1175 @param services: OrderedDict(service: [port, ...], ...)
1176 @param test: default=False, if False, test for closed, otherwise open.
1177 @returns OrderedDict(service: [port-not-open, ...]...), [boolean]
1178 """
1179 test = not(not(test)) # ensure test is True or False
1180 all_ports = list(itertools.chain(*services.values()))
1181 ports_states = [port_has_listener('0.0.0.0', p) for p in all_ports]
1182 map_ports = OrderedDict()
1183 matched_ports = [p for p, opened in zip(all_ports, ports_states)
1184 if opened == test] # essentially opened xor test
1185 for service, ports in services.items():
1186 set_ports = set(ports).intersection(matched_ports)
1187 if set_ports:
1188 map_ports[service] = set_ports
1189 return map_ports, ports_states
1190
1191
1192def _check_listening_on_ports_list(ports):
1193 """Check that the ports list given are being listened to
1194
1195 Returns a list of ports being listened to and a list of the
1196 booleans.
1197
1198 @param ports: LIST or port numbers.
1199 @returns [(port_num, boolean), ...], [boolean]
1200 """
1201 ports_open = [port_has_listener('0.0.0.0', p) for p in ports]
1202 return zip(ports, ports_open), ports_open
1203
1204
1205def _filter_tuples(services_states, state):
1206 """Return a simple list from a list of tuples according to the condition
1207
1208 @param services_states: LIST of (string, boolean): service and running
1209 state.
1210 @param state: Boolean to match the tuple against.
1211 @returns [LIST of strings] that matched the tuple RHS.
1212 """
1213 return [s for s, b in services_states if b == state]
10251214
10261215
1027def workload_state_compare(current_workload_state, workload_state):1216def workload_state_compare(current_workload_state, workload_state):
@@ -1046,8 +1235,7 @@
10461235
10471236
1048def incomplete_relation_data(configs, required_interfaces):1237def incomplete_relation_data(configs, required_interfaces):
1049 """1238 """Check complete contexts against required_interfaces
1050 Check complete contexts against required_interfaces
1051 Return dictionary of incomplete relation data.1239 Return dictionary of incomplete relation data.
10521240
1053 configs is an OSConfigRenderer object with configs registered1241 configs is an OSConfigRenderer object with configs registered
@@ -1072,19 +1260,13 @@
1072 'shared-db': {'related': True}}}1260 'shared-db': {'related': True}}}
1073 """1261 """
1074 complete_ctxts = configs.complete_contexts()1262 complete_ctxts = configs.complete_contexts()
1075 incomplete_relations = []1263 incomplete_relations = [
1076 for svc_type in required_interfaces.keys():1264 svc_type
1077 # Avoid duplicates1265 for svc_type, interfaces in required_interfaces.items()
1078 found_ctxt = False1266 if not set(interfaces).intersection(complete_ctxts)]
1079 for interface in required_interfaces[svc_type]:1267 return {
1080 if interface in complete_ctxts:1268 i: configs.get_incomplete_context_data(required_interfaces[i])
1081 found_ctxt = True1269 for i in incomplete_relations}
1082 if not found_ctxt:
1083 incomplete_relations.append(svc_type)
1084 incomplete_context_data = {}
1085 for i in incomplete_relations:
1086 incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
1087 return incomplete_context_data
10881270
10891271
1090def do_action_openstack_upgrade(package, upgrade_callback, configs):1272def do_action_openstack_upgrade(package, upgrade_callback, configs):
@@ -1145,3 +1327,242 @@
1145 relation_set(relation_id=rid,1327 relation_set(relation_id=rid,
1146 relation_settings=trigger,1328 relation_settings=trigger,
1147 )1329 )
1330
1331
1332def check_actually_paused(services=None, ports=None):
1333 """Check that services listed in the services object and and ports
1334 are actually closed (not listened to), to verify that the unit is
1335 properly paused.
1336
1337 @param services: See _extract_services_list_helper
1338 @returns status, : string for status (None if okay)
1339 message : string for problem for status_set
1340 """
1341 state = None
1342 message = None
1343 messages = []
1344 if services is not None:
1345 services = _extract_services_list_helper(services)
1346 services_running, services_states = _check_running_services(services)
1347 if any(services_states):
1348 # there shouldn't be any running so this is a problem
1349 messages.append("these services running: {}"
1350 .format(", ".join(
1351 _filter_tuples(services_running, True))))
1352 state = "blocked"
1353 ports_open, ports_open_bools = (
1354 _check_listening_on_services_ports(services, True))
1355 if any(ports_open_bools):
1356 message_parts = {service: ", ".join([str(v) for v in open_ports])
1357 for service, open_ports in ports_open.items()}
1358 message = ", ".join(
1359 ["{}: [{}]".format(s, sp) for s, sp in message_parts.items()])
1360 messages.append(
1361 "these service:ports are open: {}".format(message))
1362 state = 'blocked'
1363 if ports is not None:
1364 ports_open, bools = _check_listening_on_ports_list(ports)
1365 if any(bools):
1366 messages.append(
1367 "these ports which should be closed, but are open: {}"
1368 .format(", ".join([str(p) for p, v in ports_open if v])))
1369 state = 'blocked'
1370 if messages:
1371 message = ("Services should be paused but {}"
1372 .format(", ".join(messages)))
1373 return state, message
1374
1375
1376def set_unit_paused():
1377 """Set the unit to a paused state in the local kv() store.
1378 This does NOT actually pause the unit
1379 """
1380 with unitdata.HookData()() as kv:
1381 kv.set('unit-paused', True)
1382
1383
1384def clear_unit_paused():
1385 """Clear the unit from a paused state in the local kv() store
1386 This does NOT actually restart any services - it only clears the
1387 local state.
1388 """
1389 with unitdata.HookData()() as kv:
1390 kv.set('unit-paused', False)
1391
1392
1393def is_unit_paused_set():
1394 """Return the state of the kv().get('unit-paused').
1395 This does NOT verify that the unit really is paused.
1396
1397 To help with units that don't have HookData() (testing)
1398 if it excepts, return False
1399 """
1400 try:
1401 with unitdata.HookData()() as kv:
1402 # transform something truth-y into a Boolean.
1403 return not(not(kv.get('unit-paused')))
1404 except:
1405 return False
1406
1407
1408def pause_unit(assess_status_func, services=None, ports=None,
1409 charm_func=None):
1410 """Pause a unit by stopping the services and setting 'unit-paused'
1411 in the local kv() store.
1412
1413 Also checks that the services have stopped and ports are no longer
1414 being listened to.
1415
1416 An optional charm_func() can be called that can either raise an
1417 Exception or return non None, None to indicate that the unit
1418 didn't pause cleanly.
1419
1420 The signature for charm_func is:
1421 charm_func() -> message: string
1422
1423 charm_func() is executed after any services are stopped, if supplied.
1424
1425 The services object can either be:
1426 - None : no services were passed (an empty dict is returned)
1427 - a list of strings
1428 - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1429 - An array of [{'service': service_name, ...}, ...]
1430
1431 @param assess_status_func: (f() -> message: string | None) or None
1432 @param services: OPTIONAL see above
1433 @param ports: OPTIONAL list of port
1434 @param charm_func: function to run for custom charm pausing.
1435 @returns None
1436 @raises Exception(message) on an error for action_fail().
1437 """
1438 services = _extract_services_list_helper(services)
1439 messages = []
1440 if services:
1441 for service in services.keys():
1442 stopped = service_pause(service)
1443 if not stopped:
1444 messages.append("{} didn't stop cleanly.".format(service))
1445 if charm_func:
1446 try:
1447 message = charm_func()
1448 if message:
1449 messages.append(message)
1450 except Exception as e:
1451 message.append(str(e))
1452 set_unit_paused()
1453 if assess_status_func:
1454 message = assess_status_func()
1455 if message:
1456 messages.append(message)
1457 if messages:
1458 raise Exception("Couldn't pause: {}".format("; ".join(messages)))
1459
1460
1461def resume_unit(assess_status_func, services=None, ports=None,
1462 charm_func=None):
1463 """Resume a unit by starting the services and clearning 'unit-paused'
1464 in the local kv() store.
1465
1466 Also checks that the services have started and ports are being listened to.
1467
1468 An optional charm_func() can be called that can either raise an
1469 Exception or return non None to indicate that the unit
1470 didn't resume cleanly.
1471
1472 The signature for charm_func is:
1473 charm_func() -> message: string
1474
1475 charm_func() is executed after any services are started, if supplied.
1476
1477 The services object can either be:
1478 - None : no services were passed (an empty dict is returned)
1479 - a list of strings
1480 - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1481 - An array of [{'service': service_name, ...}, ...]
1482
1483 @param assess_status_func: (f() -> message: string | None) or None
1484 @param services: OPTIONAL see above
1485 @param ports: OPTIONAL list of port
1486 @param charm_func: function to run for custom charm resuming.
1487 @returns None
1488 @raises Exception(message) on an error for action_fail().
1489 """
1490 services = _extract_services_list_helper(services)
1491 messages = []
1492 if services:
1493 for service in services.keys():
1494 started = service_resume(service)
1495 if not started:
1496 messages.append("{} didn't start cleanly.".format(service))
1497 if charm_func:
1498 try:
1499 message = charm_func()
1500 if message:
1501 messages.append(message)
1502 except Exception as e:
1503 message.append(str(e))
1504 clear_unit_paused()
1505 if assess_status_func:
1506 message = assess_status_func()
1507 if message:
1508 messages.append(message)
1509 if messages:
1510 raise Exception("Couldn't resume: {}".format("; ".join(messages)))
1511
1512
1513def make_assess_status_func(*args, **kwargs):
1514 """Creates an assess_status_func() suitable for handing to pause_unit()
1515 and resume_unit().
1516
1517 This uses the _determine_os_workload_status(...) function to determine
1518 what the workload_status should be for the unit. If the unit is
1519 not in maintenance or active states, then the message is returned to
1520 the caller. This is so an action that doesn't result in either a
1521 complete pause or complete resume can signal failure with an action_fail()
1522 """
1523 def _assess_status_func():
1524 state, message = _determine_os_workload_status(*args, **kwargs)
1525 status_set(state, message)
1526 if state not in ['maintenance', 'active']:
1527 return message
1528 return None
1529
1530 return _assess_status_func
1531
1532
1533def pausable_restart_on_change(restart_map, stopstart=False):
1534 """A restart_on_change decorator that checks to see if the unit is
1535 paused. If it is paused then the decorated function doesn't fire.
1536
1537 This is provided as a helper, as the @restart_on_change(...) decorator
1538 is in core.host, yet the openstack specific helpers are in this file
1539 (contrib.openstack.utils). Thus, this needs to be an optional feature
1540 for openstack charms (or charms that wish to use the openstack
1541 pause/resume type features).
1542
1543 It is used as follows:
1544
1545 from contrib.openstack.utils import (
1546 pausable_restart_on_change as restart_on_change)
1547
1548 @restart_on_change(restart_map, stopstart=<boolean>)
1549 def some_hook(...):
1550 pass
1551
1552 see core.utils.restart_on_change() for more details.
1553
1554 @param f: the function to decorate
1555 @param restart_map: the restart map {conf_file: [services]}
1556 @param stopstart: DEFAULT false; whether to stop, start or just restart
1557 @returns decorator to use a restart_on_change with pausability
1558 """
1559 def wrap(f):
1560 @functools.wraps(f)
1561 def wrapped_f(*args, **kwargs):
1562 if is_unit_paused_set():
1563 return f(*args, **kwargs)
1564 # otherwise, normal restart_on_change functionality
1565 return restart_on_change_helper(
1566 (lambda: f(*args, **kwargs)), restart_map, stopstart)
1567 return wrapped_f
1568 return wrap
11481569
=== modified file 'charmhelpers/core/host.py'
--- charmhelpers/core/host.py 2016-01-19 21:53:13 +0000
+++ charmhelpers/core/host.py 2016-03-07 16:45:36 +0000
@@ -30,6 +30,8 @@
30import string30import string
31import subprocess31import subprocess
32import hashlib32import hashlib
33import functools
34import itertools
33from contextlib import contextmanager35from contextlib import contextmanager
34from collections import OrderedDict36from collections import OrderedDict
3537
@@ -428,27 +430,47 @@
428 restarted if any file matching the pattern got changed, created430 restarted if any file matching the pattern got changed, created
429 or removed. Standard wildcards are supported, see documentation431 or removed. Standard wildcards are supported, see documentation
430 for the 'glob' module for more information.432 for the 'glob' module for more information.
433
434 @param restart_map: {path_file_name: [service_name, ...]
435 @param stopstart: DEFAULT false; whether to stop, start OR restart
436 @returns result from decorated function
431 """437 """
432 def wrap(f):438 def wrap(f):
439 @functools.wraps(f)
433 def wrapped_f(*args, **kwargs):440 def wrapped_f(*args, **kwargs):
434 checksums = {path: path_hash(path) for path in restart_map}441 return restart_on_change_helper(
435 f(*args, **kwargs)442 (lambda: f(*args, **kwargs)), restart_map, stopstart)
436 restarts = []
437 for path in restart_map:
438 if path_hash(path) != checksums[path]:
439 restarts += restart_map[path]
440 services_list = list(OrderedDict.fromkeys(restarts))
441 if not stopstart:
442 for service_name in services_list:
443 service('restart', service_name)
444 else:
445 for action in ['stop', 'start']:
446 for service_name in services_list:
447 service(action, service_name)
448 return wrapped_f443 return wrapped_f
449 return wrap444 return wrap
450445
451446
447def restart_on_change_helper(lambda_f, restart_map, stopstart=False):
448 """Helper function to perform the restart_on_change function.
449
450 This is provided for decorators to restart services if files described
451 in the restart_map have changed after an invocation of lambda_f().
452
453 @param lambda_f: function to call.
454 @param restart_map: {file: [service, ...]}
455 @param stopstart: whether to stop, start or restart a service
456 @returns result of lambda_f()
457 """
458 checksums = {path: path_hash(path) for path in restart_map}
459 r = lambda_f()
460 # create a list of lists of the services to restart
461 restarts = [restart_map[path]
462 for path in restart_map
463 if path_hash(path) != checksums[path]]
464 # create a flat list of ordered services without duplicates from lists
465 services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
466 if services_list:
467 actions = ('stop', 'start') if stopstart else ('restart',)
468 for action in actions:
469 for service_name in services_list:
470 service(action, service_name)
471 return r
472
473
452def lsb_release():474def lsb_release():
453 """Return /etc/lsb-release in a dict"""475 """Return /etc/lsb-release in a dict"""
454 d = {}476 d = {}
455477
=== modified file 'tests/contrib/openstack/test_openstack_utils.py'
--- tests/contrib/openstack/test_openstack_utils.py 2016-02-17 16:29:13 +0000
+++ tests/contrib/openstack/test_openstack_utils.py 2016-03-07 16:45:36 +0000
@@ -919,7 +919,10 @@
919919
920 @patch.object(openstack, 'juju_log')920 @patch.object(openstack, 'juju_log')
921 @patch('charmhelpers.contrib.openstack.utils.status_set')921 @patch('charmhelpers.contrib.openstack.utils.status_set')
922 def test_set_os_workload_status_complete(self, status_set, log):922 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
923 return_value=False)
924 def test_set_os_workload_status_complete(
925 self, is_unit_paused_set, status_set, log):
923 configs = MagicMock()926 configs = MagicMock()
924 configs.complete_contexts.return_value = ['shared-db',927 configs.complete_contexts.return_value = ['shared-db',
925 'amqp',928 'amqp',
@@ -936,9 +939,11 @@
936 @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data',939 @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data',
937 return_value={'identity': {'identity-service': {'related': True}}})940 return_value={'identity': {'identity-service': {'related': True}}})
938 @patch('charmhelpers.contrib.openstack.utils.status_set')941 @patch('charmhelpers.contrib.openstack.utils.status_set')
939 def test_set_os_workload_status_related_incomplete(self, status_set,942 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
940 incomplete_relation_data,943 return_value=False)
941 log):944 def test_set_os_workload_status_related_incomplete(
945 self, is_unit_paused_set, status_set,
946 incomplete_relation_data, log):
942 configs = MagicMock()947 configs = MagicMock()
943 configs.complete_contexts.return_value = ['shared-db', 'amqp']948 configs.complete_contexts.return_value = ['shared-db', 'amqp']
944 required_interfaces = {949 required_interfaces = {
@@ -954,8 +959,11 @@
954 @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data',959 @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data',
955 return_value={'identity': {'identity-service': {'related': False}}})960 return_value={'identity': {'identity-service': {'related': False}}})
956 @patch('charmhelpers.contrib.openstack.utils.status_set')961 @patch('charmhelpers.contrib.openstack.utils.status_set')
957 def test_set_os_workload_status_absent(self, status_set,962 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
958 incomplete_relation_data, log):963 return_value=False)
964 def test_set_os_workload_status_absent(
965 self, is_unit_paused_set, status_set,
966 incomplete_relation_data, log):
959 configs = MagicMock()967 configs = MagicMock()
960 configs.complete_contexts.return_value = ['shared-db', 'amqp']968 configs.complete_contexts.return_value = ['shared-db', 'amqp']
961 required_interfaces = {969 required_interfaces = {
@@ -973,9 +981,11 @@
973 @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data',981 @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data',
974 return_value={'identity': {'identity-service': {'related': True}}})982 return_value={'identity': {'identity-service': {'related': True}}})
975 @patch('charmhelpers.contrib.openstack.utils.status_set')983 @patch('charmhelpers.contrib.openstack.utils.status_set')
976 def test_set_os_workload_status_related_broken(self, status_set,984 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
977 incomplete_relation_data,985 return_value=False)
978 hook_name, log):986 def test_set_os_workload_status_related_broken(
987 self, is_unit_paused_set, status_set,
988 incomplete_relation_data, hook_name, log):
979 configs = MagicMock()989 configs = MagicMock()
980 configs.complete_contexts.return_value = ['shared-db', 'amqp']990 configs.complete_contexts.return_value = ['shared-db', 'amqp']
981 required_interfaces = {991 required_interfaces = {
@@ -1000,8 +1010,11 @@
1000 {'shared-db': {'related': False}}1010 {'shared-db': {'related': False}}
1001 })1011 })
1002 @patch('charmhelpers.contrib.openstack.utils.status_set')1012 @patch('charmhelpers.contrib.openstack.utils.status_set')
1003 def test_set_os_workload_status_mixed(self, status_set, incomplete_relation_data,1013 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1004 log):1014 return_value=False)
1015 def test_set_os_workload_status_mixed(
1016 self, is_unit_paused_set, status_set,
1017 incomplete_relation_data, log):
1005 configs = MagicMock()1018 configs = MagicMock()
1006 configs.complete_contexts.return_value = ['shared-db', 'amqp']1019 configs.complete_contexts.return_value = ['shared-db', 'amqp']
1007 required_interfaces = {1020 required_interfaces = {
@@ -1025,16 +1038,14 @@
1025 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')1038 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1026 @patch.object(openstack, 'juju_log')1039 @patch.object(openstack, 'juju_log')
1027 @patch('charmhelpers.contrib.openstack.utils.status_set')1040 @patch('charmhelpers.contrib.openstack.utils.status_set')
1041 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1042 return_value=False)
1028 def test_set_os_workload_status_complete_with_services_list(1043 def test_set_os_workload_status_complete_with_services_list(
1029 self, status_set, log, port_has_listener, service_running):1044 self, is_unit_paused_set, status_set, log,
1045 port_has_listener, service_running):
1030 configs = MagicMock()1046 configs = MagicMock()
1031 configs.complete_contexts.return_value = ['shared-db',1047 configs.complete_contexts.return_value = []
1032 'amqp',1048 required_interfaces = {}
1033 'identity-service']
1034 required_interfaces = {
1035 'database': ['shared-db', 'pgsql-db'],
1036 'message': ['amqp', 'zeromq-configuration'],
1037 'identity': ['identity-service']}
10381049
1039 services = ['database', 'identity']1050 services = ['database', 'identity']
1040 # Assume that the service and ports are open.1051 # Assume that the service and ports are open.
@@ -1049,16 +1060,14 @@
1049 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')1060 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1050 @patch.object(openstack, 'juju_log')1061 @patch.object(openstack, 'juju_log')
1051 @patch('charmhelpers.contrib.openstack.utils.status_set')1062 @patch('charmhelpers.contrib.openstack.utils.status_set')
1063 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1064 return_value=False)
1052 def test_set_os_workload_status_complete_services_list_not_running(1065 def test_set_os_workload_status_complete_services_list_not_running(
1053 self, status_set, log, port_has_listener, service_running):1066 self, is_unit_paused_set, status_set, log,
1067 port_has_listener, service_running):
1054 configs = MagicMock()1068 configs = MagicMock()
1055 configs.complete_contexts.return_value = ['shared-db',1069 configs.complete_contexts.return_value = []
1056 'amqp',1070 required_interfaces = {}
1057 'identity-service']
1058 required_interfaces = {
1059 'database': ['shared-db', 'pgsql-db'],
1060 'message': ['amqp', 'zeromq-configuration'],
1061 'identity': ['identity-service']}
10621071
1063 services = ['database', 'identity']1072 services = ['database', 'identity']
1064 port_has_listener.return_value = True1073 port_has_listener.return_value = True
@@ -1075,16 +1084,14 @@
1075 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')1084 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1076 @patch.object(openstack, 'juju_log')1085 @patch.object(openstack, 'juju_log')
1077 @patch('charmhelpers.contrib.openstack.utils.status_set')1086 @patch('charmhelpers.contrib.openstack.utils.status_set')
1087 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1088 return_value=False)
1078 def test_set_os_workload_status_complete_with_services(1089 def test_set_os_workload_status_complete_with_services(
1079 self, status_set, log, port_has_listener, service_running):1090 self, is_unit_paused_set, status_set, log,
1091 port_has_listener, service_running):
1080 configs = MagicMock()1092 configs = MagicMock()
1081 configs.complete_contexts.return_value = ['shared-db',1093 configs.complete_contexts.return_value = []
1082 'amqp',1094 required_interfaces = {}
1083 'identity-service']
1084 required_interfaces = {
1085 'database': ['shared-db', 'pgsql-db'],
1086 'message': ['amqp', 'zeromq-configuration'],
1087 'identity': ['identity-service']}
10881095
1089 services = [1096 services = [
1090 {'service': 'database', 'ports': [10, 20]},1097 {'service': 'database', 'ports': [10, 20]},
@@ -1102,16 +1109,14 @@
1102 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')1109 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1103 @patch.object(openstack, 'juju_log')1110 @patch.object(openstack, 'juju_log')
1104 @patch('charmhelpers.contrib.openstack.utils.status_set')1111 @patch('charmhelpers.contrib.openstack.utils.status_set')
1112 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1113 return_value=False)
1105 def test_set_os_workload_status_complete_service_not_running(1114 def test_set_os_workload_status_complete_service_not_running(
1106 self, status_set, log, port_has_listener, service_running):1115 self, is_unit_paused_set, status_set, log,
1116 port_has_listener, service_running):
1107 configs = MagicMock()1117 configs = MagicMock()
1108 configs.complete_contexts.return_value = ['shared-db',1118 configs.complete_contexts.return_value = []
1109 'amqp',1119 required_interfaces = {}
1110 'identity-service']
1111 required_interfaces = {
1112 'database': ['shared-db', 'pgsql-db'],
1113 'message': ['amqp', 'zeromq-configuration'],
1114 'identity': ['identity-service']}
11151120
1116 services = [1121 services = [
1117 {'service': 'database', 'ports': [10, 20]},1122 {'service': 'database', 'ports': [10, 20]},
@@ -1131,16 +1136,14 @@
1131 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')1136 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1132 @patch.object(openstack, 'juju_log')1137 @patch.object(openstack, 'juju_log')
1133 @patch('charmhelpers.contrib.openstack.utils.status_set')1138 @patch('charmhelpers.contrib.openstack.utils.status_set')
1139 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1140 return_value=False)
1134 def test_set_os_workload_status_complete_port_not_open(1141 def test_set_os_workload_status_complete_port_not_open(
1135 self, status_set, log, port_has_listener, service_running):1142 self, is_unit_paused_set, status_set, log,
1143 port_has_listener, service_running):
1136 configs = MagicMock()1144 configs = MagicMock()
1137 configs.complete_contexts.return_value = ['shared-db',1145 configs.complete_contexts.return_value = []
1138 'amqp',1146 required_interfaces = {}
1139 'identity-service']
1140 required_interfaces = {
1141 'database': ['shared-db', 'pgsql-db'],
1142 'message': ['amqp', 'zeromq-configuration'],
1143 'identity': ['identity-service']}
11441147
1145 services = [1148 services = [
1146 {'service': 'database', 'ports': [10, 20]},1149 {'service': 'database', 'ports': [10, 20]},
@@ -1160,16 +1163,13 @@
1160 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')1163 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1161 @patch.object(openstack, 'juju_log')1164 @patch.object(openstack, 'juju_log')
1162 @patch('charmhelpers.contrib.openstack.utils.status_set')1165 @patch('charmhelpers.contrib.openstack.utils.status_set')
1166 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1167 return_value=False)
1163 def test_set_os_workload_status_complete_ports_not_open(1168 def test_set_os_workload_status_complete_ports_not_open(
1164 self, status_set, log, port_has_listener):1169 self, is_unit_paused_set, status_set, log, port_has_listener):
1165 configs = MagicMock()1170 configs = MagicMock()
1166 configs.complete_contexts.return_value = ['shared-db',1171 configs.complete_contexts.return_value = []
1167 'amqp',1172 required_interfaces = {}
1168 'identity-service']
1169 required_interfaces = {
1170 'database': ['shared-db', 'pgsql-db'],
1171 'message': ['amqp', 'zeromq-configuration'],
1172 'identity': ['identity-service']}
11731173
1174 ports = [50, 60, 70]1174 ports = [50, 60, 70]
1175 port_has_listener.side_effect = [True, False, True]1175 port_has_listener.side_effect = [True, False, True]
@@ -1181,6 +1181,422 @@
1181 'Ports which should be open, but are not: 60')1181 'Ports which should be open, but are not: 60')
11821182
1183 @patch.object(openstack, 'juju_log')1183 @patch.object(openstack, 'juju_log')
1184 @patch('charmhelpers.contrib.openstack.utils.status_set')
1185 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1186 return_value=True)
1187 def test_set_os_workload_status_paused_simple(
1188 self, is_unit_paused_set, status_set, log):
1189 configs = MagicMock()
1190 configs.complete_contexts.return_value = []
1191 required_interfaces = {}
1192
1193 openstack.set_os_workload_status(configs, required_interfaces)
1194 status_set.assert_called_with(
1195 'maintenance',
1196 "Paused. Use 'resume' action to resume normal service.")
1197
1198 @patch('charmhelpers.contrib.openstack.utils.service_running')
1199 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1200 @patch.object(openstack, 'juju_log')
1201 @patch('charmhelpers.contrib.openstack.utils.status_set')
1202 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1203 return_value=True)
1204 def test_set_os_workload_status_paused_services_check(
1205 self, is_unit_paused_set, status_set, log,
1206 port_has_listener, service_running):
1207 configs = MagicMock()
1208 configs.complete_contexts.return_value = []
1209 required_interfaces = {}
1210
1211 services = [
1212 {'service': 'database', 'ports': [10, 20]},
1213 {'service': 'identity', 'ports': [30]},
1214 ]
1215 port_has_listener.return_value = False
1216 service_running.side_effect = [False, False]
1217
1218 openstack.set_os_workload_status(
1219 configs, required_interfaces, services=services)
1220 status_set.assert_called_with(
1221 'maintenance',
1222 "Paused. Use 'resume' action to resume normal service.")
1223
1224 @patch('charmhelpers.contrib.openstack.utils.service_running')
1225 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1226 @patch.object(openstack, 'juju_log')
1227 @patch('charmhelpers.contrib.openstack.utils.status_set')
1228 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1229 return_value=True)
1230 def test_set_os_workload_status_paused_services_fail(
1231 self, is_unit_paused_set, status_set, log,
1232 port_has_listener, service_running):
1233 configs = MagicMock()
1234 configs.complete_contexts.return_value = []
1235 required_interfaces = {}
1236
1237 services = [
1238 {'service': 'database', 'ports': [10, 20]},
1239 {'service': 'identity', 'ports': [30]},
1240 ]
1241 port_has_listener.return_value = False
1242 # Fail the identity service
1243 service_running.side_effect = [False, True]
1244
1245 openstack.set_os_workload_status(
1246 configs, required_interfaces, services=services)
1247 status_set.assert_called_with(
1248 'blocked',
1249 "Services should be paused but these services running: identity")
1250
1251 @patch('charmhelpers.contrib.openstack.utils.service_running')
1252 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1253 @patch.object(openstack, 'juju_log')
1254 @patch('charmhelpers.contrib.openstack.utils.status_set')
1255 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1256 return_value=True)
1257 def test_set_os_workload_status_paused_services_ports_fail(
1258 self, is_unit_paused_set, status_set, log,
1259 port_has_listener, service_running):
1260 configs = MagicMock()
1261 configs.complete_contexts.return_value = []
1262 required_interfaces = {}
1263
1264 services = [
1265 {'service': 'database', 'ports': [10, 20]},
1266 {'service': 'identity', 'ports': [30]},
1267 ]
1268 # make the service 20 port be still listening.
1269 port_has_listener.side_effect = [False, True, False]
1270 service_running.return_value = False
1271
1272 openstack.set_os_workload_status(
1273 configs, required_interfaces, services=services)
1274 status_set.assert_called_with(
1275 'blocked',
1276 "Services should be paused but these service:ports are open:"
1277 " database: [20]")
1278
1279 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1280 @patch.object(openstack, 'juju_log')
1281 @patch('charmhelpers.contrib.openstack.utils.status_set')
1282 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1283 return_value=True)
1284 def test_set_os_workload_status_paused_ports_check(
1285 self, is_unit_paused_set, status_set, log,
1286 port_has_listener):
1287 configs = MagicMock()
1288 configs.complete_contexts.return_value = []
1289 required_interfaces = {}
1290
1291 ports = [50, 60, 70]
1292 port_has_listener.side_effect = [False, False, False]
1293
1294 openstack.set_os_workload_status(
1295 configs, required_interfaces, ports=ports)
1296 status_set.assert_called_with(
1297 'maintenance',
1298 "Paused. Use 'resume' action to resume normal service.")
1299
1300 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1301 @patch.object(openstack, 'juju_log')
1302 @patch('charmhelpers.contrib.openstack.utils.status_set')
1303 @patch('charmhelpers.contrib.openstack.utils.is_unit_paused_set',
1304 return_value=True)
1305 def test_set_os_workload_status_paused_ports_fail(
1306 self, is_unit_paused_set, status_set, log,
1307 port_has_listener):
1308 configs = MagicMock()
1309 configs.complete_contexts.return_value = []
1310 required_interfaces = {}
1311
1312 # fail port 70 to make it seem to be running
1313 ports = [50, 60, 70]
1314 port_has_listener.side_effect = [False, False, True]
1315
1316 openstack.set_os_workload_status(
1317 configs, required_interfaces, ports=ports)
1318 status_set.assert_called_with(
1319 'blocked',
1320 "Services should be paused but "
1321 "these ports which should be closed, but are open: 70")
1322
1323 @patch('charmhelpers.contrib.openstack.utils.service_running')
1324 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1325 def test_check_actually_paused_simple_services(
1326 self, port_has_listener, service_running):
1327 services = ['database', 'identity']
1328 port_has_listener.return_value = False
1329 service_running.return_value = False
1330
1331 state, message = openstack.check_actually_paused(
1332 services)
1333 self.assertEquals(state, None)
1334 self.assertEquals(message, None)
1335
1336 @patch('charmhelpers.contrib.openstack.utils.service_running')
1337 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1338 def test_check_actually_paused_simple_services_fail(
1339 self, port_has_listener, service_running):
1340 services = ['database', 'identity']
1341 port_has_listener.return_value = False
1342 service_running.side_effect = [False, True]
1343
1344 state, message = openstack.check_actually_paused(
1345 services)
1346 self.assertEquals(state, 'blocked')
1347 self.assertEquals(
1348 message,
1349 "Services should be paused but these services running: identity")
1350
1351 @patch('charmhelpers.contrib.openstack.utils.service_running')
1352 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1353 def test_check_actually_paused_services_dict(
1354 self, port_has_listener, service_running):
1355 services = [
1356 {'service': 'database', 'ports': [10, 20]},
1357 {'service': 'identity', 'ports': [30]},
1358 ]
1359 # Assume that the service and ports are open.
1360 port_has_listener.return_value = False
1361 service_running.return_value = False
1362
1363 state, message = openstack.check_actually_paused(
1364 services)
1365 self.assertEquals(state, None)
1366 self.assertEquals(message, None)
1367
1368 @patch('charmhelpers.contrib.openstack.utils.service_running')
1369 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1370 def test_check_actually_paused_services_dict_fail(
1371 self, port_has_listener, service_running):
1372 services = [
1373 {'service': 'database', 'ports': [10, 20]},
1374 {'service': 'identity', 'ports': [30]},
1375 ]
1376 # Assume that the service and ports are open.
1377 port_has_listener.return_value = False
1378 service_running.side_effect = [False, True]
1379
1380 state, message = openstack.check_actually_paused(
1381 services)
1382 self.assertEquals(state, 'blocked')
1383 self.assertEquals(
1384 message,
1385 "Services should be paused but these services running: identity")
1386
1387 @patch('charmhelpers.contrib.openstack.utils.service_running')
1388 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1389 def test_check_actually_paused_services_dict_ports_fail(
1390 self, port_has_listener, service_running):
1391 services = [
1392 {'service': 'database', 'ports': [10, 20]},
1393 {'service': 'identity', 'ports': [30]},
1394 ]
1395 # Assume that the service and ports are open.
1396 port_has_listener.side_effect = [False, True, False]
1397 service_running.return_value = False
1398
1399 state, message = openstack.check_actually_paused(
1400 services)
1401 self.assertEquals(state, 'blocked')
1402 self.assertEquals(message,
1403 'Services should be paused but these service:ports'
1404 ' are open: database: [20]')
1405
1406 @patch('charmhelpers.contrib.openstack.utils.service_running')
1407 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1408 def test_check_actually_paused_ports_okay(
1409 self, port_has_listener, service_running):
1410 port_has_listener.side_effect = [False, False, False]
1411 service_running.return_value = False
1412 ports = [50, 60, 70]
1413
1414 state, message = openstack.check_actually_paused(
1415 ports=ports)
1416 self.assertEquals(state, None)
1417 self.assertEquals(state, None)
1418
1419 @patch('charmhelpers.contrib.openstack.utils.service_running')
1420 @patch('charmhelpers.contrib.openstack.utils.port_has_listener')
1421 def test_check_actually_paused_ports_fail(
1422 self, port_has_listener, service_running):
1423 port_has_listener.side_effect = [False, True, False]
1424 service_running.return_value = False
1425 ports = [50, 60, 70]
1426
1427 state, message = openstack.check_actually_paused(
1428 ports=ports)
1429 self.assertEquals(state, 'blocked')
1430 self.assertEquals(message,
1431 'Services should be paused but these ports '
1432 'which should be closed, but are open: 60')
1433
1434 @patch('charmhelpers.contrib.openstack.utils.service_pause')
1435 @patch('charmhelpers.contrib.openstack.utils.set_unit_paused')
1436 def test_pause_unit_okay(self, set_unit_paused, service_pause):
1437 services = ['service1', 'service2']
1438 service_pause.side_effect = [True, True]
1439 openstack.pause_unit(None, services=services)
1440 set_unit_paused.assert_called_once_with()
1441 self.assertEquals(service_pause.call_count, 2)
1442
1443 @patch('charmhelpers.contrib.openstack.utils.service_pause')
1444 @patch('charmhelpers.contrib.openstack.utils.set_unit_paused')
1445 def test_pause_unit_service_fails(self, set_unit_paused, service_pause):
1446 services = ['service1', 'service2']
1447 service_pause.side_effect = [True, True]
1448 openstack.pause_unit(None, services=services)
1449 set_unit_paused.assert_called_once_with()
1450 self.assertEquals(service_pause.call_count, 2)
1451 # Fail the 2nd service
1452 service_pause.side_effect = [True, False]
1453 try:
1454 openstack.pause_unit(None, services=services)
1455 raise Exception("pause_unit should have raised Exception")
1456 except Exception as e:
1457 self.assertEquals(e.args[0],
1458 "Couldn't pause: service2 didn't stop cleanly.")
1459
1460 @patch('charmhelpers.contrib.openstack.utils.service_pause')
1461 @patch('charmhelpers.contrib.openstack.utils.set_unit_paused')
1462 def test_pausee_unit_service_charm_func(
1463 self, set_unit_paused, service_pause):
1464 services = ['service1', 'service2']
1465 service_pause.return_value = True
1466 charm_func = MagicMock()
1467 charm_func.return_value = None
1468 openstack.pause_unit(None, services=services, charm_func=charm_func)
1469 charm_func.assert_called_once_with()
1470 # fail the charm_func
1471 charm_func.return_value = "Custom charm failed"
1472 try:
1473 openstack.pause_unit(
1474 None, services=services, charm_func=charm_func)
1475 raise Exception("pause_unit should have raised Exception")
1476 except Exception as e:
1477 self.assertEquals(e.args[0],
1478 "Couldn't pause: Custom charm failed")
1479
1480 @patch('charmhelpers.contrib.openstack.utils.service_pause')
1481 @patch('charmhelpers.contrib.openstack.utils.set_unit_paused')
1482 def test_pause_unit_assess_status_func(
1483 self, set_unit_paused, service_pause):
1484 services = ['service1', 'service2']
1485 service_pause.return_value = True
1486 assess_status_func = MagicMock()
1487 assess_status_func.return_value = None
1488 openstack.pause_unit(assess_status_func, services=services)
1489 assess_status_func.assert_called_once_with()
1490 # fail the assess_status_func
1491 assess_status_func.return_value = "assess_status_func failed"
1492 try:
1493 openstack.pause_unit(assess_status_func, services=services)
1494 raise Exception("pause_unit should have raised Exception")
1495 except Exception as e:
1496 self.assertEquals(e.args[0],
1497 "Couldn't pause: assess_status_func failed")
1498
1499 @patch('charmhelpers.contrib.openstack.utils.service_resume')
1500 @patch('charmhelpers.contrib.openstack.utils.clear_unit_paused')
1501 def test_resume_unit_okay(self, clear_unit_paused, service_resume):
1502 services = ['service1', 'service2']
1503 service_resume.side_effect = [True, True]
1504 openstack.resume_unit(None, services=services)
1505 clear_unit_paused.assert_called_once_with()
1506 self.assertEquals(service_resume.call_count, 2)
1507
1508 @patch('charmhelpers.contrib.openstack.utils.service_resume')
1509 @patch('charmhelpers.contrib.openstack.utils.clear_unit_paused')
1510 def test_resume_unit_service_fails(self, clear_unit_paused, service_resume):
1511 services = ['service1', 'service2']
1512 service_resume.side_effect = [True, True]
1513 openstack.resume_unit(None, services=services)
1514 clear_unit_paused.assert_called_once_with()
1515 self.assertEquals(service_resume.call_count, 2)
1516 # Fail the 2nd service
1517 service_resume.side_effect = [True, False]
1518 try:
1519 openstack.resume_unit(None, services=services)
1520 raise Exception("resume_unit should have raised Exception")
1521 except Exception as e:
1522 self.assertEquals(e.args[0],
1523 "Couldn't resume: service2 didn't start cleanly.")
1524
1525 @patch('charmhelpers.contrib.openstack.utils.service_resume')
1526 @patch('charmhelpers.contrib.openstack.utils.clear_unit_paused')
1527 def test_resume_unit_service_charm_func(
1528 self, clear_unit_paused, service_resume):
1529 services = ['service1', 'service2']
1530 service_resume.return_value = True
1531 charm_func = MagicMock()
1532 charm_func.return_value = None
1533 openstack.resume_unit(None, services=services, charm_func=charm_func)
1534 charm_func.assert_called_once_with()
1535 # fail the charm_func
1536 charm_func.return_value = "Custom charm failed"
1537 try:
1538 openstack.resume_unit(
1539 None, services=services, charm_func=charm_func)
1540 raise Exception("resume_unit should have raised Exception")
1541 except Exception as e:
1542 self.assertEquals(e.args[0],
1543 "Couldn't resume: Custom charm failed")
1544
1545 @patch('charmhelpers.contrib.openstack.utils.service_resume')
1546 @patch('charmhelpers.contrib.openstack.utils.clear_unit_paused')
1547 def test_resume_unit_assess_status_func(
1548 self, clear_unit_paused, service_resume):
1549 services = ['service1', 'service2']
1550 service_resume.return_value = True
1551 assess_status_func = MagicMock()
1552 assess_status_func.return_value = None
1553 openstack.resume_unit(assess_status_func, services=services)
1554 assess_status_func.assert_called_once_with()
1555 # fail the assess_status_func
1556 assess_status_func.return_value = "assess_status_func failed"
1557 try:
1558 openstack.resume_unit(assess_status_func, services=services)
1559 raise Exception("resume_unit should have raised Exception")
1560 except Exception as e:
1561 self.assertEquals(e.args[0],
1562 "Couldn't resume: assess_status_func failed")
1563
1564 @patch('charmhelpers.contrib.openstack.utils.status_set')
1565 @patch('charmhelpers.contrib.openstack.utils.'
1566 '_determine_os_workload_status')
1567 def test_make_assess_status_func(self, _determine_os_workload_status,
1568 status_set):
1569 _determine_os_workload_status.return_value = ('active', 'fine')
1570 f = openstack.make_assess_status_func('one', 'two', three='three')
1571 r = f()
1572 self.assertEquals(r, None)
1573 _determine_os_workload_status.assert_called_once_with(
1574 'one', 'two', three='three')
1575 status_set.assert_called_once_with('active', 'fine')
1576 # return something other than 'active' or 'maintenance'
1577 _determine_os_workload_status.return_value = ('broken', 'damaged')
1578 r = f()
1579 self.assertEquals(r, 'damaged')
1580
1581 @patch.object(openstack, 'restart_on_change_helper')
1582 @patch.object(openstack, 'is_unit_paused_set')
1583 def test_pausable_restart_on_change(
1584 self, is_unit_paused_set, restart_on_change_helper):
1585 @openstack.pausable_restart_on_change({})
1586 def test_func():
1587 pass
1588
1589 # test with pause: restart_on_change_helper should not be called.
1590 is_unit_paused_set.return_value = True
1591 test_func()
1592 self.assertEquals(restart_on_change_helper.call_count, 0)
1593
1594 # test without pause: restart_on_change_helper should be called.
1595 is_unit_paused_set.return_value = False
1596 test_func()
1597 self.assertEquals(restart_on_change_helper.call_count, 1)
1598
1599 @patch.object(openstack, 'juju_log')
1184 @patch.object(openstack, 'action_set')1600 @patch.object(openstack, 'action_set')
1185 @patch.object(openstack, 'action_fail')1601 @patch.object(openstack, 'action_fail')
1186 @patch.object(openstack, 'openstack_upgrade_available')1602 @patch.object(openstack, 'openstack_upgrade_available')

Subscribers

People subscribed via source and target branches