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 }