package org.cloudfoundry.community.servicebroker.datalifecycle.aws; import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import org.apache.log4j.Logger; import org.cloudfoundry.community.servicebroker.datalifecycle.utils.HostUtils; import org.cloudfoundry.community.servicebroker.exception.ServiceBrokerException; import com.amazonaws.services.ec2.AmazonEC2Client; import com.amazonaws.services.ec2.model.AssociateAddressRequest; import com.amazonaws.services.ec2.model.CreateImageRequest; import com.amazonaws.services.ec2.model.CreateImageResult; import com.amazonaws.services.ec2.model.DeleteSnapshotRequest; import com.amazonaws.services.ec2.model.DeleteVolumeRequest; import com.amazonaws.services.ec2.model.DeregisterImageRequest; import com.amazonaws.services.ec2.model.DescribeAddressesResult; import com.amazonaws.services.ec2.model.DescribeImagesRequest; import com.amazonaws.services.ec2.model.DescribeImagesResult; import com.amazonaws.services.ec2.model.DescribeInstanceStatusRequest; import com.amazonaws.services.ec2.model.DescribeInstanceStatusResult; import com.amazonaws.services.ec2.model.DescribeInstancesRequest; import com.amazonaws.services.ec2.model.DescribeInstancesResult; import com.amazonaws.services.ec2.model.DescribeSnapshotsResult; import com.amazonaws.services.ec2.model.DescribeVolumesRequest; import com.amazonaws.services.ec2.model.DescribeVolumesResult; import com.amazonaws.services.ec2.model.Filter; import com.amazonaws.services.ec2.model.InstanceType; import com.amazonaws.services.ec2.model.RunInstancesRequest; import com.amazonaws.services.ec2.model.RunInstancesResult; import com.amazonaws.services.ec2.model.Snapshot; import com.amazonaws.services.ec2.model.TerminateInstancesRequest; import com.amazonaws.services.ec2.model.Volume; public class AWSHelper { private Logger log = Logger.getLogger(AWSHelper.class); private AmazonEC2Client ec2Client; private String subnetId; private String sourceInstanceId; private HostUtils hostUtils; private int bootCheckPort; public AWSHelper(AmazonEC2Client ec2Client, String subnetId, String sourceInstanceId, HostUtils hostUtils, int bootCheckPort) { this.ec2Client = ec2Client; this.subnetId = subnetId; this.sourceInstanceId = sourceInstanceId; this.hostUtils = hostUtils; this.bootCheckPort = bootCheckPort; } public String getEC2InstancePublicIp(String instance) { DescribeInstancesResult result = ec2Client .describeInstances(new DescribeInstancesRequest() .withInstanceIds(instance)); return result.getReservations().get(0).getInstances().get(0) .getPublicIpAddress(); } public void deregisterAMI(String ami) { log.info("Deregistering AMI " + ami); ec2Client .deregisterImage(new DeregisterImageRequest().withImageId(ami)); } public void terminateEc2Instance(String ec2Instance) { log.info("Terminating instance " + ec2Instance); ec2Client.terminateInstances(new TerminateInstancesRequest() .withInstanceIds(Collections.singletonList(ec2Instance))); } /** * Given an AMI start an EC2 Instance. * * @param amiId * to start * @return the id of the running instance. * @throws ServiceBrokerException */ public String startEC2Instance(String amiId) throws ServiceBrokerException { RunInstancesResult instance = ec2Client .runInstances(new RunInstancesRequest().withImageId(amiId) .withInstanceType("m1.small").withMinCount(1) .withMaxCount(1).withSubnetId(subnetId) .withInstanceType(InstanceType.T2Micro)); String instanceId = getInstanceId(instance); addElasticIp(instanceId); log.info("Instance " + instanceId + " started successfully"); return instanceId; } /** * Associate the next available elastic IP with an instance. * * @param instanceId * @throws ServiceBrokerException */ public void addElasticIp(String instanceId) throws ServiceBrokerException { AssociateAddressRequest addressRequest = new AssociateAddressRequest() .withInstanceId(instanceId).withPublicIp( getAvaliableElasticIp()); log.info("Associating " + addressRequest.getPublicIp() + " with instance " + instanceId); if (waitForInstance(instanceId)) { ec2Client.associateAddress(addressRequest); } else { throw new ServiceBrokerException( "Instance did not transition to 'running' in alotted time."); } // We need the machine to boot before this will work. if (!hostUtils.waitForBoot(addressRequest.getPublicIp(), bootCheckPort)) { throw new ServiceBrokerException( "Host failed to boot in time alotted"); } } /** * Pull back a list of the elastic ip's that aren't attached to anything and * return the first one. * * @return the first available IP. * @throws ServiceBrokerException */ public String getAvaliableElasticIp() throws ServiceBrokerException { DescribeAddressesResult result = ec2Client.describeAddresses(); log.info("Found " + result.getAddresses().size() + " addresses!"); return result .getAddresses() .stream() .filter(a -> null == a.getInstanceId()) .findAny() .orElseThrow( () -> new ServiceBrokerException( "No elastic IP's avaliable!")).getPublicIp(); } /** * Build an AMI of a running EC2Instance. Creates a snap which is * disassociated and tracked via a the description. * * @param sourceInstance * the EC2 instance to create an AMI from * @param description * to shove in the console so you know what your looking at * @return id of the ami * @throws TimeoutException * if the ami isn't available in time. * * @see #deleteStorageArtifacts(String) */ public String createAMI(String sourceInstance, String description) throws TimeoutException { CreateImageResult imageResult = ec2Client .createImage(new CreateImageRequest() .withInstanceId(sourceInstance) .withDescription(description) .withName( sourceInstance + "-" + System.currentTimeMillis()) .withNoReboot(true)); String amiId = imageResult.getImageId(); if (!waitForImage(amiId)) { throw new TimeoutException( "Timed out waiting for amazon to create AMI " + amiId); } log.info("Created new AMI with ID: " + amiId); return amiId; } /** * Find the snap & volumes associated with the AMI we used and delete it. * AWS doesn't help us out much and the only relationship (as of 2/14/2015) * we can leverage is the description field. * * @param ami * to find associated snaps for * @throws ServiceBrokerExceptions */ public void deleteStorageArtifacts(String ami) throws ServiceBrokerException { DescribeSnapshotsResult desc = ec2Client.describeSnapshots(); if (null == desc.getSnapshots()) { return; } List<Snapshot> snapshots = desc.getSnapshots(); // The only way I can find to track the snaps that get created (but not // cleaned up) as part of the ami creation is by the description. This // code is brittle and will probably fail in unexpected and glamorous // ways. String amiDesc = "Created by CreateImage(" + sourceInstanceId + ") for " + ami + " from vol"; // Would be nice if the aws client return optionals... List<Snapshot> matching = snapshots.stream() .filter(s -> safeContains(s::getDescription, amiDesc)) .collect(Collectors.toList()); switch (matching.size()) { case 0: // Should this throw? Might have been manually cleaned up...but it // may orphan the volume. It's done this way to allow people to // create their own instances in AWS and not jack them up by // deleting the volume log.error("No snapshots found for AMI " + ami); break; case 1: String snap = matching.get(0).getSnapshotId(); log.info("Deleting snapshot " + snap); ec2Client.deleteSnapshot(new DeleteSnapshotRequest() .withSnapshotId(snap)); deleteVolumeForSnap(snap); break; default: throw new ServiceBrokerException( "Found too many snapshots for AMI " + ami); } } private void deleteVolumeForSnap(String snap) { waitForVolume(snap); String volId = getVolume(snap).getVolumeId(); log.info("Deleting volume " + volId); ec2Client.deleteVolume(new DeleteVolumeRequest().withVolumeId(volId)); } private void waitForVolume(String snap) { int retries = 0; Volume vol = getVolume(snap); while ("in-use".equals(vol.getState()) && retries < 5) { log.error("Volume is still in use, sleeping 30"); sleep(); retries++; vol = getVolume(snap); } } private Volume getVolume(String snap) { DescribeVolumesResult volumes = ec2Client .describeVolumes(new DescribeVolumesRequest() .withFilters(new Filter().withName("snapshot-id") .withValues(snap))); return volumes.getVolumes().stream().findFirst().get(); } private boolean safeContains(Callable<String> s, String c) { try { return (null != s.call()) && s.call().contains(c); } catch (Exception e) { log.error(e); } return false; } private boolean waitForInstance(String instanceId) { log.info("Waiting for instance to transition to running"); for (int i = 0; i < 5; ++i) { DescribeInstanceStatusResult result = ec2Client .describeInstanceStatus(new DescribeInstanceStatusRequest() .withInstanceIds(instanceId)); if (!result.getInstanceStatuses().isEmpty()) { String state = result.getInstanceStatuses().get(0) .getInstanceState().getName(); log.info("Instance state is " + state); if (state.equals("running")) { return true; } } sleep(); } return false; } private boolean waitForImage(String imageId) { for (int i = 0; i < 5; i++) { String imageState = getImageState(imageId); log.info("Image state is " + imageState); switch (imageState) { case "available": return true; case "failed": return false; default: log.info("Waiting 30s for AMI " + imageId); sleep(); } } return false; } private void sleep() { try { Thread.sleep(30000); } catch (InterruptedException e) { log.error(e); } } private String getInstanceId(RunInstancesResult instance) { return instance.getReservation().getInstances().get(0).getInstanceId(); } private String getImageState(String imageId) { String state = "failed"; DescribeImagesResult result = ec2Client .describeImages(new DescribeImagesRequest() .withImageIds(imageId)); if (null != result && null != result.getImages() && !result.getImages().isEmpty()) { state = result.getImages().get(0).getState(); } return state; } }