Commit 30287111 authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

builder/digitalocean: retry on any pending event errors

/cc @pearkes - I hate this thing.
parent e5350ce5
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
IMPROVEMENTS: IMPROVEMENTS:
* builder/digitalocean: Retry on any pending event errors.
* builder/openstack: Can now specify a project. [GH-382] * builder/openstack: Can now specify a project. [GH-382]
BUG FIXES: BUG FIXES:
......
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"time"
) )
const DIGITALOCEAN_API_URL = "https://api.digitalocean.com" const DIGITALOCEAN_API_URL = "https://api.digitalocean.com"
...@@ -191,30 +192,28 @@ func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[strin ...@@ -191,30 +192,28 @@ func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[strin
url := fmt.Sprintf("%s/%s?%s", DIGITALOCEAN_API_URL, path, params.Encode()) url := fmt.Sprintf("%s/%s?%s", DIGITALOCEAN_API_URL, path, params.Encode())
var decodedResponse map[string]interface{}
// Do some basic scrubbing so sensitive information doesn't appear in logs // Do some basic scrubbing so sensitive information doesn't appear in logs
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1) scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1) scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
log.Printf("sending new request to digitalocean: %s", scrubbedUrl) log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
var lastErr error
for attempts := 1; attempts < 5; attempts++ {
resp, err := client.Get(url) resp, err := client.Get(url)
if err != nil { if err != nil {
return decodedResponse, err return nil, err
} }
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
if err != nil { if err != nil {
return decodedResponse, err return nil, err
} }
log.Printf("response from digitalocean: %s", body) log.Printf("response from digitalocean: %s", body)
var decodedResponse map[string]interface{}
err = json.Unmarshal(body, &decodedResponse) err = json.Unmarshal(body, &decodedResponse)
// Check for bad JSON
if err != nil { if err != nil {
err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s", err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
resp.StatusCode, body)) resp.StatusCode, body))
...@@ -222,15 +221,28 @@ func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[strin ...@@ -222,15 +221,28 @@ func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[strin
} }
// Check for errors sent by digitalocean // Check for errors sent by digitalocean
status := decodedResponse["status"] status := decodedResponse["status"].(string)
if status != "OK" { if status == "OK" {
// Get the actual error message if there is one return decodedResponse, nil
}
if status == "ERROR" { if status == "ERROR" {
status = decodedResponse["error_message"] status = decodedResponse["error_message"].(string)
} }
err = errors.New(fmt.Sprintf("Received bad response (HTTP %v) from DigitalOcean: %s", resp.StatusCode, status))
return decodedResponse, err lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
resp.StatusCode, status))
log.Println(lastErr)
if strings.Contains(status, "has a pending event") {
// Retry, DigitalOcean sends these dumb "pending event"
// errors all the time.
time.Sleep(5 * time.Second)
continue
} }
return decodedResponse, nil // Some other kind of error. Just return.
return decodedResponse, lastErr
}
return nil, lastErr
} }
...@@ -34,13 +34,11 @@ type config struct { ...@@ -34,13 +34,11 @@ type config struct {
SSHPort uint `mapstructure:"ssh_port"` SSHPort uint `mapstructure:"ssh_port"`
RawSSHTimeout string `mapstructure:"ssh_timeout"` RawSSHTimeout string `mapstructure:"ssh_timeout"`
RawEventDelay string `mapstructure:"event_delay"`
RawStateTimeout string `mapstructure:"state_timeout"` RawStateTimeout string `mapstructure:"state_timeout"`
// These are unexported since they're set by other fields // These are unexported since they're set by other fields
// being set. // being set.
sshTimeout time.Duration sshTimeout time.Duration
eventDelay time.Duration
stateTimeout time.Duration stateTimeout time.Duration
tpl *packer.ConfigTemplate tpl *packer.ConfigTemplate
...@@ -113,12 +111,6 @@ func (b *Builder) Prepare(raws ...interface{}) error { ...@@ -113,12 +111,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
b.config.RawSSHTimeout = "1m" b.config.RawSSHTimeout = "1m"
} }
if b.config.RawEventDelay == "" {
// Default to 5 second delays after creating events
// to allow DO to process
b.config.RawEventDelay = "5s"
}
if b.config.RawStateTimeout == "" { if b.config.RawStateTimeout == "" {
// Default to 6 minute timeouts waiting for // Default to 6 minute timeouts waiting for
// desired state. i.e waiting for droplet to become active // desired state. i.e waiting for droplet to become active
...@@ -131,7 +123,6 @@ func (b *Builder) Prepare(raws ...interface{}) error { ...@@ -131,7 +123,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
"snapshot_name": &b.config.SnapshotName, "snapshot_name": &b.config.SnapshotName,
"ssh_username": &b.config.SSHUsername, "ssh_username": &b.config.SSHUsername,
"ssh_timeout": &b.config.RawSSHTimeout, "ssh_timeout": &b.config.RawSSHTimeout,
"event_delay": &b.config.RawEventDelay,
"state_timeout": &b.config.RawStateTimeout, "state_timeout": &b.config.RawStateTimeout,
} }
...@@ -162,13 +153,6 @@ func (b *Builder) Prepare(raws ...interface{}) error { ...@@ -162,13 +153,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
} }
b.config.sshTimeout = sshTimeout b.config.sshTimeout = sshTimeout
eventDelay, err := time.ParseDuration(b.config.RawEventDelay)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed parsing event_delay: %s", err))
}
b.config.eventDelay = eventDelay
stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout) stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout)
if err != nil { if err != nil {
errs = packer.MultiErrorAppend( errs = packer.MultiErrorAppend(
......
...@@ -258,38 +258,6 @@ func TestBuilderPrepare_SSHTimeout(t *testing.T) { ...@@ -258,38 +258,6 @@ func TestBuilderPrepare_SSHTimeout(t *testing.T) {
} }
func TestBuilderPrepare_EventDelay(t *testing.T) {
var b Builder
config := testConfig()
// Test default
err := b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.RawEventDelay != "5s" {
t.Errorf("invalid: %d", b.config.RawEventDelay)
}
// Test set
config["event_delay"] = "10s"
b = Builder{}
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["event_delay"] = "tubes"
b = Builder{}
err = b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_StateTimeout(t *testing.T) { func TestBuilderPrepare_StateTimeout(t *testing.T) {
var b Builder var b Builder
config := testConfig() config := testConfig()
......
...@@ -6,7 +6,6 @@ import ( ...@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log"
"time" "time"
) )
...@@ -56,12 +55,6 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) { ...@@ -56,12 +55,6 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
// Destroy the droplet we just created // Destroy the droplet we just created
ui.Say("Destroying droplet...") ui.Say("Destroying droplet...")
// Sleep arbitrarily before sending destroy request
// Otherwise we get "pending event" errors, even though there isn't
// one.
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
time.Sleep(c.eventDelay)
var err error var err error
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
err = client.DestroyDroplet(s.dropletId) err = client.DestroyDroplet(s.dropletId)
......
...@@ -5,26 +5,17 @@ import ( ...@@ -5,26 +5,17 @@ import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log" "log"
"time"
) )
type stepPowerOff struct{} type stepPowerOff struct{}
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction { func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*DigitalOceanClient) client := state.Get("client").(*DigitalOceanClient)
c := state.Get("config").(config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
dropletId := state.Get("droplet_id").(uint) dropletId := state.Get("droplet_id").(uint)
// Sleep arbitrarily before sending power off request
// Otherwise we get "pending event" errors, even though there isn't
// one.
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
time.Sleep(c.eventDelay)
// Poweroff the droplet so it can be snapshot // Poweroff the droplet so it can be snapshot
err := client.PowerOffDroplet(dropletId) err := client.PowerOffDroplet(dropletId)
if err != nil { if err != nil {
err := fmt.Errorf("Error powering off droplet: %s", err) err := fmt.Errorf("Error powering off droplet: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -33,14 +24,6 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction { ...@@ -33,14 +24,6 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
} }
log.Println("Waiting for poweroff event to complete...") log.Println("Waiting for poweroff event to complete...")
// This arbitrary sleep is because we can't wait for the state
// of the droplet to be 'off', as stepShutdown should already
// have accomplished that, and the state indicator is the same.
// We just have to assume that this event will process quickly.
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
time.Sleep(c.eventDelay)
return multistep.ActionContinue return multistep.ActionContinue
} }
......
...@@ -4,8 +4,6 @@ import ( ...@@ -4,8 +4,6 @@ import (
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"log"
"time"
) )
type stepShutdown struct{} type stepShutdown struct{}
...@@ -16,14 +14,7 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { ...@@ -16,14 +14,7 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
dropletId := state.Get("droplet_id").(uint) dropletId := state.Get("droplet_id").(uint)
// Sleep arbitrarily before sending the request
// Otherwise we get "pending event" errors, even though there isn't
// one.
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
time.Sleep(c.eventDelay)
err := client.ShutdownDroplet(dropletId) err := client.ShutdownDroplet(dropletId)
if err != nil { if err != nil {
err := fmt.Errorf("Error shutting down droplet: %s", err) err := fmt.Errorf("Error shutting down droplet: %s", err)
state.Put("error", err) state.Put("error", err)
...@@ -32,7 +23,6 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { ...@@ -32,7 +23,6 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
} }
ui.Say("Waiting for droplet to shutdown...") ui.Say("Waiting for droplet to shutdown...")
err = waitForDropletState("off", dropletId, client, c) err = waitForDropletState("off", dropletId, client, c)
if err != nil { if err != nil {
err := fmt.Errorf("Error waiting for droplet to become 'off': %s", err) err := fmt.Errorf("Error waiting for droplet to become 'off': %s", err)
......
package digitalocean package digitalocean
import ( import (
"errors" "fmt"
"log" "log"
"time" "time"
) )
...@@ -9,8 +9,7 @@ import ( ...@@ -9,8 +9,7 @@ import (
// waitForState simply blocks until the droplet is in // waitForState simply blocks until the droplet is in
// a state we expect, while eventually timing out. // a state we expect, while eventually timing out.
func waitForDropletState(desiredState string, dropletId uint, client *DigitalOceanClient, c config) error { func waitForDropletState(desiredState string, dropletId uint, client *DigitalOceanClient, c config) error {
active := make(chan bool, 1) result := make(chan error, 1)
go func() { go func() {
attempts := 0 attempts := 0
for { for {
...@@ -19,36 +18,26 @@ func waitForDropletState(desiredState string, dropletId uint, client *DigitalOce ...@@ -19,36 +18,26 @@ func waitForDropletState(desiredState string, dropletId uint, client *DigitalOce
log.Printf("Checking droplet status... (attempt: %d)", attempts) log.Printf("Checking droplet status... (attempt: %d)", attempts)
_, status, err := client.DropletStatus(dropletId) _, status, err := client.DropletStatus(dropletId)
if err != nil { if err != nil {
log.Println(err) result <- err
break return
} }
if status == desiredState { if status == desiredState {
break result <- nil
return
} }
// Wait 3 seconds in between // Wait 3 seconds in between
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
} }
active <- true
}() }()
log.Printf("Waiting for up to %s for droplet to become %s", c.RawStateTimeout, desiredState) log.Printf("Waiting for up to %s for droplet to become %s", c.RawStateTimeout, desiredState)
timeout := time.After(c.stateTimeout)
ActiveWaitLoop:
for {
select { select {
case <-active: case err := <-result:
// We connected. Just break the loop. return err
break ActiveWaitLoop case <-time.After(c.stateTimeout):
case <-timeout: err := fmt.Errorf("Timeout while waiting to for droplet to become '%s'", desiredState)
err := errors.New("Timeout while waiting to for droplet to become active")
return err return err
} }
}
// If we got this far, there were no errors
return nil
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment