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

com.vmware.photon.controller.model.adapters.awsadapter.enumeration.AWSImageEnumerationAdapterService Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2015-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.enumeration;

import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWS_IMAGE_VIRTUALIZATION_TYPE_FILTER;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWS_IMAGE_VIRTUALIZATION_TYPE_HVM;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AWS_IMAGE_VIRTUALIZATION_TYPE_PARAVIRTUAL;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.VOLUME_TYPE;
import static com.vmware.photon.controller.model.adapters.awsadapter.util.AWSClientManagerFactory.returnClientManager;
import static com.vmware.photon.controller.model.resources.util.PhotonModelUtils.waitToComplete;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import com.amazonaws.services.ec2.AmazonEC2AsyncClient;
import com.amazonaws.services.ec2.model.BlockDeviceMapping;
import com.amazonaws.services.ec2.model.DescribeImagesRequest;
import com.amazonaws.services.ec2.model.DescribeImagesResult;
import com.amazonaws.services.ec2.model.DeviceType;
import com.amazonaws.services.ec2.model.EbsBlockDevice;
import com.amazonaws.services.ec2.model.Filter;
import com.amazonaws.services.ec2.model.Image;
import com.amazonaws.services.ec2.model.Tag;

import com.google.gson.reflect.TypeToken;

import com.vmware.photon.controller.model.adapterapi.ImageEnumerateRequest;
import com.vmware.photon.controller.model.adapterapi.ImageEnumerateRequest.ImageEnumerateRequestType;
import com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants;
import com.vmware.photon.controller.model.adapters.awsadapter.AWSConstants.AwsClientType;
import com.vmware.photon.controller.model.adapters.awsadapter.AWSUriPaths;
import com.vmware.photon.controller.model.adapters.awsadapter.util.AWSClientManager;
import com.vmware.photon.controller.model.adapters.awsadapter.util.AWSClientManagerFactory;
import com.vmware.photon.controller.model.adapters.awsadapter.util.AWSDeferredResultAsyncHandler;
import com.vmware.photon.controller.model.adapters.util.AdapterUtils;
import com.vmware.photon.controller.model.adapters.util.TaskManager;
import com.vmware.photon.controller.model.adapters.util.enums.EndpointEnumerationProcess;
import com.vmware.photon.controller.model.resources.ImageService;
import com.vmware.photon.controller.model.resources.ImageService.ImageState;
import com.vmware.photon.controller.model.resources.ImageService.ImageState.DiskConfiguration;
import com.vmware.photon.controller.model.resources.util.PhotonModelUtils;
import com.vmware.photon.controller.model.tasks.ImageEnumerationTaskService.ImageEnumerationTaskState;
import com.vmware.xenon.common.DeferredResult;
import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.StatelessService;
import com.vmware.xenon.common.Utils;
import com.vmware.xenon.services.common.QueryTask.Query.Builder;

/**
 * AWS image enumeration adapter responsible to enumerate AWS {@link ImageState}s. It handles
 * {@link ImageEnumerateRequest} as send/initiated by {@code ImageEnumerationTaskService}.
 */
public class AWSImageEnumerationAdapterService extends StatelessService {

    public static final String SELF_LINK = AWSUriPaths.AWS_IMAGE_ENUMERATION_ADAPTER;

    /**
     * @see #getImagesPageSize()
     */
    public static final String IMAGES_PAGE_SIZE_PROPERTY = "photon-model.adapter.aws.images.page.size";

    /**
     * @see #getImagesMaxConcurrentEnums()
     */
    public static final String IMAGES_MAX_CONCURRENT_ENUMS_PROPERTY = "photon-model.adapter.aws.images.max.concurrent.enums";

    /**
     * Get images page size from {@value #IMAGES_PAGE_SIZE_PROPERTY} system property. The value is
     * used to partition original AWS images list.
     *
     * @return by default return 1000
     */
    public static int getImagesPageSize() {

        final int DEFAULT = 1000;

        return Integer.getInteger(IMAGES_PAGE_SIZE_PROPERTY, DEFAULT);
    }

