Passed
Pull Request — master (#3163)
by W
05:12
created

test_callback_incomplete_state()   B

Complexity

Conditions 1

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 32
rs 8.8571
c 0
b 0
f 0
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 mock
17
from mock import call
18
import requests
19
import uuid
20
21
from mistralclient.api.v2 import action_executions
22
from oslo_config import cfg
23
24
# XXX: actionsensor import depends on config being setup.
25
import st2tests.config as tests_config
26
tests_config.parse_args()
27
28
from st2common.bootstrap import actionsregistrar
29
from st2common.bootstrap import runnersregistrar
30
from st2common.constants import action as action_constants
31
from st2common.models.db.execution import ActionExecutionDB
32
from st2common.models.db.liveaction import LiveActionDB
33
from st2common.persistence.execution import ActionExecution
34
from st2common.persistence.liveaction import LiveAction
35
from st2common.runners import base as runners
36
from st2common.services import action as action_service
37
from st2common.services import trace as trace_service
38
from st2common.transport.liveaction import LiveActionPublisher
39
from st2common.transport.publishers import CUDPublisher
40
from st2common.util import loader
41
from st2tests import DbTestCase
42
from st2tests import fixturesloader
43
from st2tests.mocks.liveaction import MockLiveActionPublisher
44
45
46
MISTRAL_RUNNER_NAME = 'mistral_v2'
47
TEST_PACK = 'mistral_tests'
48
TEST_PACK_PATH = fixturesloader.get_fixtures_packs_base_path() + '/' + TEST_PACK
49
50
PACKS = [
51
    TEST_PACK_PATH,
52
    fixturesloader.get_fixtures_packs_base_path() + '/core'
53
]
54
55
NON_EMPTY_RESULT = 'non-empty'
56
57
MOCK_ACTION_EXEC_DB = ActionExecutionDB(
58
    action={'ref': 'mock.workflow'},
59
    runner={'ref': 'mock.runner'},
60
    liveaction={'id': uuid.uuid4().hex},
61
    status=action_constants.LIVEACTION_STATUS_RUNNING,
62
    context={'mistral': {'auth_token': uuid.uuid4().hex}}
63
)
64
65
66
@mock.patch.object(
67
    CUDPublisher,
68
    'publish_update',
69
    mock.MagicMock(return_value=None))
70
@mock.patch.object(
71
    CUDPublisher,
72
    'publish_create',
73
    mock.MagicMock(side_effect=MockLiveActionPublisher.publish_create))
74
@mock.patch.object(
75
    LiveActionPublisher,
76
    'publish_state',
77
    mock.MagicMock(side_effect=MockLiveActionPublisher.publish_state))
78
class MistralRunnerCallbackTest(DbTestCase):
79
80
    @classmethod
81
    def setUpClass(cls):
82
        super(MistralRunnerCallbackTest, cls).setUpClass()
83
84
        # Override the retry configuration here otherwise st2tests.config.parse_args
85
        # in DbTestCase.setUpClass will reset these overrides.
86
        cfg.CONF.set_override('retry_exp_msec', 100, group='mistral')
87
        cfg.CONF.set_override('retry_exp_max_msec', 200, group='mistral')
88
        cfg.CONF.set_override('retry_stop_max_msec', 200, group='mistral')
89
        cfg.CONF.set_override('api_url', 'http://0.0.0.0:9101', group='auth')
90
91
        # Register runners.
92
        runnersregistrar.register_runners()
93
94
        # Register test pack(s).
95
        actions_registrar = actionsregistrar.ActionsRegistrar(
96
            use_pack_cache=False,
97
            fail_on_failure=True
98
        )
99
100
        for pack in PACKS:
101
            actions_registrar.register_from_pack(pack)
102
103
        # Get an instance of the callback module and reference to mistral status map
104
        cls.callback_module = loader.register_callback_module(MISTRAL_RUNNER_NAME)
105
        cls.callback_class = cls.callback_module.get_instance()
106
        cls.status_map = cls.callback_module.STATUS_MAP
107
108
    @classmethod
109
    def get_runner_class(cls, runner_name):
110
        return runners.get_runner(runner_name).__class__
111
112
    def test_callback_handler_status_map(self):
113
        # Ensure all StackStorm status are mapped otherwise leads to zombie workflow.
114
        self.assertListEqual(sorted(self.status_map.keys()),
115
                             sorted(action_constants.LIVEACTION_STATUSES))
116
117
    @mock.patch.object(
118
        trace_service, 'get_trace_db_by_live_action',
119
        mock.MagicMock(return_value=(None, None)))
120
    @mock.patch.object(
121
        ActionExecution, 'get_by_id',
122
        mock.MagicMock(return_value=MOCK_ACTION_EXEC_DB))
123
    @mock.patch.object(
124
        action_executions.ActionExecutionManager, 'update',
125
        mock.MagicMock(return_value=None))
126
    def test_callback_handler_with_result_as_text(self):
127
        self.callback_class.callback('http://127.0.0.1:8989/v2/action_executions/12345',
128
                                     {'parent': {'execution_id': uuid.uuid4().hex}},
129
                                     action_constants.LIVEACTION_STATUS_SUCCEEDED,
130
                                     '<html></html>')
131
132
    @mock.patch.object(
133
        trace_service, 'get_trace_db_by_live_action',
134
        mock.MagicMock(return_value=(None, None)))
135
    @mock.patch.object(
136
        ActionExecution, 'get_by_id',
137
        mock.MagicMock(return_value=MOCK_ACTION_EXEC_DB))
138
    @mock.patch.object(
139
        action_executions.ActionExecutionManager, 'update',
140
        mock.MagicMock(return_value=None))
141
    def test_callback_handler_with_result_as_dict(self):
142
        self.callback_class.callback('http://127.0.0.1:8989/v2/action_executions/12345',
143
                                     {'parent': {'execution_id': uuid.uuid4().hex}},
144
                                     action_constants.LIVEACTION_STATUS_SUCCEEDED, {'a': 1})
145
146
    @mock.patch.object(
147
        trace_service, 'get_trace_db_by_live_action',
148
        mock.MagicMock(return_value=(None, None)))
149
    @mock.patch.object(
150
        ActionExecution, 'get_by_id',
151
        mock.MagicMock(return_value=MOCK_ACTION_EXEC_DB))
152
    @mock.patch.object(
153
        action_executions.ActionExecutionManager, 'update',
154
        mock.MagicMock(return_value=None))
155
    def test_callback_handler_with_result_as_json_str(self):
156
        self.callback_class.callback('http://127.0.0.1:8989/v2/action_executions/12345',
157
                                     {'parent': {'execution_id': uuid.uuid4().hex}},
158
                                     action_constants.LIVEACTION_STATUS_SUCCEEDED, '{"a": 1}')
159
        self.callback_class.callback('http://127.0.0.1:8989/v2/action_executions/12345',
160
                                     {'parent': {'execution_id': uuid.uuid4().hex}},
161
                                     action_constants.LIVEACTION_STATUS_SUCCEEDED, "{'a': 1}")
162
163
    @mock.patch.object(
164
        trace_service, 'get_trace_db_by_live_action',
165
        mock.MagicMock(return_value=(None, None)))
166
    @mock.patch.object(
167
        ActionExecution, 'get_by_id',
168
        mock.MagicMock(return_value=MOCK_ACTION_EXEC_DB))
169
    @mock.patch.object(
170
        action_executions.ActionExecutionManager, 'update',
171
        mock.MagicMock(return_value=None))
172
    def test_callback_handler_with_result_as_list(self):
173
        self.callback_class.callback('http://127.0.0.1:8989/v2/action_executions/12345',
174
                                     {'parent': {'execution_id': uuid.uuid4().hex}},
175
                                     action_constants.LIVEACTION_STATUS_SUCCEEDED,
176
                                     ["a", "b", "c"])
177
178
    @mock.patch.object(
179
        trace_service, 'get_trace_db_by_live_action',
180
        mock.MagicMock(return_value=(None, None)))
181
    @mock.patch.object(
182
        ActionExecution, 'get_by_id',
183
        mock.MagicMock(return_value=MOCK_ACTION_EXEC_DB))
184
    @mock.patch.object(
185
        action_executions.ActionExecutionManager, 'update',
186
        mock.MagicMock(return_value=None))
187
    def test_callback_handler_with_result_as_list_str(self):
188
        self.callback_class.callback('http://127.0.0.1:8989/v2/action_executions/12345',
189
                                     {'parent': {'execution_id': uuid.uuid4().hex}},
190
                                     action_constants.LIVEACTION_STATUS_SUCCEEDED,
191
                                     '["a", "b", "c"]')
192
193
    @mock.patch.object(
194
        trace_service, 'get_trace_db_by_live_action',
195
        mock.MagicMock(return_value=(None, None)))
196
    @mock.patch.object(
197
        ActionExecution, 'get_by_id',
198
        mock.MagicMock(return_value=MOCK_ACTION_EXEC_DB))
199
    @mock.patch.object(
200
        action_executions.ActionExecutionManager, 'update',
201
        mock.MagicMock(return_value=None))
202
    def test_callback(self):
203
        local_runner_cls = self.get_runner_class('local_runner')
204
205
        liveaction = LiveActionDB(
206
            action='core.local',
207
            parameters={'cmd': 'uname -a'},
208
            context={
209
                'parent': {
210
                    'execution_id': uuid.uuid4().hex
211
                }
212
            },
213
            callback={
214
                'source': MISTRAL_RUNNER_NAME,
215
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
216
            }
217
        )
218
219
        for status in action_constants.LIVEACTION_COMPLETED_STATES:
220
            expected_mistral_status = self.status_map[status]
221
            local_runner_cls.run = mock.Mock(return_value=(status, NON_EMPTY_RESULT, None))
222
            liveaction, execution = action_service.request(liveaction)
223
            liveaction = LiveAction.get_by_id(str(liveaction.id))
224
            self.assertEqual(liveaction.status, status)
225
            action_executions.ActionExecutionManager.update.assert_called_with(
226
                '12345', state=expected_mistral_status, output=NON_EMPTY_RESULT)
227
228
    @mock.patch.object(
229
        trace_service, 'get_trace_db_by_live_action',
230
        mock.MagicMock(return_value=(None, None)))
231
    @mock.patch.object(
232
        ActionExecution, 'get_by_id',
233
        mock.MagicMock(return_value=MOCK_ACTION_EXEC_DB))
234
    @mock.patch.object(
235
        action_executions.ActionExecutionManager, 'update',
236
        mock.MagicMock(return_value=None))
237
    def test_callback_incomplete_state(self):
238
        local_runner_cls = self.get_runner_class('local_runner')
239
        local_run_result = (action_constants.LIVEACTION_STATUS_RUNNING, NON_EMPTY_RESULT, None)
240
        local_runner_cls.run = mock.Mock(return_value=local_run_result)
241
242
        liveaction = LiveActionDB(
243
            action='core.local',
244
            parameters={'cmd': 'uname -a'},
245
            context={
246
                'parent': {
247
                    'execution_id': uuid.uuid4().hex
248
                }
249
            },
250
            callback={
251
                'source': MISTRAL_RUNNER_NAME,
252
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
253
            }
254
        )
255
256
        liveaction, execution = action_service.request(liveaction)
257
        liveaction = LiveAction.get_by_id(str(liveaction.id))
258
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
259
        self.assertFalse(action_executions.ActionExecutionManager.update.called)
260
261
    @mock.patch.object(
262
        trace_service, 'get_trace_db_by_live_action',
263
        mock.MagicMock(return_value=(None, None)))
264
    @mock.patch.object(
265
        ActionExecution, 'get_by_id',
266
        mock.MagicMock(return_value=MOCK_ACTION_EXEC_DB))
267
    @mock.patch.object(
268
        action_executions.ActionExecutionManager, 'update',
269
        mock.MagicMock(side_effect=[
270
            requests.exceptions.ConnectionError(),
271
            None]))
272
    def test_callback_retry(self):
273
        local_runner_cls = self.get_runner_class('local_runner')
274
        local_run_result = (action_constants.LIVEACTION_STATUS_SUCCEEDED, NON_EMPTY_RESULT, None)
275
        local_runner_cls.run = mock.Mock(return_value=local_run_result)
276
277
        liveaction = LiveActionDB(
278
            action='core.local',
279
            parameters={'cmd': 'uname -a'},
280
            context={
281
                'parent': {
282
                    'execution_id': uuid.uuid4().hex
283
                }
284
            },
285
            callback={
286
                'source': MISTRAL_RUNNER_NAME,
287
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
288
            }
289
        )
290
291
        liveaction, execution = action_service.request(liveaction)
292
        liveaction = LiveAction.get_by_id(str(liveaction.id))
293
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
294
295
        calls = [call('12345', state='SUCCESS', output=NON_EMPTY_RESULT) for i in range(0, 2)]
296
        action_executions.ActionExecutionManager.update.assert_has_calls(calls)
297
298
    @mock.patch.object(
299
        trace_service, 'get_trace_db_by_live_action',
300
        mock.MagicMock(return_value=(None, None)))
301
    @mock.patch.object(
302
        ActionExecution, 'get_by_id',
303
        mock.MagicMock(return_value=MOCK_ACTION_EXEC_DB))
304
    @mock.patch.object(
305
        action_executions.ActionExecutionManager, 'update',
306
        mock.MagicMock(side_effect=[
307
            requests.exceptions.ConnectionError(),
308
            requests.exceptions.ConnectionError(),
309
            requests.exceptions.ConnectionError(),
310
            requests.exceptions.ConnectionError(),
311
            None]))
312
    def test_callback_retry_exhausted(self):
313
        local_runner_cls = self.get_runner_class('local_runner')
314
        local_run_result = (action_constants.LIVEACTION_STATUS_SUCCEEDED, NON_EMPTY_RESULT, None)
315
        local_runner_cls.run = mock.Mock(return_value=local_run_result)
316
317
        liveaction = LiveActionDB(
318
            action='core.local',
319
            parameters={'cmd': 'uname -a'},
320
            context={
321
                'parent': {
322
                    'execution_id': uuid.uuid4().hex
323
                }
324
            },
325
            callback={
326
                'source': MISTRAL_RUNNER_NAME,
327
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
328
            }
329
        )
330
331
        liveaction, execution = action_service.request(liveaction)
332
        liveaction = LiveAction.get_by_id(str(liveaction.id))
333
334
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
335
336
        # This test initially setup mock for action_executions.ActionExecutionManager.update
337
        # to fail the first 4 times and return success on the 5th times. The max attempts
338
        # is set to 3. We expect only 3 calls to pass thru the update method.
339
        calls = [call('12345', state='SUCCESS', output=NON_EMPTY_RESULT) for i in range(0, 2)]
340
        action_executions.ActionExecutionManager.update.assert_has_calls(calls)
341