Merge lp:~jimbaker/pyjuju/rel-mgmt-add-relation-simple into lp:pyjuju

Proposed by Jim Baker
Status: Merged
Approved by: Gustavo Niemeyer
Approved revision: 131
Merged at revision: 115
Proposed branch: lp:~jimbaker/pyjuju/rel-mgmt-add-relation-simple
Merge into: lp:pyjuju
Diff against target: 1674 lines (+608/-532)
12 files modified
ensemble/hooks/tests/test_invoker.py (+9/-5)
ensemble/state/endpoint.py (+6/-6)
ensemble/state/errors.py (+24/-10)
ensemble/state/relation.py (+88/-102)
ensemble/state/service.py (+5/-2)
ensemble/state/tests/test_endpoint.py (+8/-8)
ensemble/state/tests/test_errors.py (+31/-8)
ensemble/state/tests/test_hook.py (+9/-5)
ensemble/state/tests/test_relation.py (+297/-235)
ensemble/state/tests/test_service.py (+49/-142)
ensemble/state/tests/test_topology.py (+55/-7)
ensemble/state/topology.py (+27/-2)
To merge this branch: bzr merge lp:~jimbaker/pyjuju/rel-mgmt-add-relation-simple
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
Review via email: mp+42704@code.launchpad.net

Description of the change

This is substantially Kapil's patch (https://gist.github.com/727049). I have changed it so that add_relation_state still takes a relation type and kept assign_service, both for backwards compatibility. Otherwise, the scope of changes to the current tests would have been much larger, at least as I was able to determine. The remaining changes then are in the tests, mostly with with respect to consuming the tuple returned by add_relation_state, which is the RelationState plus any additional ServiceRelationStates, created.

This branch is of course much simpler than what I originally proposed for a command pattern approach to composable changes in https://bugs.launchpad.net/ensemble/+bug/684465

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

[1]

Jim, as Kapil stated and as I reminded over our call, Kapil's code was a quick hack put
in place to demonstrate the concept. There are error situations which should be verified
and tested within that logic. Assigning a service to the same relation twice shouldn't
be possible, for instance, and InternalTopologyErrors should never surface out of the
state code, so there are checks which must be made and appropriate errors raised in
those cases.

Please look at existing code similar to that logic to identify the pattern.

review: Needs Fixing
Revision history for this message
Jim Baker (jimbaker) wrote :

On Mon, Dec 6, 2010 at 1:13 PM, Gustavo Niemeyer <email address hidden>wrote:

> Review: Needs Fixing
>
> [1]
>
> Jim, as Kapil stated and as I reminded over our call, Kapil's code was a
> quick hack put
> in place to demonstrate the concept. There are error situations which
> should be verified
> and tested within that logic. Assigning a service to the same relation
> twice shouldn't
> be possible, for instance, and InternalTopologyErrors should never surface
> out of the
> state code, so there are checks which must be made and appropriate errors
> raised in
> those cases.
>
> Please look at existing code similar to that logic to identify the pattern.
>

ack

111. By Jim Baker

Added unit tests for duplicate assignments and missing services in add_relation_state

112. By Jim Baker

Added unit tests for duplicate assignments and missing services in add_relation_state

113. By Jim Baker

More unit tests around add_relation_state to enusre InternalTopologyError exceptions do not leak

114. By Jim Baker

PEP8

Revision history for this message
Jim Baker (jimbaker) wrote :

Please take a look at the latest version of the branch that I've pushed.

On Mon, Dec 6, 2010 at 1:58 PM, Jim Baker <email address hidden> wrote:

> On Mon, Dec 6, 2010 at 1:13 PM, Gustavo Niemeyer <email address hidden>wrote:
>
>> Review: Needs Fixing
>>
>> [1]
>>
>> Jim, as Kapil stated and as I reminded over our call, Kapil's code was a
>> quick hack put
>> in place to demonstrate the concept. There are error situations which
>> should be verified
>> and tested within that logic. Assigning a service to the same relation
>> twice shouldn't
>> be possible, for instance, and InternalTopologyErrors should never surface
>> out of the
>> state code, so there are checks which must be made and appropriate errors
>> raised in
>> those cases.
>>
>> Please look at existing code similar to that logic to identify the
>> pattern.
>>
>
> ack
>

Additional checks added, along with unit tests to verify that appropriate
errors are raised (presumably ServiceRelationAlreadyAssigned) instead of
InternalTopologyError.

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

[1]

Is the error checking really doing the right thing? It looks like it's verifying that
the services were not yet added to the relation which was just created, which will of
course always be false.

Am I misunderstanding it?

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

As an additional aid, please note that the intention is preventing that the same
relation *endpoints* are not connected twice between the same two services. It's
fine to have different endpoints connected, though.

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

[2]

One more point, I believe there's no need to have relation_type in the method signature,
since all of the endpoints must necessarily have that type as well (and they must be
the same, which is another interesting situation to catch).

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

[3]

379 + def relation_has_role(self, relation_id, role):
380 + """Return true if this `role` is already assigned to `relation_id`.
381 + """

That's missing unittests.

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

[4]

RelationState.assign_service() is probably a red herring, and should be removed so that we can
focus on the real issues. We have no plans of introducing a way to link additional *services*
into the same *relation*.

Revision history for this message
Jim Baker (jimbaker) wrote :

On Tue, Dec 7, 2010 at 1:41 PM, Gustavo Niemeyer <email address hidden>wrote:

> [4]
>
> RelationState.assign_service() is probably a red herring, and should be
> removed so that we can
> focus on the real issues. We have no plans of introducing a way to link
> additional *services*
> into the same *relation*.
>

agreed!

Revision history for this message
Jim Baker (jimbaker) wrote :

On Tue, Dec 7, 2010 at 1:38 PM, Gustavo Niemeyer <email address hidden>wrote:

> [3]
>
> 379 + def relation_has_role(self, relation_id, role):
> 380 + """Return true if this `role` is already assigned to
> `relation_id`.
> 381 + """
>
> That's missing unittests.
>

ack

Revision history for this message
Jim Baker (jimbaker) wrote :

On Tue, Dec 7, 2010 at 1:25 PM, Gustavo Niemeyer <email address hidden>wrote:

> [2]
>
> One more point, I believe there's no need to have relation_type in the
> method signature,
> since all of the endpoints must necessarily have that type as well (and
> they must be
> the same, which is another interesting situation to catch).

We should assert the endpoints share a relation_type. In addition we now
must have either one or two endpoints passed in, since we will eliminate the
relation_type (and vestigial assign_service).

Revision history for this message
Jim Baker (jimbaker) wrote :

On Tue, Dec 7, 2010 at 1:20 PM, Gustavo Niemeyer <email address hidden>wrote:

>
> [1]
>
> Is the error checking really doing the right thing? It looks like it's
> verifying that
> the services were not yet added to the relation which was just created,
> which will of
> course always be false.
>
> Am I misunderstanding it?

This is not clear to me - what error checking are you referring specifically
to?

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

[1]

Of course. I'm referring to the error checking you introduced in the branch as
a side effect of point 1 above. E.g.:

55 + if (topology.relation_has_service(
56 + service_relation.internal_relation_id,
57 + service_relation.internal_service_id) or
58 + topology.relation_has_role(
59 + service_relation.internal_relation_id,
60 + service_relation.relation_role)):
61 + raise ServiceRelationAlreadyAssigned(
62 + topology.get_service_name(
63 + service_relation.internal_service_id),
64 + service_relation.relation_name,
65 + service_relation.relation_role)

[2]

> We should assert the endpoints share a relation_type. In addition we now
> must have either one or two endpoints passed in, since we will eliminate the
> relation_type (and vestigial assign_service).

Sounds good. It doesn't ever make sense to create a relation without services, and
most probably, as we discussed yesterday, it doesn't make sense to assign new
services to an existing relation either.

115. By Jim Baker

Merge trunk

116. By Jim Baker

Transitional code to remove support for assign_service/unassign_service

117. By Jim Baker

Fix ensemble.state.tests.test_hook

118. By Jim Baker

Fix ensemble.hooks.tests.test_invoker

119. By Jim Baker

Fix test_watch_relations_when_being_created

120. By Jim Baker

Change add_relation_state so that it requires 1 or 2 endpoints, which share a relation type

121. By Jim Baker

Removed relation_type from add_relation_state signature

122. By Jim Baker

Cleanup

123. By Jim Baker

Update docs and use better method names

124. By Jim Baker

Merged trunk

125. By Jim Baker

Test topology.relation_has_role

126. By Jim Baker

Verify compatibility of endpoints in add_relation_state, so we can remove topology checking logic

127. By Jim Baker

PEP8

Revision history for this message
Jim Baker (jimbaker) wrote :

On Wed, Dec 8, 2010 at 6:07 AM, Gustavo Niemeyer <email address hidden>wrote:

> [1]
>
> Of course. I'm referring to the error checking you introduced in the branch
> as
> a side effect of point 1 above. E.g.:
>
> 55 + if (topology.relation_has_service(
> 56 + service_relation.internal_relation_id,
> 57 + service_relation.internal_service_id) or
> 58 + topology.relation_has_role(
> 59 + service_relation.internal_relation_id,
> 60 + service_relation.relation_role)):
> 61 + raise ServiceRelationAlreadyAssigned(
> 62 + topology.get_service_name(
> 63 + service_relation.internal_service_id),
> 64 + service_relation.relation_name,
> 65 + service_relation.relation_role)
>
>
Removed error checking in the topology test in favor of checking the
endpoints for compatibility. As a side effect, I also did the pending
revision of provides -> server, requires -> client, and peers -> peer to
standardize role names. Note that since we use other role names in testing,
the endpoint verification is weaker than it might be.

> [2]
>
> > We should assert the endpoints share a relation_type. In addition we now
> > must have either one or two endpoints passed in, since we will eliminate
> the
> > relation_type (and vestigial assign_service).
>
> Sounds good. It doesn't ever make sense to create a relation without
> services, and
> most probably, as we discussed yesterday, it doesn't make sense to assign
> new
> services to an existing relation either.
>
>
Done

Revision history for this message
Jim Baker (jimbaker) wrote :

On Tue, Dec 7, 2010 at 1:25 PM, Gustavo Niemeyer <email address hidden>wrote:

> [2]
>
> One more point, I believe there's no need to have relation_type in the
> method signature,
> since all of the endpoints must necessarily have that type as well (and
> they must be
> the same, which is another interesting situation to catch).

Removed, and asserted they must be the same in the endpoints, with tests.

128. By Jim Baker

Added implementation, testing, and use of InternalTopology.has_relation_between_endpoints to prevent adding the same relation for endpoints twice (peer or binary)

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

[5]

Here is a simpler version of has_relation_between_endpoints:

        service_ids = dict((e, self.find_service_with_name(e.service_name))
                           for e in endpoints)
        relations = self._state.get("relations", self._nil_dict)
        for _, services in relations.itervalues():
            for endpoint in endpoints:
                service = services.get(service_ids[endpoint])
                if not service or service["name"] != endpoint.relation_name:
                    break
            else:
                return True
        return False

