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

Proposed by Kapil Thangavelu
Status: Merged
Approved by: Gustavo Niemeyer
Approved revision: 103
Merged at revision: 104
Proposed branch: lp:~hazmat/pyjuju/agent-state
Merge into: lp:pyjuju
Diff against target: 287 lines (+188/-2)
7 files modified
ensemble/lib/testing.py (+9/-0)
ensemble/state/agent.py (+42/-0)
ensemble/state/machine.py (+7/-1)
ensemble/state/service.py (+6/-1)
ensemble/state/tests/test_agent.py (+94/-0)
ensemble/state/tests/test_machine.py (+14/-0)
ensemble/state/tests/test_service.py (+16/-0)
To merge this branch: bzr merge lp:~hazmat/pyjuju/agent-state
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
Review via email: mp+41253@code.launchpad.net

Description of the change

Basic generic agent observation and connection, wired into machine state and service unit states.

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

Neat! +1!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ensemble/lib/testing.py'
2--- ensemble/lib/testing.py 2010-11-11 23:33:18 +0000
3+++ ensemble/lib/testing.py 2010-11-18 23:58:09 +0000
4@@ -6,7 +6,11 @@
5 from twisted.internet.defer import Deferred
6 from twisted.internet import reactor
7 from twisted.trial.unittest import TestCase as TrialTestCase
8+
9+from txzookeeper import ZookeeperClient
10+
11 from ensemble.lib.mocker import MockerTestCase
12+from ensemble.tests.common import get_test_zookeeper_address
13
14
15 class TestCase(TrialTestCase, MockerTestCase):
16@@ -93,3 +97,8 @@
17 deferred = Deferred()
18 reactor.callLater(delay, deferred.callback, None)
19 return deferred
20+
21+ def get_zookeeper_client(self):
22+ client = ZookeeperClient(
23+ get_test_zookeeper_address(), session_timeout=1000)
24+ return client
25
26=== added file 'ensemble/state/agent.py'
27--- ensemble/state/agent.py 1970-01-01 00:00:00 +0000
28+++ ensemble/state/agent.py 2010-11-18 23:58:09 +0000
29@@ -0,0 +1,42 @@
30+import zookeeper
31+
32+
33+class AgentStateMixin(object):
34+ """A mixin for state objects that will have agents processes.
35+
36+ Provides for the observation and connection of agent processes.
37+ Subclasses must implement M{_get_agent_path}.
38+ """
39+
40+ def has_agent(self):
41+ """Does this domain object have an agent connected.
42+
43+ Return boolean deferred informing whether an agent is
44+ connected.
45+ """
46+ d = self._client.exists(self._get_agent_path())
47+ d.addCallback(lambda result: bool(result))
48+ return d
49+
50+ def _get_agent_path(self):
51+ raise NotImplementedError
52+
53+ def watch_agent(self):
54+ """Observe changes to an agent's presence.
55+
56+ Return two boolean deferreds informing whether an agent is
57+ connected, and whether a change happened. Both presence
58+ and content changes are encapsulated in the second deferred,
59+ callers interested in only presence need to perform event
60+ filtering as needed.
61+ """
62+ exists_d, watch_d = self._client.exists_and_watch(
63+ self._get_agent_path())
64+ exists_d.addCallback(lambda result: bool(result))
65+ return exists_d, watch_d
66+
67+ def connect_agent(self):
68+ """Inform Ensemble that this associated agent is alive.
69+ """
70+ return self._client.create(
71+ self._get_agent_path(), flags=zookeeper.EPHEMERAL)
72
73=== modified file 'ensemble/state/machine.py'
74--- ensemble/state/machine.py 2010-10-15 20:46:21 +0000
75+++ ensemble/state/machine.py 2010-11-18 23:58:09 +0000
76@@ -7,6 +7,8 @@
77
78 from txzookeeper.utils import retry_change
79
80+
81+from ensemble.state.agent import AgentStateMixin
82 from ensemble.state.errors import MachineStateNotFound, StateChanged
83 from ensemble.state.base import StateBase
84
85@@ -101,7 +103,7 @@
86 self._watch_topology(watch_topology)
87
88
89-class MachineState(StateBase):
90+class MachineState(StateBase, AgentStateMixin):
91
92 def __init__(self, client, internal_id):
93 super(MachineState, self).__init__(client)
94@@ -126,6 +128,10 @@
95 """
96 return "/machines/" + self.internal_id
97
98+ def _get_agent_path(self):
99+ """Get the zookeeper path for the machine agent."""
100+ return "%s/agent" % self._zk_path
101+
102 def set_instance_id(self, instance_id):
103 """Set the provider-specific machine id in this machine state."""
104
105
106=== modified file 'ensemble/state/service.py'
107--- ensemble/state/service.py 2010-11-04 12:55:48 +0000
108+++ ensemble/state/service.py 2010-11-18 23:58:09 +0000
109@@ -4,6 +4,7 @@
110
111 from twisted.internet.defer import inlineCallbacks, returnValue
112
113+from ensemble.state.agent import AgentStateMixin
114 from ensemble.state.errors import (
115 StateChanged, ServiceStateNotFound, ServiceUnitStateNotFound,
116 ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,
117@@ -211,7 +212,7 @@
118 return service_relations
119
120
121-class ServiceUnitState(StateBase):
122+class ServiceUnitState(StateBase, AgentStateMixin):
123 """State of a service unit registered in an environment.
124
125 Each service is composed by units, and each unit represents an
126@@ -242,6 +243,10 @@
127 """Get a nice user-oriented identifier for this unit."""
128 return "%s/%d" % (self._service_name, self._unit_sequence)
129
130+ def _get_agent_path(self):
131+ """Get the zookeeper path for the service unit agent."""
132+ return "/units/%s/agent" % self._internal_id
133+
134 def assign_to_machine(self, machine_state):
135 """Assign this service unit to the given machine.
136 """
137
138=== added file 'ensemble/state/tests/test_agent.py'
139--- ensemble/state/tests/test_agent.py 1970-01-01 00:00:00 +0000
140+++ ensemble/state/tests/test_agent.py 2010-11-18 23:58:09 +0000
141@@ -0,0 +1,94 @@
142+import zookeeper
143+
144+from twisted.internet.defer import inlineCallbacks
145+from txzookeeper.tests.utils import deleteTree
146+
147+from ensemble.lib.testing import TestCase
148+from ensemble.state.base import StateBase
149+from ensemble.state.agent import AgentStateMixin
150+
151+
152+class DomainObject(StateBase, AgentStateMixin):
153+
154+ def _get_agent_path(self):
155+ return "/agent"
156+
157+
158+class AgentDomainTest(TestCase):
159+
160+ @inlineCallbacks
161+ def setUp(self):
162+ zookeeper.set_debug_level(0)
163+ yield super(TestCase, self).setUp()
164+ self.client = self.get_zookeeper_client()
165+ yield self.client.connect()
166+
167+ @inlineCallbacks
168+ def tearDown(self):
169+ yield self.client.close()
170+ client = self.get_zookeeper_client()
171+ yield client.connect()
172+ deleteTree("/", client.handle)
173+ yield client.close()
174+
175+ @inlineCallbacks
176+ def test_has_agent(self):
177+ domain = DomainObject(self.client)
178+ exists = yield domain.has_agent()
179+ self.assertIs(exists, False)
180+ yield domain.connect_agent()
181+ exists = yield domain.has_agent()
182+ self.assertIs(exists, True)
183+
184+ @inlineCallbacks
185+ def test_watch_agent(self):
186+ domain = DomainObject(self.client)
187+ results = []
188+
189+ def on_change(event):
190+ results.append(event)
191+
192+ exists_d, watch_d = domain.watch_agent()
193+ exists = yield exists_d
194+ self.assertIs(exists, False)
195+ watch_d.addCallback(on_change)
196+ self.assertFalse(results)
197+
198+ # connect the agent, and ensure its observed.
199+ yield domain.connect_agent()
200+ yield self.sleep(0.1)
201+ self.assertEqual(len(results), 1)
202+
203+ # restablish the watch and manually delete.
204+ exists_d, watch_d = domain.watch_agent()
205+ exists = yield exists_d
206+ self.assertIs(exists, True)
207+ watch_d.addCallback(on_change)
208+ yield self.client.delete("/agent")
209+
210+ self.assertEqual(results[0].type_name, "created")
211+ self.assertEqual(results[1].type_name, "deleted")
212+
213+ @inlineCallbacks
214+ def test_connect_agent(self):
215+ client = self.get_zookeeper_client()
216+ yield client.connect()
217+
218+ exists_d, watch_d = self.client.exists_and_watch("/agent")
219+ exists = yield exists_d
220+ self.assertFalse(exists)
221+
222+ domain = DomainObject(client)
223+ yield domain.connect_agent()
224+
225+ event = yield watch_d
226+ self.assertEqual(event.type_name, "created")
227+
228+ exists_d, watch_d = self.client.exists_and_watch("/agent")
229+
230+ # Force the connection of the domain object to disappear
231+ yield client.close()
232+
233+ event = yield watch_d
234+ self.assertEqual(event.type_name, "deleted")
235+ yield exists_d
236
237=== modified file 'ensemble/state/tests/test_machine.py'
238--- ensemble/state/tests/test_machine.py 2010-10-27 21:18:51 +0000
239+++ ensemble/state/tests/test_machine.py 2010-11-18 23:58:09 +0000
240@@ -186,6 +186,20 @@
241 self.assertEquals(instance_id, None)
242
243 @inlineCallbacks
244+ def test_machine_agent(self):
245+ """A machine state has an associated machine agent.
246+ """
247+ machine_state = yield self.machine_state_manager.add_machine_state()
248+ exists_d, watch_d = machine_state.watch_agent()
249+ exists = yield exists_d
250+ self.assertFalse(exists)
251+ yield machine_state.connect_agent()
252+ event = yield watch_d
253+ self.assertEqual(event.type_name, "created")
254+ self.assertEqual(event.path,
255+ "/machines/%s/agent" % machine_state.internal_id)
256+
257+ @inlineCallbacks
258 def test_watch_machines_when_being_created(self):
259 """
260 It should be possible to start watching machines even
261
262=== modified file 'ensemble/state/tests/test_service.py'
263--- ensemble/state/tests/test_service.py 2010-11-04 12:53:38 +0000
264+++ ensemble/state/tests/test_service.py 2010-11-18 23:58:09 +0000
265@@ -351,6 +351,22 @@
266 None)
267
268 @inlineCallbacks
269+ def test_service_unit_agent(self):
270+ """A service unit state has an associated unit agent.
271+ """
272+ service_state = yield self.service_state_manager.add_service_state(
273+ "wordpress", self.formula_state)
274+ unit_state = yield service_state.add_unit_state()
275+ exists_d, watch_d = unit_state.watch_agent()
276+ exists = yield exists_d
277+ self.assertFalse(exists)
278+ yield unit_state.connect_agent()
279+ event = yield watch_d
280+ self.assertEqual(event.type_name, "created")
281+ self.assertEqual(event.path,
282+ "/units/%s/agent" % unit_state.internal_id)
283+
284+ @inlineCallbacks
285 def test_unassign_unit_from_machine_without_being_assigned(self):
286 """
287 When unassigning a machine from a unit, it is possible that

Subscribers

People subscribed via source and target branches

to status/vote changes: