/**
* 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.topology;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.api.services.stackadvisor.StackAdvisorBlueprintProcessor;
import org.apache.ambari.server.controller.ClusterRequest;
import org.apache.ambari.server.controller.ConfigurationRequest;
import org.apache.ambari.server.controller.internal.BlueprintConfigurationProcessor;
import org.apache.ambari.server.controller.internal.ClusterResourceProvider;
import org.apache.ambari.server.controller.internal.ConfigurationTopologyException;
import org.apache.ambari.server.controller.internal.Stack;
import org.apache.ambari.server.serveraction.kerberos.KerberosInvalidConfigurationException;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.SecurityType;
import org.apache.ambari.server.utils.StageUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Responsible for cluster configuration.
*/
public class ClusterConfigurationRequest {
protected final static Logger LOG = LoggerFactory.getLogger(ClusterConfigurationRequest.class);
/**
* a regular expression Pattern used to find "clusterHostInfo.(component_name)_host" placeholders in strings
*/
private static final Pattern CLUSTER_HOST_INFO_PATTERN_VARIABLE = Pattern.compile("\\$\\{clusterHostInfo/?([\\w\\-\\.]+)_host(?:\\s*\\|\\s*(.+?))?\\}");
public static final String CLUSTER_HOST_INFO = "clusterHostInfo";
private AmbariContext ambariContext;
private ClusterTopology clusterTopology;
private BlueprintConfigurationProcessor configurationProcessor;
private StackAdvisorBlueprintProcessor stackAdvisorBlueprintProcessor;
private Stack stack;
private boolean configureSecurity = false;
public ClusterConfigurationRequest(AmbariContext ambariContext, ClusterTopology topology, boolean setInitial, StackAdvisorBlueprintProcessor stackAdvisorBlueprintProcessor, boolean configureSecurity) {
this(ambariContext, topology, setInitial, stackAdvisorBlueprintProcessor);
this.configureSecurity = configureSecurity;
}
public ClusterConfigurationRequest(AmbariContext ambariContext, ClusterTopology clusterTopology, boolean setInitial,
StackAdvisorBlueprintProcessor stackAdvisorBlueprintProcessor) {
this.ambariContext = ambariContext;
this.clusterTopology = clusterTopology;
Blueprint blueprint = clusterTopology.getBlueprint();
this.stack = blueprint.getStack();
// set initial configuration (not topology resolved)
this.configurationProcessor = new BlueprintConfigurationProcessor(clusterTopology);
this.stackAdvisorBlueprintProcessor = stackAdvisorBlueprintProcessor;
removeOrphanConfigTypes();
if (setInitial) {
setConfigurationsOnCluster(clusterTopology, TopologyManager.INITIAL_CONFIG_TAG, Collections.<String>emptySet());
}
}
/**
* Remove config-types from the given configuration if there is no any services related to them (except cluster-env and global).
*/
private void removeOrphanConfigTypes(Configuration configuration) {
Blueprint blueprint = clusterTopology.getBlueprint();
Collection<String> configTypes = configuration.getAllConfigTypes();
for (String configType : configTypes) {
if (!blueprint.isValidConfigType(configType)) {
configuration.removeConfigType(configType);
LOG.info("Removing config type '{}' as related service is not present in either Blueprint or cluster creation template.", configType);
}
}
}
/**
* Remove config-types, if there is no any services related to them (except cluster-env and global).
*/
private void removeOrphanConfigTypes() {
Configuration configuration = clusterTopology.getConfiguration();
removeOrphanConfigTypes(configuration);
Map<String, HostGroupInfo> hostGroupInfoMap = clusterTopology.getHostGroupInfo();
if (MapUtils.isNotEmpty(hostGroupInfoMap)) {
for (Map.Entry<String, HostGroupInfo> hostGroupInfo : hostGroupInfoMap.entrySet()) {
configuration = hostGroupInfo.getValue().getConfiguration();
if (configuration != null) {
removeOrphanConfigTypes(configuration);
}
}
}
}
// get names of required host groups
public Collection<String> getRequiredHostGroups() {
Collection<String> requiredHostGroups = new HashSet<>();
requiredHostGroups.addAll(configurationProcessor.getRequiredHostGroups());
if (configureSecurity) {
requiredHostGroups.addAll(getRequiredHostgroupsForKerberosConfiguration());
}
return requiredHostGroups;
}
public void process() throws AmbariException, ConfigurationTopologyException {
// this will update the topo cluster config and all host group configs in the cluster topology
Set<String> updatedConfigTypes = new HashSet<>();
Map<String, Map<String, String>> userProvidedConfigurations = clusterTopology.getConfiguration().getFullProperties(1);
try {
if (configureSecurity) {
Configuration clusterConfiguration = clusterTopology.getConfiguration();
Map<String, Map<String, String>> existingConfigurations = clusterConfiguration.getFullProperties();
updatedConfigTypes.addAll(configureKerberos(clusterConfiguration, existingConfigurations));
}
// obtain recommended configurations before config updates
if (!ConfigRecommendationStrategy.NEVER_APPLY.equals(this.clusterTopology.getConfigRecommendationStrategy())) {
// get merged properties form Blueprint & cluster template (this doesn't contains stack default values)
stackAdvisorBlueprintProcessor.adviseConfiguration(this.clusterTopology, userProvidedConfigurations);
}
updatedConfigTypes.addAll(configurationProcessor.doUpdateForClusterCreate());
} catch (ConfigurationTopologyException e) {
//log and continue to set configs on cluster to make progress
LOG.error("An exception occurred while doing configuration topology update: " + e, e);
}
setConfigurationsOnCluster(clusterTopology, TopologyManager.TOPOLOGY_RESOLVED_TAG, updatedConfigTypes);
}
private Set<String> configureKerberos(Configuration clusterConfiguration, Map<String, Map<String, String>> existingConfigurations) throws AmbariException {
Set<String> updatedConfigTypes = new HashSet<>();
Cluster cluster = getCluster();
Blueprint blueprint = clusterTopology.getBlueprint();
Configuration stackDefaults = blueprint.getStack().getConfiguration(blueprint.getServices());
Map<String, Map<String, String>> stackDefaultProps = stackDefaults.getProperties();
// add clusterHostInfo containing components to hosts map, based on Topology, to use this one instead of
// StageUtils.getClusterInfo()
Map<String, String> componentHostsMap = createComponentHostMap(blueprint);
existingConfigurations.put("clusterHostInfo", componentHostsMap);
try {
// generate principals & keytabs for headless identities
AmbariContext.getController().getKerberosHelper()
.ensureHeadlessIdentities(cluster, existingConfigurations,
new HashSet<>(blueprint.getServices()));
// apply Kerberos specific configurations
Map<String, Map<String, String>> updatedConfigs = AmbariContext.getController().getKerberosHelper()
.getServiceConfigurationUpdates(cluster, existingConfigurations,
createServiceComponentMap(blueprint), null, null, true, false);
// ******************************************************************************************
// Since Kerberos is being enabled, make sure the cluster-env/security_enabled property is
// set to "true"
Map<String, String> clusterEnv = updatedConfigs.get("cluster-env");
if(clusterEnv == null) {
clusterEnv = new HashMap<>();
updatedConfigs.put("cluster-env", clusterEnv);
}
clusterEnv.put("security_enabled", "true");
// ******************************************************************************************
for (String configType : updatedConfigs.keySet()) {
// apply only if config type has related services in Blueprint
if (blueprint.isValidConfigType(configType)) {
Map<String, String> propertyMap = updatedConfigs.get(configType);
Map<String, String> clusterConfigProperties = existingConfigurations.get(configType);
Map<String, String> stackDefaultConfigProperties = stackDefaultProps.get(configType);
for (String property : propertyMap.keySet()) {
// update value only if property value configured in Blueprint / ClusterTemplate is not a custom one
String currentValue = clusterConfiguration.getPropertyValue(configType, property);
String newValue = propertyMap.get(property);
if (!propertyHasCustomValue(clusterConfigProperties, stackDefaultConfigProperties, property) &&
(currentValue == null || !currentValue.equals(newValue))) {
LOG.debug("Update Kerberos related config property: {} {} {}", configType, property, propertyMap.get
(property));
clusterConfiguration.setProperty(configType, property, newValue);
updatedConfigTypes.add(configType);
}
}
}
}
} catch (KerberosInvalidConfigurationException e) {
LOG.error("An exception occurred while doing Kerberos related configuration update: " + e, e);
}
return updatedConfigTypes;
}
/**
* Create a map of services and the relevant components that are specified in the Blueprint
*
* @param blueprint the blueprint
* @return a map of service names to component names
*/
private Map<String, Set<String>> createServiceComponentMap(Blueprint blueprint) {
Map<String, Set<String>> serviceComponents = new HashMap<>();
Collection<String> services = blueprint.getServices();
if(services != null) {
for (String service : services) {
Collection<String> components = blueprint.getComponents(service);
serviceComponents.put(service,
(components == null)
? Collections.<String>emptySet()
: new HashSet<>(blueprint.getComponents(service)));
}
}
return serviceComponents;
}
/**
* Returns true if the property exists in clusterConfigProperties and has a custom user defined value. Property has
* custom value in case we there's no stack default value for it or it's not equal to stack default value.
* @param clusterConfigProperties
* @param stackDefaultConfigProperties
* @param property
* @return
*/
private boolean propertyHasCustomValue(Map<String, String> clusterConfigProperties, Map<String, String>
stackDefaultConfigProperties, String property) {
boolean propertyHasCustomValue = false;
if (clusterConfigProperties != null) {
String propertyValue = clusterConfigProperties.get(property);
if (propertyValue != null) {
if (stackDefaultConfigProperties != null) {
String stackDefaultValue = stackDefaultConfigProperties.get(property);
if (stackDefaultValue != null) {
propertyHasCustomValue = !propertyValue.equals(stackDefaultValue);
} else {
propertyHasCustomValue = true;
}
} else {
propertyHasCustomValue = true;
}
}
}
return propertyHasCustomValue;
}
private Map<String, String> createComponentHostMap(Blueprint blueprint) {
Map<String, String> componentHostsMap = new HashMap<>();
for (String service : blueprint.getServices()) {
Collection<String> components = blueprint.getComponents(service);
for (String component : components) {
Collection<String> componentHost = clusterTopology.getHostAssignmentsForComponent(component);
// retrieve corresponding clusterInfoKey for component using StageUtils
String clusterInfoKey = StageUtils.getComponentToClusterInfoKeyMap().get(component);
if (clusterInfoKey == null) {
clusterInfoKey = component.toLowerCase() + "_hosts";
}
componentHostsMap.put(clusterInfoKey, StringUtils.join(componentHost, ","));
}
}
return componentHostsMap;
}
private Collection<String> getRequiredHostgroupsForKerberosConfiguration() {
Collection<String> requiredHostGroups = new HashSet<>();
try {
Cluster cluster = getCluster();
Blueprint blueprint = clusterTopology.getBlueprint();
Configuration clusterConfiguration = clusterTopology.getConfiguration();
Map<String, Map<String, String>> existingConfigurations = clusterConfiguration.getFullProperties();
existingConfigurations.put(CLUSTER_HOST_INFO, new HashMap<String, String>());
// apply Kerberos specific configurations
Map<String, Map<String, String>> updatedConfigs = AmbariContext.getController().getKerberosHelper()
.getServiceConfigurationUpdates(cluster, existingConfigurations,
createServiceComponentMap(blueprint), null, null, true, false);
// retrieve hostgroup for component names extracted from variables like "{clusterHostInfo.(component_name)
// _host}"
for (String configType : updatedConfigs.keySet()) {
Map<String, String> propertyMap = updatedConfigs.get(configType);
for (String property : propertyMap.keySet()) {
String propertyValue = propertyMap.get(property);
Matcher matcher = CLUSTER_HOST_INFO_PATTERN_VARIABLE.matcher(propertyValue);
while (matcher.find()) {
String component = matcher.group(1).toUpperCase();
Collection<String> hostGroups = clusterTopology.getHostGroupsForComponent(component);
if (hostGroups.isEmpty()) {
LOG.warn("No matching hostgroup found for component: {} specified in Kerberos config type: {} property:" +
" " +
"{}", component, configType, property);
} else {
requiredHostGroups.addAll(hostGroups);
}
}
}
}
} catch (KerberosInvalidConfigurationException e) {
LOG.error("An exception occurred while doing Kerberos related configuration update: " + e, e);
} catch (AmbariException e) {
LOG.error("An exception occurred while doing Kerberos related configuration update: " + e, e);
}
return requiredHostGroups;
}
private Cluster getCluster() throws AmbariException {
String clusterName = ambariContext.getClusterName(clusterTopology.getClusterId());
return AmbariContext.getController().getClusters().getCluster(clusterName);
}
/**
* Set all configurations on the cluster resource.
* @param clusterTopology cluster topology
* @param tag config tag
*/
public void setConfigurationsOnCluster(ClusterTopology clusterTopology, String tag, Set<String> updatedConfigTypes) {
//todo: also handle setting of host group scoped configuration which is updated by config processor
List<BlueprintServiceConfigRequest> configurationRequests = new LinkedList<>();
Blueprint blueprint = clusterTopology.getBlueprint();
Configuration clusterConfiguration = clusterTopology.getConfiguration();
for (String service : blueprint.getServices()) {
//todo: remove intermediate request type
// one bp config request per service
BlueprintServiceConfigRequest blueprintConfigRequest = new BlueprintServiceConfigRequest(service);
for (String serviceConfigType : stack.getAllConfigurationTypes(service)) {
Set<String> excludedConfigTypes = stack.getExcludedConfigurationTypes(service);
if (!excludedConfigTypes.contains(serviceConfigType)) {
// skip handling of cluster-env here
if (! serviceConfigType.equals("cluster-env")) {
if (clusterConfiguration.getFullProperties().containsKey(serviceConfigType)) {
blueprintConfigRequest.addConfigElement(serviceConfigType,
clusterConfiguration.getFullProperties().get(serviceConfigType),
clusterConfiguration.getFullAttributes().get(serviceConfigType));
}
}
}
}
configurationRequests.add(blueprintConfigRequest);
}
// since the stack returns "cluster-env" with each service's config ensure that only one
// ClusterRequest occurs for the global cluster-env configuration
BlueprintServiceConfigRequest globalConfigRequest = new BlueprintServiceConfigRequest("GLOBAL-CONFIG");
Map<String, String> clusterEnvProps = clusterConfiguration.getFullProperties().get("cluster-env");
Map<String, Map<String, String>> clusterEnvAttributes = clusterConfiguration.getFullAttributes().get("cluster-env");
globalConfigRequest.addConfigElement("cluster-env", clusterEnvProps,clusterEnvAttributes);
configurationRequests.add(globalConfigRequest);
setConfigurationsOnCluster(configurationRequests, tag, updatedConfigTypes);
}
/**
* Creates a ClusterRequest for each service that
* includes any associated config types and configuration. The Blueprints
* implementation will now create one ClusterRequest per service, in order
* to comply with the ServiceConfigVersioning framework in Ambari.
*
* This method will also send these requests to the management controller.
*
* @param configurationRequests a list of requests to send to the AmbariManagementController.
*/
private void setConfigurationsOnCluster(List<BlueprintServiceConfigRequest> configurationRequests,
String tag, Set<String> updatedConfigTypes) {
String clusterName = null;
try {
clusterName = ambariContext.getClusterName(clusterTopology.getClusterId());
} catch (AmbariException e) {
LOG.error("Cannot get cluster name for clusterId = " + clusterTopology.getClusterId(), e);
throw new RuntimeException(e);
}
// iterate over services to deploy
for (BlueprintServiceConfigRequest blueprintConfigRequest : configurationRequests) {
ClusterRequest clusterRequest = null;
// iterate over the config types associated with this service
List<ConfigurationRequest> requestsPerService = new LinkedList<>();
for (BlueprintServiceConfigElement blueprintElement : blueprintConfigRequest.getConfigElements()) {
Map<String, Object> clusterProperties = new HashMap<>();
clusterProperties.put(ClusterResourceProvider.CLUSTER_NAME_PROPERTY_ID, clusterName);
clusterProperties.put(ClusterResourceProvider.CLUSTER_DESIRED_CONFIGS_PROPERTY_ID + "/type", blueprintElement.getTypeName());
clusterProperties.put(ClusterResourceProvider.CLUSTER_DESIRED_CONFIGS_PROPERTY_ID + "/tag", tag);
for (Map.Entry<String, String> entry : blueprintElement.getConfiguration().entrySet()) {
clusterProperties.put(ClusterResourceProvider.CLUSTER_DESIRED_CONFIGS_PROPERTY_ID +
"/properties/" + entry.getKey(), entry.getValue());
}
if (blueprintElement.getAttributes() != null) {
for (Map.Entry<String, Map<String, String>> attribute : blueprintElement.getAttributes().entrySet()) {
String attributeName = attribute.getKey();
for (Map.Entry<String, String> attributeOccurrence : attribute.getValue().entrySet()) {
clusterProperties.put(ClusterResourceProvider.CLUSTER_DESIRED_CONFIGS_PROPERTY_ID + "/properties_attributes/"
+ attributeName + "/" + attributeOccurrence.getKey(), attributeOccurrence.getValue());
}
}
}
// only create one cluster request per service, which includes
// all the configuration types for that service
if (clusterRequest == null) {
SecurityType securityType;
String requestedSecurityType = (String) clusterProperties.get(
ClusterResourceProvider.CLUSTER_SECURITY_TYPE_PROPERTY_ID);
if(requestedSecurityType == null)
securityType = null;
else {
try {
securityType = SecurityType.valueOf(requestedSecurityType.toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(String.format(
"Cannot set cluster security type to invalid value: %s", requestedSecurityType));
}
}
clusterRequest = new ClusterRequest(
(Long) clusterProperties.get(ClusterResourceProvider.CLUSTER_ID_PROPERTY_ID),
(String) clusterProperties.get(ClusterResourceProvider.CLUSTER_NAME_PROPERTY_ID),
(String) clusterProperties.get(ClusterResourceProvider.CLUSTER_PROVISIONING_STATE_PROPERTY_ID),
securityType,
(String) clusterProperties.get(ClusterResourceProvider.CLUSTER_VERSION_PROPERTY_ID),
null);
}
List<ConfigurationRequest> listOfRequests = ambariContext.createConfigurationRequests(clusterProperties);
requestsPerService.addAll(listOfRequests);
}
// set total list of config requests, including all config types for this service
if (clusterRequest != null) {
clusterRequest.setDesiredConfig(requestsPerService);
LOG.info("Sending cluster config update request for service = " + blueprintConfigRequest.getServiceName());
ambariContext.setConfigurationOnCluster(clusterRequest);
} else {
LOG.error("ClusterRequest should not be null for service = " + blueprintConfigRequest.getServiceName());
}
}
if (tag.equals(TopologyManager.TOPOLOGY_RESOLVED_TAG)) {
// if this is a request to resolve config, then wait until resolution is completed
try {
// wait until the cluster topology configuration is set/resolved
ambariContext.waitForConfigurationResolution(clusterName, updatedConfigTypes);
} catch (AmbariException e) {
LOG.error("Error while attempting to wait for the cluster configuration to reach TOPOLOGY_RESOLVED state.", e);
}
}
}
/**
* Internal class meant to represent the collection of configuration
* items and configuration attributes that are associated with a given service.
*
* This class is used to support proper configuration versioning when
* Ambari Blueprints is used to deploy a cluster.
*/
private static class BlueprintServiceConfigRequest {
private final String serviceName;
private List<BlueprintServiceConfigElement> configElements =
new LinkedList<>();
BlueprintServiceConfigRequest(String serviceName) {
this.serviceName = serviceName;
}
void addConfigElement(String type, Map<String, String> props, Map<String, Map<String, String>> attributes) {
if (props == null) {
props = Collections.emptyMap();
}
if (attributes == null) {
attributes = Collections.emptyMap();
}
configElements.add(new BlueprintServiceConfigElement(type, props, attributes));
}
public String getServiceName() {
return serviceName;
}
List<BlueprintServiceConfigElement> getConfigElements() {
return configElements;
}
}
/**
* Internal class that represents the configuration
* and attributes for a given configuration type.
*/
private static class BlueprintServiceConfigElement {
private final String typeName;
private final Map<String, String> configuration;
private final Map<String, Map<String, String>> attributes;
BlueprintServiceConfigElement(String type, Map<String, String> props, Map<String, Map<String, String>> attributes) {
this.typeName = type;
this.configuration = props;
this.attributes = attributes;
}
public String getTypeName() {
return typeName;
}
public Map<String, String> getConfiguration() {
return configuration;
}
public Map<String, Map<String, String>> getAttributes() {
return attributes;
}
}
}