Use requests module for HTTP/HTTPS

* Implement correct certificate verification
* Add --os-cacert
* Rework tests for requests

Pinned requests module to < 1.0 as 1.0.2 is now current in pipi
as of 17Dec2012.

Blueprint: tls-verify

Change-Id: I71066ff7297f3b70c08b7ae1c8ae8b6a1b82bbae
This commit is contained in:
Dean Troyer
2012-12-13 12:01:07 -06:00
parent d3603535d2
commit 82e47d0866
10 changed files with 294 additions and 215 deletions

View File

@@ -7,9 +7,9 @@
OpenStack Client interface. Handles the REST calls and responses. OpenStack Client interface. Handles the REST calls and responses.
""" """
import httplib2
import logging import logging
import os import os
import sys
import urlparse import urlparse
try: try:
from eventlet import sleep from eventlet import sleep
@@ -26,22 +26,27 @@ if not hasattr(urlparse, 'parse_qsl'):
import cgi import cgi
urlparse.parse_qsl = cgi.parse_qsl urlparse.parse_qsl = cgi.parse_qsl
import requests
from cinderclient import exceptions from cinderclient import exceptions
from cinderclient import service_catalog from cinderclient import service_catalog
from cinderclient import utils from cinderclient import utils
class HTTPClient(httplib2.Http): class HTTPClient(object):
USER_AGENT = 'python-cinderclient' USER_AGENT = 'python-cinderclient'
requests_config = {
'danger_mode': False,
}
def __init__(self, user, password, projectid, auth_url, insecure=False, def __init__(self, user, password, projectid, auth_url, insecure=False,
timeout=None, tenant_id=None, proxy_tenant_id=None, timeout=None, tenant_id=None, proxy_tenant_id=None,
proxy_token=None, region_name=None, proxy_token=None, region_name=None,
endpoint_type='publicURL', service_type=None, endpoint_type='publicURL', service_type=None,
service_name=None, volume_service_name=None, retries=None, service_name=None, volume_service_name=None, retries=None,
http_log_debug=False): http_log_debug=False, cacert=None):
super(HTTPClient, self).__init__(timeout=timeout)
self.user = user self.user = user
self.password = password self.password = password
self.projectid = projectid self.projectid = projectid
@@ -61,15 +66,20 @@ class HTTPClient(httplib2.Http):
self.proxy_token = proxy_token self.proxy_token = proxy_token
self.proxy_tenant_id = proxy_tenant_id self.proxy_tenant_id = proxy_tenant_id
# httplib2 overrides if insecure:
self.force_exception_to_status_code = True self.verify_cert = False
self.disable_ssl_certificate_validation = insecure else:
if cacert:
self.verify_cert = cacert
else:
self.verify_cert = True
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
if self.http_log_debug: if self.http_log_debug:
ch = logging.StreamHandler() ch = logging.StreamHandler()
self._logger.setLevel(logging.DEBUG) self._logger.setLevel(logging.DEBUG)
self._logger.addHandler(ch) self._logger.addHandler(ch)
self.requests_config['verbose'] = sys.stderr
def http_log_req(self, args, kwargs): def http_log_req(self, args, kwargs):
if not self.http_log_debug: if not self.http_log_debug:
@@ -90,32 +100,43 @@ class HTTPClient(httplib2.Http):
string_parts.append(" -d '%s'" % (kwargs['body'])) string_parts.append(" -d '%s'" % (kwargs['body']))
self._logger.debug("\nREQ: %s\n" % "".join(string_parts)) self._logger.debug("\nREQ: %s\n" % "".join(string_parts))
def http_log_resp(self, resp, body): def http_log_resp(self, resp):
if not self.http_log_debug: if not self.http_log_debug:
return return
self._logger.debug("RESP: %s\nRESP BODY: %s\n", resp, body) self._logger.debug(
"RESP: [%s] %s\nRESP BODY: %s\n",
resp.status_code,
resp.headers,
resp.text)
def request(self, *args, **kwargs): def request(self, url, method, **kwargs):
kwargs.setdefault('headers', kwargs.get('headers', {})) kwargs.setdefault('headers', kwargs.get('headers', {}))
kwargs['headers']['User-Agent'] = self.USER_AGENT kwargs['headers']['User-Agent'] = self.USER_AGENT
kwargs['headers']['Accept'] = 'application/json' kwargs['headers']['Accept'] = 'application/json'
if 'body' in kwargs: if 'body' in kwargs:
kwargs['headers']['Content-Type'] = 'application/json' kwargs['headers']['Content-Type'] = 'application/json'
kwargs['body'] = json.dumps(kwargs['body']) kwargs['data'] = json.dumps(kwargs['body'])
del kwargs['body']
self.http_log_req(args, kwargs) self.http_log_req((url, method,), kwargs)
resp, body = super(HTTPClient, self).request(*args, **kwargs) resp = requests.request(
self.http_log_resp(resp, body) method,
url,
verify=self.verify_cert,
config=self.requests_config,
**kwargs)
self.http_log_resp(resp)
if body: if resp.text:
try: try:
body = json.loads(body) body = json.loads(resp.text)
except ValueError: except ValueError:
pass pass
body = None
else: else:
body = None body = None
if resp.status >= 400: if resp.status_code >= 400:
raise exceptions.from_response(resp, body) raise exceptions.from_response(resp, body)
return resp, body return resp, body
@@ -138,10 +159,6 @@ class HTTPClient(httplib2.Http):
except exceptions.BadRequest as e: except exceptions.BadRequest as e:
if attempts > self.retries: if attempts > self.retries:
raise raise
# Socket errors show up here (400) when
# force_exception_to_status_code = True
if e.message != 'n/a':
raise
except exceptions.Unauthorized: except exceptions.Unauthorized:
if auth_attempts > 0: if auth_attempts > 0:
raise raise
@@ -158,6 +175,10 @@ class HTTPClient(httplib2.Http):
pass pass
else: else:
raise raise
except requests.exceptions.ConnectionError as e:
# Catch a connection refused from requests.request
self._logger.debug("Connection refused: %s" % e)
raise
self._logger.debug( self._logger.debug(
"Failed attempt(%s of %s), retrying in %s seconds" % "Failed attempt(%s of %s), retrying in %s seconds" %
(attempts, self.retries, backoff)) (attempts, self.retries, backoff))
@@ -181,7 +202,7 @@ class HTTPClient(httplib2.Http):
We may get redirected to another site, fail or actually get We may get redirected to another site, fail or actually get
back a service catalog with a token and our endpoints.""" back a service catalog with a token and our endpoints."""
if resp.status == 200: # content must always present if resp.status_code == 200: # content must always present
try: try:
self.auth_url = url self.auth_url = url
self.service_catalog = \ self.service_catalog = \
@@ -209,7 +230,7 @@ class HTTPClient(httplib2.Http):
print "Could not find any suitable endpoint. Correct region?" print "Could not find any suitable endpoint. Correct region?"
raise raise
elif resp.status == 305: elif resp.status_code == 305:
return resp['location'] return resp['location']
else: else:
raise exceptions.from_response(resp, body) raise exceptions.from_response(resp, body)
@@ -292,16 +313,16 @@ class HTTPClient(httplib2.Http):
headers['X-Auth-Project-Id'] = self.projectid headers['X-Auth-Project-Id'] = self.projectid
resp, body = self.request(url, 'GET', headers=headers) resp, body = self.request(url, 'GET', headers=headers)
if resp.status in (200, 204): # in some cases we get No Content if resp.status_code in (200, 204): # in some cases we get No Content
try: try:
mgmt_header = 'x-server-management-url' mgmt_header = 'x-server-management-url'
self.management_url = resp[mgmt_header].rstrip('/') self.management_url = resp.headers[mgmt_header].rstrip('/')
self.auth_token = resp['x-auth-token'] self.auth_token = resp.headers['x-auth-token']
self.auth_url = url self.auth_url = url
except KeyError: except (KeyError, TypeError):
raise exceptions.AuthorizationFailure() raise exceptions.AuthorizationFailure()
elif resp.status == 305: elif resp.status_code == 305:
return resp['location'] return resp.headers['location']
else: else:
raise exceptions.from_response(resp, body) raise exceptions.from_response(resp, body)
@@ -333,13 +354,11 @@ class HTTPClient(httplib2.Http):
token_url = url + "/tokens" token_url = url + "/tokens"
# Make sure we follow redirects when trying to reach Keystone # Make sure we follow redirects when trying to reach Keystone
tmp_follow_all_redirects = self.follow_all_redirects resp, body = self.request(
self.follow_all_redirects = True token_url,
"POST",
try: body=body,
resp, body = self.request(token_url, "POST", body=body) allow_redirects=True)
finally:
self.follow_all_redirects = tmp_follow_all_redirects
return self._extract_service_catalog(url, resp, body) return self._extract_service_catalog(url, resp, body)

View File

@@ -124,16 +124,19 @@ _code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
def from_response(response, body): def from_response(response, body):
""" """
Return an instance of an ClientException or subclass Return an instance of an ClientException or subclass
based on an httplib2 response. based on an requests response.
Usage:: Usage::
resp, body = http.request(...) resp, body = requests.request(...)
if resp.status != 200: if resp.status_code != 200:
raise exception_from_response(resp, body) raise exception_from_response(resp, rest.text)
""" """
cls = _code_map.get(response.status, ClientException) cls = _code_map.get(response.status_code, ClientException)
request_id = response.get('x-compute-request-id') if response.headers:
request_id = response.headers.get('x-compute-request-id')
else:
request_id = None
if body: if body:
message = "n/a" message = "n/a"
details = "n/a" details = "n/a"
@@ -141,7 +144,7 @@ def from_response(response, body):
error = body[body.keys()[0]] error = body[body.keys()[0]]
message = error.get('message', None) message = error.get('message', None)
details = error.get('details', None) details = error.get('details', None)
return cls(code=response.status, message=message, details=details, return cls(code=response.status_code, message=message, details=details,
request_id=request_id) request_id=request_id)
else: else:
return cls(code=response.status, request_id=request_id) return cls(code=response.status_code, request_id=request_id)

View File

