Issue 861:SSHClient should provide access to input/output streams

This commit is contained in:
Adrian Cole 2012-03-08 15:48:51 -08:00
parent 38de846947
commit dd7b16075e
6 changed files with 308 additions and 12 deletions

View File

@ -0,0 +1,96 @@
/**
* Licensed to jclouds, Inc. (jclouds) under one or more
* contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. jclouds 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.jclouds.compute.domain;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import com.google.common.base.Supplier;
import com.google.common.io.Closeables;
/**
* A current connection to an exec'd command. Please ensure you call {@link ExecChannel#close}
*
* @author Adrian Cole
*/
public class ExecChannel implements Closeable {
private final OutputStream input;
private final InputStream output;
private final InputStream error;
private final Supplier<Integer> exitStatus;
private final Closeable closer;
public ExecChannel(OutputStream input, InputStream output, InputStream error, Supplier<Integer> exitStatus,
Closeable closer) {
this.input = checkNotNull(input, "input");
this.output = checkNotNull(output, "output");
this.error = checkNotNull(error, "error");
this.exitStatus = checkNotNull(exitStatus, "exitStatus");
this.closer = checkNotNull(closer, "closer");
}
/**
*
* @return the command's {@code stdin} stream.
*/
public OutputStream getInput() {
return input;
}
/**
*
* @return the command's {@code stderr} stream.
*/
public InputStream getError() {
return error;
}
/**
*
* @return the command's {@code stdout} stream.
*/
public InputStream getOutput() {
return output;
}
/**
*
* @return the exit status of the command if it was received, or {@code null} if this information
* was not received.
*/
public Supplier<Integer> getExitStatus() {
return exitStatus;
}
/**
* closes resources associated with this channel.
*/
@Override
public void close() throws IOException {
Closeables.closeQuietly(input);
Closeables.closeQuietly(output);
Closeables.closeQuietly(error);
closer.close();
}
}

View File

