summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJamie Lennox <jamielennox@redhat.com>2014-11-25 13:25:12 +1000
committerFlavio Percoco <fpercoco@redhat.com>2015-06-11 13:11:56 +0000
commit5ce9c7dc964be0b3e8f9f273169b77ada85cd8ec (patch)
treec02ac042c63cb3f474c32bd56677d15188ea451e
parentdb6420b44779411d6d1f238f6b887f83f1988986 (diff)
downloadpython-glanceclient-5ce9c7dc964be0b3e8f9f273169b77ada85cd8ec.tar.gz
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
-rw-r--r--glanceclient/client.py55
-rw-r--r--glanceclient/common/http.py191
-rw-r--r--glanceclient/common/utils.py8
-rw-r--r--glanceclient/tests/unit/test_http.py36
-rw-r--r--glanceclient/v1/client.py7
-rw-r--r--glanceclient/v2/client.py8
-rw-r--r--test-requirements.txt1
7 files changed, 209 insertions, 97 deletions
diff --git a/glanceclient/client.py b/glanceclient/client.py
index 4c1e7b8..db2a4f7 100644
--- a/glanceclient/client.py
+++ b/glanceclient/client.py
@@ -18,31 +18,44 @@ import warnings
from glanceclient.common import utils
-def Client(version=None, endpoint=None, *args, **kwargs):
+def Client(version=None, endpoint=None, session=None, *args, **kwargs):
"""Client for the OpenStack Images API.
Generic client for the OpenStack Images API. See version classes
for specific details.
- :param string version: The version of API to use. Note this is
- deprecated and should be passed as part of the URL
- (http://$HOST:$PORT/v$VERSION_NUMBER).
+ :param string version: The version of API to use.
+ :param session: A keystoneclient session that should be used for transport.
+ :type session: keystoneclient.session.Session
"""
- if version is not None:
- warnings.warn(("`version` keyword is being deprecated. Please pass the"
- " version as part of the URL. "
- "http://$HOST:$PORT/v$VERSION_NUMBER"),
- DeprecationWarning)
-
- endpoint, url_version = utils.strip_version(endpoint)
-
- if not url_version and not version:
- msg = ("Please provide either the version or an url with the form "
- "http://$HOST:$PORT/v$VERSION_NUMBER")
- raise RuntimeError(msg)
-
- version = int(version or url_version)
-
- module = utils.import_versioned_module(version, 'client')
+ # FIXME(jamielennox): Add a deprecation warning if no session is passed.
+ # Leaving it as an option until we can ensure nothing break when we switch.
+ if session:
+ if endpoint:
+ kwargs.setdefault('endpoint_override', endpoint)
+
+ if not version:
+ __, version = utils.strip_version(endpoint)
+
+ if not version:
+ msg = ("You must provide a client version when using session")
+ raise RuntimeError(msg)
+
+ else:
+ if version is not None:
+ warnings.warn(("`version` keyword is being deprecated. Please pass"
+ " the version as part of the URL. "
+ "http://$HOST:$PORT/v$VERSION_NUMBER"),
+ DeprecationWarning)
+
+ endpoint, url_version = utils.strip_version(endpoint)
+ version = version or url_version
+
+ if not version:
+ msg = ("Please provide either the version or an url with the form "
+ "http://$HOST:$PORT/v$VERSION_NUMBER")
+ raise RuntimeError(msg)
+
+ module = utils.import_versioned_module(int(version), 'client')
client_class = getattr(module, 'Client')
- return client_class(endpoint, *args, **kwargs)
+ return client_class(endpoint, *args, session=session, **kwargs)
diff --git a/glanceclient/common/http.py b/glanceclient/common/http.py
index f746db5..b5f37cb 100644
--- a/glanceclient/common/http.py
+++ b/glanceclient/common/http.py
@@ -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')
diff --git a/glanceclient/common/utils.py b/glanceclient/common/utils.py
index b199bf3..9016860 100644
--- a/glanceclient/common/utils.py
+++ b/glanceclient/common/utils.py
@@ -428,6 +428,14 @@ def safe_header(name, value):
return name, value
+def endpoint_version_from_url(endpoint, default_version=None):
+ if endpoint:
+ endpoint, version = strip_version(endpoint)
+ return endpoint, version or default_version
+ else:
+ return None, default_version
+
+
class IterableWithLength(object):
def __init__(self, iterable, length):
self.iterable = iterable
diff --git a/glanceclient/tests/unit/test_http.py b/glanceclient/tests/unit/test_http.py
index e8bfaaa..e610716 100644
--- a/glanceclient/tests/unit/test_http.py
+++ b/glanceclient/tests/unit/test_http.py
@@ -12,13 +12,17 @@
# 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 functools
import json
+from keystoneclient.auth import token_endpoint
+from keystoneclient import session
import mock
import requests
from requests_mock.contrib import fixture
import six
from six.moves.urllib import parse
+from testscenarios import load_tests_apply_scenarios as load_tests # noqa
import testtools
from testtools import matchers
import types
@@ -30,15 +34,39 @@ from glanceclient import exc
from glanceclient.tests import utils
+def original_only(f):
+ @functools.wraps(f)
+ def wrapper(self, *args, **kwargs):
+ if not hasattr(self.client, 'log_curl_request'):
+ self.skipTest('Skip logging tests for session client')
+
+ return f(self, *args, **kwargs)
+
+
class TestClient(testtools.TestCase):
+ scenarios = [
+ ('httpclient', {'create_client': '_create_http_client'}),
+ ('session', {'create_client': '_create_session_client'})
+ ]
+
+ def _create_http_client(self):
+ return http.HTTPClient(self.endpoint, token=self.token)
+
+ def _create_session_client(self):
+ auth = token_endpoint.Token(self.endpoint, self.token)
+ sess = session.Session(auth=auth)
+ return http.SessionClient(sess)
+
def setUp(self):
super(TestClient, self).setUp()
self.mock = self.useFixture(fixture.Fixture())
self.endpoint = 'http://example.com:9292'
self.ssl_endpoint = 'https://example.com:9292'
- self.client = http.HTTPClient(self.endpoint, token=u'abc123')
+ self.token = u'abc123'
+
+ self.client = getattr(self, self.create_client)()
def test_identity_headers_and_token(self):
identity_headers = {
@@ -140,6 +168,9 @@ class TestClient(testtools.TestCase):
self.assertEqual(text, resp.text)
def test_headers_encoding(self):
+ if not hasattr(self.client, 'encode_headers'):
+ self.skipTest('Cannot do header encoding check on SessionClient')
+
value = u'ni\xf1o'
headers = {"test": value, "none-val": None}
encoded = self.client.encode_headers(headers)
@@ -206,6 +237,7 @@ class TestClient(testtools.TestCase):
self.assertTrue(isinstance(body, types.GeneratorType))
self.assertEqual([data], list(body))
+ @original_only
def test_log_http_response_with_non_ascii_char(self):
try:
response = 'Ok'
@@ -216,6 +248,7 @@ class TestClient(testtools.TestCase):
except UnicodeDecodeError as e:
self.fail("Unexpected UnicodeDecodeError exception '%s'" % e)
+ @original_only
def test_log_curl_request_with_non_ascii_char(self):
try:
headers = {'header1': 'value1\xa5\xa6'}
@@ -225,6 +258,7 @@ class TestClient(testtools.TestCase):
except UnicodeDecodeError as e:
self.fail("Unexpected UnicodeDecodeError exception '%s'" % e)
+ @original_only
@mock.patch('glanceclient.common.http.LOG.debug')
def test_log_curl_request_with_body_and_header(self, mock_log):
hd_name = 'header1'
diff --git a/glanceclient/v1/client.py b/glanceclient/v1/client.py
index 68c2a33..b36f306 100644
--- a/glanceclient/v1/client.py
+++ b/glanceclient/v1/client.py
@@ -29,10 +29,9 @@ class Client(object):
http requests. (optional)
"""
- def __init__(self, endpoint, **kwargs):
+ def __init__(self, endpoint=None, **kwargs):
"""Initialize a new client for the Images v1 API."""
- endpoint, version = utils.strip_version(endpoint)
- self.version = version or 1.0
- self.http_client = http.HTTPClient(endpoint, **kwargs)
+ endpoint, self.version = utils.endpoint_version_from_url(endpoint, 1.0)
+ self.http_client = http.get_http_client(endpoint=endpoint, **kwargs)
self.images = ImageManager(self.http_client)
self.image_members = ImageMemberManager(self.http_client)
diff --git a/glanceclient/v2/client.py b/glanceclient/v2/client.py
index 428ba61..803673b 100644
--- a/glanceclient/v2/client.py
+++ b/glanceclient/v2/client.py
@@ -34,11 +34,9 @@ class Client(object):
http requests. (optional)
"""
- def __init__(self, endpoint, **kwargs):
- endpoint, version = utils.strip_version(endpoint)
- self.version = version or 2.0
- self.http_client = http.HTTPClient(endpoint, **kwargs)
-
+ def __init__(self, endpoint=None, **kwargs):
+ endpoint, self.version = utils.endpoint_version_from_url(endpoint, 2.0)
+ self.http_client = http.get_http_client(endpoint=endpoint, **kwargs)
self.schemas = schemas.Controller(self.http_client)
self.images = images.Controller(self.http_client, self.schemas)
diff --git a/test-requirements.txt b/test-requirements.txt
index 99714ee..8b296b9 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -10,6 +10,7 @@ oslosphinx>=2.5.0 # Apache-2.0
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
testrepository>=0.0.18
testtools>=0.9.36,!=1.2.0
+testscenarios>=0.4
fixtures>=0.3.14
requests-mock>=0.6.0 # Apache-2.0
tempest-lib>=0.5.0