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 <jspei...@hortonworks.com> Authored: Mon Mar 2 11:23:50 2015 -0500 Committer: John Speidel <jspei...@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; + } + } }