Passed
Pull Request — master (#3125)
by W
04:48
created

MistralRunnerCallbackTest.test_callback_retry()   B

Complexity

Conditions 2

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 2
c 1
b 1
f 0
dl 0
loc 24
rs 8.9713
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 st2actions.handlers.mistral import MistralCallbackHandler
28
from st2actions.handlers.mistral import STATUS_MAP as mistral_status_map
29
from st2common.bootstrap import actionsregistrar
30
from st2common.bootstrap import runnersregistrar
31
from st2common.constants import action as action_constants
32
from st2common.models.db.liveaction import LiveActionDB
33
from st2common.persistence.liveaction import LiveAction
34
from st2common.runners import base as runners
35
from st2common.services import action as action_service
36
from st2common.transport.liveaction import LiveActionPublisher
37
from st2common.transport.publishers import CUDPublisher
38
from st2tests import DbTestCase
39
from st2tests import fixturesloader
40
from st2tests.mocks.liveaction import MockLiveActionPublisher
41
42
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
    @classmethod
92
    def get_runner_class(cls, runner_name):
93
        return runners.get_runner(runner_name).__class__
94
95
    def test_callback_handler_status_map(self):
96
        # Ensure all StackStorm status are mapped otherwise leads to zombie workflow.
97
        self.assertListEqual(sorted(mistral_status_map.keys()),
98
                             sorted(action_constants.LIVEACTION_STATUSES))
99
100
    @mock.patch.object(
101
        action_executions.ActionExecutionManager, 'update',
102
        mock.MagicMock(return_value=None))
103
    def test_callback_handler_with_result_as_text(self):
104
        MistralCallbackHandler.callback('http://127.0.0.1:8989/v2/action_executions/12345', {},
105
                                        action_constants.LIVEACTION_STATUS_SUCCEEDED,
106
                                        '<html></html>')
107
108
    @mock.patch.object(
109
        action_executions.ActionExecutionManager, 'update',
110
        mock.MagicMock(return_value=None))
111
    def test_callback_handler_with_result_as_dict(self):
112
        MistralCallbackHandler.callback('http://127.0.0.1:8989/v2/action_executions/12345', {},
113
                                        action_constants.LIVEACTION_STATUS_SUCCEEDED, {'a': 1})
114
115
    @mock.patch.object(
116
        action_executions.ActionExecutionManager, 'update',
117
        mock.MagicMock(return_value=None))
118
    def test_callback_handler_with_result_as_json_str(self):
119
        MistralCallbackHandler.callback('http://127.0.0.1:8989/v2/action_executions/12345', {},
120
                                        action_constants.LIVEACTION_STATUS_SUCCEEDED, '{"a": 1}')
121
        MistralCallbackHandler.callback('http://127.0.0.1:8989/v2/action_executions/12345', {},
122
                                        action_constants.LIVEACTION_STATUS_SUCCEEDED, "{'a': 1}")
123
124
    @mock.patch.object(
125
        action_executions.ActionExecutionManager, 'update',
126
        mock.MagicMock(return_value=None))
127
    def test_callback_handler_with_result_as_list(self):
128
        MistralCallbackHandler.callback('http://127.0.0.1:8989/v2/action_executions/12345', {},
129
                                        action_constants.LIVEACTION_STATUS_SUCCEEDED,
130
                                        ["a", "b", "c"])
131
132
    @mock.patch.object(
133
        action_executions.ActionExecutionManager, 'update',
134
        mock.MagicMock(return_value=None))
135
    def test_callback_handler_with_result_as_list_str(self):
136
        MistralCallbackHandler.callback('http://127.0.0.1:8989/v2/action_executions/12345', {},
137
                                        action_constants.LIVEACTION_STATUS_SUCCEEDED,
138
                                        '["a", "b", "c"]')
139
140
    @mock.patch.object(
141
        action_executions.ActionExecutionManager, 'update',
142
        mock.MagicMock(return_value=None))
143
    def test_callback(self):
144
        local_runner_cls = self.get_runner_class('local_runner')
145
146
        liveaction = LiveActionDB(
147
            action='core.local', parameters={'cmd': 'uname -a'},
148
            callback={
149
                'source': 'mistral',
150
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
151
            }
152
        )
153
154
        for status in action_constants.LIVEACTION_COMPLETED_STATES:
155
            expected_mistral_status = mistral_status_map[status]
156
            local_runner_cls.run = mock.Mock(return_value=(status, NON_EMPTY_RESULT, None))
157
            liveaction, execution = action_service.request(liveaction)
158
            liveaction = LiveAction.get_by_id(str(liveaction.id))
159
            self.assertEqual(liveaction.status, status)
160
            action_executions.ActionExecutionManager.update.assert_called_with(
161
                '12345', state=expected_mistral_status, output=NON_EMPTY_RESULT)
162
163
    @mock.patch.object(
164
        action_executions.ActionExecutionManager, 'update',
165
        mock.MagicMock(return_value=None))
166
    def test_callback_incomplete_state(self):
167
        local_runner_cls = self.get_runner_class('local_runner')
168
        local_run_result = (action_constants.LIVEACTION_STATUS_RUNNING, NON_EMPTY_RESULT, None)
169
        local_runner_cls.run = mock.Mock(return_value=local_run_result)
170
171
        liveaction = LiveActionDB(
172
            action='core.local', parameters={'cmd': 'uname -a'},
173
            callback={
174
                'source': 'mistral',
175
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
176
            }
177
        )
178
179
        liveaction, execution = action_service.request(liveaction)
180
        liveaction = LiveAction.get_by_id(str(liveaction.id))
181
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
182
        self.assertFalse(action_executions.ActionExecutionManager.update.called)
183
184
    @mock.patch.object(
185
        action_executions.ActionExecutionManager, 'update',
186
        mock.MagicMock(side_effect=[
187
            requests.exceptions.ConnectionError(),
188
            None]))
189
    def test_callback_retry(self):
190
        local_runner_cls = self.get_runner_class('local_runner')
191
        local_run_result = (action_constants.LIVEACTION_STATUS_SUCCEEDED, NON_EMPTY_RESULT, None)
192
        local_runner_cls.run = mock.Mock(return_value=local_run_result)
193
194
        liveaction = LiveActionDB(
195
            action='core.local', parameters={'cmd': 'uname -a'},
196
            callback={
197
                'source': 'mistral',
198
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
199
            }
200
        )
201
202
        liveaction, execution = action_service.request(liveaction)
203
        liveaction = LiveAction.get_by_id(str(liveaction.id))
204
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
205
206
        calls = [call('12345', state='SUCCESS', output=NON_EMPTY_RESULT) for i in range(0, 2)]
207
        action_executions.ActionExecutionManager.update.assert_has_calls(calls)
208
209
    @mock.patch.object(
210
        action_executions.ActionExecutionManager, 'update',
211
        mock.MagicMock(side_effect=[
212
            requests.exceptions.ConnectionError(),
213
            requests.exceptions.ConnectionError(),
214
            requests.exceptions.ConnectionError(),
215
            requests.exceptions.ConnectionError(),
216
            None]))
217
    def test_callback_retry_exhausted(self):
218
        local_runner_cls = self.get_runner_class('local_runner')
219
        local_run_result = (action_constants.LIVEACTION_STATUS_SUCCEEDED, NON_EMPTY_RESULT, None)
220
        local_runner_cls.run = mock.Mock(return_value=local_run_result)
221
222
        liveaction = LiveActionDB(
223
            action='core.local', parameters={'cmd': 'uname -a'},
224
            callback={
225
                'source': 'mistral',
226
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
227
            }
228
        )
229
230
        liveaction, execution = action_service.request(liveaction)
231
        liveaction = LiveAction.get_by_id(str(liveaction.id))
232
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
233
234
        # This test initially setup mock for action_executions.ActionExecutionManager.update
235
        # to fail the first 4 times and return success on the 5th times. The max attempts
236
        # is set to 3. We expect only 3 calls to pass thru the update method.
237
        calls = [call('12345', state='SUCCESS', output=NON_EMPTY_RESULT) for i in range(0, 2)]
238
        action_executions.ActionExecutionManager.update.assert_has_calls(calls)
239