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,
47
is_leader as juju_is_leader
49
from charmhelpers.core.decorators import (
52
from charmhelpers.core.strutils import (
56
DC_RESOURCE_NAME = 'DC'
59
class HAIncompleteConfig(Exception):
63
class CRMResourceNotFound(Exception):
67
class CRMDCNotFound(Exception):
71
def is_elected_leader(resource):
73
Returns True if the charm executing this is the elected cluster leader.
75
It relies on two mechanisms to determine leadership:
76
1. If juju is sufficiently new and leadership election is supported,
77
the is_leader command will be used.
78
2. If the charm is part of a corosync cluster, call corosync to
80
3. If the charm is not part of a corosync cluster, the leader is
81
determined as being "the alive unit with the lowest unit numer". In
82
other words, the oldest surviving unit.
85
return juju_is_leader()
86
except NotImplementedError:
87
log('Juju leadership election feature not enabled'
88
', using fallback support',
92
if not is_crm_leader(resource):
93
log('Deferring action to CRM leader.', level=INFO)
97
if peers and not oldest_peer(peers):
98
log('Deferring action to oldest service unit.', level=INFO)
104
for r_id in (relation_ids('ha') or []):
105
for unit in (relation_list(r_id) or []):
106
clustered = relation_get('clustered',
116
Determine leadership by querying the pacemaker Designated Controller
118
cmd = ['crm', 'status']
120
status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
121
if not isinstance(status, six.text_type):
122
status = six.text_type(status, "utf-8")
123
except subprocess.CalledProcessError as ex:
124
raise CRMDCNotFound(str(ex))
127
for line in status.split('\n'):
128
if line.startswith('Current DC'):
129
# Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
130
current_dc = line.split(':')[1].split()[0]
131
if current_dc == get_unit_hostname():
133
elif current_dc == 'NONE':
134
raise CRMDCNotFound('Current DC: NONE')
139
@retry_on_exception(5, base_delay=2,
140
exc_type=(CRMResourceNotFound, CRMDCNotFound))
141
def is_crm_leader(resource, retry=False):
143
Returns True if the charm calling this is the elected corosync leader,
144
as returned by calling the external "crm" command.
146
We allow this operation to be retried to avoid the possibility of getting a
147
false negative. See LP #1396246 for more info.
149
if resource == DC_RESOURCE_NAME:
151
cmd = ['crm', 'resource', 'show', resource]
153
status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
154
if not isinstance(status, six.text_type):
155
status = six.text_type(status, "utf-8")
156
except subprocess.CalledProcessError:
159
if status and get_unit_hostname() in status:
162
if status and "resource %s is NOT running" % (resource) in status:
163
raise CRMResourceNotFound("CRM resource %s not found" % (resource))
168
def is_leader(resource):
169
log("is_leader is deprecated. Please consider using is_crm_leader "
170
"instead.", level=WARNING)
171
return is_crm_leader(resource)
174
def peer_units(peer_relation="cluster"):
176
for r_id in (relation_ids(peer_relation) or []):
177
for unit in (relation_list(r_id) or []):
182
def peer_ips(peer_relation='cluster', addr_key='private-address'):
183
'''Return a dict of peers and their private-address'''
185
for r_id in relation_ids(peer_relation):
186
for unit in relation_list(r_id):
187
peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
191
def oldest_peer(peers):
192
"""Determines who the oldest peer is by comparing unit numbers."""
193
local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
195
remote_unit_no = int(peer.split('/')[1])
196
if remote_unit_no < local_unit_no:
201
def eligible_leader(resource):
202
log("eligible_leader is deprecated. Please consider using "
203
"is_elected_leader instead.", level=WARNING)
204
return is_elected_leader(resource)
209
Determines whether enough data has been provided in configuration
210
or relation data to configure HTTPS
214
use_https = config_get('use-https')
215
if use_https and bool_from_string(use_https):
217
if config_get('ssl_cert') and config_get('ssl_key'):
219
for r_id in relation_ids('identity-service'):
220
for unit in relation_list(r_id):
221
# TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
223
relation_get('https_keystone', rid=r_id, unit=unit),
224
relation_get('ca_cert', rid=r_id, unit=unit),
226
# NOTE: works around (LP: #1203241)
227
if (None not in rel_state) and ('' not in rel_state):
232
def determine_api_port(public_port, singlenode_mode=False):
234
Determine correct API server listening port based on
235
existence of HTTPS reverse proxy and/or haproxy.
237
public_port: int: standard public port for given service
239
singlenode_mode: boolean: Shuffle ports when only a single unit is present
241
returns: int: the correct listening port for the API service
246
elif len(peer_units()) > 0 or is_clustered():
250
return public_port - (i * 10)
253
def determine_apache_port(public_port, singlenode_mode=False):
255
Description: Determine correct apache listening port based on public IP +
256
state of the cluster.
258
public_port: int: standard public port for given service
260
singlenode_mode: boolean: Shuffle ports when only a single unit is present
262
returns: int: the correct listening port for the HAProxy service
267
elif len(peer_units()) > 0 or is_clustered():
269
return public_port - (i * 10)
272
def get_hacluster_config(exclude_keys=None):
274
Obtains all relevant configuration from charm configuration required
275
for initiating a relation to hacluster:
277
ha-bindiface, ha-mcastport, vip
279
param: exclude_keys: list of setting key(s) to be excluded.
280
returns: dict: A dict containing settings keyed by setting name.
281
raises: HAIncompleteConfig if settings are missing.
283
settings = ['ha-bindiface', 'ha-mcastport', 'vip']
285
for setting in settings:
286
if exclude_keys and setting in exclude_keys:
289
conf[setting] = config_get(setting)
291
[missing.append(s) for s, v in six.iteritems(conf) if v is None]
293
log('Insufficient config data to configure hacluster.', level=ERROR)
294
raise HAIncompleteConfig
298
def canonical_url(configs, vip_setting='vip'):
300
Returns the correct HTTP URL to this host given the state of HTTPS
301
configuration and hacluster.
303
:configs : OSTemplateRenderer: A config tempating object to inspect for
304
a complete https context.
306
:vip_setting: str: Setting in charm config that specifies
310
if 'https' in configs.complete_contexts():
313
addr = config_get(vip_setting)
315
addr = unit_get('private-address')
316
return '%s://%s' % (scheme, addr)