536
654
def git_install_requested():
537
"""Returns true if openstack-origin-git is specified."""
538
return config('openstack-origin-git') != "None"
656
Returns true if openstack-origin-git is specified.
658
return config('openstack-origin-git') is not None
541
661
requirements_dir = None
544
def git_clone_and_install(file_name, core_project):
545
"""Clone/install all OpenStack repos specified in yaml config file."""
546
global requirements_dir
548
if file_name == "None":
551
yaml_file = os.path.join(charm_dir(), file_name)
553
# clone/install the requirements project first
554
installed = _git_clone_and_install_subset(yaml_file,
555
whitelist=['requirements'])
556
if 'requirements' not in installed:
557
error_out('requirements git repository must be specified')
559
# clone/install all other projects except requirements and the core project
560
blacklist = ['requirements', core_project]
561
_git_clone_and_install_subset(yaml_file, blacklist=blacklist,
562
update_requirements=True)
564
# clone/install the core project
565
whitelist = [core_project]
566
installed = _git_clone_and_install_subset(yaml_file, whitelist=whitelist,
567
update_requirements=True)
568
if core_project not in installed:
569
error_out('{} git repository must be specified'.format(core_project))
572
def _git_clone_and_install_subset(yaml_file, whitelist=[], blacklist=[],
573
update_requirements=False):
574
"""Clone/install subset of OpenStack repos specified in yaml config file."""
575
global requirements_dir
578
with open(yaml_file, 'r') as fd:
579
projects = yaml.load(fd)
580
for proj, val in projects.items():
581
# The project subset is chosen based on the following 3 rules:
582
# 1) If project is in blacklist, we don't clone/install it, period.
583
# 2) If whitelist is empty, we clone/install everything else.
584
# 3) If whitelist is not empty, we clone/install everything in the
586
if proj in blacklist:
588
if whitelist and proj not in whitelist:
590
repo = val['repository']
591
branch = val['branch']
592
repo_dir = _git_clone_and_install_single(repo, branch,
594
if proj == 'requirements':
595
requirements_dir = repo_dir
596
installed.append(proj)
600
def _git_clone_and_install_single(repo, branch, update_requirements=False):
601
"""Clone and install a single git repository."""
602
dest_parent_dir = "/mnt/openstack-git/"
603
dest_dir = os.path.join(dest_parent_dir, os.path.basename(repo))
605
if not os.path.exists(dest_parent_dir):
606
juju_log('Host dir not mounted at {}. '
607
'Creating directory there instead.'.format(dest_parent_dir))
608
os.mkdir(dest_parent_dir)
610
if not os.path.exists(dest_dir):
611
juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
612
repo_dir = install_remote(repo, dest=dest_parent_dir, branch=branch)
664
def _git_yaml_load(projects_yaml):
666
Load the specified yaml into a dictionary.
668
if not projects_yaml:
671
return yaml.load(projects_yaml)
674
def git_clone_and_install(projects_yaml, core_project):
676
Clone/install all specified OpenStack repositories.
678
The expected format of projects_yaml is:
682
repository: 'git://git.openstack.org/openstack/keystone.git',
683
branch: 'stable/icehouse'}
684
- {name: requirements,
685
repository: 'git://git.openstack.org/openstack/requirements.git',
686
branch: 'stable/icehouse'}
688
directory: /mnt/openstack-git
689
http_proxy: squid-proxy-url
690
https_proxy: squid-proxy-url
692
The directory, http_proxy, and https_proxy keys are optional.
695
global requirements_dir
696
parent_dir = '/mnt/openstack-git'
699
projects = _git_yaml_load(projects_yaml)
700
_git_validate_projects_yaml(projects, core_project)
702
old_environ = dict(os.environ)
704
if 'http_proxy' in projects.keys():
705
http_proxy = projects['http_proxy']
706
os.environ['http_proxy'] = projects['http_proxy']
707
if 'https_proxy' in projects.keys():
708
os.environ['https_proxy'] = projects['https_proxy']
710
if 'directory' in projects.keys():
711
parent_dir = projects['directory']
713
pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
715
# Upgrade setuptools and pip from default virtualenv versions. The default
716
# versions in trusty break master OpenStack branch deployments.
717
for p in ['pip', 'setuptools']:
718
pip_install(p, upgrade=True, proxy=http_proxy,
719
venv=os.path.join(parent_dir, 'venv'))
721
for p in projects['repositories']:
722
repo = p['repository']
725
if 'depth' in p.keys():
727
if p['name'] == 'requirements':
728
repo_dir = _git_clone_and_install_single(repo, branch, depth,
729
parent_dir, http_proxy,
730
update_requirements=False)
731
requirements_dir = repo_dir
733
repo_dir = _git_clone_and_install_single(repo, branch, depth,
734
parent_dir, http_proxy,
735
update_requirements=True)
737
os.environ = old_environ
740
def _git_validate_projects_yaml(projects, core_project):
742
Validate the projects yaml.
744
_git_ensure_key_exists('repositories', projects)
746
for project in projects['repositories']:
747
_git_ensure_key_exists('name', project.keys())
748
_git_ensure_key_exists('repository', project.keys())
749
_git_ensure_key_exists('branch', project.keys())
751
if projects['repositories'][0]['name'] != 'requirements':
752
error_out('{} git repo must be specified first'.format('requirements'))
754
if projects['repositories'][-1]['name'] != core_project:
755
error_out('{} git repo must be specified last'.format(core_project))
758
def _git_ensure_key_exists(key, keys):
760
Ensure that key exists in keys.
763
error_out('openstack-origin-git key \'{}\' is missing'.format(key))
766
def _git_clone_and_install_single(repo, branch, depth, parent_dir, http_proxy,
767
update_requirements):
769
Clone and install a single git repository.
771
if not os.path.exists(parent_dir):
772
juju_log('Directory already exists at {}. '
773
'No need to create directory.'.format(parent_dir))
776
juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
777
repo_dir = install_remote(
778
repo, dest=parent_dir, branch=branch, depth=depth)
780
venv = os.path.join(parent_dir, 'venv')
616
782
if update_requirements:
617
783
if not requirements_dir:
618
784
error_out('requirements repo must be cloned before '
619
785
'updating from global requirements.')
620
_git_update_requirements(repo_dir, requirements_dir)
786
_git_update_requirements(venv, repo_dir, requirements_dir)
622
788
juju_log('Installing git repo from dir: {}'.format(repo_dir))
623
pip_install(repo_dir)
790
pip_install(repo_dir, proxy=http_proxy, venv=venv)
792
pip_install(repo_dir, venv=venv)
628
def _git_update_requirements(package_dir, reqs_dir):
629
"""Update from global requirements.
797
def _git_update_requirements(venv, package_dir, reqs_dir):
799
Update from global requirements.
631
Update an OpenStack git directory's requirements.txt and
632
test-requirements.txt from global-requirements.txt."""
801
Update an OpenStack git directory's requirements.txt and
802
test-requirements.txt from global-requirements.txt.
633
804
orig_dir = os.getcwd()
634
805
os.chdir(reqs_dir)
635
cmd = "python update.py {}".format(package_dir)
806
python = os.path.join(venv, 'bin/python')
807
cmd = [python, 'update.py', package_dir]
637
subprocess.check_call(cmd.split(' '))
809
subprocess.check_call(cmd)
638
810
except subprocess.CalledProcessError:
639
811
package = os.path.basename(package_dir)
640
error_out("Error updating {} from global-requirements.txt".format(package))
812
error_out("Error updating {} from "
813
"global-requirements.txt".format(package))
641
814
os.chdir(orig_dir)
817
def git_pip_venv_dir(projects_yaml):
819
Return the pip virtualenv path.
821
parent_dir = '/mnt/openstack-git'
823
projects = _git_yaml_load(projects_yaml)
825
if 'directory' in projects.keys():
826
parent_dir = projects['directory']
828
return os.path.join(parent_dir, 'venv')
831
def git_src_dir(projects_yaml, project):
833
Return the directory where the specified project's source is located.
835
parent_dir = '/mnt/openstack-git'
837
projects = _git_yaml_load(projects_yaml)
839
if 'directory' in projects.keys():
840
parent_dir = projects['directory']
842
for p in projects['repositories']:
843
if p['name'] == project:
844
return os.path.join(parent_dir, os.path.basename(p['repository']))
849
def git_yaml_value(projects_yaml, key):
851
Return the value in projects_yaml for the specified key.
853
projects = _git_yaml_load(projects_yaml)
855
if key in projects.keys():
861
def os_workload_status(configs, required_interfaces, charm_func=None):
863
Decorator to set workload status based on complete contexts
867
def wrapped_f(*args, **kwargs):
868
# Run the original function first
870
# Set workload status now that contexts have been
872
set_os_workload_status(configs, required_interfaces, charm_func)
877
def set_os_workload_status(configs, required_interfaces, charm_func=None,
878
services=None, ports=None):
879
"""Set the state of the workload status for the charm.
881
This calls _determine_os_workload_status() to get the new state, message
882
and sets the status using status_set()
884
@param configs: a templating.OSConfigRenderer() object
885
@param required_interfaces: {generic: [specific, specific2, ...]}
886
@param charm_func: a callable function that returns state, message. The
887
signature is charm_func(configs) -> (state, message)
888
@param services: list of strings OR dictionary specifying services/ports
889
@param ports: OPTIONAL list of port numbers.
890
@returns state, message: the new workload status, user message
892
state, message = _determine_os_workload_status(
893
configs, required_interfaces, charm_func, services, ports)
894
status_set(state, message)
897
def _determine_os_workload_status(
898
configs, required_interfaces, charm_func=None,
899
services=None, ports=None):
900
"""Determine the state of the workload status for the charm.
902
This function returns the new workload status for the charm based
903
on the state of the interfaces, the paused state and whether the
904
services are actually running and any specified ports are open.
908
1. if the unit should be paused, that it is actually paused. If so the
909
state is 'maintenance' + message, else 'broken'.
910
2. that the interfaces/relations are complete. If they are not then
911
it sets the state to either 'broken' or 'waiting' and an appropriate
913
3. If all the relation data is set, then it checks that the actual
914
services really are running. If not it sets the state to 'broken'.
916
If everything is okay then the state returns 'active'.
918
@param configs: a templating.OSConfigRenderer() object
919
@param required_interfaces: {generic: [specific, specific2, ...]}
920
@param charm_func: a callable function that returns state, message. The
921
signature is charm_func(configs) -> (state, message)
922
@param services: list of strings OR dictionary specifying services/ports
923
@param ports: OPTIONAL list of port numbers.
924
@returns state, message: the new workload status, user message
926
state, message = _ows_check_if_paused(services, ports)
929
state, message = _ows_check_generic_interfaces(
930
configs, required_interfaces)
932
if state != 'maintenance' and charm_func:
933
# _ows_check_charm_func() may modify the state, message
934
state, message = _ows_check_charm_func(
935
state, message, lambda: charm_func(configs))
938
state, message = _ows_check_services_running(services, ports)
942
message = "Unit is ready"
943
juju_log(message, 'INFO')
945
return state, message
948
def _ows_check_if_paused(services=None, ports=None):
949
"""Check if the unit is supposed to be paused, and if so check that the
950
services/ports (if passed) are actually stopped/not being listened to.
952
if the unit isn't supposed to be paused, just return None, None
954
@param services: OPTIONAL services spec or list of service names.
955
@param ports: OPTIONAL list of port numbers.
956
@returns state, message or None, None
958
if is_unit_paused_set():
959
state, message = check_actually_paused(services=services,
962
# we're paused okay, so set maintenance and return
963
state = "maintenance"
964
message = "Paused. Use 'resume' action to resume normal service."
965
return state, message
969
def _ows_check_generic_interfaces(configs, required_interfaces):
970
"""Check the complete contexts to determine the workload status.
972
- Checks for missing or incomplete contexts
973
- juju log details of missing required data.
974
- determines the correct workload status
975
- creates an appropriate message for status_set(...)
977
if there are no problems then the function returns None, None
979
@param configs: a templating.OSConfigRenderer() object
980
@params required_interfaces: {generic_interface: [specific_interface], }
981
@returns state, message or None, None
983
incomplete_rel_data = incomplete_relation_data(configs,
987
missing_relations = set()
988
incomplete_relations = set()
990
for generic_interface, relations_states in incomplete_rel_data.items():
991
related_interface = None
994
for interface, relation_state in relations_states.items():
995
if relation_state.get('related'):
996
related_interface = interface
997
missing_data = relation_state.get('missing_data')
999
# No relation ID for the generic_interface?
1000
if not related_interface:
1001
juju_log("{} relation is missing and must be related for "
1002
"functionality. ".format(generic_interface), 'WARN')
1004
missing_relations.add(generic_interface)
1006
# Relation ID eists but no related unit
1007
if not missing_data:
1008
# Edge case - relation ID exists but departings
1009
_hook_name = hook_name()
1010
if (('departed' in _hook_name or 'broken' in _hook_name) and
1011
related_interface in _hook_name):
1013
missing_relations.add(generic_interface)
1014
juju_log("{} relation's interface, {}, "
1015
"relationship is departed or broken "
1016
"and is required for functionality."
1017
"".format(generic_interface, related_interface),
1019
# Normal case relation ID exists but no related unit
1022
juju_log("{} relations's interface, {}, is related but has"
1023
" no units in the relation."
1024
"".format(generic_interface, related_interface),
1026
# Related unit exists and data missing on the relation
1028
juju_log("{} relation's interface, {}, is related awaiting "
1029
"the following data from the relationship: {}. "
1030
"".format(generic_interface, related_interface,
1031
", ".join(missing_data)), "INFO")
1032
if state != 'blocked':
1034
if generic_interface not in missing_relations:
1035
incomplete_relations.add(generic_interface)
1037
if missing_relations:
1038
message = "Missing relations: {}".format(", ".join(missing_relations))
1039
if incomplete_relations:
1040
message += "; incomplete relations: {}" \
1041
"".format(", ".join(incomplete_relations))
1043
elif incomplete_relations:
1044
message = "Incomplete relations: {}" \
1045
"".format(", ".join(incomplete_relations))
1048
return state, message
1051
def _ows_check_charm_func(state, message, charm_func_with_configs):
1052
"""Run a custom check function for the charm to see if it wants to
1053
change the state. This is only run if not in 'maintenance' and
1054
tests to see if the new state is more important that the previous
1055
one determined by the interfaces/relations check.
1057
@param state: the previously determined state so far.
1058
@param message: the user orientated message so far.
1059
@param charm_func: a callable function that returns state, message
1060
@returns state, message strings.
1062
if charm_func_with_configs:
1063
charm_state, charm_message = charm_func_with_configs()
1064
if charm_state != 'active' and charm_state != 'unknown':
1065
state = workload_state_compare(state, charm_state)
1067
charm_message = charm_message.replace("Incomplete relations: ",
1069
message = "{}, {}".format(message, charm_message)
1071
message = charm_message
1072
return state, message
1075
def _ows_check_services_running(services, ports):
1076
"""Check that the services that should be running are actually running
1077
and that any ports specified are being listened to.
1079
@param services: list of strings OR dictionary specifying services/ports
1080
@param ports: list of ports
1081
@returns state, message: strings or None, None
1085
if services is not None:
1086
services = _extract_services_list_helper(services)
1087
services_running, running = _check_running_services(services)
1088
if not all(running):
1090
"Services not running that should be: {}"
1091
.format(", ".join(_filter_tuples(services_running, False))))
1093
# also verify that the ports that should be open are open
1094
# NB, that ServiceManager objects only OPTIONALLY have ports
1095
map_not_open, ports_open = (
1096
_check_listening_on_services_ports(services))
1097
if not all(ports_open):
1098
# find which service has missing ports. They are in service
1099
# order which makes it a bit easier.
1100
message_parts = {service: ", ".join([str(v) for v in open_ports])
1101
for service, open_ports in map_not_open.items()}
1102
message = ", ".join(
1103
["{}: [{}]".format(s, sp) for s, sp in message_parts.items()])
1105
"Services with ports not open that should be: {}"
1109
if ports is not None:
1110
# and we can also check ports which we don't know the service for
1111
ports_open, ports_open_bools = _check_listening_on_ports_list(ports)
1112
if not all(ports_open_bools):
1114
"Ports which should be open, but are not: {}"
1115
.format(", ".join([str(p) for p, v in ports_open
1119
if state is not None:
1120
message = "; ".join(messages)
1121
return state, message
1126
def _extract_services_list_helper(services):
1127
"""Extract a OrderedDict of {service: [ports]} of the supplied services
1128
for use by the other functions.
1130
The services object can either be:
1131
- None : no services were passed (an empty dict is returned)
1133
- A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1134
- An array of [{'service': service_name, ...}, ...]
1136
@param services: see above
1137
@returns OrderedDict(service: [ports], ...)
1139
if services is None:
1141
if isinstance(services, dict):
1142
services = services.values()
1143
# either extract the list of services from the dictionary, or if
1144
# it is a simple string, use that. i.e. works with mixed lists.
1147
if isinstance(s, dict) and 'service' in s:
1148
_s[s['service']] = s.get('ports', [])
1149
if isinstance(s, str):
1154
def _check_running_services(services):
1155
"""Check that the services dict provided is actually running and provide
1156
a list of (service, boolean) tuples for each service.
1158
Returns both a zipped list of (service, boolean) and a list of booleans
1159
in the same order as the services.
1161
@param services: OrderedDict of strings: [ports], one for each service to
1163
@returns [(service, boolean), ...], : results for checks
1164
[boolean] : just the result of the service checks
1166
services_running = [service_running(s) for s in services]
1167
return list(zip(services, services_running)), services_running
1170
def _check_listening_on_services_ports(services, test=False):
1171
"""Check that the unit is actually listening (has the port open) on the
1172
ports that the service specifies are open. If test is True then the
1173
function returns the services with ports that are open rather than
1176
Returns an OrderedDict of service: ports and a list of booleans
1178
@param services: OrderedDict(service: [port, ...], ...)
1179
@param test: default=False, if False, test for closed, otherwise open.
1180
@returns OrderedDict(service: [port-not-open, ...]...), [boolean]
1182
test = not(not(test)) # ensure test is True or False
1183
all_ports = list(itertools.chain(*services.values()))
1184
ports_states = [port_has_listener('0.0.0.0', p) for p in all_ports]
1185
map_ports = OrderedDict()
1186
matched_ports = [p for p, opened in zip(all_ports, ports_states)
1187
if opened == test] # essentially opened xor test
1188
for service, ports in services.items():
1189
set_ports = set(ports).intersection(matched_ports)
1191
map_ports[service] = set_ports
1192
return map_ports, ports_states
1195
def _check_listening_on_ports_list(ports):
1196
"""Check that the ports list given are being listened to
1198
Returns a list of ports being listened to and a list of the
1201
@param ports: LIST or port numbers.
1202
@returns [(port_num, boolean), ...], [boolean]
1204
ports_open = [port_has_listener('0.0.0.0', p) for p in ports]
1205
return zip(ports, ports_open), ports_open
1208
def _filter_tuples(services_states, state):
1209
"""Return a simple list from a list of tuples according to the condition
1211
@param services_states: LIST of (string, boolean): service and running
1213
@param state: Boolean to match the tuple against.
1214
@returns [LIST of strings] that matched the tuple RHS.
1216
return [s for s, b in services_states if b == state]
1219
def workload_state_compare(current_workload_state, workload_state):
1220
""" Return highest priority of two states"""
1221
hierarchy = {'unknown': -1,
1228
if hierarchy.get(workload_state) is None:
1229
workload_state = 'unknown'
1230
if hierarchy.get(current_workload_state) is None:
1231
current_workload_state = 'unknown'
1233
# Set workload_state based on hierarchy of statuses
1234
if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
1235
return current_workload_state
1237
return workload_state
1240
def incomplete_relation_data(configs, required_interfaces):
1241
"""Check complete contexts against required_interfaces
1242
Return dictionary of incomplete relation data.
1244
configs is an OSConfigRenderer object with configs registered
1246
required_interfaces is a dictionary of required general interfaces
1247
with dictionary values of possible specific interfaces.
1249
required_interfaces = {'database': ['shared-db', 'pgsql-db']}
1251
The interface is said to be satisfied if anyone of the interfaces in the
1252
list has a complete context.
1254
Return dictionary of incomplete or missing required contexts with relation
1255
status of interfaces and any missing data points. Example:
1257
{'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
1258
'zeromq-configuration': {'related': False}},
1260
{'identity-service': {'related': False}},
1262
{'pgsql-db': {'related': False},
1263
'shared-db': {'related': True}}}
1265
complete_ctxts = configs.complete_contexts()
1266
incomplete_relations = [
1268
for svc_type, interfaces in required_interfaces.items()
1269
if not set(interfaces).intersection(complete_ctxts)]
1271
i: configs.get_incomplete_context_data(required_interfaces[i])
1272
for i in incomplete_relations}
1275
def do_action_openstack_upgrade(package, upgrade_callback, configs):
1276
"""Perform action-managed OpenStack upgrade.
1278
Upgrades packages to the configured openstack-origin version and sets
1279
the corresponding action status as a result.
1281
If the charm was installed from source we cannot upgrade it.
1282
For backwards compatibility a config flag (action-managed-upgrade) must
1283
be set for this code to run, otherwise a full service level upgrade will
1284
fire on config-changed.
1286
@param package: package name for determining if upgrade available
1287
@param upgrade_callback: function callback to charm's upgrade function
1288
@param configs: templating object derived from OSConfigRenderer class
1290
@return: True if upgrade successful; False if upgrade failed or skipped
1294
if git_install_requested():
1295
action_set({'outcome': 'installed from source, skipped upgrade.'})
1297
if openstack_upgrade_available(package):
1298
if config('action-managed-upgrade'):
1299
juju_log('Upgrading OpenStack release')
1302
upgrade_callback(configs=configs)
1303
action_set({'outcome': 'success, upgrade completed.'})
1306
action_set({'outcome': 'upgrade failed, see traceback.'})
1307
action_set({'traceback': traceback.format_exc()})
1308
action_fail('do_openstack_upgrade resulted in an '
1311
action_set({'outcome': 'action-managed-upgrade config is '
1312
'False, skipped upgrade.'})
1314
action_set({'outcome': 'no upgrade available.'})
1319
def remote_restart(rel_name, remote_service=None):
1321
'restart-trigger': str(uuid.uuid4()),
1324
trigger['remote-service'] = remote_service
1325
for rid in relation_ids(rel_name):
1326
# This subordinate can be related to two seperate services using
1327
# different subordinate relations so only issue the restart if
1328
# the principle is conencted down the relation we think it is
1329
if related_units(relid=rid):
1330
relation_set(relation_id=rid,
1331
relation_settings=trigger,
1335
def check_actually_paused(services=None, ports=None):
1336
"""Check that services listed in the services object and and ports
1337
are actually closed (not listened to), to verify that the unit is
1340
@param services: See _extract_services_list_helper
1341
@returns status, : string for status (None if okay)
1342
message : string for problem for status_set
1347
if services is not None:
1348
services = _extract_services_list_helper(services)
1349
services_running, services_states = _check_running_services(services)
1350
if any(services_states):
1351
# there shouldn't be any running so this is a problem
1352
messages.append("these services running: {}"
1354
_filter_tuples(services_running, True))))
1356
ports_open, ports_open_bools = (
1357
_check_listening_on_services_ports(services, True))
1358
if any(ports_open_bools):
1359
message_parts = {service: ", ".join([str(v) for v in open_ports])
1360
for service, open_ports in ports_open.items()}
1361
message = ", ".join(
1362
["{}: [{}]".format(s, sp) for s, sp in message_parts.items()])
1364
"these service:ports are open: {}".format(message))
1366
if ports is not None:
1367
ports_open, bools = _check_listening_on_ports_list(ports)
1370
"these ports which should be closed, but are open: {}"
1371
.format(", ".join([str(p) for p, v in ports_open if v])))
1374
message = ("Services should be paused but {}"
1375
.format(", ".join(messages)))
1376
return state, message
1379
def set_unit_paused():
1380
"""Set the unit to a paused state in the local kv() store.
1381
This does NOT actually pause the unit
1383
with unitdata.HookData()() as t:
1385
kv.set('unit-paused', True)
1388
def clear_unit_paused():
1389
"""Clear the unit from a paused state in the local kv() store
1390
This does NOT actually restart any services - it only clears the
1393
with unitdata.HookData()() as t:
1395
kv.set('unit-paused', False)
1398
def is_unit_paused_set():
1399
"""Return the state of the kv().get('unit-paused').
1400
This does NOT verify that the unit really is paused.
1402
To help with units that don't have HookData() (testing)
1403
if it excepts, return False
1406
with unitdata.HookData()() as t:
1408
# transform something truth-y into a Boolean.
1409
return not(not(kv.get('unit-paused')))
1414
def pause_unit(assess_status_func, services=None, ports=None,
1416
"""Pause a unit by stopping the services and setting 'unit-paused'
1417
in the local kv() store.
1419
Also checks that the services have stopped and ports are no longer
1422
An optional charm_func() can be called that can either raise an
1423
Exception or return non None, None to indicate that the unit
1424
didn't pause cleanly.
1426
The signature for charm_func is:
1427
charm_func() -> message: string
1429
charm_func() is executed after any services are stopped, if supplied.
1431
The services object can either be:
1432
- None : no services were passed (an empty dict is returned)
1434
- A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1435
- An array of [{'service': service_name, ...}, ...]
1437
@param assess_status_func: (f() -> message: string | None) or None
1438
@param services: OPTIONAL see above
1439
@param ports: OPTIONAL list of port
1440
@param charm_func: function to run for custom charm pausing.
1442
@raises Exception(message) on an error for action_fail().
1444
services = _extract_services_list_helper(services)
1447
for service in services.keys():
1448
stopped = service_pause(service)
1450
messages.append("{} didn't stop cleanly.".format(service))
1453
message = charm_func()
1455
messages.append(message)
1456
except Exception as e:
1457
message.append(str(e))
1459
if assess_status_func:
1460
message = assess_status_func()
1462
messages.append(message)
1464
raise Exception("Couldn't pause: {}".format("; ".join(messages)))
1467
def resume_unit(assess_status_func, services=None, ports=None,
1469
"""Resume a unit by starting the services and clearning 'unit-paused'
1470
in the local kv() store.
1472
Also checks that the services have started and ports are being listened to.
1474
An optional charm_func() can be called that can either raise an
1475
Exception or return non None to indicate that the unit
1476
didn't resume cleanly.
1478
The signature for charm_func is:
1479
charm_func() -> message: string
1481
charm_func() is executed after any services are started, if supplied.
1483
The services object can either be:
1484
- None : no services were passed (an empty dict is returned)
1486
- A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1487
- An array of [{'service': service_name, ...}, ...]
1489
@param assess_status_func: (f() -> message: string | None) or None
1490
@param services: OPTIONAL see above
1491
@param ports: OPTIONAL list of port
1492
@param charm_func: function to run for custom charm resuming.
1494
@raises Exception(message) on an error for action_fail().
1496
services = _extract_services_list_helper(services)
1499
for service in services.keys():
1500
started = service_resume(service)
1502
messages.append("{} didn't start cleanly.".format(service))
1505
message = charm_func()
1507
messages.append(message)
1508
except Exception as e:
1509
message.append(str(e))
1511
if assess_status_func:
1512
message = assess_status_func()
1514
messages.append(message)
1516
raise Exception("Couldn't resume: {}".format("; ".join(messages)))
1519
def make_assess_status_func(*args, **kwargs):
1520
"""Creates an assess_status_func() suitable for handing to pause_unit()
1523
This uses the _determine_os_workload_status(...) function to determine
1524
what the workload_status should be for the unit. If the unit is
1525
not in maintenance or active states, then the message is returned to
1526
the caller. This is so an action that doesn't result in either a
1527
complete pause or complete resume can signal failure with an action_fail()
1529
def _assess_status_func():
1530
state, message = _determine_os_workload_status(*args, **kwargs)
1531
status_set(state, message)
1532
if state not in ['maintenance', 'active']:
1536
return _assess_status_func
1539
def pausable_restart_on_change(restart_map, stopstart=False,
1540
restart_functions=None):
1541
"""A restart_on_change decorator that checks to see if the unit is
1542
paused. If it is paused then the decorated function doesn't fire.
1544
This is provided as a helper, as the @restart_on_change(...) decorator
1545
is in core.host, yet the openstack specific helpers are in this file
1546
(contrib.openstack.utils). Thus, this needs to be an optional feature
1547
for openstack charms (or charms that wish to use the openstack
1548
pause/resume type features).
1550
It is used as follows:
1552
from contrib.openstack.utils import (
1553
pausable_restart_on_change as restart_on_change)
1555
@restart_on_change(restart_map, stopstart=<boolean>)
1559
see core.utils.restart_on_change() for more details.
1561
@param f: the function to decorate
1562
@param restart_map: the restart map {conf_file: [services]}
1563
@param stopstart: DEFAULT false; whether to stop, start or just restart
1564
@returns decorator to use a restart_on_change with pausability
1568
def wrapped_f(*args, **kwargs):
1569
if is_unit_paused_set():
1570
return f(*args, **kwargs)
1571
# otherwise, normal restart_on_change functionality
1572
return restart_on_change_helper(
1573
(lambda: f(*args, **kwargs)), restart_map, stopstart,