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 }