github.com/daniellockard/packer@v0.7.6-0.20141210173435-5a9390934716/builder/amazon/common/state.go (about) 1 package common 2 3 import ( 4 "errors" 5 "fmt" 6 "github.com/mitchellh/goamz/ec2" 7 "github.com/mitchellh/multistep" 8 "log" 9 "net" 10 "os" 11 "strconv" 12 "time" 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 // AMIStateRefreshFunc returns a StateRefreshFunc that is used to watch 36 // an AMI for state changes. 37 func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc { 38 return func() (interface{}, string, error) { 39 resp, err := conn.Images([]string{imageId}, ec2.NewFilter()) 40 if err != nil { 41 if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidAMIID.NotFound" { 42 // Set this to nil as if we didn't find anything. 43 resp = nil 44 } else if isTransientNetworkError(err) { 45 // Transient network error, treat it as if we didn't find anything 46 resp = nil 47 } else { 48 log.Printf("Error on AMIStateRefresh: %s", err) 49 return nil, "", err 50 } 51 } 52 53 if resp == nil || len(resp.Images) == 0 { 54 // Sometimes AWS has consistency issues and doesn't see the 55 // AMI. Return an empty state. 56 return nil, "", nil 57 } 58 59 i := resp.Images[0] 60 return i, i.State, nil 61 } 62 } 63 64 // InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch 65 // an EC2 instance. 66 func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc { 67 return func() (interface{}, string, error) { 68 resp, err := conn.Instances([]string{i.InstanceId}, ec2.NewFilter()) 69 if err != nil { 70 if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" { 71 // Set this to nil as if we didn't find anything. 72 resp = nil 73 } else if isTransientNetworkError(err) { 74 // Transient network error, treat it as if we didn't find anything 75 resp = nil 76 } else { 77 log.Printf("Error on InstanceStateRefresh: %s", err) 78 return nil, "", err 79 } 80 } 81 82 if resp == nil || len(resp.Reservations) == 0 || len(resp.Reservations[0].Instances) == 0 { 83 // Sometimes AWS just has consistency issues and doesn't see 84 // our instance yet. Return an empty state. 85 return nil, "", nil 86 } 87 88 i = &resp.Reservations[0].Instances[0] 89 return i, i.State.Name, nil 90 } 91 } 92 93 // SpotRequestStateRefreshFunc returns a StateRefreshFunc that is used to watch 94 // a spot request for state changes. 95 func SpotRequestStateRefreshFunc(conn *ec2.EC2, spotRequestId string) StateRefreshFunc { 96 return func() (interface{}, string, error) { 97 resp, err := conn.DescribeSpotRequests([]string{spotRequestId}, ec2.NewFilter()) 98 if err != nil { 99 if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidSpotInstanceRequestID.NotFound" { 100 // Set this to nil as if we didn't find anything. 101 resp = nil 102 } else if isTransientNetworkError(err) { 103 // Transient network error, treat it as if we didn't find anything 104 resp = nil 105 } else { 106 log.Printf("Error on SpotRequestStateRefresh: %s", err) 107 return nil, "", err 108 } 109 } 110 111 if resp == nil || len(resp.SpotRequestResults) == 0 { 112 // Sometimes AWS has consistency issues and doesn't see the 113 // SpotRequest. Return an empty state. 114 return nil, "", nil 115 } 116 117 i := resp.SpotRequestResults[0] 118 return i, i.State, nil 119 } 120 } 121 122 // WaitForState watches an object and waits for it to achieve a certain 123 // state. 124 func WaitForState(conf *StateChangeConf) (i interface{}, err error) { 125 log.Printf("Waiting for state to become: %s", conf.Target) 126 127 sleepSeconds := 2 128 maxTicks := int(TimeoutSeconds()/sleepSeconds) + 1 129 notfoundTick := 0 130 131 for { 132 var currentState string 133 i, currentState, err = conf.Refresh() 134 if err != nil { 135 return 136 } 137 138 if i == nil { 139 // If we didn't find the resource, check if we have been 140 // not finding it for awhile, and if so, report an error. 141 notfoundTick += 1 142 if notfoundTick > maxTicks { 143 return nil, errors.New("couldn't find resource") 144 } 145 } else { 146 // Reset the counter for when a resource isn't found 147 notfoundTick = 0 148 149 if currentState == conf.Target { 150 return 151 } 152 153 if conf.StepState != nil { 154 if _, ok := conf.StepState.GetOk(multistep.StateCancelled); ok { 155 return nil, errors.New("interrupted") 156 } 157 } 158 159 found := false 160 for _, allowed := range conf.Pending { 161 if currentState == allowed { 162 found = true 163 break 164 } 165 } 166 167 if !found { 168 err := fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target) 169 return nil, err 170 } 171 } 172 173 time.Sleep(time.Duration(sleepSeconds) * time.Second) 174 } 175 176 return 177 } 178 179 func isTransientNetworkError(err error) bool { 180 if nerr, ok := err.(net.Error); ok && nerr.Temporary() { 181 return true 182 } 183 184 return false 185 } 186 187 // Returns 300 seconds (5 minutes) by default 188 // Some AWS operations, like copying an AMI to a distant region, take a very long time 189 // Allow user to override with AWS_TIMEOUT_SECONDS environment variable 190 func TimeoutSeconds() (seconds int) { 191 seconds = 300 192 193 override := os.Getenv("AWS_TIMEOUT_SECONDS") 194 if override != "" { 195 n, err := strconv.Atoi(override) 196 if err != nil { 197 log.Printf("Invalid timeout seconds '%s', using default", override) 198 } else { 199 seconds = n 200 } 201 } 202 203 log.Printf("Allowing %ds to complete (change with AWS_TIMEOUT_SECONDS)", seconds) 204 return seconds 205 }