~junaidali/charms/trusty/plumgrid-director/pg-restart

« back to all changes in this revision

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

  • Committer: bbaqar at plumgrid
  • Date: 2015-07-29 18:07:31 UTC
  • Revision ID: bbaqar@plumgrid.com-20150729180731-ioynar8x3u5pxytc
Addressed reviews by Charmers

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