1/** 2 * Copyright 2016 IBM Corp. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package session 18 19import ( 20 "context" 21 "fmt" 22 "log" 23 "math/rand" 24 "net" 25 "net/http" 26 "os" 27 "os/user" 28 "runtime" 29 "strings" 30 "time" 31 32 "github.com/softlayer/softlayer-go/config" 33 "github.com/softlayer/softlayer-go/sl" 34) 35 36// Logger is the logger used by the SoftLayer session package. Can be overridden by the user. 37var Logger *log.Logger 38 39func init() { 40 // initialize the logger used by the session package. 41 Logger = log.New(os.Stderr, "", log.LstdFlags) 42} 43 44// DefaultEndpoint is the default endpoint for API calls, when no override 45// is provided. 46const DefaultEndpoint = "https://api.softlayer.com/rest/v3" 47 48var retryableErrorCodes = []string{"SoftLayer_Exception_WebService_RateLimitExceeded"} 49 50// TransportHandler interface for the protocol-specific handling of API requests. 51type TransportHandler interface { 52 // DoRequest is the protocol-specific handler for making API requests. 53 // 54 // sess is a reference to the current session object, where authentication and 55 // endpoint information can be found. 56 // 57 // service and method are the SoftLayer service name and method name, exactly as they 58 // are documented at http://sldn.softlayer.com/reference/softlayerapi (i.e., with the 59 // 'SoftLayer_' prefix and properly cased. 60 // 61 // args is a slice of arguments required for the service method being invoked. The 62 // types of each argument varies. See the method definition in the services package 63 // for the expected type of each argument. 64 // 65 // options is an sl.Options struct, containing any mask, filter, or result limit values 66 // to be applied. 67 // 68 // pResult is a pointer to a variable to be populated with the result of the API call. 69 // DoRequest should ensure that the native API response (i.e., XML or JSON) is correctly 70 // unmarshaled into the result structure. 71 // 72 // A sl.Error is returned, and can be (with a type assertion) inspected for details of 73 // the error (http code, API error message, etc.), or simply handled as a generic error, 74 // (in which case no type assertion would be necessary) 75 DoRequest( 76 sess *Session, 77 service string, 78 method string, 79 args []interface{}, 80 options *sl.Options, 81 pResult interface{}) error 82} 83 84const ( 85 DefaultTimeout = time.Second * 120 86 DefaultRetryWait = time.Second * 3 87) 88 89// Session stores the information required for communication with the SoftLayer 90// API 91type Session struct { 92 // UserName is the name of the SoftLayer API user 93 UserName string 94 95 // ApiKey is the secret for making API calls 96 APIKey string 97 98 // Endpoint is the SoftLayer API endpoint to communicate with 99 Endpoint string 100 101 // UserId is the user id for token-based authentication 102 UserId int 103 104 //IAMToken is the IAM token secret that included IMS account for token-based authentication 105 IAMToken string 106 107 // AuthToken is the token secret for token-based authentication 108 AuthToken string 109 110 // Debug controls logging of request details (URI, parameters, etc.) 111 Debug bool 112 113 // The handler whose DoRequest() function will be called for each API request. 114 // Handles the request and any response parsing specific to the desired protocol 115 // (e.g., REST). Set automatically for a new Session, based on the 116 // provided Endpoint. 117 TransportHandler TransportHandler 118 119 // HTTPClient This allows a custom user configured HTTP Client. 120 HTTPClient *http.Client 121 122 // Context allows a custom context.Context for outbound HTTP requests 123 Context context.Context 124 125 // Custom Headers to be used on each request (Currently only for rest) 126 Headers map[string]string 127 128 // Timeout specifies a time limit for http requests made by this 129 // session. Requests that take longer that the specified timeout 130 // will result in an error. 131 Timeout time.Duration 132 133 // Retries is the number of times to retry a connection that failed due to a timeout. 134 Retries int 135 136 // RetryWait minimum wait time to retry a request 137 RetryWait time.Duration 138 139 // userAgent is the user agent to send with each API request 140 // User shouldn't be able to change or set the base user agent 141 userAgent string 142} 143 144func init() { 145 rand.Seed(time.Now().UnixNano()) 146} 147 148// New creates and returns a pointer to a new session object. It takes up to 149// four parameters, all of which are optional. If specified, they will be 150// interpreted in the following sequence: 151// 152// 1. UserName 153// 2. Api Key 154// 3. Endpoint 155// 4. Timeout 156// 157// If one or more are omitted, New() will attempt to retrieve these values from 158// the environment, and the ~/.softlayer config file, in that order. 159func New(args ...interface{}) *Session { 160 keys := map[string]int{"username": 0, "api_key": 1, "endpoint_url": 2, "timeout": 3} 161 values := []string{"", "", "", ""} 162 163 for i := 0; i < len(args); i++ { 164 values[i] = args[i].(string) 165 } 166 167 // Default to the environment variables 168 169 // Prioritize SL_USERNAME 170 envFallback("SL_USERNAME", &values[keys["username"]]) 171 envFallback("SOFTLAYER_USERNAME", &values[keys["username"]]) 172 173 // Prioritize SL_API_KEY 174 envFallback("SL_API_KEY", &values[keys["api_key"]]) 175 envFallback("SOFTLAYER_API_KEY", &values[keys["api_key"]]) 176 177 // Prioritize SL_ENDPOINT_URL 178 envFallback("SL_ENDPOINT_URL", &values[keys["endpoint_url"]]) 179 envFallback("SOFTLAYER_ENDPOINT_URL", &values[keys["endpoint_url"]]) 180 181 envFallback("SL_TIMEOUT", &values[keys["timeout"]]) 182 envFallback("SOFTLAYER_TIMEOUT", &values[keys["timeout"]]) 183 184 // Read ~/.softlayer for configuration 185 var homeDir string 186 u, err := user.Current() 187 if err != nil { 188 for _, name := range []string{"HOME", "USERPROFILE"} { // *nix, windows 189 if dir := os.Getenv(name); dir != "" { 190 homeDir = dir 191 break 192 } 193 } 194 } else { 195 homeDir = u.HomeDir 196 } 197 198 if homeDir != "" { 199 configPath := fmt.Sprintf("%s/.softlayer", homeDir) 200 if _, err = os.Stat(configPath); !os.IsNotExist(err) { 201 // config file exists 202 file, err := config.LoadFile(configPath) 203 if err != nil { 204 log.Println(fmt.Sprintf("[WARN] session: Could not parse %s : %s", configPath, err)) 205 } else { 206 for k, v := range keys { 207 value, ok := file.Get("softlayer", k) 208 if ok && values[v] == "" { 209 values[v] = value 210 } 211 } 212 } 213 } 214 } else { 215 log.Println("[WARN] session: home dir could not be determined. Skipping read of ~/.softlayer.") 216 } 217 218 endpointURL := values[keys["endpoint_url"]] 219 if endpointURL == "" { 220 endpointURL = DefaultEndpoint 221 } 222 223 sess := &Session{ 224 UserName: values[keys["username"]], 225 APIKey: values[keys["api_key"]], 226 Endpoint: endpointURL, 227 userAgent: getDefaultUserAgent(), 228 } 229 230 timeout := values[keys["timeout"]] 231 if timeout != "" { 232 timeoutDuration, err := time.ParseDuration(fmt.Sprintf("%ss", timeout)) 233 if err == nil { 234 sess.Timeout = timeoutDuration 235 } 236 } 237 238 sess.RetryWait = DefaultRetryWait 239 240 return sess 241} 242 243// DoRequest hands off the processing to the assigned transport handler. It is 244// normally called internally by the service objects, but is exported so that it can 245// be invoked directly by client code in exceptional cases where direct control is 246// needed over one of the parameters. 247// 248// For a description of parameters, see TransportHandler.DoRequest in this package 249func (r *Session) DoRequest(service string, method string, args []interface{}, options *sl.Options, pResult interface{}) error { 250 if r.TransportHandler == nil { 251 r.TransportHandler = getDefaultTransport(r.Endpoint) 252 } 253 254 return r.TransportHandler.DoRequest(r, service, method, args, options, pResult) 255} 256 257// SetTimeout creates a copy of the session and sets the passed timeout into it 258// before returning it. 259func (r *Session) SetTimeout(timeout time.Duration) *Session { 260 var s Session 261 s = *r 262 s.Timeout = timeout 263 264 return &s 265} 266 267// SetRetries creates a copy of the session and sets the passed retries into it 268// before returning it. 269func (r *Session) SetRetries(retries int) *Session { 270 var s Session 271 s = *r 272 s.Retries = retries 273 274 return &s 275} 276 277// SetRetryWait creates a copy of the session and sets the passed retryWait into it 278// before returning it. 279func (r *Session) SetRetryWait(retryWait time.Duration) *Session { 280 var s Session 281 s = *r 282 s.RetryWait = retryWait 283 284 return &s 285} 286 287// AppendUserAgent allows higher level application to identify themselves by 288// appending to the useragent string 289func (r *Session) AppendUserAgent(agent string) { 290 if r.userAgent == "" { 291 r.userAgent = getDefaultUserAgent() 292 } 293 294 if agent != "" { 295 r.userAgent += " " + agent 296 } 297} 298 299// ResetUserAgent resets the current user agent to the default value 300func (r *Session) ResetUserAgent() { 301 r.userAgent = getDefaultUserAgent() 302} 303 304func envFallback(keyName string, value *string) { 305 if *value == "" { 306 *value = os.Getenv(keyName) 307 } 308} 309 310func getDefaultTransport(endpointURL string) TransportHandler { 311 var transportHandler TransportHandler 312 313 if strings.Contains(endpointURL, "/xmlrpc/") { 314 transportHandler = &XmlRpcTransport{} 315 } else { 316 transportHandler = &RestTransport{} 317 } 318 319 return transportHandler 320} 321 322func isTimeout(err error) bool { 323 if slErr, ok := err.(sl.Error); ok { 324 switch slErr.StatusCode { 325 case 408, 504, 599: 326 return true 327 } 328 } 329 330 if netErr, ok := err.(net.Error); ok && netErr.Timeout() { 331 return true 332 } 333 334 if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() { 335 return true 336 } 337 338 if netErr, ok := err.(net.UnknownNetworkError); ok && netErr.Timeout() { 339 return true 340 } 341 342 return false 343} 344 345func hasRetryableCode(err error) bool { 346 for _, code := range retryableErrorCodes { 347 if slErr, ok := err.(sl.Error); ok { 348 if slErr.Exception == code { 349 return true 350 } 351 } 352 } 353 return false 354} 355 356func isRetryable(err error) bool { 357 return isTimeout(err) || hasRetryableCode(err) 358} 359 360func getDefaultUserAgent() string { 361 return fmt.Sprintf("softlayer-go/%s (%s;%s;%s)", sl.Version.String(), 362 runtime.Version(), 363 runtime.GOARCH, 364 runtime.GOOS, 365 ) 366} 367