com.cloudera.director.aws.ec2.EC2InstanceTemplateConfigurationValidator.java Source code

Java tutorial

Introduction

Here is the source code for com.cloudera.director.aws.ec2.EC2InstanceTemplateConfigurationValidator.java

Source

// (c) Copyright 2015 Cloudera, Inc.
//
// 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.cloudera.director.aws.ec2;

import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.AVAILABILITY_ZONE;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.BLOCK_DURATION_MINUTES;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.EBS_KMS_KEY_ID;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.EBS_VOLUME_COUNT;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.EBS_VOLUME_SIZE_GIB;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.EBS_VOLUME_TYPE;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.ENCRYPT_ADDITIONAL_EBS_VOLUMES;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.IAM_PROFILE_NAME;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.IMAGE;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.KEY_NAME;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.PLACEMENT_GROUP;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.ROOT_VOLUME_SIZE_GB;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.ROOT_VOLUME_TYPE;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.SECURITY_GROUP_IDS;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.SPOT_BID_USD_PER_HR;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.SUBNET_ID;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.TENANCY;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.TYPE;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.USE_SPOT_INSTANCES;
import static com.cloudera.director.spi.v1.model.util.Validations.addError;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesRequest;
import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult;
import com.amazonaws.services.ec2.model.DescribeImagesRequest;
import com.amazonaws.services.ec2.model.DescribeImagesResult;
import com.amazonaws.services.ec2.model.DescribeKeyPairsRequest;
import com.amazonaws.services.ec2.model.DescribeKeyPairsResult;
import com.amazonaws.services.ec2.model.DescribePlacementGroupsRequest;
import com.amazonaws.services.ec2.model.DescribePlacementGroupsResult;
import com.amazonaws.services.ec2.model.DescribeSecurityGroupsRequest;
import com.amazonaws.services.ec2.model.DescribeSecurityGroupsResult;
import com.amazonaws.services.ec2.model.DescribeSubnetsRequest;
import com.amazonaws.services.ec2.model.DescribeSubnetsResult;
import com.amazonaws.services.ec2.model.Image;
import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient;
import com.amazonaws.services.identitymanagement.model.GetInstanceProfileRequest;
import com.amazonaws.services.identitymanagement.model.NoSuchEntityException;
import com.amazonaws.services.kms.AWSKMSClient;
import com.amazonaws.services.kms.model.DescribeKeyRequest;
import com.amazonaws.services.kms.model.NotFoundException;
import com.cloudera.director.aws.AWSFilters;
import com.cloudera.director.aws.ec2.ebs.EBSMetadata;
import com.cloudera.director.aws.ec2.ebs.EBSMetadata.EbsVolumeMetadata;
import com.cloudera.director.spi.v1.model.ConfigurationPropertyToken;
import com.cloudera.director.spi.v1.model.ConfigurationValidator;
import com.cloudera.director.spi.v1.model.Configured;
import com.cloudera.director.spi.v1.model.LocalizationContext;
import com.cloudera.director.spi.v1.model.exception.PluginExceptionConditionAccumulator;
import com.cloudera.director.spi.v1.util.Preconditions;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;

import java.math.BigDecimal;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Validates EC2 instance template configuration.
 */
@SuppressWarnings({ "PMD.TooManyStaticImports", "PMD.UnusedPrivateField", "PMD.UnusedPrivateField", "unused",
        "PMD.UselessParentheses" })
public class EC2InstanceTemplateConfigurationValidator implements ConfigurationValidator {

    private static final Logger LOG = LoggerFactory.getLogger(EC2InstanceTemplateConfigurationValidator.class);

    @VisibleForTesting
    static final int MIN_ROOT_VOLUME_SIZE_GB = 10;
    private static final String SIXTY_FOUR_BIT_ARCHITECTURE = "x86_64";
    private static final String AVAILABLE_STATE = "available";

    @VisibleForTesting
    static final String HVM_VIRTUALIZATION = "hvm";

    @VisibleForTesting
    static final String PARAVIRTUAL_VIRTUALIZATION = "paravirtual";

    @VisibleForTesting
    static final String ROOT_DEVICE_TYPE = "ebs";

    @VisibleForTesting
    static final Set<String> ROOT_VOLUME_TYPES = ImmutableSet.of("gp2", "standard");

    @VisibleForTesting
    static final Set<String> TENANCY_TYPES = ImmutableSet.of("default", "dedicated");

