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
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")
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)
71
result.addCallback(check_environment)
74
def test_server_supplied_env(self):
76
Server-supplied environment variables are merged with default
77
variables then passed to script.
79
server_supplied_env = {"DOG": "Woof", "CAT": "Meow"}
80
result = self.plugin.run_script(
82
"import os\nprint os.environ",
83
server_supplied_env=server_supplied_env)
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)
92
result.addCallback(check_environment)
95
def test_server_supplied_env_overrides_client(self):
97
Server-supplied environment variables override client default
98
values if the server provides them.
100
server_supplied_env = {"PATH": "server-path", "USER": "server-user",
101
"HOME": "server-home"}
102
result = self.plugin.run_script(
104
"import os\nprint os.environ",
105
server_supplied_env=server_supplied_env)
107
def check_environment(results):
108
for name, value in server_supplied_env.items():
109
self.assertIn(name, results)
110
self.assertIn(value, results)
66
112
result.addCallback(check_environment)
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)
143
190
def test_run_with_attachments(self):
144
191
result = self.plugin.run_script(
149
196
def check(result):
150
197
self.assertEqual(result, "file1\nsome data")
199
result.addCallback(check)
202
def test_run_with_attachment_ids(self):
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.
208
self.manager.config.url = "https://localhost/message-system"
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")
214
mock_fetch = self.mocker.replace("landscape.lib.fetch.fetch_async",
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,
221
self.mocker.result(succeed("some other data"))
224
result = self.plugin.run_script(
226
u"ls $LANDSCAPE_ATTACHMENTS && cat $LANDSCAPE_ATTACHMENTS/file1",
227
attachments={u"file1": 14})
230
self.assertEqual(result, "file1\nsome other data")
232
result.addCallback(check)
235
def test_run_with_attachment_ids_and_ssl(self):
237
When fetching attachments, L{ScriptExecution} passes the optional ssl
238
certificate file if the configuration specifies it.
240
self.manager.config.url = "https://localhost/message-system"
241
self.manager.config.ssl_public_key = "/some/key"
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")
247
mock_fetch = self.mocker.replace("landscape.lib.fetch.fetch_async",
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,
254
self.mocker.result(succeed("some other data"))
257
result = self.plugin.run_script(
259
u"ls $LANDSCAPE_ATTACHMENTS && cat $LANDSCAPE_ATTACHMENTS/file1",
260
attachments={u"file1": 14})
263
self.assertEqual(result, "file1\nsome other data")
151
265
result.addCallback(check)
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")
197
315
protocol = spawn[0]
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")
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
438
559
def _send_script(self, interpreter, code, operation_id=123,
439
560
user=pwd.getpwuid(os.getuid())[0],
441
return self.manager.dispatch_message(
442
{"type": "execute-script",
443
"interpreter": interpreter,
445
"operation-id": operation_id,
447
"time-limit": time_limit,
561
time_limit=None, attachments={},
562
server_supplied_env=None):
563
message = {"type": "execute-script",
564
"interpreter": interpreter,
566
"operation-id": operation_id,
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)
450
574
def test_success(self):
481
605
"operation-id": 123,
482
606
"status": SUCCEEDED,
483
607
"result-text": u"hi!\n"}])
609
result.addCallback(got_result)
612
def test_success_with_server_supplied_env(self):
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.
618
# Let's use a stub process factory, because otherwise we don't have
619
# access to the deferred.
620
factory = StubProcessFactory()
622
# ignore the call to chown!
623
mock_chown = self.mocker.replace("os.chown", passthrough=False)
626
self.manager.add(ScriptExecutionPlugin(process_factory=factory))
629
result = self._send_script(sys.executable, "print 'hi'",
630
server_supplied_env={"Dog": "Woof"})
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"])
641
self.broker_service.message_store.get_pending_messages(), [])
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)))
649
self.broker_service.message_store.get_pending_messages(),
650
[{"type": "operation-result",
653
"result-text": u"Woof\n"}])
484
655
result.addCallback(got_result)
497
668
protocol.childDataReceived(1, "hi!\n")
498
669
protocol.processEnded(Failure(ProcessDone(0)))
499
670
self._verify_script(filename, sys.executable, "print 'hi'")
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()
581
754
"operation-id": 123,
582
755
"status": FAILED,
583
756
"result-text": u"Scripts cannot be run as user whatever."}])
584
758
result.addCallback(got_result)
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)
592
763
# ignore the call to chown!
593
764
mock_chown = self.mocker.replace("os.chown", passthrough=False)
597
768
protocol.childDataReceived(1, "hi!\n")
598
769
protocol.processEnded(Failure(ProcessDone(0)))
599
770
self._verify_script(filename, sys.executable, "print 'hi'")
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)
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.
629
username = pwd.getpwuid(os.getuid())[0]
630
uid, gid, home = get_user_info(username)
632
801
mock_chown = self.mocker.replace("os.chown", passthrough=False)
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'")
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)
742
913
"operation-id": 123,
743
914
"status": FAILED,
744
915
"result-text": str(failure)}])
745
917
result.addCallback(got_result)
920
def test_fetch_attachment_failure(self):
922
If the plugin fails to retrieve the attachments with a
923
L{HTTPCodeError}, a specific error code is shown.
925
self.manager.config.url = "https://localhost/message-system"
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")
931
mock_fetch = self.mocker.replace("landscape.lib.fetch.fetch_async",
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,
938
self.mocker.result(fail(HTTPCodeError(404, "Not found")))
941
self.manager.add(ScriptExecutionPlugin())
942
result = self._send_script(
943
"/bin/sh", "echo hi", attachments={u"file1": 14})
945
def got_result(ignored):
947
self.broker_service.message_store.get_pending_messages(),
948
[{"type": "operation-result",
950
"result-text": "Server returned HTTP code 404",
951
"result-code": FETCH_ATTACHMENTS_FAILED_RESULT,
954
return result.addCallback(got_result)