1
from collections import namedtuple
1
3
from twisted.internet.defer import inlineCallbacks, returnValue, succeed, fail
3
5
from juju.state.base import StateBase
4
6
from juju.state.errors import (
5
UnitRelationStateNotFound, StateNotFound, RelationBrokenContextError)
7
UnitRelationStateNotFound, StateNotFound, RelationBrokenContextError,
8
RelationStateNotFound, InvalidRelationIdentity, StateChanged)
9
from juju.state.relation import (
10
RelationStateManager, ServiceRelationState, UnitRelationState)
6
11
from juju.state.service import ServiceStateManager, parse_service_name
7
12
from juju.state.utils import YAMLState
10
class RelationChange(object):
11
"""Encapsulation of relation change variables passed to hook.
18
"relation_ident change_type unit_name")):
14
def __init__(self, relation_name, change_type, unit_name):
15
self._relation_name = relation_name
16
self._change_type = change_type
17
self._unit_name = unit_name
20
23
def relation_name(self):
21
return self._relation_name
24
def change_type(self):
25
return self._change_type
29
return self._unit_name
24
return self.relation_ident.split(":")[0]
32
27
class HookContext(StateBase):
33
28
"""Context for hooks which don't depend on relation state. """
35
def __init__(self, client, unit_name):
30
def __init__(self, client, unit_name, topology=None):
36
31
super(HookContext, self).__init__(client)
38
33
self._unit_name = unit_name
102
97
returnValue(self._config_options)
100
def get_relations(self):
101
"""Get the relations associated to the local service."""
103
if self._topology is None:
104
self._topology = yield self._read_topology()
105
service = yield self.get_local_service()
106
internal_service_id = service.internal_id
107
for info in self._topology.get_relations_for_service(
108
internal_service_id):
109
service_info = info["service"]
111
ServiceRelationState(
117
returnValue(relations)
120
def get_relation_idents(self, relation_name):
121
"""Return the relation idents for `relation_name`"""
122
relations = yield self.get_relations()
123
returnValue(sorted([r.relation_ident for r in relations if
125
r.relation_name == relation_name]))
128
def get_relation_id_and_scope(self, relation_ident):
129
"""Return the (internal) relation id for `relation_ident`."""
130
parts = relation_ident.split(":")
131
if len(parts) != 2 or not parts[1].isdigit():
132
raise InvalidRelationIdentity(relation_ident)
133
relation_name, normalized_id = parts
134
relation_id = "%s-%s" % ("relation", normalized_id.zfill(10))
135
# Double check the internal relation id by looking it up
136
relations = yield self.get_relations()
138
if (r.relation_name == relation_name and \
139
r.internal_relation_id == relation_id):
140
returnValue((relation_id, r.relation_scope))
142
raise RelationStateNotFound()
145
def get_relation_hook_context(self, relation_ident):
146
"""Return a child hook context for `relation_ident`"""
147
service = yield self.get_local_service()
148
unit = yield self.get_local_unit_state()
149
relation_id, relation_scope = yield self.get_relation_id_and_scope(
151
unit_relation = UnitRelationState(
152
self._client, service.internal_id, unit.internal_id,
153
relation_id, relation_scope)
154
# Ensure that the topology is shared so there's a consistent view
155
# between the parent and any children.
156
returnValue(RelationHookContext(
157
self._client, unit_relation,
159
unit_name=self._unit_name,
160
topology=self._topology))
106
164
"""Flush pending state."""
107
165
config = yield self.get_config()
115
173
hook. Also buffers all writes till the flush method is invoked.
118
def __init__(self, client, unit_relation, change, members=None,
176
def __init__(self, client, unit_relation, relation_ident,
177
members=None, unit_name=None, topology=None):
121
179
@param unit_relation: The unit relation state associated to the hook.
122
180
@param change: A C{RelationChange} instance.
124
182
# Zookeeper client.
125
super(RelationHookContext, self).__init__(client, unit_name=unit_name)
183
super(RelationHookContext, self).__init__(
184
client, unit_name=unit_name, topology=topology)
126
185
self._unit_relation = unit_relation
127
# The current change we're being executed against.
128
self._change = change
130
187
# A cache of related units in the relation.
131
188
self._members = members
133
def _settings_path(self, unit_id):
134
return "/relations/%s/settings/%s" % (
135
self._unit_relation.internal_relation_id, unit_id)
190
# The relation ident of this context
191
self._relation_ident = relation_ident
193
# Whether settings have been modified (set/delete) for this context
194
self._needs_flushing = False
196
# A cache of the relation scope path
197
self._settings_scope_path = None
199
def get_settings_path(self, unit_id):
200
if self._unit_relation.relation_scope == "global":
201
return "/relations/%s/settings/%s" % (
202
self._unit_relation.internal_relation_id, unit_id)
204
if self._settings_scope_path:
205
return "%s/%s" % (self._settings_scope_path, unit_id)
207
def process(unit_settings_path):
208
self._settings_scope_path = "/".join(
209
unit_settings_path.split("/")[:-1])
210
return "%s/%s" % (self._settings_scope_path, unit_id)
212
d = self._unit_relation.get_settings_path()
213
return d.addCallback(process)
216
def relation_ident(self):
217
"""Returns the relation ident corresponding to this context."""
218
return self._relation_ident
221
def relation_name(self):
222
"""Returns the relation name corresponding to this context."""
223
return self._relation_ident.split(":")[0]
138
226
def get_members(self):
140
228
if self._members is not None:
141
229
returnValue(self._members)
143
container = yield self._unit_relation.get_related_unit_container()
232
container = yield self._unit_relation.get_related_unit_container()
234
# The unit relation has vanished, so there are no members.
144
236
unit_ids = yield self._client.get_children(container)
145
237
if self._unit_relation.internal_unit_id in unit_ids:
146
238
unit_ids.remove(self._unit_relation.internal_unit_id)
164
256
returnValue(self._node_cache[unit_name])
166
258
unit_id = yield self._resolve_name(unit_name)
167
path = self._settings_path(unit_id)
259
path = yield self.get_settings_path(unit_id)
169
261
# verify the unit relation path exists
170
262
relation_data = YAMLState(self._client, path)
209
301
if not isinstance(data, dict):
210
302
raise TypeError("A dictionary is required.")
304
self._needs_flushing = True
212
305
state = yield self._setup_relation_state()
213
306
state.update(data)
216
309
def set_value(self, key, value):
217
310
"""Set a relation value for a unit."""
311
self._needs_flushing = True
218
312
state = yield self._setup_relation_state()
219
313
state[key] = value
222
316
def delete_value(self, key):
223
317
"""Delete a relation value for a unit."""
318
self._needs_flushing = True
224
319
state = yield self._setup_relation_state()
246
341
could also be done with config settings, but given their
247
342
usage model, doesn't seem to be worth logging).
249
rel_state = yield self._setup_relation_state()
250
relation_setting_changes = yield rel_state.write()
344
relation_setting_changes = []
345
if self._needs_flushing:
346
rel_state = yield self._setup_relation_state()
347
relation_setting_changes = yield rel_state.write()
251
348
yield super(RelationHookContext, self).flush()
252
349
returnValue(relation_setting_changes)
266
363
super(DepartedRelationHookContext, self).__init__(client, unit_name)
267
364
self._relation_name = relation_name
268
365
self._relation_id = relation_id
269
self._settings_path = "/relations/%s/settings/%s" % (
270
relation_id, unit_id)
366
self._settings_path = None
272
367
# Cache of relation settings for the local unit
273
368
self._relation_cache = None
275
370
def get_members(self):
276
371
return succeed([])
374
def relation_ident(self):
375
"""Returns the external relation id corresponding to this context."""
376
return ServiceRelationState.get_relation_ident(
377
self._relation_name, self._relation_id)
380
def get_settings_path(self):
381
if self._settings_path:
382
returnValue(self._settings_path)
384
unit_id = yield self._resolve_name(self._unit_name)
385
topology = yield self._read_topology()
386
container = topology.get_service_unit_container(unit_id)
389
container_info = "%s/" % container[-1]
391
self._settings_path = "/relations/%s/%ssettings/%s" % (
392
self._relation_id, container_info, unit_id)
393
returnValue(self._settings_path)
279
396
def get(self, unit_name):
280
397
# Only this unit's settings should be accessible.
282
399
raise RelationBrokenContextError(
283
400
"Cannot access other units in broken relation")
402
settings_path = yield self.get_settings_path()
285
404
if self._relation_cache is None:
286
relation_data = YAMLState(self._client, self._settings_path)
405
relation_data = YAMLState(self._client, settings_path)
288
407
yield relation_data.read(required=True)
289
408
self._relation_cache = dict(relation_data)