@@ -20,7 +20,6 @@ Command-line interface to the OpenStack Volume API.
import argparse import argparse
import glob import glob
import httplib2
import imp import imp
import itertools import itertools
import os import os
@@ -164,6 +163,13 @@ class OpenStackCinderShell(object):
parser.add_argument('--os_volume_api_version', parser.add_argument('--os_volume_api_version',
help=argparse.SUPPRESS) help=argparse.SUPPRESS)
parser.add_argument('--os-cacert',
metavar='<ca-certificate>',
default=utils.env('OS_CACERT', default=None),
help='Specify a CA bundle file to use in '
'verifying a TLS (https) server certificate. '
'Defaults to env[OS_CACERT]')
parser.add_argument('--insecure', parser.add_argument('--insecure',
default=utils.env('CINDERCLIENT_INSECURE', default=utils.env('CINDERCLIENT_INSECURE',
default=False), default=False),
@@ -308,8 +314,6 @@ class OpenStackCinderShell(object):
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
logger.addHandler(streamhandler) logger.addHandler(streamhandler)
httplib2.debuglevel = 1
def main(self, argv): def main(self, argv):
# Parse args once to find version and debug settings # Parse args once to find version and debug settings
parser = self.get_base_parser() parser = self.get_base_parser()
@@ -343,14 +347,14 @@ class OpenStackCinderShell(object):
(os_username, os_password, os_tenant_name, os_auth_url, (os_username, os_password, os_tenant_name, os_auth_url,
os_region_name, endpoint_type, insecure, os_region_name, endpoint_type, insecure,
service_type, service_name, volume_service_name, service_type, service_name, volume_service_name,
username, apikey, projectid, url, region_name) = ( username, apikey, projectid, url, region_name, cacert) = (
args.os_username, args.os_password, args.os_username, args.os_password,
args.os_tenant_name, args.os_auth_url, args.os_tenant_name, args.os_auth_url,
args.os_region_name, args.endpoint_type, args.os_region_name, args.endpoint_type,
args.insecure, args.service_type, args.service_name, args.insecure, args.service_type, args.service_name,
args.volume_service_name, args.username, args.volume_service_name, args.username,
args.apikey, args.projectid, args.apikey, args.projectid,
args.url, args.region_name) args.url, args.region_name, args.os_cacert)
if not endpoint_type: if not endpoint_type:
endpoint_type = DEFAULT_CINDER_ENDPOINT_TYPE endpoint_type = DEFAULT_CINDER_ENDPOINT_TYPE
@@ -417,7 +421,8 @@ class OpenStackCinderShell(object):
service_name=service_name, service_name=service_name,
volume_service_name=volume_service_name, volume_service_name=volume_service_name,
retries=options.retries, retries=options.retries,
http_log_debug=args.debug) http_log_debug=args.debug,
cacert=cacert)
try: try:
if not utils.isunauthenticated(args.func): if not utils.isunauthenticated(args.func):

View File

@@ -28,7 +28,8 @@ class Client(object):
endpoint_type='publicURL', extensions=None, endpoint_type='publicURL', extensions=None,
service_type='volume', service_name=None, service_type='volume', service_name=None,
volume_service_name=None, retries=None, volume_service_name=None, retries=None,
http_log_debug=False): http_log_debug=False,
cacert=None):
# FIXME(comstud): Rename the api_key argument above when we # FIXME(comstud): Rename the api_key argument above when we
# know it's not being used as keyword argument # know it's not being used as keyword argument
password = api_key password = api_key
@@ -64,7 +65,8 @@ class Client(object):
service_name=service_name, service_name=service_name,
volume_service_name=volume_service_name, volume_service_name=volume_service_name,
retries=retries, retries=retries,
http_log_debug=http_log_debug) http_log_debug=http_log_debug,
cacert=cacert)
def authenticate(self): def authenticate(self):
""" """

View File

