/**
* 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 distribut
* ed 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 static org.apache.ambari.server.controller.internal.ProvisionAction.INSTALL_AND_START;
import static org.apache.ambari.server.controller.internal.ProvisionAction.INSTALL_ONLY;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.controller.RequestStatusResponse;
import org.apache.ambari.server.controller.internal.ProvisionAction;
import org.apache.ambari.server.controller.internal.ProvisionClusterRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents a cluster topology.
* Topology includes the the associated blueprint, cluster configuration and hostgroup -> host mapping.
*/
public class ClusterTopologyImpl implements ClusterTopology {
private Long clusterId;
//todo: currently topology is only associated with a single bp
//todo: this will need to change to allow usage of multiple bp's for the same cluster
//todo: for example: provision using bp1 and scale using bp2
private Blueprint blueprint;
private Configuration configuration;
private ConfigRecommendationStrategy configRecommendationStrategy;
private ProvisionAction provisionAction = ProvisionAction.INSTALL_AND_START;
private Map<String, AdvisedConfiguration> advisedConfigurations = new HashMap<>();
private final Map<String, HostGroupInfo> hostGroupInfoMap = new HashMap<>();
private final AmbariContext ambariContext;
private final String defaultPassword;
private final static Logger LOG = LoggerFactory.getLogger(ClusterTopologyImpl.class);
//todo: will need to convert all usages of hostgroup name to use fully qualified name (BP/HG)
//todo: for now, restrict scaling to the same BP
public ClusterTopologyImpl(AmbariContext ambariContext, TopologyRequest topologyRequest) throws InvalidTopologyException {
this.clusterId = topologyRequest.getClusterId();
// provision cluster currently requires that all hostgroups have same BP so it is ok to use root level BP here
this.blueprint = topologyRequest.getBlueprint();
this.configuration = topologyRequest.getConfiguration();
if (topologyRequest instanceof ProvisionClusterRequest) {
this.defaultPassword = ((ProvisionClusterRequest) topologyRequest).getDefaultPassword();
} else {
this.defaultPassword = null;
}
registerHostGroupInfo(topologyRequest.getHostGroupInfo());
// todo extract validation to specialized service
validateTopology();
this.ambariContext = ambariContext;
}
@Override
public void update(TopologyRequest topologyRequest) throws InvalidTopologyException {
registerHostGroupInfo(topologyRequest.getHostGroupInfo());
}
@Override
public Long getClusterId() {
return clusterId;
}
@Override
public void setClusterId(Long clusterId) {
this.clusterId = clusterId;
}
@Override
public Blueprint getBlueprint() {
return blueprint;
}
@Override
public Configuration getConfiguration() {
return configuration;
}
@Override
public Map<String, HostGroupInfo> getHostGroupInfo() {
return hostGroupInfoMap;
}
//todo: do we want to return groups with no requested hosts?
@Override
public Collection<String> getHostGroupsForComponent(String component) {
Collection<String> resultGroups = new ArrayList<>();
for (HostGroup group : getBlueprint().getHostGroups().values() ) {
if (group.getComponentNames().contains(component)) {
resultGroups.add(group.getName());
}
}
return resultGroups;
}
@Override
public String getHostGroupForHost(String hostname) {
for (HostGroupInfo groupInfo : hostGroupInfoMap.values() ) {
if (groupInfo.getHostNames().contains(hostname)) {
// a host can only be associated with a single host group
return groupInfo.getHostGroupName();
}
}
return null;
}
//todo: host info?
@Override
public void addHostToTopology(String hostGroupName, String host) throws InvalidTopologyException, NoSuchHostGroupException {
if (blueprint.getHostGroup(hostGroupName) == null) {
throw new NoSuchHostGroupException("Attempted to add host to non-existing host group: " + hostGroupName);
}
// check for host duplicates
String groupContainsHost = getHostGroupForHost(host);
// in case of reserved host, hostgroup will already contain host
if (groupContainsHost != null && ! hostGroupName.equals(groupContainsHost)) {
throw new InvalidTopologyException(String.format(
"Attempted to add host '%s' to hostgroup '%s' but it is already associated with hostgroup '%s'.",
host, hostGroupName, groupContainsHost));
}
synchronized(hostGroupInfoMap) {
HostGroupInfo existingHostGroupInfo = hostGroupInfoMap.get(hostGroupName);
if (existingHostGroupInfo == null) {
throw new RuntimeException(String.format("An attempt was made to add host '%s' to an unregistered hostgroup '%s'",
host, hostGroupName));
}
// ok to add same host multiple times to same group
existingHostGroupInfo.addHost(host);
LOG.info("ClusterTopologyImpl.addHostTopology: added host = " + host + " to host group = " + existingHostGroupInfo.getHostGroupName());
}
}
@Override
public Collection<String> getHostAssignmentsForComponent(String component) {
//todo: ordering requirements?
Collection<String> hosts = new ArrayList<>();
Collection<String> hostGroups = getHostGroupsForComponent(component);
for (String group : hostGroups) {
HostGroupInfo hostGroupInfo = getHostGroupInfo().get(group);
if (hostGroupInfo != null) {
hosts.addAll(hostGroupInfo.getHostNames());
} else {
LOG.warn("HostGroup {} not found, when checking for hosts for component {}", group, component);
}
}
return hosts;
}
@Override
public boolean isNameNodeHAEnabled() {
return isNameNodeHAEnabled(configuration.getFullProperties());
}
public static boolean isNameNodeHAEnabled(Map<String, Map<String, String>> configurationProperties) {
return configurationProperties.containsKey("hdfs-site") &&
(configurationProperties.get("hdfs-site").containsKey("dfs.nameservices") ||
configurationProperties.get("hdfs-site").containsKey("dfs.internal.nameservices"));
}
@Override
public boolean isYarnResourceManagerHAEnabled() {
return isYarnResourceManagerHAEnabled(configuration.getFullProperties());
}
/**
* Static convenience function to determine if Yarn ResourceManager HA is enabled
* @param configProperties configuration properties for this cluster
* @return true if Yarn ResourceManager HA is enabled
* false if Yarn ResourceManager HA is not enabled
*/
static boolean isYarnResourceManagerHAEnabled(Map<String, Map<String, String>> configProperties) {
return configProperties.containsKey("yarn-site") && configProperties.get("yarn-site").containsKey("yarn.resourcemanager.ha.enabled")
&& configProperties.get("yarn-site").get("yarn.resourcemanager.ha.enabled").equals("true");
}
private void validateTopology()
throws InvalidTopologyException {
if(isNameNodeHAEnabled()){
Collection<String> nnHosts = getHostAssignmentsForComponent("NAMENODE");
if (nnHosts.size() != 2) {
throw new InvalidTopologyException("NAMENODE HA requires exactly 2 hosts running NAMENODE but there are: " +
nnHosts.size() + " Hosts: " + nnHosts);
}
Map<String, String> hadoopEnvConfig = configuration.getFullProperties().get("hadoop-env");
if(hadoopEnvConfig != null && !hadoopEnvConfig.isEmpty() && hadoopEnvConfig.containsKey("dfs_ha_initial_namenode_active") && hadoopEnvConfig.containsKey("dfs_ha_initial_namenode_standby")) {
if((!HostGroup.HOSTGROUP_REGEX.matcher(hadoopEnvConfig.get("dfs_ha_initial_namenode_active")).matches() && !nnHosts.contains(hadoopEnvConfig.get("dfs_ha_initial_namenode_active")))
|| (!HostGroup.HOSTGROUP_REGEX.matcher(hadoopEnvConfig.get("dfs_ha_initial_namenode_standby")).matches() && !nnHosts.contains(hadoopEnvConfig.get("dfs_ha_initial_namenode_standby")))){
throw new IllegalArgumentException("NAMENODE HA hosts mapped incorrectly for properties 'dfs_ha_initial_namenode_active' and 'dfs_ha_initial_namenode_standby'. Expected hosts are: " + nnHosts);
}
}
}
}
@Override
public boolean isClusterKerberosEnabled() {
return ambariContext.isClusterKerberosEnabled(getClusterId());
}
@Override
public RequestStatusResponse installHost(String hostName, boolean skipInstallTaskCreate, boolean skipFailure) {
try {
String hostGroupName = getHostGroupForHost(hostName);
HostGroup hostGroup = this.blueprint.getHostGroup(hostGroupName);
Collection<String> skipInstallForComponents = new ArrayList<>();
if (skipInstallTaskCreate) {
skipInstallForComponents.add("ALL");
} else {
// get the set of components that are marked as START_ONLY for this hostgroup
skipInstallForComponents.addAll(hostGroup.getComponentNames(ProvisionAction.START_ONLY));
}
Collection<String> dontSkipInstallForComponents = hostGroup.getComponentNames(INSTALL_ONLY);
dontSkipInstallForComponents.addAll(hostGroup.getComponentNames(INSTALL_AND_START));
return ambariContext.installHost(hostName, ambariContext.getClusterName(getClusterId()),
skipInstallForComponents, dontSkipInstallForComponents, skipFailure);
} catch (AmbariException e) {
LOG.error("Cannot get cluster name for clusterId = " + getClusterId(), e);
throw new RuntimeException(e);
}
}
@Override
public RequestStatusResponse startHost(String hostName, boolean skipFailure) {
try {
String hostGroupName = getHostGroupForHost(hostName);
HostGroup hostGroup = this.blueprint.getHostGroup(hostGroupName);
// get the set of components that are marked as INSTALL_ONLY
// for this hostgroup
Collection<String> installOnlyComponents =
hostGroup.getComponentNames(ProvisionAction.INSTALL_ONLY);
return ambariContext.startHost(hostName, ambariContext.getClusterName(getClusterId()), installOnlyComponents, skipFailure);
} catch (AmbariException e) {
LOG.error("Cannot get cluster name for clusterId = " + getClusterId(), e);
throw new RuntimeException(e);
}
}
@Override
public void setConfigRecommendationStrategy(ConfigRecommendationStrategy strategy) {
this.configRecommendationStrategy = strategy;
}
@Override
public ConfigRecommendationStrategy getConfigRecommendationStrategy() {
return this.configRecommendationStrategy;
}
@Override
public ProvisionAction getProvisionAction() {
return provisionAction;
}
@Override
public void setProvisionAction(ProvisionAction provisionAction) {
this.provisionAction = provisionAction;
}
@Override
public Map<String, AdvisedConfiguration> getAdvisedConfigurations() {
return this.advisedConfigurations;
}
@Override
public AmbariContext getAmbariContext() {
return ambariContext;
}
@Override
public void removeHost(String hostname) {
for(Map.Entry<String,HostGroupInfo> entry : hostGroupInfoMap.entrySet()) {
entry.getValue().removeHost(hostname);
}
}
@Override
public String getDefaultPassword() {
return defaultPassword;
}
private void registerHostGroupInfo(Map<String, HostGroupInfo> requestedHostGroupInfoMap) throws InvalidTopologyException {
LOG.debug("Registering requested host group information for {} hostgroups", requestedHostGroupInfoMap.size());
checkForDuplicateHosts(requestedHostGroupInfoMap);
for (HostGroupInfo requestedHostGroupInfo : requestedHostGroupInfoMap.values()) {
String hostGroupName = requestedHostGroupInfo.getHostGroupName();
//todo: doesn't support using a different blueprint for update (scaling)
HostGroup baseHostGroup = getBlueprint().getHostGroup(hostGroupName);
if (baseHostGroup == null) {
throw new IllegalArgumentException("Invalid host_group specified: " + hostGroupName +
". All request host groups must have a corresponding host group in the specified blueprint");
}
//todo: split into two methods
HostGroupInfo currentHostGroupInfo = hostGroupInfoMap.get(hostGroupName);
if (currentHostGroupInfo == null) {
// blueprint host group config
Configuration bpHostGroupConfig = baseHostGroup.getConfiguration();
// parent config is BP host group config but with parent set to topology cluster scoped config
Configuration parentConfiguration = new Configuration(bpHostGroupConfig.getProperties(),
bpHostGroupConfig.getAttributes(), getConfiguration());
requestedHostGroupInfo.getConfiguration().setParentConfiguration(parentConfiguration);
hostGroupInfoMap.put(hostGroupName, requestedHostGroupInfo);
} else {
// Update. Either add hosts or increment request count
if (!requestedHostGroupInfo.getHostNames().isEmpty()) {
try {
// this validates that hosts aren't already registered with groups
addHostsToTopology(requestedHostGroupInfo);
} catch (NoSuchHostGroupException e) {
//todo
throw new InvalidTopologyException("Attempted to add hosts to unknown host group: " + hostGroupName);
}
} else {
currentHostGroupInfo.setRequestedCount(
currentHostGroupInfo.getRequestedHostCount() + requestedHostGroupInfo.getRequestedHostCount());
}
//todo: throw exception in case where request attempts to modify HG configuration in scaling operation
}
}
}
private void addHostsToTopology(HostGroupInfo hostGroupInfo) throws InvalidTopologyException, NoSuchHostGroupException {
for (String host: hostGroupInfo.getHostNames()) {
registerRackInfo(hostGroupInfo, host);
addHostToTopology(hostGroupInfo.getHostGroupName(), host);
}
}
private void registerRackInfo(HostGroupInfo hostGroupInfo, String host) {
synchronized (hostGroupInfoMap) {
HostGroupInfo cachedHGI = hostGroupInfoMap.get(hostGroupInfo.getHostGroupName());
if (null != cachedHGI) {
cachedHGI.addHostRackInfo(host, hostGroupInfo.getHostRackInfo().get(host));
}
}
}
private void checkForDuplicateHosts(Map<String, HostGroupInfo> groupInfoMap) throws InvalidTopologyException {
Set<String> hosts = new HashSet<>();
Set<String> duplicates = new HashSet<>();
for (HostGroupInfo group : groupInfoMap.values()) {
// check for duplicates within the new groups
Collection<String> groupHosts = group.getHostNames();
Collection<String> groupHostsCopy = new HashSet<>(group.getHostNames());
groupHostsCopy.retainAll(hosts);
duplicates.addAll(groupHostsCopy);
hosts.addAll(groupHosts);
// check against existing groups
for (String host : groupHosts) {
if (getHostGroupForHost(host) != null) {
duplicates.add(host);
}
}
}
if (! duplicates.isEmpty()) {
throw new InvalidTopologyException("The following hosts are mapped to multiple host groups: " + duplicates + "." +
" Be aware that host names are converted to lowercase, case differences do not matter in Ambari deployments.");
}
}
}