Test Failed
Push — master ( e380d0...f5671d )
by W
02:58
created

python_runner/tests/unit/test_pythonrunner.py (2 issues)

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
from __future__ import absolute_import
17
18
import os
19
import re
20
import sys
21
22
import six
23
import mock
24
from oslo_config import cfg
25
26
from python_runner import python_runner
27
from st2actions.container.base import RunnerContainer
28
from st2common.runners.base_action import Action
29
from st2common.runners.utils import get_action_class_instance
30
from st2common.services import config as config_service
31
from st2common.constants.action import ACTION_OUTPUT_RESULT_DELIMITER
32
from st2common.constants.action import LIVEACTION_STATUS_SUCCEEDED, LIVEACTION_STATUS_FAILED
33
from st2common.constants.action import LIVEACTION_STATUS_TIMED_OUT
34
from st2common.constants.action import MAX_PARAM_LENGTH
35
from st2common.constants.pack import SYSTEM_PACK_NAME
36
from st2common.persistence.execution import ActionExecutionOutput
37
from python_runner.python_action_wrapper import PythonActionWrapper
38
from st2tests.base import RunnerTestCase
39
from st2tests.base import CleanDbTestCase
40
from st2tests.base import blocking_eventlet_spawn
41
from st2tests.base import make_mock_stream_readline
42
from st2tests.fixturesloader import assert_submodules_are_checked_out
43
import st2tests.base as tests_base
44
45
46
PASCAL_ROW_ACTION_PATH = os.path.join(tests_base.get_resources_path(), 'packs',
47
                                      'pythonactions/actions/pascal_row.py')
48
ECHOER_ACTION_PATH = os.path.join(tests_base.get_resources_path(), 'packs',
49
                                  'pythonactions/actions/echoer.py')
50
TEST_ACTION_PATH = os.path.join(tests_base.get_resources_path(), 'packs',
51
                                'pythonactions/actions/test.py')
52
PATHS_ACTION_PATH = os.path.join(tests_base.get_resources_path(), 'packs',
53
                                'pythonactions/actions/python_paths.py')
54
ACTION_1_PATH = os.path.join(tests_base.get_fixtures_path(),
55
                             'packs/dummy_pack_9/actions/list_repos_doesnt_exist.py')
56
ACTION_2_PATH = os.path.join(tests_base.get_fixtures_path(),
57
                             'packs/dummy_pack_9/actions/invalid_syntax.py')
58
NON_SIMPLE_TYPE_ACTION = os.path.join(tests_base.get_resources_path(), 'packs',
59
                                      'pythonactions/actions/non_simple_type.py')
60
PRINT_VERSION_ACTION = os.path.join(tests_base.get_fixtures_path(), 'packs',
61
                                    'test_content_version/actions/print_version.py')
62
PRINT_VERSION_LOCAL_MODULE_ACTION = os.path.join(tests_base.get_fixtures_path(), 'packs',
63
    'test_content_version/actions/print_version_local_import.py')
64
65
PRINT_CONFIG_ITEM_ACTION = os.path.join(tests_base.get_resources_path(), 'packs',
66
                                        'pythonactions/actions/print_config_item_doesnt_exist.py')
67
68
69
# Note: runner inherits parent args which doesn't work with tests since test pass additional
70
# unrecognized args
71
mock_sys = mock.Mock()
72
mock_sys.argv = []
73
mock_sys.executable = sys.executable
74
75
MOCK_EXECUTION = mock.Mock()
76
MOCK_EXECUTION.id = '598dbf0c0640fd54bffc688b'
77
78
79
@mock.patch('python_runner.python_runner.sys', mock_sys)
80
class PythonRunnerTestCase(RunnerTestCase, CleanDbTestCase):
81
    register_packs = True
82
    register_pack_configs = True
83
84
    @classmethod
85
    def setUpClass(cls):
86
        super(PythonRunnerTestCase, cls).setUpClass()
87
        assert_submodules_are_checked_out()
88
89
    def test_runner_creation(self):
90
        runner = python_runner.get_runner()
91
        self.assertTrue(runner is not None, 'Creation failed. No instance.')
92
        self.assertEqual(type(runner), python_runner.PythonRunner, 'Creation failed. No instance.')
93
94
    def test_action_returns_non_serializable_result(self):
95
        # Actions returns non-simple type which can't be serialized, verify result is simple str()
96
        # representation of the result
97
        runner = self._get_mock_runner_obj()
98
        runner.entry_point = NON_SIMPLE_TYPE_ACTION
99
        runner.pre_run()
100
        (status, output, _) = runner.run({})
101
102
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
103
        self.assertTrue(output is not None)
104
105
        if six.PY2:
106
            expected_result_re = (r"\[{'a': '1'}, {'h': 3, 'c': 2}, {'e': "
107
                                  "<non_simple_type.Test object at .*?>}\]")
108
        else:
109
            expected_result_re = (r"\[{'a': '1'}, {'c': 2, 'h': 3}, {'e': "
110
                                  "<non_simple_type.Test object at .*?>}\]")
111
112
        match = re.match(expected_result_re, output['result'])
113
        self.assertTrue(match)
114
115
    def test_simple_action_with_result_no_status(self):
116
        runner = self._get_mock_runner_obj()
117
        runner.entry_point = PASCAL_ROW_ACTION_PATH
118
        runner.pre_run()
119
        (status, output, _) = runner.run({'row_index': 5})
120
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
121
        self.assertTrue(output is not None)
122
        self.assertEqual(output['result'], [1, 5, 10, 10, 5, 1])
123
124
    def test_simple_action_with_result_as_None_no_status(self):
