@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);
  }
} @SuppressWarnings("Guava")
public class AWSTimeouts {

  private final ImmutableMap<String, Long> timeouts;

  /**
   * Creates a new timeouts object from the given configuration. It is
   * expected that every value in the recursive tree of the configuration is
   * numeric. Each (recursive) key in the config serves as the key for a
   * timeout.
   *
   * @param config config holding timeouts
   * @throws IllegalArgumentException if any configuration value is non-numeric
   */
  public AWSTimeouts(Config config) {
    ImmutableMap.Builder<String, Long> b = ImmutableMap.builder();

    if (config != null) {
      for (Map.Entry<String, ConfigValue> e : config.entrySet()) {
        String key = e.getKey();
        ConfigValue value = e.getValue();
        switch (value.valueType()) {
          case NUMBER:
            long num = ((Number) value.unwrapped()).longValue();
            if (num <= 0L) {
              throw new IllegalArgumentException("Timeout " + key + " is negative: " + value);
            }
            b.put(key, num);
            break;
          default:
            throw new IllegalArgumentException("Timeout " + key + " is not a number: " + value);
        }
      }
    }

    timeouts = b.build();
  }

  /**
   * Gets a timeout value.
   *
   * @param key timeout key
   * @return timeout value as an Optional
   */
  public Optional<Long> getTimeout(String key) {
    return Optional.fromNullable(timeouts.get(key));
  }
} public class CustomTagMappings {

  private final ImmutableMap<String, String> customTagNames;

  /**
   * Creates a new custom tag mappings object from the given configuration. The keys of the
   * configuration are well-known tag names, and the values are the custom tag names.
   *
   * @param config config holding custom tags
   * @throws IllegalArgumentException if any configuration value is not a string or is empty
   */
  public CustomTagMappings(Config config) {
    ImmutableMap.Builder<String, String> b = ImmutableMap.builder();

    if (config != null) {
      for (Map.Entry<String, ConfigValue> e : config.entrySet()) {
        String key = e.getKey();
        ConfigValue value = e.getValue();
        switch (value.valueType()) {
          case STRING:
            String customTagName = (String) value.unwrapped();
            if (customTagName.isEmpty()) {
              throw new IllegalArgumentException("Tag mapping " + key + " is empty");
            }
            b.put(key, customTagName);
            break;
          default:
            throw new IllegalArgumentException("Tag mapping " + key + " is not a string: " + value);
        }
      }
    }

    customTagNames = b.build();
  }

  /**
   * Returns the custom tag name for the specified tag key if present, otherwise the key itself.
   *
   * @param tagKey tag key
   * @return the custom tag name for the specified tag key if present, otherwise the key itself
   */
  public String getCustomTagName(String tagKey) {
    return customTagNames.getOrDefault(tagKey, tagKey);
  }

  /**
   * Returns the name of the tag used to record the virtual instance ID as generated by Cloudera
   * Director.
   *
   * @return the name of the tag used to record the virtual instance ID as generated by Cloudera
   * Director
   */
  public String getClouderaDirectorIdTagName() {
    return getCustomTagName(Tags.ResourceTags.CLOUDERA_DIRECTOR_ID.getTagKey());
  }
} public final class Tags {

  /**
   * Director tags for cloud resources.
   */
  public enum ResourceTags {
    /**
     * The name of the resource.
     */
    RESOURCE_NAME("Name"),

    /**
     * The virtual instance ID as generated by Cloudera Altus Director.
     */
    CLOUDERA_DIRECTOR_ID("Cloudera-Director-Id"),

    /**
     * The virtual instance ID as generated by Cloudera Altus Director.
     */
    CLOUDERA_DIRECTOR_TEMPLATE_NAME("Cloudera-Director-Template-Name");

    private final String tagName;

    ResourceTags(String tagName) {
      this.tagName = tagName;
    }

    /**
     * Gets the tag's key.
     * Note that you should always call this method to get the tag's key, and not the
     * {@link ResourceTags#valueOf(String)} method.
     *
     * @return tag's key name
     */
    public String getTagKey() {
      return tagName;
    }
  }

  /**
   * Director tags specifically for cloud instances.
   */
  public enum InstanceTags {
    /**
     * The instance owner.
     */
    OWNER("owner");

    private final String tagName;

    InstanceTags(String tagName) {
      this.tagName = tagName;
    }

    /**
     * Gets the tag's key.
     * Note that you should always call this method to get the tag's key, and not the
     * {@link InstanceTags#valueOf(String)} method.
     *
     * @return tag's key name
     */
    public String getTagKey() {
      return tagName;
    }
  }


  /**
   * Private constructor to prevent instantiation.
   */
  private Tags() {
  }
} public interface ClientProvider<T> {

  /**
   * Returns a configured client.
   *
   * @param configuration               the provider configuration
   * @param accumulator                 the exception accumulator
   * @param providerLocalizationContext the resource provider localization context
   * @param verify                      whether to verify the configuration by making an API call
   * @return the configured client
   */
  T getClient(
      Configured configuration,
      PluginExceptionConditionAccumulator accumulator,
      LocalizationContext providerLocalizationContext,
      boolean verify);
} @SuppressWarnings({"PMD.TooManyStaticImports", "Guava"})
public class EC2InstanceTemplate extends ComputeInstanceTemplate {

  /**
   * A base 64 encoder for user data.
   */
  private static final BaseEncoding BASE64 = BaseEncoding.base64();

  /**
   * A splitter for comma-separated lists.
   */
  public static final Splitter CSV_SPLITTER = Splitter.on(",").trimResults().omitEmptyStrings();

  /**
   * The list of configuration properties (including inherited properties).
   */
  private static final List<ConfigurationProperty> CONFIGURATION_PROPERTIES =
      ConfigurationPropertiesUtil.merge(
          ComputeInstanceTemplate.getConfigurationProperties(),
          ConfigurationPropertiesUtil.asConfigurationPropertyList(EC2InstanceTemplateConfigurationPropertyToken.values())
      );

  /**
   * Returns the list of configuration properties for creating an EC2 instance template,
   * including inherited properties.
   *
   * @return the list of configuration properties for creating an EC2 instance template,
   * including inherited properties
   */
  public static List<ConfigurationProperty> getConfigurationProperties() {
    return CONFIGURATION_PROPERTIES;
  }

