Revision 1a2eb2dc

b/Makefile.am
894 894
	test/cfgupgrade_unittest.py \
895 895
	test/docs_unittest.py \
896 896
	test/ganeti.asyncnotifier_unittest.py \
897
	test/ganeti.backend_unittest-runasroot.py \
897 898
	test/ganeti.backend_unittest.py \
898 899
	test/ganeti.bdev_unittest.py \
899 900
	test/ganeti.cli_unittest.py \
b/lib/backend.py
87 87
_MASTER_START = "start"
88 88
_MASTER_STOP = "stop"
89 89

  
90
#: Maximum file permissions for remote command directory and executables
91
_RCMD_MAX_MODE = (stat.S_IRWXU |
92
                  stat.S_IRGRP | stat.S_IXGRP |
93
                  stat.S_IROTH | stat.S_IXOTH)
94

  
95
#: Delay before returning an error for remote commands
96
_RCMD_INVALID_DELAY = 10
97

  
98
#: How long to wait to acquire lock for remote commands (shorter than
99
#: L{_RCMD_INVALID_DELAY}) to reduce blockage of noded forks when many
100
#: command requests arrive
101
_RCMD_LOCK_TIMEOUT = _RCMD_INVALID_DELAY * 0.8
102

  
90 103

  
91 104
class RPCFail(Exception):
92 105
  """Class denoting RPC failure.
......
3567 3580
  hyper.PowercycleNode()
3568 3581

  
3569 3582

  
3583
def _VerifyRemoteCommandName(cmd):
3584
  """Verifies a remote command name.
3585

  
3586
  @type cmd: string
3587
  @param cmd: Command name
3588
  @rtype: tuple; (boolean, string or None)
3589
  @return: The tuple's first element is the status; if C{False}, the second
3590
    element is an error message string, otherwise it's C{None}
3591

  
3592
  """
3593
  if not cmd.strip():
3594
    return (False, "Missing command name")
3595

  
3596
  if os.path.basename(cmd) != cmd:
3597
    return (False, "Invalid command name")
3598

  
3599
  if not constants.EXT_PLUGIN_MASK.match(cmd):
3600
    return (False, "Command name contains forbidden characters")
3601

  
3602
  return (True, None)
3603

  
3604

  
3605
def _CommonRemoteCommandCheck(path, owner):
3606
  """Common checks for remote command file system directories and files.
3607

  
3608
  @type path: string
3609
  @param path: Path to check
3610
  @param owner: C{None} or tuple containing UID and GID
3611
  @rtype: tuple; (boolean, string or C{os.stat} result)
3612
  @return: The tuple's first element is the status; if C{False}, the second
3613
    element is an error message string, otherwise it's the result of C{os.stat}
3614

  
3615
  """
3616
  if owner is None:
3617
    # Default to root as owner
3618
    owner = (0, 0)
3619

  
3620
  try:
3621
    st = os.stat(path)
3622
  except EnvironmentError, err:
3623
    return (False, "Can't stat(2) '%s': %s" % (path, err))
3624

  
3625
  if stat.S_IMODE(st.st_mode) & (~_RCMD_MAX_MODE):
3626
    return (False, "Permissions on '%s' are too permissive" % path)
3627

  
3628
  if (st.st_uid, st.st_gid) != owner:
3629
    (owner_uid, owner_gid) = owner
3630
    return (False, "'%s' is not owned by %s:%s" % (path, owner_uid, owner_gid))
3631

  
3632
  return (True, st)
3633

  
3634

  
3635
def _VerifyRemoteCommandDirectory(path, _owner=None):
3636
  """Verifies remote command directory.
3637

  
3638
  @type path: string
3639
  @param path: Path to check
3640
  @rtype: tuple; (boolean, string or None)
3641
  @return: The tuple's first element is the status; if C{False}, the second
3642
    element is an error message string, otherwise it's C{None}
3643

  
3644
  """
3645
  (status, value) = _CommonRemoteCommandCheck(path, _owner)
3646

  
3647
  if not status:
3648
    return (False, value)
3649

  
3650
  if not stat.S_ISDIR(value.st_mode):
3651
    return (False, "Path '%s' is not a directory" % path)
3652

  
3653
  return (True, None)
3654

  
3655

  
3656
def _VerifyRemoteCommand(path, cmd, _owner=None):
3657
  """Verifies a whole remote command and returns its executable filename.
3658

  
3659
  @type path: string
3660
  @param path: Directory containing remote commands
3661
  @type cmd: string
3662
  @param cmd: Command name
3663
  @rtype: tuple; (boolean, string)
3664
  @return: The tuple's first element is the status; if C{False}, the second
3665
    element is an error message string, otherwise the second element is the
3666
    absolute path to the executable
