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

com.vmware.photon.controller.model.adapters.awsadapter.AWSEndpointAdapterService 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.awsadapter;

import static com.vmware.photon.controller.model.adapterapi.EndpointConfigRequest.ARN_KEY;
import static com.vmware.photon.controller.model.adapterapi.EndpointConfigRequest.EXTERNAL_ID_KEY;
import static com.vmware.photon.controller.model.adapterapi.EndpointConfigRequest.PRIVATE_KEYID_KEY;
import static com.vmware.photon.controller.model.adapterapi.EndpointConfigRequest.PRIVATE_KEY_KEY;
import static com.vmware.photon.controller.model.adapterapi.EndpointConfigRequest.REGION_KEY;
import static com.vmware.photon.controller.model.adapterapi.EndpointConfigRequest.ZONE_KEY;
import static com.vmware.photon.controller.model.adapters.awsadapter.AWSUtils.getEc2AsyncClient;
import static com.vmware.photon.controller.model.adapters.util.AdapterConstants.PHOTON_MODEL_ADAPTER_UNAUTHORIZED_MESSAGE;
import static com.vmware.photon.controller.model.adapters.util.AdapterConstants.PHOTON_MODEL_ADAPTER_UNAUTHORIZED_MESSAGE_CODE;
import static com.vmware.xenon.common.Operation.STATUS_CODE_UNAUTHORIZED;

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.ec2.AmazonEC2AsyncClient;
import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesRequest;
import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult;
import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient;
import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClientBuilder;

import com.vmware.photon.controller.model.adapterapi.EndpointConfigRequest;
import com.vmware.photon.controller.model.adapterapi.EndpointConfigRequest.RequestType;
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.AdapterUriUtil;
import com.vmware.photon.controller.model.adapters.util.EndpointAdapterUtils;
import com.vmware.photon.controller.model.adapters.util.EndpointAdapterUtils.Retriever;
import com.vmware.photon.controller.model.constants.PhotonModelConstants;
import com.vmware.photon.controller.model.constants.PhotonModelConstants.EndpointType;
import com.vmware.photon.controller.model.query.QueryUtils;
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.ComputeState;
import com.vmware.photon.controller.model.resources.EndpointService.EndpointState;
import com.vmware.xenon.common.DeferredResult;
import com.vmware.xenon.common.LocalizableValidationException;
import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.ServiceErrorResponse;
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.Query;
import com.vmware.xenon.services.common.QueryTask.Query.Builder;

/**
 * Adapter to validate and enhance AWS based endpoints.
 */
public class AWSEndpointAdapterService extends StatelessService {

    public static final String SELF_LINK = AWSUriPaths.AWS_ENDPOINT_CONFIG_ADAPTER;

    public static final String UNABLE_TO_VALIDATE_CREDENTIALS_IN_ANY_AWS_REGION =
            "Unable to validate credentials in any AWS region!";

    private AWSClientManager clientManager;

    @Override
    public void handleStart(Operation op) {
        this.clientManager = AWSClientManagerFactory
                .getClientManager(AWSConstants.AwsClientType.EC2);

        super.handleStart(op);
    }

    @Override
    public void handleStop(Operation op) {
        AWSClientManagerFactory.returnClientManager(this.clientManager,
                AWSConstants.AwsClientType.EC2);

        super.handleStop(op);
    }

    @Override
    public void handlePatch(Operation op) {
        if (!op.hasBody()) {
            op.fail(new IllegalArgumentException("body is required"));
            return;
        }

        EndpointConfigRequest body = op.getBody(EndpointConfigRequest.class);
        if (body.requestType == RequestType.CHECK_IF_ACCOUNT_EXISTS) {
            checkIfAccountExistsAndGetExistingDocuments(body, op);
            return;
        }

        EndpointAdapterUtils.handleEndpointRequest(this, op, body, credentials(),
                computeDesc(), compute(), endpoint(), validate(body));
    }


    private BiConsumer> validate(
            EndpointConfigRequest body) {

        return (credentials, callback) -> {
            String regionId = body.endpointProperties.get(REGION_KEY) == null ?
                    Regions.DEFAULT_REGION.getName() :
                    body.endpointProperties.get(REGION_KEY);

            validateEndpointUniqueness(credentials, body.checkForEndpointUniqueness, body.tenantLinks)
                    .thenCompose(aVoid -> validateCredentialsWithRegions(credentials, regionId))
                    .whenComplete((aVoid, e) -> {
                        if (e != null) {
                            if (e instanceof CompletionException) {
                                e = e.getCause();
                            }

                            ServiceErrorResponse r = Utils.toServiceErrorResponse(e);
                            callback.accept(r, e);
                            return;
                        }
                        callback.accept(null, null);
                    });
        };
    }


