HDFS-12273. Federation UI. Contributed by Inigo Goiri.
diff --git a/hadoop-hdfs-project/hadoop-hdfs/pom.xml b/hadoop-hdfs-project/hadoop-hdfs/pom.xml
index 0fe491be..65eea31 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/pom.xml
+++ b/hadoop-hdfs-project/hadoop-hdfs/pom.xml
@@ -259,6 +259,9 @@
                 <copy file="${basedir}/src/main/webapps/proto-web.xml"
                       tofile="${project.build.directory}/webapps/nfs3/WEB-INF/web.xml"
                       filtering="true"/>
+                <copy file="${basedir}/src/main/webapps/proto-web.xml"
+                      tofile="${project.build.directory}/webapps/router/WEB-INF/web.xml"
+                      filtering="true"/>
                 <copy toDir="${project.build.directory}/webapps">
                   <fileset dir="${basedir}/src/main/webapps">
                     <exclude name="**/proto-web.xml"/>
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSConfigKeys.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSConfigKeys.java
index 48521a3..1f96763 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSConfigKeys.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/DFSConfigKeys.java
@@ -1252,6 +1252,25 @@
       FEDERATION_ROUTER_PREFIX + "admin.enable";
   public static final boolean DFS_ROUTER_ADMIN_ENABLE_DEFAULT = true;
 
+  // HDFS Router-based federation web
+  public static final String DFS_ROUTER_HTTP_ENABLE =
+      FEDERATION_ROUTER_PREFIX + "http.enable";
+  public static final boolean DFS_ROUTER_HTTP_ENABLE_DEFAULT = true;
+  public static final String DFS_ROUTER_HTTP_ADDRESS_KEY =
+      FEDERATION_ROUTER_PREFIX + "http-address";
+  public static final int    DFS_ROUTER_HTTP_PORT_DEFAULT = 50071;
+  public static final String DFS_ROUTER_HTTP_BIND_HOST_KEY =
+      FEDERATION_ROUTER_PREFIX + "http-bind-host";
+  public static final String DFS_ROUTER_HTTP_ADDRESS_DEFAULT =
+      "0.0.0.0:" + DFS_ROUTER_HTTP_PORT_DEFAULT;
+  public static final String DFS_ROUTER_HTTPS_ADDRESS_KEY =
+      FEDERATION_ROUTER_PREFIX + "https-address";
+  public static final int    DFS_ROUTER_HTTPS_PORT_DEFAULT = 50072;
+  public static final String DFS_ROUTER_HTTPS_BIND_HOST_KEY =
+      FEDERATION_ROUTER_PREFIX + "https-bind-host";
+  public static final String DFS_ROUTER_HTTPS_ADDRESS_DEFAULT =
+      "0.0.0.0:" + DFS_ROUTER_HTTPS_PORT_DEFAULT;
+
   // dfs.client.retry confs are moved to HdfsClientConfigKeys.Retry 
   @Deprecated
   public static final String  DFS_CLIENT_RETRY_POLICY_ENABLED_KEY
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/metrics/FederationMBean.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/metrics/FederationMBean.java
index 43efb3c..cb4245a 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/metrics/FederationMBean.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/metrics/FederationMBean.java
@@ -201,4 +201,11 @@
    * @return Host and port of the router.
    */
   String getBlockPoolId();
+
+  /**
+   * Get the current state of the router.
+   *
+   * @return String label for the current router state.
+   */
+  String getRouterStatus();
 }
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/metrics/FederationMetrics.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/metrics/FederationMetrics.java
index 1e80256..7844a2e 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/metrics/FederationMetrics.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/metrics/FederationMetrics.java
@@ -21,6 +21,9 @@
 
 import java.io.IOException;
 import java.lang.reflect.Method;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -34,6 +37,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.function.ToIntFunction;
 import java.util.function.ToLongFunction;
@@ -83,6 +87,9 @@
   /** Format for a date. */
   private static final String DATE_FORMAT = "yyyy/MM/dd HH:mm:ss";
 
+  /** Prevent holding the page from load too long. */
+  private static final long TIME_OUT = TimeUnit.SECONDS.toMillis(1);
+
 
   /** Router interface. */
   private final Router router;
@@ -353,8 +360,8 @@
     final Map<String, Map<String, Object>> info = new HashMap<>();
     try {
       RouterRpcServer rpcServer = this.router.getRpcServer();
-      DatanodeInfo[] live =
-          rpcServer.getDatanodeReport(DatanodeReportType.LIVE);
+      DatanodeInfo[] live = rpcServer.getDatanodeReport(
+          DatanodeReportType.LIVE, TIME_OUT);
 
       if (live.length > 0) {
         float totalDfsUsed = 0;
@@ -446,7 +453,14 @@
 
   @Override
   public String getHostAndPort() {
-    // TODO this should be the HTTP address
+    InetSocketAddress address = this.router.getHttpServerAddress();
+    if (address != null) {
+      try {
+        String hostname = InetAddress.getLocalHost().getHostName();
+        int port = address.getPort();
+        return hostname + ":" + port;
+      } catch (UnknownHostException ignored) { }
+    }
     return "Unknown";
   }
 
@@ -479,6 +493,11 @@
     }
   }
 
+  @Override
+  public String getRouterStatus() {
+    return "RUNNING";
+  }
+
   /**
    * Build a set of unique values found in all namespaces.
    *
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/resolver/MembershipNamenodeResolver.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/resolver/MembershipNamenodeResolver.java
index d974c78..0950cde 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/resolver/MembershipNamenodeResolver.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/resolver/MembershipNamenodeResolver.java
@@ -43,6 +43,7 @@
 import org.apache.hadoop.hdfs.server.federation.store.protocol.NamenodeHeartbeatRequest;
 import org.apache.hadoop.hdfs.server.federation.store.protocol.UpdateNamenodeRegistrationRequest;
 import org.apache.hadoop.hdfs.server.federation.store.records.MembershipState;
+import org.apache.hadoop.hdfs.server.federation.store.records.MembershipStats;
 import org.apache.hadoop.util.Time;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -222,6 +223,28 @@
         report.getServiceAddress(), report.getLifelineAddress(),
         report.getWebAddress(), report.getState(), report.getSafemode());
 
+    if (report.statsValid()) {
+      MembershipStats stats = MembershipStats.newInstance();
+      stats.setNumOfFiles(report.getNumFiles());
+      stats.setNumOfBlocks(report.getNumBlocks());
+      stats.setNumOfBlocksMissing(report.getNumBlocksMissing());
+      stats.setNumOfBlocksPendingReplication(
+          report.getNumOfBlocksPendingReplication());
+      stats.setNumOfBlocksUnderReplicated(
+          report.getNumOfBlocksUnderReplicated());
+      stats.setNumOfBlocksPendingDeletion(
+          report.getNumOfBlocksPendingDeletion());
+      stats.setAvailableSpace(report.getAvailableSpace());
+      stats.setTotalSpace(report.getTotalSpace());
+      stats.setNumOfDecommissioningDatanodes(
+          report.getNumDecommissioningDatanodes());
+      stats.setNumOfActiveDatanodes(report.getNumLiveDatanodes());
+      stats.setNumOfDeadDatanodes(report.getNumDeadDatanodes());
+      stats.setNumOfDecomActiveDatanodes(report.getNumDecomLiveDatanodes());
+      stats.setNumOfDecomDeadDatanodes(report.getNumDecomDeadDatanodes());
+      record.setStats(stats);
+    }
+
     if (report.getState() != UNAVAILABLE) {
       // Set/update our last contact time
       record.setLastContact(Time.now());
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/Router.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/Router.java
index 3ab5e2d..df5549c 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/Router.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/Router.java
@@ -89,6 +89,9 @@
   private RouterAdminServer adminServer;
   private InetSocketAddress adminAddress;
 
+  /** HTTP interface and web application. */
+  private RouterHttpServer httpServer;
+
   /** Interface with the State Store. */
   private StateStoreService stateStore;
 
