From 016471dd544bdce61dd9e58603f4ca4287e8e4a1 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 27 May 2022 16:09:12 -0500 Subject: [PATCH 01/29] fixing doc build issues --- docs/cli/reports.rst | 2 ++ docs/requirements.txt | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/cli/reports.rst b/docs/cli/reports.rst index 39299e99b..377533b1d 100644 --- a/docs/cli/reports.rst +++ b/docs/cli/reports.rst @@ -9,6 +9,7 @@ There are a few report type commands in the SLCLI. :prog: summary :show-nested: + A list of datacenters, and how many servers, VSI, vlans, subnets and public_ips are in each. @@ -21,6 +22,7 @@ A list of datacenters, and how many servers, VSI, vlans, subnets and public_ips :prog: report datacenter-closures :show-nested: + Displays some basic information about the Servers and other resources that are in Datacenters scheduled to be decommissioned in the near future. See `IBM Cloud Datacenter Consolidation `_ for diff --git a/docs/requirements.txt b/docs/requirements.txt index acb2b7258..18540d3db 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ sphinx sphinx-click click -prettytable \ No newline at end of file +prettytable +rich \ No newline at end of file From 2b62252b8b323e0ebfe794ebeaa2a88d0681ac29 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 27 May 2022 17:32:50 -0500 Subject: [PATCH 02/29] islcli groundwork --- SoftLayer/API.py | 154 ++++++++++++++++++++++++++++++++++++++++ SoftLayer/CLI/login.py | 19 +++++ SoftLayer/CLI/routes.py | 1 + SoftLayer/auth.py | 22 ++++++ 4 files changed, 196 insertions(+) create mode 100644 SoftLayer/CLI/login.py diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 21f21ffc6..7477a4c27 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -27,6 +27,7 @@ __all__ = [ 'create_client_from_env', + 'employee_client', 'Client', 'BaseClient', 'API_PUBLIC_ENDPOINT', @@ -142,6 +143,88 @@ def create_client_from_env(username=None, return BaseClient(auth=auth, transport=transport, config_file=config_file) +def employee_client(username=None, + api_key=None, + endpoint_url=None, + timeout=None, + auth=None, + config_file=None, + proxy=None, + user_agent=None, + transport=None, + verify=True): + """Creates an INTERNAL SoftLayer API client using your environment. + + Settings are loaded via keyword arguments, environemtal variables and + config file. + + :param username: your user ID + :param api_key: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, 2fa_string) + :param endpoint_url: the API endpoint base URL you wish to connect to. + Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private + network. + :param proxy: proxy to be used to make API calls + :param integer timeout: timeout for API requests + :param auth: an object which responds to get_headers() to be inserted into + the xml-rpc headers. Example: `BasicAuthentication` + :param config_file: A path to a configuration file used to load settings + :param user_agent: an optional User Agent to report when making API + 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: + + >>> import SoftLayer + >>> client = SoftLayer.create_client_from_env() + >>> resp = client.call('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, + verify=verify, + config_file=config_file) + + if transport is None: + url = settings.get('endpoint_url') + if url is not None and '/rest' in url: + # If this looks like a rest endpoint, use the rest transport + transport = transports.RestTransport( + endpoint_url=settings.get('endpoint_url'), + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) + else: + # Default the transport to use XMLRPC + transport = transports.XmlRpcTransport( + endpoint_url=settings.get('endpoint_url'), + proxy=settings.get('proxy'), + timeout=settings.get('timeout'), + user_agent=user_agent, + verify=verify, + ) + + + if auth is None and settings.get('username') and settings.get('api_key'): + real_transport = getattr(transport, 'transport', transport) + if isinstance(real_transport, transports.XmlRpcTransport): + auth = slauth.EmployeeAuthentication( + settings.get('username'), + settings.get('api_key'), + ) + + return BaseClient(auth=auth, transport=transport) + + def Client(**kwargs): """Get a SoftLayer API Client using environmental settings. @@ -545,6 +628,77 @@ def __repr__(self): return "IAMClient(transport=%r, auth=%r)" % (self.transport, self.auth) +class EmployeeClient(BaseClient): + """Internal SoftLayer 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) + """ + + def authenticate_with_password(self, username, password, security_token=None): + """Performs IBM IAM Username/Password Authentication + + :param string username: your softlayer username + :param string password: your softlayer password + :param int security_token: your 2FA token, prompt if None + """ + + self.auth = None + if security_token is None: + security_token = input("Enter your 2FA Token now: ") + if len(security_token) != 6: + raise Exception("Invalid security token: {}".format(security_token)) + + auth_result = self.call('SoftLayer_User_Employee', 'performExternalAuthentication', + username, password, security_token) + + + self.settings['softlayer']['access_token'] = auth_result['hash'] + self.settings['softlayer']['userId'] = auth_result['userId'] + # self.settings['softlayer']['refresh_token'] = tokens['refresh_token'] + + config.write_config(self.settings, self.config_file) + self.auth = slauth.EmployeeAuthentication(auth_result['userId'], auth_result['hash']) + + return auth_result + + + + def authenticate_with_hash(self, userId, access_token): + """Authenticates to the Internal SL API with an employee userid + token + + :param string userId: Employee UserId + :param string access_token: Employee Hash Token + """ + self.auth = slauth.EmployeeAuthentication(userId, access_token) + + def refresh_token(self, userId, auth_token): + """Refreshes the login token""" + + self.auth = None + auth_result = self.call('SoftLayer_User_Employee', 'refreshEncryptedToken', auth_token, id=userId) + print(auth_result) + self.settings['softlayer']['access_token'] = auth_result[0] + + config.write_config(self.settings, self.config_file) + self.auth = slauth.EmployeeAuthentication(userId, auth_result[0]) + return auth_result + + def call(self, service, method, *args, **kwargs): + """Handles refreshing IAM tokens in case of a HTTP 401 error""" + try: + return super().call(service, method, *args, **kwargs) + except exceptions.SoftLayerAPIError as ex: + + if ex.faultCode == 401: + LOGGER.warning("Token has expired, trying to refresh. %s", ex.faultString) + return ex + else: + raise ex + + def __repr__(self): + return "IAMClient(transport=%r, auth=%r)" % (self.transport, self.auth) + class Service(object): """A SoftLayer Service. diff --git a/SoftLayer/CLI/login.py b/SoftLayer/CLI/login.py new file mode 100644 index 000000000..9d27b3615 --- /dev/null +++ b/SoftLayer/CLI/login.py @@ -0,0 +1,19 @@ +"""Login with your employee username, password, 2fa token""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI.command import SLCommand as SLCommand +from SoftLayer.CLI import config +from SoftLayer.CLI import environment + + +@click.command(cls=SLCommand) +@environment.pass_env +def cli(env): + """Logs you into the internal SoftLayer Network. + + username and password can be set in SL_USER and SL_PASSWORD env variables. You will be prompted for them otherwise. + """ + + print("OK") diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 603d5a1d2..f0029697e 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -8,6 +8,7 @@ ALL_ROUTES = [ ('shell', 'SoftLayer.shell.core:cli'), + ('login', 'SoftLayer.CLI.login:cli'), ('call-api', 'SoftLayer.CLI.call_api:cli'), diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index 18e0ebe96..e3caf1e54 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -12,6 +12,7 @@ 'TokenAuthentication', 'BasicHTTPAuthentication', 'AuthenticationBase', + 'EmployeeAuthentication' ] @@ -137,3 +138,24 @@ def get_request(self, request): def __repr__(self): return "BearerAuthentication(username={}, token={})".format(self.username, self.api_key) + +class EmployeeAuthentication(AuthenticationBase): + """Token-based authentication class. + + :param username str: a user's username + :param api_key str: a user's API key + """ + def __init__(self, user_id, user_hash): + self.user_id = user_id + self.hash = user_hash + + def get_request(self, request): + """Sets token-based auth headers.""" + request.headers['employeesession'] = { + 'userId': self.user_id, + 'authToken': self.hash, + } + return request + + def __repr__(self): + return "EmployeeAuthentication(userId=%r,hash=%s)" % (self.user_id, self.hash) \ No newline at end of file From a25cfdc00ab1fba7ebfbc9a20ba448191efc54f3 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 31 May 2022 17:04:17 -0500 Subject: [PATCH 03/29] Getting employee login command working --- SoftLayer/API.py | 39 +++++++++++++++++++++++------------- SoftLayer/CLI/environment.py | 2 +- SoftLayer/CLI/login.py | 37 ++++++++++++++++++++++++++++++++-- SoftLayer/auth.py | 2 +- SoftLayer/consts.py | 1 + 5 files changed, 63 insertions(+), 18 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 7477a4c27..a52645692 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -6,6 +6,7 @@ :license: MIT, see LICENSE for more details. """ # pylint: disable=invalid-name +import os import time import warnings @@ -144,7 +145,8 @@ def create_client_from_env(username=None, def employee_client(username=None, - api_key=None, + access_token=None, + password=None, endpoint_url=None, timeout=None, auth=None, @@ -159,7 +161,8 @@ def employee_client(username=None, config file. :param username: your user ID - :param api_key: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, 2fa_string) + :param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, 2fa_string) + :param password: password to use for employee authentication :param endpoint_url: the API endpoint base URL you wish to connect to. Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private network. @@ -185,15 +188,21 @@ def employee_client(username=None, """ settings = config.get_client_settings(username=username, - api_key=api_key, + api_key=None, endpoint_url=endpoint_url, timeout=timeout, proxy=proxy, verify=verify, config_file=config_file) + url = settings.get('endpoint_url') or consts.API_EMPLOYEE_ENDPOINT + + if 'internal' not in url: + raise exceptions.SoftLayerError("{} does not look like an Internal Employee url. Try {}".format( + url, consts.API_EMPLOYEE_ENDPOINT)) + if transport is None: - url = settings.get('endpoint_url') + if url is not None and '/rest' in url: # If this looks like a rest endpoint, use the rest transport transport = transports.RestTransport( @@ -214,15 +223,18 @@ def employee_client(username=None, ) - if auth is None and settings.get('username') and settings.get('api_key'): - real_transport = getattr(transport, 'transport', transport) - if isinstance(real_transport, transports.XmlRpcTransport): - auth = slauth.EmployeeAuthentication( - settings.get('username'), - settings.get('api_key'), - ) + if access_token is None: + access_token = settings.get('access_token') + + user_id = settings.get('user_id') + + # Assume access_token is valid for now, user has logged in before at least. + if access_token and user_id: + auth = slauth.EmployeeAuthentication(user_id, access_token) + return EmployeeClient(auth=auth, transport=transport) + else: + return EmployeeClient(auth=None, transport=transport) - return BaseClient(auth=auth, transport=transport) def Client(**kwargs): @@ -230,8 +242,7 @@ def Client(**kwargs): Deprecated in favor of create_client_from_env() """ - warnings.warn("use SoftLayer.create_client_from_env() instead", - DeprecationWarning) + warnings.warn("use SoftLayer.create_client_from_env() instead", DeprecationWarning) return create_client_from_env(**kwargs) diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py index 5744b7595..014ff33b2 100644 --- a/SoftLayer/CLI/environment.py +++ b/SoftLayer/CLI/environment.py @@ -198,7 +198,7 @@ def ensure_client(self, config_file=None, is_demo=False, proxy=None): ) else: # Create SL Client - client = SoftLayer.create_client_from_env( + client = SoftLayer.employee_client( proxy=proxy, config_file=config_file, ) diff --git a/SoftLayer/CLI/login.py b/SoftLayer/CLI/login.py index 9d27b3615..5e7a5c162 100644 --- a/SoftLayer/CLI/login.py +++ b/SoftLayer/CLI/login.py @@ -2,18 +2,51 @@ # :license: MIT, see LICENSE for more details. import click +import os + +from SoftLayer.API import EmployeeClient from SoftLayer.CLI.command import SLCommand as SLCommand -from SoftLayer.CLI import config +from SoftLayer import config from SoftLayer.CLI import environment +# def get_username(env): +# """Gets the username from config or env""" +# settings = + +def censor_password(value): + if value: + value = '*' * len(value) + return value + @click.command(cls=SLCommand) @environment.pass_env def cli(env): """Logs you into the internal SoftLayer Network. - username and password can be set in SL_USER and SL_PASSWORD env variables. You will be prompted for them otherwise. + username: Set this in either the softlayer config, or SL_USER ENV variable + password: Set this in SL_PASSWORD env variable. You will be prompted for them otherwise. """ + settings = config.get_config(config_file=env.config_file) + username = settings.get('username') or os.environ.get('SLCLI_USER') + password = os.environ.get('SLCLI_PASSWORD', '') + yubi = 123456 + + url = settings.get('endpoint_url') or consts.API_EMPLOYEE_ENDPOINT + click.echo("URL: {}".format(url)) + if username is None: + username = input("Username: ") + click.echo("Username: {}".format(username)) + if password is None: + password = env.getpass("Password: ") + click.echo("Password: {}".format(censor_password(password))) + # yubi = input("Yubi: ") + + client = EmployeeClient(config_file=env.config_file) + try: + client.authenticate_with_password(username, password, yubi) + except Exception as e: + click.echo("EXCEPTION: {}".format(e)) print("OK") diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index e3caf1e54..8811c77d2 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -143,7 +143,7 @@ class EmployeeAuthentication(AuthenticationBase): """Token-based authentication class. :param username str: a user's username - :param api_key str: a user's API key + :param user_hash str: a user's Authentication hash """ def __init__(self, user_id, user_hash): self.user_id = user_id diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 32ebd4ec4..5ac84b099 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -10,5 +10,6 @@ API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' API_PRIVATE_ENDPOINT_REST = 'https://api.service.softlayer.com/rest/v3.1/' +API_EMPLOYEE_ENDPOINT = 'https://internal.app0lb.dal10.softlayer.local/v3/internal/xmlrpc/' USER_AGENT = "softlayer-python/%s" % VERSION CONFIG_FILE = "~/.softlayer" From e8d4d162565dc0dd2ce83dae0eeab0a3a86350d7 Mon Sep 17 00:00:00 2001 From: allmightyspiff Date: Tue, 31 May 2022 17:42:33 -0500 Subject: [PATCH 04/29] proof of concept for refresh Token --- SoftLayer/API.py | 4 ++-- SoftLayer/CLI/login.py | 39 +++++++++++++++++++++++++++++++++------ islcli | 10 ++++++++++ 3 files changed, 45 insertions(+), 8 deletions(-) create mode 100755 islcli diff --git a/SoftLayer/API.py b/SoftLayer/API.py index a52645692..6256116a6 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -665,7 +665,7 @@ def authenticate_with_password(self, username, password, security_token=None): self.settings['softlayer']['access_token'] = auth_result['hash'] - self.settings['softlayer']['userId'] = auth_result['userId'] + self.settings['softlayer']['userId'] = str(auth_result['userId']) # self.settings['softlayer']['refresh_token'] = tokens['refresh_token'] config.write_config(self.settings, self.config_file) @@ -708,7 +708,7 @@ def call(self, service, method, *args, **kwargs): raise ex def __repr__(self): - return "IAMClient(transport=%r, auth=%r)" % (self.transport, self.auth) + return "EmployeeClient(transport=%r, auth=%r)" % (self.transport, self.auth) class Service(object): """A SoftLayer Service. diff --git a/SoftLayer/CLI/login.py b/SoftLayer/CLI/login.py index 5e7a5c162..34bd03030 100644 --- a/SoftLayer/CLI/login.py +++ b/SoftLayer/CLI/login.py @@ -28,10 +28,36 @@ def cli(env): username: Set this in either the softlayer config, or SL_USER ENV variable password: Set this in SL_PASSWORD env variable. You will be prompted for them otherwise. """ - settings = config.get_config(config_file=env.config_file) - username = settings.get('username') or os.environ.get('SLCLI_USER') + config_settings = config.get_config(config_file=env.config_file) + settings = config_settings['softlayer'] + username = settings.get('username') or os.environ.get('SLCLI_USER', None) password = os.environ.get('SLCLI_PASSWORD', '') - yubi = 123456 + yubi = None +# client = EmployeeClient(config_file=env.config_file) + client = env.client + + # Might already be logged in, try and refresh token + if settings.get('access_token') and settings.get('userid'): + client.authenticate_with_hash(settings.get('userid'), settings.get('access_token')) + try: + employee = client.call('SoftLayer_User_Employee', 'getObject', id=settings.get('userid'), mask="mask[id,username]") + print(employee) + refresh = client.call('SoftLayer_User_Employee', 'refreshEncryptedToken', settings.get('access_token'), id=settings.get('userid')) + print("REFRESH:\n{}\n".format(refresh)) + # we expect 2 results, a hash and a timeout + if len(refresh) > 1: + for returned_data in refresh: + # Access tokens should be 188 characters, but just incase. + if len(returned_data) > 180: + settings['access_token'] = returned_data + else: + raise Exception("Got unexpected data. Expected 2 properties: {}".format(refresh)) + config_settings['softlayer'] = settings + config.write_config(config_settings, env.config_file) + return + except Exception as ex: + print("Error with Hash, try with password: {}".format(ex)) + url = settings.get('endpoint_url') or consts.API_EMPLOYEE_ENDPOINT click.echo("URL: {}".format(url)) @@ -41,11 +67,12 @@ def cli(env): if password is None: password = env.getpass("Password: ") click.echo("Password: {}".format(censor_password(password))) - # yubi = input("Yubi: ") + yubi = input("Yubi: ") - client = EmployeeClient(config_file=env.config_file) + try: - client.authenticate_with_password(username, password, yubi) + result = client.authenticate_with_password(username, password, str(yubi)) + print(result) except Exception as e: click.echo("EXCEPTION: {}".format(e)) diff --git a/islcli b/islcli new file mode 100755 index 000000000..7822bc2bb --- /dev/null +++ b/islcli @@ -0,0 +1,10 @@ +#!python +import re +import sys + +from SoftLayer.CLI.core import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + # print("arvs[0] = %s" % sys.argv[0]) + sys.exit(main()) From 65df1cd75bb8c3ebf76ef19eff62ba260a4ce653 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 1 Jun 2022 17:19:49 -0500 Subject: [PATCH 05/29] building up unit tests --- SoftLayer/API.py | 5 +++++ tests/api_tests.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 6256116a6..bf510cf21 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -704,6 +704,11 @@ def call(self, service, method, *args, **kwargs): if ex.faultCode == 401: LOGGER.warning("Token has expired, trying to refresh. %s", ex.faultString) return ex + if ex.faultCode == "SoftLayer_Exception_EncryptedToken_Expired": + userId = self.settings['softlayer'].get('userId') + access_token = self.settings['softlayer'].get('access_token') + LOGGER.warning("Token has expired2, trying to refresh. %s", ex.faultString) + self.refresh_token() else: raise ex diff --git a/tests/api_tests.py b/tests/api_tests.py index ea4726a6e..b42001656 100644 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -4,12 +4,15 @@ :license: MIT, see LICENSE for more details. """ +import io from unittest import mock as mock +import requests import SoftLayer import SoftLayer.API from SoftLayer import testing from SoftLayer import transports +from SoftLayer import exceptions class Initialization(testing.TestCase): @@ -310,3 +313,47 @@ def test_authenticate_with_password(self, _call): self.assertIsNotNone(self.client.auth) self.assertEqual(self.client.auth.user_id, 12345) self.assertEqual(self.client.auth.auth_token, 'TOKEN') + + +class EmployeeClientTests(testing.TestCase): + + @staticmethod + def failed_log(): + response = requests.Response() + list_body = b''' + + + + + + faultCode + + SoftLayer_Exception_Public + + + + faultString + + Invalid username/password + + + + + + ''' + response.raw = io.BytesIO(list_body) + response.status_code = 200 + return response + + def set_up(self): + self.client = SoftLayer.API.EmployeeClient(config_file='./tests/testconfig') + + @mock.patch('SoftLayer.transports.xmlrpc.requests.Session.request') + def test_auth_with_pass(self, api_response): + api_response.return_value = self.failed_log() + exception = self.assertRaises( + exceptions.SoftLayerAPIError, + self.client.authenticate_with_password, 'testUser', 'testPassword', '123456') + + + self.assertEqual(exception.faultCode, "SoftLayer_Exception_Public") \ No newline at end of file From 96559074cda727d299a2e1dcc23851dab35e6e10 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 2 Jun 2022 15:11:14 -0500 Subject: [PATCH 06/29] expanding employee unit tests --- SoftLayer/API.py | 10 +++- SoftLayer/CLI/login.py | 13 +--- SoftLayer/fixtures/xmlrpc/invalidLogin.xml | 21 +++++++ SoftLayer/fixtures/xmlrpc/refreshFailure.xml | 0 SoftLayer/fixtures/xmlrpc/refreshSuccess.xml | 17 ++++++ SoftLayer/fixtures/xmlrpc/successLogin.xml | 21 +++++++ tests/api_tests.py | 62 ++++++++++++-------- 7 files changed, 106 insertions(+), 38 deletions(-) create mode 100644 SoftLayer/fixtures/xmlrpc/invalidLogin.xml create mode 100644 SoftLayer/fixtures/xmlrpc/refreshFailure.xml create mode 100644 SoftLayer/fixtures/xmlrpc/refreshSuccess.xml create mode 100644 SoftLayer/fixtures/xmlrpc/successLogin.xml diff --git a/SoftLayer/API.py b/SoftLayer/API.py index bf510cf21..f6b76801d 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -688,8 +688,14 @@ def refresh_token(self, userId, auth_token): self.auth = None auth_result = self.call('SoftLayer_User_Employee', 'refreshEncryptedToken', auth_token, id=userId) - print(auth_result) - self.settings['softlayer']['access_token'] = auth_result[0] + if len(auth_result) > 1: + for returned_data in auth_result: + # Access tokens should be 188 characters, but just incase its longer or something. + if len(returned_data) > 180: + self.settings['softlayer']['access_token'] = returned_data + else: + message = "Excepted 2 properties from refreshEncryptedToken, got |{}|".format(auth_result) + raise exceptions.SoftLayerAPIError(message) config.write_config(self.settings, self.config_file) self.auth = slauth.EmployeeAuthentication(userId, auth_result[0]) diff --git a/SoftLayer/CLI/login.py b/SoftLayer/CLI/login.py index 34bd03030..13afe1e8a 100644 --- a/SoftLayer/CLI/login.py +++ b/SoftLayer/CLI/login.py @@ -42,21 +42,14 @@ def cli(env): try: employee = client.call('SoftLayer_User_Employee', 'getObject', id=settings.get('userid'), mask="mask[id,username]") print(employee) + client.refresh_token(settings.get('userid'), settings.get('access_token')) refresh = client.call('SoftLayer_User_Employee', 'refreshEncryptedToken', settings.get('access_token'), id=settings.get('userid')) - print("REFRESH:\n{}\n".format(refresh)) - # we expect 2 results, a hash and a timeout - if len(refresh) > 1: - for returned_data in refresh: - # Access tokens should be 188 characters, but just incase. - if len(returned_data) > 180: - settings['access_token'] = returned_data - else: - raise Exception("Got unexpected data. Expected 2 properties: {}".format(refresh)) + config_settings['softlayer'] = settings config.write_config(config_settings, env.config_file) return except Exception as ex: - print("Error with Hash, try with password: {}".format(ex)) + print("Error with Hash Authentication, try with password: {}".format(ex)) url = settings.get('endpoint_url') or consts.API_EMPLOYEE_ENDPOINT diff --git a/SoftLayer/fixtures/xmlrpc/invalidLogin.xml b/SoftLayer/fixtures/xmlrpc/invalidLogin.xml new file mode 100644 index 000000000..1c993d2b5 --- /dev/null +++ b/SoftLayer/fixtures/xmlrpc/invalidLogin.xml @@ -0,0 +1,21 @@ + + + + + + + faultCode + + SoftLayer_Exception_Public + + + + faultString + + Invalid username/password + + + + + + \ No newline at end of file diff --git a/SoftLayer/fixtures/xmlrpc/refreshFailure.xml b/SoftLayer/fixtures/xmlrpc/refreshFailure.xml new file mode 100644 index 000000000..e69de29bb diff --git a/SoftLayer/fixtures/xmlrpc/refreshSuccess.xml b/SoftLayer/fixtures/xmlrpc/refreshSuccess.xml new file mode 100644 index 000000000..0b8003b30 --- /dev/null +++ b/SoftLayer/fixtures/xmlrpc/refreshSuccess.xml @@ -0,0 +1,17 @@ + + + + + + + + REFRESHEDTOKENaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + + 300 + + + + + + \ No newline at end of file diff --git a/SoftLayer/fixtures/xmlrpc/successLogin.xml b/SoftLayer/fixtures/xmlrpc/successLogin.xml new file mode 100644 index 000000000..880d9497e --- /dev/null +++ b/SoftLayer/fixtures/xmlrpc/successLogin.xml @@ -0,0 +1,21 @@ + + + + + + + hash + + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + + + userId + + 1234 + + + + + + diff --git a/tests/api_tests.py b/tests/api_tests.py index b42001656..e46f055c2 100644 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -5,6 +5,7 @@ :license: MIT, see LICENSE for more details. """ import io +import os from unittest import mock as mock import requests @@ -317,43 +318,52 @@ def test_authenticate_with_password(self, _call): class EmployeeClientTests(testing.TestCase): + @staticmethod - def failed_log(): + def setup_response(filename, status_code=200, total_items=1): + basepath = os.path.dirname(__file__) + body = b'' + print(f"Base Path: {basepath}") + with open(f"{basepath}/../SoftLayer/fixtures/xmlrpc/{filename}.xml", 'rb') as fixture: + body = fixture.read() response = requests.Response() - list_body = b''' - - - - - - faultCode - - SoftLayer_Exception_Public - - - - faultString - - Invalid username/password - - - - - - ''' + list_body = body response.raw = io.BytesIO(list_body) - response.status_code = 200 + response.headers['SoftLayer-Total-Items'] = total_items + response.status_code = status_code return response + def set_up(self): self.client = SoftLayer.API.EmployeeClient(config_file='./tests/testconfig') @mock.patch('SoftLayer.transports.xmlrpc.requests.Session.request') - def test_auth_with_pass(self, api_response): - api_response.return_value = self.failed_log() + def test_auth_with_pass_failure(self, api_response): + api_response.return_value = self.setup_response('invalidLogin') exception = self.assertRaises( exceptions.SoftLayerAPIError, self.client.authenticate_with_password, 'testUser', 'testPassword', '123456') + self.assertEqual(exception.faultCode, "SoftLayer_Exception_Public") + @mock.patch('SoftLayer.transports.xmlrpc.requests.Session.request') + def test_auth_with_pass_success(self, api_response): + api_response.return_value = self.setup_response('successLogin') + result = self.client.authenticate_with_password('testUser', 'testPassword', '123456') + print(result) + self.assertEqual(result['userId'], 1234) + self.assertEqual(self.client.settings['softlayer']['userid'], '1234') + self.assertIn('x'*200, self.client.settings['softlayer']['access_token']) + + def test_auth_with_hash(self): + self.client.auth = None + self.client.authenticate_with_hash(5555, 'abcdefg') + self.assertEqual(self.client.auth.user_id, 5555) + self.assertEqual(self.client.auth.hash, 'abcdefg') - self.assertEqual(exception.faultCode, "SoftLayer_Exception_Public") \ No newline at end of file + @mock.patch('SoftLayer.transports.xmlrpc.requests.Session.request') + def test_refresh_token(self, api_response): + api_response.return_value = self.setup_response('refreshSuccess') + result = self.client.refresh_token(9999, 'qweasdzxcqweasdzxcqweasdzxc') + self.assertEqual(self.client.auth.user_id, 9999) + self.assertIn('REFRESHEDTOKENaaaa', self.client.auth.hash) + \ No newline at end of file From 70689872a40d85d01b6ded36dc39f46b3368a2be Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 2 Jun 2022 16:36:17 -0500 Subject: [PATCH 07/29] added account flag to islcli --- SoftLayer/API.py | 31 +++++++++---- SoftLayer/CLI/core.py | 8 ++-- .../fixtures/xmlrpc/Employee_getObject.xml | 21 +++++++++ SoftLayer/fixtures/xmlrpc/expiredToken.xml | 21 +++++++++ tests/api_tests.py | 46 ++++++++++++++++++- 5 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 SoftLayer/fixtures/xmlrpc/Employee_getObject.xml create mode 100644 SoftLayer/fixtures/xmlrpc/expiredToken.xml diff --git a/SoftLayer/API.py b/SoftLayer/API.py index f6b76801d..d212daff5 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -646,6 +646,11 @@ class EmployeeClient(BaseClient): :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request) """ + def __init__(self, auth=None, transport=None, config_file=None, account_id=None): + BaseClient.__init__(self, auth, transport, config_file) + self.account_id = account_id + + def authenticate_with_password(self, username, password, security_token=None): """Performs IBM IAM Username/Password Authentication @@ -687,14 +692,16 @@ def refresh_token(self, userId, auth_token): """Refreshes the login token""" self.auth = None - auth_result = self.call('SoftLayer_User_Employee', 'refreshEncryptedToken', auth_token, id=userId) + + # Go directly to base client, to avoid infite loop if the token is super expired. + auth_result = BaseClient.call(self, 'SoftLayer_User_Employee', 'refreshEncryptedToken', auth_token, id=userId) if len(auth_result) > 1: for returned_data in auth_result: # Access tokens should be 188 characters, but just incase its longer or something. if len(returned_data) > 180: self.settings['softlayer']['access_token'] = returned_data else: - message = "Excepted 2 properties from refreshEncryptedToken, got |{}|".format(auth_result) + message = "Excepted 2 properties from refreshEncryptedToken, got {}|".format(auth_result) raise exceptions.SoftLayerAPIError(message) config.write_config(self.settings, self.config_file) @@ -702,19 +709,23 @@ def refresh_token(self, userId, auth_token): return auth_result def call(self, service, method, *args, **kwargs): - """Handles refreshing IAM tokens in case of a HTTP 401 error""" + """Handles refreshing Employee tokens in case of a HTTP 401 error""" + if (service == 'SoftLayer_Account' or service == 'Account') and not kwargs.get('id'): + if not self.account_id: + raise exceptions.SoftLayerError("SoftLayer_Account service requires an ID") + kwargs['id'] = self.account_id + try: - return super().call(service, method, *args, **kwargs) + return BaseClient.call(self, service, method, *args, **kwargs) except exceptions.SoftLayerAPIError as ex: - - if ex.faultCode == 401: - LOGGER.warning("Token has expired, trying to refresh. %s", ex.faultString) - return ex if ex.faultCode == "SoftLayer_Exception_EncryptedToken_Expired": userId = self.settings['softlayer'].get('userId') access_token = self.settings['softlayer'].get('access_token') - LOGGER.warning("Token has expired2, trying to refresh. %s", ex.faultString) - self.refresh_token() + LOGGER.warning("Token has expired, trying to refresh. %s", ex.faultString) + self.refresh_token(userId, access_token) + # Try the Call again this time.... + return BaseClient.call(self, service, method, *args, **kwargs) + else: raise ex diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index e79fc2880..013796f4d 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -70,9 +70,8 @@ def get_version_message(ctx, param, value): ctx.exit() -@click.group(help="SoftLayer Command-line Client", - epilog="""To use most commands your SoftLayer username and api_key need to be configured. -The easiest way to do that is to use: 'slcli setup'""", +@click.group(help="SoftLayer Employee Command-line Client", + epilog="""Run 'islcli login' to authenticate""", cls=CommandLoader, context_settings=CONTEXT_SETTINGS) @click.option('--format', @@ -103,6 +102,7 @@ def get_version_message(ctx, param, value): help="Use demo data instead of actually making API calls") @click.option('--version', is_flag=True, expose_value=False, is_eager=True, callback=get_version_message, help="Show version information.", allow_from_autoenv=False,) +@click.option('-a', '--account', help="Account Id") @environment.pass_env def cli(env, format='table', @@ -111,6 +111,7 @@ def cli(env, proxy=None, really=False, demo=False, + account_id=None, **kwargs): """Main click CLI entry-point.""" @@ -133,6 +134,7 @@ def cli(env, env.vars['_timings'] = SoftLayer.DebugTransport(env.client.transport) env.vars['verbose'] = verbose env.client.transport = env.vars['_timings'] + env.client.account_id = account_id @cli.result_callback() diff --git a/SoftLayer/fixtures/xmlrpc/Employee_getObject.xml b/SoftLayer/fixtures/xmlrpc/Employee_getObject.xml new file mode 100644 index 000000000..57ec3f140 --- /dev/null +++ b/SoftLayer/fixtures/xmlrpc/Employee_getObject.xml @@ -0,0 +1,21 @@ + + + + + + + id + + 5555 + + + + username + + testUser + + + + + + \ No newline at end of file diff --git a/SoftLayer/fixtures/xmlrpc/expiredToken.xml b/SoftLayer/fixtures/xmlrpc/expiredToken.xml new file mode 100644 index 000000000..43237bb3e --- /dev/null +++ b/SoftLayer/fixtures/xmlrpc/expiredToken.xml @@ -0,0 +1,21 @@ + + + + + + + faultCode + + SoftLayer_Exception_EncryptedToken_Expired + + + + faultString + + The token has expired. + + + + + + \ No newline at end of file diff --git a/tests/api_tests.py b/tests/api_tests.py index e46f055c2..2e1ab0fae 100644 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -14,6 +14,7 @@ from SoftLayer import testing from SoftLayer import transports from SoftLayer import exceptions +from SoftLayer import auth as slauth class Initialization(testing.TestCase): @@ -323,7 +324,6 @@ class EmployeeClientTests(testing.TestCase): def setup_response(filename, status_code=200, total_items=1): basepath = os.path.dirname(__file__) body = b'' - print(f"Base Path: {basepath}") with open(f"{basepath}/../SoftLayer/fixtures/xmlrpc/{filename}.xml", 'rb') as fixture: body = fixture.read() response = requests.Response() @@ -366,4 +366,46 @@ def test_refresh_token(self, api_response): result = self.client.refresh_token(9999, 'qweasdzxcqweasdzxcqweasdzxc') self.assertEqual(self.client.auth.user_id, 9999) self.assertIn('REFRESHEDTOKENaaaa', self.client.auth.hash) - \ No newline at end of file + + @mock.patch('SoftLayer.transports.xmlrpc.requests.Session.request') + def test_expired_token_is_refreshed(self, api_response): + api_response.side_effect = [ + self.setup_response('expiredToken'), + self.setup_response('refreshSuccess'), + self.setup_response('Employee_getObject') + ] + self.client.auth = slauth.EmployeeAuthentication(5555, 'aabbccee') + self.client.settings['softlayer']['userid'] = '5555' + result = self.client.call('SoftLayer_User_Employee', 'getObject', id=5555) + self.assertIn('REFRESHEDTOKENaaaa', self.client.auth.hash) + self.assertEqual('testUser', result['username']) + + @mock.patch('SoftLayer.transports.xmlrpc.requests.Session.request') + def test_expired_token_is_really_expored(self, api_response): + api_response.side_effect = [ + self.setup_response('expiredToken'), + self.setup_response('expiredToken') + ] + self.client.auth = slauth.EmployeeAuthentication(5555, 'aabbccee') + self.client.settings['softlayer']['userid'] = '5555' + exception = self.assertRaises( + exceptions.SoftLayerAPIError, + self.client.call, 'SoftLayer_User_Employee', 'getObject', id=5555) + self.assertEqual(None, self.client.auth) + self.assertEqual(exception.faultCode, "SoftLayer_Exception_EncryptedToken_Expired") + + @mock.patch('SoftLayer.API.BaseClient.call') + def test_account_check(self, _call): + self.client.transport = self.mocks + exception = self.assertRaises( + exceptions.SoftLayerError, + self.client.call, "SoftLayer_Account", "getObject") + self.assertEqual(str(exception), "SoftLayer_Account service requires an ID") + self.client.account_id = 1234 + self.client.call("SoftLayer_Account", "getObject") + self.client.call("SoftLayer_Account", "getObject1", id=9999) + + _call.assert_has_calls([ + mock.call(self.client, 'SoftLayer_Account', 'getObject', id=1234), + mock.call(self.client, 'SoftLayer_Account', 'getObject1', id=9999), + ]) \ No newline at end of file From f7c5146149b76447e13b1333f40b8e6a59a29c85 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 2 Jun 2022 16:47:38 -0500 Subject: [PATCH 08/29] typo fix --- SoftLayer/CLI/core.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 013796f4d..56bcd37a2 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -102,7 +102,7 @@ def get_version_message(ctx, param, value): help="Use demo data instead of actually making API calls") @click.option('--version', is_flag=True, expose_value=False, is_eager=True, callback=get_version_message, help="Show version information.", allow_from_autoenv=False,) -@click.option('-a', '--account', help="Account Id") +@click.option('--account', '-a', help="Account Id") @environment.pass_env def cli(env, format='table', @@ -111,7 +111,7 @@ def cli(env, proxy=None, really=False, demo=False, - account_id=None, + account=None, **kwargs): """Main click CLI entry-point.""" @@ -134,7 +134,8 @@ def cli(env, env.vars['_timings'] = SoftLayer.DebugTransport(env.client.transport) env.vars['verbose'] = verbose env.client.transport = env.vars['_timings'] - env.client.account_id = account_id + print("Account ID is now: {}".format(account)) + env.client.account_id = account @cli.result_callback() From d8714dd1af3fbeadb8dfbbd9bf6ca90cf63f0151 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 2 Jun 2022 16:58:30 -0500 Subject: [PATCH 09/29] Added config defaults for userid and access_token --- SoftLayer/API.py | 10 +++++----- SoftLayer/config.py | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index d212daff5..944bb16c1 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -226,13 +226,15 @@ def employee_client(username=None, if access_token is None: access_token = settings.get('access_token') - user_id = settings.get('user_id') + user_id = settings.get('userid') # Assume access_token is valid for now, user has logged in before at least. if access_token and user_id: auth = slauth.EmployeeAuthentication(user_id, access_token) return EmployeeClient(auth=auth, transport=transport) else: + # This is for logging in mostly. + LOGGER.info("No access_token or userid found in settings, creating a No Auth client for now.") return EmployeeClient(auth=None, transport=transport) @@ -670,7 +672,7 @@ def authenticate_with_password(self, username, password, security_token=None): self.settings['softlayer']['access_token'] = auth_result['hash'] - self.settings['softlayer']['userId'] = str(auth_result['userId']) + self.settings['softlayer']['userid'] = str(auth_result['userId']) # self.settings['softlayer']['refresh_token'] = tokens['refresh_token'] config.write_config(self.settings, self.config_file) @@ -691,8 +693,6 @@ def authenticate_with_hash(self, userId, access_token): def refresh_token(self, userId, auth_token): """Refreshes the login token""" - self.auth = None - # Go directly to base client, to avoid infite loop if the token is super expired. auth_result = BaseClient.call(self, 'SoftLayer_User_Employee', 'refreshEncryptedToken', auth_token, id=userId) if len(auth_result) > 1: @@ -719,7 +719,7 @@ def call(self, service, method, *args, **kwargs): return BaseClient.call(self, service, method, *args, **kwargs) except exceptions.SoftLayerAPIError as ex: if ex.faultCode == "SoftLayer_Exception_EncryptedToken_Expired": - userId = self.settings['softlayer'].get('userId') + userId = self.settings['softlayer'].get('userid') access_token = self.settings['softlayer'].get('access_token') LOGGER.warning("Token has expired, trying to refresh. %s", ex.faultString) self.refresh_token(userId, access_token) diff --git a/SoftLayer/config.py b/SoftLayer/config.py index 5ae8c7131..d909d3d49 100644 --- a/SoftLayer/config.py +++ b/SoftLayer/config.py @@ -59,6 +59,8 @@ def get_client_settings_config_file(**kwargs): # pylint: disable=inconsistent-r 'endpoint_url': '', 'timeout': '0', 'proxy': '', + 'userid': '', + 'access_token': '' }) config.read(config_files) @@ -69,6 +71,8 @@ def get_client_settings_config_file(**kwargs): # pylint: disable=inconsistent-r 'proxy': config.get('softlayer', 'proxy'), 'username': config.get('softlayer', 'username'), 'api_key': config.get('softlayer', 'api_key'), + 'userid': config.get('softlayer', 'userid'), + 'access_token': config.get('softlayer', 'access_token'), } @@ -109,6 +113,8 @@ def get_config(config_file=None): config['softlayer']['endpoint_url'] = '' config['softlayer']['api_key'] = '' config['softlayer']['timeout'] = '0' + config['softlayer']['userid'] = '' + config['softlayer']['access_tokne'] = '' return config From ac3b1e09a0ef0e88fdd2689acf71cea0212476d7 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 7 Jun 2022 11:26:55 -0500 Subject: [PATCH 10/29] updating readme and setup.py --- README.rst | 112 +++++++++++++++++------------------------ SoftLayer/CLI/login.py | 1 - setup.py | 15 +++--- 3 files changed, 52 insertions(+), 76 deletions(-) diff --git a/README.rst b/README.rst index 3cb2219f8..af53ab3e9 100644 --- a/README.rst +++ b/README.rst @@ -1,43 +1,16 @@ SoftLayer API Python Client =========================== -.. image:: https://github.com/softlayer/softlayer-python/workflows/Tests/badge.svg - :target: https://github.com/softlayer/softlayer-python/actions?query=workflow%3ATests -.. image:: https://github.com/softlayer/softlayer-python/workflows/documentation/badge.svg - :target: https://github.com/softlayer/softlayer-python/actions?query=workflow%3Adocumentation +This library is provided `as is` to make internal IMS API calls. You are responsible for your API usage, and any abuse, intentional or accidental, will result in your employee account being locked or limited. -.. image:: https://landscape.io/github/softlayer/softlayer-python/master/landscape.svg - :target: https://landscape.io/github/softlayer/softlayer-python/master - -.. image:: https://badge.fury.io/py/SoftLayer.svg - :target: http://badge.fury.io/py/SoftLayer - -.. image:: https://coveralls.io/repos/github/softlayer/softlayer-python/badge.svg?branch=master - :target: https://coveralls.io/github/softlayer/softlayer-python?branch=master - -.. image:: https://snapcraft.io//slcli/badge.svg - :target: https://snapcraft.io/slcli - - -This library provides a simple Python client to interact with `SoftLayer's -XML-RPC API `_. - -A command-line interface is also included and can be used to manage various -SoftLayer products and services. +Make sure you use the HTTPS url `https://internal.app0lb.dal10.softlayer.local/v3/internal/xmlrpc/` Documentation ------------- -Documentation for the Python client is available at `Read the Docs `_ . +DThis project is based off the `SLCLI `_ , and most things that work there will work here. -Additional API documentation can be found on the SoftLayer Development Network: - -* `SoftLayer API reference - `_ -* `Object mask information and examples - `_ -* `Code Examples - `_ +There is no internal API documentation like SLDN. Installation ------------ @@ -45,41 +18,36 @@ Install via pip: .. code-block:: bash - $ pip install softlayer - - -Or you can install from source. Download source and run: + $ git clone https://github.ibm.com/SoftLayer/internal-softlayer-cli + $ cd internal-softlayer-cli + $ python setup.py install + $ ./islcli login -.. code-block:: bash - $ python setup.py install +Configuration +------------- -Another (safer) method of installation is to use the published snap. Snaps are available for any Linux OS running snapd, the service that runs and manage snaps. Snaps are "auto-updating" packages and will not disrupt the current versions of libraries and software packages on your Linux-based system. To learn more, please visit: https://snapcraft.io/ +The config file is located at `~/.softlayer` or `~/AppData/Roaming/softlayer` for Windows systems. -To install the slcli snap: +Your config file should look something like this for using the islcli. Beware the `islcli` and `slcli` use the same config for the moment. You need to set `verify = False` in the config because the internal endpoint uses a self-signed SSL certificate. .. code-block:: bash - - $ sudo snap install slcli - - (or to get the latest release) - - $ sudo snap install slcli --edge - - + + [softlayer] + username = imsUsername + verify = False + endpoint_url = https://internal.app0lb.dal10.softlayer.local/v3/internal/xmlrpc/ -The most up-to-date version of this library can be found on the SoftLayer -GitHub public repositories at http://github.com/softlayer. For questions regarding the use of this library please post to Stack Overflow at https://stackoverflow.com/ and your posts with “SoftLayer” so our team can easily find your post. To report a bug with this library please create an Issue on github. - -InsecurePlatformWarning Notice ------------------------------- -This library relies on the `requests `_ library to make HTTP requests. On Python versions below Python 2.7.9, requests has started emitting a security warning (InsecurePlatformWarning) due to insecurities with creating SSL connections. To resolve this, upgrade to Python 2.7.9+ or follow the instructions here: http://stackoverflow.com/a/29099439. Basic Usage ----------- -- `The Complete Command Directory `_ +.. code-block:: bash + + $ islcli login + $ islcli -a vs list + Advanced Usage -------------- @@ -90,19 +58,18 @@ You can automatically set some parameters via environment variables with by usin $ export SLCLI_VERBOSE=3 $ export SLCLI_FORMAT=json - $ slcli vs list + $ slcli -a vs list is equivalent to .. code-block:: bash - $ slcli -vvv --format=json vs list + $ slcli -vvv --format=json -a vs list Getting Help ------------ -Bugs and feature requests about this library should have a `GitHub issue `_ opened about them. -Issues with the Softlayer API itself should be addressed by opening a ticket. +Feel free to open an issue if you think there is a bug, or a feature you want. Or asking in #sl-api on IBM slack. This is considered an unofficial project however, so questions might take some time to get answered. Examples @@ -110,6 +77,21 @@ Examples A curated list of examples on how to use this library can be found at `SLDN `_ + +.. code-block:: python + + import SoftLayer + client = SoftLayer.employee_client() + username = input("Username:") + password = input("Password:") + yubikey = input("Yubi key:") + client.authenticate_with_password(username, password, yubikey) + result = client.call('SoftLayer_Account', 'getObject', id="12345", mask="mask[id]") + + +After logging in with `authenticate_with_password` the EmployeeClient will try to automatically refresh the login token when it gets a TokenExpired exception. It will also record the token in the config file for future use in the CLI. + + Debugging --------- To get the exact API call that this library makes, you can do the following. @@ -131,7 +113,7 @@ If you are using the library directly in python, you can do something like this. class invoices(): def __init__(self): - self.client = SoftLayer.Client() + self.client = SoftLayer.EmployeeClient() debugger = SoftLayer.DebugTransport(self.client.transport) self.client.transport = debugger @@ -153,16 +135,13 @@ If you are using the library directly in python, you can do something like this. System Requirements ------------------- -* Python 3.5, 3.6, 3.7, 3.8, or 3.9. -* A valid SoftLayer API username and key. -* A connection to SoftLayer's private network is required to use - our private network API endpoints. +* Python 3.7, 3.8, or 3.9. +* A valid SoftLayer Employee API username, password, Yubi Key +* A connection to SoftLayer's Employee VPN Python 2.7 Support ------------------ -As of version 5.8.0 SoftLayer-Python will no longer support python2.7, which is `End Of Life as of 2020 `_ . -If you cannot install python 3.6+ for some reason, you will need to use a version of softlayer-python <= 5.7.2 - +Python 2.7 is `End Of Life as of 2020 `_ . Its not supported, you will need to upgrade to python 3.7 at least. Python Packages @@ -173,6 +152,7 @@ Python Packages * prompt_toolkit >= 2 * pygments >= 2.0.0 * urllib3 >= 1.24 +* Rich Copyright --------- diff --git a/SoftLayer/CLI/login.py b/SoftLayer/CLI/login.py index 13afe1e8a..9b9fbe887 100644 --- a/SoftLayer/CLI/login.py +++ b/SoftLayer/CLI/login.py @@ -33,7 +33,6 @@ def cli(env): username = settings.get('username') or os.environ.get('SLCLI_USER', None) password = os.environ.get('SLCLI_PASSWORD', '') yubi = None -# client = EmployeeClient(config_file=env.config_file) client = env.client # Might already be logged in, try and refresh token diff --git a/setup.py b/setup.py index fc233a095..683daa914 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ # pylint: disable=inconsistent-return-statements -DESCRIPTION = "A library for SoftLayer's API" +DESCRIPTION = "A library for SoftLayer's IMS API" if os.path.exists('README.rst'): with codecs.open('README.rst', 'r', 'utf-8') as readme_file: @@ -15,7 +15,7 @@ LONG_DESCRIPTION = DESCRIPTION setup( - name='SoftLayer', + name='SoftLayer-Internal', version='6.0.2', description=DESCRIPTION, long_description=LONG_DESCRIPTION, @@ -25,14 +25,13 @@ packages=find_packages(exclude=['tests']), license='MIT', zip_safe=False, - url='http://github.com/softlayer/softlayer-python', + url='https://github.ibm.com/SoftLayer/internal-softlayer-cli', entry_points={ 'console_scripts': [ - 'slcli = SoftLayer.CLI.core:main', - 'sl = SoftLayer.CLI.deprecated:main', + 'islcli = SoftLayer.CLI.core:main', ], }, - python_requires='>=3.6', + python_requires='>=3.7', install_requires=[ 'prettytable >= 2.5.0', 'click >= 8.0.4', @@ -42,7 +41,7 @@ 'urllib3 >= 1.24', 'rich == 12.3.0' ], - keywords=['softlayer', 'cloud', 'slcli'], + keywords=['islcli'], classifiers=[ 'Environment :: Console', 'Environment :: Web Environment', @@ -51,8 +50,6 @@ 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Topic :: Software Development :: Libraries :: Python Modules', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', From b8c548b40fa570661ce5676688028c0b5a132901 Mon Sep 17 00:00:00 2001 From: CHRISTOPHER GALLO Date: Tue, 7 Jun 2022 11:27:55 -0500 Subject: [PATCH 11/29] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index af53ab3e9..6def08c28 100644 --- a/README.rst +++ b/README.rst @@ -18,7 +18,7 @@ Install via pip: .. code-block:: bash - $ git clone https://github.ibm.com/SoftLayer/internal-softlayer-cli + $ git clone https://github.ibm.com/SoftLayer/internal-softlayer-cli $ cd internal-softlayer-cli $ python setup.py install $ ./islcli login From 11458c734daf6d734f62e74c00e74bca05718f94 Mon Sep 17 00:00:00 2001 From: Robert Houtenbrink Date: Tue, 28 Jun 2022 11:24:57 -0500 Subject: [PATCH 12/29] Update README.rst --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 6def08c28..c946c3572 100644 --- a/README.rst +++ b/README.rst @@ -32,12 +32,12 @@ The config file is located at `~/.softlayer` or `~/AppData/Roaming/softlayer` fo Your config file should look something like this for using the islcli. Beware the `islcli` and `slcli` use the same config for the moment. You need to set `verify = False` in the config because the internal endpoint uses a self-signed SSL certificate. .. code-block:: bash - [softlayer] - username = imsUsername + timeout = 600 verify = False - endpoint_url = https://internal.app0lb.dal10.softlayer.local/v3/internal/xmlrpc/ - + username = imsUsername + endpoint_url = http://internal.applb.dal10.softlayer.local/v3.1/internal/xmlrpc/ + userid = imsUserId Basic Usage From 5f8c0aa80845ef28b05d6450b7566bbd10fe77f8 Mon Sep 17 00:00:00 2001 From: Robert Houtenbrink Date: Tue, 28 Jun 2022 11:25:29 -0500 Subject: [PATCH 13/29] Update login.py --- SoftLayer/CLI/login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/login.py b/SoftLayer/CLI/login.py index 9b9fbe887..8b0efa61e 100644 --- a/SoftLayer/CLI/login.py +++ b/SoftLayer/CLI/login.py @@ -56,7 +56,7 @@ def cli(env): if username is None: username = input("Username: ") click.echo("Username: {}".format(username)) - if password is None: + if not password: password = env.getpass("Password: ") click.echo("Password: {}".format(censor_password(password))) yubi = input("Yubi: ") From 6a66a03706041cbc3fe04a1eff1a26973fcb4759 Mon Sep 17 00:00:00 2001 From: Robert Houtenbrink Date: Tue, 28 Jun 2022 12:01:14 -0500 Subject: [PATCH 14/29] Update README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index c946c3572..e98cb5b30 100644 --- a/README.rst +++ b/README.rst @@ -32,14 +32,14 @@ The config file is located at `~/.softlayer` or `~/AppData/Roaming/softlayer` fo Your config file should look something like this for using the islcli. Beware the `islcli` and `slcli` use the same config for the moment. You need to set `verify = False` in the config because the internal endpoint uses a self-signed SSL certificate. .. code-block:: bash - [softlayer] + + [softlayer] timeout = 600 verify = False username = imsUsername endpoint_url = http://internal.applb.dal10.softlayer.local/v3.1/internal/xmlrpc/ userid = imsUserId - Basic Usage ----------- From 7997ae11da0d5d590c4d2ee0265c4386d340a8c3 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 3 Feb 2023 16:11:20 -0600 Subject: [PATCH 15/29] a few updates to make this islcli work out of the box --- SoftLayer/API.py | 4 +++- SoftLayer/CLI/login.py | 1 + SoftLayer/auth.py | 12 ++++++++---- SoftLayer/consts.py | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 944bb16c1..2d0208455 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -154,7 +154,7 @@ def employee_client(username=None, proxy=None, user_agent=None, transport=None, - verify=True): + verify=False): """Creates an INTERNAL SoftLayer API client using your environment. Settings are loaded via keyword arguments, environemtal variables and @@ -187,6 +187,7 @@ def employee_client(username=None, 'Your Company' """ + # SSL verification is OFF because internal api uses a self signed cert settings = config.get_client_settings(username=username, api_key=None, endpoint_url=endpoint_url, @@ -381,6 +382,7 @@ def call(self, service, method, *args, **kwargs): request.filter = kwargs.get('filter') request.limit = kwargs.get('limit') request.offset = kwargs.get('offset') + request.url = self.settings['softlayer'].get('endpoint_url') if kwargs.get('verify') is not None: request.verify = kwargs.get('verify') diff --git a/SoftLayer/CLI/login.py b/SoftLayer/CLI/login.py index 8b0efa61e..fbef47498 100644 --- a/SoftLayer/CLI/login.py +++ b/SoftLayer/CLI/login.py @@ -8,6 +8,7 @@ from SoftLayer.API import EmployeeClient from SoftLayer.CLI.command import SLCommand as SLCommand from SoftLayer import config +from SoftLayer import consts from SoftLayer.CLI import environment diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index 8811c77d2..9786273bf 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -151,10 +151,14 @@ def __init__(self, user_id, user_hash): def get_request(self, request): """Sets token-based auth headers.""" - request.headers['employeesession'] = { - 'userId': self.user_id, - 'authToken': self.hash, - } + if 'xml' in request.url: + request.headers['employeesession'] = { + 'userId': self.user_id, + 'authToken': self.hash, + } + else: + request.transport_user = self.user_id + request.transport_password = self.hash return request def __repr__(self): diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 5ac84b099..fb65b4640 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -10,6 +10,6 @@ API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' API_PRIVATE_ENDPOINT_REST = 'https://api.service.softlayer.com/rest/v3.1/' -API_EMPLOYEE_ENDPOINT = 'https://internal.app0lb.dal10.softlayer.local/v3/internal/xmlrpc/' +API_EMPLOYEE_ENDPOINT = 'https://internal.applb.dal10.softlayer.local/v3/internal/xmlrpc/' USER_AGENT = "softlayer-python/%s" % VERSION CONFIG_FILE = "~/.softlayer" From 1ba7da6b2ee4f6605e041519f62946413017d9b7 Mon Sep 17 00:00:00 2001 From: CHRISTOPHER GALLO Date: Thu, 16 Mar 2023 14:49:09 -0500 Subject: [PATCH 16/29] Update setup.py fixed merge conflict --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index f7e0c6675..d90bcdaf6 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,6 @@ LONG_DESCRIPTION = DESCRIPTION setup( -<<<<<<< HEAD name='SoftLayer-Internal', version='6.1.3', description=DESCRIPTION, From 0ac0bd810983b7196162fb220e345ff03d165f75 Mon Sep 17 00:00:00 2001 From: CHRISTOPHER GALLO Date: Thu, 16 Mar 2023 14:50:32 -0500 Subject: [PATCH 17/29] Update README.rst fixed a typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d064ae637..a06f5bc0c 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ Make sure you use the HTTPS url `https://internal.app0lb.dal10.softlayer.local/v Documentation ------------- -DThis project is based off the `SLCLI `_ , and most things that work there will work here. +This project is based off the `SLCLI `_ , and most things that work there will work here. There is no internal API documentation like SLDN. From b63a7a13247e78287887723ada6ab86508709530 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 23 Apr 2024 17:05:06 -0500 Subject: [PATCH 18/29] Adding SSL authentication support for internal users --- SoftLayer/auth.py | 21 +++++++++++++++++++++ SoftLayer/transports/transport.py | 5 ++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index 1c16ef51b..59f899f38 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -12,6 +12,7 @@ 'TokenAuthentication', 'BasicHTTPAuthentication', 'AuthenticationBase', + 'X509Authentication' ] @@ -137,3 +138,23 @@ def get_request(self, request): def __repr__(self): return f"BearerAuthentication(username={self.username}, token={self.api_key})" + +class X509Authentication(AuthenticationBase): + """X509Authentication authentication class. + + :param certificate str: Path to a users SSL certificate for authentication + :param CA Cert str: Path to the CA bundle for softlayer hostnames. + """ + + def __init__(self, cert, ca_cert): + self.cert = cert + self.ca_cert = ca_cert + + def get_request(self, request): + """Sets token-based auth headers.""" + request.cert = self.cert + request.ca_cert = self.ca_cert + return request + + def __repr__(self): + return f"X509Authentication(cert={cert}, ca_cert={ca_cert})" \ No newline at end of file diff --git a/SoftLayer/transports/transport.py b/SoftLayer/transports/transport.py index 616e20cf8..1da88d4d2 100644 --- a/SoftLayer/transports/transport.py +++ b/SoftLayer/transports/transport.py @@ -59,9 +59,12 @@ def __init__(self): #: Boolean specifying if the server certificate should be verified. self.verify = None - #: Client certificate file path. + #: Client certificate file path. (Used by X509Authentication) self.cert = None + #: CA certificate for softlayer domains (Used by X509Authentication) + self.ca_cert = None + #: InitParameter/identifier of an object. self.identifier = None From 0cd26845e8d4c7f529c822db0129a258743d3ae1 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 23 Apr 2024 19:21:02 -0500 Subject: [PATCH 19/29] Allowing to set verify to a server cert --- SoftLayer/auth.py | 2 +- SoftLayer/transports/transport.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index 59f899f38..e5daee7fd 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -153,7 +153,7 @@ def __init__(self, cert, ca_cert): def get_request(self, request): """Sets token-based auth headers.""" request.cert = self.cert - request.ca_cert = self.ca_cert + request.verify = self.ca_cert return request def __repr__(self): diff --git a/SoftLayer/transports/transport.py b/SoftLayer/transports/transport.py index 1da88d4d2..2a804cbaf 100644 --- a/SoftLayer/transports/transport.py +++ b/SoftLayer/transports/transport.py @@ -62,9 +62,6 @@ def __init__(self): #: Client certificate file path. (Used by X509Authentication) self.cert = None - #: CA certificate for softlayer domains (Used by X509Authentication) - self.ca_cert = None - #: InitParameter/identifier of an object. self.identifier = None From 1bc264b157f5b844ba39c4f23daa587d88c07925 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 23 Apr 2024 20:53:52 -0500 Subject: [PATCH 20/29] Added unit tests --- SoftLayer/auth.py | 4 ++-- tests/auth_tests.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index e5daee7fd..62dc969f6 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -143,7 +143,7 @@ class X509Authentication(AuthenticationBase): """X509Authentication authentication class. :param certificate str: Path to a users SSL certificate for authentication - :param CA Cert str: Path to the CA bundle for softlayer hostnames. + :param CA Cert str: Path to the Servers signed certificate. """ def __init__(self, cert, ca_cert): @@ -157,4 +157,4 @@ def get_request(self, request): return request def __repr__(self): - return f"X509Authentication(cert={cert}, ca_cert={ca_cert})" \ No newline at end of file + return f"X509Authentication(cert={self.cert}, ca_cert={self.ca_cert})" \ No newline at end of file diff --git a/tests/auth_tests.py b/tests/auth_tests.py index 6bac999f9..cdff444f9 100644 --- a/tests/auth_tests.py +++ b/tests/auth_tests.py @@ -83,3 +83,22 @@ def test_repr(self): s = repr(self.auth) self.assertIn('BasicHTTPAuthentication', s) self.assertIn('USERNAME', s) + + +class TestX509AUthentication(testing.TestCase): + def set_up(self): + self.auth = auth.X509Authentication('authcert.pm', 'servercert.pm') + + def test_attribs(self): + self.assertEqual(self.auth.cert, 'authcert.pm') + self.assertEqual(self.auth.ca_cert, 'servercert.pm') + + def test_get_request(self): + req = transports.Request() + authed_req = self.auth.get_request(req) + self.assertEqual(authed_req.cert, 'authcert.pm') + self.assertEqual(authed_req.verify, 'servercert.pm') + + def test_repr(self): + s = repr(self.auth) + self.assertEqual(s, "X509Authentication(cert=authcert.pm, ca_cert=servercert.pm)") \ No newline at end of file From 317434ae258b2877a56766443fe511a90b3fad13 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 23 Apr 2024 21:07:47 -0500 Subject: [PATCH 21/29] Fixed Tox issues --- SoftLayer/auth.py | 3 ++- tests/auth_tests.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/SoftLayer/auth.py b/SoftLayer/auth.py index 62dc969f6..72ff77312 100644 --- a/SoftLayer/auth.py +++ b/SoftLayer/auth.py @@ -139,6 +139,7 @@ def get_request(self, request): def __repr__(self): return f"BearerAuthentication(username={self.username}, token={self.api_key})" + class X509Authentication(AuthenticationBase): """X509Authentication authentication class. @@ -157,4 +158,4 @@ def get_request(self, request): return request def __repr__(self): - return f"X509Authentication(cert={self.cert}, ca_cert={self.ca_cert})" \ No newline at end of file + return f"X509Authentication(cert={self.cert}, ca_cert={self.ca_cert})" diff --git a/tests/auth_tests.py b/tests/auth_tests.py index cdff444f9..cd8ffaa65 100644 --- a/tests/auth_tests.py +++ b/tests/auth_tests.py @@ -101,4 +101,4 @@ def test_get_request(self): def test_repr(self): s = repr(self.auth) - self.assertEqual(s, "X509Authentication(cert=authcert.pm, ca_cert=servercert.pm)") \ No newline at end of file + self.assertEqual(s, "X509Authentication(cert=authcert.pm, ca_cert=servercert.pm)") From c5e3fcf58e1a1f335ae3e831e3a2fd5a287eb198 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 24 Apr 2024 17:21:26 -0500 Subject: [PATCH 22/29] Cleaned up CertificateClient --- SoftLayer/API.py | 54 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 4d2918611..8d59e5bb9 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -33,6 +33,7 @@ 'API_PUBLIC_ENDPOINT', 'API_PRIVATE_ENDPOINT', 'IAMClient', + 'CertificateClient' ] VALID_CALL_ARGS = set(( @@ -144,12 +145,7 @@ def create_client_from_env(username=None, 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) + """Get a SoftLayer API Client using environmental settings.""" return create_client_from_env(**kwargs) @@ -390,6 +386,52 @@ def __repr__(self): def __len__(self): return 0 +class CertificateClient(BaseClient): + """Client that works with a X509 Certificate for authentication. + + Will read the certificate file from the config file (~/.softlayer usually). + > auth_cert = /path/to/authentication/cert.pm + > server_cert = /path/to/CAcert.pem + Set auth to a SoftLayer.auth.Authentication class to manually set authentication + """ + + def __init__(self, auth=None, transport=None, config_file=None): + if config_file is None: + config_file = CONFIG_FILE + self.config_file = config_file + self.settings = config.get_config(self.config_file) + + if auth is None: + auth_cert = self.settings['softlayer'].get('auth_cert') + serv_cert = self.settings['softlayer'].get('server_cert', None) + auth = slauth.X509Authentication(auth_cert, serv_cert) + self.auth = auth + + + + if transport is None: + url = self.settings['softlayer'].get('endpoint_url') + if url is not None and '/rest' in url: + # If this looks like a rest endpoint, use the rest transport + transport = transports.RestTransport( + endpoint_url=url, + proxy=self.settings['softlayer'].get('proxy'), + # prevents an exception incase timeout is a float number. + timeout=int(self.settings['softlayer'].getfloat('timeout', 0)), + user_agent=consts.USER_AGENT, + verify=self.settings['softlayer'].getboolean('verify'), + ) + else: + # Default the transport to use XMLRPC + transport = transports.XmlRpcTransport( + endpoint_url=url, + proxy=self.settings['softlayer'].get('proxy'), + timeout=int(self.settings['softlayer'].getfloat('timeout', 0)), + user_agent=consts.USER_AGENT, + verify=self.settings['softlayer'].getboolean('verify'), + ) + + self.transport = transport class IAMClient(BaseClient): """IBM ID Client for using IAM authentication From 5f2024d462ab9004268d9d2cc92b1a4bb00ffc39 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 24 Apr 2024 18:02:23 -0500 Subject: [PATCH 23/29] Added in employee auth to consolidate repos --- SoftLayer/API.py | 26 +++++--------------------- SoftLayer/CLI/core.py | 30 +++++++++++++----------------- SoftLayer/CLI/environment.py | 26 ++++++++++++++++++-------- SoftLayer/CLI/login.py | 4 ---- SoftLayer/consts.py | 1 - SoftLayer/transports/transport.py | 9 ++++++--- 6 files changed, 42 insertions(+), 54 deletions(-) diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 358ca2f95..9df6e88db 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -180,16 +180,7 @@ def employee_client(username=None, :param bool verify: decide to verify the server's SSL/TLS cert. DO NOT SET TO FALSE WITHOUT UNDERSTANDING THE IMPLICATIONS. - Usage: - - >>> import SoftLayer - >>> client = SoftLayer.create_client_from_env() - >>> resp = client.call('Account', 'getObject') - >>> resp['companyName'] - 'Your Company' - """ - # SSL verification is OFF because internal api uses a self signed cert settings = config.get_client_settings(username=username, api_key=None, endpoint_url=endpoint_url, @@ -198,14 +189,12 @@ def employee_client(username=None, verify=verify, config_file=config_file) - url = settings.get('endpoint_url') or consts.API_EMPLOYEE_ENDPOINT + url = settings.get('endpoint_url') if 'internal' not in url: - raise exceptions.SoftLayerError("{} does not look like an Internal Employee url. Try {}".format( - url, consts.API_EMPLOYEE_ENDPOINT)) + raise exceptions.SoftLayerError(f"{url} does not look like an Internal Employee url.") if transport is None: - if url is not None and '/rest' in url: # If this looks like a rest endpoint, use the rest transport transport = transports.RestTransport( @@ -241,7 +230,6 @@ def employee_client(username=None, return EmployeeClient(auth=None, transport=transport) - def Client(**kwargs): """Get a SoftLayer API Client using environmental settings.""" return create_client_from_env(**kwargs) @@ -251,8 +239,7 @@ 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) + :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request) """ _prefix = "SoftLayer_" @@ -288,9 +275,7 @@ def __init__(self, auth=None, transport=None, config_file=None): self.transport = transport - def authenticate_with_password(self, username, password, - security_question_id=None, - security_question_answer=None): + def authenticate_with_password(self, username, password, security_question_id=None, security_question_answer=None): """Performs Username/Password Authentication :param string username: your SoftLayer username @@ -353,8 +338,7 @@ def call(self, service, method, *args, **kwargs): invalid_kwargs = set(kwargs.keys()) - VALID_CALL_ARGS if invalid_kwargs: - raise TypeError( - 'Invalid keyword arguments: %s' % ','.join(invalid_kwargs)) + raise TypeError('Invalid keyword arguments: %s' % ','.join(invalid_kwargs)) prefixes = (self._prefix, 'BluePages_Search', 'IntegratedOfferingTeam_Region') if self._prefix and not service.startswith(prefixes): diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index e204e1216..57d81ebdb 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -73,35 +73,28 @@ def get_version_message(ctx, param, value): epilog="""Run 'islcli login' to authenticate""", cls=CommandLoader, context_settings=CONTEXT_SETTINGS) -@click.option('--format', - default=DEFAULT_FORMAT, - show_default=True, +@click.option('--format', default=DEFAULT_FORMAT, show_default=True, help="Output format", type=click.Choice(VALID_FORMATS)) -@click.option('-C', '--config', - required=False, +@click.option('-C', '--config', required=False, show_default=True, default=click.get_app_dir('softlayer', force_posix=True), - show_default=True, help="Config file location", type=click.Path(resolve_path=True)) @click.option('--verbose', '-v', help="Sets the debug noise level, specify multiple times for more verbosity.", type=click.IntRange(0, 3, clamp=True), count=True) -@click.option('--proxy', - required=False, +@click.option('--proxy', required=False, help="HTTPS or HTTP proxy to be use to make API calls") -@click.option('--really / --not-really', '-y', - is_flag=True, - required=False, +@click.option('--really / --not-really', '-y', is_flag=True, required=False, help="Confirm all prompt actions") -@click.option('--demo / --no-demo', - is_flag=True, - required=False, +@click.option('--demo / --no-demo', is_flag=True, required=False, help="Use demo data instead of actually making API calls") @click.option('--version', is_flag=True, expose_value=False, is_eager=True, callback=get_version_message, help="Show version information.", allow_from_autoenv=False,) -@click.option('--account', '-a', help="Account Id") +@click.option('--account', '-a', help="Account Id, only needed for some API calls.") +@click.option('--internal', '-i', is_flag=True, required=False, + help="Use the Employee Client instead of the Customer Client.") @environment.pass_env def cli(env, format='table', @@ -111,6 +104,7 @@ def cli(env, really=False, demo=False, account=None, + internal=False, **kwargs): """Main click CLI entry-point.""" @@ -119,7 +113,10 @@ def cli(env, env.config_file = config env.format = format env.set_env_theme(config_file=config) - env.ensure_client(config_file=config, is_demo=demo, proxy=proxy) + if internal: + env.ensure_emp_client(config_file=config, is_demo=demo, proxy=proxy) + else: + env.ensure_client(config_file=config, is_demo=demo, proxy=proxy) env.vars['_start'] = time.time() logger = logging.getLogger() @@ -134,7 +131,6 @@ def cli(env, env.vars['_timings'] = SoftLayer.DebugTransport(env.client.transport) env.vars['verbose'] = verbose env.client.transport = env.vars['_timings'] - print("Account ID is now: {}".format(account)) env.client.account_id = account diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py index 2245c4f07..e2fde6e30 100644 --- a/SoftLayer/CLI/environment.py +++ b/SoftLayer/CLI/environment.py @@ -183,16 +183,26 @@ def ensure_client(self, config_file=None, is_demo=False, proxy=None): # Environment can be passed in explicitly. This is used for testing if is_demo: - client = SoftLayer.BaseClient( - transport=SoftLayer.FixtureTransport(), - auth=None, - ) + client = SoftLayer.BaseClient(transport=SoftLayer.FixtureTransport(), auth=None) else: # Create SL Client - client = SoftLayer.employee_client( - proxy=proxy, - config_file=config_file, - ) + client = SoftLayer.create_client_from_env(proxy=proxy, config_file=config_file) + self.client = client + + def ensure_emp_client(self, config_file=None, is_demo=False, proxy=None): + """Create a new SLAPI client to the environment. + + This will be a no-op if there is already a client in this environment. + """ + if self.client is not None: + return + + # Environment can be passed in explicitly. This is used for testing + if is_demo: + client = SoftLayer.BaseClient(transport=SoftLayer.FixtureTransport(), auth=None) + else: + # Create SL Client + client = SoftLayer.employee_client(proxy=proxy, config_file=config_file) self.client = client def set_env_theme(self, config_file=None): diff --git a/SoftLayer/CLI/login.py b/SoftLayer/CLI/login.py index fbef47498..4b8717b00 100644 --- a/SoftLayer/CLI/login.py +++ b/SoftLayer/CLI/login.py @@ -12,10 +12,6 @@ from SoftLayer.CLI import environment -# def get_username(env): -# """Gets the username from config or env""" -# settings = - def censor_password(value): if value: value = '*' * len(value) diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index 944a52a75..883ba61a1 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -10,6 +10,5 @@ API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' API_PRIVATE_ENDPOINT_REST = 'https://api.service.softlayer.com/rest/v3.1/' -API_EMPLOYEE_ENDPOINT = 'https://internal.applb.dal10.softlayer.local/v3/internal/xmlrpc/' USER_AGENT = "softlayer-python/%s" % VERSION CONFIG_FILE = "~/.softlayer" diff --git a/SoftLayer/transports/transport.py b/SoftLayer/transports/transport.py index 2a804cbaf..4aa5cebab 100644 --- a/SoftLayer/transports/transport.py +++ b/SoftLayer/transports/transport.py @@ -99,9 +99,12 @@ def __repr__(self): """Prints out what this call is all about""" pretty_mask = utils.clean_string(self.mask) pretty_filter = self.filter - param_string = "id={id}, mask='{mask}', filter='{filter}', args={args}, limit={limit}, offset={offset}".format( - id=self.identifier, mask=pretty_mask, filter=pretty_filter, - args=self.args, limit=self.limit, offset=self.offset) + clean_args = self.args + # Passwords can show up here, so censor them before logging. + if self.method in ["performExternalAuthentication", "refreshEncryptedToken", "getPortalLoginToken"]: + clean_args = "*************" + param_string = (f"id={self.identifier}, mask='{pretty_mask}', filter='{pretty_filter}', args={clean_args}, " + f"limit={self.limit}, offset={self.offset}") return "{service}::{method}({params})".format( service=self.service, method=self.method, params=param_string) From dd77597429a58ccfae5d26266cebb819909b2538 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 24 Apr 2024 20:18:30 -0500 Subject: [PATCH 24/29] Fixed some tox/style/unittest issues --- .secrets.baseline | 4 +-- SoftLayer/API.py | 82 +++++++++++++++--------------------------- SoftLayer/CLI/login.py | 25 +++++++------ tests/api_tests.py | 21 +++++------ 4 files changed, 51 insertions(+), 81 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index d236a8ed1..ea850e071 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2024-04-18T01:09:09Z", + "generated_at": "2024-04-25T01:18:20Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -554,7 +554,7 @@ "hashed_secret": "a4c805a62a0387010cd172cfed6f6772eb92a5d6", "is_secret": false, "is_verified": false, - "line_number": 76, + "line_number": 81, "type": "Secret Keyword", "verified_result": null } diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 9df6e88db..622211105 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -6,9 +6,7 @@ :license: MIT, see LICENSE for more details. """ # pylint: disable=invalid-name -import os import time -import warnings import concurrent.futures as cf import json @@ -148,7 +146,6 @@ def create_client_from_env(username=None, def employee_client(username=None, access_token=None, - password=None, endpoint_url=None, timeout=None, auth=None, @@ -159,27 +156,22 @@ def employee_client(username=None, verify=False): """Creates an INTERNAL SoftLayer API client using your environment. - Settings are loaded via keyword arguments, environemtal variables and - config file. + Settings are loaded via keyword arguments, environemtal variables and config file. :param username: your user ID - :param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, 2fa_string) + :param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, token) :param password: password to use for employee authentication :param endpoint_url: the API endpoint base URL you wish to connect to. - Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private - network. + Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private network. :param proxy: proxy to be used to make API calls :param integer timeout: timeout for API requests - :param auth: an object which responds to get_headers() to be inserted into - the xml-rpc headers. Example: `BasicAuthentication` + :param auth: an object which responds to get_headers() to be inserted into the xml-rpc headers. + Example: `BasicAuthentication` :param config_file: A path to a configuration file used to load settings :param user_agent: an optional User Agent to report when making API 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. - + :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. """ settings = config.get_client_settings(username=username, api_key=None, @@ -214,7 +206,6 @@ def employee_client(username=None, verify=verify, ) - if access_token is None: access_token = settings.get('access_token') @@ -241,16 +232,23 @@ class BaseClient(object): :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_" + auth: slauth.AuthenticationBase def __init__(self, auth=None, transport=None, config_file=None): if config_file is None: config_file = CONFIG_FILE - self.auth = auth self.config_file = config_file self.settings = config.get_config(self.config_file) + self.__setAuth(auth) + self.__setTransport(transport) + + def __setAuth(self, auth=None): + """Prepares the authentication property""" + self.auth = auth + def __setTransport(self, transport=None): + """Prepares the transport property""" if transport is None: url = self.settings['softlayer'].get('endpoint_url') if url is not None and '/rest' in url: @@ -469,6 +467,7 @@ def __repr__(self): def __len__(self): return 0 + class CertificateClient(BaseClient): """Client that works with a X509 Certificate for authentication. @@ -479,42 +478,20 @@ class CertificateClient(BaseClient): """ def __init__(self, auth=None, transport=None, config_file=None): - if config_file is None: - config_file = CONFIG_FILE - self.config_file = config_file - self.settings = config.get_config(self.config_file) + BaseClient.__init__(self, auth, transport, config_file) + self.__setAuth(auth) + def __setAuth(self, auth=None): + """Prepares the authentication property""" if auth is None: auth_cert = self.settings['softlayer'].get('auth_cert') serv_cert = self.settings['softlayer'].get('server_cert', None) auth = slauth.X509Authentication(auth_cert, serv_cert) - self.auth = auth - - + self.auth = auth - if transport is None: - url = self.settings['softlayer'].get('endpoint_url') - if url is not None and '/rest' in url: - # If this looks like a rest endpoint, use the rest transport - transport = transports.RestTransport( - endpoint_url=url, - proxy=self.settings['softlayer'].get('proxy'), - # prevents an exception incase timeout is a float number. - timeout=int(self.settings['softlayer'].getfloat('timeout', 0)), - user_agent=consts.USER_AGENT, - verify=self.settings['softlayer'].getboolean('verify'), - ) - else: - # Default the transport to use XMLRPC - transport = transports.XmlRpcTransport( - endpoint_url=url, - proxy=self.settings['softlayer'].get('proxy'), - timeout=int(self.settings['softlayer'].getfloat('timeout', 0)), - user_agent=consts.USER_AGENT, - verify=self.settings['softlayer'].getboolean('verify'), - ) + def __repr__(self): + return "CertificateClient(transport=%r, auth=%r)" % (self.transport, self.auth) - self.transport = transport class IAMClient(BaseClient): """IBM ID Client for using IAM authentication @@ -711,9 +688,8 @@ def __init__(self, auth=None, transport=None, config_file=None, account_id=None) BaseClient.__init__(self, auth, transport, config_file) self.account_id = account_id - - def authenticate_with_password(self, username, password, security_token=None): - """Performs IBM IAM Username/Password Authentication + def authenticate_with_internal(self, username, password, security_token=None): + """Performs internal authentication :param string username: your softlayer username :param string password: your softlayer password @@ -724,12 +700,11 @@ def authenticate_with_password(self, username, password, security_token=None): if security_token is None: security_token = input("Enter your 2FA Token now: ") if len(security_token) != 6: - raise Exception("Invalid security token: {}".format(security_token)) + raise exceptions.SoftLayerAPIError("Invalid security token: {}".format(security_token)) auth_result = self.call('SoftLayer_User_Employee', 'performExternalAuthentication', username, password, security_token) - self.settings['softlayer']['access_token'] = auth_result['hash'] self.settings['softlayer']['userid'] = str(auth_result['userId']) # self.settings['softlayer']['refresh_token'] = tokens['refresh_token'] @@ -739,8 +714,6 @@ def authenticate_with_password(self, username, password, security_token=None): return auth_result - - def authenticate_with_hash(self, userId, access_token): """Authenticates to the Internal SL API with an employee userid + token @@ -791,6 +764,7 @@ def call(self, service, method, *args, **kwargs): def __repr__(self): return "EmployeeClient(transport=%r, auth=%r)" % (self.transport, self.auth) + class Service(object): """A SoftLayer Service. diff --git a/SoftLayer/CLI/login.py b/SoftLayer/CLI/login.py index 4b8717b00..3317e8945 100644 --- a/SoftLayer/CLI/login.py +++ b/SoftLayer/CLI/login.py @@ -1,28 +1,28 @@ """Login with your employee username, password, 2fa token""" # :license: MIT, see LICENSE for more details. +import os import click -import os - -from SoftLayer.API import EmployeeClient from SoftLayer.CLI.command import SLCommand as SLCommand +from SoftLayer.CLI import environment from SoftLayer import config from SoftLayer import consts -from SoftLayer.CLI import environment def censor_password(value): + """Replaces a password with *s""" if value: value = '*' * len(value) return value + @click.command(cls=SLCommand) @environment.pass_env def cli(env): """Logs you into the internal SoftLayer Network. - username: Set this in either the softlayer config, or SL_USER ENV variable + username: Set this in either the softlayer config, or SL_USER ENV variable password: Set this in SL_PASSWORD env variable. You will be prompted for them otherwise. """ config_settings = config.get_config(config_file=env.config_file) @@ -36,18 +36,18 @@ def cli(env): if settings.get('access_token') and settings.get('userid'): client.authenticate_with_hash(settings.get('userid'), settings.get('access_token')) try: - employee = client.call('SoftLayer_User_Employee', 'getObject', id=settings.get('userid'), mask="mask[id,username]") - print(employee) - client.refresh_token(settings.get('userid'), settings.get('access_token')) - refresh = client.call('SoftLayer_User_Employee', 'refreshEncryptedToken', settings.get('access_token'), id=settings.get('userid')) + emp_id = settings.get('userid') + client.call('SoftLayer_User_Employee', 'getObject', id=emp_id, mask="mask[id,username]") + client.refresh_token(emp_id, settings.get('access_token')) + client.call('SoftLayer_User_Employee', 'refreshEncryptedToken', settings.get('access_token'), id=emp_id) config_settings['softlayer'] = settings config.write_config(config_settings, env.config_file) return + # pylint: disable=broad-exception-caught except Exception as ex: print("Error with Hash Authentication, try with password: {}".format(ex)) - url = settings.get('endpoint_url') or consts.API_EMPLOYEE_ENDPOINT click.echo("URL: {}".format(url)) if username is None: @@ -57,11 +57,10 @@ def cli(env): password = env.getpass("Password: ") click.echo("Password: {}".format(censor_password(password))) yubi = input("Yubi: ") - - try: - result = client.authenticate_with_password(username, password, str(yubi)) + result = client.authenticate_with_internal(username, password, str(yubi)) print(result) + # pylint: disable=broad-exception-caught except Exception as e: click.echo("EXCEPTION: {}".format(e)) diff --git a/tests/api_tests.py b/tests/api_tests.py index 2e1ab0fae..438d77020 100644 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -6,15 +6,15 @@ """ import io import os -from unittest import mock as mock import requests +from unittest import mock as mock import SoftLayer import SoftLayer.API +from SoftLayer import auth as slauth +from SoftLayer import exceptions from SoftLayer import testing from SoftLayer import transports -from SoftLayer import exceptions -from SoftLayer import auth as slauth class Initialization(testing.TestCase): @@ -319,7 +319,6 @@ def test_authenticate_with_password(self, _call): class EmployeeClientTests(testing.TestCase): - @staticmethod def setup_response(filename, status_code=200, total_items=1): basepath = os.path.dirname(__file__) @@ -333,7 +332,6 @@ def setup_response(filename, status_code=200, total_items=1): response.status_code = status_code return response - def set_up(self): self.client = SoftLayer.API.EmployeeClient(config_file='./tests/testconfig') @@ -348,7 +346,7 @@ def test_auth_with_pass_failure(self, api_response): @mock.patch('SoftLayer.transports.xmlrpc.requests.Session.request') def test_auth_with_pass_success(self, api_response): api_response.return_value = self.setup_response('successLogin') - result = self.client.authenticate_with_password('testUser', 'testPassword', '123456') + result = self.client.authenticate_with_internal('testUser', 'testPassword', '123456') print(result) self.assertEqual(result['userId'], 1234) self.assertEqual(self.client.settings['softlayer']['userid'], '1234') @@ -363,10 +361,10 @@ def test_auth_with_hash(self): @mock.patch('SoftLayer.transports.xmlrpc.requests.Session.request') def test_refresh_token(self, api_response): api_response.return_value = self.setup_response('refreshSuccess') - result = self.client.refresh_token(9999, 'qweasdzxcqweasdzxcqweasdzxc') + self.client.refresh_token(9999, 'qweasdzxcqweasdzxcqweasdzxc') self.assertEqual(self.client.auth.user_id, 9999) self.assertIn('REFRESHEDTOKENaaaa', self.client.auth.hash) - + @mock.patch('SoftLayer.transports.xmlrpc.requests.Session.request') def test_expired_token_is_refreshed(self, api_response): api_response.side_effect = [ @@ -379,9 +377,9 @@ def test_expired_token_is_refreshed(self, api_response): result = self.client.call('SoftLayer_User_Employee', 'getObject', id=5555) self.assertIn('REFRESHEDTOKENaaaa', self.client.auth.hash) self.assertEqual('testUser', result['username']) - + @mock.patch('SoftLayer.transports.xmlrpc.requests.Session.request') - def test_expired_token_is_really_expored(self, api_response): + def test_expired_token_is_really_expired(self, api_response): api_response.side_effect = [ self.setup_response('expiredToken'), self.setup_response('expiredToken') @@ -391,7 +389,6 @@ def test_expired_token_is_really_expored(self, api_response): exception = self.assertRaises( exceptions.SoftLayerAPIError, self.client.call, 'SoftLayer_User_Employee', 'getObject', id=5555) - self.assertEqual(None, self.client.auth) self.assertEqual(exception.faultCode, "SoftLayer_Exception_EncryptedToken_Expired") @mock.patch('SoftLayer.API.BaseClient.call') @@ -408,4 +405,4 @@ def test_account_check(self, _call): _call.assert_has_calls([ mock.call(self.client, 'SoftLayer_Account', 'getObject', id=1234), mock.call(self.client, 'SoftLayer_Account', 'getObject1', id=9999), - ]) \ No newline at end of file + ]) From 52725d81a0dcdb7122c0c1850a269a00bc6bbeaf Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 24 Apr 2024 20:32:23 -0500 Subject: [PATCH 25/29] fixed setup.py --- islcli | 10 ---------- setup.py | 6 +++--- 2 files changed, 3 insertions(+), 13 deletions(-) delete mode 100755 islcli diff --git a/islcli b/islcli deleted file mode 100755 index 7822bc2bb..000000000 --- a/islcli +++ /dev/null @@ -1,10 +0,0 @@ -#!python -import re -import sys - -from SoftLayer.CLI.core import main - -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) - # print("arvs[0] = %s" % sys.argv[0]) - sys.exit(main()) diff --git a/setup.py b/setup.py index 78b902550..26a6d9647 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages # pylint: disable=inconsistent-return-statements -DESCRIPTION = "A library for SoftLayer's IMS API" +DESCRIPTION = "A library for SoftLayer's API" if os.path.exists('README.rst'): with codecs.open('README.rst', 'r', 'utf-8') as readme_file: @@ -14,7 +14,7 @@ LONG_DESCRIPTION = DESCRIPTION setup( - name='SoftLayer-Internal', + name='SoftLayer', version='v6.1.11', description=DESCRIPTION, long_description=LONG_DESCRIPTION, @@ -24,7 +24,7 @@ packages=find_packages(exclude=['tests']), license='MIT', zip_safe=False, - url='https://github.ibm.com/SoftLayer/internal-softlayer-cli', + url='https://github.com/SoftLayer/softlayer-python', entry_points={ 'console_scripts': [ 'islcli = SoftLayer.CLI.core:main', From 91dceed52c60eec4fa319ed31e327a801b6a1a8c Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 24 Apr 2024 20:53:40 -0500 Subject: [PATCH 26/29] Added emplogin docs --- SoftLayer/CLI/routes.py | 2 +- docs/cli/commands.rst | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index f6ab7b134..705b2cac6 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -8,7 +8,7 @@ ALL_ROUTES = [ ('shell', 'SoftLayer.shell.core:cli'), - ('login', 'SoftLayer.CLI.login:cli'), + ('emplogin', 'SoftLayer.CLI.login:cli'), ('call-api', 'SoftLayer.CLI.call_api:cli'), diff --git a/docs/cli/commands.rst b/docs/cli/commands.rst index d99126b00..9efe5803e 100644 --- a/docs/cli/commands.rst +++ b/docs/cli/commands.rst @@ -45,3 +45,13 @@ Can be called with an un-authenticated API call. .. click:: SoftLayer.CLI.search:cli :prog: search :show-nested: + + +Employee Login +============== + +Allows employees to use their login information to make API calls. + +.. click:: SoftLayer.CLI.login:cli + :prog: emplogin + :show-nested: From 5c09253ab08e06a7c8044eaec2d27d9439b4a618 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 30 Apr 2024 12:12:07 -0500 Subject: [PATCH 27/29] Added emplogin docs, added verify option to config --- SoftLayer/API.py | 5 +++-- SoftLayer/CLI/login.py | 3 ++- SoftLayer/config.py | 4 +++- SoftLayer/transports/rest.py | 1 - SoftLayer/transports/transport.py | 4 +++- SoftLayer/transports/xmlrpc.py | 3 +-- docs/cli/login.rst | 0 7 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 docs/cli/login.rst diff --git a/SoftLayer/API.py b/SoftLayer/API.py index 622211105..dc24914fe 100644 --- a/SoftLayer/API.py +++ b/SoftLayer/API.py @@ -153,7 +153,7 @@ def employee_client(username=None, proxy=None, user_agent=None, transport=None, - verify=False): + verify=True): """Creates an INTERNAL SoftLayer API client using your environment. Settings are loaded via keyword arguments, environemtal variables and config file. @@ -178,10 +178,11 @@ def employee_client(username=None, endpoint_url=endpoint_url, timeout=timeout, proxy=proxy, - verify=verify, + verify=None, config_file=config_file) url = settings.get('endpoint_url') + verify = settings.get('verify', True) if 'internal' not in url: raise exceptions.SoftLayerError(f"{url} does not look like an Internal Employee url.") diff --git a/SoftLayer/CLI/login.py b/SoftLayer/CLI/login.py index 3317e8945..7f3e76e35 100644 --- a/SoftLayer/CLI/login.py +++ b/SoftLayer/CLI/login.py @@ -4,6 +4,7 @@ import click +from SoftLayer.API import employee_client from SoftLayer.CLI.command import SLCommand as SLCommand from SoftLayer.CLI import environment from SoftLayer import config @@ -30,7 +31,7 @@ def cli(env): username = settings.get('username') or os.environ.get('SLCLI_USER', None) password = os.environ.get('SLCLI_PASSWORD', '') yubi = None - client = env.client + client = employee_client() # Might already be logged in, try and refresh token if settings.get('access_token') and settings.get('userid'): diff --git a/SoftLayer/config.py b/SoftLayer/config.py index d909d3d49..c32c0b5e1 100644 --- a/SoftLayer/config.py +++ b/SoftLayer/config.py @@ -60,7 +60,8 @@ def get_client_settings_config_file(**kwargs): # pylint: disable=inconsistent-r 'timeout': '0', 'proxy': '', 'userid': '', - 'access_token': '' + 'access_token': '', + 'verify': True }) config.read(config_files) @@ -73,6 +74,7 @@ def get_client_settings_config_file(**kwargs): # pylint: disable=inconsistent-r 'api_key': config.get('softlayer', 'api_key'), 'userid': config.get('softlayer', 'userid'), 'access_token': config.get('softlayer', 'access_token'), + 'verify': config.get('softlayer', 'verify') } diff --git a/SoftLayer/transports/rest.py b/SoftLayer/transports/rest.py index 7f8d2554f..30ce11bad 100644 --- a/SoftLayer/transports/rest.py +++ b/SoftLayer/transports/rest.py @@ -108,7 +108,6 @@ def __call__(self, request): request.url = '%s.%s' % ('/'.join(url_parts), 'json') # Prefer the request setting, if it's not None - if request.verify is None: request.verify = self.verify diff --git a/SoftLayer/transports/transport.py b/SoftLayer/transports/transport.py index 4aa5cebab..0454632ba 100644 --- a/SoftLayer/transports/transport.py +++ b/SoftLayer/transports/transport.py @@ -56,7 +56,9 @@ def __init__(self): #: Transport headers. self.transport_headers = {} - #: Boolean specifying if the server certificate should be verified. + #: False -> Don't verify the SSL certificate + #: True -> Verify the SSL certificate + #: Path String -> Verify the SSL certificate with the .pem file at path self.verify = None #: Client certificate file path. (Used by X509Authentication) diff --git a/SoftLayer/transports/xmlrpc.py b/SoftLayer/transports/xmlrpc.py index 99fbe1ddc..57ba4e9f6 100644 --- a/SoftLayer/transports/xmlrpc.py +++ b/SoftLayer/transports/xmlrpc.py @@ -84,8 +84,7 @@ def __call__(self, request): encoding="iso-8859-1") # Prefer the request setting, if it's not None - verify = request.verify - if verify is None: + if request.verify is None: request.verify = self.verify try: diff --git a/docs/cli/login.rst b/docs/cli/login.rst new file mode 100644 index 000000000..e69de29bb From be39bc25661cf2dc987161c19524adcf3fa60cb7 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 30 Apr 2024 13:53:38 -0500 Subject: [PATCH 28/29] documentation updates --- docs/cli/login.rst | 0 setup.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 docs/cli/login.rst diff --git a/docs/cli/login.rst b/docs/cli/login.rst deleted file mode 100644 index e69de29bb..000000000 diff --git a/setup.py b/setup.py index 26a6d9647..48f643d5d 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ url='https://github.com/SoftLayer/softlayer-python', entry_points={ 'console_scripts': [ - 'islcli = SoftLayer.CLI.core:main', + 'slcli = SoftLayer.CLI.core:main', ], }, python_requires='>=3.7', @@ -40,7 +40,7 @@ 'urllib3 >= 1.24', 'rich == 13.7.1' ], - keywords=['islcli'], + keywords=['softlayer', 'cloud', 'slcli', 'ibmcloud'], classifiers=[ 'Environment :: Console', 'Environment :: Web Environment', From e16e0f932f5ffcd499dff2a751b081318901f90c Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 30 Apr 2024 14:38:41 -0500 Subject: [PATCH 29/29] updating docs --- SoftLayer/CLI/core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index 57d81ebdb..870c47f0f 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -69,8 +69,9 @@ def get_version_message(ctx, param, value): ctx.exit() -@click.group(help="SoftLayer Employee Command-line Client", - epilog="""Run 'islcli login' to authenticate""", +@click.group(help="SoftLayer Command-line Client", + epilog="""To use most commands your SoftLayer username and api_key need to be configured. +The easiest way to do that is to use: 'slcli setup'""", cls=CommandLoader, context_settings=CONTEXT_SETTINGS) @click.option('--format', default=DEFAULT_FORMAT, show_default=True,