From c6fde36b5e76e87c2013a0d99aff12f3cc775b2f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 16:42:35 -0700 Subject: [PATCH 01/47] builder/amazon/chroot: boilerplate --- builder/amazon/chroot/builder.go | 111 +++++++++++++++++++ builder/amazon/chroot/builder_test.go | 18 +++ builder/amazon/common/access_config.go | 26 +++++ builder/amazon/common/access_config_test.go | 27 +++++ builder/amazon/common/run_config.go | 8 -- builder/amazon/common/run_config_test.go | 19 ---- builder/amazon/ebs/builder.go | 7 +- builder/amazon/ebs/step_create_ami.go | 2 +- builder/amazon/instance/builder.go | 7 +- builder/amazon/instance/step_register_ami.go | 2 +- plugin/builder-amazon-ebs-chroot/main.go | 10 ++ 11 files changed, 200 insertions(+), 37 deletions(-) create mode 100644 builder/amazon/chroot/builder.go create mode 100644 builder/amazon/chroot/builder_test.go create mode 100644 builder/amazon/common/access_config_test.go create mode 100644 plugin/builder-amazon-ebs-chroot/main.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go new file mode 100644 index 000000000..d04ccb97f --- /dev/null +++ b/builder/amazon/chroot/builder.go @@ -0,0 +1,111 @@ +// The chroot package is able to create an Amazon AMI without requiring +// the launch of a new instance for every build. It does this by attaching +// and mounting the root volume of another AMI and chrooting into that +// directory. It then creates an AMI from that attached drive. +package chroot + +import ( + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/multistep" + awscommon "github.com/mitchellh/packer/builder/amazon/common" + "github.com/mitchellh/packer/builder/common" + "github.com/mitchellh/packer/packer" + "log" +) + +// The unique ID for this builder +const BuilderId = "mitchellh.amazon.chroot" + +// Config is the configuration that is chained through the steps and +// settable from the template. +type Config struct { + common.PackerConfig `mapstructure:",squash"` + awscommon.AccessConfig `mapstructure:",squash"` +} + +type Builder struct { + config Config + runner multistep.Runner +} + +func (b *Builder) Prepare(raws ...interface{}) error { + md, err := common.DecodeConfig(&b.config, raws...) + if err != nil { + return err + } + + // Defaults + + // Accumulate any errors + errs := common.CheckUnusedConfig(md) + errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...) + + if errs != nil && len(errs.Errors) > 0 { + return 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) { + region, err := b.config.Region() + if err != nil { + return nil, err + } + + auth, err := b.config.AccessConfig.Auth() + if err != nil { + return nil, err + } + + ec2conn := ec2.New(auth, region) + + // 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 := []multistep.Step{} + + // Run! + if b.config.PackerDebug { + b.runner = &multistep.DebugRunner{ + Steps: steps, + PauseFn: common.MultistepDebugFn(ui), + } + } else { + b.runner = &multistep.BasicRunner{Steps: steps} + } + + b.runner.Run(state) + + // If there was an error, return that + if rawErr, ok := state["error"]; ok { + return nil, rawErr.(error) + } + + // If there are no AMIs, then just return + if _, ok := state["amis"]; !ok { + return nil, nil + } + + // Build the artifact and return it + artifact := &awscommon.Artifact{ + Amis: state["amis"].(map[string]string), + BuilderIdValue: BuilderId, + Conn: ec2conn, + } + + return artifact, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/amazon/chroot/builder_test.go b/builder/amazon/chroot/builder_test.go new file mode 100644 index 000000000..32a110545 --- /dev/null +++ b/builder/amazon/chroot/builder_test.go @@ -0,0 +1,18 @@ +package chroot + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{} +} + +func TestBuilder_ImplementsBuilder(t *testing.T) { + var raw interface{} + raw = &Builder{} + if _, ok := raw.(packer.Builder); !ok { + t.Fatalf("Builder should be a builder") + } +} diff --git a/builder/amazon/common/access_config.go b/builder/amazon/common/access_config.go index 736d2d190..e78c15fad 100644 --- a/builder/amazon/common/access_config.go +++ b/builder/amazon/common/access_config.go @@ -1,13 +1,17 @@ package common import ( + "fmt" "github.com/mitchellh/goamz/aws" + "strings" + "unicode" ) // AccessConfig is for common configuration related to AWS access type AccessConfig struct { AccessKey string `mapstructure:"access_key"` SecretKey string `mapstructure:"secret_key"` + RawRegion string `mapstructure:"region"` } // Auth returns a valid aws.Auth object for access to AWS services, or @@ -16,6 +20,28 @@ func (c *AccessConfig) Auth() (aws.Auth, error) { return aws.GetAuth(c.AccessKey, c.SecretKey) } +// Region returns the aws.Region object for access to AWS services, requesting +// the region from the instance metadata if possible. +func (c *AccessConfig) Region() (aws.Region, error) { + if c.RawRegion != "" { + return aws.Regions[c.RawRegion], nil + } + + md, err := aws.GetMetaData("placement/availability-zone") + if err != nil { + return aws.Region{}, err + } + + region := strings.TrimRightFunc(string(md), unicode.IsLetter) + return aws.Regions[region], nil +} + func (c *AccessConfig) Prepare() []error { + if c.RawRegion != "" { + if _, ok := aws.Regions[c.RawRegion]; !ok { + return []error{fmt.Errorf("Unknown region: %s", c.RawRegion)} + } + } + return nil } diff --git a/builder/amazon/common/access_config_test.go b/builder/amazon/common/access_config_test.go new file mode 100644 index 000000000..cfb9e07f1 --- /dev/null +++ b/builder/amazon/common/access_config_test.go @@ -0,0 +1,27 @@ +package common + +import ( + "testing" +) + +func testAccessConfig() *AccessConfig { + return &AccessConfig{} +} + +func TestAccessConfigPrepare_Region(t *testing.T) { + c := testAccessConfig() + c.RawRegion = "" + if err := c.Prepare(); err != nil { + t.Fatalf("shouldn't have err: %s", err) + } + + c.RawRegion = "us-east-12" + if err := c.Prepare(); err == nil { + t.Fatal("should have error") + } + + c.RawRegion = "us-east-1" + if err := c.Prepare(); err != nil { + t.Fatalf("shouldn't have err: %s", err) + } +} diff --git a/builder/amazon/common/run_config.go b/builder/amazon/common/run_config.go index bd2186d93..321378cc9 100644 --- a/builder/amazon/common/run_config.go +++ b/builder/amazon/common/run_config.go @@ -3,14 +3,12 @@ package common import ( "errors" "fmt" - "github.com/mitchellh/goamz/aws" "time" ) // RunConfig contains configuration for running an instance from a source // AMI and details on how to access that launched image. type RunConfig struct { - Region string SourceAmi string `mapstructure:"source_ami"` InstanceType string `mapstructure:"instance_type"` RawSSHTimeout string `mapstructure:"ssh_timeout"` @@ -45,12 +43,6 @@ func (c *RunConfig) Prepare() []error { errs = append(errs, errors.New("An instance_type must be specified")) } - if c.Region == "" { - errs = append(errs, errors.New("A region must be specified")) - } else if _, ok := aws.Regions[c.Region]; !ok { - errs = append(errs, fmt.Errorf("Unknown region: %s", c.Region)) - } - if c.SSHUsername == "" { errs = append(errs, errors.New("An ssh_username must be specified")) } diff --git a/builder/amazon/common/run_config_test.go b/builder/amazon/common/run_config_test.go index 0c0d40680..b34b4c614 100644 --- a/builder/amazon/common/run_config_test.go +++ b/builder/amazon/common/run_config_test.go @@ -16,7 +16,6 @@ func init() { func testConfig() *RunConfig { return &RunConfig{ - Region: "us-east-1", SourceAmi: "abcd", InstanceType: "m1.small", SSHUsername: "root", @@ -39,24 +38,6 @@ func TestRunConfigPrepare_InstanceType(t *testing.T) { } } -func TestRunConfigPrepare_Region(t *testing.T) { - c := testConfig() - c.Region = "" - if err := c.Prepare(); len(err) != 1 { - t.Fatalf("err: %s", err) - } - - c.Region = "us-east-12" - if err := c.Prepare(); len(err) != 1 { - t.Fatalf("err: %s", err) - } - - c.Region = "us-east-1" - if err := c.Prepare(); len(err) != 0 { - t.Fatalf("err: %s", err) - } -} - func TestRunConfigPrepare_SourceAmi(t *testing.T) { c := testConfig() c.SourceAmi = "" diff --git a/builder/amazon/ebs/builder.go b/builder/amazon/ebs/builder.go index 46f5231fc..714c96afc 100644 --- a/builder/amazon/ebs/builder.go +++ b/builder/amazon/ebs/builder.go @@ -8,7 +8,6 @@ package ebs import ( "errors" "fmt" - "github.com/mitchellh/goamz/aws" "github.com/mitchellh/goamz/ec2" "github.com/mitchellh/multistep" awscommon "github.com/mitchellh/packer/builder/amazon/common" @@ -67,9 +66,9 @@ func (b *Builder) Prepare(raws ...interface{}) error { } func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { - region, ok := aws.Regions[b.config.Region] - if !ok { - panic("region not found") + region, err := b.config.Region() + if err != nil { + return nil, err } auth, err := b.config.AccessConfig.Auth() diff --git a/builder/amazon/ebs/step_create_ami.go b/builder/amazon/ebs/step_create_ami.go index 0b08bc7c9..5f75fb423 100644 --- a/builder/amazon/ebs/step_create_ami.go +++ b/builder/amazon/ebs/step_create_ami.go @@ -52,7 +52,7 @@ func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction { // Set the AMI ID in the state ui.Say(fmt.Sprintf("AMI: %s", createResp.ImageId)) amis := make(map[string]string) - amis[config.Region] = createResp.ImageId + amis[ec2conn.Region.Name] = createResp.ImageId state["amis"] = amis // Wait for the image to become ready diff --git a/builder/amazon/instance/builder.go b/builder/amazon/instance/builder.go index 83e486a49..ffbdfb467 100644 --- a/builder/amazon/instance/builder.go +++ b/builder/amazon/instance/builder.go @@ -5,7 +5,6 @@ package instance import ( "errors" "fmt" - "github.com/mitchellh/goamz/aws" "github.com/mitchellh/goamz/ec2" "github.com/mitchellh/multistep" awscommon "github.com/mitchellh/packer/builder/amazon/common" @@ -134,9 +133,9 @@ func (b *Builder) Prepare(raws ...interface{}) error { } func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { - region, ok := aws.Regions[b.config.Region] - if !ok { - panic("region not found") + region, err := b.config.Region() + if err != nil { + return nil, err } auth, err := b.config.AccessConfig.Auth() diff --git a/builder/amazon/instance/step_register_ami.go b/builder/amazon/instance/step_register_ami.go index d95deb888..1accf7e48 100644 --- a/builder/amazon/instance/step_register_ami.go +++ b/builder/amazon/instance/step_register_ami.go @@ -50,7 +50,7 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction // Set the AMI ID in the state ui.Say(fmt.Sprintf("AMI: %s", registerResp.ImageId)) amis := make(map[string]string) - amis[config.Region] = registerResp.ImageId + amis[ec2conn.Region.Name] = registerResp.ImageId state["amis"] = amis // Wait for the image to become ready diff --git a/plugin/builder-amazon-ebs-chroot/main.go b/plugin/builder-amazon-ebs-chroot/main.go new file mode 100644 index 000000000..b7d71df44 --- /dev/null +++ b/plugin/builder-amazon-ebs-chroot/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/mitchellh/packer/builder/amazon/chroot" + "github.com/mitchellh/packer/packer/plugin" +) + +func main() { + plugin.ServeBuilder(new(chroot.Builder)) +} From b329323bb21689e3c3a6fd27c2a5420f9e4a05a8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 17:07:05 -0700 Subject: [PATCH 02/47] builder/amazon/chroot: rename to builder-amazon-chroot --- builder/amazon/chroot/builder.go | 4 +++- builder/amazon/chroot/step_check_ec2.go | 16 ++++++++++++++++ config.go | 1 + .../main.go | 0 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 builder/amazon/chroot/step_check_ec2.go rename plugin/{builder-amazon-ebs-chroot => builder-amazon-chroot}/main.go (100%) diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index d04ccb97f..78b448787 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -69,7 +69,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe state["ui"] = ui // Build the steps - steps := []multistep.Step{} + steps := []multistep.Step{ + &StepCheckEC2{}, + } // Run! if b.config.PackerDebug { diff --git a/builder/amazon/chroot/step_check_ec2.go b/builder/amazon/chroot/step_check_ec2.go new file mode 100644 index 000000000..d600e8a8e --- /dev/null +++ b/builder/amazon/chroot/step_check_ec2.go @@ -0,0 +1,16 @@ +package chroot + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepCheckEC2 struct{} + +func (s *StepCheckEC2) Run(state map[string]interface{}) multistep.StepAction { + ui := state["ui"].(packer.Ui) + ui.Say("Verifying we're on an EC2 instance...") + return multistep.ActionContinue +} + +func (s *StepCheckEC2) Cleanup(map[string]interface{}) {} diff --git a/config.go b/config.go index 4660fad1c..e2b701374 100644 --- a/config.go +++ b/config.go @@ -20,6 +20,7 @@ const defaultConfig = ` "builders": { "amazon-ebs": "packer-builder-amazon-ebs", + "amazon-chroot": "packer-builder-amazon-chroot", "amazon-instance": "packer-builder-amazon-instance", "digitalocean": "packer-builder-digitalocean", "virtualbox": "packer-builder-virtualbox", diff --git a/plugin/builder-amazon-ebs-chroot/main.go b/plugin/builder-amazon-chroot/main.go similarity index 100% rename from plugin/builder-amazon-ebs-chroot/main.go rename to plugin/builder-amazon-chroot/main.go From c189c7ed12e3734a38f22268d5c37df7f66dc162 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 17:19:17 -0700 Subject: [PATCH 03/47] builder/amazon/chroot: verify we're on an EC2 instance --- builder/amazon/chroot/step_check_ec2.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/builder/amazon/chroot/step_check_ec2.go b/builder/amazon/chroot/step_check_ec2.go index d600e8a8e..4e8d5e6e8 100644 --- a/builder/amazon/chroot/step_check_ec2.go +++ b/builder/amazon/chroot/step_check_ec2.go @@ -1,15 +1,32 @@ package chroot import ( + "fmt" + "github.com/mitchellh/goamz/aws" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" + "log" ) +// StepCheckEC2 verifies that this builder is running on an EC2 instance. type StepCheckEC2 struct{} func (s *StepCheckEC2) Run(state map[string]interface{}) multistep.StepAction { ui := state["ui"].(packer.Ui) + ui.Say("Verifying we're on an EC2 instance...") + id, err := aws.GetMetaData("instance-id") + if err != nil { + log.Printf("Error: %s", err) + err := fmt.Errorf( + "Error retrieving the ID of the instance Packer is running on.\n" + + "Please verify Packer is running on a proper AWS EC2 instance.") + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + log.Printf("Instance ID: %s", string(id)) + return multistep.ActionContinue } From 90a27bc57bb8243f34c2bf4707c858b31d7f013e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 17:37:22 -0700 Subject: [PATCH 04/47] builder/amazon/chroot: extract the source AMI info --- builder/amazon/chroot/builder.go | 3 ++ builder/amazon/chroot/step_source_ami_info.go | 52 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 builder/amazon/chroot/step_source_ami_info.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 78b448787..717136548 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -21,6 +21,8 @@ const BuilderId = "mitchellh.amazon.chroot" type Config struct { common.PackerConfig `mapstructure:",squash"` awscommon.AccessConfig `mapstructure:",squash"` + + SourceAmi string `mapstructure:"source_ami"` } type Builder struct { @@ -71,6 +73,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Build the steps steps := []multistep.Step{ &StepCheckEC2{}, + &StepSourceAMIInfo{}, } // Run! diff --git a/builder/amazon/chroot/step_source_ami_info.go b/builder/amazon/chroot/step_source_ami_info.go new file mode 100644 index 000000000..d8fc82f97 --- /dev/null +++ b/builder/amazon/chroot/step_source_ami_info.go @@ -0,0 +1,52 @@ +package chroot + +import ( + "fmt" + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// StepSourceAMIInfo extracts critical information from the source AMI +// that is used throughout the AMI creation process. +// +// Produces: +// source_image *ec2.Image - the source AMI info +type StepSourceAMIInfo struct{} + +func (s *StepSourceAMIInfo) Run(state map[string]interface{}) multistep.StepAction { + config := state["config"].(*Config) + ec2conn := state["ec2"].(*ec2.EC2) + ui := state["ui"].(packer.Ui) + + ui.Say("Inspecting the source AMI...") + imageResp, err := ec2conn.Images([]string{config.SourceAmi}, ec2.NewFilter()) + if err != nil { + err := fmt.Errorf("Error querying AMI: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if len(imageResp.Images) == 0 { + err := fmt.Errorf("Source AMI '%s' was not found!", config.SourceAmi) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + image := imageResp.Images[0] + + // It must be EBS-backed otherwise the build won't work + if image.RootDeviceType != "ebs" { + err := fmt.Errorf("The root device of the source AMI must be EBS-backed.") + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + state["source_image"] = image + return multistep.ActionContinue +} + +func (s *StepSourceAMIInfo) Cleanup(map[string]interface{}) {} From 726c4a68ef628d4b09f1e656f6fbf0aac30a5641 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 18:07:07 -0700 Subject: [PATCH 05/47] builder/amazon/chroot: create the volume --- builder/amazon/chroot/builder.go | 1 + builder/amazon/chroot/step_create_volume.go | 66 +++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 builder/amazon/chroot/step_create_volume.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 717136548..ecb9f8605 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -74,6 +74,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe steps := []multistep.Step{ &StepCheckEC2{}, &StepSourceAMIInfo{}, + &StepCreateVolume{}, } // Run! diff --git a/builder/amazon/chroot/step_create_volume.go b/builder/amazon/chroot/step_create_volume.go new file mode 100644 index 000000000..eea56f081 --- /dev/null +++ b/builder/amazon/chroot/step_create_volume.go @@ -0,0 +1,66 @@ +package chroot + +import ( + "fmt" + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +// StepCreateVolume creates a new volume from the snapshot of the root +// device of the AMI. +// +// Produces: +// volume_id string - The ID of the created volume +type StepCreateVolume struct { + volumeId string +} + +func (s *StepCreateVolume) Run(state map[string]interface{}) multistep.StepAction { + ec2conn := state["ec2"].(*ec2.EC2) + image := state["source_image"].(*ec2.Image) + instance := state["instance"].(*ec2.Instance) + ui := state["ui"].(packer.Ui) + + // Determine the root device snapshot + log.Printf("Searching for root device of the image (%s)", image.RootDeviceName) + var rootDevice *ec2.BlockDeviceMapping + for _, device := range image.BlockDevices { + if device.DeviceName == image.RootDeviceName { + rootDevice = &device + break + } + } + + if rootDevice == nil { + err := fmt.Errorf("Couldn't find root device!") + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say("Creating the root volume...") + createVolume := &ec2.CreateVolume{ + AvailZone: instance.AvailZone, + Size: rootDevice.VolumeSize, + SnapshotId: rootDevice.SnapshotId, + VolumeType: rootDevice.VolumeType, + IOPS: rootDevice.IOPS, + } + + createVolumeResp, err := ec2conn.CreateVolume(createVolume) + if err != nil { + err := fmt.Errorf("Error creating root volume: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Set the volume ID so we remember to delete it later + s.volumeId = createVolumeResp.VolumeId + + return multistep.ActionContinue +} + +func (s *StepCreateVolume) Cleanup(map[string]interface{}) {} From e5f0cbe2984bd6f4deb80a2e7b78c0bc2fb5dda0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 18:13:22 -0700 Subject: [PATCH 06/47] builder/amazon/chroot: step to gather instance info --- builder/amazon/chroot/builder.go | 2 +- builder/amazon/chroot/step_check_ec2.go | 33 ------------ builder/amazon/chroot/step_instance_info.go | 57 +++++++++++++++++++++ 3 files changed, 58 insertions(+), 34 deletions(-) delete mode 100644 builder/amazon/chroot/step_check_ec2.go create mode 100644 builder/amazon/chroot/step_instance_info.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index ecb9f8605..4ab739bbb 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -72,7 +72,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Build the steps steps := []multistep.Step{ - &StepCheckEC2{}, + &StepInstanceInfo{}, &StepSourceAMIInfo{}, &StepCreateVolume{}, } diff --git a/builder/amazon/chroot/step_check_ec2.go b/builder/amazon/chroot/step_check_ec2.go deleted file mode 100644 index 4e8d5e6e8..000000000 --- a/builder/amazon/chroot/step_check_ec2.go +++ /dev/null @@ -1,33 +0,0 @@ -package chroot - -import ( - "fmt" - "github.com/mitchellh/goamz/aws" - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/packer" - "log" -) - -// StepCheckEC2 verifies that this builder is running on an EC2 instance. -type StepCheckEC2 struct{} - -func (s *StepCheckEC2) Run(state map[string]interface{}) multistep.StepAction { - ui := state["ui"].(packer.Ui) - - ui.Say("Verifying we're on an EC2 instance...") - id, err := aws.GetMetaData("instance-id") - if err != nil { - log.Printf("Error: %s", err) - err := fmt.Errorf( - "Error retrieving the ID of the instance Packer is running on.\n" + - "Please verify Packer is running on a proper AWS EC2 instance.") - state["error"] = err - ui.Error(err.Error()) - return multistep.ActionHalt - } - log.Printf("Instance ID: %s", string(id)) - - return multistep.ActionContinue -} - -func (s *StepCheckEC2) Cleanup(map[string]interface{}) {} diff --git a/builder/amazon/chroot/step_instance_info.go b/builder/amazon/chroot/step_instance_info.go new file mode 100644 index 000000000..b318f161e --- /dev/null +++ b/builder/amazon/chroot/step_instance_info.go @@ -0,0 +1,57 @@ +package chroot + +import ( + "fmt" + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +// StepInstanceInfo verifies that this builder is running on an EC2 instance. +type StepInstanceInfo struct{} + +func (s *StepInstanceInfo) Run(state map[string]interface{}) multistep.StepAction { + ec2conn := state["ec2"].(*ec2.EC2) + ui := state["ui"].(packer.Ui) + + // Get our own instance ID + ui.Say("Gathering information about this EC2 instance...") + instanceIdBytes, err := aws.GetMetaData("instance-id") + if err != nil { + log.Printf("Error: %s", err) + err := fmt.Errorf( + "Error retrieving the ID of the instance Packer is running on.\n" + + "Please verify Packer is running on a proper AWS EC2 instance.") + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + instanceId := string(instanceIdBytes) + log.Printf("Instance ID: %s", instanceId) + + // Query the entire instance metadata + instancesResp, err := ec2conn.Instances([]string{instanceId}, ec2.NewFilter()) + if err != nil { + err := fmt.Errorf("Error getting instance data: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if len(instancesResp.Reservations) == 0 { + err := fmt.Errorf("Error getting instance data: no instance found.") + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + instance := instancesResp.Reservations[0].Instances[0] + state["instance"] = instance + + return multistep.ActionContinue +} + +func (s *StepInstanceInfo) Cleanup(map[string]interface{}) {} From 3f49f1a6d0da1dee21a12cd663f32ca0c7aa59e1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 18:17:27 -0700 Subject: [PATCH 07/47] builder/amazon/chroot: delete the EBS volume after we're done --- builder/amazon/chroot/step_create_volume.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/builder/amazon/chroot/step_create_volume.go b/builder/amazon/chroot/step_create_volume.go index eea56f081..21e924916 100644 --- a/builder/amazon/chroot/step_create_volume.go +++ b/builder/amazon/chroot/step_create_volume.go @@ -63,4 +63,17 @@ func (s *StepCreateVolume) Run(state map[string]interface{}) multistep.StepActio return multistep.ActionContinue } -func (s *StepCreateVolume) Cleanup(map[string]interface{}) {} +func (s *StepCreateVolume) Cleanup(state map[string]interface{}) { + if s.volumeId == "" { + return + } + + ec2conn := state["ec2"].(*ec2.EC2) + ui := state["ui"].(packer.Ui) + + ui.Say("Deleting the created EBS volume...") + _, err := ec2conn.DeleteVolume(s.volumeId) + if err != nil { + ui.Error(fmt.Sprintf("Error deleting EBS volume: %s", err)) + } +} From 3f0c4b0e1939d8c6af2318b67507f143f761444e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 18:47:43 -0700 Subject: [PATCH 08/47] builder/amazon/common: generic wait for state to wait for any state --- builder/amazon/common/instance.go | 40 +++++++++++++------ .../amazon/common/step_run_source_instance.go | 7 ++-- builder/amazon/ebs/step_stop_instance.go | 5 ++- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/builder/amazon/common/instance.go b/builder/amazon/common/instance.go index 442ed597c..5411916b0 100644 --- a/builder/amazon/common/instance.go +++ b/builder/amazon/common/instance.go @@ -11,17 +11,38 @@ import ( type StateChangeConf struct { Conn *ec2.EC2 - Instance *ec2.Instance Pending []string + Refresh func() (interface{}, string, error) StepState map[string]interface{} Target string } -func WaitForState(conf *StateChangeConf) (i *ec2.Instance, err error) { +func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) func() (interface{}, string, error) { + return func() (interface{}, string, error) { + resp, err := conn.Instances([]string{i.InstanceId}, ec2.NewFilter()) + if err != nil { + return nil, "", err + } + + i = &resp.Reservations[0].Instances[0] + return i, i.State.Name, nil + } +} + +func WaitForState(conf *StateChangeConf) (i interface{}, err error) { log.Printf("Waiting for instance state to become: %s", conf.Target) - i = conf.Instance - for i.State.Name != conf.Target { + for { + var currentState string + i, currentState, err = conf.Refresh() + if err != nil { + return + } + + if currentState == conf.Target { + return + } + if conf.StepState != nil { if _, ok := conf.StepState[multistep.StateCancelled]; ok { return nil, errors.New("interrupted") @@ -30,24 +51,17 @@ func WaitForState(conf *StateChangeConf) (i *ec2.Instance, err error) { found := false for _, allowed := range conf.Pending { - if i.State.Name == allowed { + if currentState == allowed { found = true break } } if !found { - fmt.Errorf("unexpected state '%s', wanted target '%s'", i.State.Name, conf.Target) + fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target) return } - var resp *ec2.InstancesResp - resp, err = conf.Conn.Instances([]string{i.InstanceId}, ec2.NewFilter()) - if err != nil { - return - } - - i = &resp.Reservations[0].Instances[0] time.Sleep(2 * time.Second) } diff --git a/builder/amazon/common/step_run_source_instance.go b/builder/amazon/common/step_run_source_instance.go index 22018246b..ec823ffc6 100644 --- a/builder/amazon/common/step_run_source_instance.go +++ b/builder/amazon/common/step_run_source_instance.go @@ -62,12 +62,13 @@ func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.Step ui.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", s.instance.InstanceId)) stateChange := StateChangeConf{ Conn: ec2conn, - Instance: s.instance, Pending: []string{"pending"}, Target: "running", + Refresh: InstanceStateRefreshFunc(ec2conn, s.instance), StepState: state, } - s.instance, err = WaitForState(&stateChange) + latestInstance, err := WaitForState(&stateChange) + s.instance = latestInstance.(*ec2.Instance) if err != nil { err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", s.instance.InstanceId, err) state["error"] = err @@ -96,8 +97,8 @@ func (s *StepRunSourceInstance) Cleanup(state map[string]interface{}) { stateChange := StateChangeConf{ Conn: ec2conn, - Instance: s.instance, Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"}, + Refresh: InstanceStateRefreshFunc(ec2conn, s.instance), Target: "running", } diff --git a/builder/amazon/ebs/step_stop_instance.go b/builder/amazon/ebs/step_stop_instance.go index f0c7b47a9..616e91b29 100644 --- a/builder/amazon/ebs/step_stop_instance.go +++ b/builder/amazon/ebs/step_stop_instance.go @@ -29,12 +29,13 @@ func (s *stepStopInstance) Run(state map[string]interface{}) multistep.StepActio ui.Say("Waiting for the instance to stop...") stateChange := awscommon.StateChangeConf{ Conn: ec2conn, - Instance: instance, Pending: []string{"running", "stopping"}, Target: "stopped", + Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, instance), StepState: state, } - instance, err = awscommon.WaitForState(&stateChange) + instanceRaw, err := awscommon.WaitForState(&stateChange) + instance = instanceRaw.(*ec2.Instance) if err != nil { err := fmt.Errorf("Error waiting for instance to stop: %s", err) state["error"] = err From 7c04d634f9b4643a208023c7a0c93cd1ff5902b8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 18:55:11 -0700 Subject: [PATCH 09/47] builder/amazon/chroot: wait for volume to beecome ready --- builder/amazon/chroot/builder.go | 1 + builder/amazon/chroot/step_create_volume.go | 26 +++++++++++++++++++ builder/amazon/common/instance.go | 2 +- .../amazon/common/step_run_source_instance.go | 10 +++---- builder/amazon/ebs/step_stop_instance.go | 2 +- 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 4ab739bbb..15928a220 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -75,6 +75,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepInstanceInfo{}, &StepSourceAMIInfo{}, &StepCreateVolume{}, + //&StepAttachVolume{}, } // Run! diff --git a/builder/amazon/chroot/step_create_volume.go b/builder/amazon/chroot/step_create_volume.go index 21e924916..9d7e0a1cb 100644 --- a/builder/amazon/chroot/step_create_volume.go +++ b/builder/amazon/chroot/step_create_volume.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/mitchellh/goamz/ec2" "github.com/mitchellh/multistep" + awscommon "github.com/mitchellh/packer/builder/amazon/common" "github.com/mitchellh/packer/packer" "log" ) @@ -59,6 +60,31 @@ func (s *StepCreateVolume) Run(state map[string]interface{}) multistep.StepActio // Set the volume ID so we remember to delete it later s.volumeId = createVolumeResp.VolumeId + log.Printf("Volume ID: %s", s.volumeId) + + // Wait for the volume to become ready + stateChange := awscommon.StateChangeConf{ + Conn: ec2conn, + Pending: []string{"creating"}, + StepState: state, + Target: "available", + Refresh: func() (interface{}, string, error) { + resp, err := ec2conn.Volumes([]string{s.volumeId}, ec2.NewFilter()) + if err != nil { + return nil, "", err + } + + return nil, resp.Volumes[0].Status, nil + }, + } + + _, err = awscommon.WaitForState(&stateChange) + if err != nil { + err := fmt.Errorf("Error waiting for volume: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } return multistep.ActionContinue } diff --git a/builder/amazon/common/instance.go b/builder/amazon/common/instance.go index 5411916b0..12c033d66 100644 --- a/builder/amazon/common/instance.go +++ b/builder/amazon/common/instance.go @@ -12,7 +12,7 @@ import ( type StateChangeConf struct { Conn *ec2.EC2 Pending []string - Refresh func() (interface{}, string, error) + Refresh func() (interface{}, string, error) StepState map[string]interface{} Target string } diff --git a/builder/amazon/common/step_run_source_instance.go b/builder/amazon/common/step_run_source_instance.go index ec823ffc6..d0767d4a8 100644 --- a/builder/amazon/common/step_run_source_instance.go +++ b/builder/amazon/common/step_run_source_instance.go @@ -64,7 +64,7 @@ func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.Step Conn: ec2conn, Pending: []string{"pending"}, Target: "running", - Refresh: InstanceStateRefreshFunc(ec2conn, s.instance), + Refresh: InstanceStateRefreshFunc(ec2conn, s.instance), StepState: state, } latestInstance, err := WaitForState(&stateChange) @@ -96,10 +96,10 @@ func (s *StepRunSourceInstance) Cleanup(state map[string]interface{}) { } stateChange := StateChangeConf{ - Conn: ec2conn, - Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"}, - Refresh: InstanceStateRefreshFunc(ec2conn, s.instance), - Target: "running", + Conn: ec2conn, + Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"}, + Refresh: InstanceStateRefreshFunc(ec2conn, s.instance), + Target: "running", } WaitForState(&stateChange) diff --git a/builder/amazon/ebs/step_stop_instance.go b/builder/amazon/ebs/step_stop_instance.go index 616e91b29..6f6add6c8 100644 --- a/builder/amazon/ebs/step_stop_instance.go +++ b/builder/amazon/ebs/step_stop_instance.go @@ -31,7 +31,7 @@ func (s *stepStopInstance) Run(state map[string]interface{}) multistep.StepActio Conn: ec2conn, Pending: []string{"running", "stopping"}, Target: "stopped", - Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, instance), + Refresh: awscommon.InstanceStateRefreshFunc(ec2conn, instance), StepState: state, } instanceRaw, err := awscommon.WaitForState(&stateChange) From 702f299343764479a1226b89716159d09d967b12 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 19:07:51 -0700 Subject: [PATCH 10/47] builder/amazon/chroot: Attach volume --- builder/amazon/chroot/builder.go | 2 +- builder/amazon/chroot/step_attach_volume.go | 115 ++++++++++++++++++++ builder/amazon/chroot/step_create_volume.go | 1 + 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 builder/amazon/chroot/step_attach_volume.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 15928a220..37c308a98 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -75,7 +75,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepInstanceInfo{}, &StepSourceAMIInfo{}, &StepCreateVolume{}, - //&StepAttachVolume{}, + &StepAttachVolume{}, } // Run! diff --git a/builder/amazon/chroot/step_attach_volume.go b/builder/amazon/chroot/step_attach_volume.go new file mode 100644 index 000000000..df7c85b47 --- /dev/null +++ b/builder/amazon/chroot/step_attach_volume.go @@ -0,0 +1,115 @@ +package chroot + +import ( + "errors" + "fmt" + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/multistep" + awscommon "github.com/mitchellh/packer/builder/amazon/common" + "github.com/mitchellh/packer/packer" +) + +// StepAttachVolume attaches the previously created volume to an +// available device location. +// +// Produces: +// volume_id string - The ID of the created volume +type StepAttachVolume struct { + attached bool + volumeId string +} + +func (s *StepAttachVolume) Run(state map[string]interface{}) multistep.StepAction { + ec2conn := state["ec2"].(*ec2.EC2) + instance := state["instance"].(*ec2.Instance) + ui := state["ui"].(packer.Ui) + volumeId := state["volume_id"].(string) + + device := "/dev/sdh" + + ui.Say("Attaching the root volume...") + _, err := ec2conn.AttachVolume(volumeId, instance.InstanceId, device) + if err != nil { + err := fmt.Errorf("Error attaching volume: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Mark that we attached it so we can detach it later + s.attached = true + s.volumeId = volumeId + + // Wait for the volume to become attached + stateChange := awscommon.StateChangeConf{ + Conn: ec2conn, + Pending: []string{"attaching"}, + StepState: state, + Target: "attached", + Refresh: func() (interface{}, string, error) { + resp, err := ec2conn.Volumes([]string{volumeId}, ec2.NewFilter()) + if err != nil { + return nil, "", err + } + + if len(resp.Volumes[0].Attachments) == 0 { + return nil, "", errors.New("No attachments on volume.") + } + + return nil, resp.Volumes[0].Attachments[0].Status, nil + }, + } + + _, err = awscommon.WaitForState(&stateChange) + if err != nil { + err := fmt.Errorf("Error waiting for volume: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepAttachVolume) Cleanup(state map[string]interface{}) { + if !s.attached { + return + } + + ec2conn := state["ec2"].(*ec2.EC2) + ui := state["ui"].(packer.Ui) + + ui.Say("Detaching EBS volume...") + _, err := ec2conn.DetachVolume(s.volumeId) + if err != nil { + ui.Error(fmt.Sprintf("Error detaching EBS volume: %s", err)) + return + } + + // Wait for the volume to detach + stateChange := awscommon.StateChangeConf{ + Conn: ec2conn, + Pending: []string{"detaching"}, + StepState: state, + Target: "detached", + Refresh: func() (interface{}, string, error) { + resp, err := ec2conn.Volumes([]string{s.volumeId}, ec2.NewFilter()) + if err != nil { + return nil, "", err + } + + state := "detached" + if len(resp.Volumes[0].Attachments) > 0 { + state = resp.Volumes[0].Attachments[0].Status + } + + return nil, state, nil + }, + } + + _, err = awscommon.WaitForState(&stateChange) + if err != nil { + ui.Error(fmt.Sprintf("Error waiting for volume: %s", err)) + return + } +} diff --git a/builder/amazon/chroot/step_create_volume.go b/builder/amazon/chroot/step_create_volume.go index 9d7e0a1cb..0e9cb2a36 100644 --- a/builder/amazon/chroot/step_create_volume.go +++ b/builder/amazon/chroot/step_create_volume.go @@ -86,6 +86,7 @@ func (s *StepCreateVolume) Run(state map[string]interface{}) multistep.StepActio return multistep.ActionHalt } + state["volume_id"] = s.volumeId return multistep.ActionContinue } From 5a70c82e25d31618f1914b604d7f0f16256a0b18 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 19:08:22 -0700 Subject: [PATCH 11/47] builder/amazon/chroot: more valid transition states while detaching --- builder/amazon/chroot/step_attach_volume.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/amazon/chroot/step_attach_volume.go b/builder/amazon/chroot/step_attach_volume.go index df7c85b47..eee9ad828 100644 --- a/builder/amazon/chroot/step_attach_volume.go +++ b/builder/amazon/chroot/step_attach_volume.go @@ -89,7 +89,7 @@ func (s *StepAttachVolume) Cleanup(state map[string]interface{}) { // Wait for the volume to detach stateChange := awscommon.StateChangeConf{ Conn: ec2conn, - Pending: []string{"detaching"}, + Pending: []string{"attaching", "attached", "detaching"}, StepState: state, Target: "detached", Refresh: func() (interface{}, string, error) { From af492621deb0b953bf95f787469a9df80f1b2f0e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 19:10:20 -0700 Subject: [PATCH 12/47] builder/amazon/chroot: put device in state bag --- builder/amazon/chroot/step_attach_volume.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/builder/amazon/chroot/step_attach_volume.go b/builder/amazon/chroot/step_attach_volume.go index eee9ad828..b380420b7 100644 --- a/builder/amazon/chroot/step_attach_volume.go +++ b/builder/amazon/chroot/step_attach_volume.go @@ -13,7 +13,7 @@ import ( // available device location. // // Produces: -// volume_id string - The ID of the created volume +// device string - The location where the volume was attached. type StepAttachVolume struct { attached bool volumeId string @@ -68,6 +68,7 @@ func (s *StepAttachVolume) Run(state map[string]interface{}) multistep.StepActio return multistep.ActionHalt } + state["device"] = device return multistep.ActionContinue } From 121e9791a39f17abf2bb90b5cd3410bb3d6ea3e0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 21:59:34 -0700 Subject: [PATCH 13/47] builder/amazon/chroot: only let it run on Linux --- builder/amazon/chroot/builder.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 37c308a98..1711e58d1 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -5,12 +5,14 @@ package chroot import ( + "errors" "github.com/mitchellh/goamz/ec2" "github.com/mitchellh/multistep" awscommon "github.com/mitchellh/packer/builder/amazon/common" "github.com/mitchellh/packer/builder/common" "github.com/mitchellh/packer/packer" "log" + "runtime" ) // The unique ID for this builder @@ -51,6 +53,10 @@ func (b *Builder) Prepare(raws ...interface{}) error { } func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + if runtime.GOOS != "linux" { + return nil, errors.New("The amazon-chroot builder only works on Linux environments.") + } + region, err := b.config.Region() if err != nil { return nil, err From 43588309532e9e73144cde5c54201135f635c5a9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 22:48:01 -0700 Subject: [PATCH 14/47] builder/amazon/chroot: fix types --- builder/amazon/chroot/step_instance_info.go | 2 +- builder/amazon/chroot/step_source_ami_info.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/builder/amazon/chroot/step_instance_info.go b/builder/amazon/chroot/step_instance_info.go index b318f161e..53080e171 100644 --- a/builder/amazon/chroot/step_instance_info.go +++ b/builder/amazon/chroot/step_instance_info.go @@ -48,7 +48,7 @@ func (s *StepInstanceInfo) Run(state map[string]interface{}) multistep.StepActio return multistep.ActionHalt } - instance := instancesResp.Reservations[0].Instances[0] + instance := &instancesResp.Reservations[0].Instances[0] state["instance"] = instance return multistep.ActionContinue diff --git a/builder/amazon/chroot/step_source_ami_info.go b/builder/amazon/chroot/step_source_ami_info.go index d8fc82f97..28f47149b 100644 --- a/builder/amazon/chroot/step_source_ami_info.go +++ b/builder/amazon/chroot/step_source_ami_info.go @@ -35,7 +35,7 @@ func (s *StepSourceAMIInfo) Run(state map[string]interface{}) multistep.StepActi return multistep.ActionHalt } - image := imageResp.Images[0] + image := &imageResp.Images[0] // It must be EBS-backed otherwise the build won't work if image.RootDeviceType != "ebs" { From 1e9cc89f73a3204d158ca813eacc829c135f55d3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 29 Jul 2013 22:50:29 -0700 Subject: [PATCH 15/47] builder/amazon/chroot: improved logging about what volume created --- builder/amazon/chroot/step_create_volume.go | 1 + 1 file changed, 1 insertion(+) diff --git a/builder/amazon/chroot/step_create_volume.go b/builder/amazon/chroot/step_create_volume.go index 0e9cb2a36..d0e247989 100644 --- a/builder/amazon/chroot/step_create_volume.go +++ b/builder/amazon/chroot/step_create_volume.go @@ -49,6 +49,7 @@ func (s *StepCreateVolume) Run(state map[string]interface{}) multistep.StepActio VolumeType: rootDevice.VolumeType, IOPS: rootDevice.IOPS, } + log.Printf("Create args: %#v", createVolume) createVolumeResp, err := ec2conn.CreateVolume(createVolume) if err != nil { From df85c67e4aab69fba30b0fbc2d79c05a5fd39620 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 09:55:17 -0700 Subject: [PATCH 16/47] builder/amazon/chroot: more settings, validation --- builder/amazon/chroot/builder.go | 16 +++++++++++++++- builder/amazon/chroot/builder_test.go | 21 ++++++++++++++++++++- builder/amazon/chroot/step_attach_volume.go | 3 ++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 1711e58d1..ec344d938 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -24,7 +24,10 @@ type Config struct { common.PackerConfig `mapstructure:",squash"` awscommon.AccessConfig `mapstructure:",squash"` - SourceAmi string `mapstructure:"source_ami"` + SourceAmi string `mapstructure:"source_ami"` + AttachedDevicePath string `mapstructure:"attached_device_path"` + DevicePath string `mapstructure:"device_path"` + MountPath string `mapstructure:"mount_path"` } type Builder struct { @@ -39,11 +42,22 @@ func (b *Builder) Prepare(raws ...interface{}) error { } // Defaults + if b.config.DevicePath == "" { + b.config.DevicePath = "/dev/sdh" + } + + if b.config.MountPath == "" { + b.config.MountPath = "/var/packer-amazon-chroot/volumes/{{.Device}}" + } // Accumulate any errors errs := common.CheckUnusedConfig(md) errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...) + if b.config.SourceAmi == "" { + errs = packer.MultiErrorAppend(errs, errors.New("source_ami is required.")) + } + if errs != nil && len(errs.Errors) > 0 { return errs } diff --git a/builder/amazon/chroot/builder_test.go b/builder/amazon/chroot/builder_test.go index 32a110545..2736dbbd8 100644 --- a/builder/amazon/chroot/builder_test.go +++ b/builder/amazon/chroot/builder_test.go @@ -6,7 +6,9 @@ import ( ) func testConfig() map[string]interface{} { - return map[string]interface{}{} + return map[string]interface{}{ + "source_ami": "foo", + } } func TestBuilder_ImplementsBuilder(t *testing.T) { @@ -16,3 +18,20 @@ func TestBuilder_ImplementsBuilder(t *testing.T) { t.Fatalf("Builder should be a builder") } } + +func TestBuilderPrepare_SourceAmi(t *testing.T) { + b := &Builder{} + config := testConfig() + + config["source_ami"] = "" + err := b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + config["source_ami"] = "foo" + err = b.Prepare(config) + if err != nil { + t.Errorf("err: %s", err) + } +} diff --git a/builder/amazon/chroot/step_attach_volume.go b/builder/amazon/chroot/step_attach_volume.go index b380420b7..cf5f89e3e 100644 --- a/builder/amazon/chroot/step_attach_volume.go +++ b/builder/amazon/chroot/step_attach_volume.go @@ -20,12 +20,13 @@ type StepAttachVolume struct { } func (s *StepAttachVolume) Run(state map[string]interface{}) multistep.StepAction { + config := state["config"].(*Config) ec2conn := state["ec2"].(*ec2.EC2) instance := state["instance"].(*ec2.Instance) ui := state["ui"].(packer.Ui) volumeId := state["volume_id"].(string) - device := "/dev/sdh" + device := config.DevicePath ui.Say("Attaching the root volume...") _, err := ec2conn.AttachVolume(volumeId, instance.InstanceId, device) From 22aad9c87cf56b405f2d223827bf8eb26db5bc18 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 11:05:06 -0700 Subject: [PATCH 17/47] builder/amazon/chroot: mount the root device --- builder/amazon/chroot/builder.go | 1 + builder/amazon/chroot/step_mount_device.go | 71 ++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 builder/amazon/chroot/step_mount_device.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index ec344d938..9e03e6771 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -96,6 +96,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepSourceAMIInfo{}, &StepCreateVolume{}, &StepAttachVolume{}, + &StepMountDevice{}, } // Run! diff --git a/builder/amazon/chroot/step_mount_device.go b/builder/amazon/chroot/step_mount_device.go new file mode 100644 index 000000000..4655c37c9 --- /dev/null +++ b/builder/amazon/chroot/step_mount_device.go @@ -0,0 +1,71 @@ +package chroot + +import ( + "bytes" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "os" + "os/exec" +) + +// StepMountDevice mounts the attached device. +// +// Produces: +// mount_path string - The location where the volume was mounted. +type StepMountDevice struct { + mountPath string +} + +func (s *StepMountDevice) Run(state map[string]interface{}) multistep.StepAction { + config := state["config"].(*Config) + ui := state["ui"].(packer.Ui) + device := state["device"].(string) + + mountPath := config.MountPath + log.Printf("Mount path: %s", mountPath) + + if err := os.MkdirAll(mountPath, 0755); err != nil { + err := fmt.Errorf("Error creating mount directory: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Say("Mounting the root device...") + stderr := new(bytes.Buffer) + cmd := exec.Command("mount", device, mountPath) + cmd.Stderr = stderr + if err := cmd.Run(); err != nil { + err := fmt.Errorf( + "Error mounting root volume: %s\nStderr: %s", err, stderr.String()) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepMountDevice) Cleanup(state map[string]interface{}) { + if s.mountPath == "" { + return + } + + ui := state["ui"].(packer.Ui) + ui.Say("Unmounting the root device...") + + path, err := exec.LookPath("umount") + if err != nil { + ui.Error(fmt.Sprintf("Error umounting root device: %s", err)) + return + } + + cmd := exec.Command(path, s.mountPath) + if err := cmd.Run(); err != nil { + ui.Error(fmt.Sprintf( + "Error unmounting root device: %s", err)) + return + } +} From 713f466670fcf8e32383ed803f60000a8252d43b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 11:27:55 -0700 Subject: [PATCH 18/47] builder/amazon/chroot: use mountcommand configs --- builder/amazon/chroot/builder.go | 33 +++++++++++++++++++--- builder/amazon/chroot/step_mount_device.go | 13 ++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 9e03e6771..cf8848903 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -24,10 +24,13 @@ type Config struct { common.PackerConfig `mapstructure:",squash"` awscommon.AccessConfig `mapstructure:",squash"` - SourceAmi string `mapstructure:"source_ami"` - AttachedDevicePath string `mapstructure:"attached_device_path"` - DevicePath string `mapstructure:"device_path"` - MountPath string `mapstructure:"mount_path"` + AttachedDevicePath string `mapstructure:"attached_device_path"` + ChrootMounts []string `mapstructure:"chroot_mounts"` + DevicePath string `mapstructure:"device_path"` + MountCommand string `mapstructure:"mount_command"` + MountPath string `mapstructure:"mount_path"` + SourceAmi string `mapstructure:"source_ami"` + UnmountCommand string `mapstructure:"unmount_command"` } type Builder struct { @@ -42,14 +45,36 @@ func (b *Builder) Prepare(raws ...interface{}) error { } // Defaults + if b.config.ChrootMounts == nil { + b.config.ChrootMounts = make([]string, 0) + } + + if len(b.config.ChrootMounts) == 0 { + b.config.ChrootMounts = []string{ + "{{.MountCommand}} -t proc proc {{.MountPath}}/proc", + "{{.MountCommand}} -t sysfs sysfs {{.MountPath}}/sys", + "{{.MountCommand}} -t bind /dev {{.MountPath}}/dev", + "{{.MountCommand}} -t devpts devpts {{.MountPath}}/dev/pts", + "{{.MountCommand}} -t binfmt_misc binfmt_misc {{.MountPath}}/proc/sys/fs/binfmt_misc", + } + } + if b.config.DevicePath == "" { b.config.DevicePath = "/dev/sdh" } + if b.config.MountCommand == "" { + b.config.MountCommand = "mount" + } + if b.config.MountPath == "" { b.config.MountPath = "/var/packer-amazon-chroot/volumes/{{.Device}}" } + if b.config.UnmountCommand == "" { + b.config.UnmountCommand = "umount" + } + // Accumulate any errors errs := common.CheckUnusedConfig(md) errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...) diff --git a/builder/amazon/chroot/step_mount_device.go b/builder/amazon/chroot/step_mount_device.go index 4655c37c9..dee71b456 100644 --- a/builder/amazon/chroot/step_mount_device.go +++ b/builder/amazon/chroot/step_mount_device.go @@ -35,7 +35,8 @@ func (s *StepMountDevice) Run(state map[string]interface{}) multistep.StepAction ui.Say("Mounting the root device...") stderr := new(bytes.Buffer) - cmd := exec.Command("mount", device, mountPath) + mountCommand := fmt.Sprintf("%s %s %s", config.MountCommand, device, mountPath) + cmd := exec.Command("/bin/sh", "-c", mountCommand) cmd.Stderr = stderr if err := cmd.Run(); err != nil { err := fmt.Errorf( @@ -53,16 +54,12 @@ func (s *StepMountDevice) Cleanup(state map[string]interface{}) { return } + config := state["config"].(*Config) ui := state["ui"].(packer.Ui) ui.Say("Unmounting the root device...") - path, err := exec.LookPath("umount") - if err != nil { - ui.Error(fmt.Sprintf("Error umounting root device: %s", err)) - return - } - - cmd := exec.Command(path, s.mountPath) + unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, s.mountPath) + cmd := exec.Command("bin/sh", "-c", unmountCommand) if err := cmd.Run(); err != nil { ui.Error(fmt.Sprintf( "Error unmounting root device: %s", err)) From 710b6a41ec0041c6a08d647a82f167df6b1a3ba5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 11:41:16 -0700 Subject: [PATCH 19/47] builder/amazon/chroot: use the attached device path --- builder/amazon/chroot/builder.go | 4 ++++ builder/amazon/chroot/step_attach_volume.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index cf8848903..ceb4a5c76 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -63,6 +63,10 @@ func (b *Builder) Prepare(raws ...interface{}) error { b.config.DevicePath = "/dev/sdh" } + if b.config.AttachedDevicePath == "" { + b.config.AttachedDevicePath = "/dev/xvdh" + } + if b.config.MountCommand == "" { b.config.MountCommand = "mount" } diff --git a/builder/amazon/chroot/step_attach_volume.go b/builder/amazon/chroot/step_attach_volume.go index cf5f89e3e..69d96988e 100644 --- a/builder/amazon/chroot/step_attach_volume.go +++ b/builder/amazon/chroot/step_attach_volume.go @@ -69,7 +69,7 @@ func (s *StepAttachVolume) Run(state map[string]interface{}) multistep.StepActio return multistep.ActionHalt } - state["device"] = device + state["device"] = config.AttachedDevicePath return multistep.ActionContinue } From 9bb9f02b994e22d899ecd9d40d6971860978c960 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 11:47:30 -0700 Subject: [PATCH 20/47] builder/amazon/chroot: process MountPath template --- builder/amazon/chroot/step_mount_device.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/builder/amazon/chroot/step_mount_device.go b/builder/amazon/chroot/step_mount_device.go index dee71b456..2dcb94945 100644 --- a/builder/amazon/chroot/step_mount_device.go +++ b/builder/amazon/chroot/step_mount_device.go @@ -8,8 +8,14 @@ import ( "log" "os" "os/exec" + "path/filepath" + "text/template" ) +type mountPathData struct { + Device string +} + // StepMountDevice mounts the attached device. // // Produces: @@ -23,7 +29,13 @@ func (s *StepMountDevice) Run(state map[string]interface{}) multistep.StepAction ui := state["ui"].(packer.Ui) device := state["device"].(string) - mountPath := config.MountPath + mountPathRaw := new(bytes.Buffer) + t := template.Must(template.New("mountPath").Parse(config.MountPath)) + t.Execute(mountPathRaw, &mountPathData{ + Device: filepath.Basename(device), + }) + + mountPath := mountPathRaw.String() log.Printf("Mount path: %s", mountPath) if err := os.MkdirAll(mountPath, 0755); err != nil { @@ -46,6 +58,9 @@ func (s *StepMountDevice) Run(state map[string]interface{}) multistep.StepAction return multistep.ActionHalt } + // Set the mount path so we remember to unmount it later + s.mountPath = mountPath + return multistep.ActionContinue } From 462e48cac4e35a576e689758753b7c59779fa358 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 12:08:16 -0700 Subject: [PATCH 21/47] builder/amazon/chroot: mount extra paths --- builder/amazon/chroot/builder.go | 29 ++++---- builder/amazon/chroot/step_mount_device.go | 3 +- builder/amazon/chroot/step_mount_extra.go | 79 ++++++++++++++++++++++ 3 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 builder/amazon/chroot/step_mount_extra.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index ceb4a5c76..d52a14987 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -24,13 +24,13 @@ type Config struct { common.PackerConfig `mapstructure:",squash"` awscommon.AccessConfig `mapstructure:",squash"` - AttachedDevicePath string `mapstructure:"attached_device_path"` - ChrootMounts []string `mapstructure:"chroot_mounts"` - DevicePath string `mapstructure:"device_path"` - MountCommand string `mapstructure:"mount_command"` - MountPath string `mapstructure:"mount_path"` - SourceAmi string `mapstructure:"source_ami"` - UnmountCommand string `mapstructure:"unmount_command"` + AttachedDevicePath string `mapstructure:"attached_device_path"` + ChrootMounts [][]string `mapstructure:"chroot_mounts"` + DevicePath string `mapstructure:"device_path"` + MountCommand string `mapstructure:"mount_command"` + MountPath string `mapstructure:"mount_path"` + SourceAmi string `mapstructure:"source_ami"` + UnmountCommand string `mapstructure:"unmount_command"` } type Builder struct { @@ -46,16 +46,16 @@ func (b *Builder) Prepare(raws ...interface{}) error { // Defaults if b.config.ChrootMounts == nil { - b.config.ChrootMounts = make([]string, 0) + b.config.ChrootMounts = make([][]string, 0) } if len(b.config.ChrootMounts) == 0 { - b.config.ChrootMounts = []string{ - "{{.MountCommand}} -t proc proc {{.MountPath}}/proc", - "{{.MountCommand}} -t sysfs sysfs {{.MountPath}}/sys", - "{{.MountCommand}} -t bind /dev {{.MountPath}}/dev", - "{{.MountCommand}} -t devpts devpts {{.MountPath}}/dev/pts", - "{{.MountCommand}} -t binfmt_misc binfmt_misc {{.MountPath}}/proc/sys/fs/binfmt_misc", + b.config.ChrootMounts = [][]string{ + []string{"proc", "proc", "/proc"}, + []string{"sysfs", "sysfs", "/sys"}, + []string{"bind", "/dev", "/dev"}, + []string{"devpts", "devpts", "/dev/pts"}, + []string{"binfmt_misc", "binfmt_misc", "/proc/sys/fs/binfmt_misc"}, } } @@ -126,6 +126,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepCreateVolume{}, &StepAttachVolume{}, &StepMountDevice{}, + &StepMountExtra{}, } // Run! diff --git a/builder/amazon/chroot/step_mount_device.go b/builder/amazon/chroot/step_mount_device.go index 2dcb94945..0c68986da 100644 --- a/builder/amazon/chroot/step_mount_device.go +++ b/builder/amazon/chroot/step_mount_device.go @@ -32,7 +32,7 @@ func (s *StepMountDevice) Run(state map[string]interface{}) multistep.StepAction mountPathRaw := new(bytes.Buffer) t := template.Must(template.New("mountPath").Parse(config.MountPath)) t.Execute(mountPathRaw, &mountPathData{ - Device: filepath.Basename(device), + Device: filepath.Base(device), }) mountPath := mountPathRaw.String() @@ -60,6 +60,7 @@ func (s *StepMountDevice) Run(state map[string]interface{}) multistep.StepAction // Set the mount path so we remember to unmount it later s.mountPath = mountPath + state["mount_path"] = s.mountPath return multistep.ActionContinue } diff --git a/builder/amazon/chroot/step_mount_extra.go b/builder/amazon/chroot/step_mount_extra.go new file mode 100644 index 000000000..8ce4d3794 --- /dev/null +++ b/builder/amazon/chroot/step_mount_extra.go @@ -0,0 +1,79 @@ +package chroot + +import ( + "bytes" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "os" + "os/exec" +) + +// StepMountExtra mounts the attached device. +// +// Produces: +// mount_path string - The location where the volume was mounted. +type StepMountExtra struct { + mounts []string +} + +func (s *StepMountExtra) Run(state map[string]interface{}) multistep.StepAction { + config := state["config"].(*Config) + mountPath := state["mount_path"].(string) + ui := state["ui"].(packer.Ui) + + s.mounts = make([]string, 0, len(config.ChrootMounts)) + + ui.Say("Mounting additional paths within the chroot...") + for _, mountInfo := range config.ChrootMounts { + innerPath := mountPath + mountInfo[2] + + if err := os.MkdirAll(innerPath, 0755); err != nil { + err := fmt.Errorf("Error creating mount directory: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + ui.Message(fmt.Sprintf("Mounting: %s", mountInfo[2])) + stderr := new(bytes.Buffer) + mountCommand := fmt.Sprintf( + "%s -t %s %s %s", + config.MountCommand, + mountInfo[0], + mountInfo[1], + innerPath) + cmd := exec.Command("/bin/sh", "-c", mountCommand) + cmd.Stderr = stderr + if err := cmd.Run(); err != nil { + err := fmt.Errorf( + "Error mounting: %s\nStderr: %s", err, stderr.String()) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.mounts = append(s.mounts, innerPath) + } + + return multistep.ActionContinue +} + +func (s *StepMountExtra) Cleanup(state map[string]interface{}) { + if s.mounts == nil { + return + } + + config := state["config"].(*Config) + ui := state["ui"].(packer.Ui) + + for _, path := range s.mounts { + unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, path) + cmd := exec.Command("bin/sh", "-c", unmountCommand) + if err := cmd.Run(); err != nil { + ui.Error(fmt.Sprintf( + "Error unmounting root device: %s", err)) + return + } + } +} From bec7b26836d6e317991f27e6cc15d2ea7903a8bb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 12:16:04 -0700 Subject: [PATCH 22/47] builder/amazon/chroot: /bin/sh --- builder/amazon/chroot/step_mount_device.go | 2 +- builder/amazon/chroot/step_mount_extra.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/builder/amazon/chroot/step_mount_device.go b/builder/amazon/chroot/step_mount_device.go index 0c68986da..70c186264 100644 --- a/builder/amazon/chroot/step_mount_device.go +++ b/builder/amazon/chroot/step_mount_device.go @@ -75,7 +75,7 @@ func (s *StepMountDevice) Cleanup(state map[string]interface{}) { ui.Say("Unmounting the root device...") unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, s.mountPath) - cmd := exec.Command("bin/sh", "-c", unmountCommand) + cmd := exec.Command("/bin/sh", "-c", unmountCommand) if err := cmd.Run(); err != nil { ui.Error(fmt.Sprintf( "Error unmounting root device: %s", err)) diff --git a/builder/amazon/chroot/step_mount_extra.go b/builder/amazon/chroot/step_mount_extra.go index 8ce4d3794..be27708d1 100644 --- a/builder/amazon/chroot/step_mount_extra.go +++ b/builder/amazon/chroot/step_mount_extra.go @@ -69,7 +69,7 @@ func (s *StepMountExtra) Cleanup(state map[string]interface{}) { for _, path := range s.mounts { unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, path) - cmd := exec.Command("bin/sh", "-c", unmountCommand) + cmd := exec.Command("/bin/sh", "-c", unmountCommand) if err := cmd.Run(); err != nil { ui.Error(fmt.Sprintf( "Error unmounting root device: %s", err)) From 9dc55ee56c9a9e25a7f7edf42499187bd31c0fc0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 12:18:19 -0700 Subject: [PATCH 23/47] builder/amazon/chroot: special case bind fstype --- builder/amazon/chroot/step_mount_extra.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/builder/amazon/chroot/step_mount_extra.go b/builder/amazon/chroot/step_mount_extra.go index be27708d1..3434059ff 100644 --- a/builder/amazon/chroot/step_mount_extra.go +++ b/builder/amazon/chroot/step_mount_extra.go @@ -35,12 +35,17 @@ func (s *StepMountExtra) Run(state map[string]interface{}) multistep.StepAction return multistep.ActionHalt } + flags := "-t " + mountInfo[0] + if mountInfo[0] == "bind" { + flags = "--bind" + } + ui.Message(fmt.Sprintf("Mounting: %s", mountInfo[2])) stderr := new(bytes.Buffer) mountCommand := fmt.Sprintf( - "%s -t %s %s %s", + "%s %s %s %s", config.MountCommand, - mountInfo[0], + flags, mountInfo[1], innerPath) cmd := exec.Command("/bin/sh", "-c", mountCommand) From 778657e9955b1f1aa0514424ed147b5859f5c114 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 13:21:25 -0700 Subject: [PATCH 24/47] builder/amazon/chroot: unmount in reverse --- builder/amazon/chroot/step_mount_extra.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/builder/amazon/chroot/step_mount_extra.go b/builder/amazon/chroot/step_mount_extra.go index 3434059ff..c491ed9d6 100644 --- a/builder/amazon/chroot/step_mount_extra.go +++ b/builder/amazon/chroot/step_mount_extra.go @@ -72,12 +72,16 @@ func (s *StepMountExtra) Cleanup(state map[string]interface{}) { config := state["config"].(*Config) ui := state["ui"].(packer.Ui) - for _, path := range s.mounts { + for i := len(s.mounts) - 1; i >= 0; i-- { + path := s.mounts[i] unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, path) + + stderr := new(bytes.Buffer) cmd := exec.Command("/bin/sh", "-c", unmountCommand) + cmd.Stderr = stderr if err := cmd.Run(); err != nil { ui.Error(fmt.Sprintf( - "Error unmounting root device: %s", err)) + "Error unmounting device: %s\nStderr: %s", err, stderr.String())) return } } From 9f2399516a0f06f48c21235cfc9f7205a12ca275 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 14:44:03 -0700 Subject: [PATCH 25/47] builder/amazon/chroot: provision within the chroot --- builder/amazon/chroot/communicator.go | 82 +++++++++++++++++++ builder/amazon/chroot/communicator_test.go | 14 ++++ .../amazon/chroot/step_chroot_provision.go | 34 ++++++++ 3 files changed, 130 insertions(+) create mode 100644 builder/amazon/chroot/communicator.go create mode 100644 builder/amazon/chroot/communicator_test.go create mode 100644 builder/amazon/chroot/step_chroot_provision.go diff --git a/builder/amazon/chroot/communicator.go b/builder/amazon/chroot/communicator.go new file mode 100644 index 000000000..6e4387aa0 --- /dev/null +++ b/builder/amazon/chroot/communicator.go @@ -0,0 +1,82 @@ +package chroot + +import ( + "github.com/mitchellh/packer/packer" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "syscall" +) + +// Communicator is a special communicator that works by executing +// commands locally but within a chroot. +type Communicator struct { + Chroot string +} + +func (c *Communicator) Start(cmd *packer.RemoteCmd) error { + chrootCmdPath, err := exec.LookPath("chroot") + if err != nil { + return err + } + + localCmd := exec.Command(chrootCmdPath, c.Chroot, "/bin/sh", "-c", cmd.Command) + localCmd.Stdin = cmd.Stdin + localCmd.Stdout = cmd.Stdout + localCmd.Stderr = cmd.Stderr + log.Printf("Executing: %s %#v", localCmd.Path, localCmd.Args) + if err := localCmd.Start(); err != nil { + return err + } + + go func() { + exitStatus := 0 + if err := localCmd.Wait(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + exitStatus = 1 + + // There is no process-independent way to get the REAL + // exit status so we just try to go deeper. + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + } + } + } + + cmd.SetExited(exitStatus) + }() + + return nil +} + +func (c *Communicator) Upload(dst string, r io.Reader) error { + dst = filepath.Join(c.Chroot, dst) + f, err := os.Open(dst) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.Copy(f, r); err != nil { + return err + } + + return nil +} + +func (c *Communicator) Download(src string, w io.Writer) error { + src = filepath.Join(c.Chroot, src) + f, err := os.Open(src) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.Copy(w, f); err != nil { + return err + } + + return nil +} diff --git a/builder/amazon/chroot/communicator_test.go b/builder/amazon/chroot/communicator_test.go new file mode 100644 index 000000000..56745a681 --- /dev/null +++ b/builder/amazon/chroot/communicator_test.go @@ -0,0 +1,14 @@ +package chroot + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func TestCommunicator_ImplementsCommunicator(t *testing.T) { + var raw interface{} + raw = &Communicator{} + if _, ok := raw.(packer.Communicator); !ok { + t.Fatalf("Communicator should be a communicator") + } +} diff --git a/builder/amazon/chroot/step_chroot_provision.go b/builder/amazon/chroot/step_chroot_provision.go new file mode 100644 index 000000000..a295f27bf --- /dev/null +++ b/builder/amazon/chroot/step_chroot_provision.go @@ -0,0 +1,34 @@ +package chroot + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +// StepChrootProvision provisions the instance within a chroot. +type StepChrootProvision struct { + mounts []string +} + +func (s *StepChrootProvision) Run(state map[string]interface{}) multistep.StepAction { + hook := state["hook"].(packer.Hook) + mountPath := state["mount_path"].(string) + ui := state["ui"].(packer.Ui) + + // Create our communicator + comm := &Communicator{ + Chroot: mountPath, + } + + // Provision + log.Println("Running the provision hook") + if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil { + state["error"] = err + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepChrootProvision) Cleanup(state map[string]interface{}) {} From a3a2ace8432555d0a593fd7d55d80eeb78e6db3f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 14:56:40 -0700 Subject: [PATCH 26/47] builder/amazon/chroot: enable the chroot provisioner --- builder/amazon/chroot/builder.go | 1 + 1 file changed, 1 insertion(+) diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index d52a14987..1bb00eaa3 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -127,6 +127,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepAttachVolume{}, &StepMountDevice{}, &StepMountExtra{}, + &StepChrootProvision{}, } // Run! From 759ff1ace183c3f061ea548d92d6ef0ea7f8a561 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 15:14:21 -0700 Subject: [PATCH 27/47] builder/amazon/chroot: upload should os.Create, not os.Open --- builder/amazon/chroot/communicator.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/builder/amazon/chroot/communicator.go b/builder/amazon/chroot/communicator.go index 6e4387aa0..b27c7c72d 100644 --- a/builder/amazon/chroot/communicator.go +++ b/builder/amazon/chroot/communicator.go @@ -53,7 +53,8 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error { func (c *Communicator) Upload(dst string, r io.Reader) error { dst = filepath.Join(c.Chroot, dst) - f, err := os.Open(dst) + log.Printf("Uploading to chroot dir: %s", dst) + f, err := os.Create(dst) if err != nil { return err } @@ -68,6 +69,7 @@ func (c *Communicator) Upload(dst string, r io.Reader) error { func (c *Communicator) Download(src string, w io.Writer) error { src = filepath.Join(c.Chroot, src) + log.Printf("Downloading from chroot dir: %s", src) f, err := os.Open(src) if err != nil { return err From 998712250c564a217ce030961ecbcb23ac26443b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 15:30:53 -0700 Subject: [PATCH 28/47] builder/amazon/chroot: copyfiles support --- builder/amazon/chroot/builder.go | 10 ++++ builder/amazon/chroot/step_copy_files.go | 70 ++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 builder/amazon/chroot/step_copy_files.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 1bb00eaa3..0e2b098de 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -26,6 +26,7 @@ type Config struct { AttachedDevicePath string `mapstructure:"attached_device_path"` ChrootMounts [][]string `mapstructure:"chroot_mounts"` + CopyFiles []string `mapstructure:"copy_files"` DevicePath string `mapstructure:"device_path"` MountCommand string `mapstructure:"mount_command"` MountPath string `mapstructure:"mount_path"` @@ -49,6 +50,10 @@ func (b *Builder) Prepare(raws ...interface{}) error { b.config.ChrootMounts = make([][]string, 0) } + if b.config.CopyFiles == nil { + b.config.CopyFiles = make([]string, 0) + } + if len(b.config.ChrootMounts) == 0 { b.config.ChrootMounts = [][]string{ []string{"proc", "proc", "/proc"}, @@ -59,6 +64,10 @@ func (b *Builder) Prepare(raws ...interface{}) error { } } + if len(b.config.CopyFiles) == 0 { + b.config.CopyFiles = []string{"/etc/resolv.conf"} + } + if b.config.DevicePath == "" { b.config.DevicePath = "/dev/sdh" } @@ -127,6 +136,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepAttachVolume{}, &StepMountDevice{}, &StepMountExtra{}, + &StepCopyFiles{}, &StepChrootProvision{}, } diff --git a/builder/amazon/chroot/step_copy_files.go b/builder/amazon/chroot/step_copy_files.go new file mode 100644 index 000000000..05f523c13 --- /dev/null +++ b/builder/amazon/chroot/step_copy_files.go @@ -0,0 +1,70 @@ +package chroot + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "io" + "log" + "os" + "path/filepath" +) + +// StepCopyFiles copies some files from the host into the chroot environment. +type StepCopyFiles struct { + mounts []string +} + +func (s *StepCopyFiles) Run(state map[string]interface{}) multistep.StepAction { + config := state["config"].(*Config) + mountPath := state["mount_path"].(string) + ui := state["ui"].(packer.Ui) + + if len(config.CopyFiles) > 0 { + ui.Say("Copying files from host to chroot...") + for _, path := range config.CopyFiles { + ui.Message(path) + chrootPath := filepath.Join(mountPath, path) + log.Printf("Copying '%s' to '%s'", path, chrootPath) + + if err := s.copySingle(chrootPath, path); err != nil { + err := fmt.Errorf("Error copying file: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + } + + return multistep.ActionContinue +} + +func (s *StepCopyFiles) Cleanup(state map[string]interface{}) {} + +func (s *StepCopyFiles) copySingle(dst, src string) error { + srcInfo, err := os.Stat(src) + if err != nil { + return err + } + + srcF, err := os.Open(src) + if err != nil { + return err + } + defer srcF.Close() + + dstF, err := os.Create(dst) + if err != nil { + return err + } + + if _, err := io.Copy(dstF, srcF); err != nil { + return err + } + + if err := os.Chmod(dst, srcInfo.Mode()); err != nil { + return err + } + + return nil +} From f3a4d44066ce6553eb7e50b25f214b91de351992 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 15:55:45 -0700 Subject: [PATCH 29/47] builder/amazon/chroot: make sure to remove files before copy --- builder/amazon/chroot/step_copy_files.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/builder/amazon/chroot/step_copy_files.go b/builder/amazon/chroot/step_copy_files.go index 05f523c13..e1a4878ab 100644 --- a/builder/amazon/chroot/step_copy_files.go +++ b/builder/amazon/chroot/step_copy_files.go @@ -42,11 +42,18 @@ func (s *StepCopyFiles) Run(state map[string]interface{}) multistep.StepAction { func (s *StepCopyFiles) Cleanup(state map[string]interface{}) {} func (s *StepCopyFiles) copySingle(dst, src string) error { + // Stat the src file so we can copy the mode later srcInfo, err := os.Stat(src) if err != nil { return err } + // Remove any existing destination file + if err := os.Remove(dst); err != nil { + return err + } + + // Copy the files srcF, err := os.Open(src) if err != nil { return err @@ -57,11 +64,14 @@ func (s *StepCopyFiles) copySingle(dst, src string) error { if err != nil { return err } + defer dstF.Close() if _, err := io.Copy(dstF, srcF); err != nil { return err } + dstF.Close() + // Match the mode if err := os.Chmod(dst, srcInfo.Mode()); err != nil { return err } From f79f113bccf318ef53203a1ae87fc3ce48300cc0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 16:08:45 -0700 Subject: [PATCH 30/47] plugin/builder-amazon-chroot: add test file --- plugin/builder-amazon-chroot/main_test.go | 1 + 1 file changed, 1 insertion(+) create mode 100644 plugin/builder-amazon-chroot/main_test.go diff --git a/plugin/builder-amazon-chroot/main_test.go b/plugin/builder-amazon-chroot/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/builder-amazon-chroot/main_test.go @@ -0,0 +1 @@ +package main From 44c6103fd0a524ad42a8b046f4094f3eb9152a97 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 16:41:29 -0700 Subject: [PATCH 31/47] builder/amazon/chroot: perform early cleanup --- builder/amazon/chroot/builder.go | 5 +++ builder/amazon/chroot/step_attach_volume.go | 21 +++++++++--- builder/amazon/chroot/step_copy_files.go | 31 +++++++++++++++-- builder/amazon/chroot/step_early_cleanup.go | 37 +++++++++++++++++++++ builder/amazon/chroot/step_mount_device.go | 18 +++++++--- builder/amazon/chroot/step_mount_extra.go | 30 ++++++++++++----- 6 files changed, 122 insertions(+), 20 deletions(-) create mode 100644 builder/amazon/chroot/step_early_cleanup.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 0e2b098de..d8665881b 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -18,6 +18,10 @@ import ( // The unique ID for this builder const BuilderId = "mitchellh.amazon.chroot" +// CleanupFunc is a type that is strung throughout the state bag in +// order to perform cleanup at earlier points. +type CleanupFunc func(map[string]interface{}) error + // Config is the configuration that is chained through the steps and // settable from the template. type Config struct { @@ -138,6 +142,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepMountExtra{}, &StepCopyFiles{}, &StepChrootProvision{}, + &StepEarlyCleanup{}, } // Run! diff --git a/builder/amazon/chroot/step_attach_volume.go b/builder/amazon/chroot/step_attach_volume.go index 69d96988e..9ab3b92f2 100644 --- a/builder/amazon/chroot/step_attach_volume.go +++ b/builder/amazon/chroot/step_attach_volume.go @@ -14,6 +14,7 @@ import ( // // Produces: // device string - The location where the volume was attached. +// attach_cleanup CleanupFunc type StepAttachVolume struct { attached bool volumeId string @@ -70,12 +71,20 @@ func (s *StepAttachVolume) Run(state map[string]interface{}) multistep.StepActio } state["device"] = config.AttachedDevicePath + state["attach_cleanup"] = s.CleanupFunc return multistep.ActionContinue } func (s *StepAttachVolume) Cleanup(state map[string]interface{}) { + ui := state["ui"].(packer.Ui) + if err := s.CleanupFunc(state); err != nil { + ui.Error(err.Error()) + } +} + +func (s *StepAttachVolume) CleanupFunc(state map[string]interface{}) error { if !s.attached { - return + return nil } ec2conn := state["ec2"].(*ec2.EC2) @@ -84,10 +93,11 @@ func (s *StepAttachVolume) Cleanup(state map[string]interface{}) { ui.Say("Detaching EBS volume...") _, err := ec2conn.DetachVolume(s.volumeId) if err != nil { - ui.Error(fmt.Sprintf("Error detaching EBS volume: %s", err)) - return + return fmt.Errorf("Error detaching EBS volume: %s", err) } + s.attached = false + // Wait for the volume to detach stateChange := awscommon.StateChangeConf{ Conn: ec2conn, @@ -111,7 +121,8 @@ func (s *StepAttachVolume) Cleanup(state map[string]interface{}) { _, err = awscommon.WaitForState(&stateChange) if err != nil { - ui.Error(fmt.Sprintf("Error waiting for volume: %s", err)) - return + return fmt.Errorf("Error waiting for volume: %s", err) } + + return nil } diff --git a/builder/amazon/chroot/step_copy_files.go b/builder/amazon/chroot/step_copy_files.go index e1a4878ab..432a3b28e 100644 --- a/builder/amazon/chroot/step_copy_files.go +++ b/builder/amazon/chroot/step_copy_files.go @@ -11,8 +11,12 @@ import ( ) // StepCopyFiles copies some files from the host into the chroot environment. +// +// Produces: +// copy_files_cleanup CleanupFunc - A function to clean up the copied files +// early. type StepCopyFiles struct { - mounts []string + files []string } func (s *StepCopyFiles) Run(state map[string]interface{}) multistep.StepAction { @@ -20,6 +24,7 @@ func (s *StepCopyFiles) Run(state map[string]interface{}) multistep.StepAction { mountPath := state["mount_path"].(string) ui := state["ui"].(packer.Ui) + s.files = make([]string, len(config.CopyFiles)) if len(config.CopyFiles) > 0 { ui.Say("Copying files from host to chroot...") for _, path := range config.CopyFiles { @@ -33,13 +38,35 @@ func (s *StepCopyFiles) Run(state map[string]interface{}) multistep.StepAction { ui.Error(err.Error()) return multistep.ActionHalt } + + s.files = append(s.files, chrootPath) } } + state["copy_files_cleanup"] = s.CleanupFunc return multistep.ActionContinue } -func (s *StepCopyFiles) Cleanup(state map[string]interface{}) {} +func (s *StepCopyFiles) Cleanup(state map[string]interface{}) { + ui := state["ui"].(packer.Ui) + if err := s.CleanupFunc(state); err != nil { + ui.Error(err.Error()) + } +} + +func (s *StepCopyFiles) CleanupFunc(map[string]interface{}) error { + if s.files != nil { + for _, file := range s.files { + log.Printf("Removing: %s", file) + if err := os.Remove(file); err != nil { + return err + } + } + } + + s.files = nil + return nil +} func (s *StepCopyFiles) copySingle(dst, src string) error { // Stat the src file so we can copy the mode later diff --git a/builder/amazon/chroot/step_early_cleanup.go b/builder/amazon/chroot/step_early_cleanup.go new file mode 100644 index 000000000..421e7ba27 --- /dev/null +++ b/builder/amazon/chroot/step_early_cleanup.go @@ -0,0 +1,37 @@ +package chroot + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +// StepEarlyCleanup performs some of the cleanup steps early in order to +// prepare for snapshotting and creating an AMI. +type StepEarlyCleanup struct{} + +func (s *StepEarlyCleanup) Run(state map[string]interface{}) multistep.StepAction { + ui := state["ui"].(packer.Ui) + cleanupKeys := []string{ + "copy_files_cleanup", + "mount_extra_cleanup", + "mount_device_cleanup", + "attach_cleanup", + } + + for _, key := range cleanupKeys { + f := state[key].(CleanupFunc) + log.Printf("Running cleanup func: %s", key) + if err := f(state); err != nil { + err := fmt.Errorf("Error cleaning up: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + return multistep.ActionContinue +} + +func (s *StepEarlyCleanup) Cleanup(state map[string]interface{}) {} diff --git a/builder/amazon/chroot/step_mount_device.go b/builder/amazon/chroot/step_mount_device.go index 70c186264..b5733eefd 100644 --- a/builder/amazon/chroot/step_mount_device.go +++ b/builder/amazon/chroot/step_mount_device.go @@ -20,6 +20,7 @@ type mountPathData struct { // // Produces: // mount_path string - The location where the volume was mounted. +// mount_device_cleanup CleanupFunc - To perform early cleanup type StepMountDevice struct { mountPath string } @@ -61,13 +62,21 @@ func (s *StepMountDevice) Run(state map[string]interface{}) multistep.StepAction // Set the mount path so we remember to unmount it later s.mountPath = mountPath state["mount_path"] = s.mountPath + state["mount_device_cleanup"] = s.CleanupFunc return multistep.ActionContinue } func (s *StepMountDevice) Cleanup(state map[string]interface{}) { + ui := state["ui"].(packer.Ui) + if err := s.CleanupFunc(state); err != nil { + ui.Error(err.Error()) + } +} + +func (s *StepMountDevice) CleanupFunc(state map[string]interface{}) error { if s.mountPath == "" { - return + return nil } config := state["config"].(*Config) @@ -77,8 +86,9 @@ func (s *StepMountDevice) Cleanup(state map[string]interface{}) { unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, s.mountPath) cmd := exec.Command("/bin/sh", "-c", unmountCommand) if err := cmd.Run(); err != nil { - ui.Error(fmt.Sprintf( - "Error unmounting root device: %s", err)) - return + return fmt.Errorf("Error unmounting root device: %s", err) } + + s.mountPath = "" + return nil } diff --git a/builder/amazon/chroot/step_mount_extra.go b/builder/amazon/chroot/step_mount_extra.go index c491ed9d6..165804bea 100644 --- a/builder/amazon/chroot/step_mount_extra.go +++ b/builder/amazon/chroot/step_mount_extra.go @@ -12,7 +12,7 @@ import ( // StepMountExtra mounts the attached device. // // Produces: -// mount_path string - The location where the volume was mounted. +// mount_extra_cleanup CleanupFunc - To perform early cleanup type StepMountExtra struct { mounts []string } @@ -61,28 +61,40 @@ func (s *StepMountExtra) Run(state map[string]interface{}) multistep.StepAction s.mounts = append(s.mounts, innerPath) } + state["mount_extra_cleanup"] = s.CleanupFunc return multistep.ActionContinue } func (s *StepMountExtra) Cleanup(state map[string]interface{}) { - if s.mounts == nil { + ui := state["ui"].(packer.Ui) + + if err := s.CleanupFunc(state); err != nil { + ui.Error(err.Error()) return } +} + +func (s *StepMountExtra) CleanupFunc(state map[string]interface{}) error { + if s.mounts == nil { + return nil + } config := state["config"].(*Config) - ui := state["ui"].(packer.Ui) - - for i := len(s.mounts) - 1; i >= 0; i-- { - path := s.mounts[i] + for len(s.mounts) > 0 { + var path string + lastIndex := len(s.mounts) - 1 + path, s.mounts = s.mounts[lastIndex], s.mounts[:lastIndex] unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, path) stderr := new(bytes.Buffer) cmd := exec.Command("/bin/sh", "-c", unmountCommand) cmd.Stderr = stderr if err := cmd.Run(); err != nil { - ui.Error(fmt.Sprintf( - "Error unmounting device: %s\nStderr: %s", err, stderr.String())) - return + return fmt.Errorf( + "Error unmounting device: %s\nStderr: %s", err, stderr.String()) } } + + s.mounts = nil + return nil } From 36be9a9bf653a5364661d5e0037da7b4efccf784 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 16:45:49 -0700 Subject: [PATCH 32/47] builder/amazon/chroot: initial len should be 0 so we don't have empty --- builder/amazon/chroot/step_copy_files.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/amazon/chroot/step_copy_files.go b/builder/amazon/chroot/step_copy_files.go index 432a3b28e..eab0ee6e8 100644 --- a/builder/amazon/chroot/step_copy_files.go +++ b/builder/amazon/chroot/step_copy_files.go @@ -24,7 +24,7 @@ func (s *StepCopyFiles) Run(state map[string]interface{}) multistep.StepAction { mountPath := state["mount_path"].(string) ui := state["ui"].(packer.Ui) - s.files = make([]string, len(config.CopyFiles)) + s.files = make([]string, 0, len(config.CopyFiles)) if len(config.CopyFiles) > 0 { ui.Say("Copying files from host to chroot...") for _, path := range config.CopyFiles { From 7ab449073397026a7227bd79034da160624fee3d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 16:58:58 -0700 Subject: [PATCH 33/47] builder/amazon/chroot: step to snapshot the root image --- builder/amazon/chroot/builder.go | 1 + builder/amazon/chroot/step_snapshot.go | 87 ++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 builder/amazon/chroot/step_snapshot.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index d8665881b..840ac1561 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -143,6 +143,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepCopyFiles{}, &StepChrootProvision{}, &StepEarlyCleanup{}, + &StepSnapshot{}, } // Run! diff --git a/builder/amazon/chroot/step_snapshot.go b/builder/amazon/chroot/step_snapshot.go new file mode 100644 index 000000000..837543ec8 --- /dev/null +++ b/builder/amazon/chroot/step_snapshot.go @@ -0,0 +1,87 @@ +package chroot + +import ( + "errors" + "fmt" + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/multistep" + awscommon "github.com/mitchellh/packer/builder/amazon/common" + "github.com/mitchellh/packer/packer" +) + +// StepSnapshot creates a snapshot of the created volume. +// +// Produces: +// snapshot_id string - ID of the created snapshot +type StepSnapshot struct { + snapshotId string +} + +func (s *StepSnapshot) Run(state map[string]interface{}) multistep.StepAction { + ec2conn := state["ec2"].(*ec2.EC2) + ui := state["ui"].(packer.Ui) + volumeId := state["volume_id"].(string) + + ui.Say("Creating snapshot...") + createSnapResp, err := ec2conn.CreateSnapshot(volumeId, "") + if err != nil { + err := fmt.Errorf("Error creating snapshot: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Set the snapshot ID so we can delete it later + s.snapshotId = createSnapResp.Id + ui.Message(fmt.Sprintf("Snapshot ID: %s", s.snapshotId)) + + // Wait for the snapshot to be ready + stateChange := awscommon.StateChangeConf{ + Conn: ec2conn, + Pending: []string{"pending"}, + StepState: state, + Target: "completed", + Refresh: func() (interface{}, string, error) { + resp, err := ec2conn.Snapshots([]string{s.snapshotId}, ec2.NewFilter()) + if err != nil { + return nil, "", err + } + + if len(resp.Snapshots) == 0 { + return nil, "", errors.New("No snapshots found.") + } + + return nil, resp.Snapshots[0].Status, nil + }, + } + + _, err = awscommon.WaitForState(&stateChange) + if err != nil { + err := fmt.Errorf("Error waiting for snapshot: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + state["snapshot_id"] = s.snapshotId + return multistep.ActionContinue +} + +func (s *StepSnapshot) Cleanup(state map[string]interface{}) { + if s.snapshotId == "" { + return + } + + _, cancelled := state[multistep.StateCancelled] + _, halted := state[multistep.StateHalted] + + if cancelled || halted { + ec2conn := state["ec2"].(*ec2.EC2) + ui := state["ui"].(packer.Ui) + ui.Say("Removing snapshot since we cancelled or halted...") + _, err := ec2conn.DeleteSnapshots([]string{s.snapshotId}) + if err != nil { + ui.Error(fmt.Sprintf("Error: %s", err)) + } + } +} From 613322d98bdee8e2986addb300a5c27ad27ebde4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 17:06:22 -0700 Subject: [PATCH 34/47] communicator/ssh: log when remote exits --- communicator/ssh/communicator.go | 1 + 1 file changed, 1 insertion(+) diff --git a/communicator/ssh/communicator.go b/communicator/ssh/communicator.go index 3af09e1d0..6f729e9f6 100644 --- a/communicator/ssh/communicator.go +++ b/communicator/ssh/communicator.go @@ -87,6 +87,7 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) { } } + log.Printf("remote command exited with '%d': %s", exitStatus, cmd.Command) cmd.SetExited(exitStatus) }() From df4c844493f665b47a63d8adb9b234c905d3c27f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 17:23:37 -0700 Subject: [PATCH 35/47] builder/amazon/chroot: log the exit code for the chroot communicator --- builder/amazon/chroot/communicator.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/builder/amazon/chroot/communicator.go b/builder/amazon/chroot/communicator.go index b27c7c72d..e0bc5613f 100644 --- a/builder/amazon/chroot/communicator.go +++ b/builder/amazon/chroot/communicator.go @@ -45,6 +45,9 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error { } } + log.Printf( + "Chroot executation ended with '%d': '%s'", + exitStatus, cmd.Command) cmd.SetExited(exitStatus) }() From 21002e04a181b04d0e13c321bd6c5848eb6846cf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 17:32:41 -0700 Subject: [PATCH 36/47] builder/amazon/common: correct the log statement --- builder/amazon/common/instance.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/amazon/common/instance.go b/builder/amazon/common/instance.go index 12c033d66..1ea763e54 100644 --- a/builder/amazon/common/instance.go +++ b/builder/amazon/common/instance.go @@ -30,7 +30,7 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) func() (interface{ } func WaitForState(conf *StateChangeConf) (i interface{}, err error) { - log.Printf("Waiting for instance state to become: %s", conf.Target) + log.Printf("Waiting for state to become: %s", conf.Target) for { var currentState string From 6f4db324189deb5cf20038878f96c23e82cf1944 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 17:56:42 -0700 Subject: [PATCH 37/47] builder/amazon/chroot: switch func type to interface Was getting weird behavior... see https://groups.google.com/d/msg/golang-nuts/a1kymwSVt2M/FwcCuBl1_48 --- builder/amazon/chroot/builder.go | 4 ---- builder/amazon/chroot/cleanup.go | 6 ++++++ builder/amazon/chroot/step_attach_volume.go | 2 +- builder/amazon/chroot/step_attach_volume_test.go | 11 +++++++++++ builder/amazon/chroot/step_copy_files.go | 2 +- builder/amazon/chroot/step_copy_files_test.go | 11 +++++++++++ builder/amazon/chroot/step_early_cleanup.go | 4 ++-- builder/amazon/chroot/step_mount_device.go | 2 +- builder/amazon/chroot/step_mount_device_test.go | 11 +++++++++++ builder/amazon/chroot/step_mount_extra.go | 2 +- builder/amazon/chroot/step_mount_extra_test.go | 11 +++++++++++ 11 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 builder/amazon/chroot/cleanup.go create mode 100644 builder/amazon/chroot/step_attach_volume_test.go create mode 100644 builder/amazon/chroot/step_copy_files_test.go create mode 100644 builder/amazon/chroot/step_mount_device_test.go create mode 100644 builder/amazon/chroot/step_mount_extra_test.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 840ac1561..82338cb2b 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -18,10 +18,6 @@ import ( // The unique ID for this builder const BuilderId = "mitchellh.amazon.chroot" -// CleanupFunc is a type that is strung throughout the state bag in -// order to perform cleanup at earlier points. -type CleanupFunc func(map[string]interface{}) error - // Config is the configuration that is chained through the steps and // settable from the template. type Config struct { diff --git a/builder/amazon/chroot/cleanup.go b/builder/amazon/chroot/cleanup.go new file mode 100644 index 000000000..14c47aac3 --- /dev/null +++ b/builder/amazon/chroot/cleanup.go @@ -0,0 +1,6 @@ +package chroot + +// Cleanup is an interface that some steps implement for early cleanup. +type Cleanup interface { + CleanupFunc(map[string]interface{}) error +} diff --git a/builder/amazon/chroot/step_attach_volume.go b/builder/amazon/chroot/step_attach_volume.go index 9ab3b92f2..0617adca0 100644 --- a/builder/amazon/chroot/step_attach_volume.go +++ b/builder/amazon/chroot/step_attach_volume.go @@ -71,7 +71,7 @@ func (s *StepAttachVolume) Run(state map[string]interface{}) multistep.StepActio } state["device"] = config.AttachedDevicePath - state["attach_cleanup"] = s.CleanupFunc + state["attach_cleanup"] = s return multistep.ActionContinue } diff --git a/builder/amazon/chroot/step_attach_volume_test.go b/builder/amazon/chroot/step_attach_volume_test.go new file mode 100644 index 000000000..63f629b77 --- /dev/null +++ b/builder/amazon/chroot/step_attach_volume_test.go @@ -0,0 +1,11 @@ +package chroot + +import "testing" + +func TestAttachVolumeCleanupFunc_ImplementsCleanupFunc(t *testing.T) { + var raw interface{} + raw = new(StepAttachVolume) + if _, ok := raw.(Cleanup); !ok { + t.Fatalf("cleanup func should be a CleanupFunc") + } +} diff --git a/builder/amazon/chroot/step_copy_files.go b/builder/amazon/chroot/step_copy_files.go index eab0ee6e8..45ec09db3 100644 --- a/builder/amazon/chroot/step_copy_files.go +++ b/builder/amazon/chroot/step_copy_files.go @@ -43,7 +43,7 @@ func (s *StepCopyFiles) Run(state map[string]interface{}) multistep.StepAction { } } - state["copy_files_cleanup"] = s.CleanupFunc + state["copy_files_cleanup"] = s return multistep.ActionContinue } diff --git a/builder/amazon/chroot/step_copy_files_test.go b/builder/amazon/chroot/step_copy_files_test.go new file mode 100644 index 000000000..281613e6f --- /dev/null +++ b/builder/amazon/chroot/step_copy_files_test.go @@ -0,0 +1,11 @@ +package chroot + +import "testing" + +func TestCopyFilesCleanupFunc_ImplementsCleanupFunc(t *testing.T) { + var raw interface{} + raw = new(StepCopyFiles) + if _, ok := raw.(Cleanup); !ok { + t.Fatalf("cleanup func should be a CleanupFunc") + } +} diff --git a/builder/amazon/chroot/step_early_cleanup.go b/builder/amazon/chroot/step_early_cleanup.go index 421e7ba27..01ccb48d7 100644 --- a/builder/amazon/chroot/step_early_cleanup.go +++ b/builder/amazon/chroot/step_early_cleanup.go @@ -21,9 +21,9 @@ func (s *StepEarlyCleanup) Run(state map[string]interface{}) multistep.StepActio } for _, key := range cleanupKeys { - f := state[key].(CleanupFunc) + c := state[key].(Cleanup) log.Printf("Running cleanup func: %s", key) - if err := f(state); err != nil { + if err := c.CleanupFunc(state); err != nil { err := fmt.Errorf("Error cleaning up: %s", err) state["error"] = err ui.Error(err.Error()) diff --git a/builder/amazon/chroot/step_mount_device.go b/builder/amazon/chroot/step_mount_device.go index b5733eefd..9dc4f1743 100644 --- a/builder/amazon/chroot/step_mount_device.go +++ b/builder/amazon/chroot/step_mount_device.go @@ -62,7 +62,7 @@ func (s *StepMountDevice) Run(state map[string]interface{}) multistep.StepAction // Set the mount path so we remember to unmount it later s.mountPath = mountPath state["mount_path"] = s.mountPath - state["mount_device_cleanup"] = s.CleanupFunc + state["mount_device_cleanup"] = s return multistep.ActionContinue } diff --git a/builder/amazon/chroot/step_mount_device_test.go b/builder/amazon/chroot/step_mount_device_test.go new file mode 100644 index 000000000..2eeb850eb --- /dev/null +++ b/builder/amazon/chroot/step_mount_device_test.go @@ -0,0 +1,11 @@ +package chroot + +import "testing" + +func TestMountDeviceCleanupFunc_ImplementsCleanupFunc(t *testing.T) { + var raw interface{} + raw = new(StepMountDevice) + if _, ok := raw.(Cleanup); !ok { + t.Fatalf("cleanup func should be a CleanupFunc") + } +} diff --git a/builder/amazon/chroot/step_mount_extra.go b/builder/amazon/chroot/step_mount_extra.go index 165804bea..6a306fdf9 100644 --- a/builder/amazon/chroot/step_mount_extra.go +++ b/builder/amazon/chroot/step_mount_extra.go @@ -61,7 +61,7 @@ func (s *StepMountExtra) Run(state map[string]interface{}) multistep.StepAction s.mounts = append(s.mounts, innerPath) } - state["mount_extra_cleanup"] = s.CleanupFunc + state["mount_extra_cleanup"] = s return multistep.ActionContinue } diff --git a/builder/amazon/chroot/step_mount_extra_test.go b/builder/amazon/chroot/step_mount_extra_test.go new file mode 100644 index 000000000..d53cc7056 --- /dev/null +++ b/builder/amazon/chroot/step_mount_extra_test.go @@ -0,0 +1,11 @@ +package chroot + +import "testing" + +func TestMountExtraCleanupFunc_ImplementsCleanupFunc(t *testing.T) { + var raw interface{} + raw = new(StepMountExtra) + if _, ok := raw.(Cleanup); !ok { + t.Fatalf("cleanup func should be a CleanupFunc") + } +} From 089d284067ebce9125554d52440430e0582e78ff Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 18:28:21 -0700 Subject: [PATCH 38/47] builder/amazon/chroot: register AMI --- builder/amazon/chroot/builder.go | 1 + builder/amazon/chroot/step_register_ami.go | 67 ++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 builder/amazon/chroot/step_register_ami.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 82338cb2b..75dcaed3b 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -140,6 +140,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepChrootProvision{}, &StepEarlyCleanup{}, &StepSnapshot{}, + &StepRegisterAMI{}, } // Run! diff --git a/builder/amazon/chroot/step_register_ami.go b/builder/amazon/chroot/step_register_ami.go new file mode 100644 index 000000000..9ec70f038 --- /dev/null +++ b/builder/amazon/chroot/step_register_ami.go @@ -0,0 +1,67 @@ +package chroot + +import ( + "fmt" + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/multistep" + awscommon "github.com/mitchellh/packer/builder/amazon/common" + "github.com/mitchellh/packer/packer" +) + +// StepRegisterAMI creates the AMI. +type StepRegisterAMI struct{} + +func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction { + ec2conn := state["ec2"].(*ec2.EC2) + image := state["source_image"].(*ec2.Image) + snapshotId := state["snapshot_id"].(string) + ui := state["ui"].(packer.Ui) + + amiName := "foo" + + ui.Say("Registering the AMI...") + blockDevices := make([]ec2.BlockDeviceMapping, len(image.BlockDevices)) + for i, device := range image.BlockDevices { + newDevice := device + if newDevice.DeviceName == image.RootDeviceName { + newDevice.SnapshotId = snapshotId + } + + blockDevices[i] = newDevice + } + + registerOpts := &ec2.RegisterImage{ + Name: amiName, + Architecture: image.Architecture, + KernelId: image.KernelId, + RamdiskId: image.RamdiskId, + RootDeviceName: image.RootDeviceName, + BlockDevices: blockDevices, + } + + registerResp, err := ec2conn.RegisterImage(registerOpts) + if err != nil { + state["error"] = fmt.Errorf("Error registering AMI: %s", err) + ui.Error(state["error"].(error).Error()) + return multistep.ActionHalt + } + + // Set the AMI ID in the state + ui.Say(fmt.Sprintf("AMI: %s", registerResp.ImageId)) + amis := make(map[string]string) + amis[ec2conn.Region.Name] = registerResp.ImageId + state["amis"] = amis + + // Wait for the image to become ready + ui.Say("Waiting for AMI to become ready...") + if err := awscommon.WaitForAMI(ec2conn, registerResp.ImageId); err != nil { + err := fmt.Errorf("Error waiting for AMI: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepRegisterAMI) Cleanup(state map[string]interface{}) {} From 6b7f59216e9a13ae0161437845238a690659ab44 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 20:47:47 -0700 Subject: [PATCH 39/47] builder/amazon/chroot: get rid of AttachedDevicePath --- builder/amazon/chroot/builder.go | 6 +----- builder/amazon/chroot/step_attach_volume.go | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 75dcaed3b..aa598a5e1 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -24,10 +24,10 @@ type Config struct { common.PackerConfig `mapstructure:",squash"` awscommon.AccessConfig `mapstructure:",squash"` - AttachedDevicePath string `mapstructure:"attached_device_path"` ChrootMounts [][]string `mapstructure:"chroot_mounts"` CopyFiles []string `mapstructure:"copy_files"` DevicePath string `mapstructure:"device_path"` + DevicePrefix string `mapstructure:"device_prefix"` MountCommand string `mapstructure:"mount_command"` MountPath string `mapstructure:"mount_path"` SourceAmi string `mapstructure:"source_ami"` @@ -72,10 +72,6 @@ func (b *Builder) Prepare(raws ...interface{}) error { b.config.DevicePath = "/dev/sdh" } - if b.config.AttachedDevicePath == "" { - b.config.AttachedDevicePath = "/dev/xvdh" - } - if b.config.MountCommand == "" { b.config.MountCommand = "mount" } diff --git a/builder/amazon/chroot/step_attach_volume.go b/builder/amazon/chroot/step_attach_volume.go index 0617adca0..5b9b2a5ba 100644 --- a/builder/amazon/chroot/step_attach_volume.go +++ b/builder/amazon/chroot/step_attach_volume.go @@ -70,7 +70,7 @@ func (s *StepAttachVolume) Run(state map[string]interface{}) multistep.StepActio return multistep.ActionHalt } - state["device"] = config.AttachedDevicePath + state["device"] = device state["attach_cleanup"] = s return multistep.ActionContinue } From 997b81da21a293fc56eee8ae40538b360b7cc3c9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 21:19:57 -0700 Subject: [PATCH 40/47] builder/amazon/chroot: find available device --- builder/amazon/chroot/builder.go | 20 +++---- builder/amazon/chroot/device.go | 61 ++++++++++++++++++++ builder/amazon/chroot/step_attach_volume.go | 11 ++-- builder/amazon/chroot/step_prepare_device.go | 45 +++++++++++++++ 4 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 builder/amazon/chroot/device.go create mode 100644 builder/amazon/chroot/step_prepare_device.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index aa598a5e1..1641cde57 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -24,14 +24,13 @@ type Config struct { common.PackerConfig `mapstructure:",squash"` awscommon.AccessConfig `mapstructure:",squash"` - ChrootMounts [][]string `mapstructure:"chroot_mounts"` - CopyFiles []string `mapstructure:"copy_files"` - DevicePath string `mapstructure:"device_path"` - DevicePrefix string `mapstructure:"device_prefix"` - MountCommand string `mapstructure:"mount_command"` - MountPath string `mapstructure:"mount_path"` - SourceAmi string `mapstructure:"source_ami"` - UnmountCommand string `mapstructure:"unmount_command"` + ChrootMounts [][]string `mapstructure:"chroot_mounts"` + CopyFiles []string `mapstructure:"copy_files"` + DevicePath string `mapstructure:"device_path"` + MountCommand string `mapstructure:"mount_command"` + MountPath string `mapstructure:"mount_path"` + SourceAmi string `mapstructure:"source_ami"` + UnmountCommand string `mapstructure:"unmount_command"` } type Builder struct { @@ -68,10 +67,6 @@ func (b *Builder) Prepare(raws ...interface{}) error { b.config.CopyFiles = []string{"/etc/resolv.conf"} } - if b.config.DevicePath == "" { - b.config.DevicePath = "/dev/sdh" - } - if b.config.MountCommand == "" { b.config.MountCommand = "mount" } @@ -128,6 +123,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe steps := []multistep.Step{ &StepInstanceInfo{}, &StepSourceAMIInfo{}, + &StepPrepareDevice{}, &StepCreateVolume{}, &StepAttachVolume{}, &StepMountDevice{}, diff --git a/builder/amazon/chroot/device.go b/builder/amazon/chroot/device.go new file mode 100644 index 000000000..ed5be6194 --- /dev/null +++ b/builder/amazon/chroot/device.go @@ -0,0 +1,61 @@ +package chroot + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +// AvailableDevice finds an available device and returns it. Note that +// you should externally hold a flock or something in order to guarantee +// that this device is available across processes. +func AvailableDevice() (string, error) { + prefix, err := devicePrefix() + if err != nil { + return "", err + } + + letters := "fghijklmnop" + for _, letter := range letters { + for i := 1; i < 16; i++ { + device := fmt.Sprintf("/dev/%s%c%d", prefix, letter, i) + if _, err := os.Stat(device); err != nil { + return device, nil + } + } + } + + return "", errors.New("available device could not be found") +} + +// devicePrefix returns the prefix ("sd" or "xvd" or so on) of the devices +// on the system. +func devicePrefix() (string, error) { + available := []string{"sd", "xvd"} + + f, err := os.Open("/sys/block") + if err != nil { + return "", err + } + defer f.Close() + + dirs, err := f.Readdirnames(-1) + if dirs != nil && len(dirs) > 0 { + for _, dir := range dirs { + dirBase := filepath.Base(dir) + for _, prefix := range available { + if strings.HasPrefix(dirBase, prefix) { + return prefix, nil + } + } + } + } + + if err != nil { + return "", err + } + + return "", errors.New("device prefix could not be detected") +} diff --git a/builder/amazon/chroot/step_attach_volume.go b/builder/amazon/chroot/step_attach_volume.go index 5b9b2a5ba..051c0704c 100644 --- a/builder/amazon/chroot/step_attach_volume.go +++ b/builder/amazon/chroot/step_attach_volume.go @@ -7,6 +7,7 @@ import ( "github.com/mitchellh/multistep" awscommon "github.com/mitchellh/packer/builder/amazon/common" "github.com/mitchellh/packer/packer" + "strings" ) // StepAttachVolume attaches the previously created volume to an @@ -21,16 +22,17 @@ type StepAttachVolume struct { } func (s *StepAttachVolume) Run(state map[string]interface{}) multistep.StepAction { - config := state["config"].(*Config) ec2conn := state["ec2"].(*ec2.EC2) + device := state["device"].(string) instance := state["instance"].(*ec2.Instance) ui := state["ui"].(packer.Ui) volumeId := state["volume_id"].(string) - device := config.DevicePath + // For the API call, it expects "sd" prefixed devices. + attachVolume := strings.Replace(device, "/xvd", "/sd", 1) - ui.Say("Attaching the root volume...") - _, err := ec2conn.AttachVolume(volumeId, instance.InstanceId, device) + ui.Say(fmt.Sprintf("Attaching the root volume to %s", attachVolume)) + _, err := ec2conn.AttachVolume(volumeId, instance.InstanceId, attachVolume) if err != nil { err := fmt.Errorf("Error attaching volume: %s", err) state["error"] = err @@ -70,7 +72,6 @@ func (s *StepAttachVolume) Run(state map[string]interface{}) multistep.StepActio return multistep.ActionHalt } - state["device"] = device state["attach_cleanup"] = s return multistep.ActionContinue } diff --git a/builder/amazon/chroot/step_prepare_device.go b/builder/amazon/chroot/step_prepare_device.go new file mode 100644 index 000000000..114c0ce9d --- /dev/null +++ b/builder/amazon/chroot/step_prepare_device.go @@ -0,0 +1,45 @@ +package chroot + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "os" +) + +// StepPrepareDevice finds an available device and sets it. +type StepPrepareDevice struct { + mounts []string +} + +func (s *StepPrepareDevice) Run(state map[string]interface{}) multistep.StepAction { + config := state["config"].(*Config) + ui := state["ui"].(packer.Ui) + + device := config.DevicePath + if device == "" { + var err error + log.Println("Device path not specified, searching for available device...") + device, err = AvailableDevice() + if err != nil { + err := fmt.Errorf("Error finding available device: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + if _, err := os.Stat(device); err == nil { + err := fmt.Errorf("Device is in use: %s", device) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + log.Printf("Device: %s", device) + state["device"] = device + return multistep.ActionContinue +} + +func (s *StepPrepareDevice) Cleanup(state map[string]interface{}) {} From 167bdd9a464b0f8d9b0327c5287c1161c9a49596 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 21:48:37 -0700 Subject: [PATCH 41/47] builder/amazon/chroot: flock so that device searching is safe --- builder/amazon/chroot/builder.go | 1 + builder/amazon/chroot/lockfile.go | 13 ++++++ builder/amazon/chroot/lockfile_unix.go | 32 ++++++++++++++ builder/amazon/chroot/step_flock.go | 59 ++++++++++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 builder/amazon/chroot/lockfile.go create mode 100644 builder/amazon/chroot/lockfile_unix.go create mode 100644 builder/amazon/chroot/step_flock.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 1641cde57..b01a50919 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -123,6 +123,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe steps := []multistep.Step{ &StepInstanceInfo{}, &StepSourceAMIInfo{}, + &StepFlock{}, &StepPrepareDevice{}, &StepCreateVolume{}, &StepAttachVolume{}, diff --git a/builder/amazon/chroot/lockfile.go b/builder/amazon/chroot/lockfile.go new file mode 100644 index 000000000..c9495bef4 --- /dev/null +++ b/builder/amazon/chroot/lockfile.go @@ -0,0 +1,13 @@ +// +build windows + +package chroot + +import "errors" + +func lockFile(*os.File) error { + return errors.New("not supported on Windows") +} + +func unlockFile(f *os.File) error { + return nil +} diff --git a/builder/amazon/chroot/lockfile_unix.go b/builder/amazon/chroot/lockfile_unix.go new file mode 100644 index 000000000..f84ea0d57 --- /dev/null +++ b/builder/amazon/chroot/lockfile_unix.go @@ -0,0 +1,32 @@ +// +build !windows + +package chroot + +import ( + "errors" + "os" + "syscall" +) + +// See: http://linux.die.net/include/sys/file.h +const LOCK_EX = 2 +const LOCK_NB = 4 +const LOCK_UN = 8 + +func lockFile(f *os.File) error { + err := syscall.Flock(int(f.Fd()), LOCK_EX|LOCK_NB) + if err != nil { + errno, ok := err.(syscall.Errno) + if ok && errno == syscall.EWOULDBLOCK { + return errors.New("file already locked") + } + + return err + } + + return nil +} + +func unlockFile(f *os.File) error { + return syscall.Flock(int(f.Fd()), LOCK_UN) +} diff --git a/builder/amazon/chroot/step_flock.go b/builder/amazon/chroot/step_flock.go new file mode 100644 index 000000000..57c07ebba --- /dev/null +++ b/builder/amazon/chroot/step_flock.go @@ -0,0 +1,59 @@ +package chroot + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" + "os" + "path/filepath" +) + +// StepFlock provisions the instance within a chroot. +type StepFlock struct { + fh *os.File +} + +func (s *StepFlock) Run(state map[string]interface{}) multistep.StepAction { + ui := state["ui"].(packer.Ui) + + lockfile := "/var/lock/packer-chroot/lock" + if err := os.MkdirAll(filepath.Dir(lockfile), 0755); err != nil { + err := fmt.Errorf("Error creating lock: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + log.Printf("Obtaining lock: %s", lockfile) + f, err := os.Create(lockfile) + if err != nil { + err := fmt.Errorf("Error creating lock: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // LOCK! + if err := lockFile(f); err != nil { + err := fmt.Errorf("Error creating lock: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Set the file handle, we can't close it because we need to hold + // the lock. + s.fh = f + + return multistep.ActionContinue +} + +func (s *StepFlock) Cleanup(state map[string]interface{}) { + if s.fh == nil { + return + } + + log.Printf("Unlocking: %s", s.fh.Name()) + unlockFile(s.fh) +} From cccf3ddc799599ff29c329e072c53626c283035c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 21:50:59 -0700 Subject: [PATCH 42/47] builder/amazon/chroot: fix compilaton on Windows --- builder/amazon/chroot/lockfile.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/builder/amazon/chroot/lockfile.go b/builder/amazon/chroot/lockfile.go index c9495bef4..1ba13e04b 100644 --- a/builder/amazon/chroot/lockfile.go +++ b/builder/amazon/chroot/lockfile.go @@ -2,7 +2,10 @@ package chroot -import "errors" +import ( + "errors" + "os" +) func lockFile(*os.File) error { return errors.New("not supported on Windows") From 6f8c915ef46767407ef057cf2d50f030b864e844 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 22:17:58 -0700 Subject: [PATCH 43/47] website: docs for amazon-chroot --- .../docs/builders/amazon-chroot.html.markdown | 177 ++++++++++++++++++ .../source/docs/builders/amazon.html.markdown | 7 + 2 files changed, 184 insertions(+) create mode 100644 website/source/docs/builders/amazon-chroot.html.markdown diff --git a/website/source/docs/builders/amazon-chroot.html.markdown b/website/source/docs/builders/amazon-chroot.html.markdown new file mode 100644 index 000000000..4839344e4 --- /dev/null +++ b/website/source/docs/builders/amazon-chroot.html.markdown @@ -0,0 +1,177 @@ +--- +layout: "docs" +page_title: "Amazon AMI Builder (chroot)" +--- + +# AMI Builder (chroot) + +Type: `amazon-chroot` + +The `amazon-chroot` builder is able to create Amazon AMIs backed by +an EBS volume as the root device. For more information on the difference +between instance storage and EBS-backed instances, see the +["storage for the root device" section in the EC2 documentation](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ComponentsAMIs.html#storage-for-the-root-device). + +The difference between this builder and the `amazon-ebs` builder is that +this builder is able to build an EBS-backed AMI without launching a new +EC2 instance. This can dramatically speed up AMI builds for organizations +who need the extra fast build. + +
+