@@ -169,6 +172,14 @@
     }
 
     if (conf.getBoolean(
+        DFSConfigKeys.DFS_ROUTER_HTTP_ENABLE,
+        DFSConfigKeys.DFS_ROUTER_HTTP_ENABLE_DEFAULT)) {
+      // Create HTTP server
+      this.httpServer = createHttpServer();
+      addService(this.httpServer);
+    }
+
+    if (conf.getBoolean(
         DFSConfigKeys.DFS_ROUTER_HEARTBEAT_ENABLE,
         DFSConfigKeys.DFS_ROUTER_HEARTBEAT_ENABLE_DEFAULT)) {
 
@@ -354,6 +365,31 @@
   }
 
   /////////////////////////////////////////////////////////
+  // HTTP server
+  /////////////////////////////////////////////////////////
+
+  /**
+   * Create an HTTP server for this Router.
+   *
+   * @return HTTP server for this Router.
+   */
+  protected RouterHttpServer createHttpServer() {
+    return new RouterHttpServer(this);
+  }
+
+  /**
+   * Get the current HTTP socket address for the router.
+   *
+   * @return InetSocketAddress HTTP address.
+   */
+  public InetSocketAddress getHttpServerAddress() {
+    if (httpServer != null) {
+      return httpServer.getHttpAddress();
+    }
+    return null;
+  }
+
+  /////////////////////////////////////////////////////////
   // Namenode heartbeat monitors
   /////////////////////////////////////////////////////////
 
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterHttpServer.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterHttpServer.java
new file mode 100644
index 0000000..046f0ba
--- /dev/null
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterHttpServer.java
@@ -0,0 +1,124 @@
+/**
+ * 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.hadoop.hdfs.server.federation.router;
+
+import java.net.InetSocketAddress;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hdfs.DFSConfigKeys;
+import org.apache.hadoop.hdfs.DFSUtil;
+import org.apache.hadoop.hdfs.server.common.JspHelper;
+import org.apache.hadoop.http.HttpServer2;
+import org.apache.hadoop.service.AbstractService;
+
+/**
+ * Web interface for the {@link Router}. It exposes the Web UI and the WebHDFS
+ * methods from {@link RouterWebHdfsMethods}.
+ */
+public class RouterHttpServer extends AbstractService {
+
+  protected static final String NAMENODE_ATTRIBUTE_KEY = "name.node";
+
+
+  /** Configuration for the Router HTTP server. */
+  private Configuration conf;
+
+  /** Router using this HTTP server. */
+  private final Router router;
+
+  /** HTTP server. */
+  private HttpServer2 httpServer;
+
+  /** HTTP addresses. */
+  private InetSocketAddress httpAddress;
+  private InetSocketAddress httpsAddress;
+
+
+  public RouterHttpServer(Router router) {
+    super(RouterHttpServer.class.getName());
+    this.router = router;
+  }
+
+  @Override
+  protected void serviceInit(Configuration configuration) throws Exception {
+    this.conf = configuration;
+
+    // Get HTTP address
+    this.httpAddress = conf.getSocketAddr(
+        DFSConfigKeys.DFS_ROUTER_HTTP_BIND_HOST_KEY,
+        DFSConfigKeys.DFS_ROUTER_HTTP_ADDRESS_KEY,
+        DFSConfigKeys.DFS_ROUTER_HTTP_ADDRESS_DEFAULT,
+        DFSConfigKeys.DFS_ROUTER_HTTP_PORT_DEFAULT);
+
+    // Get HTTPs address
+    this.httpsAddress = conf.getSocketAddr(
+        DFSConfigKeys.DFS_ROUTER_HTTPS_BIND_HOST_KEY,
+        DFSConfigKeys.DFS_ROUTER_HTTPS_ADDRESS_KEY,
+        DFSConfigKeys.DFS_ROUTER_HTTPS_ADDRESS_DEFAULT,
+        DFSConfigKeys.DFS_ROUTER_HTTPS_PORT_DEFAULT);
+
+    super.serviceInit(conf);
+  }
+
+  @Override
+  protected void serviceStart() throws Exception {
+    // Build and start server
+    String webApp = "router";
+    HttpServer2.Builder builder = DFSUtil.httpServerTemplateForNNAndJN(
+        this.conf, this.httpAddress, this.httpsAddress, webApp,
+        DFSConfigKeys.DFS_NAMENODE_KERBEROS_INTERNAL_SPNEGO_PRINCIPAL_KEY,
+        DFSConfigKeys.DFS_NAMENODE_KEYTAB_FILE_KEY);
+
+    this.httpServer = builder.build();
+
+    this.httpServer.setAttribute(NAMENODE_ATTRIBUTE_KEY, this.router);
+    this.httpServer.setAttribute(JspHelper.CURRENT_CONF, this.conf);
+    setupServlets(this.httpServer, this.conf);
+
+    this.httpServer.start();
+
+    // The server port can be ephemeral... ensure we have the correct info
+    InetSocketAddress listenAddress = this.httpServer.getConnectorAddress(0);
+    if (listenAddress != null) {
+      this.httpAddress = new InetSocketAddress(this.httpAddress.getHostName(),
+          listenAddress.getPort());
+    }
+    super.serviceStart();
+  }
+
+  @Override
+  protected void serviceStop() throws Exception {
+    if(this.httpServer != null) {
+      this.httpServer.stop();
+    }
+    super.serviceStop();
+  }
+
+  private static void setupServlets(
+      HttpServer2 httpServer, Configuration conf) {
+    // TODO Add servlets for FSCK, etc
+  }
+
+  public InetSocketAddress getHttpAddress() {
+    return this.httpAddress;
+  }
+
+  public InetSocketAddress getHttpsAddress() {
+    return this.httpsAddress;
+  }
+}
\ No newline at end of file
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterRpcClient.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterRpcClient.java
index 5c33c2e..932295e 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterRpcClient.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterRpcClient.java
@@ -35,11 +35,13 @@
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -713,6 +715,7 @@
    * Re-throws exceptions generated by the remote RPC call as either
    * RemoteException or IOException.
    *