  /**
   * EC2 compute instance configuration properties.
   */
  // Fully qualifying class name due to compiler bug
  public enum EC2InstanceTemplateConfigurationPropertyToken
      implements com.cloudera.director.spi.v2.model.ConfigurationPropertyToken {

    /**
     * Whether instances created from this template are part of an EC2 Auto Scaling group (ASG).
     *
     * @see <a href="http://docs.aws.amazon.com/autoscaling/ec2/userguide/AutoScalingGroup.html">Auto Scaling Groups</a>
     */
    USE_ASG(new SimpleConfigurationPropertyBuilder()
        .configKey(ComputeInstanceTemplateConfigurationPropertyToken.AUTOMATIC.unwrap().getConfigKey())
        .name("Use Auto Scaling Group (ASG)")
        .required(false)
        .widget(ConfigurationProperty.Widget.CHECKBOX)
        .defaultDescription(
            "Whether to use EC2 Auto Scaling groups.<br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/autoscaling/ec2/userguide/AutoScalingGroup.html'>More Information</a>"
        )
        .build()),

    /**
     * Whether to allow AWS Auto Scaling to automatically perform certain operations on Auto Scaling
     * instances, such as replacing those that fail health checks or rebalancing availability zones.
     *
     * @see <a href="http://docs.aws.amazon.com/autoscaling/ec2/userguide/as-suspend-resume-processes.html">Suspending and Resuming Scaling Processes</a>
     */
    AUTOMATIC_INSTANCE_PROCESSING(new SimpleConfigurationPropertyBuilder()
        .configKey("enableAutomaticInstanceProcesing")
        .name("Enable automatic instance processing")
        .defaultValue("false")
        .type(Property.Type.BOOLEAN)
        .defaultDescription(
            "Whether to allow AWS Auto Scaling to automatically perform certain operations on " +
                "Auto Scaling instances, such as replacing those that fail health checks or " +
                "rebalancing availability zones.<br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/autoscaling/ec2/userguide/as-suspend-resume-processes.html'>More Information</a>"
        ).widget(ConfigurationProperty.Widget.CHECKBOX)
        .hidden(true)
        .build()),

    /**
     * <p>The availability zone.</p>
     * <p>Multiple availability zones are linked together by high speed low latency connections.
     * Each zone is a distinct failure domain.</p>
     *
     * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html">Regions and Availability Zones</a>
     */
    AVAILABILITY_ZONE(new SimpleConfigurationPropertyBuilder()
        .configKey("availabilityZone")
        .name("Availability zone")
        .widget(ConfigurationProperty.Widget.OPENLIST)
        .defaultDescription("The availability zone.")
        .hidden(true)
        .build()),

    /**
     * Whether to enable EBS Optimization.
     *
     * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSOptimized.html">EBS–Optimized Instances</a>
     */
    EBS_OPTIMIZED(new SimpleConfigurationPropertyBuilder()
        .configKey("ebsOptimized")
        .name("EBS Optimized")
        .type(Property.Type.BOOLEAN)
        .defaultValue("false")
        .widget(ConfigurationProperty.Widget.CHECKBOX)
        .defaultDescription(
            "Specify whether to enable EBS Optimized I/O. This optimization isn't available with all instance types. " +
                "Some instance types are EBS Optimized by default regardless of this flag. Additional usage charges " +
                "may apply when using an EBS-optimized instance<br/>" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSOptimized.html'>More Information</a>"
        )
        .build()),

    /**
     * Number of additional EBS volumes to mount.
     */
    EBS_VOLUME_COUNT(new SimpleConfigurationPropertyBuilder()
        .configKey("ebsVolumeCount")
        .name("EBS Volume Count")
        .defaultValue("0")
        .defaultDescription(
            "The number of additional EBS volumes to mount. Cloudera Altus Director will create and attach these volumes to the " +
                "provisioned instance. These added volumes will be deleted when the instance is terminated from Director. <br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumes.html'>More Information</a>"
        ).widget(ConfigurationProperty.Widget.NUMBER)
        .type(Property.Type.INTEGER)
        .build()),

    /**
     * The size of the additional EBS volume in GiB.
     */
    EBS_VOLUME_SIZE_GIB(new SimpleConfigurationPropertyBuilder()
        .configKey("ebsVolumeSizeGiB")
        .name("EBS Volume Size (GiB)")
        .defaultValue("500")
        .defaultDescription(
            "The size of the additional EBS volume(s) in GiB."
        ).widget(ConfigurationProperty.Widget.NUMBER)
        .type(Property.Type.INTEGER)
        .build()),

    /**
     * The volume type of the additional EBS volumes.
     */
    EBS_VOLUME_TYPE(new SimpleConfigurationPropertyBuilder()
        .configKey("ebsVolumeType")
        .name("EBS Volume Type")
        .addValidValues("st1", "sc1", "gp2", "io1")
        .defaultValue("st1")
        .defaultDescription(
            "The EBS volume type for the additional EBS volumes. Supported volumes are Throughput Optimized HDD (st1), " +
                "Cold HDD (sc1), General Purpose SSD (gp2) and Provisioned IOPS SSD (io1) <br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html'>More Information</a>"
        ).widget(ConfigurationProperty.Widget.OPENLIST)
        .build()),

    /**
     * The number of I/O operations per second (IOPS) when using io1 volume types.
     */
    EBS_IOPS(new SimpleConfigurationPropertyBuilder()
        .configKey("ebsIops")
        .name("EBS IOPS Count")
        .defaultDescription(
            "Only valid for Provisioned IOPS (io1) SSD volumes. The number of I/O operations per second (IOPS) to " +
                "provision for the volume."
        ).widget(ConfigurationProperty.Widget.NUMBER)
        .type(Property.Type.INTEGER)
        .build()),

    /**
     * Configurations for extra EBS volumes to mount, this allows for heterogeneous volumes.
     * Note that this is a temporary solution and this configuration configuration property
     * may be removed in the future.
     */
    SYSTEM_DISKS(new SimpleConfigurationPropertyBuilder()
        .configKey("systemDisks")
        .name("Configurations for extra EBS volumes")
        .widget(ConfigurationProperty.Widget.OPENMULTI)
        .hidden(true)
        .defaultDescription(
            "Configurations for extra EBS volumes"
        )
        .build()),

    /**
     * Whether to enable ebs encryption of the additional EBS volumes.
     *
     * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html">EBS Encryption</a>
     */
    ENCRYPT_ADDITIONAL_EBS_VOLUMES(new SimpleConfigurationPropertyBuilder()
        .configKey("enableEbsEncryption")
        .name("Enable EBS Encryption")
        .defaultValue("false")
        .type(Property.Type.BOOLEAN)
        .defaultDescription(
            "Whether to enable encryption for the additional EBS volumes. Note that encryption does not apply to the root volume.<br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html'>More Information</a>"
        ).widget(ConfigurationProperty.Widget.CHECKBOX)
        .build()),

    /**
     * The CMK to use for encryption of the additional EBS volumes.
     *
     * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html">EBS Encryption</a>
     */
    EBS_KMS_KEY_ID(new SimpleConfigurationPropertyBuilder()
        .configKey("ebsKmsKeyId")
        .name("EBS KMS Key ID")
        .defaultDescription(
            "The full ARN of the KMS CMK to use when encrypting volumes. If encryption is enabled and this " +
                "is blank, the default CMK will be used. Note that encryption does not apply to the root volume.<br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html'>More Information</a>"
        ).build()),

    /**
     * Whether to allocate EBS volumes separately when using non-default KMS Key ID.
     */
    ALLOCATE_EBS_SEPARATELY(new SimpleConfigurationPropertyBuilder()
        .configKey("allocateEbsSeparately")
        .name("Allocate EBS volumes separately")
        .defaultValue("false")
        .type(Property.Type.BOOLEAN)
        .defaultDescription(
            "Whether to allocate EBS volumes separately when using non-default KMS Key ID. This should" +
                "only be enabled on certain regions (like GovCloud) that don't allow specifying EBS KMS" +
                "Key ID as part of the instance launch request."
        ).widget(ConfigurationProperty.Widget.CHECKBOX)
        .hidden(true)
        .build()),

    /**
     * Name of the IAM instance profile.
     *
     * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html">IAM Roles</a>
     */
    IAM_PROFILE_NAME(new SimpleConfigurationPropertyBuilder()
        .configKey("iamProfileName")
        .name("IAM profile name")
        .defaultDescription(
            "The IAM instance profile name.<br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html'>More Information</a>"
        ).build()),

    /**
     * The image (AMI) ID.
     */
    IMAGE(new SimpleConfigurationPropertyBuilder()
        .configKey(ComputeInstanceTemplateConfigurationPropertyToken.IMAGE.unwrap().getConfigKey())
        .name("Image (AMI) ID")
        .required(true)
        .widget(ConfigurationProperty.Widget.OPENLIST)
        .defaultDescription(
            "Must begin with 'ami-'.<br/><br/>" +
                "To learn more about which AMIs should be used, please see the " +
                "<a target='_blank' href='http://www.cloudera.com/content/cloudera/en/documentation/cloudera-director/latest/topics/director_deployment_requirements.html'>Cloudera Altus Director Deployment Requirements Guide</a> " +
                "about what operating systems when deploying a Cloudera Manager or CDH Cluster.<br/><br/>" +
                "See the AWS documentation on " +
                "<a target='_blank' href='https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html'>Amazon Machine Images</a> " +
                "for more information."
        ).defaultPlaceholder("Enter or select an image")
        .defaultErrorMessage("Image (AMI) ID is mandatory")
        .build()),

    /**
     * Name of the key pair to use for new instances.
     *
     * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html">EC2 Key Pairs</a>
     */
    KEY_NAME(new SimpleConfigurationPropertyBuilder()
        .configKey("keyName")
        .name("Key name")
        .required(false)
        .widget(ConfigurationProperty.Widget.TEXT)
        .defaultDescription(
            "The EC2 key pair ID.<br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html'>More Information</a>"
        ).hidden(true)
        .build()),

    /**
     * The placement group name for new instances.
     *
     * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html">Placement Groups</a>
     */
    PLACEMENT_GROUP(new SimpleConfigurationPropertyBuilder()
        .configKey("placementGroup")
        .name("Placement group")
        .defaultDescription("The placement group name should be defined here. " +
            "This setting allows users to launch the desired instance in a defined placement group. " +
            "Instances using placement groups will deploy in a specific zone within a region which helps network performance. <br/>" +
            "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html'>More information</a>")
        .build()),

    /**
     * Size of the root partition in GBs.
     */
    ROOT_VOLUME_SIZE_GB(new SimpleConfigurationPropertyBuilder()
        .configKey("rootVolumeSizeGB")
        .name("Root volume size (GB)")
        .defaultValue("50")
        .defaultDescription(
            "Specify a size for the root volume (in GB). " +
                "Cloudera Altus Director will automatically expand the filesystem so that you can use all " +
                "the available disk space for your application.<br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/storage_expand_partition.html'>More Information</a>"
        ).widget(ConfigurationProperty.Widget.NUMBER)
        .type(Property.Type.INTEGER)
        .build()),

    /**
     * The type of EBS volume to use for the root drive.
     *
     * @see <a href="http://aws.amazon.com/ebs/details/">EBS</a>
     */
    ROOT_VOLUME_TYPE(new SimpleConfigurationPropertyBuilder()
        .configKey("rootVolumeType")
        .name("Root volume type")
        .defaultValue("gp2")
        .widget(ConfigurationProperty.Widget.OPENLIST)
        .defaultDescription(
            "Specify the type of the EBS volume used for the root partition. The choices for AWS are gp2 and standard. Defaults to gp2.<br/>" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html'>More Information</a>"
        ).addValidValues(
            "gp2",
            "standard")
        .build()),

    /**
     * The IDs of the VPC security groups (comma separated).
     *
     * @see <a href="http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_SecurityGroups.html">Security Groups</a>
     */
    SECURITY_GROUP_IDS(new SimpleConfigurationPropertyBuilder()
        .configKey("securityGroupsIds")
        .name("Security group IDs")
        .widget(ConfigurationProperty.Widget.OPENMULTI)
        .required(true)
        .defaultDescription(
            "Specify the list of security group IDs. Must begin with 'sg-'.<br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_SecurityGroups.html'>More Information</a>"
        ).defaultErrorMessage("VPC security group IDs are mandatory")
        .build()),

    /**
     * Whether to use Spot Instances. Default is <code>false</code>.
     *
     * @see <a href="http://aws.amazon.com/ec2/spot/">Spot Instances</a>
     */
    USE_SPOT_INSTANCES(new SimpleConfigurationPropertyBuilder()
        .configKey("useSpotInstances")
        .name("Use Spot Instances")
        .widget(ConfigurationProperty.Widget.CHECKBOX)
        .defaultValue("false")
        .type(Property.Type.BOOLEAN)
        .defaultDescription("Whether to use Spot Instances. " +
            "Since Spot Instances can be terminated unexpectedly if the EC2 Spot price increases, " +
            "they should be used only for workers, and not for nodes that must be reliable, " +
            "such as masters and data nodes.<br />" +
            "<a target='_blank' href='http://aws.amazon.com/ec2/spot/'>More Information</a>")
        .build()),

    /**
     * Spot bid in USD/hr.
     *
     * @deprecated Use {@link #SPOT_PRICE_USD_PER_HR} instead.
     */
    SPOT_BID_USD_PER_HR(new SimpleConfigurationPropertyBuilder()
        .configKey("spotBidUSDPerHr")
        .name("Spot bid (USD/hr)")
        .defaultDescription(
            "Specify a Spot bid (in USD/hr). " +
                "This property is deprecated in favor of spotPriceUSDPerHr, but still works as " +
                "an alternative name for spotPriceUSDPerHr. This property will be removed in a " +
                "future plugin release."
        ).widget(ConfigurationProperty.Widget.NUMBER)
        .type(Property.Type.DOUBLE)
        .hidden(true)
        .build()),

    /**
     * Spot price in USD/hr.
     */
    SPOT_PRICE_USD_PER_HR(new SimpleConfigurationPropertyBuilder()
        .configKey("spotPriceUSDPerHr")
        .name("Spot price (USD/hr)")
        .defaultDescription(
            "Specify a Spot price (in USD/hr). " +
                "The Spot price is the maximum amount you are willing to pay per hour for each Spot Instance. " +
                "When your Spot price exceeds the Spot price set by EC2, then EC2 fulfills your request, " +
                "if capacity is available. Spot Instances will be terminated if the Spot price set by EC2 " +
                "rises above your Spot price."
        ).widget(ConfigurationProperty.Widget.NUMBER)
        .type(Property.Type.DOUBLE)
        .build()),

    /**
     * The required duration for the Spot instances.
     *
     * @see <a href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html#fixed-duration-spot-instances'>Fixed Duration Spot Instances</a>
     */
    BLOCK_DURATION_MINUTES(new SimpleConfigurationPropertyBuilder()
        .configKey("blockDurationMinutes")
        .name("Spot Block Duration (minutes)")
        .defaultDescription(
            "Specify the required duration for the Spot instances (also known as Spot blocks), " +
                "in minutes. This value must be a multiple of 60 (60, 120, 180, 240, 300, or 360). " +
                "Spot block instances will run for the predefined duration - in hourly increments " +
                "up to six hours in length - at a significant discount compared to On-Demand " +
                "pricing.<br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html#fixed-duration-spot-instances'>More Information</a>"
        ).widget(ConfigurationProperty.Widget.LIST)
        .type(Property.Type.INTEGER)
        .addValidValues("60", "120", "180", "240", "300", "360")
        .build()),

    /**
     * The ID of the Amazon VPC subnet.
     *
     * @see <a href="http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html">Subnets</a>
     */
    SUBNET_ID(new SimpleConfigurationPropertyBuilder()
        .configKey("subnetId")
        .name("VPC subnet ID")
        .required(true)
        .defaultDescription(
            "The VPC subnet ID.<br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html'>More Information</a>"
        ).defaultErrorMessage("VPC subnet ID is mandatory")
        .build()),

    /**
     * <p>The tenancy.</p>
     * <p>The tenancy of the instance (if the instance is running in a VPC).
     * An instance with dedicated tenancy runs on single-tenant hardware.</p>
     *
     * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/how-dedicated-hosts-work.html">Using Dedicated Hosts</a>
     */
    TENANCY(new SimpleConfigurationPropertyBuilder()
        .configKey("tenancy")
        .name("Tenancy")
        .defaultValue("default")
        .widget(ConfigurationProperty.Widget.LIST)
        .defaultDescription(
            "Instance tenancy should be defined here. This setting allows users to launch the desired instance in VPC with a defined tenancy. <br/>" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/dedicated-instance.html'>More information</a>"
        ).addValidValues(
            "default",
            "dedicated"
        )
        .build()),

    /**
     * The instance type (e.g. t1.micro, m1.medium, etc.).
     */
    TYPE(new SimpleConfigurationPropertyBuilder()
        .configKey(ComputeInstanceTemplateConfigurationPropertyToken.TYPE.unwrap().getConfigKey())
        .name("Instance type")
        .required(true)
        .widget(ConfigurationProperty.Widget.OPENLIST)
        .defaultDescription(
            "Select an instance type as per the needs of the processes to be run on this instance.<br />" +
                "<a target='_blank' href='http://aws.amazon.com/ec2/instance-types/'>More Information</a>"
        ).defaultPlaceholder("Select an instance type")
        .defaultErrorMessage("Instance type is mandatory")
        .addValidValues(
            "t2.micro",
            "t2.small",
            "t2.medium",
            "t2.large",
            "t2.xlarge",
            "t2.2xlarge",
            "t3.micro",
            "t3.small",
            "t3.medium",
            "t3.large",
            "t3.xlarge",
            "t3.2xlarge",
            "m3.medium",
            "m3.large",
            "m3.xlarge",
            "m3.2xlarge",
            "m4.large",
            "m4.xlarge",
            "m4.2xlarge",
            "m4.4xlarge",
            "m4.10xlarge",
            "m4.16xlarge",
            "m5.large",
            "m5.xlarge",
            "m5.2xlarge",
            "m5.4xlarge",
            "m5.12xlarge",
            "m5.24xlarge",
            "c3.large",
            "c3.xlarge",
            "c3.2xlarge",
            "c3.4xlarge",
            "c3.8xlarge",
            "c4.large",
            "c4.xlarge",
            "c4.2xlarge",
            "c4.4xlarge",
            "c4.8xlarge",
            "c5.large",
            "c5.xlarge",
            "c5.2xlarge",
            "c5.4xlarge",
            "c5.9xlarge",
            "c5.18xlarge",
            "g2.2xlarge",
            "g2.8xlarge",
            "g3.4xlarge",
            "g3.8xlarge",
            "g3.16xlarge",
            "p2.xlarge",
            "p2.8xlarge",
            "p2.16xlarge",
            "p3.2xlarge",
            "p3.8xlarge",
            "p3.16xlarge",
            "r3.large",
            "r3.xlarge",
            "r3.2xlarge",
            "r3.4xlarge",
            "r3.8xlarge",
            "r4.large",
            "r4.xlarge",
            "r4.2xlarge",
            "r4.4xlarge",
            "r4.8xlarge",
            "r4.16xlarge",
            "r5.large",
            "r5.xlarge",
            "r5.2xlarge",
            "r5.4xlarge",
            "r5.12xlarge",
            "r5.24xlarge",
            "r5d.large",
            "r5d.xlarge",
            "r5d.2xlarge",
            "r5d.4xlarge",
            "r5d.12xlarge",
            "r5d.24xlarge",
            "z1d.large",
            "z1d.xlarge",
            "z1d.2xlarge",
            "z1d.3xlarge",
            "z1d.6xlarge",
            "z1d.12xlarge",
            "x1.16xlarge",
            "x1.32xlarge",
            "i2.xlarge",
            "i2.2xlarge",
            "i2.4xlarge",
            "i2.8xlarge",
            "i3.large",
            "i3.xlarge",
            "i3.2xlarge",
            "i3.4xlarge",
            "i3.8xlarge",
            "i3.16xlarge",
            "h1.2xlarge",
            "h1.4xlarge",
            "h1.8xlarge",
            "h1.16xlarge",
            "hs1.8xlarge",
            "d2.xlarge",
            "d2.2xlarge",
            "d2.4xlarge",
            "d2.8xlarge",
            "f1.2xlarge",
            "f1.16xlarge")
        .build()),

    /**
     * The user data, base64 encoded. Mutually exclusive with {@link #USER_DATA_UNENCODED}.
     *
     * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html">Instance Metadata and User Data</a>
     */
    USER_DATA(new SimpleConfigurationPropertyBuilder()
        .configKey("userData")
        .name("User data")
        .required(false)
        .widget(ConfigurationProperty.Widget.TEXT)
        .defaultDescription(
            "The user data, base64 encoded.<br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html'>More Information</a>"
        )
        .sensitive(true)
        .build()),

    /**
     * The user data, unencoded. Mutually exclusive with {@link #USER_DATA}.
     *
     * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html">Instance Metadata and User Data</a>
     */
    USER_DATA_UNENCODED(new SimpleConfigurationPropertyBuilder()
        .configKey("userDataUnencoded")
        .name("User data (unencoded)")
        .required(false)
        .widget(ConfigurationProperty.Widget.TEXTAREA)
        .defaultDescription(
            "The user data, unencoded.<br />" +
                "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html'>More Information</a>"
        )
        .sensitive(true)
        .build());

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

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

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

  /**
   * The instance type.
   */
  private final String type;

  /**
   * The image (AMI) ID.
   */
  private final String image;

  /**
   * The availability zone.
   */
  private final Optional<String> availabilityZone;

  /**
   * The placement group.
   */
  private final Optional<String> placementGroup;

  /**
   * The tenancy.
   */
  private final String tenancy;

  /**
   * The subnet ID.
   */
  private final String subnetId;

  /**
   * The security group IDs.
   */
  private final List<String> securityGroupIds;

  /**
   * The root volume size (in GB).
   */
  private final int rootVolumeSizeGB;

  /**
   * The root volume type.
   */
  private final String rootVolumeType;

  /**
   * Whether to enable EBS Optimization.
   */
  private final boolean ebsOptimized;

  /**
   * Number of additional EBS volumes to mount.
   */
  private final int ebsVolumeCount;

  /**
   * The size of the additional EBS volume in GiB.
   */
  private final int ebsVolumeSizeGiB;

  /**
   * The volume type of the additional EBS volumes.
   */
  private final String ebsVolumeType;

  /**
   * The number of I/O operations per second (IOPS) when using io1 volume types.
   */
  private final Optional<Integer> ebsIops;

  /**
   * Configuration for extra EBS volumes.
   */
  private final List<SystemDisk> systemDisks;

  /**
   * Whether to enable ebs encryption of the additional EBS volumes.
   */
  private final boolean enableEbsEncryption;

  /**
   * The optional CMK to use for encryption of the additional EBS volumes.
   */
  private final Optional<String> ebsKmsKeyId;

  /**
   * Whether to allocate EBS volumes separately when using non-default KMS Key ID.
   */
  private final boolean allocateEbsSeparately;

  /**
   * The optional IAM profile name.
   */
  private final Optional<String> iamProfileName;

  /**
   * The optional EC2 key pair name.
   */
  private final Optional<String> keyName;

  /**
   * Whether to use Spot Instances.
   */
  private final boolean useSpotInstances;

  /**
   * The Spot price, in USD/hr.
   */
  private final Optional<BigDecimal> spotPriceUSDPerHour;

  /**
   * The block duration in minutes for Spot block instances.
   */
  private final Optional<Integer> blockDurationMinutes;

  /**
   * The user data, base64 encoded.
   */
  private final Optional<String> userData;

  /**
   * Whether to allow AWS Auto Scaling to automatically perform certain operations on Auto Scaling
   * instances, such as replacing those that fail health checks or rebalancing availability zones.
   */
  private final boolean enableAutomaticInstanceProcesing;

  /**
   * Creates an EC2 instance template with the specified parameters.
   *
   * @param name                        the name of the template
   * @param configuration               the source of configuration
   * @param tags                        the map of tags to be applied to resources created from the template
   * @param providerLocalizationContext the parent provider localization context
   */
  public EC2InstanceTemplate(String name, Configured configuration, Map<String, String> tags,
      LocalizationContext providerLocalizationContext) {
    super(name, configuration, tags, providerLocalizationContext);
    LocalizationContext localizationContext = getLocalizationContext();
    this.type = getConfigurationValue(TYPE, localizationContext);
    this.image = getConfigurationValue(IMAGE, localizationContext);

    this.subnetId = getConfigurationValue(SUBNET_ID, localizationContext);
    this.securityGroupIds =
        CSV_SPLITTER.splitToList(getConfigurationValue(SECURITY_GROUP_IDS, localizationContext));

    this.availabilityZone =
        Optional.fromNullable(getConfigurationValue(AVAILABILITY_ZONE, localizationContext));
    this.placementGroup =
        Optional.fromNullable(getConfigurationValue(PLACEMENT_GROUP, localizationContext));
    this.tenancy = getConfigurationValue(TENANCY, localizationContext);

    this.rootVolumeSizeGB =
        Integer.parseInt(getConfigurationValue(ROOT_VOLUME_SIZE_GB, localizationContext));
    this.rootVolumeType = getConfigurationValue(ROOT_VOLUME_TYPE, localizationContext);

    this.ebsOptimized = Boolean.parseBoolean(getConfigurationValue(EBS_OPTIMIZED, localizationContext));

    this.ebsVolumeCount = Integer.parseInt(getConfigurationValue(EBS_VOLUME_COUNT, localizationContext));
    this.ebsVolumeSizeGiB = Integer.parseInt(getConfigurationValue(EBS_VOLUME_SIZE_GIB, localizationContext));
    this.ebsVolumeType = getConfigurationValue(EBS_VOLUME_TYPE, localizationContext);

    Optional<String> strEbsIops = Optional.fromNullable(getConfigurationValue(EBS_IOPS, localizationContext));
    this.ebsIops = strEbsIops.isPresent() ?
        Optional.of(Integer.parseInt(strEbsIops.get())) : Optional.absent();

    this.enableEbsEncryption =
        Boolean.parseBoolean(getConfigurationValue(ENCRYPT_ADDITIONAL_EBS_VOLUMES, localizationContext));
    this.ebsKmsKeyId = Optional.fromNullable(getConfigurationValue(EBS_KMS_KEY_ID, localizationContext));

    this.allocateEbsSeparately =
        Boolean.parseBoolean(getConfigurationValue(ALLOCATE_EBS_SEPARATELY, localizationContext));

    this.systemDisks =
        SystemDisk.parse(getConfigurationValue(SYSTEM_DISKS, localizationContext));

    this.iamProfileName =
        Optional.fromNullable(getConfigurationValue(IAM_PROFILE_NAME, localizationContext));
    this.keyName =
        Optional.fromNullable(getConfigurationValue(KEY_NAME, localizationContext));

    boolean useSpotInstances =
        Boolean.parseBoolean(getConfigurationValue(USE_SPOT_INSTANCES, localizationContext));
    this.useSpotInstances = useSpotInstances;
    Optional<String> spotPriceUSDPerHourString =
        Optional.fromNullable(getConfigurationValue(SPOT_PRICE_USD_PER_HR, localizationContext))
            .or(Optional.fromNullable(getConfigurationValue(SPOT_BID_USD_PER_HR, localizationContext)));
    this.spotPriceUSDPerHour = useSpotInstances && spotPriceUSDPerHourString.isPresent()
        ? Optional.of(new BigDecimal(spotPriceUSDPerHourString.get()))
        : Optional.absent();
    String blockDurationMinutesString = Strings.emptyToNull(
        getConfigurationValue(BLOCK_DURATION_MINUTES, localizationContext));
    this.blockDurationMinutes = useSpotInstances && blockDurationMinutesString != null
        ? Optional.of(Integer.parseInt(blockDurationMinutesString))
        : Optional.absent();

    this.userData =
        Optional.fromNullable(getConfigurationValue(USER_DATA, localizationContext))
            .or(Optional.fromNullable(base64Encode(getConfigurationValue(USER_DATA_UNENCODED, localizationContext))));

    this.enableAutomaticInstanceProcesing =
        Boolean.parseBoolean(getConfigurationValue(AUTOMATIC_INSTANCE_PROCESSING, localizationContext));
  }

  private String base64Encode(String s) {
    if (s == null) {
      return null;
    }
    return BASE64.encode(s.getBytes(StandardCharsets.UTF_8));
  }

  /**
   * Returns the instance type.
   *
   * @return the instance type
   */
  public String getType() {
    return type;
  }

  /**
   * Returns the image (AMI) ID.
   *
   * @return the image (AMI) ID
   */
  public String getImage() {
    return image;
  }

  /**
   * Returns the optional availability zone.
   *
   * @return the optional availability zone
   */
  public Optional<String> getAvailabilityZone() {
    return availabilityZone;
  }

  /**
   * Returns the optional placement group.
   *
   * @return the optional placement group
   */
  public Optional<String> getPlacementGroup() {
    return placementGroup;
  }

  /**
   * Returns tenancy.
   *
   * @return tenancy
   */
  public String getTenancy() {
    return tenancy;
  }

  /**
   * Returns the subnet ID.
   *
   * @return the subnet ID
   */
  public String getSubnetId() {
    return subnetId;
  }

  /**
   * Returns the security group IDs.
   *
   * @return the security group IDs
   */
  public List<String> getSecurityGroupIds() {
    return securityGroupIds;
  }

  /**
   * Returns the root volume size (in GB).
   *
   * @return the root volume size (in GB)
   */
  public int getRootVolumeSizeGB() {
    return rootVolumeSizeGB;
  }

  /**
   * Returns the root volume type.
   *
   * @return the root volume type
   */
  public String getRootVolumeType() {
    return rootVolumeType;
  }

  /**
   * Whether to enable EBS Optimization.
   *
   * @return whether to enable EBS Optimization
   */
  public boolean isEbsOptimized() {
    return ebsOptimized;
  }

  /**
   * Returns the EBS volume count.
   *
   * @return the EBS volume count
   */
  public int getEbsVolumeCount() {
    return ebsVolumeCount;
  }

  /**
   * Returns the EBS volume size in GiB.
   *
   * @return the EBS volume size in GiB
   */
  public int getEbsVolumeSizeGiB() {
    return ebsVolumeSizeGiB;
  }

  /**
   * Returns the EBS volume type.
   *
   * @return the EBS volume type.
   */
  public String getEbsVolumeType() {
    return ebsVolumeType;
  }

  /**
   * Returns the number of I/O operations per second (IOPS).
   *
   * @return the number of I/O operations per second (IOPS).
   */
  public Optional<Integer> getEbsIops() {
    return ebsIops;
  }

  /**
   * Returns whether to enable EBS encryption.
   *
   * @return whether to enable EBS encryption.
   */
  public boolean isEnableEbsEncryption() {
    return enableEbsEncryption;
  }

  /**
   * Returns the optional CMK to use for encryption of the additional EBS volumes.
   *
   * @return the optional CMK to use for encryption of the additional EBS volumes.
   */
  public Optional<String> getEbsKmsKeyId() {
    return ebsKmsKeyId;
  }

  /**
   * Returns whether to allocate EBS volumes separately when using non-default KMS Key ID.
   *
   * @return whether to allocate EBS volumes separately when using non-default KMS Key ID
   */
  public boolean isAllocateEbsSeparately() {
    return allocateEbsSeparately;
  }

  /**
   * Returns the system disks.
   *
   * @return the system disk.
   */
  public List<SystemDisk> getSystemDisks() {
    return systemDisks;
  }

  /**
   * Returns the optional IAM profile name.
   *
   * @return the optional IAM profile name
   */
  public Optional<String> getIamProfileName() {
    return iamProfileName;
  }

  /**
   * Returns the optional EC2 key pair name.
   *
   * @return the optional EC2 key pair name
   */
  public Optional<String> getKeyName() {
    return keyName;
  }

  /**
   * Returns whether to use Spot Instances.
   *
   * @return whether to use Spot Instances
   */
  public boolean isUseSpotInstances() {
    return useSpotInstances;
  }

  /**
   * Returns the Spot price, in USD/hr.
   *
   * @return the Spot price, in USD/hr
   */
  public Optional<BigDecimal> getSpotPriceUSDPerHour() {
    return spotPriceUSDPerHour;
  }

  /**
   * Returns the Spot block duration, in minutes.
   *
   * @return the Spot block duration, in minutes
   */
  public Optional<Integer> getBlockDurationMinutes() {
    return blockDurationMinutes;
  }

  /**
   * Returns the user data, base64 encoded.
   *
   * @return the user data, base64 encoded
   */
  public Optional<String> getUserData() {
    return userData;
  }

  /**
   * Returns whether to allow AWS Auto Scaling to automatically perform certain operations on Auto Scaling
   * instances, such as replacing those that fail health checks or rebalancing availability zones.
   *
   * @return whether to allow AWS Auto Scaling to automatically perform certain operations on Auto Scaling
   * instances, such as replacing those that fail health checks or rebalancing availability zones
   */
  public boolean isEnableAutomaticInstanceProcesing() {
    return enableAutomaticInstanceProcesing;
  }
} public enum EC2InstanceTemplateConfigurationPropertyToken
    implements com.cloudera.director.spi.v2.model.ConfigurationPropertyToken {

  /**
   * Whether instances created from this template are part of an EC2 Auto Scaling group (ASG).
   *
   * @see <a href="http://docs.aws.amazon.com/autoscaling/ec2/userguide/AutoScalingGroup.html">Auto Scaling Groups</a>
   */
  USE_ASG(new SimpleConfigurationPropertyBuilder()
      .configKey(ComputeInstanceTemplateConfigurationPropertyToken.AUTOMATIC.unwrap().getConfigKey())
      .name("Use Auto Scaling Group (ASG)")
      .required(false)
      .widget(ConfigurationProperty.Widget.CHECKBOX)
      .defaultDescription(
          "Whether to use EC2 Auto Scaling groups.<br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/autoscaling/ec2/userguide/AutoScalingGroup.html'>More Information</a>"
      )
      .build()),

  /**
   * Whether to allow AWS Auto Scaling to automatically perform certain operations on Auto Scaling
   * instances, such as replacing those that fail health checks or rebalancing availability zones.
   *
   * @see <a href="http://docs.aws.amazon.com/autoscaling/ec2/userguide/as-suspend-resume-processes.html">Suspending and Resuming Scaling Processes</a>
   */
  AUTOMATIC_INSTANCE_PROCESSING(new SimpleConfigurationPropertyBuilder()
      .configKey("enableAutomaticInstanceProcesing")
      .name("Enable automatic instance processing")
      .defaultValue("false")
      .type(Property.Type.BOOLEAN)
      .defaultDescription(
          "Whether to allow AWS Auto Scaling to automatically perform certain operations on " +
              "Auto Scaling instances, such as replacing those that fail health checks or " +
              "rebalancing availability zones.<br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/autoscaling/ec2/userguide/as-suspend-resume-processes.html'>More Information</a>"
      ).widget(ConfigurationProperty.Widget.CHECKBOX)
      .hidden(true)
      .build()),

  /**
   * <p>The availability zone.</p>
   * <p>Multiple availability zones are linked together by high speed low latency connections.
   * Each zone is a distinct failure domain.</p>
   *
   * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html">Regions and Availability Zones</a>
   */
  AVAILABILITY_ZONE(new SimpleConfigurationPropertyBuilder()
      .configKey("availabilityZone")
      .name("Availability zone")
      .widget(ConfigurationProperty.Widget.OPENLIST)
      .defaultDescription("The availability zone.")
      .hidden(true)
      .build()),

  /**
   * Whether to enable EBS Optimization.
   *
   * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSOptimized.html">EBS–Optimized Instances</a>
   */
  EBS_OPTIMIZED(new SimpleConfigurationPropertyBuilder()
      .configKey("ebsOptimized")
      .name("EBS Optimized")
      .type(Property.Type.BOOLEAN)
      .defaultValue("false")
      .widget(ConfigurationProperty.Widget.CHECKBOX)
      .defaultDescription(
          "Specify whether to enable EBS Optimized I/O. This optimization isn't available with all instance types. " +
              "Some instance types are EBS Optimized by default regardless of this flag. Additional usage charges " +
              "may apply when using an EBS-optimized instance<br/>" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSOptimized.html'>More Information</a>"
      )
      .build()),

  /**
   * Number of additional EBS volumes to mount.
   */
  EBS_VOLUME_COUNT(new SimpleConfigurationPropertyBuilder()
      .configKey("ebsVolumeCount")
      .name("EBS Volume Count")
      .defaultValue("0")
      .defaultDescription(
          "The number of additional EBS volumes to mount. Cloudera Altus Director will create and attach these volumes to the " +
              "provisioned instance. These added volumes will be deleted when the instance is terminated from Director. <br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumes.html'>More Information</a>"
      ).widget(ConfigurationProperty.Widget.NUMBER)
      .type(Property.Type.INTEGER)
      .build()),

  /**
   * The size of the additional EBS volume in GiB.
   */
  EBS_VOLUME_SIZE_GIB(new SimpleConfigurationPropertyBuilder()
      .configKey("ebsVolumeSizeGiB")
      .name("EBS Volume Size (GiB)")
      .defaultValue("500")
      .defaultDescription(
          "The size of the additional EBS volume(s) in GiB."
      ).widget(ConfigurationProperty.Widget.NUMBER)
      .type(Property.Type.INTEGER)
      .build()),

  /**
   * The volume type of the additional EBS volumes.
   */
  EBS_VOLUME_TYPE(new SimpleConfigurationPropertyBuilder()
      .configKey("ebsVolumeType")
      .name("EBS Volume Type")
      .addValidValues("st1", "sc1", "gp2", "io1")
      .defaultValue("st1")
      .defaultDescription(
          "The EBS volume type for the additional EBS volumes. Supported volumes are Throughput Optimized HDD (st1), " +
              "Cold HDD (sc1), General Purpose SSD (gp2) and Provisioned IOPS SSD (io1) <br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html'>More Information</a>"
      ).widget(ConfigurationProperty.Widget.OPENLIST)
      .build()),

  /**
   * The number of I/O operations per second (IOPS) when using io1 volume types.
   */
  EBS_IOPS(new SimpleConfigurationPropertyBuilder()
      .configKey("ebsIops")
      .name("EBS IOPS Count")
      .defaultDescription(
          "Only valid for Provisioned IOPS (io1) SSD volumes. The number of I/O operations per second (IOPS) to " +
              "provision for the volume."
      ).widget(ConfigurationProperty.Widget.NUMBER)
      .type(Property.Type.INTEGER)
      .build()),

  /**
   * Configurations for extra EBS volumes to mount, this allows for heterogeneous volumes.
   * Note that this is a temporary solution and this configuration configuration property
   * may be removed in the future.
   */
  SYSTEM_DISKS(new SimpleConfigurationPropertyBuilder()
      .configKey("systemDisks")
      .name("Configurations for extra EBS volumes")
      .widget(ConfigurationProperty.Widget.OPENMULTI)
      .hidden(true)
      .defaultDescription(
          "Configurations for extra EBS volumes"
      )
      .build()),

  /**
   * Whether to enable ebs encryption of the additional EBS volumes.
   *
   * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html">EBS Encryption</a>
   */
  ENCRYPT_ADDITIONAL_EBS_VOLUMES(new SimpleConfigurationPropertyBuilder()
      .configKey("enableEbsEncryption")
      .name("Enable EBS Encryption")
      .defaultValue("false")
      .type(Property.Type.BOOLEAN)
      .defaultDescription(
          "Whether to enable encryption for the additional EBS volumes. Note that encryption does not apply to the root volume.<br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html'>More Information</a>"
      ).widget(ConfigurationProperty.Widget.CHECKBOX)
      .build()),

  /**
   * The CMK to use for encryption of the additional EBS volumes.
   *
   * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html">EBS Encryption</a>
   */
  EBS_KMS_KEY_ID(new SimpleConfigurationPropertyBuilder()
      .configKey("ebsKmsKeyId")
      .name("EBS KMS Key ID")
      .defaultDescription(
          "The full ARN of the KMS CMK to use when encrypting volumes. If encryption is enabled and this " +
              "is blank, the default CMK will be used. Note that encryption does not apply to the root volume.<br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html'>More Information</a>"
      ).build()),

  /**
   * Whether to allocate EBS volumes separately when using non-default KMS Key ID.
   */
  ALLOCATE_EBS_SEPARATELY(new SimpleConfigurationPropertyBuilder()
      .configKey("allocateEbsSeparately")
      .name("Allocate EBS volumes separately")
      .defaultValue("false")
      .type(Property.Type.BOOLEAN)
      .defaultDescription(
          "Whether to allocate EBS volumes separately when using non-default KMS Key ID. This should" +
              "only be enabled on certain regions (like GovCloud) that don't allow specifying EBS KMS" +
              "Key ID as part of the instance launch request."
      ).widget(ConfigurationProperty.Widget.CHECKBOX)
      .hidden(true)
      .build()),

  /**
   * Name of the IAM instance profile.
   *
   * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html">IAM Roles</a>
   */
  IAM_PROFILE_NAME(new SimpleConfigurationPropertyBuilder()
      .configKey("iamProfileName")
      .name("IAM profile name")
      .defaultDescription(
          "The IAM instance profile name.<br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html'>More Information</a>"
      ).build()),

  /**
   * The image (AMI) ID.
   */
  IMAGE(new SimpleConfigurationPropertyBuilder()
      .configKey(ComputeInstanceTemplateConfigurationPropertyToken.IMAGE.unwrap().getConfigKey())
      .name("Image (AMI) ID")
      .required(true)
      .widget(ConfigurationProperty.Widget.OPENLIST)
      .defaultDescription(
          "Must begin with 'ami-'.<br/><br/>" +
              "To learn more about which AMIs should be used, please see the " +
              "<a target='_blank' href='http://www.cloudera.com/content/cloudera/en/documentation/cloudera-director/latest/topics/director_deployment_requirements.html'>Cloudera Altus Director Deployment Requirements Guide</a> " +
              "about what operating systems when deploying a Cloudera Manager or CDH Cluster.<br/><br/>" +
              "See the AWS documentation on " +
              "<a target='_blank' href='https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html'>Amazon Machine Images</a> " +
              "for more information."
      ).defaultPlaceholder("Enter or select an image")
      .defaultErrorMessage("Image (AMI) ID is mandatory")
      .build()),

  /**
   * Name of the key pair to use for new instances.
   *
   * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html">EC2 Key Pairs</a>
   */
  KEY_NAME(new SimpleConfigurationPropertyBuilder()
      .configKey("keyName")
      .name("Key name")
      .required(false)
      .widget(ConfigurationProperty.Widget.TEXT)
      .defaultDescription(
          "The EC2 key pair ID.<br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html'>More Information</a>"
      ).hidden(true)
      .build()),

  /**
   * The placement group name for new instances.
   *
   * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html">Placement Groups</a>
   */
  PLACEMENT_GROUP(new SimpleConfigurationPropertyBuilder()
      .configKey("placementGroup")
      .name("Placement group")
      .defaultDescription("The placement group name should be defined here. " +
          "This setting allows users to launch the desired instance in a defined placement group. " +
          "Instances using placement groups will deploy in a specific zone within a region which helps network performance. <br/>" +
          "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html'>More information</a>")
      .build()),

  /**
   * Size of the root partition in GBs.
   */
  ROOT_VOLUME_SIZE_GB(new SimpleConfigurationPropertyBuilder()
      .configKey("rootVolumeSizeGB")
      .name("Root volume size (GB)")
      .defaultValue("50")
      .defaultDescription(
          "Specify a size for the root volume (in GB). " +
              "Cloudera Altus Director will automatically expand the filesystem so that you can use all " +
              "the available disk space for your application.<br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/storage_expand_partition.html'>More Information</a>"
      ).widget(ConfigurationProperty.Widget.NUMBER)
      .type(Property.Type.INTEGER)
      .build()),

  /**
   * The type of EBS volume to use for the root drive.
   *
   * @see <a href="http://aws.amazon.com/ebs/details/">EBS</a>
   */
  ROOT_VOLUME_TYPE(new SimpleConfigurationPropertyBuilder()
      .configKey("rootVolumeType")
      .name("Root volume type")
      .defaultValue("gp2")
      .widget(ConfigurationProperty.Widget.OPENLIST)
      .defaultDescription(
          "Specify the type of the EBS volume used for the root partition. The choices for AWS are gp2 and standard. Defaults to gp2.<br/>" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html'>More Information</a>"
      ).addValidValues(
          "gp2",
          "standard")
      .build()),

  /**
   * The IDs of the VPC security groups (comma separated).
   *
   * @see <a href="http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_SecurityGroups.html">Security Groups</a>
   */
  SECURITY_GROUP_IDS(new SimpleConfigurationPropertyBuilder()
      .configKey("securityGroupsIds")
      .name("Security group IDs")
      .widget(ConfigurationProperty.Widget.OPENMULTI)
      .required(true)
      .defaultDescription(
          "Specify the list of security group IDs. Must begin with 'sg-'.<br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_SecurityGroups.html'>More Information</a>"
      ).defaultErrorMessage("VPC security group IDs are mandatory")
      .build()),

  /**
   * Whether to use Spot Instances. Default is <code>false</code>.
   *
   * @see <a href="http://aws.amazon.com/ec2/spot/">Spot Instances</a>
   */
  USE_SPOT_INSTANCES(new SimpleConfigurationPropertyBuilder()
      .configKey("useSpotInstances")
      .name("Use Spot Instances")
      .widget(ConfigurationProperty.Widget.CHECKBOX)
      .defaultValue("false")
      .type(Property.Type.BOOLEAN)
      .defaultDescription("Whether to use Spot Instances. " +
          "Since Spot Instances can be terminated unexpectedly if the EC2 Spot price increases, " +
          "they should be used only for workers, and not for nodes that must be reliable, " +
          "such as masters and data nodes.<br />" +
          "<a target='_blank' href='http://aws.amazon.com/ec2/spot/'>More Information</a>")
      .build()),

  /**
   * Spot bid in USD/hr.
   *
   * @deprecated Use {@link #SPOT_PRICE_USD_PER_HR} instead.
   */
  SPOT_BID_USD_PER_HR(new SimpleConfigurationPropertyBuilder()
      .configKey("spotBidUSDPerHr")
      .name("Spot bid (USD/hr)")
      .defaultDescription(
          "Specify a Spot bid (in USD/hr). " +
              "This property is deprecated in favor of spotPriceUSDPerHr, but still works as " +
              "an alternative name for spotPriceUSDPerHr. This property will be removed in a " +
              "future plugin release."
      ).widget(ConfigurationProperty.Widget.NUMBER)
      .type(Property.Type.DOUBLE)
      .hidden(true)
      .build()),

  /**
   * Spot price in USD/hr.
   */
  SPOT_PRICE_USD_PER_HR(new SimpleConfigurationPropertyBuilder()
      .configKey("spotPriceUSDPerHr")
      .name("Spot price (USD/hr)")
      .defaultDescription(
          "Specify a Spot price (in USD/hr). " +
              "The Spot price is the maximum amount you are willing to pay per hour for each Spot Instance. " +
              "When your Spot price exceeds the Spot price set by EC2, then EC2 fulfills your request, " +
              "if capacity is available. Spot Instances will be terminated if the Spot price set by EC2 " +
              "rises above your Spot price."
      ).widget(ConfigurationProperty.Widget.NUMBER)
      .type(Property.Type.DOUBLE)
      .build()),

  /**
   * The required duration for the Spot instances.
   *
   * @see <a href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html#fixed-duration-spot-instances'>Fixed Duration Spot Instances</a>
   */
  BLOCK_DURATION_MINUTES(new SimpleConfigurationPropertyBuilder()
      .configKey("blockDurationMinutes")
      .name("Spot Block Duration (minutes)")
      .defaultDescription(
          "Specify the required duration for the Spot instances (also known as Spot blocks), " +
              "in minutes. This value must be a multiple of 60 (60, 120, 180, 240, 300, or 360). " +
              "Spot block instances will run for the predefined duration - in hourly increments " +
              "up to six hours in length - at a significant discount compared to On-Demand " +
              "pricing.<br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html#fixed-duration-spot-instances'>More Information</a>"
      ).widget(ConfigurationProperty.Widget.LIST)
      .type(Property.Type.INTEGER)
      .addValidValues("60", "120", "180", "240", "300", "360")
      .build()),

  /**
   * The ID of the Amazon VPC subnet.
   *
   * @see <a href="http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html">Subnets</a>
   */
  SUBNET_ID(new SimpleConfigurationPropertyBuilder()
      .configKey("subnetId")
      .name("VPC subnet ID")
      .required(true)
      .defaultDescription(
          "The VPC subnet ID.<br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html'>More Information</a>"
      ).defaultErrorMessage("VPC subnet ID is mandatory")
      .build()),

  /**
   * <p>The tenancy.</p>
   * <p>The tenancy of the instance (if the instance is running in a VPC).
   * An instance with dedicated tenancy runs on single-tenant hardware.</p>
   *
   * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/how-dedicated-hosts-work.html">Using Dedicated Hosts</a>
   */
  TENANCY(new SimpleConfigurationPropertyBuilder()
      .configKey("tenancy")
      .name("Tenancy")
      .defaultValue("default")
      .widget(ConfigurationProperty.Widget.LIST)
      .defaultDescription(
          "Instance tenancy should be defined here. This setting allows users to launch the desired instance in VPC with a defined tenancy. <br/>" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/dedicated-instance.html'>More information</a>"
      ).addValidValues(
          "default",
          "dedicated"
      )
      .build()),

  /**
   * The instance type (e.g. t1.micro, m1.medium, etc.).
   */
  TYPE(new SimpleConfigurationPropertyBuilder()
      .configKey(ComputeInstanceTemplateConfigurationPropertyToken.TYPE.unwrap().getConfigKey())
      .name("Instance type")
      .required(true)
      .widget(ConfigurationProperty.Widget.OPENLIST)
      .defaultDescription(
          "Select an instance type as per the needs of the processes to be run on this instance.<br />" +
              "<a target='_blank' href='http://aws.amazon.com/ec2/instance-types/'>More Information</a>"
      ).defaultPlaceholder("Select an instance type")
      .defaultErrorMessage("Instance type is mandatory")
      .addValidValues(
          "t2.micro",
          "t2.small",
          "t2.medium",
          "t2.large",
          "t2.xlarge",
          "t2.2xlarge",
          "t3.micro",
          "t3.small",
          "t3.medium",
          "t3.large",
          "t3.xlarge",
          "t3.2xlarge",
          "m3.medium",
          "m3.large",
          "m3.xlarge",
          "m3.2xlarge",
          "m4.large",
          "m4.xlarge",
          "m4.2xlarge",
          "m4.4xlarge",
          "m4.10xlarge",
          "m4.16xlarge",
          "m5.large",
          "m5.xlarge",
          "m5.2xlarge",
          "m5.4xlarge",
          "m5.12xlarge",
          "m5.24xlarge",
          "c3.large",
          "c3.xlarge",
          "c3.2xlarge",
          "c3.4xlarge",
          "c3.8xlarge",
          "c4.large",
          "c4.xlarge",
          "c4.2xlarge",
          "c4.4xlarge",
          "c4.8xlarge",
          "c5.large",
          "c5.xlarge",
          "c5.2xlarge",
          "c5.4xlarge",
          "c5.9xlarge",
          "c5.18xlarge",
          "g2.2xlarge",
          "g2.8xlarge",
          "g3.4xlarge",
          "g3.8xlarge",
          "g3.16xlarge",
          "p2.xlarge",
          "p2.8xlarge",
          "p2.16xlarge",
          "p3.2xlarge",
          "p3.8xlarge",
          "p3.16xlarge",
          "r3.large",
          "r3.xlarge",
          "r3.2xlarge",
          "r3.4xlarge",
          "r3.8xlarge",
          "r4.large",
          "r4.xlarge",
          "r4.2xlarge",
          "r4.4xlarge",
          "r4.8xlarge",
          "r4.16xlarge",
          "r5.large",
          "r5.xlarge",
          "r5.2xlarge",
          "r5.4xlarge",
          "r5.12xlarge",
          "r5.24xlarge",
          "r5d.large",
          "r5d.xlarge",
          "r5d.2xlarge",
          "r5d.4xlarge",
          "r5d.12xlarge",
          "r5d.24xlarge",
          "z1d.large",
          "z1d.xlarge",
          "z1d.2xlarge",
          "z1d.3xlarge",
          "z1d.6xlarge",
          "z1d.12xlarge",
          "x1.16xlarge",
          "x1.32xlarge",
          "i2.xlarge",
          "i2.2xlarge",
          "i2.4xlarge",
          "i2.8xlarge",
          "i3.large",
          "i3.xlarge",
          "i3.2xlarge",
          "i3.4xlarge",
          "i3.8xlarge",
          "i3.16xlarge",
          "h1.2xlarge",
          "h1.4xlarge",
          "h1.8xlarge",
          "h1.16xlarge",
          "hs1.8xlarge",
          "d2.xlarge",
          "d2.2xlarge",
          "d2.4xlarge",
          "d2.8xlarge",
          "f1.2xlarge",
          "f1.16xlarge")
      .build()),

  /**
   * The user data, base64 encoded. Mutually exclusive with {@link #USER_DATA_UNENCODED}.
   *
   * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html">Instance Metadata and User Data</a>
   */
  USER_DATA(new SimpleConfigurationPropertyBuilder()
      .configKey("userData")
      .name("User data")
      .required(false)
      .widget(ConfigurationProperty.Widget.TEXT)
      .defaultDescription(
          "The user data, base64 encoded.<br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html'>More Information</a>"
      )
      .sensitive(true)
      .build()),

  /**
   * The user data, unencoded. Mutually exclusive with {@link #USER_DATA}.
   *
   * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html">Instance Metadata and User Data</a>
   */
  USER_DATA_UNENCODED(new SimpleConfigurationPropertyBuilder()
      .configKey("userDataUnencoded")
      .name("User data (unencoded)")
      .required(false)
      .widget(ConfigurationProperty.Widget.TEXTAREA)
      .defaultDescription(
          "The user data, unencoded.<br />" +
              "<a target='_blank' href='http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html'>More Information</a>"
      )
      .sensitive(true)
      .build());

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

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

  @Override
  public ConfigurationProperty unwrap() {
    return configurationProperty;
  }
} @SuppressWarnings({"PMD.TooManyStaticImports", "Guava"})
public class EphemeralDeviceMappings {

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

