Passed
Push — develop ( b8d4ca...421369 )
by Plexxi
07:01 queued 03:57
created

MistralRunnerTest.test_launch_workbook()   B

Complexity

Conditions 1

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 1
c 3
b 1
f 0
dl 0
loc 26
rs 8.8571
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 copy
17
import uuid
18
19
import mock
20
from mock import call
21
import requests
22
import six
23
import yaml
24
25
from mistralclient.api.base import APIException
26
from mistralclient.api.v2 import action_executions
27
from mistralclient.api.v2 import executions
28
from mistralclient.api.v2 import workbooks
29
from mistralclient.api.v2 import workflows
30
from oslo_config import cfg
31
32
# XXX: actionsensor import depends on config being setup.
33
import st2tests.config as tests_config
34
tests_config.parse_args()
35
36
# Set defaults for retry options.
37
cfg.CONF.set_override('retry_exp_msec', 100, group='mistral')
38
cfg.CONF.set_override('retry_exp_max_msec', 200, group='mistral')
39
cfg.CONF.set_override('retry_stop_max_msec', 200, group='mistral')
40
41
import st2common.bootstrap.runnersregistrar as runners_registrar
42
from st2actions.handlers.mistral import MistralCallbackHandler
43
from st2actions.handlers.mistral import STATUS_MAP as mistral_status_map
44
from st2common.constants import action as action_constants
45
from st2common.models.api.action import ActionAPI
46
from st2common.models.api.notification import NotificationsHelper
47
from st2common.models.db.liveaction import LiveActionDB
48
from st2common.persistence.action import Action
49
from st2common.persistence.liveaction import LiveAction
50
from st2common.services import action as action_service
51
from st2common.transport.liveaction import LiveActionPublisher
52
from st2common.transport.publishers import CUDPublisher
53
from st2tests import DbTestCase
54
from st2tests.fixturesloader import FixturesLoader
55
from tests.unit.base import MockLiveActionPublisher
56
from local_runner import LocalShellRunner
57
from mistral_v2 import MistralRunner
58
59
60
TEST_FIXTURES = {
61
    'workflows': [
62
        'workbook_v2.yaml',
63
        'workbook_v2_many_workflows.yaml',
64
        'workbook_v2_many_workflows_no_default.yaml',
65
        'workflow_v2.yaml',
66
        'workflow_v2_many_workflows.yaml'
67
    ],
68
    'actions': [
69
        'workbook_v2.yaml',
70
        'workbook_v2_many_workflows.yaml',
71
        'workbook_v2_many_workflows_no_default.yaml',
72
        'workflow_v2.yaml',
73
        'workflow_v2_many_workflows.yaml',
74
        'workbook_v2_name_mismatch.yaml',
75
        'workflow_v2_name_mismatch.yaml',
76
        'local.yaml'
77
    ]
78
}
79
80
PACK = 'generic'
81
LOADER = FixturesLoader()
82
FIXTURES = LOADER.load_fixtures(fixtures_pack=PACK, fixtures_dict=TEST_FIXTURES)
83
84
MISTRAL_EXECUTION = {'id': str(uuid.uuid4()), 'state': 'RUNNING', 'workflow_name': None}
85
86
# Workbook with a single workflow
87
WB1_YAML_FILE_NAME = TEST_FIXTURES['workflows'][0]
88
WB1_YAML_FILE_PATH = LOADER.get_fixture_file_path_abs(PACK, 'workflows', WB1_YAML_FILE_NAME)
89
WB1_SPEC = FIXTURES['workflows'][WB1_YAML_FILE_NAME]
90
WB1_YAML = yaml.safe_dump(WB1_SPEC, default_flow_style=False)
91
WB1_NAME = '%s.%s' % (PACK, WB1_YAML_FILE_NAME.replace('.yaml', ''))
92
WB1 = workbooks.Workbook(None, {'name': WB1_NAME, 'definition': WB1_YAML})
93
WB1_OLD = workbooks.Workbook(None, {'name': WB1_NAME, 'definition': ''})
94
WB1_EXEC = copy.deepcopy(MISTRAL_EXECUTION)
95
WB1_EXEC['workflow_name'] = WB1_NAME
96
97
# Workbook with many workflows
98
WB2_YAML_FILE_NAME = TEST_FIXTURES['workflows'][1]
99
WB2_YAML_FILE_PATH = LOADER.get_fixture_file_path_abs(PACK, 'workflows', WB2_YAML_FILE_NAME)
100
WB2_SPEC = FIXTURES['workflows'][WB2_YAML_FILE_NAME]
101
WB2_YAML = yaml.safe_dump(WB2_SPEC, default_flow_style=False)
102
WB2_NAME = '%s.%s' % (PACK, WB2_YAML_FILE_NAME.replace('.yaml', ''))
103
WB2 = workbooks.Workbook(None, {'name': WB2_NAME, 'definition': WB2_YAML})
104
WB2_EXEC = copy.deepcopy(MISTRAL_EXECUTION)
105
WB2_EXEC['workflow_name'] = WB2_NAME
106
107
# Workbook with many workflows but no default workflow is defined
108
WB3_YAML_FILE_NAME = TEST_FIXTURES['workflows'][2]
109
WB3_YAML_FILE_PATH = LOADER.get_fixture_file_path_abs(PACK, 'workflows', WB3_YAML_FILE_NAME)
110
WB3_SPEC = FIXTURES['workflows'][WB3_YAML_FILE_NAME]
111
WB3_YAML = yaml.safe_dump(WB3_SPEC, default_flow_style=False)
112
WB3_NAME = '%s.%s' % (PACK, WB3_YAML_FILE_NAME.replace('.yaml', ''))
113
WB3 = workbooks.Workbook(None, {'name': WB3_NAME, 'definition': WB3_YAML})
114
WB3_EXEC = copy.deepcopy(MISTRAL_EXECUTION)
115
WB3_EXEC['workflow_name'] = WB3_NAME
116
117
# Non-workbook with a single workflow
118
WF1_YAML_FILE_NAME = TEST_FIXTURES['workflows'][3]
119
WF1_YAML_FILE_PATH = LOADER.get_fixture_file_path_abs(PACK, 'workflows', WF1_YAML_FILE_NAME)
120
WF1_SPEC = FIXTURES['workflows'][WF1_YAML_FILE_NAME]
121
WF1_YAML = yaml.safe_dump(WF1_SPEC, default_flow_style=False)
122
WF1_NAME = '%s.%s' % (PACK, WF1_YAML_FILE_NAME.replace('.yaml', ''))
123
WF1 = workflows.Workflow(None, {'name': WF1_NAME, 'definition': WF1_YAML})
124
WF1_OLD = workflows.Workflow(None, {'name': WF1_NAME, 'definition': ''})
125
WF1_EXEC = copy.deepcopy(MISTRAL_EXECUTION)
126
WF1_EXEC['workflow_name'] = WF1_NAME
127
WF1_EXEC_PAUSED = copy.deepcopy(WF1_EXEC)
128
WF1_EXEC_PAUSED['state'] = 'PAUSED'
129
130
# Non-workbook with a many workflows
131
WF2_YAML_FILE_NAME = TEST_FIXTURES['workflows'][4]
132
WF2_YAML_FILE_PATH = LOADER.get_fixture_file_path_abs(PACK, 'workflows', WF2_YAML_FILE_NAME)
133
WF2_SPEC = FIXTURES['workflows'][WF2_YAML_FILE_NAME]
134
WF2_YAML = yaml.safe_dump(WF2_SPEC, default_flow_style=False)
135
WF2_NAME = '%s.%s' % (PACK, WF2_YAML_FILE_NAME.replace('.yaml', ''))
136
WF2 = workflows.Workflow(None, {'name': WF2_NAME, 'definition': WF2_YAML})
137
WF2_EXEC = copy.deepcopy(MISTRAL_EXECUTION)
138
WF2_EXEC['workflow_name'] = WF2_NAME
139
140
# Action executions requirements
141
ACTION_PARAMS = {'friend': 'Rocky'}
142
143
NON_EMPTY_RESULT = 'non-empty'
144
145
146
@mock.patch.object(CUDPublisher, 'publish_update', mock.MagicMock(return_value=None))
147
@mock.patch.object(CUDPublisher, 'publish_create',
148
                   mock.MagicMock(side_effect=MockLiveActionPublisher.publish_create))
