@VisibleForTesting
static final String BOTH_USER_DATA_USED =
    "Specify only the encoded or unencoded user data, not both"; @VisibleForTesting
static final String HVM_VIRTUALIZATION = "hvm"; @VisibleForTesting
static final String IMAGE_OWNER_ID_BLACKLIST_KEY = "ownerId"; @VisibleForTesting
static final String IMAGE_PLATFORM_BLACKLIST_KEY = "platform"; @VisibleForTesting
static final String IMAGE_SPOT_OWNER_ID_BLACKLIST_KEY = "spotOwnerId"; @VisibleForTesting
static final String IMAGE_SPOT_PLATFORM_BLACKLIST_KEY = "spotPlatform"; @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_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_NAME_MSG = "AMI ID does not start with ami-: %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 Altus 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 Altus 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 Altus 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 Altus Director User Guide. "
        + "Invalid platform for AMI %s: %s (%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"; @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_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 INVALID_IAM_PROFILE_NAME_MSG = "Invalid IAM instance profile name: %s"; @VisibleForTesting
static final String INVALID_IOPS_FORMAT_MSG = "IOPS must be a positive integer"; @VisibleForTesting
static final String INVALID_KMS_NOT_FOUND_MESSAGE =
    "The KMS Key ID could not be found"; @VisibleForTesting
static final String IOPS_NOT_IN_RANGE_MSG =
    "IOPS of %d for %s is not in range, it must be between %d and %d"; @VisibleForTesting
static final String IOPS_NOT_PERMITTED_MSG = "IOPS should only be set for io1 volume type"; @VisibleForTesting
static final String IOPS_REQUIRED_MSG = "IOPS must be set for io1 volume type"; @VisibleForTesting
static final int MAX_VOLUMES_PER_INSTANCE = 10; @VisibleForTesting
static final int MIN_ROOT_VOLUME_SIZE_GB = 10; @VisibleForTesting
static final String NO_SPOT_WITH_ASG_MSG =
    "Spot Instance use with Auto Scaling Groups is not currently supported"; @VisibleForTesting
static final String PARAVIRTUAL_VIRTUALIZATION = "paravirtual"; @VisibleForTesting
static final Set<String> ROOT_VOLUME_TYPES = ImmutableSet.of("gp2", "standard"); @VisibleForTesting
static final String VOLUME_SIZE_NOT_IN_RANGE_MSG =
    "Volume size for %s must be between %d GiB and %d GiB"; @SuppressWarnings("Guava")
public class AWSFilters {

  /**
   * A configuration key representing a blacklist.
   */
  public static final String BLACKLIST = "blacklist";

  /**
   * A configuration key representing a whitelist.
   */
  public static final String WHITELIST = "whitelist";

  /**
   * Empty filters.
   */
  public static final AWSFilters EMPTY_FILTERS =
      new AWSFilters(Optional.absent());

  /**
   * Returns AWS filters based on the specified configuration.
   *
   * @param config the underlying configuration
   * @return AWS filters based on the specified configuration
   */
  public static AWSFilters getAWSFilters(Config config) {
    return (config == null) ? EMPTY_FILTERS : new AWSFilters(Optional.of(config));
  }

  /**
   * The underlying configuration.
   */
  private Optional<Config> config;

  /**
   * Creates AWS filters with the specified parameters.
   *
   * @param config the underlying configuration
   */
  public AWSFilters(Optional<Config> config) {
    this.config = Preconditions.checkNotNull(config, "config is null");
  }

  /**
   * Returns the subfilters identified by the specified
   * sequence of keys.
   *
   * @param keys the keys
   * @return the subfilters identified by the specified
   * sequence of keys
   */
  public AWSFilters getSubfilters(String... keys) {
    if (config.isPresent()) {
      Config config = this.config.get();
      for (String key : keys) {
        try {
          config = config.getConfig(HoconConfigUtils.asConfigKey(key));
        } catch (ConfigException.Missing e) {
          return EMPTY_FILTERS;
        }
      }
      return getAWSFilters(config);
    } else {
      return EMPTY_FILTERS;
    }
  }

  /**
   * Returns the filter map for the specified key.
   *
   * @param key the key
   * @return the filter map for the specified key
   */
  public Map<String, String> getFilterMap(String key) {
    Map<String, String> filterMap;
    try {
      filterMap = config.isPresent()
          ? HoconConfigUtils.getStringMap(config.get(), HoconConfigUtils.asConfigKey(key))
          : Collections.emptyMap();
    } catch (Exception e) {
      filterMap = Collections.emptyMap();
    }
    return ImmutableMap.copyOf(filterMap);
  }

