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 }