builder/amazonebs: Refactor into multiple steps
This commit is contained in:
parent
559777e5b7
commit
3a97bae000
|
@ -6,19 +6,11 @@
|
|||
package amazonebs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"cgl.tideland.biz/identifier"
|
||||
gossh "code.google.com/p/go.crypto/ssh"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
|
@ -56,179 +48,21 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook) {
|
|||
region := aws.Regions[b.config.Region]
|
||||
ec2conn := ec2.New(auth, region)
|
||||
|
||||
// Create a new keypair that we'll use to access the instance.
|
||||
keyName := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw()))
|
||||
ui.Say("Creating temporary keypair for this instance...")
|
||||
log.Printf("temporary keypair name: %s", keyName)
|
||||
keyResp, err := ec2conn.CreateKeyPair(keyName)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := make(map[string]interface{})
|
||||
state["config"] = b.config
|
||||
state["ec2"] = ec2conn
|
||||
state["hook"] = hook
|
||||
state["ui"] = ui
|
||||
|
||||
// Build the steps
|
||||
steps := []Step{
|
||||
&stepKeyPair{},
|
||||
&stepRunSourceInstance{},
|
||||
&stepStopInstance{},
|
||||
&stepCreateAMI{},
|
||||
}
|
||||
|
||||
// Make sure the keypair is properly deleted when we exit
|
||||
defer func() {
|
||||
ui.Say("Deleting temporary keypair...")
|
||||
_, err := ec2conn.DeleteKeyPair(keyName)
|
||||
if err != nil {
|
||||
ui.Error(
|
||||
"Error cleaning up keypair. Please delete the key manually: %s", keyName)
|
||||
}
|
||||
}()
|
||||
|
||||
runOpts := &ec2.RunInstances{
|
||||
KeyName: keyName,
|
||||
ImageId: b.config.SourceAmi,
|
||||
InstanceType: b.config.InstanceType,
|
||||
MinCount: 0,
|
||||
MaxCount: 0,
|
||||
}
|
||||
|
||||
ui.Say("Launching a source AWS instance...")
|
||||
runResp, err := ec2conn.RunInstances(runOpts)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
instance := &runResp.Instances[0]
|
||||
log.Printf("instance id: %s", instance.InstanceId)
|
||||
|
||||
// Make sure we clean up the instance by terminating it, no matter what
|
||||
defer func() {
|
||||
// TODO: error handling
|
||||
ui.Say("Terminating the source AWS instance...")
|
||||
ec2conn.TerminateInstances([]string{instance.InstanceId})
|
||||
}()
|
||||
|
||||
ui.Say("Waiting for instance to become ready...")
|
||||
instance, err = waitForState(ec2conn, instance, "running")
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Build the SSH configuration
|
||||
keyring := &ssh.SimpleKeychain{}
|
||||
err = keyring.AddPEMKey(keyResp.KeyMaterial)
|
||||
if err != nil {
|
||||
ui.Say("Error setting up SSH config: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sshConfig := &gossh.ClientConfig{
|
||||
User: "ubuntu",
|
||||
Auth: []gossh.ClientAuth{
|
||||
gossh.ClientAuthKeyring(keyring),
|
||||
},
|
||||
}
|
||||
|
||||
// Try to connect for SSH a few times
|
||||
var conn net.Conn
|
||||
for i := 0; i < 5; i++ {
|
||||
time.Sleep(time.Duration(i) * time.Second)
|
||||
|
||||
log.Printf(
|
||||
"Opening TCP conn for SSH to %s:22 (attempt %d)",
|
||||
instance.DNSName, i+1)
|
||||
conn, err = net.Dial("tcp", fmt.Sprintf("%s:22", instance.DNSName))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer conn.Close()
|
||||
}
|
||||
|
||||
var comm packer.Communicator
|
||||
if err == nil {
|
||||
comm, err = ssh.New(conn, sshConfig)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ui.Error("Error connecting to SSH: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// XXX: TEST
|
||||
remote, err := comm.Start("echo foo")
|
||||
if err != nil {
|
||||
ui.Error("Error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
remote.Wait()
|
||||
|
||||
bufr := bufio.NewReader(remote.Stdout)
|
||||
line, _ := bufr.ReadString('\n')
|
||||
ui.Say(line)
|
||||
|
||||
// Stop the instance so we can create an AMI from it
|
||||
_, err = ec2conn.StopInstances(instance.InstanceId)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for the instance to actual stop
|
||||
// TODO: Handle diff source states, i.e. this force state sucks
|
||||
ui.Say("Waiting for the instance to stop...")
|
||||
instance.State.Name = "stopping"
|
||||
instance, err = waitForState(ec2conn, instance, "stopped")
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Create the image
|
||||
ui.Say("Creating the AMI...")
|
||||
createOpts := &ec2.CreateImage{
|
||||
InstanceId: instance.InstanceId,
|
||||
Name: b.config.AMIName,
|
||||
}
|
||||
|
||||
createResp, err := ec2conn.CreateImage(createOpts)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ui.Say("AMI: %s", createResp.ImageId)
|
||||
|
||||
// Wait for the image to become ready
|
||||
ui.Say("Waiting for AMI to become ready...")
|
||||
for {
|
||||
imageResp, err := ec2conn.Images([]string{createResp.ImageId}, ec2.NewFilter())
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if imageResp.Images[0].State == "available" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitForState(ec2conn *ec2.EC2, originalInstance *ec2.Instance, target string) (i *ec2.Instance, err error) {
|
||||
log.Printf("Waiting for instance state to become: %s", target)
|
||||
|
||||
i = originalInstance
|
||||
original := i.State.Name
|
||||
for i.State.Name == original {
|
||||
var resp *ec2.InstancesResp
|
||||
resp, err = ec2conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
i = &resp.Reservations[0].Instances[0]
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
if i.State.Name != target {
|
||||
err = fmt.Errorf("unexpected target state '%s', wanted '%s'", i.State.Name, target)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
// Run!
|
||||
RunSteps(state, steps)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package amazonebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func waitForState(ec2conn *ec2.EC2, originalInstance *ec2.Instance, target string) (i *ec2.Instance, err error) {
|
||||
log.Printf("Waiting for instance state to become: %s", target)
|
||||
|
||||
i = originalInstance
|
||||
original := i.State.Name
|
||||
for i.State.Name == original {
|
||||
var resp *ec2.InstancesResp
|
||||
resp, err = ec2conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
i = &resp.Reservations[0].Instances[0]
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
if i.State.Name != target {
|
||||
err = fmt.Errorf("unexpected target state '%s', wanted '%s'", i.State.Name, target)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package amazonebs
|
||||
|
||||
// A StepAction determines the next step to take regarding multi-step actions.
|
||||
type StepAction uint
|
||||
|
||||
const (
|
||||
StepContinue StepAction = iota
|
||||
StepHalt
|
||||
)
|
||||
|
||||
// Step is a single step that is part of a potentially large sequence
|
||||
// of other steps, responsible for performing some specific action.
|
||||
type Step interface {
|
||||
// Run is called to perform the action. The parameter is a "state bag"
|
||||
// of untyped things. Please be very careful about type-checking the
|
||||
// items in this bag.
|
||||
//
|
||||
// The return value determines whether multi-step sequences continue
|
||||
// or should halt.
|
||||
Run(map[string]interface{}) StepAction
|
||||
|
||||
// Cleanup is called in reverse order of the steps that have run
|
||||
// and allow steps to clean up after themselves.
|
||||
//
|
||||
// The parameter is the same "state bag" as Run.
|
||||
Cleanup(map[string]interface{})
|
||||
}
|
||||
|
||||
// RunSteps runs a sequence of steps.
|
||||
func RunSteps(state map[string]interface{}, steps []Step) {
|
||||
for _, step := range steps {
|
||||
action := step.Run(state)
|
||||
defer step.Cleanup(state)
|
||||
|
||||
if action == StepHalt {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package amazonebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
gossh "code.google.com/p/go.crypto/ssh"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"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{}) StepAction {
|
||||
instance := state["instance"].(*ec2.Instance)
|
||||
privateKey := state["privateKey"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// 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("Error setting up SSH config: %s", err.Error())
|
||||
return StepHalt
|
||||
}
|
||||
|
||||
// Build the actual SSH client configuration
|
||||
sshConfig := &gossh.ClientConfig{
|
||||
User: "ubuntu",
|
||||
Auth: []gossh.ClientAuth{
|
||||
gossh.ClientAuthKeyring(keyring),
|
||||
},
|
||||
}
|
||||
|
||||
// Try to connect for SSH a few times
|
||||
ui.Say("Connecting to the instance via SSH...")
|
||||
for i := 0; i < 5; i++ {
|
||||
time.Sleep(time.Duration(i) * time.Second)
|
||||
|
||||
log.Printf(
|
||||
"Opening TCP conn for SSH to %s:22 (attempt %d)",
|
||||
instance.DNSName, i+1)
|
||||
s.conn, err = net.Dial("tcp", fmt.Sprintf("%s:22", instance.DNSName))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var comm packer.Communicator
|
||||
if err == nil {
|
||||
comm, err = ssh.New(s.conn, sshConfig)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ui.Error("Error connecting to SSH: %s", err.Error())
|
||||
return StepHalt
|
||||
}
|
||||
|
||||
// Set the communicator on the state bag so it can be used later
|
||||
state["communicator"] = comm
|
||||
|
||||
return StepContinue
|
||||
}
|
||||
|
||||
func (s *stepConnectSSH) Cleanup(map[string]interface{}) {
|
||||
if s.conn != nil {
|
||||
s.conn.Close()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package amazonebs
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepCreateAMI struct {}
|
||||
|
||||
func (s *stepCreateAMI) Run(state map[string]interface{}) StepAction {
|
||||
config := state["config"].(config)
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
instance := state["instance"].(*ec2.Instance)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Create the image
|
||||
ui.Say("Creating the AMI...")
|
||||
createOpts := &ec2.CreateImage{
|
||||
InstanceId: instance.InstanceId,
|
||||
Name: config.AMIName,
|
||||
}
|
||||
|
||||
createResp, err := ec2conn.CreateImage(createOpts)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return StepHalt
|
||||
}
|
||||
|
||||
ui.Say("AMI: %s", createResp.ImageId)
|
||||
|
||||
// Wait for the image to become ready
|
||||
ui.Say("Waiting for AMI to become ready...")
|
||||
for {
|
||||
imageResp, err := ec2conn.Images([]string{createResp.ImageId}, ec2.NewFilter())
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return StepHalt
|
||||
}
|
||||
|
||||
if imageResp.Images[0].State == "available" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return StepContinue
|
||||
}
|
||||
|
||||
func (s *stepCreateAMI) Cleanup(map[string]interface{}) {
|
||||
// No cleanup...
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package amazonebs
|
||||
|
||||
import (
|
||||
"cgl.tideland.biz/identifier"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepKeyPair struct {
|
||||
keyName string
|
||||
}
|
||||
|
||||
func (s *stepKeyPair) Run(state map[string]interface{}) StepAction {
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Creating temporary keypair for this instance...")
|
||||
keyName := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw()))
|
||||
log.Printf("temporary keypair name: %s", keyName)
|
||||
keyResp, err := ec2conn.CreateKeyPair(keyName)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return StepHalt
|
||||
}
|
||||
|
||||
// Set the keyname so we know to delete it later
|
||||
s.keyName = keyName
|
||||
|
||||
// Set some state data for use in future steps
|
||||
state["keyPair"] = keyName
|
||||
state["privateKey"] = keyResp.KeyMaterial
|
||||
|
||||
return StepContinue
|
||||
}
|
||||
|
||||
func (s *stepKeyPair) Cleanup(state map[string]interface{}) {
|
||||
// If no key name is set, then we never created it, so just return
|
||||
if s.keyName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Deleting temporary keypair...")
|
||||
_, err := ec2conn.DeleteKeyPair(s.keyName)
|
||||
if err != nil {
|
||||
ui.Error(
|
||||
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package amazonebs
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepRunSourceInstance struct {
|
||||
instance *ec2.Instance
|
||||
}
|
||||
|
||||
func (s *stepRunSourceInstance) Run(state map[string]interface{}) StepAction {
|
||||
config := state["config"].(config)
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
keyName := state["keyPair"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
runOpts := &ec2.RunInstances{
|
||||
KeyName: keyName,
|
||||
ImageId: config.SourceAmi,
|
||||
InstanceType: config.InstanceType,
|
||||
MinCount: 0,
|
||||
MaxCount: 0,
|
||||
}
|
||||
|
||||
ui.Say("Launching a source AWS instance...")
|
||||
runResp, err := ec2conn.RunInstances(runOpts)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return StepHalt
|
||||
}
|
||||
|
||||
s.instance = &runResp.Instances[0]
|
||||
log.Printf("instance id: %s", s.instance.InstanceId)
|
||||
|
||||
ui.Say("Waiting for instance to become ready...")
|
||||
s.instance, err = waitForState(ec2conn, s.instance, "running")
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return StepHalt
|
||||
}
|
||||
|
||||
state["instance"] = s.instance
|
||||
|
||||
return StepContinue
|
||||
}
|
||||
|
||||
func (s *stepRunSourceInstance) Cleanup(state map[string]interface{}) {
|
||||
if s.instance == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// TODO(mitchellh): error handling
|
||||
ui.Say("Terminating the source AWS instance...")
|
||||
ec2conn.TerminateInstances([]string{s.instance.InstanceId})
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package amazonebs
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepStopInstance struct {}
|
||||
|
||||
func (s *stepStopInstance) Run(state map[string]interface{}) StepAction {
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
instance := state["instance"].(*ec2.Instance)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Stop the instance so we can create an AMI from it
|
||||
ui.Say("Stopping the source instance...")
|
||||
_, err := ec2conn.StopInstances(instance.InstanceId)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return StepHalt
|
||||
}
|
||||
|
||||
// Wait for the instance to actual stop
|
||||
// TODO(mitchellh): Handle diff source states, i.e. this force state sucks
|
||||
ui.Say("Waiting for the instance to stop...")
|
||||
instance.State.Name = "stopping"
|
||||
instance, err = waitForState(ec2conn, instance, "stopped")
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return StepHalt
|
||||
}
|
||||
|
||||
return StepContinue
|
||||
}
|
||||
|
||||
func (s *stepStopInstance) Cleanup(map[string]interface{}) {
|
||||
// No cleanup...
|
||||
}
|
Loading…
Reference in New Issue