/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.ambari.server.controller;

import com.google.inject.Inject;
import com.google.inject.persist.Transactional;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.Role;
import org.apache.ambari.server.RoleCommand;
import org.apache.ambari.server.actionmanager.ActionManager;
import org.apache.ambari.server.actionmanager.RequestFactory;
import org.apache.ambari.server.actionmanager.Stage;
import org.apache.ambari.server.actionmanager.StageFactory;
import org.apache.ambari.server.api.services.AmbariMetaInfo;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.controller.internal.ArtifactResourceProvider;
import org.apache.ambari.server.controller.internal.RequestImpl;
import org.apache.ambari.server.controller.internal.RequestResourceFilter;
import org.apache.ambari.server.controller.internal.RequestStageContainer;
import org.apache.ambari.server.controller.spi.ClusterController;
import org.apache.ambari.server.controller.spi.NoSuchParentResourceException;
import org.apache.ambari.server.controller.spi.NoSuchResourceException;
import org.apache.ambari.server.controller.spi.Predicate;
import org.apache.ambari.server.controller.spi.Request;
import org.apache.ambari.server.controller.spi.Resource;
import org.apache.ambari.server.controller.spi.ResourceProvider;
import org.apache.ambari.server.controller.spi.SystemException;
import org.apache.ambari.server.controller.spi.UnsupportedPropertyException;
import org.apache.ambari.server.controller.utilities.ClusterControllerHelper;
import org.apache.ambari.server.controller.utilities.PredicateBuilder;
import org.apache.ambari.server.metadata.RoleCommandOrder;
import org.apache.ambari.server.serveraction.ServerAction;
import org.apache.ambari.server.serveraction.kerberos.CreateKeytabFilesServerAction;
import org.apache.ambari.server.serveraction.kerberos.CreatePrincipalsServerAction;
import org.apache.ambari.server.serveraction.kerberos.DestroyPrincipalsServerAction;
import org.apache.ambari.server.serveraction.kerberos.FinalizeKerberosServerAction;
import org.apache.ambari.server.serveraction.kerberos.KDCType;
import org.apache.ambari.server.serveraction.kerberos.KerberosActionDataFile;
import org.apache.ambari.server.serveraction.kerberos.KerberosActionDataFileBuilder;
import org.apache.ambari.server.serveraction.kerberos.KerberosAdminAuthenticationException;
import org.apache.ambari.server.serveraction.kerberos.KerberosConfigDataFile;
import org.apache.ambari.server.serveraction.kerberos.KerberosConfigDataFileBuilder;
import org.apache.ambari.server.serveraction.kerberos.KerberosCredential;
import org.apache.ambari.server.serveraction.kerberos.KerberosInvalidConfigurationException;
import org.apache.ambari.server.serveraction.kerberos.KerberosKDCConnectionException;
import org.apache.ambari.server.serveraction.kerberos.KerberosLDAPContainerException;
import org.apache.ambari.server.serveraction.kerberos.KerberosMissingAdminCredentialsException;
import org.apache.ambari.server.serveraction.kerberos.KerberosOperationException;
import org.apache.ambari.server.serveraction.kerberos.KerberosOperationHandler;
import org.apache.ambari.server.serveraction.kerberos.KerberosOperationHandlerFactory;
import org.apache.ambari.server.serveraction.kerberos.KerberosRealmException;
import org.apache.ambari.server.serveraction.kerberos.KerberosServerAction;
import org.apache.ambari.server.serveraction.kerberos.UpdateKerberosConfigsServerAction;
import org.apache.ambari.server.stageplanner.RoleGraph;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.Clusters;
import org.apache.ambari.server.state.Config;
import org.apache.ambari.server.state.ConfigHelper;
import org.apache.ambari.server.state.Host;
import org.apache.ambari.server.state.HostState;
import org.apache.ambari.server.state.PropertyInfo;
import org.apache.ambari.server.state.SecurityState;
import org.apache.ambari.server.state.SecurityType;
import org.apache.ambari.server.state.Service;
import org.apache.ambari.server.state.ServiceComponent;
import org.apache.ambari.server.state.ServiceComponentHost;
import org.apache.ambari.server.state.StackId;
import org.apache.ambari.server.state.State;
import org.apache.ambari.server.state.kerberos.KerberosComponentDescriptor;
import org.apache.ambari.server.state.kerberos.KerberosConfigurationDescriptor;
import org.apache.ambari.server.state.kerberos.KerberosDescriptor;
import org.apache.ambari.server.state.kerberos.KerberosDescriptorFactory;
import org.apache.ambari.server.state.kerberos.KerberosIdentityDescriptor;
import org.apache.ambari.server.state.kerberos.KerberosKeytabDescriptor;
import org.apache.ambari.server.state.kerberos.KerberosPrincipalDescriptor;
import org.apache.ambari.server.state.kerberos.KerberosServiceDescriptor;
import org.apache.ambari.server.state.svccomphost.ServiceComponentHostServerActionEvent;
import org.apache.ambari.server.utils.StageUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

public class KerberosHelper {
  private static final String BASE_LOG_DIR = "/tmp/ambari";

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

  /**
   * config type which contains the property used to determine if Kerberos is enabled
   */
  private static final String SECURITY_ENABLED_CONFIG_TYPE = "cluster-env";

  /**
   * name of property which states whether kerberos is enabled
   */
  private static final String SECURITY_ENABLED_PROPERTY_NAME = "security_enabled";

  /**
   * name of the property used to hold the service check identifier value, used when creating and
   * destroying the (unique) service check identity.
   */
  private static final String SERVICE_CHECK_IDENTIFIER = "_kerberos_internal_service_check_identifier";

  @Inject
  private AmbariCustomCommandExecutionHelper customCommandExecutionHelper;

  @Inject
  private AmbariManagementController ambariManagementController;

  @Inject
  private AmbariMetaInfo ambariMetaInfo;

  @Inject
  private ActionManager actionManager;

  @Inject
  private RequestFactory requestFactory;

  @Inject
  private StageFactory stageFactory;

  @Inject
  private Clusters clusters;

  @Inject
  private ConfigHelper configHelper;

  @Inject
  private Configuration configuration;

  @Inject
  private KerberosOperationHandlerFactory kerberosOperationHandlerFactory;

  @Inject
  private KerberosDescriptorFactory kerberosDescriptorFactory;

  /**
   * Used to get kerberos descriptors associated with the cluster or stack.
   * Currently not available via injection.
   */
  private static ClusterController clusterController = null;


  /**
   * Toggles Kerberos security to enable it or remove it depending on the state of the cluster.
   * <p/>
   * The cluster "security_type" property is used to determine the security state of the cluster.
   * If the declared security type is KERBEROS, than an attempt will be make to enable Kerberos; if
   * the security type is NONE, an attempt will be made to disable Kerberos; otherwise, no operations
   * will be performed.
   * <p/>
   * It is expected tht the "kerberos-env" configuration type is available.   It is used to obtain
   * information about the Kerberos configuration, generally specific to the KDC being used.
   * <p/>
   * This process is idempotent such that it may be called several times, each time only affecting
   * items that need to be brought up to date.
   *
   * @param cluster               the relevant Cluster
   * @param securityType          the SecurityType to handle; this value is expected to be either
   *                              SecurityType.KERBEROS or SecurityType.NONE
   * @param requestStageContainer a RequestStageContainer to place generated stages, if needed -
   *                              if null a new RequestStageContainer will be created.
   * @return the updated or a new RequestStageContainer containing the stages that need to be
   * executed to complete this task; or null if no stages need to be executed.
   * @throws AmbariException
   * @throws KerberosInvalidConfigurationException if an issue occurs trying to get the
   *                                               Kerberos-specific configuration details
   * @throws KerberosOperationException
   */
  public RequestStageContainer toggleKerberos(Cluster cluster, SecurityType securityType,
                                              RequestStageContainer requestStageContainer)
      throws AmbariException, KerberosOperationException {

    KerberosDetails kerberosDetails = getKerberosDetails(cluster);

    // Update KerberosDetails with the new security type - the current one in the cluster is the "old" value
    kerberosDetails.setSecurityType(securityType);

    if (securityType == SecurityType.KERBEROS) {
      LOG.info("Configuring Kerberos for realm {} on cluster, {}", kerberosDetails.getDefaultRealm(), cluster.getClusterName());
      requestStageContainer = handle(cluster, kerberosDetails, null, null, null, requestStageContainer, new EnableKerberosHandler());
    } else if (securityType == SecurityType.NONE) {
      LOG.info("Disabling Kerberos from cluster, {}", cluster.getClusterName());
      requestStageContainer = handle(cluster, kerberosDetails, null, null, null, requestStageContainer, new DisableKerberosHandler());
    } else {
      throw new AmbariException(String.format("Unexpected security type value: %s", securityType.name()));
    }

    return requestStageContainer;
  }

  /**
   * Used to execute custom security operations which are sent as directives in URI
   *
   * @param cluster               the relevant Cluster
   * @param requestProperties     this structure is expected to hold already supported and validated directives
   *                              for the 'Cluster' resource. See ClusterResourceDefinition#getUpdateDirectives
   * @param requestStageContainer a RequestStageContainer to place generated stages, if needed -
   *                              if null a new RequestStageContainer will be created.   @return the updated or a new RequestStageContainer containing the stages that need to be
   *                              executed to complete this task; or null if no stages need to be executed.
   * @throws AmbariException
   * @throws KerberosOperationException
   * @throws KerberosInvalidConfigurationException if an issue occurs trying to get the
   *                                               Kerberos-specific configuration details
   */
  public RequestStageContainer executeCustomOperations(Cluster cluster, Map<String, String> requestProperties,
                                                       RequestStageContainer requestStageContainer)
      throws AmbariException, KerberosOperationException {

    if (requestProperties != null) {

      for (SupportedCustomOperation operation : SupportedCustomOperation.values()) {
        if (requestProperties.containsKey(operation.name().toLowerCase())) {
          String value = requestProperties.get(operation.name().toLowerCase());

          // The operation specific logic is kept in one place and described here
          switch (operation) {
            case REGENERATE_KEYTABS:
              if (cluster.getSecurityType() != SecurityType.KERBEROS) {
                throw new AmbariException(String.format("Custom operation %s can only be requested with the security type cluster property: %s", operation.name(), SecurityType.KERBEROS.name()));
              }

              if ("true".equalsIgnoreCase(value) || "all".equalsIgnoreCase(value)) {
                requestStageContainer = handle(cluster, getKerberosDetails(cluster), null, null, null,
                    requestStageContainer, new CreatePrincipalsAndKeytabsHandler(true));
              } else if ("missing".equalsIgnoreCase(value)) {
                requestStageContainer = handle(cluster, getKerberosDetails(cluster), null, null, null,
                    requestStageContainer, new CreatePrincipalsAndKeytabsHandler(false));
              } else {
                throw new AmbariException(String.format("Unexpected directive value: %s", value));
              }

              break;

            default: // No other operations are currently supported
              throw new AmbariException(String.format("Custom operation not supported: %s", operation.name()));
          }
        }
      }
    }

    return requestStageContainer;
  }


  /**
   * Ensures the set of filtered principals and keytabs exist on the cluster.
   * <p/>
   * No configurations will be altered as a result of this operation, however principals and keytabs
   * may be updated or created.
   * <p/>
   * It is expected that the "kerberos-env" configuration type is available.  It is used to obtain
   * information about the relevant KDC.  For example, the "kdc_type" property is used to determine
   * what type of KDC is being used so that the appropriate actions maybe taken in order interact
   * with it properly.
   * <p/>
   * It is expected tht the "kerberos-env" configuration type is available.   It is used to obtain
   * information about the Kerberos configuration, generally specific to the KDC being used.
   *
   * @param cluster                        the relevant Cluster
   * @param serviceComponentFilter         a Map of service names to component names indicating the
   *                                       relevant set of services and components - if null, no
   *                                       filter is relevant; if empty, the filter indicates no
   *                                       relevant services or components
   * @param identityFilter                 a Collection of identity names indicating the relevant
   *                                       identities - if null, no filter is relevant; if empty,
   *                                       the filter indicates no relevant identities
   * @param hostsToForceKerberosOperations a set of host names on which it is expected that the
   *                                       Kerberos client is or will be in the INSTALLED state by
   *                                       the time the operations targeted for them are to be
   *                                       executed - if empty or null, this no hosts will be
   *                                       "forced"
   * @param requestStageContainer          a RequestStageContainer to place generated stages, if
   *                                       needed - if null a new RequestStageContainer will be
   *                                       created.
   * @return the updated or a new RequestStageContainer containing the stages that need to be
   * executed to complete this task; or null if no stages need to be executed.
   * @throws AmbariException
   * @throws KerberosOperationException
   * @throws KerberosInvalidConfigurationException if an issue occurs trying to get the
   *                                               Kerberos-specific configuration details
   */
  public RequestStageContainer ensureIdentities(Cluster cluster, Map<String, ? extends Collection<String>> serviceComponentFilter,
                                                Collection<String> identityFilter, Set<String> hostsToForceKerberosOperations,
                                                RequestStageContainer requestStageContainer)
      throws AmbariException, KerberosOperationException {
    return handle(cluster, getKerberosDetails(cluster), serviceComponentFilter, identityFilter,
        hostsToForceKerberosOperations, requestStageContainer, new CreatePrincipalsAndKeytabsHandler(false));
  }

