1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
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.
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.
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/>.
20
from charmhelpers.core.hookenv import relation_id as current_relation_id
21
from charmhelpers.core.hookenv import (
24
relation_get as _relation_get,
26
relation_set as _relation_set,
27
leader_get as _leader_get,
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.
40
To use this, the "peer_echo()" method has to be called form the peer
41
relation's relation-changed hook:
43
@hooks.hook("cluster-relation-changed") # Adapt the to your peer relation name
44
def cluster_relation_changed():
47
Once this is done, you can use peer storage from anywhere:
49
@hooks.hook("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")
58
print "No peers joind the relation, cannot share key/values :("
62
def leader_get(attribute=None, rid=None):
63
"""Wrapper to ensure that settings are migrated from the peer relation.
65
This is to support upgrading an environment that does not support
66
Juju leadership election to one that does.
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.
71
migration_key = '__leader_get_migrated_settings__'
73
return _leader_get(attribute=attribute)
75
settings_migrated = False
76
leader_settings = _leader_get(attribute=attribute)
77
previously_migrated = _leader_get(attribute=migration_key)
79
if previously_migrated:
80
migrated = set(json.loads(previously_migrated))
85
if migration_key in leader_settings:
86
del leader_settings[migration_key]
91
if attribute in migrated:
92
return leader_settings
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(),
100
leader_set(settings={attribute: peer_setting})
101
leader_settings = peer_setting
104
settings_migrated = True
105
migrated.add(attribute)
107
r_settings = _relation_get(unit=local_unit(), rid=rid)
109
for key in set(r_settings.keys()).difference(migrated):
110
# Leader setting wins
111
if not leader_settings.get(key):
112
leader_settings[key] = r_settings[key]
114
settings_migrated = True
117
if settings_migrated:
118
leader_set(**leader_settings)
120
if migrated and settings_migrated:
121
migrated = json.dumps(list(migrated))
122
leader_set(settings={migration_key: migrated})
124
return leader_settings
127
def relation_set(relation_id=None, relation_settings=None, **kwargs):
128
"""Attempt to use leader-set if supported in the current version of Juju,
129
otherwise falls back on relation-set.
131
Note that we only attempt to use leader-set if the provided relation_id is
132
a peer relation id or no relation id is provided (in which case we assume
133
we are within the peer relation context).
136
if relation_id in relation_ids('cluster'):
137
return leader_set(settings=relation_settings, **kwargs)
139
raise NotImplementedError
140
except NotImplementedError:
141
return _relation_set(relation_id=relation_id,
142
relation_settings=relation_settings, **kwargs)
145
def relation_get(attribute=None, unit=None, rid=None):
146
"""Attempt to use leader-get if supported in the current version of Juju,
147
otherwise falls back on relation-get.
149
Note that we only attempt to use leader-get if the provided rid is a peer
150
relation id or no relation id is provided (in which case we assume we are
151
within the peer relation context).
154
if rid in relation_ids('cluster'):
155
return leader_get(attribute, rid)
157
raise NotImplementedError
158
except NotImplementedError:
159
return _relation_get(attribute=attribute, rid=rid, unit=unit)
162
def peer_retrieve(key, relation_name='cluster'):
163
"""Retrieve a named key from peer relation `relation_name`."""
164
cluster_rels = relation_ids(relation_name)
165
if len(cluster_rels) > 0:
166
cluster_rid = cluster_rels[0]
167
return relation_get(attribute=key, rid=cluster_rid,
170
raise ValueError('Unable to detect'
171
'peer relation {}'.format(relation_name))
174
def peer_retrieve_by_prefix(prefix, relation_name='cluster', delimiter='_',
175
inc_list=None, exc_list=None):
176
""" Retrieve k/v pairs given a prefix and filter using {inc,exc}_list """
177
inc_list = inc_list if inc_list else []
178
exc_list = exc_list if exc_list else []
179
peerdb_settings = peer_retrieve('-', relation_name=relation_name)
181
if peerdb_settings is None:
183
for k, v in peerdb_settings.items():
184
full_prefix = prefix + delimiter
185
if k.startswith(full_prefix):
186
new_key = k.replace(full_prefix, '')
187
if new_key in exc_list:
189
if new_key in inc_list or len(inc_list) == 0:
194
def peer_store(key, value, relation_name='cluster'):
195
"""Store the key/value pair on the named peer relation `relation_name`."""
196
cluster_rels = relation_ids(relation_name)
197
if len(cluster_rels) > 0:
198
cluster_rid = cluster_rels[0]
199
relation_set(relation_id=cluster_rid,
200
relation_settings={key: value})
202
raise ValueError('Unable to detect '
203
'peer relation {}'.format(relation_name))
206
def peer_echo(includes=None, force=False):
207
"""Echo filtered attributes back onto the same relation for storage.
209
This is a requirement to use the peerstorage module - it needs to be called
210
from the peer relation's changed hook.
212
If Juju leader support exists this will be a noop unless force is True.
216
except NotImplementedError:
220
return # NOOP if leader-election is supported
222
# Use original non-leader calls
223
relation_get = _relation_get
224
relation_set = _relation_set
226
rdata = relation_get()
229
echo_data = rdata.copy()
230
for ex in ['private-address', 'public-address']:
234
for attribute, value in six.iteritems(rdata):
235
for include in includes:
236
if include in attribute:
237
echo_data[attribute] = value
238
if len(echo_data) > 0:
239
relation_set(relation_settings=echo_data)
242
def peer_store_and_set(relation_id=None, peer_relation_name='cluster',
243
peer_store_fatal=False, relation_settings=None,
244
delimiter='_', **kwargs):
245
"""Store passed-in arguments both in argument relation and in peer storage.
247
It functions like doing relation_set() and peer_store() at the same time,
250
@param relation_id: the id of the relation to store the data on. Defaults
251
to the current relation.
252
@param peer_store_fatal: Set to True, the function will raise an exception
253
should the peer sotrage not be avialable."""
255
relation_settings = relation_settings if relation_settings else {}
256
relation_set(relation_id=relation_id,
257
relation_settings=relation_settings,
259
if is_relation_made(peer_relation_name):
260
for key, value in six.iteritems(dict(list(kwargs.items()) +
261
list(relation_settings.items()))):
262
key_prefix = relation_id or current_relation_id()
263
peer_store(key_prefix + delimiter + key,
265
relation_name=peer_relation_name)
268
raise ValueError('Unable to detect '
269
'peer relation {}'.format(peer_relation_name))