001/*
002 * #%L
003 * Netarchivesuite - common
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023
024package dk.netarkivet.common.utils;
025
026import java.io.IOException;
027import java.net.MalformedURLException;
028import java.net.SocketTimeoutException;
029import java.util.Arrays;
030import java.util.HashMap;
031import java.util.Map;
032
033import javax.management.AttributeNotFoundException;
034import javax.management.InstanceNotFoundException;
035import javax.management.MBeanException;
036import javax.management.MBeanServerConnection;
037import javax.management.MalformedObjectNameException;
038import javax.management.ObjectName;
039import javax.management.ReflectionException;
040import javax.management.openmbean.CompositeData;
041import javax.management.openmbean.TabularData;
042import javax.management.remote.JMXConnector;
043import javax.management.remote.JMXConnectorFactory;
044import javax.management.remote.JMXServiceURL;
045import javax.naming.ServiceUnavailableException;
046
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050import com.sun.jndi.rmi.registry.RegistryContextFactory;
051
052import dk.netarkivet.common.CommonSettings;
053import dk.netarkivet.common.exceptions.ArgumentNotValid;
054import dk.netarkivet.common.exceptions.IOFailure;
055import dk.netarkivet.common.exceptions.UnknownID;
056
057/**
058 * Various JMX-related utility functions. FIXME: Use generic RegistryContextFactory instead of Oracle specific.
059 */
060@SuppressWarnings("restriction")
061public final class JMXUtils {
062
063    /** The logger. */
064    public static final Logger log = LoggerFactory.getLogger(JMXUtils.class);
065
066    /**
067     * The system property that Java uses to get an initial context for JNDI. This must be set for RMI connections to
068     * work.
069     */
070    private static final String JNDI_INITIAL_CONTEXT_PROPERTY = "java.naming.factory.initial";
071
072    /** seconds per milliseconds as a double figure. */
073    private static final double DOUBLE_SECONDS_IN_MILLIS = TimeUtils.SECOND_IN_MILLIS * 1.0;
074
075    /** The JMX timeout in seconds. */
076    private static final long timeoutInseconds = Settings.getLong(CommonSettings.JMX_TIMEOUT);
077
078    /** Private constructor to prevent instantiation. */
079    private JMXUtils() {
080    }
081
082    /**
083     * The maximum number of times we back off on getting an mbean or a job. The cumulative time trying is 2^(MAX_TRIES)
084     * milliseconds, thus the constant is defined as log_2(TIMEOUT), as set in settings.
085     *
086     * @return The number of tries
087     */
088    public static int getMaxTries() {
089        return (int) Math.ceil(Math.log((double) timeoutInseconds * DOUBLE_SECONDS_IN_MILLIS) / Math.log(2.0));
090    }
091
092    /**
093     * @return the JMX timeout in milliseconds.
094     */
095    public static long getJmxTimeout() {
096        return TimeUtils.SECOND_IN_MILLIS * timeoutInseconds;
097    }
098
099    /**
100     * If no initial JNDI context has been configured, configures the system to use Sun's standard one. This is
101     * necessary for RMI connections to work.
102     */
103    private static void ensureJndiInitialContext() {
104        if (System.getProperty(JNDI_INITIAL_CONTEXT_PROPERTY) == null) {
105            System.setProperty(JNDI_INITIAL_CONTEXT_PROPERTY, RegistryContextFactory.class.getCanonicalName());
106            log.info("Set property '{}' to: {}", JNDI_INITIAL_CONTEXT_PROPERTY,
107                    RegistryContextFactory.class.getCanonicalName());
108        } else {
109            log.debug("Property '{}' is set to: {}", JNDI_INITIAL_CONTEXT_PROPERTY,
110                    System.getProperty(JNDI_INITIAL_CONTEXT_PROPERTY));
111        }
112    }
113
114    /**
115     * Constructs the same service URL that JConsole does on the basis of a server name, a JMX port number, and a RMI
116     * port number.
117     * <p>
118     * Example URL: service:jmx:rmi://0.0.0.0:9999/jndi/rmi://0.0.0.0:1099/JMXConnector where RMI port number = 9999,
119     * JMX port number = 1099 server = 0.0.0.0 a.k.a localhost(?).
120     *
121     * @param server The server that should be connected to using the constructed URL.
122     * @param jmxPort The number of the JMX port that should be connected to using the constructed URL (may not be a
123     * negative number)
124     * @param rmiPort The number of the RMI port that should be connected to using the constructed URL, or -1 if the
125     * default RMI port should be used.
126     * @return the constructed URL.
127     */
128    public static JMXServiceURL getUrl(String server, int jmxPort, int rmiPort) {
129        ArgumentNotValid.checkNotNullOrEmpty(server, "String server");
130        ArgumentNotValid.checkNotNegative(jmxPort, "int jmxPort");
131
132        String url;
133        if (rmiPort != -1) {
134            url = "service:jmx:rmi://" + server + ":" + rmiPort + "/jndi/rmi://" + server + ":" + jmxPort + "/jmxrmi";
135        } else {
136            url = "service:jmx:rmi:///jndi/rmi://" + server + ":" + jmxPort + "/jmxrmi";
137        }
138        log.trace("Created url for JMX-connections: {}", url);
139        try {
140            return new JMXServiceURL(url);
141        } catch (MalformedURLException e) {
142            throw new UnknownID("Could not create new JMXServiceURL from " + url, e);
143        }
144    }
145
146    /**
147     * Returns a connection to a remote MbeanServer defined by the given arguments.
148     *
149     * @param server the remote servername
150     * @param jmxPort the remote jmx-port
151     * @param rmiPort the remote rmi-port
152     * @param userName the username
153     * @param password the password
154     * @return a MBeanServerConnection to a remote MbeanServer defined by the given arguments.
155     */
156    public static MBeanServerConnection getMBeanServerConnection(String server, int jmxPort, int rmiPort,
157            String userName, String password) {
158        ArgumentNotValid.checkNotNullOrEmpty(server, "String server");
159        ArgumentNotValid.checkNotNegative(jmxPort, "int jmxPort");
160        ArgumentNotValid.checkNotNegative(rmiPort, "int rmiPort");
161        ArgumentNotValid.checkNotNullOrEmpty(userName, "String userName");
162        ArgumentNotValid.checkNotNullOrEmpty(password, "String password");
163        String logMsgSuffix = "a connection to server '" + server + "' on jmxport/rmiport=" + jmxPort + "/" + rmiPort
164                + " using username=" + userName;
165        log.debug("Establishing {}", logMsgSuffix);
166        JMXServiceURL jmxServiceUrl = getUrl(server, jmxPort, rmiPort);
167        Map<String, String[]> credentials = packageCredentials(userName, password);
168        MBeanServerConnection connection = getMBeanServerConnection(jmxServiceUrl, credentials);
169        log.debug("Established successfully {}", logMsgSuffix);
170        return connection;
171    }
172
173    /**
174     * Connects to the given (url-specified) service point, sending the given credentials as login.
175     *
176     * @param url The JMX service url of some JVM on some machine.
177     * @param credentials a map with (at least) one entry, mapping "jmx.remote.credentials" to a String array of length
178     * 2. Its first item should be the user name. Its second item should be the password.
179     * @return An MBeanServerConnection representing the connected session.
180     */
181    public static MBeanServerConnection getMBeanServerConnection(JMXServiceURL url, Map<String, String[]> credentials) {
182        ArgumentNotValid.checkNotNull(url, "JMXServiceURL url");
183        ArgumentNotValid.checkNotNull(credentials, "Map<String,String[]> credentials");
184        try {
185            ensureJndiInitialContext();
186            return JMXConnectorFactory.connect(url, credentials).getMBeanServerConnection();
187        } catch (IOException e) {
188            throw new IOFailure("Could not connect to " + url.toString(), e);
189        }
190    }
191
192    /**
193     * Packages credentials as an environment for JMX connections. This packaging has the same form that JConsole uses:
194     * a one-entry Map, the mapping of "jmx.remote.credentials" being an array containing the user name and the
195     * password.
196     *
197     * @param userName The user to login as
198     * @param password The password to use for that user
199     * @return the packaged credentials
200     */
201    public static Map<String, String[]> packageCredentials(String userName, String password) {
202        ArgumentNotValid.checkNotNullOrEmpty(userName, "String userName");
203        ArgumentNotValid.checkNotNullOrEmpty(password, "String password");
204        Map<String, String[]> credentials = new HashMap<String, String[]>(1);
205        credentials.put("jmx.remote.credentials", new String[] {userName, password});
206        return credentials;
207    }
208
209    /**
210     * Execute a command on a bean.
211     *
212     * @param connection Connection to the server holding the bean.
213     * @param beanName Name of the bean.
214     * @param command Command to execute.
215     * @param arguments Arguments to the command. Only string arguments are possible at the moment.
216     * @return The return value of the executed command.
217     */
218    public static Object executeCommand(MBeanServerConnection connection, String beanName, String command,
219            String... arguments) {
220        ArgumentNotValid.checkNotNull(connection, "MBeanServerConnection connection");
221        ArgumentNotValid.checkNotNullOrEmpty(beanName, "String beanName");
222        ArgumentNotValid.checkNotNullOrEmpty(command, "String command");
223        ArgumentNotValid.checkNotNull(arguments, "String... arguments");
224
225        if (log.isDebugEnabled()) {
226            log.debug("Preparing to execute {} with args {} on {}", command, Arrays.toString(arguments), beanName);
227        }
228        final int maxJmxRetries = getMaxTries();
229        try {
230            final String[] signature = new String[arguments.length];
231            Arrays.fill(signature, String.class.getName());
232            // The first time we attempt to connect to an mbean, we might have
233            // to wait a bit for it to appear
234            Throwable lastException;
235            int tries = 0;
236            do {
237                ++tries;
238                try {
239                    Object ret = connection.invoke(getBeanName(beanName), command, arguments, signature);
240                    log.debug("Executed command {} returned {}", command, ret);
241                    return ret;
242                } catch (InstanceNotFoundException e) {
243                    lastException = e;
244                    if (tries < maxJmxRetries) {
245                        TimeUtils.exponentialBackoffSleep(tries);
246                    }
247                } catch (IOException e) {
248                    log.warn("Exception thrown while executing {} with args {} on {}", command,
249                            Arrays.toString(arguments), beanName, e);
250                    lastException = e;
251                    if (tries < maxJmxRetries) {
252                        TimeUtils.exponentialBackoffSleep(tries);
253                    }
254                }
255            } while (tries < maxJmxRetries);
256            throw new IOFailure("Failed to find MBean " + beanName + " for executing " + command + " after " + tries
257                    + " attempts", lastException);
258        } catch (MBeanException e) {
259            throw new IOFailure("MBean exception for " + beanName, e);
260        } catch (ReflectionException e) {
261            throw new IOFailure("Reflection exception for " + beanName, e);
262        }
263    }
264
265    /**
266     * Get the value of an attribute from a bean.
267     *
268     * @param beanName Name of the bean to get an attribute for.
269     * @param attribute Name of the attribute to get.
270     * @param connection A connection to the JMX server for the bean.
271     * @return Value of the attribute.
272     */
273    public static Object getAttribute(String beanName, String attribute, MBeanServerConnection connection) {
274        ArgumentNotValid.checkNotNullOrEmpty(beanName, "String beanName");
275        ArgumentNotValid.checkNotNullOrEmpty(attribute, "String attribute");
276        ArgumentNotValid.checkNotNull(connection, "MBeanServerConnection connection");
277
278        log.debug("Preparing to get attribute {} on {}", attribute, beanName);
279        final int maxJmxRetries = getMaxTries();
280        try {
281            // The first time we attempt to connect to an mbean, we might have
282            // to wait a bit for it to appear
283            Throwable lastException;
284            int tries = 0;
285            do {
286                ++tries;
287                try {
288                    Object ret = connection.getAttribute(getBeanName(beanName), attribute);
289                    log.debug("Getting attribute {} returned {}", attribute, ret);
290                    return ret;
291                } catch (InstanceNotFoundException e) {
292                    log.trace("Error while getting attribute {} on {}", attribute, beanName, e);
293                    lastException = e;
294                    if (tries < maxJmxRetries) {
295                        TimeUtils.exponentialBackoffSleep(tries);
296                    }
297                } catch (IOException e) {
298                    log.trace("Error while getting attribute {} on {}", attribute, beanName, e);
299                    lastException = e;
300                    if (tries < maxJmxRetries) {
301                        TimeUtils.exponentialBackoffSleep(tries);
302                    }
303                }
304            } while (tries < maxJmxRetries);
305            throw new IOFailure("Failed to find MBean " + beanName + " for getting attribute " + attribute + " after "
306                    + tries + " attempts", lastException);
307        } catch (AttributeNotFoundException e) {
308            throw new IOFailure("MBean exception for " + beanName, e);
309        } catch (MBeanException e) {
310            throw new IOFailure("MBean exception for " + beanName, e);
311        } catch (ReflectionException e) {
312            throw new IOFailure("Reflection exception for " + beanName, e);
313        }
314    }
315
316    /**
317     * Get a bean name from a string version.
318     *
319     * @param beanName String representation of bean name
320     * @return Object representing that bean name.
321     */
322    public static ObjectName getBeanName(String beanName) {
323        ArgumentNotValid.checkNotNullOrEmpty(beanName, "String beanName");
324        try {
325            return new ObjectName(beanName);
326        } catch (MalformedObjectNameException e) {
327            throw new ArgumentNotValid("Name " + beanName + " is not a valid " + "object name", e);
328        }
329    }
330
331    /**
332     * Get a JMXConnector to a given host and port, using login and password.
333     *
334     * @param hostName The host to attempt to connect to.
335     * @param jmxPort The port on the host to connect to (a non-negative number).
336     * @param login The login name to authenticate as (typically "controlRole" or "monitorRole".
337     * @param password The password for JMX access.
338     * @return A JMX connector to the given host and port, using default RMI.
339     * @throws IOFailure if connecting to JMX fails.
340     */
341    public static JMXConnector getJMXConnector(String hostName, int jmxPort, final String login, final String password) {
342        ArgumentNotValid.checkNotNullOrEmpty(hostName, "String hostName");
343        ArgumentNotValid.checkNotNegative(jmxPort, "int jmxPort");
344        ArgumentNotValid.checkNotNullOrEmpty(login, "String login");
345        ArgumentNotValid.checkNotNullOrEmpty(password, "String password");
346
347        JMXServiceURL rmiurl = getUrl(hostName, jmxPort, -1);
348        Map<String, ?> environment = packageCredentials(login, password);
349        Throwable lastException;
350        int retries = 0;
351        final int maxJmxRetries = getMaxTries();
352        do {
353            try {
354                return JMXConnectorFactory.connect(rmiurl, environment);
355            } catch (IOException e) {
356                lastException = e;
357                if (retries < maxJmxRetries
358                        && e.getCause() != null
359                        && (e.getCause() instanceof ServiceUnavailableException || e.getCause() instanceof SocketTimeoutException)) {
360                    // Sleep a bit before trying again
361                    TimeUtils.exponentialBackoffSleep(retries);
362                    /*
363                     * called exponentialBackoffSleep(retries) which used Calendar.MILLISECOND as time unit, which means
364                     * we only wait an exponential number of milliseconds.
365                     */
366                    continue;
367                }
368                break;
369            }
370        } while (retries++ < maxJmxRetries);
371        throw new IOFailure("Failed to connect to URL " + rmiurl + " after " + retries + " of " + maxJmxRetries
372                + " attempts.\nException type: " + lastException.getCause().getClass().getName(), lastException);
373    }
374
375    /**
376     * Get a single CompositeData object out of a TabularData structure.
377     *
378     * @param items TabularData structure as returned from JMX calls.
379     * @return The one item in the items structure.
380     * @throws ArgumentNotValid if there is not exactly one item in items, or items is null.
381     */
382    public static CompositeData getOneCompositeData(TabularData items) {
383        ArgumentNotValid.checkNotNull(items, "TabularData items");
384        ArgumentNotValid.checkTrue(items.size() == 1, "TabularData items should have 1 item");
385        return (CompositeData) items.values().toArray()[0];
386    }
387
388    /**
389     * Execute a single command, closing the connector afterwards. If you wish to hold on to the connector, call
390     * JMXUtils#executeCommand(MBeanServerConnection, String, String, String[])
391     *
392     * @param connector A one-shot connector object.
393     * @param beanName The name of the bean to execute a command on.
394     * @param command The command to execute.
395     * @param arguments The arguments to the command (all strings)
396     * @return Whatever the command returned.
397     */
398    public static Object executeCommand(JMXConnector connector, String beanName, String command, String... arguments) {
399        ArgumentNotValid.checkNotNull(connector, "JMXConnector connector");
400        ArgumentNotValid.checkNotNullOrEmpty(beanName, "String beanName");
401        ArgumentNotValid.checkNotNullOrEmpty(command, "String command");
402        ArgumentNotValid.checkNotNull(arguments, "String... arguments");
403
404        MBeanServerConnection connection;
405        try {
406            connection = connector.getMBeanServerConnection();
407        } catch (IOException e) {
408            throw new IOFailure("Failure getting JMX connection", e);
409        }
410        try {
411            return executeCommand(connection, beanName, command, arguments);
412        } finally {
413            try {
414                connector.close();
415            } catch (IOException e) {
416                log.warn("Couldn't close connection to {}", beanName, e);
417            }
418        }
419    }
420
421    /**
422     * Get the value of an attribute, closing the connector afterwards. If you wish to hold on to the connector, call
423     * JMXUtils#executeCommand(MBeanServerConnection, String, String, String[])
424     *
425     * @param connector A one-shot connector object.
426     * @param beanName The name of the bean to get an attribute from.
427     * @param attribute The attribute to get.
428     * @return Whatever the command returned.
429     */
430    public static Object getAttribute(JMXConnector connector, String beanName, String attribute) {
431        ArgumentNotValid.checkNotNull(connector, "JMXConnector connector");
432        ArgumentNotValid.checkNotNullOrEmpty(beanName, "String beanName");
433        ArgumentNotValid.checkNotNullOrEmpty(attribute, "String attribute");
434
435        MBeanServerConnection connection;
436        try {
437            connection = connector.getMBeanServerConnection();
438        } catch (IOException e) {
439            throw new IOFailure("Failure getting JMX connection", e);
440        }
441        try {
442            return getAttribute(beanName, attribute, connection);
443        } finally {
444            try {
445                connector.close();
446            } catch (IOException e) {
447                log.warn("Couldn't close connection to {}", beanName, e);
448            }
449        }
450    }
451
452}