+   * @param <T> The type of the remote location.
    * @param locations List of remote locations to call concurrently.
    * @param remoteMethod The remote method and parameters to invoke.
    * @param requireResponse If true an exception will be thrown if all calls do
@@ -723,10 +726,35 @@
    * @throws IOException If requiredResponse=true and any of the calls throw an
    *           exception.
    */
-  @SuppressWarnings("unchecked")
   public <T extends RemoteLocationContext> Map<T, Object> invokeConcurrent(
       final Collection<T> locations, final RemoteMethod method,
       boolean requireResponse, boolean standby) throws IOException {
+    return invokeConcurrent(locations, method, requireResponse, standby, -1);
+  }
+
+  /**
+   * Invokes multiple concurrent proxy calls to different clients. Returns an
+   * array of results.
+   *
+   * Re-throws exceptions generated by the remote RPC call as either
+   * RemoteException or IOException.
+   *
+   * @param locations List of remote locations to call concurrently.
+   * @param remoteMethod The remote method and parameters to invoke.
+   * @param requireResponse If true an exception will be thrown if all calls do
+   *          not complete. If false exceptions are ignored and all data results
+   *          successfully received are returned.
+   * @param standby If the requests should go to the standby namenodes too.
+   * @param timeoutMs Timeout for each individual call.
+   * @return Result of invoking the method per subcluster: nsId -> result.
+   * @throws IOException If requiredResponse=true and any of the calls throw an
+   *           exception.
+   */
+  @SuppressWarnings("unchecked")
+  public <T extends RemoteLocationContext> Map<T, Object> invokeConcurrent(
+      final Collection<T> locations, final RemoteMethod method,
+      boolean requireResponse, boolean standby, long timeOutMs)
+          throws IOException {
 
     final UserGroupInformation ugi = RouterRpcServer.getRemoteUser();
     final Method m = method.getMethod();
@@ -782,7 +810,13 @@
     }
 
     try {
-      List<Future<Object>> futures = executorService.invokeAll(callables);
+      List<Future<Object>> futures = null;
+      if (timeOutMs > 0) {
+        futures = executorService.invokeAll(
+            callables, timeOutMs, TimeUnit.MILLISECONDS);
+      } else {
+        futures = executorService.invokeAll(callables);
+      }
       Map<T, Object> results = new TreeMap<>();
       Map<T, IOException> exceptions = new TreeMap<>();
       for (int i=0; i<futures.size(); i++) {
@@ -791,6 +825,13 @@
           Future<Object> future = futures.get(i);
           Object result = future.get();
           results.put(location, result);
+        } catch (CancellationException ce) {
+          T loc = orderedLocations.get(i);
+          String msg =
+              "Invocation to \"" + loc + "\" for \"" + method + "\" timed out";
+          LOG.error(msg);
+          IOException ioe = new IOException(msg);
+          exceptions.put(location, ioe);
         } catch (ExecutionException ex) {
           Throwable cause = ex.getCause();
           LOG.debug("Canot execute {} in {}: {}",
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterRpcServer.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterRpcServer.java
index 650c6ab..4d3c237 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterRpcServer.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/federation/router/RouterRpcServer.java
@@ -1091,6 +1091,19 @@
   public DatanodeInfo[] getDatanodeReport(DatanodeReportType type)
       throws IOException {
     checkOperation(OperationCategory.UNCHECKED);
+    return getDatanodeReport(type, 0);
+  }
+
+  /**
+   * Get the datanode report with a timeout.
+   * @param type Type of the datanode.
+   * @param timeOutMs Time out for the reply in milliseconds.
+   * @return List of datanodes.
+   * @throws IOException If it cannot get the report.
+   */
+  public DatanodeInfo[] getDatanodeReport(
+      DatanodeReportType type, long timeOutMs) throws IOException {
+    checkOperation(OperationCategory.UNCHECKED);
 
     Map<String, DatanodeInfo> datanodesMap = new LinkedHashMap<>();
     RemoteMethod method = new RemoteMethod("getDatanodeReport",
@@ -1098,7 +1111,7 @@
 
     Set<FederationNamespaceInfo> nss = namenodeResolver.getNamespaces();
     Map<FederationNamespaceInfo, Object> results =
-        rpcClient.invokeConcurrent(nss, method, true, false);
+        rpcClient.invokeConcurrent(nss, method, true, false, timeOutMs);
     for (Entry<FederationNamespaceInfo, Object> entry : results.entrySet()) {
       FederationNamespaceInfo ns = entry.getKey();
       DatanodeInfo[] result = (DatanodeInfo[]) entry.getValue();
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/resources/hdfs-default.xml b/hadoop-hdfs-project/hadoop-hdfs/src/main/resources/hdfs-default.xml
index e1c5086..3491ed2 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/main/resources/hdfs-default.xml
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/resources/hdfs-default.xml
@@ -4802,6 +4802,62 @@
   </property>
 
   <property>
+    <name>dfs.federation.router.http-address</name>
+    <value>0.0.0.0:50071</value>
+    <description>
+      HTTP address that handles the web requests to the Router.
+      The value of this property will take the form of router-host1:http-port.
+    </description>
+  </property>
+
+  <property>
+    <name>dfs.federation.router.http-bind-host</name>
+    <value></value>
+    <description>
+      The actual address the HTTP server will bind to. If this optional
+      address is set, it overrides only the hostname portion of
+      dfs.federation.router.http-address. This is useful for making the name
+      node listen on all interfaces by setting it to 0.0.0.0.
+    </description>
+  </property>
+
+  <property>
+    <name>dfs.federation.router.https-address</name>
+    <value>0.0.0.0:50072</value>
+    <description>
+      HTTPS address that handles the web requests to the Router.
+      The value of this property will take the form of router-host1:https-port.
+    </description>
+  </property>
+
+  <property>
+    <name>dfs.federation.router.https-bind-host</name>
+    <value></value>
+    <description>
+      The actual address the HTTPS server will bind to. If this optional
+      address is set, it overrides only the hostname portion of
+      dfs.federation.router.https-address. This is useful for making the name
+      node listen on all interfaces by setting it to 0.0.0.0.
+    </description>
+  </property>
+
+  <property>
+    <name>dfs.federation.router.http.enable</name>
+    <value>true</value>
+    <description>
+      If the HTTP service to handle client requests in the router is enabled.
+    </description>
+  </property>
+
+  <property>
+    <name>dfs.federation.router.metrics.enable</name>
+    <value>true</value>
+    <description>
+      If the metrics service in the router is enabled.
+    </description>
+  </property>
+
+  <property>
     <name>dfs.federation.router.file.resolver.client.class</name>
     <value>org.apache.hadoop.hdfs.server.federation.MockResolver</value>
     <description>
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/router/federationhealth.html b/hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/router/federationhealth.html
new file mode 100644
index 0000000..3da5283
--- /dev/null
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/router/federationhealth.html
@@ -0,0 +1,371 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<!--
+   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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="X-UA-Compatible" content="IE=9" />
+<link rel="stylesheet" type="text/css" href="/static/bootstrap-3.0.2/css/bootstrap.min.css" />
+<link rel="stylesheet" type="text/css" href="/static/dataTables.bootstrap.css" />
+<link rel="stylesheet" type="text/css" href="/static/hadoop.css" />
+<title>Router Information</title>
+</head>
+<body>
+
+<header class="navbar navbar-inverse bs-docs-nav" role="banner">
+<div class="container">
+  <div class="navbar-header">
+    <div class="navbar-brand">Hadoop</div>
+  </div>
+
+  <ul class="nav navbar-nav" id="ui-tabs">
+    <li><a href="#tab-overview">Overview</a></li>
+    <li><a href="#tab-namenode">Subclusters</a></li>
+    <li><a href="#tab-datanode">Datanodes</a></li>
+    <li><a href="#tab-mounttable">Mount table</a></li>
+    <li class="dropdown">
+      <a href="#" class="dropdown-toggle" data-toggle="dropdown">Utilities <b class="caret"></b></a>
+      <ul class="dropdown-menu">
+        <li><a href="jmx">Metrics</a></li>
+        <li><a href="conf">Configuration</a></li>
+        <li><a href="logs">Logs</a></li>
+        <li><a href="fsck">FSCK</a></li>
+      </ul>
+    </li>
+  </ul>
+</div>
+</header>
+
+<div class="container">
+
+<div id="alert-panel">
+  <div class="alert alert-danger">
+    <button type="button" class="close" onclick="$('#alert-panel').hide();">&times;</button>
+    <div class="alert-body" id="alert-panel-body"></div>
+  </div>
+</div>
+
+<div class="tab-content">
+  <div class="tab-pane" id="tab-overview"></div>
+  <div class="tab-pane" id="tab-namenode"></div>
+  <div class="tab-pane" id="tab-datanode"></div>
+  <div class="tab-pane" id="tab-mounttable"></div>
+</div>
+
+<div class="row">
+  <hr />
+  <div class="col-xs-2"><p>Hadoop, {release-year-token}.</p></div>
+</div>
+</div>
+
+<!-- Overview -->
+<script type="text/x-dust-template" id="tmpl-federationhealth">
+<div class="page-header"><h1>Router {#federation}<small>'{HostAndPort}'</small>{/federation}</h1></div>
+{#federation}
+<table class="table table-bordered table-striped">
+  <tr><th>Started:</th><td>{RouterStarted}</td></tr>
+  <tr><th>Version:</th><td>{Version}</td></tr>
+  <tr><th>Compiled:</th><td>{CompileInfo}</td></tr>
+  <tr><th>Cluster ID:</th><td>{ClusterId}</td></tr>
+  <tr><th>Block Pool ID:</th><td>{BlockPoolId}</td></tr>
+  <tr><th>Status:</th><td>{RouterStatus}</td></tr>
+</table>
+{/federation}
+
+<div class="page-header"><h1>Summary</h1></div>
+{#federation}
+<table class="table table-bordered table-striped">
+  <tr><th>Total capacity</th><td>{TotalCapacity|fmt_bytes}</td></tr>
+  <tr><th>Used capacity</th><td>{UsedCapacity|fmt_bytes}</td></tr>
+  <tr><th>Remaining capacity</th><td>{RemainingCapacity|fmt_bytes}</td></tr>
+  <tr><th>Nameservices</th><td>{NumNameservices}</td></tr>
+  <tr><th>Namenodes</th><td>{NumNamenodes}</td></tr>
+  <tr>
+    <th>DataNodes usages% (Min/Median/Max/stdDev)</th>
+    <td>{#NodeUsage.nodeUsage}{min} / {median} / {max} / {stdDev}{/NodeUsage.nodeUsage}</td>
+  </tr>
+  <tr><th><a href="#tab-datanode">Live Nodes</a></th><td>{NumLiveNodes} (Decommissioned: {NumDecomLiveNodes})</td></tr>
+  <tr><th><a href="#tab-datanode">Dead Nodes</a></th><td>{NumDeadNodes} (Decommissioned: {NumDecomDeadNodes})</td></tr>
+  <tr><th><a href="#tab-datanode">Decommissioning Nodes</a></th><td>{NumDecommissioningNodes}</td></tr>
+  <tr><th>Files</th><td>{NumFiles}</td></tr>
+  <tr><th>Blocks</th><td>{NumBlocks}</td></tr>
+  <tr><th title="Excludes missing blocks.">Number of Under-Replicated Blocks</th><td>{NumOfBlocksUnderReplicated}</td></tr>
+  <tr><th>Number of Blocks Pending Deletion</th><td>{NumOfBlocksPendingDeletion}</td></tr>
+</table>
+{/federation}
+</script>
+
+<!-- Subclusters info: list of nameservices and namenodes -->
+<script type="text/x-dust-template" id="tmpl-namenode">
+<div class="page-header"><h1>Nameservice Information</h1></div>
+<div>
+  <ul class="dfshealth-node-legend">
+    <li class="dfshealth-node-icon dfshealth-node-alive">Active</li>
+    <li class="dfshealth-node-icon dfshealth-node-down-decommissioned">Standby</li>
+    <li class="dfshealth-node-icon dfshealth-node-down-maintenance">Safe mode</li>
+    <li class="dfshealth-node-icon dfshealth-node-down">Unavailable</li>
+  </ul>
+</div>
+<small>
+<table class="table">
+  <thead>
+    <tr>
+      <th colspan="6"></th>
+      <th colspan="3">Blocks</th>
+      <th colspan="2">Nodes</th>
+      <th colspan="3">Decom</th>
+    </tr>
+    <tr>
+      <th></th>
+      <th>Nameservice</th>
+      <th>Namenode</th>
+      <th>Last Contact</th>
+      <th>Capacity</th>
+      <th>Files</th>
+      <th>Total</th>
+      <th>Missing</th>
+      <th>Under-Replicated</th>
+      <th>Live</th>
+      <th>Dead</th>
+      <th>Progress</th>
+      <th>Live</th>
+      <th>Dead</th>
+    </tr>
+  </thead>
+  <tbody>
+    {#Nameservices}
+    <tr>
+      <td class="dfshealth-node-icon dfshealth-node-{iconState}" title="{title}"></td>
+      <td><a href="http://{webAddress}">{nameserviceId}</a></td>
+      <td><a href="http://{webAddress}">{namenodeId}</a></td>
+      <td>{lastHeartbeat}</td>
+      <td ng-value="{usedPercentage}" style="width:210px">
+        <div>
+          <div style="display:inline-block; float: left; padding-right: 10px; width:60px;">{totalSpace|fmt_bytes}</div>
+          <div class="clearfix progress dfshealth-node-capacity-bar" title="Used: {used|fmt_bytes}">
+            <div class="progress-bar {#helper_usage_bar value="{usedPercentage}"/}" style="width: {usedPercentage}%">
+            </div>
+          </div>
+        </div>
+      </td>
+      <td>{numOfFiles}</td>
+      <td>{numOfBlocks}</td>
+      <td>{numOfBlocksMissing}</td>
+      <td>{numOfBlocksUnderReplicated}</td>
+      <td>{numOfActiveDatanodes}</td>
+      <td>{numOfDeadDatanodes}</td>
+      <td>{numOfDecommissioningDatanodes}</td>
+      <td>{numOfDecomActiveDatanodes}</td>
+      <td>{numOfDecomDeadDatanodes}</td>
+    </tr>
+    {/Nameservices}
+  </tbody>
+</table>
+</small>
+
+<div class="page-header"><h1>Namenode Information</h1></div>
+<div>
+  <ul class="dfshealth-node-legend">
+    <li class="dfshealth-node-icon dfshealth-node-alive">Active</li>
+    <li class="dfshealth-node-icon dfshealth-node-down-decommissioned">Standby</li>
+    <li class="dfshealth-node-icon dfshealth-node-down-maintenance">Safe mode</li>
+    <li class="dfshealth-node-icon dfshealth-node-down">Unavailable</li>
+  </ul>
+</div>
+<small>
+<table class="table">
+  <thead>
+    <tr>
+      <th colspan="7"></th>
+      <th colspan="3">Blocks</th>
+      <th colspan="2">Nodes</th>
+      <th colspan="3">Decom</th>
+    </tr>
+    <tr>
+      <th></th>
+      <th colspan="2">Namenode</th>
+      <th>Web address</th>
+      <th>Last Contact</th>
+      <th>Capacity</th>
+      <th>Files</th>
+      <th>Total</th>
+      <th>Missing</th>
+      <th>Under-Replicated</th>
+      <th>Live</th>
+      <th>Dead</th>
+      <th>Progress</th>
+      <th>Live</th>
+      <th>Dead</th>
+    </tr>
+  </thead>
+  <tbody>
+    {#Namenodes}
+    <tr>
+      <td class="dfshealth-node-icon dfshealth-node-{iconState}" title="{title}"></td>
+      <td>{nameserviceId}</td>
+      <td>{namenodeId}</td>
+      <td><a href="http://{webAddress}">{webAddress}</a></td>
+      <td>{lastHeartbeat}</td>
+      <td ng-value="{usedPercentage}" style="width:210px">
+        <div>
+          <div style="display:inline-block; float: left; padding-right: 10px; width:60px;">{totalSpace|fmt_bytes}</div>
+          <div class="clearfix progress dfshealth-node-capacity-bar" title="Used: {used|fmt_bytes}">
+            <div class="progress-bar {#helper_usage_bar value="{usedPercentage}"/}" style="width: {usedPercentage}%">
+            </div>
+          </div>
+        </div>
+      </td>
+      <td>{numOfFiles}</td>
+      <td>{numOfBlocks}</td>
+      <td>{numOfBlocksMissing}</td>
+      <td>{numOfBlocksUnderReplicated}</td>
+      <td>{numOfActiveDatanodes}</td>
+      <td>{numOfDeadDatanodes}</td>
+      <td>{numOfDecommissioningDatanodes}</td>
+      <td>{numOfDecomActiveDatanodes}</td>
+      <td>{numOfDecomDeadDatanodes}</td>
+    </tr>
+    {/Namenodes}
+  </tbody>
+</table>
+</small>
+</script>
+
+<!-- Datanodes -->
+<script type="text/x-dust-template" id="tmpl-datanode">
+<div class="page-header"><h1>Datanode Information</h1></div>
+<div>
+  <ul class="dfshealth-node-legend">
+    <li class="dfshealth-node-icon dfshealth-node-alive">In service</li>
+    <li class="dfshealth-node-icon dfshealth-node-down">Down</li>
+    <li class="dfshealth-node-icon dfshealth-node-decommisioned">Decommisioned</li>
+    <li class="dfshealth-node-icon dfshealth-node-down-decommisioned">Decommissioned &amp; dead</li>
+  </ul>
+</div>
+<div class="page-header"><h1><small>In operation</small></h1></div>
+<small>
+<table class="table" id="table-datanodes">
+  <thead>
+    <tr>
+      <th>Node</th>
+      <th>Last contact</th>
+      <th style="width:180px; text-align:center">Capacity</th>
+      <!--th>Blocks</th-->
+      <th>Block pool used</th>
+      <!--th>Version</th-->
+    </tr>
+  </thead>
+  {#LiveNodes}
+  <tr>
+    <td ng-value="{state}-{name}" class="dfshealth-node-icon dfshealth-node-{state}">{location}/{name} ({xferaddr})</td>
+    <td ng-value="{lastContact}">{#helper_relative_time value="{lastContact}"/}</td>
+    <td ng-value="{usedPercentage}" style="width:210px">
+      <div>
+        <div style="display:inline-block; float: left; padding-right: 10px; width:60px;">{capacity|fmt_bytes}</div>
+        <div class="clearfix progress dfshealth-node-capacity-bar" title="Non DFS: {nonDfsUsedSpace|fmt_bytes}, Used: {used|fmt_bytes}">
+          <div class="progress-bar {#helper_usage_bar value="{usedPercentage}"/}" style="width: {usedPercentage}%">
+          </div>
+        </div>
+      </div>
+    </td>
+    <!--td>{numBlocks}</td-->
+    <td ng-value="{blockPoolUsedPercent}">{blockPoolUsed|fmt_bytes} ({blockPoolUsedPercent|fmt_percentage})</td>
+    <!--td>{version}</td-->
+  </tr>
+  {/LiveNodes}
+  {#DeadNodes}
+  <tr class="danger">
+    <td ng-value="{state}-{name}" class="dfshealth-node-icon dfshealth-node-{state}">{location}/{name} ({xferaddr})</td>
+    <td>{#helper_relative_time value="{lastContact}"/}</td>
+    <td></td>
+    <!--td></td-->
+    <td></td>
+    <!--td></td-->
+  </tr>
+  {/DeadNodes}
+</table>
+</small>
+
+<div class="page-header"><h1><small>Decommissioning</small></h1></div>
+<small>
+<table class="table">
+  <thead>
+    <tr>
+      <th>Node</th>
+      <th>Under replicated blocks</th>
+      <th>Blocks with no live replicas</th>
+      <th>Under Replicated Blocks <br/>In files under construction</th>
+    </tr>
+  </thead>
+  {#DecomNodes}
+  <tr>
+    <td>{location}/{name} ({xferaddr})</td>
+    <td>{underReplicatedBlocks}</td>
+    <td>{decommissionOnlyReplicas}</td>
+    <td>{underReplicateInOpenFiles}</td>
+  </tr>
+  {/DecomNodes}
+</table>
+</small>
+</script>
+
+<!-- Mount table -->
+<script type="text/x-dust-template" id="tmpl-mounttable">
+<div class="page-header"><h1>Mount Table</h1></div>
+<small>
+<table class="table">
+  <thead>
+    <tr>
+      <th>Global path</th>
+      <th>Target nameservice</th>
+      <th>Target path</th>
+      <th>Order</th>
+      <th>Read only</th>
+      <th>Date Modified</th>
+      <th>Date Created</th>
+    </tr>
+  </thead>
+  <tbody>
+    {#MountTable}
+    <tr>
+      <td>{sourcePath}</td>
+      <td>{nameserviceId}</td>
+      <td>{path}</td>
+      <td>{order}</td>
+      <td class="dfshealth-node-icon dfshealth-mount-read-only-{readonly}"/>
+      <td>{dateModified}</td>
+      <td>{dateCreated}</td>
+    </tr>
+    {/MountTable}
+  </tbody>
+</table>
+</small>
+</script>
+
+
+
+<script type="text/javascript" src="/static/jquery-1.10.2.min.js"></script>
+<script type="text/javascript" src="/static/jquery.dataTables.min.js"></script>
+<script type="text/javascript" src="/static/bootstrap-3.0.2/js/bootstrap.min.js"></script>
+<script type="text/javascript" src="/static/dataTables.bootstrap.js"></script>
+<script type="text/javascript" src="/static/moment.min.js"></script>
+<script type="text/javascript" src="/static/dust-full-2.0.0.min.js"></script>
+<script type="text/javascript" src="/static/dust-helpers-1.1.1.min.js"></script>
+<script type="text/javascript" src="/static/dfs-dust.js"></script>
+<script type="text/javascript" src="federationhealth.js"></script>
+</body>
+</html>
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/router/federationhealth.js b/hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/router/federationhealth.js
new file mode 100644
index 0000000..380f097
--- /dev/null
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/router/federationhealth.js
@@ -0,0 +1,313 @@
+/**
+ * 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.
+ */
+(function () {
+  "use strict";
+
+  dust.loadSource(dust.compile($('#tmpl-federationhealth').html(), 'federationhealth'));
+  dust.loadSource(dust.compile($('#tmpl-namenode').html(), 'namenode-info'));
+  dust.loadSource(dust.compile($('#tmpl-datanode').html(), 'datanode-info'));
+  dust.loadSource(dust.compile($('#tmpl-mounttable').html(), 'mounttable'));
+
+  $.fn.dataTable.ext.order['ng-value'] = function (settings, col)
+  {
+    return this.api().column(col, {order:'index'} ).nodes().map(function (td, i) {
+      return $(td).attr('ng-value');
+    });
+  };
+
+  function load_overview() {
+    var BEANS = [
+      {"name": "federation",      "url": "/jmx?qry=Hadoop:service=Router,name=FederationState"}
+    ];
+
+    var HELPERS = {
+      'helper_fs_max_objects': function (chunk, ctx, bodies, params) {
+        var o = ctx.current();
+        if (o.MaxObjects > 0) {
+          chunk.write('(' + Math.round((o.FilesTotal + o.BlockTotal) / o.MaxObjects * 100) * 100 + ')%');
+        }
+      },
+
+      'helper_dir_status': function (chunk, ctx, bodies, params) {
+        var j = ctx.current();
+        for (var i in j) {
+          chunk.write('<tr><td>' + i + '</td><td>' + j[i] + '</td><td>' + params.type + '</td></tr>');
+        }
+      },
+
+      'helper_date_tostring' : function (chunk, ctx, bodies, params) {
+        var value = dust.helpers.tap(params.value, chunk, ctx);
+        return chunk.write('' + new Date(Number(value)).toLocaleString());
+      }
+    };
+
+    var data = {};
+
+    // Workarounds for the fact that JMXJsonServlet returns non-standard JSON strings
+    function workaround(nn) {
+      nn.NodeUsage = JSON.parse(nn.NodeUsage);
+      return nn;
+    }
+
+    load_json(
+      BEANS,
+      guard_with_startup_progress(function(d) {
+        for (var k in d) {
+          data[k] = k === 'federation' ? workaround(d[k].beans[0]) : d[k].beans[0];
+        }
+        render();
+      }),
+      function (url, jqxhr, text, err) {
+        show_err_msg('<p>Failed to retrieve data from ' + url + ', cause: ' + err + '</p>');
+      });
+
+    function render() {
+      var base = dust.makeBase(HELPERS);
+      dust.render('federationhealth', base.push(data), function(err, out) {
+        $('#tab-overview').html(out);
+        $('#ui-tabs a[href="#tab-overview"]').tab('show');
+      });
+    }
+  }
+
+  function load_namenode_info() {
+    var HELPERS = {
+      'helper_lastcontact_tostring' : function (chunk, ctx, bodies, params) {
+        var value = dust.helpers.tap(params.value, chunk, ctx);
+        return chunk.write('' + new Date(Date.now()-1000*Number(value)));
+      }
+    };
+
+    function workaround(r) {
+      function node_map_to_array(nodes) {
+        var res = [];
+        for (var n in nodes) {
+          var p = nodes[n];
+          p.name = n;
+          res.push(p);
+        }
+        return res;
+      }
+
+      function capitalise(string) {
+          return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
+      }
+
+      function augment_namenodes(nodes) {
+        for (var i = 0, e = nodes.length; i < e; ++i) {
+          var n = nodes[i];
+          n.usedPercentage = Math.round(n.used * 1.0 / n.totalSpace * 100);
+          n.title = "Unavailable";
+          n.iconState = "down";
+          if (n.isSafeMode === true) {
+            n.title = capitalise(n.state) + " (safe mode)"
+            n.iconState = "decommisioned";
+          } else if (n.state === "ACTIVE") {
+            n.title = capitalise(n.state);
+            n.iconState = "alive";
+          } else if (nodes[i].state === "STANDBY") {
+            n.title = capitalise(n.state);
+            n.iconState = "down-decommisioned";
+          } else if (nodes[i].state === "UNAVAILABLE") {
+            n.title = capitalise(n.state);
+            n.iconState = "down";
+          }
+          if (n.namenodeId === "null") {
+            n.namenodeId = "";
+          }
+        }
+      }
+
+      r.Nameservices = node_map_to_array(JSON.parse(r.Nameservices));
+      augment_namenodes(r.Nameservices);
+      r.Namenodes = node_map_to_array(JSON.parse(r.Namenodes));
+      augment_namenodes(r.Namenodes);
+      return r;
+    }
+
+    $.get(
+      '/jmx?qry=Hadoop:service=Router,name=FederationState',
+      guard_with_startup_progress(function (resp) {
+        var data = workaround(resp.beans[0]);
+        var base = dust.makeBase(HELPERS);
+        dust.render('namenode-info', base.push(data), function(err, out) {
+          $('#tab-namenode').html(out);
+          $('#ui-tabs a[href="#tab-namenode"]').tab('show');
+        });
+      })).error(ajax_error_handler);
+  }
+
+  // TODO Copied directly from dfshealth.js; is there a way to import this function?
+  function load_datanode_info() {
+
+    var HELPERS = {
+      'helper_relative_time' : function (chunk, ctx, bodies, params) {
+        var value = dust.helpers.tap(params.value, chunk, ctx);
+        return chunk.write(moment().subtract(Number(value), 'seconds').format('YYYY-MM-DD HH:mm:ss'));
+      },
+      'helper_usage_bar' : function (chunk, ctx, bodies, params) {
+        var value = dust.helpers.tap(params.value, chunk, ctx);
+        var v = Number(value);
+        var r = null;
+        if (v < 70) {
+          r = 'progress-bar-success';
+        } else if (v < 85) {
+          r = 'progress-bar-warning';
+        } else {
+          r = "progress-bar-danger";
+        }
+        return chunk.write(r);
+      },
+    };
+
+    function workaround(r) {
+      function node_map_to_array(nodes) {
+        var res = [];
+        for (var n in nodes) {
+          var p = nodes[n];
+          p.name = n;
+          res.push(p);
+        }
+        return res;
+      }
+
+      function augment_live_nodes(nodes) {
+        for (var i = 0, e = nodes.length; i < e; ++i) {
+          var n = nodes[i];
+          n.usedPercentage = Math.round((n.used + n.nonDfsUsedSpace) * 1.0 / n.capacity * 100);
+          if (n.adminState === "In Service") {
+            n.state = "alive";
+          } else if (nodes[i].adminState === "Decommission In Progress") {
+            n.state = "decommisioning";
+          } else if (nodes[i].adminState === "Decommissioned") {
+            n.state = "decommissioned";
+          }
+        }
+      }
+
+      function augment_dead_nodes(nodes) {
+        for (var i = 0, e = nodes.length; i < e; ++i) {
+          if (nodes[i].decommissioned) {
+            nodes[i].state = "down-decommissioned";
+          } else {
+            nodes[i].state = "down";
+          }
+        }
+      }
+
+      r.LiveNodes = node_map_to_array(JSON.parse(r.LiveNodes));
+      augment_live_nodes(r.LiveNodes);
+      r.DeadNodes = node_map_to_array(JSON.parse(r.DeadNodes));
+      augment_dead_nodes(r.DeadNodes);
+      r.DecomNodes = node_map_to_array(JSON.parse(r.DecomNodes));
+      return r;
+    }
+
+    $.get(
+      '/jmx?qry=Hadoop:service=NameNode,name=NameNodeInfo',
+      guard_with_startup_progress(function (resp) {
+        var data = workaround(resp.beans[0]);
+        var base = dust.makeBase(HELPERS);
+        dust.render('datanode-info', base.push(data), function(err, out) {
+          $('#tab-datanode').html(out);
+          $('#table-datanodes').dataTable( {
+            'lengthMenu': [ [25, 50, 100, -1], [25, 50, 100, "All"] ],
+            'columns': [
+              { 'orderDataType': 'ng-value', 'searchable': true },
+              { 'orderDataType': 'ng-value', 'type': 'numeric' },
+              { 'orderDataType': 'ng-value', 'type': 'numeric' },
+              { 'orderDataType': 'ng-value', 'type': 'numeric'}
+            ]});
+          $('#ui-tabs a[href="#tab-datanode"]').tab('show');
+        });
+      })).error(ajax_error_handler);
+  }
+
+  function load_mount_table() {
+    var HELPERS = {}
+
+    function workaround(resource) {
+      resource.MountTable = JSON.parse(resource.MountTable)
+      return resource;
+    }
+
+    $.get(
+      '/jmx?qry=Hadoop:service=Router,name=FederationState',
+      guard_with_startup_progress(function (resp) {
+        var data = workaround(resp.beans[0]);
+        var base = dust.makeBase(HELPERS);
+        dust.render('mounttable', base.push(data), function(err, out) {
+          $('#tab-mounttable').html(out);
+          $('#ui-tabs a[href="#tab-mounttable"]').tab('show');
+        });
+      })).error(ajax_error_handler);
+  }
+
+  function toTitleCase(str) {
+    return str.replace(/\w\S*/g, function(txt){
+        return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
+      });
+  }
+
+  function show_err_msg(msg) {
+    $('#alert-panel-body').html(msg);
+    $('#alert-panel').show();
+  }
+
+  function ajax_error_handler(url, jqxhr, text, err) {
+    show_err_msg('<p>Failed to retrieve data from ' + url + ', cause: ' + err + '</p>');
+  }
+
+  function guard_with_startup_progress(fn) {
+    return function() {
+      try {
+        fn.apply(this, arguments);
+      } catch (err) {
+        if (err instanceof TypeError) {
+          show_err_msg('Router error: ' + err);
+        }
+      }
+    };
+  }
+
+  function load_page() {
+    var hash = window.location.hash;
+    switch(hash) {
+      case "#tab-mounttable":
+        load_mount_table();
+        break;
+      case "#tab-namenode":
+        load_namenode_info();
+        break;
+      case "#tab-datanode":
+        load_datanode_info();
+        break;
+      case "#tab-overview":
+        load_overview();
+        break;
+      default:
+        window.location.hash = "tab-overview";
+        break;
+    }
+  }
+  load_page();
+
+  $(window).bind('hashchange', function () {
+    load_page();
+  });
+})();
\ No newline at end of file
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/router/index.html b/hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/router/index.html
new file mode 100644
index 0000000..4ee04ac
--- /dev/null
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/webapps/router/index.html
@@ -0,0 +1,24 @@
+<!--
+   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.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="REFRESH" content="0;url=federationhealth.html" />
+<title>Hadoop Administration</title>
+</head>
+</html>
\ No newline at end of file
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/RouterConfigBuilder.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/RouterConfigBuilder.java
index 58ca1d1..88da77e 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/RouterConfigBuilder.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/RouterConfigBuilder.java
@@ -29,6 +29,7 @@
 
   private boolean enableRpcServer = false;
   private boolean enableAdminServer = false;
+  private boolean enableHttpServer = false;
   private boolean enableHeartbeat = false;
   private boolean enableLocalHeartbeat = false;
   private boolean enableStateStore = false;
@@ -45,6 +46,7 @@
   public RouterConfigBuilder all() {
     this.enableRpcServer = true;
     this.enableAdminServer = true;
+    this.enableHttpServer = true;
     this.enableHeartbeat = true;
     this.enableLocalHeartbeat = true;
     this.enableStateStore = true;
@@ -67,6 +69,11 @@
     return this;
   }
 
+  public RouterConfigBuilder http(boolean enable) {
+    this.enableHttpServer = enable;
+    return this;
+  }
+
   public RouterConfigBuilder heartbeat(boolean enable) {
     this.enableHeartbeat = enable;
     return this;
@@ -90,6 +97,10 @@
     return this.admin(true);
   }
 
+  public RouterConfigBuilder http() {
+    return this.http(true);
+  }
+
   public RouterConfigBuilder heartbeat() {
     return this.heartbeat(true);
   }
@@ -108,6 +119,8 @@
     conf.setBoolean(DFSConfigKeys.DFS_ROUTER_RPC_ENABLE, this.enableRpcServer);
     conf.setBoolean(DFSConfigKeys.DFS_ROUTER_ADMIN_ENABLE,
         this.enableAdminServer);
+    conf.setBoolean(DFSConfigKeys.DFS_ROUTER_HTTP_ENABLE,
+        this.enableHttpServer);
     conf.setBoolean(DFSConfigKeys.DFS_ROUTER_HEARTBEAT_ENABLE,
         this.enableHeartbeat);
     conf.setBoolean(DFSConfigKeys.DFS_ROUTER_MONITOR_LOCAL_NAMENODE,
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/RouterDFSCluster.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/RouterDFSCluster.java
index e79674d..4ea0f19 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/RouterDFSCluster.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/RouterDFSCluster.java
@@ -33,6 +33,9 @@
 import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ROUTER_DEFAULT_NAMESERVICE;
 import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ROUTER_HANDLER_COUNT_KEY;
 import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ROUTER_HEARTBEAT_INTERVAL_MS;
+import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ROUTER_HTTPS_ADDRESS_KEY;
+import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ROUTER_HTTP_ADDRESS_KEY;
+import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ROUTER_HTTP_BIND_HOST_KEY;
 import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ROUTER_MONITOR_NAMENODE;
 import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ROUTER_RPC_ADDRESS_KEY;
 import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_ROUTER_RPC_BIND_HOST_KEY;
@@ -134,6 +137,7 @@
     private String nameserviceId;
     private String namenodeId;
     private int rpcPort;
+    private int httpPort;
     private DFSClient client;
     private Configuration conf;
     private RouterClient adminClient;
@@ -164,6 +168,10 @@
       return this.rpcPort;
     }
 
+    public int getHttpPort() {
+      return this.httpPort;
+    }
+
     public FileContext getFileContext() {
       return this.fileContext;
     }
@@ -183,6 +191,10 @@
           this.fileContext = null;
         }
       }
+      InetSocketAddress httpAddress = router.getHttpServerAddress();
+      if (httpAddress != null) {
+        this.httpPort = httpAddress.getPort();
+      }
     }
 
     public FileSystem getFileSystem() throws IOException {
@@ -399,14 +411,19 @@
 
         conf.set(DFS_NAMENODE_RPC_ADDRESS_KEY + "." + suffix,
             "127.0.0.1:" + context.rpcPort);
-        conf.set(DFS_NAMENODE_SERVICE_RPC_ADDRESS_KEY + "." + suffix,
-            "127.0.0.1:" + context.servicePort);
         conf.set(DFS_NAMENODE_HTTP_ADDRESS_KEY + "." + suffix,
             "127.0.0.1:" + context.httpPort);
         conf.set(DFS_NAMENODE_RPC_BIND_HOST_KEY + "." + suffix,
             "0.0.0.0");
-        conf.set(DFS_NAMENODE_SERVICE_RPC_BIND_HOST_KEY + "." + suffix,
-            "0.0.0.0");
+
+        // If the service port is enabled by default, we need to set them up
+        boolean servicePortEnabled = false;
+        if (servicePortEnabled) {
+          conf.set(DFS_NAMENODE_SERVICE_RPC_ADDRESS_KEY + "." + suffix,
+              "127.0.0.1:" + context.servicePort);
+          conf.set(DFS_NAMENODE_SERVICE_RPC_BIND_HOST_KEY + "." + suffix,
+              "0.0.0.0");
+        }
       }
     }
 
@@ -447,6 +464,10 @@
     conf.set(DFS_ROUTER_ADMIN_ADDRESS_KEY, "127.0.0.1:0");
     conf.set(DFS_ROUTER_ADMIN_BIND_HOST_KEY, "0.0.0.0");
 
+    conf.set(DFS_ROUTER_HTTP_ADDRESS_KEY, "127.0.0.1:0");
+    conf.set(DFS_ROUTER_HTTPS_ADDRESS_KEY, "127.0.0.1:0");
+    conf.set(DFS_ROUTER_HTTP_BIND_HOST_KEY, "0.0.0.0");
+
     conf.set(DFS_ROUTER_DEFAULT_NAMESERVICE, nameservices.get(0));
     conf.setLong(DFS_ROUTER_HEARTBEAT_INTERVAL_MS, heartbeatInterval);
     conf.setLong(DFS_ROUTER_CACHE_TIME_TO_LIVE_MS, cacheFlushInterval);
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/metrics/TestMetricsBase.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/metrics/TestMetricsBase.java
index bbcfbe8..b455177 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/metrics/TestMetricsBase.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/metrics/TestMetricsBase.java
@@ -64,6 +64,7 @@
       routerConfig = new RouterConfigBuilder()
           .stateStore()
           .metrics()
+          .http()
           .build();
       router = new Router();
       router.init(routerConfig);
diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/router/TestRouter.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/router/TestRouter.java
index e22be84..77d1698 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/router/TestRouter.java
+++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/federation/router/TestRouter.java
@@ -24,6 +24,7 @@
 import java.net.URISyntaxException;
 
 import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.CommonConfigurationKeys;
 import org.apache.hadoop.hdfs.DFSConfigKeys;
 import org.apache.hadoop.hdfs.server.federation.MockResolver;
 import org.apache.hadoop.hdfs.server.federation.RouterConfigBuilder;
@@ -61,10 +62,13 @@
     conf.set(DFSConfigKeys.DFS_ROUTER_RPC_ADDRESS_KEY, "127.0.0.1:0");
     conf.set(DFSConfigKeys.DFS_ROUTER_ADMIN_ADDRESS_KEY, "127.0.0.1:0");
     conf.set(DFSConfigKeys.DFS_ROUTER_ADMIN_BIND_HOST_KEY, "0.0.0.0");
+    conf.set(DFSConfigKeys.DFS_ROUTER_HTTP_ADDRESS_KEY, "127.0.0.1:0");
+    conf.set(DFSConfigKeys.DFS_ROUTER_HTTPS_ADDRESS_KEY, "127.0.0.1:0");
+    conf.set(DFSConfigKeys.DFS_ROUTER_HTTP_BIND_HOST_KEY, "0.0.0.0");
 
     // Simulate a co-located NN
     conf.set(DFSConfigKeys.DFS_NAMESERVICES, "ns0");
-    conf.set("fs.defaultFS", "hdfs://" + "ns0");
+    conf.set(CommonConfigurationKeys.FS_DEFAULT_NAME_KEY, "hdfs://" + "ns0");
     conf.set(DFSConfigKeys.DFS_NAMENODE_RPC_ADDRESS_KEY + "." + "ns0",
             "127.0.0.1:0" + 0);
     conf.set(DFSConfigKeys.DFS_NAMENODE_HTTP_ADDRESS_KEY + "." + "ns0",
@@ -104,6 +108,9 @@
     // Admin only
     testRouterStartup(new RouterConfigBuilder(conf).admin().build());
 
+    // Http only
+    testRouterStartup(new RouterConfigBuilder(conf).http().build());
+
     // Rpc only
     testRouterStartup(new RouterConfigBuilder(conf).rpc().build());