You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by js...@apache.org on 2015/03/02 21:31:52 UTC

ambari git commit: AMBARI-9870. Provide ability to test KDC connection over UDP

Repository: ambari
Updated Branches:
  refs/heads/trunk 121924874 -> 86d658938


AMBARI-9870. Provide ability to test KDC connection over UDP


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/86d65893
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/86d65893
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/86d65893

Branch: refs/heads/trunk
Commit: 86d6589382ac0467640c2f286c950877387fd41a
Parents: 1219248
Author: John Speidel <js...@hortonworks.com>
Authored: Mon Mar 2 11:23:50 2015 -0500
Committer: John Speidel <js...@hortonworks.com>
Committed: Mon Mar 2 15:26:44 2015 -0500

----------------------------------------------------------------------
 ambari-project/pom.xml                          |   5 +
 ambari-server/pom.xml                           |   4 +
 .../server/KdcServerConnectionVerification.java | 128 ++++++++++++-
 .../KdcServerConnectionVerificationTest.java    | 190 +++++++++++++++++++
 4 files changed, 321 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/86d65893/ambari-project/pom.xml
----------------------------------------------------------------------
diff --git a/ambari-project/pom.xml b/ambari-project/pom.xml
index 0577bee..48c7214 100644
--- a/ambari-project/pom.xml
+++ b/ambari-project/pom.xml
@@ -247,6 +247,11 @@ instead of a SNAPSHOT. -->
       </dependency>
       <dependency>
         <groupId>org.apache.directory.server</groupId>
+        <artifactId>kerberos-client</artifactId>
+        <version>2.0.0-M19</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.directory.server</groupId>
         <artifactId>apacheds-protocol-ldap</artifactId>
         <version>2.0.0-M19</version>
         <exclusions>

http://git-wip-us.apache.org/repos/asf/ambari/blob/86d65893/ambari-server/pom.xml
----------------------------------------------------------------------
diff --git a/ambari-server/pom.xml b/ambari-server/pom.xml
index c57a2d0..4c56d78 100644
--- a/ambari-server/pom.xml
+++ b/ambari-server/pom.xml
@@ -1515,6 +1515,10 @@
       <scope>test</scope>
     </dependency>
     <dependency>
+      <groupId>org.apache.directory.server</groupId>
+      <artifactId>kerberos-client</artifactId>
+    </dependency>
+    <dependency>
       <groupId>org.apache.directory.shared</groupId>
       <artifactId>shared-ldap</artifactId>
       <scope>test</scope>

http://git-wip-us.apache.org/repos/asf/ambari/blob/86d65893/ambari-server/src/main/java/org/apache/ambari/server/KdcServerConnectionVerification.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/KdcServerConnectionVerification.java b/ambari-server/src/main/java/org/apache/ambari/server/KdcServerConnectionVerification.java
index 8bfbc5f..b7bfef9 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/KdcServerConnectionVerification.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/KdcServerConnectionVerification.java
@@ -22,9 +22,18 @@ import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.net.UnknownHostException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 import org.apache.ambari.server.configuration.Configuration;
 import org.apache.commons.lang.StringUtils;
