~ubuntu-branches/ubuntu/lucid/landscape-client/lucid-updates

« back to all changes in this revision

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

  • Committer: Package Import Robot
  • Author(s): Andreas Hasenack
  • Date: 2012-04-10 14:28:48 UTC
  • mfrom: (1.1.27)
  • mto: This revision was merged to the branch mainline in revision 35.
  • Revision ID: package-import@ubuntu.com-20120410142848-7xsy4g2xii7y7ntc
ImportĀ upstreamĀ versionĀ 12.04.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
4
4
import tempfile
5
5
import stat
6
6
 
7
 
from twisted.internet.defer import gatherResults
 
7
from twisted.internet.defer import gatherResults, succeed, fail
8
8
from twisted.internet.error import ProcessDone
9
9
from twisted.python.failure import Failure
10
10
 
 
11
from landscape import VERSION
 
12
from landscape.lib.fetch import HTTPCodeError
 
13
from landscape.lib.persist import Persist
11
14
from landscape.manager.scriptexecution import (
12
15
    ScriptExecutionPlugin, ProcessTimeLimitReachedError, PROCESS_FAILED_RESULT,
13
 
    UBUNTU_PATH, get_user_info, UnknownInterpreterError, UnknownUserError)
 
16
    UBUNTU_PATH, get_user_info, UnknownInterpreterError, UnknownUserError,
 
17
    FETCH_ATTACHMENTS_FAILED_RESULT)
14
18
from landscape.manager.manager import SUCCEEDED, FAILED
15
19
from landscape.tests.helpers import (
16
20
    LandscapeTest, ManagerHelper, StubProcessFactory, DummyProcess)
61
65
            "import os\nprint os.environ")
62
66
 
63
67
        def check_environment(results):
64
 
            for string in get_default_environment().keys():
65
 
                self.assertIn(string, results)
 
68
            for string in get_default_environment():
 
69
                self.assertIn(string, results)
 
70
 
 
71
        result.addCallback(check_environment)
 
72
        return result
 
73
 
 
74
    def test_server_supplied_env(self):
 
75
        """
 
76
        Server-supplied environment variables are merged with default
 
77
        variables then passed to script.
 
78
        """
 
79
        server_supplied_env = {"DOG": "Woof", "CAT": "Meow"}
 
80
        result = self.plugin.run_script(
 
81
            sys.executable,
 
82
            "import os\nprint os.environ",
 
83
            server_supplied_env=server_supplied_env)
 
84
 
 
85
        def check_environment(results):
 
86
            for string in get_default_environment():
 
87
                self.assertIn(string, results)
 
88
            for name, value in server_supplied_env.items():
 
89
                self.assertIn(name, results)
 
90
                self.assertIn(value, results)
 
91
 
 
92
        result.addCallback(check_environment)
 
93
        return result
 
94
 
 
95
    def test_server_supplied_env_overrides_client(self):
 
96
        """
 
97
        Server-supplied environment variables override client default
 
98
        values if the server provides them.
 
99
        """
 
100
        server_supplied_env = {"PATH": "server-path", "USER": "server-user",
 
101
                               "HOME": "server-home"}
 
102
        result = self.plugin.run_script(
 
103
            sys.executable,
 
104
            "import os\nprint os.environ",
 
105
            server_supplied_env=server_supplied_env)
 
106
 
 
107
        def check_environment(results):
 
108
            for name, value in server_supplied_env.items():
 
109
                self.assertIn(name, results)
 
110
                self.assertIn(value, results)
 
111
 
66
112
        result.addCallback(check_environment)
67
113
        return result
68
114
 
105
151
        def check(result):
106
152
            self.assertTrue(
107
153
                "%s " % (accented_content.encode("utf-8"),) in result)
 
154
 
108
155
        result.addCallback(check)
109
156
        return result
110
157
 
138
185
        self.mocker.replay()