This is an advanced builder. If you're just getting +started with Packer, we recommend starting with the +amazon-ebs builder, which is +much easier to use.

+
+ +The builder does _not_ manage AMIs. Once it creates an AMI and stores it +in your account, it is up to you to use, delete, etc. the AMI. + +## How Does it Work? + +This builder works by creating a new EBS volume from an existing source AMI +and attaching it into an already-running EC2 instance. One attached, a +[chroot](http://en.wikipedia.org/wiki/Chroot) is used to provision the +system within that volume. After provisioning, the volume is detached, +snapshotted, and an AMI is made. + +Using this process, minutes can be shaved off the AMI creation process +because a new EC2 instance doesn't need to be launched. + +There are some restrictions, however. The host EC2 instance where the +volume is attached to must be a similar system (generally the same OS +version, kernel versions, etc.) as the AMI being built. Additionally, +this process is much more expensive because the EC2 instance must be kept +running persistently in order to build AMIs, whereas the other AMI builders +start instances on-demand to build AMIs as needed. + +## Configuration Reference + +There are many configuration options available for the builder. They are +segmented below into two categories: required and optional parameters. Within +each category, the available configuration keys are alphabetized. + +Required: + +* `access_key` (string) - The access key used to communicate with AWS. + If not specified, Packer will attempt to read this from environmental + variables `AWS_ACCESS_KEY_ID` or `AWS_ACCESS_KEY` (in that order). + If the environmental variables aren't set and Packer is running on + an EC2 instance, Packer will check the instance metadata for IAM role + keys. + +* `ami_name` (string) - The name of the resulting AMI that will appear + when managing AMIs in the AWS console or via APIs. This must be unique. + To help make this unique, certain template parameters are available for + this value, which are documented below. + +* `secret_key` (string) - The secret key used to communicate with AWS. + If not specified, Packer will attempt to read this from environmental + variables `AWS_SECRET_ACCESS_KEY` or `AWS_SECRET_KEY` (in that order). + If the environmental variables aren't set and Packer is running on + an EC2 instance, Packer will check the instance metadata for IAM role + keys. + +* `source_ami` (string) - The source AMI whose root volume will be copied + and provisioned on the currently running instance. This must be an + EBS-backed AMI with a root volume snapshot that you have access to. + +Optional: + +* `chroot_mounts` (list of list of strings) - This is a list of additional + devices to mount into the chroot environment. This configuration parameter + requires some additional documentation which is in the "Chroot Mounts" section + below. Please read that section for more information on how to use this. + +* `copy_files` (list of strings) - Paths to files on the running EC2 instance + that will be copied into the chroot environment prior to provisioning. + This is useful, for example, to copy `/etc/resolv.conf` so that DNS lookups + work. + +* `device_path` (string) - The path to the device where the root volume + of the source AMI will be attached. This defaults to "" (empty string), + which forces Packer to find an open device automatically. + +* `mount_command` (string) - The command to use to mount devices. This + defaults to "mount". This may be useful to set if you want to set + environmental variables or perhaps run it with `sudo` or so on. + +* `mount_path` (string) - The path where the volume will be mounted. This is + where the chroot environment will be. This defaults to + `packer-amazon-chroot-volumes/{{.Device}}`. This is a configuration + template where the `.Device` variable is replaced with the name of the + device where the volume is attached. + +* `unmount_command` (string) - Just like `mount_command`, except this is + the command to unmount devices. + +## Basic Example + +Here is a basic example. It is completely valid except for the access keys: + +
+{
+  "type": "amazon-chroot",
+  "access_key": "YOUR KEY HERE",
+  "secret_key": "YOUR SECRET KEY HERE",
+  "source_ami": "ami-e81d5881",
+  "ami_name": "packer-amazon-chroot {{.CreateTime}}"
+}
+
+ +## AMI Name Variables + +The AMI name specified by the `ami_name` configuration variable is actually +treated as a [configuration template](/docs/templates/configuration-templates.html). +Packer provides a set of variables that it will replace +within the AMI name. This helps ensure the AMI name is unique, as AWS requires. + +The available variables are shown below: + +* `CreateTime` - This will be replaced with the Unix timestamp of when + the AMI was built. + +## Chroot Mounts + +The `chroot_mounts` configuration can be used to mount additional devices +within the chroot. By default, the following additional mounts are added +into the chroot by Packer: + +* `/proc` (proc) +* `/sys` (sysfs) +* `/dev` (bind to real `/dev`) +* `/dev/pts` (devpts) +* `/proc/sys/fs/binfmt_misc` (binfmt_misc) + +These default mounts are usually good enough for anyone and are sane +defaults. However, if you want to change or add the mount points, you may +using the `chroot_mounts` configuration. Here is an example configuration: + +
+{
+  "chroot_mounts": [
+    ["proc", "proc", "/proc"],
+    ["bind", "/dev", "/dev"]
+  ]
+}
+
+ +`chroot_mounts` is a list of a 3-tuples of strings. The three components +of the 3-tuple, in order, are: + +* The filesystem type. If this is "bind", then Packer will properly bind + the filesystem to another mount point. + +* The source device. + +* The mount directory. + +## Parallelism + +A quick note on parallelism: it is perfectly safe to run multiple +_separate_ Packer processes with the `amazon-chroot` builder on the same +EC2 instance. In fact, this is recommended as a way to push the most performance +out of your AMI builds. + +Packer properly obtains a process lock for the parallelism-sensitive parts +of its internals such as finding an available device. diff --git a/website/source/docs/builders/amazon.html.markdown b/website/source/docs/builders/amazon.html.markdown index b9dc7d2e7..604f2489a 100644 --- a/website/source/docs/builders/amazon.html.markdown +++ b/website/source/docs/builders/amazon.html.markdown @@ -18,6 +18,13 @@ AMI. Packer supports the following builders at the moment: instance-store AMIs by launching and provisioning a source instance, then rebundling it and uploading it to S3. +* [amazon-chroot](/docs/builders/amazon-chroot.html) - Create EBS-backed AMIs + from an existing EC2 instance by mounting the root device and using a + [Chroot](http://en.wikipedia.org/wiki/Chroot) environment to provision + that device. This is an **advanced builder and should not be used by + newcomers**. However, it is also the fastest way to build an EBS-backed + AMI since no new EC2 instance needs to be launched. +
Don't know which builder to use? If in doubt, use the amazon-ebs builder. It is From f0a0816736c9348a448acf15fab87d0a8de08e29 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 22:25:33 -0700 Subject: [PATCH 44/47] builder/amazon/chroot: let go of flock earlier for parallelism --- builder/amazon/chroot/builder.go | 1 + builder/amazon/chroot/step_early_unflock.go | 28 +++++++++++++++++++++ builder/amazon/chroot/step_flock.go | 17 +++++++++++-- builder/amazon/chroot/step_flock_test.go | 11 ++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 builder/amazon/chroot/step_early_unflock.go create mode 100644 builder/amazon/chroot/step_flock_test.go diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index b01a50919..3eeaa2051 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -127,6 +127,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepPrepareDevice{}, &StepCreateVolume{}, &StepAttachVolume{}, + &StepEarlyUnflock{}, &StepMountDevice{}, &StepMountExtra{}, &StepCopyFiles{}, diff --git a/builder/amazon/chroot/step_early_unflock.go b/builder/amazon/chroot/step_early_unflock.go new file mode 100644 index 000000000..d1c09008e --- /dev/null +++ b/builder/amazon/chroot/step_early_unflock.go @@ -0,0 +1,28 @@ +package chroot + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +// StepEarlyUnflock unlocks the flock. +type StepEarlyUnflock struct{} + +func (s *StepEarlyUnflock) Run(state map[string]interface{}) multistep.StepAction { + cleanup := state["flock_cleanup"].(Cleanup) + ui := state["ui"].(packer.Ui) + + log.Println("Unlocking file lock...") + if err := cleanup.CleanupFunc(state); err != nil { + err := fmt.Errorf("Error unlocking file lock: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepEarlyUnflock) Cleanup(state map[string]interface{}) {} diff --git a/builder/amazon/chroot/step_flock.go b/builder/amazon/chroot/step_flock.go index 57c07ebba..47d168d4c 100644 --- a/builder/amazon/chroot/step_flock.go +++ b/builder/amazon/chroot/step_flock.go @@ -10,6 +10,9 @@ import ( ) // StepFlock provisions the instance within a chroot. +// +// Produces: +// flock_cleanup Cleanup - To perform early cleanup type StepFlock struct { fh *os.File } @@ -46,14 +49,24 @@ func (s *StepFlock) Run(state map[string]interface{}) multistep.StepAction { // the lock. s.fh = f + state["flock_cleanup"] = s return multistep.ActionContinue } func (s *StepFlock) Cleanup(state map[string]interface{}) { + s.CleanupFunc(state) +} + +func (s *StepFlock) CleanupFunc(state map[string]interface{}) error { if s.fh == nil { - return + return nil } log.Printf("Unlocking: %s", s.fh.Name()) - unlockFile(s.fh) + if err := unlockFile(s.fh); err != nil { + return err + } + + s.fh = nil + return nil } diff --git a/builder/amazon/chroot/step_flock_test.go b/builder/amazon/chroot/step_flock_test.go new file mode 100644 index 000000000..a50cbd93d --- /dev/null +++ b/builder/amazon/chroot/step_flock_test.go @@ -0,0 +1,11 @@ +package chroot + +import "testing" + +func TestFlockCleanupFunc_ImplementsCleanupFunc(t *testing.T) { + var raw interface{} + raw = new(StepFlock) + if _, ok := raw.(Cleanup); !ok { + t.Fatalf("cleanup func should be a CleanupFunc") + } +} From 005c485bfbf779036b888153a49b563d033fa7af Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 22:29:06 -0700 Subject: [PATCH 45/47] builder/amazon/chroot: validate that chroot_mounts are 3 elements --- builder/amazon/chroot/builder.go | 8 ++++++++ builder/amazon/chroot/builder_test.go | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 3eeaa2051..3a42045fb 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -83,6 +83,14 @@ func (b *Builder) Prepare(raws ...interface{}) error { errs := common.CheckUnusedConfig(md) errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...) + for _, mounts := range b.config.ChrootMounts { + if len(mounts) != 3 { + errs = packer.MultiErrorAppend( + errs, errors.New("Each chroot_mounts entry should be three elements.")) + break + } + } + if b.config.SourceAmi == "" { errs = packer.MultiErrorAppend(errs, errors.New("source_ami is required.")) } diff --git a/builder/amazon/chroot/builder_test.go b/builder/amazon/chroot/builder_test.go index 2736dbbd8..2e4b5986a 100644 --- a/builder/amazon/chroot/builder_test.go +++ b/builder/amazon/chroot/builder_test.go @@ -19,6 +19,24 @@ func TestBuilder_ImplementsBuilder(t *testing.T) { } } +func TestBuilderPrepare_ChrootMounts(t *testing.T) { + b := &Builder{} + config := testConfig() + + config["chroot_mounts"] = nil + err := b.Prepare(config) + if err != nil { + t.Errorf("err: %s", err) + } + + config["chroot_mounts"] = [][]string{ + []string{"bad"}, + } + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} func TestBuilderPrepare_SourceAmi(t *testing.T) { b := &Builder{} config := testConfig() From ffe1e5f57bb86ba48a94027cb82158e3bf5e7e76 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 22:31:07 -0700 Subject: [PATCH 46/47] builder/amazon/chroot: default volumes dir is relative --- builder/amazon/chroot/builder.go | 2 +- builder/amazon/chroot/step_mount_device.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 3a42045fb..6eafb0b30 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -72,7 +72,7 @@ func (b *Builder) Prepare(raws ...interface{}) error { } if b.config.MountPath == "" { - b.config.MountPath = "/var/packer-amazon-chroot/volumes/{{.Device}}" + b.config.MountPath = "packer-amazon-chroot-volumes/{{.Device}}" } if b.config.UnmountCommand == "" { diff --git a/builder/amazon/chroot/step_mount_device.go b/builder/amazon/chroot/step_mount_device.go index 9dc4f1743..8d6815427 100644 --- a/builder/amazon/chroot/step_mount_device.go +++ b/builder/amazon/chroot/step_mount_device.go @@ -36,7 +36,16 @@ func (s *StepMountDevice) Run(state map[string]interface{}) multistep.StepAction Device: filepath.Base(device), }) + var err error mountPath := mountPathRaw.String() + mountPath, err = filepath.Abs(mountPath) + if err != nil { + err := fmt.Errorf("Error preparing mount directory: %s", err) + state["error"] = err + ui.Error(err.Error()) + return multistep.ActionHalt + } + log.Printf("Mount path: %s", mountPath) if err := os.MkdirAll(mountPath, 0755); err != nil { From 377493db4f7dd14dbec6eec7272b537f888f51b3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 30 Jul 2013 22:33:41 -0700 Subject: [PATCH 47/47] builder/amazon/chroot: use set AMI name --- builder/amazon/chroot/builder.go | 14 +++++++++++ builder/amazon/chroot/builder_test.go | 29 ++++++++++++++++++++++ builder/amazon/chroot/step_register_ami.go | 19 +++++++++++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go index 6eafb0b30..d96f518f6 100644 --- a/builder/amazon/chroot/builder.go +++ b/builder/amazon/chroot/builder.go @@ -6,6 +6,7 @@ package chroot import ( "errors" + "fmt" "github.com/mitchellh/goamz/ec2" "github.com/mitchellh/multistep" awscommon "github.com/mitchellh/packer/builder/amazon/common" @@ -13,6 +14,7 @@ import ( "github.com/mitchellh/packer/packer" "log" "runtime" + "text/template" ) // The unique ID for this builder @@ -24,6 +26,7 @@ type Config struct { common.PackerConfig `mapstructure:",squash"` awscommon.AccessConfig `mapstructure:",squash"` + AMIName string `mapstructure:"ami_name"` ChrootMounts [][]string `mapstructure:"chroot_mounts"` CopyFiles []string `mapstructure:"copy_files"` DevicePath string `mapstructure:"device_path"` @@ -83,6 +86,17 @@ func (b *Builder) Prepare(raws ...interface{}) error { errs := common.CheckUnusedConfig(md) errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...) + if b.config.AMIName == "" { + errs = packer.MultiErrorAppend( + errs, errors.New("ami_name must be specified")) + } else { + _, err = template.New("ami").Parse(b.config.AMIName) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Failed parsing ami_name: %s", err)) + } + } + for _, mounts := range b.config.ChrootMounts { if len(mounts) != 3 { errs = packer.MultiErrorAppend( diff --git a/builder/amazon/chroot/builder_test.go b/builder/amazon/chroot/builder_test.go index 2e4b5986a..2f13bae54 100644 --- a/builder/amazon/chroot/builder_test.go +++ b/builder/amazon/chroot/builder_test.go @@ -7,6 +7,7 @@ import ( func testConfig() map[string]interface{} { return map[string]interface{}{ + "ami_name": "foo", "source_ami": "foo", } } @@ -19,6 +20,34 @@ func TestBuilder_ImplementsBuilder(t *testing.T) { } } +func TestBuilderPrepare_AMIName(t *testing.T) { + var b Builder + config := testConfig() + + // Test good + config["ami_name"] = "foo" + err := b.Prepare(config) + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + // Test bad + config["ami_name"] = "foo {{" + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } + + // Test bad + delete(config, "ami_name") + b = Builder{} + err = b.Prepare(config) + if err == nil { + t.Fatal("should have error") + } +} + func TestBuilderPrepare_ChrootMounts(t *testing.T) { b := &Builder{} config := testConfig() diff --git a/builder/amazon/chroot/step_register_ami.go b/builder/amazon/chroot/step_register_ami.go index 9ec70f038..1c08bda61 100644 --- a/builder/amazon/chroot/step_register_ami.go +++ b/builder/amazon/chroot/step_register_ami.go @@ -1,23 +1,40 @@ package chroot import ( + "bytes" "fmt" "github.com/mitchellh/goamz/ec2" "github.com/mitchellh/multistep" awscommon "github.com/mitchellh/packer/builder/amazon/common" "github.com/mitchellh/packer/packer" + "strconv" + "text/template" + "time" ) +type amiNameData struct { + CreateTime string +} + // StepRegisterAMI creates the AMI. type StepRegisterAMI struct{} func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction { + config := state["config"].(*Config) ec2conn := state["ec2"].(*ec2.EC2) image := state["source_image"].(*ec2.Image) snapshotId := state["snapshot_id"].(string) ui := state["ui"].(packer.Ui) - amiName := "foo" + // Parse the name of the AMI + amiNameBuf := new(bytes.Buffer) + tData := amiNameData{ + strconv.FormatInt(time.Now().UTC().Unix(), 10), + } + + t := template.Must(template.New("ami").Parse(config.AMIName)) + t.Execute(amiNameBuf, tData) + amiName := amiNameBuf.String() ui.Say("Registering the AMI...") blockDevices := make([]ec2.BlockDeviceMapping, len(image.BlockDevices))