/*
* 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;
}
}
}