YARN-4767. Network issues can cause persistent RM UI outage. (Daniel Templeton via kasha)
This commit is contained in:
parent
607705c488
commit
736d33cddd
@ -41,4 +41,5 @@ public interface YarnWebParams {
|
|||||||
String NODE_LABEL = "node.label";
|
String NODE_LABEL = "node.label";
|
||||||
String WEB_UI_TYPE = "web.ui.type";
|
String WEB_UI_TYPE = "web.ui.type";
|
||||||
String NEXT_REFRESH_INTERVAL = "next.refresh.interval";
|
String NEXT_REFRESH_INTERVAL = "next.refresh.interval";
|
||||||
|
String ERROR_MESSAGE = "error.message";
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hadoop.yarn.server.resourcemanager.webapp;
|
||||||
|
|
||||||
|
import org.apache.hadoop.yarn.webapp.view.HtmlBlock;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import static org.apache.hadoop.yarn.webapp.YarnWebParams.ERROR_MESSAGE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to display an error message to the user in the UI.
|
||||||
|
*/
|
||||||
|
public class ErrorBlock extends HtmlBlock {
|
||||||
|
@Inject
|
||||||
|
ErrorBlock(ViewContext ctx) {
|
||||||
|
super(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void render(Block html) {
|
||||||
|
html.p()._($(ERROR_MESSAGE))._();
|
||||||
|
}
|
||||||
|
}
|
@ -71,6 +71,7 @@ public class RMWebApp extends WebApp implements YarnWebParams {
|
|||||||
route("/errors-and-warnings", RmController.class, "errorsAndWarnings");
|
route("/errors-and-warnings", RmController.class, "errorsAndWarnings");
|
||||||
route(pajoin("/logaggregationstatus", APPLICATION_ID),
|
route(pajoin("/logaggregationstatus", APPLICATION_ID),
|
||||||
RmController.class, "logaggregationstatus");
|
RmController.class, "logaggregationstatus");
|
||||||
|
route(pajoin("/failure", APPLICATION_ID), RmController.class, "failure");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hadoop.yarn.server.resourcemanager.webapp;
|
||||||
|
|
||||||
|
import org.apache.hadoop.yarn.webapp.SubView;
|
||||||
|
import org.apache.hadoop.yarn.webapp.YarnWebParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to display a message that the proxy request failed
|
||||||
|
* because of a redirection issue.
|
||||||
|
*/
|
||||||
|
public class RedirectionErrorPage extends RmView {
|
||||||
|
@Override protected void preHead(Page.HTML<_> html) {
|
||||||
|
String aid = $(YarnWebParams.APPLICATION_ID);
|
||||||
|
|
||||||
|
commonPreHead(html);
|
||||||
|
set(YarnWebParams.ERROR_MESSAGE,
|
||||||
|
"The application master for " + aid + " redirected the "
|
||||||
|
+ "resource manager's web proxy's request back to the web proxy, "
|
||||||
|
+ "which means your request to view the application master's web UI "
|
||||||
|
+ "cannot be fulfilled. The typical cause for this error is a "
|
||||||
|
+ "network misconfiguration that causes the resource manager's web "
|
||||||
|
+ "proxy host to resolve to an unexpected IP address on the "
|
||||||
|
+ "application master host. Please contact your cluster "
|
||||||
|
+ "administrator to resolve the issue.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected Class<? extends SubView> content() {
|
||||||
|
return ErrorBlock.class;
|
||||||
|
}
|
||||||
|
}
|
@ -62,6 +62,10 @@ public class RmController extends Controller {
|
|||||||
render(ContainerPage.class);
|
render(ContainerPage.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void failure() {
|
||||||
|
render(RedirectionErrorPage.class);
|
||||||
|
}
|
||||||
|
|
||||||
public void nodes() {
|
public void nodes() {
|
||||||
render(NodesPage.class);
|
render(NodesPage.class);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.hadoop.yarn.server.resourcemanager.webapp;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.hadoop.yarn.api.ApplicationBaseProtocol;
|
||||||
|
import org.apache.hadoop.yarn.api.records.ApplicationId;
|
||||||
|
import org.apache.hadoop.yarn.server.resourcemanager.RMContext;
|
||||||
|
import org.apache.hadoop.yarn.server.resourcemanager.ResourceManager;
|
||||||
|
import org.apache.hadoop.yarn.webapp.YarnWebParams;
|
||||||
|
import org.apache.hadoop.yarn.webapp.test.WebAppTests;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import com.google.inject.Binder;
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import com.google.inject.Module;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class tests the RedirectionErrorPage.
|
||||||
|
*/
|
||||||
|
public class TestRedirectionErrorPage {
|
||||||
|
@Test
|
||||||
|
public void testAppBlockRenderWithNullCurrentAppAttempt() throws Exception {
|
||||||
|
ApplicationId appId = ApplicationId.newInstance(1234L, 0);
|
||||||
|
Injector injector;
|
||||||
|
|
||||||
|
// initialize RM Context, and create RMApp, without creating RMAppAttempt
|
||||||
|
final RMContext rmContext = TestRMWebApp.mockRMContext(15, 1, 2, 8);
|
||||||
|
|
||||||
|
injector = WebAppTests.createMockInjector(RMContext.class, rmContext,
|
||||||
|
new Module() {
|
||||||
|
@Override
|
||||||
|
public void configure(Binder binder) {
|
||||||
|
try {
|
||||||
|
ResourceManager rm = TestRMWebApp.mockRm(rmContext);
|
||||||
|
binder.bind(ResourceManager.class).toInstance(rm);
|
||||||
|
binder.bind(ApplicationBaseProtocol.class).toInstance(
|
||||||
|
rm.getClientRMService());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ErrorBlock instance = injector.getInstance(ErrorBlock.class);
|
||||||
|
instance.set(YarnWebParams.APPLICATION_ID, appId.toString());
|
||||||
|
instance.set(YarnWebParams.ERROR_MESSAGE, "This is an error");
|
||||||
|
instance.render();
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,7 @@ import java.io.ObjectInputStream;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
|
import java.net.SocketException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
@ -42,8 +43,10 @@ import javax.servlet.http.HttpServlet;
|
|||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import javax.ws.rs.core.UriBuilderException;
|
||||||
|
|
||||||
import org.apache.hadoop.io.IOUtils;
|
import org.apache.hadoop.io.IOUtils;
|
||||||
|
import org.apache.hadoop.net.NetUtils;
|
||||||
import org.apache.hadoop.yarn.api.records.ApplicationId;
|
import org.apache.hadoop.yarn.api.records.ApplicationId;
|
||||||
import org.apache.hadoop.yarn.api.records.ApplicationReport;
|
import org.apache.hadoop.yarn.api.records.ApplicationReport;
|
||||||
import org.apache.hadoop.yarn.conf.YarnConfiguration;
|
import org.apache.hadoop.yarn.conf.YarnConfiguration;
|
||||||
@ -76,7 +79,8 @@ public class WebAppProxyServlet extends HttpServlet {
|
|||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(
|
private static final Logger LOG = LoggerFactory.getLogger(
|
||||||
WebAppProxyServlet.class);
|
WebAppProxyServlet.class);
|
||||||
private static final Set<String> passThroughHeaders =
|
private static final String REDIRECT = "/redirect";
|
||||||
|
private static final Set<String> PASS_THROUGH_HEADERS =
|
||||||
new HashSet<>(Arrays.asList(
|
new HashSet<>(Arrays.asList(
|
||||||
"User-Agent",
|
"User-Agent",
|
||||||
"Accept",
|
"Accept",
|
||||||
@ -93,6 +97,7 @@ public class WebAppProxyServlet extends HttpServlet {
|
|||||||
private transient List<TrackingUriPlugin> trackingUriPlugins;
|
private transient List<TrackingUriPlugin> trackingUriPlugins;
|
||||||
private final String rmAppPageUrlBase;
|
private final String rmAppPageUrlBase;
|
||||||
private final String ahsAppPageUrlBase;
|
private final String ahsAppPageUrlBase;
|
||||||
|
private final String failurePageUrlBase;
|
||||||
private transient YarnConfiguration conf;
|
private transient YarnConfiguration conf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -126,11 +131,16 @@ public class WebAppProxyServlet extends HttpServlet {
|
|||||||
this.trackingUriPlugins =
|
this.trackingUriPlugins =
|
||||||
conf.getInstances(YarnConfiguration.YARN_TRACKING_URL_GENERATOR,
|
conf.getInstances(YarnConfiguration.YARN_TRACKING_URL_GENERATOR,
|
||||||
TrackingUriPlugin.class);
|
TrackingUriPlugin.class);
|
||||||
this.rmAppPageUrlBase = StringHelper.pjoin(
|
this.rmAppPageUrlBase =
|
||||||
WebAppUtils.getResolvedRMWebAppURLWithScheme(conf), "cluster", "app");
|
StringHelper.pjoin(WebAppUtils.getResolvedRMWebAppURLWithScheme(conf),
|
||||||
this.ahsAppPageUrlBase = StringHelper.pjoin(
|
"cluster", "app");
|
||||||
WebAppUtils.getHttpSchemePrefix(conf) + WebAppUtils
|
this.failurePageUrlBase =
|
||||||
.getAHSWebAppURLWithoutScheme(conf), "applicationhistory", "app");
|
StringHelper.pjoin(WebAppUtils.getResolvedRMWebAppURLWithScheme(conf),
|
||||||
|
"cluster", "failure");
|
||||||
|
this.ahsAppPageUrlBase =
|
||||||
|
StringHelper.pjoin(WebAppUtils.getHttpSchemePrefix(conf)
|
||||||
|
+ WebAppUtils.getAHSWebAppURLWithoutScheme(conf),
|
||||||
|
"applicationhistory", "app");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -222,7 +232,7 @@ public class WebAppProxyServlet extends HttpServlet {
|
|||||||
Enumeration<String> names = req.getHeaderNames();
|
Enumeration<String> names = req.getHeaderNames();
|
||||||
while (names.hasMoreElements()) {
|
while (names.hasMoreElements()) {
|
||||||
String name = names.nextElement();
|
String name = names.nextElement();
|
||||||
if(passThroughHeaders.contains(name)) {
|
if (PASS_THROUGH_HEADERS.contains(name)) {
|
||||||
String value = req.getHeader(name);
|
String value = req.getHeader(name);
|
||||||
if (LOG.isDebugEnabled()) {
|
if (LOG.isDebugEnabled()) {
|
||||||
LOG.debug("REQ HEADER: {} : {}", name, value);
|
LOG.debug("REQ HEADER: {} : {}", name, value);
|
||||||
@ -312,22 +322,34 @@ public class WebAppProxyServlet extends HttpServlet {
|
|||||||
boolean userWasWarned = false;
|
boolean userWasWarned = false;
|
||||||
boolean userApproved = Boolean.parseBoolean(userApprovedParamS);
|
boolean userApproved = Boolean.parseBoolean(userApprovedParamS);
|
||||||
boolean securityEnabled = isSecurityEnabled();
|
boolean securityEnabled = isSecurityEnabled();
|
||||||
|
boolean isRedirect = false;
|
||||||
|
String pathInfo = req.getPathInfo();
|
||||||
final String remoteUser = req.getRemoteUser();
|
final String remoteUser = req.getRemoteUser();
|
||||||
final String pathInfo = req.getPathInfo();
|
|
||||||
|
|
||||||
String[] parts = null;
|
String[] parts = null;
|
||||||
|
|
||||||
if (pathInfo != null) {
|
if (pathInfo != null) {
|
||||||
|
// If there's a redirect, strip the redirect so that the path can be
|
||||||
|
// parsed
|
||||||
|
if (pathInfo.startsWith(REDIRECT)) {
|
||||||
|
pathInfo = pathInfo.substring(REDIRECT.length());
|
||||||
|
isRedirect = true;
|
||||||
|
}
|
||||||
|
|
||||||
parts = pathInfo.split("/", 3);
|
parts = pathInfo.split("/", 3);
|
||||||
}
|
}
|
||||||
if(parts == null || parts.length < 2) {
|
|
||||||
|
if ((parts == null) || (parts.length < 2)) {
|
||||||
LOG.warn("{} gave an invalid proxy path {}", remoteUser, pathInfo);
|
LOG.warn("{} gave an invalid proxy path {}", remoteUser, pathInfo);
|
||||||
notFound(resp, "Your path appears to be formatted incorrectly.");
|
notFound(resp, "Your path appears to be formatted incorrectly.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//parts[0] is empty because path info always starts with a /
|
//parts[0] is empty because path info always starts with a /
|
||||||
String appId = parts[1];
|
String appId = parts[1];
|
||||||
String rest = parts.length > 2 ? parts[2] : "";
|
String rest = parts.length > 2 ? parts[2] : "";
|
||||||
ApplicationId id = Apps.toAppID(appId);
|
ApplicationId id = Apps.toAppID(appId);
|
||||||
|
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
LOG.warn("{} attempting to access {} that is invalid",
|
LOG.warn("{} attempting to access {} that is invalid",
|
||||||
remoteUser, appId);
|
remoteUser, appId);
|
||||||
@ -335,6 +357,13 @@ public class WebAppProxyServlet extends HttpServlet {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this call is from an AM redirect, we need to be careful about how
|
||||||
|
// we handle it. If this method returns true, it means the method
|
||||||
|
// already redirected the response, so we can just return.
|
||||||
|
if (isRedirect && handleRedirect(appId, req, resp)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (securityEnabled) {
|
if (securityEnabled) {
|
||||||
String cookieName = getCheckCookieName(id);
|
String cookieName = getCheckCookieName(id);
|
||||||
Cookie[] cookies = req.getCookies();
|
Cookie[] cookies = req.getCookies();
|
||||||
@ -351,21 +380,20 @@ public class WebAppProxyServlet extends HttpServlet {
|
|||||||
|
|
||||||
boolean checkUser = securityEnabled && (!userWasWarned || !userApproved);
|
boolean checkUser = securityEnabled && (!userWasWarned || !userApproved);
|
||||||
|
|
||||||
FetchedAppReport fetchedAppReport = null;
|
FetchedAppReport fetchedAppReport;
|
||||||
ApplicationReport applicationReport = null;
|
|
||||||
try {
|
try {
|
||||||
fetchedAppReport = getApplicationReport(id);
|
fetchedAppReport = getFetchedAppReport(id);
|
||||||
if (fetchedAppReport != null) {
|
} catch (ApplicationNotFoundException e) {
|
||||||
if (fetchedAppReport.getAppReportSource() != AppReportSource.RM &&
|
fetchedAppReport = null;
|
||||||
fetchedAppReport.getAppReportSource() != AppReportSource.AHS) {
|
|
||||||
throw new UnsupportedOperationException("Application report not "
|
|
||||||
+ "fetched from RM or history server.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApplicationReport applicationReport = null;
|
||||||
|
|
||||||
|
if (fetchedAppReport != null) {
|
||||||
applicationReport = fetchedAppReport.getApplicationReport();
|
applicationReport = fetchedAppReport.getApplicationReport();
|
||||||
}
|
}
|
||||||
} catch (ApplicationNotFoundException e) {
|
|
||||||
applicationReport = null;
|
|
||||||
}
|
|
||||||
if (applicationReport == null) {
|
if (applicationReport == null) {
|
||||||
LOG.warn("{} attempting to access {} that was not found",
|
LOG.warn("{} attempting to access {} that was not found",
|
||||||
remoteUser, id);
|
remoteUser, id);
|
||||||
@ -382,57 +410,31 @@ public class WebAppProxyServlet extends HttpServlet {
|
|||||||
"in RM or history server");
|
"in RM or history server");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String original = applicationReport.getOriginalTrackingUrl();
|
|
||||||
URI trackingUri;
|
URI trackingUri = getTrackingUri(req, resp, id,
|
||||||
if (original == null || original.equals("N/A") || original.equals("")) {
|
applicationReport.getOriginalTrackingUrl(),
|
||||||
if (fetchedAppReport.getAppReportSource() == AppReportSource.RM) {
|
fetchedAppReport.getAppReportSource());
|
||||||
// fallback to ResourceManager's app page if no tracking URI provided
|
|
||||||
// and Application Report was fetched from RM
|
// If the tracking URI is null, there was a redirect, so just return.
|
||||||
LOG.debug("Original tracking url is '{}'. Redirecting to RM app page",
|
if (trackingUri == null) {
|
||||||
original == null? "NULL" : original);
|
|
||||||
ProxyUtils.sendRedirect(req, resp,
|
|
||||||
StringHelper.pjoin(rmAppPageUrlBase, id.toString()));
|
|
||||||
} else if (fetchedAppReport.getAppReportSource()
|
|
||||||
== AppReportSource.AHS) {
|
|
||||||
// fallback to Application History Server app page if the application
|
|
||||||
// report was fetched from AHS
|
|
||||||
LOG.debug("Original tracking url is '{}'. Redirecting to AHS app page"
|
|
||||||
, original == null? "NULL" : original);
|
|
||||||
ProxyUtils.sendRedirect(req, resp,
|
|
||||||
StringHelper.pjoin(ahsAppPageUrlBase, id.toString()));
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
if (ProxyUriUtils.getSchemeFromUrl(original).isEmpty()) {
|
|
||||||
trackingUri = ProxyUriUtils.getUriFromAMUrl(
|
|
||||||
WebAppUtils.getHttpSchemePrefix(conf), original);
|
|
||||||
} else {
|
|
||||||
trackingUri = new URI(original);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String runningUser = applicationReport.getUser();
|
String runningUser = applicationReport.getUser();
|
||||||
|
|
||||||
if (checkUser && !runningUser.equals(remoteUser)) {
|
if (checkUser && !runningUser.equals(remoteUser)) {
|
||||||
LOG.info("Asking {} if they want to connect to the "
|
LOG.info("Asking {} if they want to connect to the "
|
||||||
+ "app master GUI of {} owned by {}",
|
+ "app master GUI of {} owned by {}",
|
||||||
remoteUser, appId, runningUser);
|
remoteUser, appId, runningUser);
|
||||||
warnUserPage(resp, ProxyUriUtils.getPathAndQuery(id, rest,
|
warnUserPage(resp, ProxyUriUtils.getPathAndQuery(id, rest,
|
||||||
req.getQueryString(), true), runningUser, id);
|
req.getQueryString(), true), runningUser, id);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the user-provided path and query parameter to the original
|
// Append the user-provided path and query parameter to the original
|
||||||
// tracking url.
|
// tracking url.
|
||||||
UriBuilder builder = UriBuilder.fromUri(trackingUri);
|
URI toFetch = buildTrackingUrl(trackingUri, req, rest);
|
||||||
String queryString = req.getQueryString();
|
|
||||||
if (queryString != null) {
|
|
||||||
List<NameValuePair> queryPairs =
|
|
||||||
URLEncodedUtils.parse(queryString, null);
|
|
||||||
for (NameValuePair pair : queryPairs) {
|
|
||||||
builder.queryParam(pair.getName(), pair.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
URI toFetch = builder.path(rest).build();
|
|
||||||
|
|
||||||
LOG.info("{} is accessing unchecked {}"
|
LOG.info("{} is accessing unchecked {}"
|
||||||
+ " which is the app master GUI of {} owned by {}",
|
+ " which is the app master GUI of {} owned by {}",
|
||||||
@ -458,6 +460,152 @@ public class WebAppProxyServlet extends HttpServlet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a URL based on the {@code trackingUri} that includes the
|
||||||
|
* user-provided path and query parameters.
|
||||||
|
*
|
||||||
|
* @param trackingUri the base tracking URI
|
||||||
|
* @param req the service request
|
||||||
|
* @param rest the user-provided path
|
||||||
|
* @return the new tracking URI
|
||||||
|
* @throws UriBuilderException if there's an error building the URL
|
||||||
|
*/
|
||||||
|
private URI buildTrackingUrl(URI trackingUri, final HttpServletRequest req,
|
||||||
|
String rest) throws UriBuilderException {
|
||||||
|
UriBuilder builder = UriBuilder.fromUri(trackingUri);
|
||||||
|
String queryString = req.getQueryString();
|
||||||
|
|
||||||
|
if (queryString != null) {
|
||||||
|
List<NameValuePair> queryPairs = URLEncodedUtils.parse(queryString, null);
|
||||||
|
|
||||||
|
for (NameValuePair pair : queryPairs) {
|
||||||
|
builder.queryParam(pair.getName(), pair.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.path(rest).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locate the tracking URI for the application based on the reported tracking
|
||||||
|
* URI. If the reported URI is invalid, redirect to the history server or RM
|
||||||
|
* app page. If the URI is valid, covert it into a usable URI object with a
|
||||||
|
* schema. If the returned URI is null, that means there was a redirect.
|
||||||
|
*
|
||||||
|
* @param req the servlet request for redirects
|
||||||
|
* @param resp the servlet response for redirects
|
||||||
|
* @param id the application ID
|
||||||
|
* @param originalUri the reported tracking URI
|
||||||
|
* @param appReportSource the source of the application report
|
||||||
|
* @return a valid tracking URI or null if redirected instead
|
||||||
|
* @throws IOException thrown if the redirect fails
|
||||||
|
* @throws URISyntaxException if the tracking URI is invalid
|
||||||
|
*/
|
||||||
|
private URI getTrackingUri(HttpServletRequest req, HttpServletResponse resp,
|
||||||
|
ApplicationId id, String originalUri, AppReportSource appReportSource)
|
||||||
|
throws IOException, URISyntaxException {
|
||||||
|
URI trackingUri = null;
|
||||||
|
|
||||||
|
if ((originalUri == null) ||
|
||||||
|
originalUri.equals("N/A") ||
|
||||||
|
originalUri.equals("")) {
|
||||||
|
if (appReportSource == AppReportSource.RM) {
|
||||||
|
// fallback to ResourceManager's app page if no tracking URI provided
|
||||||
|
// and Application Report was fetched from RM
|
||||||
|
LOG.debug("Original tracking url is '{}'. Redirecting to RM app page",
|
||||||
|
originalUri == null ? "NULL" : originalUri);
|
||||||
|
ProxyUtils.sendRedirect(req, resp,
|
||||||
|
StringHelper.pjoin(rmAppPageUrlBase, id.toString()));
|
||||||
|
} else if (appReportSource == AppReportSource.AHS) {
|
||||||
|
// fallback to Application History Server app page if the application
|
||||||
|
// report was fetched from AHS
|
||||||
|
LOG.debug("Original tracking url is '{}'. Redirecting to AHS app page",
|
||||||
|
originalUri == null ? "NULL" : originalUri);
|
||||||
|
ProxyUtils.sendRedirect(req, resp,
|
||||||
|
StringHelper.pjoin(ahsAppPageUrlBase, id.toString()));
|
||||||
|
}
|
||||||
|
} else if (ProxyUriUtils.getSchemeFromUrl(originalUri).isEmpty()) {
|
||||||
|
trackingUri =
|
||||||
|
ProxyUriUtils.getUriFromAMUrl(WebAppUtils.getHttpSchemePrefix(conf),
|
||||||
|
originalUri);
|
||||||
|
} else {
|
||||||
|
trackingUri = new URI(originalUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trackingUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the application report from the RM.
|
||||||
|
*
|
||||||
|
* @param id the app ID
|
||||||
|
* @return the application report
|
||||||
|
* @throws IOException if the request to the RM fails
|
||||||
|
* @throws YarnException if the request to the RM fails
|
||||||
|
*/
|
||||||
|
private FetchedAppReport getFetchedAppReport(ApplicationId id)
|
||||||
|
throws IOException, YarnException {
|
||||||
|
FetchedAppReport fetchedAppReport = getApplicationReport(id);
|
||||||
|
|
||||||
|
if (fetchedAppReport != null) {
|
||||||
|
if ((fetchedAppReport.getAppReportSource() != AppReportSource.RM) &&
|
||||||
|
(fetchedAppReport.getAppReportSource() != AppReportSource.AHS)) {
|
||||||
|
throw new UnsupportedOperationException("Application report not "
|
||||||
|
+ "fetched from RM or history server.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchedAppReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the request is a redirect from the AM and handle it
|
||||||
|
* appropriately. This check exists to prevent the AM from forwarding back to
|
||||||
|
* the web proxy, which would contact the AM again, which would forward
|
||||||
|
* again... If this method returns true, there was a redirect, and
|
||||||
|
* it was handled by redirecting the current request to an error page.
|
||||||
|
*
|
||||||
|
* @param path the part of the request path after the app id
|
||||||
|
* @param id the app id
|
||||||
|
* @param req the request object
|
||||||
|
* @param resp the response object
|
||||||
|
* @return whether there was a redirect
|
||||||
|
* @throws IOException if a redirect fails
|
||||||
|
*/
|
||||||
|
private boolean handleRedirect(String id, HttpServletRequest req,
|
||||||
|
HttpServletResponse resp) throws IOException {
|
||||||
|
// If this isn't a redirect, we don't care.
|
||||||
|
boolean badRedirect = false;
|
||||||
|
|
||||||
|
// If this is a redirect, check if we're calling ourselves.
|
||||||
|
try {
|
||||||
|
badRedirect = NetUtils.getLocalInetAddress(req.getRemoteHost()) != null;
|
||||||
|
} catch (SocketException ex) {
|
||||||
|
// This exception means we can't determine the calling host. Odds are
|
||||||
|
// that means it's not us. Let it go and hope it works out better next
|
||||||
|
// time.
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the proxy tries to call itself, it gets into an endless
|
||||||
|
// loop and consumes all available handler threads until the
|
||||||
|
// application completes. Redirect to the app page with a flag
|
||||||
|
// that tells it to print an appropriate error message.
|
||||||
|
if (badRedirect) {
|
||||||
|
LOG.error("The AM's web app redirected the RM web proxy's request back "
|
||||||
|
+ "to the web proxy. The typical cause is that the AM is resolving "
|
||||||
|
+ "the RM's address as something other than what it expects. Check "
|
||||||
|
+ "your network configuration and the value of the "
|
||||||
|
+ "yarn.web-proxy.address property. Once the host resolution issue "
|
||||||
|
+ "has been resolved, you will likely need to delete the "
|
||||||
|
+ "misbehaving application, " + id);
|
||||||
|
String redirect = StringHelper.pjoin(failurePageUrlBase, id);
|
||||||
|
LOG.error("REDIRECT: sending redirect to " + redirect);
|
||||||
|
ProxyUtils.sendRedirect(req, resp, redirect);
|
||||||
|
}
|
||||||
|
|
||||||
|
return badRedirect;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is used by Java object deserialization, to fill in the
|
* This method is used by Java object deserialization, to fill in the
|
||||||
* transient {@link #trackingUriPlugins} field.
|
* transient {@link #trackingUriPlugins} field.
|
||||||
|
@ -59,8 +59,9 @@ public class AmIpFilter implements Filter {
|
|||||||
public static final String PROXY_HOSTS_DELIMITER = ",";
|
public static final String PROXY_HOSTS_DELIMITER = ",";
|
||||||
public static final String PROXY_URI_BASES = "PROXY_URI_BASES";
|
public static final String PROXY_URI_BASES = "PROXY_URI_BASES";
|
||||||
public static final String PROXY_URI_BASES_DELIMITER = ",";
|
public static final String PROXY_URI_BASES_DELIMITER = ",";
|
||||||
|
private static final String PROXY_PATH = "/proxy";
|
||||||
//update the proxy IP list about every 5 min
|
//update the proxy IP list about every 5 min
|
||||||
private static final long updateInterval = 5 * 60 * 1000;
|
private static final long UPDATE_INTERVAL = 5 * 60 * 1000;
|
||||||
|
|
||||||
private String[] proxyHosts;
|
private String[] proxyHosts;
|
||||||
private Set<String> proxyAddresses = null;
|
private Set<String> proxyAddresses = null;
|
||||||
@ -96,7 +97,7 @@ public class AmIpFilter implements Filter {
|
|||||||
protected Set<String> getProxyAddresses() throws ServletException {
|
protected Set<String> getProxyAddresses() throws ServletException {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if(proxyAddresses == null || (lastUpdate + updateInterval) >= now) {
|
if (proxyAddresses == null || (lastUpdate + UPDATE_INTERVAL) >= now) {
|
||||||
proxyAddresses = new HashSet<>();
|
proxyAddresses = new HashSet<>();
|
||||||
for (String proxyHost : proxyHosts) {
|
for (String proxyHost : proxyHosts) {
|
||||||
try {
|
try {
|
||||||
@ -131,16 +132,27 @@ public class AmIpFilter implements Filter {
|
|||||||
|
|
||||||
HttpServletRequest httpReq = (HttpServletRequest)req;
|
HttpServletRequest httpReq = (HttpServletRequest)req;
|
||||||
HttpServletResponse httpResp = (HttpServletResponse)resp;
|
HttpServletResponse httpResp = (HttpServletResponse)resp;
|
||||||
|
|
||||||
if (LOG.isDebugEnabled()) {
|
if (LOG.isDebugEnabled()) {
|
||||||
LOG.debug("Remote address for request is: {}", httpReq.getRemoteAddr());
|
LOG.debug("Remote address for request is: {}", httpReq.getRemoteAddr());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!getProxyAddresses().contains(httpReq.getRemoteAddr())) {
|
if (!getProxyAddresses().contains(httpReq.getRemoteAddr())) {
|
||||||
String redirectUrl = findRedirectUrl();
|
StringBuilder redirect = new StringBuilder(findRedirectUrl());
|
||||||
String target = redirectUrl + httpReq.getRequestURI();
|
|
||||||
ProxyUtils.sendRedirect(httpReq, httpResp, target);
|
redirect.append(httpReq.getRequestURI());
|
||||||
return;
|
|
||||||
|
int insertPoint = redirect.indexOf(PROXY_PATH);
|
||||||
|
|
||||||
|
if (insertPoint >= 0) {
|
||||||
|
// Add /redirect as the second component of the path so that the RM web
|
||||||
|
// proxy knows that this request was a redirect.
|
||||||
|
insertPoint += PROXY_PATH.length();
|
||||||
|
redirect.insert(insertPoint, "/redirect");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProxyUtils.sendRedirect(httpReq, httpResp, redirect.toString());
|
||||||
|
} else {
|
||||||
String user = null;
|
String user = null;
|
||||||
|
|
||||||
if (httpReq.getCookies() != null) {
|
if (httpReq.getCookies() != null) {
|
||||||
@ -153,17 +165,21 @@ public class AmIpFilter implements Filter {
|
|||||||
}
|
}
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
if (LOG.isDebugEnabled()) {
|
if (LOG.isDebugEnabled()) {
|
||||||
LOG.debug("Could not find " + WebAppProxyServlet.PROXY_USER_COOKIE_NAME
|
LOG.debug("Could not find "
|
||||||
|
+ WebAppProxyServlet.PROXY_USER_COOKIE_NAME
|
||||||
+ " cookie, so user will not be set");
|
+ " cookie, so user will not be set");
|
||||||
}
|
}
|
||||||
|
|
||||||
chain.doFilter(req, resp);
|
chain.doFilter(req, resp);
|
||||||
} else {
|
} else {
|
||||||
final AmIpPrincipal principal = new AmIpPrincipal(user);
|
AmIpPrincipal principal = new AmIpPrincipal(user);
|
||||||
ServletRequest requestWrapper = new AmIpServletRequestWrapper(httpReq,
|
ServletRequest requestWrapper = new AmIpServletRequestWrapper(httpReq,
|
||||||
principal);
|
principal);
|
||||||
|
|
||||||
chain.doFilter(requestWrapper, resp);
|
chain.doFilter(requestWrapper, resp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected String findRedirectUrl() throws ServletException {
|
protected String findRedirectUrl() throws ServletException {
|
||||||
String addr;
|
String addr;
|
||||||
|
@ -155,7 +155,7 @@ public class TestWebAppProxyServlet {
|
|||||||
URL emptyUrl = new URL("http://localhost:" + proxyPort + "/proxy");
|
URL emptyUrl = new URL("http://localhost:" + proxyPort + "/proxy");
|
||||||
HttpURLConnection emptyProxyConn = (HttpURLConnection) emptyUrl
|
HttpURLConnection emptyProxyConn = (HttpURLConnection) emptyUrl
|
||||||
.openConnection();
|
.openConnection();
|
||||||
emptyProxyConn.connect();;
|
emptyProxyConn.connect();
|
||||||
assertEquals(HttpURLConnection.HTTP_NOT_FOUND, emptyProxyConn.getResponseCode());
|
assertEquals(HttpURLConnection.HTTP_NOT_FOUND, emptyProxyConn.getResponseCode());
|
||||||
|
|
||||||
// wrong url. Set wrong app ID
|
// wrong url. Set wrong app ID
|
||||||
@ -176,6 +176,25 @@ public class TestWebAppProxyServlet {
|
|||||||
assertEquals(HttpURLConnection.HTTP_OK, proxyConn.getResponseCode());
|
assertEquals(HttpURLConnection.HTTP_OK, proxyConn.getResponseCode());
|
||||||
assertTrue(isResponseCookiePresent(
|
assertTrue(isResponseCookiePresent(
|
||||||
proxyConn, "checked_application_0_0000", "true"));
|
proxyConn, "checked_application_0_0000", "true"));
|
||||||
|
|
||||||
|
// test that redirection is squashed correctly
|
||||||
|
URL redirectUrl = new URL("http://localhost:" + proxyPort
|
||||||
|
+ "/proxy/redirect/application_00_0");
|
||||||
|
proxyConn = (HttpURLConnection) redirectUrl.openConnection();
|
||||||
|
proxyConn.setInstanceFollowRedirects(false);
|
||||||
|
proxyConn.connect();
|
||||||
|
assertEquals("The proxy returned an unexpected status code rather than"
|
||||||
|
+ "redirecting the connection (302)",
|
||||||
|
HttpURLConnection.HTTP_MOVED_TEMP, proxyConn.getResponseCode());
|
||||||
|
|
||||||
|
String expected =
|
||||||
|
WebAppUtils.getResolvedRMWebAppURLWithScheme(configuration)
|
||||||
|
+ "/cluster/failure/application_00_0";
|
||||||
|
String redirect = proxyConn.getHeaderField(ProxyUtils.LOCATION);
|
||||||
|
|
||||||
|
assertEquals("The proxy did not redirect the connection to the failure "
|
||||||
|
+ "page of the RM", expected, redirect);
|
||||||
|
|
||||||
// cannot found application 1: null
|
// cannot found application 1: null
|
||||||
appReportFetcher.answer = 1;
|
appReportFetcher.answer = 1;
|
||||||
proxyConn = (HttpURLConnection) url.openConnection();
|
proxyConn = (HttpURLConnection) url.openConnection();
|
||||||
@ -185,6 +204,7 @@ public class TestWebAppProxyServlet {
|
|||||||
proxyConn.getResponseCode());
|
proxyConn.getResponseCode());
|
||||||
assertFalse(isResponseCookiePresent(
|
assertFalse(isResponseCookiePresent(
|
||||||
proxyConn, "checked_application_0_0000", "true"));
|
proxyConn, "checked_application_0_0000", "true"));
|
||||||
|
|
||||||
// cannot found application 2: ApplicationNotFoundException
|
// cannot found application 2: ApplicationNotFoundException
|
||||||
appReportFetcher.answer = 4;
|
appReportFetcher.answer = 4;
|
||||||
proxyConn = (HttpURLConnection) url.openConnection();
|
proxyConn = (HttpURLConnection) url.openConnection();
|
||||||
@ -194,6 +214,7 @@ public class TestWebAppProxyServlet {
|
|||||||
proxyConn.getResponseCode());
|
proxyConn.getResponseCode());
|
||||||
assertFalse(isResponseCookiePresent(
|
assertFalse(isResponseCookiePresent(
|
||||||
proxyConn, "checked_application_0_0000", "true"));
|
proxyConn, "checked_application_0_0000", "true"));
|
||||||
|
|
||||||
// wrong user
|
// wrong user
|
||||||
appReportFetcher.answer = 2;
|
appReportFetcher.answer = 2;
|
||||||
proxyConn = (HttpURLConnection) url.openConnection();
|
proxyConn = (HttpURLConnection) url.openConnection();
|
||||||
@ -203,6 +224,7 @@ public class TestWebAppProxyServlet {
|
|||||||
assertTrue(s
|
assertTrue(s
|
||||||
.contains("to continue to an Application Master web interface owned by"));
|
.contains("to continue to an Application Master web interface owned by"));
|
||||||
assertTrue(s.contains("WARNING: The following page may not be safe!"));
|
assertTrue(s.contains("WARNING: The following page may not be safe!"));
|
||||||
|
|
||||||
//case if task has a not running status
|
//case if task has a not running status
|
||||||
appReportFetcher.answer = 3;
|
appReportFetcher.answer = 3;
|
||||||
proxyConn = (HttpURLConnection) url.openConnection();
|
proxyConn = (HttpURLConnection) url.openConnection();
|
||||||
|
@ -21,6 +21,7 @@ package org.apache.hadoop.yarn.server.webproxy.amfilter;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
@ -147,8 +148,8 @@ public class TestAmFilter {
|
|||||||
testFilter.init(config);
|
testFilter.init(config);
|
||||||
|
|
||||||
HttpServletResponseForTest response = new HttpServletResponseForTest();
|
HttpServletResponseForTest response = new HttpServletResponseForTest();
|
||||||
// Test request should implements HttpServletRequest
|
|
||||||
|
|
||||||
|
// Test request should implements HttpServletRequest
|
||||||
ServletRequest failRequest = Mockito.mock(ServletRequest.class);
|
ServletRequest failRequest = Mockito.mock(ServletRequest.class);
|
||||||
try {
|
try {
|
||||||
testFilter.doFilter(failRequest, response, chain);
|
testFilter.doFilter(failRequest, response, chain);
|
||||||
@ -159,22 +160,32 @@ public class TestAmFilter {
|
|||||||
|
|
||||||
// request with HttpServletRequest
|
// request with HttpServletRequest
|
||||||
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
|
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
|
||||||
Mockito.when(request.getRemoteAddr()).thenReturn("redirect");
|
Mockito.when(request.getRemoteAddr()).thenReturn("nowhere");
|
||||||
Mockito.when(request.getRequestURI()).thenReturn("/redirect");
|
Mockito.when(request.getRequestURI()).thenReturn("/app/application_00_0");
|
||||||
|
|
||||||
|
// address "redirect" is not in host list for non-proxy connection
|
||||||
testFilter.doFilter(request, response, chain);
|
testFilter.doFilter(request, response, chain);
|
||||||
// address "redirect" is not in host list
|
assertEquals(HttpURLConnection.HTTP_MOVED_TEMP, response.status);
|
||||||
assertEquals(302, response.status);
|
|
||||||
String redirect = response.getHeader(ProxyUtils.LOCATION);
|
String redirect = response.getHeader(ProxyUtils.LOCATION);
|
||||||
assertEquals("http://bogus/redirect", redirect);
|
assertEquals("http://bogus/app/application_00_0", redirect);
|
||||||
|
|
||||||
|
// address "redirect" is not in host list for proxy connection
|
||||||
|
Mockito.when(request.getRequestURI()).thenReturn("/proxy/application_00_0");
|
||||||
|
testFilter.doFilter(request, response, chain);
|
||||||
|
assertEquals(HttpURLConnection.HTTP_MOVED_TEMP, response.status);
|
||||||
|
redirect = response.getHeader(ProxyUtils.LOCATION);
|
||||||
|
assertEquals("http://bogus/proxy/redirect/application_00_0", redirect);
|
||||||
|
|
||||||
// "127.0.0.1" contains in host list. Without cookie
|
// "127.0.0.1" contains in host list. Without cookie
|
||||||
Mockito.when(request.getRemoteAddr()).thenReturn("127.0.0.1");
|
Mockito.when(request.getRemoteAddr()).thenReturn("127.0.0.1");
|
||||||
testFilter.doFilter(request, response, chain);
|
testFilter.doFilter(request, response, chain);
|
||||||
|
|
||||||
assertTrue(doFilterRequest
|
assertTrue(doFilterRequest
|
||||||
.contains("javax.servlet.http.HttpServletRequest"));
|
.contains("javax.servlet.http.HttpServletRequest"));
|
||||||
|
|
||||||
// cookie added
|
// cookie added
|
||||||
Cookie[] cookies = new Cookie[1];
|
Cookie[] cookies = new Cookie[] {
|
||||||
cookies[0] = new Cookie(WebAppProxyServlet.PROXY_USER_COOKIE_NAME, "user");
|
new Cookie(WebAppProxyServlet.PROXY_USER_COOKIE_NAME, "user")
|
||||||
|
};
|
||||||
|
|
||||||
Mockito.when(request.getCookies()).thenReturn(cookies);
|
Mockito.when(request.getCookies()).thenReturn(cookies);
|
||||||
testFilter.doFilter(request, response, chain);
|
testFilter.doFilter(request, response, chain);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user