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):
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())
99
leader_set(settings={attribute: peer_setting})
100
leader_settings = peer_setting
103
settings_migrated = True
104
migrated.add(attribute)
106
r_settings = relation_get(unit=local_unit())
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]
113
settings_migrated = True
116
if settings_migrated:
117
leader_set(**leader_settings)
119
if migrated and settings_migrated:
120
migrated = json.dumps(list(migrated))
121
leader_set(settings={migration_key: migrated})
123
return leader_settings
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.
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).
135
if relation_id in relation_ids('cluster'):
136
return leader_set(settings=relation_settings, **kwargs)
138
raise NotImplementedError
139
except NotImplementedError:
140
return _relation_set(relation_id=relation_id,
141
relation_settings=relation_settings, **kwargs)
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.
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).
153
if rid in relation_ids('cluster'):
154
return leader_get(attribute)
156
raise NotImplementedError
157
except NotImplementedError:
158
return _relation_get(attribute=attribute, rid=rid, unit=unit)
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,
169
raise ValueError('Unable to detect'
170
'peer relation {}'.format(relation_name))
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)
180
if peerdb_settings is None:
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:
188
if new_key in inc_list or len(inc_list) == 0:
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})
201
raise ValueError('Unable to detect '
202
'peer relation {}'.format(relation_name))
205
def peer_echo(includes=None, force=False):
206
"""Echo filtered attributes back onto the same relation for storage.
208
This is a requirement to use the peerstorage module - it needs to be called
209
from the peer relation's changed hook.
211
If Juju leader support exists this will be a noop unless force is True.
215
except NotImplementedError:
219
return # NOOP if leader-election is supported
221
# Use original non-leader calls
222
relation_get = _relation_get
223
relation_set = _relation_set
225
rdata = relation_get()
228
echo_data = rdata.copy()
229
for ex in ['private-address', 'public-address']:
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)
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.
246
It functions like doing relation_set() and peer_store() at the same time,
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."""
254
relation_settings = relation_settings if relation_settings else {}
255
relation_set(relation_id=relation_id,
256
relation_settings=relation_settings,
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,
264
relation_name=peer_relation_name)
267
raise ValueError('Unable to detect '
268
'peer relation {}'.format(peer_relation_name))