diff --git a/builder/digitalocean/config.go b/builder/digitalocean/config.go index 057138633..dd1460583 100644 --- a/builder/digitalocean/config.go +++ b/builder/digitalocean/config.go @@ -64,8 +64,13 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { } if c.SnapshotName == "" { + def, err := interpolate.Render("packer-{{timestamp}}", nil) + if err != nil { + panic(err) + } + // Default to packer-{{ unix timestamp (utc) }} - c.SnapshotName = "packer-{{timestamp}}" + c.SnapshotName = def } if c.DropletName == "" { diff --git a/builder/digitalocean/step_power_off.go b/builder/digitalocean/step_power_off.go index 3d547e8c2..94891e227 100644 --- a/builder/digitalocean/step_power_off.go +++ b/builder/digitalocean/step_power_off.go @@ -3,6 +3,7 @@ package digitalocean import ( "fmt" "log" + "time" "github.com/digitalocean/godo" "github.com/mitchellh/multistep" @@ -48,6 +49,15 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionHalt } + // Wait for the droplet to become unlocked for future steps + if err := waitForDropletUnlocked(client, dropletId, 2*time.Minute); err != nil { + // If we get an error the first time, actually report it + err := fmt.Errorf("Error powering off droplet: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + return multistep.ActionContinue } diff --git a/builder/digitalocean/step_shutdown.go b/builder/digitalocean/step_shutdown.go index 602f3e690..da04aee33 100644 --- a/builder/digitalocean/step_shutdown.go +++ b/builder/digitalocean/step_shutdown.go @@ -65,7 +65,19 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { err = waitForDropletState("off", dropletId, client, 2*time.Minute) if err != nil { - log.Printf("Error waiting for graceful off: %s", err) + // If we get an error the first time, actually report it + err := fmt.Errorf("Error shutting down droplet: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if err := waitForDropletUnlocked(client, dropletId, 2*time.Minute); err != nil { + // If we get an error the first time, actually report it + err := fmt.Errorf("Error shutting down droplet: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt } return multistep.ActionContinue diff --git a/builder/digitalocean/step_snapshot.go b/builder/digitalocean/step_snapshot.go index cfda7af20..f6902b8c5 100644 --- a/builder/digitalocean/step_snapshot.go +++ b/builder/digitalocean/step_snapshot.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "time" "github.com/digitalocean/godo" "github.com/mitchellh/multistep" @@ -27,6 +28,18 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionHalt } + // Wait for the droplet to become unlocked first. For snapshots + // this can end up taking quite a long time, so we hardcode this to + // 10 minutes. + if err := waitForDropletUnlocked(client, dropletId, 10*time.Minute); err != nil { + // If we get an error the first time, actually report it + err := fmt.Errorf("Error shutting down droplet: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // With the pending state over, verify that we're in the active state ui.Say("Waiting for snapshot to complete...") err = waitForDropletState("active", dropletId, client, c.stateTimeout) if err != nil { diff --git a/builder/digitalocean/wait.go b/builder/digitalocean/wait.go index 3d299d433..a41bbb3ed 100644 --- a/builder/digitalocean/wait.go +++ b/builder/digitalocean/wait.go @@ -8,6 +8,55 @@ import ( "github.com/digitalocean/godo" ) +// waitForDropletUnlocked waits for the Droplet to be unlocked to +// avoid "pending" errors when making state changes. +func waitForDropletUnlocked( + client *godo.Client, dropletId int, timeout time.Duration) error { + done := make(chan struct{}) + defer close(done) + + result := make(chan error, 1) + go func() { + attempts := 0 + for { + attempts += 1 + + log.Printf("[DEBUG] Checking droplet lock state... (attempt: %d)", attempts) + droplet, _, err := client.Droplets.Get(dropletId) + if err != nil { + result <- err + return + } + + if !droplet.Locked { + result <- nil + return + } + + // Wait 3 seconds in between + time.Sleep(3 * time.Second) + + // Verify we shouldn't exit + select { + case <-done: + // We finished, so just exit the goroutine + return + default: + // Keep going + } + } + }() + + log.Printf("[DEBUG] Waiting for up to %d seconds for droplet to unlock", timeout/time.Second) + select { + case err := <-result: + return err + case <-time.After(timeout): + return fmt.Errorf( + "Timeout while waiting to for droplet to unlock") + } +} + // waitForState simply blocks until the droplet is in // a state we expect, while eventually timing out. func waitForDropletState(