  public static class EphemeralDeviceMappingsConfigProperties extends DeviceMappingsConfigProperties {

    /**
     * Ephemeral device mappings configuration properties.
     */
    // Fully qualifying class name due to compiler bug
    @SuppressWarnings("PMD.UnnecessaryFullyQualifiedName")
    public enum EphemeralDeviceMappingsConfigurationPropertyToken
        implements com.cloudera.director.spi.v2.model.ConfigurationPropertyToken {
      /**
       * Path for the custom device 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 ephemeral device 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
       */
      EphemeralDeviceMappingsConfigurationPropertyToken(
          ConfigurationProperty configurationProperty) {
        this.configurationProperty = configurationProperty;
      }

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

    public static final String DEFAULT_DEVICE_NAME_PREFIX = "/dev/sd";

    /**
     * Ephemeral drives should be attached in the following range:
     * /dev/(sd|xvd)[b-y].
     */
    public static final char DEFAULT_RANGE_START_FOR_EPHEMERAL_DRIVES = 'b';

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

    private final File configurationDirectory;

    private String customMappingsPath = DEFAULT_CUSTOM_MAPPINGS_PATH;


    /**
     * Creates ephemeral device 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 EphemeralDeviceMappingsConfigProperties(Configured configuration,
        File configurationDirectory,
        LocalizationContext cloudLocalizationContext) {
      super(Configurations.EPHEMERAL_DEVICE_MAPPINGS_SECTION, configuration, cloudLocalizationContext);
      this.configurationDirectory = configurationDirectory;
      LocalizationContext localizationContext = new ChildLocalizationContext(
          cloudLocalizationContext, Configurations.EPHEMERAL_DEVICE_MAPPINGS_SECTION);
      setCustomMappingsPath(configuration.getConfigurationValue(CUSTOM_MAPPINGS_PATH,
          localizationContext));
    }

    @Override
    public String getDefaultDeviceNamePrefix() {
      return DEFAULT_DEVICE_NAME_PREFIX;
    }

    @Override
    public char getDefaultRangeStart() {
      return DEFAULT_RANGE_START_FOR_EPHEMERAL_DRIVES;
    }

    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={} (default {})",
            customMappingsPath, DEFAULT_CUSTOM_MAPPINGS_PATH);
        checkArgument(getCustomMappingsFile(customMappingsPath).exists(),
            "Custom ephemeral device mappings path " +
                customMappingsPath + " does not exist");
        this.customMappingsPath = customMappingsPath;
      }
    }
  }

  public static class EphemeralDeviceMappingsConfig {

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

    protected EphemeralDeviceMappingsConfigProperties
        ephemeralDeviceMappingsConfigProperties;

    public PropertyResolver ephemeralDeviceMappingsResolver() {
      try {
        return PropertyResolvers.newMultiResourcePropertyResolver(
            BUILT_IN_LOCATION,
            "file:" + ephemeralDeviceMappingsConfigProperties.getCustomMappingsFile().getAbsolutePath()
        );
      } catch (IOException e) {
        throw new IllegalArgumentException("Could not load ephemeral device mappings", e);
      }
    }

    /**
     * Creates an ephemeral device mappings config with the specified parameters.
     *
     * @param ephemeralDeviceMappingsConfigProperties the config properties
     */
    public EphemeralDeviceMappingsConfig(
        EphemeralDeviceMappingsConfigProperties ephemeralDeviceMappingsConfigProperties) {
      this.ephemeralDeviceMappingsConfigProperties = ephemeralDeviceMappingsConfigProperties;
    }
  }

