def test_binary_output(self): """ If a script outputs non-printable characters not handled by utf-8, they are replaced during the encoding phase but the script succeeds. """ def spawnProcess(protocol, filename, args, env, path, uid, gid): protocol.childDataReceived( 1, b"\x7fELF\x01\x01\x01\x00\x00\x00\x95\x01") protocol.processEnded(Failure(ProcessDone(0))) self._verify_script(filename, sys.executable, "print 'hi'") process_factory = mock.Mock() process_factory.spawnProcess = mock.Mock(side_effect=spawnProcess) self.manager.add( ScriptExecutionPlugin(process_factory=process_factory)) def got_result(r): self.assertTrue(self.broker_service.exchanger.is_urgent()) [message ] = (self.broker_service.message_store.get_pending_messages()) self.assertEqual(message["result-text"], u"\x7fELF\x01\x01\x01\x00\x00\x00\ufffd\x01") process_factory.spawnProcess.assert_called_with( mock.ANY, mock.ANY, args=mock.ANY, uid=None, gid=None, path=mock.ANY, env=get_default_environment()) result = self._send_script(sys.executable, "print 'hi'") return result.addCallback(got_result)
def test_unknown_error(self): """ When a completely unknown error comes back from the process protocol, the operation fails and the formatted failure is included in the response message. """ factory = StubProcessFactory() self.manager.add(ScriptExecutionPlugin(process_factory=factory)) result = self._send_script(sys.executable, "print 'hi'") self._verify_script(factory.spawns[0][1], sys.executable, "print 'hi'") self.assertMessages( self.broker_service.message_store.get_pending_messages(), []) failure = Failure(RuntimeError("Oh noes!")) factory.spawns[0][0].result_deferred.errback(failure) def got_result(r): self.assertMessages( self.broker_service.message_store.get_pending_messages(), [{ "type": "operation-result", "operation-id": 123, "status": FAILED, "result-text": str(failure) }]) result.addCallback(got_result) return result
def test_parse_error_causes_operation_failure(self): """ If there is an error parsing the message, an operation-result will be sent (assuming operation-id *is* successfully parsed). """ self.log_helper.ignore_errors(KeyError) self.manager.add(ScriptExecutionPlugin()) self.manager.dispatch_message({ "type": "execute-script", "operation-id": 444 }) expected_message = [{ "type": "operation-result", "operation-id": 444, "result-text": u"KeyError: username", "status": FAILED }] self.assertMessages( self.broker_service.message_store.get_pending_messages(), expected_message) self.assertTrue("KeyError: 'username'" in self.logfile.getvalue())
def test_user(self): """A user can be specified in the message.""" username = pwd.getpwuid(os.getuid())[0] uid, gid, home = get_user_info(username) def spawnProcess(protocol, filename, args, env, path, uid, gid): protocol.childDataReceived(1, "hi!\n") protocol.processEnded(Failure(ProcessDone(0))) self._verify_script(filename, sys.executable, "print 'hi'") process_factory = mock.Mock() process_factory.spawnProcess = mock.Mock(side_effect=spawnProcess) self.manager.add( ScriptExecutionPlugin(process_factory=process_factory)) result = self._send_script(sys.executable, "print 'hi'", user=username) def check(_): process_factory.spawnProcess.assert_called_with( mock.ANY, mock.ANY, args=mock.ANY, uid=None, gid=None, path=mock.ANY, env=get_default_environment()) return result.addCallback(check)
def test_urgent_response(self): """Responses to script execution messages are urgent.""" def spawnProcess(protocol, filename, args, env, path, uid, gid): protocol.childDataReceived(1, b"hi!\n") protocol.processEnded(Failure(ProcessDone(0))) self._verify_script(filename, sys.executable, "print 'hi'") process_factory = mock.Mock() process_factory.spawnProcess = mock.Mock(side_effect=spawnProcess) self.manager.add( ScriptExecutionPlugin(process_factory=process_factory)) def got_result(r): self.assertTrue(self.broker_service.exchanger.is_urgent()) self.assertMessages( self.broker_service.message_store.get_pending_messages(), [{ "type": "operation-result", "operation-id": 123, "result-text": u"hi!\n", "status": SUCCEEDED }]) process_factory.spawnProcess.assert_called_with( mock.ANY, mock.ANY, args=mock.ANY, uid=None, gid=None, path=mock.ANY, env=get_default_environment()) result = self._send_script(sys.executable, "print 'hi'") return result.addCallback(got_result)
def test_timeout(self): """ If a L{ProcessTimeLimitReachedError} is fired back, the operation-result should have a failed status. """ factory = StubProcessFactory() self.manager.add(ScriptExecutionPlugin(process_factory=factory)) result = self._send_script(sys.executable, "bar", time_limit=30) self._verify_script(factory.spawns[0][1], sys.executable, "bar") protocol = factory.spawns[0][0] protocol.makeConnection(DummyProcess()) protocol.childDataReceived(2, b"ONOEZ") self.manager.reactor.advance(31) protocol.processEnded(Failure(ProcessDone(0))) def got_result(r): self.assertMessages( self.broker_service.message_store.get_pending_messages(), [{ "type": "operation-result", "operation-id": 123, "status": FAILED, "result-text": u"ONOEZ", "result-code": 102 }]) result.addCallback(got_result) return result
def test_success(self): """ When a C{execute-script} message is received from the server, the specified script will be run and an operation-result will be sent back to the server. """ # Let's use a stub process factory, because otherwise we don't have # access to the deferred. factory = StubProcessFactory() self.manager.add(ScriptExecutionPlugin(process_factory=factory)) result = self._send_script(sys.executable, "print 'hi'") self._verify_script(factory.spawns[0][1], sys.executable, "print 'hi'") self.assertMessages( self.broker_service.message_store.get_pending_messages(), []) # Now let's simulate the completion of the process factory.spawns[0][0].childDataReceived(1, b"hi!\n") factory.spawns[0][0].processEnded(Failure(ProcessDone(0))) def got_result(r): self.assertMessages( self.broker_service.message_store.get_pending_messages(), [{ "type": "operation-result", "operation-id": 123, "status": SUCCEEDED, "result-text": u"hi!\n" }]) result.addCallback(got_result) return result
def test_non_zero_exit_fails_operation(self): """ If a script exits with a nen-zero exit code, the operation associated with it should fail, but the data collected should still be sent. """ self.manager.add(ScriptExecutionPlugin()) result = self._send_script("/bin/sh", "echo hi; exit 1") def got_result(ignored): self.assertMessages( self.broker_service.message_store.get_pending_messages(), [{ "type": "operation-result", "operation-id": 123, "result-text": "hi\n", "result-code": PROCESS_FAILED_RESULT, "status": FAILED }]) return result.addCallback(got_result)
def test_configured_users(self): """ Messages which try to run a script as a user that is not allowed should be rejected. """ self.manager.add(ScriptExecutionPlugin()) self.manager.config.script_users = "landscape, nobody" result = self._send_script(sys.executable, "bar", user="******") def got_result(r): self.assertMessages( self.broker_service.message_store.get_pending_messages(), [{ "type": "operation-result", "operation-id": 123, "status": FAILED, "result-text": u"Scripts cannot be run as user whatever." }]) result.addCallback(got_result) return result
def test_fetch_attachment_failure(self, mock_fetch): """ If the plugin fails to retrieve the attachments with a L{HTTPCodeError}, a specific error code is shown. """ self.manager.config.url = "https://localhost/message-system" persist = Persist( filename=os.path.join(self.config.data_path, "broker.bpickle")) registration_persist = persist.root_at("registration") registration_persist.set("secure-id", "secure_id") persist.save() headers = { "User-Agent": "landscape-client/%s" % VERSION, "Content-Type": "application/octet-stream", "X-Computer-ID": "secure_id" } mock_fetch.return_value = fail(HTTPCodeError(404, "Not found")) self.manager.add(ScriptExecutionPlugin()) result = self._send_script("/bin/sh", "echo hi", attachments={u"file1": 14}) def got_result(ignored): self.assertMessages( self.broker_service.message_store.get_pending_messages(), [{ "type": "operation-result", "operation-id": 123, "result-text": "Server returned HTTP code 404", "result-code": FETCH_ATTACHMENTS_FAILED_RESULT, "status": FAILED }]) mock_fetch.assert_called_with("https://localhost/attachment/14", headers=headers, cainfo=None) return result.addCallback(got_result)
def test_unknown_user_with_unicode(self): """ If an error happens because an unknow user is selected, and that this user name happens to contain unicode characters, the error message is correctly encoded and reported. This test mainly ensures that unicode error message works, using unknown user as an easy way to test it. """ self.log_helper.ignore_errors(UnknownUserError) username = u"non-existent-f\N{LATIN SMALL LETTER E WITH ACUTE}e" self.manager.add(ScriptExecutionPlugin()) self._send_script(sys.executable, "print 'hi'", user=username) self.assertMessages( self.broker_service.message_store.get_pending_messages(), [{ "type": "operation-result", "operation-id": 123, "result-text": u"UnknownUserError: Unknown user '%s'" % username, "status": FAILED }])
def setUp(self): super(RunScriptTests, self).setUp() self.plugin = ScriptExecutionPlugin() self.manager.add(self.plugin)
class RunScriptTests(LandscapeTest): helpers = [ManagerHelper] def setUp(self): super(RunScriptTests, self).setUp() self.plugin = ScriptExecutionPlugin() self.manager.add(self.plugin) def test_basic_run(self): """ The plugin returns a Deferred resulting in the output of basic commands. """ result = self.plugin.run_script("/bin/sh", "echo hi") result.addCallback(self.assertEqual, "hi\n") return result def test_snap_path(self): """The bin path for snaps is included in the PATH.""" deferred = self.plugin.run_script("/bin/sh", "echo $PATH") deferred.addCallback(lambda result: self.assertIn("/snap/bin", result)) return deferred def test_other_interpreter(self): """Non-shell interpreters can be specified.""" result = self.plugin.run_script("/usr/bin/python", "print 'hi'") result.addCallback(self.assertEqual, "hi\n") return result def test_other_interpreter_env(self): """ Non-shell interpreters don't have their paths set by the shell, so we need to check that other interpreters have environment variables set. """ result = self.plugin.run_script(sys.executable, "import os\nprint(os.environ)") def check_environment(results): for string in get_default_environment(): self.assertIn(string, results) result.addCallback(check_environment) return result def test_server_supplied_env(self): """ Server-supplied environment variables are merged with default variables then passed to script. """ server_supplied_env = {"DOG": "Woof", "CAT": "Meow"} result = self.plugin.run_script( sys.executable, "import os\nprint(os.environ)", server_supplied_env=server_supplied_env) def check_environment(results): for string in get_default_environment(): self.assertIn(string, results) for name, value in server_supplied_env.items(): self.assertIn(name, results) self.assertIn(value, results) result.addCallback(check_environment) return result def test_server_supplied_env_overrides_client(self): """ Server-supplied environment variables override client default values if the server provides them. """ server_supplied_env = { "PATH": "server-path", "USER": "******", "HOME": "server-home" } result = self.plugin.run_script( sys.executable, "import os\nprint(os.environ)", server_supplied_env=server_supplied_env) def check_environment(results): for name, value in server_supplied_env.items(): self.assertIn(name, results) self.assertIn(value, results) result.addCallback(check_environment) return result def test_concurrent(self): """ Scripts run with the ScriptExecutionPlugin plugin are run concurrently. """ fifo = self.makeFile() os.mkfifo(fifo) self.addCleanup(os.remove, fifo) # If the first process is blocking on a fifo, and the second process # wants to write to the fifo, the only way this will complete is if # run_script is truly async d1 = self.plugin.run_script("/bin/sh", "cat " + fifo) d2 = self.plugin.run_script("/bin/sh", "echo hi > " + fifo) d1.addCallback(self.assertEqual, "hi\n") d2.addCallback(self.assertEqual, "") return gatherResults([d1, d2]) def test_accented_run_in_code(self): """ Scripts can contain accented data both in the code and in the result. """ accented_content = u"\N{LATIN SMALL LETTER E WITH ACUTE}" result = self.plugin.run_script(u"/bin/sh", u"echo %s" % (accented_content, )) # self.assertEqual gets the result as first argument and that's what we # compare against. result.addCallback(self.assertEqual, "%s\n" % (accented_content, )) return result def test_accented_run_in_interpreter(self): """ Scripts can also contain accents in the interpreter. """ accented_content = u"\N{LATIN SMALL LETTER E WITH ACUTE}" result = self.plugin.run_script(u"/bin/echo %s" % (accented_content, ), u"") def check(result): self.assertTrue("%s " % (accented_content, ) in result) result.addCallback(check) return result def test_set_umask_appropriately(self): """ We should be setting the umask to 0o022 before executing a script, and restoring it to the previous value when finishing. """ # Get original umask. old_umask = os.umask(0) os.umask(old_umask) patch_umask = mock.patch("os.umask") mock_umask = patch_umask.start() mock_umask.return_value = old_umask result = self.plugin.run_script("/bin/sh", "umask") def check(result): self.assertEqual("%04o\n" % old_umask, result) mock_umask.assert_has_calls( [mock.call(0o22), mock.call(old_umask)]) result.addCallback(check) return result.addCallback(lambda _: patch_umask.stop()) def test_restore_umask_in_event_of_error(self): """ We set the umask before executing the script, in the event that there's an error setting up the script, we want to restore the umask. """ patch_umask = mock.patch("os.umask", return_value=0o077) mock_umask = patch_umask.start() patch_mkdtemp = mock.patch("tempfile.mkdtemp", side_effect=OSError("Fail!")) mock_mkdtemp = patch_mkdtemp.start() result = self.plugin.run_script("/bin/sh", "umask", attachments={u"file1": "some data"}) def check(error): self.assertIsInstance(error.value, OSError) self.assertEqual("Fail!", str(error.value)) mock_umask.assert_has_calls([mock.call(0o022)]) mock_mkdtemp.assert_called_with() def cleanup(result): patch_umask.stop() patch_mkdtemp.stop() return result return result.addErrback(check).addBoth(cleanup) def test_run_with_attachments(self): result = self.plugin.run_script( u"/bin/sh", u"ls $LANDSCAPE_ATTACHMENTS && cat $LANDSCAPE_ATTACHMENTS/file1", attachments={u"file1": "some data"}) def check(result): self.assertEqual(result, "file1\nsome data") result.addCallback(check) return result def test_run_with_attachment_ids(self): """ The most recent protocol for script message doesn't include the attachment body inside the message itself, but instead gives an attachment ID, and the plugin fetches the files separately. """ self.manager.config.url = "https://localhost/message-system" persist = Persist( filename=os.path.join(self.config.data_path, "broker.bpickle")) registration_persist = persist.root_at("registration") registration_persist.set("secure-id", "secure_id") persist.save() patch_fetch = mock.patch( "landscape.client.manager.scriptexecution.fetch_async") mock_fetch = patch_fetch.start() mock_fetch.return_value = succeed(b"some other data") headers = { "User-Agent": "landscape-client/%s" % VERSION, "Content-Type": "application/octet-stream", "X-Computer-ID": "secure_id" } result = self.plugin.run_script( u"/bin/sh", u"ls $LANDSCAPE_ATTACHMENTS && cat $LANDSCAPE_ATTACHMENTS/file1", attachments={u"file1": 14}) def check(result): self.assertEqual(result, "file1\nsome other data") mock_fetch.assert_called_with("https://localhost/attachment/14", headers=headers, cainfo=None) def cleanup(result): patch_fetch.stop() # We have to return the Failure or result to get a working test. return result return result.addCallback(check).addBoth(cleanup) def test_run_with_attachment_ids_and_ssl(self): """ When fetching attachments, L{ScriptExecution} passes the optional ssl certificate file if the configuration specifies it. """ self.manager.config.url = "https://localhost/message-system" self.manager.config.ssl_public_key = "/some/key" persist = Persist( filename=os.path.join(self.config.data_path, "broker.bpickle")) registration_persist = persist.root_at("registration") registration_persist.set("secure-id", "secure_id") persist.save() patch_fetch = mock.patch( "landscape.client.manager.scriptexecution.fetch_async") mock_fetch = patch_fetch.start() mock_fetch.return_value = succeed(b"some other data") headers = { "User-Agent": "landscape-client/%s" % VERSION, "Content-Type": "application/octet-stream", "X-Computer-ID": "secure_id" } result = self.plugin.run_script( u"/bin/sh", u"ls $LANDSCAPE_ATTACHMENTS && cat $LANDSCAPE_ATTACHMENTS/file1", attachments={u"file1": 14}) def check(result): self.assertEqual(result, "file1\nsome other data") mock_fetch.assert_called_with("https://localhost/attachment/14", headers=headers, cainfo="/some/key") def cleanup(result): patch_fetch.stop() return result return result.addCallback(check).addBoth(cleanup) def test_self_remove_script(self): """ If a script removes itself, it doesn't create an error when the script execution plugin tries to remove the script file. """ result = self.plugin.run_script("/bin/sh", "echo hi && rm $0") result.addCallback(self.assertEqual, "hi\n") return result def test_self_remove_attachments(self): """ If a script removes its attachments, it doesn't create an error when the script execution plugin tries to remove the attachments directory. """ result = self.plugin.run_script( u"/bin/sh", u"ls $LANDSCAPE_ATTACHMENTS && rm -r $LANDSCAPE_ATTACHMENTS", attachments={u"file1": "some data"}) def check(result): self.assertEqual(result, "file1\n") result.addCallback(check) return result def _run_script(self, username, uid, gid, path): expected_uid = uid if uid != os.getuid() else None expected_gid = gid if gid != os.getgid() else None factory = StubProcessFactory() self.plugin.process_factory = factory # ignore the call to chown! patch_chown = mock.patch("os.chown") mock_chown = patch_chown.start() result = self.plugin.run_script("/bin/sh", "echo hi", user=username) self.assertEqual(len(factory.spawns), 1) spawn = factory.spawns[0] self.assertEqual(spawn[4], path) self.assertEqual(spawn[5], expected_uid) self.assertEqual(spawn[6], expected_gid) protocol = spawn[0] protocol.childDataReceived(1, b"foobar") for fd in (0, 1, 2): protocol.childConnectionLost(fd) protocol.processEnded(Failure(ProcessDone(0))) def check(result): mock_chown.assert_called_with() self.assertEqual(result, "foobar") def cleanup(result): patch_chown.stop() return result return result.addErrback(check).addBoth(cleanup) def test_user(self): """ Running a script as a particular user calls C{IReactorProcess.spawnProcess} with an appropriate C{uid} argument, with the user's primary group as the C{gid} argument and with the user home as C{path} argument. """ uid = os.getuid() info = pwd.getpwuid(uid) username = info.pw_name gid = info.pw_gid path = info.pw_dir return self._run_script(username, uid, gid, path) def test_user_no_home(self): """ When the user specified to C{run_script} doesn't have a home, the script executes in '/'. """ patch_getpwnam = mock.patch("pwd.getpwnam") mock_getpwnam = patch_getpwnam.start() class pwnam(object): pw_uid = 1234 pw_gid = 5678 pw_dir = self.makeFile() mock_getpwnam.return_value = pwnam result = self._run_script("user", 1234, 5678, "/") def check(result): mock_getpwnam.assert_called_with("user") def cleanup(result): patch_getpwnam.stop() return result return result.addCallback(check).addBoth(cleanup) def test_user_with_attachments(self): uid = os.getuid() info = pwd.getpwuid(uid) username = info.pw_name gid = info.pw_gid patch_chown = mock.patch("os.chown") mock_chown = patch_chown.start() factory = StubProcessFactory() self.plugin.process_factory = factory result = self.plugin.run_script("/bin/sh", "echo hi", user=username, attachments={u"file 1": "some data"}) self.assertEqual(len(factory.spawns), 1) spawn = factory.spawns[0] self.assertIn("LANDSCAPE_ATTACHMENTS", spawn[3]) attachment_dir = spawn[3]["LANDSCAPE_ATTACHMENTS"] self.assertEqual(stat.S_IMODE(os.stat(attachment_dir).st_mode), 0o700) filename = os.path.join(attachment_dir, "file 1") self.assertEqual(stat.S_IMODE(os.stat(filename).st_mode), 0o600) protocol = spawn[0] protocol.childDataReceived(1, b"foobar") for fd in (0, 1, 2): protocol.childConnectionLost(fd) protocol.processEnded(Failure(ProcessDone(0))) def check(data): self.assertEqual(data, "foobar") self.assertFalse(os.path.exists(attachment_dir)) mock_chown.assert_has_calls( [mock.call(mock.ANY, uid, gid) for x in range(3)]) def cleanup(result): patch_chown.stop() return result return result.addCallback(check).addBoth(cleanup) def test_limit_size(self): """Data returned from the command is limited.""" factory = StubProcessFactory() self.plugin.process_factory = factory self.plugin.size_limit = 100 result = self.plugin.run_script("/bin/sh", "") # Ultimately we assert that the resulting output is limited to # 100 bytes and indicates its truncation. result.addCallback(self.assertEqual, ("x" * 79) + "\n**OUTPUT TRUNCATED**") protocol = factory.spawns[0][0] # Push 200 bytes of output, so we trigger truncation. protocol.childDataReceived(1, b"x" * 200) for fd in (0, 1, 2): protocol.childConnectionLost(fd) protocol.processEnded(Failure(ProcessDone(0))) return result def test_command_output_ends_with_truncation(self): """After truncation, no further output is recorded.""" factory = StubProcessFactory() self.plugin.process_factory = factory self.plugin.size_limit = 100 result = self.plugin.run_script("/bin/sh", "") # Ultimately we assert that the resulting output is limited to # 100 bytes and indicates its truncation. result.addCallback(self.assertEqual, ("x" * 79) + "\n**OUTPUT TRUNCATED**") protocol = factory.spawns[0][0] # Push 200 bytes of output, so we trigger truncation. protocol.childDataReceived(1, b"x" * 200) # Push 200 bytes more protocol.childDataReceived(1, b"x" * 200) for fd in (0, 1, 2): protocol.childConnectionLost(fd) protocol.processEnded(Failure(ProcessDone(0))) return result def test_limit_time(self): """ The process only lasts for a certain number of seconds. """ result = self.plugin.run_script("/bin/sh", "cat", time_limit=500) self.manager.reactor.advance(501) self.assertFailure(result, ProcessTimeLimitReachedError) return result def test_limit_time_accumulates_data(self): """ Data from processes that time out should still be accumulated and available from the exception object that is raised. """ factory = StubProcessFactory() self.plugin.process_factory = factory result = self.plugin.run_script("/bin/sh", "", time_limit=500) protocol = factory.spawns[0][0] protocol.makeConnection(DummyProcess()) protocol.childDataReceived(1, b"hi\n") self.manager.reactor.advance(501) protocol.processEnded(Failure(ProcessDone(0))) def got_error(f): self.assertTrue(f.check(ProcessTimeLimitReachedError)) self.assertEqual(f.value.data, "hi\n") result.addErrback(got_error) return result def test_time_limit_canceled_after_success(self): """ The timeout call is cancelled after the script terminates. """ factory = StubProcessFactory() self.plugin.process_factory = factory self.plugin.run_script("/bin/sh", "", time_limit=500) protocol = factory.spawns[0][0] transport = DummyProcess() protocol.makeConnection(transport) protocol.childDataReceived(1, b"hi\n") protocol.processEnded(Failure(ProcessDone(0))) self.manager.reactor.advance(501) self.assertEqual(transport.signals, []) def test_cancel_doesnt_blow_after_success(self): """ When the process ends successfully and is immediately followed by the timeout, the output should still be in the failure and nothing bad will happen! [regression test: killing of the already-dead process would blow up.] """ factory = StubProcessFactory() self.plugin.process_factory = factory result = self.plugin.run_script("/bin/sh", "", time_limit=500) protocol = factory.spawns[0][0] protocol.makeConnection(DummyProcess()) protocol.childDataReceived(1, b"hi") protocol.processEnded(Failure(ProcessDone(0))) self.manager.reactor.advance(501) def got_result(output): self.assertEqual(output, "hi") result.addCallback(got_result) return result @mock.patch("os.chown") @mock.patch("os.chmod") @mock.patch("tempfile.mkstemp") @mock.patch("os.fdopen") def test_script_is_owned_by_user(self, mock_fdopen, mock_mkstemp, mock_chmod, mock_chown): """ This is a very white-box test. When a script is generated, it must be created such that data NEVER gets into it before the file has the correct permissions. Therefore os.chmod and os.chown must be called before data is written. """ username = pwd.getpwuid(os.getuid())[0] uid, gid, home = get_user_info(username) called_mocks = [] mock_chown.side_effect = lambda *_: called_mocks.append(mock_chown) mock_chmod.side_effect = lambda *_: called_mocks.append(mock_chmod) def mock_mkstemp_side_effect(*_): called_mocks.append(mock_mkstemp) return (99, "tempo!") mock_mkstemp.side_effect = mock_mkstemp_side_effect script_file = mock.Mock() def mock_fdopen_side_effect(*_): called_mocks.append(mock_fdopen) return script_file mock_fdopen.side_effect = mock_fdopen_side_effect def spawnProcess(protocol, filename, args, env, path, uid, gid): self.assertIsNone(uid) self.assertIsNone(gid) self.assertEqual(get_default_environment(), env) protocol.result_deferred.callback(None) process_factory = mock.Mock() process_factory.spawnProcess = spawnProcess self.plugin.process_factory = process_factory result = self.plugin.run_script("/bin/sh", "code", user=pwd.getpwuid(uid)[0]) def check(_): mock_fdopen.assert_called_with(99, "wb") mock_chmod.assert_called_with("tempo!", 0o700) mock_chown.assert_called_with("tempo!", uid, gid) script_file.write.assert_called_with(b"#!/bin/sh\ncode") script_file.close.assert_called_with() self.assertEqual( [mock_mkstemp, mock_fdopen, mock_chmod, mock_chown], called_mocks) return result.addCallback(check) def test_script_removed(self): """ The script is removed after it is finished. """ fd, filename = tempfile.mkstemp() with mock.patch("tempfile.mkstemp") as mock_mkstemp: mock_mkstemp.return_value = (fd, filename) d = self.plugin.run_script("/bin/sh", "true") return d.addCallback( lambda _: self.assertFalse(os.path.exists(filename))) def test_unknown_interpreter(self): """ If the script is run with an unknown interpreter, it raises a meaningful error instead of crashing in execvpe. """ d = self.plugin.run_script("/bin/cantpossiblyexist", "stuff") def cb(ignore): self.fail("Should not be there") def eb(failure): failure.trap(UnknownInterpreterError) self.assertEqual(failure.value.interpreter, "/bin/cantpossiblyexist") return d.addCallback(cb).addErrback(eb)