66 :license: MIT, see LICENSE for more details.
77"""
88# pylint: disable=invalid-name
9+ import time
910import warnings
1011
12+ import json
13+ import logging
14+ import requests
15+
1116
1217from SoftLayer import auth as slauth
1318from SoftLayer import config
1419from SoftLayer import consts
20+ from SoftLayer import exceptions
1521from SoftLayer import transports
1622
23+ LOGGER = logging .getLogger (__name__ )
1724API_PUBLIC_ENDPOINT = consts .API_PUBLIC_ENDPOINT
1825API_PRIVATE_ENDPOINT = consts .API_PRIVATE_ENDPOINT
26+ CONFIG_FILE = consts .CONFIG_FILE
27+
1928__all__ = [
2029 'create_client_from_env' ,
2130 'Client' ,
@@ -80,6 +89,8 @@ def create_client_from_env(username=None,
8089 'Your Company'
8190
8291 """
92+ if config_file is None :
93+ config_file = CONFIG_FILE
8394 settings = config .get_client_settings (username = username ,
8495 api_key = api_key ,
8596 endpoint_url = endpoint_url ,
@@ -127,7 +138,7 @@ def create_client_from_env(username=None,
127138 settings .get ('api_key' ),
128139 )
129140
130- return BaseClient (auth = auth , transport = transport )
141+ return BaseClient (auth = auth , transport = transport , config_file = config_file )
131142
132143
133144def Client (** kwargs ):
@@ -150,8 +161,35 @@ class BaseClient(object):
150161
151162 _prefix = "SoftLayer_"
152163
153- def __init__ (self , auth = None , transport = None ):
164+ def __init__ (self , auth = None , transport = None , config_file = None ):
165+ if config_file is None :
166+ config_file = CONFIG_FILE
154167 self .auth = auth
168+ self .config_file = config_file
169+ self .settings = config .get_config (self .config_file )
170+
171+ if transport is None :
172+ url = self .settings ['softlayer' ].get ('endpoint_url' )
173+ if url is not None and '/rest' in url :
174+ # If this looks like a rest endpoint, use the rest transport
175+ transport = transports .RestTransport (
176+ endpoint_url = url ,
177+ proxy = self .settings ['softlayer' ].get ('proxy' ),
178+ # prevents an exception incase timeout is a float number.
179+ timeout = int (self .settings ['softlayer' ].getfloat ('timeout' )),
180+ user_agent = consts .USER_AGENT ,
181+ verify = self .settings ['softlayer' ].getboolean ('verify' ),
182+ )
183+ else :
184+ # Default the transport to use XMLRPC
185+ transport = transports .XmlRpcTransport (
186+ endpoint_url = url ,
187+ proxy = self .settings ['softlayer' ].get ('proxy' ),
188+ timeout = int (self .settings ['softlayer' ].getfloat ('timeout' )),
189+ user_agent = consts .USER_AGENT ,
190+ verify = self .settings ['softlayer' ].getboolean ('verify' ),
191+ )
192+
155193 self .transport = transport
156194
157195 def authenticate_with_password (self , username , password ,
@@ -322,6 +360,190 @@ def __len__(self):
322360 return 0
323361
324362
363+ class IAMClient (BaseClient ):
364+ """IBM ID Client for using IAM authentication
365+
366+ :param auth: auth driver that looks like SoftLayer.auth.AuthenticationBase
367+ :param transport: An object that's callable with this signature: transport(SoftLayer.transports.Request)
368+ """
369+
370+ def authenticate_with_password (self , username , password , security_question_id = None , security_question_answer = None ):
371+ """Performs IBM IAM Username/Password Authentication
372+
373+ :param string username: your IBMid username
374+ :param string password: your IBMid password
375+ """
376+
377+ iam_client = requests .Session ()
378+
379+ headers = {
380+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
381+ 'User-Agent' : consts .USER_AGENT ,
382+ 'Accept' : 'application/json'
383+ }
384+ data = {
385+ 'grant_type' : 'password' ,
386+ 'password' : password ,
387+ 'response_type' : 'cloud_iam' ,
388+ 'username' : username
389+ }
390+
391+ try :
392+ response = iam_client .request (
393+ 'POST' ,
394+ 'https://iam.cloud.ibm.com/identity/token' ,
395+ data = data ,
396+ headers = headers ,
397+ auth = requests .auth .HTTPBasicAuth ('bx' , 'bx' )
398+ )
399+ if response .status_code != 200 :
400+ LOGGER .error ("Unable to login: %s" , response .text )
401+
402+ response .raise_for_status ()
403+ tokens = json .loads (response .text )
404+ except requests .HTTPError as ex :
405+ error = json .loads (response .text )
406+ raise exceptions .IAMError (response .status_code ,
407+ error .get ('errorMessage' ),
408+ 'https://iam.cloud.ibm.com/identity/token' ) from ex
409+
410+ self .settings ['softlayer' ]['access_token' ] = tokens ['access_token' ]
411+ self .settings ['softlayer' ]['refresh_token' ] = tokens ['refresh_token' ]
412+
413+ config .write_config (self .settings , self .config_file )
414+ self .auth = slauth .BearerAuthentication ('' , tokens ['access_token' ], tokens ['refresh_token' ])
415+
416+ return tokens
417+
418+ def authenticate_with_passcode (self , passcode ):
419+ """Performs IBM IAM SSO Authentication
420+
421+ :param string passcode: your IBMid password
422+ """
423+
424+ iam_client = requests .Session ()
425+
426+ headers = {
427+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
428+ 'User-Agent' : consts .USER_AGENT ,
429+ 'Accept' : 'application/json'
430+ }
431+ data = {
432+ 'grant_type' : 'urn:ibm:params:oauth:grant-type:passcode' ,
433+ 'passcode' : passcode ,
434+ 'response_type' : 'cloud_iam'
435+ }
436+
437+ try :
438+ response = iam_client .request (
439+ 'POST' ,
440+ 'https://iam.cloud.ibm.com/identity/token' ,
441+ data = data ,
442+ headers = headers ,
443+ auth = requests .auth .HTTPBasicAuth ('bx' , 'bx' )
444+ )
445+ if response .status_code != 200 :
446+ LOGGER .error ("Unable to login: %s" , response .text )
447+
448+ response .raise_for_status ()
449+ tokens = json .loads (response .text )
450+
451+ except requests .HTTPError as ex :
452+ error = json .loads (response .text )
453+ raise exceptions .IAMError (response .status_code ,
454+ error .get ('errorMessage' ),
455+ 'https://iam.cloud.ibm.com/identity/token' ) from ex
456+
457+ self .settings ['softlayer' ]['access_token' ] = tokens ['access_token' ]
458+ self .settings ['softlayer' ]['refresh_token' ] = tokens ['refresh_token' ]
459+ a_expire = time .strftime ('%Y-%m-%d %H:%M:%S' , time .localtime (tokens ['expiration' ]))
460+ r_expire = time .strftime ('%Y-%m-%d %H:%M:%S' , time .localtime (tokens ['refresh_token_expiration' ]))
461+ LOGGER .warning ("Tokens retrieved, expires at %s, Refresh expires at %s" , a_expire , r_expire )
462+ config .write_config (self .settings , self .config_file )
463+ self .auth = slauth .BearerAuthentication ('' , tokens ['access_token' ], tokens ['refresh_token' ])
464+
465+ return tokens
466+
467+ def authenticate_with_iam_token (self , a_token , r_token = None ):
468+ """Authenticates to the SL API with an IAM Token
469+
470+ :param string a_token: Access token
471+ :param string r_token: Refresh Token, to be used if Access token is expired.
472+ """
473+ self .auth = slauth .BearerAuthentication ('' , a_token , r_token )
474+
475+ def refresh_iam_token (self , r_token , account_id = None , ims_account = None ):
476+ """Refreshes the IAM Token, will default to values in the config file"""
477+ iam_client = requests .Session ()
478+
479+ headers = {
480+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
481+ 'User-Agent' : consts .USER_AGENT ,
482+ 'Accept' : 'application/json'
483+ }
484+ data = {
485+ 'grant_type' : 'refresh_token' ,
486+ 'refresh_token' : r_token ,
487+ 'response_type' : 'cloud_iam'
488+ }
489+
490+ sl_config = self .settings ['softlayer' ]
491+
492+ if account_id is None and sl_config .get ('account_id' , False ):
493+ account_id = sl_config .get ('account_id' )
494+ if ims_account is None and sl_config .get ('ims_account' , False ):
495+ ims_account = sl_config .get ('ims_account' )
496+
497+ data ['account' ] = account_id
498+ data ['ims_account' ] = ims_account
499+
500+ try :
501+ response = iam_client .request (
502+ 'POST' ,
503+ 'https://iam.cloud.ibm.com/identity/token' ,
504+ data = data ,
505+ headers = headers ,
506+ auth = requests .auth .HTTPBasicAuth ('bx' , 'bx' )
507+ )
508+
509+ if response .status_code != 200 :
510+ LOGGER .warning ("Unable to refresh IAM Token. %s" , response .text )
511+
512+ response .raise_for_status ()
513+ tokens = json .loads (response .text )
514+
515+ except requests .HTTPError as ex :
516+ error = json .loads (response .text )
517+ raise exceptions .IAMError (response .status_code ,
518+ error .get ('errorMessage' ),
519+ 'https://iam.cloud.ibm.com/identity/token' ) from ex
520+
521+ a_expire = time .strftime ('%Y-%m-%d %H:%M:%S' , time .localtime (tokens ['expiration' ]))
522+ r_expire = time .strftime ('%Y-%m-%d %H:%M:%S' , time .localtime (tokens ['refresh_token_expiration' ]))
523+ LOGGER .warning ("Tokens retrieved, expires at %s, Refresh expires at %s" , a_expire , r_expire )
524+
525+ self .settings ['softlayer' ]['access_token' ] = tokens ['access_token' ]
526+ self .settings ['softlayer' ]['refresh_token' ] = tokens ['refresh_token' ]
527+ config .write_config (self .settings , self .config_file )
528+ self .auth = slauth .BearerAuthentication ('' , tokens ['access_token' ])
529+ return tokens
530+
531+ def call (self , service , method , * args , ** kwargs ):
532+ """Handles refreshing IAM tokens in case of a HTTP 401 error"""
533+ try :
534+ return super ().call (service , method , * args , ** kwargs )
535+ except exceptions .SoftLayerAPIError as ex :
536+
537+ if ex .faultCode == 401 :
538+ LOGGER .warning ("Token has expired, trying to refresh. %s" , ex .faultString )
539+ return ex
540+ else :
541+ raise ex
542+
543+ def __repr__ (self ):
544+ return "IAMClient(transport=%r, auth=%r)" % (self .transport , self .auth )
545+
546+
325547class Service (object ):
326548 """A SoftLayer Service.
327549
0 commit comments