hostNames) throws YarnException, IOException;
+ /**
+ *
+ * The interface used by client to get a shell to a container.
+ *
+ *
+ * @param containerId Container ID
+ * @param command Shell type
+ * @throws IOException if connection fails.
+ */
+ @Public
+ @Unstable
+ public abstract void shellToContainer(ContainerId containerId,
+ ShellContainerCommand command) throws IOException;
+
}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/api/impl/YarnClientImpl.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/api/impl/YarnClientImpl.java
index 227f7ed70a..3ec371c779 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/api/impl/YarnClientImpl.java
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/api/impl/YarnClientImpl.java
@@ -19,6 +19,7 @@
package org.apache.hadoop.yarn.client.api.impl;
import java.io.IOException;
+import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.EnumSet;
@@ -27,6 +28,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.Future;
import org.apache.hadoop.classification.InterfaceAudience.Private;
import org.apache.hadoop.classification.InterfaceStability.Unstable;
@@ -111,15 +113,18 @@
import org.apache.hadoop.yarn.api.records.QueueUserACLInfo;
import org.apache.hadoop.yarn.api.records.Resource;
import org.apache.hadoop.yarn.api.records.ResourceTypeInfo;
+import org.apache.hadoop.yarn.api.records.ShellContainerCommand;
import org.apache.hadoop.yarn.api.records.SignalContainerCommand;
import org.apache.hadoop.yarn.api.records.Token;
import org.apache.hadoop.yarn.api.records.YarnApplicationState;
import org.apache.hadoop.yarn.api.records.YarnClusterMetrics;
import org.apache.hadoop.yarn.client.ClientRMProxy;
import org.apache.hadoop.yarn.client.api.AHSClient;
+import org.apache.hadoop.yarn.client.api.ContainerShellWebSocket;
import org.apache.hadoop.yarn.client.api.TimelineClient;
import org.apache.hadoop.yarn.client.api.YarnClient;
import org.apache.hadoop.yarn.client.api.YarnClientApplication;
+import org.apache.hadoop.yarn.client.util.YarnClientUtils;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.apache.hadoop.yarn.exceptions.ApplicationIdNotProvidedException;
import org.apache.hadoop.yarn.exceptions.ApplicationNotFoundException;
@@ -132,6 +137,10 @@
import org.apache.hadoop.yarn.util.Records;
import org.apache.hadoop.yarn.util.resource.ResourceUtils;
import org.apache.hadoop.yarn.util.timeline.TimelineUtils;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.WebSocketException;
+import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
@@ -1074,4 +1083,53 @@ public Map> getNodeToAttributes(
GetNodesToAttributesRequest.newInstance(hostNames);
return rmClient.getNodesToAttributes(request).getNodeToAttributes();
}
+
+ @Override
+ public void shellToContainer(ContainerId containerId,
+ ShellContainerCommand command) throws IOException {
+ try {
+ GetContainerReportRequest request = Records
+ .newRecord(GetContainerReportRequest.class);
+ request.setContainerId(containerId);
+ GetContainerReportResponse response = rmClient
+ .getContainerReport(request);
+ URI nodeHttpAddress = new URI(response.getContainerReport()
+ .getNodeHttpAddress());
+ String host = nodeHttpAddress.getHost();
+ int port = nodeHttpAddress.getPort();
+ String scheme = nodeHttpAddress.getScheme();
+ String protocol = "ws://";
+ if (scheme.equals("https")) {
+ protocol = "wss://";
+ }
+ WebSocketClient client = new WebSocketClient();
+ URI uri = URI.create(protocol + host + ":" + port + "/container/" +
+ containerId);
+ try {
+ client.start();
+ // The socket that receives events
+ ContainerShellWebSocket socket = new ContainerShellWebSocket();
+ ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest();
+ if (UserGroupInformation.isSecurityEnabled()) {
+ String challenge = YarnClientUtils.generateToken(host);
+ upgradeRequest.setHeader("Authorization", "Negotiate " + challenge);
+ }
+ // Attempt Connect
+ Future fut = client.connect(socket, uri, upgradeRequest);
+ // Wait for Connect
+ Session session = fut.get();
+ // Send a message
+ session.getRemote().sendString("stty -echo");
+ session.getRemote().sendString("\r");
+ session.getRemote().flush();
+ socket.run();
+ } finally {
+ client.stop();
+ }
+ } catch (WebSocketException e) {
+ LOG.debug("Websocket exception: " + e.getMessage());
+ } catch (Throwable t) {
+ LOG.error("Fail to shell to container: " + t.getMessage());
+ }
+ }
}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/cli/ApplicationCLI.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/cli/ApplicationCLI.java
index 480ea231fb..2b3415413c 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/cli/ApplicationCLI.java
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/cli/ApplicationCLI.java
@@ -48,6 +48,7 @@
import org.apache.hadoop.yarn.api.records.ContainerId;
import org.apache.hadoop.yarn.api.records.ContainerReport;
import org.apache.hadoop.yarn.api.records.Priority;
+import org.apache.hadoop.yarn.api.records.ShellContainerCommand;
import org.apache.hadoop.yarn.api.records.SignalContainerCommand;
import org.apache.hadoop.yarn.api.records.YarnApplicationState;
import org.apache.hadoop.yarn.client.api.AppAdminClient;
@@ -111,6 +112,7 @@ public class ApplicationCLI extends YarnCLI {
public static final String COMPONENTS = "components";
public static final String VERSION = "version";
public static final String STATES = "states";
+ public static final String SHELL_CMD = "shell";
private static String firstArg = null;
@@ -311,6 +313,8 @@ public int run(String[] args) throws Exception {
opts.getOption(LIST_CMD).setArgName("Application ID");
opts.getOption(FAIL_CMD).setArgName("Application Attempt ID");
} else if (title != null && title.equalsIgnoreCase(CONTAINER)) {
+ opts.addOption(SHELL_CMD, true,
+ "Run a shell in the container.");
opts.addOption(STATUS_CMD, true,
"Prints the status of the container.");
opts.addOption(LIST_CMD, true,
@@ -323,6 +327,7 @@ public int run(String[] args) throws Exception {
"app version, -components to filter instances based on component " +
"names, -states to filter instances based on instance state.");
opts.addOption(HELP_CMD, false, "Displays help for all commands.");
+ opts.getOption(SHELL_CMD).setArgName("Container ID");
opts.getOption(STATUS_CMD).setArgName("Container ID");
opts.getOption(LIST_CMD).setArgName("Application Name or Attempt ID");
opts.addOption(APP_TYPE_CMD, true, "Works with -list to " +
@@ -552,6 +557,19 @@ public int run(String[] args) throws Exception {
command = SignalContainerCommand.valueOf(signalArgs[1]);
}
signalToContainer(containerId, command);
+ } else if (cliParser.hasOption(SHELL_CMD)) {
+ if (hasAnyOtherCLIOptions(cliParser, opts, SHELL_CMD)) {
+ printUsage(title, opts);
+ return exitCode;
+ }
+ final String[] shellArgs = cliParser.getOptionValues(SHELL_CMD);
+ final String containerId = shellArgs[0];
+ ShellContainerCommand command =
+ ShellContainerCommand.BASH;
+ if (shellArgs.length == 2) {
+ command = ShellContainerCommand.valueOf(shellArgs[1]);
+ }
+ shellToContainer(containerId, command);
} else if (cliParser.hasOption(LAUNCH_CMD)) {
if (hasAnyOtherCLIOptions(cliParser, opts, LAUNCH_CMD, APP_TYPE_CMD,
UPDATE_LIFETIME, CHANGE_APPLICATION_QUEUE)) {
@@ -806,7 +824,7 @@ private void updateApplicationTimeout(String applicationId,
}
/**
- * Signals the containerId
+ * Signals the containerId.
*
* @param containerIdStr the container id
* @param command the signal command
@@ -819,6 +837,20 @@ private void signalToContainer(String containerIdStr,
client.signalToContainer(containerId, command);
}
+ /**
+ * Shell to the containerId.
+ *
+ * @param containerIdStr the container id
+ * @param command the shell command
+ * @throws YarnException
+ */
+ private void shellToContainer(String containerIdStr,
+ ShellContainerCommand command) throws YarnException, IOException {
+ ContainerId containerId = ContainerId.fromString(containerIdStr);
+ sysout.println("Shelling to container " + containerIdStr);
+ client.shellToContainer(containerId, command);
+ }
+
/**
* It prints the usage of the command
*
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/util/YarnClientUtils.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/util/YarnClientUtils.java
index 17176752f2..abed6c6a30 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/util/YarnClientUtils.java
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/main/java/org/apache/hadoop/yarn/client/util/YarnClientUtils.java
@@ -19,15 +19,29 @@
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.List;
import com.google.common.collect.ImmutableSet;
+
+import org.apache.commons.codec.binary.Base64;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.SecurityUtil;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.security.authentication.client.AuthenticationException;
+import org.apache.hadoop.security.authentication.util.KerberosUtil;
import org.apache.hadoop.yarn.api.records.NodeLabel;
import org.apache.hadoop.yarn.conf.HAUtil;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* This class is a container for utility methods that are useful when creating
@@ -35,6 +49,9 @@
*/
public abstract class YarnClientUtils {
+ private static final Logger LOG =
+ LoggerFactory.getLogger(YarnClientUtils.class);
+ private static final Base64 BASE_64_CODEC = new Base64(0);
private static final String ADD_LABEL_FORMAT_ERR_MSG =
"Input format for adding node-labels is not correct, it should be "
+ "labelName1[(exclusive=true/false)],LabelName2[] ..";
@@ -187,4 +204,54 @@ static YarnConfiguration getYarnConfWithRmHaId(Configuration conf)
return yarnConf;
}
+
+ /**
+ * Generate SPNEGO challenge request token.
+ *
+ * @param server - hostname to contact
+ * @throws IOException thrown if doAs failed
+ * @throws InterruptedException thrown if doAs is interrupted
+ * @return SPNEGO token challenge
+ */
+ public static String generateToken(String server) throws IOException,
+ InterruptedException {
+ UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
+ LOG.debug("The user credential is {}", currentUser);
+ String challenge = currentUser
+ .doAs(new PrivilegedExceptionAction() {
+ @Override
+ public String run() throws Exception {
+ try {
+ // This Oid for Kerberos GSS-API mechanism.
+ Oid mechOid = KerberosUtil.getOidInstance("GSS_KRB5_MECH_OID");
+ GSSManager manager = GSSManager.getInstance();
+ // GSS name for server
+ GSSName serverName = manager.createName("HTTP@" + server,
+ GSSName.NT_HOSTBASED_SERVICE);
+ // Create a GSSContext for authentication with the service.
+ // We're passing client credentials as null since we want them to
+ // be read from the Subject.
+ GSSContext gssContext = manager.createContext(
+ serverName.canonicalize(mechOid), mechOid, null,
+ GSSContext.DEFAULT_LIFETIME);
+ gssContext.requestMutualAuth(true);
+ gssContext.requestCredDeleg(true);
+ // Establish context
+ byte[] inToken = new byte[0];
+ byte[] outToken = gssContext.initSecContext(inToken, 0,
+ inToken.length);
+ gssContext.dispose();
+ // Base64 encoded and stringified token for server
+ LOG.debug("Got valid challenge for host {}", serverName);
+ return new String(BASE_64_CODEC.encode(outToken),
+ StandardCharsets.US_ASCII);
+ } catch (GSSException | IllegalAccessException
+ | NoSuchFieldException | ClassNotFoundException e) {
+ LOG.error("Error: {}", e);
+ throw new AuthenticationException(e);
+ }
+ }
+ });
+ return challenge;
+ }
}
diff --git a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/cli/TestYarnCLI.java b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/cli/TestYarnCLI.java
index 9b1e86378b..672e3d7e43 100644
--- a/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/cli/TestYarnCLI.java
+++ b/hadoop-yarn-project/hadoop-yarn/hadoop-yarn-client/src/test/java/org/apache/hadoop/yarn/client/cli/TestYarnCLI.java
@@ -2317,6 +2317,7 @@ private String createContainerCLIHelpMessage() throws IOException {
pw.println(" -components Works with -list to filter instances based on input comma-separated list of component names.");
pw.println(" -help Displays help for all commands.");
pw.println(" -list List containers for application attempt when application attempt ID is provided. When application name is provided, then it finds the instances of the application based on app's own implementation, and -appTypes option must be specified unless it is the default yarn-service type. With app name, it supports optional use of -version to filter instances based on app version, -components to filter instances based on component names, -states to filter instances based on instance state.");
+ pw.println(" -shell Run a shell in the container.");
pw.println(" -signal Signal the container.");
pw.println("The available signal commands are ");
pw.println(java.util.Arrays.asList(SignalContainerCommand.values()));