Passed
Pull Request — master (#3507)
by W
05:41
created

MistralRunnerCallbackTest.test_callback()   A

Complexity

Conditions 2

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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