149
@mock.patch.object(LiveActionPublisher, 'publish_state',
150
                   mock.MagicMock(side_effect=MockLiveActionPublisher.publish_state))
151
class MistralRunnerTest(DbTestCase):
152
153
    @classmethod
154
    def setUpClass(cls):
155
        super(MistralRunnerTest, cls).setUpClass()
156
        runners_registrar.register_runner_types()
157
158
        for _, fixture in six.iteritems(FIXTURES['actions']):
159
            instance = ActionAPI(**fixture)
160
            Action.add_or_update(ActionAPI.to_model(instance))
161
162
    def setUp(self):
163
        super(MistralRunnerTest, self).setUp()
164
        cfg.CONF.set_override('api_url', 'http://0.0.0.0:9101', group='auth')
165
166
    @mock.patch.object(
167
        workflows.WorkflowManager, 'list',
168
        mock.MagicMock(return_value=[]))
169
    @mock.patch.object(
170
        workflows.WorkflowManager, 'get',
171
        mock.MagicMock(return_value=WF1))
172
    @mock.patch.object(
173
        workflows.WorkflowManager, 'create',
174
        mock.MagicMock(return_value=[WF1]))
175
    @mock.patch.object(
176
        executions.ExecutionManager, 'create',
177
        mock.MagicMock(return_value=executions.Execution(None, WF1_EXEC)))
178
    def test_launch_workflow(self):
179
        MistralRunner.entry_point = mock.PropertyMock(return_value=WF1_YAML_FILE_PATH)
180
        liveaction = LiveActionDB(action=WF1_NAME, parameters=ACTION_PARAMS)
181
        liveaction, execution = action_service.request(liveaction)
182
        liveaction = LiveAction.get_by_id(str(liveaction.id))
183
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
184
185
        mistral_context = liveaction.context.get('mistral', None)
186
        self.assertIsNotNone(mistral_context)
187
        self.assertEqual(mistral_context['execution_id'], WF1_EXEC.get('id'))
188
        self.assertEqual(mistral_context['workflow_name'], WF1_EXEC.get('workflow_name'))
189
190
        workflow_input = copy.deepcopy(ACTION_PARAMS)
191
        workflow_input.update({'count': '3'})
192
193
        env = {
194
            'st2_execution_id': str(execution.id),
195
            'st2_liveaction_id': str(liveaction.id),
196
            'st2_action_api_url': 'http://0.0.0.0:9101/v1',
197
            '__actions': {
198
                'st2.action': {
199
                    'st2_context': {
200
                        'api_url': 'http://0.0.0.0:9101/v1',
201
                        'endpoint': 'http://0.0.0.0:9101/v1/actionexecutions',
202
                        'parent': {
203
                            'execution_id': str(execution.id)
204
                        },
205
                        'notify': {},
206
                        'skip_notify_tasks': []
207
                    }
208
                }
209
            }
210
        }
211
212
        executions.ExecutionManager.create.assert_called_with(
213
            WF1_NAME, workflow_input=workflow_input, env=env)
214
215
    @mock.patch.object(
216
        workflows.WorkflowManager, 'list',
217
        mock.MagicMock(return_value=[]))