125
        runner = self._get_mock_runner_obj()
126
        runner.entry_point = PASCAL_ROW_ACTION_PATH
127
        runner.pre_run()
128
        (status, output, _) = runner.run({'row_index': 'b'})
129
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
130
        self.assertTrue(output is not None)
131
        self.assertEqual(output['exit_code'], 0)
132
        self.assertEqual(output['result'], None)
133
134
    def test_simple_action_timeout(self):
135
        timeout = 0
136
        runner = self._get_mock_runner_obj()
137
        runner.runner_parameters = {python_runner.RUNNER_TIMEOUT: timeout}
138
        runner.entry_point = PASCAL_ROW_ACTION_PATH
139
        runner.pre_run()
140
        (status, output, _) = runner.run({'row_index': 4})
141
        self.assertEqual(status, LIVEACTION_STATUS_TIMED_OUT)
142
        self.assertTrue(output is not None)
143
        self.assertEqual(output['result'], 'None')
144
        self.assertEqual(output['error'], 'Action failed to complete in 0 seconds')
145
        self.assertEqual(output['exit_code'], -9)
146
147
    def test_simple_action_with_status_succeeded(self):
148
        runner = self._get_mock_runner_obj()
149
        runner.entry_point = PASCAL_ROW_ACTION_PATH
150
        runner.pre_run()
151
        (status, output, _) = runner.run({'row_index': 4})
152
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
153
        self.assertTrue(output is not None)
154
        self.assertEqual(output['result'], [1, 4, 6, 4, 1])
155
156
    def test_simple_action_with_status_failed(self):
157
        runner = self._get_mock_runner_obj()
158
        runner.entry_point = PASCAL_ROW_ACTION_PATH
159
        runner.pre_run()
160
        (status, output, _) = runner.run({'row_index': 'a'})
161
        self.assertEqual(status, LIVEACTION_STATUS_FAILED)
162
        self.assertTrue(output is not None)
163
        self.assertEqual(output['result'], "This is suppose to fail don't worry!!")
164
165
    def test_simple_action_with_status_complex_type_returned_for_result(self):
166
        # Result containing a complex type shouldn't break the returning a tuple with status
167
        # behavior
168
        runner = self._get_mock_runner_obj()
169
        runner.entry_point = PASCAL_ROW_ACTION_PATH
170
        runner.pre_run()
171
        (status, output, _) = runner.run({'row_index': 'complex_type'})
172
173
        self.assertEqual(status, LIVEACTION_STATUS_FAILED)
174
        self.assertTrue(output is not None)
175
        self.assertTrue('<pascal_row.PascalRowAction object at' in output['result'])
176
177
    def test_simple_action_with_status_failed_result_none(self):
178
        runner = self._get_mock_runner_obj()
179
        runner.entry_point = PASCAL_ROW_ACTION_PATH
180
        runner.pre_run()
181
        (status, output, _) = runner.run({'row_index': 'c'})
182
        self.assertEqual(status, LIVEACTION_STATUS_FAILED)
183
        self.assertTrue(output is not None)
184
        self.assertEqual(output['result'], None)
185
186
    def test_exception_in_simple_action_with_invalid_status(self):
187
        runner = self._get_mock_runner_obj()
188
        runner.entry_point = PASCAL_ROW_ACTION_PATH
189
        runner.pre_run()
190
        self.assertRaises(ValueError,
191
                          runner.run, action_parameters={'row_index': 'd'})
192
193
    def test_simple_action_no_status_backward_compatibility(self):
194
        runner = self._get_mock_runner_obj()
195
        runner.entry_point = PASCAL_ROW_ACTION_PATH
196
        runner.pre_run()
197
        (status, output, _) = runner.run({'row_index': 'e'})
198
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
199
        self.assertTrue(output is not None)
200
        self.assertEqual(output['result'], [1, 2])
201
202
    def test_simple_action_config_value_provided_overriden_in_datastore(self):
203
        pack = 'dummy_pack_5'
204
        user = 'joe'
205
206
        # No values provided in the datastore
207
        runner = self._get_mock_runner_obj_from_container(pack=pack, user=user)
208
209
        self.assertEqual(runner._config['api_key'], 'some_api_key')  # static value
210
        self.assertEqual(runner._config['regions'], ['us-west-1'])  # static value
211
        self.assertEqual(runner._config['api_secret'], None)
212
        self.assertEqual(runner._config['private_key_path'], None)
213
214
        # api_secret overriden in the datastore (user scoped value)
215
        config_service.set_datastore_value_for_config_key(pack_name='dummy_pack_5',
216
                                                          key_name='api_secret',
217
                                                          user='joe',
218
                                                          value='foosecret',
219
                                                          secret=True)
220
221
        # private_key_path overriden in the datastore (global / non-user scoped value)
222
        config_service.set_datastore_value_for_config_key(pack_name='dummy_pack_5',
223
                                                          key_name='private_key_path',
224
                                                          value='foopath')
225
226
        runner = self._get_mock_runner_obj_from_container(pack=pack, user=user)
227
        self.assertEqual(runner._config['api_key'], 'some_api_key')  # static value
228
        self.assertEqual(runner._config['regions'], ['us-west-1'])  # static value
229
        self.assertEqual(runner._config['api_secret'], 'foosecret')
230
        self.assertEqual(runner._config['private_key_path'], 'foopath')
231
232
    def test_simple_action_fail(self):
233
        runner = self._get_mock_runner_obj()
234
        runner.entry_point = PASCAL_ROW_ACTION_PATH
235
        runner.pre_run()
236
        (status, result, _) = runner.run({'row_index': '4'})
237
        self.assertTrue(result is not None)
