/*
* 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;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Calendar;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
/**
* Test the operation of the {@link SshTool} utility class; to be extended to test concrete implementations.
* <p>
* The test <strong>assumes that</strong> you have configured keys for accessing localhost with the following keys:
* <ul>
* <li>passwordless ~/.ssh/id_rsa</li>
* <li>~/.ssh/id_rsa_with_passphrase with password mypassphrase </li>
* </ul>
*
* <br>
* Paths for the keys above can override the from Java System Properties.
* Here are the system properties you can override:
*
* <ul>
* <li>brooklyn.sshPrivateKeyWithPassphrase by default it is ~/.ssh/id_rsa_with_passphrase<br>
* <li>brooklyn.sshPrivateKeyPassphrase by default it is mypassphrase<br>
* <li>brooklyn.sshDefaultPrivateKeyFile by default it is ~/.ssh/id_rsa<br>
* </ul>
*
* Note that {@link #testSshKeyWithNoKeyDefaultsToIdrsa} requires a passwordless ~/.ssh/id_rsa
* </p>
*/
public abstract class SshToolAbstractIntegrationTest extends ShellToolAbstractTest {
private static final Logger log = LoggerFactory.getLogger(SshToolAbstractIntegrationTest.class);
// FIXME need tests which take properties set in entities and brooklyn.properties;
// but not in this class because it is lower level than entities, Aled would argue.
// TODO No tests for retry logic and exception handing yet
public static final String SSH_KEY_WITH_PASSPHRASE = System.getProperty("brooklyn.sshPrivateKeyWithPassphrase", "~/.ssh/id_rsa_with_passphrase");
public static final String SSH_PASSPHRASE = System.getProperty("brooklyn.sshPrivateKeyPassphrase", "mypassphrase");
public static final String SSH_DEFAULT_KEYFILE = System.getProperty("brooklyn.sshDefaultPrivateKeyFile", "~/.ssh/id_rsa");
protected String remoteFilePath;
protected SshTool tool() { return (SshTool)tool; }
protected abstract SshTool newUnregisteredTool(Map<String,?> flags);
@Override
protected SshTool newTool() {
return newTool(ImmutableMap.of("host", "localhost", "privateKeyFile", SSH_DEFAULT_KEYFILE));
}
@Override
protected SshTool newTool(Map<String,?> flags) {
return (SshTool) super.newTool(flags);
}
@BeforeMethod(alwaysRun=true)
public void setUp() throws Exception {
super.setUp();
remoteFilePath = "/tmp/ssh-test-remote-"+Identifiers.makeRandomId(8);
filesCreated.add(remoteFilePath);
}
protected void assertRemoteFileContents(String remotePath, String expectedContents) {
String catout = execCommands("cat "+remotePath);
assertEquals(catout, expectedContents);
}
/**
* @param remotePath
* @param expectedPermissions Of the form, for example, "-rw-r--r--"
*/
protected void assertRemoteFilePermissions(String remotePath, String expectedPermissions) {
String lsout = execCommands("ls -l "+remotePath);
assertTrue(lsout.contains(expectedPermissions), lsout);
}
protected void assertRemoteFileLastModifiedIsNow(String remotePath) {
// Check default last-modified time is `now`.
// Be lenient in assertion, in case unlucky that clock ticked over to next hour/minute as test was running.
// TODO Code could be greatly improved, but low priority!
// Output format:
// -rw-r--r-- 1 aled wheel 18 Apr 24 15:03 /tmp/ssh-test-remote-CvFN9zQA
// [0] [1] [2] [3] [4] [5] [6] [7] [8]
String lsout = execCommands("ls -l "+remotePath);
String[] lsparts = lsout.split("\\s+");
int day = Integer.parseInt(lsparts[6]);
int hour = Integer.parseInt(lsparts[7].split(":")[0]);
int minute = Integer.parseInt(lsparts[7].split(":")[1]);
Calendar expected = Calendar.getInstance();
int expectedDay = expected.get(Calendar.DAY_OF_MONTH);
int expectedHour = expected.get(Calendar.HOUR_OF_DAY);
int expectedMinute = expected.get(Calendar.MINUTE);
assertEquals(day, expectedDay, "ls="+lsout+"; lsparts="+Arrays.toString(lsparts)+"; expected="+expected+"; expectedDay="+expectedDay+"; day="+day+"; zone="+expected.getTimeZone());
assertTrue(Math.abs(hour - expectedHour) <= 1, "ls="+lsout+"; lsparts="+Arrays.toString(lsparts)+"; expected="+expected+"; expectedHour="+expectedHour+"; hour="+hour+"; zone="+expected.getTimeZone());
assertTrue(Math.abs(minute - expectedMinute) <= 1, "ls="+lsout+"; lsparts="+Arrays.toString(lsparts)+"; expected="+expected+"; expectedMinute="+expectedMinute+"; minute="+minute+"; zone="+expected.getTimeZone());
}
@Test(groups = {"Integration"})
public void testCopyToServerFromBytes() throws Exception {
String contents = "echo hello world!\n";
byte[] contentBytes = contents.getBytes();
tool().copyToServer(MutableMap.<String,Object>of(), contentBytes, remoteFilePath);
assertRemoteFileContents(remoteFilePath, contents);
assertRemoteFilePermissions(remoteFilePath, "-rw-r--r--");
// TODO would like to also assert lastModified time, but on jenkins the jvm locale
// and the OS locale are different (i.e. different timezones) so the file time-stamp
// is several hours out.
//assertRemoteFileLastModifiedIsNow(remoteFilePath);
}
@Test(groups = {"Integration"})
public void testCopyToServerFromInputStream() throws Exception {
String contents = "echo hello world!\n";
ByteArrayInputStream contentsStream = new ByteArrayInputStream(contents.getBytes());
tool().copyToServer(MutableMap.<String,Object>of(), contentsStream, remoteFilePath);
assertRemoteFileContents(remoteFilePath, contents);
}
@Test(groups = {"Integration"})
public void testCopyToServerWithPermissions() throws Exception {
tool().copyToServer(ImmutableMap.of("permissions","0754"), "echo hello world!\n".getBytes(), remoteFilePath);
assertRemoteFilePermissions(remoteFilePath, "-rwxr-xr--");
}
@Test(groups = {"Integration"})
public void testCopyToServerWithLastModifiedDate() throws Exception {
long lastModificationTime = 1234567;
tool().copyToServer(ImmutableMap.of("lastModificationDate", lastModificationTime), "echo hello world!\n".getBytes(), remoteFilePath);
String lsout = execCommands("ls -l "+remoteFilePath);//+" | awk '{print \$6 \" \" \$7 \" \" \$8}'"])
//execCommands([ "ls -l "+remoteFilePath+" | awk '{print \$6 \" \" \$7 \" \" \$8}'"])
//varies depending on timezone
assertTrue(lsout.contains("Jan 15 1970") || lsout.contains("Jan 14 1970") || lsout.contains("Jan 16 1970"), lsout);
//assertLastModified(lsout, lastModifiedDate)
}
@Test(groups = {"Integration"})
public void testCopyFileToServerWithPermissions() throws Exception {
String contents = "echo hello world!\n";
Files.write(contents, new File(localFilePath), Charsets.UTF_8);
tool().copyToServer(ImmutableMap.of("permissions", "0754"), new File(localFilePath), remoteFilePath);
assertRemoteFileContents(remoteFilePath, contents);
String lsout = execCommands("ls -l "+remoteFilePath);
assertTrue(lsout.contains("-rwxr-xr--"), lsout);
}
@Test(groups = {"Integration"})
public void testCopyFromServer() throws Exception {
String contentsWithoutLineBreak = "echo hello world!";
String contents = contentsWithoutLineBreak+"\n";
tool().copyToServer(MutableMap.<String,Object>of(), contents.getBytes(), remoteFilePath);
tool().copyFromServer(MutableMap.<String,Object>of(), remoteFilePath, new File(localFilePath));
List<String> actual = Files.readLines(new File(localFilePath), Charsets.UTF_8);
assertEquals(actual, ImmutableList.of(contentsWithoutLineBreak));
}
// TODO No config options in sshj or scp for auto-creating the parent directories
@Test(enabled=false, groups = {"Integration"})
public void testCopyFileToNonExistantDir() throws Exception {
String contents = "echo hello world!\n";
String remoteFileDirPath = "/tmp/ssh-test-remote-dir-"+Identifiers.makeRandomId(8);
String remoteFileInDirPath = remoteFileDirPath + File.separator + "ssh-test-remote-"+Identifiers.makeRandomId(8);
filesCreated.add(remoteFileInDirPath);
filesCreated.add(remoteFileDirPath);
tool().copyToServer(MutableMap.<String,Object>of(), contents.getBytes(), remoteFileInDirPath);
assertRemoteFileContents(remoteFileInDirPath, contents);
}
@Test(groups = {"Integration"})
public void testAllocatePty() {
final ShellTool localtool = newTool(MutableMap.of("host", "localhost", SshTool.PROP_ALLOCATE_PTY.getName(), true));
connect(localtool);
Map<String,Object> props = new LinkedHashMap<String, Object>();
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
props.put("out", out);
props.put("err", err);
int exitcode = localtool.execScript(props, Arrays.asList("echo hello err > /dev/stderr"), null);
assertTrue(out.toString().contains("hello err"), "no hello in output: "+out+" (err is '"+err+"')");
assertFalse(err.toString().contains("hello err"), "hello found in stderr: "+err);
assertEquals(0, exitcode);
}
// Requires setting up an extra ssh key, with a passphrase, and adding it to ~/.ssh/authorized_keys
@Test(groups = {"Integration"})
public void testSshKeyWithPassphrase() throws Exception {
final SshTool localtool = newTool(ImmutableMap.<String,Object>builder()
.put(SshTool.PROP_HOST.getName(), "localhost")
.put(SshTool.PROP_PRIVATE_KEY_FILE.getName(), SSH_KEY_WITH_PASSPHRASE)
.put(SshTool.PROP_PRIVATE_KEY_PASSPHRASE.getName(), SSH_PASSPHRASE)
.build());
localtool.connect();
assertEquals(tool.execScript(MutableMap.<String,Object>of(), ImmutableList.of("date")), 0);
// Also needs the negative test to prove that we're really using an ssh-key with a passphrase
try {
final SshTool localtool2 = newTool(ImmutableMap.<String,Object>builder()
.put(SshTool.PROP_HOST.getName(), "localhost")
.put(SshTool.PROP_PRIVATE_KEY_FILE.getName(), SSH_KEY_WITH_PASSPHRASE)
.build());
localtool2.connect();
fail();
} catch (Exception e) {
SshException se = Exceptions.getFirstThrowableOfType(e, SshException.class);
if (se == null) throw e;
}
}
@Test(groups = {"Integration"})
public void testSshKeyWithNoKeyDefaultsToIdrsa() throws Exception {
final SshTool localtool = newTool(ImmutableMap.<String,Object>builder()
.put(SshTool.PROP_HOST.getName(), "localhost")
.build());
tools.add(localtool);
localtool.connect();
assertEquals(localtool.execScript(MutableMap.<String,Object>of(), ImmutableList.of("date")), 0);
}
@Test(groups = {"Integration"})
public void testSshKeyWithPrivateKeyData() throws Exception {
final SshTool localtool = newTool(ImmutableMap.<String,Object>builder()
.put(SshTool.PROP_HOST.getName(), "localhost")
.put(SshTool.PROP_PRIVATE_KEY_DATA.getName(), new String(Files.toByteArray(new File(Os.tidyPath(SSH_DEFAULT_KEYFILE))), StandardCharsets.UTF_8))
.build());
localtool.connect();
assertEquals(localtool.execScript(MutableMap.<String,Object>of(), ImmutableList.of("date")), 0);
// Also needs the negative test to prove that we're really using an ssh-key with a passphrase
try {
final SshTool localtool2 = newTool(ImmutableMap.<String,Object>builder()
.put(SshTool.PROP_HOST.getName(), "localhost")
.put(SshTool.PROP_PRIVATE_KEY_DATA.getName(), "invalid data")
.build());
localtool2.connect();
localtool2.execScript(MutableMap.<String,Object>of(), ImmutableList.of("date"));
// Notice that executing a command may succeed for SshCliToolIntegrationTest.testSshKeyWithPrivateKeyData if you already have valid keys loaded in the ssh-agent
fail();
} catch (Exception e) {
SshException se = Exceptions.getFirstThrowableOfType(e, SshException.class);
if (se == null) throw e;
}
}
@Test(groups = {"Integration"})
public void testConnectWithInvalidUserThrowsException() throws Exception {
final ShellTool localtool = newTool(ImmutableMap.of("user", "wronguser", "host", "localhost", "privateKeyFile", SSH_DEFAULT_KEYFILE));
tools.add(localtool);
try {
connect(localtool);
fail();
} catch (SshException e) {
if (!e.toString().contains("failed to connect")) throw e;
}
}
@Test(groups = {"Integration"})
public void testOutputAsExpected() throws Exception {
final String CONTENTS = "hello world\n"
+ "bye bye\n";
execCommands("cat > "+Os.mergePaths(Os.tmp(), "test1")+" << X\n"
+ CONTENTS
+ "X\n");
String read = execCommands("echo START_FOO", "cat "+Os.mergePaths(Os.tmp(), "test1"), "echo END_FOO");
log.debug("read back data written, as:\n"+read);
String contents = Strings.getFragmentBetween(read, "START_FOO", "END_FOO");
Assert.assertEquals(CONTENTS.trim(), contents.trim());
}
@Test(groups = {"Integration"})
public void testScriptDirPropertiesIsRespected() {
// For explanation of (some of) the magic behind this command, see http://stackoverflow.com/a/229606/68898
final String command = "if [[ \"$0\" == \"/var/tmp/\"* ]]; then true; else false; fi";
SshTool sshTool = newTool(ImmutableMap.<String, Object>builder()
.put(SshTool.PROP_HOST.getName(), "localhost")
.build());
int rc = sshTool.execScript(ImmutableMap.<String, Object>builder()
.put(SshTool.PROP_SCRIPT_DIR.getName(), "/var/tmp")
.build(), ImmutableList.of(command));
assertEquals(rc, 0);
}
}