  /**
   * Deletes the set of filtered principals and keytabs from the cluster.
   * <p/>
   * No configurations will be altered as a result of this operation, however principals and keytabs
   * may be removed.
   * <p/>
   * It is expected that the "kerberos-env" configuration type is available.  It is used to obtain
   * information about the relevant KDC.  For example, the "kdc_type" property is used to determine
   * what type of KDC is being used so that the appropriate actions maybe taken in order interact
   * with it properly.
   * <p/>
   * It is expected tht the "kerberos-env" configuration type is available.   It is used to obtain
   * information about the Kerberos configuration, generally specific to the KDC being used.
   *
   * @param cluster                the relevant Cluster
   * @param serviceComponentFilter a Map of service names to component names indicating the relevant
   *                               set of services and components - if null, no filter is relevant;
   *                               if empty, the filter indicates no relevant services or components
   * @param identityFilter         a Collection of identity names indicating the relevant identities -
   *                               if null, no filter is relevant; if empty, the filter indicates no
   *                               relevant identities
   * @param requestStageContainer  a RequestStageContainer to place generated stages, if needed -
   *                               if null a new RequestStageContainer will be created.
   * @return the updated or a new RequestStageContainer containing the stages that need to be
   * executed to complete this task; or null if no stages need to be executed.
   * @throws AmbariException
   * @throws KerberosOperationException
   * @throws KerberosInvalidConfigurationException if an issue occurs trying to get the
   *                                               Kerberos-specific configuration details
   */
  public RequestStageContainer deleteIdentities(Cluster cluster, Map<String, ? extends Collection<String>> serviceComponentFilter,
                                                Collection<String> identityFilter, RequestStageContainer requestStageContainer)
      throws AmbariException, KerberosOperationException {
    return handle(cluster, getKerberosDetails(cluster), serviceComponentFilter, identityFilter, null,
        requestStageContainer, new DeletePrincipalsAndKeytabsHandler());
  }

  /**
   * Updates the relevant configurations for the given Service.
   * <p/>
   * If the relevant service and its components have Kerberos descriptors, configuration values from
   * the descriptors are used to update the relevant configuration sets.
   *
   * @param cluster              the relevant Cluster
   * @param serviceComponentHost the ServiceComponentHost
   * @throws AmbariException
   */
  public void configureService(Cluster cluster, ServiceComponentHost serviceComponentHost)
      throws AmbariException, KerberosInvalidConfigurationException {

    KerberosDetails kerberosDetails = getKerberosDetails(cluster);

    // Set properties...
    String serviceName = serviceComponentHost.getServiceName();
    KerberosDescriptor kerberosDescriptor = getKerberosDescriptor(cluster);
    KerberosServiceDescriptor serviceDescriptor = kerberosDescriptor.getService(serviceName);

    if (serviceDescriptor != null) {
      Map<String, String> kerberosDescriptorProperties = kerberosDescriptor.getProperties();
      Map<String, Map<String, String>> kerberosConfigurations = new HashMap<String, Map<String, String>>();
      Map<String, Map<String, String>> configurations = calculateConfigurations(cluster,
          serviceComponentHost.getHostName(), kerberosDescriptorProperties);

      Map<String, KerberosComponentDescriptor> componentDescriptors = serviceDescriptor.getComponents();
      for (KerberosComponentDescriptor componentDescriptor : componentDescriptors.values()) {
        if (componentDescriptor != null) {
          Map<String, Map<String, String>> identityConfigurations;
          List<KerberosIdentityDescriptor> identities;

          identities = serviceDescriptor.getIdentities(true);
          identityConfigurations = getConfigurations(identities);
          if (identityConfigurations != null) {
            for (Map.Entry<String, Map<String, String>> entry : identityConfigurations.entrySet()) {
              mergeConfigurations(kerberosConfigurations, entry.getKey(), entry.getValue(), configurations);
            }
          }

          identities = componentDescriptor.getIdentities(true);
          identityConfigurations = getConfigurations(identities);
          if (identityConfigurations != null) {
            for (Map.Entry<String, Map<String, String>> entry : identityConfigurations.entrySet()) {
              mergeConfigurations(kerberosConfigurations, entry.getKey(), entry.getValue(), configurations);
            }
          }

          mergeConfigurations(kerberosConfigurations,
              componentDescriptor.getConfigurations(true), configurations);
        }
      }

      setAuthToLocalRules(kerberosDescriptor, cluster, kerberosDetails.getDefaultRealm(), configurations, kerberosConfigurations);

      for (Map.Entry<String, Map<String, String>> entry : kerberosConfigurations.entrySet()) {
        configHelper.updateConfigType(cluster, ambariManagementController, entry.getKey(), entry.getValue(),
            ambariManagementController.getAuthName(), String.format("Enabling Kerberos for %s", serviceName));
      }
    }
  }

  /**
   * Create a unique identity to use for testing the general Kerberos configuration.
   *
   * @param cluster               the relevant Cluster
   * @param commandParamsStage    the command parameters map that will be sent to the agent side command
   * @param requestStageContainer a RequestStageContainer to place generated stages, if needed -
   *                              if null a new RequestStageContainer will be created.
   * @return the updated or a new RequestStageContainer containing the stages that need to be
   * executed to complete this task; or null if no stages need to be executed.
   */
  public RequestStageContainer createTestIdentity(Cluster cluster, Map<String, String> commandParamsStage,
                                                  RequestStageContainer requestStageContainer)
      throws KerberosOperationException, AmbariException {
    return handleTestIdentity(cluster, getKerberosDetails(cluster), commandParamsStage, requestStageContainer, new CreatePrincipalsAndKeytabsHandler(false));
  }

  /**
   * Deletes the unique identity to use for testing the general Kerberos configuration.
   *
   * @param cluster               the relevant Cluster
   * @param commandParamsStage    the command parameters map that will be sent to the agent side command
   * @param requestStageContainer a RequestStageContainer to place generated stages, if needed -
   *                              if null a new RequestStageContainer will be created.
   * @return the updated or a new RequestStageContainer containing the stages that need to be
   * executed to complete this task; or null if no stages need to be executed.
   */
  public RequestStageContainer deleteTestIdentity(Cluster cluster, Map<String, String> commandParamsStage,
                                                  RequestStageContainer requestStageContainer)
      throws KerberosOperationException, AmbariException {
    requestStageContainer = handleTestIdentity(cluster, getKerberosDetails(cluster), commandParamsStage, requestStageContainer, new DeletePrincipalsAndKeytabsHandler());

    // Clear the Kerberos service check identifier
    setKerberosServiceCheckIdentifier(cluster, null);

    return requestStageContainer;
  }

  /**
   * Validate the KDC admin credentials.
   *
   * @param cluster associated cluster
   * @throws AmbariException if any other error occurs while trying to validate the credentials
   */
  public void validateKDCCredentials(Cluster cluster) throws KerberosMissingAdminCredentialsException,
      KerberosAdminAuthenticationException,
      KerberosInvalidConfigurationException,
      AmbariException {
    String credentials = getEncryptedAdministratorCredentials(cluster);
    if (credentials == null) {
      throw new KerberosMissingAdminCredentialsException(
          "Missing KDC administrator credentials.\n" +
              "The KDC administrator credentials must be set in session by updating the relevant Cluster resource." +
              "This may be done by issuing a PUT to the api/v1/clusters/(cluster name) API entry point with the following payload:\n" +
              "{\n" +
              "  \"session_attributes\" : {\n" +
              "    \"kerberos_admin\" : {\"principal\" : \"(PRINCIPAL)\", \"password\" : \"(PASSWORD)\"}\n" +
              "  }\n" +
              "}"
      );
    } else {
      KerberosDetails kerberosDetails = getKerberosDetails(cluster);
      KerberosOperationHandler operationHandler = kerberosOperationHandlerFactory.getKerberosOperationHandler(kerberosDetails.getKdcType());

      if (operationHandler == null) {
        throw new AmbariException("Failed to get an appropriate Kerberos operation handler.");
      } else {
        byte[] key = Integer.toHexString(cluster.hashCode()).getBytes();
        KerberosCredential kerberosCredentials = KerberosCredential.decrypt(credentials, key);

        boolean missingCredentials = false;
        try {
          operationHandler.open(kerberosCredentials, kerberosDetails.getDefaultRealm(), kerberosDetails.getKerberosEnvProperties());
          // todo: this is really odd that open doesn't throw an exception if the credentials are missing
          missingCredentials = ! operationHandler.testAdministratorCredentials();
        } catch (KerberosAdminAuthenticationException e) {
          throw new KerberosAdminAuthenticationException(
              "Invalid KDC administrator credentials.\n" +
                  "The KDC administrator credentials must be set in session by updating the relevant Cluster resource." +
                  "This may be done by issuing a PUT to the api/v1/clusters/(cluster name) API entry point with the following payload:\n" +
                  "{\n" +
                  "  \"session_attributes\" : {\n" +
                  "    \"kerberos_admin\" : {\"principal\" : \"(PRINCIPAL)\", \"password\" : \"(PASSWORD)\"}\n" +
                  "  }\n" +
                  "}", e);
        } catch (KerberosKDCConnectionException e) {
          throw new KerberosInvalidConfigurationException(
              "Failed to connect to KDC - " + e.getMessage() + "\n" +
                  "Update the KDC settings in krb5-conf and kerberos-env configurations to correct this issue.",
              e);
        } catch (KerberosRealmException e) {
          throw new KerberosInvalidConfigurationException(
              "Failed to find a KDC for the specified realm - " + e.getMessage() + "\n" +
                  "Update the KDC settings in krb5-conf and kerberos-env configurations to correct this issue.",
              e);
        } catch (KerberosLDAPContainerException e) {
          throw new KerberosInvalidConfigurationException(
              "The principal container was not specified\n" +
                  "Set the 'container_dn' value in the kerberos-env configuration to correct this issue.",
              e);
        } catch (KerberosOperationException e) {
          throw new AmbariException(e.getMessage(), e);
        } finally {
          try {
            operationHandler.close();
          } catch (KerberosOperationException e) {
            // Ignore this...
          }
        }

        // need to throw this outside of the try/catch so it isn't caught
        if (missingCredentials) {
          throw new KerberosMissingAdminCredentialsException(
              "Invalid KDC administrator credentials.\n" +
                  "The KDC administrator credentials must be set in session by updating the relevant Cluster resource." +
                  "This may be done by issuing a PUT to the api/v1/clusters/(cluster name) API entry point with the following payload:\n" +
                  "{\n" +
                  "  \"session_attributes\" : {\n" +
                  "    \"kerberos_admin\" : {\"principal\" : \"(PRINCIPAL)\", \"password\" : \"(PASSWORD)\"}\n" +
                  "  }\n" +
                  "}"
          );
        }
      }
    }
  }

  /**
   * Sets the relevant auth-to-local rule configuration properties using the services installed on
   * the cluster and their relevant Kerberos descriptors to determine the rules to be created.
   *
   * @param kerberosDescriptor     the current Kerberos descriptor
   * @param cluster                the cluster
   * @param realm                  the default realm
   * @param existingConfigurations a map of the current configurations
   * @param kerberosConfigurations a map of the configurations to update, this where the generated
   *                               auth-to-local values will be stored
   * @throws AmbariException
   */
  private void setAuthToLocalRules(KerberosDescriptor kerberosDescriptor, Cluster cluster, String realm,
                                   Map<String, Map<String, String>> existingConfigurations,
                                   Map<String, Map<String, String>> kerberosConfigurations)
      throws AmbariException {

    if (kerberosDescriptor != null) {

      Set<String> authToLocalProperties;
      Set<String> authToLocalPropertiesToSet = new HashSet<String>();

      // Determine which properties need to be set
      AuthToLocalBuilder authToLocalBuilder = new AuthToLocalBuilder();

      addIdentities(authToLocalBuilder, kerberosDescriptor.getIdentities(), null, existingConfigurations);

      authToLocalProperties = kerberosDescriptor.getAuthToLocalProperties();
      if (authToLocalProperties != null) {
        authToLocalPropertiesToSet.addAll(authToLocalProperties);
      }

      Map<String, KerberosServiceDescriptor> services = kerberosDescriptor.getServices();
      if (services != null) {
        Map<String, Service> installedServices = cluster.getServices();

        for (KerberosServiceDescriptor service : services.values()) {
          if (installedServices.containsKey(service.getName())) {

            addIdentities(authToLocalBuilder, service.getIdentities(true), null, existingConfigurations);

            authToLocalProperties = service.getAuthToLocalProperties();
            if (authToLocalProperties != null) {
              authToLocalPropertiesToSet.addAll(authToLocalProperties);
            }

            Map<String, KerberosComponentDescriptor> components = service.getComponents();
            if (components != null) {
              for (KerberosComponentDescriptor component : components.values()) {
                addIdentities(authToLocalBuilder, component.getIdentities(true), null, existingConfigurations);

                authToLocalProperties = component.getAuthToLocalProperties();
                if (authToLocalProperties != null) {
                  authToLocalPropertiesToSet.addAll(authToLocalProperties);
                }
              }
            }
          }
        }
      }

      if (!authToLocalPropertiesToSet.isEmpty()) {
        for (String authToLocalProperty : authToLocalPropertiesToSet) {
          String[] parts = authToLocalProperty.split("/");

          if (parts.length == 2) {
            AuthToLocalBuilder builder = authToLocalBuilder.copy();
            String configType = parts[0];
            String propertyName = parts[1];

            // Add existing auth_to_local configuration, if set
            Map<String, String> existingConfiguration = existingConfigurations.get(configType);
            if (existingConfiguration != null) {
              builder.addRules(existingConfiguration.get(propertyName));
            }

            // Add/update descriptor auth_to_local configuration, if set
            Map<String, String> kerberosConfiguration = kerberosConfigurations.get(configType);
            if (kerberosConfiguration != null) {
              builder.addRules(kerberosConfiguration.get(propertyName));
            } else {
              kerberosConfiguration = new HashMap<String, String>();
              kerberosConfigurations.put(configType, kerberosConfiguration);
            }

            kerberosConfiguration.put(propertyName, builder.generate(realm));
          }
        }
      }
    }
  }