  EphemeralDeviceMappingsConfigProperties ephemeralDeviceMappingsConfigProperties;

  PropertyResolver ephemeralDeviceMappingsResolver;

  /**
   * Creates ephemeral device mappings with the specified parameters.
   *
   * @param configuration               the configuration
   * @param configurationDirectory      the plugin configuration directory
   * @param launcherLocalizationContext the parent launcher localization context
   */
  public EphemeralDeviceMappings(Configured configuration, File configurationDirectory,
      LocalizationContext launcherLocalizationContext) {
    this(new EphemeralDeviceMappingsConfigProperties(configuration, configurationDirectory,
        launcherLocalizationContext));
  }

  /**
   * Creates ephemeral device mappings with the specified parameters.
   *
   * @param ephemeralDeviceMappingsConfigProperties the config properties
   */
  public EphemeralDeviceMappings(
      EphemeralDeviceMappingsConfigProperties ephemeralDeviceMappingsConfigProperties) {
    this(ephemeralDeviceMappingsConfigProperties,
        new EphemeralDeviceMappingsConfig(ephemeralDeviceMappingsConfigProperties)
            .ephemeralDeviceMappingsResolver());
  }

  /**
   * Creates ephemeral device mappings with the specified parameters.
   *
   * @param ephemeralDeviceMappingsConfigProperties the config properties
   * @param ephemeralDeviceMappingsResolver         the ephemeral device mappings resolver
   */
  private EphemeralDeviceMappings(
      EphemeralDeviceMappingsConfigProperties ephemeralDeviceMappingsConfigProperties,
      PropertyResolver ephemeralDeviceMappingsResolver) {
    this.ephemeralDeviceMappingsConfigProperties = ephemeralDeviceMappingsConfigProperties;
    this.ephemeralDeviceMappingsResolver = ephemeralDeviceMappingsResolver;
  }

