001    package org.kuali.common.deploy;
002    
003    import java.io.File;
004    import java.io.IOException;
005    import java.util.ArrayList;
006    import java.util.Arrays;
007    import java.util.Collections;
008    import java.util.List;
009    import java.util.Properties;
010    
011    import org.apache.commons.io.IOUtils;
012    import org.codehaus.plexus.util.StringUtils;
013    import org.kuali.common.util.Assert;
014    import org.kuali.common.util.FormatUtils;
015    import org.kuali.common.util.LocationUtils;
016    import org.kuali.common.util.MonitorTextFileResult;
017    import org.kuali.common.util.ThreadUtils;
018    import org.kuali.common.util.UnixCmds;
019    import org.kuali.common.util.UnixProcess;
020    import org.kuali.common.util.log.LoggerLevel;
021    import org.kuali.common.util.log.LoggerUtils;
022    import org.kuali.common.util.property.Constants;
023    import org.kuali.common.util.secure.channel.RemoteFile;
024    import org.kuali.common.util.secure.channel.Result;
025    import org.kuali.common.util.secure.channel.SecureChannel;
026    import org.slf4j.Logger;
027    import org.slf4j.LoggerFactory;
028    import org.springframework.util.CollectionUtils;
029    import org.springframework.util.PropertyPlaceholderHelper;
030    
031    public class DeployUtils {
032    
033            private static final Logger logger = LoggerFactory.getLogger(DeployUtils.class);
034    
035            private static final String CMD = "CMD";
036            private static final String TRAVERSE_SYMBOLIC_LINKS = "-L";
037            private static final UnixCmds CMDS = new UnixCmds();
038            private static final PropertyPlaceholderHelper HELPER = Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER;
039    
040            /**
041             * Examine the contents of a text file, stopping as soon as it contains <code>token</code>, or <code>timeout</code> is exceeded, whichever comes first.
042             */
043            public static MonitorTextFileResult monitorTextFile(SecureChannel channel, String path, String token, int intervalMillis, int timeoutMillis, String encoding) {
044    
045                    // Make sure we are configured correctly
046                    Assert.notNull(channel, "channel is null");
047                    Assert.notNull(path, "path is null");
048                    Assert.hasText(token, "token has no text");
049                    Assert.hasText(encoding, "encoding has no text");
050                    Assert.isTrue(intervalMillis > 0, "interval must be a positive integer");
051                    Assert.isTrue(timeoutMillis > 0, "timeout must be a positive integer");
052    
053                    // Setup some member variables to record what happens
054                    long start = System.currentTimeMillis();
055                    long stop = start + timeoutMillis;
056                    boolean exists = false;
057                    boolean contains = false;
058                    boolean timeoutExceeded = false;
059                    long now = -1;
060                    String content = null;
061    
062                    // loop until timeout is exceeded or we find the token inside the file
063                    for (;;) {
064    
065                            // Always pause (unless this is the first iteration)
066                            if (now != -1) {
067                                    ThreadUtils.sleep(intervalMillis);
068                            }
069    
070                            // Check to make sure we haven't exceeded our timeout limit
071                            now = System.currentTimeMillis();
072                            if (now > stop) {
073                                    timeoutExceeded = true;
074                                    break;
075                            }
076    
077                            // If the file does not exist yet, there is nothing more we can do
078                            exists = channel.exists(path);
079                            if (!exists) {
080                                    continue;
081                            }
082    
083                            // The file exists, check to see if the token we are looking for is present in the file
084                            RemoteFile remoteFile = new RemoteFile.Builder(path).build();
085                            content = channel.toString(remoteFile);
086                            contains = StringUtils.contains(content, token);
087                            if (contains) {
088                                    // We found what we are looking for, we are done
089                                    break;
090                            }
091                    }
092    
093                    // Record how long the overall process took
094                    long elapsed = now - start;
095    
096                    // Fill in a pojo detailing what happened
097                    MonitorTextFileResult mtfr = new MonitorTextFileResult(exists, contains, timeoutExceeded, elapsed);
098                    mtfr.setAbsolutePath(path);
099                    mtfr.setContent(content);
100                    return mtfr;
101            }
102    
103            public static void killMatchingProcesses(SecureChannel channel, String user, String cmd, String processLabel) {
104                    List<UnixProcess> processes = getUnixProcesses(channel, user);
105    
106                    // No existing processes, we are done
107                    if (processes.size() == 0) {
108                            logger.info("  no running processes for user [{}]", user);
109                            return;
110                    }
111    
112                    // Figure out if any of the running processes are matches
113                    List<UnixProcess> matches = getMatchingProcesses(processes, cmd);
114    
115                    if (CollectionUtils.isEmpty(matches)) {
116                            // Nothing to do
117                            logger.info("  no machine agents detected. total running processes - {}", processes.size());
118                            return;
119                    } else {
120                            // Kill any matching processes
121                            for (UnixProcess match : matches) {
122                                    logger.info("  killing {} - [pid:{}]", processLabel, match.getProcessId());
123                                    kill(channel, match);
124                            }
125                    }
126            }
127    
128            /**
129             * Execute <code>cmd</code> as <code>user</code> using <code>nohup</code> and running it in the background.
130             */
131            public static String getNohupBackgroundProcess(String user, String cmd) {
132                    StringBuilder sb = new StringBuilder();
133                    sb.append("su");
134                    sb.append(" - ");
135                    sb.append(user);
136                    sb.append(" ");
137                    sb.append("--command");
138                    sb.append("=");
139                    sb.append("'");
140                    sb.append(CMDS.nohup(cmd));
141                    sb.append(" ");
142                    sb.append("&");
143                    sb.append("'");
144                    return sb.toString();
145            }
146    
147            public static void copyFiles(SecureChannel channel, List<Deployable> deployables, Properties filterProperties) {
148                    if (CollectionUtils.isEmpty(deployables)) {
149                            return;
150                    }
151                    for (Deployable deployable : deployables) {
152                            RemoteFile destination = new RemoteFile.Builder(deployable.getRemote()).build();
153                            String location = deployable.getLocal();
154                            logger.info("  creating -> [{}]", destination.getAbsolutePath());
155                            if (deployable.isFilter()) {
156                                    long start = System.currentTimeMillis();
157                                    String originalContent = LocationUtils.toString(location);
158                                    String resolvedContent = HELPER.replacePlaceholders(originalContent, filterProperties);
159                                    channel.copyStringToFile(resolvedContent, destination);
160                                    String elapsed = FormatUtils.getTime(System.currentTimeMillis() - start);
161                                    Object[] args = { filterProperties.size(), location, destination.getAbsolutePath(), elapsed };
162                                    logger.debug("Used {} properties to filter [{}] -> [{}] - {}", args);
163                            } else {
164                                    long start = System.currentTimeMillis();
165                                    channel.copyLocationToFile(location, destination);
166                                    logCopy(location, destination.getAbsolutePath(), System.currentTimeMillis() - start);
167                            }
168                            if (deployable.getPermissions().isPresent()) {
169                                    String path = deployable.getRemote();
170                                    String perms = deployable.getPermissions().get();
171                                    String command = CMDS.chmod(perms, path);
172                                    executePathCommand(channel, command, path);
173                            }
174                    }
175            }
176    
177            protected static void logCopy(String src, String dst, long elapsed) {
178                    String rate = "";
179                    String size = "";
180                    if (LocationUtils.isExistingFile(src)) {
181                            long bytes = new File(src).length();
182                            rate = FormatUtils.getRate(elapsed, bytes);
183                            size = FormatUtils.getSize(bytes);
184                    }
185                    Object[] args = { dst, size, FormatUtils.getTime(elapsed), rate };
186                    logger.debug("Source -> [{}]", src);
187                    logger.debug("  created [{}] - [{} {} {}]", args);
188            }
189    
190            /**
191             * Return a list of any processes where the command exactly matches the command passed in.
192             */
193            public static List<UnixProcess> getMatchingProcesses(List<UnixProcess> processes, String command) {
194                    List<UnixProcess> matches = new ArrayList<UnixProcess>();
195                    for (UnixProcess process : processes) {
196                            if (StringUtils.equals(process.getCommand(), command)) {
197                                    matches.add(process);
198                            }
199                    }
200                    return matches;
201            }
202    
203            /**
204             * Output looks like this:
205             * 
206             * <pre>
207             *   UID        PID  PPID  C STIME TTY          TIME CMD
208             *       tomcat   15461 15460  0 22:51 pts/0    00:00:00 -bash
209             *       tomcat   15480 15461  0 22:52 pts/0    00:00:02 java -jar /usr/local/machine-agent/machineagent.jar
210             * </pre>
211             */
212            public static List<UnixProcess> getUnixProcesses(Result result) {
213                    // Convert stdout to a list of strings
214                    List<String> lines = getOutputLines(result);
215    
216                    // Make sure there is at least a header line
217                    Assert.isFalse(CollectionUtils.isEmpty(lines), "There should be a header line");
218    
219                    // If there are no processes running, exit value is 1
220                    if (lines.size() == 1 && result.getExitValue() == 1) {
221                            // return an empty list
222                            return Collections.emptyList();
223                    }
224    
225                    // Make sure exit value was zero
226                    validateResult(result);
227    
228                    // Need the header line in order to parse the process lines
229                    String header = lines.get(0);
230    
231                    // Setup some storage for the list of running processes
232                    List<UnixProcess> processes = new ArrayList<UnixProcess>();
233    
234                    // Convert each line into a UnixProcess pojo
235                    for (int i = 1; i < lines.size(); i++) {
236    
237                            // Extract a line
238                            String line = lines.get(i);
239    
240                            // Convert to a pojo
241                            UnixProcess process = getUnixProcess(header, line);
242    
243                            // Add to the list
244                            processes.add(process);
245                    }
246    
247                    // return what we've found
248                    return processes;
249            }
250    
251            /**
252             * Output looks like this:
253             * 
254             * <pre>
255             *   UID        PID  PPID  C STIME TTY          TIME CMD
256             *       tomcat   15461 15460  0 22:51 pts/0    00:00:00 -bash
257             *       tomcat   15480 15461  0 22:52 pts/0    00:00:02 java -jar /usr/local/machine-agent/machineagent.jar
258             * </pre>
259             */
260            public static UnixProcess getUnixProcess(String header, String line) {
261                    // Split the strings up into tokens
262                    String[] tokens = StringUtils.split(line, " ");
263                    // First token is the user id
264                    String userId = StringUtils.trim(tokens[0]);
265                    // Second token is the process id
266                    String processId = StringUtils.trim(tokens[1]);
267                    // The command starts where "CMD" starts in the header line
268                    int pos = header.indexOf(CMD);
269                    // Make sure we found the string "CMD"
270                    Assert.isFalse(pos == -1, "[" + line + "] does not contain [" + CMD + "]");
271                    // This is the command used to launch the process
272                    String command = StringUtils.trim(StringUtils.substring(line, pos));
273    
274                    // Store the information we've parsed out into pojo
275                    UnixProcess process = new UnixProcess();
276                    process.setUserId(userId);
277                    process.setProcessId(Integer.parseInt(processId));
278                    process.setCommand(command);
279                    return process;
280            }
281    
282            public static Result executeCommand(SecureChannel channel, String command, boolean validateResult) {
283                    Result result = channel.executeCommand(command);
284                    if (validateResult) {
285                            validateResult(result);
286                    }
287                    return result;
288            }
289    
290            public static void kill(SecureChannel channel, UnixProcess process) {
291                    String command = CMDS.kill(process.getProcessId());
292                    Result result = channel.executeCommand(command);
293                    logResult(result, logger, LoggerLevel.DEBUG);
294                    validateResult(result);
295            }
296    
297            public static List<UnixProcess> getUnixProcesses(SecureChannel channel, String user) {
298                    String command = CMDS.psf(user);
299                    Result result = channel.executeCommand(command);
300                    return getUnixProcesses(result);
301    
302            }
303    
304            public static Result runscript(SecureChannel channel, String username, String script) {
305                    return executeCommand(channel, CMDS.su(username, script), true);
306            }
307    
308            public static Result runscript(SecureChannel channel, String username, String script, boolean validateExitValue) {
309                    return executeCommand(channel, CMDS.su(username, script), validateExitValue);
310            }
311    
312            public static Result delete(SecureChannel channel, List<String> paths) {
313                    return executePathCommand(channel, CMDS.rmrf(paths), paths);
314            }
315    
316            public static Result mkdirs(SecureChannel channel, List<String> paths) {
317                    return executePathCommand(channel, CMDS.mkdirp(paths), paths);
318            }
319    
320            public static Result chown(SecureChannel channel, String owner, String group, List<String> paths) {
321                    List<String> options = Arrays.asList(TRAVERSE_SYMBOLIC_LINKS);
322                    String cmd = CMDS.chownr(options, owner, group, paths);
323                    return executePathCommand(channel, cmd, paths);
324            }
325    
326            public static void executePathCommand(SecureChannel channel, String command, String path) {
327                    executePathCommand(channel, command, Collections.singletonList(path));
328            }
329    
330            public static Result executePathCommand(SecureChannel channel, String command, List<String> paths) {
331                    Result result = channel.executeCommand(command);
332                    validateResult(result);
333                    return result;
334            }
335    
336            public static List<String> getOutputLines(Result result) {
337                    try {
338                            return IOUtils.readLines(LocationUtils.getBufferedReaderFromString(result.getStdout()));
339                    } catch (IOException e) {
340                            throw new IllegalArgumentException("Unexpected IO error", e);
341                    }
342            }
343    
344            public static void logResult(Result result, Logger logger, LoggerLevel level) {
345                    LoggerUtils.logLines("[" + result.getCommand() + "] - " + FormatUtils.getTime(result.getElapsed()), logger, level);
346                    LoggerUtils.logLines(result.getStdout(), logger, level);
347                    LoggerUtils.logLines(result.getStderr(), logger, LoggerLevel.WARN);
348                    if (result.getExitValue() != 0) {
349                            logger.warn("Exit value = {}", result.getExitValue());
350                    }
351            }
352    
353            public static void logResult(Result result, Logger logger) {
354                    logResult(result, logger, LoggerLevel.INFO);
355            }
356    
357            public static void validateResult(Result result) {
358                    validateResult(result, Arrays.asList(0));
359                    logger.trace("Result is valid");
360            }
361    
362            public static void validateResult(Result result, List<Integer> exitValues) {
363                    for (Integer exitValue : exitValues) {
364                            if (exitValue.equals(result.getExitValue())) {
365                                    return;
366                            }
367                    }
368                    throw new IllegalStateException("Exit value " + result.getExitValue() + " is not allowed");
369            }
370    
371    }