~james-page/charms/trusty/ceilometer/tox

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/contrib/peerstorage/__init__.py

  • Committer: james.page at ubuntu
  • Date: 2015-06-09 09:52:34 UTC
  • mfrom: (71.1.16 ceilometer)
  • Revision ID: james.page@ubuntu.com-20150609095234-4lh9wjhjjso5bszo
Add support for leader-election

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
import json
 
18
import six
 
19
 
 
20
from charmhelpers.core.hookenv import relation_id as current_relation_id
 
21
from charmhelpers.core.hookenv import (
 
22
    is_relation_made,
 
23
    relation_ids,
 
24
    relation_get as _relation_get,
 
25
    local_unit,
 
26
    relation_set as _relation_set,
 
27
    leader_get as _leader_get,
 
28
    leader_set,
 
29
    is_leader,
 
30
)
 
31
 
 
32
 
 
33
"""
 
34
This helper provides functions to support use of a peer relation
 
35
for basic key/value storage, with the added benefit that all storage
 
36
can be replicated across peer units.
 
37
 
 
38
Requirement to use:
 
39
 
 
40
To use this, the "peer_echo()" method has to be called form the peer
 
41
relation's relation-changed hook:
 
42
 
 
43
@hooks.hook("cluster-relation-changed") # Adapt the to your peer relation name
 
44
def cluster_relation_changed():
 
45
    peer_echo()
 
46
 
 
47
Once this is done, you can use peer storage from anywhere:
 
48
 
 
49
@hooks.hook("some-hook")
 
50
def some_hook():
 
51
    # You can store and retrieve key/values this way:
 
52
    if is_relation_made("cluster"):  # from charmhelpers.core.hookenv
 
53
        # There are peers available so we can work with peer storage
 
54
        peer_store("mykey", "myvalue")
 
55
        value = peer_retrieve("mykey")
 
56
        print value
 
57
    else:
 
58
        print "No peers joind the relation, cannot share key/values :("
 
59
"""
 
60
 
 
61
 
 
62
def leader_get(attribute=None):
 
63
    """Wrapper to ensure that settings are migrated from the peer relation.
 
64
 
 
65
    This is to support upgrading an environment that does not support
 
66
    Juju leadership election to one that does.
 
67
 
 
68
    If a setting is not extant in the leader-get but is on the relation-get
 
69
    peer rel, it is migrated and marked as such so that it is not re-migrated.
 
70
    """
 
71
    migration_key = '__leader_get_migrated_settings__'
 
72
    if not is_leader():
 
73
        return _leader_get(attribute=attribute)
 
74
 
 
75
    settings_migrated = False
 
76
    leader_settings = _leader_get(attribute=attribute)
 
77
    previously_migrated = _leader_get(attribute=migration_key)
 
78
 
 
79
    if previously_migrated:
 
80
        migrated = set(json.loads(previously_migrated))
 
81
    else:
 
82
        migrated = set([])
 
83
 
 
84
    try:
 
85
        if migration_key in leader_settings:
 
86
            del leader_settings[migration_key]
 
87
    except TypeError:
 
88
        pass
 
89
 
 
90
    if attribute:
 
91
        if attribute in migrated:
 
92
            return leader_settings
 
93
 
 
94
        # If attribute not present in leader db, check if this unit has set
 
95
        # the attribute in the peer relation
 
96
        if not leader_settings:
 
97
            peer_setting = relation_get(attribute=attribute, unit=local_unit())
 
98
            if peer_setting:
 
99
                leader_set(settings={attribute: peer_setting})
 
100
                leader_settings = peer_setting
 
101
 
 
102
        if leader_settings:
 
103
            settings_migrated = True
 
104
            migrated.add(attribute)
 
105
    else:
 
106
        r_settings = relation_get(unit=local_unit())
 
107
        if r_settings:
 
108
            for key in set(r_settings.keys()).difference(migrated):
 
109
                # Leader setting wins
 
110
                if not leader_settings.get(key):
 
111
                    leader_settings[key] = r_settings[key]
 
112
 
 
113
                settings_migrated = True
 
114
                migrated.add(key)
 
115
 
 
116
            if settings_migrated:
 
117
                leader_set(**leader_settings)
 
118
 
 
119
    if migrated and settings_migrated:
 
120
        migrated = json.dumps(list(migrated))
 
121
        leader_set(settings={migration_key: migrated})
 
122
 
 
123
    return leader_settings
 
124
 
 
125
 
 
126
def relation_set(relation_id=None, relation_settings=None, **kwargs):
 
127
    """Attempt to use leader-set if supported in the current version of Juju,
 
128
    otherwise falls back on relation-set.
 
129
 
 
130
    Note that we only attempt to use leader-set if the provided relation_id is
 
131
    a peer relation id or no relation id is provided (in which case we assume
 
132
    we are within the peer relation context).
 
133
    """
 
134
    try:
 
135
        if relation_id in relation_ids('cluster'):
 
136
            return leader_set(settings=relation_settings, **kwargs)
 
137
        else:
 
138
            raise NotImplementedError
 
139
    except NotImplementedError:
 
140
        return _relation_set(relation_id=relation_id,
 
141
                             relation_settings=relation_settings, **kwargs)
 
142
 
 
143
 
 
144
def relation_get(attribute=None, unit=None, rid=None):
 
145
    """Attempt to use leader-get if supported in the current version of Juju,
 
146
    otherwise falls back on relation-get.
 
147
 
 
148
    Note that we only attempt to use leader-get if the provided rid is a peer
 
149
    relation id or no relation id is provided (in which case we assume we are
 
150
    within the peer relation context).
 
151
    """
 
152
    try:
 
153
        if rid in relation_ids('cluster'):
 
154
            return leader_get(attribute)
 
155
        else:
 
156
            raise NotImplementedError
 
157
    except NotImplementedError:
 
158
        return _relation_get(attribute=attribute, rid=rid, unit=unit)
 
159
 
 
160
 
 
161
def peer_retrieve(key, relation_name='cluster'):
 
162
    """Retrieve a named key from peer relation `relation_name`."""
 
163
    cluster_rels = relation_ids(relation_name)
 
164
    if len(cluster_rels) > 0:
 
165
        cluster_rid = cluster_rels[0]
 
166
        return relation_get(attribute=key, rid=cluster_rid,
 
167
                            unit=local_unit())
 
168
    else:
 
169
        raise ValueError('Unable to detect'
 
170
                         'peer relation {}'.format(relation_name))
 
171
 
 
172
 
 
173
def peer_retrieve_by_prefix(prefix, relation_name='cluster', delimiter='_',
 
174
                            inc_list=None, exc_list=None):
 
175
    """ Retrieve k/v pairs given a prefix and filter using {inc,exc}_list """
 
176
    inc_list = inc_list if inc_list else []
 
177
    exc_list = exc_list if exc_list else []
 
178
    peerdb_settings = peer_retrieve('-', relation_name=relation_name)
 
179
    matched = {}
 
180
    if peerdb_settings is None:
 
181
        return matched
 
182
    for k, v in peerdb_settings.items():
 
183
        full_prefix = prefix + delimiter
 
184
        if k.startswith(full_prefix):
 
185
            new_key = k.replace(full_prefix, '')
 
186
            if new_key in exc_list:
 
187
                continue
 
188
            if new_key in inc_list or len(inc_list) == 0:
 
189
                matched[new_key] = v
 
190
    return matched
 
191
 
 
192
 
 
193
def peer_store(key, value, relation_name='cluster'):
 
194
    """Store the key/value pair on the named peer relation `relation_name`."""
 
195
    cluster_rels = relation_ids(relation_name)
 
196
    if len(cluster_rels) > 0:
 
197
        cluster_rid = cluster_rels[0]
 
198
        relation_set(relation_id=cluster_rid,
 
199
                     relation_settings={key: value})
 
200
    else:
 
201
        raise ValueError('Unable to detect '
 
202
                         'peer relation {}'.format(relation_name))
 
203
 
 
204
 
 
205
def peer_echo(includes=None, force=False):
 
206
    """Echo filtered attributes back onto the same relation for storage.
 
207
 
 
208
    This is a requirement to use the peerstorage module - it needs to be called
 
209
    from the peer relation's changed hook.
 
210
 
 
211
    If Juju leader support exists this will be a noop unless force is True.
 
212
    """
 
213
    try:
 
214
        is_leader()
 
215
    except NotImplementedError:
 
216
        pass
 
217
    else:
 
218
        if not force:
 
219
            return  # NOOP if leader-election is supported
 
220
 
 
221
    # Use original non-leader calls
 
222
    relation_get = _relation_get
 
223
    relation_set = _relation_set
 
224
 
 
225
    rdata = relation_get()
 
226
    echo_data = {}
 
227
    if includes is None:
 
228
        echo_data = rdata.copy()
 
229
        for ex in ['private-address', 'public-address']:
 
230
            if ex in echo_data:
 
231
                echo_data.pop(ex)
 
232
    else:
 
233
        for attribute, value in six.iteritems(rdata):
 
234
            for include in includes:
 
235
                if include in attribute:
 
236
                    echo_data[attribute] = value
 
237
    if len(echo_data) > 0:
 
238
        relation_set(relation_settings=echo_data)
 
239
 
 
240
 
 
241
def peer_store_and_set(relation_id=None, peer_relation_name='cluster',
 
242
                       peer_store_fatal=False, relation_settings=None,
 
243
                       delimiter='_', **kwargs):
 
244
    """Store passed-in arguments both in argument relation and in peer storage.
 
245
 
 
246
    It functions like doing relation_set() and peer_store() at the same time,
 
247
    with the same data.
 
248
 
 
249
    @param relation_id: the id of the relation to store the data on. Defaults
 
250
                        to the current relation.
 
251
    @param peer_store_fatal: Set to True, the function will raise an exception
 
252
                             should the peer sotrage not be avialable."""
 
253
 
 
254
    relation_settings = relation_settings if relation_settings else {}
 
255
    relation_set(relation_id=relation_id,
 
256
                 relation_settings=relation_settings,
 
257
                 **kwargs)
 
258
    if is_relation_made(peer_relation_name):
 
259
        for key, value in six.iteritems(dict(list(kwargs.items()) +
 
260
                                             list(relation_settings.items()))):
 
261
            key_prefix = relation_id or current_relation_id()
 
262
            peer_store(key_prefix + delimiter + key,
 
263
                       value,
 
264
                       relation_name=peer_relation_name)
 
265
    else:
 
266
        if peer_store_fatal:
 
267
            raise ValueError('Unable to detect '
 
268
                             'peer relation {}'.format(peer_relation_name))