238
        self.assertEqual(status, LIVEACTION_STATUS_FAILED)
239
240
    def test_simple_action_no_file(self):
241
        runner = self._get_mock_runner_obj()
242
        runner.entry_point = 'foo.py'
243
        runner.pre_run()
244
        (status, result, _) = runner.run({})
245
        self.assertTrue(result is not None)
246
        self.assertEqual(status, LIVEACTION_STATUS_FAILED)
247
248
    def test_simple_action_no_entry_point(self):
249
        runner = self._get_mock_runner_obj()
250
        runner.entry_point = ''
251
252
        expected_msg = 'Action .*? is missing entry_point attribute'
253
        self.assertRaisesRegexp(Exception, expected_msg, runner.run, {})
254
255
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
256
    def test_action_with_user_supplied_env_vars(self, mock_popen):
257
        env_vars = {'key1': 'val1', 'key2': 'val2', 'PYTHONPATH': 'foobar'}
258
259
        mock_process = mock.Mock()
260
        mock_process.communicate.return_value = ('', '')
261
        mock_popen.return_value = mock_process
262
263
        runner = self._get_mock_runner_obj()
264
        runner.runner_parameters = {'env': env_vars}
265
        runner.entry_point = PASCAL_ROW_ACTION_PATH
266
        runner.pre_run()
267
        (_, _, _) = runner.run({'row_index': 4})
268
269
        _, call_kwargs = mock_popen.call_args
270
        actual_env = call_kwargs['env']
271
272
        for key, value in env_vars.items():
273
            # Verify that a blacklsited PYTHONPATH has been filtered out
274
            if key == 'PYTHONPATH':
275
                self.assertTrue(actual_env[key] != value)
276
            else:
277
                self.assertEqual(actual_env[key], value)
278
279
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
280
    @mock.patch('st2common.util.green.shell.eventlet.spawn')
281
    def test_action_stdout_and_stderr_is_not_stored_in_db_by_default(self, mock_spawn, mock_popen):
282
        # Feature should be disabled by default
283
        values = {'delimiter': ACTION_OUTPUT_RESULT_DELIMITER}
284
285
        # Note: We need to mock spawn function so we can test everything in single event loop
286
        # iteration
287
        mock_spawn.side_effect = blocking_eventlet_spawn
288
289
        # No output to stdout and no result (implicit None)
290
        mock_stdout = [
291
            'pre result line 1\n',
292
            '%(delimiter)sTrue%(delimiter)s' % values,
293
            'post result line 1'
294
        ]
295
        mock_stderr = [
296
            'stderr line 1\n',
297
            'stderr line 2\n',
298
            'stderr line 3\n'
299
        ]
300
301
        mock_process = mock.Mock()
302
        mock_process.returncode = 0
303
        mock_popen.return_value = mock_process
304
        mock_process.stdout.closed = False
305
        mock_process.stderr.closed = False
306
        mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout,
307
                                                                 stop_counter=3)
308
        mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr,
309
                                                                 stop_counter=3)
310
311
        runner = self._get_mock_runner_obj()
312
        runner.entry_point = PASCAL_ROW_ACTION_PATH
313
        runner.pre_run()
314
        (_, output, _) = runner.run({'row_index': 4})
315
316
        self.assertEqual(output['stdout'], 'pre result line 1\npost result line 1')
317
        self.assertEqual(output['stderr'], 'stderr line 1\nstderr line 2\nstderr line 3\n')
318
        self.assertEqual(output['result'], 'True')
319
        self.assertEqual(output['exit_code'], 0)
320
321
        output_dbs = ActionExecutionOutput.get_all()
322
        self.assertEqual(len(output_dbs), 0)
323
324
        # False is a default behavior so end result should be the same
325
        cfg.CONF.set_override(name='stream_output', group='actionrunner', override=False)
326
327
        mock_process = mock.Mock()
328
        mock_process.returncode = 0
329
        mock_popen.return_value = mock_process
330
        mock_process.stdout.closed = False
331
        mock_process.stderr.closed = False
332
        mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout,
333
                                                                 stop_counter=3)
334
        mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr,
335
                                                                 stop_counter=3)
336
337
        runner.pre_run()
338
        (_, output, _) = runner.run({'row_index': 4})
339
340
        self.assertEqual(output['stdout'], 'pre result line 1\npost result line 1')
341
        self.assertEqual(output['stderr'], 'stderr line 1\nstderr line 2\nstderr line 3\n')
342
        self.assertEqual(output['result'], 'True')
343
        self.assertEqual(output['exit_code'], 0)
344
345
        output_dbs = ActionExecutionOutput.get_all()
346
        self.assertEqual(len(output_dbs), 0)
347
348
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
349
    @mock.patch('st2common.util.green.shell.eventlet.spawn')
350
    def test_action_stdout_and_stderr_is_stored_in_the_db(self, mock_spawn, mock_popen):
351
        # Feature is enabled
352
        cfg.CONF.set_override(name='stream_output', group='actionrunner', override=True)
353
354
        values = {'delimiter': ACTION_OUTPUT_RESULT_DELIMITER}
355
356
        # Note: We need to mock spawn function so we can test everything in single event loop
357
        # iteration
358
        mock_spawn.side_effect = blocking_eventlet_spawn
359
360
        # No output to stdout and no result (implicit None)
361
        mock_stdout = [
362
            'pre result line 1\n',
363
            'pre result line 2\n',
364
            '%(delimiter)sTrue%(delimiter)s' % values,
365
            'post result line 1'
366
        ]
367
        mock_stderr = [
368
            'stderr line 1\n',
369
            'stderr line 2\n',
370
            'stderr line 3\n'
371
        ]
372
373
        mock_process = mock.Mock()
374
        mock_process.returncode = 0
375
        mock_popen.return_value = mock_process
376
        mock_process.stdout.closed = False
377
        mock_process.stderr.closed = False
378
        mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout,
379
                                                                 stop_counter=4)
380
        mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr,
381
                                                                 stop_counter=3)
382
383
        runner = self._get_mock_runner_obj()
384
        runner.entry_point = PASCAL_ROW_ACTION_PATH
385
        runner.pre_run()
386
        (_, output, _) = runner.run({'row_index': 4})
387
388
        self.assertEqual(output['stdout'],
389
                         'pre result line 1\npre result line 2\npost result line 1')
390
        self.assertEqual(output['stderr'], 'stderr line 1\nstderr line 2\nstderr line 3\n')
391
        self.assertEqual(output['result'], 'True')
392
        self.assertEqual(output['exit_code'], 0)
393
394
        # Verify stdout and stderr lines have been correctly stored in the db
395
        # Note - result delimiter should not be stored in the db
396
        output_dbs = ActionExecutionOutput.query(output_type='stdout')
397
        self.assertEqual(len(output_dbs), 3)
398
        self.assertEqual(output_dbs[0].runner_ref, 'python-script')
399
        self.assertEqual(output_dbs[0].data, mock_stdout[0])
400
        self.assertEqual(output_dbs[1].data, mock_stdout[1])
401
        self.assertEqual(output_dbs[2].data, mock_stdout[3])
402
403
        output_dbs = ActionExecutionOutput.query(output_type='stderr')
404
        self.assertEqual(len(output_dbs), 3)
405
        self.assertEqual(output_dbs[0].runner_ref, 'python-script')
406
        self.assertEqual(output_dbs[0].data, mock_stderr[0])
407
        self.assertEqual(output_dbs[1].data, mock_stderr[1])
408
        self.assertEqual(output_dbs[2].data, mock_stderr[2])
409
410
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
411
    def test_stdout_interception_and_parsing(self, mock_popen):
412
        values = {'delimiter': ACTION_OUTPUT_RESULT_DELIMITER}
413
414
        # No output to stdout and no result (implicit None)
415
        mock_stdout = ['%(delimiter)sNone%(delimiter)s' % values]
416
        mock_stderr = ['foo stderr']
417
418
        mock_process = mock.Mock()
419
        mock_process.returncode = 0
420
        mock_popen.return_value = mock_process
421
        mock_process.stdout.closed = False
422
        mock_process.stderr.closed = False
423
        mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout)
424
        mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr)
425
426
        runner = self._get_mock_runner_obj()
427
        runner.entry_point = PASCAL_ROW_ACTION_PATH
428
        runner.pre_run()
429
        (_, output, _) = runner.run({'row_index': 4})
430
431
        self.assertEqual(output['stdout'], '')
432
        self.assertEqual(output['stderr'], mock_stderr[0])
433
        self.assertEqual(output['result'], 'None')
434
        self.assertEqual(output['exit_code'], 0)
435
436
        # Output to stdout, no result (implicit None), return_code 1 and status failed
437
        mock_stdout = ['pre result%(delimiter)sNone%(delimiter)spost result' % values]
438
        mock_stderr = ['foo stderr']
439
440
        mock_process = mock.Mock()
441
        mock_process.returncode = 1
442
        mock_popen.return_value = mock_process
443
        mock_process.stdout.closed = False
444
        mock_process.stderr.closed = False
445
        mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout)
446
        mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr)
447
448
        runner = self._get_mock_runner_obj()
449
        runner.entry_point = PASCAL_ROW_ACTION_PATH
450
        runner.pre_run()
451
        (status, output, _) = runner.run({'row_index': 4})
452
453
        self.assertEqual(output['stdout'], 'pre resultpost result')
454
        self.assertEqual(output['stderr'], mock_stderr[0])
455
        self.assertEqual(output['result'], 'None')
456
        self.assertEqual(output['exit_code'], 1)
457
        self.assertEqual(status, 'failed')
458
459
        # Output to stdout, no result (implicit None), return_code 1 and status succeeded
460
        mock_stdout = ['pre result%(delimiter)sNone%(delimiter)spost result' % values]
461
        mock_stderr = ['foo stderr']
462
463
        mock_process = mock.Mock()
464
        mock_process.returncode = 0
465
        mock_popen.return_value = mock_process
466
        mock_process.stdout.closed = False
467
        mock_process.stderr.closed = False
468
        mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout)
469
        mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr)
470
471
        runner = self._get_mock_runner_obj()
472
        runner.entry_point = PASCAL_ROW_ACTION_PATH
473
        runner.pre_run()
474
        (status, output, _) = runner.run({'row_index': 4})
475
476
        self.assertEqual(output['stdout'], 'pre resultpost result')
477
        self.assertEqual(output['stderr'], mock_stderr[0])
478
        self.assertEqual(output['result'], 'None')
479
        self.assertEqual(output['exit_code'], 0)
480
        self.assertEqual(status, 'succeeded')
481
482
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
483
    def test_common_st2_env_vars_are_available_to_the_action(self, mock_popen):
484
        mock_process = mock.Mock()
485
        mock_process.communicate.return_value = ('', '')
486
        mock_popen.return_value = mock_process
487
488
        runner = self._get_mock_runner_obj()
489
        runner.auth_token = mock.Mock()
490
        runner.auth_token.token = 'ponies'
491
        runner.entry_point = PASCAL_ROW_ACTION_PATH
492
        runner.pre_run()
493
        (_, _, _) = runner.run({'row_index': 4})