@@ -1,14 +1,35 @@
import httplib2
import mock import mock
import requests
from cinderclient import client from cinderclient import client
from cinderclient import exceptions from cinderclient import exceptions
from tests import utils from tests import utils
fake_response = httplib2.Response({"status": 200}) fake_response = utils.TestResponse({
fake_body = '{"hi": "there"}' "status_code": 200,
mock_request = mock.Mock(return_value=(fake_response, fake_body)) "text": '{"hi": "there"}',
})
mock_request = mock.Mock(return_value=(fake_response))
bad_400_response = utils.TestResponse({
"status_code": 400,
"text": '{"error": {"message": "n/a", "details": "Terrible!"}}',
})
bad_400_request = mock.Mock(return_value=(bad_400_response))
bad_401_response = utils.TestResponse({
"status_code": 401,
"text": '{"error": {"message": "FAILED!", "details": "DETAILS!"}}',
})
bad_401_request = mock.Mock(return_value=(bad_401_response))
bad_500_response = utils.TestResponse({
"status_code": 500,
"text": '{"error": {"message": "FAILED!", "details": "DETAILS!"}}',
})
bad_500_request = mock.Mock(return_value=(bad_500_response))
def get_client(retries=0): def get_client(retries=0):
@@ -29,7 +50,7 @@ class ClientTest(utils.TestCase):
def test_get(self): def test_get(self):
cl = get_authed_client() cl = get_authed_client()
@mock.patch.object(httplib2.Http, "request", mock_request) @mock.patch.object(requests, "request", mock_request)
@mock.patch('time.time', mock.Mock(return_value=1234)) @mock.patch('time.time', mock.Mock(return_value=1234))
def test_get_call(): def test_get_call():
resp, body = cl.get("/hi") resp, body = cl.get("/hi")
@@ -37,8 +58,11 @@ class ClientTest(utils.TestCase):
"X-Auth-Project-Id": "project_id", "X-Auth-Project-Id": "project_id",
"User-Agent": cl.USER_AGENT, "User-Agent": cl.USER_AGENT,
'Accept': 'application/json', } 'Accept': 'application/json', }
mock_request.assert_called_with("http://example.com/hi", mock_request.assert_called_with(
"GET", headers=headers) "GET",
"http://example.com/hi",
headers=headers,
**self.TEST_REQUEST_BASE)
# Automatic JSON parsing # Automatic JSON parsing
self.assertEqual(body, {"hi": "there"}) self.assertEqual(body, {"hi": "there"})
@@ -47,10 +71,7 @@ class ClientTest(utils.TestCase):
def test_get_reauth_0_retries(self): def test_get_reauth_0_retries(self):
cl = get_authed_client(retries=0) cl = get_authed_client(retries=0)
bad_response = httplib2.Response({"status": 401}) self.requests = [bad_401_request, mock_request]
bad_body = '{"error": {"message": "FAILED!", "details": "DETAILS!"}}'
bad_request = mock.Mock(return_value=(bad_response, bad_body))
self.requests = [bad_request, mock_request]
def request(*args, **kwargs): def request(*args, **kwargs):
next_request = self.requests.pop(0) next_request = self.requests.pop(0)
@@ -61,7 +82,7 @@ class ClientTest(utils.TestCase):
cl.auth_token = "token" cl.auth_token = "token"
@mock.patch.object(cl, 'authenticate', reauth) @mock.patch.object(cl, 'authenticate', reauth)
@mock.patch.object(httplib2.Http, "request", request) @mock.patch.object(requests, "request", request)
@mock.patch('time.time', mock.Mock(return_value=1234)) @mock.patch('time.time', mock.Mock(return_value=1234))
def test_get_call(): def test_get_call():
resp, body = cl.get("/hi") resp, body = cl.get("/hi")
@@ -72,16 +93,13 @@ class ClientTest(utils.TestCase):
def test_get_retry_500(self): def test_get_retry_500(self):
cl = get_authed_client(retries=1) cl = get_authed_client(retries=1)
bad_response = httplib2.Response({"status": 500}) self.requests = [bad_500_request, mock_request]
bad_body = '{"error": {"message": "FAILED!", "details": "DETAILS!"}}'
bad_request = mock.Mock(return_value=(bad_response, bad_body))
self.requests = [bad_request, mock_request]
def request(*args, **kwargs): def request(*args, **kwargs):
next_request = self.requests.pop(0) next_request = self.requests.pop(0)
return next_request(*args, **kwargs) return next_request(*args, **kwargs)
@mock.patch.object(httplib2.Http, "request", request) @mock.patch.object(requests, "request", request)
@mock.patch('time.time', mock.Mock(return_value=1234)) @mock.patch('time.time', mock.Mock(return_value=1234))
def test_get_call(): def test_get_call():
resp, body = cl.get("/hi") resp, body = cl.get("/hi")
@@ -92,16 +110,13 @@ class ClientTest(utils.TestCase):
def test_retry_limit(self): def test_retry_limit(self):
cl = get_authed_client(retries=1) cl = get_authed_client(retries=1)
bad_response = httplib2.Response({"status": 500}) self.requests = [bad_500_request, bad_500_request, mock_request]
bad_body = '{"error": {"message": "FAILED!", "details": "DETAILS!"}}'
bad_request = mock.Mock(return_value=(bad_response, bad_body))
self.requests = [bad_request, bad_request, mock_request]
def request(*args, **kwargs): def request(*args, **kwargs):
next_request = self.requests.pop(0) next_request = self.requests.pop(0)
return next_request(*args, **kwargs) return next_request(*args, **kwargs)
@mock.patch.object(httplib2.Http, "request", request) @mock.patch.object(requests, "request", request)
@mock.patch('time.time', mock.Mock(return_value=1234)) @mock.patch('time.time', mock.Mock(return_value=1234))
def test_get_call(): def test_get_call():
resp, body = cl.get("/hi") resp, body = cl.get("/hi")
@@ -110,18 +125,15 @@ class ClientTest(utils.TestCase):
self.assertEqual(self.requests, [mock_request]) self.assertEqual(self.requests, [mock_request])
def test_get_no_retry_400(self): def test_get_no_retry_400(self):
cl = get_authed_client(retries=1) cl = get_authed_client(retries=0)
bad_response = httplib2.Response({"status": 400}) self.requests = [bad_400_request, mock_request]
bad_body = '{"error": {"message": "Bad!", "details": "Terrible!"}}'
bad_request = mock.Mock(return_value=(bad_response, bad_body))
self.requests = [bad_request, mock_request]
def request(*args, **kwargs): def request(*args, **kwargs):
next_request = self.requests.pop(0) next_request = self.requests.pop(0)
return next_request(*args, **kwargs) return next_request(*args, **kwargs)
@mock.patch.object(httplib2.Http, "request", request) @mock.patch.object(requests, "request", request)
@mock.patch('time.time', mock.Mock(return_value=1234)) @mock.patch('time.time', mock.Mock(return_value=1234))
def test_get_call(): def test_get_call():
resp, body = cl.get("/hi") resp, body = cl.get("/hi")
@@ -132,16 +144,13 @@ class ClientTest(utils.TestCase):
def test_get_retry_400_socket(self): def test_get_retry_400_socket(self):
cl = get_authed_client(retries=1) cl = get_authed_client(retries=1)
bad_response = httplib2.Response({"status": 400}) self.requests = [bad_400_request, mock_request]
bad_body = '{"error": {"message": "n/a", "details": "n/a"}}'
bad_request = mock.Mock(return_value=(bad_response, bad_body))
self.requests = [bad_request, mock_request]
def request(*args, **kwargs): def request(*args, **kwargs):
next_request = self.requests.pop(0) next_request = self.requests.pop(0)
return next_request(*args, **kwargs) return next_request(*args, **kwargs)
@mock.patch.object(httplib2.Http, "request", request) @mock.patch.object(requests, "request", request)
@mock.patch('time.time', mock.Mock(return_value=1234)) @mock.patch('time.time', mock.Mock(return_value=1234))
def test_get_call(): def test_get_call():
resp, body = cl.get("/hi") resp, body = cl.get("/hi")
@@ -152,7 +161,7 @@ class ClientTest(utils.TestCase):
def test_post(self): def test_post(self):
cl = get_authed_client() cl = get_authed_client()
@mock.patch.object(httplib2.Http, "request", mock_request) @mock.patch.object(requests, "request", mock_request)
def test_post_call(): def test_post_call():
cl.post("/hi", body=[1, 2, 3]) cl.post("/hi", body=[1, 2, 3])
headers = { headers = {
@@ -162,8 +171,12 @@ class ClientTest(utils.TestCase):
'Accept': 'application/json', 'Accept': 'application/json',
"User-Agent": cl.USER_AGENT "User-Agent": cl.USER_AGENT
} }
mock_request.assert_called_with("http://example.com/hi", "POST", mock_request.assert_called_with(
headers=headers, body='[1, 2, 3]') "POST",
"http://example.com/hi",
headers=headers,
data='[1, 2, 3]',
**self.TEST_REQUEST_BASE)
test_post_call() test_post_call()
@@ -171,7 +184,7 @@ class ClientTest(utils.TestCase):
cl = get_client() cl = get_client()
# response must not have x-server-management-url header # response must not have x-server-management-url header
@mock.patch.object(httplib2.Http, "request", mock_request) @mock.patch.object(requests, "request", mock_request)
def test_auth_call(): def test_auth_call():
self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate) self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate)

