1 /* 2 ** Zabbix 3 ** Copyright (C) 2001-2021 Zabbix SIA 4 ** 5 ** This program is free software; you can redistribute it and/or modify 6 ** it under the terms of the GNU General Public License as published by 7 ** the Free Software Foundation; either version 2 of the License, or 8 ** (at your option) any later version. 9 ** 10 ** This program is distributed in the hope that it will be useful, 11 ** but WITHOUT ANY WARRANTY; without even the implied warranty of 12 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 ** GNU General Public License for more details. 14 ** 15 ** You should have received a copy of the GNU General Public License 16 ** along with this program; if not, write to the Free Software 17 ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 **/ 19 20 package com.zabbix.gateway; 21 22 import java.io.IOException; 23 import java.util.HashMap; 24 import java.util.Map; 25 import java.util.HashSet; 26 import javax.management.AttributeList; 27 28 import javax.management.InstanceNotFoundException; 29 import javax.management.AttributeNotFoundException; 30 import javax.management.MBeanAttributeInfo; 31 import javax.management.MBeanServerConnection; 32 import javax.management.ObjectName; 33 import javax.management.MalformedObjectNameException; 34 import javax.management.openmbean.CompositeData; 35 import javax.management.openmbean.TabularDataSupport; 36 import javax.management.openmbean.TabularData; 37 import javax.management.remote.JMXConnector; 38 import javax.management.remote.JMXServiceURL; 39 import javax.rmi.ssl.SslRMIClientSocketFactory; 40 41 import org.json.*; 42 43 import org.slf4j.Logger; 44 import org.slf4j.LoggerFactory; 45 46 class JMXItemChecker extends ItemChecker 47 { 48 private static final Logger logger = LoggerFactory.getLogger(JMXItemChecker.class); 49 50 private JMXServiceURL url; 51 private JMXConnector jmxc; 52 private MBeanServerConnection mbsc; 53 54 private String username; 55 private String password; 56 private String jmx_endpoint; 57 58 private enum DiscoveryMode { 59 ATTRIBUTES, 60 BEANS 61 } 62 63 private static HashMap<String, Boolean> useRMISSLforURLHintCache = new HashMap<String, Boolean>(); 64 JMXItemChecker(JSONObject request)65 JMXItemChecker(JSONObject request) throws ZabbixException 66 { 67 super(request); 68 69 try 70 { 71 jmx_endpoint = request.getString(JSON_TAG_JMX_ENDPOINT); 72 } 73 catch (Exception e) 74 { 75 throw new ZabbixException(e); 76 } 77 78 try 79 { 80 url = new JMXServiceURL(jmx_endpoint); 81 jmxc = null; 82 mbsc = null; 83 84 username = request.optString(JSON_TAG_USERNAME, null); 85 password = request.optString(JSON_TAG_PASSWORD, null); 86 } 87 catch (Exception e) 88 { 89 throw new ZabbixException("%s: %s", e, jmx_endpoint); 90 } 91 } 92 93 @Override getValues()94 JSONArray getValues() throws ZabbixException 95 { 96 JSONArray values = new JSONArray(); 97 98 try 99 { 100 HashMap<String, Object> env = new HashMap<String, Object>(); 101 102 if (null != username && null != password) 103 { 104 env.put(JMXConnector.CREDENTIALS, new String[] {username, password}); 105 } 106 107 if (!useRMISSLforURLHintCache.containsKey(url.getURLPath()) || 108 !useRMISSLforURLHintCache.get(url.getURLPath())) 109 { 110 try 111 { 112 jmxc = ZabbixJMXConnectorFactory.connect(url, env); 113 useRMISSLforURLHintCache.put(url.getURLPath(), false); 114 } 115 catch (IOException e) 116 { 117 env.put("com.sun.jndi.rmi.factory.socket", new SslRMIClientSocketFactory()); 118 jmxc = ZabbixJMXConnectorFactory.connect(url, env); 119 useRMISSLforURLHintCache.put(url.getURLPath(), true); 120 } 121 } 122 else 123 { 124 try 125 { 126 env.put("com.sun.jndi.rmi.factory.socket", new SslRMIClientSocketFactory()); 127 jmxc = ZabbixJMXConnectorFactory.connect(url, env); 128 useRMISSLforURLHintCache.put(url.getURLPath(), true); 129 } 130 catch (IOException e) 131 { 132 env.remove("com.sun.jndi.rmi.factory.socket"); 133 jmxc = ZabbixJMXConnectorFactory.connect(url, env); 134 useRMISSLforURLHintCache.put(url.getURLPath(), false); 135 } 136 } 137 138 mbsc = jmxc.getMBeanServerConnection(); 139 logger.debug("using RMI SSL for " + url.getURLPath() + ": " + useRMISSLforURLHintCache.get(url.getURLPath())); 140 141 for (String key : keys) 142 values.put(getJSONValue(key)); 143 } 144 catch (SecurityException e1) 145 { 146 JSONObject value = new JSONObject(); 147 148 logger.warn("cannot process keys '{}': {}: {}", new Object[] {keys, 149 ZabbixException.getRootCauseMessage(e1), url}); 150 logger.debug("error caused by", e1); 151 152 try 153 { 154 value.put(JSON_TAG_ERROR, ZabbixException.getRootCauseMessage(e1)); 155 } 156 catch (JSONException e2) 157 { 158 Object[] logInfo = {JSON_TAG_ERROR, e1.getMessage(), 159 ZabbixException.getRootCauseMessage(e2)}; 160 logger.warn("cannot add JSON attribute '{}' with message '{}': {}", logInfo); 161 logger.debug("error caused by", e2); 162 } 163 164 for (int i = 0; i < keys.size(); i++) 165 values.put(value); 166 } 167 catch (Exception e) 168 { 169 throw new ZabbixException("%s: %s", ZabbixException.getRootCauseMessage(e), url); 170 } 171 finally 172 { 173 try { if (null != jmxc) jmxc.close(); } catch (java.io.IOException exception) { } 174 175 jmxc = null; 176 mbsc = null; 177 } 178 179 return values; 180 } 181 182 @Override getStringValue(String key)183 protected String getStringValue(String key) throws Exception 184 { 185 ZabbixItem item = new ZabbixItem(key); 186 int argumentCount = item.getArgumentCount(); 187 188 if (item.getKeyId().equals("jmx")) 189 { 190 if (2 != argumentCount && 3 != argumentCount) 191 throw new ZabbixException("required key format: jmx[<object name>,<attribute name>,<unique short description>]"); 192 193 ObjectName objectName = new ObjectName(item.getArgument(1)); 194 String attributeName = item.getArgument(2); 195 String realAttributeName; 196 String fieldNames = ""; 197 198 // Attribute name and composite data field names are separated by dots. On the other hand the 199 // name may contain a dot too. In this case user needs to escape it with a backslash. Also the 200 // backslash symbols in the name must be escaped. So a real separator is unescaped dot and 201 // separatorIndex() is used to locate it. 202 203 int sep = HelperFunctionChest.separatorIndex(attributeName); 204 205 if (-1 != sep) 206 { 207 logger.trace("'{}' contains composite data", attributeName); 208 209 realAttributeName = attributeName.substring(0, sep); 210 fieldNames = attributeName.substring(sep + 1); 211 } 212 else 213 realAttributeName = attributeName; 214 215 // unescape possible dots or backslashes that were escaped by user 216 realAttributeName = HelperFunctionChest.unescapeUserInput(realAttributeName); 217 218 logger.trace("attributeName:'{}'", realAttributeName); 219 logger.trace("fieldNames:'{}'", fieldNames); 220 221 try 222 { 223 Object dataObject = mbsc.getAttribute(objectName, realAttributeName); 224 225 if (dataObject instanceof TabularData) 226 { 227 logger.trace("'{}' contains tabular data", attributeName); 228 return getTabularData((TabularData)dataObject).toString(); 229 } 230 231 return getPrimitiveAttributeValue(dataObject, fieldNames); 232 } 233 catch (AttributeNotFoundException e) 234 { 235 throw new ZabbixException("Attribute not found: %s", ZabbixException.getRootCauseMessage(e)); 236 } 237 catch (InstanceNotFoundException e) 238 { 239 throw new ZabbixException("Object or attribute not found: %s", ZabbixException.getRootCauseMessage(e)); 240 } 241 } 242 else if (item.getKeyId().equals("jmx.discovery") || item.getKeyId().equals("jmx.get")) 243 { 244 if (3 < argumentCount) 245 throw new ZabbixException("required key format: " + item.getKeyId() + 246 "[<discovery mode>,<object name>,<unique short description>]"); 247 248 ObjectName filter; 249 250 try 251 { 252 filter = (2 <= argumentCount) ? new ObjectName(item.getArgument(2)) : null; 253 } 254 catch (MalformedObjectNameException e) 255 { 256 throw new ZabbixException("invalid object name format: " + item.getArgument(2)); 257 } 258 259 boolean mapped = item.getKeyId().equals("jmx.discovery"); 260 JSONArray counters = new JSONArray(); 261 DiscoveryMode mode = DiscoveryMode.ATTRIBUTES; 262 if (0 < argumentCount) 263 { 264 String modeName = item.getArgument(1); 265 if (modeName.equals("beans")) 266 mode = DiscoveryMode.BEANS; 267 else if (!modeName.equals("attributes")) 268 throw new ZabbixException("invalid discovery mode: " + modeName); 269 } 270 271 switch(mode) 272 { 273 case ATTRIBUTES: 274 discoverAttributes(counters, filter, mapped); 275 break; 276 case BEANS: 277 discoverBeans(counters, filter, mapped); 278 break; 279 } 280 281 if (mapped) 282 { 283 JSONObject mapping = new JSONObject(); 284 mapping.put(ItemChecker.JSON_TAG_DATA, counters); 285 return mapping.toString(); 286 } 287 else 288 { 289 return counters.toString(); 290 } 291 } 292 else 293 throw new ZabbixException("key ID '%s' is not supported", item.getKeyId()); 294 } 295 getPrimitiveAttributeValue(Object dataObject, String fieldNames)296 private String getPrimitiveAttributeValue(Object dataObject, String fieldNames) throws Exception 297 { 298 logger.trace("drilling down with data object '{}' and field names '{}'", dataObject, fieldNames); 299 300 if (null == dataObject) 301 throw new ZabbixException("data object is null"); 302 303 if (fieldNames.equals("")) 304 { 305 try 306 { 307 if (isPrimitiveAttributeType(dataObject)) 308 return dataObject.toString(); 309 else 310 throw new NoSuchMethodException(); 311 } 312 catch (NoSuchMethodException e) 313 { 314 throw new ZabbixException("Data object type cannot be converted to string."); 315 } 316 } 317 318 if (dataObject instanceof CompositeData) 319 { 320 logger.trace("'{}' contains composite data", dataObject); 321 322 CompositeData comp = (CompositeData)dataObject; 323 324 String dataObjectName; 325 String newFieldNames = ""; 326 327 int sep = HelperFunctionChest.separatorIndex(fieldNames); 328 329 if (-1 != sep) 330 { 331 dataObjectName = fieldNames.substring(0, sep); 332 newFieldNames = fieldNames.substring(sep + 1); 333 } 334 else 335 dataObjectName = fieldNames; 336 337 // unescape possible dots or backslashes that were escaped by user 338 dataObjectName = HelperFunctionChest.unescapeUserInput(dataObjectName); 339 340 return getPrimitiveAttributeValue(comp.get(dataObjectName), newFieldNames); 341 } 342 else 343 throw new ZabbixException("unsupported data object type along the path: %s", dataObject.getClass()); 344 } 345 getTabularData(TabularData data)346 private JSONArray getTabularData(TabularData data) throws JSONException 347 { 348 JSONArray values = new JSONArray(); 349 350 for (Object value : data.values()) 351 { 352 JSONObject tmp = getCompositeDataValues((CompositeData)value); 353 354 if (tmp.length() > 0) 355 values.put(tmp); 356 } 357 358 return values; 359 } 360 getCompositeDataValues(CompositeData compData)361 private JSONObject getCompositeDataValues(CompositeData compData) throws JSONException 362 { 363 JSONObject value = new JSONObject(); 364 365 for (String key : compData.getCompositeType().keySet()) 366 { 367 Object data = compData.get(key); 368 369 if (data == null) 370 { 371 value.put(key, JSONObject.NULL); 372 } 373 else if (data.getClass().isArray()) 374 { 375 logger.trace("found attribute of a known, unsupported type: {}", data.getClass()); 376 continue; 377 } 378 else if (data instanceof TabularData) 379 { 380 value.put(key, getTabularData((TabularData)data)); 381 } 382 else if (data instanceof CompositeData) 383 { 384 value.put(key, getCompositeDataValues((CompositeData)data)); 385 } 386 else 387 value.put(key, data); 388 } 389 390 return value; 391 } 392 discoverAttributes(JSONArray counters, ObjectName filter, boolean propertiesAsMacros)393 private void discoverAttributes(JSONArray counters, ObjectName filter, boolean propertiesAsMacros) throws Exception 394 { 395 for (ObjectName name : mbsc.queryNames(filter, null)) 396 { 397 Map<String, Object> values = new HashMap<String, Object>(); 398 MBeanAttributeInfo[] attributeArray = mbsc.getMBeanInfo(name).getAttributes(); 399 400 if (0 == attributeArray.length) 401 { 402 logger.trace("object has no attributes"); 403 return; 404 } 405 406 String[] attributeNames = getAttributeNames(attributeArray); 407 AttributeList attributes; 408 String discoveredObjKey = jmx_endpoint + "#" + name; 409 Long expirationTime = JavaGateway.iterativeObjects.get(discoveredObjKey); 410 long now = System.currentTimeMillis(); 411 412 if (null != expirationTime && now <= expirationTime) 413 { 414 attributes = getAttributesIterative(name, attributeNames); 415 } 416 else 417 { 418 try 419 { 420 attributes = getAttributesBulk(name, attributeNames); 421 422 if (null != expirationTime) 423 JavaGateway.iterativeObjects.remove(discoveredObjKey); 424 } 425 catch (Exception e) 426 { 427 attributes = getAttributesIterative(name, attributeNames); 428 429 // This object's attributes will be collected iteratively for next 24h. After that it will 430 // be checked if it is possible to successfully collect all attributes in bulk mode. 431 JavaGateway.iterativeObjects.put(discoveredObjKey, now + SocketProcessor.MILLISECONDS_IN_HOUR * 24); 432 } 433 } 434 435 if (attributes.isEmpty()) 436 { 437 logger.warn("cannot process any attribute for object '{}'", name); 438 return; 439 } 440 441 for (javax.management.Attribute attribute : attributes.asList()) 442 values.put(attribute.getName(), attribute.getValue()); 443 444 for (MBeanAttributeInfo attrInfo : attributeArray) 445 { 446 logger.trace("discovered attribute '{}'", attrInfo.getName()); 447 448 Object attribute; 449 450 if (null == (attribute = values.get(attrInfo.getName()))) 451 { 452 logger.trace("cannot retrieve attribute value, skipping"); 453 continue; 454 } 455 456 try 457 { 458 String descr = (attrInfo.getName().equals(attrInfo.getDescription()) ? null : 459 attrInfo.getDescription()); 460 461 if (attribute instanceof TabularData) 462 { 463 logger.trace("looking for attributes of tabular types"); 464 465 formatPrimitiveTypeResult(counters, name, descr, attrInfo.getName(), attribute, 466 propertiesAsMacros, getTabularData((TabularData)attribute)); 467 } 468 else 469 { 470 logger.trace("looking for attributes of primitive types"); 471 getAttributeFields(counters, name, descr, attrInfo.getName(), attribute, 472 propertiesAsMacros); 473 } 474 } 475 catch (Exception e) 476 { 477 Object[] logInfo = {name, attrInfo.getName(), ZabbixException.getRootCauseMessage(e)}; 478 logger.warn("attribute processing '{},{}' failed: {}", logInfo); 479 logger.debug("error caused by", e); 480 } 481 } 482 } 483 } 484 getAttributeNames(MBeanAttributeInfo[] attributeArray)485 private String[] getAttributeNames(MBeanAttributeInfo[] attributeArray) 486 { 487 int i = 0; 488 String[] attributeNames = new String[attributeArray.length]; 489 490 for (MBeanAttributeInfo attrInfo : attributeArray) 491 { 492 if (!attrInfo.isReadable()) 493 { 494 logger.trace("attribute '{}' not readable, skipping", attrInfo.getName()); 495 continue; 496 } 497 498 attributeNames[i++] = attrInfo.getName(); 499 } 500 501 return attributeNames; 502 } 503 getAttributesBulk(ObjectName name, String[] attributeNames)504 private AttributeList getAttributesBulk(ObjectName name, String[] attributeNames) throws Exception 505 { 506 return mbsc.getAttributes(name, attributeNames); 507 } 508 getAttributesIterative(ObjectName name, String[] attributeNames)509 private AttributeList getAttributesIterative(ObjectName name, String[] attributeNames) 510 { 511 AttributeList attributes = new AttributeList(); 512 513 for (String attributeName: attributeNames) 514 { 515 try 516 { 517 Object attrValue = mbsc.getAttribute(name, attributeName); 518 attributes.add(new javax.management.Attribute(attributeName, attrValue)); 519 } 520 catch (Exception e) 521 { 522 Object[] logInfo = {name, attributeName, ZabbixException.getRootCauseMessage(e)}; 523 logger.warn("attribute processing '{},{}' failed: {}", logInfo); 524 logger.debug("error caused by", e); 525 } 526 } 527 528 return attributes; 529 } 530 discoverBeans(JSONArray counters, ObjectName filter, boolean propertiesAsMacros)531 private void discoverBeans(JSONArray counters, ObjectName filter, boolean propertiesAsMacros) throws Exception 532 { 533 for (ObjectName name : mbsc.queryNames(filter, null)) 534 { 535 logger.trace("discovered bean '{}'", name); 536 537 try 538 { 539 JSONObject counter = new JSONObject(); 540 541 if (propertiesAsMacros) 542 { 543 HashSet<String> properties = new HashSet<String>(); 544 545 // Default properties are added. 546 counter.put("{#JMXOBJ}", name); 547 counter.put("{#JMXDOMAIN}", name.getDomain()); 548 properties.add("OBJ"); 549 properties.add("DOMAIN"); 550 551 for (Map.Entry<String, String> property : name.getKeyPropertyList().entrySet()) 552 { 553 String key = property.getKey().toUpperCase(); 554 555 // Property key should only contain valid characters and should not be already added to attribute list. 556 if (key.matches("^[A-Z0-9_\\.]+$") && !properties.contains(key)) 557 { 558 counter.put("{#JMX" + key + "}" , property.getValue()); 559 properties.add(key); 560 } 561 else 562 logger.trace("bean '{}' property '{}' was ignored", name, property.getKey()); 563 } 564 } 565 else 566 { 567 JSONObject properties = new JSONObject(); 568 counter.put("object", name); 569 counter.put("domain", name.getDomain()); 570 571 for (Map.Entry<String, String> property : name.getKeyPropertyList().entrySet()) 572 { 573 String key = property.getKey(); 574 properties.put(key, property.getValue()); 575 } 576 counter.put("properties", properties); 577 } 578 579 counters.put(counter); 580 } 581 catch (Exception e) 582 { 583 logger.warn("bean processing '{}' failed: {}", name, ZabbixException.getRootCauseMessage(e)); 584 logger.debug("error caused by", e); 585 } 586 } 587 } 588 getAttributeFields(JSONArray counters, ObjectName name, String descr, String attrPath, Object attribute, boolean propertiesAsMacros)589 private void getAttributeFields(JSONArray counters, ObjectName name, String descr, String attrPath, 590 Object attribute, boolean propertiesAsMacros) throws NoSuchMethodException, JSONException 591 { 592 if (null == attribute || isPrimitiveAttributeType(attribute)) 593 { 594 logger.trace("found attribute of a primitive type: {}", null == attribute ? "null" : 595 attribute.getClass()); 596 formatPrimitiveTypeResult(counters, name, descr, attrPath, attribute, propertiesAsMacros, attribute); 597 } 598 else if (attribute instanceof CompositeData) 599 { 600 logger.trace("found attribute of a composite type: {}", attribute.getClass()); 601 602 CompositeData comp = (CompositeData)attribute; 603 604 for (String key : comp.getCompositeType().keySet()) 605 { 606 logger.trace("drilling down with attribute path '{}'", attrPath + "." + key); 607 getAttributeFields(counters, name, comp.getCompositeType().getDescription(key), 608 attrPath + "." + key, comp.get(key), propertiesAsMacros); 609 } 610 } 611 else if (attribute.getClass().isArray()) 612 { 613 logger.trace("found attribute of a known, unsupported type: {}", attribute.getClass()); 614 } 615 else 616 logger.trace("found attribute of an unknown, unsupported type: {}", attribute.getClass()); 617 } 618 formatPrimitiveTypeResult(JSONArray counters, ObjectName name, String descr, String attrPath, Object attribute, boolean propertiesAsMacros, Object value)619 private void formatPrimitiveTypeResult(JSONArray counters, ObjectName name, String descr, String attrPath, 620 Object attribute, boolean propertiesAsMacros, Object value) throws JSONException 621 { 622 JSONObject counter = new JSONObject(); 623 624 String checkedDescription = null == descr ? name + "," + attrPath : descr; 625 Object checkedType = null == attribute ? JSONObject.NULL : attribute.getClass().getName(); 626 Object checkedValue = null == value ? JSONObject.NULL : value.toString(); 627 628 if (propertiesAsMacros) 629 { 630 counter.put("{#JMXDESC}", checkedDescription); 631 counter.put("{#JMXOBJ}", name); 632 counter.put("{#JMXATTR}", attrPath); 633 counter.put("{#JMXTYPE}", checkedType); 634 counter.put("{#JMXVALUE}", checkedValue); 635 } 636 else 637 { 638 counter.put("name", attrPath); 639 counter.put("object", name); 640 counter.put("description", checkedDescription); 641 counter.put("type", checkedType); 642 counter.put("value", checkedValue); 643 } 644 645 counters.put(counter); 646 } 647 isPrimitiveAttributeType(Object obj)648 private boolean isPrimitiveAttributeType(Object obj) throws NoSuchMethodException 649 { 650 Class<?>[] clazzez = {Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, 651 Float.class, Double.class, String.class, java.math.BigDecimal.class, java.math.BigInteger.class, 652 java.util.Date.class, javax.management.ObjectName.class, java.util.concurrent.atomic.AtomicBoolean.class, 653 java.util.concurrent.atomic.AtomicInteger.class, java.util.concurrent.atomic.AtomicLong.class}; 654 655 // check if the type is either primitive or overrides toString() 656 return HelperFunctionChest.arrayContains(clazzez, obj.getClass()) || 657 (!(obj instanceof CompositeData)) && (!(obj instanceof TabularDataSupport)) && 658 (obj.getClass().getMethod("toString").getDeclaringClass() != Object.class); 659 } 660 cleanUseRMISSLforURLHintCache()661 public void cleanUseRMISSLforURLHintCache() 662 { 663 int s = useRMISSLforURLHintCache.size(); 664 useRMISSLforURLHintCache.clear(); 665 logger.debug("Finished cleanup of RMI SSL hint cache. " + s + " entries removed."); 666 } 667 } 668