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 (
55
DC_RESOURCE_NAME = 'DC'
58
class HAIncompleteConfig(Exception):
62
class CRMResourceNotFound(Exception):
66
def is_elected_leader(resource):
68
Returns True if the charm executing this is the elected cluster leader.
70
It relies on two mechanisms to determine leadership:
71
1. If the charm is part of a corosync cluster, call corosync to
73
2. If the charm is not part of a corosync cluster, the leader is
74
determined as being "the alive unit with the lowest unit numer". In
75
other words, the oldest surviving unit.
78
if not is_crm_leader(resource):
79
log('Deferring action to CRM leader.', level=INFO)
83
if peers and not oldest_peer(peers):
84
log('Deferring action to oldest service unit.', level=INFO)
90
for r_id in (relation_ids('ha') or []):
91
for unit in (relation_list(r_id) or []):
92
clustered = relation_get('clustered',
102
Determine leadership by querying the pacemaker Designated Controller
104
cmd = ['crm', 'status']
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
for line in status.split('\n'):
113
if line.startswith('Current DC'):
114
# Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
115
current_dc = line.split(':')[1].split()[0]
116
if current_dc == get_unit_hostname():
121
@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
122
def is_crm_leader(resource, retry=False):
124
Returns True if the charm calling this is the elected corosync leader,
125
as returned by calling the external "crm" command.
127
We allow this operation to be retried to avoid the possibility of getting a
128
false negative. See LP #1396246 for more info.
130
if resource == DC_RESOURCE_NAME:
132
cmd = ['crm', 'resource', 'show', resource]
134
status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
135
if not isinstance(status, six.text_type):
136
status = six.text_type(status, "utf-8")
137
except subprocess.CalledProcessError:
140
if status and get_unit_hostname() in status:
143
if status and "resource %s is NOT running" % (resource) in status:
144
raise CRMResourceNotFound("CRM resource %s not found" % (resource))
149
def is_leader(resource):
150
log("is_leader is deprecated. Please consider using is_crm_leader "
151
"instead.", level=WARNING)
152
return is_crm_leader(resource)
155
def peer_units(peer_relation="cluster"):
157
for r_id in (relation_ids(peer_relation) or []):
158
for unit in (relation_list(r_id) or []):
163
def peer_ips(peer_relation='cluster', addr_key='private-address'):
164
'''Return a dict of peers and their private-address'''
166
for r_id in relation_ids(peer_relation):
167
for unit in relation_list(r_id):
168
peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
172
def oldest_peer(peers):
173
"""Determines who the oldest peer is by comparing unit numbers."""
174
local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
176
remote_unit_no = int(peer.split('/')[1])
177
if remote_unit_no < local_unit_no:
182
def eligible_leader(resource):
183
log("eligible_leader is deprecated. Please consider using "
184
"is_elected_leader instead.", level=WARNING)
185
return is_elected_leader(resource)
190
Determines whether enough data has been provided in configuration
191
or relation data to configure HTTPS
195
use_https = config_get('use-https')
196
if use_https and bool_from_string(use_https):
198
if config_get('ssl_cert') and config_get('ssl_key'):
200
for r_id in relation_ids('identity-service'):
201
for unit in relation_list(r_id):
202
# TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
204
relation_get('https_keystone', rid=r_id, unit=unit),
205
relation_get('ca_cert', rid=r_id, unit=unit),
207
# NOTE: works around (LP: #1203241)
208
if (None not in rel_state) and ('' not in rel_state):
213
def determine_api_port(public_port, singlenode_mode=False):
215
Determine correct API server listening port based on
216
existence of HTTPS reverse proxy and/or haproxy.
218
public_port: int: standard public port for given service
220
singlenode_mode: boolean: Shuffle ports when only a single unit is present
222
returns: int: the correct listening port for the API service
227
elif len(peer_units()) > 0 or is_clustered():
231
return public_port - (i * 10)
234
def determine_apache_port(public_port, singlenode_mode=False):
236
Description: Determine correct apache listening port based on public IP +
237
state of the cluster.
239
public_port: int: standard public port for given service
241
singlenode_mode: boolean: Shuffle ports when only a single unit is present
243
returns: int: the correct listening port for the HAProxy service
248
elif len(peer_units()) > 0 or is_clustered():
250
return public_port - (i * 10)
253
def get_hacluster_config(exclude_keys=None):
255
Obtains all relevant configuration from charm configuration required
256
for initiating a relation to hacluster:
258
ha-bindiface, ha-mcastport, vip
260
param: exclude_keys: list of setting key(s) to be excluded.
261
returns: dict: A dict containing settings keyed by setting name.
262
raises: HAIncompleteConfig if settings are missing.
264
settings = ['ha-bindiface', 'ha-mcastport', 'vip']
266
for setting in settings:
267
if exclude_keys and setting in exclude_keys:
270
conf[setting] = config_get(setting)
272
[missing.append(s) for s, v in six.iteritems(conf) if v is None]
274
log('Insufficient config data to configure hacluster.', level=ERROR)
275
raise HAIncompleteConfig
279
def canonical_url(configs, vip_setting='vip'):
281
Returns the correct HTTP URL to this host given the state of HTTPS
282
configuration and hacluster.
284
:configs : OSTemplateRenderer: A config tempating object to inspect for
285
a complete https context.
287
:vip_setting: str: Setting in charm config that specifies
291
if 'https' in configs.complete_contexts():
294
addr = config_get(vip_setting)
296
addr = unit_get('private-address')
297
return '%s://%s' % (scheme, addr)