~openstack-charmers-next/charms/xenial/nova-compute/trunk

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/contrib/openstack/utils.py

  • Committer: Edward Hope-Morley
  • Date: 2016-03-24 11:18:41 UTC
  • mto: This revision was merged to the branch mainline in revision 211.
  • Revision ID: edward.hope-morley@canonical.com-20160324111841-99ruo5hjzrpqktlx
Add hardening support

Add charmhelpers.contrib.hardening and calls to install,
config-changed, upgrade-charm and update-status hooks.
Also add new config option to allow one or more hardening
modules to be applied at runtime.

Change-Id: I525c15a14662175f2a68cdcd25a3ab2c92237850

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
import sys
25
25
import re
26
26
import itertools
 
27
import functools
27
28
 
28
29
import six
29
30
import tempfile
69
70
    pip_install,
70
71
)
71
72
 
72
 
from charmhelpers.core.host import lsb_release, mounts, umount, service_running
 
73
from 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
)
73
82
from charmhelpers.fetch import apt_install, apt_cache, install_remote
74
83
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
75
84
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
128
137
    ('liberty',
129
138
        ['2.3.0', '2.4.0', '2.5.0']),
130
139
    ('mitaka',
131
 
        ['2.5.0']),
 
140
        ['2.5.0', '2.6.0']),
132
141
])
133
142
 
134
143
# >= Liberty version->codename mapping
763
772
        os.mkdir(parent_dir)
764
773
 
765
774
    juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
766
 
    repo_dir = install_remote(repo, dest=parent_dir, branch=branch, depth=depth)
 
775
    repo_dir = install_remote(
 
776
        repo, dest=parent_dir, branch=branch, depth=depth)
767
777
 
768
778
    venv = os.path.join(parent_dir, 'venv')
769
779
 
862
872
    return wrap
863
873
 
864
874
 
865
 
def set_os_workload_status(configs, required_interfaces, charm_func=None, services=None, ports=None):
866
 
    """
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.
872
 
 
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.
875
 
 
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
880
 
                   open.
881
 
    @returns None
882
 
    """
883
 
    incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
884
 
    state = 'active'
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.
 
878
 
 
879
    This calls _determine_os_workload_status() to get the new state, message
 
880
    and sets the status using status_set()
 
881
 
 
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
 
889
    """
 
890
    state, message = _determine_os_workload_status(
 
891
        configs, required_interfaces, charm_func, services, ports)
 
892
    status_set(state, message)
 
893
 
 
894
 
 
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.
 
899
 
 
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.
 
903
 
 
904
    This checks:
 
905
 
 
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
 
910
        message.
 
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'.
 
913
 
 
914
    If everything is okay then the state returns 'active'.
 
915
 
 
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
 
923
    """
 
924
    state, message = _ows_check_if_paused(services, ports)
 
925
 
 
926
    if state is None:
 
927
        state, message = _ows_check_generic_interfaces(
 
928
            configs, required_interfaces)
 
929
 
 
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))
 
934
 
 
935
    if state is None:
 
936
        state, message = _ows_check_services_running(services, ports)
 
937
 
 
938
    if state is None:
 
939
        state = 'active'
 
940
        message = "Unit is ready"
 
941
        juju_log(message, 'INFO')
 
942
 
 
943
    return state, message
 
944
 
 
945
 
 
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.
 
949
 
 
950
    if the unit isn't supposed to be paused, just return None, None
 
951
 
 
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
 
955
    """
 
956
    if is_unit_paused_set():
 
957
        state, message = check_actually_paused(services=services,
 
958
                                               ports=ports)
 
959
        if state is None:
 
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
 
964
    return None, None
 
965
 
 
966
 
 
967
def _ows_check_generic_interfaces(configs, required_interfaces):
 
968
    """Check the complete contexts to determine the workload status.
 
969
 
 
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(...)
 
974
 
 
975
    if there are no problems then the function returns None, None
 