139
186
        result = self.plugin.run_script("/bin/sh", "umask",
140
187
                                        attachments={u"file1": "some data"})
141
 
        self.assertFailure(result, OSError)
 
188
        return self.assertFailure(result, OSError)
142
189
 
143
190
    def test_run_with_attachments(self):
144
191
        result = self.plugin.run_script(
148
195
 
149
196
        def check(result):
150
197
            self.assertEqual(result, "file1\nsome data")
 
198
 
 
199
        result.addCallback(check)
 
200
        return result
 
201
 
 
202
    def test_run_with_attachment_ids(self):
 
203
        """
 
204
        The most recent protocol for script message doesn't include the
 
205
        attachment body inside the message itself, but instead gives an
 
206
        attachment ID, and the plugin fetches the files separately.
 
207
        """
 
208
        self.manager.config.url = "https://localhost/message-system"
 
209
        persist = Persist(
 
210
            filename=os.path.join(self.config.data_path, "broker.bpickle"))
 
211
        registration_persist = persist.root_at("registration")
 
212
        registration_persist.set("secure-id", "secure_id")
 
213
        persist.save()
 
214
        mock_fetch = self.mocker.replace("landscape.lib.fetch.fetch_async",
 
215
                                         passthrough=False)
 
216
        headers = {"User-Agent": "landscape-client/%s" % VERSION,
 
217
                   "Content-Type": "application/octet-stream",
 
218
                   "X-Computer-ID": "secure_id"}
 
219
        mock_fetch("https://localhost/attachment/14", headers=headers,
 
220
                   cainfo=None)
 
221
        self.mocker.result(succeed("some other data"))
 
222
        self.mocker.replay()
 
223
 
 
224
        result = self.plugin.run_script(
 
225
            u"/bin/sh",
 
226
            u"ls $LANDSCAPE_ATTACHMENTS && cat $LANDSCAPE_ATTACHMENTS/file1",
 
227
            attachments={u"file1": 14})
 
228
 
 
229
        def check(result):
 
230
            self.assertEqual(result, "file1\nsome other data")
 
231
 
 
232
        result.addCallback(check)
 
233
        return result
 
234
 
 
235
    def test_run_with_attachment_ids_and_ssl(self):
 
236
        """
 
237
        When fetching attachments, L{ScriptExecution} passes the optional ssl
 
238
        certificate file if the configuration specifies it.
 
239
        """
 
240
        self.manager.config.url = "https://localhost/message-system"
 
241
        self.manager.config.ssl_public_key = "/some/key"
 
242
        persist = Persist(
 
243
            filename=os.path.join(self.config.data_path, "broker.bpickle"))
 
244
        registration_persist = persist.root_at("registration")
 
245
        registration_persist.set("secure-id", "secure_id")
 
246
        persist.save()
 
247
        mock_fetch = self.mocker.replace("landscape.lib.fetch.fetch_async",
 
248
                                         passthrough=False)
 
249
        headers = {"User-Agent": "landscape-client/%s" % VERSION,
 
250
                   "Content-Type": "application/octet-stream",
 
251
                   "X-Computer-ID": "secure_id"}
 
252
        mock_fetch("https://localhost/attachment/14", headers=headers,
 
253
                   cainfo="/some/key")
 
254
        self.mocker.result(succeed("some other data"))
 
255
        self.mocker.replay()
 
256
 
 
257
        result = self.plugin.run_script(
 
258
            u"/bin/sh",
 
259
            u"ls $LANDSCAPE_ATTACHMENTS && cat $LANDSCAPE_ATTACHMENTS/file1",
 
260
            attachments={u"file1": 14})
 
261
 
 
262
        def check(result):
 
263
            self.assertEqual(result, "file1\nsome other data")
 
264
 
151
265
        result.addCallback(check)
152
266
        return result
153
267
 
172
286
 
173
287
        def check(result):
174
288
            self.assertEqual(result, "file1\n")
 
289
 
175
290
        result.addCallback(check)
176
291
        return result
177
292
 
180
295
        mock_chown = self.mocker.replace("os.chown", passthrough=False)
181
296
        mock_chown(ARGS)
182
297
 
 
298
        expected_uid = uid if uid != os.getuid() else None
 
299
        expected_gid = gid if gid != os.getgid() else None
 
300
 
183
301
        factory = StubProcessFactory()
184
302
        self.plugin.process_factory = factory
185
303
 
190
308
        self.assertEqual(len(factory.spawns), 1)
191
309
        spawn = factory.spawns[0]
192
310
        self.assertEqual(spawn[4], path)
193
 
        self.assertEqual(spawn[5], uid)
194
 
        self.assertEqual(spawn[6], gid)
 
311
        self.assertEqual(spawn[5], expected_uid)
 
312
        self.assertEqual(spawn[6], expected_gid)
195
313
        result.addCallback(self.assertEqual, "foobar")
196
314
 
197
315
        protocol = spawn[0]
252
370
 
253
371
        self.assertEqual(len(factory.spawns), 1)
254
372
        spawn = factory.spawns[0]
255
 
        self.assertIn("LANDSCAPE_ATTACHMENTS", spawn[3].keys())
 
373
        self.assertIn("LANDSCAPE_ATTACHMENTS", spawn[3])
256
374
        attachment_dir = spawn[3]["LANDSCAPE_ATTACHMENTS"]
257
375
        self.assertEqual(stat.S_IMODE(os.stat(attachment_dir).st_mode), 0700)
258
376
        filename = os.path.join(attachment_dir, "file 1")
267
385
        def check(data):
268
386
            self.assertEqual(data, "foobar")
269
387
            self.assertFalse(os.path.exists(attachment_dir))
 
388
 
270
389
        return result.addCallback(check)
271
390
 
272
391
    def test_limit_size(self):
311
430
        def got_error(f):
312
431
            self.assertTrue(f.check(ProcessTimeLimitReachedError))
313
432
            self.assertEqual(f.value.data, "hi\n")
 
433
 
314
434
        result.addErrback(got_error)
315
435
        return result
316
436
 
347
467
 
348
468
        def got_result(output):
349
469
            self.assertEqual(output, "hi")
 
470
 
350
471
        result.addCallback(got_result)
351
472
        return result
352
473
 
379
500
        script_file.write("#!/bin/sh\ncode")
380
501
        script_file.close()
381
502
        process_factory.spawnProcess(
382
 
            ANY, ANY, uid=uid, gid=gid, path=ANY,
 
503
            ANY, ANY, uid=None, gid=None, path=ANY,
383
504
            env=get_default_environment())
384
505
        self.mocker.replay()
385
506
        # We don't really care about the deferred that's returned, as long as
437
558
 
438
559
    def _send_script(self, interpreter, code, operation_id=123,
439
560
                     user=pwd.getpwuid(os.getuid())[0],
440
 
                     time_limit=None):
441
 
        return self.manager.dispatch_message(
442
 
            {"type": "execute-script",
443
 
             "interpreter": interpreter,
444
 
             "code": code,
445
 
             "operation-id": operation_id,
446
 
             "username": user,
447
 
             "time-limit": time_limit,
448
 
             "attachments": {}})
 
561
                     time_limit=None, attachments={},
 
562
                     server_supplied_env=None):
 
