/* * NOTE: This copyright does *not* cover user programs that use HQ * program services by normal system calls through the application * program interfaces provided as part of the Hyperic Plug-in Development * Kit or the Hyperic Client Development Kit - this is merely considered * normal use of the program, and does *not* fall under the heading of * "derived work". * * Copyright (C) [2004-2007], Hyperic, Inc. * This file is part of HQ. * * HQ is free software; you can redistribute it and/or modify * it under the terms version 2 of the GNU General Public License 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 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA. */ package org.hyperic.hq.product.jmx; import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; import java.net.MalformedURLException; import java.rmi.RemoteException; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.StringTokenizer; import javax.management.Attribute; import javax.management.AttributeNotFoundException; import javax.management.InstanceNotFoundException; import javax.management.IntrospectionException; import javax.management.InvalidAttributeValueException; import javax.management.MBeanException; import javax.management.MBeanInfo; import javax.management.MBeanServerConnection; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import javax.management.ReflectionException; import javax.management.j2ee.statistics.CountStatistic; import javax.management.j2ee.statistics.RangeStatistic; import javax.management.j2ee.statistics.Statistic; import javax.management.j2ee.statistics.Stats; import javax.management.j2ee.statistics.TimeStatistic; import javax.management.openmbean.CompositeData; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import javax.naming.Context; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hyperic.hq.product.Metric; import org.hyperic.hq.product.MetricInvalidException; import org.hyperic.hq.product.MetricNotFoundException; import org.hyperic.hq.product.MetricUnreachableException; import org.hyperic.hq.product.PluginException; import org.hyperic.sigar.Sigar; import org.hyperic.sigar.ptql.ProcessFinder; import org.hyperic.util.config.ConfigResponse; public class MxUtil { static final String PTQL_PREFIX = "ptql:"; public static final String PROP_JMX_URL = "jmx.url"; public static final String PROP_JMX_PORT = "jmx.port"; public static final String PROP_JMX_USERNAME = "jmx.username"; public static final String PROP_JMX_PASSWORD = "jmx.password"; public static final String PROP_JMX_PROVIDER_PKGS = "jmx.provider.pkgs"; private static final String STATS_PREFIX = "Stats."; private static final String COMPOSITE_PREFIX = "Composite."; private static final Log log = LogFactory.getLog(MxUtil.class); private static HashMap cache = new HashMap(); //expand Foo=* -> Foo=%Foo% static String expandObjectName(String name) { int ix = name.indexOf("*"); if (ix == -1) { return name; } StringBuffer objectName = new StringBuffer(); ix = name.indexOf(':'); if (ix != -1) { objectName.append(name.substring(0, ix+1)); name = name.substring(ix+1); //skip domain } StringTokenizer tok = new StringTokenizer(name, ","); while (tok.hasMoreTokens()) { String pair = tok.nextToken(); if (pair.equals("*")) { objectName.append(pair); break; } ix = pair.indexOf("="); if (ix == -1) { throw new IllegalArgumentException(name); } String key = pair.substring(0, ix); String val = pair.substring(ix+1); //domain:type=Foo,name=* if (val.equals("*")) { val = "%" + key + "%"; } objectName.append(key).append('=').append(val); if (tok.hasMoreTokens()) { objectName.append(','); } } return objectName.toString(); } static String expandObjectName(String name, ConfigResponse config) { return Metric.translate(expandObjectName(name), config); } static String expandObjectName(String name, Properties config) { return Metric.translate(expandObjectName(name), config); } private static MetricInvalidException invalidObjectName(String name, Exception e) { String msg = "Malformed ObjectName [" + name + "]"; return new MetricInvalidException(msg, e); } private static MetricUnreachableException unreachable(Properties props, Exception e) { String msg = "Can't connect to MBeanServer url [" + props.getProperty(PROP_JMX_URL) + "] " + "port [" + props.getProperty(PROP_JMX_PORT) + "] " + "username [" + props.getProperty(PROP_JMX_USERNAME) + "]: " + e; return new MetricUnreachableException(msg, e); } private static MetricNotFoundException objectNotFound(String name, Exception e) { String msg = "ObjectName not found [" + name + "]: " + e; return new MetricNotFoundException(msg, e); } private static MetricNotFoundException attributeNotFound(String object, String name, Exception e) { String msg = "Attribute not found [" + object + ":" + name + "]: " + e; return new MetricNotFoundException(msg, e); } private static PluginException error(String name, Exception e) { String msg = "Invocation error [" + name + "]: " + e; return new PluginException(msg, e); } private static PluginException invalidURL(Properties props, Exception e) { String msg = "Malformed URL: [" + props.getProperty(MxUtil.PROP_JMX_URL) + "]"; return new PluginException(msg, e); } private static PluginException error(String name, Exception e, String method) { String msg = "Method '" + method + "' invocation error [" + name + "]: " + e; return new PluginException(msg, e); } static Double getJSR77Statistic(MBeanServerConnection mServer, ObjectName objName, String attribute) throws MalformedURLException, MalformedObjectNameException, IOException, MBeanException, AttributeNotFoundException, InstanceNotFoundException, ReflectionException, PluginException { Stats stats; Boolean provider = (Boolean) mServer.getAttribute(objName, "statisticsProvider"); if ((provider == null) || !provider.booleanValue()) { String msg = objName + " does not provide statistics"; throw new PluginException(msg); } stats = (Stats)mServer.getAttribute(objName, "stats"); if (stats == null) { throw new PluginException(objName + " has no stats"); } String statName = attribute.substring(STATS_PREFIX.length()); Statistic stat = stats.getStatistic(statName); if (stat == null) { String msg = "Statistic '" + statName + "' not found [" + objName + "]"; throw new AttributeNotFoundException(msg); } long value; if (stat instanceof CountStatistic) { value = ((CountStatistic)stat).getCount(); } else if (stat instanceof RangeStatistic) { value = ((RangeStatistic)stat).getCurrent(); } else if (stat instanceof TimeStatistic) { // get the average time long count = ((TimeStatistic)stat).getCount(); if (count == 0) value = 0; else value = ((TimeStatistic)stat).getTotalTime() / count; } else { String msg = "Unsupported statistic type [" + statName.getClass().getName() + " for [" + objName + ":" + attribute + "]"; throw new MetricInvalidException(msg); } //XXX: handle bug with geronimo uptime metric if (statName.equals("UpTime")) { value = System.currentTimeMillis() - value; } return new Double(value); } //e.g. "Composite.Usage.committed" static Object getCompositeMetric(MBeanServerConnection mServer, ObjectName objName, String attribute) throws MalformedURLException, MalformedObjectNameException, IOException, MBeanException, AttributeNotFoundException, InstanceNotFoundException, ReflectionException, PluginException { String name = attribute.substring(COMPOSITE_PREFIX.length()); int ix = name.indexOf('.'); if (ix == -1) { throw new MetricInvalidException("Missing composite key"); } String attr = name.substring(0, ix); String key = name.substring(ix+1); Object obj = mServer.getAttribute(objName, attr); if (obj instanceof CompositeData) { return MxCompositeData.getValue((CompositeData)obj, key); } else { throw new MetricInvalidException("Not CompositeData"); } } static Object getValue(Metric metric) throws MetricNotFoundException, MetricInvalidException, MetricUnreachableException, PluginException { String objectName = Metric.decode(metric.getObjectName()); String attribute = metric.getAttributeName(); Properties config = metric.getProperties(); try { return getValue(config, objectName, attribute); } catch (MalformedURLException e) { throw invalidURL(metric.getProperties(), e); } catch (MalformedObjectNameException e) { throw invalidObjectName(objectName, e); } catch (IOException e) { removeMBeanConnector(config); if (metric.isAvail()) { return new Double(Metric.AVAIL_DOWN); } throw unreachable(metric.getProperties(), e); } catch (MBeanException e) { throw error(metric.toString(), e); } catch (AttributeNotFoundException e) { //XXX not all MBeans have a reasonable attribute to //determine availability, so just assume if we get this far //the MBean exists and is alive. if (metric.isAvail()) { return new Double(Metric.AVAIL_UP); } throw attributeNotFound(objectName, metric.getAttributeName(), e); } catch (InstanceNotFoundException e) { if (metric.isAvail()) { return new Double(Metric.AVAIL_DOWN); } throw objectNotFound(objectName, e); } catch (UndeclaredThrowableException e) { Throwable cause1 = e.getCause(); if (cause1 instanceof InvocationTargetException) { Throwable cause2 = cause1.getCause(); if (cause2 instanceof InstanceNotFoundException) { throw objectNotFound(objectName, (InstanceNotFoundException) cause2); } } throw e; } catch (ReflectionException e) { throw error(metric.toString(), e); } catch (RuntimeException e) { // Temporary fix until availability strings can be mapped // in hq-plugin.xml. Resin wraps AttributeNotFoundException if (metric.isAvail()) { Throwable cause = e.getCause(); while (cause != null) { if (cause instanceof AttributeNotFoundException) { return new Double(Metric.AVAIL_UP); }else if (cause instanceof InstanceNotFoundException) { return new Double(Metric.AVAIL_DOWN); } cause = cause.getCause(); } } throw e; } } private static void removeMBeanConnector(Properties config) { String jmxUrl = config.getProperty(MxUtil.PROP_JMX_URL); if (cache.get(jmxUrl) != null) { log.debug("Removing (stale) cached connection for: " + jmxUrl); disconnect(jmxUrl); } } //vmid == pid; use undocumented ConnectorAddressLink.importFrom(pid) //to get the local JMXServiceURL static String getUrlFromPid(String ptql) throws IOException { Sigar sigar = new Sigar(); String address; long pid; try { pid = new ProcessFinder(sigar).findSingleProcess(ptql); Class caddrLinkClass = Class.forName("sun.management.ConnectorAddressLink"); Method importFrom = caddrLinkClass.getMethod("importFrom", new Class[] { Integer.TYPE }); address = (String)importFrom.invoke(caddrLinkClass, new Object[] { new Integer((int)pid) }); } catch (ClassNotFoundException e) { String jvm = System.getProperty("java.vm.name") + " " + System.getProperty("java.vm.version"); throw new IOException(ptql + " " + e.getMessage() + " not supported by " + jvm); } catch(InvocationTargetException e) { throw new IOException(ptql + " " + e.getCause()); } catch (Exception e) { throw new IOException(ptql + " " + e); } finally { sigar.close(); } if (address == null) { throw new IOException("Unable to determine " + PROP_JMX_URL + " using vmid=" + pid + ". Server must be started with: " + "-Dcom.sun.management.jmxremote"); } log.debug(PTQL_PREFIX + ptql + " resolved to vmid=" + pid + ", " + PROP_JMX_URL + "=" + address); return address; } private static final Map<JMXConnectorKey, JMXConnector> mbeanConns = new HashMap<JMXConnectorKey, JMXConnector>(); public static JMXConnector getCachedMBeanConnector(Properties config) throws MalformedURLException, IOException { String jmxUrl = config.getProperty(MxUtil.PROP_JMX_URL); String user = config.getProperty(PROP_JMX_USERNAME); String pass = config.getProperty(PROP_JMX_PASSWORD); JMXConnectorKey key = new JMXConnectorKey(jmxUrl, user, pass); JMXConnector rtn = null; synchronized(mbeanConns) { rtn = mbeanConns.get(key); if (rtn == null) { rtn = getMBeanConnector(config); mbeanConns.put(key, rtn); } try { // ensure that the connection is not broken rtn.getMBeanServerConnection(); } catch (IOException e) { close(rtn); rtn = getMBeanConnector(config); mbeanConns.put(key, rtn); } } final JMXConnector c = rtn; final InvocationHandler handler = new InvocationHandler() { private final JMXConnector conn = c; public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("close")) { return null; } synchronized (conn) { return method.invoke(conn, args); } } }; return (JMXConnector) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[] {JMXConnector.class}, handler); } private static class JMXConnectorKey { private final String url; private final String user; private final String pass; private JMXConnectorKey(String url, String user, String pass) { this.url = url; this.user = user; this.pass = pass; } public boolean equals(Object rhs) { if (this == rhs) { return true; } if (!(rhs instanceof JMXConnectorKey)) { return false; } JMXConnectorKey r = (JMXConnectorKey) rhs; return r.url.equals(url) && equals(r.user, user) && equals(r.pass, pass); } private boolean equals(String buf1, String buf2) { if (buf1 == buf2) { return true; } if (buf1 == null && buf2 != null || buf2 == null && buf1 != null) { return false; } return buf1.equals(buf2); } public int hashCode() { int rtn = url.hashCode() * 7; rtn += (user != null) ? user.hashCode() * 7 : 0; rtn += (pass != null) ? pass.hashCode() * 7 : 0; return rtn; } } public static JMXConnector getMBeanConnector(Properties config) throws MalformedURLException, IOException { String jmxUrl = config.getProperty(MxUtil.PROP_JMX_URL); Map map = new HashMap(); String user = config.getProperty(PROP_JMX_USERNAME); String pass = config.getProperty(PROP_JMX_PASSWORD); map.put(JMXConnector.CREDENTIALS, new String[] {user, pass}); // required for Oracle AS String providerPackages = config.getProperty(PROP_JMX_PROVIDER_PKGS); if (providerPackages != null) map.put(JMXConnectorFactory.PROTOCOL_PROVIDER_PACKAGES, providerPackages); if (jmxUrl == null) { throw new MalformedURLException(PROP_JMX_URL + "==null"); } if (jmxUrl.startsWith(PTQL_PREFIX)) { jmxUrl = getUrlFromPid(jmxUrl.substring(PTQL_PREFIX.length())); } JMXServiceURL url = new JMXServiceURL(jmxUrl); String proto = url.getProtocol(); if (proto.equals("t3") || proto.equals("t3s")) { //http://edocs.bea.com/wls/docs92/jmx/accessWLS.html //WebLogic support, requires: //cp $WLS_HOME/server/lib/wljmxclient.jar pdk/lib/ //cp $WLS_HOME/server/lib/wlclient.jar pdk/lib/ map.put(JMXConnectorFactory.PROTOCOL_PROVIDER_PACKAGES, "weblogic.management.remote"); map.put(Context.SECURITY_PRINCIPAL, user); map.put(Context.SECURITY_CREDENTIALS, pass); } JMXConnector connector = JMXConnectorFactory.connect(url, map); if (log.isDebugEnabled()) { log.debug("created new JMXConnector url=" + url + ", classloader=" + Thread.currentThread().getContextClassLoader()); } return connector; } private static void disconnect(String jmxUrl) { Object obj = cache.remove(jmxUrl); if (obj != null) { JMXConnector connector = ((ConnectorInstance)obj).connector; close(connector); if (log.isDebugEnabled()) { log.debug("Closed previous connector (" + address(connector) + ") for: " + jmxUrl); } } } private static JMXConnector connect(Properties config, String jmxUrl, String jmxUsername, String jmxPassword) throws IOException { disconnect(jmxUrl); JMXConnector connector = getMBeanConnector(config); cache.put(jmxUrl, new ConnectorInstance(connector, jmxUsername, jmxPassword)); log.debug("Opened new connector (" + address(connector) + ") for: " + jmxUrl); return connector; } private static String address(Object obj) { return "@" + Integer.toHexString(obj.hashCode()); } private static String mask(String val) { return val.replaceAll(".", "*"); } private static String diff(String old, String cur) { return "'" + old + "'->'" + cur + "'"; } public static MBeanServerConnection getMBeanServer(Properties config) throws MalformedURLException, IOException { String jmxUrl = config.getProperty(MxUtil.PROP_JMX_URL); String jmxUsername = config.getProperty(MxUtil.PROP_JMX_USERNAME, ""); String jmxPassword = config.getProperty(MxUtil.PROP_JMX_PASSWORD, ""); boolean isCached = false; JMXConnector connector; ConnectorInstance instance = (ConnectorInstance)cache.get(jmxUrl); if (instance != null) { connector = instance.connector; String username = instance.username; String password = instance.password; boolean usernameChanged = !username.equals(jmxUsername); boolean passwordChanged = !password.equals(jmxPassword); if (usernameChanged || passwordChanged) { if (log.isDebugEnabled()) { String diff = ""; if (usernameChanged) { diff += "user:" + diff(username, jmxUsername); } if (passwordChanged) { if (diff.length() != 0) { diff += ","; } diff += "pass:" + diff(mask(password), mask(jmxPassword)); } log.debug("Credentials changed (" + diff + ") reconnecting cached connection for: " + jmxUrl); } connector = connect(config, jmxUrl, jmxUsername, jmxPassword); } else { isCached = true; } } else { connector = connect(config, jmxUrl, jmxUsername, jmxPassword); log.debug("Caching connector for: " + jmxUrl); } try { return connector.getMBeanServerConnection(); } catch (IOException e) { if (isCached) { log.debug("Reconnecting cached connection for: " + jmxUrl); connector = connect(config, jmxUrl, jmxUsername, jmxPassword); return connector.getMBeanServerConnection(); } else { throw e; } } } public static Object getValue(Properties config, String objectName, String attribute) throws MalformedURLException, MalformedObjectNameException, IOException, MBeanException, AttributeNotFoundException, InstanceNotFoundException, ReflectionException, PluginException { ObjectName objName = new ObjectName(objectName); JMXConnector connector = null; try { connector = getCachedMBeanConnector(config); if (attribute.startsWith(STATS_PREFIX)) { return getJSR77Statistic(connector.getMBeanServerConnection(), objName, attribute); } else if (attribute.startsWith(COMPOSITE_PREFIX)) { return getCompositeMetric(connector.getMBeanServerConnection(), objName, attribute); } else { return connector.getMBeanServerConnection().getAttribute(objName, attribute); } } finally { close(connector); } } private static Object setAttribute(MBeanServerConnection mServer, ObjectName obj, String name, Object value) throws MetricUnreachableException, MetricNotFoundException, PluginException, ReflectionException, InstanceNotFoundException, MBeanException, IOException { if (name.startsWith("set")) { name = name.substring(3); } Attribute attr = new Attribute(name, value); try { mServer.setAttribute(obj, attr); } catch (AttributeNotFoundException e) { throw new MetricNotFoundException(e.getMessage(), e); } catch (InvalidAttributeValueException e) { throw new ReflectionException(e); } return null; } private static Object getAttribute(MBeanServerConnection mServer, ObjectName obj, String name) throws MetricUnreachableException, MetricNotFoundException, PluginException, ReflectionException, InstanceNotFoundException, MBeanException, IOException { if (name.startsWith("get")) { name = name.substring(3); } try { return mServer.getAttribute(obj, name); } catch (AttributeNotFoundException e) { throw new MetricNotFoundException(e.getMessage(), e); } } public static Object invoke(Properties config, String objectName, String method, Object[] args, String[] sig) throws MetricUnreachableException, MetricNotFoundException, PluginException { JMXConnector connector = null; try { connector = getMBeanConnector(config); MBeanServerConnection mServer = connector.getMBeanServerConnection(); ObjectName obj = new ObjectName(objectName); MBeanInfo info = mServer.getMBeanInfo(obj); if (sig.length == 0) { MBeanUtil.OperationParams params = MBeanUtil.getOperationParams(info, method, args); if (params.isAttribute) { if (method.startsWith("set")) { return setAttribute(mServer, obj, method, params.arguments[0]); } else { return getAttribute(mServer, obj, method); } } sig = params.signature; args = params.arguments; } return mServer.invoke(obj, method, args, sig); } catch (RemoteException e) { throw unreachable(config, e); } catch (MalformedObjectNameException e) { throw invalidObjectName(objectName, e); } catch (InstanceNotFoundException e) { throw objectNotFound(objectName, e); } catch (ReflectionException e) { throw error(objectName, e, method); } catch (IntrospectionException e) { throw error(objectName, e, method); } catch (MBeanException e) { throw error(objectName, e, method); } catch (IOException e) { throw error(objectName, e, method); } finally { close(connector, objectName, method); } } public static void close(JMXConnector connector) { if (connector != null) { try { connector.close(); } catch (IOException e) { log.error("error closing connector: " + e, e); } } } public static void close(JMXConnector connector, String objectName, String method) { if (connector != null) { try { connector.close(); } catch (IOException e) { log.error("error closing connector " + e + ". objectName=" + objectName + "method=" + method, e); } } } private static class ConnectorInstance { JMXConnector connector; String username; String password; ConnectorInstance(JMXConnector connector, String username, String password) { this.connector = connector; this.username = username; this.password = password; } } }