Revision dd7f6776
b/doc/hooks.rst | ||
---|---|---|
64 | 64 |
Naming |
65 | 65 |
~~~~~~ |
66 | 66 |
|
67 |
The allowed names for the scripts consist of (similar to *run-parts* )
|
|
67 |
The allowed names for the scripts consist of (similar to *run-parts*) |
|
68 | 68 |
upper and lower case, digits, underscores and hyphens. In other words, |
69 | 69 |
the regexp ``^[a-zA-Z0-9_-]+$``. Also, non-executable scripts will be |
70 | 70 |
ignored. |
... | ... | |
468 | 468 |
Environment variables |
469 | 469 |
--------------------- |
470 | 470 |
|
471 |
Note that all variables listed here are actually prefixed with |
|
472 |
*GANETI_* in order to provide a clear namespace. |
|
471 |
Note that all variables listed here are actually prefixed with *GANETI_* |
|
472 |
in order to provide a clear namespace. In addition, post-execution |
|
473 |
scripts receive another set of variables, prefixed with *GANETI_POST_*, |
|
474 |
representing the status after the opcode executed. |
|
473 | 475 |
|
474 | 476 |
Common variables |
475 | 477 |
~~~~~~~~~~~~~~~~ |
b/lib/mcpu.py | ||
---|---|---|
427 | 427 |
self.callfn = callfn |
428 | 428 |
self.lu = lu |
429 | 429 |
self.op = lu.op |
430 |
self.env, node_list_pre, node_list_post = self._BuildEnv() |
|
431 |
self.node_list = { |
|
432 |
constants.HOOKS_PHASE_PRE: node_list_pre, |
|
433 |
constants.HOOKS_PHASE_POST: node_list_post, |
|
434 |
} |
|
430 |
self.pre_env = None |
|
431 |
self.pre_nodes = None |
|
435 | 432 |
|
436 |
def _BuildEnv(self): |
|
433 |
def _BuildEnv(self, phase):
|
|
437 | 434 |
"""Compute the environment and the target nodes. |
438 | 435 |
|
439 | 436 |
Based on the opcode and the current node list, this builds the |
440 | 437 |
environment for the hooks and the target node list for the run. |
441 | 438 |
|
442 | 439 |
""" |
443 |
env = { |
|
444 |
"PATH": "/sbin:/bin:/usr/sbin:/usr/bin", |
|
445 |
"GANETI_HOOKS_VERSION": constants.HOOKS_VERSION, |
|
446 |
"GANETI_OP_CODE": self.op.OP_ID, |
|
447 |
"GANETI_OBJECT_TYPE": self.lu.HTYPE, |
|
448 |
"GANETI_DATA_DIR": constants.DATA_DIR, |
|
449 |
} |
|
440 |
if phase == constants.HOOKS_PHASE_PRE: |
|
441 |
prefix = "GANETI_" |
|
442 |
elif phase == constants.HOOKS_PHASE_POST: |
|
443 |
prefix = "GANETI_POST_" |
|
444 |
else: |
|
445 |
raise AssertionError("Unknown phase '%s'" % phase) |
|
446 |
|
|
447 |
env = {} |
|
450 | 448 |
|
451 | 449 |
if self.lu.HPATH is not None: |
452 | 450 |
(lu_env, lu_nodes_pre, lu_nodes_post) = self.lu.BuildHooksEnv() |
453 | 451 |
if lu_env: |
454 |
assert not compat.any(key.upper().startswith("GANETI")
|
|
452 |
assert not compat.any(key.upper().startswith(prefix)
|
|
455 | 453 |
for key in lu_env) |
456 |
env.update(("GANETI_%s" % key, value) for (key, value) in lu_env) |
|
454 |
env.update(("%s%s" % (prefix, key), value) |
|
455 |
for (key, value) in lu_env.items()) |
|
457 | 456 |
else: |
458 | 457 |
lu_nodes_pre = lu_nodes_post = [] |
459 | 458 |
|
459 |
if phase == constants.HOOKS_PHASE_PRE: |
|
460 |
assert compat.all((key.startswith("GANETI_") and |
|
461 |
not key.startswith("GANETI_POST_")) |
|
462 |
for key in env) |
|
463 |
|
|
464 |
# Record environment for any post-phase hooks |
|
465 |
self.pre_env = env |
|
466 |
|
|
467 |
elif phase == constants.HOOKS_PHASE_POST: |
|
468 |
assert compat.all(key.startswith("GANETI_POST_") for key in env) |
|
469 |
|
|
470 |
if self.pre_env: |
|
471 |
assert not compat.any(key.startswith("GANETI_POST_") |
|
472 |
for key in self.pre_env) |
|
473 |
env.update(self.pre_env) |
|
474 |
else: |
|
475 |
raise AssertionError("Unknown phase '%s'" % phase) |
|
476 |
|
|
460 | 477 |
return env, frozenset(lu_nodes_pre), frozenset(lu_nodes_post) |
461 | 478 |
|
462 |
def _RunWrapper(self, node_list, hpath, phase): |
|
479 |
def _RunWrapper(self, node_list, hpath, phase, phase_env):
|
|
463 | 480 |
"""Simple wrapper over self.callfn. |
464 | 481 |
|
465 | 482 |
This method fixes the environment before doing the rpc call. |
466 | 483 |
|
467 | 484 |
""" |
468 |
env = self.env.copy() |
|
469 |
env["GANETI_HOOKS_PHASE"] = phase |
|
470 |
env["GANETI_HOOKS_PATH"] = hpath |
|
471 |
if self.lu.cfg is not None: |
|
472 |
env["GANETI_CLUSTER"] = self.lu.cfg.GetClusterName() |
|
473 |
env["GANETI_MASTER"] = self.lu.cfg.GetMasterNode() |
|
485 |
cfg = self.lu.cfg |
|
486 |
|
|
487 |
env = { |
|
488 |
"PATH": "/sbin:/bin:/usr/sbin:/usr/bin", |
|
489 |
"GANETI_HOOKS_VERSION": constants.HOOKS_VERSION, |
|
490 |
"GANETI_OP_CODE": self.op.OP_ID, |
|
491 |
"GANETI_OBJECT_TYPE": self.lu.HTYPE, |
|
492 |
"GANETI_DATA_DIR": constants.DATA_DIR, |
|
493 |
"GANETI_HOOKS_PHASE": phase, |
|
494 |
"GANETI_HOOKS_PATH": hpath, |
|
495 |
} |
|
496 |
|
|
497 |
if cfg is not None: |
|
498 |
env["GANETI_CLUSTER"] = cfg.GetClusterName() |
|
499 |
env["GANETI_MASTER"] = cfg.GetMasterNode() |
|
500 |
|
|
501 |
if phase_env: |
|
502 |
assert not (set(env) & set(phase_env)), "Environment variables conflict" |
|
503 |
env.update(phase_env) |
|
474 | 504 |
|
505 |
# Convert everything to strings |
|
475 | 506 |
env = dict([(str(key), str(val)) for key, val in env.iteritems()]) |
476 | 507 |
|
477 |
assert compat.all(key == key.upper() and |
|
478 |
(key == "PATH" or key.startswith("GANETI_")) |
|
508 |
assert compat.all(key == "PATH" or key.startswith("GANETI_") |
|
479 | 509 |
for key in env) |
480 | 510 |
|
481 | 511 |
return self.callfn(node_list, hpath, phase, env) |
... | ... | |
493 | 523 |
@raise errors.HooksAbort: on failure of one of the hooks |
494 | 524 |
|
495 | 525 |
""" |
526 |
(env, node_list_pre, node_list_post) = self._BuildEnv(phase) |
|
496 | 527 |
if nodes is None: |
497 |
nodes = self.node_list[phase] |
|
528 |
if phase == constants.HOOKS_PHASE_PRE: |
|
529 |
self.pre_nodes = (node_list_pre, node_list_post) |
|
530 |
nodes = node_list_pre |
|
531 |
elif phase == constants.HOOKS_PHASE_POST: |
|
532 |
post_nodes = (node_list_pre, node_list_post) |
|
533 |
assert self.pre_nodes == post_nodes, \ |
|
534 |
("Node lists returned for post-phase hook don't match pre-phase" |
|
535 |
" lists (pre %s, post %s)" % (self.pre_nodes, post_nodes)) |
|
536 |
nodes = node_list_post |
|
537 |
else: |
|
538 |
raise AssertionError("Unknown phase '%s'" % phase) |
|
498 | 539 |
|
499 | 540 |
if not nodes: |
500 | 541 |
# empty node list, we should not attempt to run this as either |
... | ... | |
502 | 543 |
# even attempt to run, or this LU doesn't do hooks at all |
503 | 544 |
return |
504 | 545 |
|
505 |
results = self._RunWrapper(nodes, self.lu.HPATH, phase) |
|
546 |
results = self._RunWrapper(nodes, self.lu.HPATH, phase, env)
|
|
506 | 547 |
if not results: |
507 | 548 |
msg = "Communication Failure" |
508 | 549 |
if phase == constants.HOOKS_PHASE_PRE: |
... | ... | |
545 | 586 |
top-level LI if the configuration has been updated. |
546 | 587 |
|
547 | 588 |
""" |
589 |
if self.pre_env is None: |
|
590 |
raise AssertionError("Pre-phase must be run before configuration update") |
|
591 |
|
|
548 | 592 |
phase = constants.HOOKS_PHASE_POST |
549 | 593 |
hpath = constants.HOOKS_NAME_CFGUPDATE |
550 | 594 |
nodes = [self.lu.cfg.GetMasterNode()] |
551 |
self._RunWrapper(nodes, hpath, phase) |
|
595 |
self._RunWrapper(nodes, hpath, phase, self.pre_env) |
b/test/ganeti.hooks_unittest.py | ||
---|---|---|
35 | 35 |
from ganeti import constants |
36 | 36 |
from ganeti import cmdlib |
37 | 37 |
from ganeti import rpc |
38 |
from ganeti import compat |
|
38 | 39 |
from ganeti.constants import HKR_SUCCESS, HKR_FAIL, HKR_SKIP |
39 | 40 |
|
40 | 41 |
from mocks import FakeConfig, FakeProc, FakeContext |
... | ... | |
191 | 192 |
[(self._rname(fname), HKR_SUCCESS, env_exp)]) |
192 | 193 |
|
193 | 194 |
|
195 |
def FakeHooksRpcSuccess(node_list, hpath, phase, env): |
|
196 |
"""Fake call_hooks_runner function. |
|
197 |
|
|
198 |
@rtype: dict of node -> L{rpc.RpcResult} with a successful script result |
|
199 |
@return: script execution from all nodes |
|
200 |
|
|
201 |
""" |
|
202 |
rr = rpc.RpcResult |
|
203 |
return dict([(node, rr(True, [("utest", constants.HKR_SUCCESS, "ok")], |
|
204 |
node=node, call='FakeScriptOk')) |
|
205 |
for node in node_list]) |
|
206 |
|
|
207 |
|
|
194 | 208 |
class TestHooksMaster(unittest.TestCase): |
195 | 209 |
"""Testing case for HooksMaster""" |
196 | 210 |
|
... | ... | |
222 | 236 |
node=node, call='FakeScriptFail')) |
223 | 237 |
for node in node_list]) |
224 | 238 |
|
225 |
@staticmethod |
|
226 |
def _call_script_succeed(node_list, hpath, phase, env): |
|
227 |
"""Fake call_hooks_runner function. |
|
228 |
|
|
229 |
@rtype: dict of node -> L{rpc.RpcResult} with a successful script result |
|
230 |
@return: script execution from all nodes |
|
231 |
|
|
232 |
""" |
|
233 |
rr = rpc.RpcResult |
|
234 |
return dict([(node, rr(True, [("utest", constants.HKR_SUCCESS, "ok")], |
|
235 |
node=node, call='FakeScriptOk')) |
|
236 |
for node in node_list]) |
|
237 |
|
|
238 | 239 |
def setUp(self): |
239 | 240 |
self.op = opcodes.OpCode() |
240 | 241 |
self.context = FakeContext() |
... | ... | |
266 | 267 |
|
267 | 268 |
def testScriptSucceed(self): |
268 | 269 |
"""Test individual rpc failure""" |
269 |
hm = mcpu.HooksMaster(self._call_script_succeed, self.lu)
|
|
270 |
hm = mcpu.HooksMaster(FakeHooksRpcSuccess, self.lu)
|
|
270 | 271 |
for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST): |
271 | 272 |
hm.RunPhase(phase) |
272 | 273 |
|
273 | 274 |
|
275 |
class FakeEnvLU(cmdlib.LogicalUnit): |
|
276 |
HPATH = "env_test_lu" |
|
277 |
HTYPE = constants.HTYPE_GROUP |
|
278 |
|
|
279 |
def __init__(self, *args): |
|
280 |
cmdlib.LogicalUnit.__init__(self, *args) |
|
281 |
self.hook_env = None |
|
282 |
|
|
283 |
def BuildHooksEnv(self): |
|
284 |
assert self.hook_env is not None |
|
285 |
|
|
286 |
return self.hook_env, ["localhost"], ["localhost"] |
|
287 |
|
|
288 |
|
|
289 |
class TestHooksRunnerEnv(unittest.TestCase): |
|
290 |
def setUp(self): |
|
291 |
self._rpcs = [] |
|
292 |
|
|
293 |
self.op = opcodes.OpTestDummy(result=False, messages=[], fail=False) |
|
294 |
self.lu = FakeEnvLU(FakeProc(), self.op, FakeContext(), None) |
|
295 |
self.hm = mcpu.HooksMaster(self._HooksRpc, self.lu) |
|
296 |
|
|
297 |
def _HooksRpc(self, *args): |
|
298 |
self._rpcs.append(args) |
|
299 |
return FakeHooksRpcSuccess(*args) |
|
300 |
|
|
301 |
def _CheckEnv(self, env, phase, hpath): |
|
302 |
self.assertTrue(env["PATH"].startswith("/sbin")) |
|
303 |
self.assertEqual(env["GANETI_HOOKS_PHASE"], phase) |
|
304 |
self.assertEqual(env["GANETI_HOOKS_PATH"], hpath) |
|
305 |
self.assertEqual(env["GANETI_OP_CODE"], self.op.OP_ID) |
|
306 |
self.assertEqual(env["GANETI_OBJECT_TYPE"], constants.HTYPE_GROUP) |
|
307 |
self.assertEqual(env["GANETI_HOOKS_VERSION"], str(constants.HOOKS_VERSION)) |
|
308 |
self.assertEqual(env["GANETI_DATA_DIR"], constants.DATA_DIR) |
|
309 |
|
|
310 |
def testEmptyEnv(self): |
|
311 |
# Check pre-phase hook |
|
312 |
self.lu.hook_env = {} |
|
313 |
self.hm.RunPhase(constants.HOOKS_PHASE_PRE) |
|
314 |
|
|
315 |
(node_list, hpath, phase, env) = self._rpcs.pop(0) |
|
316 |
self.assertEqual(node_list, set(["localhost"])) |
|
317 |
self.assertEqual(hpath, self.lu.HPATH) |
|
318 |
self.assertEqual(phase, constants.HOOKS_PHASE_PRE) |
|
319 |
self._CheckEnv(env, constants.HOOKS_PHASE_PRE, self.lu.HPATH) |
|
320 |
|
|
321 |
# Check post-phase hook |
|
322 |
self.lu.hook_env = {} |
|
323 |
self.hm.RunPhase(constants.HOOKS_PHASE_POST) |
|
324 |
|
|
325 |
(node_list, hpath, phase, env) = self._rpcs.pop(0) |
|
326 |
self.assertEqual(node_list, set(["localhost"])) |
|
327 |
self.assertEqual(hpath, self.lu.HPATH) |
|
328 |
self.assertEqual(phase, constants.HOOKS_PHASE_POST) |
|
329 |
self._CheckEnv(env, constants.HOOKS_PHASE_POST, self.lu.HPATH) |
|
330 |
|
|
331 |
self.assertRaises(IndexError, self._rpcs.pop) |
|
332 |
|
|
333 |
def testEnv(self): |
|
334 |
# Check pre-phase hook |
|
335 |
self.lu.hook_env = { |
|
336 |
"FOO": "pre-foo-value", |
|
337 |
} |
|
338 |
self.hm.RunPhase(constants.HOOKS_PHASE_PRE) |
|
339 |
|
|
340 |
(node_list, hpath, phase, env) = self._rpcs.pop(0) |
|
341 |
self.assertEqual(node_list, set(["localhost"])) |
|
342 |
self.assertEqual(hpath, self.lu.HPATH) |
|
343 |
self.assertEqual(phase, constants.HOOKS_PHASE_PRE) |
|
344 |
self.assertEqual(env["GANETI_FOO"], "pre-foo-value") |
|
345 |
self.assertFalse(compat.any(key.startswith("GANETI_POST") for key in env)) |
|
346 |
self._CheckEnv(env, constants.HOOKS_PHASE_PRE, self.lu.HPATH) |
|
347 |
|
|
348 |
# Check post-phase hook |
|
349 |
self.lu.hook_env = { |
|
350 |
"FOO": "post-value", |
|
351 |
"BAR": 123, |
|
352 |
} |
|
353 |
self.hm.RunPhase(constants.HOOKS_PHASE_POST) |
|
354 |
|
|
355 |
(node_list, hpath, phase, env) = self._rpcs.pop(0) |
|
356 |
self.assertEqual(node_list, set(["localhost"])) |
|
357 |
self.assertEqual(hpath, self.lu.HPATH) |
|
358 |
self.assertEqual(phase, constants.HOOKS_PHASE_POST) |
|
359 |
self.assertEqual(env["GANETI_FOO"], "pre-foo-value") |
|
360 |
self.assertEqual(env["GANETI_POST_FOO"], "post-value") |
|
361 |
self.assertEqual(env["GANETI_POST_BAR"], "123") |
|
362 |
self.assertFalse("GANETI_BAR" in env) |
|
363 |
self._CheckEnv(env, constants.HOOKS_PHASE_POST, self.lu.HPATH) |
|
364 |
|
|
365 |
self.assertRaises(IndexError, self._rpcs.pop) |
|
366 |
|
|
367 |
# Check configuration update hook |
|
368 |
self.hm.RunConfigUpdate() |
|
369 |
(node_list, hpath, phase, env) = self._rpcs.pop(0) |
|
370 |
self.assertEqual(set(node_list), set([self.lu.cfg.GetMasterNode()])) |
|
371 |
self.assertEqual(hpath, constants.HOOKS_NAME_CFGUPDATE) |
|
372 |
self.assertEqual(phase, constants.HOOKS_PHASE_POST) |
|
373 |
self._CheckEnv(env, constants.HOOKS_PHASE_POST, |
|
374 |
constants.HOOKS_NAME_CFGUPDATE) |
|
375 |
self.assertFalse(compat.any(key.startswith("GANETI_POST") for key in env)) |
|
376 |
self.assertEqual(env["GANETI_FOO"], "pre-foo-value") |
|
377 |
self.assertRaises(IndexError, self._rpcs.pop) |
|
378 |
|
|
379 |
def testConflict(self): |
|
380 |
for name in ["DATA_DIR", "OP_CODE"]: |
|
381 |
self.lu.hook_env = { name: "value" } |
|
382 |
for phase in [constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST]: |
|
383 |
# Test using a clean HooksMaster instance |
|
384 |
self.assertRaises(AssertionError, |
|
385 |
mcpu.HooksMaster(self._HooksRpc, self.lu).RunPhase, |
|
386 |
phase) |
|
387 |
self.assertRaises(IndexError, self._rpcs.pop) |
|
388 |
|
|
389 |
def testNoNodes(self): |
|
390 |
self.lu.hook_env = {} |
|
391 |
self.hm.RunPhase(constants.HOOKS_PHASE_PRE, nodes=[]) |
|
392 |
self.assertRaises(IndexError, self._rpcs.pop) |
|
393 |
|
|
394 |
def testSpecificNodes(self): |
|
395 |
self.lu.hook_env = {} |
|
396 |
|
|
397 |
nodes = [ |
|
398 |
"node1.example.com", |
|
399 |
"node93782.example.net", |
|
400 |
] |
|
401 |
|
|
402 |
for phase in [constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST]: |
|
403 |
self.hm.RunPhase(phase, nodes=nodes) |
|
404 |
|
|
405 |
(node_list, hpath, rpc_phase, env) = self._rpcs.pop(0) |
|
406 |
self.assertEqual(set(node_list), set(nodes)) |
|
407 |
self.assertEqual(hpath, self.lu.HPATH) |
|
408 |
self.assertEqual(rpc_phase, phase) |
|
409 |
self._CheckEnv(env, phase, self.lu.HPATH) |
|
410 |
|
|
411 |
self.assertRaises(IndexError, self._rpcs.pop) |
|
412 |
|
|
413 |
def testRunConfigUpdateNoPre(self): |
|
414 |
self.lu.hook_env = {} |
|
415 |
self.assertRaises(AssertionError, self.hm.RunConfigUpdate) |
|
416 |
self.assertRaises(IndexError, self._rpcs.pop) |
|
417 |
|
|
418 |
|
|
274 | 419 |
if __name__ == '__main__': |
275 | 420 |
testutils.GanetiTestProgram() |
Also available in: Unified diff