+import org.apache.directory.kerberos.client.KdcConfig;
+import org.apache.directory.kerberos.client.KdcConnection;
+import org.apache.directory.shared.kerberos.exceptions.ErrorType;
+import org.apache.directory.shared.kerberos.exceptions.KerberosException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -53,6 +62,11 @@ public class KdcServerConnectionVerification {
   private static Logger LOG = LoggerFactory.getLogger(KdcServerConnectionVerification.class);
 
   private Configuration config;
+
+  /**
+   * UDP connection timeout in seconds.
+   */
+  private int udpTimeout = 10;
   
   @Inject
   public KdcServerConnectionVerification(Configuration config) {
@@ -84,15 +98,30 @@ public class KdcServerConnectionVerification {
       return false;
     }
   }
+
   /**
-   * Given server IP or hostname, checks if server is reachable i.e.
-   * we can make a socket connection to it.
-   * 
+   * Given a host and port, checks if server is reachable meaning that we
+   * can communicate with it.  First we attempt to connect via TCP and if
+   * that is unsuccessful, attempt via UDP. It is important to understand that
+   * we are not validating credentials, only attempting to communicate with server
+   * process for the give host and port.
+   *
    * @param server KDC server IP or hostname
    * @param port	 KDC port
    * @return	true, if server is accepting connection given port; false otherwise.
    */
-  public boolean isKdcReachable(String server, Integer port) {
+  public boolean isKdcReachable(String server, int port) {
+    return isKdcReachableViaTCP(server, port) || isKdcReachableViaUDP(server, port);
+  }
+
+  /**
+   * Attempt to connect to KDC server over TCP.
+   * 
+   * @param server KDC server IP or hostname
+   * @param port	 KDC server port
+   * @return	true, if server is accepting connection given port; false otherwise.
+   */
+  public boolean isKdcReachableViaTCP(String server, int port) {
     Socket socket = null;
     try {
       socket = new Socket();
@@ -117,17 +146,104 @@ public class KdcServerConnectionVerification {
   }
 
   /**
+   * Attempt to communicate with KDC server over UDP.
+   * @param server KDC hostname or IP address
+   * @param port   KDC server port
+   * @return  true if communication is successful; false otherwise
+   */
+  public boolean isKdcReachableViaUDP(final String server, final int port) {
+    int timeoutMillis = udpTimeout * 1000;
+    final KdcConfig config = KdcConfig.getDefaultConfig();
+    config.setHostName(server);
+    config.setKdcPort(port);
+    config.setUseUdp(true);
+    config.setTimeout(timeoutMillis);
+
+    final KdcConnection connection = getKdcUdpConnection(config);
+    FutureTask<Boolean> future = new FutureTask<Boolean>(new Callable<Boolean>() {
+      @Override
+      public Boolean call() {
+        try {
+          // we are only testing whether we can communicate with server and not
+          // validating credentials
+          connection.getTgt("noUser@noRealm", "noPassword");
+        } catch (KerberosException e) {
+          // unfortunately, need to look at msg as error 60 is a generic error code
+          return ! (e.getErrorCode() == ErrorType.KRB_ERR_GENERIC.getValue() &&
+                    e.getMessage().contains("TimeOut"));
+          //todo: evaluate other error codes to provide better information
+          //todo: as there may be other error codes where we should return false
+        } catch (Exception e) {
+          // some bad unexpected thing occurred
+          throw new RuntimeException(e);
+        }
+        return true;
+      }
+    });
+
+    new Thread(future).start();
+    Boolean result;
+    try {
+      // timeout after specified timeout
+      result = future.get(timeoutMillis, TimeUnit.MILLISECONDS);
+    } catch (InterruptedException e) {
+      LOG.error("Interrupted while trying to communicate with KDC server over UDP");
+      result = false;
+      future.cancel(true);
+    } catch (ExecutionException e) {
+      LOG.error("An unexpected exception occurred while attempting to communicate with the KDC server over UDP", e);
+      result = false;
+    } catch (TimeoutException e) {
+      LOG.error("Timeout occurred while attempting to to communicate with KDC server over UDP");
+      result = false;
+      future.cancel(true);
+    }
+
+    return result;
+  }
+
+  /**
+   * Get a KDC UDP connection for the given configuration.
+   * This has been extracted into it's own method primarily
+   * for unit testing purposes.
+   *
+   * @param config KDC connection configuration
+   * @return new KDC connection
+   */
+  protected KdcConnection getKdcUdpConnection(KdcConfig config) {
+    return new KdcConnection(config);
+  }
+
+  /**
+   * Set the UDP connection timeout.
+   * This is the amount of time that we will attempt to read data from UDP connection.
+   *
+   * @param timeoutSeconds  timeout in seconds
+   */
+  public void setUdpTimeout(int timeoutSeconds) {
+    udpTimeout = (timeoutSeconds < 1) ? 1 : timeoutSeconds;
+  }
+
+  /**
+   * Get the UDP timeout value.
+   *
+   * @return the UDP connection timeout value in seconds
+   */
+  public int getUdpTimeout() {
+    return udpTimeout;
+  }
+
+  /**
    * Parses port number from given string.
    * @param port port number string
    * @throws NumberFormatException if given string cannot be parsed
    * @throws IllegalArgumentException if given string is null or empty
    * @return parsed port number
    */
-  private final int parsePort(String port) {
+  private int parsePort(String port) {
     if (StringUtils.isEmpty(port)) {
       throw new IllegalArgumentException("Port number must be non-empty, non-null positive integer");
     }
     return Integer.parseInt(port);
   }
-
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/86d65893/ambari-server/src/test/java/org/apache/ambari/server/api/rest/KdcServerConnectionVerificationTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/api/rest/KdcServerConnectionVerificationTest.java b/ambari-server/src/test/java/org/apache/ambari/server/api/rest/KdcServerConnectionVerificationTest.java
index f8ec650..da47eb2 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/api/rest/KdcServerConnectionVerificationTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/api/rest/KdcServerConnectionVerificationTest.java
@@ -17,6 +17,11 @@
  */
 package org.apache.ambari.server.api.rest;
 
+import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -29,12 +34,18 @@ import org.apache.ambari.server.KdcServerConnectionVerification;
 import org.apache.ambari.server.configuration.Configuration;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+import org.apache.directory.kerberos.client.KdcConfig;
+import org.apache.directory.kerberos.client.KdcConnection;
+import org.apache.directory.kerberos.client.TgTicket;
+import org.apache.directory.shared.kerberos.exceptions.ErrorType;
+import org.apache.directory.shared.kerberos.exceptions.KerberosException;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.springframework.test.annotation.ExpectedException;
 
+
 /**
  * Test for {@link KdcServerConnectionVerification}
  */
@@ -100,6 +111,141 @@ public class KdcServerConnectionVerificationTest  {
     assertFalse(kdcConnectionVerifier.isKdcReachable("localhost:abc"));
   }
 
+  @Test
+  public void testValidateUDP__Successful() throws Exception {
+    KdcConnection connection = createStrictMock(KdcConnection.class);
+
+    expect(connection.getTgt("noUser@noRealm", "noPassword")).andReturn(null).once();
+    replay(connection);
+
+    TestKdcServerConnectionVerification kdcConnVerifier =
+        new TestKdcServerConnectionVerification(configuration, connection);
+
+    boolean result = kdcConnVerifier.isKdcReachableViaUDP("test-host", 11111);
+    assertTrue(result);
+
+    KdcConfig kdcConfig = kdcConnVerifier.getConfigUsedInConnectionCreation();
+    assertTrue(kdcConfig.isUseUdp());
+    assertEquals("test-host", kdcConfig.getHostName());
+    assertEquals(11111, kdcConfig.getKdcPort());
+    assertEquals(10 * 1000, kdcConfig.getTimeout());
+
+    verify(connection);
+  }
+
+  @Test
+  public void testValidateUDP__Successful2() throws Exception {
+    KdcConnection connection = createStrictMock(KdcConnection.class);
+
+    expect(connection.getTgt("noUser@noRealm", "noPassword")).andThrow(
+        new KerberosException(ErrorType.KDC_ERR_C_PRINCIPAL_UNKNOWN));
+    replay(connection);
+
+    TestKdcServerConnectionVerification kdcConnVerifier =
+        new TestKdcServerConnectionVerification(configuration, connection);
+
+    boolean result = kdcConnVerifier.isKdcReachableViaUDP("test-host", 11111);
+    assertTrue(result);
+
+    KdcConfig kdcConfig = kdcConnVerifier.getConfigUsedInConnectionCreation();
+    assertTrue(kdcConfig.isUseUdp());
+    assertEquals("test-host", kdcConfig.getHostName());
+    assertEquals(11111, kdcConfig.getKdcPort());
+    assertEquals(10 * 1000, kdcConfig.getTimeout());
+
+    verify(connection);
+  }
+
+  @Test
+  public void testValidateUDP__Fail_UnknownException() throws Exception {
+    KdcConnection connection = createStrictMock(KdcConnection.class);
+
+    expect(connection.getTgt("noUser@noRealm", "noPassword")).andThrow(
+        new RuntimeException("This is a really bad exception"));
+    replay(connection);
+
+    TestKdcServerConnectionVerification kdcConnVerifier =
+        new TestKdcServerConnectionVerification(configuration, connection);
+
+    boolean result = kdcConnVerifier.isKdcReachableViaUDP("test-host", 11111);
+    assertFalse(result);
+
+    KdcConfig kdcConfig = kdcConnVerifier.getConfigUsedInConnectionCreation();
+    assertTrue(kdcConfig.isUseUdp());
+    assertEquals("test-host", kdcConfig.getHostName());
+    assertEquals(11111, kdcConfig.getKdcPort());
+    assertEquals(10 * 1000, kdcConfig.getTimeout());
+
+    verify(connection);
+  }
+
+  @Test
+  public void testValidateUDP__Fail_Timeout() throws Exception {
+    int timeout = 1;
+    KdcConnection connection = new BlockingKdcConnection(null);
+
+    TestKdcServerConnectionVerification kdcConnVerifier =
+        new TestKdcServerConnectionVerification(configuration, connection);
+
+    kdcConnVerifier.setUdpTimeout(timeout);
+
+    boolean result = kdcConnVerifier.isKdcReachableViaUDP("test-host", 11111);
+    assertFalse(result);
+
+    KdcConfig kdcConfig = kdcConnVerifier.getConfigUsedInConnectionCreation();
+    assertTrue(kdcConfig.isUseUdp());
+    assertEquals("test-host", kdcConfig.getHostName());
+    assertEquals(11111, kdcConfig.getKdcPort());
+    assertEquals(timeout * 1000, kdcConfig.getTimeout());
+  }
+
+  @Test
+  public void testValidateUDP__Fail_TimeoutErrorCode() throws Exception {
+    KdcConnection connection = createStrictMock(KdcConnection.class);
+
+    expect(connection.getTgt("noUser@noRealm", "noPassword")).andThrow(
+        new KerberosException(ErrorType.KRB_ERR_GENERIC, "TimeOut occurred"));
+    replay(connection);
+
+    TestKdcServerConnectionVerification kdcConnVerifier =
+        new TestKdcServerConnectionVerification(configuration, connection);
+
+    boolean result = kdcConnVerifier.isKdcReachableViaUDP("test-host", 11111);
+    assertFalse(result);
+
+    KdcConfig kdcConfig = kdcConnVerifier.getConfigUsedInConnectionCreation();
+    assertTrue(kdcConfig.isUseUdp());
+    assertEquals("test-host", kdcConfig.getHostName());
+    assertEquals(11111, kdcConfig.getKdcPort());
+    assertEquals(10 * 1000, kdcConfig.getTimeout());
+
+    verify(connection);
+  }
+
+  @Test
+  public void testValidateUDP__Fail_GeneralErrorCode_NotTimeout() throws Exception {
+    KdcConnection connection = createStrictMock(KdcConnection.class);
+
+    expect(connection.getTgt("noUser@noRealm", "noPassword")).andThrow(
+        new KerberosException(ErrorType.KRB_ERR_GENERIC, "foo"));
+    replay(connection);
+
+    TestKdcServerConnectionVerification kdcConnVerifier =
+        new TestKdcServerConnectionVerification(configuration, connection);
+
+    boolean result = kdcConnVerifier.isKdcReachableViaUDP("test-host", 11111);
+    assertTrue(result);
+
+    KdcConfig kdcConfig = kdcConnVerifier.getConfigUsedInConnectionCreation();
+    assertTrue(kdcConfig.isUseUdp());
+    assertEquals("test-host", kdcConfig.getHostName());
+    assertEquals(11111, kdcConfig.getKdcPort());
+    assertEquals(10 * 1000, kdcConfig.getTimeout());
+
+    verify(connection);
+  }
+
+
   /**
    * Socket server for test
    * We need a separate thread as accept() is a blocking call
@@ -132,4 +278,48 @@ public class KdcServerConnectionVerificationTest  {
       LOG.debug("IOException during tearDown. Can be safely ignored");
     }
   }
+
+  // Test implementation which allows a mock KDC connection to be used.
+  private static class TestKdcServerConnectionVerification extends KdcServerConnectionVerification {
+    private KdcConnection connection;
+    private KdcConfig kdcConfig = null;
+
+    public TestKdcServerConnectionVerification(Configuration config, KdcConnection connectionMock) {
+      super(config);
+      connection = connectionMock;
+    }
+
+    @Override
+    protected KdcConnection getKdcUdpConnection(KdcConfig config) {
+      kdcConfig = config;
+      return connection;
+    }
+
+    public KdcConfig getConfigUsedInConnectionCreation() {
+      return kdcConfig;
+    }
+  }
+
+  /**
+   * Test implementation which blocks on getTgt() for 60 seconds to facilitate timeout testing.
+   */
+  private static class BlockingKdcConnection extends KdcConnection {
+
+    public BlockingKdcConnection(KdcConfig config) {
+      super(config);
+    }
+
+    @Override
+    public TgTicket getTgt(String principal, String password) throws Exception {
+      // although it is generally a bad idea to use sleep in a unit test for a
+      // timing mechanism, this is being used to simulate a timeout and should be
+      // generally safe as we are not relying on this for timing other than expecting
+      // that this will block longer than the timeout set on the connection validator
+      // which should be set to 1 second when using this implementation.
+      // We will only block the full 60 seconds in the case of a specific test failure
+      // where the callable doesn't properly set the timeout on the get.
+      Thread.sleep(60000);
+      return null;
+    }
+  }
 }