Merge lp:~fwereade/pyjuju/restore-unit-relation-nodes into lp:pyjuju

Proposed by William Reade
Status: Superseded
Proposed branch: lp:~fwereade/pyjuju/restore-unit-relation-nodes
Merge into: lp:pyjuju
Diff against target: 7460 lines (+3429/-1783)
54 files modified
docs/source/internals/unit-agent-persistence.rst (+138/-0)
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 (+174/-29)
juju/agents/tests/test_machine.py (+3/-4)
juju/agents/tests/test_unit.py (+71/-136)
juju/agents/unit.py (+42/-114)
juju/control/options.py (+4/-5)
juju/control/status.py (+2/-0)
juju/control/tests/test_resolved.py (+4/-4)
juju/control/tests/test_status.py (+3/-1)
juju/errors.py (+14/-1)
juju/hooks/scheduler.py (+102/-48)
juju/hooks/tests/test_scheduler.py (+258/-41)
juju/lib/lxc/tests/test_lxc.py (+12/-5)
juju/lib/tests/data/test_basic_install (+10/-0)
juju/lib/tests/data/test_less_basic_install (+11/-0)
juju/lib/tests/data/test_standard_install (+10/-0)
juju/lib/tests/test_statemachine.py (+14/-0)
juju/lib/tests/test_upstart.py (+339/-0)
juju/lib/upstart.py (+166/-0)
juju/machine/tests/test_unit_deployment.py (+192/-238)
juju/machine/unit.py (+101/-190)
juju/providers/common/cloudinit.py (+19/-12)
juju/providers/common/tests/data/cloud_init_bootstrap (+54/-8)
juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers (+54/-9)
juju/providers/common/tests/data/cloud_init_branch (+29/-7)
juju/providers/common/tests/data/cloud_init_branch_trunk (+29/-7)
juju/providers/common/tests/data/cloud_init_distro (+28/-6)
juju/providers/common/tests/data/cloud_init_ppa (+29/-7)
juju/providers/common/tests/test_cloudinit.py (+3/-2)
juju/providers/ec2/tests/data/bootstrap_cloud_init (+56/-11)
juju/providers/ec2/tests/data/launch_cloud_init (+28/-6)
juju/providers/ec2/tests/data/launch_cloud_init_branch (+30/-11)
juju/providers/ec2/tests/data/launch_cloud_init_ppa (+28/-6)
juju/providers/local/__init__.py (+5/-9)
juju/providers/local/agent.py (+32/-88)
juju/providers/local/files.py (+53/-39)
juju/providers/local/tests/test_agent.py (+69/-44)
juju/providers/local/tests/test_files.py (+136/-21)
juju/providers/orchestra/tests/data/bootstrap_user_data (+53/-8)
juju/providers/orchestra/tests/data/launch_user_data (+27/-5)
juju/state/relation.py (+35/-30)
juju/state/service.py (+10/-10)
juju/state/tests/test_relation.py (+312/-391)
juju/state/tests/test_security.py (+1/-0)
juju/tests/test_errors.py (+37/-9)
juju/unit/lifecycle.py (+173/-72)
juju/unit/tests/test_lifecycle.py (+116/-38)
juju/unit/tests/test_workflow.py (+196/-76)
juju/unit/workflow.py (+54/-28)
To merge this branch: bzr merge lp:~fwereade/pyjuju/restore-unit-relation-nodes
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+91307@code.launchpad.net

This proposal has been superseded by a proposal from 2012-02-02.

Description of the change

When reconstructing unit relation state in UnitLifecycle, ensure presence nodes are created for any relations which are not already departed.

To post a comment you must log in.
502. By William Reade

excise amusing print; expand testing a little

503. By William Reade

er, excise the other print

504. By William Reade

merge parent

505. By William Reade

merge parent

506. By William Reade

switch parent to trunk

507. By William Reade

