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
=== modified file 'docs/source/faq.rst'
--- docs/source/faq.rst 2011-06-20 19:04:35 +0000
+++ docs/source/faq.rst 2011-07-06 18:05:30 +0000
@@ -59,6 +59,13 @@
59 Also integration work with the `Orchestra <https://launchpad.net/orchestra>`_59 Also integration work with the `Orchestra <https://launchpad.net/orchestra>`_
60 project is underway to enable deployment to hardware machines60 project is underway to enable deployment to hardware machines
6161
62What directory are hooks executed in?
63
64 Hooks are executed in the formula directory (the parent directory to the hook
65 directory). This is primarily to encourage putting additional resources that
66 a hook may use outside of the hooks directory which is the public interface
67 of the formula.
68
62How can I contact the Ensemble team?69How can I contact the Ensemble team?
6370
64 User and formula author oriented resources71 User and formula author oriented resources
6572
=== modified file 'ensemble/hooks/invoker.py'
--- ensemble/hooks/invoker.py 2011-06-11 02:25:26 +0000
+++ ensemble/hooks/invoker.py 2011-07-06 18:05:30 +0000
@@ -57,18 +57,24 @@
57 `logger` -- instance of a logging.Logger object used to capture57 `logger` -- instance of a logging.Logger object used to capture
58 hook output.58 hook output.
59 """59 """
60 def __init__(self, context, change, client_id, socket_path, logger):60 def __init__(self, context, change, client_id, socket_path,
61 unit_path, logger):
61 self.environment = {}62 self.environment = {}
62 self._context = context63 self._context = context
63 self._change = change64 self._change = change
64 self._client_id = client_id65 self._client_id = client_id
65 self._socket_path = socket_path66 self._socket_path = socket_path
67 self._unit_path = unit_path
66 self._log = logger68 self._log = logger
67 # The twisted.internet.process.Process instance.69 # The twisted.internet.process.Process instance.
68 self._process = None70 self._process = None
69 # The hook executable path71 # The hook executable path
70 self._process_executable = None72 self._process_executable = None
7173
74 @property
75 def unit_path(self):
76 return self._unit_path
77
72 def get_environment(self):78 def get_environment(self):
73 """79 """
74 Returns the environment used to run the hook as a dict.80 Returns the environment used to run the hook as a dict.
@@ -78,6 +84,7 @@
78 """84 """
79 base = dict(ENSEMBLE_AGENT_SOCKET=self._socket_path,85 base = dict(ENSEMBLE_AGENT_SOCKET=self._socket_path,
80 ENSEMBLE_CLIENT_ID=self._client_id,86 ENSEMBLE_CLIENT_ID=self._client_id,
87 FORMULA_DIR=os.path.join(self._unit_path, "formula"),
81 ENSEMBLE_UNIT_NAME=os.environ["ENSEMBLE_UNIT_NAME"],88 ENSEMBLE_UNIT_NAME=os.environ["ENSEMBLE_UNIT_NAME"],
82 PATH=os.environ["PATH"],89 PATH=os.environ["PATH"],
83 PYTHON=os.environ.get("PYTHON", sys.executable),90 PYTHON=os.environ.get("PYTHON", sys.executable),
@@ -158,7 +165,9 @@
158165
159 return result166 return result
160167
161 self._process = reactor.spawnProcess(hook_proto, hook, [hook], env)168 self._process = reactor.spawnProcess(
169 hook_proto, hook, [hook], env,
170 os.path.join(self._unit_path, "formula"))
162 deferred.addBoth(cleanup_process)171 deferred.addBoth(cleanup_process)
163172
164 return deferred173 return deferred
165174
=== modified file 'ensemble/hooks/tests/test_executor.py'
--- ensemble/hooks/tests/test_executor.py 2011-06-03 14:17:31 +0000
+++ ensemble/hooks/tests/test_executor.py 2011-07-06 18:05:30 +0000
@@ -3,7 +3,6 @@
3import subprocess3import subprocess
44
5from twisted.internet.defer import inlineCallbacks, Deferred5from twisted.internet.defer import inlineCallbacks, Deferred
6from twisted.internet.process import Process
7from twisted.internet.error import ProcessExitedAlready6from twisted.internet.error import ProcessExitedAlready
87
9import ensemble.hooks.executor8import ensemble.hooks.executor
@@ -192,23 +191,30 @@
192 ensemble.hooks.executor, "DEBUG_HOOK_TEMPLATE",191 ensemble.hooks.executor, "DEBUG_HOOK_TEMPLATE",
193 "\n".join(("#!/bin/bash",192 "\n".join(("#!/bin/bash",
194 "exit_handler() {",193 "exit_handler() {",
195 " echo clean exit %s",194 " echo clean exit",
196 " exit 0",195 " exit 0",
197 "}",196 "}",
198 'trap "exit_handler" HUP',197 'trap "exit_handler" HUP',
199 "sleep 0.2",198 "sleep 0.2",
200 "exit 1")))199 "exit 1")))
201200
201 unit_dir = self.makeDir()
202
203 formula_dir = os.path.join(unit_dir, "formula")
204 self.makeDir(path=formula_dir)
205
202 self._executor.set_debug(["*"])206 self._executor.set_debug(["*"])
203 log = logging.getLogger("invoker")207 log = logging.getLogger("invoker")
204 # Populate environment variables for default invoker.208 # Populate environment variables for default invoker.
205 self.change_environment(209 self.change_environment(
206 ENSEMBLE_UNIT_NAME="dummy/1", PATH="/bin/:/usr/bin")210 ENSEMBLE_UNIT_NAME="dummy/1", PATH="/bin/:/usr/bin")
207 output = self.capture_logging("invoker", level=logging.DEBUG)211 output = self.capture_logging("invoker", level=logging.DEBUG)
208 invoker = Invoker(None, None, "constant", self.makeFile(), log)212 invoker = Invoker(
213 None, None, "constant", self.makeFile(), unit_dir, log)
209214
210 self._executor.start()215 self._executor.start()
211 hook_done = self._executor(invoker, "abc")216 hook_done = self._executor(invoker, "abc")
217
212 # Give a moment for execution to start.218 # Give a moment for execution to start.
213 yield self.sleep(0.1)219 yield self.sleep(0.1)
214 self._executor.set_debug(None)220 self._executor.set_debug(None)
215221
=== modified file 'ensemble/hooks/tests/test_invoker.py'
--- ensemble/hooks/tests/test_invoker.py 2011-06-11 02:37:35 +0000
+++ ensemble/hooks/tests/test_invoker.py 2011-07-06 18:05:30 +0000
@@ -8,7 +8,6 @@
8import yaml8import yaml
99
10from twisted.internet import defer10from twisted.internet import defer
11from twisted.python.log import PythonLoggingObserver
1211
13import ensemble12import ensemble
14from ensemble import errors13from ensemble import errors
@@ -26,9 +25,10 @@
26class MockUnitAgent(object):25class MockUnitAgent(object):
27 """Pretends to implement the client state cache, and the UA hook socket.26 """Pretends to implement the client state cache, and the UA hook socket.
28 """27 """
29 def __init__(self, client, socket_path):28 def __init__(self, client, socket_path, formula_dir):
30 self.client = client29 self.client = client
31 self.socket_path = socket_path30 self.socket_path = socket_path
31 self.formula_dir = formula_dir
32 self._clients = {} # client_id -> HookContext32 self._clients = {} # client_id -> HookContext
3333
34 self._agent_log = logging.getLogger("unit-agent")34 self._agent_log = logging.getLogger("unit-agent")
@@ -74,6 +74,7 @@
74 exe = invoker.Invoker(context, change,74 exe = invoker.Invoker(context, change,
75 self.get_client_id(),75 self.get_client_id(),
76 self.socket_path,76 self.socket_path,
77 self.formula_dir,
77 logger)78 logger)
78 return exe79 return exe
7980
@@ -185,7 +186,12 @@
185186
186 self.update_invoker_env("mysql/0", "wordpress/0")187 self.update_invoker_env("mysql/0", "wordpress/0")
187 self.socket_path = self.makeFile()188 self.socket_path = self.makeFile()
188 self.ua = MockUnitAgent(self.client, self.socket_path)189 unit_dir = self.makeDir()
190 self.makeDir(path=os.path.join(unit_dir, "formula"))
191 self.ua = MockUnitAgent(
192 self.client,
193 self.socket_path,
194 unit_dir)
189195
190 @defer.inlineCallbacks196 @defer.inlineCallbacks
191 def tearDown(self):197 def tearDown(self):
@@ -279,9 +285,38 @@
279 self.assertEqual(data["forgotten"], "lyrics")285 self.assertEqual(data["forgotten"], "lyrics")
280286
281 @defer.inlineCallbacks287 @defer.inlineCallbacks
288 def test_hook_exec_in_formula_directory(self):
289 """Hooks are executed in the formula directory."""
290 yield self.build_default_relationships()
291 hook_log = self.capture_logging("hook")
292 exe = self.ua.get_invoker("db", "add", "mysql/0", self.mysql_relation,
293 client_id="client_id")
294 self.assertTrue(os.path.isdir(exe.unit_path))
295 exe.environment["ENSEMBLE_REMOTE_UNIT"] = "wordpress/0"
296
297 # verify the hook's execution directory
298 hook_path = self.makeFile("#!/bin/bash\necho $PWD")
299 os.chmod(hook_path, stat.S_IEXEC | stat.S_IREAD)
300 result = yield exe(hook_path)
301 self.assertEqual(hook_log.getvalue().strip(),
302 os.path.join(exe.unit_path, "formula"))
303 self.assertEqual(result, 0)
304
305 # Reset the output capture
306 hook_log.seek(0)
307 hook_log.truncate()
308
309 # Verify the environment variable is set.
310 hook_path = self.makeFile("#!/bin/bash\necho $FORMULA_DIR")
311 os.chmod(hook_path, stat.S_IEXEC | stat.S_IREAD)
312 result = yield exe(hook_path)
313 self.assertEqual(hook_log.getvalue().strip(),
314 os.path.join(exe.unit_path, "formula"))
315
316 @defer.inlineCallbacks
282 def test_spawn_cli_config_get(self):317 def test_spawn_cli_config_get(self):
283 """Validate that config-get returns expected values."""318 """Validate that config-get returns expected values."""
284 state = yield self.build_default_relationships()319 yield self.build_default_relationships()
285320
286 hook_log = self.capture_logging("hook")321 hook_log = self.capture_logging("hook")
287322
@@ -294,13 +329,13 @@
294 exe = self.ua.get_invoker("db", "add", "mysql/0", self.mysql_relation,329 exe = self.ua.get_invoker("db", "add", "mysql/0", self.mysql_relation,
295 client_id="client_id")330 client_id="client_id")
296331
297 mysql = state["services"]["mysql"]
298 context = yield self.ua.get_context("client_id")332 context = yield self.ua.get_context("client_id")
299 config = yield context.get_config()333 config = yield context.get_config()
300 config.update(expected)334 config.update(expected)
301 yield config.write()335 yield config.write()
302336
303 # invoke relation-get and verify the result337 # invoke relation-get and verify the result
338
304 result = yield exe(self.create_hook("config-get", "--format=json"))339 result = yield exe(self.create_hook("config-get", "--format=json"))
305 self.assertEqual(result, 0)340 self.assertEqual(result, 0)
306341
@@ -318,7 +353,12 @@
318 yield self._default_relations()353 yield self._default_relations()
319 self.update_invoker_env("mysql/0", "wordpress/0")354 self.update_invoker_env("mysql/0", "wordpress/0")
320 self.socket_path = self.makeFile()355 self.socket_path = self.makeFile()
321 self.ua = MockUnitAgent(self.client, self.socket_path)356 unit_dir = self.makeDir()
357 self.makeDir(path=os.path.join(unit_dir, "formula"))
358 self.ua = MockUnitAgent(
359 self.client,
360 self.socket_path,
361 unit_dir)
322 self.log = self.capture_logging(362 self.log = self.capture_logging(
323 formatter=logging.Formatter(363 formatter=logging.Formatter(
324 "%(name)s:%(levelname)s:: %(message)s"),364 "%(name)s:%(levelname)s:: %(message)s"),
325365
=== modified file 'ensemble/unit/lifecycle.py'
--- ensemble/unit/lifecycle.py 2011-05-25 20:52:05 +0000
+++ ensemble/unit/lifecycle.py 2011-07-06 18:05:30 +0000
@@ -309,7 +309,8 @@
309 hook_path = os.path.join(unit_path, "formula", "hooks", hook_name)309 hook_path = os.path.join(unit_path, "formula", "hooks", hook_name)
310 socket_path = os.path.join(unit_path, HOOK_SOCKET_FILE)310 socket_path = os.path.join(unit_path, HOOK_SOCKET_FILE)
311311
312 invoker = Invoker(None, None, "constant", socket_path, hook_log)312 invoker = Invoker(
313 None, None, "constant", socket_path, self._unit_path, hook_log)
313314
314 if now:315 if now:
315 yield self._executor.run_priority_hook(invoker, hook_path)316 yield self._executor.run_priority_hook(invoker, hook_path)
@@ -387,8 +388,9 @@
387 hook_names = ["%s-relation-changed" % self._relation_name]388 hook_names = ["%s-relation-changed" % self._relation_name]
388 else:389 else:
389 hook_names = [hook_name]390 hook_names = [hook_name]
390 invoker = RelationInvoker(391
391 context, change, "constant", socket_path, hook_log)392 invoker = RelationInvoker(context, change, "constant", socket_path,
393 self._unit_path, hook_log)
392394
393 for hook_name in hook_names:395 for hook_name in hook_names:
394 hook_path = os.path.join(396 hook_path = os.path.join(
395397
=== modified file 'ensemble/unit/tests/test_lifecycle.py'
--- ensemble/unit/tests/test_lifecycle.py 2011-05-26 11:20:43 +0000
+++ ensemble/unit/tests/test_lifecycle.py 2011-07-06 18:05:30 +0000
@@ -750,10 +750,13 @@
750 PATH=os.environ["PATH"],750 PATH=os.environ["PATH"],
751 ENSEMBLE_UNIT_NAME="service-unit/0")751 ENSEMBLE_UNIT_NAME="service-unit/0")
752 change = RelationChange("clients", "joined", "s/2")752 change = RelationChange("clients", "joined", "s/2")
753 invoker = RelationInvoker(None, change, "", "", None)753 unit_hook_path = self.makeDir()
754 invoker = RelationInvoker(None, change, "", "", unit_hook_path, None)
754 environ = invoker.get_environment()755 environ = invoker.get_environment()
755 self.assertEqual(environ["ENSEMBLE_RELATION"], "clients")756 self.assertEqual(environ["ENSEMBLE_RELATION"], "clients")
756 self.assertEqual(environ["ENSEMBLE_REMOTE_UNIT"], "s/2")757 self.assertEqual(environ["ENSEMBLE_REMOTE_UNIT"], "s/2")
758 self.assertEqual(environ["FORMULA_DIR"],
759 os.path.join(unit_hook_path, "formula"))
757760
758761
759class UnitRelationLifecycleTest(LifecycleTestBase):762class UnitRelationLifecycleTest(LifecycleTestBase):

Subscribers

People subscribed via source and target branches

to status/vote changes: