Statistics
| Branch: | Tag: | Revision:

root / test / py / ganeti.backend_unittest.py @ fac83f8a

History | View | Annotate | Download (19.5 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
import mock
30

    
31
from ganeti import utils
32
from ganeti import constants
33
from ganeti import backend
34
from ganeti import netutils
35
from ganeti import errors
36
from ganeti import serializer
37
from ganeti import hypervisor
38

    
39
import testutils
40
import mocks
41

    
42

    
43
class TestX509Certificates(unittest.TestCase):
44
  def setUp(self):
45
    self.tmpdir = tempfile.mkdtemp()
46

    
47
  def tearDown(self):
48
    shutil.rmtree(self.tmpdir)
49

    
50
  def test(self):
51
    (name, cert_pem) = backend.CreateX509Certificate(300, cryptodir=self.tmpdir)
52

    
53
    self.assertEqual(utils.ReadFile(os.path.join(self.tmpdir, name,
54
                                                 backend._X509_CERT_FILE)),
55
                     cert_pem)
56
    self.assert_(0 < os.path.getsize(os.path.join(self.tmpdir, name,
57
                                                  backend._X509_KEY_FILE)))
58

    
59
    (name2, cert_pem2) = \
60
      backend.CreateX509Certificate(300, cryptodir=self.tmpdir)
61

    
62
    backend.RemoveX509Certificate(name, cryptodir=self.tmpdir)
63
    backend.RemoveX509Certificate(name2, cryptodir=self.tmpdir)
64

    
65
    self.assertEqual(utils.ListVisibleFiles(self.tmpdir), [])
66

    
67
  def testNonEmpty(self):
68
    (name, _) = backend.CreateX509Certificate(300, cryptodir=self.tmpdir)
69

    
70
    utils.WriteFile(utils.PathJoin(self.tmpdir, name, "hello-world"),
71
                    data="Hello World")
72

    
73
    self.assertRaises(backend.RPCFail, backend.RemoveX509Certificate,
74
                      name, cryptodir=self.tmpdir)
75

    
76
    self.assertEqual(utils.ListVisibleFiles(self.tmpdir), [name])
77

    
78

    
79
class TestNodeVerify(testutils.GanetiTestCase):
80
  def testMasterIPLocalhost(self):
81
    # this a real functional test, but requires localhost to be reachable
82
    local_data = (netutils.Hostname.GetSysName(),
83
                  constants.IP4_ADDRESS_LOCALHOST)
84
    result = backend.VerifyNode({constants.NV_MASTERIP: local_data}, None)
85
    self.failUnless(constants.NV_MASTERIP in result,
86
                    "Master IP data not returned")
87
    self.failUnless(result[constants.NV_MASTERIP], "Cannot reach localhost")
88

    
89
  def testMasterIPUnreachable(self):
90
    # Network 192.0.2.0/24 is reserved for test/documentation as per
91
    # RFC 5737
92
    bad_data =  ("master.example.com", "192.0.2.1")
93
    # we just test that whatever TcpPing returns, VerifyNode returns too
94
    netutils.TcpPing = lambda a, b, source=None: False
95
    result = backend.VerifyNode({constants.NV_MASTERIP: bad_data}, None)
96
    self.failUnless(constants.NV_MASTERIP in result,
97
                    "Master IP data not returned")
98
    self.failIf(result[constants.NV_MASTERIP],
99
                "Result from netutils.TcpPing corrupted")
100

    
101

    
102
def _DefRestrictedCmdOwner():
103
  return (os.getuid(), os.getgid())
104

    
105

    
106
class TestVerifyRestrictedCmdName(unittest.TestCase):
107
  def testAcceptableName(self):
108
    for i in ["foo", "bar", "z1", "000first", "hello-world"]:
109
      for fn in [lambda s: s, lambda s: s.upper(), lambda s: s.title()]:
110
        (status, msg) = backend._VerifyRestrictedCmdName(fn(i))
111
        self.assertTrue(status)
112
        self.assertTrue(msg is None)
113

    
114
  def testEmptyAndSpace(self):
115
    for i in ["", " ", "\t", "\n"]:
116
      (status, msg) = backend._VerifyRestrictedCmdName(i)
117
      self.assertFalse(status)
118
      self.assertEqual(msg, "Missing command name")
119

    
120
  def testNameWithSlashes(self):
121
    for i in ["/", "./foo", "../moo", "some/name"]:
122
      (status, msg) = backend._VerifyRestrictedCmdName(i)
123
      self.assertFalse(status)
124
      self.assertEqual(msg, "Invalid command name")
125

    
126
  def testForbiddenCharacters(self):
127
    for i in ["#", ".", "..", "bash -c ls", "'"]:
128
      (status, msg) = backend._VerifyRestrictedCmdName(i)
129
      self.assertFalse(status)
130
      self.assertEqual(msg, "Command name contains forbidden characters")
131

    
132

    
133
class TestVerifyRestrictedCmdDirectory(unittest.TestCase):
134
  def setUp(self):
135
    self.tmpdir = tempfile.mkdtemp()
136

    
137
  def tearDown(self):
138
    shutil.rmtree(self.tmpdir)
139

    
140
  def testCanNotStat(self):
141
    tmpname = utils.PathJoin(self.tmpdir, "foobar")
142
    self.assertFalse(os.path.exists(tmpname))
143
    (status, msg) = \
144
      backend._VerifyRestrictedCmdDirectory(tmpname, _owner=NotImplemented)
145
    self.assertFalse(status)
146
    self.assertTrue(msg.startswith("Can't stat(2) '"))
147

    
148
  def testTooPermissive(self):
149
    tmpname = utils.PathJoin(self.tmpdir, "foobar")
150
    os.mkdir(tmpname)
151

    
152
    for mode in [0777, 0706, 0760, 0722]:
153
      os.chmod(tmpname, mode)
154
      self.assertTrue(os.path.isdir(tmpname))
155
      (status, msg) = \
156
        backend._VerifyRestrictedCmdDirectory(tmpname, _owner=NotImplemented)
157
      self.assertFalse(status)
158
      self.assertTrue(msg.startswith("Permissions on '"))
159

    
160
  def testNoDirectory(self):
161
    tmpname = utils.PathJoin(self.tmpdir, "foobar")
162
    utils.WriteFile(tmpname, data="empty\n")
163
    self.assertTrue(os.path.isfile(tmpname))
164
    (status, msg) = \
165
      backend._VerifyRestrictedCmdDirectory(tmpname,
166
                                            _owner=_DefRestrictedCmdOwner())
167
    self.assertFalse(status)
168
    self.assertTrue(msg.endswith("is not a directory"))
169

    
170
  def testNormal(self):
171
    tmpname = utils.PathJoin(self.tmpdir, "foobar")
172
    os.mkdir(tmpname)
173
    self.assertTrue(os.path.isdir(tmpname))
174
    (status, msg) = \
175
      backend._VerifyRestrictedCmdDirectory(tmpname,
176
                                            _owner=_DefRestrictedCmdOwner())
177
    self.assertTrue(status)
178
    self.assertTrue(msg is None)
179

    
180

    
181
class TestVerifyRestrictedCmd(unittest.TestCase):
182
  def setUp(self):
183
    self.tmpdir = tempfile.mkdtemp()
184

    
185
  def tearDown(self):
186
    shutil.rmtree(self.tmpdir)
187

    
188
  def testCanNotStat(self):
189
    tmpname = utils.PathJoin(self.tmpdir, "helloworld")
190
    self.assertFalse(os.path.exists(tmpname))
191
    (status, msg) = \
192
      backend._VerifyRestrictedCmd(self.tmpdir, "helloworld",
193
                                   _owner=NotImplemented)
194
    self.assertFalse(status)
195
    self.assertTrue(msg.startswith("Can't stat(2) '"))
196

    
197
  def testNotExecutable(self):
198
    tmpname = utils.PathJoin(self.tmpdir, "cmdname")
199
    utils.WriteFile(tmpname, data="empty\n")
200
    (status, msg) = \
201
      backend._VerifyRestrictedCmd(self.tmpdir, "cmdname",
202
                                   _owner=_DefRestrictedCmdOwner())
203
    self.assertFalse(status)
204
    self.assertTrue(msg.startswith("access(2) thinks '"))
205

    
206
  def testExecutable(self):
207
    tmpname = utils.PathJoin(self.tmpdir, "cmdname")
208
    utils.WriteFile(tmpname, data="empty\n", mode=0700)
209
    (status, executable) = \
210
      backend._VerifyRestrictedCmd(self.tmpdir, "cmdname",
211
                                   _owner=_DefRestrictedCmdOwner())
212
    self.assertTrue(status)
213
    self.assertEqual(executable, tmpname)
214

    
215

    
216
class TestPrepareRestrictedCmd(unittest.TestCase):
217
  _TEST_PATH = "/tmp/some/test/path"
218

    
219
  def testDirFails(self):
220
    def fn(path):
221
      self.assertEqual(path, self._TEST_PATH)
222
      return (False, "test error 31420")
223

    
224
    (status, msg) = \
225
      backend._PrepareRestrictedCmd(self._TEST_PATH, "cmd21152",
226
                                    _verify_dir=fn,
227
                                    _verify_name=NotImplemented,
228
                                    _verify_cmd=NotImplemented)
229
    self.assertFalse(status)
230
    self.assertEqual(msg, "test error 31420")
231

    
232
  def testNameFails(self):
233
    def fn(cmd):
234
      self.assertEqual(cmd, "cmd4617")
235
      return (False, "test error 591")
236

    
237
    (status, msg) = \
238
      backend._PrepareRestrictedCmd(self._TEST_PATH, "cmd4617",
239
                                    _verify_dir=lambda _: (True, None),
240
                                    _verify_name=fn,
241
                                    _verify_cmd=NotImplemented)
242
    self.assertFalse(status)
243
    self.assertEqual(msg, "test error 591")
244

    
245
  def testCommandFails(self):
246
    def fn(path, cmd):
247
      self.assertEqual(path, self._TEST_PATH)
248
      self.assertEqual(cmd, "cmd17577")
249
      return (False, "test error 25524")
250

    
251
    (status, msg) = \
252
      backend._PrepareRestrictedCmd(self._TEST_PATH, "cmd17577",
253
                                    _verify_dir=lambda _: (True, None),
254
                                    _verify_name=lambda _: (True, None),
255
                                    _verify_cmd=fn)
256
    self.assertFalse(status)
257
    self.assertEqual(msg, "test error 25524")
258

    
259
  def testSuccess(self):
260
    def fn(path, cmd):
261
      return (True, utils.PathJoin(path, cmd))
262

    
263
    (status, executable) = \
264
      backend._PrepareRestrictedCmd(self._TEST_PATH, "cmd22633",
265
                                    _verify_dir=lambda _: (True, None),
266
                                    _verify_name=lambda _: (True, None),
267
                                    _verify_cmd=fn)
268
    self.assertTrue(status)
269
    self.assertEqual(executable, utils.PathJoin(self._TEST_PATH, "cmd22633"))
270

    
271

    
272
def _SleepForRestrictedCmd(duration):
273
  assert duration > 5
274

    
275

    
276
def _GenericRestrictedCmdError(cmd):
277
  return "Executing command '%s' failed" % cmd
278

    
279

    
280
class TestRunRestrictedCmd(unittest.TestCase):
281
  def setUp(self):
282
    self.tmpdir = tempfile.mkdtemp()
283

    
284
  def tearDown(self):
285
    shutil.rmtree(self.tmpdir)
286

    
287
  def testNonExistantLockDirectory(self):
288
    lockfile = utils.PathJoin(self.tmpdir, "does", "not", "exist")
289
    sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
290
    self.assertFalse(os.path.exists(lockfile))
291
    self.assertRaises(backend.RPCFail,
292
                      backend.RunRestrictedCmd, "test",
293
                      _lock_timeout=NotImplemented,
294
                      _lock_file=lockfile,
295
                      _path=NotImplemented,
296
                      _sleep_fn=sleep_fn,
297
                      _prepare_fn=NotImplemented,
298
                      _runcmd_fn=NotImplemented,
299
                      _enabled=True)
300
    self.assertEqual(sleep_fn.Count(), 1)
301

    
302
  @staticmethod
303
  def _TryLock(lockfile):
304
    sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
305

    
306
    result = False
307
    try:
308
      backend.RunRestrictedCmd("test22717",
309
                               _lock_timeout=0.1,
310
                               _lock_file=lockfile,
311
                               _path=NotImplemented,
312
                               _sleep_fn=sleep_fn,
313
                               _prepare_fn=NotImplemented,
314
                               _runcmd_fn=NotImplemented,
315
                               _enabled=True)
316
    except backend.RPCFail, err:
317
      assert str(err) == _GenericRestrictedCmdError("test22717"), \
318
             "Did not fail with generic error message"
319
      result = True
320

    
321
    assert sleep_fn.Count() == 1
322

    
323
    return result
324

    
325
  def testLockHeldByOtherProcess(self):
326
    lockfile = utils.PathJoin(self.tmpdir, "lock")
327

    
328
    lock = utils.FileLock.Open(lockfile)
329
    lock.Exclusive(blocking=True, timeout=1.0)
330
    try:
331
      self.assertTrue(utils.RunInSeparateProcess(self._TryLock, lockfile))
332
    finally:
333
      lock.Close()
334

    
335
  @staticmethod
336
  def _PrepareRaisingException(path, cmd):
337
    assert cmd == "test23122"
338
    raise Exception("test")
339

    
340
  def testPrepareRaisesException(self):
341
    lockfile = utils.PathJoin(self.tmpdir, "lock")
342

    
343
    sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
344
    prepare_fn = testutils.CallCounter(self._PrepareRaisingException)
345

    
346
    try:
347
      backend.RunRestrictedCmd("test23122",
348
                               _lock_timeout=1.0, _lock_file=lockfile,
349
                               _path=NotImplemented, _runcmd_fn=NotImplemented,
350
                               _sleep_fn=sleep_fn, _prepare_fn=prepare_fn,
351
                               _enabled=True)
352
    except backend.RPCFail, err:
353
      self.assertEqual(str(err), _GenericRestrictedCmdError("test23122"))
354
    else:
355
      self.fail("Didn't fail")
356

    
357
    self.assertEqual(sleep_fn.Count(), 1)
358
    self.assertEqual(prepare_fn.Count(), 1)
359

    
360
  @staticmethod
361
  def _PrepareFails(path, cmd):
362
    assert cmd == "test29327"
363
    return ("some error message", None)
364

    
365
  def testPrepareFails(self):
366
    lockfile = utils.PathJoin(self.tmpdir, "lock")
367

    
368
    sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
369
    prepare_fn = testutils.CallCounter(self._PrepareFails)
370

    
371
    try:
372
      backend.RunRestrictedCmd("test29327",
373
                               _lock_timeout=1.0, _lock_file=lockfile,
374
                               _path=NotImplemented, _runcmd_fn=NotImplemented,
375
                               _sleep_fn=sleep_fn, _prepare_fn=prepare_fn,
376
                               _enabled=True)
377
    except backend.RPCFail, err:
378
      self.assertEqual(str(err), _GenericRestrictedCmdError("test29327"))
379
    else:
380
      self.fail("Didn't fail")
381

    
382
    self.assertEqual(sleep_fn.Count(), 1)
383
    self.assertEqual(prepare_fn.Count(), 1)
384

    
385
  @staticmethod
386
  def _SuccessfulPrepare(path, cmd):
387
    return (True, utils.PathJoin(path, cmd))
388

    
389
  def testRunCmdFails(self):
390
    lockfile = utils.PathJoin(self.tmpdir, "lock")
391

    
392
    def fn(args, env=NotImplemented, reset_env=NotImplemented,
393
           postfork_fn=NotImplemented):
394
      self.assertEqual(args, [utils.PathJoin(self.tmpdir, "test3079")])
395
      self.assertEqual(env, {})
396
      self.assertTrue(reset_env)
397
      self.assertTrue(callable(postfork_fn))
398

    
399
      trylock = utils.FileLock.Open(lockfile)
400
      try:
401
        # See if lockfile is still held
402
        self.assertRaises(EnvironmentError, trylock.Exclusive, blocking=False)
403

    
404
        # Call back to release lock
405
        postfork_fn(NotImplemented)
406

    
407
        # See if lockfile can be acquired
408
        trylock.Exclusive(blocking=False)
409
      finally:
410
        trylock.Close()
411

    
412
      # Simulate a failed command
413
      return utils.RunResult(constants.EXIT_FAILURE, None,
414
                             "stdout", "stderr406328567",
415
                             utils.ShellQuoteArgs(args),
416
                             NotImplemented, NotImplemented)
417

    
418
    sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
419
    prepare_fn = testutils.CallCounter(self._SuccessfulPrepare)
420
    runcmd_fn = testutils.CallCounter(fn)
421

    
422
    try:
423
      backend.RunRestrictedCmd("test3079",
424
                               _lock_timeout=1.0, _lock_file=lockfile,
425
                               _path=self.tmpdir, _runcmd_fn=runcmd_fn,
426
                               _sleep_fn=sleep_fn, _prepare_fn=prepare_fn,
427
                               _enabled=True)
428
    except backend.RPCFail, err:
429
      self.assertTrue(str(err).startswith("Restricted command 'test3079'"
430
                                          " failed:"))
431
      self.assertTrue("stderr406328567" in str(err),
432
                      msg="Error did not include output")
433
    else:
434
      self.fail("Didn't fail")
435

    
436
    self.assertEqual(sleep_fn.Count(), 0)
437
    self.assertEqual(prepare_fn.Count(), 1)
438
    self.assertEqual(runcmd_fn.Count(), 1)
439

    
440
  def testRunCmdSucceeds(self):
441
    lockfile = utils.PathJoin(self.tmpdir, "lock")
442

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

    
449
      # Call back to release lock
450
      postfork_fn(NotImplemented)
451

    
452
      # Simulate a successful command
453
      return utils.RunResult(constants.EXIT_SUCCESS, None, "stdout14463", "",
454
                             utils.ShellQuoteArgs(args),
455
                             NotImplemented, NotImplemented)
456

    
457
    sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
458
    prepare_fn = testutils.CallCounter(self._SuccessfulPrepare)
459
    runcmd_fn = testutils.CallCounter(fn)
460

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

    
469
    self.assertEqual(sleep_fn.Count(), 0)
470
    self.assertEqual(prepare_fn.Count(), 1)
471
    self.assertEqual(runcmd_fn.Count(), 1)
472

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

    
489

    
490
class TestSetWatcherPause(unittest.TestCase):
491
  def setUp(self):
492
    self.tmpdir = tempfile.mkdtemp()
493
    self.filename = utils.PathJoin(self.tmpdir, "pause")
494

    
495
  def tearDown(self):
496
    shutil.rmtree(self.tmpdir)
497

    
498
  def testUnsetNonExisting(self):
499
    self.assertFalse(os.path.exists(self.filename))
500
    backend.SetWatcherPause(None, _filename=self.filename)
501
    self.assertFalse(os.path.exists(self.filename))
502

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

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

    
514
      self.assertFalse(os.path.exists(self.filename))
515

    
516
  def testSet(self):
517
    self.assertFalse(os.path.exists(self.filename))
518

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

    
524

    
525
class TestGetBlockDevSymlinkPath(unittest.TestCase):
526
  def setUp(self):
527
    self.tmpdir = tempfile.mkdtemp()
528

    
529
  def tearDown(self):
530
    shutil.rmtree(self.tmpdir)
531

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

    
538
  def test(self):
539
    for idx in range(100):
540
      self._Test("inst1.example.com", idx)
541

    
542

    
543
class TestGetInstanceList(unittest.TestCase):
544

    
545
  def setUp(self):
546
    self._test_hv = self._TestHypervisor()
547
    self._test_hv.ListInstances = mock.Mock(
548
      return_value=["instance1", "instance2", "instance3"] )
549

    
550
  class _TestHypervisor(hypervisor.hv_base.BaseHypervisor):
551
    def __init__(self):
552
      hypervisor.hv_base.BaseHypervisor.__init__(self)
553

    
554
  def _GetHypervisor(self, name):
555
    return self._test_hv
556

    
557
  def testHvparams(self):
558
    fake_hvparams = {constants.HV_XEN_CMD: constants.XEN_CMD_XL}
559
    hvparams = {constants.HT_FAKE: fake_hvparams}
560
    backend.GetInstanceList([constants.HT_FAKE], all_hvparams=hvparams,
561
                            get_hv_fn=self._GetHypervisor)
562
    self._test_hv.ListInstances.assert_called_with(hvparams=fake_hvparams)
563

    
564

    
565
if __name__ == "__main__":
566
  testutils.GanetiTestProgram()