diff --git a/builder/amazonebs/builder.go b/builder/amazonebs/builder.go index 096f16d2c..baed224b9 100644 --- a/builder/amazonebs/builder.go +++ b/builder/amazonebs/builder.go @@ -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 - } - - // 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 - } + // 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 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 + // Build the steps + steps := []Step{ + &stepKeyPair{}, + &stepRunSourceInstance{}, + &stepStopInstance{}, + &stepCreateAMI{}, } - // 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) } diff --git a/builder/amazonebs/instance.go b/builder/amazonebs/instance.go new file mode 100644 index 000000000..f3150466c --- /dev/null +++ b/builder/amazonebs/instance.go @@ -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 +} diff --git a/builder/amazonebs/step.go b/builder/amazonebs/step.go new file mode 100644 index 000000000..23970cca8 --- /dev/null +++ b/builder/amazonebs/step.go @@ -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 + } + } +} diff --git a/builder/amazonebs/step_connect_ssh.go b/builder/amazonebs/step_connect_ssh.go new file mode 100644 index 000000000..c9285c261 --- /dev/null +++ b/builder/amazonebs/step_connect_ssh.go @@ -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() + } +} diff --git a/builder/amazonebs/step_create_ami.go b/builder/amazonebs/step_create_ami.go new file mode 100644 index 000000000..aea0c05ef --- /dev/null +++ b/builder/amazonebs/step_create_ami.go @@ -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... +} diff --git a/builder/amazonebs/step_keypair.go b/builder/amazonebs/step_keypair.go new file mode 100644 index 000000000..47eb361cd --- /dev/null +++ b/builder/amazonebs/step_keypair.go @@ -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) + } +} diff --git a/builder/amazonebs/step_run_source_instance.go b/builder/amazonebs/step_run_source_instance.go new file mode 100644 index 000000000..5f557232b --- /dev/null +++ b/builder/amazonebs/step_run_source_instance.go @@ -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}) +} diff --git a/builder/amazonebs/step_stop_instance.go b/builder/amazonebs/step_stop_instance.go new file mode 100644 index 000000000..70e5a5c95 --- /dev/null +++ b/builder/amazonebs/step_stop_instance.go @@ -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... +}