@ -18,6 +18,7 @@
*/
package org.jclouds.ssh;
import org.jclouds.compute.domain.ExecChannel;
import org.jclouds.compute.domain.ExecResponse;
import org.jclouds.domain.Credentials;
import org.jclouds.domain.LoginCredentials;
@ -50,9 +51,24 @@ public interface SshClient {
void put(String path, Payload contents);
Payload get(String path);
/**
* Execute a process and block until it is complete
*
* @param command command line to invoke
* @return output of the command
*/
ExecResponse exec(String command);
/**
* Execute a process and allow the user to interact with it
*
* @param command command line to invoke
* @return reference to the running process
* @since 1.5.0
*/
ExecChannel execChannel(String command);
void connect();
void disconnect();

View File

@ -30,6 +30,7 @@ import static org.jclouds.crypto.CryptoStreams.md5;
import static org.jclouds.crypto.SshKeys.fingerprintPrivateKey;
import static org.jclouds.crypto.SshKeys.sha1PrivateKey;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
@ -41,6 +42,7 @@ import javax.inject.Named;
import org.apache.commons.io.input.ProxyInputStream;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.jclouds.compute.domain.ExecChannel;
import org.jclouds.compute.domain.ExecResponse;
import org.jclouds.http.handlers.BackoffLimitedRetryHandler;
import org.jclouds.io.Payload;
@ -57,6 +59,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.io.Closeables;
import com.google.inject.Inject;
import com.jcraft.jsch.ChannelExec;
@ -125,7 +128,7 @@ public class JschSshClient implements SshClient {
private final BackoffLimitedRetryHandler backoffLimitedRetryHandler;
public JschSshClient(BackoffLimitedRetryHandler backoffLimitedRetryHandler, IPSocket socket, int timeout,
String username, String password, byte[] privateKey) {
String username, String password, byte[] privateKey) {
this.host = checkNotNull(socket, "socket").getAddress();
checkArgument(socket.getPort() > 0, "ssh port must be greater then zero" + socket.getPort());
checkArgument(password != null || privateKey != null, "you must specify a password or a key");
@ -138,10 +141,10 @@ public class JschSshClient implements SshClient {
if (privateKey == null) {
this.toString = String.format("%s:pw[%s]@%s:%d", username, hex(md5(password.getBytes())), host, port);
} else {
String fingerPrint = fingerprintPrivateKey(new String(privateKey));
String sha1 = sha1PrivateKey(new String(privateKey));
this.toString = String.format("%s:rsa[fingerprint(%s),sha1(%s)]@%s:%d", username, fingerPrint, sha1, host,
port);
String fingerPrint = fingerprintPrivateKey(new String(privateKey));
String sha1 = sha1PrivateKey(new String(privateKey));
this.toString = String.format("%s:rsa[fingerprint(%s),sha1(%s)]@%s:%d", username, fingerPrint, sha1, host,
port);
}
}
@ -181,7 +184,8 @@ public class JschSshClient implements SshClient {
} else {
// jsch wipes out your private key
if (CredentialUtils.isPrivateKeyEncrypted(privateKey)) {
throw new IllegalArgumentException("JschSshClientModule does not support private keys that require a passphrase");
throw new IllegalArgumentException(
"JschSshClientModule does not support private keys that require a passphrase");
}
jsch.addIdentity(username, Arrays.copyOf(privateKey, privateKey.length), null, emptyPassPhrase);
}
@ -323,8 +327,8 @@ public class JschSshClient implements SshClient {
@VisibleForTesting
boolean shouldRetry(Exception from) {
Predicate<Throwable> predicate = retryAuth ? Predicates.<Throwable>or(retryPredicate, instanceOf(AuthorizationException.class))
: retryPredicate;
Predicate<Throwable> predicate = retryAuth ? Predicates.<Throwable> or(retryPredicate,
instanceOf(AuthorizationException.class)) : retryPredicate;
if (any(getCausalChain(from), predicate))
return true;
if (!retryableMessages.equals(""))
@ -343,7 +347,7 @@ public class JschSshClient implements SshClient {
@Override
public boolean apply(Throwable arg0) {
return (arg0.toString().indexOf(input) != -1)
|| (arg0.getMessage() != null && arg0.getMessage().indexOf(input) != -1);
|| (arg0.getMessage() != null && arg0.getMessage().indexOf(input) != -1);
}
});
@ -361,7 +365,7 @@ public class JschSshClient implements SshClient {
if (e.getMessage() != null && e.getMessage().indexOf("Auth fail") != -1)
throw new AuthorizationException("(" + toString() + ") " + message, e);
throw e instanceof SshException ? SshException.class.cast(e) : new SshException(
"(" + toString() + ") " + message, e);
"(" + toString() + ") " + message, e);
}
@Override
@ -467,4 +471,60 @@ public class JschSshClient implements SshClient {
return this.username;
}
class ExecChannelConnection implements Connection<ExecChannel> {
private final String command;
private ChannelExec executor = null;
ExecChannelConnection(String command) {
this.command = checkNotNull(command, "command");
}
@Override
public void clear() {
if (executor != null)
executor.disconnect();
}
@Override
public ExecChannel create() throws Exception {
checkConnected();
String channel = "exec";
executor = (ChannelExec) session.openChannel(channel);
executor.setPty(true);
executor.setCommand(command);
ByteArrayOutputStream error = new ByteArrayOutputStream();
executor.setErrStream(error);
executor.connect();
return new ExecChannel(executor.getOutputStream(), executor.getInputStream(), executor.getErrStream(),
new Supplier<Integer>() {
@Override
public Integer get() {
int exitStatus = executor.getExitStatus();
return exitStatus != -1 ? exitStatus : null;
}
}, new Closeable() {
@Override
public void close() throws IOException {
clear();
}
});
}
@Override
public String toString() {
return "ExecChannel(command=[" + command + "])";
}
};
@Override
public ExecChannel execChannel(String command) {
return acquire(new ExecChannelConnection(command));
}
}

View File

@ -20,12 +20,16 @@ package org.jclouds.ssh.jsch;
import static org.testng.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import org.jclouds.compute.domain.ExecChannel;
import org.jclouds.compute.domain.ExecResponse;
import org.jclouds.domain.LoginCredentials;
import org.jclouds.io.Payload;
@ -39,6 +43,8 @@ import org.testng.annotations.BeforeGroups;
import org.testng.annotations.Test;
import com.google.common.base.Strings;
import com.google.common.base.Suppliers;
import com.google.common.io.Closeables;
import com.google.inject.Guice;
import com.google.inject.Injector;
@ -107,6 +113,22 @@ public class JschSshClientLiveTest {
}
@Override
public ExecChannel execChannel(String command) {
if (command.equals("hostname")) {
return new ExecChannel(new ByteArrayOutputStream(), new ByteArrayInputStream(sshHost.getBytes()),
new ByteArrayInputStream(new byte[] {}), Suppliers.ofInstance(0), new Closeable() {
@Override
public void close() {
}
});
}
throw new RuntimeException("command " + command + " not stubbed");
}
};
} else {
Injector i = Guice.createInjector(new JschSshClientModule(), new SLF4JLoggingModule());
@ -147,4 +169,16 @@ public class JschSshClientLiveTest {
: sshHost);
}
public void testExecChannelHostname() throws IOException {
ExecChannel response = setupClient().execChannel("hostname");
try {
assertEquals(Strings2.toStringAndClose(response.getError()), "");
assertEquals(Strings2.toStringAndClose(response.getOutput()).trim(), "localhost".equals(sshHost) ? InetAddress
.getLocalHost().getHostName() : sshHost);
} finally {
Closeables.closeQuietly(response);
}
assertEquals(response.getExitStatus().get(), new Integer(0));
}
}

