/*
* RHQ Management Platform
* Copyright (C) 2005-2014 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.enterprise.server.install.remote;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Properties;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.clientapi.server.core.AgentRegistrationRequest;
import org.rhq.core.domain.install.remote.AgentInstallInfo;
import org.rhq.core.domain.install.remote.AgentInstallStep;
import org.rhq.core.domain.install.remote.CustomAgentInstallData;
import org.rhq.core.domain.install.remote.RemoteAccessInfo;
import org.rhq.core.domain.install.remote.SSHSecurityException;
import org.rhq.enterprise.server.util.LookupUtil;
/**
* A utility object that is used to install, start and stop agents remotely over SSH.
*
* @author Greg Hinkle
* @author John Mazzitelli
*/
public class SSHInstallUtility {
static class Credentials {
private String username;
private String password;
public Credentials () {
}
public Credentials (String username, String password) {
setUsername(username);
setPassword(password);
}
public String getUsername() {
return this.username;
}
public void setUsername(String u) {
this.username = u;
}
public String getPassword() {
return this.password;
}
public void setPassword(String p) {
this.password = p;
}
}
static class SSHConfiguration {
public static enum StrictHostKeyChecking {
yes, no, ask
};
private StrictHostKeyChecking strictHostKeyChecking = StrictHostKeyChecking.ask;
private String knownHostsFile = null;
public SSHConfiguration() {
}
public StrictHostKeyChecking getStrictHostKeyChecking() {
return strictHostKeyChecking;
}
public void setStrictHostKeyChecking(StrictHostKeyChecking strictHostKeyChecking) {
this.strictHostKeyChecking = strictHostKeyChecking;
}
public String getKnownHostsFile() {
return knownHostsFile;
}
public void setKnownHostsFile(String knownHostsFile) {
this.knownHostsFile = knownHostsFile;
}
}
private class SSHUserInfo implements UserInfo {
@Override
public void showMessage(String msg) {
//System.out.println(msg);
}
@Override
public boolean promptYesNo(String ques) {
// this is asking either to add the fingerprint for an unknown host or, more troubling, to replace
// a known host's fingerprint. If we were told to authorize this host, then accept both.
// If we need to do separate processing for either conditions, we can see which question it is via:
// ques.matches("(?s).*authenticity of host.*can't be established.*Are you sure you want to continue connecting.*")
// and
// ques.matches("(?s).*NASTY.*")
if (accessInfo.isHostAuthorized()) {
return true;
}
throw new SSHSecurityException(ques);
}
@Override
public boolean promptPassword(String arg0) {
return false;
}
@Override
public boolean promptPassphrase(String arg0) {
return false;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getPassphrase() {
return null;
}
};
public static final String AGENT_STATUS_NOT_INSTALLED = "Agent Not Installed";
private static final String RHQ_AGENT_LATEST_VERSION_PROP = "rhq-agent.latest.version";
private static final int DEFAULT_BUFFER_SIZE = 4096;
private static final int CONNECTION_TIMEOUT = 30000;
private static final long TIMEOUT = 30000L;
private static final long POLL_TIMEOUT = 1000L;
private Log log = LogFactory.getLog(SSHInstallUtility.class);
private final RemoteAccessInfo accessInfo;
private final Credentials defaultCredentials;
private final SSHConfiguration sshConfiguration;
private Session session;
public SSHInstallUtility(RemoteAccessInfo accessInfo, Credentials defaultCredentials, SSHConfiguration sshConfig) {
this.accessInfo = accessInfo;
this.defaultCredentials = defaultCredentials;
if (sshConfig == null) {
sshConfig = new SSHConfiguration();
}
this.sshConfiguration = sshConfig;
connect();
}
public SSHInstallUtility(RemoteAccessInfo accessInfo) {
this(accessInfo, null, null);
}
public RemoteAccessInfo getRemoteAccessInfo() {
return this.accessInfo;
}
public void connect() {
try {
JSch jsch = new JSch();
if (sshConfiguration.getKnownHostsFile() != null) {
jsch.setKnownHosts(sshConfiguration.getKnownHostsFile());
}
//if (accessInfo.getKey() != null) {
// jsch.addIdentity(...);
//}
Credentials credentials = getCredentialsToUse();
session = jsch.getSession(credentials.getUsername(), accessInfo.getHost(), accessInfo.getPort());
if (credentials.getPassword() != null) {
session.setPassword(credentials.getPassword());
}
if (sshConfiguration.getStrictHostKeyChecking() != null) {
Properties config = new Properties();
config.put("StrictHostKeyChecking", sshConfiguration.getStrictHostKeyChecking().name());
session.setConfig(config);
}
session.setUserInfo(new SSHUserInfo());
session.connect(CONNECTION_TIMEOUT); // making a connection with timeout.
} catch (JSchException e) {
throw new RuntimeException("Failed SSH connection", e);
}
}
public void disconnect() {
session.disconnect();
}
public boolean isConnected() {
return session.isConnected();
}
public boolean agentInstallCheck(String agentInstallPath) {
String agentWrapperScript = buildAgentWrapperScriptPath(agentInstallPath);
String value = executeCommand("if [ -f '" + agentWrapperScript + "' ]; then echo \"exists\"; fi",
"Agent Install Check");
if (value == null || value.trim().length() == 0) {
return false;
} else {
return true;
}
}
public AgentInstallInfo installAgent(CustomAgentInstallData customData, String installId) {
String agentFile;
String agentPath;
String agentVersion;
// get information about the agent distro file
try {
File agentBinaryFile = LookupUtil.getAgentManager().getAgentUpdateBinaryFile();
agentFile = agentBinaryFile.getName();
agentPath = agentBinaryFile.getCanonicalPath();
Properties props = LookupUtil.getAgentManager().getAgentUpdateVersionFileContent();
agentVersion = props.getProperty(RHQ_AGENT_LATEST_VERSION_PROP);
} catch (Exception e) {
agentVersion = getClass().getPackage().getImplementationVersion();
agentFile = "rhq-enterprise-agent-" + agentVersion + ".jar";
agentPath = "/tmp/rhq-agent/" + agentFile;
log.warn("Failed agent binary file lookup - using [" + agentPath + "]", e);
}
if (!new File(agentPath).exists()) {
throw new RuntimeException("Unable to find agent binary file for installation at [" + agentPath + "]");
}
// confirm that we still have the custom files the user was supposed to have file uploaded
if (customData.getAgentConfigurationXmlFile() != null) {
if (!new File(customData.getAgentConfigurationXmlFile()).exists()) {
throw new RuntimeException("Unable to find custom agent config file at ["
+ customData.getAgentConfigurationXmlFile() + "]");
}
}
if (customData.getRhqAgentEnvFile() != null) {
if (!new File(customData.getRhqAgentEnvFile()).exists()) {
throw new RuntimeException("Unable to find custom agent environment script file at ["
+ customData.getRhqAgentEnvFile() + "]");
}
}
// do the install work
String parentPath = customData.getParentPath();
Credentials credentials = getCredentialsToUse();
String serverAddress = LookupUtil.getServerManager().getServer().getAddress();
AgentInstallInfo info = new AgentInstallInfo(parentPath, credentials.getUsername(), agentVersion,
serverAddress, accessInfo.getHost());
executeCommand("uname -a", "Machine uname", info);
executeCommand("java -version", "Java Version Check", info);
executeCommand("mkdir -p '" + parentPath + "'", "Create Agent Install Directory", info);
executeCommand("rm -rf '" + parentPath + "/rhq-agent'", "Remove any previously installed agent", info);
executeCommand("rm -f '" + parentPath + "/rhq-agent-update.log'", "Remove any old agent update logs", info);
executeCommand("rm -f " + parentPath.replace(" ", "\\ ") + "/rhq-enterprise-agent*.jar",
"Remove any old agent update binary jars", info); // because we use * wildcard, can't wrap in quotes, so escape spaces if there are any in the path
log.info("Copying agent binary update distribution file to [" + accessInfo.getHost() + "]...");
AgentInstallStep scpStep = sendFile(agentPath, parentPath, "Remote copy the agent binary update distribution");
info.addStep(scpStep);
if(scpStep.getResultCode() != 0) {
return info; // abort and return what we did - no sense continuing if the agent distro failed to copy
}
log.info("Agent binary update distribution file copied");
executeCommand("cd '" + parentPath + "' ; " + "java -jar '" + parentPath + "/" + agentFile + "' '--install="
+ parentPath + "'", "Install Agent", info);
String agentConfigXmlFilename = parentPath + "/rhq-agent/conf/agent-configuration.xml";
if (customData.getAgentConfigurationXmlFile() != null) {
log.info("Copying custom agent configuration file...");
AgentInstallStep step = sendFile(customData.getAgentConfigurationXmlFile(), agentConfigXmlFilename, "Remote copy the agent configuration file");
info.addStep(step);
if(step.getResultCode() != 0) {
return info; // abort and return what we did - no sense continuing if the custom config file failed to copy
}
log.info("Custom agent configuration file copied.");
// tell the info object - this is needed so it adds the --config command line option
info.setCustomAgentConfigurationFile("agent-configuration.xml");
}
// try to see if we can figure out what the port will be that the agent will bind to
// this will use awk to find a line in the agent config xml that matches this:
// <entry key="rhq.communications.connector.bind-port" value="16163" />
// where we use " as the field separator and the port number will be the fourth field.
String agentPortAwkCommand = "awk '-F\"' '/key.*=.*" + AgentInstallInfo.AGENT_PORT_PROP + "/ {print $4}' "
+ "'" + agentConfigXmlFilename + "'";
String portStr = executeCommand(agentPortAwkCommand, "Determine the agent's bind port", info);
try {
int port = Integer.parseInt(portStr.trim());
info.setAgentPort(port);
} catch (Exception e) {
info.setAgentPort(0); // indicate that we don't know it
}
if (customData.getRhqAgentEnvFile() != null) {
log.info("Copying custom agent environment script...");
String destFilename = parentPath + "/rhq-agent/bin/rhq-agent-env.sh";
AgentInstallStep step = sendFile(customData.getRhqAgentEnvFile(), destFilename, "Remote copy the agent environment script file");
info.addStep(step);
if (step.getResultCode() != 0) {
return info; // abort and return what we did - no sense continuing if the custom env script file failed to copy
}
log.info("Custom agent environment script copied.");
}
// Do a quick check to see if there is something already listening on the agent's port.
long start = System.currentTimeMillis();
Boolean squatterCheck = checkAgentConnection(info, 1);
if (squatterCheck != null) { // if this is null, we weren't even able to check
if (squatterCheck.booleanValue()) {
AgentInstallStep step = new AgentInstallStep("ping " + info.getAgentAddress() + ":"
+ info.getAgentPort(), "See if anything has already taken the agent port", 1,
"Port already in use", getTimeDiff(start));
info.addStep(step);
return info; // abort, don't install an agent if something is already squatting on its port
} else {
AgentInstallStep step = new AgentInstallStep("ping " + info.getAgentAddress() + ":"
+ info.getAgentPort(), "See if anything has already taken the agent port", 0, "Port free",
getTimeDiff(start));
info.addStep(step);
}
}
log.info("Will start new agent @ [" + accessInfo.getHost() + "] pointing to server @ [" + serverAddress + "]");
String agentScript = parentPath + "/rhq-agent/bin/rhq-agent.sh"; // NOTE: NOT the wrapper script
String startStringArgs = info.getConfigurationStartString();
// this ID will be used by the agent when it registered, thus allowing the server to link this install with that agent
if (installId != null) {
startStringArgs += " -D" + AgentRegistrationRequest.SYSPROP_INSTALL_ID + "=" + installId;
}
// Tell the script to store a pid file to make the wrapper script work
String envCmd1 = "RHQ_AGENT_IN_BACKGROUND='" + parentPath + "/rhq-agent/bin/rhq-agent.pid'";
String envCmd2 = "export RHQ_AGENT_IN_BACKGROUND";
String startCommand = envCmd1 + " ; " + envCmd2 + " ; nohup '" + agentScript + "' " + startStringArgs + " &";
executeCommand(startCommand, "Start New Agent", info);
// see if we can confirm the agent connection now
Boolean pingResults = checkAgentConnection(info, 5);
if (pingResults == null) {
log.warn("Just installed an agent at [" + info.getAgentAddress()
+ "] but could not determine its port. No validation check will be made.");
} else if (!pingResults.booleanValue()) {
log.warn("Just installed an agent at [" + info.getAgentAddress()
+ "] but could not ping its port. Something might be bad with the install or it is behind a firewall.");
}
return info;
}
private AgentInstallStep sendFile(String sourceFilename, String destFilename, String description) {
long start = System.currentTimeMillis();
int scpReturnCode = 0;
String scpMessage = "Success";
try {
SSHFileSend.sendFile(session, sourceFilename, destFilename);
} catch (IOException e) {
scpReturnCode = 1;
scpMessage = e.getMessage();
} catch (JSchException e) {
scpReturnCode = 1;
scpMessage = e.getMessage();
}
AgentInstallStep step = new AgentInstallStep("ssh copy '" + sourceFilename
+ "' -> '" + destFilename + "'", description, scpReturnCode,
scpMessage, getTimeDiff(start));
return step;
}
/**
* Checks if the agent's host/port can be connected to via a TCP socket.
* This will set the given info's "ConfirmedAgentConnection" attribute as well as return it.
*
* @param info information on the agent endpoint; its confirmed-agent-connection flag will be set
* @param retries number of times to try to connect before aborting (it will set the flag to false and return false when it aborts)
* @return the flag to indicate if the agent endpoint was able to be successfully connected to (could be null
* if the agent port was not known and thus the connection attempt was never made).
*/
private Boolean checkAgentConnection(AgentInstallInfo info, int retries) {
if (info.getAgentPort() > 0) {
info.setConfirmedAgentConnection(false);
for (int attempt = 0; attempt < retries && !info.isConfirmedAgentConnection(); attempt++) {
Socket ping = new Socket();
try {
ping.connect(new InetSocketAddress(info.getAgentAddress(), info.getAgentPort()), 5000);
info.setConfirmedAgentConnection(ping.isConnected());
} catch (Exception e) {
info.setConfirmedAgentConnection(false);
} finally {
try {
ping.close();
} catch (Exception ignore) {
}
}
}
} else {
info.setConfirmedAgentConnection(false);
}
return info.isConfirmedAgentConnection();
}
public String uninstallAgent(String doomedPath) {
String theRealDoomedPath = findAgentInstallPath(doomedPath); // make sure we are looking at an agent
if (theRealDoomedPath != null) {
// if the agent is still running, make sure we stop it
stopAgent(theRealDoomedPath);
// Before removing the agent dir, remove these first. Since we use ".." it requires the parent to exist
executeCommand("rm -f '" + theRealDoomedPath + "/../rhq-agent-update.log'", "Remove old agent update logs");
executeCommand("rm -f " + theRealDoomedPath.replace(" ", "\\ ") + "/../rhq-enterprise-agent*.jar",
"Remove old agent update binary jars"); // because we use * wildcard, can't wrap in quotes, so escape spaces if there are any in the path
// now remove the actual agent dir
String results = executeCommand("rm -rf '" + theRealDoomedPath + "'", "Uninstall Agent");
return results;
} else {
log.warn("Asked to uninstall an agent from [" + accessInfo.getHost() + ":" + doomedPath
+ "] but there does not appear to be an agent there. Skipping the attempt to remove any files.");
return "There does not appear to be an agent installed here: " + accessInfo.getHost() + ":" + doomedPath;
}
}
public String startAgent(String agentInstallPath) {
String agentWrapperScript = buildAgentWrapperScriptPath(agentInstallPath);
return executeCommand("'" + agentWrapperScript + "' start", "Agent Start");
}
public String stopAgent(String agentInstallPath) {
String agentWrapperScript = buildAgentWrapperScriptPath(agentInstallPath);
return executeCommand("'" + agentWrapperScript + "' stop", "Agent Stop");
}
public String agentStatus(String agentInstallPath) {
String agentWrapperScript = buildAgentWrapperScriptPath(agentInstallPath);
if (!agentInstallCheck(agentInstallPath)) {
return AGENT_STATUS_NOT_INSTALLED;
}
return executeCommand("'" + agentWrapperScript + "' status", "Agent Status");
}
public String findAgentInstallPath(String parentPath) {
if (parentPath == null || parentPath.trim().length() == 0) {
// user doesn't know where the agent might be - let's try to guess
String[] possiblePaths = new String[] { "/opt", "/usr/local", "/usr/share", "/rhq",
"/home/" + getCredentialsToUse().getUsername() };
for (String possiblePath : possiblePaths) {
String path = findAgentInstallPath(possiblePath);
if (path != null) {
return path;
}
}
return null;
}
if (parentPath.endsWith("/rhq-agent") || parentPath.endsWith("/rhq-agent/")) {
// strip "rhq-agent" so we look to see if its really there in the parent
// we can't use java.io.File for this because we might be running on a Windows box - don't forget, we are ssh'ing into a remote box
parentPath = parentPath.substring(0, parentPath.lastIndexOf("/rhq-agent"));
}
String findOutput;
try {
findOutput = executeCommand("find '" + parentPath + "' -maxdepth 4 -name rhq-agent -print"); // don't call the other execute methods, we want to be able to catch the exception here
} catch (ExecuteException e) {
// It is possible the 'find' returned a non-zero exit code because some subdirectories were unreadable.
// Ignore that and just analyze the files that 'find' did return.
findOutput = e.stdout;
}
if (findOutput == null || findOutput.trim().length() == 0) {
return null;
}
String[] results = findOutput.split("\n");
for (String result : results) {
if (result.contains("/.java/")) {
continue; // ignore the rhq-agent Java Preference node - we know that's not an agent
}
return result; // just return the first place we find that looks like an agent
}
return null; // nothing looks like an agent
}
public String[] pathDiscovery(String parentPath) {
String full = executeCommand("ls -1 '" + parentPath + "'", "Path Discovery");
return full.split("\n");
}
private Credentials getCredentialsToUse() {
String user = accessInfo.getUser();
if ((user == null || user.length() == 0) && this.defaultCredentials != null) {
user = this.defaultCredentials.getUsername();
}
String pw = accessInfo.getPassword();
if ((pw == null || pw.length() == 0) && this.defaultCredentials != null) {
pw = this.defaultCredentials.getPassword();
}
Credentials creds = new Credentials(user, pw);
return creds;
}
private String buildAgentWrapperScriptPath(String agentInstallPath) {
// its possible the caller is giving us the parent install directory, whereas we
// want the child "rhq-agent" directory. Our find method will take care of this
// and return the path we want - if it doesn't, just use the path the user gave us
// and let the chips fall where they may
String foundAgentInstall = findAgentInstallPath(agentInstallPath);
if (foundAgentInstall != null) {
agentInstallPath = foundAgentInstall;
}
String agentWrapperScript = agentInstallPath + "/bin/rhq-agent-wrapper.sh";
return agentWrapperScript;
}
private String executeCommand(String command, String description) {
return executeCommand(command, description, new AgentInstallInfo());
}
private String executeCommand(String command, String description, AgentInstallInfo info) {
log.info("Running SSH command [" + description + "]");
long start = System.currentTimeMillis();
String result = null;
try {
result = executeCommand(command);
info.addStep(new AgentInstallStep(command, description, 0, result, getTimeDiff(start)));
} catch (ExecuteException e) {
info.addStep(new AgentInstallStep(command, description, e.errorCode, e.getMessage(), getTimeDiff(start)));
}
log.info("Result of SSH command [" + description + "]: " + result);
return result;
}
private String executeCommand(String command) throws ExecuteException {
ChannelExec channel = null;
int exitStatus = -1;
InputStream is = null;
InputStream es = null;
try {
channel = (ChannelExec) session.openChannel("exec");
channel.setCommand(command);
is = channel.getInputStream();
es = channel.getErrStream();
channel.connect(CONNECTION_TIMEOUT); // connect and execute command
String out = read(is, channel);
String err = read(es, channel);
if (log.isTraceEnabled()) {
log.trace("SSH command output: " + out);
}
if (err.length() > 0) {
exitStatus = channel.getExitStatus();
if (log.isTraceEnabled()) {
log.trace("SSH command error [" + exitStatus + "]: " + err);
}
if (exitStatus != 0) {
throw new ExecuteException(exitStatus, err, out);
} else if (out.length() == 0) {
return err;
}
} else {
exitStatus = 0;
}
return out;
} catch (ExecuteException ee) {
throw ee;
} catch (Exception e) {
throw new ExecuteException(exitStatus, e.toString());
} finally {
if (is != null) {
try {
is.close();
} catch (Exception e) {
}
}
if (es != null) {
try {
es.close();
} catch (Exception e) {
}
}
if (channel != null) {
try {
channel.disconnect();
} catch (Exception e) {
log.error("Failed to disconnect", e);
}
}
}
}
private String read(InputStream is, Channel channel) throws IOException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
final long endTime = System.currentTimeMillis() + TIMEOUT;
while (System.currentTimeMillis() < endTime) {
while (is.available() > 0) {
int count = is.read(buffer, 0, DEFAULT_BUFFER_SIZE);
if (count >= 0) {
bos.write(buffer, 0, count);
} else {
break;
}
}
if (channel.isClosed()) {
if (log.isDebugEnabled()) {
log.debug("SSH reading exit status=" + channel.getExitStatus());
}
break;
}
try {
Thread.sleep(POLL_TIMEOUT);
} catch (InterruptedException e) {
}
}
return bos.toString();
}
private long getTimeDiff(long start) {
return System.currentTimeMillis() - start;
}
private static class ExecuteException extends RuntimeException {
private static final long serialVersionUID = 1L;
int errorCode;
String stdout;
public ExecuteException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public ExecuteException(int errorCode, String message, String stdout) {
super(message);
this.errorCode = errorCode;
this.stdout = stdout;
}
}
}