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

runners/http_runner/http_runner/http_runner.py (1 issue)

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 ast
18
import copy
19
import json
20
import uuid
21
22
import requests
23
from requests.auth import HTTPBasicAuth
24
from oslo_config import cfg
25
26
from st2common.runners.base import ActionRunner
27
from st2common.runners.base import get_metadata as get_runner_metadata
28
from st2common import __version__ as st2_version
29
from st2common import log as logging
30
from st2common.constants.action import LIVEACTION_STATUS_SUCCEEDED
31
from st2common.constants.action import LIVEACTION_STATUS_FAILED
32
from st2common.constants.action import LIVEACTION_STATUS_TIMED_OUT
33
import six
34
from six.moves import range
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in range.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
35
36
__all__ = [
37
    'HttpRunner',
38
39
    'HTTPClient',
40
41
    'get_runner',
42
    'get_metadata'
43
]
44
45
LOG = logging.getLogger(__name__)
46
SUCCESS_STATUS_CODES = [code for code in range(200, 207)]
47
48
# Lookup constants for runner params
49
RUNNER_ON_BEHALF_USER = 'user'
50
RUNNER_URL = 'url'
51
RUNNER_HEADERS = 'headers'  # Debatable whether this should be action params.
52
RUNNER_COOKIES = 'cookies'
53
RUNNER_ALLOW_REDIRECTS = 'allow_redirects'
54
RUNNER_HTTP_PROXY = 'http_proxy'
55
RUNNER_HTTPS_PROXY = 'https_proxy'
56
RUNNER_VERIFY_SSL_CERT = 'verify_ssl_cert'
57
RUNNER_USERNAME = 'username'
58
RUNNER_PASSWORD = 'password'
59
60
# Lookup constants for action params
61
ACTION_AUTH = 'auth'
62
ACTION_BODY = 'body'
63
ACTION_TIMEOUT = 'timeout'
64
ACTION_METHOD = 'method'
65
ACTION_QUERY_PARAMS = 'params'
66
FILE_NAME = 'file_name'
67
FILE_CONTENT = 'file_content'
68
FILE_CONTENT_TYPE = 'file_content_type'
69
70
RESPONSE_BODY_PARSE_FUNCTIONS = {
71
    'application/json': json.loads
72
}
73
74
75
class HttpRunner(ActionRunner):
76
    def __init__(self, runner_id):
77
        super(HttpRunner, self).__init__(runner_id=runner_id)
78
        self._on_behalf_user = cfg.CONF.system_user.user
79
        self._timeout = 60
80
81
    def pre_run(self):
82
        super(HttpRunner, self).pre_run()
83
84
        LOG.debug('Entering HttpRunner.pre_run() for liveaction_id="%s"', self.liveaction_id)
85
        self._on_behalf_user = self.runner_parameters.get(RUNNER_ON_BEHALF_USER,
86
                                                          self._on_behalf_user)
87
        self._url = self.runner_parameters.get(RUNNER_URL, None)
88
        self._headers = self.runner_parameters.get(RUNNER_HEADERS, {})
89
90
        self._cookies = self.runner_parameters.get(RUNNER_COOKIES, None)
91
        self._allow_redirects = self.runner_parameters.get(RUNNER_ALLOW_REDIRECTS, False)
92
        self._username = self.runner_parameters.get(RUNNER_USERNAME, None)
93
        self._password = self.runner_parameters.get(RUNNER_PASSWORD, None)
94
        self._http_proxy = self.runner_parameters.get(RUNNER_HTTP_PROXY, None)
95
        self._https_proxy = self.runner_parameters.get(RUNNER_HTTPS_PROXY, None)
96
        self._verify_ssl_cert = self.runner_parameters.get(RUNNER_VERIFY_SSL_CERT, None)
97
98
    def run(self, action_parameters):
99
        client = self._get_http_client(action_parameters)
100
101
        try:
102
            result = client.run()
103
        except requests.exceptions.Timeout as e:
104
            result = {'error': str(e)}
105
            status = LIVEACTION_STATUS_TIMED_OUT
106
        else:
107
            status = HttpRunner._get_result_status(result.get('status_code', None))
108
109
        return (status, result, None)
110
111
    def _get_http_client(self, action_parameters):
112
        body = action_parameters.get(ACTION_BODY, None)
113
        timeout = float(action_parameters.get(ACTION_TIMEOUT, self._timeout))
114
        method = action_parameters.get(ACTION_METHOD, None)
115
        params = action_parameters.get(ACTION_QUERY_PARAMS, None)
116
        auth = action_parameters.get(ACTION_AUTH, {})
117
118
        file_name = action_parameters.get(FILE_NAME, None)
119
        file_content = action_parameters.get(FILE_CONTENT, None)
120
        file_content_type = action_parameters.get(FILE_CONTENT_TYPE, None)
121
122
        # Include our user agent and action name so requests can be tracked back
123
        headers = copy.deepcopy(self._headers) if self._headers else {}
124
        headers['User-Agent'] = 'st2/v%s' % (st2_version)
125
        headers['X-Stanley-Action'] = self.action_name
126
127
        if file_name and file_content:
128
            files = {}
129
130
            if file_content_type:
131
                value = (file_content, file_content_type)
132
            else:
133
                value = (file_content)
