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 (
51
from charmhelpers.core.strutils import (
56
class HAIncompleteConfig(Exception):
60
class CRMResourceNotFound(Exception):
64
def is_elected_leader(resource):
66
Returns True if the charm executing this is the elected cluster leader.
68
It relies on two mechanisms to determine leadership:
69
1. If the charm is part of a corosync cluster, call corosync to
71
2. If the charm is not part of a corosync cluster, the leader is
72
determined as being "the alive unit with the lowest unit numer". In
73
other words, the oldest surviving unit.
76
if not is_crm_leader(resource):
77
log('Deferring action to CRM leader.', level=INFO)
81
if peers and not oldest_peer(peers):
82
log('Deferring action to oldest service unit.', level=INFO)
88
for r_id in (relation_ids('ha') or []):
89
for unit in (relation_list(r_id) or []):
90
clustered = relation_get('clustered',
98
@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
99
def is_crm_leader(resource, retry=False):
101
Returns True if the charm calling this is the elected corosync leader,
102
as returned by calling the external "crm" command.
104
We allow this operation to be retried to avoid the possibility of getting a
105
false negative. See LP #1396246 for more info.
107
cmd = ['crm', 'resource', 'show', resource]
109
status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
110
if not isinstance(status, six.text_type):
111
status = six.text_type(status, "utf-8")
112
except subprocess.CalledProcessError:
115
if status and get_unit_hostname() in status:
118
if status and "resource %s is NOT running" % (resource) in status:
119
raise CRMResourceNotFound("CRM resource %s not found" % (resource))
124
def is_leader(resource):
125
log("is_leader is deprecated. Please consider using is_crm_leader "
126
"instead.", level=WARNING)
127
return is_crm_leader(resource)
130
def peer_units(peer_relation="cluster"):
132
for r_id in (relation_ids(peer_relation) or []):
133
for unit in (relation_list(r_id) or []):
138
def peer_ips(peer_relation='cluster', addr_key='private-address'):
139
'''Return a dict of peers and their private-address'''
141
for r_id in relation_ids(peer_relation):
142
for unit in relation_list(r_id):
143
peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
147
def oldest_peer(peers):
148
"""Determines who the oldest peer is by comparing unit numbers."""
149
local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
151
remote_unit_no = int(peer.split('/')[1])
152
if remote_unit_no < local_unit_no:
157
def eligible_leader(resource):
158
log("eligible_leader is deprecated. Please consider using "
159
"is_elected_leader instead.", level=WARNING)
160
return is_elected_leader(resource)
165
Determines whether enough data has been provided in configuration
166
or relation data to configure HTTPS
170
use_https = config_get('use-https')
171
if use_https and bool_from_string(use_https):
173
if config_get('ssl_cert') and config_get('ssl_key'):
175
for r_id in relation_ids('identity-service'):
176
for unit in relation_list(r_id):
177
# TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
179
relation_get('https_keystone', rid=r_id, unit=unit),
180
relation_get('ca_cert', rid=r_id, unit=unit),
182
# NOTE: works around (LP: #1203241)
183
if (None not in rel_state) and ('' not in rel_state):
188
def determine_api_port(public_port, singlenode_mode=False):
190
Determine correct API server listening port based on
191
existence of HTTPS reverse proxy and/or haproxy.
193
public_port: int: standard public port for given service
195
singlenode_mode: boolean: Shuffle ports when only a single unit is present
197
returns: int: the correct listening port for the API service
202
elif len(peer_units()) > 0 or is_clustered():
206
return public_port - (i * 10)
209
def determine_apache_port(public_port, singlenode_mode=False):
211
Description: Determine correct apache listening port based on public IP +
212
state of the cluster.
214
public_port: int: standard public port for given service
216
singlenode_mode: boolean: Shuffle ports when only a single unit is present
218
returns: int: the correct listening port for the HAProxy service
223
elif len(peer_units()) > 0 or is_clustered():
225
return public_port - (i * 10)
228
def get_hacluster_config(exclude_keys=None):
230
Obtains all relevant configuration from charm configuration required
231
for initiating a relation to hacluster:
233
ha-bindiface, ha-mcastport, vip
235
param: exclude_keys: list of setting key(s) to be excluded.
236
returns: dict: A dict containing settings keyed by setting name.
237
raises: HAIncompleteConfig if settings are missing.
239
settings = ['ha-bindiface', 'ha-mcastport', 'vip']
241
for setting in settings:
242
if exclude_keys and setting in exclude_keys:
245
conf[setting] = config_get(setting)
247
[missing.append(s) for s, v in six.iteritems(conf) if v is None]
249
log('Insufficient config data to configure hacluster.', level=ERROR)
250
raise HAIncompleteConfig
254
def canonical_url(configs, vip_setting='vip'):
256
Returns the correct HTTP URL to this host given the state of HTTPS
257
configuration and hacluster.
259
:configs : OSTemplateRenderer: A config tempating object to inspect for
260
a complete https context.
262
:vip_setting: str: Setting in charm config that specifies
266
if 'https' in configs.complete_contexts():
269
addr = config_get(vip_setting)
271
addr = unit_get('private-address')
272
return '%s://%s' % (scheme, addr)