/*
* Jopr 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, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* 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 and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser 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.jbossas;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import com.jboss.jbossnetwork.product.jbpm.handlers.ControlActionFacade;
import com.jboss.jbossnetwork.product.jbpm.handlers.InPluginControlActionFacade;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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.mc4j.ems.connection.support.metadata.InternalVMTypeDescriptor;
import org.jboss.on.common.jbossas.JBPMWorkflowManager;
import org.jboss.on.common.jbossas.JBossASPaths;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.content.PackageDetailsKey;
import org.rhq.core.domain.content.PackageType;
import org.rhq.core.domain.content.transfer.DeployPackageStep;
import org.rhq.core.domain.content.transfer.DeployPackagesResponse;
import org.rhq.core.domain.content.transfer.RemovePackagesResponse;
import org.rhq.core.domain.content.transfer.ResourcePackageDetails;
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.domain.resource.CreateResourceStatus;
import org.rhq.core.pluginapi.content.ContentContext;
import org.rhq.core.pluginapi.content.ContentFacet;
import org.rhq.core.pluginapi.content.ContentServices;
import org.rhq.core.pluginapi.event.log.LogFileEventResourceComponentHelper;
import org.rhq.core.pluginapi.inventory.ApplicationServerComponent;
import org.rhq.core.pluginapi.inventory.CreateChildResourceFacet;
import org.rhq.core.pluginapi.inventory.CreateResourceReport;
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.pluginapi.support.SnapshotReportRequest;
import org.rhq.core.pluginapi.support.SnapshotReportResults;
import org.rhq.core.pluginapi.support.SupportFacet;
import org.rhq.core.pluginapi.util.FileUtils;
import org.rhq.core.pluginapi.util.SelectiveSkippingEntityResolver;
import org.rhq.core.util.file.FileUtil;
import org.rhq.plugins.jbossas.helper.JavaSystemProperties;
import org.rhq.plugins.jbossas.helper.MainDeployer;
import org.rhq.plugins.jbossas.util.ConnectionFactoryConfigurationEditor;
import org.rhq.plugins.jbossas.util.DatasourceConfigurationEditor;
import org.rhq.plugins.jbossas.util.DeploymentUtility;
import org.rhq.plugins.jbossas.util.FileContentDelegate;
import org.rhq.plugins.jbossas.util.FileNameUtility;
import org.rhq.plugins.jbossas.util.JBossASContentFacetDelegate;
import org.rhq.plugins.jbossas.util.JBossASSnapshotReport;
import org.rhq.plugins.jbossas.util.XMLConfigurationEditor;
import org.rhq.plugins.jmx.JMXComponent;
import org.rhq.plugins.jmx.JMXDiscoveryComponent;
import org.rhq.plugins.jmx.util.ObjectNameQueryUtility;
/**
* Resource component for managing JBoss AS 3.2.3 through 4.2.x, and JBoss EAP and SOA-P 4.x.
*
* @author Greg Hinkle
* @author John Mazzitelli
* @author Jason Dobies
* @author Ian Springer
*/
public class JBossASServerComponent<T extends ResourceComponent<?>> implements MeasurementFacet, OperationFacet,
JMXComponent<T>, CreateChildResourceFacet, ApplicationServerComponent, ContentFacet, SupportFacet {
// Constants --------------------------------------------
private static final String LOCALHOST = "localhost";
public static final String NAMING_URL_CONFIG_PROP = "namingURL";
public static final String JBOSS_HOME_DIR_CONFIG_PROP = "jbossHomeDir";
public static final String CONFIGURATION_PATH_CONFIG_PROP = "configurationPath";
public static final String SCRIPT_PREFIX_CONFIG_PROP = "scriptPrefix";
public static final String CONFIGURATION_SET_CONFIG_PROP = "configurationSet";
public static final String START_SCRIPT_CONFIG_PROP = "startScript";
public static final String START_WAIT_MAX_PROP = "startWaitMax";
public static final String STOP_WAIT_MAX_PROP = "stopWaitMax";
public static final String SHUTDOWN_SCRIPT_CONFIG_PROP = "shutdownScript";
public static final String SHUTDOWN_MBEAN_CONFIG_PROP = "shutdownMbeanName";
public static final String SHUTDOWN_MBEAN_OPERATION_CONFIG_PROP = "shutdownMbeanOperation";
public static final String SHUTDOWN_METHOD_CONFIG_PROP = "shutdownMethod";
public static final String JAVA_HOME_PATH_CONFIG_PROP = "javaHomePath";
@Deprecated
public static final String AVAIL_CHECK_PERIOD_CONFIG_PROP = "availabilityCheckPeriod";
public static final String BINDING_ADDRESS_CONFIG_PROP = "bindingAddress";
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");
static final String DEFAULT_JAVA_HOME = System.getProperty(JavaSystemProperties.JAVA_HOME);
static final String DEFAULT_BIND_ADDRESS = "127.0.0.1";
// The following constants reference the exact name of the resource types as defined in the plugin descriptor
private static final String RESOURCE_TYPE_DATASOURCE = "Datasource";
private static final String RESOURCE_TYPE_CONNECTION_FACTORY = "ConnectionFactory";
private static final String RESOURCE_TYPE_EAR = "Enterprise Application (EAR)";
private static final String RESOURCE_TYPE_WAR = "Web Application (WAR)";
private static final String RESOURCE_TYPE_SAR = "Service Archive (SAR)"; // Not yet used
private static final String JNP_DISABLE_DISCOVERY_JNP_INIT_PROP = "jnp.disableDiscovery";
private static final String DISTRIBUTED_REPLICANT_MANAGER_MBEAN_NAME_TEMPLATE = "jboss:partitionName=%partitionName%,service=DistributedReplicantManager";
/**
* This is the timeout for the initial connection to the MBeanServer that is made by {@link #start(ResourceContext)}.
*/
private static final int JNP_TIMEOUT = 30 * 1000; // 30 seconds
/**
* This is the timeout for MBean attribute gets/sets and operations invoked on the remote MBeanServer.
* NOTE: This timeout comes into play if the JBossAS instance has gone down since the original JNP connection was made.
*/
private static final int JNP_SO_TIMEOUT = 15 * 1000; // 15 seconds
// Attributes --------------------------------------------
private final Log log = LogFactory.getLog(JBossASServerComponent.class);
private ResourceContext resourceContext;
private ContentContext contentContext;
private JBossASContentFacetDelegate contentFacetDelegate;
private EmsConnection connection;
private File configPath;
private String configSet;
private final Map<PackageType, FileContentDelegate> contentDelegates = new HashMap<PackageType, FileContentDelegate>();
/**
* Delegate instance for handling all calls to invoke operations on this component.
*/
private JBossASServerOperationsDelegate operationsDelegate;
private LogFileEventResourceComponentHelper logFileEventDelegate;
/**
* 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;
private MainDeployer mainDeployer;
private boolean loggedHijackedJnpUrlError;
// ResourceComponent Implementation --------------------------------------------
public void start(ResourceContext context) throws Exception {
this.resourceContext = context;
this.contentContext = context.getContentContext();
this.operationsDelegate = new JBossASServerOperationsDelegate(this, resourceContext.getSystemInformation());
validatePluginConfiguration();
Configuration pluginConfig = context.getPluginConfiguration();
this.configPath = resolvePathRelativeToHomeDir(getRequiredPropertyValue(pluginConfig,
CONFIGURATION_PATH_CONFIG_PROP));
if (!this.configPath.exists()) {
throw new InvalidPluginConfigurationException("Configuration path '" + configPath + "' does not exist.");
}
this.configSet = pluginConfig.getSimpleValue(CONFIGURATION_SET_CONFIG_PROP, this.configPath.getName());
// Until the bugs get worked out of the calls back into the PC's operation framework, use the implementation
// that will simply make calls directly in the plugin.
// controlFacade = new PluginContainerControlActionFacade(operationContext, this);
ControlActionFacade controlFacade = new InPluginControlActionFacade(this);
JBossASPaths jbossPaths = new JBossASPaths();
jbossPaths.setHomeDir(getPluginConfiguration().getSimpleValue(JBOSS_HOME_DIR_CONFIG_PROP, null));
jbossPaths.setServerDir(getPluginConfiguration().getSimpleValue(CONFIGURATION_PATH_CONFIG_PROP, null));
JBPMWorkflowManager workflowManager = new JBPMWorkflowManager(contentContext, controlFacade, jbossPaths);
this.contentFacetDelegate = new JBossASContentFacetDelegate(workflowManager, this.configPath);
// 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);
}
}
}
this.logFileEventDelegate = new LogFileEventResourceComponentHelper(this.resourceContext);
this.logFileEventDelegate.startLogFileEventPollers();
return;
}
public void stop() {
this.logFileEventDelegate.stopLogFileEventPollers();
if (this.connection != null) {
try {
this.connection.close();
} catch (Exception e) {
log.error("Error closing JBoss AS connection: " + e);
}
this.connection = null;
}
this.loggedHijackedJnpUrlError = false;
}
public AvailabilityType getAvailability() {
try {
File serverHomeViaJnp = getServerHome();
if (this.configPath.getCanonicalPath().equals(serverHomeViaJnp.getCanonicalPath())) {
this.loggedHijackedJnpUrlError = false;
return AvailabilityType.UP;
} else {
// A different server must have been started on our JNP URL - this is definitely something about which
// the user should be informed.
if (!this.loggedHijackedJnpUrlError) {
String namingURL = this.resourceContext.getPluginConfiguration().getSimpleValue(
NAMING_URL_CONFIG_PROP, null);
String message = "Availability check for JBoss AS Resource with configPath [" + this.configPath
+ "] has connected to a different running JBoss AS instance which is installed at ["
+ serverHomeViaJnp + "] using namingURL [" + namingURL
+ "] - returning AvailabilityType.DOWN...";
log.error(message);
this.loggedHijackedJnpUrlError = true;
// Throw an exception, so the PC can send the message to the Server for display in the GUI.
throw new RuntimeException(message);
}
return AvailabilityType.DOWN;
}
} catch (Exception e) {
return AvailabilityType.DOWN;
}
}
private File getServerHome() throws Exception {
EmsConnection connection = loadConnection();
EmsBean bean = connection.getBean("jboss.system:type=ServerConfig");
File serverHomeViaJnp;
EmsAttribute serverHomeDirAttrib = bean.getAttribute("ServerHomeDir");
if (serverHomeDirAttrib != null) {
serverHomeViaJnp = (File) serverHomeDirAttrib.refresh();
} else {
// We have a non-null MBean but a null ServerHomeDir attribute. This most likely means we're
// connected to a JBoss 5.x or 6.x instance, because in those versions the ServerConfig MBean no
// longer has a ServerHomeDir attribute. It instead has a ServerHomeLocation attribute, so give
// that a try, so getAvailabilityNow() can print a more intelligent warning.
EmsAttribute serverHomeLocationAttrib = bean.getAttribute("ServerHomeLocation");
URL serverHomeLocation = (URL) serverHomeLocationAttrib.refresh();
serverHomeViaJnp = toFile(serverHomeLocation);
}
return serverHomeViaJnp;
}
private static File toFile(URL url) {
File file;
try {
file = new File(url.toURI());
} catch (URISyntaxException e) {
file = new File(url.getPath());
}
return file;
}
// MeasurementFacet Implementation --------------------------------------------
public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> requests) {
for (MeasurementScheduleRequest request : requests) {
String name = request.getName();
if (name.equals("partitionName")) {
String partitionName = getPartitionName();
if (partitionName != null) {
report.addData(new MeasurementDataTrait(request, partitionName));
}
} else {
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(request, value.doubleValue()));
} else {
report.addData(new MeasurementDataTrait(request, valueObject.toString()));
}
} catch (Exception e) {
log.error("Failed to obtain measurement [" + name + "]", e);
}
}
}
}
// OperationFacet Implementation --------------------------------------------
public OperationResult invokeOperation(String name, Configuration configuration) throws InterruptedException {
JBossASServerSupportedOperations operation = Enum.valueOf(JBossASServerSupportedOperations.class,
name.toUpperCase());
return operationsDelegate.invoke(operation, configuration);
}
// ContentFacet Implementation --------------------------------------------
public List<DeployPackageStep> generateInstallationSteps(ResourcePackageDetails packageDetails) {
return contentFacetDelegate.generateInstallationSteps(packageDetails);
}
public DeployPackagesResponse deployPackages(Set<ResourcePackageDetails> packages, ContentServices contentServices) {
return contentFacetDelegate.deployPackages(packages, contentServices);
}
public RemovePackagesResponse removePackages(Set<ResourcePackageDetails> packages) {
return contentFacetDelegate.removePackages(packages);
}
public Set<ResourcePackageDetails> discoverDeployedPackages(PackageType type) {
return contentFacetDelegate.discoverDeployedPackages(type);
}
public InputStream retrievePackageBits(ResourcePackageDetails packageDetails) {
return contentFacetDelegate.retrievePackageBits(packageDetails);
}
// CreateChildResourceFacet Implementation --------------------------------------------
public CreateResourceReport createResource(CreateResourceReport report) {
String resourceTypeName = report.getResourceType().getName();
try {
if (resourceTypeName.equals(RESOURCE_TYPE_DATASOURCE)) {
datasourceCreate(report);
} else if (resourceTypeName.equals(RESOURCE_TYPE_CONNECTION_FACTORY)) {
connectionFactoryCreate(report);
} else if (resourceTypeName.equals(RESOURCE_TYPE_EAR) || resourceTypeName.equals(RESOURCE_TYPE_WAR)) {
earWarCreate(report, resourceTypeName);
} else {
throw new UnsupportedOperationException("Unknown Resource type: " + resourceTypeName);
}
} catch (Exception e) {
setErrorOnCreateResourceReport(report, e);
}
return report;
}
// SupportFacet Implementation --------------------------------------------
public SnapshotReportResults getSnapshotReport(SnapshotReportRequest request) throws Exception {
Configuration pluginConfig = resourceContext.getPluginConfiguration();
String tmpDir = resourceContext.getTemporaryDirectory().getAbsolutePath();
JBossASSnapshotReport report = new JBossASSnapshotReport(request.getName(), request.getDescription(),
pluginConfig, this.configPath.getCanonicalPath(), tmpDir);
File reportFile = report.generate();
InputStream inputStream = new BufferedInputStream(new FileInputStream(reportFile));
SnapshotReportResults results = new SnapshotReportResults(inputStream);
return results;
}
// JMXComponent Implementation --------------------------------------------
public EmsConnection getEmsConnection() {
EmsConnection emsConnection = null;
try {
emsConnection = loadConnection();
} catch (Exception e) {
log.error("Component attempting to access a connection that could not be loaded");
}
return emsConnection;
}
// ApplicationComponent Implementation --------------------------------------------
/**
* Return the absolute path of this JBoss server's configuration directory (e.g.
* "C:\opt\jboss-4.2.2.GA\server\default").
*
* @return the absolute path of this JBoss server's configuration directory (e.g.
* "C:\opt\jboss-4.2.2.GA\server\default")
*/
@NotNull
public File getConfigurationPath() {
return this.configPath;
}
// Public --------------------------------------------
public Configuration getPluginConfiguration() {
return resourceContext.getPluginConfiguration();
}
public File getDeploymentFilePath(String objectName) {
return DeploymentUtility.getDescriptorFile(connection, objectName);
}
public String getConfigurationSet() {
return this.configSet;
}
/**
* Return the absolute path of this JBoss server's start script (e.g. "C:\opt\jboss-4.2.1.GA\bin\run.sh").
*
* @return the absolute path of this JBoss server's start script (e.g. "C:\opt\jboss-4.2.1.GA\bin\run.sh")
*/
@NotNull
public File getStartScriptPath() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String startScript = pluginConfig.getSimpleValue(START_SCRIPT_CONFIG_PROP, DEFAULT_START_SCRIPT);
File startScriptFile = resolvePathRelativeToHomeDir(startScript);
return startScriptFile;
}
/**
* Return the absolute path of this JBoss server's shutdown script (e.g. "C:\opt\jboss-4.2.1.GA\bin\shutdown.sh").
*
* @return the absolute path of this JBoss server's shutdown script (e.g. "C:\opt\jboss-4.2.1.GA\bin\shutdown.sh")
*/
@NotNull
public File getShutdownScriptPath() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String shutdownScript = pluginConfig.getSimpleValue(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"); will only return
* null in the rare case when the "java.home" system property is not set, and when this is the case, a warning will
* be logged.
*
* @return the absolute path of this JBoss server's JAVA_HOME directory (e.g. "C:\opt\jdk1.5.0_14"); will only be
* null in the rare case when the "java.home" system property is not set
*/
@Nullable
public File getJavaHomePath() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String javaHomePath = pluginConfig.getSimpleValue(JAVA_HOME_PATH_CONFIG_PROP, DEFAULT_JAVA_HOME);
if (javaHomePath == null) {
log.warn("The '" + JavaSystemProperties.JAVA_HOME
+ "' System property is not set - unable to set default value for the '" + JAVA_HOME_PATH_CONFIG_PROP
+ "' connection property.");
}
File javaHome = (javaHomePath != null) ? new File(javaHomePath) : null;
return javaHome;
}
@NotNull
public String getBindingAddress() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String bindingAddress = pluginConfig.getSimpleValue(BINDING_ADDRESS_CONFIG_PROP, DEFAULT_BIND_ADDRESS);
return bindingAddress;
}
public MainDeployer getMainDeployer() {
return this.mainDeployer;
}
// Here we do any validation that couldn't be achieved via the metadata-based constraints.
private void validatePluginConfiguration() {
validateJBossHomeDirProperty();
validateJavaHomePathProperty();
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String principal = pluginConfig.getSimpleValue(JBossASServerComponent.PRINCIPAL_CONFIG_PROP, null);
String credentials = pluginConfig.getSimpleValue(JBossASServerComponent.CREDENTIALS_CONFIG_PROP, null);
if ((principal != null) && (credentials == null)) {
throw new InvalidPluginConfigurationException("If the '" + JBossASServerComponent.PRINCIPAL_CONFIG_PROP
+ "' connection property is set, the '" + JBossASServerComponent.CREDENTIALS_CONFIG_PROP
+ "' connection property must also be set.");
}
if ((credentials != null) && (principal == null)) {
throw new InvalidPluginConfigurationException("If the '" + JBossASServerComponent.CREDENTIALS_CONFIG_PROP
+ "' connection property is set, the '" + JBossASServerComponent.PRINCIPAL_CONFIG_PROP
+ "' connection property must also be set.");
}
}
void validateJBossHomeDirProperty() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String jbossHome = getRequiredPropertyValue(pluginConfig, JBOSS_HOME_DIR_CONFIG_PROP);
File jbossHomeDir = new File(jbossHome);
if (!jbossHomeDir.isAbsolute()) {
throw new InvalidPluginConfigurationException(
JBossASServerComponent.JBOSS_HOME_DIR_CONFIG_PROP
+ " connection property ('"
+ jbossHomeDir
+ "') is not an absolute path. Note, on Windows, absolute paths must start with the drive letter (e.g. C:).");
}
if (!jbossHomeDir.exists()) {
throw new InvalidPluginConfigurationException(JBossASServerComponent.JBOSS_HOME_DIR_CONFIG_PROP
+ " connection property ('" + jbossHomeDir + "') does not exist.");
}
if (!jbossHomeDir.isDirectory()) {
throw new InvalidPluginConfigurationException(JBossASServerComponent.JBOSS_HOME_DIR_CONFIG_PROP
+ " connection property ('" + jbossHomeDir + "') is a file, not a directory.");
}
}
void validateJavaHomePathProperty() {
Configuration pluginConfig = this.resourceContext.getPluginConfiguration();
String javaHome = pluginConfig.getSimpleValue(JBossASServerComponent.JAVA_HOME_PATH_CONFIG_PROP, null);
if (javaHome != null) {
File javaHomeDir = new File(javaHome);
if (!javaHomeDir.isAbsolute()) {
throw new InvalidPluginConfigurationException(
JBossASServerComponent.JAVA_HOME_PATH_CONFIG_PROP
+ " 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(JBossASServerComponent.JAVA_HOME_PATH_CONFIG_PROP
+ " connection property ('" + javaHomeDir + "') does not exist.");
}
if (!javaHomeDir.isDirectory()) {
throw new InvalidPluginConfigurationException(JBossASServerComponent.JAVA_HOME_PATH_CONFIG_PROP
+ " connection property ('" + javaHomeDir + "') is not a directory.");
}
}
}
private void datasourceCreate(CreateResourceReport report) throws Exception {
Configuration config = report.getResourceConfiguration();
String name = config.getSimple("jndi-name").getStringValue();
if (DeploymentUtility.isDuplicateJndiName(connection, XMLConfigurationEditor.DATASOURCE_MBEAN_NAME, name)) {
report.setStatus(CreateResourceStatus.FAILURE);
String errorMessage = getDuplicateJndiNameErrorMessage(report.getResourceType().getName(), name);
report.setErrorMessage(errorMessage);
return;
}
File deployDir = new File(getConfigurationPath(), "deploy");
File dsFile = new File(deployDir, FileNameUtility.formatFileName(name) + "-ds.xml");
DatasourceConfigurationEditor.updateDatasource(dsFile, name, report);
deployFile(dsFile);
String objectName = String.format("jboss.jca:name=%s,service=DataSourceBinding", name);
// IMPORTANT: The object name must be canonicalized so it matches the resource key that
// MBeanResourceDiscoveryComponent uses, which is the canonical object name.
report.setResourceKey(getCanonicalName(objectName));
setResourceName(report, name);
}
private void connectionFactoryCreate(CreateResourceReport report) throws MainDeployer.DeployerException {
Configuration config = report.getResourceConfiguration();
String name = config.getSimple("jndi-name").getStringValue();
if (DeploymentUtility.isDuplicateJndiName(connection, XMLConfigurationEditor.CONNECTION_MBEAN_NAME, name)) {
String errorMessage = getDuplicateJndiNameErrorMessage(report.getResourceType().getName(), name);
setErrorOnCreateResourceReport(report, errorMessage, null);
return;
}
File deployDir = new File(getConfigurationPath(), "deploy");
File dsFile = new File(deployDir, FileNameUtility.formatFileName(name) + "-ds.xml");
ConnectionFactoryConfigurationEditor.updateConnectionFactory(dsFile, name, report);
deployFile(dsFile);
String objectName = String.format("jboss.jca:name=%s,service=ConnectionFactoryBinding", name);
// IMPORTANT: The object name must be canonicalized so it matches the resource key that
// MBeanResourceDiscoveryComponent uses, which is the canonical object name.
report.setResourceKey(getCanonicalName(objectName));
setResourceName(report, name);
}
static void setErrorOnCreateResourceReport(CreateResourceReport report, String message) {
setErrorOnCreateResourceReport(report, message, null);
}
static void setErrorOnCreateResourceReport(CreateResourceReport report, Exception e) {
setErrorOnCreateResourceReport(report, null, e);
}
static void setErrorOnCreateResourceReport(CreateResourceReport report, String message, Exception e) {
report.setStatus(CreateResourceStatus.FAILURE);
report.setErrorMessage(message);
report.setException(e);
}
void deployFile(File file) throws MainDeployer.DeployerException {
getEmsConnection();
if (this.connection == null) {
log.warn("Unable to deploy " + file + ", because we could not connect to the JBoss instance.");
return;
}
if (this.mainDeployer == null) {
throw new IllegalStateException("Unable to deploy " + file + ", because MainDeployer MBean could "
+ "not be accessed - this should never happen.");
}
this.mainDeployer.deploy(file);
}
void redeployFile(File file) throws MainDeployer.DeployerException {
getEmsConnection();
if (this.connection == null) {
log.warn("Unable to redeploy " + file + ", because we could not connect to the JBoss instance.");
return;
}
if (this.mainDeployer == null) {
throw new IllegalStateException("Unable to redeploy " + file + ", because MainDeployer MBean could "
+ "not be accessed - this should never happen.");
}
this.mainDeployer.redeploy(file);
}
void undeployFile(File file) throws MainDeployer.DeployerException {
getEmsConnection();
if (this.connection == null) {
log.warn("Unable to undeploy " + file + ", because we could not connect to the JBoss instance.");
return;
}
if (this.mainDeployer == null) {
throw new IllegalStateException("Unable to undeploy " + file + ", because MainDeployer MBean could "
+ "not be accessed - this should never happen.");
}
this.mainDeployer.undeploy(file);
}
private String getDuplicateJndiNameErrorMessage(String resourceTypeName, String name) {
return "Duplicate JNDI Name: " + name + " - a " + resourceTypeName + " with that name already exists.";
}
private static String getCanonicalName(String objectName) {
ObjectName on;
try {
on = new ObjectName(objectName);
} catch (MalformedObjectNameException e) {
throw new IllegalStateException("Malformed JMX object name: " + objectName + " - "
+ e.getLocalizedMessage());
}
return on.getCanonicalName();
}
private void setResourceName(CreateResourceReport report, String baseName) {
String resourceName;
if (report.getUserSpecifiedResourceName() != null) {
resourceName = report.getUserSpecifiedResourceName();
} else {
resourceName = baseName + " " + report.getResourceType().getName();
}
report.setResourceName(resourceName);
}
@NotNull
private File resolvePathRelativeToHomeDir(@NotNull String path) {
return resolvePathRelativeToHomeDir(this.resourceContext.getPluginConfiguration(), path);
}
@NotNull
static File resolvePathRelativeToHomeDir(Configuration pluginConfig, @NotNull String path) {
File configDir = new File(path);
if (!FileUtil.isAbsolutePath(path)) {
String jbossHomeDir = getRequiredPropertyValue(pluginConfig, JBOSS_HOME_DIR_CONFIG_PROP);
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 static 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;
}
/**
* Returns the operation delegate configured against the resource represented by this component.
*
* @return will not be <code>null</code>
*/
@NotNull
public JBossASServerOperationsDelegate getOperationsDelegate() {
return operationsDelegate;
}
private void earWarCreate(CreateResourceReport report, String resourceTypeName) throws Exception {
ResourcePackageDetails details = report.getPackageDetails();
PackageDetailsKey key = details.getKey();
String archiveName = key.getName();
// First check to see if the file name has the correct extension. Reject if the user attempts to
// deploy a WAR file with a bad extension.
String expectedExtension = resourceTypeName.equals(RESOURCE_TYPE_EAR) ? "ear" : "war";
int lastPeriod = archiveName.lastIndexOf(".");
String extension = archiveName.substring(lastPeriod + 1);
if (lastPeriod == -1 || !expectedExtension.equals(extension)) {
setErrorOnCreateResourceReport(report, "Incorrect extension specified on filename [" + archiveName
+ "]. Expected [" + expectedExtension + "]");
return;
}
Configuration deployTimeConfiguration = details.getDeploymentTimeConfiguration();
String deployDirectory = deployTimeConfiguration.getSimple("deployDirectory").getStringValue();
if (deployDirectory == null) {
// should not be null, but you never know ..
setErrorOnCreateResourceReport(report, "Property 'deployDirectory' was unexpectedly null");
return;
}
// Verify the user did not enter a path that represents a security issue:
// - No absolute directories; must be relative to the configuration path
// - Cannot contain parent directory references
File relativeDeployDir = new File(deployDirectory);
if (relativeDeployDir.isAbsolute()) {
setErrorOnCreateResourceReport(report, "Path to deploy (deployDirectory) must be a relative path. "
+ "Path specified: " + deployDirectory);
return;
}
if (deployDirectory.contains("..")) {
setErrorOnCreateResourceReport(report,
"Path to deploy (deployDirectory) may not reference the parent directory. " + "Path specified: "
+ deployDirectory);
return;
}
boolean createBackup = false;
PropertySimple backupProperty = deployTimeConfiguration.getSimple("createBackup");
if (backupProperty != null && backupProperty.getBooleanValue() != null && backupProperty.getBooleanValue())
createBackup = true;
// Perform the deployment
File deployDir = new File(getConfigurationPath(), deployDirectory);
FileContentDelegate deployer = new FileContentDelegate(deployDir, "", details.getPackageTypeName());
File path = deployer.getPath(details);
if (!createBackup && path.exists()) {
setErrorOnCreateResourceReport(report, "A " + resourceTypeName + " file named " + path.getName()
+ " is already deployed with path " + path + ".");
return;
}
PropertySimple zipProperty = deployTimeConfiguration.getSimple("deployZipped");
if (zipProperty == null || zipProperty.getBooleanValue() == null) {
setErrorOnCreateResourceReport(report, "Zipped property is required.");
return;
}
boolean zip = zipProperty.getBooleanValue();
File tempDir = resourceContext.getTemporaryDirectory();
File tempFile = new File(tempDir.getAbsolutePath(), "ear_war.bin");
OutputStream osForTempDir = new BufferedOutputStream(new FileOutputStream(tempFile));
ContentServices contentServices = contentContext.getContentServices();
contentServices.downloadPackageBitsForChildResource(contentContext, resourceTypeName, key, osForTempDir);
osForTempDir.close();
// check for content
boolean valid = isOfType(tempFile, resourceTypeName);
if (!valid) {
setErrorOnCreateResourceReport(report, "Expected a " + resourceTypeName
+ " file, but its format/content did not match");
return;
}
deployer.createContent(details, tempFile, !zip, createBackup);
String vhost = null;
if (resourceTypeName.equals(RESOURCE_TYPE_WAR)) {
vhost = getVhostFromWarFile(tempFile);
}
// Resource key should match the following:
// EAR: jboss.management.local:J2EEServer=Local,j2eeType=J2EEApplication,name=rhq.ear
// WAR: jboss.management.local:J2EEApplication=null,J2EEServer=Local,j2eeType=WebModule,name=embedded-console.war
String resourceKey;
if (resourceTypeName.equals(RESOURCE_TYPE_EAR)) {
resourceKey = "jboss.management.local:J2EEServer=Local,j2eeType=J2EEApplication,name=" + archiveName;
} else {
resourceKey = "jboss.management.local:J2EEApplication=null,J2EEServer=Local,j2eeType=WebModule,name="
+ archiveName;
if (!LOCALHOST.equals(vhost))
resourceKey += ",vhost=" + vhost;
}
report.setResourceName(archiveName);
report.setResourceKey(resourceKey);
report.setStatus(CreateResourceStatus.SUCCESS);
try {
deployFile(path);
} catch (MainDeployer.DeployerException e) {
log.debug("Failed to deploy [" + path + "] - undeploying and deleting [" + path + "]...");
report.setStatus(CreateResourceStatus.FAILURE);
try {
undeployFile(path);
FileUtils.purge(path, true);
} catch (Exception e1) {
log.error("Failed to rollback deployment of [" + path + "].", e1);
}
throw e;
}
}
/**
* Parse the passed war file, try to read an enclosed jboss-web.xml and look for
* virtual-hosts in it. If found, return one virtual host name. Else return localhost.
* @param warFile File pointer pointing to a .war file
* @return The name of a defined virtual host or localhost
*/
private String getVhostFromWarFile(File warFile) {
JarFile jfile = null;
InputStream is = null;
try {
jfile = new JarFile(warFile);
JarEntry entry = jfile.getJarEntry("WEB-INF/jboss-web.xml");
if (entry != null) {
is = jfile.getInputStream(entry);
SAXBuilder saxBuilder = new SAXBuilder();
SelectiveSkippingEntityResolver entityResolver = SelectiveSkippingEntityResolver
.getDtdAndXsdSkippingInstance();
saxBuilder.setEntityResolver(entityResolver);
Document doc = saxBuilder.build(is);
Element root = doc.getRootElement(); // <jboss-web>
List<Element> vHosts = root.getChildren("virtual-host");
if (vHosts == null || vHosts.isEmpty()) {
if (log.isDebugEnabled())
log.debug("No vhosts found in war file, using " + LOCALHOST);
return LOCALHOST;
}
// So we have vhost, just return one of them, this is enough
Element vhost = vHosts.get(0);
return vhost.getText();
}
} catch (Exception ioe) {
log.warn("Exception when getting vhost from war file : " + ioe.getMessage());
} finally {
if (jfile != null) {
if (is != null) {
try {
// see http://bugs.sun.com/view_bug.do?bug_id=6735255 for why we do this
is.close();
} catch (IOException e) {
log.info("Exception when trying to close the war file stream: " + e.getMessage());
}
}
try {
jfile.close();
} catch (IOException e) {
log.info("Exception when trying to close the war file: " + e.getMessage());
}
}
}
// We're not able to determine a vhost, so return localhost
return LOCALHOST;
}
/**
* Check to see if the passed file is actually in jar format and contains a
* <ul>
* <li>WEB-INF/web.xml for .war </li>
* <li>META-INF/application.xml for .ear</li>
* <li>META-INF/jboss.service.xml for .sar</li>
* </ul>
* @param file File to check
* @param type Type to match - see RESOURCE_TYPE_SAR, RESOURCE_TYPE_WAR and RESOURCE_TYPE_EAR
* @return true is the file is in jar format and matches the type
*/
private boolean isOfType(File file, String type) {
JarFile jfile = null;
try {
jfile = new JarFile(file);
JarEntry entry;
if (RESOURCE_TYPE_WAR.equals(type))
entry = jfile.getJarEntry("WEB-INF/web.xml");
else if (RESOURCE_TYPE_EAR.equals(type))
entry = jfile.getJarEntry("META-INF/application.xml");
else if (RESOURCE_TYPE_SAR.equals(type)) // Not yet used
entry = jfile.getJarEntry("META-INF/jboss-service.xml");
else {
entry = null; // unknown type
log.warn("isOfType: " + type + " is unknown - not a valid file");
}
if (entry != null)
return true;
return false;
} catch (Exception e) {
log.info(e.getMessage());
return false;
} finally {
if (jfile != null)
try {
jfile.close();
} catch (IOException e) {
log.info("Exception when trying to close the war file: " + e.getMessage());
}
}
}
@Nullable
private String getPartitionName() {
ObjectNameQueryUtility queryUtility = new ObjectNameQueryUtility(
DISTRIBUTED_REPLICANT_MANAGER_MBEAN_NAME_TEMPLATE);
try {
List<EmsBean> mBeans = loadConnection().queryBeans(queryUtility.getTranslatedQuery());
if (mBeans.size() == 1) {
if (queryUtility.setMatchedKeyValues(mBeans.get(0).getBeanName().getKeyProperties())) {
return queryUtility.getVariableValues().get("partitionName");
}
}
} catch (Exception e) {
log.error("Could not load partition name as connection could not be loaded");
}
return null;
}
/**
* 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
*/
// TODO (ips): Refactor this method to use the JmxConnectionHelper class from the jboss-as-common module, which is
// what the jboss-as-5 plugin uses.
private synchronized EmsConnection loadConnection() throws Exception {
if (this.connection == null) {
try {
Configuration pluginConfig = resourceContext.getPluginConfiguration();
String jbossHomeDir = pluginConfig.getSimpleValue(JBOSS_HOME_DIR_CONFIG_PROP, null);
ConnectionSettings connectionSettings = new ConnectionSettings();
String connectionTypeDescriptorClass = pluginConfig.getSimple(JMXDiscoveryComponent.CONNECTION_TYPE)
.getStringValue();
connectionSettings.initializeConnectionType((ConnectionTypeDescriptor) Class.forName(
connectionTypeDescriptorClass).newInstance());
connectionSettings.setServerUrl(pluginConfig.getSimpleValue(NAMING_URL_CONFIG_PROP, null));
connectionSettings.setPrincipal(pluginConfig.getSimpleValue(PRINCIPAL_CONFIG_PROP, null));
connectionSettings.setCredentials(pluginConfig.getSimpleValue(CREDENTIALS_CONFIG_PROP, null));
connectionSettings.setLibraryURI(jbossHomeDir);
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.discoverServerClasses(connectionSettings);
if (connectionSettings.getAdvancedProperties() == null) {
connectionSettings.setAdvancedProperties(new Properties());
}
connectionSettings.getAdvancedProperties().setProperty(JNP_DISABLE_DISCOVERY_JNP_INIT_PROP, "true");
// Make sure the timeout always happens, even if the JBoss server is hung.
connectionSettings.getAdvancedProperties().setProperty("jnp.timeout", String.valueOf(JNP_TIMEOUT));
connectionSettings.getAdvancedProperties().setProperty("jnp.sotimeout", String.valueOf(JNP_SO_TIMEOUT));
// 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 GH: 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
connectionSettings.getControlProperties().setProperty(ConnectionFactory.JAR_TEMP_DIR,
resourceContext.getTemporaryDirectory().getAbsolutePath());
connectionSettings.getAdvancedProperties().setProperty(InternalVMTypeDescriptor.DEFAULT_DOMAIN_SEARCH,
"jboss");
log.info("Loading JBoss connection [" + connectionSettings.getServerUrl() + "] with install path ["
+ connectionSettings.getLibraryURI() + "]...");
ConnectionProvider connectionProvider = connectionFactory.getConnectionProvider(connectionSettings);
this.connection = connectionProvider.connect();
this.connection.loadSynchronous(false); // this loads all the MBeans
this.consecutiveConnectionErrors = 0;
try {
this.mainDeployer = new MainDeployer(this.connection);
} catch (Exception e) {
log.error("Unable to access MainDeployer MBean required for creation and deletion of managed "
+ "resources - this should never happen. Cause: " + e);
}
if (log.isDebugEnabled())
log.debug("Successfully made connection to the AS instance 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 (connection != null) {
if (log.isDebugEnabled())
log.debug("Connection created but an exception was thrown. Closing the connection.", e);
connection.close();
connection = null;
}
// Since the connection is attempted each time it's used, failure to connect could result in log
// file spamming. Log it once for every 10 consecutive times it's encountered.
if (consecutiveConnectionErrors % 10 == 0) {
log.warn(
"Could not establish connection to the JBoss AS instance [" + (consecutiveConnectionErrors + 1)
+ "] times for resource [" + resourceContext.getResourceKey() + "]", e);
}
if (log.isDebugEnabled())
log.debug(
"Could not connect to the JBoss AS instance for resource [" + resourceContext.getResourceKey()
+ "]", e);
consecutiveConnectionErrors++;
throw e;
}
}
return connection;
}
}