github.com/rothwerx/packer@v0.9.0/builder/amazon/common/state.go (about) 1 package common 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "net" 8 "os" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/aws/aws-sdk-go/aws/awserr" 14 "github.com/aws/aws-sdk-go/service/ec2" 15 "github.com/mitchellh/multistep" 16 ) 17 18 // StateRefreshFunc is a function type used for StateChangeConf that is 19 // responsible for refreshing the item being watched for a state change. 20 // 21 // It returns three results. `result` is any object that will be returned 22 // as the final object after waiting for state change. This allows you to 23 // return the final updated object, for example an EC2 instance after refreshing 24 // it. 25 // 26 // `state` is the latest state of that object. And `err` is any error that 27 // may have happened while refreshing the state. 28 type StateRefreshFunc func() (result interface{}, state string, err error) 29 30 // StateChangeConf is the configuration struct used for `WaitForState`. 31 type StateChangeConf struct { 32 Pending []string 33 Refresh StateRefreshFunc 34 StepState multistep.StateBag 35 Target string 36 } 37 38 // AMIStateRefreshFunc returns a StateRefreshFunc that is used to watch 39 // an AMI for state changes. 40 func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc { 41 return func() (interface{}, string, error) { 42 resp, err := conn.DescribeImages(&ec2.DescribeImagesInput{ 43 ImageIds: []*string{&imageId}, 44 }) 45 if err != nil { 46 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAMIID.NotFound" { 47 // Set this to nil as if we didn't find anything. 48 resp = nil 49 } else if isTransientNetworkError(err) { 50 // Transient network error, treat it as if we didn't find anything 51 resp = nil 52 } else { 53 log.Printf("Error on AMIStateRefresh: %s", err) 54 return nil, "", err 55 } 56 } 57 58 if resp == nil || len(resp.Images) == 0 { 59 // Sometimes AWS has consistency issues and doesn't see the 60 // AMI. Return an empty state. 61 return nil, "", nil 62 } 63 64 i := resp.Images[0] 65 return i, *i.State, nil 66 } 67 } 68 69 // InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch 70 // an EC2 instance. 71 func InstanceStateRefreshFunc(conn *ec2.EC2, instanceId string) StateRefreshFunc { 72 return func() (interface{}, string, error) { 73 resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{ 74 InstanceIds: []*string{&instanceId}, 75 }) 76 if err != nil { 77 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" { 78 // Set this to nil as if we didn't find anything. 79 resp = nil 80 } else if isTransientNetworkError(err) { 81 // Transient network error, treat it as if we didn't find anything 82 resp = nil 83 } else { 84 log.Printf("Error on InstanceStateRefresh: %s", err) 85 return nil, "", err 86 } 87 } 88 89 if resp == nil || len(resp.Reservations) == 0 || len(resp.Reservations[0].Instances) == 0 { 90 // Sometimes AWS just has consistency issues and doesn't see 91 // our instance yet. Return an empty state. 92 return nil, "", nil 93 } 94 95 i := resp.Reservations[0].Instances[0] 96 return i, *i.State.Name, nil 97 } 98 } 99 100 // SpotRequestStateRefreshFunc returns a StateRefreshFunc that is used to watch 101 // a spot request for state changes. 102 func SpotRequestStateRefreshFunc(conn *ec2.EC2, spotRequestId string) StateRefreshFunc { 103 return func() (interface{}, string, error) { 104 resp, err := conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{ 105 SpotInstanceRequestIds: []*string{&spotRequestId}, 106 }) 107 108 if err != nil { 109 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" { 110 // Set this to nil as if we didn't find anything. 111 resp = nil 112 } else if isTransientNetworkError(err) { 113 // Transient network error, treat it as if we didn't find anything 114 resp = nil 115 } else { 116 log.Printf("Error on SpotRequestStateRefresh: %s", err) 117 return nil, "", err 118 } 119 } 120 121 if resp == nil || len(resp.SpotInstanceRequests) == 0 { 122 // Sometimes AWS has consistency issues and doesn't see the 123 // SpotRequest. Return an empty state. 124 return nil, "", nil 125 } 126 127 i := resp.SpotInstanceRequests[0] 128 return i, *i.State, nil 129 } 130 } 131 132 func ImportImageRefreshFunc(conn *ec2.EC2, importTaskId string) StateRefreshFunc { 133 return func() (interface{}, string, error) { 134 resp, err := conn.DescribeImportImageTasks(&ec2.DescribeImportImageTasksInput{ 135 ImportTaskIds: []*string{ 136 &importTaskId, 137 }, 138 }, 139 ) 140 if err != nil { 141 if ec2err, ok := err.(awserr.Error); ok && strings.HasPrefix(ec2err.Code(), "InvalidConversionTaskId") { 142 resp = nil 143 } else if isTransientNetworkError(err) { 144 resp = nil 145 } else { 146 log.Printf("Error on ImportImageRefresh: %s", err) 147 return nil, "", err 148 } 149 } 150 151 if resp == nil || len(resp.ImportImageTasks) == 0 { 152 return nil, "", nil 153 } 154 155 i := resp.ImportImageTasks[0] 156 return i, *i.Status, nil 157 } 158 } 159 160 // WaitForState watches an object and waits for it to achieve a certain 161 // state. 162 func WaitForState(conf *StateChangeConf) (i interface{}, err error) { 163 log.Printf("Waiting for state to become: %s", conf.Target) 164 165 sleepSeconds := 2 166 maxTicks := int(TimeoutSeconds()/sleepSeconds) + 1 167 notfoundTick := 0 168 169 for { 170 var currentState string 171 i, currentState, err = conf.Refresh() 172 if err != nil { 173 return 174 } 175 176 if i == nil { 177 // If we didn't find the resource, check if we have been 178 // not finding it for awhile, and if so, report an error. 179 notfoundTick += 1 180 if notfoundTick > maxTicks { 181 return nil, errors.New("couldn't find resource") 182 } 183 } else { 184 // Reset the counter for when a resource isn't found 185 notfoundTick = 0 186 187 if currentState == conf.Target { 188 return 189 } 190 191 if conf.StepState != nil { 192 if _, ok := conf.StepState.GetOk(multistep.StateCancelled); ok { 193 return nil, errors.New("interrupted") 194 } 195 } 196 197 found := false 198 for _, allowed := range conf.Pending { 199 if currentState == allowed { 200 found = true 201 break 202 } 203 } 204 205 if !found { 206 err := fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target) 207 return nil, err 208 } 209 } 210 211 time.Sleep(time.Duration(sleepSeconds) * time.Second) 212 } 213 } 214 215 func isTransientNetworkError(err error) bool { 216 if nerr, ok := err.(net.Error); ok && nerr.Temporary() { 217 return true 218 } 219 220 return false 221 } 222 223 // Returns 300 seconds (5 minutes) by default 224 // Some AWS operations, like copying an AMI to a distant region, take a very long time 225 // Allow user to override with AWS_TIMEOUT_SECONDS environment variable 226 func TimeoutSeconds() (seconds int) { 227 seconds = 300 228 229 override := os.Getenv("AWS_TIMEOUT_SECONDS") 230 if override != "" { 231 n, err := strconv.Atoi(override) 232 if err != nil { 233 log.Printf("Invalid timeout seconds '%s', using default", override) 234 } else { 235 seconds = n 236 } 237 } 238 239 log.Printf("Allowing %ds to complete (change with AWS_TIMEOUT_SECONDS)", seconds) 240 return seconds 241 }