|
1
|
|
|
# Licensed to the StackStorm, Inc ('StackStorm') under one or more |
|
2
|
|
|
# contributor license agreements. See the NOTICE file distributed with |
|
3
|
|
|
# this work for additional information regarding copyright ownership. |
|
4
|
|
|
# The ASF licenses this file to You under the Apache License, Version 2.0 |
|
5
|
|
|
# (the "License"); you may not use this file except in compliance with |
|
6
|
|
|
# the License. You may obtain a copy of the License at |
|
7
|
|
|
# |
|
8
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0 |
|
9
|
|
|
# |
|
10
|
|
|
# Unless required by applicable law or agreed to in writing, software |
|
11
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS, |
|
12
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
13
|
|
|
# See the License for the specific language governing permissions and |
|
14
|
|
|
# limitations under the License. |
|
15
|
|
|
|
|
16
|
|
|
import os |
|
17
|
|
|
import re |
|
18
|
|
|
|
|
19
|
|
|
import mock |
|
20
|
|
|
from oslo_config import cfg |
|
21
|
|
|
|
|
22
|
|
|
import python_runner |
|
23
|
|
|
from st2actions.container.base import RunnerContainer |
|
24
|
|
|
from st2common.runners.python_action_wrapper import PythonActionWrapper |
|
25
|
|
|
from st2common.runners.base_action import Action |
|
26
|
|
|
from st2common.runners.utils import get_action_class_instance |
|
27
|
|
|
from st2common.services import config as config_service |
|
28
|
|
|
from st2common.constants.action import ACTION_OUTPUT_RESULT_DELIMITER |
|
29
|
|
|
from st2common.constants.action import LIVEACTION_STATUS_SUCCEEDED, LIVEACTION_STATUS_FAILED |
|
30
|
|
|
from st2common.constants.action import LIVEACTION_STATUS_TIMED_OUT |
|
31
|
|
|
from st2common.constants.pack import SYSTEM_PACK_NAME |
|
32
|
|
|
from st2common.persistence.execution import ActionExecutionOutput |
|
33
|
|
|
from st2tests.base import RunnerTestCase |
|
34
|
|
|
from st2tests.base import CleanDbTestCase |
|
35
|
|
|
from st2tests.base import blocking_eventlet_spawn |
|
36
|
|
|
from st2tests.base import make_mock_stream_readline |
|
37
|
|
|
import st2tests.base as tests_base |
|
38
|
|
|
|
|
39
|
|
|
|
|
40
|
|
|
PASCAL_ROW_ACTION_PATH = os.path.join(tests_base.get_resources_path(), 'packs', |
|
41
|
|
|
'pythonactions/actions/pascal_row.py') |
|
42
|
|
|
TEST_ACTION_PATH = os.path.join(tests_base.get_resources_path(), 'packs', |
|
43
|
|
|
'pythonactions/actions/test.py') |
|
44
|
|
|
PATHS_ACTION_PATH = os.path.join(tests_base.get_resources_path(), 'packs', |
|
45
|
|
|
'pythonactions/actions/python_paths.py') |
|
46
|
|
|
ACTION_1_PATH = os.path.join(tests_base.get_fixtures_path(), |
|
47
|
|
|
'packs/dummy_pack_9/actions/list_repos_doesnt_exist.py') |
|
48
|
|
|
ACTION_2_PATH = os.path.join(tests_base.get_fixtures_path(), |
|
49
|
|
|
'packs/dummy_pack_9/actions/invalid_syntax.py') |
|
50
|
|
|
NON_SIMPLE_TYPE_ACTION = os.path.join(tests_base.get_resources_path(), 'packs', |
|
51
|
|
|
'pythonactions/actions/non_simple_type.py') |
|
52
|
|
|
|
|
53
|
|
|
# Note: runner inherits parent args which doesn't work with tests since test pass additional |
|
54
|
|
|
# unrecognized args |
|
55
|
|
|
mock_sys = mock.Mock() |
|
56
|
|
|
mock_sys.argv = [] |
|
57
|
|
|
|
|
58
|
|
|
MOCK_EXECUTION = mock.Mock() |
|
59
|
|
|
MOCK_EXECUTION.id = '598dbf0c0640fd54bffc688b' |
|
60
|
|
|
|
|
61
|
|
|
|
|
62
|
|
|
@mock.patch('python_runner.sys', mock_sys) |
|
63
|
|
|
class PythonRunnerTestCase(RunnerTestCase, CleanDbTestCase): |
|
64
|
|
|
register_packs = True |
|
65
|
|
|
register_pack_configs = True |
|
66
|
|
|
|
|
67
|
|
|
def test_runner_creation(self): |
|
68
|
|
|
runner = python_runner.get_runner() |
|
69
|
|
|
self.assertTrue(runner is not None, 'Creation failed. No instance.') |
|
70
|
|
|
self.assertEqual(type(runner), python_runner.PythonRunner, 'Creation failed. No instance.') |
|
71
|
|
|
|
|
72
|
|
|
def test_action_returns_non_serializable_result(self): |
|
73
|
|
|
# Actions returns non-simple type which can't be serialized, verify result is simple str() |
|
74
|
|
|
# representation of the result |
|
75
|
|
|
runner = self._get_mock_runner_obj() |
|
76
|
|
|
runner.entry_point = NON_SIMPLE_TYPE_ACTION |
|
77
|
|
|
runner.pre_run() |
|
78
|
|
|
(status, output, _) = runner.run({}) |
|
79
|
|
|
|
|
80
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) |
|
81
|
|
|
self.assertTrue(output is not None) |
|
82
|
|
|
|
|
83
|
|
|
expected_result_re = (r"\[{'a': '1'}, {'h': 3, 'c': 2}, {'e': " |
|
84
|
|
|
"<non_simple_type.Test object at .*?>}\]") |
|
|
|
|
|
|
85
|
|
|
match = re.match(expected_result_re, output['result']) |
|
86
|
|
|
self.assertTrue(match) |
|
87
|
|
|
|
|
88
|
|
|
def test_simple_action_with_result_no_status(self): |
|
89
|
|
|
runner = self._get_mock_runner_obj() |
|
90
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
91
|
|
|
runner.pre_run() |
|
92
|
|
|
(status, output, _) = runner.run({'row_index': 5}) |
|
93
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) |
|
94
|
|
|
self.assertTrue(output is not None) |
|
95
|
|
|
self.assertEqual(output['result'], [1, 5, 10, 10, 5, 1]) |
|
96
|
|
|
|
|
97
|
|
|
def test_simple_action_with_result_as_None_no_status(self): |
|
98
|
|
|
runner = self._get_mock_runner_obj() |
|
99
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
100
|
|
|
runner.pre_run() |
|
101
|
|
|
(status, output, _) = runner.run({'row_index': 'b'}) |
|
102
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) |
|
103
|
|
|
self.assertTrue(output is not None) |
|
104
|
|
|
self.assertEqual(output['exit_code'], 0) |
|
105
|
|
|
self.assertEqual(output['result'], None) |
|
106
|
|
|
|
|
107
|
|
|
def test_simple_action_timeout(self): |
|
108
|
|
|
timeout = 0 |
|
109
|
|
|
runner = self._get_mock_runner_obj() |
|
110
|
|
|
runner.runner_parameters = {python_runner.RUNNER_TIMEOUT: timeout} |
|
111
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
112
|
|
|
runner.pre_run() |
|
113
|
|
|
(status, output, _) = runner.run({'row_index': 4}) |
|
114
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_TIMED_OUT) |
|
115
|
|
|
self.assertTrue(output is not None) |
|
116
|
|
|
self.assertEqual(output['result'], 'None') |
|
117
|
|
|
self.assertEqual(output['error'], 'Action failed to complete in 0 seconds') |
|
118
|
|
|
self.assertEqual(output['exit_code'], -9) |
|
119
|
|
|
|
|
120
|
|
|
def test_simple_action_with_status_succeeded(self): |
|
121
|
|
|
runner = self._get_mock_runner_obj() |
|
122
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
123
|
|
|
runner.pre_run() |
|
124
|
|
|
(status, output, _) = runner.run({'row_index': 4}) |
|
125
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) |
|
126
|
|
|
self.assertTrue(output is not None) |
|
127
|
|
|
self.assertEqual(output['result'], [1, 4, 6, 4, 1]) |
|
128
|
|
|
|
|
129
|
|
|
def test_simple_action_with_status_failed(self): |
|
130
|
|
|
runner = self._get_mock_runner_obj() |
|
131
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
132
|
|
|
runner.pre_run() |
|
133
|
|
|
(status, output, _) = runner.run({'row_index': 'a'}) |
|
134
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_FAILED) |
|
135
|
|
|
self.assertTrue(output is not None) |
|
136
|
|
|
self.assertEqual(output['result'], "This is suppose to fail don't worry!!") |
|
137
|
|
|
|
|
138
|
|
|
def test_simple_action_with_status_complex_type_returned_for_result(self): |
|
139
|
|
|
# Result containing a complex type shouldn't break the returning a tuple with status |
|
140
|
|
|
# behavior |
|
141
|
|
|
runner = self._get_mock_runner_obj() |
|
142
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
143
|
|
|
runner.pre_run() |
|
144
|
|
|
(status, output, _) = runner.run({'row_index': 'complex_type'}) |
|
145
|
|
|
|
|
146
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_FAILED) |
|
147
|
|
|
self.assertTrue(output is not None) |
|
148
|
|
|
self.assertTrue('<pascal_row.PascalRowAction object at' in output['result']) |
|
149
|
|
|
|
|
150
|
|
|
def test_simple_action_with_status_failed_result_none(self): |
|
151
|
|
|
runner = self._get_mock_runner_obj() |
|
152
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
153
|
|
|
runner.pre_run() |
|
154
|
|
|
(status, output, _) = runner.run({'row_index': 'c'}) |
|
155
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_FAILED) |
|
156
|
|
|
self.assertTrue(output is not None) |
|
157
|
|
|
self.assertEqual(output['result'], None) |
|
158
|
|
|
|
|
159
|
|
|
def test_exception_in_simple_action_with_invalid_status(self): |
|
160
|
|
|
runner = self._get_mock_runner_obj() |
|
161
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
162
|
|
|
runner.pre_run() |
|
163
|
|
|
self.assertRaises(ValueError, |
|
164
|
|
|
runner.run, action_parameters={'row_index': 'd'}) |
|
165
|
|
|
|
|
166
|
|
|
def test_simple_action_no_status_backward_compatibility(self): |
|
167
|
|
|
runner = self._get_mock_runner_obj() |
|
168
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
169
|
|
|
runner.pre_run() |
|
170
|
|
|
(status, output, _) = runner.run({'row_index': 'e'}) |
|
171
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) |
|
172
|
|
|
self.assertTrue(output is not None) |
|
173
|
|
|
self.assertEqual(output['result'], [1, 2]) |
|
174
|
|
|
|
|
175
|
|
|
def test_simple_action_config_value_provided_overriden_in_datastore(self): |
|
176
|
|
|
pack = 'dummy_pack_5' |
|
177
|
|
|
user = 'joe' |
|
178
|
|
|
|
|
179
|
|
|
# No values provided in the datastore |
|
180
|
|
|
runner = self._get_mock_runner_obj_from_container(pack=pack, user=user) |
|
181
|
|
|
|
|
182
|
|
|
self.assertEqual(runner._config['api_key'], 'some_api_key') # static value |
|
|
|
|
|
|
183
|
|
|
self.assertEqual(runner._config['regions'], ['us-west-1']) # static value |
|
|
|
|
|
|
184
|
|
|
self.assertEqual(runner._config['api_secret'], None) |
|
|
|
|
|
|
185
|
|
|
self.assertEqual(runner._config['private_key_path'], None) |
|
|
|
|
|
|
186
|
|
|
|
|
187
|
|
|
# api_secret overriden in the datastore (user scoped value) |
|
188
|
|
|
config_service.set_datastore_value_for_config_key(pack_name='dummy_pack_5', |
|
189
|
|
|
key_name='api_secret', |
|
190
|
|
|
user='joe', |
|
191
|
|
|
value='foosecret', |
|
192
|
|
|
secret=True) |
|
193
|
|
|
|
|
194
|
|
|
# private_key_path overriden in the datastore (global / non-user scoped value) |
|
195
|
|
|
config_service.set_datastore_value_for_config_key(pack_name='dummy_pack_5', |
|
196
|
|
|
key_name='private_key_path', |
|
197
|
|
|
value='foopath') |
|
198
|
|
|
|
|
199
|
|
|
runner = self._get_mock_runner_obj_from_container(pack=pack, user=user) |
|
200
|
|
|
self.assertEqual(runner._config['api_key'], 'some_api_key') # static value |
|
|
|
|
|
|
201
|
|
|
self.assertEqual(runner._config['regions'], ['us-west-1']) # static value |
|
|
|
|
|
|
202
|
|
|
self.assertEqual(runner._config['api_secret'], 'foosecret') |
|
|
|
|
|
|
203
|
|
|
self.assertEqual(runner._config['private_key_path'], 'foopath') |
|
|
|
|
|
|
204
|
|
|
|
|
205
|
|
|
def test_simple_action_fail(self): |
|
206
|
|
|
runner = self._get_mock_runner_obj() |
|
207
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
208
|
|
|
runner.pre_run() |
|
209
|
|
|
(status, result, _) = runner.run({'row_index': '4'}) |
|
210
|
|
|
self.assertTrue(result is not None) |
|
211
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_FAILED) |
|
212
|
|
|
|
|
213
|
|
|
def test_simple_action_no_file(self): |
|
214
|
|
|
runner = self._get_mock_runner_obj() |
|
215
|
|
|
runner.entry_point = 'foo.py' |
|
216
|
|
|
runner.pre_run() |
|
217
|
|
|
(status, result, _) = runner.run({}) |
|
218
|
|
|
self.assertTrue(result is not None) |
|
219
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_FAILED) |
|
220
|
|
|
|
|
221
|
|
|
def test_simple_action_no_entry_point(self): |
|
222
|
|
|
runner = self._get_mock_runner_obj() |
|
223
|
|
|
runner.entry_point = '' |
|
224
|
|
|
|
|
225
|
|
|
expected_msg = 'Action .*? is missing entry_point attribute' |
|
226
|
|
|
self.assertRaisesRegexp(Exception, expected_msg, runner.run, {}) |
|
227
|
|
|
|
|
228
|
|
|
@mock.patch('st2common.util.green.shell.subprocess.Popen') |
|
229
|
|
|
def test_action_with_user_supplied_env_vars(self, mock_popen): |
|
230
|
|
|
env_vars = {'key1': 'val1', 'key2': 'val2', 'PYTHONPATH': 'foobar'} |
|
231
|
|
|
|
|
232
|
|
|
mock_process = mock.Mock() |
|
233
|
|
|
mock_process.communicate.return_value = ('', '') |
|
234
|
|
|
mock_popen.return_value = mock_process |
|
235
|
|
|
|
|
236
|
|
|
runner = self._get_mock_runner_obj() |
|
237
|
|
|
runner.runner_parameters = {'env': env_vars} |
|
238
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
239
|
|
|
runner.pre_run() |
|
240
|
|
|
(_, _, _) = runner.run({'row_index': 4}) |
|
241
|
|
|
|
|
242
|
|
|
_, call_kwargs = mock_popen.call_args |
|
243
|
|
|
actual_env = call_kwargs['env'] |
|
244
|
|
|
|
|
245
|
|
|
for key, value in env_vars.items(): |
|
246
|
|
|
# Verify that a blacklsited PYTHONPATH has been filtered out |
|
247
|
|
|
if key == 'PYTHONPATH': |
|
248
|
|
|
self.assertTrue(actual_env[key] != value) |
|
249
|
|
|
else: |
|
250
|
|
|
self.assertEqual(actual_env[key], value) |
|
251
|
|
|
|
|
252
|
|
|
@mock.patch('st2common.util.green.shell.subprocess.Popen') |
|
253
|
|
|
@mock.patch('st2common.util.green.shell.eventlet.spawn') |
|
254
|
|
|
def test_action_stdout_and_stderr_is_not_stored_in_db_by_default(self, mock_spawn, mock_popen): |
|
255
|
|
|
# Feature should be disabled by default |
|
256
|
|
|
values = {'delimiter': ACTION_OUTPUT_RESULT_DELIMITER} |
|
257
|
|
|
|
|
258
|
|
|
# Note: We need to mock spawn function so we can test everything in single event loop |
|
259
|
|
|
# iteration |
|
260
|
|
|
mock_spawn.side_effect = blocking_eventlet_spawn |
|
261
|
|
|
|
|
262
|
|
|
# No output to stdout and no result (implicit None) |
|
263
|
|
|
mock_stdout = [ |
|
264
|
|
|
'pre result line 1\n', |
|
265
|
|
|
'%(delimiter)sTrue%(delimiter)s' % values, |
|
266
|
|
|
'post result line 1' |
|
267
|
|
|
] |
|
268
|
|
|
mock_stderr = [ |
|
269
|
|
|
'stderr line 1\n', |
|
270
|
|
|
'stderr line 2\n', |
|
271
|
|
|
'stderr line 3\n' |
|
272
|
|
|
] |
|
273
|
|
|
|
|
274
|
|
|
mock_process = mock.Mock() |
|
275
|
|
|
mock_process.returncode = 0 |
|
276
|
|
|
mock_popen.return_value = mock_process |
|
277
|
|
|
mock_process.stdout.closed = False |
|
278
|
|
|
mock_process.stderr.closed = False |
|
279
|
|
|
mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout, |
|
280
|
|
|
stop_counter=3) |
|
281
|
|
|
mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr, |
|
282
|
|
|
stop_counter=3) |
|
283
|
|
|
|
|
284
|
|
|
runner = self._get_mock_runner_obj() |
|
285
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
286
|
|
|
runner.pre_run() |
|
287
|
|
|
(_, output, _) = runner.run({'row_index': 4}) |
|
288
|
|
|
|
|
289
|
|
|
self.assertEqual(output['stdout'], 'pre result line 1\npost result line 1') |
|
290
|
|
|
self.assertEqual(output['stderr'], 'stderr line 1\nstderr line 2\nstderr line 3\n') |
|
291
|
|
|
self.assertEqual(output['result'], 'True') |
|
292
|
|
|
self.assertEqual(output['exit_code'], 0) |
|
293
|
|
|
|
|
294
|
|
|
output_dbs = ActionExecutionOutput.get_all() |
|
295
|
|
|
self.assertEqual(len(output_dbs), 0) |
|
296
|
|
|
|
|
297
|
|
|
# False is a default behavior so end result should be the same |
|
298
|
|
|
cfg.CONF.set_override(name='stream_output', group='actionrunner', override=False) |
|
299
|
|
|
|
|
300
|
|
|
mock_process = mock.Mock() |
|
301
|
|
|
mock_process.returncode = 0 |
|
302
|
|
|
mock_popen.return_value = mock_process |
|
303
|
|
|
mock_process.stdout.closed = False |
|
304
|
|
|
mock_process.stderr.closed = False |
|
305
|
|
|
mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout, |
|
306
|
|
|
stop_counter=3) |
|
307
|
|
|
mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr, |
|
308
|
|
|
stop_counter=3) |
|
309
|
|
|
|
|
310
|
|
|
runner.pre_run() |
|
311
|
|
|
(_, output, _) = runner.run({'row_index': 4}) |
|
312
|
|
|
|
|
313
|
|
|
self.assertEqual(output['stdout'], 'pre result line 1\npost result line 1') |
|
314
|
|
|
self.assertEqual(output['stderr'], 'stderr line 1\nstderr line 2\nstderr line 3\n') |
|
315
|
|
|
self.assertEqual(output['result'], 'True') |
|
316
|
|
|
self.assertEqual(output['exit_code'], 0) |
|
317
|
|
|
|
|
318
|
|
|
output_dbs = ActionExecutionOutput.get_all() |
|
319
|
|
|
self.assertEqual(len(output_dbs), 0) |
|
320
|
|
|
|
|
321
|
|
|
@mock.patch('st2common.util.green.shell.subprocess.Popen') |
|
322
|
|
|
@mock.patch('st2common.util.green.shell.eventlet.spawn') |
|
323
|
|
|
def test_action_stdout_and_stderr_is_stored_in_the_db(self, mock_spawn, mock_popen): |
|
324
|
|
|
# Feature is enabled |
|
325
|
|
|
cfg.CONF.set_override(name='stream_output', group='actionrunner', override=True) |
|
326
|
|
|
|
|
327
|
|
|
values = {'delimiter': ACTION_OUTPUT_RESULT_DELIMITER} |
|
328
|
|
|
|
|
329
|
|
|
# Note: We need to mock spawn function so we can test everything in single event loop |
|
330
|
|
|
# iteration |
|
331
|
|
|
mock_spawn.side_effect = blocking_eventlet_spawn |
|
332
|
|
|
|
|
333
|
|
|
# No output to stdout and no result (implicit None) |
|
334
|
|
|
mock_stdout = [ |
|
335
|
|
|
'pre result line 1\n', |
|
336
|
|
|
'pre result line 2\n', |
|
337
|
|
|
'%(delimiter)sTrue%(delimiter)s' % values, |
|
338
|
|
|
'post result line 1' |
|
339
|
|
|
] |
|
340
|
|
|
mock_stderr = [ |
|
341
|
|
|
'stderr line 1\n', |
|
342
|
|
|
'stderr line 2\n', |
|
343
|
|
|
'stderr line 3\n' |
|
344
|
|
|
] |
|
345
|
|
|
|
|
346
|
|
|
mock_process = mock.Mock() |
|
347
|
|
|
mock_process.returncode = 0 |
|
348
|
|
|
mock_popen.return_value = mock_process |
|
349
|
|
|
mock_process.stdout.closed = False |
|
350
|
|
|
mock_process.stderr.closed = False |
|
351
|
|
|
mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout, |
|
352
|
|
|
stop_counter=4) |
|
353
|
|
|
mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr, |
|
354
|
|
|
stop_counter=3) |
|
355
|
|
|
|
|
356
|
|
|
runner = self._get_mock_runner_obj() |
|
357
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
358
|
|
|
runner.pre_run() |
|
359
|
|
|
(_, output, _) = runner.run({'row_index': 4}) |
|
360
|
|
|
|
|
361
|
|
|
self.assertEqual(output['stdout'], |
|
362
|
|
|
'pre result line 1\npre result line 2\npost result line 1') |
|
363
|
|
|
self.assertEqual(output['stderr'], 'stderr line 1\nstderr line 2\nstderr line 3\n') |
|
364
|
|
|
self.assertEqual(output['result'], 'True') |
|
365
|
|
|
self.assertEqual(output['exit_code'], 0) |
|
366
|
|
|
|
|
367
|
|
|
# Verify stdout and stderr lines have been correctly stored in the db |
|
368
|
|
|
# Note - result delimiter should not be stored in the db |
|
369
|
|
|
output_dbs = ActionExecutionOutput.query(output_type='stdout') |
|
370
|
|
|
self.assertEqual(len(output_dbs), 3) |
|
371
|
|
|
self.assertEqual(output_dbs[0].runner_ref, 'python-script') |
|
372
|
|
|
self.assertEqual(output_dbs[0].data, mock_stdout[0]) |
|
373
|
|
|
self.assertEqual(output_dbs[1].data, mock_stdout[1]) |
|
374
|
|
|
self.assertEqual(output_dbs[2].data, mock_stdout[3]) |
|
375
|
|
|
|
|
376
|
|
|
output_dbs = ActionExecutionOutput.query(output_type='stderr') |
|
377
|
|
|
self.assertEqual(len(output_dbs), 3) |
|
378
|
|
|
self.assertEqual(output_dbs[0].runner_ref, 'python-script') |
|
379
|
|
|
self.assertEqual(output_dbs[0].data, mock_stderr[0]) |
|
380
|
|
|
self.assertEqual(output_dbs[1].data, mock_stderr[1]) |
|
381
|
|
|
self.assertEqual(output_dbs[2].data, mock_stderr[2]) |
|
382
|
|
|
|
|
383
|
|
|
@mock.patch('st2common.util.green.shell.subprocess.Popen') |
|
384
|
|
|
def test_stdout_interception_and_parsing(self, mock_popen): |
|
385
|
|
|
values = {'delimiter': ACTION_OUTPUT_RESULT_DELIMITER} |
|
386
|
|
|
|
|
387
|
|
|
# No output to stdout and no result (implicit None) |
|
388
|
|
|
mock_stdout = ['%(delimiter)sNone%(delimiter)s' % values] |
|
389
|
|
|
mock_stderr = ['foo stderr'] |
|
390
|
|
|
|
|
391
|
|
|
mock_process = mock.Mock() |
|
392
|
|
|
mock_process.returncode = 0 |
|
393
|
|
|
mock_popen.return_value = mock_process |
|
394
|
|
|
mock_process.stdout.closed = False |
|
395
|
|
|
mock_process.stderr.closed = False |
|
396
|
|
|
mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout) |
|
397
|
|
|
mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr) |
|
398
|
|
|
|
|
399
|
|
|
runner = self._get_mock_runner_obj() |
|
400
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
401
|
|
|
runner.pre_run() |
|
402
|
|
|
(_, output, _) = runner.run({'row_index': 4}) |
|
403
|
|
|
|
|
404
|
|
|
self.assertEqual(output['stdout'], '') |
|
405
|
|
|
self.assertEqual(output['stderr'], mock_stderr[0]) |
|
406
|
|
|
self.assertEqual(output['result'], 'None') |
|
407
|
|
|
self.assertEqual(output['exit_code'], 0) |
|
408
|
|
|
|
|
409
|
|
|
# Output to stdout, no result (implicit None), return_code 1 and status failed |
|
410
|
|
|
mock_stdout = ['pre result%(delimiter)sNone%(delimiter)spost result' % values] |
|
411
|
|
|
mock_stderr = ['foo stderr'] |
|
412
|
|
|
|
|
413
|
|
|
mock_process = mock.Mock() |
|
414
|
|
|
mock_process.returncode = 1 |
|
415
|
|
|
mock_popen.return_value = mock_process |
|
416
|
|
|
mock_process.stdout.closed = False |
|
417
|
|
|
mock_process.stderr.closed = False |
|
418
|
|
|
mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout) |
|
419
|
|
|
mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr) |
|
420
|
|
|
|
|
421
|
|
|
runner = self._get_mock_runner_obj() |
|
422
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
423
|
|
|
runner.pre_run() |
|
424
|
|
|
(status, output, _) = runner.run({'row_index': 4}) |
|
425
|
|
|
|
|
426
|
|
|
self.assertEqual(output['stdout'], 'pre resultpost result') |
|
427
|
|
|
self.assertEqual(output['stderr'], mock_stderr[0]) |
|
428
|
|
|
self.assertEqual(output['result'], 'None') |
|
429
|
|
|
self.assertEqual(output['exit_code'], 1) |
|
430
|
|
|
self.assertEqual(status, 'failed') |
|
431
|
|
|
|
|
432
|
|
|
# Output to stdout, no result (implicit None), return_code 1 and status succeeded |
|
433
|
|
|
mock_stdout = ['pre result%(delimiter)sNone%(delimiter)spost result' % values] |
|
434
|
|
|
mock_stderr = ['foo stderr'] |
|
435
|
|
|
|
|
436
|
|
|
mock_process = mock.Mock() |
|
437
|
|
|
mock_process.returncode = 0 |
|
438
|
|
|
mock_popen.return_value = mock_process |
|
439
|
|
|
mock_process.stdout.closed = False |
|
440
|
|
|
mock_process.stderr.closed = False |
|
441
|
|
|
mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout) |
|
442
|
|
|
mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr) |
|
443
|
|
|
|
|
444
|
|
|
runner = self._get_mock_runner_obj() |
|
445
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
446
|
|
|
runner.pre_run() |
|
447
|
|
|
(status, output, _) = runner.run({'row_index': 4}) |
|
448
|
|
|
|
|
449
|
|
|
self.assertEqual(output['stdout'], 'pre resultpost result') |
|
450
|
|
|
self.assertEqual(output['stderr'], mock_stderr[0]) |
|
451
|
|
|
self.assertEqual(output['result'], 'None') |
|
452
|
|
|
self.assertEqual(output['exit_code'], 0) |
|
453
|
|
|
self.assertEqual(status, 'succeeded') |
|
454
|
|
|
|
|
455
|
|
|
@mock.patch('st2common.util.green.shell.subprocess.Popen') |
|
456
|
|
|
def test_common_st2_env_vars_are_available_to_the_action(self, mock_popen): |
|
457
|
|
|
mock_process = mock.Mock() |
|
458
|
|
|
mock_process.communicate.return_value = ('', '') |
|
459
|
|
|
mock_popen.return_value = mock_process |
|
460
|
|
|
|
|
461
|
|
|
runner = self._get_mock_runner_obj() |
|
462
|
|
|
runner.auth_token = mock.Mock() |
|
463
|
|
|
runner.auth_token.token = 'ponies' |
|
464
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
|
465
|
|
|
runner.pre_run() |
|
466
|
|
|
(_, _, _) = runner.run({'row_index': 4}) |
|
467
|
|
|
|
|
468
|
|
|
_, call_kwargs = mock_popen.call_args |
|
469
|
|
|
actual_env = call_kwargs['env'] |
|
470
|
|
|
self.assertCommonSt2EnvVarsAvailableInEnv(env=actual_env) |
|
471
|
|
|
|
|
472
|
|
|
def test_action_class_instantiation_action_service_argument(self): |
|
473
|
|
|
class Action1(Action): |
|
474
|
|
|
# Constructor not overriden so no issue here |
|
475
|
|
|
pass |
|
|
|
|
|
|
476
|
|
|
|
|
477
|
|
|
def run(self): |
|
478
|
|
|
pass |
|
479
|
|
|
|
|
480
|
|
|
class Action2(Action): |
|
481
|
|
|
# Constructor overriden, but takes action_service argument |
|
482
|
|
|
def __init__(self, config, action_service=None): |
|
483
|
|
|
super(Action2, self).__init__(config=config, |
|
484
|
|
|
action_service=action_service) |
|
485
|
|
|
|
|
486
|
|
|
def run(self): |
|
487
|
|
|
pass |
|
488
|
|
|
|
|
489
|
|
|
class Action3(Action): |
|
490
|
|
|
# Constructor overriden, but doesn't take to action service |
|
491
|
|
|
def __init__(self, config): |
|
492
|
|
|
super(Action3, self).__init__(config=config) |
|
493
|
|
|
|
|
494
|
|
|
def run(self): |
|
495
|
|
|
pass |
|
496
|
|
|
|
|
497
|
|
|
config = {'a': 1, 'b': 2} |
|
498
|
|
|
action_service = 'ActionService!' |
|
499
|
|
|
|
|
500
|
|
|
action1 = get_action_class_instance(action_cls=Action1, config=config, |
|
501
|
|
|
action_service=action_service) |
|
502
|
|
|
self.assertEqual(action1.config, config) |
|
503
|
|
|
self.assertEqual(action1.action_service, action_service) |
|
504
|
|
|
|
|
505
|
|
|
action2 = get_action_class_instance(action_cls=Action2, config=config, |
|
506
|
|
|
action_service=action_service) |
|
507
|
|
|
self.assertEqual(action2.config, config) |
|
508
|
|
|
self.assertEqual(action2.action_service, action_service) |
|
509
|
|
|
|
|
510
|
|
|
action3 = get_action_class_instance(action_cls=Action3, config=config, |
|
511
|
|
|
action_service=action_service) |
|
512
|
|
|
self.assertEqual(action3.config, config) |
|
513
|
|
|
self.assertEqual(action3.action_service, action_service) |
|
514
|
|
|
|
|
515
|
|
|
def test_action_with_same_module_name_as_module_in_stdlib(self): |
|
516
|
|
|
runner = self._get_mock_runner_obj() |
|
517
|
|
|
runner.entry_point = TEST_ACTION_PATH |
|
518
|
|
|
runner.pre_run() |
|
519
|
|
|
(status, output, _) = runner.run({}) |
|
520
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) |
|
521
|
|
|
self.assertTrue(output is not None) |
|
522
|
|
|
self.assertEqual(output['result'], 'test action') |
|
523
|
|
|
|
|
524
|
|
|
def test_python_action_wrapper_script_doesnt_get_added_to_sys_path(self): |
|
525
|
|
|
# Validate that the directory where python_action_wrapper.py script is located |
|
526
|
|
|
# (st2common/runners) doesn't get added to sys.path |
|
527
|
|
|
runner = self._get_mock_runner_obj() |
|
528
|
|
|
runner.entry_point = PATHS_ACTION_PATH |
|
529
|
|
|
runner.pre_run() |
|
530
|
|
|
(status, output, _) = runner.run({}) |
|
531
|
|
|
|
|
532
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) |
|
533
|
|
|
self.assertTrue(output is not None) |
|
534
|
|
|
|
|
535
|
|
|
lines = output['stdout'].split('\n') |
|
536
|
|
|
process_sys_path = lines[0] |
|
537
|
|
|
process_pythonpath = lines[1] |
|
538
|
|
|
|
|
539
|
|
|
assert 'sys.path' in process_sys_path |
|
540
|
|
|
assert 'PYTHONPATH' in process_pythonpath |
|
541
|
|
|
|
|
542
|
|
|
wrapper_script_path = 'st2common/runners' |
|
543
|
|
|
|
|
544
|
|
|
assertion_msg = 'Found python wrapper script path in subprocess path' |
|
545
|
|
|
self.assertTrue(wrapper_script_path not in process_sys_path, assertion_msg) |
|
546
|
|
|
self.assertTrue(wrapper_script_path not in process_pythonpath, assertion_msg) |
|
547
|
|
|
|
|
548
|
|
|
def test_python_action_wrapper_action_script_file_doesnt_exist_friendly_error(self): |
|
549
|
|
|
# File in a directory which is not a Python package |
|
550
|
|
|
wrapper = PythonActionWrapper(pack='dummy_pack_5', file_path='/tmp/doesnt.exist', |
|
551
|
|
|
user='joe') |
|
552
|
|
|
|
|
553
|
|
|
expected_msg = 'File "/tmp/doesnt.exist" has no action class or the file doesn\'t exist.' |
|
554
|
|
|
self.assertRaisesRegexp(Exception, expected_msg, wrapper._get_action_instance) |
|
|
|
|
|
|
555
|
|
|
|
|
556
|
|
|
# File in a directory which is a Python package |
|
557
|
|
|
wrapper = PythonActionWrapper(pack='dummy_pack_5', file_path=ACTION_1_PATH, |
|
558
|
|
|
user='joe') |
|
559
|
|
|
|
|
560
|
|
|
expected_msg = ('Failed to load action class from file ".*?list_repos_doesnt_exist.py" ' |
|
561
|
|
|
'\(action file most likely doesn\'t exist or contains invalid syntax\): ' |
|
|
|
|
|
|
562
|
|
|
'\[Errno 2\] No such file or directory') |
|
|
|
|
|
|
563
|
|
|
self.assertRaisesRegexp(Exception, expected_msg, wrapper._get_action_instance) |
|
|
|
|
|
|
564
|
|
|
|
|
565
|
|
|
def test_python_action_wrapper_action_script_file_contains_invalid_syntax_friendly_error(self): |
|
566
|
|
|
wrapper = PythonActionWrapper(pack='dummy_pack_5', file_path=ACTION_2_PATH, |
|
567
|
|
|
user='joe') |
|
568
|
|
|
expected_msg = ('Failed to load action class from file ".*?invalid_syntax.py" ' |
|
569
|
|
|
'\(action file most likely doesn\'t exist or contains invalid syntax\): ' |
|
|
|
|
|
|
570
|
|
|
'No module named invalid') |
|
571
|
|
|
self.assertRaisesRegexp(Exception, expected_msg, wrapper._get_action_instance) |
|
|
|
|
|
|
572
|
|
|
|
|
573
|
|
|
def _get_mock_runner_obj(self): |
|
574
|
|
|
runner = python_runner.get_runner() |
|
575
|
|
|
runner.execution = MOCK_EXECUTION |
|
576
|
|
|
runner.action = self._get_mock_action_obj() |
|
577
|
|
|
runner.runner_parameters = {} |
|
578
|
|
|
|
|
579
|
|
|
return runner |
|
580
|
|
|
|
|
581
|
|
|
@mock.patch('st2actions.container.base.ActionExecution.get', mock.Mock()) |
|
582
|
|
|
def _get_mock_runner_obj_from_container(self, pack, user): |
|
583
|
|
|
container = RunnerContainer() |
|
584
|
|
|
|
|
585
|
|
|
runnertype_db = mock.Mock() |
|
586
|
|
|
runnertype_db.runner_module = 'python_runner' |
|
587
|
|
|
action_db = mock.Mock() |
|
588
|
|
|
action_db.pack = pack |
|
589
|
|
|
action_db.entry_point = 'foo.py' |
|
590
|
|
|
liveaction_db = mock.Mock() |
|
591
|
|
|
liveaction_db.id = '123' |
|
592
|
|
|
liveaction_db.context = {'user': user} |
|
593
|
|
|
runner = container._get_runner(runnertype_db=runnertype_db, action_db=action_db, |
|
|
|
|
|
|
594
|
|
|
liveaction_db=liveaction_db) |
|
595
|
|
|
runner.execution = MOCK_EXECUTION |
|
596
|
|
|
runner.action = self._get_mock_action_obj() |
|
597
|
|
|
runner.runner_parameters = {} |
|
598
|
|
|
|
|
599
|
|
|
return runner |
|
600
|
|
|
|
|
601
|
|
|
def _get_mock_action_obj(self): |
|
602
|
|
|
""" |
|
603
|
|
|
Return mock action object. |
|
604
|
|
|
|
|
605
|
|
|
Pack gets set to the system pack so the action doesn't require a separate virtualenv. |
|
606
|
|
|
""" |
|
607
|
|
|
action = mock.Mock() |
|
608
|
|
|
action.ref = 'dummy.action' |
|
609
|
|
|
action.pack = SYSTEM_PACK_NAME |
|
610
|
|
|
action.entry_point = 'foo.py' |
|
611
|
|
|
action.runner_type = { |
|
612
|
|
|
'name': 'python-script' |
|
613
|
|
|
} |
|
614
|
|
|
return action |
|
615
|
|
|
|
Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with
rorRare they interpreted as regular expressions.The escape sequence that was used indicates that you might have intended to write a regular expression.
Learn more about the available escape sequences. in the Python documentation.