
com.vmware.photon.controller.model.adapters.awsadapter.AWSComputeDiskDay2Service Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2017 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.AWSConstants.AWS_DISK_OPERATION_TIMEOUT_MINUTES;
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_VOLUME_ID_PREFIX;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.DEVICE_NAME;
import static com.vmware.photon.controller.model.adapters.registry.operations.ResourceOperationUtils.handleAdapterResourceOperationRegistration;
import static com.vmware.photon.controller.model.util.PhotonModelUriUtils.createInventoryUri;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import com.amazonaws.services.ec2.AmazonEC2AsyncClient;
import com.amazonaws.services.ec2.model.AttachVolumeRequest;
import com.amazonaws.services.ec2.model.AttachVolumeResult;
import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.ec2.model.DetachVolumeRequest;
import com.amazonaws.services.ec2.model.DetachVolumeResult;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.InstanceBlockDeviceMapping;
import com.amazonaws.services.ec2.model.Reservation;
import com.amazonaws.services.ec2.model.StartInstancesRequest;
import com.amazonaws.services.ec2.model.StartInstancesResult;
import com.amazonaws.services.ec2.model.StopInstancesRequest;
import com.amazonaws.services.ec2.model.StopInstancesResult;
import com.amazonaws.services.ec2.model.Volume;
import com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWSStorageType;
import com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWSSupportedOS;
import com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWSSupportedVirtualizationTypes;
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.registry.operations.ResourceOperation;
import com.vmware.photon.controller.model.adapters.registry.operations.ResourceOperationRequest;
import com.vmware.photon.controller.model.adapters.registry.operations.ResourceOperationSpecService;
import com.vmware.photon.controller.model.adapters.util.BaseAdapterContext.BaseAdapterStage;
import com.vmware.photon.controller.model.adapters.util.BaseAdapterContext.DefaultAdapterContext;
import com.vmware.photon.controller.model.adapters.util.Pair;
import com.vmware.photon.controller.model.constants.PhotonModelConstants;
import com.vmware.photon.controller.model.resources.ComputeService;
import com.vmware.photon.controller.model.resources.ComputeService.ComputeState;
import com.vmware.photon.controller.model.resources.DiskService;
import com.vmware.photon.controller.model.resources.DiskService.DiskState;
import com.vmware.xenon.common.DeferredResult;
import com.vmware.xenon.common.FactoryService;
import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.OperationContext;
import com.vmware.xenon.common.Service;
import com.vmware.xenon.common.ServiceDocument;
import com.vmware.xenon.common.StatelessService;
import com.vmware.xenon.common.Utils;
/**
* This service is responsible for attaching and detaching a disk to a vm provisioned on aws.
*/
public class AWSComputeDiskDay2Service extends StatelessService {
public static final String SELF_LINK = AWSUriPaths.AWS_DISK_DAY2_ADAPTER;
private AWSClientManager clientManager;
private boolean registerResourceOperation;
private static class DiskContext {
protected DefaultAdapterContext baseAdapterContext;
protected ResourceOperationRequest request;
protected ComputeState computeState;
protected DiskState diskState;
protected AmazonEC2AsyncClient amazonEC2Client;
public long taskExpirationMicros;
public Throwable error;
DiskContext() {
this.taskExpirationMicros = Utils.getNowMicrosUtc() + TimeUnit.MINUTES.toMicros(
AWS_DISK_OPERATION_TIMEOUT_MINUTES);
}
}
public static class AWSComputeDiskDay2FactoryService extends FactoryService {
private boolean registerResourceOperation;
public AWSComputeDiskDay2FactoryService(boolean registerResourceOperation) {
super(ServiceDocument.class);
this.registerResourceOperation = registerResourceOperation;
}
@Override
public Service createServiceInstance() {
return new AWSComputeDiskDay2Service(this.registerResourceOperation);
}
}
public AWSComputeDiskDay2Service() {
this(true);
}
public AWSComputeDiskDay2Service(boolean registerResourceOperation) {
this.registerResourceOperation = registerResourceOperation;
}
@Override
public void handleStart(Operation startPost) {
this.clientManager = AWSClientManagerFactory
.getClientManager(AWSConstants.AwsClientType.EC2);
handleAdapterResourceOperationRegistration(this, startPost,
this.registerResourceOperation,
getResourceOperationSpecs());
}
@Override
public void handleStop(Operation op) {
// Temporary log for VCOM-3649
logInfo("Service is stopped: %s", op.toLogString());
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;
}
ResourceOperationRequest request = op.getBody(ResourceOperationRequest.class);
try {
validateRequest(request);
op.complete();
} catch (Exception e) {
op.fail(e);
return;
}
//initialize context with baseAdapterContext and request information
DeferredResult drDiskContext = new DefaultAdapterContext(this, request)
.populateBaseContext(BaseAdapterStage.VMDESC)
.thenApply(c -> {
DiskContext context = new DiskContext();
context.baseAdapterContext = c;
context.request = request;
return context;
});
Function> fn;
// handle resource operation
if (request.operation.equals(ResourceOperation.ATTACH_DISK.operation)) {
fn = this::performAttachOperation;
} else if (request.operation.equals(ResourceOperation.DETACH_DISK.operation)) {
fn = this::performDetachOperation;
} else {
drDiskContext.thenApply(c -> {
Throwable err = new IllegalArgumentException(
String.format("Unknown Operation %s for a disk", request.operation));
c.baseAdapterContext.taskManager.patchTaskToFailure(err);
return c;
});
return;
}
drDiskContext.thenCompose(this::setComputeState)
.thenCompose(this::setDiskState)
.thenCompose(this::setClient)
.thenCompose(fn)
.whenComplete((ctx, e) -> {
if (ctx.error == null) {
ctx.baseAdapterContext.taskManager.finishTask();
} else {
ctx.baseAdapterContext.taskManager.patchTaskToFailure(ctx.error);
}
});
}
private void validateRequest(ResourceOperationRequest request) {
if (request.resourceReference == null) {
throw new IllegalArgumentException("Compute description cannot be empty");
}
if (request.operation == null || request.operation.isEmpty()) {
throw new IllegalArgumentException("Operation cannot be empty");
}
if (request.payload == null
|| request.payload.get(PhotonModelConstants.DISK_LINK) == null) {
throw new IllegalArgumentException("disk reference cannot be empty");
}
}
/**
* create and add the amazon client to the context
*/
private DeferredResult setClient(DiskContext context) {
DeferredResult dr = new DeferredResult<>();
this.clientManager.getOrCreateEC2ClientAsync(context.baseAdapterContext.endpointAuth,
context.baseAdapterContext.child.description.regionId, this)
.whenComplete((ec2Client, t) -> {
if (t != null) {
context.error = t;
dr.complete(context);
return;
}
context.amazonEC2Client = ec2Client;
dr.complete(context);
});
return dr;
}
/**
* get the compute state and set it in context
*/
private DeferredResult setComputeState(DiskContext context) {
return this.sendWithDeferredResult(
Operation.createGet(createInventoryUri(this.getHost(),
context.baseAdapterContext.resourceReference)),
ComputeState.class)
.thenApply(computeState -> {
context.computeState = computeState;
return context;
});
}
/**
* get the disk state and set it in context
*/
private DeferredResult setDiskState(DiskContext context) {
return this.sendWithDeferredResult(Operation.createGet(createInventoryUri(this.getHost(),
context.request.payload.get(PhotonModelConstants.DISK_LINK))), DiskState.class)
.thenApply(diskState -> {
context.diskState = diskState;
return context;
});
}
private DeferredResult performAttachOperation(DiskContext context) {
DeferredResult dr = new DeferredResult<>();
try {
if (context.request.isMockRequest) {
updateComputeAndDiskState(dr, context, null);
return dr;
}
String instanceId = context.computeState.id;
if (instanceId == null || !instanceId.startsWith(AWS_INSTANCE_ID_PREFIX)) {
return logAndGetFailedDr(context, "compute id cannot be empty");
}
String diskId = context.diskState.id;
if (diskId == null || !diskId.startsWith(AWS_VOLUME_ID_PREFIX)) {
return logAndGetFailedDr(context, "disk id cannot be empty");
}
String deviceName = getAvailableDeviceName(context, instanceId);
if (deviceName == null) {
return logAndGetFailedDr(context, "No device name is available for attaching new disk");
}
context.diskState.customProperties.put(DEVICE_NAME, deviceName);
AttachVolumeRequest attachVolumeRequest = new AttachVolumeRequest()
.withInstanceId(instanceId)
.withVolumeId(diskId)
.withDevice(deviceName);
AWSAsyncHandler attachDiskHandler =
new AWSAttachDiskHandler(dr, context);
context.amazonEC2Client.attachVolumeAsync(attachVolumeRequest, attachDiskHandler);
} catch (Exception e) {
context.error = e;
return DeferredResult.completed(context);
}
return dr;
}
private DeferredResult logAndGetFailedDr(DiskContext context, String message) {
this.logSevere("[AWSComputeDiskDay2Service] " + message);
Throwable e = new IllegalArgumentException(message);
context.error = e;
return DeferredResult.completed(context);
}
private DeferredResult performDetachOperation(DiskContext context) {
DeferredResult dr = new DeferredResult<>();
try {
validateDetachInfo(context.diskState);
if (context.request.isMockRequest) {
updateComputeAndDiskState(dr, context, null);
return dr;
}
String instanceId = context.computeState.id;
if (instanceId == null || !instanceId.startsWith(AWS_INSTANCE_ID_PREFIX)) {
return logAndGetFailedDr(context, "compute id cannot be empty");
}
String diskId = context.diskState.id;
if (diskId == null || !diskId.startsWith(AWS_VOLUME_ID_PREFIX)) {
return logAndGetFailedDr(context, "disk id cannot be empty");
}
//TODO: Ideally the volume must be unmounted before detaching the disk. Currently
// we don't have a way to unmount the disk. The solution is to stop the instance,
// detach the disk and then start the instance
//stop the instance, detach the disk and then start the instance.
if (context.baseAdapterContext.child.powerState.equals(ComputeService.PowerState.ON)) {
StopInstancesRequest stopRequest = new StopInstancesRequest();
stopRequest.withInstanceIds(context.baseAdapterContext.child.id);
context.amazonEC2Client.stopInstancesAsync(stopRequest,
new AWSAsyncHandler() {
@Override
protected void handleError(Exception e) {
service.logSevere(() -> String.format(
"[AWSComputeDiskDay2Service] Failed to start compute. %s",
Utils.toString(e)));
OperationContext.restoreOperationContext(this.opContext);
context.error = e;
dr.complete(context);
}
@Override
protected void handleSuccess(StopInstancesRequest request,
StopInstancesResult result) {
OperationContext.restoreOperationContext(this.opContext);
AWSUtils.waitForTransitionCompletion(getHost(),
result.getStoppingInstances(), "stopped",
context.amazonEC2Client, (is, e) -> {
if (e != null) {
service.logSevere(() -> String.format(
"[AWSComputeDiskDay2Service] Failed to stop "
+ "the compute. %s", Utils.toString(e)));
context.error = e;
dr.complete(context);
return;
}
logInfo(() -> String.format(
"[AWSComputeDiskDay2Service] Successfully stopped "
+ "the instance %s", instanceId));
//detach disk from the instance.
detachVolume(context, dr, instanceId, diskId, true);
});
}
});
} else {
detachVolume(context, dr, instanceId, diskId, false);
}
} catch (Exception e) {
context.error = e;
return DeferredResult.completed(context);
}
return dr;
}
/**
* Verifies if the disk is in attached to a vm and is not boot disk.
*/
private void validateDetachInfo(DiskState diskState) {
if (diskState.bootOrder != null) {
throw new IllegalArgumentException("Boot disk cannot be detached from the vm");
}
//Detached or available disk cannot be detached.
if (diskState.status != null && diskState.status != DiskService.DiskStatus.ATTACHED) {
throw new IllegalArgumentException(String.format("Cannot perform detach operation on "
+ "disk with id %s. The disk has to be in attached state.", diskState.id));
}
}
/**
* Send detach request to aws using amazon ec2 client.
*/
private void detachVolume(DiskContext context, DeferredResult dr,
String instanceId, String diskId, boolean startInstance) {
DetachVolumeRequest detachVolumeRequest = new DetachVolumeRequest()
.withInstanceId(instanceId)
.withVolumeId(diskId);
AWSAsyncHandler detachDiskHandler =
new AWSDetachDiskHandler(this, dr, context, startInstance);
context.amazonEC2Client.detachVolumeAsync(detachVolumeRequest, detachDiskHandler);
}
/**
* start the instance and on success updates the disk and compute state to reflect the detach information.
*/
private void startInstance(AmazonEC2AsyncClient client, DiskContext c,
DeferredResult dr, OperationContext opCtx) {
StartInstancesRequest startRequest = new StartInstancesRequest();
startRequest.withInstanceIds(c.baseAdapterContext.child.id);
client.startInstancesAsync(startRequest,
new AWSAsyncHandler() {
@Override
protected void handleError(Exception e) {
service.logSevere(() -> String.format(
"[AWSComputeDiskDay2Service] Failed to start the instance %s. %s",
c.baseAdapterContext.child.id, Utils.toString(e)));
OperationContext.restoreOperationContext(opCtx);
c.error = e;
dr.complete(c);
}
@Override
protected void handleSuccess(StartInstancesRequest request, StartInstancesResult result) {
AWSUtils.waitForTransitionCompletion(getHost(),
result.getStartingInstances(), "running",
client, (is, e) -> {
if (e != null) {
service.logSevere(() -> String.format(
"[AWSComputeDiskDay2Service] Instance %s failed to reach "
+ "running state. %s",c.baseAdapterContext.child.id,
Utils.toString(e)));
OperationContext.restoreOperationContext(opCtx);
c.error = e;
dr.complete(c);
return;
}
logInfo(() -> String.format(
"[AWSComputeDiskDay2Service] Successfully started the "
+ "instance %s",
result.getStartingInstances().get(0).getInstanceId()));
updateComputeAndDiskState(dr, c, opCtx);
});
}
});
}
/**
* Async handler for updating the disk and vm state after attaching the disk to a vm.
*/
public class AWSDetachDiskHandler
extends AWSAsyncHandler {
private StatelessService service;
private DeferredResult dr;
private DiskContext context;
Boolean performNextInstanceOp;
private AWSDetachDiskHandler(StatelessService service, DeferredResult dr,
DiskContext context, Boolean performNextInstanceOp) {
this.opContext = OperationContext.getOperationContext();
this.service = service;
this.dr = dr;
this.context = context;
this.performNextInstanceOp = performNextInstanceOp;
}
@Override
protected void handleError(Exception exception) {
this.service.logWarning(
() -> String.format("[AWSComputeDiskDay2Service] Detaching "
+ "the volume %s from instance %s for task reference :%s. FAILED",
this.context.diskState.id,
this.context.computeState.id,
this.context.request.taskLink()));
OperationContext.restoreOperationContext(this.opContext);
this.context.error = exception;
this.dr.complete(this.context);
}
@Override
protected void handleSuccess(DetachVolumeRequest request, DetachVolumeResult result) {
//consumer to be invoked once a volume is detached
Consumer
© 2015 - 2025 Weber Informatics LLC | Privacy Policy