563
        message = {"type": "execute-script",
 
564
                   "interpreter": interpreter,
 
565
                   "code": code,
 
566
                   "operation-id": operation_id,
 
567
                   "username": user,
 
568
                   "time-limit": time_limit,
 
569
                   "attachments": dict(attachments)}
 
570
        if server_supplied_env:
 
571
            message["env"] = server_supplied_env
 
572
        return self.manager.dispatch_message(message)
449
573
 
450
574
    def test_success(self):
451
575
        """
481
605
                  "operation-id": 123,
482
606
                  "status": SUCCEEDED,
483
607
                  "result-text": u"hi!\n"}])
 
608
 
 
609
        result.addCallback(got_result)
 
610
        return result
 
611
 
 
612
    def test_success_with_server_supplied_env(self):
 
613
        """
 
614
        When a C{execute-script} message is received from the server, the
 
615
        specified script will be run with the supplied environment and an
 
616
        operation-result will be sent back to the server.
 
617
        """
 
618
        # Let's use a stub process factory, because otherwise we don't have
 
619
        # access to the deferred.
 
620
        factory = StubProcessFactory()
 
621
 
 
622
        # ignore the call to chown!
 
623
        mock_chown = self.mocker.replace("os.chown", passthrough=False)
 
624
        mock_chown(ARGS)
 
625
 
 
626
        self.manager.add(ScriptExecutionPlugin(process_factory=factory))
 
627
 
 
628
        self.mocker.replay()
 
629
        result = self._send_script(sys.executable, "print 'hi'",
 
630
                                   server_supplied_env={"Dog": "Woof"})
 
631
 
 
632
        self._verify_script(factory.spawns[0][1], sys.executable, "print 'hi'")
 
633
        # Verify environment was passed
 
634
        self.assertIn("HOME", factory.spawns[0][3])
 
635
        self.assertIn("USER", factory.spawns[0][3])
 
636
        self.assertIn("PATH", factory.spawns[0][3])
 
637
        self.assertIn("Dog", factory.spawns[0][3])
 
638
        self.assertEqual("Woof", factory.spawns[0][3]["Dog"])
 
639
 
 
640
        self.assertMessages(
 
641
            self.broker_service.message_store.get_pending_messages(), [])
 
642
 
 
643
        # Now let's simulate the completion of the process
 
644
        factory.spawns[0][0].childDataReceived(1, "Woof\n")
 
645
        factory.spawns[0][0].processEnded(Failure(ProcessDone(0)))
 
646
 
 
647
        def got_result(r):
 
648
            self.assertMessages(
 
649
                self.broker_service.message_store.get_pending_messages(),
 
650
                [{"type": "operation-result",
 
651
                  "operation-id": 123,
 
652
                  "status": SUCCEEDED,
 
653
                  "result-text": u"Woof\n"}])
 
654
 
484
655
        result.addCallback(got_result)
485
656
        return result
486
657
 
497
668
            protocol.childDataReceived(1, "hi!\n")
498
669
            protocol.processEnded(Failure(ProcessDone(0)))
499
670
            self._verify_script(filename, sys.executable, "print 'hi'")
 
671
 
500
672
        process_factory = self.mocker.mock()
501
673
        process_factory.spawnProcess(
502
 
            ANY, ANY, uid=uid, gid=gid, path=ANY,
 
674
            ANY, ANY, uid=None, gid=None, path=ANY,
503
675
            env=get_default_environment())
504
676
        self.mocker.call(spawn_called)
505
677
        self.mocker.replay()
562
734
                  "status": FAILED,
563
735
                  "result-text": u"ONOEZ",
564
736
                  "result-code": 102}])
 
737
 
565
738
        result.addCallback(got_result)
566
739
        return result
567
740
 
581
754
                  "operation-id": 123,
582
755
                  "status": FAILED,
583
756
                  "result-text": u"Scripts cannot be run as user whatever."}])
 
757
 
584
758
        result.addCallback(got_result)
585
759
        return result
586
760
 
587
761
    def test_urgent_response(self):
588
762
        """Responses to script execution messages are urgent."""
589
 
        username = pwd.getpwuid(os.getuid())[0]
590
 
        uid, gid, home = get_user_info(username)
591
 
 
592
763
        # ignore the call to chown!
593
764
        mock_chown = self.mocker.replace("os.chown", passthrough=False)
594
765
        mock_chown(ARGS)
597
768
            protocol.childDataReceived(1, "hi!\n")
598
769
            protocol.processEnded(Failure(ProcessDone(0)))
599
770
            self._verify_script(filename, sys.executable, "print 'hi'")
 
771
 
600
772
        process_factory = self.mocker.mock()
601
773
        process_factory.spawnProcess(
602
 
            ANY, ANY, uid=uid, gid=gid, path=ANY,
 
774
            ANY, ANY, uid=None, gid=None, path=ANY,
603
775
            env=get_default_environment())
604
776
        self.mocker.call(spawn_called)
605
777
 
626
798
        If a script outputs non-printable characters not handled by utf-8, they
627
799
        are replaced during the encoding phase but the script succeeds.
628
800
        """
