diff --git a/builder/amazon/ebssurrogate/builder.go b/builder/amazon/ebssurrogate/builder.go index b990c5758..002560602 100644 --- a/builder/amazon/ebssurrogate/builder.go +++ b/builder/amazon/ebssurrogate/builder.go @@ -176,6 +176,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe } } + amiDevices := b.config.BuildAMIDevices() + launchDevices := b.config.BuildLaunchDevices() + // Build the steps steps := []multistep.Step{ &awscommon.StepPreValidate{ @@ -227,8 +230,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe EnableAMISriovNetSupport: b.config.AMISriovNetSupport, EnableAMIENASupport: b.config.AMIENASupport, }, - &StepSnapshotNewRootVolume{ - NewRootMountPoint: b.config.RootDevice.SourceDeviceName, + &StepSnapshotVolumes{ + LaunchDevices: launchDevices, }, &awscommon.StepDeregisterAMI{ AccessConfig: &b.config.AccessConfig, @@ -239,7 +242,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe }, &StepRegisterAMI{ RootDevice: b.config.RootDevice, - BlockDevices: b.config.BlockDevices.BuildAMIDevices(), + AMIDevices: amiDevices, + LaunchDevices: launchDevices, EnableAMISriovNetSupport: b.config.AMISriovNetSupport, EnableAMIENASupport: b.config.AMIENASupport, }, diff --git a/builder/amazon/ebssurrogate/root_block_device.go b/builder/amazon/ebssurrogate/root_block_device.go index 7e34b3733..09c13ca00 100644 --- a/builder/amazon/ebssurrogate/root_block_device.go +++ b/builder/amazon/ebssurrogate/root_block_device.go @@ -3,8 +3,6 @@ package ebssurrogate import ( "errors" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/packer/template/interpolate" ) @@ -46,21 +44,3 @@ func (c *RootBlockDevice) Prepare(ctx *interpolate.Context) []error { return nil } - -func (d *RootBlockDevice) createBlockDeviceMapping(snapshotId string) *ec2.BlockDeviceMapping { - rootBlockDevice := &ec2.EbsBlockDevice{ - SnapshotId: aws.String(snapshotId), - VolumeType: aws.String(d.VolumeType), - VolumeSize: aws.Int64(d.VolumeSize), - DeleteOnTermination: aws.Bool(d.DeleteOnTermination), - } - - if d.IOPS != 0 { - rootBlockDevice.Iops = aws.Int64(d.IOPS) - } - - return &ec2.BlockDeviceMapping{ - DeviceName: aws.String(d.DeviceName), - Ebs: rootBlockDevice, - } -} diff --git a/builder/amazon/ebssurrogate/step_register_ami.go b/builder/amazon/ebssurrogate/step_register_ami.go index 002ed00af..4c12adca6 100644 --- a/builder/amazon/ebssurrogate/step_register_ami.go +++ b/builder/amazon/ebssurrogate/step_register_ami.go @@ -14,7 +14,8 @@ import ( // StepRegisterAMI creates the AMI. type StepRegisterAMI struct { RootDevice RootBlockDevice - BlockDevices []*ec2.BlockDeviceMapping + AMIDevices []*ec2.BlockDeviceMapping + LaunchDevices []*ec2.BlockDeviceMapping EnableAMIENASupport bool EnableAMISriovNetSupport bool image *ec2.Image @@ -23,19 +24,19 @@ type StepRegisterAMI struct { func (s *StepRegisterAMI) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) ec2conn := state.Get("ec2").(*ec2.EC2) - snapshotId := state.Get("snapshot_id").(string) + snapshotIds := state.Get("snapshot_ids").(map[string]string) ui := state.Get("ui").(packer.Ui) ui.Say("Registering the AMI...") - blockDevicesExcludingRoot := DeduplicateRootVolume(s.BlockDevices, s.RootDevice, snapshotId) + blockDevices := s.combineDevices(snapshotIds) registerOpts := &ec2.RegisterImageInput{ Name: &config.AMIName, Architecture: aws.String(ec2.ArchitectureValuesX8664), RootDeviceName: aws.String(s.RootDevice.DeviceName), VirtualizationType: aws.String(config.AMIVirtType), - BlockDeviceMappings: blockDevicesExcludingRoot, + BlockDeviceMappings: blockDevices, } if s.EnableAMISriovNetSupport { @@ -120,17 +121,34 @@ func (s *StepRegisterAMI) Cleanup(state multistep.StateBag) { } } -func DeduplicateRootVolume(BlockDevices []*ec2.BlockDeviceMapping, RootDevice RootBlockDevice, snapshotId string) []*ec2.BlockDeviceMapping { - // Defensive coding to make sure we only add the root volume once - blockDevicesExcludingRoot := make([]*ec2.BlockDeviceMapping, 0, len(BlockDevices)) - for _, blockDevice := range BlockDevices { - if *blockDevice.DeviceName == RootDevice.SourceDeviceName { - continue - } +func (s *StepRegisterAMI) combineDevices(snapshotIds map[string]string) []*ec2.BlockDeviceMapping { + devices := map[string]*ec2.BlockDeviceMapping{} - blockDevicesExcludingRoot = append(blockDevicesExcludingRoot, blockDevice) + for _, device := range s.AMIDevices { + devices[*device.DeviceName] = device } - blockDevicesExcludingRoot = append(blockDevicesExcludingRoot, RootDevice.createBlockDeviceMapping(snapshotId)) - return blockDevicesExcludingRoot + // Devices in launch_block_device_mappings override any with + // the same name in ami_block_device_mappings, except for the + // one designated as the root device in ami_root_device + for _, device := range s.LaunchDevices { + snapshotId, ok := snapshotIds[*device.DeviceName] + if ok { + device.Ebs.SnapshotId = aws.String(snapshotId) + // Block devices with snapshot inherit + // encryption settings from the snapshot + device.Ebs.Encrypted = nil + device.Ebs.KmsKeyId = nil + } + if *device.DeviceName == s.RootDevice.SourceDeviceName { + device.DeviceName = aws.String(s.RootDevice.DeviceName) + } + devices[*device.DeviceName] = device + } + + blockDevices := []*ec2.BlockDeviceMapping{} + for _, device := range devices { + blockDevices = append(blockDevices, device) + } + return blockDevices } diff --git a/builder/amazon/ebssurrogate/step_register_ami_test.go b/builder/amazon/ebssurrogate/step_register_ami_test.go index b2abd7827..10872bb86 100644 --- a/builder/amazon/ebssurrogate/step_register_ami_test.go +++ b/builder/amazon/ebssurrogate/step_register_ami_test.go @@ -1,37 +1,247 @@ package ebssurrogate import ( + "reflect" + "sort" "testing" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" ) -func GetStringPointer() *string { - tmp := "/dev/name" - return &tmp -} +const sourceDeviceName = "/dev/xvdf" +const rootDeviceName = "/dev/xvda" -func GetTestDevice() *ec2.BlockDeviceMapping { - TestDev := ec2.BlockDeviceMapping{ - DeviceName: GetStringPointer(), - } - return &TestDev -} - -func TestStepRegisterAmi_DeduplicateRootVolume(t *testing.T) { - TestRootDevice := RootBlockDevice{} - TestRootDevice.SourceDeviceName = "/dev/name" - - blockDevices := []*ec2.BlockDeviceMapping{} - blockDevicesExcludingRoot := DeduplicateRootVolume(blockDevices, TestRootDevice, "12342351") - if len(blockDevicesExcludingRoot) != 1 { - t.Fatalf("Unexpected length of block devices list") - } - - TestBlockDevice := GetTestDevice() - blockDevices = append(blockDevices, TestBlockDevice) - blockDevicesExcludingRoot = DeduplicateRootVolume(blockDevices, TestRootDevice, "12342351") - if len(blockDevicesExcludingRoot) != 1 { - t.Fatalf("Unexpected length of block devices list") +func newStepRegisterAMI(amiDevices, launchDevices []*ec2.BlockDeviceMapping) *StepRegisterAMI { + return &StepRegisterAMI{ + RootDevice: RootBlockDevice{ + SourceDeviceName: sourceDeviceName, + DeviceName: rootDeviceName, + DeleteOnTermination: true, + VolumeType: "ebs", + VolumeSize: 10, + }, + AMIDevices: amiDevices, + LaunchDevices: launchDevices, + } +} + +func sorted(devices []*ec2.BlockDeviceMapping) []*ec2.BlockDeviceMapping { + sort.SliceStable(devices, func(i, j int) bool { + return *devices[i].DeviceName < *devices[j].DeviceName + }) + return devices +} + +func TestStepRegisterAmi_combineDevices(t *testing.T) { + cases := []struct { + snapshotIds map[string]string + amiDevices []*ec2.BlockDeviceMapping + launchDevices []*ec2.BlockDeviceMapping + allDevices []*ec2.BlockDeviceMapping + }{ + { + snapshotIds: map[string]string{}, + amiDevices: []*ec2.BlockDeviceMapping{}, + launchDevices: []*ec2.BlockDeviceMapping{}, + allDevices: []*ec2.BlockDeviceMapping{}, + }, + { + snapshotIds: map[string]string{}, + amiDevices: []*ec2.BlockDeviceMapping{}, + launchDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{}, + DeviceName: aws.String(sourceDeviceName), + }, + }, + allDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{}, + DeviceName: aws.String(rootDeviceName), + }, + }, + }, + { + // Minimal single device + snapshotIds: map[string]string{ + sourceDeviceName: "snap-0123456789abcdef1", + }, + amiDevices: []*ec2.BlockDeviceMapping{}, + launchDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{}, + DeviceName: aws.String(sourceDeviceName), + }, + }, + allDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + SnapshotId: aws.String("snap-0123456789abcdef1"), + }, + DeviceName: aws.String(rootDeviceName), + }, + }, + }, + { + // Single launch device with AMI device + snapshotIds: map[string]string{ + sourceDeviceName: "snap-0123456789abcdef1", + }, + amiDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{}, + DeviceName: aws.String("/dev/xvdg"), + }, + }, + launchDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{}, + DeviceName: aws.String(sourceDeviceName), + }, + }, + allDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + SnapshotId: aws.String("snap-0123456789abcdef1"), + }, + DeviceName: aws.String(rootDeviceName), + }, + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{}, + DeviceName: aws.String("/dev/xvdg"), + }, + }, + }, + { + // Multiple launch devices + snapshotIds: map[string]string{ + sourceDeviceName: "snap-0123456789abcdef1", + "/dev/xvdg": "snap-0123456789abcdef2", + }, + amiDevices: []*ec2.BlockDeviceMapping{}, + launchDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{}, + DeviceName: aws.String(sourceDeviceName), + }, + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{}, + DeviceName: aws.String("/dev/xvdg"), + }, + }, + allDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + SnapshotId: aws.String("snap-0123456789abcdef1"), + }, + DeviceName: aws.String(rootDeviceName), + }, + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + SnapshotId: aws.String("snap-0123456789abcdef2"), + }, + DeviceName: aws.String("/dev/xvdg"), + }, + }, + }, + { + // Multiple launch devices with encryption + snapshotIds: map[string]string{ + sourceDeviceName: "snap-0123456789abcdef1", + "/dev/xvdg": "snap-0123456789abcdef2", + }, + amiDevices: []*ec2.BlockDeviceMapping{}, + launchDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + Encrypted: aws.Bool(true), + }, + DeviceName: aws.String(sourceDeviceName), + }, + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + Encrypted: aws.Bool(true), + }, + DeviceName: aws.String("/dev/xvdg"), + }, + }, + allDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + SnapshotId: aws.String("snap-0123456789abcdef1"), + // Encrypted: true stripped from snapshotted devices + }, + DeviceName: aws.String(rootDeviceName), + }, + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + SnapshotId: aws.String("snap-0123456789abcdef2"), + }, + DeviceName: aws.String("/dev/xvdg"), + }, + }, + }, + { + // Multiple launch devices and AMI devices with encryption + snapshotIds: map[string]string{ + sourceDeviceName: "snap-0123456789abcdef1", + "/dev/xvdg": "snap-0123456789abcdef2", + }, + amiDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + Encrypted: aws.Bool(true), + KmsKeyId: aws.String("keyId"), + }, + // Source device name can be used in AMI devices + // since launch device of same name gets renamed + // to root device name + DeviceName: aws.String(sourceDeviceName), + }, + }, + launchDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + Encrypted: aws.Bool(true), + }, + DeviceName: aws.String(sourceDeviceName), + }, + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + Encrypted: aws.Bool(true), + }, + DeviceName: aws.String("/dev/xvdg"), + }, + }, + allDevices: []*ec2.BlockDeviceMapping{ + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + Encrypted: aws.Bool(true), + KmsKeyId: aws.String("keyId"), + }, + DeviceName: aws.String(sourceDeviceName), + }, + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + SnapshotId: aws.String("snap-0123456789abcdef1"), + }, + DeviceName: aws.String(rootDeviceName), + }, + &ec2.BlockDeviceMapping{ + Ebs: &ec2.EbsBlockDevice{ + SnapshotId: aws.String("snap-0123456789abcdef2"), + }, + DeviceName: aws.String("/dev/xvdg"), + }, + }, + }, + } + for _, tc := range cases { + stepRegisterAmi := newStepRegisterAMI(tc.amiDevices, tc.launchDevices) + allDevices := stepRegisterAmi.combineDevices(tc.snapshotIds) + if !reflect.DeepEqual(sorted(allDevices), sorted(tc.allDevices)) { + t.Fatalf("Unexpected output from combineDevices") + } } } diff --git a/builder/amazon/ebssurrogate/step_snapshot_new_root.go b/builder/amazon/ebssurrogate/step_snapshot_new_root.go deleted file mode 100644 index 3b607fbd0..000000000 --- a/builder/amazon/ebssurrogate/step_snapshot_new_root.go +++ /dev/null @@ -1,103 +0,0 @@ -package ebssurrogate - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/aws/aws-sdk-go/service/ec2" - awscommon "github.com/hashicorp/packer/builder/amazon/common" - "github.com/hashicorp/packer/helper/multistep" - "github.com/hashicorp/packer/packer" -) - -// StepSnapshotNewRootVolume creates a snapshot of the created volume. -// -// Produces: -// snapshot_id string - ID of the created snapshot -type StepSnapshotNewRootVolume struct { - NewRootMountPoint string - snapshotId string -} - -func (s *StepSnapshotNewRootVolume) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { - ec2conn := state.Get("ec2").(*ec2.EC2) - ui := state.Get("ui").(packer.Ui) - instance := state.Get("instance").(*ec2.Instance) - - var newRootVolume string - for _, volume := range instance.BlockDeviceMappings { - if *volume.DeviceName == s.NewRootMountPoint { - newRootVolume = *volume.Ebs.VolumeId - } - } - - ui.Say(fmt.Sprintf("Creating snapshot of EBS Volume %s...", newRootVolume)) - description := fmt.Sprintf("Packer: %s", time.Now().String()) - - createSnapResp, err := ec2conn.CreateSnapshot(&ec2.CreateSnapshotInput{ - VolumeId: &newRootVolume, - Description: &description, - }) - if err != nil { - err := fmt.Errorf("Error creating snapshot: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - // Set the snapshot ID so we can delete it later - s.snapshotId = *createSnapResp.SnapshotId - ui.Message(fmt.Sprintf("Snapshot ID: %s", s.snapshotId)) - - // Wait for the snapshot to be ready - stateChange := awscommon.StateChangeConf{ - Pending: []string{"pending"}, - StepState: state, - Target: "completed", - Refresh: func() (interface{}, string, error) { - resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{SnapshotIds: []*string{&s.snapshotId}}) - if err != nil { - return nil, "", err - } - - if len(resp.Snapshots) == 0 { - return nil, "", errors.New("No snapshots found.") - } - - s := resp.Snapshots[0] - return s, *s.State, nil - }, - } - - _, err = awscommon.WaitForState(&stateChange) - if err != nil { - err := fmt.Errorf("Error waiting for snapshot: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - state.Put("snapshot_id", s.snapshotId) - return multistep.ActionContinue -} - -func (s *StepSnapshotNewRootVolume) Cleanup(state multistep.StateBag) { - if s.snapshotId == "" { - return - } - - _, cancelled := state.GetOk(multistep.StateCancelled) - _, halted := state.GetOk(multistep.StateHalted) - - if cancelled || halted { - ec2conn := state.Get("ec2").(*ec2.EC2) - ui := state.Get("ui").(packer.Ui) - ui.Say("Removing snapshot since we cancelled or halted...") - _, err := ec2conn.DeleteSnapshot(&ec2.DeleteSnapshotInput{SnapshotId: &s.snapshotId}) - if err != nil { - ui.Error(fmt.Sprintf("Error: %s", err)) - } - } -} diff --git a/builder/amazon/ebssurrogate/step_snapshot_volumes.go b/builder/amazon/ebssurrogate/step_snapshot_volumes.go new file mode 100644 index 000000000..9de755ca5 --- /dev/null +++ b/builder/amazon/ebssurrogate/step_snapshot_volumes.go @@ -0,0 +1,129 @@ +package ebssurrogate + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/aws/aws-sdk-go/service/ec2" + multierror "github.com/hashicorp/go-multierror" + awscommon "github.com/hashicorp/packer/builder/amazon/common" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +// StepSnapshotVolumes creates snapshots of the created volumes. +// +// Produces: +// snapshot_ids map[string]string - IDs of the created snapshots +type StepSnapshotVolumes struct { + LaunchDevices []*ec2.BlockDeviceMapping + snapshotIds map[string]string +} + +func (s *StepSnapshotVolumes) snapshotVolume(deviceName string, state multistep.StateBag) error { + ec2conn := state.Get("ec2").(*ec2.EC2) + ui := state.Get("ui").(packer.Ui) + instance := state.Get("instance").(*ec2.Instance) + + var volumeId string + for _, volume := range instance.BlockDeviceMappings { + if *volume.DeviceName == deviceName { + volumeId = *volume.Ebs.VolumeId + } + } + if volumeId == "" { + return fmt.Errorf("Volume ID for device %s not found", deviceName) + } + + ui.Say(fmt.Sprintf("Creating snapshot of EBS Volume %s...", volumeId)) + description := fmt.Sprintf("Packer: %s", time.Now().String()) + + createSnapResp, err := ec2conn.CreateSnapshot(&ec2.CreateSnapshotInput{ + VolumeId: &volumeId, + Description: &description, + }) + if err != nil { + return err + } + + // Set the snapshot ID so we can delete it later + s.snapshotIds[deviceName] = *createSnapResp.SnapshotId + + // Wait for the snapshot to be ready + stateChange := awscommon.StateChangeConf{ + Pending: []string{"pending"}, + StepState: state, + Target: "completed", + Refresh: func() (interface{}, string, error) { + resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{ + SnapshotIds: []*string{createSnapResp.SnapshotId}, + }) + if err != nil { + return nil, "", err + } + + if len(resp.Snapshots) == 0 { + return nil, "", errors.New("No snapshots found.") + } + + s := resp.Snapshots[0] + return s, *s.State, nil + }, + } + + _, err = awscommon.WaitForState(&stateChange) + return err +} + +func (s *StepSnapshotVolumes) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + s.snapshotIds = map[string]string{} + + var wg sync.WaitGroup + var errs *multierror.Error + for _, device := range s.LaunchDevices { + wg.Add(1) + go func(device *ec2.BlockDeviceMapping) { + defer wg.Done() + if err := s.snapshotVolume(*device.DeviceName, state); err != nil { + errs = multierror.Append(errs, err) + } + }(device) + } + + wg.Wait() + + if errs != nil { + state.Put("error", errs) + ui.Error(errs.Error()) + return multistep.ActionHalt + } + + state.Put("snapshot_ids", s.snapshotIds) + return multistep.ActionContinue +} + +func (s *StepSnapshotVolumes) Cleanup(state multistep.StateBag) { + if len(s.snapshotIds) == 0 { + return + } + + _, cancelled := state.GetOk(multistep.StateCancelled) + _, halted := state.GetOk(multistep.StateHalted) + + if cancelled || halted { + ec2conn := state.Get("ec2").(*ec2.EC2) + ui := state.Get("ui").(packer.Ui) + ui.Say("Removing snapshots since we cancelled or halted...") + for _, snapshotId := range s.snapshotIds { + _, err := ec2conn.DeleteSnapshot(&ec2.DeleteSnapshotInput{SnapshotId: &snapshotId}) + if err != nil { + ui.Error(fmt.Sprintf("Error: %s", err)) + } + } + } +}