218
    @mock.patch.object(
219
        workflows.WorkflowManager, 'get',
220
        mock.MagicMock(return_value=WF1))
221
    @mock.patch.object(
222
        workflows.WorkflowManager, 'create',
223
        mock.MagicMock(return_value=[WF1]))
224
    @mock.patch.object(
225
        executions.ExecutionManager, 'create',
226
        mock.MagicMock(return_value=executions.Execution(None, WF1_EXEC)))
227
    def test_launch_workflow_with_st2_https(self):
228
        cfg.CONF.set_override('api_url', 'https://0.0.0.0:9101', group='auth')
229
230
        MistralRunner.entry_point = mock.PropertyMock(return_value=WF1_YAML_FILE_PATH)
231
        liveaction = LiveActionDB(action=WF1_NAME, parameters=ACTION_PARAMS)
232
        liveaction, execution = action_service.request(liveaction)
233
        liveaction = LiveAction.get_by_id(str(liveaction.id))
234
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
235
236
        mistral_context = liveaction.context.get('mistral', None)
237
        self.assertIsNotNone(mistral_context)
238
        self.assertEqual(mistral_context['execution_id'], WF1_EXEC.get('id'))
239
        self.assertEqual(mistral_context['workflow_name'], WF1_EXEC.get('workflow_name'))
240
241
        workflow_input = copy.deepcopy(ACTION_PARAMS)
242
        workflow_input.update({'count': '3'})
243
244
        env = {
245
            'st2_execution_id': str(execution.id),
246
            'st2_liveaction_id': str(liveaction.id),
247
            'st2_action_api_url': 'https://0.0.0.0:9101/v1',
248
            '__actions': {
249
                'st2.action': {
250
                    'st2_context': {
251
                        'api_url': 'https://0.0.0.0:9101/v1',
252
                        'endpoint': 'https://0.0.0.0:9101/v1/actionexecutions',
253
                        'parent': {
254
                            'execution_id': str(execution.id)
255
                        },
256
                        'notify': {},
257
                        'skip_notify_tasks': []
258
                    }
259
                }
260
            }
261
        }
262
263
        executions.ExecutionManager.create.assert_called_with(
264
            WF1_NAME, workflow_input=workflow_input, env=env)
265
266
    @mock.patch.object(
267
        workflows.WorkflowManager, 'list',
268
        mock.MagicMock(return_value=[]))
269
    @mock.patch.object(
270
        workflows.WorkflowManager, 'get',
271
        mock.MagicMock(return_value=WF1))
272
    @mock.patch.object(
273
        workflows.WorkflowManager, 'create',
274
        mock.MagicMock(return_value=[WF1]))
275
    @mock.patch.object(
276
        executions.ExecutionManager, 'create',
277
        mock.MagicMock(return_value=executions.Execution(None, WF1_EXEC)))
278
    def test_launch_workflow_with_notifications(self):
279
        notify_data = {'on_complete': {'routes': ['slack'],
280
                       'message': '"@channel: Action succeeded."', 'data': {}}}
281
282
        MistralRunner.entry_point = mock.PropertyMock(return_value=WF1_YAML_FILE_PATH)
283
        liveaction = LiveActionDB(action=WF1_NAME, parameters=ACTION_PARAMS, notify=notify_data)
284
        liveaction, execution = action_service.request(liveaction)
285
        liveaction = LiveAction.get_by_id(str(liveaction.id))
286
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
287
288
        mistral_context = liveaction.context.get('mistral', None)
289
        self.assertIsNotNone(mistral_context)
290
        self.assertEqual(mistral_context['execution_id'], WF1_EXEC.get('id'))
291
        self.assertEqual(mistral_context['workflow_name'], WF1_EXEC.get('workflow_name'))
292
293
        workflow_input = copy.deepcopy(ACTION_PARAMS)
294
        workflow_input.update({'count': '3'})
295
296
        env = {
297
            'st2_execution_id': str(execution.id),
298
            'st2_liveaction_id': str(liveaction.id),
299
            'st2_action_api_url': 'http://0.0.0.0:9101/v1',
300
            '__actions': {
301
                'st2.action': {
302
                    'st2_context': {
303
                        'api_url': 'http://0.0.0.0:9101/v1',
304
                        'endpoint': 'http://0.0.0.0:9101/v1/actionexecutions',
305
                        'parent': {
306
                            'execution_id': str(execution.id)
307
                        },
308
                        'notify': NotificationsHelper.from_model(liveaction.notify),
309
                        'skip_notify_tasks': []
310
                    }
311
                }
312
            }
313
        }
314
315
        executions.ExecutionManager.create.assert_called_with(
316
            WF1_NAME, workflow_input=workflow_input, env=env)
317
318
    @mock.patch.object(
319
        workflows.WorkflowManager, 'list',
320
        mock.MagicMock(side_effect=requests.exceptions.ConnectionError('Connection refused')))
321
    def test_launch_workflow_mistral_offline(self):
322
        MistralRunner.entry_point = mock.PropertyMock(return_value=WF1_YAML_FILE_PATH)
323
        liveaction = LiveActionDB(action=WF1_NAME, parameters=ACTION_PARAMS)
324
        liveaction, execution = action_service.request(liveaction)
325
        liveaction = LiveAction.get_by_id(str(liveaction.id))
326
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_FAILED)
327
        self.assertIn('Connection refused', liveaction.result['error'])
328
329
    @mock.patch.object(
330
        workflows.WorkflowManager, 'list',
331
        mock.MagicMock(side_effect=[requests.exceptions.ConnectionError(), []]))
332
    @mock.patch.object(
333
        workflows.WorkflowManager, 'get',
334
        mock.MagicMock(return_value=WF1))
335
    @mock.patch.object(
336
        workflows.WorkflowManager, 'create',
337
        mock.MagicMock(return_value=[WF1]))
