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
=== modified file 'ensemble/hooks/tests/test_invoker.py'
--- ensemble/hooks/tests/test_invoker.py 2010-12-07 19:05:09 +0000
+++ ensemble/hooks/tests/test_invoker.py 2010-12-10 23:06:27 +0000
@@ -11,6 +11,7 @@
11from ensemble.hooks import invoker11from ensemble.hooks import invoker
12from ensemble.hooks.protocol import UnitSettingsFactory12from ensemble.hooks.protocol import UnitSettingsFactory
13from ensemble.state import hook13from ensemble.state import hook
14from ensemble.state.endpoint import RelationEndpoint
14from ensemble.state.tests.test_relation import RelationTestBase15from ensemble.state.tests.test_relation import RelationTestBase
1516
1617
@@ -104,11 +105,14 @@
104105
105 @defer.inlineCallbacks106 @defer.inlineCallbacks
106 def _default_relations(self):107 def _default_relations(self):
107 self.mysql_states = yield self.add_relation_service_unit(108 wordpress_ep = RelationEndpoint(
108 "mysql", "mysql", "", "server")109 "wordpress", "client-server", "", "client")
109 self.wordpress_states = yield self.add_relation_service_unit(110 mysql_ep = RelationEndpoint(
110 "mysql", "wordpress", "", "client",111 "mysql", "client-server", "", "server")
111 relation_state=self.mysql_states["relation"])112 self.wordpress_states = yield self.add_relation_service_unit_from_endpoints(
113 wordpress_ep, mysql_ep)
114 self.mysql_states = yield self.add_opposite_service_unit(
115 self.wordpress_states)
112 self.relation = self.mysql_states["unit_relation"]116 self.relation = self.mysql_states["unit_relation"]
113117
114 def _manage_path(self):118 def _manage_path(self):
115119
=== modified file 'ensemble/state/endpoint.py'
--- ensemble/state/endpoint.py 2010-12-03 03:36:30 +0000
+++ ensemble/state/endpoint.py 2010-12-10 23:06:27 +0000
@@ -33,9 +33,9 @@
33 if not isinstance(other, RelationEndpoint):33 if not isinstance(other, RelationEndpoint):
34 raise TypeError("Not a RelationEndpoint", other)34 raise TypeError("Not a RelationEndpoint", other)
35 return (self.relation_type == other.relation_type and35 return (self.relation_type == other.relation_type and
36 ((self.relation_role == "provides" and36 ((self.relation_role == "server" and
37 other.relation_role == "requires") or37 other.relation_role == "client") or
38 (self.relation_role == "requires" and38 (self.relation_role == "client" and
39 other.relation_role == "provides") or39 other.relation_role == "server") or
40 (self.relation_role == "peers" and40 (self.relation_role == "peer" and
41 other.relation_role == "peers")))41 other.relation_role == "peer")))
4242
=== modified file 'ensemble/state/errors.py'
--- ensemble/state/errors.py 2010-12-10 04:55:47 +0000
+++ ensemble/state/errors.py 2010-12-10 23:06:27 +0000
@@ -94,18 +94,13 @@
94 self.unit_name94 self.unit_name
9595
9696
97class ServiceRelationAlreadyAssigned(StateError):97class RelationAlreadyExists(StateError):
9898
99 def __init__(self, service_name, relation_name, relation_role):99 def __init__(self, *endpoints):
100 self.service_name = service_name100 self.endpoints = endpoints
101 self.relation_name = relation_name
102 self.relation_role = relation_role
103101
104 def __str__(self):102 def __str__(self):
105 return (103 return "Relation already exists %r" % (self.endpoints,)
106 "Service %r is already in relation with different name %r "
107 "or role %r.") % (
108 self.service_name, self.relation_name, self.relation_role)
109104
110105
111class UnitRelationStateAlreadyAssigned(StateError):106class UnitRelationStateAlreadyAssigned(StateError):
@@ -156,7 +151,7 @@
156 self.relation_role, self.service_name)151 self.relation_role, self.service_name)
157152
158153
159class BadDescriptor(StateError):154class BadDescriptor(ValueError, EnsembleError):
160 """Descriptor is not valid.155 """Descriptor is not valid.
161156
162 A descriptor must be of the form <service name>[:<relation name>].157 A descriptor must be of the form <service name>[:<relation name>].
@@ -169,3 +164,22 @@
169164
170 def __str__(self):165 def __str__(self):
171 return "Bad descriptor: %r" % (self.descriptor,)166 return "Bad descriptor: %r" % (self.descriptor,)
167
168
169class DuplicateEndpoints(StateError):
170 """Endpoints cannot be duplicate."""
171
172 def __init__(self, *endpoints):
173 self.endpoints = endpoints
174
175 def __str__(self):
176 return "Duplicate endpoints: %r" % (self.endpoints,)
177
178class IncompatibleEndpoints(StateError):
179 """Endpoints are incompatible."""
180
181 def __init__(self, *endpoints):
182 self.endpoints = endpoints
183
184 def __str__(self):
185 return "Incompatible endpoints: %r" % (self.endpoints,)
172186
=== modified file 'ensemble/state/relation.py'
--- ensemble/state/relation.py 2010-12-07 21:49:45 +0000
+++ ensemble/state/relation.py 2010-12-10 23:06:27 +0000
@@ -9,37 +9,107 @@
99
10from ensemble.state.base import StateBase10from ensemble.state.base import StateBase
11from ensemble.state.errors import (11from ensemble.state.errors import (
12 StateChanged, ServiceRelationAlreadyAssigned, UnitRelationStateNotFound,12 DuplicateEndpoints, IncompatibleEndpoints, RelationAlreadyExists,
13 UnitRelationStateAlreadyAssigned, UnknownRelationRole)13 StateChanged, UnitRelationStateAlreadyAssigned, UnitRelationStateNotFound,
14 UnknownRelationRole)
1415
1516
16class RelationStateManager(StateBase):17class RelationStateManager(StateBase):
17 """Manages the state of relations in an environment."""18 """Manages the state of relations in an environment."""
1819
19 @inlineCallbacks20 @inlineCallbacks
20 def add_relation_state(self, relation_type):21 def add_relation_state(self, *endpoints):
21 """Add a new relation state. The relation is of the given type.22 """Add new relation state with the common relation type of `endpoints`.
2223
23 Type is used to distinguish the type of the relation, ie.24 There must be one or two endpoints specified, with the same
24 'postgres', or 'varnish'.25 `relation_type`. Their corresponding services will be assigned
26 atomically.
25 """27 """
28
29 # the TOODs in the following comments in this function are for
30 # type checking to be implemented ASAP. However, this will
31 # require some nontrivial test modification, so it's best to
32 # be done in a future branch. This is because these tests use
33 # such invalid role names as ``role``, ``dev``, and ``prod``;
34 # or they add non-peer relations of only one endpoint.
35
36 if len(endpoints) == 1:
37 # TODO verify that the endpoint is a peer endpoint only
38 pass
39 elif len(endpoints) == 2:
40 if endpoints[0] == endpoints[1]:
41 raise DuplicateEndpoints(endpoints)
42
43 # TODO verify that the relation roles are client or server
44 # only
45 if (endpoints[0].relation_role in ("client", "server") or
46 endpoints[1].relation_role in ("client", "server")):
47 if not endpoints[0].may_relate_to(endpoints[1]):
48 raise IncompatibleEndpoints(endpoints)
49 else:
50 raise TypeError("Requires 1 or 2 endpoints, %d given" % \
51 len(endpoints))
52
53 # first check so as to prevent unnecessary garbage in ZK in
54 # case this relation has been previously added
55 topology = yield self._read_topology()
56 if topology.has_relation_between_endpoints(endpoints):
57 raise RelationAlreadyExists(endpoints)
58
59 relation_type = endpoints[0].relation_type
60 relation_id = yield self._add_relation_state(relation_type)
61 services = []
62 for endpoint in endpoints:
63 service_id = topology.find_service_with_name(endpoint.service_name)
64 yield self._add_service_relation_state(
65 relation_id, service_id, endpoint)
66 services.append(ServiceRelationState(self._client,
67 service_id,
68 relation_id,
69 endpoint.relation_role,
70 endpoint.relation_name))
71
72 def add_relation(topology):
73 if topology.has_relation_between_endpoints(endpoints):
74 raise RelationAlreadyExists(endpoints)
75 topology.add_relation(relation_id, relation_type)
76 for service_relation in services:
77 if not topology.has_service(service_id):
78 raise StateChanged()
79 topology.assign_service_to_relation(
80 relation_id,
81 service_relation.internal_service_id,
82 service_relation.relation_name,
83 service_relation.relation_role)
84 yield self._retry_topology_change(add_relation)
85
86 returnValue((RelationState(self._client, relation_id), services))
87
88 @inlineCallbacks
89 def _add_relation_state(self, relation_type):
26 path = yield self._client.create(90 path = yield self._client.create(
27 "/relations/relation-", flags=zookeeper.SEQUENCE)91 "/relations/relation-", flags=zookeeper.SEQUENCE)
28
29 internal_id = basename(path)92 internal_id = basename(path)
30
31 # create the settings container, for individual units settings.93 # create the settings container, for individual units settings.
32 yield self._client.create(path + "/settings")94 yield self._client.create(path + "/settings")
3395 returnValue(internal_id)
34 # Not clear if we really need to update the topology till the relation96
35 # is in use. Topology updates are broadcasting to most connected97 @inlineCallbacks
36 # clients. Perhaps a separate batch api would be useful to minimize98 def _add_service_relation_state(
37 # broadcast events on the topology.99 self, relation_id, service_id, endpoint):
38 def add_relation(topology):100 """Add a service relation state.
39 topology.add_relation(internal_id, relation_type)101 """
40 yield self._retry_topology_change(add_relation)102 # Add service container in relation.
41103 node_data = yaml.safe_dump(
42 returnValue(RelationState(self._client, internal_id))104 {"name": endpoint.relation_name, "role": endpoint.relation_role})
105 path = "/relations/%s/%s" % (relation_id, endpoint.relation_role)
106
107 try:
108 yield self._client.create(path, node_data)
109 except zookeeper.NodeExistsException:
110 # If its not, then update the node, and continue.
111 yield retry_change(
112 self._client, path, lambda content, stat: node_data)
43113
44 @inlineCallbacks114 @inlineCallbacks
45 def remove_relation_state(self, relation_state):115 def remove_relation_state(self, relation_state):
@@ -86,89 +156,6 @@
86 def internal_id(self):156 def internal_id(self):
87 return self._internal_id157 return self._internal_id
88158
89 @inlineCallbacks
90 def unassign_service(self, service_state):
91 """Remove a service from the relation.
92
93 Any removed service needs to have its zookeeper state garbage collected
94 external to this api. The individual units should remove their
95 contained nodes, upon notification but even then the service container
96 node within the relation remains to be garbage collected.
97 """
98
99 def remove_service(topology):
100 # Verify our relation and service still exist.
101 if not topology.has_relation(self._internal_id) or \
102 not topology.has_service(service_state.internal_id):
103 raise StateChanged()
104
105 # Ignore concurrent requests that achieve the same state.
106 if not topology.relation_has_service(
107 self._internal_id, service_state.internal_id):
108 return
109
110 topology.unassign_service_from_relation(
111 self._internal_id, service_state.internal_id)
112
113 yield self._retry_topology_change(remove_service)
114
115 @inlineCallbacks
116 def assign_service(self, service_state, relation_name, relation_role):
117 """Add a service to the relation.
118
119 @param service_state - The service to be added to the relation.
120 @param name - The service's name for this relation.
121 @param role - The service's role within this relation.
122 """
123 # Add service container in relation.
124 node_data = yaml.safe_dump(
125 {"name": relation_name, "role": relation_role})
126 path = "/relations/%s/%s" % (self.internal_id, relation_role)
127
128 try:
129 yield self._client.create(path, node_data)
130 except zookeeper.NodeExistsException:
131 # because we don't garbage collect, we need to ascertain
132 # if the service node is actually in use.
133 topology = yield self._read_topology()
134
135 # If it is assigned, its an error.
136 # There's a small chance for a race condition between the check
137 # and set.
138 if topology.relation_has_service(
139 self._internal_id, service_state.internal_id):
140 raise ServiceRelationAlreadyAssigned(
141 service_state.service_name, relation_name, relation_role)
142
143 # If its not, then update the node, and continue.
144 yield retry_change(
145 self._client, path, lambda content, stat: node_data)
146
147 # Associate service to relation in the topology.
148 def add_service(topology):
149 # Verify our service and relation exists
150 if not topology.has_relation(self._internal_id) or \
151 not topology.has_service(service_state.internal_id):
152 raise StateChanged()
153
154 # Add it the topology if its not there.
155 if not topology.relation_has_service(
156 self._internal_id, service_state.internal_id):
157 topology.assign_service_to_relation(
158 self.internal_id,
159 service_state.internal_id,
160 relation_name,
161 relation_role)
162
163
164 yield self._retry_topology_change(add_service)
165
166 returnValue(ServiceRelationState(self._client,
167 service_state.internal_id,
168 self.internal_id,
169 relation_role,
170 relation_name))
171
172159
173class ServiceRelationState(object):160class ServiceRelationState(object):
174 """A state representative of a relation between one or more services."""161 """A state representative of a relation between one or more services."""
@@ -695,4 +682,3 @@
695 """682 """
696 return [unit_id for unit_id in units \683 return [unit_id for unit_id in units \
697 if unit_id != self._watcher_unit.internal_unit_id]684 if unit_id != self._watcher_unit.internal_unit_id]
698
699685
=== modified file 'ensemble/state/service.py'
--- ensemble/state/service.py 2010-12-10 04:59:54 +0000
+++ ensemble/state/service.py 2010-12-10 23:06:27 +0000
@@ -88,8 +88,11 @@
88 formula_id)88 formula_id)
89 formula_metadata = yield formula_state.get_metadata()89 formula_metadata = yield formula_state.get_metadata()
90 endpoints = set()90 endpoints = set()
91 for relation_role in ("peers", "provides", "requires"):91 relation_role_map = {
92 relations = getattr(formula_metadata, relation_role)92 "peer": "peers", "client": "requires", "server": "provides"}
93 for relation_role in ("peer", "client", "server"):
94 relations = getattr(
95 formula_metadata, relation_role_map[relation_role])
93 if relations:96 if relations:
94 for relation_name, spec in relations.iteritems():97 for relation_name, spec in relations.iteritems():
95 if (query_relation_name is None or98 if (query_relation_name is None or
9699
=== modified file 'ensemble/state/tests/test_endpoint.py'
--- ensemble/state/tests/test_endpoint.py 2010-11-25 05:28:27 +0000
+++ ensemble/state/tests/test_endpoint.py 2010-12-10 23:06:27 +0000
@@ -4,9 +4,9 @@
44
5class RelationEndpointTest(TestCase):5class RelationEndpointTest(TestCase):
6 def test_may_relate_to(self):6 def test_may_relate_to(self):
7 mysql_ep = RelationEndpoint("mysqldb", "mysql", "db", "provides")7 mysql_ep = RelationEndpoint("mysqldb", "mysql", "db", "server")
8 blog_ep = RelationEndpoint("blog", "mysql", "mysql", "requires")8 blog_ep = RelationEndpoint("blog", "mysql", "mysql", "client")
9 pg_ep = RelationEndpoint("postgres", "postgres", "db", "provides")9 pg_ep = RelationEndpoint("postgres", "postgres", "db", "server")
1010
11 self.assertRaises(TypeError, mysql_ep.may_relate_to, 42)11 self.assertRaises(TypeError, mysql_ep.may_relate_to, 42)
1212
@@ -21,15 +21,15 @@
21 # antireflexive om relation_role -21 # antireflexive om relation_role -
22 # must be consumer AND provider or vice versa22 # must be consumer AND provider or vice versa
23 self.assertFalse(blog_ep.may_relate_to(23 self.assertFalse(blog_ep.may_relate_to(
24 RelationEndpoint("foo", "mysql", "db", "requires")))24 RelationEndpoint("foo", "mysql", "db", "client")))
25 self.assertFalse(mysql_ep.may_relate_to(25 self.assertFalse(mysql_ep.may_relate_to(
26 RelationEndpoint("foo", "mysql", "db", "provides")))26 RelationEndpoint("foo", "mysql", "db", "server")))
2727
28 # irreflexive for provides/requires28 # irreflexive for server/client
29 self.assertFalse(mysql_ep.may_relate_to(mysql_ep))29 self.assertFalse(mysql_ep.may_relate_to(mysql_ep))
30 self.assertFalse(blog_ep.may_relate_to(blog_ep))30 self.assertFalse(blog_ep.may_relate_to(blog_ep))
31 self.assertFalse(pg_ep.may_relate_to(pg_ep))31 self.assertFalse(pg_ep.may_relate_to(pg_ep))
3232
33 # but reflexive for peers33 # but reflexive for peer
34 riak_ep = RelationEndpoint("riak", "riak", "riak", "peers")34 riak_ep = RelationEndpoint("riak", "riak", "riak", "peer")
35 self.assert_(riak_ep.may_relate_to(riak_ep))35 self.assert_(riak_ep.may_relate_to(riak_ep))
3636
=== modified file 'ensemble/state/tests/test_errors.py'
--- ensemble/state/tests/test_errors.py 2010-11-04 03:41:11 +0000
+++ ensemble/state/tests/test_errors.py 2010-12-10 23:06:27 +0000
@@ -1,12 +1,14 @@
1from ensemble.lib.testing import TestCase1from ensemble.lib.testing import TestCase
22
3from ensemble.state.endpoint import RelationEndpoint
3from ensemble.state.errors import (4from ensemble.state.errors import (
4 EnsembleError, StateError, StateChanged, FormulaStateNotFound,5 EnsembleError, StateError, StateChanged, FormulaStateNotFound,
5 ServiceStateNotFound, ServiceUnitStateNotFound, MachineStateNotFound,6 ServiceStateNotFound, ServiceUnitStateNotFound, MachineStateNotFound,
6 ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,7 ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,
7 BadServiceStateName, EnvironmentStateNotFound,8 BadServiceStateName, EnvironmentStateNotFound,
8 ServiceRelationAlreadyAssigned, UnitRelationStateNotFound,9 RelationAlreadyExists, UnitRelationStateNotFound,
9 UnitRelationStateAlreadyAssigned, UnknownRelationRole)10 UnitRelationStateAlreadyAssigned, UnknownRelationRole,
11 BadDescriptor, DuplicateEndpoints, IncompatibleEndpoints)
1012
1113
12class StateErrorsTest(TestCase):14class StateErrorsTest(TestCase):
@@ -76,13 +78,13 @@
76 self.assertEquals(str(error),78 self.assertEquals(str(error),
77 "Environment state was not found")79 "Environment state was not found")
7880
79 def test_service_relation_already_assigned(self):81 def test_relation_already_exists(self):
80 error = ServiceRelationAlreadyAssigned(82 error = RelationAlreadyExists(
81 "wordpress", "mysql", "client")83 RelationEndpoint("wordpress", "mysql", "mysql", "client"),
84 RelationEndpoint("mysql", "mysql", "db", "server"))
82 self.assertIsStateError(error)85 self.assertIsStateError(error)
83 msg = ("Service 'wordpress' is already in relation with different "86 self.assertTrue("wordpress" in str(error))
84 "name 'mysql' or role 'client'.")87 self.assertTrue("mysql" in str(error))
85 self.assertEquals(str(error), msg)
8688
87 def test_unit_relation_state_not_found(self):89 def test_unit_relation_state_not_found(self):
88 error = UnitRelationStateNotFound(90 error = UnitRelationStateNotFound(
@@ -103,3 +105,24 @@
103 self.assertIsStateError(error)105 self.assertIsStateError(error)
104 msg = "Unknown relation role 'server2' for service 'service-name'"106 msg = "Unknown relation role 'server2' for service 'service-name'"
105 self.assertEquals(str(error), msg)107 self.assertEquals(str(error), msg)
108
109 def test_bad_descriptor(self):
110 error = BadDescriptor("a:b:c")
111 self.assertTrue(isinstance(error, EnsembleError))
112 msg = "Bad descriptor: 'a:b:c'"
113 self.assertEquals(str(error), msg)
114
115 def test_duplicate_endpoints(self):
116 riak_ep = RelationEndpoint("riak", "riak", "ring", "peer")
117 error = DuplicateEndpoints(riak_ep, riak_ep)
118 self.assertIsStateError(error)
119 self.assertTrue("riak" in str(error))
120
121 def test_incompatible_endpoints(self):
122 error = IncompatibleEndpoints(
123 RelationEndpoint("mysql", "mysql", "db", "server"),
124 RelationEndpoint("riak", "riak", "ring", "peer"))
125 self.assertIsStateError(error)
126 self.assertTrue("mysql" in str(error))
127 self.assertTrue("riak" in str(error))
128
106129
=== modified file 'ensemble/state/tests/test_hook.py'
--- ensemble/state/tests/test_hook.py 2010-12-03 00:13:01 +0000
+++ ensemble/state/tests/test_hook.py 2010-12-10 23:06:27 +0000
@@ -3,6 +3,7 @@
3from twisted.internet.defer import inlineCallbacks3from twisted.internet.defer import inlineCallbacks
4from ensemble.lib.testing import TestCase4from ensemble.lib.testing import TestCase
55
6from ensemble.state.endpoint import RelationEndpoint
6from ensemble.state.hook import RelationChange, HookContext7from ensemble.state.hook import RelationChange, HookContext
7from ensemble.state.errors import UnitRelationStateNotFound, StateError8from ensemble.state.errors import UnitRelationStateNotFound, StateError
8from ensemble.state.tests.test_relation import RelationTestBase9from ensemble.state.tests.test_relation import RelationTestBase
@@ -23,11 +24,14 @@
23 @inlineCallbacks24 @inlineCallbacks
24 def setUp(self):25 def setUp(self):
25 yield super(ExecutionContextTest, self).setUp()26 yield super(ExecutionContextTest, self).setUp()
26 self.mysql_states = yield self.add_relation_service_unit(27 wordpress_ep = RelationEndpoint(
27 "mysql", "mysql", "", "server")28 "wordpress", "client-server", "", "client")
28 self.wordpress_states = yield self.add_relation_service_unit(29 mysql_ep = RelationEndpoint(
29 "mysql", "wordpress", "", "client",30 "mysql", "client-server", "", "server")
30 relation_state=self.mysql_states["relation"])31 self.wordpress_states = yield self.add_relation_service_unit_from_endpoints(
32 wordpress_ep, mysql_ep)
33 self.mysql_states = yield self.add_opposite_service_unit(
34 self.wordpress_states)
31 self.relation = self.mysql_states["relation"]35 self.relation = self.mysql_states["relation"]
3236
33 def get_execution_context(self, states, change_type, unit_name):37 def get_execution_context(self, states, change_type, unit_name):
3438
=== modified file 'ensemble/state/tests/test_relation.py'
--- ensemble/state/tests/test_relation.py 2010-12-07 00:22:20 +0000
+++ ensemble/state/tests/test_relation.py 2010-12-10 23:06:27 +0000
@@ -8,11 +8,12 @@
8 inlineCallbacks, returnValue, Deferred, fail, succeed)8 inlineCallbacks, returnValue, Deferred, fail, succeed)
9from txzookeeper import ZookeeperClient9from txzookeeper import ZookeeperClient
1010
11from ensemble.state.endpoint import RelationEndpoint
11from ensemble.state.formula import FormulaStateManager12from ensemble.state.formula import FormulaStateManager
12from ensemble.state.errors import (13from ensemble.state.errors import (
13 StateChanged, ServiceRelationAlreadyAssigned,14 DuplicateEndpoints, IncompatibleEndpoints, RelationAlreadyExists,
14 UnitRelationStateNotFound, UnitRelationStateAlreadyAssigned,15 StateChanged, UnitRelationStateAlreadyAssigned,
15 UnknownRelationRole)16 UnitRelationStateNotFound, UnknownRelationRole)
16from ensemble.state.relation import (17from ensemble.state.relation import (
17 RelationStateManager, ServiceRelationState, UnitRelationState)18 RelationStateManager, ServiceRelationState, UnitRelationState)
18from ensemble.state.service import ServiceStateManager19from ensemble.state.service import ServiceStateManager
@@ -40,13 +41,95 @@
4041
41 @inlineCallbacks42 @inlineCallbacks
42 def add_relation(self, relation_type, *services):43 def add_relation(self, relation_type, *services):
43 relation_state = yield self.relation_manager.add_relation_state(44 """Support older tests that don't use `RelationEndpoint`s"""
44 relation_type)45 endpoints = []
45 for service_meta in services:46 for service_meta in services:
46 service_state, relation_name, relation_role = service_meta47 service_state, relation_name, relation_role = service_meta
47 yield relation_state.assign_service(48 endpoints.append(RelationEndpoint(
48 service_state, relation_name, relation_role)49 service_state.service_name,
49 returnValue(relation_state)50 relation_type,
51 relation_name,
52 relation_role))
53 relation_state = yield self.relation_manager.add_relation_state(
54 *endpoints)
55 returnValue(relation_state[0])
56
57 @inlineCallbacks
58 def add_relation_service_unit_from_endpoints(self, *endpoints):
59 """Build the relation and add one service unit to the first endpoint.
60
61 This method is used to migrate older tests that would create
62 the relation, assign one service, add a service unit, AND then
63 assign a service. However, service assignment is now done all
64 at once with the relation creation. Because we are interested
65 in testing what happens with the changes to the service units,
66 such tests remain valid.
67
68 Returns a dict to collect together the various state objects
69 being created. This is created from the perspective of the
70 first endpoint, but the states of all of the endpoints are
71 also captured, so it can be worked with from the opposite
72 endpoint, as seen in :func:`add_opposite_service_unit`.
73 """
74 # 1. Setup all service states
75 service_states = []
76 for endpoint in endpoints:
77 service_state = yield self.add_service(endpoint.service_name)
78 service_states.append(service_state)
79
80 # 2. And join together in a relation
81 relation_state, service_relation_states = \
82 yield self.relation_manager.add_relation_state(
83 *endpoints)
84
85 # 3. Add a service unit to only the first endpoint - we need
86 # to test what happens when service units are added to the
87 # other service state (if any), so do so separately
88 unit_state = yield service_states[0].add_unit_state()
89 relation_unit_state = yield service_relation_states[0].add_unit_state(
90 unit_state)
91
92 returnValue({
93 "service": service_states[0],
94 "services": service_states,
95 "unit": unit_state,
96 "relation": relation_state,
97 "service_relation": service_relation_states[0],
98 "unit_relation": relation_unit_state,
99 "service_relations": service_relation_states})
100
101 @inlineCallbacks
102 def add_opposite_service_unit(self, other_states):
103 """Given `other_states`, add a service unit to the opposite endpoint.
104
105 Like :func:`add_relation_service_unit_from_endpoints`, this is
106 used to support older tests. Although it's slightly awkward to
107 use because of attempt to be backwards compatible, it does
108 enable the testing of a typical case: we are now bringing
109 online a service unit on the opposite side of a relation
110 endpoint pairing.
111
112 TODO: there's probably a better name for this method.
113 """
114
115 assert len(other_states["services"]) == 2
116 unit_state = yield other_states["services"][1].add_unit_state()
117 relation_unit_state = yield other_states["service_relations"][1].\
118 add_unit_state(unit_state)
119
120 def rotate(X):
121 rotated = X[1:]
122 rotated.append(X[0])
123 return rotated
124
125 returnValue({
126 "service": other_states["services"][1],
127 "services": rotate(other_states["services"]),
128 "unit": unit_state,
129 "relation": other_states["relation"],
130 "service_relation": other_states["service_relations"][1],
131 "unit_relation": relation_unit_state,
132 "service_relations": rotate(other_states["service_relations"])})
50133
51 @inlineCallbacks134 @inlineCallbacks
52 def add_relation_service_unit(self,135 def add_relation_service_unit(self,
@@ -54,7 +137,6 @@
54 service_name,137 service_name,
55 relation_name="name",138 relation_name="name",
56 relation_role="role",139 relation_role="role",
57 relation_state=None,
58 client=None):140 client=None):
59 """141 """
60 Create a relation, service, and service unit, with the142 Create a relation, service, and service unit, with the
@@ -63,21 +145,11 @@
63 the service relation and the service relation state will145 the service relation and the service relation state will
64 utilize that as their zookeeper client.146 utilize that as their zookeeper client.
65 """147 """
66 # Add the service148 # Add the service, relation, unit states
67 service_state = yield self.add_service(service_name)149 service_state = yield self.add_service(service_name)
68150 relation_state = yield self.add_relation(
69 if relation_state is None:151 relation_type,
70 # If no relation is passed, create the relation152 (service_state, relation_name, relation_role))
71 # and associate the service to it.
72 relation_state = yield self.add_relation(
73 relation_type,
74 (service_state, relation_name, relation_role))
75 else:
76 # If the relation exists, assign the service to it.
77 yield relation_state.assign_service(
78 service_state, relation_name, relation_role)
79
80 # add a unit of the service
81 unit_state = yield service_state.add_unit_state()153 unit_state = yield service_state.add_unit_state()
82154
83 # Get the service relation.155 # Get the service relation.
@@ -132,11 +204,11 @@
132204
133 @inlineCallbacks205 @inlineCallbacks
134 def test_add_relation_state(self):206 def test_add_relation_state(self):
135 """207 """Adding a relation will create a relation node and update topology."""
136 Adding a relation will create a relation node and update the topology.208 mysql_ep = RelationEndpoint("mysql", "mysql", "db", "server")
137 """209 yield self.add_service("mysql")
138 relation_state = yield self.relation_manager.add_relation_state(210 relation_state = (yield self.relation_manager.add_relation_state(
139 "mysql")211 mysql_ep))[0]
140 topology = yield self.get_topology()212 topology = yield self.get_topology()
141 self.assertTrue(topology.has_relation(relation_state.internal_id))213 self.assertTrue(topology.has_relation(relation_state.internal_id))
142 exists = yield self.client.exists(214 exists = yield self.client.exists(
@@ -147,12 +219,118 @@
147 self.assertTrue(exists)219 self.assertTrue(exists)
148220
149 @inlineCallbacks221 @inlineCallbacks
222 def test_add_relation_state_to_missing_service(self):
223 """Test adding a relation to a nonexistent service"""
224 mysql_ep = RelationEndpoint("mysql", "mysql", "db", "server")
225 blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
226 yield self.add_service("mysql")
227 # but didn't create the service for wordpress
228 yield self.assertFailure(
229 self.relation_manager.add_relation_state(
230 mysql_ep, blog_ep),
231 StateChanged)
232
233 @inlineCallbacks
234 def test_add_relation_state_bad_relation_role(self):
235 """Test adding a relation with a bad role when is one is well defined (client or server)"""
236 blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
237 mysql_ep = RelationEndpoint("mysql", "mysql", "db", "server")
238 bad_mysql_ep = RelationEndpoint("mysql", "mysql", "db", "bad-server-role")
239 bad_blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "bad-client-role")
240 yield self.add_service("mysql")
241 yield self.add_service("wordpress")
242 yield self.assertFailure(
243 self.relation_manager.add_relation_state(
244 bad_mysql_ep, blog_ep),
245 IncompatibleEndpoints)
246 yield self.assertFailure(
247 self.relation_manager.add_relation_state(
248 bad_blog_ep, mysql_ep),
249 IncompatibleEndpoints)
250 # TODO in future branch referenced in relation, also test
251 # bad_blog_ep *and* bad_mysql_ep
252
253 @inlineCallbacks
254 def test_add_binary_relation_state_twice(self):
255 """Test adding the same relation twice"""
256 blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
257 mysql_ep = RelationEndpoint("mysql", "mysql", "db", "server")
258 yield self.add_service("mysql")
259 yield self.add_service("wordpress")
260 yield self.relation_manager.add_relation_state(mysql_ep, blog_ep)
261 yield self.assertFailure(
262 self.relation_manager.add_relation_state(blog_ep, mysql_ep),
263 RelationAlreadyExists)
264 yield self.assertFailure(
265 self.relation_manager.add_relation_state(mysql_ep, blog_ep),
266 RelationAlreadyExists)
267
268 @inlineCallbacks
269 def test_add_peer_relation_state_twice(self):
270 """Test adding the same relation twice"""
271 riak_ep = RelationEndpoint("riak", "riak", "ring", "peer")
272 yield self.add_service("riak")
273 yield self.relation_manager.add_relation_state(riak_ep)
274 yield self.assertFailure(
275 self.relation_manager.add_relation_state(riak_ep),
276 RelationAlreadyExists)
277
278 @inlineCallbacks
279 def test_add_relation_state_no_endpoints(self):
280 """Test adding a relation with no endpoints (no longer allowed)"""
281 yield self.assertFailure(
282 self.relation_manager.add_relation_state(),
283 TypeError)
284
285 @inlineCallbacks
286 def test_add_relation_state_relation_type_unshared(self):
287 """Test adding a relation with endpoints not sharing a relation type"""
288 pg_ep = RelationEndpoint("pg", "postgres", "db", "server")
289 blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
290 yield self.assertFailure(
291 self.relation_manager.add_relation_state(pg_ep, blog_ep),
292 IncompatibleEndpoints)
293
294 @inlineCallbacks
295 def test_add_relation_state_too_many_endpoints(self):
296 """Test adding a relation between too many endpoints (> 2)"""
297 mysql_ep = RelationEndpoint("mysql", "mysql", "db", "server")
298 blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
299 yield self.add_service("mysql")
300 yield self.add_service("wordpress")
301 yield self.assertFailure(
302 self.relation_manager.add_relation_state(
303 mysql_ep, blog_ep, mysql_ep),
304 TypeError)
305
306 @inlineCallbacks
307 def test_add_relation_state_duplicate_peer_endpoints(self):
308 """Test adding a relation between duplicate peer endpoints"""
309 riak_ep = RelationEndpoint("riak", "riak", "ring", "peer")
310 yield self.add_service("riak")
311 yield self.assertFailure(
312 self.relation_manager.add_relation_state(riak_ep, riak_ep),
313 DuplicateEndpoints)
314
315 @inlineCallbacks
316 def test_add_relation_state_endpoints_duplicate_role(self):
317 """Test adding a relation with services overlapped by duplicate role"""
318 mysql_ep = RelationEndpoint("mysql", "mysql", "db", "server")
319 drizzle_ep = RelationEndpoint("drizzle", "mysql", "db", "server")
320 yield self.add_service("mysql")
321 yield self.add_service("drizzle")
322 yield self.assertFailure(
323 self.relation_manager.add_relation_state(mysql_ep, drizzle_ep),
324 IncompatibleEndpoints)
325
326 @inlineCallbacks
150 def test_remove_relation_state(self):327 def test_remove_relation_state(self):
151 """Removing a relation will remove it from the topology.328 """Removing a relation will remove it from the topology."""
152 """
153 # Simulate add and remove.329 # Simulate add and remove.
154 relation_state = yield self.relation_manager.add_relation_state(330 varnish_ep = RelationEndpoint("varnish", "webcache", "cache", "server")
155 "varnish")331 yield self.add_service("varnish")
332 relation_state = (yield self.relation_manager.add_relation_state(
333 varnish_ep))[0]
156 topology = yield self.get_topology()334 topology = yield self.get_topology()
157 self.assertTrue(topology.has_relation(relation_state.internal_id))335 self.assertTrue(topology.has_relation(relation_state.internal_id))
158 yield self.relation_manager.remove_relation_state(relation_state)336 yield self.relation_manager.remove_relation_state(relation_state)
@@ -164,8 +342,10 @@
164 @inlineCallbacks342 @inlineCallbacks
165 def test_remove_relation_with_changing_state(self):343 def test_remove_relation_with_changing_state(self):
166 # Simulate add and remove.344 # Simulate add and remove.
167 relation_state = yield self.relation_manager.add_relation_state(345 varnish_ep = RelationEndpoint("varnish", "webcache", "cache", "server")
168 "varnish")346 yield self.add_service("varnish")
347 relation_state = (yield self.relation_manager.add_relation_state(
348 varnish_ep))[0]
169 topology = yield self.get_topology()349 topology = yield self.get_topology()
170 self.assertTrue(topology.has_relation(relation_state.internal_id))350 self.assertTrue(topology.has_relation(relation_state.internal_id))
171 yield self.relation_manager.remove_relation_state(relation_state)351 yield self.relation_manager.remove_relation_state(relation_state)
@@ -218,8 +398,8 @@
218 def setUp(self):398 def setUp(self):
219 yield super(RelationStateTest, self).setUp()399 yield super(RelationStateTest, self).setUp()
220400
221 self.relation_state = yield self.relation_manager.add_relation_state(401 self.relation_state = (yield self.relation_manager.add_relation_state(
222 "rel-type")402 "rel-type"))[0]
223 self.formula_state = yield self.formula_manager.add_formula_state(403 self.formula_state = yield self.formula_manager.add_formula_state(
224 "namespace", self.formula)404 "namespace", self.formula)
225 self.service_state1 = yield self.service_manager.add_service_state(405 self.service_state1 = yield self.service_manager.add_service_state(
@@ -227,106 +407,6 @@
227 self.service_state2 = yield self.service_manager.add_service_state(407 self.service_state2 = yield self.service_manager.add_service_state(
228 "wordpress-dev", self.formula_state)408 "wordpress-dev", self.formula_state)
229409
230 @inlineCallbacks
231 def test_assign_service(self):
232 yield self.relation_state.assign_service(
233 self.service_state1,
234 "dev-instance",
235 "prod")
236
237 yield self.relation_state.assign_service(
238 self.service_state2,
239 "prod-instance",
240 "dev")
241
242 topology = yield self.get_topology()
243
244 self.assertEqual(
245 topology.get_relations_for_service(
246 self.service_state1.internal_id),
247 [(self.relation_state.internal_id,
248 "rel-type",
249 {"name": "dev-instance", "role": "prod"})])
250
251 self.assertEqual(
252 topology.get_relation_services(self.relation_state.internal_id),
253 {'service-0000000000': {'name': 'dev-instance', 'role': 'prod'},
254 'service-0000000001': {'name': 'prod-instance', 'role': 'dev'}})
255
256 @inlineCallbacks
257 def test_assign_service_already_assigned(self):
258 """
259 Attempting to add a service to a relation multiple times
260 will raise an error if its already there.
261 """
262 # Assign and verify
263 yield self.relation_state.assign_service(self.service_state1,
264 "dev-instance",
265 "prod")
266 topology = yield self.get_topology()
267 self.assertTrue(topology.relation_has_service(
268 self.relation_state.internal_id, self.service_state1.internal_id))
269
270 # If we attempt to add again, we'll get an error.
271 yield self.assertFailure(
272 self.relation_state.assign_service(self.service_state1,
273 "dev-instance",
274 "prod"),
275 ServiceRelationAlreadyAssigned)
276
277 # If we unassign and assign again, the service container state updates.
278 yield self.relation_state.unassign_service(self.service_state1)
279 yield self.relation_state.assign_service(self.service_state1,
280 "dev-instance2",
281 "prod")
282 content, stat = yield self.client.get(
283 "/relations/%s/%s" % (
284 self.relation_state.internal_id, "prod"))
285
286 node_data = yaml.load(content)
287 self.assertEqual(node_data["name"], "dev-instance2")
288
289 # Attempting to assign when the relation or service has gone away
290 # raises a state changed error.
291 yield self.relation_manager.remove_relation_state(self.relation_state)
292 topology = yield self.get_topology()
293 self.assertFalse(
294 topology.has_relation(self.relation_state.internal_id))
295
296 yield self.assertFailure(
297 self.relation_state.assign_service(self.service_state1,
298 "dev-instance2",
299 "prod2"),
300 StateChanged)
301
302 @inlineCallbacks
303 def test_unassign_service(self):
304 """A service can be disassociated from a relation.
305 """
306 yield self.relation_state.assign_service(self.service_state1,
307 "dev-instance",
308 "prod")
309 topology = yield self.get_topology()
310 self.assertTrue(topology.relation_has_service(
311 self.relation_state.internal_id,
312 self.service_state1.internal_id))
313
314 # Remove and verify
315 yield self.relation_state.unassign_service(self.service_state1)
316 topology = yield self.get_topology()
317 self.assertFalse(topology.relation_has_service(
318 self.relation_state.internal_id,
319 self.service_state1.internal_id))
320
321 # if we remove again, we'll succed as the final state is the same.
322 yield self.relation_state.unassign_service(self.service_state1)
323
324 # Removing again, without the relation state, errors on state change.
325 yield self.relation_manager.remove_relation_state(self.relation_state)
326 yield self.assertFailure(
327 self.relation_state.unassign_service(self.service_state1),
328 StateChanged)
329
330410
331class ServiceRelationStateTest(RelationTestBase):411class ServiceRelationStateTest(RelationTestBase):
332412
@@ -596,53 +676,16 @@
596 self.assertEqual(path, container_path)676 self.assertEqual(path, container_path)
597677
598 @inlineCallbacks678 @inlineCallbacks
599 def test_watch_start_existing_service(self):
600 """Invoking watcher.start returns a deferred that only fires
601 after watch on the container is in place. In the case of an
602 existing service, this is after a child watch is established.
603 """
604 wordpress_states = yield self.add_relation_service_unit(
605 "client-server", "wordpress", "", "client")
606
607 mysql_states = yield self.add_relation_service_unit(
608 "client-server", "mysql", "", "server",
609 relation_state = wordpress_states["relation"])
610
611 results = []
612 wait_callback = [Deferred() for i in range(5)]
613
614 def watch_related(old_units=None, new_units=None, modified=None):
615 results.append((old_units, new_units, modified))
616 wait_callback[len(results)-1].callback(True)
617
618 def invoked(*args, **kw):
619 # sleep to make sure that things haven't fired till the watch is
620 # in place.
621 time.sleep(0.1)
622 self.assertFalse(results)
623
624 mock_client = self.mocker.patch(self.client)
625 mock_client.get_children_and_watch("/relations/%s/server" % (
626 wordpress_states["relation"].internal_id))
627
628 self.mocker.call(invoked)
629 self.mocker.passthrough()
630 self.mocker.replay()
631
632 watcher = yield wordpress_states["unit_relation"].watch_related_units(
633 watch_related)
634 yield watcher.start()
635 yield wait_callback[0]
636
637 @inlineCallbacks
638 def test_watch_start_new_service(self):679 def test_watch_start_new_service(self):
639 """Invoking watcher.start returns a deferred that only fires680 """Invoking watcher.start returns a deferred that only fires
640 after watch on the containr is in place. In the case of a new681 after watch on the containr is in place. In the case of a new
641 service this after an existance watch is established on the682 service this after an existance watch is established on the
642 container.683 container.
643 """684 """
644 wordpress_states = yield self.add_relation_service_unit(685 wordpress_ep = RelationEndpoint(
645 "client-server", "wordpress", "", "client")686 "wordpress", "client-server", "", "client")
687 wordpress_states = yield self.add_relation_service_unit_from_endpoints(
688 wordpress_ep)
646689
647 results = []690 results = []
648691
@@ -673,8 +716,12 @@
673 first within relation, and start to monitor the server service716 first within relation, and start to monitor the server service
674 as it joins the relation, adds a unit, modifies, the unit.717 as it joins the relation, adds a unit, modifies, the unit.
675 """718 """
676 wordpress_states = yield self.add_relation_service_unit(719 wordpress_ep = RelationEndpoint(
677 "client-server", "wordpress", "", "client")720 "wordpress", "client-server", "", "client")
721 mysql_ep = RelationEndpoint(
722 "mysql", "client-server", "", "server")
723 wordpress_states = yield self.add_relation_service_unit_from_endpoints(
724 wordpress_ep, mysql_ep)
678725
679 # setup watch callbacks, and start watching.726 # setup watch callbacks, and start watching.
680 wait_callback = [Deferred() for i in range(5)]727 wait_callback = [Deferred() for i in range(5)]
@@ -700,9 +747,8 @@
700 self.assertFalse(results)747 self.assertFalse(results)
701748
702 # add the server service and a unit of that749 # add the server service and a unit of that
703 mysql_states = yield self.add_relation_service_unit(750 mysql_states = yield self.add_opposite_service_unit(
704 "client-server", "mysql", "", "server",751 wordpress_states)
705 relation_state=wordpress_states["relation"])
706752
707 topology = yield self.get_topology()753 topology = yield self.get_topology()
708 # assert the relation is established correctly754 # assert the relation is established correctly
@@ -734,17 +780,18 @@
734 @inlineCallbacks780 @inlineCallbacks
735 def test_watch_client_server_with_existing_service(self):781 def test_watch_client_server_with_existing_service(self):
736 """We simulate a scenario where the client and server are both782 """We simulate a scenario where the client and server are both
737 in place before the client begins observing. The server subequently,783 in place before the client begins observing. The server subsequently
738 modifies, and remove its unit from the relation.784 modifies, and remove its unit from the relation.
739 """785 """
740 # add the client service and a unit of that786 # add the client service and a unit of that
741 wordpress_states = yield self.add_relation_service_unit(787 wordpress_ep = RelationEndpoint(
742 "client-server", "wordpress", "", "client")788 "wordpress", "client-server", "", "client")
743789 mysql_ep = RelationEndpoint(
744 # add the server service and a unit of that790 "mysql", "client-server", "", "server")
745 mysql_states = yield self.add_relation_service_unit(791 wordpress_states = yield self.add_relation_service_unit_from_endpoints(
746 "client-server", "mysql", "", "server",792 wordpress_ep, mysql_ep)
747 relation_state=wordpress_states["relation"])793 mysql_states = yield self.add_opposite_service_unit(
794 wordpress_states)
748795
749 # setup callbacks and start observing.796 # setup callbacks and start observing.
750 wait_callback = [Deferred() for i in range(5)]797 wait_callback = [Deferred() for i in range(5)]
@@ -784,9 +831,13 @@
784 def test_watch_server_client_with_new_service(self):831 def test_watch_server_client_with_new_service(self):
785 """We simulate a server watching a client.832 """We simulate a server watching a client.
786 """833 """
834 wordpress_ep = RelationEndpoint(
835 "wordpress", "client-server", "", "client")
836 mysql_ep = RelationEndpoint(
837 "mysql", "client-server", "", "server")
787 # add the server service and a unit of that838 # add the server service and a unit of that
788 mysql_states = yield self.add_relation_service_unit(839 mysql_states = yield self.add_relation_service_unit_from_endpoints(
789 "client-server", "mysql", "", "server")840 mysql_ep, wordpress_ep)
790841
791 # setup callbacks and start observing.842 # setup callbacks and start observing.
792 wait_callback = [Deferred() for i in range(5)]843 wait_callback = [Deferred() for i in range(5)]
@@ -804,9 +855,8 @@
804 self.assertFalse(results)855 self.assertFalse(results)
805856
806 # add the client service and a unit of that857 # add the client service and a unit of that
807 wordpress_states = yield self.add_relation_service_unit(858 wordpress_states = yield self.add_opposite_service_unit(
808 "client-server", "wordpress", "", "client",859 mysql_states)
809 relation_state=mysql_states["relation"])
810860
811 yield wait_callback[0]861 yield wait_callback[0]
812 self.verify_unit_watch_result(862 self.verify_unit_watch_result(
@@ -817,10 +867,9 @@
817 """Peer relations always watch the peer container.867 """Peer relations always watch the peer container.
818 """868 """
819 # add the peer relation and two unit of the service.869 # add the peer relation and two unit of the service.
820 relation_state = yield self.relation_manager.add_relation_state("peer")870 riak_ep = RelationEndpoint("riak", "peer", "riak-db", "peer")
821 riak_states = yield self.add_relation_service_unit(871 riak_states = yield self.add_relation_service_unit_from_endpoints(
822 "peer", "riak", "riak-db", "peer",872 riak_ep)
823 relation_state=relation_state)
824873
825 riak2_unit = yield riak_states["service"].add_unit_state()874 riak2_unit = yield riak_states["service"].add_unit_state()
826 yield riak_states["service_relation"].add_unit_state(riak2_unit)875 yield riak_states["service_relation"].add_unit_state(riak2_unit)
@@ -878,13 +927,15 @@
878 created container is detected correctly and the observation927 created container is detected correctly and the observation
879 works immediately.928 works immediately.
880 """929 """
881 # Add the server service and a service unit.930 # Add the relation, services, and related units.
882 mysql_states = yield self.add_relation_service_unit(931 wordpress_ep = RelationEndpoint(
883 "client-server", "mysql", "", "server")932 "wordpress", "client-server", "", "client")
884933 mysql_ep = RelationEndpoint(
885 wordpress_states = yield self.add_relation_service_unit(934 "mysql", "client-server", "", "server")
886 "client-server", "wordpress", "", "client",935 wordpress_states = yield self.add_relation_service_unit_from_endpoints(
887 relation_state=mysql_states["relation"])936 wordpress_ep, mysql_ep)
937 mysql_states = yield self.add_opposite_service_unit(
938 wordpress_states)
888939
889 container_path = "/relations/%s/client" % (940 container_path = "/relations/%s/client" % (
890 mysql_states["relation"].internal_id)941 mysql_states["relation"].internal_id)
@@ -929,12 +980,14 @@
929 notification.980 notification.
930 """981 """
931 # Add the relation, services, and related units.982 # Add the relation, services, and related units.
932 mysql_states = yield self.add_relation_service_unit(983 wordpress_ep = RelationEndpoint(
933 "client-server", "mysql", "", "server")984 "wordpress", "client-server", "", "client")
934985 mysql_ep = RelationEndpoint(
935 wordpress_states = yield self.add_relation_service_unit(986 "mysql", "client-server", "", "server")
936 "client-server", "wordpress", "", "client",987 wordpress_states = yield self.add_relation_service_unit_from_endpoints(
937 relation_state=mysql_states["relation"])988 wordpress_ep, mysql_ep)
989 mysql_states = yield self.add_opposite_service_unit(
990 wordpress_states)
938991
939 # setup callbacks and start observing.992 # setup callbacks and start observing.
940 wait_callback = [Deferred() for i in range(5)]993 wait_callback = [Deferred() for i in range(5)]
@@ -981,12 +1034,14 @@
981 the unit is being observed, the watcher will ignore the deletion.1034 the unit is being observed, the watcher will ignore the deletion.
982 """1035 """
983 # Add the relation, services, and related units.1036 # Add the relation, services, and related units.
984 mysql_states = yield self.add_relation_service_unit(1037 wordpress_ep = RelationEndpoint(
985 "client-server", "mysql", "", "server")1038 "wordpress", "client-server", "", "client")
9861039 mysql_ep = RelationEndpoint(
987 wordpress_states = yield self.add_relation_service_unit(1040 "mysql", "client-server", "", "server")
988 "client-server", "wordpress", "", "client",1041 wordpress_states = yield self.add_relation_service_unit_from_endpoints(
989 relation_state=mysql_states["relation"])1042 wordpress_ep, mysql_ep)
1043 mysql_states = yield self.add_opposite_service_unit(
1044 wordpress_states)
9901045
991 # setup callbacks and start observing.1046 # setup callbacks and start observing.
992 wait_callback = [Deferred() for i in range(5)]1047 wait_callback = [Deferred() for i in range(5)]
@@ -1039,12 +1094,14 @@
1039 without additional callbacks.1094 without additional callbacks.
1040 """1095 """
1041 # Add the relation, services, and related units.1096 # Add the relation, services, and related units.
1042 mysql_states = yield self.add_relation_service_unit(1097 wordpress_ep = RelationEndpoint(
1043 "client-server", "mysql", "", "server")1098 "wordpress", "client-server", "", "client")
10441099 mysql_ep = RelationEndpoint(
1045 wordpress_states = yield self.add_relation_service_unit(1100 "mysql", "client-server", "", "server")
1046 "client-server", "wordpress", "", "client",1101 wordpress_states = yield self.add_relation_service_unit_from_endpoints(
1047 relation_state=mysql_states["relation"])1102 wordpress_ep, mysql_ep)
1103 mysql_states = yield self.add_opposite_service_unit(
1104 wordpress_states)
10481105
1049 # setup callbacks and start observing.1106 # setup callbacks and start observing.
1050 wait_callback = [Deferred() for i in range(5)]1107 wait_callback = [Deferred() for i in range(5)]
@@ -1099,8 +1156,12 @@
1099 without additional callbacks.1156 without additional callbacks.
1100 """1157 """
1101 # Add the relation, services, and related units.1158 # Add the relation, services, and related units.
1102 mysql_states = yield self.add_relation_service_unit(1159 wordpress_ep = RelationEndpoint(
1103 "client-server", "mysql", "", "server")1160 "wordpress", "client-server", "", "client")
1161 mysql_ep = RelationEndpoint(
1162 "mysql", "client-server", "", "server")
1163 mysql_states = yield self.add_relation_service_unit_from_endpoints(
1164 mysql_ep, wordpress_ep)
11041165
1105 # setup callbacks and start observing.1166 # setup callbacks and start observing.
1106 wait_callback = [Deferred() for i in range(5)]1167 wait_callback = [Deferred() for i in range(5)]
@@ -1119,9 +1180,8 @@
1119 watcher.stop()1180 watcher.stop()
11201181
1121 # Add the new service and a unit1182 # Add the new service and a unit
1122 wordpress_states = yield self.add_relation_service_unit(1183 wordpress_states = yield self.add_opposite_service_unit(
1123 "client-server", "wordpress", "", "client",1184 mysql_states)
1124 relation_state=mysql_states["relation"])
11251185
1126 # Add another unit1186 # Add another unit
1127 wordpress2_states = yield self.add_related_service_unit(1187 wordpress2_states = yield self.add_related_service_unit(
@@ -1314,12 +1374,14 @@
1314 watcher_callback = ParallelWatcherCallback()1374 watcher_callback = ParallelWatcherCallback()
13151375
1316 # Add the relation, services, and related units.1376 # Add the relation, services, and related units.
1317 mysql_states = yield self.add_relation_service_unit(1377 wordpress_ep = RelationEndpoint(
1318 "client-server", "mysql", "", "server")1378 "wordpress", "client-server", "", "client")
13191379 mysql_ep = RelationEndpoint(
1320 wordpress_states = yield self.add_relation_service_unit(1380 "mysql", "client-server", "", "server")
1321 "client-server", "wordpress", "", "client",1381 wordpress_states = yield self.add_relation_service_unit_from_endpoints(
1322 relation_state=mysql_states["relation"])1382 wordpress_ep, mysql_ep)
1383 mysql_states = yield self.add_opposite_service_unit(
1384 wordpress_states)
13231385
1324 # Start watching, and give a moment to establish1386 # Start watching, and give a moment to establish
1325 watcher = yield mysql_states["unit_relation"].watch_related_units(1387 watcher = yield mysql_states["unit_relation"].watch_related_units(
13261388
=== modified file 'ensemble/state/tests/test_service.py'
--- ensemble/state/tests/test_service.py 2010-12-10 04:59:54 +0000
+++ ensemble/state/tests/test_service.py 2010-12-10 23:06:27 +0000
@@ -27,8 +27,8 @@
27 self.formula_state_manager = FormulaStateManager(self.client)27 self.formula_state_manager = FormulaStateManager(self.client)
28 self.service_state_manager = ServiceStateManager(self.client)28 self.service_state_manager = ServiceStateManager(self.client)
29 self.machine_state_manager = MachineStateManager(self.client)29 self.machine_state_manager = MachineStateManager(self.client)
30 self.formula_state = yield self.formula_state_manager.add_formula_state(30 self.formula_state = yield self.formula_state_manager.\
31 "namespace", self.formula)31 add_formula_state("namespace", self.formula)
32 self.relation_state_manager = RelationStateManager(self.client)32 self.relation_state_manager = RelationStateManager(self.client)
3333
34 @inlineCallbacks34 @inlineCallbacks
@@ -39,13 +39,17 @@
3939
40 @inlineCallbacks40 @inlineCallbacks
41 def add_relation(self, relation_type, *services):41 def add_relation(self, relation_type, *services):
42 relation_state = yield self.relation_state_manager.add_relation_state(42 endpoints = []
43 relation_type)
44 for service_meta in services:43 for service_meta in services:
45 service_state, relation_name, relation_role = service_meta44 service_state, relation_name, relation_role = service_meta
46 yield relation_state.assign_service(45 endpoints.append(RelationEndpoint(
47 service_state, relation_name, relation_role)46 service_state.service_name,
48 returnValue(relation_state)47 relation_type,
48 relation_name,
49 relation_role))
50 relation_state = yield self.relation_state_manager.add_relation_state(
51 *endpoints)
52 returnValue(relation_state[0])
4953
50 @inlineCallbacks54 @inlineCallbacks
51 def remove_service(self, internal_service_id):55 def remove_service(self, internal_service_id):
@@ -435,55 +439,12 @@
435 yield self.assertFailure(d, StateChanged)439 yield self.assertFailure(d, StateChanged)
436440
437 @inlineCallbacks441 @inlineCallbacks
438 def test_watch_relations_with_changing_topology(self):
439 """
440 If the topology changes in an unrelated way, the
441 relations watch callback should not be called.
442 """
443 service_state = yield self.add_service("wordpress")
444 relation_state = yield self.add_relation("rel-type")
445
446 wait_callback = [Deferred() for i in range(5)]
447 calls = []
448
449 def watch_relations(old_relations, new_relations):
450 calls.append((old_relations, new_relations))
451 wait_callback[len(calls)-1].callback(True)
452
453 # Start watching
454 service_state.watch_relation_states(watch_relations)
455
456 # Callback is still untouched.
457 self.assertEquals(calls, [])
458
459 # assign service to relation, and wait for callback
460 yield relation_state.assign_service(service_state, "name", "role")
461 yield wait_callback[0]
462 yield self.sleep(0.1)
463 self.assertEqual(len(calls), 2)
464 self.assertEquals(calls[0], (None, []))
465 self.assertEquals(calls[1][0], [])
466 self.assertEquals(
467 calls[1][1][0].internal_service_id, service_state.internal_id)
468 self.assertEquals(
469 calls[1][1][0].internal_relation_id, relation_state.internal_id)
470
471 # Change the topology in an unrelated way, and give it some time
472 # in case a callback happens.
473 yield self.add_service("s-1")
474 yield self.add_relation("r-2-type")
475 yield self.sleep(0.2)
476
477 # Assert no changes observed.
478 self.assertEquals(len(calls), 2)
479
480 @inlineCallbacks
481 def test_watch_relations_when_being_created(self):442 def test_watch_relations_when_being_created(self):
482 """443 """
483 We can watch relations before we have any.444 We can watch relations before we have any.
484 """445 """
485 service_state = yield self.add_service("wordpress")446 service_state = yield self.add_service("wordpress")
486 relation_state = yield self.add_relation("rel-type")447
487448
488 wait_callback = [Deferred() for i in range(5)]449 wait_callback = [Deferred() for i in range(5)]
489 calls = []450 calls = []
@@ -499,10 +460,11 @@
499 self.assertEquals(calls, [])460 self.assertEquals(calls, [])
500461
501 # add a service relation and wait for the callback462 # add a service relation and wait for the callback
502 yield relation_state.assign_service(service_state, "name", "role")463 relation_state = yield self.add_relation(
464 "rel-type", [service_state, "name", "role"])
503 yield wait_callback[1]465 yield wait_callback[1]
504466
505 # verify the result467 # # verify the result
506 self.assertEquals(len(calls), 2)468 self.assertEquals(len(calls), 2)
507 old_relations, new_relations = calls[1]469 old_relations, new_relations = calls[1]
508 self.assertFalse(old_relations)470 self.assertFalse(old_relations)
@@ -511,7 +473,6 @@
511 # add a new relation with the service assigned to it.473 # add a new relation with the service assigned to it.
512 relation_state2 = yield self.add_relation(474 relation_state2 = yield self.add_relation(
513 "rel-type2", [service_state, "app", "server"])475 "rel-type2", [service_state, "app", "server"])
514
515 yield wait_callback[2]476 yield wait_callback[2]
516477
517 self.assertEquals(len(calls), 3)478 self.assertEquals(len(calls), 3)
@@ -555,7 +516,7 @@
555516
556 # Assign to another relation.517 # Assign to another relation.
557 yield self.add_relation("rel-type",518 yield self.add_relation("rel-type",
558 (service_state, "name", "role"))519 (service_state, "name2", "role"))
559520
560 # Give a chance for something bad to happen.521 # Give a chance for something bad to happen.
561 yield self.sleep(0.3)522 yield self.sleep(0.3)
@@ -573,61 +534,6 @@
573 self.assertEquals(len(calls), 2)534 self.assertEquals(len(calls), 2)
574535
575 @inlineCallbacks536 @inlineCallbacks
576 def test_watch_relation_assign_and_unassign_at_once(self):
577 """
578 If a service is quickly assigned and unassigned to a relation, the
579 change maybe be observed, as a single modificaiton in state.
580 """
581 wait_callback = [Deferred() for i in range(10)]
582
583 calls = []
584
585 def watch_units(old_units, new_units):
586 calls.append((old_units, new_units))
587 wait_callback[len(calls)-1].callback(True)
588
589 # Create the service
590 service_state = yield self.add_service("s-1")
591
592 # Create a relation with the service assigned.
593 relation_state1 = yield self.add_relation(
594 "rel-type", (service_state, "name", "role"))
595 relation_state2 = yield self.add_relation("rel-type")
596
597 # Start watching
598 yield service_state.watch_relation_states(watch_units)
599
600 yield wait_callback[0]
601 self.assertEquals(len(calls), 1)
602 old_relations, new_relations = calls[0]
603 self.assertEqual(old_relations, None)
604
605 # Modify the topology by hand so we get notification of multiple
606 # things simultaneously.
607 topology = yield self.get_topology()
608 topology.unassign_service_from_relation(
609 relation_state1.internal_id,
610 service_state.internal_id)
611
612 topology.assign_service_to_relation(
613 relation_state2.internal_id,
614 service_state.internal_id,
615 "role", "name")
616
617 yield self.set_topology(topology)
618
619 yield wait_callback[1]
620 self.assertEqual(len(calls), 2)
621 old_relations, new_relations = calls[1]
622
623 # Verify the changes seen.
624 self.assertEqual([r.internal_relation_id for r in old_relations],
625 [relation_state1.internal_id])
626
627 self.assertEqual([r.internal_relation_id for r in new_relations],
628 [relation_state2.internal_id])
629
630 @inlineCallbacks
631 def add_service_from_formula(self, service_name):537 def add_service_from_formula(self, service_name):
632 formula_dir = FormulaDirectory(os.path.join(538 formula_dir = FormulaDirectory(os.path.join(
633 test_repository_path, service_name))539 test_repository_path, service_name))
@@ -644,17 +550,17 @@
644 self.assertEqual(550 self.assertEqual(
645 (yield self.service_state_manager.get_relation_endpoints(551 (yield self.service_state_manager.get_relation_endpoints(
646 "wordpress")),552 "wordpress")),
647 set([RelationEndpoint("wordpress", "http", "url", "provides"),553 set([RelationEndpoint("wordpress", "http", "url", "server"),
648 RelationEndpoint("wordpress", "mysql", "db", "requires"),554 RelationEndpoint("wordpress", "mysql", "db", "client"),
649 RelationEndpoint(555 RelationEndpoint(
650 "wordpress", "varnish", "cache", "requires")]))556 "wordpress", "varnish", "cache", "client")]))
651 yield self.add_service_from_formula("riak")557 yield self.add_service_from_formula("riak")
652 self.assertEqual(558 self.assertEqual(
653 (yield self.service_state_manager.get_relation_endpoints(559 (yield self.service_state_manager.get_relation_endpoints(
654 "riak")),560 "riak")),
655 set([RelationEndpoint("riak", "http", "admin", "provides"),561 set([RelationEndpoint("riak", "http", "admin", "server"),
656 RelationEndpoint("riak", "http", "endpoint", "provides"),562 RelationEndpoint("riak", "http", "endpoint", "server"),
657 RelationEndpoint("riak", "riak", "ring", "peers")]))563 RelationEndpoint("riak", "riak", "ring", "peer")]))
658 564
659 @inlineCallbacks565 @inlineCallbacks
660 def test_get_relation_endpoints_service_name_relation_name(self):566 def test_get_relation_endpoints_service_name_relation_name(self):
@@ -663,21 +569,21 @@
663 self.assertEqual(569 self.assertEqual(
664 (yield self.service_state_manager.get_relation_endpoints(570 (yield self.service_state_manager.get_relation_endpoints(
665 "wordpress:url")),571 "wordpress:url")),
666 set([RelationEndpoint("wordpress", "http", "url", "provides")]))572 set([RelationEndpoint("wordpress", "http", "url", "server")]))
667 self.assertEqual(573 self.assertEqual(
668 (yield self.service_state_manager.get_relation_endpoints(574 (yield self.service_state_manager.get_relation_endpoints(
669 "wordpress:db")),575 "wordpress:db")),
670 set([RelationEndpoint("wordpress", "mysql", "db", "requires")]))576 set([RelationEndpoint("wordpress", "mysql", "db", "client")]))
671 self.assertEqual(577 self.assertEqual(
672 (yield self.service_state_manager.get_relation_endpoints(578 (yield self.service_state_manager.get_relation_endpoints(
673 "wordpress:cache")),579 "wordpress:cache")),
674 set([RelationEndpoint(580 set([RelationEndpoint(
675 "wordpress", "varnish", "cache", "requires")]))581 "wordpress", "varnish", "cache", "client")]))
676 yield self.add_service_from_formula("riak")582 yield self.add_service_from_formula("riak")
677 self.assertEqual(583 self.assertEqual(
678 (yield self.service_state_manager.get_relation_endpoints(584 (yield self.service_state_manager.get_relation_endpoints(
679 "riak:ring")),585 "riak:ring")),
680 set([RelationEndpoint("riak", "riak", "ring", "peers")]))586 set([RelationEndpoint("riak", "riak", "ring", "peer")]))
681587
682 @inlineCallbacks588 @inlineCallbacks
683 def test_descriptor_for_services_without_formulas(self):589 def test_descriptor_for_services_without_formulas(self):
@@ -718,20 +624,20 @@
718 self.assertEqual(624 self.assertEqual(
719 (yield self.service_state_manager.join_descriptors(625 (yield self.service_state_manager.join_descriptors(
720 "wordpress", "mysql")),626 "wordpress", "mysql")),
721 [(RelationEndpoint("wordpress", "mysql", "db", "requires"),627 [(RelationEndpoint("wordpress", "mysql", "db", "client"),
722 RelationEndpoint("mysql", "mysql", "server", "provides"))])628 RelationEndpoint("mysql", "mysql", "server", "server"))])
723 # symmetric - note the pair has rotated629 # symmetric - note the pair has rotated
724 self.assertEqual(630 self.assertEqual(
725 (yield self.service_state_manager.join_descriptors(631 (yield self.service_state_manager.join_descriptors(
726 "mysql", "wordpress")),632 "mysql", "wordpress")),
727 [(RelationEndpoint("mysql", "mysql", "server", "provides"),633 [(RelationEndpoint("mysql", "mysql", "server", "server"),
728 RelationEndpoint("wordpress", "mysql", "db", "requires"))])634 RelationEndpoint("wordpress", "mysql", "db", "client"))])
729 yield self.add_service_from_formula("varnish")635 yield self.add_service_from_formula("varnish")
730 self.assertEqual(636 self.assertEqual(
731 (yield self.service_state_manager.join_descriptors(637 (yield self.service_state_manager.join_descriptors(
732 "wordpress", "varnish")),638 "wordpress", "varnish")),
733 [(RelationEndpoint("wordpress", "varnish", "cache", "requires"),639 [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
734 RelationEndpoint("varnish", "varnish", "webcache", "provides"))])640 RelationEndpoint("varnish", "varnish", "webcache", "server"))])
735641
736 @inlineCallbacks642 @inlineCallbacks
737 def test_join_descriptors_service_name_relation_name(self):643 def test_join_descriptors_service_name_relation_name(self):
@@ -741,30 +647,30 @@
741 self.assertEqual(647 self.assertEqual(
742 (yield self.service_state_manager.join_descriptors(648 (yield self.service_state_manager.join_descriptors(
743 "wordpress:db", "mysql")),649 "wordpress:db", "mysql")),
744 [(RelationEndpoint("wordpress", "mysql", "db", "requires"),650 [(RelationEndpoint("wordpress", "mysql", "db", "client"),
745 RelationEndpoint("mysql", "mysql", "server", "provides"))])651 RelationEndpoint("mysql", "mysql", "server", "server"))])
746 self.assertEqual(652 self.assertEqual(
747 (yield self.service_state_manager.join_descriptors(653 (yield self.service_state_manager.join_descriptors(
748 "mysql:server", "wordpress")),654 "mysql:server", "wordpress")),
749 [(RelationEndpoint("mysql", "mysql", "server", "provides"),655 [(RelationEndpoint("mysql", "mysql", "server", "server"),
750 RelationEndpoint("wordpress", "mysql", "db", "requires"))])656 RelationEndpoint("wordpress", "mysql", "db", "client"))])
751 self.assertEqual(657 self.assertEqual(
752 (yield self.service_state_manager.join_descriptors(658 (yield self.service_state_manager.join_descriptors(
753 "mysql:server", "wordpress:db")),659 "mysql:server", "wordpress:db")),
754 [(RelationEndpoint("mysql", "mysql", "server", "provides"),660 [(RelationEndpoint("mysql", "mysql", "server", "server"),
755 RelationEndpoint("wordpress", "mysql", "db", "requires"))])661 RelationEndpoint("wordpress", "mysql", "db", "client"))])
756662
757 yield self.add_service_from_formula("varnish")663 yield self.add_service_from_formula("varnish")
758 self.assertEqual(664 self.assertEqual(
759 (yield self.service_state_manager.join_descriptors(665 (yield self.service_state_manager.join_descriptors(
760 "wordpress:cache", "varnish")),666 "wordpress:cache", "varnish")),
761 [(RelationEndpoint("wordpress", "varnish", "cache", "requires"),667 [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
762 RelationEndpoint("varnish", "varnish", "webcache", "provides"))])668 RelationEndpoint("varnish", "varnish", "webcache", "server"))])
763 self.assertEqual(669 self.assertEqual(
764 (yield self.service_state_manager.join_descriptors(670 (yield self.service_state_manager.join_descriptors(
765 "wordpress:cache", "varnish:webcache")),671 "wordpress:cache", "varnish:webcache")),
766 [(RelationEndpoint("wordpress", "varnish", "cache", "requires"),672 [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
767 RelationEndpoint("varnish", "varnish", "webcache", "provides"))])673 RelationEndpoint("varnish", "varnish", "webcache", "server"))])
768674
769 @inlineCallbacks675 @inlineCallbacks
770 def test_join_peer_descriptors(self):676 def test_join_peer_descriptors(self):
@@ -773,18 +679,18 @@
773 self.assertEqual(679 self.assertEqual(
774 (yield self.service_state_manager.join_descriptors(680 (yield self.service_state_manager.join_descriptors(
775 "riak", "riak")),681 "riak", "riak")),
776 [(RelationEndpoint("riak", "riak", "ring", "peers"),682 [(RelationEndpoint("riak", "riak", "ring", "peer"),
777 RelationEndpoint("riak", "riak", "ring", "peers"))])683 RelationEndpoint("riak", "riak", "ring", "peer"))])
778 self.assertEqual(684 self.assertEqual(
779 (yield self.service_state_manager.join_descriptors(685 (yield self.service_state_manager.join_descriptors(
780 "riak:ring", "riak")),686 "riak:ring", "riak")),
781 [(RelationEndpoint("riak", "riak", "ring", "peers"),687 [(RelationEndpoint("riak", "riak", "ring", "peer"),
782 RelationEndpoint("riak", "riak", "ring", "peers"))])688 RelationEndpoint("riak", "riak", "ring", "peer"))])
783 self.assertEqual(689 self.assertEqual(
784 (yield self.service_state_manager.join_descriptors(690 (yield self.service_state_manager.join_descriptors(
785 "riak:ring", "riak:ring")),691 "riak:ring", "riak:ring")),
786 [(RelationEndpoint("riak", "riak", "ring", "peers"),692 [(RelationEndpoint("riak", "riak", "ring", "peer"),
787 RelationEndpoint("riak", "riak", "ring", "peers"))])693 RelationEndpoint("riak", "riak", "ring", "peer"))])
788 self.assertEqual(694 self.assertEqual(
789 (yield self.service_state_manager.join_descriptors(695 (yield self.service_state_manager.join_descriptors(
790 "riak:no-ring", "riak:ring")),696 "riak:no-ring", "riak:ring")),
@@ -828,3 +734,4 @@
828 yield self.assertFailure(734 yield self.assertFailure(
829 self.service_state_manager.join_descriptors("notyet", "nosuch"),735 self.service_state_manager.join_descriptors("notyet", "nosuch"),
830 ServiceStateNotFound)736 ServiceStateNotFound)
737
831738
=== modified file 'ensemble/state/tests/test_topology.py'
--- ensemble/state/tests/test_topology.py 2010-11-17 20:40:28 +0000
+++ ensemble/state/tests/test_topology.py 2010-12-10 23:06:27 +0000
@@ -1,9 +1,8 @@
1import yaml1import yaml
22
3from ensemble.lib.testing import TestCase3from ensemble.lib.testing import TestCase
44from ensemble.state.endpoint import RelationEndpoint
5from ensemble.state.topology import (5from ensemble.state.topology import InternalTopology, InternalTopologyError
6 InternalTopology, InternalTopologyError)
76
87
9class InternalTopologyMapTest(TestCase):8class InternalTopologyMapTest(TestCase):
@@ -790,8 +789,7 @@
790 self.assertFalse(self.topology.get_relations_for_service("s-0"))789 self.assertFalse(self.topology.get_relations_for_service("s-0"))
791790
792 def test_relation_has_service(self):791 def test_relation_has_service(self):
793 """We can test to see if a service is associated to a relation.792 """We can test to see if a service is associated to a relation."""
794 """
795 self.assertFalse(self.topology.relation_has_service("r-1", "s-0"))793 self.assertFalse(self.topology.relation_has_service("r-1", "s-0"))
796 self.topology.add_relation("r-1", "type")794 self.topology.add_relation("r-1", "type")
797 self.topology.add_service("s-0", "wordpress")795 self.topology.add_service("s-0", "wordpress")
@@ -799,8 +797,7 @@
799 self.assertTrue(self.topology.relation_has_service("r-1", "s-0"))797 self.assertTrue(self.topology.relation_has_service("r-1", "s-0"))
800798
801 def test_get_relation_service(self):799 def test_get_relation_service(self):
802 """We can fetch the setting of a service within a relation.800 """We can fetch the setting of a service within a relation."""
803 """
804 # Invalid relations cause an exception.801 # Invalid relations cause an exception.
805 self.assertRaises(InternalTopologyError,802 self.assertRaises(InternalTopologyError,
806 self.topology.get_relation_service,803 self.topology.get_relation_service,
@@ -903,3 +900,54 @@
903 self.assertRaises(InternalTopologyError,900 self.assertRaises(InternalTopologyError,
904 self.topology.remove_service,901 self.topology.remove_service,
905 "s-0")902 "s-0")
903
904 def test_has_relation_between_dyadic_endpoints(self):
905 mysql_ep = RelationEndpoint("mysqldb", "mysql", "db", "server")
906 blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
907 self.topology.add_service("s-0", "wordpress")
908 self.topology.add_service("s-1", "mysqldb")
909 self.topology.add_relation("r-0", "mysql")
910 self.topology.assign_service_to_relation(
911 "r-0", "s-0", "mysql", "client")
912 self.topology.assign_service_to_relation(
913 "r-0", "s-1", "db", "server")
914 self.assertTrue(self.topology.has_relation_between_endpoints([
915 mysql_ep, blog_ep]))
916 self.assertTrue(self.topology.has_relation_between_endpoints([
917 blog_ep, mysql_ep]))
918
919 def test_has_relation_between_dyadic_endpoints_missing_assignment(self):
920 mysql_ep = RelationEndpoint("mysqldb", "mysql", "db", "server")
921 blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
922 self.topology.add_service("s-0", "wordpress")
923 self.topology.add_service("s-1", "mysqldb")
924 self.topology.add_relation("r-0", "mysql")
925 self.topology.assign_service_to_relation(
926 "r-0", "s-1", "db", "server")
927 self.assertFalse(self.topology.has_relation_between_endpoints([
928 mysql_ep, blog_ep]))
929 self.assertFalse(self.topology.has_relation_between_endpoints([
930 blog_ep, mysql_ep]))
931
932 def test_has_relation_between_dyadic_endpoints_wrong_relation_name(self):
933 mysql_ep = RelationEndpoint("mysqldb", "mysql", "wrong-name", "server")
934 blog_ep = RelationEndpoint("wordpress", "mysql", "mysql", "client")
935 self.topology.add_service("s-0", "wordpress")
936 self.topology.add_service("s-1", "mysqldb")
937 self.topology.add_relation("r-0", "mysql")
938 self.topology.assign_service_to_relation(
939 "r-0", "s-0", "mysql", "client")
940 self.topology.assign_service_to_relation(
941 "r-0", "s-1", "db", "server")
942 self.assertFalse(self.topology.has_relation_between_endpoints([
943 mysql_ep, blog_ep]))
944 self.assertFalse(self.topology.has_relation_between_endpoints([
945 blog_ep, mysql_ep]))
946
947 def test_has_relation_between_monadic_endpoints(self):
948 riak_ep = RelationEndpoint("riak", "riak", "riak", "peer")
949 self.topology.add_service("s-0", "riak")
950 self.topology.add_relation("r-0", "riak")
951 self.topology.assign_service_to_relation("r-0", "s-0", "riak", "peer")
952 self.assertTrue(self.topology.has_relation_between_endpoints(
953 [riak_ep]))
906954
=== modified file 'ensemble/state/topology.py'
--- ensemble/state/topology.py 2010-11-17 20:40:28 +0000
+++ ensemble/state/topology.py 2010-12-10 23:06:27 +0000
@@ -308,8 +308,7 @@
308 return relation_type308 return relation_type
309309
310 def relation_has_service(self, relation_id, service_id):310 def relation_has_service(self, relation_id, service_id):
311 """Return true if service_id is assigned to relation_id.311 """Return if `service_id` is assigned to `relation_id`."""
312 """
313 relations = self._state.get("relations", self._nil_dict)312 relations = self._state.get("relations", self._nil_dict)
314 relation_type, services = relations.get(relation_id,313 relation_type, services = relations.get(relation_id,
315 (None, self._nil_dict))314 (None, self._nil_dict))
@@ -396,3 +395,29 @@
396 raise InternalTopologyError(395 raise InternalTopologyError(
397 "Service unit %s not found in service %s" % (396 "Service unit %s not found in service %s" % (
398 unit_id, service_id))397 unit_id, service_id))
398
399 def has_relation_between_endpoints(self, endpoints):
400 """Examine topology to check if relation exists between `endpoints`.
401
402 The relation, with a ``relation type`` common to the
403 endpoints, must exist between all endpoints (presumably one
404 for peer, two for client-server). The topology for the
405 relations looks like the following in YAML::
406
407 relations:
408 relation-0000000000:
409 - mysql
410 - service-0000000000: {name: db, role: client}
411 service-0000000001: {name: server, role: server}
412 """
413 service_ids = dict((e, self.find_service_with_name(e.service_name))
414 for e in endpoints)
415 relations = self._state.get("relations", self._nil_dict)
416 for _, services in relations.itervalues():
417 for endpoint in endpoints:
418 service = services.get(service_ids[endpoint])
419 if not service or service["name"] != endpoint.relation_name:
420 break
421 else:
422 return True
423 return False

Subscribers

People subscribed via source and target branches

to status/vote changes: