diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/src/main/java/org/apache/hadoop/yarn/server/webapp/AppBlock.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/src/main/java/org/apache/hadoop/yarn/server/webapp/AppBlock.java index 69beef2..e801dc3 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/src/main/java/org/apache/hadoop/yarn/server/webapp/AppBlock.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-common/src/main/java/org/apache/hadoop/yarn/server/webapp/AppBlock.java @@ -81,9 +81,22 @@ protected AppBlock(ApplicationBaseProtocol appBaseProt, ViewContext ctx, protected void render(Block html) { String webUiType = $(WEB_UI_TYPE); String aid = $(APPLICATION_ID); + if (aid.isEmpty()) { puts("Bad request: requires Application ID"); return; + } else if (aid.endsWith("R")) { + aid = aid.substring(0, aid.length() - 1); + html.p()._("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.")._(); + + return; } try { @@ -164,6 +177,50 @@ public ApplicationReport run() throws Exception { String schedulerPath = WebAppUtils.getResolvedRMWebAppURLWithScheme(conf) + "/cluster/scheduler?openQueues=" + app.getQueue(); + generateOverviewTable(app, webUiType, schedulerPath); + + Collection attempts; + try { + final GetApplicationAttemptsRequest request = + GetApplicationAttemptsRequest.newInstance(appID); + if (callerUGI == null) { + attempts = appBaseProt.getApplicationAttempts(request) + .getApplicationAttemptList(); + } else { + attempts = callerUGI.doAs( + new PrivilegedExceptionAction>() { + @Override + public Collection run() throws Exception { + return appBaseProt.getApplicationAttempts(request) + .getApplicationAttemptList(); + } + }); + } + } catch (Exception e) { + String message = + "Failed to read the attempts of the application " + appID + "."; + LOG.error(message, e); + html.p()._(message)._(); + return; + } + + createApplicationMetricsTable(html); + + html._(InfoBlock.class); + + generateApplicationTable(html, callerUGI, attempts); + } + + /** + * Generate the overview table. + * + * @param app the app info + * @param webUiType the web UI type + * @param schedulerPath the scheduler path + * @return the overview table + */ + private void generateOverviewTable(AppInfo app, String webUiType, + String schedulerPath) { ResponseInfo overviewTable = info("Application Overview") ._("User:", schedulerPath, app.getUser()) ._("Name:", app.getName()) @@ -173,8 +230,9 @@ public ApplicationReport run() throws Exception { ._("Application Priority:", clarifyAppPriority(app.getPriority())) ._( "YarnApplicationState:", - app.getAppState() == null ? UNAVAILABLE : clarifyAppState(app - .getAppState())) + app.getAppState() == null ? + UNAVAILABLE : + clarifyAppState(app.getAppState())) ._("Queue:", schedulerPath, app.getQueue()) ._("FinalStatus Reported by AM:", clairfyAppFinalStatus(app.getFinalAppStatus())) @@ -185,29 +243,32 @@ public ApplicationReport run() throws Exception { app.getFinishedTime()))) ._( "Tracking URL:", - app.getTrackingUrl() == null - || app.getTrackingUrl().equals(UNAVAILABLE) ? null : root_url(app - .getTrackingUrl()), - app.getTrackingUrl() == null - || app.getTrackingUrl().equals(UNAVAILABLE) ? "Unassigned" : app - .getAppState() == YarnApplicationState.FINISHED - || app.getAppState() == YarnApplicationState.FAILED - || app.getAppState() == YarnApplicationState.KILLED ? "History" + app.getTrackingUrl() == null || + app.getTrackingUrl().equals(UNAVAILABLE) ? + null : + root_url(app.getTrackingUrl()), + app.getTrackingUrl() == null || + app.getTrackingUrl().equals(UNAVAILABLE) ? "Unassigned" : + app.getAppState() == YarnApplicationState.FINISHED || + app.getAppState() == YarnApplicationState.FAILED || + app.getAppState() == YarnApplicationState.KILLED ? "History" : "ApplicationMaster"); - if (webUiType != null - && webUiType.equals(YarnWebParams.RM_WEB_UI)) { + + if ((webUiType != null) && webUiType.equals(YarnWebParams.RM_WEB_UI)) { LogAggregationStatus status = getLogAggregationStatus(); + if (status == null) { overviewTable._("Log Aggregation Status:", "N/A"); - } else if (status == LogAggregationStatus.DISABLED - || status == LogAggregationStatus.NOT_START - || status == LogAggregationStatus.SUCCEEDED) { + } else if (status == LogAggregationStatus.DISABLED || + status == LogAggregationStatus.NOT_START || + status == LogAggregationStatus.SUCCEEDED) { overviewTable._("Log Aggregation Status:", status.name()); } else { overviewTable._("Log Aggregation Status:", root_url("logaggregationstatus", app.getAppId()), status.name()); } } + overviewTable._("Diagnostics:", app.getDiagnosticsInfo() == null ? "" : app.getDiagnosticsInfo()); overviewTable._("Unmanaged Application:", app.isUnmanagedApp()); @@ -217,38 +278,6 @@ public ApplicationReport run() throws Exception { overviewTable._("AM container Node Label expression:", app.getAmNodeLabelExpression() == null ? "" : app.getAmNodeLabelExpression()); - - Collection attempts; - try { - final GetApplicationAttemptsRequest request = - GetApplicationAttemptsRequest.newInstance(appID); - if (callerUGI == null) { - attempts = appBaseProt.getApplicationAttempts(request) - .getApplicationAttemptList(); - } else { - attempts = callerUGI.doAs( - new PrivilegedExceptionAction> () { - @Override - public Collection run() throws Exception { - return appBaseProt.getApplicationAttempts(request) - .getApplicationAttemptList(); - } - }); - } - } catch (Exception e) { - String message = - "Failed to read the attempts of the application " + appID + "."; - LOG.error(message, e); - html.p()._(message)._(); - return; - } - - createApplicationMetricsTable(html); - - html._(InfoBlock.class); - - generateApplicationTable(html, callerUGI, attempts); - } protected void generateApplicationTable(Block html, diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/AppPage.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/AppPage.java index 0c5516a..2741f9e 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/AppPage.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-resourcemanager/src/main/java/org/apache/hadoop/yarn/server/resourcemanager/webapp/AppPage.java @@ -33,10 +33,19 @@ protected void preHead(Page.HTML<_> html) { commonPreHead(html); String appId = $(YarnWebParams.APPLICATION_ID); - set( - TITLE, - appId.isEmpty() ? "Bad request: missing application ID" : join( - "Application ", $(YarnWebParams.APPLICATION_ID))); + + if (appId.isEmpty()) { + set(TITLE, "Bad request: missing application ID"); + } else { + // An app ID that ends with 'R' means that this page is being loaded to + // display an error message. As far as this class is concerned, the 'R' + // can just be ignored. + if (appId.endsWith("R")) { + appId = appId.substring(0, appId.length() - 1); + } + + set(TITLE, join("Application ", appId)); + } set(DATATABLES_ID, "attempts ResourceRequests"); set(initID(DATATABLES, "attempts"), WebPageUtils.attemptsTableInit()); diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java index 0b621aa..e17aef5 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/WebAppProxyServlet.java @@ -26,6 +26,7 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.net.InetAddress; +import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; @@ -44,6 +45,7 @@ import javax.ws.rs.core.UriBuilder; 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.ApplicationReport; import org.apache.hadoop.yarn.conf.YarnConfiguration; @@ -316,26 +318,37 @@ private void methodAction(final HttpServletRequest req, final String pathInfo = req.getPathInfo(); String[] parts = null; + if (pathInfo != null) { 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); notFound(resp, "Your path appears to be formatted incorrectly."); return; } + //parts[0] is empty because path info always starts with a / String appId = parts[1]; String rest = parts.length > 2 ? parts[2] : ""; ApplicationId id = Apps.toAppID(appId); - if(id == null) { + + if (id == null) { LOG.warn("{} attempting to access {} that is invalid", remoteUser, appId); notFound(resp, appId + " appears to be formatted incorrectly."); return; } - - if(securityEnabled) { + + // 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 (handleRedirect(rest, appId, req, resp)) { + return; + } + + if (securityEnabled) { String cookieName = getCheckCookieName(id); Cookie[] cookies = req.getCookies(); if (cookies != null) { @@ -351,22 +364,21 @@ private void methodAction(final HttpServletRequest req, boolean checkUser = securityEnabled && (!userWasWarned || !userApproved); - FetchedAppReport fetchedAppReport = null; - ApplicationReport applicationReport = null; + FetchedAppReport fetchedAppReport; + try { - 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."); - } - applicationReport = fetchedAppReport.getApplicationReport(); - } + fetchedAppReport = getFetchedAppReport(id); } catch (ApplicationNotFoundException e) { - applicationReport = null; + fetchedAppReport = null; } - if(applicationReport == null) { + + ApplicationReport applicationReport = null; + + if (fetchedAppReport != null) { + applicationReport = fetchedAppReport.getApplicationReport(); + } + + if (applicationReport == null) { LOG.warn("{} attempting to access {} that was not found", remoteUser, id); @@ -382,42 +394,25 @@ private void methodAction(final HttpServletRequest req, "in RM or history server"); return; } - String original = applicationReport.getOriginalTrackingUrl(); - URI trackingUri; - if (original == null || original.equals("N/A") || original.equals("")) { - if (fetchedAppReport.getAppReportSource() == 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", - 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())); - } + + URI trackingUri = getTrackingUri(req, resp, id, + applicationReport.getOriginalTrackingUrl(), + fetchedAppReport.getAppReportSource()); + + // If the tracking URI is null, there was a redirect, so just return. + if (trackingUri == null) { return; - } else { - if (ProxyUriUtils.getSchemeFromUrl(original).isEmpty()) { - trackingUri = ProxyUriUtils.getUriFromAMUrl( - WebAppUtils.getHttpSchemePrefix(conf), original); - } else { - trackingUri = new URI(original); - } } String runningUser = applicationReport.getUser(); - if(checkUser && !runningUser.equals(remoteUser)) { + + if (checkUser && !runningUser.equals(remoteUser)) { LOG.info("Asking {} if they want to connect to the " + "app master GUI of {} owned by {}", remoteUser, appId, runningUser); warnUserPage(resp, ProxyUriUtils.getPathAndQuery(id, rest, req.getQueryString(), true), runningUser, id); + return; } @@ -459,6 +454,127 @@ private void methodAction(final HttpServletRequest req, } /** + * 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 path, String id, HttpServletRequest req, + HttpServletResponse resp) throws IOException { + // If this isn't a redirect, we don't care. + boolean handle = path.endsWith("/redirect"); + + if (handle) { + // If this is a redirect, check if we're calling ourselves. + try { + handle = 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 bettor next + // time. + handle = false; + } + } + + // 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 (handle) { + 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); + ProxyUtils.sendRedirect(req, resp, + StringHelper.pjoin(rmAppPageUrlBase, id + "R")); + } + + return handle; + } + + /** * This method is used by Java object deserialization, to fill in the * transient {@link #trackingUriPlugins} field. * See {@link ObjectInputStream#defaultReadObject()} diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/amfilter/AmIpFilter.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/amfilter/AmIpFilter.java index e7617f0..e7da3e7 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/amfilter/AmIpFilter.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/main/java/org/apache/hadoop/yarn/server/webproxy/amfilter/AmIpFilter.java @@ -136,7 +136,10 @@ public void doFilter(ServletRequest req, ServletResponse resp, } if (!getProxyAddresses().contains(httpReq.getRemoteAddr())) { String redirectUrl = findRedirectUrl(); - String target = redirectUrl + httpReq.getRequestURI(); + + // Add /redirect to the end so that the RM web proxy knows that this + // request was a redirect. + String target = redirectUrl + httpReq.getRequestURI() + "/redirect"; ProxyUtils.sendRedirect(httpReq, httpResp, target); return; } diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/amfilter/TestAmFilter.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/amfilter/TestAmFilter.java index 4601c20..7327254 100644 --- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/amfilter/TestAmFilter.java +++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-web-proxy/src/test/java/org/apache/hadoop/yarn/server/webproxy/amfilter/TestAmFilter.java @@ -159,13 +159,13 @@ public void doFilter(ServletRequest servletRequest, // request with HttpServletRequest HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - Mockito.when(request.getRemoteAddr()).thenReturn("redirect"); - Mockito.when(request.getRequestURI()).thenReturn("/redirect"); + Mockito.when(request.getRemoteAddr()).thenReturn("nowhere"); + Mockito.when(request.getRequestURI()).thenReturn("/path"); testFilter.doFilter(request, response, chain); // address "redirect" is not in host list assertEquals(302, response.status); String redirect = response.getHeader(ProxyUtils.LOCATION); - assertEquals("http://bogus/redirect", redirect); + assertEquals("http://bogus/path/redirect", redirect); // "127.0.0.1" contains in host list. Without cookie Mockito.when(request.getRemoteAddr()).thenReturn("127.0.0.1"); testFilter.doFilter(request, response, chain);