View Javadoc
1   /**
2    * Copyright 2004-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.common.aws.ec2.impl;
17  
18  import static com.google.common.base.Optional.absent;
19  import static com.google.common.base.Optional.fromNullable;
20  import static com.google.common.base.Preconditions.checkArgument;
21  import static com.google.common.base.Preconditions.checkNotNull;
22  import static com.google.common.base.Preconditions.checkState;
23  import static com.google.common.collect.Lists.newArrayList;
24  import static java.lang.String.format;
25  import static java.util.Collections.singletonList;
26  import static org.kuali.common.aws.ec2.model.InstanceStateName.RUNNING;
27  import static org.kuali.common.aws.ec2.model.InstanceStateName.STOPPED;
28  import static org.kuali.common.aws.ec2.model.InstanceStateName.TERMINATED;
29  import static org.kuali.common.util.FormatUtils.getMillisAsInt;
30  import static org.kuali.common.util.FormatUtils.getTime;
31  import static org.kuali.common.util.base.Exceptions.illegalState;
32  import static org.kuali.common.util.base.Precondition.checkNotBlank;
33  import static org.kuali.common.util.base.Precondition.checkNotNull;
34  import static org.kuali.common.util.base.Threads.sleep;
35  import static org.kuali.common.util.log.Loggers.newLogger;
36  
37  import java.util.ArrayList;
38  import java.util.Collection;
39  import java.util.Collections;
40  import java.util.HashSet;
41  import java.util.List;
42  import java.util.Set;
43  
44  import org.apache.commons.lang3.StringUtils;
45  import org.kuali.common.aws.ec2.api.EC2Service;
46  import org.kuali.common.aws.ec2.model.CreateAMIRequest;
47  import org.kuali.common.aws.ec2.model.EC2ServiceContext;
48  import org.kuali.common.aws.ec2.model.InstanceStateName;
49  import org.kuali.common.aws.ec2.model.LaunchInstanceContext;
50  import org.kuali.common.aws.ec2.model.Regions;
51  import org.kuali.common.aws.ec2.model.RootVolume;
52  import org.kuali.common.aws.ec2.model.security.KualiSecurityGroup;
53  import org.kuali.common.aws.ec2.model.security.Permission;
54  import org.kuali.common.aws.ec2.model.security.Protocol;
55  import org.kuali.common.aws.ec2.model.security.SetPermissionsResult;
56  import org.kuali.common.aws.ec2.model.status.InstanceStatusType;
57  import org.kuali.common.aws.ec2.model.status.InstanceStatusValue;
58  import org.kuali.common.aws.ec2.util.LaunchUtils;
59  import org.kuali.common.core.ssh.PublicKey;
60  import org.kuali.common.util.Assert;
61  import org.kuali.common.util.CollectionUtils;
62  import org.kuali.common.util.FormatUtils;
63  import org.kuali.common.util.SetUtils;
64  import org.kuali.common.util.base.Threads;
65  import org.kuali.common.util.condition.Condition;
66  import org.kuali.common.util.wait.DefaultWaitService;
67  import org.kuali.common.util.wait.WaitContext;
68  import org.kuali.common.util.wait.WaitResult;
69  import org.kuali.common.util.wait.WaitService;
70  import org.slf4j.Logger;
71  
72  import com.amazonaws.auth.AWSCredentials;
73  import com.amazonaws.regions.RegionUtils;
74  import com.amazonaws.services.ec2.AmazonEC2Client;
75  import com.amazonaws.services.ec2.model.AuthorizeSecurityGroupIngressRequest;
76  import com.amazonaws.services.ec2.model.BlockDeviceMapping;
77  import com.amazonaws.services.ec2.model.CopyImageRequest;
78  import com.amazonaws.services.ec2.model.CopyImageResult;
79  import com.amazonaws.services.ec2.model.CreateSecurityGroupRequest;
80  import com.amazonaws.services.ec2.model.CreateSnapshotRequest;
81  import com.amazonaws.services.ec2.model.CreateSnapshotResult;
82  import com.amazonaws.services.ec2.model.CreateTagsRequest;
83  import com.amazonaws.services.ec2.model.DeleteSnapshotRequest;
84  import com.amazonaws.services.ec2.model.DeregisterImageRequest;
85  import com.amazonaws.services.ec2.model.DescribeImagesRequest;
86  import com.amazonaws.services.ec2.model.DescribeImagesResult;
87  import com.amazonaws.services.ec2.model.DescribeInstanceStatusRequest;
88  import com.amazonaws.services.ec2.model.DescribeInstanceStatusResult;
89  import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
90  import com.amazonaws.services.ec2.model.DescribeInstancesResult;
91  import com.amazonaws.services.ec2.model.DescribeKeyPairsResult;
92  import com.amazonaws.services.ec2.model.DescribeSecurityGroupsRequest;
93  import com.amazonaws.services.ec2.model.DescribeSecurityGroupsResult;
94  import com.amazonaws.services.ec2.model.DescribeSnapshotsRequest;
95  import com.amazonaws.services.ec2.model.DescribeSnapshotsResult;
96  import com.amazonaws.services.ec2.model.EbsBlockDevice;
97  import com.amazonaws.services.ec2.model.EbsInstanceBlockDevice;
98  import com.amazonaws.services.ec2.model.Image;
99  import com.amazonaws.services.ec2.model.ImportKeyPairRequest;
100 import com.amazonaws.services.ec2.model.ImportKeyPairResult;
101 import com.amazonaws.services.ec2.model.Instance;
102 import com.amazonaws.services.ec2.model.InstanceBlockDeviceMapping;
103 import com.amazonaws.services.ec2.model.InstanceStatus;
104 import com.amazonaws.services.ec2.model.InstanceStatusDetails;
105 import com.amazonaws.services.ec2.model.InstanceStatusSummary;
106 import com.amazonaws.services.ec2.model.IpPermission;
107 import com.amazonaws.services.ec2.model.KeyPairInfo;
108 import com.amazonaws.services.ec2.model.ModifyInstanceAttributeRequest;
109 import com.amazonaws.services.ec2.model.Placement;
110 import com.amazonaws.services.ec2.model.Region;
111 import com.amazonaws.services.ec2.model.RegisterImageRequest;
112 import com.amazonaws.services.ec2.model.RegisterImageResult;
113 import com.amazonaws.services.ec2.model.Reservation;
114 import com.amazonaws.services.ec2.model.RevokeSecurityGroupIngressRequest;
115 import com.amazonaws.services.ec2.model.RunInstancesRequest;
116 import com.amazonaws.services.ec2.model.RunInstancesResult;
117 import com.amazonaws.services.ec2.model.SecurityGroup;
118 import com.amazonaws.services.ec2.model.Snapshot;
119 import com.amazonaws.services.ec2.model.StartInstancesRequest;
120 import com.amazonaws.services.ec2.model.StopInstancesRequest;
121 import com.amazonaws.services.ec2.model.Tag;
122 import com.amazonaws.services.ec2.model.TerminateInstancesRequest;
123 import com.google.common.base.Optional;
124 import com.google.common.collect.ImmutableList;
125 import com.google.common.collect.Lists;
126 
127 /**
128  * This service implementation performs operations using a single set of AWS credentials on a single EC2 region.
129  */
130 public final class DefaultEC2Service implements EC2Service {
131 
132 	private static final Logger logger = newLogger();
133 	private static final String SNAPSHOT_COMPLETED_STATE = "completed";
134 	private static final String AMI_AVAILABLE_STATE = "available";
135 
136 	// Don't expose the AmazonEC2Client object via a getter, it's not thread safe
137 	private final AmazonEC2Client client;
138 
139 	private final EC2ServiceContext context;
140 	private final WaitService service;
141 
142 	public DefaultEC2Service(AWSCredentials credentials) {
143 		this(credentials, new Region().withRegionName(Regions.DEFAULT_REGION.getName()));
144 	}
145 
146 	public DefaultEC2Service(AWSCredentials credentials, String region) {
147 		this(credentials, new Region().withRegionName(region));
148 	}
149 
150 	public DefaultEC2Service(AWSCredentials credentials, Region region) {
151 		this(EC2ServiceContext.create(credentials, region), new DefaultWaitService());
152 	}
153 
154 	public DefaultEC2Service(EC2ServiceContext context, WaitService service) {
155 		this.context = checkNotNull(context, "context");
156 		this.service = checkNotNull(service, "service");
157 		this.client = LaunchUtils.getClient(context);
158 	}
159 
160 	@Override
161 	public String getAccessKey() {
162 		return context.getCredentials().getAWSAccessKeyId();
163 	}
164 
165 	@Override
166 	public String getRegion() {
167 		return context.getRegion();
168 	}
169 
170 	@Override
171 	public String copyAmi(String region, String ami) {
172 		return copyAmi(region, ami, Optional.<String> absent());
173 	}
174 
175 	@Override
176 	public String copyAmi(String region, String ami, String name) {
177 		return copyAmi(region, ami, Optional.of(name));
178 	}
179 
180 	public String copyAmi(String region, String ami, Optional<String> name) {
181 		checkNotBlank(region, "region");
182 		checkNotBlank(ami, "ami");
183 		checkNotBlank(name, "name");
184 		checkNotNull(RegionUtils.getRegion(region), "region %s is unknown", region);
185 		CopyImageRequest request = new CopyImageRequest();
186 		request.setSourceImageId(ami);
187 		request.setSourceRegion(region);
188 		if (name.isPresent()) {
189 			request.setName(name.get());
190 		}
191 		CopyImageResult result = client.copyImage(request);
192 		Threads.sleep(1000);
193 		waitForAmiState(result.getImageId(), AMI_AVAILABLE_STATE, getMillisAsInt("4h"));
194 		return result.getImageId();
195 	}
196 
197 	@Override
198 	public List<Image> getImages() {
199 		return client.describeImages().getImages();
200 	}
201 
202 	@Override
203 	public void deleteSnapshot(String snapshotId) {
204 		DeleteSnapshotRequest request = new DeleteSnapshotRequest();
205 		request.setSnapshotId(snapshotId);
206 		client.deleteSnapshot(request);
207 	}
208 
209 	@Override
210 	public void purgeAmi(String imageId) {
211 		Image image = getImage(imageId);
212 		List<String> snapshotIds = newArrayList();
213 		List<BlockDeviceMapping> mappings = image.getBlockDeviceMappings();
214 		for (BlockDeviceMapping mapping : mappings) {
215 			Optional<EbsBlockDevice> ebsBlockDevice = fromNullable(mapping.getEbs());
216 			if (ebsBlockDevice.isPresent()) {
217 				snapshotIds.add(ebsBlockDevice.get().getSnapshotId());
218 			}
219 		}
220 		DeregisterImageRequest request = new DeregisterImageRequest();
221 		request.setImageId(imageId);
222 		client.deregisterImage(request);
223 		for (String snapshotId : snapshotIds) {
224 			deleteSnapshot(snapshotId);
225 		}
226 	}
227 
228 	@Override
229 	public Image createAmi(CreateAMIRequest request) {
230 		Instance instance = getInstance(request.getInstanceId());
231 		String rootVolumeId = getRootVolumeId(instance);
232 		Snapshot snapshot = createSnapshot(rootVolumeId, request.getDescription(), request.getTimeoutMillis());
233 		tag(snapshot.getSnapshotId(), request.getName());
234 		return createAmi(request, instance, snapshot);
235 	}
236 
237 	protected Image createAmi(CreateAMIRequest create, Instance instance, Snapshot snapshot) {
238 		BlockDeviceMapping rootVolumeMapping = getRootVolumeMapping(instance, snapshot.getSnapshotId(), create.getRootVolume());
239 		List<BlockDeviceMapping> mappings = newArrayList();
240 		mappings.add(rootVolumeMapping);
241 		for (BlockDeviceMapping mapping : create.getAdditionalMappings()) {
242 			mappings.add(mapping);
243 		}
244 
245 		RegisterImageRequest request = new RegisterImageRequest();
246 		request.setName(create.getName().getValue());
247 		request.setDescription(create.getDescription());
248 		request.setArchitecture(instance.getArchitecture());
249 		request.setRootDeviceName(instance.getRootDeviceName());
250 		request.setKernelId(instance.getKernelId());
251 		request.setBlockDeviceMappings(mappings);
252 		RegisterImageResult result = client.registerImage(request);
253 		String imageId = result.getImageId();
254 		waitForAmiState(imageId, AMI_AVAILABLE_STATE, create.getTimeoutMillis());
255 		tag(imageId, create.getName());
256 		return getImage(imageId);
257 	}
258 
259 	protected BlockDeviceMapping getRootVolumeMapping(Instance instance, String snapshotId, RootVolume rootVolume) {
260 		InstanceBlockDeviceMapping ibdm = getRootVolumeMapping(instance);
261 
262 		EbsBlockDevice ebs = new EbsBlockDevice();
263 		ebs.setDeleteOnTermination(rootVolume.getDeleteOnTermination().orNull());
264 		ebs.setSnapshotId(snapshotId);
265 		ebs.setVolumeSize(rootVolume.getSizeInGigabytes().orNull());
266 
267 		BlockDeviceMapping bdm = new BlockDeviceMapping();
268 		bdm.setDeviceName(ibdm.getDeviceName());
269 		bdm.setEbs(ebs);
270 		return bdm;
271 	}
272 
273 	protected BlockDeviceMapping convert(InstanceBlockDeviceMapping mapping, String snapshotId, int sizeInGigabytes) {
274 		BlockDeviceMapping converted = new BlockDeviceMapping();
275 		converted.setDeviceName(mapping.getDeviceName());
276 		converted.setEbs(convert(mapping.getEbs(), snapshotId, sizeInGigabytes));
277 		return converted;
278 	}
279 
280 	protected EbsBlockDevice convert(EbsInstanceBlockDevice device, String snapshotId, int sizeInGigabytes) {
281 		EbsBlockDevice converted = new EbsBlockDevice();
282 		converted.setDeleteOnTermination(device.getDeleteOnTermination());
283 		converted.setSnapshotId(snapshotId);
284 		converted.setVolumeSize(sizeInGigabytes);
285 		return converted;
286 	}
287 
288 	protected Optional<Tag> getTag(List<Tag> tags, String key) {
289 		for (Tag tag : tags) {
290 			if (tag.getKey().equals(key)) {
291 				return Optional.of(tag);
292 			}
293 		}
294 		return absent();
295 	}
296 
297 	protected String getRootVolumeId(Instance instance) {
298 		checkNotNull(instance, "instance");
299 		InstanceBlockDeviceMapping rootMapping = getRootVolumeMapping(instance);
300 		return rootMapping.getEbs().getVolumeId();
301 	}
302 
303 	protected InstanceBlockDeviceMapping getRootVolumeMapping(Instance instance) {
304 		checkNotNull(instance, "instance");
305 		String rootDeviceName = instance.getRootDeviceName();
306 		List<InstanceBlockDeviceMapping> mappings = instance.getBlockDeviceMappings();
307 		for (InstanceBlockDeviceMapping mapping : mappings) {
308 			if (rootDeviceName.equals(mapping.getDeviceName())) {
309 				return mapping;
310 			}
311 		}
312 		throw illegalState("Unable to locate the root volume mapping for [%s]", instance.getInstanceId());
313 	}
314 
315 	@Override
316 	public Snapshot createSnapshot(String volumeId, String description, int timeoutMillis) {
317 		CreateSnapshotRequest request = new CreateSnapshotRequest(volumeId, description);
318 		CreateSnapshotResult result = client.createSnapshot(request);
319 		waitForSnapshotState(result.getSnapshot().getSnapshotId(), SNAPSHOT_COMPLETED_STATE, timeoutMillis);
320 		return result.getSnapshot();
321 	}
322 
323 	@Override
324 	public Snapshot getSnapshot(String snapshotId) {
325 		DescribeSnapshotsRequest request = new DescribeSnapshotsRequest();
326 		request.setSnapshotIds(singletonList(snapshotId));
327 		DescribeSnapshotsResult result = client.describeSnapshots(request);
328 		List<Snapshot> snapshots = result.getSnapshots();
329 		checkState(snapshots.size() == 1, "expected 1 snapshot, but there were %s instead", snapshots.size());
330 		return snapshots.get(0);
331 	}
332 
333 	protected void waitForSnapshotState(String snapshotId, String state, int timeoutMillis) {
334 		Condition condition = new SnapshotStateCondition(this, snapshotId, state);
335 		WaitContext waitContext = getWaitContext(timeoutMillis);
336 		Object[] args = { FormatUtils.getTime(waitContext.getTimeoutMillis()), snapshotId, state };
337 		logger.info(format("waiting up to %s for snapshot [%s] to reach state '%s'", args));
338 		WaitResult result = service.wait(waitContext, condition);
339 		Object[] resultArgs = { snapshotId, state, FormatUtils.getTime(result.getElapsed()) };
340 		logger.info(format("snapshot [%s] is now '%s' - %s", resultArgs));
341 	}
342 
343 	protected void waitForAmiState(String imageId, String state, int timeoutMillis) {
344 		Condition condition = new AmiStateCondition(this, imageId, state);
345 		WaitContext waitContext = getWaitContext(timeoutMillis);
346 		Object[] args = { FormatUtils.getTime(waitContext.getTimeoutMillis()), imageId, state };
347 		logger.info(format("waiting up to %s for image [%s] to reach state '%s'", args));
348 		WaitResult result = service.wait(waitContext, condition);
349 		Object[] resultArgs = { imageId, state, FormatUtils.getTime(result.getElapsed()) };
350 		logger.info(format("ami [%s] is now '%s' - %s", resultArgs));
351 	}
352 
353 	@Override
354 	public List<Image> getMyImages() {
355 		DescribeImagesRequest request = new DescribeImagesRequest();
356 		request.withOwners(AmiOwner.SELF.getValue());
357 		DescribeImagesResult result = client.describeImages(request);
358 		return result.getImages();
359 	}
360 
361 	protected void checkSizeEquals(Collection<?> c, int size) {
362 		checkState(c.size() == size, "expected size of %s, was %s instead", size, c.size());
363 	}
364 
365 	@Override
366 	public Image getImage(String imageId) {
367 		DescribeImagesRequest request = new DescribeImagesRequest();
368 		request.setImageIds(singletonList(imageId));
369 		DescribeImagesResult result = client.describeImages(request);
370 		List<Image> images = result.getImages();
371 		checkSizeEquals(images, 1);
372 		return images.get(0);
373 	}
374 
375 	@Override
376 	public String importKey(String keyName, String publicKey) {
377 		checkNotBlank(keyName, "keyName");
378 		checkNotBlank(publicKey, "publicKey");
379 		ImportKeyPairRequest request = new ImportKeyPairRequest(keyName, publicKey);
380 		ImportKeyPairResult result = client.importKeyPair(request);
381 		return result.getKeyFingerprint();
382 	}
383 
384 	@Override
385 	public boolean isExistingKey(String keyName) {
386 		checkNotBlank(keyName, "keyName");
387 		DescribeKeyPairsResult result = client.describeKeyPairs();
388 		List<KeyPairInfo> keys = result.getKeyPairs();
389 		Optional<KeyPairInfo> optional = getKeyPairInfo(keyName, keys);
390 		return optional.isPresent();
391 	}
392 
393 	protected Optional<KeyPairInfo> getKeyPairInfo(String name, List<KeyPairInfo> list) {
394 		for (KeyPairInfo element : list) {
395 			logger.debug("fingerprint - {}, name - {}", element.getKeyFingerprint(), element.getKeyName());
396 			if (name.equals(element.getKeyName())) {
397 				return Optional.of(element);
398 			}
399 		}
400 		return Optional.<KeyPairInfo> absent();
401 	}
402 
403 	@Override
404 	public List<String> getSecurityGroupNames() {
405 		DescribeSecurityGroupsResult result = client.describeSecurityGroups();
406 		List<String> names = newArrayList();
407 		for (SecurityGroup group : result.getSecurityGroups()) {
408 			names.add(group.getGroupName());
409 		}
410 		Collections.sort(names);
411 		return ImmutableList.copyOf(names);
412 	}
413 
414 	@Override
415 	public void createSecurityGroup(KualiSecurityGroup group) {
416 		checkNotNull(group, "group");
417 		CreateSecurityGroupRequest request = new CreateSecurityGroupRequest();
418 		request.setGroupName(group.getName());
419 		if (group.getDescription().isPresent()) {
420 			request.setDescription(group.getDescription().get());
421 		}
422 		client.createSecurityGroup(request);
423 		setPermissions(group.getName(), group.getPermissions());
424 	}
425 
426 	@Override
427 	public boolean isExistingSecurityGroup(String name) {
428 		checkNotBlank(name, "name");
429 		return getSecurityGroup(name).isPresent();
430 	}
431 
432 	@Override
433 	public SetPermissionsResult setPermissions(String securityGroupName, List<Permission> permissions) {
434 		checkNotBlank(securityGroupName, "securityGroupName");
435 		checkNotNull(permissions, "permissions");
436 		Optional<SecurityGroup> optional = getSecurityGroup(securityGroupName);
437 		checkState(optional.isPresent(), "Security group [%s] does not exist", securityGroupName);
438 		SecurityGroup group = optional.get();
439 		List<IpPermission> oldPerms = group.getIpPermissions();
440 		List<Permission> oldPermissions = getPermissions(oldPerms);
441 
442 		Set<Permission> newSet = new HashSet<Permission>(permissions);
443 		Set<Permission> oldSet = new HashSet<Permission>(oldPermissions);
444 
445 		Set<Permission> adds = SetUtils.difference(newSet, oldSet);
446 		Set<Permission> deletes = SetUtils.difference(oldSet, newSet);
447 		Set<Permission> existing = SetUtils.intersection(newSet, oldSet);
448 
449 		// Delete any permissions that are not in the list, but exist in the security group
450 		if (deletes.size() > 0) {
451 			RevokeSecurityGroupIngressRequest revoker = new RevokeSecurityGroupIngressRequest(securityGroupName, getIpPermissions(deletes));
452 			client.revokeSecurityGroupIngress(revoker);
453 		}
454 
455 		// Add any permissions that are in the list but don't exist in the security group
456 		if (adds.size() > 0) {
457 			AuthorizeSecurityGroupIngressRequest authorizer = new AuthorizeSecurityGroupIngressRequest();
458 			authorizer.withGroupName(securityGroupName).withIpPermissions(getIpPermissions(adds));
459 			client.authorizeSecurityGroupIngress(authorizer);
460 		}
461 
462 		return new SetPermissionsResult(adds, deletes, existing);
463 	}
464 
465 	@Override
466 	public Optional<SecurityGroup> getSecurityGroup(String name) {
467 		checkNotBlank(name, "name");
468 		List<String> names = getSecurityGroupNames();
469 		if (names.contains(name)) {
470 			DescribeSecurityGroupsRequest request = new DescribeSecurityGroupsRequest();
471 			request.setGroupNames(Collections.singletonList(name));
472 			DescribeSecurityGroupsResult result = client.describeSecurityGroups(request);
473 			List<SecurityGroup> groups = result.getSecurityGroups();
474 			checkState(groups.size() == 1, "Expected exactly 1 security group but there were %s instead", groups.size());
475 			SecurityGroup group = groups.get(0);
476 			return Optional.of(group);
477 		} else {
478 			return Optional.<SecurityGroup> absent();
479 		}
480 	}
481 
482 	protected List<IpPermission> getIpPermissions(Collection<Permission> permissions) {
483 		List<IpPermission> newPerms = new ArrayList<IpPermission>();
484 		for (Permission perm : permissions) {
485 			IpPermission newPerm = getIpPermission(perm);
486 			newPerms.add(newPerm);
487 		}
488 		return ImmutableList.copyOf(newPerms);
489 	}
490 
491 	protected List<Permission> getPermissions(Collection<IpPermission> permissions) {
492 		List<Permission> newPerms = new ArrayList<Permission>();
493 		for (IpPermission perm : permissions) {
494 			Permission newPerm = getPermission(perm);
495 			newPerms.add(newPerm);
496 		}
497 		return ImmutableList.copyOf(newPerms);
498 	}
499 
500 	protected Permission getPermission(IpPermission perm) {
501 		checkArgument(CollectionUtils.isEmpty(perm.getUserIdGroupPairs()), "User id / group pairs are not supported");
502 		String protocolName = perm.getIpProtocol();
503 		Integer fromPort = perm.getFromPort();
504 		Integer toPort = perm.getToPort();
505 		List<String> ipRanges = perm.getIpRanges();
506 		Assert.noNulls(fromPort, toPort, ipRanges);
507 		Assert.noBlanks(protocolName);
508 		Assert.isTrue(fromPort.equals(toPort), "port ranges are not supported");
509 		Protocol protocol = Protocol.valueOf(protocolName.toUpperCase());
510 		return Permission.builder(fromPort).withCidrNotations(ipRanges).withProtocol(protocol).build();
511 	}
512 
513 	protected IpPermission getIpPermission(Permission perm) {
514 		IpPermission newPerm = new IpPermission();
515 		newPerm.withIpRanges(perm.getCidrNotations());
516 		newPerm.withIpProtocol(perm.getProtocol().getValue());
517 		newPerm.withFromPort(perm.getPort());
518 		newPerm.withToPort(perm.getPort());
519 		return newPerm;
520 	}
521 
522 	@Override
523 	public Instance getInstance(String instanceId) {
524 		checkNotBlank(instanceId, "instanceId");
525 		List<Instance> instances = getInstances(ImmutableList.of(instanceId));
526 		checkState(instances.size() == 1, "Expected exactly 1 instance but there were %s instead", instances.size());
527 		return instances.get(0);
528 	}
529 
530 	@Override
531 	public List<Instance> getInstances(List<String> instances) {
532 		checkNotNull(instances, "'instances' cannot be null");
533 		DescribeInstancesRequest request = new DescribeInstancesRequest();
534 		if (!instances.isEmpty()) {
535 			request.setInstanceIds(instances);
536 		}
537 		DescribeInstancesResult result = client.describeInstances(request);
538 		List<Instance> list = Lists.newArrayList();
539 		List<Reservation> reservations = result.getReservations();
540 		for (Reservation reservation : reservations) {
541 			list.addAll(reservation.getInstances());
542 		}
543 		return ImmutableList.copyOf(list);
544 	}
545 
546 	@Override
547 	public List<Instance> getInstances() {
548 		return getInstances(ImmutableList.<String> of());
549 	}
550 
551 	@Override
552 	public Instance launchInstance(LaunchInstanceContext context) {
553 		checkNotNull(context, "context");
554 
555 		// Connect to AWS and ask them to create an instance
556 		Instance instance = issueRunInstanceRequest(context);
557 
558 		// Was getting some flaky behavior from AWS without a small delay after issuing the RunInstancesRequest
559 		// Since it generally takes a few minutes for the instance to spin up, pausing here for 1 second should be ok
560 		sleep(this.context.getInitialPauseMillis());
561 
562 		// Tag the instance
563 		tag(instance.getInstanceId(), context.getTags());
564 
565 		// Wait for confirmation that the instance is online and functioning
566 		waitForOnlineConfirmation(instance, context);
567 
568 		// Return the fully populated instance object
569 		return getInstance(instance.getInstanceId());
570 	}
571 
572 	@Override
573 	public void allowTermination(String instanceId) {
574 		Assert.noBlanks(instanceId);
575 		preventTermination(instanceId, false);
576 	}
577 
578 	@Override
579 	public void preventTermination(String instanceId) {
580 		Assert.noBlanks(instanceId);
581 		preventTermination(instanceId, true);
582 	}
583 
584 	@Override
585 	public Instance startInstance(String instanceId) {
586 		checkNotBlank(instanceId, "instanceId");
587 		StartInstancesRequest request = new StartInstancesRequest();
588 		request.setInstanceIds(singletonList(instanceId));
589 		client.startInstances(request);
590 		WaitContext waitContext = getWaitContext(context.getTerminationTimeoutMillis());
591 		Object[] args = { FormatUtils.getTime(waitContext.getTimeoutMillis()), instanceId, RUNNING.getValue() };
592 		logger.info("Waiting up to {} for [{}] to start", args);
593 		Condition online = new IsOnlineCondition(this, instanceId);
594 		WaitResult result = service.wait(waitContext, online);
595 		Object[] resultArgs = { instanceId, getTime(result.getElapsed()) };
596 		logger.info("[{}] has been started - {}", resultArgs);
597 		return getInstance(instanceId);
598 	}
599 
600 	@Override
601 	public void stopInstance(String instanceId) {
602 		checkNotBlank(instanceId, "instanceId");
603 		StopInstancesRequest request = new StopInstancesRequest();
604 		request.setInstanceIds(singletonList(instanceId));
605 		client.stopInstances(request);
606 		WaitContext waitContext = getWaitContext(context.getTerminationTimeoutMillis());
607 		Object[] args = { FormatUtils.getTime(waitContext.getTimeoutMillis()), instanceId, STOPPED.getValue() };
608 		logger.info("Waiting up to {} for [{}] to stop", args);
609 		Condition condition = new InstanceStateCondition(this, instanceId, STOPPED);
610 		WaitResult result = service.wait(waitContext, condition);
611 		Object[] resultArgs = { instanceId, getTime(result.getElapsed()) };
612 		logger.info("[{}] has been stopped - {}", resultArgs);
613 	}
614 
615 	@Override
616 	public void terminateInstance(String instanceId) {
617 		checkNotBlank(instanceId, "instanceId");
618 		TerminateInstancesRequest request = new TerminateInstancesRequest();
619 		request.setInstanceIds(singletonList(instanceId));
620 		client.terminateInstances(request);
621 		WaitContext waitContext = getWaitContext(context.getTerminationTimeoutMillis());
622 		Object[] args = { FormatUtils.getTime(waitContext.getTimeoutMillis()), instanceId, TERMINATED.getValue() };
623 		logger.info("Waiting up to {} for [{}] to terminate", args);
624 		Condition condition = new InstanceStateCondition(this, instanceId, TERMINATED);
625 		WaitResult result = service.wait(waitContext, condition);
626 		Object[] resultArgs = { instanceId, getTime(result.getElapsed()) };
627 		logger.info("[{}] has been terminated - {}", resultArgs);
628 	}
629 
630 	@Override
631 	public void tag(String resourceId, List<Tag> tags) {
632 		checkNotBlank(resourceId, "resourceId");
633 		checkNotNull(tags, "tags");
634 		if (CollectionUtils.isEmpty(tags)) {
635 			return;
636 		}
637 		List<String> resources = Collections.singletonList(resourceId);
638 		CreateTagsRequest request = new CreateTagsRequest(resources, tags);
639 		client.createTags(request);
640 	}
641 
642 	@Override
643 	public void tag(String resourceId, Tag tag) {
644 		tag(resourceId, ImmutableList.of(tag));
645 	}
646 
647 	@Override
648 	public boolean isOnline(String instanceId) {
649 		return new IsOnlineCondition(this, instanceId).isTrue();
650 	}
651 
652 	@Override
653 	public String getStatus(String instanceId, InstanceStatusType type, String statusName) {
654 		List<InstanceStatus> statuses = getStatusList(instanceId);
655 		return getStatus(statuses, type, statusName);
656 	}
657 
658 	protected void preventTermination(String instanceId, boolean preventTermination) {
659 		Assert.noBlanks(instanceId);
660 		ModifyInstanceAttributeRequest request = new ModifyInstanceAttributeRequest();
661 		request.withInstanceId(instanceId);
662 
663 		// EC2 instances can normally be terminated by a single API call
664 		// Disabling API termination forces 2 API calls. (1 to re-enable termination, and a 2nd one to actually terminate the instance)
665 		request.withDisableApiTermination(preventTermination);
666 
667 		client.modifyInstanceAttribute(request);
668 	}
669 
670 	protected List<InstanceStatus> getStatusList(String instanceId) {
671 		DescribeInstanceStatusRequest request = new DescribeInstanceStatusRequest();
672 		request.setInstanceIds(Collections.singletonList(instanceId));
673 		DescribeInstanceStatusResult result = client.describeInstanceStatus(request);
674 		return result.getInstanceStatuses();
675 	}
676 
677 	protected String getStatus(List<InstanceStatus> statuses, InstanceStatusType type, String name) {
678 		for (InstanceStatus status : statuses) {
679 			InstanceStatusSummary summary = getSummary(status, type);
680 			Optional<String> detail = getStatusDetail(summary, name);
681 			if (detail.isPresent()) {
682 				return detail.get();
683 			}
684 		}
685 		return InstanceStatusValue.UNKNOWN.getValue();
686 	}
687 
688 	protected InstanceStatusSummary getSummary(InstanceStatus status, InstanceStatusType type) {
689 		switch (type) {
690 		case INSTANCE:
691 			return status.getInstanceStatus();
692 		case SYSTEM:
693 			return status.getSystemStatus();
694 		default:
695 			throw new IllegalArgumentException("[" + type + "] is unknown");
696 		}
697 	}
698 
699 	protected Optional<String> getStatusDetail(InstanceStatusSummary summary, String name) {
700 		List<InstanceStatusDetails> details = summary.getDetails();
701 		for (InstanceStatusDetails detail : details) {
702 			if (name.equals(detail.getName())) {
703 				return Optional.of(detail.getStatus());
704 			}
705 		}
706 		return Optional.absent();
707 	}
708 
709 	protected Instance issueRunInstanceRequest(LaunchInstanceContext context) {
710 		PublicKey publicKey = context.getPublicKey();
711 		if (!isExistingKey(publicKey.getName())) {
712 			logger.info("Importing key [{}]", publicKey.getName());
713 			importKey(publicKey.getName(), publicKey.getValue());
714 		}
715 
716 		List<String> securityGroupNames = getSecurityGroupNames();
717 		for (KualiSecurityGroup securityGroup : context.getSecurityGroups()) {
718 			if (!securityGroupNames.contains(securityGroup.getName())) {
719 				logger.info("Creating security group {}", securityGroup.getName());
720 				createSecurityGroup(securityGroup);
721 			}
722 			if (context.isOverrideExistingSecurityGroupPermissions()) {
723 				SetPermissionsResult result = setPermissions(securityGroup.getName(), securityGroup.getPermissions());
724 				logPermissionChanges(securityGroup, result.getDeletes(), "deleted");
725 				logPermissionChanges(securityGroup, result.getAdds(), "added");
726 			}
727 		}
728 
729 		RunInstancesRequest request = getRunInstanceRequest(context);
730 		RunInstancesResult result = client.runInstances(request);
731 		Reservation r = result.getReservation();
732 		List<Instance> instances = r.getInstances();
733 		checkState(instances.size() == 1, "Expected exactly 1 instance but there were %s instead", instances.size());
734 		return instances.get(0);
735 	}
736 
737 	protected void logPermissionChanges(KualiSecurityGroup group, List<Permission> perms, String changeDescription) {
738 		for (Permission perm : perms) {
739 			String port = StringUtils.leftPad(perm.getPort() + "", 5);
740 			String permDescription = "port:" + port + ", protocol:" + perm.getProtocol() + ", CIDR:" + CollectionUtils.asCSV(perm.getCidrNotations());
741 			Object[] args = { group.getName(), StringUtils.rightPad(changeDescription, 7, " "), permDescription };
742 			logger.info("Security Group:[{}] - permission {} [{}]", args);
743 		}
744 	}
745 
746 	protected WaitContext getWaitContext(int timeout) {
747 		int sleep = context.getSleepIntervalMillis();
748 		int pause = context.getInitialPauseMillis();
749 		return WaitContext.builder(timeout).sleepMillis(sleep).initialPauseMillis(pause).build();
750 	}
751 
752 	protected void waitForOnlineConfirmation(Instance instance, LaunchInstanceContext context) {
753 		InstanceStateName running = InstanceStateName.RUNNING;
754 		WaitContext waitContext = getWaitContext(context.getTimeoutMillis());
755 		Object[] args = { FormatUtils.getTime(waitContext.getTimeoutMillis()), instance.getInstanceId(), running.getValue() };
756 		logger.info("Waiting up to {} for [{}] to come online", args);
757 		Condition online = new IsOnlineCondition(this, instance.getInstanceId());
758 		WaitResult result = service.wait(waitContext, online);
759 		Object[] resultArgs = { instance.getInstanceId(), FormatUtils.getTime(result.getElapsed()) };
760 		logger.info("[{}] is now online - {}", resultArgs);
761 	}
762 
763 	protected List<String> getNames(List<KualiSecurityGroup> groups) {
764 		// Extract the names of any security groups into a list of strings
765 		List<String> names = new ArrayList<String>();
766 		for (KualiSecurityGroup group : groups) {
767 			names.add(group.getName());
768 		}
769 		Collections.sort(names);
770 		return ImmutableList.copyOf(names);
771 	}
772 
773 	/**
774 	 * Return a request that spins up exactly one instance.
775 	 */
776 	protected RunInstancesRequest getRunInstanceRequest(LaunchInstanceContext context) {
777 		RunInstancesRequest rir = new RunInstancesRequest();
778 		rir.setMaxCount(1);
779 		rir.setMinCount(1);
780 		rir.setImageId(context.getAmi());
781 		rir.setKeyName(context.getPublicKey().getName());
782 		rir.setSecurityGroups(getNames(context.getSecurityGroups()));
783 		rir.setInstanceType(context.getType());
784 		rir.setDisableApiTermination(context.isPreventTermination());
785 		rir.setEbsOptimized(context.isEbsOptimized());
786 		rir.setMonitoring(context.isEnableMonitoring());
787 
788 		// Update the request with an availability zone (if one has been supplied)
789 		if (context.getAvailabilityZone().isPresent()) {
790 			String zone = context.getAvailabilityZone().get();
791 			Placement placement = new Placement(zone);
792 			rir.setPlacement(placement);
793 		}
794 
795 		// Update the request with custom root volume settings (if any have been supplied)
796 
797 		// Get the list of block device mappings associated with this AMI after updating the BlockDeviceMapping for the root volume
798 		List<BlockDeviceMapping> mappings = getUpdatedBlockDeviceMappings(context);
799 
800 		// Store the block device mappings on the request
801 		rir.setBlockDeviceMappings(mappings);
802 		return rir;
803 	}
804 
805 	protected List<BlockDeviceMapping> getUpdatedBlockDeviceMappings(LaunchInstanceContext context) {
806 		// Get an Image object from Amazon for the AMI we are working with
807 		Image ami = getAmi(context.getAmi());
808 
809 		// Update the root volume as needed
810 		if (context.getRootVolume().isPresent()) {
811 			updateRootBlockDeviceMapping(ami, context.getRootVolume().get());
812 		}
813 
814 		// Store all of the existing block device mappings
815 		List<BlockDeviceMapping> mappings = newArrayList(ami.getBlockDeviceMappings());
816 
817 		// Cycle through the additional mappings, updating existing mappings with new mappings as we go
818 		for (BlockDeviceMapping additionalMapping : context.getAdditionalMappings()) {
819 
820 			// Look for a match in the existing mappings
821 			Optional<BlockDeviceMapping> optional = findMatch(mappings, additionalMapping);
822 
823 			// Check to see if we found a match
824 			if (optional.isPresent()) {
825 				// If so, override any existing block device settings with the new settings
826 				BlockDeviceMapping existing = optional.get();
827 				existing.setDeviceName(additionalMapping.getDeviceName());
828 				existing.setEbs(additionalMapping.getEbs());
829 				existing.setNoDevice(additionalMapping.getNoDevice());
830 				existing.setVirtualName(additionalMapping.getVirtualName());
831 			} else {
832 				// Otherwise just add the new mapping
833 				mappings.add(additionalMapping);
834 			}
835 		}
836 
837 		// Return the updated list
838 		return mappings;
839 	}
840 
841 	protected Optional<BlockDeviceMapping> findMatch(List<BlockDeviceMapping> mappings, BlockDeviceMapping mapping) {
842 		for (BlockDeviceMapping element : mappings) {
843 			if (element.getDeviceName().equals(mapping.getDeviceName())) {
844 				return Optional.of(element);
845 			}
846 		}
847 		return absent();
848 	}
849 
850 	protected void updateRootBlockDeviceMapping(Image ami, RootVolume rootVolume) {
851 
852 		// Extract the default root block device mapping specific to this AMI
853 		BlockDeviceMapping mapping = getRootBlockDeviceMapping(ami);
854 
855 		// Extract the block device
856 		EbsBlockDevice device = mapping.getEbs();
857 
858 		// If a size in gigabytes has been provided, update the device with the new size
859 		if (rootVolume.getSizeInGigabytes().isPresent()) {
860 			int sizeInGigabytes = rootVolume.getSizeInGigabytes().get();
861 			device.setVolumeSize(sizeInGigabytes);
862 		}
863 
864 		// If the delete on termination setting has been provided, update the device with the new setting
865 		if (rootVolume.getDeleteOnTermination().isPresent()) {
866 			boolean deleteOnTermination = rootVolume.getDeleteOnTermination().get();
867 			device.setDeleteOnTermination(deleteOnTermination);
868 		}
869 
870 	}
871 
872 	protected BlockDeviceMapping getRootBlockDeviceMapping(Image image) {
873 		String rootDeviceName = image.getRootDeviceName();
874 		List<BlockDeviceMapping> mappings = image.getBlockDeviceMappings();
875 		for (BlockDeviceMapping mapping : mappings) {
876 			String deviceName = mapping.getDeviceName();
877 			if (rootDeviceName.equals(deviceName)) {
878 				return mapping;
879 			}
880 		}
881 		throw illegalState("Could not locate the root block device mapping for AMI [%s]", image.getImageId());
882 	}
883 
884 	public Image getAmi(String ami) {
885 		checkNotBlank(ami, "ami");
886 		DescribeImagesRequest request = new DescribeImagesRequest();
887 		request.setImageIds(singletonList(ami));
888 		DescribeImagesResult result = client.describeImages(request);
889 		List<Image> images = result.getImages();
890 		checkState(images.size() == 1, "Expected exactly 1 image but there were %s instead", images.size());
891 		return images.get(0);
892 	}
893 
894 	public EC2ServiceContext getContext() {
895 		return context;
896 	}
897 
898 }