github.com/rahart/packer@v0.12.2-0.20161229105310-282bb6ad370f/builder/amazon/common/step_run_source_instance.go (about)

     1  package common
     2  
     3  import (
     4  	"encoding/base64"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"strconv"
     9  	"time"
    10  
    11  	"github.com/aws/aws-sdk-go/aws"
    12  	"github.com/aws/aws-sdk-go/service/ec2"
    13  
    14  	"github.com/mitchellh/multistep"
    15  	"github.com/mitchellh/packer/packer"
    16  )
    17  
    18  type StepRunSourceInstance struct {
    19  	AssociatePublicIpAddress          bool
    20  	AvailabilityZone                  string
    21  	BlockDevices                      BlockDevices
    22  	Debug                             bool
    23  	EbsOptimized                      bool
    24  	ExpectedRootDevice                string
    25  	InstanceType                      string
    26  	IamInstanceProfile                string
    27  	SourceAMI                         string
    28  	SpotPrice                         string
    29  	SpotPriceProduct                  string
    30  	SubnetId                          string
    31  	Tags                              map[string]string
    32  	UserData                          string
    33  	UserDataFile                      string
    34  	InstanceInitiatedShutdownBehavior string
    35  
    36  	instanceId  string
    37  	spotRequest *ec2.SpotInstanceRequest
    38  }
    39  
    40  func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepAction {
    41  	ec2conn := state.Get("ec2").(*ec2.EC2)
    42  	keyName := state.Get("keyPair").(string)
    43  	tempSecurityGroupIds := state.Get("securityGroupIds").([]string)
    44  	ui := state.Get("ui").(packer.Ui)
    45  
    46  	securityGroupIds := make([]*string, len(tempSecurityGroupIds))
    47  	for i, sg := range tempSecurityGroupIds {
    48  		log.Printf("[DEBUG] Waiting for tempSecurityGroup: %s", sg)
    49  		err := WaitUntilSecurityGroupExists(ec2conn,
    50  			&ec2.DescribeSecurityGroupsInput{
    51  				GroupIds: []*string{aws.String(sg)},
    52  			},
    53  		)
    54  		if err == nil {
    55  			log.Printf("[DEBUG] Found security group %s", sg)
    56  			securityGroupIds[i] = aws.String(sg)
    57  		} else {
    58  			err := fmt.Errorf("Timed out waiting for security group %s", sg)
    59  			log.Printf("[DEBUG] %s", err.Error())
    60  			state.Put("error", err)
    61  			return multistep.ActionHalt
    62  		}
    63  	}
    64  
    65  	userData := s.UserData
    66  	if s.UserDataFile != "" {
    67  		contents, err := ioutil.ReadFile(s.UserDataFile)
    68  		if err != nil {
    69  			state.Put("error", fmt.Errorf("Problem reading user data file: %s", err))
    70  			return multistep.ActionHalt
    71  		}
    72  
    73  		userData = string(contents)
    74  	}
    75  
    76  	// Test if it is encoded already, and if not, encode it
    77  	if _, err := base64.StdEncoding.DecodeString(userData); err != nil {
    78  		log.Printf("[DEBUG] base64 encoding user data...")
    79  		userData = base64.StdEncoding.EncodeToString([]byte(userData))
    80  	}
    81  
    82  	ui.Say("Launching a source AWS instance...")
    83  	image, ok := state.Get("source_image").(*ec2.Image)
    84  	if !ok {
    85  		state.Put("error", fmt.Errorf("source_image type assertion failed"))
    86  		return multistep.ActionHalt
    87  	}
    88  	s.SourceAMI = *image.ImageId
    89  
    90  	if s.ExpectedRootDevice != "" && *image.RootDeviceType != s.ExpectedRootDevice {
    91  		state.Put("error", fmt.Errorf(
    92  			"The provided source AMI has an invalid root device type.\n"+
    93  				"Expected '%s', got '%s'.",
    94  			s.ExpectedRootDevice, *image.RootDeviceType))
    95  		return multistep.ActionHalt
    96  	}
    97  
    98  	spotPrice := s.SpotPrice
    99  	availabilityZone := s.AvailabilityZone
   100  	if spotPrice == "auto" {
   101  		ui.Message(fmt.Sprintf(
   102  			"Finding spot price for %s %s...",
   103  			s.SpotPriceProduct, s.InstanceType))
   104  
   105  		// Detect the spot price
   106  		startTime := time.Now().Add(-1 * time.Hour)
   107  		resp, err := ec2conn.DescribeSpotPriceHistory(&ec2.DescribeSpotPriceHistoryInput{
   108  			InstanceTypes:       []*string{&s.InstanceType},
   109  			ProductDescriptions: []*string{&s.SpotPriceProduct},
   110  			AvailabilityZone:    &s.AvailabilityZone,
   111  			StartTime:           &startTime,
   112  		})
   113  		if err != nil {
   114  			err := fmt.Errorf("Error finding spot price: %s", err)
   115  			state.Put("error", err)
   116  			ui.Error(err.Error())
   117  			return multistep.ActionHalt
   118  		}
   119  
   120  		var price float64
   121  		for _, history := range resp.SpotPriceHistory {
   122  			log.Printf("[INFO] Candidate spot price: %s", *history.SpotPrice)
   123  			current, err := strconv.ParseFloat(*history.SpotPrice, 64)
   124  			if err != nil {
   125  				log.Printf("[ERR] Error parsing spot price: %s", err)
   126  				continue
   127  			}
   128  			if price == 0 || current < price {
   129  				price = current
   130  				if s.AvailabilityZone == "" {
   131  					availabilityZone = *history.AvailabilityZone
   132  				}
   133  			}
   134  		}
   135  		if price == 0 {
   136  			err := fmt.Errorf("No candidate spot prices found!")
   137  			state.Put("error", err)
   138  			ui.Error(err.Error())
   139  			return multistep.ActionHalt
   140  		} else {
   141  			// Add 0.5 cents to minimum spot bid to ensure capacity will be available
   142  			// Avoids price-too-low error in active markets which can fluctuate
   143  			price = price + 0.005
   144  		}
   145  
   146  		spotPrice = strconv.FormatFloat(price, 'f', -1, 64)
   147  	}
   148  
   149  	var instanceId string
   150  
   151  	if spotPrice == "" || spotPrice == "0" {
   152  		runOpts := &ec2.RunInstancesInput{
   153  			ImageId:             &s.SourceAMI,
   154  			InstanceType:        &s.InstanceType,
   155  			UserData:            &userData,
   156  			MaxCount:            aws.Int64(1),
   157  			MinCount:            aws.Int64(1),
   158  			IamInstanceProfile:  &ec2.IamInstanceProfileSpecification{Name: &s.IamInstanceProfile},
   159  			BlockDeviceMappings: s.BlockDevices.BuildLaunchDevices(),
   160  			Placement:           &ec2.Placement{AvailabilityZone: &s.AvailabilityZone},
   161  			EbsOptimized:        &s.EbsOptimized,
   162  		}
   163  
   164  		if keyName != "" {
   165  			runOpts.KeyName = &keyName
   166  		}
   167  
   168  		if s.SubnetId != "" && s.AssociatePublicIpAddress {
   169  			runOpts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{
   170  				{
   171  					DeviceIndex:              aws.Int64(0),
   172  					AssociatePublicIpAddress: &s.AssociatePublicIpAddress,
   173  					SubnetId:                 &s.SubnetId,
   174  					Groups:                   securityGroupIds,
   175  					DeleteOnTermination:      aws.Bool(true),
   176  				},
   177  			}
   178  		} else {
   179  			runOpts.SubnetId = &s.SubnetId
   180  			runOpts.SecurityGroupIds = securityGroupIds
   181  		}
   182  
   183  		if s.ExpectedRootDevice == "ebs" {
   184  			runOpts.InstanceInitiatedShutdownBehavior = &s.InstanceInitiatedShutdownBehavior
   185  		}
   186  
   187  		runResp, err := ec2conn.RunInstances(runOpts)
   188  		if err != nil {
   189  			err := fmt.Errorf("Error launching source instance: %s", err)
   190  			state.Put("error", err)
   191  			ui.Error(err.Error())
   192  			return multistep.ActionHalt
   193  		}
   194  		instanceId = *runResp.Instances[0].InstanceId
   195  	} else {
   196  		ui.Message(fmt.Sprintf(
   197  			"Requesting spot instance '%s' for: %s",
   198  			s.InstanceType, spotPrice))
   199  
   200  		runOpts := &ec2.RequestSpotLaunchSpecification{
   201  			ImageId:            &s.SourceAMI,
   202  			InstanceType:       &s.InstanceType,
   203  			UserData:           &userData,
   204  			IamInstanceProfile: &ec2.IamInstanceProfileSpecification{Name: &s.IamInstanceProfile},
   205  			Placement: &ec2.SpotPlacement{
   206  				AvailabilityZone: &availabilityZone,
   207  			},
   208  			BlockDeviceMappings: s.BlockDevices.BuildLaunchDevices(),
   209  			EbsOptimized:        &s.EbsOptimized,
   210  		}
   211  
   212  		if s.SubnetId != "" && s.AssociatePublicIpAddress {
   213  			runOpts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{
   214  				{
   215  					DeviceIndex:              aws.Int64(0),
   216  					AssociatePublicIpAddress: &s.AssociatePublicIpAddress,
   217  					SubnetId:                 &s.SubnetId,
   218  					Groups:                   securityGroupIds,
   219  					DeleteOnTermination:      aws.Bool(true),
   220  				},
   221  			}
   222  		} else {
   223  			runOpts.SubnetId = &s.SubnetId
   224  			runOpts.SecurityGroupIds = securityGroupIds
   225  		}
   226  
   227  		if keyName != "" {
   228  			runOpts.KeyName = &keyName
   229  		}
   230  
   231  		runSpotResp, err := ec2conn.RequestSpotInstances(&ec2.RequestSpotInstancesInput{
   232  			SpotPrice:           &spotPrice,
   233  			LaunchSpecification: runOpts,
   234  		})
   235  		if err != nil {
   236  			err := fmt.Errorf("Error launching source spot instance: %s", err)
   237  			state.Put("error", err)
   238  			ui.Error(err.Error())
   239  			return multistep.ActionHalt
   240  		}
   241  
   242  		s.spotRequest = runSpotResp.SpotInstanceRequests[0]
   243  
   244  		spotRequestId := s.spotRequest.SpotInstanceRequestId
   245  		ui.Message(fmt.Sprintf("Waiting for spot request (%s) to become active...", *spotRequestId))
   246  		stateChange := StateChangeConf{
   247  			Pending:   []string{"open"},
   248  			Target:    "active",
   249  			Refresh:   SpotRequestStateRefreshFunc(ec2conn, *spotRequestId),
   250  			StepState: state,
   251  		}
   252  		_, err = WaitForState(&stateChange)
   253  		if err != nil {
   254  			err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", *spotRequestId, err)
   255  			state.Put("error", err)
   256  			ui.Error(err.Error())
   257  			return multistep.ActionHalt
   258  		}
   259  
   260  		spotResp, err := ec2conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{
   261  			SpotInstanceRequestIds: []*string{spotRequestId},
   262  		})
   263  		if err != nil {
   264  			err := fmt.Errorf("Error finding spot request (%s): %s", *spotRequestId, err)
   265  			state.Put("error", err)
   266  			ui.Error(err.Error())
   267  			return multistep.ActionHalt
   268  		}
   269  		instanceId = *spotResp.SpotInstanceRequests[0].InstanceId
   270  	}
   271  
   272  	// Set the instance ID so that the cleanup works properly
   273  	s.instanceId = instanceId
   274  
   275  	ui.Message(fmt.Sprintf("Instance ID: %s", instanceId))
   276  	ui.Say(fmt.Sprintf("Waiting for instance (%v) to become ready...", instanceId))
   277  	stateChange := StateChangeConf{
   278  		Pending:   []string{"pending"},
   279  		Target:    "running",
   280  		Refresh:   InstanceStateRefreshFunc(ec2conn, instanceId),
   281  		StepState: state,
   282  	}
   283  	latestInstance, err := WaitForState(&stateChange)
   284  	if err != nil {
   285  		err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", instanceId, err)
   286  		state.Put("error", err)
   287  		ui.Error(err.Error())
   288  		return multistep.ActionHalt
   289  	}
   290  
   291  	instance := latestInstance.(*ec2.Instance)
   292  
   293  	ec2Tags := make([]*ec2.Tag, 1, len(s.Tags)+1)
   294  	ec2Tags[0] = &ec2.Tag{Key: aws.String("Name"), Value: aws.String("Packer Builder")}
   295  	for k, v := range s.Tags {
   296  		ec2Tags = append(ec2Tags, &ec2.Tag{Key: aws.String(k), Value: aws.String(v)})
   297  	}
   298  
   299  	_, err = ec2conn.CreateTags(&ec2.CreateTagsInput{
   300  		Tags:      ec2Tags,
   301  		Resources: []*string{instance.InstanceId},
   302  	})
   303  	if err != nil {
   304  		ui.Message(
   305  			fmt.Sprintf("Failed to tag a Name on the builder instance: %s", err))
   306  	}
   307  
   308  	if s.Debug {
   309  		if instance.PublicDnsName != nil && *instance.PublicDnsName != "" {
   310  			ui.Message(fmt.Sprintf("Public DNS: %s", *instance.PublicDnsName))
   311  		}
   312  
   313  		if instance.PublicIpAddress != nil && *instance.PublicIpAddress != "" {
   314  			ui.Message(fmt.Sprintf("Public IP: %s", *instance.PublicIpAddress))
   315  		}
   316  
   317  		if instance.PrivateIpAddress != nil && *instance.PrivateIpAddress != "" {
   318  			ui.Message(fmt.Sprintf("Private IP: %s", *instance.PrivateIpAddress))
   319  		}
   320  	}
   321  
   322  	state.Put("instance", instance)
   323  
   324  	return multistep.ActionContinue
   325  }
   326  
   327  func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
   328  
   329  	ec2conn := state.Get("ec2").(*ec2.EC2)
   330  	ui := state.Get("ui").(packer.Ui)
   331  
   332  	// Cancel the spot request if it exists
   333  	if s.spotRequest != nil {
   334  		ui.Say("Cancelling the spot request...")
   335  		input := &ec2.CancelSpotInstanceRequestsInput{
   336  			SpotInstanceRequestIds: []*string{s.spotRequest.SpotInstanceRequestId},
   337  		}
   338  		if _, err := ec2conn.CancelSpotInstanceRequests(input); err != nil {
   339  			ui.Error(fmt.Sprintf("Error cancelling the spot request, may still be around: %s", err))
   340  			return
   341  		}
   342  		stateChange := StateChangeConf{
   343  			Pending: []string{"active", "open"},
   344  			Refresh: SpotRequestStateRefreshFunc(ec2conn, *s.spotRequest.SpotInstanceRequestId),
   345  			Target:  "cancelled",
   346  		}
   347  
   348  		WaitForState(&stateChange)
   349  
   350  	}
   351  
   352  	// Terminate the source instance if it exists
   353  	if s.instanceId != "" {
   354  		ui.Say("Terminating the source AWS instance...")
   355  		if _, err := ec2conn.TerminateInstances(&ec2.TerminateInstancesInput{InstanceIds: []*string{&s.instanceId}}); err != nil {
   356  			ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
   357  			return
   358  		}
   359  		stateChange := StateChangeConf{
   360  			Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
   361  			Refresh: InstanceStateRefreshFunc(ec2conn, s.instanceId),
   362  			Target:  "terminated",
   363  		}
   364  
   365  		WaitForState(&stateChange)
   366  	}
   367  }
   368  
   369  func WaitUntilSecurityGroupExists(c *ec2.EC2, input *ec2.DescribeSecurityGroupsInput) error {
   370  	for i := 0; i < 40; i++ {
   371  		_, err := c.DescribeSecurityGroups(input)
   372  		if err != nil {
   373  			log.Printf("[DEBUG] Error querying security group %v: %s", input.GroupIds, err)
   374  			time.Sleep(15 * time.Second)
   375  			continue
   376  		}
   377  		return nil
   378  	}
   379  	return fmt.Errorf("timed out")
   380  }