338
    @mock.patch.object(
339
        executions.ExecutionManager, 'create',
340
        mock.MagicMock(return_value=executions.Execution(None, WF1_EXEC)))
341
    def test_launch_workflow_mistral_retry(self):
342
        MistralRunner.entry_point = mock.PropertyMock(return_value=WF1_YAML_FILE_PATH)
343
        liveaction = LiveActionDB(action=WF1_NAME, parameters=ACTION_PARAMS)
344
        liveaction, execution = action_service.request(liveaction)
345
        liveaction = LiveAction.get_by_id(str(liveaction.id))
346
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
347
348
        mistral_context = liveaction.context.get('mistral', None)
349
        self.assertIsNotNone(mistral_context)
350
        self.assertEqual(mistral_context['execution_id'], WF1_EXEC.get('id'))
351
        self.assertEqual(mistral_context['workflow_name'], WF1_EXEC.get('workflow_name'))
352
353
    @mock.patch.object(
354
        workflows.WorkflowManager, 'list',
355
        mock.MagicMock(return_value=[]))
356
    @mock.patch.object(
357
        workflows.WorkflowManager, 'get',
358
        mock.MagicMock(return_value=WF1))
359
    @mock.patch.object(
360
        workflows.WorkflowManager, 'create',
361
        mock.MagicMock(side_effect=[APIException(error_message='Duplicate entry.'), WF1]))
362
    @mock.patch.object(
363
        executions.ExecutionManager, 'create',
364
        mock.MagicMock(return_value=executions.Execution(None, WF1_EXEC)))
365
    def test_launch_workflow_duplicate_error(self):
366
        MistralRunner.entry_point = mock.PropertyMock(return_value=WF1_YAML_FILE_PATH)
367
        liveaction = LiveActionDB(action=WF1_NAME, parameters=ACTION_PARAMS)
368
        liveaction, execution = action_service.request(liveaction)
369
        liveaction = LiveAction.get_by_id(str(liveaction.id))
370
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
371
372
        mistral_context = liveaction.context.get('mistral', None)
373
        self.assertIsNotNone(mistral_context)
374
        self.assertEqual(mistral_context['execution_id'], WF1_EXEC.get('id'))
375
        self.assertEqual(mistral_context['workflow_name'], WF1_EXEC.get('workflow_name'))
376
377
    @mock.patch.object(
378
        workflows.WorkflowManager, 'list',
379
        mock.MagicMock(return_value=[]))
380
    @mock.patch.object(
381
        workflows.WorkflowManager, 'get',
382
        mock.MagicMock(return_value=WF1_OLD))
383
    @mock.patch.object(
384
        workflows.WorkflowManager, 'create',
385
        mock.MagicMock(return_value=[WF1]))
386
    @mock.patch.object(
387
        workflows.WorkflowManager, 'update',
388
        mock.MagicMock(return_value=[WF1]))
389
    @mock.patch.object(
390
        executions.ExecutionManager, 'create',
391
        mock.MagicMock(return_value=executions.Execution(None, WF1_EXEC)))
392
    def test_launch_when_workflow_definition_changed(self):
393
        MistralRunner.entry_point = mock.PropertyMock(return_value=WF1_YAML_FILE_PATH)
394
        liveaction = LiveActionDB(action=WF1_NAME, parameters=ACTION_PARAMS)
395
        liveaction, execution = action_service.request(liveaction)
396
        liveaction = LiveAction.get_by_id(str(liveaction.id))
397
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
398
399
        mistral_context = liveaction.context.get('mistral', None)
400
        self.assertIsNotNone(mistral_context)
401
        self.assertEqual(mistral_context['execution_id'], WF1_EXEC.get('id'))
402
        self.assertEqual(mistral_context['workflow_name'], WF1_EXEC.get('workflow_name'))
403
404
    @mock.patch.object(
405
        workflows.WorkflowManager, 'list',
406
        mock.MagicMock(return_value=[]))
407
    @mock.patch.object(
408
        workflows.WorkflowManager, 'get',
409
        mock.MagicMock(side_effect=Exception()))
410
    @mock.patch.object(
411
        workbooks.WorkbookManager, 'delete',
412
        mock.MagicMock(side_effect=Exception()))
413
    @mock.patch.object(
414
        workflows.WorkflowManager, 'create',
415
        mock.MagicMock(return_value=[WF1]))
416
    @mock.patch.object(
417
        executions.ExecutionManager, 'create',
418
        mock.MagicMock(return_value=executions.Execution(None, WF1_EXEC)))
419
    def test_launch_when_workflow_not_exists(self):
420
        liveaction = LiveActionDB(action=WF1_NAME, parameters=ACTION_PARAMS)
421
        liveaction, execution = action_service.request(liveaction)
422
        liveaction = LiveAction.get_by_id(str(liveaction.id))
423
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
424
425
        mistral_context = liveaction.context.get('mistral', None)
426
        self.assertIsNotNone(mistral_context)
427
        self.assertEqual(mistral_context['execution_id'], WF1_EXEC.get('id'))
428
        self.assertEqual(mistral_context['workflow_name'], WF1_EXEC.get('workflow_name'))
429
430
    @mock.patch.object(
431
        workflows.WorkflowManager, 'list',
432
        mock.MagicMock(return_value=[]))
433
    @mock.patch.object(
434
        workflows.WorkflowManager, 'get',
435
        mock.MagicMock(return_value=WF2))
436
    def test_launch_workflow_with_many_workflows(self):
437
        MistralRunner.entry_point = mock.PropertyMock(return_value=WF2_YAML_FILE_PATH)
438
        liveaction = LiveActionDB(action=WF2_NAME, parameters=ACTION_PARAMS)
439
        liveaction, execution = action_service.request(liveaction)