  private final DeviceNameUtils deviceNameUtils = new DeviceNameUtils();

  /**
   * Generates a list of block device mappings for all ephemeral drives for
   * the given instance type.
   *
   * @param instanceType       EC2 instance type
   * @param excludeDeviceNames set of device names that shouldn't be used for the block device mappings
   * @return list of block device mappings
   * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html">Block Device Mapping</a>
   */
  public List<BlockDeviceMapping> getBlockDeviceMappings(String instanceType, Set<String> excludeDeviceNames) {
    checkNotNull(instanceType, "instanceType is null");

    Optional<Integer> optCount = getCount(instanceType);
    if (!optCount.isPresent()) {
      LOG.error("Unsupported instance type {}, add its ephemeral instance " +
          "volume count as a custom mapping; assuming zero", instanceType);
      return Collections.emptyList();
    }

    int count = optCount.get();
    if (count == 0) {
      return Collections.emptyList();
    }

    List<String> deviceNames = deviceNameUtils.getDeviceNames(
        ephemeralDeviceMappingsConfigProperties.getDeviceNamePrefix(),
        ephemeralDeviceMappingsConfigProperties.getRangeStart(),
        count, excludeDeviceNames);

    List<BlockDeviceMapping> result = Lists.newArrayListWithExpectedSize(count);
    int index = 0;
    for (String device : deviceNames) {
      result.add(new BlockDeviceMapping()
          .withDeviceName(device)
          .withVirtualName("ephemeral" + index));
      index += 1;
    }

    return result;
  }

