1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
#
# -------------------------------------------------------------------------
# Copyright (c) 2015-2017 AT&T Intellectual Property
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# -------------------------------------------------------------------------
#
"""REST Helper"""
import json
from os import path
from oslo_config import cfg
from oslo_log import log
import requests
from requests.auth import HTTPBasicAuth
from six.moves.urllib import parse
from conductor.i18n import _LE, _LW # pylint: disable=W0212
LOG = log.getLogger(__name__)
CONF = cfg.CONF
class RESTException(IOError):
"""Basic exception for errors raised by REST"""
class CertificateFileNotFoundException(RESTException, ValueError):
"""Certificate file was not found"""
class MissingURLNetlocException(RESTException, ValueError):
"""URL is missing a host/port"""
class ProhibitedURLSchemeException(RESTException, ValueError):
"""URL is using a prohibited scheme"""
class REST(object):
"""Helper class for REST operations."""
server_url = None
timeout = None
# Why the funny looking connect/read timeouts? Here, read this:
# http://docs.python-requests.org/en/master/user/advanced/#timeouts
def __init__(self, server_url, retries=3, connect_timeout=3.05,
read_timeout=12.05, username=None, password=None,
cert_file=None, cert_key_file=None, ca_bundle_file=None,
log_debug=False):
"""Initializer."""
parsed = parse.urlparse(server_url, 'http')
if parsed.scheme not in ('http', 'https'):
raise ProhibitedURLSchemeException
if not parsed.netloc:
raise MissingURLNetlocException
for file_path in (cert_file, cert_key_file, ca_bundle_file):
if file_path and not path.exists(file_path):
raise CertificateFileNotFoundException
self.server_url = server_url.rstrip('/')
self.retries = int(retries)
self.timeout = (float(connect_timeout), float(read_timeout))
self.log_debug = log_debug
self.username = username
self.password = password
self.cert = cert_file
self.key = cert_key_file
self.verify = ca_bundle_file
# FIXME(jdandrea): Require a CA bundle; do not suppress warnings.
# This is here due to an A&AI's cert/server name mismatch.
# Permitting this defeats the purpose of using SSL/TLS.
if self.verify == "":
requests.packages.urllib3.disable_warnings()
self.verify = False
# Use connection pooling, kthx.
# http://docs.python-requests.org/en/master/user/advanced/
self.session = requests.Session()
def request(self, method='get', content_type='application/json',
path='', headers=None, data=None):
"""Performs HTTP request. Returns a requests.Response object."""
if method not in ('post', 'get', 'put', 'delete'):
method = 'get'
method_fn = getattr(self.session, method)
full_headers = {
'Accept': content_type,
'Content-Type': content_type,
}
if headers:
full_headers.update(headers)
full_url = '{}/{}'.format(self.server_url, path.lstrip('/'))
# Prepare the request args
try:
data_str = json.dumps(data) if data else None
except (TypeError, ValueError):
data_str = data
kwargs = {
'data': data_str,
'headers': full_headers,
'timeout': self.timeout,
'cert': (self.cert, self.key),
'verify': self.verify,
'stream': False,
}
if self.username or self.password:
LOG.debug("Using HTTPBasicAuth")
kwargs['auth'] = HTTPBasicAuth(self.username, self.password)
if self.cert and self.key:
LOG.debug("Using SSL/TLS Certificate/Key")
if self.log_debug:
LOG.debug("Request: {} {}".format(method.upper(), full_url))
if data:
LOG.debug("Request Body: {}".format(json.dumps(data)))
response = None
for attempt in range(self.retries):
if attempt > 0:
LOG.warn(_LW("Retry #{}/{}").format(
attempt + 1, self.retries))
try:
response = method_fn(full_url, **kwargs)
# We shouldn't have to do this since stream is set to False,
# but we're gonna anyway. See "Body Content Workflow" here:
# http://docs.python-requests.org/en/master/user/advanced/
response.close()
if not response.ok:
LOG.warn("Response Status: {} {}".format(
response.status_code, response.reason))
if self.log_debug and response.text:
try:
response_dict = json.loads(response.text)
LOG.debug("Response JSON: {}".format(
json.dumps(response_dict)))
except ValueError:
LOG.debug("Response Body: {}".format(response.text))
if response.ok:
break
except requests.exceptions.RequestException as err:
LOG.error("Exception: %s", err.message)
# Response.__bool__ returns false if status is not ok. Ruh roh!
# That means we must check the object type vs treating it as a bool.
# More info: https://github.com/kennethreitz/requests/issues/2002
if isinstance(response, requests.models.Response) and not response.ok:
LOG.error(_LE("Status {} {} after {} retries for URL: {}").format(
response.status_code, response.reason, self.retries, full_url))
return response
|