~ahasenack/landscape-client/landscape-client-1.5.5-0ubuntu0.9.04.0

« back to all changes in this revision

Viewing changes to landscape/manager/tests/test_processkiller.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
from datetime import datetime
 
2
import signal
 
3
import subprocess
 
4
 
 
5
from landscape.tests.helpers import (LandscapeTest, ManagerHelper,
 
6
                                     ProcessDataBuilder)
 
7
from landscape.lib.process import ProcessInformation
 
8
 
 
9
from landscape.manager.manager import SUCCEEDED, FAILED
 
10
from landscape.manager.processkiller import (
 
11
    ProcessKiller, ProcessNotFoundError, ProcessMismatchError,
 
12
    SignalProcessError)
 
13
 
 
14
 
 
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)
 
20
 
 
21
 
 
22
def get_missing_pid():
 
23
    popen = subprocess.Popen(["hostname"], stdout=subprocess.PIPE)
 
24
    popen.wait()
 
25
    return popen.pid
 
26
 
 
27
 
 
28
class ProcessKillerTests(LandscapeTest):
 
29
    """Tests for L{ProcessKiller}."""
 
30
 
 
31
    helpers = [ManagerHelper]
 
32
 
 
33
    def setUp(self):
 
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"])
 
42
 
 
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,
 
47
                                 process_name="ooga")
 
48
 
 
49
        kill = self.mocker.replace("os.kill", passthrough=False)
 
50
        kill(100, signum)
 
51
        self.mocker.replay()
 
52
 
 
53
        self.manager.dispatch_message(
 
54
            {"type": "signal-process",
 
55
             "operation-id": 1,
 
56
             "pid": 100, "name": "ooga",
 
57
             "start-time": 20, "signal": signame})
 
58
 
 
59
    def test_kill_process_signal(self):
 
60
        """
 
61
        When specifying the signal name as 'KILL', os.kill should be passed the
 
62
        KILL signal.
 
63
        """
 
64
        self._test_signal_name("KILL", signal.SIGKILL)
 
65
 
 
66
    def test_end_process_signal(self):
 
67
        """
 
68
        When specifying the signal name as 'TERM', os.kill should be passed the
 
69
        TERM signal.
 
70
        """
 
71
        self._test_signal_name("TERM", signal.SIGTERM)
 
72
 
 
73
    def _test_signal_real_process(self, signame):
 
74
        """
 
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.
 
80
        """
 
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"]
 
88
 
 
89
        self.manager.dispatch_message(
 
90
            {"type": "signal-process",
 
91
             "operation-id": 1,
 
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
 
100
        # a signal.
 
101
        self.assertTrue(return_code < 0)
 
102
        process_info = process_info_factory.get_process_info(popen.pid)
 
103
        self.assertEquals(process_info, None)
 
104
 
 
105
        service = self.broker_service
 
106
        self.assertMessages(service.message_store.get_pending_messages(),
 
107
                            [{"type": "operation-result",
 
108
                              "status": SUCCEEDED, "operation-id": 1}])
 
109
 
 
110
    def test_kill_real_process(self):
 
111
        self._test_signal_real_process("KILL")
 
112
 
 
113
    def test_end_real_process(self):
 
114
        self._test_signal_real_process("TERM")
 
115
 
 
116
    def test_signal_missing_process(self):
 
117
        """
 
118
        When a 'signal-process' message is received for a process that
 
119
        no longer the exists the plugin should generate an error.
 
120
        """
 
121
        self.log_helper.ignore_errors(ProcessNotFoundError)
 
122
        self.manager.add(self.signaller)
 
123
 
 
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,
 
128
             "signal": "KILL"})
 
129
        expected_text = ("ProcessNotFoundError: The process zsh with PID %d "
 
130
                         "that started at 1970-01-01 00:01:50 UTC was not "
 
131
                         "found" % (pid,))
 
132
 
 
133
        service = self.broker_service
 
134
        self.assertMessages(service.message_store.get_pending_messages(),
 
135
                            [{"type": "operation-result",
 
136
                              "operation-id": 1,
 
137
                              "status": FAILED,
 
138
                              "result-text": expected_text}])
 
139
        self.assertTrue("ProcessNotFoundError" in self.logfile.getvalue())
 
140
 
 
141
 
 
142
    def test_signal_process_start_time_mismatch(self):
 
143
        """
 
144
        When a 'signal-process' message is received with a mismatched
 
145
        start time the plugin should generate an error.
 
146
        """
 
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")
 
153
 
 
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))
 
166
 
 
167
        service = self.broker_service
 
168
        self.assertMessages(service.message_store.get_pending_messages(),
 
169
                            [{"type": "operation-result",
 
170
                              "operation-id": 1,
 
171
                              "status": FAILED,
 
172
                              "result-text": expected_text}])
 
173
        self.assertTrue("ProcessMismatchError" in self.logfile.getvalue())
 
174
 
 
175
    def test_signal_process_race(self):
 
176
        """
 
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.
 
181
        """
 
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")
 
190
 
 
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,
 
195
             "signal": "KILL"})
 
196
        expected_text = ("SignalProcessError: Attempting to send the KILL "
 
197
                         "signal to the process hostname with PID %d failed"
 
198
                         % (pid,))
 
199
 
 
200
        service = self.broker_service
 
201
        self.assertMessages(service.message_store.get_pending_messages(),
 
202
                            [{"type": "operation-result",
 
203
                              "operation-id": 1,
 
204
                              "status": FAILED,
 
205
                              "result-text": expected_text}])
 
206
        self.assertTrue("SignalProcessError" in self.logfile.getvalue())
 
207
 
 
208
    def test_accept_small_start_time_skews(self):
 
209
        """
 
210
        The boot time isn't very precise, so accept small skews in the
 
211
        computed process start time.
 
212
        """
 
213
        self.manager.add(self.signaller)
 
214
        self.builder.create_data(100, self.builder.RUNNING,
 
215
                                 uid=1000, gid=1000, started_after_boot=10,
 
216
                                 process_name="ooga")
 
217
 
 
218
        kill = self.mocker.replace("os.kill", passthrough=False)
 
219
        kill(100, signal.SIGKILL)
 
220
        self.mocker.replay()
 
221
 
 
222
        self.manager.dispatch_message(
 
223
            {"type": "signal-process",
 
224
             "operation-id": 1,
 
225
             "pid": 100, "name": "ooga",
 
226
             "start-time": 21, "signal": "KILL"})