440
        liveaction = LiveAction.get_by_id(str(liveaction.id))
441
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_FAILED)
442
        self.assertIn('Multiple workflows is not supported.', liveaction.result['error'])
443
444
    @mock.patch.object(
445
        workflows.WorkflowManager, 'list',
446
        mock.MagicMock(return_value=[]))
447
    @mock.patch.object(
448
        workflows.WorkflowManager, 'get',
449
        mock.MagicMock(side_effect=Exception()))
450
    def test_launch_workflow_name_mistmatch(self):
451
        action_ref = 'generic.workflow_v2_name_mismatch'
452
        MistralRunner.entry_point = mock.PropertyMock(return_value=WF1_YAML_FILE_PATH)
453
        liveaction = LiveActionDB(action=action_ref, parameters=ACTION_PARAMS)
454
        liveaction, execution = action_service.request(liveaction)
455
        liveaction = LiveAction.get_by_id(str(liveaction.id))
456
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_FAILED)
457
        self.assertIn('Name of the workflow must be the same', liveaction.result['error'])
458
459
    @mock.patch.object(
460
        workflows.WorkflowManager, 'list',
461
        mock.MagicMock(return_value=[]))
462
    @mock.patch.object(
463
        workbooks.WorkbookManager, 'get',
464
        mock.MagicMock(return_value=WB1))
465
    @mock.patch.object(
466
        workbooks.WorkbookManager, 'create',
467
        mock.MagicMock(return_value=WB1))
468
    @mock.patch.object(
469
        workbooks.WorkbookManager, 'update',
470
        mock.MagicMock(return_value=WB1))
471
    @mock.patch.object(
472
        executions.ExecutionManager, 'create',
473
        mock.MagicMock(return_value=executions.Execution(None, WB1_EXEC)))
474
    def test_launch_workbook(self):
475
        MistralRunner.entry_point = mock.PropertyMock(return_value=WB1_YAML_FILE_PATH)
476
        liveaction = LiveActionDB(action=WB1_NAME, parameters=ACTION_PARAMS)
477
        liveaction, execution = action_service.request(liveaction)
478
        liveaction = LiveAction.get_by_id(str(liveaction.id))
479
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
480
481
        mistral_context = liveaction.context.get('mistral', None)
482
        self.assertIsNotNone(mistral_context)
483
        self.assertEqual(mistral_context['execution_id'], WB1_EXEC.get('id'))
484
        self.assertEqual(mistral_context['workflow_name'], WB1_EXEC.get('workflow_name'))
485
486
    @mock.patch.object(
487
        workflows.WorkflowManager, 'list',
488
        mock.MagicMock(return_value=[]))
489
    @mock.patch.object(
490
        workbooks.WorkbookManager, 'get',
491
        mock.MagicMock(return_value=WB2))
492
    @mock.patch.object(
493
        workbooks.WorkbookManager, 'create',
494
        mock.MagicMock(return_value=WB2))
495
    @mock.patch.object(
496
        workbooks.WorkbookManager, 'update',
497
        mock.MagicMock(return_value=WB2))
498
    @mock.patch.object(
499
        executions.ExecutionManager, 'create',
500
        mock.MagicMock(return_value=executions.Execution(None, WB2_EXEC)))
501
    def test_launch_workbook_with_many_workflows(self):
502
        MistralRunner.entry_point = mock.PropertyMock(return_value=WB2_YAML_FILE_PATH)
503
        liveaction = LiveActionDB(action=WB2_NAME, parameters=ACTION_PARAMS)
504
        liveaction, execution = action_service.request(liveaction)
505
        liveaction = LiveAction.get_by_id(str(liveaction.id))
506
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
507
508
        mistral_context = liveaction.context.get('mistral', None)
509
        self.assertIsNotNone(mistral_context)
510
        self.assertEqual(mistral_context['execution_id'], WB2_EXEC.get('id'))
511
        self.assertEqual(mistral_context['workflow_name'], WB2_EXEC.get('workflow_name'))
512
513
    @mock.patch.object(
514
        workflows.WorkflowManager, 'list',
515
        mock.MagicMock(return_value=[]))
516
    @mock.patch.object(
517
        workbooks.WorkbookManager, 'get',
518
        mock.MagicMock(return_value=WB3))
519
    @mock.patch.object(
520
        workbooks.WorkbookManager, 'create',
521
        mock.MagicMock(return_value=WB3))
522
    @mock.patch.object(
523
        workbooks.WorkbookManager, 'update',
524
        mock.MagicMock(return_value=WB3))
525
    @mock.patch.object(
526
        executions.ExecutionManager, 'create',
527
        mock.MagicMock(return_value=executions.Execution(None, WB3_EXEC)))
528
    def test_launch_workbook_with_many_workflows_no_default(self):
529
        MistralRunner.entry_point = mock.PropertyMock(return_value=WB3_YAML_FILE_PATH)
530
        liveaction = LiveActionDB(action=WB3_NAME, parameters=ACTION_PARAMS)
531
        liveaction, execution = action_service.request(liveaction)
532
        liveaction = LiveAction.get_by_id(str(liveaction.id))
533
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_FAILED)
534
        self.assertIn('Default workflow cannot be determined.', liveaction.result['error'])
535
536
    @mock.patch.object(
537
        workflows.WorkflowManager, 'list',
538
        mock.MagicMock(return_value=[]))
539
    @mock.patch.object(
540
        workbooks.WorkbookManager, 'get',
541
        mock.MagicMock(return_value=WB1_OLD))
542
    @mock.patch.object(
543
        workbooks.WorkbookManager, 'create',
544
        mock.MagicMock(return_value=WB1))
545
    @mock.patch.object(
546
        workbooks.WorkbookManager, 'update',
547
        mock.MagicMock(return_value=WB1))
