|
6 | 6 | :license: MIT, see LICENSE for more details.
|
7 | 7 | """
|
8 | 8 | # pylint: disable=invalid-name
|
| 9 | +import os |
9 | 10 | import time
|
10 | 11 | import warnings
|
11 | 12 |
|
|
28 | 29 |
|
29 | 30 | __all__ = [
|
30 | 31 | 'create_client_from_env',
|
| 32 | + 'employee_client', |
31 | 33 | 'Client',
|
32 | 34 | 'BaseClient',
|
33 | 35 | 'API_PUBLIC_ENDPOINT',
|
@@ -144,6 +146,102 @@ def create_client_from_env(username=None,
|
144 | 146 | return BaseClient(auth=auth, transport=transport, config_file=config_file)
|
145 | 147 |
|
146 | 148 |
|
| 149 | +def employee_client(username=None, |
| 150 | + access_token=None, |
| 151 | + password=None, |
| 152 | + endpoint_url=None, |
| 153 | + timeout=None, |
| 154 | + auth=None, |
| 155 | + config_file=None, |
| 156 | + proxy=None, |
| 157 | + user_agent=None, |
| 158 | + transport=None, |
| 159 | + verify=False): |
| 160 | + """Creates an INTERNAL SoftLayer API client using your environment. |
| 161 | +
|
| 162 | + Settings are loaded via keyword arguments, environemtal variables and |
| 163 | + config file. |
| 164 | +
|
| 165 | + :param username: your user ID |
| 166 | + :param access_token: hash from SoftLayer_User_Employee::performExternalAuthentication(username, password, 2fa_string) |
| 167 | + :param password: password to use for employee authentication |
| 168 | + :param endpoint_url: the API endpoint base URL you wish to connect to. |
| 169 | + Set this to API_PRIVATE_ENDPOINT to connect via SoftLayer's private |
| 170 | + network. |
| 171 | + :param proxy: proxy to be used to make API calls |
| 172 | + :param integer timeout: timeout for API requests |
| 173 | + :param auth: an object which responds to get_headers() to be inserted into |
| 174 | + the xml-rpc headers. Example: `BasicAuthentication` |
| 175 | + :param config_file: A path to a configuration file used to load settings |
| 176 | + :param user_agent: an optional User Agent to report when making API |
| 177 | + calls if you wish to bypass the packages built in User Agent string |
| 178 | + :param transport: An object that's callable with this signature: |
| 179 | + transport(SoftLayer.transports.Request) |
| 180 | + :param bool verify: decide to verify the server's SSL/TLS cert. DO NOT SET |
| 181 | + TO FALSE WITHOUT UNDERSTANDING THE IMPLICATIONS. |
| 182 | +
|
| 183 | + Usage: |
| 184 | +
|
| 185 | + >>> import SoftLayer |
| 186 | + >>> client = SoftLayer.create_client_from_env() |
| 187 | + >>> resp = client.call('Account', 'getObject') |
| 188 | + >>> resp['companyName'] |
| 189 | + 'Your Company' |
| 190 | +
|
| 191 | + """ |
| 192 | + # SSL verification is OFF because internal api uses a self signed cert |
| 193 | + settings = config.get_client_settings(username=username, |
| 194 | + api_key=None, |
| 195 | + endpoint_url=endpoint_url, |
| 196 | + timeout=timeout, |
| 197 | + proxy=proxy, |
| 198 | + verify=verify, |
| 199 | + config_file=config_file) |
| 200 | + |
| 201 | + url = settings.get('endpoint_url') or consts.API_EMPLOYEE_ENDPOINT |
| 202 | + |
| 203 | + if 'internal' not in url: |
| 204 | + raise exceptions.SoftLayerError("{} does not look like an Internal Employee url. Try {}".format( |
| 205 | + url, consts.API_EMPLOYEE_ENDPOINT)) |
| 206 | + |
| 207 | + if transport is None: |
| 208 | + |
| 209 | + if url is not None and '/rest' in url: |
| 210 | + # If this looks like a rest endpoint, use the rest transport |
| 211 | + transport = transports.RestTransport( |
| 212 | + endpoint_url=settings.get('endpoint_url'), |
| 213 | + proxy=settings.get('proxy'), |
| 214 | + timeout=settings.get('timeout'), |
| 215 | + user_agent=user_agent, |
| 216 | + verify=verify, |
| 217 | + ) |
| 218 | + else: |
| 219 | + # Default the transport to use XMLRPC |
| 220 | + transport = transports.XmlRpcTransport( |
| 221 | + endpoint_url=settings.get('endpoint_url'), |
| 222 | + proxy=settings.get('proxy'), |
| 223 | + timeout=settings.get('timeout'), |
| 224 | + user_agent=user_agent, |
| 225 | + verify=verify, |
| 226 | + ) |
| 227 | + |
| 228 | + |
| 229 | + if access_token is None: |
| 230 | + access_token = settings.get('access_token') |
| 231 | + |
| 232 | + user_id = settings.get('userid') |
| 233 | + |
| 234 | + # Assume access_token is valid for now, user has logged in before at least. |
| 235 | + if access_token and user_id: |
| 236 | + auth = slauth.EmployeeAuthentication(user_id, access_token) |
| 237 | + return EmployeeClient(auth=auth, transport=transport) |
| 238 | + else: |
| 239 | + # This is for logging in mostly. |
| 240 | + LOGGER.info("No access_token or userid found in settings, creating a No Auth client for now.") |
| 241 | + return EmployeeClient(auth=None, transport=transport) |
| 242 | + |
| 243 | + |
| 244 | + |
147 | 245 | def Client(**kwargs):
|
148 | 246 | """Get a SoftLayer API Client using environmental settings."""
|
149 | 247 | return create_client_from_env(**kwargs)
|
@@ -282,6 +380,7 @@ def call(self, service, method, *args, **kwargs):
|
282 | 380 | request.filter = kwargs.get('filter')
|
283 | 381 | request.limit = kwargs.get('limit')
|
284 | 382 | request.offset = kwargs.get('offset')
|
| 383 | + request.url = self.settings['softlayer'].get('endpoint_url') |
285 | 384 | if kwargs.get('verify') is not None:
|
286 | 385 | request.verify = kwargs.get('verify')
|
287 | 386 |
|
@@ -617,6 +716,97 @@ def __repr__(self):
|
617 | 716 | return "IAMClient(transport=%r, auth=%r)" % (self.transport, self.auth)
|
618 | 717 |
|
619 | 718 |
|
| 719 | +class EmployeeClient(BaseClient): |
| 720 | + """Internal SoftLayer Client |
| 721 | +
|
| 722 | + :param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase |
| 723 | + :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request) |
| 724 | + """ |
| 725 | + |
| 726 | + def __init__(self, auth=None, transport=None, config_file=None, account_id=None): |
| 727 | + BaseClient.__init__(self, auth, transport, config_file) |
| 728 | + self.account_id = account_id |
| 729 | + |
| 730 | + |
| 731 | + def authenticate_with_password(self, username, password, security_token=None): |
| 732 | + """Performs IBM IAM Username/Password Authentication |
| 733 | +
|
| 734 | + :param string username: your softlayer username |
| 735 | + :param string password: your softlayer password |
| 736 | + :param int security_token: your 2FA token, prompt if None |
| 737 | + """ |
| 738 | + |
| 739 | + self.auth = None |
| 740 | + if security_token is None: |
| 741 | + security_token = input("Enter your 2FA Token now: ") |
| 742 | + if len(security_token) != 6: |
| 743 | + raise Exception("Invalid security token: {}".format(security_token)) |
| 744 | + |
| 745 | + auth_result = self.call('SoftLayer_User_Employee', 'performExternalAuthentication', |
| 746 | + username, password, security_token) |
| 747 | + |
| 748 | + |
| 749 | + self.settings['softlayer']['access_token'] = auth_result['hash'] |
| 750 | + self.settings['softlayer']['userid'] = str(auth_result['userId']) |
| 751 | + # self.settings['softlayer']['refresh_token'] = tokens['refresh_token'] |
| 752 | + |
| 753 | + config.write_config(self.settings, self.config_file) |
| 754 | + self.auth = slauth.EmployeeAuthentication(auth_result['userId'], auth_result['hash']) |
| 755 | + |
| 756 | + return auth_result |
| 757 | + |
| 758 | + |
| 759 | + |
| 760 | + def authenticate_with_hash(self, userId, access_token): |
| 761 | + """Authenticates to the Internal SL API with an employee userid + token |
| 762 | +
|
| 763 | + :param string userId: Employee UserId |
| 764 | + :param string access_token: Employee Hash Token |
| 765 | + """ |
| 766 | + self.auth = slauth.EmployeeAuthentication(userId, access_token) |
| 767 | + |
| 768 | + def refresh_token(self, userId, auth_token): |
| 769 | + """Refreshes the login token""" |
| 770 | + |
| 771 | + # Go directly to base client, to avoid infite loop if the token is super expired. |
| 772 | + auth_result = BaseClient.call(self, 'SoftLayer_User_Employee', 'refreshEncryptedToken', auth_token, id=userId) |
| 773 | + if len(auth_result) > 1: |
| 774 | + for returned_data in auth_result: |
| 775 | + # Access tokens should be 188 characters, but just incase its longer or something. |
| 776 | + if len(returned_data) > 180: |
| 777 | + self.settings['softlayer']['access_token'] = returned_data |
| 778 | + else: |
| 779 | + message = "Excepted 2 properties from refreshEncryptedToken, got {}|".format(auth_result) |
| 780 | + raise exceptions.SoftLayerAPIError(message) |
| 781 | + |
| 782 | + config.write_config(self.settings, self.config_file) |
| 783 | + self.auth = slauth.EmployeeAuthentication(userId, auth_result[0]) |
| 784 | + return auth_result |
| 785 | + |
| 786 | + def call(self, service, method, *args, **kwargs): |
| 787 | + """Handles refreshing Employee tokens in case of a HTTP 401 error""" |
| 788 | + if (service == 'SoftLayer_Account' or service == 'Account') and not kwargs.get('id'): |
| 789 | + if not self.account_id: |
| 790 | + raise exceptions.SoftLayerError("SoftLayer_Account service requires an ID") |
| 791 | + kwargs['id'] = self.account_id |
| 792 | + |
| 793 | + try: |
| 794 | + return BaseClient.call(self, service, method, *args, **kwargs) |
| 795 | + except exceptions.SoftLayerAPIError as ex: |
| 796 | + if ex.faultCode == "SoftLayer_Exception_EncryptedToken_Expired": |
| 797 | + userId = self.settings['softlayer'].get('userid') |
| 798 | + access_token = self.settings['softlayer'].get('access_token') |
| 799 | + LOGGER.warning("Token has expired, trying to refresh. %s", ex.faultString) |
| 800 | + self.refresh_token(userId, access_token) |
| 801 | + # Try the Call again this time.... |
| 802 | + return BaseClient.call(self, service, method, *args, **kwargs) |
| 803 | + |
| 804 | + else: |
| 805 | + raise ex |
| 806 | + |
| 807 | + def __repr__(self): |
| 808 | + return "EmployeeClient(transport=%r, auth=%r)" % (self.transport, self.auth) |
| 809 | + |
620 | 810 | class Service(object):
|
621 | 811 | """A SoftLayer Service.
|
622 | 812 |
|
|
0 commit comments