/******************************************************************************* * Copyright (c) 2006-2011 Gluster, Inc. <http://www.gluster.com> * This file is part of Gluster Management Gateway. * * Gluster Management Gateway is free software; you can redistribute * it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * Gluster Management Gateway 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 for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see * <http://www.gnu.org/licenses/>. *******************************************************************************/ package org.gluster.storage.management.gateway.utils; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.util.Arrays; import org.apache.log4j.Logger; import org.gluster.storage.management.core.constants.CoreConstants; import org.gluster.storage.management.core.exceptions.ConnectionException; import org.gluster.storage.management.core.exceptions.GlusterRuntimeException; import org.gluster.storage.management.core.utils.FileUtil; import org.gluster.storage.management.core.utils.LRUCache; import org.gluster.storage.management.core.utils.ProcessResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import ch.ethz.ssh2.ChannelCondition; import ch.ethz.ssh2.Connection; import ch.ethz.ssh2.SCPClient; import ch.ethz.ssh2.Session; import ch.ethz.ssh2.StreamGobbler; /** * */ @Component public class SshUtil { private static final String TEMP_DIR = "/tmp/"; public static final String SSH_AUTHORIZED_KEYS_DIR_LOCAL = "/opt/glustermg/keys/"; public static final String SSH_AUTHORIZED_KEYS_DIR_REMOTE = "/root/.ssh/"; private static final String SSH_AUTHORIZED_KEYS_FILE = "authorized_keys"; private static final String SSH_AUTHORIZED_KEYS_PATH_REMOTE = SSH_AUTHORIZED_KEYS_DIR_REMOTE + SSH_AUTHORIZED_KEYS_FILE; public static final File PRIVATE_KEY_FILE = new File(SSH_AUTHORIZED_KEYS_DIR_LOCAL + "gluster.pem"); public static final File PUBLIC_KEY_FILE = new File(SSH_AUTHORIZED_KEYS_DIR_LOCAL + "gluster.pub"); private LRUCache<String, Connection> sshConnCache = new LRUCache<String, Connection>(10); // TODO: Make user name configurable private static final String USER_NAME = "root"; // TODO: Make default password configurable private static final String DEFAULT_PASSWORD = "syst3m"; private static final Logger logger = Logger.getLogger(SshUtil.class); @Autowired private Integer sshConnectTimeout; @Autowired private Integer sshKexTimeout; @Autowired private Integer sshExecTimeout; public boolean hasDefaultPassword(String serverName) { try { getConnectionWithPassword(serverName).close(); return true; } catch(Exception e) { logger.warn("Couldn't connect to [" + serverName + "] with default password!", e); return false; } } /** * Checks if public key of management gateway is configured on given server * * @param serverName * @return true if public key is configured, else false */ public boolean isPublicKeyInstalled(String serverName) { try { getConnectionWithPubKey(serverName).close(); return true; } catch(ConnectionException e) { logger.warn("Couldn't connect to [" + serverName + "] with public key!", e); return false; } } public void getFile(String serverName, String remoteFile, String localDir) { try { Connection conn = getConnection(serverName); SCPClient scpClient = new SCPClient(conn); scpClient.get(remoteFile, localDir); } catch (IOException e) { throw new GlusterRuntimeException("Error while fetching file [" + remoteFile + "] from server [" + serverName + "]", e); } } public synchronized void installPublicKey(String serverName) { Connection conn = null; try { conn = getConnectionWithPassword(serverName); } catch(Exception e) { // authentication failed. close the connection. conn.close(); if (e instanceof GlusterRuntimeException) { throw (GlusterRuntimeException) e; } else { throw new GlusterRuntimeException("Exception during authentication with public key on server [" + serverName + "]", e); } } SCPClient scpClient = new SCPClient(conn); // delete file if it exists File localTempFile = new File(TEMP_DIR + SSH_AUTHORIZED_KEYS_FILE); if(localTempFile.exists()) { localTempFile.delete(); } try { // get authorized_keys from server scpClient.get(SSH_AUTHORIZED_KEYS_PATH_REMOTE, TEMP_DIR); } catch (IOException e) { // file doesn't exist. it will get created. // create the .ssh directory in case it doesn't exist logger.info("Couldn't fetch file [" + SSH_AUTHORIZED_KEYS_PATH_REMOTE +"].", e); logger.info("Creating /root/.ssh on [" + serverName + "] in case it doesn't exist."); String command = "mkdir -p " + SSH_AUTHORIZED_KEYS_DIR_REMOTE; ProcessResult result = executeCommand(conn, command); if(!result.isSuccess()) { String errMsg = "Command [" + command + "] failed on server [" + serverName + "] with error: " + result; logger.error(errMsg); throw new GlusterRuntimeException(errMsg); } } byte[] publicKeyData; try { publicKeyData = FileUtil.readFileAsByteArray(PUBLIC_KEY_FILE); } catch (Exception e) { conn.close(); throw new GlusterRuntimeException("Couldn't load public key file [" + PUBLIC_KEY_FILE + "]", e); } try { // append it FileOutputStream outputStream = new FileOutputStream(localTempFile, true); outputStream.write(CoreConstants.NEWLINE.getBytes()); outputStream.write(publicKeyData); outputStream.close(); } catch (Exception e) { conn.close(); throw new GlusterRuntimeException("Couldnt append file [" + localTempFile + "] with public key!", e); } try { scpClient.put(localTempFile.getAbsolutePath(), SSH_AUTHORIZED_KEYS_FILE, SSH_AUTHORIZED_KEYS_DIR_REMOTE, "0600"); } catch (IOException e) { throw new GlusterRuntimeException("Couldn't add public key to server [" + serverName + "]", e); } finally { conn.close(); localTempFile.delete(); } // It was decided NOT to disable password login as this may not be acceptable in a bare-metal environment // disableSshPasswordLogin(serverName, scpClient); } // private void disableSshPasswordLogin(String serverName, SCPClient scpClient) { // ProcessResult result = executeRemote(serverName, SCRIPT_DISABLE_SSH_PASSWORD_AUTH); // if(!result.isSuccess()) { // throw new GlusterRuntimeException("Couldn't disable SSH password authentication on [" + serverName // + "]. Error: " + result); // } // } private synchronized Connection getConnectionWithPassword(String serverName) { Connection conn = createConnection(serverName); if(!authenticateWithPassword(conn)) { conn.close(); throw new ConnectionException("SSH Authentication (password) failed for server [" + conn.getHostname() + "]"); } return conn; } private synchronized Connection getConnectionWithPubKey(String serverName) { Connection conn = createConnection(serverName); if(!authenticateWithPublicKey(conn)) { conn.close(); throw new ConnectionException("SSH Authentication (public key) failed for server [" + conn.getHostname() + "]"); } return conn; } private synchronized Connection getConnection(String serverName) { Connection conn = sshConnCache.get(serverName); if (conn != null) { return conn; } conn = createConnection(serverName); try { if(!authenticateWithPublicKey(conn)) { if(!authenticateWithPassword(conn)) { conn.close(); throw new ConnectionException("SSH authentication failed on server [" + serverName + "]!"); } } } catch(Exception e) { // authentication failed. close the connection. conn.close(); if(e instanceof GlusterRuntimeException) { throw (GlusterRuntimeException)e; } else { throw new GlusterRuntimeException("Exception during authentication on server [" + serverName + "]", e); } } sshConnCache.put(serverName, conn); return conn; } private boolean authenticateWithPublicKey(Connection conn) { try { if (!supportsPublicKeyAuthentication(conn)) { throw new ConnectionException("Public key authentication not supported on [" + conn.getHostname() + "]"); } if (!conn.authenticateWithPublicKey(USER_NAME, PRIVATE_KEY_FILE, null)) { return false; } return true; } catch (IOException e) { throw new ConnectionException("Exception during SSH authentication (public key) for server [" + conn.getHostname() + "]", e); } } private boolean authenticateWithPassword(Connection conn) { try { if (!supportsPasswordAuthentication(conn)) { throw new ConnectionException("Password authentication not supported on [" + conn.getHostname() + "]"); } if (!conn.authenticateWithPassword(USER_NAME, DEFAULT_PASSWORD)) { return false; } return true; } catch (IOException e) { throw new ConnectionException("Exception during SSH authentication (password) for server [" + conn.getHostname() + "]", e); } } private boolean supportsPasswordAuthentication(Connection conn) throws IOException { return Arrays.asList(conn.getRemainingAuthMethods(USER_NAME)).contains("password"); } private boolean supportsPublicKeyAuthentication(Connection conn) throws IOException { return Arrays.asList(conn.getRemainingAuthMethods(USER_NAME)).contains("publickey"); } private synchronized Connection createConnection(String serverName) { Connection conn = new Connection(serverName); try { conn.connect(null, sshConnectTimeout, sshKexTimeout); } catch (IOException e) { logger.error("Couldn't establish SSH connection with server [" + serverName + "]", e); conn.close(); throw new ConnectionException("Exception while creating SSH connection with server [" + serverName + "]", e); } return conn; } private boolean wasTerminated(int condition) { return ((condition | ChannelCondition.EXIT_SIGNAL) == condition); } private boolean hasErrors(int condition, Session session) { return (hasErrorStream(condition) || (exitedGracefully(condition) && exitedWithError(session))); } private boolean timedOut(int condition) { return (condition == ChannelCondition.TIMEOUT); } private boolean exitedWithError(Session session) { return session.getExitStatus() != ProcessResult.SUCCESS; } private boolean exitedGracefully(int condition) { return (condition | ChannelCondition.EXIT_STATUS) == condition; } private boolean hasErrorStream(int condition) { return (condition | ChannelCondition.STDERR_DATA) == condition; } private ProcessResult executeCommand(Connection sshConnection, String command) { Session session = null; try { session = sshConnection.openSession(); BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(new StreamGobbler( session.getStdout()))); BufferedReader stderrReader = new BufferedReader(new InputStreamReader(new StreamGobbler( session.getStderr()))); session.execCommand(command); ProcessResult result = getResultOfExecution(session, stdoutReader, stderrReader); return result; } catch (Exception e) { String errMsg = "Exception while executing command [" + command + "] on [" + sshConnection.getHostname() + "]"; logger.error(errMsg, e); // remove the connection from cache and close it sshConnCache.remove(sshConnection.getHostname()); sshConnection.close(); if(e instanceof IllegalStateException || e instanceof IOException) { // The connection is no more valid. Create and throw a connection exception. throw new ConnectionException("Couldn't open SSH session on [" + sshConnection.getHostname() + "]!", e); } else { throw new GlusterRuntimeException(errMsg, e); } } finally { if(session != null) { session.close(); } } } private ProcessResult getResultOfExecution(Session session, BufferedReader stdoutReader, BufferedReader stderrReader) { // Wait for program to come out either // a) gracefully with an exit status, OR // b) because of a termination signal // c) command takes to long to exit (timeout) int condition = session.waitForCondition(ChannelCondition.EXIT_SIGNAL | ChannelCondition.EXIT_STATUS, sshExecTimeout); StringBuilder output = new StringBuilder(); try { if(!timedOut(condition)) { readFromStream(stdoutReader, output); if (hasErrors(condition, session)) { readFromStream(stderrReader, output); } } return prepareProcessResult(session, condition, output.toString().trim()); } catch (IOException e) { String errMsg = "Error while reading output stream from SSH connection!"; logger.error(errMsg, e); return new ProcessResult(ProcessResult.FAILURE, errMsg); } } private ProcessResult prepareProcessResult(Session session, int condition, String output) { ProcessResult result = null; if (wasTerminated(condition)) { result = new ProcessResult(ProcessResult.FAILURE, output); } else if (timedOut(condition)) { result = new ProcessResult(ProcessResult.FAILURE, "Command timed out!"); } else if (hasErrors(condition, session)) { Integer exitStatus = session.getExitStatus(); int statusCode = (exitStatus == null ? ProcessResult.FAILURE : exitStatus); result = new ProcessResult(statusCode, output); } else { result = new ProcessResult(ProcessResult.SUCCESS, output); } return result; } private void readFromStream(BufferedReader streamReader, StringBuilder output) throws IOException { while (true) { String line = streamReader.readLine(); if (line == null) { break; } output.append(line + CoreConstants.NEWLINE); } } /** * Executes given command on remote machine using password authentication * * @param serverName * @param command * @return Result of remote execution */ public ProcessResult executeRemoteWithPassword(String serverName, String command) { logger.info("Executing command [" + command + "] on server [" + serverName + "] with default password."); Connection conn = null; try { conn = getConnectionWithPassword(serverName); return executeCommand(conn, command); } finally { // we don't cache password based connections. hence the connection must be closed. if(conn != null) { conn.close(); } } } /** * Executes given command on remote machine using public key authentication * * @param serverName * @param command * @return Result of remote execution */ public ProcessResult executeRemote(String serverName, String command) { logger.info("Executing command [" + command + "] on server [" + serverName + "]"); return executeCommand(getConnection(serverName), command); } public void cleanup() { for (Connection conn : sshConnCache.values()) { conn.close(); } } public Integer getSshConnectTimeout() { return sshConnectTimeout; } public void setSshConnectTimeout(Integer sshConnectTimeout) { this.sshConnectTimeout = sshConnectTimeout; } public Integer getSshKexTimeout() { return sshKexTimeout; } public void setSshKexTimeout(Integer sshKexTimeout) { this.sshKexTimeout = sshKexTimeout; } public Integer getSshExecTimeout() { return sshExecTimeout; } public void setSshExecTimeout(Integer sshExecTimeout) { this.sshExecTimeout = sshExecTimeout; } }