548
    @mock.patch.object(
549
        executions.ExecutionManager, 'create',
550
        mock.MagicMock(return_value=executions.Execution(None, WB1_EXEC)))
551
    def test_launch_when_workbook_definition_changed(self):
552
        MistralRunner.entry_point = mock.PropertyMock(return_value=WB1_YAML_FILE_PATH)
553
        liveaction = LiveActionDB(action=WB1_NAME, parameters=ACTION_PARAMS)
554
        liveaction, execution = action_service.request(liveaction)
555
        liveaction = LiveAction.get_by_id(str(liveaction.id))
556
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
557
558
        mistral_context = liveaction.context.get('mistral', None)
559
        self.assertIsNotNone(mistral_context)
560
        self.assertEqual(mistral_context['execution_id'], WB1_EXEC.get('id'))
561
        self.assertEqual(mistral_context['workflow_name'], WB1_EXEC.get('workflow_name'))
562
563
    @mock.patch.object(
564
        workflows.WorkflowManager, 'list',
565
        mock.MagicMock(return_value=[]))
566
    @mock.patch.object(
567
        workbooks.WorkbookManager, 'get',
568
        mock.MagicMock(side_effect=Exception()))
569
    @mock.patch.object(
570
        workflows.WorkflowManager, 'delete',
571
        mock.MagicMock(side_effect=Exception()))
572
    @mock.patch.object(
573
        workbooks.WorkbookManager, 'create',
574
        mock.MagicMock(return_value=WB1))
575
    @mock.patch.object(
576
        executions.ExecutionManager, 'create',
577
        mock.MagicMock(return_value=executions.Execution(None, WB1_EXEC)))
578
    def test_launch_when_workbook_not_exists(self):
579
        liveaction = LiveActionDB(action=WB1_NAME, parameters=ACTION_PARAMS)
580
        liveaction, execution = action_service.request(liveaction)
581
        liveaction = LiveAction.get_by_id(str(liveaction.id))
582
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
583
584
        mistral_context = liveaction.context.get('mistral', None)
585
        self.assertIsNotNone(mistral_context)
586
        self.assertEqual(mistral_context['execution_id'], WB1_EXEC.get('id'))
587
        self.assertEqual(mistral_context['workflow_name'], WB1_EXEC.get('workflow_name'))
588
589
    @mock.patch.object(
590
        workflows.WorkflowManager, 'list',
591
        mock.MagicMock(return_value=[]))
592
    @mock.patch.object(
593
        workbooks.WorkbookManager, 'get',
594
        mock.MagicMock(side_effect=Exception()))
595
    def test_launch_workbook_name_mismatch(self):
596
        action_ref = 'generic.workbook_v2_name_mismatch'
597
        MistralRunner.entry_point = mock.PropertyMock(return_value=WB1_YAML_FILE_PATH)
598
        liveaction = LiveActionDB(action=action_ref, parameters=ACTION_PARAMS)
599
        liveaction, execution = action_service.request(liveaction)
600
        liveaction = LiveAction.get_by_id(str(liveaction.id))
601
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_FAILED)
602
        self.assertIn('Name of the workbook must be the same', liveaction.result['error'])
603
604
    def test_callback_handler_status_map(self):
605
        # Ensure all StackStorm status are mapped otherwise leads to zombie workflow.
606
        self.assertListEqual(sorted(mistral_status_map.keys()),
607
                             sorted(action_constants.LIVEACTION_STATUSES))
608
609
    @mock.patch.object(
610
        action_executions.ActionExecutionManager, 'update',
611
        mock.MagicMock(return_value=None))
612
    def test_callback_handler_with_result_as_text(self):
613
        MistralCallbackHandler.callback('http://127.0.0.1:8989/v2/action_executions/12345', {},
614
                                        action_constants.LIVEACTION_STATUS_SUCCEEDED,
615
                                        '<html></html>')
616
617
    @mock.patch.object(
618
        action_executions.ActionExecutionManager, 'update',
619
        mock.MagicMock(return_value=None))
620
    def test_callback_handler_with_result_as_dict(self):
621
        MistralCallbackHandler.callback('http://127.0.0.1:8989/v2/action_executions/12345', {},
622
                                        action_constants.LIVEACTION_STATUS_SUCCEEDED, {'a': 1})
623
624
    @mock.patch.object(
625
        action_executions.ActionExecutionManager, 'update',
626
        mock.MagicMock(return_value=None))
627
    def test_callback_handler_with_result_as_json_str(self):
628
        MistralCallbackHandler.callback('http://127.0.0.1:8989/v2/action_executions/12345', {},
629
                                        action_constants.LIVEACTION_STATUS_SUCCEEDED, '{"a": 1}')
630
        MistralCallbackHandler.callback('http://127.0.0.1:8989/v2/action_executions/12345', {},
631
                                        action_constants.LIVEACTION_STATUS_SUCCEEDED, "{'a': 1}")
632
633
    @mock.patch.object(
634
        action_executions.ActionExecutionManager, 'update',
635
        mock.MagicMock(return_value=None))
636
    def test_callback_handler_with_result_as_list(self):
637
        MistralCallbackHandler.callback('http://127.0.0.1:8989/v2/action_executions/12345', {},
638
                                        action_constants.LIVEACTION_STATUS_SUCCEEDED,
639
                                        ["a", "b", "c"])
640
641
    @mock.patch.object(
642
        action_executions.ActionExecutionManager, 'update',
643
        mock.MagicMock(return_value=None))
644
    def test_callback_handler_with_result_as_list_str(self):
645
        MistralCallbackHandler.callback('http://127.0.0.1:8989/v2/action_executions/12345', {},
646
                                        action_constants.LIVEACTION_STATUS_SUCCEEDED,
647
                                        '["a", "b", "c"]')