    @VisibleForTesting
    static final String INVALID_AMI_NAME_MSG = "AMI ID does not start with ami-: %s";
    private static final String INVALID_AMI_ID = "InvalidAMIID";
    @VisibleForTesting
    static final String INVALID_AMI_MSG = "Invalid AMI: %s";
    @VisibleForTesting
    static final String INVALID_AMI_ARCHITECTURE_MSG = "Only 64-bit architecture is supported. Invalid architecture for AMI %s: %s";
    @VisibleForTesting
    static final String INVALID_AMI_OWNER_MSG = "Only certain Linux platforms are supported. "
            + "See the \"Supported Distributions and Resource Requirements\" section of "
            + "the Cloudera Director User Guide. " + "Invalid owner Id for AMI %s: %s (%s)";
    @VisibleForTesting
    static final String INVALID_AMI_OWNER_SPOT_MSG = "Only certain Linux platforms are supported for use with Spot instances. "
            + "See the \"Supported Distributions and Resource Requirements\" section of "
            + "the Cloudera Director User Guide. " + "Invalid owner Id for AMI %s: %s (%s)";
    @VisibleForTesting
    static final String INVALID_AMI_PLATFORM_MSG = "Only certain Linux platforms are supported. "
            + "See the \"Supported Distributions and Resource Requirements\" section of "
            + "the Cloudera Director User Guide. " + "Invalid platform for AMI %s: %s (%s)";
    @VisibleForTesting
    static final String INVALID_AMI_PLATFORM_SPOT_MSG = "Only certain Linux platforms are supported for use with Spot instances. "
            + "See the \"Supported Distributions and Resource Requirements\" section of "
            + "the Cloudera Director User Guide. " + "Invalid platform for AMI %s: %s (%s)";
    private static final String INVALID_AMI_STATE_MSG = "AMI should be available. Invalid state for AMI %s: %s";
    @VisibleForTesting
    static final String INVALID_AMI_INSTANCE_TYPE_COMPATIBILITY_MSG = "Incompatible AMI virtualization type."
            + " Instance type %s does not support %s virtualization type of AMI %s.";
    @VisibleForTesting
    static final String INVALID_AMI_ROOT_DEVICE_TYPE_MSG = "Only EBS root device type is supported."
            + " Invalid root device type for AMI %s: %s";

    private static final String INVALID_PARAMETER_VALUE = "InvalidParameterValue";

    private static final String INVALID_AVAILABILITY_ZONE = "Invalid availability zone";
    @VisibleForTesting
    static final String INVALID_AVAILABILITY_ZONE_MSG = INVALID_AVAILABILITY_ZONE + " : %s";

    private static final String INVALID_PLACEMENT_GROUP_ID = "InvalidPlacementGroup";
    @VisibleForTesting
    static final String INVALID_PLACEMENT_GROUP_MSG = "Invalid placement group: %s";

    @VisibleForTesting
    static final String INVALID_TENANCY_MSG = "Invalid tenancy type: %s. Available options: %s";

    @VisibleForTesting
    static final String INVALID_IAM_PROFILE_NAME_MSG = "Invalid IAM instance profile name: %s";

    private static final String INVALID_SUBNET_ID = "InvalidSubnetID";
    @VisibleForTesting
    static final String INVALID_SUBNET_MSG = "Invalid subnet ID: %s";

    private static final String INVALID_SECURITY_GROUP = "InvalidGroupId";
    @VisibleForTesting
    static final String INVALID_SECURITY_GROUP_MSG = "Invalid security group ID: %s";

    private static final String INVALID_KEY_PAIR = "InvalidKeyPair";
    @VisibleForTesting
    static final String INVALID_KEY_NAME_MSG = "Invalid key name: %s";

    public static final String INVALID_ROOT_VOLUME_TYPE_MSG = "Invalid root volume type %s. Available options: %s";
    public static final String INVALID_ROOT_VOLUME_SIZE_FORMAT_MSG = "Root volume size must be an integer: %s";
    public static final String INVALID_ROOT_VOLUME_SIZE_MSG = "Root volume size should be at least %dGB. Current configuration: %dGB";

    private static final String INVALID_COUNT_EMPTY_MSG = "%s not found: %s";
    private static final String INVALID_COUNT_DUPLICATES_MSG = "More than one %s found with identifier %s";

    @VisibleForTesting
    static final String INVALID_SPOT_BID_MSG = "Invalid Spot bid %s. Spot bid must be a positive value representing the price in USD/hr";

