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