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