HBASE-26160: Configurable disallowlist for live editing of loglevels (#3549)
Signed-off-by: Wei-Chiu Chuang <weichiu@apache.org>
This commit is contained in:
parent
83661c5636
commit
da950b9be2
|
@ -22,8 +22,8 @@ import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
@ -41,6 +41,7 @@ import org.apache.hadoop.hbase.logging.Log4jUtils;
|
||||||
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
|
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
|
||||||
import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
|
import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
|
||||||
import org.apache.hadoop.security.ssl.SSLFactory;
|
import org.apache.hadoop.security.ssl.SSLFactory;
|
||||||
|
import org.apache.hadoop.util.HttpExceptionUtils;
|
||||||
import org.apache.hadoop.util.ServletUtil;
|
import org.apache.hadoop.util.ServletUtil;
|
||||||
import org.apache.hadoop.util.Tool;
|
import org.apache.hadoop.util.Tool;
|
||||||
import org.apache.yetus.audience.InterfaceAudience;
|
import org.apache.yetus.audience.InterfaceAudience;
|
||||||
|
@ -59,6 +60,8 @@ public final class LogLevel {
|
||||||
public static final String PROTOCOL_HTTP = "http";
|
public static final String PROTOCOL_HTTP = "http";
|
||||||
public static final String PROTOCOL_HTTPS = "https";
|
public static final String PROTOCOL_HTTPS = "https";
|
||||||
|
|
||||||
|
public static final String READONLY_LOGGERS_CONF_KEY = "hbase.ui.logLevels.readonly.loggers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command line implementation
|
* A command line implementation
|
||||||
*/
|
*/
|
||||||
|
@ -247,11 +250,11 @@ public final class LogLevel {
|
||||||
* @return a connected connection
|
* @return a connected connection
|
||||||
* @throws Exception if it can not establish a connection.
|
* @throws Exception if it can not establish a connection.
|
||||||
*/
|
*/
|
||||||
private URLConnection connect(URL url) throws Exception {
|
private HttpURLConnection connect(URL url) throws Exception {
|
||||||
AuthenticatedURL.Token token = new AuthenticatedURL.Token();
|
AuthenticatedURL.Token token = new AuthenticatedURL.Token();
|
||||||
AuthenticatedURL aUrl;
|
AuthenticatedURL aUrl;
|
||||||
SSLFactory clientSslFactory;
|
SSLFactory clientSslFactory;
|
||||||
URLConnection connection;
|
HttpURLConnection connection;
|
||||||
// If https is chosen, configures SSL client.
|
// If https is chosen, configures SSL client.
|
||||||
if (PROTOCOL_HTTPS.equals(url.getProtocol())) {
|
if (PROTOCOL_HTTPS.equals(url.getProtocol())) {
|
||||||
clientSslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, this.getConf());
|
clientSslFactory = new SSLFactory(SSLFactory.Mode.CLIENT, this.getConf());
|
||||||
|
@ -280,7 +283,9 @@ public final class LogLevel {
|
||||||
URL url = new URL(urlString);
|
URL url = new URL(urlString);
|
||||||
System.out.println("Connecting to " + url);
|
System.out.println("Connecting to " + url);
|
||||||
|
|
||||||
URLConnection connection = connect(url);
|
HttpURLConnection connection = connect(url);
|
||||||
|
|
||||||
|
HttpExceptionUtils.validateResponse(connection, 200);
|
||||||
|
|
||||||
// read from the servlet
|
// read from the servlet
|
||||||
|
|
||||||
|
@ -317,8 +322,10 @@ public final class LogLevel {
|
||||||
Configuration conf = (Configuration) getServletContext().getAttribute(
|
Configuration conf = (Configuration) getServletContext().getAttribute(
|
||||||
HttpServer.CONF_CONTEXT_ATTRIBUTE);
|
HttpServer.CONF_CONTEXT_ATTRIBUTE);
|
||||||
if (conf.getBoolean("hbase.master.ui.readonly", false)) {
|
if (conf.getBoolean("hbase.master.ui.readonly", false)) {
|
||||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Modification of HBase via"
|
sendError(
|
||||||
+ " the UI is disallowed in configuration.");
|
response,
|
||||||
|
HttpServletResponse.SC_FORBIDDEN,
|
||||||
|
"Modification of HBase via the UI is disallowed in configuration.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
response.setContentType("text/html");
|
response.setContentType("text/html");
|
||||||
|
@ -336,6 +343,8 @@ public final class LogLevel {
|
||||||
String logName = ServletUtil.getParameter(request, "log");
|
String logName = ServletUtil.getParameter(request, "log");
|
||||||
String level = ServletUtil.getParameter(request, "level");
|
String level = ServletUtil.getParameter(request, "level");
|
||||||
|
|
||||||
|
String[] readOnlyLogLevels = conf.getStrings(READONLY_LOGGERS_CONF_KEY);
|
||||||
|
|
||||||
if (logName != null) {
|
if (logName != null) {
|
||||||
out.println("<p>Results:</p>");
|
out.println("<p>Results:</p>");
|
||||||
out.println(MARKER
|
out.println(MARKER
|
||||||
|
@ -345,6 +354,14 @@ public final class LogLevel {
|
||||||
out.println(MARKER
|
out.println(MARKER
|
||||||
+ "Log Class: <b>" + log.getClass().getName() +"</b><br />");
|
+ "Log Class: <b>" + log.getClass().getName() +"</b><br />");
|
||||||
if (level != null) {
|
if (level != null) {
|
||||||
|
if (!isLogLevelChangeAllowed(logName, readOnlyLogLevels)) {
|
||||||
|
sendError(
|
||||||
|
response,
|
||||||
|
HttpServletResponse.SC_PRECONDITION_FAILED,
|
||||||
|
"Modification of logger " + logName + " is disallowed in configuration.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
out.println(MARKER + "Submitted Level: <b>" + level + "</b><br />");
|
out.println(MARKER + "Submitted Level: <b>" + level + "</b><br />");
|
||||||
}
|
}
|
||||||
process(log, level, out);
|
process(log, level, out);
|
||||||
|
@ -360,6 +377,24 @@ public final class LogLevel {
|
||||||
out.close();
|
out.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isLogLevelChangeAllowed(String logger, String[] readOnlyLogLevels) {
|
||||||
|
if (readOnlyLogLevels == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (String readOnlyLogLevel : readOnlyLogLevels) {
|
||||||
|
if (logger.startsWith(readOnlyLogLevel)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendError(HttpServletResponse response, int code, String message)
|
||||||
|
throws IOException {
|
||||||
|
response.setStatus(code, message);
|
||||||
|
response.sendError(code, message);
|
||||||
|
}
|
||||||
|
|
||||||
static final String FORMS = "<div class='container-fluid content'>\n"
|
static final String FORMS = "<div class='container-fluid content'>\n"
|
||||||
+ "<div class='row inner_header'>\n" + "<div class='page-header'>\n"
|
+ "<div class='row inner_header'>\n" + "<div class='page-header'>\n"
|
||||||
+ "<h1>Get/Set Log Level</h1>\n" + "</div>\n" + "</div>\n" + "Actions:" + "<p>"
|
+ "<h1>Get/Set Log Level</h1>\n" + "</div>\n" + "</div>\n" + "Actions:" + "<p>"
|
||||||
|
|
|
@ -24,12 +24,14 @@ import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.BindException;
|
import java.net.BindException;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.security.PrivilegedExceptionAction;
|
import java.security.PrivilegedExceptionAction;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import javax.net.ssl.SSLException;
|
import javax.net.ssl.SSLException;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.hadoop.HadoopIllegalArgumentException;
|
import org.apache.hadoop.HadoopIllegalArgumentException;
|
||||||
import org.apache.hadoop.conf.Configuration;
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
@ -73,6 +75,8 @@ public class TestLogLevel {
|
||||||
private static Configuration clientConf;
|
private static Configuration clientConf;
|
||||||
private static Configuration sslConf;
|
private static Configuration sslConf;
|
||||||
private static final String logName = TestLogLevel.class.getName();
|
private static final String logName = TestLogLevel.class.getName();
|
||||||
|
private static final String protectedPrefix = "protected";
|
||||||
|
private static final String protectedLogName = protectedPrefix + "." + logName;
|
||||||
private static final org.apache.logging.log4j.Logger log =
|
private static final org.apache.logging.log4j.Logger log =
|
||||||
org.apache.logging.log4j.LogManager.getLogger(logName);
|
org.apache.logging.log4j.LogManager.getLogger(logName);
|
||||||
private final static String PRINCIPAL = "loglevel.principal";
|
private final static String PRINCIPAL = "loglevel.principal";
|
||||||
|
@ -89,6 +93,7 @@ public class TestLogLevel {
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void setUp() throws Exception {
|
public static void setUp() throws Exception {
|
||||||
serverConf = new Configuration();
|
serverConf = new Configuration();
|
||||||
|
serverConf.setStrings(LogLevel.READONLY_LOGGERS_CONF_KEY, protectedPrefix);
|
||||||
HTU = new HBaseCommonTestingUtil(serverConf);
|
HTU = new HBaseCommonTestingUtil(serverConf);
|
||||||
|
|
||||||
File keystoreDir = new File(HTU.getDataTestDir("keystore").toString());
|
File keystoreDir = new File(HTU.getDataTestDir("keystore").toString());
|
||||||
|
@ -259,9 +264,17 @@ public class TestLogLevel {
|
||||||
private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol,
|
private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol,
|
||||||
final boolean isSpnego) throws Exception {
|
final boolean isSpnego) throws Exception {
|
||||||
testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego,
|
testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego,
|
||||||
|
logName,
|
||||||
org.apache.logging.log4j.Level.DEBUG.toString());
|
org.apache.logging.log4j.Level.DEBUG.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol,
|
||||||
|
final boolean isSpnego, final String newLevel) throws Exception {
|
||||||
|
testDynamicLogLevel(bindProtocol, connectProtocol, isSpnego,
|
||||||
|
logName,
|
||||||
|
newLevel);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run both client and server using the given protocol.
|
* Run both client and server using the given protocol.
|
||||||
* @param bindProtocol specify either http or https for server
|
* @param bindProtocol specify either http or https for server
|
||||||
|
@ -270,13 +283,14 @@ public class TestLogLevel {
|
||||||
* @throws Exception if client can't accesss server.
|
* @throws Exception if client can't accesss server.
|
||||||
*/
|
*/
|
||||||
private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol,
|
private void testDynamicLogLevel(final String bindProtocol, final String connectProtocol,
|
||||||
final boolean isSpnego, final String newLevel) throws Exception {
|
final boolean isSpnego, final String loggerName, final String newLevel) throws Exception {
|
||||||
if (!LogLevel.isValidProtocol(bindProtocol)) {
|
if (!LogLevel.isValidProtocol(bindProtocol)) {
|
||||||
throw new Exception("Invalid server protocol " + bindProtocol);
|
throw new Exception("Invalid server protocol " + bindProtocol);
|
||||||
}
|
}
|
||||||
if (!LogLevel.isValidProtocol(connectProtocol)) {
|
if (!LogLevel.isValidProtocol(connectProtocol)) {
|
||||||
throw new Exception("Invalid client protocol " + connectProtocol);
|
throw new Exception("Invalid client protocol " + connectProtocol);
|
||||||
}
|
}
|
||||||
|
org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(loggerName);
|
||||||
org.apache.logging.log4j.Level oldLevel = log.getLevel();
|
org.apache.logging.log4j.Level oldLevel = log.getLevel();
|
||||||
assertNotEquals("Get default Log Level which shouldn't be ERROR.",
|
assertNotEquals("Get default Log Level which shouldn't be ERROR.",
|
||||||
org.apache.logging.log4j.Level.ERROR, oldLevel);
|
org.apache.logging.log4j.Level.ERROR, oldLevel);
|
||||||
|
@ -305,8 +319,8 @@ public class TestLogLevel {
|
||||||
try {
|
try {
|
||||||
clientUGI.doAs((PrivilegedExceptionAction<Void>) () -> {
|
clientUGI.doAs((PrivilegedExceptionAction<Void>) () -> {
|
||||||
// client command line
|
// client command line
|
||||||
getLevel(connectProtocol, authority);
|
getLevel(connectProtocol, authority, loggerName);
|
||||||
setLevel(connectProtocol, authority, newLevel);
|
setLevel(connectProtocol, authority, loggerName, newLevel);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -324,7 +338,7 @@ public class TestLogLevel {
|
||||||
* @param authority daemon's web UI address
|
* @param authority daemon's web UI address
|
||||||
* @throws Exception if unable to connect
|
* @throws Exception if unable to connect
|
||||||
*/
|
*/
|
||||||
private void getLevel(String protocol, String authority) throws Exception {
|
private void getLevel(String protocol, String authority, String logName) throws Exception {
|
||||||
String[] getLevelArgs = { "-getlevel", authority, logName, "-protocol", protocol };
|
String[] getLevelArgs = { "-getlevel", authority, logName, "-protocol", protocol };
|
||||||
CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf);
|
CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf);
|
||||||
cli.run(getLevelArgs);
|
cli.run(getLevelArgs);
|
||||||
|
@ -336,13 +350,27 @@ public class TestLogLevel {
|
||||||
* @param authority daemon's web UI address
|
* @param authority daemon's web UI address
|
||||||
* @throws Exception if unable to run or log level does not change as expected
|
* @throws Exception if unable to run or log level does not change as expected
|
||||||
*/
|
*/
|
||||||
private void setLevel(String protocol, String authority, String newLevel) throws Exception {
|
private void setLevel(String protocol, String authority, String logName, String newLevel) throws Exception {
|
||||||
String[] setLevelArgs = { "-setlevel", authority, logName, newLevel, "-protocol", protocol };
|
String[] setLevelArgs = { "-setlevel", authority, logName, newLevel, "-protocol", protocol };
|
||||||
CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf);
|
CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf);
|
||||||
cli.run(setLevelArgs);
|
cli.run(setLevelArgs);
|
||||||
|
|
||||||
|
org.apache.logging.log4j.Logger logger = org.apache.logging.log4j.LogManager.getLogger(logName);
|
||||||
|
|
||||||
assertEquals("new level not equal to expected: ", newLevel.toUpperCase(),
|
assertEquals("new level not equal to expected: ", newLevel.toUpperCase(),
|
||||||
log.getLevel().toString());
|
logger.getLevel().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSettingProtectedLogLevel() throws Exception {
|
||||||
|
try {
|
||||||
|
testDynamicLogLevel(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP, true, protectedLogName,
|
||||||
|
"DEBUG");
|
||||||
|
fail("Expected IO exception due to protected logger");
|
||||||
|
} catch (IOException e) {
|
||||||
|
assertTrue(e.getMessage().contains("" + HttpServletResponse.SC_PRECONDITION_FAILED));
|
||||||
|
assertTrue(e.getMessage().contains("Modification of logger " + protectedLogName + " is disallowed in configuration."));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue