/*
* RHQ Management Platform
* Copyright (C) 2005-2014 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.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
package org.rhq.plugins.jbossas5;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.rhq.core.util.StringUtil.isBlank;
import static org.rhq.plugins.jbossas5.ApplicationServerPluginConfigurationProperties.START_WAIT_MAX_PROP;
import static org.rhq.plugins.jbossas5.ApplicationServerPluginConfigurationProperties.STOP_WAIT_MAX_PROP;
import static org.rhq.plugins.jbossas5.ApplicationServerShutdownMethod.JMX;
import java.io.File;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mc4j.ems.connection.EmsConnection;
import org.mc4j.ems.connection.bean.EmsBean;
import org.mc4j.ems.connection.bean.operation.EmsOperation;
import org.mc4j.ems.connection.bean.parameter.EmsParameter;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.pluginapi.inventory.InvalidPluginConfigurationException;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.core.pluginapi.util.ProcessExecutionUtility;
import org.rhq.core.pluginapi.util.StartScriptConfiguration;
import org.rhq.core.system.ProcessExecution;
import org.rhq.core.system.ProcessExecutionResults;
import org.rhq.core.system.SystemInfo;
import org.rhq.core.util.file.FileUtil;
/**
* Handles performing operations (Start, Shut Down, and Restart) on a JBoss AS 5.x instance.
*
* @author Ian Springer
* @author Jason Dobies
* @author Jay Shaughnessy
*/
public class ApplicationServerOperationsDelegate {
private static class ExecutionFailedException extends Exception {
private static final long serialVersionUID = 1L;
@SuppressWarnings("unused")
public ExecutionFailedException() {
}
public ExecutionFailedException(String message, Throwable cause) {
super(message, cause);
}
public ExecutionFailedException(String message) {
super(message);
}
@SuppressWarnings("unused")
public ExecutionFailedException(Throwable cause) {
super(cause);
}
}
/**
* default max amount of time to wait for server to show as unavailable after
* executing stop - in milliseconds
*/
private static final long DEFAULT_STOP_WAIT_MAX = 1000L * 150; // 2.5 minutes
/**
* amount of time to wait between availability checks when performing a stop
* - in milliseconds
*/
private static final long STOP_WAIT_INTERVAL = 1000L * 5; // 5 seconds
/**
* amount of time to wait for stop to complete after the loop that checks
* for DOWN availability terminates - in milliseconds
*/
private static final long STOP_WAIT_FINAL = 1000L * 30; // 30 seconds
/** default max amount of time to wait for start to complete - in milliseconds */
private static final long DEFAULT_START_WAIT_MAX = 1000L * 300; // 5 minutes
/**
* amount of time to wait between availability checks when performing a
* start - in milliseconds
*/
private static final long START_WAIT_INTERVAL = 1000L * 5; // 5 seconds
private final Log log = LogFactory.getLog(ApplicationServerOperationsDelegate.class);
private static final String SEPARATOR = "\n-----------------------\n";
static final String DEFAULT_START_SCRIPT = "bin" + File.separator + "run."
+ ((File.separatorChar == '/') ? "sh" : "bat");
static final String DEFAULT_SHUTDOWN_SCRIPT = "bin" + File.separator + "shutdown."
+ ((File.separatorChar == '/') ? "sh" : "bat");
/**
* Server component against which the operations are being performed.
*/
private ApplicationServerComponent serverComponent;
private File configPath;
// Constructors --------------------------------------------
public ApplicationServerOperationsDelegate(ApplicationServerComponent serverComponent) {
this.serverComponent = serverComponent;
}
// Public --------------------------------------------
/**
* Performs the specified operation. The result of the operation will be
* indicated in the return. If there is an error, an
* <code>RuntimeException</code> will be thrown.
*
* @param operation
* the operation to perform
* @param parameters
* parameters to the operation call
*
* @return if successful, the result object will contain a success message
*
* @throws RuntimeException
* if any errors occur while trying to perform the operation
*/
public OperationResult invoke(ApplicationServerSupportedOperations operation, Configuration parameters)
throws InterruptedException {
OperationResult result = null;
switch (operation) {
case START: {
result = start();
break;
}
case SHUTDOWN: {
result = shutDown();
break;
}
case RESTART: {
result = restart();
break;
}
}
return result;
}
// Private --------------------------------------------
/**
* Starts the underlying AS server. Uses the StartScript connection properties, if set, or defaults to
* using the minimal required settings, which may not start or restart the app server in the same way
* it was initially started.
*
* @return success message if no errors are encountered
* @throws InterruptedException
* if the plugin container stops this operation while its
* executing
*/
private OperationResult start() throws InterruptedException {
AvailabilityType avail = this.serverComponent.getAvailability();
if (avail == AvailabilityType.UP) {
OperationResult result = new OperationResult();
result.setErrorMessage("The server is already started.");
return result;
}
Configuration pluginConfig = serverComponent.getResourceContext().getPluginConfiguration();
StartScriptConfiguration startScriptConfig = new StartScriptConfiguration(pluginConfig);
File startScriptFile = getStartScriptPath(startScriptConfig);
validateScriptFile(startScriptFile, ApplicationServerPluginConfigurationProperties.START_SCRIPT_CONFIG_PROP);
// The optional command prefix (e.g. sudo or nohup).
String prefix = pluginConfig
.getSimpleValue(ApplicationServerPluginConfigurationProperties.SCRIPT_PREFIX_CONFIG_PROP, null);
if ((prefix != null) && prefix.replaceAll("\\s", "").equals("")) {
// all whitespace - normalize to null
prefix = null;
}
ProcessExecution processExecution = ProcessExecutionUtility.createProcessExecution(prefix, startScriptFile);
addProcessExecutionArguments(processExecution, startScriptFile, startScriptConfig, false);
// processExecution is initialized to the current process' env. This isn't really right, it's the
// rhq agent env. Override this if the startScriptEnv property has been set.
Map<String, String> startScriptEnv = startScriptConfig.getStartScriptEnv();
if (!startScriptEnv.isEmpty()) {
for (String envVarName : startScriptEnv.keySet()) {
String envVarValue = startScriptEnv.get(envVarName);
// TODO: If we migrate the AS7 util to a general util then hook it up
// envVarValue = replacePropertyPatterns(envVarValue);
startScriptEnv.put(envVarName, envVarValue);
}
processExecution.setEnvironmentVariables(startScriptEnv);
} else {
// set JAVA_HOME to the value of the deprecated 'javaHome' plugin config prop.
setJavaHomeEnvironmentVariable(processExecution);
}
// perform any init common for start and shutdown scripts
initProcessExecution(processExecution, startScriptFile);
long start = System.currentTimeMillis();
if (log.isDebugEnabled()) {
log.debug("About to execute the following process: [" + processExecution + "]");
}
SystemInfo systemInfo = serverComponent.getResourceContext().getSystemInformation();
ProcessExecutionResults results = systemInfo.executeProcess(processExecution);
logExecutionResults(results);
if (results.getError() == null) {
avail = waitForServerToStart(start);
} else {
log.error(
"Error from process execution while starting the AS instance. Exit code [" + results.getExitCode()
+ "]", results.getError());
avail = this.serverComponent.getAvailability();
}
// If, after the loop, the Server is still down, consider the start to be a failure.
OperationResult result;
if (avail == AvailabilityType.DOWN) {
result = new OperationResult();
result.setErrorMessage("The server failed to start: " + results.getCapturedOutput());
} else {
result = new OperationResult("The server has been started.");
}
return result;
}
private void addProcessExecutionArguments(ProcessExecution processExecution, File startScriptFile,
StartScriptConfiguration startScriptConfig, boolean asSingleArg) {
List<String> startScriptArgs = startScriptConfig.getStartScriptArgs();
// If the scriptArgs property is unset fall back to using just the other props we have
if (startScriptArgs.isEmpty()) {
startScriptArgs.add("-c");
startScriptArgs.add(getConfigurationSet());
String bindAddress = startScriptConfig.getPluginConfig().getSimpleValue(
ApplicationServerPluginConfigurationProperties.BIND_ADDRESS, null);
if (bindAddress != null) {
startScriptArgs.add("-b");
startScriptArgs.add(bindAddress);
}
}
if (asSingleArg) {
// typically, the sudo case
StringBuilder sb = new StringBuilder(startScriptFile.getAbsolutePath());
for (String startScriptArg : startScriptArgs) {
sb.append(" ");
// TODO: If we migrate the AS7 util to a general util then hook it up
//startScriptArg = replacePropertyPatterns(startScriptArg);
sb.append(startScriptArg);
}
processExecution.getArguments().add(sb.toString());
} else {
for (String startScriptArg : startScriptArgs) {
// TODO: If we migrate the AS7 util to a general util then hook it up
//startScriptArg = replacePropertyPatterns(startScriptArg);
processExecution.getArguments().add(startScriptArg);
}
}
}
private String getConfigurationSet() {
Configuration pluginConfig = serverComponent.getResourceContext().getPluginConfiguration();
configPath = resolvePathRelativeToHomeDir(getRequiredPropertyValue(pluginConfig,
ApplicationServerPluginConfigurationProperties.SERVER_HOME_DIR));
if (!configPath.exists()) {
throw new InvalidPluginConfigurationException("Configuration path '" + configPath + "' does not exist.");
}
return pluginConfig.getSimpleValue(ApplicationServerPluginConfigurationProperties.SERVER_NAME,
configPath.getName());
}
private void initProcessExecution(ProcessExecution processExecution, File scriptFile) {
// NOTE: For both run.bat and shutdown.bat, the current working dir must
// be set to the script's parent dir
// (e.g. ${JBOSS_HOME}/bin) for the script to work.
processExecution.setWorkingDirectory(scriptFile.getParent());
processExecution.setCaptureOutput(true);
processExecution.setWaitForCompletion(1000L);
processExecution.setKillOnTimeout(false);
}
private void setJavaHomeEnvironmentVariable(ProcessExecution processExecution) {
File javaHomeDir = getJavaHomePath();
if (javaHomeDir == null) {
throw new RuntimeException("JAVA_HOME environment variable must be specified via the 'javaHome' connection "
+ "property in order to shut down the application server via script.");
}
validateJavaHomePathProperty();
processExecution.getEnvironmentVariables().put("JAVA_HOME", javaHomeDir.getPath());
}
/**
* Shuts down the server by dispatching to shutdown via script or JMX. Waits
* until the server is down.
*
* @return The result of the shutdown operation - is successful
*/
private OperationResult shutDown() {
Configuration pluginConfig = serverComponent.getResourceContext().getPluginConfiguration();
ApplicationServerShutdownMethod shutdownMethod = Enum.valueOf(ApplicationServerShutdownMethod.class,
pluginConfig.getSimple(ApplicationServerPluginConfigurationProperties.SHUTDOWN_METHOD_CONFIG_PROP)
.getStringValue());
String errorMessage = null;
String resultMessage = null;
try {
resultMessage = JMX.equals(shutdownMethod) ? shutdownViaJmx() : shutdownViaScript();
} catch (ExecutionFailedException e) {
errorMessage = e.getMessage();
}
AvailabilityType avail = waitForServerToShutdown();
OperationResult result;
if (avail == AvailabilityType.UP) {
result = new OperationResult();
result.setErrorMessage("The server failed to shut down.");
} else {
result = new OperationResult();
result.setSimpleResult(resultMessage);
result.setErrorMessage(errorMessage);
}
return result;
}
/**
* Shuts down the AS server using a shutdown script.
*
* @return success message if no errors are encountered
*/
private String shutdownViaScript() throws ExecutionFailedException {
File shutdownScriptFile = getShutdownScriptPath();
validateScriptFile(shutdownScriptFile,
ApplicationServerPluginConfigurationProperties.SHUTDOWN_SCRIPT_CONFIG_PROP);
Configuration pluginConfig = serverComponent.getResourceContext().getPluginConfiguration();
String prefix = pluginConfig
.getSimple(ApplicationServerPluginConfigurationProperties.SCRIPT_PREFIX_CONFIG_PROP).getStringValue();
ProcessExecution processExecution = ProcessExecutionUtility.createProcessExecution(prefix, shutdownScriptFile);
initProcessExecution(processExecution, shutdownScriptFile);
setJavaHomeEnvironmentVariable(processExecution);
String server = pluginConfig.getSimple(ApplicationServerPluginConfigurationProperties.NAMING_URL)
.getStringValue();
if (server != null) {
processExecution.getArguments().add("--server=" + server);
}
String user = pluginConfig.getSimple(ApplicationServerComponent.PRINCIPAL_CONFIG_PROP).getStringValue();
if (user != null) {
processExecution.getArguments().add("--user=" + user);
}
String password = pluginConfig.getSimple(ApplicationServerComponent.CREDENTIALS_CONFIG_PROP).getStringValue();
if (password != null) {
processExecution.getArguments().add("--password=" + password);
}
processExecution.getArguments().add("--shutdown");
/*
* This tells shutdown.bat not to call the Windows PAUSE command, which
* would cause the script to hang indefinitely waiting for input.
* noinspection ConstantConditions
*/
processExecution.getEnvironmentVariables().put("NOPAUSE", "1");
if (log.isDebugEnabled()) {
log.debug("About to execute the following process: [" + processExecution + "]");
}
SystemInfo systemInfo = serverComponent.getResourceContext().getSystemInformation();
ProcessExecutionResults results = systemInfo.executeProcess(processExecution);
logExecutionResults(results);
if (results.getError() != null || (results.getExitCode() != null && results.getExitCode() != 0)) {
throw new ExecutionFailedException(
"Error executing shutdown script while stopping AS instance. Shutdown script returned exit code ["
+ results.getExitCode() + "]"
+ (results.getError() != null ? ": " + results.getError().getMessage() : ""), results.getError());
}
return "The server has been shut down.";
}
private void logExecutionResults(ProcessExecutionResults results) {
// Always log the output at info level. On Unix we could switch
// depending on a exitCode being !=0, but ...
log.info("Exit code from process execution: " + results.getExitCode());
log.info("Output from process execution: " + SEPARATOR + results.getCapturedOutput() + SEPARATOR);
}
/**
* Shuts down the AS server via a JMX call.
*
* @return success message if no errors are encountered
*/
private String shutdownViaJmx() throws ExecutionFailedException {
Configuration pluginConfig = serverComponent.getResourceContext().getPluginConfiguration();
String mbeanName = pluginConfig.getSimple(
ApplicationServerPluginConfigurationProperties.SHUTDOWN_MBEAN_CONFIG_PROP).getStringValue();
String operationName = pluginConfig.getSimple(
ApplicationServerPluginConfigurationProperties.SHUTDOWN_MBEAN_OPERATION_CONFIG_PROP).getStringValue();
EmsConnection connection = this.serverComponent.getEmsConnection();
if (connection == null) {
throw new ExecutionFailedException("Can not connect to the server");
}
EmsBean bean = connection.getBean(mbeanName);
EmsOperation operation = bean.getOperation(operationName);
/*
* Now see if we got the 'real' method (the one with no param) or the
* overloaded one. This is a workaround for a bug in EMS that prevents
* finding operations with same name and different signature.
* http://sourceforge
* .net/tracker/index.php?func=detail&aid=2007692&group_id
* =60228&atid=493495
*
* In addition, as we offer the user to specify any MBean and any
* method, we'd need a clever way for the user to specify parameters
* anyway.
*/
try {
List<EmsParameter> params = operation.getParameters();
int count = params.size();
if (count == 0)
operation.invoke(new Object[0]);
else { // overloaded operation
operation.invoke(new Object[] { 0 }); // return code of 0
}
} catch (RuntimeException e) {
throw new ExecutionFailedException("Shutting down the server using JMX failed: " + e.getMessage(), e);
}
return "The server has been shut down.";
}
private void validateScriptFile(File scriptFile, String scriptPropertyName) {
if (!scriptFile.exists()) {
throw new RuntimeException("Script (" + scriptFile + ") specified via '" + scriptPropertyName
+ "' connection property does not exist.");
}
if (scriptFile.isDirectory()) {
throw new RuntimeException("Script (" + scriptFile + ") specified via '" + scriptPropertyName
+ "' connection property is a directory, not a file.");
}
}
/**
* Restart the server by first trying a shutdown and then a start. This is
* fail fast.
*
* @return A success message on success
*/
private OperationResult restart() {
try {
OperationResult result = shutDown();
if (result.getErrorMessage() != null) {
return result;
}
} catch (Exception e) {
throw new RuntimeException("Shutdown may have failed: " + e);
}
try {
OperationResult result = start();
if (result.getErrorMessage() != null) {
return result;
}
} catch (Exception e) {
throw new RuntimeException("Start following shutdown may have failed: " + e);
}
return new OperationResult("Server has been restarted.");
}
private AvailabilityType waitForServerToStart(long start) throws InterruptedException {
AvailabilityType avail;
//detect whether startWaitMax property has been set.
Configuration pluginConfig = serverComponent.getResourceContext().getPluginConfiguration();
long startWaitMax = getMaxWait(pluginConfig.getSimple(START_WAIT_MAX_PROP), DEFAULT_START_WAIT_MAX);
while (((avail = this.serverComponent.getAvailability()) == AvailabilityType.DOWN)
&& (System.currentTimeMillis() < (start + startWaitMax))) {
try {
Thread.sleep(START_WAIT_INTERVAL);
} catch (InterruptedException e) {
// ignore
}
}
return avail;
}
private AvailabilityType waitForServerToShutdown() {
long start = System.currentTimeMillis();
AvailabilityType avail;
//detect whether stopWaitMax property has been set.
Configuration pluginConfig = serverComponent.getResourceContext().getPluginConfiguration();
long stopWaitMax = getMaxWait(pluginConfig.getSimple(STOP_WAIT_MAX_PROP), DEFAULT_STOP_WAIT_MAX);
while (((avail = this.serverComponent.getAvailability()) == AvailabilityType.UP)
&& (System.currentTimeMillis() < (start + stopWaitMax))) {
try {
Thread.sleep(STOP_WAIT_INTERVAL);
} catch (InterruptedException e) {
// ignore
}
}
// After the server becomes unavailable, wait a little longer to hopefully
// ensure shutdown is complete.
try {
Thread.sleep(STOP_WAIT_FINAL);
} catch (InterruptedException e) {
// ignore
}
return avail;
}
private long getMaxWait(PropertySimple propertySimple, long defaultValueInMillis) {
if (propertySimple == null || isBlank(propertySimple.getStringValue())) {
return defaultValueInMillis;
}
try {
long valueInMinutes = Long.parseLong(propertySimple.getStringValue());
if (valueInMinutes > 0) {
return MINUTES.toMillis(valueInMinutes);
} else {
return defaultValueInMillis;
}
} catch (NumberFormatException e) {
return defaultValueInMillis;
}
}
/**
* Return the absolute path of this JBoss server's start script (e.g.
* "C:\opt\jboss-5.1.0.GA\bin\run.sh").
*
* @return the absolute path of this JBoss server's start script (e.g.
* "C:\opt\jboss-5.1.0.GA\bin\run.sh")
*/
@NotNull
public File getStartScriptPath(StartScriptConfiguration startScriptConfig) {
File startScriptFile = startScriptConfig.getStartScript();
if (null == startScriptFile) {
startScriptFile = resolvePathRelativeToHomeDir(DEFAULT_START_SCRIPT);
}
return startScriptFile;
}
@NotNull
private File resolvePathRelativeToHomeDir(@NotNull String path) {
return resolvePathRelativeToHomeDir(serverComponent.getResourceContext().getPluginConfiguration(), path);
}
@NotNull
private File resolvePathRelativeToHomeDir(Configuration pluginConfig, @NotNull String path) {
File configDir = new File(path);
if (!FileUtil.isAbsolutePath(path)) {
String jbossHomeDir = getRequiredPropertyValue(pluginConfig,
ApplicationServerPluginConfigurationProperties.HOME_DIR);
configDir = new File(jbossHomeDir, path);
}
// BZ 903402 - get the real absolute path - under most conditions, it's the same thing, but if on windows
// the drive letter might not have been specified - this makes sure the drive letter is specified.
return configDir.getAbsoluteFile();
}
@NotNull
private String getRequiredPropertyValue(@NotNull Configuration config, @NotNull String propName) {
String propValue = config.getSimpleValue(propName, null);
if (propValue == null) {
// Something's not right - neither autodiscovery, nor the config
// edit GUI, should ever allow this.
throw new IllegalStateException("Required property '" + propName + "' is not set.");
}
return propValue;
}
/**
* Return the absolute path of this JBoss server's shutdown script (e.g.
* "C:\opt\jboss-5.1.0.GA\bin\shutdown.sh").
*
* @return the absolute path of this JBoss server's shutdown script (e.g.
* "C:\opt\jboss-5.1.0.GA\bin\shutdown.sh")
*/
@NotNull
public File getShutdownScriptPath() {
Configuration pluginConfig = serverComponent.getResourceContext().getPluginConfiguration();
String shutdownScript = pluginConfig.getSimpleValue(
ApplicationServerPluginConfigurationProperties.SHUTDOWN_SCRIPT_CONFIG_PROP, DEFAULT_SHUTDOWN_SCRIPT);
File shutdownScriptFile = resolvePathRelativeToHomeDir(shutdownScript);
return shutdownScriptFile;
}
/**
* Return the absolute path of this JBoss server's JAVA_HOME directory (e.g. "C:\opt\jdk1.5.0_14"), as defined by
* the 'javaHome' plugin config prop, or null if that prop is not set.
*
* @return the absolute path of this JBoss server's JAVA_HOME directory, as defined by
* the 'javaHome' plugin config prop, or null if that prop is not set
*/
@Nullable
public File getJavaHomePath() {
Configuration pluginConfig = serverComponent.getResourceContext().getPluginConfiguration();
String javaHomePath = pluginConfig.getSimpleValue(ApplicationServerPluginConfigurationProperties.JAVA_HOME,
null);
File javaHome = (javaHomePath != null) ? new File(javaHomePath) : null;
return javaHome;
}
void validateJavaHomePathProperty() {
Configuration pluginConfig = serverComponent.getResourceContext().getPluginConfiguration();
String javaHome = pluginConfig.getSimpleValue(ApplicationServerPluginConfigurationProperties.JAVA_HOME, null);
if (javaHome != null) {
File javaHomeDir = new File(javaHome);
if (!javaHomeDir.isAbsolute()) {
throw new InvalidPluginConfigurationException(
ApplicationServerPluginConfigurationProperties.JAVA_HOME
+ " connection property ('"
+ javaHomeDir
+ "') is not an absolute path. Note, on Windows, absolute paths must start with the drive letter (e.g. C:).");
}
if (!javaHomeDir.exists()) {
throw new InvalidPluginConfigurationException(ApplicationServerPluginConfigurationProperties.JAVA_HOME
+ " connection property ('" + javaHomeDir + "') does not exist.");
}
if (!javaHomeDir.isDirectory()) {
throw new InvalidPluginConfigurationException(ApplicationServerPluginConfigurationProperties.JAVA_HOME
+ " connection property ('" + javaHomeDir + "') is not a directory.");
}
}
}
}