feature: bsusurrogate, add clean volumes and run source vm step

This commit is contained in:
Marin Salinas 2019-02-04 10:37:31 -06:00 committed by Megan Marsh
parent 5ef3e81234
commit 7ef09bba13
10 changed files with 546 additions and 19 deletions

View File

@ -164,6 +164,28 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
CommConfig: &b.config.RunConfig.Comm,
TemporarySGSourceCidr: b.config.TemporarySGSourceCidr,
},
&osccommon.StepCleanupVolumes{
BlockDevices: b.config.BlockDevices,
},
&osccommon.StepRunSourceVm{
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
BlockDevices: b.config.BlockDevices,
Comm: &b.config.RunConfig.Comm,
Ctx: b.config.ctx,
Debug: b.config.PackerDebug,
BsuOptimized: b.config.BsuOptimized,
EnableT2Unlimited: b.config.EnableT2Unlimited,
ExpectedRootDevice: "ebs", // should it be bsu
IamVmProfile: b.config.IamVmProfile,
VmInitiatedShutdownBehavior: b.config.VmInitiatedShutdownBehavior,
VmType: b.config.VmType,
IsRestricted: false,
SourceOMI: b.config.SourceOmi,
Tags: b.config.RunTags,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
VolumeTags: b.config.VolumeRunTags,
},
}
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)

View File

