/*******************************************************************************
* Copyright (c) 2013 GigaSpaces Technologies Ltd. All rights reserved
*
* Licensed 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.cloudifysource.esc.installer.filetransfer;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.connection.ConnectionException;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.connection.channel.direct.Session.Command;
import net.schmizz.sshj.transport.TransportException;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
import net.schmizz.sshj.userauth.UserAuthException;
import net.schmizz.sshj.userauth.method.AuthNone;
import net.schmizz.sshj.xfer.FileSystemFile;
import net.schmizz.sshj.xfer.LocalFileFilter;
import net.schmizz.sshj.xfer.LocalSourceFile;
import net.schmizz.sshj.xfer.scp.SCPFileTransfer;
import net.schmizz.sshj.xfer.scp.SCPUploadClient;
import org.apache.commons.lang.StringUtils;
import org.cloudifysource.esc.installer.InstallationDetails;
import org.cloudifysource.esc.installer.InstallerException;
/********
* A file transfer implementation using Secure Copy (SCP), based on the sshj library.
*
* @author barakme
* @since 2.5.0
*
*/
public class ScpFileTransfer implements FileTransfer {
private static final String CREATE_REMOTE_DIRECTORY_WITH_DELETE =
"if [ -d {0} ]; then rm -rf {0}; fi; mkdir -p {0}";
private static final String CREATE_REMOTE_DIRECTORY = "if [ ! -d {0} ]; then mkdir -p {0}; fi";
private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(ScpFileTransfer.class
.getName());
private String host;
private String localDirPath;
private boolean deleteRemoteDirectoryContents;
@Override
public void copyFiles(final InstallationDetails details, final Set<String> excludedFiles,
final List<File> additionalFiles,
final long endTimeMillis) throws TimeoutException, InstallerException {
final SSHClient ssh = new SSHClient();
ssh.addHostKeyVerifier(new PromiscuousVerifier());
try {
ssh.connect(host);
} catch (final IOException e) {
try {
ssh.close();
} catch (IOException e1) {
logger.log(Level.SEVERE, "Failed to close down ssh client after connection to: " + host
+ " failed. Error was: " + e.getMessage(), e);
}
throw new InstallerException("Failed to connect to host: " + host + ": " + e.getMessage(), e);
}
try {
try {
if (!StringUtils.isEmpty(details.getKeyFile())) {
final File keyFile = new File(details.getKeyFile());
if (!keyFile.exists() || !keyFile.isFile()) {
throw new InstallerException("Expected to find key file at: " + keyFile.getAbsolutePath());
}
ssh.authPublickey(details.getUsername(), keyFile.getAbsolutePath());
} else if (!StringUtils.isEmpty(details.getPassword())) {
ssh.authPassword(details.getUsername(), details.getPassword());
} else {
ssh.auth(details.getUsername(), new AuthNone());
}
} catch (final TransportException e) {
throw new InstallerException("Failed to authenticate to remote server: " + e.getMessage(), e);
} catch (final UserAuthException e) {
throw new InstallerException("Failed to authenticate to remote server: " + e.getMessage(), e);
}
// First, we need to create the remote directory
createRemoteDirectory(details, endTimeMillis, ssh);
// Note: With SCP, we do not delete the contents of the remote directory! Use SFTP if y ou want this
// feature.
final SCPFileTransfer transfer = ssh.newSCPFileTransfer();
try {
final SCPUploadClient uploadClient = transfer.newSCPUploadClient();
final File localDirectory = new File(this.localDirPath);
final String localDirectoryPath = localDirectory.getCanonicalPath();
// Filter out excluded files
uploadClient.setUploadFilter(new LocalFileFilter() {
@Override
public boolean accept(final LocalSourceFile originalFile) {
final FileSystemFile file = (FileSystemFile) originalFile;
final File localFile = file.getFile();
String localFilePath;
try {
localFilePath = localFile.getCanonicalPath();
} catch (final IOException e) {
throw new IllegalStateException("Failed to get canonical path of: " + localFile, e);
}
if (!localFilePath.startsWith(localDirectoryPath)) {
throw new IllegalStateException("Upload candidate file: " + localFilePath
+ " is not a descendant of local directory: " + localDirectoryPath);
}
String relativePath = localFilePath.substring(localDirectoryPath.length());
// remote trailing separator
if (relativePath.startsWith(File.separator)) {
relativePath = relativePath.substring(1);
}
// convert windows separator to linux
relativePath = relativePath.replace("\\", "/");
// this condition will never be satisfied and should be removed.
if (excludedFiles.contains(relativePath)) {
return false;
}
return true;
}
});
// uploading local directory to remote directory
File[] files = localDirectory.listFiles();
if (files != null) {
for (File file : files) {
if (!excludedFiles.contains(file.getName())) {
uploadClient.copy(new FileSystemFile(file), details.getRemoteDir());
}
}
}
// uploading additional files to remote directory
for (File file : additionalFiles) {
transfer.upload(new FileSystemFile(file), details.getRemoteDir());
}
} catch (final IOException e) {
throw new InstallerException("Failed to upload files to remote server: " + e.getMessage(), e);
}
} finally {
try {
ssh.disconnect();
} catch (final IOException e) {
logger.log(Level.WARNING, "Failed to disconnect ssh session", e);
}
}
}
private void createRemoteDirectory(final InstallationDetails details, final long endTimeMillis, final SSHClient ssh)
throws InstallerException {
Session session = null;
try {
session = ssh.startSession();
session.allocateDefaultPTY();
final long timeout = endTimeMillis - System.currentTimeMillis();
String commandString = createCommandString(details);
Command command = session.exec(commandString);
final String commandOutput = IOUtils.readFully(command.getInputStream()).toString();
command.join((int) timeout, TimeUnit.MILLISECONDS);
Integer exitStatus = command.getExitStatus();
if (exitStatus == null) {
throw new InstallerException("Remote command to create directory did not return");
} else {
if (exitStatus != 0) {
throw new InstallerException("Failed to create remote directory: " + details.getRemoteDir()
+ ". Command output was: " + commandOutput);
}
}
} catch (IOException e) {
throw new InstallerException("Failed to create remote directory: " + details.getRemoteDir()
+ ". Error was: " + e.getMessage(), e);
} finally {
if (session != null) {
try {
session.close();
} catch (TransportException e) {
logger.log(Level.WARNING, "Failed to close ssh session while creating remote directory", e);
} catch (ConnectionException e) {
logger.log(Level.WARNING, "Failed to close ssh session while creating remote directory", e);
}
}
}
}
private String createCommandString(final InstallationDetails details) {
if (this.deleteRemoteDirectoryContents) {
return MessageFormat.format(CREATE_REMOTE_DIRECTORY_WITH_DELETE,
details.getRemoteDir());
} else {
return MessageFormat.format(CREATE_REMOTE_DIRECTORY,
details.getRemoteDir());
}
}
@Override
public void initialize(final InstallationDetails details, final long endTimeMillis) throws TimeoutException,
InstallerException {
// TODO - this code should be in the installer, and not copied in each of the file transfer implementations!
this.deleteRemoteDirectoryContents = details.isDeleteRemoteDirectoryContents();
if (details.isConnectedToPrivateIp()) {
host = details.getPrivateIp();
} else {
host = details.getPublicIp();
}
// when bootstrapping a management machine, pass all of the cloud
// configuration, including all template
// for an agent machine, just pass the upload dir fot the specific
// template.
localDirPath = details.getLocalDir();
if (details.isManagement()) {
if (details.getCloudFile() == null) {
throw new IllegalArgumentException("While bootstrapping a management machine, cloud file is null");
}
localDirPath = details.getCloudFile().getParentFile().getAbsolutePath();
}
}
}