001/**
002 * Copyright 2010-2014 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.common.util;
017
018import java.io.File;
019import java.io.IOException;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collections;
023import java.util.List;
024
025import org.apache.commons.io.FileUtils;
026import org.apache.commons.lang3.StringUtils;
027import org.codehaus.plexus.util.cli.CommandLineException;
028import org.codehaus.plexus.util.cli.CommandLineUtils;
029import org.codehaus.plexus.util.cli.Commandline;
030import org.codehaus.plexus.util.cli.StreamConsumer;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034/**
035 * Execute Unix utilities using java.
036 */
037public class UnixUtils {
038
039        private static final Logger logger = LoggerFactory.getLogger(UnixUtils.class);
040
041        private static final String SCP = "scp";
042        private static final String SSH = "ssh";
043        private static final String RSYNC = "rsync";
044        private static final String FORWARD_SLASH = "/";
045        public static final int SUCCESS = 0;
046
047        private static final UnixCmds cmds = new UnixCmds();
048
049        /**
050         * <pre>
051         *  rsync source destination
052         * </pre>
053         * 
054         * Where <code>source</code> and <code>destination</code> are both directories on the local file system. <code>source</code> must already exist. <code>destination</code> will
055         * be created if it does not exist.
056         */
057        public static final int rsyncdirs(File source, File destination) {
058                String sourcePath = validateRsyncSourceDir(source);
059                String destinationPath = validateRsyncDestinationDir(destination);
060
061                // Make sure source is a different directory than destination
062                boolean different = !source.equals(destination);
063                Assert.isTrue(different);
064
065                return rsyncdirs(null, sourcePath, destinationPath);
066        }
067
068        /**
069         * <pre>
070         *  rsync source [user@]hostname:destination
071         * </pre>
072         * 
073         * Where <code>source</code> is a directory on the local file system. <code>source</code> must already exist.
074         */
075        public static final int rsyncdirs(File source, String destination) {
076                String sourcePath = validateRsyncSourceDir(source);
077                return rsyncdirs(null, sourcePath, destination);
078        }
079
080        /**
081         * <pre>
082         *  rsync [user@]hostname:source destination
083         * </pre>
084         * 
085         * Where <code>destination</code> is a directory on the local file system. <code>destination</code> will be created if it does not exist
086         */
087        public static final int rsyncdirs(String source, File destination) {
088                String destinationPath = validateRsyncDestinationDir(destination);
089                return rsyncdirs(null, source, destinationPath);
090        }
091
092        /**
093         * <pre>
094         *  rsync [options] source destination
095         * </pre>
096         * 
097         * Where <code>source</code> and <code>destination</code> are both directories on the local file system. <code>source</code> must already exist. <code>destination</code> will
098         * be created if it does not exist.
099         */
100        public static final int rsyncdirs(List<String> options, File source, File destination) {
101                String sourcePath = validateRsyncSourceDir(source);
102                String destinationPath = validateRsyncDestinationDir(destination);
103                return rsyncdirs(options, sourcePath, destinationPath);
104        }
105
106        /**
107         * <pre>
108         *  rsync [options] source [user@]hostname:destination
109         * </pre>
110         * 
111         * Where <code>source</code> is a directory on the local file system. <code>source</code> must already exist.
112         */
113        public static final int rsync(List<String> options, File source, String destination) {
114                String sourcePath = validateRsyncSourceDir(source);
115                return rsyncdirs(options, sourcePath, destination);
116        }
117
118        /**
119         * <pre>
120         *  rsync [options] [user@]hostname:source destination
121         * </pre>
122         * 
123         * Where <code>destination</code> is a directory on the local file system. <code>destination</code> will be created if it does not exist
124         */
125        public static final int rsyncdirs(List<String> options, String source, File destination) {
126                String destinationPath = validateRsyncDestinationDir(destination);
127                return rsyncdirs(options, source, destinationPath);
128        }
129
130        /**
131         * <pre>
132         *  rsync [options] source destination
133         *  rsync [options] source [user@]hostname:destination
134         *  rsync [options] [user@]hostname:source destination
135         * </pre>
136         * 
137         * Always add a trailing slash to source when sync'ing directories.<br>
138         * This forces rsync to behave like <code>cp</code>
139         * 
140         * <pre>
141         * cp -R /tmp/foo/bar  /tmp/xyz  -  creates files in /tmp/xyz
142         * rsync /tmp/foo/bar/ /tmp/xyz  -  creates files in /tmp/xyz
143         * 
144         * rsync /tmp/foo/bar  /tmp/xyz  -  creates files in /tmp/xyz/bar
145         * </pre>
146         */
147        public static final int rsyncdirs(List<String> options, String source, String destination) {
148                List<String> rsyncDirOptions = getRsyncDirOptions(options);
149                // Make sure source always has a trailing slash
150                String trailingSlashSource = StringUtils.endsWith(source, FORWARD_SLASH) ? source : source + FORWARD_SLASH;
151                return rsync(rsyncDirOptions, trailingSlashSource, destination);
152        }
153
154        /**
155         * <pre>
156         *  rsync [options] source destination
157         *  rsync [options] source [user@]hostname:destination
158         *  rsync [options] [user@]hostname:source destination
159         * </pre>
160         */
161        public static final int rsyncdirs(String source, String destination) {
162                return rsyncdirs(null, source, destination);
163        }
164
165        /**
166         * <pre>
167         *  rsync source destination
168         *  rsync source [user@]hostname:destination
169         *  rsync [user@]hostname:source destination
170         * </pre>
171         */
172        public static final int rsync(String source, String destination) {
173                return rsync(null, source, destination);
174        }
175
176        /**
177         * <pre>
178         *  rsync [options] source destination
179         *  rsync [options] source [user@]hostname:destination
180         *  rsync [options] [user@]hostname:source destination
181         * </pre>
182         */
183        public static final int rsync(List<String> options, String source, String destination) {
184                Assert.notNull(source);
185                Assert.notNull(destination);
186                List<String> arguments = new ArrayList<String>();
187                arguments.addAll(CollectionUtils.toEmptyList(options));
188                arguments.add(source);
189                arguments.add(destination);
190                Commandline cl = new Commandline();
191                cl.setExecutable(RSYNC);
192                cl.addArguments(CollectionUtils.toStringArray(arguments));
193                return execute(cl);
194        }
195
196        /**
197         * <pre>
198         * ssh [args] [user@]hostname chown [chownargs] owner:group file
199         * </pre>
200         */
201        public static final int sshchown(List<String> args, String user, String hostname, List<String> options, String owner, String group, String file) {
202                Assert.notNull(owner);
203                Assert.notNull(group);
204                Assert.notNull(file);
205                String command = cmds.chown(options, owner, group, file);
206                return ssh(args, user, hostname, command);
207        }
208
209        /**
210         * <pre>
211         * ssh hostname chown -R owner:group file
212         * </pre>
213         */
214        public static final int sshchownr(String hostname, String owner, String group, String file) {
215                return sshchownr(null, null, hostname, owner, group, file);
216        }
217
218        /**
219         * <pre>
220         * ssh [user@]hostname chown -R owner:group file
221         * </pre>
222         */
223        public static final int sshchownr(String user, String hostname, String owner, String group, String file) {
224                return sshchownr(null, user, hostname, owner, group, file);
225        }
226
227        /**
228         * <pre>
229         * ssh [args] hostname chown -R owner:group file
230         * </pre>
231         */
232        public static final int sshchownr(List<String> args, String hostname, String owner, String group, String file) {
233                return sshchownr(args, null, hostname, owner, group, file);
234        }
235
236        /**
237         * <pre>
238         * ssh [args] [user@]hostname chown -R owner:group file
239         * </pre>
240         */
241        public static final int sshchownr(List<String> args, String user, String hostname, String owner, String group, String file) {
242                return sshchown(args, user, hostname, Arrays.asList("-R"), owner, group, file);
243        }
244
245        /**
246         * <pre>
247         * ssh [args] hostname chown owner:group file
248         * </pre>
249         */
250        public static final int sshchown(List<String> args, String hostname, String owner, String group, String file) {
251                return sshchown(args, null, hostname, null, owner, group, file);
252        }
253
254        /**
255         * <pre>
256         * ssh [args] [user@]hostname chown owner:group file
257         * </pre>
258         */
259        public static final int sshchown(List<String> args, String user, String hostname, String owner, String group, String file) {
260                return sshchown(args, user, hostname, null, owner, group, file);
261        }
262
263        /**
264         * <pre>
265         * ssh [user@]hostname chown owner:group file
266         * </pre>
267         */
268        public static final int sshchown(String user, String hostname, String owner, String group, String file) {
269                return sshchown(null, user, hostname, null, owner, group, file);
270        }
271
272        /**
273         * <pre>
274         * ssh hostname chown owner:group file
275         * </pre>
276         */
277        public static final int sshchown(String hostname, String owner, String group, String file) {
278                return sshchown(null, null, hostname, owner, group, file);
279        }
280
281        /**
282         * <pre>
283         * ssh hostname rm -rf file
284         * </pre>
285         */
286        public static final int sshrm(String hostname, String file) {
287                return sshrm(null, null, hostname, file);
288        }
289
290        /**
291         * <pre>
292         * ssh [user@]hostname rm -rf file
293         * </pre>
294         */
295        public static final int sshrm(String user, String hostname, String file) {
296                return sshrm(null, user, hostname, file);
297        }
298
299        /**
300         * <pre>
301         * ssh [args] hostname rm -rf file
302         * </pre>
303         */
304        public static final int sshrm(List<String> args, String hostname, String file) {
305                return sshrm(args, null, hostname, file);
306        }
307
308        /**
309         * <pre>
310         * ssh [args] [user@]hostname rm -rf file
311         * </pre>
312         */
313        public static final int sshrm(List<String> args, String user, String hostname, String file) {
314                Assert.notNull(file);
315                return sshrm(args, user, hostname, Collections.singletonList(file));
316        }
317
318        /**
319         * <pre>
320         * ssh hostname rm -rf file ...
321         * </pre>
322         */
323        public static final int sshrm(String hostname, List<String> paths) {
324                return sshrm(null, null, hostname, paths);
325        }
326
327        /**
328         * <pre>
329         * ssh [user@]hostname rm -rf file ...
330         * </pre>
331         */
332        public static final int sshrm(String user, String hostname, List<String> paths) {
333                return sshrm(null, user, hostname, paths);
334        }
335
336        /**
337         * <pre>
338         * ssh [args] hostname rm -rf file ...
339         * </pre>
340         */
341        public static final int sshrm(List<String> args, String hostname, List<String> paths) {
342                return sshrm(args, null, hostname, paths);
343        }
344
345        /**
346         * <pre>
347         * ssh [args] [user@]hostname rm -rf file ...
348         * </pre>
349         */
350        public static final int sshrm(List<String> args, String user, String hostname, List<String> paths) {
351                return sshrm(args, user, hostname, Arrays.asList("-r", "-f"), paths);
352        }
353
354        /**
355         * <pre>
356         * ssh [args] [user@]hostname rm [rmargs] file ...
357         * </pre>
358         */
359        public static final int sshrm(List<String> args, String user, String hostname, List<String> options, List<String> paths) {
360                Assert.notEmpty(paths);
361                String command = cmds.rm(options, paths);
362                return ssh(args, user, hostname, command);
363        }
364
365        /**
366         * <pre>
367         * ssh [args] [user@]hostname chmod mode file
368         * </pre>
369         */
370        public static final int sshchmod(List<String> args, String user, String hostname, String mode, String path) {
371                Assert.hasLength(mode);
372                Assert.notNull(path);
373                return ssh(args, user, hostname, cmds.chmod(mode, path));
374        }
375
376        /**
377         * <pre>
378         * ssh [user@]hostname chmod mode file
379         * </pre>
380         */
381        public static final int sshchmod(String user, String hostname, String mode, String file) {
382                return sshchmod(null, user, hostname, mode, file);
383        }
384
385        /**
386         * <pre>
387         * ssh [user@]hostname mkdir -p directory
388         * </pre>
389         */
390        public static final int sshmkdir(String user, String hostname, String path) {
391                return sshmkdir(null, user, hostname, path);
392        }
393
394        /**
395         * <pre>
396         * ssh [args] [user@]hostname mkdir -p directory
397         * </pre>
398         */
399        public static final int sshmkdir(List<String> args, String user, String hostname, String path) {
400                Assert.notBlank(path);
401                return ssh(args, user, hostname, cmds.mkdirp(path));
402        }
403
404        /**
405         * <pre>
406         * ssh hostname mkdir -p directory
407         * </pre>
408         */
409        public static final int sshmkdir(String hostname, String path) {
410                return sshmkdir(null, null, hostname, path);
411        }
412
413        /**
414         * <pre>
415         * ssh [args] hostname mkdir -p directory
416         * </pre>
417         */
418        public static final int sshmkdir(List<String> args, String hostname, String path) {
419                return sshmkdir(args, null, hostname, path);
420        }
421
422        /**
423         * <pre>
424         * ssh hostname su - login command
425         * </pre>
426         */
427        public static final int sshsu(String hostname, String login, String command) {
428                return sshsu(null, null, hostname, login, command);
429        }
430
431        /**
432         * <pre>
433         * ssh [args] hostname su - login command
434         * </pre>
435         */
436        public static final int sshsu(List<String> args, String hostname, String login, String command) {
437                return sshsu(args, null, hostname, login, command);
438        }
439
440        /**
441         * <pre>
442         * ssh [user@]hostname su - login command
443         * </pre>
444         */
445        public static final int sshsu(String user, String hostname, String login, String command) {
446                return sshsu(null, user, hostname, login, command);
447        }
448
449        /**
450         * <pre>
451         * ssh [args] [user@]hostname su - login command
452         * </pre>
453         */
454        public static final int sshsu(List<String> args, String user, String hostname, String login, String command) {
455                Assert.notNull(login);
456                Assert.notNull(command);
457                return ssh(user, hostname, cmds.su(login, command));
458        }
459
460        /**
461         * <pre>
462         * ssh hostname command
463         * </pre>
464         */
465        public static final int ssh(String hostname, String command) {
466                return ssh(null, null, hostname, command);
467        }
468
469        /**
470         * <pre>
471         * ssh [user@]hostname command
472         * </pre>
473         */
474        public static final int ssh(String user, String hostname, String command) {
475                return ssh(null, user, hostname, command);
476        }
477
478        /**
479         * <pre>
480         * ssh [args] hostname command
481         * </pre>
482         */
483        public static final int ssh(List<String> args, String hostname, String command) {
484                return ssh(args, null, hostname, command);
485        }
486
487        /**
488         * <pre>
489         * ssh [args] [user@]hostname command
490         * </pre>
491         */
492        public static final int ssh(List<String> args, String user, String hostname, String command) {
493                Assert.notNull(hostname);
494                Assert.notNull(command);
495                List<String> arguments = new ArrayList<String>();
496                arguments.addAll(CollectionUtils.toEmptyList(args));
497                if (!StringUtils.isBlank(user)) {
498                        arguments.add(user + "@" + hostname);
499                } else {
500                        arguments.add(hostname);
501                }
502                arguments.add(command);
503                Commandline cl = new Commandline();
504                cl.setExecutable(SSH);
505                cl.addArguments(CollectionUtils.toStringArray(arguments));
506                return execute(cl);
507        }
508
509        /**
510         * <pre>
511         * scp source destination
512         * </pre>
513         * 
514         * Where both <code>source</code> and <code>destination</code> are in the format
515         * 
516         * <pre>
517         * [[user@]host:]file
518         * </pre>
519         */
520        public static final int scp(String source, String destination) {
521                return scp(null, source, destination);
522        }
523
524        /**
525         * <pre>
526         * scp [args] source destination
527         * </pre>
528         * 
529         * Where both <code>source</code> and <code>destination</code> are in the format
530         * 
531         * <pre>
532         * [[user@]host:]file
533         * </pre>
534         */
535        public static final int scp(List<String> args, String source, String destination) {
536                Assert.notNull(source);
537                Assert.notNull(destination);
538                List<String> arguments = new ArrayList<String>();
539                arguments.addAll(CollectionUtils.toEmptyList(args));
540                arguments.add(source);
541                arguments.add(destination);
542                Commandline cl = new Commandline();
543                cl.setExecutable(SCP);
544                cl.addArguments(CollectionUtils.toStringArray(arguments));
545                return execute(cl);
546        }
547
548        /**
549         * <pre>
550         * scp [args] source destination
551         * </pre>
552         * 
553         * Where <code>source</code> is a file on the local file system and <code>destination</code> is in the format
554         * 
555         * <pre>
556         * [[user@]host:]file
557         * </pre>
558         */
559        public static final int scp(List<String> args, File source, String destination) {
560                Assert.notNull(source);
561                String sourcePath = LocationUtils.getCanonicalPath(source);
562                if (!source.exists()) {
563                        throw new IllegalArgumentException(sourcePath + " does not exist");
564                }
565                return scp(args, sourcePath, destination);
566        }
567
568        /**
569         * <pre>
570         * scp [args] source destination
571         * </pre>
572         * 
573         * Where <code>destination</code> is a file on the local file system and <code>source</code> is in the format
574         * 
575         * <pre>
576         * [[user@]host:]file
577         * </pre>
578         */
579        public static final int scp(List<String> args, String source, File destination) {
580                try {
581                        FileUtils.touch(destination);
582                } catch (IOException e) {
583                        throw new IllegalStateException("Unexpected IO error", e);
584                }
585                String localPath = LocationUtils.getCanonicalPath(destination);
586                return scp(args, source, localPath);
587        }
588
589        /**
590         * <pre>
591         * scp source destination
592         * </pre>
593         * 
594         * Where <code>source</code> is a file on the local file system and <code>destination</code> is in the format
595         * 
596         * <pre>
597         * [[user@]host:]file
598         * </pre>
599         */
600        public static final int scp(File source, String destination) {
601                return scp(null, source, destination);
602        }
603
604        /**
605         * <pre>
606         * scp source destination
607         * </pre>
608         * 
609         * Where <code>destination</code> is a file on the local file system and <code>source</code> is in the format
610         * 
611         * <pre>
612         * [[user@]host:]file
613         * </pre>
614         */
615        public static final int scp(String source, File destination) {
616                return scp(null, source, destination);
617        }
618
619        public static final void validate(int exitValue, String message, Mode mode) {
620                if (exitValue != UnixUtils.SUCCESS) {
621                        ModeUtils.validate(mode, message + " Exit value=[" + exitValue + "]");
622                }
623        }
624
625        public static final void validate(int exitValue, String message) {
626                validate(exitValue, message, Mode.ERROR);
627        }
628
629        public static final int execute(Commandline cl) {
630                try {
631                        StreamConsumer stdout = new org.kuali.common.util.stream.LoggingStreamConsumer(logger, org.kuali.common.util.log.LoggerLevel.INFO);
632                        StreamConsumer stderr = new org.kuali.common.util.stream.LoggingStreamConsumer(logger, org.kuali.common.util.log.LoggerLevel.WARN);
633                        logger.info(cl.toString());
634                        return CommandLineUtils.executeCommandLine(cl, stdout, stderr);
635                } catch (CommandLineException e) {
636                        throw new IllegalStateException(e);
637                }
638        }
639
640        public static final String getLocation(String user, String hostname, String filename) {
641                Assert.notNull(user);
642                Assert.notNull(filename);
643                StringBuilder sb = new StringBuilder();
644                if (!StringUtils.isBlank(user)) {
645                        sb.append(user + "@");
646                }
647                sb.append(hostname);
648                sb.append(":");
649                sb.append(filename);
650                return sb.toString();
651        }
652
653        protected static final String validateRsyncSourceDir(File dir) {
654                String path = LocationUtils.getCanonicalPath(dir);
655                if (!dir.exists()) {
656                        throw new IllegalArgumentException(path + " does not exist");
657                }
658                if (!dir.isDirectory()) {
659                        throw new IllegalArgumentException(path + " is not a directory");
660                }
661                if (!StringUtils.endsWith(path, FORWARD_SLASH)) {
662                        return path + FORWARD_SLASH;
663                } else {
664                        return path;
665                }
666        }
667
668        protected static final String validateRsyncDestinationDir(File dir) {
669                try {
670                        FileUtils.forceMkdir(dir);
671                        return dir.getCanonicalPath();
672                } catch (IOException e) {
673                        throw new IllegalArgumentException("Unexpected IO error", e);
674                }
675        }
676
677        /**
678         * Return a list containing the options <code>--recursive</code>, <code>--archive</code>, and <code>--delete</code> as the first 3 elements, with additional options coming
679         * after.
680         */
681        protected static final List<String> getRsyncDirOptions(List<String> options) {
682                List<String> rsyncDirOptions = new ArrayList<String>();
683                rsyncDirOptions.add("--recursive");
684                rsyncDirOptions.add("--archive");
685                rsyncDirOptions.add("--delete");
686                for (String option : CollectionUtils.toEmptyList(options)) {
687                        if (!rsyncDirOptions.contains(option)) {
688                                rsyncDirOptions.add(option);
689                        }
690                }
691                return rsyncDirOptions;
692        }
693
694}