
com.vmware.photon.controller.model.adapters.awsadapter.AWSInstanceService Maven / Gradle / Ivy
/*
* 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.ComputeProperties.CUSTOM_OS_TYPE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWSStorageType;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWSSupportedOS;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWSSupportedVirtualizationTypes;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWS_DEPENDENCY_VIOLATION_ERROR_CODE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWS_INSTANCE_ID_PREFIX;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWS_INVALID_INSTANCE_ID_ERROR_CODE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWS_TAG_NAME;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWS_VIRTUAL_NAMES;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.DEVICE_NAME;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.DEVICE_TYPE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.DISK_IOPS;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.MAX_IOPS_PER_GiB;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.VOLUME_TYPE;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.VOLUME_TYPE_PROVISIONED_SSD;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils.removeDiskLinks;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils.setDeleteOnTerminateAttribute;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils.setEbsDefaultsIfNotSet;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils.updatePersistentDiskAsAvailable;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils.validateSizeSupportedByVolumeType;
import static com.vmware.photon.controller.model.constants.PhotonModelConstants.CLOUD_CONFIG_DEFAULT_FILE_INDEX;
import static com.vmware.photon.controller.model.constants.PhotonModelConstants.CUSTOM_PROP_SSH_KEY_NAME;
import static com.vmware.photon.controller.model.constants.PhotonModelConstants.SOURCE_TASK_LINK;
import static com.vmware.photon.controller.model.util.PhotonModelUriUtils.createInventoryUri;
import static com.vmware.xenon.common.Operation.STATUS_CODE_UNAUTHORIZED;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.handlers.AsyncHandler;
import com.amazonaws.services.ec2.AmazonEC2AsyncClient;
import com.amazonaws.services.ec2.model.AmazonEC2Exception;
import com.amazonaws.services.ec2.model.AttachVolumeRequest;
import com.amazonaws.services.ec2.model.AttachVolumeResult;
import com.amazonaws.services.ec2.model.BlockDeviceMapping;
import com.amazonaws.services.ec2.model.DeleteSubnetRequest;
import com.amazonaws.services.ec2.model.DeleteSubnetResult;
import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesRequest;
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.DescribeVolumesRequest;
import com.amazonaws.services.ec2.model.DescribeVolumesResult;
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.InstanceBlockDeviceMapping;
import com.amazonaws.services.ec2.model.InstanceNetworkInterface;
import com.amazonaws.services.ec2.model.Placement;
import com.amazonaws.services.ec2.model.ResourceType;
import com.amazonaws.services.ec2.model.RunInstancesRequest;
import com.amazonaws.services.ec2.model.RunInstancesResult;
import com.amazonaws.services.ec2.model.Tag;
import com.amazonaws.services.ec2.model.TagSpecification;
import com.amazonaws.services.ec2.model.TerminateInstancesRequest;
import com.amazonaws.services.ec2.model.TerminateInstancesResult;
import org.apache.commons.lang3.EnumUtils;
import com.vmware.photon.controller.model.ComputeProperties;
import com.vmware.photon.controller.model.Constraint.Condition;
import com.vmware.photon.controller.model.Constraint.Condition.Enforcement;
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.AWSInstanceContext.AWSNicContext;
import com.vmware.photon.controller.model.adapters.awsadapter.util.AWSAsyncHandler;
import com.vmware.photon.controller.model.adapters.awsadapter.util.AWSBlockDeviceNameMapper;
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.awsadapter.util.AWSDeferredResultAsyncHandler;
import com.vmware.photon.controller.model.adapters.awsadapter.util.AWSEnumerationUtils;
import com.vmware.photon.controller.model.adapters.util.AdapterUtils;
import com.vmware.photon.controller.model.adapters.util.BaseAdapterContext.BaseAdapterStage;
import com.vmware.photon.controller.model.adapters.util.Pair;
import com.vmware.photon.controller.model.query.QueryUtils.QueryTop;
import com.vmware.photon.controller.model.resources.ComputeDescriptionService.ComputeDescription;
import com.vmware.photon.controller.model.resources.ComputeDescriptionService.ComputeDescription.ComputeType;
import com.vmware.photon.controller.model.resources.ComputeService;
import com.vmware.photon.controller.model.resources.ComputeService.ComputeState;
import com.vmware.photon.controller.model.resources.ComputeService.LifecycleState;
import com.vmware.photon.controller.model.resources.DiskService;
import com.vmware.photon.controller.model.resources.DiskService.DiskState;
import com.vmware.photon.controller.model.resources.NetworkInterfaceService.NetworkInterfaceState;
import com.vmware.photon.controller.model.resources.ResourceState;
import com.vmware.photon.controller.model.resources.SubnetService.SubnetState;
import com.vmware.photon.controller.model.support.InstanceTypeList.InstanceType;
import com.vmware.photon.controller.model.tasks.ProvisionComputeTaskService;
import com.vmware.photon.controller.model.tasks.ProvisionComputeTaskService.ProvisionComputeTaskState;
import com.vmware.photon.controller.model.util.AssertUtil;
import com.vmware.photon.controller.model.util.ClusterUtil.ServiceTypeCluster;
import com.vmware.xenon.common.DeferredResult;
import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.OperationContext;
import com.vmware.xenon.common.OperationJoin;
import com.vmware.xenon.common.ServiceErrorResponse;
import com.vmware.xenon.common.StatelessService;
import com.vmware.xenon.common.Utils;
import com.vmware.xenon.services.common.QueryTask.Query;
import com.vmware.xenon.services.common.QueryTask.Query.Builder;
/**
* Adapter to create an EC2 instance on AWS.
*/
public class AWSInstanceService extends StatelessService {
public static final String SELF_LINK = AWSUriPaths.AWS_INSTANCE_ADAPTER;
private AWSClientManager clientManager;
/**
* Extend default 'start' logic with loading AWS client.
*/
@Override
public void handleStart(Operation op) {
this.clientManager = AWSClientManagerFactory
.getClientManager(AWSConstants.AwsClientType.EC2);
super.handleStart(op);
}
@Override
public void handleStop(Operation op) {
AWSClientManagerFactory.returnClientManager(this.clientManager,
AWSConstants.AwsClientType.EC2);
super.handleStop(op);
}
@Override
public void handlePatch(Operation op) {
if (!op.hasBody()) {
op.fail(new IllegalArgumentException("body is required"));
return;
}
AWSInstanceContext ctx = new AWSInstanceContext(this,
op.getBody(ComputeInstanceRequest.class));
try {
final BaseAdapterStage startingStage;
final AWSInstanceStage nextStage;
switch (ctx.computeRequest.requestType) {
case VALIDATE_CREDENTIALS:
ctx.operation = op;
startingStage = BaseAdapterStage.PARENTAUTH;
nextStage = AWSInstanceStage.CLIENT;
break;
default:
op.complete();
if (ctx.computeRequest.isMockRequest
&& ctx.computeRequest.requestType == InstanceRequestType.CREATE) {
ctx.taskManager.finishTask();
return;
}
startingStage = BaseAdapterStage.VMDESC;
nextStage = AWSInstanceStage.PROVISIONTASK;
break;
}
// Populate BaseAdapterContext and then continue with this state machine
ctx.populateBaseContext(startingStage).whenComplete(thenAllocation(ctx, nextStage));
} catch (Throwable t) {
finishExceptionally(ctx, t);
}
}
/**
* Shortcut method that sets the next stage into the context and delegates to {@link
* #handleAllocation(AWSInstanceContext)}.
*/
private void handleAllocation(AWSInstanceContext context, AWSInstanceStage nextStage) {
context.stage = nextStage;
handleAllocation(context);
}
/**
* Shortcut method that stores the error into context, sets next stage to {@link
* AWSInstanceStage#ERROR} and delegates to {@link #handleAllocation(AWSInstanceContext)}.
*/
private void handleError(AWSInstanceContext context, Throwable e) {
context.error = e;
context.stage = AWSInstanceStage.ERROR;
handleAllocation(context);
}
/**
* {@code handleAllocation} version suitable for chaining to {@code
* DeferredResult.whenComplete}.
*/
private BiConsumer thenAllocation(AWSInstanceContext context,
AWSInstanceStage next) {
// NOTE: In case of error 'ignoreCtx' is null so use passed context!
return (ignoreCtx, exc) -> {
if (exc != null) {
handleError(context, exc);
return;
}
handleAllocation(context, next);
};
}
/**
* State machine to handle different stages of VM creation/deletion.
*
* 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
* @see #handleError(AWSInstanceContext, Throwable)
* @see #handleAllocation(AWSInstanceContext, AWSInstanceStage)
*/
private void handleAllocation(AWSInstanceContext context) {
logFine(() -> String.format("Transition to: %s", context.stage));
try {
switch (context.stage) {
case PROVISIONTASK:
getProvisioningTaskReference(context, AWSInstanceStage.CLIENT);
break;
case CLIENT:
Consumer c = t -> {
if (context.computeRequest.requestType
== InstanceRequestType.VALIDATE_CREDENTIALS) {
context.operation.fail(t);
} else {
context.taskManager.patchTaskToFailure(t);
}
};
this.clientManager.getOrCreateEC2ClientAsync(context.endpointAuth,
getRequestRegionId(context), this)
.whenComplete((ec2Client, t) -> {
if (t != null) {
c.accept(t);
return;
}
context.amazonEC2Client = ec2Client;
if (context.amazonEC2Client == null) {
return;
}
// now that we have a client lets move onto the next step
switch (context.computeRequest.requestType) {
case CREATE:
handleAllocation(context, AWSInstanceStage.POPULATE_CONTEXT);
break;
case DELETE:
handleAllocation(context, AWSInstanceStage.DELETE);
break;
case VALIDATE_CREDENTIALS:
validateAWSCredentials(context);
break;
default:
handleError(context,
new IllegalStateException("Unknown AWS provisioning stage: "
+ context.computeRequest.requestType));
}
});
break;
case DELETE:
deleteInstance(context);
break;
case POPULATE_CONTEXT:
context.populateContext()
.whenComplete(thenAllocation(context, AWSInstanceStage.CREATE));
break;
case CREATE:
createInstance(context);
break;
case ERROR:
finishExceptionally(context);
break;
default:
handleError(context,
new IllegalStateException("Unknown AWS context stage: " + context.stage));
break;
}
} catch (Throwable e) {
// NOTE: Do not use handleError(err) cause that might result in endless recursion.
finishExceptionally(context, e);
}
}
private void finishExceptionally(AWSInstanceContext context, Throwable failure) {
context.error = failure;
finishExceptionally(context);
}
private void finishExceptionally(AWSInstanceContext context) {
String errorMessage = context.error != null ? context.error.getMessage() : "no error set";
this.logWarning(() -> "[AWSInstanceService] finished exceptionally. " + errorMessage);
context.taskManager.patchTaskToFailure(context.error);
if (context.operation != null) {
context.operation.fail(context.error);
}
}
/*
* 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(AWSInstanceContext aws, AWSInstanceStage next) {
Consumer onSuccess = (op) -> {
ProvisionComputeTaskState provisioningTaskState = op
.getBody(ProvisionComputeTaskState.class);
aws.taskExpirationMicros = provisioningTaskState.documentExpirationTimeMicros;
handleAllocation(aws, next);
};
AdapterUtils.getServiceState(this, aws.computeRequest.taskReference, onSuccess,
getFailureConsumer(aws));
}
private Consumer getFailureConsumer(AWSInstanceContext aws) {
return (t) -> handleError(aws, t);
}
private void createInstance(AWSInstanceContext aws) {
if (aws.computeRequest.isMockRequest) {
aws.taskManager.finishTask();
return;
}
final DiskState bootDisk = aws.bootDisk;
if (bootDisk == null) {
aws.taskManager
.patchTaskToFailure(new IllegalStateException("AWS bootDisk not specified"));
return;
}
if (bootDisk.bootConfig != null && bootDisk.bootConfig.files.length > 1) {
aws.taskManager.patchTaskToFailure(
new IllegalStateException("Only 1 configuration file allowed"));
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 > CLOUD_CONFIG_DEFAULT_FILE_INDEX) {
cloudConfig = bootDisk.bootConfig.files[CLOUD_CONFIG_DEFAULT_FILE_INDEX].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 = AWSInstanceStage.ERROR;
handleAllocation(aws);
return;
}
RunInstancesRequest runInstancesRequest = new RunInstancesRequest()
.withImageId(aws.bootDiskImageNativeId)
.withInstanceType(instanceType)
.withMinCount(1)
.withMaxCount(1)
.withMonitoring(true)
.withTagSpecifications(new TagSpecification()
.withResourceType(ResourceType.Instance)
.withTags(aws.getAWSTags())
);
if (aws.placement != null) {
runInstancesRequest.withPlacement(new Placement(aws.placement));
}
if (aws.child.customProperties != null &&
aws.child.customProperties.containsKey(CUSTOM_PROP_SSH_KEY_NAME)) {
runInstancesRequest = runInstancesRequest.withKeyName(aws.child.customProperties
.get(CUSTOM_PROP_SSH_KEY_NAME));
}
if (!aws.dataDisks.isEmpty() || bootDisk.capacityMBytes > 0 ||
bootDisk.customProperties != null) {
DescribeImagesRequest imagesDescriptionRequest = new DescribeImagesRequest();
imagesDescriptionRequest.withImageIds(aws.bootDiskImageNativeId);
DescribeImagesResult imagesDescriptionResult =
aws.amazonEC2Client.describeImages(imagesDescriptionRequest);
if (imagesDescriptionResult.getImages().size() != 1) {
handleError(aws, new IllegalStateException("AWS ImageId is not available"));
return;
}
Image image = imagesDescriptionResult.getImages().get(0);
AssertUtil.assertNotNull(aws.instanceTypeInfo, "instanceType cannot be null");
List blockDeviceMappings = image.getBlockDeviceMappings();
String rootDeviceType = image.getRootDeviceType();
String bootDiskType = bootDisk.customProperties.get(DEVICE_TYPE);
boolean hasHardConstraint = containsHardConstraint(bootDisk);
BlockDeviceMapping rootDeviceMapping = null;
try {
// The number of instance-store disks that will be provisioned is limited by the instance-type.
suppressExcessInstanceStoreDevices(blockDeviceMappings, aws.instanceTypeInfo);
for (BlockDeviceMapping blockDeviceMapping : blockDeviceMappings) {
EbsBlockDevice ebs = blockDeviceMapping.getEbs();
String diskType = getDeviceType(ebs);
if (hasHardConstraint) {
validateIfDeviceTypesAreMatching(diskType, bootDiskType);
}
if (blockDeviceMapping.getNoDevice() != null) {
continue;
}
if (rootDeviceType.equals(AWSStorageType.EBS.getName()) &&
blockDeviceMapping.getDeviceName().equals(image.getRootDeviceName())) {
rootDeviceMapping = blockDeviceMapping;
continue;
}
DiskState diskState = new DiskState();
copyCustomProperties(diskState, bootDisk);
addMandatoryProperties(diskState, blockDeviceMapping, aws);
updateDeviceMapping(diskType, bootDiskType, blockDeviceMapping.getDeviceName(),
ebs,
diskState);
//update disk state with final volume-type and iops
if (diskType.equals(AWSStorageType.EBS.getName())) {
diskState.customProperties.put(VOLUME_TYPE, ebs.getVolumeType());
diskState.customProperties.put(DISK_IOPS, String.valueOf(ebs.getIops()));
}
aws.imageDisks.add(diskState);
}
customizeBootDiskProperties(bootDisk, rootDeviceType, rootDeviceMapping,
hasHardConstraint, aws);
List ebsDisks = new ArrayList<>();
List instanceStoreDisks = new ArrayList<>();
if (!aws.dataDisks.isEmpty()) {
if (!rootDeviceType.equals(AWSStorageType.EBS.name().toLowerCase())) {
instanceStoreDisks = aws.dataDisks;
assertAndResetPersistence(instanceStoreDisks);
validateSupportForAdditionalInstanceStoreDisks(instanceStoreDisks,
blockDeviceMappings, aws.instanceTypeInfo, rootDeviceType);
} else {
splitDataDisks(aws.dataDisks, instanceStoreDisks, ebsDisks);
setEbsDefaultsIfNotSpecified(ebsDisks, Boolean.FALSE);
if (!instanceStoreDisks.isEmpty()) {
assertAndResetPersistence(instanceStoreDisks);
validateSupportForAdditionalInstanceStoreDisks(instanceStoreDisks,
blockDeviceMappings, aws.instanceTypeInfo, rootDeviceType);
}
}
}
//get the available attach paths for new disks and external disks
List usedDeviceNames = null;
if (!instanceStoreDisks.isEmpty() || !ebsDisks.isEmpty() ||
!aws.externalDisks.isEmpty()) {
usedDeviceNames = getUsedDeviceNames(blockDeviceMappings);
}
if (!instanceStoreDisks.isEmpty()) {
List usedVirtualNames = getUsedVirtualNames(blockDeviceMappings);
blockDeviceMappings.addAll(createInstanceStoreMappings(instanceStoreDisks,
usedDeviceNames, usedVirtualNames, aws.instanceTypeInfo.id,
aws.instanceTypeInfo.dataDiskSizeInMB,
image.getPlatform(),
image.getVirtualizationType()));
}
if (!ebsDisks.isEmpty() || !aws.externalDisks.isEmpty()) {
aws.availableEbsDiskNames = AWSBlockDeviceNameMapper.getAvailableNames(
AWSSupportedOS.get(image.getPlatform()),
AWSSupportedVirtualizationTypes.get(image.getVirtualizationType()),
AWSStorageType.EBS, instanceType, usedDeviceNames);
}
if (!ebsDisks.isEmpty()) {
blockDeviceMappings.addAll(createEbsDeviceMappings(ebsDisks,
aws.availableEbsDiskNames));
}
runInstancesRequest.withBlockDeviceMappings(blockDeviceMappings);
} catch (Exception e) {
aws.error = e;
aws.stage = AWSInstanceStage.ERROR;
handleAllocation(aws);
return;
}
}
AWSNicContext primaryNic = aws.getPrimaryNic();
if (primaryNic != null && primaryNic.nicSpec != null) {
runInstancesRequest.withNetworkInterfaces(primaryNic.nicSpec);
} else {
runInstancesRequest.withSecurityGroupIds(AWSUtils.getOrCreateSecurityGroups(aws, null));
}
if (cloudConfig != null) {
try {
runInstancesRequest.setUserData(Base64.getEncoder()
.encodeToString(cloudConfig.getBytes(Utils.CHARSET)));
} catch (UnsupportedEncodingException e) {
handleError(aws, new IllegalStateException("Error encoding user data"));
return;
}
}
String message = "[AWSInstanceService] Sending run instance request for instance id: "
+ aws.bootDiskImageNativeId
+ ", instance type: " + instanceType
+ ", parent task id: " + aws.computeRequest.taskReference;
this.logInfo(() -> message);
// handler invoked once the EC2 runInstancesAsync commands completes
AsyncHandler creationHandler = new AWSCreationHandler(
this, aws);
aws.amazonEC2Client.runInstancesAsync(runInstancesRequest, creationHandler);
}
/**
* Instance Type(like r3.large etc) supports a limited number of instance-store disks. All the
* extra instance-store mappings that are more than those supported by the instance-type are
* termed as excess. Excess mappings does not result in provisioning a disk.
*
* Suppress the excess instance-store mappings in the image by setting NoDevice to 'true'.
*/
private void suppressExcessInstanceStoreDevices(List deviceMappings,
InstanceType type) {
List unsuppressedInstanceStoreMappings =
getUnsuppressedInstanceStoreMappings(deviceMappings);
int imageInstanceStoreCount = unsuppressedInstanceStoreMappings != null ?
unsuppressedInstanceStoreMappings.size() : 0;
int maxSupported = type.dataDiskMaxCount != null ? type.dataDiskMaxCount : 0;
if (imageInstanceStoreCount > maxSupported) {
for (int i = 0; i < imageInstanceStoreCount; i++) {
if (i >= maxSupported) {
unsuppressedInstanceStoreMappings.get(i).setNoDevice("");
}
}
}
}
private boolean containsHardConstraint(DiskState bootDisk) {
boolean hasHardConstraint = false;
if (bootDisk.constraint != null) {
List bootDiskConditions = bootDisk.constraint.conditions;
if (bootDiskConditions != null) {
if (bootDiskConditions.stream()
.anyMatch(condition -> condition.enforcement == Enforcement.HARD)) {
hasHardConstraint = true;
}
}
}
return hasHardConstraint;
}
/**
* update the deviceMappings for ebs devices.
*/
private void updateDeviceMapping(String currentDiskType, String requestedDiskType,
String deviceName, EbsBlockDevice ebs, DiskState diskState) {
if (requestedDiskType != null) {
String ebsType = AWSStorageType.EBS.getName();
String instanceStoreType = AWSStorageType.INSTANCE_STORE.getName();
if (requestedDiskType.equals(ebsType) && currentDiskType.equals(ebsType)) {
updateEbsBlockDeviceMapping(ebs, diskState);
} else if (requestedDiskType.equals(instanceStoreType) && currentDiskType
.equals(instanceStoreType)) {
String message = String.format("[AWSInstanceService] No customization is applied"
+ " to image disk at %s", deviceName);
this.logInfo(message);
} else {
String message = String
.format("[AWSInstanceService] Image disk at %s is of type %s and cannot be "
+ "changed to %s type. Ignoring the request to change the type.",
deviceName, currentDiskType, requestedDiskType);
this.logWarning(message);
}
}
if (ebs != null && ebs.getSnapshotId() != null) {
ebs.setEncrypted(null);
}
}
private void updateEbsBlockDeviceMapping(EbsBlockDevice ebs, DiskState diskState) {
String requestedVolumeType = diskState.customProperties.get(VOLUME_TYPE);
if (requestedVolumeType == null) {
return;
}
String requestedIops = diskState.customProperties.get(DISK_IOPS);
String currentVolumeType = ebs.getVolumeType();
if (!requestedVolumeType.equals(currentVolumeType)) {
if (currentVolumeType.equals(VOLUME_TYPE_PROVISIONED_SSD)) {
//converting io1 volume to one of {gp2, st1, sc1, magnetic} type.
ebs.setIops(0);
} else if (requestedVolumeType.equals(VOLUME_TYPE_PROVISIONED_SSD)) {
//converting from one of {gp2, st1, sc1, magnetic} type to io1 type.
int iops = Math.min(ebs.getVolumeSize() * MAX_IOPS_PER_GiB,
Integer.parseInt(requestedIops));
ebs.setIops(iops);
} else {
//converting from one of {gp2, st1, sc1, magnetic} type to another one in the same set.
//No need to set iops for this type of volume conversion.
}
} else if (currentVolumeType.equals(VOLUME_TYPE_PROVISIONED_SSD)) {
//Changing the iops value of the the volume.
int iops = Math.min(ebs.getIops(), Integer.parseInt(requestedIops));
ebs.setIops(iops);
}
ebs.setVolumeType(requestedVolumeType);
}
/**
* Fail the request in case of hard constraints and device type mismatch.
*/
private void validateIfDeviceTypesAreMatching(String currentDeviceType,
String requestedDeviceType) {
if (requestedDeviceType != null && !currentDeviceType.equals(requestedDeviceType)) {
String message = String.format("Found hard constraint on existing disk. %s type "
+ "cannot be changed to %s type.", currentDeviceType, requestedDeviceType);
this.logSevere("[AWSInstanceService] " + message);
throw new IllegalArgumentException(message);
}
}
private String getDeviceType(EbsBlockDevice ebs) {
return ebs != null ? AWSStorageType.EBS.getName() : AWSStorageType.INSTANCE_STORE.getName();
}
/**
* copy the custom properties from the boot disk to the existing disk.
*/
private void copyCustomProperties(DiskState diskState, DiskState bootDisk) {
Map bootDiskCustomProperties = bootDisk.customProperties;
Map customProperties = new HashMap<>();
if (bootDiskCustomProperties.containsKey(DEVICE_TYPE)) {
customProperties.put(DEVICE_TYPE, bootDiskCustomProperties.get(DEVICE_TYPE));
}
if (bootDiskCustomProperties.containsKey(VOLUME_TYPE)) {
customProperties.put(VOLUME_TYPE, bootDiskCustomProperties.get(VOLUME_TYPE));
}
if (bootDiskCustomProperties.containsKey(DISK_IOPS)) {
customProperties.put(DISK_IOPS, bootDiskCustomProperties.get(DISK_IOPS));
}
diskState.customProperties = customProperties.size() > 0 ? customProperties : null;
}
/**
* Add the disk information to disk state so that the disk state reflects the volume
* information
*/
private void addMandatoryProperties(DiskState diskState, BlockDeviceMapping deviceMapping,
AWSInstanceContext instanceType) {
if (diskState.customProperties == null) {
diskState.customProperties = new HashMap<>();
}
String deviceName = deviceMapping.getDeviceName();
diskState.customProperties.put(DEVICE_NAME, deviceName);
diskState.persistent = Boolean.FALSE;
EbsBlockDevice ebs = deviceMapping.getEbs();
if (ebs != null) {
diskState.capacityMBytes = ebs.getVolumeSize() * 1024;
diskState.customProperties.put(DEVICE_TYPE, AWSStorageType.EBS.getName());
} else {
diskState.capacityMBytes = instanceType.instanceTypeInfo.dataDiskSizeInMB;
diskState.customProperties.put(DEVICE_TYPE, AWSStorageType.INSTANCE_STORE.getName());
}
}
/**
* Splits the set of existing disks into instance-store disks and ebs disks
*/
private void splitDataDisks(List dataDisks, List instanceStoreDisks,
List ebsDisks) {
dataDisks.forEach(diskState -> {
if (diskState.customProperties != null &&
diskState.customProperties.get(DEVICE_TYPE) != null &&
diskState.customProperties.get(DEVICE_TYPE)
.equals(AWSStorageType.INSTANCE_STORE.getName())) {
instanceStoreDisks.add(diskState);
} else {
ebsDisks.add(diskState);
}
});
}
/**
*
* set default values for device type, volumeType and persistent fields in each disk.
*/
private void setEbsDefaultsIfNotSpecified(List dataDisks, Boolean persist) {
for (DiskState diskState : dataDisks) {
setEbsDefaultsIfNotSet(diskState, persist);
}
}
private class AWSCreationHandler
extends AWSAsyncHandler {
private StatelessService service;
private final OperationContext opContext;
private AWSInstanceContext context;
private AWSCreationHandler(StatelessService service, AWSInstanceContext context) {
this.opContext = OperationContext.getOperationContext();
this.service = service;
this.context = context;
}
@Override
protected void handleError(Exception exception) {
OperationContext.restoreOperationContext(this.opContext);
this.context.taskManager.patchTaskToFailure(exception);
}
@Override
protected void handleSuccess(RunInstancesRequest request, RunInstancesResult result) {
// consumer to be invoked once a VM is in the running state
Consumer