66 :license: MIT, see LICENSE for more details.
77"""
88# pylint: disable=invalid-name
9+ import json
10+ import logging
11+ import requests
912import warnings
1013
1114
1215from SoftLayer import auth as slauth
1316from SoftLayer import config
1417from SoftLayer import consts
18+ from SoftLayer import exceptions
1519from SoftLayer import transports
1620
21+
22+ LOGGER = logging .getLogger (__name__ )
1723API_PUBLIC_ENDPOINT = consts .API_PUBLIC_ENDPOINT
1824API_PRIVATE_ENDPOINT = consts .API_PRIVATE_ENDPOINT
25+ CONFIG_FILE = consts .CONFIG_FILE
26+
1927__all__ = [
2028 'create_client_from_env' ,
2129 'Client' ,
@@ -80,6 +88,8 @@ def create_client_from_env(username=None,
8088 'Your Company'
8189
8290 """
91+ if config_file is None :
92+ config_file = CONFIG_FILE
8393 settings = config .get_client_settings (username = username ,
8494 api_key = api_key ,
8595 endpoint_url = endpoint_url ,
@@ -127,7 +137,7 @@ def create_client_from_env(username=None,
127137 settings .get ('api_key' ),
128138 )
129139
130- return BaseClient (auth = auth , transport = transport )
140+ return BaseClient (auth = auth , transport = transport , config_file = config_file )
131141
132142
133143def Client (** kwargs ):
@@ -150,9 +160,35 @@ class BaseClient(object):
150160
151161 _prefix = "SoftLayer_"
152162
153- def __init__ (self , auth = None , transport = None ):
163+ def __init__ (self , auth = None , transport = None , config_file = None ):
164+ if config_file is None :
165+ config_file = CONFIG_FILE
154166 self .auth = auth
155- self .transport = transport
167+ self .config_file = config_file
168+ self .settings = config .get_config (self .config_file )
169+
170+ if transport is None :
171+ url = self .settings ['softlayer' ].get ('endpoint_url' )
172+ if url is not None and '/rest' in url :
173+ # If this looks like a rest endpoint, use the rest transport
174+ transport = transports .RestTransport (
175+ endpoint_url = url ,
176+ proxy = self .settings ['softlayer' ].get ('proxy' ),
177+ timeout = self .settings ['softlayer' ].getint ('timeout' ),
178+ user_agent = consts .USER_AGENT ,
179+ verify = self .settings ['softlayer' ].getboolean ('verify' ),
180+ )
181+ else :
182+ # Default the transport to use XMLRPC
183+ transport = transports .XmlRpcTransport (
184+ endpoint_url = url ,
185+ proxy = self .settings ['softlayer' ].get ('proxy' ),
186+ timeout = self .settings ['softlayer' ].getint ('timeout' ),
187+ user_agent = consts .USER_AGENT ,
188+ verify = self .settings ['softlayer' ].getboolean ('verify' ),
189+ )
190+
191+ self .transport = transport
156192
157193 def authenticate_with_password (self , username , password ,
158194 security_question_id = None ,
@@ -321,6 +357,127 @@ def __repr__(self):
321357 def __len__ (self ):
322358 return 0
323359
360+ class IAMClient (BaseClient ):
361+ """IBM ID Client for using IAM authentication
362+
363+ :param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase
364+ :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request)
365+ """
366+
367+
368+ def authenticate_with_password (self , username , password ):
369+ """Performs IBM IAM Username/Password Authentication
370+
371+ :param string username: your IBMid username
372+ :param string password: your IBMid password
373+ """
374+
375+ iam_client = requests .Session ()
376+
377+ headers = {
378+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
379+ 'User-Agent' : consts .USER_AGENT ,
380+ 'Accept' : 'application/json'
381+ }
382+ data = {
383+ 'grant_type' : 'password' ,
384+ 'password' : password ,
385+ 'response_type' : 'cloud_iam' ,
386+ 'username' : username
387+ }
388+
389+ response = iam_client .request (
390+ 'POST' ,
391+ 'https://iam.cloud.ibm.com/identity/token' ,
392+ data = data ,
393+ headers = headers ,
394+ auth = requests .auth .HTTPBasicAuth ('bx' , 'bx' )
395+ )
396+ if response .status_code != 200 :
397+ LOGGER .error ("Unable to login: {}" .format (response .text ))
398+
399+ response .raise_for_status ()
400+
401+ tokens = json .loads (response .text )
402+ self .settings ['softlayer' ]['access_token' ] = tokens ['access_token' ]
403+ self .settings ['softlayer' ]['refresh_token' ] = tokens ['refresh_token' ]
404+
405+ config .write_config (self .settings )
406+ self .auth = slauth .BearerAuthentication ('' , tokens ['access_token' ], tokens ['refresh_token' ])
407+ return tokens
408+
409+ def authenticate_with_iam_token (self , a_token , r_token ):
410+ """Authenticates to the SL API with an IAM Token
411+
412+ :param string a_token: Access token
413+ :param string r_token: Refresh Token, to be used if Access token is expired.
414+ """
415+ self .auth = slauth .BearerAuthentication ('' , a_token )
416+ user = None
417+ try :
418+ user = self .call ('Account' , 'getCurrentUser' )
419+ except exceptions .SoftLayerAPIError as ex :
420+ if ex .faultCode == 401 :
421+ LOGGER .warning ("Token has expired, trying to refresh." )
422+ # self.refresh_iam_token(r_token)
423+ else :
424+ raise ex
425+ return user
426+
427+ def refresh_iam_token (self , r_token ):
428+ iam_client = requests .Session ()
429+
430+ headers = {
431+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
432+ 'User-Agent' : consts .USER_AGENT ,
433+ 'Accept' : 'application/json'
434+ }
435+ data = {
436+ 'grant_type' : 'refresh_token' ,
437+ 'refresh_token' : r_token ,
438+ 'response_type' : 'cloud_iam'
439+ }
440+
441+ config = self .settings .get ('softlayer' )
442+ if config .get ('account' , False ):
443+ data ['account' ] = account
444+ if config .get ('ims_account' , False ):
445+ data ['ims_account' ] = ims_account
446+
447+ response = iam_client .request (
448+ 'POST' ,
449+ 'https://iam.cloud.ibm.com/identity/token' ,
450+ data = data ,
451+ headers = headers ,
452+ auth = requests .auth .HTTPBasicAuth ('bx' , 'bx' )
453+ )
454+ response .raise_for_status ()
455+
456+ LOGGER .warning ("Successfully refreshed Tokens, saving to config" )
457+ tokens = json .loads (response .text )
458+ self .settings ['softlayer' ]['access_token' ] = tokens ['access_token' ]
459+ self .settings ['softlayer' ]['refresh_token' ] = tokens ['refresh_token' ]
460+ config .write_config (self .settings )
461+ return tokens
462+
463+
464+
465+ def call (self , service , method , * args , ** kwargs ):
466+ """Handles refreshing IAM tokens in case of a HTTP 401 error"""
467+ try :
468+ return super ().call (service , method , * args , ** kwargs )
469+ except exceptions .SoftLayerAPIError as ex :
470+ if ex .faultCode == 401 :
471+ LOGGER .warning ("Token has expired, trying to refresh." )
472+ self .refresh_iam_token (r_token )
473+ return super ().call (service , method , * args , ** kwargs )
474+ else :
475+ raise ex
476+
477+
478+ def __repr__ (self ):
479+ return "IAMClient(transport=%r, auth=%r)" % (self .transport , self .auth )
480+
324481
325482class Service (object ):
326483 """A SoftLayer Service.
0 commit comments