Make TaskotronRemoteTimeoutError a descendant of
TaskotronRemoteError (which sounds logical). Ajust minion code to
reflect that change. Add tests.
This is a followup of D826.
mkrizek |
Make TaskotronRemoteTimeoutError a descendant of
TaskotronRemoteError (which sounds logical). Ajust minion code to
reflect that change. Add tests.
This is a followup of D826.
test suite works, tested rpmlint execution locally and with a disposable client
Automatic diff as part of commit; lint not applicable. |
Automatic diff as part of commit; unit tests not applicable. |
Path | Packages | |||
---|---|---|---|---|
M | libtaskotron/exceptions.py (2 lines) | |||
M | libtaskotron/minion.py (32 lines) | |||
M | testing/test_minion.py (61 lines) |
Commit | Tree | Parents | Author | Summary | Date |
---|---|---|---|---|---|
db82f8028245 | dd5f619b2ec7 | 78a004c2b5d1 | Kamil Páral | exc: adjust TaskotronRemoteTimeoutError hierarchy (Show More…) | Jun 24 2016, 1:39 PM |
Show All 32 Lines | 31 | class TaskotronDirectiveError(TaskotronError): | |||
---|---|---|---|---|---|
33 | pass | 33 | pass | ||
34 | 34 | | |||
35 | 35 | | |||
36 | class TaskotronRemoteError(TaskotronError): | 36 | class TaskotronRemoteError(TaskotronError): | ||
37 | '''All network and remote-server related errors''' | 37 | '''All network and remote-server related errors''' | ||
38 | pass | 38 | pass | ||
39 | 39 | | |||
40 | 40 | | |||
41 | class TaskotronRemoteTimeoutError(TaskotronError): | 41 | class TaskotronRemoteTimeoutError(TaskotronRemoteError): | ||
42 | '''A remote-server did not send any output in a given timeout''' | 42 | '''A remote-server did not send any output in a given timeout''' | ||
43 | pass | 43 | pass | ||
44 | 44 | | |||
45 | 45 | | |||
46 | class TaskotronRemoteProcessError(TaskotronRemoteError): | 46 | class TaskotronRemoteProcessError(TaskotronRemoteError): | ||
47 | '''A process/command executed remotely returned a non-zero exit code, crashed or failed in a | 47 | '''A process/command executed remotely returned a non-zero exit code, crashed or failed in a | ||
48 | different way''' | 48 | different way''' | ||
49 | pass | 49 | pass | ||
Show All 29 Lines |
Show First 20 Lines • Show All 157 Lines • ▼ Show 20 Line(s) | 151 | def execute(self): | |||
---|---|---|---|---|---|
158 | 158 | | |||
159 | log.info("Running task over SSH on machine '%s'...", self.machine) | 159 | log.info("Running task over SSH on machine '%s'...", self.machine) | ||
160 | 160 | | |||
161 | with remote_exec.ParamikoWrapper(self.machine, self.port, self.user, | 161 | with remote_exec.ParamikoWrapper(self.machine, self.port, self.user, | ||
162 | self.ssh_privkey, stdio_filename) as self.ssh: | 162 | self.ssh_privkey, stdio_filename) as self.ssh: | ||
163 | try: | 163 | try: | ||
164 | self._prepare_task() | 164 | self._prepare_task() | ||
165 | self._run() | 165 | self._run() | ||
166 | except exc.TaskotronRemoteTimeoutError: | ||||
167 | # do not try to get logs - the minion is likely dead and we would hang on | ||||
168 | # getting the logs, see https://phab.qadevel.cloud.fedoraproject.org/T665 | ||||
169 | raise | ||||
166 | except exc.TaskotronRemoteError: | 170 | except exc.TaskotronRemoteError: | ||
167 | # This is the case when remote execution failed but it should be possible to | 171 | # this is the case when remote execution failed but it should be possible to | ||
168 | # get logs from the minion, so try it. We don't try getting logs in case when | 172 | # get logs from the minion, so try it | ||
169 | # exc.TaskotronRemoteTimeoutError is thrown which means that the minion is | | |||
170 | # likely dead and we would hang on getting the logs. | | |||
171 | # See https://phab.qadevel.cloud.fedoraproject.org/T665 | | |||
172 | self._get_output() | 173 | self._get_output() | ||
173 | raise | 174 | raise | ||
174 | 175 | else: | |||
175 | self._get_output() | 176 | self._get_output() | ||
176 | | ||||
177 | 177 | | |||
178 | 178 | | |||
179 | class DisposableMinion(BaseMinion): | 179 | class DisposableMinion(BaseMinion): | ||
180 | '''Minion class that creates a disposable client, connects to it over SSH and runs the task | 180 | '''Minion class that creates a disposable client, connects to it over SSH and runs the task | ||
181 | there. | 181 | there. | ||
182 | ''' | 182 | ''' | ||
183 | 183 | | |||
184 | def execute(self): | 184 | def execute(self): | ||
Show All 23 Lines | 193 | try: | |||
208 | 208 | | |||
209 | task_vm.wait_for_port(self.port) | 209 | task_vm.wait_for_port(self.port) | ||
210 | 210 | | |||
211 | with remote_exec.ParamikoWrapper(self.machine, self.port, self.user, | 211 | with remote_exec.ParamikoWrapper(self.machine, self.port, self.user, | ||
212 | self.ssh_privkey, stdio_filename) as self.ssh: | 212 | self.ssh_privkey, stdio_filename) as self.ssh: | ||
213 | try: | 213 | try: | ||
214 | self._prepare_task() | 214 | self._prepare_task() | ||
215 | self._run() | 215 | self._run() | ||
216 | except exc.TaskotronRemoteTimeoutError: | ||||
217 | # do not try to get logs - the minion is likely dead and we would hang on | ||||
218 | # getting the logs, see https://phab.qadevel.cloud.fedoraproject.org/T665 | ||||
219 | raise | ||||
216 | except exc.TaskotronRemoteError: | 220 | except exc.TaskotronRemoteError: | ||
217 | # This is the case when remote execution failed but it should be possible to | 221 | # this is the case when remote execution failed but it should be possible to | ||
218 | # get logs from the minion, so try it. We don't try getting logs in case when | 222 | # get logs from the minion, so try it | ||
219 | # exc.TaskotronRemoteTimeoutError is thrown which means that the minion is | | |||
220 | # likely dead and we would hang on getting the logs. | | |||
221 | # See https://phab.qadevel.cloud.fedoraproject.org/T665 | | |||
222 | self._get_output() | 223 | self._get_output() | ||
223 | # propagate the exception into teardown | | |||
224 | raise | 224 | raise | ||
225 | 225 | else: | |||
226 | self._get_output() | 226 | self._get_output() | ||
227 | 227 | | |||
228 | execution_ok = True | 228 | execution_ok = True | ||
229 | finally: | 229 | finally: | ||
230 | if not execution_ok: | 230 | if not execution_ok: | ||
231 | log.warn('Task execution was interrupted by an error, doing emergency cleanup.') | 231 | log.warn('Task execution was interrupted by an error, doing emergency cleanup.') | ||
232 | 232 | | |||
233 | if self.arg_data['no_destroy']: | 233 | if self.arg_data['no_destroy']: | ||
234 | log.info('--no-destroy mode was set from command line, not tearing VM ' | 234 | log.info('--no-destroy mode was set from command line, not tearing VM ' | ||
Show All 17 Lines |
1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- | ||
---|---|---|---|---|---|
2 | # Copyright 2016, Red Hat, Inc. | 2 | # Copyright 2016, Red Hat, Inc. | ||
3 | # License: GPL-2.0+ <http://spdx.org/licenses/GPL-2.0+> | 3 | # License: GPL-2.0+ <http://spdx.org/licenses/GPL-2.0+> | ||
4 | # See the LICENSE file for more details on Licensing | 4 | # See the LICENSE file for more details on Licensing | ||
5 | 5 | | |||
6 | '''Unit tests for libtaskotron/minion.py''' | 6 | '''Unit tests for libtaskotron/minion.py''' | ||
7 | 7 | | |||
8 | import argparse | 8 | import argparse | ||
9 | import mock | 9 | import mock | ||
10 | import pytest | 10 | import pytest | ||
11 | import shlex | 11 | import shlex | ||
12 | 12 | | |||
13 | from libtaskotron import minion | 13 | from libtaskotron import minion | ||
14 | from libtaskotron import main | 14 | from libtaskotron import main | ||
15 | import libtaskotron.exceptions as exc | ||||
15 | 16 | | |||
16 | 17 | | |||
17 | @pytest.mark.usefixtures('setup') | 18 | @pytest.mark.usefixtures('setup') | ||
18 | class TestBaseMinion(object): | 19 | class TestBaseMinion(object): | ||
19 | 20 | | |||
20 | @pytest.fixture | 21 | @pytest.fixture | ||
21 | def setup(self, monkeypatch): | 22 | def setup(self): | ||
22 | self.ref_arg_data = {'artifactsdir': '/somedir/', | 23 | self.ref_arg_data = {'artifactsdir': '/somedir/', | ||
23 | 'task': '/path/mytask.yaml', | 24 | 'task': '/path/mytask.yaml', | ||
24 | 'ssh_privkey': None, | 25 | 'ssh_privkey': None, | ||
25 | '_orig_args': {'item': 'foo', | 26 | '_orig_args': {'item': 'foo', | ||
26 | 'type': 'koji_tag'}} | 27 | 'type': 'koji_tag'}} | ||
27 | self.bm = minion.BaseMinion(None, self.ref_arg_data) | 28 | self.bm = minion.BaseMinion(None, self.ref_arg_data) | ||
28 | self.bm.taskdir = '/minion/path/' | 29 | self.bm.taskdir = '/minion/path/' | ||
29 | self.mock_ssh = mock.Mock() | 30 | self.mock_ssh = mock.Mock() | ||
▲ Show 20 Lines • Show All 63 Lines • ▼ Show 20 Line(s) | 92 | def test_run_exclude_args(self): | |||
93 | self.bm._run() | 94 | self.bm._run() | ||
94 | cmd_calls = [call for call in self.mock_ssh.method_calls if call[0] == 'cmd'] | 95 | cmd_calls = [call for call in self.mock_ssh.method_calls if call[0] == 'cmd'] | ||
95 | pos_args = cmd_calls[0][1] | 96 | pos_args = cmd_calls[0][1] | ||
96 | cmdline = pos_args[0] | 97 | cmdline = pos_args[0] | ||
97 | elems = shlex.split(cmdline) | 98 | elems = shlex.split(cmdline) | ||
98 | assert '--item' not in elems | 99 | assert '--item' not in elems | ||
99 | assert 'foo' not in elems | 100 | assert 'foo' not in elems | ||
100 | assert '--type' in elems | 101 | assert '--type' in elems | ||
102 | | ||||
103 | | ||||
104 | @pytest.mark.usefixtures('setup') | ||||
105 | class TestRemoteMinion(object): | ||||
106 | '''A common class for running tests shared between all remote minions''' | ||||
107 | | ||||
108 | remote_minions = (minion.PersistentMinion, minion.DisposableMinion) | ||||
109 | | ||||
110 | # everything calls setup, and setup is parametrized, therefore everything gets run for all | ||||
111 | # remote minion classes | ||||
112 | @pytest.fixture(params=remote_minions) | ||||
113 | def setup(self, monkeypatch, request): | ||||
114 | minion_cls = request.param | ||||
115 | self.ref_arg_data = {'artifactsdir': '/somedir/', | ||||
116 | 'task': '/path/mytask.yaml', | ||||
117 | 'ssh_privkey': None, | ||||
118 | '_orig_args': {'item': 'foo', | ||||
119 | 'type': 'koji_tag'}, | ||||
120 | 'no_destroy': False, | ||||
121 | 'uuid': 1, | ||||
122 | } | ||||
123 | | ||||
124 | monkeypatch.setattr(minion, 'logger', mock.Mock()) | ||||
125 | monkeypatch.setattr(minion, 'log', mock.Mock()) | ||||
126 | monkeypatch.setattr(minion, 'remote_exec', mock.MagicMock()) | ||||
127 | monkeypatch.setattr(minion, 'taskformula', mock.MagicMock()) | ||||
128 | monkeypatch.setattr(minion, 'vm', mock.Mock()) | ||||
129 | | ||||
130 | self.mock_prepare = mock.Mock() | ||||
131 | self.mock_run = mock.Mock() | ||||
132 | self.mock_output = mock.Mock() | ||||
133 | self.mini = minion_cls(None, self.ref_arg_data) | ||||
134 | monkeypatch.setattr(self.mini, '_prepare_task', self.mock_prepare) | ||||
135 | monkeypatch.setattr(self.mini, '_run', self.mock_run) | ||||
136 | monkeypatch.setattr(self.mini, '_get_output', self.mock_output) | ||||
137 | | ||||
138 | def test_timeout_no_logs(self): | ||||
139 | '''don't gather logs if timeout is reached during execution''' | ||||
140 | self.mock_run.side_effect = exc.TaskotronRemoteTimeoutError() | ||||
141 | | ||||
142 | with pytest.raises(exc.TaskotronRemoteTimeoutError): | ||||
143 | self.mini.execute() | ||||
144 | | ||||
145 | assert self.mock_output.called is False | ||||
146 | | ||||
147 | def test_crash_get_logs(self): | ||||
148 | '''logs should be gathered after execution crash''' | ||||
149 | self.mock_run.side_effect = exc.TaskotronRemoteProcessError() | ||||
150 | | ||||
151 | with pytest.raises(exc.TaskotronRemoteProcessError): | ||||
152 | self.mini.execute() | ||||
153 | | ||||
154 | assert self.mock_output.call_count == 1 | ||||
155 | | ||||
156 | def test_no_crash_get_logs(self): | ||||
157 | '''logs should be gathered if execution doesn't crash''' | ||||
158 | self.mini.execute() | ||||
159 | assert self.mock_output.call_count == 1 |