forked from TrueCloudLab/lego
357 lines
8.4 KiB
Go
357 lines
8.4 KiB
Go
|
package egoscale
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"crypto/hmac"
|
||
|
"crypto/sha1"
|
||
|
"encoding/base64"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// Error formats a CloudStack error into a standard error
|
||
|
func (e *ErrorResponse) Error() string {
|
||
|
return fmt.Sprintf("API error %s %d (%s %d): %s", e.ErrorCode, e.ErrorCode, e.CSErrorCode, e.CSErrorCode, e.ErrorText)
|
||
|
}
|
||
|
|
||
|
// Success computes the values based on the RawMessage, either string or bool
|
||
|
func (e *booleanResponse) IsSuccess() (bool, error) {
|
||
|
if e.Success == nil {
|
||
|
return false, fmt.Errorf("Not a valid booleanResponse")
|
||
|
}
|
||
|
|
||
|
str := ""
|
||
|
if err := json.Unmarshal(e.Success, &str); err != nil {
|
||
|
boolean := false
|
||
|
if e := json.Unmarshal(e.Success, &boolean); e != nil {
|
||
|
return false, e
|
||
|
}
|
||
|
return boolean, nil
|
||
|
}
|
||
|
return str == "true", nil
|
||
|
}
|
||
|
|
||
|
// Error formats a CloudStack job response into a standard error
|
||
|
func (e *booleanResponse) Error() error {
|
||
|
success, err := e.IsSuccess()
|
||
|
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if success {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
fmt.Printf("%#v", e)
|
||
|
return fmt.Errorf("API error: %s", e.DisplayText)
|
||
|
}
|
||
|
|
||
|
func (exo *Client) parseResponse(resp *http.Response) (json.RawMessage, error) {
|
||
|
b, err := ioutil.ReadAll(resp.Body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
a, _ := rawValues(b)
|
||
|
|
||
|
if a == nil {
|
||
|
b, err = rawValue(b)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if resp.StatusCode >= 400 {
|
||
|
errorResponse := new(ErrorResponse)
|
||
|
if e := json.Unmarshal(b, errorResponse); e == nil && errorResponse.ErrorCode > 0 {
|
||
|
return nil, errorResponse
|
||
|
}
|
||
|
return nil, fmt.Errorf("%d %s", resp.StatusCode, b)
|
||
|
}
|
||
|
|
||
|
return b, nil
|
||
|
}
|
||
|
|
||
|
// asyncRequest perform an asynchronous job with a context
|
||
|
func (exo *Client) asyncRequest(ctx context.Context, request AsyncCommand) (interface{}, error) {
|
||
|
var err error
|
||
|
|
||
|
res := request.asyncResponse()
|
||
|
exo.AsyncRequestWithContext(ctx, request, func(j *AsyncJobResult, er error) bool {
|
||
|
if er != nil {
|
||
|
err = er
|
||
|
return false
|
||
|
}
|
||
|
if j.JobStatus == Success {
|
||
|
if r := j.Response(res); err != nil {
|
||
|
err = r
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
return true
|
||
|
})
|
||
|
return res, err
|
||
|
}
|
||
|
|
||
|
// syncRequest performs a sync request with a context
|
||
|
func (exo *Client) syncRequest(ctx context.Context, request syncCommand) (interface{}, error) {
|
||
|
body, err := exo.request(ctx, request)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
response := request.response()
|
||
|
err = json.Unmarshal(body, response)
|
||
|
|
||
|
// booleanResponse will alway be valid...
|
||
|
if err == nil {
|
||
|
if br, ok := response.(*booleanResponse); ok {
|
||
|
success, e := br.IsSuccess()
|
||
|
if e != nil {
|
||
|
return nil, e
|
||
|
}
|
||
|
if !success {
|
||
|
err = fmt.Errorf("Not a valid booleanResponse")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if err != nil {
|
||
|
errResponse := new(ErrorResponse)
|
||
|
if e := json.Unmarshal(body, errResponse); e == nil && errResponse.ErrorCode > 0 {
|
||
|
return errResponse, nil
|
||
|
}
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return response, nil
|
||
|
}
|
||
|
|
||
|
// BooleanRequest performs the given boolean command
|
||
|
func (exo *Client) BooleanRequest(req Command) error {
|
||
|
resp, err := exo.Request(req)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if b, ok := resp.(*booleanResponse); ok {
|
||
|
return b.Error()
|
||
|
}
|
||
|
|
||
|
panic(fmt.Errorf("The command %s is not a proper boolean response. %#v", req.name(), resp))
|
||
|
}
|
||
|
|
||
|
// BooleanRequestWithContext performs the given boolean command
|
||
|
func (exo *Client) BooleanRequestWithContext(ctx context.Context, req Command) error {
|
||
|
resp, err := exo.RequestWithContext(ctx, req)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if b, ok := resp.(*booleanResponse); ok {
|
||
|
return b.Error()
|
||
|
}
|
||
|
|
||
|
panic(fmt.Errorf("The command %s is not a proper boolean response. %#v", req.name(), resp))
|
||
|
}
|
||
|
|
||
|
// Request performs the given command
|
||
|
func (exo *Client) Request(request Command) (interface{}, error) {
|
||
|
ctx, cancel := context.WithTimeout(context.Background(), exo.Timeout)
|
||
|
defer cancel()
|
||
|
|
||
|
switch request.(type) {
|
||
|
case syncCommand:
|
||
|
return exo.syncRequest(ctx, request.(syncCommand))
|
||
|
case AsyncCommand:
|
||
|
return exo.asyncRequest(ctx, request.(AsyncCommand))
|
||
|
default:
|
||
|
panic(fmt.Errorf("The command %s is not a proper Sync or Async command", request.name()))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// RequestWithContext preforms a request with a context
|
||
|
func (exo *Client) RequestWithContext(ctx context.Context, request Command) (interface{}, error) {
|
||
|
switch request.(type) {
|
||
|
case syncCommand:
|
||
|
return exo.syncRequest(ctx, request.(syncCommand))
|
||
|
case AsyncCommand:
|
||
|
return exo.asyncRequest(ctx, request.(AsyncCommand))
|
||
|
default:
|
||
|
panic(fmt.Errorf("The command %s is not a proper Sync or Async command", request.name()))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// AsyncRequest performs the given command
|
||
|
func (exo *Client) AsyncRequest(request AsyncCommand, callback WaitAsyncJobResultFunc) {
|
||
|
ctx, cancel := context.WithTimeout(context.Background(), exo.Timeout)
|
||
|
defer cancel()
|
||
|
exo.AsyncRequestWithContext(ctx, request, callback)
|
||
|
}
|
||
|
|
||
|
// AsyncRequestWithContext preforms a request with a context
|
||
|
func (exo *Client) AsyncRequestWithContext(ctx context.Context, request AsyncCommand, callback WaitAsyncJobResultFunc) {
|
||
|
body, err := exo.request(ctx, request)
|
||
|
if err != nil {
|
||
|
callback(nil, err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
jobResult := new(AsyncJobResult)
|
||
|
if err := json.Unmarshal(body, jobResult); err != nil {
|
||
|
r := new(ErrorResponse)
|
||
|
if e := json.Unmarshal(body, r); e != nil && r.ErrorCode > 0 {
|
||
|
if !callback(nil, r) {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
if !callback(nil, err) {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Successful response
|
||
|
if jobResult.JobID == "" || jobResult.JobStatus != Pending {
|
||
|
callback(jobResult, nil)
|
||
|
// without a JobID, the next requests will only fail
|
||
|
return
|
||
|
}
|
||
|
|
||
|
for iteration := 0; ; iteration++ {
|
||
|
time.Sleep(exo.RetryStrategy(int64(iteration)))
|
||
|
|
||
|
req := &QueryAsyncJobResult{JobID: jobResult.JobID}
|
||
|
resp, err := exo.syncRequest(ctx, req)
|
||
|
if err != nil && !callback(nil, err) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
result, ok := resp.(*QueryAsyncJobResultResponse)
|
||
|
if !ok && !callback(nil, fmt.Errorf("AsyncJobResult expected, got %t", resp)) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
res := (*AsyncJobResult)(result)
|
||
|
|
||
|
if res.JobStatus == Failure {
|
||
|
if !callback(nil, res.Error()) {
|
||
|
return
|
||
|
}
|
||
|
} else {
|
||
|
if !callback(res, nil) {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Payload builds the HTTP request from the given command
|
||
|
func (exo *Client) Payload(request Command) (string, error) {
|
||
|
params := url.Values{}
|
||
|
err := prepareValues("", ¶ms, request)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
if hookReq, ok := request.(onBeforeHook); ok {
|
||
|
if err := hookReq.onBeforeSend(¶ms); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
params.Set("apikey", exo.APIKey)
|
||
|
params.Set("command", request.name())
|
||
|
params.Set("response", "json")
|
||
|
|
||
|
// This code is borrowed from net/url/url.go
|
||
|
// The way it's encoded by net/url doesn't match
|
||
|
// how CloudStack works.
|
||
|
var buf bytes.Buffer
|
||
|
keys := make([]string, 0, len(params))
|
||
|
for k := range params {
|
||
|
keys = append(keys, k)
|
||
|
}
|
||
|
|
||
|
sort.Strings(keys)
|
||
|
for _, k := range keys {
|
||
|
prefix := csEncode(k) + "="
|
||
|
for _, v := range params[k] {
|
||
|
if buf.Len() > 0 {
|
||
|
buf.WriteByte('&')
|
||
|
}
|
||
|
buf.WriteString(prefix)
|
||
|
buf.WriteString(csEncode(v))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return buf.String(), nil
|
||
|
}
|
||
|
|
||
|
// Sign signs the HTTP request and return it
|
||
|
func (exo *Client) Sign(query string) (string, error) {
|
||
|
mac := hmac.New(sha1.New, []byte(exo.apiSecret))
|
||
|
_, err := mac.Write([]byte(strings.ToLower(query)))
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
signature := csEncode(base64.StdEncoding.EncodeToString(mac.Sum(nil)))
|
||
|
return fmt.Sprintf("%s&signature=%s", csQuotePlus(query), signature), nil
|
||
|
}
|
||
|
|
||
|
// request makes a Request while being close to the metal
|
||
|
func (exo *Client) request(ctx context.Context, req Command) (json.RawMessage, error) {
|
||
|
payload, err := exo.Payload(req)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
query, err := exo.Sign(payload)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
method := "GET"
|
||
|
url := fmt.Sprintf("%s?%s", exo.Endpoint, query)
|
||
|
|
||
|
var body io.Reader
|
||
|
// respect Internet Explorer limit of 2048
|
||
|
if len(url) > 1<<11 {
|
||
|
url = exo.Endpoint
|
||
|
method = "POST"
|
||
|
body = strings.NewReader(query)
|
||
|
}
|
||
|
|
||
|
request, err := http.NewRequest(method, url, body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
request = request.WithContext(ctx)
|
||
|
request.Header.Add("User-Agent", fmt.Sprintf("exoscale/egoscale (%v)", Version))
|
||
|
|
||
|
if method == "POST" {
|
||
|
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||
|
request.Header.Add("Content-Length", strconv.Itoa(len(query)))
|
||
|
}
|
||
|
|
||
|
resp, err := exo.HTTPClient.Do(request)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer resp.Body.Close() // nolint: errcheck
|
||
|
|
||
|
text, err := exo.parseResponse(resp)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return text, nil
|
||
|
}
|