/*******************************************************************************
* Copyright (c) 2012 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package com.openshift.internal.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.openshift.client.IApplication;
import com.openshift.client.IApplicationPortForwarding;
import com.openshift.client.IApplicationSSHSession;
import com.openshift.client.OpenShiftException;
import com.openshift.client.OpenShiftSSHOperationException;
import com.openshift.client.utils.TarFileUtils;
import com.openshift.internal.client.ssh.ApplicationPortForwarding;
import com.openshift.internal.client.utils.StreamUtils;
/**
* @author Xavier Coulon
* @author André Dietisheim
* @author Corey Daley
*/
public class ApplicationSSHSession implements IApplicationSSHSession {
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationSSHSession.class);
private static final int CONNECT_TIMEOUT = 10 * 60 * 1000;
private static final String JSCH_EXEC_CHANNEL = "exec";
/** SSH Session to use for all methods */
private Session session;
/** Application that is associated with this SSH Session */
private IApplication application;
/** List of ports available for port forwarding */
private List<IApplicationPortForwarding> ports = null;
/**
* Sets the SSH session that this application will use to connect to
* OpenShift to perform some operations. This SSH session must be
* initialized out of the library, since the user's SSH settings may depend
* on the runtime environment (Eclipse, etc.).
*
* @param application
* The application that this SSH session is connecting to
* @param session
* The SSH session that is connected to the application
*/
public ApplicationSSHSession(IApplication application, Session session) {
this.application = application;
this.session = session;
}
/**
* Set the current SSH session
*
* @param session
* A new SSH session to use for the ApplicationSSHSession object
*/
public void setSSHSession(final Session session) {
this.session = session;
}
/**
* Get the application associated with this ApplicationSSHSession
*
* @return The application associated with this ssh session
*/
public IApplication getApplication() {
return this.application;
}
/**
* Check if the current SSH session is connected
*
* @return True if the SSH session is connected
*/
public boolean isConnected() {
return this.session.isConnected();
}
/**
* Check if port forwarding has been started
*
* @return true if port forwarding is started, false otherwise
* @throws OpenShiftSSHOperationException
*/
public boolean isPortFowardingStarted() throws OpenShiftSSHOperationException {
try {
return isConnected()
&& session.getPortForwardingL().length > 0;
} catch (JSchException e) {
throw new OpenShiftSSHOperationException(e,
"Unable to verify if port-forwarding has been started for application \"{0}\"",
application.getName());
}
}
/**
* Start forwarding available ports to this application
*
* @return Current list of ports
* @throws OpenShiftSSHOperationException
*/
public List<IApplicationPortForwarding> startPortForwarding() throws OpenShiftSSHOperationException {
assertLiveSSHSession();
for (IApplicationPortForwarding port : ports) {
try {
port.start(session);
} catch (OpenShiftSSHOperationException oss) {
/*
* ignore for now FIXME: should store this error on the forward
* to let user know why it could not start/stop
*/
}
}
return ports;
}
/**
* Stop forwarding of all ports to this application
*
* @return The current list of ports
* @throws OpenShiftSSHOperationException
*/
public List<IApplicationPortForwarding> stopPortForwarding() throws OpenShiftSSHOperationException {
assertLiveSSHSession();
for (IApplicationPortForwarding port : ports) {
try {
port.stop(session);
} catch (OpenShiftSSHOperationException oss) {
/*
* ignore for now should store this error on the forward to let
* user know why it could not start/stop
*/
}
}
// make sure port forwarding is stopped by closing session...
session.disconnect();
return ports;
}
/**
* Refresh the list of forwardable ports for an application
*
* @return List of forwardable ports for your application
* @throws OpenShiftSSHOperationException
*/
public List<IApplicationPortForwarding> refreshForwardablePorts() throws OpenShiftSSHOperationException {
assertLiveSSHSession();
this.ports = loadPorts();
return getForwardablePorts();
}
/**
* Gets a list of forwardable ports for your application
*
* @return List of forwardable ports for your application
* @throws OpenShiftSSHOperationException
*/
public List<IApplicationPortForwarding> getForwardablePorts() throws OpenShiftSSHOperationException {
assertLiveSSHSession();
if (ports == null) {
this.ports = loadPorts();
}
return ports;
}
/**
* Get a list of properties from your OpenShift Application
*
* @return List of properties from your OpenShift application
* @throws OpenShiftSSHOperationException
*/
@Override
public List<String> getEnvironmentProperties() throws OpenShiftSSHOperationException {
assertLiveSSHSession();
List<String> openshiftProps = new ArrayList<String>();
InputStream in = execCommand("set", ChannelInputStreams.DATA, session);
try {
for (String line : new SshCommandResponse(in).getLines()) {
openshiftProps.add(line);
}
return openshiftProps;
} catch (IOException e) {
throw new OpenShiftSSHOperationException(e,
"Could not execute \"set\" command via ssh on application {0}", application.getName());
}
}
public InputStream saveFullSnapshot() {
assertLiveSSHSession();
return new FullSnapshotCommand(session).save();
}
public InputStream restoreFullSnapshot(InputStream inputStream) {
return restoreFullSnapshot(inputStream, true);
}
/**
* Restores the given full snapshot to the application that this session is
* bound to. Providing <code>true</code> for includeGit will also activate
* the snapshot (and having the page reflecting the changes in the
* snapshot). It only works though if the snapshot has a /git/ folder with
* content.
*
* @param inputStream
* the snapshot
* @param includeGit
* will activate the new snapshot given the snapshot includes a
* /git folder
* @return
*
* @see TarFileUtils#hasGitFolder(InputStream)
* @see #saveFullSnapshot()
*/
public InputStream restoreFullSnapshot(InputStream inputStream, boolean includeGit) {
assertLiveSSHSession();
return new FullSnapshotCommand(session).restore(inputStream, includeGit);
}
public InputStream saveDeploymentSnapshot() {
assertLiveSSHSession();
return new DeploymentSnapshotCommand(session).save();
}
/**
* Restores the given snapshot to the application that this session is bound
* to.
*
* @param inputStream
* the snapshot
* @param hotDeploy
* will not restart the application if <code>true</code>
* @return
* @throws OpenShiftException
*
* @see #saveDeploymentSnapshot()
*/
public InputStream restoreDeploymentSnapshot(InputStream inputStream, boolean hotDeploy)
throws OpenShiftException {
return new DeploymentSnapshotCommand(session).restore(inputStream, hotDeploy);
}
/**
* List all forwardable ports for a given application. saveSnapshot
*
* @return the forwardable ports in an unmodifiable collection
* @throws OpenShiftSSHOperationException
*/
private List<IApplicationPortForwarding> loadPorts() throws OpenShiftSSHOperationException {
assertLiveSSHSession();
this.ports = new ArrayList<IApplicationPortForwarding>();
InputStream in = execCommand("rhc-list-ports", ChannelInputStreams.EXTENDED_DATA, session);
try {
return this.ports =
new RhcListPortsCommandResponse(application, in).getPortForwardings();
} catch (IOException e) {
throw new OpenShiftSSHOperationException("Could not execute \"rhc-list-ports\" via ssh in application {0}",
application.getName());
} finally {
try {
StreamUtils.close(in);
} catch (IOException e) {
LOGGER.error("Could not close channel to ssh server", e);
}
}
}
/**
* Refreshes the list of forwardable ports
*
* @throws OpenShiftException
*/
public void refresh() throws OpenShiftException {
if (this.ports != null) {
this.ports = loadPorts();
}
}
@Override
public boolean equals(Object object) {
if (object == null) {
return false;
} else if (getClass() != object.getClass()) {
return false;
} else if (object == this) {
return true;
}
ApplicationSSHSession other = (ApplicationSSHSession) object;
ApplicationResource otherapp = (ApplicationResource) ((ApplicationSSHSession) object).getApplication();
if (application.getUUID() == null) {
if (otherapp.getUUID() != null) {
return false;
}
} else if (!application.getUUID().equals(otherapp.getUUID())) {
return false;
} else if (isConnected() != other.isConnected()) {
return false;
} else if (isPortFowardingStarted() != other.isPortFowardingStarted()) {
return false;
} else if (!application.equals(otherapp)) {
return false;
}
return true;
}
@Override
public String toString() {
return "ApplicationSSHSession ["
+ "applicationuuid=" + application.getUUID()
+ ", applicationname=" + application.getName()
+ ", isconnected=" + isConnected()
+ ", isportforwardingstarted=" + isPortFowardingStarted()
+ "]";
}
protected void assertLiveSSHSession() {
if (!isConnected()) {
throw new OpenShiftSSHOperationException(
"SSH session for application \"{0}\" is closed.",
application.getName());
}
}
protected InputStream execCommand(final String command, ChannelInputStreams factory, Session session)
throws OpenShiftSSHOperationException {
return execCommand(command, null, factory, session);
}
/**
*
* @param command
* The remote command to run on the server
* @param sshMsgChannelData
* @param sshStream
* The ssh stream to use
* @return The output of the command that is run on the server
* @throws OpenShiftSSHOperationException
*/
protected InputStream execCommand(final String command, InputStream forStdIn,
ChannelInputStreams channelInputStream, Session session) throws OpenShiftSSHOperationException {
assertLiveSSHSession();
ChannelExec channel = null;
try {
channel = (ChannelExec) session.openChannel(JSCH_EXEC_CHANNEL);
((ChannelExec) channel).setCommand(command);
final OutputStream remoteStdIn = channel.getOutputStream();
InputStream in = channel.getInputStream();
ChannelResponse channelResponse = new ChannelResponse(in, channel);
channel.connect(CONNECT_TIMEOUT);
if (forStdIn != null) {
writeToRemoteStdInput(forStdIn, remoteStdIn);
}
return channelResponse;
} catch (JSchException e) {
channel.disconnect();
throw new OpenShiftSSHOperationException(e,
"Could no execute remote ssh command \"{0}\" on application {1}",
command, application.getName());
} catch (IOException e) {
channel.disconnect();
throw new OpenShiftSSHOperationException(e,
"Could not get response channel for remote ssh command \"{0}\" on application {1}",
command, application.getName());
}
}
private void writeToRemoteStdInput(InputStream forStdInput, OutputStream remoteStdIn) throws IOException {
for (int data = -1; (data = forStdInput.read()) != -1;) {
remoteStdIn.write(data);
}
remoteStdIn.close();
forStdInput.close();
}
public abstract class AbstractSnapshotType {
private String saveCommand;
private String restoreCommand;
private AbstractSnapshotType(String saveCommand, String restoreCommand) {
this.saveCommand = saveCommand;
this.restoreCommand = restoreCommand;
}
public String getSaveCommand() {
return saveCommand;
}
public String getRestoreCommand() {
return restoreCommand;
}
}
protected abstract class AbstractSnapshotSshCommand {
protected Session session;
AbstractSnapshotSshCommand(Session session) {
this.session = session;
}
}
class FullSnapshotCommand extends AbstractSnapshotSshCommand {
FullSnapshotCommand(Session session) {
super(session);
}
public InputStream save() {
/* rhc snapshot save -a <application> */
return execCommand(
"snapshot", ChannelInputStreams.DATA, session);
}
public InputStream restore(InputStream in, boolean includeGit) {
return execCommand(
MessageFormat.format("restore{0}", includeGit ? " INCLUDE_GIT" : ""),
in,
ChannelInputStreams.DATA,
session);
}
}
class DeploymentSnapshotCommand extends AbstractSnapshotSshCommand {
DeploymentSnapshotCommand(Session session) {
super(session);
}
public InputStream save() {
/* rhc snapshot save -a <application> */
return execCommand(
"gear archive-deployment", ChannelInputStreams.DATA, session);
}
public InputStream restore(InputStream inputStream, boolean hotDeploy) {
return execCommand(
MessageFormat.format("oo-binary-deploy{0}", hotDeploy ? " --hot-deploy" : ""),
inputStream,
ChannelInputStreams.DATA,
session);
}
}
protected static class SshCommandResponse {
private InputStream inputStream;
SshCommandResponse(InputStream inputStream) {
this.inputStream = inputStream;
}
public List<String> getLines() throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
List<String> lines = new ArrayList<String>();
String line = null;
while ((line = reader.readLine()) != null) {
lines.add(line);
}
return lines;
}
}
private static class RhcListPortsCommandResponse extends SshCommandResponse {
/** Regex for port forwarding */
private static final Pattern REGEX_FORWARDED_PORT = Pattern.compile("([^ ]+) -> ([^:]+):(\\d+)");
private IApplication application;
RhcListPortsCommandResponse(IApplication application, InputStream inputStream) {
super(inputStream);
this.application = application;
}
public List<IApplicationPortForwarding> getPortForwardings() throws IOException {
List<IApplicationPortForwarding> ports = new ArrayList<IApplicationPortForwarding>();
for (String line : getLines()) {
ApplicationPortForwarding port = extractForwardablePortFrom(line);
if (port != null) {
ports.add(port);
}
}
return ports;
}
/**
* Extracts the named forwardable port from the 'rhc-list-ports' command
* result line, with the following format:
* <code>java -> 127.10.187.1:4447</code>.
*
* @param rhcListPortsOutput
* The raw port data to parse
* @return the forwardable port.
*/
private ApplicationPortForwarding extractForwardablePortFrom(final String rhcListPortsOutput) {
Matcher matcher = REGEX_FORWARDED_PORT.matcher(rhcListPortsOutput);
if (!matcher.find()
|| matcher.groupCount() != 3) {
return null;
}
try {
final String name = matcher.group(1);
final String host = matcher.group(2);
final int remotePort = Integer.parseInt(matcher.group(3));
return new ApplicationPortForwarding(application, name, host, remotePort);
} catch (NumberFormatException e) {
throw new OpenShiftSSHOperationException(e,
"Couild not determine forwarded port in application {0}", application.getName());
}
}
}
enum ChannelInputStreams {
DATA {
@Override
public InputStream get(Channel channel) throws IOException, JSchException {
return channel.getInputStream();
}
},
EXTENDED_DATA {
@Override
public InputStream get(Channel channel) throws IOException, JSchException {
return channel.getExtInputStream();
}
};
public abstract InputStream get(Channel channel) throws IOException, JSchException;
}
class ChannelResponse extends InputStream {
/** the delay to wait for further data from the remote **/
private static final int WAIT_DELAY = 1000;
private ChannelExec channel;
private InputStream channelInputStream;
private InputStream channelErrorStream;
protected ChannelResponse(InputStream response, ChannelExec channel)
throws IOException, JSchException {
this.channel = channel;
// ATTENTION: stream must be get before connecting
this.channelInputStream = response;
this.channelErrorStream = channel.getErrStream();
}
@Override
public int read() throws IOException {
if (channel.isClosed()
&& channel.getExitStatus() != 0) {
throw new IOException(StreamUtils.readToString(channelErrorStream));
}
while (!(channel.isClosed()
&& channelInputStream.available() == 0)) {
if (channelInputStream.available() > 0) {
int data = channelInputStream.read();
if (data == -1) {
continue;
}
return data;
}
try {
Thread.sleep(WAIT_DELAY);
} catch (InterruptedException e) {
break;
}
}
return -1;
}
@Override
public void close() throws IOException {
channel.disconnect();
channelInputStream.close();
}
@Override
public int available() throws IOException {
return channelInputStream.available();
}
}
}