  public List<BlockDeviceMapping> getBlockDeviceMappings(String instanceType) {
    return getBlockDeviceMappings(instanceType, Collections.emptySet());
  }

  private Optional<Integer> getCount(String instanceType) {
    return Optional.fromNullable(
        ephemeralDeviceMappingsResolver.getProperty(instanceType, Integer.class)
    );
  }

  /**
   * Gets a test instance of this class that uses only the given mapping.
   *
   * @param counts                      map of instance types to counts
   * @param launcherLocalizationContext the parent launcher localization context
   * @return new mapping object
   */
  public static EphemeralDeviceMappings getTestInstance(Map<String, Integer> counts,
      LocalizationContext launcherLocalizationContext) {
    Map<String, String> propertyMap =
        Maps.transformValues(counts, Functions.toStringFunction());
    PropertyResolver ephemeralDeviceMappingsResolver =
        PropertyResolvers.newMapPropertyResolver(propertyMap);
    File tempDir = Files.createTempDir();
    tempDir.deleteOnExit();
    EphemeralDeviceMappingsConfigProperties ephemeralDeviceMappingsConfigProperties =
        new EphemeralDeviceMappingsConfigProperties(new SimpleConfiguration(),
            tempDir, launcherLocalizationContext);
    return new EphemeralDeviceMappings(ephemeralDeviceMappingsConfigProperties,
        ephemeralDeviceMappingsResolver);
  }
} 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);
  }
} public class EBSDeviceMappings {

  private final DeviceNameUtils deviceNameUtils = new DeviceNameUtils();

  static final String DEFAULT_EBS_DEVICE_NAME_PREFIX = "/dev/sd";
  static final char DEFAULT_EBS_DEVICE_NAME_START_CHAR = 'f';

  public static class EbsDeviceMappingsConfigProperties extends DeviceMappingsConfigProperties {

    /**
     * Creates EBS device mappings config properties with the specified parameters.
     *
     * @param configSection            the plugin configuration section
     * @param configuration            the configuration
     * @param cloudLocalizationContext the parent cloud localization context
     */
    public EbsDeviceMappingsConfigProperties(String configSection, Configured configuration,
        LocalizationContext cloudLocalizationContext) {
      super(configSection, configuration, cloudLocalizationContext);
    }

    @Override
    public String getDefaultDeviceNamePrefix() {
      return DEFAULT_EBS_DEVICE_NAME_PREFIX;
    }

    @Override
    public char getDefaultRangeStart() {
      return DEFAULT_EBS_DEVICE_NAME_START_CHAR;
    }
  }

  private final EbsDeviceMappingsConfigProperties ebsDeviceMappingsConfigProperties;

  /**
   * Creates EBS device mappings with the specified configuration.
   *
   * @param configuration            the configuration
   * @param cloudLocalizationContext the parent cloud localization context
   */
  public EBSDeviceMappings(Configured configuration, LocalizationContext cloudLocalizationContext) {
    this.ebsDeviceMappingsConfigProperties = new EbsDeviceMappingsConfigProperties(
        Configurations.EBS_DEVICE_MAPPINGS_SECTION, configuration, cloudLocalizationContext);
  }

  /**
   * Gets a list of EBS block device mappings with the specified parameters.
   *
   * @param template     EC2InstanceTemplate which contains information about
   *                     the EBS volumes requested
   * @param excludeDeviceNames set device names that shouldn't be used for the block device mappings
   * @return set of EBS BlockDeviceMapping
   */
  @SuppressWarnings("Guava")
  public List<BlockDeviceMapping> getBlockDeviceMappings(EC2InstanceTemplate template,
                                                         Set<String> excludeDeviceNames) {
    List<SystemDisk> systemDisks = template.getSystemDisks();

    int templateDiskCount = template.getEbsVolumeCount();
    int systemDiskCount = systemDisks.size();
    int totalVolumeCount = templateDiskCount + systemDiskCount;

    List<String> deviceNames = getDeviceNames(totalVolumeCount, excludeDeviceNames);

    List<BlockDeviceMapping> mappings = Lists.newArrayList();

    for (int i = 0; i < systemDisks.size(); i++) {
      String deviceName = deviceNames.get(i);
      SystemDisk systemDisk = systemDisks.get(i);
      mappings.add(systemDisk.toBlockDeviceMapping(deviceName));
    }

    for (int i = 0; i < templateDiskCount; i++) {
      String deviceName = deviceNames.get(i + systemDiskCount);

      EbsBlockDevice ebs = new EbsBlockDevice()
          .withVolumeType(template.getEbsVolumeType())
          .withVolumeSize(template.getEbsVolumeSizeGiB())
          .withEncrypted(template.isEnableEbsEncryption())
          .withDeleteOnTermination(true);

      Optional<Integer> iops = template.getEbsIops();
      Optional<String> kmsKeyid = template.getEbsKmsKeyId();

      if (iops.isPresent()) {
        ebs = ebs.withIops(iops.get());
      }
      if (kmsKeyid.isPresent()) {
        ebs = ebs.withKmsKeyId(kmsKeyid.get());
      }

      BlockDeviceMapping mapping = new BlockDeviceMapping()
          .withDeviceName(deviceName)
          .withEbs(ebs);

      mappings.add(mapping);
    }

    return mappings;
  }

  List<String> getDeviceNames(int count, Set<String> filter) {
    return deviceNameUtils.getDeviceNames(
        ebsDeviceMappingsConfigProperties.getDeviceNamePrefix(),
        ebsDeviceMappingsConfigProperties.getRangeStart(),
        count, filter
    );
  }


  /**
   * Gets an instance of this class that uses only the given EBS device mapping configuration.
   *
   * @param configuration       EBS device mapping configuration
   * @param localizationContext the localization context
   * @return new EBS metadata object
   */
  public static EBSDeviceMappings getDefaultInstance(
      final Map<String, String> configuration, LocalizationContext localizationContext) {
    return new EBSDeviceMappings(new SimpleConfiguration(configuration), localizationContext);
  }
} @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);
  }
} public final class NetworkRules {

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

  /**
   * The root key for network rules config.
   */
  private static final String ROOT_KEY = NetworkRules.class.getCanonicalName();

  /**
   * The table holding all network rules.
   */
  private final Table<Direction, AccessType, Iterable<NetworkRule>> rules;

  /**
   * A constant represents an empty set of network rules.
   */
  public static final NetworkRules EMPTY_RULES
      = new NetworkRules(ImmutableTable.of());

  /**
   * Internal constructor.
   *
   * @param rules a rules table
   */
  private NetworkRules(Table<Direction, AccessType, Iterable<NetworkRule>> rules) {
    Preconditions.checkNotNull(rules, "rules is null");
    this.rules = rules;
  }

  /**
   * Constructs a NetworkRules object using the given HOCON config.
   *
   * @param config a HOCON config
   * @return a NetworkRules object
   */
  public static NetworkRules fromConfig(Config config) {
    if (config == null) {
      return EMPTY_RULES;
    }

    try {
      return new NetworkRules(parse(config.getConfig(ROOT_KEY)));
    } catch (ConfigException | IllegalArgumentException e) {
      String msg = "Failed to parse network rules";
      LOG.error(msg, e);
      throw new IllegalArgumentException(msg, e);
    }
  }

  /**
   * Parses a HOCON config to a table of network rules.
   */
  private static Table<Direction, AccessType, Iterable<NetworkRule>> parse(Config config) {
    Table<Direction, AccessType, Iterable<NetworkRule>> rules =
        ArrayTable.create(EnumSet.allOf(Direction.class), EnumSet.allOf(AccessType.class));
    for (Direction direction : Direction.values()) {
      for (AccessType access : AccessType.values()) {
        populateRules(config, direction, access, rules);
      }
    }
    return rules;
  }

  /**
   * Populates the rule table.
   *
   * @param config    the HOCON config
   * @param direction the network direction
   * @param access    the network rule access type
   * @param rules     the rules table
   */
  private static void populateRules(Config config, Direction direction, AccessType access,
      Table<Direction, AccessType, Iterable<NetworkRule>> rules) {
    List<? extends Config> configList =
        config.getConfig(direction.toString().toLowerCase()).
            getConfigList(access.toString().toLowerCase());
    Set<NetworkRule> cell = Sets.newHashSet();
    for (Config val : configList) {
      Iterables.addAll(cell, NetworkRule.fromConfig(val, access));
    }
    rules.put(direction, access, cell);
  }

  /**
   * Returns the number of network rules.
   *
   * @return the number of network rules
   */
  public int size() {
    return this.rules.size();
  }

  /**
   * Checks if the network rule set is empty.
   *
   * @return true if it contains no network rules
   */
  public boolean isEmpty() {
    return this.rules.isEmpty();
  }

  /**
   * Retrieves rules based on the direction of network traffic.
   * <p>
   * <p>The returned rules are just copies of actual network rules. Changes to the
   * returned collection will not update the underlying NetworkRules, and vice versa.
   *
   * @param direction the network direction
   * @return the associated list of network rules
   */
  public Iterable<NetworkRule> getRules(Direction direction) {
    if (this.rules.isEmpty()) {
      return Collections.emptySet();
    }
    Set<NetworkRule> ruleSet = Sets.newHashSet();
    for (AccessType access : AccessType.values()) {
      Iterables.addAll(ruleSet, getRules(direction, access));
    }
    return ruleSet;
  }

  /**
   * Retrieves rules based on the network direction and access type.
   * <p>
   * <p>The returned rules are just copies of actual network rules. Changes to the
   * returned collection will not update the underlying NetworkRules, and vice versa.
   *
   * @param direction the network direction
   * @param access    the network rule access type
   * @return the associated list of network rules
   */
  public Iterable<NetworkRule> getRules(Direction direction, AccessType access) {
    return this.rules.isEmpty()
        ? Collections.emptySet()
        : Sets.newHashSet(this.rules.get(direction, access));
  }
}
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.autoscaling.AmazonAutoScalingAsyncClient;
import com.amazonaws.services.ec2.AmazonEC2AsyncClient;
import com.amazonaws.services.ec2.model.BlockDeviceMapping;
import com.amazonaws.services.ec2.model.DescribeImagesRequest;
import com.amazonaws.services.ec2.model.DescribeImagesResult;
import com.amazonaws.services.ec2.model.DescribeInstanceAttributeRequest;
import com.amazonaws.services.ec2.model.DescribeInstanceStatusRequest;
import com.amazonaws.services.ec2.model.DescribeInstanceStatusResult;
import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.ec2.model.EbsBlockDevice;
import com.amazonaws.services.ec2.model.Image;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.InstanceState;
import com.amazonaws.services.ec2.model.InstanceStateName;
import com.amazonaws.services.ec2.model.InstanceStatus;
import com.amazonaws.services.ec2.model.Reservation;
import com.amazonaws.services.ec2.model.RunInstancesRequest;
import com.amazonaws.services.ec2.model.RunInstancesResult;
import com.amazonaws.services.ec2.model.Tag;
import com.amazonaws.services.ec2.model.TagSpecification;
import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient;
import com.amazonaws.services.kms.AWSKMSClient;
import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceAsyncClient;
import com.cloudera.director.aws.AWSFilters;
import com.cloudera.director.aws.AWSTimeouts;
import com.cloudera.director.aws.CustomTagMappings;
import com.cloudera.director.aws.Tags;
import com.cloudera.director.aws.clientprovider.ClientProvider;
import com.cloudera.director.aws.ec2.EC2InstanceTemplate;
import com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken;
import com.cloudera.director.aws.ec2.EphemeralDeviceMappings;
import com.cloudera.director.aws.ec2.VirtualizationMappings;
import com.cloudera.director.aws.ec2.ebs.EBSDeviceMappings;
import com.cloudera.director.aws.ec2.ebs.EBSMetadata;
import com.cloudera.director.aws.network.NetworkRules;
import com.cloudera.director.spi.v2.model.Configured;
import com.cloudera.director.spi.v2.model.LocalizationContext;
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.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.typesafe.config.ConfigFactory;
import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.junit.Test;
import org.mockito.stubbing.Answer;
// (c) Copyright 2017 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.provider;





