python-glanceclient/glanceclient/common/http.py

239 lines
8.6 KiB
Python

# Copyright 2012 OpenStack Foundation
# All Rights Reserved.
#
# 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.
import copy
import logging
import socket
import requests
import six
from six.moves.urllib import parse
try:
import json
except ImportError:
import simplejson as json
# Python 2.5 compat fix
if not hasattr(parse, 'parse_qsl'):
import cgi
parse.parse_qsl = cgi.parse_qsl
from glanceclient.common import https
from glanceclient import exc
from glanceclient.openstack.common import importutils
from glanceclient.openstack.common import network_utils
from glanceclient.openstack.common import strutils
osprofiler_web = importutils.try_import("osprofiler.web")
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-glanceclient'
CHUNKSIZE = 1024 * 64 # 64kB
class HTTPClient(object):
def __init__(self, endpoint, **kwargs):
self.endpoint = endpoint
self.identity_headers = kwargs.get('identity_headers')
self.auth_token = kwargs.get('token')
if self.identity_headers:
if self.identity_headers.get('X-Auth-Token'):
self.auth_token = self.identity_headers.get('X-Auth-Token')
del self.identity_headers['X-Auth-Token']
self.session = requests.Session()
self.session.headers["User-Agent"] = USER_AGENT
self.session.headers["X-Auth-Token"] = self.auth_token
self.timeout = float(kwargs.get('timeout', 600))
if self.endpoint.startswith("https"):
compression = kwargs.get('ssl_compression', True)
if not compression:
self.session.mount("https://", https.HTTPSAdapter())
self.session.verify = kwargs.get('cacert',
not kwargs.get('insecure', True))
self.session.cert = (kwargs.get('cert_file'),
kwargs.get('key_file'))
@staticmethod
def parse_endpoint(endpoint):
return network_utils.urlsplit(endpoint)
def log_curl_request(self, method, url, headers, data, kwargs):
curl = ['curl -i -X %s' % method]
for (key, value) in self.session.headers.items():
if key.lower() == 'x-auth-token':
value = '*' * 3
header = '-H \'%s: %s\'' % (key, value)
curl.append(strutils.safe_encode(header))
if not self.session.verify:
curl.append('-k')
else:
if isinstance(self.session.verify, six.string_types):
curl.append(' --cacert %s' % self.session.verify)
if self.session.cert:
curl.append(' --cert %s --key %s' % self.session.cert)
if data and isinstance(data, six.string_types):
curl.append('-d \'%s\'' % data)
if "//:" not in url:
url = '%s%s' % (self.endpoint, url)
curl.append(url)
LOG.debug(strutils.safe_encode(' '.join(curl), errors='ignore'))
@staticmethod
def log_http_response(resp, body=None):
status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
dump = ['\nHTTP/%.1f %s %s' % status]
headers = resp.headers.items()
if 'X-Auth-Token' in headers:
headers['X-Auth-Token'] = '*' * 3
dump.extend(['%s: %s' % (k, v) for k, v in headers])
dump.append('')
if body:
body = strutils.safe_decode(body)
dump.extend([body, ''])
LOG.debug('\n'.join([strutils.safe_encode(x) for x in dump]))
@staticmethod
def encode_headers(headers):
"""Encodes headers.
Note: This should be used right before
sending anything out.
:param headers: Headers to encode
:returns: Dictionary with encoded headers'
names and values
"""
return dict((strutils.safe_encode(h), strutils.safe_encode(v))
for h, v in six.iteritems(headers))
def _request(self, method, url, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
as setting headers and error handling.
"""
# Copy the kwargs so we can reuse the original in case of redirects
headers = kwargs.pop("headers", {})
headers = headers and copy.deepcopy(headers) or {}
# Default Content-Type is octet-stream
content_type = headers.get('Content-Type', 'application/octet-stream')
def chunk_body(body):
chunk = body
while chunk:
chunk = body.read(CHUNKSIZE)
yield chunk
data = kwargs.pop("data", None)
if data is not None and not isinstance(data, six.string_types):
try:
data = json.dumps(data)
content_type = 'application/json'
except TypeError:
# Here we assume it's
# a file-like object
# and we'll chunk it
data = chunk_body(data)
headers['Content-Type'] = content_type
# Note(flaper87): Before letting headers / url fly,
# they should be encoded otherwise httplib will
# complain.
headers = self.encode_headers(headers)
try:
conn_url = "%s/%s" % (self.endpoint, url)
self.log_curl_request(method, conn_url, headers, data, kwargs)
resp = self.session.request(method,
conn_url,
data=data,
stream=True,
headers=headers,
**kwargs)
except requests.exceptions.Timeout as e:
message = ("Error communicating with %(endpoint)s %(e)s" %
dict(url=conn_url, e=e))
raise exc.InvalidEndpoint(message=message)
except requests.exceptions.ConnectionError as e:
message = ("Error finding address for %(url)s: %(e)s" %
dict(url=conn_url, e=e))
raise exc.CommunicationError(message=message)
except socket.gaierror as e:
message = "Error finding address for %s: %s" % (
self.endpoint_hostname, e)
raise exc.InvalidEndpoint(message=message)
except (socket.error, socket.timeout) as e:
endpoint = self.endpoint
message = ("Error communicating with %(endpoint)s %(e)s" %
{'endpoint': endpoint, 'e': e})
raise exc.CommunicationError(message=message)
if not resp.ok:
LOG.error("Request returned failure status %s." % resp.status_code)
raise exc.from_response(resp, resp.content)
elif resp.status_code == requests.codes.MULTIPLE_CHOICES:
raise exc.from_response(resp)
content_type = resp.headers.get('Content-Type')
# Read body into string if it isn't obviously image data
if content_type == 'application/octet-stream':
# Do not read all response in memory when
# downloading an image.
body_iter = resp.iter_content(chunk_size=CHUNKSIZE)
self.log_http_response(resp)
else:
content = resp.content
self.log_http_response(resp, content)
if content_type and content_type.startswith('application/json'):
# Let's use requests json method,
# it should take care of response
# encoding
body_iter = resp.json()
else:
body_iter = six.StringIO(content)
return resp, body_iter
def head(self, url, **kwargs):
return self._request('HEAD', url, **kwargs)
def get(self, url, **kwargs):
return self._request('GET', url, **kwargs)
def post(self, url, **kwargs):
return self._request('POST', url, **kwargs)
def put(self, url, **kwargs):
return self._request('PUT', url, **kwargs)
def patch(self, url, **kwargs):
return self._request('PATCH', url, **kwargs)
def delete(self, url, **kwargs):
return self._request('DELETE', url, **kwargs)