~bbaqar/charms/trusty/plumgrid-director/config-changed-fix

« back to all changes in this revision

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

  • Committer: bbaqar at plumgrid
  • Date: 2015-05-19 21:31:00 UTC
  • Revision ID: bbaqar@plumgrid.com-20150519213100-5xdsjx0qzsj3d21k
Adding charmhelpers

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
)
 
48
from charmhelpers.core.decorators import (
 
49
    retry_on_exception,
 
50
)
 
51
from charmhelpers.core.strutils import (
 
52
    bool_from_string,
 
53
)
 
54
 
 
55
DC_RESOURCE_NAME = 'DC'
 
56
 
 
57
 
 
58
class HAIncompleteConfig(Exception):
 
59
    pass
 
60
 
 
61
 
 
62
class CRMResourceNotFound(Exception):
 
63
    pass
 
64
 
 
65
 
 
66
def is_elected_leader(resource):
 
67
    """
 
68
    Returns True if the charm executing this is the elected cluster leader.
 
69
 
 
70
    It relies on two mechanisms to determine leadership:
 
71
        1. If the charm is part of a corosync cluster, call corosync to
 
72
        determine leadership.
 
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.
 
76
    """
 
77
    if is_clustered():
 
78
        if not is_crm_leader(resource):
 
79
            log('Deferring action to CRM leader.', level=INFO)
 
80
            return False
 
81
    else:
 
82
        peers = peer_units()
 
83
        if peers and not oldest_peer(peers):
 
84
            log('Deferring action to oldest service unit.', level=INFO)
 
85
            return False
 
86
    return True
 
87
 
 
88
 
 
89
def is_clustered():
 
90
    for r_id in (relation_ids('ha') or []):
 
91
        for unit in (relation_list(r_id) or []):
 
92
            clustered = relation_get('clustered',
 
93
                                     rid=r_id,
 
94
                                     unit=unit)
 
95
            if clustered:
 
96
                return True
 
97
    return False
 
98
 
 
99
 
 
100
def is_crm_dc():
 
101
    """
 
102
    Determine leadership by querying the pacemaker Designated Controller
 
103
    """
 
104
    cmd = ['crm', 'status']
 
105
    try:
 
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:
 
110
        return False
 
111
    current_dc = ''
 
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():
 
117
        return True
 
118
    return False
 
119
 
 
120
 
 
121
@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
 
122
def is_crm_leader(resource, retry=False):
 
123
    """
 
124
    Returns True if the charm calling this is the elected corosync leader,
 
125
    as returned by calling the external "crm" command.
 
126
 
 
127
    We allow this operation to be retried to avoid the possibility of getting a
 
128
    false negative. See LP #1396246 for more info.
 
129
    """
 
130
    if resource == DC_RESOURCE_NAME:
 
131
        return is_crm_dc()
 
132
    cmd = ['crm', 'resource', 'show', resource]
 
133
    try:
 
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:
 
138
        status = None
 
139
 
 
140
    if status and get_unit_hostname() in status:
 
141
        return True
 
142
 
 
143
    if status and "resource %s is NOT running" % (resource) in status:
 
144
        raise CRMResourceNotFound("CRM resource %s not found" % (resource))
 
145
 
 
146
    return False
 
147
 
 
148
 
 
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)
 
153
 
 
154
 
 
155
def peer_units(peer_relation="cluster"):
 
156
    peers = []
 
157
    for r_id in (relation_ids(peer_relation) or []):
 
158
        for unit in (relation_list(r_id) or []):
 
159
            peers.append(unit)
 
160
    return peers
 
161
 
 
162
 
 
163
def peer_ips(peer_relation='cluster', addr_key='private-address'):
 
164
    '''Return a dict of peers and their private-address'''
 
165
    peers = {}
 
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)
 
169
    return peers
 
170
 
 
171
 
 
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])
 
175
    for peer in peers:
 
176
        remote_unit_no = int(peer.split('/')[1])
 
177
        if remote_unit_no < local_unit_no:
 
178
            return False
 
179
    return True
 
