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