package org.ovirt.engine.core.utils.hostinstall;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.nio.channels.FileChannel;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.concurrent.TimeoutException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import javax.naming.TimeLimitExceededException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.sshd.ClientChannel;
import org.apache.sshd.ClientSession;
import org.apache.sshd.SshClient;
import org.apache.sshd.client.future.AuthFuture;
import org.apache.sshd.client.future.ConnectFuture;
import org.ovirt.engine.core.common.config.Config;
import org.ovirt.engine.core.common.config.ConfigValues;
import org.ovirt.engine.core.compat.backendcompat.Path;
/**
* This Class implements file upload/download and command execution over SSH. Currently the code is very ugly (!!!)
* since it implements file transfer using exec channel and large file upload using reverse SSH. The reason for this is
* mina's lack of SFTP/SCP functionality. Once Mina completes the SFTP (or SCP) functionality, this class should be
* fixed to use mina's built-in file transfer functionality. In this case the current UploadFile should be deleted (with
* uploadLargeFile), and leave only uploadSmallfile with proper SFTP implementation, which should be renamed to
* UploadFile.
*/
public class MinaInstallWrapper implements IVdsInstallWrapper {
private static Log log = LogFactory.getLog(MinaInstallWrapper.class);
private IVdsInstallCallBack callback;
private SshClient client = null;
private ClientSession session = null;
private HostKeyVerifier hkvVerifier = null;
private String host = null;
private int maxSSHTimeout = 600000; // value is in milisec's.
private int nSSHPort = 22;
private final static int ONE_SECOND_MILI = 1000;
private final static int DEFAULT_BUF_SIZE = 1024;
private final static int LARGE_BUF_SIZE = 32768;
private final static long ONE_MB = 1048576l;
private final static long TWENTY_MBI = 20971520l;
private final static long MAX_FILE_COUNT = 65536;
public MinaInstallWrapper() {
this(null);
}
protected MinaInstallWrapper(final SshClient sshClient) {
try {
if(sshClient == null) {
this.client = SshClient.setUpDefaultClient();
} else {
client = sshClient;
}
client.start();
} catch (Throwable t) {
log.error("Unable to create SSH client. Please check mina jars: ", t);
}
maxSSHTimeout = ONE_SECOND_MILI * Config.<Integer> GetValue(ConfigValues.SSHInactivityTimoutSeconds);
hkvVerifier = new HostKeyVerifier();
client.setServerKeyVerifier(hkvVerifier);
}
/***
* We use finalize in order to properly close the SSH client. This is important in order to avoid leaks
* (memory/threads).
*/
protected void finalize() throws Throwable {
try {
wrapperShutdown();
} finally {
super.finalize();
}
}
/*
* Start org.ovirt.engine.core.utils.hostinstall.IVdsInstallWrapper implementation
*/
public final void InitCallback(IVdsInstallCallBack callback) {
this.callback = callback;
}
public final boolean ConnectToServer(String server) {
return ConnectToServer(server, Config.resolveKeyStorePath(),
Config.<String> GetValue(ConfigValues.keystorePass));
}
public final boolean ConnectToServer(String server, String rootPassword) {
Credentials creds = prepareCredentials(rootPassword);
return _do_connect(server, creds);
}
public final boolean ConnectToServer(String server, String rootPassword, long timeout) {
Credentials creds = prepareCredentials(rootPassword);
return _do_connect(server, creds, timeout);
}
private Credentials prepareCredentials(String rootPassword) {
Credentials creds = new Credentials();
creds.setPassword(rootPassword);
creds.setUsername("root");
return creds;
}
public final boolean ConnectToServer(String server, String certPath, String password) {
Credentials creds = new Credentials();
creds.setPassphrase(password);
creds.setCertPath(certPath);
creds.setUsername("root");
return _do_connect(server, creds);
}
/***
* This method executes a given command in remote SSH shell. The method returns false if session was not closed
* normally.
*
* Note: This method is using piped (in+out streams). This is possible only when producer & consumer are different
* threads. If it happens to run on the same thread it may create deadlocks.
*/
public final boolean RunSSHCommand(String command) {
boolean fReturn = true;
log.info(String.format("Invoking %s on %s", command, host));
ClientChannel channel = null;
PipedInputStream pisOut = null;
PipedOutputStream out = null;
PipedInputStream pisErr = null;
PipedOutputStream err = null;
try {
channel = session.createExecChannel(command);
pisOut = new PipedInputStream();
out = new PipedOutputStream(pisOut);
pisErr = new PipedInputStream();
err = new PipedOutputStream(pisErr);
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
channel.setIn(in);
channel.setOut(out);
channel.setErr(err);
log.debug("open");
channel.open();
log.debug("wait close");
long timeout = System.currentTimeMillis();
while (true) {
int nStat = channel.waitFor(ClientChannel.CLOSED, ONE_SECOND_MILI);
if (pisOut.available() > 0) {
String sshMessage = readLineFromInput(pisOut);
log.debug(sshMessage);
callbackAddMessage(sshMessage);
}
if (pisErr.available() > 0) {
String errorSshMessage = readInput(pisErr);
log.error(errorSshMessage);
callbackAddError(errorSshMessage);
}
if ((nStat & ClientChannel.CLOSED) == ClientChannel.CLOSED) {
log.debug("Got channel close: " + nStat);
break;
} else if ((nStat & ClientChannel.EOF) == ClientChannel.EOF) {
log.debug("Got channel EOF: " + nStat);
fReturn = false;
break;
}
if (timeout + maxSSHTimeout <= System.currentTimeMillis()) {
String message = (
"The required action is taking longer than allowed by configuration." +
" Verify host networking and storage settings."
);
log.error(message + " Current timeout is set to: " + maxSSHTimeout);
throw new TimeLimitExceededException(message);
}
}
if (!fReturn && pisErr.available() > 0) {
byte[] bytes = new byte[pisErr.available()];
pisErr.read(bytes);
String errorSshMessage = readInput(pisErr);
log.error(errorSshMessage);
callbackAddError(errorSshMessage);
}
} catch (Exception e) {
callbackFailed(e.getMessage());
log.error("Error running command " + command, e);
fReturn = false;
} finally {
if (channel != null) {
channel.close(true);
channel = null;
}
try {
if (pisOut != null) {
pisOut.close();
pisOut = null;
}
if (out != null) {
out.close();
out = null;
}
if (pisErr != null) {
pisErr.close();
pisErr = null;
}
if (err != null) {
err.close();
err = null;
}
} catch (IOException e) {
log.debug("Caught pipe closing exception", e);
}
}
log.info("RunSSHCommand returns " + fReturn);
return fReturn;
}
/***
* This implementation uses gzip at the remote end to create a binary stream we gunzip on local machine to the
* desired destination. Note, this implementation should be replaced by mina SSHD's standard scp/sftp implementation
* once available.
*
* @param remoteSource
* source file on the remote machine. This should be a valid path and file name.
* @param localDestination
* destination file on the local machine. This should be a valid path and file name.
*/
public final boolean DownloadFile(String remoteSource, String localDestination) {
log.info(String.format("Downloading file %s from %s to %s", remoteSource, host, localDestination));
boolean fReturn = true;
ClientChannel channel = null;
File tempFile = null;
FileOutputStream fout = null;
try {
// 1. Get exec channel
log.debug("create channel");
channel = session.createExecChannel("gzip -1 -q -c " + remoteSource);
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
channel.setIn(in);
ByteArrayOutputStream out = new ByteArrayOutputStream();
channel.setOut(out);
ByteArrayOutputStream err = new ByteArrayOutputStream();
channel.setErr(err);
// 2. Start transfer
log.debug("open channel");
channel.open();
log.debug("wait close");
long timeout = System.currentTimeMillis();
while (true) {
int nStat = channel.waitFor(ClientChannel.CLOSED, ONE_SECOND_MILI);
String sshMessage = new String(out.toByteArray());
if (sshMessage.length() > 0) {
log.debug(sshMessage);
}
String errorSshMessage = new String(err.toByteArray());
if (errorSshMessage.length() > 0) {
log.error(errorSshMessage);
callbackAddError(errorSshMessage);
}
if ((nStat & ClientChannel.CLOSED) == ClientChannel.CLOSED) {
log.debug("Got channel close: " + nStat);
break;
} else if ((nStat & ClientChannel.EOF) == ClientChannel.EOF) {
log.debug("Got channel EOF: " + nStat);
fReturn = false;
break;
}
if (timeout + maxSSHTimeout <= System.currentTimeMillis()) {
log.error("Transfer time exceeded internal SSH timeout: " + maxSSHTimeout);
throw new TimeLimitExceededException(
"Transfer time exceeded internal SSH timeout: "
+ maxSSHTimeout);
}
}
// 3. Check incoming data length
int nDataLength = out.toByteArray().length;
log.debug("Incoming data length:" + nDataLength);
if (nDataLength <= 0) {
log.error("Got zero bytes. File empty or not found!");
throw new FileNotFoundException("Downloaded zero bytes from " + remoteSource);
}
// 4. Write incoming data into a temp file
tempFile = File.createTempFile("dlz.", null);
FileOutputStream file = new FileOutputStream(tempFile);
file.write(out.toByteArray());
// Close the output stream
file.close();
// 5. Unzip temp file into local file.
GZIPInputStream zipin = new GZIPInputStream(new FileInputStream(tempFile));
byte[] buf = new byte[DEFAULT_BUF_SIZE];
// Open the output file
fout = new FileOutputStream(localDestination);
MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
// Transfer bytes from the GZIP stream to the output file
int len;
while ((len = zipin.read(buf)) > 0) {
fout.write(buf, 0, len);
digest.update(buf, 0, len);
}
fout.close();
zipin.close();
if (!tempFile.delete()) {
log.warn("Warn: unable to delete " + tempFile.getName());
}
tempFile = null;
// 6. Get source hash and compare the results
String remoteStringHash = getRemoteHash(remoteSource);
if (remoteStringHash != null) {
String localStringHash = byteArrayToHexString(digest.digest(), false);
log.debug("Debug: Comparing local hash string " + localStringHash + " to remote hash string "
+ remoteStringHash);
fReturn = localStringHash.equalsIgnoreCase(remoteStringHash);
} else {
fReturn = false;
}
if (fReturn) {
callbackEndTransfer();
} else {
callbackAddError("Exit Status from transfer: -1");
log.error("err : " + new String(err.toByteArray()));
}
} catch (Throwable t) {
callbackAddError(t.getMessage());
log.error("Error downloading file: ", t);
} finally {
if (channel != null) {
channel.close(true);
channel = null;
}
if (tempFile != null) {
if (!tempFile.delete()) {
log.warn("Warn: unable to delete " + tempFile.getName());
}
tempFile = null;
}
if (fout != null) {
try {
fout.close();
} catch (IOException e) {
}
fout = null;
}
}
log.info("return " + fReturn);
return fReturn;
}
/***
* This implementation checks for file size, and uses the relevant implementation to upload the given file. Note:
* this implementation should be replaced by mina- SSHD's standard scp/sftp implementation once available.
*
* @param source
* Source file on the local machine. This should be a valid path and file name.
* @param destination
* Destination file on the remote machine. This should be a valid path and file name.
*/
public final boolean UploadFile(String source, String destination) {
log.info(String.format("Uploading file %s to %s on %s", source, destination, host));
boolean fReturn = true;
File file = new File(source);
if (!file.exists()) {
this.callbackFailed(String.format("Upload failed. File: %s not exist", source));
log.error("File to upload does not exist: " + source);
fReturn = false;
} else {
long lSize = file.length();
if (lSize > ONE_MB) // Size is bigger than 1 MB
{
log.debug("Upload using uploadLargeFile");
fReturn = uploadLargeFile(source, destination);
} else {
log.debug("Upload using uploadSmallFile");
fReturn = uploadSmallFile(source, destination);
}
}
return fReturn;
}
/*
* End org.ovirt.engine.core.hostinstall.IVdsInstallWrapper implementation
*
*
* Start supporting methods for MinaInstallWrapper implementation
*/
/***
* Establish SSH connection to the given host, using the given parameters. Connection authentication may be using
* password or public key authentication, according to the given data in the Credentials object. If successful, a
* session object will be maintained for this instance.
*
* @param server
* Host IP or name we wish to connect to.
* @param creds
* Authentication data
* @return boolean (success/failure).
*/
private boolean _do_connect(String server, Credentials creds) {
return _do_connect(server, creds, maxSSHTimeout);
}
private boolean _do_connect(String server, Credentials creds, long timeout) {
boolean fReturn = true;
host = server;
FileInputStream fis = null;
try {
log.debug("_do_connect entry");
ConnectFuture future = client.connect(host, nSSHPort);
if(future.await(timeout)) {
session = future.getSession();
} else {
throwTimeout();
}
if (creds.getCertPath() != null) {
log.debug("Using Public Key Authentication.");
char[] pass = creds.getPassphrase().toCharArray();
KeyStore ks = KeyStore.getInstance("JKS");
fis = new FileInputStream(creds.getCertPath());
ks.load(fis, pass);
fis.close();
PrivateKey kPrivate = null;
try {
kPrivate = ((KeyStore.PrivateKeyEntry) ks.getEntry(
Config.<String> GetValue(ConfigValues.CertAlias), new KeyStore.PasswordProtection(pass)))
.getPrivateKey();
} catch (Exception e) {
String message = ("Failed to get certificate entry for alias: " + Config
.<String> GetValue(ConfigValues.CertAlias));
log.error(message, e);
throw new Exception(message);
}
Arrays.fill(pass, '\0');
PublicKey kPublic = ks.getCertificate(Config.<String> GetValue(ConfigValues.CertAlias)).getPublicKey();
KeyPair pair = new KeyPair(kPublic, kPrivate);
AuthFuture authKeyFuture = session.authPublicKey(creds.getUsername(), pair);
if(!authKeyFuture.await(timeout)) {
throwTimeout();
}
if (!authKeyFuture.isSuccess()) {
throw new Exception("Failed connecting to " + host
+ " using Public-Key! Please verify that the host accepts public-key authentication");
}
} else {
log.debug("Using password authentication.");
AuthFuture authPasswordFuture = session.authPassword(creds.getUsername(), creds.getPassword());
if(!authPasswordFuture.await(timeout)) {
throwTimeout();
}
if (!authPasswordFuture.isSuccess()) {
throw new Exception(
"Failed connecting to "
+ host
+ " using given password! Please verify your password is correct and that the host accepts password-based authentication");
}
}
callbackConnected();
} catch (Exception e) {
callbackFailed(e.getMessage());
fReturn = false;
log.error("Could not connect to server " + host + ": " + e.getMessage());
log.debug("Error details for connection error in " + host + ":", e);
if (fis != null) {
try {
fis.close();
} catch (Exception ee) {
}
fis = null;
}
}
return fReturn;
}
/***
* Upload a large file (such as ISO) to a remote host, using the given parameters. This implementation Copies the
* given source file to a public accessible local folder, then connects to the host and runs wget to local machine
* is order to fetch the file. Note: this implementation should be replaced by mina SSHD's standard scp/sftp
* implementation once available.
*
* @param localSource
* Local large file to be uploaded. This should be a valid path and file name.
* @param remoteDest
* Destination file on remote machine. This should be a valid path and file name.
* @return boolean (success/failure).
*/
private boolean uploadLargeFile(String localSource, String remoteDest) {
log.debug("uploadLargeFile entry");
long lTime = System.currentTimeMillis();
String strBaseFileName = new File(localSource).getName() + lTime + ".txt";
boolean fReturn = true;
ClientChannel channel = null;
String localHost = null;
String copyDest = null;
// Fast copy the selected file to a public site, to be downloaded by the
// RHEV-H machine.
try {
callbackAddMessage("Preparing ISO file");
copyDest = Path.Combine(
Path.Combine(Path.Combine(System.getProperty("jboss.server.home.dir"), "deploy"), "ROOT.war"),
strBaseFileName);
fastFileCopy(localSource, copyDest, false);
} catch (Throwable t) {
log.error("uploadLargeFile: Unable to copy local file: ", t);
callbackFailed("Failed to get a hold on ISO file");
fReturn = false;
}
// Get our IP as RHEV-H will see it (we may have more than one NIC).
if (fReturn) {
callbackAddMessage("Determining current address from host perspective");
localHost = getLocalIP();
log.debug("local IP: " + localHost);
if (localHost == null) {
log.error("uploadLargeFile: Unable to get local IP");
callbackFailed("Failed to upload ISO file: remote RHEV-H unable to calculate oVirt IP address");
fReturn = false;
} else {
localHost = localHost.trim().replace("\n", "");
}
}
if (fReturn) {
try {
String cmd = String.format("wget http://%s:%s/%s -O %s; echo $?", localHost,
Config.<String> GetValue(ConfigValues.PublicURLPort), strBaseFileName, remoteDest);
log.debug("uploadLargeFile executing: " + cmd);
channel = session.createExecChannel(cmd);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
channel.setIn(in);
channel.setOut(out);
channel.setErr(err);
callbackAddMessage("Starting ISO upload");
log.debug("open");
channel.open();
log.debug("wait close");
if (!waitWithTimeout(channel, 2 * maxSSHTimeout)) {
throw new TimeLimitExceededException("Timeout waiting for ISO upload results!");
}
// Check output
String strOut = new String(out.toByteArray());
log.debug("stdout: " + strOut);
int nResult = Integer.parseInt(strOut.trim());
fReturn = (nResult == 0);
if (fReturn) {
callbackAddMessage("ISO upload ended successfully");
callbackEndTransfer();
} else {
callbackFailed("Failed to upload ISO file. Return status: " + nResult);
}
} catch (Exception e) {
callbackFailed("Error uploading iso file " + e.getMessage());
log.error("Error uploading iso file " + localSource, e);
fReturn = false;
} finally {
if (channel != null) {
channel.close(true);
channel = null;
}
File fileCopyDest = new File(copyDest);
if (fileCopyDest.exists()) {
// Delete temp ISO file from public area.
fileCopyDest.delete();
}
fileCopyDest = null;
}
}
log.info("uploadLargeFile returns " + fReturn);
return fReturn;
}
/***
* This implementation uses zip stream on local machine to create a binary stream we gunzip on remote machine to the
* desired destination. Note, this implementation should be replaced by mina SSHD's standard scp/sftp implementation
* once available.
*
* @param source
* Source file on the local machine. This should be a valid path and file name.
* @param destination
* Destination file on the remote machine. This should be a valid path and file name.
*/
private boolean uploadSmallFile(String source, String destination) {
log.info(String.format("Uploading file %s to %s on %s", source, destination, host));
File file = new File(source);
boolean fReturn = true;
// Create the GZIP output stream
ByteArrayOutputStream baosGZipped = null;
GZIPOutputStream gzOut = null;
byte[] hash = null;
ClientChannel channel = null;
ByteArrayInputStream in = null;
if (file.exists()) {
try {
// Create the GZIP output stream
baosGZipped = new ByteArrayOutputStream();
gzOut = new GZIPOutputStream(baosGZipped);
MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
// Open the input file
FileInputStream src = new FileInputStream(source);
// Transfer bytes from the input file to the GZIP output stream
byte[] buf = new byte[DEFAULT_BUF_SIZE];
int len;
while ((len = src.read(buf)) > 0) {
gzOut.write(buf, 0, len);
digest.update(buf, 0, len);
}
src.close();
hash = digest.digest();
// Complete the GZIP file
gzOut.finish();
gzOut.flush();
gzOut.close();
gzOut = null;
} catch (Throwable t) {
callbackAddError(t.getMessage());
log.error(t);
fReturn = false;
} finally {
if (gzOut != null) {
try {
gzOut.close();
} catch (IOException e) {
}
gzOut = null;
}
}
} else {
this.callbackFailed(String.format("Upload failed. File: %s not exist", source));
log.error("File to upload does not exist: " + source);
fReturn = false;
}
if (fReturn) {
try {
log.debug("create channel");
channel = session.createExecChannel("gunzip -c >" + destination);
in = new ByteArrayInputStream(baosGZipped.toByteArray());
channel.setIn(in);
ByteArrayOutputStream out = new ByteArrayOutputStream();
channel.setOut(out);
ByteArrayOutputStream err = new ByteArrayOutputStream();
channel.setErr(err);
log.debug("open");
channel.open();
log.debug("wait close");
int nBytesLeft = in.available();
log.debug("Bytes left2: " + nBytesLeft);
int nMaxTimeoutSec = Config.<Integer> GetValue(ConfigValues.SSHInactivityTimoutSeconds);
for (int i = 0; nBytesLeft > 0 && i < nMaxTimeoutSec; i++) {
channel.waitFor(ClientChannel.CLOSED, ONE_SECOND_MILI);
nBytesLeft = in.available();
log.debug("Bytes left: " + nBytesLeft);
}
in.close();
in = null;
log.debug("++++debug: waiting for channel to close");
if (!waitWithTimeout(channel, nMaxTimeoutSec * ONE_SECOND_MILI)) {
throw new TimeLimitExceededException("Failed to close channel in less than " + nMaxTimeoutSec
+ " seconds");
}
log.debug("++++debug: channel closed");
channel = null;
if (nBytesLeft > 0) {
throw new TimeLimitExceededException("Failed to transfer all data. There are " + nBytesLeft
+ " bytes left.");
}
String localStringHash = null;
String remoteStringHash = getRemoteHash(destination);
if (remoteStringHash != null) {
localStringHash = byteArrayToHexString(hash, false);
log.debug("Debug: Comparing local hash string " + localStringHash + " to remote hash string "
+ remoteStringHash);
fReturn = localStringHash.equalsIgnoreCase(remoteStringHash);
} else {
fReturn = false;
}
if (fReturn) {
callbackEndTransfer();
} else {
callbackAddError("Failed to upload file: bad hash.");
log.error("err : " + new String(err.toByteArray()));
}
} catch (Exception e) {
callbackAddError(e.getMessage());
log.error(e);
fReturn = false;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
}
in = null;
}
if (channel != null) {
channel.close(false);
channel = null;
}
}
} // if gzipping file went fine
log.info("return " + fReturn);
return fReturn;
}
protected void callbackAddError(String message) {
if (haveCallback()) {
callback.AddError(message);
}
}
protected void callbackAddMessage(String message) {
if (haveCallback()) {
callback.AddMessage(message);
}
}
protected void callbackConnected() {
byte[] fp = hkvVerifier.getServerFingerprint();
String strFingerprint = "Unknown";
if (fp != null) {
strFingerprint = byteArrayToHexString(fp, true);
} else {
log.error("Unable to get host fingerprint!");
}
if (haveCallback()) {
callback.AddMessage(
String.format(
"<BSTRAP component='RHEV_INSTALL' status='OK' message='Connected to Host %s with SSH key fingerprint: %s'/>",
host,
strFingerprint
)
);
callback.Connected();
}
}
protected void callbackEndTransfer() {
if (haveCallback()) {
callback.EndTransfer();
}
}
protected void callbackFailed(String message) {
if (haveCallback()) {
callback.Failed(message);
}
}
protected boolean haveCallback() {
return callback != null;
}
/***
* A utility method to wait for channel to close.
* @param channel
* A reference to client channel.
* @param timeoutInMilliSeconds
* Timeout period in milliseconds to wait for.
* @return . * True- if got channel CLOSED within timeout period. . * False- Any other exit reason.
*/
public boolean waitWithTimeout(ClientChannel channel, int timeoutInMilliSeconds) {
boolean fReturn = true;
boolean fQuit = false;
int nStat = 0;
int timeoutInSeconds = timeoutInMilliSeconds / ONE_SECOND_MILI;
for (int i = 0; fQuit == false && i < timeoutInSeconds; i++) {
nStat = channel.waitFor(ClientChannel.CLOSED, ONE_SECOND_MILI);
if ((nStat & ClientChannel.CLOSED) == ClientChannel.CLOSED) {
log.debug("Got channel CLOSED: " + nStat);
fQuit = true;
} else if ((nStat & ClientChannel.EOF) == ClientChannel.EOF) {
log.debug("Got channel EOF: " + nStat);
fReturn = false;
fQuit = true;
}
log.debug("Waiting for: " + i + " seconds.");
}
if ((nStat & ClientChannel.CLOSED) != ClientChannel.CLOSED) {
log.debug("Quit waiting for close. Stat is:" + nStat);
fReturn = false;
}
return fReturn;
}
/***
* A utility method to convert bytes into a printable string.
*
* @param b
* An array of bytes to be printed
* @param fFormat
* Should we use pretty format (add [] for each byte).
* @return A printable string.
*/
public static String byteArrayToHexString(byte[] b, boolean fFormat) {
StringBuffer sb = new StringBuffer(b.length * 2);
for (int i = 0; i < b.length; i++) {
int v = b[i] & 0xff;
if (v < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(v));
if (fFormat && (i < b.length - 1)) {
sb.append(':');
}
}
log.debug("Converted bytes " + b.toString() + "to hexString " + sb.toString().toLowerCase());
return sb.toString().toLowerCase();
}
/**
* Calculate md5sum hash for a given file in a remote machine.
*
* @param strFileName
* : a valid path and file name in the remote machine.
* @return: boolean (success/failure)
* @throws Exception
*/
private String getRemoteHash(String strFileName) {
String strReturn = null;
ClientChannel channel = null;
log.debug("getRemoteHash entry for " + strFileName);
try {
channel =
session.createExecChannel("echo `md5sum " + strFileName + " ; ls -l " + strFileName
+ " ; echo 'lsof:' && lsof | grep " + strFileName + " ; echo 'cat:' && cat " + strFileName
+ "`");
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
channel.setIn(in);
ByteArrayOutputStream out = new ByteArrayOutputStream();
channel.setOut(out);
ByteArrayOutputStream err = new ByteArrayOutputStream();
channel.setErr(err);
channel.open();
if (!waitWithTimeout(channel, maxSSHTimeout)) {
throw new TimeLimitExceededException("Timeout waiting for RemoteHash results!");
}
log.debug("+++++++++++++++++++++debug getRemoteHash: The ByteArrayOutputStream is " + out);
String strOut = new String(out.toByteArray());
log.debug("debug getRemoteHash: The ByteArrayOutputStream.toByteArray() is " + strOut);
String strOutNew = out.toString();
log.debug("debug getRemoteHash: The ByteArrayOutputStream.toString() is " + strOutNew);
strReturn = strOutNew.split(" ")[0];
} catch (Exception e) {
log.error("Error checking remote hash: ", e);
} finally {
if (channel != null) {
channel.close(true);
channel = null;
}
}
log.debug("getRemoteHash return: " + strReturn);
return strReturn;
}
/***
* Consume data from input stream and convert is into a String
*
* @param input
* An input stream.
* @return A string object representing the contents of the given stream.
*/
protected String readInput(InputStream input) {
StringBuilder builder = new StringBuilder();
byte[] tmp = new byte[DEFAULT_BUF_SIZE];
try {
while (input.available() > 0) {
int i = input.read(tmp, 0, DEFAULT_BUF_SIZE);
if (i < 0)
break;
builder.append(new String(tmp, 0, i));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return builder.toString();
}
/***
* Consume String lines from input stream
*
* @param input
* An input stream
* @return A string object representing the contents of the given stream split into lines.
*/
protected String readLineFromInput(InputStream input) {
InputStreamReader isrReader = new InputStreamReader(input);
BufferedReader in = new BufferedReader(isrReader);
StringBuilder builder = new StringBuilder();
try {
while (in.ready()) {
builder.append(in.readLine() + '\n');
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return builder.toString();
}
/**
* Optimized copy source file to destination, based on file size. If destination is a path then source file name is
* appended. If destination file exists then: overwrite=true, destination file is replaced; overwrite=false,
* exception is thrown.
*
* @param src
* source file
* @param dst
* destination file or path
* @param overwrite
* overwrite destination file
* @exception IOException
* I/O problem
* @exception IllegalArgumentException
* illegal argument
*/
public void fastFileCopy(final String srcFile, String dstFile, final boolean overwrite) throws IOException,
IllegalArgumentException {
File src = new File(srcFile);
File dst = new File(dstFile);
// checks
if (!src.isFile() || !src.exists()) {
throw new IllegalArgumentException("Source file '" + src.getAbsolutePath() + "' not found!");
}
if (dst.exists()) {
if (dst.isDirectory()) // Directory? -> use source file name
{
dst = new File(dst, src.getName());
} else if (dst.isFile()) {
if (!overwrite) {
throw new IllegalArgumentException("Destination file '" + dst.getAbsolutePath()
+ "' already exists!");
}
} else {
throw new IllegalArgumentException("Invalid destination object '" + dst.getAbsolutePath() + "'!");
}
}
File dstParent = dst.getParentFile();
if (!dstParent.exists()) {
if (!dstParent.mkdirs()) {
throw new IOException("Failed to create directory " + dstParent.getAbsolutePath());
}
}
long fileSize = src.length();
if (fileSize > TWENTY_MBI) // for larger files (20Mb) use streams
{
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dst);
try {
int doneCnt = -1, bufSize = LARGE_BUF_SIZE;
byte buf[] = new byte[LARGE_BUF_SIZE];
while ((doneCnt = in.read(buf, 0, bufSize)) >= 0) {
if (doneCnt == 0) {
Thread.yield();
} else {
out.write(buf, 0, doneCnt);
}
}
out.flush();
} finally {
try {
in.close();
} catch (IOException e) {
}
try {
out.close();
} catch (IOException e) {
}
}
} else // smaller files, use channels
{
FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dst);
FileChannel in = fis.getChannel(), out = fos.getChannel();
try {
long offs = 0;
long doneCnt = 0;
long copyCnt = Math.min(MAX_FILE_COUNT, fileSize);
do {
doneCnt = in.transferTo(offs, copyCnt, out);
offs += doneCnt;
fileSize -= doneCnt;
} while (fileSize > 0);
} finally // cleanup
{
try {
in.close();
} catch (IOException e) {
}
try {
out.close();
} catch (IOException e) {
}
try {
fis.close();
} catch (IOException e) {
}
try {
fos.close();
} catch (IOException e) {
}
}
} // else smaller files, use channels
}
/***
* A method to extract local machine's IP from remote host. This is needed since local machine may have several
* NIC's and several addresses. So we should be able to tell which address the remote machine sees.
*
* @return Extracted address or null if failed to extract.
*/
private String getLocalIP() {
log.info("getLocalIP entry");
String strOut = null;
try {
ClientChannel channel = session.createExecChannel("echo $SSH_CLIENT | sed 's/ .*$//g'");
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
channel.setIn(in);
channel.setOut(out);
channel.setErr(err);
log.debug("open");
channel.open();
log.debug("wait close");
if (!waitWithTimeout(channel, maxSSHTimeout)) {
throw new TimeLimitExceededException("Timeout waiting for IP results!");
}
strOut = new String(out.toByteArray());
} catch (Exception e) {
log.error("getLocalIP: Unable to get IP: ", e);
}
log.info("getLocalIP return: " + strOut);
return strOut;
}
@Override
public void wrapperShutdown() {
if (session != null) {
session.close(true);
session = null;
}
if (client != null) {
client.stop();
client = null;
}
}
public int getPort() {
return this.nSSHPort;
}
public void setPort(int port) {
this.nSSHPort = port;
}
private void throwTimeout() throws TimeoutException {
throw new TimeoutException("SSH connection timed out connecting to " + host);
}
}