View Javadoc

1   /**
2    *
3    * Copyright (C) 2011 Cloud Conscious, LLC. <info@cloudconscious.com>
4    *
5    * ====================================================================
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * 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, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   * ====================================================================
18   */
19  package org.jclouds.tools.ant.taskdefs.sshjava;
20  
21  import static com.google.common.base.Preconditions.checkNotNull;
22  import static org.jclouds.scriptbuilder.domain.Statements.exec;
23  
24  import java.io.BufferedWriter;
25  import java.io.File;
26  import java.io.FileWriter;
27  import java.io.IOException;
28  import java.security.SecureRandom;
29  import java.util.LinkedHashMap;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Map.Entry;
33  import java.util.concurrent.TimeoutException;
34  
35  import org.apache.tools.ant.BuildException;
36  import org.apache.tools.ant.Location;
37  import org.apache.tools.ant.Project;
38  import org.apache.tools.ant.Target;
39  import org.apache.tools.ant.Task;
40  import org.apache.tools.ant.taskdefs.Java;
41  import org.apache.tools.ant.taskdefs.Replace;
42  import org.apache.tools.ant.taskdefs.Replace.Replacefilter;
43  import org.apache.tools.ant.taskdefs.optional.ssh.SSHUserInfo;
44  import org.apache.tools.ant.taskdefs.optional.ssh.Scp;
45  import org.apache.tools.ant.types.CommandlineJava;
46  import org.apache.tools.ant.types.Environment;
47  import org.apache.tools.ant.types.FileSet;
48  import org.apache.tools.ant.types.Path;
49  import org.apache.tools.ant.types.Environment.Variable;
50  import org.jclouds.scriptbuilder.InitBuilder;
51  import org.jclouds.scriptbuilder.domain.OsFamily;
52  import org.jclouds.scriptbuilder.domain.ShellToken;
53  import org.jclouds.scriptbuilder.domain.Statement;
54  import org.jclouds.scriptbuilder.domain.StatementList;
55  import org.jclouds.scriptbuilder.domain.Statements;
56  import org.jclouds.tools.ant.util.SSHExecute;
57  
58  import com.google.common.annotations.VisibleForTesting;
59  import com.google.common.base.Function;
60  import com.google.common.base.Joiner;
61  import com.google.common.collect.ImmutableList;
62  import com.google.common.collect.Iterables;
63  import com.google.common.collect.Lists;
64  import com.google.common.collect.Maps;
65  import com.jcraft.jsch.JSchException;
66  
67  /**
68   * Version of the Java task that executes over ssh.
69   * 
70   * @author Adrian Cole
71   */
72  public class SSHJava extends Java {
73     private final SSHExecute exec;
74     private final Scp scp;
75     private final SSHUserInfo userInfo;
76     private File localDirectory;
77     File remotebase;
78     @VisibleForTesting
79     File remotedir;
80     @VisibleForTesting
81     Environment env = new Environment();
82  
83     private OsFamily osFamily = OsFamily.UNIX;
84     private File errorFile;
85     private String errorProperty;
86     private File outputFile;
87     private String outputProperty;
88     String id = "sshjava" + new SecureRandom().nextLong();
89     private boolean append;
90  
91     @VisibleForTesting
92     final LinkedHashMap<String, String> shiftMap = Maps.newLinkedHashMap();
93     @VisibleForTesting
94     final LinkedHashMap<String, String> replace = Maps.newLinkedHashMap();
95  
96     public SSHJava() {
97        super();
98        exec = new SSHExecute();
99        exec.setProject(getProject());
100       scp = new Scp();
101       userInfo = new SSHUserInfo();
102       scp.init();
103       setFork(true);
104       setTrust(true);
105    }
106 
107    public SSHJava(Task owner) {
108       this();
109       bindToOwner(owner);
110    }
111 
112    public void setId(String id) {
113       this.id = id;
114    }
115 
116    @Override
117    public int executeJava() throws BuildException {
118       checkNotNull(remotebase, "remotebase must be set");
119 
120       if (localDirectory == null) {
121          try {
122             localDirectory = File.createTempFile("sshjava", "dir");
123             localDirectory.delete();
124             localDirectory.mkdirs();
125          } catch (IOException e) {
126             throw new BuildException(e);
127          }
128       }
129 
130       if (remotedir == null)
131          remotedir = new File(remotebase, id);
132 
133       String command = createInitScript(osFamily, id, remotedir.getAbsolutePath(), env, getCommandLine());
134 
135       try {
136          BufferedWriter out = new BufferedWriter(new FileWriter(new File(localDirectory, "init."
137                   + ShellToken.SH.to(osFamily))));
138          out.write(command);
139          out.close();
140       } catch (IOException e) {
141          throw new BuildException(e);
142       }
143 
144       replaceAllTokensIn(localDirectory);
145 
146       FileSet cwd = new FileSet();
147       cwd.setDir(localDirectory);
148       if (osFamily == OsFamily.UNIX) {
149          log("removing old contents: " + remotedir.getAbsolutePath(), Project.MSG_VERBOSE);
150          sshexec(exec("rm -rf " + remotedir.getAbsolutePath()).render(osFamily));
151       } else {
152          // TODO need recursive remove on windows
153       }
154       mkdirAndCopyTo(remotedir.getAbsolutePath(), ImmutableList.of(cwd));
155 
156       for (Entry<String, String> entry : shiftMap.entrySet()) {
157          FileSet set = new FileSet();
158          File source = new File(entry.getKey());
159          if (source.isDirectory()) {
160             set.setDir(new File(entry.getKey()));
161             mkdirAndCopyTo(remotebase.getAbsolutePath() + ShellToken.FS.to(osFamily) + entry.getValue(), ImmutableList
162                      .of(set));
163          } else {
164             String destination = remotebase.getAbsolutePath() + ShellToken.FS.to(osFamily)
165                      + new File(entry.getValue()).getParent();
166             sshexec(exec("{md} " + destination).render(osFamily));
167             scp.init();
168             String scpDestination = getScpDir(destination);
169             log("staging: " + scpDestination, Project.MSG_VERBOSE);
170             scp.setFile(source.getAbsolutePath());
171             scp.setTodir(scpDestination);
172             scp.execute();
173          }
174       }
175 
176       if (getCommandLine().getClasspath() != null) {
177          copyPathTo(getCommandLine().getClasspath(), remotedir.getAbsolutePath() + "/classpath");
178       }
179 
180       if (getCommandLine().getBootclasspath() != null) {
181          copyPathTo(getCommandLine().getBootclasspath(), remotedir.getAbsolutePath() + "/bootclasspath");
182       }
183 
184       if (osFamily == OsFamily.UNIX) {
185          sshexec(exec("chmod 755 " + remotedir.getAbsolutePath() + "{fs}init.{sh}").render(osFamily));
186       }
187 
188       Statement statement = new StatementList(exec("{cd} " + remotedir.getAbsolutePath()), exec(remotedir
189                .getAbsolutePath()
190                + "{fs}init.{sh} init"), exec(remotedir.getAbsolutePath() + "{fs}init.{sh} run"));
191       try {
192          return sshexecRedirectStreams(statement);
193       } catch (IOException e) {
194          throw new BuildException(e, getLocation());
195       }
196    }
197 
198    void replaceAllTokensIn(File directory) {
199       Replace replacer = new Replace();
200       replacer.setProject(getProject());
201       replacer.setDir(directory);
202 
203       Map<String, String> map = Maps.newLinkedHashMap();
204       // this has to go first
205       map.put(directory.getAbsolutePath(), remotedir.getAbsolutePath());
206 
207       map.putAll(Maps.transformValues(shiftMap, new Function<String, String>() {
208 
209          @Override
210          public String apply(String in) {
211             return remotebase + ShellToken.FS.to(osFamily) + in;
212          }
213 
214       }));
215       map.putAll(replace);
216 
217       for (Entry<String, String> entry : map.entrySet()) {
218          Replacefilter filter = replacer.createReplacefilter();
219          filter.setToken(entry.getKey());
220          filter.setValue(entry.getValue());
221       }
222       replacer.execute();
223    }
224 
225    private int sshexec(String command) {
226       try {
227          return exec.execute(command);
228       } catch (JSchException e) {
229          throw new BuildException(e, getLocation());
230       } catch (IOException e) {
231          throw new BuildException(e, getLocation());
232       } catch (TimeoutException e) {
233          throw new BuildException(e, getLocation());
234       }
235    }
236 
237    private int sshexecRedirectStreams(Statement statement) throws IOException {
238       exec.setStreamHandler(redirector.createHandler());
239       log("starting java as:\n" + statement.render(osFamily), Project.MSG_VERBOSE);
240       int rc;
241       try {
242          rc = sshexec(statement.render(osFamily));
243       } finally {
244          redirector.complete();
245       }
246       return rc;
247    }
248 
249    private void mkdirAndCopyTo(String destination, Iterable<FileSet> sets) {
250       if (Iterables.size(sets) == 0) {
251          log("no content: " + destination, Project.MSG_DEBUG);
252          return;
253       }
254       if (sshexec(exec("test -d " + destination).render(osFamily)) == 0) {// TODO windows
255          log("already created: " + destination, Project.MSG_VERBOSE);
256          return;
257       }
258       sshexec(exec("{md} " + destination).render(osFamily));
259       scp.init();
260       String scpDestination = getScpDir(destination);
261       log("staging: " + scpDestination, Project.MSG_VERBOSE);
262       for (FileSet set : sets)
263          scp.addFileset(set);
264       scp.setTodir(scpDestination);
265       scp.execute();
266    }
267 
268    private String getScpDir(String path) {
269       return String.format("%s:%s@%s:%s", userInfo.getName(), userInfo.getKeyfile() == null ? userInfo.getPassword()
270                : userInfo.getPassphrase(), scp.getHost(), path);
271    }
272 
273    void resetPathToUnderPrefixIfExistsAndIsFileIfNotExistsAddAsIs(Path path, String prefix, StringBuilder destination) {
274       if (path == null)
275          return;
276       String[] paths = path.list();
277       if (paths != null && paths.length > 0) {
278          for (int i = 0; i < paths.length; i++) {
279             log("converting: " + paths[i], Project.MSG_DEBUG);
280             File file = new File(reprefix(paths[i]));
281             if (file.getAbsolutePath().equals(paths[i]) && file.exists() && file.isFile()) {
282                String newPath = prefix + "{fs}" + file.getName();
283                log("adding new: " + newPath, Project.MSG_DEBUG);
284                destination.append("{ps}").append(prefix + "{fs}" + file.getName());
285             } else {
286                // if the file doesn't exist, it is probably a "forward reference" to something that
287                // is already on the remote machine
288                destination.append("{ps}").append(file.getAbsolutePath());
289                log("adding existing: " + file.getAbsolutePath(), Project.MSG_DEBUG);
290             }
291          }
292       }
293    }
294 
295    void copyPathTo(Path path, String destination) {
296       List<FileSet> filesets = Lists.newArrayList();
297       if (path.list() != null && path.list().length > 0) {
298          for (String filepath : path.list()) {
299             if (!filepath.equals(reprefix(filepath)))
300                continue;// we've already copied
301             File file = new File(filepath);
302             if (file.exists()) {
303                FileSet fileset = new FileSet();
304                if (file.isFile()) {
305                   fileset.setFile(file);
306                } else {
307                   fileset.setDir(file);
308                }
309                filesets.add(fileset);
310             }
311          }
312       }
313       mkdirAndCopyTo(destination, filesets);
314    }
315 
316    String reprefix(String in) {
317       log("comparing: " + in, Project.MSG_DEBUG);
318       for (Entry<String, String> entry : shiftMap.entrySet()) {
319          if (in.startsWith(entry.getKey())) {
320             log("match shift map: " + entry.getKey(), Project.MSG_DEBUG);
321             in = remotebase + ShellToken.FS.to(osFamily) + entry.getValue() + in.substring(entry.getKey().length());
322          }
323       }
324       for (Entry<String, String> entry : replace.entrySet()) {
325          if (in.startsWith(entry.getKey())) {
326             log("match replaceMap: " + entry.getKey(), Project.MSG_DEBUG);
327             in = entry.getValue() + in.substring(entry.getKey().length());
328          }
329       }
330       log("now: " + in, Project.MSG_DEBUG);
331       return in;
332    }
333 
334    String createInitScript(OsFamily osFamily, String id, String basedir, Environment env,
335             CommandlineJava commandLine) {
336       Map<String, String> envVariables = Maps.newHashMap();
337       String[] environment = env.getVariables();
338       if (environment != null) {
339          for (int i = 0; i < environment.length; i++) {
340             log("Setting environment variable: " + environment[i], Project.MSG_DEBUG);
341             String[] keyValue = environment[i].split("=");
342             envVariables.put(keyValue[0], keyValue[1]);
343          }
344       }
345       StringBuilder commandBuilder = new StringBuilder(commandLine.getVmCommand().getExecutable());
346       if (commandLine.getBootclasspath() != null) {
347          commandBuilder.append(" -Xbootclasspath:bootclasspath");
348          resetPathToUnderPrefixIfExistsAndIsFileIfNotExistsAddAsIs(commandLine.getBootclasspath(),
349                   "bootclasspath", commandBuilder);
350       }
351 
352       if (commandLine.getVmCommand().getArguments() != null
353                && commandLine.getVmCommand().getArguments().length > 0) {
354          commandBuilder.append(" ").append(
355                   Joiner.on(' ').join(commandLine.getVmCommand().getArguments()));
356       }
357       commandBuilder.append(" -cp classpath");
358       resetPathToUnderPrefixIfExistsAndIsFileIfNotExistsAddAsIs(commandLine.getClasspath(),
359                "classpath", commandBuilder);
360 
361       if (commandLine.getSystemProperties() != null
362                && commandLine.getSystemProperties().getVariables() != null
363                && commandLine.getSystemProperties().getVariables().length > 0) {
364          commandBuilder.append(" ").append(
365                   Joiner.on(' ').join(commandLine.getSystemProperties().getVariables()));
366       }
367 
368       commandBuilder.append(" ").append(commandLine.getClassname());
369 
370       if (commandLine.getJavaCommand().getArguments() != null
371                && commandLine.getJavaCommand().getArguments().length > 0) {
372          commandBuilder.append(" ").append(
373                   Joiner.on(' ').join(commandLine.getJavaCommand().getArguments()));
374       }
375 
376       InitBuilder testInitBuilder = new InitBuilder(id, basedir, basedir, envVariables,
377                ImmutableList.<Statement> of(Statements.interpret( commandBuilder.toString())));
378       return testInitBuilder.render(osFamily);
379    }
380 
381    @Override
382    public void addEnv(Environment.Variable var) {
383       env.addVariable(var);
384    }
385 
386    /**
387     * Note that if the {@code dir} property is set, this will be copied recursively to the remote
388     * host.
389     */
390    @Override
391    public void setDir(File localDir) {
392       this.localDirectory = checkNotNull(localDir, "dir");
393    }
394 
395    /**
396     * All files transfered to the host will be relative to this. The java process itself will be at
397     * this path/{@code id}.
398     */
399    public void setRemotebase(File remotebase) {
400       this.remotebase = checkNotNull(remotebase, "remotebase");
401    }
402 
403    @Override
404    public void setFork(boolean fork) {
405       if (!fork)
406          throw new IllegalArgumentException("this only operates when fork is set");
407    }
408 
409    /**
410     * Remote host, either DNS name or IP.
411     * 
412     * @param host
413     *           The new host value
414     */
415    public void setHost(String host) {
416       exec.setHost(host);
417       scp.setHost(host);
418    }
419 
420    /**
421     * Username known to remote host.
422     * 
423     * @param username
424     *           The new username value
425     */
426    public void setUsername(String username) {
427       exec.setUsername(username);
428       scp.setUsername(username);
429       userInfo.setName(username);
430    }
431 
432    /**
433     * Sets the password for the user.
434     * 
435     * @param password
436     *           The new password value
437     */
438    public void setPassword(String password) {
439       exec.setPassword(password);
440       scp.setPassword(password);
441       userInfo.setPassword(password);
442    }
443 
444    /**
445     * Sets the keyfile for the user.
446     * 
447     * @param keyfile
448     *           The new keyfile value
449     */
450    public void setKeyfile(String keyfile) {
451       exec.setKeyfile(keyfile);
452       scp.setKeyfile(keyfile);
453       userInfo.setKeyfile(keyfile);
454       if (userInfo.getPassphrase() == null)
455          userInfo.setPassphrase("");
456    }
457 
458    /**
459     * Sets the passphrase for the users key.
460     * 
461     * @param passphrase
462     *           The new passphrase value
463     */
464    public void setPassphrase(String passphrase) {
465       exec.setPassphrase(passphrase);
466       scp.setPassphrase(passphrase);
467       userInfo.setPassphrase(passphrase);
468    }
469 
470    /**
471     * Sets the path to the file that has the identities of all known hosts. This is used by SSH
472     * protocol to validate the identity of the host. The default is
473     * <i>${user.home}/.ssh/known_hosts</i>.
474     * 
475     * @param knownHosts
476     *           a path to the known hosts file.
477     */
478    public void setKnownhosts(String knownHosts) {
479       exec.setKnownhosts(knownHosts);
480       scp.setKnownhosts(knownHosts);
481    }
482 
483    /**
484     * Setting this to true trusts hosts whose identity is unknown.
485     * 
486     * @param yesOrNo
487     *           if true trust the identity of unknown hosts.
488     */
489    public void setTrust(boolean yesOrNo) {
490       exec.setTrust(yesOrNo);
491       scp.setTrust(yesOrNo);
492       userInfo.setTrust(yesOrNo);
493    }
494 
495    /**
496     * Changes the port used to connect to the remote host.
497     * 
498     * @param port
499     *           port number of remote host.
500     */
501    public void setPort(int port) {
502       exec.setPort(port);
503       scp.setPort(port);
504    }
505 
506    /**
507     * The connection can be dropped after a specified number of milliseconds. This is sometimes
508     * useful when a connection may be flaky. Default is 0, which means &quot;wait forever&quot;.
509     * 
510     * @param timeout
511     *           The new timeout value in seconds
512     */
513    public void setTimeout(long timeout) {
514       exec.setTimeout(timeout);
515    }
516 
517    @Override
518    public void setProject(Project project) {
519       super.setProject(project);
520       exec.setProject(project);
521       scp.setProject(project);
522    }
523 
524    @Override
525    public void setOwningTarget(Target target) {
526       super.setOwningTarget(target);
527       scp.setOwningTarget(target);
528    }
529 
530    @Override
531    public void setTaskName(String taskName) {
532       super.setTaskName(taskName);
533       scp.setTaskName(taskName);
534    }
535 
536    @Override
537    public void setDescription(String description) {
538       super.setDescription(description);
539       scp.setDescription(description);
540    }
541 
542    @Override
543    public void setLocation(Location location) {
544       super.setLocation(location);
545       scp.setLocation(location);
546    }
547 
548    @Override
549    public void setTaskType(String type) {
550       super.setTaskType(type);
551       scp.setTaskType(type);
552    }
553 
554    @Override
555    public String toString() {
556       return "SSHJava [append=" + append + ", env=" + env + ", errorFile=" + errorFile + ", errorProperty="
557                + errorProperty + ", localDirectory=" + localDirectory + ", osFamily=" + osFamily + ", outputFile="
558                + outputFile + ", outputProperty=" + outputProperty + ", remoteDirectory=" + remotebase + ", userInfo="
559                + userInfo + "]";
560    }
561 
562    @Override
563    public void addSysproperty(Variable sysp) {
564       if (sysp.getKey().startsWith("sshjava.shift.")) {
565          shiftMap.put(sysp.getKey().replaceFirst("sshjava.shift.", ""), sysp.getValue());
566       } else if (sysp.getKey().startsWith("sshjava.replace.")) {
567          replace.put(sysp.getKey().replaceFirst("sshjava.replace.", ""), sysp.getValue());
568       } else if (sysp.getKey().equals("sshjava.id")) {
569          setId(sysp.getValue());
570       } else if (sysp.getKey().equals("sshjava.host")) {
571          setHost(sysp.getValue());
572       } else if (sysp.getKey().equals("sshjava.port") && !sysp.getValue().equals("")) {
573          setPort(Integer.parseInt(sysp.getValue()));
574       } else if (sysp.getKey().equals("sshjava.username")) {
575          setUsername(sysp.getValue());
576       } else if (sysp.getKey().equals("sshjava.password") && !sysp.getValue().equals("")) {
577          setPassword(sysp.getValue());
578       } else if (sysp.getKey().equals("sshjava.keyfile") && !sysp.getValue().equals("")) {
579          setKeyfile(sysp.getValue());
580       } else if (sysp.getKey().equals("sshjava.remotebase")) {
581          setRemotebase(new File(sysp.getValue()));
582       } else {
583          super.addSysproperty(sysp);
584       }
585    }
586 
587 }