/* * RHQ Management Platform * Copyright (C) 2005-2012 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.jmx; import java.io.File; import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.management.MBeanServerConnection; import javax.management.openmbean.CompositeData; import javax.management.openmbean.TabularData; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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.support.ConnectionProvider; import org.mc4j.ems.connection.support.metadata.J2SE5ConnectionTypeDescriptor; import org.rhq.core.domain.configuration.Configuration; import org.rhq.core.domain.configuration.PropertySimple; import org.rhq.core.domain.resource.ResourceUpgradeReport; import org.rhq.core.pluginapi.inventory.DiscoveredResourceDetails; import org.rhq.core.pluginapi.inventory.InvalidPluginConfigurationException; import org.rhq.core.pluginapi.inventory.ManualAddFacet; import org.rhq.core.pluginapi.inventory.ProcessScanResult; import org.rhq.core.pluginapi.inventory.ResourceComponent; import org.rhq.core.pluginapi.inventory.ResourceDiscoveryComponent; import org.rhq.core.pluginapi.inventory.ResourceDiscoveryContext; import org.rhq.core.pluginapi.upgrade.ResourceUpgradeContext; import org.rhq.core.pluginapi.upgrade.ResourceUpgradeFacet; import org.rhq.core.system.ProcessInfo; import org.rhq.plugins.jmx.util.ConnectionProviderFactory; import org.rhq.plugins.jmx.util.JvmResourceKey; import org.rhq.plugins.jmx.util.JvmUtility; /** * This component will discover JVM processes that appear to be long-running (i.e. "servers"). Specifically, it will * discover java processes that: * <ul> * <li>have enabled JMX Remoting (JSR-160) via com.sun.management.jmxremote* system properties on their command lines, * or</li> * <li>are Sun/Oracle-compatible java processes accessible via the com.sun.tools.attach API AND specify the * org.rhq.resourceKey system property on their command lines (e.g. -Dorg.rhq.resourceKey=FOO); the attach API uses IPC * under the covers, so for a process to be accessible, it must be running as the same user as the RHQ Agent; even * if the Agent is running as root, processes running as other users cannot be accessed via the attach API</li> * </ul> * Some other java processes that do not meet these criteria can be manually added if they expose JMX remotely in * another supported form (WebLogic, WebSphere, etc.). * * @author Greg Hinkle * @author Ian Springer */ public class JMXDiscoveryComponent implements ResourceDiscoveryComponent, ManualAddFacet, ResourceUpgradeFacet { //, ClassLoaderFacet { private static final Log log = LogFactory.getLog(JMXDiscoveryComponent.class); public static final String COMMAND_LINE_CONFIG_PROPERTY = "commandLine"; public static final String CONNECTOR_ADDRESS_CONFIG_PROPERTY = "connectorAddress"; public static final String INSTALL_URI = "installURI"; public static final String CONNECTION_TYPE = "type"; public static final String PARENT_TYPE = "PARENT"; public static final String ADDITIONAL_CLASSPATH_ENTRIES = "additionalClassPathEntries"; private static final String SYSPROP_JMXREMOTE_PORT = "com.sun.management.jmxremote.port"; private static final String SYSPROP_RHQ_JMXPLUGIN_PROCESS_FILTERS = "rhq.jmxplugin.process-filters"; public static final String SYSPROP_RHQ_RESOURCE_KEY = "org.rhq.resourceKey"; private static final String SYSPROP_JAVA_VERSION = "java.version"; /* * Ignore certain java processes that are managed by their own plugin. For example, the Tomcat plugin will handle * Tomcat processes configured for JMX management. */ private static final String[] DEFAULT_PROCESS_EXCLUDES = new String[] { "org.rhq.enterprise.agent.AgentMain", // RHQ Agent "org.jboss.Main", // JBoss AS 3.x-6.x "catalina.startup.Bootstrap", // Tomcat "org.apache.cassandra.thrift.CassandraDaemon", // Cassnadra 1.1.x "org.apache.cassandra.service.CassandraDaemon" // Cassandra 1.2.x }; @Override public Set<DiscoveredResourceDetails> discoverResources(ResourceDiscoveryContext context) { Set<DiscoveredResourceDetails> discoveredResources = new LinkedHashSet<DiscoveredResourceDetails>(); Map<String, List<DiscoveredResourceDetails>> duplicatesByKey = new LinkedHashMap<String, List<DiscoveredResourceDetails>>(); // Filter out JBoss, Tomcat, etc. processes, which will be represented by more specific types of Resources // discovered by other plugins. List<ProcessScanResult> nonExcludedProcesses = getNonExcludedJavaProcesses(context); for (ProcessScanResult process : nonExcludedProcesses) { try { ProcessInfo processInfo = process.getProcessInfo(); DiscoveredResourceDetails details = discoverResourceDetails(context, processInfo); if (details != null) { //detect discovered jmx resources that are erroneously using the same key if (discoveredResources.contains(details)) { List<DiscoveredResourceDetails> duplicates = duplicatesByKey.get(details.getResourceKey()); if (duplicates == null) { duplicates = new ArrayList<DiscoveredResourceDetails>(); duplicatesByKey.put(details.getResourceKey(), duplicates); } duplicates.add(details); } discoveredResources.add(details); } } catch (RuntimeException re) { // Don't let a runtime exception for a particular ProcessInfo cause the entire discovery scan to fail. if (log.isDebugEnabled()) { log.debug("Error when trying to discover JVM process [" + process + "].", re); } else { log.warn("Error when trying to discover JVM process [" + process + "] (enable DEBUG for stack trace): " + re); } } } //Log the erroneous collisions and take them out of the discoveredResource list. for (String duplicateKey : duplicatesByKey.keySet()) { List<DiscoveredResourceDetails> duplicates = duplicatesByKey.get(duplicateKey); log.error("Multiple Resources with the same key (" + duplicateKey + ") were discovered - none will be reported to the plugin container! This most likely means that there are multiple java processes running with the same value for the " + SYSPROP_RHQ_RESOURCE_KEY + " system property specified on their command lines. Here is the list of Resources: " + duplicates); discoveredResources.remove(duplicates.get(0)); } return discoveredResources; } private List<ProcessScanResult> getNonExcludedJavaProcesses(ResourceDiscoveryContext context) { // This is the list of all currently running java processes. List<ProcessScanResult> javaProcesses = context.getAutoDiscoveredProcesses(); List<ProcessScanResult> nonExcludedJavaProcesses = new ArrayList<ProcessScanResult>(); Set<String> processExcludes = getProcessExcludes(); for (ProcessScanResult javaProcess : javaProcesses) { String[] args = javaProcess.getProcessInfo().getCommandLine(); StringBuilder buffer = new StringBuilder(); for (String arg : args) { buffer.append(arg).append(" "); } String commandLine = buffer.toString(); if (!isExcluded(commandLine, processExcludes)) { nonExcludedJavaProcesses.add(javaProcess); } else { if (log.isDebugEnabled()) { log.debug("Process [" + javaProcess.getProcessInfo() + "] excluded since its command line contains one of the following: " + processExcludes); } } } return nonExcludedJavaProcesses; } private boolean isExcluded(String commandLine, Set<String> processExcludes) { for (String processExclude : processExcludes) { if (commandLine.contains(processExclude)) { return true; } } return false; } protected Set<String> getProcessExcludes() { Set<String> processExcludes; String overrideProcessExcludes = System.getProperty(SYSPROP_RHQ_JMXPLUGIN_PROCESS_FILTERS); if (overrideProcessExcludes != null) { processExcludes = new HashSet<String>(Arrays.asList(overrideProcessExcludes.split(","))); } else { processExcludes = new HashSet<String>(Arrays.asList(DEFAULT_PROCESS_EXCLUDES)); } return processExcludes; } // MANUAL ADD @Override public DiscoveredResourceDetails discoverResource(Configuration pluginConfig, ResourceDiscoveryContext discoveryContext) throws InvalidPluginConfigurationException { String type = pluginConfig.getSimple(JMXDiscoveryComponent.CONNECTION_TYPE).getStringValue(); if (type.equals(PARENT_TYPE)) { throw new InvalidPluginConfigurationException("'" + PARENT_TYPE + "' is not a valid type for a manually added JVM."); } String connectorAddress = pluginConfig.getSimpleValue(CONNECTOR_ADDRESS_CONFIG_PROPERTY, null); if (connectorAddress == null) { throw new InvalidPluginConfigurationException("A connector address must be specified when manually adding a JVM."); } ConnectionProvider connectionProvider; EmsConnection connection = null; try { connectionProvider = ConnectionProviderFactory.createConnectionProvider(pluginConfig, null, discoveryContext.getParentResourceContext().getTemporaryDirectory()); connection = connectionProvider.connect(); connection.loadSynchronous(false); String key = connectorAddress; String name = connectorAddress; String version = getJavaVersion(connection); if (version == null) { log.warn("Unable to determine version of JVM with connector address [" + connectorAddress + "]."); } String connectionType = pluginConfig.getSimpleValue(CONNECTION_TYPE, null); String description = connectionType + " JVM (" + connectorAddress + ")"; DiscoveredResourceDetails resourceDetails = new DiscoveredResourceDetails(discoveryContext.getResourceType(), key, name, version, description, pluginConfig, null); return resourceDetails; } catch (Exception e) { if (e.getCause() instanceof SecurityException) { throw new InvalidPluginConfigurationException("Failed to authenticate to JVM with connector address [" + connectorAddress + "] - principal and/or credentials connection properties are not set correctly."); } throw new RuntimeException("Failed to connect to JVM with connector address [" + connectorAddress + "].", e); } finally { if(connection != null) { connection.close(); } } } private String getJavaVersion(EmsConnection connection) { String version = null; try { EmsBean runtimeMXBean = connection.getBean(ManagementFactory.RUNTIME_MXBEAN_NAME); if (runtimeMXBean != null) { EmsAttribute systemPropertiesAttribute = runtimeMXBean.getAttribute("systemProperties"); TabularData systemProperties = (TabularData) systemPropertiesAttribute.getValue(); CompositeData compositeData = systemProperties.get(new String[]{"java.version"}); if (compositeData != null) { version = (String) compositeData.get("value"); } } } catch (Exception e) { log.error("An error occurred while trying to determine Java version of remote JVM at [" + connection.getConnectionProvider().getConnectionSettings().getServerUrl() + "].", e); } return version; } // For now, this method is not used. This method is the ClassLoaderFacet method, but I commented // out the fact that this class implements that interface. As of today, 7/20/2009, I'm not sure we really // need to implement the classloader facet since EMS's ability to use additionalClasspathEntries in its // classloaders is all we need today to support JMX Server resource types. But I have a feeling it may be // necessary in the future to allow plugins to extend the JMX Server resource type and have it specify // "classLoaderType='instance'" in its plugin descriptor - if that use-case is ever needed, we'll want to // have this class implement ClassLoaderFacet which then brings this method in use (nothing would need to // be changed other than to add "implements ClassLoaderFacet" to the class definition). // For now, since we don't want the additional overhead of calling into this method when we currently have // no need for it, we do not implement the ClassLoaderFacet. public List<URL> getAdditionalClasspathUrls(ResourceDiscoveryContext<ResourceComponent<?>> context, DiscoveredResourceDetails details) throws Exception { List<File> jars = ConnectionProviderFactory.getAdditionalJarsFromConfig(details.getPluginConfiguration()); if (jars == null || jars.isEmpty()) { return null; } List<URL> urls = new ArrayList<URL>(jars.size()); for (File jar : jars) { urls.add(jar.toURI().toURL()); } return urls; } @Override public ResourceUpgradeReport upgrade(ResourceUpgradeContext inventoriedResource) { JvmResourceKey oldKey = JvmResourceKey.valueOf(inventoriedResource.getResourceKey()); JvmResourceKey.Type oldKeyType = oldKey.getType(); if (oldKeyType == JvmResourceKey.Type.Legacy || oldKeyType == JvmResourceKey.Type.JmxRemotingPort) { if (!inventoriedResource.getSystemInformation().isNative()) { log.warn("Cannot attempt to upgrade Resource key [" + inventoriedResource.getResourceKey() + "] of JVM Resource, because this Agent is not running with native system info support (i.e. SIGAR)."); return null; } Configuration pluginConfig = inventoriedResource.getPluginConfiguration(); String connectorAddress = pluginConfig.getSimpleValue(CONNECTOR_ADDRESS_CONFIG_PROPERTY, null); JMXServiceURL jmxServiceURL; try { jmxServiceURL = new JMXServiceURL(connectorAddress); } catch (MalformedURLException e) { throw new RuntimeException("Failed to parse connector address: " + connectorAddress, e); } JMXConnector jmxConnector = null; Long pid; try { jmxConnector = connect(jmxServiceURL); MBeanServerConnection mbeanServerConnection = jmxConnector.getMBeanServerConnection(); RuntimeMXBean runtimeMXBean = ManagementFactory.newPlatformMXBeanProxy(mbeanServerConnection, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class); pid = getJvmPid(runtimeMXBean); if (pid == null) { throw new RuntimeException("Failed to determine JVM pid by parsing JVM name."); } } catch (SecurityException e) { // Authentication failed, which most likely means the username and password are not set correctly in // the Resource's plugin config. This is not an error, so return null. log.info("Unable to upgrade key of JVM Resource with key [" + inventoriedResource.getResourceKey() + "], since authenticating to its JMX service URL [" + jmxServiceURL + "] failed: " + e.getMessage()); return null; } catch (IOException e) { // The JVM's not currently running, which means we won't be able to figure out its main class name, // which is needed to upgrade its key. This is not an error, so return null. log.debug("Unable to upgrade key of JVM Resource with key [" + inventoriedResource.getResourceKey() + "], since connecting to its JMX service URL [" + jmxServiceURL + "] failed: " + e); return null; } finally { close(jmxConnector); } List<ProcessInfo> processes = inventoriedResource.getSystemInformation().getProcesses( "process|pid|match=" + pid); if (processes.size() != 1) { throw new IllegalStateException("Failed to find process with PID [" + pid + "]."); } ProcessInfo process = processes.get(0); String mainClassName = getJavaMainClassName(process); String explicitKeyValue = getSystemPropertyValue(process, SYSPROP_RHQ_RESOURCE_KEY); if (oldKeyType == JvmResourceKey.Type.Legacy || explicitKeyValue != null) { // We need to upgrade the key. JvmResourceKey newKey; if (explicitKeyValue != null) { newKey = JvmResourceKey.fromExplicitValue(mainClassName, explicitKeyValue); } else { newKey = JvmResourceKey.fromJmxRemotingPort(mainClassName, oldKey.getJmxRemotingPort()); } ResourceUpgradeReport resourceUpgradeReport = new ResourceUpgradeReport(); resourceUpgradeReport.setNewResourceKey(newKey.toString()); return resourceUpgradeReport; } } return null; } private void close(JMXConnector jmxConnector) { try { if (jmxConnector != null) jmxConnector.close(); } catch (Exception e) {} } private static Long getJvmPid(RuntimeMXBean runtimeMXBean) { Long pid; String jvmName = runtimeMXBean.getName(); int atIndex = jvmName.indexOf('@'); pid = (atIndex != -1) ? Long.valueOf(jvmName.substring(0, atIndex)) : null; return pid; } protected DiscoveredResourceDetails discoverResourceDetails(ResourceDiscoveryContext context, ProcessInfo process) { Integer jmxRemotingPort = getJmxRemotingPort(process); JMXServiceURL jmxServiceURL = null; if (jmxRemotingPort != null) { // Use JMX Remoting when possible, since it doesn't require the RHQ Agent to have OS-level permissions to // communicate with the remote JVM via IPC. try { jmxServiceURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://127.0.0.1:" + jmxRemotingPort + "/jmxrmi"); } catch (MalformedURLException e) { throw new RuntimeException(e); } } else { // If JMX Remoting is not enabled, it's required that a Resource key is explicitly specified via the org.rhq.resourceKey sysprop. String keyString = getSystemPropertyValue(process, SYSPROP_RHQ_RESOURCE_KEY); if (keyString != null && !keyString.equals("")) { // Start up a JMX agent within the JVM via the Sun Attach API, and return a URL that can be used to connect // to that agent. // Note, this will only work if the remote JVM is Java 6 or later, and maybe some 64 bit Java 5 - see // JBNADM-3332. Also, the RHQ Agent will have to be running on a JDK, not a JRE, so that we can access // the JDK's tools.jar, which contains the Sun JVM Attach API classes. jmxServiceURL = JvmUtility.extractJMXServiceURL(process); if (jmxServiceURL == null) { return null; } } } log.debug("JMX service URL for java process [" + process + "] is [" + jmxServiceURL + "]."); return buildResourceDetails(context, process, jmxServiceURL, jmxRemotingPort); } protected DiscoveredResourceDetails buildResourceDetails(ResourceDiscoveryContext context, ProcessInfo process, JMXServiceURL jmxServiceURL, Integer jmxRemotingPort) { JvmResourceKey key = buildResourceKey(process, jmxRemotingPort); if (key == null) { return null; } String name = buildResourceName(key); String version = getJavaVersion(process, jmxServiceURL); String description = "JVM, monitored via " + ((jmxRemotingPort != null) ? "JMX Remoting" : "Sun JVM Attach API"); Configuration pluginConfig = context.getDefaultPluginConfiguration(); pluginConfig.put(new PropertySimple(CONNECTION_TYPE, J2SE5ConnectionTypeDescriptor.class.getName())); if (jmxRemotingPort != null) { pluginConfig.put(new PropertySimple(CONNECTOR_ADDRESS_CONFIG_PROPERTY, jmxServiceURL)); } return new DiscoveredResourceDetails(context.getResourceType(), key.toString(), name, version, description, pluginConfig, process); } private JvmResourceKey buildResourceKey(ProcessInfo process, Integer jmxRemotingPort) { JvmResourceKey key; String mainClassName = getJavaMainClassName(process); String keyString = getSystemPropertyValue(process, SYSPROP_RHQ_RESOURCE_KEY); if (keyString != null && !keyString.equals("")) { log.debug("Using explicitly specified Resource key: [" + keyString + "]..."); key = JvmResourceKey.fromExplicitValue(mainClassName, keyString); } else { if (jmxRemotingPort != null) { log.debug("Using JMX remoting port [" + jmxRemotingPort + "] as Resource key..."); key = JvmResourceKey.fromJmxRemotingPort(mainClassName, jmxRemotingPort); } else { log.debug("Process [" + process.getPid() + "] with command line [" + Arrays.asList(process.getCommandLine()) + "] cannot be discovered, because it does not specify either of the following system properties: " + "-D" + SYSPROP_JMXREMOTE_PORT + "=12345, -D" + SYSPROP_RHQ_RESOURCE_KEY + "=UNIQUE_KEY"); key = null; } } return key; } protected String getJavaVersion(ProcessInfo process, JMXServiceURL jmxServiceURL) { JMXConnector jmxConnector = null; try { jmxConnector = connect(jmxServiceURL); return getJavaVersion(jmxConnector); } catch (SecurityException e) { log.warn("Unable to to authenticate to JMX service URL [" + jmxServiceURL + "]: " + e.getMessage()); } catch (IOException e) { log.error("Failed to connect to JMX service URL [" + jmxServiceURL + "].", e); } catch (Exception e) { log.error("Failed to determine JVM version for process [" + process.getPid() + "] with command line [" + Arrays.asList(process.getCommandLine()) + "].", e); } finally { close(jmxConnector); } // TODO: We could exec "java -version" here. return null; } protected String getJavaVersion(JMXConnector jmxConnector) throws Exception { String version; MBeanServerConnection mbeanServerConnection = jmxConnector.getMBeanServerConnection(); RuntimeMXBean runtimeMXBean = ManagementFactory.newPlatformMXBeanProxy(mbeanServerConnection, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class); version = runtimeMXBean.getSystemProperties().get(SYSPROP_JAVA_VERSION); if (version == null) { throw new IllegalStateException("System property [" + SYSPROP_JAVA_VERSION + "] is not defined."); } return version; } private static JMXConnector connect(JMXServiceURL jmxServiceURL) throws IOException { JMXConnector jmxConnector; try { jmxConnector = JMXConnectorFactory.connect(jmxServiceURL); } catch (IOException e) { throw new IOException("Failed to connect to JMX service URL [" + jmxServiceURL + "]."); } return jmxConnector; } private String buildResourceName(JvmResourceKey key) { StringBuilder name = new StringBuilder(); String mainClassName = key.getMainClassName(); if (mainClassName != null) { if (mainClassName.length() <= 200) { name.append(mainClassName); } else { // Truncate it if it's really long for a more palatable Resource name. name.append(mainClassName.substring(mainClassName.length() - 200)); } } //build the resource names from supported JvmResourceKey instances. See JvmResourceKey.Type for more details. switch (key.getType()) { case Legacy: // implies main classname was not found. Include earlier naming format as well. name.append("JMX Server (" + key.getJmxRemotingPort() + ")"); break; case ConnectorAddress: name.append(key.getConnectorAddress()); break; case JmxRemotingPort: name.append(':').append(key.getJmxRemotingPort()); break; case Explicit: name.append(' ').append(key.getExplicitValue()); break; default: throw new IllegalStateException("Unsupported key type: " + key.getType()); } return name.toString(); } protected String getJavaMainClassName(ProcessInfo process) { // TODO (ips, 04/02/12): If command line contains "-jar foo.jar", pull the main class name out of foo.jar's // MANIFEST.MF. String className = null; for (int i = 1; i < process.getCommandLine().length; i++) { String arg = process.getCommandLine()[i]; if (!arg.startsWith("-")) { className = arg; break; } else if (arg.equals("-cp") || arg.equals("-classpath")) { // The next arg is the classpath - skip it. i++; } } return className; } protected Integer getJmxRemotingPort(ProcessInfo process) { String value = getSystemPropertyValue(process, SYSPROP_JMXREMOTE_PORT); if (value != null) { try { return Integer.valueOf(value); } catch (NumberFormatException e) { return null; } } else { return null; } } protected String getSystemPropertyValue(ProcessInfo process, String systemPropertyName) { for (String argument : process.getCommandLine()) { String prefix = "-D" + systemPropertyName + "="; if (argument.startsWith(prefix)) { return argument.substring(prefix.length()); } } return null; } }