Skip to content

Commit

Permalink
Merge pull request #4676 from betawaffle/packet-failure-handling
Browse files Browse the repository at this point in the history
Handle external state changes for Packet resources gracefully.
  • Loading branch information
phinze committed Jan 14, 2016
2 parents ea45958 + a1935a1 commit 6bc93ba
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 120 deletions.
2 changes: 1 addition & 1 deletion builtin/providers/packet/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Config struct {
AuthToken string
}

// Client() returns a new client for accessing packet.
// Client() returns a new client for accessing Packet's API.
func (c *Config) Client() *packngo.Client {
return packngo.NewClient(consumerToken, c.AuthToken, cleanhttp.DefaultClient())
}
43 changes: 43 additions & 0 deletions builtin/providers/packet/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package packet

import (
"net/http"
"strings"

"github.com/packethost/packngo"
)

func friendlyError(err error) error {
if e, ok := err.(*packngo.ErrorResponse); ok {
return &ErrorResponse{
StatusCode: e.Response.StatusCode,
Errors: Errors(e.Errors),
}
}
return err
}

func isForbidden(err error) bool {
if r, ok := err.(*ErrorResponse); ok {
return r.StatusCode == http.StatusForbidden
}
return false
}

func isNotFound(err error) bool {
if r, ok := err.(*ErrorResponse); ok {
return r.StatusCode == http.StatusNotFound
}
return false
}

type Errors []string

func (e Errors) Error() string {
return strings.Join(e, "; ")
}

type ErrorResponse struct {
StatusCode int
Errors
}
3 changes: 1 addition & 2 deletions builtin/providers/packet/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"github.com/hashicorp/terraform/terraform"
)

// Provider returns a schema.Provider for Packet.
// Provider returns a schema.Provider for managing Packet infrastructure.
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
Expand All @@ -31,6 +31,5 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{
AuthToken: d.Get("auth_token").(string),
}

return config.Client(), nil
}
121 changes: 54 additions & 67 deletions builtin/providers/packet/resource_packet_device.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package packet

import (
"errors"
"fmt"
"log"
"time"

"github.com/hashicorp/terraform/helper/resource"
Expand Down Expand Up @@ -146,22 +146,23 @@ func resourcePacketDeviceCreate(d *schema.ResourceData, meta interface{}) error
}
}

log.Printf("[DEBUG] Device create configuration: %#v", createRequest)

newDevice, _, err := client.Devices.Create(createRequest)
if err != nil {
return fmt.Errorf("Error creating device: %s", err)
return friendlyError(err)
}

// Assign the device id
d.SetId(newDevice.ID)

log.Printf("[INFO] Device ID: %s", d.Id())

_, err = WaitForDeviceAttribute(d, "active", []string{"queued", "provisioning"}, "state", meta)
// Wait for the device so we can get the networking attributes that show up after a while.
_, err = waitForDeviceAttribute(d, "active", []string{"queued", "provisioning"}, "state", meta)
if err != nil {
return fmt.Errorf(
"Error waiting for device (%s) to become ready: %s", d.Id(), err)
if isForbidden(err) {
// If the device doesn't get to the active state, we can't recover it from here.
d.SetId("")

return errors.New("provisioning time limit exceeded; the Packet team will investigate")
}
return err
}

return resourcePacketDeviceRead(d, meta)
Expand All @@ -170,10 +171,17 @@ func resourcePacketDeviceCreate(d *schema.ResourceData, meta interface{}) error
func resourcePacketDeviceRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client)

// Retrieve the device properties for updating the state
device, _, err := client.Devices.Get(d.Id())
if err != nil {
return fmt.Errorf("Error retrieving device: %s", err)
err = friendlyError(err)

// If the device somehow already destroyed, mark as succesfully gone.
if isNotFound(err) {
d.SetId("")
return nil
}

return err
}

d.Set("name", device.Hostname)
Expand All @@ -186,35 +194,36 @@ func resourcePacketDeviceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("created", device.Created)
d.Set("updated", device.Updated)

tags := make([]string, 0)
tags := make([]string, 0, len(device.Tags))
for _, tag := range device.Tags {
tags = append(tags, tag)
}
d.Set("tags", tags)

provisionerAddress := ""

networks := make([]map[string]interface{}, 0, 1)
var (
host string
networks = make([]map[string]interface{}, 0, 1)
)
for _, ip := range device.Network {
network := make(map[string]interface{})
network["address"] = ip.Address
network["gateway"] = ip.Gateway
network["family"] = ip.Family
network["cidr"] = ip.Cidr
network["public"] = ip.Public
network := map[string]interface{}{
"address": ip.Address,
"gateway": ip.Gateway,
"family": ip.Family,
"cidr": ip.Cidr,
"public": ip.Public,
}
networks = append(networks, network)

if ip.Family == 4 && ip.Public == true {
provisionerAddress = ip.Address
host = ip.Address
}
}
d.Set("network", networks)

log.Printf("[DEBUG] Provisioner Address set to %v", provisionerAddress)

