| /** |
| * 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.nodemanager; |
| |
| import static junit.framework.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.when; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.util.HashMap; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.hadoop.conf.Configuration; |
| import org.apache.hadoop.fs.FileContext; |
| import org.apache.hadoop.fs.FileStatus; |
| import org.apache.hadoop.fs.Path; |
| import org.apache.hadoop.fs.permission.FsPermission; |
| import org.apache.hadoop.yarn.api.records.ContainerId; |
| import org.apache.hadoop.yarn.api.records.ContainerLaunchContext; |
| import org.apache.hadoop.yarn.conf.YarnConfiguration; |
| import org.apache.hadoop.yarn.server.nodemanager.ContainerExecutor.Signal; |
| import org.apache.hadoop.yarn.server.nodemanager.containermanager.container.Container; |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| /** |
| * This is intended to test the LinuxContainerExecutor code, but because of |
| * some security restrictions this can only be done with some special setup |
| * first. |
| * <br><ol> |
| * <li>Compile the code with container-executor.conf.dir set to the location you |
| * want for testing. |
| * <br><pre><code> |
| * > mvn clean install -Pnative -Dcontainer-executor.conf.dir=/etc/hadoop |
| * -DskipTests |
| * </code></pre> |
| * |
| * <li>Set up <code>${container-executor.conf.dir}/container-executor.cfg</code> |
| * container-executor.cfg needs to be owned by root and have in it the proper |
| * config values. |
| * <br><pre><code> |
| * > cat /etc/hadoop/container-executor.cfg |
| * yarn.nodemanager.linux-container-executor.group=mapred |
| * #depending on the user id of the application.submitter option |
| * min.user.id=1 |
| * > sudo chown root:root /etc/hadoop/container-executor.cfg |
| * > sudo chmod 444 /etc/hadoop/container-executor.cfg |
| * </code></pre> |
| * |
| * <li>Move the binary and set proper permissions on it. It needs to be owned |
| * by root, the group needs to be the group configured in container-executor.cfg, |
| * and it needs the setuid bit set. (The build will also overwrite it so you |
| * need to move it to a place that you can support it. |
| * <br><pre><code> |
| * > cp ./hadoop-yarn-project/hadoop-yarn/hadoop-yarn-server/hadoop-yarn-server-nodemanager/src/main/c/container-executor/container-executor /tmp/ |
| * > sudo chown root:mapred /tmp/container-executor |
| * > sudo chmod 4550 /tmp/container-executor |
| * </code></pre> |
| * |
| * <li>Run the tests with the execution enabled (The user you run the tests as |
| * needs to be part of the group from the config. |
| * <br><pre><code> |
| * mvn test -Dtest=TestLinuxContainerExecutor -Dapplication.submitter=nobody -Dcontainer-executor.path=/tmp/container-executor |
| * </code></pre> |
| * </ol> |
| */ |
| public class TestLinuxContainerExecutor { |
| private static final Log LOG = LogFactory |
| .getLog(TestLinuxContainerExecutor.class); |
| |
| private static File workSpace = new File("target", |
| TestLinuxContainerExecutor.class.getName() + "-workSpace"); |
| |
| private LinuxContainerExecutor exec = null; |
| private String appSubmitter = null; |
| private LocalDirsHandlerService dirsHandler; |
| |
| @Before |
| public void setup() throws Exception { |
| FileContext files = FileContext.getLocalFSFileContext(); |
| Path workSpacePath = new Path(workSpace.getAbsolutePath()); |
| files.mkdir(workSpacePath, null, true); |
| workSpace.setReadable(true, false); |
| workSpace.setExecutable(true, false); |
| workSpace.setWritable(true, false); |
| File localDir = new File(workSpace.getAbsoluteFile(), "localDir"); |
| files.mkdir(new Path(localDir.getAbsolutePath()), |
| new FsPermission("777"), false); |
| File logDir = new File(workSpace.getAbsoluteFile(), "logDir"); |
| files.mkdir(new Path(logDir.getAbsolutePath()), |
| new FsPermission("777"), false); |
| String exec_path = System.getProperty("container-executor.path"); |
| if(exec_path != null && !exec_path.isEmpty()) { |
| Configuration conf = new Configuration(false); |
| LOG.info("Setting "+YarnConfiguration.NM_LINUX_CONTAINER_EXECUTOR_PATH |
| +"="+exec_path); |
| conf.set(YarnConfiguration.NM_LINUX_CONTAINER_EXECUTOR_PATH, exec_path); |
| exec = new LinuxContainerExecutor(); |
| exec.setConf(conf); |
| conf.set(YarnConfiguration.NM_LOCAL_DIRS, localDir.getAbsolutePath()); |
| conf.set(YarnConfiguration.NM_LOG_DIRS, logDir.getAbsolutePath()); |
| dirsHandler = new LocalDirsHandlerService(); |
| dirsHandler.init(conf); |
| } |
| appSubmitter = System.getProperty("application.submitter"); |
| if(appSubmitter == null || appSubmitter.isEmpty()) { |
| appSubmitter = "nobody"; |
| } |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| FileContext.getLocalFSFileContext().delete( |
| new Path(workSpace.getAbsolutePath()), true); |
| } |
| |
| private boolean shouldRun() { |
| if(exec == null) { |
| LOG.warn("Not running test because container-executor.path is not set"); |
| return false; |
| } |
| return true; |
| } |
| |
| private String writeScriptFile(String ... cmd) throws IOException { |
| File f = File.createTempFile("TestLinuxContainerExecutor", ".sh"); |
| f.deleteOnExit(); |
| PrintWriter p = new PrintWriter(new FileOutputStream(f)); |
| p.println("#!/bin/sh"); |
| p.print("exec"); |
| for(String part: cmd) { |
| p.print(" '"); |
| p.print(part.replace("\\", "\\\\").replace("'", "\\'")); |
| p.print("'"); |
| } |
| p.println(); |
| p.close(); |
| return f.getAbsolutePath(); |
| } |
| |
| private int id = 0; |
| private synchronized int getNextId() { |
| id += 1; |
| return id; |
| } |
| |
| private ContainerId getNextContainerId() { |
| ContainerId cId = mock(ContainerId.class); |
| String id = "CONTAINER_"+getNextId(); |
| when(cId.toString()).thenReturn(id); |
| return cId; |
| } |
| |
| |
| private int runAndBlock(String ... cmd) throws IOException { |
| return runAndBlock(getNextContainerId(), cmd); |
| } |
| |
| private int runAndBlock(ContainerId cId, String ... cmd) throws IOException { |
| String appId = "APP_"+getNextId(); |
| Container container = mock(Container.class); |
| ContainerLaunchContext context = mock(ContainerLaunchContext.class); |
| HashMap<String, String> env = new HashMap<String,String>(); |
| |
| when(container.getContainerID()).thenReturn(cId); |
| when(container.getLaunchContext()).thenReturn(context); |
| |
| when(context.getEnvironment()).thenReturn(env); |
| |
| String script = writeScriptFile(cmd); |
| |
| Path scriptPath = new Path(script); |
| Path tokensPath = new Path("/dev/null"); |
| Path workDir = new Path(workSpace.getAbsolutePath()); |
| Path pidFile = new Path(workDir, "pid.txt"); |
| |
| exec.activateContainer(cId, pidFile); |
| return exec.launchContainer(container, scriptPath, tokensPath, |
| appSubmitter, appId, workDir, dirsHandler.getLocalDirs(), |
| dirsHandler.getLogDirs()); |
| } |
| |
| |
| @Test |
| public void testContainerLaunch() throws IOException { |
| if (!shouldRun()) { |
| return; |
| } |
| |
| File touchFile = new File(workSpace, "touch-file"); |
| int ret = runAndBlock("touch", touchFile.getAbsolutePath()); |
| |
| assertEquals(0, ret); |
| FileStatus fileStatus = FileContext.getLocalFSFileContext().getFileStatus( |
| new Path(touchFile.getAbsolutePath())); |
| assertEquals(appSubmitter, fileStatus.getOwner()); |
| } |
| |
| @Test |
| public void testContainerKill() throws Exception { |
| if (!shouldRun()) { |
| return; |
| } |
| |
| final ContainerId sleepId = getNextContainerId(); |
| Thread t = new Thread() { |
| public void run() { |
| try { |
| runAndBlock(sleepId, "sleep", "100"); |
| } catch (IOException e) { |
| LOG.warn("Caught exception while running sleep",e); |
| } |
| }; |
| }; |
| t.setDaemon(true); //If it does not exit we shouldn't block the test. |
| t.start(); |
| |
| assertTrue(t.isAlive()); |
| |
| String pid = null; |
| int count = 10; |
| while ((pid = exec.getProcessId(sleepId)) == null && count > 0) { |
| LOG.info("Sleeping for 200 ms before checking for pid "); |
| Thread.sleep(200); |
| count--; |
| } |
| assertNotNull(pid); |
| |
| LOG.info("Going to killing the process."); |
| exec.signalContainer(appSubmitter, pid, Signal.TERM); |
| LOG.info("sleeping for 100ms to let the sleep be killed"); |
| Thread.sleep(100); |
| |
| assertFalse(t.isAlive()); |
| } |
| } |