Java tutorial
/* * Copyright (c) 2015-2016 VMware, Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed * under the License is distributed on an "AS IS" BASIS, without warranties or * conditions of any kind, EITHER EXPRESS OR IMPLIED. See the License for the * specific language governing permissions and limitations under the License. */ package com.vmware.photon.controller.model.adapters.awsadapter; import static com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils.allocateSecurityGroup; import static com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils.getSecurityGroup; import static com.vmware.photon.controller.model.adapters.awsadapter.util.AWSNetworkUtils.mapInstanceIPAddressToNICCreationOperations; import static com.vmware.photon.controller.model.constants.PhotonModelConstants.SOURCE_TASK_LINK; import static com.vmware.xenon.common.Operation.STATUS_CODE_UNAUTHORIZED; import java.io.UnsupportedEncodingException; import java.net.URI; import java.util.ArrayList; import java.util.Base64; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import com.amazonaws.AmazonServiceException; import com.amazonaws.handlers.AsyncHandler; import com.amazonaws.services.ec2.AmazonEC2AsyncClient; import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesRequest; import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult; import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ec2.model.RunInstancesRequest; import com.amazonaws.services.ec2.model.RunInstancesResult; import com.amazonaws.services.ec2.model.TerminateInstancesRequest; import com.amazonaws.services.ec2.model.TerminateInstancesResult; import com.vmware.photon.controller.model.adapterapi.ComputeInstanceRequest; import com.vmware.photon.controller.model.adapterapi.ComputeInstanceRequest.InstanceRequestType; import com.vmware.photon.controller.model.adapters.awsadapter.util.AWSClientManager; import com.vmware.photon.controller.model.adapters.awsadapter.util.AWSClientManagerFactory; import com.vmware.photon.controller.model.adapters.util.AdapterUtils; import com.vmware.photon.controller.model.resources.ComputeService.ComputeStateWithDescription; import com.vmware.photon.controller.model.resources.DiskService.DiskState; import com.vmware.photon.controller.model.resources.DiskService.DiskType; import com.vmware.photon.controller.model.tasks.ProvisionComputeTaskService; import com.vmware.photon.controller.model.tasks.ProvisionComputeTaskService.ProvisionComputeTaskState; import com.vmware.xenon.common.Operation; import com.vmware.xenon.common.Operation.CompletionHandler; import com.vmware.xenon.common.OperationContext; import com.vmware.xenon.common.OperationJoin; import com.vmware.xenon.common.ServiceDocument; import com.vmware.xenon.common.ServiceErrorResponse; import com.vmware.xenon.common.StatelessService; import com.vmware.xenon.common.UriUtils; import com.vmware.xenon.common.Utils; import com.vmware.xenon.services.common.AuthCredentialsService.AuthCredentialsServiceState; /** * Adapter to create an EC2 instance on AWS. * */ public class AWSInstanceService extends StatelessService { public static final String SELF_LINK = AWSUriPaths.AWS_INSTANCE_ADAPTER; public static final String AWS_ENVIRONMENT_NAME = "AWS_EC2"; private AWSClientManager clientManager; // The security group specifies things such as the ports to be open, // firewall rules etc and is // specific to an instance and should come from the compute desc for the VM private static final String AWS_RUNNING_NAME = "running"; private static final String AWS_TERMINATED_NAME = "terminated"; public AWSInstanceService() { this.clientManager = AWSClientManagerFactory.getClientManager(false); } @Override public void handleStop(Operation op) { AWSClientManagerFactory.returnClientManager(this.clientManager, false); super.handleStop(op); } @Override public void handlePatch(Operation op) { if (!op.hasBody()) { op.fail(new IllegalArgumentException("body is required")); return; } AWSAllocation aws = new AWSAllocation(op.getBody(ComputeInstanceRequest.class)); switch (aws.computeRequest.requestType) { case VALIDATE_CREDENTIALS: aws.stage = AWSStages.PARENTAUTH; aws.awsOperation = op; handleAllocation(aws); break; default: op.complete(); if (aws.computeRequest.isMockRequest && aws.computeRequest.requestType == ComputeInstanceRequest.InstanceRequestType.CREATE) { AdapterUtils.sendPatchToProvisioningTask(this, aws.computeRequest.taskReference); return; } handleAllocation(aws); } } /* * method will act much like handlePatch, but without persistence as this is a stateless * service. Each call to the service will result in a synchronous execution of the stages below * * Each stage is responsible for setting the next stage on success -- the next stage is passed * into action methods */ private void handleAllocation(AWSAllocation aws) { switch (aws.stage) { case VMDESC: getVMDescription(aws, AWSStages.PARENTDESC); break; case PARENTDESC: getParentDescription(aws, AWSStages.PROVISIONTASK); break; case PROVISIONTASK: getProvisioningTaskReference(aws, AWSStages.PARENTAUTH); break; case PARENTAUTH: getParentAuth(aws, AWSStages.CLIENT); break; case CLIENT: aws.amazonEC2Client = this.clientManager.getOrCreateEC2Client(aws.parentAuth, getRequestRegionId(aws), this, aws.computeRequest.taskReference, false); // now that we have a client lets move onto the next step switch (aws.computeRequest.requestType) { case CREATE: aws.stage = AWSStages.VMDISKS; handleAllocation(aws); break; case DELETE: case DELETE_DOCUMENTS_ONLY: aws.stage = AWSStages.DELETE; handleAllocation(aws); break; case VALIDATE_CREDENTIALS: validateAWSCredentials(aws); break; default: aws.error = new Exception("Unknown AWS provisioning stage"); aws.stage = AWSStages.ERROR; handleAllocation(aws); } break; case DELETE: deleteInstance(aws); break; case VMDISKS: getVMDisks(aws, AWSStages.FIREWALL); break; case FIREWALL: aws.securityGroupId = allocateSecurityGroup(aws); aws.subnetId = getSubnetId(aws); aws.stage = AWSStages.CREATE; handleAllocation(aws); break; case CREATE: createInstance(aws); break; case ERROR: if (aws.computeRequest.taskReference != null) { AdapterUtils.sendFailurePatchToProvisioningTask(this, aws.computeRequest.taskReference, aws.error); } else { aws.awsOperation.fail(aws.error); } break; case DONE: break; default: logSevere("Unhandled stage: %s", aws.stage.toString()); break; } } /* * method will be responsible for getting the compute description for the requested resource and * then passing to the next step */ private void getVMDescription(AWSAllocation aws, AWSStages next) { Consumer<Operation> onSuccess = (op) -> { aws.child = op.getBody(ComputeStateWithDescription.class); aws.stage = next; handleAllocation(aws); }; URI computeUri = UriUtils.extendUriWithQuery(aws.computeRequest.resourceReference, UriUtils.URI_PARAM_ODATA_EXPAND, Boolean.TRUE.toString()); AdapterUtils.getServiceState(this, computeUri, onSuccess, getFailureConsumer(aws)); } /* * Gets the provisioning task reference for this operation. Sets the task expiration time in the * context to be used for bounding the status checks for creation and termination requests. */ private void getProvisioningTaskReference(AWSAllocation aws, AWSStages next) { Consumer<Operation> onSuccess = (op) -> { ProvisionComputeTaskState provisioningTaskState = op.getBody(ProvisionComputeTaskState.class); aws.taskExpirationMicros = provisioningTaskState.documentExpirationTimeMicros; aws.stage = next; handleAllocation(aws); }; AdapterUtils.getServiceState(this, aws.computeRequest.taskReference, onSuccess, getFailureConsumer(aws)); } /* * Method will get the service for the identified link */ private void getParentDescription(AWSAllocation aws, AWSStages next) { Consumer<Operation> onSuccess = (op) -> { aws.parent = op.getBody(ComputeStateWithDescription.class); aws.stage = next; handleAllocation(aws); }; URI parentURI = UriUtils.buildExpandLinksQueryUri(UriUtils.buildUri(this.getHost(), aws.child.parentLink)); AdapterUtils.getServiceState(this, parentURI, onSuccess, getFailureConsumer(aws)); } private void getParentAuth(AWSAllocation aws, AWSStages next) { String parentAuthLink; if (aws.computeRequest.requestType == InstanceRequestType.VALIDATE_CREDENTIALS) { parentAuthLink = aws.computeRequest.authCredentialsLink; } else { parentAuthLink = aws.parent.description.authCredentialsLink; } Consumer<Operation> onSuccess = (op) -> { aws.parentAuth = op.getBody(AuthCredentialsServiceState.class); aws.stage = next; handleAllocation(aws); }; AdapterUtils.getServiceState(this, parentAuthLink, onSuccess, getFailureConsumer(aws)); } private Consumer<Throwable> getFailureConsumer(AWSAllocation aws) { return (t) -> { aws.stage = AWSStages.ERROR; aws.error = t; handleAllocation(aws); }; } /* * method will retrieve disks for targeted image */ private void getVMDisks(AWSAllocation aws, AWSStages next) { if (aws.child.diskLinks == null || aws.child.diskLinks.size() == 0) { aws.error = new IllegalStateException("a minimum of 1 disk is required"); aws.stage = AWSStages.ERROR; handleAllocation(aws); return; } Collection<Operation> operations = new ArrayList<>(); // iterate thru disks and create operations for (String disk : aws.child.diskLinks) { operations.add(Operation.createGet(UriUtils.buildUri(this.getHost(), disk))); } OperationJoin operationJoin = OperationJoin.create(operations).setCompletion((ops, exc) -> { if (exc != null) { aws.error = new IllegalStateException("Error getting disk information"); aws.stage = AWSStages.ERROR; handleAllocation(aws); return; } aws.childDisks = new HashMap<>(); for (Operation op : ops.values()) { DiskState disk = op.getBody(DiskState.class); aws.childDisks.put(disk.type, disk); } aws.stage = next; handleAllocation(aws); }); operationJoin.sendWith(this); } private void createInstance(AWSAllocation aws) { if (aws.computeRequest.isMockRequest) { AdapterUtils.sendPatchToProvisioningTask(this, aws.computeRequest.taskReference); return; } DiskState bootDisk = aws.childDisks.get(DiskType.HDD); if (bootDisk == null) { AdapterUtils.sendFailurePatchToProvisioningTask(this, aws.computeRequest.taskReference, new IllegalStateException("AWS bootDisk not specified")); return; } if (bootDisk.bootConfig != null && bootDisk.bootConfig.files.length > 1) { AdapterUtils.sendFailurePatchToProvisioningTask(this, aws.computeRequest.taskReference, new IllegalStateException("Only 1 configuration file allowed")); return; } URI imageId = bootDisk.sourceImageReference; if (imageId == null) { aws.error = new IllegalStateException("AWS ImageId not specified"); aws.stage = AWSStages.ERROR; handleAllocation(aws); return; } // This a single disk state with a bootConfig. There's no expectation // that it does exists, but if it does, we only support cloud configs at // this point. String cloudConfig = null; if (bootDisk.bootConfig != null && bootDisk.bootConfig.files.length > 0) { cloudConfig = bootDisk.bootConfig.files[0].contents; } String instanceType = aws.child.description.instanceType; if (instanceType == null) { // fallback to legacy usage of name instanceType = aws.child.description.name; } if (instanceType == null) { aws.error = new IllegalStateException("AWS Instance type not specified"); aws.stage = AWSStages.ERROR; handleAllocation(aws); return; } // let's try one more time for the security group -- just in case it // didn't // get retrieved during the firewall stage // // This will be removed in EN-1251 if (aws.securityGroupId == null) { aws.securityGroupId = getSecurityGroup(aws.amazonEC2Client).getGroupId(); // if we still don't have it kill allocation if (aws.securityGroupId == null) { aws.error = new IllegalStateException("SecurityGroup not found"); aws.stage = AWSStages.ERROR; handleAllocation(aws); return; } } RunInstancesRequest runInstancesRequest = new RunInstancesRequest().withImageId(imageId.toString()) .withInstanceType(instanceType).withMinCount(1).withMaxCount(1).withMonitoring(true) .withSecurityGroupIds(aws.securityGroupId); // use the subnet if provided if (aws.subnetId != null) { runInstancesRequest = runInstancesRequest.withSubnetId(aws.subnetId); } if (cloudConfig != null) { try { runInstancesRequest .setUserData(Base64.getEncoder().encodeToString(cloudConfig.getBytes(Utils.CHARSET))); } catch (UnsupportedEncodingException e) { aws.error = new IllegalStateException("Error encoding user data"); aws.stage = AWSStages.ERROR; handleAllocation(aws); return; } } // handler invoked once the EC2 runInstancesAsync commands completes AsyncHandler<RunInstancesRequest, RunInstancesResult> creationHandler = buildCreationCallbackHandler(this, aws.computeRequest, aws.child, aws.amazonEC2Client, aws.taskExpirationMicros); aws.amazonEC2Client.runInstancesAsync(runInstancesRequest, creationHandler); } private static class AWSCreationHandler implements AsyncHandler<RunInstancesRequest, RunInstancesResult> { private StatelessService service; private ComputeInstanceRequest computeReq; private ComputeStateWithDescription computeDesc; private AmazonEC2AsyncClient amazonEC2Client; private OperationContext opContext; private long taskExpirationTimeMicros; private AWSCreationHandler(StatelessService service, ComputeInstanceRequest computeReq, ComputeStateWithDescription computeDesc, AmazonEC2AsyncClient amazonEC2Client, long taskExpirationTimeMicros) { this.service = service; this.computeReq = computeReq; this.computeDesc = computeDesc; this.amazonEC2Client = amazonEC2Client; this.opContext = OperationContext.getOperationContext(); this.taskExpirationTimeMicros = taskExpirationTimeMicros; } @Override public void onError(Exception exception) { OperationContext.restoreOperationContext(this.opContext); AdapterUtils.sendFailurePatchToProvisioningTask(this.service, this.computeReq.taskReference, exception); } @Override public void onSuccess(RunInstancesRequest request, RunInstancesResult result) { List<Operation> createOperations = new ArrayList<Operation>(); // consumer to be invoked once a VM is in the running state Consumer<Instance> consumer = instance -> { OperationContext.restoreOperationContext(this.opContext); if (instance == null) { AdapterUtils.sendFailurePatchToProvisioningTask(this.service, this.computeReq.taskReference, new IllegalStateException("Error getting instance EC2 instance")); return; } ComputeStateWithDescription resultDesc = new ComputeStateWithDescription(); resultDesc.address = instance.getPublicIpAddress(); resultDesc.powerState = AWSUtils.mapToPowerState(instance.getState()); if (this.computeDesc.customProperties == null) { resultDesc.customProperties = new HashMap<String, String>(); } else { resultDesc.customProperties = this.computeDesc.customProperties; } resultDesc.customProperties.put(SOURCE_TASK_LINK, ProvisionComputeTaskService.FACTORY_LINK); resultDesc.id = instance.getInstanceId(); resultDesc.customProperties.put(AWSConstants.AWS_VPC_ID, instance.getVpcId()); // Create operations List<Operation> networkOperations = mapInstanceIPAddressToNICCreationOperations(instance, resultDesc, this.computeDesc.tenantLinks, this.service); if (networkOperations != null && !networkOperations.isEmpty()) { createOperations.addAll(networkOperations); } Operation patchState = Operation.createPatch(this.computeReq.resourceReference).setBody(resultDesc) .setReferer(this.service.getHost().getUri()); createOperations.add(patchState); OperationJoin.JoinedCompletionHandler joinCompletion = (ox, exc) -> { if (exc != null) { this.service.logSevere("Error updating VM state. %s", Utils.toString(exc)); AdapterUtils.sendFailurePatchToProvisioningTask(this.service, this.computeReq.taskReference, new IllegalStateException("Error updating VM state")); return; } AdapterUtils.sendPatchToProvisioningTask(this.service, this.computeReq.taskReference); }; OperationJoin joinOp = OperationJoin.create(createOperations); joinOp.setCompletion(joinCompletion); joinOp.sendWith(this.service.getHost()); }; String instanceId = result.getReservation().getInstances().get(0).getInstanceId(); AWSTaskStatusChecker.create(this.amazonEC2Client, instanceId, AWSInstanceService.AWS_RUNNING_NAME, consumer, this.computeReq, this.service, this.taskExpirationTimeMicros).start(); } } // callback to be invoked when a VM creation operation returns private AsyncHandler<RunInstancesRequest, RunInstancesResult> buildCreationCallbackHandler( StatelessService service, ComputeInstanceRequest computeReq, ComputeStateWithDescription computeDesc, AmazonEC2AsyncClient amazonEC2Client, long taskExpirationTimeMicros) { return new AWSCreationHandler(service, computeReq, computeDesc, amazonEC2Client, taskExpirationTimeMicros); } private void deleteInstance(AWSAllocation aws) { if (aws.computeRequest.isMockRequest || aws.computeRequest.requestType == InstanceRequestType.DELETE_DOCUMENTS_ONLY) { deleteComputeResource(this, aws.child, aws.computeRequest); return; } String instanceId = aws.child.id; if (instanceId == null) { aws.error = new IllegalStateException("AWS InstanceId not available"); aws.stage = AWSStages.ERROR; handleAllocation(aws); return; } List<String> instanceIdList = new ArrayList<String>(); instanceIdList.add(instanceId); TerminateInstancesRequest termRequest = new TerminateInstancesRequest(instanceIdList); StatelessService service = this; AsyncHandler<TerminateInstancesRequest, TerminateInstancesResult> terminateHandler = buildTerminationCallbackHandler( service, aws.computeRequest, aws.child, aws.amazonEC2Client, instanceId, aws.taskExpirationMicros); aws.amazonEC2Client.terminateInstancesAsync(termRequest, terminateHandler); } private class AWSTerminateHandler implements AsyncHandler<TerminateInstancesRequest, TerminateInstancesResult> { private StatelessService service; private ComputeInstanceRequest computeReq; private ComputeStateWithDescription computeDesc; private AmazonEC2AsyncClient amazonEC2Client; private OperationContext opContext; private String instanceId; private long taskExpirationTimeMicros; private AWSTerminateHandler(StatelessService service, ComputeInstanceRequest computeReq, ComputeStateWithDescription computeDesc, AmazonEC2AsyncClient amazonEC2Client, String instanceId, long taskExpirationTimeMicros) { this.service = service; this.computeReq = computeReq; this.computeDesc = computeDesc; this.amazonEC2Client = amazonEC2Client; this.opContext = OperationContext.getOperationContext(); this.instanceId = instanceId; this.taskExpirationTimeMicros = taskExpirationTimeMicros; } @Override public void onError(Exception exception) { OperationContext.restoreOperationContext(this.opContext); AdapterUtils.sendFailurePatchToProvisioningTask(this.service, this.computeReq.taskReference, exception); } @Override public void onSuccess(TerminateInstancesRequest request, TerminateInstancesResult result) { Consumer<Instance> consumer = new Consumer<Instance>() { @Override public void accept(Instance instance) { OperationContext.restoreOperationContext(AWSTerminateHandler.this.opContext); if (instance == null) { AdapterUtils.sendFailurePatchToProvisioningTask(AWSTerminateHandler.this.service, AWSTerminateHandler.this.computeReq.taskReference, new IllegalStateException("Error getting instance")); return; } deleteComputeResource(AWSTerminateHandler.this.service, AWSTerminateHandler.this.computeDesc, AWSTerminateHandler.this.computeReq); } }; AWSTaskStatusChecker .create(this.amazonEC2Client, this.instanceId, AWSInstanceService.AWS_TERMINATED_NAME, consumer, this.computeReq, this.service, this.taskExpirationTimeMicros) .start(); } } // callback handler to be invoked once a aws terminate calls returns private AsyncHandler<TerminateInstancesRequest, TerminateInstancesResult> buildTerminationCallbackHandler( StatelessService service, ComputeInstanceRequest computeReq, ComputeStateWithDescription computeDesc, AmazonEC2AsyncClient amazonEC2Client, String instanceId, long taskExpirationTimeMicros) { return new AWSTerminateHandler(service, computeReq, computeDesc, amazonEC2Client, instanceId, taskExpirationTimeMicros); } private void deleteComputeResource(StatelessService service, ComputeStateWithDescription computeDesc, ComputeInstanceRequest computeReq) { List<String> resourcesToDelete = new ArrayList<>(); resourcesToDelete.add(computeDesc.documentSelfLink); if (computeDesc.diskLinks != null) { resourcesToDelete.addAll(computeDesc.diskLinks); } AtomicInteger deleteCallbackCount = new AtomicInteger(0); CompletionHandler deletionKickoffCompletion = (sendDeleteOp, sendDeleteEx) -> { if (sendDeleteEx != null) { AdapterUtils.sendFailurePatchToProvisioningTask(this, computeReq.taskReference, sendDeleteEx); return; } if (deleteCallbackCount.incrementAndGet() == resourcesToDelete.size()) { AdapterUtils.sendPatchToProvisioningTask(this, computeReq.taskReference); } }; for (String resourcetoDelete : resourcesToDelete) { sendRequest(Operation.createDelete(UriUtils.buildUri(service.getHost(), resourcetoDelete)) .setBody(new ServiceDocument()).setCompletion(deletionKickoffCompletion)); } } /* * Simple helper method to get the region id from either the compute request or the child * description. * * The child description will be null during a credential validation operation. */ private String getRequestRegionId(AWSAllocation aws) { String regionId; if (aws.child == null) { regionId = aws.computeRequest.regionId; } else { regionId = aws.child.description.zoneId; } return regionId; } /* * Helper method to get amazon subnet id provided in the description custom properties */ private String getSubnetId(AWSAllocation aws) { if (aws.child != null && aws.child.description != null && aws.child.description.customProperties != null) { return aws.child.description.customProperties.get(AWSConstants.AWS_SUBNET_ID); } return null; } private void validateAWSCredentials(final AWSAllocation aws) { if (aws.computeRequest.isMockRequest) { aws.awsOperation.complete(); return; } aws.amazonEC2Client = this.clientManager.getOrCreateEC2Client(aws.parentAuth, getRequestRegionId(aws), this, aws.computeRequest.taskReference, false); // make a call to validate credentials aws.amazonEC2Client.describeAvailabilityZonesAsync(new DescribeAvailabilityZonesRequest(), new AsyncHandler<DescribeAvailabilityZonesRequest, DescribeAvailabilityZonesResult>() { @Override public void onError(Exception e) { if (e instanceof AmazonServiceException) { AmazonServiceException ase = (AmazonServiceException) e; if (ase.getStatusCode() == STATUS_CODE_UNAUTHORIZED) { ServiceErrorResponse r = Utils.toServiceErrorResponse(e); r.statusCode = STATUS_CODE_UNAUTHORIZED; aws.awsOperation.fail(e, r); } return; } aws.awsOperation.fail(e); } @Override public void onSuccess(DescribeAvailabilityZonesRequest request, DescribeAvailabilityZonesResult describeAvailabilityZonesResult) { aws.awsOperation.complete(); } }); } }