if provisionerAddress != "" {
if host != "" {
d.SetConnInfo(map[string]string{
"type": "ssh",
"host": provisionerAddress,
"host": host,
})
}

Expand All @@ -224,19 +233,15 @@ func resourcePacketDeviceRead(d *schema.ResourceData, meta interface{}) error {
func resourcePacketDeviceUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client)

if d.HasChange("locked") && d.Get("locked").(bool) {
_, err := client.Devices.Lock(d.Id())

if err != nil {
return fmt.Errorf(
"Error locking device (%s): %s", d.Id(), err)
if d.HasChange("locked") {
var action func(string) (*packngo.Response, error)
if d.Get("locked").(bool) {
action = client.Devices.Lock
} else {
action = client.Devices.Unlock
}
} else if d.HasChange("locked") {
_, err := client.Devices.Unlock(d.Id())

if err != nil {
return fmt.Errorf(
"Error unlocking device (%s): %s", d.Id(), err)
if _, err := action(d.Id()); err != nil {
return friendlyError(err)
}
}

Expand All @@ -246,22 +251,14 @@ func resourcePacketDeviceUpdate(d *schema.ResourceData, meta interface{}) error
func resourcePacketDeviceDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client)

log.Printf("[INFO] Deleting device: %s", d.Id())
if _, err := client.Devices.Delete(d.Id()); err != nil {
return fmt.Errorf("Error deleting device: %s", err)
return friendlyError(err)
}

return nil
}

func WaitForDeviceAttribute(
d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) {
// Wait for the device so we can get the networking attributes
// that show up after a while
log.Printf(
"[INFO] Waiting for device (%s) to have %s of %s",
d.Id(), attribute, target)

func waitForDeviceAttribute(d *schema.ResourceData, target string, pending []string, attribute string, meta interface{}) (interface{}, error) {
stateConf := &resource.StateChangeConf{
Pending: pending,
Target: target,
Expand All @@ -270,47 +267,37 @@ func WaitForDeviceAttribute(
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}

return stateConf.WaitForState()
}

func newDeviceStateRefreshFunc(
d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc {
func newDeviceStateRefreshFunc(d *schema.ResourceData, attribute string, meta interface{}) resource.StateRefreshFunc {
client := meta.(*packngo.Client)

return func() (interface{}, string, error) {
err := resourcePacketDeviceRead(d, meta)
if err != nil {
if err := resourcePacketDeviceRead(d, meta); err != nil {
return nil, "", err
}

// See if we can access our attribute
if attr, ok := d.GetOk(attribute); ok {
// Retrieve the device properties
device, _, err := client.Devices.Get(d.Id())
if err != nil {
return nil, "", fmt.Errorf("Error retrieving device: %s", err)
return nil, "", friendlyError(err)
}

return &device, attr.(string), nil
}

return nil, "", nil
}
}

// Powers on the device and waits for it to be active
// powerOnAndWait Powers on the device and waits for it to be active.
func powerOnAndWait(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client)
_, err := client.Devices.PowerOn(d.Id())
if err != nil {
return err
}

// Wait for power on
_, err = WaitForDeviceAttribute(d, "active", []string{"off"}, "state", client)
if err != nil {
return err
return friendlyError(err)
}

return nil
_, err = waitForDeviceAttribute(d, "active", []string{"off"}, "state", client)
return err
}
24 changes: 9 additions & 15 deletions builtin/providers/packet/resource_packet_project.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package packet

import (
"fmt"
"log"
"strings"

"github.com/hashicorp/terraform/helper/schema"
"github.com/packethost/packngo"
)
Expand Down Expand Up @@ -53,14 +49,12 @@ func resourcePacketProjectCreate(d *schema.ResourceData, meta interface{}) error
PaymentMethod: d.Get("payment_method").(string),
}

log.Printf("[DEBUG] Project create configuration: %#v", createRequest)
project, _, err := client.Projects.Create(createRequest)
if err != nil {
return fmt.Errorf("Error creating Project: %s", err)
return friendlyError(err)
}

d.SetId(project.ID)
log.Printf("[INFO] Project created: %s", project.ID)

return resourcePacketProjectRead(d, meta)
}
Expand All @@ -70,14 +64,16 @@ func resourcePacketProjectRead(d *schema.ResourceData, meta interface{}) error {

key, _, err := client.Projects.Get(d.Id())
if err != nil {
// If the project somehow already destroyed, mark as
// succesfully gone
if strings.Contains(err.Error(), "404") {
err = friendlyError(err)

// If the project somehow already destroyed, mark as succesfully gone.
if isNotFound(err) {
d.SetId("")

return nil
}

return fmt.Errorf("Error retrieving Project: %s", err)
return err
}

d.Set("id", key.ID)
Expand All @@ -100,10 +96,9 @@ func resourcePacketProjectUpdate(d *schema.ResourceData, meta interface{}) error
updateRequest.PaymentMethod = attr.(string)
}

log.Printf("[DEBUG] Project update: %#v", d.Get("id"))
_, _, err := client.Projects.Update(updateRequest)
if err != nil {
return fmt.Errorf("Failed to update Project: %s", err)
return friendlyError(err)
}

return resourcePacketProjectRead(d, meta)
Expand All @@ -112,10 +107,9 @@ func resourcePacketProjectUpdate(d *schema.ResourceData, meta interface{}) error
func resourcePacketProjectDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*packngo.Client)

log.Printf("[INFO] Deleting Project: %s", d.Id())
_, err := client.Projects.Delete(d.Id())
if err != nil {
return fmt.Errorf("Error deleting SSH key: %s", err)
return friendlyError(err)
}

d.SetId("")
Expand Down
Loading

0 comments on commit 6bc93ba

Please sign in to comment.