github.com/amanya/packer@v0.12.1-0.20161117214323-902ac5ab2eb6/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  			NetworkInterfaces: []*ec2.InstanceNetworkInterfaceSpecification{
   206  				{
   207  					DeviceIndex:              aws.Int64(0),
   208  					AssociatePublicIpAddress: &s.AssociatePublicIpAddress,
   209  					SubnetId:                 &s.SubnetId,
   210  					Groups:                   securityGroupIds,
   211  					DeleteOnTermination:      aws.Bool(true),
   212  				},
   213  			},
   214  			Placement: &ec2.SpotPlacement{
   215  				AvailabilityZone: &availabilityZone,
   216  			},
   217  			BlockDeviceMappings: s.BlockDevices.BuildLaunchDevices(),
   218  			EbsOptimized:        &s.EbsOptimized,
   219  		}
   220  
   221  		if keyName != "" {
   222  			runOpts.KeyName = &keyName
   223  		}
   224  
   225  		runSpotResp, err := ec2conn.RequestSpotInstances(&ec2.RequestSpotInstancesInput{
   226  			SpotPrice:           &spotPrice,
   227  			LaunchSpecification: runOpts,
   228  		})
   229  		if err != nil {
   230  			err := fmt.Errorf("Error launching source spot instance: %s", err)
   231  			state.Put("error", err)
   232  			ui.Error(err.Error())
   233  			return multistep.ActionHalt
   234  		}
   235  
   236  		s.spotRequest = runSpotResp.SpotInstanceRequests[0]
   237  
   238  		spotRequestId := s.spotRequest.SpotInstanceRequestId
   239  		ui.Message(fmt.Sprintf("Waiting for spot request (%s) to become active...", *spotRequestId))
   240  		stateChange := StateChangeConf{
   241  			Pending:   []string{"open"},
   242  			Target:    "active",
   243  			Refresh:   SpotRequestStateRefreshFunc(ec2conn, *spotRequestId),
   244  			StepState: state,
   245  		}
   246  		_, err = WaitForState(&stateChange)
   247  		if err != nil {
   248  			err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", *spotRequestId, err)
   249  			state.Put("error", err)
   250  			ui.Error(err.Error())
   251  			return multistep.ActionHalt
   252  		}
   253  
   254  		spotResp, err := ec2conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{
   255  			SpotInstanceRequestIds: []*string{spotRequestId},
   256  		})
   257  		if err != nil {
   258  			err := fmt.Errorf("Error finding spot request (%s): %s", *spotRequestId, err)
   259  			state.Put("error", err)
   260  			ui.Error(err.Error())
   261  			return multistep.ActionHalt
   262  		}
   263  		instanceId = *spotResp.SpotInstanceRequests[0].InstanceId
   264  	}
   265  
   266  	// Set the instance ID so that the cleanup works properly
   267  	s.instanceId = instanceId
   268  
   269  	ui.Message(fmt.Sprintf("Instance ID: %s", instanceId))
   270  	ui.Say(fmt.Sprintf("Waiting for instance (%v) to become ready...", instanceId))
   271  	stateChange := StateChangeConf{
   272  		Pending:   []string{"pending"},
   273  		Target:    "running",
   274  		Refresh:   InstanceStateRefreshFunc(ec2conn, instanceId),
   275  		StepState: state,
   276  	}
   277  	latestInstance, err := WaitForState(&stateChange)
   278  	if err != nil {
   279  		err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", instanceId, err)
   280  		state.Put("error", err)
   281  		ui.Error(err.Error())
   282  		return multistep.ActionHalt
   283  	}
   284  
   285  	instance := latestInstance.(*ec2.Instance)
   286  
   287  	ec2Tags := make([]*ec2.Tag, 1, len(s.Tags)+1)
   288  	ec2Tags[0] = &ec2.Tag{Key: aws.String("Name"), Value: aws.String("Packer Builder")}
   289  	for k, v := range s.Tags {
   290  		ec2Tags = append(ec2Tags, &ec2.Tag{Key: aws.String(k), Value: aws.String(v)})
   291  	}
   292  
   293  	_, err = ec2conn.CreateTags(&ec2.CreateTagsInput{
   294  		Tags:      ec2Tags,
   295  		Resources: []*string{instance.InstanceId},
   296  	})
   297  	if err != nil {
   298  		ui.Message(
   299  			fmt.Sprintf("Failed to tag a Name on the builder instance: %s", err))
   300  	}
   301  
   302  	if s.Debug {
   303  		if instance.PublicDnsName != nil && *instance.PublicDnsName != "" {
   304  			ui.Message(fmt.Sprintf("Public DNS: %s", *instance.PublicDnsName))
   305  		}
   306  
   307  		if instance.PublicIpAddress != nil && *instance.PublicIpAddress != "" {
   308  			ui.Message(fmt.Sprintf("Public IP: %s", *instance.PublicIpAddress))
   309  		}
   310  
   311  		if instance.PrivateIpAddress != nil && *instance.PrivateIpAddress != "" {
   312  			ui.Message(fmt.Sprintf("Private IP: %s", *instance.PrivateIpAddress))
   313  		}
   314  	}
   315  
   316  	state.Put("instance", instance)
   317  
   318  	return multistep.ActionContinue
   319  }
   320  
   321  func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
   322  
   323  	ec2conn := state.Get("ec2").(*ec2.EC2)
   324  	ui := state.Get("ui").(packer.Ui)
   325  
   326  	// Cancel the spot request if it exists
   327  	if s.spotRequest != nil {
   328  		ui.Say("Cancelling the spot request...")
   329  		input := &ec2.CancelSpotInstanceRequestsInput{
   330  			SpotInstanceRequestIds: []*string{s.spotRequest.SpotInstanceRequestId},
   331  		}
   332  		if _, err := ec2conn.CancelSpotInstanceRequests(input); err != nil {
   333  			ui.Error(fmt.Sprintf("Error cancelling the spot request, may still be around: %s", err))
   334  			return
   335  		}
   336  		stateChange := StateChangeConf{
   337  			Pending: []string{"active", "open"},
   338  			Refresh: SpotRequestStateRefreshFunc(ec2conn, *s.spotRequest.SpotInstanceRequestId),
   339  			Target:  "cancelled",
   340  		}
   341  
   342  		WaitForState(&stateChange)
   343  
   344  	}
   345  
   346  	// Terminate the source instance if it exists
   347  	if s.instanceId != "" {
   348  		ui.Say("Terminating the source AWS instance...")
   349  		if _, err := ec2conn.TerminateInstances(&ec2.TerminateInstancesInput{InstanceIds: []*string{&s.instanceId}}); err != nil {
   350  			ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
   351  			return
   352  		}
   353  		stateChange := StateChangeConf{
   354  			Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
   355  			Refresh: InstanceStateRefreshFunc(ec2conn, s.instanceId),
   356  			Target:  "terminated",
   357  		}
   358  
   359  		WaitForState(&stateChange)
   360  	}
   361  }
   362  
   363  func WaitUntilSecurityGroupExists(c *ec2.EC2, input *ec2.DescribeSecurityGroupsInput) error {
   364  	for i := 0; i < 40; i++ {
   365  		_, err := c.DescribeSecurityGroups(input)
   366  		if err != nil {
   367  			log.Printf("[DEBUG] Error querying security group %v: %s", input.GroupIds, err)
   368  			time.Sleep(15 * time.Second)
   369  			continue
   370  		}
   371  		return nil
   372  	}
   373  	return fmt.Errorf("timed out")
   374  }