Merge lp:~fwereade/pyjuju/agent-trash-old-sessions into lp:pyjuju

Proposed by William Reade
Status: Merged
Approved by: Kapil Thangavelu
Approved revision: 485
Merged at revision: 461
Proposed branch: lp:~fwereade/pyjuju/agent-trash-old-sessions
Merge into: lp:pyjuju
Prerequisite: lp:~fwereade/pyjuju/upstartify-services
Diff against target: 970 lines (+304/-68)
30 files modified
examples/oneiric/mysql/hooks/install (+2/-2)
examples/oneiric/mysql/hooks/start (+3/-1)
examples/oneiric/mysql/hooks/stop (+1/-1)
juju/agents/base.py (+56/-3)
juju/agents/tests/common.py (+1/-0)
juju/agents/tests/test_base.py (+170/-22)
juju/agents/tests/test_machine.py (+3/-4)
juju/agents/tests/test_unit.py (+3/-1)
juju/agents/unit.py (+8/-3)
juju/control/tests/test_status.py (+1/-0)
juju/machine/tests/test_unit_deployment.py (+3/-1)
juju/machine/unit.py (+6/-2)
juju/providers/common/cloudinit.py (+4/-2)
juju/providers/common/tests/data/cloud_init_bootstrap (+4/-2)
juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers (+4/-2)
juju/providers/common/tests/data/cloud_init_branch (+2/-1)
juju/providers/common/tests/data/cloud_init_branch_trunk (+2/-1)
juju/providers/common/tests/data/cloud_init_distro (+2/-1)
juju/providers/common/tests/data/cloud_init_ppa (+2/-1)
juju/providers/ec2/tests/data/bootstrap_cloud_init (+4/-2)
juju/providers/ec2/tests/data/launch_cloud_init (+2/-1)
juju/providers/ec2/tests/data/launch_cloud_init_branch (+2/-1)
juju/providers/ec2/tests/data/launch_cloud_init_ppa (+2/-1)
juju/providers/local/agent.py (+3/-1)
juju/providers/local/tests/test_agent.py (+1/-0)
juju/providers/orchestra/tests/data/bootstrap_user_data (+4/-2)
juju/providers/orchestra/tests/data/launch_user_data (+2/-1)
juju/state/tests/test_security.py (+1/-0)
juju/unit/lifecycle.py (+0/-4)
juju/unit/tests/test_workflow.py (+6/-5)
To merge this branch: bzr merge lp:~fwereade/pyjuju/agent-trash-old-sessions
Reviewer Review Type Date Requested Status
Benjamin Saller (community) Approve
Kapil Thangavelu (community) Approve
Review via email: mp+85337@code.launchpad.net

This proposal supersedes a proposal from 2011-11-30.

Description of the change

Agents now require a --session-file argument, which is written to on connect and deleted on close.

If, on startup, the session file exists and refers to a valid session, the session is explicitly closed before any further work is done.

IMO this is the best approach because it minimises the number of cases we have to think about; rather than attempting to re-engage with an existing session (set up watches but skip ephemeral node creation... more considerations?), we always have to set up exactly the same state, whatever the reason we're restarting.

Session files are chmodded to 600 but, on reflection, I'm not sure this actually helps... a malicious hook could still plausibly hijack them. Thoughts?