    @VisibleForTesting
    static final String INVALID_BLOCK_DURATION_MINUTES_MSG = "Invalid block duration in minutes, %s. Block duration must be a multiple of 60 "
            + "(60, 120, 180, 240, 300, or 360)";

    @VisibleForTesting
    static final String IMAGE_OWNER_ID_BLACKLIST_KEY = "ownerId";

    @VisibleForTesting
    static final String IMAGE_SPOT_OWNER_ID_BLACKLIST_KEY = "spotOwnerId";

    @VisibleForTesting
    static final String IMAGE_PLATFORM_BLACKLIST_KEY = "platform";

    @VisibleForTesting
    static final String IMAGE_SPOT_PLATFORM_BLACKLIST_KEY = "spotPlatform";

    @VisibleForTesting
    static final int MAX_VOLUMES_PER_INSTANCE = 10;

    @VisibleForTesting
    static final String INVALID_EBS_VOLUME_COUNT_FORMAT_MSG = "EBS volume count must be a integer: %s";

    @VisibleForTesting
    static final String INVALID_EBS_VOLUME_COUNT_MSG = "EBS volume count must be a non-negative integer no greater than %s";

    @VisibleForTesting
    static final String INVALID_EBS_VOLUME_SIZE_FORMAT_MSG = "EBS volume size must be a positive integer: %s";

    @VisibleForTesting
    static final String VOLUME_SIZE_NOT_IN_RANGE_MSG = "Volume size for %s must be between %d GiB and %d GiB";

    @VisibleForTesting
    static final String INVALID_EBS_ENCRYPTION_MSG = "EBS volume count should be greater than 0 to specify EBS encryption properties";

    @VisibleForTesting
    static final String INVALID_KMS_WHEN_ENCRYPTION_DISABLED_MSG = "The KMS Key ID can only be set with encryption enabled";

    @VisibleForTesting
    static final String INVALID_KMS_NOT_FOUND_MESSAGE = "The KMS Key ID could not be found";

    @VisibleForTesting
    static final String KMS_KEY_DENIED_MESSAGE = "Access denied attempting to verify the KMS Key ID. Ensure kms:DescribeKey permission is granted";

    /**
     * The EC2 provider.
     */
    private final EC2Provider provider;

    /**
     * The template filters.
     */
    private final AWSFilters templateFilters;

    /**
     * The EBS metadata.
     */
    private final EBSMetadata ebsMetadata;

    /**
     * Creates an EC2 instance template configuration validator with the specified parameters.
     *
     * @param provider the EC2 provider
     */
    public EC2InstanceTemplateConfigurationValidator(EC2Provider provider, EBSMetadata ebsMetadata) {
        this.provider = Preconditions.checkNotNull(provider, "provider");
        this.ebsMetadata = Preconditions.checkNotNull(ebsMetadata, "ebsMetadata");
        templateFilters = provider.getEC2Filters().getSubfilters("template");
    }

    @Override
    public void validate(String name, Configured configuration, PluginExceptionConditionAccumulator accumulator,
            LocalizationContext localizationContext) {

        AmazonEC2Client ec2Client = provider.getClient();
        AWSKMSClient kmsClient = provider.getKmsClient();

        checkImage(ec2Client, configuration, accumulator, localizationContext);
        checkSubnetId(ec2Client, configuration, accumulator, localizationContext);
        checkSecurityGroupIds(ec2Client, configuration, accumulator, localizationContext);
        checkAvailabilityZone(ec2Client, configuration, accumulator, localizationContext);
        checkPlacementGroup(ec2Client, configuration, accumulator, localizationContext);
        checkTenancy(configuration, accumulator, localizationContext);
        checkIamProfileName(configuration, accumulator, localizationContext);
        checkRootVolumeSize(configuration, accumulator, localizationContext);
        checkRootVolumeType(configuration, accumulator, localizationContext);
        checkEbsVolumes(kmsClient, configuration, accumulator, localizationContext);
        checkKeyName(ec2Client, configuration, accumulator, localizationContext);
        checkSpotParameters(configuration, accumulator, localizationContext);
    }