494
495
        _, call_kwargs = mock_popen.call_args
496
        actual_env = call_kwargs['env']
497
        self.assertCommonSt2EnvVarsAvailableInEnv(env=actual_env)
498
499
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
500
    def test_pythonpath_env_var_contains_common_libs_config_enabled(self, mock_popen):
501
        mock_process = mock.Mock()
502
        mock_process.communicate.return_value = ('', '')
503
        mock_popen.return_value = mock_process
504
505
        runner = self._get_mock_runner_obj()
506
        runner._enable_common_pack_libs = True
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _enable_common_pack_libs was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
507
        runner.auth_token = mock.Mock()
508
        runner.auth_token.token = 'ponies'
509
        runner.entry_point = PASCAL_ROW_ACTION_PATH
510
        runner.pre_run()
511
        (_, _, _) = runner.run({'row_index': 4})
512
513
        _, call_kwargs = mock_popen.call_args
514
        actual_env = call_kwargs['env']
515
        pack_common_lib_path = 'fixtures/packs/core/lib'
516
        self.assertTrue('PYTHONPATH' in actual_env)
517
        self.assertTrue(pack_common_lib_path in actual_env['PYTHONPATH'])
518
519
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
520
    def test_pythonpath_env_var_not_contains_common_libs_config_disabled(self, mock_popen):
521
        mock_process = mock.Mock()
522
        mock_process.communicate.return_value = ('', '')
523
        mock_popen.return_value = mock_process
524
525
        runner = self._get_mock_runner_obj()
526
        runner._enable_common_pack_libs = False
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _enable_common_pack_libs was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
527
        runner.auth_token = mock.Mock()
528
        runner.auth_token.token = 'ponies'
529
        runner.entry_point = PASCAL_ROW_ACTION_PATH
530
        runner.pre_run()
531
        (_, _, _) = runner.run({'row_index': 4})
532
533
        _, call_kwargs = mock_popen.call_args
534
        actual_env = call_kwargs['env']
535
        pack_common_lib_path = '/mnt/src/storm/st2/st2tests/st2tests/fixtures/packs/core/lib'
536
        self.assertTrue('PYTHONPATH' in actual_env)
537
        self.assertTrue(pack_common_lib_path not in actual_env['PYTHONPATH'])
538
539
    def test_action_class_instantiation_action_service_argument(self):
540
        class Action1(Action):
541
            # Constructor not overriden so no issue here
542
            pass
543
544
            def run(self):
545
                pass
546
547
        class Action2(Action):
548
            # Constructor overriden, but takes action_service argument
549
            def __init__(self, config, action_service=None):
550
                super(Action2, self).__init__(config=config,
551
                                              action_service=action_service)
552
553
            def run(self):
554
                pass
555
556
        class Action3(Action):
557
            # Constructor overriden, but doesn't take to action service
558
            def __init__(self, config):
559
                super(Action3, self).__init__(config=config)
560
561
            def run(self):
562
                pass
563
564
        config = {'a': 1, 'b': 2}
565
        action_service = 'ActionService!'
566
567
        action1 = get_action_class_instance(action_cls=Action1, config=config,
568
                                            action_service=action_service)
569
        self.assertEqual(action1.config, config)
570
        self.assertEqual(action1.action_service, action_service)
571
572
        action2 = get_action_class_instance(action_cls=Action2, config=config,
573
                                            action_service=action_service)
574
        self.assertEqual(action2.config, config)
575
        self.assertEqual(action2.action_service, action_service)
576
577
        action3 = get_action_class_instance(action_cls=Action3, config=config,
578
                                            action_service=action_service)
579
        self.assertEqual(action3.config, config)
580
        self.assertEqual(action3.action_service, action_service)
581
582
    def test_action_with_same_module_name_as_module_in_stdlib(self):
583
        runner = self._get_mock_runner_obj()
584
        runner.entry_point = TEST_ACTION_PATH
585
        runner.pre_run()
586
        (status, output, _) = runner.run({})
587
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
588
        self.assertTrue(output is not None)
589
        self.assertEqual(output['result'], 'test action')
590
591
    def test_python_action_wrapper_script_doesnt_get_added_to_sys_path(self):
592
        # Validate that the directory where python_action_wrapper.py script is located
593
        # (st2common/runners) doesn't get added to sys.path
594
        runner = self._get_mock_runner_obj()
595
        runner.entry_point = PATHS_ACTION_PATH
596
        runner.pre_run()
597
        (status, output, _) = runner.run({})
598
599
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
600
        self.assertTrue(output is not None)
601
602
        lines = output['stdout'].split('\n')
603
        process_sys_path = lines[0]
604
        process_pythonpath = lines[1]
605
606
        assert 'sys.path' in process_sys_path
607
        assert 'PYTHONPATH' in process_pythonpath
608
609
        wrapper_script_path = 'st2common/runners'
610
611
        assertion_msg = 'Found python wrapper script path in subprocess path'
612
        self.assertTrue(wrapper_script_path not in process_sys_path, assertion_msg)
613
        self.assertTrue(wrapper_script_path not in process_pythonpath, assertion_msg)
614
615
    def test_python_action_wrapper_action_script_file_doesnt_exist_friendly_error(self):
616
        # File in a directory which is not a Python package
617
        wrapper = PythonActionWrapper(pack='dummy_pack_5', file_path='/tmp/doesnt.exist',
618
                                      user='joe')
619
620
        expected_msg = 'File "/tmp/doesnt.exist" has no action class or the file doesn\'t exist.'
621
        self.assertRaisesRegexp(Exception, expected_msg, wrapper._get_action_instance)
622
623
        # File in a directory which is a Python package
624
        wrapper = PythonActionWrapper(pack='dummy_pack_5', file_path=ACTION_1_PATH,
625
                                      user='joe')
626
627
        expected_msg = ('Failed to load action class from file ".*?list_repos_doesnt_exist.py" '
628
                       '\(action file most likely doesn\'t exist or contains invalid syntax\): '
629
                       '\[Errno 2\] No such file or directory')
630
        self.assertRaisesRegexp(Exception, expected_msg, wrapper._get_action_instance)
631
632
    def test_python_action_wrapper_action_script_file_contains_invalid_syntax_friendly_error(self):
633
        wrapper = PythonActionWrapper(pack='dummy_pack_5', file_path=ACTION_2_PATH,
634
                                      user='joe')
635
        expected_msg = ('Failed to load action class from file ".*?invalid_syntax.py" '
636
                       '\(action file most likely doesn\'t exist or contains invalid syntax\): '
637
                       'No module named \'?invalid\'?')
638
        self.assertRaisesRegexp(Exception, expected_msg, wrapper._get_action_instance)
639
640
    def test_simple_action_log_messages_and_log_level_runner_param(self):
641
        expected_msg_1 = 'st2.actions.python.PascalRowAction: DEBUG    Creating new Client object.'
642
        expected_msg_2 = 'Retrieving all the values from the datastore'
643
644
        expected_msg_3 = 'st2.actions.python.PascalRowAction: INFO     test info log message'
645
        expected_msg_4 = 'st2.actions.python.PascalRowAction: DEBUG    test debug log message'
646
        expected_msg_5 = 'st2.actions.python.PascalRowAction: ERROR    test error log message'
647
648
        runner = self._get_mock_runner_obj()
649
        runner.entry_point = PASCAL_ROW_ACTION_PATH
650
        runner.pre_run()
651
        (status, output, _) = runner.run({'row_index': 'e'})
652
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
653
        self.assertTrue(output is not None)
654
        self.assertEqual(output['result'], [1, 2])
655
656
        self.assertTrue(expected_msg_1 in output['stderr'])
657
        self.assertTrue(expected_msg_2 in output['stderr'])
658
        self.assertTrue(expected_msg_3 in output['stderr'])
659
        self.assertTrue(expected_msg_4 in output['stderr'])
660
        self.assertTrue(expected_msg_5 in output['stderr'])
661
662
        stderr = output['stderr'].strip().split('\n')
663
        expected_count = 5
664
665
        # Remove lines we don't care about
666
        lines = []
667
        for line in stderr:
668
            if 'configuration option is not configured' in line:
669
                continue
670
671
            if 'No handlers could be found for logger' in line:
672
                continue
673
674
            lines.append(line)
675
676
        msg = ('Expected %s lines, got %s - "%s"' % (expected_count, len(lines), str(lines)))
677
        self.assertEqual(len(lines), expected_count, msg)
678
679
        # Only log messages with level info and above should be displayed
680
        runner = self._get_mock_runner_obj()
681
        runner.entry_point = PASCAL_ROW_ACTION_PATH
682
        runner.runner_parameters = {
683
            'log_level': 'info'
684
        }
685
        runner.pre_run()
686
        (status, output, _) = runner.run({'row_index': 'e'})
687
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
688
        self.assertTrue(output is not None)
689
        self.assertEqual(output['result'], [1, 2])
690
691
        self.assertTrue(expected_msg_3 in output['stderr'])
692
        self.assertTrue(expected_msg_4 not in output['stderr'])
693
        self.assertTrue(expected_msg_5 in output['stderr'])
694
695
        # Only log messages with level error and above should be displayed
696
        runner = self._get_mock_runner_obj()
697
        runner.entry_point = PASCAL_ROW_ACTION_PATH
698
        runner.runner_parameters = {
699
            'log_level': 'error'
700
        }
701
        runner.pre_run()
702
        (status, output, _) = runner.run({'row_index': 'e'})
703
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
704
        self.assertTrue(output is not None)
705
        self.assertEqual(output['result'], [1, 2])
706
707
        self.assertTrue(expected_msg_3 not in output['stderr'])
708
        self.assertTrue(expected_msg_4 not in output['stderr'])
709
        self.assertTrue(expected_msg_5 in output['stderr'])
710
711
        # Default log level is changed in st2.config
712
        cfg.CONF.set_override(name='python_runner_log_level', override='INFO',
713
                              group='actionrunner')
714
715
        runner = self._get_mock_runner_obj()
716
        runner.entry_point = PASCAL_ROW_ACTION_PATH
717
        runner.runner_parameters = {}
718
        runner.pre_run()
719
        (status, output, _) = runner.run({'row_index': 'e'})
720
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
721
        self.assertTrue(output is not None)
722
        self.assertEqual(output['result'], [1, 2])
723
724
        self.assertTrue(expected_msg_3 in output['stderr'])
725
        self.assertTrue(expected_msg_4 not in output['stderr'])
726
        self.assertTrue(expected_msg_5 in output['stderr'])
727
728
    def test_traceback_messages_are_not_duplicated_in_stderr(self):
729
        # Verify tracebacks are not duplicated
730
        runner = self._get_mock_runner_obj()
731
        runner.entry_point = PASCAL_ROW_ACTION_PATH
732
        runner.pre_run()
733
        (status, output, _) = runner.run({'row_index': 'f'})
734
        self.assertEqual(status, LIVEACTION_STATUS_FAILED)
735
        self.assertTrue(output is not None)
736
737
        expected_msg_1 = 'Traceback (most recent'
738
        expected_msg_2 = 'ValueError: Duplicate traceback test'
