Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring LibcloudConnection and Connection classes from feedback #976

Merged
merged 8 commits into from Jan 13, 2017
9 changes: 6 additions & 3 deletions libcloud/common/base.py
Expand Up @@ -674,9 +674,12 @@ def request(self, action, params=None, data=None, headers=None,
return response

def morph_action_hook(self, action):
if not action.startswith("/"):
action = "/" + action
return self.request_path + action
url = urlparse.urljoin(self.request_path.lstrip('/').rstrip('/') +
'/', action.lstrip('/'))
if not url.startswith('/'):
return '/' + url
else:
return url

def add_default_params(self, params):
"""
Expand Down
2 changes: 1 addition & 1 deletion libcloud/compute/drivers/profitbricks.py
Expand Up @@ -127,7 +127,7 @@ def request(self, action, params=None, data=None, headers=None,
host and protocol components.
'''
if not with_full_url or with_full_url is False:
action = self.api_prefix + action
action = self.api_prefix + action.lstrip('/')
else:
action = action.replace(
'https://{host}'.format(host=self.host),
Expand Down
65 changes: 13 additions & 52 deletions libcloud/httplib_ssl.py
Expand Up @@ -19,7 +19,6 @@
"""

import os
import socket
import warnings
import requests

Expand All @@ -32,25 +31,9 @@
'LibcloudConnection'
]

HTTP_PROXY_ENV_VARIABLE_NAME = 'http_proxy'
ALLOW_REDIRECTS = 1

# Error message which is thrown when establishing SSL / TLS connection fails
UNSUPPORTED_TLS_VERSION_ERROR_MSG = """
Failed to establish SSL / TLS connection (%s). It is possible that the server \
doesn't support requested SSL / TLS version (%s).
For information on how to work around this issue, please see \
https://libcloud.readthedocs.org/en/latest/other/\
ssl-certificate-validation.html#changing-used-ssl-tls-version
""".strip()

# Maps ssl.PROTOCOL_* constant to the actual SSL / TLS version name
SSL_CONSTANT_TO_TLS_VERSION_MAP = {
0: 'SSL v2',
2: 'SSLv3, TLS v1.0, TLS v1.1, TLS v1.2',
3: 'TLS v1.0',
4: 'TLS v1.1',
5: 'TLS v1.2'
}
HTTP_PROXY_ENV_VARIABLE_NAME = 'http_proxy'


class LibcloudBaseConnection(object):
Expand Down Expand Up @@ -182,16 +165,24 @@ def __init__(self, host, port, secure=None, **kwargs):
self.set_http_proxy(proxy_url=proxy_url)
self.session.timeout = kwargs.get('timeout', 60)

@property
def verification(self):
"""
The option for SSL verification given to underlying requests
"""
return self.ca_cert if self.ca_cert is not None else self.verify

def request(self, method, url, body=None, headers=None, raw=False,
stream=False):
url = urlparse.urljoin(self.host, url)
self.response = self.session.request(
method=method.lower(),
url=''.join([self.host, url]),
url=url,
data=body,
headers=headers,
allow_redirects=1,
allow_redirects=ALLOW_REDIRECTS,
stream=stream,
verify=self.ca_cert if self.ca_cert is not None else self.verify
verify=self.verification
)

