Adding RAPI resource for multi-allocation
[ganeti-local] / test / ganeti.hooks_unittest.py
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2006, 2007 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Script for unittesting the hooks module"""
23
24
25 import unittest
26 import os
27 import time
28 import tempfile
29 import os.path
30
31 from ganeti import errors
32 from ganeti import opcodes
33 from ganeti import mcpu
34 from ganeti import backend
35 from ganeti import constants
36 from ganeti import cmdlib
37 from ganeti import rpc
38 from ganeti import compat
39 from ganeti import pathutils
40 from ganeti.constants import HKR_SUCCESS, HKR_FAIL, HKR_SKIP
41
42 from mocks import FakeConfig, FakeProc, FakeContext
43
44 import testutils
45
46
47 class FakeLU(cmdlib.LogicalUnit):
48   HPATH = "test"
49
50   def BuildHooksEnv(self):
51     return {}
52
53   def BuildHooksNodes(self):
54     return ["localhost"], ["localhost"]
55
56
57 class TestHooksRunner(unittest.TestCase):
58   """Testing case for HooksRunner"""
59   def setUp(self):
60     self.torm = []
61     self.tmpdir = tempfile.mkdtemp()
62     self.torm.append((self.tmpdir, True))
63     self.logdir = tempfile.mkdtemp()
64     self.torm.append((self.logdir, True))
65     self.hpath = "fake"
66     self.ph_dirs = {}
67     for i in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
68       dname = "%s/%s-%s.d" % (self.tmpdir, self.hpath, i)
69       os.mkdir(dname)
70       self.torm.append((dname, True))
71       self.ph_dirs[i] = dname
72     self.hr = backend.HooksRunner(hooks_base_dir=self.tmpdir)
73
74   def tearDown(self):
75     self.torm.reverse()
76     for path, kind in self.torm:
77       if kind:
78         os.rmdir(path)
79       else:
80         os.unlink(path)
81
82   def _rname(self, fname):
83     return "/".join(fname.split("/")[-2:])
84
85   def testEmpty(self):
86     """Test no hooks"""
87     for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
88       self.failUnlessEqual(self.hr.RunHooks(self.hpath, phase, {}), [])
89
90   def testSkipNonExec(self):
91     """Test skip non-exec file"""
92     for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
93       fname = "%s/test" % self.ph_dirs[phase]
94       f = open(fname, "w")
95       f.close()
96       self.torm.append((fname, False))
97       self.failUnlessEqual(self.hr.RunHooks(self.hpath, phase, {}),
98                            [(self._rname(fname), HKR_SKIP, "")])
99
100   def testSkipInvalidName(self):
101     """Test skip script with invalid name"""
102     for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
103       fname = "%s/a.off" % self.ph_dirs[phase]
104       f = open(fname, "w")
105       f.write("#!/bin/sh\nexit 0\n")
106       f.close()
107       os.chmod(fname, 0700)
108       self.torm.append((fname, False))
109       self.failUnlessEqual(self.hr.RunHooks(self.hpath, phase, {}),
110                            [(self._rname(fname), HKR_SKIP, "")])
111
112   def testSkipDir(self):
113     """Test skip directory"""
114     for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
115       fname = "%s/testdir" % self.ph_dirs[phase]
116       os.mkdir(fname)
117       self.torm.append((fname, True))
118       self.failUnlessEqual(self.hr.RunHooks(self.hpath, phase, {}),
119                            [(self._rname(fname), HKR_SKIP, "")])
120
121   def testSuccess(self):
122     """Test success execution"""
123     for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
124       fname = "%s/success" % self.ph_dirs[phase]
125       f = open(fname, "w")
126       f.write("#!/bin/sh\nexit 0\n")
127       f.close()
128       self.torm.append((fname, False))
129       os.chmod(fname, 0700)
130       self.failUnlessEqual(self.hr.RunHooks(self.hpath, phase, {}),
131                            [(self._rname(fname), HKR_SUCCESS, "")])
132
133   def testSymlink(self):
134     """Test running a symlink"""
135     for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
136       fname = "%s/success" % self.ph_dirs[phase]
137       os.symlink("/bin/true", fname)
138       self.torm.append((fname, False))
139       self.failUnlessEqual(self.hr.RunHooks(self.hpath, phase, {}),
140                            [(self._rname(fname), HKR_SUCCESS, "")])
141
142   def testFail(self):
143     """Test success execution"""
144     for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
145       fname = "%s/success" % self.ph_dirs[phase]
146       f = open(fname, "w")
147       f.write("#!/bin/sh\nexit 1\n")
148       f.close()
149       self.torm.append((fname, False))
150       os.chmod(fname, 0700)
151       self.failUnlessEqual(self.hr.RunHooks(self.hpath, phase, {}),
152                            [(self._rname(fname), HKR_FAIL, "")])
153
154   def testCombined(self):
155     """Test success, failure and skip all in one test"""
156     for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
157       expect = []
158       for fbase, ecode, rs in [("00succ", 0, HKR_SUCCESS),
159                                ("10fail", 1, HKR_FAIL),
160                                ("20inv.", 0, HKR_SKIP),
161                                ]:
162         fname = "%s/%s" % (self.ph_dirs[phase], fbase)
163         f = open(fname, "w")
164         f.write("#!/bin/sh\nexit %d\n" % ecode)
165         f.close()
166         self.torm.append((fname, False))
167         os.chmod(fname, 0700)
168         expect.append((self._rname(fname), rs, ""))
169       self.failUnlessEqual(self.hr.RunHooks(self.hpath, phase, {}), expect)
170
171   def testOrdering(self):
172     for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
173       expect = []
174       for fbase in ["10s1",
175                     "00s0",
176                     "10sa",
177                     "80sc",
178                     "60sd",
179                     ]:
180         fname = "%s/%s" % (self.ph_dirs[phase], fbase)
181         os.symlink("/bin/true", fname)
182         self.torm.append((fname, False))
183         expect.append((self._rname(fname), HKR_SUCCESS, ""))
184       expect.sort()
185       self.failUnlessEqual(self.hr.RunHooks(self.hpath, phase, {}), expect)
186
187   def testEnv(self):
188     """Test environment execution"""
189     for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
190       fbase = "success"
191       fname = "%s/%s" % (self.ph_dirs[phase], fbase)
192       os.symlink("/usr/bin/env", fname)
193       self.torm.append((fname, False))
194       env_snt = {"PHASE": phase}
195       env_exp = "PHASE=%s" % phase
196       self.failUnlessEqual(self.hr.RunHooks(self.hpath, phase, env_snt),
197                            [(self._rname(fname), HKR_SUCCESS, env_exp)])
198
199
200 def FakeHooksRpcSuccess(node_list, hpath, phase, env):
201   """Fake call_hooks_runner function.
202
203   @rtype: dict of node -> L{rpc.RpcResult} with a successful script result
204   @return: script execution from all nodes
205
206   """
207   rr = rpc.RpcResult
208   return dict([(node, rr((True, [("utest", constants.HKR_SUCCESS, "ok")]),
209                          node=node, call='FakeScriptOk'))
210                for node in node_list])
211
212
213 class TestHooksMaster(unittest.TestCase):
214   """Testing case for HooksMaster"""
215
216   def _call_false(*args):
217     """Fake call_hooks_runner function which returns False."""
218     return False
219
220   @staticmethod
221   def _call_nodes_false(node_list, hpath, phase, env):
222     """Fake call_hooks_runner function.
223
224     @rtype: dict of node -> L{rpc.RpcResult} with an rpc error
225     @return: rpc failure from all nodes
226
227     """
228     return dict([(node, rpc.RpcResult('error', failed=True,
229                   node=node, call='FakeError')) for node in node_list])
230
231   @staticmethod
232   def _call_script_fail(node_list, hpath, phase, env):
233     """Fake call_hooks_runner function.
234
235     @rtype: dict of node -> L{rpc.RpcResult} with a failed script result
236     @return: script execution failure from all nodes
237
238     """
239     rr = rpc.RpcResult
240     return dict([(node, rr((True, [("utest", constants.HKR_FAIL, "err")]),
241                            node=node, call='FakeScriptFail'))
242                   for node in node_list])
243
244   def setUp(self):
245     self.op = opcodes.OpCode()
246     self.context = FakeContext()
247     # WARNING: here we pass None as RpcRunner instance since we know
248     # our usage via HooksMaster will not use lu.rpc
249     self.lu = FakeLU(FakeProc(), self.op, self.context, None)
250
251   def testTotalFalse(self):
252     """Test complete rpc failure"""
253     hm = mcpu.HooksMaster.BuildFromLu(self._call_false, self.lu)
254     self.failUnlessRaises(errors.HooksFailure,
255                           hm.RunPhase, constants.HOOKS_PHASE_PRE)
256     hm.RunPhase(constants.HOOKS_PHASE_POST)
257
258   def testIndividualFalse(self):
259     """Test individual node failure"""
260     hm = mcpu.HooksMaster.BuildFromLu(self._call_nodes_false, self.lu)
261     hm.RunPhase(constants.HOOKS_PHASE_PRE)
262     #self.failUnlessRaises(errors.HooksFailure,
263     #                      hm.RunPhase, constants.HOOKS_PHASE_PRE)
264     hm.RunPhase(constants.HOOKS_PHASE_POST)
265
266   def testScriptFalse(self):
267     """Test individual rpc failure"""
268     hm = mcpu.HooksMaster.BuildFromLu(self._call_script_fail, self.lu)
269     self.failUnlessRaises(errors.HooksAbort,
270                           hm.RunPhase, constants.HOOKS_PHASE_PRE)
271     hm.RunPhase(constants.HOOKS_PHASE_POST)
272
273   def testScriptSucceed(self):
274     """Test individual rpc failure"""
275     hm = mcpu.HooksMaster.BuildFromLu(FakeHooksRpcSuccess, self.lu)
276     for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
277       hm.RunPhase(phase)
278
279
280 class FakeEnvLU(cmdlib.LogicalUnit):
281   HPATH = "env_test_lu"
282   HTYPE = constants.HTYPE_GROUP
283
284   def __init__(self, *args):
285     cmdlib.LogicalUnit.__init__(self, *args)
286     self.hook_env = None
287
288   def BuildHooksEnv(self):
289     assert self.hook_env is not None
290     return self.hook_env
291
292   def BuildHooksNodes(self):
293     return (["localhost"], ["localhost"])
294
295
296 class FakeNoHooksLU(cmdlib.NoHooksLU):
297   pass
298
299
300 class TestHooksRunnerEnv(unittest.TestCase):
301   def setUp(self):
302     self._rpcs = []
303
304     self.op = opcodes.OpTestDummy(result=False, messages=[], fail=False)
305     self.lu = FakeEnvLU(FakeProc(), self.op, FakeContext(), None)
306
307   def _HooksRpc(self, *args):
308     self._rpcs.append(args)
309     return FakeHooksRpcSuccess(*args)
310
311   def _CheckEnv(self, env, phase, hpath):
312     self.assertTrue(env["PATH"].startswith("/sbin"))
313     self.assertEqual(env["GANETI_HOOKS_PHASE"], phase)
314     self.assertEqual(env["GANETI_HOOKS_PATH"], hpath)
315     self.assertEqual(env["GANETI_OP_CODE"], self.op.OP_ID)
316     self.assertEqual(env["GANETI_HOOKS_VERSION"], str(constants.HOOKS_VERSION))
317     self.assertEqual(env["GANETI_DATA_DIR"], pathutils.DATA_DIR)
318     if "GANETI_OBJECT_TYPE" in env:
319       self.assertEqual(env["GANETI_OBJECT_TYPE"], constants.HTYPE_GROUP)
320     else:
321       self.assertTrue(self.lu.HTYPE is None)
322
323   def testEmptyEnv(self):
324     # Check pre-phase hook
325     self.lu.hook_env = {}
326     hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
327     hm.RunPhase(constants.HOOKS_PHASE_PRE)
328
329     (node_list, hpath, phase, env) = self._rpcs.pop(0)
330     self.assertEqual(node_list, set(["localhost"]))
331     self.assertEqual(hpath, self.lu.HPATH)
332     self.assertEqual(phase, constants.HOOKS_PHASE_PRE)
333     self._CheckEnv(env, constants.HOOKS_PHASE_PRE, self.lu.HPATH)
334
335     # Check post-phase hook
336     self.lu.hook_env = {}
337     hm.RunPhase(constants.HOOKS_PHASE_POST)
338
339     (node_list, hpath, phase, env) = self._rpcs.pop(0)
340     self.assertEqual(node_list, set(["localhost"]))
341     self.assertEqual(hpath, self.lu.HPATH)
342     self.assertEqual(phase, constants.HOOKS_PHASE_POST)
343     self._CheckEnv(env, constants.HOOKS_PHASE_POST, self.lu.HPATH)
344
345     self.assertRaises(IndexError, self._rpcs.pop)
346
347   def testEnv(self):
348     # Check pre-phase hook
349     self.lu.hook_env = {
350       "FOO": "pre-foo-value",
351       }
352     hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
353     hm.RunPhase(constants.HOOKS_PHASE_PRE)
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_PRE)
359     self.assertEqual(env["GANETI_FOO"], "pre-foo-value")
360     self.assertFalse(compat.any(key.startswith("GANETI_POST") for key in env))
361     self._CheckEnv(env, constants.HOOKS_PHASE_PRE, self.lu.HPATH)
362
363     # Check post-phase hook
364     self.lu.hook_env = {
365       "FOO": "post-value",
366       "BAR": 123,
367       }
368     hm.RunPhase(constants.HOOKS_PHASE_POST)
369
370     (node_list, hpath, phase, env) = self._rpcs.pop(0)
371     self.assertEqual(node_list, set(["localhost"]))
372     self.assertEqual(hpath, self.lu.HPATH)
373     self.assertEqual(phase, constants.HOOKS_PHASE_POST)
374     self.assertEqual(env["GANETI_FOO"], "pre-foo-value")
375     self.assertEqual(env["GANETI_POST_FOO"], "post-value")
376     self.assertEqual(env["GANETI_POST_BAR"], "123")
377     self.assertFalse("GANETI_BAR" in env)
378     self._CheckEnv(env, constants.HOOKS_PHASE_POST, self.lu.HPATH)
379
380     self.assertRaises(IndexError, self._rpcs.pop)
381
382     # Check configuration update hook
383     hm.RunConfigUpdate()
384     (node_list, hpath, phase, env) = self._rpcs.pop(0)
385     self.assertEqual(set(node_list), set([self.lu.cfg.GetMasterNode()]))
386     self.assertEqual(hpath, constants.HOOKS_NAME_CFGUPDATE)
387     self.assertEqual(phase, constants.HOOKS_PHASE_POST)
388     self._CheckEnv(env, constants.HOOKS_PHASE_POST,
389                    constants.HOOKS_NAME_CFGUPDATE)
390     self.assertFalse(compat.any(key.startswith("GANETI_POST") for key in env))
391     self.assertEqual(env["GANETI_FOO"], "pre-foo-value")
392     self.assertRaises(IndexError, self._rpcs.pop)
393
394   def testConflict(self):
395     for name in ["DATA_DIR", "OP_CODE"]:
396       self.lu.hook_env = { name: "value" }
397
398       # Test using a clean HooksMaster instance
399       hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
400
401       for phase in [constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST]:
402         self.assertRaises(AssertionError, hm.RunPhase, phase)
403         self.assertRaises(IndexError, self._rpcs.pop)
404
405   def testNoNodes(self):
406     self.lu.hook_env = {}
407     hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
408     hm.RunPhase(constants.HOOKS_PHASE_PRE, nodes=[])
409     self.assertRaises(IndexError, self._rpcs.pop)
410
411   def testSpecificNodes(self):
412     self.lu.hook_env = {}
413
414     nodes = [
415       "node1.example.com",
416       "node93782.example.net",
417       ]
418
419     hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
420
421     for phase in [constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST]:
422       hm.RunPhase(phase, nodes=nodes)
423
424       (node_list, hpath, rpc_phase, env) = self._rpcs.pop(0)
425       self.assertEqual(set(node_list), set(nodes))
426       self.assertEqual(hpath, self.lu.HPATH)
427       self.assertEqual(rpc_phase, phase)
428       self._CheckEnv(env, phase, self.lu.HPATH)
429
430       self.assertRaises(IndexError, self._rpcs.pop)
431
432   def testRunConfigUpdateNoPre(self):
433     self.lu.hook_env = {
434       "FOO": "value",
435       }
436
437     hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
438     hm.RunConfigUpdate()
439
440     (node_list, hpath, phase, env) = self._rpcs.pop(0)
441     self.assertEqual(set(node_list), set([self.lu.cfg.GetMasterNode()]))
442     self.assertEqual(hpath, constants.HOOKS_NAME_CFGUPDATE)
443     self.assertEqual(phase, constants.HOOKS_PHASE_POST)
444     self.assertEqual(env["GANETI_FOO"], "value")
445     self.assertFalse(compat.any(key.startswith("GANETI_POST") for key in env))
446     self._CheckEnv(env, constants.HOOKS_PHASE_POST,
447                    constants.HOOKS_NAME_CFGUPDATE)
448
449     self.assertRaises(IndexError, self._rpcs.pop)
450
451   def testNoPreBeforePost(self):
452     self.lu.hook_env = {
453       "FOO": "value",
454       }
455
456     hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
457     hm.RunPhase(constants.HOOKS_PHASE_POST)
458
459     (node_list, hpath, phase, env) = self._rpcs.pop(0)
460     self.assertEqual(node_list, set(["localhost"]))
461     self.assertEqual(hpath, self.lu.HPATH)
462     self.assertEqual(phase, constants.HOOKS_PHASE_POST)
463     self.assertEqual(env["GANETI_FOO"], "value")
464     self.assertEqual(env["GANETI_POST_FOO"], "value")
465     self._CheckEnv(env, constants.HOOKS_PHASE_POST, self.lu.HPATH)
466
467     self.assertRaises(IndexError, self._rpcs.pop)
468
469   def testNoHooksLU(self):
470     self.lu = FakeNoHooksLU(FakeProc(), self.op, FakeContext(), None)
471     self.assertRaises(AssertionError, self.lu.BuildHooksEnv)
472     self.assertRaises(AssertionError, self.lu.BuildHooksNodes)
473
474     hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
475     self.assertEqual(hm.pre_env, {})
476     self.assertRaises(IndexError, self._rpcs.pop)
477
478     hm.RunPhase(constants.HOOKS_PHASE_PRE)
479     self.assertRaises(IndexError, self._rpcs.pop)
480
481     hm.RunPhase(constants.HOOKS_PHASE_POST)
482     self.assertRaises(IndexError, self._rpcs.pop)
483
484     hm.RunConfigUpdate()
485
486     (node_list, hpath, phase, env) = self._rpcs.pop(0)
487     self.assertEqual(set(node_list), set([self.lu.cfg.GetMasterNode()]))
488     self.assertEqual(hpath, constants.HOOKS_NAME_CFGUPDATE)
489     self.assertEqual(phase, constants.HOOKS_PHASE_POST)
490     self.assertFalse(compat.any(key.startswith("GANETI_POST") for key in env))
491     self._CheckEnv(env, constants.HOOKS_PHASE_POST,
492                    constants.HOOKS_NAME_CFGUPDATE)
493     self.assertRaises(IndexError, self._rpcs.pop)
494
495     assert isinstance(self.lu, FakeNoHooksLU), "LU was replaced"
496
497
498 if __name__ == '__main__':
499   testutils.GanetiTestProgram()