blob: 8ad01cad024a0a630c4bf69fe4670100fec7edc2 [file] [log] [blame]
/*
* 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.drill.yarn.appMaster.http;
import static org.apache.drill.exec.server.rest.auth.DrillUserPrincipal.ADMIN_ROLE;
import java.net.URI;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.lang3.StringUtils;
import org.apache.drill.yarn.appMaster.Dispatcher;
import org.apache.drill.yarn.core.DoYUtil;
import org.apache.drill.yarn.core.DrillOnYarnConfig;
import org.eclipse.jetty.security.authentication.FormAuthenticator;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.server.mvc.Viewable;
import org.glassfish.jersey.server.mvc.freemarker.FreemarkerMvcFeature;
/**
* The Drill AM web UI. The format is highly compact. We use javax.ws.rs to mark
* up a Pojo with page path, permissions and HTTP methods. The ADMIN_ROLE is
* reused from Drill's web UI.
* <p>
* In general, all pages require admin role, except for two: the login page and
* the redirect page which the YARN web UI follows to start the AM UI.
*/
public class WebUiPageTree extends PageTree {
/**
* Main DoY page that displays cluster status, and the status of
* the resource groups. Available only to the admin user when
* DoY is secured.
*/
@Path("/")
@RolesAllowed(ADMIN_ROLE)
public static class RootPage {
@Inject
SecurityContext sc;
@GET
@Produces(MediaType.TEXT_HTML)
public Viewable getRoot() {
ControllerModel model = new ControllerModel();
dispatcher.getController().visit(model);
model.countStrayDrillbits(dispatcher.getController());
return new Viewable("/drill-am/index.ftl", toModel(sc, model));
}
}
/**
* Pages, adapted from Drill, that display the login and logout pages.
* Login uses the security mechanism, again borrowed from Drill, to
* validate the user against either the simple user/password
* configured in DoY, or the user who launched DoY using the
* Drill security mechanism.
*/
@Path("/")
@PermitAll
public static class LogInLogOutPages {
@Inject
SecurityContext sc;
public static final String REDIRECT_QUERY_PARM = "redirect";
public static final String LOGIN_RESOURCE = "login";
@GET
@Path("/login")
@Produces(MediaType.TEXT_HTML)
public Viewable getLoginPage(@Context HttpServletRequest request,
@Context HttpServletResponse response, @Context SecurityContext sc,
@Context UriInfo uriInfo,
@QueryParam(REDIRECT_QUERY_PARM) String redirect) throws Exception {
if (!StringUtils.isEmpty(redirect)) {
// If the URL has redirect in it, set the redirect URI in session, so
// that after the login is successful, request
// is forwarded to the redirect page.
final HttpSession session = request.getSession(true);
final URI destURI = UriBuilder
.fromUri(URLDecoder.decode(redirect, "UTF-8")).build();
session.setAttribute(FormAuthenticator.__J_URI, destURI.getPath());
}
return new Viewable("/drill-am/login.ftl", toModel(sc, (Object) null));
}
// Request type is POST because POST request which contains the login
// credentials are invalid and the request is
// dispatched here directly.
@POST
@Path("/login")
@Produces(MediaType.TEXT_HTML)
public Viewable getLoginPageAfterValidationError() {
return new Viewable("/drill-am/login.ftl",
toModel(sc, "Invalid user name or password."));
}
@GET
@Path("/logout")
public Viewable logout(@Context HttpServletRequest req,
@Context HttpServletResponse resp) throws Exception {
final HttpSession session = req.getSession();
if (session != null) {
session.invalidate();
}
req.getRequestDispatcher("/login").forward(req, resp);
return null;
}
}
/**
* DoY provides a link to YARN to display the AM UI. YARN wants to display the
* linked page in a frame, which does not play well with the DoY UI. To avoid
* this, we give YARN a link to this redirect page which does nothing other
* than to redirect the browser to the (full) DoY main UI.
*/
@Path("/redirect")
@PermitAll
public static class RedirectPage {
@GET
@Produces(MediaType.TEXT_HTML)
public Viewable getRoot() {
Map<String, String> map = new HashMap<>();
String baseUrl = DoYUtil.unwrapAmUrl(dispatcher.getTrackingUrl());
map.put("amLink", baseUrl);
map.put("clusterName", config.getString(DrillOnYarnConfig.APP_NAME));
return new Viewable("/drill-am/redirect.ftl", map);
}
}
/**
* Display the configuration page which displays the contents of
* DoY and selected Drill config as name/value pairs. Visible only
* to the admin when DoY is secure.
*/
@Path("/config")
@RolesAllowed(ADMIN_ROLE)
public static class ConfigPage {
@Inject
private SecurityContext sc;
@GET
@Produces(MediaType.TEXT_HTML)
public Viewable getRoot() {
return new Viewable("/drill-am/config.ftl",
toModel(sc, DrillOnYarnConfig.instance().getPairs()));
}
}
/**
* Displays the list of Drillbits showing details for each Drillbit.
* (DoY uses the generic term "task", but, at present, the only
* task that DoY runs is a Drillbit.
*/
@Path("/drillbits")
@RolesAllowed(ADMIN_ROLE)
public static class DrillbitsPage {
@Inject
private SecurityContext sc;
@GET
@Produces(MediaType.TEXT_HTML)
public Viewable getRoot() {
AbstractTasksModel.TasksModel model = new AbstractTasksModel.TasksModel();
dispatcher.getController().visitTasks(model);
model.listAnomalies(dispatcher.getController());
model.sortTasks();
// Done this funky way because FreeMarker only understands lists if they
// are members of a hash (grumble, grumble...)
Map<String, Object> map = new HashMap<>();
map.put("model", model);
map.put("tasks", model.getTasks());
if (model.hasUnmanagedDrillbits()) {
map.put("strays", model.getUnnamaged());
}
if (model.hasBlacklist()) {
map.put("blacklist", model.getBlacklist());
}
map.put("showDisks", dispatcher.getController().supportsDiskResource());
map.put("refreshSecs", DrillOnYarnConfig.config()
.getInt(DrillOnYarnConfig.HTTP_REFRESH_SECS));
return new Viewable("/drill-am/tasks.ftl", toMapModel(sc, map));
}
}
/**
* Displays a warning page to ask the user if they want to cancel
* a Drillbit. This is a bit old-school; we display this as a
* separate page. A good future enhancement is to do this as
* a pop-up in Javascript. The GET request display the confirmation
* page, the PUT request confirms cancellation and does the deed.
* The task to be cancelled appears as a query parameter:
* <pre>.../cancel?id=&lt;task id></pre>
*/
@Path("/cancel/")
@RolesAllowed(ADMIN_ROLE)
public static class CancelDrillbitPage {
@Inject
private SecurityContext sc;
@Inject
private HttpServletRequest request;
@QueryParam("id")
private int id;
@GET
@Produces(MediaType.TEXT_HTML)
public Viewable getPage() {
ConfirmShrink confirm;
if (dispatcher.getController().isTaskLive(id)) {
confirm = new ConfirmShrink(ConfirmShrink.Mode.KILL);
} else {
confirm = new ConfirmShrink(ConfirmShrink.Mode.CANCEL);
}
confirm.id = id;
return new Viewable("/drill-am/shrink-warning.ftl", toModel(sc, confirm, request));
}
@POST
@Produces(MediaType.TEXT_HTML)
public Viewable postPage() {
Acknowledge ack;
if (dispatcher.getController().cancelTask(id)) {
ack = new Acknowledge(Acknowledge.Mode.CANCELLED);
} else {
ack = new Acknowledge(Acknowledge.Mode.INVALID_TASK);
}
ack.value = id;
return new Viewable("/drill-am/confirm.ftl", toModel(sc, ack));
}
}
/**
* Displays a history of completed tasks which indicates failed or cancelled
* Drillbits. Helps the admin to understand what has been happening on the
* cluster if Drillbits have died.
*/
@Path("/history")
@RolesAllowed(ADMIN_ROLE)
public static class HistoryPage {
@Inject
SecurityContext sc;
@GET
@Produces(MediaType.TEXT_HTML)
public Viewable getRoot() {
AbstractTasksModel.HistoryModel model = new AbstractTasksModel.HistoryModel();
dispatcher.getController().visit(model);
Map<String, Object> map = new HashMap<>();
map.put("model", model.results);
map.put("refreshSecs", DrillOnYarnConfig.config()
.getInt(DrillOnYarnConfig.HTTP_REFRESH_SECS));
return new Viewable("/drill-am/history.ftl", toMapModel(sc, map));
}
}
/**
* Page that lets the admin change the cluster size or shut down the cluster.
*/
@Path("/manage")
@RolesAllowed(ADMIN_ROLE)
public static class ManagePage {
@Inject
SecurityContext sc;
@Inject
HttpServletRequest request;
@GET
@Produces(MediaType.TEXT_HTML)
public Viewable getRoot() {
ControllerModel model = new ControllerModel();
dispatcher.getController().visit(model);
return new Viewable("/drill-am/manage.ftl", toModel(sc, model, request));
}
}
/**
* Passes information to the acknowledgement page.
*/
public static class Acknowledge {
public enum Mode {
STOPPED, INVALID_RESIZE, INVALID_ACTION, NULL_RESIZE, RESIZED, CANCELLED, INVALID_TASK
};
Mode mode;
Object value;
public Acknowledge(Mode mode) {
this.mode = mode;
}
public String getType() {
return mode.toString();
}
public Object getValue() {
return value;
}
}
/**
* Passes information to the confirmation page.
*/
public static class ConfirmShrink {
public enum Mode {
SHRINK, STOP, CANCEL, KILL
};
Mode mode;
int value;
int id;
public ConfirmShrink(Mode mode) {
this.mode = mode;
}
public boolean isStop() {
return mode == Mode.STOP;
}
public boolean isCancel() {
return mode == Mode.CANCEL;
}
public boolean isKill() {
return mode == Mode.KILL;
}
public boolean isShrink() {
return mode == Mode.SHRINK;
}
public int getCount() {
return value;
}
public int getId() {
return id;
}
}
/**
* Confirm that the user wants to resize the cluster. Displays a warning if
* the user wants to shrink the cluster, since, at present, doing so will
* kill any in-flight queries. The GET request display the warning,
* the POST request confirms the action. The action itself is provided
* as query parameters:
* <pre>.../resize?type=&lt;type>&n=&lt;quantity></pre>
* Where the type is one of "resize", "grow", "shrink" or
* "force-shrink" and n is the associated quantity.
* <p>
* Note that the manage page only provides the "resize" option; the
* grow and shrink options were removed from the Web UI and are only
* visible through the REST API.
*/
@Path("/resize")
@RolesAllowed(ADMIN_ROLE)
public static class ResizePage {
@Inject
SecurityContext sc;
@Inject
HttpServletRequest request;
@FormParam("n")
int n;
@FormParam("type")
String type;
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.TEXT_HTML)
public Viewable resize() {
int curSize = dispatcher.getController().getTargetCount();
if (n <= 0) {
Acknowledge confirm = new Acknowledge(Acknowledge.Mode.INVALID_RESIZE);
confirm.value = n;
return new Viewable("/drill-am/confirm.ftl", toModel(sc, confirm));
}
if (type == null) {
type = "null";
}
int newSize;
boolean confirmed = false;
if (type.equalsIgnoreCase("resize")) {
newSize = n;
} else if (type.equalsIgnoreCase("grow")) {
newSize = curSize + n;
} else if (type.equalsIgnoreCase("shrink")) {
newSize = curSize - n;
} else if (type.equalsIgnoreCase("force-shrink")) {
newSize = curSize - n;
confirmed = true;
} else {
Acknowledge confirm = new Acknowledge(Acknowledge.Mode.INVALID_ACTION);
confirm.value = type;
return new Viewable("/drill-am/confirm.ftl", toModel(sc, confirm));
}
if (curSize == newSize) {
Acknowledge confirm = new Acknowledge(Acknowledge.Mode.NULL_RESIZE);
confirm.value = newSize;
return new Viewable("/drill-am/confirm.ftl", toModel(sc, confirm));
} else if (confirmed || curSize < newSize) {
Acknowledge confirm = new Acknowledge(Acknowledge.Mode.RESIZED);
confirm.value = dispatcher.getController().resizeTo(newSize);
return new Viewable("/drill-am/confirm.ftl", toModel(sc, confirm));
} else {
ConfirmShrink confirm = new ConfirmShrink(ConfirmShrink.Mode.SHRINK);
confirm.value = curSize - newSize;
return new Viewable("/drill-am/shrink-warning.ftl",
toModel(sc, confirm, request));
}
}
}
/**
* Confirmation page when the admin asks to stop the cluster.
* The GET request displays the confirmation, the POST does
* the deed. As for other confirmation pages, this is an old-style,
* quick & dirty solution. A more modern solution would be to use JavaScript
* to pop up a confirmation dialog.
*/
@Path("/stop/")
@RolesAllowed(ADMIN_ROLE)
public static class StopPage {
@Inject
SecurityContext sc;
@Inject
HttpServletRequest request;
@GET
@Produces(MediaType.TEXT_HTML)
public Viewable requestStop() {
ConfirmShrink confirm = new ConfirmShrink(ConfirmShrink.Mode.STOP);
return new Viewable("/drill-am/shrink-warning.ftl", toModel(sc, confirm, request));
}
@POST
@Produces(MediaType.TEXT_HTML)
public Viewable doStop() {
dispatcher.getController().shutDown();
Acknowledge confirm = new Acknowledge(Acknowledge.Mode.STOPPED);
return new Viewable("/drill-am/confirm.ftl", toModel(sc, confirm));
}
}
/**
* Build the pages for the Web UI using Freemarker to implement the
* MVC mechanism. This class builds on a rather complex mechanism; understand
* that to understand what the lines of code below are doing.
*
* @param dispatcher the DoY AM dispatcher that receives requests for
* information about, or requests to change the state of, the Drill clutser
*/
public WebUiPageTree(Dispatcher dispatcher) {
super(dispatcher);
// Markup engine
register(FreemarkerMvcFeature.class);
// Web UI Pages
register(RootPage.class);
register(RedirectPage.class);
register(ConfigPage.class);
register(DrillbitsPage.class);
register(CancelDrillbitPage.class);
register(HistoryPage.class);
register(ManagePage.class);
register(ResizePage.class);
register(StopPage.class);
// Authorization
// See: https://jersey.java.net/documentation/latest/security.html
if (AMSecurityManagerImpl.isEnabled()) {
register(LogInLogOutPages.class);
register(AuthDynamicFeature.class);
register(RolesAllowedDynamicFeature.class);
}
}
}