provider/opc: update opc provider (#15159)

* provider/opc: update opc provider

* update opc-sdk
This commit is contained in:
Jake Champlin 2017-06-07 10:33:50 -04:00 committed by Paul Stack
parent 6b3ec0c193
commit f64b5c9480
8 changed files with 283 additions and 34 deletions

View File

@ -16,6 +16,7 @@ import (
const CMP_USERNAME = "/Compute-%s/%s"
const CMP_QUALIFIED_NAME = "%s/%s"
const DEFAULT_MAX_RETRIES = 1
// Client represents an authenticated compute client, with compute credentials and an api client.
type Client struct {
@ -26,6 +27,7 @@ type Client struct {
httpClient *http.Client
authCookie *http.Cookie
cookieIssued time.Time
maxRetries *int
logger opc.Logger
loglevel opc.LogLevelType
}
@ -38,6 +40,7 @@ func NewComputeClient(c *opc.Config) (*Client, error) {
password: c.Password,
apiEndpoint: c.APIEndpoint,
httpClient: c.HTTPClient,
maxRetries: c.MaxRetries,
loglevel: c.LogLevel,
}
@ -58,6 +61,16 @@ func NewComputeClient(c *opc.Config) (*Client, error) {
return nil, err
}
// Default max retries if unset
if c.MaxRetries == nil {
client.maxRetries = opc.Int(DEFAULT_MAX_RETRIES)
}
// Protect against any nil http client
if c.HTTPClient == nil {
return nil, fmt.Errorf("No HTTP client specified in config")
}
return client, nil
}
@ -108,7 +121,8 @@ func (c *Client) executeRequest(method, path string, body interface{}) (*http.Re
}
// Execute request with supplied client
resp, err := c.httpClient.Do(req)
resp, err := c.retryRequest(req)
//resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
@ -133,6 +147,47 @@ func (c *Client) executeRequest(method, path string, body interface{}) (*http.Re
return nil, oracleErr
}
// Allow retrying the request until it either returns no error,
// or we exceed the number of max retries
func (c *Client) retryRequest(req *http.Request) (*http.Response, error) {
// Double check maxRetries is not nil
var retries int
if c.maxRetries == nil {
retries = DEFAULT_MAX_RETRIES
} else {
retries = *c.maxRetries
}
var statusCode int
var errMessage string
for i := 0; i < retries; i++ {
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
return resp, nil
}
buf := new(bytes.Buffer)
buf.ReadFrom(resp.Body)
errMessage = buf.String()
statusCode = resp.StatusCode
c.debugLogString(fmt.Sprintf("Encountered HTTP (%d) Error: %s", statusCode, errMessage))
c.debugLogString(fmt.Sprintf("%d/%d retries left", i+1, retries))
}
oracleErr := &opc.OracleError{
StatusCode: statusCode,
Message: errMessage,
}
// We ran out of retries to make, return the error and response
return nil, oracleErr
}
func (c *Client) formatURL(path *url.URL) string {
return c.apiEndpoint.ResolveReference(path).String()
}

View File

@ -32,11 +32,20 @@ const (
InstanceRunning InstanceState = "running"
InstanceInitializing InstanceState = "initializing"
InstancePreparing InstanceState = "preparing"
InstanceStarting InstanceState = "starting"
InstanceStopping InstanceState = "stopping"
InstanceShutdown InstanceState = "shutdown"
InstanceQueued InstanceState = "queued"
InstanceError InstanceState = "error"
)
type InstanceDesiredState string
const (
InstanceDesiredRunning InstanceDesiredState = "running"
InstanceDesiredShutdown InstanceDesiredState = "shutdown"
)
// InstanceInfo represents the Compute API's view of the state of an instance.
type InstanceInfo struct {
// The ID for the instance. Set by the SDK based on the request - not the API.
@ -55,6 +64,9 @@ type InstanceInfo struct {
// The default domain to use for the hostname and DNS lookups
Domain string `json:"domain"`
// The desired state of an instance
DesiredState InstanceDesiredState `json:"desired_state"`
// Optional ImageListEntry number. Default will be used if not specified
Entry int `json:"entry"`
@ -160,6 +172,10 @@ type CreateInstanceInput struct {
// Boot order list
// Optional
BootOrder []int `json:"boot_order"`
// The desired state of the opc instance. Can only be `running` or `shutdown`
// Omits if empty.
// Optional
DesiredState InstanceDesiredState `json:"desired_state,omitempty"`
// The host name assigned to the instance. On an Oracle Linux instance,
// this host name is displayed in response to the hostname command.
// Only relative DNS is supported. The domain name is suffixed to the host name
@ -374,6 +390,55 @@ func (c *InstancesClient) GetInstance(input *GetInstanceInput) (*InstanceInfo, e
return &responseBody, nil
}
type UpdateInstanceInput struct {
// Name of this instance, generated by the server.
// Required
Name string `json:"name"`
// The desired state of the opc instance. Can only be `running` or `shutdown`
// Omits if empty.
// Optional
DesiredState InstanceDesiredState `json:"desired_state,omitempty"`
// The ID of the instance
// Required
ID string `json:"-"`
// A list of tags to be supplied to the instance
// Optional
Tags []string `json:"tags,omitempty"`
}
func (g *UpdateInstanceInput) String() string {
return fmt.Sprintf(CMP_QUALIFIED_NAME, g.Name, g.ID)
}
func (c *InstancesClient) UpdateInstance(input *UpdateInstanceInput) (*InstanceInfo, error) {
if input.Name == "" || input.ID == "" {
return nil, errors.New("Both instance name and ID need to be specified")
}
input.Name = fmt.Sprintf(CMP_QUALIFIED_NAME, c.getUserName(), input.Name)
var responseBody InstanceInfo
if err := c.updateResource(input.String(), input, &responseBody); err != nil {
return nil, err
}
getInput := &GetInstanceInput{
Name: input.Name,
ID: input.ID,
}
// Wait for the correct instance action depending on the current desired state.
// If the instance is already running, and the desired state is to be "running", the
// wait loop will only execute a single time to verify the instance state. Otherwise
// we wait until the correct action has finalized, either a shutdown or restart, catching
// any intermittent errors during the process.
if responseBody.DesiredState == InstanceDesiredRunning {
return c.WaitForInstanceRunning(getInput, WaitForInstanceReadyTimeout)
} else {
return c.WaitForInstanceShutdown(getInput, WaitForInstanceDeleteTimeout)
}
}
type DeleteInstanceInput struct {
// The Unqualified Name of this Instance
Name string
@ -407,7 +472,7 @@ func (c *InstancesClient) WaitForInstanceRunning(input *GetInstanceInput, timeou
switch s := info.State; s {
case InstanceError:
return false, fmt.Errorf("Error initializing instance: %s", info.ErrorReason)
case InstanceRunning:
case InstanceRunning: // Target State
c.debugLogString("Instance Running")
return true, nil
case InstanceQueued:
@ -419,6 +484,47 @@ func (c *InstancesClient) WaitForInstanceRunning(input *GetInstanceInput, timeou
case InstancePreparing:
c.debugLogString("Instance Preparing")
return false, nil
case InstanceStarting:
c.debugLogString("Instance Starting")
return false, nil
default:
c.debugLogString(fmt.Sprintf("Unknown instance state: %s, waiting", s))
return false, nil
}
})
return info, err
}
// WaitForInstanceShutdown waits for an instance to be shutdown
func (c *InstancesClient) WaitForInstanceShutdown(input *GetInstanceInput, timeoutSeconds int) (*InstanceInfo, error) {
var info *InstanceInfo
var getErr error
err := c.waitFor("instance to be shutdown", timeoutSeconds, func() (bool, error) {
info, getErr = c.GetInstance(input)
if getErr != nil {
return false, getErr
}
switch s := info.State; s {
case InstanceError:
return false, fmt.Errorf("Error initializing instance: %s", info.ErrorReason)
case InstanceRunning:
c.debugLogString("Instance Running")
return false, nil
case InstanceQueued:
c.debugLogString("Instance Queuing")
return false, nil
case InstanceInitializing:
c.debugLogString("Instance Initializing")
return false, nil
case InstancePreparing:
c.debugLogString("Instance Preparing")
return false, nil
case InstanceStarting:
c.debugLogString("Instance Starting")
return false, nil
case InstanceShutdown: // Target State
c.debugLogString("Instance Shutdown")
return true, nil
default:
c.debugLogString(fmt.Sprintf("Unknown instance state: %s, waiting", s))
return false, nil

View File

@ -3,16 +3,14 @@ package compute
import (
"bytes"
"encoding/json"
"log"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
"log"
"github.com/hashicorp/go-oracle-terraform/opc"
)

View File

@ -3,3 +3,7 @@ package opc
func String(v string) *string {
return &v
}
func Int(v int) *int {
return &v
}

View File

@ -1,6 +1,7 @@
package opc
import (
"crypto/tls"
"fmt"
"log"
"net/url"
@ -13,16 +14,17 @@ import (
)
type Config struct {
User string
Password string
IdentityDomain string
Endpoint string
MaxRetryTimeout int
User string
Password string
IdentityDomain string
Endpoint string
MaxRetries int
Insecure bool
}
type OPCClient struct {
Client *compute.Client
MaxRetryTimeout int
Client *compute.Client
MaxRetries int
}
func (c *Config) Client() (*compute.Client, error) {
@ -36,7 +38,7 @@ func (c *Config) Client() (*compute.Client, error) {
Username: &c.User,
Password: &c.Password,
APIEndpoint: u,
HTTPClient: cleanhttp.DefaultClient(),
MaxRetries: &c.MaxRetries,
}
if logging.IsDebugOrHigher() {
@ -44,6 +46,18 @@ func (c *Config) Client() (*compute.Client, error) {
config.Logger = opcLogger{}
}
// Setup HTTP Client based on insecure
httpClient := cleanhttp.DefaultClient()
if c.Insecure {
transport := cleanhttp.DefaultTransport()
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
httpClient.Transport = transport
}
config.HTTPClient = httpClient
return compute.NewComputeClient(&config)
}

View File

@ -36,12 +36,18 @@ func Provider() terraform.ResourceProvider {
Description: "The HTTP endpoint for OPC API operations.",
},
// TODO Actually implement this
"max_retry_timeout": {
"max_retries": {
Type: schema.TypeInt,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("OPC_MAX_RETRY_TIMEOUT", 3000),
Description: "Max num seconds to wait for successful response when operating on resources within OPC (defaults to 3000)",
DefaultFunc: schema.EnvDefaultFunc("OPC_MAX_RETRIES", 1),
Description: "Maximum number retries to wait for a successful response when operating on resources within OPC (defaults to 1)",
},
"insecure": {
Type: schema.TypeBool,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("OPC_INSECURE", false),
Description: "Skip TLS Verification for self-signed certificates. Should only be used if absolutely required.",
},
},
@ -82,11 +88,12 @@ func Provider() terraform.ResourceProvider {
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
User: d.Get("user").(string),
Password: d.Get("password").(string),
IdentityDomain: d.Get("identity_domain").(string),
Endpoint: d.Get("endpoint").(string),
MaxRetryTimeout: d.Get("max_retry_timeout").(int),
User: d.Get("user").(string),
Password: d.Get("password").(string),
IdentityDomain: d.Get("identity_domain").(string),
Endpoint: d.Get("endpoint").(string),
MaxRetries: d.Get("max_retries").(int),
Insecure: d.Get("insecure").(bool),
}
return config.Client()

View File

@ -18,6 +18,7 @@ func resourceInstance() *schema.Resource {
return &schema.Resource{
Create: resourceInstanceCreate,
Read: resourceInstanceRead,
Update: resourceInstanceUpdate,
Delete: resourceInstanceDelete,
Importer: &schema.ResourceImporter{
State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
@ -84,6 +85,12 @@ func resourceInstance() *schema.Resource {
ForceNew: true,
},
"desired_state": {
Type: schema.TypeString,
Optional: true,
Default: compute.InstanceDesiredRunning,
},
"networking_info": {
Type: schema.TypeSet,
Optional: true,
@ -213,6 +220,14 @@ func resourceInstance() *schema.Resource {
"storage": {
Type: schema.TypeSet,
Optional: true,
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
desired := compute.InstanceDesiredState(d.Get("desired_state").(string))
state := compute.InstanceState(d.Get("state").(string))
if desired == compute.InstanceDesiredShutdown || state == compute.InstanceShutdown {
return true
}
return false
},
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
@ -265,6 +280,11 @@ func resourceInstance() *schema.Resource {
Computed: true,
},
"fqdn": {
Type: schema.TypeString,
Computed: true,
},
"image_format": {
Type: schema.TypeString,
Computed: true,
@ -456,7 +476,13 @@ func updateInstanceAttributes(d *schema.ResourceData, instance *compute.Instance
if err := setIntList(d, "boot_order", instance.BootOrder); err != nil {
return err
}
d.Set("hostname", instance.Hostname)
split_hostname := strings.Split(instance.Hostname, ".")
if len(split_hostname) == 0 {
return fmt.Errorf("Unable to parse hostname: %s", instance.Hostname)
}
d.Set("hostname", split_hostname[0])
d.Set("fqdn", instance.Hostname)
d.Set("image_list", instance.ImageList)
d.Set("label", instance.Label)
@ -482,6 +508,7 @@ func updateInstanceAttributes(d *schema.ResourceData, instance *compute.Instance
d.Set("fingerprint", instance.Fingerprint)
d.Set("image_format", instance.ImageFormat)
d.Set("ip_address", instance.IPAddress)
d.Set("desired_state", instance.DesiredState)
if err := setStringList(d, "placement_requirements", instance.PlacementRequirements); err != nil {
return err
@ -514,6 +541,36 @@ func updateInstanceAttributes(d *schema.ResourceData, instance *compute.Instance
return nil
}
func resourceInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*compute.Client).Instances()
name := d.Get("name").(string)
input := &compute.UpdateInstanceInput{
Name: name,
ID: d.Id(),
}
if d.HasChange("desired_state") {
input.DesiredState = compute.InstanceDesiredState(d.Get("desired_state").(string))
}
if d.HasChange("tags") {
tags := getStringList(d, "tags")
input.Tags = tags
}
result, err := client.UpdateInstance(input)
if err != nil {
return fmt.Errorf("Error updating instance %s: %s", input.Name, err)
}
log.Printf("[DEBUG] Updated instance %s: %#v", result.Name, result.ID)
return resourceInstanceRead(d, meta)
}
func resourceInstanceDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*compute.Client).Instances()

30
vendor/vendor.json vendored
View File

@ -2078,22 +2078,28 @@
"revision": "d30f09973e19c1dfcd120b2d9c4f168e68d6b5d5"
},
{
"checksumSHA1": "gyD43l5lH8orGQl7TiqLHRNFtFU=",
"checksumSHA1": "pUFuRSzqgTm6BiYFRTgNj3LnvVU=",
"path": "github.com/hashicorp/go-oracle-terraform/compute",
"revision": "cf979675cc5074d460720f7b97626687bcb1e203",
"revisionTime": "2017-05-09T16:19:50Z"
"revision": "f1c5e84899f45d77e94d06bd3c9e61be2165f584",
"revisionTime": "2017-05-31T19:26:20Z",
"version": "v0.1.1",
"versionExact": "v0.1.1"
},
{
"checksumSHA1": "DzK7lYwHt5Isq5Zf73cnQqBO2LI=",
"path": "github.com/hashicorp/go-oracle-terraform/helper",
"revision": "cf979675cc5074d460720f7b97626687bcb1e203",
"revisionTime": "2017-05-09T16:19:50Z"
"revision": "f1c5e84899f45d77e94d06bd3c9e61be2165f584",
"revisionTime": "2017-05-31T19:26:20Z",
"version": "v0.1.1",
"versionExact": "v0.1.1"
},
{
"checksumSHA1": "AyNRs19Es9pDw2VMxVKWuLx3Afg=",
"checksumSHA1": "JbuYtbJkx3r1BLuCMymkri0Q1BI=",
"path": "github.com/hashicorp/go-oracle-terraform/opc",
"revision": "cf979675cc5074d460720f7b97626687bcb1e203",
"revisionTime": "2017-05-09T16:19:50Z"
"revision": "f1c5e84899f45d77e94d06bd3c9e61be2165f584",
"revisionTime": "2017-05-31T19:26:20Z",
"version": "v0.1.1",
"versionExact": "v0.1.1"
},
{
"checksumSHA1": "b0nQutPMJHeUmz4SjpreotAo6Yk=",
@ -2263,10 +2269,12 @@
"revisionTime": "2017-03-08T19:39:51Z"
},
{
"checksumSHA1": "NWP140S/k5J2L9nLVVgrt9rEh1g=",
"checksumSHA1": "zYSPNTuMDAnTygPEmi8gNeBP/40=",
"path": "github.com/hashicorp/terraform-provider-opc/opc",
"revision": "bf837a8edaadefbac871feb8560faa60f811c8d9",
"revisionTime": "2017-05-23T21:46:41Z"
"revision": "291363639bb9403d0c76a6515f04e4b1b680b07f",
"revisionTime": "2017-06-07T13:48:21Z",
"version": "v0.1.3",
"versionExact": "v0.1.3"
},
{
"checksumSHA1": "2fkVZIzvxIGBLhSiVnkTgGiqpQ4=",