def prepared_request(self, method, url, body=None,
Expand Down Expand Up @@ -280,33 +271,3 @@ def reason(self):
def version(self):
# requests doesn't expose this
return '11'


def get_socket_error_exception(ssl_version, exc):
"""
Function which intercepts socket.error exceptions and re-throws an
exception with a more user-friendly message in case server doesn't support
requested SSL version.
"""
exc_msg = str(exc)

# Re-throw an exception with a more friendly error message
if 'connection reset by peer' in exc_msg.lower():
ssl_version_name = SSL_CONSTANT_TO_TLS_VERSION_MAP[ssl_version]
msg = (UNSUPPORTED_TLS_VERSION_ERROR_MSG %
(exc_msg, ssl_version_name))

# Note: In some cases arguments are (errno, message) and in
# other it's just (message,)
exc_args = getattr(exc, 'args', [])

if len(exc_args) == 2:
new_exc_args = [exc.args[0], msg]
else:
new_exc_args = [msg]

new_exc = socket.error(*new_exc_args)
new_exc.original_exc = exc
return new_exc
else:
return exc
10 changes: 2 additions & 8 deletions libcloud/storage/base.py
Expand Up @@ -650,20 +650,14 @@ def _hash_buffered_stream(self, stream, hasher, blocksize=65536):
return (hasher.hexdigest(), total_len)
if not hasattr(stream, '__exit__'):
for s in stream:
if isinstance(s, str):
hasher.update(s)
else:
hasher.update(s)
hasher.update(s)
total_len = total_len + len(s)
return (hasher.hexdigest(), total_len)
with stream:
buf = stream.read(blocksize)
while len(buf) > 0:
total_len = total_len + len(buf)
if isinstance(buf, str):
hasher.update(buf)
else:
hasher.update(buf)
hasher.update(buf)
buf = stream.read(blocksize)
return (hasher.hexdigest(), total_len)

Expand Down
2 changes: 1 addition & 1 deletion libcloud/test/compute/test_profitbricks.py
Expand Up @@ -4962,7 +4962,7 @@ def _cloudapi_v3_datacenters_dc_1_lans_10(

GET - fetch a list of snapshots
'''
def _cloudapi_v3__snapshots(
def _cloudapi_v3_snapshots(
self, method, url, body, headers
):
if method == 'GET':
Expand Down
51 changes: 51 additions & 0 deletions libcloud/test/test_connection.py
Expand Up @@ -21,6 +21,8 @@

from mock import Mock, patch

import requests_mock

from libcloud.test import unittest
from libcloud.common.base import Connection
from libcloud.httplib_ssl import LibcloudBaseConnection
Expand Down Expand Up @@ -109,6 +111,55 @@ def test_connection_to_unusual_port(self):
conn = LibcloudConnection(host='localhost', port=80)
self.assertEqual(conn.host, 'http://localhost')

def test_connection_url_merging(self):
"""
Test that the connection class will parse URLs correctly
"""
conn = Connection(url='http://test.com/')
conn.connect()
self.assertEqual(conn.connection.host, 'http://test.com')
with requests_mock.mock() as m:
m.get('http://test.com/test', text='data')
response = conn.request('/test')
self.assertEqual(response.body, 'data')

def test_morph_action_hook(self):
conn = Connection(url="http://test.com")

conn.request_path = ''
self.assertEqual(conn.morph_action_hook('/test'), '/test')
self.assertEqual(conn.morph_action_hook('test'), '/test')

conn.request_path = '/v1'
self.assertEqual(conn.morph_action_hook('/test'), '/v1/test')
self.assertEqual(conn.morph_action_hook('test'), '/v1/test')

conn.request_path = '/v1'
self.assertEqual(conn.morph_action_hook('/test'), '/v1/test')
self.assertEqual(conn.morph_action_hook('test'), '/v1/test')

conn.request_path = 'v1'
self.assertEqual(conn.morph_action_hook('/test'), '/v1/test')
self.assertEqual(conn.morph_action_hook('test'), '/v1/test')

conn.request_path = 'v1/'
self.assertEqual(conn.morph_action_hook('/test'), '/v1/test')
self.assertEqual(conn.morph_action_hook('test'), '/v1/test')

def test_connect_with_prefix(self):
"""
Test that a connection with a base path (e.g. /v1/) will
add the base path to requests
"""
conn = Connection(url='http://test.com/')
conn.connect()
conn.request_path = '/v1'
self.assertEqual(conn.connection.host, 'http://test.com')
with requests_mock.mock() as m:
m.get('http://test.com/v1/test', text='data')
response = conn.request('/test')
self.assertEqual(response.body, 'data')

def test_secure_connection_unusual_port(self):
"""
Test that the connection class will default to secure (https) even
Expand Down