org.rhq.plugins.script.ScriptServerComponent Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rhq-script-plugin Show documentation
Show all versions of rhq-script-plugin Show documentation
A plugin for managing resources that have command line executables or scripts as their management interface.
/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.plugins.script;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.Property;
import org.rhq.core.domain.configuration.PropertyList;
import org.rhq.core.domain.configuration.PropertyMap;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.measurement.DataType;
import org.rhq.core.domain.measurement.MeasurementDataNumeric;
import org.rhq.core.domain.measurement.MeasurementDataTrait;
import org.rhq.core.domain.measurement.MeasurementReport;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.pluginapi.inventory.InvalidPluginConfigurationException;
import org.rhq.core.pluginapi.inventory.ResourceComponent;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.pluginapi.measurement.MeasurementFacet;
import org.rhq.core.pluginapi.operation.OperationFacet;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.core.system.ProcessExecution;
import org.rhq.core.system.ProcessExecutionResults;
import org.rhq.core.system.SystemInfo;
import org.rhq.core.util.exception.ThrowableUtil;
/**
* Represents a managed resource whose management interface is a command line executable or script,
* sometimes referred to as the "CLI" (command line interface).
*
* @author John Mazzitelli
*/
public class ScriptServerComponent implements ResourceComponent, MeasurementFacet, OperationFacet {
private final Log log = LogFactory.getLog(ScriptServerComponent.class);
private static final long DEFAULT_MAX_WAIT_TIME = 3600000L;
protected static final String PLUGINCONFIG_EXECUTABLE = "executable";
protected static final String PLUGINCONFIG_WORKINGDIR = "workingDirectory";
protected static final String PLUGINCONFIG_ENVVARS = "environmentVariables";
protected static final String PLUGINCONFIG_ENVVAR_NAME = "name";
protected static final String PLUGINCONFIG_ENVVAR_VALUE = "value";
protected static final String PLUGINCONFIG_AVAIL_EXECUTE_CHECK = "availabilityExecuteCheck";
protected static final String PLUGINCONFIG_AVAIL_EXITCODE_REGEX = "availabilityExitCodeRegex";
protected static final String PLUGINCONFIG_AVAIL_OUTPUT_REGEX = "availabilityOutputRegex";
protected static final String PLUGINCONFIG_AVAIL_ARGS = "availabilityArguments";
protected static final String PLUGINCONFIG_VERSION_ARGS = "versionArguments";
protected static final String PLUGINCONFIG_VERSION_REGEX = "versionRegex";
protected static final String PLUGINCONFIG_FIXED_VERSION = "fixedVersion";
protected static final String PLUGINCONFIG_DESC_ARGS = "descriptionArguments";
protected static final String PLUGINCONFIG_DESC_REGEX = "descriptionRegex";
protected static final String PLUGINCONFIG_FIXED_DESC = "fixedDescription";
protected static final String OPERATION_PARAM_ARGUMENTS = "arguments";
protected static final String OPERATION_PARAM_WAIT_TIME = "waitTime";
protected static final String OPERATION_PARAM_CAPTURE_OUTPUT = "captureOutput";
protected static final String OPERATION_PARAM_KILL_ON_TIMEOUT = "killOnTimeout";
protected static final String OPERATION_RESULT_EXITCODE = "exitCode";
protected static final String OPERATION_RESULT_OUTPUT = "output";
protected static final String METRIC_PROPERTY_ARGUMENTS = "arguments";
protected static final String METRIC_PROPERTY_REGEX = "regex";
protected static final String METRIC_PROPERTY_EXITCODE = "exitcode";
private Configuration resourceConfiguration;
private ResourceContext resourceContext;
public void start(ResourceContext context) {
if (log.isDebugEnabled()) {
log.debug("Script Server started: " + context.getPluginConfiguration());
}
this.resourceContext = context;
}
public void stop() {
if (log.isDebugEnabled()) {
log.debug("Script Server stopped: " + this.resourceContext.getPluginConfiguration());
}
}
public AvailabilityType getAvailability() {
boolean result = checkAvailability();
return result ? AvailabilityType.UP : AvailabilityType.DOWN;
}
/**
* Executes the CLI and based on the measurement property, collects the appropriate measurement value.
* This supports measurement data and traits.
* A metric property can be a string that is assumed the arguments to pass to the CLI. However, if
* the property is in the form "{arguments}|regex", the arguments are passed to the CLI and the metric
* value is taken from the regex match. Examples of metric properties:
*
*
* - "-a --arg2=123" - passes that string as the arguments list, the metric value is the output of the CLI
* - "{}|exitcode" - no arguments are passed to the CLI and the metric value is the exit code of the CLI
* - "{-a}|[lL]abel(\p{Digit}+)[\r\n]*" - "-a" is the argument passed to the CLI and the metric value is the digits following the label of the output
* - "{}|[ABC]+" - no arguments are passed and the value is the output of the CLI assuming it matches the regex
* - "{--foobar}|foobar (.*) blah" - passes "--foobar" as the argument and the metric value is the string that matches the regex group
*
*
* @see MeasurementFacet#getValues(MeasurementReport, Set)
*/
public void getValues(MeasurementReport report, Set requests) {
Map exeResultsCache = new HashMap();
for (MeasurementScheduleRequest request : requests) {
String metricPropertyName = request.getName();
boolean dataMustBeNumeric;
if (request.getDataType() == DataType.MEASUREMENT) {
dataMustBeNumeric = true;
} else if (request.getDataType() == DataType.TRAIT) {
dataMustBeNumeric = false;
} else {
log.error("Plugin does not support metric [" + metricPropertyName + "] of type ["
+ request.getDataType() + "]");
continue;
}
try {
// determine how to execute the CLI for this metric
Map argsRegex = parseMetricProperty(metricPropertyName);
Object dataValue;
if (argsRegex == null) {
continue; // this metric's property was invalid, skip this one and move on to the next
}
String arguments = argsRegex.get(METRIC_PROPERTY_ARGUMENTS);
String regex = argsRegex.get(METRIC_PROPERTY_REGEX);
boolean valueIsExitCode = METRIC_PROPERTY_EXITCODE.equals(regex);
// if we already executed it with the same arguments, don't bother doing it again
ProcessExecutionResults exeResults = exeResultsCache.get((arguments == null) ? "" : arguments);
if (exeResults == null) {
boolean captureOutput = !valueIsExitCode; // don't need output if we need to just check exit code
exeResults = executeExecutable(arguments, DEFAULT_MAX_WAIT_TIME, captureOutput);
exeResultsCache.put((arguments == null) ? "" : arguments, exeResults);
}
// don't report a metric value if the CLI failed to execute
if (exeResults.getError() != null) {
log.error("Cannot collect CLI metric [" + metricPropertyName + "]. Cause: "
+ ThrowableUtil.getAllMessages(exeResults.getError()));
continue;
}
// set dataValue to the appropriate value based on how the metric property defined it
if (valueIsExitCode) {
dataValue = exeResults.getExitCode();
if (dataValue == null) {
log.error("Could not determine exit code for metric property [" + metricPropertyName
+ "] - metric will not be collected");
continue;
}
} else if (regex != null) {
final String output = exeResults.getCapturedOutput();
if (output == null) {
log.error("Could not get output for metric property [" + metricPropertyName
+ "] -- metric will not be collected");
continue;
}
Pattern pattern = Pattern.compile(regex);
Matcher match = pattern.matcher(output);
if (match.find()) {
if (match.groupCount() > 0) {
dataValue = match.group(1);
} else {
dataValue = output;
}
} else {
log.error("Output did not match metric property [" + metricPropertyName
+ "] - metric will not be collected: " + truncateString(output));
continue;
}
} else {
dataValue = exeResults.getCapturedOutput();
if (dataValue == null) {
log.error("Could not get output for metric property [" + metricPropertyName
+ "] - metric will not be collected");
continue;
}
}
// add the metric value to the measurement report
if (dataMustBeNumeric) {
Double numeric = Double.parseDouble(dataValue.toString().trim());
report.addData(new MeasurementDataNumeric(request, numeric.doubleValue()));
} else {
report.addData(new MeasurementDataTrait(request, dataValue.toString().trim()));
}
} catch (Exception e) {
log.error("Failed to obtain measurement [" + metricPropertyName + "]. Cause: " + e);
}
}
exeResultsCache.clear(); // help out the garbage collector, since this could be alot of data
exeResultsCache = null;
return;
}
/**
* Given a metric property name, this parses it into its different pieces and returns a
* map of the different tokens. The format of the metric property one of the following:
*
* arguments
* {arguments}|regex
* {arguments}|exitcode
*
*
* where "arguments" is the empty or non-empty string of the arguments to pass to the CLI executable,
* regex is a empty or non-empty regular expresssion string to match the output (if there is a matching
* group in the regex, its value will be used as the metric value, not the full output of the executable),
* exitcode is the literal string "exitcode" to indicate that the exit code value is to be used as the metric value.
*
* @param metricPropertyName the name of the property in the metric descriptor
* @return map containing the pieces that have been parsed out - the keys are
* either {@link #METRIC_PROPERTY_ARGUMENTS} or {@link #METRIC_PROPERTY_REGEX}.
* The map will be null
if the property name is invalid for some reason.
* A map entry will not exist if it was not specified in the property name.
*/
protected Map parseMetricProperty(String metricPropertyName) {
HashMap map = new HashMap();
if (metricPropertyName != null && metricPropertyName.length() > 0) {
if (!metricPropertyName.startsWith("{")) {
map.put(METRIC_PROPERTY_ARGUMENTS, metricPropertyName); // the property is entirely the arguments
} else {
String[] argsRegex = metricPropertyName.substring(1).split("\\}\\|", 2);
if (argsRegex.length != 2) {
log.error("Invalid metric property [" + metricPropertyName + "] - metric will not be collected");
return null;
}
if (!isValidRegularExpression(argsRegex[1])) {
log.error("Invalid regex [" + argsRegex[1] + "] for metric property [" + metricPropertyName
+ "] - metric will not be collected");
return null;
}
if (argsRegex[0].length() > 0) {
map.put(METRIC_PROPERTY_ARGUMENTS, argsRegex[0]);
}
if (argsRegex[1].length() > 0) {
map.put(METRIC_PROPERTY_REGEX, argsRegex[1]);
}
}
}
return map;
}
/**
* Invokes the CLI executable. User can tell us what arguments to pass to the executable.
* The result includes the output of the process as well as the exit code.
*
* @see OperationFacet#invokeOperation(String, Configuration)
*/
public OperationResult invokeOperation(String name, Configuration configuration) throws Exception {
OperationResult result = new OperationResult();
String arguments = configuration.getSimpleValue(OPERATION_PARAM_ARGUMENTS, null);
String waitTimeStr = configuration.getSimpleValue(OPERATION_PARAM_WAIT_TIME, null);
String captureOutputStr = configuration.getSimpleValue(OPERATION_PARAM_CAPTURE_OUTPUT, null);
String killOnTimeoutStr = configuration.getSimpleValue(OPERATION_PARAM_KILL_ON_TIMEOUT, null);
long waitTime;
boolean captureOutput;
boolean killOnTimeout;
if (waitTimeStr != null) {
try {
waitTime = Long.parseLong(waitTimeStr);
waitTime *= 1000L; // the parameter is specified in seconds, but we need it in milliseconds
} catch (NumberFormatException e) {
throw new NumberFormatException("Wait time parameter value is invalid: " + waitTimeStr);
}
} else {
waitTime = DEFAULT_MAX_WAIT_TIME;
}
if (captureOutputStr != null) {
captureOutput = Boolean.parseBoolean(captureOutputStr);
} else {
captureOutput = true;
}
if (killOnTimeoutStr != null) {
killOnTimeout = Boolean.parseBoolean(killOnTimeoutStr);
} else {
killOnTimeout = true;
}
ProcessExecutionResults exeResults = executeExecutable(arguments, waitTime, captureOutput, killOnTimeout);
Integer exitcode = exeResults.getExitCode();
String output = exeResults.getCapturedOutput();
Throwable error = exeResults.getError();
if (error != null) {
result.setErrorMessage(ThrowableUtil.getAllMessages(error));
}
Configuration resultsConfig = result.getComplexResults();
if (exitcode != null) {
resultsConfig.put(new PropertySimple(OPERATION_RESULT_EXITCODE, exitcode));
}
if (output != null) {
resultsConfig.put(new PropertySimple(OPERATION_RESULT_OUTPUT, output));
}
return result;
}
protected ResourceContext getResourcContext() {
return this.resourceContext;
}
/**
* Executes the CLI executable with the given arguments.
*
* Same as {@link #executeExecutable(String, long, boolean, boolean)} with 'killOnTimeout' being true.
*
* @return the results of the execution
*
* @throws InvalidPluginConfigurationException
*/
protected ProcessExecutionResults executeExecutable(String args, long wait, boolean captureOutput) {
return executeExecutable(args, wait, captureOutput, true);
}
/**
* Executes the CLI executable with the given arguments.
*
* @param args the arguments to send to the executable (may be null
)
* @param wait the maximum time in milliseconds to wait for the process to execute; 0 means do not wait
* @param captureOutput if true
, the executables output will be captured and returned
* @param killOnTimeout if true
and if 'wait' is greater than 0, the process will be killed if it times out
*
* @return the results of the execution
*
* @throws InvalidPluginConfigurationException
*/
protected ProcessExecutionResults executeExecutable(String args, long wait, boolean captureOutput,
boolean killOnTimeout)
throws InvalidPluginConfigurationException {
SystemInfo sysInfo = this.resourceContext.getSystemInformation();
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
ProcessExecutionResults results = executeExecutable(sysInfo, pluginConfig, args, wait, captureOutput,
killOnTimeout);
if (log.isDebugEnabled()) {
logDebug("CLI results: exitcode=[" + results.getExitCode() + "]; error=[" + results.getError()
+ "]; output=" + truncateString(results.getCapturedOutput()));
}
return results;
}
// This is protected static so the discovery component can use it.
protected static ProcessExecutionResults executeExecutable(SystemInfo sysInfo, Configuration pluginConfig,
String args, long wait, boolean captureOutput) throws InvalidPluginConfigurationException {
return executeExecutable(sysInfo, pluginConfig, args, wait, captureOutput, true);
}
private static ProcessExecutionResults executeExecutable(SystemInfo sysInfo, Configuration pluginConfig,
String args, long wait, boolean captureOutput, boolean killOnTimeout)
throws InvalidPluginConfigurationException {
ProcessExecution processExecution = getProcessExecutionInfo(pluginConfig);
if (args != null) {
processExecution.setArguments(args.split(" "));
}
processExecution.setCaptureOutput(captureOutput);
processExecution.setWaitForCompletion(wait);
processExecution.setKillOnTimeout(killOnTimeout);
ProcessExecutionResults results = sysInfo.executeProcess(processExecution);
return results;
}
private boolean checkAvailability() throws InvalidPluginConfigurationException {
String executable;
boolean availExecuteCheck = false;
String availArgs = null;
String availExitCodeRegex = null;
String availOutputRegex = null;
// determine how we are to consider the CLI available by looking at the plugin configuration
try {
Configuration pc = this.resourceContext.getPluginConfiguration();
executable = pc.getSimpleValue(PLUGINCONFIG_EXECUTABLE, null);
if (executable == null) {
throw new Exception("Missing executable in plugin configuraton");
}
PropertySimple availExecuteCheckProp = pc.getSimple(PLUGINCONFIG_AVAIL_EXECUTE_CHECK);
PropertySimple availArgsProp = pc.getSimple(PLUGINCONFIG_AVAIL_ARGS);
PropertySimple availExitCodeRegexProp = pc.getSimple(PLUGINCONFIG_AVAIL_EXITCODE_REGEX);
PropertySimple availOutputRegexProp = pc.getSimple(PLUGINCONFIG_AVAIL_OUTPUT_REGEX);
if (availExecuteCheckProp != null && availExecuteCheckProp.getBooleanValue() != null) {
availExecuteCheck = availExecuteCheckProp.getBooleanValue().booleanValue();
}
if (availArgsProp != null && availArgsProp.getStringValue() != null) {
availArgs = availArgsProp.getStringValue();
}
if (availExitCodeRegexProp != null && availExitCodeRegexProp.getStringValue() != null) {
availExitCodeRegex = availExitCodeRegexProp.getStringValue();
}
if (availOutputRegexProp != null && availOutputRegexProp.getStringValue() != null) {
availOutputRegex = availOutputRegexProp.getStringValue();
}
} catch (Exception e) {
throw new InvalidPluginConfigurationException("Cannot get avail plugin config. Cause: " + e);
}
// first, make sure the executable actually exists
File executableFile = new File(executable);
if (!executableFile.exists()) {
if (log.isDebugEnabled()) {
logDebug("The executable [" + executable + "] does not exist - resource is considered DOWN");
}
return false;
}
// if we need to check the exit code or output, execute the CLI now
if (availExecuteCheck || availExitCodeRegex != null || availOutputRegex != null) {
ProcessExecutionResults results = executeExecutable(availArgs, 3000L, (availOutputRegex != null));
// if we get some error while trying to run the executable, immediately consider the resource down
if (results.getError() != null) {
if (log.isDebugEnabled()) {
logDebug("CLI execution encountered an error, resource is considered DOWN: "
+ ThrowableUtil.getAllMessages(results.getError()));
}
return false;
}
// if the exit code is used to determine availability, check it now
if (availExitCodeRegex != null) {
if (results.getExitCode() == null) {
if (log.isDebugEnabled()) {
logDebug("Cannot get exit code, resource is considered DOWN");
}
return false;
}
boolean exitcodeMatches = results.getExitCode().toString().matches(availExitCodeRegex);
if (!exitcodeMatches) {
if (log.isDebugEnabled()) {
logDebug("CLI exit code=[" + results.getExitCode() + "] != [" + availExitCodeRegex + "]. DOWN");
}
return false;
}
}
// if the output is used to determine availability, check it now
if (availOutputRegex != null) {
String output = results.getCapturedOutput();
if (output == null) {
output = "";
}
boolean outputMatches = output.matches(availOutputRegex);
if (!outputMatches) {
if (log.isDebugEnabled()) {
logDebug("CLI output [" + truncateString(output) + "] did not match regex [" + availOutputRegex
+ "], resource is considered DOWN");
}
return false;
}
}
}
// everything passes, resource is UP
return true;
}
/**
* Truncate a string so it is short, usually for display or logging purposes.
*
* @param output the output to trim
* @return the trimmed output
*/
private String truncateString(String output) {
String outputToLog = output;
if (outputToLog != null && outputToLog.length() > 100) {
outputToLog = outputToLog.substring(0, 100) + "...";
}
return outputToLog;
}
private static ProcessExecution getProcessExecutionInfo(Configuration pluginConfig)
throws InvalidPluginConfigurationException {
PropertySimple executableProp = pluginConfig.getSimple(PLUGINCONFIG_EXECUTABLE);
PropertySimple workingDirProp = pluginConfig.getSimple(PLUGINCONFIG_WORKINGDIR);
PropertyList envvarsProp = pluginConfig.getList(PLUGINCONFIG_ENVVARS);
String executable = null;
String workingDir = null;
Map envvars = null;
if (executableProp == null) {
throw new InvalidPluginConfigurationException("Missing required plugin config: " + PLUGINCONFIG_EXECUTABLE);
} else {
executable = executableProp.getStringValue();
if (executable == null || executable.length() == 0) {
throw new InvalidPluginConfigurationException("Bad plugin config: " + PLUGINCONFIG_EXECUTABLE);
}
}
if (workingDirProp != null) {
workingDir = workingDirProp.getStringValue();
if (workingDir != null && workingDir.length() == 0) {
workingDir = null; // empty string is as good as unsetting it (i.e. making it null)
}
}
if (envvarsProp != null) {
try {
// we want envvars to be null if there are no envvars defined, so the agent env is passed
// but if there are 1 or more envvars in our config, then we define our envvars list
List listOfMaps = envvarsProp.getList();
if (listOfMaps != null && listOfMaps.size() > 0) {
envvars = new HashMap();
for (Property envvarMap : listOfMaps) {
PropertySimple name = (PropertySimple) ((PropertyMap) envvarMap).get(PLUGINCONFIG_ENVVAR_NAME);
PropertySimple value = (PropertySimple) ((PropertyMap) envvarMap)
.get(PLUGINCONFIG_ENVVAR_VALUE);
envvars.put(name.getStringValue(), value.getStringValue());
}
}
} catch (Exception e) {
throw new InvalidPluginConfigurationException("Bad plugin config: " + PLUGINCONFIG_ENVVARS
+ ". Cause: " + e);
}
}
ProcessExecution processExecution = new ProcessExecution(executable);
processExecution.setEnvironmentVariables(envvars);
processExecution.setWorkingDirectory(workingDir);
return processExecution;
}
private boolean isValidRegularExpression(String regex) {
try {
Pattern.compile(regex);
return true;
} catch (Exception e) {
return false;
}
}
private void logDebug(String msg) {
log.debug("[" + this.resourceContext.getResourceKey() + "]: " + msg);
}
}