merge parent

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'docs/source/internals/unit-agent-persistence.rst'
--- docs/source/internals/unit-agent-persistence.rst 1970-01-01 00:00:00 +0000
+++ docs/source/internals/unit-agent-persistence.rst 2012-02-02 16:42:42 +0000
@@ -0,0 +1,138 @@
1Notes on unit agent persistence
2===============================
3
4Introduction
5------------
6
7This was first written to explain the extensive changes made in the branch
8lp:~fwereade/juju/restart-transitions; that branch has been split out into
9four separate branches, but this discussion should remain a useful guide to
10the changes made to the unit and relation workflows and lifecycles in the
11course of making the unit agent resumable.
12
13
14Glossary
15--------
16
17UA = UnitAgent
18UL = UnitLifecycle
19UWS = UnitWorkflowState
20URL = UnitRelationLifecycle
21RWS = RelationWorkflowState
22URS = UnitRelationState
23SRS = ServiceRelationState
24
25
26Technical discussion
27--------------------
28
29Probably the most fundamental change is the addition of a "synchronize" method
30to both UWS and RWS. Calling "synchronize" should generally be *all* you need
31to do to put the workflow and associated components into "the right state"; ie
32ZK state will be restored, the appropriate lifecycle will be started (or not),
33and any initial transitons will automatically be fired ("start" for RWS;
34"install", "start" for UWS).
35
36The synchronize method keeps responsibility for the lifecycle's state purely in
37the hands of the workflow; once a workflow is synced, the *only* necessary
38interactions with it should be in response to changes in ZK.
39
40The disadvantage is that lifecycle "start" and "stop" methods have become a
41touch overloaded:
42
43* UL.stop(): now takes "stop_relations" in addition to "fire_hooks", in which
44 "stop_relations" being True causes the orginal behaviour (transition "up"
45 RWSs to "down", as when transitioning the UWS to a "stopped" or "error"
46 state), but False simply causes them to stop watching for changes (in
47 preparation for an orderly shutdown, for example).
48
49* UL.start(): now takes "start_relations" in addition to "fire_hooks", in which
50 the "start_relations" flag being True causes the original behaviour
51 (automatically transition "down" RWSs to "up", as when restarting/resolving
52 the UWS), while False causes the RWSs only to be synced.
53
54* URL.start(): now takes "scheduler" in addition to "watches", allowing the
55 watching and the contained HookScheduler to be controlled separately
56 (allowing us to actually perform the RWS synchronise correctly).
57
58* URL.stop(): still just takes "watches", because there wasn't a scenario in
59 which I wanted to stop the watches but not the HookScheduler.
60
61I still think it's a win, though: and I don't think that turning them into
62separate methods is the right way to go; "start" and "stop" remain perfectly
63decent and appropriate names for what they do.
64
65Now this has been done, we can always launch directly into whatever state we
66shut down in, and that's great, because sudden process death doesn't hurt us
67any more [0] [1]. Except... when we're upgrading a charm. It emerges that the
68charm upgrade state transition only covers the process of firing the hook, and
69not the process of actually upgrading the charm.
70
71In short, we had a mechanism, completely outside the workflow's purview, for
72potentially *brutal* modifications of state (both in terms of the charm itself,
73on disk, and also in that the hook executor should remain stopped forever while
74in "charm_upgrade_error" state); and this rather scuppered the "restart in the
75same state" goal. The obvious thing to do was to move the charm upgrade
76operation into the "charm_upgrade" transition, so we had a *chance* of being
77able to start in the correct state.
78
79UL.upgrade_charm, called by UWS, does itself have subtleties, but it should be
80reasonably clear when examined in context; the most important point is that it
81will call back at the start and end of the risky period, and that the UWS's
82handler for this callback sets a flag in "started"'s state_vars for the
83duration of the upgrade. If that flag is set when we subsequently start up
84again and synchronize the UWS, then we know to immediately force the
85charm_upgrade_error state and work from there.
86
87[0] Well, it does, because we need to persist more than just the (already-
88persisted) workflow state. This branch includes RWS persistence in the UL, as
89requested in this branch's first pre-review (back in the day...), but does not
90include HookScheduler persistence in the URLs, so it remains possible for
91relation hooks which have been queued, but not yet executed, to be lost if the
92process executes before the queue empties. That will be coming in another
93branch (resolve-unit-relation-diffs).
94
95[1] This seems like a good time to mention the UL's relation-broken handling
96for relations that went away while the process was stopped: every time
97._relations is changed, it writes out enough state to recreate a Frankenstein's
98URS object, which it can then use on load to reconstruct the necessary URL and
99hence RWS.
100
101We don't strictly need to *reconstruct* it in every case -- we can just use
102SRS.get_unit_state if the relation still exists -- but given that sometimes we
103do, it seemed senseless to have two code paths for the same operations. Of the
104RWSs we reconstruct, those with existing SRSs will be synchronized (because we
105know it's safe to do so), and the remainder will be stored untouched (because
106we know that _process_service_changes will fire the "depart" transition for us
107before doing anything else... and the "relation-broken" hook will be executed
108in a DepartedRelationHookContext, which is rather restricted, and so shouldn't
109cause the Frankenstein's URS to hit state we can't be sure exists).
110
111
112Appendix: a rough history of changes to restart-transitions
113-----------------------------------------------------------
114
115* Add UWS transitions from "stopped" to "started", so that process restarts can
116 be made to restart UWSs.
117* Upon review, add RWS persistence to UL, to ensure we can't miss
118 relation-broken hooks; as part of this, as discussed, add
119 DepartedRelationHookContext in which to execute them.
120* Upon discussion, discover that original UWS "started" -> "stopped" behaviour
121 on process shutdown is not actually the desired behaviour (and that the
122 associated RWS "up" -> "down" shouldn't happen either.
123* Make changes to UL.start/stop, and add UWS/RWS.synchronize, to allow us to
124 shut down workflows cleanly without transitions and bring them up again in
125 the same state.
126* Discover that we don't have any other reason to transition UWS to "stopped";
127 to actually fire stop hooks at the right time, we need a more sophisticated
128 system (possibly involving the machine agent telling the unit agent to shut
129 itself down). Remove the newly-added "restart" transitions, because they're
130 meaningless now; ponder what good it does us to have a "stopped" state that
131 we never actually enter; chicken out of actually removing it.
132* Realise that charm upgrades do an end-run around the whole UWS mechanism, and
133 resolve to intergate them so I can actually detect upgrades left incomplete
134 due to process death.
135* Move charm upgrade operation from agent into UL; come to appreciate the
136 subtleties of the charm upgrade process; make necessary tweaks to
137 UL.upgrade_charm, and UWS, to allow for synchronization of incomplete
138 upgrades.
0139
=== modified file 'examples/oneiric/mysql/hooks/install'
--- examples/oneiric/mysql/hooks/install 2011-09-15 18:56:08 +0000
+++ examples/oneiric/mysql/hooks/install 2012-02-02 16:42:42 +0000
@@ -21,5 +21,5 @@
21juju-log "Editing my.cnf to allow listening on all interfaces"21juju-log "Editing my.cnf to allow listening on all interfaces"
22sed --in-place=old 's/127\.0\.0\.1/0.0.0.0/' /etc/mysql/my.cnf22sed --in-place=old 's/127\.0\.0\.1/0.0.0.0/' /etc/mysql/my.cnf
2323
24juju-log "Restarting mysql service"24juju-log "Stopping mysql service"
25service mysql restart25service mysql stop
2626
=== modified file 'examples/oneiric/mysql/hooks/start'
--- examples/oneiric/mysql/hooks/start 2011-02-03 01:23:43 +0000
+++ examples/oneiric/mysql/hooks/start 2012-02-02 16:42:42 +0000
@@ -1,1 +1,3 @@
1#!/bin/bash
2\ No newline at end of file1\ No newline at end of file
2#!/bin/bash
3juju-log "Starting mysql service"
4service mysql start || service mysql restart
35
=== modified file 'examples/oneiric/mysql/hooks/stop'
--- examples/oneiric/mysql/hooks/stop 2011-09-15 18:56:08 +0000
+++ examples/oneiric/mysql/hooks/stop 2012-02-02 16:42:42 +0000
@@ -1,3 +1,3 @@
1#!/bin/bash1#!/bin/bash
2juju-log "Stopping mysql service"2juju-log "Stopping mysql service"
3/etc/init.d/mysql stop3service mysql stop || true
44
=== modified file 'juju/agents/base.py'
--- juju/agents/base.py 2011-09-22 13:23:00 +0000
+++ juju/agents/base.py 2012-02-02 16:42:42 +0000
@@ -1,7 +1,9 @@
1import argparse1import argparse
2import os2import os
3import logging
4import stat
3import sys5import sys
4import logging6import yaml
57
6import zookeeper8import zookeeper
79
@@ -18,6 +20,23 @@
18from juju.state.environment import GlobalSettingsStateManager20from juju.state.environment import GlobalSettingsStateManager
1921
2022
23def load_client_id(path):
24 try:
25 with open(path) as f:
26 return yaml.load(f.read())
27 except IOError:
28 return None
29
30
31def save_client_id(path, client_id):
32 parent = os.path.dirname(path)
33 if not os.path.exists(parent):
34 os.makedirs(parent)
35 with open(path, "w") as f:
36 f.write(yaml.dump(client_id))
37 os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
38
39
21class TwistedOptionNamespace(object):40class TwistedOptionNamespace(object):
22 """41 """
23 An argparse namespace implementation that is compatible with twisted42 An argparse namespace implementation that is compatible with twisted
@@ -153,13 +172,40 @@
153 "Invalid juju-directory %r, does not exist." % (172 "Invalid juju-directory %r, does not exist." % (
154 options.get("juju_directory")))173 options.get("juju_directory")))
155174
175 if options["session_file"] is None:
176 raise JujuError("No session file specified")
177
156 self.config = options178 self.config = options
157179
158 @inlineCallbacks180 @inlineCallbacks
181 def _kill_existing_session(self):
182 try:
183 # We might have died suddenly, in which case the session may
184 # still be alive. If this is the case, shoot it in the head, so
185 # it doesn't interfere with our attempts to recreate our state.
186 # (We need to be able to recreate our state *anyway*, and it's
187 # much simpler to force ourselves to recreate it every time than
188 # it is to mess around partially recreating partial state.)
189 client_id = load_client_id(self.config["session_file"])
190 if client_id is None:
191 return
192 temp_client = yield ZookeeperClient().connect(
193 self.config["zookeeper_servers"], client_id=client_id)
194 yield temp_client.close()
195 except zookeeper.ZooKeeperException:
196 # We don't really care what went wrong; just that we're not able
197 # to connect using the old session, and therefore we should be ok
198 # to start a fresh one without transient state hanging around.
199 pass
200
201 @inlineCallbacks
159 def connect(self):202 def connect(self):
160 """Return an authenticated connection to the juju zookeeper."""203 """Return an authenticated connection to the juju zookeeper."""
161 hosts = self.config["zookeeper_servers"]204 yield self._kill_existing_session()
162 self.client = yield ZookeeperClient().connect(hosts)205 self.client = yield ZookeeperClient().connect(
206 self.config["zookeeper_servers"])
207 save_client_id(
208 self.config["session_file"], self.client.client_id)
163209
164 principals = self.config.get("principals", ())210 principals = self.config.get("principals", ())
165 for principal in principals:211 for principal in principals:
@@ -200,6 +246,9 @@
200 finally:246 finally:
201 if self.client and self.client.connected:247 if self.client and self.client.connected:
202 self.client.close()248 self.client.close()
249 session_file = self.config["session_file"]
250 if os.path.exists(session_file):
251 os.unlink(session_file)
203252
204 def set_watch_enabled(self, flag):253 def set_watch_enabled(self, flag):
205 """Set boolean flag for whether this agent should watching zookeeper.254 """Set boolean flag for whether this agent should watching zookeeper.
@@ -285,3 +334,7 @@
285 parser.add_argument(334 parser.add_argument(
286 "--juju-directory", default=juju_home, type=os.path.abspath,335 "--juju-directory", default=juju_home, type=os.path.abspath,
287 help="juju working directory ($JUJU_HOME)")336 help="juju working directory ($JUJU_HOME)")
337
338 parser.add_argument(
339 "--session-file", default=None, type=os.path.abspath,
340 help="like a pidfile, but for the zookeeper session id")
288341
=== modified file 'juju/agents/tests/common.py'
--- juju/agents/tests/common.py 2011-09-15 19:24:47 +0000
+++ juju/agents/tests/common.py 2012-02-02 16:42:42 +0000
@@ -55,6 +55,7 @@
55 options = TwistedOptionNamespace()55 options = TwistedOptionNamespace()
56 options["juju_directory"] = self.juju_directory56 options["juju_directory"] = self.juju_directory
57 options["zookeeper_servers"] = get_test_zookeeper_address()57 options["zookeeper_servers"] = get_test_zookeeper_address()
58 options["session_file"] = self.makeFile()
58 return succeed(options)59 return succeed(options)
5960
60 @inlineCallbacks61 @inlineCallbacks
6162
=== modified file 'juju/agents/tests/test_base.py'
--- juju/agents/tests/test_base.py 2011-09-15 18:50:23 +0000
+++ juju/agents/tests/test_base.py 2012-02-02 16:42:42 +0000
@@ -2,13 +2,14 @@
2import json2import json
3import logging3import logging
4import os4import os
5import stat
5import sys6import sys
67import yaml
78
8from twisted.application.app import AppLogger9from twisted.application.app import AppLogger
9from twisted.application.service import IService, IServiceCollection10from twisted.application.service import IService, IServiceCollection
10from twisted.internet.defer import (11from twisted.internet.defer import (
11 succeed, Deferred, inlineCallbacks, returnValue)12 fail, succeed, Deferred, inlineCallbacks, returnValue)
12from twisted.python.components import Componentized13from twisted.python.components import Componentized
13from twisted.python import log14from twisted.python import log
1415
@@ -20,6 +21,7 @@
2021
21from juju.agents.base import (22from juju.agents.base import (
22 BaseAgent, TwistedOptionNamespace, AgentRunner, AgentLogger)23 BaseAgent, TwistedOptionNamespace, AgentRunner, AgentLogger)
24from juju.agents.dummy import DummyAgent
23from juju.errors import NoConnection, JujuError25from juju.errors import NoConnection, JujuError
24from juju.lib.zklog import ZookeeperHandler26from juju.lib.zklog import ZookeeperHandler
2527
@@ -34,8 +36,8 @@
34 @inlineCallbacks36 @inlineCallbacks
35 def setUp(self):37 def setUp(self):
36 yield super(BaseAgentTest, self).setUp()38 yield super(BaseAgentTest, self).setUp()
37 self.change_environment(39 self.juju_home = self.makeDir()
38 JUJU_HOME=self.makeDir())40 self.change_environment(JUJU_HOME=self.juju_home)
3941
40 def test_as_app(self):42 def test_as_app(self):
41 """The agent class can be accessed as an application."""43 """The agent class can be accessed as an application."""
@@ -53,11 +55,10 @@
53 # Daemon group55 # Daemon group
54 self.assertEqual(56 self.assertEqual(
55 parser.get_default("logfile"), "%s.log" % BaseAgent.name)57 parser.get_default("logfile"), "%s.log" % BaseAgent.name)
56 self.assertEqual(58 self.assertEqual(parser.get_default("pidfile"), "")
57 parser.get_default("pidfile"), "%s.pid" % BaseAgent.name)
5859
59 self.assertEqual(parser.get_default("loglevel"), "DEBUG")60 self.assertEqual(parser.get_default("loglevel"), "DEBUG")
60 self.assertTrue(parser.get_default("nodaemon"))61 self.assertFalse(parser.get_default("nodaemon"))
61 self.assertEqual(parser.get_default("rundir"), ".")62 self.assertEqual(parser.get_default("rundir"), ".")
62 self.assertEqual(parser.get_default("chroot"), None)63 self.assertEqual(parser.get_default("chroot"), None)
63 self.assertEqual(parser.get_default("umask"), None)64 self.assertEqual(parser.get_default("umask"), None)
@@ -80,6 +81,8 @@
80 # Agent options81 # Agent options
81 self.assertEqual(parser.get_default("principals"), [])82 self.assertEqual(parser.get_default("principals"), [])
82 self.assertEqual(parser.get_default("zookeeper_servers"), "")83 self.assertEqual(parser.get_default("zookeeper_servers"), "")
84 self.assertEqual(parser.get_default("juju_directory"), self.juju_home)
85 self.assertEqual(parser.get_default("session_file"), None)
8386
84 def test_twistd_flags_correspond(self):87 def test_twistd_flags_correspond(self):
85 parser = argparse.ArgumentParser()88 parser = argparse.ArgumentParser()
@@ -87,11 +90,11 @@
87 args = [90 args = [
88 "--profile",91 "--profile",
89 "--savestats",92 "--savestats",
90 "--daemon"]93 "--nodaemon"]
9194
92 options = parser.parse_args(args, namespace=TwistedOptionNamespace())95 options = parser.parse_args(args, namespace=TwistedOptionNamespace())
93 self.assertEqual(options.get("savestats"), True)96 self.assertEqual(options.get("savestats"), True)
94 self.assertEqual(options.get("nodaemon"), False)97 self.assertEqual(options.get("nodaemon"), True)
95 self.assertEqual(options.get("profile"), True)98 self.assertEqual(options.get("profile"), True)
9699
97 def test_agent_logger(self):100 def test_agent_logger(self):
@@ -100,7 +103,8 @@
100 log_file_path = self.makeFile()103 log_file_path = self.makeFile()
101104
102 options = parser.parse_args(105 options = parser.parse_args(
103 ["--logfile", log_file_path], namespace=TwistedOptionNamespace())106 ["--logfile", log_file_path, "--session-file", self.makeFile()],
107 namespace=TwistedOptionNamespace())
104108
105 def match_observer(observer):109 def match_observer(observer):
106 return isinstance(observer.im_self, log.PythonLoggingObserver)110 return isinstance(observer.im_self, log.PythonLoggingObserver)
@@ -181,8 +185,9 @@
181 This will create an agent instance, parse the cli args, passes them to185 This will create an agent instance, parse the cli args, passes them to
182 the agent, and starts the agent runner.186 the agent, and starts the agent runner.
183 """187 """
184 self.change_args("es-agent", "--zookeeper-servers",188 self.change_args(
185 get_test_zookeeper_address())189 "es-agent", "--zookeeper-servers", get_test_zookeeper_address(),
190 "--session-file", self.makeFile())
186 runner = self.mocker.patch(AgentRunner)191 runner = self.mocker.patch(AgentRunner)
187 runner.run()192 runner.run()
188 mock_agent = self.mocker.patch(BaseAgent)193 mock_agent = self.mocker.patch(BaseAgent)
@@ -219,11 +224,10 @@
219224
220 started.addCallback(validate_started)225 started.addCallback(validate_started)
221226
222 pid_file = self.makeFile()
223 self.change_args(227 self.change_args(
224 "es-agent",228 "es-agent", "--nodaemon",
225 "--zookeeper-servers", get_test_zookeeper_address(),229 "--zookeeper-servers", get_test_zookeeper_address(),
226 "--pidfile", pid_file)230 "--session-file", self.makeFile())
227 runner = self.mocker.patch(AgentRunner)231 runner = self.mocker.patch(AgentRunner)
228 logger = self.mocker.patch(AppLogger)232 logger = self.mocker.patch(AppLogger)
229 logger.start(MATCH_APP)233 logger.start(MATCH_APP)
@@ -233,6 +237,7 @@
233 DummyAgent.run()237 DummyAgent.run()
234 return started238 return started
235239
240 @inlineCallbacks
236 def test_stop_service_stub_closes_agent(self):241 def test_stop_service_stub_closes_agent(self):
237 """The base class agent, stopService will the stop method.242 """The base class agent, stopService will the stop method.
238243
@@ -241,6 +246,7 @@
241 """246 """
242 mock_agent = self.mocker.patch(BaseAgent)247 mock_agent = self.mocker.patch(BaseAgent)
243 mock_client = self.mocker.mock(ZookeeperClient)248 mock_client = self.mocker.mock(ZookeeperClient)
249 session_file = self.makeFile()
244250
245 # connection is closed after agent.stop invoked.251 # connection is closed after agent.stop invoked.
246 with self.mocker.order():252 with self.mocker.order():
@@ -262,11 +268,17 @@
262 self.mocker.result(mock_client)268 self.mocker.result(mock_client)
263 mock_client.close()269 mock_client.close()
264270
271 # delete session file
272 mock_agent.config
273 self.mocker.result({"session_file": session_file})
274
265 self.mocker.replay()275 self.mocker.replay()
266276
267 agent = BaseAgent()277 agent = BaseAgent()
268 return agent.stopService()278 yield agent.stopService()
279 self.assertFalse(os.path.exists(session_file))
269280
281 @inlineCallbacks
270 def test_stop_service_stub_ignores_disconnected_agent(self):282 def test_stop_service_stub_ignores_disconnected_agent(self):
271 """The base class agent, stopService will the stop method.283 """The base class agent, stopService will the stop method.
272284
@@ -274,6 +286,7 @@
274 """286 """
275 mock_agent = self.mocker.patch(BaseAgent)287 mock_agent = self.mocker.patch(BaseAgent)
276 mock_client = self.mocker.mock(ZookeeperClient)288 mock_client = self.mocker.mock(ZookeeperClient)
289 session_file = self.makeFile()
277290
278 # connection is closed after agent.stop invoked.291 # connection is closed after agent.stop invoked.
279 with self.mocker.order():292 with self.mocker.order():
@@ -289,10 +302,14 @@
289 mock_client.connected302 mock_client.connected
290 self.mocker.result(False)303 self.mocker.result(False)
291304
305 mock_agent.config
306 self.mocker.result({"session_file": session_file})
307
292 self.mocker.replay()308 self.mocker.replay()
293309
294 agent = BaseAgent()310 agent = BaseAgent()
295 return agent.stopService()311 yield agent.stopService()
312 self.assertFalse(os.path.exists(session_file))
296313
297 def test_run_base_raises_error(self):314 def test_run_base_raises_error(self):
298 """The base class agent, raises a notimplemented error when started."""315 """The base class agent, raises a notimplemented error when started."""
@@ -300,12 +317,15 @@
300 client.connect(get_test_zookeeper_address())317 client.connect(get_test_zookeeper_address())
301 client_mock = self.mocker.mock()318 client_mock = self.mocker.mock()
302 self.mocker.result(succeed(client_mock))319 self.mocker.result(succeed(client_mock))
320 client_mock.client_id
321 self.mocker.result((123, "abc"))
303 self.mocker.replay()322 self.mocker.replay()
304323
305 agent = BaseAgent()324 agent = BaseAgent()
306 agent.configure({325 agent.configure({
307 "zookeeper_servers": get_test_zookeeper_address(),326 "zookeeper_servers": get_test_zookeeper_address(),
308 "juju_directory": self.makeDir()})327 "juju_directory": self.makeDir(),
328 "session_file": self.makeFile()})
309 d = agent.startService()329 d = agent.startService()
310 self.failUnlessFailure(d, NotImplementedError)330 self.failUnlessFailure(d, NotImplementedError)
311 return d331 return d
@@ -316,35 +336,43 @@
316 client = self.mocker.patch(ZookeeperClient)336 client = self.mocker.patch(ZookeeperClient)
317 client.connect("x2.example.com")337 client.connect("x2.example.com")
318 self.mocker.result(succeed(mock_client))338 self.mocker.result(succeed(mock_client))
339 mock_client.client_id
340 self.mocker.result((123, "abc"))
319 self.mocker.replay()341 self.mocker.replay()
320342
321 agent = BaseAgent()343 agent = BaseAgent()
322 agent.configure({"zookeeper_servers": "x2.example.com",344 agent.configure({"zookeeper_servers": "x2.example.com",
323 "juju_directory": self.makeDir()})345 "juju_directory": self.makeDir(),
346 "session_file": self.makeFile()})
324 result = agent.connect()347 result = agent.connect()
325 self.assertEqual(result.result, mock_client)348 self.assertEqual(result.result, mock_client)
326 self.assertEqual(agent.client, mock_client)349 self.assertEqual(agent.client, mock_client)
327350
328 def test_non_existant_directory(self):351 def test_nonexistent_directory(self):
329 """If the juju directory does not exist an error should be raised.352 """If the juju directory does not exist an error should be raised.
330 """353 """
331 juju_directory = self.makeDir()354 juju_directory = self.makeDir()
332 os.rmdir(juju_directory)355 os.rmdir(juju_directory)
333 data = {"zookeeper_servers": get_test_zookeeper_address(),356 data = {"zookeeper_servers": get_test_zookeeper_address(),
334 "juju_directory": juju_directory}357 "juju_directory": juju_directory,
358 "session_file": self.makeFile()}
359 self.assertRaises(JujuError, BaseAgent().configure, data)
335360
336 agent = BaseAgent()361 def test_bad_session_file(self):
337 self.assertRaises(362 """If the session file cannot be created an error should be raised.
338 JujuError,363 """
339 agent.configure,364 data = {"zookeeper_servers": get_test_zookeeper_address(),
340 data)365 "juju_directory": self.makeDir(),
366 "session_file": None}
367 self.assertRaises(JujuError, BaseAgent().configure, data)
341368
342 def test_directory_cli_option(self):369 def test_directory_cli_option(self):
343 """The juju directory can be configured on the cli."""370 """The juju directory can be configured on the cli."""
344 juju_directory = self.makeDir()371 juju_directory = self.makeDir()
345 self.change_args(372 self.change_args(
346 "es-agent", "--zookeeper-servers", get_test_zookeeper_address(),373 "es-agent", "--zookeeper-servers", get_test_zookeeper_address(),
347 "--juju-directory", juju_directory)374 "--juju-directory", juju_directory,
375 "--session-file", self.makeFile())
348376
349 agent = BaseAgent()377 agent = BaseAgent()
350 parser = argparse.ArgumentParser()378 parser = argparse.ArgumentParser()
@@ -366,7 +394,9 @@
366 agent = BaseAgent()394 agent = BaseAgent()
367 parser = argparse.ArgumentParser()395 parser = argparse.ArgumentParser()
368 agent.setup_options(parser)396 agent.setup_options(parser)
369 options = parser.parse_args(namespace=TwistedOptionNamespace())397 options = parser.parse_args(
398 ["--session-file", self.makeFile()],
399 namespace=TwistedOptionNamespace())
370 agent.configure(options)400 agent.configure(options)
371 self.assertEqual(401 self.assertEqual(
372 agent.config["juju_directory"], juju_directory)402 agent.config["juju_directory"], juju_directory)
@@ -382,6 +412,8 @@
382 client = self.mocker.patch(ZookeeperClient)412 client = self.mocker.patch(ZookeeperClient)
383 client.connect("x1.example.com")413 client.connect("x1.example.com")
384 self.mocker.result(succeed(client))414 self.mocker.result(succeed(client))
415 client.client_id
416 self.mocker.result((123, "abc"))
385 client.add_auth("digest", "admin:abc")417 client.add_auth("digest", "admin:abc")
386 client.add_auth("digest", "agent:xyz")418 client.add_auth("digest", "agent:xyz")
387 client.exists("/")419 client.exists("/")
@@ -390,7 +422,105 @@
390 agent = BaseAgent()422 agent = BaseAgent()
391 parser = argparse.ArgumentParser()423 parser = argparse.ArgumentParser()
392 agent.setup_options(parser)424 agent.setup_options(parser)
393 options = parser.parse_args(namespace=TwistedOptionNamespace())425 options = parser.parse_args(
426 ["--session-file", self.makeFile()],
427 namespace=TwistedOptionNamespace())
428 agent.configure(options)
429 d = agent.startService()
430 self.failUnlessFailure(d, NotImplementedError)
431 return d
432
433 def test_connect_closes_running_session(self):
434 self.change_args("es-agent")
435 self.change_environment(
436 JUJU_HOME=self.makeDir(),
437 JUJU_ZOOKEEPER="x1.example.com")
438
439 session_file = self.makeFile()
440 with open(session_file, "w") as f:
441 f.write(yaml.dump((123, "abc")))
442 mock_client_1 = self.mocker.mock()
443 client = self.mocker.patch(ZookeeperClient)
444 client.connect("x1.example.com", client_id=(123, "abc"))
445 self.mocker.result(succeed(mock_client_1))
446 mock_client_1.close()
447 self.mocker.result(None)
448
449 mock_client_2 = self.mocker.mock()
450 client.connect("x1.example.com")
451 self.mocker.result(succeed(mock_client_2))
452 mock_client_2.client_id
453 self.mocker.result((456, "def"))
454 self.mocker.replay()
455
456 agent = BaseAgent()
457 parser = argparse.ArgumentParser()
458 agent.setup_options(parser)
459 options = parser.parse_args(
460 ["--session-file", session_file],
461 namespace=TwistedOptionNamespace())
462 agent.configure(options)
463 d = agent.startService()
464 self.failUnlessFailure(d, NotImplementedError)
465 return d
466
467 def test_connect_handles_expired_session(self):
468 self.change_args("es-agent")
469 self.change_environment(
470 JUJU_HOME=self.makeDir(),
471 JUJU_ZOOKEEPER="x1.example.com")
472
473 session_file = self.makeFile()
474 with open(session_file, "w") as f:
475 f.write(yaml.dump((123, "abc")))
476 client = self.mocker.patch(ZookeeperClient)
477 client.connect("x1.example.com", client_id=(123, "abc"))
478 self.mocker.result(fail(zookeeper.SessionExpiredException()))
479
480 mock_client = self.mocker.mock()
481 client.connect("x1.example.com")
482 self.mocker.result(succeed(mock_client))
483 mock_client.client_id
484 self.mocker.result((456, "def"))
485 self.mocker.replay()
486
487 agent = BaseAgent()
488 parser = argparse.ArgumentParser()
489 agent.setup_options(parser)
490 options = parser.parse_args(
491 ["--session-file", session_file],
492 namespace=TwistedOptionNamespace())
493 agent.configure(options)
494 d = agent.startService()
495 self.failUnlessFailure(d, NotImplementedError)
496 return d
497
498 def test_connect_handles_nonsense_session(self):
499 self.change_args("es-agent")
500 self.change_environment(
501 JUJU_HOME=self.makeDir(),
502 JUJU_ZOOKEEPER="x1.example.com")
503
504 session_file = self.makeFile()
505 with open(session_file, "w") as f:
506 f.write(yaml.dump("cheesy wotsits"))
507 client = self.mocker.patch(ZookeeperClient)
508 client.connect("x1.example.com", client_id="cheesy wotsits")
509 self.mocker.result(fail(zookeeper.ZooKeeperException()))
510
511 mock_client = self.mocker.mock()
512 client.connect("x1.example.com")
513 self.mocker.result(succeed(mock_client))
514 mock_client.client_id
515 self.mocker.result((456, "def"))
516 self.mocker.replay()
517
518 agent = BaseAgent()
519 parser = argparse.ArgumentParser()
520 agent.setup_options(parser)
521 options = parser.parse_args(
522 ["--session-file", session_file],
523 namespace=TwistedOptionNamespace())
394 agent.configure(options)524 agent.configure(options)
395 d = agent.startService()525 d = agent.startService()
396 self.failUnlessFailure(d, NotImplementedError)526 self.failUnlessFailure(d, NotImplementedError)
@@ -408,6 +538,21 @@
408 agent.set_watch_enabled(False)538 agent.set_watch_enabled(False)
409 self.assertFalse(agent.get_watch_enabled())539 self.assertFalse(agent.get_watch_enabled())
410540
541 @inlineCallbacks
542 def test_session_file_permissions(self):
543 session_file = self.makeFile()
544 agent = DummyAgent()
545 agent.configure({
546 "session_file": session_file,
547 "juju_directory": self.makeDir(),
548 "zookeeper_servers": get_test_zookeeper_address()})
549 yield agent.startService()
550 mode = os.stat(session_file).st_mode
551 mask = stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO
552 self.assertEquals(mode & mask, stat.S_IRUSR | stat.S_IWUSR)
553 yield agent.stopService()
554 self.assertFalse(os.path.exists(session_file))
555
411556
412class AgentDebugLogSettingsWatch(AgentTestBase):557class AgentDebugLogSettingsWatch(AgentTestBase):
413558
414559
=== modified file 'juju/agents/tests/test_machine.py'
--- juju/agents/tests/test_machine.py 2011-10-05 13:59:44 +0000
+++ juju/agents/tests/test_machine.py 2012-02-02 16:42:42 +0000
@@ -120,10 +120,9 @@
120 # initially setup by get_agent_config in setUp120 # initially setup by get_agent_config in setUp
121 self.change_environment(JUJU_MACHINE_ID="")121 self.change_environment(JUJU_MACHINE_ID="")
122 self.change_args("es-agent",122 self.change_args("es-agent",
123 "--zookeeper-servers",123 "--zookeeper-servers", get_test_zookeeper_address(),
124 get_test_zookeeper_address(),124 "--juju-directory", self.makeDir(),
125 "--juju-directory",125 "--session-file", self.makeFile())
126 self.makeDir())
127 parser = argparse.ArgumentParser()126 parser = argparse.ArgumentParser()
128 self.agent.setup_options(parser)127 self.agent.setup_options(parser)
129 options = parser.parse_args(namespace=TwistedOptionNamespace())128 options = parser.parse_args(namespace=TwistedOptionNamespace())
130129
=== modified file 'juju/agents/tests/test_unit.py'
--- juju/agents/tests/test_unit.py 2011-12-16 09:23:31 +0000
+++ juju/agents/tests/test_unit.py 2012-02-02 16:42:42 +0000
@@ -3,10 +3,9 @@
3import os3import os
4import yaml4import yaml
55
6from twisted.internet.defer import (6from twisted.internet.defer import inlineCallbacks, returnValue
7 inlineCallbacks, returnValue, fail, Deferred)
87
9from juju.agents.unit import UnitAgent, CharmUpgradeOperation8from juju.agents.unit import UnitAgent
10from juju.agents.base import TwistedOptionNamespace9from juju.agents.base import TwistedOptionNamespace
11from juju.charm import get_charm_from_path10from juju.charm import get_charm_from_path
12from juju.charm.url import CharmURL11from juju.charm.url import CharmURL
@@ -74,8 +73,10 @@
74 "stop", "#!/bin/bash\necho stop >> %s" % output_file)73 "stop", "#!/bin/bash\necho stop >> %s" % output_file)
7574
76 for k in kw.keys():75 for k in kw.keys():
77 self.write_hook(k.replace("_", "-"),76 hook_name = k.replace("_", "-")
78 "#!/bin/bash\necho $0 >> %s" % output_file)77 self.write_hook(
78 hook_name,
79 "#!/bin/bash\necho %s >> %s" % (hook_name, output_file))
7980
80 return output_file81 return output_file
8182
@@ -183,7 +184,8 @@
183 self.change_args(184 self.change_args(
184 "unit-agent",185 "unit-agent",
185 "--juju-directory", self.makeDir(),186 "--juju-directory", self.makeDir(),
186 "--zookeeper-servers", get_test_zookeeper_address())187 "--zookeeper-servers", get_test_zookeeper_address(),
188 "--session-file", self.makeFile())
187189
188 parser = argparse.ArgumentParser()190 parser = argparse.ArgumentParser()
189 self.agent.setup_options(parser)191 self.agent.setup_options(parser)
@@ -215,6 +217,7 @@
215 options = {}217 options = {}
216 options["juju_directory"] = self.juju_directory218 options["juju_directory"] = self.juju_directory
217 options["zookeeper_servers"] = get_test_zookeeper_address()219 options["zookeeper_servers"] = get_test_zookeeper_address()
220 options["session_file"] = self.makeFile()
218 options["unit_name"] = "rabbit-1"221 options["unit_name"] = "rabbit-1"
219 agent = self.agent_class()222 agent = self.agent_class()
220 agent.configure(options)223 agent.configure(options)
@@ -568,6 +571,12 @@
568 self.makeDir(path=os.path.join(self.juju_directory, "charms"))571 self.makeDir(path=os.path.join(self.juju_directory, "charms"))
569572
570 @inlineCallbacks573 @inlineCallbacks
574 def wait_for_log(self, logger_name, message, level=logging.DEBUG):
575 output = self.capture_logging(logger_name, level=level)
576 while message not in output.getvalue():
577 yield self.sleep(0.1)
578
579 @inlineCallbacks
571 def mark_charm_upgrade(self):580 def mark_charm_upgrade(self):
572 # Create a new version of the charm581 # Create a new version of the charm
573 repository = self.increment_charm(self.charm)582 repository = self.increment_charm(self.charm)
@@ -592,158 +601,84 @@
592 yield self.assertState(self.agent.workflow, "started")601 yield self.assertState(self.agent.workflow, "started")
593602
594 @inlineCallbacks603 @inlineCallbacks
595 def test_agent_upgrade_watch_continues_on_unexpected_error(self):
596 """The agent watches for unit upgrades and continues if there is an
597 unexpected error."""
598 yield self.mark_charm_upgrade()
599 self.agent.set_watch_enabled(True)
600
601 output = self.capture_logging(
602 "juju.agents.unit", level=logging.DEBUG)
603
604 upgrade_done = Deferred()
605
606 def operation_has_run():
607 upgrade_done.callback(True)
608
609 operation = self.mocker.patch(CharmUpgradeOperation)
610 operation.run()
611
612 self.mocker.call(operation_has_run)
613 self.mocker.result(fail(ValueError("magic mouse")))
614 self.mocker.replay()
615
616 yield self.agent.startService()
617
618 yield upgrade_done
619 self.assertIn("Error while upgrading", output.getvalue())
620 self.assertIn("magic mouse", output.getvalue())
621
622 yield self.agent.workflow.fire_transition("stop")
623
624 @inlineCallbacks
625 def test_agent_upgrade(self):604 def test_agent_upgrade(self):
626 """The agent can succesfully upgrade its charm."""605 """The agent can succesfully upgrade its charm."""
627 self.agent.set_watch_enabled(False)606 log_written = self.wait_for_log("juju.agents.unit", "Upgrade complete")
628 yield self.agent.startService()
629
630 yield self.mark_charm_upgrade()
631
632 hook_done = self.wait_on_hook(607 hook_done = self.wait_on_hook(
633 "upgrade-charm", executor=self.agent.executor)608 "upgrade-charm", executor=self.agent.executor)
634 self.write_hook("upgrade-charm", "#!/bin/bash\nexit 0")609
635 output = self.capture_logging("unit.upgrade", level=logging.DEBUG)610 self.agent.set_watch_enabled(True)
636611 yield self.agent.startService()
637 # Do the upgrade612 yield self.mark_charm_upgrade()
638 upgrade = CharmUpgradeOperation(self.agent)
639 value = yield upgrade.run()
640
641 # Verify the upgrade.
642 self.assertIdentical(value, True)
643 self.assertIn("Unit upgraded", output.getvalue())
644 yield hook_done613 yield hook_done
614 yield log_written
645615
616 self.assertIdentical(
617 (yield self.states["unit"].get_upgrade_flag()),
618 False)
646 new_charm = get_charm_from_path(619 new_charm = get_charm_from_path(
647 os.path.join(self.agent.unit_directory, "charm"))620 os.path.join(self.agent.unit_directory, "charm"))
648
649 self.assertEqual(621 self.assertEqual(
650 self.charm.get_revision() + 1, new_charm.get_revision())622 self.charm.get_revision() + 1, new_charm.get_revision())
651623
652 @inlineCallbacks624 @inlineCallbacks
625 def test_agent_upgrade_version_current(self):
626 """If the unit is running the latest charm, do nothing."""
627 log_written = self.wait_for_log(
628 "juju.agents.unit",
629 "Upgrade ignored: already running latest charm")
630
631 old_charm_id = yield self.states["unit"].get_charm_id()
632 self.agent.set_watch_enabled(True)
633 yield self.agent.startService()
634 yield self.states["unit"].set_upgrade_flag()
635 yield log_written
636
637 self.assertIdentical(
638 (yield self.states["unit"].get_upgrade_flag()), False)
639 self.assertEquals(
640 (yield self.states["unit"].get_charm_id()), old_charm_id)
641
642
643 @inlineCallbacks
653 def test_agent_upgrade_bad_unit_state(self):644 def test_agent_upgrade_bad_unit_state(self):
654 """The an upgrade fails if the unit is in a bad state."""645 """The upgrade fails if the unit is in a bad state."""
655 self.agent.set_watch_enabled(False)
656 yield self.agent.startService()
657
658 # Upload a new version of the unit's charm646 # Upload a new version of the unit's charm
659 repository = self.increment_charm(self.charm)647 repository = self.increment_charm(self.charm)
660 charm = yield repository.find(CharmURL.parse("local:series/mysql"))648 charm = yield repository.find(CharmURL.parse("local:series/mysql"))
661 charm, charm_state = yield self.publish_charm(charm.path)649 charm, charm_state = yield self.publish_charm(charm.path)
650 old_charm_id = yield self.states["unit"].get_charm_id()
651
652 log_written = self.wait_for_log(
653 "juju.agents.unit",
654 "Cannot upgrade: unit is in non-started state configure_error. "
655 "Reissue upgrade command to try again.")
656 self.agent.set_watch_enabled(True)
657 yield self.agent.startService()
662658
663 # Mark the unit for upgrade, with an invalid state.659 # Mark the unit for upgrade, with an invalid state.
660 yield self.agent.workflow.fire_transition("error_configure")
664 yield self.states["service"].set_charm_id(charm_state.id)661 yield self.states["service"].set_charm_id(charm_state.id)
665 yield self.states["unit"].set_upgrade_flag()662 yield self.states["unit"].set_upgrade_flag()
666 yield self.agent.workflow.set_state("start_error")663 yield log_written
667664
668 output = self.capture_logging("unit.upgrade", level=logging.DEBUG)
669
670 # Do the upgrade
671 upgrade = CharmUpgradeOperation(self.agent)
672 value = yield upgrade.run()
673
674 # Verify the upgrade.
675 self.assertIdentical(value, False)
676 self.assertIn("Unit not in an upgradeable state: start_error",
677 output.getvalue())
678 self.assertIdentical(665 self.assertIdentical(
679 (yield self.states["unit"].get_upgrade_flag()),666 (yield self.states["unit"].get_upgrade_flag()), False)
680 False)667 self.assertEquals(
668 (yield self.states["unit"].get_charm_id()), old_charm_id)
681669
682 @inlineCallbacks670 @inlineCallbacks
683 def test_agent_upgrade_no_flag(self):671 def test_agent_upgrade_no_flag(self):
684 """An upgrade fails if there is no upgrade flag set."""672 """An upgrade stops if there is no upgrade flag set."""
685 self.agent.set_watch_enabled(False)673 log_written = self.wait_for_log(
686 yield self.agent.startService()674 "juju.agents.unit", "No upgrade flag set")
687 output = self.capture_logging("unit.upgrade", level=logging.DEBUG)675 old_charm_id = yield self.states["unit"].get_charm_id()
688 upgrade = CharmUpgradeOperation(self.agent)676 self.agent.set_watch_enabled(True)
689 value = yield upgrade.run()677 yield self.agent.startService()
690 self.assertIdentical(value, False)678 yield log_written
691 self.assertIn("No upgrade flag set", output.getvalue())679
692 yield self.agent.workflow.fire_transition("stop")680 self.assertIdentical(
693681 (yield self.states["unit"].get_upgrade_flag()),
694 @inlineCallbacks682 False)
695 def test_agent_upgrade_version_current(self):683 new_charm_id = yield self.states["unit"].get_charm_id()
696 """An upgrade fails if the unit is running the latest charm."""684 self.assertEquals(new_charm_id, old_charm_id)
697 self.agent.set_watch_enabled(False)
698 yield self.agent.startService()
699 yield self.states["unit"].set_upgrade_flag()
700 output = self.capture_logging("unit.upgrade", level=logging.DEBUG)
701 upgrade = CharmUpgradeOperation(self.agent)
702 value = yield upgrade.run()
703 self.assertIdentical(value, True)
704 self.assertIn("Unit already running latest charm", output.getvalue())
705 self.assertFalse((yield self.states["unit"].get_upgrade_flag()))
706
707 @inlineCallbacks
708 def test_agent_upgrade_hook_failure(self):
709 """An upgrade fails if the upgrade hook errors."""
710 self.agent.set_watch_enabled(False)
711 yield self.agent.startService()
712
713 # Upload a new version of the unit's charm
714 repository = self.increment_charm(self.charm)
715 charm = yield repository.find(CharmURL.parse("local:series/mysql"))
716 charm, charm_state = yield self.publish_charm(charm.path)
717
718 # Mark the unit for upgrade
719 yield self.states["service"].set_charm_id(charm_state.id)
720 yield self.states["unit"].set_upgrade_flag()
721
722 hook_done = self.wait_on_hook(
723 "upgrade-charm", executor=self.agent.executor)
724 self.write_hook("upgrade-charm", "#!/bin/bash\nexit 1")
725 output = self.capture_logging("unit.upgrade", level=logging.DEBUG)
726
727 # Do the upgrade
728 upgrade = CharmUpgradeOperation(self.agent)
729 value = yield upgrade.run()
730
731 # Verify the failed upgrade.
732 self.assertIdentical(value, False)
733 self.assertIn("Invoking upgrade transition", output.getvalue())
734 self.assertIn("Upgrade failed.", output.getvalue())
735 yield hook_done
736
737 # Verify state
738 workflow_state = yield self.agent.workflow.get_state()
739 self.assertEqual("charm_upgrade_error", workflow_state)
740
741 # Verify new charm is in place
742 new_charm = get_charm_from_path(
743 os.path.join(self.agent.unit_directory, "charm"))
744
745 self.assertEqual(
746 self.charm.get_revision() + 1, new_charm.get_revision())
747
748 # Verify upgrade flag is cleared.
749 self.assertFalse((yield self.states["unit"].get_upgrade_flag()))
750685
=== modified file 'juju/agents/unit.py'
--- juju/agents/unit.py 2012-01-10 14:14:28 +0000
+++ juju/agents/unit.py 2012-02-02 16:42:42 +0000
@@ -1,7 +1,5 @@
1import os1import os
2import logging2import logging
3import shutil
4import tempfile
53
6from twisted.internet.defer import inlineCallbacks, returnValue4from twisted.internet.defer import inlineCallbacks, returnValue
75
@@ -14,8 +12,6 @@
14from juju.unit.lifecycle import UnitLifecycle, HOOK_SOCKET_FILE12from juju.unit.lifecycle import UnitLifecycle, HOOK_SOCKET_FILE
15from juju.unit.workflow import UnitWorkflowState13from juju.unit.workflow import UnitWorkflowState
1614
17from juju.unit.charm import download_charm
18
19from juju.agents.base import BaseAgent15from juju.agents.base import BaseAgent
2016
2117
@@ -66,14 +62,14 @@
66 @inlineCallbacks62 @inlineCallbacks
67 def start(self):63 def start(self):
68 """Start the unit agent process."""64 """Start the unit agent process."""
69 self.service_state_manager = ServiceStateManager(self.client)65 service_state_manager = ServiceStateManager(self.client)
7066
71 # Retrieve our unit and configure working directories.67 # Retrieve our unit and configure working directories.
72 service_name = self.unit_name.split("/")[0]68 service_name = self.unit_name.split("/")[0]
73 service_state = yield self.service_state_manager.get_service_state(69 self.service_state = yield service_state_manager.get_service_state(
74 service_name)70 service_name)
7571
76 self.unit_state = yield service_state.get_unit_state(72 self.unit_state = yield self.service_state.get_unit_state(
77 self.unit_name)73 self.unit_name)
78 self.unit_directory = os.path.join(74 self.unit_directory = os.path.join(
79 self.config["juju_directory"], "units",75 self.config["juju_directory"], "units",
@@ -82,10 +78,11 @@
82 self.config["juju_directory"], "state")78 self.config["juju_directory"], "state")
8379
84 # Setup the server portion of the cli api exposed to hooks.80 # Setup the server portion of the cli api exposed to hooks.
81 socket_path = os.path.join(self.unit_directory, HOOK_SOCKET_FILE)
82 if os.path.exists(socket_path):
83 os.unlink(socket_path)
85 from twisted.internet import reactor84 from twisted.internet import reactor
86 self.api_socket = reactor.listenUNIX(85 self.api_socket = reactor.listenUNIX(socket_path, self.api_factory)
87 os.path.join(self.unit_directory, HOOK_SOCKET_FILE),
88 self.api_factory)
8986
90 # Setup the unit state's address87 # Setup the unit state's address
91 address = yield get_unit_address(self.client)88 address = yield get_unit_address(self.client)
@@ -100,9 +97,13 @@
100 # Inform the system, we're alive.97 # Inform the system, we're alive.
101 yield self.unit_state.connect_agent()98 yield self.unit_state.connect_agent()
10299
100 # Start paying attention to the debug-log setting
101 if self.get_watch_enabled():
102 yield self.unit_state.watch_hook_debug(self.cb_watch_hook_debug)
103
103 self.lifecycle = UnitLifecycle(104 self.lifecycle = UnitLifecycle(
104 self.client, self.unit_state, service_state, self.unit_directory,105 self.client, self.unit_state, self.service_state,
105 self.state_directory, self.executor)106 self.unit_directory, self.state_directory, self.executor)
106107
107 self.workflow = UnitWorkflowState(108 self.workflow = UnitWorkflowState(
108 self.client, self.unit_state, self.lifecycle, self.state_directory)109 self.client, self.unit_state, self.lifecycle, self.state_directory)
@@ -113,7 +114,7 @@
113114
114 if self.get_watch_enabled():115 if self.get_watch_enabled():
115 yield self.unit_state.watch_resolved(self.cb_watch_resolved)116 yield self.unit_state.watch_resolved(self.cb_watch_resolved)
116 yield service_state.watch_config_state(117 yield self.service_state.watch_config_state(
117 self.cb_watch_config_changed)118 self.cb_watch_config_changed)
118 yield self.unit_state.watch_upgrade_flag(119 yield self.unit_state.watch_upgrade_flag(
119 self.cb_watch_upgrade_flag)120 self.cb_watch_upgrade_flag)
@@ -175,13 +176,34 @@
175 """Update the unit's charm when requested.176 """Update the unit's charm when requested.
176 """177 """
177 upgrade_flag = yield self.unit_state.get_upgrade_flag()178 upgrade_flag = yield self.unit_state.get_upgrade_flag()
178 if upgrade_flag:179 if not upgrade_flag:
179 log.info("Upgrade detected, starting upgrade")180 log.info("No upgrade flag set.")
180 upgrade = CharmUpgradeOperation(self)181 return
181 try:182
182 yield upgrade.run()183 log.info("Upgrade detected")
183 except Exception:184 # Clear the flag immediately; this means that upgrade requests will
184 log.exception("Error while upgrading")185 # be *ignored* by units which are not "started", and will need to be
186 # reissued when the units are in acceptable states.
187 yield self.unit_state.clear_upgrade_flag()
188
189 new_id = yield self.service_state.get_charm_id()
190 old_id = yield self.unit_state.get_charm_id()
191 if new_id == old_id:
192 log.info("Upgrade ignored: already running latest charm")
193 return
194
195 state = yield self.workflow.get_state()
196 if state != "started":
197 log.warning(
198 "Cannot upgrade: unit is in non-started state %s. Reissue "
199 "upgrade command to try again.", state)
200 return
201
202 log.info("Starting upgrade")
203 if (yield self.workflow.fire_transition("upgrade_charm")):
204 log.info("Upgrade complete")
205 else:
206 log.info("Upgrade failed")
185207
186 @inlineCallbacks208 @inlineCallbacks
187 def cb_watch_config_changed(self, change):209 def cb_watch_config_changed(self, change):
@@ -198,99 +220,5 @@
198 yield self.workflow.fire_transition("reconfigure")220 yield self.workflow.fire_transition("reconfigure")
199221
200222
201class CharmUpgradeOperation(object):
202 """A unit agent charm upgrade operation."""
203
204 def __init__(self, agent):
205 self._agent = agent
206 self._log = logging.getLogger("unit.upgrade")
207 self._charm_directory = tempfile.mkdtemp(
208 suffix="charm-upgrade", prefix="tmp")
209
210 def retrieve_charm(self, charm_id):
211 return download_charm(
212 self._agent.client, charm_id, self._charm_directory)
213
214 def _remove_tree(self, result):
215 if os.path.exists(self._charm_directory):
216 shutil.rmtree(self._charm_directory)
217 return result
218
219 def run(self):
220 d = self._run()
221 d.addBoth(self._remove_tree)
222 return d
223
224 @inlineCallbacks
225 def _run(self):
226 self._log.info("Starting charm upgrade...")
227
228 # Verify the workflow state
229 workflow_state = yield self._agent.workflow.get_state()
230 if workflow_state != "started":
231 self._log.warning(
232 "Unit not in an upgradeable state: %s", workflow_state)
233 # Upgrades can only be supported while the unit is
234 # running, we clear the flag because we don't support
235 # persistent upgrade requests across unit starts. The
236 # upgrade request will need to be reissued, after
237 # resolving or restarting the unit.
238 yield self._agent.unit_state.clear_upgrade_flag()
239 returnValue(False)
240
241 # Get, check, and clear the flag. Do it first so a second upgrade
242 # will restablish the upgrade request.
243 upgrade_flag = yield self._agent.unit_state.get_upgrade_flag()
244 if not upgrade_flag:
245 self._log.warning("No upgrade flag set.")
246 returnValue(False)
247
248 self._log.debug("Clearing upgrade flag.")
249 yield self._agent.unit_state.clear_upgrade_flag()
250
251 # Retrieve the service state
252 service_state_manager = ServiceStateManager(self._agent.client)
253 service_state = yield service_state_manager.get_service_state(
254 self._agent.unit_name.split("/")[0])
255
256 # Verify unit state, upgrade flag, and newer version requested.
257 service_charm_id = yield service_state.get_charm_id()
258 unit_charm_id = yield self._agent.unit_state.get_charm_id()
259
260 if service_charm_id == unit_charm_id:
261 self._log.debug("Unit already running latest charm")
262 yield self._agent.unit_state.clear_upgrade_flag()
263 returnValue(True)
264
265 # Retrieve charm
266 self._log.debug("Retrieving charm %s", service_charm_id)
267 charm = yield self.retrieve_charm(service_charm_id)
268
269 # Stop hook executions
270 self._log.debug("Stopping hook execution.")
271 yield self._agent.executor.stop()
272
273 # Note the current charm version
274 self._log.debug("Setting unit charm id to %s", service_charm_id)
275 yield self._agent.unit_state.set_charm_id(service_charm_id)
276
277 # Extract charm
278 self._log.debug("Extracting new charm.")
279 charm.extract_to(
280 os.path.join(self._agent.unit_directory, "charm"))
281
282 # Upgrade
283 self._log.debug("Invoking upgrade transition.")
284
285 success = yield self._agent.workflow.fire_transition(
286 "upgrade_charm")
287
288 if success:
289 self._log.debug("Unit upgraded.")
290 else:
291 self._log.warning("Upgrade failed.")
292
293 returnValue(success)
294
295if __name__ == '__main__':223if __name__ == '__main__':
296 UnitAgent.run()224 UnitAgent.run()
297225
=== modified file 'juju/control/options.py'
--- juju/control/options.py 2011-01-20 18:00:23 +0000
+++ juju/control/options.py 2012-02-02 16:42:42 +0000
@@ -53,9 +53,8 @@
53 )53 )
5454
55 unix_group.add_argument(55 unix_group.add_argument(
56 "--pidfile", default="%s.pid" % agent.name,56 "--pidfile", default="",
57 help="Path to the pid file",57 help="Path to the pid file",
58 type=ensure_abs_path,
59 )58 )
6059
61 unix_group.add_argument(60 unix_group.add_argument(
@@ -91,9 +90,9 @@
91 )90 )
9291
93 unix_group.add_argument(92 unix_group.add_argument(
94 "--daemon", "-n", default=True,93 "--nodaemon", "-n", default=False,
95 dest="nodaemon", action="store_false",94 dest="nodaemon", action="store_true",
96 help="Daemonize the process",95 help="Don't daemonize (stay in foreground)",
97 )96 )
9897
99 unix_group.add_argument(98 unix_group.add_argument(
10099
=== modified file 'juju/control/status.py'
--- juju/control/status.py 2011-12-07 05:02:08 +0000
+++ juju/control/status.py 2012-02-02 16:42:42 +0000
@@ -216,8 +216,10 @@
216 relation_status = {}216 relation_status = {}
217 for relation in relations:217 for relation in relations:
218 try:218 try:
219 print unit.unit_name
219 relation_unit = yield relation.get_unit_state(unit)220 relation_unit = yield relation.get_unit_state(unit)
220 except UnitRelationStateNotFound:221 except UnitRelationStateNotFound:
222 print "POW SPLAT"
221 # This exception will occur when relations are223 # This exception will occur when relations are
222 # established between services without service224 # established between services without service
223 # units, and therefore never have any225 # units, and therefore never have any
224226
=== modified file 'juju/control/tests/test_resolved.py'
--- juju/control/tests/test_resolved.py 2012-01-12 10:18:07 +0000
+++ juju/control/tests/test_resolved.py 2012-02-02 16:42:42 +0000
@@ -88,10 +88,10 @@
88 """88 """
89 for unit, state in units:89 for unit, state in units:
90 unit_relation = yield service_relation.add_unit_state(unit)90 unit_relation = yield service_relation.add_unit_state(unit)
91 lifecycle = UnitRelationLifecycle(self.client,91 lifecycle = UnitRelationLifecycle(
92 unit.unit_name, unit_relation,92 self.client, unit.unit_name, unit_relation,
93 service_relation.relation_name,93 service_relation.relation_name, self.makeDir(), self.makeDir(),
94 self.makeDir(), self.executor)94 self.executor)
95 workflow_state = RelationWorkflowState(95 workflow_state = RelationWorkflowState(
96 self.client, unit_relation, service_relation.relation_name,96 self.client, unit_relation, service_relation.relation_name,
97 lifecycle, self.makeDir())97 lifecycle, self.makeDir())
9898
=== modified file 'juju/control/tests/test_status.py'
--- juju/control/tests/test_status.py 2011-12-07 18:29:12 +0000
+++ juju/control/tests/test_status.py 2012-02-02 16:42:42 +0000
@@ -39,7 +39,7 @@
39 # Status tests setup a large tree every time, make allowances for it.39 # Status tests setup a large tree every time, make allowances for it.
40 # TODO: create minimal trees needed per test.40 # TODO: create minimal trees needed per test.
41 timeout = 1041 timeout = 10
42 42
43 @inlineCallbacks43 @inlineCallbacks
44 def setUp(self):44 def setUp(self):
45 yield super(StatusTestBase, self).setUp()45 yield super(StatusTestBase, self).setUp()
@@ -107,6 +107,7 @@
107 options = TwistedOptionNamespace()107 options = TwistedOptionNamespace()
108 options["juju_directory"] = path108 options["juju_directory"] = path
109 options["zookeeper_servers"] = get_test_zookeeper_address()109 options["zookeeper_servers"] = get_test_zookeeper_address()
110 options["session_file"] = self.makeFile()
110 for k, v in extra_options.items():111 for k, v in extra_options.items():
111 options[k] = v112 options[k] = v
112 agent.configure(options)113 agent.configure(options)
@@ -302,6 +303,7 @@
302 options = TwistedOptionNamespace()303 options = TwistedOptionNamespace()
303 options["juju_directory"] = self.makeDir()304 options["juju_directory"] = self.makeDir()
304 options["zookeeper_servers"] = get_test_zookeeper_address()305 options["zookeeper_servers"] = get_test_zookeeper_address()
306 options["session_file"] = self.makeFile()
305 options["machine_id"] = "0"307 options["machine_id"] = "0"
306 agent.configure(options)308 agent.configure(options)
307 agent.set_watch_enabled(False)309 agent.set_watch_enabled(False)
308310
=== modified file 'juju/errors.py'
--- juju/errors.py 2011-09-24 22:21:23 +0000
+++ juju/errors.py 2012-02-02 16:42:42 +0000
@@ -62,7 +62,7 @@
62 return "Error processing %r: %s" % (self.path, self.message)62 return "Error processing %r: %s" % (self.path, self.message)
6363
6464
65class CharmInvocationError(JujuError):65class CharmInvocationError(CharmError):
66 """A charm's hook invocation exited with an error"""66 """A charm's hook invocation exited with an error"""
6767
68 def __init__(self, path, exit_code):68 def __init__(self, path, exit_code):
@@ -74,6 +74,16 @@
74 self.path, self.exit_code)74 self.path, self.exit_code)
7575
7676
77class CharmUpgradeError(CharmError):
78 """Something went wrong trying to upgrade a charm"""
79
80 def __init__(self, message):
81 self.message = message
82
83 def __str__(self):
84 return "Cannot upgrade charm: %s" % self.message
85
86
77class FileAlreadyExists(JujuError):87class FileAlreadyExists(JujuError):
78 """Raised when something refuses to overwrite an existing file.88 """Raised when something refuses to overwrite an existing file.
7989
@@ -164,3 +174,6 @@
164 self.user_policy,174 self.user_policy,
165 self.provider_type,175 self.provider_type,
166 ", ".join(self.provider_policies)))176 ", ".join(self.provider_policies)))
177
178class ServiceError(JujuError):
179 """Some problem with an upstart service"""
167180
=== modified file 'juju/hooks/scheduler.py'
--- juju/hooks/scheduler.py 2011-12-12 01:56:05 +0000
+++ juju/hooks/scheduler.py 2012-02-02 16:42:42 +0000
@@ -1,4 +1,6 @@
1import logging1import logging
2import os
3import yaml
24
3from twisted.internet.defer import DeferredQueue, inlineCallbacks5from twisted.internet.defer import DeferredQueue, inlineCallbacks
4from juju.state.hook import RelationHookContext, RelationChange6from juju.state.hook import RelationHookContext, RelationChange
@@ -28,31 +30,69 @@
28 the run queue.30 the run queue.
29 """31 """
3032
31 def __init__(self, client, executor, unit_relation, relation_name, unit_name):33 def __init__(self, client, executor, unit_relation, relation_name,
32 self._running = None34 unit_name, state_path):
35 self._running = False
36 self._state_path = state_path
3337
34 # The thing that will actually run the hook for us38 # The thing that will actually run the hook for us
35 self._executor = executor39 self._executor = executor
36
37 # For hook context construction.40 # For hook context construction.
38 self._client = client41 self._client = client
39 self._unit_relation = unit_relation42 self._unit_relation = unit_relation
40 self._relation_name = relation_name43 self._relation_name = relation_name
41 self._members = None
42 self._unit_name = unit_name44 self._unit_name = unit_name
4345
44 # Track next operation by node46 if os.path.exists(self._state_path):
45 self._node_queue = {}47 self._load_state()
4648 else:
47 # Track node operations by clock tick49 self._create_state()
48 self._clock_queue = {}50
4951 def _create_state(self):
52 # Current units (as far as the next hook should know)
53 self._context_members = None
54 # Current units and settings versions (as far as the queue knows)
55 self._member_versions = {}
56 # Tracks next operation by unit
57 self._unit_ops = {}
58 # Tracks unit operations by clock tick
59 self._clock_units = {}
50 # Run queue (clock)60 # Run queue (clock)
51 self._run_queue = DeferredQueue()61 self._run_queue = DeferredQueue()
52
53 # Artifical clock sequence62 # Artifical clock sequence
54 self._clock_sequence = 063 self._clock_sequence = 0
5564
65 def _load_state(self):
66 with open(self._state_path) as f:
67 state = yaml.load(f.read())
68 if not state:
69 return self._create_state()
70 self._context_members = state["context_members"]
71 self._member_versions = state["member_versions"]
72 self._unit_ops = state["unit_ops"]
73 self._clock_units = state["clock_units"]
74 self._run_queue = DeferredQueue()
75 self._run_queue.pending = state["clock_queue"]
76 self._clock_sequence = state["clock_sequence"]
77
78 def _save_state(self):
79 state = yaml.dump({
80 "context_members": self._context_members,
81 "member_versions": self._member_versions,
82 "unit_ops": self._unit_ops,
83 "clock_units": self._clock_units,
84 "clock_queue": [
85 # Strip "stop" instructions: if the lifecycle stopped us,
86 # then if/when the lifecycle comes up again in a stopped
87 # state, it won't start us in the first place.
88 c for c in self._run_queue.pending if c is not None],
89 "clock_sequence": self._clock_sequence})
90
91 temp_path = self._state_path + "~"
92 with open(temp_path, "w") as f:
93 f.write(state)
94 os.rename(temp_path, self._state_path)
95
56 @property96 @property
57 def running(self):97 def running(self):
58 return self._running is True98 return self._running is True
@@ -61,6 +101,12 @@
61 def run(self):101 def run(self):
62 """Run the hook scheduler and execution."""102 """Run the hook scheduler and execution."""
63 assert not self._running, "Scheduler is already running"103 assert not self._running, "Scheduler is already running"
104 try:
105 with open(self._state_path, "a"):
106 pass
107 except IOError:
108 raise AssertionError("%s is not writable!" % self._state_path)
109
64 self._running = True110 self._running = True
65 log.debug("start")111 log.debug("start")
66112
@@ -72,16 +118,17 @@
72 break118 break
73119
74 # Get all the units with changes in this clock tick.120 # Get all the units with changes in this clock tick.
75 for unit_name in self._clock_queue.pop(clock):121 for unit_name in self._clock_units.pop(clock):
76122
77 # Get the change for the unit.123 # Get the change for the unit.
78 change_clock, change_type = self._node_queue.pop(unit_name)124 change_clock, change_type = self._unit_ops.pop(unit_name)
79125
80 log.debug("executing hook for %s:%s",126 log.debug("executing hook for %s:%s",
81 unit_name, CHANGE_LABELS[change_type])127 unit_name, CHANGE_LABELS[change_type])
82128
83 # Execute the hook129 # Execute the hook
84 yield self._execute(unit_name, change_type)130 yield self._execute(unit_name, change_type)
131 self._save_state()
85132
86 def stop(self):133 def stop(self):
87 """Stop the hook execution.134 """Stop the hook execution.
@@ -99,26 +146,24 @@
99 # occurs.146 # occurs.
100 self._run_queue.put(None)147 self._run_queue.put(None)
101148
102 def notify_change(self, old_units=(), new_units=(), modified=()):149 def cb_change_members(self, old_units, new_units):
103 """Receive changes regarding related units and schedule hook execution.150 log.debug("members changed: old=%s, new=%s", old_units, new_units)
104 """151 scheduled = 0
105 log.debug("relation change old:%s, new:%s, modified:%s",
106 old_units, new_units, modified)
107
108 self._clock_sequence += 1152 self._clock_sequence += 1
109153
110 # keep track if we've scheduled changes during this clock154 if self._context_members is None:
111 scheduled = 0155 self._context_members = list(old_units)
112156
113 # Handle membership changes157 if set(self._member_versions) != set(old_units):
114158 log.debug(
115 # If we don't have a cached membership yet, use the old units159 "old does not match last recorded units: %s",
116 # as a baseline.160 sorted(self._member_versions))
117 if self._members is None:161
118 self._members = list(old_units)162 added = set(new_units) - set(self._member_versions)
119163 removed = set(self._member_versions) - set(new_units)
120 added = set(new_units) - set(old_units)164 self._member_versions.update(dict((unit, 0) for unit in added))
121 removed = set(old_units) - set(new_units)165 for unit in removed:
166 del self._member_versions[unit]
122167
123 for unit_name in sorted(added):168 for unit_name in sorted(added):
124 scheduled += self._queue_change(169 scheduled += self._queue_change(
@@ -128,59 +173,68 @@
128 scheduled += self._queue_change(173 scheduled += self._queue_change(
129 unit_name, REMOVED, self._clock_sequence)174 unit_name, REMOVED, self._clock_sequence)
130175
131 # Handle modified change176 if scheduled:
132 for unit_name in modified:177 self._run_queue.put(self._clock_sequence)
133 scheduled += self._queue_change(178 self._save_state()
134 unit_name, MODIFIED, self._clock_sequence)
135179
180 def cb_change_settings(self, unit_versions):
181 log.debug("settings changed: %s", unit_versions)
182 scheduled = 0
183 self._clock_sequence += 1
184 for (unit_name, version) in unit_versions:
185 if version > self._member_versions.get(unit_name, 0):
186 self._member_versions[unit_name] = version
187 scheduled += self._queue_change(
188 unit_name, MODIFIED, self._clock_sequence)
136 if scheduled:189 if scheduled:
137 self._run_queue.put(self._clock_sequence)190 self._run_queue.put(self._clock_sequence)
191 self._save_state()
138192
139 def get_hook_context(self, change):193 def get_hook_context(self, change):
140 """194 """
141 Return a hook context, corresponding to the current state of the195 Return a hook context, corresponding to the current state of the
142 system.196 system.
143 """197 """
144 members = self._members or ()198 context_members = self._context_members or ()
145 context = RelationHookContext(199 context = RelationHookContext(
146 self._client, self._unit_relation, change,200 self._client, self._unit_relation, change,
147 sorted(members), unit_name=self._unit_name)201 sorted(context_members), unit_name=self._unit_name)
148 return context202 return context
149203
150 def _queue_change(self, unit_name, change_type, clock):204 def _queue_change(self, unit_name, change_type, clock):
151 """Queue up the node change for execution.205 """Queue up the node change for execution.
152 """206 """
153 # If its a new change for the unit store it, and return.207 # If its a new change for the unit store it, and return.
154 if not unit_name in self._node_queue:208 if not unit_name in self._unit_ops:
155 self._node_queue[unit_name] = (clock, change_type)209 self._unit_ops[unit_name] = (clock, change_type)
156 self._clock_queue.setdefault(clock, []).append(unit_name)210 self._clock_units.setdefault(clock, []).append(unit_name)
157 return True211 return True
158212
159 # Else merge/reduce with the previous operation.213 # Else merge/reduce with the previous operation.
160 previous_clock, previous_change = self._node_queue[unit_name]214 previous_clock, previous_change = self._unit_ops[unit_name]
161 change_clock, change_type = self._reduce(215 change_clock, change_type = self._reduce(
162 (previous_clock, previous_change),216 (previous_clock, previous_change),
163 (self._clock_sequence, change_type))217 (self._clock_sequence, change_type))
164218
165 # If they've cancelled, remove from node and clock queues219 # If they've cancelled, remove from node and clock queues
166 if change_type is None:220 if change_type is None:
167 del self._node_queue[unit_name]221 del self._unit_ops[unit_name]
168 self._clock_queue[previous_clock].remove(unit_name)222 self._clock_units[previous_clock].remove(unit_name)
169 return False223 return False
170224
171 # Update the node queue with the merged change.225 # Update the node queue with the merged change.
172 self._node_queue[unit_name] = (change_clock, change_type)226 self._unit_ops[unit_name] = (change_clock, change_type)
173227
174 # If the clock has changed, remove the old entry.228 # If the clock has changed, remove the old entry.
175 if change_clock != previous_clock:229 if change_clock != previous_clock:
176 self._clock_queue[previous_clock].remove(unit_name)230 self._clock_units[previous_clock].remove(unit_name)
177231
178 # If the old entry has precedence, we didn't schedule anything for232 # If the old entry has precedence, we didn't schedule anything for
179 # this clock cycle.233 # this clock cycle.
180 if change_clock != clock:234 if change_clock != clock:
181 return False235 return False
182236
183 self._clock_queue.setdefault(clock, []).append(unit_name)237 self._clock_units.setdefault(clock, []).append(unit_name)
184 return True238 return True
185239
186 def _reduce(self, previous, new):240 def _reduce(self, previous, new):
@@ -214,9 +268,9 @@
214 """268 """
215 # Determine the current members as of the change.269 # Determine the current members as of the change.
216 if change_type == ADDED:270 if change_type == ADDED:
217 self._members.append(unit_name)271 self._context_members.append(unit_name)
218 elif change_type == REMOVED:272 elif change_type == REMOVED:
219 self._members.remove(unit_name)273 self._context_members.remove(unit_name)
220274
221 # Assemble the change and hook execution context275 # Assemble the change and hook execution context
222 change = RelationChange(276 change = RelationChange(
223277
=== modified file 'juju/hooks/tests/test_scheduler.py'
--- juju/hooks/tests/test_scheduler.py 2011-12-12 01:56:05 +0000
+++ juju/hooks/tests/test_scheduler.py 2012-02-02 16:42:42 +0000
@@ -1,11 +1,18 @@
1import logging1import logging
22import os
3from twisted.internet.defer import inlineCallbacks3import yaml
44
5from juju.hooks.scheduler import HookScheduler5from twisted.internet.defer import inlineCallbacks, fail, succeed
6
7from juju.hooks.scheduler import HookScheduler, ADDED, MODIFIED, REMOVED
6from juju.state.hook import RelationChange8from juju.state.hook import RelationChange
7from juju.state.tests.test_service import ServiceStateManagerTestBase9from juju.state.tests.test_service import ServiceStateManagerTestBase
810
11
12class SomeError(Exception):
13 pass
14
15
9class HookSchedulerTest(ServiceStateManagerTestBase):16class HookSchedulerTest(ServiceStateManagerTestBase):
1017
11 @inlineCallbacks18 @inlineCallbacks
@@ -15,29 +22,49 @@
15 self.unit_relation = self.mocker.mock()22 self.unit_relation = self.mocker.mock()
16 self.executions = []23 self.executions = []
17 self.service = yield self.add_service_from_charm("wordpress")24 self.service = yield self.add_service_from_charm("wordpress")
18 self.scheduler = HookScheduler(self.client,25 self.state_file = self.makeFile()
19 self.collect_executor,26 self.executor = self.collect_executor
20 self.unit_relation, "",27 self._scheduler = None
21 unit_name="wordpress/0")
22 self.log_stream = self.capture_logging(28 self.log_stream = self.capture_logging(
23 "hook.scheduler", level=logging.DEBUG)29 "hook.scheduler", level=logging.DEBUG)
2430
31 @property
32 def scheduler(self):
33 # Create lazily, so we can create with a state file if we want to,
34 # and swap out collect_executor when helpful to do so.
35 if self._scheduler is None:
36 self._scheduler = HookScheduler(
37 self.client, self.executor, self.unit_relation, "",
38 "wordpress/0", self.state_file)
39 return self._scheduler
40
25 def collect_executor(self, context, change):41 def collect_executor(self, context, change):
26 self.executions.append((context, change))42 self.executions.append((context, change))
2743
44 def write_single_unit_state(self):
45 with open(self.state_file, "w") as f:
46 f.write(yaml.dump({
47 "context_members": ["u-1"],
48 "member_versions": {"u-1": 0},
49 "unit_ops": {},
50 "clock_units": {},
51 "clock_queue": [],
52 "clock_sequence": 1}))
53
28 # Event reduction/coalescing cases54 # Event reduction/coalescing cases
29 def test_reduce_removed_added(self):55 def test_reduce_removed_added(self):
30 """ A remove event for a node followed by an add event,56 """ A remove event for a node followed by an add event,
31 results in a modify event.57 results in a modify event.
32 """58 """
33 self.scheduler.notify_change(old_units=["u-1"], new_units=[])59 self.write_single_unit_state()
34 self.scheduler.notify_change(old_units=[], new_units=["u-1"])60 self.scheduler.cb_change_members(["u-1"], [])
61 self.scheduler.cb_change_members([], ["u-1"])
35 self.scheduler.run()62 self.scheduler.run()
36 self.assertEqual(len(self.executions), 1)63 self.assertEqual(len(self.executions), 1)
37 self.assertEqual(self.executions[0][1].change_type, "modified")64 self.assertEqual(self.executions[0][1].change_type, "modified")
3865
39 output = ("relation change old:['u-1'], new:[], modified:()",66 output = ("members changed: old=['u-1'], new=[]",
40 "relation change old:[], new:['u-1'], modified:()",67 "members changed: old=[], new=['u-1']",
41 "start",68 "start",
42 "executing hook for u-1:modified\n")69 "executing hook for u-1:modified\n")
43 self.assertEqual(self.log_stream.getvalue(), "\n".join(output))70 self.assertEqual(self.log_stream.getvalue(), "\n".join(output))
@@ -46,34 +73,34 @@
46 """A modify, remove, add event for a node results in a modify.73 """A modify, remove, add event for a node results in a modify.
47 An extra validation of the previous test.74 An extra validation of the previous test.
48 """75 """
49 self.scheduler.notify_change(modified=["u-1"])76 self.write_single_unit_state()
50 self.scheduler.notify_change(old_units=["u-1"], new_units=[])77 self.scheduler.cb_change_settings([("u-1", 1)])
51 self.scheduler.notify_change(old_units=[], new_units=["u-1"])78 self.scheduler.cb_change_members(["u-1"], [])
79 self.scheduler.cb_change_members([], ["u-1"])
52 self.scheduler.run()80 self.scheduler.run()
53 self.assertEqual(len(self.executions), 1)81 self.assertEqual(len(self.executions), 1)
54 self.assertEqual(self.executions[0][1].change_type, "modified")82 self.assertEqual(self.executions[0][1].change_type, "modified")
5583
56 def test_reduce_add_modify(self):84 def test_reduce_add_modify(self):
57 """An add and modify event for a node are coalesced to an add."""85 """An add and modify event for a node are coalesced to an add."""
58 self.scheduler.notify_change(old_units=[], new_units=["u-1"])86 self.scheduler.cb_change_members([], ["u-1"])
59 self.scheduler.notify_change(modified=["u-1"])87 self.scheduler.cb_change_settings([("u-1", 1)])
60 self.scheduler.run()88 self.scheduler.run()
61 self.assertEqual(len(self.executions), 1)89 self.assertEqual(len(self.executions), 1)
62 self.assertEqual(self.executions[0][1].change_type, "joined")90 self.assertEqual(self.executions[0][1].change_type, "joined")
6391
64 def test_reduce_add_remove(self):92 def test_reduce_add_remove(self):
65 """an add followed by a removal results in a noop."""93 """an add followed by a removal results in a noop."""
66 self.scheduler.notify_change(old_units=[], new_units=["u-1"])94 self.scheduler.cb_change_members([], ["u-1"])
67 self.scheduler.notify_change(old_units=["u-1"], new_units=[])95 self.scheduler.cb_change_members(["u-1"], [])
68 self.scheduler.run()96 self.scheduler.run()
69 self.assertEqual(len(self.executions), 0)97 self.assertEqual(len(self.executions), 0)
7098
71 def test_reduce_modify_remove(self):99 def test_reduce_modify_remove(self):
72 """Modifying and then removing a node, results in just the removal."""100 """Modifying and then removing a node, results in just the removal."""
73 self.scheduler.notify_change(old_units=["u-1"],101 self.write_single_unit_state()
74 new_units=["u-1"],102 self.scheduler.cb_change_settings([("u-1", 1)])
75 modified=["u-1"])103 self.scheduler.cb_change_members(["u-1"], [])
76 self.scheduler.notify_change(old_units=["u-1"], new_units=[])
77 self.scheduler.run()104 self.scheduler.run()
78 self.assertEqual(len(self.executions), 1)105 self.assertEqual(len(self.executions), 1)
79 self.assertEqual(self.executions[0][1].change_type, "departed")106 self.assertEqual(self.executions[0][1].change_type, "departed")
@@ -82,15 +109,15 @@
82 """Multiple modifies get coalesced to a single modify."""109 """Multiple modifies get coalesced to a single modify."""
83 # simulate normal startup, the first notify will always be the existing110 # simulate normal startup, the first notify will always be the existing
84 # membership set.111 # membership set.
85 self.scheduler.notify_change(old_units=[], new_units=["u-1"])112 self.scheduler.cb_change_members([], ["u-1"])
86 self.scheduler.run()113 self.scheduler.run()
87 self.scheduler.stop()114 self.scheduler.stop()
88 self.assertEqual(len(self.executions), 1)115 self.assertEqual(len(self.executions), 1)
89116
90 # Now continue the modify/modify reduction.117 # Now continue the modify/modify reduction.
91 self.scheduler.notify_change(modified=["u-1"])118 self.scheduler.cb_change_settings([("u-1", 1)])
92 self.scheduler.notify_change(modified=["u-1"])119 self.scheduler.cb_change_settings([("u-1", 2)])
93 self.scheduler.notify_change(modified=["u-1"])120 self.scheduler.cb_change_settings([("u-1", 3)])
94 self.scheduler.run()121 self.scheduler.run()
95122
96 self.assertEqual(len(self.executions), 2)123 self.assertEqual(len(self.executions), 2)
@@ -112,20 +139,35 @@
112 self.assertFalse(self.scheduler.running)139 self.assertFalse(self.scheduler.running)
113140
114 @inlineCallbacks141 @inlineCallbacks
142 def test_run_requires_writable_state(self):
143 # Induce lazy creation of scheduler, then break state file
144 self.scheduler
145 with open(self.state_file, "w"):
146 pass
147 os.chmod(self.state_file, 0)
148 e = yield self.assertFailure(self.scheduler.run(), AssertionError)
149 self.assertEquals(str(e), "%s is not writable!" % self.state_file)
150
151 def test_empty_state(self):
152 with open(self.state_file, "w") as f:
153 f.write(yaml.dump({}))
154
155 # Induce lazy creation to verify it can still survive
156 self.scheduler
157
158 @inlineCallbacks
115 def test_membership_visibility_per_change(self):159 def test_membership_visibility_per_change(self):
116 """Hooks are executed against changes, those changes are160 """Hooks are executed against changes, those changes are
117 associated to a temporal timestamp, however the changes161 associated to a temporal timestamp, however the changes
118 are scheduled for execution, and the state/time of the162 are scheduled for execution, and the state/time of the
119 world may have advanced, to present a logically consistent163 world may have advanced, to present a logically consistent
120 view, we try to gaurantee at a minimum, that hooks will164 view, we try to guarantee at a minimum, that hooks will
121 always see the membership of a relations it was at the165 always see the membership of a relation as it was at the
122 time of their associated change.166 time of their associated change.
123 """167 """
124 self.scheduler.notify_change(168 self.scheduler.cb_change_members([], ["u-1", "u-2"])
125 old_units=[], new_units=["u-1", "u-2"])169 self.scheduler.cb_change_members(["u-1", "u-2"], ["u-2", "u-3"])
126 self.scheduler.notify_change(170 self.scheduler.cb_change_settings([("u-2", 1)])
127 old_units=["u-1", "u-2"], new_units=["u-2", "u-3"])
128 self.scheduler.notify_change(modified=["u-2"])
129171
130 self.scheduler.run()172 self.scheduler.run()
131 self.scheduler.stop()173 self.scheduler.stop()
@@ -139,9 +181,8 @@
139 change_members = yield self.executions[0][0].get_members()181 change_members = yield self.executions[0][0].get_members()
140 self.assertEqual(change_members, ["u-2"])182 self.assertEqual(change_members, ["u-2"])
141183
142 self.scheduler.notify_change(modified=["u-2"])184 self.scheduler.cb_change_settings([("u-2", 2)])
143 self.scheduler.notify_change(185 self.scheduler.cb_change_members(["u-2", "u-3"], ["u-2"])
144 old_units=["u-2", "u-3"], new_units=["u-2"])
145 self.scheduler.run()186 self.scheduler.run()
146187
147 self.assertEqual(len(self.executions), 4)188 self.assertEqual(len(self.executions), 4)
@@ -156,10 +197,17 @@
156 a hook wont see any 'active' members in a membership list, that197 a hook wont see any 'active' members in a membership list, that
157 it hasn't previously been given a notify of before.198 it hasn't previously been given a notify of before.
158 """199 """
159 self.scheduler.notify_change(200 with open(self.state_file, "w") as f:
160 old_units=["u-1", "u-2"],201 f.write(yaml.dump({
161 new_units=["u-2", "u-3", "u-4"],202 "context_members": ["u-1", "u-2"],
162 modified=["u-2"])203 "member_versions": {"u-1": 0, "u-2": 0},
204 "unit_ops": {},
205 "clock_units": {},
206 "clock_queue": [],
207 "clock_sequence": 1}))
208
209 self.scheduler.cb_change_members(["u-1", "u-2"], ["u-2", "u-3", "u-4"])
210 self.scheduler.cb_change_settings([("u-2", 1)])
163211
164 self.scheduler.run()212 self.scheduler.run()
165 self.scheduler.stop()213 self.scheduler.stop()
@@ -197,3 +245,172 @@
197 RelationChange("", "", ""))245 RelationChange("", "", ""))
198 members = yield context.get_members()246 members = yield context.get_members()
199 self.assertEqual(members, [])247 self.assertEqual(members, [])
248
249 @inlineCallbacks
250 def test_state_is_loaded(self):
251 with open(self.state_file, "w") as f:
252 f.write(yaml.dump({
253 "context_members": ["u-1", "u-2"],
254 "member_versions": {"u-1": 5, "u-2": 2, "u-3": 0},
255 "unit_ops": {"u-1": (3, MODIFIED), "u-3": (4, ADDED)},
256 "clock_units": {3: ["u-1"], 4: ["u-3"]},
257 "clock_queue": [3, 4],
258 "clock_sequence": 4}))
259
260 self.scheduler.run()
261 while len(self.executions) < 2:
262 yield self.poke_zk()
263 self.scheduler.stop()
264
265 self.assertEqual(self.executions[0][1].change_type, "modified")
266 members = yield self.executions[0][0].get_members()
267 self.assertEqual(members, ["u-1", "u-2"])
268
269 self.assertEqual(self.executions[1][1].change_type, "joined")
270 members = yield self.executions[1][0].get_members()
271 self.assertEqual(members, ["u-1", "u-2", "u-3"])
272
273 with open(self.state_file) as f:
274 state = yaml.load(f.read())
275 self.assertEquals(state, {
276 "context_members": ["u-1", "u-2", "u-3"],
277 "member_versions": {"u-1": 5, "u-2": 2, "u-3": 0},
278 "unit_ops": {},
279 "clock_units": {},
280 "clock_queue": [],
281 "clock_sequence": 4})
282
283 def test_state_is_stored(self):
284 with open(self.state_file, "w") as f:
285 f.write(yaml.dump({
286 "context_members": ["u-1", "u-2"],
287 "member_versions": {"u-1": 0, "u-2": 2},
288 "unit_ops": {},
289 "clock_units": {},
290 "clock_queue": [],
291 "clock_sequence": 7}))
292
293 self.scheduler.cb_change_members(["u-1", "u-2"], ["u-2", "u-3"])
294 self.scheduler.cb_change_settings([("u-2", 3)])
295
296 # Add a stop instruction to the queue, which should *not* be saved.
297 self.scheduler.stop()
298
299 with open(self.state_file) as f:
300 state = yaml.load(f.read())
301 self.assertEquals(state, {
302 "context_members": ["u-1", "u-2"],
303 "member_versions": {"u-2": 3, "u-3": 0},
304 "unit_ops": {"u-1": (8, REMOVED),
305 "u-2": (9, MODIFIED),
306 "u-3": (8, ADDED)},
307 "clock_units": {8: ["u-3", "u-1"], 9: ["u-2"]},
308 "clock_queue": [8, 9],
309 "clock_sequence": 9})
310
311 @inlineCallbacks
312 def test_state_stored_after_tick(self):
313
314 def execute(context, change):
315 self.execute_calls += 1
316 if self.execute_calls > 1:
317 return fail(SomeError())
318 return succeed(None)
319 self.execute_calls = 0
320 self.executor = execute
321
322 with open(self.state_file, "w") as f:
323 f.write(yaml.dump({
324 "context_members": ["u-1", "u-2"],
325 "member_versions": {"u-1": 1, "u-2": 0, "u-3": 0},
326 "unit_ops": {"u-1": (3, MODIFIED), "u-3": (4, ADDED)},
327 "clock_units": {3: ["u-1"], 4: ["u-3"]},
328 "clock_queue": [3, 4],
329 "clock_sequence": 4}))
330
331 d = self.scheduler.run()
332 while self.execute_calls < 2:
333 yield self.poke_zk()
334 yield self.assertFailure(d, SomeError)
335 with open(self.state_file) as f:
336 self.assertEquals(yaml.load(f.read()), {
337 "context_members": ["u-1", "u-2"],
338 "member_versions": {"u-1": 1, "u-2": 0, "u-3": 0},
339 "unit_ops": {"u-3": (4, ADDED)},
340 "clock_units": {4: ["u-3"]},
341 "clock_queue": [4],
342 "clock_sequence": 4})
343
344 @inlineCallbacks
345 def test_state_not_stored_mid_tick(self):
346
347 def execute(context, change):
348 self.execute_called = True
349 return fail(SomeError())
350 self.execute_called = False
351 self.executor = execute
352
353 initial_state = {
354 "context_members": ["u-1", "u-2"],
355 "member_versions": {"u-1": 1, "u-2": 0, "u-3": 0},
356 "unit_ops": {"u-1": (3, MODIFIED), "u-3": (4, ADDED)},
357 "clock_units": {3: ["u-1"], 4: ["u-3"]},
358 "clock_queue": [3, 4],
359 "clock_sequence": 4}
360 with open(self.state_file, "w") as f:
361 f.write(yaml.dump(initial_state))
362
363 d = self.scheduler.run()
364 while not self.execute_called:
365 yield self.poke_zk()
366 yield self.assertFailure(d, SomeError)
367 with open(self.state_file) as f:
368 self.assertEquals(yaml.load(f.read()), initial_state)
369
370 def test_ignore_equal_settings_version(self):
371 """
372 A modified event whose version is not greater than the latest known
373 version for that unit will be ignored.
374 """
375 self.write_single_unit_state()
376 self.scheduler.cb_change_settings([("u-1", 0),])
377 self.scheduler.run()
378 self.assertEquals(len(self.executions), 0)
379
380 def test_settings_version_0_on_add(self):
381 """
382 When a unit is added, we assume its settings version to be 0, and
383 therefore modified events with version 0 will be ignored.
384 """
385 self.scheduler.cb_change_members([], ["u-1"])
386 self.scheduler.cb_change_settings([("u-1", 0),])
387 self.scheduler.run()
388 self.assertEquals(len(self.executions), 1)
389 self.assertEqual(self.executions[0][1].change_type, "joined")
390
391 def test_membership_timeslip(self):
392 """
393 Adds and removes are calculated based on known membership state, NOT
394 on old_units.
395 """
396 with open(self.state_file, "w") as f:
397 f.write(yaml.dump({
398 "context_members": ["u-1", "u-2"],
399 "member_versions": {"u-1": 0, "u-2": 0},
400 "unit_ops": {},
401 "clock_units": {},
402 "clock_queue": [],
403 "clock_sequence": 4}))
404
405 self.scheduler.cb_change_members(["u-2"], ["u-3", "u-4"])
406 self.scheduler.run()
407
408 output = (
409 "members changed: old=['u-2'], new=['u-3', 'u-4']",
410 "old does not match last recorded units: ['u-1', 'u-2']",
411 "start",
412 "executing hook for u-3:joined",
413 "executing hook for u-4:joined",
414 "executing hook for u-1:departed",
415 "executing hook for u-2:departed\n")
416 self.assertEqual(self.log_stream.getvalue(), "\n".join(output))
200417
=== modified file 'juju/lib/lxc/tests/test_lxc.py'
--- juju/lib/lxc/tests/test_lxc.py 2011-10-01 00:04:14 +0000
+++ juju/lib/lxc/tests/test_lxc.py 2012-02-02 16:42:42 +0000
@@ -10,10 +10,17 @@
10from juju.lib.testing import TestCase10from juju.lib.testing import TestCase
1111
1212
13def run_lxc_tests():13def skip_sudo_tests():
14 if os.environ.get("TEST_LXC"):14 if os.environ.get("TEST_SUDO"):
15 return None15 # Get user's password *now*, if needed, not mid-run
16 return "TEST_LXC=1 to include lxc tests"16 os.system("sudo false")
17 return False
18 return "TEST_SUDO=1 to include tests which use sudo (including lxc tests)"
19
20
21def uses_sudo(f):
22 f.skip = skip_sudo_tests()
23 return f
1724
1825
19DATA_PATH = os.path.abspath(26DATA_PATH = os.path.abspath(
@@ -23,9 +30,9 @@
23DEFAULT_CONTAINER = "lxc_test"30DEFAULT_CONTAINER = "lxc_test"
2431
2532
33@uses_sudo
26class LXCTest(TestCase):34class LXCTest(TestCase):
27 timeout = 24035 timeout = 240
28 skip = run_lxc_tests()
2936
30 def setUp(self):37 def setUp(self):
31 self.config = self.make_config()38 self.config = self.make_config()
3239
=== added directory 'juju/lib/tests/data'
=== added file 'juju/lib/tests/data/test_basic_install'
--- juju/lib/tests/data/test_basic_install 1970-01-01 00:00:00 +0000
+++ juju/lib/tests/data/test_basic_install 2012-02-02 16:42:42 +0000
@@ -0,0 +1,10 @@
1description "uninteresting service"
2author "Juju Team <juju@lists.ubuntu.com>"
3
4start on runlevel [2345]
5stop on runlevel [!2345]
6respawn
7
8
9
10exec /bin/false >> /tmp/some-name.output 2>&1
011
=== added file 'juju/lib/tests/data/test_less_basic_install'
--- juju/lib/tests/data/test_less_basic_install 1970-01-01 00:00:00 +0000
+++ juju/lib/tests/data/test_less_basic_install 2012-02-02 16:42:42 +0000
@@ -0,0 +1,11 @@
1description "pew pew pew blam"
2author "Juju Team <juju@lists.ubuntu.com>"
3
4start on runlevel [2345]
5stop on runlevel [!2345]
6respawn
7
8env FOO="bar baz qux"
9env PEW="pew"
10
11exec /bin/deathstar --ignore-ewoks endor >> /somewhere/else 2>&1
012
=== added file 'juju/lib/tests/data/test_standard_install'
--- juju/lib/tests/data/test_standard_install 1970-01-01 00:00:00 +0000
+++ juju/lib/tests/data/test_standard_install 2012-02-02 16:42:42 +0000
@@ -0,0 +1,10 @@
1description "a wretched hive of scum and villainy"
2author "Juju Team <juju@lists.ubuntu.com>"
3
4start on runlevel [2345]
5stop on runlevel [!2345]
6respawn
7
8env LIGHTSABER="civilised weapon"
9
10exec /bin/imagination-failure --no-ideas >> /tmp/some-name.output 2>&1
011
=== modified file 'juju/lib/tests/test_statemachine.py'
--- juju/lib/tests/test_statemachine.py 2011-09-15 18:50:23 +0000
+++ juju/lib/tests/test_statemachine.py 2012-02-02 16:42:42 +0000
@@ -121,10 +121,14 @@
121 workflow_state = AttributeWorkflowState(workflow)121 workflow_state = AttributeWorkflowState(workflow)
122 current_state = yield workflow_state.get_state()122 current_state = yield workflow_state.get_state()
123 self.assertEqual(current_state, None)123 self.assertEqual(current_state, None)
124 current_vars = yield workflow_state.get_state_variables()
125 self.assertEqual(current_vars, {})
124126
125 yield workflow_state.set_state("started")127 yield workflow_state.set_state("started")
126 current_state = yield workflow_state.get_state()128 current_state = yield workflow_state.get_state()
127 self.assertEqual(current_state, "started")129 self.assertEqual(current_state, "started")
130 current_vars = yield workflow_state.get_state_variables()
131 self.assertEqual(current_vars, {})
128132
129 @inlineCallbacks133 @inlineCallbacks
130 def test_state_fire_transition(self):134 def test_state_fire_transition(self):
@@ -333,3 +337,13 @@
333337
334 self.assertFailure(workflow_state.transition_state("unknown"),338 self.assertFailure(workflow_state.transition_state("unknown"),
335 InvalidStateError)339 InvalidStateError)
340
341 @inlineCallbacks
342 def test_load_bad_state(self):
343 class BadLoadWorkflowState(WorkflowState):
344 def _load(self):
345 return succeed({"some": "other-data"})
346
347 workflow = BadLoadWorkflowState(Workflow())
348 yield self.assertFailure(workflow.get_state(), KeyError)
349 yield self.assertFailure(workflow.get_state_variables(), KeyError)
336350
=== added file 'juju/lib/tests/test_upstart.py'
--- juju/lib/tests/test_upstart.py 1970-01-01 00:00:00 +0000
+++ juju/lib/tests/test_upstart.py 2012-02-02 16:42:42 +0000
@@ -0,0 +1,339 @@
1import os
2
3from twisted.internet.defer import inlineCallbacks, succeed
4
5from juju.errors import ServiceError
6from juju.lib.mocker import ANY, KWARGS
7from juju.lib.testing import TestCase
8from juju.lib.upstart import UpstartService
9
10
11DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")
12
13
14class UpstartServiceTest(TestCase):
15
16 @inlineCallbacks
17 def setUp(self):
18 yield super(UpstartServiceTest, self).setUp()
19 self.init_dir = self.makeDir()
20 self.conf = os.path.join(self.init_dir, "some-name.conf")
21 self.output = "/tmp/some-name.output"
22 self.patch(UpstartService, "init_dir", self.init_dir)
23 self.service = UpstartService("some-name")
24
25 def setup_service(self):
26 self.service.set_description("a wretched hive of scum and villainy")
27 self.service.set_command("/bin/imagination-failure --no-ideas")
28 self.service.set_environ({"LIGHTSABER": "civilised weapon"})
29
30 def setup_mock(self):
31 self.check_call = self.mocker.replace("subprocess.check_call")
32 self.getProcessOutput = self.mocker.replace(
33 "twisted.internet.utils.getProcessOutput")
34
35 def mock_status(self, result):
36 self.getProcessOutput("/sbin/status", ["some-name"])
37 self.mocker.result(result)
38
39 def mock_call(self, args, output=None):
40 self.check_call(args, KWARGS)
41 if output is None:
42 self.mocker.result(0)
43 else:
44 def write(ANY, **_):
45 with open(self.output, "w") as f:
46 f.write(output)
47 self.mocker.call(write)
48
49 def mock_start(self, output=None):
50 self.mock_call(("/sbin/start", "some-name"), output)
51
52 def mock_stop(self):
53 self.mock_call(("/sbin/stop", "some-name"))
54
55 def mock_check_success(self):
56 for _ in range(5):
57 self.mock_status(succeed("blah start/running blah 12345"))
58
59 def mock_check_unstable(self):
60 for _ in range(4):
61 self.mock_status(succeed("blah start/running blah 12345"))
62 self.mock_status(succeed("blah start/running blah 12346"))
63
64 def mock_check_not_running(self):
65 self.mock_status(succeed("blah"))
66
67 def write_dummy_conf(self):
68 with open(self.conf, "w") as f:
69 f.write("dummy")
70
71 def assert_dummy_conf(self):
72 with open(self.conf) as f:
73 self.assertEquals(f.read(), "dummy")
74
75 def assert_no_conf(self):
76 self.assertFalse(os.path.exists(self.conf))
77
78 def assert_conf(self, name="test_standard_install"):
79 with open(os.path.join(DATA_DIR, name)) as expected:
80 with open(self.conf) as actual:
81 self.assertEquals(actual.read(), expected.read())
82
83 def test_is_installed(self):
84 """Check is_installed depends on conf file existence"""
85 self.assertFalse(self.service.is_installed())
86 self.write_dummy_conf()
87 self.assertTrue(self.service.is_installed())
88
89 def test_init_dir(self):
90 """
91 Check is_installed still works when init_dir specified explicitly
92 """
93 self.patch(UpstartService, "init_dir", "/BAD/PATH")
94 self.service = UpstartService("some-name", init_dir=self.init_dir)
95 self.setup_service()
96
97 self.assertFalse(self.service.is_installed())
98 self.write_dummy_conf()
99 self.assertTrue(self.service.is_installed())
100
101 @inlineCallbacks
102 def test_is_running(self):
103 """
104 Check is_running interprets status output (when service is installed)
105 """
106 self.setup_mock()
107 self.mock_status(succeed("blah stop/waiting blah"))
108 self.mock_status(succeed("blah blob/gibbering blah"))
109 self.mock_status(succeed("blah start/running blah 12345"))
110 self.mocker.replay()
111
112 # Won't hit status; conf is not installed
113 self.assertFalse((yield self.service.is_running()))
114 self.write_dummy_conf()
115
116 # These 3 calls correspond to the first 3 mock_status calls above
117 self.assertFalse((yield self.service.is_running()))
118 self.assertFalse((yield self.service.is_running()))
119 self.assertTrue((yield self.service.is_running()))
120
121 @inlineCallbacks
122 def test_is_stable_yes(self):
123 self.setup_mock()
124 self.mock_check_success()
125 self.mocker.replay()
126
127 self.write_dummy_conf()
128 self.assertTrue((yield self.service.is_stable()))
129
130 @inlineCallbacks
131 def test_is_stable_no(self):
132 self.setup_mock()
133 self.mock_check_unstable()
134 self.mocker.replay()
135
136 self.write_dummy_conf()
137 self.assertFalse((yield self.service.is_stable()))
138
139 @inlineCallbacks
140 def test_is_stable_not_running(self):
141 self.setup_mock()
142 self.mock_check_not_running()
143 self.mocker.replay()
144
145 self.write_dummy_conf()
146 self.assertFalse((yield self.service.is_stable()))
147
148 @inlineCallbacks
149 def test_is_stable_not_even_installed(self):
150 self.assertFalse((yield self.service.is_stable()))
151
152 @inlineCallbacks
153 def test_get_pid(self):
154 """
155 Check get_pid interprets status output (when service is installed)
156 """
157 self.setup_mock()
158 self.mock_status(succeed("blah stop/waiting blah"))
159 self.mock_status(succeed("blah blob/gibbering blah"))
160 self.mock_status(succeed("blah start/running blah 12345"))
161 self.mocker.replay()
162
163 # Won't hit status; conf is not installed
164 self.assertEquals((yield self.service.get_pid()), None)
165 self.write_dummy_conf()
166
167 # These 3 calls correspond to the first 3 mock_status calls above
168 self.assertEquals((yield self.service.get_pid()), None)
169 self.assertEquals((yield self.service.get_pid()), None)
170 self.assertEquals((yield self.service.get_pid()), 12345)
171
172 @inlineCallbacks
173 def test_basic_install(self):
174 """Check a simple UpstartService writes expected conf file"""
175 e = yield self.assertFailure(self.service.install(), ServiceError)
176 self.assertEquals(str(e), "Cannot render .conf: no description set")
177 self.service.set_description("uninteresting service")
178 e = yield self.assertFailure(self.service.install(), ServiceError)
179 self.assertEquals(str(e), "Cannot render .conf: no command set")
180 self.service.set_command("/bin/false")
181 yield self.service.install()
182
183 self.assert_conf("test_basic_install")
184
185 @inlineCallbacks
186 def test_less_basic_install(self):
187 """Check conf for a different UpstartService (which sets an env var)"""
188 self.service.set_description("pew pew pew blam")
189 self.service.set_command("/bin/deathstar --ignore-ewoks endor")
190 self.service.set_environ({"FOO": "bar baz qux", "PEW": "pew"})
191 self.service.set_output_path("/somewhere/else")
192 yield self.service.install()
193
194 self.assert_conf("test_less_basic_install")
195
196 def test_install_via_script(self):
197 """Check that the output-as-script form does the right thing"""
198 self.setup_service()
199 install, start = self.service.get_cloud_init_commands()
200
201 os.system(install)
202 self.assert_conf()
203 self.assertEquals(start, "/sbin/start some-name")
204
205 @inlineCallbacks
206 def test_start_not_installed(self):
207 """Check that .start() also installs if necessary"""
208 self.setup_mock()
209 self.mock_status(succeed("blah stop/waiting blah"))
210 self.mock_start()
211 self.mock_check_success()
212 self.mocker.replay()
213
214 self.setup_service()
215 yield self.service.start()
216 self.assert_conf()
217
218 @inlineCallbacks
219 def test_start_not_started_stable(self):
220 """Check that .start() starts if stopped, and checks for stable pid"""
221 self.write_dummy_conf()
222 self.setup_mock()
223 self.mock_status(succeed("blah stop/waiting blah"))
224 self.mock_start("ignored")
225 self.mock_check_success()
226 self.mocker.replay()
227
228 self.setup_service()
229 yield self.service.start()
230 self.assert_dummy_conf()
231
232 @inlineCallbacks
233 def test_start_not_started_unstable(self):
234 """Check that .start() starts if stopped, and raises on unstable pid"""
235 self.write_dummy_conf()
236 self.setup_mock()
237 self.mock_status(succeed("blah stop/waiting blah"))
238 self.mock_start("kangaroo")
239 self.mock_check_unstable()
240 self.mocker.replay()
241
242 self.setup_service()
243 e = yield self.assertFailure(self.service.start(), ServiceError)
244 self.assertEquals(
245 str(e), "Failed to start job some-name; got output:\nkangaroo")
246 self.assert_dummy_conf()
247
248 @inlineCallbacks
249 def test_start_not_started_failure(self):
250 """Check that .start() starts if stopped, and raises on no pid"""
251 self.write_dummy_conf()
252 self.setup_mock()
253 self.mock_status(succeed("blah stop/waiting blah"))
254 self.mock_start()
255 self.mock_check_not_running()
256 self.mocker.replay()
257
258 self.setup_service()
259 e = yield self.assertFailure(self.service.start(), ServiceError)
260 self.assertEquals(
261 str(e), "Failed to start job some-name; no output detected")
262 self.assert_dummy_conf()
263
264 @inlineCallbacks
265 def test_start_started(self):
266 """Check that .start() does nothing if already running"""
267 self.write_dummy_conf()
268 self.setup_mock()
269 self.mock_status(succeed("blah start/running blah 12345"))
270 self.mocker.replay()
271
272 self.setup_service()
273 yield self.service.start()
274 self.assert_dummy_conf()
275
276 @inlineCallbacks
277 def test_destroy_not_installed(self):
278 """Check .destroy() does nothing if not installed"""
279 yield self.service.destroy()
280 self.assert_no_conf()
281
282 @inlineCallbacks
283 def test_destroy_not_started(self):
284 """Check .destroy just deletes conf if not running"""
285 self.write_dummy_conf()
286 self.setup_mock()
287 self.mock_status(succeed("blah stop/waiting blah"))
288 self.mocker.replay()
289
290 yield self.service.destroy()
291 self.assert_no_conf()
292
293 @inlineCallbacks
294 def test_destroy_started(self):
295 """Check .destroy() stops running service and deletes conf file"""
296 self.write_dummy_conf()
297 self.setup_mock()
298 self.mock_status(succeed("blah start/running blah 54321"))
299 self.mock_stop()
300 self.mocker.replay()
301
302 yield self.service.destroy()
303 self.assert_no_conf()
304
305 @inlineCallbacks
306 def test_use_sudo(self):
307 """Check that expected commands are generated when use_sudo is set"""
308 self.setup_mock()
309 self.service = UpstartService("some-name", use_sudo=True)
310 self.setup_service()
311 with open(self.output, "w") as f:
312 f.write("clear this file out...")
313
314 def verify_cp(args, **kwargs):
315 sudo, cp, src, dst = args
316 self.assertEquals(sudo, "sudo")
317 self.assertEquals(cp, "cp")
318 with open(os.path.join(DATA_DIR, "test_standard_install")) as exp:
319 with open(src) as actual:
320 self.assertEquals(actual.read(), exp.read())
321 self.assertEquals(dst, self.conf)
322 self.write_dummy_conf()
323
324 self.check_call(ANY, KWARGS)
325 self.mocker.call(verify_cp)
326 self.mock_call(("sudo", "rm", self.output))
327 self.mock_call(("sudo", "chmod", "644", self.conf))
328 self.mock_status(succeed("blah stop/waiting blah"))
329 self.mock_call(("sudo", "/sbin/start", "some-name"))
330 # 5 for initial stability check; 1 for final do-we-need-to-stop check
331 for _ in range(6):
332 self.mock_status(succeed("blah start/running blah 12345"))
333 self.mock_call(("sudo", "/sbin/stop", "some-name"))
334 self.mock_call(("sudo", "rm", self.conf))
335 self.mock_call(("sudo", "rm", self.output))
336
337 self.mocker.replay()
338 yield self.service.start()
339 yield self.service.destroy()
0340
=== added file 'juju/lib/upstart.py'
--- juju/lib/upstart.py 1970-01-01 00:00:00 +0000
+++ juju/lib/upstart.py 2012-02-02 16:42:42 +0000
@@ -0,0 +1,166 @@
1import os
2import subprocess
3from tempfile import NamedTemporaryFile
4
5from twisted.internet.defer import inlineCallbacks, returnValue
6from twisted.internet.threads import deferToThread
7from twisted.internet.utils import getProcessOutput
8
9from juju.errors import ServiceError
10from juju.lib.twistutils import sleep
11
12
13_CONF_TEMPLATE = """\
14description "%s"
15author "Juju Team <juju@lists.ubuntu.com>"
16
17start on runlevel [2345]
18stop on runlevel [!2345]
19respawn
20
21%s
22
23exec %s >> %s 2>&1
24"""
25
26def _silent_check_call(args):
27 with open(os.devnull, "w") as f:
28 return subprocess.check_call(
29 args, stdout=f.fileno(), stderr=f.fileno())
30
31
32class UpstartService(object):
33
34 # on class for ease of testing
35 init_dir = "/etc/init"
36
37 def __init__(self, name, init_dir=None, use_sudo=False):
38 self._name = name
39 if init_dir is not None:
40 self.init_dir = init_dir
41 self._use_sudo = use_sudo
42 self._output_path = None
43 self._description = None
44 self._environ = {}
45 self._command = None
46
47 @property
48 def _conf_path(self):
49 return os.path.join(
50 self.init_dir, "%s.conf" % self._name)
51
52 @property
53 def output_path(self):
54 if self._output_path is not None:
55 return self._output_path
56 return "/tmp/%s.output" % self._name
57
58 def set_description(self, description):
59 self._description = description
60
61 def set_environ(self, environ):
62 self._environ = environ
63
64 def set_command(self, command):
65 self._command = command
66
67 def set_output_path(self, path):
68 self._output_path = path
69
70 @inlineCallbacks
71 def _trash_output(self):
72 if os.path.exists(self.output_path):
73 # Just using os.unlink will fail when we're running TEST_SUDO tests
74 # which hit this code path (because root will own self.output_path)
75 yield self._call("rm", self.output_path)
76
77 def _render(self):
78 if self._description is None:
79 raise ServiceError("Cannot render .conf: no description set")
80 if self._command is None:
81 raise ServiceError("Cannot render .conf: no command set")
82 return _CONF_TEMPLATE % (
83 self._description,
84 "\n".join('env %s="%s"' % kv
85 for kv in sorted(self._environ.items())),
86 self._command,
87 self.output_path)
88
89 def _call(self, *args):
90 if self._use_sudo:
91 args = ("sudo",) + args
92 return deferToThread(_silent_check_call, args)
93
94 def get_cloud_init_commands(self):
95 return ["cat >> %s <<EOF\n%sEOF\n" % (self._conf_path, self._render()),
96 "/sbin/start %s" % self._name]
97
98 @inlineCallbacks
99 def install(self):
100 with NamedTemporaryFile() as f:
101 f.write(self._render())
102 f.flush()
103 yield self._call("cp", f.name, self._conf_path)
104 yield self._call("chmod", "644", self._conf_path)
105
106 @inlineCallbacks
107 def start(self):
108 if not self.is_installed():
109 yield self.install()
110 if (yield self.is_running()):
111 return
112 yield self._trash_output()
113 yield self._call("/sbin/start", self._name)
114 if (yield self.is_stable()):
115 return
116
117 output = None
118 if os.path.exists(self.output_path):
119 with open(self.output_path) as f:
120 output = f.read()
121 if not output:
122 raise ServiceError(
123 "Failed to start job %s; no output detected" % self._name)
124 raise ServiceError(
125 "Failed to start job %s; got output:\n%s" % (self._name, output))
126
127 @inlineCallbacks
128 def destroy(self):
129 if (yield self.is_running()):
130 yield self._call("/sbin/stop", self._name)
131 if self.is_installed():
132 yield self._call("rm", self._conf_path)
133 yield self._trash_output()
134
135 @inlineCallbacks
136 def get_pid(self):
137 if not self.is_installed():
138 returnValue(None)
139 status = yield getProcessOutput("/sbin/status", [self._name])
140 if "start/running" not in status:
141 returnValue(None)
142 pid = status.split(" ")[-1]
143 returnValue(int(pid))
144
145 @inlineCallbacks
146 def is_running(self):
147 pid = yield self.get_pid()
148 returnValue(pid is not None)
149
150 @inlineCallbacks
151 def is_stable(self):
152 """Does the process continue to run with the same pid?
153
154 (5 times in a row, with a gap of 0.1s between each check)
155 """
156 pid = yield self.get_pid()
157 if pid is None:
158 returnValue(False)
159 for _ in range(4):
160 yield sleep(0.1)
161 if pid != (yield self.get_pid()):
162 returnValue(False)
163 returnValue(True)
164
165 def is_installed(self):
166 return os.path.exists(self._conf_path)
0167
=== modified file 'juju/machine/tests/test_unit_deployment.py'
--- juju/machine/tests/test_unit_deployment.py 2011-10-05 13:59:44 +0000
+++ juju/machine/tests/test_unit_deployment.py 2012-02-02 16:42:42 +0000
@@ -3,25 +3,21 @@
3"""3"""
4import logging4import logging
5import os5import os
6import sys6import subprocess
77
8from twisted.internet.protocol import ProcessProtocol
9from twisted.internet.defer import inlineCallbacks, succeed8from twisted.internet.defer import inlineCallbacks, succeed
109
11import juju
12from juju.charm import get_charm_from_path10from juju.charm import get_charm_from_path
13from juju.charm.tests.test_repository import RepositoryTestBase11from juju.charm.tests.test_repository import RepositoryTestBase
14from juju.lib.lxc import LXCContainer12from juju.lib.lxc import LXCContainer
15from juju.lib.mocker import MATCH, ANY13from juju.lib.lxc.tests.test_lxc import uses_sudo
16from juju.lib.twistutils import get_module_directory14from juju.lib.mocker import ANY, KWARGS
15from juju.lib.upstart import UpstartService
17from juju.machine.unit import UnitMachineDeployment, UnitContainerDeployment16from juju.machine.unit import UnitMachineDeployment, UnitContainerDeployment
18from juju.machine.errors import UnitDeploymentError17from juju.machine.errors import UnitDeploymentError
19from juju.tests.common import get_test_zookeeper_address18from juju.tests.common import get_test_zookeeper_address
2019
2120
22MATCH_PROTOCOL = MATCH(lambda x: isinstance(x, ProcessProtocol))
23
24
25class UnitMachineDeploymentTest(RepositoryTestBase):21class UnitMachineDeploymentTest(RepositoryTestBase):
2622
27 def setUp(self):23 def setUp(self):
@@ -32,6 +28,11 @@
32 self.units_directory = os.path.join(self.juju_directory, "units")28 self.units_directory = os.path.join(self.juju_directory, "units")
33 os.mkdir(self.units_directory)29 os.mkdir(self.units_directory)
34 self.unit_name = "wordpress/0"30 self.unit_name = "wordpress/0"
31 self.rootfs = self.makeDir()
32 self.init_dir = os.path.join(self.rootfs, "etc", "init")
33 os.makedirs(self.init_dir)
34 self.real_init_dir = self.patch(
35 UpstartService, "init_dir", self.init_dir)
3536
36 self.deployment = UnitMachineDeployment(37 self.deployment = UnitMachineDeployment(
37 self.unit_name,38 self.unit_name,
@@ -41,11 +42,43 @@
41 self.deployment.unit_agent_module, "juju.agents.unit")42 self.deployment.unit_agent_module, "juju.agents.unit")
42 self.deployment.unit_agent_module = "juju.agents.dummy"43 self.deployment.unit_agent_module = "juju.agents.dummy"
4344
44 def process_kill(self, pid):45 def setup_mock(self):
45 try:46 self.check_call = self.mocker.replace("subprocess.check_call")
46 os.kill(pid, 9)47 self.getProcessOutput = self.mocker.replace(
47 except OSError:48 "twisted.internet.utils.getProcessOutput")
48 pass49
50 def mock_is_running(self, running):
51 self.getProcessOutput("/sbin/status", ["juju-wordpress-0"])
52 if running:
53 self.mocker.result(succeed(
54 "juju-wordpress-0 start/running, process 12345"))
55 else:
56 self.mocker.result(succeed("juju-wordpress-0 stop/waiting"))
57
58 def _without_sudo(self, args, **_):
59 self.assertEquals(args[0], "sudo")
60 return subprocess.call(args[1:])
61
62 def mock_install(self):
63 self.check_call(ANY, KWARGS) # cp to init dir
64 self.mocker.call(self._without_sudo)
65 self.check_call(ANY, KWARGS) # chmod 644
66 self.mocker.call(self._without_sudo)
67
68 def mock_start(self):
69 self.check_call(("sudo", "/sbin/start", "juju-wordpress-0"), KWARGS)
70 self.mocker.result(0)
71 for _ in range(5):
72 self.mock_is_running(True)
73
74 def mock_destroy(self):
75 self.check_call(("sudo", "/sbin/stop", "juju-wordpress-0"), KWARGS)
76 self.mocker.result(0)
77 self.check_call(ANY, KWARGS) # rm from init dir
78 self.mocker.call(self._without_sudo)
79
80 def assert_pid_running(self, pid, expect):
81 self.assertEquals(os.path.exists("/proc/%s" % pid), expect)
4982
50 def test_unit_name_with_path_manipulation_raises_assertion(self):83 def test_unit_name_with_path_manipulation_raises_assertion(self):
51 self.assertRaises(84 self.assertRaises(
@@ -60,129 +93,50 @@
60 os.path.join(self.units_directory,93 os.path.join(self.units_directory,
61 self.unit_name.replace("/", "-")))94 self.unit_name.replace("/", "-")))
6295
63 def test_unit_pid_file(self):
64 self.assertEqual(
65 self.deployment.pid_file,
66 os.path.join(self.units_directory,
67 "%s.pid" % (self.unit_name.replace("/", "-"))))
68
69 def test_service_unit_start(self):96 def test_service_unit_start(self):
70 """97 """
71 Starting a service unit will result in a unit workspace being created98 Starting a service unit will result in a unit workspace being created
72 if it does not exist and a running service unit agent.99 if it does not exist and a running service unit agent.
73 """100 """
74101 self.setup_mock()
75 d = self.deployment.start(102 self.mock_install()
76 "0", get_test_zookeeper_address(), self.bundle)103 self.mock_is_running(False)
77104 self.mock_start()
78 @inlineCallbacks
79 def validate_result(result):
80 # give process time to write its pid
81 yield self.sleep(0.1)
82 self.addCleanup(
83 self.process_kill,
84 int(open(self.deployment.pid_file).read()))
85 self.assertEqual(result, True)
86
87 d.addCallback(validate_result)
88 return d
89
90 def test_deployment_get_environment(self):
91 zk_address = get_test_zookeeper_address()
92 environ = self.deployment.get_environment(21, zk_address)
93 environ.pop("PYTHONPATH")
94 self.assertEqual(environ["JUJU_HOME"], self.juju_directory)
95 self.assertEqual(environ["JUJU_UNIT_NAME"], self.unit_name)
96 self.assertEqual(environ["JUJU_ZOOKEEPER"], zk_address)
97 self.assertEqual(environ["JUJU_MACHINE_ID"], "21")
98
99 def test_service_unit_start_with_integer_machine_id(self):
100 """
101 Starting a service unit will result in a unit workspace being created
102 if it does not exist and a running service unit agent.
103 """
104 d = self.deployment.start(
105 21, get_test_zookeeper_address(), self.bundle)
106
107 @inlineCallbacks
108 def validate_result(result):
109 # give process time to write its pid
110 yield self.sleep(0.1)
111 self.addCleanup(
112 self.process_kill,
113 int(open(self.deployment.pid_file).read()))
114 self.assertEqual(result, True)
115
116 d.addCallback(validate_result)
117 return d
118
119 def test_service_unit_start_with_agent_startup_error(self):
120 """
121 Starting a service unit will result in a unit workspace being created
122 if it does not exist and a running service unit agent.
123 """
124 self.deployment.unit_agent_module = "magichat.xr1"
125 d = self.deployment.start(
126 "0", get_test_zookeeper_address(), self.bundle)
127
128 self.failUnlessFailure(d, UnitDeploymentError)
129
130 def validate_result(error):
131 self.assertIn("No module named magichat", str(error))
132
133 d.addCallback(validate_result)
134 return d
135
136 def test_service_unit_start_agent_arguments(self):
137 """
138 Starting a service unit will start a service unit agent with arguments
139 denoting the current machine id, zookeeper server location, and the
140 unit name. Additionally it will configure the log and pid file
141 locations.
142 """
143 machine_id = "0"
144 zookeeper_hosts = "menagerie.example.com:2181"
145
146 from twisted.internet import reactor
147 mock_reactor = self.mocker.patch(reactor)
148
149 environ = dict(os.environ)
150 environ["JUJU_UNIT_NAME"] = self.unit_name
151 environ["JUJU_HOME"] = self.juju_directory
152 environ["JUJU_MACHINE_ID"] = machine_id
153 environ["JUJU_ZOOKEEPER"] = zookeeper_hosts
154 environ["PYTHONPATH"] = ":".join(
155 filter(None, [
156 os.path.dirname(get_module_directory(juju)),
157 environ.get("PYTHONPATH")]))
158
159 pid_file = os.path.join(
160 self.units_directory,
161 "%s.pid" % self.unit_name.replace("/", "-"))
162
163 log_file = os.path.join(
164 self.deployment.directory,
165 "charm.log")
166
167 args = [sys.executable, "-m", "juju.agents.dummy", "-n",
168 "--pidfile", pid_file, "--logfile", log_file]
169
170 mock_reactor.spawnProcess(
171 MATCH_PROTOCOL, sys.executable, args, environ)
172 self.mocker.replay()105 self.mocker.replay()
173 self.deployment.start(106
174 machine_id, zookeeper_hosts, self.bundle)107 d = self.deployment.start(
175108 "123", get_test_zookeeper_address(), self.bundle)
176 def xtest_service_unit_start_pre_unpack(self):109
177 """110 def verify_upstart(_):
178 Attempting to start a charm before the charm is unpacked111 conf_path = os.path.join(self.init_dir, "juju-wordpress-0.conf")
179 results in an exception.112 with open(conf_path) as f:
180 """113 lines = f.readlines()
181 error = yield self.assertFailure(114
182 self.deployment.start(115 env = []
183 "0", get_test_zookeeper_address(), self.bundle),116 for line in lines:
184 UnitDeploymentError)117 if line.startswith("env "):
185 self.assertEquals(str(error), "Charm must be unpacked first.")118 env.append(line[4:-1].split("=", 1))
119 if line.startswith("exec "):
120 exec_ = line[5:-1]
121
122 env = dict((k, v.strip('"')) for (k, v) in env)
123 env.pop("PYTHONPATH")
124 self.assertEquals(env, {
125 "JUJU_HOME": self.juju_directory,
126 "JUJU_UNIT_NAME": self.unit_name,
127 "JUJU_ZOOKEEPER": get_test_zookeeper_address(),
128 "JUJU_MACHINE_ID": "123"})
129
130 log_file = os.path.join(
131 self.deployment.directory, "charm.log")
132 command = " ".join([
133 "/usr/bin/python", "-m", "juju.agents.dummy", "--nodaemon",
134 "--logfile", log_file, "--session-file",
135 "/var/run/juju/unit-wordpress-0-agent.zksession",
136 ">> /tmp/juju-wordpress-0.output 2>&1"])
137 self.assertEquals(exec_, command)
138 d.addCallback(verify_upstart)
139 return d
186140
187 @inlineCallbacks141 @inlineCallbacks
188 def test_service_unit_destroy(self):142 def test_service_unit_destroy(self):
@@ -190,48 +144,22 @@
190 Forcibly stop a unit, and destroy any directories associated to it144 Forcibly stop a unit, and destroy any directories associated to it
191 on the machine, and kills the unit agent process.145 on the machine, and kills the unit agent process.
192 """146 """
147 self.setup_mock()
148 self.mock_install()
149 self.mock_is_running(False)
150 self.mock_start()
151 self.mock_is_running(True)
152 self.mock_destroy()
153 self.mocker.replay()
154
193 yield self.deployment.start(155 yield self.deployment.start(
194 "0", get_test_zookeeper_address(), self.bundle)156 "0", get_test_zookeeper_address(), self.bundle)
195 # give the process time to write its pid file157
196 yield self.sleep(0.1)
197 pid = int(open(self.deployment.pid_file).read())
198 yield self.deployment.destroy()158 yield self.deployment.destroy()
199 # give the process time to die.
200 yield self.sleep(0.1)
201 e = self.assertRaises(OSError, os.kill, pid, 0)
202 self.assertEqual(e.errno, 3)
203 self.assertFalse(os.path.exists(self.deployment.directory))159 self.assertFalse(os.path.exists(self.deployment.directory))
204 self.assertFalse(os.path.exists(self.deployment.pid_file))160
205161 conf_path = os.path.join(self.init_dir, "juju-wordpress-0.conf")
206 def test_service_unit_destroy_stale_pid(self):162 self.assertFalse(os.path.exists(conf_path))
207 """
208 A stale pid file does not cause any errors.
209
210 We mock away is_running as otherwise it will check for this, but
211 there exists a small window when the result may disagree.
212 """
213 self.makeFile("8917238", path=self.deployment.pid_file)
214 mock_deployment = self.mocker.patch(self.deployment)
215 mock_deployment.is_running()
216 self.mocker.result(succeed(True))
217 self.mocker.replay()
218 return self.deployment.destroy()
219
220 def test_service_unit_destroy_perm_error(self):
221 """
222 A stale pid file does not cause any errors.
223
224 We mock away is_running as otherwise it will check for this, but
225 there exists a small window when the result may disagree.
226 """
227 if os.geteuid() == 0:
228 return
229 self.makeFile("1", path=self.deployment.pid_file)
230 mock_deployment = self.mocker.patch(self.deployment)
231 mock_deployment.is_running()
232 self.mocker.result(succeed(True))
233 self.mocker.replay()
234 return self.assertFailure(self.deployment.destroy(), OSError)
235163
236 @inlineCallbacks164 @inlineCallbacks
237 def test_service_unit_destroy_undeployed(self):165 def test_service_unit_destroy_undeployed(self):
@@ -247,11 +175,23 @@
247 If the unit is not running, then destroy will just remove175 If the unit is not running, then destroy will just remove
248 its directory.176 its directory.
249 """177 """
250 self.deployment.unpack_charm(self.bundle)178 self.setup_mock()
251 self.assertTrue(os.path.exists(self.deployment.directory))179 self.mock_install()
180 self.mock_is_running(False)
181 self.mock_start()
182 self.mock_is_running(False)
183 self.check_call(ANY, KWARGS) # rm from init dir
184 self.mocker.call(self._without_sudo)
185 self.mocker.replay()
186
187 yield self.deployment.start(
188 "0", get_test_zookeeper_address(), self.bundle)
252 yield self.deployment.destroy()189 yield self.deployment.destroy()
253 self.assertFalse(os.path.exists(self.deployment.directory))190 self.assertFalse(os.path.exists(self.deployment.directory))
254191
192 conf_path = os.path.join(self.init_dir, "juju-wordpress-0.conf")
193 self.assertFalse(os.path.exists(conf_path))
194
255 def test_unpack_charm(self):195 def test_unpack_charm(self):
256 """196 """
257 The deployment unpacks a charm bundle into the unit workspace.197 The deployment unpacks a charm bundle into the unit workspace.
@@ -279,62 +219,81 @@
279 str(error),219 str(error),
280 "Invalid charm for deployment: %s" % self.charm.path)220 "Invalid charm for deployment: %s" % self.charm.path)
281221
282 def test_is_running_no_pid_file(self):222 @inlineCallbacks
283 """223 def test_is_running_not_installed(self):
284 If there is no pid file the service unit is not running.224 """
285 """225 If there is no conf file the service unit is not running.
286 self.assertEqual((yield self.deployment.is_running()), False)226 """
287227 self.assertEqual((yield self.deployment.is_running()), False)
288 def test_is_running(self):228
289 """229 @inlineCallbacks
290 The service deployment will check the pid and validate230 def test_is_running_not_running(self):
291 that the pid found is a running process.231 """
292 """232 If the conf file exists, but job not running, unit not running
293 self.makeFile(233 """
294 str(os.getpid()), path=self.deployment.pid_file)234 conf_path = os.path.join(self.init_dir, "juju-wordpress-0.conf")
235 with open(conf_path, "w") as f:
236 f.write("blah")
237 self.setup_mock()
238 self.mock_is_running(False)
239 self.mocker.replay()
240 self.assertEqual((yield self.deployment.is_running()), False)
241
242 @inlineCallbacks
243 def test_is_running_success(self):
244 """
245 Check running job.
246 """
247 conf_path = os.path.join(self.init_dir, "juju-wordpress-0.conf")
248 with open(conf_path, "w") as f:
249 f.write("blah")
250 self.setup_mock()
251 self.mock_is_running(True)
252 self.mocker.replay()
295 self.assertEqual((yield self.deployment.is_running()), True)253 self.assertEqual((yield self.deployment.is_running()), True)
296254
297 def test_is_running_against_unknown_error(self):255 @uses_sudo
298 """256 @inlineCallbacks
299 If we don't have permission to access the process, the257 def test_run_actual_process(self):
300 original error should get passed along.258 # "unpatch" to use real /etc/init
301 """259 self.patch(UpstartService, "init_dir", self.real_init_dir)
302 if os.geteuid() == 0:260 yield self.deployment.start(
303 return261 "0", get_test_zookeeper_address(), self.bundle)
304 self.makeFile("1", path=self.deployment.pid_file)262 old_pid = yield self.deployment.get_pid()
305 self.assertFailure(self.deployment.is_running(), OSError)263 self.assert_pid_running(old_pid, True)
306264
307 def test_is_running_invalid_pid_file(self):265 # Give the job a chance to fall over and be restarted (if the
308 """266 # pid doesn't change, that hasn't hapened)
309 If the pid file is corrupted on disk, and does not contain267 yield self.sleep(0.1)
310 a valid integer, then the agent is not running.268 self.assertEquals((yield self.deployment.get_pid()), old_pid)
311 """269 self.assert_pid_running(old_pid, True)
312 self.makeFile("abcdef", path=self.deployment.pid_file)270
313 self.assertEqual(271 # Kick the job over ourselves; check it comes back
314 (yield self.deployment.is_running()), False)272 os.system("sudo kill -9 %s" % old_pid)
315273 yield self.sleep(0.1)
316 def test_is_running_invalid_pid(self):274 self.assert_pid_running(old_pid, False)
317 """275 new_pid = yield self.deployment.get_pid()
318 If the pid file refers to an invalid process then the276 self.assertNotEquals(new_pid, old_pid)
319 agent is not running.277 self.assert_pid_running(new_pid, True)
320 """278
321 self.makeFile("669966", path=self.deployment.pid_file)279 yield self.deployment.destroy()
322 self.assertEqual(280 self.assertEquals((yield self.deployment.get_pid()), None)
323 (yield self.deployment.is_running()), False)281 self.assert_pid_running(new_pid, False)
324282
325283 @uses_sudo
326upstart_job_sample = '''\284 @inlineCallbacks
327description "Unit agent for riak/0"285 def test_fail_to_run_actual_process(self):
328author "Juju Team <juju@lists.canonical.com>"286 self.deployment.unit_agent_module = "haha.disregard.that"
329start on start on filesystem or runlevel [2345]287 self.patch(UpstartService, "init_dir", self.real_init_dir)
330stop on runlevel [!2345]288
331289 d = self.deployment.start(
332respawn290 "0", get_test_zookeeper_address(), self.bundle)
333291 e = yield self.assertFailure(d, UnitDeploymentError)
334env JUJU_MACHINE_ID="0"292 self.assertTrue(str(e).startswith(
335env JUJU_HOME="/var/lib/juju"293 "Failed to start job juju-wordpress-0; got output:\n"))
336env JUJU_ZOOKEEPER="127.0.1.1:2181"294 self.assertIn("No module named haha", str(e))
337env JUJU_UNIT_NAME="riak/0"'''295
296 yield self.deployment.destroy()
338297
339298
340class UnitContainerDeploymentTest(RepositoryTestBase):299class UnitContainerDeploymentTest(RepositoryTestBase):
@@ -365,14 +324,6 @@
365 "ns1-riak-0",324 "ns1-riak-0",
366 self.unit_deploy.container_name)325 self.unit_deploy.container_name)
367326
368 def test_get_upstart_job(self):
369 upstart_job = self.unit_deploy.get_upstart_unit_job(
370 0, "127.0.1.1:2181")
371 job = self.get_normalized(upstart_job)
372 self.assertIn('JUJU_ZOOKEEPER="127.0.1.1:2181"', job)
373 self.assertIn('JUJU_MACHINE_ID="0"', job)
374 self.assertIn('JUJU_UNIT_NAME="riak/0"', job)
375
376 @inlineCallbacks327 @inlineCallbacks
377 def test_destroy(self):328 def test_destroy(self):
378 mock_container = self.mocker.patch(self.unit_deploy.container)329 mock_container = self.mocker.patch(self.unit_deploy.container)
@@ -403,7 +354,7 @@
403 unit_deploy = UnitContainerDeployment(354 unit_deploy = UnitContainerDeployment(
404 self.unit_name, self.juju_home)355 self.unit_name, self.juju_home)
405 container = yield unit_deploy._get_master_template(356 container = yield unit_deploy._get_master_template(
406 "local", "127.0.0.1:1", "abc")357 "local", "abc")
407 self.assertEqual(container.origin, "lp:~juju/foobar")358 self.assertEqual(container.origin, "lp:~juju/foobar")
408 self.assertEqual(359 self.assertEqual(
409 container.customize_log,360 container.customize_log,
@@ -420,7 +371,7 @@
420 mock_deploy = self.mocker.patch(self.unit_deploy)371 mock_deploy = self.mocker.patch(self.unit_deploy)
421 # this minimally validates that we are also called with the372 # this minimally validates that we are also called with the
422 # expect public key373 # expect public key
423 mock_deploy._get_container(ANY, ANY, ANY, env["JUJU_PUBLIC_KEY"])374 mock_deploy._get_container(ANY, ANY, env["JUJU_PUBLIC_KEY"])
424 self.mocker.result((container, rootfs))375 self.mocker.result((container, rootfs))
425376
426 mock_container = self.mocker.patch(container)377 mock_container = self.mocker.patch(container)
@@ -434,7 +385,7 @@
434 yield self.unit_deploy.start("0", "127.0.1.1:2181", self.bundle)385 yield self.unit_deploy.start("0", "127.0.1.1:2181", self.bundle)
435386
436 # Verify the upstart job387 # Verify the upstart job
437 upstart_agent_name = "%s-unit-agent.conf" % (388 upstart_agent_name = "juju-%s.conf" % (
438 self.unit_name.replace("/", "-"))389 self.unit_name.replace("/", "-"))
439 content = open(390 content = open(
440 os.path.join(rootfs, "etc", "init", upstart_agent_name)).read()391 os.path.join(rootfs, "etc", "init", upstart_agent_name)).read()
@@ -443,10 +394,13 @@
443 self.assertIn('JUJU_MACHINE_ID="0"', job)394 self.assertIn('JUJU_MACHINE_ID="0"', job)
444 self.assertIn('JUJU_UNIT_NAME="riak/0"', job)395 self.assertIn('JUJU_UNIT_NAME="riak/0"', job)
445396
446 # Verify the symlink exists397 # Verify the symlinks exist
447 self.assertTrue(os.path.lexists(os.path.join(398 self.assertTrue(os.path.lexists(os.path.join(
448 self.unit_deploy.juju_home, "units",399 self.unit_deploy.juju_home, "units",
449 self.unit_deploy.unit_path_name, "unit.log")))400 self.unit_deploy.unit_path_name, "unit.log")))
401 self.assertTrue(os.path.lexists(os.path.join(
402 self.unit_deploy.juju_home, "units",
403 self.unit_deploy.unit_path_name, "output.log")))
450404
451 # Verify the charm is on disk.405 # Verify the charm is on disk.
452 self.assertTrue(os.path.exists(os.path.join(406 self.assertTrue(os.path.exists(os.path.join(
@@ -471,7 +425,7 @@
471 container = LXCContainer(self.unit_name, None, None, None)425 container = LXCContainer(self.unit_name, None, None, None)
472426
473 mock_deploy = self.mocker.patch(self.unit_deploy)427 mock_deploy = self.mocker.patch(self.unit_deploy)
474 mock_deploy._get_master_template(ANY, ANY, ANY)428 mock_deploy._get_master_template(ANY, ANY)
475 self.mocker.result(container)429 self.mocker.result(container)
476430
477 mock_container = self.mocker.patch(container)431 mock_container = self.mocker.patch(container)
@@ -481,7 +435,7 @@
481 self.mocker.replay()435 self.mocker.replay()
482436
483 container, rootfs = yield self.unit_deploy._get_container(437 container, rootfs = yield self.unit_deploy._get_container(
484 "0", "127.0.0.1:2181", None, "dsa...")438 "0", None, "dsa...")
485439
486 output = self.output.getvalue()440 output = self.output.getvalue()
487 self.assertIn("Container created for %s" % self.unit_deploy.unit_name,441 self.assertIn("Container created for %s" % self.unit_deploy.unit_name,
488442
=== modified file 'juju/machine/unit.py'
--- juju/machine/unit.py 2011-10-01 00:04:14 +0000
+++ juju/machine/unit.py 2012-02-02 16:42:42 +0000
@@ -1,19 +1,17 @@
1import os1import os
2import errno
3import signal
4import shutil2import shutil
5import sys3import sys
6import logging4import logging
75
8import juju6import juju
97
10from twisted.internet.defer import (8from twisted.internet.defer import inlineCallbacks, returnValue
11 Deferred, inlineCallbacks, returnValue, succeed, fail)
12from twisted.internet.protocol import ProcessProtocol
139
14from juju.charm.bundle import CharmBundle10from juju.charm.bundle import CharmBundle
11from juju.errors import ServiceError
12from juju.lib.lxc import LXCContainer, get_containers, LXCError
15from juju.lib.twistutils import get_module_directory13from juju.lib.twistutils import get_module_directory
16from juju.lib.lxc import LXCContainer, get_containers, LXCError14from juju.lib.upstart import UpstartService
1715
18from .errors import UnitDeploymentError16from .errors import UnitDeploymentError
1917
@@ -26,22 +24,17 @@
26 return UnitMachineDeployment24 return UnitMachineDeployment
2725
2826
29class AgentProcessProtocol(ProcessProtocol):27def _get_environment(unit_name, juju_home, machine_id, zookeeper_hosts):
3028 environ = dict()
31 def __init__(self, deferred):29 environ["JUJU_MACHINE_ID"] = str(machine_id)
32 self.deferred = deferred30 environ["JUJU_UNIT_NAME"] = unit_name
33 self._error_buffer = []31 environ["JUJU_HOME"] = juju_home
3432 environ["JUJU_ZOOKEEPER"] = zookeeper_hosts
35 def errReceived(self, data):33 environ["PYTHONPATH"] = ":".join(
36 self._error_buffer.append(data)34 filter(None, [
3735 os.path.dirname(get_module_directory(juju)),
38 def processEnded(self, reason):36 os.environ.get("PYTHONPATH")]))
39 if self._error_buffer:37 return environ
40 msg = "".join(self._error_buffer)
41 msg.strip()
42 self.deferred.errback(UnitDeploymentError(msg))
43 else:
44 self.deferred.callback(True)
4538
4639
47class UnitMachineDeployment(object):40class UnitMachineDeployment(object):
@@ -58,46 +51,35 @@
58 unit_agent_module = "juju.agents.unit"51 unit_agent_module = "juju.agents.unit"
5952
60 def __init__(self, unit_name, juju_home):53 def __init__(self, unit_name, juju_home):
61 self.unit_name = unit_name
62
63 assert ".." not in unit_name, "Invalid Unit Name"54 assert ".." not in unit_name, "Invalid Unit Name"
55 self.unit_name = unit_name
56 self.juju_home = juju_home
64 self.unit_path_name = unit_name.replace("/", "-")57 self.unit_path_name = unit_name.replace("/", "-")
65 self.juju_home = juju_home
66
67 self.directory = os.path.join(58 self.directory = os.path.join(
68 self.juju_home, "units", self.unit_path_name)59 self.juju_home, "units", self.unit_path_name)
6960 self.service = UpstartService(
70 self.pid_file = os.path.join(61 # NOTE: we need use_sudo to work correctly during tests that
71 self.juju_home, "units", "%s.pid" % self.unit_path_name)62 # launch actual processes (rather than just mocking/trusting).
7263 "juju-%s" % self.unit_path_name, use_sudo=True)
64
65 @inlineCallbacks
73 def start(self, machine_id, zookeeper_hosts, bundle):66 def start(self, machine_id, zookeeper_hosts, bundle):
74 """Start a service unit agent."""67 """Start a service unit agent."""
75 # Extract the charm into the unit directory.
76 self.unpack_charm(bundle)68 self.unpack_charm(bundle)
7769 self.service.set_description(
78 # Start the service unit agent70 "Juju unit agent for %s" % self.unit_name)
79 log_file = os.path.join(self.directory, "charm.log")71 self.service.set_environ(_get_environment(
80 environ = self.get_environment(machine_id, zookeeper_hosts)72 self.unit_name, self.juju_home, machine_id, zookeeper_hosts))
81 args = [sys.executable, "-m", self.unit_agent_module, "-n",73 self.service.set_command(" ".join((
82 "--pidfile", self.pid_file, "--logfile", log_file]74 "/usr/bin/python", "-m", self.unit_agent_module,
8375 "--nodaemon",
84 from twisted.internet import reactor76 "--logfile", os.path.join(self.directory, "charm.log"),
85 process_deferred = Deferred()77 "--session-file",
86 protocol = AgentProcessProtocol(process_deferred)78 "/var/run/juju/unit-%s-agent.zksession" % self.unit_path_name)))
87 reactor.spawnProcess(protocol, sys.executable, args, environ)79 try:
88 return process_deferred80 yield self.service.start()
8981 except ServiceError as e:
90 def get_environment(self, machine_id, zookeeper_hosts):82 raise UnitDeploymentError(str(e))
91 environ = dict(os.environ)
92 environ["JUJU_MACHINE_ID"] = str(machine_id)
93 environ["JUJU_UNIT_NAME"] = self.unit_name
94 environ["JUJU_HOME"] = self.juju_home
95 environ["JUJU_ZOOKEEPER"] = zookeeper_hosts
96 environ["PYTHONPATH"] = ":".join(
97 filter(None, [
98 os.path.dirname(get_module_directory(juju)),
99 environ.get("PYTHONPATH")]))
100 return environ
10183
102 @inlineCallbacks84 @inlineCallbacks
103 def destroy(self):85 def destroy(self):
@@ -105,41 +87,17 @@
10587
106 This will destroy/unmount any state on disk.88 This will destroy/unmount any state on disk.
107 """89 """
108 running = yield self.is_running()90 yield self.service.destroy()
109 if running:
110 pid = int(open(self.pid_file).read())
111 try:
112 os.kill(pid, signal.SIGKILL)
113 except OSError, e:
114 if e.errno != errno.ESRCH:
115 raise
116
117 if os.path.exists(self.pid_file):
118 os.remove(self.pid_file)
119
120 if os.path.exists(self.directory):91 if os.path.exists(self.directory):
121 shutil.rmtree(self.directory)92 shutil.rmtree(self.directory)
12293
94 def get_pid(self):
95 """Get the service unit's process id."""
96 return self.service.get_pid()
97
123 def is_running(self):98 def is_running(self):
124 """Is the service unit running."""99 """Is the service unit running."""
125 try:100 return self.service.is_running()
126 with open(self.pid_file) as pid_fh:
127 pid = int(pid_fh.read())
128 except (IOError, ValueError):
129 return succeed(False)
130
131 # Attempt to send a signal to the process to verify its a valid process
132 # From man 2 kill
133 # "If sig is 0, then no signal is sent, but error checking is still
134 # performed; this can be used to check for the existence of a process
135 # ID or process group ID."
136 try:
137 os.kill(pid, 0)
138 except OSError, e:
139 if e.errno == errno.ESRCH:
140 return succeed(False)
141 return fail(e)
142 return succeed(True)
143101
144 def unpack_charm(self, charm):102 def unpack_charm(self, charm):
145 """Unpack a charm to the service units directory."""103 """Unpack a charm to the service units directory."""
@@ -150,27 +108,7 @@
150 charm.extract_to(os.path.join(self.directory, "charm"))108 charm.extract_to(os.path.join(self.directory, "charm"))
151109
152110
153container_upstart_job_template = """\111class UnitContainerDeployment(object):
154description "Unit agent for %(JUJU_UNIT_NAME)s"
155author "Juju Team <juju@lists.canonical.com>"
156
157start on start on filesystem or runlevel [2345]
158stop on runlevel [!2345]
159
160respawn
161
162env JUJU_MACHINE_ID="%(JUJU_MACHINE_ID)s"
163env JUJU_HOME="%(JUJU_HOME)s"
164env JUJU_ZOOKEEPER="%(JUJU_ZOOKEEPER)s"
165env JUJU_UNIT_NAME="%(JUJU_UNIT_NAME)s"
166env PYTHONPATH="%(PYTHONPATH)s"
167
168exec /usr/bin/python -m juju.agents.unit \
169 --logfile=/var/log/juju/unit-%(UNIT_PATH_NAME)s.log
170"""
171
172
173class UnitContainerDeployment(UnitMachineDeployment):
174 """Deploy a service unit in a container.112 """Deploy a service unit in a container.
175113
176 Units deployed in a container have strong isolation between114 Units deployed in a container have strong isolation between
@@ -185,66 +123,35 @@
185 """123 """
186124
187 def __init__(self, unit_name, juju_home):125 def __init__(self, unit_name, juju_home):
188 super(UnitContainerDeployment, self).__init__(unit_name, juju_home)126 self.unit_name = unit_name
127 self.juju_home = juju_home
128 self.unit_path_name = unit_name.replace("/", "-")
189129
130 self._juju_origin = os.environ.get("JUJU_ORIGIN")
190 self._unit_namespace = os.environ.get("JUJU_UNIT_NS")131 self._unit_namespace = os.environ.get("JUJU_UNIT_NS")
191 self._juju_origin = os.environ.get("JUJU_ORIGIN")
192 assert self._unit_namespace is not None, "Required unit ns not found"132 assert self._unit_namespace is not None, "Required unit ns not found"
133 self.container_name = "%s-%s" % (
134 self._unit_namespace, self.unit_path_name)
193135
194 self.pid_file = None
195 self.container = LXCContainer(self.container_name, None, None, None)136 self.container = LXCContainer(self.container_name, None, None, None)
196137 self.directory = None
197 @property
198 def container_name(self):
199 """Get a qualfied name for the container.
200
201 The units directory for the machine points to a path like::
202
203 /var/lib/juju/units
204
205 In the case of the local provider this directory is qualified
206 to allow for multiple users with multiple environments::
207
208 /var/lib/juju/username-envname
209
210 This value is passed to the agent via the JUJU_HOME environment
211 variable.
212
213 This function extracts the name qualifier for the container from
214 the JUJU_HOME value.
215 """
216 return "%s-%s" % (self._unit_namespace,
217 self.unit_name.replace("/", "-"))
218138
219 def setup_directories(self):139 def setup_directories(self):
220 # Create state directories for unit in the container140 # Create state directories for unit in the container
221 # Move to juju-create script141 # Move to juju-create script
222 units_dir = os.path.join(142 base = self.directory
223 self.directory, "var", "lib", "juju", "units")143 dirs = ((base, "var", "lib", "juju", "units", self.unit_path_name),
224 if not os.path.exists(units_dir):144 (base, "var", "lib", "juju", "state"),
225 os.makedirs(units_dir)145 (base, "var", "log", "juju"),
226146 (self.juju_home, "units", self.unit_path_name))
227 state_dir = os.path.join(147
228 self.directory, "var", "lib", "juju", "state")148 for parts in dirs:
229 if not os.path.exists(state_dir):149 dir_ = os.path.join(*parts)
230 os.makedirs(state_dir)150 if not os.path.exists(dir_):
231151 os.makedirs(dir_)
232 log_dir = os.path.join(
233 self.directory, "var", "log", "juju")
234 if not os.path.exists(log_dir):
235 os.makedirs(log_dir)
236
237 unit_dir = os.path.join(units_dir, self.unit_path_name)
238 if not os.path.exists(unit_dir):
239 os.mkdir(unit_dir)
240
241 host_unit_dir = os.path.join(
242 self.juju_home, "units", self.unit_path_name)
243 if not os.path.exists(host_unit_dir):
244 os.makedirs(host_unit_dir)
245152
246 @inlineCallbacks153 @inlineCallbacks
247 def _get_master_template(self, machine_id, zookeeper_hosts, public_key):154 def _get_master_template(self, machine_id, public_key):
248 container_template_name = "%s-%s-template" % (155 container_template_name = "%s-%s-template" % (
249 self._unit_namespace, machine_id)156 self._unit_namespace, machine_id)
250157
@@ -260,7 +167,7 @@
260 if not master_template.is_constructed():167 if not master_template.is_constructed():
261 log.debug("Creating master container...")168 log.debug("Creating master container...")
262 yield master_template.create()169 yield master_template.create()
263 log.debug("Created master container %s" % container_template_name)170 log.debug("Created master container %s", container_template_name)
264171
265 # it wasn't constructed and we couldn't construct it172 # it wasn't constructed and we couldn't construct it
266 if not master_template.is_constructed():173 if not master_template.is_constructed():
@@ -269,15 +176,15 @@
269 returnValue(master_template)176 returnValue(master_template)
270177
271 @inlineCallbacks178 @inlineCallbacks
272 def _get_container(self, machine_id, zookeeper_hosts, bundle, public_key):179 def _get_container(self, machine_id, bundle, public_key):
273 master_template = yield self._get_master_template(180 master_template = yield self._get_master_template(
274 machine_id, zookeeper_hosts, public_key)181 machine_id, public_key)
275 log.info(182 log.info(
276 "Creating container %s...", os.path.basename(self.directory))183 "Creating container %s...", self.unit_path_name)
277184
278 container = yield master_template.clone(self.container_name)185 container = yield master_template.clone(self.container_name)
279 directory = container.rootfs186 directory = container.rootfs
280 log.info("Container created for %s" % self.unit_name)187 log.info("Container created for %s", self.unit_name)
281 returnValue((container, directory))188 returnValue((container, directory))
282189
283 @inlineCallbacks190 @inlineCallbacks
@@ -293,10 +200,9 @@
293 # Build a template container that can be cloned in deploy200 # Build a template container that can be cloned in deploy
294 # we leave the loosely initialized self.container in place for201 # we leave the loosely initialized self.container in place for
295 # the class as thats all we need for methods other than start.202 # the class as thats all we need for methods other than start.
296 self.container, self.directory = yield self._get_container(machine_id,203 self.container, self.directory = yield self._get_container(
297 zookeeper_hosts,204 machine_id, bundle, public_key)
298 bundle,205
299 public_key)
300 # Create state directories for unit in the container206 # Create state directories for unit in the container
301 self.setup_directories()207 self.setup_directories()
302208
@@ -308,13 +214,25 @@
308 log.debug("Charm extracted into container")214 log.debug("Charm extracted into container")
309215
310 # Write upstart file for the agent into the container216 # Write upstart file for the agent into the container
311 upstart_path = os.path.join(217 service_name = "juju-%s" % self.unit_path_name
312 self.directory, "etc", "init",218 init_dir = os.path.join(self.directory, "etc", "init")
313 "%s-unit-agent.conf" % self.unit_path_name)219 service = UpstartService(service_name, init_dir=init_dir)
314 with open(upstart_path, "w") as fh:220 service.set_description(
315 fh.write(self.get_upstart_unit_job(machine_id, zookeeper_hosts))221 "Juju unit agent for %s" % self.unit_name)
222 service.set_environ(_get_environment(
223 self.unit_name, "/var/lib/juju", machine_id, zookeeper_hosts))
224 service.set_output_path(
225 "/var/log/juju/unit-%s-output.log" % self.unit_path_name)
226 service.set_command(" ".join((
227 "/usr/bin/python",
228 "-m", "juju.agents.unit",
229 "--nodaemon",
230 "--logfile", "/var/log/juju/unit-%s.log" % self.unit_path_name,
231 "--session-file",
232 "/var/run/juju/unit-%s-agent.zksession" % self.unit_path_name)))
233 yield service.install()
316234
317 # Create a symlink on the host for easier access to the unit log file235 # Create symlinks on the host for easier access to the unit log files
318 unit_log_path_host = os.path.join(236 unit_log_path_host = os.path.join(
319 self.juju_home, "units", self.unit_path_name, "unit.log")237 self.juju_home, "units", self.unit_path_name, "unit.log")
320 if not os.path.lexists(unit_log_path_host):238 if not os.path.lexists(unit_log_path_host):
@@ -322,6 +240,13 @@
322 os.path.join(self.directory, "var", "log", "juju",240 os.path.join(self.directory, "var", "log", "juju",
323 "unit-%s.log" % self.unit_path_name),241 "unit-%s.log" % self.unit_path_name),
324 unit_log_path_host)242 unit_log_path_host)
243 unit_output_path_host = os.path.join(
244 self.juju_home, "units", self.unit_path_name, "output.log")
245 if not os.path.lexists(unit_output_path_host):
246 os.symlink(
247 os.path.join(self.directory, "var", "log", "juju",
248 "unit-%s-output.log" % self.unit_path_name),
249 unit_output_path_host)
325250
326 # Debug log for the container251 # Debug log for the container
327 container_log_path = os.path.join(252 container_log_path = os.path.join(
@@ -330,36 +255,22 @@
330255
331 log.debug("Starting container...")256 log.debug("Starting container...")
332 yield self.container.run()257 yield self.container.run()
333 log.info("Started container for %s" % self.unit_name)258 log.info("Started container for %s", self.unit_name)
334259
335 @inlineCallbacks260 @inlineCallbacks
336 def destroy(self):261 def destroy(self):
337 """Destroy the unit container.262 """Destroy the unit container."""
338 """
339 log.debug("Destroying container...")263 log.debug("Destroying container...")
340 yield self.container.destroy()264 yield self.container.destroy()
341 log.info("Destroyed container for %s" % self.unit_name)265 log.info("Destroyed container for %s", self.unit_name)
342266
343 @inlineCallbacks267 @inlineCallbacks
344 def is_running(self):268 def is_running(self):
345 """Is the unit container running.269 """Is the unit container running?"""
346 """270 # TODO: container running may not imply agent running.
347 # TODO: container running may not imply agent running. the271 # query zookeeper for the unit agent presence node?
348 # pid file has the pid from the container, we need a container
349 # pid -> host pid mapping to query status from the machine agent.
350 # alternatively querying zookeeper for the unit agent presence
351 # node.
352 if not self.container:272 if not self.container:
353 returnValue(False)273 returnValue(False)
354 container_map = yield get_containers(274 container_map = yield get_containers(
355 prefix=self.container.container_name)275 prefix=self.container.container_name)
356 returnValue(container_map.get(self.container.container_name, False))276 returnValue(container_map.get(self.container.container_name, False))
357
358 def get_upstart_unit_job(self, machine_id, zookeeper_hosts):
359 """Return a string containing the upstart job to start the unit agent.
360 """
361 environ = self.get_environment(machine_id, zookeeper_hosts)
362 # Keep qualified locations within the container for colo support
363 environ["JUJU_HOME"] = "/var/lib/juju"
364 environ["UNIT_PATH_NAME"] = self.unit_path_name
365 return container_upstart_job_template % environ
366277
=== modified file 'juju/providers/common/cloudinit.py'
--- juju/providers/common/cloudinit.py 2012-01-09 13:58:21 +0000
+++ juju/providers/common/cloudinit.py 2012-02-02 16:42:42 +0000
@@ -1,6 +1,7 @@
1from subprocess import Popen, PIPE1from subprocess import Popen, PIPE
22
3from juju.errors import CloudInitError3from juju.errors import CloudInitError
4from juju.lib.upstart import UpstartService
4from juju.providers.common.utils import format_cloud_init5from juju.providers.common.utils import format_cloud_init
5from juju.state.auth import make_identity6from juju.state.auth import make_identity
6import juju7import juju
@@ -41,21 +42,26 @@
4142
4243
43def _machine_scripts(machine_id, zookeeper_hosts):44def _machine_scripts(machine_id, zookeeper_hosts):
44 return [45 service = UpstartService("juju-machine-agent")
45 "JUJU_MACHINE_ID=%s JUJU_ZOOKEEPER=%s "46 service.set_description("Juju machine agent")
46 "python -m juju.agents.machine -n "47 service.set_environ(
47 "--logfile=/var/log/juju/machine-agent.log "48 {"JUJU_MACHINE_ID": machine_id, "JUJU_ZOOKEEPER": zookeeper_hosts})
48 "--pidfile=/var/run/juju/machine-agent.pid"49 service.set_command(
49 % (machine_id, zookeeper_hosts)]50 "python -m juju.agents.machine --nodaemon "
51 "--logfile /var/log/juju/machine-agent.log "
52 "--session-file /var/run/juju/machine-agent.zksession")
53 return service.get_cloud_init_commands()
5054
5155
52def _provision_scripts(zookeeper_hosts):56def _provision_scripts(zookeeper_hosts):
53 return [57 service = UpstartService("juju-provision-agent")
54 "JUJU_ZOOKEEPER=%s "58 service.set_description("Juju provisioning agent")
55 "python -m juju.agents.provision -n "59 service.set_environ({"JUJU_ZOOKEEPER": zookeeper_hosts})
56 "--logfile=/var/log/juju/provision-agent.log "60 service.set_command(
57 "--pidfile=/var/run/juju/provision-agent.pid"61 "python -m juju.agents.provision --nodaemon "
58 % zookeeper_hosts]62 "--logfile /var/log/juju/provision-agent.log "
63 "--session-file /var/run/juju/provision-agent.zksession")
64 return service.get_cloud_init_commands()
5965
6066
61def _line_generator(data):67def _line_generator(data):
@@ -64,6 +70,7 @@
64 if stripped:70 if stripped:
65 yield (len(line)-len(stripped), stripped)71 yield (len(line)-len(stripped), stripped)
6672
73
67def parse_juju_origin(data):74def parse_juju_origin(data):
68 next = _line_generator(data).next75 next = _line_generator(data).next
69 try:76 try:
7077
=== modified file 'juju/providers/common/tests/data/cloud_init_bootstrap'
--- juju/providers/common/tests/data/cloud_init_bootstrap 2012-01-09 14:17:21 +0000
+++ juju/providers/common/tests/data/cloud_init_bootstrap 2012-02-02 16:42:42 +0000
@@ -4,12 +4,58 @@
4machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'localhost:2181',4machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'localhost:2181',
5 machine-id: passport}5 machine-id: passport}
6output: {all: '| tee -a /var/log/cloud-init-output.log'}6output: {all: '| tee -a /var/log/cloud-init-output.log'}
7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws,7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
8 python-zookeeper, default-jre-headless, zookeeper, zookeeperd]8 default-jre-headless, zookeeper, zookeeperd]
9runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir9runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir -p
10 -p /var/log/juju, 'juju-admin initialize --instance-id=token --admin-identity=admin:19vlzY4Vc3q4Ew5OsCwKYqrq1HI= --provider-type=dummy',10 /var/log/juju, 'juju-admin initialize --instance-id=token --admin-identity=admin:19vlzY4Vc3q4Ew5OsCwKYqrq1HI=
11 'JUJU_MACHINE_ID=passport JUJU_ZOOKEEPER=localhost:2181 python -m juju.agents.machine11 --provider-type=dummy', 'cat >> /etc/init/juju-machine-agent.conf <<EOF
12 -n --logfile=/var/log/juju/machine-agent.log --pidfile=/var/run/juju/machine-agent.pid',12
13 'JUJU_ZOOKEEPER=localhost:2181 python -m juju.agents.provision -n --logfile=/var/log/juju/provision-agent.log13 description "Juju machine agent"
14 --pidfile=/var/run/juju/provision-agent.pid']14
15 author "Juju Team <juju@lists.ubuntu.com>"
16
17
18 start on runlevel [2345]
19
20 stop on runlevel [!2345]
21
22 respawn
23
24
25 env JUJU_MACHINE_ID="passport"
26
27 env JUJU_ZOOKEEPER="localhost:2181"
28
29
30 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
31 --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
32 2>&1
33
34 EOF
35
36 ', /sbin/start juju-machine-agent, 'cat >> /etc/init/juju-provision-agent.conf
37 <<EOF
38
39 description "Juju provisioning agent"
40
41 author "Juju Team <juju@lists.ubuntu.com>"
42
43
44 start on runlevel [2345]
45
46 stop on runlevel [!2345]
47
48 respawn
49
50
51 env JUJU_ZOOKEEPER="localhost:2181"
52
53
54 exec python -m juju.agents.provision --nodaemon --logfile /var/log/juju/provision-agent.log
55 --session-file /var/run/juju/provision-agent.zksession >> /tmp/juju-provision-agent.output
56 2>&1
57
58 EOF
59
60 ', /sbin/start juju-provision-agent]
15ssh_authorized_keys: [chubb]61ssh_authorized_keys: [chubb]
1662
=== modified file 'juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers'
--- juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers 2012-01-09 14:17:21 +0000
+++ juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers 2012-02-02 16:42:42 +0000
@@ -4,13 +4,58 @@
4machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181,localhost:2181',4machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181,localhost:2181',
5 machine-id: passport}5 machine-id: passport}
6output: {all: '| tee -a /var/log/cloud-init-output.log'}6output: {all: '| tee -a /var/log/cloud-init-output.log'}
7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws,7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
8 python-zookeeper, default-jre-headless, zookeeper, zookeeperd]8 default-jre-headless, zookeeper, zookeeperd]
9runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir9runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir -p
10 -p /var/log/juju, 'juju-admin initialize --instance-id=token --admin-identity=admin:19vlzY4Vc3q4Ew5OsCwKYqrq1HI= --provider-type=dummy',10 /var/log/juju, 'juju-admin initialize --instance-id=token --admin-identity=admin:19vlzY4Vc3q4Ew5OsCwKYqrq1HI=
11 'JUJU_MACHINE_ID=passport JUJU_ZOOKEEPER=cotswold:2181,longleat:2181,localhost:218111 --provider-type=dummy', 'cat >> /etc/init/juju-machine-agent.conf <<EOF
12 python -m juju.agents.machine -n --logfile=/var/log/juju/machine-agent.log12
13 --pidfile=/var/run/juju/machine-agent.pid', 'JUJU_ZOOKEEPER=cotswold:2181,longleat:2181,localhost:218113 description "Juju machine agent"
14 python -m juju.agents.provision -n --logfile=/var/log/juju/provision-agent.log14
15 --pidfile=/var/run/juju/provision-agent.pid']15 author "Juju Team <juju@lists.ubuntu.com>"
16
17
18 start on runlevel [2345]
19
20 stop on runlevel [!2345]
21
22 respawn
23
24
25 env JUJU_MACHINE_ID="passport"
26
27 env JUJU_ZOOKEEPER="cotswold:2181,longleat:2181,localhost:2181"
28
29
30 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
31 --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
32 2>&1
33
34 EOF
35
36 ', /sbin/start juju-machine-agent, 'cat >> /etc/init/juju-provision-agent.conf
37 <<EOF
38
39 description "Juju provisioning agent"
40
41 author "Juju Team <juju@lists.ubuntu.com>"
42
43
44 start on runlevel [2345]
45
46 stop on runlevel [!2345]
47
48 respawn
49
50
51 env JUJU_ZOOKEEPER="cotswold:2181,longleat:2181,localhost:2181"
52
53
54 exec python -m juju.agents.provision --nodaemon --logfile /var/log/juju/provision-agent.log
55 --session-file /var/run/juju/provision-agent.zksession >> /tmp/juju-provision-agent.output
56 2>&1
57
58 EOF
59
60 ', /sbin/start juju-provision-agent]
16ssh_authorized_keys: [chubb]61ssh_authorized_keys: [chubb]
1762
=== modified file 'juju/providers/common/tests/data/cloud_init_branch'
--- juju/providers/common/tests/data/cloud_init_branch 2012-01-09 14:17:21 +0000
+++ juju/providers/common/tests/data/cloud_init_branch 2012-02-02 16:42:42 +0000
@@ -6,12 +6,34 @@
6machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',6machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',
7 machine-id: passport}7 machine-id: passport}
8output: {all: '| tee -a /var/log/cloud-init-output.log'}8output: {all: '| tee -a /var/log/cloud-init-output.log'}
9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws,9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper]
10 python-zookeeper]
11runcmd: [sudo apt-get install -y python-txzookeeper, sudo mkdir -p /usr/lib/juju,10runcmd: [sudo apt-get install -y python-txzookeeper, sudo mkdir -p /usr/lib/juju,
12 'cd /usr/lib/juju && sudo /usr/bin/bzr co lp:blah/juju/blah-blah juju',11 'cd /usr/lib/juju && sudo /usr/bin/bzr co lp:blah/juju/blah-blah juju', cd /usr/lib/juju/juju
13 cd /usr/lib/juju/juju && sudo python setup.py develop, sudo mkdir -p /var/lib/juju,12 && sudo python setup.py develop, sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju,
14 sudo mkdir -p /var/log/juju, 'JUJU_MACHINE_ID=passport JUJU_ZOOKEEPER=cotswold:2181,longleat:218113 'cat >> /etc/init/juju-machine-agent.conf <<EOF
15 python -m juju.agents.machine -n --logfile=/var/log/juju/machine-agent.log14
16 --pidfile=/var/run/juju/machine-agent.pid']15 description "Juju machine agent"
16
17 author "Juju Team <juju@lists.ubuntu.com>"
18
19
20 start on runlevel [2345]
21
22 stop on runlevel [!2345]
23
24 respawn
25
26
27 env JUJU_MACHINE_ID="passport"
28
29 env JUJU_ZOOKEEPER="cotswold:2181,longleat:2181"
30
31
32 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
33 --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
34 2>&1
35
36 EOF
37
38 ', /sbin/start juju-machine-agent]
17ssh_authorized_keys: [chubb]39ssh_authorized_keys: [chubb]
1840
=== modified file 'juju/providers/common/tests/data/cloud_init_branch_trunk'
--- juju/providers/common/tests/data/cloud_init_branch_trunk 2012-01-09 14:17:21 +0000
+++ juju/providers/common/tests/data/cloud_init_branch_trunk 2012-02-02 16:42:42 +0000
@@ -6,12 +6,34 @@
6machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',6machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',
7 machine-id: passport}7 machine-id: passport}
8output: {all: '| tee -a /var/log/cloud-init-output.log'}8output: {all: '| tee -a /var/log/cloud-init-output.log'}
9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws,9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper]
10 python-zookeeper]
11runcmd: [sudo apt-get install -y python-txzookeeper, sudo mkdir -p /usr/lib/juju,10runcmd: [sudo apt-get install -y python-txzookeeper, sudo mkdir -p /usr/lib/juju,
12 'cd /usr/lib/juju && sudo /usr/bin/bzr co lp:juju juju',11 'cd /usr/lib/juju && sudo /usr/bin/bzr co lp:juju juju', cd /usr/lib/juju/juju &&
13 cd /usr/lib/juju/juju && sudo python setup.py develop, sudo mkdir -p /var/lib/juju,12 sudo python setup.py develop, sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju,
14 sudo mkdir -p /var/log/juju, 'JUJU_MACHINE_ID=passport JUJU_ZOOKEEPER=cotswold:2181,longleat:218113 'cat >> /etc/init/juju-machine-agent.conf <<EOF
15 python -m juju.agents.machine -n --logfile=/var/log/juju/machine-agent.log14
16 --pidfile=/var/run/juju/machine-agent.pid']15 description "Juju machine agent"
16
17 author "Juju Team <juju@lists.ubuntu.com>"
18
19
20 start on runlevel [2345]
21
22 stop on runlevel [!2345]
23
24 respawn
25
26
27 env JUJU_MACHINE_ID="passport"
28
29 env JUJU_ZOOKEEPER="cotswold:2181,longleat:2181"
30
31
32 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
33 --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
34 2>&1
35
36 EOF
37
38 ', /sbin/start juju-machine-agent]
17ssh_authorized_keys: [chubb]39ssh_authorized_keys: [chubb]
1840
=== modified file 'juju/providers/common/tests/data/cloud_init_distro'
--- juju/providers/common/tests/data/cloud_init_distro 2012-01-09 14:17:21 +0000
+++ juju/providers/common/tests/data/cloud_init_distro 2012-02-02 16:42:42 +0000
@@ -4,10 +4,32 @@
4machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',4machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',
5 machine-id: passport}5 machine-id: passport}
6output: {all: '| tee -a /var/log/cloud-init-output.log'}6output: {all: '| tee -a /var/log/cloud-init-output.log'}
7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws,7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper]
8 python-zookeeper]8runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir -p
9runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir9 /var/log/juju, 'cat >> /etc/init/juju-machine-agent.conf <<EOF
10 -p /var/log/juju, 'JUJU_MACHINE_ID=passport JUJU_ZOOKEEPER=cotswold:2181,longleat:218110
11 python -m juju.agents.machine -n --logfile=/var/log/juju/machine-agent.log11 description "Juju machine agent"
12 --pidfile=/var/run/juju/machine-agent.pid']12
13 author "Juju Team <juju@lists.ubuntu.com>"
14
15
16 start on runlevel [2345]
17
18 stop on runlevel [!2345]
19
20 respawn
21
22
23 env JUJU_MACHINE_ID="passport"
24
25 env JUJU_ZOOKEEPER="cotswold:2181,longleat:2181"
26
27
28 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
29 --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
30 2>&1
31
32 EOF
33
34 ', /sbin/start juju-machine-agent]
13ssh_authorized_keys: [chubb]35ssh_authorized_keys: [chubb]
1436
=== modified file 'juju/providers/common/tests/data/cloud_init_ppa'
--- juju/providers/common/tests/data/cloud_init_ppa 2012-01-09 14:17:21 +0000
+++ juju/providers/common/tests/data/cloud_init_ppa 2012-02-02 16:42:42 +0000
@@ -2,14 +2,36 @@
2apt-update: true2apt-update: true
3apt-upgrade: true3apt-upgrade: true
4apt_sources:4apt_sources:
5- {'source': 'ppa:juju/pkgs'}5- {source: 'ppa:juju/pkgs'}
6machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',6machine-data: {juju-provider-type: dummy, juju-zookeeper-hosts: 'cotswold:2181,longleat:2181',
7 machine-id: passport}7 machine-id: passport}
8output: {all: '| tee -a /var/log/cloud-init-output.log'}8output: {all: '| tee -a /var/log/cloud-init-output.log'}
9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws,9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper]
10 python-zookeeper]10runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir -p
11runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir11 /var/log/juju, 'cat >> /etc/init/juju-machine-agent.conf <<EOF
12 -p /var/log/juju, 'JUJU_MACHINE_ID=passport JUJU_ZOOKEEPER=cotswold:2181,longleat:218112
13 python -m juju.agents.machine -n --logfile=/var/log/juju/machine-agent.log13 description "Juju machine agent"
14 --pidfile=/var/run/juju/machine-agent.pid']14
15 author "Juju Team <juju@lists.ubuntu.com>"
16
17
18 start on runlevel [2345]
19
20 stop on runlevel [!2345]
21
22 respawn
23
24
25 env JUJU_MACHINE_ID="passport"
26
27 env JUJU_ZOOKEEPER="cotswold:2181,longleat:2181"
28
29
30 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
31 --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
32 2>&1
33
34 EOF
35
36 ', /sbin/start juju-machine-agent]
15ssh_authorized_keys: [chubb]37ssh_authorized_keys: [chubb]
1638
=== modified file 'juju/providers/common/tests/test_cloudinit.py'
--- juju/providers/common/tests/test_cloudinit.py 2011-10-04 21:22:48 +0000
+++ juju/providers/common/tests/test_cloudinit.py 2012-02-02 16:42:42 +0000
@@ -44,8 +44,9 @@
44 def assert_render(self, cloud_init, name):44 def assert_render(self, cloud_init, name):
45 with open(os.path.join(DATA_DIR, name)) as f:45 with open(os.path.join(DATA_DIR, name)) as f:
46 expected = yaml.load(f.read())46 expected = yaml.load(f.read())
47 obtained = yaml.load(cloud_init.render())47 rendered = cloud_init.render()
48 self.assertEquals(obtained, expected)48 self.assertTrue(rendered.startswith("#cloud-config"))
49 self.assertEquals(yaml.load(rendered), expected)
4950
50 def test_render_validate_normal(self):51 def test_render_validate_normal(self):
51 cloud_init = CloudInit()52 cloud_init = CloudInit()
5253
=== modified file 'juju/providers/ec2/tests/data/bootstrap_cloud_init'
--- juju/providers/ec2/tests/data/bootstrap_cloud_init 2012-01-09 14:17:21 +0000
+++ juju/providers/ec2/tests/data/bootstrap_cloud_init 2012-02-02 16:42:42 +0000
@@ -1,16 +1,61 @@
1#cloud-config1#cloud-config
2apt-update: true2apt-update: true
3apt-upgrade: true3apt-upgrade: true
4machine-data: {juju-provider-type: ec2, juju-zookeeper-hosts: 'localhost:2181',4machine-data: {juju-provider-type: ec2, juju-zookeeper-hosts: 'localhost:2181', machine-id: '0'}
5 machine-id: '0'}
6output: {all: '| tee -a /var/log/cloud-init-output.log'}5output: {all: '| tee -a /var/log/cloud-init-output.log'}
7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws,6packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
8 python-zookeeper, default-jre-headless, zookeeper, zookeeperd]7 default-jre-headless, zookeeper, zookeeperd]
9runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir8runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir -p
10 -p /var/log/juju, 'juju-admin initialize --instance-id=$(curl http://169.254.169.254/1.0/meta-data/instance-id)9 /var/log/juju, 'juju-admin initialize --instance-id=$(curl http://169.254.169.254/1.0/meta-data/instance-id)
11 --admin-identity=admin:JbJ6sDGV37EHzbG9FPvttk64cmg= --provider-type=ec2', 'JUJU_MACHINE_ID=0 JUJU_ZOOKEEPER=localhost:218110 --admin-identity=admin:JbJ6sDGV37EHzbG9FPvttk64cmg= --provider-type=ec2', 'cat
12 python -m juju.agents.machine -n --logfile=/var/log/juju/machine-agent.log11 >> /etc/init/juju-machine-agent.conf <<EOF
13 --pidfile=/var/run/juju/machine-agent.pid', 'JUJU_ZOOKEEPER=localhost:218112
14 python -m juju.agents.provision -n --logfile=/var/log/juju/provision-agent.log13 description "Juju machine agent"
15 --pidfile=/var/run/juju/provision-agent.pid']14
15 author "Juju Team <juju@lists.ubuntu.com>"
16
17
18 start on runlevel [2345]
19
20 stop on runlevel [!2345]
21
22 respawn
23
24
25 env JUJU_MACHINE_ID="0"
26
27 env JUJU_ZOOKEEPER="localhost:2181"
28
29
30 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
31 --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
32 2>&1
33
34 EOF
35
36 ', /sbin/start juju-machine-agent, 'cat >> /etc/init/juju-provision-agent.conf
37 <<EOF
38
39 description "Juju provisioning agent"
40
41 author "Juju Team <juju@lists.ubuntu.com>"
42
43
44 start on runlevel [2345]
45
46 stop on runlevel [!2345]
47
48 respawn
49
50
51 env JUJU_ZOOKEEPER="localhost:2181"
52
53
54 exec python -m juju.agents.provision --nodaemon --logfile /var/log/juju/provision-agent.log
55 --session-file /var/run/juju/provision-agent.zksession >> /tmp/juju-provision-agent.output
56 2>&1
57
58 EOF
59
60 ', /sbin/start juju-provision-agent]
16ssh_authorized_keys: [zebra]61ssh_authorized_keys: [zebra]
1762
=== modified file 'juju/providers/ec2/tests/data/launch_cloud_init'
--- juju/providers/ec2/tests/data/launch_cloud_init 2012-01-09 14:17:21 +0000
+++ juju/providers/ec2/tests/data/launch_cloud_init 2012-02-02 16:42:42 +0000
@@ -4,10 +4,32 @@
4machine-data: {juju-provider-type: ec2, juju-zookeeper-hosts: 'es.example.internal:2181',4machine-data: {juju-provider-type: ec2, juju-zookeeper-hosts: 'es.example.internal:2181',
5 machine-id: '1'}5 machine-id: '1'}
6output: {all: '| tee -a /var/log/cloud-init-output.log'}6output: {all: '| tee -a /var/log/cloud-init-output.log'}
7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws,7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper]
8 python-zookeeper]8runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir -p
9runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir9 /var/log/juju, 'cat >> /etc/init/juju-machine-agent.conf <<EOF
10 -p /var/log/juju, 'JUJU_MACHINE_ID=1 JUJU_ZOOKEEPER=es.example.internal:218110
11 python -m juju.agents.machine -n --logfile=/var/log/juju/machine-agent.log11 description "Juju machine agent"
12 --pidfile=/var/run/juju/machine-agent.pid']12
13 author "Juju Team <juju@lists.ubuntu.com>"
14
15
16 start on runlevel [2345]
17
18 stop on runlevel [!2345]
19
20 respawn
21
22
23 env JUJU_MACHINE_ID="1"
24
25 env JUJU_ZOOKEEPER="es.example.internal:2181"
26
27
28 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
29 --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
30 2>&1
31
32 EOF
33
34 ', /sbin/start juju-machine-agent]
13ssh_authorized_keys: [zebra]35ssh_authorized_keys: [zebra]
1436
=== modified file 'juju/providers/ec2/tests/data/launch_cloud_init_branch'
--- juju/providers/ec2/tests/data/launch_cloud_init_branch 2012-01-09 14:17:21 +0000
+++ juju/providers/ec2/tests/data/launch_cloud_init_branch 2012-02-02 16:42:42 +0000
@@ -6,15 +6,34 @@
6machine-data: {juju-provider-type: ec2, juju-zookeeper-hosts: 'es.example.internal:2181',6machine-data: {juju-provider-type: ec2, juju-zookeeper-hosts: 'es.example.internal:2181',
7 machine-id: '1'}7 machine-id: '1'}
8output: {all: '| tee -a /var/log/cloud-init-output.log'}8output: {all: '| tee -a /var/log/cloud-init-output.log'}
9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws,9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper]
10 python-zookeeper]10runcmd: [sudo apt-get install -y python-txzookeeper, sudo mkdir -p /usr/lib/juju,
11runcmd: [sudo apt-get install -y python-txzookeeper,11 'cd /usr/lib/juju && sudo /usr/bin/bzr co lp:~wizard/juju-juicebar juju', cd /usr/lib/juju/juju
12 sudo mkdir -p /usr/lib/juju,12 && sudo python setup.py develop, sudo mkdir -p /var/lib/juju, sudo mkdir -p /var/log/juju,
13 'cd /usr/lib/juju && sudo /usr/bin/bzr co lp:~wizard/juju-juicebar juju',13 'cat >> /etc/init/juju-machine-agent.conf <<EOF
14 cd /usr/lib/juju/juju && sudo python setup.py develop,14
15 sudo mkdir -p /var/lib/juju,15 description "Juju machine agent"
16 sudo mkdir -p /var/log/juju,16
17 'JUJU_MACHINE_ID=1 JUJU_ZOOKEEPER=es.example.internal:218117 author "Juju Team <juju@lists.ubuntu.com>"
18 python -m juju.agents.machine -n --logfile=/var/log/juju/machine-agent.log18
19 --pidfile=/var/run/juju/machine-agent.pid']19
20 start on runlevel [2345]
21
22 stop on runlevel [!2345]
23
24 respawn
25
26
27 env JUJU_MACHINE_ID="1"
28
29 env JUJU_ZOOKEEPER="es.example.internal:2181"
30
31
32 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
33 --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
34 2>&1
35
36 EOF
37
38 ', /sbin/start juju-machine-agent]
20ssh_authorized_keys: [zebra]39ssh_authorized_keys: [zebra]
2140
=== modified file 'juju/providers/ec2/tests/data/launch_cloud_init_ppa'
--- juju/providers/ec2/tests/data/launch_cloud_init_ppa 2012-01-09 14:17:21 +0000
+++ juju/providers/ec2/tests/data/launch_cloud_init_ppa 2012-02-02 16:42:42 +0000
@@ -6,10 +6,32 @@
6machine-data: {juju-provider-type: ec2, juju-zookeeper-hosts: 'es.example.internal:2181',6machine-data: {juju-provider-type: ec2, juju-zookeeper-hosts: 'es.example.internal:2181',
7 machine-id: '1'}7 machine-id: '1'}
8output: {all: '| tee -a /var/log/cloud-init-output.log'}8output: {all: '| tee -a /var/log/cloud-init-output.log'}
9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws,9packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper]
10 python-zookeeper]10runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir -p
11runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir11 /var/log/juju, 'cat >> /etc/init/juju-machine-agent.conf <<EOF
12 -p /var/log/juju, 'JUJU_MACHINE_ID=1 JUJU_ZOOKEEPER=es.example.internal:218112
13 python -m juju.agents.machine -n --logfile=/var/log/juju/machine-agent.log13 description "Juju machine agent"
14 --pidfile=/var/run/juju/machine-agent.pid']14
15 author "Juju Team <juju@lists.ubuntu.com>"
16
17
18 start on runlevel [2345]
19
20 stop on runlevel [!2345]
21
22 respawn
23
24
25 env JUJU_MACHINE_ID="1"
26
27 env JUJU_ZOOKEEPER="es.example.internal:2181"
28
29
30 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
31 --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
32 2>&1
33
34 EOF
35
36 ', /sbin/start juju-machine-agent]
15ssh_authorized_keys: [zebra]37ssh_authorized_keys: [zebra]
1638
=== modified file 'juju/providers/local/__init__.py'
--- juju/providers/local/__init__.py 2011-11-16 13:56:03 +0000
+++ juju/providers/local/__init__.py 2012-02-02 16:42:42 +0000
@@ -102,11 +102,11 @@
102 # Starting provider storage server102 # Starting provider storage server
103 log.info("Starting storage server...")103 log.info("Starting storage server...")
104 storage_server = StorageServer(104 storage_server = StorageServer(
105 pid_file=os.path.join(self._directory, "storage-server.pid"),105 self._qualified_name,
106 storage_dir=os.path.join(self._directory, "files"),106 storage_dir=os.path.join(self._directory, "files"),
107 host=net_attributes["ip"]["address"],107 host=net_attributes["ip"]["address"],
108 port=get_open_port(net_attributes["ip"]["address"]),108 port=get_open_port(net_attributes["ip"]["address"]),
109 log_file=os.path.join(self._directory, "storage-server.log"))109 logfile=os.path.join(self._directory, "storage-server.log"))
110 yield storage_server.start()110 yield storage_server.start()
111111
112 # Save the zookeeper start to provider storage.112 # Save the zookeeper start to provider storage.
@@ -130,17 +130,15 @@
130 raise ProviderError(str(e))130 raise ProviderError(str(e))
131131
132 # Startup the machine agent132 # Startup the machine agent
133 pid_file = os.path.join(self._directory, "machine-agent.pid")
134 log_file = os.path.join(self._directory, "machine-agent.log")133 log_file = os.path.join(self._directory, "machine-agent.log")
135134
136 juju_origin = self.config.get("juju-origin")135 juju_origin = self.config.get("juju-origin")
137 agent = ManagedMachineAgent(pid_file,136 agent = ManagedMachineAgent(self._qualified_name,
138 zookeeper_hosts=zookeeper.address,137 zookeeper_hosts=zookeeper.address,
139 machine_id="0",138 machine_id="0",
140 juju_directory=self._directory,139 juju_directory=self._directory,
141 log_file=log_file,140 log_file=log_file,
142 juju_origin=juju_origin,141 juju_origin=juju_origin,
143 juju_unit_namespace=self._qualified_name,
144 public_key=public_key)142 public_key=public_key)
145 log.info(143 log.info(
146 "Starting machine agent (origin: %s)... ", agent.juju_origin)144 "Starting machine agent (origin: %s)... ", agent.juju_origin)
@@ -158,14 +156,12 @@
158156
159 # Stop the machine agent157 # Stop the machine agent
160 log.debug("Stopping machine agent...")158 log.debug("Stopping machine agent...")
161 pid_file = os.path.join(self._directory, "machine-agent.pid")159 agent = ManagedMachineAgent(self._qualified_name)
162 agent = ManagedMachineAgent(pid_file)
163 yield agent.stop()160 yield agent.stop()
164161
165 # Stop the storage server162 # Stop the storage server
166 log.debug("Stopping storage server...")163 log.debug("Stopping storage server...")
167 pid_file = os.path.join(self._directory, "storage-server.pid")164 storage_server = StorageServer(self._qualified_name)
168 storage_server = StorageServer(pid_file)
169 yield storage_server.stop()165 yield storage_server.stop()
170166
171 # Stop zookeeper167 # Stop zookeeper
172168
=== modified file 'juju/providers/local/agent.py'
--- juju/providers/local/agent.py 2011-10-05 12:14:41 +0000
+++ juju/providers/local/agent.py 2012-02-02 16:42:42 +0000
@@ -1,12 +1,6 @@
1import errno
2import os
3import pipes
4import subprocess
5import sys1import sys
62
7from twisted.internet.defer import inlineCallbacks, returnValue3from juju.lib.upstart import UpstartService
8from twisted.internet.threads import deferToThread
9
10from juju.providers.common.cloudinit import get_default_origin, BRANCH4from juju.providers.common.cloudinit import get_default_origin, BRANCH
115
126
@@ -15,11 +9,10 @@
15 agent_module = "juju.agents.machine"9 agent_module = "juju.agents.machine"
1610
17 def __init__(11 def __init__(
18 self, pid_file, zookeeper_hosts=None, machine_id="0",12 self, juju_unit_namespace, zookeeper_hosts=None,
19 log_file=None, juju_directory="/var/lib/juju",13 machine_id="0", log_file=None, juju_directory="/var/lib/juju",
20 juju_unit_namespace="", public_key=None, juju_origin="ppa"):14 public_key=None, juju_origin="ppa"):
21 """15 """
22 :param pid_file: Path to file used to store process id.
23 :param machine_id: machine id for the local machine.16 :param machine_id: machine id for the local machine.
24 :param zookeeper_hosts: Zookeeper hosts to connect.17 :param zookeeper_hosts: Zookeeper hosts to connect.
25 :param log_file: A file to use for the agent logs.18 :param log_file: A file to use for the agent logs.
@@ -31,95 +24,46 @@
31 :param public_key: An SSH public key (string) that will be24 :param public_key: An SSH public key (string) that will be
32 used in the container for access.25 used in the container for access.
33 """26 """
34 self._pid_file = pid_file
35 self._machine_id = machine_id
36 self._zookeeper_hosts = zookeeper_hosts
37 self._juju_directory = juju_directory
38 self._juju_unit_namespace = juju_unit_namespace
39 self._log_file = log_file
40 self._public_key = public_key
41 self._juju_origin = juju_origin27 self._juju_origin = juju_origin
42
43 if self._juju_origin is None:28 if self._juju_origin is None:
44 origin, source = get_default_origin()29 origin, source = get_default_origin()
45 if origin == BRANCH:30 if origin == BRANCH:
46 origin = source31 origin = source
47 self._juju_origin = origin32 self._juju_origin = origin
4833
34 env = {"JUJU_MACHINE_ID": machine_id,
35 "JUJU_ZOOKEEPER": zookeeper_hosts,
36 "JUJU_HOME": juju_directory,
37 "JUJU_ORIGIN": self._juju_origin,
38 "JUJU_UNIT_NS": juju_unit_namespace,
39 "PYTHONPATH": ":".join(sys.path)}
40 if public_key:
41 env["JUJU_PUBLIC_KEY"] = public_key
42
43 self._service = UpstartService(
44 "juju-%s-machine-agent" % juju_unit_namespace, use_sudo=True)
45 self._service.set_description(
46 "Juju machine agent for %s" % juju_unit_namespace)
47 self._service.set_environ(env)
48 self._service_args = [
49 "/usr/bin/python", "-m", self.agent_module,
50 "--nodaemon", "--logfile", log_file,
51 "--session-file",
52 "/var/run/juju/%s-machine-agent.zksession" % juju_unit_namespace]
53
49 @property54 @property
50 def juju_origin(self):55 def juju_origin(self):
51 return self._juju_origin56 return self._juju_origin
5257
53 @inlineCallbacks
54 def start(self):58 def start(self):
55 """Start the machine agent.59 """Start the machine agent."""
56 """60 self._service.set_command(" ".join(self._service_args))
57 assert self._zookeeper_hosts and self._log_file61 return self._service.start()
5862
59 if (yield self.is_running()):
60 return
61
62 # sudo even with -E will strip pythonpath, so pass it directly
63 # to the command.
64 args = ["sudo",
65 "JUJU_ZOOKEEPER=%s" % self._zookeeper_hosts,
66 "JUJU_ORIGIN=%s" % self._juju_origin,
67 "JUJU_MACHINE_ID=%s" % self._machine_id,
68 "JUJU_HOME=%s" % self._juju_directory,
69 "JUJU_UNIT_NS=%s" % self._juju_unit_namespace,
70 "PYTHONPATH=%s" % ":".join(sys.path),
71 sys.executable, "-m", self.agent_module,
72 "-n", "--pidfile", self._pid_file,
73 "--logfile", self._log_file]
74
75 if self._public_key:
76 args.insert(
77 1, "JUJU_PUBLIC_KEY=%s" % pipes.quote(self._public_key))
78
79 yield deferToThread(subprocess.check_call, args)
80
81 @inlineCallbacks
82 def stop(self):63 def stop(self):
83 """Stop the machine agent.64 """Stop the machine agent."""
84 """65 return self._service.destroy()
85 pid = yield self._get_pid()66
86 if pid is None:
87 return
88
89 # Verify the cmdline before attempting to kill.
90 try:
91 with open("/proc/%s/cmdline" % pid) as cmd_file:
92 cmdline = cmd_file.read()
93 if self.agent_module not in cmdline:
94 raise RuntimeError("Mismatch cmdline")
95 except IOError, e:
96 # Process already died.
97 if e.errno == errno.ENOENT:
98 return
99
100 yield deferToThread(
101 subprocess.check_call, ["sudo", "kill", str(pid)])
102
103 @inlineCallbacks
104 def _get_pid(self):
105 """Return the agent process id or None.
106 """
107 # Default root pidfile mask is restrictive
108 try:
109 pid = yield deferToThread(
110 subprocess.check_output,
111 ["sudo", "cat", self._pid_file],
112 stderr=subprocess.STDOUT)
113 except subprocess.CalledProcessError:
114 return
115 if not pid:
116 return
117 returnValue(int(pid.strip()))
118
119 @inlineCallbacks
120 def is_running(self):67 def is_running(self):
121 """Boolean value, true if the machine agent is running."""68 """Boolean value, true if the machine agent is running."""
122 pid = yield self._get_pid()69 return self._service.is_running()
123 if pid is None:
124 returnValue(False)
125 returnValue(os.path.isdir("/proc/%s" % pid))
12670
=== modified file 'juju/providers/local/files.py'
--- juju/providers/local/files.py 2011-10-07 18:19:58 +0000
+++ juju/providers/local/files.py 2012-02-02 16:42:42 +0000
@@ -1,13 +1,14 @@
1import errno1from getpass import getuser
2import os2import os
3import signal
4from StringIO import StringIO3from StringIO import StringIO
5import subprocess
6import yaml4import yaml
75
8from twisted.internet.defer import inlineCallbacks, returnValue6from twisted.internet.defer import inlineCallbacks, returnValue
7from twisted.internet.error import ConnectionRefusedError
8from twisted.web.client import getPage
99
10from juju.errors import ProviderError, FileNotFound10from juju.errors import ProviderError, FileNotFound
11from juju.lib.upstart import UpstartService
11from juju.providers.common.files import FileStorage12from juju.providers.common.files import FileStorage
1213
1314
@@ -16,22 +17,46 @@
1617
17class StorageServer(object):18class StorageServer(object):
1819
19 def __init__(20 def __init__(self, juju_unit_namespace, storage_dir=None,
20 self, pid_file, storage_dir=None, host=None, port=None, log_file=None):21 host=None, port=None, logfile=None):
21 """Management facade for a web server on top of the provider storage.22 """Management facade for a web server on top of the provider storage.
2223
23 :param pid_file: Path to the web server pid file.24 :param juju_unit_namespace: For disambiguation.
24 :param host: Host interface to bind to.25 :param host: Host interface to bind to.
25 :param port: Port to bind to.26 :param port: Port to bind to.
26 :param log_file: Path to store log output.27 :param logfile: Path to store log output.
27 """28 """
28 if storage_dir:29 if storage_dir:
29 storage_dir = os.path.abspath(storage_dir)30 storage_dir = os.path.abspath(storage_dir)
30 self._storage_dir = storage_dir31 self._storage_dir = storage_dir
31 self._host = host32 self._host = host
32 self._port = port33 self._port = port
33 self._pid_file = pid_file34 self._logfile = logfile
34 self._log_file = log_file35
36 self._service = UpstartService(
37 "juju-%s-file-storage" % juju_unit_namespace, use_sudo=True)
38 self._service.set_description(
39 "Juju file storage for %s" % juju_unit_namespace)
40 self._service_args = [
41 "twistd",
42 "--nodaemon",
43 "--uid", str(os.getuid()),
44 "--gid", str(os.getgid()),
45 "--logfile", logfile,
46 "--pidfile=",
47 "-d", self._storage_dir,
48 "web",
49 "--port", "tcp:%s:interface=%s" % (self._port, self._host),
50 "--path", self._storage_dir]
51
52 @inlineCallbacks
53 def is_serving(self):
54 try:
55 storage = LocalStorage(self._storage_dir)
56 yield getPage((yield storage.get_url(SERVER_URL_KEY)))
57 returnValue(True)
58 except ConnectionRefusedError:
59 returnValue(False)
3560
36 @inlineCallbacks61 @inlineCallbacks
37 def start(self):62 def start(self):
@@ -39,44 +64,33 @@
3964
40 Also stores the storage server url directly into provider storage.65 Also stores the storage server url directly into provider storage.
41 """66 """
42 assert (self._storage_dir and self._host67 assert self._storage_dir, "no storage_dir set"
43 and self._port and self._log_file), "Missing start params."68 assert self._host, "no host set"
69 assert self._port, "no port set"
70 assert None not in self._service_args, "unset params"
44 assert os.path.exists(self._storage_dir), "Invalid storage directory"71 assert os.path.exists(self._storage_dir), "Invalid storage directory"
72 try:
73 with open(self._logfile, "a"):
74 pass
75 except IOError:
76 raise AssertionError("logfile not writable by this user")
77
4578
46 storage = LocalStorage(self._storage_dir)79 storage = LocalStorage(self._storage_dir)
47 yield storage.put(80 yield storage.put(
48 SERVER_URL_KEY,81 SERVER_URL_KEY,
49 StringIO(yaml.safe_dump(82 StringIO(yaml.safe_dump(
50 {"storage-url": "http://%s:%s/" % (83 {"storage-url": "http://%s:%s/" % (self._host, self._port)})))
51 self._host, self._port)})))84
5285 self._service.set_command(" ".join(self._service_args))
53 subprocess.check_output(86 yield self._service.start()
54 ["twistd",87
55 "--pidfile", self._pid_file,88 def get_pid(self):
56 "--logfile", self._log_file,89 return self._service.get_pid()
57 "-d", self._storage_dir,
58 "web", "--port",
59 "tcp:%s:interface=%s" % (self._port, self._host),
60 "--path", self._storage_dir])
6190
62 def stop(self):91 def stop(self):
63 """Stop the storage server.92 """Stop the storage server."""
64 """93 return self._service.destroy()
65 try:
66 with open(self._pid_file) as pid_file:
67 pid = int(pid_file.read().strip())
68 except IOError:
69 # No pid, move on
70 return
71
72 try:
73 os.kill(pid, 0)
74 except OSError, e:
75 if e.errno == errno.ESRCH: # No such process, already dead.
76 return
77 raise
78
79 os.kill(pid, signal.SIGKILL)
8094
8195
82class LocalStorage(FileStorage):96class LocalStorage(FileStorage):
8397
=== modified file 'juju/providers/local/tests/test_agent.py'
--- juju/providers/local/tests/test_agent.py 2011-10-05 12:14:41 +0000
+++ juju/providers/local/tests/test_agent.py 2012-02-02 16:42:42 +0000
@@ -1,10 +1,13 @@
1import os1import os
2import tempfile2import tempfile
3import subprocess3import subprocess
4import sys
5
4from twisted.internet.defer import inlineCallbacks, succeed6from twisted.internet.defer import inlineCallbacks, succeed
57
8from juju.lib.lxc.tests.test_lxc import uses_sudo
6from juju.lib.testing import TestCase9from juju.lib.testing import TestCase
7from juju.lib.lxc.tests.test_lxc import run_lxc_tests10from juju.lib.upstart import UpstartService
8from juju.tests.common import get_test_zookeeper_address11from juju.tests.common import get_test_zookeeper_address
9from juju.providers.local.agent import ManagedMachineAgent12from juju.providers.local.agent import ManagedMachineAgent
1013
@@ -12,61 +15,93 @@
12class ManagedAgentTest(TestCase):15class ManagedAgentTest(TestCase):
1316
14 @inlineCallbacks17 @inlineCallbacks
15 def test_managed_agent_args(self):18 def test_managed_agent_config(self):
1619 subprocess_calls = []
17 captured_args = []20
1821 def intercept_args(args, **kwargs):
19 def intercept_args(args):22 subprocess_calls.append(args)
20 captured_args.extend(args)23 self.assertEquals(args[0], "sudo")
21 return True24 if args[1] == "cp":
2225 return real_check_call(args[1:], **kwargs)
23 self.patch(subprocess, "check_call", intercept_args)26 return 0
27
28 real_check_call = self.patch(subprocess, "check_call", intercept_args)
29 init_dir = self.makeDir()
30 self.patch(UpstartService, "init_dir", init_dir)
31
32 # Mock out the repeated checking for unstable pid, after an initial
33 # stop/waiting to induce the actual start
34 getProcessOutput = self.mocker.replace(
35 "twisted.internet.utils.getProcessOutput")
36 getProcessOutput("/sbin/status", ["juju-ns1-machine-agent"])
37 self.mocker.result(succeed("stop/waiting"))
38 for _ in range(5):
39 getProcessOutput("/sbin/status", ["juju-ns1-machine-agent"])
40 self.mocker.result(succeed("start/running 123"))
41 self.mocker.replay()
2442
25 juju_directory = self.makeDir()43 juju_directory = self.makeDir()
26 pid_file = self.makeFile()
27 log_file = self.makeFile()44 log_file = self.makeFile()
28
29 agent = ManagedMachineAgent(45 agent = ManagedMachineAgent(
30 pid_file, get_test_zookeeper_address(),46 "ns1",
47 get_test_zookeeper_address(),
31 juju_directory=juju_directory,48 juju_directory=juju_directory,
32 log_file=log_file, juju_unit_namespace="ns1",49 log_file=log_file,
33 juju_origin="lp:juju/trunk")50 juju_origin="lp:juju/trunk")
3451
35 mock_agent = self.mocker.patch(agent)52 try:
36 mock_agent.is_running()53 os.remove("/tmp/juju-ns1-machine-agent.output")
37 self.mocker.result(succeed(False))54 except OSError:
38 self.mocker.replay()55 pass # just make sure it's not there, so the .start()
56 # doesn't insert a spurious rm
3957
40 self.assertEqual(agent.juju_origin, "lp:juju/trunk")
41 yield agent.start()58 yield agent.start()
4259
43 # Verify machine agent environment60 conf_dest = os.path.join(
44 env_vars = dict(61 init_dir, "juju-ns1-machine-agent.conf")
45 [arg.split("=") for arg in captured_args if "=" in arg])62 chmod, start = subprocess_calls[1:]
46 env_vars.pop("PYTHONPATH")63 self.assertEquals(chmod, ("sudo", "chmod", "644", conf_dest))
47 self.assertEqual(64 self.assertEquals(
48 env_vars,65 start, ("sudo", "/sbin/start", "juju-ns1-machine-agent"))
49 dict(JUJU_ZOOKEEPER=get_test_zookeeper_address(),66
50 JUJU_MACHINE_ID="0",67 env = []
51 JUJU_HOME=juju_directory,68 with open(conf_dest) as f:
52 JUJU_ORIGIN="lp:juju/trunk",69 for line in f:
53 JUJU_UNIT_NS="ns1"))70 if line.startswith("env"):
5471 env.append(line[4:-1].split("=", 1))
72 if line.startswith("exec"):
73 exec_ = line[5:-1]
74
75 expect_exec = (
76 "/usr/bin/python -m juju.agents.machine --nodaemon --logfile %s "
77 "--session-file /var/run/juju/ns1-machine-agent.zksession "
78 ">> /tmp/juju-ns1-machine-agent.output 2>&1"
79 % log_file)
80 self.assertEquals(exec_, expect_exec)
81
82 env = dict((k, v.strip('"')) for (k, v) in env)
83 self.assertEquals(env, {
84 "JUJU_ZOOKEEPER": get_test_zookeeper_address(),
85 "JUJU_MACHINE_ID": "0",
86 "JUJU_HOME": juju_directory,
87 "JUJU_ORIGIN": "lp:juju/trunk",
88 "JUJU_UNIT_NS": "ns1",
89 "PYTHONPATH": ":".join(sys.path)})
90
91 @uses_sudo
55 @inlineCallbacks92 @inlineCallbacks
56 def test_managed_agent_root(self):93 def test_managed_agent_root(self):
57 juju_directory = self.makeDir()94 juju_directory = self.makeDir()
58 pid_file = tempfile.mktemp()
59 log_file = tempfile.mktemp()95 log_file = tempfile.mktemp()
6096
61 # The pid file and log file get written as root97 # The pid file and log file get written as root
62 def cleanup_root_file(cleanup_file):98 def cleanup_root_file(cleanup_file):
63 subprocess.check_call(99 subprocess.check_call(
64 ["sudo", "rm", "-f", cleanup_file], stderr=subprocess.STDOUT)100 ["sudo", "rm", "-f", cleanup_file], stderr=subprocess.STDOUT)
65 self.addCleanup(cleanup_root_file, pid_file)
66 self.addCleanup(cleanup_root_file, log_file)101 self.addCleanup(cleanup_root_file, log_file)
67102
68 agent = ManagedMachineAgent(103 agent = ManagedMachineAgent(
69 pid_file, machine_id="0", log_file=log_file,104 "test-ns", machine_id="0", log_file=log_file,
70 zookeeper_hosts=get_test_zookeeper_address(),105 zookeeper_hosts=get_test_zookeeper_address(),
71 juju_directory=juju_directory)106 juju_directory=juju_directory)
72107
@@ -85,13 +120,3 @@
85120
86 # running stop again is fine, detects the process is stopped.121 # running stop again is fine, detects the process is stopped.
87 yield agent.stop()122 yield agent.stop()
88
89 self.assertFalse(os.path.exists(pid_file))
90
91 # Stop raises runtime error if the process doesn't match up.
92 with open(pid_file, "w") as pid_handle:
93 pid_handle.write("1")
94 self.assertFailure(agent.stop(), RuntimeError)
95
96 # Reuse the lxc flag for tests needing sudo
97 test_managed_agent_root.skip = run_lxc_tests()
98123
=== modified file 'juju/providers/local/tests/test_files.py'
--- juju/providers/local/tests/test_files.py 2011-10-07 18:19:58 +0000
+++ juju/providers/local/tests/test_files.py 2012-02-02 16:42:42 +0000
@@ -1,14 +1,16 @@
1import os1import os
2import signal
2from StringIO import StringIO3from StringIO import StringIO
4import subprocess
3import yaml5import yaml
46
5from twisted.internet.defer import inlineCallbacks7from twisted.internet.defer import inlineCallbacks, succeed
6from twisted.web.client import getPage8from twisted.web.client import getPage
79
10from juju.errors import ProviderError, ServiceError
11from juju.lib.lxc.tests.test_lxc import uses_sudo
8from juju.lib.testing import TestCase12from juju.lib.testing import TestCase
913from juju.lib.upstart import UpstartService
10
11from juju.errors import ProviderError
12from juju.providers.local.files import (14from juju.providers.local.files import (
13 LocalStorage, StorageServer, SERVER_URL_KEY)15 LocalStorage, StorageServer, SERVER_URL_KEY)
14from juju.state.utils import get_open_port16from juju.state.utils import get_open_port
@@ -16,38 +18,151 @@
1618
17class WebFileStorageTest(TestCase):19class WebFileStorageTest(TestCase):
1820
21 @inlineCallbacks
19 def setUp(self):22 def setUp(self):
23 yield super(WebFileStorageTest, self).setUp()
20 self._storage_path = self.makeDir()24 self._storage_path = self.makeDir()
25 self._logfile = self.makeFile()
21 self._storage = LocalStorage(self._storage_path)26 self._storage = LocalStorage(self._storage_path)
22 self._log_path = self.makeFile()
23 self._pid_path = self.makeFile()
24 self._port = get_open_port()27 self._port = get_open_port()
25 self._server = StorageServer(28 self._server = StorageServer(
26 self._pid_path, self._storage_path, "localhost",29 "ns1", self._storage_path, "localhost", self._port, self._logfile)
27 get_open_port(), self._log_path)
2830
29 @inlineCallbacks31 @inlineCallbacks
30 def test_start_stop(self):32 def wait_for_server(self, server):
31 yield self._storage.put("abc", StringIO("hello world"))33 while not (yield server.is_serving()):
32 yield self._server.start()34 yield self.sleep(0.1)
33 storage_url = yield self._storage.get_url("abc")
34 contents = yield getPage(storage_url)
35 self.assertEqual("hello world", contents)
36 self._server.stop()
37 # Stopping multiple times is fine.
38 self._server.stop()
3935
40 def test_start_missing_args(self):36 def test_start_missing_args(self):
41 server = StorageServer(self._pid_path)37 server = StorageServer("ns1", self._storage_path)
42 return self.assertFailure(server.start(), AssertionError)38 return self.assertFailure(server.start(), AssertionError)
4339
44 def test_start_invalid_directory(self):40 def test_start_invalid_directory(self):
45 os.rmdir(self._storage_path)41 os.rmdir(self._storage_path)
46 return self.assertFailure(self._server.start(), AssertionError)42 return self.assertFailure(self._server.start(), AssertionError)
4743
48 def test_stop_missing_pid(self):44 @inlineCallbacks
49 server = StorageServer(self._pid_path)45 def test_upstart(self):
50 server.stop()46 subprocess_calls = []
47
48 def intercept_args(args, **kwargs):
49 subprocess_calls.append(args)
50 self.assertEquals(args[0], "sudo")
51 if args[1] == "cp":
52 return real_check_call(args[1:], **kwargs)
53 return 0
54
55 real_check_call = self.patch(subprocess, "check_call", intercept_args)
56 init_dir = self.makeDir()
57 self.patch(UpstartService, "init_dir", init_dir)
58
59 # Mock out the repeated checking for unstable pid, after an initial
60 # stop/waiting to induce the actual start
61 getProcessOutput = self.mocker.replace(
62 "twisted.internet.utils.getProcessOutput")
63 getProcessOutput("/sbin/status", ["juju-ns1-file-storage"])
64 self.mocker.result(succeed("stop/waiting"))
65 for _ in range(5):
66 getProcessOutput("/sbin/status", ["juju-ns1-file-storage"])
67 self.mocker.result(succeed("start/running 123"))
68 self.mocker.replay()
69
70 try:
71 os.remove("/tmp/juju-ns1-file-storage.output")
72 except OSError:
73 pass # just make sure it's not there, so the .start()
74 # doesn't insert a spurious rm
75
76 yield self._server.start()
77 chmod = subprocess_calls[1]
78 conf_dest = os.path.join(init_dir, "juju-ns1-file-storage.conf")
79 self.assertEquals(chmod, ("sudo", "chmod", "644", conf_dest))
80 start = subprocess_calls[-1]
81 self.assertEquals(
82 start, ("sudo", "/sbin/start", "juju-ns1-file-storage"))
83
84 with open(conf_dest) as f:
85 for line in f:
86 if line.startswith("env"):
87 self.fail("didn't expect any special environment")
88 if line.startswith("exec"):
89 exec_ = line[5:].strip()
90
91 expect_exec = (
92 "twistd --nodaemon --uid %s --gid %s --logfile %s --pidfile= -d "
93 "%s web --port tcp:%s:interface=localhost --path %s >> "
94 "/tmp/juju-ns1-file-storage.output 2>&1"
95 % (os.getuid(), os.getgid(), self._logfile, self._storage_path,
96 self._port, self._storage_path))
97 self.assertEquals(exec_, expect_exec)
98
99 @uses_sudo
100 @inlineCallbacks
101 def test_start_stop(self):
102 yield self._storage.put("abc", StringIO("hello world"))
103 yield self._server.start()
104 # Starting multiple times is fine.
105 yield self._server.start()
106 storage_url = yield self._storage.get_url("abc")
107
108 # It might not have started actually accepting connections yet...
109 yield self.wait_for_server(self._server)
110 self.assertEqual((yield getPage(storage_url)), "hello world")
111
112 # Check that it can be killed by the current user (ie, is not running
113 # as root) and still comes back up
114 old_pid = yield self._server.get_pid()
115 os.kill(old_pid, signal.SIGKILL)
116 new_pid = yield self._server.get_pid()
117 self.assertNotEquals(old_pid, new_pid)
118
119 # Give it a moment to actually start serving again
120 yield self.wait_for_server(self._server)
121 self.assertEqual((yield getPage(storage_url)), "hello world")
122
123 yield self._server.stop()
124 # Stopping multiple times is fine too.
125 yield self._server.stop()
126
127 @uses_sudo
128 @inlineCallbacks
129 def test_namespacing(self):
130 alt_storage_path = self.makeDir()
131 alt_storage = LocalStorage(alt_storage_path)
132 yield alt_storage.put("some-path", StringIO("alternative"))
133 yield self._storage.put("some-path", StringIO("original"))
134
135 alt_server = StorageServer(
136 "ns2", alt_storage_path, "localhost", get_open_port(),
137 self.makeFile())
138 yield alt_server.start()
139 yield self._server.start()
140 yield self.wait_for_server(alt_server)
141 yield self.wait_for_server(self._server)
142
143 alt_contents = yield getPage(
144 (yield alt_storage.get_url("some-path")))
145 self.assertEquals(alt_contents, "alternative")
146 orig_contents = yield getPage(
147 (yield self._storage.get_url("some-path")))
148 self.assertEquals(orig_contents, "original")
149
150 yield alt_server.stop()
151 yield self._server.stop()
152
153 @uses_sudo
154 @inlineCallbacks
155 def test_capture_errors(self):
156 self._port = get_open_port()
157 self._server = StorageServer(
158 "borken", self._storage_path, "lol borken", self._port,
159 self._logfile)
160 d = self._server.start()
161 e = yield self.assertFailure(d, ServiceError)
162 self.assertTrue(str(e).startswith(
163 "Failed to start job juju-borken-file-storage; got output:\n"))
164 self.assertIn("Wrong number of arguments", str(e))
165 yield self._server.stop()
51166
52167
53class FileStorageTest(TestCase):168class FileStorageTest(TestCase):
54169
=== modified file 'juju/providers/orchestra/tests/data/bootstrap_user_data'
--- juju/providers/orchestra/tests/data/bootstrap_user_data 2012-01-09 14:17:21 +0000
+++ juju/providers/orchestra/tests/data/bootstrap_user_data 2012-02-02 16:42:42 +0000
@@ -1,15 +1,60 @@
1#cloud-config
2apt-update: true1apt-update: true
3apt-upgrade: true2apt-upgrade: true
4machine-data: {juju-provider-type: orchestra, juju-zookeeper-hosts: 'localhost:2181',3machine-data: {juju-provider-type: orchestra, juju-zookeeper-hosts: 'localhost:2181',
5 machine-id: '0'}4 machine-id: '0'}
6output: {all: '| tee -a /var/log/cloud-init-output.log'}5output: {all: '| tee -a /var/log/cloud-init-output.log'}
7packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws,6packages: [bzr, byobu, tmux, python-setuptools, python-twisted, python-txaws, python-zookeeper,
8 python-zookeeper, default-jre-headless, zookeeper, zookeeperd]7 default-jre-headless, zookeeper, zookeeperd]
9runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir -p8runcmd: [sudo apt-get -y install juju, sudo mkdir -p /var/lib/juju, sudo mkdir -p
10 /var/log/juju, 'juju-admin initialize --instance-id=winston-uid --admin-identity=admin:qRBXC1ubEEUqRL6wcBhgmc9xkaY= --provider-type=orchestra',9 /var/log/juju, 'juju-admin initialize --instance-id=winston-uid --admin-identity=admin:qRBXC1ubEEUqRL6wcBhgmc9xkaY=
11 'JUJU_MACHINE_ID=0 JUJU_ZOOKEEPER=localhost:2181 python -m juju.agents.machine -n10 --provider-type=orchestra', 'cat >> /etc/init/juju-machine-agent.conf <<EOF
12 --logfile=/var/log/juju/machine-agent.log --pidfile=/var/run/juju/machine-agent.pid',11
13 'JUJU_ZOOKEEPER=localhost:2181 python -m juju.agents.provision -n --logfile=/var/log/juju/provision-agent.log12 description "Juju machine agent"
14 --pidfile=/var/run/juju/provision-agent.pid']13
14 author "Juju Team <juju@lists.ubuntu.com>"
15
16
17 start on runlevel [2345]
18
19 stop on runlevel [!2345]
20
21 respawn
22
23
24 env JUJU_MACHINE_ID="0"
25
26 env JUJU_ZOOKEEPER="localhost:2181"
27
28
29 exec python -m juju.agents.machine --nodaemon --logfile /var/log/juju/machine-agent.log
30 --session-file /var/run/juju/machine-agent.zksession >> /tmp/juju-machine-agent.output
31 2>&1
32
33 EOF
34
35 ', /sbin/start juju-machine-agent, 'cat >> /etc/init/juju-provision-agent.conf
36 <<EOF
37
38 description "Juju provisioning agent"
39
40 author "Juju Team <juju@lists.ubuntu.com>"
41
42
43 start on runlevel [2345]
44
45 stop on runlevel [!2345]
46
47 respawn
48
49
50 env JUJU_ZOOKEEPER="localhost:2181"
51
52
53 exec python -m juju.agents.provision --nodaemon --logfile /var/log/juju/provision-agent.log
54 --session-file /var/run/juju/provision-agent.zksession >> /tmp/juju-provision-agent.output
55 2>&1
56
57 EOF
58
59 ', /sbin/start juju-provision-agent]
15ssh_authorized_keys: [this-is-a-public-key]60ssh_authorized_keys: [this-is-a-public-key]
1661
=== modified file 'juju/providers/orchestra/tests/data/launch_user_data'
--- juju/providers/orchestra/tests/data/launch_user_data 2012-01-09 14:17:21 +0000
+++ juju/providers/orchestra/tests/data/launch_user_data 2012-02-02 16:42:42 +0000
@@ -1,12 +1,34 @@
1#cloud-config
2apt-update: true1apt-update: true
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to status/vote changes: