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