~psivaa/charms/trusty/core-image-publisher/core-worker-nopasswd

« back to all changes in this revision

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

  • Committer: Celso Providelo
  • Date: 2015-03-25 04:13:43 UTC
  • Revision ID: celso.providelo@canonical.com-20150325041343-jw05jaz6jscs3c8f
fork of core-image-watcher

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