From dd6e4e4933865f81c1394a3aaa215652da529efd Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Thu, 13 Jun 2013 18:48:19 +0200 Subject: [PATCH] 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 +}