diff --git a/client/client.go b/client/client.go index e3818f8a..a2a84c0d 100644 --- a/client/client.go +++ b/client/client.go @@ -119,16 +119,31 @@ type XOClient interface { } type Client struct { - rpc jsonrpc2.JSONRPC2 - httpClient http.Client - restApiURL *url.URL + RetryMode RetryMode + RetryMaxTime time.Duration + rpc jsonrpc2.JSONRPC2 + httpClient http.Client + restApiURL *url.URL } +type RetryMode int + +const ( + None RetryMode = iota // specifies that no retries will be made + // Specifies that exponential backoff will be used for certain retryable errors. When + // a guest is booting there is the potential for a race condition if the given action + // relies on the existance of a PV driver (unplugging / plugging a device). This open + // allows the provider to retry these errors until the guest is initialized. + Backoff +) + type Config struct { Url string Username string Password string InsecureSkipVerify bool + RetryMode RetryMode + RetryMaxTime time.Duration } var dialer = gorillawebsocket.Dialer{ @@ -223,13 +238,20 @@ func NewClient(config Config) (XOClient, error) { }, } return &Client{ - rpc: c, - httpClient: httpClient, - restApiURL: restApiURL, + RetryMode: config.RetryMode, + RetryMaxTime: config.RetryMaxTime, + rpc: c, + httpClient: httpClient, + restApiURL: restApiURL, }, nil } -func IsRetryableError(err jsonrpc2.Error) bool { +func (c *Client) IsRetryableError(err jsonrpc2.Error) bool { + + if c.RetryMode == None { + return false + } + // Error code 11 corresponds to an error condition where a VM is missing PV drivers. // https://github.com/vatesfr/xen-orchestra/blob/a3a2fda157fa30af4b93d34c99bac550f7c82bbc/packages/xo-common/api-errors.js#L95 @@ -262,7 +284,7 @@ func (c *Client) Call(method string, params, result interface{}) error { return backoff.Permanent(err) } - if IsRetryableError(*rpcErr) { + if c.IsRetryableError(*rpcErr) { return err } @@ -278,6 +300,7 @@ func (c *Client) Call(method string, params, result interface{}) error { } bo := backoff.NewExponentialBackOff() + bo.MaxElapsedTime = c.RetryMaxTime return backoff.Retry(operation, bo) } diff --git a/xoa/provider.go b/xoa/provider.go index 6a7b7b0e..87e22baf 100644 --- a/xoa/provider.go +++ b/xoa/provider.go @@ -1,8 +1,21 @@ package xoa import ( + "errors" + "fmt" + "regexp" + "time" + "github.com/ddelnano/terraform-provider-xenorchestra/client" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +var ( + retryModeMap = map[string]client.RetryMode{ + "none": client.None, + "backoff": client.Backoff, + } ) func Provider() *schema.Provider { @@ -33,6 +46,20 @@ func Provider() *schema.Provider { DefaultFunc: schema.EnvDefaultFunc("XOA_INSECURE", nil), Description: "Whether SSL should be verified or not. Can be set via the XOA_INSECURE environment variable.", }, + "retry_mode": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("XOA_RETRY_MODE", "backoff"), + Description: "Specifies if retries should be attempted for requests that require eventual . Can be set via the XOA_RETRY_MODE environment variable.", + ValidateFunc: validation.StringInSlice([]string{"backoff", "none"}, false), + }, + "retry_max_time": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("XOA_RETRY_MAX_TIME", "5m"), + Description: "If `retry_mode` is set, this specifies the duration for which the backoff method will continue retries. Can be set via the `XOA_RETRY_MAX_TIME` environment variable", + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[0-9]+(\.[0-9]+)?(ms|s|m|h)$`), "must be a number immediately followed by ms (milliseconds), s (seconds), m (minutes), or h (hours). For example, \"30s\" for 30 seconds."), + }, }, ResourcesMap: map[string]*schema.Resource{ "xenorchestra_acl": resourceAcl(), @@ -66,11 +93,26 @@ func xoaConfigure(d *schema.ResourceData) (interface{}, error) { username := d.Get("username").(string) password := d.Get("password").(string) insecure := d.Get("insecure").(bool) + retryMode := d.Get("retry_mode").(string) + retryMaxTime := d.Get("retry_max_time").(string) + + duration, err := time.ParseDuration(retryMaxTime) + if err != nil { + return client.Config{}, err + } + + retry, ok := retryModeMap[retryMode] + if !ok { + return client.Config{}, errors.New(fmt.Sprintf("retry mode provided invalid: %s", retryMode)) + } + config := client.Config{ Url: url, Username: username, Password: password, InsecureSkipVerify: insecure, + RetryMode: retry, + RetryMaxTime: duration, } return client.NewClient(config) }