134
135
            files[file_name] = value
136
        else:
137
            files = None
138
139
        proxies = {}
140
141
        if self._http_proxy:
142
            proxies['http'] = self._http_proxy
143
144
        if self._https_proxy:
145
            proxies['https'] = self._https_proxy
146
147
        return HTTPClient(url=self._url, method=method, body=body, params=params,
148
                          headers=headers, cookies=self._cookies, auth=auth,
149
                          timeout=timeout, allow_redirects=self._allow_redirects,
150
                          proxies=proxies, files=files, verify=self._verify_ssl_cert,
151
                          username=self._username, password=self._password)
152
153
    @staticmethod
154
    def _get_result_status(status_code):
155
        return LIVEACTION_STATUS_SUCCEEDED if status_code in SUCCESS_STATUS_CODES \
156
            else LIVEACTION_STATUS_FAILED
157
158
159
class HTTPClient(object):
160
    def __init__(self, url=None, method=None, body='', params=None, headers=None, cookies=None,
161
                 auth=None, timeout=60, allow_redirects=False, proxies=None,
162
                 files=None, verify=False, username=None, password=None):
163
        if url is None:
164
            raise Exception('URL must be specified.')
165
166
        if method is None:
167
            if files or body:
168
                method = 'POST'
169
            else:
170
                method = 'GET'
171
172
        headers = headers or {}
173
        normalized_headers = self._normalize_headers(headers=headers)
174
        if body and 'content-length' not in normalized_headers:
175
            headers['Content-Length'] = str(len(body))
176
177
        self.url = url
178
        self.method = method
179
        self.headers = headers
180
        self.body = body
181
        self.params = params
182
        self.headers = headers
183
        self.cookies = cookies
184
        self.auth = auth
185
        self.timeout = timeout
186
        self.allow_redirects = allow_redirects
187
        self.proxies = proxies
188
        self.files = files
189
        self.verify = verify
190
        self.username = username
191
        self.password = password
192
193
    def run(self):
194
        results = {}
195
        resp = None
196
        json_content = self._is_json_content()
197
198
        try:
199
            if json_content:
200
                # cast params (body) to dict
201
                data = self._cast_object(self.body)
202
203
                try:
204
                    data = json.dumps(data)
205
                except ValueError:
206
                    msg = 'Request body (%s) can\'t be parsed as JSON' % (data)
207
                    raise ValueError(msg)
208
            else:
209
                data = self.body
210
211
            if self.username or self.password:
212
                self.auth = HTTPBasicAuth(self.username, self.password)
213
214
            resp = requests.request(
215
                self.method,
216
                self.url,
217
                params=self.params,
218
                data=data,
219
                headers=self.headers,
220
                cookies=self.cookies,
221
                auth=self.auth,
222
                timeout=self.timeout,
223
                allow_redirects=self.allow_redirects,
224
                proxies=self.proxies,
225
                files=self.files,
226
                verify=self.verify
227
            )
228
229
            headers = dict(resp.headers)
230
            body, parsed = self._parse_response_body(headers=headers, body=resp.text)
231
232
            results['status_code'] = resp.status_code
233
            results['body'] = body
234
            results['parsed'] = parsed  # flag which indicates if body has been parsed
235
            results['headers'] = headers
236
            return results
237
        except Exception as e:
238
            LOG.exception('Exception making request to remote URL: %s, %s', self.url, e)
239
            raise
240
        finally:
241
            if resp:
242
                resp.close()
243
244
    def _parse_response_body(self, headers, body):
245
        """
246
        :param body: Response body.
247
        :type body: ``str``
248
249
        :return: (parsed body, flag which indicates if body has been parsed)
250
        :rtype: (``object``, ``bool``)
251
        """
252
        body = body or ''
253
        headers = self._normalize_headers(headers=headers)
254
        content_type = headers.get('content-type', None)
255
        parsed = False
256
257
        if not content_type:
258
            return (body, parsed)
259
260
        # The header can also contain charset which we simply discard
261
        content_type = content_type.split(';')[0]
262
        parse_func = RESPONSE_BODY_PARSE_FUNCTIONS.get(content_type, None)
263
264
        if not parse_func:
265
            return (body, parsed)
266
267
        LOG.debug('Parsing body with content type: %s', content_type)
268
269
        try:
270
            body = parse_func(body)
271
        except Exception:
272
            LOG.exception('Failed to parse body')
273
        else:
274
            parsed = True
275
276
        return (body, parsed)
277
278
    def _normalize_headers(self, headers):
279
        """
280
        Normalize the header keys by lowercasing all the keys.
281
        """
282
        result = {}
283
        for key, value in headers.items():
284
            result[key.lower()] = value
285
286
        return result
287
288
    def _is_json_content(self):
289
        normalized = self._normalize_headers(self.headers)
290
        return normalized.get('content-type', None) == 'application/json'
291
292
    def _cast_object(self, value):
293
        if isinstance(value, str) or isinstance(value, six.text_type):
294
            try:
295
                return json.loads(value)
296
            except:
297
                return ast.literal_eval(value)
298
        else:
299
            return value
300
301
302
def get_runner():
303
    return HttpRunner(str(uuid.uuid4()))
304
305
306
def get_metadata():
307
    return get_runner_metadata('http_runner')
308