Statistics
| Branch: | Tag: | Revision:

root / test / py / ganeti.backend_unittest.py @ 45bc4635

History | View | Annotate | Download (18.7 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2010, 2013 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("Restricted command 'test3079'"
427
                                          " failed:"))
428
      self.assertTrue("stderr406328567" in str(err),
429
                      msg="Error did not include output")
430
    else:
431
      self.fail("Didn't fail")
432

    
433
    self.assertEqual(sleep_fn.Count(), 0)
434
    self.assertEqual(prepare_fn.Count(), 1)
435
    self.assertEqual(runcmd_fn.Count(), 1)
436

    
437
  def testRunCmdSucceeds(self):
438
    lockfile = utils.PathJoin(self.tmpdir, "lock")
439

    
440
    def fn(args, env=NotImplemented, reset_env=NotImplemented,
441
           postfork_fn=NotImplemented):
442
      self.assertEqual(args, [utils.PathJoin(self.tmpdir, "test5667")])
443
      self.assertEqual(env, {})
444
      self.assertTrue(reset_env)
445

    
446
      # Call back to release lock
447
      postfork_fn(NotImplemented)
448

    
449
      # Simulate a successful command
450
      return utils.RunResult(constants.EXIT_SUCCESS, None, "stdout14463", "",
451
                             utils.ShellQuoteArgs(args),
452
                             NotImplemented, NotImplemented)
453

    
454
    sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
455
    prepare_fn = testutils.CallCounter(self._SuccessfulPrepare)
456
    runcmd_fn = testutils.CallCounter(fn)
457

    
458
    result = backend.RunRestrictedCmd("test5667",
459
                                      _lock_timeout=1.0, _lock_file=lockfile,
460
                                      _path=self.tmpdir, _runcmd_fn=runcmd_fn,
461
                                      _sleep_fn=sleep_fn,
462
                                      _prepare_fn=prepare_fn,
463
                                      _enabled=True)
464
    self.assertEqual(result, "stdout14463")
465

    
466
    self.assertEqual(sleep_fn.Count(), 0)
467
    self.assertEqual(prepare_fn.Count(), 1)
468
    self.assertEqual(runcmd_fn.Count(), 1)
469

    
470
  def testCommandsDisabled(self):
471
    try:
472
      backend.RunRestrictedCmd("test",
473
                               _lock_timeout=NotImplemented,
474
                               _lock_file=NotImplemented,
475
                               _path=NotImplemented,
476
                               _sleep_fn=NotImplemented,
477
                               _prepare_fn=NotImplemented,
478
                               _runcmd_fn=NotImplemented,
479
                               _enabled=False)
480
    except backend.RPCFail, err:
481
      self.assertEqual(str(err),
482
                       "Restricted commands disabled at configure time")
483
    else:
484
      self.fail("Did not raise exception")
485

    
486

    
487
class TestSetWatcherPause(unittest.TestCase):
488
  def setUp(self):
489
    self.tmpdir = tempfile.mkdtemp()
490
    self.filename = utils.PathJoin(self.tmpdir, "pause")
491

    
492
  def tearDown(self):
493
    shutil.rmtree(self.tmpdir)
494

    
495
  def testUnsetNonExisting(self):
496
    self.assertFalse(os.path.exists(self.filename))
497
    backend.SetWatcherPause(None, _filename=self.filename)
498
    self.assertFalse(os.path.exists(self.filename))
499

    
500
  def testSetNonNumeric(self):
501
    for i in ["", [], {}, "Hello World", "0", "1.0"]:
502
      self.assertFalse(os.path.exists(self.filename))
503

    
504
      try:
505
        backend.SetWatcherPause(i, _filename=self.filename)
506
      except backend.RPCFail, err:
507
        self.assertEqual(str(err), "Duration must be numeric")
508
      else:
509
        self.fail("Did not raise exception")
510

    
511
      self.assertFalse(os.path.exists(self.filename))
512

    
513
  def testSet(self):
514
    self.assertFalse(os.path.exists(self.filename))
515

    
516
    for i in range(10):
517
      backend.SetWatcherPause(i, _filename=self.filename)
518
      self.assertEqual(utils.ReadFile(self.filename), "%s\n" % i)
519
      self.assertEqual(os.stat(self.filename).st_mode & 0777, 0644)
520

    
521

    
522
class TestGetBlockDevSymlinkPath(unittest.TestCase):
523
  def setUp(self):
524
    self.tmpdir = tempfile.mkdtemp()
525

    
526
  def tearDown(self):
527
    shutil.rmtree(self.tmpdir)
528

    
529
  def _Test(self, name, idx):
530
    self.assertEqual(backend._GetBlockDevSymlinkPath(name, idx,
531
                                                     _dir=self.tmpdir),
532
                     ("%s/%s%s%s" % (self.tmpdir, name,
533
                                     constants.DISK_SEPARATOR, idx)))
534

    
535
  def test(self):
536
    for idx in range(100):
537
      self._Test("inst1.example.com", idx)
538

    
539

    
540
if __name__ == "__main__":
541
  testutils.GanetiTestProgram()