976
 
 
977
    @param configs: a templating.OSConfigRenderer() object
 
978
    @params required_interfaces: {generic_interface: [specific_interface], }
 
979
    @returns state, message or None, None
 
980
    """
 
981
    incomplete_rel_data = incomplete_relation_data(configs,
 
982
                                                   required_interfaces)
 
983
    state = None
887
984
    message = None
888
 
    charm_state = None
889
 
    charm_message = None
 
985
    missing_relations = set()
 
986
    incomplete_relations = set()
890
987
 
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')
 
996
                break
 
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)
906
1003
        else:
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),
 
1016
                             "WARN")
919
1017
                # Normal case relation ID exists but no related unit
920
1018
                # (joining)
921
1019
                else:
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),
 
1023
                             "INFO")
925
1024
            # Related unit exists and data missing on the relation
926
1025
            else:
927
1026
                juju_log("{} relation's interface, {}, is related awaiting "
930
1029
                                   ", ".join(missing_data)), "INFO")
931
1030
            if state != 'blocked':
932
1031
                state = 'waiting'
933
 
            if generic_interface not in incomplete_relations \
934
 
                    and generic_interface not in missing_relations:
935
 
                incomplete_relations.append(generic_interface)
 
1032
            if generic_interface not in missing_relations:
 
1033
                incomplete_relations.add(generic_interface)
936
1034
 
937
1035
    if missing_relations:
938
1036
        message = "Missing relations: {}".format(", ".join(missing_relations))
945
1043
                  "".format(", ".join(incomplete_relations))
946
1044
        state = 'waiting'
947
1045
 
948
 
    # Run charm specific checks
949
 
    if charm_func:
950
 
        charm_state, charm_message = charm_func(configs)
 
1046
    return state, message
 
1047
 
 
1048
 
 
1049
def _ows_check_charm_func(state, message, charm_func_with_configs):
 
1050
    """Run a custom check function for the charm to see if it wants to
 
1051
    change the state.  This is only run if not in 'maintenance' and
 
1052
    tests to see if the new state is more important that the previous
 
1053
    one determined by the interfaces/relations check.
 
1054
 
 
1055
    @param state: the previously determined state so far.
 
1056
    @param message: the user orientated message so far.
 
1057
    @param charm_func: a callable function that returns state, message
 
1058
    @returns state, message strings.
 
1059
    """
 
1060
    if charm_func_with_configs:
 
1061
        charm_state, charm_message = charm_func_with_configs()
951
1062
        if charm_state != 'active' and charm_state != 'unknown':
952
1063
            state = workload_state_compare(state, charm_state)
953
1064
            if message:
956
1067
                message = "{}, {}".format(message, charm_message)
957
1068
            else:
958
1069
                message = charm_message
959
 
 
960
 
    # If the charm thinks the unit is active, check that the actual services
961
 
    # really are active.
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.
968
 
        _s = []
969
 
        for s in services:
970
 
            if isinstance(s, dict) and 'service' in s:
971
 
                _s.append(s['service'])
972
 
            if isinstance(s, str):
973
 
                _s.append(s)
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)
977
 
                           if not running]
978
 
            message = ("Services not running that should be: {}"
979
 
                       .format(", ".join(not_running)))
 
1070
    return state, message
 
1071
 
 
1072
 
 
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.
 
1076
 
 
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
 
1080
    """
 
1081
    messages = []
 
1082
    state = 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):
 
1087
            messages.append(
 
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)
988
 
                          for p in all_ports]
989
 
            if not all(ports_open):
990
 
                not_opened = [p for p, opened in zip(all_ports, ports_open)
991
 
                              if not opened]
992
 
                map_not_open = OrderedDict()
993
 
                for service, ports in port_map.items():
994
 
                    closed_ports = set(ports).intersection(not_opened)
995
 
                    if closed_ports:
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'
 
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()])
 
