View Javadoc

1   /*******************************************************************************
2    * Copyright (c) 2007, 2014 Massimiliano Ziccardi
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *******************************************************************************/
16  package it.jnrpe.plugin;
17  
18  import it.jnrpe.ICommandLine;
19  import it.jnrpe.ReturnValue;
20  import it.jnrpe.Status;
21  import it.jnrpe.plugin.utils.ShellUtils;
22  import it.jnrpe.plugins.PluginBase;
23  import it.jnrpe.plugins.annotations.Option;
24  import it.jnrpe.plugins.annotations.Plugin;
25  import it.jnrpe.plugins.annotations.PluginOptions;
26  import it.jnrpe.utils.BadThresholdException;
27  import it.jnrpe.utils.ThresholdUtil;
28  
29  import java.io.ByteArrayOutputStream;
30  import java.io.InputStream;
31  import java.math.BigDecimal;
32  import java.util.ArrayList;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Map.Entry;
37  import java.util.regex.Pattern;
38  
39  import org.apache.commons.io.IOUtils;
40  
41  /**
42   * Checks system processes and does check against threshold metrics.
43   * 
44   * @author Frederico Campos
45   * 
46   */
47  @Plugin(name = "CHECK_PROCS", description = "Checks system processes and does check against metrics. Default metrics is number of processes.")
48  @PluginOptions({
49  
50  		@Option(shortName = "w", longName = "warning", description = "Warning value if metric is out of range", required = false, hasArgs = true, argName = "warning", optionalArgs = false, option = "warning"),
51  		@Option(shortName = "c", longName = "critical", description = "Critical value if metric is out of range", required = false, hasArgs = true, argName = "critical", optionalArgs = false, option = "critical"),
52  
53  		@Option(shortName = "m", longName = "metric", description = "Metric type. Valid values are: PROCS - number of processes"
54  				+ "; VSZ - virtual memory size (unix only); RSS - resident set memory size (unix only); MEM - memory usage in KB (Windows only); CPU - CPU percentage; "
55  				+ "ELAPSED - elapsed time in seconds (unix only)", required = false, hasArgs = true, argName = "metric", optionalArgs = false, option = "metric"),
56  
57  		@Option(shortName = "a", longName = "argument-array", description = "Only scan for processes with args that contain STRING. (unix only). Use instead of ereg-argument-array.", required = false, hasArgs = true, argName = "argument-array", optionalArgs = false, option = "argument-array"),
58  		@Option(shortName = "e", longName = "ereg-argument-array", description = "Only scan for processes with args that contain the regex. (unix only). Use instead of argument-array.", required = false, hasArgs = true, argName = "ereg-argument-array", optionalArgs = false, option = "ereg-argument-array"),
59  
60  		@Option(shortName = "p", longName = "ppid", description = "Only scan for children of the parent process ID indicated (unix only).", required = false, hasArgs = true, argName = "ppid", optionalArgs = false, option = "ppid"),
61  
62  		@Option(shortName = "z", longName = "vsz", description = "Only scan for processes with VSZ higher than indicated (unix only).", required = false, hasArgs = true, argName = "vsz", optionalArgs = false, option = "vsz"),
63  		@Option(shortName = "r", longName = "rss", description = "Only scan for processes with RSS higher than indicated (unix only).", required = false, hasArgs = true, argName = "rss", optionalArgs = false, option = "rss"),
64  		@Option(shortName = "M", longName = "memory", description = "Only scan for processes with memory usage higher than indicated (windows only).", required = false, hasArgs = true, argName = "memory", optionalArgs = false, option = "memory"),
65  
66  		@Option(shortName = "C", longName = "command", description = "Only scan for exact matches of COMMAND (without path).", required = false, hasArgs = true, argName = "command", optionalArgs = false, option = "command"),
67  		@Option(shortName = "u", longName = "user", description = "Only scan for exact matches of USER", required = false, hasArgs = true, argName = "user", optionalArgs = false, option = "user") })
68  public class CheckProcs extends PluginBase {
69  
70  	private final static int SECONDS_IN_MINUTE = 60;
71  	private final static int SECONDS_IN_HOUR = SECONDS_IN_MINUTE * 60;
72  	private final static int SECONDS_IN_DAY = SECONDS_IN_HOUR * 24;
73  
74  	private final static String[] DEFAULT_WINDOWS_CMD = new String[] { "cmd",
75  			"/C", "tasklist /FO CSV /V" };
76  
77  	private final static String[] DEFAULT_UNIX_CMD = new String[] { "/bin/ps",
78  			"-eo", "comm,pid,ppid,user,c,rss,vsz,time,args" };
79  
80  	private final static String METRIC_PROCS = "PROCS";
81  
82  	private final static String METRIC_RSS = "RSS";
83  
84  	private final static String METRIC_VSZ = "VSZ";
85  
86  	private final static String METRIC_CPU = "CPU";
87  
88  	private final static String METRIC_ELAPSED = "ELAPSED";
89  
90  	private final static String METRIC_MEMORY = "MEMORY";
91  
92  	private final static String FILTER_COMMAND = "command";
93  
94  	private final static String FILTER_PPID = "ppid";
95  
96  	private final static String FILTER_VSZ = "vsz";
97  
98  	private final static String FILTER_RSS = "rss";
99  
100 	private final static String FILTER_USER = "user";
101 
102 	private final static String FILTER_ARG_ARRAY = "argument-array";
103 
104 	private final static String FILTER_EREG_ARG_ARRAY = "ereg-argument-array";
105 
106 	private final static String FILTER_MEMORY = "memory";
107 
108 	private final static String[] FILTERS = new String[] { FILTER_COMMAND,
109 			FILTER_PPID, FILTER_VSZ, FILTER_RSS, FILTER_USER, FILTER_ARG_ARRAY,
110 			FILTER_EREG_ARG_ARRAY };
111 
112 	private final static String[] UNIX_ONLY = new String[] { FILTER_ARG_ARRAY,
113 			FILTER_RSS, FILTER_PPID, FILTER_VSZ, FILTER_EREG_ARG_ARRAY,
114 
115 	};
116 
117 	// private final static String UNIX_TMP_FILE = "/tmp/checkprocs.out";
118 
119 	@Override
120 	protected String getPluginName() {
121 		return "CHECK_PROCS";
122 	}
123 
124 	/**
125 	 * Execute the plugin
126 	 * 
127 	 * @param cl
128 	 * @return
129 	 * @throws BadThresholdException
130 	 */
131 	@Override
132 	public ReturnValue execute(final ICommandLine cl)
133 			throws BadThresholdException {
134 		boolean windows = ShellUtils.isWindows();
135 		try {
136 			String metric = cl.getOptionValue("metric");
137 			if (metric == null) {
138 				metric = METRIC_PROCS;
139 			}
140 			metric = metric.toUpperCase();
141 
142 			validateArguments(cl, windows, metric);
143 
144 			String output = exec(!windows);
145 
146 			List<Map<String, String>> result = windows ? parseWindowsOutput(output)
147 					: parseUnixOutput(output);
148 			return analyze(result, cl, metric);
149 		} catch (Exception e) {
150 			e.printStackTrace();
151 			throw new BadThresholdException(e);
152 		}
153 	}
154 
155 	/**
156 	 * Checks command line arguments for operating system specific filters and
157 	 * metrics
158 	 * 
159 	 * @param output
160 	 * @param cl
161 	 * @param metric
162 	 * @return
163 	 */
164 	private void validateArguments(ICommandLine cl, boolean windows,
165 			String metric) throws Exception {
166 		if (windows) {
167 			if (metric.equals(METRIC_VSZ) || metric.equals(METRIC_RSS)
168 					|| metric.endsWith(METRIC_ELAPSED)) {
169 				throw new Exception("Metric " + metric
170 						+ " not supported in Wndows.");
171 			} else {
172 				for (String opt : UNIX_ONLY) {
173 					if (cl.getOptionValue("opt") != null) {
174 						throw new Exception("Option " + opt
175 								+ " is not supported in Windows.");
176 					}
177 				}
178 			}
179 		} else {
180 			if (metric.equals(METRIC_MEMORY)) {
181 				throw new Exception("Metric " + metric
182 						+ " not supported in unix.");
183 			}
184 		}
185 	}
186 
187 	/**
188 	 * Analyze output and gather metrics
189 	 * 
190 	 * @param output
191 	 * @param cl
192 	 * @param metric
193 	 * @return
194 	 */
195 	private ReturnValue analyze(List<Map<String, String>> output,
196 			ICommandLine cl, String metric) {
197 		Map<String, String> filterAndValue = getFilterAndValue(cl);
198 		output = applyFilters(output, filterAndValue);
199 		String message = getMessage(filterAndValue);
200 		String critical = cl.getOptionValue("critical");
201 		String warning = cl.getOptionValue("warning");
202 
203 		ReturnValue retVal = null;
204 		try {
205 			if (metric.equals(METRIC_PROCS)) {
206 				retVal = analyzeProcMetrics(output, cl, critical, warning,
207 						message);
208 			} else {
209 				retVal = analyzeMetrics(output, cl, critical, warning, message,
210 						metric);
211 			}
212 		} catch (Exception e) {
213 			e.printStackTrace();
214 		}
215 		return retVal;
216 	}
217 
218 	private ReturnValue analyzeProcMetrics(List<Map<String, String>> output,
219 			ICommandLine cl, String critical, String warning, String message)
220 			throws Exception {
221 		int size = output.size();
222 		if (critical != null) {
223 			if (ThresholdUtil.isValueInRange(critical, new BigDecimal(size))) {
224 				return new ReturnValue(Status.CRITICAL, "PROCS CRITICAL: "
225 						+ message + " " + size + " processes.");
226 			}
227 		}
228 		if (warning != null) {
229 			if (ThresholdUtil.isValueInRange(warning, new BigDecimal(size))) {
230 				return new ReturnValue(Status.WARNING, "PROCS WARNING: "
231 						+ message + " " + size + " processes.");
232 			}
233 		}
234 		return new ReturnValue(Status.OK, "PROCS OK: " + message + " " + size
235 				+ " processes.");
236 	}
237 
238 	/**
239 	 * Analyze process cpu thresholds
240 	 * 
241 	 * @param output
242 	 * @param cl
243 	 * @param critical
244 	 * @param warning
245 	 * @param message
246 	 * @return
247 	 * @throws Exception
248 	 */
249 	private ReturnValue analyzeMetrics(List<Map<String, String>> output,
250 			ICommandLine cl, String critical, String warning, String message,
251 			String metric) throws Exception {
252 
253 		if (critical != null) {
254 			int checkCritical = compareMetric(output, critical,
255 					metric.toUpperCase());
256 			if (checkCritical > 0) {
257 				return new ReturnValue(Status.CRITICAL, metric.toUpperCase()
258 						+ " CRITCAL: " + message + " "
259 						+ (output.size() - checkCritical) + " critical out of "
260 						+ output.size() + " processes.");
261 			}
262 		}
263 		if (warning != null) {
264 			int checkWarning = compareMetric(output, warning, metric);
265 			if (checkWarning > 0) {
266 				return new ReturnValue(Status.WARNING, metric.toUpperCase()
267 						+ " WARNING: " + message + " "
268 						+ (output.size() - checkWarning) + " warning out of "
269 						+ output.size() + " processes.");
270 			}
271 		}
272 		return new ReturnValue(Status.OK, metric.toUpperCase() + " OK: "
273 				+ message + " " + output.size() + " processes.");
274 	}
275 
276 	@SuppressWarnings("deprecation")
277 	private int compareMetric(List<Map<String, String>> output, String value,
278 			String metric) throws BadThresholdException {
279 		List<Map<String, String>> list = new ArrayList<Map<String, String>>();
280 		for (Map<String, String> values : output) {
281 			int procValue = Integer.parseInt(values.get(metric.toLowerCase()));
282 			if (ThresholdUtil.isValueInRange(value, procValue)) {
283 				list.add(values);
284 			}
285 		}
286 		return list.size();
287 	}
288 
289 	/**
290 	 * Get parameter list in return message
291 	 * 
292 	 * @param filterAndValue
293 	 * @return
294 	 */
295 	private String getMessage(Map<String, String> filterAndValue) {
296 
297 		String msgString = "";
298 		if (!filterAndValue.isEmpty()) {
299 			StringBuilder msg = new StringBuilder();
300 			msg.append("with ");
301 
302 			for (Entry<String, String> entry : filterAndValue.entrySet()) {
303 				msg.append(entry.getKey()).append(" = ")
304 						.append(entry.getValue()).append(", ");
305 			}
306 
307 			msgString = msg.toString().trim();
308 			if (msgString.endsWith(", ")) {
309 				msgString = msgString.substring(0, msgString.length() - 2);
310 			}
311 		}
312 		return msgString;
313 	}
314 
315 	/**
316 	 * Execute a system command and return the output.
317 	 * 
318 	 * @param command
319 	 * @return String
320 	 */
321 	private String exec(boolean unix) throws Exception {
322 		String output = null;
323 		InputStream input = null;
324 		String[] command = null;
325 		if (unix) {
326 			// write output to tmp file
327 			command = DEFAULT_UNIX_CMD;
328 			Process p = Runtime.getRuntime().exec(command);
329 			input = p.getInputStream();
330 
331 			ByteArrayOutputStream bout = new ByteArrayOutputStream();
332 			IOUtils.copy(input, bout);
333 			output = getFormattedOutput(new String(bout.toByteArray(), "UTF-8"));
334 			input.close();
335 
336 		} else {
337 			command = DEFAULT_WINDOWS_CMD;
338 			output = ShellUtils.executeSystemCommandAndGetOutput(command,
339 					"CP437");
340 		}
341 
342 		return output;
343 	}
344 
345 	/**
346 	 * Comma separate the command output
347 	 */
348 	private String getFormattedOutput(String output) {
349 		String out = "";
350 		StringBuilder lines = new StringBuilder();
351 		String[] splittedLines = output.split("\n");
352 		for (int i = 0; i < splittedLines.length; i++) {
353 			if (i == 0) {
354 				continue;
355 			}
356 			String splittedLine = splittedLines[i];
357 			if (splittedLine.contains("<defunct>")) {
358 				continue;
359 			}
360 
361 			String line = splittedLine.replaceAll("\\s+", ",");
362 
363 			lines.append(line).append('\n');
364 		}
365 		out = lines.toString();
366 		return out;
367 	}
368 
369 	/**
370 	 * Parse command output for windows environment
371 	 * 
372 	 * @param output
373 	 * @return List<Map<String,String>>
374 	 */
375 
376 	private List<Map<String, String>> parseWindowsOutput(String output) {
377 		List<Map<String, String>> info = new ArrayList<Map<String, String>>();
378 		String[] lines = output.split("\n");
379 
380 		int totalRunTime = 0;
381 		String cpu = METRIC_CPU.toLowerCase();
382 		int count = 0;
383 		for (String l : lines) {
384 			if (count == 0) {
385 				count++;
386 				continue;
387 			}
388 
389 			Map<String, String> values = new HashMap<String, String>();
390 			String[] line = l.replaceAll("\"", "").split(",");
391 			values.put(FILTER_COMMAND, line[0]);
392 			values.put("pid", line[1]);
393 			values.put(FILTER_MEMORY, "" + convertToMemoryInt(line[4]));
394 			values.put(FILTER_USER, line[6]);
395 			int seconds = 0;
396 			if (!ShellUtils.isWindowsIdleProc(line[0])) {
397 				seconds = convertToSeconds(line[7].trim());
398 			}
399 			totalRunTime += seconds;
400 			values.put(cpu, Integer.toString(seconds));
401 			info.add(values);
402 		}
403 
404 		for (Map<String, String> map : info) {
405 			int secs = Integer.parseInt(map.get(cpu));
406 			log.debug("secs " + secs);
407 			double perc = ((double) secs / (double) totalRunTime) * 100.0;
408 			map.put(cpu, Integer.toString((int) perc));
409 		}
410 
411 		log.debug(info + "");
412 		return info;
413 	}
414 
415 	/**
416 	 * Parse command output for unix environment
417 	 * 
418 	 * @param output
419 	 * @return List<Map<String,String>>
420 	 */
421 	private List<Map<String, String>> parseUnixOutput(String output) {
422 		List<Map<String, String>> info = new ArrayList<Map<String, String>>();
423 		output = output.replaceAll("\"", "");
424 		String[] lines = output.split("\n");
425 		for (String l : lines) {
426 			if (l.startsWith("PID")) {
427 				continue;
428 			}
429 
430 			String[] line = l.split(",", 9);
431 
432 			// FIXME : the ps command itself should be skipped
433 			// if (line[8].contains(DEFAULT_UNIX_CMD[2])) {
434 			// // continue;
435 			// }
436 
437 			Map<String, String> values = new HashMap<String, String>();
438 			values.put(FILTER_COMMAND, line[0].trim());
439 			values.put("pid", line[1].trim());
440 			values.put("ppid", line[2].trim());
441 			values.put(FILTER_USER, line[3].trim());
442 			values.put(METRIC_CPU.toLowerCase(), line[4].trim());
443 			values.put(METRIC_RSS.toLowerCase(), line[5].trim());
444 			values.put(METRIC_VSZ.toLowerCase(), line[6].trim());
445 			values.put(METRIC_ELAPSED.toLowerCase(),
446 					convertToSeconds(line[7].trim()) + "");
447 			values.put(FILTER_ARG_ARRAY, line[8]);
448 
449 			info.add(values);
450 		}
451 		return info;
452 	}
453 
454 	private int convertToMemoryInt(String mem) {
455 		mem = mem.replaceAll("[^0-9]", "");
456 
457 		return Integer.parseInt(mem);
458 	}
459 
460 	/**
461 	 * Apply filters to processes output
462 	 * 
463 	 * @param values
464 	 * @param filterAndValue
465 	 * @return
466 	 */
467 	private List<Map<String, String>> applyFilters(
468 			List<Map<String, String>> values, Map<String, String> filterAndValue) {
469 		if (filterAndValue == null || filterAndValue.size() == 0) {
470 			return values;
471 		}
472 		List<Map<String, String>> filtered = new ArrayList<Map<String, String>>();
473 		for (Map<String, String> map : values) {
474 			boolean matchesAll = true;
475 			for (Entry<String, String> entry : filterAndValue.entrySet()) {
476 				String filter = entry.getKey();
477 				String filterValue = entry.getValue();
478 				if (filter.contains(FILTER_COMMAND)
479 						|| filter.contains(FILTER_USER)
480 						|| filter.equals(FILTER_ARG_ARRAY)
481 						|| filter.contains(FILTER_PPID)) {
482 					if (!map.get(filter).contains(filterValue)) {
483 						matchesAll = false;
484 						break;
485 					}
486 				} else if (filter.contains(FILTER_EREG_ARG_ARRAY)
487 						&& !Pattern.matches(filterValue,
488 								map.get(FILTER_ARG_ARRAY))) {
489 					matchesAll = false;
490 					break;
491 				} else if (filter.contains(FILTER_VSZ)
492 						|| filter.contains(FILTER_RSS)
493 						|| filter.contains(FILTER_MEMORY)) {
494 					int filterval = Integer.parseInt(filterValue);
495 					int value = Integer.parseInt(map.get(filter));
496 					if (value < filterval) {
497 						matchesAll = false;
498 						break;
499 					}
500 				}
501 			}
502 			if (matchesAll) {
503 				filtered.add(map);
504 			}
505 		}
506 		return filtered;
507 	}
508 
509 	private Map<String, String> getFilterAndValue(ICommandLine cl) {
510 		Map<String, String> map = new HashMap<String, String>();
511 		for (String filter : FILTERS) {
512 			if (cl.getOptionValue(filter) != null) {
513 				map.put(filter, cl.getOptionValue(filter));
514 			}
515 		}
516 		return map;
517 	}
518 
519 	/**
520 	 * Convert date in format DD-HH:MM:SS to seconds.
521 	 * 
522 	 * @param input
523 	 * @return
524 	 */
525 	private int convertToSeconds(final String input) {
526 		int days = 0;
527 		int hours = 0;
528 		int minutes = 0;
529 		int seconds = 0;
530 
531 		String remainingTokens = input;
532 
533 		if (input.indexOf('-') != -1) {
534 			String[] parts = remainingTokens.split("-");
535 			days = Integer.parseInt(parts[0]);
536 			remainingTokens = parts[1];
537 		}
538 
539 		String[] timeParts = remainingTokens.split(":");
540 
541 		for (int i = timeParts.length - 1, partType = 0; i >= 0; i--, partType++) {
542 			switch (partType) {
543 			case 0: // Seconds
544 				seconds = Integer.parseInt(timeParts[i]);
545 				break;
546 			case 1: // Minutes
547 				minutes = Integer.parseInt(timeParts[i]);
548 				break;
549 			case 2: // Hours
550 				hours = Integer.parseInt(timeParts[i]);
551 				break;
552 			default: // bad input
553 			}
554 		}
555 
556 		return (days * SECONDS_IN_DAY) + (hours * SECONDS_IN_HOUR)
557 				+ (minutes * SECONDS_IN_MINUTE) + seconds;
558 	}
559 }