Make glanceclient accept a session object
To make this work we create a different HTTPClient that extends the basic keystoneclient Adapter. The Adapter is a standard set of parameters that all clients should know how to use like region_name and user_agent. We extend this with the glance specific response manipulation like loading and sending iterables. Implements: bp session-objects Change-Id: Ie8eb4bbf7d1a037099a6d4b272cab70525fbfc85
This commit is contained in:
committed by
Flavio Percoco
parent
db6420b447
commit
5ce9c7dc96
+125
-66
@@ -17,6 +17,8 @@ import copy
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from keystoneclient import adapter
|
||||
from keystoneclient import exceptions as ksc_exc
|
||||
from oslo_utils import importutils
|
||||
from oslo_utils import netutils
|
||||
import requests
|
||||
@@ -50,7 +52,71 @@ USER_AGENT = 'python-glanceclient'
|
||||
CHUNKSIZE = 1024 * 64 # 64kB
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
class _BaseHTTPClient(object):
|
||||
|
||||
@staticmethod
|
||||
def _chunk_body(body):
|
||||
chunk = body
|
||||
while chunk:
|
||||
chunk = body.read(CHUNKSIZE)
|
||||
if chunk == '':
|
||||
break
|
||||
yield chunk
|
||||
|
||||
def _set_common_request_kwargs(self, headers, kwargs):
|
||||
"""Handle the common parameters used to send the request."""
|
||||
|
||||
# Default Content-Type is octet-stream
|
||||
content_type = headers.get('Content-Type', 'application/octet-stream')
|
||||
|
||||
# NOTE(jamielennox): remove this later. Managers should pass json= if
|
||||
# they want to send json data.
|
||||
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 = self._chunk_body(data)
|
||||
|
||||
headers['Content-Type'] = content_type
|
||||
kwargs['stream'] = content_type == 'application/octet-stream'
|
||||
|
||||
return data
|
||||
|
||||
def _handle_response(self, resp):
|
||||
if not resp.ok:
|
||||
LOG.debug("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 = _close_after_stream(resp, CHUNKSIZE)
|
||||
else:
|
||||
content = resp.text
|
||||
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)
|
||||
try:
|
||||
body_iter = json.loads(''.join([c for c in body_iter]))
|
||||
except ValueError:
|
||||
body_iter = None
|
||||
|
||||
return resp, body_iter
|
||||
|
||||
|
||||
class HTTPClient(_BaseHTTPClient):
|
||||
|
||||
def __init__(self, endpoint, **kwargs):
|
||||
self.endpoint = endpoint
|
||||
@@ -123,15 +189,16 @@ class HTTPClient(object):
|
||||
LOG.debug(msg)
|
||||
|
||||
@staticmethod
|
||||
def log_http_response(resp, body=None):
|
||||
def log_http_response(resp):
|
||||
status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
|
||||
dump = ['\nHTTP/%.1f %s %s' % status]
|
||||
headers = resp.headers.items()
|
||||
dump.extend(['%s: %s' % safe_header(k, v) for k, v in headers])
|
||||
dump.append('')
|
||||
if body:
|
||||
body = encodeutils.safe_decode(body)
|
||||
dump.extend([body, ''])
|
||||
content_type = resp.headers.get('Content-Type')
|
||||
|
||||
if content_type != 'application/octet-stream':
|
||||
dump.extend([resp.text, ''])
|
||||
LOG.debug('\n'.join([encodeutils.safe_decode(x, errors='ignore')
|
||||
for x in dump]))
|
||||
|
||||
@@ -155,37 +222,13 @@ class HTTPClient(object):
|
||||
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 {}
|
||||
headers = copy.deepcopy(kwargs.pop('headers', {}))
|
||||
|
||||
if self.identity_headers:
|
||||
for k, v in six.iteritems(self.identity_headers):
|
||||
headers.setdefault(k, v)
|
||||
|
||||
# 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)
|
||||
if chunk == '':
|
||||
break
|
||||
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
|
||||
stream = True if content_type == 'application/octet-stream' else False
|
||||
data = self._set_common_request_kwargs(headers, kwargs)
|
||||
|
||||
if osprofiler_web:
|
||||
headers.update(osprofiler_web.get_trace_id_headers())
|
||||
@@ -195,20 +238,20 @@ class HTTPClient(object):
|
||||
# complain.
|
||||
headers = self.encode_headers(headers)
|
||||
|
||||
if self.endpoint.endswith("/") or url.startswith("/"):
|
||||
conn_url = "%s%s" % (self.endpoint, url)
|
||||
else:
|
||||
conn_url = "%s/%s" % (self.endpoint, url)
|
||||
self.log_curl_request(method, conn_url, headers, data, kwargs)
|
||||
|
||||
try:
|
||||
if self.endpoint.endswith("/") or url.startswith("/"):
|
||||
conn_url = "%s%s" % (self.endpoint, url)
|
||||
else:
|
||||
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=stream,
|
||||
headers=headers,
|
||||
**kwargs)
|
||||
except requests.exceptions.Timeout as e:
|
||||
message = ("Error communicating with %(endpoint)s %(e)s" %
|
||||
message = ("Error communicating with %(endpoint)s: %(e)s" %
|
||||
dict(url=conn_url, e=e))
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
except (requests.exceptions.ConnectionError, ProtocolError) as e:
|
||||
@@ -225,34 +268,8 @@ class HTTPClient(object):
|
||||
{'endpoint': endpoint, 'e': e})
|
||||
raise exc.CommunicationError(message=message)
|
||||
|
||||
if not resp.ok:
|
||||
LOG.debug("Request returned failure status %s." % resp.status_code)
|
||||
raise exc.from_response(resp, resp.text)
|
||||
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 = _close_after_stream(resp, CHUNKSIZE)
|
||||
self.log_http_response(resp)
|
||||
else:
|
||||
content = resp.text
|
||||
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)
|
||||
try:
|
||||
body_iter = json.loads(''.join([c for c in body_iter]))
|
||||
except ValueError:
|
||||
body_iter = None
|
||||
resp, body_iter = self._handle_response(resp)
|
||||
self.log_http_response(resp)
|
||||
return resp, body_iter
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
@@ -283,3 +300,45 @@ def _close_after_stream(response, chunk_size):
|
||||
# This will return the connection to the HTTPConnectionPool in urllib3
|
||||
# and ideally reduce the number of HTTPConnectionPool full warnings.
|
||||
response.close()
|
||||
|
||||
|
||||
class SessionClient(adapter.Adapter, _BaseHTTPClient):
|
||||
|
||||
def __init__(self, session, **kwargs):
|
||||
kwargs.setdefault('user_agent', USER_AGENT)
|
||||
kwargs.setdefault('service_type', 'image')
|
||||
super(SessionClient, self).__init__(session, **kwargs)
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
headers = kwargs.pop('headers', {})
|
||||
kwargs['raise_exc'] = False
|
||||
data = self._set_common_request_kwargs(headers, kwargs)
|
||||
|
||||
try:
|
||||
resp = super(SessionClient, self).request(url,
|
||||
method,
|
||||
headers=headers,
|
||||
data=data,
|
||||
**kwargs)
|
||||
except ksc_exc.RequestTimeout as e:
|
||||
message = ("Error communicating with %(endpoint)s %(e)s" %
|
||||
dict(url=conn_url, e=e))
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
except ksc_exc.ConnectionRefused as e:
|
||||
conn_url = self.get_endpoint(auth=kwargs.get('auth'))
|
||||
conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/'))
|
||||
message = ("Error finding address for %(url)s: %(e)s" %
|
||||
dict(url=conn_url, e=e))
|
||||
raise exc.CommunicationError(message=message)
|
||||
|
||||
return self._handle_response(resp)
|
||||
|
||||
|
||||
def get_http_client(endpoint=None, session=None, **kwargs):
|
||||
if session:
|
||||
return SessionClient(session, **kwargs)
|
||||
elif endpoint:
|
||||
return HTTPClient(endpoint, **kwargs)
|
||||
else:
|
||||
raise AttributeError('Constructing a client must contain either an '
|
||||
'endpoint or a session')
|
||||
|
||||
Reference in New Issue
Block a user