    /**
     * Get max number of concurrent images enumerations from
     * {@value #IMAGES_MAX_CONCURRENT_ENUMS_PROPERTY} system property.
     *
     * @return by default return 1, which implies sequential enumerations
     */
    public static int getImagesMaxConcurrentEnums() {
        final int DEFAULT = 1;

        return Math.max(DEFAULT, Integer.getInteger(IMAGES_MAX_CONCURRENT_ENUMS_PROPERTY, DEFAULT));
    }

    /**
     * {@link EndpointEnumerationProcess} specialization that loads AWS {@link Image}s into
     * {@link ImageState} store.
     */
    private static class AWSImageEnumerationContext
            extends EndpointEnumerationProcess {

        /**
         * As of now there are AWS regions with 90K+ images and their loading in a single call is
         * memory and time consuming. To overcome the memory leap we split images loading using
         * virtualizationType as partitioning criteria.
         *
         * 

* This class represents an iterator on top of images partitions. * *

* The iterator is sync. Still its loading done by * {@link AWSImageEnumerationContext#loadAwsImages()} is async. */ private static class PartitioningIterator extends PaginatingIterator { /** * The AWS images criteria (as {@link Filter}) used to partition all images during * enumeration. The sum of all partitions represents all AWS images and is evenly * distributed. * *

* As of now we are using partitioning by virtualizationType which results in two * buckets: paravirtual = 40968 images, hvm = 54800 images. */ final Iterator partitioningCriteria = Arrays.asList( new Filter(AWS_IMAGE_VIRTUALIZATION_TYPE_FILTER) .withValues(AWS_IMAGE_VIRTUALIZATION_TYPE_PARAVIRTUAL), new Filter(AWS_IMAGE_VIRTUALIZATION_TYPE_FILTER) .withValues(AWS_IMAGE_VIRTUALIZATION_TYPE_HVM)) .iterator(); /** * Current partition. Internally it is being paginated. */ PaginatingIterator partition = PaginatingIterator.empty(); PartitioningIterator withPartition(PaginatingIterator partition) { this.partition = partition; return this; } /** *

    *
  • current partition is fully iterated
  • *
  • there are more partitions to iterate
  • *
*/ boolean shouldLoadNextPartition() { return !this.partition.hasNext() && this.partitioningCriteria.hasNext(); } /** * From client perspective either current partition has more pages or there are more * partitions to iterate. */ @Override public boolean hasNext() { return this.partition.hasNext() || this.partitioningCriteria.hasNext(); } /** * Just delegate to current partition. * *

* It is up to the {@link AWSImageEnumerationContext#loadAwsImages()} to load next * partition through {@link #withPartition(PaginatingIterator)}. */ @Override public List next() { List delegatedPage = this.partition.next(); this.pageNumber++; this.totalNumber += delegatedPage.size(); return delegatedPage; } } /** * The underlying image-enum request. */ final ImageEnumerateRequest request; /** * The image-enum task that triggered this request. */ ImageEnumerationTaskState imageEnumTaskState; AmazonEC2AsyncClient awsClient; PartitioningIterator awsImages = new PartitioningIterator(); TaskManager taskManager; AWSImageEnumerationContext( AWSImageEnumerationAdapterService service, ImageEnumerateRequest request) { // TODO: set "computeHostLink" value for AWS image resources. super(service, request.resourceReference, null, ImageState.class, ImageService.FACTORY_LINK); this.taskManager = new TaskManager(this.service, request.taskReference, request.resourceLink()); this.request = request; if (request.requestType == ImageEnumerateRequestType.PUBLIC) { // Public/Shared images should NOT consider tenantLinks and endpointLink setApplyInfraFields(false); setApplyEndpointLink(false); } } @Override public String getEndpointRegion() { return this.request.regionId; } /** *

    *
  • Extract calling image-enum task state prior end-point loading.
  • *
  • Extract end-point region id once end-point state is loaded.
  • *
*/ @Override protected DeferredResult getEndpointState( AWSImageEnumerationContext context) { return DeferredResult.completed(context) .thenCompose(this::getImageEnumTaskState) .thenCompose(ctx -> super.getEndpointState(ctx)); } /** * Extract {@link ImageEnumerationTaskState} from {@code request.taskReference} and set it * to {@link #imageEnumTaskState}. */ private DeferredResult getImageEnumTaskState( AWSImageEnumerationContext context) { Operation op = Operation.createGet(context.request.taskReference); return context.service .sendWithDeferredResult(op, ImageEnumerationTaskState.class) .thenApply(state -> { context.imageEnumTaskState = state; return context; }); } /** * Create Amazon client prior core page-by-page enumeration. */ @Override protected DeferredResult enumeratePageByPage( AWSImageEnumerationContext context) { return DeferredResult.completed(context) .thenCompose(this::createAmazonClient) .thenCompose(ctx -> super.enumeratePageByPage(ctx)); } protected DeferredResult createAmazonClient( AWSImageEnumerationContext context) { DeferredResult r = new DeferredResult<>(); ((AWSImageEnumerationAdapterService) context.service).clientManager .getOrCreateEC2ClientAsync(context.endpointAuthState, context.getEndpointRegion(), context.service) .whenComplete((ec2Client, t) -> { if (t != null) { r.fail(t); return; } context.awsClient = ec2Client; r.complete(context); }); return r; } @Override protected DeferredResult getExternalResources(String nextPageLink) { // AWS does not support pagination of images so we internally partition // all results thus simulating paging return loadAwsImages().thenApply(imagesIterator -> { RemoteResourcesPage page = new RemoteResourcesPage(); if (imagesIterator.hasNext()) { final List awsImagesPage = imagesIterator.next(); for (Image awsImage : awsImagesPage) { page.resourcesPage.put(awsImage.getImageId(), awsImage); } } // Return a non-null nextPageLink to the parent so we are called back. if (imagesIterator.hasNext()) { page.nextPageLink = "awsImages_" + (imagesIterator.pageNumber() + 1); } else { this.service.logInfo("Enumerating AWS images: TOTAL number %s", imagesIterator.totalNumber()); } return page; }); } static final class ListOfFilters extends TypeToken> { static Type asType() { return new ListOfFilters().getType(); } } /** * */ private DeferredResult> loadAwsImages() { if (!this.awsImages.shouldLoadNextPartition()) { return DeferredResult.completed(this.awsImages); } // Otherwise load next partition of AWS images boolean isPublic = this.request.requestType == ImageEnumerateRequestType.PUBLIC; DescribeImagesRequest request = new DescribeImagesRequest() .withFilters(new Filter(AWSConstants.AWS_IMAGE_STATE_FILTER) .withValues(AWSConstants.AWS_IMAGE_STATE_AVAILABLE)) .withFilters(new Filter(AWSConstants.AWS_IMAGE_IS_PUBLIC_FILTER) .withValues(Boolean.toString(isPublic))) // The filter used as partitioning criteria .withFilters(this.awsImages.partitioningCriteria.next()); // Apply additional filtering to AWS images (used by tests) if (this.imageEnumTaskState.filter != null && !this.imageEnumTaskState.filter.isEmpty()) { // Deserialize the JSON string to a list of AWS Filters List filters = Utils.fromJson( this.imageEnumTaskState.filter, ListOfFilters.asType()); // NOTE: use withFilters(Filter...) to append NOT withFilter(List<>) request.withFilters(filters.toArray(new Filter[0])); } final String msg = "Enumerating AWS images by partition " + request; // ALL AWS images are returned with a single call, NO pagination! AWSDeferredResultAsyncHandler handler = new AWSDeferredResultAsyncHandler<>( this.service, msg); this.awsClient.describeImagesAsync(request, handler); return handler.toDeferredResult().thenCompose(awsImagesResult -> { this.service.logInfo("%s: TOTAL number %s", msg, awsImagesResult.getImages().size()); if (awsImagesResult.getImages().isEmpty()) { // Current partition is empty. Try recursively with next one. return loadAwsImages(); } // "artificially" paginate images once we load them all PaginatingIterator partition = new PaginatingIterator<>( awsImagesResult.getImages(), getImagesPageSize()); // Use loaded images as current partition this.awsImages.withPartition(partition); return DeferredResult.completed(this.awsImages); }); } @Override protected DeferredResult buildLocalResourceState( Image remoteImage, ImageState existingImageState) { LocalStateHolder holder = new LocalStateHolder(); holder.localState = new ImageState(); if (existingImageState == null) { // Create flow if (this.request.requestType == ImageEnumerateRequestType.PUBLIC) { holder.localState.endpointType = this.endpointState.endpointType; } } else { // Update flow: do nothing } // Both flows - populate from remote Image holder.localState.name = remoteImage.getName(); holder.localState.description = remoteImage.getDescription(); holder.localState.osFamily = remoteImage.getPlatform(); holder.localState.diskConfigs = new ArrayList<>(); if (DeviceType.Ebs == DeviceType.fromValue(remoteImage.getRootDeviceType())) { for (BlockDeviceMapping blockDeviceMapping : remoteImage.getBlockDeviceMappings()) { // blockDeviceMapping can be with noDevice EbsBlockDevice ebs = blockDeviceMapping.getEbs(); if (ebs != null) { DiskConfiguration diskConfig = new DiskConfiguration(); diskConfig.id = blockDeviceMapping.getDeviceName(); diskConfig.encrypted = ebs.getEncrypted(); diskConfig.persistent = true; if (ebs.getVolumeSize() != null) { diskConfig.capacityMBytes = ebs.getVolumeSize() * 1024; } diskConfig.properties = Collections.singletonMap( VOLUME_TYPE, ebs.getVolumeType()); holder.localState.diskConfigs.add(diskConfig); } } } for (Tag remoteImageTag : remoteImage.getTags()) { holder.remoteTags.put(remoteImageTag.getKey(), remoteImageTag.getValue()); } return DeferredResult.completed(holder); } /** *
    *
  • During PUBLIC image enum explicitly set {@code imageType}.
  • *
  • During PRIVATE image enum setting of {@code tenantLinks} and {@code endpointType} (by * default logic) is enough.
  • *
*/ @Override protected void customizeLocalStatesQuery(Builder qBuilder) { if (this.request.requestType == ImageEnumerateRequestType.PUBLIC) { qBuilder.addFieldClause( ImageState.FIELD_NAME_ENDPOINT_TYPE, this.endpointState.endpointType); } } } private AWSClientManager clientManager; private ExecutorService executorService; public AWSImageEnumerationAdapterService() { super.toggleOption(ServiceOption.INSTRUMENTATION, true); } /** * Extend default 'start' logic with loading AWS client. */ @Override public void handleStart(Operation op) { this.clientManager = AWSClientManagerFactory.getClientManager(AwsClientType.EC2); this.executorService = allocateExecutor(); super.handleStart(op); } /** * Extend default 'stop' logic with releasing AWS client. */ @Override public void handleStop(Operation op) { returnClientManager(this.clientManager, AwsClientType.EC2); this.executorService.shutdown(); AdapterUtils.awaitTermination(this.executorService); super.handleStop(op); } @Override public void handlePatch(Operation op) { if (!op.hasBody()) { op.fail(new IllegalArgumentException("body is required")); return; } // Immediately complete the Operation from calling task. op.complete(); AWSImageEnumerationContext ctx = new AWSImageEnumerationContext( this, op.getBody(ImageEnumerateRequest.class)); if (ctx.request.isMockRequest) { // Complete the task with FINISHED completeWithSuccess(ctx); return; } // Encapsulate core images enum code as Supplier, so we can manipulate and pass it. final Supplier> imagesEnum = () -> { final String msg = ctx.request.requestType + " images enum"; logInfo(() -> msg + ": STARTED"); // Start image enumeration process... DeferredResult imagesEnumDR = ctx.enumerate() .whenComplete((o, e) -> { // Once done patch the calling task with correct stage. if (e == null) { logInfo(() -> msg + ": COMPLETED"); completeWithSuccess(ctx); } else { logSevere(() -> msg + ": FAILED with " + Utils.toString(e)); completeWithFailure(ctx, e); } }); return imagesEnumDR; }; // Apply different execution strategies depending on enum type if (ctx.request.requestType == ImageEnumerateRequestType.PRIVATE) { // PRIVATE enums are handled immediately imagesEnum.get(); } else { // PUBLIC enums are executed sequentially in a dedicated Thread Pool Runnable publicImagesEnum = () -> waitToComplete(imagesEnum.get()); PhotonModelUtils.runInExecutor( this.executorService, publicImagesEnum, exc -> completeWithFailure(ctx, exc)); } } private void completeWithFailure(AWSImageEnumerationContext ctx, Throwable exc) { ctx.taskManager.patchTaskToFailure(exc); } private void completeWithSuccess(AWSImageEnumerationContext ctx) { ctx.taskManager.finishTask(); } /** * Creates an executor specific to AWS public images enum, which by default is single threaded * with a queue of size 20. */ private ExecutorService allocateExecutor() { final int corePoolSize = 1; // By default returns 1, so effectively we have single threaded executor; // otherwise the pool size varies final int imagesMaxConcurrentEnums = getImagesMaxConcurrentEnums(); final long keepAliveTime = 0L; final TimeUnit unit = TimeUnit.MILLISECONDS; // Use queue with size 20, which is close to AWS regions count final BlockingQueue workQueue = new ArrayBlockingQueue<>(20); ThreadFactory tFactory = r -> new Thread(r, "/" + getUri() + "/" + Utils.getSystemNowMicrosUtc()); return new ThreadPoolExecutor( corePoolSize, imagesMaxConcurrentEnums, keepAliveTime, unit, workQueue, tFactory); } /** * An iterator of pages of a list, each of the same size (the final page may be smaller). */ public static class PaginatingIterator implements Iterator> { public static PaginatingIterator empty() { return new PaginatingIterator<>(); } private final List originalList; final int pageSize; private int lastIndex = 0; int pageNumber = 0; int totalNumber = 0; private List page = null; /** * For internal use only! */ private PaginatingIterator() { this(null, 0); } public PaginatingIterator(List originalList, int pageSize) { // we are tolerant to null values this.originalList = originalList == null ? Collections.emptyList() : originalList; this.pageSize = pageSize; } @Override public boolean hasNext() { try { return this.lastIndex < this.originalList.size(); } finally { // Since AWS serves all images as single List, // we do our best to release it as soon as possible. clearLastPage(); } } /** * Returns the next page from original list. */ @Override public List next() { if (!hasNext()) { throw new NoSuchElementException( getClass().getSimpleName() + " has already been consumed."); } // Store prev lastIndex as beginIndex final int beginIndex = this.lastIndex; // Calculate current lastIndex this.lastIndex = Math.min(beginIndex + this.pageSize, this.originalList.size()); // Get a subList this.page = this.originalList.subList(beginIndex, this.lastIndex); this.pageNumber++; this.totalNumber += this.page.size(); return this.page; } /** * Clear consumed page to let GC do its work. */ private void clearLastPage() { if (this.page != null) { for (int i = 0, size = this.page.size(); i < size; i++) { this.page.set(i, null); } } this.page = null; } /** * Return the number of pages returned by {@link #next()} so far. */ public int pageNumber() { return this.pageNumber; } /** * Return the total number of elements returned by {@link #next()} so far. */ public int totalNumber() { return this.totalNumber; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy