Test Failed
Push — master ( e380d0...f5671d )
by W
02:58
created

st2common/st2common/util/workflow/mistral.py (13 issues)

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
from __future__ import absolute_import
17
import copy
18
import json
19
import re
20
21
import requests
22
import six
23
import yaml
24
from mistralclient.api.base import APIException
25
26
from st2common.exceptions.workflow import WorkflowDefinitionException
27
from st2common import log as logging
28
from st2common.models.system.common import ResourceReference
29
from st2common.models.utils import action_param_utils
30
from st2common.util import action_db as action_utils
31
32
33
LOG = logging.getLogger(__name__)
34
35
36
CMD_PTRN = re.compile("^[\w\.]+[^=\s\"]*")
0 ignored issues
show
A suspicious escape sequence \w was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
A suspicious escape sequence \. was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
A suspicious escape sequence \s was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
37
38
INLINE_YAQL = '<%.*?%>'
39
_ALL_IN_BRACKETS = "\[.*\]\s*"
0 ignored issues
show
A suspicious escape sequence \[ was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
A suspicious escape sequence \] was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
A suspicious escape sequence \s was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
40
_ALL_IN_QUOTES = "\"[^\"]*\"\s*"
0 ignored issues
show
A suspicious escape sequence \s was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
41
_ALL_IN_APOSTROPHES = "'[^']*'\s*"
0 ignored issues
show
A suspicious escape sequence \s was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
42
_DIGITS = "\d+"
0 ignored issues
show
A suspicious escape sequence \d was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
43
_TRUE = "true"
44
_FALSE = "false"
45
_NULL = "null"
46
47
ALL = (
48
    _ALL_IN_QUOTES, _ALL_IN_APOSTROPHES, INLINE_YAQL,
49
    _ALL_IN_BRACKETS, _TRUE, _FALSE, _NULL, _DIGITS
50
)
51
52
PARAMS_PTRN = re.compile("([\w]+)=(%s)" % "|".join(ALL))
0 ignored issues
show
A suspicious escape sequence \w was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
53
54
SPEC_TYPES = {
55
    'adhoc': {
56
        'action_key': 'base',
57
        'input_key': 'base-input'
58
    },
59
    'task': {
60
        'action_key': 'action',
61
        'input_key': 'input'
62
    }
63
}
64
65
JINJA_REGEX_WITH_ST2KV = '{{st2kv\..*?|.*?\sst2kv\..*?}}'
0 ignored issues
show
A suspicious escape sequence \. was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
A suspicious escape sequence \s was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
66
JINJA_REGEX_WITH_ST2KV_PTRN = re.compile(JINJA_REGEX_WITH_ST2KV)
67
JINJA_REGEX_WITH_LOCAL_CTX = '{{.*?_\..*?}}'
0 ignored issues
show
A suspicious escape sequence \. was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
68
JINJA_REGEX_WITH_LOCAL_CTX_PTRN = re.compile(JINJA_REGEX_WITH_LOCAL_CTX)
69
70
71
def _parse_cmd_and_input(cmd_str):
72
    cmd_matcher = CMD_PTRN.search(cmd_str)
73
74
    if not cmd_matcher:
75
        raise ValueError("Invalid action/workflow task property: %s" % cmd_str)
76
77
    cmd = cmd_matcher.group()
78
79
    params = {}
80
    for k, v in re.findall(PARAMS_PTRN, cmd_str):
81
        # Remove embracing quotes.
82
        v = v.strip()
83
        if v[0] == '"' or v[0] == "'":
84
            v = v[1:-1]
85
        else:
86
            try:
87
                v = json.loads(v)
88
            except Exception:
89
                pass
90
91
        params[k] = v
92
93
    return cmd, params
94
95
96
def _merge_dicts(left, right):
97
    if left is None:
98
        return right
99
100
    if right is None:
101
        return left
102
103
    for k, v in six.iteritems(right):
104
        if k not in left:
105
            left[k] = v
106
        else:
107
            left_v = left[k]
108
109
            if isinstance(left_v, dict) and isinstance(v, dict):
110
                _merge_dicts(left_v, v)
111
112
    return left
113
114
115
def _eval_inline_params(spec, action_key, input_key):
116
    action_str = spec.get(action_key)
117
    command, inputs = _parse_cmd_and_input(action_str)
118
    if inputs:
119
        spec[action_key] = command
120
        if input_key not in spec:
121
            spec[input_key] = {}
122
            _merge_dicts(spec[input_key], inputs)
123
124
125
def _validate_action_parameters(name, action, action_params):
126
    requires, unexpected = action_param_utils.validate_action_parameters(action.ref, action_params)
127
128
    if requires:
129
        raise WorkflowDefinitionException('Missing required parameters in "%s" for action "%s": '
130
                                          '"%s"' % (name, action.ref, '", "'.join(requires)))
131
132
    if unexpected:
133
        raise WorkflowDefinitionException('Unexpected parameters in "%s" for action "%s": '
134
                                          '"%s"' % (name, action.ref, '", "'.join(unexpected)))
135
136
137
def _transform_action_param(action_ref, param_name, param_value):
138
    if isinstance(param_value, list):
139
        param_value = [
140
            _transform_action_param(action_ref, param_name, value)
141
            for value in param_value
142
        ]
143
144
    if isinstance(param_value, dict):
