This is an automated email from the ASF dual-hosted git repository. reidchan pushed a commit to branch branch-1 in repository https://gitbox.apache.org/repos/asf/hbase.git
The following commit(s) were added to refs/heads/branch-1 by this push: new 4af4afc HBASE-21048 Get LogLevel is not working from console in secure environment 4af4afc is described below commit 4af4afc94f2836400b716dfaeef2c661064eb4fe Author: Wei-Chiu Chuang <weic...@cloudera.com> AuthorDate: Tue Apr 16 13:58:46 2019 -0700 HBASE-21048 Get LogLevel is not working from console in secure environment Signed-off-by: Reid Chan <reidc...@apache.org> --- .../org/apache/hadoop/hbase/http/log/LogLevel.java | 236 +++++++++++-- .../apache/hadoop/hbase/http/log/TestLogLevel.java | 374 +++++++++++++++++---- 2 files changed, 514 insertions(+), 96 deletions(-) diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java index 7701a25..328e1b1 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java @@ -17,6 +17,9 @@ */ package org.apache.hadoop.hbase.http.log; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -34,59 +37,223 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.impl.Jdk14Logger; import org.apache.commons.logging.impl.Log4JLogger; +import org.apache.hadoop.HadoopIllegalArgumentException; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.conf.Configured; import org.apache.hadoop.hbase.classification.InterfaceAudience; import org.apache.hadoop.hbase.classification.InterfaceStability; import org.apache.hadoop.hbase.http.HttpServer; +import org.apache.hadoop.security.authentication.client.AuthenticatedURL; +import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; import org.apache.hadoop.util.ServletUtil; +import org.apache.hadoop.util.Tool; /** * Change log level in runtime. */ @InterfaceStability.Evolving public class LogLevel { - public static final String USAGES = "\nUsage: General options are:\n" - + "\t[-getlevel <host:httpPort> <name>]\n" - + "\t[-setlevel <host:httpPort> <name> <level>]\n"; + private static final String USAGES = "\nUsage: General options are:\n" + + "\t[-getlevel <host:port> <classname>\n" + + "\t[-setlevel <host:port> <classname> <level> "; + public static final String PROTOCOL_HTTP = "http"; /** * A command line implementation */ - public static void main(String[] args) { - if (args.length == 3 && "-getlevel".equals(args[0])) { - process("http://" + args[1] + "/logLevel?log=" + args[2]); - return; - } - else if (args.length == 4 && "-setlevel".equals(args[0])) { - process("http://" + args[1] + "/logLevel?log=" + args[2] - + "&level=" + args[3]); - return; - } + public static void main(String[] args) throws Exception { + CLI cli = new CLI(new Configuration()); + System.exit(cli.run(args)); + } + + /** + * Valid command line options. + */ + private enum Operations { + GETLEVEL, + SETLEVEL, + UNKNOWN + } + private static void printUsage() { System.err.println(USAGES); System.exit(-1); } - private static void process(String urlstring) { - try { - URL url = new URL(urlstring); - System.out.println("Connecting to " + url); - URLConnection connection = url.openConnection(); + @VisibleForTesting + static class CLI extends Configured implements Tool { + private Operations operation = Operations.UNKNOWN; + private String hostName; + private String className; + private String level; + + CLI(Configuration conf) { + setConf(conf); + } + + @Override + public int run(String[] args) throws Exception { + try { + parseArguments(args); + sendLogLevelRequest(); + } catch (HadoopIllegalArgumentException e) { + printUsage(); + } + return 0; + } + + /** + * Send HTTP request to the daemon. + * @throws HadoopIllegalArgumentException if arguments are invalid. + * @throws Exception if unable to connect + */ + private void sendLogLevelRequest() + throws HadoopIllegalArgumentException, Exception { + switch (operation) { + case GETLEVEL: + doGetLevel(); + break; + case SETLEVEL: + doSetLevel(); + break; + default: + throw new HadoopIllegalArgumentException( + "Expect either -getlevel or -setlevel"); + } + } + + public void parseArguments(String[] args) throws + HadoopIllegalArgumentException { + if (args.length == 0) { + throw new HadoopIllegalArgumentException("No arguments specified"); + } + int nextArgIndex = 0; + while (nextArgIndex < args.length) { + switch (args[nextArgIndex]) { + case "-getlevel": + nextArgIndex = parseGetLevelArgs(args, nextArgIndex); + break; + case "-setlevel": + nextArgIndex = parseSetLevelArgs(args, nextArgIndex); + break; + default: + throw new HadoopIllegalArgumentException( + "Unexpected argument " + args[nextArgIndex]); + } + } + + // if operation is never specified in the arguments + if (operation == Operations.UNKNOWN) { + throw new HadoopIllegalArgumentException( + "Must specify either -getlevel or -setlevel"); + } + } + + private int parseGetLevelArgs(String[] args, int index) throws + HadoopIllegalArgumentException { + // fail if multiple operations are specified in the arguments + if (operation != Operations.UNKNOWN) { + throw new HadoopIllegalArgumentException("Redundant -getlevel command"); + } + // check number of arguments is sufficient + if (index + 2 >= args.length) { + throw new HadoopIllegalArgumentException("-getlevel needs two parameters"); + } + operation = Operations.GETLEVEL; + hostName = args[index + 1]; + className = args[index + 2]; + return index + 3; + } + + private int parseSetLevelArgs(String[] args, int index) throws + HadoopIllegalArgumentException { + // fail if multiple operations are specified in the arguments + if (operation != Operations.UNKNOWN) { + throw new HadoopIllegalArgumentException("Redundant -setlevel command"); + } + // check number of arguments is sufficient + if (index + 3 >= args.length) { + throw new HadoopIllegalArgumentException("-setlevel needs three parameters"); + } + operation = Operations.SETLEVEL; + hostName = args[index + 1]; + className = args[index + 2]; + level = args[index + 3]; + return index + 4; + } + + /** + * Send HTTP request to get log level. + * + * @throws HadoopIllegalArgumentException if arguments are invalid. + * @throws Exception if unable to connect + */ + private void doGetLevel() throws Exception { + process(PROTOCOL_HTTP + "://" + hostName + "/logLevel?log=" + className); + } + + /** + * Send HTTP request to set log level. + * + * @throws HadoopIllegalArgumentException if arguments are invalid. + * @throws Exception if unable to connect + */ + private void doSetLevel() throws Exception { + process(PROTOCOL_HTTP + "://" + hostName + "/logLevel?log=" + className + + "&level=" + level); + } + + /** + * Connect to the URL. Supports HTTP and supports SPNEGO + * authentication. It falls back to simple authentication if it fails to + * initiate SPNEGO. + * + * @param url the URL address of the daemon servlet + * @return a connected connection + * @throws Exception if it can not establish a connection. + */ + private URLConnection connect(URL url) throws Exception { + AuthenticatedURL.Token token = new AuthenticatedURL.Token(); + AuthenticatedURL aUrl; + URLConnection connection; + + aUrl = new AuthenticatedURL(new KerberosAuthenticator()); + connection = aUrl.openConnection(url, token); connection.connect(); + return connection; + } - BufferedReader in = new BufferedReader(new InputStreamReader( - connection.getInputStream())); - for(String line; (line = in.readLine()) != null; ) - if (line.startsWith(MARKER)) { - System.out.println(TAG.matcher(line).replaceAll("")); + /** + * Configures the client to send HTTP request to the URL. + * Supports SPENGO for authentication. + * @param urlString URL and query string to the daemon's web UI + * @throws Exception if unable to connect + */ + private void process(String urlString) throws Exception { + URL url = new URL(urlString); + System.out.println("Connecting to " + url); + + URLConnection connection = connect(url); + + // read from the servlet + + try (InputStreamReader streamReader = + new InputStreamReader(connection.getInputStream(), Charsets.UTF_8); + BufferedReader bufferedReader = new BufferedReader(streamReader)) { + String line; + while((line = bufferedReader.readLine()) != null) { + if (line.startsWith(MARKER)) { + System.out.println(TAG.matcher(line).replaceAll("")); + } } - in.close(); - } catch (IOException ioe) { - System.err.println("" + ioe); + } catch (IOException ioe) { + System.err.println("" + ioe); + } } } - static final String MARKER = "<!-- OUTPUT -->"; - static final Pattern TAG = Pattern.compile("<[^>]*>"); + private static final String MARKER = "<!-- OUTPUT -->"; + private static final Pattern TAG = Pattern.compile("<[^>]*>"); /** * A servlet implementation @@ -97,8 +264,8 @@ public class LogLevel { private static final long serialVersionUID = 1L; @Override - public void doGet(HttpServletRequest request, HttpServletResponse response - ) throws ServletException, IOException { + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { // Do the authorization if (!HttpServer.hasAdministratorAccess(getServletContext(), request, @@ -146,8 +313,7 @@ public class LogLevel { + "<input type='submit' value='Set Log Level' />" + "</form>"; - private static void process(org.apache.log4j.Logger log, String level, - PrintWriter out) throws IOException { + private static void process(org.apache.log4j.Logger log, String level, PrintWriter out) { if (level != null) { if (!level.equals(org.apache.log4j.Level.toLevel(level).toString())) { out.println(MARKER + "Bad level : <b>" + level + "</b><br />"); @@ -161,14 +327,16 @@ public class LogLevel { } private static void process(java.util.logging.Logger log, String level, - PrintWriter out) throws IOException { + PrintWriter out) { if (level != null) { log.setLevel(java.util.logging.Level.parse(level)); out.println(MARKER + "Setting Level to " + level + " ...<br />"); } java.util.logging.Level lev; - for(; (lev = log.getLevel()) == null; log = log.getParent()); + while ((lev = log.getLevel()) == null) { + log = log.getParent(); + } out.println(MARKER + "Effective level: <b>" + lev + "</b><br />"); } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java index 15efb71..224bfcb 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/http/log/TestLogLevel.java @@ -17,81 +17,331 @@ */ package org.apache.hadoop.hbase.http.log; +import static org.apache.hadoop.hbase.http.log.LogLevel.PROTOCOL_HTTP; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; -import java.io.*; -import java.net.*; +import java.io.File; +import java.net.BindException; +import java.net.URI; +import java.security.PrivilegedExceptionAction; +import java.util.Properties; -import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.apache.commons.io.FileUtils; +import org.apache.hadoop.HadoopIllegalArgumentException; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CommonConfigurationKeys; +import org.apache.hadoop.fs.CommonConfigurationKeysPublic; +import org.apache.hadoop.fs.FileUtil; +import org.apache.hadoop.hbase.HBaseCommonTestingUtility; import org.apache.hadoop.hbase.http.HttpServer; +import org.apache.hadoop.hbase.http.log.LogLevel.CLI; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.apache.hadoop.minikdc.MiniKdc; import org.apache.hadoop.net.NetUtils; -import org.apache.commons.logging.*; -import org.apache.commons.logging.impl.*; -import org.apache.log4j.*; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.authorize.AccessControlList; +import org.apache.hadoop.test.GenericTestUtils; +import org.apache.log4j.Level; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; -@Category(SmallTests.class) +/** + * Test LogLevel. + */ +@Category({MiscTests.class, SmallTests.class}) public class TestLogLevel { - static final PrintStream out = System.out; - - @Test (timeout=60000) - @SuppressWarnings("deprecation") - public void testDynamicLogLevel() throws Exception { - String logName = TestLogLevel.class.getName(); - Log testlog = LogFactory.getLog(logName); - - //only test Log4JLogger - if (testlog instanceof Log4JLogger) { - Logger log = ((Log4JLogger)testlog).getLogger(); - log.debug("log.debug1"); - log.info("log.info1"); - log.error("log.error1"); - assertTrue(!Level.ERROR.equals(log.getEffectiveLevel())); - - HttpServer server = null; + private static File BASEDIR; + private static Configuration serverConf; + private static Configuration clientConf; + private static final String logName = TestLogLevel.class.getName(); + private static final Logger log = LogManager.getLogger(logName); + private final static String PRINCIPAL = "loglevel.principal"; + private final static String KEYTAB = "loglevel.keytab"; + + private static MiniKdc kdc; + private static HBaseCommonTestingUtility htu = new HBaseCommonTestingUtility(); + + private static final String LOCALHOST = "localhost"; + private static final String clientPrincipal = "client/" + LOCALHOST; + private static String HTTP_PRINCIPAL = "HTTP/" + LOCALHOST; + + private static final File KEYTAB_FILE = new File( + htu.getDataTestDir("keytab").toUri().getPath()); + + @BeforeClass + public static void setUp() throws Exception { + BASEDIR = new File(htu.getDataTestDir().toUri().getPath()); + + FileUtil.fullyDelete(BASEDIR); + if (!BASEDIR.mkdirs()) { + throw new Exception("unable to create the base directory for testing"); + } + serverConf = new Configuration(); + clientConf = new Configuration(); + + kdc = setupMiniKdc(); + // Create two principles: a client and a HTTP principal + kdc.createPrincipal(KEYTAB_FILE, clientPrincipal, HTTP_PRINCIPAL); + } + + /** + * Sets up {@link MiniKdc} for testing security. + * Copied from HBaseTestingUtility#setupMiniKdc(). + */ + static private MiniKdc setupMiniKdc() throws Exception { + Properties conf = MiniKdc.createConf(); + conf.put(MiniKdc.DEBUG, true); + MiniKdc kdc = null; + File dir = null; + // There is time lag between selecting a port and trying to bind with it. It's possible that + // another service captures the port in between which'll result in BindException. + boolean bindException; + int numTries = 0; + do { try { - server = new HttpServer.Builder().setName("..") - .addEndpoint(new URI("http://localhost:0")).setFindPort(true) - .build(); - - server.start(); - String authority = NetUtils.getHostPortString(server - .getConnectorAddress(0)); - - //servlet - URL url = new URL("http://" + authority + "/logLevel?log=" + logName - + "&level=" + Level.ERROR); - out.println("*** Connecting to " + url); - HttpURLConnection connection = (HttpURLConnection)url.openConnection(); - connection.connect(); - - BufferedReader in = new BufferedReader(new InputStreamReader( - connection.getInputStream())); - for(String line; (line = in.readLine()) != null; out.println(line)); - in.close(); - connection.disconnect(); - - log.debug("log.debug2"); - log.info("log.info2"); - log.error("log.error2"); - assertTrue(Level.ERROR.equals(log.getEffectiveLevel())); - - //command line - String[] args = {"-setlevel", authority, logName, Level.DEBUG.toString()}; - LogLevel.main(args); - log.debug("log.debug3"); - log.info("log.info3"); - log.error("log.error3"); - assertTrue(Level.DEBUG.equals(log.getEffectiveLevel())); - } finally { - if (server != null) { - server.stop(); + bindException = false; + dir = new File(htu.getDataTestDir("kdc").toUri().getPath()); + kdc = new MiniKdc(conf, dir); + kdc.start(); + } catch (BindException e) { + FileUtils.deleteDirectory(dir); // clean directory + numTries++; + if (numTries == 3) { + log.error("Failed setting up MiniKDC. Tried " + numTries + " times."); + throw e; } + log.error("BindException encountered when setting up MiniKdc. Trying again."); + bindException = true; } + } while (bindException); + return kdc; + } + + @AfterClass + public static void tearDown() { + if (kdc != null) { + kdc.stop(); + } + + FileUtil.fullyDelete(BASEDIR); + } + + /** + * Test client command line options. Does not validate server behavior. + * @throws Exception if commands return unexpected results. + */ + @Test(timeout=120000) + public void testCommandOptions() throws Exception { + final String className = this.getClass().getName(); + + assertFalse(validateCommand(new String[] {"-foo" })); + // fail due to insufficient number of arguments + assertFalse(validateCommand(new String[] {})); + assertFalse(validateCommand(new String[] {"-getlevel" })); + assertFalse(validateCommand(new String[] {"-setlevel" })); + assertFalse(validateCommand(new String[] {"-getlevel", "foo.bar:8080" })); + + // valid command arguments + assertTrue(validateCommand( + new String[] {"-getlevel", "foo.bar:8080", className })); + assertTrue(validateCommand( + new String[] {"-setlevel", "foo.bar:8080", className, "DEBUG" })); + assertTrue(validateCommand( + new String[] {"-getlevel", "foo.bar:8080", className })); + assertTrue(validateCommand( + new String[] {"-setlevel", "foo.bar:8080", className, "DEBUG" })); + + // fail due to the extra argument + assertFalse(validateCommand( + new String[] {"-getlevel", "foo.bar:8080", className, "blah" })); + assertFalse(validateCommand( + new String[] {"-setlevel", "foo.bar:8080", className, "DEBUG", "blah" })); + assertFalse(validateCommand( + new String[] { + "-getlevel", "foo.bar:8080", className, "-setlevel", "foo.bar:8080", className })); + } + + /** + * Check to see if a command can be accepted. + * + * @param args a String array of arguments + * @return true if the command can be accepted, false if not. + */ + private boolean validateCommand(String[] args) { + CLI cli = new CLI(clientConf); + try { + cli.parseArguments(args); + } catch (HadoopIllegalArgumentException e) { + return false; + } catch (Exception e) { + // this is used to verify the command arguments only. + // no HadoopIllegalArgumentException = the arguments are good. + return true; + } + return true; + } + + /** + * Creates and starts a Jetty server binding at an ephemeral port to run + * LogLevel servlet. + * @param isSpnego true if SPNEGO is enabled + * @return a created HttpServer object + * @throws Exception if unable to create or start a Jetty server + */ + private HttpServer createServer(boolean isSpnego) + throws Exception { + HttpServer.Builder builder = new HttpServer.Builder() + .setName("..") + .addEndpoint(new URI(PROTOCOL_HTTP + "://localhost:0")) + .setFindPort(true) + .setConf(serverConf); + if (isSpnego) { + // Set up server Kerberos credentials. + // Since the server may fall back to simple authentication, + // use ACL to make sure the connection is Kerberos/SPNEGO authenticated. + builder.setSecurityEnabled(true) + .setUsernameConfKey(PRINCIPAL) + .setKeytabConfKey(KEYTAB) + .setACL(new AccessControlList("client")); } - else { - out.println(testlog.getClass() + " not tested."); + + HttpServer server = builder.build(); + server.start(); + return server; + } + + private void testDynamicLogLevel(final boolean isSpnego) + throws Exception { + testDynamicLogLevel(isSpnego, Level.DEBUG.toString()); + } + + /** + * Run both client and server using the given protocol. + * + * @param isSpnego true if SPNEGO is enabled + * @throws Exception if client can't accesss server. + */ + private void testDynamicLogLevel(final boolean isSpnego, final String newLevel) + throws Exception { + Level oldLevel = log.getEffectiveLevel(); + assertNotEquals("Get default Log Level which shouldn't be ERROR.", + Level.ERROR, oldLevel); + + // configs needed for SPNEGO at server side + if (isSpnego) { + serverConf.set(PRINCIPAL, HTTP_PRINCIPAL); + serverConf.set(KEYTAB, KEYTAB_FILE.getAbsolutePath()); + serverConf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos"); + serverConf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true); + UserGroupInformation.setConfiguration(serverConf); + } else { + serverConf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "simple"); + serverConf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, false); + UserGroupInformation.setConfiguration(serverConf); } + + final HttpServer server = createServer(isSpnego); + // get server port + final String authority = NetUtils.getHostPortString(server.getConnectorAddress(0)); + + String keytabFilePath = KEYTAB_FILE.getAbsolutePath(); + + UserGroupInformation clientUGI = UserGroupInformation. + loginUserFromKeytabAndReturnUGI(clientPrincipal, keytabFilePath); + try { + clientUGI.doAs(new PrivilegedExceptionAction<Void>() { + @Override public Void run() throws Exception { + // client command line + TestLogLevel.this.getLevel(authority); + TestLogLevel.this.setLevel(authority, newLevel); + return null; + } + }); + } finally { + clientUGI.logoutUserFromKeytab(); + server.stop(); + } + + // restore log level + GenericTestUtils.setLogLevel(log, oldLevel); + } + + /** + * Run LogLevel command line to start a client to get log level of this test + * class. + * + * @param authority daemon's web UI address + * @throws Exception if unable to connect + */ + private void getLevel(String authority) throws Exception { + String[] getLevelArgs = {"-getlevel", authority, logName}; + CLI cli = new CLI(clientConf); + cli.run(getLevelArgs); + } + + /** + * Run LogLevel command line to start a client to set log level of this test + * class to debug. + * + * @param authority daemon's web UI address + * @throws Exception if unable to run or log level does not change as expected + */ + private void setLevel(String authority, String newLevel) + throws Exception { + String[] setLevelArgs = {"-setlevel", authority, logName, newLevel}; + CLI cli = new CLI(clientConf); + cli.run(setLevelArgs); + + assertEquals("new level not equal to expected: ", newLevel.toUpperCase(), + log.getEffectiveLevel().toString()); + } + + /** + * Test setting log level to "Info". + * + * @throws Exception if client can't set log level to INFO. + */ + @Test(timeout=60000) + public void testInfoLogLevel() throws Exception { + testDynamicLogLevel(true, "INFO"); + } + + /** + * Test setting log level to "Error". + * + * @throws Exception if client can't set log level to ERROR. + */ + @Test(timeout=60000) + public void testErrorLogLevel() throws Exception { + testDynamicLogLevel(true, "ERROR"); + } + + /** + * Server runs HTTP, no SPNEGO. + * + * @throws Exception if http client can't access http server. + */ + @Test(timeout=60000) + public void testLogLevelByHttp() throws Exception { + testDynamicLogLevel(false); + } + + /** + * Server runs HTTP + SPNEGO. + * + * @throws Exception if http client can't access http server. + */ + @Test(timeout=60000) + public void testLogLevelByHttpWithSpnego() throws Exception { + testDynamicLogLevel(true); } }