diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 5b75fd96c..fc0494c66 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -12,11 +12,20 @@ from SoftLayer import consts from SoftLayer import transports +# pylint: disable=invalid-name + + API_PUBLIC_ENDPOINT = consts.API_PUBLIC_ENDPOINT API_PRIVATE_ENDPOINT = consts.API_PRIVATE_ENDPOINT -__all__ = ['Client', 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT'] - -VALID_CALL_ARGS = set([ +__all__ = [ + 'create_client_from_env', + 'Client', + 'BaseClient', + 'API_PUBLIC_ENDPOINT', + 'API_PRIVATE_ENDPOINT', +] + +VALID_CALL_ARGS = set(( 'id', 'mask', 'filter', @@ -25,11 +34,22 @@ 'raw_headers', 'limit', 'offset', -]) +)) -class Client(object): - """A SoftLayer API client. +def create_client_from_env(username=None, + api_key=None, + endpoint_url=None, + timeout=None, + auth=None, + config_file=None, + proxy=None, + user_agent=None, + transport=None): + """Creates a SoftLayer API client using your environment. + + Settings are loaded via keyword arguments, environemtal variables and + config file. :param username: an optional API username if you wish to bypass the package's built-in username @@ -51,38 +71,62 @@ class Client(object): Usage: >>> import SoftLayer - >>> client = SoftLayer.Client(username="username", api_key="api_key") + >>> client = SoftLayer.create_client_from_env() >>> resp = client['Account'].getObject() >>> resp['companyName'] 'Your Company' """ + settings = config.get_client_settings(username=username, + api_key=api_key, + endpoint_url=endpoint_url, + timeout=timeout, + proxy=proxy, + config_file=config_file) + + # Default the transport to use XMLRPC + if transport is None: + transport = transports.XmlRpcTransport( + endpoint_url=settings.get('endpoint_url'), + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + ) + + # If we have enough information to make an auth driver, let's do it + if auth is None and settings.get('username') and settings.get('api_key'): + + auth = slauth.BasicAuthentication( + settings.get('username'), + settings.get('api_key'), + ) + + return BaseClient(auth=auth, transport=transport) + + +def Client(**kwargs): + """Get a SoftLayer API Client using environmental settings. + + Deprecated in favor of create_client_from_env() + """ + warnings.warn("use SoftLayer.create_client_from_env() instead", + DeprecationWarning) + return create_client_from_env(**kwargs) + + +class BaseClient(object): + """Base SoftLayer API client. + + :param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase + :param transport: An object that's callable with this signature: + transport(SoftLayer.transports.Request) + """ + _prefix = "SoftLayer_" - def __init__(self, username=None, api_key=None, endpoint_url=None, - timeout=None, auth=None, config_file=None, proxy=None, - user_agent=None, transport=None): - - settings = config.get_client_settings(username=username, - api_key=api_key, - endpoint_url=endpoint_url, - timeout=timeout, - auth=auth, - proxy=proxy, - config_file=config_file) - self.auth = settings.get('auth') - - self.endpoint_url = (settings.get('endpoint_url') or - API_PUBLIC_ENDPOINT).rstrip('/') - self.transport = transport or transports.XmlRpcTransport() - - self.timeout = None - if settings.get('timeout'): - self.timeout = float(settings.get('timeout')) - self.proxy = None - if settings.get('proxy'): - self.proxy = settings.get('proxy') - self.user_agent = user_agent + def __init__(self, auth=None, transport=None): + self.auth = auth + self.transport = transport def authenticate_with_password(self, username, password, security_question_id=None, @@ -145,13 +189,10 @@ def call(self, service, method, *args, **kwargs): raise TypeError( 'Invalid keyword arguments: %s' % ','.join(invalid_kwargs)) - if not service.startswith(self._prefix): + if self._prefix and not service.startswith(self._prefix): service = self._prefix + service - http_headers = { - 'User-Agent': self.user_agent or consts.USER_AGENT, - 'Content-Type': 'application/xml', - } + http_headers = {} if kwargs.get('compress', True): http_headers['Accept'] = '*/*' @@ -161,13 +202,10 @@ def call(self, service, method, *args, **kwargs): http_headers.update(kwargs.get('raw_headers')) request = transports.Request() - request.endpoint = self.endpoint_url request.service = service request.method = method request.args = args request.transport_headers = http_headers - request.timeout = self.timeout - request.proxy = self.proxy request.identifier = kwargs.get('id') request.mask = kwargs.get('mask') request.filter = kwargs.get('filter') @@ -244,8 +282,7 @@ def iter_call(self, service, method, *args, **kwargs): break def __repr__(self): - return "" % (self.endpoint_url, - self.auth) + return "Client(transport=%r, auth=%r)" % (self.transport, self.auth) __str__ = __repr__ diff --git a/SoftLayer/CLI/config/__init__.py b/SoftLayer/CLI/config/__init__.py index f309ad656..3a45cfff8 100644 --- a/SoftLayer/CLI/config/__init__.py +++ b/SoftLayer/CLI/config/__init__.py @@ -12,8 +12,8 @@ def get_settings_from_client(client): settings = { 'username': '', 'api_key': '', - 'timeout': client.timeout or None, - 'endpoint_url': client.endpoint_url, + 'timeout': '', + 'endpoint_url': '', } try: settings['username'] = client.auth.username @@ -21,6 +21,12 @@ def get_settings_from_client(client): except AttributeError: pass + try: + settings['timeout'] = client.transport.transport.timeout + settings['endpoint_url'] = client.transport.transport.endpoint_url + except AttributeError: + pass + return settings diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index 191351e01..660492324 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -7,7 +7,12 @@ """ # pylint: disable=no-self-use -__all__ = ['BasicAuthentication', 'TokenAuthentication', 'AuthenticationBase'] +__all__ = [ + 'BasicAuthentication', + 'TokenAuthentication', + 'BasicHTTPAuthentication', + 'AuthenticationBase', +] class AuthenticationBase(object): @@ -51,7 +56,7 @@ def get_request(self, request): return request def __repr__(self): - return "" % (self.user_id, self.auth_token) + return "TokenAuthentication(%r)" % self.user_id class BasicAuthentication(AuthenticationBase): @@ -73,4 +78,24 @@ def get_request(self, request): return request def __repr__(self): - return "" % (self.username) + return "BasicAuthentication(username=%r)" % self.username + + +class BasicHTTPAuthentication(AuthenticationBase): + """Token-based authentication class. + + :param username str: a user's username + :param api_key str: a user's API key + """ + def __init__(self, username, api_key): + self.username = username + self.api_key = api_key + + def get_request(self, request): + """Sets token-based auth headers.""" + request.transport_user = self.username + request.transport_password = self.api_key + return request + + def __repr__(self): + return "BasicHTTPAuthentication(username=%r)" % self.username diff --git a/SoftLayer/config.py b/SoftLayer/config.py index 81030517f..8c679e6cc 100644 --- a/SoftLayer/config.py +++ b/SoftLayer/config.py @@ -8,7 +8,6 @@ import os import os.path -from SoftLayer import auth from SoftLayer import utils @@ -17,17 +16,13 @@ def get_client_settings_args(**kwargs): :param \\*\\*kwargs: Arguments that are passed into the client instance """ - settings = { + return { 'endpoint_url': kwargs.get('endpoint_url'), - 'timeout': kwargs.get('timeout'), - 'auth': kwargs.get('auth'), + 'timeout': float(kwargs.get('timeout') or 0), 'proxy': kwargs.get('proxy'), + 'username': kwargs.get('username'), + 'api_key': kwargs.get('api_key'), } - username = kwargs.get('username') - api_key = kwargs.get('api_key') - if username and api_key and not settings['auth']: - settings['auth'] = auth.BasicAuthentication(username, api_key) - return settings def get_client_settings_env(**_): @@ -35,14 +30,12 @@ def get_client_settings_env(**_): :param \\*\\*kwargs: Arguments that are passed into the client instance """ - username = os.environ.get('SL_USERNAME') - api_key = os.environ.get('SL_API_KEY') - proxy = os.environ.get('https_proxy') - config = {'proxy': proxy} - if username and api_key: - config['auth'] = auth.BasicAuthentication(username, api_key) - return config + return { + 'proxy': os.environ.get('https_proxy'), + 'username': os.environ.get('SL_USERNAME'), + 'api_key': os.environ.get('SL_API_KEY'), + } def get_client_settings_config_file(**kwargs): @@ -58,7 +51,7 @@ def get_client_settings_config_file(**kwargs): 'username': '', 'api_key': '', 'endpoint_url': '', - 'timeout': '', + 'timeout': '0', 'proxy': '', }) config.read(config_files) @@ -66,16 +59,14 @@ def get_client_settings_config_file(**kwargs): if not config.has_section('softlayer'): return - settings = { + return { 'endpoint_url': config.get('softlayer', 'endpoint_url'), - 'timeout': config.get('softlayer', 'timeout'), + 'timeout': config.getfloat('softlayer', 'timeout'), 'proxy': config.get('softlayer', 'proxy'), + 'username': config.get('softlayer', 'username'), + 'api_key': config.get('softlayer', 'api_key'), } - username = config.get('softlayer', 'username') - api_key = config.get('softlayer', 'api_key') - if username and api_key: - settings['auth'] = auth.BasicAuthentication(username, api_key) - return settings + SETTING_RESOLVERS = [get_client_settings_args, get_client_settings_env, @@ -86,8 +77,7 @@ def get_client_settings(**kwargs): """Parse client settings. Parses settings from various input methods, preferring earlier values - to later ones. Once an 'auth' value is found, it returns the gathered - settings. The settings currently come from explicit user arguments, + to later ones. The settings currently come from explicit user arguments, environmental variables and config files. :param \\*\\*kwargs: Arguments that are passed into the client instance @@ -98,6 +88,5 @@ def get_client_settings(**kwargs): if settings: settings.update((k, v) for k, v in all_settings.items() if v) all_settings = settings - if all_settings.get('auth'): - break + return all_settings diff --git a/SoftLayer/managers/metadata.py b/SoftLayer/managers/metadata.py index acad47b70..405f88da7 100644 --- a/SoftLayer/managers/metadata.py +++ b/SoftLayer/managers/metadata.py @@ -56,11 +56,13 @@ class MetadataManager(object): attribs = METADATA_MAPPING def __init__(self, client=None, timeout=5): - url = consts.API_PRIVATE_ENDPOINT_REST.rstrip('/') if client is None: - client = SoftLayer.Client(endpoint_url=url, - timeout=timeout, - transport=transports.RestTransport()) + transport = transports.RestTransport( + timeout=timeout, + endpoint_url=consts.API_PRIVATE_ENDPOINT_REST, + ) + client = SoftLayer.BaseClient(transport=transport) + self.client = client def get(self, name, param=None): @@ -83,7 +85,7 @@ def get(self, name, param=None): try: return self.client.call('Resource_Metadata', self.attribs[name]['call'], - id=param) + param) except exceptions.SoftLayerAPIError as ex: if ex.faultCode == 404: return None diff --git a/SoftLayer/tests/CLI/modules/config_tests.py b/SoftLayer/tests/CLI/modules/config_tests.py index 8299d7b93..73baf54ae 100644 --- a/SoftLayer/tests/CLI/modules/config_tests.py +++ b/SoftLayer/tests/CLI/modules/config_tests.py @@ -24,8 +24,8 @@ def test_show(self): self.assertEqual(json.loads(result.output), {'Username': 'default-user', 'API Key': 'default-key', - 'Endpoint URL': 'default-endpoint-url', - 'Timeout': 10.0}) + 'Endpoint URL': 'not set', + 'Timeout': 'not set'}) class TestHelpSetup(testing.TestCase): @@ -80,7 +80,6 @@ def test_get_user_input_private(self, input, getpass): self.assertEqual(username, 'user') self.assertEqual(secret, 'A' * 64) self.assertEqual(endpoint_url, consts.API_PRIVATE_ENDPOINT) - self.assertEqual(timeout, 10) @mock.patch('SoftLayer.CLI.environment.Environment.getpass') @mock.patch('SoftLayer.CLI.environment.Environment.input') diff --git a/SoftLayer/tests/api_tests.py b/SoftLayer/tests/api_tests.py index 6df365108..daae5aed7 100644 --- a/SoftLayer/tests/api_tests.py +++ b/SoftLayer/tests/api_tests.py @@ -8,7 +8,6 @@ import SoftLayer import SoftLayer.API -from SoftLayer import consts from SoftLayer import testing TEST_AUTH_HEADERS = { @@ -24,22 +23,21 @@ def test_init(self): self.assertIsInstance(client.auth, SoftLayer.BasicAuthentication) self.assertEqual(client.auth.username, 'doesnotexist') self.assertEqual(client.auth.api_key, 'issurelywrong') - self.assertEqual(client.endpoint_url, + self.assertEqual(client.transport.endpoint_url, SoftLayer.API_PUBLIC_ENDPOINT.rstrip('/')) - self.assertEqual(client.timeout, 10) + self.assertEqual(client.transport.timeout, 10) @mock.patch('SoftLayer.config.get_client_settings') def test_env(self, get_client_settings): auth = mock.Mock() get_client_settings.return_value = { - 'auth': auth, 'timeout': 10, 'endpoint_url': 'http://endpoint_url/', } - client = SoftLayer.Client() + client = SoftLayer.Client(auth=auth) self.assertEqual(client.auth.get_headers(), auth.get_headers()) - self.assertEqual(client.timeout, 10) - self.assertEqual(client.endpoint_url, 'http://endpoint_url') + self.assertEqual(client.transport.timeout, 10) + self.assertEqual(client.transport.endpoint_url, 'http://endpoint_url') class ClientMethods(testing.TestCase): @@ -76,7 +74,6 @@ def test_simple_call(self): self.assertEqual(resp, {"test": "result"}) self.assert_called_with('SoftLayer_SERVICE', 'METHOD', - endpoint=self.client.endpoint_url, mask=None, filter=None, identifier=None, @@ -101,7 +98,6 @@ def test_complex(self): self.assertEqual(resp, {"test": "result"}) self.assert_called_with('SoftLayer_SERVICE', 'METHOD', - endpoint=self.client.endpoint_url, mask={'object': {'attribute': ''}}, filter=_filter, identifier=5678, @@ -111,22 +107,22 @@ def test_complex(self): headers=TEST_AUTH_HEADERS, ) - @mock.patch('SoftLayer.API.Client.iter_call') + @mock.patch('SoftLayer.API.BaseClient.iter_call') def test_iterate(self, _iter_call): self.client['SERVICE'].METHOD(iter=True) _iter_call.assert_called_with('SERVICE', 'METHOD') - @mock.patch('SoftLayer.API.Client.iter_call') + @mock.patch('SoftLayer.API.BaseClient.iter_call') def test_service_iter_call(self, _iter_call): self.client['SERVICE'].iter_call('METHOD', 'ARG') _iter_call.assert_called_with('SERVICE', 'METHOD', 'ARG') - @mock.patch('SoftLayer.API.Client.iter_call') + @mock.patch('SoftLayer.API.BaseClient.iter_call') def test_service_iter_call_with_chunk(self, _iter_call): self.client['SERVICE'].iter_call('METHOD', 'ARG', chunk=2) _iter_call.assert_called_with('SERVICE', 'METHOD', 'ARG', chunk=2) - @mock.patch('SoftLayer.API.Client.call') + @mock.patch('SoftLayer.API.BaseClient.call') def test_iter_call(self, _call): # chunk=100, no limit _call.side_effect = [list(range(100)), list(range(100, 125))] @@ -207,10 +203,8 @@ def test_call_compression_enabled(self): self.client['SERVICE'].METHOD(compress=True) expected_headers = { - 'Content-Type': 'application/xml', 'Accept-Encoding': 'gzip, deflate, compress', 'Accept': '*/*', - 'User-Agent': consts.USER_AGENT, } self.assert_called_with('SoftLayer_SERVICE', 'METHOD', transport_headers=expected_headers) @@ -223,9 +217,7 @@ def test_call_compression_override(self): raw_headers={'Accept-Encoding': 'gzip'}) expected_headers = { - 'Content-Type': 'application/xml', 'Accept-Encoding': 'gzip', - 'User-Agent': consts.USER_AGENT, } self.assert_called_with('SoftLayer_SERVICE', 'METHOD', transport_headers=expected_headers) @@ -245,9 +237,9 @@ def test_init(self, get_client_settings): def test_init_with_proxy(self, get_client_settings): get_client_settings.return_value = {'proxy': 'http://localhost:3128'} client = SoftLayer.Client() - self.assertEqual(client.proxy, 'http://localhost:3128') + self.assertEqual(client.transport.proxy, 'http://localhost:3128') - @mock.patch('SoftLayer.API.Client.call') + @mock.patch('SoftLayer.API.BaseClient.call') def test_authenticate_with_password(self, _call): _call.return_value = { 'userId': 12345, diff --git a/SoftLayer/tests/auth_tests.py b/SoftLayer/tests/auth_tests.py index 240af3a99..6bac999f9 100644 --- a/SoftLayer/tests/auth_tests.py +++ b/SoftLayer/tests/auth_tests.py @@ -63,4 +63,23 @@ def test_repr(self): s = repr(self.auth) self.assertIn('TokenAuthentication', s) self.assertIn('12345', s) - self.assertIn('TOKEN', s) + + +class TestBasicHTTPAuthentication(testing.TestCase): + def set_up(self): + self.auth = auth.BasicHTTPAuthentication('USERNAME', 'APIKEY') + + def test_attribs(self): + self.assertEqual(self.auth.username, 'USERNAME') + self.assertEqual(self.auth.api_key, 'APIKEY') + + def test_get_request(self): + req = transports.Request() + authed_req = self.auth.get_request(req) + self.assertEqual(authed_req.transport_user, 'USERNAME') + self.assertEqual(authed_req.transport_password, 'APIKEY') + + def test_repr(self): + s = repr(self.auth) + self.assertIn('BasicHTTPAuthentication', s) + self.assertIn('USERNAME', s) diff --git a/SoftLayer/tests/config_tests.py b/SoftLayer/tests/config_tests.py index 3db988c01..3ef805e9f 100644 --- a/SoftLayer/tests/config_tests.py +++ b/SoftLayer/tests/config_tests.py @@ -50,28 +50,10 @@ def test_username_api_key(self): self.assertEqual(result['endpoint_url'], 'http://endpoint/') self.assertEqual(result['timeout'], 10) - self.assertEqual(result['auth'].username, 'username') - self.assertEqual(result['auth'].api_key, 'api_key') + self.assertEqual(result['username'], 'username') + self.assertEqual(result['api_key'], 'api_key') self.assertEqual(result['proxy'], 'https://localhost:3128') - def test_no_auth(self): - result = config.get_client_settings_args() - - self.assertEqual(result, { - 'endpoint_url': None, - 'timeout': None, - 'proxy': None, - 'auth': None, - }) - - def test_with_auth(self): - auth = mock.Mock() - result = config.get_client_settings_args(auth=auth) - - self.assertEqual(result['endpoint_url'], None) - self.assertEqual(result['timeout'], None) - self.assertEqual(result['auth'], auth) - class TestGetClientSettingsEnv(testing.TestCase): @@ -81,15 +63,8 @@ class TestGetClientSettingsEnv(testing.TestCase): def test_username_api_key(self): result = config.get_client_settings_env() - self.assertEqual(result['auth'].username, 'username') - self.assertEqual(result['auth'].api_key, 'api_key') - - @mock.patch.dict('os.environ', {'SL_USERNAME': '', 'SL_API_KEY': ''}) - def test_no_auth(self): - result = config.get_client_settings_env() - - # proxy might get ANY value depending on test env. - self.assertEqual(result, {'proxy': mock.ANY}) + self.assertEqual(result['username'], 'username') + self.assertEqual(result['api_key'], 'api_key') class TestGetClientSettingsConfigFile(testing.TestCase): @@ -99,10 +74,10 @@ def test_username_api_key(self, config_parser): result = config.get_client_settings_config_file() self.assertEqual(result['endpoint_url'], config_parser().get()) - self.assertEqual(result['timeout'], config_parser().get()) + self.assertEqual(result['timeout'], config_parser().getfloat()) self.assertEqual(result['proxy'], config_parser().get()) - self.assertEqual(result['auth'].username, config_parser().get()) - self.assertEqual(result['auth'].api_key, config_parser().get()) + self.assertEqual(result['username'], config_parser().get()) + self.assertEqual(result['api_key'], config_parser().get()) @mock.patch('six.moves.configparser.RawConfigParser') def test_no_section(self, config_parser): diff --git a/SoftLayer/tests/functional_tests.py b/SoftLayer/tests/functional_tests.py index 52383431f..53abe6eb9 100644 --- a/SoftLayer/tests/functional_tests.py +++ b/SoftLayer/tests/functional_tests.py @@ -38,13 +38,14 @@ def test_failed_auth(self): def test_no_hostname(self): try: request = transports.Request() - request.endpoint = 'http://notvalidsoftlayer.com' request.service = 'SoftLayer_Account' request.method = 'getObject' request.id = 1234 # This test will fail if 'notvalidsoftlayer.com' becomes a thing - transport = transports.XmlRpcTransport() + transport = transports.XmlRpcTransport( + endpoint_url='http://notvalidsoftlayer.com', + ) transport(request) except SoftLayer.SoftLayerAPIError as ex: self.assertIn('not known', str(ex)) diff --git a/SoftLayer/tests/managers/metadata_tests.py b/SoftLayer/tests/managers/metadata_tests.py index 8a5c28cb9..9d4712657 100644 --- a/SoftLayer/tests/managers/metadata_tests.py +++ b/SoftLayer/tests/managers/metadata_tests.py @@ -21,7 +21,6 @@ def test_get(self): self.assertEqual('dal01', resp) self.assert_called_with('SoftLayer_Resource_Metadata', 'Datacenter', - timeout=10.0, identifier=None) def test_no_param(self): @@ -36,7 +35,7 @@ def test_w_param(self): self.assertEqual([10, 124], resp) self.assert_called_with('SoftLayer_Resource_Metadata', 'Vlans', - identifier='1:2:3:4:5') + args=('1:2:3:4:5',)) def test_user_data(self): resp = self.metadata.get('user_data') diff --git a/SoftLayer/tests/transport_tests.py b/SoftLayer/tests/transport_tests.py index 29b9a7e7a..c4977ff05 100644 --- a/SoftLayer/tests/transport_tests.py +++ b/SoftLayer/tests/transport_tests.py @@ -10,6 +10,7 @@ import requests import SoftLayer +from SoftLayer import consts from SoftLayer import testing from SoftLayer import transports @@ -17,7 +18,9 @@ class TestXmlRpcAPICall(testing.TestCase): def set_up(self): - self.transport = transports.XmlRpcTransport() + self.transport = transports.XmlRpcTransport( + endpoint_url='http://something.com', + ) self.response = mock.MagicMock() self.response.content = ''' @@ -52,14 +55,14 @@ def test_call(self, request): ''' req = transports.Request() - req.endpoint = 'http://something.com' req.service = 'SoftLayer_Service' req.method = 'getObject' resp = self.transport(req) request.assert_called_with('POST', 'http://something.com/SoftLayer_Service', - headers={}, + headers={'Content-Type': 'application/xml', + 'User-Agent': consts.USER_AGENT}, proxies=None, data=data, timeout=None, @@ -69,7 +72,6 @@ def test_call(self, request): def test_proxy_without_protocol(self): req = transports.Request() - req.endpoint = 'http://something.com' req.service = 'SoftLayer_Service' req.method = 'Resource' req.proxy = 'localhost:3128' @@ -83,21 +85,20 @@ def test_proxy_without_protocol(self): @mock.patch('requests.request') def test_valid_proxy(self, request): request.return_value = self.response + self.transport.proxy = 'http://localhost:3128' req = transports.Request() - req.endpoint = 'http://something.com' req.service = 'SoftLayer_Service' req.method = 'Resource' - req.proxy = 'http://localhost:3128' self.transport(req) request.assert_called_with( 'POST', mock.ANY, - headers={}, proxies={'https': 'http://localhost:3128', 'http': 'http://localhost:3128'}, data=mock.ANY, + headers=mock.ANY, timeout=None, cert=None, verify=True) @@ -237,7 +238,6 @@ def test_request_exception(self, request): request().raise_for_status.side_effect = e req = transports.Request() - req.endpoint = 'http://something.com' req.service = 'SoftLayer_Service' req.method = 'getObject' @@ -247,13 +247,14 @@ def test_request_exception(self, request): class TestRestAPICall(testing.TestCase): def set_up(self): - self.transport = transports.RestTransport() + self.transport = transports.RestTransport( + endpoint_url='http://something.com', + ) @mock.patch('requests.request') def test_basic(self, request): request().content = '{}' req = transports.Request() - req.endpoint = 'http://something.com' req.service = 'SoftLayer_Service' req.method = 'Resource' @@ -261,7 +262,7 @@ def test_basic(self, request): self.assertEqual(resp, {}) request.assert_called_with( 'GET', 'http://something.com/SoftLayer_Service/Resource.json', - headers={}, + headers=mock.ANY, verify=True, cert=None, proxies=None, @@ -281,7 +282,6 @@ def test_basic(self, request): def test_proxy_without_protocol(self): req = transports.Request() - req.endpoint = 'http://something.com' req.service = 'SoftLayer_Service' req.method = 'Resource' req.proxy = 'localhost:3128' @@ -295,12 +295,11 @@ def test_proxy_without_protocol(self): @mock.patch('requests.request') def test_valid_proxy(self, request): request().content = '{}' + self.transport.proxy = 'http://localhost:3128' req = transports.Request() - req.endpoint = 'http://something.com' req.service = 'SoftLayer_Service' req.method = 'Resource' - req.proxy = 'http://localhost:3128' self.transport(req) request.assert_called_with( @@ -317,7 +316,6 @@ def test_with_id(self, request): request().content = '{}' req = transports.Request() - req.endpoint = 'http://something.com' req.service = 'SoftLayer_Service' req.method = 'getObject' req.identifier = 2 @@ -327,8 +325,29 @@ def test_with_id(self, request): self.assertEqual(resp, {}) request.assert_called_with( 'GET', - 'http://something.com/SoftLayer_Service/getObject/2.json', - headers={}, + 'http://something.com/SoftLayer_Service/2/getObject.json', + headers=mock.ANY, + verify=True, + cert=None, + proxies=None, + timeout=None) + + @mock.patch('requests.request') + def test_with_args(self, request): + request().content = '{}' + + req = transports.Request() + req.service = 'SoftLayer_Service' + req.method = 'getObject' + req.args = ('test', 1) + + resp = self.transport(req) + + self.assertEqual(resp, {}) + request.assert_called_with( + 'GET', + 'http://something.com/SoftLayer_Service/getObject/test/1.json', + headers=mock.ANY, verify=True, cert=None, proxies=None, @@ -343,7 +362,6 @@ def test_unknown_error(self, request): request().raise_for_status.side_effect = e req = transports.Request() - req.endpoint = 'http://something.com' req.service = 'SoftLayer_Service' req.method = 'getObject' diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index d73e59b7e..b30d5552b 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -5,6 +5,7 @@ :license: MIT, see LICENSE for more details. """ +from SoftLayer import consts from SoftLayer import exceptions from SoftLayer import utils @@ -19,20 +20,19 @@ # transports.Request does have a lot of instance attributes. :( # pylint: disable=too-many-instance-attributes -__all__ = ['Request', - 'XmlRpcTransport', - 'RestTransport', - 'TimingTransport', - 'FixtureTransport'] +__all__ = [ + 'Request', + 'XmlRpcTransport', + 'RestTransport', + 'TimingTransport', + 'FixtureTransport', +] class Request(object): """Transport request object.""" def __init__(self): - #: The SoftLayer endpoint address. - self.endpoint = None - #: API service name. E.G. SoftLayer_Account self.service = None @@ -45,14 +45,14 @@ def __init__(self): #: API headers, used for authentication, masks, limits, offsets, etc. self.headers = {} - #: Transport headers. - self.transport_headers = {} + #: Transport user. + self.transport_user = None - #: Integer timeout. - self.timeout = None + #: Transport password. + self.transport_password = None - #: URL to proxy API requests to. - self.proxy = None + #: Transport headers. + self.transport_headers = {} #: Boolean specifying if the server certificate should be verified. self.verify = True @@ -78,58 +78,69 @@ def __init__(self): class XmlRpcTransport(object): """XML-RPC transport.""" + def __init__(self, + endpoint_url=None, + timeout=None, + proxy=None, + user_agent=None): + + 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 def __call__(self, request): """Makes a SoftLayer API call against the XML-RPC endpoint. :param request request: Request object """ - try: - largs = list(request.args) + largs = list(request.args) - headers = request.headers + headers = request.headers - if request.identifier is not None: - header_name = request.service + 'InitParameters' - headers[header_name] = {'id': request.identifier} + if request.identifier is not None: + header_name = request.service + 'InitParameters' + headers[header_name] = {'id': request.identifier} - if request.mask is not None: - headers.update(_format_object_mask(request.mask, - request.service)) + if request.mask is not None: + headers.update(_format_object_mask(request.mask, request.service)) - if request.filter is not None: - headers['%sObjectFilter' % request.service] = request.filter + if request.filter is not None: + headers['%sObjectFilter' % request.service] = request.filter - if request.limit: - headers['resultLimit'] = { - 'limit': request.limit, - 'offset': request.offset or 0, - } + if request.limit: + headers['resultLimit'] = { + 'limit': request.limit, + 'offset': request.offset or 0, + } - largs.insert(0, {'headers': headers}) + largs.insert(0, {'headers': headers}) + request.transport_headers.setdefault('Content-Type', 'application/xml') + request.transport_headers.setdefault('User-Agent', self.user_agent) - url = '/'.join([request.endpoint, request.service]) - payload = utils.xmlrpc_client.dumps(tuple(largs), - methodname=request.method, - allow_none=True) - LOGGER.debug("=== REQUEST ===") - LOGGER.info('POST %s', url) - LOGGER.debug(request.transport_headers) - LOGGER.debug(payload) + url = '/'.join([self.endpoint_url, request.service]) + payload = utils.xmlrpc_client.dumps(tuple(largs), + methodname=request.method, + allow_none=True) + LOGGER.debug("=== REQUEST ===") + LOGGER.info('POST %s', url) + LOGGER.debug(request.transport_headers) + LOGGER.debug(payload) + try: response = requests.request('POST', url, data=payload, headers=request.transport_headers, - timeout=request.timeout, + timeout=self.timeout, verify=request.verify, cert=request.cert, - proxies=_proxies_dict(request.proxy)) + proxies=_proxies_dict(self.proxy)) LOGGER.debug("=== RESPONSE ===") LOGGER.debug(response.headers) LOGGER.debug(response.content) response.raise_for_status() - result = utils.xmlrpc_client.loads(response.content,)[0][0] - return result + return utils.xmlrpc_client.loads(response.content)[0][0] except utils.xmlrpc_client.Fault as ex: # These exceptions are formed from the XML-RPC spec # http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php @@ -154,7 +165,23 @@ def __call__(self, request): class RestTransport(object): - """REST transport.""" + """REST transport. + + Currently only supports GET requests (no POST, PUT, DELETE) and lacks + support for masks, filters, limits and offsets. + """ + + def __init__(self, + endpoint_url=None, + timeout=None, + proxy=None, + user_agent=None): + + 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 def __call__(self, request): """Makes a SoftLayer API call against the REST endpoint. @@ -163,9 +190,17 @@ def __call__(self, request): :param request request: Request object """ - url_parts = [request.endpoint, request.service, request.method] + url_parts = [self.endpoint_url, request.service] if request.identifier is not None: url_parts.append(str(request.identifier)) + if request.method is not None: + url_parts.append(request.method) + for arg in request.args: + url_parts.append(str(arg)) + + request.transport_headers.setdefault('Content-Type', + 'application/json') + request.transport_headers.setdefault('User-Agent', self.user_agent) url = '%s.%s' % ('/'.join(url_parts), 'json') @@ -175,10 +210,10 @@ def __call__(self, request): try: resp = requests.request('GET', url, headers=request.transport_headers, - timeout=request.timeout, + timeout=self.timeout, verify=request.verify, cert=request.cert, - proxies=_proxies_dict(request.proxy)) + proxies=_proxies_dict(self.proxy)) LOGGER.debug("=== RESPONSE ===") LOGGER.debug(resp.headers) LOGGER.debug(resp.content)