Skip to content

Commit

Permalink
Upgrade metal device resource and data source to use timeouts
Browse files Browse the repository at this point in the history
  • Loading branch information
aayushrangwala committed Aug 23, 2023
1 parent d672cd5 commit fa9d102
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 85 deletions.
5 changes: 3 additions & 2 deletions equinix/data_source_metal_device.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package equinix

import (
"context"
"encoding/json"
"fmt"
"path"
Expand All @@ -14,7 +15,7 @@ import (

func dataSourceMetalDevice() *schema.Resource {
return &schema.Resource{
Read: dataSourceMetalDeviceRead,
ReadWithoutTimeout: diagnosticsWrapper(dataSourceMetalDeviceRead),
Schema: map[string]*schema.Schema{
"hostname": {
Type: schema.TypeString,
Expand Down Expand Up @@ -201,7 +202,7 @@ func dataSourceMetalDevice() *schema.Resource {
}
}

func dataSourceMetalDeviceRead(d *schema.ResourceData, meta interface{}) error {
func dataSourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
client := meta.(*Config).metal

hostnameRaw, hostnameOK := d.GetOk("hostname")
Expand Down
66 changes: 8 additions & 58 deletions equinix/helpers_device.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package equinix

import (
"context"
"errors"
"fmt"
"log"
"strings"
"sync"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/packethost/packngo"
Expand Down Expand Up @@ -142,7 +143,7 @@ func hwReservationStateRefreshFunc(client *packngo.Client, reservationId, instan
}
}

func waitUntilReservationProvisionable(client *packngo.Client, reservationId, instanceId string, delay, timeout, minTimeout time.Duration) error {
func waitUntilReservationProvisionable(ctx context.Context, client *packngo.Client, reservationId, instanceId string, delay, timeout, minTimeout time.Duration) error {
stateConf := &retry.StateChangeConf{
Pending: []string{deprovisioning},
Target: []string{provisionable, reprovisioned},
Expand All @@ -151,7 +152,7 @@ func waitUntilReservationProvisionable(client *packngo.Client, reservationId, in
Delay: delay,
MinTimeout: minTimeout,
}
_, err := stateConf.WaitForState()
_, err := stateConf.WaitForStateContext(ctx)
return err
}

Expand All @@ -166,7 +167,7 @@ func getWaitForDeviceLock(deviceID string) *sync.WaitGroup {
return wg
}

func waitForDeviceAttribute(d *schema.ResourceData, targets []string, pending []string, attribute string, meta interface{}) (string, error) {
func waitForDeviceAttribute(ctx context.Context, d *schema.ResourceData, stateConf *retry.StateChangeConf) (string, error) {
wg := getWaitForDeviceLock(d.Id())
wg.Wait()

Expand All @@ -180,34 +181,11 @@ func waitForDeviceAttribute(d *schema.ResourceData, targets []string, pending []
wgMutex.Unlock()
}()

if attribute != "state" && attribute != "network_type" {
return "", fmt.Errorf("unsupported attr to wait for: %s", attribute)
if stateConf == nil || stateConf.Refresh == nil {
return "", errors.New("invalid stateconf to wait for")
}

stateConf := &retry.StateChangeConf{
Pending: pending,
Target: targets,
Refresh: func() (interface{}, string, error) {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

device, _, err := client.Devices.Get(d.Id(), &packngo.GetOptions{Includes: []string{"project"}})
if err == nil {
retAttrVal := device.State
if attribute == "network_type" {
networkType := device.GetNetworkType()
retAttrVal = networkType
}
return retAttrVal, retAttrVal, nil
}
return "error", "error", err
},
Timeout: 60 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}

attrValRaw, err := stateConf.WaitForState()
attrValRaw, err := stateConf.WaitForStateContext(ctx)

if v, ok := attrValRaw.(string); ok {
return v, err
Expand All @@ -216,34 +194,6 @@ func waitForDeviceAttribute(d *schema.ResourceData, targets []string, pending []
return "", err
}

// powerOnAndWait Powers on the device and waits for it to be active.
func powerOnAndWait(d *schema.ResourceData, meta interface{}) error {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

_, err := client.Devices.PowerOn(d.Id())
if err != nil {
return friendlyError(err)
}

_, err = waitForDeviceAttribute(d, []string{"active", "failed"}, []string{"off"}, "state", client)
if err != nil {
return err
}
state := d.Get("state").(string)
if state != "active" {
return friendlyError(fmt.Errorf("device in non-active state \"%s\"", state))
}
return nil
}

func validateFacilityForDevice(v interface{}, k string) (ws []string, errors []error) {
if v.(string) == "any" {
errors = append(errors, fmt.Errorf(`cannot use facility: "any"`))
}
return
}

func ipAddressSchema() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
Expand Down
10 changes: 10 additions & 0 deletions equinix/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,3 +444,13 @@ func schemaSetToMap(set *schema.Set) map[int]interface{} {
}
return transformed
}

func diagnosticsWrapper(fn func(ctx context.Context, d *schema.ResourceData, meta interface{}) error) func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
if err := fn(ctx, d, meta); err != nil {
return diag.FromErr(err)
}

return nil
}
}
71 changes: 49 additions & 22 deletions equinix/resource_metal_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
"sort"
"time"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
Expand All @@ -37,12 +37,12 @@ func resourceMetalDevice() *schema.Resource {
Update: schema.DefaultTimeout(30 * time.Minute),
Delete: schema.DefaultTimeout(30 * time.Minute),
},
Create: resourceMetalDeviceCreate,
Read: resourceMetalDeviceRead,
Update: resourceMetalDeviceUpdate,
Delete: resourceMetalDeviceDelete,
CreateContext: diagnosticsWrapper(resourceMetalDeviceCreate),
ReadWithoutTimeout: diagnosticsWrapper(resourceMetalDeviceRead),
UpdateContext: diagnosticsWrapper(resourceMetalDeviceUpdate),
DeleteContext: diagnosticsWrapper(resourceMetalDeviceDelete),
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
"project_id": {
Expand Down Expand Up @@ -468,7 +468,7 @@ func reinstallDisabledAndNoChangesAllowed(attribute string) customdiff.ResourceC
}
}

func resourceMetalDeviceCreate(d *schema.ResourceData, meta interface{}) error {
func resourceMetalDeviceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

Expand All @@ -492,7 +492,7 @@ func resourceMetalDeviceCreate(d *schema.ResourceData, meta interface{}) error {
metroRaw, metroOk := d.GetOk("metro")

if !facsOk && !metroOk {
return friendlyError(errors.New("one of facilies and metro must be configured"))
return errors.New("one of facilies and metro must be configured")
}

if facsOk {
Expand Down Expand Up @@ -575,16 +575,17 @@ func resourceMetalDeviceCreate(d *schema.ResourceData, meta interface{}) error {
if attr, ok := d.GetOk("storage"); ok {
s, err := structure.NormalizeJsonString(attr.(string))
if err != nil {
return errwrap.Wrapf("storage param contains invalid JSON: {{err}}", err)
return fmt.Errorf("storage param contains invalid JSON: %s", err)
}
var cpr packngo.CPR
err = json.Unmarshal([]byte(s), &cpr)
if err != nil {
return errwrap.Wrapf("Error parsing Storage string: {{err}}", err)
return fmt.Errorf("error parsing Storage string: %s", err)
}
createRequest.Storage = &cpr
}

start := time.Now()
newDevice, _, err := client.Devices.Create(createRequest)
if err != nil {
retErr := friendlyError(err)
Expand All @@ -596,14 +597,15 @@ func resourceMetalDeviceCreate(d *schema.ResourceData, meta interface{}) error {

d.SetId(newDevice.ID)

if err = waitForActiveDevice(d, meta); err != nil {
createTimeout := d.Timeout(schema.TimeoutCreate) - time.Minute - time.Since(start)
if err = waitForActiveDevice(ctx, d, meta, createTimeout); err != nil {
return err
}

return resourceMetalDeviceRead(d, meta)
return resourceMetalDeviceRead(ctx, d, meta)
}

func resourceMetalDeviceRead(d *schema.ResourceData, meta interface{}) error {
func resourceMetalDeviceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

Expand Down Expand Up @@ -706,7 +708,7 @@ func resourceMetalDeviceRead(d *schema.ResourceData, meta interface{}) error {
return nil
}

func resourceMetalDeviceUpdate(d *schema.ResourceData, meta interface{}) error {
func resourceMetalDeviceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

Expand Down Expand Up @@ -761,20 +763,22 @@ func resourceMetalDeviceUpdate(d *schema.ResourceData, meta interface{}) error {
dPXE := d.Get("always_pxe").(bool)
ur.AlwaysPXE = &dPXE
}

start := time.Now()
if !reflect.DeepEqual(ur, packngo.DeviceUpdateRequest{}) {
if _, _, err := client.Devices.Update(d.Id(), &ur); err != nil {
return friendlyError(err)
}
}

if err := doReinstall(client, d, meta); err != nil {
if err := doReinstall(ctx, client, d, meta, start); err != nil {
return err
}

return resourceMetalDeviceRead(d, meta)
return resourceMetalDeviceRead(ctx, d, meta)
}

func doReinstall(client *packngo.Client, d *schema.ResourceData, meta interface{}) error {
func doReinstall(ctx context.Context, client *packngo.Client, d *schema.ResourceData, meta interface{}, start time.Time) error {
if d.HasChange("operating_system") || d.HasChange("user_data") || d.HasChange("custom_data") {
reinstall, ok := d.GetOk("reinstall")

Expand Down Expand Up @@ -802,15 +806,16 @@ func doReinstall(client *packngo.Client, d *schema.ResourceData, meta interface{
return friendlyError(err)
}

if err := waitForActiveDevice(d, meta); err != nil {
deleteTimeout := d.Timeout(schema.TimeoutUpdate) - time.Minute - time.Since(start)
if err := waitForActiveDevice(ctx, d, meta, deleteTimeout); err != nil {
return err
}
}

return nil
}

func resourceMetalDeviceDelete(d *schema.ResourceData, meta interface{}) error {
func resourceMetalDeviceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) error {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

Expand All @@ -834,7 +839,7 @@ func resourceMetalDeviceDelete(d *schema.ResourceData, meta interface{}) error {
// avoid "context: deadline exceeded"
timeout := d.Timeout(schema.TimeoutDelete) - time.Minute - time.Since(start)

err := waitUntilReservationProvisionable(client, resId.(string), d.Id(), 10*time.Second, timeout, 3*time.Second)
err := waitUntilReservationProvisionable(ctx, client, resId.(string), d.Id(), 10*time.Second, timeout, 3*time.Second)
if err != nil {
return err
}
Expand All @@ -843,9 +848,31 @@ func resourceMetalDeviceDelete(d *schema.ResourceData, meta interface{}) error {
return nil
}

func waitForActiveDevice(d *schema.ResourceData, meta interface{}) error {
func waitForActiveDevice(ctx context.Context, d *schema.ResourceData, meta interface{}, timeout time.Duration) error {
targets := []string{"active", "failed"}
pending := []string{"queued", "provisioning", "reinstalling"}

stateConf := &retry.StateChangeConf{
Pending: pending,
Target: targets,
Refresh: func() (interface{}, string, error) {
meta.(*Config).addModuleToMetalUserAgent(d)
client := meta.(*Config).metal

device, _, err := client.Devices.Get(d.Id(), &packngo.GetOptions{Includes: []string{"project"}})
if err == nil {
retAttrVal := device.State
return retAttrVal, retAttrVal, nil
}
return "error", "error", err
},
Timeout: timeout,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}

// Wait for the device so we can get the networking attributes that show up after a while.
state, err := waitForDeviceAttribute(d, []string{"active", "failed"}, []string{"queued", "provisioning", "reinstalling"}, "state", meta)
state, err := waitForDeviceAttribute(ctx, d, stateConf)
if err != nil {
d.SetId("")
fErr := friendlyError(err)
Expand Down
Loading

0 comments on commit fa9d102

Please sign in to comment.