  /**
   * Returns whether the specified value is in the specified blacklist.
   *
   * @param key   the key identifying the specific blacklist to check
   * @param value the value to check
   * @return whether the specified value is in the specified blacklist
   */
  public boolean isBlacklisted(String key, String value) {
    return getBlacklistValue(key, value) != null;
  }

  /**
   * If the specified value is in the specified blacklist, returns the associated
   * blacklist value. Otherwise, returns {@code null}.
   *
   * @param key   the key identifying the specific blacklist to check
   * @param value the value to check
   * @return the blacklist value associated with the specified value in the specified blacklist,
   * or {@code null}
   */
  public String getBlacklistValue(String key, String value) {
    Map<String, String> blacklistMap = getSubfilters(key).getFilterMap(AWSFilters.BLACKLIST);
    return blacklistMap.get(value);
  }
} public class VirtualizationMappings implements Function<String, List<String>> {

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

  private static final Splitter SPLITTER = Splitter.on(',').trimResults();
  private static final Joiner JOINER = Joiner.on(',');

  public static class VirtualizationMappingsConfigProperties {

    /**
     * Virtualization mappings configuration properties.
     */
    // Fully qualifying class name due to compiler bug
    public enum VirtualizationMappingsConfigurationPropertyToken
        implements com.cloudera.director.spi.v2.model.ConfigurationPropertyToken {

      /**
       * Path for the custom virtualization mappings file. Relative paths are
       * based on the plugin configuration directory.
       */
      CUSTOM_MAPPINGS_PATH(new SimpleConfigurationPropertyBuilder()
          .configKey("customMappingsPath")
          .name("Custom mappings path")
          .defaultDescription("The path for the custom virtualization mappings file. Relative " +
              "paths are based on the plugin configuration directory.")
          .build());

      /**
       * The configuration property.
       */
      private final ConfigurationProperty configurationProperty;

      /**
       * Creates a configuration property token with the specified parameters.
       *
       * @param configurationProperty the configuration property
       */
      VirtualizationMappingsConfigurationPropertyToken(
          ConfigurationProperty configurationProperty) {
        this.configurationProperty = configurationProperty;
      }

      @Override
      public ConfigurationProperty unwrap() {
        return configurationProperty;
      }
    }

    public static final String DEFAULT_CUSTOM_MAPPINGS_PATH =
        "ec2.virtualizationmappings.properties";

    private final File configurationDirectory;

    private String customMappingsPath = DEFAULT_CUSTOM_MAPPINGS_PATH;

    /**
     * Creates virtualization mappings config properties with the specified parameters.
     *
     * @param configuration            the configuration
     * @param configurationDirectory   the plugin configuration directory
     * @param cloudLocalizationContext the parent cloud localization context
     */
    public VirtualizationMappingsConfigProperties(Configured configuration,
        File configurationDirectory,
        LocalizationContext cloudLocalizationContext) {
      this.configurationDirectory = configurationDirectory;
      LocalizationContext localizationContext = new ChildLocalizationContext(
          cloudLocalizationContext, "virtualizationMappings");
      setCustomMappingsPath(
          configuration.getConfigurationValue(CUSTOM_MAPPINGS_PATH, localizationContext));
    }

    public String getCustomMappingsPath() {
      return customMappingsPath;
    }

    File getCustomMappingsFile() {
      return getCustomMappingsFile(customMappingsPath);
    }

    File getCustomMappingsFile(String customMappingsPath) {
      File customMappingsPathFile = new File(customMappingsPath);
      return customMappingsPathFile.isAbsolute() ?
          customMappingsPathFile :
          new File(configurationDirectory, customMappingsPath);
    }

    public void setCustomMappingsPath(String customMappingsPath) {
      if (customMappingsPath != null) {
        LOG.info("Overriding customMappingsPath=%s (default {})",
            customMappingsPath, DEFAULT_CUSTOM_MAPPINGS_PATH);
        checkArgument(getCustomMappingsFile(customMappingsPath).exists(),
            "Custom virtualization mappings path " +
                customMappingsPath + " does not exist");
        this.customMappingsPath = customMappingsPath;
      }
    }
  }

  public static class VirtualizationMappingsConfig {

    public static final String BUILT_IN_LOCATION =
        "classpath:/com/cloudera/director/aws/ec2/virtualizationmappings.properties";

    protected VirtualizationMappingsConfigProperties
        virtualizationMappingsConfigProperties;

    public PropertyResolver virtualizationMappingsResolver() {
      try {
        return PropertyResolvers.newMultiResourcePropertyResolver(
            BUILT_IN_LOCATION,
            "file:" + virtualizationMappingsConfigProperties.getCustomMappingsFile().getAbsolutePath()
        );
      } catch (IOException e) {
        throw new IllegalArgumentException("Could not load custom virtualization mappings", e);
      }
    }

    /**
     * Creates an virtualization mappings config with the specified parameters.
     *
     * @param virtualizationMappingsConfigProperties the config properties
     */
    public VirtualizationMappingsConfig(
        VirtualizationMappingsConfigProperties virtualizationMappingsConfigProperties) {
      this.virtualizationMappingsConfigProperties = virtualizationMappingsConfigProperties;
    }
  }

  VirtualizationMappingsConfigProperties virtualizationMappingsConfigProperties;

  PropertyResolver virtualizationMappingsResolver;

  /**
   * Creates virtualization mappings with the specified parameters.
   *
   * @param configuration          the configuration
   * @param configurationDirectory the plugin configuration directory
   * @param localizationContext    the localization context
   */
  public VirtualizationMappings(Configured configuration, File configurationDirectory,
      LocalizationContext localizationContext) {
    this(new VirtualizationMappingsConfigProperties(configuration, configurationDirectory,
        localizationContext));
  }

  /**
   * Creates virtualization mappings with the specified parameters.
   *
   * @param virtualizationMappingsConfigProperties the config properties
   */
  public VirtualizationMappings(VirtualizationMappingsConfigProperties virtualizationMappingsConfigProperties) {
    this(virtualizationMappingsConfigProperties, new VirtualizationMappingsConfig(virtualizationMappingsConfigProperties).virtualizationMappingsResolver());
  }

  /**
   * Creates virtualization mappings with the specified parameters.
   *
   * @param virtualizationMappingsConfigProperties the config properties
   * @param virtualizationMappingsResolver         the virtualization mappings resolver
   */
  private VirtualizationMappings(VirtualizationMappingsConfigProperties virtualizationMappingsConfigProperties,
      PropertyResolver virtualizationMappingsResolver) {
    this.virtualizationMappingsConfigProperties = virtualizationMappingsConfigProperties;
    this.virtualizationMappingsResolver = virtualizationMappingsResolver;
  }

  /**
   * Gets the instance types that support the given virtualization type.
   *
   * @param virtualizationType virtualization type
   * @return list of instance types
   * @throws IllegalArgumentException if the virtualization type is unknown
   */
  @Nonnull
  public List<String> apply(String virtualizationType) {
    checkNotNull(virtualizationType, "virtualizationType is null");
    String instanceTypeList =
        virtualizationMappingsResolver.getProperty(virtualizationType);
    if (instanceTypeList == null) {
      throw new IllegalArgumentException("Unknown virtualization type " +
          virtualizationType);
    }
    return SPLITTER.splitToList(instanceTypeList);
  }

  /**
   * Gets a test instance of this class that uses only the given mapping.
   *
   * @param instanceTypes       map of virtualization types to instance types
   * @param localizationContext the localization context
   * @return new mapping object
   */
  public static VirtualizationMappings getTestInstance(
      final Map<String, List<String>> instanceTypes, LocalizationContext localizationContext) {
    Map<String, String> propertyMap =
        Maps.transformValues(instanceTypes, JOINER::join);
    PropertyResolver virtualizationMappingsResolver =
        PropertyResolvers.newMapPropertyResolver(propertyMap);
    File tempDir = Files.createTempDir();
    tempDir.deleteOnExit();
    VirtualizationMappingsConfigProperties virtualizationMappingsConfigProperties =
        new VirtualizationMappingsConfigProperties(new SimpleConfiguration(),
            tempDir, localizationContext);
    return new VirtualizationMappings(virtualizationMappingsConfigProperties,
        virtualizationMappingsResolver);
  }
} @SuppressWarnings("PMD.UnusedPrivateField")
public class EBSMetadata implements Function<String, EBSMetadata.EbsVolumeMetadata> {

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