  /**
   * Performs operations needed to process Kerberos related tasks on the relevant cluster.
   * <p/>
   * Iterates through the components installed on the relevant cluster to determine if work
   * need to be done.  Calls into the Handler implementation to provide guidance and set up stages
   * to perform the work needed to complete the relative action.
   *
   * @param cluster                        the relevant Cluster
   * @param kerberosDetails                a KerberosDetails containing information about relevant Kerberos configuration
   * @param serviceComponentFilter         a Map of service names to component names indicating the relevant
   *                                       set of services and components - if null, no filter is relevant;
   *                                       if empty, the filter indicates no relevant services or components
   * @param identityFilter                 a Collection of identity names indicating the relevant identities -
   *                                       if null, no filter is relevant; if empty, the filter indicates no
   *                                       relevant identities
   * @param requestStageContainer          a RequestStageContainer to place generated stages, if needed -
   *                                       if null a new RequestStageContainer will be created.
   * @param hostsToForceKerberosOperations a set of host names on which it is expected that the
   *                                       Kerberos client is or will be in the INSTALLED state by
   *                                       the time the operations targeted for them are to be
   *                                       executed - if empty or null, this no hosts will be
   *                                       "forced"
   * @param handler                        a Handler to use to provide guidance and set up stages
   *                                       to perform the work needed to complete the relative action
   * @return the updated or a new RequestStageContainer containing the stages that need to be
   * executed to complete this task; or null if no stages need to be executed.
   * @throws AmbariException
   * @throws KerberosInvalidConfigurationException if an issue occurs trying to get the
   *                                               Kerberos-specific configuration details
   */
  @Transactional
  private RequestStageContainer handle(Cluster cluster,
                                       KerberosDetails kerberosDetails,
                                       Map<String, ? extends Collection<String>> serviceComponentFilter,
                                       Collection<String> identityFilter,
                                       Set<String> hostsToForceKerberosOperations,
                                       RequestStageContainer requestStageContainer,
                                       Handler handler)
      throws AmbariException, KerberosOperationException {

    Map<String, Service> services = cluster.getServices();

    if ((services != null) && !services.isEmpty()) {
      SecurityState desiredSecurityState = handler.getNewServiceSecurityState();
      String clusterName = cluster.getClusterName();
      Map<String, Host> hosts = clusters.getHostsForCluster(clusterName);

      if ((hosts != null) && !hosts.isEmpty()) {
        List<ServiceComponentHost> serviceComponentHostsToProcess = new ArrayList<ServiceComponentHost>();
        KerberosDescriptor kerberosDescriptor = getKerberosDescriptor(cluster);
        KerberosActionDataFileBuilder kerberosActionDataFileBuilder = null;
        Map<String, String> kerberosDescriptorProperties = kerberosDescriptor.getProperties();
        Map<String, Map<String, String>> kerberosConfigurations = new HashMap<String, Map<String, String>>();

        // While iterating over all the ServiceComponentHosts find hosts that have KERBEROS_CLIENT
        // components in the INSTALLED state and add them to the hostsWithValidKerberosClient Set.
        // This is needed to help determine which hosts to perform actions for and create tasks for.
        Set<String> hostsWithValidKerberosClient = new HashSet<String>();

        // Ensure that that hosts that should be assumed to be in the correct state when needed are
        // in the hostsWithValidKerberosClient collection.
        if (hostsToForceKerberosOperations != null) {
          hostsWithValidKerberosClient.addAll(hostsToForceKerberosOperations);
        }

        // Create a temporary directory to store metadata needed to complete this task.  Information
        // such as which principals and keytabs files to create as well as what configurations need
        // to be update are stored in data files in this directory. Any keytab files are stored in
        // this directory until they are distributed to their appropriate hosts.
        File dataDirectory = createTemporaryDirectory();

        // Create the file used to store details about principals and keytabs to create
        File indexFile = new File(dataDirectory, KerberosActionDataFile.DATA_FILE_NAME);

        try {
          // Iterate over the hosts in the cluster to find the components installed in each.  For each
          // component (aka service component host - sch) determine the configuration updates and
          // and the principals an keytabs to create.
          for (Host host : hosts.values()) {
            String hostname = host.getHostName();

            // Get a list of components on the current host
            List<ServiceComponentHost> serviceComponentHosts = cluster.getServiceComponentHosts(hostname);

            if ((serviceComponentHosts != null) && !serviceComponentHosts.isEmpty()) {
              // Calculate the current host-specific configurations. These will be used to replace
              // variables within the Kerberos descriptor data
              Map<String, Map<String, String>> configurations = calculateConfigurations(cluster, hostname, kerberosDescriptorProperties);

              // Iterate over the components installed on the current host to get the service and
              // component-level Kerberos descriptors in order to determine which principals,
              // keytab files, and configurations need to be created or updated.
              for (ServiceComponentHost sch : serviceComponentHosts) {
                String serviceName = sch.getServiceName();
                String componentName = sch.getServiceComponentName();

                // If the current ServiceComponentHost represents the KERBEROS/KERBEROS_CLIENT and
                // indicates that the KERBEROS_CLIENT component is in the INSTALLED state, add the
                // current host to the set of hosts that should be handled...
                if (Service.Type.KERBEROS.name().equals(serviceName) &&
                    Role.KERBEROS_CLIENT.name().equals(componentName) &&
                    (sch.getState() == State.INSTALLED)) {
                  hostsWithValidKerberosClient.add(hostname);
                }

                // If there is no filter or the filter contains the current service name...
                if ((serviceComponentFilter == null) || serviceComponentFilter.containsKey(serviceName)) {
                  Collection<String> componentFilter = (serviceComponentFilter == null) ? null : serviceComponentFilter.get(serviceName);
                  KerberosServiceDescriptor serviceDescriptor = kerberosDescriptor.getService(serviceName);

                  if (serviceDescriptor != null) {
                    int identitiesAdded = 0;
                    List<KerberosIdentityDescriptor> serviceIdentities = serviceDescriptor.getIdentities(true);

                    // Lazily create the KerberosActionDataFileBuilder instance...
                    if (kerberosActionDataFileBuilder == null) {
                      kerberosActionDataFileBuilder = new KerberosActionDataFileBuilder(indexFile);
                    }

                    // Add service-level principals (and keytabs)
                    identitiesAdded += addIdentities(kerberosActionDataFileBuilder, serviceIdentities,
                        identityFilter, hostname, serviceName, componentName, configurations);

                    // If there is no filter or the filter contains the current component name,
                    // test to see if this component should be process by querying the handler...
                    if (((componentFilter == null) || componentFilter.contains(componentName)) && handler.shouldProcess(desiredSecurityState, sch)) {
                      KerberosComponentDescriptor componentDescriptor = serviceDescriptor.getComponent(componentName);

                      if (componentDescriptor != null) {
                        List<KerberosIdentityDescriptor> componentIdentities = componentDescriptor.getIdentities(true);

                        // Calculate the set of configurations to update and replace any variables
                        // using the previously calculated Map of configurations for the host.
                        mergeConfigurations(kerberosConfigurations,
                            componentDescriptor.getConfigurations(true), configurations);

                        // Add component-level principals (and keytabs)
                        identitiesAdded += addIdentities(kerberosActionDataFileBuilder, componentIdentities,
                            identityFilter, hostname, serviceName, componentName, configurations);
                      }
                    }

                    if (identitiesAdded > 0) {
                      serviceComponentHostsToProcess.add(sch);
                    }
                  }
                }
              }
            }
          }
        } catch (IOException e) {
          String message = String.format("Failed to write index file - %s", indexFile.getAbsolutePath());
          LOG.error(message);
          throw new AmbariException(message, e);
        } finally {
          if (kerberosActionDataFileBuilder != null) {
            // Make sure the data file is closed
            try {
              kerberosActionDataFileBuilder.close();
            } catch (IOException e) {
              LOG.warn("Failed to close the index file writer", e);
            }
          }
        }

        // If there are ServiceComponentHosts to process, make sure the administrator credentials
        // are available
        if (!serviceComponentHostsToProcess.isEmpty()) {
          try {
            validateKDCCredentials(cluster);
          } catch (KerberosOperationException e) {
            try {
              FileUtils.deleteDirectory(dataDirectory);
            } catch (Throwable t) {
              LOG.warn(String.format("The data directory (%s) was not deleted due to an error condition - {%s}",
                  dataDirectory.getAbsolutePath(), t.getMessage()), t);
            }

            throw e;
          }

          setAuthToLocalRules(kerberosDescriptor, cluster, kerberosDetails.getDefaultRealm(),
              calculateConfigurations(cluster, null, kerberosDescriptorProperties),
              kerberosConfigurations);
        }

        // Ensure the cluster-env/security_enabled flag is set properly
        Map<String, String> clusterEnvProperties = kerberosConfigurations.get(SECURITY_ENABLED_CONFIG_TYPE);
        if (clusterEnvProperties == null) {
          clusterEnvProperties = new HashMap<String, String>();
          kerberosConfigurations.put(SECURITY_ENABLED_CONFIG_TYPE, clusterEnvProperties);
        }
        clusterEnvProperties.put(SECURITY_ENABLED_PROPERTY_NAME,
            (kerberosDetails.getSecurityType() == SecurityType.KERBEROS) ? "true" : "false");

        // Always set up the necessary stages to perform the tasks needed to complete the operation.
        // Some stages may be no-ops, this is expected.
        // Gather data needed to create stages and tasks...
        Map<String, Set<String>> clusterHostInfo = StageUtils.getClusterHostInfo(hosts, cluster);
        String clusterHostInfoJson = StageUtils.getGson().toJson(clusterHostInfo);
        Map<String, String> hostParams = customCommandExecutionHelper.createDefaultHostParams(cluster);
        String hostParamsJson = StageUtils.getGson().toJson(hostParams);
        String ambariServerHostname = StageUtils.getHostName();
        ServiceComponentHostServerActionEvent event = new ServiceComponentHostServerActionEvent(
            "AMBARI_SERVER",
            ambariServerHostname, // TODO: Choose a random hostname from the cluster. All tasks for the AMBARI_SERVER service will be executed on this Ambari server
            System.currentTimeMillis());
        RoleCommandOrder roleCommandOrder = ambariManagementController.getRoleCommandOrder(cluster);

        // If a RequestStageContainer does not already exist, create a new one...
        if (requestStageContainer == null) {
          requestStageContainer = new RequestStageContainer(
              actionManager.getNextRequestId(),
              null,
              requestFactory,
              actionManager);
        }

        // Use the handler implementation to setup the relevant stages.
        handler.createStages(cluster, hosts, kerberosConfigurations, clusterHostInfoJson,
            hostParamsJson, event, roleCommandOrder, kerberosDetails, dataDirectory,
            requestStageContainer, serviceComponentHostsToProcess, hostsWithValidKerberosClient);

        // Add the cleanup stage...
        handler.addFinalizeOperationStage(cluster, clusterHostInfoJson, hostParamsJson, event,
            dataDirectory, roleCommandOrder, requestStageContainer);

        // If all goes well, set the appropriate states on the relevant ServiceComponentHosts
        for (ServiceComponentHost sch : serviceComponentHostsToProcess) {
          // Update the desired and current states for the ServiceComponentHost
          // using new state information from the the handler implementation
          SecurityState newSecurityState;

          newSecurityState = handler.getNewDesiredSCHSecurityState();
          if (newSecurityState != null) {
            sch.setDesiredSecurityState(newSecurityState);
          }

          newSecurityState = handler.getNewSCHSecurityState();
          if (newSecurityState != null) {
            sch.setSecurityState(newSecurityState);
          }
        }
      }

      // If all goes well, set all services to _desire_ to be secured or unsecured, depending on handler
      if (desiredSecurityState != null) {
        for (Service service : services.values()) {
          if ((serviceComponentFilter == null) || serviceComponentFilter.containsKey(service.getName())) {
            service.setSecurityState(desiredSecurityState);
          }
        }
      }
    }

    return requestStageContainer;
  }

