/** * 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.stack; import java.lang.reflect.Type; import java.net.MalformedURLException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.ambari.server.AmbariException; import org.apache.ambari.server.state.Cluster; import org.apache.ambari.server.state.ConfigHelper; import org.apache.ambari.server.state.Host; import org.apache.ambari.server.state.MaintenanceState; import org.apache.ambari.server.state.ServiceComponent; import org.apache.ambari.server.state.ServiceComponentHost; import org.apache.ambari.server.state.UpgradeState; import org.apache.ambari.server.utils.HTTPUtils; import org.apache.ambari.server.utils.HostAndPort; import org.apache.ambari.server.utils.StageUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.reflect.TypeToken; public class MasterHostResolver { private static Logger LOG = LoggerFactory.getLogger(MasterHostResolver.class); private Cluster m_cluster; private String m_version; private ConfigHelper m_configHelper; public enum Service { HDFS, HBASE, YARN, OTHER } /** * Union of status for several services. */ protected enum Status { ACTIVE, STANDBY } /** * Create a resolver that does not consider HostComponents' version when * resolving hosts. Common use case is creating an upgrade that should * include an entire cluster. * @param configHelper Configuration Helper * @param cluster the cluster */ public MasterHostResolver(ConfigHelper configHelper, Cluster cluster) { this(configHelper, cluster, null); } /** * Create a resolver that compares HostComponents' version when calculating * hosts for the stage. Common use case is for downgrades when only some * HostComponents need to be downgraded, and HostComponents already at the * correct version are skipped. * @param configHelper Configuration Helper * @param cluster the cluster * @param version the version, or {@code null} to not compare versions */ public MasterHostResolver(ConfigHelper configHelper, Cluster cluster, String version) { m_configHelper = configHelper; m_cluster = cluster; m_version = version; } /** * Gets the cluster that this instance of the {@link MasterHostResolver} is * initialized with. * * @return the cluster (not {@code null}). */ public Cluster getCluster() { return m_cluster; } /** * Get the master hostname of the given service and component. * @param serviceName Service * @param componentName Component * @return The hostname that is the master of the service and component if successful, null otherwise. */ public HostsType getMasterAndHosts(String serviceName, String componentName) { if (serviceName == null || componentName == null) { return null; } Set<String> componentHosts = m_cluster.getHosts(serviceName, componentName); if (0 == componentHosts.size()) { return null; } HostsType hostsType = new HostsType(); hostsType.hosts.addAll(componentHosts); Service s = Service.OTHER; try { s = Service.valueOf(serviceName.toUpperCase()); } catch (Exception e) { // !!! nothing to do } try { switch (s) { case HDFS: if (componentName.equalsIgnoreCase("NAMENODE")) { if (componentHosts.size() != 2) { return filterHosts(hostsType, serviceName, componentName); } Map<Status, String> pair = getNameNodePair(); if (pair != null) { hostsType.master = pair.containsKey(Status.ACTIVE) ? pair.get(Status.ACTIVE) : null; hostsType.secondary = pair.containsKey(Status.STANDBY) ? pair.get(Status.STANDBY) : null; } else { // !!! we KNOW we have 2 componentHosts if we're here. Iterator<String> iterator = componentHosts.iterator(); hostsType.master = iterator.next(); hostsType.secondary = iterator.next(); LOG.warn("Could not determine the active/standby states from NameNodes {}. " + "Using {} as active and {} as standby.", StringUtils.join(componentHosts, ','), hostsType.master, hostsType.secondary); } } break; case YARN: if (componentName.equalsIgnoreCase("RESOURCEMANAGER")) { resolveResourceManagers(getCluster(), hostsType); } break; case HBASE: if (componentName.equalsIgnoreCase("HBASE_MASTER")) { resolveHBaseMasters(getCluster(), hostsType); } break; default: break; } } catch (Exception err) { LOG.error("Unable to get master and hosts for Component " + componentName + ". Error: " + err.getMessage(), err); } hostsType = filterHosts(hostsType, serviceName, componentName); return hostsType; } /** * Filters the supplied list of hosts in the following ways: * <ul> * <li>Compares the versions of a HostComponent to the version for the * resolver. Only versions that do not match are retained.</li> * <li>Removes unhealthy hosts in maintenance mode from the list of healthy * hosts</li> * </ul> * * @param hostsType * the hosts to resolve * @param service * the service name * @param component * the component name * @return the modified hosts instance with filtered and unhealthy hosts * filled */ private HostsType filterHosts(HostsType hostsType, String service, String component) { try { org.apache.ambari.server.state.Service svc = m_cluster.getService(service); ServiceComponent sc = svc.getServiceComponent(component); // !!! not really a fan of passing these around List<ServiceComponentHost> unhealthyHosts = new ArrayList<>(); LinkedHashSet<String> upgradeHosts = new LinkedHashSet<>(); for (String hostName : hostsType.hosts) { ServiceComponentHost sch = sc.getServiceComponentHost(hostName); Host host = sch.getHost(); MaintenanceState maintenanceState = host.getMaintenanceState(sch.getClusterId()); // !!! FIXME: only rely on maintenance state once the upgrade endpoint // is using the pre-req endpoint for determining if an upgrade is // possible if (maintenanceState != MaintenanceState.OFF) { unhealthyHosts.add(sch); } else if (null == m_version || null == sch.getVersion() || !sch.getVersion().equals(m_version) || sch.getUpgradeState() == UpgradeState.FAILED) { upgradeHosts.add(hostName); } } hostsType.unhealthy = unhealthyHosts; hostsType.hosts = upgradeHosts; return hostsType; } catch (AmbariException e) { // !!! better not LOG.warn("Could not determine host components to upgrade. Defaulting to saved hosts.", e); return hostsType; } } /** * Determine if HDFS is present and it has NameNode High Availability. * @return true if has NameNode HA, otherwise, false. */ public boolean isNameNodeHA() throws AmbariException { Map<String, org.apache.ambari.server.state.Service> services = m_cluster.getServices(); if (services != null && services.containsKey("HDFS")) { Set<String> secondaryNameNodeHosts = m_cluster.getHosts("HDFS", "SECONDARY_NAMENODE"); Set<String> nameNodeHosts = m_cluster.getHosts("HDFS", "NAMENODE"); if (secondaryNameNodeHosts.size() == 1 && nameNodeHosts.size() == 1) { return false; } if (nameNodeHosts.size() > 1) { return true; } throw new AmbariException("Unable to determine if cluster has NameNode HA."); } return false; } /** * Get mapping of the HDFS Namenodes from the state ("active" or "standby") to the hostname. * @return Returns a map from the state ("active" or "standby" to the hostname with that state if exactly * one active and one standby host were found, otherwise, return null. * The hostnames are returned in lowercase. */ private Map<Status, String> getNameNodePair() { Map<Status, String> stateToHost = new HashMap<>(); Cluster cluster = getCluster(); String nameService = m_configHelper.getValueFromDesiredConfigurations(cluster, ConfigHelper.HDFS_SITE, "dfs.internal.nameservices"); if (nameService == null || nameService.isEmpty()) { return null; } String nnUniqueIDstring = m_configHelper.getValueFromDesiredConfigurations(cluster, ConfigHelper.HDFS_SITE, "dfs.ha.namenodes." + nameService); if (nnUniqueIDstring == null || nnUniqueIDstring.isEmpty()) { return null; } String[] nnUniqueIDs = nnUniqueIDstring.split(","); if (nnUniqueIDs == null || nnUniqueIDs.length != 2) { return null; } String policy = m_configHelper.getValueFromDesiredConfigurations(cluster, ConfigHelper.HDFS_SITE, "dfs.http.policy"); boolean encrypted = (policy != null && policy.equalsIgnoreCase(ConfigHelper.HTTPS_ONLY)); String namenodeFragment = "dfs.namenode." + (encrypted ? "https-address" : "http-address") + ".{0}.{1}"; for (String nnUniqueID : nnUniqueIDs) { String key = MessageFormat.format(namenodeFragment, nameService, nnUniqueID); String value = m_configHelper.getValueFromDesiredConfigurations(cluster, ConfigHelper.HDFS_SITE, key); try { HostAndPort hp = HTTPUtils.getHostAndPortFromProperty(value); if (hp == null) { throw new MalformedURLException("Could not parse host and port from " + value); } String state = queryJmxBeanValue(hp.host, hp.port, "Hadoop:service=NameNode,name=NameNodeStatus", "State", true, encrypted); if (null != state && (state.equalsIgnoreCase(Status.ACTIVE.toString()) || state.equalsIgnoreCase(Status.STANDBY.toString()))) { Status status = Status.valueOf(state.toUpperCase()); stateToHost.put(status, hp.host.toLowerCase()); } else { LOG.error(String.format("Could not retrieve state for NameNode %s from property %s by querying JMX.", hp.host, key)); } } catch (MalformedURLException e) { LOG.error(e.getMessage()); } } if (stateToHost.containsKey(Status.ACTIVE) && stateToHost.containsKey(Status.STANDBY) && !stateToHost.get(Status.ACTIVE).equalsIgnoreCase(stateToHost.get(Status.STANDBY))) { return stateToHost; } return null; } /** * Resolve the name of the Resource Manager master and convert the hostname to lowercase. * @param cluster Cluster * @param hostType RM hosts * @throws MalformedURLException */ private void resolveResourceManagers(Cluster cluster, HostsType hostType) throws MalformedURLException { LinkedHashSet<String> orderedHosts = new LinkedHashSet<>(hostType.hosts); // IMPORTANT, for RM, only the master returns jmx String rmWebAppAddress = m_configHelper.getValueFromDesiredConfigurations(cluster, ConfigHelper.YARN_SITE, "yarn.resourcemanager.webapp.address"); HostAndPort hp = HTTPUtils.getHostAndPortFromProperty(rmWebAppAddress); if (hp == null) { throw new MalformedURLException("Could not parse host and port from " + rmWebAppAddress); } for (String hostname : hostType.hosts) { String value = queryJmxBeanValue(hostname, hp.port, "Hadoop:service=ResourceManager,name=RMNMInfo", "modelerType", true); if (null != value) { if (null == hostType.master) { hostType.master = hostname.toLowerCase(); } // Quick and dirty to make sure the master is last in the list orderedHosts.remove(hostname.toLowerCase()); orderedHosts.add(hostname.toLowerCase()); } } hostType.hosts = orderedHosts; } /** * Resolve the HBASE master and convert the hostname to lowercase. * @param cluster Cluster * @param hostsType HBASE master host. * @throws AmbariException */ private void resolveHBaseMasters(Cluster cluster, HostsType hostsType) throws AmbariException { String hbaseMasterInfoPortProperty = "hbase.master.info.port"; String hbaseMasterInfoPortValue = m_configHelper.getValueFromDesiredConfigurations(cluster, ConfigHelper.HBASE_SITE, hbaseMasterInfoPortProperty); if (hbaseMasterInfoPortValue == null || hbaseMasterInfoPortValue.isEmpty()) { throw new AmbariException("Could not find property " + hbaseMasterInfoPortProperty); } final int hbaseMasterInfoPort = Integer.parseInt(hbaseMasterInfoPortValue); for (String hostname : hostsType.hosts) { String value = queryJmxBeanValue(hostname, hbaseMasterInfoPort, "Hadoop:service=HBase,name=Master,sub=Server", "tag.isActiveMaster", false); if (null != value) { Boolean bool = Boolean.valueOf(value); if (bool.booleanValue()) { hostsType.master = hostname.toLowerCase(); } else { hostsType.secondary = hostname.toLowerCase(); } } } } protected String queryJmxBeanValue(String hostname, int port, String beanName, String attributeName, boolean asQuery) { return queryJmxBeanValue(hostname, port, beanName, attributeName, asQuery, false); } /** * Query the JMX attribute at http(s)://$server:$port/jmx?qry=$query or http(s)://$server:$port/jmx?get=$bean::$attribute * @param hostname host name * @param port port number * @param beanName if asQuery is false, then search for this bean name * @param attributeName if asQuery is false, then search for this attribute name * @param asQuery whether to search bean or query * @param encrypted true if using https instead of http. * @return The jmx value. */ protected String queryJmxBeanValue(String hostname, int port, String beanName, String attributeName, boolean asQuery, boolean encrypted) { String protocol = encrypted ? "https://" : "http://"; String endPoint = protocol + (asQuery ? String.format("%s:%s/jmx?qry=%s", hostname, port, beanName) : String.format("%s:%s/jmx?get=%s::%s", hostname, port, beanName, attributeName)); String response = HTTPUtils.requestURL(endPoint); if (null == response || response.isEmpty()) { return null; } Type type = new TypeToken<Map<String, ArrayList<HashMap<String, String>>>>() {}.getType(); try { Map<String, ArrayList<HashMap<String, String>>> jmxBeans = StageUtils.getGson().fromJson(response, type); return jmxBeans.get("beans").get(0).get(attributeName); } catch (Exception e) { if (LOG.isDebugEnabled()) { LOG.debug("Could not load JMX from {}/{} from {}", beanName, attributeName, hostname, e); } else { LOG.debug("Could not load JMX from {}/{} from {}", beanName, attributeName, hostname); } } return null; } }