Passed
Pull Request — master (#3163)
by W
05:12
created

get_client()   A

Complexity

Conditions 3

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
dl 0
loc 20
rs 9.4285
c 2
b 0
f 0
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 json
18
import re
19
20
import requests
21
import six
22
import yaml
23
from mistralclient.api import client as mistral
24
from mistralclient.api.base import APIException
25
from oslo_config import cfg
26
27
from st2common.exceptions.workflow import WorkflowDefinitionException
28
from st2common import log as logging
29
from st2common.models.system.common import ResourceReference
30
from st2common.models.utils import action_param_utils
31
from st2common.util import action_db as action_utils
32
33
34
LOG = logging.getLogger(__name__)
35
36
37
CMD_PTRN = re.compile("^[\w\.]+[^=\s\"]*")
0 ignored issues
show
Bug introduced by
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...
Bug introduced by
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...
Bug introduced by
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...
38
39
INLINE_YAQL = '<%.*?%>'
40
_ALL_IN_BRACKETS = "\[.*\]\s*"
0 ignored issues
show
Bug introduced by
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...
Bug introduced by
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...
Bug introduced by
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_QUOTES = "\"[^\"]*\"\s*"
0 ignored issues
show
Bug introduced by
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
_ALL_IN_APOSTROPHES = "'[^']*'\s*"
0 ignored issues
show
Bug introduced by
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...
43
_DIGITS = "\d+"
0 ignored issues
show
Bug introduced by
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...
44
_TRUE = "true"
45
_FALSE = "false"
46
_NULL = "null"
47
48
ALL = (
49
    _ALL_IN_QUOTES, _ALL_IN_APOSTROPHES, INLINE_YAQL,
50
    _ALL_IN_BRACKETS, _TRUE, _FALSE, _NULL, _DIGITS
51
)
52
53
PARAMS_PTRN = re.compile("([\w]+)=(%s)" % "|".join(ALL))
0 ignored issues
show
Bug introduced by
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...
54
55
SPEC_TYPES = {
56
    'adhoc': {
57
        'action_key': 'base',
58
        'input_key': 'base-input'
59
    },
60
    'task': {
61
        'action_key': 'action',
62
        'input_key': 'input'
63
    }
64
}
65
66
JINJA_WITH_ST2KV = '{{st2kv\..*?|.*?\sst2kv\..*?}}'
0 ignored issues
show
Bug introduced by
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...
Bug introduced by
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...
67
JINJA_WITH_ST2KV_PTRN = re.compile(JINJA_WITH_ST2KV)
68
JINJA_WITH_LOCAL_CTX = '{{.*?_\..*?}}'
0 ignored issues
show
Bug introduced by
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...
69
JINJA_WITH_LOCAL_CTX_PTRN = re.compile(JINJA_WITH_LOCAL_CTX)
70
71
72
def _parse_cmd_and_input(cmd_str):
73
    cmd_matcher = CMD_PTRN.search(cmd_str)
74
75
    if not cmd_matcher:
76
        raise ValueError("Invalid action/workflow task property: %s" % cmd_str)
77
78
    cmd = cmd_matcher.group()
79
80
    params = {}
81
    for k, v in re.findall(PARAMS_PTRN, cmd_str):
82
        # Remove embracing quotes.
83
        v = v.strip()
84
        if v[0] == '"' or v[0] == "'":
85
            v = v[1:-1]
86
        else:
87
            try:
88
                v = json.loads(v)
89
            except Exception:
90
                pass
91
92
        params[k] = v
93
94
    return cmd, params
95
96
97
def _merge_dicts(left, right):
98
    if left is None:
99
        return right
100
101
    if right is None:
102
        return left
103
104
    for k, v in right.iteritems():
105
        if k not in left:
106
            left[k] = v
107
        else:
108
            left_v = left[k]
109
110
            if isinstance(left_v, dict) and isinstance(v, dict):
111
                _merge_dicts(left_v, v)
112
113
    return left
114
115
116
def _eval_inline_params(spec, action_key, input_key):
117
    action_str = spec.get(action_key)
118
    command, inputs = _parse_cmd_and_input(action_str)
119
    if inputs:
120
        spec[action_key] = command
121
        if input_key not in spec:
122
            spec[input_key] = {}
123
            _merge_dicts(spec[input_key], inputs)
124
125
126
def _validate_action_parameters(name, action, action_params):
127
    requires, unexpected = action_param_utils.validate_action_parameters(action.ref, action_params)
128
129
    if requires:
130
        raise WorkflowDefinitionException('Missing required parameters in "%s" for action "%s": '
131
                                          '"%s"' % (name, action.ref, '", "'.join(requires)))
132
133
    if unexpected:
134
        raise WorkflowDefinitionException('Unexpected parameters in "%s" for action "%s": '
135
                                          '"%s"' % (name, action.ref, '", "'.join(unexpected)))
136
137
138
def _transform_action_param(action_ref, param_name, param_value):
139
    if isinstance(param_value, list):
140
        param_value = [
141
            _transform_action_param(action_ref, param_name, value)
142
            for value in param_value
143
        ]
144
145
    if isinstance(param_value, dict):
146
        param_value = {
147
            name: _transform_action_param(action_ref, name, value)
148
            for name, value in six.iteritems(param_value)
149
        }
150
151
    if isinstance(param_value, six.string_types):
152
        st2kv_matches = JINJA_WITH_ST2KV_PTRN.findall(param_value)
153
        local_ctx_matches = JINJA_WITH_LOCAL_CTX_PTRN.findall(param_value)
154
155
        if st2kv_matches and local_ctx_matches:
156
            raise WorkflowDefinitionException('Parameter "%s" for action "%s" containing '
157
                                              'references to both local context (i.e. _.var1) '
158
                                              'and st2kv (i.e. st2kv.system.var1) is not '
159
                                              'supported.' % (param_name, action_ref))
160
161
        if st2kv_matches:
162
            param_value = '{% raw %}' + param_value + '{% endraw %}'
163
164
    return param_value
165
166
167
def _transform_action(name, spec):
168
169
    action_key, input_key = None, None
170
171
    for spec_type, spec_meta in six.iteritems(SPEC_TYPES):
172
        if spec_meta['action_key'] in spec:
173
            action_key = spec_meta['action_key']
174
            input_key = spec_meta['input_key']
175
            break
176
177
    if not action_key:
178
        return
179
180
    if spec[action_key] == 'st2.callback':
181
        raise WorkflowDefinitionException('st2.callback is deprecated.')
182
183
    # Convert parameters that are inline (i.e. action: some_action var1={$.value1} var2={$.value2})
184
    # and split it to action name and input dict as illustrated below.
185
    #
186
    # action: some_action
187
    # input:
188
    #   var1: <% $.value1 %>
189
    #   var2: <% $.value2 %>
190
    #
191
    # This step to separate the action name and the input parameters is required
192
    # to wrap them with the st2.action proxy.
193
    #
194
    # action: st2.action
195
    # input:
196
    #   ref: some_action
197
    #   parameters:
198
    #     var1: <% $.value1 %>
199
    #     var2: <% $.value2 %>
200
    _eval_inline_params(spec, action_key, input_key)
201
202
    transformed = (spec[action_key] == 'st2.action')
203
204
    action_ref = spec[input_key]['ref'] if transformed else spec[action_key]
205
206
    action = None
207
208
    # Identify if action is a registered StackStorm action.
209
    if action_ref and ResourceReference.is_resource_reference(action_ref):
210
        action = action_utils.get_action_by_ref(ref=action_ref)
211
212
    # If action is a registered StackStorm action, then wrap the
213
    # action with the st2 proxy and validate the action input.
214
    if action:
215
        if not transformed:
216
            spec[action_key] = 'st2.action'
217
            action_input = spec.get(input_key)
218
            spec[input_key] = {'ref': action_ref}
219
            if action_input:
220
                spec[input_key]['parameters'] = action_input
221
222
        action_input = spec.get(input_key, {})
223
        action_params = action_input.get('parameters', {})
224
        _validate_action_parameters(name, action, action_params)
225
226
        xformed_action_params = {}
227
228
        for param_name in action_params.keys():
229
            param_value = copy.deepcopy(action_params[param_name])
230
            xformed_param_value = _transform_action_param(
231
                action_ref, param_name, param_value)
232
            xformed_action_params[param_name] = xformed_param_value
233
234
        if xformed_action_params != action_params:
235
            spec[input_key]['parameters'] = xformed_action_params
236
237
238
def transform_definition(definition):
239
    # If definition is a dictionary, there is no need to load from YAML.
240
    is_dict = isinstance(definition, dict)
241
    spec = copy.deepcopy(definition) if is_dict else yaml.safe_load(definition)
242
243
    # Transform adhoc actions
244
    for action_name, action_spec in six.iteritems(spec.get('actions', {})):
245
        _transform_action(action_name, action_spec)
246
247
    # Determine if definition is a workbook or workflow
248
    is_workbook = 'workflows' in spec
249
250
    # Transform tasks
251
    if is_workbook:
252
        for workflow_name, workflow_spec in six.iteritems(spec.get('workflows', {})):
253
            if 'tasks' in workflow_spec:
254
                for task_name, task_spec in six.iteritems(workflow_spec.get('tasks')):
255
                    _transform_action(task_name, task_spec)
256
    else:
257
        for key, value in six.iteritems(spec):
258
            if 'tasks' in value:
259
                for task_name, task_spec in six.iteritems(value.get('tasks')):
260
                    _transform_action(task_name, task_spec)
261
262
    # Return the same type as original input.
263
    return spec if is_dict else yaml.safe_dump(spec, default_flow_style=False)
264
265
266
def retry_on_exceptions(exc):
267
    LOG.warning('Determining if %s should be retried...', type(exc))
268
269
    is_connection_error = isinstance(exc, requests.exceptions.ConnectionError)
270
    is_duplicate_error = isinstance(exc, APIException) and 'Duplicate' in exc.error_message
271
    is_messaging_error = isinstance(exc, APIException) and 'MessagingTimeout' in exc.error_message
272
    retrying = is_connection_error or is_duplicate_error or is_messaging_error
273
274
    if retrying:
275
        LOG.warning('Retrying Mistral API invocation on exception type %s.', type(exc))
276
277
    return retrying
278
279
280
def get_client(base_url, auth_token=None):
281
    if cfg.CONF.mistral.auth_type and cfg.CONF.mistral.auth_type == 'st2':
282
        return mistral.client(
283
            mistral_url=base_url,
284
            auth_type='st2',
285
            auth_token=auth_token,
286
            api_key=cfg.CONF.mistral.st2_api_key,
287
            cacert=cfg.CONF.mistral.cacert,
288
            insecure=cfg.CONF.mistral.insecure,
289
        )
290
    else:
291
        return mistral.client(
292
            mistral_url=base_url,
293
            auth_type='keystone',
294
            username=cfg.CONF.mistral.keystone_username,
295
            api_key=cfg.CONF.mistral.keystone_password,
296
            project_name=cfg.CONF.mistral.keystone_project_name,
297
            auth_url=cfg.CONF.mistral.keystone_auth_url,
298
            cacert=cfg.CONF.mistral.cacert,
299
            insecure=cfg.CONF.mistral.insecure
300
        )
301