/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.util; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.StringReader; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.ExitStatusException; import org.apache.tools.ant.Project; import org.apache.tools.ant.taskdefs.optional.ssh.SSHBase; import org.apache.tools.ant.taskdefs.optional.testing.BuildTimeoutException; import org.apache.tools.ant.types.Resource; import org.apache.tools.ant.types.resources.FileResource; import org.apache.tools.ant.util.FileUtils; import org.apache.tools.ant.util.KeepAliveOutputStream; import org.apache.tools.ant.util.TeeOutputStream; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; // CHECKSTYLE:OFF /** * This is a copy of the SSHExec class from Ant, with a few minor modifications required for Cloudify. * Looks for the comment "Cloudify Modification" to see what was changed. * @since Ant 1.6 (created February 2, 2003) */ public class SSHExec extends SSHBase { private static final int BUFFER_SIZE = 8192; private static final int RETRY_INTERVAL = 500; /** the command to execute via ssh */ private String command = null; /** units are milliseconds, default is 0=infinite */ private long maxwait = 0; /** for waiting for the command to finish */ private Thread thread = null; private String outputProperty = null; // like <exec> private File outputFile = null; // like <exec> private String inputProperty = null; private String inputString = null; // like <exec> private File inputFile = null; // like <exec> private boolean append = false; // like <exec> private boolean usePty = false; private Resource commandResource = null; // Cloudify Modification private OutputStream outputStream = KeepAliveOutputStream.wrapSystemOut(); private static final String TIMEOUT_MESSAGE = "Timeout period exceeded, connection dropped."; /** * Constructor for SSHExecTask. */ public SSHExec() { super(); } /** * Sets the command to execute on the remote host. * * @param command The new command value */ public void setCommand(String command) { this.command = command; } /** * Sets a commandResource from a file * @param f the value to use. * @since Ant 1.7.1 */ public void setCommandResource(String f) { this.commandResource = new FileResource(new File(f)); } /** * The connection can be dropped after a specified number of * milliseconds. This is sometimes useful when a connection may be * flaky. Default is 0, which means "wait forever". * * @param timeout The new timeout value in seconds */ public void setTimeout(long timeout) { maxwait = timeout; } /** * If used, stores the output of the command to the given file. * * @param output The file to write to. */ public void setOutput(File output) { outputFile = output; } // Cloudify Modification /** * If used, passed output through this stream instead of System.out * * @param outputStream The stream to write to */ public void setOutputStream(OutputStream outputStream) { this.outputStream = outputStream; } /** * If used, the content of the file is piped to the remote command * * @param input The file which provides the input data for the remote command * * @since Ant 1.8.0 */ public void setInput(File input) { inputFile = input; } /** * If used, the content of the property is piped to the remote command * * @param inputProperty The property which contains the input data * for the remote command. * * @since Ant 1.8.0 */ public void setInputProperty(String inputProperty) { this.inputProperty = inputProperty; } /** * If used, the string is piped to the remote command. * * @param inputString the input data for the remote command. * * @since Ant 1.8.3 */ public void setInputString(String inputString) { this.inputString = inputString; } /** * Determines if the output is appended to the file given in * <code>setOutput</code>. Default is false, that is, overwrite * the file. * * @param append True to append to an existing file, false to overwrite. */ public void setAppend(boolean append) { this.append = append; } /** * If set, the output of the command will be stored in the given property. * * @param property The name of the property in which the command output * will be stored. */ public void setOutputproperty(String property) { outputProperty = property; } /** * Whether a pseudo-tty should be allocated. * @since Apache Ant 1.8.3 */ public void setUsePty(boolean b) { usePty = b; } /** * Execute the command on the remote host. * * @exception BuildException Most likely a network error or bad parameter. */ @Override public void execute() throws BuildException { if (getHost() == null) { throw new BuildException("Host is required."); } if (getUserInfo().getName() == null) { throw new BuildException("Username is required."); } if (getUserInfo().getKeyfile() == null && getUserInfo().getPassword() == null) { throw new BuildException("Password or Keyfile is required."); } if (command == null && commandResource == null) { throw new BuildException("Command or commandResource is required."); } int numberOfInputs = (inputFile != null ? 1 : 0) + (inputProperty != null ? 1 : 0) + (inputString != null ? 1 : 0); if (numberOfInputs > 1) { throw new BuildException("You can't specify more than one of" + " inputFile, inputProperty and" + " inputString."); } if (inputFile != null && !inputFile.exists()) { throw new BuildException("The input file " + inputFile.getAbsolutePath() + " does not exist."); } Session session = null; StringBuilder output = new StringBuilder(); try { session = openSession(); /* called once */ if (command != null) { log("cmd : " + command, Project.MSG_DEBUG); executeCommand(session, command, output); } else { // read command resource and execute for each command try { BufferedReader br = new BufferedReader( new InputStreamReader(commandResource.getInputStream())); String cmd; while ((cmd = br.readLine()) != null) { log("cmd : " + cmd, Project.MSG_INFO); output.append(cmd).append(" : "); executeCommand(session, cmd, output); output.append('\n'); } FileUtils.close(br); } catch (IOException e) { if (getFailonerror()) { throw new BuildException(e); } else { log("Caught exception: " + e.getMessage(), Project.MSG_ERR); } } } } catch (JSchException e) { if (getFailonerror()) { throw new BuildException(e); } else { log("Caught exception: " + e.getMessage(), Project.MSG_ERR); } } finally { if (outputProperty != null) { getProject().setNewProperty(outputProperty, output.toString()); } if (session != null && session.isConnected()) { session.disconnect(); } } } private void executeCommand(Session session, String cmd, StringBuilder sb) throws BuildException { ByteArrayOutputStream out = new ByteArrayOutputStream(); // Cloudify Modification TeeOutputStream tee = new TeeOutputStream(out, outputStream); InputStream istream = null ; if (inputFile != null) { try { istream = new FileInputStream(inputFile) ; } catch (IOException e) { // because we checked the existence before, this one // shouldn't happen What if the file exists, but there // are no read permissions? log("Failed to read " + inputFile + " because of: " + e.getMessage(), Project.MSG_WARN); } } if (inputProperty != null) { String inputData = getProject().getProperty(inputProperty) ; if (inputData != null) { istream = new ByteArrayInputStream(inputData.getBytes()) ; } } if (inputString != null) { istream = new ByteArrayInputStream(inputString.getBytes()); } try { final ChannelExec channel; session.setTimeout(CalcUtils.safeLongToInt(maxwait, true)); /* execute the command */ channel = (ChannelExec) session.openChannel("exec"); channel.setCommand(cmd); channel.setOutputStream(tee); channel.setExtOutputStream(tee); if (istream != null) { channel.setInputStream(istream); } channel.setPty(usePty); channel.connect(); // wait for it to finish thread = new Thread() { @Override public void run() { while (!channel.isClosed()) { if (thread == null) { return; } try { sleep(RETRY_INTERVAL); } catch (Exception e) { // ignored } } } }; thread.start(); thread.join(maxwait); if (thread.isAlive()) { // ran out of time thread = null; if (getFailonerror()) { throw new BuildTimeoutException(TIMEOUT_MESSAGE); } else { log(TIMEOUT_MESSAGE, Project.MSG_ERR); } } else { //success if (outputFile != null) { writeToFile(out.toString(), append, outputFile); } // this is the wrong test if the remote OS is OpenVMS, // but there doesn't seem to be a way to detect it. int ec = channel.getExitStatus(); if (ec != 0) { String msg = "Remote command execution failed with exit status " + ec + "\noutput:\n" + out.toString(); if (getFailonerror()) { throw new ExitStatusException(msg, ec); } else { log(msg, Project.MSG_ERR); } } } } catch (BuildException e) { throw e; } catch (JSchException e) { if (e.getMessage().contains("session is down")) { if (getFailonerror()) { throw new BuildTimeoutException(TIMEOUT_MESSAGE, e); } else { log(TIMEOUT_MESSAGE, Project.MSG_ERR); } } else { if (getFailonerror()) { throw new BuildException(e); } else { log("Caught exception: " + e.getMessage(), Project.MSG_ERR); } } } catch (Exception e) { if (getFailonerror()) { throw new BuildException(e); } else { log("Caught exception: " + e.getMessage(), Project.MSG_ERR); } } finally { sb.append(out.toString()); FileUtils.close(istream); } } /** * Writes a string to a file. If destination file exists, it may be * overwritten depending on the "append" value. * * @param from string to write * @param to file to write to * @param append if true, append to existing file, else overwrite * @exception Exception most likely an IOException */ private void writeToFile(String from, boolean append, File to) throws IOException { FileWriter out = null; try { out = new FileWriter(to.getAbsolutePath(), append); StringReader in = new StringReader(from); char[] buffer = new char[BUFFER_SIZE]; int bytesRead; while (true) { bytesRead = in.read(buffer); if (bytesRead == -1) { break; } out.write(buffer, 0, bytesRead); } out.flush(); } finally { if (out != null) { out.close(); } } } }