View Javadoc

1   /**
2    * Licensed to jclouds, Inc. (jclouds) under one or more
3    * contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  jclouds licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.jclouds.tools.ant.util;
20  
21  import java.io.IOException;
22  import java.util.concurrent.TimeoutException;
23  
24  import org.apache.tools.ant.BuildException;
25  import org.apache.tools.ant.Project;
26  import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;
27  import org.apache.tools.ant.taskdefs.PumpStreamHandler;
28  import org.apache.tools.ant.taskdefs.optional.ssh.SSHUserInfo;
29  import org.apache.tools.ant.util.FileUtils;
30  
31  import com.jcraft.jsch.ChannelExec;
32  import com.jcraft.jsch.JSch;
33  import com.jcraft.jsch.JSchException;
34  import com.jcraft.jsch.Session;
35  
36  /**
37   * Executes a command on a remote machine via ssh.
38   * 
39   * <p/>
40   * adapted from SSHBase and SSHExec ant tasks, and Execute from the ant 1.7.1 release.
41   * 
42   * @author Adrian Cole
43   * 
44   */
45  public class SSHExecute {
46  
47     private static final int RETRY_INTERVAL = 500;
48  
49     /** units are milliseconds, default is 0=infinite */
50     private long maxwait = 0;
51  
52     private ExecuteStreamHandler streamHandler;
53     private String host;
54     private SSHUserInfo userInfo;
55     private int port = 22;
56     private Project project;
57     private String knownHosts = System.getProperty("user.home") + "/.ssh/known_hosts";
58  
59     /**
60      * Creates a new execute object using <code>PumpStreamHandler</code> for stream handling.
61      */
62     public SSHExecute() {
63        this(new PumpStreamHandler());
64     }
65  
66     /**
67      * Creates a new ssh object.
68      * 
69      * @param streamHandler
70      *           the stream handler used to handle the input and output streams of the subprocess.
71      */
72     public SSHExecute(ExecuteStreamHandler streamHandler) {
73        setStreamHandler(streamHandler);
74        userInfo = new SSHUserInfo();
75     }
76  
77     /**
78      * Set the stream handler to use.
79      * 
80      * @param streamHandler
81      *           ExecuteStreamHandler.
82      */
83     public void setStreamHandler(ExecuteStreamHandler streamHandler) {
84        this.streamHandler = streamHandler;
85     }
86  
87     /**
88      * Setting this to true trusts hosts whose identity is unknown.
89      * 
90      * @param yesOrNo
91      *           if true trust the identity of unknown hosts.
92      */
93     public void setTrust(boolean yesOrNo) {
94        userInfo.setTrust(yesOrNo);
95     }
96  
97     /**
98      * Used for logging
99      */
100    public void setProject(Project project) {
101       this.project = project;
102    }
103 
104    /**
105     * Username known to remote host.
106     * 
107     * @param username
108     *           The new username value
109     */
110    public void setUsername(String username) {
111       userInfo.setName(username);
112    }
113 
114    /**
115     * Sets the password for the user.
116     * 
117     * @param password
118     *           The new password value
119     */
120    public void setPassword(String password) {
121       userInfo.setPassword(password);
122    }
123 
124    /**
125     * Sets the keyfile for the user.
126     * 
127     * @param keyfile
128     *           The new keyfile value
129     */
130    public void setKeyfile(String keyfile) {
131       userInfo.setKeyfile(keyfile);
132    }
133 
134    /**
135     * Sets the passphrase for the users key.
136     * 
137     * @param passphrase
138     *           The new passphrase value
139     */
140    public void setPassphrase(String passphrase) {
141       userInfo.setPassphrase(passphrase);
142    }
143 
144    /**
145     * Remote host, either DNS name or IP.
146     * 
147     * @param host
148     *           The new host value
149     */
150    public void setHost(String host) {
151       this.host = host;
152    }
153 
154    /**
155     * Changes the port used to connect to the remote host.
156     * 
157     * @param port
158     *           port number of remote host.
159     */
160    public void setPort(int port) {
161       this.port = port;
162    }
163 
164    /**
165     * The connection can be dropped after a specified number of milliseconds. This is sometimes
166     * useful when a connection may be flaky. Default is 0, which means &quot;wait forever&quot;.
167     * 
168     * @param timeout
169     *           The new timeout value in seconds
170     */
171    public void setTimeout(long timeout) {
172       maxwait = timeout;
173    }
174 
175    /**
176     * Sets the path to the file that has the identities of all known hosts. This is used by SSH
177     * protocol to validate the identity of the host. The default is
178     * <i>${user.home}/.ssh/known_hosts</i>.
179     * 
180     * @param knownHosts
181     *           a path to the known hosts file.
182     */
183    public void setKnownhosts(String knownHosts) {
184       this.knownHosts = knownHosts;
185    }
186 
187    /**
188     * Execute the command on the remote host.
189     * 
190     * @param command
191     *           - what to execute on the remote host.
192     * 
193     * @return return code of the process.
194     * @throws BuildException
195     *            bad parameter.
196     * @throws JSchException
197     *            if there's an underlying problem exposed in SSH
198     * @throws IOException
199     *            if there's a problem attaching streams.
200     * @throws TimeoutException
201     *            if we exceeded our timeout
202     */
203    public int execute(String command) throws BuildException, JSchException, IOException,
204             TimeoutException {
205       if (command == null) {
206          throw new BuildException("Command is required.");
207       }
208       if (host == null) {
209          throw new BuildException("Host is required.");
210       }
211       if (userInfo.getName() == null) {
212          throw new BuildException("Username is required.");
213       }
214       if (userInfo.getKeyfile() == null && userInfo.getPassword() == null) {
215          throw new BuildException("Password or Keyfile is required.");
216       }
217 
218       Session session = null;
219       try {
220          session = openSession();
221          return executeCommand(session, command);
222       } finally {
223          if (session != null && session.isConnected()) {
224             session.disconnect();
225          }
226       }
227    }
228 
229    /**
230     * Open an ssh seession.
231     * 
232     * @return the opened session
233     * @throws JSchException
234     *            on error
235     */
236    protected Session openSession() throws JSchException {
237       JSch jsch = new JSch();
238       if (null != userInfo.getKeyfile()) {
239          jsch.addIdentity(userInfo.getKeyfile());
240       }
241 
242       if (!userInfo.getTrust() && knownHosts != null) {
243          project.log("Using known hosts: " + knownHosts, Project.MSG_DEBUG);
244          jsch.setKnownHosts(knownHosts);
245       }
246 
247       Session session = jsch.getSession(userInfo.getName(), host, port);
248       session.setUserInfo(userInfo);
249       project.log("Connecting to " + host + ":" + port, Project.MSG_VERBOSE);
250       session.connect();
251       return session;
252    }
253 
254    /**
255     * 
256     * FIXME Comment this
257     * 
258     * @param session
259     * @param cmd
260     * @return return code of the process.
261     * @throws JSchException
262     *            if there's an underlying problem exposed in SSH
263     * @throws IOException
264     *            if there's a problem attaching streams.
265     * @throws TimeoutException
266     *            if we exceeded our timeout
267     */
268    private int executeCommand(Session session, String cmd) throws JSchException, IOException,
269             TimeoutException {
270       final ChannelExec channel;
271       session.setTimeout((int) maxwait);
272       /* execute the command */
273       channel = (ChannelExec) session.openChannel("exec");
274       channel.setCommand(cmd);
275       attachStreams(channel);
276       project.log("executing command: " + cmd, Project.MSG_VERBOSE);
277       channel.connect();
278       try {
279          waitFor(channel);
280       } finally {
281          streamHandler.stop();
282          closeStreams(channel);
283       }
284       return channel.getExitStatus();
285    }
286 
287    private void attachStreams(final ChannelExec channel) throws IOException {
288       streamHandler.setProcessInputStream(channel.getOutputStream());
289       streamHandler.setProcessOutputStream(channel.getInputStream());
290       streamHandler.setProcessErrorStream(channel.getErrStream());
291       streamHandler.start();
292    }
293 
294    /**
295     * Close the streams belonging to the given Process.
296     * 
297     * @param process
298     *           the <code>Process</code>.
299     * @throws IOException
300     */
301    public static void closeStreams(ChannelExec process) throws IOException {
302       FileUtils.close(process.getInputStream());
303       FileUtils.close(process.getOutputStream());
304       FileUtils.close(process.getErrStream());
305    }
306 
307    /**
308     * @throws TimeoutException
309     */
310    @SuppressWarnings("deprecation")
311    private void waitFor(final ChannelExec channel) throws TimeoutException {
312       // wait for it to finish
313       Thread thread = new Thread() {
314          public void run() {
315             while (!channel.isClosed()) {
316                try {
317                   sleep(RETRY_INTERVAL);
318                } catch (InterruptedException e) {
319                   // ignored
320                }
321             }
322          }
323       };
324 
325       thread.start();
326       try {
327          thread.join(maxwait);
328       } catch (InterruptedException e) {
329          // ignored
330       }
331 
332       if (thread.isAlive()) {
333          thread.destroy();
334          throw new TimeoutException("command still running");
335       }
336    }
337 
338 }