~hopem/charms/trusty/swift-proxy/lp1510865-stable-backport

« back to all changes in this revision

Viewing changes to charmhelpers/contrib/hahelpers/cluster.py

  • Committer: James Page
  • Date: 2015-10-22 13:24:57 UTC
  • Revision ID: james.page@ubuntu.com-20151022132457-4p14oifelnzjz5n3
Tags: 15.10
15.10 Charm release

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# This file is part of charm-helpers.
 
4
#
 
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.
 
8
#
 
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.
 
13
#
 
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/>.
 
16
 
 
17
#
 
18
# Copyright 2012 Canonical Ltd.
 
19
#
 
20
# Authors:
 
21
#  James Page <james.page@ubuntu.com>
 
22
#  Adam Gandelman <adamg@ubuntu.com>
 
23
#
 
24
 
 
25
"""
 
26
Helpers for clustering and determining "cluster leadership" and other
 
27
clustering-related helpers.
 
28
"""
 
29
 
 
30
import subprocess
 
31
import os
 
32
 
 
33
from socket import gethostname as get_unit_hostname
 
34
 
 
35
import six
 
36
 
 
37
from charmhelpers.core.hookenv import (
 
38
    log,
 
39
    relation_ids,
 
40
    related_units as relation_list,
 
41
    relation_get,
 
42
    config as config_get,
 
43
    INFO,
 
44
    ERROR,
 
45
    WARNING,
 
46
    unit_get,
 
47
    is_leader as juju_is_leader
 
48
)
 
49
from charmhelpers.core.decorators import (
 
50
    retry_on_exception,
 
51
)
 
52
from charmhelpers.core.strutils import (
 
53
    bool_from_string,
 
54
)
 
55
 
 
56
DC_RESOURCE_NAME = 'DC'
 
57
 
 
58
 
 
59
class HAIncompleteConfig(Exception):
 
60
    pass
 
61
 
 
62
 
 
63
class CRMResourceNotFound(Exception):
 
64
    pass
 
65
 
 
66
 
 
67
class CRMDCNotFound(Exception):
 
68
    pass
 
69
 
 
70
 
 
71
def is_elected_leader(resource):
 
72
    """
 
73
    Returns True if the charm executing this is the elected cluster leader.
 
74
 
 
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
 
79
        determine leadership.
 
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.
 
83
    """
 
84
    try:
 
85
        return juju_is_leader()
 
86
    except NotImplementedError:
 
87
        log('Juju leadership election feature not enabled'
 
88
            ', using fallback support',
 
89
            level=WARNING)
 
90
 
 
91
    if is_clustered():
 
92
        if not is_crm_leader(resource):
 
93
            log('Deferring action to CRM leader.', level=INFO)
 
94
            return False
 
95
    else:
 
96
        peers = peer_units()
 
97
        if peers and not oldest_peer(peers):
 
98
            log('Deferring action to oldest service unit.', level=INFO)
 
99
            return False
 
100
    return True
 
101
 
 
102
 
 
103
def is_clustered():
 
104
    for r_id in (relation_ids('ha') or []):
 
105
        for unit in (relation_list(r_id) or []):
 
106
            clustered = relation_get('clustered',
 
107
                                     rid=r_id,
 
108
                                     unit=unit)
 
109
            if clustered:
 
110
                return True
 
111
    return False
 
112
 
 
113
 
 
114
def is_crm_dc():
 
115
    """
 
116
    Determine leadership by querying the pacemaker Designated Controller
 
117
    """
 
118
    cmd = ['crm', 'status']
 
119
    try:
 
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))
 
125
 
 
126
    current_dc = ''
 
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():
 
132
        return True
 
133
    elif current_dc == 'NONE':
 
134
        raise CRMDCNotFound('Current DC: NONE')
 
135
 
 
136
    return False
 
137
 
 
138
 
 
139
@retry_on_exception(5, base_delay=2,
 
140
                    exc_type=(CRMResourceNotFound, CRMDCNotFound))
 
141
def is_crm_leader(resource, retry=False):
 
142
    """
 
143
    Returns True if the charm calling this is the elected corosync leader,
 
144
    as returned by calling the external "crm" command.
 
145
 
 
146
    We allow this operation to be retried to avoid the possibility of getting a
 
147
    false negative. See LP #1396246 for more info.
 
148
    """
 
149
    if resource == DC_RESOURCE_NAME:
 
150
        return is_crm_dc()
 
151
    cmd = ['crm', 'resource', 'show', resource]
 
152
    try:
 
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:
 
157
        status = None
 
158
 
 
159
    if status and get_unit_hostname() in status:
 
160
        return True
 
161
 
 
162
    if status and "resource %s is NOT running" % (resource) in status:
 
163
        raise CRMResourceNotFound("CRM resource %s not found" % (resource))
 
164
 
 
165
    return False
 