1102
            messages.append(
 
1103
                "Services with ports not open that should be: {}"
 
1104
                .format(message))
 
1105
            state = 'blocked'
1008
1106
 
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):
1013
 
            message = (
 
1109
        ports_open, ports_open_bools = _check_listening_on_ports_list(ports)
 
1110
        if not all(ports_open_bools):
 
1111
            messages.append(
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
1016
1114
                                   if not v])))
1017
1115
            state = 'blocked'
1018
1116
 
1019
 
    # Set to active if all requirements have been met
1020
 
    if state == 'active':
1021
 
        message = "Unit is ready"
1022
 
        juju_log(message, "INFO")
1023
 
 
1024
 
    status_set(state, message)
 
1117
    if state is not None:
 
1118
        message = "; ".join(messages)
 
1119
        return state, message
 
1120
 
 
1121
    return None, None
 
1122
 
 
1123
 
 
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.
 
1127
 
 
1128
    The services object can either be:
 
1129
      - None : no services were passed (an empty dict is returned)
 
1130
      - a list of strings
 
1131
      - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
 
1132
      - An array of [{'service': service_name, ...}, ...]
 
1133
 
 
1134
    @param services: see above
 
1135
    @returns OrderedDict(service: [ports], ...)
 
1136
    """
 
1137
    if services is None:
 
1138
        return {}
 
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.
 
1143
    _s = OrderedDict()
 
1144
    for s in services:
 
1145
        if isinstance(s, dict) and 'service' in s:
 
1146
            _s[s['service']] = s.get('ports', [])
 
1147
        if isinstance(s, str):
 
1148
            _s[s] = []
 
1149
    return _s
 
1150
 
 
1151
 
 
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.
 
1155
 
 
1156
    Returns both a zipped list of (service, boolean) and a list of booleans
 
1157
    in the same order as the services.
 
1158
 
 
1159
    @param services: OrderedDict of strings: [ports], one for each service to
 
1160
                     check.
 
1161
    @returns [(service, boolean), ...], : results for checks
 
1162
             [boolean]                  : just the result of the service checks
 
1163
    """
 
1164
    services_running = [service_running(s) for s in services]
 
1165
    return list(zip(services, services_running)), services_running
 
1166
 
 
1167
 
 
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
 
1172
    closed.
 
1173
 
 
1174
    Returns an OrderedDict of service: ports and a list of booleans
 
1175
 
 
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]
 
1179
    """
 
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)
 
1188
        if set_ports:
 
1189
            map_ports[service] = set_ports
 
1190
    return map_ports, ports_states
 
1191
 
 
1192
 
 
1193
def _check_listening_on_ports_list(ports):
 
1194
    """Check that the ports list given are being listened to
 
1195
 
 
1196
    Returns a list of ports being listened to and a list of the
 
1197
    booleans.
 
1198
 
 
1199
    @param ports: LIST or port numbers.
 
1200
    @returns [(port_num, boolean), ...], [boolean]
 
1201
    """
 
1202
    ports_open = [port_has_listener('0.0.0.0', p) for p in ports]
 
1203
    return zip(ports, ports_open), ports_open
 
1204
 
 
1205
 
 
1206
def _filter_tuples(services_states, state):
 
1207
    """Return a simple list from a list of tuples according to the condition
 
1208
 
 
1209
    @param services_states: LIST of (string, boolean): service and running
 
1210
           state.
 
1211
    @param state: Boolean to match the tuple against.
 
1212
    @returns [LIST of strings] that matched the tuple RHS.
 
1213
    """
 
1214
    return [s for s, b in services_states if b == state]
1025
1215
 
1026
1216
 
1027
1217
def workload_state_compare(current_workload_state, workload_state):
1046
1236
 
1047
1237
 
1048
1238
def incomplete_relation_data(configs, required_interfaces):
1049
 
    """
1050
 
    Check complete contexts against required_interfaces
 