648
649
    @mock.patch.object(
650
        action_executions.ActionExecutionManager, 'update',
651
        mock.MagicMock(return_value=None))
652
    def test_callback(self):
653
        liveaction = LiveActionDB(
654
            action='core.local', parameters={'cmd': 'uname -a'},
655
            callback={
656
                'source': 'mistral',
657
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
658
            }
659
        )
660
661
        for status in action_constants.LIVEACTION_COMPLETED_STATES:
662
            expected_mistral_status = mistral_status_map[status]
663
            LocalShellRunner.run = mock.Mock(return_value=(status, NON_EMPTY_RESULT, None))
664
            liveaction, execution = action_service.request(liveaction)
665
            liveaction = LiveAction.get_by_id(str(liveaction.id))
666
            self.assertEqual(liveaction.status, status)
667
            action_executions.ActionExecutionManager.update.assert_called_with(
668
                '12345', state=expected_mistral_status, output=NON_EMPTY_RESULT)
669
670
    @mock.patch.object(
671
        LocalShellRunner, 'run',
672
        mock.MagicMock(return_value=(action_constants.LIVEACTION_STATUS_RUNNING,
673
                                     NON_EMPTY_RESULT, None)))
674
    @mock.patch.object(
675
        action_executions.ActionExecutionManager, 'update',
676
        mock.MagicMock(return_value=None))
677
    def test_callback_incomplete_state(self):
678
        liveaction = LiveActionDB(
679
            action='core.local', parameters={'cmd': 'uname -a'},
680
            callback={
681
                'source': 'mistral',
682
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
683
            }
684
        )
685
686
        liveaction, execution = action_service.request(liveaction)
687
        liveaction = LiveAction.get_by_id(str(liveaction.id))
688
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
689
        self.assertFalse(action_executions.ActionExecutionManager.update.called)
690
691
    @mock.patch.object(
692
        LocalShellRunner, 'run',
693
        mock.MagicMock(return_value=(action_constants.LIVEACTION_STATUS_SUCCEEDED,
694
                                     NON_EMPTY_RESULT, None)))
695
    @mock.patch.object(
696
        action_executions.ActionExecutionManager, 'update',
697
        mock.MagicMock(side_effect=[
698
            requests.exceptions.ConnectionError(),
699
            None]))
700
    def test_callback_retry(self):
701
        liveaction = LiveActionDB(
702
            action='core.local', parameters={'cmd': 'uname -a'},
703
            callback={
704
                'source': 'mistral',
705
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
706
            }
707
        )
708
709
        liveaction, execution = action_service.request(liveaction)
710
        liveaction = LiveAction.get_by_id(str(liveaction.id))
711
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
712
713
        calls = [call('12345', state='SUCCESS', output=NON_EMPTY_RESULT) for i in range(0, 2)]
714
        action_executions.ActionExecutionManager.update.assert_has_calls(calls)
715
716
    @mock.patch.object(
717
        LocalShellRunner, 'run',
718
        mock.MagicMock(return_value=(action_constants.LIVEACTION_STATUS_SUCCEEDED,
719
                                     NON_EMPTY_RESULT, None)))
720
    @mock.patch.object(
721
        action_executions.ActionExecutionManager, 'update',
722
        mock.MagicMock(side_effect=[
723
            requests.exceptions.ConnectionError(),
724
            requests.exceptions.ConnectionError(),
725
            requests.exceptions.ConnectionError(),
726
            requests.exceptions.ConnectionError(),
727
            None]))
728
    def test_callback_retry_exhausted(self):
729
        liveaction = LiveActionDB(
730
            action='core.local', parameters={'cmd': 'uname -a'},
731
            callback={
732
                'source': 'mistral',
733
                'url': 'http://127.0.0.1:8989/v2/action_executions/12345'
734
            }
735
        )
736
737
        liveaction, execution = action_service.request(liveaction)
738
        liveaction = LiveAction.get_by_id(str(liveaction.id))
739
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_SUCCEEDED)
740
741
        # This test initially setup mock for action_executions.ActionExecutionManager.update
742
        # to fail the first 4 times and return success on the 5th times. The max attempts
743
        # is set to 3. We expect only 3 calls to pass thru the update method.
744
        calls = [call('12345', state='SUCCESS', output=NON_EMPTY_RESULT) for i in range(0, 2)]
745
        action_executions.ActionExecutionManager.update.assert_has_calls(calls)
746
747
    @mock.patch.object(
748
        workflows.WorkflowManager, 'list',
749
        mock.MagicMock(return_value=[]))
750
    @mock.patch.object(
751
        workflows.WorkflowManager, 'get',
752
        mock.MagicMock(return_value=WF1))
753
    @mock.patch.object(
754
        workflows.WorkflowManager, 'create',
755
        mock.MagicMock(return_value=[WF1]))
756
    @mock.patch.object(
757
        executions.ExecutionManager, 'create',
758
        mock.MagicMock(return_value=executions.Execution(None, WF1_EXEC)))
759
    @mock.patch.object(
760
        executions.ExecutionManager, 'update',
761
        mock.MagicMock(return_value=executions.Execution(None, WF1_EXEC_PAUSED)))
762
    def test_cancel(self):
763
        MistralRunner.entry_point = mock.PropertyMock(return_value=WF1_YAML_FILE_PATH)
764
        liveaction = LiveActionDB(action=WF1_NAME, parameters=ACTION_PARAMS)
765
        liveaction, execution = action_service.request(liveaction)
766
        liveaction = LiveAction.get_by_id(str(liveaction.id))
767
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
768
769
        mistral_context = liveaction.context.get('mistral', None)
770
        self.assertIsNotNone(mistral_context)
771
        self.assertEqual(mistral_context['execution_id'], WF1_EXEC.get('id'))