  /**
   * Contains metadata information for an EBS volume type.
   */
  @SuppressWarnings("Guava")
  public static class EbsVolumeMetadata {
    private final String type;
    private final int minSizeGiB;
    private final int maxSizeGiB;
    private final Optional<Integer> minIops;
    private final Optional<Integer> maxIops;

    public EbsVolumeMetadata(String type, int minSizeGiB, int maxSizeGiB) {
      this.type = Objects.requireNonNull(type, "type is null");
      this.minSizeGiB = minSizeGiB;
      this.maxSizeGiB = maxSizeGiB;
      this.minIops = Optional.absent();
      this.maxIops = Optional.absent();
    }

    public EbsVolumeMetadata(String type, int minSizeGiB, int maxSizeGiB, int minIops, int maxIops) {
      this.type = Objects.requireNonNull(type, "type is null");
      this.minSizeGiB = minSizeGiB;
      this.maxSizeGiB = maxSizeGiB;
      this.minIops = Optional.of(minIops);
      this.maxIops = Optional.of(maxIops);
    }

    public String getType() {
      return type;
    }

    public int getMinSizeGiB() {
      return minSizeGiB;
    }

    public int getMaxSizeGiB() {
      return maxSizeGiB;
    }

    public Optional<Integer> getMinIops() {
      return minIops;
    }