3667

  
3668
  """
3669
  executable = utils.PathJoin(path, cmd)
3670

  
3671
  (status, msg) = _CommonRemoteCommandCheck(executable, _owner)
3672

  
3673
  if not status:
3674
    return (False, msg)
3675

  
3676
  if not utils.IsExecutable(executable):
3677
    return (False, "access(2) thinks '%s' can't be executed" % executable)
3678

  
3679
  return (True, executable)
3680

  
3681

  
3682
def _PrepareRemoteCommand(path, cmd,
3683
                          _verify_dir=_VerifyRemoteCommandDirectory,
3684
                          _verify_name=_VerifyRemoteCommandName,
3685
                          _verify_cmd=_VerifyRemoteCommand):
3686
  """Performs a number of tests on a remote command.
3687

  
3688
  @type path: string
3689
  @param path: Directory containing remote commands
3690
  @type cmd: string
3691
  @param cmd: Command name
3692
  @return: Same as L{_VerifyRemoteCommand}
3693

  
3694
  """
3695
  # Verify the directory first
3696
  (status, msg) = _verify_dir(path)
3697
  if status:
3698
    # Check command if everything was alright
3699
    (status, msg) = _verify_name(cmd)
3700

  
3701
  if not status:
3702
    return (False, msg)
3703

  
3704
  # Check actual executable
3705
  return _verify_cmd(path, cmd)
3706

  
3707

  
3708
def RunRemoteCommand(cmd,
3709
                     _lock_timeout=_RCMD_LOCK_TIMEOUT,
3710
                     _lock_file=pathutils.REMOTE_COMMANDS_LOCK_FILE,
3711
                     _path=pathutils.REMOTE_COMMANDS_DIR,
3712
                     _sleep_fn=time.sleep,
3713
                     _prepare_fn=_PrepareRemoteCommand,
3714
                     _runcmd_fn=utils.RunCmd,
3715
                     _enabled=constants.ENABLE_REMOTE_COMMANDS):
3716
  """Executes a remote command after performing strict tests.
3717

  
3718
  @type cmd: string
3719
  @param cmd: Command name
3720
  @rtype: string
3721
  @return: Command output
3722
  @raise RPCFail: In case of an error
3723

  
3724
  """
3725
  logging.info("Preparing to run remote command '%s'", cmd)
3726

  
3727
  if not _enabled:
3728
    _Fail("Remote commands disabled at configure time")
3729

  
3730
  lock = None
3731
  try:
3732
    cmdresult = None
3733
    try:
3734
      lock = utils.FileLock.Open(_lock_file)
3735
      lock.Exclusive(blocking=True, timeout=_lock_timeout)
3736

  
3737
      (status, value) = _prepare_fn(_path, cmd)
3738

  
3739
      if status:
3740
        cmdresult = _runcmd_fn([value], env={}, reset_env=True,
3741
                               postfork_fn=lambda _: lock.Unlock())
3742
      else:
3743
        logging.error(value)
3744
    except Exception: # pylint: disable=W0703
3745
      # Keep original error in log
3746
      logging.exception("Caught exception")
3747

  
3748
    if cmdresult is None:
3749
      logging.info("Sleeping for %0.1f seconds before returning",
3750
                   _RCMD_INVALID_DELAY)
3751
      _sleep_fn(_RCMD_INVALID_DELAY)
3752

  
3753
      # Do not include original error message in returned error
3754
      _Fail("Executing command '%s' failed" % cmd)
3755
    elif cmdresult.failed or cmdresult.fail_reason:
3756
      _Fail("Remote command '%s' failed: %s; output: %s",
3757
            cmd, cmdresult.fail_reason, cmdresult.output)
3758
    else:
3759
      return cmdresult.output
3760
  finally:
3761
    if lock is not None:
3762
      # Release lock at last
3763
      lock.Close()
3764
      lock = None
3765

  
3766

  
3570 3767
class HooksRunner(object):
3571 3768
  """Hook runner.
3572 3769

  
b/test/ganeti.backend_unittest-runasroot.py
1
#!/usr/bin/python
2
#
3

  
4
# Copyright (C) 2012 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 (tests requiring root access)"""
23

  
24
import os
25
import tempfile
26
import shutil
27
import errno
28

  
29
from ganeti import constants
30
from ganeti import utils
31
from ganeti import compat
32
from ganeti import backend
33

  
34
import testutils
35

  
36

  
37
class TestWriteFile(testutils.GanetiTestCase):
38
  def setUp(self):
39
    self.tmpdir = tempfile.mkdtemp()
40

  
41
  def tearDown(self):
42
    shutil.rmtree(self.tmpdir)
43

  
44
  def _PrepareTest(self):
45
    tmpname = utils.PathJoin(self.tmpdir, "foobar")
46
    os.mkdir(tmpname)
47
    os.chmod(tmpname, 0700)
48
    return tmpname
49

  
50
  def testCorrectOwner(self):
51
    tmpname = self._PrepareTest()
52

  
53
    os.chown(tmpname, 0, 0)
54
    (status, value) = backend._CommonRemoteCommandCheck(tmpname, None)
55
    self.assertTrue(status)
56
    self.assertTrue(value)
57

  
58
  def testWrongOwner(self):
59
    tmpname = self._PrepareTest()
60

  
61
    tests = [
62
      (1, 0),
63
      (0, 1),
64
      (100, 50),
65
      ]
66

  
67
    for (uid, gid) in tests:
68
      self.assertFalse(uid == os.getuid() and gid == os.getgid())
69
      os.chown(tmpname, uid, gid)
70

  
71
      (status, errmsg) = backend._CommonRemoteCommandCheck(tmpname, None)
72
      self.assertFalse(status)
73
      self.assertTrue("foobar' is not owned by " in errmsg)
74

  
75

  
76
if __name__ == "__main__":
77
  testutils.GanetiTestProgram()
b/test/ganeti.backend_unittest.py
31 31
from ganeti import constants
32 32
from ganeti import backend
33 33
from ganeti import netutils
34
from ganeti import errors
34 35

  
35 36
import testutils
37
import mocks
36 38

  
37 39

  
38 40
class TestX509Certificates(unittest.TestCase):
......
94 96
                "Result from netutils.TcpPing corrupted")
95 97

  
96 98

  
99
def _DefRemoteCommandOwner():
100
  return (os.getuid(), os.getgid())
101

  
102

  
103
class TestVerifyRemoteCommandName(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._VerifyRemoteCommandName(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._VerifyRemoteCommandName(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._VerifyRemoteCommandName(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._VerifyRemoteCommandName(i)
126
      self.assertFalse(status)
127
      self.assertEqual(msg, "Command name contains forbidden characters")
128

  
129

  
130
class TestVerifyRemoteCommandDirectory(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._VerifyRemoteCommandDirectory(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._VerifyRemoteCommandDirectory(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._VerifyRemoteCommandDirectory(tmpname,
163
                                            _owner=_DefRemoteCommandOwner())
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._VerifyRemoteCommandDirectory(tmpname,
173
                                            _owner=_DefRemoteCommandOwner())
174
    self.assertTrue(status)
175
    self.assertTrue(msg is None)
176

  
177

  
178
class TestVerifyRemoteCommand(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._VerifyRemoteCommand(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._VerifyRemoteCommand(self.tmpdir, "cmdname",
199
                                   _owner=_DefRemoteCommandOwner())
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._VerifyRemoteCommand(self.tmpdir, "cmdname",
208
                                   _owner=_DefRemoteCommandOwner())
209
    self.assertTrue(status)
210
    self.assertEqual(executable, tmpname)
211

  
212

  
213
class TestPrepareRemoteCommand(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._PrepareRemoteCommand(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._PrepareRemoteCommand(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._PrepareRemoteCommand(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._PrepareRemoteCommand(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 _SleepForRemoteCommand(duration):
270
  assert duration > 5
271

  
272

  
273
def _GenericRemoteCommandError(cmd):
274
  return "Executing command '%s' failed" % cmd
275

  
276

  
277
class TestRunRemoteCommand(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(_SleepForRemoteCommand)
287
    self.assertFalse(os.path.exists(lockfile))
288
    self.assertRaises(backend.RPCFail,
289
                      backend.RunRemoteCommand, "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(_SleepForRemoteCommand)
302

  
303
    result = False
304
    try:
305
      backend.RunRemoteCommand("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) == _GenericRemoteCommandError("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(_SleepForRemoteCommand)
341
    prepare_fn = testutils.CallCounter(self._PrepareRaisingException)
342

  
343
    try:
344
      backend.RunRemoteCommand("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), _GenericRemoteCommandError("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(_SleepForRemoteCommand)
366
    prepare_fn = testutils.CallCounter(self._PrepareFails)
367

  
368
    try:
369
      backend.RunRemoteCommand("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), _GenericRemoteCommandError("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(_SleepForRemoteCommand)
416
    prepare_fn = testutils.CallCounter(self._SuccessfulPrepare)
417
    runcmd_fn = testutils.CallCounter(fn)
418

  
419
    try:
420
      backend.RunRemoteCommand("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(_SleepForRemoteCommand)
454
    prepare_fn = testutils.CallCounter(self._SuccessfulPrepare)
455
    runcmd_fn = testutils.CallCounter(fn)
456

  
457
    result = backend.RunRemoteCommand("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.RunRemoteCommand("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

  
97 485
if __name__ == "__main__":
98 486
  testutils.GanetiTestProgram()

Also available in: Unified diff