Passed
Pull Request — master (#3277)
by W
05:11
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(self):
149
        local_runner_cls = self.get_runner_class('local_runner')
150
151
        liveaction = LiveActionDB(
152
            action='core.local', parameters={'cmd': 'uname -a'},
153
            callback={
154
                'source': MISTRAL_RUNNER_NAME,
155
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
156
            }
157
        )
158
159
        for status in action_constants.LIVEACTION_COMPLETED_STATES:
160
            expected_mistral_status = self.status_map[status]
161
            local_runner_cls.run = mock.Mock(return_value=(status, NON_EMPTY_RESULT, None))
162
            liveaction, execution = action_service.request(liveaction)
163
            liveaction = LiveAction.get_by_id(str(liveaction.id))
164
            self.assertEqual(liveaction.status, status)
165
            action_executions.ActionExecutionManager.update.assert_called_with(
166
                '12345', state=expected_mistral_status, output=NON_EMPTY_RESULT)
167
168
    @mock.patch.object(
169
        action_executions.ActionExecutionManager, 'update',
170
        mock.MagicMock(return_value=None))
171
    def test_callback_incomplete_state(self):
172
        local_runner_cls = self.get_runner_class('local_runner')
173
        local_run_result = (action_constants.LIVEACTION_STATUS_RUNNING, NON_EMPTY_RESULT, None)
174
        local_runner_cls.run = mock.Mock(return_value=local_run_result)
175
176
        liveaction = LiveActionDB(
177
            action='core.local', parameters={'cmd': 'uname -a'},
178
            callback={
179
                'source': MISTRAL_RUNNER_NAME,
180
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
181
            }
182
        )
183
184
        liveaction, execution = action_service.request(liveaction)
185
        liveaction = LiveAction.get_by_id(str(liveaction.id))
186
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
187
        self.assertFalse(action_executions.ActionExecutionManager.update.called)
188
189
    @mock.patch.object(
190
        action_executions.ActionExecutionManager, 'update',
191
        mock.MagicMock(side_effect=[
192
            requests.exceptions.ConnectionError(),
193
            None]))
194
    def test_callback_retry(self):
195
        local_runner_cls = self.get_runner_class('local_runner')
196
        local_run_result = (action_constants.LIVEACTION_STATUS_SUCCEEDED, NON_EMPTY_RESULT, None)
197
        local_runner_cls.run = mock.Mock(return_value=local_run_result)
198
199
        liveaction = LiveActionDB(
200
            action='core.local', parameters={'cmd': 'uname -a'},
201
            callback={
202
                'source': MISTRAL_RUNNER_NAME,
203
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
204
            }
205
        )
206
207
        liveaction, execution = action_service.request(liveaction)
208
        liveaction = LiveAction.get_by_id(str(liveaction.id))
209
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
210
211
        calls = [call('12345', state='SUCCESS', output=NON_EMPTY_RESULT) for i in range(0, 2)]
212
        action_executions.ActionExecutionManager.update.assert_has_calls(calls)
213
214
    @mock.patch.object(
215
        action_executions.ActionExecutionManager, 'update',
216
        mock.MagicMock(side_effect=[
217
            requests.exceptions.ConnectionError(),
218
            requests.exceptions.ConnectionError(),
219
            requests.exceptions.ConnectionError(),
220
            requests.exceptions.ConnectionError(),
221
            None]))
222
    def test_callback_retry_exhausted(self):
223
        local_runner_cls = self.get_runner_class('local_runner')
224
        local_run_result = (action_constants.LIVEACTION_STATUS_SUCCEEDED, NON_EMPTY_RESULT, None)
225
        local_runner_cls.run = mock.Mock(return_value=local_run_result)
226
227
        liveaction = LiveActionDB(
228
            action='core.local', parameters={'cmd': 'uname -a'},
229
            callback={
230
                'source': MISTRAL_RUNNER_NAME,
231
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
232
            }
233
        )
234
235
        liveaction, execution = action_service.request(liveaction)
236
        liveaction = LiveAction.get_by_id(str(liveaction.id))
237
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
238
239
        # This test initially setup mock for action_executions.ActionExecutionManager.update
240
        # to fail the first 4 times and return success on the 5th times. The max attempts
241
        # is set to 3. We expect only 3 calls to pass thru the update method.
242
        calls = [call('12345', state='SUCCESS', output=NON_EMPTY_RESULT) for i in range(0, 2)]
243
        action_executions.ActionExecutionManager.update.assert_has_calls(calls)
244