166
 
 
167
 
 
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)
 
172
 
 
173
 
 
174
def peer_units(peer_relation="cluster"):
 
175
    peers = []
 
176
    for r_id in (relation_ids(peer_relation) or []):
 
177
        for unit in (relation_list(r_id) or []):
 
178
            peers.append(unit)
 
179
    return peers
 
180
 
 
181
 
 
182
def peer_ips(peer_relation='cluster', addr_key='private-address'):
 
183
    '''Return a dict of peers and their private-address'''
 
184
    peers = {}
 
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)
 
188
    return peers
 
189
 
 
190
 
 
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])
 
194
    for peer in peers:
 
195
        remote_unit_no = int(peer.split('/')[1])
 
196
        if remote_unit_no < local_unit_no:
 
197
            return False
 
198
    return True
 
199
 
 
200
 
 
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)
 
205
 
 
206
 
 
207
def https():
 
208
    '''
 
209
    Determines whether enough data has been provided in configuration
 
210
    or relation data to configure HTTPS
 
211
    .
 
212
    returns: boolean
 
213
    '''
 
214
    use_https = config_get('use-https')
 
215
    if use_https and bool_from_string(use_https):
 
216
        return True
 
217
    if config_get('ssl_cert') and config_get('ssl_key'):
 
218
        return True
 
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
 
222
            rel_state = [
 
223
                relation_get('https_keystone', rid=r_id, unit=unit),
 
224
                relation_get('ca_cert', rid=r_id, unit=unit),
 
225
            ]
 
226
            # NOTE: works around (LP: #1203241)
 
227
            if (None not in rel_state) and ('' not in rel_state):
 
228
                return True
 
229
    return False
 
230
 
 
231
 
 
232
def determine_api_port(public_port, singlenode_mode=False):
 
233
    '''
 
234
    Determine correct API server listening port based on
 
235
    existence of HTTPS reverse proxy and/or haproxy.
 
236
 
 
237
    public_port: int: standard public port for given service
 
238
 
 
239
    singlenode_mode: boolean: Shuffle ports when only a single unit is present
 
240
 
 
241
    returns: int: the correct listening port for the API service
 
242
    '''
 
243
    i = 0
 
244
    if singlenode_mode:
 
245
        i += 1
 
246
    elif len(peer_units()) > 0 or is_clustered():
 
247
        i += 1
 
248
    if https():
 
249
        i += 1
 
250
    return public_port - (i * 10)
 
251
 
 
252
 
 
253
def determine_apache_port(public_port, singlenode_mode=False):
 
254
    '''
 
255
    Description: Determine correct apache listening port based on public IP +
 
256
    state of the cluster.
 
257
 
 
258
    public_port: int: standard public port for given service
 
259
 
 
260
    singlenode_mode: boolean: Shuffle ports when only a single unit is present
 
261
 
 
262
    returns: int: the correct listening port for the HAProxy service
 
263
    '''
 
264
    i = 0
 
265
    if singlenode_mode:
 
266
        i += 1
 
267
    elif len(peer_units()) > 0 or is_clustered():
 
268
        i += 1
 
269
    return public_port - (i * 10)
 
270
 
 
271
 
 
272
def get_hacluster_config(exclude_keys=None):
 
273
    '''
 
274
    Obtains all relevant configuration from charm configuration required
 
275
    for initiating a relation to hacluster:
 
276
 
 
277
        ha-bindiface, ha-mcastport, vip
 
278
 
 
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.
 
282
    '''
 
283
    settings = ['ha-bindiface', 'ha-mcastport', 'vip']
 
284
    conf = {}
 
285
    for setting in settings:
 
286
        if exclude_keys and setting in exclude_keys:
 
287
            continue
 
288
 
 
289
        conf[setting] = config_get(setting)
 
290
    missing = []
 
291
    [missing.append(s) for s, v in six.iteritems(conf) if v is None]
 
292
    if missing:
 
293
        log('Insufficient config data to configure hacluster.', level=ERROR)
 
294
        raise HAIncompleteConfig
 
295
    return conf
 
296
 
 
297
 
 
298
def canonical_url(configs, vip_setting='vip'):
 
299
    '''
 
300
    Returns the correct HTTP URL to this host given the state of HTTPS
 
301
    configuration and hacluster.
 
302
 
 
303
    :configs    : OSTemplateRenderer: A config tempating object to inspect for
 
304
                                      a complete https context.
 
305
 
 
306
    :vip_setting:                str: Setting in charm config that specifies
 
307
                                      VIP address.
 
308
    '''
 
309
    scheme = 'http'
 
310
    if 'https' in configs.complete_contexts():
 
311
        scheme = 'https'
 
312
    if is_clustered():
 
313
        addr = config_get(vip_setting)
 
314
    else:
 
315
        addr = unit_get('private-address')
 
316
    return '%s://%s' % (scheme, addr)