The cloud-init runcmds have changed again, and these make up a tediously large proportion of the diff. Sorry for the noise :(.

To post a comment you must log in.
Revision history for this message
Kapil Thangavelu (hazmat) wrote : Posted in a previous version of this proposal

Excerpts from William Reade's message of Wed Nov 30 09:56:37 UTC 2011:
> William Reade has proposed merging lp:~fwereade/juju/agent-trash-old-sessions into lp:juju with lp:~fwereade/juju/upstartify-agents as a prerequisite.
>
> Requested reviews:
> juju hackers (juju)
> Related bugs:
> Bug #898082 in juju: "immediate agent restart fails"
> https://bugs.launchpad.net/juju/+bug/898082
>
> For more details, see:
> https://code.launchpad.net/~fwereade/juju/agent-trash-old-sessions/+merge/83905
>
> Agents now require a --session-file argument, which is written to on connect and deleted on close.
>
> If, on startup, the session file exists and refers to a valid session, the session is explicitly closed before any further work is done.
>
> IMO this is the best approach because it minimises the number of cases we have to think about; rather than attempting to re-engage with an existing session (set up watches but skip ephemeral node creation... more considerations?), we always have to set up exactly the same state, whatever the reason we're restarting.
>
> Session files are chmodded to 600 but, on reflection, I'm not sure this actually helps... a malicious hook could still plausibly hijack them. Thoughts?
>
> The cloud-init runcmds have changed again, and these make up a tediously large proportion of the diff. Sorry for the noise :(.

Haven't looked at the diff, just a comment on the approach, this seems pretty
reasonable and simple. The watch callbacks (transient registration with libzk)
are gone so we can't usefully reconnect to the original session watches, we'd
have to restablish them anyways. Effectively sessions are primarily useful for
transient error activity not process restarts.

However there is a caveat wrt to session closing and ephemeral nodes that's
worth keeping in mind, namely that the ephemerals are coordination points for
things like relation membership notification, and this will result in transient
-depart/-join hook execution as the old session closes and the new one comes
into place. I don't see any reasonable alternatives that are significantly more
complex though.

Revision history for this message
William Reade (fwereade) wrote : Posted in a previous version of this proposal

rejecting due to abandonment of prereq

Revision history for this message
William Reade (fwereade) wrote :

...and resubmitting because I've managed to untangle the prereqs.

478. By William Reade

merge parent

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

> However there is a caveat wrt to session closing and ephemeral nodes that's
> worth keeping in mind, namely that the ephemerals are coordination points for
> things like relation membership notification, and this will result in transient
> -depart/-join hook execution as the old session closes and the new one comes
> into place. I don't see any reasonable alternatives that are significantly more
> complex though.

This is a serious issue IMO, for the same reason that an agent stopping should not
stop the user's agent: a small bug in juju should not kill the highly resilient
and highly tested database server from the user.

There's a simple solution to this problem: anything monitoring an ephemeral presence
node should hold off for a grace period before taking action on it. After the grace
period is over, check back to see if the node is there again. In case the node is
still not around, then follow suit and take action. In case the node is back, re-watch
it and forget about the hiccup.

The same approach should be used across the board with other agents.

479. By William Reade

merge parent

480. By William Reade

merge parent

Revision history for this message
William Reade (fwereade) wrote :

As Gustavo points out, we will indeed need a followup branch to ignore brief agent presence blips.

481. By William Reade

merge parent

482. By William Reade

merge parent

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

LGTM +1

review: Approve
Revision history for this message
Benjamin Saller (bcsaller) wrote :

LGTM + 1

A few minors

[0] test_session_file_permissions checks the permissions of the file but doesn't seem to handle/test the case where the configured path for the session file isn't writeable. test_nonexistent_directory and test_bad_session_file seem to get at this but it seems to be the check once rather than on each iteration of client. It might be that we want to be able to handle changes to fs perms during the lifetime of the agent.

[1] aggressive use of newline
        + self.assertRaises(
 + JujuError,
 + agent.configure,
 + data)
 +

review: Approve
483. By William Reade

merge parent

484. By William Reade

merge parent

485. By William Reade

address review point

486. By William Reade

merge parent

487. By William Reade

merge parent

488. By William Reade

merge parent

489. By William Reade

merge parent

490. By William Reade

merge parent

491. By William Reade

merge parent

492. By William Reade

merge parent

493. By William Reade

merge parent

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'examples/oneiric/mysql/hooks/install'
2--- examples/oneiric/mysql/hooks/install 2011-09-15 18:56:08 +0000
3+++ examples/oneiric/mysql/hooks/install 2012-02-21 12:19:20 +0000
4@@ -21,5 +21,5 @@
5 juju-log "Editing my.cnf to allow listening on all interfaces"
6 sed --in-place=old 's/127\.0\.0\.1/0.0.0.0/' /etc/mysql/my.cnf
7
8-juju-log "Restarting mysql service"
9-service mysql restart
10+juju-log "Stopping mysql service"
11+service mysql stop
12
13=== modified file 'examples/oneiric/mysql/hooks/start'
14--- examples/oneiric/mysql/hooks/start 2011-02-03 01:23:43 +0000
15+++ examples/oneiric/mysql/hooks/start 2012-02-21 12:19:20 +0000
16@@ -1,1 +1,3 @@
17-#!/bin/bash
18\ No newline at end of file
19+#!/bin/bash
20+juju-log "Starting mysql service"
21+service mysql start || service mysql restart
22
23=== modified file 'examples/oneiric/mysql/hooks/stop'
24--- examples/oneiric/mysql/hooks/stop 2011-09-15 18:56:08 +0000
25+++ examples/oneiric/mysql/hooks/stop 2012-02-21 12:19:20 +0000
26@@ -1,3 +1,3 @@
27 #!/bin/bash
28 juju-log "Stopping mysql service"
29-/etc/init.d/mysql stop
30+service mysql stop || true
31
32=== modified file 'juju/agents/base.py'
33--- juju/agents/base.py 2011-09-22 13:23:00 +0000
34+++ juju/agents/base.py 2012-02-21 12:19:20 +0000
35@@ -1,7 +1,9 @@
36 import argparse
37 import os
38+import logging
39+import stat
40 import sys
41-import logging
42+import yaml
43
44 import zookeeper
45
46@@ -18,6 +20,23 @@
47 from juju.state.environment import GlobalSettingsStateManager
48
49
50+def load_client_id(path):
51+ try:
52+ with open(path) as f:
53+ return yaml.load(f.read())
54+ except IOError:
55+ return None
56+
57+
58+def save_client_id(path, client_id):
59+ parent = os.path.dirname(path)
60+ if not os.path.exists(parent):
61+ os.makedirs(parent)
62+ with open(path, "w") as f:
63+ f.write(yaml.dump(client_id))
64+ os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
65+
66+
67 class TwistedOptionNamespace(object):
68 """
69 An argparse namespace implementation that is compatible with twisted
70@@ -153,13 +172,40 @@
71 "Invalid juju-directory %r, does not exist." % (
72 options.get("juju_directory")))
73
74+ if options["session_file"] is None:
75+ raise JujuError("No session file specified")
76+
77 self.config = options
78
79 @inlineCallbacks
80+ def _kill_existing_session(self):
81+ try:
82+ # We might have died suddenly, in which case the session may
83+ # still be alive. If this is the case, shoot it in the head, so
84+ # it doesn't interfere with our attempts to recreate our state.
85+ # (We need to be able to recreate our state *anyway*, and it's
86+ # much simpler to force ourselves to recreate it every time than
87+ # it is to mess around partially recreating partial state.)
88+ client_id = load_client_id(self.config["session_file"])
89+ if client_id is None:
90+ return
91+ temp_client = yield ZookeeperClient().connect(
92+ self.config["zookeeper_servers"], client_id=client_id)
93+ yield temp_client.close()
94+ except zookeeper.ZooKeeperException:
95+ # We don't really care what went wrong; just that we're not able
96+ # to connect using the old session, and therefore we should be ok
97+ # to start a fresh one without transient state hanging around.
98+ pass
99+
100+ @inlineCallbacks
101 def connect(self):
102 """Return an authenticated connection to the juju zookeeper."""
103- hosts = self.config["zookeeper_servers"]
104- self.client = yield ZookeeperClient().connect(hosts)
105+ yield self._kill_existing_session()
106+ self.client = yield ZookeeperClient().connect(
107+ self.config["zookeeper_servers"])
108+ save_client_id(
109+ self.config["session_file"], self.client.client_id)
110
111 principals = self.config.get("principals", ())
112 for principal in principals:
113@@ -200,6 +246,9 @@
114 finally:
115 if self.client and self.client.connected:
116 self.client.close()
117+ session_file = self.config["session_file"]
118+ if os.path.exists(session_file):
119+ os.unlink(session_file)
120
121 def set_watch_enabled(self, flag):
122 """Set boolean flag for whether this agent should watching zookeeper.
123@@ -285,3 +334,7 @@
124 parser.add_argument(
125 "--juju-directory", default=juju_home, type=os.path.abspath,
126 help="juju working directory ($JUJU_HOME)")
127+
128+ parser.add_argument(
129+ "--session-file", default=None, type=os.path.abspath,
130+ help="like a pidfile, but for the zookeeper session id")
131
132=== modified file 'juju/agents/tests/common.py'
133--- juju/agents/tests/common.py 2011-09-15 19:24:47 +0000
134+++ juju/agents/tests/common.py 2012-02-21 12:19:20 +0000
135@@ -55,6 +55,7 @@
136 options = TwistedOptionNamespace()
137 options["juju_directory"] = self.juju_directory
138 options["zookeeper_servers"] = get_test_zookeeper_address()
139+ options["session_file"] = self.makeFile()
140 return succeed(options)
141
142 @inlineCallbacks
143
144=== modified file 'juju/agents/tests/test_base.py'
145--- juju/agents/tests/test_base.py 2012-02-21 12:19:19 +0000
146+++ juju/agents/tests/test_base.py 2012-02-21 12:19:20 +0000
147@@ -2,13 +2,14 @@
148 import json
149 import logging
150 import os
151+import stat
152 import sys
153-
154+import yaml
155
156 from twisted.application.app import AppLogger
157 from twisted.application.service import IService, IServiceCollection
158 from twisted.internet.defer import (
159- succeed, Deferred, inlineCallbacks, returnValue)
160+ fail, succeed, Deferred, inlineCallbacks, returnValue)
161 from twisted.python.components import Componentized
162 from twisted.python import log
163
164@@ -20,6 +21,7 @@
165
166 from juju.agents.base import (
167 BaseAgent, TwistedOptionNamespace, AgentRunner, AgentLogger)
168+from juju.agents.dummy import DummyAgent
169 from juju.errors import NoConnection, JujuError
170 from juju.lib.zklog import ZookeeperHandler
171
172@@ -34,8 +36,8 @@
173 @inlineCallbacks
174 def setUp(self):
175 yield super(BaseAgentTest, self).setUp()
176- self.change_environment(
177- JUJU_HOME=self.makeDir())
178+ self.juju_home = self.makeDir()
179+ self.change_environment(JUJU_HOME=self.juju_home)
180
181 def test_as_app(self):
182 """The agent class can be accessed as an application."""
183@@ -79,6 +81,8 @@
184 # Agent options
185 self.assertEqual(parser.get_default("principals"), [])
186 self.assertEqual(parser.get_default("zookeeper_servers"), "")
187+ self.assertEqual(parser.get_default("juju_directory"), self.juju_home)
188+ self.assertEqual(parser.get_default("session_file"), None)
189
190 def test_twistd_flags_correspond(self):
191 parser = argparse.ArgumentParser()
192@@ -99,7 +103,8 @@
193 log_file_path = self.makeFile()
194
195 options = parser.parse_args(
196- ["--logfile", log_file_path], namespace=TwistedOptionNamespace())
197+ ["--logfile", log_file_path, "--session-file", self.makeFile()],
198+ namespace=TwistedOptionNamespace())
199
200 def match_observer(observer):
201 return isinstance(observer.im_self, log.PythonLoggingObserver)
202@@ -180,8 +185,9 @@
203 This will create an agent instance, parse the cli args, passes them to
204 the agent, and starts the agent runner.
205 """
206- self.change_args("es-agent", "--zookeeper-servers",
207- get_test_zookeeper_address())
208+ self.change_args(
209+ "es-agent", "--zookeeper-servers", get_test_zookeeper_address(),
210+ "--session-file", self.makeFile())
211 runner = self.mocker.patch(AgentRunner)
212 runner.run()
213 mock_agent = self.mocker.patch(BaseAgent)
214@@ -220,7 +226,8 @@
215
216 self.change_args(
217 "es-agent", "--nodaemon",
218- "--zookeeper-servers", get_test_zookeeper_address())
219+ "--zookeeper-servers", get_test_zookeeper_address(),
220+ "--session-file", self.makeFile())
221 runner = self.mocker.patch(AgentRunner)
222 logger = self.mocker.patch(AppLogger)
223 logger.start(MATCH_APP)
224@@ -230,6 +237,7 @@
225 DummyAgent.run()
226 return started
227
228+ @inlineCallbacks
229 def test_stop_service_stub_closes_agent(self):
230 """The base class agent, stopService will the stop method.
231
232@@ -238,6 +246,7 @@
233 """
234 mock_agent = self.mocker.patch(BaseAgent)
235 mock_client = self.mocker.mock(ZookeeperClient)
236+ session_file = self.makeFile()
237
238 # connection is closed after agent.stop invoked.
239 with self.mocker.order():
240@@ -259,11 +268,17 @@
241 self.mocker.result(mock_client)
242 mock_client.close()
243
244+ # delete session file
245+ mock_agent.config
246+ self.mocker.result({"session_file": session_file})
247+
248 self.mocker.replay()
249
250 agent = BaseAgent()
251- return agent.stopService()
252+ yield agent.stopService()
253+ self.assertFalse(os.path.exists(session_file))
254
255+ @inlineCallbacks
256 def test_stop_service_stub_ignores_disconnected_agent(self):
257 """The base class agent, stopService will the stop method.
258
259@@ -271,6 +286,7 @@
260 """
261 mock_agent = self.mocker.patch(BaseAgent)
262 mock_client = self.mocker.mock(ZookeeperClient)
263+ session_file = self.makeFile()
264
265 # connection is closed after agent.stop invoked.
266 with self.mocker.order():
267@@ -286,10 +302,14 @@
268 mock_client.connected
269 self.mocker.result(False)
270
271+ mock_agent.config
272+ self.mocker.result({"session_file": session_file})
273+
274 self.mocker.replay()
275
276 agent = BaseAgent()
277- return agent.stopService()
278+ yield agent.stopService()
279+ self.assertFalse(os.path.exists(session_file))
280
281 def test_run_base_raises_error(self):
282 """The base class agent, raises a notimplemented error when started."""
283@@ -297,12 +317,15 @@
284 client.connect(get_test_zookeeper_address())
285 client_mock = self.mocker.mock()
286 self.mocker.result(succeed(client_mock))
287+ client_mock.client_id
288+ self.mocker.result((123, "abc"))
289 self.mocker.replay()
290
291 agent = BaseAgent()
292 agent.configure({
293 "zookeeper_servers": get_test_zookeeper_address(),
294- "juju_directory": self.makeDir()})
295+ "juju_directory": self.makeDir(),
296+ "session_file": self.makeFile()})
297 d = agent.startService()
298 self.failUnlessFailure(d, NotImplementedError)
299 return d
300@@ -313,35 +336,43 @@
301 client = self.mocker.patch(ZookeeperClient)
302 client.connect("x2.example.com")
303 self.mocker.result(succeed(mock_client))
304+ mock_client.client_id
305+ self.mocker.result((123, "abc"))
306 self.mocker.replay()
307
308 agent = BaseAgent()
309 agent.configure({"zookeeper_servers": "x2.example.com",
310- "juju_directory": self.makeDir()})
311+ "juju_directory": self.makeDir(),
312+ "session_file": self.makeFile()})
313 result = agent.connect()
314 self.assertEqual(result.result, mock_client)
315 self.assertEqual(agent.client, mock_client)
316
317- def test_non_existant_directory(self):
318+ def test_nonexistent_directory(self):
319 """If the juju directory does not exist an error should be raised.
320 """
321 juju_directory = self.makeDir()
322 os.rmdir(juju_directory)
323 data = {"zookeeper_servers": get_test_zookeeper_address(),
324- "juju_directory": juju_directory}
325+ "juju_directory": juju_directory,
326+ "session_file": self.makeFile()}
327+ self.assertRaises(JujuError, BaseAgent().configure, data)
328
329- agent = BaseAgent()
330- self.assertRaises(
331- JujuError,
332- agent.configure,
333- data)
334+ def test_bad_session_file(self):
335+ """If the session file cannot be created an error should be raised.
336+ """
337+ data = {"zookeeper_servers": get_test_zookeeper_address(),
338+ "juju_directory": self.makeDir(),
339+ "session_file": None}
340+ self.assertRaises(JujuError, BaseAgent().configure, data)
341
342 def test_directory_cli_option(self):
343 """The juju directory can be configured on the cli."""
344 juju_directory = self.makeDir()
345 self.change_args(
346 "es-agent", "--zookeeper-servers", get_test_zookeeper_address(),
347- "--juju-directory", juju_directory)
348+ "--juju-directory", juju_directory,
349+ "--session-file", self.makeFile())
350
351 agent = BaseAgent()
352 parser = argparse.ArgumentParser()
353@@ -363,7 +394,9 @@
354 agent = BaseAgent()
355 parser = argparse.ArgumentParser()
356 agent.setup_options(parser)
357- options = parser.parse_args(namespace=TwistedOptionNamespace())
358+ options = parser.parse_args(
359+ ["--session-file", self.makeFile()],
360+ namespace=TwistedOptionNamespace())
361 agent.configure(options)
362 self.assertEqual(
363 agent.config["juju_directory"], juju_directory)
364@@ -379,6 +412,8 @@
365 client = self.mocker.patch(ZookeeperClient)
366 client.connect("x1.example.com")
367 self.mocker.result(succeed(client))
368+ client.client_id
369+ self.mocker.result((123, "abc"))
370 client.add_auth("digest", "admin:abc")
371 client.add_auth("digest", "agent:xyz")
372 client.exists("/")
373@@ -387,7 +422,105 @@
374 agent = BaseAgent()
375 parser = argparse.ArgumentParser()
376 agent.setup_options(parser)
377- options = parser.parse_args(namespace=TwistedOptionNamespace())
378+ options = parser.parse_args(
379+ ["--session-file", self.makeFile()],
380+ namespace=TwistedOptionNamespace())
381+ agent.configure(options)
382+ d = agent.startService()
383+ self.failUnlessFailure(d, NotImplementedError)
384+ return d
385+
386+ def test_connect_closes_running_session(self):
387+ self.change_args("es-agent")
388+ self.change_environment(
389+ JUJU_HOME=self.makeDir(),
390+ JUJU_ZOOKEEPER="x1.example.com")
391+
392+ session_file = self.makeFile()
393+ with open(session_file, "w") as f:
394+ f.write(yaml.dump((123, "abc")))
395+ mock_client_1 = self.mocker.mock()
396+ client = self.mocker.patch(ZookeeperClient)
397+ client.connect("x1.example.com", client_id=(123, "abc"))
398+ self.mocker.result(succeed(mock_client_1))
399+ mock_client_1.close()
400+ self.mocker.result(None)
401+
402+ mock_client_2 = self.mocker.mock()
403+ client.connect("x1.example.com")
404+ self.mocker.result(succeed(mock_client_2))
405+ mock_client_2.client_id
406+ self.mocker.result((456, "def"))
407+ self.mocker.replay()
408+
409+ agent = BaseAgent()
410+ parser = argparse.ArgumentParser()
411+ agent.setup_options(parser)
412+ options = parser.parse_args(
413+ ["--session-file", session_file],
414+ namespace=TwistedOptionNamespace())
415+ agent.configure(options)
416+ d = agent.startService()
417+ self.failUnlessFailure(d, NotImplementedError)
418+ return d
419+
420+ def test_connect_handles_expired_session(self):
421+ self.change_args("es-agent")
422+ self.change_environment(
423+ JUJU_HOME=self.makeDir(),
424+ JUJU_ZOOKEEPER="x1.example.com")
425+
426+ session_file = self.makeFile()
427+ with open(session_file, "w") as f:
428+ f.write(yaml.dump((123, "abc")))
429+ client = self.mocker.patch(ZookeeperClient)
430+ client.connect("x1.example.com", client_id=(123, "abc"))
431+ self.mocker.result(fail(zookeeper.SessionExpiredException()))
432+
433+ mock_client = self.mocker.mock()
434+ client.connect("x1.example.com")
435+ self.mocker.result(succeed(mock_client))
436+ mock_client.client_id
437+ self.mocker.result((456, "def"))
438+ self.mocker.replay()
439+
440+ agent = BaseAgent()
441+ parser = argparse.ArgumentParser()
442+ agent.setup_options(parser)
443+ options = parser.parse_args(
444+ ["--session-file", session_file],
445+ namespace=TwistedOptionNamespace())
446+ agent.configure(options)
447+ d = agent.startService()
448+ self.failUnlessFailure(d, NotImplementedError)
449+ return d
450+
451+ def test_connect_handles_nonsense_session(self):
452+ self.change_args("es-agent")
453+ self.change_environment(
454+ JUJU_HOME=self.makeDir(),
455+ JUJU_ZOOKEEPER="x1.example.com")
456+
457+ session_file = self.makeFile()
458+ with open(session_file, "w") as f:
459+ f.write(yaml.dump("cheesy wotsits"))
460+ client = self.mocker.patch(ZookeeperClient)
461+ client.connect("x1.example.com", client_id="cheesy wotsits")
462+ self.mocker.result(fail(zookeeper.ZooKeeperException()))
463+
464+ mock_client = self.mocker.mock()
465+ client.connect("x1.example.com")
466+ self.mocker.result(succeed(mock_client))
467+ mock_client.client_id
468+ self.mocker.result((456, "def"))
469+ self.mocker.replay()
470+
471+ agent = BaseAgent()
472+ parser = argparse.ArgumentParser()
473+ agent.setup_options(parser)
474+ options = parser.parse_args(
475+ ["--session-file", session_file],
476+ namespace=TwistedOptionNamespace())
477 agent.configure(options)
478 d = agent.startService()
479 self.failUnlessFailure(d, NotImplementedError)
480@@ -405,6 +538,21 @@
481 agent.set_watch_enabled(False)
482 self.assertFalse(agent.get_watch_enabled())
483
484+ @inlineCallbacks
485+ def test_session_file_permissions(self):
486+ session_file = self.makeFile()
487+ agent = DummyAgent()
488+ agent.configure({
489+ "session_file": session_file,
490+ "juju_directory": self.makeDir(),
491+ "zookeeper_servers": get_test_zookeeper_address()})
492+ yield agent.startService()
493+ mode = os.stat(session_file).st_mode
494+ mask = stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO
495+ self.assertEquals(mode & mask, stat.S_IRUSR | stat.S_IWUSR)
496+ yield agent.stopService()
497+ self.assertFalse(os.path.exists(session_file))
498+
499
500 class AgentDebugLogSettingsWatch(AgentTestBase):
501
502
503=== modified file 'juju/agents/tests/test_machine.py'
504--- juju/agents/tests/test_machine.py 2011-10-05 13:59:44 +0000
505+++ juju/agents/tests/test_machine.py 2012-02-21 12:19:20 +0000
506@@ -120,10 +120,9 @@
507 # initially setup by get_agent_config in setUp
508 self.change_environment(JUJU_MACHINE_ID="")
509 self.change_args("es-agent",
510- "--zookeeper-servers",
511- get_test_zookeeper_address(),
512- "--juju-directory",
513- self.makeDir())
514+ "--zookeeper-servers", get_test_zookeeper_address(),
515+ "--juju-directory", self.makeDir(),
516+ "--session-file", self.makeFile())
517 parser = argparse.ArgumentParser()
518 self.agent.setup_options(parser)
519 options = parser.parse_args(namespace=TwistedOptionNamespace())
520
521=== modified file 'juju/agents/tests/test_unit.py'
522--- juju/agents/tests/test_unit.py 2012-02-21 12:19:19 +0000
523+++ juju/agents/tests/test_unit.py 2012-02-21 12:19:20 +0000
524@@ -186,7 +186,8 @@
525 self.change_args(
526 "unit-agent",
527 "--juju-directory", self.makeDir(),
528- "--zookeeper-servers", get_test_zookeeper_address())
529+ "--zookeeper-servers", get_test_zookeeper_address(),
530+ "--session-file", self.makeFile())
531
532 parser = argparse.ArgumentParser()
533 self.agent.setup_options(parser)
534@@ -218,6 +219,7 @@
535 options = {}
536 options["juju_directory"] = self.juju_directory
537 options["zookeeper_servers"] = get_test_zookeeper_address()
538+ options["session_file"] = self.makeFile()
539 options["unit_name"] = "rabbit-1"
540 agent = self.agent_class()
541 agent.configure(options)
542
543=== modified file 'juju/agents/unit.py'
544--- juju/agents/unit.py 2012-02-21 12:19:19 +0000
545+++ juju/agents/unit.py 2012-02-21 12:19:20 +0000
546@@ -78,10 +78,11 @@
547 self.config["juju_directory"], "state")
548
549 # Setup the server portion of the cli api exposed to hooks.
550+ socket_path = os.path.join(self.unit_directory, HOOK_SOCKET_FILE)
551+ if os.path.exists(socket_path):
552+ os.unlink(socket_path)
553 from twisted.internet import reactor
554- self.api_socket = reactor.listenUNIX(
555- os.path.join(self.unit_directory, HOOK_SOCKET_FILE),
556- self.api_factory)
557+ self.api_socket = reactor.listenUNIX(socket_path, self.api_factory)
558
559 # Setup the unit state's address
560 address = yield get_unit_address(self.client)
561@@ -96,6 +97,10 @@
562 # Inform the system, we're alive.
563 yield self.unit_state.connect_agent()
564
565+ # Start paying attention to the debug-log setting
566+ if self.get_watch_enabled():
567+ yield self.unit_state.watch_hook_debug(self.cb_watch_hook_debug)
568+
569 self.lifecycle = UnitLifecycle(
570 self.client, self.unit_state, self.service_state,
571 self.unit_directory, self.state_directory, self.executor)
572
573=== modified file 'juju/control/tests/test_status.py'
574--- juju/control/tests/test_status.py 2012-02-21 12:19:19 +0000
575+++ juju/control/tests/test_status.py 2012-02-21 12:19:20 +0000
576@@ -276,6 +276,7 @@
577 options = TwistedOptionNamespace()
578 options["juju_directory"] = self.makeDir()
579 options["zookeeper_servers"] = get_test_zookeeper_address()
580+ options["session_file"] = self.makeFile()
581 options["machine_id"] = "0"
582 agent.configure(options)
583 agent.set_watch_enabled(False)
584
585=== modified file 'juju/machine/tests/test_unit_deployment.py'
586--- juju/machine/tests/test_unit_deployment.py 2012-02-21 12:19:19 +0000
587+++ juju/machine/tests/test_unit_deployment.py 2012-02-21 12:19:20 +0000
588@@ -131,7 +131,9 @@
589 self.deployment.directory, "charm.log")
590 command = " ".join([
591 "/usr/bin/python", "-m", "juju.agents.dummy", "--nodaemon",
592- "--logfile", log_file, ">> /tmp/juju-wordpress-0.output 2>&1"])
593+ "--logfile", log_file, "--session-file",
594+ "/var/run/juju/unit-wordpress-0-agent.zksession",
595+ ">> /tmp/juju-wordpress-0.output 2>&1"])
596 self.assertEquals(exec_, command)
597 d.addCallback(verify_upstart)
598 return d
599
600=== modified file 'juju/machine/unit.py'
601--- juju/machine/unit.py 2012-02-21 12:19:19 +0000
602+++ juju/machine/unit.py 2012-02-21 12:19:20 +0000
603@@ -73,7 +73,9 @@
604 self.service.set_command(" ".join((
605 "/usr/bin/python", "-m", self.unit_agent_module,
606 "--nodaemon",
607- "--logfile", os.path.join(self.directory, "charm.log"))))
608+ "--logfile", os.path.join(self.directory, "charm.log"),
609+ "--session-file",
610+ "/var/run/juju/unit-%s-agent.zksession" % self.unit_path_name)))
611 try:
612 yield self.service.start()
613 except ServiceError as e:
614@@ -227,7 +229,9 @@
615 "/usr/bin/python",
616 "-m", "juju.agents.unit",
617 "--nodaemon",
618- "--logfile", "/var/log/juju/unit-%s.log" % self.unit_path_name)))
619+ "--logfile", "/var/log/juju/unit-%s.log" % self.unit_path_name,
620+ "--session-file",
621+ "/var/run/juju/unit-%s-agent.zksession" % self.unit_path_name)))
622 yield service.install()
623
624 # Create symlinks on the host for easier access to the unit log files
625
626=== modified file 'juju/providers/common/cloudinit.py'
627--- juju/providers/common/cloudinit.py 2012-02-21 12:19:19 +0000
628+++ juju/providers/common/cloudinit.py 2012-02-21 12:19:20 +0000
629@@ -48,7 +48,8 @@
630 {"JUJU_MACHINE_ID": machine_id, "JUJU_ZOOKEEPER": zookeeper_hosts})
631 service.set_command(
632 "python -m juju.agents.machine --nodaemon "
633- "--logfile /var/log/juju/machine-agent.log")
634+ "--logfile /var/log/juju/machine-agent.log "
635+ "--session-file /var/run/juju/machine-agent.zksession")
636 return service.get_cloud_init_commands()
637
638
639@@ -58,7 +59,8 @@
640 service.set_environ({"JUJU_ZOOKEEPER": zookeeper_hosts})
641 service.set_command(
642 "python -m juju.agents.provision --nodaemon "
643- "--logfile /var/log/juju/provision-agent.log")
644+ "--logfile /var/log/juju/provision-agent.log "
645+ "--session-file /var/run/juju/provision-agent.zksession")
646 return service.get_cloud_init_commands()
647
648
649
650=== modified file 'juju/providers/common/tests/data/cloud_init_bootstrap'
651--- juju/providers/common/tests/data/cloud_init_bootstrap 2012-02-21 12:19:19 +0000
652+++ juju/providers/common/tests/data/cloud_init_bootstrap 2012-02-21 12:19:20 +0000
653@@ -28,7 +28,8 @@
654
655
656 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
657- >> /tmp/juju-machine-agent.output 2>&1
658+ --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
659+ 2>&1
660
661 EOF
662
663@@ -51,7 +52,8 @@
664
665
666 exec python -m juju.agents.provision --nodaemon --logfile /var/log/juju/provision-agent.log
667- >> /tmp/juju-provision-agent.output 2>&1
668+ --session-file /var/run/juju/provision-agent.zksession >> /tmp/juju-provision-agent.output
669+ 2>&1
670
671 EOF
672
673
674=== modified file 'juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers'
675--- juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers 2012-02-21 12:19:19 +0000
676+++ juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers 2012-02-21 12:19:20 +0000
677@@ -28,7 +28,8 @@
678
679
680 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
681- >> /tmp/juju-machine-agent.output 2>&1
682+ --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
683+ 2>&1
684
685 EOF
686
687@@ -51,7 +52,8 @@
688
689
690 exec python -m juju.agents.provision --nodaemon --logfile /var/log/juju/provision-agent.log
691- >> /tmp/juju-provision-agent.output 2>&1
692+ --session-file /var/run/juju/provision-agent.zksession >> /tmp/juju-provision-agent.output
693+ 2>&1
694
695 EOF
696
697
698=== modified file 'juju/providers/common/tests/data/cloud_init_branch'
699--- juju/providers/common/tests/data/cloud_init_branch 2012-02-21 12:19:19 +0000
700+++ juju/providers/common/tests/data/cloud_init_branch 2012-02-21 12:19:20 +0000
701@@ -30,7 +30,8 @@
702
703
704 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
705- >> /tmp/juju-machine-agent.output 2>&1
706+ --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
707+ 2>&1
708
709 EOF
710
711
712=== modified file 'juju/providers/common/tests/data/cloud_init_branch_trunk'
713--- juju/providers/common/tests/data/cloud_init_branch_trunk 2012-02-21 12:19:19 +0000
714+++ juju/providers/common/tests/data/cloud_init_branch_trunk 2012-02-21 12:19:20 +0000
715@@ -30,7 +30,8 @@
716
717
718 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
719- >> /tmp/juju-machine-agent.output 2>&1
720+ --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
721+ 2>&1
722
723 EOF
724
725
726=== modified file 'juju/providers/common/tests/data/cloud_init_distro'
727--- juju/providers/common/tests/data/cloud_init_distro 2012-02-21 12:19:19 +0000
728+++ juju/providers/common/tests/data/cloud_init_distro 2012-02-21 12:19:20 +0000
729@@ -26,7 +26,8 @@
730
731
732 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
733- >> /tmp/juju-machine-agent.output 2>&1
734+ --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
735+ 2>&1
736
737 EOF
738
739
740=== modified file 'juju/providers/common/tests/data/cloud_init_ppa'
741--- juju/providers/common/tests/data/cloud_init_ppa 2012-02-21 12:19:19 +0000
742+++ juju/providers/common/tests/data/cloud_init_ppa 2012-02-21 12:19:20 +0000
743@@ -28,7 +28,8 @@
744
745
746 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
747- >> /tmp/juju-machine-agent.output 2>&1
748+ --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
749+ 2>&1
750
751 EOF
752
753
754=== modified file 'juju/providers/ec2/tests/data/bootstrap_cloud_init'
755--- juju/providers/ec2/tests/data/bootstrap_cloud_init 2012-02-21 12:19:19 +0000
756+++ juju/providers/ec2/tests/data/bootstrap_cloud_init 2012-02-21 12:19:20 +0000
757@@ -28,7 +28,8 @@
758
759
760 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
761- >> /tmp/juju-machine-agent.output 2>&1
762+ --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
763+ 2>&1
764
765 EOF
766
767@@ -51,7 +52,8 @@
768
769
770 exec python -m juju.agents.provision --nodaemon --logfile /var/log/juju/provision-agent.log
771- >> /tmp/juju-provision-agent.output 2>&1
772+ --session-file /var/run/juju/provision-agent.zksession >> /tmp/juju-provision-agent.output
773+ 2>&1
774
775 EOF
776
777
778=== modified file 'juju/providers/ec2/tests/data/launch_cloud_init'
779--- juju/providers/ec2/tests/data/launch_cloud_init 2012-02-21 12:19:19 +0000
780+++ juju/providers/ec2/tests/data/launch_cloud_init 2012-02-21 12:19:20 +0000
781@@ -26,7 +26,8 @@
782
783
784 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
785- >> /tmp/juju-machine-agent.output 2>&1
786+ --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
787+ 2>&1
788
789 EOF
790
791
792=== modified file 'juju/providers/ec2/tests/data/launch_cloud_init_branch'
793--- juju/providers/ec2/tests/data/launch_cloud_init_branch 2012-02-21 12:19:19 +0000
794+++ juju/providers/ec2/tests/data/launch_cloud_init_branch 2012-02-21 12:19:20 +0000
795@@ -30,7 +30,8 @@
796
797
798 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
799- >> /tmp/juju-machine-agent.output 2>&1
800+ --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
801+ 2>&1
802
803 EOF
804
805
806=== modified file 'juju/providers/ec2/tests/data/launch_cloud_init_ppa'
807--- juju/providers/ec2/tests/data/launch_cloud_init_ppa 2012-02-21 12:19:19 +0000
808+++ juju/providers/ec2/tests/data/launch_cloud_init_ppa 2012-02-21 12:19:20 +0000
809@@ -28,7 +28,8 @@
810
811
812 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
813- >> /tmp/juju-machine-agent.output 2>&1
814+ --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
815+ 2>&1
816
817 EOF
818
819
820=== modified file 'juju/providers/local/agent.py'
821--- juju/providers/local/agent.py 2012-02-21 12:19:19 +0000
822+++ juju/providers/local/agent.py 2012-02-21 12:19:20 +0000
823@@ -49,7 +49,9 @@
824 self._service.set_environ(env)
825 self._service_args = [
826 "/usr/bin/python", "-m", self.agent_module,
827- "--nodaemon", "--logfile", log_file]
828+ "--nodaemon", "--logfile", log_file,
829+ "--session-file",
830+ "/var/run/juju/%s-machine-agent.zksession" % juju_unit_namespace]
831
832 @property
833 def juju_origin(self):
834
835=== modified file 'juju/providers/local/tests/test_agent.py'
836--- juju/providers/local/tests/test_agent.py 2012-02-21 12:19:19 +0000
837+++ juju/providers/local/tests/test_agent.py 2012-02-21 12:19:20 +0000
838@@ -75,6 +75,7 @@
839
840 expect_exec = (
841 "/usr/bin/python -m juju.agents.machine --nodaemon --logfile %s "
842+ "--session-file /var/run/juju/ns1-machine-agent.zksession "
843 ">> /tmp/juju-ns1-machine-agent.output 2>&1"
844 % log_file)
845 self.assertEquals(exec_, expect_exec)
846
847=== modified file 'juju/providers/orchestra/tests/data/bootstrap_user_data'
848--- juju/providers/orchestra/tests/data/bootstrap_user_data 2012-02-21 12:19:19 +0000
849+++ juju/providers/orchestra/tests/data/bootstrap_user_data 2012-02-21 12:19:20 +0000
850@@ -27,7 +27,8 @@
851
852
853 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
854- >> /tmp/juju-machine-agent.output 2>&1
855+ --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
856+ 2>&1
857
858 EOF
859
860@@ -50,7 +51,8 @@
861
862
863 exec python -m juju.agents.provision --nodaemon --logfile /var/log/juju/provision-agent.log
864- >> /tmp/juju-provision-agent.output 2>&1
865+ --session-file /var/run/juju/provision-agent.zksession >> /tmp/juju-provision-agent.output
866+ 2>&1
867
868 EOF
869
870
871=== modified file 'juju/providers/orchestra/tests/data/launch_user_data'
872--- juju/providers/orchestra/tests/data/launch_user_data 2012-02-21 12:19:19 +0000
873+++ juju/providers/orchestra/tests/data/launch_user_data 2012-02-21 12:19:20 +0000
874@@ -25,7 +25,8 @@
875
876
877 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
878- >> /tmp/juju-machine-agent.output 2>&1
879+ --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
880+ 2>&1
881
882 EOF
883
884
885=== modified file 'juju/state/tests/test_security.py'
886--- juju/state/tests/test_security.py 2011-12-08 20:17:21 +0000
887+++ juju/state/tests/test_security.py 2012-02-21 12:19:20 +0000
888@@ -67,6 +67,7 @@
889 content, stat = yield client.get("/acl-test")
890 self.assertEqual(content, "content")
891
892+
893 class GroupPrincipalTests(TestCase):
894
895 @inlineCallbacks
896
897=== modified file 'juju/unit/lifecycle.py'
898--- juju/unit/lifecycle.py 2012-02-21 12:19:19 +0000
899+++ juju/unit/lifecycle.py 2012-02-21 12:19:20 +0000
900@@ -145,10 +145,6 @@
901 watches = []
902
903 try:
904- # Verify current state
905- assert not self._running, "Already started"
906-
907- # Execute the start hook
908 if fire_hooks:
909 yield self._execute_hook("config-changed")
910 yield self._execute_hook("start")
911
912=== modified file 'juju/unit/tests/test_workflow.py'
913--- juju/unit/tests/test_workflow.py 2012-02-21 12:19:19 +0000
914+++ juju/unit/tests/test_workflow.py 2012-02-21 12:19:20 +0000
915@@ -522,7 +522,6 @@
916 yield self.assert_transition("upgrade_charm")
917 yield self.assert_state("started")
918 yield self.assert_charm_upgraded(True)
919-
920 self.assert_hooks(
921 "install", "config-changed", "start", "upgrade-charm")
922 yield self.assert_history_concise(
923@@ -530,12 +529,14 @@
924 ("start", "started"),
925 ("upgrade_charm", "started"))
926
927+
928 @inlineCallbacks
929 def test_upgrade_error_retry(self):
930 """A hook error during an upgrade transitions to
931 upgrade_error.
932 """
933 yield self.assert_transition("install")
934+ self.write_exit_hook("upgrade-charm", 1)
935 yield self.assert_state("started")
936 yield self.ready_upgrade(True)
937
938@@ -543,7 +544,8 @@
939 yield self.assert_transition("upgrade_charm", False)
940 yield self.assert_state("charm_upgrade_error")
941 self.assertFalse(self.executor.running)
942- # The upgrade should complete before the hook blows up.
943+
944+ # The upgrade should have completed before the hook blew up.
945 yield self.assert_charm_upgraded(True)
946
947 # The bad hook is still in place, but we don't run it again
948@@ -551,7 +553,6 @@
949 yield self.assert_state("started")
950 yield self.assert_charm_upgraded(True)
951 self.assertTrue(self.executor.running)
952-
953 self.assert_hooks(
954 "install", "config-changed", "start", "upgrade-charm")
955 yield self.assert_history_concise(
956@@ -568,12 +569,12 @@
957 yield self.assert_transition("install")
958 yield self.assert_state("started")
959 yield self.ready_upgrade(True)
960-
961 yield self.assert_charm_upgraded(False)
962 yield self.assert_transition("upgrade_charm", False)
963 yield self.assert_state("charm_upgrade_error")
964 self.assertFalse(self.executor.running)
965- # The upgrade should complete before the hook blows up.
966+
967+ # The upgrade should have completed before the hook blew up.
968 yield self.assert_charm_upgraded(True)
969
970 yield self.assert_transition("retry_upgrade_charm_hook", False)

Subscribers

People subscribed via source and target branches

to status/vote changes: