
com.vmware.photon.controller.model.adapters.gcp.enumeration.GCPEnumerationAdapterService Maven / Gradle / Ivy
The newest version!
/*
* 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.gcp.enumeration;
import static com.vmware.photon.controller.model.ComputeProperties.CUSTOM_OS_TYPE;
import static com.vmware.photon.controller.model.adapters.gcp.constants.GCPConstants.AUTH_HEADER_BEARER_PREFIX;
import static com.vmware.photon.controller.model.adapters.gcp.constants.GCPConstants.DEFAULT_DISK_CAPACITY;
import static com.vmware.photon.controller.model.adapters.gcp.constants.GCPConstants.DEFAULT_DISK_SERVICE_REFERENCE;
import static com.vmware.photon.controller.model.adapters.gcp.constants.GCPConstants.DEFAULT_DISK_SOURCE_IMAGE;
import static com.vmware.photon.controller.model.adapters.gcp.constants.GCPConstants.DISK_AUTO_DELETE;
import static com.vmware.photon.controller.model.adapters.gcp.constants.GCPConstants.LIST_VM_TEMPLATE_URI;
import static com.vmware.photon.controller.model.adapters.gcp.constants.GCPConstants.MAX_RESULTS;
import static com.vmware.photon.controller.model.adapters.gcp.constants.GCPConstants.PAGE_TOKEN;
import static com.vmware.photon.controller.model.adapters.gcp.utils.GCPUtils.assignIPAddress;
import static com.vmware.photon.controller.model.adapters.gcp.utils.GCPUtils.assignPowerState;
import static com.vmware.photon.controller.model.adapters.gcp.utils.GCPUtils.extractActualInstanceType;
import static com.vmware.photon.controller.model.adapters.gcp.utils.GCPUtils.extractRegionFromZone;
import static com.vmware.photon.controller.model.adapters.gcp.utils.GCPUtils.privateKeyFromPkcs8;
import static com.vmware.photon.controller.model.resources.ComputeDescriptionService.ComputeDescription.ENVIRONMENT_NAME_GCP;
import static com.vmware.photon.controller.model.util.PhotonModelUriUtils.createInventoryUri;
import java.io.IOException;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import com.google.api.services.compute.ComputeScopes;
import com.vmware.photon.controller.model.ComputeProperties.OSType;
import com.vmware.photon.controller.model.UriPaths;
import com.vmware.photon.controller.model.adapterapi.ComputeEnumerateResourceRequest;
import com.vmware.photon.controller.model.adapterapi.EnumerationAction;
import com.vmware.photon.controller.model.adapters.gcp.GCPUriPaths;
import com.vmware.photon.controller.model.adapters.gcp.podo.authorization.GCPAccessTokenResponse;
import com.vmware.photon.controller.model.adapters.gcp.podo.vm.GCPDisk;
import com.vmware.photon.controller.model.adapters.gcp.podo.vm.GCPInstance;
import com.vmware.photon.controller.model.adapters.gcp.podo.vm.GCPInstancesList;
import com.vmware.photon.controller.model.adapters.gcp.utils.GCPUtils;
import com.vmware.photon.controller.model.adapters.gcp.utils.JSONWebToken;
import com.vmware.photon.controller.model.adapters.util.AdapterUtils;
import com.vmware.photon.controller.model.adapters.util.TaskManager;
import com.vmware.photon.controller.model.adapters.util.enums.EnumerationStages;
import com.vmware.photon.controller.model.query.QueryUtils;
import com.vmware.photon.controller.model.resources.ComputeDescriptionService;
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.ComputeStateWithDescription;
import com.vmware.photon.controller.model.resources.DiskService;
import com.vmware.photon.controller.model.resources.DiskService.DiskState;
import com.vmware.photon.controller.model.resources.DiskService.DiskType;
import com.vmware.photon.controller.model.resources.ResourceGroupService.ResourceGroupState;
import com.vmware.photon.controller.model.security.util.EncryptionUtils;
import com.vmware.photon.controller.model.util.PhotonModelUriUtils;
import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.OperationJoin;
import com.vmware.xenon.common.ServiceDocumentQueryResult;
import com.vmware.xenon.common.ServiceHost;
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;
import com.vmware.xenon.services.common.QueryTask;
import com.vmware.xenon.services.common.QueryTask.NumericRange;
import com.vmware.xenon.services.common.QueryTask.QuerySpecification.QueryOption;
/**
* Enumeration Adapter for the Google Cloud Platform. Performs a list call to the GCP API and
* reconciles the local state with the state on the remote system. It lists the instances on the
* remote system. Compares those with the local system, creates the instances that are missing
* in the local system and deletes the instances that are redundant.
*/
public class GCPEnumerationAdapterService extends StatelessService {
public static final String SELF_LINK = GCPUriPaths.GCP_ENUMERATION_ADAPTER;
private static final String PROPERTY_NAME_ENUM_QUERY_RESULT_LIMIT = UriPaths.PROPERTY_PREFIX
+ "GCPEnumerationAdapterService.QUERY_RESULT_LIMIT";
private static final int QUERY_RESULT_LIMIT =
Integer.getInteger(PROPERTY_NAME_ENUM_QUERY_RESULT_LIMIT, 100);
private static final String PROPERTY_NAME_ENUM_VM_PAGE_SIZE = UriPaths.PROPERTY_PREFIX
+ "GCPEnumerationAdapterService.VM_PAGE_SIZE";
// The vm page size cannot be larger than 500. And it must be a string.
private static final String VM_PAGE_SIZE = String.valueOf(Math.min(
Integer.getInteger(PROPERTY_NAME_ENUM_VM_PAGE_SIZE, 50), 500));
/**
* SubStages to handle GCP VMs data collection.
*/
private enum EnumerationSubStages {
LIST_REMOTE_VMS,
QUERY_LOCAL_VMS,
UPDATE_COMPUTESTATE_COMPUTEDESCRIPTION_DISK,
CREATE_LOCAL_VMS,
DELETE_LOCAL_VMS,
FINISHED
}
private static class EnumerationContext {
// Basic fields
ComputeEnumerateResourceRequest enumRequest;
ComputeDescription computeHostDesc;
EnumerationStages stage;
Throwable error;
AuthCredentialsServiceState parentAuth;
ResourceGroupState resourceGroup;
Long enumerationStartTimeInMicros;
String enumNextPageLink;
// Substage specific fields
EnumerationSubStages subStage;
Map virtualMachines = new ConcurrentHashMap<>();
List computeStates = new LinkedList<>();
Set vmIds = new HashSet<>();
// GCP specific fields
String accessToken;
String userEmail;
String privateKey;
String projectId;
String zoneId;
TaskManager taskManager;
EnumerationContext(StatelessService service, Operation op) {
this.enumRequest = op.getBody(ComputeEnumerateResourceRequest.class);
this.stage = EnumerationStages.HOSTDESC;
this.taskManager = new TaskManager(service, this.enumRequest.taskReference,
this.enumRequest.resourceLink());
}
}
public GCPEnumerationAdapterService() {
super.toggleOption(ServiceOption.INSTRUMENTATION, true);
}
/**
* The REST PATCH request handler. This is the entry of starting an enumeration.
* @param op Operation which should contain request body.
*/
@Override
public void handlePatch(Operation op) {
setOperationHandlerInvokeTimeStat(op);
if (!op.hasBody()) {
op.fail(new IllegalArgumentException("body is required"));
return;
}
EnumerationContext ctx = new EnumerationContext(this, op);
AdapterUtils.validateEnumRequest(ctx.enumRequest);
op.complete();
if (ctx.enumRequest.isMockRequest) {
ctx.taskManager.finishTask();
;
return;
}
handleEnumerationRequest(ctx);
}
/**
* The basic flow of dealing with an enumeration request.
* @param ctx Enumeration Context which should decide the current stage of enumeration.
*/
private void handleEnumerationRequest(EnumerationContext ctx) {
switch (ctx.stage) {
case HOSTDESC:
getHostComputeDescription(ctx, EnumerationStages.PARENTAUTH);
break;
case PARENTAUTH:
getParentAuth(ctx, EnumerationStages.RESOURCEGROUP);
break;
case RESOURCEGROUP:
getResourceGroup(ctx, EnumerationStages.CLIENT);
break;
case CLIENT:
try {
// The access token will expire in one hour.
// And this access token can be only used for readonly operations.
getAccessToken(ctx, ctx.userEmail,
Collections.singleton(ComputeScopes.COMPUTE_READONLY),
privateKeyFromPkcs8(ctx.privateKey), EnumerationStages.ENUMERATE);
} catch (Throwable e) {
logSevere(e);
ctx.error = e;
ctx.stage = EnumerationStages.ERROR;
handleEnumerationRequest(ctx);
return;
}
break;
case ENUMERATE:
switch (ctx.enumRequest.enumerationAction) {
case START:
ctx.enumerationStartTimeInMicros = Utils.getNowMicrosUtc();
ctx.enumRequest.enumerationAction = EnumerationAction.REFRESH;
handleEnumerationRequest(ctx);
break;
case REFRESH:
ctx.subStage = EnumerationSubStages.LIST_REMOTE_VMS;
handleSubStage(ctx);
break;
case STOP:
ctx.stage = EnumerationStages.FINISHED;
handleEnumerationRequest(ctx);
break;
default:
logSevere(() -> String.format("Unknown enumeration action %s",
ctx.enumRequest.enumerationAction));
ctx.stage = EnumerationStages.ERROR;
handleEnumerationRequest(ctx);
}
break;
case FINISHED:
ctx.taskManager.finishTask();
break;
case ERROR:
ctx.taskManager.patchTaskToFailure(ctx.error);
break;
default:
String msg = String.format("Unknown GCP enumeration stage %s ", ctx.stage.toString());
logSevere(() -> msg);
ctx.error = new IllegalStateException(msg);
ctx.taskManager.patchTaskToFailure(ctx.error);
}
}
/**
* The basic flow of dealing with each sub stage in an enumeration.
* @param ctx Enumeration Context which should decide the current sub stage of enumeration.
*/
private void handleSubStage(EnumerationContext ctx) {
switch (ctx.subStage) {
case LIST_REMOTE_VMS:
enumerate(ctx);
break;
case QUERY_LOCAL_VMS:
queryForComputeStates(ctx, ctx.virtualMachines);
break;
case UPDATE_COMPUTESTATE_COMPUTEDESCRIPTION_DISK:
update(ctx);
break;
case CREATE_LOCAL_VMS:
create(ctx);
break;
case DELETE_LOCAL_VMS:
delete(ctx);
break;
case FINISHED:
ctx.stage = EnumerationStages.FINISHED;
handleEnumerationRequest(ctx);
break;
default:
String msg = String
.format("Unknown GCP enumeration sub-stage %s ", ctx.subStage.toString());
ctx.error = new IllegalStateException(msg);
ctx.stage = EnumerationStages.ERROR;
handleEnumerationRequest(ctx);
}
}
/**
* Deletes undiscovered resources.
*
* The logic works by recording a timestamp when enumeration starts. This timestamp is used to
* lookup resources which haven't been touched as part of current enumeration cycle. The other
* data point this method uses is the virtual machines discovered as part of list vm call.
*
* Finally, deletion on a resource is invoked only if it meets two criteria:
* - Timestamp older than current enumeration cycle.
* - VM not present on GCP.
*
* The method paginates through list of resources for deletion.
* @param ctx The Enumeration Context.
*/
private void delete(EnumerationContext ctx) {
QueryTask.Query query = QueryTask.Query.Builder.create()
.addKindFieldClause(ComputeState.class)
.addFieldClause(ComputeState.FIELD_NAME_RESOURCE_POOL_LINK,
ctx.enumRequest.resourcePoolLink)
.addFieldClause(ComputeState.FIELD_NAME_PARENT_LINK,
ctx.enumRequest.resourceLink())
.addRangeClause(ComputeState.FIELD_NAME_UPDATE_TIME_MICROS,
NumericRange.createLessThanRange(ctx.enumerationStartTimeInMicros))
.build();
QueryTask q = QueryTask.Builder.createDirectTask()
.addOption(QueryOption.EXPAND_CONTENT)
.addOption(QueryOption.INDEXED_METADATA)
.setResultLimit(QUERY_RESULT_LIMIT)
.setQuery(query)
.build();
q.tenantLinks = ctx.computeHostDesc.tenantLinks;
logFine(() -> "Querying compute resources for deletion");
QueryUtils.startInventoryQueryTask(this, q)
.whenComplete((o, e) -> deleteQueryCompletionHandler(ctx, o, e));
}
/**
* This helper function deletes all compute states in one deletion page
* every iteration. For each compute state, its associated disks will
* also be deleted. After deletion, it will check if there is a next
* deletion page. If there is, it will delete that page recursively.
* If there are nothing to delete, it will jump to the finished stage
* of the whole enumeration.
* @param ctx The enumeration context.
* @param results The results of deletion query.
*/
private void deleteHelper(EnumerationContext ctx, ServiceDocumentQueryResult results) {
if (results.documentCount == 0) {
checkLinkAndFinishDeleting(ctx, results.nextPageLink);
return;
}
List operations = new ArrayList<>();
results.documents.values().forEach(json -> {
ComputeState computeState = Utils.fromJson(json, ComputeState.class);
Long vmId = Long.parseLong(computeState.id);
if (!ctx.vmIds.contains(vmId)) {
operations.add(Operation.createDelete(this, computeState.documentSelfLink));
logFine(() -> String.format("Deleting compute state %s",
computeState.documentSelfLink));
if (computeState.diskLinks != null && !computeState.diskLinks.isEmpty()) {
computeState.diskLinks.forEach(diskLink -> {
operations.add(Operation.createDelete(this, diskLink));
logFine(() -> String.format("Deleting disk state %s", diskLink));
});
}
}
});
if (operations.isEmpty()) {
checkLinkAndFinishDeleting(ctx, results.nextPageLink);
return;
}
OperationJoin.create(operations)
.setCompletion((ops, exs) -> {
if (exs != null) {
// We don't want to fail the whole data collection if some of the
// operation fails.
exs.values().forEach(
ex -> logWarning(() -> String.format("Error: %s", ex.getMessage())));
}
checkLinkAndFinishDeleting(ctx, results.nextPageLink);
}).sendWith(this);
}
/**
* The completion handler of the operation of deletion query.
* @param ctx The Enumeration Context.
* @param queryTask The query task.
* @param e The Error of completion handler.
*/
private void deleteQueryCompletionHandler(EnumerationContext ctx, QueryTask queryTask,
Throwable e) {
if (e != null) {
handleError(ctx, e);
return;
}
deleteHelper(ctx, queryTask.results);
}
/**
* The helper function which checks if the deletion is finished.
* If finished, go to Finished sub stage.
* @param ctx The Enumeration Context.
* @param deletionNextPageLink The next deletion page link.
*/
private void checkLinkAndFinishDeleting(EnumerationContext ctx, String deletionNextPageLink) {
if (deletionNextPageLink != null) {
URI nextPageInventoryLinkUri = PhotonModelUriUtils.createInventoryUri(this.getHost(),
deletionNextPageLink);
logFine(() -> String.format("Querying page [%s] for resources to be deleted",
nextPageInventoryLinkUri));
Operation.createGet(nextPageInventoryLinkUri)
.setCompletion((o, e) -> deleteQueryCompletionHandler(ctx, o.getBody
(QueryTask.class), e))
.sendWith(this);
return;
}
logFine(() -> "No compute states match for deletion");
ctx.subStage = EnumerationSubStages.FINISHED;
handleSubStage(ctx);
}
/**
* Creates relevant resources for given VMs.
* @param ctx The Enumeration Context.
*/
private void create(EnumerationContext ctx) {
logFine(() -> "Creating Local Compute States");
AtomicInteger size = new AtomicInteger(ctx.virtualMachines.size());
logFine(() -> String.format("%s compute description with states to be created",
size.toString()));
ctx.virtualMachines.values().forEach(virtualMachine ->
createHelper(ctx, virtualMachine, size));
}
/**
* This helper function creates a compute state according to the corresponding
* instance on the cloud. It will also creates an auth credential and a compute
* description with it. Moreover, if there is a root disk of the instance on
* cloud, it will creates a root disk locally. Otherwise, it will create a
* default root disk with the compute state. All the creation operations will
* run in parallel. After all remote instances get created, it will jump to
* list vm stage if there is a next page of list vms. Otherwise it will jump
* to the delete stage.
* @param ctx The Enumeration Context.
* @param virtualMachine The virtual machine to be created.
* @param size The number of remaining virtual machines to be created.
*/
private void createHelper(EnumerationContext ctx, GCPInstance virtualMachine, AtomicInteger size) {
List operations = new ArrayList<>();
// TODO VSYM-1106: refactor the creation logic here.
// Create compute description.
// Map GCP instance data to compute description.
ComputeDescription computeDescription = new ComputeDescription();
computeDescription.id = UUID.randomUUID().toString();
computeDescription.name = virtualMachine.name;
computeDescription.zoneId = virtualMachine.zone;
// TODO VSYM-1139: dynamically acquire all gcp zones, regions and mappings.
computeDescription.regionId = extractRegionFromZone(virtualMachine.zone);
computeDescription.instanceType = extractActualInstanceType(virtualMachine.machineType);
computeDescription.authCredentialsLink = ctx.parentAuth.documentSelfLink;
computeDescription.documentSelfLink = computeDescription.id;
computeDescription.environmentName = ENVIRONMENT_NAME_GCP;
computeDescription.instanceAdapterReference = UriUtils.buildUri(
ServiceHost.LOCAL_HOST,
this.getHost().getPort(),
GCPUriPaths.GCP_INSTANCE_ADAPTER, null);
computeDescription.statsAdapterReference = UriUtils.buildUri(
ServiceHost.LOCAL_HOST,
this.getHost().getPort(),
GCPUriPaths.GCP_STATS_ADAPTER, null);
computeDescription.tenantLinks = ctx.computeHostDesc.tenantLinks;
Operation compDescOp = Operation
.createPost(getHost(), ComputeDescriptionService.FACTORY_LINK)
.setBody(computeDescription);
operations.add(compDescOp);
// Create root disk.
DiskService.DiskState rootDisk = new DiskService.DiskState();
rootDisk.id = UUID.randomUUID().toString();
rootDisk.documentSelfLink = rootDisk.id;
rootDisk.customProperties = new HashMap<>();
boolean foundRoot = false;
if (virtualMachine.disks != null && !virtualMachine.disks.isEmpty()) {
for (GCPDisk gcpDisk : virtualMachine.disks) {
if (gcpDisk.boot) {
foundRoot = true;
rootDisk.name = gcpDisk.deviceName;
rootDisk.customProperties.put(DISK_AUTO_DELETE, gcpDisk.autoDelete.toString());
break;
}
}
}
if (!foundRoot) {
rootDisk.name = rootDisk.id;
}
// These are required fields in disk service.
// They cannot be accessed during vm enumeration.
rootDisk.type = DiskType.HDD;
rootDisk.capacityMBytes = DEFAULT_DISK_CAPACITY;
rootDisk.sourceImageReference = URI.create(DEFAULT_DISK_SOURCE_IMAGE);
rootDisk.customizationServiceReference = URI.create(DEFAULT_DISK_SERVICE_REFERENCE);
// No matter we find root disk or not, the root disk should be booted first.
rootDisk.bootOrder = 1;
rootDisk.tenantLinks = ctx.computeHostDesc.tenantLinks;
Operation diskOp = Operation.createPost(getHost(), DiskService.FACTORY_LINK)
.setBody(rootDisk);
operations.add(diskOp);
List vmDisks = new ArrayList<>();
vmDisks.add(UriUtils.buildUriPath(DiskService.FACTORY_LINK, rootDisk.documentSelfLink));
// Create compute state
ComputeState resource = new ComputeState();
resource.id = virtualMachine.id.toString();
resource.type = ComputeType.VM_GUEST;
resource.environmentName = ComputeDescription.ENVIRONMENT_NAME_GCP;
resource.name = virtualMachine.name;
resource.parentLink = ctx.enumRequest.resourceLink();
resource.descriptionLink = UriUtils.buildUriPath(
ComputeDescriptionService.FACTORY_LINK, computeDescription.documentSelfLink);
resource.resourcePoolLink = ctx.enumRequest.resourcePoolLink;
resource.diskLinks = vmDisks;
resource.customProperties = new HashMap<>();
String osType = getNormalizedOSType(virtualMachine);
if (osType != null) {
resource.customProperties.put(CUSTOM_OS_TYPE, osType);
}
resource.tenantLinks = ctx.computeHostDesc.tenantLinks;
assignIPAddress(resource, virtualMachine);
assignPowerState(resource, virtualMachine.status);
Operation resourceOp = Operation
.createPost(getHost(), ComputeService.FACTORY_LINK)
.setBody(resource);
operations.add(resourceOp);
OperationJoin.create(operations)
.setCompletion((ops, exs) -> {
if (exs != null) {
exs.values().forEach(ex -> logWarning(() -> String.format("Error: %s",
ex.getMessage())));
}
if (size.decrementAndGet() == 0) {
ctx.virtualMachines.clear();
if (ctx.enumNextPageLink != null) {
ctx.subStage = EnumerationSubStages.LIST_REMOTE_VMS;
} else {
logFine(() -> "Finished creating compute states");
ctx.subStage = EnumerationSubStages.DELETE_LOCAL_VMS;
}
handleSubStage(ctx);
}
}).sendWith(this);
}
/**
* Updates matching compute states for given VMs.
* @param ctx The Enumeration Context.
*/
private void update(EnumerationContext ctx) {
logFine(() -> "Updating Local VMs");
AtomicInteger numOfUpdates = new AtomicInteger(ctx.computeStates.size());
ctx.computeStates.forEach(computeState -> {
Long instanceId = Long.parseLong(computeState.id);
GCPInstance GCPInstance = ctx.virtualMachines.remove(instanceId);
updateHelper(ctx, computeState, GCPInstance, numOfUpdates);
});
}
/**
* This function will update the root disk data of a compute state. If the
* instance on the cloud does not have any disks or does not have a boot
* disk, the update will skip this instance. After all local vms are updated
* it will jump to create stage.
*
* So far we update ip address, power state in compute state and instance type
* in compute description and disk custom properties
* @param ctx The Enumeration Context.
* @param computeState The compute state to be updated.
* @param vm The virtual machine used to update compute state.
* @param numOfUpdates The number of remaining compute states to be updated.
*/
private void updateHelper(EnumerationContext ctx, ComputeState computeState, GCPInstance vm,
AtomicInteger numOfUpdates) {
List operations = new ArrayList<>();
ComputeState computeStatePatch = new ComputeState();
assignIPAddress(computeStatePatch, vm);
assignPowerState(computeStatePatch, vm.status);
operations.add(Operation.createPatch(getHost(),
computeState.documentSelfLink).setBody(computeStatePatch));
ComputeDescription computeDescription = new ComputeDescription();
computeDescription.instanceType = extractActualInstanceType(vm.machineType);
operations.add(Operation.createPatch(getHost(),
computeState.descriptionLink).setBody(computeDescription));
if (vm.disks != null && !vm.disks.isEmpty()) {
for (GCPDisk gcpDisk : vm.disks) {
if (gcpDisk.boot) {
DiskState diskState = new DiskState();
diskState.customProperties = new HashMap<>();
diskState.customProperties.put(DISK_AUTO_DELETE, gcpDisk.autoDelete.toString());
diskState.documentSelfLink = computeState.diskLinks.get(0);
operations.add(Operation.createPatch(getHost(),
diskState.documentSelfLink).setBody(diskState));
break;
}
}
}
OperationJoin.create(operations)
.setCompletion((ops, exs) -> {
if (exs != null) {
exs.values().forEach(ex -> logWarning(() -> String.format("Error: %s",
ex.getMessage())));
}
countAndFinishUpdating(ctx, numOfUpdates);
}).sendWith(this);
}
/**
* The helper function which checks if the update is finished.
* If finished, go to Delete or List VMs sub stage depending on
* if there are still remaining virtual machines.
* @param ctx The enumeration context.
* @param numOfUpdates The
*/
private void countAndFinishUpdating(EnumerationContext ctx, AtomicInteger numOfUpdates) {
if (numOfUpdates.decrementAndGet() == 0) {
ctx.computeStates.clear();
logFine(() -> "Finished updating compute states");
// If there are still some cloud instances left, these instances
// should be mapped to local compute states. So we jump to create stage.
// Otherwise, we go to delete stage or list vms stage depending on
// whether the next enumeration page link is valid or not.
if (ctx.virtualMachines.isEmpty()) {
// If the next enumeration page link is not valid, we can
// jump directly to delete stage since there are no more
// instances on cloud.
// Otherwise, we need to go to list vms page and fetch instances
// on next page.
if (ctx.enumNextPageLink == null) {
ctx.subStage = EnumerationSubStages.DELETE_LOCAL_VMS;
} else {
ctx.subStage = EnumerationSubStages.LIST_REMOTE_VMS;
}
} else {
ctx.subStage = EnumerationSubStages.CREATE_LOCAL_VMS;
}
handleSubStage(ctx);
}
}
/**
* Query all compute states for the cluster filtered by the received set of instance Ids.
* @param ctx The Enumeration Context.
* @param vms The Map of VM IDs and VMs.
*/
private void queryForComputeStates(EnumerationContext ctx, Map vms) {
logFine(() -> "Enumerating Local Compute States");
QueryTask.Query.Builder instanceIdFilterParentQuery =
QueryTask.Query.Builder.create(QueryTask.Query.Occurance.MUST_OCCUR);
for (Long instanceId : vms.keySet()) {
QueryTask.Query instanceIdFilter = QueryTask.Query.Builder
.create(QueryTask.Query.Occurance.SHOULD_OCCUR)
.addFieldClause(ComputeState.FIELD_NAME_ID, instanceId.toString())
.build();
instanceIdFilterParentQuery.addClause(instanceIdFilter);
}
QueryTask.Query query = QueryTask.Query.Builder.create()
.addKindFieldClause(ComputeState.class)
.addFieldClause(ComputeState.FIELD_NAME_RESOURCE_POOL_LINK,
ctx.enumRequest.resourcePoolLink)
.addFieldClause(ComputeState.FIELD_NAME_PARENT_LINK,
ctx.enumRequest.resourceLink())
.build()
.addBooleanClause(instanceIdFilterParentQuery.build());
QueryTask q = QueryTask.Builder.createDirectTask()
.addOption(QueryTask.QuerySpecification.QueryOption.EXPAND_CONTENT)
.addOption(QueryTask.QuerySpecification.QueryOption.INDEXED_METADATA)
.setQuery(query)
.build();
q.tenantLinks = ctx.computeHostDesc.tenantLinks;
QueryUtils.startInventoryQueryTask(this, q)
.whenComplete((queryTask, e) -> {
if (e != null) {
handleError(ctx, e);
return;
}
logFine(() -> String.format("Found %d matching compute states for GCP Instances",
queryTask.results.documentCount));
// If there are local compute states with same id as cloud instances,
// these compute states need to be updated.
// Otherwise, we can jump directly to create stage.
if (queryTask.results.documentCount > 0) {
for (Object s : queryTask.results.documents.values()) {
ComputeState computeState = Utils.fromJson(s, ComputeState.class);
ctx.computeStates.add(computeState);
}
ctx.subStage = EnumerationSubStages.UPDATE_COMPUTESTATE_COMPUTEDESCRIPTION_DISK;
} else {
ctx.subStage = EnumerationSubStages.CREATE_LOCAL_VMS;
}
handleSubStage(ctx);
});
}
/**
* Enumerate VMs from Google Cloud Platform.
* @param ctx The Enumeration Context.
*/
private void enumerate(EnumerationContext ctx) {
logFine(() -> "Enumerating VMs from GCP");
URI uri;
if (ctx.enumNextPageLink != null) {
uri = UriUtils.extendUriWithQuery(UriUtils.buildUri(String.format(LIST_VM_TEMPLATE_URI,
ctx.projectId, ctx.zoneId)), MAX_RESULTS, VM_PAGE_SIZE, PAGE_TOKEN,
ctx.enumNextPageLink);
} else {
uri = UriUtils.extendUriWithQuery(UriUtils.buildUri(String.format(LIST_VM_TEMPLATE_URI,
ctx.projectId, ctx.zoneId)), MAX_RESULTS, VM_PAGE_SIZE);
}
Operation.createGet(uri)
.addRequestHeader(Operation.AUTHORIZATION_HEADER, AUTH_HEADER_BEARER_PREFIX
+ ctx.accessToken)
.setCompletion((op, er) -> {
if (er != null) {
handleError(ctx, er);
return;
}
GCPInstancesList GCPInstancesList = op.getBody(GCPInstancesList.class);
List GCPInstances = GCPInstancesList.items;
if (GCPInstances == null || GCPInstances.size() == 0) {
ctx.subStage = EnumerationSubStages.DELETE_LOCAL_VMS;
handleSubStage(ctx);
return;
}
ctx.enumNextPageLink = GCPInstancesList.nextPageToken;
logFine(() -> String.format("Retrieved %d VMs from GCP", GCPInstances.size()));
logFine(() -> String.format("Next page link %s", ctx.enumNextPageLink));
for (GCPInstance GCPInstance : GCPInstances) {
ctx.virtualMachines.put(GCPInstance.id, GCPInstance);
ctx.vmIds.add(GCPInstance.id);
}
logFine(() -> String.format("Processing %d VMs", ctx.vmIds.size()));
ctx.subStage = EnumerationSubStages.QUERY_LOCAL_VMS;
handleSubStage(ctx);
}).sendWith(this);
}
/**
* Method to retrieve the parent compute host on which the enumeration task will be performed.
* @param ctx The Enumeration Context.
* @param next The next enumeration sub stage.
*/
private void getHostComputeDescription(EnumerationContext ctx, EnumerationStages next) {
Consumer onSuccess = (op) -> {
ComputeStateWithDescription csd = op.getBody(ComputeStateWithDescription.class);
ctx.computeHostDesc = csd.description;
validateHost(ctx, ctx.computeHostDesc);
ctx.stage = next;
handleEnumerationRequest(ctx);
};
URI computeUri = UriUtils
.extendUriWithQuery(
UriUtils.buildUri(this.getHost(), ctx.enumRequest.resourceLink()),
UriUtils.URI_PARAM_ODATA_EXPAND,
Boolean.TRUE.toString());
AdapterUtils.getServiceState(this, computeUri, onSuccess, getFailureConsumer(ctx));
}
/**
* Method to arrive at the credentials needed to call the GCP API for enumerating the instances.
* @param ctx The Enumeration Context.
* @param next The next enumeration sub stage.
*/
private void getParentAuth(EnumerationContext ctx, EnumerationStages next) {
Consumer onSuccess = (op) -> {
ctx.parentAuth = op.getBody(AuthCredentialsServiceState.class);
validateAuth(ctx, ctx.parentAuth);
ctx.stage = next;
handleEnumerationRequest(ctx);
};
AdapterUtils.getServiceState(this,
createInventoryUri(this.getHost(), ctx.computeHostDesc.authCredentialsLink),
onSuccess, getFailureConsumer(ctx));
}
/**
* Method to retrieve the resource group on which the enumeration task will be performed.
* @param ctx The Enumeration Context.
* @param next The next enumeration sub stage.
*/
private void getResourceGroup(EnumerationContext ctx, EnumerationStages next) {
Consumer onSuccess = op -> {
ctx.resourceGroup = op.getBody(ResourceGroupState.class);
validateResourceGroup(ctx, ctx.resourceGroup);
ctx.stage = next;
handleEnumerationRequest(ctx);
};
URI resourceGroupURI = UriUtils.buildUri(this.getHost(),
ctx.computeHostDesc.groupLinks.iterator().next());
AdapterUtils.getServiceState(this, resourceGroupURI, onSuccess, getFailureConsumer(ctx));
}
/**
* Method to get the access token to send RESTful APIs later.
* Every access token is only valid for an hour.
* @param ctx The Enumeration Context.
* @param clientEmail The client email in service account's credential file.
* @param scopes The limitation of application's access.
* @param privateKey The key generated by private key in service account's credential file.
* @throws GeneralSecurityException The exception will be thrown when private key is invalid.
* @throws IOException The exception will be thrown when inputs are mal-formatted.
*/
private void getAccessToken(EnumerationContext ctx, String clientEmail,
Collection scopes, PrivateKey privateKey, EnumerationStages next)
throws GeneralSecurityException, IOException {
Consumer onSuccess = response -> {
ctx.accessToken = response.access_token;
ctx.stage = next;
handleEnumerationRequest(ctx);
};
JSONWebToken jwt = new JSONWebToken(clientEmail, scopes, privateKey);
String assertion = jwt.getAssertion();
GCPUtils.getAccessToken(this, assertion, onSuccess, getFailureConsumer(ctx));
}
/**
* The call back function of failure handler when get necessary service start for enumeration.
* @param ctx The Enumeration Context.
* @return The call back interface.
*/
private Consumer getFailureConsumer(EnumerationContext ctx) {
return t -> {
ctx.stage = EnumerationStages.ERROR;
ctx.error = t;
handleEnumerationRequest(ctx);
};
}
/**
* Method to validate that the passed in Compute Host Description is valid.
* Validating that the zoneId is populated in Compute Host Description.
* @param ctx The enumeration context.
* @param computeHostDesc The compute host description.
*/
private void validateHost(EnumerationContext ctx, ComputeDescription computeHostDesc) {
if (computeHostDesc.zoneId == null) {
throw new IllegalArgumentException("zoneId is required");
}
if (computeHostDesc.authCredentialsLink == null) {
throw new IllegalArgumentException("auth credential is required");
}
if (computeHostDesc.groupLinks == null) {
throw new IllegalArgumentException("resource group link is required");
}
if (computeHostDesc.groupLinks.size() != 1) {
throw new IllegalArgumentException("number of resource groups should be one");
}
ctx.zoneId = computeHostDesc.zoneId;
}
/**
* Method to validate that the passed in Auth Credential Response is valid.
* Validating that the userEmail and privateKey are populated in the response.
* @param parentAuth the auth credential
*/
private void validateAuth(EnumerationContext ctx, AuthCredentialsServiceState parentAuth) {
if (parentAuth.userEmail == null) {
throw new IllegalArgumentException("userEmail is required");
}
if (parentAuth.privateKey == null) {
throw new IllegalArgumentException("privateKey is required");
}
ctx.userEmail = parentAuth.userEmail;
ctx.privateKey = EncryptionUtils.decrypt(parentAuth.privateKey);
}
/**
* Method to validate that the passed in Resource Group is valid.
* Validating that the projectId is populated in Resource Group.
* @param ctx The enumeration context.
* @param resourceGroup The resource group.
*/
private void validateResourceGroup(EnumerationContext ctx, ResourceGroupState resourceGroup) {
if (resourceGroup.name == null) {
throw new IllegalArgumentException("projectName is required");
}
ctx.projectId = resourceGroup.name;
}
/**
* Error handler if there are exceptions during the enumeration.
* @param ctx The enumeration context.
* @param e The exception during enumeration.
*/
private void handleError(EnumerationContext ctx, Throwable e) {
logSevere(e);
ctx.error = e;
ctx.stage = EnumerationStages.ERROR;
handleEnumerationRequest(ctx);
}
/**
* Get the os type from GCP instance response.
* @param instance The response of GCP instance resource.
* @return The string value of the os type.
*/
private String getNormalizedOSType(GCPInstance instance) {
if (instance.disks == null || instance.disks.isEmpty()) {
return null;
}
String license = null;
for (GCPDisk disk : instance.disks) {
if (disk.boot) {
if (disk.licenses != null && disk.licenses.size() == 1) {
license = disk.licenses.get(0).toLowerCase();
}
break;
}
}
if (license == null) {
return null;
}
if (license.contains("windows")) {
return OSType.WINDOWS.toString();
}
return OSType.LINUX.toString();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy