github.phpd.cn/hashicorp/packer@v1.3.2/builder/amazon/common/state.go (about) 1 package common 2 3 import ( 4 "log" 5 "os" 6 "strconv" 7 "time" 8 9 "github.com/aws/aws-sdk-go/aws" 10 "github.com/aws/aws-sdk-go/aws/request" 11 "github.com/aws/aws-sdk-go/service/ec2" 12 "github.com/hashicorp/packer/helper/multistep" 13 ) 14 15 // StateRefreshFunc is a function type used for StateChangeConf that is 16 // responsible for refreshing the item being watched for a state change. 17 // 18 // It returns three results. `result` is any object that will be returned 19 // as the final object after waiting for state change. This allows you to 20 // return the final updated object, for example an EC2 instance after refreshing 21 // it. 22 // 23 // `state` is the latest state of that object. And `err` is any error that 24 // may have happened while refreshing the state. 25 type StateRefreshFunc func() (result interface{}, state string, err error) 26 27 // StateChangeConf is the configuration struct used for `WaitForState`. 28 type StateChangeConf struct { 29 Pending []string 30 Refresh StateRefreshFunc 31 StepState multistep.StateBag 32 Target string 33 } 34 35 // Following are wrapper functions that use Packer's environment-variables to 36 // determing retry logic, then call the AWS SDK's built-in waiters. 37 38 func WaitUntilAMIAvailable(ctx aws.Context, conn *ec2.EC2, imageId string) error { 39 imageInput := ec2.DescribeImagesInput{ 40 ImageIds: []*string{&imageId}, 41 } 42 43 waitOpts := getWaiterOptions() 44 if len(waitOpts) == 0 { 45 // Bump this default to 30 minutes because the aws default 46 // of ten minutes doesn't work for some of our long-running copies. 47 waitOpts = append(waitOpts, request.WithWaiterMaxAttempts(120)) 48 } 49 err := conn.WaitUntilImageAvailableWithContext( 50 ctx, 51 &imageInput, 52 waitOpts...) 53 return err 54 } 55 56 func WaitUntilInstanceTerminated(ctx aws.Context, conn *ec2.EC2, instanceId string) error { 57 58 instanceInput := ec2.DescribeInstancesInput{ 59 InstanceIds: []*string{&instanceId}, 60 } 61 62 err := conn.WaitUntilInstanceTerminatedWithContext( 63 ctx, 64 &instanceInput, 65 getWaiterOptions()...) 66 return err 67 } 68 69 // This function works for both requesting and cancelling spot instances. 70 func WaitUntilSpotRequestFulfilled(ctx aws.Context, conn *ec2.EC2, spotRequestId string) error { 71 spotRequestInput := ec2.DescribeSpotInstanceRequestsInput{ 72 SpotInstanceRequestIds: []*string{&spotRequestId}, 73 } 74 75 err := conn.WaitUntilSpotInstanceRequestFulfilledWithContext( 76 ctx, 77 &spotRequestInput, 78 getWaiterOptions()...) 79 return err 80 } 81 82 func WaitUntilVolumeAvailable(ctx aws.Context, conn *ec2.EC2, volumeId string) error { 83 volumeInput := ec2.DescribeVolumesInput{ 84 VolumeIds: []*string{&volumeId}, 85 } 86 87 err := conn.WaitUntilVolumeAvailableWithContext( 88 ctx, 89 &volumeInput, 90 getWaiterOptions()...) 91 return err 92 } 93 94 func WaitUntilSnapshotDone(ctx aws.Context, conn *ec2.EC2, snapshotID string) error { 95 snapInput := ec2.DescribeSnapshotsInput{ 96 SnapshotIds: []*string{&snapshotID}, 97 } 98 99 err := conn.WaitUntilSnapshotCompletedWithContext( 100 ctx, 101 &snapInput, 102 getWaiterOptions()...) 103 return err 104 } 105 106 // Wrappers for our custom AWS waiters 107 108 func WaitUntilVolumeAttached(ctx aws.Context, conn *ec2.EC2, volumeId string) error { 109 volumeInput := ec2.DescribeVolumesInput{ 110 VolumeIds: []*string{&volumeId}, 111 } 112 113 err := WaitForVolumeToBeAttached(conn, 114 ctx, 115 &volumeInput, 116 getWaiterOptions()...) 117 return err 118 } 119 120 func WaitUntilVolumeDetached(ctx aws.Context, conn *ec2.EC2, volumeId string) error { 121 volumeInput := ec2.DescribeVolumesInput{ 122 VolumeIds: []*string{&volumeId}, 123 } 124 125 err := WaitForVolumeToBeDetached(conn, 126 ctx, 127 &volumeInput, 128 getWaiterOptions()...) 129 return err 130 } 131 132 func WaitUntilImageImported(ctx aws.Context, conn *ec2.EC2, taskID string) error { 133 importInput := ec2.DescribeImportImageTasksInput{ 134 ImportTaskIds: []*string{&taskID}, 135 } 136 137 err := WaitForImageToBeImported(conn, 138 ctx, 139 &importInput, 140 getWaiterOptions()...) 141 return err 142 } 143 144 // Custom waiters using AWS's request.Waiter 145 146 func WaitForVolumeToBeAttached(c *ec2.EC2, ctx aws.Context, input *ec2.DescribeVolumesInput, opts ...request.WaiterOption) error { 147 w := request.Waiter{ 148 Name: "DescribeVolumes", 149 MaxAttempts: 40, 150 Delay: request.ConstantWaiterDelay(5 * time.Second), 151 Acceptors: []request.WaiterAcceptor{ 152 { 153 State: request.SuccessWaiterState, 154 Matcher: request.PathAllWaiterMatch, 155 Argument: "Volumes[].Attachments[].State", 156 Expected: "attached", 157 }, 158 }, 159 Logger: c.Config.Logger, 160 NewRequest: func(opts []request.Option) (*request.Request, error) { 161 var inCpy *ec2.DescribeVolumesInput 162 if input != nil { 163 tmp := *input 164 inCpy = &tmp 165 } 166 req, _ := c.DescribeVolumesRequest(inCpy) 167 req.SetContext(ctx) 168 req.ApplyOptions(opts...) 169 return req, nil 170 }, 171 } 172 w.ApplyOptions(opts...) 173 174 return w.WaitWithContext(ctx) 175 } 176 177 func WaitForVolumeToBeDetached(c *ec2.EC2, ctx aws.Context, input *ec2.DescribeVolumesInput, opts ...request.WaiterOption) error { 178 w := request.Waiter{ 179 Name: "DescribeVolumes", 180 MaxAttempts: 40, 181 Delay: request.ConstantWaiterDelay(5 * time.Second), 182 Acceptors: []request.WaiterAcceptor{ 183 { 184 State: request.SuccessWaiterState, 185 Matcher: request.PathAllWaiterMatch, 186 Argument: "length(Volumes[].Attachments[]) == `0`", 187 Expected: true, 188 }, 189 }, 190 Logger: c.Config.Logger, 191 NewRequest: func(opts []request.Option) (*request.Request, error) { 192 var inCpy *ec2.DescribeVolumesInput 193 if input != nil { 194 tmp := *input 195 inCpy = &tmp 196 } 197 req, _ := c.DescribeVolumesRequest(inCpy) 198 req.SetContext(ctx) 199 req.ApplyOptions(opts...) 200 return req, nil 201 }, 202 } 203 w.ApplyOptions(opts...) 204 205 return w.WaitWithContext(ctx) 206 } 207 208 func WaitForImageToBeImported(c *ec2.EC2, ctx aws.Context, input *ec2.DescribeImportImageTasksInput, opts ...request.WaiterOption) error { 209 w := request.Waiter{ 210 Name: "DescribeImages", 211 MaxAttempts: 720, 212 Delay: request.ConstantWaiterDelay(5 * time.Second), 213 Acceptors: []request.WaiterAcceptor{ 214 { 215 State: request.SuccessWaiterState, 216 Matcher: request.PathAllWaiterMatch, 217 Argument: "ImportImageTasks[].Status", 218 Expected: "completed", 219 }, 220 { 221 State: request.FailureWaiterState, 222 Matcher: request.PathAnyWaiterMatch, 223 Argument: "ImportImageTasks[].Status", 224 Expected: "deleted", 225 }, 226 }, 227 Logger: c.Config.Logger, 228 NewRequest: func(opts []request.Option) (*request.Request, error) { 229 var inCpy *ec2.DescribeImportImageTasksInput 230 if input != nil { 231 tmp := *input 232 inCpy = &tmp 233 } 234 req, _ := c.DescribeImportImageTasksRequest(inCpy) 235 req.SetContext(ctx) 236 req.ApplyOptions(opts...) 237 return req, nil 238 }, 239 } 240 w.ApplyOptions(opts...) 241 242 return w.WaitWithContext(ctx) 243 } 244 245 // This helper function uses the environment variables AWS_TIMEOUT_SECONDS and 246 // AWS_POLL_DELAY_SECONDS to generate waiter options that can be passed into any 247 // request.Waiter function. These options will control how many times the waiter 248 // will retry the request, as well as how long to wait between the retries. 249 250 // DEFAULTING BEHAVIOR: 251 // if AWS_POLL_DELAY_SECONDS is set but the others are not, Packer will set this 252 // poll delay and use the waiter-specific default 253 254 // if AWS_TIMEOUT_SECONDS is set but AWS_MAX_ATTEMPTS is not, Packer will use 255 // AWS_TIMEOUT_SECONDS and _either_ AWS_POLL_DELAY_SECONDS _or_ 2 if the user has not set AWS_POLL_DELAY_SECONDS, to determine a max number of attempts to make. 256 257 // if AWS_TIMEOUT_SECONDS, _and_ AWS_MAX_ATTEMPTS are both set, 258 // AWS_TIMEOUT_SECONDS will be ignored. 259 260 // if AWS_MAX_ATTEMPTS is set but AWS_POLL_DELAY_SECONDS is not, then we will 261 // use waiter-specific defaults. 262 263 type envInfo struct { 264 envKey string 265 Val int 266 overridden bool 267 } 268 269 type overridableWaitVars struct { 270 awsPollDelaySeconds envInfo 271 awsMaxAttempts envInfo 272 awsTimeoutSeconds envInfo 273 } 274 275 func getWaiterOptions() []request.WaiterOption { 276 envOverrides := getEnvOverrides() 277 waitOpts := applyEnvOverrides(envOverrides) 278 return waitOpts 279 } 280 281 func getOverride(varInfo envInfo) envInfo { 282 override := os.Getenv(varInfo.envKey) 283 if override != "" { 284 n, err := strconv.Atoi(override) 285 if err != nil { 286 log.Printf("Invalid %s '%s', using default", varInfo.envKey, override) 287 } else { 288 varInfo.overridden = true 289 varInfo.Val = n 290 } 291 } 292 293 return varInfo 294 } 295 func getEnvOverrides() overridableWaitVars { 296 // Load env vars from environment. 297 envValues := overridableWaitVars{ 298 envInfo{"AWS_POLL_DELAY_SECONDS", 2, false}, 299 envInfo{"AWS_MAX_ATTEMPTS", 0, false}, 300 envInfo{"AWS_TIMEOUT_SECONDS", 0, false}, 301 } 302 303 envValues.awsMaxAttempts = getOverride(envValues.awsMaxAttempts) 304 envValues.awsPollDelaySeconds = getOverride(envValues.awsPollDelaySeconds) 305 envValues.awsTimeoutSeconds = getOverride(envValues.awsTimeoutSeconds) 306 307 return envValues 308 } 309 310 func applyEnvOverrides(envOverrides overridableWaitVars) []request.WaiterOption { 311 waitOpts := make([]request.WaiterOption, 0) 312 // If user has set poll delay seconds, overwrite it. If user has NOT, 313 // default to a poll delay of 2 seconds 314 if envOverrides.awsPollDelaySeconds.overridden { 315 delaySeconds := request.ConstantWaiterDelay(time.Duration(envOverrides.awsPollDelaySeconds.Val) * time.Second) 316 waitOpts = append(waitOpts, request.WithWaiterDelay(delaySeconds)) 317 } 318 319 // If user has set max attempts, overwrite it. If user hasn't set max 320 // attempts, default to whatever the waiter has set as a default. 321 if envOverrides.awsMaxAttempts.overridden { 322 waitOpts = append(waitOpts, request.WithWaiterMaxAttempts(envOverrides.awsMaxAttempts.Val)) 323 } 324 325 if envOverrides.awsMaxAttempts.overridden && envOverrides.awsTimeoutSeconds.overridden { 326 log.Printf("WARNING: AWS_MAX_ATTEMPTS and AWS_TIMEOUT_SECONDS are" + 327 " both set. Packer will be using AWS_MAX_ATTEMPTS and discarding " + 328 "AWS_TIMEOUT_SECONDS. If you have not set AWS_POLL_DELAY_SECONDS, " + 329 "Packer will default to a 2 second poll delay.") 330 } else if envOverrides.awsTimeoutSeconds.overridden { 331 log.Printf("DEPRECATION WARNING: env var AWS_TIMEOUT_SECONDS is " + 332 "deprecated in favor of AWS_MAX_ATTEMPTS. If you have not " + 333 "explicitly set AWS_POLL_DELAY_SECONDS, we are defaulting to a " + 334 "poll delay of 2 seconds, regardless of the AWS waiter's default.") 335 maxAttempts := envOverrides.awsTimeoutSeconds.Val / envOverrides.awsPollDelaySeconds.Val 336 // override the delay so we can get the timeout right 337 if !envOverrides.awsPollDelaySeconds.overridden { 338 delaySeconds := request.ConstantWaiterDelay(time.Duration(envOverrides.awsPollDelaySeconds.Val) * time.Second) 339 waitOpts = append(waitOpts, request.WithWaiterDelay(delaySeconds)) 340 } 341 waitOpts = append(waitOpts, request.WithWaiterMaxAttempts(maxAttempts)) 342 } 343 if len(waitOpts) == 0 { 344 log.Printf("No AWS timeout and polling overrides have been set. " + 345 "Packer will default to waiter-specific delays and timeouts. If you would " + 346 "like to customize the length of time between retries and max " + 347 "number of retries you may do so by setting the environment " + 348 "variables AWS_POLL_DELAY_SECONDS and AWS_MAX_ATTEMPTS to your " + 349 "desired values.") 350 } 351 352 return waitOpts 353 }