001/*
002 * #%L
003 * Netarchivesuite - monitor
004 * %%
005 * Copyright (C) 2005 - 2018 The Royal Danish 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        log.info("Constructing a HostForwarding object for query {} on Mbeanserver {}", mBeanQuery, mBeanServer);
146    }
147
148    /**
149     * Get a host forwarding instance. As a side effect of this, all mbeans matching a query from remote hosts, are
150     * proxied and registered in the given mbean server. Only one HostForwarding instance will be made for each query
151     * string. Any subsequent call with the same query string will simply return the previously initiated instance.
152     *
153     * @param asInterface The interface remote mbeans should implement.
154     * @param mBeanServer The MBean server to register proxy mbeans in.
155     * @param query The query for which we should proxy matching mbeans on remote servers.
156     * @param <T> The type of HostForwarding to return.
157     * @return This host forwarding instance.
158     */
159    public static synchronized <T> HostForwarding getInstance(Class<T> asInterface, MBeanServer mBeanServer,
160            String query) {
161        if (instances.get(query) == null) {
162            instances.put(query, new HostForwarding<T>(asInterface, mBeanServer, query));
163        }
164        HostForwarding hf = instances.get(query);
165        hf.updateJmx();
166        return hf;
167    }
168
169    /**
170     * Gets the list of hosts and corresponding JMX ports from the monitor registry. For all unknown hosts, it registers
171     * proxies to all Mbeans registered on the remote MBeanservers in the given MBeanserver.
172     */
173    private synchronized void updateJmx() {
174        // Update username/password setting, if either of the settings
175        // MonitorSettings.JMX_PASSWORD_SETTING,
176        // MonitorSettings.JMX_USERNAME_SETTING have changed.
177
178        boolean changed = updateJmxUsernameAndPassword();
179        if (changed) {
180            log.info(
181                    "Settings '{}' and '{}' has been updated with value from a System property or one of the files: {}",
182                    MonitorSettings.JMX_USERNAME_SETTING, MonitorSettings.JMX_PASSWORD_SETTING,
183                    StringUtils.conjoin(",", Settings.getSettingsFiles()));
184        }
185
186        List<HostEntry> newJmxHosts = new ArrayList<HostEntry>();
187        for (Map.Entry<String, Set<HostEntry>> entries : getCurrentHostEntries().entrySet()) {
188            String host = entries.getKey();
189            // Take a copy of the host entries, to avoid concurrent
190            // modifications.
191            Set<HostEntry> hostEntries = new HashSet<HostEntry>(entries.getValue());
192            if (knownJmxConnections.containsKey(host)) {
193                Set<HostEntry> registeredJmxPortsOnHost = knownJmxConnections.get(host);
194                for (HostEntry he : hostEntries) {
195                    if (!registeredJmxPortsOnHost.contains(he)) {
196                        log.debug("Adding new jmx host '{}'", he);
197                        newJmxHosts.add(he);
198                        registeredJmxPortsOnHost.add(he);
199                    } else {
200                        log.trace("Updating last seen time for jmx host '{}'", he);
201                        for (HostEntry existing : registeredJmxPortsOnHost) {
202                            if (existing.equals(he)) {
203                                existing.setTime(he.getTime());
204                            }
205                        }
206                    }
207                }
208                knownJmxConnections.put(host, registeredJmxPortsOnHost);
209            } else {
210                log.debug("Adding new jmx hosts '{}'", hostEntries);
211                newJmxHosts.addAll(hostEntries);
212                knownJmxConnections.put(host, new HashSet<HostEntry>(hostEntries));
213            }
214        }
215        if (newJmxHosts.size() > 0) {
216            log.info("Found {} new JMX hosts", newJmxHosts.size());
217            registerRemoteMbeans(newJmxHosts);
218        }
219    }
220
221    /**
222     * Update JMX username and password.
223     *
224     * @return true if the username and/or the password were changed.
225     */
226    private synchronized boolean updateJmxUsernameAndPassword() {
227        boolean changed = false;
228
229        String newJmxUsername = Settings.get(MonitorSettings.JMX_USERNAME_SETTING);
230
231        String newJmxPassword = Settings.get(MonitorSettings.JMX_PASSWORD_SETTING);
232
233        if (jmxUsername == null || !jmxUsername.equals(newJmxUsername)) {
234            setJmxUsername(newJmxUsername);
235            changed = true;
236        }
237
238        if (jmxPassword == null || !jmxPassword.equals(newJmxPassword)) {
239            setJmxPassword(newJmxPassword);
240            changed = true;
241        }
242
243        return changed;
244    }
245
246    /**
247     * Get current list of host-JMX port mappings. This lists the mappings from the registry server.
248     *
249     * @return current list of host-JMX port mappings.
250     */
251    public static Map<String, Set<HostEntry>> getCurrentHostEntries() {
252        return MonitorRegistry.getInstance().getHostEntries();
253    }
254
255    /**
256     * Register all remote Mbeans on the given MBeanServer. The username, and password are the same for all
257     * JMX-connections. For hosts which cannot be connected to, an mbean is registered in the same domain, which tries
258     * to reconnect on any invocation, and returns the status of the attempt as a string.
259     *
260     * @param hosts the list of remote Hosts.
261     */
262    private void registerRemoteMbeans(List<HostEntry> hosts) {
263        for (HostEntry hostEntry : hosts) {
264            log.debug("Forwarding mbeans '{}' for host: {}", this.mBeanQuery, hostEntry);
265            try {
266                createProxyMBeansForHost(hostEntry);
267            } catch (Exception e) {
268                log.warn("Failure connecting to remote JMX MBeanserver ({}). Creating an error MBean", hostEntry, e);
269                try {
270                    // This creates a proxy object that calls the handler on any
271                    // invocation of any method on the object.
272                    NoHostInvocationHandler handler = new NoHostInvocationHandler(hostEntry);
273                    Class<T> proxyClass = (Class<T>) Proxy.getProxyClass(asInterface.getClassLoader(),
274                            new Class[] {asInterface});
275                    T noHostMBean = proxyClass.getConstructor(InvocationHandler.class).newInstance(handler);
276                    SingleMBeanObject<T> singleMBeanObject = new SingleMBeanObject<T>(queryToDomain(mBeanQuery),
277                            noHostMBean, asInterface, mBeanServer);
278                    Hashtable<String, String> names = singleMBeanObject.getNameProperties();
279                    names.put("name", "error_host_" + hostEntry.getName() + "_" + hostEntry.getJmxPort());
280                    names.put("index", Integer.toString(0));
281                    names.put("hostname", hostEntry.getName());
282                    handler.setSingleMBeanObject(singleMBeanObject);
283                    singleMBeanObject.register();
284                } catch (Exception e1) {
285                    log.warn("Failure registering error mbean for hostentry: {}", hostEntry, e1);
286                }
287            }
288        }
289    }
290
291    /**
292     * Connects to the given host, and lists all mbeans matching the query. For each of these mbeans, registers a
293     * proxymbean, that on any invocation will connect to the remote host, and return the result of invoking the method
294     * on the remote object.
295     *
296     * @param hostEntry The host to connect to.
297     * @throws IOFailure if remote host cannot be connected to.
298     */
299    private synchronized void createProxyMBeansForHost(HostEntry hostEntry) {
300        Set<ObjectName> remoteObjectNames;
301        JMXProxyConnection connection = connectionFactory.getConnection(hostEntry.getName(), hostEntry.getJmxPort(),
302                hostEntry.getRmiPort(), getJmxUsername(), getJmxPassword());
303
304        remoteObjectNames = connection.query(mBeanQuery);
305        for (ObjectName name : remoteObjectNames) {
306            try {
307                // This creates a proxy object that calls the handler on any
308                // invocation of any method on the object.
309                ProxyMBeanInvocationHandler handler = new ProxyMBeanInvocationHandler(name, hostEntry);
310                Class<T> proxyClass = (Class<T>) Proxy.getProxyClass(asInterface.getClassLoader(),
311                        new Class[] {asInterface});
312                T mbean = proxyClass.getConstructor(InvocationHandler.class).newInstance(handler);
313
314                SingleMBeanObject<T> singleMBeanObject = new SingleMBeanObject<T>(name, mbean, asInterface, mBeanServer);
315                singleMBeanObject.register();
316            } catch (Exception e) {
317                log.warn("Error registering mbean", e);
318            }
319        }
320    }
321
322    /**
323     * Returns the domain from a given query. Used for constructing an error-mbean-name on connection trouble.
324     *
325     * @param aMBeanQuery The query to return the domain from.
326     * @return the domain from a given query.
327     */
328    private String queryToDomain(String aMBeanQuery) {
329        return aMBeanQuery.replaceAll(":.*$", "");
330    }
331
332    /**
333     * Get the mbean server that proxies to remote mbeans are registered in.
334     *
335     * @return The mbean server with proxy mbeans.
336     */
337    public MBeanServer getMBeanServer() {
338        return mBeanServer;
339    }
340
341    /**
342     * An invocation handler for the mbeans registered when a host does not respond. This handler will on any invocation
343     * attempt to reconnect, and then return a string with the result. Unsuccessfully connecting, it will unregister the
344     * mbean.
345     */
346    private class NoHostInvocationHandler implements InvocationHandler {
347        /** The mbean this invocation handler handles. */
348        private SingleMBeanObject singleMBeanObject;
349        /** The host we should retry connecting to. */
350        private HostEntry hostEntry;
351
352        /**
353         * Make a new invocation handler for showing errors and retrying connect.
354         *
355         * @param hostEntry The host to retry connecting to.
356         */
357        public NoHostInvocationHandler(HostEntry hostEntry) {
358            this.hostEntry = hostEntry;
359        }
360
361        /**
362         * Remembers the mbean this invocation handler is registered in. Should always be called before actually
363         * registering the mbean.
364         *
365         * @param singleMBeanObject The mbean this object handles.
366         */
367        public void setSingleMBeanObject(SingleMBeanObject singleMBeanObject) {
368            this.singleMBeanObject = singleMBeanObject;
369        }
370
371        /**
372         * Retries connecting to the host. On success, returns a string with success, and unregisters. On failure,
373         * returns a string with failure.
374         *
375         * @param proxy The error mbean that invoked this, ignored.
376         * @param method The method attempted invoked, ignored.
377         * @param args The arguments for the method, ignored.
378         * @return A string with success or failure.
379         * @throws Throwable Shouldn't throw exceptions.
380         */
381        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
382            try {
383                createProxyMBeansForHost(hostEntry);
384                singleMBeanObject.unregister();
385                return "Now proxying JMX beans for host '" + hostEntry.getName() + ":" + hostEntry.getJmxPort() + "'";
386            } catch (Exception e) {
387                // Still unable to connect. Oh well.
388                return "[" + new Date() + "] Unable to proxy JMX beans on host '" + hostEntry.getName() + ":"
389                        + hostEntry.getJmxPort() + "', last seen active at '" + hostEntry.getTime() + "'\n"
390                        + ExceptionUtils.getStackTrace(e);
391            }
392        }
393    }
394
395    /** An invocation handler that forwards invocations to a remote mbean. */
396    private class ProxyMBeanInvocationHandler implements InvocationHandler {
397        /** The name of the remote mbean. */
398        private final ObjectName name;
399        /** The host for the remote mbean. */
400        private final HostEntry hostEntry;
401
402        /**
403         * Make a new forwarding mbean handler.
404         *
405         * @param name The name of the remote mbean.
406         * @param hostEntry The host for the remote mbean.
407         */
408        public ProxyMBeanInvocationHandler(ObjectName name, HostEntry hostEntry) {
409            this.name = name;
410            this.hostEntry = hostEntry;
411        }
412
413        /**
414         * Initialises a connection to a remote bean. Then invokes the method on that bean.
415         *
416         * @param proxy This proxying object. Ignored.
417         * @param method The method invoked. This is called on the remote mbean.
418         * @param args The arguments to the method. These are given to the remote mbean.
419         * @return Whatever the remote mbean returns.
420         * @throws IOFailure On trouble establishing the connection.
421         * @throws javax.management.RuntimeMBeanException On exceptions in the mbean invocations.
422         * @throws Throwable What ever the remote mbean has thrown.
423         */
424        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
425            // establish or reestablish mbean.
426            JMXProxyConnection connection;
427            try {
428                connection = connectionFactory.getConnection(hostEntry.getName(), hostEntry.getJmxPort(),
429                        hostEntry.getRmiPort(), getJmxUsername(), getJmxPassword());
430            } catch (Exception e) {
431                throw new IOFailure("Could not connect to host '" + hostEntry.getName() + ":" + hostEntry.getJmxPort()
432                        + "', last seen active at '" + hostEntry.getTime() + "'\n", e);
433            }
434            // call remote method.
435            T mBean = connection.createProxy(name, asInterface);
436            return method.invoke(mBean, args);
437        }
438    }
439
440}