[6]

1450 + def test_has_relation_between_dyadic_endpoints_wrong_role(self):

This test is verifying a situation we don't ever want to allow, and if the situation
does to through as a bug, we shouldn't allow a new relation to be created among the
same endpoints. In other words, (service1:a, service2:b) shouldn't *ever* be ambiguous.

[7]

+ # TODO: consider adding this assertion, but it will break some
+ # test code (example: some tests use the role name of
+ # ``role``). It may also make sense to have other types of
+ # role names outside of testing (such as ``dev`` or ``prod``).

The assertion seems sensible. The alternative relation roles don't seem to make sense, though.

[8]

+ def add_relation(topology):
+ if topology.has_relation_between_endpoints(endpoints):
+ raise RelationAlreadyExists(endpoints)

Besides doing this inside the change method, I suggest redundantly doing that
upfront before the relation state is created, so that we avoid garbage which
we know won't be usable.

[9]

+ if len(endpoints) == 2:
+ assert endpoints[0].relation_role != "peer", \
+ "Duplicate peer endpoint"
+ if (endpoints[0].relation_role in ("client", "server") or
+ endpoints[1].relation_role in ("client", "server")):
+ assert endpoints[0].may_relate_to(endpoints[1]), \
+ "Endpoints are not compatible: %r" % (endpoints,)

These assertions should be proper well-tested errors, rather than AssertionErrors,
since they are acceptable situations within the application which need proper
handling and proper notifying.

Also, note that the nested "if" isn't really necessary. If there are two endpoints,
they must necessarily be compatible, so you can safely enforce endpoints[0].may_relate_to(endpoints[1]) at all times.

[10]

+ self.topology._relation_matches_endpoints(
+ "mysql",
+ ("mysql", {"s-0": {"name": "mysql", "role": "client"}}),
+ {"s-0": blog_ep, "s-1": mysql_ep}),
+ "Missing assigned service")

Unless really necessary, we try to avoid whitebox testing, since it binds the test to
the specifics of the implementation. That case is even more obvious given how convoluted
the signature of this method is. Just looking at this method, for instance, I have no
idea about what these arguments mean.

Luckily, given [5], this should easily go away.

review: Needs Fixing
Revision history for this message
Jim Baker (jimbaker) wrote :
Download full text (3.8 KiB)

Pushed the latest revision, so please do what I hope is one last review on
that.

On Thu, Dec 9, 2010 at 8:36 AM, Gustavo Niemeyer <email address hidden>wrote:

> Review: Needs Fixing
> [5]
>
> Here is a simpler version of has_relation_between_endpoints:
>
> service_ids = dict((e, self.find_service_with_name(e.service_name))
> for e in endpoints)
> relations = self._state.get("relations", self._nil_dict)
> for _, services in relations.itervalues():
> for endpoint in endpoints:
> service = services.get(service_ids[endpoint])
> if not service or service["name"] != endpoint.relation_name:
> break
> else:
> return True
> return False
>
>
Implemented, thanks!

> [6]
>
> 1450 + def
> test_has_relation_between_dyadic_endpoints_wrong_role(self):
>
> This test is verifying a situation we don't ever want to allow, and if the
> situation
> does to through as a bug, we shouldn't allow a new relation to be created
> among the
> same endpoints. In other words, (service1:a, service2:b) shouldn't *ever*
> be ambiguous.
>
>
Removed

> [7]
>
> + # TODO: consider adding this assertion, but it will break some
> + # test code (example: some tests use the role name of
> + # ``role``). It may also make sense to have other types of
> + # role names outside of testing (such as ``dev`` or ``prod``).
>
> The assertion seems sensible. The alternative relation roles don't seem to
> make sense, though.
>

Fixed the TODO to document the intention for a future branch, where we will
change the tests accordingly so they don't use invalid role names.

> [8]
>
> + def add_relation(topology):
> + if topology.has_relation_between_endpoints(endpoints):
> + raise RelationAlreadyExists(endpoints)
>
> Besides doing this inside the change method, I suggest redundantly doing
> that
> upfront before the relation state is created, so that we avoid garbage
> which
> we know won't be usable.
>

Makes sense, added. Ideally this race on adds would be tested via mocker's
patching support, but deferring that for now, since this can be considered
an optimization.

> [9]
>
> + if len(endpoints) == 2:
> + assert endpoints[0].relation_role != "peer", \
> + "Duplicate peer endpoint"
> + if (endpoints[0].relation_role in ("client", "server") or
> + endpoints[1].relation_role in ("client", "server")):
> + assert endpoints[0].may_relate_to(endpoints[1]), \
> + "Endpoints are not compatible: %r" % (endpoints,)
>
> These assertions should be proper well-tested errors, rather than
> AssertionErrors,
> since they are acceptable situations within the application which need
> proper
> handling and proper notifying.
>
> Also, note that the nested "if" isn't really necessary. If there are two
> endpoints,
> they must necessarily be compatible, so you can safely enforce
> endpoints[0].may_relate_to(endpoints[1]) at all times.
>

Changed the errors accordingly and cleaned up the type checking logic. The
n...

Read more...

129. By Jim Baker

Rewritten InternalTopology.has_relation_between_endpoints, per review

130. By Jim Baker

Resolved conflicts with trunk merge

131. By Jim Baker

Fixes per review comments

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Thanks for the changes. Just one follow up:

[9]

> Changed the errors accordingly and cleaned up the type checking logic. The
> nested-if is still required until we update the tests, per [7].

No, it's not required. As mentioned, the only chance of may_relate_to() being true
in that location is them being of the correct roles and types, and if it is false
the endpoints are necessarily incompatible.

