001/*
002 * #%L
003 * Netarchivesuite - monitor
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.monitor.jmx;
025
026import java.lang.reflect.InvocationHandler;
027import java.lang.reflect.Method;
028import java.lang.reflect.Proxy;
029import java.util.ArrayList;
030import java.util.Date;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.Hashtable;
034import java.util.List;
035import java.util.Map;
036import java.util.Set;
037
038import javax.management.MBeanServer;
039import javax.management.ObjectName;
040
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043
044import dk.netarkivet.common.distribute.monitorregistry.HostEntry;
045import dk.netarkivet.common.exceptions.ArgumentNotValid;
046import dk.netarkivet.common.exceptions.IOFailure;
047import dk.netarkivet.common.management.SingleMBeanObject;
048import dk.netarkivet.common.utils.ExceptionUtils;
049import dk.netarkivet.common.utils.Settings;
050import dk.netarkivet.common.utils.StringUtils;
051import dk.netarkivet.monitor.MonitorSettings;
052import dk.netarkivet.monitor.registry.MonitorRegistry;
053
054/**
055 * Handles the forwarding of other hosts' MBeans matching a specific regular query and interface to a given mbean
056 * server. The interface should be of type T.
057 *
058 * @param <T> The type of object exposed through the MBeans.
059 */
060@SuppressWarnings({"unchecked", "rawtypes"})
061public class HostForwarding<T> {
062
063    /** The log. */
064    public static final Logger log = LoggerFactory.getLogger(HostForwarding.class);
065
066    /** List of all known and established JMX connections for this object. */
067    private Map<String, Set<HostEntry>> knownJmxConnections = new HashMap<String, Set<HostEntry>>();
068
069    /** The query to the MBeanserver to get the MBeans. */
070    private final String mBeanQuery;
071    /** The MBean server we register the forwarded mbeans in. */
072    private final MBeanServer mBeanServer;
073    /** The interface the remote mbeans should implement. */
074    private final Class<T> asInterface;
075
076    /**
077     * The username for JMX read from either a System property, the overriding settings given by the installer, or the
078     * default value stored in src/dk/netarkivet/monitor/settings.xml.
079     */
080    private String jmxUsername;
081
082    /**
083     * @return the JMX-Username
084     */
085    private String getJmxUsername() {
086        return jmxUsername;
087    }
088
089    /**
090     * Set the JMX-username with a new value. Null or empty username is not allowed.
091     *
092     * @param newJmxUsername New value for the JMX-username
093     */
094    private synchronized void setJmxUsername(String newJmxUsername) {
095        ArgumentNotValid.checkNotNullOrEmpty(newJmxUsername, "String newJmxUsername");
096        this.jmxUsername = newJmxUsername;
097    }
098
099    /**
100     * The password for JMX read from either a System property, the overriding settings given by the installer, or the
101     * default value stored in src/dk/netarkivet/monitor/settings.xml.
102     */
103    private String jmxPassword;
104
105    /**
106     * @return the JMX-password
107     */
108    private String getJmxPassword() {
109        return jmxPassword;
110    }
111
112    /**
113     * Set the JMX-password with a new value. Null or empty password is not allowed.
114     *
115     * @param newJmxPassword New value for the JMX-password
116     */
117    private synchronized void setJmxPassword(String newJmxPassword) {
118        ArgumentNotValid.checkNotNullOrEmpty(newJmxPassword, "String newJmxPassword");
119        this.jmxPassword = newJmxPassword;
120    }
121
122    /**
123     * The instances of host forwardings, to ensure mbeans are only forwarded once.
124     */
125    private static Map<String, HostForwarding> instances = new HashMap<String, HostForwarding>();
126
127    /** The factory used for producing connections to remote mbean servers. */
128    private final JMXProxyConnectionFactory connectionFactory;
129
130    /**
131     * Initialise forwarding MBeans. This will connect to all hosts mentioned in settings, and register proxy beans for
132     * each bean on remote servers matching the given query. The remote beans should implement the given interface.
133     *
134     * @param asInterface The interface remote beans should implement.
135     * @param mBeanServer The MBean server the proxy beans should be registered in.
136     * @param mBeanQuery The query that returns the mbeans that should be proxied.
137     */
138    private HostForwarding(Class<T> asInterface, MBeanServer mBeanServer, String mBeanQuery) {
139        this.mBeanServer = mBeanServer;
140        this.asInterface = asInterface;
141        this.mBeanQuery = mBeanQuery;
142        this.connectionFactory = new CachingProxyConnectionFactory(new RmiProxyConnectionFactory());
143
144        updateJmx();
145    }
146
147    /**
148     * Get a host forwarding instance. As a side effect of this, all mbeans matching a query from remote hosts, are
149     * proxied and registered in the given mbean server. Only one HostForwarding instance will be made for each query
150     * string. Any subsequent call with the same query string will simply return the previously initiated instance.
151     *
152     * @param asInterface The interface remote mbeans should implement.
153     * @param mBeanServer The MBean server to register proxy mbeans in.
154     * @param query The query for which we should proxy matching mbeans on remote servers.
155     * @param <T> The type of HostForwarding to return.
156     * @return This host forwarding instance.
157     */
158    public static synchronized <T> HostForwarding getInstance(Class<T> asInterface, MBeanServer mBeanServer,
159            String query) {
160        if (instances.get(query) == null) {
161            instances.put(query, new HostForwarding<T>(asInterface, mBeanServer, query));
162        }
163        HostForwarding hf = instances.get(query);
164        hf.updateJmx();
165        return hf;
166    }
167
168    /**
169     * Gets the list of hosts and corresponding JMX ports from the monitor registry. For all unknown hosts, it registers
170     * proxies to all Mbeans registered on the remote MBeanservers in the given MBeanserver.
171     */
172    private synchronized void updateJmx() {
173        // Update username/password setting, if either of the settings
174        // MonitorSettings.JMX_PASSWORD_SETTING,
175        // MonitorSettings.JMX_USERNAME_SETTING have changed.
176
177        boolean changed = updateJmxUsernameAndPassword();
178        if (changed) {
179            log.info(
180                    "Settings '{}' and '{}' has been updated with value from a System property or one of the files: {}",
181                    MonitorSettings.JMX_USERNAME_SETTING, MonitorSettings.JMX_PASSWORD_SETTING,
182                    StringUtils.conjoin(",", Settings.getSettingsFiles()));
183        }
184
185        List<HostEntry> newJmxHosts = new ArrayList<HostEntry>();
186        for (Map.Entry<String, Set<HostEntry>> entries : getCurrentHostEntries().entrySet()) {
187            String host = entries.getKey();
188            // Take a copy of the host entries, to avoid concurrent
189            // modifications.
190            Set<HostEntry> hostEntries = new HashSet<HostEntry>(entries.getValue());
191            if (knownJmxConnections.containsKey(host)) {
192                Set<HostEntry> registeredJmxPortsOnHost = knownJmxConnections.get(host);
193                for (HostEntry he : hostEntries) {
194                    if (!registeredJmxPortsOnHost.contains(he)) {
195                        log.debug("Adding new jmx host '{}'", he);
196                        newJmxHosts.add(he);
197                        registeredJmxPortsOnHost.add(he);
198                    } else {
199                        log.trace("Updating last seen time for jmx host '{}'", he);
200                        for (HostEntry existing : registeredJmxPortsOnHost) {
201                            if (existing.equals(he)) {
202                                existing.setTime(he.getTime());
203                            }
204                        }
205                    }
206                }
207                knownJmxConnections.put(host, registeredJmxPortsOnHost);
208            } else {
209                log.debug("Adding new jmx hosts '{}'", hostEntries);
210                newJmxHosts.addAll(hostEntries);
211                knownJmxConnections.put(host, new HashSet<HostEntry>(hostEntries));
212            }
213        }
214        if (newJmxHosts.size() > 0) {
215            log.info("Found {} new JMX hosts", newJmxHosts.size());
216            registerRemoteMbeans(newJmxHosts);
217        }
218    }
219
220    /**
221     * Update JMX username and password.
222     *
223     * @return true if the username and/or the password were changed.
224     */
225    private synchronized boolean updateJmxUsernameAndPassword() {
226        boolean changed = false;
227
228        String newJmxUsername = Settings.get(MonitorSettings.JMX_USERNAME_SETTING);
229
230        String newJmxPassword = Settings.get(MonitorSettings.JMX_PASSWORD_SETTING);
231
232        if (jmxUsername == null || !jmxUsername.equals(newJmxUsername)) {
233            setJmxUsername(newJmxUsername);
234            changed = true;
235        }
236
237        if (jmxPassword == null || !jmxPassword.equals(newJmxPassword)) {
238            setJmxPassword(newJmxPassword);
239            changed = true;
240        }
241
242        return changed;
243    }
244
245    /**
246     * Get current list of host-JMX port mappings. This lists the mappings from the registry server.
247     *
248     * @return current list of host-JMX port mappings.
249     */
250    public static Map<String, Set<HostEntry>> getCurrentHostEntries() {
251        return MonitorRegistry.getInstance().getHostEntries();
252    }
253
254    /**
255     * Register all remote Mbeans on the given MBeanServer. The username, and password are the same for all
256     * JMX-connections. For hosts which cannot be connected to, an mbean is registered in the same domain, which tries
257     * to reconnect on any invocation, and returns the status of the attempt as a string.
258     *
259     * @param hosts the list of remote Hosts.
260     */
261    private void registerRemoteMbeans(List<HostEntry> hosts) {
262        for (HostEntry hostEntry : hosts) {
263            log.debug("Forwarding mbeans '{}' for host: {}", this.mBeanQuery, hostEntry);
264            try {
265                createProxyMBeansForHost(hostEntry);
266            } catch (Exception e) {
267                log.warn("Failure connecting to remote JMX MBeanserver ({})", hostEntry, e);
268                try {
269                    // This creates a proxy object that calls the handler on any
270                    // invocation of any method on the object.
271                    NoHostInvocationHandler handler = new NoHostInvocationHandler(hostEntry);
272                    Class<T> proxyClass = (Class<T>) Proxy.getProxyClass(asInterface.getClassLoader(),
273                            new Class[] {asInterface});
274                    T noHostMBean = proxyClass.getConstructor(InvocationHandler.class).newInstance(handler);
275                    SingleMBeanObject<T> singleMBeanObject = new SingleMBeanObject<T>(queryToDomain(mBeanQuery),
276                            noHostMBean, asInterface, mBeanServer);
277                    Hashtable<String, String> names = singleMBeanObject.getNameProperties();
278                    names.put("name", "error_host_" + hostEntry.getName() + "_" + hostEntry.getJmxPort());
279                    names.put("index", Integer.toString(0));
280                    names.put("hostname", hostEntry.getName());
281                    handler.setSingleMBeanObject(singleMBeanObject);
282                    singleMBeanObject.register();
283                } catch (Exception e1) {
284                    log.warn("Failure registering error mbean for hostentry: {}", hostEntry, e1);
285                }
286            }
287        }
288    }
289
290    /**
291     * Connects to the given host, and lists all mbeans matching the query. For each of these mbeans, registers a
292     * proxymbean, that on any invocation will connect to the remote host, and return the result of invoking the method
293     * on the remote object.
294     *
295     * @param hostEntry The host to connect to.
296     * @throws IOFailure if remote host cannot be connected to.
297     */
298    private synchronized void createProxyMBeansForHost(HostEntry hostEntry) {
299        Set<ObjectName> remoteObjectNames;
300        JMXProxyConnection connection = connectionFactory.getConnection(hostEntry.getName(), hostEntry.getJmxPort(),
301                hostEntry.getRmiPort(), getJmxUsername(), getJmxPassword());
302
303        remoteObjectNames = connection.query(mBeanQuery);
304        for (ObjectName name : remoteObjectNames) {
305            try {
306                // This creates a proxy object that calls the handler on any
307                // invocation of any method on the object.
308                ProxyMBeanInvocationHandler handler = new ProxyMBeanInvocationHandler(name, hostEntry);
309                Class<T> proxyClass = (Class<T>) Proxy.getProxyClass(asInterface.getClassLoader(),
310                        new Class[] {asInterface});
311                T mbean = proxyClass.getConstructor(InvocationHandler.class).newInstance(handler);
312
313                SingleMBeanObject<T> singleMBeanObject = new SingleMBeanObject<T>(name, mbean, asInterface, mBeanServer);
314                singleMBeanObject.register();
315            } catch (Exception e) {
316                log.warn("Error registering mbean", e);
317            }
318        }
319    }
320
321    /**
322     * Returns the domain from a given query. Used for constructing an error-mbean-name on connection trouble.
323     *
324     * @param aMBeanQuery The query to return the domain from.
325     * @return the domain from a given query.
326     */
327    private String queryToDomain(String aMBeanQuery) {
328        return aMBeanQuery.replaceAll(":.*$", "");
329    }
330
331    /**
332     * Get the mbean server that proxies to remote mbeans are registered in.
333     *
334     * @return The mbean server with proxy mbeans.
335     */
336    public MBeanServer getMBeanServer() {
337        return mBeanServer;
338    }
339
340    /**
341     * An invocation handler for the mbeans registered when a host does not respond. This handler will on any invocation
342     * attempt to reconnect, and then return a string with the result. Unsuccessfully connecting, it will unregister the
343     * mbean.
344     */
345    private class NoHostInvocationHandler implements InvocationHandler {
346        /** The mbean this invocation handler handles. */
347        private SingleMBeanObject singleMBeanObject;
348        /** The host we should retry connecting to. */
349        private HostEntry hostEntry;
350
351        /**
352         * Make a new invocation handler for showing errors and retrying connect.
353         *
354         * @param hostEntry The host to retry connecting to.
355         */
356        public NoHostInvocationHandler(HostEntry hostEntry) {
357            this.hostEntry = hostEntry;
358        }
359
360        /**
361         * Remembers the mbean this invocation handler is registered in. Should always be called before actually
362         * registering the mbean.
363         *
364         * @param singleMBeanObject The mbean this object handles.
365         */
366        public void setSingleMBeanObject(SingleMBeanObject singleMBeanObject) {
367            this.singleMBeanObject = singleMBeanObject;
368        }
369
370        /**
371         * Retries connecting to the host. On success, returns a string with success, and unregisters. On failure,
372         * returns a string with failure.
373         *
374         * @param proxy The error mbean that invoked this, ignored.
375         * @param method The method attempted invoked, ignored.
376         * @param args The arguments for the method, ignored.
377         * @return A string with success or failure.
378         * @throws Throwable Shouldn't throw exceptions.
379         */
380        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
381            try {
382                createProxyMBeansForHost(hostEntry);
383                singleMBeanObject.unregister();
384                return "Now proxying JMX beans for host '" + hostEntry.getName() + ":" + hostEntry.getJmxPort() + "'";
385            } catch (Exception e) {
386                // Still unable to connect. Oh well.
387                return "[" + new Date() + "] Unable to proxy JMX beans on host '" + hostEntry.getName() + ":"
388                        + hostEntry.getJmxPort() + "', last seen active at '" + hostEntry.getTime() + "'\n"
389                        + ExceptionUtils.getStackTrace(e);
390            }
391        }
392    }
393
394    /** An invocation handler that forwards invocations to a remote mbean. */
395    private class ProxyMBeanInvocationHandler implements InvocationHandler {
396        /** The name of the remote mbean. */
397        private final ObjectName name;
398        /** The host for the remote mbean. */
399        private final HostEntry hostEntry;
400
401        /**
402         * Make a new forwarding mbean handler.
403         *
404         * @param name The name of the remote mbean.
405         * @param hostEntry The host for the remote mbean.
406         */
407        public ProxyMBeanInvocationHandler(ObjectName name, HostEntry hostEntry) {
408            this.name = name;
409            this.hostEntry = hostEntry;
410        }
411
412        /**
413         * Initialises a connection to a remote bean. Then invokes the method on that bean.
414         *
415         * @param proxy This proxying object. Ignored.
416         * @param method The method invoked. This is called on the remote mbean.
417         * @param args The arguments to the method. These are given to the remote mbean.
418         * @return Whatever the remote mbean returns.
419         * @throws IOFailure On trouble establishing the connection.
420         * @throws javax.management.RuntimeMBeanException On exceptions in the mbean invocations.
421         * @throws Throwable What ever the remote mbean has thrown.
422         */
423        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
424            // establish or reestablish mbean.
425            JMXProxyConnection connection;
426            try {
427                connection = connectionFactory.getConnection(hostEntry.getName(), hostEntry.getJmxPort(),
428                        hostEntry.getRmiPort(), getJmxUsername(), getJmxPassword());
429            } catch (Exception e) {
430                throw new IOFailure("Could not connect to host '" + hostEntry.getName() + ":" + hostEntry.getJmxPort()
431                        + "', last seen active at '" + hostEntry.getTime() + "'\n", e);
432            }
433            // call remote method.
434            T mBean = connection.createProxy(name, asInterface);
435            return method.invoke(mBean, args);
436        }
437    }
438
439}