All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vmware.photon.controller.model.adapters.util.enums.EndpointEnumerationProcess 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.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 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); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy