~free.ekanayaka/landscape-client/lucid-1.5.0-0ubuntu0.10.04.0

« back to all changes in this revision

Viewing changes to landscape/manager/tests/test_scriptexecution.py

  • Committer: Bazaar Package Importer
  • Author(s): Rick Clark
  • Date: 2008-09-08 16:35:57 UTC
  • mfrom: (1.1.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080908163557-l3ixzj5dxz37wnw2
Tags: 1.0.18-0ubuntu1
New upstream release 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import pwd
 
2
import os
 
3
import sys
 
4
import tempfile
 
5
import stat
 
6
 
 
7
from twisted.internet.defer import gatherResults
 
8
from twisted.internet.error import ProcessDone
 
9
from twisted.python.failure import Failure
 
10
 
 
11
from landscape.manager.scriptexecution import (
 
12
    ScriptExecution, ProcessTimeLimitReachedError, PROCESS_FAILED_RESULT,
 
13
    ProcessFailedError)
 
14
from landscape.manager.manager import SUCCEEDED, FAILED
 
15
from landscape.tests.helpers import (
 
16
    LandscapeTest, LandscapeIsolatedTest, ManagerHelper,
 
17
    StubProcessFactory, DummyProcess)
 
18
from landscape.tests.mocker import ANY, ARGS
 
19
 
 
20
 
 
21
class RunScriptTests(LandscapeTest):
 
22
 
 
23
    helpers = [ManagerHelper]
 
24
 
 
25
    def setUp(self):
 
26
        super(RunScriptTests, self).setUp()
 
27
        self.plugin = ScriptExecution()
 
28
        self.manager.add(self.plugin)
 
29
 
 
30
    def test_basic_run(self):
 
31
        """
 
32
        The plugin returns a Deferred resulting in the output of basic
 
33
        commands.
 
34
        """
 
35
        result = self.plugin.run_script("/bin/sh", "echo hi")
 
36
        result.addCallback(self.assertEquals, "hi\n")
 
37
        return result
 
38
 
 
39
    def test_other_interpreter(self):
 
40
        """Non-shell interpreters can be specified."""
 
41
        result = self.plugin.run_script("/usr/bin/python", "print 'hi'")
 
42
        result.addCallback(self.assertEquals, "hi\n")
 
43
        return result
 
44
 
 
45
    def test_concurrent(self):
 
46
        """Scripts run with the ScriptExecution plugin are run concurrently."""
 
47
        fifo = self.make_path()
 
48
        os.mkfifo(fifo)
 
49
        # If the first process is blocking on a fifo, and the second process
 
50
        # wants to write to the fifo, the only way this will complete is if
 
51
        # run_script is truly async
 
52
        d1 = self.plugin.run_script("/bin/sh", "cat " + fifo)
 
53
        d2 = self.plugin.run_script("/bin/sh", "echo hi > " + fifo)
 
54
        d1.addCallback(self.assertEquals, "hi\n")
 
55
        d2.addCallback(self.assertEquals, "")
 
56
        return gatherResults([d1, d2])
 
57
 
 
58
    def test_accented_run_in_code(self):
 
59
        """
 
60
        Scripts can contain accented data both in the code and in the
 
61
        result.
 
62
        """
 
63
        accented_content = u"\N{LATIN SMALL LETTER E WITH ACUTE}"
 
64
        result = self.plugin.run_script(
 
65
            u"/bin/sh", u"echo %s" % (accented_content,))
 
66
        result.addCallback(
 
67
            self.assertEquals, "%s\n" % (accented_content.encode("utf-8"),))
 
68
        return result
 
69
 
 
70
    def test_accented_run_in_interpreter(self):
 
71
        """
 
72
        Scripts can also contain accents in the interpreter.
 
73
        """
 
74
        accented_content = u"\N{LATIN SMALL LETTER E WITH ACUTE}"
 
75
        result = self.plugin.run_script(
 
76
            u"/bin/echo %s" % (accented_content,), u"")
 
77
        def check(result):
 
78
            self.assertTrue(
 
79
                "%s " % (accented_content.encode("utf-8"),) in result)
 
80
        result.addCallback(check)
 
81
        return result
 
82
 
 
83
    def test_set_umask_appropriately(self):
 
84
        """
 
85
        We should be setting the umask to 0022 before executing a script, and
 
86
        restoring it to the previous value when finishing.
 
87
        """
 
88
        mock_umask = self.mocker.replace("os.umask")
 
89
        mock_umask(0022)
 
90
        self.mocker.result(0077)
 
91
        mock_umask(0077)
 
92
        self.mocker.replay()
 
93
        result = self.plugin.run_script("/bin/sh", "umask")
 
94
        result.addCallback(self.assertEquals, "0022\n")
 
95
        return result
 
96
 
 
97
    def test_restore_umask_in_event_of_error(self):
 
98
        """
 
99
        We set the umask before executing the script, in the event that there's
 
100
        an error setting up the script, we want to restore the umask.
 
101
        """
 
102
        mock_umask = self.mocker.replace("os.umask")
 
103
        mock_umask(0022)
 
104
        self.mocker.result(0077)
 
105
        mock_mkdtemp = self.mocker.replace("tempfile.mkdtemp", passthrough=False)
 
106
        mock_mkdtemp()
 
107
        self.mocker.throw(OSError("Fail!"))
 
108
        mock_umask(0077)
 
109
        self.mocker.replay()
 
110
        self.assertRaises(OSError, self.plugin.run_script, "/bin/sh", "umask",
 
111
                          attachments={u"file1": "some data"})
 
112
 
 
113
    def test_run_with_attachments(self):
 
114
        result = self.plugin.run_script(
 
115
            u"/bin/sh",
 
116
            u"ls $LANDSCAPE_ATTACHMENTS && cat $LANDSCAPE_ATTACHMENTS/file1",
 
117
            attachments={u"file1": "some data"})
 
118
        def check(result):
 
119
            self.assertEquals(result, "file1\nsome data")
 
120
        result.addCallback(check)
 
121
        return result
 
122
 
 
123
    def test_self_remove_script(self):
 
124
        """
 
125
        If a script removes itself, it doesn't create an error when the script
 
126
        execution plugin tries to remove the script file.
 
127
        """
 
128
        result = self.plugin.run_script("/bin/sh", "echo hi && rm $0")
 
129
        result.addCallback(self.assertEquals, "hi\n")
 
130
        return result
 
131
 
 
132
    def test_self_remove_attachments(self):
 
133
        """
 
134
        If a script removes its attachments, it doesn't create an error when
 
135
        the script execution plugin tries to remove the attachments directory.
 
136
        """
 
137
        result = self.plugin.run_script(
 
138
            u"/bin/sh",
 
139
            u"ls $LANDSCAPE_ATTACHMENTS && rm -r $LANDSCAPE_ATTACHMENTS",
 
140
            attachments={u"file1": "some data"})
 
141
        def check(result):
 
142
            self.assertEquals(result, "file1\n")
 
143
        result.addCallback(check)
 
144
        return result
 
145
 
 
146
    def _run_script(self, username, uid, gid, path):
 
147
        # ignore the call to chown!
 
148
        mock_chown = self.mocker.replace("os.chown", passthrough=False)
 
149
        mock_chown(ARGS)
 
150
 
 
151
        factory = StubProcessFactory()
 
152
        self.plugin.process_factory = factory
 
153
 
 
154
        self.mocker.replay()
 
155
 
 
156
        result = self.plugin.run_script("/bin/sh", "echo hi", user=username)
 
157
 
 
158
        self.assertEquals(len(factory.spawns), 1)
 
159
        spawn = factory.spawns[0]
 
160
        self.assertEquals(spawn[4], path)
 
161
        self.assertEquals(spawn[5], uid)
 
162
        self.assertEquals(spawn[6], gid)
 
163
        result.addCallback(self.assertEquals, "foobar")
 
164
 
 
165
        protocol = spawn[0]
 
166
        protocol.childDataReceived(1, "foobar")
 
167
        for fd in (0, 1, 2):
 
168
            protocol.childConnectionLost(fd)
 
169
        protocol.processEnded(Failure(ProcessDone(0)))
 
170
        return result
 
171
 
 
172
    def test_user(self):
 
173
        """
 
174
        Running a script as a particular user calls
 
175
        C{IReactorProcess.spawnProcess} with an appropriate C{uid} argument,
 
176
        with the user's primary group as the C{gid} argument and with the user
 
177
        home as C{path} argument.
 
178
        """
 
179
        uid = os.getuid()
 
180
        info = pwd.getpwuid(uid)
 
181
        username = info.pw_name
 
182
        gid = info.pw_gid
 
183
        path = info.pw_dir
 
184
 
 
185
        return self._run_script(username, uid, gid, path)
 
186
 
 
187
    def test_user_no_home(self):
 
188
        """
 
189
        When the user specified to C{run_script} doesn't have a home, the
 
190
        script executes in '/'.
 
191
        """
 
192
        mock_getpwnam = self.mocker.replace("pwd.getpwnam", passthrough=False)
 
193
        class pwnam(object):
 
194
            pw_uid = 1234
 
195
            pw_gid = 5678
 
196
            pw_dir = self.make_path()
 
197
 
 
198
        self.expect(mock_getpwnam("user")).result(pwnam)
 
199
 
 
200
        return self._run_script("user", 1234, 5678, "/")
 
201
 
 
202
    def test_user_with_attachments(self):
 
203
        uid = os.getuid()
 
204
        info = pwd.getpwuid(uid)
 
205
        username = info.pw_name
 
206
        gid = info.pw_gid
 
207
        path = info.pw_dir
 
208
 
 
209
        mock_chown = self.mocker.replace("os.chown", passthrough=False)
 
210
        mock_chown(ANY, uid, gid)
 
211
        self.mocker.count(3)
 
212
 
 
213
        factory = StubProcessFactory()
 
214
        self.plugin.process_factory = factory
 
215
 
 
216
        self.mocker.replay()
 
217
 
 
218
        result = self.plugin.run_script("/bin/sh", "echo hi", user=username,
 
219
            attachments={u"file 1": "some data"})
 
220
 
 
221
        self.assertEquals(len(factory.spawns), 1)
 
222
        spawn = factory.spawns[0]
 
223
        self.assertEquals(spawn[3].keys(), ["LANDSCAPE_ATTACHMENTS"])
 
224
        attachment_dir = spawn[3]["LANDSCAPE_ATTACHMENTS"]
 
225
        self.assertEquals(stat.S_IMODE(os.stat(attachment_dir).st_mode), 0700)
 
226
        filename = os.path.join(attachment_dir, "file 1")
 
227
        self.assertEquals(stat.S_IMODE(os.stat(filename).st_mode), 0600)
 
228
 
 
229
        protocol = spawn[0]
 
230
        protocol.childDataReceived(1, "foobar")
 
231
        for fd in (0, 1, 2):
 
232
            protocol.childConnectionLost(fd)
 
233
        protocol.processEnded(Failure(ProcessDone(0)))
 
234
        def check(data):
 
235
            self.assertEquals(data, "foobar")
 
236
            self.assertFalse(os.path.exists(attachment_dir))
 
237
        return result.addCallback(check)
 
238
 
 
239
    def test_limit_size(self):
 
240
        """Data returned from the command is limited."""
 
241
        factory = StubProcessFactory()
 
242
        self.plugin.process_factory = factory
 
243
        self.plugin.size_limit = 100
 
244
        result = self.plugin.run_script("/bin/sh", "")
 
245
        result.addCallback(self.assertEquals, "x"*100)
 
246
 
 
247
        protocol = factory.spawns[0][0]
 
248
        protocol.childDataReceived(1, "x"*200)
 
249
        for fd in (0, 1, 2):
 
250
            protocol.childConnectionLost(fd)
 
251
        protocol.processEnded(Failure(ProcessDone(0)))
 
252
 
 
253
        return result
 
254
 
 
255
    def test_limit_time(self):
 
256
        """
 
257
        The process only lasts for a certain number of seconds.
 
258
        """
 
259
        result = self.plugin.run_script("/bin/sh", "cat", time_limit=500)
 
260
        self.manager.reactor.advance(501)
 
261
        self.assertFailure(result, ProcessTimeLimitReachedError)
 
262
        return result
 
263
 
 
264
    def test_limit_time_accumulates_data(self):
 
265
        """
 
266
        Data from processes that time out should still be accumulated and
 
267
        available from the exception object that is raised.
 
268
        """
 
269
        factory = StubProcessFactory()
 
270
        self.plugin.process_factory = factory
 
271
        result = self.plugin.run_script("/bin/sh", "", time_limit=500)
 
272
        protocol = factory.spawns[0][0]
 
273
        protocol.makeConnection(DummyProcess())
 
274
        protocol.childDataReceived(1, "hi\n")
 
275
        self.manager.reactor.advance(501)
 
276
        protocol.processEnded(Failure(ProcessDone(0)))
 
277
        def got_error(f):
 
278
            self.assertTrue(f.check(ProcessTimeLimitReachedError))
 
279
            self.assertEquals(f.value.data, "hi\n")
 
280
        result.addErrback(got_error)
 
281
        return result
 
282
 
 
283
    def test_time_limit_canceled_after_success(self):
 
284
        """
 
285
        The timeout call is cancelled after the script terminates.
 
286
        """
 
287
        factory = StubProcessFactory()
 
288
        self.plugin.process_factory = factory
 
289
        result = self.plugin.run_script("/bin/sh", "", time_limit=500)
 
290
        protocol = factory.spawns[0][0]
 
291
        transport = DummyProcess()
 
292
        protocol.makeConnection(transport)
 
293
        protocol.childDataReceived(1, "hi\n")
 
294
        protocol.processEnded(Failure(ProcessDone(0)))
 
295
        self.manager.reactor.advance(501)
 
296
        self.assertEquals(transport.signals, [])
 
297
 
 
298
    def test_cancel_doesnt_blow_after_success(self):
 
299
        """
 
300
        When the process ends successfully and is immediately followed by the
 
301
        timeout, the output should still be in the failure and nothing bad will
 
302
        happen!
 
303
        [regression test: killing of the already-dead process would blow up.]
 
304
        """
 
305
        factory = StubProcessFactory()
 
306
        self.plugin.process_factory = factory
 
307
        result = self.plugin.run_script("/bin/sh", "", time_limit=500)
 
308
        protocol = factory.spawns[0][0]
 
309
        protocol.makeConnection(DummyProcess())
 
310
        protocol.childDataReceived(1, "hi")
 
311
        protocol.processEnded(Failure(ProcessDone(0)))
 
312
        self.manager.reactor.advance(501)
 
313
        def got_error(f):
 
314
            print f
 
315
            self.assertTrue(f.check(ProcessTimeLimitReachedError))
 
316
            self.assertEquals(f.value.data, "hi\n")
 
317
        result.addErrback(got_error)
 
318
        return result
 
319
 
 
320
    def test_script_is_owned_by_user(self):
 
321
        """
 
322
        This is a very white-box test. When a script is generated, it must be
 
323
        created such that data NEVER gets into it before the file has the
 
324
        correct permissions. Therefore os.chmod and os.chown must be called
 
325
        before data is written.
 
326
        """
 
327
        uid = os.getuid()
 
328
        gid = os.getgid()
 
329
 
 
330
        mock_chown = self.mocker.replace("os.chown", passthrough=False)
 
331
        mock_chmod = self.mocker.replace("os.chmod", passthrough=False)
 
332
        mock_mkstemp = self.mocker.replace("tempfile.mkstemp",
 
333
                                           passthrough=False)
 
334
        mock_fdopen = self.mocker.replace("os.fdopen", passthrough=False)
 
335
        process_factory = self.mocker.mock()
 
336
        self.plugin.process_factory = process_factory
 
337
 
 
338
        self.mocker.order()
 
339
 
 
340
        self.expect(mock_mkstemp()).result((99, "tempo!"))
 
341
 
 
342
        script_file = mock_fdopen(99, "w")
 
343
        mock_chmod("tempo!", 0700)
 
344
        mock_chown("tempo!", uid, gid)
 
345
        # The contents are written *after* the permissions have been set up!
 
346
        script_file.write("#!/bin/sh\ncode")
 
347
        script_file.close()
 
348
 
 
349
        process_factory.spawnProcess(
 
350
            ANY, ANY, uid=uid, gid=gid, path=ANY, env={})
 
351
 
 
352
        self.mocker.replay()
 
353
 
 
354
        # We don't really care about the deferred that's returned, as long as
 
355
        # those things happened in the correct order.
 
356
        self.plugin.run_script("/bin/sh", "code",
 
357
                               user=pwd.getpwuid(uid)[0])
 
358
 
 
359
    def test_script_removed(self):
 
360
        """
 
361
        The script is removed after it is finished.
 
362
        """
 
363
        mock_mkstemp = self.mocker.replace("tempfile.mkstemp",
 
364
                                           passthrough=False)
 
365
        fd, filename = tempfile.mkstemp()
 
366
        self.expect(mock_mkstemp()).result((fd, filename))
 
367
        self.mocker.replay()
 
368
        d = self.plugin.run_script("/bin/sh", "true")
 
369
        d.addCallback(lambda ign: self.assertFalse(os.path.exists(filename)))
 
370
        return d
 
371
 
 
372
    def test_unknown_interpreter(self):
 
373
        """
 
374
        If the script is run with an unknown interpreter, it raises a
 
375
        meaningful error instead of crashing in execvpe.
 
376
        """
 
377
        d = self.plugin.run_script("/bin/cantpossiblyexist", "stuff")
 
378
        def cb(ignore):
 
379
            self.fail("Should not be there")
 
380
        def eb(failure):
 
381
            failure.trap(ProcessFailedError)
 
382
            self.assertEquals(
 
383
                failure.value.data,
 
384
                "Unknown interpreter: '/bin/cantpossiblyexist'")
 
385
        return d.addCallback(cb).addErrback(eb)
 
386
 
 
387
 
 
388
class ScriptExecutionMessageTests(LandscapeIsolatedTest):
 
389
    helpers = [ManagerHelper]
 
390
 
 
391
    def setUp(self):
 
392
        super(ScriptExecutionMessageTests, self).setUp()
 
393
        self.broker_service.message_store.set_accepted_types(
 
394
            ["operation-result"])
 
395
        self.manager.config.script_users = "ALL"
 
396
 
 
397
    def _verify_script(self, executable, interp, code):
 
398
        """
 
399
        Given spawnProcess arguments, check to make sure that the temporary
 
400
        script has the correct content.
 
401
        """
 
402
        data = open(executable, "r").read()
 
403
        self.assertEquals(data, "#!%s\n%s" % (interp, code))
 
404
 
 
405
 
 
406
    def _send_script(self, interpreter, code, operation_id=123,
 
407
                     user=pwd.getpwuid(os.getuid())[0],
 
408
                     time_limit=None):
 
409
        return self.manager.dispatch_message(
 
410
            {"type": "execute-script",
 
411
             "interpreter": interpreter,
 
412
             "code": code,
 
413
             "operation-id": operation_id,
 
414
             "username": user,
 
415
             "time-limit": time_limit,
 
416
             "attachments": {}})
 
417
 
 
418
    def test_success(self):
 
419
        """
 
420
        When a C{execute-script} message is received from the server, the
 
421
        specified script will be run and an operation-result will be sent back
 
422
        to the server.
 
423
        """
 
424
        # Let's use a stub process factory, because otherwise we don't have
 
425
        # access to the deferred.
 
426
        factory = StubProcessFactory()
 
427
 
 
428
        # ignore the call to chown!
 
429
        mock_chown = self.mocker.replace("os.chown", passthrough=False)
 
430
        mock_chown(ARGS)
 
431
 
 
432
        self.manager.add(ScriptExecution(process_factory=factory))
 
433
 
 
434
        self.mocker.replay()
 
435
        result = self._send_script(sys.executable, "print 'hi'")
 
436
 
 
437
        self._verify_script(factory.spawns[0][1], sys.executable, "print 'hi'")
 
438
        self.assertMessages(
 
439
            self.broker_service.message_store.get_pending_messages(), [])
 
440
 
 
441
        # Now let's simulate the completion of the process
 
442
        factory.spawns[0][0].childDataReceived(1, "hi!\n")
 
443
        factory.spawns[0][0].processEnded(Failure(ProcessDone(0)))
 
444
 
 
445
        def got_result(r):
 
446
            self.assertMessages(
 
447
                self.broker_service.message_store.get_pending_messages(),
 
448
                [{"type": "operation-result",
 
449
                  "operation-id": 123,
 
450
                  "status": SUCCEEDED,
 
451
                  "result-text": u"hi!\n"}])
 
452
        result.addCallback(got_result)
 
453
        return result
 
454
 
 
455
    def test_user(self):
 
456
        """A user can be specified in the message."""
 
457
        uid = os.getuid()
 
458
        gid = os.getgid()
 
459
        username = pwd.getpwuid(uid)[0]
 
460
 
 
461
        # ignore the call to chown!
 
462
        mock_chown = self.mocker.replace("os.chown", passthrough=False)
 
463
        mock_chown(ARGS)
 
464
 
 
465
        def spawn_called(protocol, filename, uid, gid, path, env):
 
466
            protocol.childDataReceived(1, "hi!\n")
 
467
            protocol.processEnded(Failure(ProcessDone(0)))
 
468
            self._verify_script(filename, sys.executable, "print 'hi'")
 
469
 
 
470
        process_factory = self.mocker.mock()
 
471
        process_factory.spawnProcess(
 
472
            ANY, ANY, uid=uid, gid=gid, path=ANY, env={})
 
473
        self.mocker.call(spawn_called)
 
474
 
 
475
        self.mocker.replay()
 
476
 
 
477
        self.manager.add(ScriptExecution(process_factory=process_factory))
 
478
 
 
479
        result = self._send_script(sys.executable, "print 'hi'", user=username)
 
480
        return result
 
481
 
 
482
    def test_timeout(self):
 
483
        """
 
484
        If a L{ProcessTimeLimitReachedError} is fired back, the
 
485
        operation-result should have a failed status.
 
486
        """
 
487
        factory = StubProcessFactory()
 
488
        self.manager.add(ScriptExecution(process_factory=factory))
 
489
 
 
490
        # ignore the call to chown!
 
491
        mock_chown = self.mocker.replace("os.chown", passthrough=False)
 
492
        mock_chown(ARGS)
 
493
 
 
494
        self.mocker.replay()
 
495
        result = self._send_script(sys.executable, "bar", time_limit=30)
 
496
        self._verify_script(factory.spawns[0][1], sys.executable, "bar")
 
497
 
 
498
        protocol = factory.spawns[0][0]
 
499
        protocol.makeConnection(DummyProcess())
 
500
        protocol.childDataReceived(2, "ONOEZ")
 
501
        self.manager.reactor.advance(31)
 
502
        protocol.processEnded(Failure(ProcessDone(0)))
 
503
 
 
504
        def got_result(r):
 
505
            self.assertMessages(
 
506
                self.broker_service.message_store.get_pending_messages(),
 
507
                [{"type": "operation-result",
 
508
                  "operation-id": 123,
 
509
                  "status": FAILED,
 
510
                  "result-text": u"ONOEZ",
 
511
                  "result-code": 102}])
 
512
        result.addCallback(got_result)
 
513
        return result
 
514
 
 
515
    def test_configured_users(self):
 
516
        """
 
517
        Messages which try to run a script as a user that is not allowed should
 
518
        be rejected.
 
519
        """
 
520
        self.manager.add(ScriptExecution())
 
521
        self.manager.config.script_users = "landscape, nobody"
 
522
        result = self._send_script(sys.executable, "bar", user="whatever")
 
523
        def got_result(r):
 
524
            self.assertMessages(
 
525
                self.broker_service.message_store.get_pending_messages(),
 
526
                [{"type": "operation-result",
 
527
                  "operation-id": 123,
 
528
                  "status": FAILED,
 
529
                  "result-text": u"Scripts cannot be run as user whatever."}])
 
530
        result.addCallback(got_result)
 
531
        return result
 
532
 
 
533
    def test_urgent_response(self):
 
534
        """Responses to script execution messages are urgent."""
 
535
        uid = os.getuid()
 
536
        gid = os.getgid()
 
537
        username = pwd.getpwuid(uid)[0]
 
538
 
 
539
        # ignore the call to chown!
 
540
        mock_chown = self.mocker.replace("os.chown", passthrough=False)
 
541
        mock_chown(ARGS)
 
542
 
 
543
        def spawn_called(protocol, filename, uid, gid, path, env):
 
544
            protocol.childDataReceived(1, "hi!\n")
 
545
            protocol.processEnded(Failure(ProcessDone(0)))
 
546
            self._verify_script(filename, sys.executable, "print 'hi'")
 
547
 
 
548
        process_factory = self.mocker.mock()
 
549
        process_factory.spawnProcess(
 
550
            ANY, ANY, uid=uid, gid=gid, path=ANY, env={})
 
551
        self.mocker.call(spawn_called)
 
552
 
 
553
        self.mocker.replay()
 
554
 
 
555
        self.manager.add(ScriptExecution(process_factory=process_factory))
 
556
 
 
557
        def got_result(r):
 
558
            self.assertTrue(self.broker_service.exchanger.is_urgent())
 
559
            self.assertMessages(
 
560
                self.broker_service.message_store.get_pending_messages(),
 
561
                [{"type": "operation-result",
 
562
                  "operation-id": 123,
 
563
                  "result-text": u"hi!\n",
 
564
                  "status": SUCCEEDED}])
 
565
 
 
566
        result = self._send_script(sys.executable, "print 'hi'")
 
567
        result.addCallback(got_result)
 
568
        return result
 
569
 
 
570
    def test_parse_error_causes_operation_failure(self):
 
571
        """
 
572
        If there is an error parsing the message, an operation-result will be
 
573
        sent (assuming operation-id *is* successfully parsed).
 
574
        """
 
575
        self.log_helper.ignore_errors(KeyError)
 
576
        self.manager.add(ScriptExecution())
 
577
 
 
578
        self.manager.dispatch_message(
 
579
            {"type": "execute-script", "operation-id": 444})
 
580
 
 
581
        self.assertMessages(
 
582
            self.broker_service.message_store.get_pending_messages(),
 
583
            [{"type": "operation-result",
 
584
              "operation-id": 444,
 
585
              "result-text": u"KeyError: 'username'",
 
586
              "status": FAILED}])
 
587
 
 
588
        self.assertTrue("KeyError: 'username'" in self.logfile.getvalue())
 
589
 
 
590
    def test_non_zero_exit_fails_operation(self):
 
591
        """
 
592
        If a script exits with a nen-zero exit code, the operation associated
 
593
        with it should fail, but the data collected should still be sent.
 
594
        """
 
595
        # Mock a bunch of crap so that we can run a real process
 
596
        self.mocker.replace("os.chown", passthrough=False)(ARGS)
 
597
        self.mocker.replace("os.setuid", passthrough=False)(ARGS)
 
598
        self.mocker.count(0, None)
 
599
        self.mocker.replace("os.setgid", passthrough=False)(ARGS)
 
600
        self.mocker.count(0, None)
 
601
        self.mocker.replay()
 
602
 
 
603
        self.manager.add(ScriptExecution())
 
604
        result = self._send_script("/bin/sh", "echo hi; exit 1")
 
605
 
 
606
        def got_result(ignored):
 
607
            self.assertMessages(
 
608
                self.broker_service.message_store.get_pending_messages(),
 
609
                [{"type": "operation-result",
 
610
                  "operation-id": 123,
 
611
                  "result-text": "hi\n",
 
612
                  "result-code": PROCESS_FAILED_RESULT,
 
613
                  "status": FAILED}])
 
614
        return result.addCallback(got_result)
 
615
 
 
616
    def test_unknown_error(self):
 
617
        """
 
618
        When a completely unknown error comes back from the process protocol,
 
619
        the operation fails and the formatted failure is included in the
 
620
        response message.
 
621
        """
 
622
        factory = StubProcessFactory()
 
623
 
 
624
        # ignore the call to chown!
 
625
        mock_chown = self.mocker.replace("os.chown", passthrough=False)
 
626
        mock_chown(ARGS)
 
627
 
 
628
        self.manager.add(ScriptExecution(process_factory=factory))
 
629
 
 
630
        self.mocker.replay()
 
631
        result = self._send_script(sys.executable, "print 'hi'")
 
632
 
 
633
        self._verify_script(factory.spawns[0][1], sys.executable, "print 'hi'")
 
634
        self.assertMessages(
 
635
            self.broker_service.message_store.get_pending_messages(), [])
 
636
 
 
637
        failure = Failure(RuntimeError("Oh noes!"))
 
638
        factory.spawns[0][0].result_deferred.errback(failure)
 
639
 
 
640
        def got_result(r):
 
641
            self.assertMessages(
 
642
                self.broker_service.message_store.get_pending_messages(),
 
643
                [{"type": "operation-result",
 
644
                  "operation-id": 123,
 
645
                  "status": FAILED,
 
646
                  "result-text": str(failure)}])
 
647
        result.addCallback(got_result)
 
648
        return result