Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (19.1 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
from ganeti import serializer
36

    
37
import testutils
38
import mocks
39

    
40

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

    
45
  def tearDown(self):
46
    shutil.rmtree(self.tmpdir)
47

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

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

    
57
    (name2, cert_pem2) = \
58
      backend.CreateX509Certificate(300, cryptodir=self.tmpdir)
59

    
60
    backend.RemoveX509Certificate(name, cryptodir=self.tmpdir)
61
    backend.RemoveX509Certificate(name2, cryptodir=self.tmpdir)
62

    
63
    self.assertEqual(utils.ListVisibleFiles(self.tmpdir), [])
64

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

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

    
71
    self.assertRaises(backend.RPCFail, backend.RemoveX509Certificate,
72
                      name, cryptodir=self.tmpdir)
73

    
74
    self.assertEqual(utils.ListVisibleFiles(self.tmpdir), [name])
75

    
76

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

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

    
99

    
100
def _DefRestrictedCmdOwner():
101
  return (os.getuid(), os.getgid())
102

    
103

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

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

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

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

    
130

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

    
135
  def tearDown(self):
136
    shutil.rmtree(self.tmpdir)
137

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

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

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

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

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

    
178

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

    
183
  def tearDown(self):
184
    shutil.rmtree(self.tmpdir)
185

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

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

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

    
213

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

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

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

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

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

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

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

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

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

    
269

    
270
def _SleepForRestrictedCmd(duration):
271
  assert duration > 5
272

    
273

    
274
def _GenericRestrictedCmdError(cmd):
275
  return "Executing command '%s' failed" % cmd
276

    
277

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

    
282
  def tearDown(self):
283
    shutil.rmtree(self.tmpdir)
284

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

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

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

    
319
    assert sleep_fn.Count() == 1
320

    
321
    return result
322

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

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

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

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

    
341
    sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
342
    prepare_fn = testutils.CallCounter(self._PrepareRaisingException)
343

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

    
355
    self.assertEqual(sleep_fn.Count(), 1)
356
    self.assertEqual(prepare_fn.Count(), 1)
357

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

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

    
366
    sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
367
    prepare_fn = testutils.CallCounter(self._PrepareFails)
368

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

    
380
    self.assertEqual(sleep_fn.Count(), 1)
381
    self.assertEqual(prepare_fn.Count(), 1)
382

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

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

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

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

    
402
        # Call back to release lock
403
        postfork_fn(NotImplemented)
404

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

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

    
416
    sleep_fn = testutils.CallCounter(_SleepForRestrictedCmd)
417
    prepare_fn = testutils.CallCounter(self._SuccessfulPrepare)
418
    runcmd_fn = testutils.CallCounter(fn)
419

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

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

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

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

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

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

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

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

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

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

    
487

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

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

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

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

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

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

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

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

    
522

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

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

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

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

    
540

    
541
class TestInstReason(unittest.TestCase):
542
  def testGetJson(self):
543
    reason_text = "OS Update"
544
    reason_source = constants.INSTANCE_REASON_SOURCE_CLI
545
    origDict = dict(text=reason_text, source=reason_source)
546

    
547
    reason = backend.InstReason(reason_source, reason_text)
548
    json = reason.GetJson()
549
    resultDict = serializer.LoadJson(json)
550

    
551
    self.assertEqual(origDict, resultDict)
552

    
553

    
554
if __name__ == "__main__":
555
  testutils.GanetiTestProgram()