180
 
 
181
 
 
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)
 
186
 
 
187
 
 
188
def https():
 
189
    '''
 
190
    Determines whether enough data has been provided in configuration
 
191
    or relation data to configure HTTPS
 
192
    .
 
193
    returns: boolean
 
194
    '''
 
195
    use_https = config_get('use-https')
 
196
    if use_https and bool_from_string(use_https):
 
197
        return True
 
198
    if config_get('ssl_cert') and config_get('ssl_key'):
 
199
        return True
 
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
 
203
            rel_state = [
 
204
                relation_get('https_keystone', rid=r_id, unit=unit),
 
205
                relation_get('ca_cert', rid=r_id, unit=unit),
 
206
            ]
 
207
            # NOTE: works around (LP: #1203241)
 
208
            if (None not in rel_state) and ('' not in rel_state):
 
209
                return True
 
210
    return False
 
211
 
 
212
 
 
213
def determine_api_port(public_port, singlenode_mode=False):
 
214
    '''
 
215
    Determine correct API server listening port based on
 
216
    existence of HTTPS reverse proxy and/or haproxy.
 
217
 
 
218
    public_port: int: standard public port for given service
 
219
 
 
220
    singlenode_mode: boolean: Shuffle ports when only a single unit is present
 
221
 
 
222
    returns: int: the correct listening port for the API service
 
223
    '''
 
224
    i = 0
 
225
    if singlenode_mode:
 
226
        i += 1
 
227
    elif len(peer_units()) > 0 or is_clustered():
 
228
        i += 1
 
229
    if https():
 
230
        i += 1
 
231
    return public_port - (i * 10)
 
232
 
 
233
 
 
234
def determine_apache_port(public_port, singlenode_mode=False):
 
235
    '''
 
236
    Description: Determine correct apache listening port based on public IP +
 
237
    state of the cluster.
 
238
 
 
239
    public_port: int: standard public port for given service
 
240
 
 
241
    singlenode_mode: boolean: Shuffle ports when only a single unit is present
 
242
 
 
243
    returns: int: the correct listening port for the HAProxy service
 
244
    '''
 
245
    i = 0
 
246
    if singlenode_mode:
 
247
        i += 1
 
248
    elif len(peer_units()) > 0 or is_clustered():
 
249
        i += 1
 
250
    return public_port - (i * 10)
 
251
 
 
252
 
 
253
def get_hacluster_config(exclude_keys=None):
 
254
    '''
 
255
    Obtains all relevant configuration from charm configuration required
 
256
    for initiating a relation to hacluster:
 
257
 
 
258
        ha-bindiface, ha-mcastport, vip
 
259
 
 
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.
 
263
    '''
 
264
    settings = ['ha-bindiface', 'ha-mcastport', 'vip']
 
265
    conf = {}
 
266
    for setting in settings:
 
267
        if exclude_keys and setting in exclude_keys:
 
268
            continue
 
269
 
 
270
        conf[setting] = config_get(setting)
 
271
    missing = []
 
272
    [missing.append(s) for s, v in six.iteritems(conf) if v is None]
 
273
    if missing:
 
274
        log('Insufficient config data to configure hacluster.', level=ERROR)
 
275
        raise HAIncompleteConfig
 
276
    return conf
 
277
 
 
278
 
 
279
def canonical_url(configs, vip_setting='vip'):
 
280
    '''
 
281
    Returns the correct HTTP URL to this host given the state of HTTPS
 
282
    configuration and hacluster.
 
283
 
 
284
    :configs    : OSTemplateRenderer: A config tempating object to inspect for
 
285
                                      a complete https context.
 
286
 
 
287
    :vip_setting:                str: Setting in charm config that specifies
 
288
                                      VIP address.
 
289
    '''
 
290
    scheme = 'http'
 
291
    if 'https' in configs.complete_contexts():
 
292
        scheme = 'https'
 
293
    if is_clustered():
 
294
        addr = config_get(vip_setting)
 
295
    else:
 
296
        addr = unit_get('private-address')
 
297
    return '%s://%s' % (scheme, addr)