Merge lp:~hazmat/pyjuju/hooks-with-formula-dir into lp:pyjuju

Proposed by Kapil Thangavelu
Status: Merged
Approved by: Kapil Thangavelu
Approved revision: 262
Merged at revision: 267
Proposed branch: lp:~hazmat/pyjuju/hooks-with-formula-dir
Merge into: lp:pyjuju
Diff against target: 271 lines (+82/-15)
6 files modified
docs/source/faq.rst (+7/-0)
ensemble/hooks/invoker.py (+11/-2)
ensemble/hooks/tests/test_executor.py (+9/-3)
ensemble/hooks/tests/test_invoker.py (+46/-6)
ensemble/unit/lifecycle.py (+5/-3)
ensemble/unit/tests/test_lifecycle.py (+4/-1)
To merge this branch: bzr merge lp:~hazmat/pyjuju/hooks-with-formula-dir
Reviewer Review Type Date Requested Status
William Reade (community) Approve
Gustavo Niemeyer Approve
Jim Baker Pending
Review via email: mp+65557@code.launchpad.net

Description of the change

Hooks are now executed in the unit root directory, and have a FORMULA_DIR environment variable pointing to the formula directory.

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

[1]

We talked about having the formula path as the cwd, and I've seen at least one person mentioning the expectation that the cwd is actually formula/hooks. I'd probably be worth to do a quick individual questioning about what each formula author we have expects cwd to be, and use that instead of the unit dir.

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

Putting it to WIP for pondering about [1] and so that the second reviewer gets to see the result of it.

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

Excerpts from Gustavo Niemeyer's message of Wed Jul 06 00:51:26 UTC 2011:
> [1]
>
> We talked about having the formula path as the cwd, and I've seen at least one person mentioning the expectation that the cwd is actually formula/hooks. I'd probably be worth to do a quick individual questioning about what each formula author we have expects cwd to be, and use that instead of the unit dir.

from irc

<hazmat> a question for formula authors in the room, which directory would you want your hooks to run in.. The formula hooks directory, the formula directory, or the root of the unit directory (the formula for the unit lives at '/formula' under the unit root)?
<fwereade> what else lives in the unit root at the moment?
<fwereade> the formula directory feels natural to me but I am basically 100% ignorant
<hazmat> fwereade, with lxc, the unit root will be the fs root "/" of the container
<fwereade> ah ok
<hazmat> at the moment, there's just the service unit log file and the formula directory in the root
<fwereade> ok, the hooks directory feels wrong because... well, it's the formula's interface, and if we run in that dir we encourage people to clutter it up with other stuff
<fwereade> I don't think I have a context to judge the distinction between running in the formula dir and the unit root
<hazmat> fwereade, agreed, authors can include arbitrary library or configuration stuff in the formula, its probably nicer to address that from the root, then including a '..' everywhere or cluttering up the hooks directory
<hazmat> er... s/root/ i mean the formula directory
<fwereade> yep
* hazmat makes it so

262. By Kapil Thangavelu

merge the debug-with-formula-dir, forget this was setup as a bzr-pipeline

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

This looks good, thanks!

Two details to ponder about before merging:

[2]

The variable name: should it be prefixed by ENSEMBLE_ like the rest of them? Is it more common to have _PATH rather than _DIR?

[3]

Would be good to document the directory next to the hooks description in the formulas spec.

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

hmm.. i actually meant to yank the formula dir env variable, but i had shelved it while resolving some test failures. i don't really see the need for it anymore since its the starting current dir of the executing hook. some hooks may want the env variable, but they can easily save cwd if they do.

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

Can we name it as ENSEMBLE_FORMULA_PATH then, since it's not so common anymore?

I think it's still useful to have it around, for the purpose of tools. It's convenient to be able to call anything in a tool and have the tool knowing where to find things relative to the formula, independently from the CWD (e.g. templates).

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

