content: /*
   * 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 static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.AGENT_STACK_RETRY_COUNT;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.AGENT_STACK_RETRY_ON_UNAVAILABILITY;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.AMBARI_DB_RCA_DRIVER;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.AMBARI_DB_RCA_PASSWORD;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.AMBARI_DB_RCA_URL;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.AMBARI_DB_RCA_USERNAME;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.AMBARI_JAVA_HOME;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.CLIENTS_TO_UPDATE_CONFIGS;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.CLUSTER_NAME;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.COMMAND_RETRY_ENABLED;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.COMMAND_TIMEOUT;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.CUSTOM_FOLDER;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.DB_DRIVER_FILENAME;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.DB_NAME;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.DFS_TYPE;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.GPL_LICENSE_ACCEPTED;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.GROUP_LIST;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.HOOKS_FOLDER;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.HOST_SYS_PREPPED;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.JAVA_HOME;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.JAVA_VERSION;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.JCE_NAME;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.JDK_LOCATION;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.JDK_NAME;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.MAX_DURATION_OF_RETRIES;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.MYSQL_JDBC_URL;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.NOT_MANAGED_HDFS_PATH_LIST;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.ORACLE_JDBC_URL;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.PACKAGE_LIST;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.SCRIPT;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.SCRIPT_TYPE;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.SERVICE_PACKAGE_FOLDER;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.SERVICE_REPO_INFO;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.STACK_NAME;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.STACK_VERSION;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.USER_GROUPS;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.USER_LIST;
  import static org.apache.ambari.server.agent.ExecutionCommand.KeyNames.VERSION;
  import static org.apache.ambari.server.controller.AmbariCustomCommandExecutionHelper.masterToSlaveMappingForDecom;
  
  import java.io.File;
  import java.io.FileNotFoundException;
  import java.io.FileReader;
  import java.io.IOException;
  import java.lang.reflect.Type;
  import java.net.InetAddress;
  import java.text.MessageFormat;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.EnumMap;
  import java.util.EnumSet;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.LinkedHashSet;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Set;
  import java.util.SortedMap;
  import java.util.TreeMap;
  import java.util.UUID;
  import java.util.concurrent.TimeUnit;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  import java.util.stream.Collectors;
  
  import javax.annotation.Nullable;
  
  import jakarta.persistence.RollbackException;
  
  import org.apache.ambari.annotations.Experimental;
  import org.apache.ambari.annotations.ExperimentalFeature;
  import org.apache.ambari.server.AmbariException;
  import org.apache.ambari.server.ClusterNotFoundException;
  import org.apache.ambari.server.DuplicateResourceException;
  import org.apache.ambari.server.HostNotFoundException;
  import org.apache.ambari.server.ObjectNotFoundException;
  import org.apache.ambari.server.ParentObjectNotFoundException;
  import org.apache.ambari.server.Role;
  import org.apache.ambari.server.RoleCommand;
  import org.apache.ambari.server.ServiceComponentHostNotFoundException;
  import org.apache.ambari.server.ServiceComponentNotFoundException;
  import org.apache.ambari.server.ServiceNotFoundException;
  import org.apache.ambari.server.StackAccessException;
  import org.apache.ambari.server.actionmanager.ActionManager;
  import org.apache.ambari.server.actionmanager.CommandExecutionType;
  import org.apache.ambari.server.actionmanager.ExecutionCommandWrapper;
  import org.apache.ambari.server.actionmanager.HostRoleCommand;
  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.agent.CommandRepository;
  import org.apache.ambari.server.agent.ExecutionCommand;
  import org.apache.ambari.server.agent.stomp.HostLevelParamsHolder;
  import org.apache.ambari.server.agent.stomp.TopologyHolder;
  import org.apache.ambari.server.agent.stomp.dto.MetadataCluster;
  import org.apache.ambari.server.agent.stomp.dto.MetadataServiceInfo;
  import org.apache.ambari.server.agent.stomp.dto.TopologyCluster;
  import org.apache.ambari.server.agent.stomp.dto.TopologyComponent;
  import org.apache.ambari.server.agent.stomp.dto.TopologyUpdateHandlingReport;
  import org.apache.ambari.server.api.services.AmbariMetaInfo;
  import org.apache.ambari.server.api.services.LoggingService;
  import org.apache.ambari.server.configuration.Configuration;
  import org.apache.ambari.server.configuration.Configuration.DatabaseType;
  import org.apache.ambari.server.controller.internal.DeleteHostComponentStatusMetaData;
  import org.apache.ambari.server.controller.internal.DeleteStatusMetaData;
  import org.apache.ambari.server.controller.internal.HostComponentResourceProvider;
  import org.apache.ambari.server.controller.internal.RequestOperationLevel;
  import org.apache.ambari.server.controller.internal.RequestResourceFilter;
  import org.apache.ambari.server.controller.internal.RequestResourceProvider;
  import org.apache.ambari.server.controller.internal.RequestStageContainer;
  import org.apache.ambari.server.controller.internal.URLRedirectProvider;
  import org.apache.ambari.server.controller.internal.WidgetLayoutResourceProvider;
  import org.apache.ambari.server.controller.internal.WidgetResourceProvider;
  import org.apache.ambari.server.controller.logging.LoggingSearchPropertyProvider;
  import org.apache.ambari.server.controller.metrics.MetricPropertyProviderFactory;
  import org.apache.ambari.server.controller.metrics.MetricsCollectorHAManager;
  import org.apache.ambari.server.controller.metrics.timeline.cache.TimelineMetricCacheProvider;
  import org.apache.ambari.server.controller.spi.Resource;
  import org.apache.ambari.server.controller.spi.ResourceAlreadyExistsException;
  import org.apache.ambari.server.controller.spi.SystemException;
  import org.apache.ambari.server.customactions.ActionDefinition;
  import org.apache.ambari.server.events.MetadataUpdateEvent;
  import org.apache.ambari.server.events.TopologyUpdateEvent;
  import org.apache.ambari.server.events.UpdateEventType;
  import org.apache.ambari.server.events.publishers.AmbariEventPublisher;
  import org.apache.ambari.server.metadata.ActionMetadata;
  import org.apache.ambari.server.metadata.RoleCommandOrder;
  import org.apache.ambari.server.metadata.RoleCommandOrderProvider;
  import org.apache.ambari.server.orm.dao.ClusterDAO;
  import org.apache.ambari.server.orm.dao.ExtensionDAO;
  import org.apache.ambari.server.orm.dao.ExtensionLinkDAO;
  import org.apache.ambari.server.orm.dao.HostComponentDesiredStateDAO;
  import org.apache.ambari.server.orm.dao.RepositoryVersionDAO;
  import org.apache.ambari.server.orm.dao.ServiceComponentDesiredStateDAO;
  import org.apache.ambari.server.orm.dao.SettingDAO;
  import org.apache.ambari.server.orm.dao.StackDAO;
  import org.apache.ambari.server.orm.dao.WidgetDAO;
  import org.apache.ambari.server.orm.dao.WidgetLayoutDAO;
  import org.apache.ambari.server.orm.entities.ClusterEntity;
  import org.apache.ambari.server.orm.entities.ExtensionLinkEntity;
  import org.apache.ambari.server.orm.entities.HostComponentDesiredStateEntity;
  import org.apache.ambari.server.orm.entities.HostEntity;
  import org.apache.ambari.server.orm.entities.MpackEntity;
  import org.apache.ambari.server.orm.entities.RepoDefinitionEntity;
  import org.apache.ambari.server.orm.entities.RepoOsEntity;
  import org.apache.ambari.server.orm.entities.RepositoryVersionEntity;
  import org.apache.ambari.server.orm.entities.ServiceComponentDesiredStateEntity;
  import org.apache.ambari.server.orm.entities.SettingEntity;
  import org.apache.ambari.server.orm.entities.StackEntity;
  import org.apache.ambari.server.orm.entities.WidgetEntity;
  import org.apache.ambari.server.orm.entities.WidgetLayoutEntity;
  import org.apache.ambari.server.orm.entities.WidgetLayoutUserWidgetEntity;
  import org.apache.ambari.server.scheduler.ExecutionScheduleManager;
  import org.apache.ambari.server.security.authorization.AuthorizationException;
  import org.apache.ambari.server.security.authorization.AuthorizationHelper;
  import org.apache.ambari.server.security.authorization.Group;
  import org.apache.ambari.server.security.authorization.GroupType;
  import org.apache.ambari.server.security.authorization.ResourceType;
  import org.apache.ambari.server.security.authorization.RoleAuthorization;
  import org.apache.ambari.server.security.authorization.User;
  import org.apache.ambari.server.security.authorization.Users;
  import org.apache.ambari.server.security.encryption.CredentialStoreService;
  import org.apache.ambari.server.security.encryption.CredentialStoreType;
  import org.apache.ambari.server.security.ldap.AmbariLdapDataPopulator;
  import org.apache.ambari.server.security.ldap.LdapBatchDto;
  import org.apache.ambari.server.security.ldap.LdapSyncDto;
  import org.apache.ambari.server.security.ldap.LdapUserDto;
  import org.apache.ambari.server.serveraction.kerberos.KerberosInvalidConfigurationException;
  import org.apache.ambari.server.serveraction.kerberos.KerberosOperationException;
  import org.apache.ambari.server.stack.ExtensionHelper;
  import org.apache.ambari.server.stack.RepoUtil;
  import org.apache.ambari.server.stack.upgrade.RepositoryVersionHelper;
  import org.apache.ambari.server.stageplanner.RoleGraph;
  import org.apache.ambari.server.stageplanner.RoleGraphFactory;
  import org.apache.ambari.server.state.BlueprintProvisioningState;
  import org.apache.ambari.server.state.Cluster;
  import org.apache.ambari.server.state.Clusters;
  import org.apache.ambari.server.state.CommandScriptDefinition;
  import org.apache.ambari.server.state.ComponentInfo;
  import org.apache.ambari.server.state.Config;
  import org.apache.ambari.server.state.ConfigFactory;
  import org.apache.ambari.server.state.ConfigHelper;
  import org.apache.ambari.server.state.DependencyInfo;
  import org.apache.ambari.server.state.DesiredConfig;
  import org.apache.ambari.server.state.ExtensionInfo;
  import org.apache.ambari.server.state.Host;
  import org.apache.ambari.server.state.HostState;
  import org.apache.ambari.server.state.MaintenanceState;
  import org.apache.ambari.server.state.Module;
  import org.apache.ambari.server.state.Mpack;
  import org.apache.ambari.server.state.OperatingSystemInfo;
  import org.apache.ambari.server.state.PropertyDependencyInfo;
  import org.apache.ambari.server.state.PropertyInfo;
  import org.apache.ambari.server.state.PropertyInfo.PropertyType;
  import org.apache.ambari.server.state.RepositoryInfo;
  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.ServiceComponentFactory;
  import org.apache.ambari.server.state.ServiceComponentHost;
  import org.apache.ambari.server.state.ServiceComponentHostEvent;
  import org.apache.ambari.server.state.ServiceComponentHostFactory;
  import org.apache.ambari.server.state.ServiceInfo;
  import org.apache.ambari.server.state.ServiceOsSpecific;
  import org.apache.ambari.server.state.StackId;
  import org.apache.ambari.server.state.StackInfo;
  import org.apache.ambari.server.state.State;
  import org.apache.ambari.server.state.UnlimitedKeyJCERequirement;
  import org.apache.ambari.server.state.configgroup.ConfigGroupFactory;
  import org.apache.ambari.server.state.fsm.InvalidStateTransitionException;
  import org.apache.ambari.server.state.quicklinksprofile.QuickLinkVisibilityController;
  import org.apache.ambari.server.state.quicklinksprofile.QuickLinkVisibilityControllerFactory;
  import org.apache.ambari.server.state.quicklinksprofile.QuickLinksProfile;
  import org.apache.ambari.server.state.repository.VersionDefinitionXml;
  import org.apache.ambari.server.state.scheduler.RequestExecutionFactory;
  import org.apache.ambari.server.state.stack.OsFamily;
  import org.apache.ambari.server.state.stack.RepositoryXml;
  import org.apache.ambari.server.state.stack.WidgetLayout;
  import org.apache.ambari.server.state.stack.WidgetLayoutInfo;
  import org.apache.ambari.server.state.svccomphost.ServiceComponentHostInstallEvent;
  import org.apache.ambari.server.state.svccomphost.ServiceComponentHostOpInProgressEvent;
  import org.apache.ambari.server.state.svccomphost.ServiceComponentHostOpSucceededEvent;
  import org.apache.ambari.server.state.svccomphost.ServiceComponentHostStartEvent;
  import org.apache.ambari.server.state.svccomphost.ServiceComponentHostStopEvent;
  import org.apache.ambari.server.state.svccomphost.ServiceComponentHostUpgradeEvent;
  import org.apache.ambari.server.topology.STOMPComponentsDeleteHandler;
  import org.apache.ambari.server.topology.Setting;
  import org.apache.ambari.server.utils.SecretReference;
  import org.apache.ambari.server.utils.StageUtils;
  import org.apache.ambari.server.utils.URLCredentialsHider;
  import org.apache.commons.collections4.CollectionUtils;
  import org.apache.commons.collections4.MapUtils;
  import org.apache.commons.lang.BooleanUtils;
  import org.apache.commons.lang.StringUtils;
  import org.apache.commons.lang.math.NumberUtils;
  import org.apache.commons.text.StringEscapeUtils;
  import org.apache.http.HttpStatus;
  import org.apache.http.client.utils.URIBuilder;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.google.common.cache.Cache;
  import com.google.common.cache.CacheBuilder;
  import com.google.common.collect.ArrayListMultimap;
  import com.google.common.collect.ListMultimap;
  import com.google.common.collect.Multimap;
  import com.google.gson.Gson;
  import com.google.gson.reflect.TypeToken;
  import com.google.inject.Inject;
  import com.google.inject.Injector;
  import com.google.inject.Provider;
  import com.google.inject.Singleton;
  import com.google.inject.persist.Transactional;
  
  @Singleton
  public class AmbariManagementControllerImpl implements AmbariManagementController {
  
    private final static Logger LOG =
        LoggerFactory.getLogger(AmbariManagementControllerImpl.class);
    private final static Logger configChangeLog = LoggerFactory.getLogger("configchange");
  
    private static final Type hostAttributesType =
            new TypeToken<Map<String, String>>() {}.getType();
  
    public static final String CLUSTER_PHASE_PROPERTY = "phase";
    public static final String CLUSTER_PHASE_INITIAL_INSTALL = "INITIAL_INSTALL";
    public static final String CLUSTER_PHASE_INITIAL_START = "INITIAL_START";
    private static final String AMBARI_SERVER_HOST = "ambari_server_host";
    private static final String AMBARI_SERVER_PORT = "ambari_server_port";
    private static final String AMBARI_SERVER_USE_SSL = "ambari_server_use_ssl";
  
    private static final String BASE_LOG_DIR = "/tmp/ambari";
  
    private static final String PASSWORD = "password";
  
    public static final String CLUSTER_NAME_VALIDATION_REGEXP = "^[a-zA-Z0-9_-]{1,100}$";
    public static final Pattern CLUSTER_NAME_PTRN = Pattern.compile(CLUSTER_NAME_VALIDATION_REGEXP);
  
    private final Clusters clusters;
  
    private final ActionManager actionManager;
  
    private final Injector injector;
  
    private final Gson gson;
  
  
    @Inject
    private RoleCommandOrderProvider roleCommandOrderProvider;
  
    @Inject
    private ServiceComponentFactory serviceComponentFactory;
    @Inject
    private ServiceComponentHostFactory serviceComponentHostFactory;
    @Inject
    private ConfigFactory configFactory;
    @Inject
    private StageFactory stageFactory;
    @Inject
    private RequestFactory requestFactory;
    @Inject
    private ActionMetadata actionMetadata;
    @Inject
    private AmbariMetaInfo ambariMetaInfo;
    @Inject
    private Users users;
    @Inject
    private HostsMap hostsMap;
    @Inject
    private Configuration configs;
    @Inject
    private AbstractRootServiceResponseFactory rootServiceResponseFactory;
    @Inject
    private RoleGraphFactory roleGraphFactory;
    @Inject
    private ConfigGroupFactory configGroupFactory;
    @Inject
    private ConfigHelper configHelper;
    @Inject
    private RequestExecutionFactory requestExecutionFactory;
    @Inject
    private ExecutionScheduleManager executionScheduleManager;
    @Inject
    private AmbariLdapDataPopulator ldapDataPopulator;
    @Inject
    private RepositoryVersionDAO repositoryVersionDAO;
    @Inject
    private WidgetDAO widgetDAO;
    @Inject
    private WidgetLayoutDAO widgetLayoutDAO;
    @Inject
    private ClusterDAO clusterDAO;
    @Inject
    private CredentialStoreService credentialStoreService;
    @Inject
    private SettingDAO settingDAO;
  
    private MaintenanceStateHelper maintenanceStateHelper;
  
    private AmbariManagementHelper helper;
  
    @Inject
    private ExtensionDAO extensionDAO;
    @Inject
    private ExtensionLinkDAO linkDAO;
    @Inject
    private StackDAO stackDAO;
    @Inject
    protected OsFamily osFamily;
  
    @Inject
    private STOMPComponentsDeleteHandler STOMPComponentsDeleteHandler;
  
    @Inject
    private Provider<TopologyHolder> m_topologyHolder;
  
    @Inject
    private Provider<HostLevelParamsHolder> m_hostLevelParamsHolder;
  
    @Inject
    private ServiceComponentDesiredStateDAO serviceComponentDesiredStateDAO;
  
    @Inject
    private RepositoryVersionHelper repoVersionHelper;
  
    @Inject
    private HostComponentDesiredStateDAO hostComponentDesiredStateDAO;
  
    /**
     * The KerberosHelper to help setup for enabling for disabling Kerberos
     */
    private KerberosHelper kerberosHelper;
  
    final private String masterHostname;
    final private Integer masterPort;
    final private String masterProtocol;
  
    final private static String JDK_RESOURCE_LOCATION = "/resources";
  
    final private static int REPO_URL_CONNECT_TIMEOUT = 3000;
    final private static int REPO_URL_READ_TIMEOUT = 2000;
  
    final private String jdkResourceUrl;
    final private String javaHome;
    final private String ambariJavaHome;
    final private String jdkName;
    final private String jceName;
    final private String ojdbcUrl;
    final private String serverDB;
    final private String mysqljdbcUrl;
  
    private boolean ldapSyncInProgress;
  
    private Cache<ClusterRequest, ClusterResponse> clusterUpdateCache =
        CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build();
    private Cache<ConfigGroupRequest, ConfigGroupResponse> configGroupUpdateCache =
            CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build();
  
    @Inject
    private AmbariCustomCommandExecutionHelper customCommandExecutionHelper;
  
    @Inject
    private AmbariActionExecutionHelper actionExecutionHelper;
  
    private Map<String, Map<String, Map<String, String>>> configCredentialsForService = new HashMap<>();
  
    @Inject
    public AmbariManagementControllerImpl(ActionManager actionManager,
        Clusters clusters, Injector injector) throws Exception {
      this.clusters = clusters;
      this.actionManager = actionManager;
      this.injector = injector;
      injector.injectMembers(this);
      gson = injector.getInstance(Gson.class);
      LOG.info("Initializing the AmbariManagementControllerImpl");
      masterHostname =  InetAddress.getLocalHost().getCanonicalHostName();
      maintenanceStateHelper = injector.getInstance(MaintenanceStateHelper.class);
      kerberosHelper = injector.getInstance(KerberosHelper.class);
      if(configs != null)
      {
        if (configs.getApiSSLAuthentication()) {
          masterProtocol = "https";
          masterPort = configs.getClientSSLApiPort();
        } else {
          masterProtocol = "http";
          masterPort = configs.getClientApiPort();
        }
        jdkResourceUrl = getAmbariServerURI(JDK_RESOURCE_LOCATION);
        javaHome = configs.getJavaHome();
        ambariJavaHome = configs.getAmbariJavaHome();
        jdkName = configs.getJDKName();
        jceName = configs.getJCEName();
        ojdbcUrl = getAmbariServerURI(JDK_RESOURCE_LOCATION + "/" + configs.getOjdbcJarName());
        mysqljdbcUrl = getAmbariServerURI(JDK_RESOURCE_LOCATION + "/" + configs.getMySQLJarName());
  
        serverDB = configs.getServerDBName();
      } else {
        masterProtocol = null;
        masterPort = null;
  
        jdkResourceUrl = null;
        javaHome = null;
        ambariJavaHome = null;
        jdkName = null;
        jceName = null;
        ojdbcUrl = null;
        mysqljdbcUrl = null;
        serverDB = null;
      }
      helper = new AmbariManagementHelper(stackDAO, extensionDAO, linkDAO);
    }
  
    @Override
    public String getAmbariServerURI(String path) {
      if(masterProtocol==null || masterHostname==null || masterPort==null) {
        return null;
      }
  
      URIBuilder uriBuilder = new URIBuilder();
      uriBuilder.setScheme(masterProtocol);
      uriBuilder.setHost(masterHostname);
      uriBuilder.setPort(masterPort);
  
      String[] parts = path.split("\\?");
  
      if (parts.length > 1) {
        uriBuilder.setPath(parts[0]);
        uriBuilder.setQuery(parts[1]);
      } else {
        uriBuilder.setPath(path);
      }
  
      return uriBuilder.toString();
    }
  
    @Override
    public RoleCommandOrder getRoleCommandOrder(Cluster cluster) {
        return roleCommandOrderProvider.getRoleCommandOrder(cluster);
    }
  
    @Override
    public void createCluster(ClusterRequest request)
        throws AmbariException {
      if (request.getClusterName() == null
          || request.getClusterName().isEmpty()
          || request.getClusterId() != null) {
        throw new IllegalArgumentException(
            "Cluster name should be provided and clusterId should be null");
      }
  
      if (LOG.isDebugEnabled()) {
        LOG.debug("Received a createCluster request, clusterName={}, request={}",
            request.getClusterName(), request);
      }
  
      if (request.getStackVersion() == null
          || request.getStackVersion().isEmpty()) {
        throw new IllegalArgumentException(
            "Stack information should be provided when creating a cluster");
      }
  
      StackId stackId = new StackId(request.getStackVersion());
      StackInfo stackInfo = ambariMetaInfo.getStack(stackId.getStackName(),
        stackId.getStackVersion());
  
      if (stackInfo == null) {
        throw new StackAccessException("stackName=" + stackId.getStackName() + ", stackVersion=" + stackId.getStackVersion());
      }
  
      // FIXME add support for desired configs at cluster level
  
      boolean foundInvalidHosts = false;
      StringBuilder invalidHostsStr = new StringBuilder();
      if (request.getHostNames() != null) {
        for (String hostname : request.getHostNames()) {
          try {
            clusters.getHost(hostname);
          } catch (HostNotFoundException e) {
            if (foundInvalidHosts) {
              invalidHostsStr.append(",");
            }
            foundInvalidHosts = true;
            invalidHostsStr.append(hostname);
          }
        }
      }
  
      if (foundInvalidHosts) {
        throw new HostNotFoundException(invalidHostsStr.toString());
      }
  
      clusters.addCluster(request.getClusterName(), stackId, request.getSecurityType());
      Cluster c = clusters.getCluster(request.getClusterName());
  
      if (request.getHostNames() != null) {
        clusters.mapAndPublishHostsToCluster(request.getHostNames(),
            request.getClusterName());
      }
      // Create cluster widgets and layouts
      initializeWidgetsAndLayouts(c, null);
    }
  
    @Override
    public synchronized void createHostComponents(Set<ServiceComponentHostRequest> requests)
        throws AmbariException, AuthorizationException {
  
      createHostComponents(requests, false);
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public MpackResponse registerMpack(MpackRequest request)
      throws IOException, AuthorizationException, ResourceAlreadyExistsException{
      MpackResponse mpackResponse = ambariMetaInfo.registerMpack(request);
      updateStacks();
      return mpackResponse;
    }
  
    @Override
    public Set<MpackResponse> getMpacks(){
      Collection<Mpack> mpacks = ambariMetaInfo.getMpacks();
      Set<MpackResponse> responseSet = new HashSet<>();
      for (Mpack mpack : mpacks){
        responseSet.add(new MpackResponse(mpack));
      }
      return responseSet;
    }
  
    @Override
    public MpackResponse getMpack(Long mpackId) {
      Mpack mpack = ambariMetaInfo.getMpack(mpackId);
      if (mpack != null) {
        return new MpackResponse(mpack);
      }else{
        return null;
      }
    }
  
    @Override
    public List<Module> getModules(Long mpackId) {
      return ambariMetaInfo.getModules(mpackId);
    }
  
  
    @Override
    public synchronized void createHostComponents(Set<ServiceComponentHostRequest> requests, boolean isBlueprintProvisioned)
       throws AmbariException, AuthorizationException {
  
      if (requests.isEmpty()) {
        LOG.warn("Received an empty requests set");
        return;
      }
  
      // do all validation checks
      Map<String, Map<String, Map<String, Set<String>>>> hostComponentNames =
          new HashMap<>();
      Set<String> duplicates = new HashSet<>();
      for (ServiceComponentHostRequest request : requests) {
        validateServiceComponentHostRequest(request);
  
        Cluster cluster;
        try {
          cluster = clusters.getCluster(request.getClusterName());
        } catch (ClusterNotFoundException e) {
          throw new ParentObjectNotFoundException(
              "Attempted to add a host_component to a cluster which doesn't exist: ", e);
        }
  
        if(!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(),
            EnumSet.of(RoleAuthorization.SERVICE_ADD_DELETE_SERVICES,RoleAuthorization.HOST_ADD_DELETE_COMPONENTS))) {
          throw new AuthorizationException("The authenticated user is not authorized to install service components on to hosts");
        }
  
        if (StringUtils.isEmpty(request.getServiceName())) {
          request.setServiceName(findServiceName(cluster, request.getComponentName()));
        }
  
        if (LOG.isDebugEnabled()) {
          LOG.debug("Received a createHostComponent request, clusterName={}, serviceName={}, componentName={}, hostname={}, request={}",
            request.getClusterName(), request.getServiceName(), request.getComponentName(), request.getHostname(), request);
        }
  
        if (!hostComponentNames.containsKey(request.getClusterName())) {
          hostComponentNames.put(request.getClusterName(), new HashMap<>());
        }
        if (!hostComponentNames.get(request.getClusterName())
            .containsKey(request.getServiceName())) {
          hostComponentNames.get(request.getClusterName()).put(
              request.getServiceName(), new HashMap<>());
        }
        if (!hostComponentNames.get(request.getClusterName())
            .get(request.getServiceName())
            .containsKey(request.getComponentName())) {
          hostComponentNames.get(request.getClusterName())
              .get(request.getServiceName()).put(request.getComponentName(), new HashSet<>());
        }
        if (hostComponentNames.get(request.getClusterName())
            .get(request.getServiceName())
            .get(request.getComponentName())
            .contains(request.getHostname())) {
          duplicates.add("[clusterName=" + request.getClusterName() + ", hostName=" + request.getHostname() +
              ", componentName=" +request.getComponentName() +']');
          continue;
        }
        hostComponentNames.get(request.getClusterName())
            .get(request.getServiceName()).get(request.getComponentName())
            .add(request.getHostname());
  
        if (request.getDesiredState() != null
            && !request.getDesiredState().isEmpty()) {
          State state = State.valueOf(request.getDesiredState());
          if (!state.isValidDesiredState()
              || state != State.INIT) {
            throw new IllegalArgumentException("Invalid desired state"
                + " only INIT state allowed during creation"
                + ", providedDesiredState=" + request.getDesiredState());
          }
        }
  
        Service s;
        try {
          s = cluster.getService(request.getServiceName());
        } catch (ServiceNotFoundException e) {
          throw new IllegalArgumentException(
              "The service[" + request.getServiceName() + "] associated with the component[" +
              request.getComponentName() + "] doesn't exist for the cluster[" + request.getClusterName() + "]");
        }
        ServiceComponent sc = s.getServiceComponent(
            request.getComponentName());
  
        setRestartRequiredServices(s, request.getComponentName());
  
        Host host;
        try {
          host = clusters.getHost(request.getHostname());
        } catch (HostNotFoundException e) {
          throw new ParentObjectNotFoundException(
              "Attempted to add a host_component to a host that doesn't exist: ", e);
        }
        Set<Cluster> mappedClusters =
            clusters.getClustersForHost(request.getHostname());
        boolean validCluster = false;
        if (LOG.isDebugEnabled()) {
          LOG.debug("Looking to match host to cluster, hostnameViaReg={}, hostname={}, clusterName={}, hostClusterMapCount={}",
            host.getHostName(), request.getHostname(), request.getClusterName(), mappedClusters.size());
        }
        for (Cluster mappedCluster : mappedClusters) {
          if (LOG.isDebugEnabled()) {
            LOG.debug("Host belongs to cluster, hostname={}, clusterName={}", request.getHostname(), mappedCluster.getClusterName());
          }
          if (mappedCluster.getClusterName().equals(
              request.getClusterName())) {
            validCluster = true;
            break;
          }
        }
        if (!validCluster) {
          throw new ParentObjectNotFoundException("Attempted to add a host_component to a host that doesn't exist: " +
              "clusterName=" + request.getClusterName() + ", hostName=" + request.getHostname());
        }
        try {
          ServiceComponentHost sch = sc.getServiceComponentHost(
              request.getHostname());
          if (sch != null) {
            duplicates.add("[clusterName=" + request.getClusterName() + ", hostName=" + request.getHostname() +
                ", componentName=" +request.getComponentName() +']');
          }
        } catch (AmbariException e) {
          // Expected
        }
      }
  
      // ensure only a single cluster update
      if (hostComponentNames.size() != 1) {
        throw new IllegalArgumentException("Invalid arguments - updates allowed"
            + " on only one cluster at a time");
      }
  
      if (!duplicates.isEmpty()) {
        final String names = String.join(",", duplicates);
        String msg;
        if (duplicates.size() == 1) {
          msg = "Attempted to create a host_component which already exists: ";
        } else {
          msg = "Attempted to create host_component's which already exist: ";
        }
        throw new DuplicateResourceException(msg + names);
      }
  
      if (!isBlueprintProvisioned) {
        validateExclusiveDependencies(hostComponentNames);
      }
  
      // set restartRequired flag for  monitoring services
      setMonitoringServicesRestartRequired(requests);
      // now doing actual work
      persistServiceComponentHosts(requests, isBlueprintProvisioned);
      m_topologyHolder.get().updateData(getAddedComponentsTopologyEvent(requests));
    }
  
    /**
     * For all components that will be added validate the exclusive components dependencies using the services metainfo
     * and respecting already installed components
     *
     * @throws AmbariException is thrown if the exclusive dependency is violated or if the data is invalid
     */
    private void validateExclusiveDependencies(Map<String, Map<String, Map<String, Set<String>>>> hostComponentNames) throws AmbariException {
      List<String> validationIssues = new ArrayList<>();
  
      for (Entry<String, Map<String, Map<String, Set<String>>>> clusterEntry : hostComponentNames.entrySet()) {
        for (Entry<String, Map<String, Set<String>>> serviceEntry : clusterEntry.getValue().entrySet()) {
          for (Entry<String, Set<String>> componentEntry : serviceEntry.getValue().entrySet()) {
            Set<String> hostnames = componentEntry.getValue();
            if (hostnames != null && !hostnames.isEmpty()) {
              //get dependency info
              ServiceComponent sc = clusters.getCluster(clusterEntry.getKey()).getService(serviceEntry.getKey()).getServiceComponent(componentEntry.getKey());
              StackId stackId = sc.getDesiredStackId();
              List<DependencyInfo> dependencyInfos = ambariMetaInfo.getComponentDependencies(stackId.getStackName(),
                stackId.getStackVersion(), serviceEntry.getKey(), componentEntry.getKey());
  
              for (DependencyInfo dependencyInfo : dependencyInfos) {
                if ("host".equals(dependencyInfo.getScope()) && "exclusive".equals(dependencyInfo.getType())) {
                  Service depService;
                  try {
                    depService = clusters.getCluster(clusterEntry.getKey()).getService(dependencyInfo.getServiceName());
                  } catch (ServiceNotFoundException e) {
                    LOG.debug("Skipping dependency " + dependencyInfo + " for " + serviceEntry.getKey() + " since the dependent service is not installed ");
                    continue;
                  }
                  if (depService != null && depService.getServiceComponents().containsKey(dependencyInfo.getComponentName())) {
                    ServiceComponent dependentSC = depService.getServiceComponent(dependencyInfo.getComponentName());
                    if (dependentSC != null) {
                      //get cluster dependent component hosts
                      Set<String> dependentComponentHosts = new HashSet<>(dependentSC.getServiceComponentHosts().keySet());
                      //get request dependent component hosts
                      if (clusterEntry.getValue().containsKey(dependentSC.getServiceName()) &&
                          clusterEntry.getValue().get(dependentSC.getServiceName()).containsKey(dependentSC.getName())) {
                        dependentComponentHosts.addAll(clusterEntry.getValue().
                            get(dependentSC.getServiceName()).get(dependentSC.getName()));
                      }
                      //get the intersection
                      dependentComponentHosts.retainAll(hostnames);
                      if (!dependentComponentHosts.isEmpty()) {
                        validationIssues.add("Component " + componentEntry.getKey() + " can't be co-hosted with component "
                          + dependencyInfo.getComponentName() + " on hosts " + dependentComponentHosts + " due to exclusive dependency");
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
  
      if (!validationIssues.isEmpty()) {
        throw new AmbariException("The components exclusive dependencies are not respected: " + validationIssues);
      }
    }
  
    void persistServiceComponentHosts(Set<ServiceComponentHostRequest> requests, boolean isBlueprintProvisioned)
      throws AmbariException {
      Multimap<Cluster, ServiceComponentHost> schMap = ArrayListMultimap.create();
      Map<Long, Map<String, List<String>>> serviceComponentNames = new HashMap<>();
      Map<Long, Map<String, Map<String, ServiceComponentDesiredStateEntity>>> serviceComponentDesiredStateEntities = new HashMap<>();
  
      for (ServiceComponentHostRequest request : requests) {
        Cluster cluster = clusters.getCluster(request.getClusterName());
        Service s = cluster.getService(request.getServiceName());
        ServiceComponent sc = s.getServiceComponent(
            request.getComponentName());
        serviceComponentNames.computeIfAbsent(sc.getClusterId(), c -> new HashMap<>())
            .computeIfAbsent(sc.getServiceName(), h ->new ArrayList<>()).add(sc.getName());
      }
  
      List<ServiceComponentDesiredStateEntity> entities = serviceComponentDesiredStateDAO.findByNames(serviceComponentNames);
  
      for (ServiceComponentDesiredStateEntity stateEntity : entities) {
        serviceComponentDesiredStateEntities.computeIfAbsent(stateEntity.getClusterId(), c -> new HashMap<>())
            .computeIfAbsent(stateEntity.getServiceName(), h ->new HashMap<>())
            .putIfAbsent(stateEntity.getComponentName(), stateEntity);
      }
  
      for (ServiceComponentHostRequest request : requests) {
        Cluster cluster = clusters.getCluster(request.getClusterName());
        Service s = cluster.getService(request.getServiceName());
        ServiceComponent sc = s.getServiceComponent(
            request.getComponentName());
  
        ServiceComponentHost sch =
            serviceComponentHostFactory.createNew(sc, request.getHostname(),
                serviceComponentDesiredStateEntities.get(cluster.getClusterId()).get(s.getName()).get(sc.getName()));
        if (request.getDesiredState() != null
            && !request.getDesiredState().isEmpty()) {
          State state = State.valueOf(request.getDesiredState());
          sch.setDesiredState(state);
        }
        if (isBlueprintProvisioned && !sch.isClientComponent()) {
          HostComponentDesiredStateEntity desiredStateEntity = sch.getDesiredStateEntity();
          desiredStateEntity.setBlueprintProvisioningState(BlueprintProvisioningState.IN_PROGRESS);
          hostComponentDesiredStateDAO.merge(desiredStateEntity);
        }
  
        schMap.put(cluster, sch);
      }
  
      for (Cluster cluster : schMap.keySet()) {
        cluster.addServiceComponentHosts(schMap.get(cluster));
      }
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public TopologyUpdateEvent getAddedComponentsTopologyEvent(Set<ServiceComponentHostRequest> requests)
      throws AmbariException {
      TreeMap<String, TopologyCluster> topologyUpdates = new TreeMap<>();
      Set<String> hostsToUpdate = new HashSet<>();
      for (ServiceComponentHostRequest request : requests) {
        String serviceName = request.getServiceName();
        String componentName = request.getComponentName();
        Cluster cluster = clusters.getCluster(request.getClusterName());
        Collection<Host> clusterHosts = cluster.getHosts();
        Service s = cluster.getService(serviceName);
        ServiceComponent sc = s.getServiceComponent(componentName);
        String hostName = request.getHostname();
        hostsToUpdate.add(hostName);
        Set<Long> hostIds = clusterHosts.stream()
            .filter(h -> hostName.equals(h.getHostName()))
            .map(h -> h.getHostId()).collect(Collectors.toSet());
        Set<String> publicHostNames = clusterHosts.stream()
            .filter(h -> hostName.equals(h.getHostName()))
            .map(h -> h.getPublicHostName()).collect(Collectors.toSet());
        Set<String> hostNames = new HashSet<>();
        hostNames.add(hostName);
        ServiceComponentHost sch = sc.getServiceComponentHost(request.getHostname());
  
        TopologyComponent newComponent = TopologyComponent.newBuilder()
            .setComponentName(sch.getServiceComponentName())
            .setServiceName(sch.getServiceName())
            .setDisplayName(sc.getDisplayName())
            .setHostIdentifiers(hostIds, hostNames)
            .setPublicHostNames(publicHostNames)
            .setComponentLevelParams(getTopologyComponentLevelParams(cluster.getClusterId(), serviceName, componentName,
                cluster.getSecurityType()))
            .setCommandParams(getTopologyCommandParams(cluster.getClusterId(), serviceName, componentName, sch))
            .build();
  
        String clusterId = Long.toString(cluster.getClusterId());
        if (!topologyUpdates.containsKey(clusterId)) {
          topologyUpdates.put(clusterId, new TopologyCluster());
        }
        if (topologyUpdates.get(clusterId).getTopologyComponents().contains(newComponent)) {
          Set<TopologyComponent> newComponents = new HashSet<>();
          newComponents.add(newComponent);
          topologyUpdates.get(clusterId).update(newComponents, Collections.emptySet(),
              UpdateEventType.UPDATE, new TopologyUpdateHandlingReport());
        } else {
          topologyUpdates.get(clusterId).addTopologyComponent(newComponent);
        }
      }
      for (String hostName : hostsToUpdate) {
        Host host = clusters.getHost(hostName);
        m_hostLevelParamsHolder.get().updateData(m_hostLevelParamsHolder.get().getCurrentData(host.getHostId()));
      }
      return new TopologyUpdateEvent(topologyUpdates, UpdateEventType.UPDATE);
    }
  
    private void setMonitoringServicesRestartRequired(
      Set<ServiceComponentHostRequest> requests) throws AmbariException {
  
      for (ServiceComponentHostRequest request : requests) {
        Cluster cluster = clusters.getCluster(request.getClusterName());
  
        for (Service service : cluster.getServices().values()) {
          ServiceInfo serviceInfo = ambariMetaInfo.getService(service);
  
          if (!BooleanUtils.toBoolean(serviceInfo.isMonitoringService())) {
            continue;
          }
  
          for (ServiceComponent sc : service.getServiceComponents().values()) {
            if (sc.isMasterComponent()) {
              for (ServiceComponentHost sch : sc.getServiceComponentHosts().values()) {
                sch.setRestartRequired(true);
              }
              continue;
            }
  
            String hostname = request.getHostname();
            if (sc.getServiceComponentHosts().containsKey(hostname)) {
              ServiceComponentHost sch = sc.getServiceComponentHost(hostname);
              sch.setRestartRequired(true);
            }
          }
        }
      }
    }
  
    private void setRestartRequiredServices(
            Service service, String componentName) throws AmbariException {
  
      StackId stackId = service.getDesiredStackId();
      if (service.getServiceComponent(componentName).isClientComponent()) {
        return;
      }
      Set<String> needRestartServices = ambariMetaInfo.getRestartRequiredServicesNames(
        stackId.getStackName(), stackId.getStackVersion());
  
      if(needRestartServices.contains(service.getName())) {
        Map<String, ServiceComponent> m = service.getServiceComponents();
        for (Entry<String, ServiceComponent> entry : m.entrySet()) {
          ServiceComponent serviceComponent = entry.getValue();
          Map<String, ServiceComponentHost> schMap = serviceComponent.getServiceComponentHosts();
  
            for (Entry<String, ServiceComponentHost> sch : schMap.entrySet()) {
              ServiceComponentHost serviceComponentHost = sch.getValue();
              serviceComponentHost.setRestartRequired(true);
            }
  
        }
      }
    }
  
    @Override
    public void registerRackChange(String clusterName) throws AmbariException {
      Cluster cluster = clusters.getCluster(clusterName);
  
      for (Service service : cluster.getServices().values()) {
        ServiceInfo serviceInfo = ambariMetaInfo.getService(service);
  
        if (!BooleanUtils.toBoolean(serviceInfo.isRestartRequiredAfterRackChange())) {
          continue;
        }
  
        Map<String, ServiceComponent> serviceComponents = service.getServiceComponents();
  
        for (ServiceComponent serviceComponent : serviceComponents.values()) {
          Map<String, ServiceComponentHost> schMap = serviceComponent.getServiceComponentHosts();
          for (Entry<String, ServiceComponentHost> sch : schMap.entrySet()) {
            ServiceComponentHost serviceComponentHost = sch.getValue();
            serviceComponentHost.setRestartRequired(true);
          }
        }
      }
    }
  
    /**
     * Creates a configuration.
     * <p>
     * This implementation ensures the authenticated user is authorized to create the new configuration
     * based on the details of what properties are being changed and the authorizations the authenticated
     * user has been granted.
     * <p>
     * Example
     * <ul>
     * <li>
     * If the user is attempting to change a service-level configuration that user must be granted the
     * <code>SERVICE_MODIFY_CONFIGS</code> privilege (authorization)
     * </li>
     * <li>
     * If the user is attempting to change the cluster-wide value to enable or disable auto-start
     * (<code>cluster-env/recovery_enabled</code>), that user must be granted the
     * <code>CLUSTER_MANAGE_AUTO_START</code> privilege (authorization)
     * </li>
     * </ul>
     *
     * @param request the request object which defines the configuration.
     * @throws AmbariException when the configuration cannot be created.
     */
    @Override
    public synchronized ConfigurationResponse createConfiguration(
        ConfigurationRequest request, boolean refreshCluster) throws AmbariException, AuthorizationException {
      if (null == request.getClusterName() || request.getClusterName().isEmpty()
          || null == request.getType() || request.getType().isEmpty()
          || null == request.getProperties()) {
        throw new IllegalArgumentException("Invalid Arguments,"
            + " clustername, config type and configs should not"
            + " be null or empty");
      }
  
      Cluster cluster = clusters.getCluster(request.getClusterName());
  
      String configType = request.getType();
  
      // If the config type is for a service, then allow a user with SERVICE_MODIFY_CONFIGS to
      // update, else ensure the user has CLUSTER_MODIFY_CONFIGS
      String service = cluster.getServiceByConfigType(configType);
  
  
      // Get the changes so that the user's intention can be determined. For example, maybe
      // the user wants to change the run-as user for a service or maybe the the cluster-wide
      // recovery mode setting.
      Map<String, String[]> propertyChanges = getPropertyChanges(cluster, request);
  
      if(StringUtils.isEmpty(service)) {
        // If the configuration is not attached to a specific service, it is a cluster-wide configuration
        // type. For example, cluster-env.
  
        // If the user is trying to set the cluster-wide recovery mode, ensure that user
        // has the appropriate authorization
        validateAuthorizationToManageServiceAutoStartConfiguration(cluster, configType, propertyChanges);
  
        // If the user is trying to set any other cluster-wide property, ensure that user
        // has the appropriate authorization
        validateAuthorizationToModifyConfigurations(cluster, configType, propertyChanges,
            Collections.singletonMap("cluster-env", Collections.singleton("recovery_enabled")),
            false);
      }
      else {
        // If the user is trying to set any service-level property, ensure that user
        // has the appropriate authorization
        validateAuthorizationToModifyConfigurations(cluster, configType, propertyChanges, null, true);
  
        // Ensure the user is allowed to update service users and groups.
        validateAuthorizationToUpdateServiceUsersAndGroups(cluster, configType, propertyChanges);
      }
  
      Map<String, String> requestProperties = request.getProperties();
  
      // Configuration attributes are optional. If not present, use default(provided by stack), otherwise merge default
      // with request-provided
      Map<String, Map<String, String>> requestPropertiesAttributes = request.getPropertiesAttributes();
  
      if (requestPropertiesAttributes != null && requestPropertiesAttributes.containsKey(PASSWORD)) {
        for (Map.Entry<String, String> requestEntry : requestPropertiesAttributes.get(PASSWORD).entrySet()) {
          String passwordProperty = requestEntry.getKey();
          if(requestProperties.containsKey(passwordProperty) && requestEntry.getValue().equals("true")) {
            String passwordPropertyValue = requestProperties.get(passwordProperty);
            if (!SecretReference.isSecret(passwordPropertyValue)) {
              continue;
            }
            SecretReference ref = new SecretReference(passwordPropertyValue, cluster);
            String refValue = ref.getValue();
            requestProperties.put(passwordProperty, refValue);
          }
        }
      }
  
      Map<PropertyInfo.PropertyType, Set<String>> propertiesTypes = cluster.getConfigPropertiesTypes(request.getType());
      if(propertiesTypes.containsKey(PropertyType.PASSWORD)) {
        for(String passwordProperty : propertiesTypes.get(PropertyType.PASSWORD)) {
          if(requestProperties.containsKey(passwordProperty)) {
            String passwordPropertyValue = requestProperties.get(passwordProperty);
            if (!SecretReference.isSecret(passwordPropertyValue)) {
              continue;
            }
            SecretReference ref = new SecretReference(passwordPropertyValue, cluster);
            String refValue = ref.getValue();
            requestProperties.put(passwordProperty, refValue);
          }
        }
      }
  
  
  
      Map<String, Config> configs = cluster.getConfigsByType(
          request.getType());
      if (null == configs) {
        configs = new HashMap<>();
      }
  
      Map<String, Map<String, String>> propertiesAttributes = new HashMap<>();
  
      Set<StackId> visitedStacks = new HashSet<>();
  
      for (Service clusterService : cluster.getServices().values()) {
        StackId stackId = clusterService.getDesiredStackId();
        StackInfo stackInfo = ambariMetaInfo.getStack(clusterService.getDesiredStackId());
  
        if (visitedStacks.contains(stackId)) {
          continue;
        }
  
        Map<String, Map<String, String>> defaultConfigAttributes = stackInfo.getDefaultConfigAttributesForConfigType(configType);
        if (null != defaultConfigAttributes) {
          ConfigHelper.mergeConfigAttributes(propertiesAttributes, defaultConfigAttributes);
        }
  
        visitedStacks.add(stackId);
      }
  
      // overwrite default attributes with request attributes
      if(requestPropertiesAttributes != null){
        ConfigHelper.mergeConfigAttributes(propertiesAttributes, requestPropertiesAttributes);
      }
  
      if (configs.containsKey(request.getVersionTag())) {
        throw new AmbariException(MessageFormat.format("Configuration with tag ''{0}'' exists for ''{1}''",
            request.getVersionTag(),
            request.getType()));
      }
  
      StackId stackId = null;
      if (null != service) {
        try {
          Service svc = cluster.getService(service);
          stackId = svc.getDesiredStackId();
        } catch (AmbariException ambariException) {
          LOG.warn("Adding configurations for {} even though its parent service {} is not installed",
              configType, service);
        }
      }
  
      if (null == stackId) {
        stackId = cluster.getDesiredStackVersion();
      }
  
      Config config = createConfig(cluster, stackId, request.getType(), requestProperties,
        request.getVersionTag(), propertiesAttributes, refreshCluster);
  
      LOG.info(MessageFormat.format("Creating configuration with tag ''{0}'' to cluster ''{1}''  for configuration type {2}",
          request.getVersionTag(),
          request.getClusterName(),
          configType));
  
      return new ConfigurationResponse(cluster.getClusterName(), config);
    }
  
    @Override
    public synchronized ConfigurationResponse createConfiguration(
        ConfigurationRequest request) throws AmbariException, AuthorizationException {
      return createConfiguration(request, true);
    }
  
    @Override
    public Config createConfig(Cluster cluster, StackId stackId, String type, Map<String, String> properties,
                               String versionTag, Map<String, Map<String, String>> propertiesAttributes, boolean refreshCluster) {
  
      Config config = configFactory.createNew(stackId, type, cluster, versionTag, properties,
          propertiesAttributes, refreshCluster);
  
      cluster.addConfig(config);
      return config;
    }
  
    @Override
    public Config createConfig(Cluster cluster, StackId stackId, String type, Map<String, String> properties,
                               String versionTag, Map<String, Map<String, String>> propertiesAttributes) {
  
      return createConfig(cluster, stackId, type, properties, versionTag, propertiesAttributes, true);
    }
  
    @Override
    public void createGroups(Set<GroupRequest> requests) throws AmbariException {
      for (GroupRequest request : requests) {
        if (StringUtils.isBlank(request.getGroupName())) {
          throw new AmbariException("Group name must be supplied.");
        }
        final Group group = users.getGroup(request.getGroupName());
        if (group != null) {
          throw new AmbariException("Group already exists.");
        }
        users.createGroup(request.getGroupName(), GroupType.LOCAL);
      }
    }
  
    @Override
    public void createMembers(Set<MemberRequest> requests) throws AmbariException {
      for (MemberRequest request : requests) {
        if (StringUtils.isBlank(request.getGroupName()) || StringUtils.isBlank(request.getUserName())) {
          throw new AmbariException("Both group name and user name must be supplied.");
        }
        users.addMemberToGroup(request.getGroupName(), request.getUserName());
      }
    }
  
    @Override
    public Set<MemberResponse> getMembers(Set<MemberRequest> requests)
        throws AmbariException {
      final Set<MemberResponse> responses = new HashSet<>();
      for (MemberRequest request: requests) {
        LOG.debug("Received a getMembers request, {}", request);
        final Group group = users.getGroup(request.getGroupName());
        if (null == group) {
          if (requests.size() == 1) {
            // only throw exception if there is a single request
            // if there are multiple requests, this indicates an OR predicate
            throw new ObjectNotFoundException("Cannot find group '"
                + request.getGroupName() + "'");
          }
        } else {
          for (User user: users.getGroupMembers(group.getGroupName())) {
            final MemberResponse response = new MemberResponse(group.getGroupName(), user.getUserName());
            responses.add(response);
          }
        }
      }
      return responses;
    }
  
    @Override
    @SuppressWarnings("unchecked")
    public synchronized void updateMembers(Set<MemberRequest> requests) throws AmbariException {
      // validate
      String groupName = null;
      for (MemberRequest request: requests) {
        if (groupName != null && !request.getGroupName().equals(groupName)) {
          throw new AmbariException("Can't manage members of different groups in one request");
        }
        groupName = request.getGroupName();
      }
      final List<String> requiredMembers = new ArrayList<>();
      for (MemberRequest request: requests) {
        if (request.getUserName() != null) {
          requiredMembers.add(request.getUserName());
        }
      }
      final List<String> currentMembers = users.getAllMembers(groupName);
      for (String user: (Collection<String>) CollectionUtils.subtract(currentMembers, requiredMembers)) {
        users.removeMemberFromGroup(groupName, user);
      }
      for (String user: (Collection<String>) CollectionUtils.subtract(requiredMembers, currentMembers)) {
        users.addMemberToGroup(groupName, user);
      }
    }
  
    private Stage createNewStage(long id, Cluster cluster, long requestId,
                                 String requestContext,
                                 String commandParamsStage, String hostParamsStage) {
      String logDir = BASE_LOG_DIR + File.pathSeparator + requestId;
      Stage stage =
          stageFactory.createNew(requestId, logDir,
            null == cluster ? null : cluster.getClusterName(),
            null == cluster ? -1L : cluster.getClusterId(),
            requestContext, commandParamsStage,
            hostParamsStage);
      stage.setStageId(id);
      return stage;
    }
  
    private Set<ClusterResponse> getClusters(ClusterRequest request)
        throws AmbariException, AuthorizationException {
  
      Set<ClusterResponse> response = new HashSet<>();
  
      if (LOG.isDebugEnabled()) {
        LOG.debug("Received a getClusters request, clusterName={}, clusterId={}, stackInfo={}",
          request.getClusterName(), request.getClusterId(), request.getStackVersion());
      }
  
      Cluster singleCluster = null;
      try {
        if (request.getClusterName() != null) {
          singleCluster = clusters.getCluster(request.getClusterName());
        } else if (request.getClusterId() != null) {
          singleCluster = clusters.getClusterById(request.getClusterId());
        }
      }
      catch(ClusterNotFoundException e) {
        // the user shouldn't know the difference between a cluster that does not exist or one that
        // he doesn't have access to.
        if (AuthorizationHelper.isAuthorized(ResourceType.AMBARI, null, RoleAuthorization.AMBARI_ADD_DELETE_CLUSTERS)) {
          throw e;
        } else {
          throw new AuthorizationException();
        }
      }
  
      if (singleCluster != null) {
        ClusterResponse cr = singleCluster.convertToResponse();
        cr.setDesiredConfigs(singleCluster.getDesiredConfigs());
        cr.setDesiredServiceConfigVersions(singleCluster.getActiveServiceConfigVersions());
        cr.setCredentialStoreServiceProperties(getCredentialStoreServiceProperties());
  
       // If the user is authorized to view information about this cluster, add it to the response
  // TODO: Uncomment this when the UI doesn't require view access for View-only users.
  //      if (AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cr.getResourceId(),
  //          RoleAuthorization.AUTHORIZATIONS_VIEW_CLUSTER)) {
        response.add(cr);
  //      }
  //      else {
  //        // the user shouldn't know the difference between a cluster that does not exist or one that
  //        // he doesn't have access to.
  //        throw new AuthorizationException();
  //      }
  
        return response;
      }
  
  
      Map<String, Cluster> allClusters = clusters.getClusters();
      for (Cluster c : allClusters.values()) {
  
  // TODO: Uncomment this when the UI doesn't require view access for View-only users.
  //       If the user is authorized to view information about this cluster, add it to the response
  //       if (AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, c.getResourceId(),
  //        RoleAuthorization.AUTHORIZATIONS_VIEW_CLUSTER)) {
        ClusterResponse cr = c.convertToResponse();
        cr.setDesiredConfigs(c.getDesiredConfigs());
        cr.setDesiredServiceConfigVersions(c.getActiveServiceConfigVersions());
        cr.setCredentialStoreServiceProperties(getCredentialStoreServiceProperties());
        response.add(cr);
  //       }
      }
      StringBuilder builder = new StringBuilder();
      if (LOG.isDebugEnabled()) {
        clusters.debugDump(builder);
        LOG.debug("Cluster State for cluster {}", builder);
      }
      return response;
    }
  
    private Set<ServiceComponentHostResponse> getHostComponents(
        ServiceComponentHostRequest request) throws AmbariException {
      return getHostComponents(request, false);
    }
  
    private Set<ServiceComponentHostResponse> getHostComponents(
        ServiceComponentHostRequest request, boolean statusOnly) throws AmbariException {
      LOG.debug("Processing request {}", request);
  
      if (request.getClusterName() == null
          || request.getClusterName().isEmpty()) {
        IllegalArgumentException e = new IllegalArgumentException("Invalid arguments, cluster name should not be null");
        LOG.debug("Cluster not specified in request", e);
        throw e;
      }
  
      final Cluster cluster;
      try {
        cluster = clusters.getCluster(request.getClusterName());
      } catch (ClusterNotFoundException e) {
        LOG.error("Cluster not found ", e);
        throw new ParentObjectNotFoundException("Parent Cluster resource doesn't exist", e);
      }
  
      if (request.getHostname() != null) {
        try {
          if (!clusters.getClustersForHost(request.getHostname()).contains(cluster)) {
            // case where host exists but not associated with given cluster
            LOG.error("Host doesn't belong to cluster - " + request.getHostname());
            throw new ParentObjectNotFoundException("Parent Host resource doesn't exist",
                new HostNotFoundException(request.getClusterName(), request.getHostname()));
          }
        } catch (HostNotFoundException e) {
          LOG.error("Host not found", e);
          // creating new HostNotFoundException to add cluster name
          throw new ParentObjectNotFoundException("Parent Host resource doesn't exist",
              new HostNotFoundException(request.getClusterName(), request.getHostname()));
        }
      }
  
      if (request.getComponentName() != null) {
        if (StringUtils.isBlank(request.getServiceName())) {
  
          // !!! FIXME the assumption that a component is unique across all stacks is a ticking
          // time bomb.  Blueprints are making this assumption.
          String serviceName = findServiceName(cluster, request.getComponentName());
  
          if (StringUtils.isBlank(serviceName)) {
            LOG.error("Unable to find service for component {}", request.getComponentName());
            throw new ServiceComponentHostNotFoundException(
                cluster.getClusterName(), null, request.getComponentName(), request.getHostname());
          }
          request.setServiceName(serviceName);
        }
      }
  
      Set<Service> services = new HashSet<>();
      if (request.getServiceName() != null && !request.getServiceName().isEmpty()) {
        services.add(cluster.getService(request.getServiceName()));
      } else {
        services.addAll(cluster.getServices().values());
      }
  
      Set<ServiceComponentHostResponse> response =
          new HashSet<>();
  
      boolean checkDesiredState = false;
      State desiredStateToCheck = null;
      boolean checkState = false;
      State stateToCheck = null;
      boolean filterBasedConfigStaleness = false;
      boolean staleConfig = true;
      if (request.getStaleConfig() != null) {
        filterBasedConfigStaleness = true;
        staleConfig = "true".equals(request.getStaleConfig().toLowerCase());
      }
      if (request.getDesiredState() != null
          && !request.getDesiredState().isEmpty()) {
        desiredStateToCheck = State.valueOf(request.getDesiredState());
        if (!desiredStateToCheck.isValidDesiredState()) {
          throw new IllegalArgumentException("Invalid arguments, invalid desired"
              + " state, desiredState=" + desiredStateToCheck);
        }
        checkDesiredState = true;
      }
  
      if (!StringUtils.isEmpty(request.getState())) {
        stateToCheck = State.valueOf(request.getState());
        // maybe check should be more wider
        if (stateToCheck == null) {
          throw new IllegalArgumentException("Invalid arguments, invalid state, State=" + request.getState());
        }
        checkState = true;
      }
  
      Map<String, DesiredConfig> desiredConfigs = cluster.getDesiredConfigs();
      Map<String, Host> hosts = clusters.getHostsForCluster(cluster.getClusterName());
  
      for (Service s : services) {
        // filter on component name if provided
        Set<ServiceComponent> components = new HashSet<>();
        if (request.getComponentName() != null) {
          components.add(s.getServiceComponent(request.getComponentName()));
        } else {
          components.addAll(s.getServiceComponents().values());
        }
        for (ServiceComponent sc : components) {
          if (request.getComponentName() != null) {
            if (!sc.getName().equals(request.getComponentName())) {
              continue;
            }
          }
  
          // filter on hostname if provided
          // filter on desired state if provided
  
          Map<String, ServiceComponentHost> serviceComponentHostMap =
            sc.getServiceComponentHosts();
  
          if (request.getHostname() != null) {
            try {
              if (serviceComponentHostMap == null
                  || !serviceComponentHostMap.containsKey(request.getHostname())) {
                throw new ServiceComponentHostNotFoundException(cluster.getClusterName(),
                  s.getName(), sc.getName(), request.getHostname());
              }
  
              ServiceComponentHost sch = serviceComponentHostMap.get(request.getHostname());
  
              if (null == sch) {
                // It's possible that the host was deleted during the time that the request was generated.
                continue;
              }
  
              if (checkDesiredState && (desiredStateToCheck != sch.getDesiredState())) {
                continue;
              }
  
              if (checkState && stateToCheck != sch.getState()) {
                continue;
              }
  
              if (request.getAdminState() != null) {
                String stringToMatch =
                    sch.getComponentAdminState() == null ? "" : sch.getComponentAdminState().name();
                if (!request.getAdminState().equals(stringToMatch)) {
                  continue;
                }
              }
  
              ServiceComponentHostResponse r = statusOnly ? sch.convertToResponseStatusOnly(desiredConfigs,
                  filterBasedConfigStaleness)
                  : sch.convertToResponse(desiredConfigs);
              if (null == r || (filterBasedConfigStaleness && r.isStaleConfig() != staleConfig)) {
                continue;
              }
  
              Host host = hosts.get(sch.getHostName());
              if (host == null) {
                throw new HostNotFoundException(cluster.getClusterName(), sch.getHostName());
              }
  
              MaintenanceState effectiveMaintenanceState = maintenanceStateHelper.getEffectiveState(sch, host);
              if(filterByMaintenanceState(request, effectiveMaintenanceState)) {
                continue;
              }
              r.setMaintenanceState(effectiveMaintenanceState.name());
  
              response.add(r);
            } catch (ServiceComponentHostNotFoundException e) {
              if (request.getServiceName() == null || request.getComponentName() == null) {
                // Ignore the exception if either the service name or component name are not specified.
                // This is an artifact of how we get host_components and can happen in the case where
                // we get all host_components for a host, for example.
                LOG.debug("Ignoring not specified host_component ", e);
  
              } else {
                // Otherwise rethrow the exception and let the caller decide if it's an error condition.
                // Logging the exception as debug since this does not necessarily indicate an error
                // condition.
                LOG.debug("ServiceComponentHost not found ", e);
                throw new ServiceComponentHostNotFoundException(cluster.getClusterName(),
                    request.getServiceName(), request.getComponentName(), request.getHostname());
              }
            }
          } else {
            for (ServiceComponentHost sch : serviceComponentHostMap.values()) {
              if (null == sch) {
                // It's possible that the host was deleted during the time that the request was generated.
                continue;
              }
  
              if (checkDesiredState && (desiredStateToCheck != sch.getDesiredState())) {
                continue;
              }
  
              if (checkState && stateToCheck != sch.getState()) {
                continue;
              }
  
              if (request.getAdminState() != null) {
                String stringToMatch =
                    sch.getComponentAdminState() == null ? "" : sch.getComponentAdminState().name();
                if (!request.getAdminState().equals(stringToMatch)) {
                  continue;
                }
              }
  
              ServiceComponentHostResponse r = statusOnly ? sch.convertToResponseStatusOnly(desiredConfigs,
                  filterBasedConfigStaleness)
                  : sch.convertToResponse(desiredConfigs);
              if (null == r || (filterBasedConfigStaleness && r.isStaleConfig() != staleConfig)) {
                continue;
              }
  
              Host host = hosts.get(sch.getHostName());
              if (host == null) {
                throw new HostNotFoundException(cluster.getClusterName(), sch.getHostName());
              }
  
              MaintenanceState effectiveMaintenanceState = maintenanceStateHelper.getEffectiveState(sch, host);
              if(filterByMaintenanceState(request, effectiveMaintenanceState)) {
                continue;
              }
              r.setMaintenanceState(effectiveMaintenanceState.name());
  
              response.add(r);
            }
          }
        }
      }
      return response;
    }
  
    private boolean filterByMaintenanceState(ServiceComponentHostRequest request, MaintenanceState effectiveMaintenanceState) {
      if (request.getMaintenanceState() != null) {
        MaintenanceState desiredMaintenanceState = MaintenanceState.valueOf(request.getMaintenanceState());
        if (desiredMaintenanceState.equals(MaintenanceState.ON)) {
          /*
           * if we want components with ON state it can be one of IMPLIED_FROM_SERVICE,
           * IMPLIED_FROM_SERVICE_AND_HOST, IMPLIED_FROM_HOST, ON, ro simply - not OFF
           */
          if (effectiveMaintenanceState.equals(MaintenanceState.OFF)) {
            return true;
          }
        } else if (!desiredMaintenanceState.equals(effectiveMaintenanceState)){
          return true;
        }
      }
      return false;
    }
  
    @Override
    public MaintenanceState getEffectiveMaintenanceState(ServiceComponentHost sch)
        throws AmbariException {
  
      return maintenanceStateHelper.getEffectiveState(sch);
    }
  
  
    private Set<ConfigurationResponse> getConfigurations(
        ConfigurationRequest request) throws AmbariException {
      if (request.getClusterName() == null) {
        throw new IllegalArgumentException("Invalid arguments, cluster name"
            + " should not be null");
      }
  
      Cluster cluster = clusters.getCluster(request.getClusterName());
  
      Set<ConfigurationResponse> responses = new HashSet<>();
  
      // !!! if only one, then we need full properties
      if (null != request.getType() && null != request.getVersionTag()) {
        Config config = cluster.getConfig(request.getType(),
            request.getVersionTag());
        if (null != config) {
          ConfigurationResponse response = new ConfigurationResponse(
              cluster.getClusterName(), config);
          responses.add(response);
        }
      }
      else {
        boolean includeProps = request.includeProperties();
        if (null != request.getType()) {
          Map<String, Config> configs = cluster.getConfigsByType(
              request.getType());
  
          if (null != configs) {
            for (Entry<String, Config> entry : configs.entrySet()) {
              Config config = entry.getValue();
              ConfigurationResponse response = new ConfigurationResponse(
                  cluster.getClusterName(), config.getStackId(),
                  request.getType(),
                  config.getTag(), entry.getValue().getVersion(),
                  includeProps ? config.getProperties() : new HashMap<>(),
                  includeProps ? config.getPropertiesAttributes() : new HashMap<>(),
                  config.getPropertiesTypes());
              responses.add(response);
            }
          }
        } else {
          // !!! all configuration
          Collection<Config> all = cluster.getAllConfigs();
  
          for (Config config : all) {
            ConfigurationResponse response = new ConfigurationResponse(
                cluster.getClusterName(), config.getStackId(), config.getType(),
                config.getTag(), config.getVersion(),
                includeProps ? config.getProperties() : new HashMap<>(),
                includeProps ? config.getPropertiesAttributes() : new HashMap<>(),
                config.getPropertiesTypes());
  
            responses.add(response);
          }
        }
      }
  
      return responses;
  
    }
  
    @Override
    @Transactional
    public synchronized RequestStatusResponse updateClusters(Set<ClusterRequest> requests,
                                                             Map<String, String> requestProperties)
        throws AmbariException, AuthorizationException {
      return updateClusters(requests, requestProperties, true, true);
    }
  
    @Override
    @Transactional
    public synchronized RequestStatusResponse updateClusters(Set<ClusterRequest> requests,
                                                             Map<String, String> requestProperties,
                                                             boolean fireAgentUpdates, boolean refreshCluster)
        throws AmbariException, AuthorizationException {
  
      RequestStatusResponse response = null;
  
      // We have to allow for multiple requests to account for multiple
      // configuration updates (create multiple configuration resources)...
      for (ClusterRequest request : requests) {
        response = updateCluster(request, requestProperties, fireAgentUpdates, refreshCluster);
      }
      return response;
    }
  
    /**
     * Get a dictionary of all config differences between existingConfig and newConfigValues where the key is the config name and the action is one of "changed", "added", or "deleted".
     * @param existingConfig
     * @param newConfigValues
     * @return Delta of configs
     */
    private Map<String, String> getConfigKeyDeltaToAction(Config existingConfig, Map<String, String> newConfigValues) {
      Map<String, String> configsChanged = new HashMap<>();
  
      if (null != existingConfig) {
        Map<String, String> existingConfigValues = existingConfig.getProperties();
  
        Iterator it = existingConfigValues.entrySet().iterator();
        while (it.hasNext()) {
          Map.Entry<String, String> pair = (Map.Entry) it.next();
          // Check the value if both keys exist
          if (newConfigValues.containsKey(pair.getKey())) {
            if (!newConfigValues.get(pair.getKey()).equals(pair.getValue())) {
              configsChanged.put(pair.getKey(), "changed");
            }
          } else {
            // Deleted
            configsChanged.put(pair.getKey(), "deleted");
          }
        }
  
        it = newConfigValues.entrySet().iterator();
        while (it.hasNext()) {
          Map.Entry<String, String> pair = (Map.Entry) it.next();
          if (!existingConfigValues.keySet().contains(pair.getKey())) {
            configsChanged.put(pair.getKey(), "added");
          }
        }
      } else {
        // All of the configs in this config type are new.
        for (String key : newConfigValues.keySet()) {
          configsChanged.put(key, "added");
        }
      }
      return configsChanged;
    }
  
    /**
     * Inverse a HashMap of the form key_i: value_j to value_j: [key_a, ..., key_z]
     * for all keys that contain that value.
     * This is useful for printing config deltas.
     * @param configKeyToAction Original dictionary
     * @return Inverse of the dictionary.
     */
    private Map<String, List<String>> inverseMapByValue(Map<String, String> configKeyToAction) {
      Map<String, List<String>> mapByValue = new HashMap<>();
  
      Iterator it = configKeyToAction.entrySet().iterator();
      while (it.hasNext()) {
        Map.Entry<String, String> pair = (Map.Entry) it.next();
        // Key is the config name, Value is the action (added, deleted, changed)
        if (mapByValue.containsKey(pair.getValue())) {
          mapByValue.get(pair.getValue()).add(pair.getKey());
        } else {
          List<String> configListForAction = new ArrayList<>();
          configListForAction.add(pair.getKey());
          mapByValue.put(pair.getValue(), configListForAction);
        }
      }
      return mapByValue;
    }
  
    /**
     * Get a string that represents config keys that are changed, added, or deleted.
     * @param actionToConfigKeyList Dictionary from the action to a list of configs in that category.
     * @return String that represents config keys that are changed, added, or deleted.
     */
    private String getActionToConfigListAsString(Map<String, List<String>> actionToConfigKeyList) {
      String output = "";
  
      String[] actions = {"added", "deleted", "changed"};
      int i = 0;
      for (String action : actions) {
        i++;
        output += action + ": [";
        if (actionToConfigKeyList.containsKey(action)) {
          List<String> values = actionToConfigKeyList.get(action);
          output += StringUtils.join(values, ", ");
  
        }
        if (i < actions.length) {
          output += "], ";
        } else {
          output += "]";
        }
      }
      return output;
    }
  
    private synchronized RequestStatusResponse updateCluster(ClusterRequest request,
                                                             Map<String, String> requestProperties,
                                                             boolean fireAgentUpdates, boolean refreshCluster
    )
        throws AmbariException, AuthorizationException {
  
      RequestStageContainer requestStageContainer = null;
  
      if (request.getClusterId() == null
          && (request.getClusterName() == null
          || request.getClusterName().isEmpty())) {
        throw new IllegalArgumentException("Invalid arguments, cluster id or cluster name should not be null");
      }
  
      LOG.info("Received a updateCluster request"
          + ", clusterId=" + request.getClusterId()
          + ", clusterName=" + request.getClusterName()
          + ", securityType=" + request.getSecurityType()
          + ", request=" + request);
  
      final Cluster cluster;
      if (request.getClusterId() == null) {
        cluster = clusters.getCluster(request.getClusterName());
      } else {
        cluster = clusters.getClusterById(request.getClusterId());
      }
  
      List<ConfigurationRequest> desiredConfigs = request.getDesiredConfig();
      if (desiredConfigs != null) {
        for (ConfigurationRequest configurationRequest : desiredConfigs) {
          if (StringUtils.isEmpty(configurationRequest.getVersionTag())) {
            configurationRequest.setVersionTag(UUID.randomUUID().toString());
          }
        }
      }
  
      // Ensure the user has access to update this cluster
      AuthorizationHelper.verifyAuthorization(ResourceType.CLUSTER, cluster.getResourceId(), RoleAuthorization.AUTHORIZATIONS_UPDATE_CLUSTER);
  
      //save data to return configurations created
      List<ConfigurationResponse> configurationResponses =
        new LinkedList<>();
      ServiceConfigVersionResponse serviceConfigVersionResponse = null;
      boolean nonServiceConfigsChanged = false;
  
      if (desiredConfigs != null && request.getServiceConfigVersionRequest() != null) {
        String msg = "Unable to set desired configs and rollback at same time, request = " + request;
        LOG.error(msg);
        throw new IllegalArgumentException(msg);
      }
  
      // set the new name of the cluster if change is requested
      if (request.getClusterName()!=null && !cluster.getClusterName().equals(request.getClusterName())) {
        validateClusterName(request.getClusterName());
        if (LOG.isDebugEnabled()) {
          LOG.debug("Received cluster name change request from {} to {}", cluster.getClusterName(), request.getClusterName());
        }
  
        if(!AuthorizationHelper.isAuthorized(ResourceType.AMBARI, null, EnumSet.of(RoleAuthorization.AMBARI_RENAME_CLUSTER))) {
          throw new AuthorizationException("The authenticated user does not have authorization to rename the cluster");
        }
  
        cluster.setClusterName(request.getClusterName());
      }
  
      //check if desired configs are available in request and they were changed
      boolean isConfigurationCreationNeeded = false;
      if (desiredConfigs != null) {
        for (ConfigurationRequest desiredConfig : desiredConfigs) {
          Map<String, String> requestConfigProperties = desiredConfig.getProperties();
          Map<String,Map<String,String>> requestConfigAttributes = desiredConfig.getPropertiesAttributes();
  
          // processing password properties
          if(requestConfigProperties != null && !requestConfigProperties.isEmpty()) {
            Map<PropertyInfo.PropertyType, Set<String>> propertiesTypes = cluster.getConfigPropertiesTypes(
                desiredConfig.getType()
            );
            for (Entry<String, String> property : requestConfigProperties.entrySet()) {
              String propertyName = property.getKey();
              String propertyValue = property.getValue();
              if ((propertiesTypes.containsKey(PropertyType.PASSWORD) &&
                  propertiesTypes.get(PropertyType.PASSWORD).contains(propertyName)) ||
                  (requestConfigAttributes != null && requestConfigAttributes.containsKey(PASSWORD) &&
                  requestConfigAttributes.get(PASSWORD).containsKey(propertyName) &&
                  requestConfigAttributes.get(PASSWORD).get(propertyName).equals("true"))) {
                if (SecretReference.isSecret(propertyValue)) {
                  SecretReference ref = new SecretReference(propertyValue, cluster);
                  requestConfigProperties.put(propertyName, ref.getValue());
                }
              }
            }
          }
  
          Config clusterConfig = cluster.getDesiredConfigByType(desiredConfig.getType());
          Map<String, String> clusterConfigProperties = null;
          Map<String,Map<String,String>> clusterConfigAttributes = null;
          if (clusterConfig != null) {
            clusterConfigProperties = clusterConfig.getProperties();
            clusterConfigAttributes = clusterConfig.getPropertiesAttributes();
            if (!isAttributeMapsEqual(requestConfigAttributes, clusterConfigAttributes)){
              isConfigurationCreationNeeded = true;
              break;
            }
          } else {
            isConfigurationCreationNeeded = true;
            break;
          }
  
          if (requestConfigProperties == null || requestConfigProperties.isEmpty()) {
            Config existingConfig = cluster.getConfig(desiredConfig.getType(), desiredConfig.getVersionTag());
            if (existingConfig != null) {
              if (!StringUtils.equals(existingConfig.getTag(), clusterConfig.getTag())) {
                isConfigurationCreationNeeded = true;
                break;
              }
            }
          }
          if (requestConfigProperties != null && clusterConfigProperties != null) {
            if (requestConfigProperties.size() != clusterConfigProperties.size()) {
              isConfigurationCreationNeeded = true;
              break;
            } else {
              if ( cluster.getServiceByConfigType(clusterConfig.getType()) != null &&  clusterConfig.getServiceConfigVersions().isEmpty() ) {
  
                //If there's no service config versions containing this config (except cluster configs), recreate it even if exactly equal
                LOG.warn("Existing desired config doesn't belong to any service config version, " +
                    "forcing config recreation, " +
                    "clusterName={}, type = {}, tag={}", cluster.getClusterName(), clusterConfig.getType(),
                    clusterConfig.getTag());
                isConfigurationCreationNeeded = true;
                break;
              }
              for (Entry<String, String> property : requestConfigProperties.entrySet()) {
                if (!StringUtils.equals(property.getValue(), clusterConfigProperties.get(property.getKey()))) {
                  isConfigurationCreationNeeded = true;
                  break;
                }
              }
            }
          }
        }
      }
  
      // set or create configuration mapping (and optionally create the map of properties)
      if (isConfigurationCreationNeeded) {
  
        if (!desiredConfigs.isEmpty()) {
          Set<Config> configs = new HashSet<>();
          String note = null;
  
          for (ConfigurationRequest cr : desiredConfigs) {
            String configType = cr.getType();
  
            if (null != cr.getProperties()) {
              // !!! empty property sets are supported, and need to be able to use
              // previously-defined configs (revert)
              Map<String, Config> all = cluster.getConfigsByType(configType);
              if (null == all ||                              // none set
                  !all.containsKey(cr.getVersionTag()) ||     // tag not set
                  cr.getProperties().size() > 0) {            // properties to set
  
                cr.setClusterName(cluster.getClusterName());
                configurationResponses.add(createConfiguration(cr, refreshCluster));
  
  
                LOG.info(MessageFormat.format("Applying configuration with tag ''{0}'' to cluster ''{1}''  for configuration type {2}",
                    cr.getVersionTag(),
                    request.getClusterName(),
                    configType));
              }
            }
            note = cr.getServiceConfigVersionNote();
            Config config = cluster.getConfig(configType, cr.getVersionTag());
            if (null != config) {
              configs.add(config);
            }
          }
          if (!configs.isEmpty()) {
            Map<String, Config> existingConfigTypeToConfig = new HashMap();
            for (Config config : configs) {
              Config existingConfig = cluster.getDesiredConfigByType(config.getType());
              existingConfigTypeToConfig.put(config.getType(), existingConfig);
            }
  
            String authName = getAuthName();
            serviceConfigVersionResponse = cluster.addDesiredConfig(authName, configs, note);
            if (serviceConfigVersionResponse != null) {
              List<String> hosts = serviceConfigVersionResponse.getHosts();
              int numAffectedHosts = null != hosts ? hosts.size() : 0;
              configChangeLog.info("(configchange) Changing default config. cluster: '{}', changed by: '{}', service_name: '{}', config_group: '{}', num affected hosts during creation: '{}', note: '{}'",
                  request.getClusterName(), authName, serviceConfigVersionResponse.getServiceName(),
                  serviceConfigVersionResponse.getGroupName(), numAffectedHosts, serviceConfigVersionResponse.getNote());
  
              for (Config config : configs) {
                config.getVersion();
                serviceConfigVersionResponse.getNote();
                configChangeLog.info("(configchange)    type: '{}', tag: '{}', version: '{}'", config.getType(), config.getTag(), config.getVersion());
  
                Map<String, String> configKeyToAction = getConfigKeyDeltaToAction(existingConfigTypeToConfig.get(config.getType()), config.getProperties());
                Map<String, List<String>> actionToListConfigKeys = inverseMapByValue(configKeyToAction);
  
                if (!actionToListConfigKeys.isEmpty()) {
                  String configOutput = getActionToConfigListAsString(actionToListConfigKeys);
                  configChangeLog.info("(configchange)    Config type '{}' was modified with the following keys, {}", config.getType(), configOutput);
                }
              }
            } else {
              nonServiceConfigsChanged = true;
            }
          }
        }
      }
  
      StackId currentVersion = cluster.getCurrentStackVersion();
      StackId desiredVersion = cluster.getDesiredStackVersion();
  
      // Set the current version value if its not already set
      if (currentVersion == null) {
        if(!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(), EnumSet.of(RoleAuthorization.CLUSTER_UPGRADE_DOWNGRADE_STACK))) {
          throw new AuthorizationException("The authenticated user does not have authorization to modify stack version");
        }
  
        cluster.setCurrentStackVersion(desiredVersion);
      }
  
      boolean requiresHostListUpdate =
          request.getHostNames() != null && !request.getHostNames().isEmpty();
  
      if (requiresHostListUpdate) {
        clusters.mapAndPublishHostsToCluster(
            request.getHostNames(), request.getClusterName());
      }
  
      // set the provisioning state of the cluster
      if (null != request.getProvisioningState()) {
        State oldProvisioningState = cluster.getProvisioningState();
        State provisioningState = State.valueOf(request.getProvisioningState());
  
        if (provisioningState != State.INIT
            && provisioningState != State.INSTALLED) {
          LOG.warn(
              "Invalid cluster provisioning state {} cannot be set on the cluster {}",
              provisioningState, request.getClusterName());
  
          throw new IllegalArgumentException(
              "Invalid cluster provisioning state "
              + provisioningState + " cannot be set on cluster "
              + request.getClusterName());
        }
  
        if (provisioningState != oldProvisioningState) {
          boolean isStateTransitionValid = State.isValidDesiredStateTransition(
              oldProvisioningState, provisioningState);
  
          if (!isStateTransitionValid) {
            LOG.warn(
                "Invalid cluster provisioning state {} cannot be set on the cluster {} because the current state is {}",
                provisioningState, request.getClusterName(), oldProvisioningState);
  
            throw new AmbariException("Invalid transition for"
                + " cluster provisioning state" + ", clusterName="
                + cluster.getClusterName() + ", clusterId="
                + cluster.getClusterId() + ", currentProvisioningState="
                + oldProvisioningState + ", newProvisioningState="
                + provisioningState);
          }
        }
  
        cluster.setProvisioningState(provisioningState);
      }
  
      if (null != request.getServiceConfigVersionRequest()) {
        if(!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(), EnumSet.of(RoleAuthorization.SERVICE_MODIFY_CONFIGS))) {
          throw new AuthorizationException("The authenticated user does not have authorization to modify service configurations");
        }
  
        ServiceConfigVersionRequest serviceConfigVersionRequest = request.getServiceConfigVersionRequest();
        if (StringUtils.isEmpty(serviceConfigVersionRequest.getServiceName()) ||
            null == serviceConfigVersionRequest.getVersion()) {
          String msg = "Service name and version should be specified in service config version";
          LOG.error(msg);
          throw new IllegalArgumentException(msg);
        }
  
        serviceConfigVersionResponse = cluster.setServiceConfigVersion(serviceConfigVersionRequest.getServiceName(),
            serviceConfigVersionRequest.getVersion(), getAuthName(),
            serviceConfigVersionRequest.getNote());
      }
  
      if (serviceConfigVersionResponse != null) {
        if (!configurationResponses.isEmpty()) {
          serviceConfigVersionResponse.setConfigurations(configurationResponses);
        }
  
        ClusterResponse clusterResponse =
            new ClusterResponse(cluster.getClusterId(), cluster.getClusterName(), null, null, null, 0, null, null);
  
        Map<String, Collection<ServiceConfigVersionResponse>> map =
          new HashMap<>();
        map.put(serviceConfigVersionResponse.getServiceName(), Collections.singletonList(serviceConfigVersionResponse));
  
        clusterResponse.setDesiredServiceConfigVersions(map);
  
        //workaround to be able to retrieve update results in resource provider
        //as this method only expected to return request response
        saveClusterUpdate(request, clusterResponse);
      }
  
      // set the new security type of the cluster if change is requested
      SecurityType securityType = request.getSecurityType();
  
      if(securityType != null) {
        // if any custom operations are valid and requested, the process of executing them should be initiated,
        // most of the validation logic will be left to the KerberosHelper to avoid polluting the controller
        if (kerberosHelper.shouldExecuteCustomOperations(securityType, requestProperties)) {
          if(!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(), EnumSet.of(RoleAuthorization.CLUSTER_TOGGLE_KERBEROS))) {
            throw new AuthorizationException("The authenticated user does not have authorization to perform Kerberos-specific operations");
          }
  
          try {
            requestStageContainer = kerberosHelper.executeCustomOperations(cluster, requestProperties, requestStageContainer,
                kerberosHelper.getManageIdentitiesDirective(requestProperties));
          } catch (KerberosOperationException e) {
            throw new IllegalArgumentException(e.getMessage(), e);
          }
        } else {
          // If force_toggle_kerberos is not specified, null will be returned. Therefore, perform an
          // equals check to yield true if the result is Boolean.TRUE, otherwise false.
          boolean forceToggleKerberos = kerberosHelper.getForceToggleKerberosDirective(requestProperties);
  
          if (forceToggleKerberos || (cluster.getSecurityType() != securityType)) {
            LOG.info("Received cluster security type change request from {} to {} (forced: {})",
                cluster.getSecurityType().name(), securityType.name(), forceToggleKerberos);
  
            if ((securityType == SecurityType.KERBEROS) || (securityType == SecurityType.NONE)) {
              if (!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(), EnumSet.of(RoleAuthorization.CLUSTER_TOGGLE_KERBEROS))) {
                throw new AuthorizationException("The authenticated user does not have authorization to enable or disable Kerberos");
              }
  
              // Since the security state of the cluster has changed, invoke toggleKerberos to handle
              // adding or removing Kerberos from the cluster. This may generate multiple stages
              // or not depending the current state of the cluster.
              try {
                requestStageContainer = kerberosHelper.toggleKerberos(cluster, securityType, requestStageContainer,
                    kerberosHelper.getManageIdentitiesDirective(requestProperties));
              } catch (KerberosOperationException e) {
                throw new IllegalArgumentException(e.getMessage(), e);
              }
            } else {
              throw new IllegalArgumentException(String.format("Unexpected security type encountered: %s", securityType.name()));
            }
  
            cluster.setSecurityType(securityType);
          }
        }
      }
      if (fireAgentUpdates && (serviceConfigVersionResponse != null || nonServiceConfigsChanged)) {
        configHelper.updateAgentConfigs(Collections.singleton(cluster.getClusterName()));
      }
  
      if (requestStageContainer != null) {
        requestStageContainer.persist();
        return requestStageContainer.getRequestStatusResponse();
      } else {
        return null;
      }
    }
  
    /**
     * validate cluster name character and length requirements and throw IllegalArgumentException if not valid.
     * <p>
     * Character Requirements
     * <p>
     * A through Z
     * a through z
     * 0 through 9
     * _ (underscore)
     * - (dash)
     * Length Requirements
     * <p>
     * Minimum: 1 character
     * Maximum: 100 characters
     * @see AmbariManagementControllerImpl#CLUSTER_NAME_VALIDATION_REGEXP
     *
     * @param clusterName name to validate
     * @throws IllegalArgumentException if validation result
     */
    public static void validateClusterName(String clusterName) {
      if (clusterName == null) {
        throw new IllegalArgumentException("Invalid arguments, cluster name should not be null");
      }
      if (clusterName.isEmpty()) {
        throw new IllegalArgumentException("Invalid arguments, cluster name should not be empty");
      }
      Matcher mtch = CLUSTER_NAME_PTRN.matcher(clusterName);
      if(!mtch.matches()){
        throw new IllegalArgumentException("Invalid arguments, cluster name should contains only alphabetical, numeric, '_' and '-' characters and length 1-100 characters");
      }
    }
  
  
    /**
     * Given a configuration request, compares the requested properties to the current set of desired
     * properties for the same configuration type and returns a map of property names to an array of
     * Strings representing the current value (index 0), and the requested value (index 1).
     * <p>
     * <ul>
     * <li>
     * If a property is set in the requested property set and not found in the current property set,
     * the current value (index 0) will be <code>null</code> - {<code>null</code>, "requested value"}
     * </li>
     * <li>
     * If a property is set in the current property set and not found in the requested property set,
     * the requested value (index 1) will be <code>null</code> - {"current value", <code>null</code>}
     * </li>
     * <li>
     * If a property found in bother current property set and the requested property set,
     * the requested value (index 1) will be <code>null</code> - {"current value", "requested value"}
     * </li>
     * </ul>
     *
     * @param cluster the relevant cluster
     * @param request the request data
     * @return a map lf property names to String arrays indicating the requsted changes ({current value, requested valiue})
     */
    private Map<String, String[]> getPropertyChanges(Cluster cluster, ConfigurationRequest request) {
      Map<String, String[]>  changedProperties = new HashMap<>();
  
      // Ensure that the requested property map is not null.
      Map<String, String> requestedProperties  = request.getProperties();
      if (requestedProperties == null) {
        requestedProperties = Collections.emptyMap();
      }
  
      // Get the current/desired properties for the relevant configuration type and ensure that the
      // property map is not null.
      Config existingConfig = cluster.getDesiredConfigByType(request.getType());
      Map<String, String> existingProperties = (existingConfig == null) ? null : existingConfig.getProperties();
      if (existingProperties == null) {
        existingProperties = Collections.emptyMap();
      }
  
      // Ensure all propery names are captured, including missing ones from either set.
      Set<String> propertyNames = new HashSet<>();
      propertyNames.addAll(requestedProperties.keySet());
      propertyNames.addAll(existingProperties.keySet());
  
      for(String propertyName:propertyNames) {
        String requestedValue = requestedProperties.get(propertyName);
        String existingValue = existingProperties.get(propertyName);
  
        // Perform case-sensitive match.  It is possible that case matters here.
        if((requestedValue == null) ? (existingValue != null) : !requestedValue.equals(existingValue)) {
          changedProperties.put(propertyName, new String[]{existingValue, requestedValue});
        }
      }
  
      return changedProperties;
    }
  
    /**
     * Comparison of two attributes maps
     * @param requestConfigAttributes - attribute map sent from API
     * @param clusterConfigAttributes - existed attribute map
     * @return true if maps is equal (have the same attributes and their values)
     */
    public boolean isAttributeMapsEqual(Map<String, Map<String, String>> requestConfigAttributes,
            Map<String, Map<String, String>> clusterConfigAttributes) {
      boolean isAttributesEqual = true;
      if ((requestConfigAttributes != null && clusterConfigAttributes == null)
              || (requestConfigAttributes == null && clusterConfigAttributes != null)
              || (requestConfigAttributes != null && clusterConfigAttributes != null
              && !requestConfigAttributes.keySet().equals(clusterConfigAttributes.keySet()))) {
        return false;
      } else if (clusterConfigAttributes != null && requestConfigAttributes != null) {
        for (Entry<String, Map<String, String>> ClusterEntrySet : clusterConfigAttributes.entrySet()) {
          Map<String, String> clusterMapAttributes = ClusterEntrySet.getValue();
          Map<String, String> requestMapAttributes = requestConfigAttributes.get(ClusterEntrySet.getKey());
          if ((requestMapAttributes != null && clusterMapAttributes == null)
                  || (requestMapAttributes == null && clusterMapAttributes != null)
                  || (requestMapAttributes != null && clusterMapAttributes != null
                  && !requestMapAttributes.keySet().equals(clusterMapAttributes.keySet()))) {
            return false;
          } else if (requestMapAttributes != null && clusterMapAttributes != null) {
            for (Entry<String, String> requestPropertyEntrySet : requestMapAttributes.entrySet()) {
              String requestPropertyValue = requestPropertyEntrySet.getValue();
              String clusterPropertyValue = clusterMapAttributes.get(requestPropertyEntrySet.getKey());
              if ((requestPropertyValue != null && clusterPropertyValue == null)
                      || (requestPropertyValue == null && clusterPropertyValue != null)
                      || (requestPropertyValue != null && clusterPropertyValue != null
                      && !requestPropertyValue.equals(clusterPropertyValue))) {
                return false;
              }
            }
          }
  
        }
      }
      return isAttributesEqual;
    }
  
    /**
     * Save cluster update results to retrieve later
     * @param clusterRequest   cluster request info
     * @param clusterResponse  cluster response info
     */
    public void saveClusterUpdate(ClusterRequest clusterRequest, ClusterResponse clusterResponse) {
      clusterUpdateCache.put(clusterRequest, clusterResponse);
    }
  
  
    @Override
    public ClusterResponse getClusterUpdateResults(ClusterRequest clusterRequest) {
      return clusterUpdateCache.getIfPresent(clusterRequest);
    }
  
    @Override
    public ConfigGroupResponse getConfigGroupUpdateResults(ConfigGroupRequest configGroupRequest) {
      return configGroupUpdateCache.getIfPresent(configGroupRequest);
    }
  
    @Override
    public void saveConfigGroupUpdate(ConfigGroupRequest configGroupRequest, ConfigGroupResponse configGroupResponse) {
      configGroupUpdateCache.put(configGroupRequest, configGroupResponse);
    }
  
    @Override
    public String getJobTrackerHost(Cluster cluster) {
      try {
        Service svc = cluster.getService("MAPREDUCE");
        ServiceComponent sc = svc.getServiceComponent(Role.JOBTRACKER.toString());
        if (sc.getServiceComponentHosts() != null
            && !sc.getServiceComponentHosts().isEmpty()) {
          return sc.getServiceComponentHosts().keySet().iterator().next();
        }
      } catch (AmbariException ex) {
        return null;
      }
      return null;
    }
  
    private Set<String> getServicesForSmokeTests(Cluster cluster,
               Map<State, List<Service>> changedServices,
               Map<String, Map<State, List<ServiceComponentHost>>> changedScHosts,
               boolean runSmokeTest) throws AmbariException {
  
      // We choose the most general (high-level) op level here. As a result,
      // service checks will be only launched for services/components that
      // are not in a Maintenance state.
      Resource.Type opLvl = Resource.Type.Cluster;
  
      Set<String> smokeTestServices = new HashSet<>();
  
      // Adding smoke checks for changed services
      if (changedServices != null) {
        for (Entry<State, List<Service>> entry : changedServices.entrySet()) {
          if (State.STARTED != entry.getKey()) {
            continue;
          }
          for (Service s : entry.getValue()) {
            if (runSmokeTest && (State.INSTALLED == s.getDesiredState() &&
                    maintenanceStateHelper.isOperationAllowed(opLvl, s))) {
              smokeTestServices.add(s.getName());
            }
          }
        }
      }
  
      // Adding smoke checks for changed host components
      Map<String, Map<String, Integer>> changedComponentCount =
        new HashMap<>();
      for (Map<State, List<ServiceComponentHost>> stateScHostMap :
        changedScHosts.values()) {
        for (Entry<State, List<ServiceComponentHost>> entry :
          stateScHostMap.entrySet()) {
          if (State.STARTED != entry.getKey()) {
            continue;
          }
          for (ServiceComponentHost sch : entry.getValue()) {
            if (State.INSTALLED != sch.getState()) {
              continue;
            }
            if (! maintenanceStateHelper.isOperationAllowed(opLvl, sch)) {
              continue;
            }
            if (!changedComponentCount.containsKey(sch.getServiceName())) {
              changedComponentCount.put(sch.getServiceName(), new HashMap<>());
            }
            if (!changedComponentCount.get(sch.getServiceName())
              .containsKey(sch.getServiceComponentName())) {
              changedComponentCount.get(sch.getServiceName())
                .put(sch.getServiceComponentName(), 1);
            } else {
              Integer i = changedComponentCount.get(sch.getServiceName())
                .get(sch.getServiceComponentName());
              changedComponentCount.get(sch.getServiceName())
                .put(sch.getServiceComponentName(), ++i);
            }
          }
        }
      }
  
      // Add service checks for any changed master component hosts or if
      // more then one component has been changed for a service
      for (Entry<String, Map<String, Integer>> entry :
        changedComponentCount.entrySet()) {
        String serviceName = entry.getKey();
        Service s = cluster.getService(serviceName);
        // smoke test service if more than one component is started
        if (runSmokeTest && (entry.getValue().size() > 1) &&
                maintenanceStateHelper.isOperationAllowed(opLvl, s)) {
          smokeTestServices.add(serviceName);
          continue;
        }
        for (String componentName :
          changedComponentCount.get(serviceName).keySet()) {
          ServiceComponent sc = cluster.getService(serviceName).
            getServiceComponent(componentName);
          StackId stackId = sc.getDesiredStackId();
          ComponentInfo compInfo = ambariMetaInfo.getComponent(
            stackId.getStackName(), stackId.getStackVersion(), serviceName,
            componentName);
          if (runSmokeTest && compInfo.isMaster() &&
                  // op lvl handling for service component
                  // is the same as for service
                  maintenanceStateHelper.isOperationAllowed(opLvl, s)) {
            smokeTestServices.add(serviceName);
          }
          // FIXME if master check if we need to run a smoke test for the master
        }
      }
      return smokeTestServices;
    }
  
    private void addClientSchForReinstall(Cluster cluster,
              Map<State, List<Service>> changedServices,
              Map<String, Map<State, List<ServiceComponentHost>>> changedScHosts)
              throws AmbariException {
  
      Set<String> services = new HashSet<>();
  
      // This is done to account for services with client only components.
      if (changedServices != null) {
        for (Entry<State, List<Service>> entry : changedServices.entrySet()) {
          if (State.STARTED != entry.getKey()) {
            continue;
          }
          for (Service s : entry.getValue()) {
            if (State.INSTALLED == s.getDesiredState()) {
              services.add(s.getName());
            }
          }
        }
      }
  
      // Flatten changed Schs that are going to be Started
      List<ServiceComponentHost> serviceComponentHosts = new ArrayList<>();
      if (changedScHosts != null && !changedScHosts.isEmpty()) {
        for (Entry<String, Map<State, List<ServiceComponentHost>>> stringMapEntry : changedScHosts.entrySet()) {
          for (State state : stringMapEntry.getValue().keySet()) {
            if (state == State.STARTED) {
              serviceComponentHosts.addAll(stringMapEntry.getValue().get(state));
            }
          }
        }
      }
  
      if (!serviceComponentHosts.isEmpty()) {
        for (ServiceComponentHost sch : serviceComponentHosts) {
          services.add(sch.getServiceName());
        }
      }
  
      if (services.isEmpty()) {
        return;
      }
  
      Map<String, List<ServiceComponentHost>> clientSchs = new HashMap<>();
  
      for (String serviceName : services) {
        Service s = cluster.getService(serviceName);
        for (String component : s.getServiceComponents().keySet()) {
          List<ServiceComponentHost> potentialHosts = new ArrayList<>();
          ServiceComponent sc = s.getServiceComponents().get(component);
          if (sc.isClientComponent()) {
            for (ServiceComponentHost potentialSch : sc.getServiceComponentHosts().values()) {
              Host host = clusters.getHost(potentialSch.getHostName());
              // Host is alive and neither host nor SCH is in Maintenance State
              if (!potentialSch.getHostState().equals(HostState.HEARTBEAT_LOST)
                  && potentialSch.getMaintenanceState() != MaintenanceState.ON
                  && host.getMaintenanceState(cluster.getClusterId()) == MaintenanceState.OFF) {
                potentialHosts.add(potentialSch);
              }
            }
          }
          if (!potentialHosts.isEmpty()) {
            clientSchs.put(sc.getName(), potentialHosts);
          }
        }
      }
      LOG.info("Client hosts for reinstall : " + clientSchs.size());
  
      if (changedScHosts != null) {
        for (Entry<String, List<ServiceComponentHost>> stringListEntry : clientSchs.entrySet()) {
          Map<State, List<ServiceComponentHost>> schMap = new EnumMap<>(State.class);
          schMap.put(State.INSTALLED, stringListEntry.getValue());
          changedScHosts.put(stringListEntry.getKey(), schMap);
        }
      }
    }
  
    @Override
    public Map<String, Map<String,String>> findConfigurationTagsWithOverrides(
            Cluster cluster, String hostName,
            @Nullable Map<String, DesiredConfig> desiredConfigs) throws AmbariException {
  
      return configHelper.getEffectiveDesiredTags(cluster, hostName, desiredConfigs);
    }
  
    @Override
    public RequestExecutionFactory getRequestExecutionFactory() {
      return requestExecutionFactory;
    }
  
    @Override
    public ExecutionScheduleManager getExecutionScheduleManager() {
      return executionScheduleManager;
    }
  
    /**
     * Creates and populates an EXECUTION_COMMAND for host
     */
    private void createHostAction(Cluster cluster,
                                  Stage stage,
                                  ServiceComponentHost scHost,
                                  RoleCommand roleCommand,
                                  Map<String, String> commandParamsInp,
                                  ServiceComponentHostEvent event,
                                  boolean skipFailure,
                                  RepositoryVersionEntity repoVersion,
                                  boolean isUpgradeSuspended,
                                  DatabaseType databaseType,
                                  Map<String, DesiredConfig> clusterDesiredConfigs,
                                  boolean useLatestConfigs)
                                  throws AmbariException {
  
      String serviceName = scHost.getServiceName();
  
      stage.addHostRoleExecutionCommand(scHost.getHost(),
          Role.valueOf(scHost.getServiceComponentName()), roleCommand, event, cluster, serviceName, false, skipFailure);
  
      String componentName = scHost.getServiceComponentName();
      String hostname = scHost.getHostName();
      Host host = clusters.getHost(hostname);
      HostEntity hostEntity = host.getHostEntity();
      Map<String, String> hostAttributes = gson.fromJson(hostEntity.getHostAttributes(), hostAttributesType);
      String osFamily = host.getOSFamilyFromHostAttributes(hostAttributes);
  
      StackId stackId = scHost.getServiceComponent().getDesiredStackId();
  
      ServiceInfo serviceInfo = ambariMetaInfo.getService(stackId.getStackName(),
          stackId.getStackVersion(), serviceName);
      ComponentInfo componentInfo = ambariMetaInfo.getComponent(
        stackId.getStackName(), stackId.getStackVersion(),
        serviceName, componentName);
      StackInfo stackInfo = ambariMetaInfo.getStack(stackId.getStackName(),
          stackId.getStackVersion());
      Map<String, ServiceInfo> servicesMap = ambariMetaInfo.getServices(stackInfo.getName(), stackInfo.getVersion());
  
      ExecutionCommandWrapper execCmdWrapper = stage.getExecutionCommandWrapper(hostname, componentName);
      ExecutionCommand execCmd = execCmdWrapper.getExecutionCommand();
  
      execCmd.setConfigurations(new TreeMap<>());
  
      // Get the value of credential store enabled from the DB
      Service clusterService = cluster.getService(serviceName);
      execCmd.setCredentialStoreEnabled(String.valueOf(clusterService.isCredentialStoreEnabled()));
  
      ServiceComponent component = clusterService.getServiceComponent(componentName);
  
      // Get the map of service config type to password properties for the service
      Map<String, Map<String, String>> configCredentials;
      configCredentials = configCredentialsForService.get(clusterService.getName());
      if (configCredentials == null) {
        configCredentials = configHelper.getCredentialStoreEnabledProperties(stackId, clusterService);
        configCredentialsForService.put(clusterService.getName(), configCredentials);
      }
  
      execCmd.setConfigurationCredentials(configCredentials);
  
      // Create a local copy for each command
      Map<String, String> commandParams = new TreeMap<>();
      if (commandParamsInp != null) { // if not defined
        commandParams.putAll(commandParamsInp);
      }
  
      boolean isInstallCommand = roleCommand.equals(RoleCommand.INSTALL);
      String agentDefaultCommandTimeout = configs.getDefaultAgentTaskTimeout(isInstallCommand);
      String scriptCommandTimeout = "";
      /*
       * This script is only used for
       * default commands like INSTALL/STOP/START
       */
      CommandScriptDefinition script = componentInfo.getCommandScript();
      if (serviceInfo.getSchemaVersion().equals(AmbariMetaInfo.SCHEMA_VERSION_2)) {
        if (script != null) {
          commandParams.put(SCRIPT, script.getScript());
          commandParams.put(SCRIPT_TYPE, script.getScriptType().toString());
  
          boolean retryEnabled = false;
          Integer retryMaxTime = 0;
          if (commandParams.containsKey(CLUSTER_PHASE_PROPERTY) &&
              (commandParams.get(CLUSTER_PHASE_PROPERTY).equals(CLUSTER_PHASE_INITIAL_INSTALL) ||
              commandParams.get(CLUSTER_PHASE_PROPERTY).equals(CLUSTER_PHASE_INITIAL_START))) {
            String retryEnabledStr =
                configHelper.getValueFromDesiredConfigurations(cluster, ConfigHelper.CLUSTER_ENV,
                                                               ConfigHelper.CLUSTER_ENV_RETRY_ENABLED);
            String commandsStr =
                configHelper.getValueFromDesiredConfigurations(cluster, ConfigHelper.CLUSTER_ENV,
                                                               ConfigHelper.CLUSTER_ENV_RETRY_COMMANDS);
            String retryMaxTimeStr =
                configHelper.getValueFromDesiredConfigurations(cluster,
                                                               ConfigHelper.CLUSTER_ENV,
                                                               ConfigHelper.CLUSTER_ENV_RETRY_MAX_TIME_IN_SEC);
            if (StringUtils.isNotEmpty(retryEnabledStr)) {
              retryEnabled = Boolean.TRUE.toString().equals(retryEnabledStr);
            }
  
            if (retryEnabled) {
              retryMaxTime = NumberUtils.toInt(retryMaxTimeStr, 0);
              if (retryMaxTime < 0) {
                retryMaxTime = 0;
              }
  
              if (StringUtils.isNotEmpty(commandsStr)) {
                boolean commandMayBeRetried = false;
                String[] commands = commandsStr.split(",");
                for (String command : commands) {
                  if (roleCommand.toString().equals(command.trim())) {
                    commandMayBeRetried = true;
                  }
                }
                retryEnabled = commandMayBeRetried;
              }
            }
            LOG.info("Auto retry setting for {}-{} on {} is retryEnabled={} and retryMaxTime={}", serviceName, componentName, scHost.getHostName(), retryEnabled, retryMaxTime);
          }
          commandParams.put(MAX_DURATION_OF_RETRIES, Integer.toString(retryMaxTime));
          commandParams.put(COMMAND_RETRY_ENABLED, Boolean.toString(retryEnabled));
  
          if (script.getTimeout() > 0) {
            scriptCommandTimeout = String.valueOf(script.getTimeout());
          }
        } else {
          String message = String.format("Component %s of service %s has no " +
            "command script defined", componentName, serviceName);
          throw new AmbariException(message);
        }
      }
  
      String actualTimeout = (!scriptCommandTimeout.equals("") ? scriptCommandTimeout : agentDefaultCommandTimeout);
  
      // Because the INSTALL command can take much longer than typical commands, set the timeout to be the max
      // between the script's service component timeout and the agent default timeout.
      if (roleCommand.equals(RoleCommand.INSTALL) && !agentDefaultCommandTimeout.equals("") &&
          Integer.parseInt(actualTimeout) < Integer.parseInt(agentDefaultCommandTimeout)) {
        actualTimeout = agentDefaultCommandTimeout;
      }
  
      commandParams.put(COMMAND_TIMEOUT, actualTimeout);
  
      String customCacheDirectory = componentInfo.getCustomFolder();
      if (customCacheDirectory != null) {
        File customCache = new File(configs.getResourceDirPath(), customCacheDirectory);
        if (customCache.exists() && customCache.isDirectory()) {
          commandParams.put(CUSTOM_FOLDER, customCacheDirectory);
        }
      }
  
      String clusterName = cluster.getClusterName();
      if (customCommandExecutionHelper.isTopologyRefreshRequired(roleCommand.name(), clusterName, serviceName)) {
        commandParams.put(ExecutionCommand.KeyNames.REFRESH_TOPOLOGY, "True");
      }
      StageUtils.useAmbariJdkInCommandParams(commandParams, configs);
  
      String repoInfo;
      try {
        repoInfo = repoVersionHelper.getRepoInfoString(cluster, component, host);
      } catch (SystemException e) {
        throw new RuntimeException(e);
      }
      if (LOG.isDebugEnabled()) {
        LOG.debug("Sending repo information to agent, hostname={}, clusterName={}, stackInfo={}, repoInfo={}",
          scHost.getHostName(), clusterName, stackId.getStackId(), repoInfo);
      }
  
      Map<String, String> hostParams = new TreeMap<>();
  
      if (roleCommand.equals(RoleCommand.INSTALL)) {
        List<ServiceOsSpecific.Package> packages =
            getPackagesForServiceHost(serviceInfo, hostParams, osFamily);
        String packageList = gson.toJson(packages);
        commandParams.put(PACKAGE_LIST, packageList);
      }
  
      Set<PropertyInfo> stackProperties = ambariMetaInfo.getStackProperties(stackInfo.getName(), stackInfo.getVersion());
  
      Set<String> userSet = configHelper.getPropertyValuesWithPropertyType(PropertyType.USER, cluster, clusterDesiredConfigs, servicesMap, stackProperties);
      String userList = gson.toJson(userSet);
      hostParams.put(USER_LIST, userList);
  
      //Create a user_group mapping and send it as part of the hostLevelParams
      Map<String, Set<String>> userGroupsMap = configHelper.createUserGroupsMap(
        cluster, clusterDesiredConfigs, servicesMap, stackProperties);
      String userGroups = gson.toJson(userGroupsMap);
      hostParams.put(USER_GROUPS, userGroups);
  
      Set<String> groupSet = configHelper.getPropertyValuesWithPropertyType(PropertyType.GROUP, cluster, clusterDesiredConfigs, servicesMap, stackProperties);
      String groupList = gson.toJson(groupSet);
      hostParams.put(GROUP_LIST, groupList);
  
      Map<PropertyInfo, String> notManagedHdfsPathMap = configHelper.getPropertiesWithPropertyType(PropertyType.NOT_MANAGED_HDFS_PATH, cluster, clusterDesiredConfigs, servicesMap, stackProperties);
      Set<String> notManagedHdfsPathSet = configHelper.filterInvalidPropertyValues(notManagedHdfsPathMap, NOT_MANAGED_HDFS_PATH_LIST);
      String notManagedHdfsPathList = gson.toJson(notManagedHdfsPathSet);
      hostParams.put(NOT_MANAGED_HDFS_PATH_LIST, notManagedHdfsPathList);
  
      if (databaseType == DatabaseType.ORACLE) {
        hostParams.put(DB_DRIVER_FILENAME, configs.getOjdbcJarName());
      } else if (databaseType == DatabaseType.MYSQL) {
        hostParams.put(DB_DRIVER_FILENAME, configs.getMySQLJarName());
      }
  
      hostParams.put(CLIENTS_TO_UPDATE_CONFIGS, getClientsToUpdateConfigs(componentInfo));
  
      execCmd.setHostLevelParams(hostParams);
  
      Map<String, String> roleParams = new TreeMap<>();
  
      // !!! consistent with where custom commands put variables
      // !!! after-INSTALL hook checks this such that the stack selection tool won't
      // select-all to a version that is not being upgraded, breaking RU
      if (isUpgradeSuspended) {
        cluster.addSuspendedUpgradeParameters(commandParams, roleParams);
      }
  
      execCmd.setRoleParams(roleParams);
      execCmd.setCommandParams(commandParams);
  
      CommandRepository commandRepository;
      try {
        commandRepository = repoVersionHelper.getCommandRepository(cluster, component, host);
      } catch (SystemException e) {
        throw new RuntimeException(e);
      }
      execCmd.setRepositoryFile(commandRepository);
      execCmdWrapper.setVersions(cluster, null);
  
      if (useLatestConfigs) {
        execCmd.setUseLatestConfigs(useLatestConfigs);
      }
    }
  
    /**
     * Computes os-dependent packages for service/host. Does not take into
     * account package dependencies for ANY_OS. Instead of this method
     * you should use getPackagesForServiceHost()
     * because it takes into account both os-dependent and os-independent lists
     * of packages for service.
     * @param hostParams may be modified (appended SERVICE_REPO_INFO)
     * @return a list of os-dependent packages for host
     */
    protected ServiceOsSpecific populateServicePackagesInfo(ServiceInfo serviceInfo, Map<String, String> hostParams,
                                                          String osFamily) {
      ServiceOsSpecific hostOs = new ServiceOsSpecific(osFamily);
      List<ServiceOsSpecific> foundOSSpecifics = getOSSpecificsByFamily(serviceInfo.getOsSpecifics(), osFamily);
      if (!foundOSSpecifics.isEmpty()) {
        for (ServiceOsSpecific osSpecific : foundOSSpecifics) {
          hostOs.addPackages(osSpecific.getPackages());
        }
        // Choose repo that is relevant for host
        ServiceOsSpecific.Repo serviceRepo = hostOs.getRepo();
        if (serviceRepo != null) {
          String serviceRepoInfo = gson.toJson(serviceRepo);
          hostParams.put(SERVICE_REPO_INFO, serviceRepoInfo);
        }
      }
      return hostOs;
    }
  
    @Override
    public List<ServiceOsSpecific.Package> getPackagesForServiceHost(ServiceInfo serviceInfo, Map<String, String> hostParams, String osFamily) {
      // Write down os specific info for the service
      ServiceOsSpecific anyOs = null;
      if (serviceInfo.getOsSpecifics().containsKey(AmbariMetaInfo.ANY_OS)) {
        anyOs = serviceInfo.getOsSpecifics().get(AmbariMetaInfo.ANY_OS);
      }
  
      ServiceOsSpecific hostOs = populateServicePackagesInfo(serviceInfo, hostParams, osFamily);
  
      // Build package list that is relevant for host
      List<ServiceOsSpecific.Package> packages =
              new ArrayList<>();
      if (anyOs != null) {
        packages.addAll(anyOs.getPackages());
      }
  
      if (hostOs != null) {
        packages.addAll(hostOs.getPackages());
      }
  
      return packages;
    }
  
    private List<ServiceOsSpecific> getOSSpecificsByFamily(Map<String, ServiceOsSpecific> osSpecifics, String osFamily) {
      List<ServiceOsSpecific> foundOSSpecifics = new ArrayList<>();
      for (Entry<String, ServiceOsSpecific> osSpecific : osSpecifics.entrySet()) {
        String[] osFamilyNames = osSpecific.getKey().split("\\s*,\\s*");
  
        for(String osFamilyName:osFamilyNames) {
          if (this.osFamily.isVersionedOsFamilyExtendedByVersionedFamily(osFamily, osFamilyName)) {
            foundOSSpecifics.add(osSpecific.getValue());
            break;
          }
        }
      }
      return foundOSSpecifics;
    }
  
    private ActionExecutionContext getActionExecutionContext
            (ExecuteActionRequest actionRequest) throws AmbariException {
      RequestOperationLevel operationLevel = actionRequest.getOperationLevel();
      if (actionRequest.isCommand()) {
        ActionExecutionContext actionExecutionContext =
                new ActionExecutionContext(actionRequest.getClusterName(),
                actionRequest.getCommandName(), actionRequest.getResourceFilters(),
                actionRequest.getParameters());
        actionExecutionContext.setOperationLevel(operationLevel);
        return actionExecutionContext;
      } else { // If action
  
        ActionDefinition actionDef =
                ambariMetaInfo.getActionDefinition(actionRequest.getActionName());
  
        if (actionDef == null) {
          throw new AmbariException(
                  "Action " + actionRequest.getActionName() + " does not exist");
        }
  
        ActionExecutionContext actionExecutionContext =
                new ActionExecutionContext(actionRequest.getClusterName(),
                actionRequest.getActionName(), actionRequest.getResourceFilters(),
                actionRequest.getParameters(), actionDef.getTargetType(),
                actionDef.getDefaultTimeout(), actionDef.getTargetService(),
                actionDef.getTargetComponent());
        actionExecutionContext.setOperationLevel(operationLevel);
        return actionExecutionContext;
      }
    }
  
    protected RequestStageContainer doStageCreation(RequestStageContainer requestStages,
        Cluster cluster,
        Map<State, List<Service>> changedServices,
        Map<State, List<ServiceComponent>> changedComps,
        Map<String, Map<State, List<ServiceComponentHost>>> changedScHosts,
        Map<String, String> requestParameters,
        Map<String, String> requestProperties,
        boolean runSmokeTest, boolean reconfigureClients, boolean useLatestConfigs, boolean useClusterHostInfo)
        throws AmbariException {
  
  
      // TODO handle different transitions?
      // Say HDFS to stopped and MR to started, what order should actions be done
      // in?
  
      // TODO additional validation?
      // verify all configs
      // verify all required components
  
      if ((changedServices == null || changedServices.isEmpty())
          && (changedComps == null || changedComps.isEmpty())
          && (changedScHosts == null || changedScHosts.isEmpty())) {
        LOG.info("Created 0 stages");
        return requestStages;
      }
  
      // check all stack configs are present in desired configs
      configHelper.checkAllStageConfigsPresentInDesiredConfigs(cluster);
  
      // caching upgrade suspended
      boolean isUpgradeSuspended = cluster.isUpgradeSuspended();
  
      // caching database type
      DatabaseType databaseType = configs.getDatabaseType();
  
      // smoke test any service that goes from installed to started
      Set<String> smokeTestServices = getServicesForSmokeTests(cluster,
        changedServices, changedScHosts, runSmokeTest);
  
      if (reconfigureClients) {
        // Re-install client only hosts to reattach changed configs on service
        // restart
        addClientSchForReinstall(cluster, changedServices, changedScHosts);
      }
  
      if (!changedScHosts.isEmpty()
          || !smokeTestServices.isEmpty()) {
        long nowTimestamp = System.currentTimeMillis();
  
        // FIXME cannot work with a single stage
        // multiple stages may be needed for reconfigure
        Map<String, Set<String>> clusterHostInfo = StageUtils.getClusterHostInfo(cluster);
  
        String clusterHostInfoJson = StageUtils.getGson().toJson(clusterHostInfo);
  
        Stage stage = createNewStage(requestStages.getLastStageId(), cluster,
            requestStages.getId(), requestProperties.get(RequestResourceProvider.CONTEXT),
            "{}", null);
        boolean skipFailure = false;
        if (requestProperties.containsKey(Setting.SETTING_NAME_SKIP_FAILURE) && requestProperties.get(Setting.SETTING_NAME_SKIP_FAILURE).equalsIgnoreCase("true")) {
          skipFailure = true;
        }
        stage.setAutoSkipFailureSupported(skipFailure);
        stage.setSkippable(skipFailure);
  
        Collection<ServiceComponentHost> componentsToEnableKerberos = new ArrayList<>();
        Set<String> hostsToForceKerberosOperations = new HashSet<>();
  
        /* *******************************************************************************************
         * If Kerberos is enabled, pre-process the changed components to update any configurations and
         * indicate which components may need to have principals or keytab files created.
         *
         * NOTE: Configurations need to be updated before tasks are created to install components
         *       so that any configuration changes are included before the task is queued.
         *
         *       Kerberos-related stages need to be inserted between the INSTALLED and STARTED states
         *       because some services need to set up the host (i,e, create user accounts, etc...)
         *       before Kerberos-related tasks an occur (like distribute keytabs)
         * **************************************************************************************** */
        if(kerberosHelper.isClusterKerberosEnabled(cluster)) {
          Collection<ServiceComponentHost> componentsToConfigureForKerberos = new ArrayList<>();
  
          for (Map<State, List<ServiceComponentHost>> changedScHostStates : changedScHosts.values()) {
  
            if (changedScHostStates != null) {
              for (Map.Entry<State, List<ServiceComponentHost>> changedScHostState : changedScHostStates.entrySet()) {
                State newState = changedScHostState.getKey();
  
                if (newState == State.INSTALLED) {
                  List<ServiceComponentHost> scHosts = changedScHostState.getValue();
  
                  if (scHosts != null) {
                    for (ServiceComponentHost scHost : scHosts) {
                      State oldSchState = scHost.getState();
  
                      // If the state is transitioning from INIT TO INSTALLED and the cluster has Kerberos
                      // enabled, mark this ServiceComponentHost to see if anything needs to be done to
                      // make sure it is properly configured.
                      //
                      // If the component is transitioning from an INSTALL_FAILED to an INSTALLED state
                      // indicates a failure attempt on install followed by a new installation attempt and
                      // will also need consideration for Kerberos-related tasks
                      if ((oldSchState == State.INIT || oldSchState == State.INSTALL_FAILED)) {
                        // Check if the host component already exists, if it exists there is no need to
                        // reset Kerberos-related configs.
                        // Check if it's blueprint install. If it is, then do not configure this service
                        // at this time.
                        if (!hostComponentAlreadyExists(cluster, scHost) && !(CLUSTER_PHASE_INITIAL_INSTALL.equals(requestProperties.get(CLUSTER_PHASE_PROPERTY)))) {
                          componentsToConfigureForKerberos.add(scHost);
                        }
  
                        // Add the ServiceComponentHost to the componentsToEnableKerberos Set to indicate
                        // it may need Kerberos-related operations to be performed on its behalf.
                        // For example, creating principals and keytab files.
                        componentsToEnableKerberos.add(scHost);
  
                        if (Service.Type.KERBEROS.name().equalsIgnoreCase(scHost.getServiceName()) &&
                            Role.KERBEROS_CLIENT.name().equalsIgnoreCase(scHost.getServiceComponentName())) {
                          // Since the KERBEROS/KERBEROS_CLIENT is about to be moved from the INIT to the
                          // INSTALLED state (and it should be by the time the stages (in this request)
                          // that need to be execute), collect the relevant hostname to make sure the
                          // Kerberos logic doest not skip operations for it.
                          hostsToForceKerberosOperations.add(scHost.getHostName());
                        }
                      }
                    }
                  }
                }
              }
            }
          }
  
          // If there are any components that may need Kerberos-related configuration changes, do it
          // here - before the INSTALL tasks get created so the configuration updates are set and
          // get included in the task details.
          if (!componentsToConfigureForKerberos.isEmpty()) {
            // Build service/component filter to declare what services and components are being added
            // so kerberosHelper.configureServices know which to work on.  Null indicates no filter
            // and all services and components will be (re)configured, however null will not be
            // passed in from here.
            Map<String, Collection<String>> serviceFilter = new HashMap<>();
  
            for (ServiceComponentHost scHost : componentsToConfigureForKerberos) {
              String serviceName = scHost.getServiceName();
              Collection<String> componentFilter = serviceFilter.get(serviceName);
  
              if (componentFilter == null) {
                componentFilter = new HashSet<>();
                serviceFilter.put(serviceName, componentFilter);
              }
  
              componentFilter.add(scHost.getServiceComponentName());
            }
  
            try {
              kerberosHelper.configureServices(cluster, serviceFilter);
            } catch (KerberosInvalidConfigurationException e) {
              throw new AmbariException(e.getMessage(), e);
            }
          }
        }
  
        for (String compName : changedScHosts.keySet()) {
          for (State newState : changedScHosts.get(compName).keySet()) {
            for (ServiceComponentHost scHost :
                changedScHosts.get(compName).get(newState)) {
  
              Service service = cluster.getService(scHost.getServiceName());
              ServiceComponent serviceComponent = service.getServiceComponent(compName);
  
              if (StringUtils.isBlank(stage.getHostParamsStage())) {
                RepositoryVersionEntity repositoryVersion = serviceComponent.getDesiredRepositoryVersion();
                stage.setHostParamsStage(StageUtils.getGson().toJson(
                    customCommandExecutionHelper.createDefaultHostParams(cluster, repositoryVersion.getStackId())));
              }
  
  
              // Do not create role command for hosts that are not responding
              if (scHost.getHostState().equals(HostState.HEARTBEAT_LOST)) {
                LOG.info("Command is not created for servicecomponenthost "
                    + ", clusterName=" + cluster.getClusterName()
                    + ", clusterId=" + cluster.getClusterId()
                    + ", serviceName=" + scHost.getServiceName()
                    + ", componentName=" + scHost.getServiceComponentName()
                    + ", hostname=" + scHost.getHostName()
                    + ", hostState=" + scHost.getHostState()
                    + ", targetNewState=" + newState);
                continue;
              }
  
              RoleCommand roleCommand;
              State oldSchState = scHost.getState();
              ServiceComponentHostEvent event;
  
              switch (newState) {
                case INSTALLED:
                  if (oldSchState == State.INIT
                      || oldSchState == State.UNINSTALLED
                      || oldSchState == State.INSTALLED
                      || oldSchState == State.INSTALLING
                      || oldSchState == State.UNKNOWN
                      || oldSchState == State.INSTALL_FAILED) {
                    roleCommand = RoleCommand.INSTALL;
  
                    if (scHost.isClientComponent() && oldSchState == State.INSTALLED) {
                      // Client reinstalls are executed to reattach changed configs on service.
                      // Do not transition a client component to INSTALLING state if it was installed.
                      // Prevents INSTALL_FAILED state if a command gets aborted.
                      event = new ServiceComponentHostOpInProgressEvent(
                          scHost.getServiceComponentName(), scHost.getHostName(),
                          nowTimestamp);
                    } else {
                      event = new ServiceComponentHostInstallEvent(
                          scHost.getServiceComponentName(), scHost.getHostName(),
                          nowTimestamp,
                          serviceComponent.getDesiredStackId().getStackId());
                    }
                  } else if (oldSchState == State.STARTED
                        // TODO: oldSchState == State.INSTALLED is always false, looks like a bug
                        //|| oldSchState == State.INSTALLED
                      || oldSchState == State.STOPPING) {
                    roleCommand = RoleCommand.STOP;
                    event = new ServiceComponentHostStopEvent(
                        scHost.getServiceComponentName(), scHost.getHostName(),
                        nowTimestamp);
                  } else if (oldSchState == State.UPGRADING) {
                    roleCommand = RoleCommand.UPGRADE;
                    event = new ServiceComponentHostUpgradeEvent(
                        scHost.getServiceComponentName(), scHost.getHostName(),
                        nowTimestamp, serviceComponent.getDesiredStackId().getStackId());
                  } else {
                    throw new AmbariException("Invalid transition for"
                        + " servicecomponenthost"
                        + ", clusterName=" + cluster.getClusterName()
                        + ", clusterId=" + cluster.getClusterId()
                        + ", serviceName=" + scHost.getServiceName()
                        + ", componentName=" + scHost.getServiceComponentName()
                        + ", hostname=" + scHost.getHostName()
                        + ", currentState=" + oldSchState
                        + ", newDesiredState=" + newState);
                  }
                  break;
                case STARTED:
                  StackId stackId = serviceComponent.getDesiredStackId();
                  ComponentInfo compInfo = ambariMetaInfo.getComponent(
                      stackId.getStackName(), stackId.getStackVersion(), scHost.getServiceName(),
                      scHost.getServiceComponentName());
  
                  if (oldSchState == State.INSTALLED ||
                      oldSchState == State.STARTING ||
                      //todo: after separating install and start, the install stage is no longer in request stage container
                      //todo: so projected state will not equal INSTALLED which causes an exception for invalid state transition
                      //todo: so for now disabling this check
                      //todo: this change breaks test AmbariManagementControllerTest.testServiceComponentHostUpdateRecursive()
                      true) {
  //                    requestStages.getProjectedState(scHost.getHostName(),
  //                        scHost.getServiceComponentName()) == State.INSTALLED) {
                    roleCommand = RoleCommand.START;
                    event = new ServiceComponentHostStartEvent(
                        scHost.getServiceComponentName(), scHost.getHostName(),
                        nowTimestamp);
                  } else {
                    String error = "Invalid transition for"
                        + " servicecomponenthost"
                        + ", clusterName=" + cluster.getClusterName()
                        + ", clusterId=" + cluster.getClusterId()
                        + ", serviceName=" + scHost.getServiceName()
                        + ", componentName=" + scHost.getServiceComponentName()
                        + ", hostname=" + scHost.getHostName()
                        + ", currentState=" + oldSchState
                        + ", newDesiredState=" + newState;
                    if (compInfo.isMaster()) {
                      throw new AmbariException(error);
                    } else {
                      LOG.info("Ignoring: " + error);
                      continue;
                    }
                  }
                  break;
                case UNINSTALLED:
                  if (oldSchState == State.INSTALLED
                      || oldSchState == State.UNINSTALLING) {
                    roleCommand = RoleCommand.UNINSTALL;
                    event = new ServiceComponentHostStartEvent(
                        scHost.getServiceComponentName(), scHost.getHostName(),
                        nowTimestamp);
                  } else {
                    throw new AmbariException("Invalid transition for"
                        + " servicecomponenthost"
                        + ", clusterName=" + cluster.getClusterName()
                        + ", clusterId=" + cluster.getClusterId()
                        + ", serviceName=" + scHost.getServiceName()
                        + ", componentName=" + scHost.getServiceComponentName()
                        + ", hostname=" + scHost.getHostName()
                        + ", currentState=" + oldSchState
                        + ", newDesiredState=" + newState);
                  }
                  break;
                case INIT:
                  if (oldSchState == State.INSTALLED ||
                      oldSchState == State.INSTALL_FAILED ||
                      oldSchState == State.INIT) {
                    scHost.setState(State.INIT);
                    continue;
                  } else  {
                    throw new AmbariException("Unsupported transition to INIT for"
                        + " servicecomponenthost"
                        + ", clusterName=" + cluster.getClusterName()
                        + ", clusterId=" + cluster.getClusterId()
                        + ", serviceName=" + scHost.getServiceName()
                        + ", componentName=" + scHost.getServiceComponentName()
                        + ", hostname=" + scHost.getHostName()
                        + ", currentState=" + oldSchState
                        + ", newDesiredState=" + newState);
                  }
                default:
                  throw new AmbariException("Unsupported state change operation"
                      + ", newState=" + newState);
              }
  
              if (LOG.isDebugEnabled()) {
                LOG.debug("Create a new host action, requestId={}, componentName={}, hostname={}, roleCommand={}",
                  requestStages.getId(), scHost.getServiceComponentName(), scHost.getHostName(), roleCommand.name());
              }
  
              // any targeted information
              String keyName = scHost.getServiceComponentName().toLowerCase();
              if (requestProperties.containsKey(keyName)) {
                // in the case where the command is targeted, but the states
                // of the old and new are the same, the targeted component
                // may still need to get the command.  This is true for Flume.
                if (oldSchState == newState) {
                  switch (oldSchState) {
                    case INSTALLED:
                      roleCommand = RoleCommand.STOP;
                      event = new ServiceComponentHostStopEvent(
                          scHost.getServiceComponentName(), scHost.getHostName(),
                          nowTimestamp);
                      break;
                    case STARTED:
                      roleCommand = RoleCommand.START;
                      event = new ServiceComponentHostStartEvent(
                          scHost.getServiceComponentName(), scHost.getHostName(),
                          nowTimestamp);
                      break;
                    default:
                      break;
                  }
                }
  
                if (null == requestParameters) {
                  requestParameters = new HashMap<>();
                }
                requestParameters.put(keyName, requestProperties.get(keyName));
              }
  
              if (requestProperties.containsKey(CLUSTER_PHASE_PROPERTY)) {
                if (null == requestParameters) {
                  requestParameters = new HashMap<>();
                }
                requestParameters.put(CLUSTER_PHASE_PROPERTY, requestProperties.get(CLUSTER_PHASE_PROPERTY));
  
              }
  
              Map<String, DesiredConfig> clusterDesiredConfigs = cluster.getDesiredConfigs();
  
              // Skip INSTALL task in case SysPrepped hosts and in case of server components. In case of server component
              // START task should run configuration script.
              if (newState == State.INSTALLED && skipInstallTaskForComponent(requestProperties, cluster, scHost)) {
                LOG.info("Skipping create of INSTALL task for {} on {}.", scHost.getServiceComponentName(), scHost.getHostName());
                // set state to INSTALLING, then immediately send an ServiceComponentHostOpSucceededEvent to allow
                // transitioning from INSTALLING --> INSTALLED.
                scHost.setState(State.INSTALLING);
                long now = System.currentTimeMillis();
                try {
                  scHost.handleEvent(new ServiceComponentHostOpSucceededEvent(scHost.getServiceComponentName(), scHost.getHostName(), now));
                } catch (InvalidStateTransitionException e) {
                  LOG.error("Error transitioning ServiceComponentHost state to INSTALLED", e);
                }
              } else {
                // !!! can never be null
                RepositoryVersionEntity repoVersion = serviceComponent.getDesiredRepositoryVersion();
  
                createHostAction(cluster, stage, scHost,
                  roleCommand, requestParameters, event, skipFailure, repoVersion, isUpgradeSuspended,
                  databaseType, clusterDesiredConfigs, useLatestConfigs);
              }
  
            }
          }
        }
  
        for (String serviceName : smokeTestServices) { // Creates smoke test commands
          Service s = cluster.getService(serviceName);
          // find service component host
          ServiceComponent component = getClientComponentForRunningAction(cluster, s);
          String componentName = component != null ? component.getName() : null;
          String clientHost = getClientHostForRunningAction(cluster, s, component);
          String smokeTestRole = actionMetadata.getServiceCheckAction(serviceName);
  
          if (clientHost == null || smokeTestRole == null) {
            LOG.info("Nothing to do for service check as could not find role or"
                + " or host to run check on"
                + ", clusterName=" + cluster.getClusterName()
                + ", serviceName=" + serviceName
                + ", clientHost=" + clientHost
                + ", serviceCheckRole=" + smokeTestRole);
            continue;
          }
  
          if (StringUtils.isBlank(stage.getHostParamsStage())) {
            RepositoryVersionEntity repositoryVersion = component.getDesiredRepositoryVersion();
            stage.setHostParamsStage(StageUtils.getGson().toJson(
                customCommandExecutionHelper.createDefaultHostParams(cluster, repositoryVersion.getStackId())));
          }
  
          customCommandExecutionHelper.addServiceCheckAction(stage, clientHost, smokeTestRole,
              nowTimestamp, serviceName, componentName, null, false, false, useLatestConfigs);
        }
  
        RoleCommandOrder rco = getRoleCommandOrder(cluster);
        RoleGraph rg = roleGraphFactory.createNew(rco);
  
  
        if (CommandExecutionType.DEPENDENCY_ORDERED == configs.getStageExecutionType() &&
          CLUSTER_PHASE_INITIAL_START.equals(requestProperties.get(CLUSTER_PHASE_PROPERTY))
        ) {
          LOG.info("Set DEPENDENCY_ORDERED CommandExecutionType on stage: {}", stage.getRequestContext());
          rg.setCommandExecutionType(CommandExecutionType.DEPENDENCY_ORDERED);
        }
        rg.build(stage);
        if (useClusterHostInfo) {
          requestStages.setClusterHostInfo(clusterHostInfoJson);
        }
  
        requestStages.addStages(rg.getStages());
  
        if (!componentsToEnableKerberos.isEmpty()) {
          Map<String, Collection<String>> serviceFilter = new HashMap<>();
          Set<String> hostFilter = new HashSet<>();
  
          for (ServiceComponentHost scHost : componentsToEnableKerberos) {
            String serviceName = scHost.getServiceName();
            Collection<String> componentFilter = serviceFilter.get(serviceName);
  
            if (componentFilter == null) {
              componentFilter = new HashSet<>();
              serviceFilter.put(serviceName, componentFilter);
            }
  
            componentFilter.add(scHost.getServiceComponentName());
            hostFilter.add(scHost.getHostName());
          }
  
          try {
            kerberosHelper.ensureIdentities(cluster, serviceFilter, hostFilter, null, hostsToForceKerberosOperations, requestStages,
                kerberosHelper.getManageIdentitiesDirective(requestProperties));
          } catch (KerberosOperationException e) {
            throw new IllegalArgumentException(e.getMessage(), e);
          }
        }
  
        List<Stage> stages = requestStages.getStages();
        LOG.debug("Created {} stages", ((stages != null) ? stages.size() : 0));
  
      } else {
        LOG.debug("Created 0 stages");
      }
  
      return requestStages;
    }
  
    private boolean hostComponentAlreadyExists(Cluster cluster, ServiceComponentHost sch) throws AmbariException {
      Service service = cluster.getService(sch.getServiceName());
      if (service != null) {
        ServiceComponent serviceComponent = service.getServiceComponent(sch.getServiceComponentName());
        if (serviceComponent != null) {
          Map<String, ServiceComponentHost> serviceComponentHostMap = serviceComponent.getServiceComponentHosts();
          for (ServiceComponentHost serviceComponentHost : serviceComponentHostMap.values()) {
            if (serviceComponentHost.getState() == State.INSTALLED || serviceComponentHost.getState() == State.STARTED) {
              return true;
            }
          }
        }
      }
      return false;
    }
  
  
    private boolean skipInstallTaskForComponent(Map<String, String> requestProperties, Cluster cluster,
                                                ServiceComponentHost sch) throws AmbariException {
      boolean isClientComponent = false;
      Service service = cluster.getService(sch.getServiceName());
      String componentName = sch.getServiceComponentName();
      if (service != null) {
        ServiceComponent serviceComponent = service.getServiceComponent(componentName);
        if (serviceComponent != null) {
          isClientComponent = serviceComponent.isClientComponent();
        }
      }
      return HostComponentResourceProvider.shouldSkipInstallTaskForComponent(componentName, isClientComponent, requestProperties);
  
    }
  
    @Override
    public ExecutionCommand getExecutionCommand(Cluster cluster,
                                                ServiceComponentHost scHost,
                                                RoleCommand roleCommand) throws AmbariException {
      Map<String, String> hostParamsCmd = customCommandExecutionHelper.createDefaultHostParams(
          cluster, scHost.getServiceComponent().getDesiredStackId());
  
      Stage stage = createNewStage(0, cluster, 1, "", "{}", "");
  
      RepositoryVersionEntity repoVersion = null;
      if (null != scHost.getServiceComponent().getDesiredRepositoryVersion()) {
        repoVersion = scHost.getServiceComponent().getDesiredRepositoryVersion();
      } else {
        Service service = cluster.getService(scHost.getServiceName());
        repoVersion = service.getDesiredRepositoryVersion();
      }
  
      boolean isUpgradeSuspended = cluster.isUpgradeSuspended();
      DatabaseType databaseType = configs.getDatabaseType();
      Map<String, DesiredConfig> clusterDesiredConfigs = cluster.getDesiredConfigs();
      createHostAction(cluster, stage, scHost,
                       roleCommand, null, null, false, repoVersion, isUpgradeSuspended, databaseType,
                       clusterDesiredConfigs, false);
      ExecutionCommand ec = stage.getExecutionCommands().get(scHost.getHostName()).get(0).getExecutionCommand();
  
      // createHostAction does not take a hostLevelParams but creates one
      hostParamsCmd.putAll(ec.getHostLevelParams());
      ec.getHostLevelParams().putAll(hostParamsCmd);
  
      // Generate localComponents
      for (ServiceComponentHost sch : cluster.getServiceComponentHosts(scHost.getHostName())) {
        ec.getLocalComponents().add(sch.getServiceComponentName());
      }
  
      return ec;
    }
  
    @Override
    public Set<StackConfigurationDependencyResponse> getStackConfigurationDependencies(
            Set<StackConfigurationDependencyRequest> requests) throws AmbariException {
      Set<StackConfigurationDependencyResponse> response
              = new HashSet<>();
      if (requests != null) {
        for (StackConfigurationDependencyRequest request : requests) {
  
          String stackName = request.getStackName();
          String stackVersion = request.getStackVersion();
          String serviceName = request.getServiceName();
          String propertyName = request.getPropertyName();
  
          Set<StackConfigurationDependencyResponse> stackConfigurations
                  = getStackConfigurationDependencies(request);
  
          for (StackConfigurationDependencyResponse dependencyResponse : stackConfigurations) {
            dependencyResponse.setStackName(stackName);
            dependencyResponse.setStackVersion(stackVersion);
            dependencyResponse.setServiceName(serviceName);
            dependencyResponse.setPropertyName(propertyName);
          }
          response.addAll(stackConfigurations);
        }
      }
      return response;
    }
  
    private Set<StackConfigurationDependencyResponse> getStackConfigurationDependencies(StackConfigurationDependencyRequest request) throws AmbariException {
      Set<StackConfigurationDependencyResponse> response =
        new HashSet<>();
  
      String stackName = request.getStackName();
      String stackVersion = request.getStackVersion();
      String serviceName = request.getServiceName();
      String propertyName = request.getPropertyName();
      String dependencyName = request.getDependencyName();
  
      Set<PropertyInfo> properties = ambariMetaInfo.getPropertiesByName(stackName, stackVersion, serviceName, propertyName);
  
      for (PropertyInfo property: properties) {
        for (PropertyDependencyInfo dependency: property.getDependedByProperties()) {
          if (dependencyName == null || dependency.getName().equals(dependencyName)) {
            response.add(dependency.convertToResponse());
          }
        }
      }
  
      return response;  }
  
    @Transactional
    void updateServiceStates(
        Cluster cluster,
        Map<State, List<Service>> changedServices,
        Map<State, List<ServiceComponent>> changedComps,
        Map<String, Map<State, List<ServiceComponentHost>>> changedScHosts,
        Collection<ServiceComponentHost> ignoredScHosts
    ) {
      if (changedServices != null) {
        for (Entry<State, List<Service>> entry : changedServices.entrySet()) {
          State newState = entry.getKey();
          for (Service s : entry.getValue()) {
            if (s.isClientOnlyService()
                && newState == State.STARTED) {
              continue;
            }
            s.setDesiredState(newState);
          }
        }
      }
  
      if (changedComps != null) {
        for (Entry<State, List<ServiceComponent>> entry :
            changedComps.entrySet()) {
          State newState = entry.getKey();
          for (ServiceComponent sc : entry.getValue()) {
            sc.setDesiredState(newState);
          }
        }
      }
  
      //keep 2 maps for simpler maintenance
      Map<String, String> serviceMasterForDecommissionMap = new HashMap<>();
      Map<String, Set<String>> masterSlaveHostsMap = new HashMap<>();
      for (Map<State, List<ServiceComponentHost>> stateScHostMap :
          changedScHosts.values()) {
        for (Entry<State, List<ServiceComponentHost>> entry :
            stateScHostMap.entrySet()) {
          State newState = entry.getKey();
          for (ServiceComponentHost sch : entry.getValue()) {
            String componentName = sch.getServiceComponentName();
            //Create map for include/exclude files refresh
            if (masterToSlaveMappingForDecom.containsValue(componentName) &&
              sch.getState() == State.INIT && newState == State.INSTALLED) {
              String serviceName = sch.getServiceName();
              String masterComponentName = null;
              for (Entry<String, String> entrySet : masterToSlaveMappingForDecom.entrySet()) {
                if (entrySet.getValue().equals(componentName)) {
                  masterComponentName = entrySet.getKey();
                }
              }
              try {
                //Filter services whose masters are not started
                if (isServiceComponentStartedOnAnyHost(cluster, serviceName, masterComponentName)) {
                  serviceMasterForDecommissionMap.put(serviceName, masterComponentName);
                  masterSlaveHostsMap.putIfAbsent(masterComponentName, new HashSet<>());
                  masterSlaveHostsMap.get(masterComponentName).add(sch.getHostName());
                } else {
                  LOG.info(String.format("Not adding %s service from include/exclude files refresh map because it's master is not started", serviceName));
                }
              } catch (AmbariException e) {
                LOG.error("Exception during INIT masters cleanup : ", e);
              }
            }
  
            //actually set the new state
            sch.setDesiredState(newState);
          }
        }
      }
  
      try {
        createAndExecuteRefreshIncludeExcludeFilesActionForMasters(serviceMasterForDecommissionMap, masterSlaveHostsMap, cluster.getClusterName(), false);
      } catch (AmbariException e) {
        LOG.error("Exception during refresh include exclude files action : ", e);
      }
  
      if (ignoredScHosts != null) {
        for (ServiceComponentHost scHost : ignoredScHosts) {
          scHost.setDesiredState(scHost.getState());
        }
      }
    }
  
    private boolean isServiceComponentStartedOnAnyHost(Cluster cluster, String serviceName, String masterComponentName) throws AmbariException {
      Service service = cluster.getService(serviceName);
      ServiceComponent serviceComponent = service.getServiceComponent(masterComponentName);
      Map<String, ServiceComponentHost> schMap = serviceComponent.getServiceComponentHosts();
      for (ServiceComponentHost sch : schMap.values()) {
         if (sch.getState() == State.STARTED) {
           return true;
         }
      }
      return false;
    }
  
    @Override
    public RequestStatusResponse createAndPersistStages(Cluster cluster, Map<String, String> requestProperties,
                                                        Map<String, String> requestParameters,
                                                        Map<State, List<Service>> changedServices,
                                                        Map<State, List<ServiceComponent>> changedComponents,
                                                        Map<String, Map<State, List<ServiceComponentHost>>> changedHosts,
                                                        Collection<ServiceComponentHost> ignoredHosts,
                                                        boolean runSmokeTest, boolean reconfigureClients) throws AmbariException {
  
      RequestStageContainer request = addStages(null, cluster, requestProperties, requestParameters, changedServices,
        changedComponents, changedHosts, ignoredHosts, runSmokeTest, reconfigureClients, false);
  
      request.persist();
      return request.getRequestStatusResponse();
    }
  
    @Override
    public RequestStageContainer addStages(RequestStageContainer requestStages, Cluster cluster, Map<String, String> requestProperties,
                                   Map<String, String> requestParameters, Map<State, List<Service>> changedServices,
                                   Map<State, List<ServiceComponent>> changedComponents,
                                   Map<String, Map<State, List<ServiceComponentHost>>> changedHosts,
                                   Collection<ServiceComponentHost> ignoredHosts, boolean runSmokeTest,
                                   boolean reconfigureClients, boolean useGeneratedConfigs) throws AmbariException {
  
      return addStages(requestStages, cluster, requestProperties, requestParameters, changedServices, changedComponents,
          changedHosts, ignoredHosts, runSmokeTest, reconfigureClients, useGeneratedConfigs, false);
    }
  
    @Override
    public RequestStageContainer addStages(RequestStageContainer requestStages, Cluster cluster, Map<String, String> requestProperties,
                                   Map<String, String> requestParameters, Map<State, List<Service>> changedServices,
                                   Map<State, List<ServiceComponent>> changedComponents,
                                   Map<String, Map<State, List<ServiceComponentHost>>> changedHosts,
                                   Collection<ServiceComponentHost> ignoredHosts, boolean runSmokeTest,
                                   boolean reconfigureClients, boolean useGeneratedConfigs, boolean useClusterHostInfo) throws AmbariException {
  
      if (requestStages == null) {
        requestStages = new RequestStageContainer(actionManager.getNextRequestId(), null, requestFactory, actionManager);
      }
  
      requestStages = doStageCreation(requestStages, cluster, changedServices, changedComponents,
          changedHosts, requestParameters, requestProperties,
          runSmokeTest, reconfigureClients, useGeneratedConfigs, useClusterHostInfo);
  
      updateServiceStates(cluster, changedServices, changedComponents, changedHosts, ignoredHosts);
  
      return requestStages;
    }
  
    //todo: for now made this public since is is still used by createHostComponents
    //todo: delete after all host component logic is in HostComponentResourceProvider
    public void validateServiceComponentHostRequest(ServiceComponentHostRequest request) {
      if (request.getClusterName() == null
          || request.getClusterName().isEmpty()
          || request.getComponentName() == null
          || request.getComponentName().isEmpty()
          || request.getHostname() == null
          || request.getHostname().isEmpty()) {
        throw new IllegalArgumentException("Invalid arguments"
            + ", cluster name, component name and host name should be"
            + " provided");
      }
  
      if (request.getAdminState() != null) {
        throw new IllegalArgumentException("Property adminState cannot be modified through update. Use service " +
            "specific DECOMMISSION action to decommision/recommission components.");
      }
    }
  
    private void checkIfHostComponentsInDeleteFriendlyState(ServiceComponentHostRequest request, Cluster cluster) throws AmbariException {
      Service service = cluster.getService(request.getServiceName());
      ServiceComponent component = service.getServiceComponent(request.getComponentName());
      ServiceComponentHost componentHost = component.getServiceComponentHost(request.getHostname());
  
      if (!componentHost.canBeRemoved()) {
        throw new AmbariException("Current host component state prohibiting component removal."
                + ", clusterName=" + request.getClusterName()
                + ", serviceName=" + request.getServiceName()
                + ", componentName=" + request.getComponentName()
                + ", hostname=" + request.getHostname()
                + ", request=" + request
                + ", state=" + componentHost.getState());
      }
    }
  
    @Override
    public String findServiceName(Cluster cluster, String componentName) throws AmbariException {
      return cluster.getServiceByComponentName(componentName).getName();
    }
  
    @Override
    public synchronized void deleteCluster(ClusterRequest request)
        throws AmbariException {
  
      if (request.getClusterName() == null
          || request.getClusterName().isEmpty()) {
        // FIXME throw correct error
        throw new AmbariException("Invalid arguments");
      }
      LOG.info("Received a delete cluster request, clusterName = " + request.getClusterName());
      if (request.getHostNames() != null) {
        // FIXME treat this as removing a host from a cluster?
      } else {
        // deleting whole cluster
        clusters.deleteCluster(request.getClusterName());
      }
    }
  
    @Override
    public DeleteStatusMetaData deleteHostComponents(
        Set<ServiceComponentHostRequest> requests) throws AmbariException, AuthorizationException {
  
      Set<ServiceComponentHostRequest> expanded = new HashSet<>();
  
      // if any request are for the whole host, they need to be expanded
      for (ServiceComponentHostRequest request : requests) {
        if (null == request.getComponentName()) {
          if (null == request.getClusterName() || request.getClusterName().isEmpty() ||
              null == request.getHostname() || request.getHostname().isEmpty()) {
            throw new IllegalArgumentException("Cluster name and hostname must be specified.");
          }
          Cluster cluster = clusters.getCluster(request.getClusterName());
  
          if (!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(),
              EnumSet.of(RoleAuthorization.SERVICE_ADD_DELETE_SERVICES,RoleAuthorization.HOST_ADD_DELETE_COMPONENTS))) {
            throw new AuthorizationException("The authenticated user is not authorized to delete service components from hosts");
          }
  
          for (ServiceComponentHost sch : cluster.getServiceComponentHosts(request.getHostname())) {
            ServiceComponentHostRequest schr = new ServiceComponentHostRequest(request.getClusterName(),
                sch.getServiceName(), sch.getServiceComponentName(), sch.getHostName(), null);
            expanded.add(schr);
          }
        }
        else {
          expanded.add(request);
        }
      }
  
      Map<ServiceComponent, Set<ServiceComponentHost>> safeToRemoveSCHs = new HashMap<>();
      DeleteHostComponentStatusMetaData deleteMetaData = new DeleteHostComponentStatusMetaData();
  
      for (ServiceComponentHostRequest request : expanded) {
  
        validateServiceComponentHostRequest(request);
  
        Cluster cluster = clusters.getCluster(request.getClusterName());
  
        if (StringUtils.isEmpty(request.getServiceName())) {
          request.setServiceName(findServiceName(cluster, request.getComponentName()));
        }
  
        LOG.info("Received a hostComponent DELETE request"
          + ", clusterName=" + request.getClusterName()
          + ", serviceName=" + request.getServiceName()
          + ", componentName=" + request.getComponentName()
          + ", hostname=" + request.getHostname()
          + ", request=" + request);
  
        Service service = cluster.getService(request.getServiceName());
        ServiceComponent component = service.getServiceComponent(request.getComponentName());
        ServiceComponentHost componentHost = component.getServiceComponentHost(request.getHostname());
  
        setRestartRequiredServices(service, request.getComponentName());
        try {
          checkIfHostComponentsInDeleteFriendlyState(request, cluster);
          if (!safeToRemoveSCHs.containsKey(component)) {
            safeToRemoveSCHs.put(component, new HashSet<>());
          }
          safeToRemoveSCHs.get(component).add(componentHost);
        } catch (Exception ex) {
          deleteMetaData.addException(request.getHostname() + "/" + request.getComponentName(), ex);
        }
      }
  
      //keep 2 maps for simpler maintenance
      Map<String, Map<String, String>> clusterServiceMasterForDecommissionMap = new HashMap<>();
      Map<String, Map<String, Set<String>>> clusterMasterSlaveHostsMap = new HashMap<>();
      for (Entry<ServiceComponent, Set<ServiceComponentHost>> entry : safeToRemoveSCHs.entrySet()) {
        for (ServiceComponentHost componentHost : entry.getValue()) {
          try {
            //actually delete the component
            //TODO update metadata processing according to AMBARI-21587
            entry.getKey().deleteServiceComponentHosts(componentHost.getHostName(), deleteMetaData);
  
            //create cluster-master-service map to update all include/exclude files in one action
            String componentName = componentHost.getServiceComponentName();
            if (masterToSlaveMappingForDecom.containsValue(componentName)) {
              String masterComponentName = null;
              for (Entry<String, String> entrySet : masterToSlaveMappingForDecom.entrySet()) {
                if (entrySet.getValue().equals(componentName)) {
                  masterComponentName = entrySet.getKey();
                }
              }
              if (clusterServiceMasterForDecommissionMap.containsKey(componentHost.getClusterName())) {
                clusterServiceMasterForDecommissionMap.get(componentHost.getClusterName()).put(componentHost.getServiceName(), masterComponentName);
                Map<String, Set<String>> masterSlaveMap  = clusterMasterSlaveHostsMap.get(componentHost.getClusterName());
                masterSlaveMap.putIfAbsent(masterComponentName, new HashSet<>());
                masterSlaveMap.get(masterComponentName).add(componentHost.getHostName());
              } else {
                Map<String, String> serviceMasterMap = new HashMap<>();
                serviceMasterMap.put(componentHost.getServiceName(), masterComponentName);
                clusterServiceMasterForDecommissionMap.put(componentHost.getClusterName(), serviceMasterMap);
  
                Map<String, Set<String>> masterSlaveHostsMap = new HashMap<>();
                masterSlaveHostsMap.put(masterComponentName, new HashSet<>(Collections.singletonList(componentHost.getHostName())));
                clusterMasterSlaveHostsMap.put(componentHost.getClusterName(), masterSlaveHostsMap);
              }
            }
          } catch (Exception ex) {
            deleteMetaData.addException(componentHost.getHostName() + "/" + componentHost.getServiceComponentName(), ex);
          }
        }
      }
  
      for (String cluster : clusterServiceMasterForDecommissionMap.keySet()) {
        createAndExecuteRefreshIncludeExcludeFilesActionForMasters(clusterServiceMasterForDecommissionMap.get(cluster), clusterMasterSlaveHostsMap.get(cluster), cluster, true);
      }
  
      //Do not break behavior for existing clients where delete request contains only 1 host component.
      //Response for these requests will have empty body with appropriate error code.
      if (deleteMetaData.getDeletedKeys().size() + deleteMetaData.getExceptionForKeys().size() == 1) {
        if (deleteMetaData.getDeletedKeys().size() == 1) {
          STOMPComponentsDeleteHandler.processDeleteByMetaData(deleteMetaData);
          return null;
        }
        Exception ex = deleteMetaData.getExceptionForKeys().values().iterator().next();
        if (ex instanceof AmbariException) {
          throw (AmbariException)ex;
        } else {
          throw new AmbariException(ex.getMessage(), ex);
        }
      }
  
      // set restartRequired flag for  monitoring services
      if (!safeToRemoveSCHs.isEmpty()) {
        setMonitoringServicesRestartRequired(requests);
      }
      STOMPComponentsDeleteHandler.processDeleteByMetaData(deleteMetaData);
      return deleteMetaData;
    }
  
    @Override
    public void deleteGroups(Set<GroupRequest> requests) throws AmbariException {
      for (GroupRequest request: requests) {
        LOG.debug("Received a delete group request, groupname={}", request.getGroupName());
        final Group group = users.getGroup(request.getGroupName());
        if (group != null) {
          users.removeGroup(group);
        }
      }
    }
  
    /**
     * Creates and triggers an action to update include and exclude files for the master components depending on current cluster topology and components state
     * @param serviceMasterMap
     * @param masterSlaveHostsMap
     *@param clusterName  @throws AmbariException
     */
    private void createAndExecuteRefreshIncludeExcludeFilesActionForMasters(Map<String, String> serviceMasterMap, Map<String, Set<String>> masterSlaveHostsMap, String clusterName, boolean isDecommission) throws AmbariException {
      //Clear include/exclude files or draining list except HBASE
      serviceMasterMap.remove(Service.Type.HBASE.toString());
      //exit if empty
      if (serviceMasterMap.isEmpty()) {
        return;
      }
      LOG.debug("Refresh include/exclude files action will be executed for " + serviceMasterMap);
      HashMap<String, String> requestProperties = new HashMap<>();
      requestProperties.put(RequestResourceProvider.CONTEXT, "Update Include/Exclude Files for " + serviceMasterMap.keySet().toString());
      HashMap<String, String> params = new HashMap<>();
      params.put(AmbariCustomCommandExecutionHelper.UPDATE_FILES_ONLY, String.valueOf(isDecommission));
  
      for (String masterName : masterSlaveHostsMap.keySet()) {
        if (!isDecommission) {
          params.put(masterName + "_" + AmbariCustomCommandExecutionHelper.DECOM_INCLUDED_HOSTS, StringUtils.join(masterSlaveHostsMap.get(masterName).toArray(), ","));
        }
      }
  
      params.put(AmbariCustomCommandExecutionHelper.IS_ADD_OR_DELETE_SLAVE_REQUEST, "true");
  
      //Create filter for command
      List<RequestResourceFilter> resourceFilters = new ArrayList<>(serviceMasterMap.size());
      for (String serviceName : serviceMasterMap.keySet()) {
        resourceFilters.add(new RequestResourceFilter(serviceName, serviceMasterMap.get(serviceName), null));
      }
  
      //Create request for command
      ExecuteActionRequest actionRequest = new ExecuteActionRequest(
        clusterName, AmbariCustomCommandExecutionHelper.DECOMMISSION_COMMAND_NAME, null,
        resourceFilters, null, params, false);
      //Send action
      createAction(actionRequest, requestProperties);
    }
  
    @Override
    public void deleteMembers(java.util.Set<MemberRequest> requests) throws AmbariException {
      for (MemberRequest request : requests) {
        LOG.debug("Received a delete member request, {}", request);
        users.removeMemberFromGroup(request.getGroupName(), request.getUserName());
      }
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public void removeMpack(MpackEntity mpackEntity, StackEntity stackEntity) throws IOException{
  
      ambariMetaInfo.removeMpack(mpackEntity, stackEntity);
    }
  
    @Override
    public Set<ServiceConfigVersionResponse> createServiceConfigVersion(Set<ServiceConfigVersionRequest> requests) throws AmbariException, AuthorizationException {
      return null;
    }
  
    /**
     * Get a request response for the given request ids.  Note that this method
     * fully populates a request resource including the set of task sub-resources
     * in the request response.
     */
    RequestStatusResponse getRequestStatusResponse(long requestId) {
      RequestStatusResponse response = new RequestStatusResponse(requestId);
      List<HostRoleCommand> hostRoleCommands =
          actionManager.getRequestTasks(requestId);
  
      response.setRequestContext(actionManager.getRequestContext(requestId));
      List<ShortTaskStatus> tasks = new ArrayList<>();
  
      for (HostRoleCommand hostRoleCommand : hostRoleCommands) {
        tasks.add(new ShortTaskStatus(hostRoleCommand));
      }
      response.setTasks(tasks);
  
      return response;
    }
  
    @Override
    public Set<ClusterResponse> getClusters(Set<ClusterRequest> requests) throws AmbariException, AuthorizationException {
      Set<ClusterResponse> response = new HashSet<>();
      for (ClusterRequest request : requests) {
        try {
          response.addAll(getClusters(request));
        } catch (ClusterNotFoundException e) {
          if (requests.size() == 1) {
            // only throw exception if 1 request.
            // there will be > 1 request in case of OR predicate
            throw e;
          }
        }
      }
      return response;
    }
  
    @Override
    public Set<ServiceComponentHostResponse> getHostComponents(
        Set<ServiceComponentHostRequest> requests) throws AmbariException {
      return getHostComponents(requests, false);
    }
  
    @Override
    public Set<ServiceComponentHostResponse> getHostComponents(
        Set<ServiceComponentHostRequest> requests, boolean statusOnly) throws AmbariException {
      LOG.debug("Processing requests: {}", requests);
      Set<ServiceComponentHostResponse> response =
          new HashSet<>();
      for (ServiceComponentHostRequest request : requests) {
        try {
          response.addAll(getHostComponents(request, statusOnly));
        } catch (ServiceComponentHostNotFoundException | ServiceComponentNotFoundException | ServiceNotFoundException e) {
          if (requests.size() == 1) {
            // only throw exception if 1 request.
            // there will be > 1 request in case of OR predicate
            throw e;
          } else {
            LOG.debug("Ignoring not found exception due to other requests", e);
          }
        } catch (ParentObjectNotFoundException e) {
          // If there is only one request, always throw exception.
          // There will be > 1 request in case of OR predicate.
  
          // For HostNotFoundException, only throw exception if host_name is
          // provided in URL.  If host_name is part of query, don't throw exception.
          boolean throwException = true;
          if (requests.size() > 1 && HostNotFoundException.class.isInstance(e.getCause())) {
            for (ServiceComponentHostRequest r : requests) {
              if (r.getHostname() == null) {
                // host_name provided in query since all requests don't have host_name set
                throwException = false;
                LOG.debug("HostNotFoundException ignored", e);
                break;
              }
            }
          }
          if (throwException) {
            throw e;
          }
        }
      }
      return response;
    }
  
    @Override
    public Set<ConfigurationResponse> getConfigurations(
        Set<ConfigurationRequest> requests) throws AmbariException {
      Set<ConfigurationResponse> response =
          new HashSet<>();
      for (ConfigurationRequest request : requests) {
        response.addAll(getConfigurations(request));
      }
      return response;
    }
  
    @Override
    public Set<ServiceConfigVersionResponse> getServiceConfigVersions(Set<ServiceConfigVersionRequest> requests)
        throws AmbariException {
      Set<ServiceConfigVersionResponse> responses = new LinkedHashSet<>();
  
      for (ServiceConfigVersionRequest request : requests) {
        responses.addAll(getServiceConfigVersions(request));
      }
  
      return responses;
    }
  
    private Set<ServiceConfigVersionResponse> getServiceConfigVersions(ServiceConfigVersionRequest request)
        throws AmbariException {
      if (request.getClusterName() == null) {
        throw new IllegalArgumentException("Invalid arguments, cluster name"
            + " should not be null");
      }
  
      Cluster cluster = clusters.getCluster(request.getClusterName());
  
      Set<ServiceConfigVersionResponse> result = new LinkedHashSet<>();
      String serviceName = request.getServiceName();
      List<ServiceConfigVersionResponse> serviceConfigVersionResponses =  new ArrayList<>();
  
      if (Boolean.TRUE.equals(request.getIsCurrent()) && serviceName != null) {
        serviceConfigVersionResponses.addAll(cluster.getActiveServiceConfigVersionResponse(serviceName));
      } else {
        serviceConfigVersionResponses.addAll(cluster.getServiceConfigVersions());
      }
  
      for (ServiceConfigVersionResponse response : serviceConfigVersionResponses) {
        if (serviceName != null && !StringUtils.equals(serviceName, response.getServiceName())) {
          continue;
        }
        if (request.getVersion() != null && NumberUtils.compare(request.getVersion(), response.getVersion()) != 0) {
          continue;
        }
        if (request.getUserName() != null && !StringUtils.equals(request.getUserName(), response.getUserName())) {
          continue;
        }
        result.add(response);
      }
  
      return result;
    }
  
    @Override
    public Set<GroupResponse> getGroups(Set<GroupRequest> requests)
        throws AmbariException {
      final Set<GroupResponse> responses = new HashSet<>();
      for (GroupRequest request: requests) {
        LOG.debug("Received a getGroups request, groupRequest={}", request);
        // get them all
        if (null == request.getGroupName()) {
          for (Group group: users.getAllGroups()) {
            final GroupResponse response = new GroupResponse(group.getGroupName(), group.isLdapGroup(), group.getGroupType());
            responses.add(response);
          }
        } else {
          final Group group = users.getGroup(request.getGroupName());
          if (null == group) {
            if (requests.size() == 1) {
              // only throw exception if there is a single request
              // if there are multiple requests, this indicates an OR predicate
              throw new ObjectNotFoundException("Cannot find group '"
                  + request.getGroupName() + "'");
            }
          } else {
            final GroupResponse response = new GroupResponse(group.getGroupName(), group.isLdapGroup(), group.getGroupType());
            responses.add(response);
          }
        }
      }
      return responses;
    }
  
    @Override
    public void updateGroups(Set<GroupRequest> requests) throws AmbariException {
      // currently no group updates are supported
    }
  
    protected String getClientHostForRunningAction(Cluster cluster, Service service, ServiceComponent serviceComponent)
        throws AmbariException {
      if (serviceComponent != null && !serviceComponent.getServiceComponentHosts().isEmpty()) {
        Set<String> candidateHosts = serviceComponent.getServiceComponentHosts().keySet();
        filterHostsForAction(candidateHosts, service, cluster, Resource.Type.Cluster);
        return getHealthyHost(candidateHosts);
      }
      return null;
    }
  
    protected ServiceComponent getClientComponentForRunningAction(Cluster cluster,
        Service service) throws AmbariException {
      /*
       * We assume Cluster level here. That means that we never run service
       * checks on clients/hosts that are in maintenance state.
       * That also means that we can not run service check if the only host
       * that has client component is in maintenance state
       */
  
      StackId stackId = service.getDesiredStackId();
      ComponentInfo compInfo =
          ambariMetaInfo.getService(stackId.getStackName(),
              stackId.getStackVersion(), service.getName()).getClientComponent();
      if (compInfo != null) {
        try {
          ServiceComponent serviceComponent =
              service.getServiceComponent(compInfo.getName());
          if (!serviceComponent.getServiceComponentHosts().isEmpty()) {
            return serviceComponent;
          }
        } catch (ServiceComponentNotFoundException e) {
          LOG.warn("Could not find required component to run action"
              + ", clusterName=" + cluster.getClusterName()
              + ", serviceName=" + service.getName()
              + ", componentName=" + compInfo.getName());
        }
      }
  
      // any component will do
      Map<String, ServiceComponent> components = service.getServiceComponents();
      if (!components.isEmpty()) {
        for (ServiceComponent serviceComponent : components.values()) {
          if (!serviceComponent.getServiceComponentHosts().isEmpty()) {
            return serviceComponent;
          }
        }
      }
      return null;
    }
  
    /**
     * Utility method that filters out hosts from set based on their maintenance
     * state status.
     */
    protected void filterHostsForAction(Set<String> candidateHosts, Service service,
                                      final Cluster cluster,
                                      final Resource.Type level)
                                      throws AmbariException {
      Set<String> ignoredHosts = maintenanceStateHelper.filterHostsInMaintenanceState(
        candidateHosts, new MaintenanceStateHelper.HostPredicate() {
          @Override
          public boolean shouldHostBeRemoved(final String hostname)
            throws AmbariException {
            Host host = clusters.getHost(hostname);
            return !maintenanceStateHelper.isOperationAllowed(
              host, cluster.getClusterId(), level);
          }
        }
      );
      LOG.debug("Ignoring hosts when selecting available hosts for action due to maintenance state.Ignored hosts ={}, cluster={}, service={}",
        ignoredHosts, cluster.getClusterName(), service.getName());
    }
  
    /**
     * Filters hosts to only select healthy ones that are heartbeating.
     * <p/>
     * The host's {@link HostState} is used to determine if a host is healthy.
     *
     * @return a List of healthy hosts, or an empty List if none exist.
     * @throws AmbariException
     * @see {@link HostState#HEALTHY}
     */
    @Override
    public List<String> selectHealthyHosts(Set<String> hostList) throws AmbariException {
      List<String> healthyHosts = new ArrayList<>();
  
      for (String candidateHostName : hostList) {
        Host candidateHost = clusters.getHost(candidateHostName);
        if (candidateHost.getState() == HostState.HEALTHY) {
          healthyHosts.add(candidateHostName);
        }
      }
  
      return healthyHosts;
    }
  
    /**
     * Chooses a healthy host from the list of candidate hosts randomly. If there
     * are no healthy hosts, then this method will return {@code null}.
     * <p/>
     * The host's {@link HostState} is used to determine if a host is healthy.
     *
     * @return a random healthy host, or {@code null}.
     * @throws AmbariException
     * @see {@link HostState#HEALTHY}
     */
    @Override
    public String getHealthyHost(Set<String> hostList) throws AmbariException {
      List<String> healthyHosts = selectHealthyHosts(hostList);
  
      if (!healthyHosts.isEmpty()) {
        Collections.shuffle(healthyHosts);
        return healthyHosts.get(0);
      }
  
      return null;
    }
  
    @Override
    public RequestStatusResponse createAction(ExecuteActionRequest actionRequest,
        Map<String, String> requestProperties)
        throws AmbariException {
      String clusterName = actionRequest.getClusterName();
  
      String requestContext = "";
  
      if (requestProperties != null) {
        requestContext = StringEscapeUtils.escapeHtml4(requestProperties.get(RequestResourceProvider.CONTEXT));
        if (requestContext == null) {
          // guice needs a non-null value as there is no way to mark this parameter @Nullable
          requestContext = "";
        }
      }
  
      Cluster cluster = null;
      if (null != clusterName) {
        cluster = clusters.getCluster(clusterName);
  
        LOG.info("Received action execution request"
          + ", clusterName=" + actionRequest.getClusterName()
          + ", request=" + actionRequest);
      }
  
      ActionExecutionContext actionExecContext = getActionExecutionContext(actionRequest);
      if (actionRequest.isCommand()) {
        customCommandExecutionHelper.validateAction(actionRequest);
      } else {
        actionExecutionHelper.validateAction(actionRequest);
      }
      // TODO Alejandro, Called First. insert params.version. Called during Rebalance HDFS, ZOOKEEPER Restart, Zookeeper Service Check.
      long requestId = actionManager.getNextRequestId();
      RequestStageContainer requestStageContainer = new RequestStageContainer(
          requestId,
          null,
          requestFactory,
          actionManager,
          actionRequest);
  
      @Experimental(feature=ExperimentalFeature.MULTI_SERVICE,
          comment = "This must change with Multi-Service since the cluster won't have a desired stack version")
      ExecuteCommandJson jsons = customCommandExecutionHelper.getCommandJson(actionExecContext, cluster,
          null == cluster ? null : cluster.getDesiredStackVersion(), requestContext);
  
      String commandParamsForStage = jsons.getCommandParamsForStage();
  
      Map<String, String> commandParamsStage = gson.fromJson(commandParamsForStage, new TypeToken<Map<String, String>>()
        {}.getType());
      // Ensure that the specified requestContext (if any) is set as the request context
      if (!requestContext.isEmpty()) {
        requestStageContainer.setRequestContext(requestContext);
      }
  
      // replace password references in requestProperties
      SecretReference.replaceReferencesWithPasswords(commandParamsStage, cluster);
  
      // If the request is to perform the Kerberos service check, set up the stages to
      // ensure that the (cluster-level) smoke user principal and keytab is available on all hosts
      boolean kerberosServiceCheck = Role.KERBEROS_SERVICE_CHECK.name().equals(actionRequest.getCommandName());
      if (kerberosServiceCheck) {
        // Parse the command parameters into a map so that additional values may be added to it
  
        try {
          requestStageContainer = kerberosHelper.createTestIdentity(cluster, commandParamsStage, requestStageContainer);
        } catch (KerberosOperationException e) {
          throw new IllegalArgumentException(e.getMessage(), e);
        }
      }
  
      commandParamsForStage = gson.toJson(commandParamsStage);
  
      Stage stage = createNewStage(requestStageContainer.getLastStageId(), cluster, requestId, requestContext,
          commandParamsForStage, jsons.getHostParamsForStage());
  
      if (actionRequest.isCommand()) {
        customCommandExecutionHelper.addExecutionCommandsToStage(actionExecContext, stage,
            requestProperties, jsons);
      } else {
        actionExecutionHelper.addExecutionCommandsToStage(actionExecContext, stage, requestProperties);
      }
  
      RoleGraph rg;
      if (null != cluster) {
        RoleCommandOrder rco = getRoleCommandOrder(cluster);
        rg = roleGraphFactory.createNew(rco);
      } else {
        rg = roleGraphFactory.createNew();
      }
  
      rg.build(stage);
      List<Stage> stages = rg.getStages();
  
      if (stages != null && !stages.isEmpty()) {
        requestStageContainer.addStages(stages);
      }
  
      // If the request is to perform the Kerberos service check, delete the test-specific principal
      // and keytab that was created for this service check
      if (kerberosServiceCheck) {
        // Parse the command parameters into a map so that existing values may be accessed and
        // additional values may be added to it.
        commandParamsStage = gson.fromJson(commandParamsForStage,
            new TypeToken<Map<String, String>>() {
            }.getType());
  
        try {
          requestStageContainer = kerberosHelper.deleteTestIdentity(cluster, commandParamsStage, requestStageContainer);
        } catch (KerberosOperationException e) {
          throw new IllegalArgumentException(e.getMessage(), e);
        }
      }
  
      requestStageContainer.persist();
      return requestStageContainer.getRequestStatusResponse();
    }
  
    @Override
    public Set<StackResponse> getStacks(Set<StackRequest> requests)
        throws AmbariException {
      Set<StackResponse> response = new HashSet<>();
      for (StackRequest request : requests) {
        try {
          response.addAll(getStacks(request));
        } catch (StackAccessException e) {
          if (requests.size() == 1) {
            // only throw exception if 1 request.
            // there will be > 1 request in case of OR predicate
            throw e;
          }
        }
      }
      return response;
  
    }
  
  
    private Set<StackResponse> getStacks(StackRequest request)
        throws AmbariException {
      Set<StackResponse> response;
  
      String stackName = request.getStackName();
  
      if (stackName != null) {
        // this will throw an exception if the stack doesn't exist
        ambariMetaInfo.getStacks(stackName);
        response = Collections.singleton(new StackResponse(stackName));
      } else {
        Collection<StackInfo> supportedStacks = ambariMetaInfo.getStacks();
        response = new HashSet<>();
        for (StackInfo stack: supportedStacks) {
          response.add(new StackResponse(stack.getName()));
        }
      }
      return response;
    }
  
    @Override
    public synchronized RequestStatusResponse updateStacks() throws AmbariException {
  
      try {
        ambariMetaInfo.init();
      } catch (AmbariException e) {
        throw e;
      } catch (Exception e) {
        throw new AmbariException(
            "Ambari Meta Information can't be read from the stack root directory");
      }
  
      return null;
    }
  
    @Override
    public Set<ExtensionResponse> getExtensions(Set<ExtensionRequest> requests)
        throws AmbariException {
      Set<ExtensionResponse> response = new HashSet<>();
      for (ExtensionRequest request : requests) {
        try {
          response.addAll(getExtensions(request));
        } catch (StackAccessException e) {
          if (requests.size() == 1) {
            // only throw exception if 1 request.
            // there will be > 1 request in case of OR predicate
            throw e;
          }
        }
      }
      return response;
  
    }
  
  
    private Set<ExtensionResponse> getExtensions(ExtensionRequest request)
        throws AmbariException {
      Set<ExtensionResponse> response;
  
      String extensionName = request.getExtensionName();
  
      if (extensionName != null) {
        // this will throw an exception if the extension doesn't exist
        ambariMetaInfo.getExtensions(extensionName);
        response = Collections.singleton(new ExtensionResponse(extensionName));
      } else {
        Collection<ExtensionInfo> supportedExtensions = ambariMetaInfo.getExtensions();
        response = new HashSet<>();
        for (ExtensionInfo extension: supportedExtensions) {
          response.add(new ExtensionResponse(extension.getName()));
        }
      }
      return response;
    }
  
    @Override
    public Set<ExtensionVersionResponse> getExtensionVersions(
        Set<ExtensionVersionRequest> requests) throws AmbariException {
      Set<ExtensionVersionResponse> response = new HashSet<>();
      for (ExtensionVersionRequest request : requests) {
        String extensionName = request.getExtensionName();
        try {
          Set<ExtensionVersionResponse> stackVersions = getExtensionVersions(request);
          for (ExtensionVersionResponse stackVersionResponse : stackVersions) {
            stackVersionResponse.setExtensionName(extensionName);
          }
          response.addAll(stackVersions);
        } catch (StackAccessException e) {
          if (requests.size() == 1) {
            // only throw exception if 1 request.
            // there will be > 1 request in case of OR predicate
            throw e;
          }
        }
      }
  
      return response;
    }
  
    private Set<ExtensionVersionResponse> getExtensionVersions(ExtensionVersionRequest request) throws AmbariException {
      Set<ExtensionVersionResponse> response;
  
      String extensionName = request.getExtensionName();
      String extensionVersion = request.getExtensionVersion();
  
      if (extensionVersion != null) {
        ExtensionInfo extensionInfo = ambariMetaInfo.getExtension(extensionName, extensionVersion);
        response = Collections.singleton(extensionInfo.convertToResponse());
      } else {
        try {
          Collection<ExtensionInfo> extensionInfos = ambariMetaInfo.getExtensions(extensionName);
          response = new HashSet<>();
          for (ExtensionInfo extensionInfo: extensionInfos) {
            response.add(extensionInfo.convertToResponse());
          }
        } catch (StackAccessException e) {
          response = Collections.emptySet();
        }
      }
  
      return response;
    }
  
    @Override
    public Set<RepositoryResponse> getRepositories(Set<RepositoryRequest> requests)
        throws AmbariException {
      Set<RepositoryResponse> response = new HashSet<>();
      for (RepositoryRequest request : requests) {
        try {
          String stackName    = request.getStackName();
          String stackVersion = request.getStackVersion();
  
          Set<RepositoryResponse> repositories = getRepositories(request);
  
          for (RepositoryResponse repositoryResponse : repositories) {
            if (repositoryResponse.getStackName() == null) {
              repositoryResponse.setStackName(stackName);
            }
            if (repositoryResponse.getStackVersion() == null) {
              repositoryResponse.setStackVersion(stackVersion);
            }
  
            repositoryResponse.setClusterVersionId(request.getClusterVersionId());
          }
          response.addAll(repositories);
        } catch (StackAccessException e) {
          if (requests.size() == 1) {
            // only throw exception if 1 request.
            // there will be > 1 request in case of OR predicate
            throw e;
          }
        }
      }
      return response;
    }
  
    @Experimental(feature = ExperimentalFeature.CUSTOM_SERVICE_REPOS,
      comment = "Remove logic for handling custom service repos after enabling multi-mpack cluster deployment")
    private Set<RepositoryResponse> getRepositories(RepositoryRequest request) throws AmbariException {
  
      String stackName = request.getStackName();
      String stackVersion = request.getStackVersion();
      String osType = request.getOsType();
      String repoId = request.getRepoId();
      Long repositoryVersionId = request.getRepositoryVersionId();
      String versionDefinitionId = request.getVersionDefinitionId();
  
      // !!! when asking for Repository responses for a versionDefinition, it is either for
      // an established repo version (a Long) OR from the in-memory generated ones (a String)
      if (null == repositoryVersionId && null != versionDefinitionId) {
  
        if (NumberUtils.isDigits(versionDefinitionId)) {
          repositoryVersionId = Long.valueOf(versionDefinitionId);
        }
      }
  
      Set<RepositoryResponse> responses = new HashSet<>();
  
      if (repositoryVersionId != null) {
        final RepositoryVersionEntity repositoryVersion = repositoryVersionDAO.findByPK(repositoryVersionId);
        if (repositoryVersion != null) {
          for (RepoOsEntity operatingSystem : repositoryVersion.getRepoOsEntities()) {
            if (operatingSystem.getFamily().equals(osType)) {
              for (RepoDefinitionEntity repository : operatingSystem.getRepoDefinitionEntities()) {
  
                final RepositoryResponse response = new RepositoryResponse(repository.getBaseUrl(), osType, repository.getRepoID(),
                    repository.getRepoName(), repository.getDistribution(), repository.getComponents(), "", "",
                        repository.getTags(), repository.getApplicableServices());
                if (null != versionDefinitionId) {
                  response.setVersionDefinitionId(versionDefinitionId);
                } else {
                  response.setRepositoryVersionId(repositoryVersionId);
                }
                response.setStackName(repositoryVersion.getStackName());
                response.setStackVersion(repositoryVersion.getStackVersion());
  
                responses.add(response);
              }
              break;
            }
          }
        }
      } else if (null != versionDefinitionId) {
        VersionDefinitionXml xml = ambariMetaInfo.getVersionDefinition(versionDefinitionId);
  
        if (null == xml) {
          throw new AmbariException(String.format("Version identified by %s does not exist",
              versionDefinitionId));
        }
        StackId stackId = new StackId(xml.release.stackId);
  
        ListMultimap<String, RepositoryInfo> stackRepositoriesByOs = ambariMetaInfo.getStackManager().getStack(stackName, stackVersion).getRepositoriesByOs();
        for (RepositoryXml.Os os : xml.repositoryInfo.getOses()) {
  
          for (RepositoryXml.Repo repo : os.getRepos()) {
            RepositoryResponse resp = new RepositoryResponse(repo.getBaseUrl(), os.getFamily(),
                repo.getRepoId(), repo.getRepoName(), repo.getDistribution(), repo.getComponents(), repo.getMirrorsList(),
                repo.getBaseUrl(), repo.getTags(), Collections.EMPTY_LIST);
  
            resp.setVersionDefinitionId(versionDefinitionId);
            resp.setStackName(stackId.getStackName());
            resp.setStackVersion(stackId.getStackVersion());
  
            responses.add(resp);
          }
        }
  
        // Add service repos to the response. (These are not contained by the VDF but are present in the stack model)
        List<RepositoryInfo> serviceRepos =
            RepoUtil.getServiceRepos(xml.repositoryInfo.getRepositories(), stackRepositoriesByOs);
        responses.addAll(RepoUtil.asResponses(serviceRepos, versionDefinitionId, stackName, stackVersion));
  
      } else {
        if (repoId == null) {
          List<RepositoryInfo> repositories = ambariMetaInfo.getRepositories(stackName, stackVersion, osType);
  
          for (RepositoryInfo repository: repositories) {
            responses.add(repository.convertToResponse());
          }
  
        } else {
          RepositoryInfo repository = ambariMetaInfo.getRepository(stackName, stackVersion, osType, repoId);
          responses = Collections.singleton(repository.convertToResponse());
        }
      }
  
      return responses;
    }
  
    @Override
    public void verifyRepositories(Set<RepositoryRequest> requests) throws AmbariException {
      for (RepositoryRequest request: requests) {
        if (request.getBaseUrl() == null) {
          throw new AmbariException("Base url is missing for request " + request);
        }
        verifyRepository(request);
      }
    }
  
    /**
     * Verifies single repository, see {{@link #verifyRepositories(Set)}.
     *
     * @param request request
     * @throws AmbariException if verification fails
     */
    private void verifyRepository(RepositoryRequest request) throws AmbariException {
      URLRedirectProvider usp = new URLRedirectProvider(REPO_URL_CONNECT_TIMEOUT, REPO_URL_READ_TIMEOUT, true);
  
      String repoName = request.getRepoName();
      if (StringUtils.isEmpty(repoName)) {
        throw new IllegalArgumentException("repo_name is required to verify repository");
      }
  
      String errorMessage = null;
      Exception e = null;
  
      String[] suffixes = configs.getRepoValidationSuffixes(request.getOsType());
      for (String suffix : suffixes) {
        String formatted_suffix = String.format(suffix, repoName);
        String spec = request.getBaseUrl().trim();
  
        // This logic is to identify if the end of baseurl has a slash ('/') and/or the beginning of suffix String (e.g. "/repodata/repomd.xml")
        // has a slash and they can form a good url.
        // e.g. "http://baseurl.com/" + "/repodata/repomd.xml" becomes "http://baseurl.com/repodata/repomd.xml" but not "http://baseurl.com//repodata/repomd.xml"
        if (spec.charAt(spec.length() - 1) != '/' && formatted_suffix.charAt(0) != '/') {
          spec = spec + "/" + formatted_suffix;
        } else if (spec.charAt(spec.length() - 1) == '/' && formatted_suffix.charAt(0) == '/') {
          spec = spec + formatted_suffix.substring(1);
        } else {
          spec = spec + formatted_suffix;
        }
  
        // if spec contains "file://" then check local file system.
        final String FILE_SCHEME = "file://";
        if(spec.toLowerCase().startsWith(FILE_SCHEME)){
          String filePath = spec.substring(FILE_SCHEME.length());
          File f = new File(filePath);
          if(!f.exists()){
            errorMessage = "Could not access base url . " + spec + " . ";
            e = new FileNotFoundException(errorMessage);
            break;
          }
  
        }else{
          try {
            URLRedirectProvider.RequestResult result = usp.executeGet(spec);
            if (result.getCode() != HttpStatus.SC_OK) {
              errorMessage = String.format("Could not access base url '%s', code: '%d', response: '%s'",
                                           URLCredentialsHider.hideCredentials(request.getBaseUrl()),
                                           result.getCode(),
                                           result.getContent());
            }
          } catch (IOException ioe) {
            e = ioe;
            errorMessage = String.format("Could not access base url '%s'",
                                         URLCredentialsHider.hideCredentials(request.getBaseUrl()));
            if (LOG.isDebugEnabled()) {
              errorMessage += ioe;
            } else {
              errorMessage += ioe.getMessage();
            }
            break;
          }
        }
      }
  
      if (errorMessage != null) {
        LOG.error(errorMessage);
        if (e == null) {
          throw new IllegalArgumentException(errorMessage);
        } else {
          throw new IllegalArgumentException(errorMessage, e);
        }
      }
    }
  
    @Override
    public Set<StackVersionResponse> getStackVersions(
        Set<StackVersionRequest> requests) throws AmbariException {
      Set<StackVersionResponse> response = new HashSet<>();
      for (StackVersionRequest request : requests) {
        String stackName = request.getStackName();
        try {
          Set<StackVersionResponse> stackVersions = getStackVersions(request);
          for (StackVersionResponse stackVersionResponse : stackVersions) {
            stackVersionResponse.setStackName(stackName);
          }
          response.addAll(stackVersions);
        } catch (StackAccessException e) {
          if (requests.size() == 1) {
            // only throw exception if 1 request.
            // there will be > 1 request in case of OR predicate
            throw e;
          }
        }
      }
  
      return response;
  
    }
  
    private Set<StackVersionResponse> getStackVersions(StackVersionRequest request) throws AmbariException {
      Set<StackVersionResponse> response;
  
      String stackName = request.getStackName();
      String stackVersion = request.getStackVersion();
  
      if (stackVersion != null) {
        StackInfo stackInfo = ambariMetaInfo.getStack(stackName, stackVersion);
        response = Collections.singleton(stackInfo.convertToResponse());
      } else {
        try {
          Collection<StackInfo> stackInfos = ambariMetaInfo.getStacks(stackName);
          response = new HashSet<>();
          for (StackInfo stackInfo: stackInfos) {
            response.add(stackInfo.convertToResponse());
          }
        } catch (StackAccessException e) {
          response = Collections.emptySet();
        }
      }
  
      return response;
    }
  
    @Override
    public Set<StackServiceResponse> getStackServices(
        Set<StackServiceRequest> requests) throws AmbariException {
  
      Set<StackServiceResponse> response = new HashSet<>();
  
      for (StackServiceRequest request : requests) {
        String stackName    = request.getStackName();
        String stackVersion = request.getStackVersion();
  
        try {
          Set<StackServiceResponse> stackServices = getStackServices(request);
  
          for (StackServiceResponse stackServiceResponse : stackServices) {
            stackServiceResponse.setStackName(stackName);
            stackServiceResponse.setStackVersion(stackVersion);
          }
  
          response.addAll(stackServices);
        } catch (StackAccessException e) {
          if (requests.size() == 1) {
            // only throw exception if 1 request.
            // there will be > 1 request in case of OR predicate
            throw e;
          }
        }
      }
  
      return response;
    }
  
    private Set<StackServiceResponse> getStackServices(StackServiceRequest request) throws AmbariException {
      Set<StackServiceResponse> response;
  
      String stackName = request.getStackName();
      String stackVersion = request.getStackVersion();
      String serviceName = request.getServiceName();
  
      if (serviceName != null) {
        ServiceInfo service = ambariMetaInfo.getService(stackName, stackVersion, serviceName);
        response = Collections.singleton(new StackServiceResponse(service));
      } else {
        Map<String, ServiceInfo> services = ambariMetaInfo.getServices(stackName, stackVersion);
        response = new HashSet<>();
        for (ServiceInfo service : services.values()) {
          response.add(new StackServiceResponse(service));
        }
      }
      return response;
    }
  
    @Override
    public Set<StackConfigurationResponse> getStackLevelConfigurations(
        Set<StackLevelConfigurationRequest> requests) throws AmbariException {
      Set<StackConfigurationResponse> response = new HashSet<>();
      for (StackLevelConfigurationRequest request : requests) {
  
        String stackName    = request.getStackName();
        String stackVersion = request.getStackVersion();
  
        Set<StackConfigurationResponse> stackConfigurations = getStackLevelConfigurations(request);
  
        for (StackConfigurationResponse stackConfigurationResponse : stackConfigurations) {
          stackConfigurationResponse.setStackName(stackName);
          stackConfigurationResponse.setStackVersion(stackVersion);
        }
  
        response.addAll(stackConfigurations);
      }
  
      return response;
    }
  
    private Set<StackConfigurationResponse> getStackLevelConfigurations(
        StackLevelConfigurationRequest request) throws AmbariException {
  
      Set<StackConfigurationResponse> response = new HashSet<>();
  
      String stackName = request.getStackName();
      String stackVersion = request.getStackVersion();
      String propertyName = request.getPropertyName();
      Set<PropertyInfo> configs;
  
      //properties : cluster-env
      //TODO: Remove after getting rid of cluster-env
      if (propertyName != null) {
        configs = ambariMetaInfo.getStackPropertiesByName(stackName, stackVersion, propertyName);
      } else {
        configs = ambariMetaInfo.getStackProperties(stackName, stackVersion);
      }
  
      for (PropertyInfo property: configs) {
        response.add(property.convertToResponse());
      }
  
      return response;
    }
  
    @Override
    public Set<StackConfigurationResponse> getStackConfigurations(
        Set<StackConfigurationRequest> requests) throws AmbariException {
      Set<StackConfigurationResponse> response = new HashSet<>();
      for (StackConfigurationRequest request : requests) {
  
        String stackName    = request.getStackName();
        String stackVersion = request.getStackVersion();
        String serviceName  = request.getServiceName();
  
        Set<StackConfigurationResponse> stackConfigurations = getStackConfigurations(request);
  
        for (StackConfigurationResponse stackConfigurationResponse : stackConfigurations) {
          stackConfigurationResponse.setStackName(stackName);
          stackConfigurationResponse.setStackVersion(stackVersion);
          stackConfigurationResponse.setServiceName(serviceName);
        }
  
        response.addAll(stackConfigurations);
      }
  
      return response;
    }
  
    private Set<StackConfigurationResponse> getStackConfigurations(
        StackConfigurationRequest request) throws AmbariException {
  
      Set<StackConfigurationResponse> response = new HashSet<>();
  
      String stackName = request.getStackName();
      String stackVersion = request.getStackVersion();
      String serviceName = request.getServiceName();
      String propertyName = request.getPropertyName();
  
      Set<PropertyInfo> properties;
      if (propertyName != null) {
        properties = ambariMetaInfo.getPropertiesByName(stackName, stackVersion, serviceName, propertyName);
      } else {
        properties = ambariMetaInfo.getServiceProperties(stackName, stackVersion, serviceName);
      }
      for (PropertyInfo property: properties) {
        response.add(property.convertToResponse());
      }
  
      return response;
    }
  
    @Override
    public Set<StackServiceComponentResponse> getStackComponents(
        Set<StackServiceComponentRequest> requests) throws AmbariException {
      Set<StackServiceComponentResponse> response = new HashSet<>();
      for (StackServiceComponentRequest request : requests) {
        String stackName    = request.getStackName();
        String stackVersion = request.getStackVersion();
        String serviceName  = request.getServiceName();
  
        try {
          Set<StackServiceComponentResponse> stackComponents = getStackComponents(request);
  
          for (StackServiceComponentResponse stackServiceComponentResponse : stackComponents) {
            stackServiceComponentResponse.setStackName(stackName);
            stackServiceComponentResponse.setStackVersion(stackVersion);
            stackServiceComponentResponse.setServiceName(serviceName);
          }
  
          response.addAll(stackComponents);
        } catch (StackAccessException e) {
          if (requests.size() == 1) {
            // only throw exception if 1 request.
            // there will be > 1 request in case of OR predicate
            throw e;
          }
        }
      }
  
      return response;
    }
  
    private Set<StackServiceComponentResponse> getStackComponents(
        StackServiceComponentRequest request) throws AmbariException {
      Set<StackServiceComponentResponse> response;
  
      String stackName     = request.getStackName();
      String stackVersion  = request.getStackVersion();
      String serviceName   = request.getServiceName();
      String componentName = request.getComponentName();
  
      if (componentName != null) {
        ComponentInfo component = ambariMetaInfo.getComponent(stackName, stackVersion, serviceName, componentName);
        response = Collections.singleton(new StackServiceComponentResponse(
            component));
  
      } else {
        List<ComponentInfo> components = ambariMetaInfo.getComponentsByService(stackName, stackVersion, serviceName);
        response = new HashSet<>();
  
        for (ComponentInfo component: components) {
          response.add(new StackServiceComponentResponse(component));
        }
      }
      return response;
    }
  
    @Override
    public Set<OperatingSystemResponse> getOperatingSystems(
        Set<OperatingSystemRequest> requests) throws AmbariException {
      Set<OperatingSystemResponse> response = new HashSet<>();
      for (OperatingSystemRequest request : requests) {
        try {
          String stackName    = request.getStackName();
          String stackVersion = request.getStackVersion();
  
          Set<OperatingSystemResponse> stackOperatingSystems = getOperatingSystems(request);
  
          for (OperatingSystemResponse operatingSystemResponse : stackOperatingSystems) {
            if (operatingSystemResponse.getStackName() == null) {
              operatingSystemResponse.setStackName(stackName);
            }
            if (operatingSystemResponse.getStackVersion() == null) {
              operatingSystemResponse.setStackVersion(stackVersion);
            }
          }
          response.addAll(stackOperatingSystems);
        } catch (StackAccessException e) {
          if (requests.size() == 1) {
            // only throw exception if 1 request.
            // there will be > 1 request in case of OR predicate
            throw e;
          }
        }
      }
      return response;
    }
  
    private Set<OperatingSystemResponse> getOperatingSystems(
        OperatingSystemRequest request) throws AmbariException {
  
      Set<OperatingSystemResponse> responses = new HashSet<>();
  
      String stackName = request.getStackName();
      String stackVersion = request.getStackVersion();
      String osType = request.getOsType();
      Long repositoryVersionId = request.getRepositoryVersionId();
      String versionDefinitionId = request.getVersionDefinitionId();
  
      // !!! when asking for OperatingSystem responses for a versionDefinition, it is either for
      // an established repo version (a Long) OR from the in-memory generated ones (a String)
      if (null == repositoryVersionId && null != versionDefinitionId) {
        if (NumberUtils.isDigits(versionDefinitionId)) {
          repositoryVersionId = Long.valueOf(versionDefinitionId);
        }
      }
  
      if (repositoryVersionId != null) {
        final RepositoryVersionEntity repositoryVersion = repositoryVersionDAO.findByPK(repositoryVersionId);
        if (repositoryVersion != null) {
          for (RepoOsEntity operatingSystem : repositoryVersion.getRepoOsEntities()) {
            final OperatingSystemResponse response = new OperatingSystemResponse(operatingSystem.getFamily());
            if (null != versionDefinitionId) {
              response.setVersionDefinitionId(repositoryVersionId.toString());
            } else {
              response.setRepositoryVersionId(repositoryVersionId);
            }
            response.setStackName(repositoryVersion.getStackName());
            response.setStackVersion(repositoryVersion.getStackVersion());
            response.setAmbariManagedRepos(operatingSystem.isAmbariManaged());
            responses.add(response);
          }
        }
      } else if (null != versionDefinitionId) {
        VersionDefinitionXml xml = ambariMetaInfo.getVersionDefinition(versionDefinitionId);
  
        if (null == xml) {
          throw new AmbariException(String.format("Version identified by %s does not exist",
              versionDefinitionId));
        }
        StackId stackId = new StackId(xml.release.stackId);
  
        for (RepositoryXml.Os os : xml.repositoryInfo.getOses()) {
          OperatingSystemResponse resp = new OperatingSystemResponse(os.getFamily());
          resp.setVersionDefinitionId(versionDefinitionId);
          resp.setStackName(stackId.getStackName());
          resp.setStackVersion(stackId.getStackVersion());
  
          responses.add(resp);
        }
  
      } else {
        if (osType != null) {
          OperatingSystemInfo operatingSystem = ambariMetaInfo.getOperatingSystem(stackName, stackVersion, osType);
          responses = Collections.singleton(operatingSystem.convertToResponse());
        } else {
          Set<OperatingSystemInfo> operatingSystems = ambariMetaInfo.getOperatingSystems(stackName, stackVersion);
          for (OperatingSystemInfo operatingSystem : operatingSystems) {
            responses.add(operatingSystem.convertToResponse());
          }
        }
      }
  
      return responses;
    }
  
    @Override
    public String getAuthName() {
      return AuthorizationHelper.getAuthenticatedName(configs.getAnonymousAuditName());
    }
  
    @Override
    public int getAuthId() {
      return AuthorizationHelper.getAuthenticatedId();
    }
  
    @Override
    public Set<RootServiceResponse> getRootServices(
        Set<RootServiceRequest> requests) throws AmbariException {
      Set<RootServiceResponse> response = new HashSet<>();
      for (RootServiceRequest request : requests) {
        try {
          response.addAll(getRootServices(request));
        } catch (AmbariException e) {
          if (requests.size() == 1) {
            // only throw exception if 1 request.
            // there will be > 1 request in case of OR predicate
            throw e;
          }
        }
      }
      return response;
    }
  
    private Set<RootServiceResponse> getRootServices (RootServiceRequest request)
        throws AmbariException{
      return rootServiceResponseFactory.getRootServices(request);
    }
  
    @Override
    public Set<RootServiceComponentResponse> getRootServiceComponents(
        Set<RootServiceComponentRequest> requests) throws AmbariException {
      Set<RootServiceComponentResponse> response = new HashSet<>();
      for (RootServiceComponentRequest request : requests) {
        try {
          Set<RootServiceComponentResponse> rootServiceComponents = getRootServiceComponents(request);
          response.addAll(rootServiceComponents);
        } catch (AmbariException e) {
          if (requests.size() == 1) {
            // only throw exception if 1 request.
            // there will be > 1 request in case of OR predicate
            throw e;
          }
        }
      }
      return response;
    }
  
    private Set<RootServiceComponentResponse> getRootServiceComponents(
        RootServiceComponentRequest request) throws AmbariException{
      return rootServiceResponseFactory.getRootServiceComponents(request);
    }
  
    @Override
    public Clusters getClusters() {
      return clusters;
    }
  
    @Override
    public ConfigHelper getConfigHelper() {
      return configHelper;
    }
  
    @Override
    public AmbariMetaInfo getAmbariMetaInfo() {
      return ambariMetaInfo;
    }
  
    @Override
    public ServiceComponentFactory getServiceComponentFactory() {
      return serviceComponentFactory;
    }
  
    @Override
    public ConfigGroupFactory getConfigGroupFactory() {
      return configGroupFactory;
    }
  
    @Override
    public RoleGraphFactory getRoleGraphFactory() {
      return roleGraphFactory;
    }
  
    @Override
    public AbstractRootServiceResponseFactory getRootServiceResponseFactory() {
      return rootServiceResponseFactory;
  
    }
  
    @Override
    public ActionManager getActionManager() {
      return actionManager;
    }
  
    @Override
    public String getJdkResourceUrl() {
      return jdkResourceUrl;
    }
  
    @Override
    public String getJavaHome() {
      return javaHome;
    }
  
    @Override
    public String getAmbariJavaHome() {
      return ambariJavaHome;
    }
  
    @Override
    public String getJDKName() {
      return jdkName;
    }
  
    @Override
    public String getJCEName() {
      return jceName;
    }
  
    @Override
    public String getServerDB() {
      return serverDB;
    }
  
    @Override
    public String getOjdbcUrl() {
      return ojdbcUrl;
    }
  
    @Override
    public String getMysqljdbcUrl() {
      return mysqljdbcUrl;
    }
  
    @Override
    public Map<String, String> getRcaParameters() {
  
      String hostName = StageUtils.getHostName();
  
      String url = configs.getRcaDatabaseUrl();
      if (url.contains(Configuration.HOSTNAME_MACRO)) {
        url =
            url.replace(Configuration.HOSTNAME_MACRO,
                hostsMap.getHostMap(hostName));
      }
  
      Map<String, String> rcaParameters = new HashMap<>();
  
      rcaParameters.put(AMBARI_DB_RCA_URL, url);
      rcaParameters.put(AMBARI_DB_RCA_DRIVER, configs.getRcaDatabaseDriver());
      rcaParameters.put(AMBARI_DB_RCA_USERNAME, configs.getRcaDatabaseUser());
      rcaParameters.put(AMBARI_DB_RCA_PASSWORD, configs.getRcaDatabasePassword());
  
      return rcaParameters;
    }
  
    @Override
    public boolean checkLdapConfigured() {
      return ldapDataPopulator.isLdapEnabled();
    }
  
    @Override
    public LdapSyncDto getLdapSyncInfo() throws AmbariException {
      return ldapDataPopulator.getLdapSyncInfo();
    }
  
    @Override
    public boolean isLdapSyncInProgress() {
      return ldapSyncInProgress;
    }
  
    @Override
    public synchronized LdapBatchDto synchronizeLdapUsersAndGroups(
        LdapSyncRequest userRequest, LdapSyncRequest groupRequest)
        throws AmbariException {
      ldapSyncInProgress = true;
      try {
  
        final LdapBatchDto batchInfo = new LdapBatchDto();
        boolean postProcessExistingUsers = false;
        boolean postProcessExistingUsersInGroups = false;
  
        if (userRequest != null) {
          postProcessExistingUsers = userRequest.getPostProcessExistingUsers();
  
          if(postProcessExistingUsers && !configs.isUserHookEnabled()) {
            LOG.warn("Post processing existing users is requested while processing users; however, the user post creation hook is turned off.");
            postProcessExistingUsers = false;
          }
  
          switch (userRequest.getType()) {
            case ALL:
              ldapDataPopulator.synchronizeAllLdapUsers(batchInfo, postProcessExistingUsers);
              break;
            case EXISTING:
              ldapDataPopulator.synchronizeExistingLdapUsers(batchInfo, postProcessExistingUsers);
              break;
            case SPECIFIC:
              ldapDataPopulator.synchronizeLdapUsers(userRequest.getPrincipalNames(), batchInfo, postProcessExistingUsers);
              break;
          }
        }
        if (groupRequest != null) {
          postProcessExistingUsersInGroups = groupRequest.getPostProcessExistingUsers();
  
          if(postProcessExistingUsersInGroups && !configs.isUserHookEnabled()) {
            LOG.warn("Post processing existing users is requested while processing groups; however, the user post creation hook is turned off.");
            postProcessExistingUsersInGroups = false;
          }
  
          switch (groupRequest.getType()) {
            case ALL:
              ldapDataPopulator.synchronizeAllLdapGroups(batchInfo, postProcessExistingUsersInGroups);
              break;
            case EXISTING:
              ldapDataPopulator.synchronizeExistingLdapGroups(batchInfo, postProcessExistingUsersInGroups);
              break;
            case SPECIFIC:
              ldapDataPopulator.synchronizeLdapGroups(groupRequest.getPrincipalNames(), batchInfo, postProcessExistingUsersInGroups);
              break;
          }
        }
  
        users.processLdapSync(batchInfo);
  
        if (postProcessExistingUsers || postProcessExistingUsersInGroups) {
          // Execute post user creation hook on ignored users. These users were previously synced with
          // Ambari but the post user creation script may not have been run on them due to various
          // reasons
          Set<LdapUserDto> ignoredUsers = batchInfo.getUsersIgnored();
          if(CollectionUtils.isNotEmpty(ignoredUsers)) {
            Map<String, Set<String>> userGroupsMap = new HashMap<>();
            for (LdapUserDto ignoredUser : ignoredUsers) {
              // The set of groups is empty here since the groups are not used in the script and the
              // existing usage of the post user creation hook does not supply a set of groups either.
              userGroupsMap.put(ignoredUser.getUserName(), Collections.emptySet());
            }
  
            users.executeUserHook(userGroupsMap);
          }
        }
  
        return batchInfo;
      } finally {
        ldapSyncInProgress = false;
      }
    }
  
    @SuppressWarnings("unchecked")
    @Override
    public void initializeWidgetsAndLayouts(Cluster cluster, Service service) throws AmbariException {
      Type widgetLayoutType = new TypeToken<Map<String, List<WidgetLayout>>>(){}.getType();
  
      Set<File> widgetDescriptorFiles = new HashSet<>();
  
      if (null != service) {
        ServiceInfo serviceInfo = ambariMetaInfo.getService(service);
        File widgetDescriptorFile = serviceInfo.getWidgetsDescriptorFile();
        if (widgetDescriptorFile != null && widgetDescriptorFile.exists()) {
          widgetDescriptorFiles.add(widgetDescriptorFile);
        }
      } else {
        // common cluster level widgets
        File commonWidgetsFile = ambariMetaInfo.getCommonWidgetsDescriptorFile();
        if (commonWidgetsFile != null && commonWidgetsFile.exists()) {
          widgetDescriptorFiles.add(commonWidgetsFile);
        } else {
          LOG.warn("Common widgets file with path {%s} doesn't exist. No cluster widgets will be created.", commonWidgetsFile);
        }
      }
  
      for (File widgetDescriptorFile : widgetDescriptorFiles) {
        Map<String, Object> widgetDescriptor = null;
  
        try {
          widgetDescriptor = gson.fromJson(new FileReader(widgetDescriptorFile), widgetLayoutType);
  
          for (Object artifact : widgetDescriptor.values()) {
            List<WidgetLayout> widgetLayouts = (List<WidgetLayout>) artifact;
            createWidgetsAndLayouts(cluster, widgetLayouts);
          }
  
        } catch (Exception ex) {
          String msg = "Error loading widgets from file: " + widgetDescriptorFile;
          LOG.error(msg, ex);
          throw new AmbariException(msg);
        }
      }
    }
  
    private WidgetEntity addIfNotExistsWidgetEntity(WidgetLayoutInfo layoutInfo, ClusterEntity clusterEntity,
                                            String user, long createTime) {
      List<WidgetEntity> createdEntities =
        widgetDAO.findByName(clusterEntity.getClusterId(), layoutInfo.getWidgetName(),
          user, layoutInfo.getDefaultSectionName());
  
      if (createdEntities == null || createdEntities.isEmpty()) {
        WidgetEntity widgetEntity = new WidgetEntity();
        widgetEntity.setClusterId(clusterEntity.getClusterId());
        widgetEntity.setClusterEntity(clusterEntity);
        widgetEntity.setScope(WidgetResourceProvider.SCOPE.CLUSTER.name());
        widgetEntity.setWidgetName(layoutInfo.getWidgetName());
        widgetEntity.setDefaultSectionName(layoutInfo.getDefaultSectionName());
        widgetEntity.setAuthor(user);
        widgetEntity.setDescription(layoutInfo.getDescription());
        widgetEntity.setTimeCreated(createTime);
        widgetEntity.setWidgetType(layoutInfo.getType());
        widgetEntity.setMetrics(gson.toJson(layoutInfo.getMetricsInfo()));
        widgetEntity.setProperties(gson.toJson(layoutInfo.getProperties()));
        widgetEntity.setWidgetValues(gson.toJson(layoutInfo.getValues()));
        widgetEntity.setListWidgetLayoutUserWidgetEntity(new LinkedList<>());
        LOG.info("Creating cluster widget with: name = " +
          layoutInfo.getWidgetName() + ", type = " + layoutInfo.getType() + ", " +
          "cluster = " + clusterEntity.getClusterName());
        // Persisting not visible widgets
        // visible one will be cascaded on creation of layout
        if (!layoutInfo.isVisible()) {
          widgetDAO.create(widgetEntity);
        }
        return widgetEntity;
      } else {
        LOG.warn("Skip creating widget from stack artifact since one or more " +
          "already exits with name = " + layoutInfo.getWidgetName() + ", " +
            "clusterId = " + clusterEntity.getClusterId() + ", user = " + user);
      }
      return null;
    }
  
    @Transactional
    void createWidgetsAndLayouts(Cluster cluster, List<WidgetLayout> widgetLayouts) {
      String user = "ambari";
      Long clusterId = cluster.getClusterId();
      ClusterEntity clusterEntity = clusterDAO.findById(clusterId);
      if (clusterEntity == null) {
        return;
      }
      Long now = System.currentTimeMillis();
  
      if (widgetLayouts != null) {
        for (WidgetLayout widgetLayout : widgetLayouts) {
          List<WidgetLayoutEntity> existingEntities =
            widgetLayoutDAO.findByName(clusterId, widgetLayout.getLayoutName(), user);
          // Update layout properties if the layout exists
          if (existingEntities == null || existingEntities.isEmpty()) {
            WidgetLayoutEntity layoutEntity = new WidgetLayoutEntity();
            layoutEntity.setClusterEntity(clusterEntity);
            layoutEntity.setClusterId(clusterId);
            layoutEntity.setLayoutName(widgetLayout.getLayoutName());
            layoutEntity.setDisplayName(widgetLayout.getDisplayName());
            layoutEntity.setSectionName(widgetLayout.getSectionName());
            layoutEntity.setScope(WidgetLayoutResourceProvider.SCOPE.CLUSTER.name());
            layoutEntity.setUserName(user);
  
            List<WidgetLayoutUserWidgetEntity> widgetLayoutUserWidgetEntityList = new LinkedList<>();
            int order = 0;
            for (WidgetLayoutInfo layoutInfo : widgetLayout.getWidgetLayoutInfoList()) {
              if (layoutInfo.getDefaultSectionName() == null) {
                layoutInfo.setDefaultSectionName(layoutEntity.getSectionName());
              }
              WidgetEntity widgetEntity = addIfNotExistsWidgetEntity(layoutInfo, clusterEntity, user, now);
              // Add to layout if visibility is true and widget was newly added
              if (widgetEntity != null && layoutInfo.isVisible()) {
                WidgetLayoutUserWidgetEntity widgetLayoutUserWidgetEntity = new WidgetLayoutUserWidgetEntity();
                widgetLayoutUserWidgetEntity.setWidget(widgetEntity);
                widgetLayoutUserWidgetEntity.setWidgetOrder(order++);
                widgetLayoutUserWidgetEntity.setWidgetLayout(layoutEntity);
                widgetLayoutUserWidgetEntityList.add(widgetLayoutUserWidgetEntity);
                widgetEntity.getListWidgetLayoutUserWidgetEntity().add(widgetLayoutUserWidgetEntity);
              }
            }
            layoutEntity.setListWidgetLayoutUserWidgetEntity(widgetLayoutUserWidgetEntityList);
            widgetLayoutDAO.createWithFlush(layoutEntity);
          } else {
            if (existingEntities.size() > 1) {
              LOG.warn("Skip updating layout since multiple widget layouts " +
                "found with: name = " + widgetLayout.getLayoutName() + ", " +
                "user = " + user + ", cluster = " + cluster.getClusterName());
            } else {
              WidgetLayoutEntity existingLayoutEntity = existingEntities.iterator().next();
              existingLayoutEntity.setSectionName(widgetLayout.getSectionName());
              existingLayoutEntity.setDisplayName(widgetLayout.getDisplayName());
              // Add new widgets to end of the existing ones
              List<WidgetLayoutUserWidgetEntity> layoutUserWidgetEntities = existingLayoutEntity.getListWidgetLayoutUserWidgetEntity();
              if (layoutUserWidgetEntities == null) {
                layoutUserWidgetEntities = new LinkedList<>();
                existingLayoutEntity.setListWidgetLayoutUserWidgetEntity(layoutUserWidgetEntities);
              }
              int order = layoutUserWidgetEntities.size() - 1;
              List<WidgetLayoutInfo> layoutInfoList = widgetLayout.getWidgetLayoutInfoList();
              if (layoutInfoList != null && !layoutInfoList.isEmpty()) {
                for (WidgetLayoutInfo layoutInfo : layoutInfoList) {
                  WidgetEntity widgetEntity = addIfNotExistsWidgetEntity(layoutInfo, clusterEntity, user, now);
                  if (widgetEntity != null && layoutInfo.isVisible()) {
                    WidgetLayoutUserWidgetEntity widgetLayoutUserWidgetEntity = new WidgetLayoutUserWidgetEntity();
                    widgetLayoutUserWidgetEntity.setWidget(widgetEntity);
                    widgetLayoutUserWidgetEntity.setWidgetOrder(order++);
                    widgetLayoutUserWidgetEntity.setWidgetLayout(existingLayoutEntity);
                    layoutUserWidgetEntities.add(widgetLayoutUserWidgetEntity);
                    widgetEntity.getListWidgetLayoutUserWidgetEntity().add(widgetLayoutUserWidgetEntity);
                  }
                }
              }
              widgetLayoutDAO.mergeWithFlush(existingLayoutEntity);
            }
          }
        }
      }
    }
  
    @Override
    public TimelineMetricCacheProvider getTimelineMetricCacheProvider() {
      return injector.getInstance(TimelineMetricCacheProvider.class);
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public MetricPropertyProviderFactory getMetricPropertyProviderFactory() {
      return injector.getInstance(MetricPropertyProviderFactory.class);
    }
  
    @Override
    public LoggingSearchPropertyProvider getLoggingSearchPropertyProvider() {
      return injector.getInstance(LoggingSearchPropertyProvider.class);
    }
  
    @Override
    public LoggingService getLoggingService(String clusterName) {
      LoggingService loggingService = new LoggingService(clusterName);
      injector.injectMembers(loggingService);
      return loggingService;
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    public AmbariEventPublisher getAmbariEventPublisher() {
      return injector.getInstance(AmbariEventPublisher.class);
    }
  
    @Override
    public KerberosHelper getKerberosHelper() {
      return kerberosHelper;
    }
  
    @Override
    public CredentialStoreService getCredentialStoreService() {
      return credentialStoreService;
    }
  
    /**
     * Queries the CredentialStoreService to gather properties about it.
     * <p/>
     * In particular, the details about which storage facilities are avaialble are returned via Boolean
     * properties.
     *
     * @return a map of properties
     */
    public Map<String,String> getCredentialStoreServiceProperties() {
      Map<String,String> properties = new HashMap<>();
      properties.put("storage.persistent", String.valueOf(credentialStoreService.isInitialized(CredentialStoreType.PERSISTED)));
      properties.put("storage.temporary", String.valueOf(credentialStoreService.isInitialized(CredentialStoreType.TEMPORARY)));
      return properties;
    }
  
    @Override
    public MetricsCollectorHAManager getMetricsCollectorHAManager() {
      return injector.getInstance(MetricsCollectorHAManager.class);
    }
  
    /**
     * Validates that the authenticated user can set a service's (run-as) user and group.
     * <p/>
     * If the user is authorized to set service users and groups, than this method exits quickly.
     * If the user is not authorized to set service users and groups, then this method verifies that
     * the properties of types USER and GROUP have not been changed. If they have been, an
     * AuthorizationException is thrown.
     *
     * @param cluster         the relevant cluster
     * @param configType      the changed configuration type
     * @param propertyChanges a map of the property changes for the relevant configuration type
     * @throws AuthorizationException if the user is not authorized to perform this operation
     */
    protected void validateAuthorizationToUpdateServiceUsersAndGroups(Cluster cluster,
                                                                      String configType,
                                                                      Map<String, String[]> propertyChanges)
        throws AuthorizationException {
  
      if ((propertyChanges != null) && !propertyChanges.isEmpty()) {
        // If the authenticated user is not authorized to set service users or groups, make sure the
        // relevant properties are not changed. However, if the user is authorized to set service
        // users and groups, there is nothing to check.
        if (!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(),
            RoleAuthorization.SERVICE_SET_SERVICE_USERS_GROUPS)) {
  
          Map<PropertyInfo.PropertyType, Set<String>> propertyTypes = cluster.getConfigPropertiesTypes(configType);
  
          //  Create a composite set of properties to check...
          Set<String> propertiesToCheck = new HashSet<>();
  
          Set<String> userProperties = propertyTypes.get(PropertyType.USER);
          if (userProperties != null) {
            propertiesToCheck.addAll(userProperties);
          }
  
          Set<String> groupProperties = propertyTypes.get(PropertyType.GROUP);
          if (groupProperties != null) {
            propertiesToCheck.addAll(groupProperties);
          }
  
          // If there are no USER or GROUP type properties, skip the validation check...
          for (String propertyName : propertiesToCheck) {
            String[] values = propertyChanges.get(propertyName);
            if (values != null) {
              String existingValue = values[0];
              String requestedValue = values[1];
  
              // If the properties don't match, so thrown an authorization exception
              if ((existingValue == null) ? (requestedValue != null) : !existingValue.equals(requestedValue)) {
                throw new AuthorizationException("The authenticated user is not authorized to set service user and groups");
              }
            }
          }
        }
      }
    }
  
    /**
     * Validates that the authenticated user can manage the cluster-wide configuration for a service's
     * ability to be set to auto-start.
     * <p/>
     * If the user is authorized, than this method exits quickly.
     * If the user is not authorized, then this method verifies that the configuration property
     * <code>cluster-env/recovery_enabled</code> is not changed. If it was, an
     * {@link AuthorizationException} is thrown.
     *
     * @param cluster         the relevant cluster
     * @param configType      the changed configuration type
     * @param propertyChanges a map of the property changes for the relevant configuration type
     * @throws AuthorizationException if the user is not authorized to perform this operation
     */
    protected void validateAuthorizationToManageServiceAutoStartConfiguration(Cluster cluster,
                                                                              String configType,
                                                                              Map<String, String[]> propertyChanges)
        throws AuthorizationException {
      // If the authenticated user is authorized to manage the cluster-wide configuration for a
      // service's ability to be set to auto-start, there is nothing to check.
      if (!AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(),
          RoleAuthorization.CLUSTER_MANAGE_AUTO_START)) {
  
        if ("cluster-env".equals(configType) && propertyChanges.containsKey("recovery_enabled")) {
          throw new AuthorizationException("The authenticated user is not authorized to set service user and groups");
        }
      }
    }
  
    /**
     * Validates that the authenticated user can modify configurations for either a service or the
     * cluster.
     * <p>
     * Since some properties have special meaning, they may be ignored when perfoming this authorization
     * check. For example, to change the cluster's overall auto-start setting (cluster-env/recovery_enabled)
     * requires a specific permission that is not the same as the ability to set cluster-wide properties
     * (in general).  Because of this, the <code>cluster-env/recovery_enabled</code> propery should be
     * ignored in this check since permission to change it is expected to be validated elsewhere.
     *
     * @param cluster                the relevant cluster
     * @param configType             the changed configuration type
     * @param propertyChanges        a map of the property changes for the relevant configuration type
     * @param changesToIgnore        a map of configuration type names to sets of propery names to be ignored
     * @param isServiceConfiguration <code>true</code>, if the configuration type is a service-level configuration;
     *                               <code>false</code>, if the configuration type is a cluster-level configuration
     * @throws AuthorizationException if the authenticated user is not authorized to change the requested configuration
     */
    private void validateAuthorizationToModifyConfigurations(Cluster cluster, String configType,
                                                             Map<String, String[]> propertyChanges,
                                                             Map<String, Set<String>> changesToIgnore,
                                                             boolean isServiceConfiguration)
        throws AuthorizationException {
      // If the authenticated user is authorized to update cluster-wide/service-level configurations
      // there is nothing to check, else ensure no (relevant) configurations are being changed - ignoring
      // the specified configurations which may fall under a special category.
      // For example cluster-env/recovery_enabled requires a special permission - CLUSTER.MANAGE_AUTO_START
      if ((propertyChanges != null) && !propertyChanges.isEmpty()) {
        boolean isAuthorized = (isServiceConfiguration)
            ? AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(), RoleAuthorization.SERVICE_MODIFY_CONFIGS)
            : AuthorizationHelper.isAuthorized(ResourceType.CLUSTER, cluster.getResourceId(), RoleAuthorization.CLUSTER_MODIFY_CONFIGS);
  
        if (!isAuthorized && changesToIgnore != null) {
          Set<String> relevantChangesToIgnore = changesToIgnore.get(configType);
          Map<String, String[]> relevantPropertyChanges;
  
          // If necessary remove any non-relevant property changes.
          if (relevantChangesToIgnore == null) {
            relevantPropertyChanges = propertyChanges;
          } else {
            relevantPropertyChanges = new HashMap<>(propertyChanges);
  
            for (String propertyName : relevantChangesToIgnore) {
              relevantPropertyChanges.remove(propertyName);
            }
          }
  
          // If relevant configuration changes have been made, then the user is not authorized to
          // perform the requested operation and an AuthorizationException must be thrown
          if (relevantPropertyChanges.size() == 0) {
            return;
          }
        }
        if (!isAuthorized) {
          throw new AuthorizationException(String.format("The authenticated user does not have authorization to modify %s configurations",
            (isServiceConfiguration) ? "service" : "cluster"));
        }
      }
    }
  
    /**
     * This method will delete a link between an extension version and a stack version (Extension Link).
     *
     * An extension version is like a stack version but it contains custom services.  Linking an extension
     * version to the current stack version allows the cluster to install the custom services contained in
     * the extension version.
     */
    @Override
    public void deleteExtensionLink(ExtensionLinkRequest request) throws AmbariException {
      if (request.getLinkId() == null) {
        throw new IllegalArgumentException("Link ID should be provided");
      }
      ExtensionLinkEntity linkEntity = null;
      try {
        linkEntity = linkDAO.findById(Long.parseLong(request.getLinkId()));
      } catch (RollbackException e) {
        throw new AmbariException("Unable to find extension link"
              + ", linkId=" + request.getLinkId(), e);
      }
  
      StackInfo stackInfo = ambariMetaInfo.getStack(linkEntity.getStack().getStackName(), linkEntity.getStack().getStackVersion());
  
      if (stackInfo == null) {
        throw new StackAccessException("stackName=" + linkEntity.getStack().getStackName() + ", stackVersion=" + linkEntity.getStack().getStackVersion());
      }
  
      ExtensionInfo extensionInfo = ambariMetaInfo.getExtension(linkEntity.getExtension().getExtensionName(), linkEntity.getExtension().getExtensionVersion());
  
      if (extensionInfo == null) {
        throw new StackAccessException("extensionName=" + linkEntity.getExtension().getExtensionName() + ", extensionVersion=" + linkEntity.getExtension().getExtensionVersion());
      }
  
      ExtensionHelper.validateDeleteLink(getClusters(), stackInfo, extensionInfo);
      ambariMetaInfo.getStackManager().unlinkStackAndExtension(stackInfo, extensionInfo);
  
      try {
        linkDAO.remove(linkEntity);
      } catch (RollbackException e) {
        throw new AmbariException("Unable to delete extension link"
                + ", linkId=" + request.getLinkId()
                + ", stackName=" + request.getStackName()
                + ", stackVersion=" + request.getStackVersion()
                + ", extensionName=" + request.getExtensionName()
                + ", extensionVersion=" + request.getExtensionVersion(), e);
      }
    }
  
    /**
     * This method will create a link between an extension version and a stack version (Extension Link).
     *
     * An extension version is like a stack version but it contains custom services.  Linking an extension
     * version to the current stack version allows the cluster to install the custom services contained in
     * the extension version.
     */
    @Override
    public void createExtensionLink(ExtensionLinkRequest request) throws AmbariException {
      if (StringUtils.isBlank(request.getStackName())
              || StringUtils.isBlank(request.getStackVersion())
              || StringUtils.isBlank(request.getExtensionName())
              || StringUtils.isBlank(request.getExtensionVersion())) {
  
        throw new IllegalArgumentException("Stack name, stack version, extension name and extension version should be provided");
      }
  
      StackInfo stackInfo = ambariMetaInfo.getStack(request.getStackName(), request.getStackVersion());
  
      if (stackInfo == null) {
        throw new StackAccessException("stackName=" + request.getStackName() + ", stackVersion=" + request.getStackVersion());
      }
  
      ExtensionInfo extensionInfo = ambariMetaInfo.getExtension(request.getExtensionName(), request.getExtensionVersion());
  
      if (extensionInfo == null) {
        throw new StackAccessException("extensionName=" + request.getExtensionName() + ", extensionVersion=" + request.getExtensionVersion());
      }
  
      helper.createExtensionLink(ambariMetaInfo.getStackManager(), stackInfo, extensionInfo);
    }
  
    /**
     * Update a link - switch the link's extension version while keeping the same stack version and extension name
     *
     * @throws AmbariException if we fail to link the extension to the stack
     */
    @Override
    public void updateExtensionLink(ExtensionLinkRequest request) throws AmbariException {
      if (request.getLinkId() == null) {
        throw new AmbariException("Link ID should be provided");
      }
      ExtensionLinkEntity linkEntity = null;
      try {
        linkEntity = linkDAO.findById(Long.parseLong(request.getLinkId()));
      } catch (RollbackException e) {
        throw new AmbariException("Unable to find extension link"
              + ", linkId=" + request.getLinkId(), e);
      }
      updateExtensionLink(linkEntity, request);
    }
  
    /**
     * Update a link - switch the link's extension version while keeping the same stack version and extension name
     *
     * @throws AmbariException if we fail to link the extension to the stack
     */
    @Override
    public void updateExtensionLink(ExtensionLinkEntity oldLinkEntity, ExtensionLinkRequest newLinkRequest) throws AmbariException {
      StackInfo stackInfo = ambariMetaInfo.getStack(oldLinkEntity.getStack().getStackName(), oldLinkEntity.getStack().getStackVersion());
  
      if (stackInfo == null) {
        throw new StackAccessException(String.format("stackName=%s, stackVersion=%s", oldLinkEntity.getStack().getStackName(), oldLinkEntity.getStack().getStackVersion()));
      }
  
      if (newLinkRequest.getExtensionName() == null || newLinkRequest.getExtensionVersion() == null) {
        throw new AmbariException(String.format("Invalid extension name or version: %s/%s",
  		  newLinkRequest.getExtensionName(), newLinkRequest.getExtensionVersion()));
      }
  
      if (!newLinkRequest.getExtensionName().equals(oldLinkEntity.getExtension().getExtensionName())) {
        throw new AmbariException(String.format("Update is not allowed to switch the extension name, only the version.  Old name/new name: %s/%s",
  		  oldLinkEntity.getExtension().getExtensionName(), newLinkRequest.getExtensionName()));
      }
  
      ExtensionInfo oldExtensionInfo = ambariMetaInfo.getExtension(oldLinkEntity.getExtension().getExtensionName(), oldLinkEntity.getExtension().getExtensionVersion());
      ExtensionInfo newExtensionInfo = ambariMetaInfo.getExtension(newLinkRequest.getExtensionName(), newLinkRequest.getExtensionVersion());
  
      if (oldExtensionInfo == null) {
        throw new StackAccessException(String.format("Old extensionName=%s, extensionVersion=%s", oldLinkEntity.getExtension().getExtensionName(), oldLinkEntity.getExtension().getExtensionVersion()));
      }
      if (newExtensionInfo == null) {
        throw new StackAccessException(String.format("New extensionName=%s, extensionVersion=%s", newLinkRequest.getExtensionName(), newLinkRequest.getExtensionVersion()));
      }
  
      helper.updateExtensionLink(ambariMetaInfo.getStackManager(), oldLinkEntity, stackInfo, oldExtensionInfo, newExtensionInfo);
    }
  
    @Override
    public QuickLinkVisibilityController getQuicklinkVisibilityController() {
      SettingEntity entity = settingDAO.findByName(QuickLinksProfile.SETTING_NAME_QUICKLINKS_PROFILE);
      String quickLinkProfileJson = null != entity ? entity.getContent() : null;
      return QuickLinkVisibilityControllerFactory.get(quickLinkProfileJson);
    }
  
    /**
     * Collects full metadata info about clusters for agent.
     * @return metadata info about clusters
     * @throws AmbariException
     */
    public MetadataUpdateEvent getClustersMetadata() throws AmbariException {
      TreeMap<String, MetadataCluster> metadataClusters = new TreeMap<>();
  
      for (Cluster cl : clusters.getClusters().values()) {
        StackId stackId = cl.getDesiredStackVersion();
  
        SecurityType securityType = cl.getSecurityType();
  
        MetadataCluster metadataCluster = new MetadataCluster(securityType, getMetadataServiceLevelParams(cl), true, getMetadataClusterLevelParams(cl, stackId), null);
        metadataClusters.put(Long.toString(cl.getClusterId()), metadataCluster);
      }
  
      MetadataUpdateEvent metadataUpdateEvent = new MetadataUpdateEvent(metadataClusters,
          getMetadataAmbariLevelParams(), getMetadataAgentConfigs(), UpdateEventType.CREATE);
      return metadataUpdateEvent;
    }
  
    public MetadataUpdateEvent getClusterMetadata(Cluster cl) throws AmbariException {
      final TreeMap<String, MetadataCluster> metadataClusters = new TreeMap<>();
      MetadataCluster metadataCluster = new MetadataCluster(cl.getSecurityType(), getMetadataServiceLevelParams(cl), true, getMetadataClusterLevelParams(cl, cl.getDesiredStackVersion()), null);
      metadataClusters.put(Long.toString(cl.getClusterId()), metadataCluster);
      return new MetadataUpdateEvent(metadataClusters, null, getMetadataAgentConfigs(), UpdateEventType.UPDATE);
    }
  
    @Override
    public MetadataUpdateEvent getClusterMetadataOnConfigsUpdate(Cluster cl) throws AmbariException {
      final TreeMap<String, MetadataCluster> metadataClusters = new TreeMap<>();
      metadataClusters.put(Long.toString(cl.getClusterId()), MetadataCluster.clusterLevelParamsMetadataCluster(null, getMetadataClusterLevelParams(cl, cl.getDesiredStackVersion())));
      return new MetadataUpdateEvent(metadataClusters, null, getMetadataAgentConfigs(), UpdateEventType.UPDATE);
    }
  
    public MetadataUpdateEvent getClusterMetadataOnRepoUpdate(Cluster cl) throws AmbariException {
      TreeMap<String, MetadataCluster> metadataClusters = new TreeMap<>();
      metadataClusters.put(Long.toString(cl.getClusterId()), MetadataCluster.serviceLevelParamsMetadataCluster(null, getMetadataServiceLevelParams(cl), true));
      return new MetadataUpdateEvent(metadataClusters, null, getMetadataAgentConfigs(), UpdateEventType.UPDATE);
    }
  
    public MetadataUpdateEvent getClusterMetadataOnServiceInstall(Cluster cl, String serviceName) throws AmbariException {
      return getClusterMetadataOnServiceCredentialStoreUpdate(cl, serviceName);
    }
  
    public MetadataUpdateEvent getClusterMetadataOnServiceCredentialStoreUpdate(Cluster cl, String serviceName) throws AmbariException {
      final TreeMap<String, MetadataCluster> metadataClusters = new TreeMap<>();
      metadataClusters.put(Long.toString(cl.getClusterId()), MetadataCluster.serviceLevelParamsMetadataCluster(null, getMetadataServiceLevelParams(cl), false));
      return new MetadataUpdateEvent(metadataClusters, null, getMetadataAgentConfigs(), UpdateEventType.UPDATE);
    }
  
    private String getClientsToUpdateConfigs(ComponentInfo componentInfo) {
      List<String> clientsToUpdateConfigsList = componentInfo.getClientsToUpdateConfigs();
      if (clientsToUpdateConfigsList == null) {
        clientsToUpdateConfigsList = new ArrayList<>();
        clientsToUpdateConfigsList.add("*");
      }
      return gson.toJson(clientsToUpdateConfigsList);
    }
  
    private Boolean getUnlimitedKeyJCERequirement(ComponentInfo componentInfo, SecurityType clusterSecurityType) {
      UnlimitedKeyJCERequirement unlimitedKeyJCERequirement = componentInfo.getUnlimitedKeyJCERequired();
      // Ensure that the unlimited key requirement is set. If null, the default value should be used.
      if(unlimitedKeyJCERequirement == null) {
        unlimitedKeyJCERequirement = UnlimitedKeyJCERequirement.DEFAULT;
      }
  
      return (UnlimitedKeyJCERequirement.ALWAYS == unlimitedKeyJCERequirement) ||
          ((UnlimitedKeyJCERequirement.KERBEROS_ENABLED == unlimitedKeyJCERequirement) &&
              (clusterSecurityType == SecurityType.KERBEROS));
  
    }
  
    //TODO will be a need to change to multi-instance usage
    public TreeMap<String, String> getTopologyComponentLevelParams(Long clusterId, String serviceName, String componentName,
                                                               SecurityType clusterSecurityType) throws AmbariException {
  
      TreeMap<String, String> statusCommandParams = new TreeMap<>();
      RepositoryVersionEntity repositoryVersion = getComponentRepositoryVersion(clusterId, serviceName, componentName);
      if (null != repositoryVersion) {
        StackId stackId = repositoryVersion.getStackId();
        ComponentInfo componentInfo = ambariMetaInfo.getComponent(
            stackId.getStackName(), stackId.getStackVersion(),
            serviceName, componentName);
  
        statusCommandParams.put(ExecutionCommand.KeyNames.CLIENTS_TO_UPDATE_CONFIGS,
            getClientsToUpdateConfigs(componentInfo));
  
        // If we are starting a component, calculate whether the unlimited key JCE policy is
        // required for the relevant host.  One of the following indicates that the unlimited
        // key JCE policy is required:
        //
        //   * The component explicitly requires it whether Kerberos is enabled or not (example, SMARTSENSE/HST_SERVER)
        //   * The component explicitly requires it only when Kerberos is enabled AND Kerberos is enabled (example, most components)
        //
        // Set/update the unlimited_key_jce_required value as needed
        statusCommandParams.put(ExecutionCommand.KeyNames.UNLIMITED_KEY_JCE_REQUIRED,
            Boolean.toString(getUnlimitedKeyJCERequirement(componentInfo, clusterSecurityType)));
      }
      return statusCommandParams;
    }
  
    @Override
    public Map<String, BlueprintProvisioningState> getBlueprintProvisioningStates(Long clusterId, Long hostId)
        throws AmbariException {
      Map<String, BlueprintProvisioningState> blueprintProvisioningStates = new HashMap<>();
      Host host = clusters.getHostById(hostId);
      Cluster cl = clusters.getCluster(clusterId);
      for (ServiceComponentHost sch : cl.getServiceComponentHosts(host.getHostName())) {
        if (!sch.isClientComponent()) {
          blueprintProvisioningStates.put(sch.getServiceComponentName(),
              sch.getDesiredStateEntity().getBlueprintProvisioningState());
        }
      }
      return blueprintProvisioningStates;
    }
  
    //TODO will be a need to change to multi-instance usage
    public TreeMap<String, String> getTopologyCommandParams(Long clusterId, String serviceName, String componentName, ServiceComponentHost sch) throws AmbariException {
      TreeMap<String, String> commandParams = new TreeMap<>();
      RepositoryVersionEntity repositoryVersion = getComponentRepositoryVersion(clusterId, serviceName, componentName);
      if (null != repositoryVersion) {
        StackId stackId = repositoryVersion.getStackId();
        ServiceInfo serviceInfo = ambariMetaInfo.getService(stackId.getStackName(),
            stackId.getStackVersion(), serviceName);
        ComponentInfo componentInfo = ambariMetaInfo.getComponent(
            stackId.getStackName(), stackId.getStackVersion(),
            serviceName, componentName);
  
        commandParams.put(SERVICE_PACKAGE_FOLDER, serviceInfo.getServicePackageFolder());
        String scriptName = null;
        String scriptCommandTimeout = "";
        CommandScriptDefinition script = componentInfo.getCommandScript();
        if (serviceInfo.getSchemaVersion().equals(AmbariMetaInfo.SCHEMA_VERSION_2)) {
          if (script != null) {
            scriptName = script.getScript();
            if (script.getTimeout() > 0) {
              scriptCommandTimeout = String.valueOf(script.getTimeout());
            }
          } else {
            String message = String.format("Component %s of service %s has not " +
                "command script defined", componentName, serviceName);
            throw new AmbariException(message);
          }
        }
        String agentDefaultCommandTimeout = configs.getDefaultAgentTaskTimeout(false);
        String actualTimeout = (!scriptCommandTimeout.equals("") ? scriptCommandTimeout : agentDefaultCommandTimeout);
  
        commandParams.put(COMMAND_TIMEOUT, actualTimeout);
        commandParams.put(SCRIPT, scriptName);
        commandParams.put(SCRIPT_TYPE, script.getScriptType().toString());
      }
      String schVersion = sch.getVersion();
      if (!schVersion.equals("UNKNOWN")) {
        commandParams.put(VERSION, schVersion);
      }
      return commandParams;
    }
  
    private RepositoryVersionEntity getComponentRepositoryVersion(Long clusterId, String serviceName, String componentName) throws AmbariException {
      RepositoryVersionEntity repositoryVersion = null;
      if (!StringUtils.isEmpty(serviceName)) {
        Service service = clusters.getCluster(clusterId).getService(serviceName);
        if (null != service) {
          repositoryVersion = service.getDesiredRepositoryVersion();
  
          if (!StringUtils.isEmpty(componentName) && service.getServiceComponents().containsKey(componentName)) {
            ServiceComponent serviceComponent = service.getServiceComponent(componentName);
            if (null != serviceComponent) {
              repositoryVersion = serviceComponent.getDesiredRepositoryVersion();
            }
          }
        }
      }
      return repositoryVersion;
    }
  
    public TreeMap<String, String> getMetadataClusterLevelParams(Cluster cluster, StackId stackId) throws AmbariException {
      TreeMap<String, String> clusterLevelParams = new TreeMap<>();
      clusterLevelParams.put(STACK_NAME, stackId.getStackName());
      clusterLevelParams.put(STACK_VERSION, stackId.getStackVersion());
  
      clusterLevelParams.putAll(getMetadataClusterLevelConfigsParams(cluster, stackId));
      clusterLevelParams.put(CLUSTER_NAME, cluster.getClusterName());
      clusterLevelParams.put(HOOKS_FOLDER, configs.getProperty(Configuration.HOOKS_FOLDER));
  
      return clusterLevelParams;
    }
  
    private TreeMap<String, String> getMetadataClusterLevelConfigsParams(Cluster cluster, StackId stackId) throws AmbariException {
      TreeMap<String, String> clusterLevelParams = new TreeMap<>();
  
      Map<String, DesiredConfig> desiredConfigs = cluster.getDesiredConfigs(false);
      if (MapUtils.isNotEmpty(desiredConfigs)) {
  
        Set<String> userSet = configHelper.getPropertyValuesWithPropertyType(stackId, PropertyType.USER, cluster, desiredConfigs);
        String userList = gson.toJson(userSet);
        clusterLevelParams.put(USER_LIST, userList);
  
        //Create a user_group mapping and send it as part of the hostLevelParams
        Map<String, Set<String>> userGroupsMap = configHelper.createUserGroupsMap(
            stackId, cluster, desiredConfigs);
        String userGroups = gson.toJson(userGroupsMap);
        clusterLevelParams.put(USER_GROUPS, userGroups);
  
        Set<String> groupSet = configHelper.getPropertyValuesWithPropertyType(stackId, PropertyType.GROUP, cluster, desiredConfigs);
        String groupList = gson.toJson(groupSet);
        clusterLevelParams.put(GROUP_LIST, groupList);
      }
      Set<String> notManagedHdfsPathSet = configHelper.getPropertyValuesWithPropertyType(stackId,
          PropertyType.NOT_MANAGED_HDFS_PATH, cluster, desiredConfigs);
      String notManagedHdfsPathList = gson.toJson(notManagedHdfsPathSet);
      clusterLevelParams.put(NOT_MANAGED_HDFS_PATH_LIST, notManagedHdfsPathList);
  
      for (Service service : cluster.getServices().values()) {
        ServiceInfo serviceInfoInstance = ambariMetaInfo.getService(service);
        String serviceType = serviceInfoInstance.getServiceType();
        if (serviceType != null) {
          LOG.debug("Adding {} to command parameters for {}", serviceInfoInstance.getServiceType(),
            serviceInfoInstance.getName());
          clusterLevelParams.put(DFS_TYPE, serviceInfoInstance.getServiceType());
          break;
        }
      }
  
      return clusterLevelParams;
    }
  
    public TreeMap<String, MetadataServiceInfo> getMetadataServiceLevelParams(Cluster cluster) throws AmbariException {
      TreeMap<String, MetadataServiceInfo> serviceLevelParams = new TreeMap<>();
      for (Map.Entry<String, Service> serviceEntry : cluster.getServices().entrySet()) {
        Service service = serviceEntry.getValue();
        serviceLevelParams.putAll(getMetadataServiceLevelParams(service));
      }
      return serviceLevelParams;
    }
  
    public TreeMap<String, MetadataServiceInfo> getMetadataServiceLevelParams(Service service) throws AmbariException {
      TreeMap<String, MetadataServiceInfo> serviceLevelParams = new TreeMap<>();
  
      RepositoryVersionEntity repositoryVersion = service.getDesiredRepositoryVersion();
  
      if (null != repositoryVersion) {
        StackId serviceStackId = repositoryVersion.getStackId();
  
        ServiceInfo serviceInfo = ambariMetaInfo.getService(serviceStackId.getStackName(),
            serviceStackId.getStackVersion(), service.getName());
        Long statusCommandTimeout = null;
        if (serviceInfo.getCommandScript() != null) {
          statusCommandTimeout = new Long(customCommandExecutionHelper.getStatusCommandTimeout(serviceInfo));
        }
  
        String servicePackageFolder = serviceInfo.getServicePackageFolder();
  
        // Get the map of service config type to password properties for the service
        Map<String, Map<String, String>> configCredentials;
        configCredentials = configCredentialsForService.get(service.getName());
        if (configCredentials == null) {
          configCredentials = configHelper.getCredentialStoreEnabledProperties(serviceStackId, service);
          configCredentialsForService.put(service.getName(), configCredentials);
        }
  
        serviceLevelParams.put(serviceInfo.getName(),
            new MetadataServiceInfo(serviceInfo.getVersion(),
                service.isCredentialStoreEnabled(),
                configCredentials,
                statusCommandTimeout,
                servicePackageFolder));
      }
      return serviceLevelParams;
    }
  
    public TreeMap<String, String> getMetadataAmbariLevelParams() {
      TreeMap<String, String> clusterLevelParams = new TreeMap<>();
      clusterLevelParams.put(JDK_LOCATION, getJdkResourceUrl());
      clusterLevelParams.put(JAVA_HOME, getJavaHome());
      clusterLevelParams.put(AMBARI_JAVA_HOME, getAmbariJavaHome());
      clusterLevelParams.put(JAVA_VERSION, String.valueOf(configs.getJavaVersion()));
      clusterLevelParams.put(JDK_NAME, getJDKName());
      clusterLevelParams.put(JCE_NAME, getJCEName());
      clusterLevelParams.put(DB_NAME, getServerDB());
      clusterLevelParams.put(MYSQL_JDBC_URL, getMysqljdbcUrl());
      clusterLevelParams.put(ORACLE_JDBC_URL, getOjdbcUrl());
      clusterLevelParams.put(DB_DRIVER_FILENAME, configs.getMySQLJarName());
      clusterLevelParams.putAll(getRcaParameters());
      clusterLevelParams.put(HOST_SYS_PREPPED, configs.areHostsSysPrepped());
      clusterLevelParams.put(AGENT_STACK_RETRY_ON_UNAVAILABILITY, configs.isAgentStackRetryOnInstallEnabled());
      clusterLevelParams.put(AGENT_STACK_RETRY_COUNT, configs.getAgentStackRetryOnInstallCount());
  
      boolean serverUseSsl = configs.getApiSSLAuthentication();
      int port = serverUseSsl ? configs.getClientSSLApiPort() : configs.getClientApiPort();
      clusterLevelParams.put(AMBARI_SERVER_HOST, StageUtils.getHostName());
      clusterLevelParams.put(AMBARI_SERVER_PORT, Integer.toString(port));
      clusterLevelParams.put(AMBARI_SERVER_USE_SSL, Boolean.toString(serverUseSsl));
  
      for (Map.Entry<String, String> dbConnectorName : configs.getDatabaseConnectorNames().entrySet()) {
        clusterLevelParams.put(dbConnectorName.getKey(), dbConnectorName.getValue());
      }
      for (Map.Entry<String, String> previousDBConnectorName : configs.getPreviousDatabaseConnectorNames().entrySet()) {
        clusterLevelParams.put(previousDBConnectorName.getKey(), previousDBConnectorName.getValue());
      }
      clusterLevelParams.put(GPL_LICENSE_ACCEPTED, configs.getGplLicenseAccepted().toString());
  
      return clusterLevelParams;
    }
  
    public SortedMap<String, SortedMap<String,String>> getMetadataAgentConfigs() {
      SortedMap<String, SortedMap<String,String>> agentConfigs = new TreeMap<>();
      Map<String, Map<String,String>> agentConfigsMap = configs.getAgentConfigsMap();
  
      for (String key : agentConfigsMap.keySet()) {
        agentConfigs.put(key, new TreeMap<>(agentConfigsMap.get(key)));
      }
  
      return agentConfigs;
    }
  }
  