View File

@ -30,6 +30,7 @@ import static org.jclouds.crypto.CryptoStreams.md5;
import static org.jclouds.crypto.SshKeys.fingerprintPrivateKey;
import static org.jclouds.crypto.SshKeys.sha1PrivateKey;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
@ -46,6 +47,7 @@ import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.connection.ConnectionException;
import net.schmizz.sshj.connection.channel.direct.PTYMode;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.connection.channel.direct.SessionChannel;
import net.schmizz.sshj.connection.channel.direct.Session.Command;
import net.schmizz.sshj.sftp.SFTPClient;
import net.schmizz.sshj.sftp.SFTPException;
@ -56,6 +58,7 @@ import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
import net.schmizz.sshj.xfer.InMemorySourceFile;
import org.apache.commons.io.input.ProxyInputStream;
import org.jclouds.compute.domain.ExecChannel;
import org.jclouds.compute.domain.ExecResponse;
import org.jclouds.http.handlers.BackoffLimitedRetryHandler;
import org.jclouds.io.Payload;
@ -71,7 +74,9 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.io.Closeables;
import com.google.inject.Inject;
/**
@ -499,6 +504,59 @@ public class SshjSshClient implements SshClient {
return acquire(new ExecConnection(command));
}
class ExecChannelConnection implements Connection<ExecChannel> {
private final String command;
private SessionChannel session;
ExecChannelConnection(String command) {
this.command = checkNotNull(command, "command");
}
@Override
public void clear() {
if (session != null)
Closeables.closeQuietly(session);
}
@Override
public ExecChannel create() throws Exception {
try {
session = SessionChannel.class.cast(acquire(execConnection()));
Command output = session.exec(command);
output.join(timeoutMillis, TimeUnit.SECONDS);
return new ExecChannel(session.getOutputStream(), session.getInputStream(), session.getErrorStream(),
new Supplier<Integer>() {
@Override
public Integer get() {
return session.getExitStatus();
}
}, new Closeable() {
@Override
public void close() throws IOException {
clear();
}
});
} finally {
clear();
}
}
@Override
public String toString() {
return "ExecChannel(command=[" + command + "])";
}
}
@Override
public ExecChannel execChannel(String command) {
return acquire(new ExecChannelConnection(command));
}
@Override
public String getHostAddress() {
return this.host;

View File

@ -20,12 +20,16 @@ package org.jclouds.sshj;
import static org.testng.Assert.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import org.jclouds.compute.domain.ExecChannel;
import org.jclouds.compute.domain.ExecResponse;
import org.jclouds.domain.LoginCredentials;
import org.jclouds.io.Payload;
@ -39,6 +43,8 @@ import org.testng.annotations.BeforeGroups;
import org.testng.annotations.Test;
import com.google.common.base.Strings;
import com.google.common.base.Suppliers;
import com.google.common.io.Closeables;
import com.google.inject.Guice;
import com.google.inject.Injector;
@ -106,7 +112,22 @@ public class SshjSshClientLiveTest {
public void put(String path, String contents) {
}
@Override
public ExecChannel execChannel(String command) {
if (command.equals("hostname")) {
return new ExecChannel(new ByteArrayOutputStream(), new ByteArrayInputStream(sshHost.getBytes()),
new ByteArrayInputStream(new byte[] {}), Suppliers.ofInstance(0), new Closeable() {
@Override
public void close() {
}
});
}
throw new RuntimeException("command " + command + " not stubbed");
}
};
} else {
Injector i = Guice.createInjector(new SshjSshClientModule(), new SLF4JLoggingModule());
@ -147,5 +168,16 @@ public class SshjSshClientLiveTest {
assertEquals(response.getOutput().trim(), "localhost".equals(sshHost) ? InetAddress.getLocalHost().getHostName()
: sshHost);
}
public void testExecChannelHostname() throws IOException {
ExecChannel response = setupClient().execChannel("hostname");
try {
assertEquals(Strings2.toStringAndClose(response.getError()), "");
assertEquals(Strings2.toStringAndClose(response.getOutput()).trim(), "localhost".equals(sshHost) ? InetAddress
.getLocalHost().getHostName() : sshHost);
} finally {
Closeables.closeQuietly(response);
}
assertEquals(response.getExitStatus().get(), new Integer(0));
}
}