145
        param_value = {
146
            name: _transform_action_param(action_ref, name, value)
147
            for name, value in six.iteritems(param_value)
148
        }
149
150
    if isinstance(param_value, six.string_types):
151
        st2kv_matches = JINJA_REGEX_WITH_ST2KV_PTRN.findall(param_value)
152
        local_ctx_matches = JINJA_REGEX_WITH_LOCAL_CTX_PTRN.findall(param_value)
153
154
        if st2kv_matches and local_ctx_matches:
155
            raise WorkflowDefinitionException('Parameter "%s" for action "%s" containing '
156
                                              'references to both local context (i.e. _.var1) '
157
                                              'and st2kv (i.e. st2kv.system.var1) is not '
158
                                              'supported.' % (param_name, action_ref))
159
160
        if st2kv_matches:
161
            param_value = '{% raw %}' + param_value + '{% endraw %}'
162
163
    return param_value
164
165
166
def _transform_action(name, spec):
167
168
    action_key, input_key = None, None
169
170
    for spec_type, spec_meta in six.iteritems(SPEC_TYPES):
171
        if spec_meta['action_key'] in spec:
172
            action_key = spec_meta['action_key']
173
            input_key = spec_meta['input_key']
174
            break
175
176
    if not action_key:
177
        return
178
179
    if spec[action_key] == 'st2.callback':
180
        raise WorkflowDefinitionException('st2.callback is deprecated.')
181
182
    # Convert parameters that are inline (i.e. action: some_action var1={$.value1} var2={$.value2})
183
    # and split it to action name and input dict as illustrated below.
184
    #
185
    # action: some_action
186
    # input:
187
    #   var1: <% $.value1 %>
188
    #   var2: <% $.value2 %>
189
    #
190
    # This step to separate the action name and the input parameters is required
191
    # to wrap them with the st2.action proxy.
192
    #
193
    # action: st2.action
194
    # input:
195
    #   ref: some_action
196
    #   parameters:
197
    #     var1: <% $.value1 %>
198
    #     var2: <% $.value2 %>
199
    _eval_inline_params(spec, action_key, input_key)
200
201
    transformed = (spec[action_key] == 'st2.action')
202
203
    action_ref = spec[input_key]['ref'] if transformed else spec[action_key]
204
205
    action = None
206
207
    # Identify if action is a registered StackStorm action.
208
    if action_ref and ResourceReference.is_resource_reference(action_ref):
209
        action = action_utils.get_action_by_ref(ref=action_ref)
210
211
    # If action is a registered StackStorm action, then wrap the
212
    # action with the st2 proxy and validate the action input.
213
    if action:
214
        if not transformed:
215
            spec[action_key] = 'st2.action'
216
            action_input = spec.get(input_key)
217
            spec[input_key] = {'ref': action_ref}
218
            if action_input:
219
                spec[input_key]['parameters'] = action_input
220
221
        action_input = spec.get(input_key, {})
222
        action_params = action_input.get('parameters', {})
223
        _validate_action_parameters(name, action, action_params)
224
225
        xformed_action_params = {}
226
227
        for param_name in action_params.keys():
228
            param_value = copy.deepcopy(action_params[param_name])
229
            xformed_param_value = _transform_action_param(
230
                action_ref, param_name, param_value)
231
            xformed_action_params[param_name] = xformed_param_value
232
233
        if xformed_action_params != action_params:
234
            spec[input_key]['parameters'] = xformed_action_params
235
236
237
def transform_definition(definition):
238
    # If definition is a dictionary, there is no need to load from YAML.
239
    is_dict = isinstance(definition, dict)
240
    spec = copy.deepcopy(definition) if is_dict else yaml.safe_load(definition)
241
242
    # Transform adhoc actions
243
    for action_name, action_spec in six.iteritems(spec.get('actions', {})):
244
        _transform_action(action_name, action_spec)
245
246
    # Determine if definition is a workbook or workflow
247
    is_workbook = 'workflows' in spec
248
249
    # Transform tasks
250
    if is_workbook:
251
        for workflow_name, workflow_spec in six.iteritems(spec.get('workflows', {})):
252
            if 'tasks' in workflow_spec:
253
                for task_name, task_spec in six.iteritems(workflow_spec.get('tasks')):
254
                    _transform_action(task_name, task_spec)
255
    else:
256
        for key, value in six.iteritems(spec):
257
            if 'tasks' in value:
258
                for task_name, task_spec in six.iteritems(value.get('tasks')):
259
                    _transform_action(task_name, task_spec)
260
261
    # Return the same type as original input.
262
    return spec if is_dict else yaml.safe_dump(spec, default_flow_style=False)
263
264
265
def retry_on_exceptions(exc):
266
    LOG.warning('Determining if %s should be retried...', type(exc))
267
268
    is_connection_error = isinstance(exc, requests.exceptions.ConnectionError)
269
    is_duplicate_error = isinstance(exc, APIException) and 'Duplicate' in exc.error_message
270
    is_messaging_error = isinstance(exc, APIException) and 'MessagingTimeout' in exc.error_message
271
    retrying = is_connection_error or is_duplicate_error or is_messaging_error
272
273
    if retrying:
274
        LOG.warning('Retrying Mistral API invocation on exception type %s.', type(exc))
275
276
    return retrying
277