    public Optional<Integer> getMaxIops() {
      return maxIops;
    }

    @SuppressWarnings("SimplifiableIfStatement")
    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      EbsVolumeMetadata that = (EbsVolumeMetadata) o;

      if (minSizeGiB != that.minSizeGiB) return false;
      if (maxSizeGiB != that.maxSizeGiB) return false;
      if (!type.equals(that.type)) return false;
      if (!minIops.equals(that.minIops)) return false;
      return maxIops.equals(that.maxIops);
    }

    @Override
    public int hashCode() {
      int result = type.hashCode();
      result = 31 * result + minSizeGiB;
      result = 31 * result + maxSizeGiB;
      result = 31 * result + minIops.hashCode();
      result = 31 * result + maxIops.hashCode();
      return result;
    }
  }

  public static class EBSMetadataConfigProperties {

    /**
     * EBS metadata configuration properties.
     */
    // Fully qualifying class name due to compiler bug
    public enum EBSMetadataConfigurationPropertyToken
        implements com.cloudera.director.spi.v2.model.ConfigurationPropertyToken {

      /**
       * Path for the custom EBS metadata file. Relative paths are based on the
       * plugin configuration directory.
       */
      CUSTOM_EBS_METADATA_PATH(new SimpleConfigurationPropertyBuilder()
          .configKey("ebsMetadataPath")
          .name("Custom EBS metadata path")
          .defaultDescription("The path for the custom ebs metadata. Relative paths are based " +
              "on the plugin configuration directory.")
          .build());

      /**
       * The configuration property.
       */
      private final ConfigurationProperty configurationProperty;

      /**
       * Creates a configuration property token with the specified parameters.
       *
       * @param configurationProperty the configuration property
       */
      EBSMetadataConfigurationPropertyToken(
          ConfigurationProperty configurationProperty) {
        this.configurationProperty = configurationProperty;
      }

      @Override
      public ConfigurationProperty unwrap() {
        return configurationProperty;
      }
    }

    public static final String DEFAULT_CUSTOM_EBS_METADATA_PATH =
        "ec2.ebs.ebsmetadata.properties";

    private final File configurationDirectory;

    private String ebsMetadataPath = DEFAULT_CUSTOM_EBS_METADATA_PATH;

    /**
     * Creates EBS metadata config properties with the specified parameters.
     *
     * @param configuration            the configuration
     * @param configurationDirectory   the plugin configuration directory
     * @param cloudLocalizationContext the parent cloud localization context
     */
    public EBSMetadataConfigProperties(Configured configuration,
        File configurationDirectory,
        LocalizationContext cloudLocalizationContext) {
      this.configurationDirectory = configurationDirectory;
      LocalizationContext localizationContext = new ChildLocalizationContext(
          cloudLocalizationContext, "ebsMetadata");
      setCustomEbsMetadataPath(
          configuration.getConfigurationValue(EBSMetadataConfigurationPropertyToken.CUSTOM_EBS_METADATA_PATH,
              localizationContext));
    }

    public String getCustomEbsMetadataPath() {
      return ebsMetadataPath;
    }

    File getCustomEbsMetadataFile() {
      return getCustomEbsMetadataFile(ebsMetadataPath);
    }

    File getCustomEbsMetadataFile(String ebsMetadataPath) {
      File ebsMetadataPathFile = new File(ebsMetadataPath);
      return ebsMetadataPathFile.isAbsolute() ?
          ebsMetadataPathFile :
          new File(configurationDirectory, ebsMetadataPath);
    }

    public void setCustomEbsMetadataPath(String ebsMetadataPath) {
      if (ebsMetadataPath != null) {
        LOG.info("Overriding ebsMetadataPath={} (default {})",
            ebsMetadataPath, DEFAULT_CUSTOM_EBS_METADATA_PATH);
        checkArgument(getCustomEbsMetadataFile(ebsMetadataPath).exists(),
            "Custom EBS metadata path " +
                ebsMetadataPath + " does not exist");
        this.ebsMetadataPath = ebsMetadataPath;
      }
    }
  }

  public static class EBSMetadataConfig {

    public static final String BUILT_IN_LOCATION =
        "classpath:/com/cloudera/director/aws/ec2/ebs/ebsmetadata.properties";

    protected EBSMetadataConfigProperties ebsMetadataConfigProperties;

    public PropertyResolver ebsMetadataResolver() {
      try {
        return PropertyResolvers.newMultiResourcePropertyResolver(
            BUILT_IN_LOCATION,
            "file:" + ebsMetadataConfigProperties.getCustomEbsMetadataFile().getAbsolutePath()
        );
      } catch (IOException e) {
        throw new IllegalArgumentException("Could not load custom ebs metadata", e);
      }
    }

    /**
     * Creates EBS metadata config with the specified parameters.
     *
     * @param ebsMetadataConfigProperties the config properties
     */
    public EBSMetadataConfig(EBSMetadataConfigProperties ebsMetadataConfigProperties) {
      this.ebsMetadataConfigProperties = ebsMetadataConfigProperties;
    }
  }

  private final EBSMetadataConfigProperties ebsMetadataConfigProperties;

  private final PropertyResolver ebsMetadataResolver;

  /**
   * Creates EBS metadata with the specified parameters.
   *
   * @param configuration          the configuration
   * @param configurationDirectory the plugin configuration directory
   * @param localizationContext    the localization context
   */
  public EBSMetadata(Configured configuration, File configurationDirectory,
      LocalizationContext localizationContext) {
    this(new EBSMetadataConfigProperties(configuration, configurationDirectory,
        localizationContext));
  }

  /**
   * Creates EBS metadata with the specified parameters.
   *
   * @param ebsMetadataConfigProperties the config properties
   */
  public EBSMetadata(EBSMetadataConfigProperties ebsMetadataConfigProperties) {
    this(ebsMetadataConfigProperties, new EBSMetadataConfig(ebsMetadataConfigProperties).ebsMetadataResolver());
  }

  /**
   * Creates EBS metadata with the specified parameters.
   *
   * @param ebsMetadataConfigProperties the config properties
   * @param ebsMetadataResolver         the ebs metadata resolver
   */
  private EBSMetadata(EBSMetadataConfigProperties ebsMetadataConfigProperties,
      PropertyResolver ebsMetadataResolver) {
    this.ebsMetadataConfigProperties = ebsMetadataConfigProperties;
    this.ebsMetadataResolver = ebsMetadataResolver;
  }

  /**
   * Gets the metadata for an EBS volume type. A custom definition overrides a
   * built-in definition.
   *
   * @param volumeType ebs volume type
   * @return metadata associated with the volume type
   * @throws NullPointerException  if no metadata could be found for a volume type
   * @throws IllegalStateException if the metadata for a volume type has invalid format
   */
  @Override
  public EbsVolumeMetadata apply(String volumeType) {
    String strCapacityRange = ebsMetadataResolver.getProperty(volumeType);
    Objects.requireNonNull(strCapacityRange, String.format("Could not get metadata for volume type %s", volumeType));

    Range capacityRange = Range.resolveRange(volumeType, strCapacityRange);
    int minSizeGiB = capacityRange.getMin();
    int maxSizeGiB = capacityRange.getMax();

    if (volumeType.equals("io1")) {
      String key = "io1-iops";
      String strIopsMetadata = ebsMetadataResolver.getProperty(key);
      Objects.requireNonNull(strIopsMetadata, String.format("Could not get metadata for %s", key));

      Range iopsRange = Range.resolveRange(key, strIopsMetadata);
      return new EbsVolumeMetadata(volumeType, minSizeGiB, maxSizeGiB, iopsRange.getMin(), iopsRange.getMax());
    }

    return new EbsVolumeMetadata(volumeType, minSizeGiB, maxSizeGiB);
  }

  private static class Range {
    private int min;
    private int max;

    private Range(int min, int max) {
      this.min = min;
      this.max = max;
    }

    public int getMin() {
      return min;
    }

    public int getMax() {
      return max;
    }

    static Range resolveRange(String key, String rangeStr) {

      // range string is expected to be in the form "minValue-maxValue"

      String[] parts = rangeStr.trim().split("-");
      checkState(parts.length == 2, String.format("invalid format for %s=%s, expecting " +
          "exactly 2 parts for the value", key, rangeStr));

      int min, max;
      try {
        min = Integer.parseInt(parts[0]);
      } catch (NumberFormatException ex) {
        throw new IllegalStateException(
            String.format("invalid format for %s=%s, %s should be an integer", key, rangeStr, parts[0])
        );
      }

      try {
        max = Integer.parseInt(parts[1]);
      } catch (NumberFormatException ex) {
        throw new IllegalStateException(
            String.format("invalid format for %s=%s, %s should be an integer", key, rangeStr, parts[1])
        );
      }

      checkState(min > 0, String.format("invalid format for %s=%s, %d should " +
          "be a positive integer", key, rangeStr, min));

      checkState(max > 0, String.format("invalid format for %s=%s, %d should " +
          "be a positive integer", key, rangeStr, max));

      checkState(max >= min, String.format("invalid format for %s=%s, maximum size %d should " +
          "be greater than or equal to minimum size %d", key, rangeStr, max, min));

      return new Range(min, max);
    }
  }


  /**
   * Gets an instance of this class that uses only the given ebs metadata.
   *
   * @param metadata            map of ebs volume type to it's associated metadata
   * @param localizationContext the localization context
   * @return new ebs metadata object
   */
  public static EBSMetadata getDefaultInstance(
      final Map<String, String> metadata, LocalizationContext localizationContext) {
    PropertyResolver ebsMetadataResolver =
        PropertyResolvers.newMapPropertyResolver(metadata);
    File tempDir = Files.createTempDir();
    tempDir.deleteOnExit();
    EBSMetadataConfigProperties ebsMetadataConfigProperties =
        new EBSMetadataConfigProperties(new SimpleConfiguration(), tempDir, localizationContext);
    return new EBSMetadata(ebsMetadataConfigProperties, ebsMetadataResolver);
  }
} @SuppressWarnings("Guava")
public static class EbsVolumeMetadata {
  private final String type;
  private final int minSizeGiB;
  private final int maxSizeGiB;
  private final Optional<Integer> minIops;
  private final Optional<Integer> maxIops;

