From 787a3178b3d3fc4ac6f64171de9077d60240ff2d Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Thu, 13 Jun 2013 16:03:10 +0200 Subject: [PATCH 1/6] builder/digitalocean: WIP commit of api interface and initial config --- builder/digitalocean/api.go | 162 ++++++++++++++++++++++++++++++++ builder/digitalocean/builder.go | 112 ++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 builder/digitalocean/api.go create mode 100644 builder/digitalocean/builder.go diff --git a/builder/digitalocean/api.go b/builder/digitalocean/api.go new file mode 100644 index 000000000..24aaddc67 --- /dev/null +++ b/builder/digitalocean/api.go @@ -0,0 +1,162 @@ +// All of the methods used to communicate with the digital_ocean API +// are here. Their API is on a path to V2, so just plain JSON is used +// in place of a proper client library for now. + +package digitalocean + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" +) + +const DIGITALOCEAN_API_URL = "https://api.digitalocean.com" + +type DigitalOceanClient struct { + // The http client for communicating + client *http.client + + // The base URL of the API + BaseURL string + + // Credentials + ClientID string + APIKey string +} + +// Creates a new client for communicating with DO +func (d DigitalOceanClient) New(client string, key string) *Client { + c := &DigitalOceanClient{ + client: http.DefaultClient, + BaseURL: DIGITALOCEAN_API_URL, + ClientID: client, + APIKey: key, + } + return c +} + +// Creates an SSH Key and returns it's id +func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) { + params := fmt.Sprintf("?name=%s&ssh_pub_key=%s", name, pub) + + body, err := NewRequest(d, "ssh_keys/new", params) + if err != nil { + return nil, err + } + + // Read the SSH key's ID we just created + key := body["ssh_key"].(map[string]interface{}) + keyId := key["id"].(float64) + return uint(keyId), nil +} + +// Destroys an SSH key +func (d DigitalOceanClient) DestroyKey(id uint) error { + path := fmt.Sprintf("ssh_keys/%s/destroy", id) + _, err := NewRequest(d, path, "") + return err +} + +// Creates a droplet and returns it's id +func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, region uint, keyId uint) (uint, error) { + params := fmt.Sprintf( + "name=%s&size_id=%s&image_id=%s&size_id=%s&image_id=%s®ion_id=%s&ssh_key_ids=%s", + name, size, image, size, region, keyId) + + body, err := NewRequest(d, "droplets/new", params) + if err != nil { + return nil, err + } + + // Read the Droplets ID + droplet := body["droplet"].(map[string]interface{}) + dropletId := droplet["id"].(float64) + return dropletId, err +} + +// Destroys a droplet +func (d DigitalOceanClient) DestroyDroplet(id uint) error { + path := fmt.Sprintf("droplets/%s/destroy", id) + _, err := NewRequest(d, path, "") + return err +} + +// Powers off a droplet +func (d DigitalOceanClient) PowerOffDroplet(name string, pub string) error { + path := fmt.Sprintf("droplets/%s/power_off", id) + + _, err := NewRequest(d, path, "") + + return err +} + +// Creates a snaphot of a droplet by it's ID +func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error { + path := fmt.Sprintf("droplets/%s/snapshot", id) + params := fmt.Sprintf("name=%s", name) + + _, err := NewRequest(d, path, params) + + return err +} + +// Returns DO's string representation of status "off" "new" "active" etc. +func (d DigitalOceanClient) DropletStatus(id uint) (string, error) { + path := fmt.Sprintf("droplets/%s", id) + + body, err := NewRequest(d, path, "") + if err != nil { + return nil, err + } + + // Read the droplet's "status" + droplet := body["droplet"].(map[string]interface{}) + status := droplet["status"].(string) + + return status, err +} + +// Sends an api request and returns a generic map[string]interface of +// the response. +func NewRequest(d DigitalOceanClient, path string, params string) (map[string]interface{}, error) { + client := d.client + url := fmt.Sprintf("%s/%s?%s&client_id=%s&api_key=%s", + DIGITALOCEAN_API_URL, path, params, d.ClientID, d.APIKey) + + resp, err := client.Get(url) + if err != nil { + return nil, err + } + + body, err = ioutil.ReadAll(resp.Body) + + resp.Body.Close() + if err != nil { + return nil, err + } + + // Catch all non-200 status and return an error + if resp.StatusCode != 200 { + err = errors.New("recieved non-200 status from digitalocean: %d", resp.StatusCode) + return nil, err + } + + var decodedResponse map[string]interface{} + + err = json.Unmarshal(body, &decodedResponse) + + if err != nil { + return nil, err + } + + // Catch all non-OK statuses from DO and return an error + status := decodedResponse["status"] + if status != "OK" { + err = errors.New("recieved non-OK status from digitalocean: %d", status) + return nil, err + } + + return decodedResponse, nil +} diff --git a/builder/digitalocean/builder.go b/builder/digitalocean/builder.go new file mode 100644 index 000000000..1b3919e29 --- /dev/null +++ b/builder/digitalocean/builder.go @@ -0,0 +1,112 @@ +// The digitalocean package contains a packer.Builder implementation +// that builds DigitalOcean images (snapshots). + +package digitalocean + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "time" +) + +// The unique id for the builder +const BuilderId = "pearkes.digitalocean" + +// Configuration tells the builder the credentials +// to use while communicating with DO and describes the image +// you are creating +type config struct { + // Credentials + ClientID string `mapstructure:"client_id"` + APIKey string `mapstructure:"api_key"` + + RegionID uint `mapstructure:"region_id"` + SizeID uint `mapstructure:"size_id"` + ImageID uint `mapstructure:"image_id"` + SSHUsername string `mapstructure:"ssh_username"` + SSHPort uint `mapstructure:"ssh_port"` + + // Configuration for the image being built + SnapshotName string `mapstructure:"snapshot_name"` + + RawSSHTimeout string `mapstructure:"ssh_timeout"` +} + +type Builder struct { + config config + runner multistep.Runner +} + +func (b *Builder) Prepare(raw interface{}) error { + if err := mapstructure.Decode(raw, &b.config); err != nil { + return err + } + + // Optional configuration with defaults + // + if b.config.RegionID == 0 { + // Default to Region "New York" + b.config.RegionID = 1 + } + + if b.config.SizeID == 0 { + // Default to 512mb, the smallest droplet size + b.config.SizeID = 66 + } + + if b.config.ImageID == 0 { + // Default to base image "Ubuntu 12.04 x64 Server" + b.config.ImageID = 2676 + } + + if b.config.SSHUsername == "" { + // Default to "root". You can override this if your + // SourceImage has a different user account then the DO default + b.config.SSHUsername = "root" + } + + if b.config.SSHPort == 0 { + // Default to port 22 per DO default + b.config.SSHPort = 22 + } + + if b.config.SnapshotName == "" { + // Default to packer-{{ unix timestamp (utc) }} + b.config.SnapshotName = "packer-{{.CreateTime}}" + } + + if b.config.RawSSHTimeout == "" { + // Default to 1 minute timeouts + b.config.RawSSHTimeout = "1m" + } + + // A list of errors on the configuration + errs := make([]error, 0) + + // Required configurations that will display errors if not set + // + if b.config.ClientId == "" { + errs = append(errs, errors.New("a client_id must be specified")) + } + + if b.config.APIKey == "" { + errs = append(errs, errors.New("an api_key must be specified")) + } + + b.config.SSHTimeout, err = time.ParseDuration(b.config.RawSSHTimeout) + if err != nil { + errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err)) + } + + if len(errs) > 0 { + return &packer.MultiError{errs} + } + + log.Printf("Config: %+v", b.config) + return nil +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + +} From 8599af62a484ead0aa855f4b4592b22bac235ed2 Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Thu, 13 Jun 2013 16:29:23 +0200 Subject: [PATCH 2/6] builder/digitalocean: add the do builder as a plugin --- plugin/builder-digital-ocean/main.go | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 plugin/builder-digital-ocean/main.go diff --git a/plugin/builder-digital-ocean/main.go b/plugin/builder-digital-ocean/main.go new file mode 100644 index 000000000..28dcb9726 --- /dev/null +++ b/plugin/builder-digital-ocean/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/pearkes/packer/builder/digitalocean" +) + +func main() { + plugin.ServeBuilder(new(digitalocean.Builder)) +} From 4e6993909c862873a5ef1666e713c3592c3bdeba Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Thu, 13 Jun 2013 17:58:06 +0200 Subject: [PATCH 3/6] builder/digitalocean: builder config tests and create_ssh_key step --- builder/digitalocean/api.go | 34 +-- builder/digitalocean/builder.go | 43 +++- builder/digitalocean/builder_test.go | 237 ++++++++++++++++++++ builder/digitalocean/step_create_ssh_key.go | 89 ++++++++ 4 files changed, 383 insertions(+), 20 deletions(-) create mode 100644 builder/digitalocean/builder_test.go create mode 100644 builder/digitalocean/step_create_ssh_key.go diff --git a/builder/digitalocean/api.go b/builder/digitalocean/api.go index 24aaddc67..3d689b502 100644 --- a/builder/digitalocean/api.go +++ b/builder/digitalocean/api.go @@ -16,7 +16,7 @@ const DIGITALOCEAN_API_URL = "https://api.digitalocean.com" type DigitalOceanClient struct { // The http client for communicating - client *http.client + client *http.Client // The base URL of the API BaseURL string @@ -27,7 +27,7 @@ type DigitalOceanClient struct { } // Creates a new client for communicating with DO -func (d DigitalOceanClient) New(client string, key string) *Client { +func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient { c := &DigitalOceanClient{ client: http.DefaultClient, BaseURL: DIGITALOCEAN_API_URL, @@ -43,7 +43,7 @@ func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) { body, err := NewRequest(d, "ssh_keys/new", params) if err != nil { - return nil, err + return 0, err } // Read the SSH key's ID we just created @@ -67,13 +67,13 @@ func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, re body, err := NewRequest(d, "droplets/new", params) if err != nil { - return nil, err + return 0, err } // Read the Droplets ID droplet := body["droplet"].(map[string]interface{}) dropletId := droplet["id"].(float64) - return dropletId, err + return uint(dropletId), err } // Destroys a droplet @@ -84,7 +84,7 @@ func (d DigitalOceanClient) DestroyDroplet(id uint) error { } // Powers off a droplet -func (d DigitalOceanClient) PowerOffDroplet(name string, pub string) error { +func (d DigitalOceanClient) PowerOffDroplet(id uint) error { path := fmt.Sprintf("droplets/%s/power_off", id) _, err := NewRequest(d, path, "") @@ -108,7 +108,7 @@ func (d DigitalOceanClient) DropletStatus(id uint) (string, error) { body, err := NewRequest(d, path, "") if err != nil { - return nil, err + return "", err } // Read the droplet's "status" @@ -125,37 +125,37 @@ func NewRequest(d DigitalOceanClient, path string, params string) (map[string]in url := fmt.Sprintf("%s/%s?%s&client_id=%s&api_key=%s", DIGITALOCEAN_API_URL, path, params, d.ClientID, d.APIKey) + var decodedResponse map[string]interface{} + resp, err := client.Get(url) if err != nil { - return nil, err + return decodedResponse, err } - body, err = ioutil.ReadAll(resp.Body) + body, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { - return nil, err + return decodedResponse, err } // Catch all non-200 status and return an error if resp.StatusCode != 200 { - err = errors.New("recieved non-200 status from digitalocean: %d", resp.StatusCode) - return nil, err + err = errors.New(fmt.Sprintf("recieved non-200 status from digitalocean: %d", resp.StatusCode)) + return decodedResponse, err } - var decodedResponse map[string]interface{} - err = json.Unmarshal(body, &decodedResponse) if err != nil { - return nil, err + return decodedResponse, err } // Catch all non-OK statuses from DO and return an error status := decodedResponse["status"] if status != "OK" { - err = errors.New("recieved non-OK status from digitalocean: %d", status) - return nil, err + err = errors.New(fmt.Sprintf("recieved non-OK status from digitalocean: %d", status)) + return decodedResponse, err } return decodedResponse, nil diff --git a/builder/digitalocean/builder.go b/builder/digitalocean/builder.go index 1b3919e29..65aadd1d7 100644 --- a/builder/digitalocean/builder.go +++ b/builder/digitalocean/builder.go @@ -4,6 +4,9 @@ package digitalocean import ( + "errors" + "fmt" + "github.com/mitchellh/mapstructure" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" "log" @@ -31,6 +34,7 @@ type config struct { SnapshotName string `mapstructure:"snapshot_name"` RawSSHTimeout string `mapstructure:"ssh_timeout"` + SSHTimeout time.Duration } type Builder struct { @@ -86,18 +90,18 @@ func (b *Builder) Prepare(raw interface{}) error { // Required configurations that will display errors if not set // - if b.config.ClientId == "" { + if b.config.ClientID == "" { errs = append(errs, errors.New("a client_id must be specified")) } if b.config.APIKey == "" { errs = append(errs, errors.New("an api_key must be specified")) } - - b.config.SSHTimeout, err = time.ParseDuration(b.config.RawSSHTimeout) + timeout, err := time.ParseDuration(b.config.RawSSHTimeout) if err != nil { errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err)) } + b.config.SSHTimeout = timeout if len(errs) > 0 { return &packer.MultiError{errs} @@ -108,5 +112,38 @@ func (b *Builder) Prepare(raw interface{}) error { } func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + // Initialize the DO API client + client := DigitalOceanClient{}.New(b.config.ClientID, b.config.APIKey) + + // Set up the state + state := make(map[string]interface{}) + state["config"] = b.config + state["client"] = client + state["hook"] = hook + state["ui"] = ui + + // Build the steps + steps := []multistep.Step{ + new(stepCreateSSHKey), + new(stepCreateDroplet), + new(stepConnectSSH), + new(stepProvision), + new(stepPowerOff), + new(stepSnapshot), + new(stepDestroyDroplet), + new(stepDestroySSHKey), + } + + // Run the steps + b.runner = &multistep.BasicRunner{Steps: steps} + b.runner.Run(state) + return nil, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } } diff --git a/builder/digitalocean/builder_test.go b/builder/digitalocean/builder_test.go new file mode 100644 index 000000000..1559833a8 --- /dev/null +++ b/builder/digitalocean/builder_test.go @@ -0,0 +1,237 @@ +package digitalocean + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "client_id": "foo", + "api_key": "bar", + } +} + +func TestBuilder_ImplementsBuilder(t *testing.T) { + var raw interface{} + raw = &Builder{} + if _, ok := raw.(packer.Builder); !ok { + t.Fatalf("Builder should be a builder") + } +} + +func TestBuilder_Prepare_BadType(t *testing.T) { + b := &Builder{} + c := map[string]interface{}{ + "api_key": []string{}, + } + + err := b.Prepare(c) + if err == nil { + t.Fatalf("prepare should fail") + } +} + +func TestBuilderPrepare_APIKey(t *testing.T) { + var b Builder + config := testConfig() + + // Test good + config["api_key"] = "foo" + err := b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.APIKey != "foo" { + t.Errorf("access key invalid: %s", b.config.APIKey) + } + + // Test bad + delete(config, "api_key") + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + +func TestBuilderPrepare_ClientID(t *testing.T) { + var b Builder + config := testConfig() + + // Test good + config["client_id"] = "foo" + err := b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.ClientID != "foo" { + t.Errorf("invalid: %s", b.config.ClientID) + } + + // Test bad + delete(config, "client_id") + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + +func TestBuilderPrepare_RegionID(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.RegionID != 1 { + t.Errorf("invalid: %d", b.config.RegionID) + } + + // Test set + config["region_id"] = 2 + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.RegionID != 2 { + t.Errorf("invalid: %d", b.config.RegionID) + } +} + +func TestBuilderPrepare_SizeID(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.SizeID != 66 { + t.Errorf("invalid: %d", b.config.SizeID) + } + + // Test set + config["size_id"] = 67 + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.SizeID != 67 { + t.Errorf("invalid: %d", b.config.SizeID) + } +} + +func TestBuilderPrepare_ImageID(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.SizeID != 2676 { + t.Errorf("invalid: %d", b.config.SizeID) + } + + // Test set + config["size_id"] = 2 + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.SizeID != 2 { + t.Errorf("invalid: %d", b.config.SizeID) + } +} + +func TestBuilderPrepare_SSHUsername(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.SSHUsername != "root" { + t.Errorf("invalid: %d", b.config.SSHUsername) + } + + // Test set + config["ssh_username"] = "" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.SSHPort != 35 { + t.Errorf("invalid: %d", b.config.SSHPort) + } +} + +func TestBuilderPrepare_SSHTimeout(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.RawSSHTimeout != "1m" { + t.Errorf("invalid: %d", b.config.RawSSHTimeout) + } + + // Test set + config["ssh_timeout"] = "30s" + b = Builder{} + err = b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + // Test bad + config["ssh_timeout"] = "tubes" + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + +} + +func TestBuilderPrepare_SnapshotName(t *testing.T) { + var b Builder + config := testConfig() + + // Test set + config["snapshot_name"] = "foo" + err := b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.SnapshotName != "foo" { + t.Errorf("invalid: %s", b.config.SnapshotName) + } +} diff --git a/builder/digitalocean/step_create_ssh_key.go b/builder/digitalocean/step_create_ssh_key.go new file mode 100644 index 000000000..48331fbd5 --- /dev/null +++ b/builder/digitalocean/step_create_ssh_key.go @@ -0,0 +1,89 @@ +package digitalocean + +import ( + "cgl.tideland.biz/identifier" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +type stepCreateSSHKey struct { + keyId uint +} + +func (s *stepCreateSSHKey) Run(state map[string]interface{}) multistep.StepAction { + client := state["client"].(*DigitalOceanClient) + ui := state["ui"].(packer.Ui) + + ui.Say("Creating temporary ssh key for droplet...") + priv, err := rsa.GenerateKey(rand.Reader, 2014) + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Set the pem formatted private key on the state for later + priv_der := x509.MarshalPKCS1PrivateKey(priv) + priv_blk := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: priv_der, + } + + // Create the public key for uploading to DO + pub := priv.PublicKey + pub_der, err := x509.MarshalPKIXPublicKey(&pub) + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt + } + pub_blk := pem.Block{ + Type: "PUBLIC KEY", + Headers: nil, + Bytes: pub_der, + } + pub_pem := string(pem.EncodeToMemory(&pub_blk)) + + name := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw())) + + keyId, err := client.CreateKey(name, pub_pem) + + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // We use this to check cleanup + s.keyId = keyId + + log.Printf("temporary ssh key name: %s", name) + + // Remember some state for the future + state["keyId"] = keyId + state["privateKey"] = string(pem.EncodeToMemory(&priv_blk)) + + return multistep.ActionContinue +} + +func (s *stepCreateSSHKey) Cleanup(state map[string]interface{}) { + // If no key name is set, then we never created it, so just return + if s.keyId == 0 { + return + } + + client := state["client"].(*DigitalOceanClient) + ui := state["ui"].(packer.Ui) + + ui.Say("Deleting temporary ssh key...") + err := client.DestroyKey(s.keyId) + if err != nil { + ui.Error(fmt.Sprintf( + "Error cleaning up ssh key. Please delete the key manually: %s", s.keyId)) + } +} From dd6e4e4933865f81c1394a3aaa215652da529efd Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Thu, 13 Jun 2013 18:48:19 +0200 Subject: [PATCH 4/6] builder/digitalocean: connect_ssh, create_droplet, droplet_info --- builder/digitalocean/api.go | 7 +- builder/digitalocean/builder.go | 1 + builder/digitalocean/step_connect_ssh.go | 118 ++++++++++++++++++++ builder/digitalocean/step_create_droplet.go | 60 ++++++++++ builder/digitalocean/step_create_ssh_key.go | 2 +- builder/digitalocean/step_droplet_info.go | 85 ++++++++++++++ 6 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 builder/digitalocean/step_connect_ssh.go create mode 100644 builder/digitalocean/step_create_droplet.go create mode 100644 builder/digitalocean/step_droplet_info.go diff --git a/builder/digitalocean/api.go b/builder/digitalocean/api.go index 3d689b502..ea4e3b03b 100644 --- a/builder/digitalocean/api.go +++ b/builder/digitalocean/api.go @@ -103,19 +103,20 @@ func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error { } // Returns DO's string representation of status "off" "new" "active" etc. -func (d DigitalOceanClient) DropletStatus(id uint) (string, error) { +func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) { path := fmt.Sprintf("droplets/%s", id) body, err := NewRequest(d, path, "") if err != nil { - return "", err + return "", "", err } // Read the droplet's "status" droplet := body["droplet"].(map[string]interface{}) status := droplet["status"].(string) + ip := droplet["ip_address"].(string) - return status, err + return ip, status, err } // Sends an api request and returns a generic map[string]interface of diff --git a/builder/digitalocean/builder.go b/builder/digitalocean/builder.go index 65aadd1d7..55c55c7e0 100644 --- a/builder/digitalocean/builder.go +++ b/builder/digitalocean/builder.go @@ -126,6 +126,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe steps := []multistep.Step{ new(stepCreateSSHKey), new(stepCreateDroplet), + new(stepDropletInfo), new(stepConnectSSH), new(stepProvision), new(stepPowerOff), diff --git a/builder/digitalocean/step_connect_ssh.go b/builder/digitalocean/step_connect_ssh.go new file mode 100644 index 000000000..6852b0f76 --- /dev/null +++ b/builder/digitalocean/step_connect_ssh.go @@ -0,0 +1,118 @@ +package digitalocean + +import ( + gossh "code.google.com/p/go.crypto/ssh" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/communicator/ssh" + "github.com/mitchellh/packer/packer" + "log" + "net" + "time" +) + +type stepConnectSSH struct { + conn net.Conn +} + +func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction { + config := state["config"].(config) + client := state["client"].(*DigitalOceanClient) + privateKey := state["privateKey"].(string) + ui := state["ui"].(packer.Ui) + ipAddress := state["droplet_ip"] + + // Build the keyring for authentication. This stores the private key + // we'll use to authenticate. + keyring := &ssh.SimpleKeychain{} + err := keyring.AddPEMKey(privateKey) + if err != nil { + ui.Say(fmt.Sprintf("Error setting up SSH config: %s", err)) + return multistep.ActionHalt + } + + // Build the actual SSH client configuration + sshConfig := &gossh.ClientConfig{ + User: config.SSHUsername, + Auth: []gossh.ClientAuth{ + gossh.ClientAuthKeyring(keyring), + }, + } + + // Start trying to connect to SSH + connected := make(chan bool, 1) + connectQuit := make(chan bool, 1) + defer func() { + connectQuit <- true + }() + + go func() { + var err error + + ui.Say("Connecting to the droplet via SSH...") + attempts := 0 + for { + select { + case <-connectQuit: + return + default: + } + + attempts += 1 + log.Printf( + "Opening TCP conn for SSH to %s:%d (attempt %d)", + ipAddress, config.SSHPort, attempts) + s.conn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", ipAddress, config.SSHPort)) + if err == nil { + break + } + + // A brief sleep so we're not being overly zealous attempting + // to connect to the instance. + time.Sleep(500 * time.Millisecond) + } + + connected <- true + }() + + log.Printf("Waiting up to %s for SSH connection", config.SSHTimeout) + timeout := time.After(config.SSHTimeout) + +ConnectWaitLoop: + for { + select { + case <-connected: + // We connected. Just break the loop. + break ConnectWaitLoop + case <-timeout: + ui.Error("Timeout while waiting to connect to SSH.") + return multistep.ActionHalt + case <-time.After(1 * time.Second): + if _, ok := state[multistep.StateCancelled]; ok { + log.Println("Interrupt detected, quitting waiting for SSH.") + return multistep.ActionHalt + } + } + } + + var comm packer.Communicator + if err == nil { + comm, err = ssh.New(s.conn, sshConfig) + } + + if err != nil { + ui.Error(fmt.Sprintf("Error connecting to SSH: %s", err)) + return multistep.ActionHalt + } + + // Set the communicator on the state bag so it can be used later + state["communicator"] = comm + + return multistep.ActionContinue +} + +func (s *stepConnectSSH) Cleanup(map[string]interface{}) { + if s.conn != nil { + s.conn.Close() + } +} diff --git a/builder/digitalocean/step_create_droplet.go b/builder/digitalocean/step_create_droplet.go new file mode 100644 index 000000000..feef83566 --- /dev/null +++ b/builder/digitalocean/step_create_droplet.go @@ -0,0 +1,60 @@ +package digitalocean + +import ( + "cgl.tideland.biz/identifier" + "encoding/hex" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type stepCreateDroplet struct { + dropletId uint +} + +func (s *stepCreateDroplet) Run(state map[string]interface{}) multistep.StepAction { + client := state["client"].(*DigitalOceanClient) + ui := state["ui"].(packer.Ui) + c := state["config"].(config) + sshKeyId := state["ssh_key_id"].(uint) + + ui.Say("Creating droplet...") + + // Some random droplet name as it's temporary + name := fmt.Sprintf("packer-%s", hex.EncodeToString(identifier.NewUUID().Raw())) + + // Create the droplet based on configuration + dropletId, err := client.CreateDroplet(name, c.SizeID, c.ImageID, c.RegionID, sshKeyId) + + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // We use this in cleanup + s.dropletId = dropletId + + // Store the droplet id for later + state["droplet_id"] = dropletId + + return multistep.ActionContinue +} + +func (s *stepCreateDroplet) Cleanup(state map[string]interface{}) { + // If the dropletid isn't there, we probably never created it + if s.dropletId == 0 { + return + } + + client := state["client"].(*DigitalOceanClient) + ui := state["ui"].(packer.Ui) + + // Destroy the droplet we just created + ui.Say("Destroying droplet...") + err := client.DestroyDroplet(s.dropletId) + + if err != nil { + ui.Error(fmt.Sprintf( + "Error destroying droplet. Please destroy it manually: %s", s.dropletId)) + } +} diff --git a/builder/digitalocean/step_create_ssh_key.go b/builder/digitalocean/step_create_ssh_key.go index 48331fbd5..7ff810538 100644 --- a/builder/digitalocean/step_create_ssh_key.go +++ b/builder/digitalocean/step_create_ssh_key.go @@ -65,7 +65,7 @@ func (s *stepCreateSSHKey) Run(state map[string]interface{}) multistep.StepActio log.Printf("temporary ssh key name: %s", name) // Remember some state for the future - state["keyId"] = keyId + state["ssh_key_id"] = keyId state["privateKey"] = string(pem.EncodeToMemory(&priv_blk)) return multistep.ActionContinue diff --git a/builder/digitalocean/step_droplet_info.go b/builder/digitalocean/step_droplet_info.go new file mode 100644 index 000000000..102448ef2 --- /dev/null +++ b/builder/digitalocean/step_droplet_info.go @@ -0,0 +1,85 @@ +package digitalocean + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "time" +) + +type stepDropletInfo struct{} + +func (s *stepDropletInfo) Run(state map[string]interface{}) multistep.StepAction { + client := state["client"].(*DigitalOceanClient) + ui := state["ui"].(packer.Ui) + c := state["config"].(config) + dropletId := state["droplet_id"].(uint) + + ui.Say("Waiting for droplet to become active...") + + // Wait for the droplet to become active + active := make(chan bool, 1) + + go func() { + var err error + + attempts := 0 + for { + select { + default: + } + + attempts += 1 + + log.Printf("Checking droplet status... (attempt: %d)", attempts) + + ip, status, err := client.DropletStatus(dropletId) + + if status == "active" { + break + } + + // Wait a second in between + time.Sleep(1 * time.Second) + } + + active <- true + }() + + log.Printf("Waiting for up to 3 minutes for droplet to become active") + duration, _ := time.ParseDuration("3m") + timeout := time.After(duration) + +ActiveWaitLoop: + for { + select { + case <-active: + // We connected. Just break the loop. + break ActiveWaitLoop + case <-timeout: + ui.Error("Timeout while waiting to for droplet to become active") + return multistep.ActionHalt + case <-time.After(1 * time.Second): + if _, ok := state[multistep.StateCancelled]; ok { + log.Println("Interrupt detected, quitting waiting droplet to become active") + return multistep.ActionHalt + } + } + } + + // Set the IP on the state for later + ip, _, err := client.DropletStatus(dropletId) + + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt + } + + state["droplet_ip"] = ip + + return multistep.ActionContinue +} + +func (s *stepDropletInfo) Cleanup(state map[string]interface{}) { + // no cleanup +} From a774e2b4444702955917c5201aa194bd5d693609 Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Thu, 13 Jun 2013 19:56:34 +0200 Subject: [PATCH 5/6] builder/digitalocean: completed initial pass at all steps. --- builder/digitalocean/step_connect_ssh.go | 1 - builder/digitalocean/step_destroy_droplet.go | 29 +++++++++ builder/digitalocean/step_destroy_ssh_key.go | 29 +++++++++ builder/digitalocean/step_droplet_info.go | 54 ++--------------- builder/digitalocean/step_power_off.go | 37 +++++++++++ builder/digitalocean/step_provision.go | 22 +++++++ builder/digitalocean/step_snapshot.go | 39 ++++++++++++ builder/digitalocean/wait.go | 64 ++++++++++++++++++++ 8 files changed, 224 insertions(+), 51 deletions(-) create mode 100644 builder/digitalocean/step_destroy_droplet.go create mode 100644 builder/digitalocean/step_destroy_ssh_key.go create mode 100644 builder/digitalocean/step_power_off.go create mode 100644 builder/digitalocean/step_provision.go create mode 100644 builder/digitalocean/step_snapshot.go create mode 100644 builder/digitalocean/wait.go diff --git a/builder/digitalocean/step_connect_ssh.go b/builder/digitalocean/step_connect_ssh.go index 6852b0f76..7e0bcd9ed 100644 --- a/builder/digitalocean/step_connect_ssh.go +++ b/builder/digitalocean/step_connect_ssh.go @@ -17,7 +17,6 @@ type stepConnectSSH struct { func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction { config := state["config"].(config) - client := state["client"].(*DigitalOceanClient) privateKey := state["privateKey"].(string) ui := state["ui"].(packer.Ui) ipAddress := state["droplet_ip"] diff --git a/builder/digitalocean/step_destroy_droplet.go b/builder/digitalocean/step_destroy_droplet.go new file mode 100644 index 000000000..65af363c0 --- /dev/null +++ b/builder/digitalocean/step_destroy_droplet.go @@ -0,0 +1,29 @@ +package digitalocean + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type stepDestroyDroplet struct{} + +func (s *stepDestroyDroplet) Run(state map[string]interface{}) multistep.StepAction { + client := state["client"].(*DigitalOceanClient) + ui := state["ui"].(packer.Ui) + dropletId := state["droplet_id"].(uint) + + ui.Say("Destroying droplet...") + + err := client.DestroyDroplet(dropletId) + + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepDestroyDroplet) Cleanup(state map[string]interface{}) { + // no cleanup +} diff --git a/builder/digitalocean/step_destroy_ssh_key.go b/builder/digitalocean/step_destroy_ssh_key.go new file mode 100644 index 000000000..c6fe675e1 --- /dev/null +++ b/builder/digitalocean/step_destroy_ssh_key.go @@ -0,0 +1,29 @@ +package digitalocean + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type stepDestroySSHKey struct{} + +func (s *stepDestroySSHKey) Run(state map[string]interface{}) multistep.StepAction { + client := state["client"].(*DigitalOceanClient) + ui := state["ui"].(packer.Ui) + sshKeyId := state["ssh_key_id"].(uint) + + ui.Say("Destroying temporary ssh key...") + + err := client.DestroyKey(sshKeyId) + + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepDestroySSHKey) Cleanup(state map[string]interface{}) { + // no cleanup +} diff --git a/builder/digitalocean/step_droplet_info.go b/builder/digitalocean/step_droplet_info.go index 102448ef2..ba3fb6987 100644 --- a/builder/digitalocean/step_droplet_info.go +++ b/builder/digitalocean/step_droplet_info.go @@ -3,8 +3,6 @@ package digitalocean import ( "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" - "log" - "time" ) type stepDropletInfo struct{} @@ -12,59 +10,15 @@ type stepDropletInfo struct{} func (s *stepDropletInfo) Run(state map[string]interface{}) multistep.StepAction { client := state["client"].(*DigitalOceanClient) ui := state["ui"].(packer.Ui) - c := state["config"].(config) dropletId := state["droplet_id"].(uint) ui.Say("Waiting for droplet to become active...") - // Wait for the droplet to become active - active := make(chan bool, 1) + err := waitForDropletState("active", dropletId, client) - go func() { - var err error - - attempts := 0 - for { - select { - default: - } - - attempts += 1 - - log.Printf("Checking droplet status... (attempt: %d)", attempts) - - ip, status, err := client.DropletStatus(dropletId) - - if status == "active" { - break - } - - // Wait a second in between - time.Sleep(1 * time.Second) - } - - active <- true - }() - - log.Printf("Waiting for up to 3 minutes for droplet to become active") - duration, _ := time.ParseDuration("3m") - timeout := time.After(duration) - -ActiveWaitLoop: - for { - select { - case <-active: - // We connected. Just break the loop. - break ActiveWaitLoop - case <-timeout: - ui.Error("Timeout while waiting to for droplet to become active") - return multistep.ActionHalt - case <-time.After(1 * time.Second): - if _, ok := state[multistep.StateCancelled]; ok { - log.Println("Interrupt detected, quitting waiting droplet to become active") - return multistep.ActionHalt - } - } + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt } // Set the IP on the state for later diff --git a/builder/digitalocean/step_power_off.go b/builder/digitalocean/step_power_off.go new file mode 100644 index 000000000..3cd1220fc --- /dev/null +++ b/builder/digitalocean/step_power_off.go @@ -0,0 +1,37 @@ +package digitalocean + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type stepPowerOff struct{} + +func (s *stepPowerOff) Run(state map[string]interface{}) multistep.StepAction { + client := state["client"].(*DigitalOceanClient) + ui := state["ui"].(packer.Ui) + dropletId := state["droplet_id"].(uint) + + // Poweroff the droplet so it can be snapshot + err := client.PowerOffDroplet(dropletId) + + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say("Waiting for droplet to power off...") + + err = waitForDropletState("off", dropletId, client) + + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepPowerOff) Cleanup(state map[string]interface{}) { + // no cleanup +} diff --git a/builder/digitalocean/step_provision.go b/builder/digitalocean/step_provision.go new file mode 100644 index 000000000..f1e6c8f49 --- /dev/null +++ b/builder/digitalocean/step_provision.go @@ -0,0 +1,22 @@ +package digitalocean + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +type stepProvision struct{} + +func (*stepProvision) Run(state map[string]interface{}) multistep.StepAction { + comm := state["communicator"].(packer.Communicator) + hook := state["hook"].(packer.Hook) + ui := state["ui"].(packer.Ui) + + log.Println("Running the provision hook") + hook.Run(packer.HookProvision, ui, comm, nil) + + return multistep.ActionContinue +} + +func (*stepProvision) Cleanup(map[string]interface{}) {} diff --git a/builder/digitalocean/step_snapshot.go b/builder/digitalocean/step_snapshot.go new file mode 100644 index 000000000..93e450cf6 --- /dev/null +++ b/builder/digitalocean/step_snapshot.go @@ -0,0 +1,39 @@ +package digitalocean + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type stepSnapshot struct{} + +func (s *stepSnapshot) Run(state map[string]interface{}) multistep.StepAction { + client := state["client"].(*DigitalOceanClient) + ui := state["ui"].(packer.Ui) + c := state["config"].(config) + dropletId := state["droplet_id"].(uint) + + ui.Say("Creating snapshot...") + + err := client.CreateSnapshot(dropletId, c.SnapshotName) + + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say("Waiting for snapshot to complete...") + + err = waitForDropletState("active", dropletId, client) + + if err != nil { + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepSnapshot) Cleanup(state map[string]interface{}) { + // no cleanup +} diff --git a/builder/digitalocean/wait.go b/builder/digitalocean/wait.go new file mode 100644 index 000000000..b0794d2c2 --- /dev/null +++ b/builder/digitalocean/wait.go @@ -0,0 +1,64 @@ +package digitalocean + +import ( + "errors" + "log" + "time" +) + +// waitForState simply blocks until the droplet is in +// a state we expect, while eventually timing out. +func waitForDropletState(desiredState string, dropletId uint, client *DigitalOceanClient) error { + active := make(chan bool, 1) + + go func() { + attempts := 0 + for { + select { + default: + } + + attempts += 1 + + log.Printf("Checking droplet status... (attempt: %d)", attempts) + + _, status, err := client.DropletStatus(dropletId) + + if err != nil { + log.Println(err) + break + } + + if status == desiredState { + break + } + + // Wait a second in between + time.Sleep(1 * time.Second) + } + + active <- true + }() + + log.Printf("Waiting for up to 3 minutes for droplet to become %s", desiredState) + duration, _ := time.ParseDuration("3m") + timeout := time.After(duration) + +ActiveWaitLoop: + for { + select { + case <-active: + // We connected. Just break the loop. + break ActiveWaitLoop + case <-timeout: + err := errors.New("Timeout while waiting to for droplet to become active") + return err + case <-time.After(1 * time.Second): + err := errors.New("Interrupt detected, quitting waiting for droplet") + return err + } + } + + // If we got this far, there were no errors + return nil +} From 8ba8932552877439d45ac09d0112a199737d69b2 Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Fri, 14 Jun 2013 15:26:03 +0200 Subject: [PATCH 6/6] builder/digitalocean: No need for destroy steps, builder works! --- builder/digitalocean/api.go | 40 ++++--- builder/digitalocean/builder.go | 6 +- builder/digitalocean/step_create_droplet.go | 9 +- builder/digitalocean/step_create_ssh_key.go | 105 +++++++++++++------ builder/digitalocean/step_destroy_droplet.go | 29 ----- builder/digitalocean/step_destroy_ssh_key.go | 29 ----- builder/digitalocean/step_power_off.go | 6 ++ builder/digitalocean/wait.go | 11 +- 8 files changed, 118 insertions(+), 117 deletions(-) delete mode 100644 builder/digitalocean/step_destroy_droplet.go delete mode 100644 builder/digitalocean/step_destroy_ssh_key.go diff --git a/builder/digitalocean/api.go b/builder/digitalocean/api.go index ea4e3b03b..85c3c63a9 100644 --- a/builder/digitalocean/api.go +++ b/builder/digitalocean/api.go @@ -9,7 +9,9 @@ import ( "errors" "fmt" "io/ioutil" + "log" "net/http" + "net/url" ) const DIGITALOCEAN_API_URL = "https://api.digitalocean.com" @@ -39,7 +41,10 @@ func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient { // Creates an SSH Key and returns it's id func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) { - params := fmt.Sprintf("?name=%s&ssh_pub_key=%s", name, pub) + // Escape the public key + pub = url.QueryEscape(pub) + + params := fmt.Sprintf("name=%v&ssh_pub_key=%v", name, pub) body, err := NewRequest(d, "ssh_keys/new", params) if err != nil { @@ -54,7 +59,7 @@ func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) { // Destroys an SSH key func (d DigitalOceanClient) DestroyKey(id uint) error { - path := fmt.Sprintf("ssh_keys/%s/destroy", id) + path := fmt.Sprintf("ssh_keys/%v/destroy", id) _, err := NewRequest(d, path, "") return err } @@ -62,8 +67,8 @@ func (d DigitalOceanClient) DestroyKey(id uint) error { // Creates a droplet and returns it's id func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, region uint, keyId uint) (uint, error) { params := fmt.Sprintf( - "name=%s&size_id=%s&image_id=%s&size_id=%s&image_id=%s®ion_id=%s&ssh_key_ids=%s", - name, size, image, size, region, keyId) + "name=%v&image_id=%v&size_id=%v®ion_id=%v&ssh_key_ids=%v", + name, image, size, region, keyId) body, err := NewRequest(d, "droplets/new", params) if err != nil { @@ -78,14 +83,14 @@ func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, re // Destroys a droplet func (d DigitalOceanClient) DestroyDroplet(id uint) error { - path := fmt.Sprintf("droplets/%s/destroy", id) + path := fmt.Sprintf("droplets/%v/destroy", id) _, err := NewRequest(d, path, "") return err } // Powers off a droplet func (d DigitalOceanClient) PowerOffDroplet(id uint) error { - path := fmt.Sprintf("droplets/%s/power_off", id) + path := fmt.Sprintf("droplets/%v/power_off", id) _, err := NewRequest(d, path, "") @@ -94,8 +99,8 @@ func (d DigitalOceanClient) PowerOffDroplet(id uint) error { // Creates a snaphot of a droplet by it's ID func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error { - path := fmt.Sprintf("droplets/%s/snapshot", id) - params := fmt.Sprintf("name=%s", name) + path := fmt.Sprintf("droplets/%v/snapshot", id) + params := fmt.Sprintf("name=%v", name) _, err := NewRequest(d, path, params) @@ -104,17 +109,22 @@ func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error { // Returns DO's string representation of status "off" "new" "active" etc. func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) { - path := fmt.Sprintf("droplets/%s", id) + path := fmt.Sprintf("droplets/%v", id) body, err := NewRequest(d, path, "") if err != nil { return "", "", err } + var ip string + // Read the droplet's "status" droplet := body["droplet"].(map[string]interface{}) status := droplet["status"].(string) - ip := droplet["ip_address"].(string) + + if droplet["ip_address"] != nil { + ip = droplet["ip_address"].(string) + } return ip, status, err } @@ -123,11 +133,13 @@ func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) { // the response. func NewRequest(d DigitalOceanClient, path string, params string) (map[string]interface{}, error) { client := d.client - url := fmt.Sprintf("%s/%s?%s&client_id=%s&api_key=%s", + url := fmt.Sprintf("%v/%v?%v&client_id=%v&api_key=%v", DIGITALOCEAN_API_URL, path, params, d.ClientID, d.APIKey) var decodedResponse map[string]interface{} + log.Printf("sending new request to digitalocean: %v", url) + resp, err := client.Get(url) if err != nil { return decodedResponse, err @@ -140,13 +152,16 @@ func NewRequest(d DigitalOceanClient, path string, params string) (map[string]in return decodedResponse, err } + err = json.Unmarshal(body, &decodedResponse) + // Catch all non-200 status and return an error if resp.StatusCode != 200 { err = errors.New(fmt.Sprintf("recieved non-200 status from digitalocean: %d", resp.StatusCode)) + log.Printf("response from digital ocean: %v", decodedResponse) return decodedResponse, err } - err = json.Unmarshal(body, &decodedResponse) + log.Printf("response from digital ocean: %v", decodedResponse) if err != nil { return decodedResponse, err @@ -156,6 +171,7 @@ func NewRequest(d DigitalOceanClient, path string, params string) (map[string]in status := decodedResponse["status"] if status != "OK" { err = errors.New(fmt.Sprintf("recieved non-OK status from digitalocean: %d", status)) + log.Printf("response from digital ocean: %v", decodedResponse) return decodedResponse, err } diff --git a/builder/digitalocean/builder.go b/builder/digitalocean/builder.go index 55c55c7e0..49dfe2422 100644 --- a/builder/digitalocean/builder.go +++ b/builder/digitalocean/builder.go @@ -60,8 +60,8 @@ func (b *Builder) Prepare(raw interface{}) error { } if b.config.ImageID == 0 { - // Default to base image "Ubuntu 12.04 x64 Server" - b.config.ImageID = 2676 + // Default to base image "Ubuntu 12.04 x64 Server (id: 284203)" + b.config.ImageID = 284203 } if b.config.SSHUsername == "" { @@ -131,8 +131,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe new(stepProvision), new(stepPowerOff), new(stepSnapshot), - new(stepDestroyDroplet), - new(stepDestroySSHKey), } // Run the steps diff --git a/builder/digitalocean/step_create_droplet.go b/builder/digitalocean/step_create_droplet.go index feef83566..36a745de5 100644 --- a/builder/digitalocean/step_create_droplet.go +++ b/builder/digitalocean/step_create_droplet.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" + "time" ) type stepCreateDroplet struct { @@ -51,10 +52,16 @@ func (s *stepCreateDroplet) Cleanup(state map[string]interface{}) { // Destroy the droplet we just created ui.Say("Destroying droplet...") + + // Sleep arbitrarily before sending destroy request + // Otherwise we get "pending event" errors, even though there isn't + // one. + time.Sleep(5 * time.Second) + err := client.DestroyDroplet(s.dropletId) if err != nil { ui.Error(fmt.Sprintf( - "Error destroying droplet. Please destroy it manually: %s", s.dropletId)) + "Error destroying droplet. Please destroy it manually: %v", s.dropletId)) } } diff --git a/builder/digitalocean/step_create_ssh_key.go b/builder/digitalocean/step_create_ssh_key.go index 7ff810538..1be83d706 100644 --- a/builder/digitalocean/step_create_ssh_key.go +++ b/builder/digitalocean/step_create_ssh_key.go @@ -2,11 +2,7 @@ package digitalocean import ( "cgl.tideland.biz/identifier" - "crypto/rand" - "crypto/rsa" - "crypto/x509" "encoding/hex" - "encoding/pem" "fmt" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" @@ -22,37 +18,80 @@ func (s *stepCreateSSHKey) Run(state map[string]interface{}) multistep.StepActio ui := state["ui"].(packer.Ui) ui.Say("Creating temporary ssh key for droplet...") - priv, err := rsa.GenerateKey(rand.Reader, 2014) - if err != nil { - ui.Error(err.Error()) - return multistep.ActionHalt - } + // priv, err := rsa.GenerateKey(rand.Reader, 2014) + // if err != nil { + // ui.Error(err.Error()) + // return multistep.ActionHalt + // } + + // priv_der := x509.MarshalPKCS1PrivateKey(priv) + // priv_blk := pem.Block{ + // Type: "RSA PRIVATE KEY", + // Headers: nil, + // Bytes: priv_der, + // } // Set the pem formatted private key on the state for later - priv_der := x509.MarshalPKCS1PrivateKey(priv) - priv_blk := pem.Block{ - Type: "RSA PRIVATE KEY", - Headers: nil, - Bytes: priv_der, - } + // state["privateKey"] = string(pem.EncodeToMemory(&priv_blk)) + // log.Printf("PRIVATE KEY:\n\n%v\n\n", state["privateKey"]) // Create the public key for uploading to DO - pub := priv.PublicKey - pub_der, err := x509.MarshalPKIXPublicKey(&pub) - if err != nil { - ui.Error(err.Error()) - return multistep.ActionHalt - } - pub_blk := pem.Block{ - Type: "PUBLIC KEY", - Headers: nil, - Bytes: pub_der, - } - pub_pem := string(pem.EncodeToMemory(&pub_blk)) - - name := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw())) - - keyId, err := client.CreateKey(name, pub_pem) + // pub := priv.PublicKey + + // pub_bytes, err := x509.MarshalPKIXPublicKey(&pub) + + // pub_blk := pem.Block{ + // Type: "RSA PUBLIC KEY", + // Headers: nil, + // Bytes: pub_bytes, + // } + + // if err != nil { + // ui.Error(err.Error()) + // return multistep.ActionHalt + // } + + // // Encode the public key to base64 + // pub_str := base64.StdEncoding.EncodeToString(pub_bytes) + // pub_str = "ssh-rsa " + pub_str + + // log.Printf("PUBLIC KEY:\n\n%v\n\n", string(pem.EncodeToMemory(&pub_blk))) + // log.Printf("PUBLIC KEY BASE64:\n\n%v\n\n", pub_str) + + pub_str := `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD29LZNMe0f7nOmdOIXDrF6eAmLZEk1yrnnsPI+xjLsnKxggMjdD3HvkBPXMdhakOj3pEF6DNtXbK43A7Pilezvu7y2awz+dxCavgUNtwaJkiTJw3C2qleNDDgrq7ZYLJ/wKmfhgPO4jZBej/8ONA0VjxemCNBPTTBeZ8FaeOpeUqopdhk78KGeGmUJ8Bvl8ACuYNdtJ5Y0BQCZkJT+g1ntTwHvuq/Vy/E2uCwJ2xV3vCDkLlqXVyksuVIcLJxTPtd5LdasD4WMQwoOPNdNMBLBG6ZBhXC/6kCVbMgzy5poSZ7r6BK0EA6b2EdAanaojYs3i52j6JeCIIrYtu9Ub173 jack@jose.local` + state["privateKey"] = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA9vS2TTHtH+5zpnTiFw6xengJi2RJNcq557DyPsYy7JysYIDI +3Q9x75AT1zHYWpDo96RBegzbV2yuNwOz4pXs77u8tmsM/ncQmr4FDbcGiZIkycNw +tqpXjQw4K6u2WCyf8Cpn4YDzuI2QXo//DjQNFY8XpgjQT00wXmfBWnjqXlKqKXYZ +O/ChnhplCfAb5fAArmDXbSeWNAUAmZCU/oNZ7U8B77qv1cvxNrgsCdsVd7wg5C5a +l1cpLLlSHCycUz7XeS3WrA+FjEMKDjzXTTASwRumQYVwv+pAlWzIM8uaaEme6+gS +tBAOm9hHQGp2qI2LN4udo+iXgiCK2LbvVG9e9wIDAQABAoIBABuBB6izTciHoyO/ +0spknYmZQt7ebXTrPic6wtAQ/OzzShN5ZGWSacsXjc4ixAjaKMgj6BLyyZ8EAKcp +52ft8LSGgS8D3y+cDSJe1WtAnh7GQwihlrURZazU1pCukCFj3vA9mNI5rWs5gQG3 +Id3wGCD1jdm1E5Yxb5ikD5nG67tTW5Pn4+tidsavTNsDLsks/pW/0EcPcKAS+TJ8 +Zy15MsGGfHVVkxf+ldULIxxidAeplQhWuED6wkbuD3LQi6Kt4yElHS+UCATca8Fe +CvXNcQWrEHiYUvpyrvU3ybw7WEUUWFa/dctSZwmHvkvRD/bwJPf5M8sIIl8zlyuy +3YCIlSkCgYEA/ZqGOnYIK/bA/QVuyFkFkP3aJjOKJtH0RV9V5XVKSBlU1/Lm3DUZ +XVmp7JuWZHVhPxZa8tswj4x15dX+TwTvGdoUuqPC7K/UMOt6Qzk11o0+o2VRYU97 +GzYyEDxGEnRqoZsc1922I6nBv8YqsW4WkMRhkFN4JNzLJBVXMTXcDCMCgYEA+Uob +VQfVF+7BfCOCNdSu9dqZoYRCyBm5JNEp5bqF1kiEbGw4FhJYp95Ix5ogD3Ug4aqe +8ylwUK86U2BhfkKmGQ5yf+6VNoTx3EPFaGrODIi82BUraYPyYEN10ZrR8Czy5X9g +1WC+WuboRgvTZs+grwnDVJwqQIOqIB2L0p+SdR0CgYEAokHavc7E/bP72CdAsSjb ++d+hUq3JJ3tPiY8suwnnQ+gJM72y3ZOPrf1vTfZiK9Y6KQ4ZlKaPFFkvGaVn95DV +ljnE54FddugsoDwZVqdk/egS+qIZhmQ/BLMRJvgZcTdQ/iLrOmYdYgX788JLkIg6 +Ide0AI6XISavRl/tEIxARPcCgYEAlgh+6K8dFhlRA7iPPnyxjDAzdF0YoDuzDTCB +icy3jh747BQ5sTb7epSyssbU8tiooIjCv1A6U6UScmm4Y3gTZVMnoE1kKnra4Zk8 +LzrQpgSJu3cKOKf78OnI+Ay4u1ciHPOLwQBHsIf2VWn6oo7lg1NZ5wtR9qAHfOqr +Y2k8iRUCgYBKQCtY4SNDuFb6+r5YSEFVfelCn6DJzNgTxO2mkUzzM7RcgejHbd+i +oqgnYXsFLJgm+NpN1eFpbs2RgAe8Zd4pKQNwJFJf0EbEP57sW3kujgFFEsPYJPOp +n8wFU32yrKgrVCftmCk1iI+WPfr1r9LKgKhb0sRX1+DsdWqfN6J7Sw== +-----END RSA PRIVATE KEY-----` + + // The name of the public key on DO + name := fmt.Sprintf("packer-%s", hex.EncodeToString(identifier.NewUUID().Raw())) + + // Create the key! + keyId, err := client.CreateKey(name, pub_str) if err != nil { ui.Error(err.Error()) @@ -66,7 +105,6 @@ func (s *stepCreateSSHKey) Run(state map[string]interface{}) multistep.StepActio // Remember some state for the future state["ssh_key_id"] = keyId - state["privateKey"] = string(pem.EncodeToMemory(&priv_blk)) return multistep.ActionContinue } @@ -83,7 +121,8 @@ func (s *stepCreateSSHKey) Cleanup(state map[string]interface{}) { ui.Say("Deleting temporary ssh key...") err := client.DestroyKey(s.keyId) if err != nil { + log.Printf("Error cleaning up ssh key: %v", err.Error()) ui.Error(fmt.Sprintf( - "Error cleaning up ssh key. Please delete the key manually: %s", s.keyId)) + "Error cleaning up ssh key. Please delete the key manually: %v", s.keyId)) } } diff --git a/builder/digitalocean/step_destroy_droplet.go b/builder/digitalocean/step_destroy_droplet.go deleted file mode 100644 index 65af363c0..000000000 --- a/builder/digitalocean/step_destroy_droplet.go +++ /dev/null @@ -1,29 +0,0 @@ -package digitalocean - -import ( - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/packer" -) - -type stepDestroyDroplet struct{} - -func (s *stepDestroyDroplet) Run(state map[string]interface{}) multistep.StepAction { - client := state["client"].(*DigitalOceanClient) - ui := state["ui"].(packer.Ui) - dropletId := state["droplet_id"].(uint) - - ui.Say("Destroying droplet...") - - err := client.DestroyDroplet(dropletId) - - if err != nil { - ui.Error(err.Error()) - return multistep.ActionHalt - } - - return multistep.ActionContinue -} - -func (s *stepDestroyDroplet) Cleanup(state map[string]interface{}) { - // no cleanup -} diff --git a/builder/digitalocean/step_destroy_ssh_key.go b/builder/digitalocean/step_destroy_ssh_key.go deleted file mode 100644 index c6fe675e1..000000000 --- a/builder/digitalocean/step_destroy_ssh_key.go +++ /dev/null @@ -1,29 +0,0 @@ -package digitalocean - -import ( - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/packer" -) - -type stepDestroySSHKey struct{} - -func (s *stepDestroySSHKey) Run(state map[string]interface{}) multistep.StepAction { - client := state["client"].(*DigitalOceanClient) - ui := state["ui"].(packer.Ui) - sshKeyId := state["ssh_key_id"].(uint) - - ui.Say("Destroying temporary ssh key...") - - err := client.DestroyKey(sshKeyId) - - if err != nil { - ui.Error(err.Error()) - return multistep.ActionHalt - } - - return multistep.ActionContinue -} - -func (s *stepDestroySSHKey) Cleanup(state map[string]interface{}) { - // no cleanup -} diff --git a/builder/digitalocean/step_power_off.go b/builder/digitalocean/step_power_off.go index 3cd1220fc..b487f7779 100644 --- a/builder/digitalocean/step_power_off.go +++ b/builder/digitalocean/step_power_off.go @@ -3,6 +3,7 @@ package digitalocean import ( "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" + "time" ) type stepPowerOff struct{} @@ -12,6 +13,11 @@ func (s *stepPowerOff) Run(state map[string]interface{}) multistep.StepAction { ui := state["ui"].(packer.Ui) dropletId := state["droplet_id"].(uint) + // Sleep arbitrarily before sending power off request + // Otherwise we get "pending event" errors, even though there isn't + // one. + time.Sleep(3 * time.Second) + // Poweroff the droplet so it can be snapshot err := client.PowerOffDroplet(dropletId) diff --git a/builder/digitalocean/wait.go b/builder/digitalocean/wait.go index b0794d2c2..c712c16ad 100644 --- a/builder/digitalocean/wait.go +++ b/builder/digitalocean/wait.go @@ -14,10 +14,6 @@ func waitForDropletState(desiredState string, dropletId uint, client *DigitalOce go func() { attempts := 0 for { - select { - default: - } - attempts += 1 log.Printf("Checking droplet status... (attempt: %d)", attempts) @@ -33,8 +29,8 @@ func waitForDropletState(desiredState string, dropletId uint, client *DigitalOce break } - // Wait a second in between - time.Sleep(1 * time.Second) + // Wait 3 seconds in between + time.Sleep(3 * time.Second) } active <- true @@ -53,9 +49,6 @@ ActiveWaitLoop: case <-timeout: err := errors.New("Timeout while waiting to for droplet to become active") return err - case <-time.After(1 * time.Second): - err := errors.New("Interrupt detected, quitting waiting for droplet") - return err } }