Passed
Push — master ( 6aeca2...dec5f2 )
by
unknown
03:57
created

LocalShellCommandRunnerTestCase   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 380
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 380
rs 10
wmc 23

13 Methods

Rating   Name   Duplication   Size   Complexity  
A setUp() 0 5 1
A test_action_stdout_and_stderr_is_stored_in_the_db() 0 57 1
B test_shell_command_action_basic() 0 32 1
A test_shell_script_action() 0 12 1
A test_timeout() 0 10 1
A test_shutdown() 0 11 1
A test_common_st2_env_vars_are_available_to_the_action() 0 20 1
A test_large_stdout() 0 13 1
A test_sudo_and_env_variable_preservation() 0 17 1
B test_action_stdout_and_stderr_is_stored_in_the_db_short_running_action() 0 80 6
B _get_runner() 0 28 1
B test_shell_command_sudo_password_is_passed_to_sudo_binary() 0 60 6
A test_shell_command_invalid_stdout_password() 0 20 1
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 uuid
18
19
import mock
20
from oslo_config import cfg
21
22
import st2tests.config as tests_config
23
tests_config.parse_args()
24
25
from st2common.constants import action as action_constants
26
from st2common.persistence.execution import ActionExecutionOutput
27
from st2tests.fixturesloader import FixturesLoader
28
from st2tests.fixturesloader import get_fixtures_base_path
29
from st2common.util.api import get_full_public_api_url
30
from st2common.util.green import shell
31
from st2common.constants.runners import LOCAL_RUNNER_DEFAULT_ACTION_TIMEOUT
32
from st2tests.base import RunnerTestCase
33
from st2tests.base import CleanDbTestCase
34
from st2tests.base import blocking_eventlet_spawn
35
from st2tests.base import make_mock_stream_readline
36
from local_runner import local_runner
37
38
__all__ = [
39
    'LocalShellCommandRunnerTestCase',
40
    'LocalShellScriptRunnerTestCase'
41
]
42
43
MOCK_EXECUTION = mock.Mock()
44
MOCK_EXECUTION.id = '598dbf0c0640fd54bffc688b'
45
46
47
class LocalShellCommandRunnerTestCase(RunnerTestCase, CleanDbTestCase):
48
    fixtures_loader = FixturesLoader()
49
50
    def setUp(self):
51
        super(LocalShellCommandRunnerTestCase, self).setUp()
52
53
        # False is a default behavior so end result should be the same
54
        cfg.CONF.set_override(name='stream_output', group='actionrunner', override=False)
55
56
    def test_shell_command_action_basic(self):
57
        models = self.fixtures_loader.load_models(
58
            fixtures_pack='generic', fixtures_dict={'actions': ['local.yaml']})
59
        action_db = models['actions']['local.yaml']
60
61
        runner = self._get_runner(action_db, cmd='echo 10')
62
        runner.pre_run()
63
        status, result, _ = runner.run({})
64
        runner.post_run(status, result)