Looks good to me(as do niemeyer's suggestions).

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'docs/source/faq.rst'
2--- docs/source/faq.rst 2011-06-20 19:04:35 +0000
3+++ docs/source/faq.rst 2011-07-06 18:05:30 +0000
4@@ -59,6 +59,13 @@
5 Also integration work with the `Orchestra <https://launchpad.net/orchestra>`_
6 project is underway to enable deployment to hardware machines
7
8+What directory are hooks executed in?
9+
10+ Hooks are executed in the formula directory (the parent directory to the hook
11+ directory). This is primarily to encourage putting additional resources that
12+ a hook may use outside of the hooks directory which is the public interface
13+ of the formula.
14+
15 How can I contact the Ensemble team?
16
17 User and formula author oriented resources
18
19=== modified file 'ensemble/hooks/invoker.py'
20--- ensemble/hooks/invoker.py 2011-06-11 02:25:26 +0000
21+++ ensemble/hooks/invoker.py 2011-07-06 18:05:30 +0000
22@@ -57,18 +57,24 @@
23 `logger` -- instance of a logging.Logger object used to capture
24 hook output.
25 """
26- def __init__(self, context, change, client_id, socket_path, logger):
27+ def __init__(self, context, change, client_id, socket_path,
28+ unit_path, logger):
29 self.environment = {}
30 self._context = context
31 self._change = change
32 self._client_id = client_id
33 self._socket_path = socket_path
34+ self._unit_path = unit_path
35 self._log = logger
36 # The twisted.internet.process.Process instance.
37 self._process = None
38 # The hook executable path
39 self._process_executable = None
40
41+ @property
42+ def unit_path(self):
43+ return self._unit_path
44+
45 def get_environment(self):
46 """
47 Returns the environment used to run the hook as a dict.
48@@ -78,6 +84,7 @@
49 """
50 base = dict(ENSEMBLE_AGENT_SOCKET=self._socket_path,
51 ENSEMBLE_CLIENT_ID=self._client_id,
52+ FORMULA_DIR=os.path.join(self._unit_path, "formula"),
53 ENSEMBLE_UNIT_NAME=os.environ["ENSEMBLE_UNIT_NAME"],
54 PATH=os.environ["PATH"],
55 PYTHON=os.environ.get("PYTHON", sys.executable),
56@@ -158,7 +165,9 @@
57
58 return result
59
60- self._process = reactor.spawnProcess(hook_proto, hook, [hook], env)
61+ self._process = reactor.spawnProcess(
62+ hook_proto, hook, [hook], env,
63+ os.path.join(self._unit_path, "formula"))
64 deferred.addBoth(cleanup_process)
65
66 return deferred
67
68=== modified file 'ensemble/hooks/tests/test_executor.py'
69--- ensemble/hooks/tests/test_executor.py 2011-06-03 14:17:31 +0000
70+++ ensemble/hooks/tests/test_executor.py 2011-07-06 18:05:30 +0000
71@@ -3,7 +3,6 @@
72 import subprocess
73
74 from twisted.internet.defer import inlineCallbacks, Deferred
75-from twisted.internet.process import Process
76 from twisted.internet.error import ProcessExitedAlready
77
78 import ensemble.hooks.executor
79@@ -192,23 +191,30 @@
80 ensemble.hooks.executor, "DEBUG_HOOK_TEMPLATE",
81 "\n".join(("#!/bin/bash",
82 "exit_handler() {",
83- " echo clean exit %s",
84+ " echo clean exit",
85 " exit 0",
86 "}",
87 'trap "exit_handler" HUP',
88 "sleep 0.2",
89 "exit 1")))
90
91+ unit_dir = self.makeDir()
92+
93+ formula_dir = os.path.join(unit_dir, "formula")
94+ self.makeDir(path=formula_dir)
95+
96 self._executor.set_debug(["*"])
97 log = logging.getLogger("invoker")
98 # Populate environment variables for default invoker.
99 self.change_environment(
100 ENSEMBLE_UNIT_NAME="dummy/1", PATH="/bin/:/usr/bin")
101 output = self.capture_logging("invoker", level=logging.DEBUG)
102- invoker = Invoker(None, None, "constant", self.makeFile(), log)
103+ invoker = Invoker(
104+ None, None, "constant", self.makeFile(), unit_dir, log)
105
106 self._executor.start()
107 hook_done = self._executor(invoker, "abc")
108+
109 # Give a moment for execution to start.
110 yield self.sleep(0.1)
111 self._executor.set_debug(None)
112
113=== modified file 'ensemble/hooks/tests/test_invoker.py'
114--- ensemble/hooks/tests/test_invoker.py 2011-06-11 02:37:35 +0000
115+++ ensemble/hooks/tests/test_invoker.py 2011-07-06 18:05:30 +0000
116@@ -8,7 +8,6 @@
117 import yaml
118
119 from twisted.internet import defer
120-from twisted.python.log import PythonLoggingObserver
121
122 import ensemble
123 from ensemble import errors
124@@ -26,9 +25,10 @@
125 class MockUnitAgent(object):
126 """Pretends to implement the client state cache, and the UA hook socket.
127 """
128- def __init__(self, client, socket_path):
129+ def __init__(self, client, socket_path, formula_dir):
130 self.client = client
131 self.socket_path = socket_path
132+ self.formula_dir = formula_dir
133 self._clients = {} # client_id -> HookContext
134
135 self._agent_log = logging.getLogger("unit-agent")
136@@ -74,6 +74,7 @@
137 exe = invoker.Invoker(context, change,
138 self.get_client_id(),
139 self.socket_path,
140+ self.formula_dir,
141 logger)
142 return exe
143
144@@ -185,7 +186,12 @@
145
146 self.update_invoker_env("mysql/0", "wordpress/0")
147 self.socket_path = self.makeFile()
148- self.ua = MockUnitAgent(self.client, self.socket_path)
149+ unit_dir = self.makeDir()
150+ self.makeDir(path=os.path.join(unit_dir, "formula"))
151+ self.ua = MockUnitAgent(
152+ self.client,
153+ self.socket_path,
154+ unit_dir)
155
156 @defer.inlineCallbacks
157 def tearDown(self):
158@@ -279,9 +285,38 @@
159 self.assertEqual(data["forgotten"], "lyrics")
160
161 @defer.inlineCallbacks
162+ def test_hook_exec_in_formula_directory(self):
163+ """Hooks are executed in the formula directory."""
164+ yield self.build_default_relationships()
165+ hook_log = self.capture_logging("hook")
166+ exe = self.ua.get_invoker("db", "add", "mysql/0", self.mysql_relation,
167+ client_id="client_id")
168+ self.assertTrue(os.path.isdir(exe.unit_path))
169+ exe.environment["ENSEMBLE_REMOTE_UNIT"] = "wordpress/0"
170+
171+ # verify the hook's execution directory
172+ hook_path = self.makeFile("#!/bin/bash\necho $PWD")
173+ os.chmod(hook_path, stat.S_IEXEC | stat.S_IREAD)
174+ result = yield exe(hook_path)
175+ self.assertEqual(hook_log.getvalue().strip(),
176+ os.path.join(exe.unit_path, "formula"))
177+ self.assertEqual(result, 0)
178+
179+ # Reset the output capture
180+ hook_log.seek(0)
181+ hook_log.truncate()
182+
183+ # Verify the environment variable is set.
184+ hook_path = self.makeFile("#!/bin/bash\necho $FORMULA_DIR")
185+ os.chmod(hook_path, stat.S_IEXEC | stat.S_IREAD)
186+ result = yield exe(hook_path)
187+ self.assertEqual(hook_log.getvalue().strip(),
188+ os.path.join(exe.unit_path, "formula"))
189+
190+ @defer.inlineCallbacks
191 def test_spawn_cli_config_get(self):
192 """Validate that config-get returns expected values."""
193- state = yield self.build_default_relationships()
194+ yield self.build_default_relationships()
195
196 hook_log = self.capture_logging("hook")
197
198@@ -294,13 +329,13 @@
199 exe = self.ua.get_invoker("db", "add", "mysql/0", self.mysql_relation,
200 client_id="client_id")
201
202- mysql = state["services"]["mysql"]
203 context = yield self.ua.get_context("client_id")
204 config = yield context.get_config()
205 config.update(expected)
206 yield config.write()
207
208 # invoke relation-get and verify the result
209+
210 result = yield exe(self.create_hook("config-get", "--format=json"))
211 self.assertEqual(result, 0)
212
213@@ -318,7 +353,12 @@
214 yield self._default_relations()
215 self.update_invoker_env("mysql/0", "wordpress/0")
216 self.socket_path = self.makeFile()
217- self.ua = MockUnitAgent(self.client, self.socket_path)
218+ unit_dir = self.makeDir()
219+ self.makeDir(path=os.path.join(unit_dir, "formula"))
220+ self.ua = MockUnitAgent(
221+ self.client,
222+ self.socket_path,
223+ unit_dir)
224 self.log = self.capture_logging(
225 formatter=logging.Formatter(
226 "%(name)s:%(levelname)s:: %(message)s"),
227
228=== modified file 'ensemble/unit/lifecycle.py'
229--- ensemble/unit/lifecycle.py 2011-05-25 20:52:05 +0000
230+++ ensemble/unit/lifecycle.py 2011-07-06 18:05:30 +0000
231@@ -309,7 +309,8 @@
232 hook_path = os.path.join(unit_path, "formula", "hooks", hook_name)
233 socket_path = os.path.join(unit_path, HOOK_SOCKET_FILE)
234
235- invoker = Invoker(None, None, "constant", socket_path, hook_log)
236+ invoker = Invoker(
237+ None, None, "constant", socket_path, self._unit_path, hook_log)
238
239 if now:
240 yield self._executor.run_priority_hook(invoker, hook_path)
241@@ -387,8 +388,9 @@
242 hook_names = ["%s-relation-changed" % self._relation_name]
243 else:
244 hook_names = [hook_name]
245- invoker = RelationInvoker(
246- context, change, "constant", socket_path, hook_log)
247+
248+ invoker = RelationInvoker(context, change, "constant", socket_path,
249+ self._unit_path, hook_log)
250
251 for hook_name in hook_names:
252 hook_path = os.path.join(
253
254=== modified file 'ensemble/unit/tests/test_lifecycle.py'
255--- ensemble/unit/tests/test_lifecycle.py 2011-05-26 11:20:43 +0000
256+++ ensemble/unit/tests/test_lifecycle.py 2011-07-06 18:05:30 +0000
257@@ -750,10 +750,13 @@
258 PATH=os.environ["PATH"],
259 ENSEMBLE_UNIT_NAME="service-unit/0")
260 change = RelationChange("clients", "joined", "s/2")
261- invoker = RelationInvoker(None, change, "", "", None)
262+ unit_hook_path = self.makeDir()
263+ invoker = RelationInvoker(None, change, "", "", unit_hook_path, None)
264 environ = invoker.get_environment()
265 self.assertEqual(environ["ENSEMBLE_RELATION"], "clients")
266 self.assertEqual(environ["ENSEMBLE_REMOTE_UNIT"], "s/2")
267+ self.assertEqual(environ["FORMULA_DIR"],
268+ os.path.join(unit_hook_path, "formula"))
269
270
271 class UnitRelationLifecycleTest(LifecycleTestBase):

Subscribers

People subscribed via source and target branches

to status/vote changes: