/** * 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.mapred; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.PrivilegedExceptionAction; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.jsp.JspWriter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.http.HtmlQuoting; import org.apache.hadoop.mapred.JobHistory.JobInfo; import org.apache.hadoop.mapred.JobHistory.Keys; import org.apache.hadoop.mapred.JobTracker.RetireJobInfo; import org.apache.hadoop.mapreduce.JobACL; import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.authorize.AccessControlList; import org.apache.hadoop.util.ServletUtil; import org.apache.hadoop.util.StringUtils; class JSPUtil { static final String PRIVATE_ACTIONS_KEY = "webinterface.private.actions"; //LRU based cache private static final Map<String, JobInfo> jobHistoryCache = new LinkedHashMap<String, JobInfo>(); private static final Log LOG = LogFactory.getLog(JSPUtil.class); /** * Wraps the {@link JobInProgress} object and contains boolean for * 'job view access' allowed or not. * This class is only for usage by JSPs and Servlets. */ static class JobWithViewAccessCheck { private JobInProgress job = null; // true if user is authorized to view this job private boolean isViewAllowed = true; JobWithViewAccessCheck(JobInProgress job) { this.job = job; } JobInProgress getJob() { return job; } boolean isViewJobAllowed() { return isViewAllowed; } void setViewAccess(boolean isViewAllowed) { this.isViewAllowed = isViewAllowed; } } /** * Validates if current user can view the job. * If user is not authorized to view the job, this method will modify the * response and forwards to an error page and returns Job with * viewJobAccess flag set to false. * @return JobWithViewAccessCheck object(contains JobInProgress object and * viewJobAccess flag). Callers of this method will check the flag * and decide if view should be allowed or not. Job will be null if * the job with given jobid doesnot exist at the JobTracker. */ public static JobWithViewAccessCheck checkAccessAndGetJob(final JobTracker jt, JobID jobid, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { final JobInProgress job = jt.getJob(jobid); JobWithViewAccessCheck myJob = new JobWithViewAccessCheck(job); if (!jt.areACLsEnabled() || job == null) { return myJob; } String user = request.getRemoteUser(); if (user == null) { JSPUtil.setErrorAndForward("Null user", request, response); myJob.setViewAccess(false); return myJob; } final UserGroupInformation ugi = UserGroupInformation.createRemoteUser(user); try { ugi.doAs(new PrivilegedExceptionAction<Void>() { public Void run() throws IOException, ServletException { // checks job view permission jt.getACLsManager().checkAccess(job, ugi, Operation.VIEW_JOB_DETAILS); return null; } }); } catch (AccessControlException e) { String errMsg = "User " + ugi.getShortUserName() + " failed to view " + jobid + "!<br><br>" + e.getMessage() + "<hr><a href=\"jobtracker.jsp\">Go back to JobTracker</a><br>"; JSPUtil.setErrorAndForward(errMsg, request, response); myJob.setViewAccess(false); } catch (InterruptedException e) { String errMsg = " Interrupted while trying to access " + jobid + "<hr><a href=\"jobtracker.jsp\">Go back to JobTracker</a><br>"; JSPUtil.setErrorAndForward(errMsg, request, response); myJob.setViewAccess(false); } return myJob; } /** * Sets error code SC_UNAUTHORIZED in response and forwards to * error page which contains error message and a back link. */ public static void setErrorAndForward(String errMsg, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setAttribute("error.msg", errMsg); RequestDispatcher dispatcher = request.getRequestDispatcher( "/job_authorization_error.jsp"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); dispatcher.forward(request, response); } /** * Method used to process the request from the job page based on the * request which it has received. For example like changing priority. * * @param request HTTP request Object. * @param response HTTP response object. * @param tracker {@link JobTracker} instance * @throws IOException * @throws InterruptedException * @throws ServletException */ public static void processButtons(HttpServletRequest request, HttpServletResponse response, final JobTracker tracker) throws IOException, InterruptedException, ServletException { String user = request.getRemoteUser(); if (privateActionsAllowed(tracker.conf) && request.getParameter("killJobs") != null) { String[] jobs = request.getParameterValues("jobCheckBox"); if (jobs != null) { boolean notAuthorized = false; String errMsg = "User " + user + " failed to kill the following job(s)!<br><br>"; for (String job : jobs) { final JobID jobId = JobID.forName(job); if (user != null) { UserGroupInformation ugi = UserGroupInformation.createRemoteUser(user); try { ugi.doAs(new PrivilegedExceptionAction<Void>() { public Void run() throws IOException{ tracker.killJob(jobId);// checks job modify permission return null; } }); } catch(AccessControlException e) { errMsg = errMsg.concat("<br>" + e.getMessage()); notAuthorized = true; // We don't return right away so that we can try killing other // jobs that are requested to be killed. continue; } } else {// no authorization needed tracker.killJob(jobId); } } if (notAuthorized) {// user is not authorized to kill some/all of jobs errMsg = errMsg.concat( "<br><hr><a href=\"jobtracker.jsp\">Go back to JobTracker</a><br>"); setErrorAndForward(errMsg, request, response); return; } } } if (privateActionsAllowed(tracker.conf) && request.getParameter("changeJobPriority") != null) { String[] jobs = request.getParameterValues("jobCheckBox"); if (jobs != null) { final JobPriority jobPri = JobPriority.valueOf(request .getParameter("setJobPriority")); boolean notAuthorized = false; String errMsg = "User " + user + " failed to set priority for the following job(s)!<br><br>"; for (String job : jobs) { final JobID jobId = JobID.forName(job); if (user != null) { UserGroupInformation ugi = UserGroupInformation. createRemoteUser(user); try { ugi.doAs(new PrivilegedExceptionAction<Void>() { public Void run() throws IOException{ // checks job modify permission tracker.setJobPriority(jobId, jobPri); return null; } }); } catch(AccessControlException e) { errMsg = errMsg.concat("<br>" + e.getMessage()); notAuthorized = true; // We don't return right away so that we can try operating on // other jobs. continue; } } else {// no authorization needed tracker.setJobPriority(jobId, jobPri); } } if (notAuthorized) {// user is not authorized to kill some/all of jobs errMsg = errMsg.concat( "<br><hr><a href=\"jobtracker.jsp\">Go back to JobTracker</a><br>"); setErrorAndForward(errMsg, request, response); return; } } } } /** * Method used to generate the Job table for Job pages. * * @param label display heading to be used in the job table. * @param jobs vector of jobs to be displayed in table. * @param refresh refresh interval to be used in jobdetails page. * @param rowId beginning row id to be used in the table. * @return * @throws IOException */ public static String generateJobTable(String label, Collection<JobInProgress> jobs , int refresh, int rowId, JobConf conf) throws IOException { boolean isModifiable = label.equals("Running") && privateActionsAllowed(conf); StringBuffer sb = new StringBuffer(); sb.append("<table border=\"1\" cellpadding=\"5\" cellspacing=\"0\" class=\"sortable\">\n"); if (jobs.size() > 0) { if (isModifiable) { sb.append("<form action=\"/jobtracker.jsp\" onsubmit=\"return confirmAction();\" method=\"POST\">"); sb.append("<tr>"); sb.append("<td><input type=\"Button\" onclick=\"selectAll()\" " + "value=\"Select All\" id=\"checkEm\"></td>"); sb.append("<td>"); sb.append("<input type=\"submit\" name=\"killJobs\" value=\"Kill Selected Jobs\">"); sb.append("</td"); sb.append("<td><nobr>"); sb.append("<select name=\"setJobPriority\">"); for (JobPriority prio : JobPriority.values()) { sb.append("<option" + (JobPriority.NORMAL == prio ? " selected=\"selected\">" : ">") + prio + "</option>"); } sb.append("</select>"); sb.append("<input type=\"submit\" name=\"changeJobPriority\" " + "value=\"Change\">"); sb.append("</nobr></td>"); sb.append("<td colspan=\"10\"> </td>"); sb.append("</tr>"); sb.append("<td> </td>"); } else { sb.append("<tr>"); } sb.append("<td><b>Jobid</b></td>"); sb.append("<td><b>Started</b></td>"); sb.append("<td><b>Priority</b></td>"); sb.append("<td><b>User</b></td>"); sb.append("<td><b>Name</b></td>"); sb.append("<td><b>Map % Complete</b></td>"); sb.append("<td><b>Map Total</b></td>"); sb.append("<td><b>Maps Completed</b></td>"); sb.append("<td><b>Reduce % Complete</b></td>"); sb.append("<td><b>Reduce Total</b></td>"); sb.append("<td><b>Reduces Completed</b></td>"); sb.append("<td><b>Job Scheduling Information</b></td>"); sb.append("<td><b>Diagnostic Info </b></td>"); sb.append("</tr>\n"); for (Iterator<JobInProgress> it = jobs.iterator(); it.hasNext(); ++rowId) { JobInProgress job = it.next(); Date time = new Date(job.getStartTime()); JobProfile profile = job.getProfile(); JobStatus status = job.getStatus(); JobID jobid = profile.getJobID(); int desiredMaps = job.desiredMaps(); int desiredReduces = job.desiredReduces(); int completedMaps = job.finishedMaps(); int completedReduces = job.finishedReduces(); String name = HtmlQuoting.quoteHtmlChars(profile.getJobName()); String jobpri = job.getPriority().toString(); String schedulingInfo = HtmlQuoting.quoteHtmlChars(job.getStatus().getSchedulingInfo()); String diagnosticInfo = HtmlQuoting.quoteHtmlChars(job.getStatus().getFailureInfo()); if (isModifiable) { sb.append("<tr><td><input TYPE=\"checkbox\" " + "onclick=\"checkButtonVerbage()\" " + "name=\"jobCheckBox\" value=" + jobid + "></td>"); } else { sb.append("<tr>"); } sb.append("<td id=\"job_" + rowId + "\"><a href=\"jobdetails.jsp?jobid=" + jobid + "&refresh=" + refresh + "\">" + jobid + "</a></td>" + "<td id=\"started_" + rowId + "\">" + time + "</td>" + "<td id=\"priority_" + rowId + "\">" + jobpri + "</td>" + "<td id=\"user_" + rowId + "\">" + HtmlQuoting.quoteHtmlChars(profile.getUser()) + "</td>" + "<td id=\"name_" + rowId + "\">" + ("".equals(name) ? " " : name) + "</td>" + "<td>" + StringUtils.formatPercent(status.mapProgress(), 2) + ServletUtil.percentageGraph(status.mapProgress() * 100, 80) + "</td><td>" + desiredMaps + "</td><td>" + completedMaps + "</td><td>" + StringUtils.formatPercent(status.reduceProgress(), 2) + ServletUtil.percentageGraph(status.reduceProgress() * 100, 80) + "</td><td>" + desiredReduces + "</td><td> " + completedReduces + "</td><td>" + schedulingInfo + "</td><td>" + diagnosticInfo + "</td></tr>\n"); } if (isModifiable) { sb.append("</form>\n"); } } else { sb.append("<tr><td align=\"center\" colspan=\"8\"><i>none</i>" + "</td></tr>\n"); } sb.append("</table>\n"); return sb.toString(); } @SuppressWarnings("unchecked") public static String generateRetiredJobTable(JobTracker tracker, int rowId) throws IOException { StringBuffer sb = new StringBuffer(); sb.append("<table border=\"1\" cellpadding=\"5\" cellspacing=\"0\">\n"); Iterator<RetireJobInfo> iterator = tracker.retireJobs.getAll().descendingIterator(); if (!iterator.hasNext()) { sb.append("<tr><td align=\"center\" colspan=\"8\"><i>none</i>" + "</td></tr>\n"); } else { sb.append("<tr>"); sb.append("<td><b>Jobid</b></td>"); sb.append("<td><b>Priority</b></td>"); sb.append("<td><b>User</b></td>"); sb.append("<td><b>Name</b></td>"); sb.append("<td><b>State</b></td>"); sb.append("<td><b>Start Time</b></td>"); sb.append("<td><b>Finish Time</b></td>"); sb.append("<td><b>Map % Complete</b></td>"); sb.append("<td><b>Reduce % Complete</b></td>"); sb.append("<td><b>Job Scheduling Information</b></td>"); sb.append("<td><b>Diagnostic Info </b></td>"); sb.append("</tr>\n"); for (int i = 0; i < 100 && iterator.hasNext(); i++) { RetireJobInfo info = iterator.next(); String historyFile = info.getHistoryFile(); String historyFileUrl = null; if (historyFile != null && !historyFile.equals("")) { try { historyFileUrl = URLEncoder.encode(info.getHistoryFile(), "UTF-8"); } catch (UnsupportedEncodingException e) { LOG.warn("Can't create history url ", e); } } sb.append("<tr>"); sb.append( "<td id=\"job_" + rowId + "\">" + (historyFileUrl == null ? "" : "<a href=\"" + JobHistoryServer.getHistoryUrlPrefix(tracker.conf) + "/jobdetailshistory.jsp?logFile=" + historyFileUrl + "\">") + info.status.getJobId() + "</a></td>" + "<td id=\"priority_" + rowId + "\">" + info.status.getJobPriority().toString() + "</td>" + "<td id=\"user_" + rowId + "\">" + HtmlQuoting.quoteHtmlChars(info.profile.getUser()) + "</td>" + "<td id=\"name_" + rowId + "\">" + HtmlQuoting.quoteHtmlChars(info.profile.getJobName()) + "</td>" + "<td>" + JobStatus.getJobRunState(info.status.getRunState()) + "</td>" + "<td>" + new Date(info.status.getStartTime()) + "</td>" + "<td>" + new Date(info.finishTime) + "</td>" + "<td>" + StringUtils.formatPercent(info.status.mapProgress(), 2) + ServletUtil.percentageGraph(info.status.mapProgress() * 100, 80) + "</td>" + "<td>" + StringUtils.formatPercent(info.status.reduceProgress(), 2) + ServletUtil.percentageGraph( info.status.reduceProgress() * 100, 80) + "</td>" + "<td>" + HtmlQuoting.quoteHtmlChars(info.status.getSchedulingInfo()) + "</td>" + "<td>" + HtmlQuoting.quoteHtmlChars(info.status.getFailureInfo()) + "</td></tr>\n"); rowId++; } } sb.append("</table>\n"); return sb.toString(); } static Path getJobConfFilePath(Path logFile) { return JobHistory.confPathFromLogFilePath(logFile); } /** * Read a job-history log file and construct the corresponding {@link JobInfo} * . Also cache the {@link JobInfo} for quick serving further requests. * * @param logFile * @param fs * @return JobInfo * @throws IOException */ static JobInfo getJobInfo(Path logFile, FileSystem fs, JobConf jobConf, ACLsManager acLsManager, String user) throws IOException { String jobid = getJobID(logFile.getName()); JobInfo jobInfo = null; synchronized(jobHistoryCache) { jobInfo = jobHistoryCache.remove(jobid); if (jobInfo == null) { jobInfo = new JobHistory.JobInfo(jobid); LOG.info("Loading Job History file "+jobid + ". Cache size is " + jobHistoryCache.size()); DefaultJobHistoryParser.parseJobTasks(logFile.toUri().getPath(), jobInfo, fs); } jobHistoryCache.put(jobid, jobInfo); int CACHE_SIZE = jobConf.getInt("mapred.job.tracker.jobhistory.lru.cache.size", 5); if (jobHistoryCache.size() > CACHE_SIZE) { Iterator<Map.Entry<String, JobInfo>> it = jobHistoryCache.entrySet().iterator(); String removeJobId = it.next().getKey(); it.remove(); LOG.info("Job History file removed form cache "+removeJobId); } } UserGroupInformation currentUser; if (user == null) { currentUser = UserGroupInformation.getCurrentUser(); } else { currentUser = UserGroupInformation.createRemoteUser(user); } // Authorize the user for view access of this job acLsManager.checkAccess(jobid, currentUser, jobInfo.getJobQueue(), Operation.VIEW_JOB_DETAILS, jobInfo.get(Keys.USER), jobInfo.getJobACLs().get(JobACL.VIEW_JOB)); return jobInfo; } /** * Check the access for users to view job-history pages. * * @param request * @param response * @param fs * @param logFile * @return the job if authorization is disabled or if the authorization checks * pass. Otherwise return null. * @throws IOException * @throws InterruptedException * @throws ServletException */ static JobInfo checkAccessAndGetJobInfo(HttpServletRequest request, HttpServletResponse response, final JobConf jobConf, final ACLsManager acLsManager, final FileSystem fs, final Path logFile) throws IOException, InterruptedException, ServletException { String jobid = getJobID(logFile.getName()); String user = request.getRemoteUser(); JobInfo job = null; if (user != null) { try { job = JSPUtil.getJobInfo(logFile, fs, jobConf, acLsManager, user); } catch (AccessControlException e) { String trackerAddress = jobConf.get("mapred.job.tracker.http.address"); String errMsg = String.format( "User %s failed to view %s!<br><br>%s" + "<hr>" + "<a href=\"jobhistory.jsp\">Go back to JobHistory</a><br>" + "<a href=\"http://" + trackerAddress + "/jobtracker.jsp\">Go back to JobTracker</a>", user, jobid, e.getMessage()); JSPUtil.setErrorAndForward(errMsg, request, response); return null; } } else { // no authorization needed job = JSPUtil.getJobInfo(logFile, fs, jobConf, acLsManager, null); } return job; } static String getJobID(String historyFileName) { return JobHistory.jobIdNameFromLogFileName(historyFileName); } static String getUserName(String historyFileName) { return JobHistory.userNameFromLogFileName(historyFileName); } static String getJobName(String historyFileName) { return JobHistory.jobNameFromLogFileName(historyFileName); } /** * Nicely print the Job-ACLs * @param tracker * @param jobAcls * @param out * @throws IOException */ static void printJobACLs(JobTracker tracker, Map<JobACL, AccessControlList> jobAcls, JspWriter out) throws IOException { if (tracker.areACLsEnabled()) { printJobACLsInternal(jobAcls, out); } else { out.print("<b>Job-ACLs: " + new AccessControlList("*").toString() + "</b><br>"); } } static void printJobACLs(JobConf conf, Map<JobACL, AccessControlList> jobAcls, JspWriter out) throws IOException { if (conf.getBoolean(JobConf.MR_ACLS_ENABLED, false)) { printJobACLsInternal(jobAcls, out); } else { out.print("<b>Job-ACLs: " + new AccessControlList("*").toString() + "</b><br>"); } } private static void printJobACLsInternal(Map<JobACL, AccessControlList> jobAcls, JspWriter out) throws IOException { // Display job-view-acls and job-modify-acls configured for this job out.print("<b>Job-ACLs:</b><br>"); for (JobACL aclName : JobACL.values()) { String aclConfigName = aclName.getAclName(); AccessControlList aclConfigured = jobAcls.get(aclName); if (aclConfigured != null) { String aclStr = aclConfigured.toString(); out.print("    " + aclConfigName + ": " + aclStr + "<br>"); } } } static boolean privateActionsAllowed(JobConf conf) { return conf.getBoolean(PRIVATE_ACTIONS_KEY, false); } }