/*
* RHQ Management Platform
* Copyright (C) 2005-2013 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.jboss.on.plugins.tomcat;
import java.io.File;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.mc4j.ems.connection.ConnectionFactory;
import org.mc4j.ems.connection.EmsConnectException;
import org.mc4j.ems.connection.EmsConnection;
import org.mc4j.ems.connection.bean.EmsBean;
import org.mc4j.ems.connection.bean.attribute.EmsAttribute;
import org.mc4j.ems.connection.settings.ConnectionSettings;
import org.mc4j.ems.connection.support.ConnectionProvider;
import org.mc4j.ems.connection.support.metadata.ConnectionTypeDescriptor;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.PropertyList;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.measurement.AvailabilityType;
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.util.file.FileUtil;
import org.rhq.plugins.jmx.JMXComponent;
import org.rhq.plugins.jmx.JMXDiscoveryComponent;
/**
* Management for an Apache or JBoss EWS Tomcat server
*
* @author Jay Shaughnessy
*/
public class TomcatServerComponent<T extends ResourceComponent<?>> implements JMXComponent<T>, MeasurementFacet,
OperationFacet {
public enum SupportedOperations {
/**
* Restarts a Tomcat instance by calling a configurable restart script.
*/
RESTART,
/**
* Shuts down a Tomcat instance via a shutdown script, depending on plug-in configuration
*/
SHUTDOWN,
/**
* Starts a Tomcat instance by calling a configurable start script.
*/
START,
/**
* Physically writes out the latest state to server.xml.
*/
STORECONFIG
}
public enum ControlMethod {
/** Control operations should be performed via System V init script. */
RPM,
/** Control operations should be performed via the scripts set in the plugin configuration. */
SCRIPT
}
/**
* Plugin configuration properties.
*/
public static final String PLUGIN_CONFIG_CONTROL_METHOD = "controlMethod";
// has legacy property name
public static final String PLUGIN_CONFIG_CATALINA_HOME_PATH = "installationPath";
public static final String PLUGIN_CONFIG_CATALINA_BASE_PATH = "catalinaBase";
public static final String PLUGIN_CONFIG_SCRIPT_PREFIX = "scriptPrefix";
public static final String PLUGIN_CONFIG_SHUTDOWN_SCRIPT = "shutdownScript";
public static final String PLUGIN_CONFIG_START_SCRIPT = "startScript";
public static final String START_WAIT_MAX_PROP = "startWaitMax";
public static final String STOP_WAIT_MAX_PROP = "stopWaitMax";
public static final String PLUGIN_CONFIG_SERVICE_NAME="serviceName";
private Log log = LogFactory.getLog(this.getClass());
private EmsConnection connection;
/**
* Controls the dampening of connection error stack traces in an attempt to control spam to the log
* file. Each time a connection error is encountered, this will be incremented. When the connection
* is finally established, this will be reset to zero.
*/
private int consecutiveConnectionErrors;
/**
* Delegate instance for handling all calls to invoke operations on this component.
*/
private TomcatServerOperationsDelegate operationsDelegate;
private ResourceContext<T> resourceContext;
// JMXComponent Implementation --------------------------------------------
public EmsConnection getEmsConnection() {
EmsConnection emsConnection = null;
try {
emsConnection = loadConnection();
} catch (Exception e) {
if (log.isTraceEnabled()) {
log.debug("Component attempting to access a connection that could not be loaded:" + e.getMessage());
}
}
return emsConnection;
}
/**
* This is the preferred way to use a connection from within this class; methods should not access the connection
* property directly as it may not have been instantiated if the connection could not be made.
*
* <p>If the connection has already been established, return the object reference to it. If not, attempt to make
* a live connection to the JMX server.</p>
*
* <p>If the connection could not be made in the {@link #start(org.rhq.core.pluginapi.inventory.ResourceContext)}
* method, this method will effectively try to load the connection on each attempt to use it. As such, multiple
* threads may attempt to access the connection through this means at a time. Therefore, the method has been
* made synchronized on instances of the class.</p>
*
* <p>If any errors are encountered, this method will log the error, taking into account logic to prevent spamming
* the log file. Calling methods should take care to not redundantly log the exception thrown by this method.</p>
*
* @return live connection to the JMX server; this will not be <code>null</code>
*
* @throws Exception if there are any issues at all connecting to the server
*/
private synchronized EmsConnection loadConnection() throws Exception {
if (this.connection != null && !this.connection.getConnectionProvider().isConnected()) {
try {
this.connection.close();
} catch (Exception ignore) {
} finally {
this.connection = null;
}
}
if (this.connection == null) {
try {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
ConnectionSettings connectionSettings = new ConnectionSettings();
String connectionTypeDescriptorClass = pluginConfig.getSimple(JMXDiscoveryComponent.CONNECTION_TYPE)
.getStringValue();
PropertySimple serverUrl = pluginConfig
.getSimple(JMXDiscoveryComponent.CONNECTOR_ADDRESS_CONFIG_PROPERTY);
connectionSettings.initializeConnectionType((ConnectionTypeDescriptor) Class.forName(
connectionTypeDescriptorClass).newInstance());
// if not provided use the default serverUrl
if (null != serverUrl) {
connectionSettings.setServerUrl(serverUrl.getStringValue());
}
connectionSettings.setPrincipal(pluginConfig.getSimpleValue(PRINCIPAL_CONFIG_PROP, null));
connectionSettings.setCredentials(pluginConfig.getSimpleValue(CREDENTIALS_CONFIG_PROP, null));
if (connectionSettings.getAdvancedProperties() == null) {
connectionSettings.setAdvancedProperties(new Properties());
}
ConnectionFactory connectionFactory = new ConnectionFactory();
// EMS can connect to a Tomcat Server without using version-compatible jars from a local TC install. So,
// If we are connecting to a remote TC Server and the install path is not valid on the local host, don't
// configure to use the local jars. But, to be safe, if for some overlooked or future reason we require
// the jars then use them if they are available. Note, for a remote TC Server that would mean you'd have
// to have a version compatible local install and set the install path to the local path, even though
// the server url was remote.
String catalinaHome = pluginConfig.getSimpleValue(PLUGIN_CONFIG_CATALINA_HOME_PATH, null);
File libDir = getLibDir(catalinaHome);
if (libDir != null) {
connectionSettings.setLibraryURI(libDir.getAbsolutePath());
connectionFactory.discoverServerClasses(connectionSettings);
// Tell EMS to make copies of jar files so that the ems classloader doesn't lock
// application files (making us unable to update them) Bug: JBNADM-670
// TODO (ips): Turn this off in the embedded case.
connectionSettings.getControlProperties().setProperty(ConnectionFactory.COPY_JARS_TO_TEMP,
String.valueOf(Boolean.TRUE));
// But tell it to put them in a place that we clean up when shutting down the agent (make sure tmp dir exists)
File tempDir = resourceContext.getTemporaryDirectory();
if (!tempDir.exists()) {
tempDir.mkdirs();
}
connectionSettings.getControlProperties().setProperty(ConnectionFactory.JAR_TEMP_DIR,
tempDir.getAbsolutePath());
if (log.isDebugEnabled()) {
log.debug("Loading connection [" + connectionSettings.getServerUrl() + "] with install path ["
+ connectionSettings.getLibraryURI() + "] and temp directory [" + tempDir.getAbsolutePath()
+ "]");
}
} else {
if (log.isDebugEnabled()) {
log.debug("Loading connection [" + connectionSettings.getServerUrl()
+ "] ignoring remote install path [" + catalinaHome + "]");
}
}
ConnectionProvider connectionProvider = connectionFactory.getConnectionProvider(connectionSettings);
this.connection = connectionProvider.connect();
this.connection.loadSynchronous(false); // this loads all the MBeans
this.consecutiveConnectionErrors = 0;
if (log.isDebugEnabled())
log.debug("Successfully made connection to the Tomcat Server for resource ["
+ this.resourceContext.getResourceKey() + "]");
} catch (Exception e) {
// The connection will be established even in the case that the principal cannot be authenticated,
// but the connection will not work. That failure seems to come from the call to loadSynchronous after
// the connection is established. If we get to this point that an exception was thrown, close any
// connection that was made and null it out so we can try to establish it again.
if (this.connection != null) {
if (log.isDebugEnabled()) {
log.debug("Connection created but an exception was thrown. Closing the connection.", e);
}
try {
this.connection.close();
} catch (Exception e2) {
log.error("Error closing Tomcat EMS connection: " + e2);
}
this.connection = null;
}
// Since the connection is attempted each time it's used, failure to connect could result in log
// file spamming. Log a warning only one time outside of debug mode, and throttle even in debug
// mode (once for every 10 connect errors).
if (0 == consecutiveConnectionErrors) {
log.warn("Could not connect to the Tomcat instance for resource ["
+ resourceContext.getResourceKey() + "] (enable debug logging for more info): "
+ e.getMessage());
}
if (log.isDebugEnabled() && (consecutiveConnectionErrors % 10 == 0)) {
log.debug(
"Could not establish connection to the Tomcat instance [" + (consecutiveConnectionErrors + 1)
+ "] times for resource [" + resourceContext.getResourceKey() + "]", e);
}
++consecutiveConnectionErrors;
throw e;
}
}
return connection;
}
private File getLibDir(String catalinaHome) {
if (catalinaHome != null) {
// Tomcat 6 and Tomcat 7 have Catalina JARS in CATALINA_HOME/lib
File libDir = new File(catalinaHome, "lib");
if (libDir.isDirectory()) {
return libDir;
}
// Tomcat 5.5 has Catalina JARS in CATALINA_HOME/server/lib
libDir = new File(catalinaHome, "server" + File.separator + "lib");
if (libDir.isDirectory()) {
return libDir;
}
}
return null;
}
public Configuration getPluginConfiguration() {
return resourceContext.getPluginConfiguration();
}
// Here we do any validation that couldn't be achieved via the metadata-based constraints.
private void validatePluginConfiguration() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String principal = pluginConfig.getSimpleValue(TomcatServerComponent.PRINCIPAL_CONFIG_PROP, null);
String credentials = pluginConfig.getSimpleValue(TomcatServerComponent.CREDENTIALS_CONFIG_PROP, null);
if ((principal != null) && (credentials == null)) {
throw new InvalidPluginConfigurationException("If the '" + TomcatServerComponent.PRINCIPAL_CONFIG_PROP
+ "' connection property is set, the '" + TomcatServerComponent.CREDENTIALS_CONFIG_PROP
+ "' connection property must also be set.");
}
if ((credentials != null) && (principal == null)) {
throw new InvalidPluginConfigurationException("If the '" + TomcatServerComponent.CREDENTIALS_CONFIG_PROP
+ "' connection property is set, the '" + TomcatServerComponent.PRINCIPAL_CONFIG_PROP
+ "' connection property must also be set.");
}
}
@Override
public void start(ResourceContext<T> context) throws InvalidPluginConfigurationException, Exception {
this.resourceContext = context;
this.operationsDelegate = new TomcatServerOperationsDelegate(this, resourceContext.getSystemInformation());
validatePluginConfiguration();
// Attempt to load the connection now. If we cannot, do not consider the start operation as failed. The only
// exception to this rule is if the connection cannot be made due to a JMX security exception. In this case,
// we treat it as an invalid plugin configuration and throw the appropriate exception (see the javadoc for
// ResourceComponent)
try {
loadConnection();
} catch (Exception e) {
// Explicit checking for security exception (i.e. invalid credentials for connecting to JMX)
if (e instanceof EmsConnectException) {
Throwable cause = e.getCause();
if (cause instanceof SecurityException) {
throw new InvalidPluginConfigurationException(
"Invalid JMX credentials specified for connecting to this server.", e);
}
}
}
// TODO: If we add event checking by default
//startLogFileEventPollers();
}
public void stop() {
// TODO: If we add event checking by default
// stopLogFileEventPollers();
closeConnection();
}
/**
* If necessary attempt to close the EMS connection, then set this.connection null. Synchronized ensure we play well
* with loadConnection.
*/
private synchronized void closeConnection() {
if (this.connection != null) {
try {
this.connection.close();
} catch (Exception e) {
log.error("Error closing Tomcat EMS connection: " + e);
}
this.connection = null;
}
}
public AvailabilityType getAvailability() {
AvailabilityType avail;
try {
EmsConnection connection = loadConnection();
EmsBean bean = connection.getBean("Catalina:type=Server");
// this is necessary to prove that that not only the connection exists but is servicing requests.
bean.getAttribute("serverInfo").refresh();
avail = AvailabilityType.UP;
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug("An exception occurred during availability check for Tomcat Server Resource with key ["
+ this.getResourceContext().getResourceKey() + "] and plugin config ["
+ this.getPluginConfiguration().getAllProperties() + "].", e);
}
// If the connection is not servicing requests, then close it. this seems necessary for the
// Tomcat connection, as, when Tomcat does come up again, it seems a new EMS connection is required,
// otherwise EMS is not able to pick up the new process.
closeConnection();
avail = AvailabilityType.DOWN;
}
return avail;
}
ResourceContext<T> getResourceContext() {
return this.resourceContext;
}
public File getStartScriptPath() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String script = pluginConfig.getSimpleValue(TomcatServerComponent.PLUGIN_CONFIG_START_SCRIPT, "");
File scriptFile = resolvePathRelativeToHomeDir(script);
return scriptFile;
}
public File getCatalinaHome() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
return new File(pluginConfig.getSimpleValue(TomcatServerComponent.PLUGIN_CONFIG_CATALINA_HOME_PATH, ""));
}
public File getCatalinaBase() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String base = pluginConfig.getSimpleValue(TomcatServerComponent.PLUGIN_CONFIG_CATALINA_BASE_PATH, null);
return (null != base) ? new File(base) : getCatalinaHome();
}
public File getShutdownScriptPath() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String script = pluginConfig.getSimpleValue(TomcatServerComponent.PLUGIN_CONFIG_SHUTDOWN_SCRIPT, "");
File scriptFile = resolvePathRelativeToHomeDir(script);
return scriptFile;
}
public String getServiceName() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String servicename = pluginConfig.getSimpleValue(TomcatServerComponent.PLUGIN_CONFIG_SERVICE_NAME, null);
return servicename;
}
private File resolvePathRelativeToHomeDir(@NotNull
String path) {
return resolvePathRelativeToHomeDir(this.resourceContext.getPluginConfiguration(), path);
}
static File resolvePathRelativeToHomeDir(Configuration pluginConfig, String path) {
File configDir = new File(path);
if (!FileUtil.isAbsolutePath(path)) {
String jbossHomeDir = getRequiredPropertyValue(pluginConfig,
TomcatServerComponent.PLUGIN_CONFIG_CATALINA_HOME_PATH);
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();
}
private static String getRequiredPropertyValue(Configuration config, 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;
}
/** Persist local changes to the server.xml */
void storeConfig() throws Exception {
invokeOperation(SupportedOperations.STORECONFIG.name(), new Configuration());
}
public OperationResult invokeOperation(String name, Configuration parameters) throws InterruptedException,
Exception {
SupportedOperations operation = Enum.valueOf(SupportedOperations.class, name.toUpperCase());
addScriptsEnvironment(parameters);
return operationsDelegate.invoke(operation, parameters);
}
public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> metrics) throws Exception {
for (MeasurementScheduleRequest schedule : metrics) {
String name = schedule.getName();
int delimIndex = name.lastIndexOf(':');
String beanName = name.substring(0, delimIndex);
String attributeName = name.substring(delimIndex + 1);
try {
// Bean is cached by EMS, so no problem with getting the bean from the connection on each call
EmsConnection emsConnection = loadConnection();
EmsBean bean = emsConnection.getBean(beanName);
EmsAttribute attribute = bean.getAttribute(attributeName);
Object valueObject = attribute.refresh();
if (valueObject instanceof Number) {
Number value = (Number) valueObject;
report.addData(new MeasurementDataNumeric(schedule, value.doubleValue()));
} else {
report.addData(new MeasurementDataTrait(schedule, valueObject.toString()));
}
} catch (Exception e) {
log.error("Failed to obtain measurement [" + name + "]", e);
}
}
}
private void addScriptsEnvironment(Configuration operationParameters) {
Configuration pluginConfiguration = resourceContext.getPluginConfiguration();
PropertyList startScriptEnv = pluginConfiguration
.getList(TomcatServerOperationsDelegate.START_SCRIPT_ENVIRONMENT_PROPERTY);
PropertyList shutdownScriptEnv = pluginConfiguration
.getList(TomcatServerOperationsDelegate.SHUTDOWN_SCRIPT_ENVIRONMENT_PROPERTY);
if (startScriptEnv != null) {
operationParameters.put(startScriptEnv);
}
if (shutdownScriptEnv != null) {
operationParameters.put(shutdownScriptEnv);
}
}
}