From 016471dd544bdce61dd9e58603f4ca4287e8e4a1 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 27 May 2022 16:09:12 -0500 Subject: [PATCH 001/129] 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 002/129] 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 003/129] 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 004/129] 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 005/129] 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 006/129] 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 007/129] 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 008/129] 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 009/129] 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 010/129] 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 011/129] 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 012/129] 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 013/129] 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 014/129] 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 015/129] 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 016/129] 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 017/129] 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 70e7dde8b7dbbcdf3e283c98ffd1c13e5d5650c6 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 10 Oct 2023 15:48:14 -0500 Subject: [PATCH 018/129] #2107 updating block|file volume-modify help messages --- SoftLayer/CLI/block/limit.py | 2 +- SoftLayer/CLI/block/modify.py | 19 +++++++++---------- SoftLayer/CLI/command.py | 9 ++++++--- SoftLayer/CLI/file/modify.py | 15 +++++++-------- SoftLayer/managers/storage.py | 6 +----- SoftLayer/managers/storage_utils.py | 6 ++---- SoftLayer/utils.py | 4 ++++ 7 files changed, 30 insertions(+), 31 deletions(-) diff --git a/SoftLayer/CLI/block/limit.py b/SoftLayer/CLI/block/limit.py index 7b64b0b98..6af71c9e3 100644 --- a/SoftLayer/CLI/block/limit.py +++ b/SoftLayer/CLI/block/limit.py @@ -23,7 +23,7 @@ def cli(env, sortby, datacenter): Example:: slcli block volume-limits This command lists the storage limits per datacenter for this account. -""" + """ block_manager = SoftLayer.BlockStorageManager(env.client) block_volumes = block_manager.list_block_volume_limit() diff --git a/SoftLayer/CLI/block/modify.py b/SoftLayer/CLI/block/modify.py index fac2af594..187ff8d7e 100644 --- a/SoftLayer/CLI/block/modify.py +++ b/SoftLayer/CLI/block/modify.py @@ -20,22 +20,21 @@ @click.option('--new-iops', '-i', type=int, help='Performance Storage IOPS, between 100 and 6000 in multiples of 100 [only for performance volumes] ' - '***If no IOPS value is specified, the original IOPS value of the volume will be used.***\n' - 'Requirements: [If original IOPS/GB for the volume is less than 0.3, new IOPS/GB must also be ' - 'less than 0.3. If original IOPS/GB for the volume is greater than or equal to 0.3, new IOPS/GB ' - 'for the volume must also be greater than or equal to 0.3.]') + '***If no IOPS value is specified, the original IOPS value of the volume will be used.***') @click.option('--new-tier', '-t', - help='Endurance Storage Tier (IOPS per GB) [only for endurance volumes] ' - '***If no tier is specified, the original tier of the volume will be used.***\n' - 'Requirements: [If original IOPS/GB for the volume is 0.25, new IOPS/GB for the volume must also ' - 'be 0.25. If original IOPS/GB for the volume is greater than 0.25, new IOPS/GB for the volume ' - 'must also be greater than 0.25.]', + help='Endurance Storage Tier (IOPS per GB) [only for endurance volumes] Classic Choices: ' + '***If no tier is specified, the original tier of the volume will be used.***', type=click.Choice(['0.25', '2', '4', '10'])) @environment.pass_env def cli(env, volume_id, new_size, new_iops, new_tier): - """Modify an existing block storage volume. + """Modify an existing block storage volume. Choices. + + Valid size and iops options can be found here: + https://cloud.ibm.com/docs/BlockStorage/index.html#provisioning-considerations + https://cloud.ibm.com/docs/BlockStorage?topic=BlockStorage-orderingBlockStorage&interface=cli Example:: + slcli block volume-modify 12345678 --new-size 1000 --new-iops 4000 This command modify a volume 12345678 with size is 1000GB, IOPS is 4000. diff --git a/SoftLayer/CLI/command.py b/SoftLayer/CLI/command.py index b38f89d7c..b64232014 100644 --- a/SoftLayer/CLI/command.py +++ b/SoftLayer/CLI/command.py @@ -28,6 +28,8 @@ class OptionHighlighter(RegexHighlighter): r"(?P