/**
 * <p>Tests EC2Provider.</p>
 * <p>
 * <p><em>Note:</em> This test is deliberately placed in the provider module, rather than the tests
 * module, due to the inconveniences introduced by shading. </p>
 */
public class EC2ProviderLocalTest {

  private static final LocalizationContext LOCALIZATION_CONTEXT =
      new DefaultLocalizationContext(Locale.getDefault(), "");
  private static final Map<String, String> TEMPLATE_CONFIGS = ImmutableMap.<String, String>builder()
      .put(EC2InstanceTemplateConfigurationPropertyToken.TYPE.unwrap().getConfigKey(), "m4.xlarge")
      .put(EC2InstanceTemplateConfigurationPropertyToken.IMAGE.unwrap().getConfigKey(), "ami-12345678")
      .put(EC2InstanceTemplateConfigurationPropertyToken.SUBNET_ID.unwrap().getConfigKey(), "ami-12345678")
      .put(EC2InstanceTemplateConfigurationPropertyToken.SECURITY_GROUP_IDS.unwrap().getConfigKey(), "sg-12345678")
      .build();
  private static final EC2InstanceTemplate TEST_TEMPLATE = new EC2InstanceTemplate("instance-template",
      new SimpleConfiguration(TEMPLATE_CONFIGS), ImmutableMap.of(), LOCALIZATION_CONTEXT);