@ -26,25 +26,27 @@ const testBuilderAccBasic = `
"builders": [{
"type": "test",
"region": "eu-west-2",
"vm_type": "m3.medium",
"vm_type": "c4.large",
"source_omi": "ami-46260446",
"ssh_username": "ubuntu",
"omi_name": "packer-test {{timestamp}}",
"omi_virtualization_type": "hvm",
"subregion_name": "eu-west-2a",
"launch_block_device_mappings" : [
{
"volume_type" : "gp2",
"device_name" : "/dev/sda1",
"volume_type" : "io1",
"device_name" : "/dev/xvdf",
"delete_on_vm_deletion" : false,
"volume_size" : 10
"volume_size" : 10,
"iops": 300
}
],
"omi_root_device":{
"source_device_name": "/dev/sda1",
"device_name": "/dev/sda2",
"source_device_name": "/dev/xvdf",
"device_name": "/dev/sda1",
"delete_on_vm_deletion": true,
"volume_size": 16,
"volume_type": "gp2"
"volume_size": 10,
"volume_type": "standard"
}
}]

View File

@ -83,6 +83,54 @@ func buildBlockDevices(b []BlockDevice) []*oapi.BlockDeviceMapping {
return blockDevices
}
func buildBlockDevicesVmCreation(b []BlockDevice) []oapi.BlockDeviceMappingVmCreation {
var blockDevices []oapi.BlockDeviceMappingVmCreation
for _, blockDevice := range b {
mapping := oapi.BlockDeviceMappingVmCreation{
DeviceName: blockDevice.DeviceName,
}
if blockDevice.NoDevice {
mapping.NoDevice = ""
} else if blockDevice.VirtualName != "" {
if strings.HasPrefix(blockDevice.VirtualName, "ephemeral") {
mapping.VirtualDeviceName = blockDevice.VirtualName
}
} else {
bsu := oapi.BsuToCreate{
DeleteOnVmDeletion: blockDevice.DeleteOnVmDeletion,
}
if blockDevice.VolumeType != "" {
bsu.VolumeType = blockDevice.VolumeType
}
if blockDevice.VolumeSize > 0 {
bsu.VolumeSize = blockDevice.VolumeSize
}
// IOPS is only valid for io1 type
if blockDevice.VolumeType == "io1" {
bsu.Iops = blockDevice.IOPS
}
if blockDevice.SnapshotId != "" {
bsu.SnapshotId = blockDevice.SnapshotId
}
//missing
//BlockDevice Encrypted
//KmsKeyId
mapping.Bsu = bsu
}
blockDevices = append(blockDevices, mapping)
}
return blockDevices
}
func (b *BlockDevice) Prepare(ctx *interpolate.Context) error {
if b.DeviceName == "" {
return fmt.Errorf("The `device_name` must be specified " +
@ -114,6 +162,6 @@ func (b *OMIBlockDevices) BuildOMIDevices() []*oapi.BlockDeviceMapping {
return buildBlockDevices(b.OMIMappings)
}
func (b *LaunchBlockDevices) BuildLaunchDevices() []*oapi.BlockDeviceMapping {
return buildBlockDevices(b.LaunchMappings)
func (b *LaunchBlockDevices) BuildLaunchDevices() []oapi.BlockDeviceMappingVmCreation {
return buildBlockDevicesVmCreation(b.LaunchMappings)
}

View File

@ -20,7 +20,7 @@ func extractBuildInfo(region string, state multistep.StateBag) *BuildInfoTemplat
}
}
sourceOMI := rawSourceOMI.(*oapi.Image)
sourceOMI := rawSourceOMI.(oapi.Image)
sourceOMITags := make(map[string]string, len(sourceOMI.Tags))
for _, tag := range sourceOMI.Tags {
sourceOMITags[tag.Key] = tag.Value

View File

@ -59,10 +59,10 @@ func (d *SecurityGroupFilterOptions) Empty() bool {
// AMI and details on how to access that launched image.
type RunConfig struct {
AssociatePublicIpAddress bool `mapstructure:"associate_public_ip_address"`
Subregion string `mapstructure:"availability_zone"`
Subregion string `mapstructure:"subregion_name"`
BlockDurationMinutes int64 `mapstructure:"block_duration_minutes"`
DisableStopVm bool `mapstructure:"disable_stop_vm"`
BsuOptimized bool `mapstructure:"ebs_optimized"`
BsuOptimized bool `mapstructure:"bsu_optimized"`
EnableT2Unlimited bool `mapstructure:"enable_t2_unlimited"`
IamVmProfile string `mapstructure:"iam_vm_profile"`
VmInitiatedShutdownBehavior string `mapstructure:"shutdown_behavior"`

View File

@ -17,6 +17,19 @@ func waitForSecurityGroup(conn *oapi.Client, securityGroupID string) error {
return err
}
func waitUntilForVmRunning(conn *oapi.Client, vmID string) error {
errCh := make(chan error, 1)
go waitForState(errCh, "running", waitUntilVmStateFunc(conn, vmID))
err := <-errCh
return err
}
func waitUntilVmDeleted(conn *oapi.Client, vmID string) error {
errCh := make(chan error, 1)
go waitForState(errCh, "terminated", waitUntilVmStateFunc(conn, vmID))
return <-errCh
}
func waitForState(errCh chan<- error, target string, refresh stateRefreshFunc) error {
err := common.Retry(2, 2, 0, func(_ uint) (bool, error) {
state, err := refresh()
@ -31,6 +44,33 @@ func waitForState(errCh chan<- error, target string, refresh stateRefreshFunc) e
return err
}
func waitUntilVmStateFunc(conn *oapi.Client, id string) stateRefreshFunc {
return func() (string, error) {
log.Printf("[Debug] Check if SG with id %s exists", id)
resp, err := conn.POST_ReadVms(oapi.ReadVmsRequest{
Filters: oapi.FiltersVm{
VmIds: []string{id},
},
})
log.Printf("[Debug] Read Response %+v", resp.OK)
if err != nil {
return "", err
}
if resp.OK == nil {
return "", fmt.Errorf("Vm with ID %s. Not Found", id)
}
if len(resp.OK.Vms) == 0 {
return "pending", nil
}
return resp.OK.Vms[0].State, nil
}
}
func securityGroupWaitFunc(conn *oapi.Client, id string) stateRefreshFunc {
return func() (string, error) {
log.Printf("[Debug] Check if SG with id %s exists", id)

View File

@ -0,0 +1,96 @@
package common
import (
"context"
"fmt"
"reflect"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/outscale/osc-go/oapi"
)
// stepCleanupVolumes cleans up any orphaned volumes that were not designated to
// remain after termination of the vm. These volumes are typically ones
// that are marked as "delete on terminate:false" in the source_ami of a build.
type StepCleanupVolumes struct {
BlockDevices BlockDevices
}
func (s *StepCleanupVolumes) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
// stepCleanupVolumes is for Cleanup only
return multistep.ActionContinue
}
func (s *StepCleanupVolumes) Cleanup(state multistep.StateBag) {
oapiconn := state.Get("oapi").(*oapi.Client)
vmRaw := state.Get("vm")
var vm *oapi.Vm
if vmRaw != nil {
vm = vmRaw.(*oapi.Vm)
}
ui := state.Get("ui").(packer.Ui)
if vm == nil {
ui.Say("No volumes to clean up, skipping")
return
}
ui.Say("Cleaning up any extra volumes...")
// Collect Volume information from the cached Vm as a map of volume-id
// to device name, to compare with save list below
var vl []string
volList := make(map[string]string)
for _, bdm := range vm.BlockDeviceMappings {
if !reflect.DeepEqual(bdm.Bsu, oapi.BsuCreated{}) {
vl = append(vl, bdm.Bsu.VolumeId)
volList[bdm.Bsu.VolumeId] = bdm.DeviceName
}
}
// Using the volume list from the cached Vm, check with Outscale for up to
// date information on them
resp, err := oapiconn.POST_ReadVolumes(oapi.ReadVolumesRequest{
Filters: oapi.FiltersVolume{
VolumeIds: vl,
},
})
if err != nil {
ui.Say(fmt.Sprintf("Error describing volumes: %s", err))
return
}
// If any of the returned volumes are in a "deleting" stage or otherwise not
// available, remove them from the list of volumes
for _, v := range resp.OK.Volumes {
if v.State != "" && v.State != "available" {
delete(volList, v.VolumeId)
}
}
if len(resp.OK.Volumes) == 0 {
ui.Say("No volumes to clean up, skipping")
return
}
// Filter out any devices created as part of the launch mappings, since
// we'll let amazon follow the `delete_on_termination` setting.
for _, b := range s.BlockDevices.LaunchMappings {
for volKey, volName := range volList {
if volName == b.DeviceName {
delete(volList, volKey)
}
}
}
// Destroy remaining volumes
for k := range volList {
ui.Say(fmt.Sprintf("Destroying volume (%s)...", k))
_, err := oapiconn.POST_DeleteVolume(oapi.DeleteVolumeRequest{VolumeId: k})
if err != nil {
ui.Say(fmt.Sprintf("Error deleting volume: %s", err))
}
}
}

View File

@ -150,7 +150,7 @@ func (s *StepNetworkInfo) Run(_ context.Context, state multistep.StateBag) multi
}
state.Put("net_id", s.NetId)
state.Put("availability_zone", s.SubregionName)
state.Put("subregion_name", s.SubregionName)
state.Put("subnet_id", s.SubnetId)
return multistep.ActionContinue
}

View File

@ -0,0 +1,319 @@
package common
import (
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"reflect"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/outscale/osc-go/oapi"
retry "github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
type StepRunSourceVm struct {
AssociatePublicIpAddress bool
BlockDevices BlockDevices
Comm *communicator.Config
Ctx interpolate.Context
Debug bool
BsuOptimized bool
EnableT2Unlimited bool
ExpectedRootDevice string
IamVmProfile string
VmInitiatedShutdownBehavior string
VmType string
IsRestricted bool
SourceOMI string
Tags TagMap
UserData string
UserDataFile string
VolumeTags TagMap
vmId string
}
func (s *StepRunSourceVm) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
oapiconn := state.Get("oapi").(*oapi.Client)
securityGroupIds := state.Get("securityGroupIds").([]string)
ui := state.Get("ui").(packer.Ui)
userData := s.UserData
if s.UserDataFile != "" {
contents, err := ioutil.ReadFile(s.UserDataFile)
if err != nil {
state.Put("error", fmt.Errorf("Problem reading user data file: %s", err))
return multistep.ActionHalt
}
userData = string(contents)
}
// Test if it is encoded already, and if not, encode it
if _, err := base64.StdEncoding.DecodeString(userData); err != nil {
log.Printf("[DEBUG] base64 encoding user data...")
userData = base64.StdEncoding.EncodeToString([]byte(userData))
}
ui.Say("Launching a source OUTSCALE vm...")
image, ok := state.Get("source_image").(oapi.Image)
if !ok {
state.Put("error", fmt.Errorf("source_image type assertion failed"))
return multistep.ActionHalt
}
s.SourceOMI = image.ImageId
if s.ExpectedRootDevice != "" && image.RootDeviceType != s.ExpectedRootDevice {
state.Put("error", fmt.Errorf(
"The provided source OMI has an invalid root device type.\n"+
"Expected '%s', got '%s'.",
s.ExpectedRootDevice, image.RootDeviceType))
return multistep.ActionHalt
}
var vmId string
ui.Say("Adding tags to source vm")
if _, exists := s.Tags["Name"]; !exists {
s.Tags["Name"] = "Packer Builder"
}
oapiTags, err := s.Tags.OAPITags(s.Ctx, oapiconn.GetConfig().Region, state)
if err != nil {
err := fmt.Errorf("Error tagging source vm: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// volTags, err := s.VolumeTags.OAPITags(s.Ctx, oapiconn.GetConfig().Region, state)
// if err != nil {
// err := fmt.Errorf("Error tagging volumes: %s", err)
// state.Put("error", err)
// ui.Error(err.Error())
// return multistep.ActionHalt
// }
subregion := state.Get("subregion_name").(string)
runOpts := oapi.CreateVmsRequest{
ImageId: s.SourceOMI,
VmType: s.VmType,
UserData: userData,
MaxVmsCount: 1,
MinVmsCount: 1,
Placement: oapi.Placement{SubregionName: subregion},
BsuOptimized: s.BsuOptimized,
BlockDeviceMappings: s.BlockDevices.BuildLaunchDevices(),
//IamVmProfile: oapi.IamVmProfileSpecification{Name: &s.IamVmProfile},
}
// if s.EnableT2Unlimited {
// creditOption := "unlimited"
// runOpts.CreditSpecification = &oapi.CreditSpecificationRequest{CpuCredits: &creditOption}
// }
// Collect tags for tagging on resource creation
// var tagSpecs []oapi.ResourceTag
// if len(oapiTags) > 0 {
// runTags := &oapi.ResourceTag{
// ResourceType: aws.String("vm"),
// Tags: oapiTags,
// }
// tagSpecs = append(tagSpecs, runTags)
// }
// if len(volTags) > 0 {
// runVolTags := &oapi.TagSpecification{
// ResourceType: aws.String("volume"),
// Tags: volTags,
// }
// tagSpecs = append(tagSpecs, runVolTags)
// }
// // If our region supports it, set tag specifications
// if len(tagSpecs) > 0 && !s.IsRestricted {
// runOpts.SetTagSpecifications(tagSpecs)
// oapiTags.Report(ui)
// volTags.Report(ui)
// }
if s.Comm.SSHKeyPairName != "" {
runOpts.KeypairName = s.Comm.SSHKeyPairName
}
subnetId := state.Get("subnet_id").(string)
if subnetId != "" && s.AssociatePublicIpAddress {
runOpts.Nics = []oapi.NicForVmCreation{
{
DeviceNumber: 0,
//AssociatePublicIpAddress: s.AssociatePublicIpAddress,
SubnetId: subnetId,
SecurityGroupIds: securityGroupIds,
DeleteOnVmDeletion: true,
},
}
} else {
runOpts.SubnetId = subnetId
runOpts.SecurityGroupIds = securityGroupIds
}
if s.ExpectedRootDevice == "bsu" {
runOpts.VmInitiatedShutdownBehavior = s.VmInitiatedShutdownBehavior
}
runResp, err := oapiconn.POST_CreateVms(runOpts)
if err != nil {
err := fmt.Errorf("Error launching source vm: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
vmId = runResp.OK.Vms[0].VmId
// Set the vm ID so that the cleanup works properly
s.vmId = vmId
ui.Message(fmt.Sprintf("Vm ID: %s", vmId))
ui.Say(fmt.Sprintf("Waiting for vm (%v) to become ready...", vmId))
request := oapi.ReadVmsRequest{
Filters: oapi.FiltersVm{
VmIds: []string{vmId},
},
}
if err := waitUntilForVmRunning(oapiconn, vmId); err != nil {
err := fmt.Errorf("Error waiting for vm (%s) to become ready: %s", vmId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
//TODO:Set Vm and Volume Tags,
//TODO: LinkPublicIp i
resp, err := oapiconn.POST_ReadVms(request)
r := resp.OK
if err != nil || len(r.Vms) == 0 {
err := fmt.Errorf("Error finding source vm.")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
vm := r.Vms[0]
if s.Debug {
if vm.PublicDnsName != "" {
ui.Message(fmt.Sprintf("Public DNS: %s", vm.PublicDnsName))
}
if vm.PublicIp != "" {
ui.Message(fmt.Sprintf("Public IP: %s", vm.PublicIp))
}
if vm.PrivateIp != "" {
ui.Message(fmt.Sprintf("Private IP: %s", vm.PublicIp))
}
}
state.Put("vm", vm)
// If we're in a region that doesn't support tagging on vm creation,
// do that now.
if s.IsRestricted {
oapiTags.Report(ui)
// Retry creating tags for about 2.5 minutes
err = retry.Retry(0.2, 30, 11, func(_ uint) (bool, error) {
_, err := oapiconn.POST_CreateTags(oapi.CreateTagsRequest{
Tags: oapiTags,
ResourceIds: []string{vmId},
})
if err == nil {
return true, nil
}
//TODO: improve error
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "InvalidVmID.NotFound" {
return false, nil
}
}
return true, err
})
if err != nil {
err := fmt.Errorf("Error tagging source vm: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Now tag volumes
volumeIds := make([]string, 0)
for _, v := range vm.BlockDeviceMappings {
if bsu := v.Bsu; !reflect.DeepEqual(bsu, oapi.BsuCreated{}) {
volumeIds = append(volumeIds, bsu.VolumeId)
}
}
if len(volumeIds) > 0 && s.VolumeTags.IsSet() {
ui.Say("Adding tags to source BSU Volumes")
volumeTags, err := s.VolumeTags.OAPITags(s.Ctx, oapiconn.GetConfig().Region, state)
if err != nil {
err := fmt.Errorf("Error tagging source BSU Volumes on %s: %s", vm.VmId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
volumeTags.Report(ui)
_, err = oapiconn.POST_CreateTags(oapi.CreateTagsRequest{
ResourceIds: volumeIds,
Tags: volumeTags,
})
if err != nil {
err := fmt.Errorf("Error tagging source BSU Volumes on %s: %s", vm.VmId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *StepRunSourceVm) Cleanup(state multistep.StateBag) {
oapiconn := state.Get("oapi").(*oapi.Client)
ui := state.Get("ui").(packer.Ui)
// Terminate the source vm if it exists
if s.vmId != "" {
ui.Say("Terminating the source OUTSCALE vm...")
if _, err := oapiconn.POST_DeleteVms(oapi.DeleteVmsRequest{VmIds: []string{s.vmId}}); err != nil {
ui.Error(fmt.Sprintf("Error terminating vm, may still be around: %s", err))
return
}
if err := waitUntilVmDeleted(oapiconn, s.vmId); err != nil {
ui.Error(err.Error())
}
}
}

View File

@ -10,9 +10,9 @@ import (
)
type TagMap map[string]string
type OAPI []*oapi.ResourceTag
type OAPITags []oapi.ResourceTag
func (t OAPI) Report(ui packer.Ui) {
func (t OAPITags) Report(ui packer.Ui) {
for _, tag := range t {
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"",
tag.Key, tag.Value))
@ -23,8 +23,8 @@ func (t TagMap) IsSet() bool {
return len(t) > 0
}
func (t TagMap) OAPI(ctx interpolate.Context, region string, state multistep.StateBag) (OAPI, error) {
var oapiTags []*oapi.ResourceTag
func (t TagMap) OAPITags(ctx interpolate.Context, region string, state multistep.StateBag) (OAPITags, error) {
var oapiTags []oapi.ResourceTag
ctx.Data = extractBuildInfo(region, state)
for key, value := range t {
@ -36,7 +36,7 @@ func (t TagMap) OAPI(ctx interpolate.Context, region string, state multistep.Sta
if err != nil {
return nil, fmt.Errorf("Error processing tag: %s:%s - %s", key, value, err)
}
oapiTags = append(oapiTags, &oapi.ResourceTag{
oapiTags = append(oapiTags, oapi.ResourceTag{
Key: interpolatedKey,
Value: interpolatedValue,
})