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.LoggerLevel;
17  import org.kuali.common.util.LoggerUtils;
18  import org.kuali.common.util.MonitorTextFileResult;
19  import org.kuali.common.util.ThreadUtils;
20  import org.kuali.common.util.UnixCmds;
21  import org.kuali.common.util.UnixProcess;
22  import org.kuali.common.util.property.Constants;
23  import org.kuali.common.util.secure.RemoteFile;
24  import org.kuali.common.util.secure.Result;
25  import org.kuali.common.util.secure.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, no point in going any further
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(path);
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(deployable.getRemote());
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() != null) {
169 				String path = deployable.getRemote();
170 				String perms = deployable.getPermissions();
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 = DeployUtils.getOutputLines(result);
215 
216 		// Something has gone wrong
217 		if (CollectionUtils.isEmpty(lines)) {
218 			throw new IllegalStateException("There should be a header line");
219 		}
220 
221 		// If there are no processes running, exit value is 1
222 		if (lines.size() == 1 && result.getExitValue() == 1) {
223 			// return an empty list
224 			return Collections.emptyList();
225 		}
226 
227 		// Make sure exit value was zero
228 		DeployUtils.validateResult(result);
229 
230 		// Need the header line in order to parse the process lines
231 		String header = lines.get(0);
232 
233 		// Setup some storage for the list of running processes
234 		List<UnixProcess> processes = new ArrayList<UnixProcess>();
235 
236 		// Convert each line into a UnixProcess pojo
237 		for (int i = 1; i < lines.size(); i++) {
238 
239 			// Extract a line
240 			String line = lines.get(i);
241 
242 			// Convert to a pojo
243 			UnixProcess process = getUnixProcess(header, line);
244 
245 			// Add to the list
246 			processes.add(process);
247 		}
248 
249 		// return what we've found
250 		return processes;
251 	}
252 
253 	/**
254 	 * Output looks like this:
255 	 * 
256 	 * <pre>
257 	 *   UID        PID  PPID  C STIME TTY          TIME CMD
258 	 * 	 tomcat   15461 15460  0 22:51 pts/0    00:00:00 -bash
259 	 * 	 tomcat   15480 15461  0 22:52 pts/0    00:00:02 java -jar /usr/local/machine-agent/machineagent.jar
260 	 * </pre>
261 	 */
262 	public static UnixProcess getUnixProcess(String header, String line) {
263 		// Split the strings up into tokens
264 		String[] tokens = StringUtils.split(line, " ");
265 		// First token is the user id
266 		String userId = StringUtils.trim(tokens[0]);
267 		// Second token is the process id
268 		String processId = StringUtils.trim(tokens[1]);
269 		// The command starts where "CMD" starts in the header line
270 		int pos = header.indexOf(CMD);
271 		if (pos == -1) {
272 			throw new IllegalStateException("[" + line + "] does not contain [" + CMD + "]");
273 		}
274 		String command = StringUtils.trim(StringUtils.substring(line, pos));
275 
276 		//
277 		UnixProcess process = new UnixProcess();
278 		process.setUserId(userId);
279 		process.setProcessId(Integer.parseInt(processId));
280 		process.setCommand(command);
281 		return process;
282 	}
283 
284 	public static Result executeCommand(SecureChannel channel, String command, boolean validateResult) {
285 		Result result = channel.executeCommand(command);
286 		if (validateResult) {
287 			DeployUtils.validateResult(result);
288 		}
289 		return result;
290 	}
291 
292 	public static void kill(SecureChannel channel, UnixProcess process) {
293 		String command = CMDS.kill(process.getProcessId());
294 		Result result = channel.executeCommand(command);
295 		DeployUtils.logResult(result, logger, LoggerLevel.DEBUG);
296 		DeployUtils.validateResult(result);
297 	}
298 
299 	public static List<UnixProcess> getUnixProcesses(SecureChannel channel, String user) {
300 		String command = CMDS.psf(user);
301 		Result result = channel.executeCommand(command);
302 		return getUnixProcesses(result);
303 
304 	}
305 
306 	public static Result runscript(SecureChannel channel, String username, String script) {
307 		return executeCommand(channel, CMDS.su(username, script), true);
308 	}
309 
310 	public static Result runscript(SecureChannel channel, String username, String script, boolean validateExitValue) {
311 		return executeCommand(channel, CMDS.su(username, script), validateExitValue);
312 	}
313 
314 	public static Result delete(SecureChannel channel, List<String> paths) {
315 		return executePathCommand(channel, CMDS.rmrf(paths), paths);
316 	}
317 
318 	public static Result mkdirs(SecureChannel channel, List<String> paths) {
319 		return executePathCommand(channel, CMDS.mkdirp(paths), paths);
320 	}
321 
322 	public static Result chown(SecureChannel channel, String owner, String group, List<String> paths) {
323 		List<String> options = Arrays.asList(TRAVERSE_SYMBOLIC_LINKS);
324 		String cmd = CMDS.chownr(options, owner, group, paths);
325 		return executePathCommand(channel, cmd, paths);
326 	}
327 
328 	public static void executePathCommand(SecureChannel channel, String command, String path) {
329 		executePathCommand(channel, command, Collections.singletonList(path));
330 	}
331 
332 	public static Result executePathCommand(SecureChannel channel, String command, List<String> paths) {
333 		Result result = channel.executeCommand(command);
334 		validateResult(result);
335 		return result;
336 	}
337 
338 	public static List<String> getOutputLines(Result result) {
339 		try {
340 			return IOUtils.readLines(LocationUtils.getBufferedReaderFromString(result.getStdout()));
341 		} catch (IOException e) {
342 			throw new IllegalArgumentException("Unexpected IO error", e);
343 		}
344 	}
345 
346 	public static void logResult(Result result, Logger logger, LoggerLevel level) {
347 		LoggerUtils.logLines("[" + result.getCommand() + "] - " + FormatUtils.getTime(result.getElapsed()), logger, level);
348 		LoggerUtils.logLines(result.getStdout(), logger, level);
349 		LoggerUtils.logLines(result.getStderr(), logger, LoggerLevel.WARN);
350 		if (result.getExitValue() != 0) {
351 			logger.warn("Exit value = {}", result.getExitValue());
352 		}
353 	}
354 
355 	public static void logResult(Result result, Logger logger) {
356 		logResult(result, logger, LoggerLevel.INFO);
357 	}
358 
359 	public static void validateResult(Result result) {
360 		validateResult(result, Arrays.asList(0));
361 		logger.trace("Result is valid");
362 	}
363 
364 	public static void validateResult(Result result, List<Integer> exitValues) {
365 		for (Integer exitValue : exitValues) {
366 			if (exitValue.equals(result.getExitValue())) {
367 				return;
368 			}
369 		}
370 		throw new IllegalStateException("Exit value " + result.getExitValue() + " is not allowed");
371 	}
372 
373 }