package core.aws.client;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.AmazonEC2ClientBuilder;
import com.amazonaws.services.ec2.model.AuthorizeSecurityGroupIngressRequest;
import com.amazonaws.services.ec2.model.AvailabilityZone;
import com.amazonaws.services.ec2.model.BlockDeviceMapping;
import com.amazonaws.services.ec2.model.CreateKeyPairRequest;
import com.amazonaws.services.ec2.model.CreateKeyPairResult;
import com.amazonaws.services.ec2.model.CreateSecurityGroupRequest;
import com.amazonaws.services.ec2.model.CreateSecurityGroupResult;
import com.amazonaws.services.ec2.model.CreateTagsRequest;
import com.amazonaws.services.ec2.model.DeleteKeyPairRequest;
import com.amazonaws.services.ec2.model.DeleteSecurityGroupRequest;
import com.amazonaws.services.ec2.model.DeleteSnapshotRequest;
import com.amazonaws.services.ec2.model.DeregisterImageRequest;
import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult;
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.DescribeInstancesRequest;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.ec2.model.DescribeKeyPairsRequest;
import com.amazonaws.services.ec2.model.DescribeTagsRequest;
import com.amazonaws.services.ec2.model.DescribeTagsResult;
import com.amazonaws.services.ec2.model.EbsBlockDevice;
import com.amazonaws.services.ec2.model.Image;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.InstanceStatus;
import com.amazonaws.services.ec2.model.IpPermission;
import com.amazonaws.services.ec2.model.KeyPair;
import com.amazonaws.services.ec2.model.RevokeSecurityGroupIngressRequest;
import com.amazonaws.services.ec2.model.RunInstancesRequest;
import com.amazonaws.services.ec2.model.RunInstancesResult;
import com.amazonaws.services.ec2.model.SecurityGroup;
import com.amazonaws.services.ec2.model.StartInstancesRequest;
import com.amazonaws.services.ec2.model.StopInstancesRequest;
import com.amazonaws.services.ec2.model.Tag;
import com.amazonaws.services.ec2.model.TagDescription;
import com.amazonaws.services.ec2.model.TerminateInstancesRequest;
import core.aws.resource.ec2.InstanceState;
import core.aws.util.Asserts;
import core.aws.util.Runner;
import core.aws.util.Threads;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author neo
*/
public class EC2 {
public final AmazonEC2 ec2;
private final Logger logger = LoggerFactory.getLogger(getClass());
private volatile List<String> availabilityZones;
public EC2(AWSCredentialsProvider credentials, Regions region) {
ec2 = AmazonEC2ClientBuilder.standard().withRegion(region).withCredentials(credentials).build();
}
public synchronized List<String> availabilityZones() {
if (availabilityZones == null) {
DescribeAvailabilityZonesResult result = ec2.describeAvailabilityZones();
availabilityZones = result.getAvailabilityZones().stream()
.filter(zone -> "available".equals(zone.getState()))
.map(AvailabilityZone::getZoneName)
.collect(Collectors.toList());
logger.info("availability zones => {}", availabilityZones);
}
return availabilityZones;
}
public KeyPair createKeyPair(String keyName) {
logger.info("create key pair, keyName={}", keyName);
CreateKeyPairResult result = ec2.createKeyPair(new CreateKeyPairRequest().withKeyName(keyName));
return result.getKeyPair();
}
public boolean keyPairExists(String name) {
try {
return !ec2.describeKeyPairs(new DescribeKeyPairsRequest().withKeyNames(name)).getKeyPairs().isEmpty();
} catch (AmazonServiceException e) {
if ("InvalidKeyPair.NotFound".equals(e.getErrorCode())) {
return false;
}
throw e;
}
}
public void deleteKeyPair(String keyPairName) {
logger.info("delete keyPair, keyName={}", keyPairName);
ec2.deleteKeyPair(new DeleteKeyPairRequest(keyPairName));
}
public List<Instance> runInstances(RunInstancesRequest request, Tag... tags) throws Exception {
logger.info("create ec2 instance, request={}", request);
RunInstancesResult result = new Runner<RunInstancesResult>()
.maxAttempts(3)
.retryInterval(Duration.ofSeconds(20))
.retryOn(this::retryOnRunInstance)
.run(() -> ec2.runInstances(request));
Threads.sleepRoughly(Duration.ofSeconds(5)); // wait little bit to make sure instance is visible to tag service
List<String> instanceIds = result.getReservation().getInstances().stream().map(Instance::getInstanceId).collect(Collectors.toList());
CreateTagsRequest tagsRequest = new CreateTagsRequest()
.withResources(instanceIds)
.withTags(tags);
createTags(tagsRequest);
waitUntilRunning(instanceIds);
return describeInstances(instanceIds);
}
private boolean retryOnRunInstance(Exception e) {
if (!(e instanceof AmazonServiceException)) {
return false;
}
AmazonServiceException awsException = (AmazonServiceException) e;
if (awsException.getErrorMessage().contains("iamInstanceProfile.name"))
return true; // iam may not be visible immediately after creation
if ("RequestLimitExceeded".equals(awsException.getErrorCode()))
return true; // retry if request rate limit exceeds, this seems depends on load on AWS side, and may happen if bake many instances same time.
return false;
}
public void stopInstances(List<String> instanceIds) throws InterruptedException {
if (instanceIds.isEmpty()) throw new Error("instanceIds must not be empty");
logger.info("stop instances, instanceIds={}", instanceIds);
ec2.stopInstances(new StopInstancesRequest().withInstanceIds(instanceIds));
waitUntil(instanceIds, InstanceState.STOPPED);
}
public void startInstances(List<String> instanceIds) throws InterruptedException {
if (instanceIds.isEmpty()) throw new Error("instanceIds must not be empty");
logger.info("start instances, instanceIds={}", instanceIds);
ec2.startInstances(new StartInstancesRequest().withInstanceIds(instanceIds));
waitUntilRunning(instanceIds);
}
public void terminateInstances(List<String> instanceIds) throws InterruptedException {
logger.info("terminate instances, instanceIds={}", instanceIds);
ec2.terminateInstances(new TerminateInstancesRequest().withInstanceIds(instanceIds));
waitUntil(instanceIds, InstanceState.TERMINATED);
}
public SecurityGroup createSecurityGroup(CreateSecurityGroupRequest request) {
logger.info("create security group, groupName={}", request.getGroupName());
SecurityGroup securityGroup = new SecurityGroup();
CreateSecurityGroupResult result = ec2.createSecurityGroup(request);
securityGroup.setGroupName(request.getGroupName());
securityGroup.setGroupId(result.getGroupId());
return securityGroup;
}
public void deleteSGIngressRules(String securityGroupId, List<IpPermission> rules) {
logger.info("delete ingress sg rules, sgId={}, rules={}", securityGroupId, rules);
ec2.revokeSecurityGroupIngress(new RevokeSecurityGroupIngressRequest()
.withGroupId(securityGroupId)
.withIpPermissions(rules));
}
public void createSGIngressRules(String securityGroupId, List<IpPermission> rules) {
logger.info("create ingress sg rules, sgId={}, rules={}", securityGroupId, rules);
ec2.authorizeSecurityGroupIngress(new AuthorizeSecurityGroupIngressRequest()
.withGroupId(securityGroupId)
.withIpPermissions(rules));
}
public void deleteSecurityGroup(String securityGroupId) {
logger.info("delete security group, securityGroupId={}", securityGroupId);
ec2.deleteSecurityGroup(new DeleteSecurityGroupRequest().withGroupId(securityGroupId));
}
public void createTags(final CreateTagsRequest request) throws Exception {
new Runner<>()
.retryInterval(Duration.ofSeconds(5))
.maxAttempts(3)
.retryOn(e -> e instanceof AmazonServiceException)
.run(() -> {
logger.info("create tags, request={}", request);
ec2.createTags(request);
return null;
});
}
public List<TagDescription> describeTags(DescribeTagsRequest request) {
logger.info("describe tags, request={}", request);
DescribeTagsResult result = ec2.describeTags(request);
Asserts.isNull(result.getNextToken(), "tags pagination is not supported yet, token={}", result.getNextToken());
return result.getTags();
}
public List<Instance> describeInstances(Collection<String> instanceIds) {
if (instanceIds.isEmpty())
throw new IllegalArgumentException("instanceIds can not be empty, otherwise it requires all instances");
logger.info("describe instances, instanceIds={}", instanceIds);
DescribeInstancesResult result = ec2.describeInstances(new DescribeInstancesRequest().withInstanceIds(instanceIds));
return result.getReservations().stream()
.flatMap(reservation -> reservation.getInstances().stream())
.collect(Collectors.toList());
}
public List<Image> describeImages(Collection<String> imageIds) {
if (imageIds.isEmpty())
throw new IllegalArgumentException("imageIds can not be empty, otherwise it requires all images");
logger.info("describe images, imageIds={}", imageIds);
DescribeImagesResult result = ec2.describeImages(new DescribeImagesRequest().withImageIds(imageIds));
return result.getImages();
}
public void deleteImage(Image image) {
String imageId = image.getImageId();
logger.info("delete image, imageId={}", imageId);
ec2.deregisterImage(new DeregisterImageRequest(imageId));
// our image always uses EBS as first and only drive
List<BlockDeviceMapping> mappings = image.getBlockDeviceMappings();
if (!mappings.isEmpty()) {
EbsBlockDevice ebs = mappings.get(0).getEbs();
if (ebs != null) {
String snapshotId = ebs.getSnapshotId();
logger.info("delete snapshot, snapshotId={}", snapshotId);
ec2.deleteSnapshot(new DeleteSnapshotRequest(snapshotId));
}
}
}
public void waitUntilRunning(List<String> instanceIds) throws InterruptedException {
int attempts = 0;
while (true) {
attempts++;
Threads.sleepRoughly(Duration.ofSeconds(30));
List<InstanceStatus> statuses = ec2.describeInstanceStatus(new DescribeInstanceStatusRequest()
.withInstanceIds(instanceIds)).getInstanceStatuses();
if (statuses.size() < instanceIds.size()) {
logger.info("status is not synced, continue to wait");
continue;
}
for (InstanceStatus status : statuses) {
logger.info("instance status {} => {}, checks => {}, {}",
status.getInstanceId(),
status.getInstanceState().getName(),
status.getSystemStatus().getStatus(),
status.getInstanceStatus().getStatus());
}
boolean allOK = statuses.stream().allMatch(status ->
"running".equalsIgnoreCase(status.getInstanceState().getName())
&& "ok".equalsIgnoreCase(status.getSystemStatus().getStatus())
&& "ok".equalsIgnoreCase(status.getInstanceStatus().getStatus()));
if (allOK) {
break;
} else if (attempts > 20) { // roughly after 10 mins
throw new Error("waited too long to get instance status, something is wrong, please check aws console");
}
}
}
private void waitUntil(List<String> instanceIds, final InstanceState expectedState) throws InterruptedException {
while (true) {
Threads.sleepRoughly(Duration.ofSeconds(20));
List<Instance> instances = describeInstances(instanceIds);
for (Instance instance : instances) {
logger.info("instance status {} => {}", instance.getInstanceId(), instance.getState().getName());
}
boolean allOK = instances.stream().allMatch(instance -> expectedState.equalsTo(instance.getState()));
if (allOK) break;
}
}
}