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
|
|
|
|
18
|
|
|
import mock |
19
|
|
|
|
20
|
|
|
import python_runner |
21
|
|
|
from st2common.runners.python_action_wrapper import PythonActionWrapper |
22
|
|
|
from st2common.runners.base_action import Action |
23
|
|
|
from st2actions.container import service |
24
|
|
|
from st2common.runners.utils import get_action_class_instance |
25
|
|
|
from st2common.services import config as config_service |
26
|
|
|
from st2common.constants.action import ACTION_OUTPUT_RESULT_DELIMITER |
27
|
|
|
from st2common.constants.action import LIVEACTION_STATUS_SUCCEEDED, LIVEACTION_STATUS_FAILED |
28
|
|
|
from st2common.constants.action import LIVEACTION_STATUS_TIMED_OUT |
29
|
|
|
from st2common.constants.pack import SYSTEM_PACK_NAME |
30
|
|
|
from st2tests.base import RunnerTestCase |
31
|
|
|
from st2tests.base import CleanDbTestCase |
32
|
|
|
import st2tests.base as tests_base |
33
|
|
|
|
34
|
|
|
|
35
|
|
|
PASCAL_ROW_ACTION_PATH = os.path.join(tests_base.get_resources_path(), 'packs', |
36
|
|
|
'pythonactions/actions/pascal_row.py') |
37
|
|
|
TEST_ACTION_PATH = os.path.join(tests_base.get_resources_path(), 'packs', |
38
|
|
|
'pythonactions/actions/test.py') |
39
|
|
|
|
40
|
|
|
# Note: runner inherits parent args which doesn't work with tests since test pass additional |
41
|
|
|
# unrecognized args |
42
|
|
|
mock_sys = mock.Mock() |
43
|
|
|
mock_sys.argv = [] |
44
|
|
|
|
45
|
|
|
|
46
|
|
|
@mock.patch('python_runner.sys', mock_sys) |
47
|
|
|
class PythonRunnerTestCase(RunnerTestCase, CleanDbTestCase): |
48
|
|
|
register_packs = True |
49
|
|
|
register_pack_configs = True |
50
|
|
|
|
51
|
|
|
def test_runner_creation(self): |
52
|
|
|
runner = python_runner.get_runner() |
53
|
|
|
self.assertTrue(runner is not None, 'Creation failed. No instance.') |
54
|
|
|
self.assertEqual(type(runner), python_runner.PythonRunner, 'Creation failed. No instance.') |
55
|
|
|
|
56
|
|
|
def test_simple_action_with_result_no_status(self): |
57
|
|
|
runner = python_runner.get_runner() |
58
|
|
|
runner.action = self._get_mock_action_obj() |
59
|
|
|
runner.runner_parameters = {} |
60
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
61
|
|
|
runner.container_service = service.RunnerContainerService() |
62
|
|
|
runner.pre_run() |
63
|
|
|
(status, output, _) = runner.run({'row_index': 5}) |
64
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) |
65
|
|
|
self.assertTrue(output is not None) |
66
|
|
|
self.assertEqual(output['result'], [1, 5, 10, 10, 5, 1]) |
67
|
|
|
|
68
|
|
|
def test_simple_action_with_result_as_None_no_status(self): |
69
|
|
|
runner = python_runner.get_runner() |
70
|
|
|
runner.action = self._get_mock_action_obj() |
71
|
|
|
runner.runner_parameters = {} |
72
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
73
|
|
|
runner.container_service = service.RunnerContainerService() |
74
|
|
|
runner.pre_run() |
75
|
|
|
(status, output, _) = runner.run({'row_index': 'b'}) |
76
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) |
77
|
|
|
self.assertTrue(output is not None) |
78
|
|
|
self.assertEqual(output['exit_code'], 0) |
79
|
|
|
self.assertEqual(output['result'], None) |
80
|
|
|
|
81
|
|
|
def test_simple_action_timeout(self): |
82
|
|
|
timeout = 0 |
83
|
|
|
runner = python_runner.get_runner() |
84
|
|
|
runner.action = self._get_mock_action_obj() |
85
|
|
|
runner.runner_parameters = {python_runner.RUNNER_TIMEOUT: timeout} |
86
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
87
|
|
|
runner.container_service = service.RunnerContainerService() |
88
|
|
|
runner.pre_run() |
89
|
|
|
(status, output, _) = runner.run({'row_index': 4}) |
90
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_TIMED_OUT) |
91
|
|
|
self.assertTrue(output is not None) |
92
|
|
|
self.assertEqual(output['result'], 'None') |
93
|
|
|
self.assertEqual(output['error'], 'Action failed to complete in 0 seconds') |
94
|
|
|
self.assertEqual(output['exit_code'], -9) |
95
|
|
|
|
96
|
|
|
def test_simple_action_with_status_succeeded(self): |
97
|
|
|
runner = python_runner.get_runner() |
98
|
|
|
runner.action = self._get_mock_action_obj() |
99
|
|
|
runner.runner_parameters = {} |
100
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
101
|
|
|
runner.container_service = service.RunnerContainerService() |
102
|
|
|
runner.pre_run() |
103
|
|
|
(status, output, _) = runner.run({'row_index': 4}) |
104
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) |
105
|
|
|
self.assertTrue(output is not None) |
106
|
|
|
self.assertEqual(output['result'], [1, 4, 6, 4, 1]) |
107
|
|
|
|
108
|
|
|
def test_simple_action_with_status_failed(self): |
109
|
|
|
runner = python_runner.get_runner() |
110
|
|
|
runner.action = self._get_mock_action_obj() |
111
|
|
|
runner.runner_parameters = {} |
112
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
113
|
|
|
runner.container_service = service.RunnerContainerService() |
114
|
|
|
runner.pre_run() |
115
|
|
|
(status, output, _) = runner.run({'row_index': 'a'}) |
116
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_FAILED) |
117
|
|
|
self.assertTrue(output is not None) |
118
|
|
|
self.assertEqual(output['result'], "This is suppose to fail don't worry!!") |
119
|
|
|
|
120
|
|
|
def test_simple_action_with_status_failed_result_none(self): |
121
|
|
|
runner = python_runner.get_runner() |
122
|
|
|
runner.action = self._get_mock_action_obj() |
123
|
|
|
runner.runner_parameters = {} |
124
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
125
|
|
|
runner.container_service = service.RunnerContainerService() |
126
|
|
|
runner.pre_run() |
127
|
|
|
(status, output, _) = runner.run({'row_index': 'c'}) |
128
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_FAILED) |
129
|
|
|
self.assertTrue(output is not None) |
130
|
|
|
self.assertEqual(output['result'], None) |
131
|
|
|
|
132
|
|
|
def test_exception_in_simple_action_with_invalid_status(self): |
133
|
|
|
runner = python_runner.get_runner() |
134
|
|
|
runner.action = self._get_mock_action_obj() |
135
|
|
|
runner.runner_parameters = {} |
136
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
137
|
|
|
runner.container_service = service.RunnerContainerService() |
138
|
|
|
runner.pre_run() |
139
|
|
|
self.assertRaises(ValueError, |
140
|
|
|
runner.run, action_parameters={'row_index': 'd'}) |
141
|
|
|
|
142
|
|
|
def test_simple_action_no_status_backward_compatibility(self): |
143
|
|
|
runner = python_runner.get_runner() |
144
|
|
|
runner.action = self._get_mock_action_obj() |
145
|
|
|
runner.runner_parameters = {} |
146
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
147
|
|
|
runner.container_service = service.RunnerContainerService() |
148
|
|
|
runner.pre_run() |
149
|
|
|
(status, output, _) = runner.run({'row_index': 'e'}) |
150
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) |
151
|
|
|
self.assertTrue(output is not None) |
152
|
|
|
self.assertEqual(output['result'], [1, 2]) |
153
|
|
|
|
154
|
|
|
def test_simple_action_config_value_provided_overriden_in_datastore(self): |
155
|
|
|
wrapper = PythonActionWrapper(pack='dummy_pack_5', file_path=PASCAL_ROW_ACTION_PATH, |
156
|
|
|
user='joe') |
157
|
|
|
|
158
|
|
|
# No values provided in the datastore |
159
|
|
|
instance = wrapper._get_action_instance() |
160
|
|
|
self.assertEqual(instance.config['api_key'], 'some_api_key') # static value |
161
|
|
|
self.assertEqual(instance.config['regions'], ['us-west-1']) # static value |
162
|
|
|
self.assertEqual(instance.config['api_secret'], None) |
163
|
|
|
self.assertEqual(instance.config['private_key_path'], None) |
164
|
|
|
|
165
|
|
|
# api_secret overriden in the datastore (user scoped value) |
166
|
|
|
config_service.set_datastore_value_for_config_key(pack_name='dummy_pack_5', |
167
|
|
|
key_name='api_secret', |
168
|
|
|
user='joe', |
169
|
|
|
value='foosecret', |
170
|
|
|
secret=True) |
171
|
|
|
|
172
|
|
|
# private_key_path overriden in the datastore (global / non-user scoped value) |
173
|
|
|
config_service.set_datastore_value_for_config_key(pack_name='dummy_pack_5', |
174
|
|
|
key_name='private_key_path', |
175
|
|
|
value='foopath') |
176
|
|
|
|
177
|
|
|
instance = wrapper._get_action_instance() |
178
|
|
|
self.assertEqual(instance.config['api_key'], 'some_api_key') # static value |
179
|
|
|
self.assertEqual(instance.config['regions'], ['us-west-1']) # static value |
180
|
|
|
self.assertEqual(instance.config['api_secret'], 'foosecret') |
181
|
|
|
self.assertEqual(instance.config['private_key_path'], 'foopath') |
182
|
|
|
|
183
|
|
|
def test_simple_action_fail(self): |
184
|
|
|
runner = python_runner.get_runner() |
185
|
|
|
runner.action = self._get_mock_action_obj() |
186
|
|
|
runner.runner_parameters = {} |
187
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
188
|
|
|
runner.container_service = service.RunnerContainerService() |
189
|
|
|
runner.pre_run() |
190
|
|
|
(status, result, _) = runner.run({'row_index': '4'}) |
191
|
|
|
self.assertTrue(result is not None) |
192
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_FAILED) |
193
|
|
|
|
194
|
|
|
def test_simple_action_no_file(self): |
195
|
|
|
runner = python_runner.get_runner() |
196
|
|
|
runner.action = self._get_mock_action_obj() |
197
|
|
|
runner.runner_parameters = {} |
198
|
|
|
runner.entry_point = 'foo.py' |
199
|
|
|
runner.container_service = service.RunnerContainerService() |
200
|
|
|
runner.pre_run() |
201
|
|
|
(status, result, _) = runner.run({}) |
202
|
|
|
self.assertTrue(result is not None) |
203
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_FAILED) |
204
|
|
|
|
205
|
|
|
def test_simple_action_no_entry_point(self): |
206
|
|
|
runner = python_runner.get_runner() |
207
|
|
|
runner.action = self._get_mock_action_obj() |
208
|
|
|
runner.runner_parameters = {} |
209
|
|
|
runner.entry_point = '' |
210
|
|
|
runner.container_service = service.RunnerContainerService() |
211
|
|
|
|
212
|
|
|
expected_msg = 'Action .*? is missing entry_point attribute' |
213
|
|
|
self.assertRaisesRegexp(Exception, expected_msg, runner.run, {}) |
214
|
|
|
|
215
|
|
|
@mock.patch('st2common.util.green.shell.subprocess.Popen') |
216
|
|
|
def test_action_with_user_supplied_env_vars(self, mock_popen): |
217
|
|
|
env_vars = {'key1': 'val1', 'key2': 'val2', 'PYTHONPATH': 'foobar'} |
218
|
|
|
|
219
|
|
|
mock_process = mock.Mock() |
220
|
|
|
mock_process.communicate.return_value = ('', '') |
221
|
|
|
mock_popen.return_value = mock_process |
222
|
|
|
|
223
|
|
|
runner = python_runner.get_runner() |
224
|
|
|
runner.action = self._get_mock_action_obj() |
225
|
|
|
runner.runner_parameters = {'env': env_vars} |
226
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
227
|
|
|
runner.container_service = service.RunnerContainerService() |
228
|
|
|
runner.pre_run() |
229
|
|
|
(_, _, _) = runner.run({'row_index': 4}) |
230
|
|
|
|
231
|
|
|
_, call_kwargs = mock_popen.call_args |
232
|
|
|
actual_env = call_kwargs['env'] |
233
|
|
|
|
234
|
|
|
for key, value in env_vars.items(): |
235
|
|
|
# Verify that a blacklsited PYTHONPATH has been filtered out |
236
|
|
|
if key == 'PYTHONPATH': |
237
|
|
|
self.assertTrue(actual_env[key] != value) |
238
|
|
|
else: |
239
|
|
|
self.assertEqual(actual_env[key], value) |
240
|
|
|
|
241
|
|
|
@mock.patch('st2common.util.green.shell.subprocess.Popen') |
242
|
|
|
def test_stdout_interception_and_parsing(self, mock_popen): |
243
|
|
|
values = {'delimiter': ACTION_OUTPUT_RESULT_DELIMITER} |
244
|
|
|
|
245
|
|
|
# No output to stdout and no result (implicit None) |
246
|
|
|
mock_stdout = '%(delimiter)sNone%(delimiter)s' % values |
247
|
|
|
mock_stderr = 'foo stderr' |
248
|
|
|
mock_process = mock.Mock() |
249
|
|
|
mock_process.communicate.return_value = (mock_stdout, mock_stderr) |
250
|
|
|
mock_process.returncode = 0 |
251
|
|
|
mock_popen.return_value = mock_process |
252
|
|
|
|
253
|
|
|
runner = python_runner.get_runner() |
254
|
|
|
runner.action = self._get_mock_action_obj() |
255
|
|
|
runner.runner_parameters = {} |
256
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
257
|
|
|
runner.container_service = service.RunnerContainerService() |
258
|
|
|
runner.pre_run() |
259
|
|
|
(_, output, _) = runner.run({'row_index': 4}) |
260
|
|
|
|
261
|
|
|
self.assertEqual(output['stdout'], '') |
262
|
|
|
self.assertEqual(output['stderr'], mock_stderr) |
263
|
|
|
self.assertEqual(output['result'], 'None') |
264
|
|
|
self.assertEqual(output['exit_code'], 0) |
265
|
|
|
|
266
|
|
|
# Output to stdout, no result (implicit None),return_code 1 and status |
267
|
|
|
# failed |
268
|
|
|
mock_stdout = 'pre result%(delimiter)sNone%(delimiter)spost result' % values |
269
|
|
|
mock_stderr = 'foo stderr' |
270
|
|
|
mock_process = mock.Mock() |
271
|
|
|
mock_process.communicate.return_value = (mock_stdout, mock_stderr) |
272
|
|
|
mock_process.returncode = 1 |
273
|
|
|
mock_popen.return_value = mock_process |
274
|
|
|
|
275
|
|
|
runner = python_runner.get_runner() |
276
|
|
|
runner.action = self._get_mock_action_obj() |
277
|
|
|
runner.runner_parameters = {} |
278
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
279
|
|
|
runner.container_service = service.RunnerContainerService() |
280
|
|
|
runner.pre_run() |
281
|
|
|
(status, output, _) = runner.run({'row_index': 4}) |
282
|
|
|
self.assertEqual(output['stdout'], 'pre resultpost result') |
283
|
|
|
self.assertEqual(output['stderr'], mock_stderr) |
284
|
|
|
self.assertEqual(output['result'], 'None') |
285
|
|
|
self.assertEqual(output['exit_code'], 1) |
286
|
|
|
self.assertEqual(status, 'failed') |
287
|
|
|
|
288
|
|
|
# Output to stdout, no result (implicit None), return_code 1 and status |
289
|
|
|
# succedded |
290
|
|
|
mock_stdout = 'pre result%(delimiter)sNone%(delimiter)spost result' % values |
291
|
|
|
mock_stderr = 'foo stderr' |
292
|
|
|
mock_process = mock.Mock() |
293
|
|
|
mock_process.communicate.return_value = (mock_stdout, mock_stderr) |
294
|
|
|
mock_process.returncode = 0 |
295
|
|
|
mock_popen.return_value = mock_process |
296
|
|
|
runner = python_runner.get_runner() |
297
|
|
|
runner.action = self._get_mock_action_obj() |
298
|
|
|
runner.runner_parameters = {} |
299
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
300
|
|
|
runner.container_service = service.RunnerContainerService() |
301
|
|
|
runner.pre_run() |
302
|
|
|
(status, output, _) = runner.run({'row_index': 4}) |
303
|
|
|
self.assertEqual(output['stdout'], 'pre resultpost result') |
304
|
|
|
self.assertEqual(output['stderr'], mock_stderr) |
305
|
|
|
self.assertEqual(output['result'], 'None') |
306
|
|
|
self.assertEqual(output['exit_code'], 0) |
307
|
|
|
self.assertEqual(status, 'succeeded') |
308
|
|
|
|
309
|
|
|
@mock.patch('st2common.util.green.shell.subprocess.Popen') |
310
|
|
|
def test_common_st2_env_vars_are_available_to_the_action(self, mock_popen): |
311
|
|
|
mock_process = mock.Mock() |
312
|
|
|
mock_process.communicate.return_value = ('', '') |
313
|
|
|
mock_popen.return_value = mock_process |
314
|
|
|
|
315
|
|
|
runner = python_runner.get_runner() |
316
|
|
|
runner.auth_token = mock.Mock() |
317
|
|
|
runner.auth_token.token = 'ponies' |
318
|
|
|
runner.action = self._get_mock_action_obj() |
319
|
|
|
runner.runner_parameters = {} |
320
|
|
|
runner.entry_point = PASCAL_ROW_ACTION_PATH |
321
|
|
|
runner.container_service = service.RunnerContainerService() |
322
|
|
|
runner.pre_run() |
323
|
|
|
(_, _, _) = runner.run({'row_index': 4}) |
324
|
|
|
|
325
|
|
|
_, call_kwargs = mock_popen.call_args |
326
|
|
|
actual_env = call_kwargs['env'] |
327
|
|
|
self.assertCommonSt2EnvVarsAvailableInEnv(env=actual_env) |
328
|
|
|
|
329
|
|
|
def test_action_class_instantiation_action_service_argument(self): |
330
|
|
|
class Action1(Action): |
331
|
|
|
# Constructor not overriden so no issue here |
332
|
|
|
pass |
333
|
|
|
|
334
|
|
|
def run(self): |
335
|
|
|
pass |
336
|
|
|
|
337
|
|
|
class Action2(Action): |
338
|
|
|
# Constructor overriden, but takes action_service argument |
339
|
|
|
def __init__(self, config, action_service=None): |
340
|
|
|
super(Action2, self).__init__(config=config, |
341
|
|
|
action_service=action_service) |
342
|
|
|
|
343
|
|
|
def run(self): |
344
|
|
|
pass |
345
|
|
|
|
346
|
|
|
class Action3(Action): |
347
|
|
|
# Constructor overriden, but doesn't take to action service |
348
|
|
|
def __init__(self, config): |
349
|
|
|
super(Action3, self).__init__(config=config) |
350
|
|
|
|
351
|
|
|
def run(self): |
352
|
|
|
pass |
353
|
|
|
|
354
|
|
|
config = {'a': 1, 'b': 2} |
355
|
|
|
action_service = 'ActionService!' |
356
|
|
|
|
357
|
|
|
action1 = get_action_class_instance(action_cls=Action1, config=config, |
358
|
|
|
action_service=action_service) |
359
|
|
|
self.assertEqual(action1.config, config) |
360
|
|
|
self.assertEqual(action1.action_service, action_service) |
361
|
|
|
|
362
|
|
|
action2 = get_action_class_instance(action_cls=Action2, config=config, |
363
|
|
|
action_service=action_service) |
364
|
|
|
self.assertEqual(action2.config, config) |
365
|
|
|
self.assertEqual(action2.action_service, action_service) |
366
|
|
|
|
367
|
|
|
action3 = get_action_class_instance(action_cls=Action3, config=config, |
368
|
|
|
action_service=action_service) |
369
|
|
|
self.assertEqual(action3.config, config) |
370
|
|
|
self.assertEqual(action3.action_service, action_service) |
371
|
|
|
|
372
|
|
|
def test_action_with_same_module_name_as_module_in_stdlib(self): |
373
|
|
|
runner = python_runner.get_runner() |
374
|
|
|
runner.action = self._get_mock_action_obj() |
375
|
|
|
runner.runner_parameters = {} |
376
|
|
|
runner.entry_point = TEST_ACTION_PATH |
377
|
|
|
runner.container_service = service.RunnerContainerService() |
378
|
|
|
runner.pre_run() |
379
|
|
|
(status, output, _) = runner.run({}) |
380
|
|
|
self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) |
381
|
|
|
self.assertTrue(output is not None) |
382
|
|
|
self.assertEqual(output['result'], 'test action') |
383
|
|
|
|
384
|
|
|
def _get_mock_action_obj(self): |
385
|
|
|
""" |
386
|
|
|
Return mock action object. |
387
|
|
|
|
388
|
|
|
Pack gets set to the system pack so the action doesn't require a separate virtualenv. |
389
|
|
|
""" |
390
|
|
|
action = mock.Mock() |
391
|
|
|
action.pack = SYSTEM_PACK_NAME |
392
|
|
|
action.entry_point = 'foo.py' |
393
|
|
|
return action |
394
|
|
|
|