  /**
   * Performs operations needed to process Kerberos related tasks to manage a (unique) test identity
   * on the relevant cluster.
   *
   * @param cluster               the relevant Cluster
   * @param kerberosDetails       a KerberosDetails containing information about relevant Kerberos
   *                              configuration
   * @param commandParameters     the command parameters map used to read and/or write attributes
   *                              related to this operation
   * @param requestStageContainer a RequestStageContainer to place generated stages, if needed -
   *                              if null a new RequestStageContainer will be created.
   * @param handler               a Handler to use to provide guidance and set up stages
   *                              to perform the work needed to complete the relative action
   * @return the updated or a new RequestStageContainer containing the stages that need to be
   * executed to complete this task; or null if no stages need to be executed.
   * @throws AmbariException
   * @throws KerberosOperationException
   */
  private RequestStageContainer handleTestIdentity(Cluster cluster,
                                                   KerberosDetails kerberosDetails,
                                                   Map<String, String> commandParameters, RequestStageContainer requestStageContainer,
                                                   Handler handler) throws AmbariException, KerberosOperationException {

    if (commandParameters == null) {
      throw new AmbariException("The properties map must not be null.  It is needed to store data related to the service check identity");
    }

    Map<String, Service> services = cluster.getServices();

    if ((services != null) && !services.isEmpty()) {
      String clusterName = cluster.getClusterName();
      Map<String, Host> hosts = clusters.getHostsForCluster(clusterName);

      if ((hosts != null) && !hosts.isEmpty()) {
        List<ServiceComponentHost> serviceComponentHostsToProcess = new ArrayList<ServiceComponentHost>();
        KerberosDescriptor kerberosDescriptor = getKerberosDescriptor(cluster);
        KerberosActionDataFileBuilder kerberosActionDataFileBuilder = null;
        Map<String, String> kerberosDescriptorProperties = kerberosDescriptor.getProperties();

        // While iterating over all the ServiceComponentHosts find hosts that have KERBEROS_CLIENT
        // components in the INSTALLED state and add them to the hostsWithValidKerberosClient Set.
        // This is needed to help determine which hosts to perform actions for and create tasks for.
        Set<String> hostsWithValidKerberosClient = new HashSet<String>();

        // Create a temporary directory to store metadata needed to complete this task.  Information
        // such as which principals and keytabs files to create as well as what configurations need
        // to be update are stored in data files in this directory. Any keytab files are stored in
        // this directory until they are distributed to their appropriate hosts.
        File dataDirectory = createTemporaryDirectory();

        // Create the file used to store details about principals and keytabs to create
        File indexFile = new File(dataDirectory, KerberosActionDataFile.DATA_FILE_NAME);

        // Create a special identity for the test user
        KerberosIdentityDescriptor identity = new KerberosIdentityDescriptor(new HashMap<String, Object>() {
          {
            put("principal",
                new HashMap<String, Object>() {
                  {
                    put("value", "${cluster-env/smokeuser}_${service_check_id}@${realm}");
                    put("type", "user");
                  }
                });
            put("keytab",
                new HashMap<String, Object>() {
                  {
                    put("file", "${keytab_dir}/kerberos.service_check.${service_check_id}.keytab");

                    put("owner", new HashMap<String, Object>() {{
                      put("name", "${cluster-env/smokeuser}");
                      put("access", "rw");
                    }});

                    put("group", new HashMap<String, Object>() {{
                      put("name", "${cluster-env/user_group}");
                      put("access", "r");
                    }});

                    put("cachable", "false");
                  }
                });
          }
        });

        // Get or create the unique service check identifier
        String serviceCheckId = getKerberosServiceCheckIdentifier(cluster, true);

        try {
          // Iterate over the hosts in the cluster to find the components installed in each.  For each
          // component (aka service component host - sch) determine the configuration updates and
          // and the principals an keytabs to create.
          for (Host host : hosts.values()) {
            String hostname = host.getHostName();

            // Get a list of components on the current host
            List<ServiceComponentHost> serviceComponentHosts = cluster.getServiceComponentHosts(hostname);

            if ((serviceComponentHosts != null) && !serviceComponentHosts.isEmpty()) {
              // Calculate the current host-specific configurations. These will be used to replace
              // variables within the Kerberos descriptor data
              Map<String, Map<String, String>> configurations = calculateConfigurations(cluster, hostname, kerberosDescriptorProperties);

              // Set the unique service check identifier
              configurations.get("").put("service_check_id", serviceCheckId);

              // Iterate over the components installed on the current host to get the service and
              // component-level Kerberos descriptors in order to determine which principals,
              // keytab files, and configurations need to be created or updated.
              for (ServiceComponentHost sch : serviceComponentHosts) {
                String serviceName = sch.getServiceName();
                String componentName = sch.getServiceComponentName();

                // If the current ServiceComponentHost represents the KERBEROS/KERBEROS_CLIENT and
                // indicates that the KERBEROS_CLIENT component is in the INSTALLED state, add the
                // current host to the set of hosts that should be handled...
                if (Service.Type.KERBEROS.name().equals(serviceName) &&
                    Role.KERBEROS_CLIENT.name().equals(componentName) &&
                    (sch.getState() == State.INSTALLED)) {
                  hostsWithValidKerberosClient.add(hostname);

                  int identitiesAdded = 0;

                  // Lazily create the KerberosActionDataFileBuilder instance...
                  if (kerberosActionDataFileBuilder == null) {
                    kerberosActionDataFileBuilder = new KerberosActionDataFileBuilder(indexFile);
                  }

                  // Add service-level principals (and keytabs)
                  identitiesAdded += addIdentities(kerberosActionDataFileBuilder, Collections.singleton(identity),
                      null, hostname, serviceName, componentName, configurations);

                  if (identitiesAdded > 0) {
                    // Add the relevant principal name and keytab file data to the command params state
                    if (!commandParameters.containsKey("principal_name") || !commandParameters.containsKey("keytab_file")) {
                      commandParameters.put("principal_name",
                          KerberosDescriptor.replaceVariables(identity.getPrincipalDescriptor().getValue(), configurations));
                      commandParameters.put("keytab_file",
                          KerberosDescriptor.replaceVariables(identity.getKeytabDescriptor().getFile(), configurations));
                    }

                    serviceComponentHostsToProcess.add(sch);
                  }
                }
              }
            }
          }
        } catch (IOException e) {
          String message = String.format("Failed to write index file - %s", indexFile.getAbsolutePath());
          LOG.error(message);
          throw new AmbariException(message, e);
        } finally {
          if (kerberosActionDataFileBuilder != null) {
            // Make sure the data file is closed
            try {
              kerberosActionDataFileBuilder.close();
            } catch (IOException e) {
              LOG.warn("Failed to close the index file writer", e);
            }
          }
        }

        // If there are ServiceComponentHosts to process, make sure the administrator credentials
        // are available
        if (!serviceComponentHostsToProcess.isEmpty()) {
          try {
            validateKDCCredentials(cluster);
          } catch (KerberosOperationException e) {
            try {
              FileUtils.deleteDirectory(dataDirectory);
            } catch (Throwable t) {
              LOG.warn(String.format("The data directory (%s) was not deleted due to an error condition - {%s}",
                  dataDirectory.getAbsolutePath(), t.getMessage()), t);
            }

            throw e;
          }
        }

        // Always set up the necessary stages to perform the tasks needed to complete the operation.
        // Some stages may be no-ops, this is expected.
        // Gather data needed to create stages and tasks...
        Map<String, Set<String>> clusterHostInfo = StageUtils.getClusterHostInfo(hosts, cluster);
        String clusterHostInfoJson = StageUtils.getGson().toJson(clusterHostInfo);
        Map<String, String> hostParams = customCommandExecutionHelper.createDefaultHostParams(cluster);
        String hostParamsJson = StageUtils.getGson().toJson(hostParams);
        String ambariServerHostname = StageUtils.getHostName();
        ServiceComponentHostServerActionEvent event = new ServiceComponentHostServerActionEvent(
            "AMBARI_SERVER",
            ambariServerHostname, // TODO: Choose a random hostname from the cluster. All tasks for the AMBARI_SERVER service will be executed on this Ambari server
            System.currentTimeMillis());
        RoleCommandOrder roleCommandOrder = ambariManagementController.getRoleCommandOrder(cluster);

        // If a RequestStageContainer does not already exist, create a new one...
        if (requestStageContainer == null) {
          requestStageContainer = new RequestStageContainer(
              actionManager.getNextRequestId(),
              null,
              requestFactory,
              actionManager);
        }

        // Use the handler implementation to setup the relevant stages.
        handler.createStages(cluster, hosts, Collections.<String, Map<String, String>>emptyMap(),
            clusterHostInfoJson, hostParamsJson, event, roleCommandOrder, kerberosDetails,
            dataDirectory, requestStageContainer, serviceComponentHostsToProcess, hostsWithValidKerberosClient);

        handler.addFinalizeOperationStage(cluster, clusterHostInfoJson, hostParamsJson, event,
            dataDirectory, roleCommandOrder, requestStageContainer);
      }
    }

    return requestStageContainer;
  }


  /**
   * Gathers the Kerberos-related data from configurations and stores it in a new KerberosDetails
   * instance.
   *
   * @param cluster the relevant Cluster
   * @return a new KerberosDetails with the collected configuration data
   * @throws AmbariException
   */
  private KerberosDetails getKerberosDetails(Cluster cluster)
      throws KerberosInvalidConfigurationException, AmbariException {

    KerberosDetails kerberosDetails = new KerberosDetails();

    if (cluster == null) {
      String message = "The cluster object is not available";
      LOG.error(message);
      throw new AmbariException(message);
    }

    Config configKrb5Conf = cluster.getDesiredConfigByType("krb5-conf");
    if (configKrb5Conf == null) {
      String message = "The 'krb5-conf' configuration is not available";
      LOG.error(message);
      throw new AmbariException(message);
    }

    Map<String, String> krb5ConfProperties = configKrb5Conf.getProperties();
    if (krb5ConfProperties == null) {
      String message = "The 'krb5-conf' configuration properties are not available";
      LOG.error(message);
      throw new AmbariException(message);
    }

    Config configKerberosEnv = cluster.getDesiredConfigByType("kerberos-env");
    if (configKerberosEnv == null) {
      String message = "The 'kerberos-env' configuration is not available";
      LOG.error(message);
      throw new AmbariException(message);
    }

    Map<String, String> kerberosEnvProperties = configKerberosEnv.getProperties();
    if (kerberosEnvProperties == null) {
      String message = "The 'kerberos-env' configuration properties are not available";
      LOG.error(message);
      throw new AmbariException(message);
    }

    KDCType kdcType;
    String kdcTypeProperty = kerberosEnvProperties.get("kdc_type");
    if (kdcTypeProperty == null) {
      String message = "The 'kerberos-env/kdc_type' value must be set to a valid KDC type";
      LOG.error(message);
      throw new KerberosInvalidConfigurationException(message);
    }

    try {
      kdcType = KDCType.translate(kdcTypeProperty);
    } catch (IllegalArgumentException e) {
      String message = String.format("Invalid 'kdc_type' value: %s", kdcTypeProperty);
      LOG.error(message);
      throw new AmbariException(message);
    }

    kerberosDetails.setSecurityType(cluster.getSecurityType());
    kerberosDetails.setDefaultRealm(kerberosEnvProperties.get("realm"));

    // Set the KDCType to the the MIT_KDC as a fallback.
    kerberosDetails.setKdcType((kdcType == null) ? KDCType.MIT_KDC : kdcType);

    kerberosDetails.setKerberosEnvProperties(kerberosEnvProperties);

    return kerberosDetails;
  }

  /**
   * Builds a composite Kerberos descriptor using the default Kerberos descriptor and a user-specified
   * Kerberos descriptor, if it exists.
   * <p/>
   * The default Kerberos descriptor is built from the kerberos.json files in the stack. It can be
   * retrieved via the <code>stacks/:stackName/versions/:version/artifacts/kerberos_descriptor</code>
   * endpoint
   * <p/>
   * The user-specified Kerberos descriptor was registered to the
   * <code>cluster/:clusterName/artifacts/kerberos_descriptor</code> endpoint.
   * <p/>
   * If the user-specified Kerberos descriptor exists, it is used to update the default Kerberos
   * descriptor and the composite is returned.  If not, the default cluster descriptor is returned
   * as-is.
   *
   * @param cluster cluster instance
   * @return the kerberos descriptor associated with the specified cluster
   * @throws AmbariException if unable to obtain the descriptor
   */
  private KerberosDescriptor getKerberosDescriptor(Cluster cluster) throws AmbariException {
    StackId stackId = cluster.getCurrentStackVersion();

    // -------------------------------
    // Get the default Kerberos descriptor from the stack, which is the same as the value from
    // stacks/:stackName/versions/:version/artifacts/kerberos_descriptor
    KerberosDescriptor defaultDescriptor = ambariMetaInfo.getKerberosDescriptor(stackId.getStackName(), stackId.getStackVersion());
    // -------------------------------

    // Get the user-supplied Kerberos descriptor from cluster/:clusterName/artifacts/kerberos_descriptor
    KerberosDescriptor descriptor = null;

    PredicateBuilder pb = new PredicateBuilder();
    Predicate predicate = pb.begin().property("Artifacts/cluster_name").equals(cluster.getClusterName()).and().
        property(ArtifactResourceProvider.ARTIFACT_NAME_PROPERTY).equals("kerberos_descriptor").
        end().toPredicate();

    synchronized (KerberosHelper.class) {
      if (clusterController == null) {
        clusterController = ClusterControllerHelper.getClusterController();
      }
    }

    ResourceProvider artifactProvider =
        clusterController.ensureResourceProvider(Resource.Type.Artifact);

    Request request = new RequestImpl(Collections.<String>emptySet(),
        Collections.<Map<String, Object>>emptySet(), Collections.<String, String>emptyMap(), null);

    Set<Resource> response = null;
    try {
      response = artifactProvider.getResources(request, predicate);
    } catch (SystemException e) {
      e.printStackTrace();
      throw new AmbariException("An unknown error occurred while trying to obtain the cluster kerberos descriptor", e);
    } catch (UnsupportedPropertyException e) {
      e.printStackTrace();
      throw new AmbariException("An unknown error occurred while trying to obtain the cluster kerberos descriptor", e);
    } catch (NoSuchParentResourceException e) {
      // parent cluster doesn't exist.  shouldn't happen since we have the cluster instance
      e.printStackTrace();
      throw new AmbariException("An unknown error occurred while trying to obtain the cluster kerberos descriptor", e);
    } catch (NoSuchResourceException e) {
      // no descriptor registered, use the default from the stack
    }

    if (response != null && !response.isEmpty()) {
      Resource descriptorResource = response.iterator().next();
      Map<String, Map<String, Object>> propertyMap = descriptorResource.getPropertiesMap();
      if (propertyMap != null) {
        Map<String, Object> artifactData = propertyMap.get(ArtifactResourceProvider.ARTIFACT_DATA_PROPERTY);
        Map<String, Object> artifactDataProperties = propertyMap.get(ArtifactResourceProvider.ARTIFACT_DATA_PROPERTY + "/properties");
        HashMap<String, Object> data = new HashMap<String, Object>();

        if (artifactData != null) {
          data.putAll(artifactData);
        }

        if (artifactDataProperties != null) {
          data.put("properties", artifactDataProperties);
        }

        descriptor = kerberosDescriptorFactory.createInstance(data);
      }
    }
    // -------------------------------

    // -------------------------------
    // Attempt to build and return a composite of the default Kerberos descriptor and the user-supplied
    // Kerberos descriptor. If the default descriptor exists, overlay the user-supplied Kerberos
    // descriptor on top of it (if it exists) and return the composite; else return the user-supplied
    // Kerberos descriptor. If both values are null, null may be returned.
    if (defaultDescriptor == null) {
      return descriptor;
    } else {
      if (descriptor != null) {
        defaultDescriptor.update(descriptor);
      }
      return defaultDescriptor;
    }
    // -------------------------------
  }

  /**
   * Creates a temporary directory within the system temporary directory
   * <p/>
   * The resulting directory is to be removed by the caller when desired.
   *
   * @return a File pointing to the new temporary directory, or null if one was not created
   * @throws AmbariException if a new temporary directory cannot be created
   */
  private File createTemporaryDirectory() throws AmbariException {
    String tempDirectoryPath = configuration.getProperty(Configuration.SERVER_TMP_DIR_KEY);

    if ((tempDirectoryPath == null) || tempDirectoryPath.isEmpty()) {
      tempDirectoryPath = System.getProperty("java.io.tmpdir");
    }

    try {
      if (tempDirectoryPath == null) {
        throw new IOException("The System property 'java.io.tmpdir' does not specify a temporary directory");
      }

      File directory;
      int tries = 0;
      long now = System.currentTimeMillis();

      do {
        directory = new File(tempDirectoryPath, String.format("%s%d-%d.d",
            KerberosServerAction.DATA_DIRECTORY_PREFIX, now, tries));

        if ((directory.exists()) || !directory.mkdirs()) {
          directory = null; // Rest and try again...
        } else {
          LOG.debug("Created temporary directory: {}", directory.getAbsolutePath());
        }
      } while ((directory == null) && (++tries < 100));

      if (directory == null) {
        throw new IOException(String.format("Failed to create a temporary directory in %s", tempDirectoryPath));
      }

      return directory;
    } catch (IOException e) {
      String message = "Failed to create the temporary data directory.";
      LOG.error(message, e);
      throw new AmbariException(message, e);
    }
  }

  /**
   * Merges configuration from a Map of configuration updates into a main configurations Map.  Each
   * property in the updates Map is processed to replace variables using the replacement Map.
   * <p/>
   * See {@link org.apache.ambari.server.state.kerberos.KerberosDescriptor#replaceVariables(String, java.util.Map)}
   * for information on variable replacement.
   *
   * @param configurations a Map of configurations
   * @param updates        a Map of configuration updates
   * @param replacements   a Map of (grouped) replacement values
   * @return the merged Map
   * @throws AmbariException
   */
  private Map<String, Map<String, String>> mergeConfigurations(Map<String, Map<String, String>> configurations,
                                                               Map<String, KerberosConfigurationDescriptor> updates,
                                                               Map<String, Map<String, String>> replacements)
      throws AmbariException {

    if ((updates != null) && !updates.isEmpty()) {
      if (configurations == null) {
        configurations = new HashMap<String, Map<String, String>>();
      }

      for (Map.Entry<String, KerberosConfigurationDescriptor> entry : updates.entrySet()) {
        String type = entry.getKey();
        KerberosConfigurationDescriptor configurationDescriptor = entry.getValue();

        if (configurationDescriptor != null) {
          Map<String, String> updatedProperties = configurationDescriptor.getProperties();
          mergeConfigurations(configurations, type, updatedProperties, replacements);
        }
      }
    }

    return configurations;
  }

  private void mergeConfigurations(Map<String, Map<String, String>> configurations, String type,
                                   Map<String, String> updates,
                                   Map<String, Map<String, String>> replacements) throws AmbariException {
    if (updates != null) {
      Map<String, String> existingProperties = configurations.get(type);
      if (existingProperties == null) {
        existingProperties = new HashMap<String, String>();
        configurations.put(type, existingProperties);
      }

      for (Map.Entry<String, String> property : updates.entrySet()) {
        existingProperties.put(
            KerberosDescriptor.replaceVariables(property.getKey(), replacements),
            KerberosDescriptor.replaceVariables(property.getValue(), replacements)
        );
      }
    }
  }

  /**
   * Adds identities to the KerberosActionDataFileBuilder.
   *
   * @param kerberosActionDataFileBuilder a KerberosActionDataFileBuilder to use for storing identity
   *                                      records
   * @param identities                    a List of KerberosIdentityDescriptors to add to the data
   *                                      file
   * @param identityFilter                a Collection of identity names indicating the relevant identities -
   *                                      if null, no filter is relevant; if empty, the filter indicates no
   *                                      relevant identities
   * @param hostname                      the relevant hostname
   * @param serviceName                   the relevant service name
   * @param componentName                 the relevant component name
   * @param configurations                a Map of configurations to use a replacements for variables
   *                                      in identity fields
   * @return an integer indicating the number of identities added to the data file
   * @throws java.io.IOException if an error occurs while writing a record to the data file
   */
  private int addIdentities(KerberosActionDataFileBuilder kerberosActionDataFileBuilder,
                            Collection<KerberosIdentityDescriptor> identities,
                            Collection<String> identityFilter, String hostname, String serviceName,
                            String componentName, Map<String, Map<String, String>> configurations)
      throws IOException {
    int identitiesAdded = 0;

    if (identities != null) {
      for (KerberosIdentityDescriptor identity : identities) {
        // If there is no filter or the filter contains the current identity's name...
        if ((identityFilter == null) || identityFilter.contains(identity.getName())) {
          KerberosPrincipalDescriptor principalDescriptor = identity.getPrincipalDescriptor();
          String principal = null;
          String principalType = null;
          String principalConfiguration = null;

          if (principalDescriptor != null) {
            principal = KerberosDescriptor.replaceVariables(principalDescriptor.getValue(), configurations);
            principalType = principalDescriptor.getType().name().toLowerCase();
            principalConfiguration = KerberosDescriptor.replaceVariables(principalDescriptor.getConfiguration(), configurations);
          }

          if (principal != null) {
            KerberosKeytabDescriptor keytabDescriptor = identity.getKeytabDescriptor();
            String keytabFilePath = null;
            String keytabFileOwnerName = null;
            String keytabFileOwnerAccess = null;
            String keytabFileGroupName = null;
            String keytabFileGroupAccess = null;
            String keytabFileConfiguration = null;

            if (keytabDescriptor != null) {
              keytabFilePath = KerberosDescriptor.replaceVariables(keytabDescriptor.getFile(), configurations);
              keytabFileOwnerName = KerberosDescriptor.replaceVariables(keytabDescriptor.getOwnerName(), configurations);
              keytabFileOwnerAccess = KerberosDescriptor.replaceVariables(keytabDescriptor.getOwnerAccess(), configurations);
              keytabFileGroupName = KerberosDescriptor.replaceVariables(keytabDescriptor.getGroupName(), configurations);
              keytabFileGroupAccess = KerberosDescriptor.replaceVariables(keytabDescriptor.getGroupAccess(), configurations);
              keytabFileConfiguration = KerberosDescriptor.replaceVariables(keytabDescriptor.getConfiguration(), configurations);
            }

            // Append an entry to the action data file builder...
            kerberosActionDataFileBuilder.addRecord(
                hostname,
                serviceName,
                componentName,
                principal,
                principalType,
                principalConfiguration,
                keytabFilePath,
                keytabFileOwnerName,
                keytabFileOwnerAccess,
                keytabFileGroupName,
                keytabFileGroupAccess,
                keytabFileConfiguration,
                (keytabDescriptor.isCachable()) ? "true" : "false");

            identitiesAdded++;
          }
        }
      }
    }

    return identitiesAdded;
  }

  /**
   * Adds identities to the AuthToLocalBuilder.
   *
   * @param authToLocalBuilder the AuthToLocalBuilder to use to build the auth_to_local mapping
   * @param identities         a List of KerberosIdentityDescriptors to process
   * @param identityFilter     a Collection of identity names indicating the relevant identities -
   *                           if null, no filter is relevant; if empty, the filter indicates no
   *                           relevant identities
   * @param configurations     a Map of configurations to use a replacements for variables
   *                           in identity fields
   * @throws org.apache.ambari.server.AmbariException
   */
  private void addIdentities(AuthToLocalBuilder authToLocalBuilder,
                             List<KerberosIdentityDescriptor> identities, Collection<String> identityFilter,
                             Map<String, Map<String, String>> configurations) throws AmbariException {
    if (identities != null) {
      for (KerberosIdentityDescriptor identity : identities) {
        // If there is no filter or the filter contains the current identity's name...
        if ((identityFilter == null) || identityFilter.contains(identity.getName())) {
          KerberosPrincipalDescriptor principalDescriptor = identity.getPrincipalDescriptor();
          if (principalDescriptor != null) {
            authToLocalBuilder.addRule(
                KerberosDescriptor.replaceVariables(principalDescriptor.getValue(), configurations),
                KerberosDescriptor.replaceVariables(principalDescriptor.getLocalUsername(), configurations));
          }
        }
      }
    }
  }

  /**
   * Calculates the map of configurations relative to the cluster and host.
   * <p/>
   * Most of this was borrowed from {@link org.apache.ambari.server.actionmanager.ExecutionCommandWrapper#getExecutionCommand()}
   *
   * @param cluster                      the relevant Cluster
   * @param hostname                     the relevant hostname
   * @param kerberosDescriptorProperties a map of general Kerberos descriptor properties
   * @return a Map of calculated configuration types
   * @throws AmbariException
   */
  private Map<String, Map<String, String>> calculateConfigurations(Cluster cluster, String hostname,
                                                                   Map<String, String> kerberosDescriptorProperties)
      throws AmbariException {
    // For a configuration type, both tag and an actual configuration can be stored
    // Configurations from the tag is always expanded and then over-written by the actual
    // global:version1:{a1:A1,b1:B1,d1:D1} + global:{a1:A2,c1:C1,DELETED_d1:x} ==>
    // global:{a1:A2,b1:B1,c1:C1}
    Map<String, Map<String, String>> configurations = new HashMap<String, Map<String, String>>();
    Map<String, Map<String, String>> configurationTags = ambariManagementController.findConfigurationTagsWithOverrides(cluster, hostname);

    if (configurationTags.get(Configuration.GLOBAL_CONFIG_TAG) != null) {
      configHelper.applyCustomConfig(
          configurations, Configuration.GLOBAL_CONFIG_TAG,
          Configuration.RCA_ENABLED_PROPERTY, "false", false);
    }

    Map<String, Map<String, String>> configProperties = configHelper.getEffectiveConfigProperties(cluster, configurationTags);

    // Apply the configurations saved with the Execution Cmd on top of
    // derived configs - This will take care of all the hacks
    for (Map.Entry<String, Map<String, String>> entry : configProperties.entrySet()) {
      String type = entry.getKey();
      Map<String, String> allLevelMergedConfig = entry.getValue();
      Map<String, String> configuration = configurations.get(type);

      if (configuration == null) {
        configuration = new HashMap<String, String>(allLevelMergedConfig);
      } else {
        Map<String, String> mergedConfig = configHelper.getMergedConfig(allLevelMergedConfig, configuration);
        configuration.clear();
        configuration.putAll(mergedConfig);
      }

      configurations.put(type, configuration);
    }

    // A map to hold un-categorized properties.  This may come from the KerberosDescriptor
    // and will also contain a value for the current host
    Map<String, String> generalProperties = configurations.get("");
    if (generalProperties == null) {
      generalProperties = new HashMap<String, String>();
      configurations.put("", generalProperties);
    }

    // If any properties are set in the calculated KerberosDescriptor, add them into the
    // Map of configurations as an un-categorized type (using an empty string)
    if (kerberosDescriptorProperties != null) {
      generalProperties.putAll(kerberosDescriptorProperties);
    }

    // Add the current hostname under "host" and "hostname"
    generalProperties.put("host", hostname);
    generalProperties.put("hostname", hostname);

    // Add the current cluster's name
    generalProperties.put("cluster_name", cluster.getClusterName());

    // add clusterHostInfo config
    Map<String, String> componentHosts = new HashMap<String, String>();
    for (Map.Entry<String, Service> service : cluster.getServices().entrySet()) {
      for (Map.Entry<String, ServiceComponent> serviceComponent : service.getValue().getServiceComponents().entrySet()) {
        if (StageUtils.getComponentToClusterInfoKeyMap().keySet().contains(serviceComponent.getValue().getName())) {
          componentHosts.put(StageUtils.getComponentToClusterInfoKeyMap().get(serviceComponent.getValue().getName()),
              StringUtils.join(serviceComponent.getValue().getServiceComponentHosts().keySet(), ","));
        }
      }
    }
    configurations.put("clusterHostInfo", componentHosts);

    return configurations;
  }

  /**
   * Creates a new stage
   *
   * @param id              the new stage's id
   * @param cluster         the relevant Cluster
   * @param requestId       the relevant request Id
   * @param requestContext  a String describing the stage
   * @param clusterHostInfo JSON-encoded clusterHostInfo structure
   * @param commandParams   JSON-encoded command parameters
   * @param hostParams      JSON-encoded host parameters
   * @return a newly created Stage
   */
  private Stage createNewStage(long id, Cluster cluster, long requestId,
                               String requestContext, String clusterHostInfo,
                               String commandParams, String hostParams) {
    Stage stage = stageFactory.createNew(requestId,
        BASE_LOG_DIR + File.pathSeparator + requestId,
        cluster.getClusterName(),
        cluster.getClusterId(),
        requestContext,
        clusterHostInfo,
        commandParams,
        hostParams);

    stage.setStageId(id);
    return stage;
  }

