Merge lp:~hazmat/pyjuju/relation-state into lp:pyjuju
- relation-state
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 89 |
Proposed branch: | lp:~hazmat/pyjuju/relation-state |
Merge into: | lp:pyjuju |
Diff against target: |
1184 lines (+1092/-2) 8 files modified
ensemble/errors.py (+1/-1) ensemble/state/errors.py (+42/-0) ensemble/state/relation.py (+289/-0) ensemble/state/tests/common.py (+1/-0) ensemble/state/tests/test_errors.py (+25/-1) ensemble/state/tests/test_relation.py (+471/-0) ensemble/state/tests/test_topology.py (+168/-0) ensemble/state/topology.py (+95/-0) |
To merge this branch: | bzr merge lp:~hazmat/pyjuju/relation-state |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gustavo Niemeyer | Approve | ||
Review via email: mp+39328@code.launchpad.net |
Commit message
Description of the change
relation state management, lacking watch api at the moment which is for a separate branch, also i'm using service id instead of service role name atm, but that's a fairly minor change. I mostly want to get some eyes on this code to make sure its going in a good direction.
Gustavo Niemeyer (niemeyer) wrote : | # |
Kapil Thangavelu (hazmat) wrote : | # |
On Tue, 26 Oct 2010 11:32:44 -0400, Gustavo Niemeyer
<email address hidden> wrote:
> The overall feeling of the work so far is very good. Feels generally
> clean, and understandable.
Cool, thanks for the review, replies inline.
>
> Some specific points:
>
>
> [1]
>
> + def add_relation(self, relation_id):
> + """Add a relation with given id between the given services.
> (...)
> + def has_relation(self, relation_id):
> + """Return True if service_id was previously added to this
> topology.
>
> These docstrings aren't reflecting the implementation.
Yeah.. they where prior to some discussions we had, when we discussed
separating out add relation from associating a service to it. I'll update
them.
>
> [2]
>
> + def has_relation_
>
> relation_
agreed.
>
> [3]
>
> + def unassign_
> + """Disassociate service to relation.
>
> Similarly, shouldn't that be "_from_relation" and "from relation"?
>
sounds good as well.
> [4]
>
> All of the date for the relation is missing from the topology (relation
> name, relation type, relation role). Without at least some of these in
> the topology, we can't do the mapping of a user provided name to the
> relation id without iterating through the nodes.
>
The formula author aka 'user' name for a relation is specific to a
service, its not global to the relation. We can lookup this meta
information when we retrieve the services for a relation, we don't have to
iterate all relation nodes, just the ones associated to a service.
Additionally, we don't want to bloat the limited space we have in the
topology node for non coordination data, all of this additional metadata
is effectively static, formula derived metadata.
> [5]
>
> + Type is used to distinguish the type of the relation, ie.
> + 'database.
>
> We haven't discussed/specified the namespacing of the types. We have to
> talk about "http.cache.
>
agreed, i can remove the docstring namespaced type reference, as its not
germane to the implementation at this point.
> [6]
>
>
> + node_data = {"type": relation_type}
> + path = yield self._client.
> + yaml.safe_
>
> I'm wondering a bit about where to store the information for the
> relation. It feels a bit clumsy to have the relation type inside the
> relation node itself, as a first gut feeling. Imagine what it'd take to
> implement the matching of relation types to resolve dependencies. We'd
> basically have to look at the topology and then iterate over every node
> we have to discover which of them is responsible for which relation
> type/name/role.
>
We haven't really discussed dependency resolution using existing services.
I think we can construct a secondary index (potentially multi-node) of the
relation data to do resolutions, but unless we want the index to be
primary storage, i think there is still value to having this ...
Gustavo Niemeyer (niemeyer) wrote : | # |
> > [4]
> >
> > All of the date for the relation is missing from the topology (relation
> > name, relation type, relation role). Without at least some of these in
> > the topology, we can't do the mapping of a user provided name to the
> > relation id without iterating through the nodes.
>
> The formula author aka 'user' name for a relation is specific to a
> service, its not global to the relation.
Sure, (relation name, relation type, relation role) are all equally associated
to the relation endpoint from the service.
> We can lookup this meta
> information when we retrieve the services for a relation, we don't have to
> iterate all relation nodes, just the ones associated to a service.
> Additionally, we don't want to bloat the limited space we have in the
> topology node for non coordination data, all of this additional metadata
> is effectively static, formula derived metadata.
The additional complexity that this will introduce feels a bit like an early
optimization, even more considering that we'd be storing that data per service
rather than per unit, but ok, let's see how that turns out then.
> > [6]
>
> We haven't really discussed dependency resolution using existing services.
> I think we can construct a secondary index (potentially multi-node) of the
> relation data to do resolutions, but unless we want the index to be
> primary storage, i think there is still value to having this 'identity'
> information stored on the node.
Cool, same case as above.
> noted, fwiw i copied it from the service state impl ;-)
Hah. Self-note taken! ;-)
> > [9]
(...)
> We should agree on terminology. In both cases we're referring to a
> service's role within a relationship. As long as we define and use the
> term consistently the ambiguity can be minimized. I'm okay with either.
> relation name and relation role are fine, but we need to stress in
> definition that while its target/context is a relation, its subject is a
> service.
Agreed. It's the "service relation role", but I propose that when shortened we call it "relation role", rather than "service role", to avoid the misconception that the service has a single role for all relations.
> > [11]
(...)
> i used a shortcut so i could it to review, as the branch was getting
> larged, as noted in the merge request. i'll fix that up.
Thank you!
Kapil Thangavelu (hazmat) wrote : | # |
On Tue, 26 Oct 2010 17:57:45 -0400, Gustavo Niemeyer
<email address hidden> wrote:
>
>> > [4]
>> >
>> > All of the date for the relation is missing from the topology
>> (relation
>> > name, relation type, relation role). Without at least some of these
>> in
>> > the topology, we can't do the mapping of a user provided name to the
>> > relation id without iterating through the nodes.
>>
>> The formula author aka 'user' name for a relation is specific to a
>> service, its not global to the relation.
>
> Sure, (relation name, relation type, relation role) are all equally
> associated
> to the relation endpoint from the service.
>
>> We can lookup this meta
>> information when we retrieve the services for a relation, we don't have
>> to
>> iterate all relation nodes, just the ones associated to a service.
>> Additionally, we don't want to bloat the limited space we have in the
>> topology node for non coordination data, all of this additional metadata
>> is effectively static, formula derived metadata.
>
> The additional complexity that this will introduce feels a bit like an
> early
> optimization, even more considering that we'd be storing that data per
> service
> rather than per unit, but ok, let's see how that turns out then.
I'm going to have backtrack on this, as i'll need it to maintain the
current relation manager get relations for service api with a
relation-role container. I'll need to add the role to topology, so i might
as well add the relation type as well.
- 97. By Kapil Thangavelu
-
store relation info in topology
- 98. By Kapil Thangavelu
-
update relation model with topology stored rel info
- 99. By Kapil Thangavelu
-
doc string updates
- 100. By Kapil Thangavelu
-
separate out api for service retrieval from relation type retrieval
Kapil Thangavelu (hazmat) wrote : | # |
all review comments, addressed. re the docstring in #9, its not clear that its different then what we agreed on re relation-role usage.
- 101. By Kapil Thangavelu
-
address some review comments
Gustavo Niemeyer (niemeyer) wrote : | # |
Thanks for the changes, this is looking good. I'm raising some further points and issues for discussion below, but considering these the branch looks good for committing.
[9]
Ok, given that we went back and forth a few times on this, let's use the following terminology for clarity and convention alignment:
Long description:
- Service relation role
- Service relation name
- Service relation type
Short description:
- Relation role
- Relation name
- Relation type
Variable names:
- relation_role
- relation_name
- relation_type
[12]
+ internal_id = os.path.
Please use one or the other. No benefit in having both joined by a comment like this.
[13]
+ node_data = {"type": relation_type}
This node data doesn't seem to be used, and is redundant with the topology now.
[14]
+class RelationState(
+
+ def __init__(self, client, internal_
+ self._client = client
This should use the StateBase constructor for the client assignment. So far, this is the only thing done, but it may do additional tasks in the future and in such case this will break given that it's ignoring the base class.
[15]
+ node_data = yaml.safe_
As per PEP8, in these cases let's use spacing like dict(name=name, role=role).
[16]
+ service_id = service_
Would you mind to use the name "internal_
[17]
+ # previous persistent settings are not an error.
This raises some concerns. It's *adding* the unit to the relation, and even then any previous settings are blindly accepted. I'd guess that user hooks fired when adding the unit to the relation will not expect a previous dirty state. What would be the reasoning behind this behavior, and how do you imagine users will relate to it?
[18]
+ def __init__(self, client, service_id, unit_id, relation_id):
+ self._client = client
+ self._service_id = service_id
+ self._unit_id = unit_id
+ self._relation_id = relation_id
Would be nice to have these exported as properties and tested for equality in this round, just to ensure that the logic being put in place thus far is correct (parameter swapping is a common issue).
Kapil Thangavelu (hazmat) wrote : | # |
On Thu, 28 Oct 2010 09:49:41 -0400, Gustavo Niemeyer
<email address hidden> wrote:
> Review: Approve
> Thanks for the changes, this is looking good. I'm raising some further
> points and issues for discussion below, but considering these the branch
> looks good for committing.
>
> [9]
>
> Ok, given that we went back and forth a few times on this, let's use the
> following terminology for clarity and convention alignment:
>
<...>
Sounds good.
[12-16, 18] In progress
> [17]
>
> + # previous persistent settings are not an error.
>
> This raises some concerns. It's *adding* the unit to the relation, and
> even then any previous settings are blindly accepted. I'd guess that
> user hooks fired when adding the unit to the relation will not expect a
> previous dirty state. What would be the reasoning behind this behavior,
> and how do you imagine users will relate to it?
>
This behavior is to allow units that have had transient disconnection or
errors, the ability to resume their work with the relation without loss of
configuration state. Take a mysql/wordpress relation as an example. If
mysql creates a database for wordpress and stores the db info its relation
unit settings, and subsequently the wordpress unit becomes unavailable
temporarily, when it reconnects we don't want it to create a new database
for wordpress, we want it to resume its role with its previous settings.
The main point for formula authors, is that unit relation settings are
persistent. Indeed as we now only utilize a relation-changed hook, there
is little distinction for formula authors from new/old, they must always
update their settings based on the current state of the world. If the
relation is explicitly broken the participating units agents will be
responsible for removing/cleaning up their settings.
Thanks for the review.
Kapil
- 102. By Kapil Thangavelu
-
address some review comments re style/naming
- 103. By Kapil Thangavelu
-
use internal prefix on ids, relation prefix on role/name
- 104. By Kapil Thangavelu
-
unit rel state property tests, and get/set settings data
- 105. By Kapil Thangavelu
-
align the assign service to relation signature between topology and relation state
Preview Diff
1 | === modified file 'ensemble/errors.py' |
2 | --- ensemble/errors.py 2010-09-21 19:03:46 +0000 |
3 | +++ ensemble/errors.py 2010-10-29 18:34:42 +0000 |
4 | @@ -6,7 +6,7 @@ |
5 | |
6 | class EnsembleError(Exception): |
7 | """All errors in Ensemble are subclasses of this. |
8 | - |
9 | + |
10 | This error should not be raised by itself, though, since it means |
11 | pretty much nothing. It's useful mostly as something to catch instead. |
12 | """ |
13 | |
14 | === modified file 'ensemble/state/errors.py' |
15 | --- ensemble/state/errors.py 2010-09-16 20:13:37 +0000 |
16 | +++ ensemble/state/errors.py 2010-10-29 18:34:42 +0000 |
17 | @@ -94,6 +94,48 @@ |
18 | self.unit_name |
19 | |
20 | |
21 | +class ServiceRelationAlreadyAssigned(StateError): |
22 | + |
23 | + def __init__(self, service_name, relation_name, relation_role): |
24 | + self.service_name = service_name |
25 | + self.relation_name = relation_name |
26 | + self.relation_role = relation_role |
27 | + |
28 | + def __str__(self): |
29 | + return ( |
30 | + "Service %r is already in relation with different name %r " |
31 | + "or role %r.") % ( |
32 | + self.service_name, self.relation_name, self.relation_role) |
33 | + |
34 | + |
35 | +class UnitRelationStateAlreadyAssigned(StateError): |
36 | + """The unit already exists in the relation.""" |
37 | + |
38 | + def __init__(self, relation_id, relation_name, unit_name): |
39 | + self.relation_id = relation_id |
40 | + self.relation_name = relation_name |
41 | + self.unit_name = unit_name |
42 | + |
43 | + def __str__(self): |
44 | + return "The relation %r already contains a unit for %r" % ( |
45 | + self.relation_name, |
46 | + self.unit_name) |
47 | + |
48 | + |
49 | +class UnitRelationStateNotFound(StateError): |
50 | + """The unit does not exist in the relation.""" |
51 | + |
52 | + def __init__(self, relation_id, relation_name, unit_name): |
53 | + self.relation_id = relation_id |
54 | + self.relation_name = relation_name |
55 | + self.unit_name = unit_name |
56 | + |
57 | + def __str__(self): |
58 | + return "The relation %r has no unit state for %r" % ( |
59 | + self.relation_name, |
60 | + self.unit_name) |
61 | + |
62 | + |
63 | class EnvironmentStateNotFound(StateError): |
64 | """Environment state was not found.""" |
65 | |
66 | |
67 | === added file 'ensemble/state/relation.py' |
68 | --- ensemble/state/relation.py 1970-01-01 00:00:00 +0000 |
69 | +++ ensemble/state/relation.py 2010-10-29 18:34:42 +0000 |
70 | @@ -0,0 +1,289 @@ |
71 | +import yaml |
72 | +import os |
73 | +import zookeeper |
74 | + |
75 | +from twisted.internet.defer import inlineCallbacks, returnValue |
76 | +from txzookeeper.utils import retry_change |
77 | + |
78 | +from ensemble.state.base import StateBase |
79 | +from ensemble.state.errors import ( |
80 | + StateChanged, ServiceRelationAlreadyAssigned, UnitRelationStateNotFound, |
81 | + UnitRelationStateAlreadyAssigned) |
82 | + |
83 | + |
84 | +class RelationStateManager(StateBase): |
85 | + """Manages the state of relations in an environment.""" |
86 | + |
87 | + @inlineCallbacks |
88 | + def add_relation_state(self, relation_type): |
89 | + """Add a new relation state. The relation is of the given type. |
90 | + |
91 | + Type is used to distinguish the type of the relation, ie. |
92 | + 'postgres', or 'varnish'. |
93 | + """ |
94 | + path = yield self._client.create( |
95 | + "/relations/relation-", flags=zookeeper.SEQUENCE) |
96 | + |
97 | + internal_id = os.path.basename(path) |
98 | + |
99 | + # create the settings container, for individual units settings. |
100 | + yield self._client.create(path + "/settings") |
101 | + |
102 | + # Not clear if we really need to update the topology till the relation |
103 | + # is in use. Topology updates are broadcasting to most connected |
104 | + # clients. Perhaps a separate batch api would be useful to minimize |
105 | + # broadcast events on the topology. |
106 | + def add_relation(topology): |
107 | + topology.add_relation(internal_id, relation_type) |
108 | + yield self._retry_topology_change(add_relation) |
109 | + |
110 | + returnValue(RelationState(self._client, internal_id)) |
111 | + |
112 | + @inlineCallbacks |
113 | + def remove_relation_state(self, relation_state): |
114 | + """Remove the relation of the given id. |
115 | + |
116 | + The relation is removed from the topology, however its container |
117 | + node is not removed, as associated units will still be processing |
118 | + its removal. |
119 | + """ |
120 | + |
121 | + def remove_relation(topology): |
122 | + if not topology.has_relation(relation_state.internal_id): |
123 | + raise StateChanged() |
124 | + |
125 | + topology.remove_relation(relation_state.internal_id) |
126 | + |
127 | + yield self._retry_topology_change(remove_relation) |
128 | + |
129 | + @inlineCallbacks |
130 | + def get_relations_for_service(self, service_state): |
131 | + """Get the relations associated to the service. |
132 | + """ |
133 | + relations = [] |
134 | + internal_service_id = service_state.internal_id |
135 | + topology = yield self._read_topology() |
136 | + for info in topology.get_relations_for_service(internal_service_id): |
137 | + relation_id, relation_type, service_info = info |
138 | + relations.append( |
139 | + ServiceRelationState( |
140 | + self._client, |
141 | + internal_service_id, |
142 | + relation_id, |
143 | + **service_info)) |
144 | + returnValue(relations) |
145 | + |
146 | + |
147 | +class RelationState(StateBase): |
148 | + |
149 | + def __init__(self, client, internal_relation_id): |
150 | + super(RelationState, self).__init__(client) |
151 | + self._internal_id = internal_relation_id |
152 | + |
153 | + @property |
154 | + def internal_id(self): |
155 | + return self._internal_id |
156 | + |
157 | + @inlineCallbacks |
158 | + def unassign_service(self, service_state): |
159 | + """Remove a service from the relation. |
160 | + |
161 | + Any removed service needs to have its zookeeper state garbage collected |
162 | + external to this api. The individual units should remove their |
163 | + contained nodes, upon notification but even then the service container |
164 | + node within the relation remains to be garbage collected. |
165 | + """ |
166 | + |
167 | + def remove_service(topology): |
168 | + # Verify our relation and service still exist. |
169 | + if not topology.has_relation(self._internal_id) or \ |
170 | + not topology.has_service(service_state.internal_id): |
171 | + raise StateChanged() |
172 | + |
173 | + # Ignore concurrent requests that achieve the same state. |
174 | + if not topology.relation_has_service( |
175 | + self._internal_id, service_state.internal_id): |
176 | + return |
177 | + |
178 | + topology.unassign_service_from_relation( |
179 | + self._internal_id, service_state.internal_id) |
180 | + |
181 | + yield self._retry_topology_change(remove_service) |
182 | + |
183 | + @inlineCallbacks |
184 | + def assign_service(self, service_state, relation_name, relation_role): |
185 | + """Add a service to the relation. |
186 | + |
187 | + @param service_state - The service to be added to the relation. |
188 | + @param name - The service's name for this relation. |
189 | + @param role - The service's role within this relation. |
190 | + """ |
191 | + # Add service container in relation. |
192 | + node_data = yaml.safe_dump( |
193 | + {"name": relation_name, "role": relation_role}) |
194 | + path = "/relations/%s/%s" % (self.internal_id, relation_role) |
195 | + |
196 | + try: |
197 | + yield self._client.create(path, node_data) |
198 | + except zookeeper.NodeExistsException: |
199 | + # Because we don't garbage collect, we need to ascertain |
200 | + # if the service node is actually in use. |
201 | + topology = yield self._read_topology() |
202 | + |
203 | + # If it is assigned, its an error. |
204 | + # There's a small chance for a race condition between the check |
205 | + # and set. |
206 | + if topology.relation_has_service( |
207 | + self._internal_id, service_state.internal_id): |
208 | + raise ServiceRelationAlreadyAssigned( |
209 | + service_state.service_name, relation_name, relation_role) |
210 | + |
211 | + # If its not, then update the node, and continue. |
212 | + yield retry_change( |
213 | + self._client, path, lambda content, stat: node_data) |
214 | + |
215 | + # Associate service to relation in the topology. |
216 | + def add_service(topology): |
217 | + # Verify our service and relation exists |
218 | + if not topology.has_relation(self._internal_id) or \ |
219 | + not topology.has_service(service_state.internal_id): |
220 | + raise StateChanged() |
221 | + |
222 | + # Add it the topology if its not there. |
223 | + if not topology.relation_has_service( |
224 | + self._internal_id, service_state.internal_id): |
225 | + topology.assign_service_to_relation( |
226 | + self.internal_id, |
227 | + service_state.internal_id, |
228 | + relation_name, |
229 | + relation_role) |
230 | + |
231 | + |
232 | + yield self._retry_topology_change(add_service) |
233 | + |
234 | + returnValue(ServiceRelationState(self._client, |
235 | + service_state.internal_id, |
236 | + self.internal_id, |
237 | + relation_role, |
238 | + relation_name)) |
239 | + |
240 | + |
241 | +class ServiceRelationState(object): |
242 | + """A state representative of a relation between one or more services.""" |
243 | + |
244 | + def __init__(self, client, service_id, relation_id, role, name): |
245 | + self._client = client |
246 | + self._service_id = service_id |
247 | + self._relation_id = relation_id |
248 | + self._role = role |
249 | + self._name = name |
250 | + |
251 | + @property |
252 | + def internal_relation_id(self): |
253 | + return self._relation_id |
254 | + |
255 | + @property |
256 | + def internal_service_id(self): |
257 | + return self._service_id |
258 | + |
259 | + @property |
260 | + def relation_name(self): |
261 | + """The service's name for the relation.""" |
262 | + return self._name |
263 | + |
264 | + @property |
265 | + def relation_role(self): |
266 | + """The service's role within the relation.""" |
267 | + return self._role |
268 | + |
269 | + @inlineCallbacks |
270 | + def add_unit_state(self, unit_state): |
271 | + """Add a unit to the service relation. |
272 | + |
273 | + returns a unit relation state. |
274 | + """ |
275 | + settings_path = "/relations/%s/settings/%s" % ( |
276 | + self._relation_id, unit_state.internal_id) |
277 | + |
278 | + # We create settings node first, so that presence node events |
279 | + # have a chance to inspect state. |
280 | + try: |
281 | + yield self._client.create(settings_path) |
282 | + except zookeeper.NodeExistsException: |
283 | + # previous persistent settings are not an error. |
284 | + pass |
285 | + |
286 | + alive_path = "/relations/%s/%s/%s" % ( |
287 | + self._relation_id, self._role, unit_state.internal_id) |
288 | + |
289 | + try: |
290 | + yield self._client.create(alive_path, flags=zookeeper.EPHEMERAL) |
291 | + except zookeeper.NodeExistsException: |
292 | + raise UnitRelationStateAlreadyAssigned( |
293 | + self._relation_id, self._name, unit_state.unit_name) |
294 | + |
295 | + returnValue( |
296 | + UnitRelationState( |
297 | + self._client, |
298 | + self._service_id, |
299 | + unit_state.internal_id, |
300 | + self._relation_id)) |
301 | + |
302 | + @inlineCallbacks |
303 | + def get_unit_state(self, unit_state): |
304 | + """Given a service unit state, return its unit relation state.""" |
305 | + alive_path = "/relations/%s/%s/%s" % ( |
306 | + self._relation_id, self._role, unit_state.internal_id) |
307 | + stat = yield self._client.exists(alive_path) |
308 | + |
309 | + if not stat: |
310 | + raise UnitRelationStateNotFound( |
311 | + self._relation_id, self._name, unit_state.unit_name) |
312 | + |
313 | + returnValue( |
314 | + UnitRelationState( |
315 | + self._client, |
316 | + self._service_id, |
317 | + unit_state.internal_id, |
318 | + self._relation_id)) |
319 | + |
320 | + |
321 | +class UnitRelationState(object): |
322 | + """A service unit's relation state. |
323 | + """ |
324 | + |
325 | + def __init__(self, client, service_id, unit_id, relation_id): |
326 | + self._client = client |
327 | + self._service_id = service_id |
328 | + self._unit_id = unit_id |
329 | + self._relation_id = relation_id |
330 | + |
331 | + @property |
332 | + def internal_service_id(self): |
333 | + return self._service_id |
334 | + |
335 | + @property |
336 | + def internal_unit_id(self): |
337 | + return self._unit_id |
338 | + |
339 | + @property |
340 | + def internal_relation_id(self): |
341 | + return self._relation_id |
342 | + |
343 | + @inlineCallbacks |
344 | + def set_data(self, data): |
345 | + """Set the relation local configuration data for a unit. |
346 | + """ |
347 | + unit_settings_path = "/relations/%s/settings/%s" % ( |
348 | + self._relation_id, self._unit_id) |
349 | + yield retry_change( |
350 | + self._client, unit_settings_path, lambda content, stat: data) |
351 | + |
352 | + @inlineCallbacks |
353 | + def get_data(self): |
354 | + """Get the relation local configuration data for a unit. |
355 | + """ |
356 | + data, stat = yield self._client.get( |
357 | + "/relations/%s/settings/%s" % ( |
358 | + self._relation_id, self._unit_id)) |
359 | + returnValue(data) |
360 | |
361 | === modified file 'ensemble/state/tests/common.py' |
362 | --- ensemble/state/tests/common.py 2010-09-20 19:53:57 +0000 |
363 | +++ ensemble/state/tests/common.py 2010-10-29 18:34:42 +0000 |
364 | @@ -25,6 +25,7 @@ |
365 | yield self.client.create("/machines") |
366 | yield self.client.create("/services") |
367 | yield self.client.create("/units") |
368 | + yield self.client.create("/relations") |
369 | |
370 | @inlineCallbacks |
371 | def tearDown(self): |
372 | |
373 | === modified file 'ensemble/state/tests/test_errors.py' |
374 | --- ensemble/state/tests/test_errors.py 2010-09-16 20:13:37 +0000 |
375 | +++ ensemble/state/tests/test_errors.py 2010-10-29 18:34:42 +0000 |
376 | @@ -4,7 +4,9 @@ |
377 | EnsembleError, StateError, StateChanged, FormulaStateNotFound, |
378 | ServiceStateNotFound, ServiceUnitStateNotFound, MachineStateNotFound, |
379 | ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse, |
380 | - BadServiceStateName, EnvironmentStateNotFound) |
381 | + BadServiceStateName, EnvironmentStateNotFound, |
382 | + ServiceRelationAlreadyAssigned, UnitRelationStateNotFound, |
383 | + UnitRelationStateAlreadyAssigned) |
384 | |
385 | |
386 | class StateErrorsTest(TestCase): |
387 | @@ -73,3 +75,25 @@ |
388 | self.assertIsStateError(error) |
389 | self.assertEquals(str(error), |
390 | "Environment state was not found") |
391 | + |
392 | + def test_service_relation_already_assigned(self): |
393 | + error = ServiceRelationAlreadyAssigned( |
394 | + "wordpress", "mysql", "client") |
395 | + self.assertIsStateError(error) |
396 | + msg = ("Service 'wordpress' is already in relation with different " |
397 | + "name 'mysql' or role 'client'.") |
398 | + self.assertEquals(str(error), msg) |
399 | + |
400 | + def test_unit_relation_state_not_found(self): |
401 | + error = UnitRelationStateNotFound( |
402 | + "rel-1", "rel-client", "mysql/0") |
403 | + self.assertIsStateError(error) |
404 | + msg = "The relation 'rel-client' has no unit state for 'mysql/0'" |
405 | + self.assertEquals(str(error), msg) |
406 | + |
407 | + def test_unit_relation_state_exists(self): |
408 | + error = UnitRelationStateAlreadyAssigned( |
409 | + "rel-id", "rel-client", "mysql/0") |
410 | + self.assertIsStateError(error) |
411 | + msg = "The relation 'rel-client' already contains a unit for 'mysql/0'" |
412 | + self.assertEquals(str(error), msg) |
413 | |
414 | === added file 'ensemble/state/tests/test_relation.py' |
415 | --- ensemble/state/tests/test_relation.py 1970-01-01 00:00:00 +0000 |
416 | +++ ensemble/state/tests/test_relation.py 2010-10-29 18:34:42 +0000 |
417 | @@ -0,0 +1,471 @@ |
418 | +import yaml |
419 | + |
420 | +from twisted.internet.defer import inlineCallbacks, returnValue, Deferred |
421 | + |
422 | +from txzookeeper import ZookeeperClient |
423 | + |
424 | +from ensemble.state.formula import FormulaStateManager |
425 | +from ensemble.state.errors import ( |
426 | + StateChanged, ServiceRelationAlreadyAssigned, |
427 | + UnitRelationStateNotFound, UnitRelationStateAlreadyAssigned) |
428 | +from ensemble.state.relation import ( |
429 | + RelationStateManager, ServiceRelationState, UnitRelationState) |
430 | +from ensemble.state.service import ServiceStateManager |
431 | +from ensemble.state.tests.common import StateTestBase |
432 | + |
433 | + |
434 | +class RelationTestBase(StateTestBase): |
435 | + |
436 | + @inlineCallbacks |
437 | + def setUp(self): |
438 | + yield super(RelationTestBase, self).setUp() |
439 | + self.relation_manager = RelationStateManager(self.client) |
440 | + self.formula_manager = FormulaStateManager(self.client) |
441 | + self.service_manager = ServiceStateManager(self.client) |
442 | + self.formula_state = None |
443 | + |
444 | + @inlineCallbacks |
445 | + def add_service(self, name): |
446 | + if not self.formula_state: |
447 | + self.formula_state = yield self.formula_manager.add_formula_state( |
448 | + "namespace", self.formula) |
449 | + service_state = yield self.service_manager.add_service_state( |
450 | + name, self.formula_state) |
451 | + returnValue(service_state) |
452 | + |
453 | + @inlineCallbacks |
454 | + def add_relation(self, relation_type, *services): |
455 | + relation_state = yield self.relation_manager.add_relation_state( |
456 | + relation_type) |
457 | + for service_meta in services: |
458 | + service_state, relation_name, relation_role = service_meta |
459 | + yield relation_state.assign_service( |
460 | + service_state, relation_name, relation_role) |
461 | + returnValue(relation_state) |
462 | + |
463 | + |
464 | +class RelationStateManagerTest(RelationTestBase): |
465 | + |
466 | + @inlineCallbacks |
467 | + def test_add_relation_state(self): |
468 | + """ |
469 | + Adding a relation will create a relation node and update the topology. |
470 | + """ |
471 | + relation_state = yield self.relation_manager.add_relation_state( |
472 | + "mysql") |
473 | + topology = yield self.get_topology() |
474 | + self.assertTrue(topology.has_relation(relation_state.internal_id)) |
475 | + exists = yield self.client.exists( |
476 | + "/relations/%s" % relation_state.internal_id) |
477 | + self.assertTrue(exists) |
478 | + exists = yield self.client.get( |
479 | + "/relations/%s/settings" % relation_state.internal_id) |
480 | + self.assertTrue(exists) |
481 | + |
482 | + @inlineCallbacks |
483 | + def test_remove_relation_state(self): |
484 | + """Removing a relation will remove it from the topology. |
485 | + """ |
486 | + # Simulate add and remove. |
487 | + relation_state = yield self.relation_manager.add_relation_state( |
488 | + "varnish") |
489 | + topology = yield self.get_topology() |
490 | + self.assertTrue(topology.has_relation(relation_state.internal_id)) |
491 | + yield self.relation_manager.remove_relation_state(relation_state) |
492 | + |
493 | + # Verify removal. |
494 | + topology = yield self.get_topology() |
495 | + self.assertFalse(topology.has_relation(relation_state.internal_id)) |
496 | + |
497 | + @inlineCallbacks |
498 | + def test_remove_relation_with_changing_state(self): |
499 | + # Simulate add and remove. |
500 | + relation_state = yield self.relation_manager.add_relation_state( |
501 | + "varnish") |
502 | + topology = yield self.get_topology() |
503 | + self.assertTrue(topology.has_relation(relation_state.internal_id)) |
504 | + yield self.relation_manager.remove_relation_state(relation_state) |
505 | + |
506 | + topology = yield self.get_topology() |
507 | + self.assertFalse(topology.has_relation(relation_state.internal_id)) |
508 | + |
509 | + # try to remove again, should get state change error. |
510 | + yield self.assertFailure( |
511 | + self.relation_manager.remove_relation_state(relation_state), |
512 | + StateChanged) |
513 | + |
514 | + @inlineCallbacks |
515 | + def test_get_relations_for_service(self): |
516 | + # Create some services and relations |
517 | + service1 = yield self.add_service("database") |
518 | + service2 = yield self.add_service("application") |
519 | + service3 = yield self.add_service("cache") |
520 | + |
521 | + relation1 = yield self.add_relation( |
522 | + "database", |
523 | + (service1, "client", "server"), (service2, "db", "client")) |
524 | + |
525 | + relation2 = yield self.add_relation( |
526 | + "cache", |
527 | + (service3, "app", "server"), (service2, "cache", "client")) |
528 | + |
529 | + relations = yield self.relation_manager.get_relations_for_service( |
530 | + service2) |
531 | + rel_ids = [r.internal_relation_id for r in relations] |
532 | + self.assertEqual(sorted(rel_ids), |
533 | + [relation1.internal_id, relation2.internal_id]) |
534 | + |
535 | + relations = yield self.relation_manager.get_relations_for_service( |
536 | + service1) |
537 | + rel_ids = [r.internal_relation_id for r in relations] |
538 | + self.assertEqual(sorted(rel_ids), [relation1.internal_id]) |
539 | + |
540 | + def test_get_relations_for_invalid_service(self): |
541 | + pass |
542 | + |
543 | + @inlineCallbacks |
544 | + def test_get_relations_for_service_with_none(self): |
545 | + service1 = yield self.add_service("database") |
546 | + yield self.relation_manager.get_relations_for_service(service1) |
547 | + |
548 | + |
549 | +class RelationStateTest(RelationTestBase): |
550 | + |
551 | + @inlineCallbacks |
552 | + def setUp(self): |
553 | + yield super(RelationStateTest, self).setUp() |
554 | + |
555 | + self.relation_state = yield self.relation_manager.add_relation_state( |
556 | + "rel-type") |
557 | + self.formula_state = yield self.formula_manager.add_formula_state( |
558 | + "namespace", self.formula) |
559 | + self.service_state1 = yield self.service_manager.add_service_state( |
560 | + "wordpress-prod", self.formula_state) |
561 | + self.service_state2 = yield self.service_manager.add_service_state( |
562 | + "wordpress-dev", self.formula_state) |
563 | + |
564 | + @inlineCallbacks |
565 | + def test_assign_service(self): |
566 | + yield self.relation_state.assign_service( |
567 | + self.service_state1, |
568 | + "dev-instance", |
569 | + "prod") |
570 | + |
571 | + yield self.relation_state.assign_service( |
572 | + self.service_state2, |
573 | + "prod-instance", |
574 | + "dev") |
575 | + |
576 | + topology = yield self.get_topology() |
577 | + |
578 | + self.assertEqual( |
579 | + topology.get_relations_for_service( |
580 | + self.service_state1.internal_id), |
581 | + [(self.relation_state.internal_id, |
582 | + "rel-type", |
583 | + {"name": "dev-instance", "role": "prod"})]) |
584 | + |
585 | + self.assertEqual( |
586 | + topology.get_relation_services(self.relation_state.internal_id), |
587 | + {'service-0000000000': {'name': 'dev-instance', 'role': 'prod'}, |
588 | + 'service-0000000001': {'name': 'prod-instance', 'role': 'dev'}}) |
589 | + |
590 | + @inlineCallbacks |
591 | + def test_assign_service_already_assigned(self): |
592 | + """ |
593 | + Attempting to add a service to a relation multiple times |
594 | + will raise an error if its already there. |
595 | + """ |
596 | + # Assign and verify |
597 | + yield self.relation_state.assign_service(self.service_state1, |
598 | + "dev-instance", |
599 | + "prod") |
600 | + topology = yield self.get_topology() |
601 | + self.assertTrue(topology.relation_has_service( |
602 | + self.relation_state.internal_id, self.service_state1.internal_id)) |
603 | + |
604 | + # If we attempt to add again, we'll get an error. |
605 | + yield self.assertFailure( |
606 | + self.relation_state.assign_service(self.service_state1, |
607 | + "dev-instance", |
608 | + "prod"), |
609 | + ServiceRelationAlreadyAssigned) |
610 | + |
611 | + # If we unassign and assign again, the service container state updates. |
612 | + yield self.relation_state.unassign_service(self.service_state1) |
613 | + yield self.relation_state.assign_service(self.service_state1, |
614 | + "dev-instance2", |
615 | + "prod") |
616 | + content, stat = yield self.client.get( |
617 | + "/relations/%s/%s" % ( |
618 | + self.relation_state.internal_id, "prod")) |
619 | + |
620 | + node_data = yaml.load(content) |
621 | + self.assertEqual(node_data["name"], "dev-instance2") |
622 | + |
623 | + # Attempting to assign when the relation or service has gone away |
624 | + # raises a state changed error. |
625 | + yield self.relation_manager.remove_relation_state(self.relation_state) |
626 | + topology = yield self.get_topology() |
627 | + self.assertFalse( |
628 | + topology.has_relation(self.relation_state.internal_id)) |
629 | + |
630 | + yield self.assertFailure( |
631 | + self.relation_state.assign_service(self.service_state1, |
632 | + "dev-instance2", |
633 | + "prod2"), |
634 | + StateChanged) |
635 | + |
636 | + @inlineCallbacks |
637 | + def test_unassign_service(self): |
638 | + """A service can be disassociated from a relation. |
639 | + """ |
640 | + yield self.relation_state.assign_service(self.service_state1, |
641 | + "dev-instance", |
642 | + "prod") |
643 | + topology = yield self.get_topology() |
644 | + self.assertTrue(topology.relation_has_service( |
645 | + self.relation_state.internal_id, |
646 | + self.service_state1.internal_id)) |
647 | + |
648 | + # Remove and verify |
649 | + yield self.relation_state.unassign_service(self.service_state1) |
650 | + topology = yield self.get_topology() |
651 | + self.assertFalse(topology.relation_has_service( |
652 | + self.relation_state.internal_id, |
653 | + self.service_state1.internal_id)) |
654 | + |
655 | + # if we remove again, we'll succed as the final state is the same. |
656 | + yield self.relation_state.unassign_service(self.service_state1) |
657 | + |
658 | + # Removing again, without the relation state, errors on state change. |
659 | + yield self.relation_manager.remove_relation_state(self.relation_state) |
660 | + yield self.assertFailure( |
661 | + self.relation_state.unassign_service(self.service_state1), |
662 | + StateChanged) |
663 | + |
664 | + |
665 | +class ServiceRelationStateTest(RelationTestBase): |
666 | + |
667 | + @inlineCallbacks |
668 | + def setUp(self): |
669 | + yield super(ServiceRelationStateTest, self).setUp() |
670 | + |
671 | + self.service_state1 = yield self.add_service("wordpress-prod") |
672 | + self.service_state2 = yield self.add_service("wordpress-dev") |
673 | + self.relation_state = yield self.add_relation( |
674 | + "riak", |
675 | + (self.service_state1, "dev-connect", "prod"), |
676 | + (self.service_state2, "prod-connect", "dev")) |
677 | + |
678 | + relations = yield self.relation_manager.get_relations_for_service( |
679 | + self.service_state1) |
680 | + self.service1_relation = relations.pop() |
681 | + |
682 | + def get_presence_path(self, relation_state, relation_role, unit_state): |
683 | + presence_path = "/relations/%s/%s/%s" % ( |
684 | + relation_state.internal_id, |
685 | + relation_role, |
686 | + unit_state.internal_id) |
687 | + return presence_path |
688 | + |
689 | + def test_property_internal_service_id(self): |
690 | + self.assertEqual(self.service1_relation.internal_service_id, |
691 | + self.service_state1.internal_id) |
692 | + |
693 | + def test_property_internal_relation_id(self): |
694 | + self.assertEqual(self.service1_relation.internal_relation_id, |
695 | + self.relation_state.internal_id) |
696 | + |
697 | + def test_property_relation_role(self): |
698 | + self.assertEqual(self.service1_relation.relation_role, "prod") |
699 | + |
700 | + def test_property_relation_name(self): |
701 | + """ |
702 | + The service's name from the relation is accessible from the service |
703 | + relation state. |
704 | + """ |
705 | + self.assertEqual(self.service1_relation.relation_name, "dev-connect") |
706 | + |
707 | + @inlineCallbacks |
708 | + def test_add_unit_state(self): |
709 | + """The service state is used to create units in the relation.""" |
710 | + unit_state = yield self.service_state1.add_unit_state() |
711 | + |
712 | + # set some watches to verify the order things are created. |
713 | + state_created = Deferred() |
714 | + creation_order = [] |
715 | + presence_path = self.get_presence_path( |
716 | + self.relation_state, "prod", unit_state) |
717 | + |
718 | + def append_event(name, event): |
719 | + creation_order.append((name, event)) |
720 | + if len(creation_order) == 2: |
721 | + state_created.callback(creation_order) |
722 | + |
723 | + self.client.exists_and_watch(presence_path)[1].addCallback( |
724 | + lambda result: append_event("presence", result)) |
725 | + |
726 | + settings_path = "/relations/%s/settings/%s" % ( |
727 | + self.relation_state.internal_id, |
728 | + unit_state.internal_id) |
729 | + |
730 | + self.client.exists_and_watch(settings_path)[1].addCallback( |
731 | + lambda result: append_event("settings", result)) |
732 | + |
733 | + # add the unit agent |
734 | + yield self.service1_relation.add_unit_state(unit_state) |
735 | + |
736 | + # wait for the watches |
737 | + yield state_created |
738 | + |
739 | + # Verify order of creation, settings first, then presence. |
740 | + self.assertEqual(creation_order[0][0], "settings") |
741 | + self.assertEqual(creation_order[0][1].type_name, "created") |
742 | + self.assertEqual(creation_order[1][0], "presence") |
743 | + self.assertEqual(creation_order[1][1].type_name, "created") |
744 | + |
745 | + @inlineCallbacks |
746 | + def test_presence_node_is_ephemeral(self): |
747 | + """ |
748 | + A unit relation state is composed of two nodes, an ephemeral |
749 | + presence node, and a persistent settings node. Verify that |
750 | + the presence node is ephemeral. |
751 | + """ |
752 | + unit_state = yield self.service_state1.add_unit_state() |
753 | + |
754 | + # manually construct a unit relation state using a separate |
755 | + # connection. |
756 | + client2 = ZookeeperClient(self.client.servers) |
757 | + yield client2.connect() |
758 | + service_relation = ServiceRelationState( |
759 | + client2, |
760 | + self.service_state1.internal_id, |
761 | + self.relation_state.internal_id, |
762 | + "prod", |
763 | + "name") |
764 | + yield service_relation.add_unit_state(unit_state) |
765 | + |
766 | + presence_path = self.get_presence_path( |
767 | + self.relation_state, "prod", unit_state) |
768 | + |
769 | + exists_d, watch_d = self.client.exists_and_watch(presence_path) |
770 | + exists = yield exists_d |
771 | + |
772 | + self.assertTrue(exists) |
773 | + |
774 | + yield client2.close() |
775 | + event = yield watch_d |
776 | + self.assertEquals(event.type_name, "deleted") |
777 | + |
778 | + @inlineCallbacks |
779 | + def test_add_unit_state_with_preexisting_presence(self): |
780 | + """ |
781 | + If a unit relation presence node exists, attempting |
782 | + to add it again raises an error. |
783 | + """ |
784 | + unit_state = yield self.service_state1.add_unit_state() |
785 | + presence_path = self.get_presence_path( |
786 | + self.relation_state, "prod", unit_state) |
787 | + yield self.client.create(presence_path) |
788 | + |
789 | + yield self.assertFailure( |
790 | + self.service1_relation.add_unit_state(unit_state), |
791 | + UnitRelationStateAlreadyAssigned) |
792 | + |
793 | + @inlineCallbacks |
794 | + def test_add_unit_state_with_preexisting_settings(self): |
795 | + """A unit coming backup retians its existing settings.""" |
796 | + unit_state = yield self.service_state1.add_unit_state() |
797 | + settings_path = "/relations/%s/settings/%s" % ( |
798 | + self.relation_state.internal_id, |
799 | + unit_state.internal_id) |
800 | + |
801 | + data = yaml.dump({"hello": "world"}) |
802 | + yield self.client.create(settings_path, data) |
803 | + yield self.service1_relation.add_unit_state(unit_state) |
804 | + |
805 | + node_data, stat = yield self.client.get(settings_path) |
806 | + self.assertEqual(node_data, data) |
807 | + |
808 | + @inlineCallbacks |
809 | + def test_get_unit_state(self): |
810 | + unit_state = yield self.service_state1.add_unit_state() |
811 | + unit_relation_state = yield self.service1_relation.add_unit_state( |
812 | + unit_state) |
813 | + self.assertTrue(isinstance(unit_relation_state, UnitRelationState)) |
814 | + |
815 | + @inlineCallbacks |
816 | + def test_get_unit_state_nonexistant(self): |
817 | + unit_state = yield self.service_state1.add_unit_state() |
818 | + yield self.assertFailure( |
819 | + self.service1_relation.get_unit_state(unit_state), |
820 | + UnitRelationStateNotFound) |
821 | + |
822 | + |
823 | +class UnitRelationStateTest(RelationTestBase): |
824 | + |
825 | + @inlineCallbacks |
826 | + def add_relation_service_unit(self, relation_type, service_name): |
827 | + service_state = yield self.add_service(service_name) |
828 | + relation_state = yield self.add_relation( |
829 | + relation_type, |
830 | + (service_state, "name", "role")) |
831 | + unit_state = yield service_state.add_unit_state() |
832 | + relations = yield self.relation_manager.get_relations_for_service( |
833 | + service_state) |
834 | + |
835 | + service_relation = relations.pop() |
836 | + relation_unit_state = yield service_relation.add_unit_state( |
837 | + unit_state) |
838 | + |
839 | + returnValue({ |
840 | + "service": service_state, |
841 | + "unit": unit_state, |
842 | + "relation": relation_state, |
843 | + "service_relation": service_relation, |
844 | + "unit_relation": relation_unit_state}) |
845 | + |
846 | + def get_unit_settings_path(self, state_dict): |
847 | + unit_relation_path = "/relations/%s/settings/%s" % ( |
848 | + state_dict["relation"].internal_id, |
849 | + state_dict["unit"].internal_id) |
850 | + return unit_relation_path |
851 | + |
852 | + @inlineCallbacks |
853 | + def test_properties(self): |
854 | + states = yield self.add_relation_service_unit("webcache", "varnish") |
855 | + unit_relation = states["unit_relation"] |
856 | + |
857 | + self.assertEqual( |
858 | + unit_relation.internal_service_id, |
859 | + states["service"].internal_id) |
860 | + self.assertEqual( |
861 | + unit_relation.internal_relation_id, |
862 | + states["relation"].internal_id) |
863 | + self.assertEqual( |
864 | + unit_relation.internal_unit_id, |
865 | + states["unit"].internal_id) |
866 | + |
867 | + @inlineCallbacks |
868 | + def test_get_data(self): |
869 | + states = yield self.add_relation_service_unit("webcache", "varnish") |
870 | + unit_relation = states["unit_relation"] |
871 | + |
872 | + data = yield unit_relation.get_data() |
873 | + self.assertEqual(data, "") |
874 | + |
875 | + unit_relation_path = self.get_unit_settings_path(states) |
876 | + self.client.set(unit_relation_path, "hello world") |
877 | + |
878 | + data = yield unit_relation.get_data() |
879 | + self.assertEqual(data, "hello world") |
880 | + |
881 | + @inlineCallbacks |
882 | + def test_set_data(self): |
883 | + states = yield self.add_relation_service_unit("webcache", "varnish") |
884 | + unit_relation = states["unit_relation"] |
885 | + unit_relation_path = self.get_unit_settings_path(states) |
886 | + yield unit_relation.set_data("hello world") |
887 | + data, stat = yield self.client.get(unit_relation_path) |
888 | + self.assertEqual(data, "hello world") |
889 | |
890 | === modified file 'ensemble/state/tests/test_topology.py' |
891 | --- ensemble/state/tests/test_topology.py 2010-09-14 13:56:56 +0000 |
892 | +++ ensemble/state/tests/test_topology.py 2010-10-29 18:34:42 +0000 |
893 | @@ -57,6 +57,7 @@ |
894 | self.topology.add_machine("m-1") |
895 | |
896 | # Add a non-assigned unit, to test that the logic of |
897 | + |
898 | # checking for assigned units validates this. |
899 | self.topology.add_service("s-0", "wordpress") |
900 | self.topology.add_service_unit("s-0", "u-0") |
901 | @@ -659,3 +660,170 @@ |
902 | self.topology.add_machine("m-0") |
903 | self.topology.reset() |
904 | self.assertEquals(self.topology.dump(), empty_data) |
905 | + |
906 | + def test_has_relation(self): |
907 | + """Testing if a relation exists should be possible. |
908 | + """ |
909 | + self.topology.add_service("s-0", "wordpress") |
910 | + self.assertFalse(self.topology.has_relation("r-1")) |
911 | + self.topology.add_relation("r-1", "type") |
912 | + self.assertTrue(self.topology.has_relation("r-1")) |
913 | + |
914 | + def test_add_relation(self): |
915 | + """Add a relation between the given service ids. |
916 | + """ |
917 | + self.assertFalse(self.topology.has_relation("r-1")) |
918 | + |
919 | + # Verify add relation works correctly. |
920 | + self.topology.add_relation("r-1", "type") |
921 | + self.assertTrue(self.topology.has_relation("r-1")) |
922 | + |
923 | + # Attempting to add again raises an exception |
924 | + self.assertRaises( |
925 | + InternalTopologyError, |
926 | + self.topology.add_relation, "r-1", "type") |
927 | + |
928 | + def test_assign_service_to_relation(self): |
929 | + """A service can be associated to a relation. |
930 | + """ |
931 | + # Both service and relation must be valid. |
932 | + self.assertRaises(InternalTopologyError, |
933 | + self.topology.assign_service_to_relation, |
934 | + "r-1", |
935 | + "s-0", |
936 | + "name", |
937 | + "role") |
938 | + self.topology.add_relation("r-1", "type") |
939 | + self.assertRaises(InternalTopologyError, |
940 | + self.topology.assign_service_to_relation, |
941 | + "r-1", |
942 | + "s-0", |
943 | + "name", |
944 | + "role") |
945 | + self.topology.add_service("s-0", "wordpress") |
946 | + |
947 | + # The relation can be assigned. |
948 | + self.assertFalse(self.topology.relation_has_service("r-1", "s-0")) |
949 | + self.topology.assign_service_to_relation("r-1", "s-0", "name", "role") |
950 | + self.assertEqual(self.topology.get_relations_for_service("s-0"), |
951 | + [("r-1", "type", {"name": "name", "role": "role"})]) |
952 | + |
953 | + # Adding it again raises an error, even with a different name/role |
954 | + self.assertRaises(InternalTopologyError, |
955 | + self.topology.assign_service_to_relation, |
956 | + "r-1", |
957 | + "s-0", |
958 | + "name2", |
959 | + "role2") |
960 | + |
961 | + # Another service can't provide the same role within a relation. |
962 | + self.topology.add_service("s-1", "database") |
963 | + self.assertRaises(InternalTopologyError, |
964 | + self.topology.assign_service_to_relation, |
965 | + "r-1", |
966 | + "s-1", |
967 | + "name", |
968 | + "role") |
969 | + |
970 | + def test_unassign_service_from_relation(self): |
971 | + """A service can be disassociated from a relation. |
972 | + """ |
973 | + # Both service and relation must be valid. |
974 | + self.assertRaises(InternalTopologyError, |
975 | + self.topology.unassign_service_from_relation, |
976 | + "r-1", |
977 | + "s-0") |
978 | + self.topology.add_relation("r-1", "type") |
979 | + self.assertRaises(InternalTopologyError, |
980 | + self.topology.unassign_service_from_relation, |
981 | + "r-1", |
982 | + "s-0") |
983 | + self.topology.add_service("s-0", "wordpress") |
984 | + |
985 | + # If the service is not assigned to the relation, raises an error. |
986 | + self.assertRaises(InternalTopologyError, |
987 | + self.topology.unassign_service_from_relation, |
988 | + "r-1", |
989 | + "s-0") |
990 | + |
991 | + self.topology.assign_service_to_relation("r-1", "s-0", "name", "role") |
992 | + self.assertEqual(self.topology.get_relations_for_service("s-0"), |
993 | + [("r-1", "type", {"name": "name", "role": "role"})]) |
994 | + |
995 | + self.topology.unassign_service_from_relation("r-1", "s-0") |
996 | + self.assertFalse(self.topology.get_relations_for_service("s-0")) |
997 | + |
998 | + def test_relation_has_service(self): |
999 | + """We can test to see if a service is associated to a relation. |
1000 | + """ |
1001 | + self.assertFalse(self.topology.relation_has_service("r-1", "s-0")) |
1002 | + self.topology.add_relation("r-1", "type") |
1003 | + self.topology.add_service("s-0", "wordpress") |
1004 | + self.topology.assign_service_to_relation("r-1", "s-0", "name", "role") |
1005 | + self.assertTrue(self.topology.relation_has_service("r-1", "s-0")) |
1006 | + |
1007 | + def test_get_services_for_relations(self): |
1008 | + """The services for a given relation can be retrieved.""" |
1009 | + self.topology.add_relation("r-1", "type") |
1010 | + self.topology.add_service("s-0", "wordpress") |
1011 | + self.topology.add_service("s-1", "database") |
1012 | + self.topology.assign_service_to_relation("r-1", "s-0", "name", "role") |
1013 | + self.topology.assign_service_to_relation("r-1", "s-1", "name", "role2") |
1014 | + self.assertEqual( |
1015 | + self.topology.get_relation_services("r-1"), |
1016 | + {'s-1': {'role': 'role2', 'name': 'name'}, |
1017 | + 's-0': {'role': 'role', 'name': 'name'}}) |
1018 | + |
1019 | + def test_get_relations_for_service(self): |
1020 | + """The relations for a given service can be retrieved. |
1021 | + """ |
1022 | + # Getting relations for unknown service raises a topologyerror. |
1023 | + self.assertRaises(InternalTopologyError, |
1024 | + self.topology.get_relations_for_service, |
1025 | + "s-0") |
1026 | + |
1027 | + # A new service has no relations. |
1028 | + self.topology.add_service("s-0", "wordpress") |
1029 | + self.assertFalse(self.topology.get_relations_for_service("s-0")) |
1030 | + |
1031 | + # Add a relation and fetch it. |
1032 | + self.topology.add_relation("r-1", "type") |
1033 | + self.topology.assign_service_to_relation("r-1", "s-0", "name", "role") |
1034 | + self.topology.add_relation("r-2", "type") |
1035 | + self.topology.assign_service_to_relation("r-2", "s-0", "name", "role") |
1036 | + |
1037 | + self.assertEqual( |
1038 | + sorted(self.topology.get_relations_for_service("s-0")), |
1039 | + [("r-1", "type", {"name": "name", "role": "role"}), |
1040 | + ("r-2", "type", {"name": "name", "role": "role"})]) |
1041 | + |
1042 | + self.topology.unassign_service_from_relation("r-2", "s-0") |
1043 | + |
1044 | + def test_remove_relation(self): |
1045 | + """A relation can be removed. |
1046 | + """ |
1047 | + # Attempting to remove unknown relation raises a topologyerror |
1048 | + self.assertRaises(InternalTopologyError, |
1049 | + self.topology.remove_relation, |
1050 | + "r-1") |
1051 | + |
1052 | + # Adding a relation with associated service, and remove it. |
1053 | + self.topology.add_service("s-0", "wordpress") |
1054 | + self.topology.add_relation("r-1", "type") |
1055 | + self.topology.assign_service_to_relation("r-1", "s-0", "name", "role") |
1056 | + self.assertTrue(self.topology.has_relation("r-1")) |
1057 | + self.topology.remove_relation("r-1") |
1058 | + self.assertFalse(self.topology.has_relation("r-1")) |
1059 | + |
1060 | + def test_remove_service_with_relations(self): |
1061 | + """ |
1062 | + Attempting to remove a service that's assigned to relations |
1063 | + raises an InternalTopologyError. |
1064 | + """ |
1065 | + self.topology.add_service("s-0", "wordpress") |
1066 | + self.topology.add_relation("r-1", "type") |
1067 | + self.topology.assign_service_to_relation("r-1", "s-0", "name", "role") |
1068 | + |
1069 | + self.assertRaises(InternalTopologyError, |
1070 | + self.topology.remove_service, |
1071 | + "s-0") |
1072 | |
1073 | === modified file 'ensemble/state/topology.py' |
1074 | --- ensemble/state/topology.py 2010-10-15 20:46:21 +0000 |
1075 | +++ ensemble/state/topology.py 2010-10-29 18:34:42 +0000 |
1076 | @@ -134,6 +134,11 @@ |
1077 | """Remove service_id from this topology. |
1078 | """ |
1079 | self._assert_service(service_id) |
1080 | + relations = self.get_relations_for_service(service_id) |
1081 | + if relations: |
1082 | + raise InternalTopologyError( |
1083 | + "Service %r is associated to relations %s" % ( |
1084 | + service_id, relations)) |
1085 | del self._state["services"][service_id] |
1086 | |
1087 | def add_service_unit(self, service_id, unit_id): |
1088 | @@ -259,6 +264,96 @@ |
1089 | units.append(unit_id) |
1090 | return units |
1091 | |
1092 | + def add_relation(self, relation_id, relation_type): |
1093 | + """Add a relation with given id and of the given type. |
1094 | + """ |
1095 | + relations = self._state.setdefault("relations", {}) |
1096 | + if relation_id in relations: |
1097 | + raise InternalTopologyError( |
1098 | + "Relation id %r already in use" % relation_id) |
1099 | + relations[relation_id] = (relation_type, {}) |
1100 | + |
1101 | + def has_relation(self, relation_id): |
1102 | + """Return True if relation with the given id exists in the topology. |
1103 | + """ |
1104 | + return relation_id in self._state.get("relations", self._nil_dict) |
1105 | + |
1106 | + def get_relation_services(self, relation_id): |
1107 | + """Get all the services associated to the relation. |
1108 | + """ |
1109 | + self._assert_relation(relation_id) |
1110 | + relation_type, services = self._state["relations"][relation_id] |
1111 | + return services |
1112 | + |
1113 | + def get_relation_type(self, relation_id): |
1114 | + """Get the type of a relation.""" |
1115 | + self._assert_relation(relation_id) |
1116 | + relation_type, services = self._states["relations"][relation_id] |
1117 | + return relation_type |
1118 | + |
1119 | + def relation_has_service(self, relation_id, service_id): |
1120 | + """Return true if service_id is assigned to relation_id. |
1121 | + """ |
1122 | + relations = self._state.get("relations", self._nil_dict) |
1123 | + relation_type, services = relations.get(relation_id, |
1124 | + (None, self._nil_dict)) |
1125 | + return service_id in services |
1126 | + |
1127 | + def remove_relation(self, relation_id): |
1128 | + """It should be possible to remove a relation. |
1129 | + """ |
1130 | + self._assert_relation(relation_id) |
1131 | + del self._state["relations"][relation_id] |
1132 | + |
1133 | + def assign_service_to_relation(self, relation_id, service_id, name, role): |
1134 | + """Associate a service to a relation. |
1135 | + |
1136 | + @param role: The relation role of the service. |
1137 | + @param name: The relation name from the service. |
1138 | + """ |
1139 | + self._assert_service(service_id) |
1140 | + self._assert_relation(relation_id) |
1141 | + relation_type, services = self._state["relations"][relation_id] |
1142 | + for sid in services: |
1143 | + if sid == service_id: |
1144 | + raise InternalTopologyError( |
1145 | + "Service %r is already assigned to relation %r" % ( |
1146 | + service_id, relation_id)) |
1147 | + service_info = services[sid] |
1148 | + if service_info["role"] == role: |
1149 | + raise InternalTopologyError( |
1150 | + ("Another service %r is already providing %r " |
1151 | + "role in relation") % (sid, service_info["role"])) |
1152 | + |
1153 | + services[service_id] = {"role": role, "name": name} |
1154 | + |
1155 | + def unassign_service_from_relation(self, relation_id, service_id): |
1156 | + """Disassociate service to relation. |
1157 | + """ |
1158 | + self._assert_service(service_id) |
1159 | + self._assert_relation(relation_id) |
1160 | + relation_type, services = self._state["relations"][relation_id] |
1161 | + if not service_id in services: |
1162 | + raise InternalTopologyError( |
1163 | + "Service %r is not assigned to relation %r" % ( |
1164 | + service_id, relation_id)) |
1165 | + del services[service_id] |
1166 | + |
1167 | + def get_relations_for_service(self, service_id): |
1168 | + """Given a service id we should be able to retrieve its relations.""" |
1169 | + self._assert_service(service_id) |
1170 | + relations = [] |
1171 | + for relation_id, (relation_type, services) in self._state.get( |
1172 | + "relations", self._nil_dict).items(): |
1173 | + if service_id in services: |
1174 | + relations.append(( |
1175 | + relation_id, relation_type, services[service_id])) |
1176 | + return relations |
1177 | + |
1178 | + def _assert_relation(self, relation_id): |
1179 | + if relation_id not in self._state.get("relations", self._nil_dict): |
1180 | + raise InternalTopologyError("Relation not found: %s" % relation_id) |
1181 | + |
1182 | def _assert_machine(self, machine_id): |
1183 | if machine_id not in self._state.get("machines", self._nil_dict): |
1184 | raise InternalTopologyError("Machine not found: %s" % machine_id) |
The overall feeling of the work so far is very good. Feels generally clean, and understandable.
Some specific points:
[1]
+ def add_relation(self, relation_id):
+ """Add a relation with given id between the given services.
(...)
+ def has_relation(self, relation_id):
+ """Return True if service_id was previously added to this topology.
These docstrings aren't reflecting the implementation.
[2]
+ def has_relation_ service( self, relation_id, service_id):
relation_ has_service might read better here.
[3]
+ def unassign_ service_ to_relation( self, relation_id, service_id):
+ """Disassociate service to relation.
Similarly, shouldn't that be "_from_relation" and "from relation"?
[4]
All of the date for the relation is missing from the topology (relation name, relation type, relation role). Without at least some of these in the topology, we can't do the mapping of a user provided name to the relation id without iterating through the nodes.
[5]
+ Type is used to distinguish the type of the relation, ie. postgres' , or 'http.cache. varnish' .
+ 'database.
We haven't discussed/specified the namespacing of the types. We have to talk about "http.cache. varnish" vs. "varnish".
[6]
+ node_data = {"type": relation_type} create( "/relations/ relation- ", dump(node_ data),
+ path = yield self._client.
+ yaml.safe_
I'm wondering a bit about where to store the information for the relation. It feels a bit clumsy to have the relation type inside the relation node itself, as a first gut feeling. Imagine what it'd take to implement the matching of relation types to resolve dependencies. We'd basically have to look at the topology and then iterate over every node we have to discover which of them is responsible for which relation type/name/role.
[7]
+ internal_id = path.rsplit("/", 1)[1]
FWIW, that's os.path. basename( path)
[8]
+ # Not clear if we really need to update the topology till the relation
+ # is in use. Topology updates are broadcasting to most connected
+ # clients. perhaps a separate batch api would be useful to minimize
+ # broadcast events on the topology.
Yeah, it's a good question. I guess it's alright for now, since the model feels good. Adding brand new relations should be an operation generally done after a user action, anyway, so this is not a fast path we'll have to worry about hopefully.
[9]
+ @param name - The service's name for this relation.
+ @param role - The service's role within this relation.
I've mentioned this a few times, and we really have to define how to call these. I've been calling these "relation role" and "relation name". These terms are unique for the relation, and can be used by themselves. We cannot use "service role" and "service name" by itself, though. We'll always have to say "the service role in this relation" and "the service name in this relation" instead, due to the fact that a service has many relations and thus many names and roles.
We have to make a decision regarding this nomenclature over UDS and stop using mixed terms to avoid confus...