~ubuntu-branches/ubuntu/wily/python-ceilometerclient/wily

« back to all changes in this revision

Viewing changes to ceilometerclient/common/http.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2013-03-01 12:47:39 UTC
  • Revision ID: package-import@ubuntu.com-20130301124739-0in66psacnqpksch
Tags: upstream-0.1~dde86a3
ImportĀ upstreamĀ versionĀ 0.1~dde86a3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2012 OpenStack LLC.
 
2
# All Rights Reserved.
 
3
#
 
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
 
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, WITHOUT
 
12
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
13
#    License for the specific language governing permissions and limitations
 
14
#    under the License.
 
15
 
 
16
import copy
 
17
import httplib
 
18
import logging
 
19
import os
 
20
import socket
 
21
import StringIO
 
22
import urlparse
 
23
 
 
24
try:
 
25
    import ssl
 
26
except ImportError:
 
27
    #TODO(bcwaldon): Handle this failure more gracefully
 
28
    pass
 
29
 
 
30
try:
 
31
    import json
 
32
except ImportError:
 
33
    import simplejson as json
 
34
 
 
35
# Python 2.5 compat fix
 
36
if not hasattr(urlparse, 'parse_qsl'):
 
37
    import cgi
 
38
    urlparse.parse_qsl = cgi.parse_qsl
 
39
 
 
40
 
 
41
from ceilometerclient import exc
 
42
 
 
43
 
 
44
LOG = logging.getLogger(__name__)
 
45
USER_AGENT = 'python-ceilometerclient'
 
46
CHUNKSIZE = 1024 * 64  # 64kB
 
47
 
 
48
 
 
49
class HTTPClient(object):
 
50
 
 
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)
 
55
 
 
56
    @staticmethod
 
57
    def get_connection_params(endpoint, **kwargs):
 
58
        parts = urlparse.urlparse(endpoint)
 
59
 
 
60
        _args = (parts.hostname, parts.port, parts.path)
 
61
        _kwargs = {'timeout': float(kwargs.get('timeout', 600))}
 
62
 
 
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
 
71
        else:
 
72
            msg = 'Unsupported scheme: %s' % parts.scheme
 
73
            raise exc.InvalidEndpoint(msg)
 
74
 
 
75
        return (_class, _args, _kwargs)
 
76
 
 
77
    def get_connection(self):
 
78
        _class = self.connection_params[0]
 
79
        try:
 
80
            return _class(*self.connection_params[1],
 
81
                          **self.connection_params[2])
 
82
        except httplib.InvalidURL:
 
83
            raise exc.InvalidEndpoint()
 
84
 
 
85
    def log_curl_request(self, method, url, kwargs):
 
86
        curl = ['curl -i -X %s' % method]
 
87
 
 
88
        for (key, value) in kwargs['headers'].items():
 
89
            header = '-H \'%s: %s\'' % (key, value)
 
90
            curl.append(header)
 
91
 
 
92
        conn_params_fmt = [
 
93
            ('key_file', '--key %s'),
 
94
            ('cert_file', '--cert %s'),
 
95
            ('ca_file', '--cacert %s'),
 
96
        ]
 
97
        for (key, fmt) in conn_params_fmt:
 
98
            value = self.connection_params[2].get(key)
 
99
            if value:
 
100
                curl.append(fmt % value)
 
101
 
 
102
        if self.connection_params[2].get('insecure'):
 
103
            curl.append('-k')
 
104
 
 
105
        if 'body' in kwargs:
 
106
            curl.append('-d \'%s\'' % kwargs['body'])
 
107
 
 
108
        curl.append('%s%s' % (self.endpoint, url))
 
109
        LOG.debug(' '.join(curl))
 
110
 
 
111
    @staticmethod
 
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()])
 
116
        dump.append('')
 
117
        if body:
 
118
            dump.extend([body, ''])
 
119
        LOG.debug('\n'.join(dump))
 
120
 
 
121
    def _http_request(self, url, method, **kwargs):
 
122
        """ Send an http request with the specified characteristics.
 
123
 
 
124
        Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
 
125
        as setting headers and error handling.
 
126
        """
 
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)
 
130
        if self.auth_token:
 
131
            kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
 
132
 
 
133
        self.log_curl_request(method, url, kwargs)
 
134
        conn = self.get_connection()
 
135
 
 
136
        try:
 
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)
 
148
 
 
149
        body_iter = ResponseBodyIterator(resp)
 
150
 
 
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)
 
156
        else:
 
157
            self.log_http_response(resp)
 
158
 
 
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)
 
167
 
 
168
        return resp, body_iter
 
169
 
 
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')
 
174
 
 
175
        if 'body' in kwargs:
 
176
            kwargs['body'] = json.dumps(kwargs['body'])
 
177
 
 
178
        resp, body_iter = self._http_request(url, method, **kwargs)
 
179
 
 
180
        if 'application/json' in resp.getheader('content-type', None):
 
181
            body = ''.join([chunk for chunk in body_iter])
 
182
            try:
 
183
                body = json.loads(body)
 
184
            except ValueError:
 
185
                LOG.error('Could not decode response body as JSON')
 
186
        else:
 
187
            body = None
 
188
 
 
189
        return resp, body
 
190
 
 
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)
 
196
 
 
197
 
 
198
class VerifiedHTTPSConnection(httplib.HTTPSConnection):
 
199
    """httplib-compatibile connection using client-side SSL authentication
 
200
 
 
201
    :see http://code.activestate.com/recipes/
 
202
            577548-https-httplib-client-connection-with-certificate-v/
 
203
    """
 
204
 
 
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,
 
208
                                         cert_file=cert_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
 
213
        else:
 
214
            self.ca_file = self.get_system_ca_file()
 
215
        self.timeout = timeout
 
216
        self.insecure = insecure
 
217
 
 
218
    def connect(self):
 
219
        """
 
220
        Connect to a host on a given (SSL) port.
 
221
        If ca_file is pointing somewhere, use it to check Server Certificate.
 
222
 
 
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.
 
227
        """
 
228
        sock = socket.create_connection((self.host, self.port), self.timeout)
 
229
 
 
230
        if self._tunnel_host:
 
231
            self.sock = sock
 
232
            self._tunnel()
 
233
 
 
234
        if self.insecure is True:
 
235
            kwargs = {'cert_reqs': ssl.CERT_NONE}
 
236
        else:
 
237
            kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file}
 
238
 
 
239
        if self.cert_file:
 
240
            kwargs['certfile'] = self.cert_file
 
241
            if self.key_file:
 
242
                kwargs['keyfile'] = self.key_file
 
243
 
 
244
        self.sock = ssl.wrap_socket(sock, **kwargs)
 
245
 
 
246
    @staticmethod
 
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',
 
254
                   '/etc/ssl/cert.pem']
 
255
        for ca in ca_path:
 
256
            if os.path.exists(ca):
 
257
                return ca
 
258
        return None
 
259
 
 
260
 
 
261
class ResponseBodyIterator(object):
 
262
    """A class that acts as an iterator over an HTTP response."""
 
263
 
 
264
    def __init__(self, resp):
 
265
        self.resp = resp
 
266
 
 
267
    def __iter__(self):
 
268
        while True:
 
269
            yield self.next()
 
270
 
 
271
    def next(self):
 
272
        chunk = self.resp.read(CHUNKSIZE)
 
273
        if chunk:
 
274
            return chunk
 
275
        else:
 
276
            raise StopIteration()