You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by be...@apache.org on 2018/11/19 15:50:33 UTC

[ambari] branch trunk updated: AMBARI-24919 external Namenode HA (benyoka) (#2625)

This is an automated email from the ASF dual-hosted git repository.

benyoka pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 93576b7  AMBARI-24919 external Namenode HA (benyoka) (#2625)
93576b7 is described below

commit 93576b7c9bc25361ba1e43c9dfb08fc136667520
Author: benyoka <be...@users.noreply.github.com>
AuthorDate: Mon Nov 19 16:50:28 2018 +0100

    AMBARI-24919 external Namenode HA (benyoka) (#2625)
---
 .../internal/BlueprintConfigurationProcessor.java  |  26 ++-
 .../ambari/server/topology/ClusterTopology.java    |   5 +
 .../server/topology/ClusterTopologyImpl.java       |  27 +--
 .../topology/validators/NameNodeHaValidator.java   | 128 ++++++++++
 .../validators/TopologyValidatorFactory.java       |   9 +-
 .../BlueprintConfigurationProcessorTest.java       | 257 +++++++++++++++++++++
 .../server/topology/ClusterTopologyImplTest.java   |  55 +----
 .../validators/NameNodeHaValidatorTest.java        | 219 ++++++++++++++++++
 8 files changed, 649 insertions(+), 77 deletions(-)

diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/BlueprintConfigurationProcessor.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/BlueprintConfigurationProcessor.java
index 1daf1f5..46d7e9b 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/BlueprintConfigurationProcessor.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/BlueprintConfigurationProcessor.java
@@ -57,6 +57,8 @@ import org.apache.ambari.server.topology.ConfigRecommendationStrategy;
 import org.apache.ambari.server.topology.Configuration;
 import org.apache.ambari.server.topology.HostGroup;
 import org.apache.ambari.server.topology.HostGroupInfo;
+import org.apache.ambari.server.topology.InvalidTopologyException;
+import org.apache.ambari.server.topology.validators.NameNodeHaValidator;
 import org.apache.ambari.server.topology.validators.UnitValidatedProperty;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
@@ -480,6 +482,23 @@ public class BlueprintConfigurationProcessor {
   private void doNameNodeHAUpdateOnClusterCreation(Configuration clusterConfig,
                                                    Map<String, Map<String, String>> clusterProps,
                                                    Set<String> configTypesUpdated) throws ConfigurationTopologyException {
+
+    final Collection<String> nnHosts = clusterTopology.getHostAssignmentsForComponent("NAMENODE");
+
+    // external namenodes
+    if (nnHosts.isEmpty()) {
+      LOG.info("NAMENODE HA is enabled but there are no NAMENODE components in the cluster. Assuming external name nodes.");
+      // need to redo validation here as required information (explicit hostnames for some host groups) may have been
+      // missing by the time the ClusterTopology object was validated
+      try {
+        new NameNodeHaValidator().validateExternalNamenodeHa(clusterTopology);
+      }
+      catch (InvalidTopologyException ex) {
+        throw new ConfigurationTopologyException(ex.getMessage(), ex);
+      }
+      return;
+    }
+
     // add "dfs.internal.nameservices" if it's not specified
     Map<String, String> hdfsSiteConfig = clusterConfig.getFullProperties().get("hdfs-site");
     String nameservices = hdfsSiteConfig.get("dfs.nameservices");
@@ -496,10 +515,9 @@ public class BlueprintConfigurationProcessor {
       LOG.info("Processing a single HDFS NameService, which indicates a default HDFS NameNode HA deployment");
       // if the active/standby namenodes are not specified, assign them automatically
       if (! isNameNodeHAInitialActiveNodeSet(clusterProps) && ! isNameNodeHAInitialStandbyNodeSet(clusterProps)) {
-        Collection<String> nnHosts = clusterTopology.getHostAssignmentsForComponent("NAMENODE");
-        if (nnHosts.size() < 2) {
-          throw new ConfigurationTopologyException("NAMENODE HA requires at least 2 hosts running NAMENODE but there are: " +
-            nnHosts.size() + " Hosts: " + nnHosts);
+        if (nnHosts.size() == 1) { // can't be 0 as in that case was handled above
+          throw new ConfigurationTopologyException("NAMENODE HA requires at least two hosts running NAMENODE but there is " +
+            "only one: " + nnHosts.iterator().next());
         }
 
         // set the properties that configure which namenode is active,
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/topology/ClusterTopology.java b/ambari-server/src/main/java/org/apache/ambari/server/topology/ClusterTopology.java
index e1cab2b..4d8bba3 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/topology/ClusterTopology.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/topology/ClusterTopology.java
@@ -20,6 +20,7 @@ package org.apache.ambari.server.topology;
 
 import java.util.Collection;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.ambari.server.controller.RequestStatusResponse;
 import org.apache.ambari.server.controller.internal.ProvisionAction;
@@ -68,6 +69,10 @@ public interface ClusterTopology {
   Map<String, HostGroupInfo> getHostGroupInfo();
 
   /**
+   * @return all hosts in the topology
+   */
+  Set<String> getAllHosts();
+  /**
    * Get the names of  all of host groups which contain the specified component.
    *
    * @param component  component name
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/topology/ClusterTopologyImpl.java b/ambari-server/src/main/java/org/apache/ambari/server/topology/ClusterTopologyImpl.java
index 0945821..54abae2 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/topology/ClusterTopologyImpl.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/topology/ClusterTopologyImpl.java
@@ -18,6 +18,7 @@
 
 package org.apache.ambari.server.topology;
 
+import static java.util.stream.Collectors.toSet;
 import static org.apache.ambari.server.controller.internal.ProvisionAction.INSTALL_AND_START;
 import static org.apache.ambari.server.controller.internal.ProvisionAction.INSTALL_ONLY;
 import static org.apache.ambari.server.state.ServiceInfo.HADOOP_COMPATIBLE_FS;
@@ -74,8 +75,6 @@ public class ClusterTopologyImpl implements ClusterTopology {
 
     registerHostGroupInfo(topologyRequest.getHostGroupInfo());
 
-    // todo extract validation to specialized service
-    validateTopology();
     this.ambariContext = ambariContext;
   }
 
@@ -162,6 +161,11 @@ public class ClusterTopologyImpl implements ClusterTopology {
   }
 
   @Override
+  public Set<String> getAllHosts() {
+    return hostGroupInfoMap.values().stream().flatMap(hg -> hg.getHostNames().stream()).collect(toSet());
+  }
+
+  @Override
   public Collection<String> getHostAssignmentsForComponent(String component) {
     //todo: ordering requirements?
     Collection<String> hosts = new ArrayList<>();
@@ -204,25 +208,6 @@ public class ClusterTopologyImpl implements ClusterTopology {
       && 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 at least 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());
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/topology/validators/NameNodeHaValidator.java b/ambari-server/src/main/java/org/apache/ambari/server/topology/validators/NameNodeHaValidator.java
new file mode 100644
index 0000000..26a4d4f
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/topology/validators/NameNodeHaValidator.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed 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.validators;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.ambari.server.topology.ClusterTopology;
+import org.apache.ambari.server.topology.HostGroup;
+import org.apache.ambari.server.topology.InvalidTopologyException;
+import org.apache.ambari.server.topology.TopologyValidator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+
+/**
+ * Validates NAMENODE HA setup correctness.
+ */
+public class NameNodeHaValidator implements TopologyValidator {
+
+  private static final Logger LOG = LoggerFactory.getLogger(NameNodeHaValidator.class);
+  private static final Splitter SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults();
+
+  @Override
+  public void validate(ClusterTopology topology) throws InvalidTopologyException {
+    if( topology.isNameNodeHAEnabled() ) {
+
+      if (!allHostNamesAreKnown(topology)) {
+        LOG.warn("Provision cluster template contains hostgroup(s) without explicit hostnames. Cannot validate NAMENODE HA setup in this case.");
+        return;
+      }
+
+      Collection<String> nnHosts = topology.getHostAssignmentsForComponent("NAMENODE");
+
+      if (nnHosts.isEmpty()) {
+        LOG.info("NAMENODE HA is enabled but there are no NAMENODE components in the cluster. Assuming external name nodes.");
+        validateExternalNamenodeHa(topology);
+      }
+      else if (nnHosts.size() == 1) {
+        throw new InvalidTopologyException("NAMENODE HA requires at least two hosts running NAMENODE but there is only one: " +
+          nnHosts.iterator().next());
+      }
+
+      Map<String, String> hadoopEnvConfig = topology.getConfiguration().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 InvalidTopologyException("NAMENODE HA hosts mapped incorrectly for properties 'dfs_ha_initial_namenode_active' and 'dfs_ha_initial_namenode_standby'. Expected hosts are: " + nnHosts);
+        }
+      }
+    }
+
+  }
+
+  private boolean allHostNamesAreKnown(ClusterTopology topology) {
+    return topology.getHostGroupInfo().values().stream().allMatch(
+      hg -> !hg.getHostNames().isEmpty());
+  }
+
+  /**
+   * Verifies that all respective properties are set for an external NANENODE HA setup
+   */
+  public void validateExternalNamenodeHa(ClusterTopology topology) throws InvalidTopologyException {
+    Map<String, String> hdfsSite = topology.getConfiguration().getFullProperties().get("hdfs-site");
+    Set<String> hosts = topology.getAllHosts();
+
+    for (Map.Entry<String, List<String>> entry: getNameServicesToNameNodes(hdfsSite).entrySet()) {
+      String nameService = entry.getKey();
+      List<String> nameNodes = entry.getValue();
+      for (String nameNode: nameNodes) {
+        String address = hdfsSite.get("dfs.namenode.rpc-address." + nameService + "." + nameNode);
+        checkValidExternalNameNodeAddress(nameService, nameNode, address, hosts);
+      }
+    }
+  }
+
+  private void checkValidExternalNameNodeAddress(String nameService, String nameNode, String address, Set<String> hosts) throws InvalidTopologyException {
+    final String errorMessage = "Illegal external HA NAMENODE address for name service [%s], namenode [%s]: [%s]. Address " +
+      "must be in <host>:<port> format where host is external to the cluster.";
+
+    checkInvalidTopology(
+      Strings.isNullOrEmpty(address) || address.contains("localhost") || address.contains("%HOSTGROUP") || !address.contains(":"),
+      errorMessage, nameService, nameNode, address);
+
+    String hostName = address.substring(0, address.indexOf(':'));
+
+    checkInvalidTopology(hostName.isEmpty() || hosts.contains(hostName),
+      errorMessage, nameService, nameNode, address);
+  }
+
+  Map<String, List<String>> getNameServicesToNameNodes(Map<String, String> hdfsSite) throws InvalidTopologyException {
+    String nameServices = null != hdfsSite.get("dfs.internal.nameservices") ?
+      hdfsSite.get("dfs.internal.nameservices") : hdfsSite.get("dfs.nameservices");
+    Map<String, List<String>> nameServicesToNameNodes = new HashMap<>();
+
+    for (String ns: SPLITTER.splitToList(nameServices)) {
+      String nameNodes = hdfsSite.get("dfs.ha.namenodes." + ns);
+      checkInvalidTopology(Strings.isNullOrEmpty(nameNodes),
+        "No namenodes specified for nameservice %s.", ns);
+      nameServicesToNameNodes.put(ns, SPLITTER.splitToList(nameNodes));
+    }
+
+    return nameServicesToNameNodes;
+  }
+
+
+  private void checkInvalidTopology(boolean errorCondition, String errorMessageTemplate, Object... errorMessageParams) throws InvalidTopologyException {
+    if (errorCondition) throw new InvalidTopologyException(String.format(errorMessageTemplate, errorMessageParams));
+  }
+
+}
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/topology/validators/TopologyValidatorFactory.java b/ambari-server/src/main/java/org/apache/ambari/server/topology/validators/TopologyValidatorFactory.java
index bc76bff..56e1484 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/topology/validators/TopologyValidatorFactory.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/topology/validators/TopologyValidatorFactory.java
@@ -24,8 +24,13 @@ public class TopologyValidatorFactory {
   List<TopologyValidator> validators;
 
   public TopologyValidatorFactory() {
-    validators = ImmutableList.of(new RequiredConfigPropertiesValidator(), new RequiredPasswordValidator(), new HiveServiceValidator(),
-      new StackConfigTypeValidator(), new UnitValidator(UnitValidatedProperty.ALL));
+    validators = ImmutableList.of(
+      new RequiredConfigPropertiesValidator(),
+      new RequiredPasswordValidator(),
+      new HiveServiceValidator(),
+      new StackConfigTypeValidator(),
+      new UnitValidator(UnitValidatedProperty.ALL),
+      new NameNodeHaValidator());
   }
 
   public TopologyValidator createConfigurationValidatorChain() {
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/BlueprintConfigurationProcessorTest.java b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/BlueprintConfigurationProcessorTest.java
index 835cda5..e7cd271 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/BlueprintConfigurationProcessorTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/BlueprintConfigurationProcessorTest.java
@@ -5661,6 +5661,263 @@ public class BlueprintConfigurationProcessorTest extends EasyMockSupport {
     assertEquals(ImmutableSet.of("cluster-env", "hdfs-site"), updatedConfigTypes);
   }
 
+
+  @Test(expected = ConfigurationTopologyException.class)
+  public void testDoUpdateForClusterWithNameNodeHAEnabled_insufficientNameNodes() throws Exception {
+    final String expectedNameService = "mynameservice";
+    final String expectedHostName = "c6401.apache.ambari.org";
+    final String expectedHostNameTwo = "server-two";
+    final String expectedPortNum = "808080";
+    final String expectedNodeOne = "nn1";
+    final String expectedNodeTwo = "nn2";
+    final String expectedHostGroupName = "host_group_1";
+
+    Map<String, Map<String, String>> properties = new HashMap<>();
+
+    Map<String, String> hdfsSiteProperties = new HashMap<>();
+    Map<String, String> hbaseSiteProperties = new HashMap<>();
+    Map<String, String> hadoopEnvProperties = new HashMap<>();
+    Map<String, String> coreSiteProperties = new HashMap<>();
+    Map<String, String> accumuloSiteProperties = new HashMap<>();
+
+    properties.put("hdfs-site", hdfsSiteProperties);
+    properties.put("hadoop-env", hadoopEnvProperties);
+    properties.put("core-site", coreSiteProperties);
+    properties.put("hbase-site", hbaseSiteProperties);
+    properties.put("accumulo-site", accumuloSiteProperties);
+
+    // setup hdfs HA config for test
+    hdfsSiteProperties.put("dfs.nameservices", expectedNameService);
+    hdfsSiteProperties.put("dfs.ha.namenodes.mynameservice", expectedNodeOne + ", " + expectedNodeTwo);
+
+    // setup properties that include exported host group information
+    hdfsSiteProperties.put("dfs.namenode.https-address." + expectedNameService + "." + expectedNodeOne, createExportedAddress(expectedPortNum, expectedHostGroupName));
+    hdfsSiteProperties.put("dfs.namenode.https-address." + expectedNameService + "." + expectedNodeTwo, createExportedAddress(expectedPortNum, expectedHostGroupName));
+    hdfsSiteProperties.put("dfs.namenode.http-address." + expectedNameService + "." + expectedNodeOne, createExportedAddress(expectedPortNum, expectedHostGroupName));
+    hdfsSiteProperties.put("dfs.namenode.http-address." + expectedNameService + "." + expectedNodeTwo, createExportedAddress(expectedPortNum, expectedHostGroupName));
+    hdfsSiteProperties.put("dfs.namenode.rpc-address." + expectedNameService + "." + expectedNodeOne, createExportedAddress(expectedPortNum, expectedHostGroupName));
+    hdfsSiteProperties.put("dfs.namenode.rpc-address." + expectedNameService + "." + expectedNodeTwo, createExportedAddress(expectedPortNum, expectedHostGroupName));
+
+    // add properties that require the SECONDARY_NAMENODE, which
+    // is not included in this test
+    hdfsSiteProperties.put("dfs.secondary.http.address", "localhost:8080");
+    hdfsSiteProperties.put("dfs.namenode.secondary.http-address", "localhost:8080");
+
+
+    // add properties that are used in non-HA HDFS NameNode settings
+    // to verify that these are eventually removed by the filter
+    hdfsSiteProperties.put("dfs.namenode.http-address", "localhost:8080");
+    hdfsSiteProperties.put("dfs.namenode.https-address", "localhost:8081");
+    hdfsSiteProperties.put("dfs.namenode.rpc-address", "localhost:8082");
+
+    // configure the defaultFS to use the nameservice URL
+    coreSiteProperties.put("fs.defaultFS", "hdfs://" + expectedNameService);
+
+    // configure the hbase rootdir to use the nameservice URL
+    hbaseSiteProperties.put("hbase.rootdir", "hdfs://" + expectedNameService + "/hbase/test/root/dir");
+
+    // configure the hbase rootdir to use the nameservice URL
+    accumuloSiteProperties.put("instance.volumes", "hdfs://" + expectedNameService + "/accumulo/test/instance/volumes");
+
+    Configuration clusterConfig = new Configuration(properties, emptyMap());
+
+    // Only one namenode, two is expected
+    Collection<String> hgComponents = Sets.newHashSet("NAMENODE");
+    TestHostGroup group1 = new TestHostGroup(expectedHostGroupName, hgComponents, Collections.singleton(expectedHostName));
+
+    Collection<String> hgComponents2 = Sets.newHashSet("DATANODE");
+    TestHostGroup group2 = new TestHostGroup("host-group-2", hgComponents2, Collections.singleton(expectedHostNameTwo));
+
+    Collection<TestHostGroup> hostGroups = new ArrayList<>();
+    hostGroups.add(group1);
+    hostGroups.add(group2);
+
+    expect(stack.getCardinality("NAMENODE")).andReturn(new Cardinality("1-2")).anyTimes();
+    expect(stack.getCardinality("SECONDARY_NAMENODE")).andReturn(new Cardinality("1")).anyTimes();
+
+    ClusterTopology topology = createClusterTopology(bp, clusterConfig, hostGroups);
+    BlueprintConfigurationProcessor updater = new BlueprintConfigurationProcessor(topology);
+
+    updater.doUpdateForClusterCreate();
+  }
+
+  @Test
+  public void testDoUpdateForClusterWithNameNodeHAEnabled_externalNameNodes() throws Exception {
+    final String expectedNameService = "mynameservice";
+    final String expectedHostName = "c6401.apache.ambari.org";
+    final String expectedHostNameTwo = "server-two";
+    final String expectedPortNum = "808080";
+    final String expectedNodeOne = "nn1";
+    final String expectedNodeTwo = "nn2";
+    final String expectedHostGroupName = "host_group_1";
+
+    Map<String, Map<String, String>> properties = new HashMap<>();
+
+    Map<String, String> hdfsSiteProperties = new HashMap<>();
+    Map<String, String> hbaseSiteProperties = new HashMap<>();
+    Map<String, String> hadoopEnvProperties = new HashMap<>();
+    Map<String, String> coreSiteProperties = new HashMap<>();
+    Map<String, String> accumuloSiteProperties = new HashMap<>();
+
+    properties.put("hdfs-site", hdfsSiteProperties);
+    properties.put("hadoop-env", hadoopEnvProperties);
+    properties.put("core-site", coreSiteProperties);
+    properties.put("hbase-site", hbaseSiteProperties);
+    properties.put("accumulo-site", accumuloSiteProperties);
+
+    // setup hdfs HA config for test
+    hdfsSiteProperties.put("dfs.nameservices", expectedNameService);
+    hdfsSiteProperties.put("dfs.ha.namenodes.mynameservice", expectedNodeOne + ", " + expectedNodeTwo);
+
+    // need to set these to fqdn's for external namenodes
+    hdfsSiteProperties.put("dfs.namenode.rpc-address." + expectedNameService + "." + expectedNodeOne, "externalhost1:" + expectedPortNum);
+    hdfsSiteProperties.put("dfs.namenode.rpc-address." + expectedNameService + "." + expectedNodeTwo, "externalhost2:" + expectedPortNum);
+
+    // add properties that require the SECONDARY_NAMENODE, which
+    // is not included in this test
+    hdfsSiteProperties.put("dfs.secondary.http.address", "localhost:8080");
+    hdfsSiteProperties.put("dfs.namenode.secondary.http-address", "localhost:8080");
+
+
+    // add properties that are used in non-HA HDFS NameNode settings
+    // to verify that these are eventually removed by the filter
+    hdfsSiteProperties.put("dfs.namenode.http-address", "localhost:8080");
+    hdfsSiteProperties.put("dfs.namenode.https-address", "localhost:8081");
+    hdfsSiteProperties.put("dfs.namenode.rpc-address", "localhost:8082");
+
+    // configure the defaultFS to use the nameservice URL
+    coreSiteProperties.put("fs.defaultFS", "hdfs://" + expectedNameService);
+
+    // configure the hbase rootdir to use the nameservice URL
+    hbaseSiteProperties.put("hbase.rootdir", "hdfs://" + expectedNameService + "/hbase/test/root/dir");
+
+    // configure the hbase rootdir to use the nameservice URL
+    accumuloSiteProperties.put("instance.volumes", "hdfs://" + expectedNameService + "/accumulo/test/instance/volumes");
+
+    Configuration clusterConfig = new Configuration(properties, emptyMap());
+
+    // No namenodes. This will not be an error as external namenodes are assumed in this case.
+    Collection<String> hgComponents = Sets.newHashSet("JOURNALNODE");
+    TestHostGroup group1 = new TestHostGroup(expectedHostGroupName, hgComponents, Collections.singleton(expectedHostName));
+
+    Collection<String> hgComponents2 = Sets.newHashSet("DATANODE");
+    TestHostGroup group2 = new TestHostGroup("host-group-2", hgComponents2, Collections.singleton(expectedHostNameTwo));
+
+    Collection<TestHostGroup> hostGroups = new ArrayList<>();
+    hostGroups.add(group1);
+    hostGroups.add(group2);
+
+    expect(stack.getCardinality("NAMENODE")).andReturn(new Cardinality("1-2")).anyTimes();
+    expect(stack.getCardinality("SECONDARY_NAMENODE")).andReturn(new Cardinality("1")).anyTimes();
+
+    ClusterTopology topology = createClusterTopology(bp, clusterConfig, hostGroups);
+    BlueprintConfigurationProcessor updater = new BlueprintConfigurationProcessor(topology);
+
+    // Zero namenodes means external namenodes are assumed.
+    // Validation will pass as namenode rpc addresses are set to external fqdn addresses
+    Set<String> updatedConfigTypes = updater.doUpdateForClusterCreate();
+
+    // verify that doNameNodeHAUpdateOnClusterCreation() made no updates in the abscence of namenodes
+    assertFalse("dfs.internal.nameservices shouldn't be set", hdfsSiteProperties.containsKey("dfs.internal.nameservices"));
+    Map<String, String> clusterEnv = clusterConfig.getProperties().get("cluster-env");
+    assertFalse("dfs_ha_initial_namenode_active shouldn't be set", clusterEnv.containsKey("dfs_ha_initial_namenode_active"));
+    assertFalse("dfs_ha_initial_namenode_standby shouldn't be set", clusterEnv.containsKey("dfs_ha_initial_namenode_standby"));
+
+    assertEquals("fs.defaultFS should not be modified by cluster update when NameNode HA is enabled.",
+      "hdfs://" + expectedNameService, coreSiteProperties.get("fs.defaultFS"));
+
+    assertEquals("hbase.rootdir should not be modified by cluster update when NameNode HA is enabled.",
+      "hdfs://" + expectedNameService + "/hbase/test/root/dir", hbaseSiteProperties.get("hbase.rootdir"));
+
+    assertEquals("instance.volumes should not be modified by cluster update when NameNode HA is enabled.",
+      "hdfs://" + expectedNameService + "/accumulo/test/instance/volumes", accumuloSiteProperties.get("instance.volumes"));
+
+    // verify that the non-HA properties are filtered out in HA mode
+    assertFalse("dfs.namenode.http-address should have been filtered out of this HA configuration",
+      hdfsSiteProperties.containsKey("dfs.namenode.http-address"));
+    assertFalse("dfs.namenode.https-address should have been filtered out of this HA configuration",
+      hdfsSiteProperties.containsKey("dfs.namenode.https-address"));
+    assertFalse("dfs.namenode.rpc-address should have been filtered out of this HA configuration",
+      hdfsSiteProperties.containsKey("dfs.namenode.rpc-address"));
+
+    // verify that correct configuration types were listed as updated in the returned set
+    assertEquals(ImmutableSet.of("cluster-env", "hdfs-site"), updatedConfigTypes);
+  }
+
+  @Test(expected = ConfigurationTopologyException.class)
+  public void testDoUpdateForClusterWithNameNodeHAEnabled_externalNameNodes_invalid() throws Exception {
+    final String expectedNameService = "mynameservice";
+    final String expectedHostName = "c6401.apache.ambari.org";
+    final String expectedHostNameTwo = "server-two";
+    final String expectedPortNum = "808080";
+    final String expectedNodeOne = "nn1";
+    final String expectedNodeTwo = "nn2";
+    final String expectedHostGroupName = "host_group_1";
+
+    Map<String, Map<String, String>> properties = new HashMap<>();
+
+    Map<String, String> hdfsSiteProperties = new HashMap<>();
+    Map<String, String> hbaseSiteProperties = new HashMap<>();
+    Map<String, String> hadoopEnvProperties = new HashMap<>();
+    Map<String, String> coreSiteProperties = new HashMap<>();
+    Map<String, String> accumuloSiteProperties = new HashMap<>();
+
+    properties.put("hdfs-site", hdfsSiteProperties);
+    properties.put("hadoop-env", hadoopEnvProperties);
+    properties.put("core-site", coreSiteProperties);
+    properties.put("hbase-site", hbaseSiteProperties);
+    properties.put("accumulo-site", accumuloSiteProperties);
+
+    // setup hdfs HA config for test
+    hdfsSiteProperties.put("dfs.nameservices", expectedNameService);
+    hdfsSiteProperties.put("dfs.ha.namenodes.mynameservice", expectedNodeOne + ", " + expectedNodeTwo);
+
+    // add properties that require the SECONDARY_NAMENODE, which
+    // is not included in this test
+    hdfsSiteProperties.put("dfs.secondary.http.address", "localhost:8080");
+    hdfsSiteProperties.put("dfs.namenode.secondary.http-address", "localhost:8080");
+
+
+    // add properties that are used in non-HA HDFS NameNode settings
+    // to verify that these are eventually removed by the filter
+    hdfsSiteProperties.put("dfs.namenode.http-address", "localhost:8080");
+    hdfsSiteProperties.put("dfs.namenode.https-address", "localhost:8081");
+    hdfsSiteProperties.put("dfs.namenode.rpc-address", "localhost:8082");
+
+    // configure the defaultFS to use the nameservice URL
+    coreSiteProperties.put("fs.defaultFS", "hdfs://" + expectedNameService);
+
+    // configure the hbase rootdir to use the nameservice URL
+    hbaseSiteProperties.put("hbase.rootdir", "hdfs://" + expectedNameService + "/hbase/test/root/dir");
+
+    // configure the hbase rootdir to use the nameservice URL
+    accumuloSiteProperties.put("instance.volumes", "hdfs://" + expectedNameService + "/accumulo/test/instance/volumes");
+
+    Configuration clusterConfig = new Configuration(properties, emptyMap());
+
+    // No namenodes. This will not be an error as external namenodes are assumed in this case.
+    Collection<String> hgComponents = Sets.newHashSet("JOURNALNODE");
+    TestHostGroup group1 = new TestHostGroup(expectedHostGroupName, hgComponents, Collections.singleton(expectedHostName));
+
+    Collection<String> hgComponents2 = Sets.newHashSet("DATANODE");
+    TestHostGroup group2 = new TestHostGroup("host-group-2", hgComponents2, Collections.singleton(expectedHostNameTwo));
+
+    Collection<TestHostGroup> hostGroups = new ArrayList<>();
+    hostGroups.add(group1);
+    hostGroups.add(group2);
+
+    expect(stack.getCardinality("NAMENODE")).andReturn(new Cardinality("1-2")).anyTimes();
+    expect(stack.getCardinality("SECONDARY_NAMENODE")).andReturn(new Cardinality("1")).anyTimes();
+
+    ClusterTopology topology = createClusterTopology(bp, clusterConfig, hostGroups);
+    BlueprintConfigurationProcessor updater = new BlueprintConfigurationProcessor(topology);
+
+    // Zero namenodes means external namenodes are assumed.
+    // validation will fail as namenode rpc addresses are not set (must be set to external fqdn's)
+    Set<String> updatedConfigTypes = updater.doUpdateForClusterCreate();
+  }
+
+
   @Test
   public void testDoUpdateForClusterWithNameNodeHAEnabledThreeNameNodes() throws Exception {
     final String expectedNameService = "mynameservice";
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/topology/ClusterTopologyImplTest.java b/ambari-server/src/test/java/org/apache/ambari/server/topology/ClusterTopologyImplTest.java
index 7ce5098..d157ff6 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/topology/ClusterTopologyImplTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/topology/ClusterTopologyImplTest.java
@@ -49,11 +49,11 @@ public class ClusterTopologyImplTest {
   private static final String CLUSTER_NAME = "cluster_name";
   private static final long CLUSTER_ID = 1L;
   private static final String predicate = "Hosts/host_name=foo";
-  private static final Blueprint blueprint = createNiceMock(Blueprint.class);
-  private static final HostGroup group1 = createNiceMock(HostGroup.class);
-  private static final HostGroup group2 = createNiceMock(HostGroup.class);
-  private static final HostGroup group3 = createNiceMock(HostGroup.class);
-  private static final HostGroup group4 = createNiceMock(HostGroup.class);
+  private final Blueprint blueprint = createNiceMock(Blueprint.class);
+  private final HostGroup group1 = createNiceMock(HostGroup.class);
+  private final HostGroup group2 = createNiceMock(HostGroup.class);
+  private final HostGroup group3 = createNiceMock(HostGroup.class);
+  private final HostGroup group4 = createNiceMock(HostGroup.class);
   private final Map<String, HostGroupInfo> hostGroupInfoMap = new HashMap<>();
   private final Map<String, HostGroup> hostGroupMap = new HashMap<>();
 
@@ -148,7 +148,6 @@ public class ClusterTopologyImplTest {
     verify(blueprint, group1, group2, group3, group4);
     reset(blueprint, group1, group2, group3, group4);
 
-
     hostGroupInfoMap.clear();
     hostGroupMap.clear();
   }
@@ -180,50 +179,6 @@ public class ClusterTopologyImplTest {
     new ClusterTopologyImpl(null, request).getHostAssignmentsForComponent("component1");
   }
 
-  @Test(expected = InvalidTopologyException.class)
-  public void testCreate_NNHAInvaid() throws Exception {
-    bpconfiguration.setProperty("hdfs-site", "dfs.nameservices", "val");
-    expect(group4.getName()).andReturn("group4");
-    hostGroupInfoMap.get("group4").removeHost("host5");
-    TestTopologyRequest request = new TestTopologyRequest(TopologyRequest.Type.PROVISION);
-    replayAll();
-    new ClusterTopologyImpl(null, request);
-    hostGroupInfoMap.get("group4").addHost("host5");
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void testCreate_NNHAHostNameNotCorrectForStandby() throws Exception {
-    expect(group4.getName()).andReturn("group4");
-    bpconfiguration.setProperty("hdfs-site", "dfs.nameservices", "val");
-    bpconfiguration.setProperty("hadoop-env", "dfs_ha_initial_namenode_active", "host4");
-    bpconfiguration.setProperty("hadoop-env", "dfs_ha_initial_namenode_standby", "val");
-    TestTopologyRequest request = new TestTopologyRequest(TopologyRequest.Type.PROVISION);
-    replayAll();
-    new ClusterTopologyImpl(null, request);
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void testCreate_NNHAHostNameNotCorrectForActive() throws Exception {
-    expect(group4.getName()).andReturn("group4");
-    bpconfiguration.setProperty("hdfs-site", "dfs.nameservices", "val");
-    bpconfiguration.setProperty("hadoop-env", "dfs_ha_initial_namenode_active", "val");
-    bpconfiguration.setProperty("hadoop-env", "dfs_ha_initial_namenode_standby", "host5");
-    TestTopologyRequest request = new TestTopologyRequest(TopologyRequest.Type.PROVISION);
-    replayAll();
-    new ClusterTopologyImpl(null, request);
-  }
-
-  @Test(expected = IllegalArgumentException.class)
-  public void testCreate_NNHAHostNameNotCorrectForStandbyWithActiveAsVariable() throws Exception {
-    expect(group4.getName()).andReturn("group4");
-    bpconfiguration.setProperty("hdfs-site", "dfs.nameservices", "val");
-    bpconfiguration.setProperty("hadoop-env", "dfs_ha_initial_namenode_active", "%HOSTGROUP::group4%");
-    bpconfiguration.setProperty("hadoop-env", "dfs_ha_initial_namenode_standby", "host6");
-    TestTopologyRequest request = new TestTopologyRequest(TopologyRequest.Type.PROVISION);
-    replayAll();
-    new ClusterTopologyImpl(null, request);
-  }
-
   @Test
   public void testDecidingIfComponentIsHadoopCompatible() throws Exception {
     expect(blueprint.getServiceInfos()).andReturn(asList(
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/topology/validators/NameNodeHaValidatorTest.java b/ambari-server/src/test/java/org/apache/ambari/server/topology/validators/NameNodeHaValidatorTest.java
new file mode 100644
index 0000000..54f2648
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/topology/validators/NameNodeHaValidatorTest.java
@@ -0,0 +1,219 @@
+/*
+ * Licensed 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.validators;
+
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.ambari.server.topology.ClusterTopology;
+import org.apache.ambari.server.topology.ClusterTopologyImpl;
+import org.apache.ambari.server.topology.Configuration;
+import org.apache.ambari.server.topology.HostGroupInfo;
+import org.apache.ambari.server.topology.InvalidTopologyException;
+import org.easymock.EasyMockRunner;
+import org.easymock.Mock;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+@RunWith(EasyMockRunner.class)
+public class NameNodeHaValidatorTest {
+
+  Set<String> hosts = new HashSet<>();
+  Set<String> nameNodes = new HashSet<>();
+  Map<String, HostGroupInfo> hostGroupInfos = new HashMap<>();
+  Map<String, String> hdfsSite = new HashMap<>();
+  Map<String, String> hadoopEnv = new HashMap<>();
+  Map<String, Map<String, String>> fullProperties = ImmutableMap.of("hdfs-site", hdfsSite, "hadoop-env", hadoopEnv);
+  NameNodeHaValidator validator = new NameNodeHaValidator();
+
+  @Mock
+  Configuration clusterConfig;
+
+  @Mock
+  ClusterTopology clusterTopology;
+
+  @Before
+  public void setUp() {
+    expect(clusterTopology.getConfiguration()).andReturn(clusterConfig).anyTimes();
+    expect(clusterTopology.getAllHosts()).andReturn(hosts).anyTimes();
+    expect(clusterTopology.getHostAssignmentsForComponent("NAMENODE")).andReturn(nameNodes).anyTimes();
+    expect(clusterTopology.isNameNodeHAEnabled())
+      .andAnswer(() -> ClusterTopologyImpl.isNameNodeHAEnabled(fullProperties))
+      .anyTimes();
+    expect(clusterTopology.getHostGroupInfo()).andReturn(hostGroupInfos).anyTimes();
+    expect(clusterConfig.getFullProperties()).andReturn(fullProperties).anyTimes();
+    replay(clusterTopology, clusterConfig);
+    createDefaultHdfsSite();
+    hosts.addAll(ImmutableSet.of("internalhost1", "internalhost2"));
+  }
+
+  private void createDefaultHdfsSite() {
+    hdfsSite.put("dfs.nameservices", "demo1, demo2");
+    hdfsSite.put("dfs.ha.namenodes.demo1", "nn1, nn2");
+    hdfsSite.put("dfs.ha.namenodes.demo2", "nn1, nn2");
+  }
+
+  private void addExternalNameNodesToHdfsSite() {
+    hdfsSite.put("dfs.namenode.rpc-address.demo1.nn1", "demohost1:8020");
+    hdfsSite.put("dfs.namenode.rpc-address.demo1.nn2", "demohost2:8020");
+    hdfsSite.put("dfs.namenode.rpc-address.demo2.nn1", "demohost3:8020");
+    hdfsSite.put("dfs.namenode.rpc-address.demo2.nn2", "demohost4:8020");
+  }
+
+  @Test
+  public void nonHA() throws Exception {
+     hdfsSite.clear();
+     validator.validate(clusterTopology);
+  }
+
+  @Test
+  public void externalNameNodes_properConfig1() throws Exception {
+    addExternalNameNodesToHdfsSite();
+    validator.validate(clusterTopology);
+  }
+
+  @Test
+  public void externalNameNodes_properConfig2() throws Exception {
+    addExternalNameNodesToHdfsSite();
+    hdfsSite.remove("dfs.nameservices");
+    hdfsSite.put("dfs.internal.nameservices", "demo1, demo2");
+    validator.validate(clusterTopology);
+  }
+
+  @Test(expected = InvalidTopologyException.class)
+  public void externalNameNodes_missingNameNodes() throws Exception {
+    addExternalNameNodesToHdfsSite();
+    hdfsSite.remove("dfs.ha.namenodes.demo2");
+    validator.validate(clusterTopology);
+  }
+
+  @Test(expected = InvalidTopologyException.class)
+  public void externalNameNodes_missingRpcAddress() throws Exception {
+    addExternalNameNodesToHdfsSite();
+    hdfsSite.remove("dfs.namenode.rpc-address.demo2.nn1");
+    validator.validate(clusterTopology);
+  }
+
+  @Test(expected = InvalidTopologyException.class)
+  public void externalNameNodes_localhost() throws Exception {
+    addExternalNameNodesToHdfsSite();
+    hdfsSite.put("dfs.namenode.rpc-address.demo2.nn1", "localhost:8020");
+    validator.validate(clusterTopology);
+  }
+
+  @Test(expected = InvalidTopologyException.class)
+  public void externalNameNodes_hostGroupToken() throws Exception {
+    addExternalNameNodesToHdfsSite();
+    hdfsSite.put("dfs.namenode.rpc-address.demo2.nn1", "%HOSTGROUP::group1%:8020");
+    validator.validate(clusterTopology);
+  }
+
+  @Test(expected = InvalidTopologyException.class)
+  public void externalNameNodes_missingPort() throws Exception {
+    addExternalNameNodesToHdfsSite();
+    hdfsSite.put("dfs.namenode.rpc-address.demo2.nn1", "demohost3");
+    validator.validate(clusterTopology);
+  }
+
+  @Test(expected = InvalidTopologyException.class)
+  public void externalNameNodes_internalHost() throws Exception {
+    addExternalNameNodesToHdfsSite();
+    hdfsSite.put("dfs.namenode.rpc-address.demo2.nn1", "internalhost1:8020");
+    validator.validate(clusterTopology);
+  }
+
+  @Test
+  public void validationSkippedDueToMissingHostInformation() throws Exception {
+    // this is an invalid HA topology as there is only one namenode
+    nameNodes.add(hosts.iterator().next());
+
+    // adding a host group without hosts prohibits validation
+    HostGroupInfo groupInfo = new HostGroupInfo("group1");
+    hostGroupInfos.put(groupInfo.getHostGroupName(), groupInfo);
+
+    // yet this call should not throw exception as validation should be skipped
+    validator.validate(clusterTopology);
+  }
+
+  @Test(expected = InvalidTopologyException.class)
+  public void notEnoughNameNodesForHa() throws Exception {
+    nameNodes.add(hosts.iterator().next());
+    validator.validate(clusterTopology);
+  }
+
+  @Test
+  public void haNoInitialSetup() throws Exception {
+    List<String> nameNodeHosts = ImmutableList.copyOf(hosts).subList(0, 2);
+    nameNodes.addAll(nameNodeHosts);
+    validator.validate(clusterTopology);
+  }
+
+  @Test
+  public void haProperInitialSetup() throws Exception {
+    List<String> nameNodeHosts = ImmutableList.copyOf(hosts).subList(0, 2);
+    nameNodes.addAll(nameNodeHosts);
+
+    hadoopEnv.put("dfs_ha_initial_namenode_active", nameNodeHosts.get(0));
+    hadoopEnv.put("dfs_ha_initial_namenode_active", nameNodeHosts.get(1));
+
+    validator.validate(clusterTopology);
+  }
+
+  @Test
+  public void haProperInitialSetupWithHostGroups() throws Exception {
+    List<String> nameNodeHosts = ImmutableList.copyOf(hosts).subList(0, 2);
+    nameNodes.addAll(nameNodeHosts);
+
+    hadoopEnv.put("dfs_ha_initial_namenode_active", "%HOSTGROUP::group1%");
+    hadoopEnv.put("dfs_ha_initial_namenode_standby", "%HOSTGROUP::group2%");
+
+    validator.validate(clusterTopology);
+  }
+
+  @Test(expected = InvalidTopologyException.class)
+  public void haInvalidInitialActive() throws Exception {
+    List<String> nameNodeHosts = ImmutableList.copyOf(hosts).subList(0, 2);
+    nameNodes.addAll(nameNodeHosts);
+
+    hadoopEnv.put("dfs_ha_initial_namenode_active", "externalhost");
+    hadoopEnv.put("dfs_ha_initial_namenode_standby", nameNodeHosts.get(0));
+
+    validator.validate(clusterTopology);
+  }
+
+  @Test(expected = InvalidTopologyException.class)
+  public void haInvalidInitialStandby() throws Exception {
+    List<String> nameNodeHosts = ImmutableList.copyOf(hosts).subList(0, 2);
+    nameNodes.addAll(nameNodeHosts);
+
+    hadoopEnv.put("dfs_ha_initial_namenode_active", nameNodeHosts.get(0));
+    hadoopEnv.put("dfs_ha_initial_namenode_standby", "externalhost");
+
+    validator.validate(clusterTopology);
+  }
+
+}
\ No newline at end of file