mirror of https://github.com/apache/jclouds.git
Issue 861:SSHClient should provide access to input/output streams
This commit is contained in:
parent
38de846947
commit
dd7b16075e
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue