
com.vmware.photon.controller.model.adapters.util.enums.EndpointEnumerationProcess 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.util.enums;
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;
import static com.vmware.photon.controller.model.adapters.util.AdapterUtils.getDeletionState;
import static com.vmware.photon.controller.model.resources.util.PhotonModelUtils.setEndpointLink;
import static com.vmware.photon.controller.model.resources.util.PhotonModelUtils.updateEndpointLinks;
import static com.vmware.photon.controller.model.util.PhotonModelUriUtils.createInventoryUri;
import static com.vmware.xenon.services.common.QueryTask.NumericRange.createLessThanRange;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
import com.vmware.photon.controller.model.UriPaths;
import com.vmware.photon.controller.model.adapterapi.EndpointConfigRequest;
import com.vmware.photon.controller.model.adapters.util.TagsUtil;
import com.vmware.photon.controller.model.constants.PhotonModelConstants.EndpointType;
import com.vmware.photon.controller.model.query.QueryUtils.QueryByPages;
import com.vmware.photon.controller.model.resources.EndpointService.EndpointState;
import com.vmware.photon.controller.model.resources.ResourceState;
import com.vmware.photon.controller.model.resources.util.PhotonModelUtils;
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.Operation.CompletionHandler;
import com.vmware.xenon.common.ServiceDocument;
import com.vmware.xenon.common.StatelessService;
import com.vmware.xenon.common.Utils;
import com.vmware.xenon.services.common.AuthCredentialsService.AuthCredentialsServiceState;
import com.vmware.xenon.services.common.QueryTask.Query;
import com.vmware.xenon.services.common.QueryTask.Query.Occurance;
/**
* The class abstracts the core enumeration logic per end-point. It consists of the following steps:
*
* - Loads the end-point state and authentication from provided end-point reference.
* - Loads remote resources from the remote system page-by-page.
* - Creates or updates corresponding local resource states.
* - Deletes stale local resource states.
*
*
* To use the class override its abstract methods and call {@link #enumerate()} method.
*
* @param
* The derived enumeration class.
* @param
* The class representing the local resource states.
* @param
* The class representing the remote resource.
*/
public abstract class EndpointEnumerationProcess, LOCAL_STATE extends ResourceState, REMOTE> {
private static final int MAX_RESOURCES_TO_QUERY_ON_DELETE = Integer
.getInteger(UriPaths.PROPERTY_PREFIX
+ "enum.max.resources.query.on.delete", 950);
/**
* The service that is creating and initiating this enumeration process.
*/
public final StatelessService service;
/**
* The end-point URI for which the enumeration process is triggered.
*/
public final URI endpointReference;
/**
* Resource link to current resource's compute host.
*/
public final String computeHostLink;
// Extracted from endpointReference {{
public EndpointState endpointState;
public AuthCredentialsServiceState endpointAuthState;
// }}
protected final Class localStateClass;
protected final String localStateServiceFactoryLink;
public final ResourceState resourceDeletionState;
protected final LOCAL_STATE SKIP = null;
/**
* Flag controlling whether infra fields (such as tenantLinks and endpointLink) should be
* applied (for example set or populated). Default value is {@code true}.
*
* @see #createUpdateLocalResourceState(LocalStateHolder)
*/
private boolean applyInfraFields = true;
/**
* Flag controlling whether queries should be agnostic to endpointLink or not (as part of
* resource deduplication work). Once the dedup is completed, we should be able to remove this
* flag completely.
*/
private boolean applyEndpointLink = true;
/**
* Represents a single page of remote resources.
*/
public class RemoteResourcesPage {
/**
* A link to the next page. Null if next page is not available.
*
* The value returned will be passed to the next call of
* {@link #getExternalResources(String)} method to fetch the next page.
*/
public String nextPageLink;
/**
* The loaded page of remote resources.
*/
public LinkedHashMap resourcesPage = new LinkedHashMap<>();
}
/**
* Current page of remote resources as fetched from the remote end-point.
*
* - key = remote object id
* - value = remote object
*
*/
public final Map remoteResources = new ConcurrentHashMap<>();
/**
* In-memory store of all> remote resource ids being enumerated.
*/
public final Set enumExternalResourcesIds = new HashSet<>();
/**
* The time when this enumeration started. It is used to identify stale resources that should be
* deleted during deletion stage.
*/
protected long enumStartTimeInMicros;
/**
* Link to the next page of remote resources. {@code null} indicates 'no more pages'.
*/
protected String enumExternalResourcesNextPageLink;
/**
* States stored in local store that correspond to current page of {@link #remoteResources}.
*
* - key = local state id (matching remote object id)
* - value = local state
*
*/
public final Map localResourceStates = new ConcurrentHashMap<>();
public class LocalStateHolder {
public LOCAL_STATE localState;
/**
* From the key-value pairs, TagStates are created or updated. The localState's tagLinks
* list is updated with the new remote tags, and the local-only tags are preserved.
*/
public Map remoteTags = new HashMap<>();
/**
* Each enumerated resource is associated with an internal tag/tags and are created the very
* first time the resource is enumerated.
*/
public Set internalTagLinks = new HashSet<>();
}
/**
* Constructs the {@link EndpointEnumerationProcess}.
*
* @param service
* The service that is creating and using this enumeration logic.
* @param endpointReference
* Reference to the end-point that is target of this enumeration.
* @param computeHostLink
* Parent compute host link for resource.
* @param localStateClass
* The class representing the local resource states.
* @param localStateServiceFactoryLink
* The factory link of the service handling the local resource states.
*/
public EndpointEnumerationProcess(StatelessService service,
URI endpointReference,
String computeHostLink,
Class localStateClass,
String localStateServiceFactoryLink) {
this(service,
endpointReference,
computeHostLink,
localStateClass,
localStateServiceFactoryLink,
0 /* deletedResourceExpirationMicros */);
}
/**
* Constructs the {@link EndpointEnumerationProcess}.
*
* @param service
* The service that is creating and using this enumeration logic.
* @param endpointReference
* Reference to the end-point that is target of this enumeration.
* @param computeHostLink
* Parent compute host link for resource.
* @param localStateClass
* The class representing the local resource states.
* @param localStateServiceFactoryLink
* The factory link of the service handling the local resource states.
* @param deletedResourceExpirationMicros
* Time in micros at which to expire deleted resources.
*/
public EndpointEnumerationProcess(
StatelessService service,
URI endpointReference,
String computeHostLink,
Class localStateClass,
String localStateServiceFactoryLink,
long deletedResourceExpirationMicros) {
AssertUtil.assertTrue(ResourceState.class != localStateClass,
"A specific descendant class of " + ResourceState.class.getName()
+ " should be pass to " + EndpointEnumerationProcess.class.getSimpleName());
this.service = service;
this.endpointReference = endpointReference;
this.computeHostLink = computeHostLink;
this.localStateClass = localStateClass;
this.localStateServiceFactoryLink = localStateServiceFactoryLink;
this.resourceDeletionState = getDeletionState(deletedResourceExpirationMicros);
}
public boolean isApplyInfraFields() {
return this.applyInfraFields;
}
public void setApplyInfraFields(boolean applyInfraFields) {
this.applyInfraFields = applyInfraFields;
}
public boolean isApplyEndpointLink() {
return this.applyEndpointLink;
}
public void setApplyEndpointLink(boolean applyEndpointLink) {
this.applyEndpointLink = applyEndpointLink;
}
/**
* The main method that starts the enumeration process and returns a {@link DeferredResult} to
* signal completion.
*/
public DeferredResult enumerate() {
this.enumStartTimeInMicros = Utils.getNowMicrosUtc();
return DeferredResult.completed(self())
.thenCompose(this::getEndpointState)
.thenApply(log("getEndpointState"))
.thenCompose(this::getEndpointAuthState)
.thenApply(log("getEndpointAuthState"))
.thenCompose(this::enumeratePageByPage)
.thenApply(log("enumeratePageByPage"))
.thenCompose(this::disassociateLocalResourceStates)
.thenApply(log("disassociateLocalResourceStates"));
}
/**
* Use this to log success after completing async execution stage.
*/
protected Function super T, ? extends T> log(String stage) {
return (ctx) -> {
ctx.service.log(Level.FINE, "%s.%s: SUCCESS", this.getClass().getSimpleName(), stage);
return ctx;
};
}
/**
* Return a page of external resources from the remote system.
*
* @param nextPageLink
* Link to the the next page. null if this is the call for the first page.
*
* @see {@link RemoteResourcesPage} for information about the format of the returned data.
*/
protected abstract DeferredResult getExternalResources(
String nextPageLink);
/**
* Creates/updates a resource base on the remote resource.
*
* Note: Descendants are responsible to provide key-values map describing the tags for
* the remote resource, and are off-loaded from setting the following properties:
*
* - {@code id} property should not be set since it is automatically set to the id of the
* remote resource.
* - {@code tenantLinks} property should not be set since it is automatically set to
* {@code endpointState.tenantLinks}.
* - {@code endpointLink} property should not be set since it is automatically set to
* {@code endpointState.documentSelfLink}.
*
*
* @param remoteResource
* The remote resource that should be represented in Photon model as a result of
* current enumeration.
* @param existingLocalResourceState
* The existing local resource state that matches the remote resource. {@code null}
* means there is no local resource state representing the remote resource.
*
* @return An instance of local state holder, consisting of the resource state (either existing
* or new) that describes the remote resource, and its tags placed in a map.
*/
protected abstract DeferredResult buildLocalResourceState(
REMOTE remoteResource, LOCAL_STATE existingLocalResourceState);
/**
* Descendants should override this method to specify the criteria to locate the local resources
* managed by this enumeration.
*
* @param qBuilder
* The builder used to express the query criteria.
*
* @see {@link #queryLocalStates(EndpointEnumerationProcess)} for details about the GET criteria
* being pre-set/used by this enumeration logic.
* @see {@link #disassociateLocalResourceStates(EndpointEnumerationProcess)} for details about
* the DELETE criteria being pre-set/used by this enumeration logic.
*/
protected abstract void customizeLocalStatesQuery(Query.Builder qBuilder);
/**
* Isolate all cases when this instance should be cast to T. For internal use only.
*/
@SuppressWarnings("unchecked")
protected T self() {
return (T) this;
}
/**
* Resolve {@code EndpointState} from {@link #endpointReference} and set it to
* {@link #endpointState}.
*/
protected DeferredResult getEndpointState(T context) {
Operation op = Operation.createGet(context.endpointReference);
return context.service
.sendWithDeferredResult(op, EndpointState.class)
.thenApply(state -> {
context.endpointState = state;
return context;
});
}
/**
* By default return
* {@code this.endpointState.endpointProperties.get(EndpointConfigRequest.REGION_KEY)} which
* might be {@code null}. Descendants might override to provide specific region in case
* REGION_KEY property is not specified.
*/
public String getEndpointRegion() {
AssertUtil.assertNotNull(
this.endpointState,
"endpointState should have been initialized by getEndpointState()");
return this.endpointState.endpointType != null &&
!this.endpointState.endpointType.equals(EndpointType.azure.name()) &&
this.endpointState.endpointProperties != null
? this.endpointState.endpointProperties.get(EndpointConfigRequest.REGION_KEY)
: null;
}
/**
* Resolve {@code AuthCredentialsServiceState end-point auth} from {@link #endpointState} and
* set it to {@link #endpointAuthState}.
*/
protected DeferredResult getEndpointAuthState(T context) {
Operation op = Operation.createGet(createInventoryUri(context.service.getHost(),
context.endpointState.authCredentialsLink));
return context.service
.sendWithDeferredResult(op, AuthCredentialsServiceState.class)
.thenApply(state -> {
context.endpointAuthState = state;
return context;
});
}
/**
* Get a page of remote resources from the remote system.
*/
protected DeferredResult getRemoteResources(T context) {
// Clear any previous results.
this.remoteResources.clear();
this.localResourceStates.clear();
// Delegate to descendants to fetch remote resources
return getExternalResources(this.enumExternalResourcesNextPageLink)
.thenApply(remoteResourcesPage -> {
context.service.logFine(() -> String.format(
"Fetch page [%s] of %d remote resources: SUCCESS",
this.enumExternalResourcesNextPageLink == null
? "FIRST" : this.enumExternalResourcesNextPageLink,
remoteResourcesPage.resourcesPage.size()));
// Store locally.
this.remoteResources.putAll(remoteResourcesPage.resourcesPage);
this.enumExternalResourcesNextPageLink = remoteResourcesPage.nextPageLink;
// Store ALL enum'd resource ids
this.enumExternalResourcesIds.addAll(this.remoteResources.keySet());
return context;
});
}
/**
* Enumerate remote resources page-by-page.
*/
protected DeferredResult enumeratePageByPage(T context) {
return DeferredResult.completed(context)
.thenCompose(this::getRemoteResources)
.thenCompose(this::queryLocalStates)
.thenCompose(this::createUpdateLocalResourceStates)
.thenCompose(ctx -> ctx.enumExternalResourcesNextPageLink != null
? enumeratePageByPage(ctx)
: DeferredResult.completed(ctx));
}
/**
* Load local resource states that match the {@link #getExternalResources(String) page} of
* remote resources that are being processed.
*
* Here is the list of criteria used to locate the local resources states:
*
* - Add local documents' kind:
* {@code qBuilder.addKindFieldClause(context.localStateClass)}
* - Add remote resources ids:
* {@code qBuilder.addInClause(ResourceState.FIELD_NAME_ID, remoteResourceIds)}
* - Add {@code tenantLinks} and {@code endpointLink} criteria as defined by
* {@code QueryTemplate}
* - Add descendant specific criteria as defined by
* {@link #customizeLocalStatesQuery(com.vmware.xenon.services.common.QueryTask.Query.Builder)}
*
*/
protected DeferredResult queryLocalStates(T context) {
String msg = "Query local %ss to match %d remote resources";
context.service.logFine(
() -> String.format(msg + ": STARTED",
context.localStateClass.getSimpleName(),
context.remoteResources.size()));
if (context.remoteResources.isEmpty()) {
return DeferredResult.completed(context);
}
Set remoteIds = context.remoteResources.keySet();
Query.Builder qBuilder = Query.Builder.create()
// Add documents' class
.addKindFieldClause(context.localStateClass)
// Add remote resources IDs
.addInClause(ResourceState.FIELD_NAME_ID, remoteIds);
if (getEndpointRegion() != null) {
// Limit documents within end-point region
qBuilder.addFieldClause(ResourceState.FIELD_NAME_REGION_ID, getEndpointRegion());
}
// Delegate to descendants to any doc specific criteria
customizeLocalStatesQuery(qBuilder);
QueryByPages queryLocalStates = new QueryByPages<>(
context.service.getHost(),
qBuilder.build(),
context.localStateClass,
isApplyInfraFields() ? context.endpointState.tenantLinks : null,
isApplyEndpointLink() ? context.endpointState.documentSelfLink : null,
null)
.setQueryTaskTenantLinks(context.endpointState.tenantLinks);
queryLocalStates.setMaxPageSize(remoteIds.size());
queryLocalStates.setClusterType(ServiceTypeCluster.INVENTORY_SERVICE);
return queryLocalStates
.queryDocuments(doc -> context.localResourceStates.put(doc.id, doc))
.thenApply(ignore -> {
context.service.logFine(
() -> String.format(
msg + ": FOUND %s",
context.localStateClass.getSimpleName(),
context.remoteResources.size(),
context.localResourceStates.size()));
return context;
});
}
/**
* Create new local resource states or update matching resource states with the actual state in
* the remote system.
*/
protected DeferredResult createUpdateLocalResourceStates(T context) {
String msg = "POST/PATCH local %ss to match %d remote resources: %s";
context.service.logFine(() -> String.format(msg,
context.localStateClass.getSimpleName(),
context.remoteResources.size(),
"STARTING"));
if (context.remoteResources.isEmpty()) {
return DeferredResult.completed(context);
}
List> drs = context.remoteResources.entrySet().stream()
.map(remoteResourceEntry -> {
String remoteResourceId = remoteResourceEntry.getKey();
REMOTE remoteResource = remoteResourceEntry.getValue();
LOCAL_STATE localResource = context.localResourceStates.get(remoteResourceId);
// Delegate to descendants to provide the local resource state to create/update
return buildLocalResourceState(remoteResource, localResource)
/*
* Explicitly set the local resource state id to be equal to the remote
* resource state id. This is important in the query for local states.
*/
.thenApply(lsHolder -> {
if (lsHolder.localState != context.SKIP) {
lsHolder.localState.id = remoteResourceId;
}
return lsHolder;
})
// Then actually update/create the state
.thenCompose(this::createUpdateLocalResourceState);
})
.collect(Collectors.toList());
return DeferredResult.allOf(drs).thenApply(ops -> {
this.service.logFine(() -> String.format(msg,
context.localStateClass.getSimpleName(),
context.remoteResources.size(),
ops.stream().filter(Objects::nonNull)
.collect(groupingBy(Operation::getAction, counting()))));
return context;
});
}
protected DeferredResult createUpdateLocalResourceState(
LocalStateHolder localStateHolder) {
final ResourceState localState = localStateHolder.localState;
if (localState == this.SKIP) {
return DeferredResult.completed(null);
}
final LOCAL_STATE currentState = this.localResourceStates.get(localState.id);
// POST or PATCH local state
final Operation localStateOp;
if (currentState == null) {
// Create case
if (localState.regionId == null) {
// By default populate REGION_ID, if not already set by descendant
localState.regionId = getEndpointRegion();
}
if (isApplyInfraFields()) {
// By default populate TENANT_LINKS
localState.tenantLinks = this.endpointState.tenantLinks;
// By default populate ENDPOINT_LINK
setEndpointLink(localState, this.endpointState.documentSelfLink);
updateEndpointLinks(localState, this.endpointState.documentSelfLink);
}
localState.computeHostLink = this.computeHostLink;
localStateOp = Operation.createPost(createInventoryUri(this.service.getHost(),
this.localStateServiceFactoryLink));
} else {
// Update case
if (isApplyInfraFields()) {
setEndpointLink(localState, this.endpointState.documentSelfLink);
// update the endpointLinks
updateEndpointLinks(localState, this.endpointState.documentSelfLink);
}
localStateOp = Operation.createPatch(createInventoryUri(this.service.getHost(),
currentState.documentSelfLink));
}
DeferredResult> tagLinksDR = TagsUtil.createOrUpdateTagStates(
this.service,
localState,
currentState,
localStateHolder.remoteTags, localStateHolder.internalTagLinks);
return tagLinksDR
.thenApply(tagLinks -> {
localState.tagLinks = tagLinks;
localStateOp.setBodyNoCloning(localState);
return localStateOp;
})
.thenCompose(this.service::sendWithDeferredResult)
.whenComplete((ignoreOp, exc) -> {
String msg = "%s local %s(id=%s) to match remote resources";
if (exc != null) {
this.service.logWarning(
() -> String.format(msg + ": FAILED with %s",
localStateOp.getAction(),
localState.getClass().getSimpleName(),
localState.id,
Utils.toString(exc)));
} else {
this.service.log(Level.FINEST,
() -> String.format(msg + ": SUCCESS",
localStateOp.getAction(),
localState.getClass().getSimpleName(),
localState.id));
}
});
}
/**
* Disassociate stale local resource states. The logic works by recording a timestamp when
* enumeration starts. This timestamp is used to lookup resources which have not been touched as
* part of current enumeration cycle. Resources not associated with any endpointLink will be
* removed by the groomer task.
*
* Here is the list of criteria used to locate the stale local resources states:
*
* - Add local documents' kind:
* {@code qBuilder.addKindFieldClause(context.localStateClass)}
* - Add time stamp older than current enumeration cycle:
* {@code qBuilder.addRangeClause(ServiceDocument.FIELD_NAME_UPDATE_TIME_MICROS, createLessThanRange(context.enumStartTimeInMicros))}
* - Add {@code tenantLinks} and {@code endpointLink} criteria as defined by
* {@code QueryTemplate}
* - Add descendant specific criteria as defined by
* {@link #customizeLocalStatesQuery(com.vmware.xenon.services.common.QueryTask.Query.Builder)}
*
*/
protected DeferredResult disassociateLocalResourceStates(T context) {
final String msg = "Disassociate %ss that no longer exist in the endpoint: %s";
context.service.logFine(
() -> String.format(msg, context.localStateClass.getSimpleName(), "STARTING"));
Query.Builder qBuilder = Query.Builder.create()
// Add documents' class
.addKindFieldClause(context.localStateClass)
.addRangeClause(
ServiceDocument.FIELD_NAME_UPDATE_TIME_MICROS,
createLessThanRange(context.enumStartTimeInMicros));
if (getEndpointRegion() != null) {
// Limit documents within end-point region
qBuilder.addFieldClause(ResourceState.FIELD_NAME_REGION_ID, getEndpointRegion());
}
if (!this.enumExternalResourcesIds.isEmpty() &&
this.enumExternalResourcesIds.size() <= MAX_RESOURCES_TO_QUERY_ON_DELETE) {
// do not load resources from enumExternalResourcesIds
qBuilder.addInClause(
ResourceState.FIELD_NAME_ID,
this.enumExternalResourcesIds,
Occurance.MUST_NOT_OCCUR);
}
// Delegate to descendants to any doc specific criteria
customizeLocalStatesQuery(qBuilder);
QueryByPages queryLocalStates = new QueryByPages<>(
context.service.getHost(),
qBuilder.build(),
context.localStateClass,
isApplyInfraFields() ? context.endpointState.tenantLinks : null,
isApplyEndpointLink() ? context.endpointState.documentSelfLink : null,
null)
.setQueryTaskTenantLinks(context.endpointState.tenantLinks);
queryLocalStates.setClusterType(ServiceTypeCluster.INVENTORY_SERVICE);
List> disassociateDRs = new ArrayList<>();
// Delete stale resources.
return queryLocalStates
.queryDocuments(localState -> {
if (!shouldDelete(localState)) {
return;
}
// Deleting the localResourceState is done by disassociating the
// endpointLink from the localResourceState. If the localResourceState
// isn't associated with any other endpointLink, it should be eventually
// deleted by the groomer task
Operation disassociateOp = PhotonModelUtils.createRemoveEndpointLinksOperation(
context.service,
context.endpointState.documentSelfLink,
localState);
if (disassociateOp == null) {
return;
}
// NOTE: The original Op is set with completion that must be executed.
// Since sendWithDeferredResult is used we must manually call it, otherwise it's
// just ignored.
CompletionHandler disassociateOpCompletion = disassociateOp.getCompletion();
DeferredResult disassociateDR = context.service
.sendWithDeferredResult(disassociateOp)
// First complete ORIGINAL disassociate callback
.whenComplete(disassociateOpCompletion::handle)
// Then do the logging
.whenComplete((o, e) -> {
final String message = "Disassociate stale %s state";
if (e != null) {
context.service.logWarning(message + ": FAILED with %s",
localState.documentSelfLink, Utils.toString(e));
} else {
context.service.log(Level.FINEST, message + ": SUCCESS",
localState.documentSelfLink);
}
});
disassociateDRs.add(disassociateDR);
})
.thenCompose(ignore -> DeferredResult.allOf(disassociateDRs))
.thenApply(ignore -> context);
}
/**
* Checks whether the local state should be deleted.
*/
protected boolean shouldDelete(LOCAL_STATE localState) {
return !this.enumExternalResourcesIds.contains(localState.id);
}
}