772
        self.assertEqual(mistral_context['workflow_name'], WF1_EXEC.get('workflow_name'))
773
774
        requester = cfg.CONF.system_user.user
775
        liveaction, execution = action_service.request_cancellation(liveaction, requester)
776
        executions.ExecutionManager.update.assert_called_with(WF1_EXEC.get('id'), 'PAUSED')
777
        liveaction = LiveAction.get_by_id(str(liveaction.id))
778
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_CANCELED)
779
780
    @mock.patch.object(
781
        workflows.WorkflowManager, 'list',
782
        mock.MagicMock(return_value=[]))
783
    @mock.patch.object(
784
        workflows.WorkflowManager, 'get',
785
        mock.MagicMock(return_value=WF1))
786
    @mock.patch.object(
787
        workflows.WorkflowManager, 'create',
788
        mock.MagicMock(return_value=[WF1]))
789
    @mock.patch.object(
790
        executions.ExecutionManager, 'create',
791
        mock.MagicMock(return_value=executions.Execution(None, WF1_EXEC)))
792
    @mock.patch.object(
793
        executions.ExecutionManager, 'update',
794
        mock.MagicMock(side_effect=[requests.exceptions.ConnectionError(),
795
                                    executions.Execution(None, WF1_EXEC_PAUSED)]))
796
    def test_cancel_retry(self):
797
        MistralRunner.entry_point = mock.PropertyMock(return_value=WF1_YAML_FILE_PATH)
798
        liveaction = LiveActionDB(action=WF1_NAME, parameters=ACTION_PARAMS)
799
        liveaction, execution = action_service.request(liveaction)
800
        liveaction = LiveAction.get_by_id(str(liveaction.id))
801
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
802
803
        mistral_context = liveaction.context.get('mistral', None)
804
        self.assertIsNotNone(mistral_context)
805
        self.assertEqual(mistral_context['execution_id'], WF1_EXEC.get('id'))
806
        self.assertEqual(mistral_context['workflow_name'], WF1_EXEC.get('workflow_name'))
807
808
        requester = cfg.CONF.system_user.user
809
        liveaction, execution = action_service.request_cancellation(liveaction, requester)
810
        executions.ExecutionManager.update.assert_called_with(WF1_EXEC.get('id'), 'PAUSED')
811
        liveaction = LiveAction.get_by_id(str(liveaction.id))
812
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_CANCELED)
813
814
    @mock.patch.object(
815
        workflows.WorkflowManager, 'list',
816
        mock.MagicMock(return_value=[]))
817
    @mock.patch.object(
818
        workflows.WorkflowManager, 'get',
819
        mock.MagicMock(return_value=WF1))
820
    @mock.patch.object(
821
        workflows.WorkflowManager, 'create',
822
        mock.MagicMock(return_value=[WF1]))
823
    @mock.patch.object(
824
        executions.ExecutionManager, 'create',
825
        mock.MagicMock(return_value=executions.Execution(None, WF1_EXEC)))
826
    @mock.patch.object(
827
        executions.ExecutionManager, 'update',
828
        mock.MagicMock(side_effect=requests.exceptions.ConnectionError('Connection refused')))
829
    def test_cancel_retry_exhausted(self):
830
        MistralRunner.entry_point = mock.PropertyMock(return_value=WF1_YAML_FILE_PATH)
831
        liveaction = LiveActionDB(action=WF1_NAME, parameters=ACTION_PARAMS)
832
        liveaction, execution = action_service.request(liveaction)
833
        liveaction = LiveAction.get_by_id(str(liveaction.id))
834
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_RUNNING)
835
836
        mistral_context = liveaction.context.get('mistral', None)
837
        self.assertIsNotNone(mistral_context)
838
        self.assertEqual(mistral_context['execution_id'], WF1_EXEC.get('id'))
839
        self.assertEqual(mistral_context['workflow_name'], WF1_EXEC.get('workflow_name'))
840
841
        requester = cfg.CONF.system_user.user
842
        liveaction, execution = action_service.request_cancellation(liveaction, requester)
843
844
        calls = [call(WF1_EXEC.get('id'), 'PAUSED') for i in range(0, 2)]
845
        executions.ExecutionManager.update.assert_has_calls(calls)
846
847
        liveaction = LiveAction.get_by_id(str(liveaction.id))
848
        self.assertEqual(liveaction.status, action_constants.LIVEACTION_STATUS_CANCELING)
849
850
    def test_build_context(self):
851
        parent = {
852
            'mistral': {
853
                'workflow_name': 'foo',
854
                'workflow_execution_id': 'b222b934-7473-4cd4-a2ec-e204a8c93848',
855
                'task_tags': None,
856
                'task_name': 'some_fancy_wf_task',
857
                'task_id': '6c7d4334-3e7d-49c6-918d-698e846affaf',
858
                'action_execution_id': '24da5c88-834c-4a65-8b56-4ddbd654eb68'
859
            }
860
        }
861
862
        current = {
863
            'workflow_name': 'foo.subwf',
864
            'workflow_execution_id': '135e3446-4c89-4afe-821f-6ec6a0849b27'
865
        }
866
867
        context = MistralRunner._build_mistral_context(parent, current)
868
        self.assertTrue(context is not None)
869
        self.assertTrue('parent' in context['mistral'].keys())
870
871
        parent_dict = {
872
            'workflow_name': parent['mistral']['workflow_name'],
873
            'workflow_execution_id': parent['mistral']['workflow_execution_id']
874
        }
875
876
        self.assertDictEqual(context['mistral']['parent'], parent_dict)
877
        self.assertEqual(context['mistral']['workflow_execution_id'],
878
                         current['workflow_execution_id'])
879
880
        parent = None
881
        context = MistralRunner._build_mistral_context(parent, current)
882
        self.assertDictEqual(context['mistral'], current)
883