Merge lp:~hazmat/pyjuju/relation-state into lp:pyjuju

Proposed by Kapil Thangavelu
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
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
Review via email: mp+39328@code.launchpad.net

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.

To post a comment you must log in.
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :
Download full text (4.0 KiB)

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.
+ 'database.postgres', or 'http.cache.varnish'.

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}
+ path = yield self._client.create("/relations/relation-",
+ yaml.safe_dump(node_data),

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...

Read more...

Revision history for this message
Kapil Thangavelu (hazmat) wrote :
Download full text (6.4 KiB)

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_service(self, relation_id, service_id):
>
> relation_has_service might read better here.

agreed.

>
> [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"?
>

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.postgres', or 'http.cache.varnish'.
>
> We haven't discussed/specified the namespacing of the types. We have to
> talk about "http.cache.varnish" vs. "varnish".
>

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.create("/relations/relation-",
> + yaml.safe_dump(node_data),
>
> 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 ...

Read more...

Revision history for this message
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!

Revision history for this message
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.

lp:~hazmat/pyjuju/relation-state updated
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

Revision history for this message
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.

lp:~hazmat/pyjuju/relation-state updated
101. By Kapil Thangavelu

address some review comments

Revision history for this message
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.basename(path)#.rsplit("/", 1)[1]

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(StateBase):
+
+ def __init__(self, client, internal_relation_id):
+ 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_dump(dict(name = name, role = role))

As per PEP8, in these cases let's use spacing like dict(name=name, role=role).

[16]

+ service_id = service_state.internal_id

Would you mind to use the name "internal_service_id" for this variable and respective attribute in classes? I've been trying to be explicit about this to avoid surfacing this id outside of the state API (we have the service_state.name for that).

[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).

review: Approve
Revision history for this message
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

lp:~hazmat/pyjuju/relation-state updated
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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)

Subscribers

People subscribed via source and target branches

to status/vote changes: