github.com/coreos/mantle@v0.13.0/platform/api/aws/ec2.go (about) 1 // Copyright 2016 CoreOS, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package aws 16 17 import ( 18 "encoding/base64" 19 "fmt" 20 "strings" 21 "time" 22 23 "github.com/aws/aws-sdk-go/aws" 24 "github.com/aws/aws-sdk-go/aws/awserr" 25 "github.com/aws/aws-sdk-go/service/ec2" 26 27 "github.com/coreos/mantle/util" 28 ) 29 30 func (a *API) AddKey(name, key string) error { 31 _, err := a.ec2.ImportKeyPair(&ec2.ImportKeyPairInput{ 32 KeyName: &name, 33 PublicKeyMaterial: []byte(key), 34 }) 35 36 return err 37 } 38 39 func (a *API) DeleteKey(name string) error { 40 _, err := a.ec2.DeleteKeyPair(&ec2.DeleteKeyPairInput{ 41 KeyName: &name, 42 }) 43 44 return err 45 } 46 47 // CreateInstances creates EC2 instances with a given name tag, optional ssh key name, user data. The image ID, instance type, and security group set in the API will be used. CreateInstances will block until all instances are running and have an IP address. 48 func (a *API) CreateInstances(name, keyname, userdata string, count uint64) ([]*ec2.Instance, error) { 49 cnt := int64(count) 50 51 var ud *string 52 if len(userdata) > 0 { 53 tud := base64.StdEncoding.EncodeToString([]byte(userdata)) 54 ud = &tud 55 } 56 57 err := a.ensureInstanceProfile(a.opts.IAMInstanceProfile) 58 if err != nil { 59 return nil, fmt.Errorf("error verifying IAM instance profile: %v", err) 60 } 61 62 sgId, err := a.getSecurityGroupID(a.opts.SecurityGroup) 63 if err != nil { 64 return nil, fmt.Errorf("error resolving security group: %v", err) 65 } 66 67 vpcId, err := a.getVPCID(sgId) 68 if err != nil { 69 return nil, fmt.Errorf("error resolving vpc: %v", err) 70 } 71 72 subnetId, err := a.getSubnetID(vpcId) 73 if err != nil { 74 return nil, fmt.Errorf("error resolving subnet: %v", err) 75 } 76 77 key := &keyname 78 if keyname == "" { 79 key = nil 80 } 81 inst := ec2.RunInstancesInput{ 82 ImageId: &a.opts.AMI, 83 MinCount: &cnt, 84 MaxCount: &cnt, 85 KeyName: key, 86 InstanceType: &a.opts.InstanceType, 87 SecurityGroupIds: []*string{&sgId}, 88 SubnetId: &subnetId, 89 UserData: ud, 90 IamInstanceProfile: &ec2.IamInstanceProfileSpecification{ 91 Name: &a.opts.IAMInstanceProfile, 92 }, 93 TagSpecifications: []*ec2.TagSpecification{ 94 &ec2.TagSpecification{ 95 ResourceType: aws.String(ec2.ResourceTypeInstance), 96 Tags: []*ec2.Tag{ 97 &ec2.Tag{ 98 Key: aws.String("Name"), 99 Value: aws.String(name), 100 }, 101 &ec2.Tag{ 102 Key: aws.String("CreatedBy"), 103 Value: aws.String("mantle"), 104 }, 105 }, 106 }, 107 }, 108 } 109 110 var reservations *ec2.Reservation 111 err = util.RetryConditional(5, 5*time.Second, func(err error) bool { 112 // due to AWS' eventual consistency despite ensuring that the IAM Instance 113 // Profile has been created it may not be available to ec2 yet. 114 if awsErr, ok := err.(awserr.Error); ok && (awsErr.Code() == "InvalidParameterValue" && strings.Contains(awsErr.Message(), "iamInstanceProfile.name")) { 115 return true 116 } 117 return false 118 }, func() error { 119 var ierr error 120 reservations, ierr = a.ec2.RunInstances(&inst) 121 return ierr 122 }) 123 if err != nil { 124 return nil, fmt.Errorf("error running instances: %v", err) 125 } 126 127 ids := make([]string, len(reservations.Instances)) 128 for i, inst := range reservations.Instances { 129 ids[i] = *inst.InstanceId 130 } 131 132 // loop until all machines are online 133 var insts []*ec2.Instance 134 135 // 10 minutes is a pretty reasonable timeframe for AWS instances to work. 136 timeout := 10 * time.Minute 137 // don't make api calls too quickly, or we will hit the rate limit 138 delay := 10 * time.Second 139 err = util.WaitUntilReady(timeout, delay, func() (bool, error) { 140 desc, err := a.ec2.DescribeInstances(&ec2.DescribeInstancesInput{ 141 InstanceIds: aws.StringSlice(ids), 142 }) 143 if err != nil { 144 // Keep retrying if the InstanceID disappears momentarily 145 if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "InvalidInstanceID.NotFound" { 146 plog.Debugf("instance ID not found, retrying: %v", err) 147 return false, nil 148 } 149 return false, err 150 } 151 insts = desc.Reservations[0].Instances 152 153 for _, i := range insts { 154 if *i.State.Name != ec2.InstanceStateNameRunning || i.PublicIpAddress == nil { 155 return false, nil 156 } 157 } 158 return true, nil 159 }) 160 if err != nil { 161 a.TerminateInstances(ids) 162 return nil, fmt.Errorf("waiting for instances to run: %v", err) 163 } 164 165 return insts, nil 166 } 167 168 // gcEC2 will terminate ec2 instances older than gracePeriod. 169 // It will only operate on ec2 instances tagged with 'mantle' to avoid stomping 170 // on other resources in the account. 171 func (a *API) gcEC2(gracePeriod time.Duration) error { 172 durationAgo := time.Now().Add(-1 * gracePeriod) 173 174 instances, err := a.ec2.DescribeInstances(&ec2.DescribeInstancesInput{ 175 Filters: []*ec2.Filter{ 176 &ec2.Filter{ 177 Name: aws.String("tag:CreatedBy"), 178 Values: aws.StringSlice([]string{"mantle"}), 179 }, 180 }, 181 }) 182 if err != nil { 183 return fmt.Errorf("error describing instances: %v", err) 184 } 185 186 toTerminate := []string{} 187 188 for _, reservation := range instances.Reservations { 189 for _, instance := range reservation.Instances { 190 if instance.LaunchTime.After(durationAgo) { 191 plog.Debugf("ec2: skipping instance %s due to being too new", *instance.InstanceId) 192 // Skip, still too new 193 continue 194 } 195 196 if instance.State != nil { 197 switch *instance.State.Name { 198 case ec2.InstanceStateNamePending, ec2.InstanceStateNameRunning, ec2.InstanceStateNameStopped: 199 toTerminate = append(toTerminate, *instance.InstanceId) 200 case ec2.InstanceStateNameTerminated, ec2.InstanceStateNameShuttingDown: 201 default: 202 plog.Infof("ec2: skipping instance in state %s", *instance.State.Name) 203 } 204 } else { 205 plog.Warningf("ec2 instance had no state: %s", *instance.InstanceId) 206 } 207 } 208 } 209 210 return a.TerminateInstances(toTerminate) 211 } 212 213 // TerminateInstances schedules EC2 instances to be terminated. 214 func (a *API) TerminateInstances(ids []string) error { 215 if len(ids) == 0 { 216 return nil 217 } 218 input := &ec2.TerminateInstancesInput{ 219 InstanceIds: aws.StringSlice(ids), 220 } 221 222 if _, err := a.ec2.TerminateInstances(input); err != nil { 223 return err 224 } 225 226 return nil 227 } 228 229 func (a *API) CreateTags(resources []string, tags map[string]string) error { 230 if len(tags) == 0 { 231 return nil 232 } 233 234 tagObjs := make([]*ec2.Tag, 0, len(tags)) 235 for key, value := range tags { 236 tagObjs = append(tagObjs, &ec2.Tag{ 237 Key: aws.String(key), 238 Value: aws.String(value), 239 }) 240 } 241 _, err := a.ec2.CreateTags(&ec2.CreateTagsInput{ 242 Resources: aws.StringSlice(resources), 243 Tags: tagObjs, 244 }) 245 if err != nil { 246 return fmt.Errorf("error creating tags: %v", err) 247 } 248 return err 249 } 250 251 // GetConsoleOutput returns the console output. Returns "", nil if no logs 252 // are available. 253 func (a *API) GetConsoleOutput(instanceID string) (string, error) { 254 res, err := a.ec2.GetConsoleOutput(&ec2.GetConsoleOutputInput{ 255 InstanceId: aws.String(instanceID), 256 }) 257 if err != nil { 258 return "", fmt.Errorf("couldn't get console output of %v: %v", instanceID, err) 259 } 260 261 if res.Output == nil { 262 return "", nil 263 } 264 265 decoded, err := base64.StdEncoding.DecodeString(*res.Output) 266 if err != nil { 267 return "", fmt.Errorf("couldn't decode console output of %v: %v", instanceID, err) 268 } 269 270 return string(decoded), nil 271 }