65
66
        self.assertEquals(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
67
        self.assertEquals(result['stdout'], 10)
68
69
        # End result should be the same when streaming is enabled
70
        cfg.CONF.set_override(name='stream_output', group='actionrunner', override=True)
71
72
        # Verify initial state
73
        output_dbs = ActionExecutionOutput.get_all()
74
        self.assertEqual(len(output_dbs), 0)
75
76
        runner = self._get_runner(action_db, cmd='echo 10')
77
        runner.pre_run()
78
        status, result, _ = runner.run({})
79
        runner.post_run(status, result)
80
81
        self.assertEquals(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
82
        self.assertEquals(result['stdout'], 10)
83
84
        output_dbs = ActionExecutionOutput.get_all()
85
        self.assertEqual(len(output_dbs), 1)
86
        self.assertEqual(output_dbs[0].output_type, 'stdout')
87
        self.assertEqual(output_dbs[0].data, '10\n')
88
89
    def test_shell_script_action(self):
90
        models = self.fixtures_loader.load_models(
91
            fixtures_pack='localrunner_pack', fixtures_dict={'actions': ['text_gen.yml']})
92
        action_db = models['actions']['text_gen.yml']
93
        entry_point = self.fixtures_loader.get_fixture_file_path_abs(
94
            'localrunner_pack', 'actions', 'text_gen.py')
95
        runner = self._get_runner(action_db, entry_point=entry_point)
96
        runner.pre_run()
97
        status, result, _ = runner.run({'chars': 1000})
98
        runner.post_run(status, result)
99
        self.assertEquals(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
100
        self.assertEquals(len(result['stdout']), 1000)
101
102
    def test_timeout(self):
103
        models = self.fixtures_loader.load_models(
104
            fixtures_pack='generic', fixtures_dict={'actions': ['local.yaml']})
105
        action_db = models['actions']['local.yaml']
106
        # smaller timeout == faster tests.
107
        runner = self._get_runner(action_db, cmd='sleep 10', timeout=0.01)
108
        runner.pre_run()
109
        status, result, _ = runner.run({})
110
        runner.post_run(status, result)
111
        self.assertEquals(status, action_constants.LIVEACTION_STATUS_TIMED_OUT)
112
113
    @mock.patch.object(
114
        shell, 'run_command',
115
        mock.MagicMock(return_value=(-15, '', '', False)))
116
    def test_shutdown(self):
117
        models = self.fixtures_loader.load_models(
118
            fixtures_pack='generic', fixtures_dict={'actions': ['local.yaml']})
119
        action_db = models['actions']['local.yaml']
120
        runner = self._get_runner(action_db, cmd='sleep 0.1')
121
        runner.pre_run()
122
        status, result, _ = runner.run({})
123
        self.assertEquals(status, action_constants.LIVEACTION_STATUS_ABANDONED)
124
125
    def test_large_stdout(self):
126
        models = self.fixtures_loader.load_models(
127
            fixtures_pack='localrunner_pack', fixtures_dict={'actions': ['text_gen.yml']})
128
        action_db = models['actions']['text_gen.yml']
129
        entry_point = self.fixtures_loader.get_fixture_file_path_abs(
130
            'localrunner_pack', 'actions', 'text_gen.py')
131
        runner = self._get_runner(action_db, entry_point=entry_point)
132
        runner.pre_run()
133
        char_count = 10 ** 6  # Note 10^7 succeeds but ends up being slow.
134
        status, result, _ = runner.run({'chars': char_count})
135
        runner.post_run(status, result)
136
        self.assertEquals(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
137
        self.assertEquals(len(result['stdout']), char_count)
138
139
    def test_common_st2_env_vars_are_available_to_the_action(self):
140
        models = self.fixtures_loader.load_models(
141
            fixtures_pack='generic', fixtures_dict={'actions': ['local.yaml']})
142
        action_db = models['actions']['local.yaml']
143
144
        runner = self._get_runner(action_db, cmd='echo $ST2_ACTION_API_URL')
145
        runner.pre_run()
146
        status, result, _ = runner.run({})
147
        runner.post_run(status, result)
148
149
        self.assertEquals(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
150
        self.assertEqual(result['stdout'].strip(), get_full_public_api_url())
151
152
        runner = self._get_runner(action_db, cmd='echo $ST2_ACTION_AUTH_TOKEN')
153
        runner.pre_run()
154
        status, result, _ = runner.run({})
155
        runner.post_run(status, result)
156
157
        self.assertEquals(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
158
        self.assertEqual(result['stdout'].strip(), 'mock-token')
159
160
    def test_sudo_and_env_variable_preservation(self):
161
        # Verify that the environment environment are correctly preserved when running as a
162
        # root / non-system user
163
        # Note: This test will fail if SETENV option is not present in the sudoers file
164
        models = self.fixtures_loader.load_models(
165
            fixtures_pack='generic', fixtures_dict={'actions': ['local.yaml']})
166
        action_db = models['actions']['local.yaml']
167
168
        cmd = 'echo `whoami` ; echo ${VAR1}'
169
        env = {'VAR1': 'poniesponies'}
170
        runner = self._get_runner(action_db, cmd=cmd, sudo=True, env=env)
171
        runner.pre_run()
172
        status, result, _ = runner.run({})
173
        runner.post_run(status, result)
174
175
        self.assertEquals(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
176
        self.assertEqual(result['stdout'].strip(), 'root\nponiesponies')
177
178
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
179
    @mock.patch('st2common.util.green.shell.eventlet.spawn')
180
    def test_action_stdout_and_stderr_is_stored_in_the_db(self, mock_spawn, mock_popen):
181
        # Feature is enabled
182
        cfg.CONF.set_override(name='stream_output', group='actionrunner', override=True)
183
184
        # Note: We need to mock spawn function so we can test everything in single event loop
185
        # iteration
186
        mock_spawn.side_effect = blocking_eventlet_spawn
187
188
        # No output to stdout and no result (implicit None)
189
        mock_stdout = [
190
            'stdout line 1\n',
191
            'stdout line 2\n',
192
        ]
193
        mock_stderr = [
194
            'stderr line 1\n',
195
            'stderr line 2\n',
196
            'stderr line 3\n'
197
        ]
198
199
        mock_process = mock.Mock()
200
        mock_process.returncode = 0
201
        mock_popen.return_value = mock_process
202
        mock_process.stdout.closed = False
203
        mock_process.stderr.closed = False
204
        mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout,
205
                                                                 stop_counter=2)
206
        mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr,
207
                                                                 stop_counter=3)
208
209
        models = self.fixtures_loader.load_models(
210
            fixtures_pack='generic', fixtures_dict={'actions': ['local.yaml']})
211
        action_db = models['actions']['local.yaml']
212
213
        runner = self._get_runner(action_db, cmd='echo $ST2_ACTION_API_URL')
214
        runner.pre_run()
215
        status, result, _ = runner.run({})
216
        runner.post_run(status, result)
217
218
        self.assertEquals(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
219
220
        self.assertEqual(result['stdout'], 'stdout line 1\nstdout line 2')
221
        self.assertEqual(result['stderr'], 'stderr line 1\nstderr line 2\nstderr line 3')
222
        self.assertEqual(result['return_code'], 0)
223
224
        # Verify stdout and stderr lines have been correctly stored in the db
225
        output_dbs = ActionExecutionOutput.query(output_type='stdout')
226
        self.assertEqual(len(output_dbs), 2)
227
        self.assertEqual(output_dbs[0].data, mock_stdout[0])
228
        self.assertEqual(output_dbs[1].data, mock_stdout[1])
229
230
        output_dbs = ActionExecutionOutput.query(output_type='stderr')
231
        self.assertEqual(len(output_dbs), 3)
232
        self.assertEqual(output_dbs[0].data, mock_stderr[0])
233
        self.assertEqual(output_dbs[1].data, mock_stderr[1])
234
        self.assertEqual(output_dbs[2].data, mock_stderr[2])
235
236
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
237
    @mock.patch('st2common.util.green.shell.eventlet.spawn')
238
    def test_action_stdout_and_stderr_is_stored_in_the_db_short_running_action(self, mock_spawn,
239
                                                                               mock_popen):
240
        # Verify that we correctly retrieve all the output and wait for stdout and stderr reading
241
        # threads for short running actions.
242
        models = self.fixtures_loader.load_models(
243
            fixtures_pack='generic', fixtures_dict={'actions': ['local.yaml']})
244
        action_db = models['actions']['local.yaml']
245
246
        # Feature is enabled
247
        cfg.CONF.set_override(name='stream_output', group='actionrunner', override=True)
248
249
        # Note: We need to mock spawn function so we can test everything in single event loop
250
        # iteration
251
        mock_spawn.side_effect = blocking_eventlet_spawn
252
253
        # No output to stdout and no result (implicit None)
254
        mock_stdout = [
255
            'stdout line 1\n',
256
            'stdout line 2\n'
257
        ]
258
        mock_stderr = [
259
            'stderr line 1\n',
260
            'stderr line 2\n'
261
        ]
262
263
        # We add a sleep to simulate action process exiting before we finish reading data from
264
        mock_process = mock.Mock()
265
        mock_process.returncode = 0
266
        mock_popen.return_value = mock_process
267
        mock_process.stdout.closed = False
268
        mock_process.stderr.closed = False
269
        mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout,
270
                                                                 stop_counter=2,
271
                                                                 sleep_delay=1)
272
        mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr,
273
                                                                 stop_counter=2)
274
275
        for index in range(1, 4):
276
            mock_process.stdout.closed = False
277
            mock_process.stderr.closed = False
278
279
            mock_process.stdout.counter = 0
280
            mock_process.stderr.counter = 0
281
282
            runner = self._get_runner(action_db, cmd='echo "foobar"')
283
            runner.pre_run()
284
            status, result, _ = runner.run({})
285
286
            self.assertEquals(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
287
288
            self.assertEqual(result['stdout'], 'stdout line 1\nstdout line 2')
289
            self.assertEqual(result['stderr'], 'stderr line 1\nstderr line 2')
290
            self.assertEqual(result['return_code'], 0)
291
292
            # Verify stdout and stderr lines have been correctly stored in the db
293
            output_dbs = ActionExecutionOutput.query(output_type='stdout')
294
295
            if index == 1:
296
                db_index_1 = 0
297
                db_index_2 = 1
298
            elif index == 2:
299
                db_index_1 = 2
300
                db_index_2 = 3
301
            elif index == 3:
302
                db_index_1 = 4
303
                db_index_2 = 5
304
            elif index == 4:
305
                db_index_1 = 6
306
                db_index_2 = 7
307
308
            self.assertEqual(len(output_dbs), (index * 2))
309
            self.assertEqual(output_dbs[db_index_1].data, mock_stdout[0])
310
            self.assertEqual(output_dbs[db_index_2].data, mock_stdout[1])
311
312
            output_dbs = ActionExecutionOutput.query(output_type='stderr')
313
            self.assertEqual(len(output_dbs), (index * 2))
314
            self.assertEqual(output_dbs[db_index_1].data, mock_stderr[0])
315
            self.assertEqual(output_dbs[db_index_2].data, mock_stderr[1])
316
317
    def test_shell_command_sudo_password_is_passed_to_sudo_binary(self):
318
        # Verify that sudo password is correctly passed to sudo binary via stdin
319
        models = self.fixtures_loader.load_models(
320
            fixtures_pack='generic', fixtures_dict={'actions': ['local.yaml']})
321
        action_db = models['actions']['local.yaml']
322
323
        sudo_passwords = [
324
            'pass 1',
325
            'sudopass',
326
            '$sudo p@ss 2'
327
        ]
328
329
        cmd = ('{ read sudopass; echo $sudopass; }')
330
331
        # without sudo
332
        for sudo_password in sudo_passwords:
333
            runner = self._get_runner(action_db, cmd=cmd)
334
            runner.pre_run()
335
            runner._sudo_password = sudo_password
336
            status, result, _ = runner.run({})
337
            runner.post_run(status, result)
338
339
            self.assertEquals(status,
340
                    action_constants.LIVEACTION_STATUS_SUCCEEDED)
341
            self.assertEquals(result['stdout'], sudo_password)
342
343
        # with sudo
344
        for sudo_password in sudo_passwords:
345
            runner = self._get_runner(action_db, cmd=cmd)
346
            runner.pre_run()
347
            runner._sudo = True
348
            runner._sudo_password = sudo_password
349
            status, result, _ = runner.run({})
350
            runner.post_run(status, result)
351
352
            self.assertEquals(status,
353
                    action_constants.LIVEACTION_STATUS_SUCCEEDED)
354
            self.assertEquals(result['stdout'], sudo_password)
355
356
        # Verify new process which provides password via stdin to the command is created
357
        with mock.patch('eventlet.green.subprocess.Popen') as mock_subproc_popen:
358
            index = 0
359
            for sudo_password in sudo_passwords:
360
                runner = self._get_runner(action_db, cmd=cmd)
361
                runner.pre_run()
362
                runner._sudo = True
363
                runner._sudo_password = sudo_password
364
                status, result, _ = runner.run({})
365
                runner.post_run(status, result)
366
367
                if index == 0:
368
                    call_args = mock_subproc_popen.call_args_list[index]
369
                else:
370
                    call_args = mock_subproc_popen.call_args_list[index * 2]
371
372
                index += 1
373
374
                self.assertEqual(call_args[0][0], ['echo', '%s\n' % (sudo_password)])
375
376
        self.assertEqual(index, len(sudo_passwords))
377
378
    def test_shell_command_invalid_stdout_password(self):
379
        # Simulate message printed to stderr by sudo when invalid sudo password is provided
380
        models = self.fixtures_loader.load_models(
381
            fixtures_pack='generic', fixtures_dict={'actions': ['local.yaml']})
382
        action_db = models['actions']['local.yaml']
383
384
        cmd = ('echo  "[sudo] password for bar: Sorry, try again.\n[sudo] password for bar:'
385
               ' Sorry, try again.\n[sudo] password for bar: \nsudo: 2 incorrect password '
386
               'attempts" 1>&2; exit 1')
387
        runner = self._get_runner(action_db, cmd=cmd)
388
        runner.pre_run()
389
        runner._sudo_password = 'pass'
390
        status, result, _ = runner.run({})
391
        runner.post_run(status, result)
392
393
        expected_error = ('Invalid sudo password provided or sudo is not configured for this '
394
                          'user (bar)')
395
        self.assertEquals(status, action_constants.LIVEACTION_STATUS_FAILED)
396
        self.assertEqual(result['error'], expected_error)
397
        self.assertEquals(result['stdout'], '')
398
399
    @staticmethod
400
    def _get_runner(action_db,
401
                    entry_point=None,
402
                    cmd=None,
403
                    on_behalf_user=None,
404
                    user=None,
405
                    kwarg_op=local_runner.DEFAULT_KWARG_OP,
406
                    timeout=LOCAL_RUNNER_DEFAULT_ACTION_TIMEOUT,
407
                    sudo=False,
408
                    env=None):
409
        runner = local_runner.LocalShellRunner(uuid.uuid4().hex)
410
        runner.execution = MOCK_EXECUTION
411
        runner.action = action_db
412
        runner.action_name = action_db.name
413
        runner.liveaction_id = uuid.uuid4().hex
414
        runner.entry_point = entry_point
415
        runner.runner_parameters = {local_runner.RUNNER_COMMAND: cmd,
416
                                    local_runner.RUNNER_SUDO: sudo,
417
                                    local_runner.RUNNER_ENV: env,
418
                                    local_runner.RUNNER_ON_BEHALF_USER: user,
419
                                    local_runner.RUNNER_KWARG_OP: kwarg_op,
420
                                    local_runner.RUNNER_TIMEOUT: timeout}
421
        runner.context = dict()
422
        runner.callback = dict()
423
        runner.libs_dir_path = None
424
        runner.auth_token = mock.Mock()
425
        runner.auth_token.token = 'mock-token'
426
        return runner
427
428
429
class LocalShellScriptRunnerTestCase(RunnerTestCase, CleanDbTestCase):
430
    fixtures_loader = FixturesLoader()
431
432
    def setUp(self):
433
        super(LocalShellScriptRunnerTestCase, self).setUp()
434
435
        # False is a default behavior so end result should be the same
436
        cfg.CONF.set_override(name='stream_output', group='actionrunner', override=False)
437
438
    def test_script_with_paramters_parameter_serialization(self):
439
        models = self.fixtures_loader.load_models(
440
            fixtures_pack='generic', fixtures_dict={'actions': ['local_script_with_params.yaml']})
441
        action_db = models['actions']['local_script_with_params.yaml']
442
        entry_point = os.path.join(get_fixtures_base_path(),
443
                                   'generic/actions/local_script_with_params.sh')
444
445
        action_parameters = {
446
            'param_string': 'test string',
447
            'param_integer': 1,
448
            'param_float': 2.55,
449
            'param_boolean': True,
450
            'param_list': ['a', 'b', 'c'],
451
            'param_object': {'foo': 'bar'}
452
        }
453
454
        runner = self._get_runner(action_db=action_db, entry_point=entry_point)
455
        runner.pre_run()
456
        status, result, _ = runner.run(action_parameters=action_parameters)
457
        runner.post_run(status, result)
458
459
        self.assertEqual(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
460
        self.assertTrue('PARAM_STRING=test string' in result['stdout'])
461
        self.assertTrue('PARAM_INTEGER=1' in result['stdout'])
462
        self.assertTrue('PARAM_FLOAT=2.55' in result['stdout'])
463
        self.assertTrue('PARAM_BOOLEAN=1' in result['stdout'])
464
        self.assertTrue('PARAM_LIST=a,b,c' in result['stdout'])
465
        self.assertTrue('PARAM_OBJECT={"foo": "bar"}' in result['stdout'])
466
467
        action_parameters = {
468
            'param_string': 'test string',
469
            'param_integer': 1,
470
            'param_float': 2.55,
471
            'param_boolean': False,
472
            'param_list': ['a', 'b', 'c'],
473
            'param_object': {'foo': 'bar'}
474
        }
475
476
        runner = self._get_runner(action_db=action_db, entry_point=entry_point)
477
        runner.pre_run()
478
        status, result, _ = runner.run(action_parameters=action_parameters)
479
        runner.post_run(status, result)
480
481
        self.assertEqual(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
482
        self.assertTrue('PARAM_BOOLEAN=0' in result['stdout'])
483
484
        action_parameters = {
485
            'param_string': '',
486
            'param_integer': None,
487
            'param_float': None,
488
        }
489
490
        runner = self._get_runner(action_db=action_db, entry_point=entry_point)
491
        runner.pre_run()
492
        status, result, _ = runner.run(action_parameters=action_parameters)
493
        runner.post_run(status, result)
494
495
        self.assertEqual(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
496
        self.assertTrue('PARAM_STRING=\n' in result['stdout'])
497
        self.assertTrue('PARAM_INTEGER=\n' in result['stdout'])
498
        self.assertTrue('PARAM_FLOAT=\n' in result['stdout'])
499
500
        # End result should be the same when streaming is enabled
501
        cfg.CONF.set_override(name='stream_output', group='actionrunner', override=True)
502
503
        # Verify initial state
504
        output_dbs = ActionExecutionOutput.get_all()
505
        self.assertEqual(len(output_dbs), 0)
506
507
        action_parameters = {
508
            'param_string': 'test string',
509
            'param_integer': 1,
510
            'param_float': 2.55,
511
            'param_boolean': True,
512
            'param_list': ['a', 'b', 'c'],
513
            'param_object': {'foo': 'bar'}
514
        }
515
516
        runner = self._get_runner(action_db=action_db, entry_point=entry_point)
517
        runner.pre_run()
518
        status, result, _ = runner.run(action_parameters=action_parameters)
519
        runner.post_run(status, result)
520
521
        self.assertEqual(status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
522
        self.assertTrue('PARAM_STRING=test string' in result['stdout'])
523
        self.assertTrue('PARAM_INTEGER=1' in result['stdout'])
524
        self.assertTrue('PARAM_FLOAT=2.55' in result['stdout'])
525
        self.assertTrue('PARAM_BOOLEAN=1' in result['stdout'])
526
        self.assertTrue('PARAM_LIST=a,b,c' in result['stdout'])
527
        self.assertTrue('PARAM_OBJECT={"foo": "bar"}' in result['stdout'])
528
529
        output_dbs = ActionExecutionOutput.query(output_type='stdout')
530
        self.assertEqual(len(output_dbs), 6)
531
        self.assertEqual(output_dbs[0].data, 'PARAM_STRING=test string\n')
532
        self.assertEqual(output_dbs[5].data, 'PARAM_OBJECT={"foo": "bar"}\n')
533
534
        output_dbs = ActionExecutionOutput.query(output_type='stderr')
535
        self.assertEqual(len(output_dbs), 0)
536
537
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
538
    @mock.patch('st2common.util.green.shell.eventlet.spawn')
539
    def test_action_stdout_and_stderr_is_stored_in_the_db(self, mock_spawn, mock_popen):
540
        # Feature is enabled
541
        cfg.CONF.set_override(name='stream_output', group='actionrunner', override=True)
542
543
        # Note: We need to mock spawn function so we can test everything in single event loop
544
        # iteration
545
        mock_spawn.side_effect = blocking_eventlet_spawn
546
547
        # No output to stdout and no result (implicit None)
548
        mock_stdout = [
549
            'stdout line 1\n',
550
            'stdout line 2\n',
551
            'stdout line 3\n',
552
            'stdout line 4\n'
553
        ]
554
        mock_stderr = [
555
            'stderr line 1\n',
556
            'stderr line 2\n',
557
            'stderr line 3\n'
558
        ]
559
560
        mock_process = mock.Mock()
561
        mock_process.returncode = 0
562
        mock_popen.return_value = mock_process
563
        mock_process.stdout.closed = False
564
        mock_process.stderr.closed = False
565
        mock_process.stdout.readline = make_mock_stream_readline(mock_process.stdout, mock_stdout,
566
                                                                 stop_counter=4)
567
        mock_process.stderr.readline = make_mock_stream_readline(mock_process.stderr, mock_stderr,
568
                                                                 stop_counter=3)
569
570
        models = self.fixtures_loader.load_models(
571
            fixtures_pack='generic', fixtures_dict={'actions': ['local_script_with_params.yaml']})
572
        action_db = models['actions']['local_script_with_params.yaml']
573
        entry_point = os.path.join(get_fixtures_base_path(),
574
                                   'generic/actions/local_script_with_params.sh')
575
576
        action_parameters = {
577
            'param_string': 'test string',
578
            'param_integer': 1,
579
            'param_float': 2.55,
580
            'param_boolean': True,
581
            'param_list': ['a', 'b', 'c'],
582
            'param_object': {'foo': 'bar'}
583
        }
584
585
        runner = self._get_runner(action_db=action_db, entry_point=entry_point)
586
        runner.pre_run()
587
        status, result, _ = runner.run(action_parameters=action_parameters)
588
        runner.post_run(status, result)
589
590
        self.assertEqual(result['stdout'],
591
                         'stdout line 1\nstdout line 2\nstdout line 3\nstdout line 4')
592
        self.assertEqual(result['stderr'], 'stderr line 1\nstderr line 2\nstderr line 3')
593
        self.assertEqual(result['return_code'], 0)
594
595
        # Verify stdout and stderr lines have been correctly stored in the db
596
        output_dbs = ActionExecutionOutput.query(output_type='stdout')
597
        self.assertEqual(len(output_dbs), 4)
598
        self.assertEqual(output_dbs[0].data, mock_stdout[0])
599
        self.assertEqual(output_dbs[1].data, mock_stdout[1])
600
        self.assertEqual(output_dbs[2].data, mock_stdout[2])
601
        self.assertEqual(output_dbs[3].data, mock_stdout[3])
602
603
        output_dbs = ActionExecutionOutput.query(output_type='stderr')
604
        self.assertEqual(len(output_dbs), 3)
605
        self.assertEqual(output_dbs[0].data, mock_stderr[0])
606
        self.assertEqual(output_dbs[1].data, mock_stderr[1])
607
        self.assertEqual(output_dbs[2].data, mock_stderr[2])
608
609
    def _get_runner(self, action_db, entry_point):
610
        runner = local_runner.LocalShellRunner(uuid.uuid4().hex)
611
        runner.execution = MOCK_EXECUTION
612
        runner.action = action_db
613
        runner.action_name = action_db.name
614
        runner.liveaction_id = uuid.uuid4().hex
615
        runner.entry_point = entry_point
616
        runner.runner_parameters = {}
617
        runner.context = dict()
618
        runner.callback = dict()
619
        runner.libs_dir_path = None
620
        runner.auth_token = mock.Mock()
621
        runner.auth_token.token = 'mock-token'
622
        return runner
623