diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 322309f2b..2a79264c2 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -46,7 +46,8 @@ def create_client_from_env(username=None, config_file=None, proxy=None, user_agent=None, - transport=None): + transport=None, + verify=True): """Creates a SoftLayer API client using your environment. Settings are loaded via keyword arguments, environemtal variables and @@ -68,6 +69,8 @@ def create_client_from_env(username=None, calls if you wish to bypass the packages built in User Agent string :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request) + :param bool verify: decide to verify the server's SSL/TLS cert. DO NOT SET + TO FALSE WITHOUT UNDERSTANDING THE IMPLICATIONS. Usage: @@ -83,6 +86,7 @@ def create_client_from_env(username=None, endpoint_url=endpoint_url, timeout=timeout, proxy=proxy, + verify=verify, config_file=config_file) if transport is None: @@ -94,6 +98,7 @@ def create_client_from_env(username=None, proxy=settings.get('proxy'), timeout=settings.get('timeout'), user_agent=user_agent, + verify=verify, ) else: # Default the transport to use XMLRPC @@ -102,6 +107,7 @@ def create_client_from_env(username=None, proxy=settings.get('proxy'), timeout=settings.get('timeout'), user_agent=user_agent, + verify=verify, ) # If we have enough information to make an auth driver, let's do it @@ -240,7 +246,8 @@ def call(self, service, method, *args, **kwargs): request.filter = kwargs.get('filter') request.limit = kwargs.get('limit') request.offset = kwargs.get('offset') - request.verify = kwargs.get('verify') + if kwargs.get('verify') is not None: + request.verify = kwargs.get('verify') if self.auth: extra_headers = self.auth.get_headers() diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 8e143818d..b3239fc14 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -64,7 +64,7 @@ def __init__(self): self.transport_headers = {} #: Boolean specifying if the server certificate should be verified. - self.verify = True + self.verify = None #: Client certificate file path. self.cert = None @@ -103,13 +103,15 @@ def __init__(self, endpoint_url=None, timeout=None, proxy=None, - user_agent=None): + user_agent=None, + verify=True): self.endpoint_url = (endpoint_url or consts.API_PUBLIC_ENDPOINT).rstrip('/') self.timeout = timeout or None self.proxy = proxy self.user_agent = user_agent or consts.USER_AGENT + self.verify = verify def __call__(self, request): """Makes a SoftLayer API call against the XML-RPC endpoint. @@ -145,6 +147,12 @@ def __call__(self, request): payload = utils.xmlrpc_client.dumps(tuple(largs), methodname=request.method, allow_none=True) + + # Prefer the request setting, if it's not None + verify = request.verify + if verify is None: + verify = self.verify + LOGGER.debug("=== REQUEST ===") LOGGER.info('POST %s', url) LOGGER.debug(request.transport_headers) @@ -155,7 +163,7 @@ def __call__(self, request): data=payload, headers=request.transport_headers, timeout=self.timeout, - verify=request.verify, + verify=verify, cert=request.cert, proxies=_proxies_dict(self.proxy)) LOGGER.debug("=== RESPONSE ===") @@ -202,13 +210,15 @@ def __init__(self, endpoint_url=None, timeout=None, proxy=None, - user_agent=None): + user_agent=None, + verify=True): self.endpoint_url = (endpoint_url or consts.API_PUBLIC_ENDPOINT_REST).rstrip('/') self.timeout = timeout or None self.proxy = proxy self.user_agent = user_agent or consts.USER_AGENT + self.verify = verify def __call__(self, request): """Makes a SoftLayer API call against the REST endpoint. @@ -269,6 +279,11 @@ def __call__(self, request): url = '%s.%s' % ('/'.join(url_parts), 'json') + # Prefer the request setting, if it's not None + verify = request.verify + if verify is None: + verify = self.verify + LOGGER.debug("=== REQUEST ===") LOGGER.info(url) LOGGER.debug(request.transport_headers) @@ -280,14 +295,14 @@ def __call__(self, request): params=params, data=raw_body, timeout=self.timeout, - verify=request.verify, + verify=verify, cert=request.cert, proxies=_proxies_dict(self.proxy)) LOGGER.debug("=== RESPONSE ===") LOGGER.debug(resp.headers) - LOGGER.debug(resp.content) + LOGGER.debug(resp.text) resp.raise_for_status() - result = json.loads(resp.content) + result = json.loads(resp.text) if isinstance(result, list): return SoftLayerListResult( @@ -295,9 +310,9 @@ def __call__(self, request): else: return result except requests.HTTPError as ex: - content = json.loads(ex.response.content) + message = json.loads(ex.response.text)['error'] raise exceptions.SoftLayerAPIError(ex.response.status_code, - content['error']) + message) except requests.RequestException as ex: raise exceptions.TransportError(0, str(ex)) diff --git a/setup.cfg b/setup.cfg index 20520b557..ba4e6f120 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ -[pytest] +[tool:pytest] python_files = *_tests.py [wheel] diff --git a/tests/api_tests.py b/tests/api_tests.py index ff5fce608..db42ee350 100644 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -16,7 +16,8 @@ class Inititialization(testing.TestCase): def test_init(self): client = SoftLayer.Client(username='doesnotexist', api_key='issurelywrong', - timeout=10) + timeout=10, + endpoint_url='http://example.com/v3/xmlrpc/') self.assertIsInstance(client.auth, SoftLayer.BasicAuthentication) self.assertEqual(client.auth.username, 'doesnotexist') @@ -95,7 +96,7 @@ def test_simple_call(self): offset=None, ) - def test_verify(self): + def test_verify_request_false(self): client = SoftLayer.BaseClient(transport=self.mocks) mock = self.set_mock('SoftLayer_SERVICE', 'METHOD') mock.return_value = {"test": "result"} @@ -105,6 +106,26 @@ def test_verify(self): self.assertEqual(resp, {"test": "result"}) self.assert_called_with('SoftLayer_SERVICE', 'METHOD', verify=False) + def test_verify_request_true(self): + client = SoftLayer.BaseClient(transport=self.mocks) + mock = self.set_mock('SoftLayer_SERVICE', 'METHOD') + mock.return_value = {"test": "result"} + + resp = client.call('SERVICE', 'METHOD', verify=True) + + self.assertEqual(resp, {"test": "result"}) + self.assert_called_with('SoftLayer_SERVICE', 'METHOD', verify=True) + + def test_verify_request_not_specified(self): + client = SoftLayer.BaseClient(transport=self.mocks) + mock = self.set_mock('SoftLayer_SERVICE', 'METHOD') + mock.return_value = {"test": "result"} + + resp = client.call('SERVICE', 'METHOD') + + self.assertEqual(resp, {"test": "result"}) + self.assert_called_with('SoftLayer_SERVICE', 'METHOD', verify=None) + @mock.patch('SoftLayer.API.BaseClient.iter_call') def test_iterate(self, _iter_call): self.client['SERVICE'].METHOD(iter=True) diff --git a/tests/transport_tests.py b/tests/transport_tests.py index e262b7eb0..690882f69 100644 --- a/tests/transport_tests.py +++ b/tests/transport_tests.py @@ -8,6 +8,7 @@ import warnings import mock +import pytest import requests import six @@ -17,26 +18,31 @@ from SoftLayer import transports +def get_xmlrpc_response(): + response = requests.Response() + list_body = six.b(''' + + + + + + + + +''') + response.raw = io.BytesIO(list_body) + response.headers['SoftLayer-Total-Items'] = 10 + response.status_code = 200 + return response + + class TestXmlRpcAPICall(testing.TestCase): def set_up(self): self.transport = transports.XmlRpcTransport( endpoint_url='http://something.com', ) - self.response = requests.Response() - list_body = six.b(''' - - - - - - - - -''') - self.response.raw = io.BytesIO(list_body) - self.response.headers['SoftLayer-Total-Items'] = 10 - self.response.status_code = 200 + self.response = get_xmlrpc_response() @mock.patch('requests.request') def test_call(self, request): @@ -251,6 +257,55 @@ def test_request_exception(self, request): self.assertRaises(SoftLayer.TransportError, self.transport, req) +@mock.patch('requests.request') +@pytest.mark.parametrize( + "transport_verify,request_verify,expected", + [ + (True, True, True), + (True, False, False), + (True, None, True), + + (False, True, True), + (False, False, False), + (False, None, False), + + (None, True, True), + (None, False, False), + (None, None, True), + ] +) +def test_verify(request, + transport_verify, + request_verify, + expected): + request.return_value = get_xmlrpc_response() + + transport = transports.XmlRpcTransport( + endpoint_url='http://something.com', + ) + + req = transports.Request() + req.service = 'SoftLayer_Service' + req.method = 'getObject' + + if request_verify is not None: + req.verify = request_verify + + if transport_verify is not None: + transport.verify = transport_verify + + transport(req) + + request.assert_called_with('POST', + 'http://something.com/SoftLayer_Service', + data=mock.ANY, + headers=mock.ANY, + cert=mock.ANY, + proxies=mock.ANY, + timeout=mock.ANY, + verify=expected) + + class TestRestAPICall(testing.TestCase): def set_up(self): @@ -261,6 +316,7 @@ def set_up(self): @mock.patch('requests.request') def test_basic(self, request): request().content = '[]' + request().text = '[]' request().headers = requests.structures.CaseInsensitiveDict({ 'SoftLayer-Total-Items': '10', }) @@ -290,7 +346,7 @@ def test_error(self, request): e = requests.HTTPError('error') e.response = mock.MagicMock() e.response.status_code = 404 - e.response.content = '''{ + e.response.text = '''{ "error": "description", "code": "Error Code" }''' @@ -315,7 +371,7 @@ def test_proxy_without_protocol(self): @mock.patch('requests.request') def test_valid_proxy(self, request): - request().content = '{}' + request().text = '{}' self.transport.proxy = 'http://localhost:3128' req = transports.Request() @@ -323,6 +379,7 @@ def test_valid_proxy(self, request): req.method = 'Resource' self.transport(req) + request.assert_called_with( 'GET', 'http://something.com/SoftLayer_Service/Resource.json', proxies={'https': 'http://localhost:3128', @@ -337,7 +394,7 @@ def test_valid_proxy(self, request): @mock.patch('requests.request') def test_with_id(self, request): - request().content = '{}' + request().text = '{}' req = transports.Request() req.service = 'SoftLayer_Service' @@ -361,7 +418,7 @@ def test_with_id(self, request): @mock.patch('requests.request') def test_with_args(self, request): - request().content = '{}' + request().text = '{}' req = transports.Request() req.service = 'SoftLayer_Service' @@ -385,7 +442,7 @@ def test_with_args(self, request): @mock.patch('requests.request') def test_with_filter(self, request): - request().content = '{}' + request().text = '{}' req = transports.Request() req.service = 'SoftLayer_Service' @@ -410,7 +467,7 @@ def test_with_filter(self, request): @mock.patch('requests.request') def test_with_mask(self, request): - request().content = '{}' + request().text = '{}' req = transports.Request() req.service = 'SoftLayer_Service'