public static final int MAX_TAGS_ALLOWED =
    50 - Tags.InstanceTags.values().length - Tags.ResourceTags.values().length; public static final Map<String, Ami> CENTOS67_HVM_AMI_BY_REGION = ImmutableMap.of(
    "us-west-1", new Ami("ami-ac5f2fcc", "us-west-1", false),
    "us-west-2", new Ami("ami-05cf2265", "us-west-2", false)
); public static Map<String, InstanceState> waitUntilRunningOrTerminal(EC2Provider ec2Provider,
    EC2InstanceTemplate instanceTemplate, Collection<String> virtualInstanceIds) throws InterruptedException {
  return waitForInstanceStates(
      ec2Provider,
      instanceTemplate,
      virtualInstanceIds,
      EnumSet.of(InstanceStatus.RUNNING),
      EnumSet.of(InstanceStatus.DELETED, InstanceStatus.FAILED, InstanceStatus.STOPPED)
  );
} public static EC2Provider getEc2Provider(Configured configuration,
                                         AWSCredentialsProvider credentialsProvider) {
  return getEc2Provider(configuration, credentialsProvider,
      new AmazonEC2ClientProvider(credentialsProvider, new ClientConfigurationFactory().getConfig()));
} public static boolean isLive() {
  String liveString = System.getProperty("test.aws.live");
  return Boolean.parseBoolean(liveString);
} public static void putConfig(Map<String, String> configMap, ConfigurationPropertyToken propertyToken,
                              String value) {
  if (value != null) {
    configMap.put(propertyToken.unwrap().getConfigKey(), value);
  }
} @SuppressWarnings("PMD.TooManyStaticImports")
public class AWSCredentialsProviderChainProvider
    implements CredentialsProvider<AWSCredentialsProvider> {

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

  private final List<RoleConfiguration> roleConfigurations;

  /**
   * Creates an instance of AWS credential provider chain provider.
   */
  public AWSCredentialsProviderChainProvider() {
    this(null);
  }

  /**
   * Creates an instance of AWS credential provider chain provider.
   *
   * @param roleConfigurations the role configurations
   */
  public AWSCredentialsProviderChainProvider(List<RoleConfiguration> roleConfigurations) {
    this.roleConfigurations = roleConfigurations != null
        ? ImmutableList.copyOf(roleConfigurations)
        : Collections.emptyList();
  }

  /**
   * The credentials provider metadata.
   */
  public static final CredentialsProviderMetadata METADATA =
      new SimpleCredentialsProviderMetadata(
          AWSConfigCredentialsProviderProvider.getConfigurationProperties());

  @Override
  public CredentialsProviderMetadata getMetadata() {
    return METADATA;
  }

  @Override
  public AWSCredentialsProvider createCredentials(
      Configured config,
      LocalizationContext localizationContext) {
    return new AWSConfigCredentialsProviderProvider(roleConfigurations, config, localizationContext)
        .getCredentialsProvider();
  }

  /**
   * Config-based AWS credentials provider provider.
   */
  // Fully qualifying class names due to compiler bug
  @SuppressWarnings("Guava")
  public static class AWSConfigCredentialsProviderProvider
      extends com.cloudera.director.spi.v2.model.util.AbstractConfigured {

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

    /**
     * Returns the list of configuration properties for creating credentials.
     *
     * @return the list of configuration properties for creating credentials
     */
    public static List<ConfigurationProperty> getConfigurationProperties() {
      return CONFIGURATION_PROPERTIES;
    }

    /**
     * AWS credentials configuration property tokens.
     */
    // Fully qualifying class name due to compiler bug
    public enum AWSConfigCredentialsProviderConfigurationPropertyToken
        implements com.cloudera.director.spi.v2.model.ConfigurationPropertyToken {

      /**
       * An alphanumeric text string that uniquely identifies the user who
       * owns the account. No two accounts can have the same AWS Access Key.
       *
       * @see <a href="http://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html">AWS Security Credentials</a>
       */
      ACCESS_KEY_ID(new SimpleConfigurationPropertyBuilder()
          .configKey("accessKeyId")
          .name("Access key ID")
          .defaultDescription("The AWS access key ID.<br />"
              + "Leave unset to get Amazon Web Services credentials from the OS environment.")
          .defaultErrorMessage("AWS credentials configuration is missing an access key ID")
          .sensitive(true)
          .build()),

      /**
       * This key plays the role of a  password. Together with the access key
       * this forms a secure pair that confirms the user's identity
       *
       * @see <a href="http://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html">AWS Security Credentials</a>
       */
      SECRET_ACCESS_KEY(new SimpleConfigurationPropertyBuilder()
          .configKey("secretAccessKey")
          .name("Secret access key")
          .widget(ConfigurationProperty.Widget.PASSWORD)
          .defaultDescription("The AWS secret access key.")
          .defaultErrorMessage("AWS credentials configuration is missing a secret access key")
          .sensitive(true)
          .build()),

      /**
       * The token that users must pass to the service API to use the temporary credentials.
       *
       * @see <a href="http://docs.aws.amazon.com/STS/latest/APIReference/API_Credentials.html">API Credentials</a>
       * @see <a href="http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html">API AssumeRole</a>
       */
      SESSION_TOKEN(new SimpleConfigurationPropertyBuilder()
          .configKey("sessionToken")
          .name("Session token")
          .defaultDescription("The AWS session token.")
          .sensitive(true)
          .hidden(true)
          .build()),

      /**
       * The Amazon Resource Name (ARN) of the role to assume.
       *
       * @see <a href="http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html">API AssumeRole</a>
       */
      DELEGATED_ROLE_ARN(new SimpleConfigurationPropertyBuilder()
          .configKey("delegatedRoleArn")
          .name("Delegated Role ARN")
          .defaultDescription("The Amazon Resource Name (ARN) of the role to assume.")
          .sensitive(false)
          .hidden(true)
          .build()),

      /**
       * A unique identifier that is used by third parties when assuming roles
       * in their customers' accounts. It is used to prevent the confused
       * deputy problem.
       *
       * @see <a href="http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html">
       * How to Use an External ID When Granting Access to Your AWS Resources to a Third Party</a>
       */
      DELEGATED_ROLE_EXTERNAL_ID(new SimpleConfigurationPropertyBuilder()
          .configKey("delegatedRoleExternalId")
          .name("Delegated Role External ID")
          .defaultDescription("The external id for delegated role access.")
          .sensitive(true)
          .hidden(true)
          .build());

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

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

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

    /**
     * The AWS credentials provider.
     */
    private final AWSCredentialsProvider credentialsProvider;

    /**
     * The duration, in seconds, of the role session. The value can range from
     * 900 seconds (15 minutes) to 3600 seconds (1 hour). By default, the value
     * is set to 3600 seconds.
     */
    private final static int ASSUME_ROLE_DURATION_SECONDS = 3600;

    private final static DefaultAwsRegionProviderChain DEFAULT_REGION_PROVIDER =
        new DefaultAwsRegionProviderChain();

    private final static String STS_GLOBAL_ENDPOINT = "sts.amazonaws.com";

    /**
     * Creates a config-based AWS credentials provider with the specified parameters.
     *
     * @param roleConfigurations  the role configurations
     * @param configuration       the configuration
     * @param localizationContext the localization context
     */
    @SuppressWarnings("PMD.UselessParentheses")
    public AWSConfigCredentialsProviderProvider(
        List<RoleConfiguration> roleConfigurations,
        Configured configuration,
        LocalizationContext localizationContext) {
      super(configuration);
      Optional<String> optionalAccessKeyId =
          getOptionalConfigurationValue(ACCESS_KEY_ID, localizationContext);
      Optional<String> optionalSecretAccessKey =
          getOptionalConfigurationValue(SECRET_ACCESS_KEY, localizationContext);
      if (optionalAccessKeyId.isPresent() && optionalSecretAccessKey.isPresent()) {
        String accessKeyId = optionalAccessKeyId.get();
        String secretAccessKey = optionalSecretAccessKey.get();
        Optional<String> sessionToken =
            getOptionalConfigurationValue(SESSION_TOKEN, localizationContext);
        credentialsProvider = new AWSStaticCredentialsProvider(
            sessionToken.isPresent()
                ? new BasicSessionCredentials(accessKeyId, secretAccessKey, sessionToken.get())
                : new BasicAWSCredentials(accessKeyId, secretAccessKey)
        );
      } else {
        Optional<String> roleArn =
            getOptionalConfigurationValue(DELEGATED_ROLE_ARN, localizationContext);
        Optional<String> externalId =
            getOptionalConfigurationValue(DELEGATED_ROLE_EXTERNAL_ID, localizationContext);
        if (roleArn.isPresent() && externalId.isPresent()) {
          roleConfigurations = FluentIterable
              .from(roleConfigurations)
              .append(new RoleConfiguration(
                  roleArn.get(),
                  getTimestampedSessionName(parseRoleName(roleArn.get())),
                  externalId.get()))
              .toList();
        }

        if (!roleConfigurations.isEmpty()) {
          credentialsProvider = createAssumeRoleSessionCredentialsProvider(roleConfigurations);
        } else {
          LOG.info("AWS credentials not fully specified."
              + " Credentials provider chain fallback will be used.");
          credentialsProvider = new DefaultAWSCredentialsProviderChain();
        }
      }
    }

    /**
     * Returns the AWS credentials provider.
     *
     * @return the AWS credentials provider
     */
    public AWSCredentialsProvider getCredentialsProvider() {
      return credentialsProvider;
    }

    /**
     * Creates the STS assume role session credentials provider.
     *
     * @param roleConfigurations the STS role configurations
     * @return an AWS session credentials provider
     */
    private AWSSessionCredentialsProvider createAssumeRoleSessionCredentialsProvider(
        List<RoleConfiguration> roleConfigurations) {
      LOG.info(
          "Building provider to assume role {} with duration {} in seconds",
          roleConfigurations,
          ASSUME_ROLE_DURATION_SECONDS);

      return new STSAssumeNRolesSessionCredentialsProvider
          .Builder(roleConfigurations, getSTSClientBuilder())
          .withRoleSessionDurationSeconds(ASSUME_ROLE_DURATION_SECONDS)
          .build();
    }

    private AWSSecurityTokenServiceClientBuilder getSTSClientBuilder() {
      AWSSecurityTokenServiceClientBuilder builder = AWSSecurityTokenServiceClientBuilder.standard();

      try {
        builder.withRegion(DEFAULT_REGION_PROVIDER.getRegion());
      } catch (AmazonClientException e) {
        LOG.warn("failed to retrieve region, falling back to STS global endpoint", e);
        builder.withEndpointConfiguration(
            new AwsClientBuilder.EndpointConfiguration(STS_GLOBAL_ENDPOINT, null));
      }

      return builder;
    }

    /**
     * Returns the value of the specified configuration property, or the default value (with
     * {@code null} replaced by an absent {@code Optional}) if the value is not present and the
     * configuration property is optional.
     *
     * @param token               the configuration property token
     * @param localizationContext the localization context
     * @return the value of the specified configuration property, or the default value (with
     * {@code null} replaced by an absent {@code Optional}) if the value is not present and the
     * configuration property is optional
     * @throws IllegalArgumentException if the specified configuration property is not present and
     *                                  required
     */
    private Optional<String> getOptionalConfigurationValue(
        com.cloudera.director.spi.v2.model.ConfigurationPropertyToken token,
        LocalizationContext localizationContext) {
      return Optional.fromNullable(getConfigurationValue(token.unwrap(), localizationContext));
    }
  }
} public class AWSExceptions {

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

  /**
   * Error code suffix indicating that a resource was not found.
   */
  private static final String NOT_FOUND_ERROR_CODE = ".NotFound";

  private static final String RESOURCE_NOT_FOUND = "Resource not found, might be a transient error";

  /**
   * Returns whether a {@code Throwable} indicates that an AWS resource cannot be found.
   *
   * @param throwable a throwable
   * @return whether a {@code Throwable} indicates that an AWS resource cannot be found
   */
  public static boolean isNotFound(Throwable throwable) {
    boolean isNotFound = AmazonServiceException.class.isInstance(throwable)
        && ((AmazonServiceException) throwable).getErrorCode().endsWith(NOT_FOUND_ERROR_CODE);

    if (isNotFound) {
      if (LOG.isDebugEnabled()) {
        LOG.debug(RESOURCE_NOT_FOUND, throwable);
      } else {
        LOG.info(RESOURCE_NOT_FOUND);
      }
    }

    return isNotFound;
  }

  /**
   * Insufficient instance capacity error code.
   */
  public static final String INSUFFICIENT_INSTANCE_CAPACITY = "InsufficientInstanceCapacity";

  /**
   * Instance limit exceeded error code.
   */
  public static final String INSTANCE_LIMIT_EXCEEDED = "InstanceLimitExceeded";

  /**
   * Request limit exceeded error code..
   */
  public static final String REQUEST_LIMIT_EXCEEDED = "RequestLimitExceeded";

  /**
   * Volume limit exceeded error code.
   */
  public static final String VOLUME_LIMIT_EXCEEDED = "VolumeLimitExceeded";

  /**
   * Volume limit exceeded state reason.
   */
  public static final String VOLUME_LIMIT_EXCEEDED_STATE_REASON = "Client.VolumeLimitExceeded";

  /**
   * Internal error error code.
   */
  public static final String INTERNAL_ERROR = "InternalError";


  // The set of error codes representing authorization failures.
  public static final Set<String> AUTHORIZATION_ERROR_CODES = ImmutableSet.of(
      "AuthFailure",
      "UnauthorizedOperation"
  );

  public static final String ENCODED_MESSAGE_STRING = "Encoded authorization failure message:";


  /**
   * Parses a set of exceptions and a set of failed state reasons and throws
   * an appropriate plugin exception with appropriate plugin exception details.
   * This will throw an {@link UnrecoverableProviderException} if the exception
   * set contains any exceptions that are unrecoverable. An UnrecoverableProviderException
   * will also be thrown if the set of failed state reason is not empty. In other cases a
   * {@link TransientProviderException} is thrown.
   *
   * @param stsClient          the AWS STS client for decoding authorization messages
   * @param message            the plugin exception message to set
   * @param exceptions         a set of exceptions
   * @param failedStateReasons state reasons for instances that transitioned to terminated
   * @param template           the EC2 instance template
   */
  public static void propagate(AWSSecurityTokenServiceAsyncClient stsClient, String message, Set<Exception> exceptions,
      Set<StateReason> failedStateReasons, EC2InstanceTemplate template) {
    PluginExceptionConditionAccumulator accumulator = new PluginExceptionConditionAccumulator();

    Set<Exception> decodedExceptions = exceptions.stream()
        .map(e -> {
          if (e instanceof AmazonServiceException)
            return decodeAuthorizationMessageIfPossible(stsClient, (AmazonServiceException) e);
          return e;
        })
        .collect(Collectors.toSet());
    boolean isUnrecoverable = addErrors(template, decodedExceptions, accumulator);

    if (!failedStateReasons.isEmpty()) {
      isUnrecoverable = true;
      addStateReasonErrors(template, failedStateReasons, accumulator);
    }

    PluginExceptionDetails pluginExceptionDetails =
        new PluginExceptionDetails(accumulator.getConditionsByKey());

    if (isUnrecoverable) {
      throw new UnrecoverableProviderException(message, pluginExceptionDetails);
    }

    throw new TransientProviderException(message, pluginExceptionDetails);
  }

  /**
   * Returns an appropriate SPI exception in response to the specified AWS exception.
   *
   * @param stsClient the AWS STS client for decoding authorization messages
   * @param e         the AWS exception
   * @return the corresponding SPI exception
   */
  public static RuntimeException propagate(AWSSecurityTokenServiceAsyncClient stsClient, AmazonClientException e) {
    propagateIfUnrecoverable(stsClient, e);

    // otherwise assume this is a transient error
    throw new TransientProviderException(e.getMessage(), e);
  }

  /**
   * Propagates exception as an unrecoverable error when the relevant
   * indicators are present.
   *
   * @param stsClient the AWS STS client for decoding authorization messages
   * @param e         the Amazon client exception
   */
  public static void propagateIfUnrecoverable(AWSSecurityTokenServiceAsyncClient stsClient, AmazonClientException e) {
    if (isUnrecoverable(e)) {
      if (e instanceof AmazonServiceException) {
        AmazonServiceException ase = (AmazonServiceException) e;
        if (AUTHORIZATION_ERROR_CODES.contains(ase.getErrorCode())) {
          AmazonServiceException decodedException = decodeAuthorizationMessageIfPossible(stsClient, ase);
          throw new InvalidCredentialsException(decodedException.getErrorMessage(), decodedException);
        }
      }
      throw new UnrecoverableProviderException(e.getMessage(), e);
    }
  }

  /**
   * Returns whether the specified throwable is unrecoverable, treating any throwable that is
   * not an Amazon client exception to be unrecoverable.
   *
   * @param t the throwable
   * @return whether the specified throwable is unrecoverable
   */
  public static boolean isUnrecoverable(Throwable t) {
    return !(t instanceof AmazonClientException) || isUnrecoverable((AmazonClientException) t);
  }

  /**
   * Returns whether the specified Amazon client exception is unrecoverable, considering a few
   * categories of exception as unrecoverable in addition to those which are flagged as not
   * retryable by Amazon.
   *
   * @param e the Amazon client exception
   * @return whether the specified Amazon client exception is unrecoverable
   */
  public static boolean isUnrecoverable(AmazonClientException e) {
    if (e instanceof AmazonServiceException) {
      AmazonServiceException ase = (AmazonServiceException) e;
      if (AUTHORIZATION_ERROR_CODES.contains(ase.getErrorCode())) {
        return true;
      }

      // All exceptions that represent client errors are unrecoverable because the request itself is wrong
      // See {@see AmazonServiceException#ErrorType}
      // * OperationNotPermitted exception is unrecoverable. This can happen when terminating an
      //   instance that has termination protection enabled, or trying to detach the primary
      //   network interface (eth0) from an instance.
      // * Unsupported exception is also unrecoverable, since it represents an unsupported request.
      //   See docs at http://docs.aws.amazon.com/AWSEC2/latest/APIReference/errors-overview.html
      // * InvalidParameterValue is unrecoverable, as one of the parameters supplied by the user is invalid.
      // * PendingVerification is unrecoverable since it can take up to 2 hours for verification to complete.
      if (ase.getErrorType() == AmazonServiceException.ErrorType.Client ||
          "OperationNotPermitted".equals(ase.getErrorCode()) ||
          "Unsupported".equals(ase.getErrorCode()) ||
          "InvalidParameterValue".equals(ase.getErrorCode()) ||
          "PendingVerification".equals(ase.getErrorCode())) {
        return true;
      }

      // Consider instance limits, insufficient capacity, volume limits, and internal error as unrecoverable
      if (INSTANCE_LIMIT_EXCEEDED.equals(ase.getErrorCode()) ||
          INSUFFICIENT_INSTANCE_CAPACITY.equals(ase.getErrorCode()) ||
          VOLUME_LIMIT_EXCEEDED.equals(ase.getErrorCode()) ||
          INTERNAL_ERROR.equals(ase.getErrorCode())) {
        return true;
      }
    }

    return !e.isRetryable();
  }

  /**
   * Decodes authorization messages if possible. Will not fail if the messages cannot be decoded.
   *
   * @param stsClient the client to use to decode the authorization messages
   * @param e         the exception to decode the message from
   * @return the exception with a decoded message, or as is if the message could not be decoded
   */
  public static AmazonServiceException decodeAuthorizationMessageIfPossible(
      AWSSecurityTokenServiceAsyncClient stsClient,
      AmazonServiceException e) {

    String errorMessage = e.getErrorMessage();
    if (stsClient != null && AUTHORIZATION_ERROR_CODES.contains(e.getErrorCode()) &&
        errorMessage.contains(ENCODED_MESSAGE_STRING)) {
      try {
        int indexOfEncodedMessage = errorMessage.indexOf(ENCODED_MESSAGE_STRING);
        String encodedErrorMessage = errorMessage.substring(
            errorMessage.indexOf(ENCODED_MESSAGE_STRING) + ENCODED_MESSAGE_STRING.length());
        DecodeAuthorizationMessageRequest request = new DecodeAuthorizationMessageRequest();
        request.setEncodedMessage(encodedErrorMessage);
        e.setErrorMessage(errorMessage.substring(0, indexOfEncodedMessage) + "Decoded authorization message: " +
            stsClient.decodeAuthorizationMessage(request).getDecodedMessage());
        LOG.debug("Successfully decoded authorization message.");
      } catch (AmazonClientException clientException) {
        LOG.warn("Unable to decode authorization message.");
        LOG.debug("Unable to decode authorization message.", e);
      }
    }

    return e;
  }

  /**
   * Returns whether the specified throwable is an {@code AmazonServiceException} with
   * the specified error code.
   *
   * @param throwable the throwable
   * @param errorCode the error code
   * @return whether the specified throwable is an {@code AmazonServiceException} with
   * the specified error code
   */
  public static boolean isAmazonServiceException(Throwable throwable, String errorCode) {
    return (throwable instanceof AmazonServiceException)
        && errorCode.equals(((AmazonServiceException) throwable).getErrorCode());
  }

  /**
   * Converts a set of exceptions to plugin error conditions. These conditions
   * will be added to the provided accumulator. Returns whether an unrecoverable
   * error is present in provided exceptions.
   *
   * @param template    the EC2 instance template
   * @param exceptions  the list of encountered exceptions
   * @param accumulator the exception condition accumulator to add to
   * @return whether an unrecoverable exception is present
   */
  private static boolean addErrors(EC2InstanceTemplate template,
      Set<Exception> exceptions,
      PluginExceptionConditionAccumulator accumulator) {
    boolean hasUnrecoverableExceptions = false;

    // Only report each error code once
    Map<String, AmazonServiceException> awsExceptions = Maps.newHashMap();
    for (Exception e : exceptions) {
      hasUnrecoverableExceptions = hasUnrecoverableExceptions || isUnrecoverable(e);
      if (e instanceof AmazonServiceException) {
        AmazonServiceException awsEx = (AmazonServiceException) e;
        awsExceptions.put(awsEx.getErrorCode(), awsEx);
      } else {
        accumulator.addError(toExceptionInfoMap(e.getMessage(), "N/A", "N/A"));
      }
    }

    for (AmazonServiceException awsException : awsExceptions.values()) {
      accumulator.addError(toExceptionInfoMap(template, awsException));
    }

    return hasUnrecoverableExceptions;
  }

  private static void addStateReasonErrors(EC2InstanceTemplate template,
      Set<StateReason> stateReasons,
      PluginExceptionConditionAccumulator accumulator) {
    for (StateReason stateReason : stateReasons) {
      String stateReasonCode = stateReason.getCode();
      String stateReasonMessage = stateReason.getMessage();

      String message = "Instance(s) were unexpectedly terminated";
      if (stateReasonCode.equals(VOLUME_LIMIT_EXCEEDED_STATE_REASON)) {
        message = String.format("Instance(s) were terminated due to volume limits for %s volume type",
            template.getEbsVolumeType());
      }

      accumulator.addError(toExceptionInfoMap(message, stateReasonCode, stateReasonMessage));
    }
  }

  private static Map<String, String> toExceptionInfoMap(EC2InstanceTemplate template,
      AmazonServiceException ex) {
    String awsErrorCode = ex.getErrorCode();
    String message = "Encountered AWS exception";

    // give a clearer message on more common AWS exceptions
    switch (awsErrorCode) {
      case INSUFFICIENT_INSTANCE_CAPACITY:
        message = String.format("AWS does not have available capacity for instance type %s",
            template.getType());
        break;
      case INSTANCE_LIMIT_EXCEEDED:
        message = String.format("Exceeded instance limit for instance type %s",
            template.getType());
        break;
      case REQUEST_LIMIT_EXCEEDED:
        message = "API request limit exceeded";
        break;
      default:
        break;
    }

    return toExceptionInfoMap(message, ex.getErrorCode(), ex.getErrorMessage());
  }

  private static Map<String, String> toExceptionInfoMap(String message, String awsErrorCode,
      String awsErrorMessage) {
    return ImmutableMap.of(
        "message", message,
        "awsErrorCode", awsErrorCode,
        "awsErrorMessage", awsErrorMessage
    );
  }

  /**
   * Private constructor to prevent instantiation.
   */
  private AWSExceptions() {
  }
} 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;
  }
} public class EC2Instance extends AbstractComputeInstance<EC2InstanceTemplate, Instance> {

  /**
   * The list of display properties (including inherited properties).
   */
  private static final List<DisplayProperty> DISPLAY_PROPERTIES =
      DisplayPropertiesUtil.asDisplayPropertyList(EC2InstanceDisplayPropertyToken.values());

  /**
   * Returns the list of display properties for an EC2 instance, including inherited properties.
   *
   * @return the list of display properties for an EC2 instance, including inherited properties
   */
  public static List<DisplayProperty> getDisplayProperties() {
    return DISPLAY_PROPERTIES;
  }

  /**
   * EC2 compute instance display properties.
   */
  public enum EC2InstanceDisplayPropertyToken implements DisplayPropertyToken {

    /**
     * The architecture of the image.
     */
    ARCHITECTURE(new SimpleDisplayPropertyBuilder()
        .displayKey("architecture")
        .name("Architecture")
        .defaultDescription("The architecture of the image.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getArchitecture();
      }
    },

    /**
     * Whether the instance is optimized for EBS I/O.
     */
    EBS_OPTIMIZED(new SimpleDisplayPropertyBuilder()
        .displayKey("ebsOptimized")
        .name("EBS-optimized")
        .defaultDescription("Whether the instance is optimized for EBS I/O.")
        .widget(DisplayProperty.Widget.CHECKBOX)
        .type(DisplayProperty.Type.BOOLEAN)
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getEbsOptimized().toString();
      }
    },

    /**
     * The hypervisor type of the instance.
     */
    HYPERVISOR(new SimpleDisplayPropertyBuilder()
        .displayKey("hypervisor")
        .name("Hypervisor")
        .defaultDescription("The hypervisor type of the instance.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getHypervisor();
      }
    },

    /**
     * The ID of the AMI used to launch the instance.
     */
    IMAGE_ID(new SimpleDisplayPropertyBuilder()
        .displayKey("imageId")
        .name("Image (AMI) ID")
        .defaultDescription("The ID of the AMI used to launch the instance.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getImageId();
      }
    },

    /**
     * The ID of the instance.
     */
    INSTANCE_ID(new SimpleDisplayPropertyBuilder()
        .displayKey("instanceId")
        .name("Instance ID")
        .defaultDescription("The ID of the instance.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getInstanceId();
      }
    },

    /**
     * The instance lifecycle (e.g. spot).
     */
    INSTANCE_LIFECYCLE(new SimpleDisplayPropertyBuilder()
        .displayKey("instanceLifecycle")
        .name("Lifecycle")
        .defaultDescription("The instance lifecycle (e.g. spot).")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getInstanceLifecycle();
      }
    },

    /**
     * The instance type.
     */
    INSTANCE_TYPE(new SimpleDisplayPropertyBuilder()
        .displayKey("instanceType")
        .name("Instance type")
        .defaultDescription("The instance type.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getInstanceType();
      }
    },

    /**
     * The name of the key pair, if this instance was launched with an associated key pair.
     */
    KEY_PAIR(new SimpleDisplayPropertyBuilder()
        .displayKey("keyName")
        .name("Key pair name")
        .defaultDescription("The name of the key pair, if this instance was launched with an associated key pair.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getKeyName();
      }
    },

    /**
     * The time the instance was launched.
     */
    LAUNCH_TIME(new SimpleDisplayPropertyBuilder()
        .displayKey("launchTime")
        .name("Launch time")
        .defaultDescription("The time the instance was launched.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        // TODO use appropriate date formatting
        return instance.getLaunchTime().toString();
      }
    },

    /**
     * The platform of the instance (Windows for Windows instances; otherwise blank).
     */
    PLATFORM(new SimpleDisplayPropertyBuilder()
        .displayKey("platform")
        .name("Platform")
        .defaultDescription("The platform of the instance (Windows for Windows instances; otherwise blank).")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getPlatform();
      }
    },

    /**
     * The private DNS name assigned to the instance.
     */
    PRIVATE_DNS_NAME(new SimpleDisplayPropertyBuilder()
        .displayKey("privateDnsName")
        .name("Private DNS")
        .defaultDescription("The private DNS name assigned to the instance.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getPrivateDnsName();
      }
    },

    /**
     * The private IP address assigned to the instance.
     */
    PRIVATE_IP_ADDRESS(new SimpleDisplayPropertyBuilder()
        .displayKey("privateIpAddress")
        .name("Private IP")
        .defaultDescription("The private IP address assigned to the instance.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getPrivateIpAddress();
      }
    },

    /**
     * The public DNS name assigned to the instance.
     */
    PUBLIC_DNS_NAME(new SimpleDisplayPropertyBuilder()
        .displayKey("publicDnsName")
        .name("Public DNS")
        .defaultDescription("The public DNS name assigned to the instance.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getPublicDnsName();
      }
    },

    /**
     * The public IP address assigned to the instance.
     */
    PUBLIC_IP_ADDRESS(new SimpleDisplayPropertyBuilder()
        .displayKey("publicIpAddress")
        .name("Public IP")
        .defaultDescription("The public IP address assigned to the instance.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getPublicIpAddress();
      }
    },

    /**
     * The root device name (for example, /dev/sda1).
     */
    ROOT_DEVICE_NAME(new SimpleDisplayPropertyBuilder()
        .displayKey("rootDeviceName")
        .name("Root device")
        .defaultDescription("The root device name (for example, /dev/sda1).")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getRootDeviceName();
      }
    },

    /**
     * The root device type used by the AMI.
     */
    ROOT_DEVICE_TYPE(new SimpleDisplayPropertyBuilder()
        .displayKey("rootDeviceType")
        .name("Root device type")
        .defaultDescription("The root device type used by the AMI.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getRootDeviceType();
      }
    },

    /**
     * Whether to enable an instance launched in a VPC to perform NAT.
     */
    SOURCE_DEST_CHECK(new SimpleDisplayPropertyBuilder()
        .displayKey("sourceDestCheck")
        .name("Source/dest. check")
        .defaultDescription("Whether to enable an instance launched in a VPC to perform NAT.")
        .widget(DisplayProperty.Widget.CHECKBOX)
        .type(DisplayProperty.Type.BOOLEAN)
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        Boolean sourceDestCheck = instance.getSourceDestCheck();
        return Boolean.toString(sourceDestCheck != null && sourceDestCheck);
      }
    },

    /**
     * Whether the instance is a Spot Instance.
     */
    SPOT(new SimpleDisplayPropertyBuilder()
        .displayKey("spot")
        .name("Spot Instance")
        .defaultDescription("Whether the instance is a Spot Instance.")
        .widget(DisplayProperty.Widget.CHECKBOX)
        .type(DisplayProperty.Type.BOOLEAN)
        .sensitive(false)
        .build()) {
      @SuppressWarnings("PMD.EmptyCatchBlock")
      @Override
      protected String getPropertyValue(Instance instance) {
        boolean spot = false;
        try {
          InstanceLifecycleType instanceLifecycleType =
              InstanceLifecycleType.fromValue(instance.getInstanceLifecycle());
          spot = InstanceLifecycleType.Spot.equals(instanceLifecycleType);
        } catch (IllegalArgumentException ignore) {
        }
        return String.valueOf(spot);
      }
    },

    /**
     * Whether enhanced networking is enabled.
     */
    SRIOV_NET_SUPPORT(new SimpleDisplayPropertyBuilder()
        .displayKey("sriovNetSupport")
        .name("Enhanced networking (SR-IOV)")
        .defaultDescription("Whether enhanced networking with the " +
            "Intel 82599 Virtual Function interface is enabled.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getSriovNetSupport();
      }
    },

    /**
     * The name and IDs of all the security groups attached to this instance.
     */
    SECURITY_GROUPS(new SimpleDisplayPropertyBuilder()
        .displayKey("securityGroups")
        .name("Security Groups")
        .defaultDescription("Security group names and IDs attached to this instance.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        List<String> result = Lists.newArrayList();
        for (GroupIdentifier group : instance.getSecurityGroups()) {
          result.add(String.format("%s(%s)", group.getGroupName(), group.getGroupId()));
        }
        return Joiner.on(", ").join(result);
      }
    },

    /**
     * The raw IDs of all the security groups attached to this instance. This is meant to be used when pushing
     * instance properties back into the instance template.
     */
    SECURITY_GROUPS_IDS_RAW(new SimpleDisplayPropertyBuilder()
        .displayKey("securityGroupsIdsRaw")
        .name("Security Groups")
        .defaultDescription("Security group IDs attached to this instance.")
        .sensitive(false)
        .hidden(true)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        // Sorted to avoid false detection of modification
        Set<String> result = Sets.newTreeSet();
        for (GroupIdentifier group : instance.getSecurityGroups()) {
          result.add(group.getGroupId());
        }
        return Joiner.on(",").join(result);
      }
    },


    /**
     * The ID of the subnet in which the instance is running.
     */
    SUBNET_ID(new SimpleDisplayPropertyBuilder()
        .displayKey("subnetId")
        .name("Subnet ID")
        .defaultDescription("The ID of the subnet in which the instance is running.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getSubnetId();
      }
    },

    /**
     * The availability zone in which the instance is running.
     */
    AVAILABILITY_ZONE(new SimpleDisplayPropertyBuilder()
        .displayKey("availabilityZone")
        .name("Availability Zone")
        .defaultDescription("The ID of the availability zone in which the instance is running.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getPlacement().getAvailabilityZone();
      }
    },

    /**
     * The name of the placement group used by this instance.
     */
    PLACEMENT_GROUP_NAME(new SimpleDisplayPropertyBuilder()
        .displayKey("placementGroupName")
        .name("Placement Group")
        .defaultDescription("Placement group name in which the instance is running.")
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getPlacement().getGroupName();
      }
    },

    /**
     * The tenancy type of the instance.
     */
    TENANCY(new SimpleDisplayPropertyBuilder()
        .displayKey("tenancy")
        .name("Tenancy")
        .defaultDescription("Tenancy of the instance.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getPlacement().getTenancy();
      }
    },

    /**
     * The virtualization type of the instance.
     */
    VIRTUALIZATION_TYPE(new SimpleDisplayPropertyBuilder()
        .displayKey("virtualizationType")
        .name("Virtualization")
        .defaultDescription("The virtualization type of the instance.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getVirtualizationType();
      }
    },

    /**
     * The ID of the VPC in which the instance is running.
     */
    VPC_ID(new SimpleDisplayPropertyBuilder()
        .displayKey("vpcId")
        .name("VPC ID")
        .defaultDescription("The ID of the VPC in which the instance is running.")
        .sensitive(false)
        .build()) {
      @Override
      protected String getPropertyValue(Instance instance) {
        return instance.getVpcId();
      }
    };

    /**
     * The display property.
     */
    private final DisplayProperty displayProperty;

    /**
     * Creates an EC2 instance display property token with the specified parameters.
     *
     * @param displayProperty the display property
     */
    EC2InstanceDisplayPropertyToken(DisplayProperty displayProperty) {
      this.displayProperty = displayProperty;
    }

    /**
     * Returns the value of the property from the specified instance.
     *
     * @param instance the instance
     * @return the value of the property from the specified instance
     */
    protected abstract String getPropertyValue(Instance instance);

    @Override
    public DisplayProperty unwrap() {
      return displayProperty;
    }

  }

  /**
   * EC2 virtualization types.
   *
   * @see <a href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/virtualization_types.html"/>
   */
  private enum EC2VirtualizationType {

    /**
     * Hardware virtual machine (HVM).
     */
    HVM("hvm", VirtualizationType.HARDWARE_ASSISTED),

    /**
     * Paravirtual (PV).
     */
    PV("paravirtual", VirtualizationType.PARAVIRTUALIZATION);

    /**
     * The map from type strings to EC2 virtualization types.
     */
    private static final Map<String, EC2VirtualizationType> TYPES_BY_KEY =
        ImmutableMap.<String, EC2VirtualizationType>builder()
            .put(HVM.getKey(), HVM)
            .put(PV.getKey(), PV)
            .build();

    /**
     * Returns the EC2 virtualization type identified by the specified key, or <code>null</code>
     * if there is no such EC2 virtualization type.
     *
     * @param key the type string
     * @return the EC2 virtualization type identified by the specified key, or <code>null</code>
     * if there is no such EC2 virtualization type
     */
    private static EC2VirtualizationType forKey(String key) {
      return TYPES_BY_KEY.get(key);
    }

    /**
     * The virtualization type string, as returned by {@code Instance.getVirtualizationType()}.
     */
    private final String key;

    /**
     * The provider-agnostic virtualization type.
     */
    private final VirtualizationType virtualizationType;

    /**
     * Creates an EC2 virtualization type with the specified parameters.
     *
     * @param key                the virtualization type string, as returned by
     *                           {@code Instance.getVirtualizationType()}.
     * @param virtualizationType the provider-agnostic virtualization type
     */
    EC2VirtualizationType(String key, VirtualizationType virtualizationType) {
      this.key = key;
      this.virtualizationType = virtualizationType;
    }

    /**
     * Returns the virtualization type string, as returned by
     * {@code Instance.getVirtualizationType()}.
     *
     * @return the virtualization type string, as returned by
     * {@code Instance.getVirtualizationType()}
     */
    public String getKey() {
      return key;
    }

    /**
     * Returns the provider-agnostic virtualization type.
     *
     * @return the provider-agnostic virtualization type
     */
    public VirtualizationType getVirtualizationType() {
      return virtualizationType;
    }
  }

  /**
   * The resource type representing an EC2 instance.
   */
  public static final Type TYPE = new ResourceType("EC2Instance");

  /**
   * Returns the private IP address of the specified EC2 instance.
   *
   * @param instance the instance
   * @return the private IP address of the specified EC2 instance
   * @throws IllegalArgumentException if the instance does not have a valid private IP address
   */
  public static InetAddress getPrivateIpAddress(Instance instance) {
    Preconditions.checkNotNull(instance, "instance is null");
    InetAddress privateIpAddress = null;
    try {
      String privateIpString = instance.getPrivateIpAddress();
      if (privateIpString != null && !privateIpString.isEmpty()) {
        privateIpAddress = InetAddress.getByName(privateIpString);
      }
    } catch (UnknownHostException e) {
      throw new IllegalArgumentException("Invalid private IP address", e);
    }
    return privateIpAddress;
  }

  /**
   * Returns the provider-agnostic virtualization type for the specified EC2 instance.
   *
   * @param instance the instance
   * @return the provider-agnostic virtualization type for the specified EC2 instance
   */
  private static VirtualizationType getVirtualizationType(Instance instance) {
    String key = instance.getVirtualizationType();
    EC2VirtualizationType ec2VirtualizationType = EC2VirtualizationType.forKey(key);
    return (ec2VirtualizationType == null)
        ? VirtualizationType.UNKNOWN
        : ec2VirtualizationType.getVirtualizationType();
  }

  /**
   * Creates an EC2 compute instance with the specified parameters.
   *
   * @param template        the template from which the instance was created
   * @param instanceId      the instance identifier
   * @param instanceDetails the provider-specific instance details
   * @throws IllegalArgumentException if the instance does not have a valid private IP address
   */
  public EC2Instance(EC2InstanceTemplate template, String instanceId, Instance instanceDetails) {
    super(template, instanceId, getPrivateIpAddress(instanceDetails),
        getVirtualizationType(instanceDetails), instanceDetails);
  }

  @Override
  public Type getType() {
    return TYPE;
  }

  @Override
  public Map<String, String> getProperties() {
    Map<String, String> properties = Maps.newHashMap();
    Instance instance = unwrap();
    if (instance != null) {
      for (EC2InstanceDisplayPropertyToken propertyToken : EC2InstanceDisplayPropertyToken.values()) {
        properties.put(propertyToken.unwrap().getDisplayKey(), propertyToken.getPropertyValue(instance));
      }
    }
    return properties;
  }

  /**
   * Sets the EC2 instance.
   *
   * @param instance the EC2 instance
   * @throws IllegalArgumentException if the instance does not have a valid private IP address
   */
  protected void setInstance(Instance instance) {
    super.setDetails(instance);
    InetAddress privateIpAddress = getPrivateIpAddress(instance);
    setPrivateIpAddress(privateIpAddress);
    VirtualizationType virtualizationType = getVirtualizationType(instance);
    setVirtualizationType(virtualizationType);
  }
} @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 class LiveTestProperties {

  private String testRegion;
  private String testSubnet;
  private String testSecurityGroup;
  private String testKmsKeyArn;

  // live test properties for delegated role access
  private List<RoleConfiguration> delegatedRoleConfigurations;
  private String externalAccountRegion;
  private String externalAccountSubnetId;
  private String externalAccountSecurityGroup;
  private String externalAccountKmsKeyArn;

  private LiveTestProperties(
      String testRegion, String testSubnet, String testSecurityGroup,
      String testKmsKeyArn, List<RoleConfiguration> roleConfigurations,
      String externalAccountRegion, String externalAccountSubnetId,
      String externalAccountSecurityGroup, String externalAccountKmsKeyArn) {
    this.testRegion = testRegion;
    this.testSubnet = testSubnet;
    this.testSecurityGroup = testSecurityGroup;
    this.testKmsKeyArn = testKmsKeyArn;
    this.delegatedRoleConfigurations = roleConfigurations;
    this.externalAccountRegion = externalAccountRegion;
    this.externalAccountSubnetId = externalAccountSubnetId;
    this.externalAccountSecurityGroup = externalAccountSecurityGroup;
    this.externalAccountKmsKeyArn = externalAccountKmsKeyArn;
  }

  private static String getProperty(Properties properties, String key) {
    String value = properties.getProperty(key);
    if (value == null) {
      fail("Could not find " + key + " in properties file");
    }
    return value;
  }

  public static LiveTestProperties loadLiveTestProperties() {
    String liveTestPropertyFile = System.getProperty("test.aws.live.file");

    if (liveTestPropertyFile == null) {
      fail("live test file property is not set");
    }

    Properties prop = new Properties();

    LiveTestProperties liveTestProperties = null;
    try(InputStream input = new FileInputStream(liveTestPropertyFile)) {
      prop.load(input);

      String testRegion = getProperty(prop, "region");
      String testSubnet = getProperty(prop, "subnet_id");
      String testSecurityGroup = getProperty(prop, "security_group");
      String testKmsKeyArn = getProperty(prop, "kms_key_arn");
      String externalAccountRegion = getProperty(prop, "external_account_region");
      String externalAccountSubnetId = getProperty(prop, "external_account_subnet_id");
      String externalAccountSecurityGroup = getProperty(prop, "external_account_security_group");
      String externalAccountKmsKeyArn = getProperty(prop, "external_account_kms_key");

      ImmutableList.Builder<RoleConfiguration> roleConfigurationsBuilder = ImmutableList.builder();
      for (int i = 0; i < 10; ++i) {
        String delegatedRoleArn = prop.getProperty("delegated_role_arn_" + i, null);
        if (delegatedRoleArn == null) {
          break;
        }
        String delegatedRoleSessionName = prop.getProperty(
            "delegated_role_session_name_" + i,
            "director-live-test");
        String delegatedRoleExternalId = prop.getProperty(
            "delegated_role_external_id_" + i,
            null);
        roleConfigurationsBuilder.add(
            new RoleConfiguration(delegatedRoleArn, delegatedRoleSessionName, delegatedRoleExternalId));
      }

      liveTestProperties = new LiveTestProperties(
          testRegion,
          testSubnet,
          testSecurityGroup,
          testKmsKeyArn,
          roleConfigurationsBuilder.build(),
          externalAccountRegion,
          externalAccountSubnetId,
          externalAccountSecurityGroup,
          externalAccountKmsKeyArn
      );
    } catch (IOException e) {
      e.printStackTrace();
      fail("Could not load live test properties file with path " + liveTestPropertyFile);
    }
    return liveTestProperties;
  }

  public String getTestRegion() {
    return testRegion;
  }

  public String getTestSubnet() {
    return testSubnet;
  }

  public String getTestSecurityGroup() {
    return testSecurityGroup;
  }

  public String getTestKmsKeyArn() {
    return testKmsKeyArn;
  }

  public List<RoleConfiguration> getDelegatedRoleConfigurations() {
    return delegatedRoleConfigurations;
  }

  public String getExternalAccountRegion() {
    return externalAccountRegion;
  }

  public String getExternalAccountSubnetId() {
    return externalAccountSubnetId;
  }

  public String getExternalAccountSecurityGroup() {
    return externalAccountSecurityGroup;
  }

  public String getExternalAccountKmsKeyArn() {
    return externalAccountKmsKeyArn;
  }
} public class TestInstanceTemplate {

  private String templateName;
  private Map<String, String> configs;
  private Map<String, String> tags;

  public void addConfig(ConfigurationPropertyToken propertyToken, String value) {
    if (value != null) {
      configs.put(propertyToken.unwrap().getConfigKey(), value);
    }
  }

  public void addTag(String tagKey, String tagValue) {
    tags.put(tagKey, tagValue);
  }

  /**
   * Constructor, will set an owner tag and instance name prefix based
   * on the current user name.
   */
  public TestInstanceTemplate() {
    configs = new LinkedHashMap<>();
    tags = new LinkedHashMap<>();

    String username = System.getProperty("user.name");
    templateName = username + "-test";

    addConfig(INSTANCE_NAME_PREFIX, templateName);
    addTag(Tags.InstanceTags.OWNER.getTagKey(), username);
  }

  public String getTemplateName() {
    return templateName;
  }

  public Map<String, String> getConfigs() {
    return configs;
  }

  public Map<String, String> getTags() {
    return tags;
  }
}
import static com.cloudera.director.aws.AWSLauncher.DEFAULT_PLUGIN_LOCALIZATION_CONTEXT;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.EBS_VOLUME_COUNT;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.EBS_VOLUME_SIZE_GIB;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.EBS_VOLUME_TYPE;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.SYSTEM_DISKS;
import static com.cloudera.director.aws.ec2.EC2TagHelper.MAX_TAGS_ALLOWED;
import static com.cloudera.director.aws.test.EC2ProviderCommon.CENTOS67_HVM_AMI_BY_REGION;
import static com.cloudera.director.aws.test.EC2ProviderCommon.waitUntilRunningOrTerminal;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.IMAGE;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.SECURITY_GROUP_IDS;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.SUBNET_ID;
import static com.cloudera.director.aws.ec2.EC2InstanceTemplate.EC2InstanceTemplateConfigurationPropertyToken.TYPE;
import static com.cloudera.director.aws.ec2.EC2ProviderConfigurationPropertyToken.REGION;
import static com.cloudera.director.aws.test.EC2ProviderCommon.getEc2Provider;
import static com.cloudera.director.aws.test.EC2ProviderCommon.isLive;
import static com.cloudera.director.aws.test.EC2ProviderCommon.putConfig;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.cloudera.director.aws.AWSCredentialsProviderChainProvider;
import com.cloudera.director.aws.AWSExceptions;
import com.cloudera.director.aws.Tags.InstanceTags;
import com.cloudera.director.aws.ec2.EC2Instance;
import com.cloudera.director.aws.ec2.EC2InstanceTemplate;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.Volume;
import com.cloudera.director.aws.test.LiveTestProperties;
import com.cloudera.director.aws.shaded.com.amazonaws.auth.AWSCredentialsProvider;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.AmazonEC2Client;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribeInstancesRequest;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.cloudera.director.aws.shaded.com.amazonaws.services.ec2.model.Filter;
import com.cloudera.director.aws.test.TestInstanceTemplate;
import com.cloudera.director.spi.v2.model.InstanceState;
import com.cloudera.director.spi.v2.model.InstanceStatus;
import com.cloudera.director.spi.v2.model.exception.AbstractPluginException;
import com.cloudera.director.spi.v2.model.exception.PluginExceptionCondition;
import com.cloudera.director.spi.v2.model.exception.UnrecoverableProviderException;
import com.cloudera.director.spi.v2.model.util.SimpleConfiguration;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.UUID;
import org.assertj.core.util.Lists;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
// (c) Copyright 2015 Cloudera, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.cloudera.director.aws.ec2.provider;





/**
 * Tests {@link EC2Provider}.
 */
public class EC2ProviderLiveTest {

  @Rule
  public ExpectedException thrown = ExpectedException.none();

  private static LiveTestProperties liveTestProperties;