/*******************************************************************************
* Copyright (c) 2011 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;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.cloudifysource.domain.cloud.CloudTemplateInstallerConfiguration;
import org.cloudifysource.dsl.utils.IPUtils;
import org.cloudifysource.esc.installer.filetransfer.FileTransfer;
import org.cloudifysource.esc.installer.filetransfer.FileTransferFactory;
import org.cloudifysource.esc.installer.remoteExec.RemoteExecutor;
import org.cloudifysource.esc.installer.remoteExec.RemoteExecutorFactory;
import org.cloudifysource.esc.util.CalcUtils;
import org.cloudifysource.esc.util.Utils;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
/************
* The agentless installer class is responsible for installing Cloudify on a remote machine, using only SSH. It will
* upload all relevant files and start the Cloudify agent.
*
* File transfer is handled using Apache commons vfs.
*
* @author barakme
*
*/
public class AgentlessInstaller {
@Override
public String toString() {
return "NewAgentlessInstaller [eventsListenersList=" + eventsListenersList + "]";
}
private static final String LINUX_STARTUP_SCRIPT_NAME = "bootstrap-management.sh";
private static final String POWERSHELL_STARTUP_SCRIPT_NAME = "bootstrap-management.bat";
private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(AgentlessInstaller.class
.getName());
private final List<AgentlessInstallerListener> eventsListenersList = new LinkedList<AgentlessInstallerListener>();
// Set this field to override the default environment file builder with a custom one.
private String environmentFileContents = null;
/******
* Name of the logger used for piping out ssh output.
*/
public static final String SSH_OUTPUT_LOGGER_NAME = AgentlessInstaller.class.getName() + ".ssh.output";
/********
* Name of the internal logger used by the ssh component.
*/
public static final String SSH_LOGGER_NAME = "com.jcraft.jsch";
/***********
* Constructor.
*/
public AgentlessInstaller() {
final Logger sshLogger = Logger.getLogger(SSH_LOGGER_NAME);
com.jcraft.jsch.JSch.setLogger(new JschJdkLogger(sshLogger));
}
private static InetAddress waitForRoute(final CloudTemplateInstallerConfiguration installerConfiguration,
final String ip, final long endTime) throws InstallerException, InterruptedException {
Exception lastException = null;
while (System.currentTimeMillis() < endTime) {
try {
return InetAddress.getByName(ip);
} catch (final IOException e) {
lastException = e;
}
Thread.sleep(installerConfiguration.getConnectionTestIntervalMillis());
}
throw new InstallerException("Failed to resolve installation target: " + ip, lastException);
}
/*******
* Checks if a TCP connection to a remote machine and port is possible.
*
* @param ip
* remote machine ip.
* @param port
* remote machine port.
* @param installerConfiguration
* .
* @param timeout
* duration to wait for successful connection.
* @param unit
* time unit to wait.
* @throws InstallerException .
* @throws TimeoutException .
* @throws InterruptedException .
*/
public static void checkConnection(final String ip, final int port,
final CloudTemplateInstallerConfiguration installerConfiguration,
final long timeout, final TimeUnit unit)
throws TimeoutException, InterruptedException, InstallerException {
final long end = System.currentTimeMillis() + unit.toMillis(timeout);
final InetAddress inetAddress = waitForRoute(installerConfiguration, ip,
Math.min(end, System.currentTimeMillis()
+ installerConfiguration.getConnectionTestRouteResolutionTimeoutMillis()));
final InetSocketAddress socketAddress = new InetSocketAddress(inetAddress, port);
logger.fine("Checking connection to: " + socketAddress);
while (System.currentTimeMillis() + installerConfiguration.getConnectionTestIntervalMillis() < end) {
// need to sleep since sock.connect may return immediately, and
// server may take time to start
Thread.sleep(installerConfiguration.getConnectionTestIntervalMillis());
final Socket sock = new Socket();
try {
sock.connect(socketAddress, installerConfiguration.getConnectionTestConnectTimeoutMillis());
return;
} catch (final IOException e) {
// retry
} finally {
if (sock != null) {
try {
sock.close();
} catch (final IOException e) {
logger.fine("Failed to close socket");
}
}
}
}
//timeout was reached
String ipAddress = inetAddress.getHostAddress(); //if resolving fails we don't reach this line
throw new TimeoutException("Failed connecting to " + IPUtils.getSafeIpAddress(ipAddress) + ":" + port);
}
/******
* Performs installation on a remote machine with a known IP.
*
* @param details
* the installation details.
* @param timeout
* the timeout duration.
* @param unit
* the timeout unit.
* @throws InterruptedException .
* @throws TimeoutException .
* @throws InstallerException .
*/
public void installOnMachineWithIP(final InstallationDetails details, final long timeout, final TimeUnit unit)
throws TimeoutException, InterruptedException, InstallerException {
final long end = System.currentTimeMillis() + unit.toMillis(timeout);
if (details.getLocator() == null) {
// We are installing the lus now
details.setLocator(details.getPrivateIp());
}
logger.fine("Executing agentless installer with the following details:\n" + details.toString());
// this is the right way to get the target, but the naming is off.
final String targetHost = details.isConnectedToPrivateIp() ? details.getPrivateIp() : details.getPublicIp();
if (StringUtils.isBlank(targetHost)) {
throw new InstallerException("Target host is blank. Connect to private: "
+ details.isConnectedToPrivateIp() + ", Private IP: " + details.getPrivateIp() + ", Public IP: "
+ details.getPublicIp() + ". Details: " + details);
}
final int port = Utils.getFileTransferPort(details.getInstallerConfiguration(), details.getFileTransferMode());
publishEvent("attempting_to_access_vm", targetHost);
logger.fine("Checking connection with target host " + targetHost);
checkConnection(targetHost, port, details.getInstallerConfiguration(), CalcUtils.millisUntil(end),
TimeUnit.MILLISECONDS);
File environmentFile = null;
// create the environment file
try {
environmentFile = createEnvironmentFile(details);
// upload bootstrap files
publishEvent("uploading_files_to_node", targetHost);
uploadFilesToServer(details, environmentFile, end, targetHost);
} catch (final IOException e) {
throw new InstallerException("Failed to create environment file", e);
} finally {
// delete the temp directory and temp env file.
if (environmentFile != null) {
FileUtils.deleteQuietly(environmentFile.getParentFile());
}
}
// launch the cloudify agent
publishEvent("launching_agent_on_node", targetHost);
remoteExecuteAgentOnServer(details, end, targetHost);
publishEvent("install_completed_on_node", targetHost);
}
private File createEnvironmentFile(final InstallationDetails details) throws IOException {
String fileContents = null;
final EnvironmentFileBuilder builder = new EnvironmentFileBuilder(details.getScriptLanguage(),
details.getExtraRemoteEnvironmentVariables());
if (this.environmentFileContents == null) {
builder.loadEnvironmentFileFromDetails(details);
final String generatedFileContents = builder.build().toString();
fileContents = generatedFileContents;
} else {
fileContents = this.environmentFileContents;
}
final File tempFolder = Utils.createTempFolder();
final File tempFile = new File(tempFolder, builder.getEnvironmentFileName());
tempFile.deleteOnExit();
FileUtils.writeStringToFile(tempFile, fileContents);
if (logger.isLoggable(Level.FINE)) {
logger.fine("Created environment file with the following contents: " + fileContents);
}
return tempFile;
}
private void remoteExecuteAgentOnServer(final InstallationDetails details, final long end, final String targetHost)
throws InstallerException, TimeoutException, InterruptedException {
// get script for execution mode
final String scriptFileName = getScriptFileName(details);
String remoteDirectory = details.getRemoteDir();
if (remoteDirectory.endsWith("/")) {
remoteDirectory = remoteDirectory.substring(0, remoteDirectory.length() - 1);
}
if (details.isManagement()) {
// add the relative path to the cloud file location
remoteDirectory = remoteDirectory + "/" + details.getRelativeLocalDir();
}
final String scriptPath = remoteDirectory + "/" + scriptFileName;
final RemoteExecutor remoteExecutor =
RemoteExecutorFactory.createRemoteExecutorProvider(details.getRemoteExecutionMode());
logger.fine("Initializing remote executor " + remoteExecutor);
remoteExecutor.initialize(this, details);
remoteExecutor.execute(targetHost, details, scriptPath, end);
return;
}
private String getScriptFileName(final InstallationDetails details) {
final String scriptFileName;
switch (details.getScriptLanguage()) {
case WINDOWS_BATCH:
scriptFileName = POWERSHELL_STARTUP_SCRIPT_NAME;
break;
case LINUX_SHELL:
scriptFileName = LINUX_STARTUP_SCRIPT_NAME;
break;
default:
throw new UnsupportedOperationException("Remote Execution Mode: " + details.getRemoteExecutionMode()
+ " not supported");
}
return scriptFileName;
}
private void uploadFilesToServer(final InstallationDetails details, final File environmentFile, final long end,
final String targetHost)
throws TimeoutException, InstallerException, InterruptedException {
final Set<String> excludedFiles = new HashSet<String>();
if (!details.isManagement() && details.getManagementOnlyFiles() != null) {
excludedFiles.addAll(Arrays.asList(details.getManagementOnlyFiles()));
}
final FileTransfer fileTransfer = FileTransferFactory.getFileTrasnferProvider(details.getFileTransferMode());
fileTransfer.initialize(details, end);
fileTransfer.copyFiles(details, excludedFiles, Arrays.asList(environmentFile), end);
}
/**********
* Registers an event listener for installation events.
*
* @param listener
* the listener.
*/
public void addListener(final AgentlessInstallerListener listener) {
this.eventsListenersList.add(listener);
}
/*********
* This method is public so that implementation classes for file copy and remote execution can publish events.
*
* @param eventName
* .
* @param args
* .
*/
public void publishEvent(final String eventName, final Object... args) {
for (final AgentlessInstallerListener listner : this.eventsListenersList) {
try {
listner.onInstallerEvent(eventName, args);
} catch (final Exception e) {
logger.log(Level.FINE, "Exception in listener while publishing event: " + eventName
+ " with arguments: " + Arrays.toString(args), e);
}
}
}
public String getEnvironmentFileContents() {
return environmentFileContents;
}
public void setEnvironmentFileContents(final String environmentFileContents) {
this.environmentFileContents = environmentFileContents;
}
}