001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.http.log; 019 020import static org.junit.Assert.assertEquals; 021import static org.junit.Assert.assertFalse; 022import static org.junit.Assert.assertNotEquals; 023import static org.junit.Assert.assertSame; 024import static org.junit.Assert.assertTrue; 025import static org.junit.Assert.fail; 026 027import java.io.File; 028import java.io.IOException; 029import java.net.BindException; 030import java.net.SocketException; 031import java.net.URI; 032import java.security.PrivilegedExceptionAction; 033import java.util.Properties; 034import java.util.regex.Matcher; 035import java.util.regex.Pattern; 036import javax.net.ssl.SSLException; 037import javax.servlet.http.HttpServletResponse; 038import org.apache.commons.io.FileUtils; 039import org.apache.hadoop.HadoopIllegalArgumentException; 040import org.apache.hadoop.conf.Configuration; 041import org.apache.hadoop.fs.CommonConfigurationKeys; 042import org.apache.hadoop.fs.CommonConfigurationKeysPublic; 043import org.apache.hadoop.fs.FileUtil; 044import org.apache.hadoop.hbase.HBaseClassTestRule; 045import org.apache.hadoop.hbase.HBaseCommonTestingUtil; 046import org.apache.hadoop.hbase.http.HttpConfig; 047import org.apache.hadoop.hbase.http.HttpServer; 048import org.apache.hadoop.hbase.http.log.LogLevel.CLI; 049import org.apache.hadoop.hbase.http.ssl.KeyStoreTestUtil; 050import org.apache.hadoop.hbase.logging.Log4jUtils; 051import org.apache.hadoop.hbase.testclassification.MiscTests; 052import org.apache.hadoop.hbase.testclassification.SmallTests; 053import org.apache.hadoop.hdfs.DFSConfigKeys; 054import org.apache.hadoop.minikdc.MiniKdc; 055import org.apache.hadoop.net.NetUtils; 056import org.apache.hadoop.security.UserGroupInformation; 057import org.apache.hadoop.security.authorize.AccessControlList; 058import org.apache.hadoop.security.ssl.SSLFactory; 059import org.apache.hadoop.test.GenericTestUtils; 060import org.apache.hadoop.util.StringUtils; 061import org.junit.AfterClass; 062import org.junit.BeforeClass; 063import org.junit.ClassRule; 064import org.junit.Test; 065import org.junit.experimental.categories.Category; 066 067/** 068 * Test LogLevel. 069 */ 070@Category({ MiscTests.class, SmallTests.class }) 071public class TestLogLevel { 072 @ClassRule 073 public static final HBaseClassTestRule CLASS_RULE = 074 HBaseClassTestRule.forClass(TestLogLevel.class); 075 076 private static String keystoresDir; 077 private static String sslConfDir; 078 private static Configuration serverConf; 079 private static Configuration clientConf; 080 private static Configuration sslConf; 081 private static final String logName = TestLogLevel.class.getName(); 082 private static final String protectedPrefix = "protected"; 083 private static final String protectedLogName = protectedPrefix + "." + logName; 084 private static final org.apache.logging.log4j.Logger log = 085 org.apache.logging.log4j.LogManager.getLogger(logName); 086 private final static String PRINCIPAL = "loglevel.principal"; 087 private final static String KEYTAB = "loglevel.keytab"; 088 089 private static MiniKdc kdc; 090 091 private static final String LOCALHOST = "localhost"; 092 private static final String clientPrincipal = "client/" + LOCALHOST; 093 private static String HTTP_PRINCIPAL = "HTTP/" + LOCALHOST; 094 private static HBaseCommonTestingUtil HTU; 095 private static File keyTabFile; 096 private static final Pattern EFFECTIVE_LEVEL = Pattern.compile("Effective level:\\s*(\\S+)"); 097 098 @FunctionalInterface 099 private interface ThrowingRunnable { 100 void run() throws Exception; 101 } 102 103 @FunctionalInterface 104 private interface ThrowingConsumer { 105 void accept(String url) throws Exception; 106 } 107 108 private enum Protocol { 109 C_HTTP_S_HTTP(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTP), 110 C_HTTP_S_HTTPS(LogLevel.PROTOCOL_HTTP, LogLevel.PROTOCOL_HTTPS), 111 C_HTTPS_S_HTTP(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTP), 112 C_HTTPS_S_HTTPS(LogLevel.PROTOCOL_HTTPS, LogLevel.PROTOCOL_HTTPS); 113 114 final String client; 115 final String server; 116 117 Protocol(String client, String server) { 118 this.client = client; 119 this.server = server; 120 } 121 } 122 123 @BeforeClass 124 public static void setUp() throws Exception { 125 serverConf = new Configuration(); 126 serverConf.setStrings(LogLevel.READONLY_LOGGERS_CONF_KEY, protectedPrefix); 127 HTU = new HBaseCommonTestingUtil(serverConf); 128 129 File keystoreDir = new File(HTU.getDataTestDir("keystore").toString()); 130 keystoreDir.mkdirs(); 131 keyTabFile = new File(HTU.getDataTestDir("keytab").toString(), "keytabfile"); 132 keyTabFile.getParentFile().mkdirs(); 133 clientConf = new Configuration(); 134 135 setupSSL(keystoreDir); 136 137 kdc = setupMiniKdc(); 138 // Create two principles: a client and an HTTP principal 139 kdc.createPrincipal(keyTabFile, clientPrincipal, HTTP_PRINCIPAL); 140 } 141 142 /** 143 * Sets up {@link MiniKdc} for testing security. Copied from HBaseTestingUtility#setupMiniKdc(). 144 */ 145 static private MiniKdc setupMiniKdc() throws Exception { 146 Properties conf = MiniKdc.createConf(); 147 conf.put(MiniKdc.DEBUG, true); 148 MiniKdc kdc = null; 149 File dir = null; 150 // There is time lag between selecting a port and trying to bind with it. It's possible that 151 // another service captures the port in between which'll result in BindException. 152 boolean bindException; 153 int numTries = 0; 154 do { 155 try { 156 bindException = false; 157 dir = new File(HTU.getDataTestDir("kdc").toUri().getPath()); 158 kdc = new MiniKdc(conf, dir); 159 kdc.start(); 160 } catch (BindException e) { 161 FileUtils.deleteDirectory(dir); // clean directory 162 numTries++; 163 if (numTries == 3) { 164 log.error("Failed setting up MiniKDC. Tried " + numTries + " times."); 165 throw e; 166 } 167 log.error("BindException encountered when setting up MiniKdc. Trying again."); 168 bindException = true; 169 } 170 } while (bindException); 171 return kdc; 172 } 173 174 static private void setupSSL(File base) throws Exception { 175 clientConf.set(DFSConfigKeys.DFS_HTTP_POLICY_KEY, HttpConfig.Policy.HTTPS_ONLY.name()); 176 clientConf.set(DFSConfigKeys.DFS_NAMENODE_HTTPS_ADDRESS_KEY, "localhost:0"); 177 clientConf.set(DFSConfigKeys.DFS_DATANODE_HTTPS_ADDRESS_KEY, "localhost:0"); 178 179 keystoresDir = base.getAbsolutePath(); 180 sslConfDir = KeyStoreTestUtil.getClasspathDir(TestLogLevel.class); 181 KeyStoreTestUtil.setupSSLConfig(keystoresDir, sslConfDir, serverConf, false); 182 183 sslConf = getSslConfig(serverConf); 184 } 185 186 /** 187 * Get the SSL configuration. This method is copied from KeyStoreTestUtil#getSslConfig() in 188 * Hadoop. 189 * @return {@link Configuration} instance with ssl configs loaded. 190 * @param conf to pull client/server SSL settings filename from 191 */ 192 private static Configuration getSslConfig(Configuration conf) { 193 Configuration sslConf = new Configuration(false); 194 String sslServerConfFile = conf.get(SSLFactory.SSL_SERVER_CONF_KEY); 195 String sslClientConfFile = conf.get(SSLFactory.SSL_CLIENT_CONF_KEY); 196 sslConf.addResource(sslServerConfFile); 197 sslConf.addResource(sslClientConfFile); 198 sslConf.set(SSLFactory.SSL_SERVER_CONF_KEY, sslServerConfFile); 199 sslConf.set(SSLFactory.SSL_CLIENT_CONF_KEY, sslClientConfFile); 200 return sslConf; 201 } 202 203 @AfterClass 204 public static void tearDown() { 205 if (kdc != null) { 206 kdc.stop(); 207 } 208 209 FileUtil.fullyDelete(new File(HTU.getDataTestDir().toString())); 210 } 211 212 /** 213 * Test client command line options. Does not validate server behavior. 214 * @throws Exception if commands return unexpected results. 215 */ 216 @Test 217 public void testCommandOptions() throws Exception { 218 final String className = this.getClass().getName(); 219 220 assertFalse(validateCommand(new String[] { "-foo" })); 221 // fail due to insufficient number of arguments 222 assertFalse(validateCommand(new String[] {})); 223 assertFalse(validateCommand(new String[] { "-getlevel" })); 224 assertFalse(validateCommand(new String[] { "-setlevel" })); 225 assertFalse(validateCommand(new String[] { "-getlevel", "foo.bar:8080" })); 226 227 // valid command arguments 228 assertTrue(validateCommand(new String[] { "-getlevel", "foo.bar:8080", className })); 229 assertTrue(validateCommand(new String[] { "-setlevel", "foo.bar:8080", className, "DEBUG" })); 230 assertTrue(validateCommand(new String[] { "-getlevel", "foo.bar:8080", className })); 231 assertTrue(validateCommand(new String[] { "-setlevel", "foo.bar:8080", className, "DEBUG" })); 232 233 // fail due to the extra argument 234 assertFalse(validateCommand(new String[] { "-getlevel", "foo.bar:8080", className, "blah" })); 235 assertFalse( 236 validateCommand(new String[] { "-setlevel", "foo.bar:8080", className, "DEBUG", "blah" })); 237 assertFalse(validateCommand(new String[] { "-getlevel", "foo.bar:8080", className, "-setlevel", 238 "foo.bar:8080", className })); 239 } 240 241 /** 242 * Check to see if a command can be accepted. 243 * @param args a String array of arguments 244 * @return true if the command can be accepted, false if not. 245 */ 246 private boolean validateCommand(String[] args) { 247 CLI cli = new CLI(clientConf); 248 try { 249 cli.parseArguments(args); 250 } catch (HadoopIllegalArgumentException e) { 251 return false; 252 } catch (Exception e) { 253 // this is used to verify the command arguments only. 254 // no HadoopIllegalArgumentException = the arguments are good. 255 return true; 256 } 257 return true; 258 } 259 260 /** 261 * Creates and starts a Jetty server binding at an ephemeral port to run LogLevel servlet. 262 * @param protocol "http" or "https" 263 * @param isSpnego true if SPNEGO is enabled 264 * @return a created HttpServer object 265 * @throws Exception if unable to create or start a Jetty server 266 */ 267 private HttpServer createServer(String protocol, boolean isSpnego) throws Exception { 268 // Changed to "" as ".." moves it a steps back in path because the path is relative to the 269 // current working directory. throws "java.lang.IllegalArgumentException: Base Resource is not 270 // valid: hbase-http/target/test-classes/static" as it is not able to find the static folder. 271 HttpServer.Builder builder = new HttpServer.Builder().setName("") 272 .addEndpoint(new URI(protocol + "://localhost:0")).setFindPort(true).setConf(serverConf); 273 if (isSpnego) { 274 // Set up server Kerberos credentials. 275 // Since the server may fall back to simple authentication, 276 // use ACL to make sure the connection is Kerberos/SPNEGO authenticated. 277 builder.setSecurityEnabled(true).setUsernameConfKey(PRINCIPAL).setKeytabConfKey(KEYTAB) 278 .setACL(new AccessControlList("client")); 279 } 280 281 // if using HTTPS, configure keystore/truststore properties. 282 if (protocol.equals(LogLevel.PROTOCOL_HTTPS)) { 283 builder = builder.keyPassword(sslConf.get("ssl.server.keystore.keypassword")) 284 .keyStore(sslConf.get("ssl.server.keystore.location"), 285 sslConf.get("ssl.server.keystore.password"), 286 sslConf.get("ssl.server.keystore.type", "jks")) 287 .trustStore(sslConf.get("ssl.server.truststore.location"), 288 sslConf.get("ssl.server.truststore.password"), 289 sslConf.get("ssl.server.truststore.type", "jks")); 290 } 291 292 HttpServer server = builder.build(); 293 server.start(); 294 return server; 295 } 296 297 private void testGetLogLevel(Protocol protocol, boolean isSpnego, String loggerName, 298 String expectedLevel) throws Exception { 299 withLogLevelServer(protocol, isSpnego, (authority) -> { 300 final String level = getLevel(protocol.client, authority, loggerName); 301 assertEquals("Log level not equal to expected: ", expectedLevel, level); 302 }); 303 } 304 305 private void testSetLogLevel(Protocol protocol, boolean isSpnego, String loggerName, 306 String newLevel) throws Exception { 307 String oldLevel = Log4jUtils.getEffectiveLevel(loggerName); 308 assertNotEquals("New level is same as old level: ", newLevel, oldLevel); 309 310 try { 311 withLogLevelServer(protocol, isSpnego, (authority) -> { 312 setLevel(protocol.client, authority, loggerName, newLevel); 313 }); 314 } finally { 315 // restore log level 316 Log4jUtils.setLogLevel(loggerName, oldLevel); 317 } 318 } 319 320 /** 321 * Starts a LogLevel server and executes a client action against it. 322 * @param protocol protocol configuration for client and server 323 * @param isSpnego whether SPNEGO authentication is enabled 324 * @param consumer client action executed with the server authority (host:port) 325 * @throws Exception if server setup or client execution fails 326 */ 327 private void withLogLevelServer(Protocol protocol, final boolean isSpnego, 328 ThrowingConsumer consumer) throws Exception { 329 if (!LogLevel.isValidProtocol(protocol.server)) { 330 throw new Exception("Invalid server protocol " + protocol.server); 331 } 332 if (!LogLevel.isValidProtocol(protocol.client)) { 333 throw new Exception("Invalid client protocol " + protocol.client); 334 } 335 336 // configs needed for SPNEGO at server side 337 if (isSpnego) { 338 serverConf.set(PRINCIPAL, HTTP_PRINCIPAL); 339 serverConf.set(KEYTAB, keyTabFile.getAbsolutePath()); 340 serverConf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "kerberos"); 341 serverConf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, true); 342 UserGroupInformation.setConfiguration(serverConf); 343 } else { 344 serverConf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION, "simple"); 345 serverConf.setBoolean(CommonConfigurationKeys.HADOOP_SECURITY_AUTHORIZATION, false); 346 UserGroupInformation.setConfiguration(serverConf); 347 } 348 349 final HttpServer server = createServer(protocol.server, isSpnego); 350 final String authority = NetUtils.getHostPortString(server.getConnectorAddress(0)); 351 String keytabFilePath = keyTabFile.getAbsolutePath(); 352 353 UserGroupInformation clientUGI = 354 UserGroupInformation.loginUserFromKeytabAndReturnUGI(clientPrincipal, keytabFilePath); 355 try { 356 clientUGI.doAs((PrivilegedExceptionAction<Void>) () -> { 357 consumer.accept(authority); 358 return null; 359 }); 360 } finally { 361 clientUGI.logoutUserFromKeytab(); 362 server.stop(); 363 } 364 } 365 366 /** 367 * Run LogLevel command line to start a client to get log level of this test class. 368 * @param protocol specify either http or https 369 * @param authority daemon's web UI address 370 * @throws Exception if unable to connect 371 */ 372 private String getLevel(String protocol, String authority, String logName) throws Exception { 373 String[] getLevelArgs = { "-getlevel", authority, logName, "-protocol", protocol }; 374 CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf); 375 cli.parseArguments(getLevelArgs); 376 final String response = cli.fetchGetLevelResponse(); 377 return extractEffectiveLevel(response); 378 } 379 380 /** 381 * Run LogLevel command line to start a client to set log level of this test class to debug. 382 * @param protocol specify either http or https 383 * @param authority daemon's web UI address 384 * @throws Exception if unable to run or log level does not change as expected 385 */ 386 private void setLevel(String protocol, String authority, String logName, String newLevel) 387 throws Exception { 388 String[] setLevelArgs = { "-setlevel", authority, logName, newLevel, "-protocol", protocol }; 389 CLI cli = new CLI(protocol.equalsIgnoreCase("https") ? sslConf : clientConf); 390 cli.parseArguments(setLevelArgs); 391 final String response = cli.fetchSetLevelResponse(); 392 final String responseLevel = extractEffectiveLevel(response); 393 final String currentLevel = Log4jUtils.getEffectiveLevel(logName); 394 assertEquals("new level not equal to expected: ", newLevel, currentLevel); 395 assertSame("new level not equal to response level: ", newLevel, responseLevel); 396 } 397 398 /** 399 * Extract effective log level from server response. 400 * @param response server body response string 401 * @return the effective log level 402 */ 403 private String extractEffectiveLevel(String response) { 404 Matcher m = EFFECTIVE_LEVEL.matcher(response); 405 if (m.find()) { 406 return org.apache.logging.log4j.Level.toLevel(m.group(1)).name(); 407 } 408 409 fail("Cannot find effective log level from response: " + response); 410 return null; 411 } 412 413 @Test 414 public void testSettingProtectedLogLevel() throws Exception { 415 try { 416 testSetLogLevel(Protocol.C_HTTP_S_HTTP, true, protectedLogName, "DEBUG"); 417 fail("Expected IO exception due to protected logger"); 418 } catch (IOException e) { 419 assertTrue(e.getMessage().contains("" + HttpServletResponse.SC_PRECONDITION_FAILED)); 420 assertTrue(e.getMessage().contains( 421 "Modification of logger " + protectedLogName + " is disallowed in configuration.")); 422 } 423 } 424 425 @Test 426 public void testGetDebugLogLevel() throws Exception { 427 Log4jUtils.setLogLevel(logName, "DEBUG"); 428 testGetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "DEBUG"); 429 } 430 431 @Test 432 public void testGetInfoLogLevel() throws Exception { 433 Log4jUtils.setLogLevel(logName, "INFO"); 434 testGetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "INFO"); 435 } 436 437 /** 438 * Test setting log level to "Info". 439 * @throws Exception if client can't set log level to INFO. 440 */ 441 @Test 442 public void testSetInfoLogLevel() throws Exception { 443 Log4jUtils.setLogLevel(logName, "DEBUG"); 444 testSetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "INFO"); 445 } 446 447 /** 448 * Test setting log level to "Error". 449 * @throws Exception if client can't set log level to ERROR. 450 */ 451 @Test 452 public void testSetErrorLogLevel() throws Exception { 453 Log4jUtils.setLogLevel(logName, "DEBUG"); 454 testSetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "ERROR"); 455 } 456 457 /** 458 * Server runs HTTP, no SPNEGO. 459 * @throws Exception if http client can't access http server, or http client can access https 460 * server. 461 */ 462 @Test 463 public void testLogLevelByHttp() throws Exception { 464 Log4jUtils.setLogLevel(logName, "DEBUG"); 465 testGetLogLevel(Protocol.C_HTTP_S_HTTP, false, logName, "DEBUG"); 466 try { 467 testGetLogLevel(Protocol.C_HTTPS_S_HTTP, false, logName, "DEBUG"); 468 fail("An HTTPS Client should not have succeeded in connecting to a HTTP server"); 469 } catch (SSLException e) { 470 exceptionShouldContains("Unrecognized SSL message", e); 471 } 472 } 473 474 /** 475 * Server runs HTTP + SPNEGO. 476 * @throws Exception if http client can't access http server, or http client can access https 477 * server. 478 */ 479 @Test 480 public void testLogLevelByHttpWithSpnego() throws Exception { 481 Log4jUtils.setLogLevel(logName, "DEBUG"); 482 testGetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "DEBUG"); 483 try { 484 testGetLogLevel(Protocol.C_HTTPS_S_HTTP, true, logName, "DEBUG"); 485 fail("An HTTPS Client should not have succeeded in connecting to a HTTP server"); 486 } catch (SSLException e) { 487 exceptionShouldContains("Unrecognized SSL message", e); 488 } 489 } 490 491 /** 492 * Server runs HTTPS, no SPNEGO. 493 * @throws Exception if https client can't access https server, or https client can access http 494 * server. 495 */ 496 @Test 497 public void testLogLevelByHttps() throws Exception { 498 Log4jUtils.setLogLevel(logName, "DEBUG"); 499 testGetLogLevel(Protocol.C_HTTPS_S_HTTPS, false, logName, "DEBUG"); 500 try { 501 testGetLogLevel(Protocol.C_HTTP_S_HTTPS, false, logName, "DEBUG"); 502 fail("An HTTP Client should not have succeeded in connecting to a HTTPS server"); 503 } catch (SocketException e) { 504 exceptionShouldContains("Unexpected end of file from server", e); 505 } 506 } 507 508 /** 509 * Server runs HTTPS + SPNEGO. 510 * @throws Exception if https client can't access https server, or https client can access http 511 * server. 512 */ 513 @Test 514 public void testLogLevelByHttpsWithSpnego() throws Exception { 515 Log4jUtils.setLogLevel(logName, "DEBUG"); 516 testGetLogLevel(Protocol.C_HTTPS_S_HTTPS, true, logName, "DEBUG"); 517 try { 518 testGetLogLevel(Protocol.C_HTTP_S_HTTPS, true, logName, "DEBUG"); 519 fail("An HTTP Client should not have succeeded in connecting to a HTTPS server"); 520 } catch (SocketException e) { 521 exceptionShouldContains("Unexpected end of file from server", e); 522 } 523 } 524 525 /** 526 * Test getting log level in readonly mode. 527 * @throws Exception if a client can't get log level. 528 */ 529 @Test 530 public void testGetLogLevelAllowedInReadonlyMode() throws Exception { 531 withMasterUIReadonly(() -> { 532 Log4jUtils.setLogLevel(logName, "DEBUG"); 533 testGetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "DEBUG"); 534 }); 535 } 536 537 /** 538 * Test setting log level in readonly mode. 539 * @throws Exception if a client can set log level. 540 */ 541 @Test 542 public void testSetLogLevelDisallowedInReadonlyMode() throws Exception { 543 withMasterUIReadonly(() -> { 544 Log4jUtils.setLogLevel(logName, "DEBUG"); 545 try { 546 testSetLogLevel(Protocol.C_HTTP_S_HTTP, true, logName, "INFO"); 547 fail("Setting log level should be disallowed in readonly mode."); 548 } catch (IOException e) { 549 exceptionShouldContains("Modification of HBase via the UI is disallowed in configuration.", 550 e); 551 } 552 }); 553 } 554 555 private void withMasterUIReadonly(ThrowingRunnable runnable) throws Exception { 556 boolean prev = serverConf.getBoolean(LogLevel.MASTER_UI_READONLY_CONF_KEY, false); 557 serverConf.setBoolean(LogLevel.MASTER_UI_READONLY_CONF_KEY, true); 558 try { 559 runnable.run(); 560 } finally { 561 serverConf.setBoolean(LogLevel.MASTER_UI_READONLY_CONF_KEY, prev); 562 } 563 } 564 565 /** 566 * Assert that a throwable or one of its causes should contain the substr in its message. Ideally 567 * we should use {@link GenericTestUtils#assertExceptionContains(String, Throwable)} util method 568 * which asserts t.toString() contains the substr. As the original throwable may have been wrapped 569 * in Hadoop3 because of HADOOP-12897, it's required to check all the wrapped causes. After stop 570 * supporting Hadoop2, this method can be removed and assertion in tests can use t.getCause() 571 * directly, similar to HADOOP-15280. 572 */ 573 private static void exceptionShouldContains(String substr, Throwable throwable) { 574 Throwable t = throwable; 575 while (t != null) { 576 String msg = t.toString(); 577 if (msg != null && msg.toLowerCase().contains(substr.toLowerCase())) { 578 return; 579 } 580 t = t.getCause(); 581 } 582 throw new AssertionError("Expected to find '" + substr + "' but got unexpected exception:" 583 + StringUtils.stringifyException(throwable), throwable); 584 } 585}