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}