Replace httplib2 with httplib as http driver

* This allows us to send truly chunked responses to users
* Handle bad connection url schemes with a new InvalidEndpoint exception
* Fixes bug 1023240

Change-Id: I34500987f51d4e0c6e1f89ecf93853de3fcbb1c3
This commit is contained in:
Brian Waldon
2012-07-10 20:51:00 -07:00
parent 71a0caece8
commit 1e744f162e
6 changed files with 55 additions and 60 deletions

View File

@@ -3,12 +3,11 @@ OpenStack Client interface. Handles the REST calls and responses.
""" """
import copy import copy
import httplib
import logging import logging
import os import os
import StringIO
import urlparse import urlparse
import httplib2
try: try:
import json import json
@@ -29,18 +28,26 @@ USER_AGENT = 'python-glanceclient'
CHUNKSIZE = 1024 * 64 # 64kB CHUNKSIZE = 1024 * 64 # 64kB
class HTTPClient(httplib2.Http): class HTTPClient(object):
def __init__(self, endpoint, token=None, timeout=600, insecure=False): def __init__(self, endpoint, token=None, timeout=600, insecure=False):
super(HTTPClient, self).__init__(timeout=timeout) parts = urlparse.urlparse(endpoint)
self.endpoint = endpoint self.connection_class = self.get_connection_class(parts.scheme)
self.endpoint = (parts.hostname, parts.port)
self.auth_token = token self.auth_token = token
# httplib2 overrides @staticmethod
self.force_exception_to_status_code = True def get_connection_class(scheme):
self.disable_ssl_certificate_validation = insecure try:
return getattr(httplib, '%sConnection' % scheme.upper())
except AttributeError:
msg = 'Unsupported scheme: %s' % scheme
raise exc.InvalidEndpoint(msg)
def http_log(self, args, kwargs, resp, body): def get_connection(self):
return self.connection_class(*self.endpoint)
def http_log(self, args, kwargs, resp):
if os.environ.get('GLANCECLIENT_DEBUG', False): if os.environ.get('GLANCECLIENT_DEBUG', False):
ch = logging.StreamHandler() ch = logging.StreamHandler()
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@@ -64,37 +71,34 @@ class HTTPClient(httplib2.Http):
logger.debug("REQ BODY (RAW): %s\n" % (kwargs['raw_body'])) logger.debug("REQ BODY (RAW): %s\n" % (kwargs['raw_body']))
if 'body' in kwargs: if 'body' in kwargs:
logger.debug("REQ BODY: %s\n" % (kwargs['body'])) logger.debug("REQ BODY: %s\n" % (kwargs['body']))
logger.debug("RESP: %s\nRESP BODY: %s\n", resp, body) logger.debug("RESP: %s", resp)
def _http_request(self, url, method, **kwargs): def _http_request(self, url, method, **kwargs):
""" Send an http request with the specified characteristics. """ Send an http request with the specified characteristics.
Wrapper around httplib2.Http.request to handle tasks such as Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
setting headers, JSON encoding/decoding, and error handling. as setting headers and error handling.
""" """
url = self.endpoint + url
# Copy the kwargs so we can reuse the original in case of redirects # Copy the kwargs so we can reuse the original in case of redirects
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
kwargs['headers'].setdefault('User-Agent', USER_AGENT) kwargs['headers'].setdefault('User-Agent', USER_AGENT)
if self.auth_token: if self.auth_token:
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
resp, body = super(HTTPClient, self).request(url, method, **kwargs) conn = self.get_connection()
self.http_log((url, method,), kwargs, resp, body) conn.request(method, url, **kwargs)
resp = conn.getresponse()
self.http_log((url, method,), kwargs, resp)
if 400 <= resp.status < 600: if 400 <= resp.status < 600:
logger.exception("Request returned failure status.") logger.exception("Request returned failure status.")
raise exc.from_response(resp, body) raise exc.from_response(resp)
elif resp.status in (301, 302, 305): elif resp.status in (301, 302, 305):
# Redirected. Reissue the request to the new location. # Redirected. Reissue the request to the new location.
return self._http_request(resp['location'], method, **kwargs) return self._http_request(resp['location'], method, **kwargs)
#NOTE(bcwaldon): body has been loaded to a string at this point, body_iter = ResponseBodyIterator(resp)
# but we want to move to a world where it can be read from a
# socket-level cache. This is here until we can do that.
body_iter = ResponseBodyIterator(StringIO.StringIO(body))
return resp, body_iter return resp, body_iter
def json_request(self, method, url, **kwargs): def json_request(self, method, url, **kwargs):

View File