  /**
   * Creates a new stage with a single task describing the ServerAction class to invoke and the other
   * task-related information.
   *
   * @param id                the new stage's id
   * @param cluster           the relevant Cluster
   * @param requestId         the relevant request Id
   * @param requestContext    a String describing the stage
   * @param clusterHostInfo   JSON-encoded clusterHostInfo structure
   * @param commandParams     JSON-encoded command parameters
   * @param hostParams        JSON-encoded host parameters
   * @param actionClass       The ServeAction class that implements the action to invoke
   * @param event             The relevant ServiceComponentHostServerActionEvent
   * @param commandParameters a Map of command parameters to attach to the task added to the new
   *                          stage
   * @param commandDetail     a String declaring a descriptive name to pass to the action - null or an
   *                          empty string indicates no value is to be set
   * @param timeout           the timeout for the task/action  @return a newly created Stage
   */
  private Stage createServerActionStage(long id, Cluster cluster, long requestId,
                                        String requestContext, String clusterHostInfo,
                                        String commandParams, String hostParams,
                                        Class<? extends ServerAction> actionClass,
                                        ServiceComponentHostServerActionEvent event,
                                        Map<String, String> commandParameters, String commandDetail,
                                        Integer timeout) throws AmbariException {

    Stage stage = createNewStage(id, cluster, requestId, requestContext, clusterHostInfo, commandParams, hostParams);
    stage.addServerActionCommand(actionClass.getName(),
        Role.AMBARI_SERVER_ACTION,
        RoleCommand.EXECUTE,
        cluster.getClusterName(),
        event,
        commandParameters,
        commandDetail,
        ambariManagementController.findConfigurationTagsWithOverrides(cluster, null),
        timeout, false);

    return stage;
  }

  /**
   * Using the session data from the relevant Cluster object, creates a KerberosCredential,
   * serializes, and than encrypts it.
   * <p/>
   * Since the relevant data is stored in the HTTPSession (which relies on ThreadLocalStorage),
   * it needs to be retrieved now and placed in the action's command parameters so it will be
   * available when needed.  Because command parameters are stored in plaintext in the Ambari database,
   * this (sensitive) data needs to be encrypted, however it needs to be done using a key the can be
   * recreated sometime later when the data needs to be access. Since it is expected that the Cluster
   * object will be able now and later, the hashcode of this object is used to build the key - it
   * is expected that the same instance will be retrieved from the Clusters instance, thus yielding
   * the same hashcode value.
   * <p/>
   * If the Ambari server architecture changes, this will need to be revisited.
   *
   * @param cluster the relevant Cluster
   * @return a serialized and encrypted KerberosCredential, or null if administrator data is not found
   * @throws AmbariException
   */
  private String getEncryptedAdministratorCredentials(Cluster cluster) throws AmbariException {
    String encryptedAdministratorCredentials = null;

    Map<String, Object> sessionAttributes = cluster.getSessionAttributes();
    if (sessionAttributes != null) {
      KerberosCredential credential = KerberosCredential.fromMap(sessionAttributes, "kerberos_admin/");
      if (credential != null) {
        byte[] key = Integer.toHexString(cluster.hashCode()).getBytes();
        encryptedAdministratorCredentials = credential.encrypt(key);
      }
    }

    return encryptedAdministratorCredentials;
  }

  /**
   * Using the session data from the relevant Cluster object, gets the previously stored
   * Kerberos service check identifier value or creates a new one if indicated to do so.
   * <p/>
   * This value is used intended to be used by the KerberosHelper to manage uniquely crated
   * principals for use in service checks.
   *
   * @param cluster the relevant Cluster
   * @return the previously stored Kerberos service check identifier value, or null if
   * not previously stored
   */
  private String getKerberosServiceCheckIdentifier(Cluster cluster, boolean createIfNull) {
    Map<String, Object> sessionAttributes = cluster.getSessionAttributes();
    Object value = (sessionAttributes == null) ? null : sessionAttributes.get(SERVICE_CHECK_IDENTIFIER);
    String serviceCheckIdentifier = (value instanceof String) ? (String) value : null;

    if ((serviceCheckIdentifier == null) && createIfNull) {
      // Create a new (ideally) unique(ish) identifier
      Random random = new Random(System.currentTimeMillis());
      char[] chars = new char[8];

      for (int i = 0; i < 8; i++) {
        chars[i] = (char) ((int) 'a' + random.nextInt(26));
      }

      serviceCheckIdentifier = String.valueOf(chars);
      setKerberosServiceCheckIdentifier(cluster, serviceCheckIdentifier);
    }

    return serviceCheckIdentifier;
  }

  /**
   * Stores the Kerberos service check identifier value into the session data from the
   * relevant Cluster object.
   * <p/>
   * This value is used intended to be used by the KerberosHelper to manage uniquely crated
   * principals for use in service checks.
   *
   * @param cluster the relevant Cluster
   * @param value   the Kerberos service check identifier to store or null to clear any previously set value
   */
  private void setKerberosServiceCheckIdentifier(Cluster cluster, String value) {
    if (value == null) {
      cluster.removeSessionAttribute(SERVICE_CHECK_IDENTIFIER);
    } else {
      cluster.setSessionAttribute(SERVICE_CHECK_IDENTIFIER, value);
    }
  }

  /**
   * Given a Collection of ServiceComponentHosts generates a unique list of hosts.
   *
   * @param serviceComponentHosts a Collection of ServiceComponentHosts from which to to retrieve host names
   * @param allowedStates         a Set of HostStates to use to filter the list of hosts, if null, no filter is applied
   * @return a List of (unique) host names
   * @throws org.apache.ambari.server.AmbariException
   */
  private List<String> createUniqueHostList(Collection<ServiceComponentHost> serviceComponentHosts, Set<HostState> allowedStates)
      throws AmbariException {
    Set<String> hostNames = new HashSet<String>();
    Set<String> visitedHostNames = new HashSet<String>();

    if (serviceComponentHosts != null) {
      for (ServiceComponentHost sch : serviceComponentHosts) {
        String hostname = sch.getHostName();
        if (!visitedHostNames.contains(hostname)) {
          // If allowedStates is null, assume the caller doesnt care about the state of the host
          // so skip the call to get the relevant Host data and just add the host to the list
          if (allowedStates == null) {
            hostNames.add(hostname);
          } else {
            Host host = clusters.getHost(hostname);

            if (allowedStates.contains(host.getState())) {
              hostNames.add(hostname);
            }
          }

          visitedHostNames.add(hostname);
        }
      }
    }

    return new ArrayList<String>(hostNames);
  }

  /**
   * Determine if a cluster has kerberos enabled.
   *
   * @param cluster cluster to test
   * @return true if the provided cluster has kerberos enabled; false otherwise
   */
  public boolean isClusterKerberosEnabled(Cluster cluster) {
    return cluster.getSecurityType() == SecurityType.KERBEROS;
  }

  /**
   * Tests the request properties to check for directives that need to be acted upon
   * <p/>
   * It is required that the SecurityType from the request is either KERBEROS or NONE and that at
   * least one directive in the requestProperties map is supported.
   *
   * @param requestSecurityType the SecurityType from the request
   * @param requestProperties   A Map of request directives and their values
   * @return true if custom operations should be executed; false otherwise
   */
  public boolean shouldExecuteCustomOperations(SecurityType requestSecurityType, Map<String, String> requestProperties) {

    if (((requestSecurityType == SecurityType.KERBEROS) || (requestSecurityType == SecurityType.NONE)) &&
        (requestProperties != null) && !requestProperties.isEmpty()) {
      for (SupportedCustomOperation type : SupportedCustomOperation.values()) {
        if (requestProperties.containsKey(type.name().toLowerCase())) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * Given a list of KerberosIdentityDescriptors, returns a Map fo configuration types to property
   * names and values.
   * <p/>
   * The property names and values are not expected to have any variable replacements done.
   *
   * @param identityDescriptors a List of KerberosIdentityDescriptor from which to retrieve configurations
   * @return a Map of configuration types to property name/value pairs (as a Map)
   */
  private Map<String, Map<String, String>> getConfigurations(List<KerberosIdentityDescriptor> identityDescriptors) {
    Map<String, Map<String, String>> map = new HashMap<String, Map<String, String>>();

    if (identityDescriptors != null) {
      for (KerberosIdentityDescriptor identityDescriptor : identityDescriptors) {
        KerberosPrincipalDescriptor principalDescriptor = identityDescriptor.getPrincipalDescriptor();
        if (principalDescriptor != null) {
          putConfiguration(map, principalDescriptor.getConfiguration(), principalDescriptor.getValue());
        }

        KerberosKeytabDescriptor keytabDescriptor = identityDescriptor.getKeytabDescriptor();
        if (keytabDescriptor != null) {
          putConfiguration(map, keytabDescriptor.getConfiguration(), keytabDescriptor.getFile());
        }
      }
    }

    return map;
  }

  /**
   * Inserts a configuration property and value into a map of configuration types to property
   * name/value pair maps.
   *
   * @param map           the Map to insert into
   * @param configuration a configuration property in the form of config-type/property_name
   * @param value         the value of the configuration property
   */
  private void putConfiguration(Map<String, Map<String, String>> map, String configuration, String value) {
    if (configuration != null) {
      String[] principalTokens = configuration.split("/");

      if (principalTokens.length == 2) {
        String type = principalTokens[0];
        String propertyName = principalTokens[1];

        Map<String, String> properties = map.get(type);
        if (properties == null) {
          properties = new HashMap<String, String>();
          map.put(type, properties);
        }

        properties.put(propertyName, value);
      }
    }
  }

  /**
   * A enumeration of the supported custom operations
   */
  public static enum SupportedCustomOperation {
    REGENERATE_KEYTABS
  }

  /**
   * Handler is an interface that needs to be implemented by toggle handler classes to do the
   * "right" thing for the task at hand.
   */
  private abstract class Handler {
    /**
     * Tests the Service and ServiceComponentHost to see if they are in the appropriate security
     * state to be processed for the relevant task.
     *
     * @param desiredSecurityState the SecurityState to be transitioned to
     * @param sch                  the ServiceComponentHost to test
     * @return true if both the Service and ServiceComponentHost are in the appropriate security
     * state to be processed; otherwise false
     * @throws AmbariException of an error occurs while testing
     */
    abstract boolean shouldProcess(SecurityState desiredSecurityState, ServiceComponentHost sch) throws AmbariException;

    /**
     * Returns the new SecurityState to be set as the ServiceComponentHost's _desired_ SecurityState.
     *
     * @return a SecurityState to be set as the ServiceComponentHost's _desired_ SecurityState;
     * or null if no state change is desired
     */
    abstract SecurityState getNewDesiredSCHSecurityState();

    /**
     * Returns the new SecurityState to be set as the ServiceComponentHost's _current_ SecurityState.
     *
     * @return a SecurityState to be set as the ServiceComponentHost's _current_ SecurityState;
     * or null if no state change is desired
     */
    abstract SecurityState getNewSCHSecurityState();


    /**
     * Returns the new SecurityState to be set as the Service's SecurityState.
     *
     * @return a SecurityState to be set as the Service's SecurityState;
     * or null if no state change is desired
     */
    abstract SecurityState getNewServiceSecurityState();

    /**
     * Creates the necessary stages to complete the relevant task and stores them in the supplied
     * or a newly created RequestStageContainer.
     * <p/>
     * If the supplied RequestStageContainer is null, a new one must be created and filled.
     * {@link org.apache.ambari.server.controller.internal.RequestStageContainer#persist()} should
     * not be called since it is not known if the set of states for this container is complete.
     *
     * @param cluster                the relevant Cluster
     * @param hosts                  the relevant Map of Hosts
     * @param kerberosConfigurations the compiled KerberosConfigurations for the entire Kerberos
     *                               descriptor hierarchy with all variables replaced
     * @param clusterHostInfo        JSON-encoded clusterHostInfo structure
     * @param hostParams             JSON-encoded host parameters
     * @param event                  a ServiceComponentHostServerActionEvent to pass to any created tasks
     * @param roleCommandOrder       the RoleCommandOrder to use to generate the RoleGraph for any newly created Stages
     * @param kerberosDetails        a KerberosDetails containing the information about the relevant Kerberos configuration
     * @param dataDirectory          a File pointing to the (temporary) data directory
     * @param requestStageContainer  a RequestStageContainer to store the new stages in, if null a
     *                               new RequestStageContainer will be created
     * @param serviceComponentHosts  a List of ServiceComponentHosts that needs to be updated as part of this operation
     * @return the last stage id generated, or -1 if no stages were created
     * @throws AmbariException if an error occurs while creating the relevant stages
     */
    abstract long createStages(Cluster cluster, Map<String, Host> hosts,
                               Map<String, Map<String, String>> kerberosConfigurations,
                               String clusterHostInfo, String hostParams,
                               ServiceComponentHostServerActionEvent event,
                               RoleCommandOrder roleCommandOrder,
                               KerberosDetails kerberosDetails, File dataDirectory,
                               RequestStageContainer requestStageContainer,
                               List<ServiceComponentHost> serviceComponentHosts,
                               Set<String> hostsWithValidKerberosClient)
        throws AmbariException;


    public void addCreatePrincipalsStage(Cluster cluster, String clusterHostInfoJson,
                                         String hostParamsJson, ServiceComponentHostServerActionEvent event,
                                         Map<String, String> commandParameters,
                                         RoleCommandOrder roleCommandOrder, RequestStageContainer requestStageContainer)
        throws AmbariException {
      Stage stage = createServerActionStage(requestStageContainer.getLastStageId(),
          cluster,
          requestStageContainer.getId(),
          "Create Principals",
          clusterHostInfoJson,
          "{}",
          hostParamsJson,
          CreatePrincipalsServerAction.class,
          event,
          commandParameters,
          "Create Principals",
          1200);

      RoleGraph roleGraph = new RoleGraph(roleCommandOrder);
      roleGraph.build(stage);
      requestStageContainer.addStages(roleGraph.getStages());
    }

    public void addDestroyPrincipalsStage(Cluster cluster, String clusterHostInfoJson,
                                          String hostParamsJson, ServiceComponentHostServerActionEvent event,
                                          Map<String, String> commandParameters,
                                          RoleCommandOrder roleCommandOrder, RequestStageContainer requestStageContainer)
        throws AmbariException {
      Stage stage = createServerActionStage(requestStageContainer.getLastStageId(),
          cluster,
          requestStageContainer.getId(),
          "Destroy Principals",
          clusterHostInfoJson,
          "{}",
          hostParamsJson,
          DestroyPrincipalsServerAction.class,
          event,
          commandParameters,
          "Destroy Principals",
          1200);

      RoleGraph roleGraph = new RoleGraph(roleCommandOrder);
      roleGraph.build(stage);
      requestStageContainer.addStages(roleGraph.getStages());
    }

    public void addCreateKeytabFilesStage(Cluster cluster, String clusterHostInfoJson,
                                          String hostParamsJson, ServiceComponentHostServerActionEvent event,
                                          Map<String, String> commandParameters,
                                          RoleCommandOrder roleCommandOrder, RequestStageContainer requestStageContainer)
        throws AmbariException {
      Stage stage = createServerActionStage(requestStageContainer.getLastStageId(),
          cluster,
          requestStageContainer.getId(),
          "Create Keytabs",
          clusterHostInfoJson,
          "{}",
          hostParamsJson,
          CreateKeytabFilesServerAction.class,
          event,
          commandParameters,
          "Create Keytabs",
          1200);

      RoleGraph roleGraph = new RoleGraph(roleCommandOrder);
      roleGraph.build(stage);
      requestStageContainer.addStages(roleGraph.getStages());
    }

    public void addDistributeKeytabFilesStage(Cluster cluster, List<ServiceComponentHost> serviceComponentHosts,
                                              String clusterHostInfoJson, String hostParamsJson,
                                              Map<String, String> commandParameters,
                                              RoleCommandOrder roleCommandOrder,
                                              RequestStageContainer requestStageContainer,
                                              Set<String> hostsWithValidKerberosClient)
        throws AmbariException {

      Stage stage = createNewStage(requestStageContainer.getLastStageId(),
          cluster,
          requestStageContainer.getId(),
          "Distribute Keytabs",
          clusterHostInfoJson,
          StageUtils.getGson().toJson(commandParameters),
          hostParamsJson);

      Collection<ServiceComponentHost> filteredComponents = filterServiceComponentHostsForHosts(
          new ArrayList<ServiceComponentHost>(serviceComponentHosts), hostsWithValidKerberosClient);

      if (!filteredComponents.isEmpty()) {
        List<String> hostsToUpdate = createUniqueHostList(filteredComponents, Collections.singleton(HostState.HEALTHY));
        Map<String, String> requestParams = new HashMap<String, String>();
        List<RequestResourceFilter> requestResourceFilters = new ArrayList<RequestResourceFilter>();
        RequestResourceFilter reqResFilter = new RequestResourceFilter(Service.Type.KERBEROS.name(), Role.KERBEROS_CLIENT.name(), hostsToUpdate);
        requestResourceFilters.add(reqResFilter);

        ActionExecutionContext actionExecContext = new ActionExecutionContext(
            cluster.getClusterName(),
            "SET_KEYTAB",
            requestResourceFilters,
            requestParams);
        customCommandExecutionHelper.addExecutionCommandsToStage(actionExecContext, stage, requestParams, false);
      }

      RoleGraph roleGraph = new RoleGraph(roleCommandOrder);
      roleGraph.build(stage);
      requestStageContainer.addStages(roleGraph.getStages());
    }

    /**
     * Filter out ServiceComponentHosts that are on on hosts in the specified set of host names.
     * <p/>
     * It is expected that the supplied collection is modifiable. It will be modified inplace.
     *
     * @param serviceComponentHosts a collection of ServiceComponentHost items to test
     * @param hosts                 a set of host names indicating valid hosts
     * @return a collection of filtered ServiceComponentHost items
     */
    private Collection<ServiceComponentHost> filterServiceComponentHostsForHosts(Collection<ServiceComponentHost> serviceComponentHosts,
                                                                                 Set<String> hosts) {

      if ((serviceComponentHosts != null) && (hosts != null)) {
        Iterator<ServiceComponentHost> iterator = serviceComponentHosts.iterator();
        while (iterator.hasNext()) {
          ServiceComponentHost sch = iterator.next();

          if (!hosts.contains(sch.getHostName())) {
            iterator.remove();
          }
        }
      }

      return serviceComponentHosts;
    }

    public void addDeleteKeytabFilesStage(Cluster cluster, List<ServiceComponentHost> serviceComponentHosts,
                                          String clusterHostInfoJson, String hostParamsJson,
                                          Map<String, String> commandParameters,
                                          RoleCommandOrder roleCommandOrder,
                                          RequestStageContainer requestStageContainer,
                                          Set<String> hostsWithValidKerberosClient)
        throws AmbariException {

      Stage stage = createNewStage(requestStageContainer.getLastStageId(),
          cluster,
          requestStageContainer.getId(),
          "Delete Keytabs",
          clusterHostInfoJson,
          StageUtils.getGson().toJson(commandParameters),
          hostParamsJson);

      Collection<ServiceComponentHost> filteredComponents = filterServiceComponentHostsForHosts(
          new ArrayList<ServiceComponentHost>(serviceComponentHosts), hostsWithValidKerberosClient);

      if (!filteredComponents.isEmpty()) {
        List<String> hostsToUpdate = createUniqueHostList(filteredComponents, Collections.singleton(HostState.HEALTHY));
        Map<String, String> requestParams = new HashMap<String, String>();
        List<RequestResourceFilter> requestResourceFilters = new ArrayList<RequestResourceFilter>();
        RequestResourceFilter reqResFilter = new RequestResourceFilter("KERBEROS", "KERBEROS_CLIENT", hostsToUpdate);
        requestResourceFilters.add(reqResFilter);

        ActionExecutionContext actionExecContext = new ActionExecutionContext(
            cluster.getClusterName(),
            "REMOVE_KEYTAB",
            requestResourceFilters,
            requestParams);
        customCommandExecutionHelper.addExecutionCommandsToStage(actionExecContext, stage, requestParams, false);
      }

      RoleGraph roleGraph = new RoleGraph(roleCommandOrder);
      roleGraph.build(stage);
      requestStageContainer.addStages(roleGraph.getStages());
    }

    public void addUpdateConfigurationsStage(Cluster cluster, String clusterHostInfoJson,
                                             String hostParamsJson, ServiceComponentHostServerActionEvent event,
                                             Map<String, String> commandParameters,
                                             RoleCommandOrder roleCommandOrder, RequestStageContainer requestStageContainer)
        throws AmbariException {
      Stage stage = createServerActionStage(requestStageContainer.getLastStageId(),
          cluster,
          requestStageContainer.getId(),
          "Update Configurations",
          clusterHostInfoJson,
          "{}",
          hostParamsJson,
          UpdateKerberosConfigsServerAction.class,
          event,
          commandParameters,
          "Update Service Configurations",
          1200);

      RoleGraph roleGraph = new RoleGraph(roleCommandOrder);
      roleGraph.build(stage);
      requestStageContainer.addStages(roleGraph.getStages());
    }

    public void addFinalizeOperationStage(Cluster cluster, String clusterHostInfoJson,
                                          String hostParamsJson, ServiceComponentHostServerActionEvent event,
                                          File dataDirectory,
                                          RoleCommandOrder roleCommandOrder, RequestStageContainer requestStageContainer)
        throws AmbariException {

      // Add the cleanup stage...
      Map<String, String> commandParameters = new HashMap<String, String>();
      commandParameters.put(KerberosServerAction.AUTHENTICATED_USER_NAME, ambariManagementController.getAuthName());
      commandParameters.put(KerberosServerAction.DATA_DIRECTORY, dataDirectory.getAbsolutePath());

      Stage stage = createServerActionStage(requestStageContainer.getLastStageId(),
          cluster,
          requestStageContainer.getId(),
          "Finalize Operations",
          clusterHostInfoJson,
          "{}",
          hostParamsJson,
          FinalizeKerberosServerAction.class,
          event,
          commandParameters,
          "Finalize Operations", 300);

      RoleGraph roleGraph = new RoleGraph(roleCommandOrder);
      roleGraph.build(stage);
      requestStageContainer.addStages(roleGraph.getStages());
    }
  }

  /**
   * EnableKerberosHandler is an implementation of the Handler interface used to enable Kerberos
   * on the relevant cluster
   * <p/>
   * This implementation attempts to set the Service and ServiceComponentHost _desired_ security
   * states to {@link org.apache.ambari.server.state.SecurityState#SECURED_KERBEROS} and the
   * ServiceComponentHost _current_ security state to {@link org.apache.ambari.server.state.SecurityState#SECURING}.
   * <p/>
   * To complete the process, this implementation creates the following stages:
   * <ol>
   * <li>create principals</li>
   * <li>create keytab files</li>
   * <li>distribute keytab files to the appropriate hosts</li>
   * <li>update relevant configurations</li>
   * </ol>
   */
  private class EnableKerberosHandler extends Handler {
    @Override
    public boolean shouldProcess(SecurityState desiredSecurityState, ServiceComponentHost sch) throws AmbariException {
      return (desiredSecurityState == SecurityState.SECURED_KERBEROS) &&
          (sch.getSecurityState() != SecurityState.SECURED_KERBEROS) &&
          (sch.getSecurityState() != SecurityState.SECURING);
    }

    @Override
    public SecurityState getNewDesiredSCHSecurityState() {
      return SecurityState.SECURED_KERBEROS;
    }

    @Override
    public SecurityState getNewSCHSecurityState() {
      return SecurityState.SECURING;
    }

    @Override
    public SecurityState getNewServiceSecurityState() {
      return SecurityState.SECURED_KERBEROS;
    }

    @Override
    public long createStages(Cluster cluster, Map<String, Host> hosts,
                             Map<String, Map<String, String>> kerberosConfigurations,
                             String clusterHostInfoJson, String hostParamsJson,
                             ServiceComponentHostServerActionEvent event,
                             RoleCommandOrder roleCommandOrder, KerberosDetails kerberosDetails,
                             File dataDirectory, RequestStageContainer requestStageContainer,
                             List<ServiceComponentHost> serviceComponentHosts,
                             Set<String> hostsWithValidKerberosClient)
        throws AmbariException {
      // If there are principals, keytabs, and configurations to process, setup the following sages:
      //  1) generate principals
      //  2) generate keytab files
      //  3) distribute keytab files
      //  4) update configurations

      // If a RequestStageContainer does not already exist, create a new one...
      if (requestStageContainer == null) {
        requestStageContainer = new RequestStageContainer(
            actionManager.getNextRequestId(),
            null,
            requestFactory,
            actionManager);
      }

      // If there are configurations to set, create a (temporary) data file to store the configuration
      // updates and fill it will the relevant configurations.
      if (!kerberosConfigurations.isEmpty()) {
        File configFile = new File(dataDirectory, KerberosConfigDataFile.DATA_FILE_NAME);
        KerberosConfigDataFileBuilder kerberosConfDataFileBuilder = null;
        try {
          kerberosConfDataFileBuilder = new KerberosConfigDataFileBuilder(configFile);

          for (Map.Entry<String, Map<String, String>> entry : kerberosConfigurations.entrySet()) {
            String type = entry.getKey();
            Map<String, String> properties = entry.getValue();

            if (properties != null) {
              for (Map.Entry<String, String> configTypeEntry : properties.entrySet()) {
                kerberosConfDataFileBuilder.addRecord(type,
                    configTypeEntry.getKey(),
                    configTypeEntry.getValue());
              }
            }
          }
        } catch (IOException e) {
          String message = String.format("Failed to write kerberos configurations file - %s", configFile.getAbsolutePath());
          LOG.error(message);
          throw new AmbariException(message, e);
        } finally {
          if (kerberosConfDataFileBuilder != null) {
            try {
              kerberosConfDataFileBuilder.close();
            } catch (IOException e) {
              LOG.warn("Failed to close the kerberos configurations file writer", e);
            }
          }
        }
      }

      Map<String, String> commandParameters = new HashMap<String, String>();
      commandParameters.put(KerberosServerAction.AUTHENTICATED_USER_NAME, ambariManagementController.getAuthName());
      commandParameters.put(KerberosServerAction.DATA_DIRECTORY, dataDirectory.getAbsolutePath());
      commandParameters.put(KerberosServerAction.DEFAULT_REALM, kerberosDetails.getDefaultRealm());
      commandParameters.put(KerberosServerAction.KDC_TYPE, kerberosDetails.getKdcType().name());
      commandParameters.put(KerberosServerAction.ADMINISTRATOR_CREDENTIAL, getEncryptedAdministratorCredentials(cluster));

      // *****************************************************************
      // Create stage to create principals
      addCreatePrincipalsStage(cluster, clusterHostInfoJson, hostParamsJson, event, commandParameters,
          roleCommandOrder, requestStageContainer);

      // *****************************************************************
      // Create stage to generate keytabs
      addCreateKeytabFilesStage(cluster, clusterHostInfoJson, hostParamsJson, event, commandParameters,
          roleCommandOrder, requestStageContainer);

      // *****************************************************************
      // Create stage to distribute keytabs
      addDistributeKeytabFilesStage(cluster, serviceComponentHosts, clusterHostInfoJson, hostParamsJson,
          commandParameters, roleCommandOrder, requestStageContainer, hostsWithValidKerberosClient);

      // *****************************************************************
      // Create stage to update configurations of services
      addUpdateConfigurationsStage(cluster, clusterHostInfoJson, hostParamsJson, event, commandParameters,
          roleCommandOrder, requestStageContainer);

      return requestStageContainer.getLastStageId();
    }

  }

  /**
   * DisableKerberosHandler is an implementation of the Handler interface used to disable Kerberos
   * on the relevant cluster
   * <p/>
   * This implementation attempts to set the Service and ServiceComponentHost _desired_ security
   * states to {@link org.apache.ambari.server.state.SecurityState#UNSECURED} and the ServiceComponentHost
   * _current_ security state to {@link org.apache.ambari.server.state.SecurityState#UNSECURING}.
   * <p/>
   * To complete the process, this implementation creates the following stages:
   * <ol>
   * <li>update relevant configurations</li>
   * <li>delete keytab files</li>
   * <li>remove principals</li>
   * <li>restart services</li>
   * </ol>
   */
  private class DisableKerberosHandler extends Handler {
    @Override
    public boolean shouldProcess(SecurityState desiredSecurityState, ServiceComponentHost sch) throws AmbariException {
      return (desiredSecurityState == SecurityState.UNSECURED) &&
          ((sch.getDesiredSecurityState() != SecurityState.UNSECURED) || (sch.getSecurityState() != SecurityState.UNSECURED)) &&
          (sch.getSecurityState() != SecurityState.UNSECURING);
    }

    @Override
    public SecurityState getNewDesiredSCHSecurityState() {
      return SecurityState.UNSECURED;
    }

    @Override
    public SecurityState getNewSCHSecurityState() {
      return SecurityState.UNSECURING;
    }

    @Override
    public SecurityState getNewServiceSecurityState() {
      return SecurityState.UNSECURED;
    }

    @Override
    public long createStages(Cluster cluster, Map<String, Host> hosts,
                             Map<String, Map<String, String>> kerberosConfigurations,
                             String clusterHostInfoJson, String hostParamsJson,
                             ServiceComponentHostServerActionEvent event,
                             RoleCommandOrder roleCommandOrder, KerberosDetails kerberosDetails,
                             File dataDirectory, RequestStageContainer requestStageContainer,
                             List<ServiceComponentHost> serviceComponentHosts,
                             Set<String> hostsWithValidKerberosClient) throws AmbariException {
      //  1) revert configurations

      // If a RequestStageContainer does not already exist, create a new one...
      if (requestStageContainer == null) {
        requestStageContainer = new RequestStageContainer(
            actionManager.getNextRequestId(),
            null,
            requestFactory,
            actionManager);
      }

      Map<String, String> commandParameters = new HashMap<String, String>();
      commandParameters.put(KerberosServerAction.AUTHENTICATED_USER_NAME, ambariManagementController.getAuthName());
      commandParameters.put(KerberosServerAction.DATA_DIRECTORY, dataDirectory.getAbsolutePath());
      commandParameters.put(KerberosServerAction.DEFAULT_REALM, kerberosDetails.getDefaultRealm());
      commandParameters.put(KerberosServerAction.KDC_TYPE, kerberosDetails.getKdcType().name());
      commandParameters.put(KerberosServerAction.ADMINISTRATOR_CREDENTIAL, getEncryptedAdministratorCredentials(cluster));

      // If there are configurations to set, create a (temporary) data file to store the configuration
      // updates and fill it will the relevant configurations.
      if (!kerberosConfigurations.isEmpty()) {
        File configFile = new File(dataDirectory, KerberosConfigDataFile.DATA_FILE_NAME);
        KerberosConfigDataFileBuilder kerberosConfDataFileBuilder = null;

        if (serviceComponentHosts != null) {
          Set<String> visitedServices = new HashSet<String>();

          for (ServiceComponentHost sch : serviceComponentHosts) {
            String serviceName = sch.getServiceName();

            if (!visitedServices.contains(serviceName)) {
              StackId stackVersion = sch.getStackVersion();

              visitedServices.add(serviceName);

              if (stackVersion != null) {
                Set<PropertyInfo> serviceProperties = configHelper.getServiceProperties(stackVersion, serviceName, true);

                if (serviceProperties != null) {
                  for (PropertyInfo propertyInfo : serviceProperties) {
                    String filename = propertyInfo.getFilename();

                    if (filename != null) {
                      Map<String, String> kerberosConfiguration = kerberosConfigurations.get(ConfigHelper.fileNameToConfigType(filename));

                      if ((kerberosConfiguration != null) && (kerberosConfiguration.containsKey(propertyInfo.getName()))) {
                        kerberosConfiguration.put(propertyInfo.getName(), propertyInfo.getValue());
                      }
                    }
                  }
                }
              }
            }
          }
        }

        try {
          kerberosConfDataFileBuilder = new KerberosConfigDataFileBuilder(configFile);

          for (Map.Entry<String, Map<String, String>> entry : kerberosConfigurations.entrySet()) {
            String type = entry.getKey();
            Map<String, String> properties = entry.getValue();

            if (properties != null) {
              for (Map.Entry<String, String> configTypeEntry : properties.entrySet()) {
                kerberosConfDataFileBuilder.addRecord(type,
                    configTypeEntry.getKey(),
                    configTypeEntry.getValue());
              }
            }
          }
        } catch (IOException e) {
          String message = String.format("Failed to write kerberos configurations file - %s", configFile.getAbsolutePath());
          LOG.error(message);
          throw new AmbariException(message, e);
        } finally {
          if (kerberosConfDataFileBuilder != null) {
            try {
              kerberosConfDataFileBuilder.close();
            } catch (IOException e) {
              LOG.warn("Failed to close the kerberos configurations file writer", e);
            }
          }
        }
      }

      // *****************************************************************
      // Create stage to update configurations of services
      addUpdateConfigurationsStage(cluster, clusterHostInfoJson, hostParamsJson, event, commandParameters,
          roleCommandOrder, requestStageContainer);

      // *****************************************************************
      // Create stage to remove principals
      addDestroyPrincipalsStage(cluster, clusterHostInfoJson, hostParamsJson, event, commandParameters,
          roleCommandOrder, requestStageContainer);

      // *****************************************************************
      // Create stage to delete keytabs
      addDeleteKeytabFilesStage(cluster, serviceComponentHosts, clusterHostInfoJson,
          hostParamsJson, commandParameters, roleCommandOrder, requestStageContainer, hostsWithValidKerberosClient);

      return requestStageContainer.getLastStageId();
    }
  }

  /**
   * CreatePrincipalsAndKeytabsHandler is an implementation of the Handler interface used to create
   * principals and keytabs and distribute them throughout the cluster.  This is similar to enabling
   * Kerberos however no states or configurations will be updated.
   * <p/>
   * To complete the process, this implementation creates the following stages:
   * <ol>
   * <li>create principals</li>
   * <li>create keytab files</li>
   * <li>distribute keytab files to the appropriate hosts</li>
   * </ol>
   */
  private class CreatePrincipalsAndKeytabsHandler extends Handler {
    /**
     * A boolean value indicating whether to create keytabs for all principals (<code>true</code>)
     * or only the ones that are missing (<code>false</code>).
     */
    private boolean regenerateAllKeytabs;

    /**
     * CreatePrincipalsAndKeytabsHandler constructor to set whether this instance should be used to
     * regenerate all keytabs or just the ones that have not been distributed
     *
     * @param regenerateAllKeytabs A boolean value indicating whether to create keytabs for all
     *                             principals (<code>true</code> or only the ones that are missing
     *                             (<code>false</code>)
     */
    public CreatePrincipalsAndKeytabsHandler(boolean regenerateAllKeytabs) {
      this.regenerateAllKeytabs = regenerateAllKeytabs;
    }

    @Override
    public boolean shouldProcess(SecurityState desiredSecurityState, ServiceComponentHost sch) throws AmbariException {
      return true;
    }

    @Override
    public SecurityState getNewDesiredSCHSecurityState() {
      return null;
    }

    @Override
    public SecurityState getNewSCHSecurityState() {
      return null;
    }

    @Override
    public SecurityState getNewServiceSecurityState() {
      return null;
    }

    @Override
    public long createStages(Cluster cluster, Map<String, Host> hosts,
                             Map<String, Map<String, String>> kerberosConfigurations,
                             String clusterHostInfoJson, String hostParamsJson,
                             ServiceComponentHostServerActionEvent event,
                             RoleCommandOrder roleCommandOrder, KerberosDetails kerberosDetails,
                             File dataDirectory, RequestStageContainer requestStageContainer,
                             List<ServiceComponentHost> serviceComponentHosts,
                             Set<String> hostsWithValidKerberosClient)
        throws AmbariException {
      // If there are principals and keytabs to process, setup the following sages:
      //  1) generate principals
      //  2) generate keytab files
      //  3) distribute keytab files

      // If a RequestStageContainer does not already exist, create a new one...
      if (requestStageContainer == null) {
        requestStageContainer = new RequestStageContainer(
            actionManager.getNextRequestId(),
            null,
            requestFactory,
            actionManager);
      }

      Map<String, String> commandParameters = new HashMap<String, String>();
      commandParameters.put(KerberosServerAction.AUTHENTICATED_USER_NAME, ambariManagementController.getAuthName());
      commandParameters.put(KerberosServerAction.DATA_DIRECTORY, dataDirectory.getAbsolutePath());
      commandParameters.put(KerberosServerAction.DEFAULT_REALM, kerberosDetails.getDefaultRealm());
      commandParameters.put(KerberosServerAction.KDC_TYPE, kerberosDetails.getKdcType().name());
      commandParameters.put(KerberosServerAction.ADMINISTRATOR_CREDENTIAL, getEncryptedAdministratorCredentials(cluster));
      commandParameters.put(KerberosServerAction.REGENERATE_ALL, (regenerateAllKeytabs) ? "true" : "false");

      // *****************************************************************
      // Create stage to create principals
      addCreatePrincipalsStage(cluster, clusterHostInfoJson, hostParamsJson, event,
          commandParameters, roleCommandOrder, requestStageContainer);

      // *****************************************************************
      // Create stage to generate keytabs
      addCreateKeytabFilesStage(cluster, clusterHostInfoJson, hostParamsJson, event,
          commandParameters, roleCommandOrder, requestStageContainer);

      // Create stage to distribute keytabs
      addDistributeKeytabFilesStage(cluster, serviceComponentHosts, clusterHostInfoJson,
          hostParamsJson, commandParameters, roleCommandOrder, requestStageContainer, hostsWithValidKerberosClient);

      return requestStageContainer.getLastStageId();
    }
  }

  /**
   * DeletePrincipalsAndKeytabsHandler is an implementation of the Handler interface used to delete
   * principals and keytabs throughout the cluster.
   * <p/>
   * To complete the process, this implementation creates the following stages:
   * <ol>
   * <li>delete principals</li>
   * <li>remove keytab files</li>
   * </ol>
   */
  private class DeletePrincipalsAndKeytabsHandler extends Handler {

    @Override
    public boolean shouldProcess(SecurityState desiredSecurityState, ServiceComponentHost sch) throws AmbariException {
      return true;
    }

    @Override
    public SecurityState getNewDesiredSCHSecurityState() {
      return null;
    }

    @Override
    public SecurityState getNewSCHSecurityState() {
      return null;
    }

    @Override
    public SecurityState getNewServiceSecurityState() {
      return null;
    }

    @Override
    public long createStages(Cluster cluster, Map<String, Host> hosts,
                             Map<String, Map<String, String>> kerberosConfigurations,
                             String clusterHostInfoJson, String hostParamsJson,
                             ServiceComponentHostServerActionEvent event,
                             RoleCommandOrder roleCommandOrder, KerberosDetails kerberosDetails,
                             File dataDirectory, RequestStageContainer requestStageContainer,
                             List<ServiceComponentHost> serviceComponentHosts,
                             Set<String> hostsWithValidKerberosClient)
        throws AmbariException {
      // If there are principals and keytabs to process, setup the following sages:
      //  1) delete principals
      //  2) delete keytab files

      // If a RequestStageContainer does not already exist, create a new one...
      if (requestStageContainer == null) {
        requestStageContainer = new RequestStageContainer(
            actionManager.getNextRequestId(),
            null,
            requestFactory,
            actionManager);
      }

      Map<String, String> commandParameters = new HashMap<String, String>();
      commandParameters.put(KerberosServerAction.AUTHENTICATED_USER_NAME, ambariManagementController.getAuthName());
      commandParameters.put(KerberosServerAction.DATA_DIRECTORY, dataDirectory.getAbsolutePath());
      commandParameters.put(KerberosServerAction.DEFAULT_REALM, kerberosDetails.getDefaultRealm());
      commandParameters.put(KerberosServerAction.KDC_TYPE, kerberosDetails.getKdcType().name());
      commandParameters.put(KerberosServerAction.ADMINISTRATOR_CREDENTIAL, getEncryptedAdministratorCredentials(cluster));

      // *****************************************************************
      // Create stage to delete principals
      addDestroyPrincipalsStage(cluster, clusterHostInfoJson, hostParamsJson, event,
          commandParameters, roleCommandOrder, requestStageContainer);

      // *****************************************************************
      // Create stage to delete keytabs
      addDeleteKeytabFilesStage(cluster, serviceComponentHosts, clusterHostInfoJson,
          hostParamsJson, commandParameters, roleCommandOrder, requestStageContainer, hostsWithValidKerberosClient);

      return requestStageContainer.getLastStageId();
    }
  }

  /**
   * KerberosDetails is a helper class to hold the details of the relevant Kerberos-specific
   * configurations so they may be passed around more easily.
   */
  private static class KerberosDetails {
    private String defaultRealm;
    private KDCType kdcType;
    private Map<String, String> kerberosEnvProperties;
    private SecurityType securityType;

    public void setDefaultRealm(String defaultRealm) {
      this.defaultRealm = defaultRealm;
    }

    public String getDefaultRealm() {
      return defaultRealm;
    }

    public void setKdcType(KDCType kdcType) {
      this.kdcType = kdcType;
    }

    public KDCType getKdcType() {
      return kdcType;
    }

    public void setKerberosEnvProperties(Map<String, String> kerberosEnvProperties) {
      this.kerberosEnvProperties = kerberosEnvProperties;
    }

    public Map<String, String> getKerberosEnvProperties() {
      return kerberosEnvProperties;
    }

    public void setSecurityType(SecurityType securityType) {
      this.securityType = securityType;
    }

    public SecurityType getSecurityType() {
      return securityType;
    }
  }
}
