704
855
return projects[key]
860
def os_workload_status(configs, required_interfaces, charm_func=None):
862
Decorator to set workload status based on complete contexts
866
def wrapped_f(*args, **kwargs):
867
# Run the original function first
869
# Set workload status now that contexts have been
871
set_os_workload_status(configs, required_interfaces, charm_func)
876
def set_os_workload_status(configs, required_interfaces, charm_func=None,
877
services=None, ports=None):
878
"""Set the state of the workload status for the charm.
880
This calls _determine_os_workload_status() to get the new state, message
881
and sets the status using status_set()
883
@param configs: a templating.OSConfigRenderer() object
884
@param required_interfaces: {generic: [specific, specific2, ...]}
885
@param charm_func: a callable function that returns state, message. The
886
signature is charm_func(configs) -> (state, message)
887
@param services: list of strings OR dictionary specifying services/ports
888
@param ports: OPTIONAL list of port numbers.
889
@returns state, message: the new workload status, user message
891
state, message = _determine_os_workload_status(
892
configs, required_interfaces, charm_func, services, ports)
893
status_set(state, message)
896
def _determine_os_workload_status(
897
configs, required_interfaces, charm_func=None,
898
services=None, ports=None):
899
"""Determine the state of the workload status for the charm.
901
This function returns the new workload status for the charm based
902
on the state of the interfaces, the paused state and whether the
903
services are actually running and any specified ports are open.
907
1. if the unit should be paused, that it is actually paused. If so the
908
state is 'maintenance' + message, else 'broken'.
909
2. that the interfaces/relations are complete. If they are not then
910
it sets the state to either 'broken' or 'waiting' and an appropriate
912
3. If all the relation data is set, then it checks that the actual
913
services really are running. If not it sets the state to 'broken'.
915
If everything is okay then the state returns 'active'.
917
@param configs: a templating.OSConfigRenderer() object
918
@param required_interfaces: {generic: [specific, specific2, ...]}
919
@param charm_func: a callable function that returns state, message. The
920
signature is charm_func(configs) -> (state, message)
921
@param services: list of strings OR dictionary specifying services/ports
922
@param ports: OPTIONAL list of port numbers.
923
@returns state, message: the new workload status, user message
925
state, message = _ows_check_if_paused(services, ports)
928
state, message = _ows_check_generic_interfaces(
929
configs, required_interfaces)
931
if state != 'maintenance' and charm_func:
932
# _ows_check_charm_func() may modify the state, message
933
state, message = _ows_check_charm_func(
934
state, message, lambda: charm_func(configs))
937
state, message = _ows_check_services_running(services, ports)
941
message = "Unit is ready"
942
juju_log(message, 'INFO')
944
return state, message
947
def _ows_check_if_paused(services=None, ports=None):
948
"""Check if the unit is supposed to be paused, and if so check that the
949
services/ports (if passed) are actually stopped/not being listened to.
951
if the unit isn't supposed to be paused, just return None, None
953
@param services: OPTIONAL services spec or list of service names.
954
@param ports: OPTIONAL list of port numbers.
955
@returns state, message or None, None
957
if is_unit_paused_set():
958
state, message = check_actually_paused(services=services,
961
# we're paused okay, so set maintenance and return
962
state = "maintenance"
963
message = "Paused. Use 'resume' action to resume normal service."
964
return state, message
968
def _ows_check_generic_interfaces(configs, required_interfaces):
969
"""Check the complete contexts to determine the workload status.
971
- Checks for missing or incomplete contexts
972
- juju log details of missing required data.
973
- determines the correct workload status
974
- creates an appropriate message for status_set(...)
976
if there are no problems then the function returns None, None
978
@param configs: a templating.OSConfigRenderer() object
979
@params required_interfaces: {generic_interface: [specific_interface], }
980
@returns state, message or None, None
982
incomplete_rel_data = incomplete_relation_data(configs,
986
missing_relations = set()
987
incomplete_relations = set()
989
for generic_interface, relations_states in incomplete_rel_data.items():
990
related_interface = None
993
for interface, relation_state in relations_states.items():
994
if relation_state.get('related'):
995
related_interface = interface
996
missing_data = relation_state.get('missing_data')
998
# No relation ID for the generic_interface?
999
if not related_interface:
1000
juju_log("{} relation is missing and must be related for "
1001
"functionality. ".format(generic_interface), 'WARN')
1003
missing_relations.add(generic_interface)
1005
# Relation ID eists but no related unit
1006
if not missing_data:
1007
# Edge case - relation ID exists but departings
1008
_hook_name = hook_name()
1009
if (('departed' in _hook_name or 'broken' in _hook_name) and
1010
related_interface in _hook_name):
1012
missing_relations.add(generic_interface)
1013
juju_log("{} relation's interface, {}, "
1014
"relationship is departed or broken "
1015
"and is required for functionality."
1016
"".format(generic_interface, related_interface),
1018
# Normal case relation ID exists but no related unit
1021
juju_log("{} relations's interface, {}, is related but has"
1022
" no units in the relation."
1023
"".format(generic_interface, related_interface),
1025
# Related unit exists and data missing on the relation
1027
juju_log("{} relation's interface, {}, is related awaiting "
1028
"the following data from the relationship: {}. "
1029
"".format(generic_interface, related_interface,
1030
", ".join(missing_data)), "INFO")
1031
if state != 'blocked':
1033
if generic_interface not in missing_relations:
1034
incomplete_relations.add(generic_interface)
1036
if missing_relations:
1037
message = "Missing relations: {}".format(", ".join(missing_relations))
1038
if incomplete_relations:
1039
message += "; incomplete relations: {}" \
1040
"".format(", ".join(incomplete_relations))
1042
elif incomplete_relations:
1043
message = "Incomplete relations: {}" \
1044
"".format(", ".join(incomplete_relations))
1047
return state, message
1050
def _ows_check_charm_func(state, message, charm_func_with_configs):
1051
"""Run a custom check function for the charm to see if it wants to
1052
change the state. This is only run if not in 'maintenance' and
1053
tests to see if the new state is more important that the previous
1054
one determined by the interfaces/relations check.
1056
@param state: the previously determined state so far.
1057
@param message: the user orientated message so far.
1058
@param charm_func: a callable function that returns state, message
1059
@returns state, message strings.
1061
if charm_func_with_configs:
1062
charm_state, charm_message = charm_func_with_configs()
1063
if charm_state != 'active' and charm_state != 'unknown':
1064
state = workload_state_compare(state, charm_state)
1066
charm_message = charm_message.replace("Incomplete relations: ",
1068
message = "{}, {}".format(message, charm_message)
1070
message = charm_message
1071
return state, message
1074
def _ows_check_services_running(services, ports):
1075
"""Check that the services that should be running are actually running
1076
and that any ports specified are being listened to.
1078
@param services: list of strings OR dictionary specifying services/ports
1079
@param ports: list of ports
1080
@returns state, message: strings or None, None
1084
if services is not None:
1085
services = _extract_services_list_helper(services)
1086
services_running, running = _check_running_services(services)
1087
if not all(running):
1089
"Services not running that should be: {}"
1090
.format(", ".join(_filter_tuples(services_running, False))))
1092
# also verify that the ports that should be open are open
1093
# NB, that ServiceManager objects only OPTIONALLY have ports
1094
map_not_open, ports_open = (
1095
_check_listening_on_services_ports(services))
1096
if not all(ports_open):
1097
# find which service has missing ports. They are in service
1098
# order which makes it a bit easier.
1099
message_parts = {service: ", ".join([str(v) for v in open_ports])
1100
for service, open_ports in map_not_open.items()}
1101
message = ", ".join(
1102
["{}: [{}]".format(s, sp) for s, sp in message_parts.items()])
1104
"Services with ports not open that should be: {}"
1108
if ports is not None:
1109
# and we can also check ports which we don't know the service for
1110
ports_open, ports_open_bools = _check_listening_on_ports_list(ports)
1111
if not all(ports_open_bools):
1113
"Ports which should be open, but are not: {}"
1114
.format(", ".join([str(p) for p, v in ports_open
1118
if state is not None:
1119
message = "; ".join(messages)
1120
return state, message
1125
def _extract_services_list_helper(services):
1126
"""Extract a OrderedDict of {service: [ports]} of the supplied services
1127
for use by the other functions.
1129
The services object can either be:
1130
- None : no services were passed (an empty dict is returned)
1132
- A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1133
- An array of [{'service': service_name, ...}, ...]
1135
@param services: see above
1136
@returns OrderedDict(service: [ports], ...)
1138
if services is None:
1140
if isinstance(services, dict):
1141
services = services.values()
1142
# either extract the list of services from the dictionary, or if
1143
# it is a simple string, use that. i.e. works with mixed lists.
1146
if isinstance(s, dict) and 'service' in s:
1147
_s[s['service']] = s.get('ports', [])
1148
if isinstance(s, str):
1153
def _check_running_services(services):
1154
"""Check that the services dict provided is actually running and provide
1155
a list of (service, boolean) tuples for each service.
1157
Returns both a zipped list of (service, boolean) and a list of booleans
1158
in the same order as the services.
1160
@param services: OrderedDict of strings: [ports], one for each service to
1162
@returns [(service, boolean), ...], : results for checks
1163
[boolean] : just the result of the service checks
1165
services_running = [service_running(s) for s in services]
1166
return list(zip(services, services_running)), services_running
1169
def _check_listening_on_services_ports(services, test=False):
1170
"""Check that the unit is actually listening (has the port open) on the
1171
ports that the service specifies are open. If test is True then the
1172
function returns the services with ports that are open rather than
1175
Returns an OrderedDict of service: ports and a list of booleans
1177
@param services: OrderedDict(service: [port, ...], ...)
1178
@param test: default=False, if False, test for closed, otherwise open.
1179
@returns OrderedDict(service: [port-not-open, ...]...), [boolean]
1181
test = not(not(test)) # ensure test is True or False
1182
all_ports = list(itertools.chain(*services.values()))
1183
ports_states = [port_has_listener('0.0.0.0', p) for p in all_ports]
1184
map_ports = OrderedDict()
1185
matched_ports = [p for p, opened in zip(all_ports, ports_states)
1186
if opened == test] # essentially opened xor test
1187
for service, ports in services.items():
1188
set_ports = set(ports).intersection(matched_ports)
1190
map_ports[service] = set_ports
1191
return map_ports, ports_states
1194
def _check_listening_on_ports_list(ports):
1195
"""Check that the ports list given are being listened to
1197
Returns a list of ports being listened to and a list of the
1200
@param ports: LIST or port numbers.
1201
@returns [(port_num, boolean), ...], [boolean]
1203
ports_open = [port_has_listener('0.0.0.0', p) for p in ports]
1204
return zip(ports, ports_open), ports_open
1207
def _filter_tuples(services_states, state):
1208
"""Return a simple list from a list of tuples according to the condition
1210
@param services_states: LIST of (string, boolean): service and running
1212
@param state: Boolean to match the tuple against.
1213
@returns [LIST of strings] that matched the tuple RHS.
1215
return [s for s, b in services_states if b == state]
1218
def workload_state_compare(current_workload_state, workload_state):
1219
""" Return highest priority of two states"""
1220
hierarchy = {'unknown': -1,
1227
if hierarchy.get(workload_state) is None:
1228
workload_state = 'unknown'
1229
if hierarchy.get(current_workload_state) is None:
1230
current_workload_state = 'unknown'
1232
# Set workload_state based on hierarchy of statuses
1233
if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
1234
return current_workload_state
1236
return workload_state
1239
def incomplete_relation_data(configs, required_interfaces):
1240
"""Check complete contexts against required_interfaces
1241
Return dictionary of incomplete relation data.
1243
configs is an OSConfigRenderer object with configs registered
1245
required_interfaces is a dictionary of required general interfaces
1246
with dictionary values of possible specific interfaces.
1248
required_interfaces = {'database': ['shared-db', 'pgsql-db']}
1250
The interface is said to be satisfied if anyone of the interfaces in the
1251
list has a complete context.
1253
Return dictionary of incomplete or missing required contexts with relation
1254
status of interfaces and any missing data points. Example:
1256
{'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
1257
'zeromq-configuration': {'related': False}},
1259
{'identity-service': {'related': False}},
1261
{'pgsql-db': {'related': False},
1262
'shared-db': {'related': True}}}
1264
complete_ctxts = configs.complete_contexts()
1265
incomplete_relations = [
1267
for svc_type, interfaces in required_interfaces.items()
1268
if not set(interfaces).intersection(complete_ctxts)]
1270
i: configs.get_incomplete_context_data(required_interfaces[i])
1271
for i in incomplete_relations}
1274
def do_action_openstack_upgrade(package, upgrade_callback, configs):
1275
"""Perform action-managed OpenStack upgrade.
1277
Upgrades packages to the configured openstack-origin version and sets
1278
the corresponding action status as a result.
1280
If the charm was installed from source we cannot upgrade it.
1281
For backwards compatibility a config flag (action-managed-upgrade) must
1282
be set for this code to run, otherwise a full service level upgrade will
1283
fire on config-changed.
1285
@param package: package name for determining if upgrade available
1286
@param upgrade_callback: function callback to charm's upgrade function
1287
@param configs: templating object derived from OSConfigRenderer class
1289
@return: True if upgrade successful; False if upgrade failed or skipped
1293
if git_install_requested():
1294
action_set({'outcome': 'installed from source, skipped upgrade.'})
1296
if openstack_upgrade_available(package):
1297
if config('action-managed-upgrade'):
1298
juju_log('Upgrading OpenStack release')
1301
upgrade_callback(configs=configs)
1302
action_set({'outcome': 'success, upgrade completed.'})
1305
action_set({'outcome': 'upgrade failed, see traceback.'})
1306
action_set({'traceback': traceback.format_exc()})
1307
action_fail('do_openstack_upgrade resulted in an '
1310
action_set({'outcome': 'action-managed-upgrade config is '
1311
'False, skipped upgrade.'})
1313
action_set({'outcome': 'no upgrade available.'})
1318
def remote_restart(rel_name, remote_service=None):
1320
'restart-trigger': str(uuid.uuid4()),
1323
trigger['remote-service'] = remote_service
1324
for rid in relation_ids(rel_name):
1325
# This subordinate can be related to two seperate services using
1326
# different subordinate relations so only issue the restart if
1327
# the principle is conencted down the relation we think it is
1328
if related_units(relid=rid):
1329
relation_set(relation_id=rid,
1330
relation_settings=trigger,
1334
def check_actually_paused(services=None, ports=None):
1335
"""Check that services listed in the services object and and ports
1336
are actually closed (not listened to), to verify that the unit is
1339
@param services: See _extract_services_list_helper
1340
@returns status, : string for status (None if okay)
1341
message : string for problem for status_set
1346
if services is not None:
1347
services = _extract_services_list_helper(services)
1348
services_running, services_states = _check_running_services(services)
1349
if any(services_states):
1350
# there shouldn't be any running so this is a problem
1351
messages.append("these services running: {}"
1353
_filter_tuples(services_running, True))))
1355
ports_open, ports_open_bools = (
1356
_check_listening_on_services_ports(services, True))
1357
if any(ports_open_bools):
1358
message_parts = {service: ", ".join([str(v) for v in open_ports])
1359
for service, open_ports in ports_open.items()}
1360
message = ", ".join(
1361
["{}: [{}]".format(s, sp) for s, sp in message_parts.items()])
1363
"these service:ports are open: {}".format(message))
1365
if ports is not None:
1366
ports_open, bools = _check_listening_on_ports_list(ports)
1369
"these ports which should be closed, but are open: {}"
1370
.format(", ".join([str(p) for p, v in ports_open if v])))
1373
message = ("Services should be paused but {}"
1374
.format(", ".join(messages)))
1375
return state, message
1378
def set_unit_paused():
1379
"""Set the unit to a paused state in the local kv() store.
1380
This does NOT actually pause the unit
1382
with unitdata.HookData()() as t:
1384
kv.set('unit-paused', True)
1387
def clear_unit_paused():
1388
"""Clear the unit from a paused state in the local kv() store
1389
This does NOT actually restart any services - it only clears the
1392
with unitdata.HookData()() as t:
1394
kv.set('unit-paused', False)
1397
def is_unit_paused_set():
1398
"""Return the state of the kv().get('unit-paused').
1399
This does NOT verify that the unit really is paused.
1401
To help with units that don't have HookData() (testing)
1402
if it excepts, return False
1405
with unitdata.HookData()() as t:
1407
# transform something truth-y into a Boolean.
1408
return not(not(kv.get('unit-paused')))
1413
def pause_unit(assess_status_func, services=None, ports=None,
1415
"""Pause a unit by stopping the services and setting 'unit-paused'
1416
in the local kv() store.
1418
Also checks that the services have stopped and ports are no longer
1421
An optional charm_func() can be called that can either raise an
1422
Exception or return non None, None to indicate that the unit
1423
didn't pause cleanly.
1425
The signature for charm_func is:
1426
charm_func() -> message: string
1428
charm_func() is executed after any services are stopped, if supplied.
1430
The services object can either be:
1431
- None : no services were passed (an empty dict is returned)
1433
- A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1434
- An array of [{'service': service_name, ...}, ...]
1436
@param assess_status_func: (f() -> message: string | None) or None
1437
@param services: OPTIONAL see above
1438
@param ports: OPTIONAL list of port
1439
@param charm_func: function to run for custom charm pausing.
1441
@raises Exception(message) on an error for action_fail().
1443
services = _extract_services_list_helper(services)
1446
for service in services.keys():
1447
stopped = service_pause(service)
1449
messages.append("{} didn't stop cleanly.".format(service))
1452
message = charm_func()
1454
messages.append(message)
1455
except Exception as e:
1456
message.append(str(e))
1458
if assess_status_func:
1459
message = assess_status_func()
1461
messages.append(message)
1463
raise Exception("Couldn't pause: {}".format("; ".join(messages)))
1466
def resume_unit(assess_status_func, services=None, ports=None,
1468
"""Resume a unit by starting the services and clearning 'unit-paused'
1469
in the local kv() store.
1471
Also checks that the services have started and ports are being listened to.
1473
An optional charm_func() can be called that can either raise an
1474
Exception or return non None to indicate that the unit
1475
didn't resume cleanly.
1477
The signature for charm_func is:
1478
charm_func() -> message: string
1480
charm_func() is executed after any services are started, if supplied.
1482
The services object can either be:
1483
- None : no services were passed (an empty dict is returned)
1485
- A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1486
- An array of [{'service': service_name, ...}, ...]
1488
@param assess_status_func: (f() -> message: string | None) or None
1489
@param services: OPTIONAL see above
1490
@param ports: OPTIONAL list of port
1491
@param charm_func: function to run for custom charm resuming.
1493
@raises Exception(message) on an error for action_fail().
1495
services = _extract_services_list_helper(services)
1498
for service in services.keys():
1499
started = service_resume(service)
1501
messages.append("{} didn't start cleanly.".format(service))
1504
message = charm_func()
1506
messages.append(message)
1507
except Exception as e:
1508
message.append(str(e))
1510
if assess_status_func:
1511
message = assess_status_func()
1513
messages.append(message)
1515
raise Exception("Couldn't resume: {}".format("; ".join(messages)))
1518
def make_assess_status_func(*args, **kwargs):
1519
"""Creates an assess_status_func() suitable for handing to pause_unit()
1522
This uses the _determine_os_workload_status(...) function to determine
1523
what the workload_status should be for the unit. If the unit is
1524
not in maintenance or active states, then the message is returned to
1525
the caller. This is so an action that doesn't result in either a
1526
complete pause or complete resume can signal failure with an action_fail()
1528
def _assess_status_func():
1529
state, message = _determine_os_workload_status(*args, **kwargs)
1530
status_set(state, message)
1531
if state not in ['maintenance', 'active']:
1535
return _assess_status_func
1538
def pausable_restart_on_change(restart_map, stopstart=False,
1539
restart_functions=None):
1540
"""A restart_on_change decorator that checks to see if the unit is
1541
paused. If it is paused then the decorated function doesn't fire.
1543
This is provided as a helper, as the @restart_on_change(...) decorator
1544
is in core.host, yet the openstack specific helpers are in this file
1545
(contrib.openstack.utils). Thus, this needs to be an optional feature
1546
for openstack charms (or charms that wish to use the openstack
1547
pause/resume type features).
1549
It is used as follows:
1551
from contrib.openstack.utils import (
1552
pausable_restart_on_change as restart_on_change)
1554
@restart_on_change(restart_map, stopstart=<boolean>)
1558
see core.utils.restart_on_change() for more details.
1560
@param f: the function to decorate
1561
@param restart_map: the restart map {conf_file: [services]}
1562
@param stopstart: DEFAULT false; whether to stop, start or just restart
1563
@returns decorator to use a restart_on_change with pausability
1567
def wrapped_f(*args, **kwargs):
1568
if is_unit_paused_set():
1569
return f(*args, **kwargs)
1570
# otherwise, normal restart_on_change functionality
1571
return restart_on_change_helper(
1572
(lambda: f(*args, **kwargs)), restart_map, stopstart,