Update Lego

This commit is contained in:
Ludovic Fernandez 2018-09-14 10:06:03 +02:00 committed by Traefiker Bot
parent 36966da701
commit 253060b4f3
185 changed files with 16653 additions and 3210 deletions

View file

@ -2,334 +2,380 @@ package egoscale
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
// Command represent a CloudStack request
type Command interface {
// CloudStack API command name
name() string
// Response interface to Unmarshal the JSON into
response() interface{}
}
// AsyncCommand represents a async CloudStack request
type AsyncCommand interface {
// CloudStack API command name
name() string
// Response interface to Unmarshal the JSON into
asyncResponse() interface{}
}
// Command represents an action to be done on the params before sending them
//
// This little took helps with issue of relying on JSON serialization logic only.
// `omitempty` may make sense in some cases but not all the time.
type onBeforeHook interface {
onBeforeSend(params *url.Values) error
}
const (
// Pending represents a job in progress
Pending JobStatusType = iota
// Success represents a successfully completed job
Success
// Failure represents a job that has failed to complete
Failure
)
// JobStatusType represents the status of a Job
type JobStatusType int
const (
// Unauthorized represents ... (TODO)
Unauthorized ErrorCode = 401
// MethodNotAllowed represents ... (TODO)
MethodNotAllowed = 405
// UnsupportedActionError represents ... (TODO)
UnsupportedActionError = 422
// APILimitExceeded represents ... (TODO)
APILimitExceeded = 429
// MalformedParameterError represents ... (TODO)
MalformedParameterError = 430
// ParamError represents ... (TODO)
ParamError = 431
// InternalError represents a server error
InternalError = 530
// AccountError represents ... (TODO)
AccountError = 531
// AccountResourceLimitError represents ... (TODO)
AccountResourceLimitError = 532
// InsufficientCapacityError represents ... (TODO)
InsufficientCapacityError = 533
// ResourceUnavailableError represents ... (TODO)
ResourceUnavailableError = 534
// ResourceAllocationError represents ... (TODO)
ResourceAllocationError = 535
// ResourceInUseError represents ... (TODO)
ResourceInUseError = 536
// NetworkRuleConflictError represents ... (TODO)
NetworkRuleConflictError = 537
)
// ErrorCode represents the CloudStack ApiErrorCode enum
//
// See: https://github.com/apache/cloudstack/blob/master/api/src/org/apache/cloudstack/api/ApiErrorCode.java
type ErrorCode int
// JobResultResponse represents a generic response to a job task
type JobResultResponse struct {
AccountID string `json:"accountid,omitempty"`
Cmd string `json:"cmd"`
Created string `json:"created"`
JobID string `json:"jobid"`
JobProcStatus int `json:"jobprocstatus"`
JobResult *json.RawMessage `json:"jobresult"`
JobStatus JobStatusType `json:"jobstatus"`
JobResultType string `json:"jobresulttype"`
UserID string `json:"userid,omitempty"`
}
// ErrorResponse represents the standard error response from CloudStack
type ErrorResponse struct {
ErrorCode ErrorCode `json:"errorcode"`
CsErrorCode int `json:"cserrorcode"`
ErrorText string `json:"errortext"`
UUIDList []string `json:"uuidList,omitempty"` // uuid*L*ist is not a typo
}
// Error formats a CloudStack error into a standard error
func (e *ErrorResponse) Error() string {
return fmt.Sprintf("API error %d (internal code: %d): %s", e.ErrorCode, e.CsErrorCode, e.ErrorText)
}
// booleanAsyncResponse represents a boolean response (usually after a deletion)
type booleanAsyncResponse struct {
Success bool `json:"success"`
DisplayText string `json:"diplaytext,omitempty"`
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)
}
// Error formats a CloudStack job response into a standard error
func (e *booleanAsyncResponse) Error() error {
if e.Success {
return nil
}
return fmt.Errorf("API error: %s", e.DisplayText)
}
// booleanAsyncResponse represents a boolean response for sync calls
type booleanSyncResponse struct {
Success string `json:"success"`
DisplayText string `json:"displaytext,omitempty"`
}
func (e *booleanSyncResponse) Error() error {
if e.Success == "true" {
return nil
func (e booleanResponse) Error() error {
if !e.Success {
return fmt.Errorf("API error: %s", e.DisplayText)
}
return fmt.Errorf("API error: %s", e.DisplayText)
return nil
}
// AsyncInfo represents the details for any async call
//
// It retries at most Retries time and waits for Delay between each retry
type AsyncInfo struct {
Retries int
Delay int
// XXX: addIpToNic, activateIp6, restorevmresponse are kind of special
var responseKeys = map[string]string{
"addiptonicresponse": "addiptovmnicresponse",
"activateip6response": "activateip6nicresponse",
"restorevirtualmachineresponse": "restorevmresponse",
}
func csQuotePlus(s string) string {
s = strings.Replace(s, "+", "%20", -1)
s = strings.Replace(s, "%5B", "[", -1)
s = strings.Replace(s, "%5D", "]", -1)
return s
}
func csEncode(s string) string {
return csQuotePlus(url.QueryEscape(s))
}
func rawValue(b json.RawMessage) (json.RawMessage, error) {
var m map[string]json.RawMessage
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
for _, v := range m {
return v, nil
}
return nil, nil
}
func rawValues(b json.RawMessage) (json.RawMessage, error) {
var i []json.RawMessage
if err := json.Unmarshal(b, &i); err != nil {
return nil, nil
}
return i[0], nil
}
func (exo *Client) parseResponse(resp *http.Response) (json.RawMessage, error) {
func (client *Client) parseResponse(resp *http.Response, apiName string) (json.RawMessage, error) {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
a, err := rawValues(b)
m := map[string]json.RawMessage{}
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
if a == nil {
b, err = rawValue(b)
if err != nil {
return nil, err
key := fmt.Sprintf("%sresponse", strings.ToLower(apiName))
response, ok := m[key]
if !ok {
if resp.StatusCode >= 400 {
response, ok = m["errorresponse"]
}
if !ok {
// try again with the special keys
value, ok := responseKeys[key]
if ok {
key = value
}
response, ok = m[key]
if !ok {
for k := range m {
return nil, fmt.Errorf("malformed JSON response, %q was expected, got %q", key, k)
}
}
}
}
if resp.StatusCode >= 400 {
var e ErrorResponse
if err := json.Unmarshal(b, &e); err != nil {
return nil, err
errorResponse := new(ErrorResponse)
if e := json.Unmarshal(response, errorResponse); e != nil && errorResponse.ErrorCode <= 0 {
return nil, fmt.Errorf("%d %s", resp.StatusCode, b)
}
return b, &e
return nil, errorResponse
}
return b, nil
n := map[string]json.RawMessage{}
if err := json.Unmarshal(response, &n); err != nil {
return nil, err
}
// list response may contain only one key
if len(n) > 1 || strings.HasPrefix(key, "list") {
return response, nil
}
if len(n) == 1 {
for k := range n {
// boolean response and asyncjob result may also contain
// only one key
if k == "success" || k == "jobid" {
return response, nil
}
return n[k], nil
}
}
return response, nil
}
// AsyncRequest performs an asynchronous request and polls it for retries * day [s]
func (exo *Client) AsyncRequest(req AsyncCommand, async AsyncInfo) (interface{}, error) {
body, err := exo.request(req.name(), req)
// asyncRequest perform an asynchronous job with a context
func (client *Client) asyncRequest(ctx context.Context, asyncCommand AsyncCommand) (interface{}, error) {
var err error
resp := asyncCommand.asyncResponse()
client.AsyncRequestWithContext(
ctx,
asyncCommand,
func(j *AsyncJobResult, e error) bool {
if e != nil {
err = e
return false
}
if j.JobStatus != Pending {
if r := j.Result(resp); r != nil {
err = r
}
return false
}
return true
},
)
return resp, err
}
// SyncRequestWithContext performs a sync request with a context
func (client *Client) SyncRequestWithContext(ctx context.Context, command Command) (interface{}, error) {
body, err := client.request(ctx, command)
if err != nil {
return nil, err
}
// Is it a Job?
job := new(JobResultResponse)
if err := json.Unmarshal(body, &job); err != nil {
response := command.response()
b, ok := response.(*booleanResponse)
if ok {
m := make(map[string]interface{})
if errUnmarshal := json.Unmarshal(body, &m); errUnmarshal != nil {
return nil, errUnmarshal
}
b.DisplayText, _ = m["displaytext"].(string)
if success, okSuccess := m["success"].(string); okSuccess {
b.Success = success == "true"
}
if success, okSuccess := m["success"].(bool); okSuccess {
b.Success = success
}
return b, nil
}
if err := json.Unmarshal(body, response); err != nil {
errResponse := new(ErrorResponse)
if e := json.Unmarshal(body, errResponse); e == nil && errResponse.ErrorCode > 0 {
return errResponse, nil
}
return nil, err
}
// Error response
errorResponse := new(ErrorResponse)
return response, nil
}
// BooleanRequest performs the given boolean command
func (client *Client) BooleanRequest(command Command) error {
resp, err := client.Request(command)
if err != nil {
return err
}
if b, ok := resp.(*booleanResponse); ok {
return b.Error()
}
panic(fmt.Errorf("command %q is not a proper boolean response. %#v", client.APIName(command), resp))
}
// BooleanRequestWithContext performs the given boolean command
func (client *Client) BooleanRequestWithContext(ctx context.Context, command Command) error {
resp, err := client.RequestWithContext(ctx, command)
if err != nil {
return err
}
if b, ok := resp.(*booleanResponse); ok {
return b.Error()
}
panic(fmt.Errorf("command %q is not a proper boolean response. %#v", client.APIName(command), resp))
}
// Request performs the given command
func (client *Client) Request(command Command) (interface{}, error) {
ctx, cancel := context.WithTimeout(context.Background(), client.Timeout)
defer cancel()
return client.RequestWithContext(ctx, command)
}
// RequestWithContext preforms a command with a context
func (client *Client) RequestWithContext(ctx context.Context, command Command) (interface{}, error) {
switch command.(type) {
case AsyncCommand:
return client.asyncRequest(ctx, command.(AsyncCommand))
default:
return client.SyncRequestWithContext(ctx, command)
}
}
// SyncRequest performs the command as is
func (client *Client) SyncRequest(command Command) (interface{}, error) {
ctx, cancel := context.WithTimeout(context.Background(), client.Timeout)
defer cancel()
return client.SyncRequestWithContext(ctx, command)
}
// AsyncRequest performs the given command
func (client *Client) AsyncRequest(asyncCommand AsyncCommand, callback WaitAsyncJobResultFunc) {
ctx, cancel := context.WithTimeout(context.Background(), client.Timeout)
defer cancel()
client.AsyncRequestWithContext(ctx, asyncCommand, callback)
}
// AsyncRequestWithContext preforms a request with a context
func (client *Client) AsyncRequestWithContext(ctx context.Context, asyncCommand AsyncCommand, callback WaitAsyncJobResultFunc) {
result, err := client.SyncRequestWithContext(ctx, asyncCommand)
if err != nil {
if !callback(nil, err) {
return
}
}
jobResult, ok := result.(*AsyncJobResult)
if !ok {
callback(nil, fmt.Errorf("wrong type, AsyncJobResult was expected instead of %T", result))
}
// Successful response
resp := req.asyncResponse()
if job.JobID == "" || job.JobStatus != Pending {
if err := json.Unmarshal(*job.JobResult, resp); err != nil {
return job, err
if jobResult.JobID == nil || jobResult.JobStatus != Pending {
callback(jobResult, nil)
// without a JobID, the next requests will only fail
return
}
for iteration := 0; ; iteration++ {
time.Sleep(client.RetryStrategy(int64(iteration)))
req := &QueryAsyncJobResult{JobID: jobResult.JobID}
resp, err := client.SyncRequestWithContext(ctx, req)
if err != nil && !callback(nil, err) {
return
}
return resp, nil
}
// we've got a pending job
result := &QueryAsyncJobResultResponse{
JobStatus: job.JobStatus,
}
for async.Retries > 0 && result.JobStatus == Pending {
time.Sleep(time.Duration(async.Delay) * time.Second)
async.Retries--
req := &QueryAsyncJobResult{JobID: job.JobID}
resp, err := exo.Request(req)
if err != nil {
return nil, err
result, ok := resp.(*AsyncJobResult)
if !ok {
if !callback(nil, fmt.Errorf("wrong type. AsyncJobResult expected, got %T", resp)) {
return
}
}
result = resp.(*QueryAsyncJobResultResponse)
}
if result.JobStatus == Failure {
if err := json.Unmarshal(*result.JobResult, &errorResponse); err != nil {
return nil, err
if !callback(result, nil) {
return
}
return errorResponse, errorResponse
}
if result.JobStatus == Pending {
return result, fmt.Errorf("Maximum number of retries reached")
}
if err := json.Unmarshal(*result.JobResult, resp); err != nil {
if err := json.Unmarshal(*result.JobResult, errorResponse); err != nil {
return nil, err
}
return errorResponse, errorResponse
}
return resp, nil
}
// BooleanRequest performs a sync request on a boolean call
func (exo *Client) BooleanRequest(req Command) error {
resp, err := exo.Request(req)
// Payload builds the HTTP request params from the given command
func (client *Client) Payload(command Command) (url.Values, error) {
params := url.Values{}
err := prepareValues("", params, command)
if err != nil {
return err
return params, err
}
return resp.(*booleanSyncResponse).Error()
}
// BooleanAsyncRequest performs a sync request on a boolean call
func (exo *Client) BooleanAsyncRequest(req AsyncCommand, async AsyncInfo) error {
resp, err := exo.AsyncRequest(req, async)
if err != nil {
return err
}
return resp.(*booleanAsyncResponse).Error()
}
// Request performs a sync request on a generic command
func (exo *Client) Request(req Command) (interface{}, error) {
body, err := exo.request(req.name(), req)
if err != nil {
return nil, err
}
resp := req.response()
if err := json.Unmarshal(body, resp); err != nil {
r := new(ErrorResponse)
if e := json.Unmarshal(body, r); e != nil {
return nil, r
if hookReq, ok := command.(onBeforeHook); ok {
if err := hookReq.onBeforeSend(params); err != nil {
return params, err
}
return nil, err
}
params.Set("apikey", client.APIKey)
params.Set("command", client.APIName(command))
params.Set("response", "json")
return params, nil
}
// Sign signs the HTTP request and returns the signature as as base64 encoding
func (client *Client) Sign(params url.Values) (string, error) {
query := encodeValues(params)
query = strings.ToLower(query)
mac := hmac.New(sha1.New, []byte(client.apiSecret))
_, err := mac.Write([]byte(query))
if err != nil {
return "", err
}
return resp, nil
signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
return signature, nil
}
// request makes a Request while being close to the metal
func (exo *Client) request(command string, req interface{}) (json.RawMessage, error) {
params := url.Values{}
err := prepareValues("", &params, req)
func (client *Client) request(ctx context.Context, command Command) (json.RawMessage, error) {
params, err := client.Payload(command)
if err != nil {
return nil, err
}
if hookReq, ok := req.(onBeforeHook); ok {
hookReq.onBeforeSend(&params)
signature, err := client.Sign(params)
if err != nil {
return nil, err
}
params.Set("apikey", exo.apiKey)
params.Set("command", command)
params.Set("response", "json")
params.Add("signature", signature)
method := "GET"
query := params.Encode()
url := fmt.Sprintf("%s?%s", client.Endpoint, query)
var body io.Reader
// respect Internet Explorer limit of 2048
if len(url) > 2048 {
url = client.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 := client.HTTPClient.Do(request)
if err != nil {
return nil, err
}
defer resp.Body.Close() // nolint: errcheck
contentType := resp.Header.Get("content-type")
if !strings.Contains(contentType, "application/json") {
return nil, fmt.Errorf(`body content-type response expected "application/json", got %q`, contentType)
}
text, err := client.parseResponse(resp, client.APIName(command))
if err != nil {
return nil, err
}
return text, nil
}
func encodeValues(params url.Values) string {
// This code is borrowed from net/url/url.go
// The way it's encoded by net/url doesn't match
// how CloudStack works.
// how CloudStack works to determine the signature.
//
// CloudStack only encodes the values of the query parameters
// and furthermore doesn't use '+' for whitespaces. Therefore
// after encoding the values all '+' are replaced with '%20'.
if params == nil {
return ""
}
var buf bytes.Buffer
keys := make([]string, 0, len(params))
for k := range params {
@ -338,7 +384,7 @@ func (exo *Client) request(command string, req interface{}) (json.RawMessage, er
sort.Strings(keys)
for _, k := range keys {
prefix := csEncode(k) + "="
prefix := k + "="
for _, v := range params[k] {
if buf.Len() > 0 {
buf.WriteByte('&')
@ -347,218 +393,5 @@ func (exo *Client) request(command string, req interface{}) (json.RawMessage, er
buf.WriteString(csEncode(v))
}
}
query := buf.String()
mac := hmac.New(sha1.New, []byte(exo.apiSecret))
mac.Write([]byte(strings.ToLower(query)))
signature := csEncode(base64.StdEncoding.EncodeToString(mac.Sum(nil)))
reader := strings.NewReader(fmt.Sprintf("%s&signature=%s", csQuotePlus(query), signature))
resp, err := exo.client.Post(exo.endpoint, "application/x-www-form-urlencoded", reader)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := exo.parseResponse(resp)
if err != nil {
return nil, err
}
return body, nil
}
// prepareValues uses a command to build a POST request
//
// command is not a Command so it's easier to Test
func prepareValues(prefix string, params *url.Values, command interface{}) error {
value := reflect.ValueOf(command)
typeof := reflect.TypeOf(command)
// Going up the pointer chain to find the underlying struct
for typeof.Kind() == reflect.Ptr {
typeof = typeof.Elem()
value = value.Elem()
}
for i := 0; i < typeof.NumField(); i++ {
field := typeof.Field(i)
val := value.Field(i)
tag := field.Tag
if json, ok := tag.Lookup("json"); ok {
n, required := extractJSONTag(field.Name, json)
name := prefix + n
switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
v := val.Int()
if v == 0 {
if required {
return fmt.Errorf("%s.%s (%v) is required, got 0", typeof.Name(), field.Name, val.Kind())
}
} else {
(*params).Set(name, strconv.FormatInt(v, 10))
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
v := val.Uint()
if v == 0 {
if required {
return fmt.Errorf("%s.%s (%v) is required, got 0", typeof.Name(), field.Name, val.Kind())
}
} else {
(*params).Set(name, strconv.FormatUint(v, 10))
}
case reflect.Float32, reflect.Float64:
v := val.Float()
if v == 0 {
if required {
return fmt.Errorf("%s.%s (%v) is required, got 0", typeof.Name(), field.Name, val.Kind())
}
} else {
(*params).Set(name, strconv.FormatFloat(v, 'f', -1, 64))
}
case reflect.String:
v := val.String()
if v == "" {
if required {
return fmt.Errorf("%s.%s (%v) is required, got \"\"", typeof.Name(), field.Name, val.Kind())
}
} else {
(*params).Set(name, v)
}
case reflect.Bool:
v := val.Bool()
if v == false {
if required {
params.Set(name, "false")
}
} else {
(*params).Set(name, "true")
}
case reflect.Slice:
switch field.Type.Elem().Kind() {
case reflect.Uint8:
switch field.Type {
case reflect.TypeOf(net.IPv4zero):
ip := (net.IP)(val.Bytes())
if ip == nil || ip.Equal(net.IPv4zero) {
if required {
return fmt.Errorf("%s.%s (%v) is required, got zero IPv4 address", typeof.Name(), field.Name, val.Kind())
}
} else {
(*params).Set(name, ip.String())
}
default:
if val.Len() == 0 {
if required {
return fmt.Errorf("%s.%s (%v) is required, got empty slice", typeof.Name(), field.Name, val.Kind())
}
} else {
v := val.Bytes()
(*params).Set(name, base64.StdEncoding.EncodeToString(v))
}
}
case reflect.String:
{
if val.Len() == 0 {
if required {
return fmt.Errorf("%s.%s (%v) is required, got empty slice", typeof.Name(), field.Name, val.Kind())
}
} else {
elems := make([]string, 0, val.Len())
for i := 0; i < val.Len(); i++ {
// XXX what if the value contains a comma? Double encode?
s := val.Index(i).String()
elems = append(elems, s)
}
(*params).Set(name, strings.Join(elems, ","))
}
}
default:
if val.Len() == 0 {
if required {
return fmt.Errorf("%s.%s (%v) is required, got empty slice", typeof.Name(), field.Name, val.Kind())
}
} else {
err := prepareList(name, params, val.Interface())
if err != nil {
return err
}
}
}
case reflect.Map:
if val.Len() == 0 {
if required {
return fmt.Errorf("%s.%s (%v) is required, got empty map", typeof.Name(), field.Name, val.Kind())
}
} else {
err := prepareMap(name, params, val.Interface())
if err != nil {
return err
}
}
default:
if required {
return fmt.Errorf("Unsupported type %s.%s (%v)", typeof.Name(), field.Name, val.Kind())
}
}
} else {
log.Printf("[SKIP] %s.%s no json label found", typeof.Name(), field.Name)
}
}
return nil
}
func prepareList(prefix string, params *url.Values, slice interface{}) error {
value := reflect.ValueOf(slice)
for i := 0; i < value.Len(); i++ {
prepareValues(fmt.Sprintf("%s[%d].", prefix, i), params, value.Index(i).Interface())
}
return nil
}
func prepareMap(prefix string, params *url.Values, m interface{}) error {
value := reflect.ValueOf(m)
for i, key := range value.MapKeys() {
var keyName string
var keyValue string
switch key.Kind() {
case reflect.String:
keyName = key.String()
default:
return fmt.Errorf("Only map[string]string are supported (XXX)")
}
val := value.MapIndex(key)
switch val.Kind() {
case reflect.String:
keyValue = val.String()
default:
return fmt.Errorf("Only map[string]string are supported (XXX)")
}
params.Set(fmt.Sprintf("%s[%d].%s", prefix, i, keyName), keyValue)
}
return nil
}
// extractJSONTag returns the variable name or defaultName as well as if the field is required (!omitempty)
func extractJSONTag(defaultName, jsonTag string) (string, bool) {
tags := strings.Split(jsonTag, ",")
name := tags[0]
required := true
for _, tag := range tags {
if tag == "omitempty" {
required = false
}
}
if name == "" || name == "omitempty" {
name = defaultName
}
return name, required
return buf.String()
}