1239
    """Check complete contexts against required_interfaces
1051
1240
    Return dictionary of incomplete relation data.
1052
1241
 
1053
1242
    configs is an OSConfigRenderer object with configs registered
1072
1261
              'shared-db': {'related': True}}}
1073
1262
    """
1074
1263
    complete_ctxts = configs.complete_contexts()
1075
 
    incomplete_relations = []
1076
 
    for svc_type in required_interfaces.keys():
1077
 
        # Avoid duplicates
1078
 
        found_ctxt = False
1079
 
        for interface in required_interfaces[svc_type]:
1080
 
            if interface in complete_ctxts:
1081
 
                found_ctxt = True
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
 
1264
    incomplete_relations = [
 
1265
        svc_type
 
1266
        for svc_type, interfaces in required_interfaces.items()
 
1267
        if not set(interfaces).intersection(complete_ctxts)]
 
1268
    return {
 
1269
        i: configs.get_incomplete_context_data(required_interfaces[i])
 
1270
        for i in incomplete_relations}
1088
1271
 
1089
1272
 
1090
1273
def do_action_openstack_upgrade(package, upgrade_callback, configs):
1145
1328
            relation_set(relation_id=rid,
1146
1329
                         relation_settings=trigger,
1147
1330
                         )
 
1331
 
 
1332
 
 
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
 
1336
    properly paused.
 
1337
 
 
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
 
1341
    """
 
1342
    state = None
 
1343
    message = None
 
1344
    messages = []
 
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: {}"
 
1351
                            .format(", ".join(
 
1352
                                _filter_tuples(services_running, True))))
 
1353
            state = "blocked"
 
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()])
 
1361
            messages.append(
 
1362
                "these service:ports are open: {}".format(message))
 
1363
            state = 'blocked'
 
1364
    if ports is not None:
 
1365
        ports_open, bools = _check_listening_on_ports_list(ports)
 
1366
        if any(bools):
 
1367
            messages.append(
 
1368
                "these ports which should be closed, but are open: {}"
 
1369
                .format(", ".join([str(p) for p, v in ports_open if v])))
 
1370
            state = 'blocked'
 
1371
    if messages:
 
1372
        message = ("Services should be paused but {}"
 
1373
                   .format(", ".join(messages)))
 
1374
    return state, message
 
1375
 
 
1376
 
 
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
 
1380
    """
 
1381
    with unitdata.HookData()() as t:
 
1382
        kv = t[0]
 
1383
        kv.set('unit-paused', True)
 
1384
 
 
1385
 
 
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
 
1389
    local state.
 
1390
    """
 
1391
    with unitdata.HookData()() as t:
 
1392
        kv = t[0]
 
1393
        kv.set('unit-paused', False)
 
1394
 
 
1395
 
 
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.
 
1399
 
 
1400
    To help with units that don't have HookData() (testing)
 
1401
    if it excepts, return False
 
1402
    """
 
1403
    try:
 
1404
        with unitdata.HookData()() as t:
 
1405
            kv = t[0]
 
1406
            # transform something truth-y into a Boolean.
 
1407
            return not(not(kv.get('unit-paused')))
 
1408
    except:
 
1409
        return False
 
1410
 
 
1411
 
 
1412
def pause_unit(assess_status_func, services=None, ports=None,
 
1413
               charm_func=None):
 
1414
    """Pause a unit by stopping the services and setting 'unit-paused'
 
1415
    in the local kv() store.
 
1416
 
 
1417
    Also checks that the services have stopped and ports are no longer
 
1418
    being listened to.
 
1419
 
 
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.
 
1423
 
 
1424
    The signature for charm_func is:
 
1425
    charm_func() -> message: string
 
1426
 
 
1427
    charm_func() is executed after any services are stopped, if supplied.
 
1428
 
 
1429
    The services object can either be:
 
1430
      - None : no services were passed (an empty dict is returned)
 
1431
      - a list of strings
 
1432
      - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
 
1433
      - An array of [{'service': service_name, ...}, ...]
 
1434
 
 
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.
 
1439
    @returns None
 
1440
    @raises Exception(message) on an error for action_fail().
 
1441
    """
 
