1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
18
# Copyright 2012 Canonical Ltd.
21
# James Page <james.page@ubuntu.com>
22
# Adam Gandelman <adamg@ubuntu.com>
26
Helpers for clustering and determining "cluster leadership" and other
27
clustering-related helpers.
33
from socket import gethostname as get_unit_hostname
37
from charmhelpers.core.hookenv import (
40
related_units as relation_list,
48
from charmhelpers.core.decorators import (
53
class HAIncompleteConfig(Exception):
57
class CRMResourceNotFound(Exception):
61
def is_elected_leader(resource):
63
Returns True if the charm executing this is the elected cluster leader.
65
It relies on two mechanisms to determine leadership:
66
1. If the charm is part of a corosync cluster, call corosync to
68
2. If the charm is not part of a corosync cluster, the leader is
69
determined as being "the alive unit with the lowest unit numer". In
70
other words, the oldest surviving unit.
73
if not is_crm_leader(resource):
74
log('Deferring action to CRM leader.', level=INFO)
78
if peers and not oldest_peer(peers):
79
log('Deferring action to oldest service unit.', level=INFO)
85
for r_id in (relation_ids('ha') or []):
86
for unit in (relation_list(r_id) or []):
87
clustered = relation_get('clustered',
95
@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
96
def is_crm_leader(resource, retry=False):
98
Returns True if the charm calling this is the elected corosync leader,
99
as returned by calling the external "crm" command.
101
We allow this operation to be retried to avoid the possibility of getting a
102
false negative. See LP #1396246 for more info.
104
cmd = ['crm', 'resource', 'show', resource]
106
status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
107
if not isinstance(status, six.text_type):
108
status = six.text_type(status, "utf-8")
109
except subprocess.CalledProcessError:
112
if status and get_unit_hostname() in status:
115
if status and "resource %s is NOT running" % (resource) in status:
116
raise CRMResourceNotFound("CRM resource %s not found" % (resource))
121
def is_leader(resource):
122
log("is_leader is deprecated. Please consider using is_crm_leader "
123
"instead.", level=WARNING)
124
return is_crm_leader(resource)
127
def peer_units(peer_relation="cluster"):
129
for r_id in (relation_ids(peer_relation) or []):
130
for unit in (relation_list(r_id) or []):
135
def peer_ips(peer_relation='cluster', addr_key='private-address'):
136
'''Return a dict of peers and their private-address'''
138
for r_id in relation_ids(peer_relation):
139
for unit in relation_list(r_id):
140
peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
144
def oldest_peer(peers):
145
"""Determines who the oldest peer is by comparing unit numbers."""
146
local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
148
remote_unit_no = int(peer.split('/')[1])
149
if remote_unit_no < local_unit_no:
154
def eligible_leader(resource):
155
log("eligible_leader is deprecated. Please consider using "
156
"is_elected_leader instead.", level=WARNING)
157
return is_elected_leader(resource)
162
Determines whether enough data has been provided in configuration
163
or relation data to configure HTTPS
167
if config_get('use-https') == "yes":
169
if config_get('ssl_cert') and config_get('ssl_key'):
171
for r_id in relation_ids('identity-service'):
172
for unit in relation_list(r_id):
173
# TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
175
relation_get('https_keystone', rid=r_id, unit=unit),
176
relation_get('ca_cert', rid=r_id, unit=unit),
178
# NOTE: works around (LP: #1203241)
179
if (None not in rel_state) and ('' not in rel_state):
184
def determine_api_port(public_port, singlenode_mode=False):
186
Determine correct API server listening port based on
187
existence of HTTPS reverse proxy and/or haproxy.
189
public_port: int: standard public port for given service
191
singlenode_mode: boolean: Shuffle ports when only a single unit is present
193
returns: int: the correct listening port for the API service
198
elif len(peer_units()) > 0 or is_clustered():
202
return public_port - (i * 10)
205
def determine_apache_port(public_port, singlenode_mode=False):
207
Description: Determine correct apache listening port based on public IP +
208
state of the cluster.
210
public_port: int: standard public port for given service
212
singlenode_mode: boolean: Shuffle ports when only a single unit is present
214
returns: int: the correct listening port for the HAProxy service
219
elif len(peer_units()) > 0 or is_clustered():
221
return public_port - (i * 10)
224
def get_hacluster_config(exclude_keys=None):
226
Obtains all relevant configuration from charm configuration required
227
for initiating a relation to hacluster:
229
ha-bindiface, ha-mcastport, vip
231
param: exclude_keys: list of setting key(s) to be excluded.
232
returns: dict: A dict containing settings keyed by setting name.
233
raises: HAIncompleteConfig if settings are missing.
235
settings = ['ha-bindiface', 'ha-mcastport', 'vip']
237
for setting in settings:
238
if exclude_keys and setting in exclude_keys:
241
conf[setting] = config_get(setting)
243
[missing.append(s) for s, v in six.iteritems(conf) if v is None]
245
log('Insufficient config data to configure hacluster.', level=ERROR)
246
raise HAIncompleteConfig
250
def canonical_url(configs, vip_setting='vip'):
252
Returns the correct HTTP URL to this host given the state of HTTPS
253
configuration and hacluster.
255
:configs : OSTemplateRenderer: A config tempating object to inspect for
256
a complete https context.
258
:vip_setting: str: Setting in charm config that specifies
262
if 'https' in configs.complete_contexts():
265
addr = config_get(vip_setting)
267
addr = unit_get('private-address')
268
return '%s://%s' % (scheme, addr)