    private DeferredResult validateCredentialsWithRegions(
            AuthCredentialsServiceState credentials, String endpointRegion) {

        AtomicInteger index = new AtomicInteger(0);
        Regions[] regions = Regions.values();

        int epRegionIndex = Arrays.stream(regions)
                .map(Regions::getName)
                .collect(Collectors.toList())
                .indexOf(endpointRegion);

        // if found, swap defaultRegion with the first region to optimize
        if (epRegionIndex != -1) {
            Regions temp = regions[0];
            regions[0] = regions[epRegionIndex];
            regions[epRegionIndex] = temp;
        }

        DeferredResult deferredResult = new DeferredResult<>();
        validateCredentialsWithRegions(
                credentials, index, regions, deferredResult);
        return  deferredResult;
    }


    /**
     * Method to validate credentials until atleast one region returns success. Validation fails if
     * unable to validate in any region.
     */
    private void validateCredentialsWithRegions(
            AuthCredentialsServiceState credentials, AtomicInteger index,
            Regions[] regions, DeferredResult deferredResult) {

        if (index.get() >= regions.length) {
            //Unable to validate in any of the Regions.
            deferredResult.fail(new LocalizableValidationException(
                    UNABLE_TO_VALIDATE_CREDENTIALS_IN_ANY_AWS_REGION,
                    PHOTON_MODEL_ADAPTER_UNAUTHORIZED_MESSAGE,
                    PHOTON_MODEL_ADAPTER_UNAUTHORIZED_MESSAGE_CODE));
            return;
        }

        String region = regions[index.get()].getName();
        getEc2AsyncClient(credentials, region, this.clientManager.getExecutor())
                .thenCompose(this::validateCredentials)
                .whenComplete((res,e) -> {
                    if (e == null) {
                        //Validation succeeded in the region
                        deferredResult.complete((Void) null);
                        return;
                    }

                    if (!(e.getCause() instanceof LocalizableValidationException)) {
                        deferredResult.fail(new LocalizableValidationException(e,
                                PHOTON_MODEL_ADAPTER_UNAUTHORIZED_MESSAGE,
                                PHOTON_MODEL_ADAPTER_UNAUTHORIZED_MESSAGE_CODE));
                        return;
                    }

                    index.getAndIncrement();
                    validateCredentialsWithRegions(credentials, index, regions, deferredResult);
                });
    }


    private DeferredResult validateCredentials(AmazonEC2AsyncClient client) {
        AWSDeferredResultAsyncHandler asyncHandler =
                new AWSDeferredResultAsyncHandler<>(this, "Validate Credentials");

        client.describeAvailabilityZonesAsync(asyncHandler);

        return asyncHandler
                .toDeferredResult()
                .handle((describeAvailabilityZonesResult, e) -> {
                    if (e instanceof AmazonServiceException) {
                        AmazonServiceException ase = (AmazonServiceException) e;
                        if (ase.getStatusCode() == STATUS_CODE_UNAUTHORIZED) {

                            throw new LocalizableValidationException(
                                    e,
                                    PHOTON_MODEL_ADAPTER_UNAUTHORIZED_MESSAGE,
                                    PHOTON_MODEL_ADAPTER_UNAUTHORIZED_MESSAGE_CODE);
                        }
                    }
                    return null;
                });
    }

    /**
     * Validate that the endpoint is unique by comparing the Access key ID
     */
    private DeferredResult validateEndpointUniqueness(AuthCredentialsServiceState credentials,
            Boolean endpointUniqueness, List queryTaskTenantLinks) {
        if (Boolean.TRUE.equals(endpointUniqueness)) {
            Query authQuery = Builder.create()
                    .addFieldClause(PRIVATE_KEYID_KEY, credentials.privateKeyId).build();

            return EndpointAdapterUtils.validateEndpointUniqueness(this.getHost(), authQuery,
                    null, EndpointType.aws.name(), queryTaskTenantLinks);
        }
        return DeferredResult.completed(null);
    }

    private BiConsumer credentials() {
        return (c, r) -> {
            // overwrite fields that are set in endpointProperties, otherwise use the present ones
            if (c.privateKey != null) {
                r.get(PRIVATE_KEY_KEY).ifPresent(pKey -> c.privateKey = pKey);
            } else {
                c.privateKey = r.getRequired(PRIVATE_KEY_KEY);
            }
            if (c.privateKeyId != null) {
                r.get(PRIVATE_KEYID_KEY).ifPresent(pKeyId -> c.privateKeyId = pKeyId);
            } else {
                c.privateKeyId = r.getRequired(PRIVATE_KEYID_KEY);
            }
            c.customProperties = new HashMap<>();
            r.get(ARN_KEY).ifPresent(arn -> {
                c.customProperties.put(ARN_KEY, arn);
            });
            r.get(EXTERNAL_ID_KEY).ifPresent(externalId -> {
                c.customProperties.put(EXTERNAL_ID_KEY, externalId);
            });
            c.type = "accessKey";
        };
    }

    private BiConsumer computeDesc() {
        return (cd, r) -> {
            cd.regionId = r.get(REGION_KEY).orElse(null);
            cd.zoneId = r.get(ZONE_KEY).orElse(null);
            cd.environmentName = ComputeDescription.ENVIRONMENT_NAME_AWS;

            List children = new ArrayList<>();
            children.add(ComputeType.ZONE.toString());
            cd.supportedChildren = children;

            cd.instanceAdapterReference = AdapterUriUtil.buildPublicAdapterUri(getHost(),
                    AWSUriPaths.AWS_INSTANCE_ADAPTER);
            cd.enumerationAdapterReference = AdapterUriUtil.buildPublicAdapterUri(getHost(),
                    AWSUriPaths.AWS_ENUMERATION_ADAPTER);
            cd.powerAdapterReference = AdapterUriUtil.buildPublicAdapterUri(getHost(),
                    AWSUriPaths.AWS_POWER_ADAPTER);
            cd.diskAdapterReference = AdapterUriUtil.buildPublicAdapterUri(getHost(),
                    AWSUriPaths.AWS_DISK_ADAPTER);

            {
                URI statsAdapterUri = AdapterUriUtil.buildPublicAdapterUri(getHost(),
                        AWSUriPaths.AWS_STATS_ADAPTER);
                URI costStatsAdapterUri = AdapterUriUtil.buildPublicAdapterUri(getHost(),
                        AWSUriPaths.AWS_COST_STATS_ADAPTER);

                cd.statsAdapterReferences = new LinkedHashSet<>();
                cd.statsAdapterReferences.add(costStatsAdapterUri);
                cd.statsAdapterReferences.add(statsAdapterUri);
                cd.statsAdapterReference = statsAdapterUri;
            }
        };
    }

    private BiConsumer compute() {
        return (c, r) -> {
            StringBuffer b = new StringBuffer("https://ec2.");
            b.append(r.get(REGION_KEY).orElse(""));
            b.append(".amazonaws.com");

            c.type = ComputeType.ENDPOINT_HOST;
            c.regionId = r.get(REGION_KEY).orElse(null);
            c.environmentName = ComputeDescription.ENVIRONMENT_NAME_AWS;
            c.adapterManagementReference = UriUtils.buildUri(b.toString());
            String billsBucketName = r.get(AWSConstants.AWS_BILLS_S3_BUCKET_NAME_KEY).orElse(null);
            if (billsBucketName != null) {
                addEntryToCustomProperties(c, AWSConstants.AWS_BILLS_S3_BUCKET_NAME_KEY,
                        billsBucketName);
            }

            String isAutoDiscoveryEnabled = r
                    .get(PhotonModelConstants.IS_RESOURCE_AUTO_DISCOVERY_ENABLED).orElse(null);
            if (isAutoDiscoveryEnabled != null) {
                addEntryToCustomProperties(c,
                        PhotonModelConstants.IS_RESOURCE_AUTO_DISCOVERY_ENABLED,
                        isAutoDiscoveryEnabled);
            }

            Boolean mock = Boolean.valueOf(r.getRequired(EndpointAdapterUtils.MOCK_REQUEST));
            if (!mock) {
                String accountId = getAccountId(r.get(ARN_KEY).orElse(null),
                        r.get(PRIVATE_KEYID_KEY).orElse(null),
                        r.get(PRIVATE_KEY_KEY).orElse(null));
                if (accountId != null && !accountId.isEmpty()) {
                    addEntryToCustomProperties(c, AWSConstants.AWS_ACCOUNT_ID_KEY, accountId);
                    EndpointState es = new EndpointState();
                    es.endpointProperties = new HashMap<>();
                    es.endpointProperties.put(AWSConstants.AWS_ACCOUNT_ID_KEY, accountId);
                    String endpointReference = r
                            .getRequired(EndpointAdapterUtils.ENDPOINT_REFERENCE_URI);
                    Operation.createPatch(UriUtils.buildUri(endpointReference)).setReferer(getUri())
                            .setBody(es).sendWith(this);
                }
            }
        };
    }

    private BiConsumer endpoint() {
        return (e, r) -> {
            e.endpointProperties.put(EndpointConfigRequest.REGION_KEY,
                    r.get(REGION_KEY).orElse(null));
            e.endpointProperties.put(EndpointConfigRequest.PRIVATE_KEYID_KEY,
                    r.getRequired(PRIVATE_KEYID_KEY));

            // AWS end-point does support public images enumeration
            e.endpointProperties.put(EndpointConfigRequest.SUPPORT_PUBLIC_IMAGES,
                    Boolean.TRUE.toString());
        };
    }

    private void addEntryToCustomProperties(ComputeState c, String key, String value) {
        if (c.customProperties == null) {
            c.customProperties = new HashMap<>();
        }
        c.customProperties.put(key, value);
    }

    /**
     * Method to get the aws account ID from the specified credentials. If the ARN is set, it will
     * retrieve the account ID from the ARN directly. Otherwise, will attempt via the private key ID
     * and private key.
     * @param arn
     *         An Amazon Resource Name
     * @param privateKeyId
     *         An AWS account private key ID.
     * @param privateKey
     *         An AWS account private key.
     * @return
     */
    private String getAccountId(String arn, String privateKeyId, String privateKey) {
        if (arn != null) {
            return getAccountId(arn);
        }

        return getAccountId(privateKeyId, privateKey);
    }

    /**
     * Splits the ARN key to retrieve the account ID.
     * 

* An ARN is of the format arn:aws:service:region:account:resource -> so limiting the split to * 6 words and extracting the accountId which is 5th one in list. If the user is not authorized * to perform iam:GetUser on that resource,still error mesage will have accountId * @param arn * An Amazon Resource Name. * @return The account ID. */ private String getAccountId(String arn) { if (arn == null) { return null; } return arn.split(":", 6)[4]; } /** * Method gets the aws accountId from the specified credentials. * @param privateKeyId * @param privateKey * @return account ID */ private String getAccountId(String privateKeyId, String privateKey) { AWSCredentials awsCredentials = new BasicAWSCredentials(privateKeyId, privateKey); AWSStaticCredentialsProvider awsStaticCredentialsProvider = new AWSStaticCredentialsProvider( awsCredentials); AmazonIdentityManagementClientBuilder amazonIdentityManagementClientBuilder = AmazonIdentityManagementClientBuilder .standard() .withCredentials(awsStaticCredentialsProvider) .withRegion(Regions.DEFAULT_REGION); AmazonIdentityManagementClient iamClient = (AmazonIdentityManagementClient) amazonIdentityManagementClientBuilder .build(); String userId = null; try { if ((iamClient.getUser() != null) && (iamClient.getUser().getUser() != null) && (iamClient.getUser().getUser().getArn() != null)) { return getAccountId(iamClient.getUser().getUser().getArn()); } } catch (AmazonServiceException ex) { if (ex.getErrorCode().compareTo("AccessDenied") == 0) { String msg = ex.getMessage(); userId = msg.split(":", 7)[5]; } else { logSevere("Exception getting the accountId %s", ex); } } return userId; } private void checkIfAccountExistsAndGetExistingDocuments(EndpointConfigRequest req, Operation op) { if (req.isMockRequest) { req.accountAlreadyExists = false; op.setBody(req); op.complete(); return; } String accountId = getAccountId(req.endpointProperties.get(ARN_KEY), req.endpointProperties.get(EndpointConfigRequest.PRIVATE_KEYID_KEY), req.endpointProperties.get(EndpointConfigRequest.PRIVATE_KEY_KEY)); if (accountId != null && !accountId.isEmpty()) { QueryTask queryTask = QueryUtils .createAccountQuery(accountId, PhotonModelConstants.EndpointType.aws.name(), req.tenantLinks); queryTask.tenantLinks = req.tenantLinks; QueryUtils.startInventoryQueryTask(this, queryTask) .whenComplete((qrt, e) -> { if (e != null) { logSevere( () -> String.format( "Failure retrieving query results for compute host corresponding to" + "the account ID: %s", e.toString())); op.fail(e); return; } if (qrt.results.documentCount > 0) { req.accountAlreadyExists = true; Object state = qrt.results.documents.values().iterator().next(); ComputeState computeHost = Utils.fromJson(state, ComputeState.class); req.existingComputeState = computeHost; getComputeDescription(req, computeHost.descriptionLink, op); } else { req.accountAlreadyExists = false; op.setBody(req); op.complete(); return; } }); } else { //If the account Id cannot be looked up with the given set of credentials then de duplication is not possible. req.accountAlreadyExists = false; op.setBody(req); op.complete(); } } /** * Retrieves the compute description corresponding to the compute host for a given account id. */ private void getComputeDescription(EndpointConfigRequest req, String descriptionLink, Operation op) { Operation.createGet(getHost(), descriptionLink) .setCompletion((o, ex) -> { if (ex != null) { logSevere( () -> String.format( "Failure retrieving the compute host description corresponding to" + " the account ID: %s", ex.toString())); op.fail(ex); return; } ComputeDescription computeHostDescription = o.getBody(ComputeDescription.class); req.existingComputeDescription = computeHostDescription; op.setBody(req); op.complete(); return; }).sendWith(this); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy