/*
* 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.brooklyn.util.core.internal.ssh.sshj;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import net.schmizz.sshj.connection.channel.direct.Session;
import org.apache.brooklyn.core.BrooklynFeatureEnablement;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.core.internal.ssh.SshException;
import org.apache.brooklyn.util.core.internal.ssh.SshTool;
import org.apache.brooklyn.util.core.internal.ssh.SshToolAbstractIntegrationTest;
import org.apache.brooklyn.util.core.internal.ssh.sshj.SshjTool;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.exceptions.RuntimeTimeoutException;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.time.Duration;
import org.testng.annotations.Test;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
/**
* Test the operation of the {@link SshJschTool} utility class.
*/
public class SshjToolIntegrationTest extends SshToolAbstractIntegrationTest {
@Override
protected SshTool newUnregisteredTool(Map<String,?> flags) {
return new SshjTool(flags);
}
// TODO requires vt100 terminal emulation to work?
@Test(enabled = false, groups = {"Integration"})
public void testExecShellWithCommandTakingStdin() throws Exception {
// Uses `tee` to redirect stdin to the given file; cntr-d (i.e. char 4) stops tee with exit code 0
String content = "blah blah";
String out = execShellDirectWithTerminalEmulation("tee "+remoteFilePath, content, ""+(char)4, "echo file contents: `cat "+remoteFilePath+"`");
assertTrue(out.contains("file contents: blah blah"), "out="+out);
}
@Test(groups = {"Integration"})
public void testGivesUpAfterMaxRetries() throws Exception {
final AtomicInteger callCount = new AtomicInteger();
final SshTool localtool = new SshjTool(ImmutableMap.of("sshTries", 3, "host", "localhost", "privateKeyFile", "~/.ssh/id_rsa")) {
protected SshAction<Session> newSessionAction() {
callCount.incrementAndGet();
throw new RuntimeException("Simulating ssh execution failure");
}
};
tools.add(localtool);
try {
localtool.execScript(ImmutableMap.<String,Object>of(), ImmutableList.of("true"));
fail();
} catch (SshException e) {
if (!e.toString().contains("out of retries")) throw e;
assertEquals(callCount.get(), 3);
}
}
@Test(groups = {"Integration"})
public void testReturnsOnSuccessWhenRetrying() throws Exception {
final AtomicInteger callCount = new AtomicInteger();
final int successOnAttempt = 2;
final SshTool localtool = new SshjTool(ImmutableMap.of("sshTries", 3, "host", "localhost", "privateKeyFile", "~/.ssh/id_rsa")) {
protected SshAction<Session> newSessionAction() {
callCount.incrementAndGet();
if (callCount.incrementAndGet() >= successOnAttempt) {
return super.newSessionAction();
} else {
throw new RuntimeException("Simulating ssh execution failure");
}
}
};
tools.add(localtool);
localtool.execScript(ImmutableMap.<String,Object>of(), ImmutableList.of("true"));
assertEquals(callCount.get(), successOnAttempt);
}
@Test(groups = {"Integration"})
public void testGivesUpAfterMaxTime() throws Exception {
final AtomicInteger callCount = new AtomicInteger();
final SshTool localtool = new SshjTool(ImmutableMap.of("sshTriesTimeout", 1000, "host", "localhost", "privateKeyFile", "~/.ssh/id_rsa")) {
protected SshAction<Session> newSessionAction() {
callCount.incrementAndGet();
try {
Thread.sleep(600);
} catch (InterruptedException e) {
throw Exceptions.propagate(e);
}
throw new RuntimeException("Simulating ssh execution failure");
}
};
tools.add(localtool);
try {
localtool.execScript(ImmutableMap.<String,Object>of(), ImmutableList.of("true"));
fail();
} catch (RuntimeTimeoutException e) {
if (!e.toString().contains("out of time")) throw e;
assertEquals(callCount.get(), 2);
}
}
@Test(groups = {"Integration"})
public void testUsesCustomLocalTempDir() throws Exception {
class SshjToolForTest extends SshjTool {
public SshjToolForTest(Map<String, ?> map) {
super(map);
}
public File getLocalTempDir() {
return localTempDir;
}
};
final SshjToolForTest localtool = new SshjToolForTest(ImmutableMap.<String, Object>of("host", "localhost"));
assertNotNull(localtool.getLocalTempDir());
assertEquals(localtool.getLocalTempDir(), new File(Os.tidyPath(SshjTool.PROP_LOCAL_TEMP_DIR.getDefaultValue())));
String customTempDir = Os.tmp();
final SshjToolForTest localtool2 = new SshjToolForTest(ImmutableMap.of(
"host", "localhost",
SshjTool.PROP_LOCAL_TEMP_DIR.getName(), customTempDir));
assertEquals(localtool2.getLocalTempDir(), new File(customTempDir));
String customRelativeTempDir = "~/tmp";
final SshjToolForTest localtool3 = new SshjToolForTest(ImmutableMap.of(
"host", "localhost",
SshjTool.PROP_LOCAL_TEMP_DIR.getName(), customRelativeTempDir));
assertEquals(localtool3.getLocalTempDir(), new File(Os.tidyPath(customRelativeTempDir)));
}
@Test(groups = {"Integration"})
public void testAsyncExecStdoutAndStderr() throws Exception {
boolean origFeatureEnablement = BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC);
try {
// Include a sleep, to ensure that the contents retrieved in first poll and subsequent polls are appended
List<String> cmds = ImmutableList.of(
"echo mystringToStdout",
"echo mystringToStderr 1>&2",
"sleep 5",
"echo mystringPostSleepToStdout",
"echo mystringPostSleepToStderr 1>&2");
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
int exitCode = tool.execScript(
ImmutableMap.of(
"out", out,
"err", err,
SshjTool.PROP_EXEC_ASYNC.getName(), true,
SshjTool.PROP_NO_EXTRA_OUTPUT.getName(), true,
SshjTool.PROP_EXEC_ASYNC_POLLING_TIMEOUT.getName(), Duration.ONE_SECOND),
cmds,
ImmutableMap.<String,String>of());
String outStr = new String(out.toByteArray());
String errStr = new String(err.toByteArray());
assertEquals(exitCode, 0);
assertEquals(outStr.trim(), "mystringToStdout\nmystringPostSleepToStdout");
assertEquals(errStr.trim(), "mystringToStderr\nmystringPostSleepToStderr");
} finally {
BrooklynFeatureEnablement.setEnablement(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC, origFeatureEnablement);
}
}
@Test(groups = {"Integration"})
public void testAsyncExecReturnsExitCode() throws Exception {
boolean origFeatureEnablement = BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC);
try {
int exitCode = tool.execScript(
ImmutableMap.of(SshjTool.PROP_EXEC_ASYNC.getName(), true),
ImmutableList.of("exit 123"),
ImmutableMap.<String,String>of());
assertEquals(exitCode, 123);
} finally {
BrooklynFeatureEnablement.setEnablement(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC, origFeatureEnablement);
}
}
@Test(groups = {"Integration"})
public void testAsyncExecTimesOut() throws Exception {
Stopwatch stopwatch = Stopwatch.createStarted();
boolean origFeatureEnablement = BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC);
try {
tool.execScript(
ImmutableMap.of(SshjTool.PROP_EXEC_ASYNC.getName(), true, SshjTool.PROP_EXEC_TIMEOUT.getName(), Duration.millis(1)),
ImmutableList.of("sleep 60"),
ImmutableMap.<String,String>of());
fail();
} catch (Exception e) {
TimeoutException te = Exceptions.getFirstThrowableOfType(e, TimeoutException.class);
if (te == null) throw e;
} finally {
BrooklynFeatureEnablement.setEnablement(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC, origFeatureEnablement);
}
long seconds = stopwatch.elapsed(TimeUnit.SECONDS);
assertTrue(seconds < 30, "exec took "+seconds+" seconds");
}
@Test(groups = {"Integration"})
public void testAsyncExecAbortsIfProcessFails() throws Exception {
final AtomicReference<Throwable> error = new AtomicReference<Throwable>();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Stopwatch stopwatch = Stopwatch.createStarted();
int exitStatus = tool.execScript(
ImmutableMap.of(SshjTool.PROP_EXEC_ASYNC.getName(), true, SshjTool.PROP_EXEC_TIMEOUT.getName(), Duration.millis(1)),
ImmutableList.of("sleep 63"),
ImmutableMap.<String,String>of());
assertEquals(exitStatus, 143 /* 128 + Signal number (SIGTERM) */);
long seconds = stopwatch.elapsed(TimeUnit.SECONDS);
assertTrue(seconds < 30, "exec took "+seconds+" seconds");
} catch (Throwable t) {
error.set(t);
}
}});
boolean origFeatureEnablement = BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC);
try {
thread.start();
Asserts.succeedsEventually(new Runnable() {
@Override
public void run() {
int exitStatus = tool.execCommands(ImmutableMap.<String,Object>of(), ImmutableList.of("ps aux| grep \"sleep 63\" | grep -v grep"));
assertEquals(exitStatus, 0);
}});
tool.execCommands(ImmutableMap.<String,Object>of(), ImmutableList.of("ps aux| grep \"sleep 63\" | grep -v grep | awk '{print($2)}' | xargs kill"));
thread.join(30*1000);
assertFalse(thread.isAlive());
if (error.get() != null) {
throw Exceptions.propagate(error.get());
}
} finally {
thread.interrupt();
BrooklynFeatureEnablement.setEnablement(BrooklynFeatureEnablement.FEATURE_SSH_ASYNC_EXEC, origFeatureEnablement);
}
}
protected String execShellDirect(List<String> cmds) {
return execShellDirect(cmds, ImmutableMap.<String,Object>of());
}
protected String execShellDirect(List<String> cmds, Map<String,?> env) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int exitcode = ((SshjTool)tool).execShellDirect(ImmutableMap.of("out", out), cmds, env);
String outstr = new String(out.toByteArray());
assertEquals(exitcode, 0, outstr);
return outstr;
}
private String execShellDirectWithTerminalEmulation(String... cmds) {
return execShellDirectWithTerminalEmulation(Arrays.asList(cmds));
}
private String execShellDirectWithTerminalEmulation(List<String> cmds) {
return execShellDirectWithTerminalEmulation(cmds, ImmutableMap.<String,Object>of());
}
private String execShellDirectWithTerminalEmulation(List<String> cmds, Map<String,?> env) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
int exitcode = ((SshjTool)tool).execShellDirect(ImmutableMap.of("allocatePTY", true, "out", out), cmds, env);
String outstr = new String(out.toByteArray());
assertEquals(exitcode, 0, outstr);
return outstr;
}
}