739
740
        self.assertTrue(expected_msg_1 in output['stderr'])
741
        self.assertTrue(expected_msg_2 in output['stderr'])
742
743
        self.assertEqual(output['stderr'].count(expected_msg_1), 1)
744
        self.assertEqual(output['stderr'].count(expected_msg_2), 1)
745
746
    def test_execution_with_very_large_parameter(self):
747
        runner = self._get_mock_runner_obj()
748
        runner.entry_point = ECHOER_ACTION_PATH
749
        runner.pre_run()
750
        large_value = ''.join(['1' for _ in range(MAX_PARAM_LENGTH)])
751
        (status, output, _) = runner.run({'action_input': large_value})
752
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
753
        self.assertTrue(output is not None)
754
        self.assertEqual(output['result']['action_input'], large_value)
755
756
    def test_execution_with_close_to_very_large_parameter(self):
757
        runner = self._get_mock_runner_obj()
758
        runner.entry_point = ECHOER_ACTION_PATH
759
        runner.pre_run()
760
        # 21 is the minimum overhead required to make the param fall back to
761
        # param based payload. The linux max includes all parts of the param
762
        # not just the value portion. So we need to subtract the remaining
763
        # overhead from the initial padding.
764
        large_value = ''.join(['1' for _ in range(MAX_PARAM_LENGTH - 21)])
765
        (status, output, _) = runner.run({'action_input': large_value})
766
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
767
        self.assertTrue(output is not None)
768
        self.assertEqual(output['result']['action_input'], large_value)
769
770
    @mock.patch('python_runner.python_runner.get_sandbox_virtualenv_path')
771
    def test_content_version_success(self, mock_get_sandbox_virtualenv_path):
772
        mock_get_sandbox_virtualenv_path.return_value = None
773
774
        # 1. valid version - 0.2.0
775
        runner = self._get_mock_runner_obj(pack='test_content_version', sandbox=False)
776
        runner.entry_point = PRINT_VERSION_ACTION
777
        runner.runner_parameters = {'content_version': 'v0.2.0'}
778
        runner.pre_run()
779
780
        (status, output, _) = runner.run({})
781
782
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
783
        self.assertEqual(output['result'], 'v0.2.0')
784
        self.assertEqual(output['stdout'].strip(), 'v0.2.0')
785
786
        # 2. valid version - 0.23.0
787
        runner = self._get_mock_runner_obj(pack='test_content_version', sandbox=False)
788
        runner.entry_point = PRINT_VERSION_ACTION
789
        runner.runner_parameters = {'content_version': 'v0.3.0'}
790
        runner.pre_run()
791
792
        (status, output, _) = runner.run({})
793
794
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
795
        self.assertEqual(output['result'], 'v0.3.0')
796
        self.assertEqual(output['stdout'].strip(), 'v0.3.0')
797
798
        # 3. invalid version = 0.30.0
799
        runner = self._get_mock_runner_obj(pack='test_content_version', sandbox=False)
800
        runner.entry_point = PRINT_VERSION_ACTION
801
        runner.runner_parameters = {'content_version': 'v0.30.0'}
802
803
        expected_msg = (r'Failed to create git worktree for pack "test_content_version": '
804
                        'Invalid content_version '
805
                        '"v0.30.0" provided. Make sure that git repository is up '
806
                        'to date and contains that revision.')
807
        self.assertRaisesRegexp(ValueError, expected_msg, runner.pre_run)
808
809
    @mock.patch('python_runner.python_runner.get_sandbox_virtualenv_path')
810
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
811
    def test_content_version_contains_common_libs_config_enabled(self, mock_popen,
812
                                                                 mock_get_sandbox_virtualenv_path):
813
        # Verify that the common libs path correctly reflects directory in git worktree
814
        mock_get_sandbox_virtualenv_path.return_value = None
815
816
        mock_process = mock.Mock()
817
        mock_process.communicate.return_value = ('', '')
818
        mock_popen.return_value = mock_process
819
820
        runner = self._get_mock_runner_obj(pack='test_content_version', sandbox=False)
821
        runner._enable_common_pack_libs = True
822
        runner.auth_token = mock.Mock()
823
        runner.auth_token.token = 'ponies'
824
        runner.runner_parameters = {'content_version': 'v0.3.0'}
825
        runner.entry_point = PRINT_VERSION_ACTION
826
        runner.pre_run()
827
        (_, _, _) = runner.run({'row_index': 4})
828
829
        _, call_kwargs = mock_popen.call_args
830
        actual_env = call_kwargs['env']
831
        pack_common_lib_path = os.path.join(runner.git_worktree_path, 'lib')
832
        self.assertTrue('PYTHONPATH' in actual_env)
833
        self.assertTrue(pack_common_lib_path in actual_env['PYTHONPATH'])
834
835
    @mock.patch('python_runner.python_runner.get_sandbox_virtualenv_path')
836
    def test_content_version_success_local_modules_work_fine(self,
837
                                                             mock_get_sandbox_virtualenv_path):
838
        # Verify that local module import correctly use git worktree directory
839
        mock_get_sandbox_virtualenv_path.return_value = None
840
841
        runner = self._get_mock_runner_obj(pack='test_content_version', sandbox=False)
842
        runner.entry_point = PRINT_VERSION_LOCAL_MODULE_ACTION
843
        runner.runner_parameters = {'content_version': 'v0.2.0'}
844
        runner.pre_run()
845
846
        (status, output, _) = runner.run({})
847
848
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
849
        self.assertEqual(output['result'], 'v0.2.0')
850
851
        # Verify local_module has been correctly loaded from git work tree directory
