Statistics
| Branch: | Tag: | Revision:

root / test / ganeti.backend_unittest.py @ 99e222b1

History | View | Annotate | Download (18.1 kB)

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
if __name__ == "__main__":
521
  testutils.GanetiTestProgram()