Merge pull request #6056 from rjosephwright/snapshots

Modify ebssurrogate builder to snapshot all launch devices
This commit is contained in:
Matthew Hooker 2018-03-29 10:15:19 -07:00 committed by GitHub
commit f2fc064037
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 404 additions and 166 deletions

View File

@ -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,
},

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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{
{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String(sourceDeviceName),
},
},
allDevices: []*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{
{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String(sourceDeviceName),
},
},
allDevices: []*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{
{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String("/dev/xvdg"),
},
},
launchDevices: []*ec2.BlockDeviceMapping{
{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String(sourceDeviceName),
},
},
allDevices: []*ec2.BlockDeviceMapping{
{
Ebs: &ec2.EbsBlockDevice{
SnapshotId: aws.String("snap-0123456789abcdef1"),
},
DeviceName: aws.String(rootDeviceName),
},
{
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{
{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String(sourceDeviceName),
},
{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String("/dev/xvdg"),
},
},
allDevices: []*ec2.BlockDeviceMapping{
{
Ebs: &ec2.EbsBlockDevice{
SnapshotId: aws.String("snap-0123456789abcdef1"),
},
DeviceName: aws.String(rootDeviceName),
},
{
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{
{
Ebs: &ec2.EbsBlockDevice{
Encrypted: aws.Bool(true),
},
DeviceName: aws.String(sourceDeviceName),
},
{
Ebs: &ec2.EbsBlockDevice{
Encrypted: aws.Bool(true),
},
DeviceName: aws.String("/dev/xvdg"),
},
},
allDevices: []*ec2.BlockDeviceMapping{
{
Ebs: &ec2.EbsBlockDevice{
SnapshotId: aws.String("snap-0123456789abcdef1"),
// Encrypted: true stripped from snapshotted devices
},
DeviceName: aws.String(rootDeviceName),
},
{
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{
{
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{
{
Ebs: &ec2.EbsBlockDevice{
Encrypted: aws.Bool(true),
},
DeviceName: aws.String(sourceDeviceName),
},
{
Ebs: &ec2.EbsBlockDevice{
Encrypted: aws.Bool(true),
},
DeviceName: aws.String("/dev/xvdg"),
},
},
allDevices: []*ec2.BlockDeviceMapping{
{
Ebs: &ec2.EbsBlockDevice{
Encrypted: aws.Bool(true),
KmsKeyId: aws.String("keyId"),
},
DeviceName: aws.String(sourceDeviceName),
},
{
Ebs: &ec2.EbsBlockDevice{
SnapshotId: aws.String("snap-0123456789abcdef1"),
},
DeviceName: aws.String(rootDeviceName),
},
{
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")
}
}
}

View File

@ -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))
}
}
}

View File

@ -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))
}
}
}
}