View File

@@ -1,6 +1,5 @@
import cStringIO import cStringIO
import os import os
import httplib2
import sys import sys
from cinderclient import exceptions from cinderclient import exceptions
@@ -44,11 +43,6 @@ class ShellTest(utils.TestCase):
def test_help_unknown_command(self): def test_help_unknown_command(self):
self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo') self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo')
def test_debug(self):
httplib2.debuglevel = 0
self.shell('--debug help')
assert httplib2.debuglevel == 1
def test_help(self): def test_help(self):
required = [ required = [
'^usage: ', '^usage: ',

View File

@@ -1,5 +1,33 @@
import unittest2 import unittest2
import requests
class TestCase(unittest2.TestCase): class TestCase(unittest2.TestCase):
pass TEST_REQUEST_BASE = {
'config': {'danger_mode': False},
'verify': True,
}
class TestResponse(requests.Response):
""" Class used to wrap requests.Response and provide some
convenience to initialize with a dict """
def __init__(self, data):
self._text = None
super(TestResponse, self)
if isinstance(data, dict):
self.status_code = data.get('status_code', None)
self.headers = data.get('headers', None)
# Fake the text attribute to streamline Response creation
self._text = data.get('text', None)
else:
self.status_code = data
def __eq__(self, other):
return self.__dict__ == other.__dict__
@property
def text(self):
return self._text

View File

@@ -13,12 +13,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import httplib2
import urlparse import urlparse
from cinderclient import client as base_client from cinderclient import client as base_client
from cinderclient.v1 import client from cinderclient.v1 import client
from tests import fakes from tests import fakes
import tests.utils as utils
def _stub_volume(**kwargs): def _stub_volume(**kwargs):
@@ -97,28 +97,35 @@ class FakeHTTPClient(base_client.HTTPClient):
# Note the call # Note the call
self.callstack.append((method, url, kwargs.get('body', None))) self.callstack.append((method, url, kwargs.get('body', None)))
status, body = getattr(self, callback)(**kwargs) status, headers, body = getattr(self, callback)(**kwargs)
r = utils.TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})
return r, body
if hasattr(status, 'items'): if hasattr(status, 'items'):
return httplib2.Response(status), body return utils.TestResponse(status), body
else: else:
return httplib2.Response({"status": status}), body return utils.TestResponse({"status": status}), body
# #
# Snapshots # Snapshots
# #
def get_snapshots_detail(self, **kw): def get_snapshots_detail(self, **kw):
return (200, {'snapshots': [ return (200, {}, {'snapshots': [
_stub_snapshot(), _stub_snapshot(),
]}) ]})
def get_snapshots_1234(self, **kw): def get_snapshots_1234(self, **kw):
return (200, {'snapshot': _stub_snapshot(id='1234')}) return (200, {}, {'snapshot': _stub_snapshot(id='1234')})
def put_snapshots_1234(self, **kw): def put_snapshots_1234(self, **kw):
snapshot = _stub_snapshot(id='1234') snapshot = _stub_snapshot(id='1234')
snapshot.update(kw['body']['snapshot']) snapshot.update(kw['body']['snapshot'])
return (200, {'snapshot': snapshot}) return (200, {}, {'snapshot': snapshot})
# #
# Volumes # Volumes
@@ -127,10 +134,10 @@ class FakeHTTPClient(base_client.HTTPClient):
def put_volumes_1234(self, **kw): def put_volumes_1234(self, **kw):
volume = _stub_volume(id='1234') volume = _stub_volume(id='1234')
volume.update(kw['body']['volume']) volume.update(kw['body']['volume'])
return (200, {'volume': volume}) return (200, {}, {'volume': volume})
def get_volumes(self, **kw): def get_volumes(self, **kw):
return (200, {"volumes": [ return (200, {}, {"volumes": [
{'id': 1234, 'name': 'sample-volume'}, {'id': 1234, 'name': 'sample-volume'},
{'id': 5678, 'name': 'sample-volume2'} {'id': 5678, 'name': 'sample-volume2'}
]}) ]})
@@ -138,15 +145,15 @@ class FakeHTTPClient(base_client.HTTPClient):
# TODO(jdg): This will need to change # TODO(jdg): This will need to change
# at the very least it's not complete # at the very least it's not complete
def get_volumes_detail(self, **kw): def get_volumes_detail(self, **kw):
return (200, {"volumes": [ return (200, {}, {"volumes": [
{'id': 1234, {'id': 1234,
'name': 'sample-volume', 'name': 'sample-volume',
'attachments': [{'server_id': 1234}]}, 'attachments': [{'server_id': 1234}]},
]}) ]})
def get_volumes_1234(self, **kw): def get_volumes_1234(self, **kw):
r = {'volume': self.get_volumes_detail()[1]['volumes'][0]} r = {'volume': self.get_volumes_detail()[2]['volumes'][0]}
return (200, r) return (200, {}, r)
def post_volumes_1234_action(self, body, **kw): def post_volumes_1234_action(self, body, **kw):
_body = None _body = None
@@ -163,7 +170,7 @@ class FakeHTTPClient(base_client.HTTPClient):
assert body[action] is None assert body[action] is None
elif action == 'os-initialize_connection': elif action == 'os-initialize_connection':
assert body[action].keys() == ['connector'] assert body[action].keys() == ['connector']
return (202, {'connection_info': 'foos'}) return (202, {}, {'connection_info': 'foos'})
elif action == 'os-terminate_connection': elif action == 'os-terminate_connection':
assert body[action].keys() == ['connector'] assert body[action].keys() == ['connector']
elif action == 'os-begin_detaching': elif action == 'os-begin_detaching':
@@ -172,27 +179,27 @@ class FakeHTTPClient(base_client.HTTPClient):
assert body[action] is None assert body[action] is None
else: else:
raise AssertionError("Unexpected server action: %s" % action) raise AssertionError("Unexpected server action: %s" % action)
return (resp, _body) return (resp, {}, _body)
def post_volumes(self, **kw): def post_volumes(self, **kw):
return (202, {'volume': {}}) return (202, {}, {'volume': {}})
def delete_volumes_1234(self, **kw): def delete_volumes_1234(self, **kw):
return (202, None) return (202, {}, None)
# #
# Quotas # Quotas
# #
def get_os_quota_sets_test(self, **kw): def get_os_quota_sets_test(self, **kw):
return (200, {'quota_set': { return (200, {}, {'quota_set': {
'tenant_id': 'test', 'tenant_id': 'test',
'metadata_items': [], 'metadata_items': [],
'volumes': 1, 'volumes': 1,
'gigabytes': 1}}) 'gigabytes': 1}})
def get_os_quota_sets_test_defaults(self): def get_os_quota_sets_test_defaults(self):
return (200, {'quota_set': { return (200, {}, {'quota_set': {
'tenant_id': 'test', 'tenant_id': 'test',
'metadata_items': [], 'metadata_items': [],
'volumes': 1, 'volumes': 1,
@@ -202,7 +209,7 @@ class FakeHTTPClient(base_client.HTTPClient):
assert body.keys() == ['quota_set'] assert body.keys() == ['quota_set']
fakes.assert_has_keys(body['quota_set'], fakes.assert_has_keys(body['quota_set'],
required=['tenant_id']) required=['tenant_id'])
return (200, {'quota_set': { return (200, {}, {'quota_set': {
'tenant_id': 'test', 'tenant_id': 'test',
'metadata_items': [], 'metadata_items': [],
'volumes': 2, 'volumes': 2,
@@ -213,7 +220,7 @@ class FakeHTTPClient(base_client.HTTPClient):
# #
def get_os_quota_class_sets_test(self, **kw): def get_os_quota_class_sets_test(self, **kw):
return (200, {'quota_class_set': { return (200, {}, {'quota_class_set': {
'class_name': 'test', 'class_name': 'test',
'metadata_items': [], 'metadata_items': [],
'volumes': 1, 'volumes': 1,
@@ -223,7 +230,7 @@ class FakeHTTPClient(base_client.HTTPClient):
assert body.keys() == ['quota_class_set'] assert body.keys() == ['quota_class_set']
fakes.assert_has_keys(body['quota_class_set'], fakes.assert_has_keys(body['quota_class_set'],
required=['class_name']) required=['class_name'])
return (200, {'quota_class_set': { return (200, {}, {'quota_class_set': {
'class_name': 'test', 'class_name': 'test',
'metadata_items': [], 'metadata_items': [],
'volumes': 2, 'volumes': 2,
@@ -233,7 +240,7 @@ class FakeHTTPClient(base_client.HTTPClient):
# VolumeTypes # VolumeTypes
# #
def get_types(self, **kw): def get_types(self, **kw):
return (200, { return (200, {}, {
'volume_types': [{'id': 1, 'volume_types': [{'id': 1,
'name': 'test-type-1', 'name': 'test-type-1',
'extra_specs':{}}, 'extra_specs':{}},
@@ -242,21 +249,21 @@ class FakeHTTPClient(base_client.HTTPClient):
'extra_specs':{}}]}) 'extra_specs':{}}]})
def get_types_1(self, **kw): def get_types_1(self, **kw):
return (200, {'volume_type': {'id': 1, return (200, {}, {'volume_type': {'id': 1,
'name': 'test-type-1', 'name': 'test-type-1',
'extra_specs': {}}}) 'extra_specs': {}}})
def post_types(self, body, **kw): def post_types(self, body, **kw):
return (202, {'volume_type': {'id': 3, return (202, {}, {'volume_type': {'id': 3,
'name': 'test-type-3', 'name': 'test-type-3',
'extra_specs': {}}}) 'extra_specs': {}}})
def post_types_1_extra_specs(self, body, **kw): def post_types_1_extra_specs(self, body, **kw):
assert body.keys() == ['extra_specs'] assert body.keys() == ['extra_specs']
return (200, {'extra_specs': {'k': 'v'}}) return (200, {}, {'extra_specs': {'k': 'v'}})
def delete_types_1_extra_specs_k(self, **kw): def delete_types_1_extra_specs_k(self, **kw):
return(204, None) return(204, {}, None)
def delete_types_1(self, **kw): def delete_types_1(self, **kw):
return (202, None) return (202, {}, None)

View File

@@ -1,20 +1,13 @@
import httplib2
import json import json
import mock import mock
import requests
from cinderclient.v1 import client from cinderclient.v1 import client
from cinderclient import exceptions from cinderclient import exceptions
from tests import utils from tests import utils
def to_http_response(resp_dict):
"""Converts dict of response attributes to httplib response."""
resp = httplib2.Response(resp_dict)
for k, v in resp_dict['headers'].items():
resp[k] = v
return resp
class AuthenticateAgainstKeystoneTests(utils.TestCase): class AuthenticateAgainstKeystoneTests(utils.TestCase):
def test_authenticate_success(self): def test_authenticate_success(self):
cs = client.Client("username", "password", "project_id", cs = client.Client("username", "password", "project_id",
@@ -40,14 +33,14 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
], ],
}, },
} }
auth_response = httplib2.Response({ auth_response = utils.TestResponse({
"status": 200, "status_code": 200,
"body": json.dumps(resp), }) "text": json.dumps(resp),
})
mock_request = mock.Mock(return_value=(auth_response, mock_request = mock.Mock(return_value=(auth_response))
json.dumps(resp)))
@mock.patch.object(httplib2.Http, "request", mock_request) @mock.patch.object(requests, "request", mock_request)
def test_auth_call(): def test_auth_call():
cs.client.authenticate() cs.client.authenticate()
headers = { headers = {
@@ -66,9 +59,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
} }
token_url = cs.client.auth_url + "/tokens" token_url = cs.client.auth_url + "/tokens"
mock_request.assert_called_with(token_url, "POST", mock_request.assert_called_with(
"POST",
token_url,
headers=headers, headers=headers,
body=json.dumps(body)) data=json.dumps(body),
allow_redirects=True,
**self.TEST_REQUEST_BASE)
endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
public_url = endpoints[0]["publicURL"].rstrip('/') public_url = endpoints[0]["publicURL"].rstrip('/')
@@ -108,14 +105,14 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
], ],
}, },
} }
auth_response = httplib2.Response({ auth_response = utils.TestResponse({
"status": 200, "status_code": 200,
"body": json.dumps(resp), }) "text": json.dumps(resp),
})
mock_request = mock.Mock(return_value=(auth_response, mock_request = mock.Mock(return_value=(auth_response))
json.dumps(resp)))
@mock.patch.object(httplib2.Http, "request", mock_request) @mock.patch.object(requests, "request", mock_request)
def test_auth_call(): def test_auth_call():
cs.client.authenticate() cs.client.authenticate()
headers = { headers = {
@@ -134,9 +131,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
} }
token_url = cs.client.auth_url + "/tokens" token_url = cs.client.auth_url + "/tokens"
mock_request.assert_called_with(token_url, "POST", mock_request.assert_called_with(
"POST",
token_url,
headers=headers, headers=headers,
body=json.dumps(body)) data=json.dumps(body),
allow_redirects=True,
**self.TEST_REQUEST_BASE)
endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
public_url = endpoints[0]["publicURL"].rstrip('/') public_url = endpoints[0]["publicURL"].rstrip('/')
@@ -152,14 +153,14 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
cs = client.Client("username", "password", "project_id", cs = client.Client("username", "password", "project_id",
"auth_url/v2.0") "auth_url/v2.0")
resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}}
auth_response = httplib2.Response({ auth_response = utils.TestResponse({
"status": 401, "status_code": 401,
"body": json.dumps(resp), }) "text": json.dumps(resp),
})
mock_request = mock.Mock(return_value=(auth_response, mock_request = mock.Mock(return_value=(auth_response))
json.dumps(resp)))
@mock.patch.object(httplib2.Http, "request", mock_request) @mock.patch.object(requests, "request", mock_request)
def test_auth_call(): def test_auth_call():
self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
@@ -192,29 +193,28 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
correct_response = json.dumps(dict_correct_response) correct_response = json.dumps(dict_correct_response)
dict_responses = [ dict_responses = [
{"headers": {'location':'http://127.0.0.1:5001'}, {"headers": {'location':'http://127.0.0.1:5001'},
"status": 305, "status_code": 305,
"body": "Use proxy"}, "text": "Use proxy"},
# Configured on admin port, cinder redirects to v2.0 port. # Configured on admin port, cinder redirects to v2.0 port.
# When trying to connect on it, keystone auth succeed by v1.0 # When trying to connect on it, keystone auth succeed by v1.0
# protocol (through headers) but tokens are being returned in # protocol (through headers) but tokens are being returned in
# body (looks like keystone bug). Leaved for compatibility. # body (looks like keystone bug). Leaved for compatibility.
{"headers": {}, {"headers": {},
"status": 200, "status_code": 200,
"body": correct_response}, "text": correct_response},
{"headers": {}, {"headers": {},
"status": 200, "status_code": 200,
"body": correct_response} "text": correct_response}
] ]
responses = [(to_http_response(resp), resp['body']) responses = [(utils.TestResponse(resp)) for resp in dict_responses]
for resp in dict_responses]
def side_effect(*args, **kwargs): def side_effect(*args, **kwargs):
return responses.pop(0) return responses.pop(0)
mock_request = mock.Mock(side_effect=side_effect) mock_request = mock.Mock(side_effect=side_effect)
@mock.patch.object(httplib2.Http, "request", mock_request) @mock.patch.object(requests, "request", mock_request)
def test_auth_call(): def test_auth_call():
cs.client.authenticate() cs.client.authenticate()
headers = { headers = {
@@ -233,9 +233,13 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
} }
token_url = cs.client.auth_url + "/tokens" token_url = cs.client.auth_url + "/tokens"
mock_request.assert_called_with(token_url, "POST", mock_request.assert_called_with(
"POST",
token_url,
headers=headers, headers=headers,
body=json.dumps(body)) data=json.dumps(body),
allow_redirects=True,
**self.TEST_REQUEST_BASE)
resp = dict_correct_response resp = dict_correct_response
endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
@@ -282,16 +286,14 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
], ],
}, },
} }
auth_response = httplib2.Response( auth_response = utils.TestResponse({
{ "status_code": 200,
"status": 200, "text": json.dumps(resp),
"body": json.dumps(resp),
}) })
mock_request = mock.Mock(return_value=(auth_response, mock_request = mock.Mock(return_value=(auth_response))
json.dumps(resp)))
@mock.patch.object(httplib2.Http, "request", mock_request) @mock.patch.object(requests, "request", mock_request)
def test_auth_call(): def test_auth_call():
self.assertRaises(exceptions.AmbiguousEndpoints, self.assertRaises(exceptions.AmbiguousEndpoints,
cs.client.authenticate) cs.client.authenticate)
@@ -302,15 +304,17 @@ class AuthenticateAgainstKeystoneTests(utils.TestCase):
class AuthenticationTests(utils.TestCase): class AuthenticationTests(utils.TestCase):
def test_authenticate_success(self): def test_authenticate_success(self):
cs = client.Client("username", "password", "project_id", "auth_url") cs = client.Client("username", "password", "project_id", "auth_url")
management_url = 'https://servers.api.rackspacecloud.com/v1.1/443470' management_url = 'https://localhost/v1.1/443470'
auth_response = httplib2.Response({ auth_response = utils.TestResponse({
'status': 204, 'status_code': 204,
'headers': {
'x-server-management-url': management_url, 'x-server-management-url': management_url,
'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1', 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
},
}) })
mock_request = mock.Mock(return_value=(auth_response, None)) mock_request = mock.Mock(return_value=(auth_response))
@mock.patch.object(httplib2.Http, "request", mock_request) @mock.patch.object(requests, "request", mock_request)
def test_auth_call(): def test_auth_call():
cs.client.authenticate() cs.client.authenticate()
headers = { headers = {
@@ -320,21 +324,25 @@ class AuthenticationTests(utils.TestCase):
'X-Auth-Project-Id': 'project_id', 'X-Auth-Project-Id': 'project_id',
'User-Agent': cs.client.USER_AGENT 'User-Agent': cs.client.USER_AGENT
} }
mock_request.assert_called_with(cs.client.auth_url, 'GET', mock_request.assert_called_with(
headers=headers) "GET",
cs.client.auth_url,
headers=headers,
**self.TEST_REQUEST_BASE)
self.assertEqual(cs.client.management_url, self.assertEqual(cs.client.management_url,
auth_response['x-server-management-url']) auth_response.headers['x-server-management-url'])
self.assertEqual(cs.client.auth_token, self.assertEqual(cs.client.auth_token,
auth_response['x-auth-token']) auth_response.headers['x-auth-token'])
test_auth_call() test_auth_call()
def test_authenticate_failure(self): def test_authenticate_failure(self):
cs = client.Client("username", "password", "project_id", "auth_url") cs = client.Client("username", "password", "project_id", "auth_url")
auth_response = httplib2.Response({'status': 401}) auth_response = utils.TestResponse({"status_code": 401})
mock_request = mock.Mock(return_value=(auth_response, None)) mock_request = mock.Mock(return_value=(auth_response))
@mock.patch.object(httplib2.Http, "request", mock_request) @mock.patch.object(requests, "request", mock_request)
def test_auth_call(): def test_auth_call():
self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)

View File

@@ -1,4 +1,4 @@
argparse argparse
httplib2
prettytable prettytable
requests<1.0
simplejson simplejson