865
def set_os_workload_status(configs, required_interfaces, charm_func=None, services=None, ports=None):
867
Set workload status based on complete contexts.
868
status-set missing or incomplete contexts
869
and juju-log details of missing required data.
870
charm_func is a charm specific function to run checking
871
for charm specific requirements such as a VIP setting.
873
This function also checks for whether the services defined are ACTUALLY
874
running and that the ports they advertise are open and being listened to.
876
@param services - OPTIONAL: a [{'service': <string>, 'ports': [<int>]]
877
The ports are optional.
878
If services is a [<string>] then ports are ignored.
879
@param ports - OPTIONAL: an [<int>] representing ports that shoudl be
883
incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
885
missing_relations = []
886
incomplete_relations = []
875
def set_os_workload_status(configs, required_interfaces, charm_func=None,
876
services=None, ports=None):
877
"""Set the state of the workload status for the charm.
879
This calls _determine_os_workload_status() to get the new state, message
880
and sets the status using status_set()
882
@param configs: a templating.OSConfigRenderer() object
883
@param required_interfaces: {generic: [specific, specific2, ...]}
884
@param charm_func: a callable function that returns state, message. The
885
signature is charm_func(configs) -> (state, message)
886
@param services: list of strings OR dictionary specifying services/ports
887
@param ports: OPTIONAL list of port numbers.
888
@returns state, message: the new workload status, user message
890
state, message = _determine_os_workload_status(
891
configs, required_interfaces, charm_func, services, ports)
892
status_set(state, message)
895
def _determine_os_workload_status(
896
configs, required_interfaces, charm_func=None,
897
services=None, ports=None):
898
"""Determine the state of the workload status for the charm.
900
This function returns the new workload status for the charm based
901
on the state of the interfaces, the paused state and whether the
902
services are actually running and any specified ports are open.
906
1. if the unit should be paused, that it is actually paused. If so the
907
state is 'maintenance' + message, else 'broken'.
908
2. that the interfaces/relations are complete. If they are not then
909
it sets the state to either 'broken' or 'waiting' and an appropriate
911
3. If all the relation data is set, then it checks that the actual
912
services really are running. If not it sets the state to 'broken'.
914
If everything is okay then the state returns 'active'.
916
@param configs: a templating.OSConfigRenderer() object
917
@param required_interfaces: {generic: [specific, specific2, ...]}
918
@param charm_func: a callable function that returns state, message. The
919
signature is charm_func(configs) -> (state, message)
920
@param services: list of strings OR dictionary specifying services/ports
921
@param ports: OPTIONAL list of port numbers.
922
@returns state, message: the new workload status, user message
924
state, message = _ows_check_if_paused(services, ports)
927
state, message = _ows_check_generic_interfaces(
928
configs, required_interfaces)
930
if state != 'maintenance' and charm_func:
931
# _ows_check_charm_func() may modify the state, message
932
state, message = _ows_check_charm_func(
933
state, message, lambda: charm_func(configs))
936
state, message = _ows_check_services_running(services, ports)
940
message = "Unit is ready"
941
juju_log(message, 'INFO')
943
return state, message
946
def _ows_check_if_paused(services=None, ports=None):
947
"""Check if the unit is supposed to be paused, and if so check that the
948
services/ports (if passed) are actually stopped/not being listened to.
950
if the unit isn't supposed to be paused, just return None, None
952
@param services: OPTIONAL services spec or list of service names.
953
@param ports: OPTIONAL list of port numbers.
954
@returns state, message or None, None
956
if is_unit_paused_set():
957
state, message = check_actually_paused(services=services,
960
# we're paused okay, so set maintenance and return
961
state = "maintenance"
962
message = "Paused. Use 'resume' action to resume normal service."
963
return state, message
967
def _ows_check_generic_interfaces(configs, required_interfaces):
968
"""Check the complete contexts to determine the workload status.
970
- Checks for missing or incomplete contexts
971
- juju log details of missing required data.
972
- determines the correct workload status
973
- creates an appropriate message for status_set(...)
975
if there are no problems then the function returns None, None
977
@param configs: a templating.OSConfigRenderer() object
978
@params required_interfaces: {generic_interface: [specific_interface], }
979
@returns state, message or None, None
981
incomplete_rel_data = incomplete_relation_data(configs,
985
missing_relations = set()
986
incomplete_relations = set()
891
for generic_interface in incomplete_rel_data.keys():
988
for generic_interface, relations_states in incomplete_rel_data.items():
892
989
related_interface = None
893
990
missing_data = {}
894
991
# Related or not?
895
for interface in incomplete_rel_data[generic_interface]:
896
if incomplete_rel_data[generic_interface][interface].get('related'):
992
for interface, relation_state in relations_states.items():
993
if relation_state.get('related'):
897
994
related_interface = interface
898
missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')
899
# No relation ID for the generic_interface
995
missing_data = relation_state.get('missing_data')
997
# No relation ID for the generic_interface?
900
998
if not related_interface:
901
999
juju_log("{} relation is missing and must be related for "
902
1000
"functionality. ".format(generic_interface), 'WARN')
903
1001
state = 'blocked'
904
if generic_interface not in missing_relations:
905
missing_relations.append(generic_interface)
1002
missing_relations.add(generic_interface)
907
# Relation ID exists but no related unit
1004
# Relation ID eists but no related unit
908
1005
if not missing_data:
909
# Edge case relation ID exists but departing
910
if ('departed' in hook_name() or 'broken' in hook_name()) \
911
and related_interface in hook_name():
1006
# Edge case - relation ID exists but departings
1007
_hook_name = hook_name()
1008
if (('departed' in _hook_name or 'broken' in _hook_name) and
1009
related_interface in _hook_name):
912
1010
state = 'blocked'
913
if generic_interface not in missing_relations:
914
missing_relations.append(generic_interface)
1011
missing_relations.add(generic_interface)
915
1012
juju_log("{} relation's interface, {}, "
916
1013
"relationship is departed or broken "
917
1014
"and is required for functionality."
918
"".format(generic_interface, related_interface), "WARN")
1015
"".format(generic_interface, related_interface),
919
1017
# Normal case relation ID exists but no related unit
922
juju_log("{} relations's interface, {}, is related but has "
923
"no units in the relation."
924
"".format(generic_interface, related_interface), "INFO")
1020
juju_log("{} relations's interface, {}, is related but has"
1021
" no units in the relation."
1022
"".format(generic_interface, related_interface),
925
1024
# Related unit exists and data missing on the relation
927
1026
juju_log("{} relation's interface, {}, is related awaiting "
956
1067
message = "{}, {}".format(message, charm_message)
958
1069
message = charm_message
960
# If the charm thinks the unit is active, check that the actual services
962
if services is not None and state == 'active':
963
# if we're passed the dict() then just grab the values as a list.
964
if isinstance(services, dict):
965
services = services.values()
966
# either extract the list of services from the dictionary, or if
967
# it is a simple string, use that. i.e. works with mixed lists.
970
if isinstance(s, dict) and 'service' in s:
971
_s.append(s['service'])
972
if isinstance(s, str):
974
services_running = [service_running(s) for s in _s]
975
if not all(services_running):
976
not_running = [s for s, running in zip(_s, services_running)
978
message = ("Services not running that should be: {}"
979
.format(", ".join(not_running)))
1070
return state, message
1073
def _ows_check_services_running(services, ports):
1074
"""Check that the services that should be running are actually running
1075
and that any ports specified are being listened to.
1077
@param services: list of strings OR dictionary specifying services/ports
1078
@param ports: list of ports
1079
@returns state, message: strings or None, None
1083
if services is not None:
1084
services = _extract_services_list_helper(services)
1085
services_running, running = _check_running_services(services)
1086
if not all(running):
1088
"Services not running that should be: {}"
1089
.format(", ".join(_filter_tuples(services_running, False))))
980
1090
state = 'blocked'
981
1091
# also verify that the ports that should be open are open
982
1092
# NB, that ServiceManager objects only OPTIONALLY have ports
983
port_map = OrderedDict([(s['service'], s['ports'])
984
for s in services if 'ports' in s])
985
if state == 'active' and port_map:
986
all_ports = list(itertools.chain(*port_map.values()))
987
ports_open = [port_has_listener('0.0.0.0', p)
989
if not all(ports_open):
990
not_opened = [p for p, opened in zip(all_ports, ports_open)
992
map_not_open = OrderedDict()
993
for service, ports in port_map.items():
994
closed_ports = set(ports).intersection(not_opened)
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.
1000
"Services with ports not open that should be: {}"
1005
", ".join([str(v) for v in ports]))
1006
for service, ports in map_not_open.items()])))
1093
map_not_open, ports_open = (
1094
_check_listening_on_services_ports(services))
1095
if not all(ports_open):
1096
# find which service has missing ports. They are in service
1097
# order which makes it a bit easier.
1098
message_parts = {service: ", ".join([str(v) for v in open_ports])
1099
for service, open_ports in map_not_open.items()}
1100
message = ", ".join(
1101
["{}: [{}]".format(s, sp) for s, sp in message_parts.items()])
1103
"Services with ports not open that should be: {}"
1009
if ports is not None and state == 'active':
1107
if ports is not None:
1010
1108
# 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]
1012
if not all(ports_open):
1109
ports_open, ports_open_bools = _check_listening_on_ports_list(ports)
1110
if not all(ports_open_bools):
1014
1112
"Ports which should be open, but are not: {}"
1015
.format(", ".join([str(p) for p, v in zip(ports, ports_open)
1113
.format(", ".join([str(p) for p, v in ports_open
1017
1115
state = 'blocked'
1019
# Set to active if all requirements have been met
1020
if state == 'active':
1021
message = "Unit is ready"
1022
juju_log(message, "INFO")
1024
status_set(state, message)
1117
if state is not None:
1118
message = "; ".join(messages)
1119
return state, message
1124
def _extract_services_list_helper(services):
1125
"""Extract a OrderedDict of {service: [ports]} of the supplied services
1126
for use by the other functions.
1128
The services object can either be:
1129
- None : no services were passed (an empty dict is returned)
1131
- A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1132
- An array of [{'service': service_name, ...}, ...]
1134
@param services: see above
1135
@returns OrderedDict(service: [ports], ...)
1137
if services is None:
1139
if isinstance(services, dict):
1140
services = services.values()
1141
# either extract the list of services from the dictionary, or if
1142
# it is a simple string, use that. i.e. works with mixed lists.
1145
if isinstance(s, dict) and 'service' in s:
1146
_s[s['service']] = s.get('ports', [])
1147
if isinstance(s, str):
1152
def _check_running_services(services):
1153
"""Check that the services dict provided is actually running and provide
1154
a list of (service, boolean) tuples for each service.
1156
Returns both a zipped list of (service, boolean) and a list of booleans
1157
in the same order as the services.
1159
@param services: OrderedDict of strings: [ports], one for each service to
1161
@returns [(service, boolean), ...], : results for checks
1162
[boolean] : just the result of the service checks
1164
services_running = [service_running(s) for s in services]
1165
return list(zip(services, services_running)), services_running
1168
def _check_listening_on_services_ports(services, test=False):
1169
"""Check that the unit is actually listening (has the port open) on the
1170
ports that the service specifies are open. If test is True then the
1171
function returns the services with ports that are open rather than
1174
Returns an OrderedDict of service: ports and a list of booleans
1176
@param services: OrderedDict(service: [port, ...], ...)
1177
@param test: default=False, if False, test for closed, otherwise open.
1178
@returns OrderedDict(service: [port-not-open, ...]...), [boolean]
1180
test = not(not(test)) # ensure test is True or False
1181
all_ports = list(itertools.chain(*services.values()))
1182
ports_states = [port_has_listener('0.0.0.0', p) for p in all_ports]
1183
map_ports = OrderedDict()
1184
matched_ports = [p for p, opened in zip(all_ports, ports_states)
1185
if opened == test] # essentially opened xor test
1186
for service, ports in services.items():
1187
set_ports = set(ports).intersection(matched_ports)
1189
map_ports[service] = set_ports
1190
return map_ports, ports_states
1193
def _check_listening_on_ports_list(ports):
1194
"""Check that the ports list given are being listened to
1196
Returns a list of ports being listened to and a list of the
1199
@param ports: LIST or port numbers.
1200
@returns [(port_num, boolean), ...], [boolean]
1202
ports_open = [port_has_listener('0.0.0.0', p) for p in ports]
1203
return zip(ports, ports_open), ports_open
1206
def _filter_tuples(services_states, state):
1207
"""Return a simple list from a list of tuples according to the condition
1209
@param services_states: LIST of (string, boolean): service and running
1211
@param state: Boolean to match the tuple against.
1212
@returns [LIST of strings] that matched the tuple RHS.
1214
return [s for s, b in services_states if b == state]
1027
1217
def workload_state_compare(current_workload_state, workload_state):
1145
1328
relation_set(relation_id=rid,
1146
1329
relation_settings=trigger,
1333
def check_actually_paused(services=None, ports=None):
1334
"""Check that services listed in the services object and and ports
1335
are actually closed (not listened to), to verify that the unit is
1338
@param services: See _extract_services_list_helper
1339
@returns status, : string for status (None if okay)
1340
message : string for problem for status_set
1345
if services is not None:
1346
services = _extract_services_list_helper(services)
1347
services_running, services_states = _check_running_services(services)
1348
if any(services_states):
1349
# there shouldn't be any running so this is a problem
1350
messages.append("these services running: {}"
1352
_filter_tuples(services_running, True))))
1354
ports_open, ports_open_bools = (
1355
_check_listening_on_services_ports(services, True))
1356
if any(ports_open_bools):
1357
message_parts = {service: ", ".join([str(v) for v in open_ports])
1358
for service, open_ports in ports_open.items()}
1359
message = ", ".join(
1360
["{}: [{}]".format(s, sp) for s, sp in message_parts.items()])
1362
"these service:ports are open: {}".format(message))
1364
if ports is not None:
1365
ports_open, bools = _check_listening_on_ports_list(ports)
1368
"these ports which should be closed, but are open: {}"
1369
.format(", ".join([str(p) for p, v in ports_open if v])))
1372
message = ("Services should be paused but {}"
1373
.format(", ".join(messages)))
1374
return state, message
1377
def set_unit_paused():
1378
"""Set the unit to a paused state in the local kv() store.
1379
This does NOT actually pause the unit
1381
with unitdata.HookData()() as t:
1383
kv.set('unit-paused', True)
1386
def clear_unit_paused():
1387
"""Clear the unit from a paused state in the local kv() store
1388
This does NOT actually restart any services - it only clears the
1391
with unitdata.HookData()() as t:
1393
kv.set('unit-paused', False)
1396
def is_unit_paused_set():
1397
"""Return the state of the kv().get('unit-paused').
1398
This does NOT verify that the unit really is paused.
1400
To help with units that don't have HookData() (testing)
1401
if it excepts, return False
1404
with unitdata.HookData()() as t:
1406
# transform something truth-y into a Boolean.
1407
return not(not(kv.get('unit-paused')))
1412
def pause_unit(assess_status_func, services=None, ports=None,
1414
"""Pause a unit by stopping the services and setting 'unit-paused'
1415
in the local kv() store.
1417
Also checks that the services have stopped and ports are no longer
1420
An optional charm_func() can be called that can either raise an
1421
Exception or return non None, None to indicate that the unit
1422
didn't pause cleanly.
1424
The signature for charm_func is:
1425
charm_func() -> message: string
1427
charm_func() is executed after any services are stopped, if supplied.
1429
The services object can either be:
1430
- None : no services were passed (an empty dict is returned)
1432
- A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1433
- An array of [{'service': service_name, ...}, ...]
1435
@param assess_status_func: (f() -> message: string | None) or None
1436
@param services: OPTIONAL see above
1437
@param ports: OPTIONAL list of port
1438
@param charm_func: function to run for custom charm pausing.
1440
@raises Exception(message) on an error for action_fail().
1442
services = _extract_services_list_helper(services)
1445
for service in services.keys():
1446
stopped = service_pause(service)
1448
messages.append("{} didn't stop cleanly.".format(service))
1451
message = charm_func()
1453
messages.append(message)
1454
except Exception as e:
1455
message.append(str(e))
1457
if assess_status_func:
1458
message = assess_status_func()
1460
messages.append(message)
1462
raise Exception("Couldn't pause: {}".format("; ".join(messages)))
1465
def resume_unit(assess_status_func, services=None, ports=None,
1467
"""Resume a unit by starting the services and clearning 'unit-paused'
1468
in the local kv() store.
1470
Also checks that the services have started and ports are being listened to.
1472
An optional charm_func() can be called that can either raise an
1473
Exception or return non None to indicate that the unit
1474
didn't resume cleanly.
1476
The signature for charm_func is:
1477
charm_func() -> message: string
1479
charm_func() is executed after any services are started, if supplied.
1481
The services object can either be:
1482
- None : no services were passed (an empty dict is returned)
1484
- A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1485
- An array of [{'service': service_name, ...}, ...]
1487
@param assess_status_func: (f() -> message: string | None) or None
1488
@param services: OPTIONAL see above
1489
@param ports: OPTIONAL list of port
1490
@param charm_func: function to run for custom charm resuming.
1492
@raises Exception(message) on an error for action_fail().
1494
services = _extract_services_list_helper(services)
1497
for service in services.keys():
1498
started = service_resume(service)
1500
messages.append("{} didn't start cleanly.".format(service))
1503
message = charm_func()
1505
messages.append(message)
1506
except Exception as e:
1507
message.append(str(e))
1509
if assess_status_func:
1510
message = assess_status_func()
1512
messages.append(message)
1514
raise Exception("Couldn't resume: {}".format("; ".join(messages)))
1517
def make_assess_status_func(*args, **kwargs):
1518
"""Creates an assess_status_func() suitable for handing to pause_unit()
1521
This uses the _determine_os_workload_status(...) function to determine
1522
what the workload_status should be for the unit. If the unit is
1523
not in maintenance or active states, then the message is returned to
1524
the caller. This is so an action that doesn't result in either a
1525
complete pause or complete resume can signal failure with an action_fail()
1527
def _assess_status_func():
1528
state, message = _determine_os_workload_status(*args, **kwargs)
1529
status_set(state, message)
1530
if state not in ['maintenance', 'active']:
1534
return _assess_status_func
1537
def pausable_restart_on_change(restart_map, stopstart=False):
1538
"""A restart_on_change decorator that checks to see if the unit is
1539
paused. If it is paused then the decorated function doesn't fire.
1541
This is provided as a helper, as the @restart_on_change(...) decorator
1542
is in core.host, yet the openstack specific helpers are in this file
1543
(contrib.openstack.utils). Thus, this needs to be an optional feature
1544
for openstack charms (or charms that wish to use the openstack
1545
pause/resume type features).
1547
It is used as follows:
1549
from contrib.openstack.utils import (
1550
pausable_restart_on_change as restart_on_change)
1552
@restart_on_change(restart_map, stopstart=<boolean>)
1556
see core.utils.restart_on_change() for more details.
1558
@param f: the function to decorate
1559
@param restart_map: the restart map {conf_file: [services]}
1560
@param stopstart: DEFAULT false; whether to stop, start or just restart
1561
@returns decorator to use a restart_on_change with pausability
1565
def wrapped_f(*args, **kwargs):
1566
if is_unit_paused_set():
1567
return f(*args, **kwargs)
1568
# otherwise, normal restart_on_change functionality
1569
return restart_on_change_helper(
1570
(lambda: f(*args, **kwargs)), restart_map, stopstart)