    /**
     * Validates the configured AMI.
     *
     * @param client              the EC2 client
     * @param configuration       the configuration to be validated
     * @param accumulator         the exception condition accumulator
     * @param localizationContext the localization context
     */
    @VisibleForTesting
    @SuppressWarnings("PMD.CollapsibleIfStatements")
    void checkImage(AmazonEC2Client client, Configured configuration,
            PluginExceptionConditionAccumulator accumulator, LocalizationContext localizationContext) {

        String imageName = configuration.getConfigurationValue(IMAGE, localizationContext);
        String type = configuration.getConfigurationValue(TYPE, localizationContext);

        int conditionCount = accumulator.getConditionsByKey().size();

        if (!imageName.startsWith("ami-")) {
            addError(accumulator, IMAGE, localizationContext, null, INVALID_AMI_NAME_MSG, imageName);
            return;
        }

        LOG.info(">> Describing AMI '{}'", imageName);
        DescribeImagesResult result = null;
        try {
            result = client.describeImages(new DescribeImagesRequest().withImageIds(imageName));
            checkCount(accumulator, IMAGE, localizationContext, imageName, result.getImages());
        } catch (AmazonServiceException e) {
            if (e.getErrorCode().startsWith(INVALID_AMI_ID)) {
                addError(accumulator, IMAGE, localizationContext, null, INVALID_AMI_MSG, imageName);
            } else {
                throw Throwables.propagate(e);
            }
        }

        if ((result == null) || (accumulator.getConditionsByKey().size() > conditionCount)) {
            return;
        }

        Image image = Iterables.getOnlyElement(result.getImages());
        if (!SIXTY_FOUR_BIT_ARCHITECTURE.equals(image.getArchitecture())) {
            addError(accumulator, IMAGE, localizationContext, null, INVALID_AMI_ARCHITECTURE_MSG, imageName,
                    image.getArchitecture());
        }

        AWSFilters imageFilters = templateFilters.getSubfilters(IMAGE.unwrap().getConfigKey());
        boolean useSpotInstances = Boolean
                .parseBoolean(configuration.getConfigurationValue(USE_SPOT_INSTANCES, localizationContext));

        String ownerId = image.getOwnerId();
        if (ownerId != null) {
            String blacklistValue = imageFilters.getBlacklistValue(IMAGE_OWNER_ID_BLACKLIST_KEY,
                    ownerId.toLowerCase());
            if (blacklistValue != null) {
                addError(accumulator, IMAGE, localizationContext, null, INVALID_AMI_OWNER_MSG, imageName, ownerId,
                        blacklistValue);
            } else {
                if (useSpotInstances) {
                    blacklistValue = imageFilters.getBlacklistValue(IMAGE_SPOT_OWNER_ID_BLACKLIST_KEY,
                            ownerId.toLowerCase());
                    if (blacklistValue != null) {
                        addError(accumulator, IMAGE, localizationContext, null, INVALID_AMI_OWNER_SPOT_MSG,
                                imageName, ownerId, blacklistValue);
                    }
                }
            }
        }

        String platform = image.getPlatform();
        if (platform != null) {
            String blacklistValue = imageFilters.getBlacklistValue(IMAGE_PLATFORM_BLACKLIST_KEY,
                    platform.toLowerCase());
            if (blacklistValue != null) {
                addError(accumulator, IMAGE, localizationContext, null, INVALID_AMI_PLATFORM_MSG, imageName,
                        platform, blacklistValue);
            } else {
                if (useSpotInstances) {
                    blacklistValue = imageFilters.getBlacklistValue(IMAGE_SPOT_PLATFORM_BLACKLIST_KEY,
                            platform.toLowerCase());
                    if (blacklistValue != null) {
                        addError(accumulator, IMAGE, localizationContext, null, INVALID_AMI_PLATFORM_SPOT_MSG,
                                imageName, platform, blacklistValue);
                    }
                }
            }
        }

        if (!AVAILABLE_STATE.equals(image.getState())) {
            addError(accumulator, IMAGE, localizationContext, null, INVALID_AMI_STATE_MSG, imageName,
                    image.getState());
        }

        if (!provider.getVirtualizationMappings().apply(image.getVirtualizationType()).contains(type)) {
            addError(accumulator, IMAGE, localizationContext, null, INVALID_AMI_INSTANCE_TYPE_COMPATIBILITY_MSG,
                    type, image.getVirtualizationType(), imageName);
        }

        if (!ROOT_DEVICE_TYPE.equals(image.getRootDeviceType())) {
            addError(accumulator, IMAGE, localizationContext, null, INVALID_AMI_ROOT_DEVICE_TYPE_MSG, imageName,
                    image.getRootDeviceType());
        }
    }