  public EbsVolumeMetadata(String type, int minSizeGiB, int maxSizeGiB) {
    this.type = Objects.requireNonNull(type, "type is null");
    this.minSizeGiB = minSizeGiB;
    this.maxSizeGiB = maxSizeGiB;
    this.minIops = Optional.absent();
    this.maxIops = Optional.absent();
  }

  public EbsVolumeMetadata(String type, int minSizeGiB, int maxSizeGiB, int minIops, int maxIops) {
    this.type = Objects.requireNonNull(type, "type is null");
    this.minSizeGiB = minSizeGiB;
    this.maxSizeGiB = maxSizeGiB;
    this.minIops = Optional.of(minIops);
    this.maxIops = Optional.of(maxIops);
  }

  public String getType() {
    return type;
  }

  public int getMinSizeGiB() {
    return minSizeGiB;
  }

  public int getMaxSizeGiB() {
    return maxSizeGiB;
  }

  public Optional<Integer> getMinIops() {
    return minIops;
  }

  public Optional<Integer> getMaxIops() {
    return maxIops;
  }

  @SuppressWarnings("SimplifiableIfStatement")
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    EbsVolumeMetadata that = (EbsVolumeMetadata) o;

    if (minSizeGiB != that.minSizeGiB) return false;
    if (maxSizeGiB != that.maxSizeGiB) return false;
    if (!type.equals(that.type)) return false;
    if (!minIops.equals(that.minIops)) return false;
    return maxIops.equals(that.maxIops);
  }

  @Override
  public int hashCode() {
    int result = type.hashCode();
    result = 31 * result + minSizeGiB;
    result = 31 * result + maxSizeGiB;
    result = 31 * result + minIops.hashCode();
    result = 31 * result + maxIops.hashCode();
    return result;
  }
}
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_IOPS;
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.SPOT_PRICE_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.USER_DATA;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.USER_DATA_UNENCODED;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.USE_SPOT_INSTANCES;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.BOTH_USER_DATA_USED;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.HVM_VIRTUALIZATION;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.IMAGE_OWNER_ID_BLACKLIST_KEY;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.IMAGE_PLATFORM_BLACKLIST_KEY;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.IMAGE_SPOT_OWNER_ID_BLACKLIST_KEY;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.IMAGE_SPOT_PLATFORM_BLACKLIST_KEY;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_AMI_ARCHITECTURE_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_AMI_INSTANCE_TYPE_COMPATIBILITY_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_AMI_NAME_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_AMI_OWNER_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_AMI_OWNER_SPOT_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_AMI_PLATFORM_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_AMI_PLATFORM_SPOT_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_AMI_ROOT_DEVICE_TYPE_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_EBS_ENCRYPTION_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_EBS_VOLUME_COUNT_FORMAT_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_EBS_VOLUME_COUNT_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_EBS_VOLUME_SIZE_FORMAT_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_IAM_PROFILE_NAME_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_IOPS_FORMAT_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.INVALID_KMS_NOT_FOUND_MESSAGE;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.IOPS_NOT_IN_RANGE_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.IOPS_NOT_PERMITTED_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.IOPS_REQUIRED_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.MAX_VOLUMES_PER_INSTANCE;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.MIN_ROOT_VOLUME_SIZE_GB;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.NO_SPOT_WITH_ASG_MSG;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.PARAVIRTUAL_VIRTUALIZATION;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.ROOT_VOLUME_TYPES;
import static com.cloudera.director.aws.ec2.provider.EC2InstanceTemplateConfigurationValidator.VOLUME_SIZE_NOT_IN_RANGE_MSG;
import static com.cloudera.director.spi.v2.compute.ComputeInstanceTemplate.ComputeInstanceTemplateConfigurationPropertyToken.AUTOMATIC;
import static java.time.LocalDateTime.now;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.cloudera.director.aws.AWSFilters;
import com.cloudera.director.aws.ec2.VirtualizationMappings;
import com.cloudera.director.aws.ec2.ebs.EBSMetadata;
import com.cloudera.director.aws.ec2.ebs.EBSMetadata.EbsVolumeMetadata;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.AmazonEC2Client;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.AvailabilityZone;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribeAvailabilityZonesRequest;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribeImagesRequest;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribeImagesResult;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribeKeyPairsRequest;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribeKeyPairsResult;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribePlacementGroupsRequest;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribePlacementGroupsResult;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribeSecurityGroupsRequest;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribeSecurityGroupsResult;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribeSubnetsRequest;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribeSubnetsResult;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.Filter;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.Image;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.InstanceType;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.KeyPairInfo;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.PlacementGroup;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.SecurityGroup;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.Subnet;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.VolumeType;
import com.cloudera.director.aws.shaded.com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient;
import com.cloudera.director.aws.shaded.com.amazonaws.services.identitymanagement.model.GetInstanceProfileRequest;
import com.cloudera.director.aws.shaded.com.amazonaws.services.identitymanagement.model.GetInstanceProfileResult;
import com.cloudera.director.aws.shaded.com.amazonaws.services.identitymanagement.model.NoSuchEntityException;
import com.cloudera.director.aws.shaded.com.amazonaws.services.kms.AWSKMSClient;
import com.cloudera.director.aws.shaded.com.amazonaws.services.kms.model.DescribeKeyRequest;
import com.cloudera.director.aws.shaded.com.amazonaws.services.kms.model.NotFoundException;
import com.cloudera.director.spi.v2.model.ConfigurationPropertyToken;
import com.cloudera.director.spi.v2.model.Configured;
import com.cloudera.director.spi.v2.model.LocalizationContext;
import com.cloudera.director.spi.v2.model.exception.PluginExceptionCondition;
import com.cloudera.director.spi.v2.model.exception.PluginExceptionConditionAccumulator;
import com.cloudera.director.spi.v2.model.util.DefaultLocalizationContext;
import com.cloudera.director.spi.v2.model.util.SimpleConfiguration;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Matchers;
    checkImage(IMAGE_NAME, TYPE_STRING, false);
    verifySingleError(IMAGE);
  }

  @Test
  public void testCheckImage_DuplicateAMI() {
    DescribeImagesResult diResult = mock(DescribeImagesResult.class);
    Image image = mock(Image.class);
    when(ec2Client.describeImages(any(DescribeImagesRequest.class))).thenReturn(diResult);
    when(diResult.getImages()).thenReturn(Lists.newArrayList(image, image));

    checkImage(IMAGE_NAME, TYPE_STRING, false);
    verifySingleError(IMAGE);
  }

  @Test
  public void testCheckImage_Not64Bit() {
    Image image = mockSingleCheckedImage();
    mockImageAttributes(image, "x86", null, "available", "paravirtual", "ebs", null);
    when(virtualizationMappings.apply(PARAVIRTUAL_VIRTUALIZATION))
        .thenReturn(ImmutableList.of(TYPE_STRING));

    checkImage(IMAGE_NAME, TYPE_STRING, false);
    verifySingleError(IMAGE, INVALID_AMI_ARCHITECTURE_MSG, IMAGE_NAME, "x86");
  }

  @Test
  public void testCheckImage_Windows() {
    Image image = mockSingleCheckedImage();
    mockImageAttributes(image, "x86_64", "Windows", "available", "paravirtual", "ebs", null);
    when(virtualizationMappings.apply(PARAVIRTUAL_VIRTUALIZATION))
        .thenReturn(ImmutableList.of(TYPE_STRING));

    checkImage(IMAGE_NAME, TYPE_STRING, false);
    verifySingleError(IMAGE, INVALID_AMI_PLATFORM_MSG, IMAGE_NAME, "Windows", "Windows");
  }

  @Test
  public void testCheckImage_InvalidPlatform() {
    Image image = mockSingleCheckedImage();
    mockImageAttributes(image, "x86_64", INVALID_PLATFORM, "available", "paravirtual", "ebs", null);
    when(virtualizationMappings.apply(PARAVIRTUAL_VIRTUALIZATION))
        .thenReturn(ImmutableList.of(TYPE_STRING));

    checkImage(IMAGE_NAME, TYPE_STRING, false);
    verifySingleError(IMAGE, INVALID_AMI_PLATFORM_MSG, IMAGE_NAME, INVALID_PLATFORM,
        INVALID_PLATFORM);
  }

  @Test
  public void testCheckImage_InvalidSpotPlatform() {
    Image image = mockSingleCheckedImage();
    mockImageAttributes(image, "x86_64", INVALID_SPOT_PLATFORM, "available", "paravirtual", "ebs", null);
    when(virtualizationMappings.apply(PARAVIRTUAL_VIRTUALIZATION))
        .thenReturn(ImmutableList.of(TYPE_STRING));

    checkImage(IMAGE_NAME, TYPE_STRING, true);
    verifySingleError(IMAGE, INVALID_AMI_PLATFORM_SPOT_MSG, IMAGE_NAME, INVALID_SPOT_PLATFORM,
        INVALID_SPOT_PLATFORM);
  }

  @Test
  public void testCheckImage_InvalidOwner() {
    Image image = mockSingleCheckedImage();
    mockImageAttributes(image, "x86_64", null, "available", "paravirtual", "ebs", INVALID_OWNER_ID);
    when(virtualizationMappings.apply(PARAVIRTUAL_VIRTUALIZATION))
        .thenReturn(ImmutableList.of(TYPE_STRING));

    checkImage(IMAGE_NAME, TYPE_STRING, false);
    verifySingleError(IMAGE, INVALID_AMI_OWNER_MSG, IMAGE_NAME, INVALID_OWNER_ID,
        INVALID_OWNER_ID);
  }

  @Test
  public void testCheckImage_InvalidSpotOwner() {
    Image image = mockSingleCheckedImage();
    mockImageAttributes(
        image, "x86_64", null, "available", "paravirtual", "ebs", INVALID_SPOT_OWNER_ID);
    when(virtualizationMappings.apply(PARAVIRTUAL_VIRTUALIZATION))
        .thenReturn(ImmutableList.of(TYPE_STRING));

    checkImage(IMAGE_NAME, TYPE_STRING, true);
    verifySingleError(
        IMAGE, INVALID_AMI_OWNER_SPOT_MSG, IMAGE_NAME, INVALID_SPOT_OWNER_ID,
        INVALID_SPOT_OWNER_ID);
  }

  @Test
  public void testCheckImage_InvalidState() {
    Image image = mockSingleCheckedImage();
    mockImageAttributes(image, "x86_64", null, "newjersey", "paravirtual", "ebs", null);
    when(virtualizationMappings.apply(PARAVIRTUAL_VIRTUALIZATION))
        .thenReturn(ImmutableList.of(TYPE_STRING));

    checkImage(IMAGE_NAME, TYPE_STRING, false);
    verifySingleError(IMAGE);
  }

  @Test
  public void testCheckImage_IncompatibleInstanceType() {
    Image image = mockSingleCheckedImage();
    mockImageAttributes(image, "x86_64", null, "available", "hvm", "ebs", null);
    when(virtualizationMappings.apply(HVM_VIRTUALIZATION))
        .thenReturn(ImmutableList.of(TYPE_STRING + "x"));
    when(virtualizationMappings.apply(PARAVIRTUAL_VIRTUALIZATION))
        .thenReturn(ImmutableList.of(TYPE_STRING));

    checkImage(IMAGE_NAME, TYPE_STRING, false);
    verifySingleError(IMAGE, INVALID_AMI_INSTANCE_TYPE_COMPATIBILITY_MSG, TYPE_STRING, "hvm",
        IMAGE_NAME);
  }

  @Test
  public void testCheckImage_RootDeviceType() {
    Image image = mockSingleCheckedImage();
    mockImageAttributes(image, "x86_64", null, "available", "paravirtual", "instance-store", null);
    when(virtualizationMappings.apply(PARAVIRTUAL_VIRTUALIZATION))
        .thenReturn(ImmutableList.of(TYPE_STRING));

    checkImage(IMAGE_NAME, TYPE_STRING, false);
    verifySingleError(IMAGE, INVALID_AMI_ROOT_DEVICE_TYPE_MSG, IMAGE_NAME, "instance-store");