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