    /**
     * Validates the configured availability zone.
     *
     * @param client              the EC2 client
     * @param configuration       the configuration to be validated
     * @param accumulator         the exception condition accumulator
     * @param localizationContext the localization context
     */
    @VisibleForTesting
    void checkAvailabilityZone(AmazonEC2Client client, Configured configuration,
            PluginExceptionConditionAccumulator accumulator, LocalizationContext localizationContext) {

        String zoneName = configuration.getConfigurationValue(AVAILABILITY_ZONE, localizationContext);
        if (zoneName != null) {
            LOG.info(">> Describing zone '{}'", zoneName);

            try {
                DescribeAvailabilityZonesResult result = client
                        .describeAvailabilityZones(new DescribeAvailabilityZonesRequest().withZoneNames(zoneName));

                checkCount(accumulator, AVAILABILITY_ZONE, localizationContext, "Availability zone",
                        result.getAvailabilityZones());
            } catch (AmazonServiceException e) {
                if (e.getErrorCode().equals(INVALID_PARAMETER_VALUE)
                        && e.getMessage().contains(INVALID_AVAILABILITY_ZONE)) {
                    addError(accumulator, AVAILABILITY_ZONE, localizationContext, null,
                            INVALID_AVAILABILITY_ZONE_MSG, zoneName);
                } else {
                    throw Throwables.propagate(e);
                }
            }
        }
    }

    /**
     * Validates the configured placement group.
     *
     * @param client              the EC2 client
     * @param configuration       the configuration to be validated
     * @param accumulator         the exception condition accumulator
     * @param localizationContext the localization context
     */
    @VisibleForTesting
    void checkPlacementGroup(AmazonEC2Client client, Configured configuration,
            PluginExceptionConditionAccumulator accumulator, LocalizationContext localizationContext) {

        String placementGroup = configuration.getConfigurationValue(PLACEMENT_GROUP, localizationContext);

        if (placementGroup != null) {
            LOG.info(">> Describing placement group '{}'", placementGroup);

            try {
                DescribePlacementGroupsResult result = client.describePlacementGroups(
                        new DescribePlacementGroupsRequest().withGroupNames(placementGroup));

                checkCount(accumulator, PLACEMENT_GROUP, localizationContext, "Placement group",
                        result.getPlacementGroups());
            } catch (AmazonServiceException e) {
                if (e.getErrorCode().startsWith(INVALID_PLACEMENT_GROUP_ID)) {
                    addError(accumulator, PLACEMENT_GROUP, localizationContext, null, INVALID_PLACEMENT_GROUP_MSG,
                            placementGroup);
                } else {
                    throw Throwables.propagate(e);
                }
            }
        }
    }

    /**
     * Validates the configured tenancy type.
     *
     * @param configuration       the configuration to be validated
     * @param accumulator         the exception condition accumulator
     * @param localizationContext the localization context
     */
    @VisibleForTesting
    void checkTenancy(Configured configuration, PluginExceptionConditionAccumulator accumulator,
            LocalizationContext localizationContext) {

        String tenancy = configuration.getConfigurationValue(TENANCY, localizationContext);

        if (!TENANCY_TYPES.contains(tenancy)) {
            addError(accumulator, TENANCY, localizationContext, null, INVALID_TENANCY_MSG, tenancy,
                    Joiner.on(", ").join(TENANCY_TYPES));
        }
    }

    /**
     * Validates the configured IAM profile.
     *
     * @param configuration       the configuration to be validated
     * @param accumulator         the exception condition accumulator
     * @param localizationContext the localization context
     */
    @VisibleForTesting
    void checkIamProfileName(Configured configuration, PluginExceptionConditionAccumulator accumulator,
            LocalizationContext localizationContext) {

        String iamProfileName = configuration.getConfigurationValue(IAM_PROFILE_NAME, localizationContext);

        if (iamProfileName != null) {
            AmazonIdentityManagementClient iamClient = provider.getIdentityManagementClient();

            try {
                iamClient.getInstanceProfile(
                        new GetInstanceProfileRequest().withInstanceProfileName(iamProfileName));

            } catch (NoSuchEntityException e) {
                addError(accumulator, IAM_PROFILE_NAME, localizationContext, null, INVALID_IAM_PROFILE_NAME_MSG,
                        iamProfileName);
            }
        }
    }

    /**
     * Validates the configured subnet ID.
     *
     * @param client              the EC2 client
     * @param configuration       the configuration to be validated
     * @param accumulator         the exception condition accumulator
     * @param localizationContext the localization context
     */
    @VisibleForTesting
    void checkSubnetId(AmazonEC2Client client, Configured configuration,
            PluginExceptionConditionAccumulator accumulator, LocalizationContext localizationContext) {
        String subnetId = configuration.getConfigurationValue(SUBNET_ID, localizationContext);
        LOG.info(">> Describing subnet '{}'", subnetId);

        try {
            DescribeSubnetsResult result = client
                    .describeSubnets(new DescribeSubnetsRequest().withSubnetIds(subnetId));
            checkCount(accumulator, SUBNET_ID, localizationContext, "Subnet", result.getSubnets());
        } catch (AmazonServiceException e) {
            if (e.getErrorCode().startsWith(INVALID_SUBNET_ID)) {
                addError(accumulator, SUBNET_ID, localizationContext, null, INVALID_SUBNET_MSG, subnetId);
            } else {
                throw Throwables.propagate(e);
            }
        }
    }

    /**
     * Validates the configured security group IDs.
     *
     * @param client              the EC2 client
     * @param configuration       the configuration to be validated
     * @param accumulator         the exception condition accumulator
     * @param localizationContext the localization context
     */
    @VisibleForTesting
    void checkSecurityGroupIds(AmazonEC2Client client, Configured configuration,
            PluginExceptionConditionAccumulator accumulator, LocalizationContext localizationContext) {

        List<String> securityGroupsIds = EC2InstanceTemplate.CSV_SPLITTER
                .splitToList(configuration.getConfigurationValue(SECURITY_GROUP_IDS, localizationContext));

        for (String securityGroupId : securityGroupsIds) {
            LOG.info(">> Describing security group '{}'", securityGroupId);

            try {
                DescribeSecurityGroupsResult result = client
                        .describeSecurityGroups(new DescribeSecurityGroupsRequest().withGroupIds(securityGroupId));
                checkCount(accumulator, SECURITY_GROUP_IDS, localizationContext, securityGroupId,
                        result.getSecurityGroups());
            } catch (AmazonServiceException e) {
                if (e.getErrorCode().startsWith(INVALID_SECURITY_GROUP)) {
                    addError(accumulator, SECURITY_GROUP_IDS, localizationContext, null, INVALID_SECURITY_GROUP_MSG,
                            securityGroupId);
                } else {
                    throw Throwables.propagate(e);
                }
            }
        }
    }

    /**
     * Validates the configured root volume size.
     *
     * @param configuration       the configuration to be validated
     * @param accumulator         the exception condition accumulator
     * @param localizationContext the localization context
     */
    @VisibleForTesting
    void checkRootVolumeSize(Configured configuration, PluginExceptionConditionAccumulator accumulator,
            LocalizationContext localizationContext) {

        String rootVolumeSizeGBString = configuration.getConfigurationValue(ROOT_VOLUME_SIZE_GB,
                localizationContext);

        try {
            int rootVolumeSizeGB = Integer.parseInt(rootVolumeSizeGBString);
            if (rootVolumeSizeGB < MIN_ROOT_VOLUME_SIZE_GB) {
                addError(accumulator, ROOT_VOLUME_SIZE_GB, localizationContext, null, INVALID_ROOT_VOLUME_SIZE_MSG,
                        MIN_ROOT_VOLUME_SIZE_GB, rootVolumeSizeGB);
            }
        } catch (NumberFormatException e) {
            addError(accumulator, ROOT_VOLUME_SIZE_GB, localizationContext, null,
                    INVALID_ROOT_VOLUME_SIZE_FORMAT_MSG, rootVolumeSizeGBString);
        }
    }

    /**
     * Validates the configured root volume type.
     *
     * @param configuration       the configuration to be validated
     * @param accumulator         the exception condition accumulator
     * @param localizationContext the localization context
     */
    @VisibleForTesting
    void checkRootVolumeType(Configured configuration, PluginExceptionConditionAccumulator accumulator,
            LocalizationContext localizationContext) {

        String rootVolumeType = configuration.getConfigurationValue(ROOT_VOLUME_TYPE, localizationContext);

        if (!ROOT_VOLUME_TYPES.contains(rootVolumeType)) {
            addError(accumulator, ROOT_VOLUME_TYPE, localizationContext, null, INVALID_ROOT_VOLUME_TYPE_MSG,
                    rootVolumeType, Joiner.on(", ").join(ROOT_VOLUME_TYPES));
        }
    }

    /**
     * Validates the configuration for EBS volumes.
     *
     * @param kmsClient           the AWS KMS client
     * @param configuration       the configuration to be validated
     * @param accumulator         the exception condition accumulator
     * @param localizationContext the localization context
     */
    @VisibleForTesting
    void checkEbsVolumes(AWSKMSClient kmsClient, Configured configuration,
            PluginExceptionConditionAccumulator accumulator, LocalizationContext localizationContext) {
        String ebsVolumeCountString = configuration.getConfigurationValue(EBS_VOLUME_COUNT, localizationContext);

        int ebsVolumeCount;
        try {
            ebsVolumeCount = Integer.parseInt(ebsVolumeCountString);
        } catch (NumberFormatException e) {
            addError(accumulator, EBS_VOLUME_COUNT, localizationContext, null, INVALID_EBS_VOLUME_COUNT_FORMAT_MSG,
                    ebsVolumeCountString);
            return;
        }

        if (ebsVolumeCount < 0 || ebsVolumeCount > MAX_VOLUMES_PER_INSTANCE) {
            addError(accumulator, EBS_VOLUME_COUNT, localizationContext, null, INVALID_EBS_VOLUME_COUNT_MSG,
                    MAX_VOLUMES_PER_INSTANCE);
            return;
        }

        boolean enableEbsEncryption;

        enableEbsEncryption = Boolean.parseBoolean(
                configuration.getConfigurationValue(ENCRYPT_ADDITIONAL_EBS_VOLUMES, localizationContext));

        String kmsKeyId = configuration.getConfigurationValue(EBS_KMS_KEY_ID, localizationContext);

        if (ebsVolumeCount == 0) {

            // Disallow setting any EBS encryption configuration when not adding EBS
            // volumes. This makes it more apparent that encryption is done on the
            // added EBS volumes and not the root.

            if (enableEbsEncryption) {
                addError(accumulator, ENCRYPT_ADDITIONAL_EBS_VOLUMES, localizationContext, null,
                        INVALID_EBS_ENCRYPTION_MSG);
            }

            if (kmsKeyId != null) {
                addError(accumulator, EBS_KMS_KEY_ID, localizationContext, null, INVALID_EBS_ENCRYPTION_MSG);
            }
        }

        if (ebsVolumeCount > 0) {

            if (kmsKeyId != null) {
                if (!enableEbsEncryption) {
                    addError(accumulator, EBS_KMS_KEY_ID, localizationContext, null,
                            INVALID_KMS_WHEN_ENCRYPTION_DISABLED_MSG);
                }
                // verify that we can find the key in KMS
                DescribeKeyRequest keyRequest = new DescribeKeyRequest().withKeyId(kmsKeyId);
                try {
                    kmsClient.describeKey(keyRequest);
                } catch (NotFoundException ex) {
                    addError(accumulator, EBS_KMS_KEY_ID, localizationContext, null, INVALID_KMS_NOT_FOUND_MESSAGE);
                } catch (AmazonServiceException ex) {
                    if (ex.getErrorCode().equals("AccessDeniedException")) {
                        addError(accumulator, EBS_KMS_KEY_ID, localizationContext, null, KMS_KEY_DENIED_MESSAGE);
                    } else {
                        addError(accumulator, EBS_KMS_KEY_ID, localizationContext, null,
                                "AmazonServiceException exception " + ex.getErrorMessage());
                    }
                }
            }

            String strEbsVolumeSizeGiB = configuration.getConfigurationValue(EBS_VOLUME_SIZE_GIB,
                    localizationContext);

            int ebsVolumeSizeGiB;
            try {
                ebsVolumeSizeGiB = Integer.parseInt(strEbsVolumeSizeGiB);
            } catch (NumberFormatException e) {
                addError(accumulator, EBS_VOLUME_SIZE_GIB, localizationContext, null,
                        INVALID_EBS_VOLUME_SIZE_FORMAT_MSG, strEbsVolumeSizeGiB);
                return;
            }

            String volumeType = configuration.getConfigurationValue(EBS_VOLUME_TYPE, localizationContext);
            EbsVolumeMetadata metadata;

            try {
                metadata = ebsMetadata.apply(volumeType);
            } catch (NullPointerException e) {
                addError(accumulator, EBS_VOLUME_TYPE, localizationContext, null,
                        "Volume type unknown: " + e.getMessage());
                return;
            } catch (IllegalStateException e) {
                addError(accumulator, EBS_VOLUME_TYPE, localizationContext, null,
                        "Malformed metadata: " + e.getMessage());
                return;
            }

            int minAllowableSize = metadata.getMinSizeGiB();
            int maxAllowableSize = metadata.getMaxSizeGiB();

            if (ebsVolumeSizeGiB > maxAllowableSize || ebsVolumeSizeGiB < minAllowableSize) {
                addError(accumulator, EBS_VOLUME_SIZE_GIB, localizationContext, null, VOLUME_SIZE_NOT_IN_RANGE_MSG,
                        volumeType, minAllowableSize, maxAllowableSize);
            }
        }
    }

    /**
     * Validates the EC2 key name.
     *
     * @param client              the EC2 client
     * @param accumulator         the exception condition accumulator
     * @param localizationContext the localization context
     */
    @VisibleForTesting
    void checkKeyName(AmazonEC2Client client, Configured configuration,
            PluginExceptionConditionAccumulator accumulator, LocalizationContext localizationContext) {

        String keyName = configuration.getConfigurationValue(KEY_NAME, localizationContext);

        if (keyName != null) {
            LOG.info(">> Describing key pair");
            try {
                DescribeKeyPairsResult result = client
                        .describeKeyPairs(new DescribeKeyPairsRequest().withKeyNames(keyName));
                // TODO Should this be REDACTED instead of NotDisplayed?
                checkCount(accumulator, KEY_NAME, localizationContext, "NotDisplayed", result.getKeyPairs());

            } catch (AmazonServiceException e) {
                if (e.getErrorCode().startsWith(INVALID_KEY_PAIR)) {
                    addError(accumulator, KEY_NAME, localizationContext, null, INVALID_KEY_NAME_MSG, keyName);
                } else {
                    throw Throwables.propagate(e);
                }
            }
        }
    }

    /**
     * Validates the configured Spot parameters.
     *
     * @param configuration       the configuration to be validated
     * @param accumulator         the exception condition accumulator
     * @param localizationContext the localization context
     */
    @VisibleForTesting
    @SuppressWarnings("PMD.EmptyCatchBlock")
    void checkSpotParameters(Configured configuration, PluginExceptionConditionAccumulator accumulator,
            LocalizationContext localizationContext) {

        boolean useSpotInstances = Boolean
                .parseBoolean(configuration.getConfigurationValue(USE_SPOT_INSTANCES, localizationContext));
        String spotBidUSDPerHr = configuration.getConfigurationValue(SPOT_BID_USD_PER_HR, localizationContext);
        String blockDurationMinutes = Strings
                .emptyToNull(configuration.getConfigurationValue(BLOCK_DURATION_MINUTES, localizationContext));

        if ((spotBidUSDPerHr == null) || spotBidUSDPerHr.isEmpty()) {
            if (useSpotInstances) {
                addError(accumulator, SPOT_BID_USD_PER_HR, localizationContext, null,
                        SPOT_BID_USD_PER_HR.unwrap().getMissingValueErrorMessage(localizationContext));
            }
        } else {
            boolean valid = false;
            try {
                BigDecimal spotBid = new BigDecimal(spotBidUSDPerHr);
                valid = spotBid.compareTo(BigDecimal.ZERO) > 0;
            } catch (NumberFormatException ignore) {
            }
            if (!valid) {
                addError(accumulator, SPOT_BID_USD_PER_HR, localizationContext, null, INVALID_SPOT_BID_MSG,
                        spotBidUSDPerHr);
            }
        }
        if (blockDurationMinutes != null) {
            boolean valid = false;
            try {
                Integer blockDuration = Integer.valueOf(blockDurationMinutes);
                valid = blockDuration >= 60 && blockDuration <= 360 && blockDuration % 60 == 0;
            } catch (NumberFormatException ignore) {
            }
            if (!valid) {
                addError(accumulator, BLOCK_DURATION_MINUTES, localizationContext, null,
                        INVALID_BLOCK_DURATION_MINUTES_MSG, blockDurationMinutes);
            }
        }
    }

    /**
     * Verifies that the specified result list has exactly one element, and reports an appropriate
     * error otherwise.
     *
     * @param accumulator         the exception condition accumulator
     * @param token               the token representing the configuration property in error
     * @param localizationContext the localization context
     * @param field               the problem field value
     * @param result              the result list to be validated
     */
    private void checkCount(PluginExceptionConditionAccumulator accumulator, ConfigurationPropertyToken token,
            LocalizationContext localizationContext, String field, List<?> result) {

        if (result.isEmpty()) {
            addError(accumulator, token, localizationContext, null, INVALID_COUNT_EMPTY_MSG,
                    token.unwrap().getName(localizationContext), field);
        }

        if (result.size() > 1) {
            addError(accumulator, token, localizationContext, null, INVALID_COUNT_DUPLICATES_MSG,
                    token.unwrap().getName(localizationContext), field);
        }
    }
}