View Javadoc
1   package org.kuali.common.deploy;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.util.ArrayList;
6   import java.util.Arrays;
7   import java.util.Collections;
8   import java.util.List;
9   import java.util.Properties;
10  
11  import org.apache.commons.io.IOUtils;
12  import org.codehaus.plexus.util.StringUtils;
13  import org.kuali.common.util.Assert;
14  import org.kuali.common.util.FormatUtils;
15  import org.kuali.common.util.LocationUtils;
16  import org.kuali.common.util.MonitorTextFileResult;
17  import org.kuali.common.util.ThreadUtils;
18  import org.kuali.common.util.UnixCmds;
19  import org.kuali.common.util.UnixProcess;
20  import org.kuali.common.util.log.LoggerLevel;
21  import org.kuali.common.util.log.LoggerUtils;
22  import org.kuali.common.util.property.Constants;
23  import org.kuali.common.util.secure.channel.RemoteFile;
24  import org.kuali.common.util.secure.channel.Result;
25  import org.kuali.common.util.secure.channel.SecureChannel;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  import org.springframework.util.CollectionUtils;
29  import org.springframework.util.PropertyPlaceholderHelper;
30  
31  public class DeployUtils {
32  
33  	private static final Logger logger = LoggerFactory.getLogger(DeployUtils.class);
34  
35  	private static final String CMD = "CMD";
36  	private static final String TRAVERSE_SYMBOLIC_LINKS = "-L";
37  	private static final UnixCmds CMDS = new UnixCmds();
38  	private static final PropertyPlaceholderHelper HELPER = Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER;
39  
40  	/**
41  	 * 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.
42  	 */
43  	public static MonitorTextFileResult monitorTextFile(SecureChannel channel, String path, String token, int intervalMillis, int timeoutMillis, String encoding) {
44  
45  		// Make sure we are configured correctly
46  		Assert.notNull(channel, "channel is null");
47  		Assert.notNull(path, "path is null");
48  		Assert.hasText(token, "token has no text");
49  		Assert.hasText(encoding, "encoding has no text");
50  		Assert.isTrue(intervalMillis > 0, "interval must be a positive integer");
51  		Assert.isTrue(timeoutMillis > 0, "timeout must be a positive integer");
52  
53  		// Setup some member variables to record what happens
54  		long start = System.currentTimeMillis();
55  		long stop = start + timeoutMillis;
56  		boolean exists = false;
57  		boolean contains = false;
58  		boolean timeoutExceeded = false;
59  		long now = -1;
60  		String content = null;
61  
62  		// loop until timeout is exceeded or we find the token inside the file
63  		for (;;) {
64  
65  			// Always pause (unless this is the first iteration)
66  			if (now != -1) {
67  				ThreadUtils.sleep(intervalMillis);
68  			}
69  
70  			// Check to make sure we haven't exceeded our timeout limit
71  			now = System.currentTimeMillis();
72  			if (now > stop) {
73  				timeoutExceeded = true;
74  				break;
75  			}
76  
77  			// If the file does not exist yet, there is nothing more we can do
78  			exists = channel.exists(path);
79  			if (!exists) {
80  				continue;
81  			}
82  
83  			// The file exists, check to see if the token we are looking for is present in the file
84  			RemoteFile remoteFile = new RemoteFile.Builder(path).build();
85  			content = channel.toString(remoteFile);
86  			contains = StringUtils.contains(content, token);
87  			if (contains) {
88  				// We found what we are looking for, we are done
89  				break;
90  			}
91  		}
92  
93  		// Record how long the overall process took
94  		long elapsed = now - start;
95  
96  		// Fill in a pojo detailing what happened
97  		MonitorTextFileResult mtfr = new MonitorTextFileResult(exists, contains, timeoutExceeded, elapsed);
98  		mtfr.setAbsolutePath(path);
99  		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 }