629
 
        username = pwd.getpwuid(os.getuid())[0]
630
 
        uid, gid, home = get_user_info(username)
631
 
 
632
801
        mock_chown = self.mocker.replace("os.chown", passthrough=False)
633
802
        mock_chown(ARGS)
634
803
 
637
806
            "\x7fELF\x01\x01\x01\x00\x00\x00\x95\x01")
638
807
            protocol.processEnded(Failure(ProcessDone(0)))
639
808
            self._verify_script(filename, sys.executable, "print 'hi'")
 
809
 
640
810
        process_factory = self.mocker.mock()
641
811
        process_factory.spawnProcess(
642
 
            ANY, ANY, uid=uid, gid=gid, path=ANY,
 
812
            ANY, ANY, uid=None, gid=None, path=ANY,
643
813
            env=get_default_environment())
644
814
        self.mocker.call(spawn_called)
645
815
 
709
879
                  "result-text": "hi\n",
710
880
                  "result-code": PROCESS_FAILED_RESULT,
711
881
                  "status": FAILED}])
 
882
 
712
883
        return result.addCallback(got_result)
713
884
 
714
885
    def test_unknown_error(self):
742
913
                  "operation-id": 123,
743
914
                  "status": FAILED,
744
915
                  "result-text": str(failure)}])
 
