1
# Copyright 2012 OpenStack LLC.
4
# Licensed under the Apache License, Version 2.0 (the "License"); you may
5
# not use this file except in compliance with the License. You may obtain
6
# a copy of the License at
8
# http://www.apache.org/licenses/LICENSE-2.0
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
# License for the specific language governing permissions and limitations
27
#TODO(bcwaldon): Handle this failure more gracefully
33
import simplejson as json
35
# Python 2.5 compat fix
36
if not hasattr(urlparse, 'parse_qsl'):
38
urlparse.parse_qsl = cgi.parse_qsl
41
from ceilometerclient import exc
44
LOG = logging.getLogger(__name__)
45
USER_AGENT = 'python-ceilometerclient'
46
CHUNKSIZE = 1024 * 64 # 64kB
49
class HTTPClient(object):
51
def __init__(self, endpoint, **kwargs):
52
self.endpoint = endpoint
53
self.auth_token = kwargs.get('token')
54
self.connection_params = self.get_connection_params(endpoint, **kwargs)
57
def get_connection_params(endpoint, **kwargs):
58
parts = urlparse.urlparse(endpoint)
60
_args = (parts.hostname, parts.port, parts.path)
61
_kwargs = {'timeout': float(kwargs.get('timeout', 600))}
63
if parts.scheme == 'https':
64
_class = VerifiedHTTPSConnection
65
_kwargs['ca_file'] = kwargs.get('ca_file', None)
66
_kwargs['cert_file'] = kwargs.get('cert_file', None)
67
_kwargs['key_file'] = kwargs.get('key_file', None)
68
_kwargs['insecure'] = kwargs.get('insecure', False)
69
elif parts.scheme == 'http':
70
_class = httplib.HTTPConnection
72
msg = 'Unsupported scheme: %s' % parts.scheme
73
raise exc.InvalidEndpoint(msg)
75
return (_class, _args, _kwargs)
77
def get_connection(self):
78
_class = self.connection_params[0]
80
return _class(*self.connection_params[1],
81
**self.connection_params[2])
82
except httplib.InvalidURL:
83
raise exc.InvalidEndpoint()
85
def log_curl_request(self, method, url, kwargs):
86
curl = ['curl -i -X %s' % method]
88
for (key, value) in kwargs['headers'].items():
89
header = '-H \'%s: %s\'' % (key, value)
93
('key_file', '--key %s'),
94
('cert_file', '--cert %s'),
95
('ca_file', '--cacert %s'),
97
for (key, fmt) in conn_params_fmt:
98
value = self.connection_params[2].get(key)
100
curl.append(fmt % value)
102
if self.connection_params[2].get('insecure'):
106
curl.append('-d \'%s\'' % kwargs['body'])
108
curl.append('%s%s' % (self.endpoint, url))
109
LOG.debug(' '.join(curl))
112
def log_http_response(resp, body=None):
113
status = (resp.version / 10.0, resp.status, resp.reason)
114
dump = ['\nHTTP/%.1f %s %s' % status]
115
dump.extend(['%s: %s' % (k, v) for k, v in resp.getheaders()])
118
dump.extend([body, ''])
119
LOG.debug('\n'.join(dump))
121
def _http_request(self, url, method, **kwargs):
122
""" Send an http request with the specified characteristics.
124
Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
125
as setting headers and error handling.
127
# Copy the kwargs so we can reuse the original in case of redirects
128
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
129
kwargs['headers'].setdefault('User-Agent', USER_AGENT)
131
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
133
self.log_curl_request(method, url, kwargs)
134
conn = self.get_connection()
137
conn_params = self.connection_params[1][2]
138
conn_url = os.path.normpath('%s/%s' % (conn_params, url))
139
conn.request(method, conn_url, **kwargs)
140
resp = conn.getresponse()
141
except socket.gaierror as e:
142
message = "Error finding address for %(url)s: %(e)s" % locals()
143
raise exc.InvalidEndpoint(message=message)
144
except (socket.error, socket.timeout) as e:
145
endpoint = self.endpoint
146
message = "Error communicating with %(endpoint)s %(e)s" % locals()
147
raise exc.CommunicationError(message=message)
149
body_iter = ResponseBodyIterator(resp)
151
# Read body into string if it isn't obviously image data
152
if resp.getheader('content-type', None) != 'application/octet-stream':
153
body_str = ''.join([chunk for chunk in body_iter])
154
self.log_http_response(resp, body_str)
155
body_iter = StringIO.StringIO(body_str)
157
self.log_http_response(resp)
159
if 400 <= resp.status < 600:
160
LOG.warn("Request returned failure status.")
161
raise exc.from_response(resp)
162
elif resp.status in (301, 302, 305):
163
# Redirected. Reissue the request to the new location.
164
return self._http_request(resp['location'], method, **kwargs)
165
elif resp.status == 300:
166
raise exc.from_response(resp)
168
return resp, body_iter
170
def json_request(self, method, url, **kwargs):
171
kwargs.setdefault('headers', {})
172
kwargs['headers'].setdefault('Content-Type', 'application/json')
173
kwargs['headers'].setdefault('Accept', 'application/json')
176
kwargs['body'] = json.dumps(kwargs['body'])
178
resp, body_iter = self._http_request(url, method, **kwargs)
180
if 'application/json' in resp.getheader('content-type', None):
181
body = ''.join([chunk for chunk in body_iter])
183
body = json.loads(body)
185
LOG.error('Could not decode response body as JSON')
191
def raw_request(self, method, url, **kwargs):
192
kwargs.setdefault('headers', {})
193
kwargs['headers'].setdefault('Content-Type',
194
'application/octet-stream')
195
return self._http_request(url, method, **kwargs)
198
class VerifiedHTTPSConnection(httplib.HTTPSConnection):
199
"""httplib-compatibile connection using client-side SSL authentication
201
:see http://code.activestate.com/recipes/
202
577548-https-httplib-client-connection-with-certificate-v/
205
def __init__(self, host, port, key_file=None, cert_file=None,
206
ca_file=None, timeout=None, insecure=False):
207
httplib.HTTPSConnection.__init__(self, host, port, key_file=key_file,
209
self.key_file = key_file
210
self.cert_file = cert_file
211
if ca_file is not None:
212
self.ca_file = ca_file
214
self.ca_file = self.get_system_ca_file()
215
self.timeout = timeout
216
self.insecure = insecure
220
Connect to a host on a given (SSL) port.
221
If ca_file is pointing somewhere, use it to check Server Certificate.
223
Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
224
This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to
225
ssl.wrap_socket(), which forces SSL to check server certificate against
226
our client certificate.
228
sock = socket.create_connection((self.host, self.port), self.timeout)
230
if self._tunnel_host:
234
if self.insecure is True:
235
kwargs = {'cert_reqs': ssl.CERT_NONE}
237
kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file}
240
kwargs['certfile'] = self.cert_file
242
kwargs['keyfile'] = self.key_file
244
self.sock = ssl.wrap_socket(sock, **kwargs)
247
def get_system_ca_file():
248
""""Return path to system default CA file"""
249
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
250
# Suse, FreeBSD/OpenBSD
251
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
252
'/etc/pki/tls/certs/ca-bundle.crt',
253
'/etc/ssl/ca-bundle.pem',
256
if os.path.exists(ca):
261
class ResponseBodyIterator(object):
262
"""A class that acts as an iterator over an HTTP response."""
264
def __init__(self, resp):
272
chunk = self.resp.read(CHUNKSIZE)
276
raise StopIteration()