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}