  private AmazonEC2AsyncClient ec2Client;

  @Test
  public void testAllocate() throws ExecutionException, InterruptedException {
    EC2Provider ec2Provider = getTestEc2Provider();

    when(ec2Client.describeInstances(any(DescribeInstancesRequest.class)))
        .thenReturn(makeDescribeInstancesResult(
            makeInstance("1i", "1v", InstanceStateName.Running, "192.0.2.1"),
            makeInstance("2i", "2v", InstanceStateName.Running, "192.0.2.2"),
            makeInstance("3i", "3v", InstanceStateName.Running, "192.0.2.3")));
    when(ec2Client.describeInstanceAttribute(any(DescribeInstanceAttributeRequest.class)))
        .thenThrow(new AmazonServiceException("dummyException"));

    when(ec2Client.describeImages(any(DescribeImagesRequest.class)))
        .thenReturn(makeDescribeImagesResult());

    when(ec2Client.runInstancesAsync(any(RunInstancesRequest.class))).then(
        (Answer<Future<RunInstancesResult>>) invocation -> {

          List<Tag> allTags =
              tagSpecificationsToTags(((RunInstancesRequest) invocation.getArguments()[0]).getTagSpecifications());

          if (tagsContainsVirtualInstanceId(allTags, "4v")) {
            return makeRunInstancesResult("4i", "4v", InstanceStateName.Running, "192.0.2.4");
          } else if (tagsContainsVirtualInstanceId(allTags, "5v")) {
            return makeRunInstancesResult("5i", "5v", InstanceStateName.Running, "192.0.2.5");
          }

          return null;
        });

    when(ec2Client.describeInstanceStatus(makeDescribeInstanceStatusRequest("1i")))
        .thenReturn(makeDescribeInstanceStatusResult("1i", InstanceStateName.Running));
    when(ec2Client.describeInstanceStatus(makeDescribeInstanceStatusRequest("2i")))
        .thenReturn(makeDescribeInstanceStatusResult("2i", InstanceStateName.Running));
    when(ec2Client.describeInstanceStatus(makeDescribeInstanceStatusRequest("3i")))
        .thenReturn(makeDescribeInstanceStatusResult("3i", InstanceStateName.Running));
    when(ec2Client.describeInstanceStatus(makeDescribeInstanceStatusRequest("4i")))
        .thenReturn(makeDescribeInstanceStatusResult("4i", InstanceStateName.Running));
    when(ec2Client.describeInstanceStatus(makeDescribeInstanceStatusRequest("5i")))
        .thenReturn(makeDescribeInstanceStatusResult("5i", InstanceStateName.Running));

    assertThat(ec2Provider.allocate(TEST_TEMPLATE, Lists.newArrayList("1v", "2v", "3v", "4v", "5v"), 1)).hasSize(5);
  }

  @Test
  public void testAllocateSomeTerminateWhileWaitingForIp() throws ExecutionException, InterruptedException {
    EC2Provider ec2Provider = getTestEc2Provider();

    when(ec2Client.describeInstances(any(DescribeInstancesRequest.class)))
        .thenAnswer((Answer<DescribeInstancesResult>) invocation -> {
          DescribeInstancesRequest request = (DescribeInstancesRequest) invocation.getArguments()[0];

          if (!request.getFilters().isEmpty()) {
            return makeDescribeInstancesResult(
                makeInstance("1i", "1v", InstanceStateName.Running, "192.0.2.1"),
                makeInstance("2i", "2v", InstanceStateName.Running, "192.0.2.2"),
                makeInstance("3i", "3v", InstanceStateName.Running, "192.0.2.3"));
          } else if (!request.getInstanceIds().isEmpty()) {
            return makeDescribeInstancesResult(
                makeInstance("5i", "5v", InstanceStateName.Terminated, null),
                makeInstance("6i", "6v", InstanceStateName.Terminated, null));
          }

          return null;
        });

    when(ec2Client.describeInstanceAttribute(any(DescribeInstanceAttributeRequest.class)))
        .thenThrow(new AmazonServiceException("dummyException"));

    when(ec2Client.describeImages(any(DescribeImagesRequest.class)))
        .thenReturn(makeDescribeImagesResult());

    when(ec2Client.runInstancesAsync(any(RunInstancesRequest.class))).then(
        (Answer<Future<RunInstancesResult>>) invocation -> {

          List<Tag> allTags =
              tagSpecificationsToTags(((RunInstancesRequest) invocation.getArguments()[0]).getTagSpecifications());

          if (tagsContainsVirtualInstanceId(allTags, "4v")) {
            return makeRunInstancesResult("4i", "4v", InstanceStateName.Running, "192.0.2.4");
          } else if (tagsContainsVirtualInstanceId(allTags, "5v")) {
            return makeRunInstancesResult("5i", "5v", InstanceStateName.Running, null);
          } else if (tagsContainsVirtualInstanceId(allTags, "6v")) {
            return makeRunInstancesResult("6i", "6v", InstanceStateName.Running, null);
          }

          return null;
        });

    when(ec2Client.describeInstanceStatus(makeDescribeInstanceStatusRequest("1i")))
        .thenReturn(makeDescribeInstanceStatusResult("1i", InstanceStateName.Running));
    when(ec2Client.describeInstanceStatus(makeDescribeInstanceStatusRequest("2i")))
        .thenReturn(makeDescribeInstanceStatusResult("2i", InstanceStateName.Running));
    when(ec2Client.describeInstanceStatus(makeDescribeInstanceStatusRequest("3i")))
        .thenReturn(makeDescribeInstanceStatusResult("3i", InstanceStateName.Running));
    when(ec2Client.describeInstanceStatus(makeDescribeInstanceStatusRequest("4i")))
        .thenReturn(makeDescribeInstanceStatusResult("4i", InstanceStateName.Running));
    when(ec2Client.describeInstanceStatus(makeDescribeInstanceStatusRequest("5i")))
        .thenReturn(makeDescribeInstanceStatusResult("5i", InstanceStateName.Running));
    when(ec2Client.describeInstanceStatus(makeDescribeInstanceStatusRequest("6i")))
        .thenReturn(makeDescribeInstanceStatusResult("6i", InstanceStateName.Running));

    assertThat(ec2Provider.allocate(TEST_TEMPLATE, Lists.newArrayList("1v", "2v", "3v", "4v", "5v", "6v"), 1))
        .hasSize(4);
  }

  private EC2Provider getTestEc2Provider() {
    return getTestEc2Provider(ImmutableMap.of());
  }

  @SuppressWarnings("unchecked")
  private EC2Provider getTestEc2Provider(Map<String, String> tagMappings) {

    EphemeralDeviceMappings ephemeralDeviceMappings = new EphemeralDeviceMappings(new SimpleConfiguration(),
        new File("dummy"), LOCALIZATION_CONTEXT);
    EBSDeviceMappings ebsDeviceMappings = mock(EBSDeviceMappings.class);
    EBSMetadata ebsMetadata = new EBSMetadata(new SimpleConfiguration(),