package common import ( "context" "encoding/base64" "fmt" "io/ioutil" "log" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/packer/common/retry" "github.com/hashicorp/packer/helper/communicator" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template/interpolate" ) type StepRunSourceInstance struct { AssociatePublicIpAddress bool LaunchMappings EC2BlockDeviceMappingsBuilder Comm *communicator.Config Ctx interpolate.Context Debug bool EbsOptimized bool EnableT2Unlimited bool ExpectedRootDevice string InstanceInitiatedShutdownBehavior string InstanceType string IsRestricted bool SourceAMI string Tags TagMap UserData string UserDataFile string VolumeTags TagMap NoEphemeral bool instanceId string } func (s *StepRunSourceInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { ec2conn := state.Get("ec2").(*ec2.EC2) securityGroupIds := aws.StringSlice(state.Get("securityGroupIds").([]string)) iamInstanceProfile := aws.String(state.Get("iamInstanceProfile").(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 AWS instance...") image, ok := state.Get("source_image").(*ec2.Image) if !ok { state.Put("error", fmt.Errorf("source_image type assertion failed")) return multistep.ActionHalt } s.SourceAMI = *image.ImageId if s.ExpectedRootDevice != "" && *image.RootDeviceType != s.ExpectedRootDevice { state.Put("error", fmt.Errorf( "The provided source AMI has an invalid root device type.\n"+ "Expected '%s', got '%s'.", s.ExpectedRootDevice, *image.RootDeviceType)) return multistep.ActionHalt } var instanceId string ui.Say("Adding tags to source instance") if _, exists := s.Tags["Name"]; !exists { s.Tags["Name"] = "Packer Builder" } ec2Tags, err := s.Tags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state) if err != nil { err := fmt.Errorf("Error tagging source instance: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } volTags, err := s.VolumeTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state) if err != nil { err := fmt.Errorf("Error tagging volumes: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } az := state.Get("availability_zone").(string) runOpts := &ec2.RunInstancesInput{ ImageId: &s.SourceAMI, InstanceType: &s.InstanceType, UserData: &userData, MaxCount: aws.Int64(1), MinCount: aws.Int64(1), IamInstanceProfile: &ec2.IamInstanceProfileSpecification{Name: iamInstanceProfile}, BlockDeviceMappings: s.LaunchMappings.BuildEC2BlockDeviceMappings(), Placement: &ec2.Placement{AvailabilityZone: &az}, EbsOptimized: &s.EbsOptimized, } if s.NoEphemeral { // This is only relevant for windows guests. Ephemeral drives by // default are assigned to drive names xvdca-xvdcz. // When vms are launched from the AWS console, they're automatically // removed from the block devices if the user hasn't said to use them, // but the SDK does not perform this cleanup. The following code just // manually removes the ephemeral drives from the mapping so that they // don't clutter up console views and cause confusion. log.Printf("no_ephemeral was set, so creating drives xvdca-xvdcz as empty mappings") DefaultEphemeralDeviceLetters := "abcdefghijklmnopqrstuvwxyz" for _, letter := range DefaultEphemeralDeviceLetters { bd := &ec2.BlockDeviceMapping{ DeviceName: aws.String("xvdc" + string(letter)), NoDevice: aws.String(""), } runOpts.BlockDeviceMappings = append(runOpts.BlockDeviceMappings, bd) } } if s.EnableT2Unlimited { creditOption := "unlimited" runOpts.CreditSpecification = &ec2.CreditSpecificationRequest{CpuCredits: &creditOption} } // Collect tags for tagging on resource creation var tagSpecs []*ec2.TagSpecification if len(ec2Tags) > 0 { runTags := &ec2.TagSpecification{ ResourceType: aws.String("instance"), Tags: ec2Tags, } tagSpecs = append(tagSpecs, runTags) } if len(volTags) > 0 { runVolTags := &ec2.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) ec2Tags.Report(ui) volTags.Report(ui) } if s.Comm.SSHKeyPairName != "" { runOpts.KeyName = &s.Comm.SSHKeyPairName } subnetId := state.Get("subnet_id").(string) if subnetId != "" && s.AssociatePublicIpAddress { runOpts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ { DeviceIndex: aws.Int64(0), AssociatePublicIpAddress: &s.AssociatePublicIpAddress, SubnetId: aws.String(subnetId), Groups: securityGroupIds, DeleteOnTermination: aws.Bool(true), }, } } else { runOpts.SubnetId = aws.String(subnetId) runOpts.SecurityGroupIds = securityGroupIds } if s.ExpectedRootDevice == "ebs" { runOpts.InstanceInitiatedShutdownBehavior = &s.InstanceInitiatedShutdownBehavior } runReq, runResp := ec2conn.RunInstancesRequest(runOpts) runReq.RetryCount = 11 err = runReq.Send() if isAWSErr(err, "VPCIdNotSpecified", "No default VPC for this user") && subnetId == "" { err := fmt.Errorf("Error launching source instance: a valid Subnet Id was not specified") state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } if err != nil { err := fmt.Errorf("Error launching source instance: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } instanceId = *runResp.Instances[0].InstanceId // Set the instance ID so that the cleanup works properly s.instanceId = instanceId ui.Message(fmt.Sprintf("Instance ID: %s", instanceId)) ui.Say(fmt.Sprintf("Waiting for instance (%v) to become ready...", instanceId)) describeInstance := &ec2.DescribeInstancesInput{ InstanceIds: []*string{aws.String(instanceId)}, } if err := ec2conn.WaitUntilInstanceRunningWithContext(ctx, describeInstance); err != nil { err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", instanceId, err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } // there's a race condition that can happen because of AWS's eventual // consistency where even though the wait is complete, the describe call // will fail. Retry a couple of times to try to mitigate that race. var r *ec2.DescribeInstancesOutput err = retry.Config{Tries: 11, ShouldRetry: func(err error) bool { if isAWSErr(err, "InvalidInstanceID.NotFound", "") { return true } return false }, RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 30 * time.Second, Multiplier: 2}).Linear, }.Run(ctx, func(ctx context.Context) error { r, err = ec2conn.DescribeInstances(describeInstance) return err }) if err != nil || len(r.Reservations) == 0 || len(r.Reservations[0].Instances) == 0 { err := fmt.Errorf("Error finding source instance.") state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } instance := r.Reservations[0].Instances[0] if s.Debug { if instance.PublicDnsName != nil && *instance.PublicDnsName != "" { ui.Message(fmt.Sprintf("Public DNS: %s", *instance.PublicDnsName)) } if instance.PublicIpAddress != nil && *instance.PublicIpAddress != "" { ui.Message(fmt.Sprintf("Public IP: %s", *instance.PublicIpAddress)) } if instance.PrivateIpAddress != nil && *instance.PrivateIpAddress != "" { ui.Message(fmt.Sprintf("Private IP: %s", *instance.PrivateIpAddress)) } } state.Put("instance", instance) // If we're in a region that doesn't support tagging on instance creation, // do that now. if s.IsRestricted { ec2Tags.Report(ui) // Retry creating tags for about 2.5 minutes err = retry.Config{Tries: 11, ShouldRetry: func(error) bool { if isAWSErr(err, "InvalidInstanceID.NotFound", "") { return true } return false }, RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 30 * time.Second, Multiplier: 2}).Linear, }.Run(ctx, func(ctx context.Context) error { _, err := ec2conn.CreateTags(&ec2.CreateTagsInput{ Tags: ec2Tags, Resources: []*string{instance.InstanceId}, }) return err }) if err != nil { err := fmt.Errorf("Error tagging source instance: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } // Now tag volumes volumeIds := make([]*string, 0) for _, v := range instance.BlockDeviceMappings { if ebs := v.Ebs; ebs != nil { volumeIds = append(volumeIds, ebs.VolumeId) } } if len(volumeIds) > 0 && s.VolumeTags.IsSet() { ui.Say("Adding tags to source EBS Volumes") volumeTags, err := s.VolumeTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state) if err != nil { err := fmt.Errorf("Error tagging source EBS Volumes on %s: %s", *instance.InstanceId, err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } volumeTags.Report(ui) _, err = ec2conn.CreateTags(&ec2.CreateTagsInput{ Resources: volumeIds, Tags: volumeTags, }) if err != nil { err := fmt.Errorf("Error tagging source EBS Volumes on %s: %s", *instance.InstanceId, err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } } } return multistep.ActionContinue } func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) { ec2conn := state.Get("ec2").(*ec2.EC2) ui := state.Get("ui").(packer.Ui) // Terminate the source instance if it exists if s.instanceId != "" { ui.Say("Terminating the source AWS instance...") if _, err := ec2conn.TerminateInstances(&ec2.TerminateInstancesInput{InstanceIds: []*string{&s.instanceId}}); err != nil { ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err)) return } if err := WaitUntilInstanceTerminated(aws.BackgroundContext(), ec2conn, s.instanceId); err != nil { ui.Error(err.Error()) } } }