1442
    services = _extract_services_list_helper(services)
 
1443
    messages = []
 
1444
    if services:
 
1445
        for service in services.keys():
 
1446
            stopped = service_pause(service)
 
1447
            if not stopped:
 
1448
                messages.append("{} didn't stop cleanly.".format(service))
 
1449
    if charm_func:
 
1450
        try:
 
1451
            message = charm_func()
 
1452
            if message:
 
1453
                messages.append(message)
 
1454
        except Exception as e:
 
1455
            message.append(str(e))
 
1456
    set_unit_paused()
 
1457
    if assess_status_func:
 
1458
        message = assess_status_func()
 
1459
        if message:
 
1460
            messages.append(message)
 
1461
    if messages:
 
1462
        raise Exception("Couldn't pause: {}".format("; ".join(messages)))
 
1463
 
 
1464
 
 
1465
def resume_unit(assess_status_func, services=None, ports=None,
 
1466
                charm_func=None):
 
1467
    """Resume a unit by starting the services and clearning 'unit-paused'
 
1468
    in the local kv() store.
 
1469
 
 
1470
    Also checks that the services have started and ports are being listened to.
 
1471
 
 
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.
 
1475
 
 
1476
    The signature for charm_func is:
 
1477
    charm_func() -> message: string
 
1478
 
 
1479
    charm_func() is executed after any services are started, if supplied.
 
1480
 
 
1481
    The services object can either be:
 
1482
      - None : no services were passed (an empty dict is returned)
 
1483
      - a list of strings
 
1484
      - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
 
1485
      - An array of [{'service': service_name, ...}, ...]
 
1486
 
 
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.
 
1491
    @returns None
 
1492
    @raises Exception(message) on an error for action_fail().
 
1493
    """
 
1494
    services = _extract_services_list_helper(services)
 
1495
    messages = []
 
1496
    if services:
 
1497
        for service in services.keys():
 
1498
            started = service_resume(service)
 
1499
            if not started:
 
1500
                messages.append("{} didn't start cleanly.".format(service))
 
1501
    if charm_func:
 
1502
        try:
 
1503
            message = charm_func()
 
1504
            if message:
 
1505
                messages.append(message)
 
1506
        except Exception as e:
 
1507
            message.append(str(e))
 
1508
    clear_unit_paused()
 
1509
    if assess_status_func:
 
1510
        message = assess_status_func()
 
1511
        if message:
 
1512
            messages.append(message)
 
1513
    if messages:
 
1514
        raise Exception("Couldn't resume: {}".format("; ".join(messages)))
 
1515
 
 
1516
 
 
1517
def make_assess_status_func(*args, **kwargs):
 
1518
    """Creates an assess_status_func() suitable for handing to pause_unit()
 
1519
    and resume_unit().
 
1520
 
 
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()
 
1526
    """
 
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']:
 
1531
            return message
 
1532
        return None
 
1533
 
 
1534
    return _assess_status_func
 
1535
 
 
1536
 
 
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.
 
1540
 
 
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).
 
1546
 
 
1547
    It is used as follows:
 
1548
 
 
1549
        from contrib.openstack.utils import (
 
1550
            pausable_restart_on_change as restart_on_change)
 
1551
 
 
1552
        @restart_on_change(restart_map, stopstart=<boolean>)
 
1553
        def some_hook(...):
 
1554
            pass
 
1555
 
 
1556
    see core.utils.restart_on_change() for more details.
 
1557
 
 
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
 
1562
    """
 
1563
    def wrap(f):
 
1564
        @functools.wraps(f)
 
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)
 
1571
        return wrapped_f
 
1572
    return wrap