1
from datetime import datetime
5
from landscape.tests.helpers import (LandscapeTest, ManagerHelper,
7
from landscape.lib.process import ProcessInformation
9
from landscape.manager.manager import SUCCEEDED, FAILED
10
from landscape.manager.processkiller import (
11
ProcessKiller, ProcessNotFoundError, ProcessMismatchError,
15
def get_active_process():
16
return subprocess.Popen(["python", "-c", "raw_input()"],
17
stdin=subprocess.PIPE,
18
stdout=subprocess.PIPE,
19
stderr=subprocess.PIPE)
22
def get_missing_pid():
23
popen = subprocess.Popen(["hostname"], stdout=subprocess.PIPE)
28
class ProcessKillerTests(LandscapeTest):
29
"""Tests for L{ProcessKiller}."""
31
helpers = [ManagerHelper]
34
LandscapeTest.setUp(self)
35
self.sample_dir = self.make_dir()
36
self.builder = ProcessDataBuilder(self.sample_dir)
37
self.process_info = ProcessInformation(proc_dir=self.sample_dir,
38
jiffies=1, boot_time=10)
39
self.signaller = ProcessKiller(process_info=self.process_info)
40
service = self.broker_service
41
service.message_store.set_accepted_types(["operation-result"])
43
def _test_signal_name(self, signame, signum):
44
self.manager.add(self.signaller)
45
self.builder.create_data(100, self.builder.RUNNING,
46
uid=1000, gid=1000, started_after_boot=10,
49
kill = self.mocker.replace("os.kill", passthrough=False)
53
self.manager.dispatch_message(
54
{"type": "signal-process",
56
"pid": 100, "name": "ooga",
57
"start-time": 20, "signal": signame})
59
def test_kill_process_signal(self):
61
When specifying the signal name as 'KILL', os.kill should be passed the
64
self._test_signal_name("KILL", signal.SIGKILL)
66
def test_end_process_signal(self):
68
When specifying the signal name as 'TERM', os.kill should be passed the
71
self._test_signal_name("TERM", signal.SIGTERM)
73
def _test_signal_real_process(self, signame):
75
When a 'signal-process' message is received the plugin should
76
signal the appropriate process and generate an operation-result
77
message with details of the outcome. Data is gathered from
78
internal plugin methods to get the start time of the test
79
process being signalled.
81
process_info_factory = ProcessInformation()
82
signaller = ProcessKiller()
83
signaller.register(self.manager)
84
popen = get_active_process()
85
process_info = process_info_factory.get_process_info(popen.pid)
86
self.assertNotEquals(process_info, None)
87
start_time = process_info["start-time"]
89
self.manager.dispatch_message(
90
{"type": "signal-process",
92
"pid": popen.pid, "name": "python",
93
"start-time": start_time, "signal": signame})
94
# We're waiting on the child process here so that we (the
95
# parent process) consume it's return code; this prevents it
96
# from becoming a zombie and makes the test do a better job of
97
# reflecting the real world.
98
return_code = popen.wait()
99
# The return code is negative if the process was terminated by
101
self.assertTrue(return_code < 0)
102
process_info = process_info_factory.get_process_info(popen.pid)
103
self.assertEquals(process_info, None)
105
service = self.broker_service
106
self.assertMessages(service.message_store.get_pending_messages(),
107
[{"type": "operation-result",
108
"status": SUCCEEDED, "operation-id": 1}])
110
def test_kill_real_process(self):
111
self._test_signal_real_process("KILL")
113
def test_end_real_process(self):
114
self._test_signal_real_process("TERM")
116
def test_signal_missing_process(self):
118
When a 'signal-process' message is received for a process that
119
no longer the exists the plugin should generate an error.
121
self.log_helper.ignore_errors(ProcessNotFoundError)
122
self.manager.add(self.signaller)
124
pid = get_missing_pid()
125
self.manager.dispatch_message(
126
{"operation-id": 1, "type": "signal-process",
127
"pid": pid, "name": "zsh", "start-time": 110,
129
expected_text = ("ProcessNotFoundError: The process zsh with PID %d "
130
"that started at 1970-01-01 00:01:50 UTC was not "
133
service = self.broker_service
134
self.assertMessages(service.message_store.get_pending_messages(),
135
[{"type": "operation-result",
138
"result-text": expected_text}])
139
self.assertTrue("ProcessNotFoundError" in self.logfile.getvalue())
142
def test_signal_process_start_time_mismatch(self):
144
When a 'signal-process' message is received with a mismatched
145
start time the plugin should generate an error.
147
self.log_helper.ignore_errors(ProcessMismatchError)
148
self.manager.add(self.signaller)
149
pid = get_missing_pid()
150
self.builder.create_data(pid, self.builder.RUNNING,
151
uid=1000, gid=1000, started_after_boot=10,
152
process_name="hostname")
154
self.manager.dispatch_message(
155
{"operation-id": 1, "type": "signal-process",
156
"pid": pid, "name": "python",
157
"start-time": 11, "signal": "KILL"})
158
expected_time = datetime.utcfromtimestamp(11)
159
# boot time + proc start time = 20
160
actual_time = datetime.utcfromtimestamp(20)
161
expected_text = ("ProcessMismatchError: The process python with "
162
"PID %d that started at %s UTC was not found. A "
163
"process with the same PID that started at %s UTC "
164
"was found and not sent the KILL signal"
165
% (pid, expected_time, actual_time))
167
service = self.broker_service
168
self.assertMessages(service.message_store.get_pending_messages(),
169
[{"type": "operation-result",
172
"result-text": expected_text}])
173
self.assertTrue("ProcessMismatchError" in self.logfile.getvalue())
175
def test_signal_process_race(self):
177
Before trying to signal a process it first checks to make sure a
178
process with a matching PID and name exist. It's possible for the
179
process to disappear after checking the process exists and before
180
sending the signal; a generic error should be raised in that case.
182
self.log_helper.ignore_errors(SignalProcessError)
183
pid = get_missing_pid()
184
self.builder.create_data(pid, self.builder.RUNNING,
185
uid=1000, gid=1000, started_after_boot=10,
186
process_name="hostname")
187
self.assertRaises(SignalProcessError,
188
self.signaller.signal_process, pid,
189
"hostname", 20, "KILL")
191
self.manager.add(self.signaller)
192
self.manager.dispatch_message(
193
{"operation-id": 1, "type": "signal-process",
194
"pid": pid, "name": "hostname", "start-time": 20,
196
expected_text = ("SignalProcessError: Attempting to send the KILL "
197
"signal to the process hostname with PID %d failed"
200
service = self.broker_service
201
self.assertMessages(service.message_store.get_pending_messages(),
202
[{"type": "operation-result",
205
"result-text": expected_text}])
206
self.assertTrue("SignalProcessError" in self.logfile.getvalue())
208
def test_accept_small_start_time_skews(self):
210
The boot time isn't very precise, so accept small skews in the
211
computed process start time.
213
self.manager.add(self.signaller)
214
self.builder.create_data(100, self.builder.RUNNING,
215
uid=1000, gid=1000, started_after_boot=10,
218
kill = self.mocker.replace("os.kill", passthrough=False)
219
kill(100, signal.SIGKILL)
222
self.manager.dispatch_message(
223
{"type": "signal-process",
225
"pid": 100, "name": "ooga",
226
"start-time": 21, "signal": "KILL"})