916
 
745
917
        result.addCallback(got_result)
746
918
        return result
 
919
 
 
920
    def test_fetch_attachment_failure(self):
 
921
        """
 
922
        If the plugin fails to retrieve the attachments with a
 
923
        L{HTTPCodeError}, a specific error code is shown.
 
924
        """
 
925
        self.manager.config.url = "https://localhost/message-system"
 
926
        persist = Persist(
 
927
            filename=os.path.join(self.config.data_path, "broker.bpickle"))
 
928
        registration_persist = persist.root_at("registration")
 
929
        registration_persist.set("secure-id", "secure_id")
 
930
        persist.save()
 
931
        mock_fetch = self.mocker.replace("landscape.lib.fetch.fetch_async",
 
932
                                         passthrough=False)
 
933
        headers = {"User-Agent": "landscape-client/%s" % VERSION,
 
934
                   "Content-Type": "application/octet-stream",
 
935
                   "X-Computer-ID": "secure_id"}
 
936
        mock_fetch("https://localhost/attachment/14", headers=headers,
 
937
                   cainfo=None)
 
938
        self.mocker.result(fail(HTTPCodeError(404, "Not found")))
 
939
        self.mocker.replay()
 
940
 
 
941
        self.manager.add(ScriptExecutionPlugin())
 
942
        result = self._send_script(
 
943
            "/bin/sh", "echo hi", attachments={u"file1": 14})
 
944
 
 
945
        def got_result(ignored):
 
946
            self.assertMessages(
 
947
                self.broker_service.message_store.get_pending_messages(),
 
948
                [{"type": "operation-result",
 
949
                  "operation-id": 123,
 
950
                  "result-text": "Server returned HTTP code 404",
 
951
                  "result-code": FETCH_ATTACHMENTS_FAILED_RESULT,
 
952
                  "status": FAILED}])
 
953
 
 
954
        return result.addCallback(got_result)