@@ -23,6 +23,11 @@ class SchemaNotFound(KeyError):
pass pass
class InvalidEndpoint(ValueError):
"""The provided endpoint could not be used"""
pass
class ClientException(Exception): class ClientException(Exception):
""" """
The base exception class for all exceptions this library raises. The base exception class for all exceptions this library raises.
@@ -104,28 +109,7 @@ _code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
Forbidden, NotFound, OverLimit, HTTPNotImplemented]) Forbidden, NotFound, OverLimit, HTTPNotImplemented])
def from_response(response, body): def from_response(response):
""" """Return an instance of an ClientException based on httplib response."""
Return an instance of an ClientException or subclass
based on an httplib2 response.
Usage::
resp, body = http.request(...)
if resp.status != 200:
raise exception_from_response(resp, body)
"""
cls = _code_map.get(response.status, ClientException) cls = _code_map.get(response.status, ClientException)
if body: return cls(code=response.status)
if hasattr(body, 'keys'):
error = body[body.keys()[0]]
message = error.get('message', None)
details = error.get('details', None)
else:
# If we didn't get back a properly formed error message we
# probably couldn't communicate with Keystone at all.
message = "Unable to communicate with image service: %s." % body
details = None
return cls(code=response.status, message=message, details=details)
else:
return cls(code=response.status)

View File

@@ -18,7 +18,6 @@ Command-line interface to the OpenStack Images API.
""" """
import argparse import argparse
import httplib2
import re import re
import sys import sys
@@ -234,10 +233,6 @@ class OpenStackImagesShell(object):
# Parse args again and call whatever callback was selected # Parse args again and call whatever callback was selected
args = subcommand_parser.parse_args(argv) args = subcommand_parser.parse_args(argv)
# Deal with global arguments
if args.debug:
httplib2.debuglevel = 1
# Short-circuit and deal with help command right away. # Short-circuit and deal with help command right away.
if args.func == self.do_help: if args.func == self.do_help:
self.do_help(args) self.do_help(args)
@@ -315,8 +310,5 @@ def main():
OpenStackImagesShell().main(sys.argv[1:]) OpenStackImagesShell().main(sys.argv[1:])
except Exception, e: except Exception, e:
if httplib2.debuglevel == 1: print >> sys.stderr, e
raise
else:
print >> sys.stderr, e
sys.exit(1) sys.exit(1)

View File

@@ -74,7 +74,7 @@ class ImageManager(base.Manager):
:rtype: :class:`Image` :rtype: :class:`Image`
""" """
resp, body = self.api.raw_request('HEAD', '/v1/images/%s' % image_id) resp, body = self.api.raw_request('HEAD', '/v1/images/%s' % image_id)
meta = self._image_meta_from_headers(resp) meta = self._image_meta_from_headers(dict(resp.getheaders()))
return Image(self, meta) return Image(self, meta)
def data(self, image): def data(self, image):
@@ -84,7 +84,7 @@ class ImageManager(base.Manager):
:rtype: iterable containing image data :rtype: iterable containing image data
""" """
image_id = base.getid(image) image_id = base.getid(image)
resp, body = self.api.raw_request('GET', '/v1/images/%s' % image_id) _, body = self.api.raw_request('GET', '/v1/images/%s' % image_id)
return body return body
def list(self, **kwargs): def list(self, **kwargs):

View File

@@ -13,6 +13,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import copy
import StringIO
from glanceclient.common import http
class FakeAPI(object): class FakeAPI(object):
def __init__(self, fixtures): def __init__(self, fixtures):
@@ -25,7 +30,18 @@ class FakeAPI(object):
return self.fixtures[url][method] return self.fixtures[url][method]
def raw_request(self, *args, **kwargs): def raw_request(self, *args, **kwargs):
return self._request(*args, **kwargs) fixture = self._request(*args, **kwargs)
body_iter = http.ResponseBodyIterator(StringIO.StringIO(fixture[1]))
return FakeResponse(fixture[0]), body_iter
def json_request(self, *args, **kwargs): def json_request(self, *args, **kwargs):
return self._request(*args, **kwargs) fixture = self._request(*args, **kwargs)
return FakeResponse(fixture[0]), fixture[1]
class FakeResponse(object):
def __init__(self, headers):
self.headers = headers
def getheaders(self):
return copy.deepcopy(self.headers).items()

View File

@@ -1,5 +1,4 @@
argparse argparse
httplib2
prettytable>=0.6,<0.7 prettytable>=0.6,<0.7
python-keystoneclient>=0.1,<0.2 python-keystoneclient>=0.1,<0.2
warlock==0.1.0 warlock==0.1.0