852
        expected_stdout = ("<module '?local_module'? from '?%s/actions/local_module.py'?>.*" %
853
                           runner.git_worktree_path)
854
        self.assertRegexpMatches(output['stdout'].strip(), expected_stdout)
855
856
    @mock.patch('st2common.runners.base.run_command')
857
    def test_content_version_old_git_version(self, mock_run_command):
858
        mock_stdout = ''
859
        mock_stderr = '''
860
git: 'worktree' is not a git command. See 'git --help'.
861
'''
862
        mock_stderr = six.text_type(mock_stderr)
863
        mock_run_command.return_value = 1, mock_stdout, mock_stderr, False
864
865
        runner = self._get_mock_runner_obj()
866
        runner.entry_point = PASCAL_ROW_ACTION_PATH
867
        runner.runner_parameters = {'content_version': 'v0.10.0'}
868
869
        expected_msg = (r'Failed to create git worktree for pack "core": Installed git version '
870
                        'doesn\'t support git worktree command. To be able to utilize this '
871
                        'functionality you need to use git >= 2.5.0.')
872
        self.assertRaisesRegexp(ValueError, expected_msg, runner.pre_run)
873
874
    @mock.patch('st2common.runners.base.run_command')
875
    def test_content_version_pack_repo_not_git_repository(self, mock_run_command):
876
        mock_stdout = ''
877
        mock_stderr = '''
878
fatal: Not a git repository (or any parent up to mount point /home)
879
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
880
'''
881
        mock_stderr = six.text_type(mock_stderr)
882
        mock_run_command.return_value = 1, mock_stdout, mock_stderr, False
883
884
        runner = self._get_mock_runner_obj()
885
        runner.entry_point = PASCAL_ROW_ACTION_PATH
886
        runner.runner_parameters = {'content_version': 'v0.10.0'}
887
888
        expected_msg = (r'Failed to create git worktree for pack "core": Pack directory '
889
                        '".*" is not a '
890
                        'git repository. To utilize this functionality, pack directory needs to '
891
                        'be a git repository.')
892
        self.assertRaisesRegexp(ValueError, expected_msg, runner.pre_run)
893
894
    @mock.patch('st2common.runners.base.run_command')
895
    def test_content_version_invalid_git_revision(self, mock_run_command):
896
        mock_stdout = ''
897
        mock_stderr = '''
898
fatal: invalid reference: vinvalid
899
'''
900
        mock_stderr = six.text_type(mock_stderr)
901
        mock_run_command.return_value = 1, mock_stdout, mock_stderr, False
902
903
        runner = self._get_mock_runner_obj()
904
        runner.entry_point = PASCAL_ROW_ACTION_PATH
905
        runner.runner_parameters = {'content_version': 'vinvalid'}
906
907
        expected_msg = (r'Failed to create git worktree for pack "core": Invalid content_version '
908
                        '"vinvalid" provided. Make sure that git repository is up '
909
                        'to date and contains that revision.')
910
        self.assertRaisesRegexp(ValueError, expected_msg, runner.pre_run)
911
912
    def test_missing_config_item_user_friendly_error(self):
913
        runner = self._get_mock_runner_obj()
914
        runner.entry_point = PRINT_CONFIG_ITEM_ACTION
915
        runner.pre_run()
916
        (status, output, _) = runner.run({})
917
918
        self.assertEqual(status, LIVEACTION_STATUS_FAILED)
919
        self.assertTrue(output is not None)
920
        self.assertTrue('{}' in output['stdout'])
921
        self.assertTrue('default_value' in output['stdout'])
922
        self.assertTrue('Config for pack "core" is missing key "key"' in output['stderr'])
923
        self.assertTrue('make sure you run "st2ctl reload --register-configs"' in output['stderr'])
924
925
    def _get_mock_runner_obj(self, pack=None, sandbox=None):
926
        runner = python_runner.get_runner()
927
        runner.execution = MOCK_EXECUTION
928
        runner.action = self._get_mock_action_obj()
929
        runner.runner_parameters = {}
930
931
        if pack:
932
            runner.action.pack = pack
933
934
        if sandbox is not None:
935
            runner._sandbox = sandbox
936
937
        return runner
938
939
    @mock.patch('st2actions.container.base.ActionExecution.get', mock.Mock())
940
    def _get_mock_runner_obj_from_container(self, pack, user, sandbox=None):
941
        container = RunnerContainer()
942
943
        runnertype_db = mock.Mock()
944
        runnertype_db.runner_package = 'python_runner'
945
        runnertype_db.runner_module = 'python_runner'
946
        action_db = mock.Mock()
947
        action_db.pack = pack
948
        action_db.entry_point = 'foo.py'
949
        liveaction_db = mock.Mock()
950
        liveaction_db.id = '123'
951
        liveaction_db.context = {'user': user}
952
        runner = container._get_runner(runner_type_db=runnertype_db, action_db=action_db,
953
                                       liveaction_db=liveaction_db)
954
        runner.execution = MOCK_EXECUTION
955
        runner.action = action_db
956
        runner.runner_parameters = {}
957
958
        if sandbox is not None:
959
            runner._sandbox = sandbox
960
961
        return runner
962
963
    def _get_mock_action_obj(self):
964
        """
965
        Return mock action object.
966
967
        Pack gets set to the system pack so the action doesn't require a separate virtualenv.
968
        """
969
        action = mock.Mock()
970
        action.ref = 'dummy.action'
971
        action.pack = SYSTEM_PACK_NAME
972
        action.entry_point = 'foo.py'
973
        action.runner_type = {
974
            'name': 'python-script'
975
        }
976
        return action
977