Considering that, +1!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ensemble/hooks/tests/test_invoker.py'
2--- ensemble/hooks/tests/test_invoker.py 2010-12-07 19:05:09 +0000
3+++ ensemble/hooks/tests/test_invoker.py 2010-12-10 23:06:27 +0000
4@@ -11,6 +11,7 @@
5 from ensemble.hooks import invoker
6 from ensemble.hooks.protocol import UnitSettingsFactory
7 from ensemble.state import hook
8+from ensemble.state.endpoint import RelationEndpoint
9 from ensemble.state.tests.test_relation import RelationTestBase
10
11
12@@ -104,11 +105,14 @@
13
14 @defer.inlineCallbacks
15 def _default_relations(self):
16- self.mysql_states = yield self.add_relation_service_unit(
17- "mysql", "mysql", "", "server")
18- self.wordpress_states = yield self.add_relation_service_unit(
19- "mysql", "wordpress", "", "client",
20- relation_state=self.mysql_states["relation"])
21+ wordpress_ep = RelationEndpoint(
22+ "wordpress", "client-server", "", "client")
23+ mysql_ep = RelationEndpoint(
24+ "mysql", "client-server", "", "server")
25+ self.wordpress_states = yield self.add_relation_service_unit_from_endpoints(
26+ wordpress_ep, mysql_ep)
27+ self.mysql_states = yield self.add_opposite_service_unit(
28+ self.wordpress_states)
29 self.relation = self.mysql_states["unit_relation"]
30
31 def _manage_path(self):
32
33=== modified file 'ensemble/state/endpoint.py'
34--- ensemble/state/endpoint.py 2010-12-03 03:36:30 +0000
35+++ ensemble/state/endpoint.py 2010-12-10 23:06:27 +0000
36@@ -33,9 +33,9 @@
37 if not isinstance(other, RelationEndpoint):
38 raise TypeError("Not a RelationEndpoint", other)
39 return (self.relation_type == other.relation_type and
40- ((self.relation_role == "provides" and
41- other.relation_role == "requires") or
42- (self.relation_role == "requires" and
43- other.relation_role == "provides") or
44- (self.relation_role == "peers" and
45- other.relation_role == "peers")))
46+ ((self.relation_role == "server" and
47+ other.relation_role == "client") or
48+ (self.relation_role == "client" and
49+ other.relation_role == "server") or
50+ (self.relation_role == "peer" and
51+ other.relation_role == "peer")))
52
53=== modified file 'ensemble/state/errors.py'
54--- ensemble/state/errors.py 2010-12-10 04:55:47 +0000
55+++ ensemble/state/errors.py 2010-12-10 23:06:27 +0000
56@@ -94,18 +94,13 @@
57 self.unit_name
58
59
60-class ServiceRelationAlreadyAssigned(StateError):
61+class RelationAlreadyExists(StateError):
62
63- def __init__(self, service_name, relation_name, relation_role):
64- self.service_name = service_name
65- self.relation_name = relation_name
66- self.relation_role = relation_role
67+ def __init__(self, *endpoints):
68+ self.endpoints = endpoints
69
70 def __str__(self):
71- return (
72- "Service %r is already in relation with different name %r "
73- "or role %r.") % (
74- self.service_name, self.relation_name, self.relation_role)
75+ return "Relation already exists %r" % (self.endpoints,)
76
77
78 class UnitRelationStateAlreadyAssigned(StateError):
79@@ -156,7 +151,7 @@
80 self.relation_role, self.service_name)
81
82
83-class BadDescriptor(StateError):
84+class BadDescriptor(ValueError, EnsembleError):
85 """Descriptor is not valid.
86
87 A descriptor must be of the form <service name>[:<relation name>].
88@@ -169,3 +164,22 @@
89
90 def __str__(self):
91 return "Bad descriptor: %r" % (self.descriptor,)
92+
93+
94+class DuplicateEndpoints(StateError):
95+ """Endpoints cannot be duplicate."""
96+
97+ def __init__(self, *endpoints):
98+ self.endpoints = endpoints
99+
100+ def __str__(self):
101+ return "Duplicate endpoints: %r" % (self.endpoints,)
102+
103+class IncompatibleEndpoints(StateError):
104+ """Endpoints are incompatible."""
105+
106+ def __init__(self, *endpoints):
107+ self.endpoints = endpoints
108+
109+ def __str__(self):
110+ return "Incompatible endpoints: %r" % (self.endpoints,)
111
112=== modified file 'ensemble/state/relation.py'
113--- ensemble/state/relation.py 2010-12-07 21:49:45 +0000
114+++ ensemble/state/relation.py 2010-12-10 23:06:27 +0000
115@@ -9,37 +9,107 @@
116
117 from ensemble.state.base import StateBase
118 from ensemble.state.errors import (
119- StateChanged, ServiceRelationAlreadyAssigned, UnitRelationStateNotFound,
120- UnitRelationStateAlreadyAssigned, UnknownRelationRole)
121+ DuplicateEndpoints, IncompatibleEndpoints, RelationAlreadyExists,
122+ StateChanged, UnitRelationStateAlreadyAssigned, UnitRelationStateNotFound,
123+ UnknownRelationRole)
124
125
126 class RelationStateManager(StateBase):
127 """Manages the state of relations in an environment."""
128
129 @inlineCallbacks
130- def add_relation_state(self, relation_type):
131- """Add a new relation state. The relation is of the given type.
132+ def add_relation_state(self, *endpoints):
133+ """Add new relation state with the common relation type of `endpoints`.
134
135- Type is used to distinguish the type of the relation, ie.
136- 'postgres', or 'varnish'.
137+ There must be one or two endpoints specified, with the same
138+ `relation_type`. Their corresponding services will be assigned
139+ atomically.
140 """
141+
142+ # the TOODs in the following comments in this function are for
143+ # type checking to be implemented ASAP. However, this will
144+ # require some nontrivial test modification, so it's best to
145+ # be done in a future branch. This is because these tests use
146+ # such invalid role names as ``role``, ``dev``, and ``prod``;
147+ # or they add non-peer relations of only one endpoint.
148+
149+ if len(endpoints) == 1:
150+ # TODO verify that the endpoint is a peer endpoint only
151+ pass
152+ elif len(endpoints) == 2:
153+ if endpoints[0] == endpoints[1]:
154+ raise DuplicateEndpoints(endpoints)
155+
156+ # TODO verify that the relation roles are client or server
157+ # only
158+ if (endpoints[0].relation_role in ("client", "server") or
159+ endpoints[1].relation_role in ("client", "server")):
160+ if not endpoints[0].may_relate_to(endpoints[1]):
161+ raise IncompatibleEndpoints(endpoints)
162+ else:
163+ raise TypeError("Requires 1 or 2 endpoints, %d given" % \
164+ len(endpoints))
165+
166+ # first check so as to prevent unnecessary garbage in ZK in
167+ # case this relation has been previously added
168+ topology = yield self._read_topology()
169+ if topology.has_relation_between_endpoints(endpoints):
170+ raise RelationAlreadyExists(endpoints)
171+
172+ relation_type = endpoints[0].relation_type
173+ relation_id = yield self._add_relation_state(relation_type)
174+ services = []
175+ for endpoint in endpoints:
176+ service_id = topology.find_service_with_name(endpoint.service_name)
177+ yield self._add_service_relation_state(
178+ relation_id, service_id, endpoint)
179+ services.append(ServiceRelationState(self._client,
180+ service_id,
181+ relation_id,
182+ endpoint.relation_role,
183+ endpoint.relation_name))
184+
185+ def add_relation(topology):
186+ if topology.has_relation_between_endpoints(endpoints):
187+ raise RelationAlreadyExists(endpoints)
188+ topology.add_relation(relation_id, relation_type)
189+ for service_relation in services:
190+ if not topology.has_service(service_id):
191+ raise StateChanged()
192+ topology.assign_service_to_relation(
193+ relation_id,
194+ service_relation.internal_service_id,
195+ service_relation.relation_name,
196+ service_relation.relation_role)
197+ yield self._retry_topology_change(add_relation)
198+
199+ returnValue((RelationState(self._client, relation_id), services))
200+
201+ @inlineCallbacks
202+ def _add_relation_state(self, relation_type):
203 path = yield self._client.create(
204 "/relations/relation-", flags=zookeeper.SEQUENCE)
205-
206 internal_id = basename(path)
207-
208 # create the settings container, for individual units settings.
209 yield self._client.create(path + "/settings")
210-
211- # Not clear if we really need to update the topology till the relation
212- # is in use. Topology updates are broadcasting to most connected
213- # clients. Perhaps a separate batch api would be useful to minimize
214- # broadcast events on the topology.
215- def add_relation(topology):
216- topology.add_relation(internal_id, relation_type)
217- yield self._retry_topology_change(add_relation)
218-
219- returnValue(RelationState(self._client, internal_id))
220+ returnValue(internal_id)
221+
222+ @inlineCallbacks
223+ def _add_service_relation_state(
224+ self, relation_id, service_id, endpoint):
225+ """Add a service relation state.
226+ """
227+ # Add service container in relation.
228+ node_data = yaml.safe_dump(
229+ {"name": endpoint.relation_name, "role": endpoint.relation_role})
230+ path = "/relations/%s/%s" % (relation_id, endpoint.relation_role)
231+
232+ try:
233+ yield self._client.create(path, node_data)
234+ except zookeeper.NodeExistsException:
235+ # If its not, then update the node, and continue.
236+ yield retry_change(
237+ self._client, path, lambda content, stat: node_data)
238
239 @inlineCallbacks
240 def remove_relation_state(self, relation_state):
241@@ -86,89 +156,6 @@
242 def internal_id(self):
243 return self._internal_id
244
245- @inlineCallbacks
246- def unassign_service(self, service_state):
247- """Remove a service from the relation.
248-
249- Any removed service needs to have its zookeeper state garbage collected
250- external to this api. The individual units should remove their
251- contained nodes, upon notification but even then the service container
252- node within the relation remains to be garbage collected.
253- """
254-
255- def remove_service(topology):
256- # Verify our relation and service still exist.
257- if not topology.has_relation(self._internal_id) or \
258- not topology.has_service(service_state.internal_id):
259- raise StateChanged()
260-
261- # Ignore concurrent requests that achieve the same state.
262- if not topology.relation_has_service(
263- self._internal_id, service_state.internal_id):
264- return
265-
266- topology.unassign_service_from_relation(
267- self._internal_id, service_state.internal_id)
268-
269- yield self._retry_topology_change(remove_service)
270-
271- @inlineCallbacks
272- def assign_service(self, service_state, relation_name, relation_role):
273- """Add a service to the relation.
274-
275- @param service_state - The service to be added to the relation.
276- @param name - The service's name for this relation.
277- @param role - The service's role within this relation.
278- """
279- # Add service container in relation.
280- node_data = yaml.safe_dump(
281- {"name": relation_name, "role": relation_role})
282- path = "/relations/%s/%s" % (self.internal_id, relation_role)
283-
284- try:
285- yield self._client.create(path, node_data)
286- except zookeeper.NodeExistsException:
287- # because we don't garbage collect, we need to ascertain
288- # if the service node is actually in use.
289- topology = yield self._read_topology()
290-
291- # If it is assigned, its an error.
292- # There's a small chance for a race condition between the check
293- # and set.
294- if topology.relation_has_service(
295- self._internal_id, service_state.internal_id):
296- raise ServiceRelationAlreadyAssigned(
297- service_state.service_name, relation_name, relation_role)
298-
299- # If its not, then update the node, and continue.
300- yield retry_change(
301- self._client, path, lambda content, stat: node_data)
302-
303- # Associate service to relation in the topology.
304- def add_service(topology):
305- # Verify our service and relation exists
306- if not topology.has_relation(self._internal_id) or \
307- not topology.has_service(service_state.internal_id):
308- raise StateChanged()
309-
310- # Add it the topology if its not there.
311- if not topology.relation_has_service(
312- self._internal_id, service_state.internal_id):
313- topology.assign_service_to_relation(
314- self.internal_id,
315- service_state.internal_id,
316- relation_name,
317- relation_role)
318-
319-
320- yield self._retry_topology_change(add_service)
321-
322- returnValue(ServiceRelationState(self._client,
323- service_state.internal_id,
324- self.internal_id,
325- relation_role,
326- relation_name))
327-
328
329 class ServiceRelationState(object):
330 """A state representative of a relation between one or more services."""
331@@ -695,4 +682,3 @@
332 """
333 return [unit_id for unit_id in units \
334 if unit_id != self._watcher_unit.internal_unit_id]
335-
336
337=== modified file 'ensemble/state/service.py'
338--- ensemble/state/service.py 2010-12-10 04:59:54 +0000
339+++ ensemble/state/service.py 2010-12-10 23:06:27 +0000
340@@ -88,8 +88,11 @@
341 formula_id)
342 formula_metadata = yield formula_state.get_metadata()
343 endpoints = set()
344- for relation_role in ("peers", "provides", "requires"):
345- relations = getattr(formula_metadata, relation_role)
346+ relation_role_map = {
347+ "peer": "peers", "client": "requires", "server": "provides"}
348+ for relation_role in ("peer", "client", "server"):
349+ relations = getattr(
350+ formula_metadata, relation_role_map[relation_role])
351 if relations:
352 for relation_name, spec in relations.iteritems():
353 if (query_relation_name is None or
354
355=== modified file 'ensemble/state/tests/test_endpoint.py'
356--- ensemble/state/tests/test_endpoint.py 2010-11-25 05:28:27 +0000
357+++ ensemble/state/tests/test_endpoint.py 2010-12-10 23:06:27 +0000
358@@ -4,9 +4,9 @@
359
360 class RelationEndpointTest(TestCase):
361 def test_may_relate_to(self):
362- mysql_ep = RelationEndpoint("mysqldb", "mysql", "db", "provides")
363- blog_ep = RelationEndpoint("blog", "mysql", "mysql", "requires")
364- pg_ep = RelationEndpoint("postgres", "postgres", "db", "provides")
365+ mysql_ep = RelationEndpoint("mysqldb", "mysql", "db", "server")
366+ blog_ep = RelationEndpoint("blog", "mysql", "mysql", "client")
367+ pg_ep = RelationEndpoint("postgres", "postgres", "db", "server")
368
369 self.assertRaises(TypeError, mysql_ep.may_relate_to, 42)
370
371@@ -21,15 +21,15 @@
372 # antireflexive om relation_role -
373 # must be consumer AND provider or vice versa
374 self.assertFalse(blog_ep.may_relate_to(
375- RelationEndpoint("foo", "mysql", "db", "requires")))
376+ RelationEndpoint("foo", "mysql", "db", "client")))
377 self.assertFalse(mysql_ep.may_relate_to(
378- RelationEndpoint("foo", "mysql", "db", "provides")))
379+ RelationEndpoint("foo", "mysql", "db", "server")))
380
381- # irreflexive for provides/requires
382+ # irreflexive for server/client
383 self.assertFalse(mysql_ep.may_relate_to(mysql_ep))
384 self.assertFalse(blog_ep.may_relate_to(blog_ep))
385 self.assertFalse(pg_ep.may_relate_to(pg_ep))
386
387- # but reflexive for peers
388- riak_ep = RelationEndpoint("riak", "riak", "riak", "peers")
389+ # but reflexive for peer
390+ riak_ep = RelationEndpoint("riak", "riak", "riak", "peer")
391 self.assert_(riak_ep.may_relate_to(riak_ep))
392
393=== modified file 'ensemble/state/tests/test_errors.py'
394--- ensemble/state/tests/test_errors.py 2010-11-04 03:41:11 +0000
395+++ ensemble/state/tests/test_errors.py 2010-12-10 23:06:27 +0000
396@@ -1,12 +1,14 @@
397 from ensemble.lib.testing import TestCase
398
399+from ensemble.state.endpoint import RelationEndpoint
400 from ensemble.state.errors import (
401 EnsembleError, StateError, StateChanged, FormulaStateNotFound,
402 ServiceStateNotFound, ServiceUnitStateNotFound, MachineStateNotFound,
403 ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,
404 BadServiceStateName, EnvironmentStateNotFound,
405- ServiceRelationAlreadyAssigned, UnitRelationStateNotFound,
406- UnitRelationStateAlreadyAssigned, UnknownRelationRole)
407+ RelationAlreadyExists, UnitRelationStateNotFound,
408+ UnitRelationStateAlreadyAssigned, UnknownRelationRole,
409+ BadDescriptor, DuplicateEndpoints, IncompatibleEndpoints)
410
411
412 class StateErrorsTest(TestCase):
413@@ -76,13 +78,13 @@
414 self.assertEquals(str(error),
415 "Environment state was not found")
416
417- def test_service_relation_already_assigned(self):
418- error = ServiceRelationAlreadyAssigned(
419- "wordpress", "mysql", "client")
420+ def test_relation_already_exists(self):
421+ error = RelationAlreadyExists(
422+ RelationEndpoint("wordpress", "mysql", "mysql", "client"),
423+ RelationEndpoint("mysql", "mysql", "db", "server"))
424 self.assertIsStateError(error)
425- msg = ("Service 'wordpress' is already in relation with different "
426- "name 'mysql' or role 'client'.")
427- self.assertEquals(str(error), msg)
428+ self.assertTrue("wordpress" in str(error))
429+ self.assertTrue("mysql" in str(error))
430
431 def test_unit_relation_state_not_found(self):
432 error = UnitRelationStateNotFound(
433@@ -103,3 +105,24 @@
434 self.assertIsStateError(error)
435 msg = "Unknown relation role 'server2' for service 'service-name'"
436 self.assertEquals(str(error), msg)
437+
438+ def test_bad_descriptor(self):
439+ error = BadDescriptor("a:b:c")
440+ self.assertTrue(isinstance(error, EnsembleError))
441+ msg = "Bad descriptor: 'a:b:c'"
442+ self.assertEquals(str(error), msg)
443+
444+ def test_duplicate_endpoints(self):
445+ riak_ep = RelationEndpoint("riak", "riak", "ring", "peer")
446+ error = DuplicateEndpoints(riak_ep, riak_ep)
447+ self.assertIsStateError(error)
448+ self.assertTrue("riak" in str(error))
449+
450+ def test_incompatible_endpoints(self):
451+ error = IncompatibleEndpoints(
452+ RelationEndpoint("mysql", "mysql", "db", "server"),
453+ RelationEndpoint("riak", "riak", "ring", "peer"))
454+ self.assertIsStateError(error)
455+ self.assertTrue("mysql" in str(error))
456+ self.assertTrue("riak" in str(error))
457+
458
459=== modified file 'ensemble/state/tests/test_hook.py'
460--- ensemble/state/tests/test_hook.py 2010-12-03 00:13:01 +0000
461+++ ensemble/state/tests/test_hook.py 2010-12-10 23:06:27 +0000
462@@ -3,6 +3,7 @@
463 from twisted.internet.defer import inlineCallbacks
464 from ensemble.lib.testing import TestCase
465
466+from ensemble.state.endpoint import RelationEndpoint
467 from ensemble.state.hook import RelationChange, HookContext
468 from ensemble.state.errors import UnitRelationStateNotFound, StateError
469 from ensemble.state.tests.test_relation import RelationTestBase
470@@ -23,11 +24,14 @@
471 @inlineCallbacks
472 def setUp(self):
473 yield super(ExecutionContextTest, self).setUp()
474- self.mysql_states = yield self.add_relation_service_unit(
475- "mysql", "mysql", "", "server")
476- self.wordpress_states = yield self.add_relation_service_unit(
477- "mysql", "wordpress", "", "client",
478- relation_state=self.mysql_states["relation"])
479+ wordpress_ep = RelationEndpoint(
480+ "wordpress", "client-server", "", "client")
481+ mysql_ep = RelationEndpoint(
482+ "mysql", "client-server", "", "server")
483+ self.wordpress_states = yield self.add_relation_service_unit_from_endpoints(
484+ wordpress_ep, mysql_ep)
485+ self.mysql_states = yield self.add_opposite_service_unit(
486+ self.wordpress_states)
487 self.relation = self.mysql_states["relation"]
488
489 def get_execution_context(self, states, change_type, unit_name):
490
491=== modified file 'ensemble/state/tests/test_relation.py'
492--- ensemble/state/tests/test_relation.py 2010-12-07 00:22:20 +0000
493+++ ensemble/state/tests/test_relation.py 2010-12-10 23:06:27 +0000
494@@ -8,11 +8,12 @@
495 inlineCallbacks, returnValue, Deferred, fail, succeed)
496 from txzookeeper import ZookeeperClient
497
498+from ensemble.state.endpoint import RelationEndpoint
499 from ensemble.state.formula import FormulaStateManager
500 from ensemble.state.errors import (
501- StateChanged, ServiceRelationAlreadyAssigned,
502- UnitRelationStateNotFound, UnitRelationStateAlreadyAssigned,
503- UnknownRelationRole)
504+ DuplicateEndpoints, IncompatibleEndpoints, RelationAlreadyExists,
505+ StateChanged, UnitRelationStateAlreadyAssigned,
506+ UnitRelationStateNotFound, UnknownRelationRole)
507 from ensemble.state.relation import (
508 RelationStateManager, ServiceRelationState, UnitRelationState)
509 from ensemble.state.service import ServiceStateManager
510@@ -40,13 +41,95 @@
511
512 @inlineCallbacks
513 def add_relation(self, relation_type, *services):
514- relation_state = yield self.relation_manager.add_relation_state(
515- relation_type)
516+ """Support older tests that don't use `RelationEndpoint`s"""
517+ endpoints = []
518 for service_meta in services:
519 service_state, relation_name, relation_role = service_meta
520- yield relation_state.assign_service(
521- service_state, relation_name, relation_role)
522- returnValue(relation_state)
523+ endpoints.append(RelationEndpoint(
524+ service_state.service_name,
525+ relation_type,
526+ relation_name,
527+ relation_role))
528+ relation_state = yield self.relation_manager.add_relation_state(
529+ *endpoints)
530+ returnValue(relation_state[0])
531+
532+ @inlineCallbacks
533+ def add_relation_service_unit_from_endpoints(self, *endpoints):
534+ """Build the relation and add one service unit to the first endpoint.
535+
536+ This method is used to migrate older tests that would create
537+ the relation, assign one service, add a service unit, AND then
538+ assign a service. However, service assignment is now done all
539+ at once with the relation creation. Because we are interested
540+ in testing what happens with the changes to the service units,
541+ such tests remain valid.
542+
543+ Returns a dict to collect together the various state objects
544+ being created. This is created from the perspective of the
545+ first endpoint, but the states of all of the endpoints are
546+ also captured, so it can be worked with from the opposite
547+ endpoint, as seen in :func:`add_opposite_service_unit`.
548+ """
549+ # 1. Setup all service states
550+ service_states = []
551+ for endpoint in endpoints:
552+ service_state = yield self.add_service(endpoint.service_name)
553+ service_states.append(service_state)
554+
555+ # 2. And join together in a relation
556+ relation_state, service_relation_states = \
557+ yield self.relation_manager.add_relation_state(
558+ *endpoints)
559+
560+ # 3. Add a service unit to only the first endpoint - we need
561+ # to test what happens when service units are added to the
562+ # other service state (if any), so do so separately
563+ unit_state = yield service_states[0].add_unit_state()
564+ relation_unit_state = yield service_relation_states[0].add_unit_state(
565+ unit_state)
566+
567+ returnValue({
568+ "service": service_states[0],
569+ "services": service_states,
570+ "unit": unit_state,
571+ "relation": relation_state,
572+ "service_relation": service_relation_states[0],
573+ "unit_relation": relation_unit_state,
574+ "service_relations": service_relation_states})
575+
576+ @inlineCallbacks
577+ def add_opposite_service_unit(self, other_states):
578+ """Given `other_states`, add a service unit to the opposite endpoint.
579+
580+ Like :func:`add_relation_service_unit_from_endpoints`, this is
581+ used to support older tests. Although it's slightly awkward to
582+ use because of attempt to be backwards compatible, it does
583+ enable the testing of a typical case: we are now bringing
584+ online a service unit on the opposite side of a relation
585+ endpoint pairing.
586+
587+ TODO: there's probably a better name for this method.
588+ """
589+
590+ assert len(other_states["services"]) == 2
591+ unit_state = yield other_states["services"][1].add_unit_state()
592+ relation_unit_state = yield other_states["service_relations"][1].\
593+ add_unit_state(unit_state)
594+
595+ def rotate(X):
596+ rotated = X[1:]
597+ rotated.append(X[0])
598+ return rotated
599+
600+ returnValue({
601+ "service": other_states["services"][1],
602+ "services": rotate(other_states["services"]),
603+ "unit": unit_state,
604+ "relation": other_states["relation"],
605+ "service_relation": other_states["service_relations"][1],
606+ "unit_relation": relation_unit_state,
607+ "service_relations": rotate(other_states["service_relations"])})
608
609 @inlineCallbacks
610 def add_relation_service_unit(self,
611@@ -54,7 +137,6 @@
612 service_name,
613 relation_name="name",
614 relation_role="role",
615- relation_state=None,
616 client=None):
617 """
618 Create a relation, service, and service unit, with the
619@@ -63,21 +145,11 @@
620 the service relation and the service relation state will
621 utilize that as their zookeeper client.
622 """
623- # Add the service
624+ # Add the service, relation, unit states
625 service_state = yield self.add_service(service_name)
626-
627- if relation_state is None:
628- # If no relation is passed, create the relation
629- # and associate the service to it.
630- relation_state = yield self.add_relation(
631- relation_type,
632- (service_state, relation_name, relation_role))
633- else:
634- # If the relation exists, assign the service to it.
635- yield relation_state.assign_service(
636- service_state, relation_name, relation_role)
637-
638- # add a unit of the service
639+ relation_state = yield self.add_relation(
640+ relation_type,
641+ (service_state, relation_name, relation_role))
642 unit_state = yield service_state.add_unit_state()
643
644 # Get the service relation.
645@@ -132,11 +204,11 @@
646
647 @inlineCallbacks
648 def test_add_relation_state(self):
649- """
650- Adding a relation will create a relation node and update the topology.
651- """
652- relation_state = yield self.relation_manager.add_relation_state(
653- "mysql")
654+ """Adding a relation will create a relation node and update topology."""
655+ mysql_ep = RelationEndpoint("mysql", "mysql", "db", "server")
656+ yield self.add_service("mysql")
657+ relation_state = (yield self.relation_manager.add_relation_state(
658+ mysql_ep))[0]
659 topology = yield self.get_topology()
660 self.assertTrue(topology.has_relation(relation_state.internal_id))
661 exists = yield self.client.exists(
662@@ -147,12 +219,118 @@
663 self.assertTrue(exists)
664
665 @inlineCallbacks
666+ def test_add_relation_state_to_missing_service(self):
667+ """Test adding a relation to a nonexistent service"""
668+ mysql_ep = RelationEndpoint("mysql", "mysql", "db", "server")
669+ blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
670+ yield self.add_service("mysql")
671+ # but didn't create the service for wordpress
672+ yield self.assertFailure(
673+ self.relation_manager.add_relation_state(
674+ mysql_ep, blog_ep),
675+ StateChanged)
676+
677+ @inlineCallbacks
678+ def test_add_relation_state_bad_relation_role(self):
679+ """Test adding a relation with a bad role when is one is well defined (client or server)"""
680+ blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
681+ mysql_ep = RelationEndpoint("mysql", "mysql", "db", "server")
682+ bad_mysql_ep = RelationEndpoint("mysql", "mysql", "db", "bad-server-role")
683+ bad_blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "bad-client-role")
684+ yield self.add_service("mysql")
685+ yield self.add_service("wordpress")
686+ yield self.assertFailure(
687+ self.relation_manager.add_relation_state(
688+ bad_mysql_ep, blog_ep),
689+ IncompatibleEndpoints)
690+ yield self.assertFailure(
691+ self.relation_manager.add_relation_state(
692+ bad_blog_ep, mysql_ep),
693+ IncompatibleEndpoints)
694+ # TODO in future branch referenced in relation, also test
695+ # bad_blog_ep *and* bad_mysql_ep
696+
697+ @inlineCallbacks
698+ def test_add_binary_relation_state_twice(self):
699+ """Test adding the same relation twice"""
700+ blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
701+ mysql_ep = RelationEndpoint("mysql", "mysql", "db", "server")
702+ yield self.add_service("mysql")
703+ yield self.add_service("wordpress")
704+ yield self.relation_manager.add_relation_state(mysql_ep, blog_ep)
705+ yield self.assertFailure(
706+ self.relation_manager.add_relation_state(blog_ep, mysql_ep),
707+ RelationAlreadyExists)
708+ yield self.assertFailure(
709+ self.relation_manager.add_relation_state(mysql_ep, blog_ep),
710+ RelationAlreadyExists)
711+
712+ @inlineCallbacks
713+ def test_add_peer_relation_state_twice(self):
714+ """Test adding the same relation twice"""
715+ riak_ep = RelationEndpoint("riak", "riak", "ring", "peer")
716+ yield self.add_service("riak")
717+ yield self.relation_manager.add_relation_state(riak_ep)
718+ yield self.assertFailure(
719+ self.relation_manager.add_relation_state(riak_ep),
720+ RelationAlreadyExists)
721+
722+ @inlineCallbacks
723+ def test_add_relation_state_no_endpoints(self):
724+ """Test adding a relation with no endpoints (no longer allowed)"""
725+ yield self.assertFailure(
726+ self.relation_manager.add_relation_state(),
727+ TypeError)
728+
729+ @inlineCallbacks
730+ def test_add_relation_state_relation_type_unshared(self):
731+ """Test adding a relation with endpoints not sharing a relation type"""
732+ pg_ep = RelationEndpoint("pg", "postgres", "db", "server")
733+ blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
734+ yield self.assertFailure(
735+ self.relation_manager.add_relation_state(pg_ep, blog_ep),
736+ IncompatibleEndpoints)
737+
738+ @inlineCallbacks
739+ def test_add_relation_state_too_many_endpoints(self):
740+ """Test adding a relation between too many endpoints (> 2)"""
741+ mysql_ep = RelationEndpoint("mysql", "mysql", "db", "server")
742+ blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
743+ yield self.add_service("mysql")
744+ yield self.add_service("wordpress")
745+ yield self.assertFailure(
746+ self.relation_manager.add_relation_state(
747+ mysql_ep, blog_ep, mysql_ep),
748+ TypeError)
749+
750+ @inlineCallbacks
751+ def test_add_relation_state_duplicate_peer_endpoints(self):
752+ """Test adding a relation between duplicate peer endpoints"""
753+ riak_ep = RelationEndpoint("riak", "riak", "ring", "peer")
754+ yield self.add_service("riak")
755+ yield self.assertFailure(
756+ self.relation_manager.add_relation_state(riak_ep, riak_ep),
757+ DuplicateEndpoints)
758+
759+ @inlineCallbacks
760+ def test_add_relation_state_endpoints_duplicate_role(self):
761+ """Test adding a relation with services overlapped by duplicate role"""
762+ mysql_ep = RelationEndpoint("mysql", "mysql", "db", "server")
763+ drizzle_ep = RelationEndpoint("drizzle", "mysql", "db", "server")
764+ yield self.add_service("mysql")
765+ yield self.add_service("drizzle")
766+ yield self.assertFailure(
767+ self.relation_manager.add_relation_state(mysql_ep, drizzle_ep),
768+ IncompatibleEndpoints)
769+
770+ @inlineCallbacks
771 def test_remove_relation_state(self):
772- """Removing a relation will remove it from the topology.
773- """
774+ """Removing a relation will remove it from the topology."""
775 # Simulate add and remove.
776- relation_state = yield self.relation_manager.add_relation_state(
777- "varnish")
778+ varnish_ep = RelationEndpoint("varnish", "webcache", "cache", "server")
779+ yield self.add_service("varnish")
780+ relation_state = (yield self.relation_manager.add_relation_state(
781+ varnish_ep))[0]
782 topology = yield self.get_topology()
783 self.assertTrue(topology.has_relation(relation_state.internal_id))
784 yield self.relation_manager.remove_relation_state(relation_state)
785@@ -164,8 +342,10 @@
786 @inlineCallbacks
787 def test_remove_relation_with_changing_state(self):
788 # Simulate add and remove.
789- relation_state = yield self.relation_manager.add_relation_state(
790- "varnish")
791+ varnish_ep = RelationEndpoint("varnish", "webcache", "cache", "server")
792+ yield self.add_service("varnish")
793+ relation_state = (yield self.relation_manager.add_relation_state(
794+ varnish_ep))[0]
795 topology = yield self.get_topology()
796 self.assertTrue(topology.has_relation(relation_state.internal_id))
797 yield self.relation_manager.remove_relation_state(relation_state)
798@@ -218,8 +398,8 @@
799 def setUp(self):
800 yield super(RelationStateTest, self).setUp()
801
802- self.relation_state = yield self.relation_manager.add_relation_state(
803- "rel-type")
804+ self.relation_state = (yield self.relation_manager.add_relation_state(
805+ "rel-type"))[0]
806 self.formula_state = yield self.formula_manager.add_formula_state(
807 "namespace", self.formula)
808 self.service_state1 = yield self.service_manager.add_service_state(
809@@ -227,106 +407,6 @@
810 self.service_state2 = yield self.service_manager.add_service_state(
811 "wordpress-dev", self.formula_state)
812
813- @inlineCallbacks
814- def test_assign_service(self):
815- yield self.relation_state.assign_service(
816- self.service_state1,
817- "dev-instance",
818- "prod")
819-
820- yield self.relation_state.assign_service(
821- self.service_state2,
822- "prod-instance",
823- "dev")
824-
825- topology = yield self.get_topology()
826-
827- self.assertEqual(
828- topology.get_relations_for_service(
829- self.service_state1.internal_id),
830- [(self.relation_state.internal_id,
831- "rel-type",
832- {"name": "dev-instance", "role": "prod"})])
833-
834- self.assertEqual(
835- topology.get_relation_services(self.relation_state.internal_id),
836- {'service-0000000000': {'name': 'dev-instance', 'role': 'prod'},
837- 'service-0000000001': {'name': 'prod-instance', 'role': 'dev'}})
838-
839- @inlineCallbacks
840- def test_assign_service_already_assigned(self):
841- """
842- Attempting to add a service to a relation multiple times
843- will raise an error if its already there.
844- """
845- # Assign and verify
846- yield self.relation_state.assign_service(self.service_state1,
847- "dev-instance",
848- "prod")
849- topology = yield self.get_topology()
850- self.assertTrue(topology.relation_has_service(
851- self.relation_state.internal_id, self.service_state1.internal_id))
852-
853- # If we attempt to add again, we'll get an error.
854- yield self.assertFailure(
855- self.relation_state.assign_service(self.service_state1,
856- "dev-instance",
857- "prod"),
858- ServiceRelationAlreadyAssigned)
859-
860- # If we unassign and assign again, the service container state updates.
861- yield self.relation_state.unassign_service(self.service_state1)
862- yield self.relation_state.assign_service(self.service_state1,
863- "dev-instance2",
864- "prod")
865- content, stat = yield self.client.get(
866- "/relations/%s/%s" % (
867- self.relation_state.internal_id, "prod"))
868-
869- node_data = yaml.load(content)
870- self.assertEqual(node_data["name"], "dev-instance2")
871-
872- # Attempting to assign when the relation or service has gone away
873- # raises a state changed error.
874- yield self.relation_manager.remove_relation_state(self.relation_state)
875- topology = yield self.get_topology()
876- self.assertFalse(
877- topology.has_relation(self.relation_state.internal_id))
878-
879- yield self.assertFailure(
880- self.relation_state.assign_service(self.service_state1,
881- "dev-instance2",
882- "prod2"),
883- StateChanged)
884-
885- @inlineCallbacks
886- def test_unassign_service(self):
887- """A service can be disassociated from a relation.
888- """
889- yield self.relation_state.assign_service(self.service_state1,
890- "dev-instance",
891- "prod")
892- topology = yield self.get_topology()
893- self.assertTrue(topology.relation_has_service(
894- self.relation_state.internal_id,
895- self.service_state1.internal_id))
896-
897- # Remove and verify
898- yield self.relation_state.unassign_service(self.service_state1)
899- topology = yield self.get_topology()
900- self.assertFalse(topology.relation_has_service(
901- self.relation_state.internal_id,
902- self.service_state1.internal_id))
903-
904- # if we remove again, we'll succed as the final state is the same.
905- yield self.relation_state.unassign_service(self.service_state1)
906-
907- # Removing again, without the relation state, errors on state change.
908- yield self.relation_manager.remove_relation_state(self.relation_state)
909- yield self.assertFailure(
910- self.relation_state.unassign_service(self.service_state1),
911- StateChanged)
912-
913
914 class ServiceRelationStateTest(RelationTestBase):
915
916@@ -596,53 +676,16 @@
917 self.assertEqual(path, container_path)
918
919 @inlineCallbacks
920- def test_watch_start_existing_service(self):
921- """Invoking watcher.start returns a deferred that only fires
922- after watch on the container is in place. In the case of an
923- existing service, this is after a child watch is established.
924- """
925- wordpress_states = yield self.add_relation_service_unit(
926- "client-server", "wordpress", "", "client")
927-
928- mysql_states = yield self.add_relation_service_unit(
929- "client-server", "mysql", "", "server",
930- relation_state = wordpress_states["relation"])
931-
932- results = []
933- wait_callback = [Deferred() for i in range(5)]
934-
935- def watch_related(old_units=None, new_units=None, modified=None):
936- results.append((old_units, new_units, modified))
937- wait_callback[len(results)-1].callback(True)
938-
939- def invoked(*args, **kw):
940- # sleep to make sure that things haven't fired till the watch is
941- # in place.
942- time.sleep(0.1)
943- self.assertFalse(results)
944-
945- mock_client = self.mocker.patch(self.client)
946- mock_client.get_children_and_watch("/relations/%s/server" % (
947- wordpress_states["relation"].internal_id))
948-
949- self.mocker.call(invoked)
950- self.mocker.passthrough()
951- self.mocker.replay()
952-
953- watcher = yield wordpress_states["unit_relation"].watch_related_units(
954- watch_related)
955- yield watcher.start()
956- yield wait_callback[0]
957-
958- @inlineCallbacks
959 def test_watch_start_new_service(self):
960 """Invoking watcher.start returns a deferred that only fires
961 after watch on the containr is in place. In the case of a new
962 service this after an existance watch is established on the
963 container.
964 """
965- wordpress_states = yield self.add_relation_service_unit(
966- "client-server", "wordpress", "", "client")
967+ wordpress_ep = RelationEndpoint(
968+ "wordpress", "client-server", "", "client")
969+ wordpress_states = yield self.add_relation_service_unit_from_endpoints(
970+ wordpress_ep)
971
972 results = []
973
974@@ -673,8 +716,12 @@
975 first within relation, and start to monitor the server service
976 as it joins the relation, adds a unit, modifies, the unit.
977 """
978- wordpress_states = yield self.add_relation_service_unit(
979- "client-server", "wordpress", "", "client")
980+ wordpress_ep = RelationEndpoint(
981+ "wordpress", "client-server", "", "client")
982+ mysql_ep = RelationEndpoint(
983+ "mysql", "client-server", "", "server")
984+ wordpress_states = yield self.add_relation_service_unit_from_endpoints(
985+ wordpress_ep, mysql_ep)
986
987 # setup watch callbacks, and start watching.
988 wait_callback = [Deferred() for i in range(5)]
989@@ -700,9 +747,8 @@
990 self.assertFalse(results)
991
992 # add the server service and a unit of that
993- mysql_states = yield self.add_relation_service_unit(
994- "client-server", "mysql", "", "server",
995- relation_state=wordpress_states["relation"])
996+ mysql_states = yield self.add_opposite_service_unit(
997+ wordpress_states)
998
999 topology = yield self.get_topology()
1000 # assert the relation is established correctly
1001@@ -734,17 +780,18 @@
1002 @inlineCallbacks
1003 def test_watch_client_server_with_existing_service(self):
1004 """We simulate a scenario where the client and server are both
1005- in place before the client begins observing. The server subequently,
1006+ in place before the client begins observing. The server subsequently
1007 modifies, and remove its unit from the relation.
1008 """
1009 # add the client service and a unit of that
1010- wordpress_states = yield self.add_relation_service_unit(
1011- "client-server", "wordpress", "", "client")
1012-
1013- # add the server service and a unit of that
1014- mysql_states = yield self.add_relation_service_unit(
1015- "client-server", "mysql", "", "server",
1016- relation_state=wordpress_states["relation"])
1017+ wordpress_ep = RelationEndpoint(
1018+ "wordpress", "client-server", "", "client")
1019+ mysql_ep = RelationEndpoint(
1020+ "mysql", "client-server", "", "server")
1021+ wordpress_states = yield self.add_relation_service_unit_from_endpoints(
1022+ wordpress_ep, mysql_ep)
1023+ mysql_states = yield self.add_opposite_service_unit(
1024+ wordpress_states)
1025
1026 # setup callbacks and start observing.
1027 wait_callback = [Deferred() for i in range(5)]
1028@@ -784,9 +831,13 @@
1029 def test_watch_server_client_with_new_service(self):
1030 """We simulate a server watching a client.
1031 """
1032+ wordpress_ep = RelationEndpoint(
1033+ "wordpress", "client-server", "", "client")
1034+ mysql_ep = RelationEndpoint(
1035+ "mysql", "client-server", "", "server")
1036 # add the server service and a unit of that
1037- mysql_states = yield self.add_relation_service_unit(
1038- "client-server", "mysql", "", "server")
1039+ mysql_states = yield self.add_relation_service_unit_from_endpoints(
1040+ mysql_ep, wordpress_ep)
1041
1042 # setup callbacks and start observing.
1043 wait_callback = [Deferred() for i in range(5)]
1044@@ -804,9 +855,8 @@
1045 self.assertFalse(results)
1046
1047 # add the client service and a unit of that
1048- wordpress_states = yield self.add_relation_service_unit(
1049- "client-server", "wordpress", "", "client",
1050- relation_state=mysql_states["relation"])
1051+ wordpress_states = yield self.add_opposite_service_unit(
1052+ mysql_states)
1053
1054 yield wait_callback[0]
1055 self.verify_unit_watch_result(
1056@@ -817,10 +867,9 @@
1057 """Peer relations always watch the peer container.
1058 """
1059 # add the peer relation and two unit of the service.
1060- relation_state = yield self.relation_manager.add_relation_state("peer")
1061- riak_states = yield self.add_relation_service_unit(
1062- "peer", "riak", "riak-db", "peer",
1063- relation_state=relation_state)
1064+ riak_ep = RelationEndpoint("riak", "peer", "riak-db", "peer")
1065+ riak_states = yield self.add_relation_service_unit_from_endpoints(
1066+ riak_ep)
1067
1068 riak2_unit = yield riak_states["service"].add_unit_state()
1069 yield riak_states["service_relation"].add_unit_state(riak2_unit)
1070@@ -878,13 +927,15 @@
1071 created container is detected correctly and the observation
1072 works immediately.
1073 """
1074- # Add the server service and a service unit.
1075- mysql_states = yield self.add_relation_service_unit(
1076- "client-server", "mysql", "", "server")
1077-
1078- wordpress_states = yield self.add_relation_service_unit(
1079- "client-server", "wordpress", "", "client",
1080- relation_state=mysql_states["relation"])
1081+ # Add the relation, services, and related units.
1082+ wordpress_ep = RelationEndpoint(
1083+ "wordpress", "client-server", "", "client")
1084+ mysql_ep = RelationEndpoint(
1085+ "mysql", "client-server", "", "server")
1086+ wordpress_states = yield self.add_relation_service_unit_from_endpoints(
1087+ wordpress_ep, mysql_ep)
1088+ mysql_states = yield self.add_opposite_service_unit(
1089+ wordpress_states)
1090
1091 container_path = "/relations/%s/client" % (
1092 mysql_states["relation"].internal_id)
1093@@ -929,12 +980,14 @@
1094 notification.
1095 """
1096 # Add the relation, services, and related units.
1097- mysql_states = yield self.add_relation_service_unit(
1098- "client-server", "mysql", "", "server")
1099-
1100- wordpress_states = yield self.add_relation_service_unit(
1101- "client-server", "wordpress", "", "client",
1102- relation_state=mysql_states["relation"])
1103+ wordpress_ep = RelationEndpoint(
1104+ "wordpress", "client-server", "", "client")
1105+ mysql_ep = RelationEndpoint(
1106+ "mysql", "client-server", "", "server")
1107+ wordpress_states = yield self.add_relation_service_unit_from_endpoints(
1108+ wordpress_ep, mysql_ep)
1109+ mysql_states = yield self.add_opposite_service_unit(
1110+ wordpress_states)
1111
1112 # setup callbacks and start observing.
1113 wait_callback = [Deferred() for i in range(5)]
1114@@ -981,12 +1034,14 @@
1115 the unit is being observed, the watcher will ignore the deletion.
1116 """
1117 # Add the relation, services, and related units.
1118- mysql_states = yield self.add_relation_service_unit(
1119- "client-server", "mysql", "", "server")
1120-
1121- wordpress_states = yield self.add_relation_service_unit(
1122- "client-server", "wordpress", "", "client",
1123- relation_state=mysql_states["relation"])
1124+ wordpress_ep = RelationEndpoint(
1125+ "wordpress", "client-server", "", "client")
1126+ mysql_ep = RelationEndpoint(
1127+ "mysql", "client-server", "", "server")
1128+ wordpress_states = yield self.add_relation_service_unit_from_endpoints(
1129+ wordpress_ep, mysql_ep)
1130+ mysql_states = yield self.add_opposite_service_unit(
1131+ wordpress_states)
1132
1133 # setup callbacks and start observing.
1134 wait_callback = [Deferred() for i in range(5)]
1135@@ -1039,12 +1094,14 @@
1136 without additional callbacks.
1137 """
1138 # Add the relation, services, and related units.
1139- mysql_states = yield self.add_relation_service_unit(
1140- "client-server", "mysql", "", "server")
1141-
1142- wordpress_states = yield self.add_relation_service_unit(
1143- "client-server", "wordpress", "", "client",
1144- relation_state=mysql_states["relation"])
1145+ wordpress_ep = RelationEndpoint(
1146+ "wordpress", "client-server", "", "client")
1147+ mysql_ep = RelationEndpoint(
1148+ "mysql", "client-server", "", "server")
1149+ wordpress_states = yield self.add_relation_service_unit_from_endpoints(
1150+ wordpress_ep, mysql_ep)
1151+ mysql_states = yield self.add_opposite_service_unit(
1152+ wordpress_states)
1153
1154 # setup callbacks and start observing.
1155 wait_callback = [Deferred() for i in range(5)]
1156@@ -1099,8 +1156,12 @@
1157 without additional callbacks.
1158 """
1159 # Add the relation, services, and related units.
1160- mysql_states = yield self.add_relation_service_unit(
1161- "client-server", "mysql", "", "server")
1162+ wordpress_ep = RelationEndpoint(
1163+ "wordpress", "client-server", "", "client")
1164+ mysql_ep = RelationEndpoint(
1165+ "mysql", "client-server", "", "server")
1166+ mysql_states = yield self.add_relation_service_unit_from_endpoints(
1167+ mysql_ep, wordpress_ep)
1168
1169 # setup callbacks and start observing.
1170 wait_callback = [Deferred() for i in range(5)]
1171@@ -1119,9 +1180,8 @@
1172 watcher.stop()
1173
1174 # Add the new service and a unit
1175- wordpress_states = yield self.add_relation_service_unit(
1176- "client-server", "wordpress", "", "client",
1177- relation_state=mysql_states["relation"])
1178+ wordpress_states = yield self.add_opposite_service_unit(
1179+ mysql_states)
1180
1181 # Add another unit
1182 wordpress2_states = yield self.add_related_service_unit(
1183@@ -1314,12 +1374,14 @@
1184 watcher_callback = ParallelWatcherCallback()
1185
1186 # Add the relation, services, and related units.
1187- mysql_states = yield self.add_relation_service_unit(
1188- "client-server", "mysql", "", "server")
1189-
1190- wordpress_states = yield self.add_relation_service_unit(
1191- "client-server", "wordpress", "", "client",
1192- relation_state=mysql_states["relation"])
1193+ wordpress_ep = RelationEndpoint(
1194+ "wordpress", "client-server", "", "client")
1195+ mysql_ep = RelationEndpoint(
1196+ "mysql", "client-server", "", "server")
1197+ wordpress_states = yield self.add_relation_service_unit_from_endpoints(
1198+ wordpress_ep, mysql_ep)
1199+ mysql_states = yield self.add_opposite_service_unit(
1200+ wordpress_states)
1201
1202 # Start watching, and give a moment to establish
1203 watcher = yield mysql_states["unit_relation"].watch_related_units(
1204
1205=== modified file 'ensemble/state/tests/test_service.py'
1206--- ensemble/state/tests/test_service.py 2010-12-10 04:59:54 +0000
1207+++ ensemble/state/tests/test_service.py 2010-12-10 23:06:27 +0000
1208@@ -27,8 +27,8 @@
1209 self.formula_state_manager = FormulaStateManager(self.client)
1210 self.service_state_manager = ServiceStateManager(self.client)
1211 self.machine_state_manager = MachineStateManager(self.client)
1212- self.formula_state = yield self.formula_state_manager.add_formula_state(
1213- "namespace", self.formula)
1214+ self.formula_state = yield self.formula_state_manager.\
1215+ add_formula_state("namespace", self.formula)
1216 self.relation_state_manager = RelationStateManager(self.client)
1217
1218 @inlineCallbacks
1219@@ -39,13 +39,17 @@
1220
1221 @inlineCallbacks
1222 def add_relation(self, relation_type, *services):
1223- relation_state = yield self.relation_state_manager.add_relation_state(
1224- relation_type)
1225+ endpoints = []
1226 for service_meta in services:
1227 service_state, relation_name, relation_role = service_meta
1228- yield relation_state.assign_service(
1229- service_state, relation_name, relation_role)
1230- returnValue(relation_state)
1231+ endpoints.append(RelationEndpoint(
1232+ service_state.service_name,
1233+ relation_type,
1234+ relation_name,
1235+ relation_role))
1236+ relation_state = yield self.relation_state_manager.add_relation_state(
1237+ *endpoints)
1238+ returnValue(relation_state[0])
1239
1240 @inlineCallbacks
1241 def remove_service(self, internal_service_id):
1242@@ -435,55 +439,12 @@
1243 yield self.assertFailure(d, StateChanged)
1244
1245 @inlineCallbacks
1246- def test_watch_relations_with_changing_topology(self):
1247- """
1248- If the topology changes in an unrelated way, the
1249- relations watch callback should not be called.
1250- """
1251- service_state = yield self.add_service("wordpress")
1252- relation_state = yield self.add_relation("rel-type")
1253-
1254- wait_callback = [Deferred() for i in range(5)]
1255- calls = []
1256-
1257- def watch_relations(old_relations, new_relations):
1258- calls.append((old_relations, new_relations))
1259- wait_callback[len(calls)-1].callback(True)
1260-
1261- # Start watching
1262- service_state.watch_relation_states(watch_relations)
1263-
1264- # Callback is still untouched.
1265- self.assertEquals(calls, [])
1266-
1267- # assign service to relation, and wait for callback
1268- yield relation_state.assign_service(service_state, "name", "role")
1269- yield wait_callback[0]
1270- yield self.sleep(0.1)
1271- self.assertEqual(len(calls), 2)
1272- self.assertEquals(calls[0], (None, []))
1273- self.assertEquals(calls[1][0], [])
1274- self.assertEquals(
1275- calls[1][1][0].internal_service_id, service_state.internal_id)
1276- self.assertEquals(
1277- calls[1][1][0].internal_relation_id, relation_state.internal_id)
1278-
1279- # Change the topology in an unrelated way, and give it some time
1280- # in case a callback happens.
1281- yield self.add_service("s-1")
1282- yield self.add_relation("r-2-type")
1283- yield self.sleep(0.2)
1284-
1285- # Assert no changes observed.
1286- self.assertEquals(len(calls), 2)
1287-
1288- @inlineCallbacks
1289 def test_watch_relations_when_being_created(self):
1290 """
1291 We can watch relations before we have any.
1292 """
1293 service_state = yield self.add_service("wordpress")
1294- relation_state = yield self.add_relation("rel-type")
1295+
1296
1297 wait_callback = [Deferred() for i in range(5)]
1298 calls = []
1299@@ -499,10 +460,11 @@
1300 self.assertEquals(calls, [])
1301
1302 # add a service relation and wait for the callback
1303- yield relation_state.assign_service(service_state, "name", "role")
1304+ relation_state = yield self.add_relation(
1305+ "rel-type", [service_state, "name", "role"])
1306 yield wait_callback[1]
1307
1308- # verify the result
1309+ # # verify the result
1310 self.assertEquals(len(calls), 2)
1311 old_relations, new_relations = calls[1]
1312 self.assertFalse(old_relations)
1313@@ -511,7 +473,6 @@
1314 # add a new relation with the service assigned to it.
1315 relation_state2 = yield self.add_relation(
1316 "rel-type2", [service_state, "app", "server"])
1317-
1318 yield wait_callback[2]
1319
1320 self.assertEquals(len(calls), 3)
1321@@ -555,7 +516,7 @@
1322
1323 # Assign to another relation.
1324 yield self.add_relation("rel-type",
1325- (service_state, "name", "role"))
1326+ (service_state, "name2", "role"))
1327
1328 # Give a chance for something bad to happen.
1329 yield self.sleep(0.3)
1330@@ -573,61 +534,6 @@
1331 self.assertEquals(len(calls), 2)
1332
1333 @inlineCallbacks
1334- def test_watch_relation_assign_and_unassign_at_once(self):
1335- """
1336- If a service is quickly assigned and unassigned to a relation, the
1337- change maybe be observed, as a single modificaiton in state.
1338- """
1339- wait_callback = [Deferred() for i in range(10)]
1340-
1341- calls = []
1342-
1343- def watch_units(old_units, new_units):
1344- calls.append((old_units, new_units))
1345- wait_callback[len(calls)-1].callback(True)
1346-
1347- # Create the service
1348- service_state = yield self.add_service("s-1")
1349-
1350- # Create a relation with the service assigned.
1351- relation_state1 = yield self.add_relation(
1352- "rel-type", (service_state, "name", "role"))
1353- relation_state2 = yield self.add_relation("rel-type")
1354-
1355- # Start watching
1356- yield service_state.watch_relation_states(watch_units)
1357-
1358- yield wait_callback[0]
1359- self.assertEquals(len(calls), 1)
1360- old_relations, new_relations = calls[0]
1361- self.assertEqual(old_relations, None)
1362-
1363- # Modify the topology by hand so we get notification of multiple
1364- # things simultaneously.
1365- topology = yield self.get_topology()
1366- topology.unassign_service_from_relation(
1367- relation_state1.internal_id,
1368- service_state.internal_id)
1369-
1370- topology.assign_service_to_relation(
1371- relation_state2.internal_id,
1372- service_state.internal_id,
1373- "role", "name")
1374-
1375- yield self.set_topology(topology)
1376-
1377- yield wait_callback[1]
1378- self.assertEqual(len(calls), 2)
1379- old_relations, new_relations = calls[1]
1380-
1381- # Verify the changes seen.
1382- self.assertEqual([r.internal_relation_id for r in old_relations],
1383- [relation_state1.internal_id])
1384-
1385- self.assertEqual([r.internal_relation_id for r in new_relations],
1386- [relation_state2.internal_id])
1387-
1388- @inlineCallbacks
1389 def add_service_from_formula(self, service_name):
1390 formula_dir = FormulaDirectory(os.path.join(
1391 test_repository_path, service_name))
1392@@ -644,17 +550,17 @@
1393 self.assertEqual(
1394 (yield self.service_state_manager.get_relation_endpoints(
1395 "wordpress")),
1396- set([RelationEndpoint("wordpress", "http", "url", "provides"),
1397- RelationEndpoint("wordpress", "mysql", "db", "requires"),
1398+ set([RelationEndpoint("wordpress", "http", "url", "server"),
1399+ RelationEndpoint("wordpress", "mysql", "db", "client"),
1400 RelationEndpoint(
1401- "wordpress", "varnish", "cache", "requires")]))
1402+ "wordpress", "varnish", "cache", "client")]))
1403 yield self.add_service_from_formula("riak")
1404 self.assertEqual(
1405 (yield self.service_state_manager.get_relation_endpoints(
1406 "riak")),
1407- set([RelationEndpoint("riak", "http", "admin", "provides"),
1408- RelationEndpoint("riak", "http", "endpoint", "provides"),
1409- RelationEndpoint("riak", "riak", "ring", "peers")]))
1410+ set([RelationEndpoint("riak", "http", "admin", "server"),
1411+ RelationEndpoint("riak", "http", "endpoint", "server"),
1412+ RelationEndpoint("riak", "riak", "ring", "peer")]))
1413
1414 @inlineCallbacks
1415 def test_get_relation_endpoints_service_name_relation_name(self):
1416@@ -663,21 +569,21 @@
1417 self.assertEqual(
1418 (yield self.service_state_manager.get_relation_endpoints(
1419 "wordpress:url")),
1420- set([RelationEndpoint("wordpress", "http", "url", "provides")]))
1421+ set([RelationEndpoint("wordpress", "http", "url", "server")]))
1422 self.assertEqual(
1423 (yield self.service_state_manager.get_relation_endpoints(
1424 "wordpress:db")),
1425- set([RelationEndpoint("wordpress", "mysql", "db", "requires")]))
1426+ set([RelationEndpoint("wordpress", "mysql", "db", "client")]))
1427 self.assertEqual(
1428 (yield self.service_state_manager.get_relation_endpoints(
1429 "wordpress:cache")),
1430 set([RelationEndpoint(
1431- "wordpress", "varnish", "cache", "requires")]))
1432+ "wordpress", "varnish", "cache", "client")]))
1433 yield self.add_service_from_formula("riak")
1434 self.assertEqual(
1435 (yield self.service_state_manager.get_relation_endpoints(
1436 "riak:ring")),
1437- set([RelationEndpoint("riak", "riak", "ring", "peers")]))
1438+ set([RelationEndpoint("riak", "riak", "ring", "peer")]))
1439
1440 @inlineCallbacks
1441 def test_descriptor_for_services_without_formulas(self):
1442@@ -718,20 +624,20 @@
1443 self.assertEqual(
1444 (yield self.service_state_manager.join_descriptors(
1445 "wordpress", "mysql")),
1446- [(RelationEndpoint("wordpress", "mysql", "db", "requires"),
1447- RelationEndpoint("mysql", "mysql", "server", "provides"))])
1448+ [(RelationEndpoint("wordpress", "mysql", "db", "client"),
1449+ RelationEndpoint("mysql", "mysql", "server", "server"))])
1450 # symmetric - note the pair has rotated
1451 self.assertEqual(
1452 (yield self.service_state_manager.join_descriptors(
1453 "mysql", "wordpress")),
1454- [(RelationEndpoint("mysql", "mysql", "server", "provides"),
1455- RelationEndpoint("wordpress", "mysql", "db", "requires"))])
1456+ [(RelationEndpoint("mysql", "mysql", "server", "server"),
1457+ RelationEndpoint("wordpress", "mysql", "db", "client"))])
1458 yield self.add_service_from_formula("varnish")
1459 self.assertEqual(
1460 (yield self.service_state_manager.join_descriptors(
1461 "wordpress", "varnish")),
1462- [(RelationEndpoint("wordpress", "varnish", "cache", "requires"),
1463- RelationEndpoint("varnish", "varnish", "webcache", "provides"))])
1464+ [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
1465+ RelationEndpoint("varnish", "varnish", "webcache", "server"))])
1466
1467 @inlineCallbacks
1468 def test_join_descriptors_service_name_relation_name(self):
1469@@ -741,30 +647,30 @@
1470 self.assertEqual(
1471 (yield self.service_state_manager.join_descriptors(
1472 "wordpress:db", "mysql")),
1473- [(RelationEndpoint("wordpress", "mysql", "db", "requires"),
1474- RelationEndpoint("mysql", "mysql", "server", "provides"))])
1475+ [(RelationEndpoint("wordpress", "mysql", "db", "client"),
1476+ RelationEndpoint("mysql", "mysql", "server", "server"))])
1477 self.assertEqual(
1478 (yield self.service_state_manager.join_descriptors(
1479 "mysql:server", "wordpress")),
1480- [(RelationEndpoint("mysql", "mysql", "server", "provides"),
1481- RelationEndpoint("wordpress", "mysql", "db", "requires"))])
1482+ [(RelationEndpoint("mysql", "mysql", "server", "server"),
1483+ RelationEndpoint("wordpress", "mysql", "db", "client"))])
1484 self.assertEqual(
1485 (yield self.service_state_manager.join_descriptors(
1486 "mysql:server", "wordpress:db")),
1487- [(RelationEndpoint("mysql", "mysql", "server", "provides"),
1488- RelationEndpoint("wordpress", "mysql", "db", "requires"))])
1489+ [(RelationEndpoint("mysql", "mysql", "server", "server"),
1490+ RelationEndpoint("wordpress", "mysql", "db", "client"))])
1491
1492 yield self.add_service_from_formula("varnish")
1493 self.assertEqual(
1494 (yield self.service_state_manager.join_descriptors(
1495 "wordpress:cache", "varnish")),
1496- [(RelationEndpoint("wordpress", "varnish", "cache", "requires"),
1497- RelationEndpoint("varnish", "varnish", "webcache", "provides"))])
1498+ [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
1499+ RelationEndpoint("varnish", "varnish", "webcache", "server"))])
1500 self.assertEqual(
1501 (yield self.service_state_manager.join_descriptors(
1502 "wordpress:cache", "varnish:webcache")),
1503- [(RelationEndpoint("wordpress", "varnish", "cache", "requires"),
1504- RelationEndpoint("varnish", "varnish", "webcache", "provides"))])
1505+ [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
1506+ RelationEndpoint("varnish", "varnish", "webcache", "server"))])
1507
1508 @inlineCallbacks
1509 def test_join_peer_descriptors(self):
1510@@ -773,18 +679,18 @@
1511 self.assertEqual(
1512 (yield self.service_state_manager.join_descriptors(
1513 "riak", "riak")),
1514- [(RelationEndpoint("riak", "riak", "ring", "peers"),
1515- RelationEndpoint("riak", "riak", "ring", "peers"))])
1516+ [(RelationEndpoint("riak", "riak", "ring", "peer"),
1517+ RelationEndpoint("riak", "riak", "ring", "peer"))])
1518 self.assertEqual(
1519 (yield self.service_state_manager.join_descriptors(
1520 "riak:ring", "riak")),
1521- [(RelationEndpoint("riak", "riak", "ring", "peers"),
1522- RelationEndpoint("riak", "riak", "ring", "peers"))])
1523+ [(RelationEndpoint("riak", "riak", "ring", "peer"),
1524+ RelationEndpoint("riak", "riak", "ring", "peer"))])
1525 self.assertEqual(
1526 (yield self.service_state_manager.join_descriptors(
1527 "riak:ring", "riak:ring")),
1528- [(RelationEndpoint("riak", "riak", "ring", "peers"),
1529- RelationEndpoint("riak", "riak", "ring", "peers"))])
1530+ [(RelationEndpoint("riak", "riak", "ring", "peer"),
1531+ RelationEndpoint("riak", "riak", "ring", "peer"))])
1532 self.assertEqual(
1533 (yield self.service_state_manager.join_descriptors(
1534 "riak:no-ring", "riak:ring")),
1535@@ -828,3 +734,4 @@
1536 yield self.assertFailure(
1537 self.service_state_manager.join_descriptors("notyet", "nosuch"),
1538 ServiceStateNotFound)
1539+
1540
1541=== modified file 'ensemble/state/tests/test_topology.py'
1542--- ensemble/state/tests/test_topology.py 2010-11-17 20:40:28 +0000
1543+++ ensemble/state/tests/test_topology.py 2010-12-10 23:06:27 +0000
1544@@ -1,9 +1,8 @@
1545 import yaml
1546
1547 from ensemble.lib.testing import TestCase
1548-
1549-from ensemble.state.topology import (
1550- InternalTopology, InternalTopologyError)
1551+from ensemble.state.endpoint import RelationEndpoint
1552+from ensemble.state.topology import InternalTopology, InternalTopologyError
1553
1554
1555 class InternalTopologyMapTest(TestCase):
1556@@ -790,8 +789,7 @@
1557 self.assertFalse(self.topology.get_relations_for_service("s-0"))
1558
1559 def test_relation_has_service(self):
1560- """We can test to see if a service is associated to a relation.
1561- """
1562+ """We can test to see if a service is associated to a relation."""
1563 self.assertFalse(self.topology.relation_has_service("r-1", "s-0"))
1564 self.topology.add_relation("r-1", "type")
1565 self.topology.add_service("s-0", "wordpress")
1566@@ -799,8 +797,7 @@
1567 self.assertTrue(self.topology.relation_has_service("r-1", "s-0"))
1568
1569 def test_get_relation_service(self):
1570- """We can fetch the setting of a service within a relation.
1571- """
1572+ """We can fetch the setting of a service within a relation."""
1573 # Invalid relations cause an exception.
1574 self.assertRaises(InternalTopologyError,
1575 self.topology.get_relation_service,
1576@@ -903,3 +900,54 @@
1577 self.assertRaises(InternalTopologyError,
1578 self.topology.remove_service,
1579 "s-0")
1580+
1581+ def test_has_relation_between_dyadic_endpoints(self):
1582+ mysql_ep = RelationEndpoint("mysqldb", "mysql", "db", "server")
1583+ blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
1584+ self.topology.add_service("s-0", "wordpress")
1585+ self.topology.add_service("s-1", "mysqldb")
1586+ self.topology.add_relation("r-0", "mysql")
1587+ self.topology.assign_service_to_relation(
1588+ "r-0", "s-0", "mysql", "client")
1589+ self.topology.assign_service_to_relation(
1590+ "r-0", "s-1", "db", "server")
1591+ self.assertTrue(self.topology.has_relation_between_endpoints([
1592+ mysql_ep, blog_ep]))
1593+ self.assertTrue(self.topology.has_relation_between_endpoints([
1594+ blog_ep, mysql_ep]))
1595+
1596+ def test_has_relation_between_dyadic_endpoints_missing_assignment(self):
1597+ mysql_ep = RelationEndpoint("mysqldb", "mysql", "db", "server")
1598+ blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
1599+ self.topology.add_service("s-0", "wordpress")
1600+ self.topology.add_service("s-1", "mysqldb")
1601+ self.topology.add_relation("r-0", "mysql")
1602+ self.topology.assign_service_to_relation(
1603+ "r-0", "s-1", "db", "server")
1604+ self.assertFalse(self.topology.has_relation_between_endpoints([
1605+ mysql_ep, blog_ep]))
1606+ self.assertFalse(self.topology.has_relation_between_endpoints([
1607+ blog_ep, mysql_ep]))
1608+
1609+ def test_has_relation_between_dyadic_endpoints_wrong_relation_name(self):
1610+ mysql_ep = RelationEndpoint("mysqldb", "mysql", "wrong-name", "server")
1611+ blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
1612+ self.topology.add_service("s-0", "wordpress")
1613+ self.topology.add_service("s-1", "mysqldb")
1614+ self.topology.add_relation("r-0", "mysql")
1615+ self.topology.assign_service_to_relation(
1616+ "r-0", "s-0", "mysql", "client")
1617+ self.topology.assign_service_to_relation(
1618+ "r-0", "s-1", "db", "server")
1619+ self.assertFalse(self.topology.has_relation_between_endpoints([
1620+ mysql_ep, blog_ep]))
1621+ self.assertFalse(self.topology.has_relation_between_endpoints([
1622+ blog_ep, mysql_ep]))
1623+
1624+ def test_has_relation_between_monadic_endpoints(self):
1625+ riak_ep = RelationEndpoint("riak", "riak", "riak", "peer")
1626+ self.topology.add_service("s-0", "riak")
1627+ self.topology.add_relation("r-0", "riak")
1628+ self.topology.assign_service_to_relation("r-0", "s-0", "riak", "peer")
1629+ self.assertTrue(self.topology.has_relation_between_endpoints(
1630+ [riak_ep]))
1631
1632=== modified file 'ensemble/state/topology.py'
1633--- ensemble/state/topology.py 2010-11-17 20:40:28 +0000
1634+++ ensemble/state/topology.py 2010-12-10 23:06:27 +0000
1635@@ -308,8 +308,7 @@
1636 return relation_type
1637
1638 def relation_has_service(self, relation_id, service_id):
1639- """Return true if service_id is assigned to relation_id.
1640- """
1641+ """Return if `service_id` is assigned to `relation_id`."""
1642 relations = self._state.get("relations", self._nil_dict)
1643 relation_type, services = relations.get(relation_id,
1644 (None, self._nil_dict))
1645@@ -396,3 +395,29 @@
1646 raise InternalTopologyError(
1647 "Service unit %s not found in service %s" % (
1648 unit_id, service_id))
1649+
1650+ def has_relation_between_endpoints(self, endpoints):
1651+ """Examine topology to check if relation exists between `endpoints`.
1652+
1653+ The relation, with a ``relation type`` common to the
1654+ endpoints, must exist between all endpoints (presumably one
1655+ for peer, two for client-server). The topology for the
1656+ relations looks like the following in YAML::
1657+
1658+ relations:
1659+ relation-0000000000:
1660+ - mysql
1661+ - service-0000000000: {name: db, role: client}
1662+ service-0000000001: {name: server, role: server}
1663+ """
1664+ service_ids = dict((e, self.find_service_with_name(e.service_name))
1665+ for e in endpoints)
1666+ relations = self._state.get("relations", self._nil_dict)
1667+ for _, services in relations.itervalues():
1668+ for endpoint in endpoints:
1669+ service = services.get(service_ids[endpoint])
1670+ if not service or service["name"] != endpoint.relation_name:
1671+ break
1672+ else:
1673+ return True
1674+ return False

Subscribers

People subscribed via source and target branches

to status/vote changes: