74ae4e74846320a31c648a30dca6c6971a241bdd
[ganeti-local] / test / py / ganeti.backend_unittest.py
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2010 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 testing ganeti.backend"""
23
24 import os
25 import sys
26 import shutil
27 import tempfile
28 import unittest
29
30 from ganeti import utils
31 from ganeti import constants
32 from ganeti import backend
33 from ganeti import netutils
34 from ganeti import errors
35
36 import testutils
37 import mocks
38
39
40 class TestX509Certificates(unittest.TestCase):
41   def setUp(self):
42     self.tmpdir = tempfile.mkdtemp()
43
44   def tearDown(self):
45     shutil.rmtree(self.tmpdir)
46
47   def test(self):
48     (name, cert_pem) = backend.CreateX509Certificate(300, cryptodir=self.tmpdir)
49
50     self.assertEqual(utils.ReadFile(os.path.join(self.tmpdir, name,
51                                                  backend._X509_CERT_FILE)),
52                      cert_pem)
53     self.assert_(0 < os.path.getsize(os.path.join(self.tmpdir, name,
54                                                   backend._X509_KEY_FILE)))
55
56     (name2, cert_pem2) = \
57       backend.CreateX509Certificate(300, cryptodir=self.tmpdir)
58
59     backend.RemoveX509Certificate(name, cryptodir=self.tmpdir)
60     backend.RemoveX509Certificate(name2, cryptodir=self.tmpdir)
61
62     self.assertEqual(utils.ListVisibleFiles(self.tmpdir), [])
63
64   def testNonEmpty(self):
65     (name, _) = backend.CreateX509Certificate(300, cryptodir=self.tmpdir)
66
67     utils.WriteFile(utils.PathJoin(self.tmpdir, name, "hello-world"),
68                     data="Hello World")
69
70     self.assertRaises(backend.RPCFail, backend.RemoveX509Certificate,
71                       name, cryptodir=self.tmpdir)
72
73     self.assertEqual(utils.ListVisibleFiles(self.tmpdir), [name])
74
75
76 class TestNodeVerify(testutils.GanetiTestCase):
77   def testMasterIPLocalhost(self):
78     # this a real functional test, but requires localhost to be reachable
79     local_data = (netutils.Hostname.GetSysName(),
80                   constants.IP4_ADDRESS_LOCALHOST)
81     result = backend.VerifyNode({constants.NV_MASTERIP: local_data}, None)
82     self.failUnless(constants.NV_MASTERIP in result,
83                     "Master IP data not returned")
84     self.failUnless(result[constants.NV_MASTERIP], "Cannot reach localhost")
85
86   def testMasterIPUnreachable(self):
87     # Network 192.0.2.0/24 is reserved for test/documentation as per
88     # RFC 5737
89     bad_data =  ("master.example.com", "192.0.2.1")
90     # we just test that whatever TcpPing returns, VerifyNode returns too
91     netutils.TcpPing = lambda a, b, source=None: False
92     result = backend.VerifyNode({constants.NV_MASTERIP: bad_data}, None)
93     self.failUnless(constants.NV_MASTERIP in result,
94                     "Master IP data not returned")
95     self.failIf(result[constants.NV_MASTERIP],
96                 "Result from netutils.TcpPing corrupted")
97
98
99 def _DefRestrictedCmdOwner():
100   return (os.getuid(), os.getgid())
101
102
103 class TestVerifyRestrictedCmdName(unittest.TestCase):
104   def testAcceptableName(self):
105     for i in ["foo", "bar", "z1", "000first", "hello-world"]:
106       for fn in [lambda s: s, lambda s: s.upper(), lambda s: s.title()]:
107         (status, msg) = backend._VerifyRestrictedCmdName(fn(i))
108         self.assertTrue(status)
109         self.assertTrue(msg is None)
110
111   def testEmptyAndSpace(self):
112     for i in ["", " ", "\t", "\n"]:
113       (status, msg) = backend._VerifyRestrictedCmdName(i)
114       self.assertFalse(status)
115       self.assertEqual(msg, "Missing command name")
116
117   def testNameWithSlashes(self):
118     for i in ["/", "./foo", "../moo", "some/name"]:
119       (status, msg) = backend._VerifyRestrictedCmdName(i)
120       self.assertFalse(status)
121       self.assertEqual(msg, "Invalid command name")
122
123   def testForbiddenCharacters(self):
124     for i in ["#", ".", "..", "bash -c ls", "'"]:
125       (status, msg) = backend._VerifyRestrictedCmdName(i)
126       self.assertFalse(status)
127       self.assertEqual(msg, "Command name contains forbidden characters")
128
129
130 class TestVerifyRestrictedCmdDirectory(unittest.TestCase):
131   def setUp(self):
132     self.tmpdir = tempfile.mkdtemp()
133
134   def tearDown(self):
135     shutil.rmtree(self.tmpdir)
136
137   def testCanNotStat(self):
138     tmpname = utils.PathJoin(self.tmpdir, "foobar")
139     self.assertFalse(os.path.exists(tmpname))
140     (status, msg) = \
141       backend._VerifyRestrictedCmdDirectory(tmpname, _owner=NotImplemented)
142     self.assertFalse(status)
143     self.assertTrue(msg.startswith("Can't stat(2) '"))
144
145   def testTooPermissive(self):
146     tmpname = utils.PathJoin(self.tmpdir, "foobar")
147     os.mkdir(tmpname)
148
149     for mode in [0777, 0706, 0760, 0722]:
150       os.chmod(tmpname, mode)
151       self.assertTrue(os.path.isdir(tmpname))
152       (status, msg) = \
153         backend._VerifyRestrictedCmdDirectory(tmpname, _owner=NotImplemented)
154       self.assertFalse(status)
155       self.assertTrue(msg.startswith("Permissions on '"))
156
157   def testNoDirectory(self):
158     tmpname = utils.PathJoin(self.tmpdir, "foobar")
159     utils.WriteFile(tmpname, data="empty\n")
160     self.assertTrue(os.path.isfile(tmpname))
161     (status, msg) = \
162       backend._VerifyRestrictedCmdDirectory(tmpname,
163                                             _owner=_DefRestrictedCmdOwner())
164     self.assertFalse(status)
165     self.assertTrue(msg.endswith("is not a directory"))
166
167   def testNormal(self):
168     tmpname = utils.PathJoin(self.tmpdir, "foobar")
169     os.mkdir(tmpname)
170     self.assertTrue(os.path.isdir(tmpname))
171     (status, msg) = \
172       backend._VerifyRestrictedCmdDirectory(tmpname,
173                                             _owner=_DefRestrictedCmdOwner())
174     self.assertTrue(status)
175     self.assertTrue(msg is None)
176
177
178 class TestVerifyRestrictedCmd(unittest.TestCase):
179   def setUp(self):
180     self.tmpdir = tempfile.mkdtemp()
181
182   def tearDown(self):
183     shutil.rmtree(self.tmpdir)
184
185   def testCanNotStat(self):
186     tmpname = utils.PathJoin(self.tmpdir, "helloworld")
187     self.assertFalse(os.path.exists(tmpname))
188     (status, msg) = \
189       backend._VerifyRestrictedCmd(self.tmpdir, "helloworld",
190                                    _owner=NotImplemented)
191     self.assertFalse(status)
192     self.assertTrue(msg.startswith("Can't stat(2) '"))
193
194   def testNotExecutable(self):
195     tmpname = utils.PathJoin(self.tmpdir, "cmdname")
196     utils.WriteFile(tmpname, data="empty\n")
197     (status, msg) = \
198       backend._VerifyRestrictedCmd(self.tmpdir, "cmdname",
199                                    _owner=_DefRestrictedCmdOwner())
200     self.assertFalse(status)
201     self.assertTrue(msg.startswith("access(2) thinks '"))
202
203   def testExecutable(self):
204     tmpname = utils.PathJoin(self.tmpdir, "cmdname")
205     utils.WriteFile(tmpname, data="empty\n", mode=0700)
206     (status, executable) = \
207       backend._VerifyRestrictedCmd(self.tmpdir, "cmdname",
208                                    _owner=_DefRestrictedCmdOwner())
209     self.assertTrue(status)
210     self.assertEqual(executable, tmpname)
211
212
213 class TestPrepareRestrictedCmd(unittest.TestCase):
214   _TEST_PATH = "/tmp/some/test/path"
215
216   def testDirFails(self):
217     def fn(path):
218       self.assertEqual(path, self._TEST_PATH)
219       return (False, "test error 31420")
220
221     (status, msg) = \
222       backend._PrepareRestrictedCmd(self._TEST_PATH, "cmd21152",
223                                     _verify_dir=fn,
224                                     _verify_name=NotImplemented,
225                                     _verify_cmd=NotImplemented)
226     self.assertFalse(status)
227     self.assertEqual(msg, "test error 31420")
228
229   def testNameFails(self):
230     def fn(cmd):
231       self.assertEqual(cmd, "cmd4617")
232       return (False, "test error 591")
233
234     (status, msg) = \
235       backend._PrepareRestrictedCmd(self._TEST_PATH, "cmd4617",
236                                     _verify_dir=lambda _: (True, None),
237                                     _verify_name=fn,
238                                     _verify_cmd=NotImplemented)
239     self.assertFalse(status)
240     self.assertEqual(msg, "test error 591")
241
242   def testCommandFails(self):
243     def fn(path, cmd):
244       self.assertEqual(path, self._TEST_PATH)
245       self.assertEqual(cmd, "cmd17577")
246       return (False, "test error 25524")
247
248     (status, msg) = \
249       backend._PrepareRestrictedCmd(self._TEST_PATH, "cmd17577",
250                                     _verify_dir=lambda _: (True, None),
251                                     _verify_name=lambda _: (True, None),
252                                     _verify_cmd=fn)
253     self.assertFalse(status)
254     self.assertEqual(msg, "test error 25524")
255
256   def testSuccess(self):
257     def fn(path, cmd):
258       return (True, utils.PathJoin(path, cmd))
259
260     (status, executable) = \
261       backend._PrepareRestrictedCmd(self._TEST_PATH, "cmd22633",
262                                     _verify_dir=lambda _: (True, None),
263                                     _verify_name=lambda _: (True, None),
264                                     _verify_cmd=fn)
265     self.assertTrue(status)
266     self.assertEqual(executable, utils.PathJoin(self._TEST_PATH, "cmd22633"))
267
268
269 def _SleepForRestrictedCmd(duration):
270   assert duration > 5
271
272
273 def _GenericRestrictedCmdError(cmd):
274   return "Executing command '%s' failed" % cmd
275
276
277 class TestRunRestrictedCmd(unittest.TestCase):
278   def setUp(self):
279     self.tmpdir = tempfile.mkdtemp()
280
281   def tearDown(self):
282     shutil.rmtree(self.tmpdir)
283
284   def testNonExistantLockDirectory(self):
285     lockfile = utils.PathJoin(self.tmpdir, "does", "not", "exist")
286     sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
287     self.assertFalse(os.path.exists(lockfile))
288     self.assertRaises(backend.RPCFail,
289                       backend.RunRestrictedCmd, "test",
290                       _lock_timeout=NotImplemented,
291                       _lock_file=lockfile,
292                       _path=NotImplemented,
293                       _sleep_fn=sleep_fn,
294                       _prepare_fn=NotImplemented,
295                       _runcmd_fn=NotImplemented,
296                       _enabled=True)
297     self.assertEqual(sleep_fn.Count(), 1)
298
299   @staticmethod
300   def _TryLock(lockfile):
301     sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
302
303     result = False
304     try:
305       backend.RunRestrictedCmd("test22717",
306                                _lock_timeout=0.1,
307                                _lock_file=lockfile,
308                                _path=NotImplemented,
309                                _sleep_fn=sleep_fn,
310                                _prepare_fn=NotImplemented,
311                                _runcmd_fn=NotImplemented,
312                                _enabled=True)
313     except backend.RPCFail, err:
314       assert str(err) == _GenericRestrictedCmdError("test22717"), \
315              "Did not fail with generic error message"
316       result = True
317
318     assert sleep_fn.Count() == 1
319
320     return result
321
322   def testLockHeldByOtherProcess(self):
323     lockfile = utils.PathJoin(self.tmpdir, "lock")
324
325     lock = utils.FileLock.Open(lockfile)
326     lock.Exclusive(blocking=True, timeout=1.0)
327     try:
328       self.assertTrue(utils.RunInSeparateProcess(self._TryLock, lockfile))
329     finally:
330       lock.Close()
331
332   @staticmethod
333   def _PrepareRaisingException(path, cmd):
334     assert cmd == "test23122"
335     raise Exception("test")
336
337   def testPrepareRaisesException(self):
338     lockfile = utils.PathJoin(self.tmpdir, "lock")
339
340     sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
341     prepare_fn = testutils.CallCounter(self._PrepareRaisingException)
342
343     try:
344       backend.RunRestrictedCmd("test23122",
345                                _lock_timeout=1.0, _lock_file=lockfile,
346                                _path=NotImplemented, _runcmd_fn=NotImplemented,
347                                _sleep_fn=sleep_fn, _prepare_fn=prepare_fn,
348                                _enabled=True)
349     except backend.RPCFail, err:
350       self.assertEqual(str(err), _GenericRestrictedCmdError("test23122"))
351     else:
352       self.fail("Didn't fail")
353
354     self.assertEqual(sleep_fn.Count(), 1)
355     self.assertEqual(prepare_fn.Count(), 1)
356
357   @staticmethod
358   def _PrepareFails(path, cmd):
359     assert cmd == "test29327"
360     return ("some error message", None)
361
362   def testPrepareFails(self):
363     lockfile = utils.PathJoin(self.tmpdir, "lock")
364
365     sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
366     prepare_fn = testutils.CallCounter(self._PrepareFails)
367
368     try:
369       backend.RunRestrictedCmd("test29327",
370                                _lock_timeout=1.0, _lock_file=lockfile,
371                                _path=NotImplemented, _runcmd_fn=NotImplemented,
372                                _sleep_fn=sleep_fn, _prepare_fn=prepare_fn,
373                                _enabled=True)
374     except backend.RPCFail, err:
375       self.assertEqual(str(err), _GenericRestrictedCmdError("test29327"))
376     else:
377       self.fail("Didn't fail")
378
379     self.assertEqual(sleep_fn.Count(), 1)
380     self.assertEqual(prepare_fn.Count(), 1)
381
382   @staticmethod
383   def _SuccessfulPrepare(path, cmd):
384     return (True, utils.PathJoin(path, cmd))
385
386   def testRunCmdFails(self):
387     lockfile = utils.PathJoin(self.tmpdir, "lock")
388
389     def fn(args, env=NotImplemented, reset_env=NotImplemented,
390            postfork_fn=NotImplemented):
391       self.assertEqual(args, [utils.PathJoin(self.tmpdir, "test3079")])
392       self.assertEqual(env, {})
393       self.assertTrue(reset_env)
394       self.assertTrue(callable(postfork_fn))
395
396       trylock = utils.FileLock.Open(lockfile)
397       try:
398         # See if lockfile is still held
399         self.assertRaises(EnvironmentError, trylock.Exclusive, blocking=False)
400
401         # Call back to release lock
402         postfork_fn(NotImplemented)
403
404         # See if lockfile can be acquired
405         trylock.Exclusive(blocking=False)
406       finally:
407         trylock.Close()
408
409       # Simulate a failed command
410       return utils.RunResult(constants.EXIT_FAILURE, None,
411                              "stdout", "stderr406328567",
412                              utils.ShellQuoteArgs(args),
413                              NotImplemented, NotImplemented)
414
415     sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
416     prepare_fn = testutils.CallCounter(self._SuccessfulPrepare)
417     runcmd_fn = testutils.CallCounter(fn)
418
419     try:
420       backend.RunRestrictedCmd("test3079",
421                                _lock_timeout=1.0, _lock_file=lockfile,
422                                _path=self.tmpdir, _runcmd_fn=runcmd_fn,
423                                _sleep_fn=sleep_fn, _prepare_fn=prepare_fn,
424                                _enabled=True)
425     except backend.RPCFail, err:
426       self.assertTrue(str(err).startswith("Remote command 'test3079' failed:"))
427       self.assertTrue("stderr406328567" in str(err),
428                       msg="Error did not include output")
429     else:
430       self.fail("Didn't fail")
431
432     self.assertEqual(sleep_fn.Count(), 0)
433     self.assertEqual(prepare_fn.Count(), 1)
434     self.assertEqual(runcmd_fn.Count(), 1)
435
436   def testRunCmdSucceeds(self):
437     lockfile = utils.PathJoin(self.tmpdir, "lock")
438
439     def fn(args, env=NotImplemented, reset_env=NotImplemented,
440            postfork_fn=NotImplemented):
441       self.assertEqual(args, [utils.PathJoin(self.tmpdir, "test5667")])
442       self.assertEqual(env, {})
443       self.assertTrue(reset_env)
444
445       # Call back to release lock
446       postfork_fn(NotImplemented)
447
448       # Simulate a successful command
449       return utils.RunResult(constants.EXIT_SUCCESS, None, "stdout14463", "",
450                              utils.ShellQuoteArgs(args),
451                              NotImplemented, NotImplemented)
452
453     sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
454     prepare_fn = testutils.CallCounter(self._SuccessfulPrepare)
455     runcmd_fn = testutils.CallCounter(fn)
456
457     result = backend.RunRestrictedCmd("test5667",
458                                       _lock_timeout=1.0, _lock_file=lockfile,
459                                       _path=self.tmpdir, _runcmd_fn=runcmd_fn,
460                                       _sleep_fn=sleep_fn,
461                                       _prepare_fn=prepare_fn,
462                                       _enabled=True)
463     self.assertEqual(result, "stdout14463")
464
465     self.assertEqual(sleep_fn.Count(), 0)
466     self.assertEqual(prepare_fn.Count(), 1)
467     self.assertEqual(runcmd_fn.Count(), 1)
468
469   def testCommandsDisabled(self):
470     try:
471       backend.RunRestrictedCmd("test",
472                                _lock_timeout=NotImplemented,
473                                _lock_file=NotImplemented,
474                                _path=NotImplemented,
475                                _sleep_fn=NotImplemented,
476                                _prepare_fn=NotImplemented,
477                                _runcmd_fn=NotImplemented,
478                                _enabled=False)
479     except backend.RPCFail, err:
480       self.assertEqual(str(err), "Remote commands disabled at configure time")
481     else:
482       self.fail("Did not raise exception")
483
484
485 class TestSetWatcherPause(unittest.TestCase):
486   def setUp(self):
487     self.tmpdir = tempfile.mkdtemp()
488     self.filename = utils.PathJoin(self.tmpdir, "pause")
489
490   def tearDown(self):
491     shutil.rmtree(self.tmpdir)
492
493   def testUnsetNonExisting(self):
494     self.assertFalse(os.path.exists(self.filename))
495     backend.SetWatcherPause(None, _filename=self.filename)
496     self.assertFalse(os.path.exists(self.filename))
497
498   def testSetNonNumeric(self):
499     for i in ["", [], {}, "Hello World", "0", "1.0"]:
500       self.assertFalse(os.path.exists(self.filename))
501
502       try:
503         backend.SetWatcherPause(i, _filename=self.filename)
504       except backend.RPCFail, err:
505         self.assertEqual(str(err), "Duration must be numeric")
506       else:
507         self.fail("Did not raise exception")
508
509       self.assertFalse(os.path.exists(self.filename))
510
511   def testSet(self):
512     self.assertFalse(os.path.exists(self.filename))
513
514     for i in range(10):
515       backend.SetWatcherPause(i, _filename=self.filename)
516       self.assertEqual(utils.ReadFile(self.filename), "%s\n" % i)
517       self.assertEqual(os.stat(self.filename).st_mode & 0777, 0644)
518
519
520 class TestGetBlockDevSymlinkPath(unittest.TestCase):
521   def setUp(self):
522     self.tmpdir = tempfile.mkdtemp()
523
524   def tearDown(self):
525     shutil.rmtree(self.tmpdir)
526
527   def _Test(self, name, idx):
528     self.assertEqual(backend._GetBlockDevSymlinkPath(name, idx,
529                                                      _dir=self.tmpdir),
530                      ("%s/%s%s%s" % (self.tmpdir, name,
531                                      constants.DISK_SEPARATOR, idx)))
532
533   def test(self):
534     for idx in range(100):
535       self._Test("inst1.example.com", idx)
536
537
538 if __name__ == "__main__":
539   testutils.GanetiTestProgram()