001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2010 Sun Microsystems, Inc. 025 * Portions Copyright 2014-2015 ForgeRock AS 026 */ 027package org.opends.server.extensions; 028 029import static org.opends.messages.CoreMessages.*; 030import static org.opends.server.core.DirectoryServer.*; 031import static org.opends.server.util.CollectionUtils.*; 032import static org.opends.server.util.ServerConstants.*; 033 034import java.io.File; 035import java.io.IOException; 036import java.nio.file.FileStore; 037import java.nio.file.Files; 038import java.nio.file.Path; 039import java.util.ArrayList; 040import java.util.HashMap; 041import java.util.Iterator; 042import java.util.LinkedHashMap; 043import java.util.List; 044import java.util.Map; 045import java.util.Map.Entry; 046import java.util.concurrent.TimeUnit; 047 048import org.forgerock.i18n.LocalizableMessage; 049import org.forgerock.i18n.slf4j.LocalizedLogger; 050import org.forgerock.opendj.config.server.ConfigException; 051import org.forgerock.opendj.ldap.schema.Syntax; 052import org.opends.server.admin.std.server.MonitorProviderCfg; 053import org.opends.server.api.AlertGenerator; 054import org.opends.server.api.DiskSpaceMonitorHandler; 055import org.opends.server.api.MonitorProvider; 056import org.opends.server.api.ServerShutdownListener; 057import org.opends.server.core.DirectoryServer; 058import org.opends.server.types.Attribute; 059import org.opends.server.types.AttributeType; 060import org.opends.server.types.Attributes; 061import org.opends.server.types.DN; 062import org.opends.server.types.DirectoryException; 063import org.opends.server.types.InitializationException; 064 065/** 066 * This class provides an application-wide disk space monitoring service. 067 * It provides the ability for registered handlers to receive notifications 068 * when the free disk space falls below a certain threshold. 069 * 070 * The handler will only be notified once when when the free space 071 * have dropped below any of the thresholds. Once the "full" threshold 072 * have been reached, the handler will not be notified again until the 073 * free space raises above the "low" threshold. 074 */ 075public class DiskSpaceMonitor extends MonitorProvider<MonitorProviderCfg> implements Runnable, AlertGenerator, 076 ServerShutdownListener 077{ 078 /** 079 * Helper class for each requestor for use with cn=monitor reporting and users of a spcific mountpoint. 080 */ 081 private class MonitoredDirectory extends MonitorProvider<MonitorProviderCfg> 082 { 083 private volatile File directory; 084 private volatile long lowThreshold; 085 private volatile long fullThreshold; 086 private final DiskSpaceMonitorHandler handler; 087 private final String instanceName; 088 private final String baseName; 089 private int lastState; 090 091 private MonitoredDirectory(File directory, String instanceName, String baseName, DiskSpaceMonitorHandler handler) 092 { 093 this.directory = directory; 094 this.instanceName = instanceName; 095 this.baseName = baseName; 096 this.handler = handler; 097 } 098 099 /** {@inheritDoc} */ 100 @Override 101 public String getMonitorInstanceName() { 102 return instanceName + "," + "cn=" + baseName; 103 } 104 105 /** {@inheritDoc} */ 106 @Override 107 public void initializeMonitorProvider(MonitorProviderCfg configuration) 108 throws ConfigException, InitializationException { 109 } 110 111 /** {@inheritDoc} */ 112 @Override 113 public List<Attribute> getMonitorData() { 114 final List<Attribute> monitorAttrs = new ArrayList<>(); 115 monitorAttrs.add(attr("disk-dir", getDefaultStringSyntax(), directory.getPath())); 116 monitorAttrs.add(attr("disk-free", getDefaultIntegerSyntax(), getFreeSpace())); 117 monitorAttrs.add(attr("disk-state", getDefaultStringSyntax(), getState())); 118 return monitorAttrs; 119 } 120 121 private File getDirectory() { 122 return directory; 123 } 124 125 private long getFreeSpace() { 126 return directory.getUsableSpace(); 127 } 128 129 private long getFullThreshold() { 130 return fullThreshold; 131 } 132 133 private long getLowThreshold() { 134 return lowThreshold; 135 } 136 137 private void setFullThreshold(long fullThreshold) { 138 this.fullThreshold = fullThreshold; 139 } 140 141 private void setLowThreshold(long lowThreshold) { 142 this.lowThreshold = lowThreshold; 143 } 144 145 private Attribute attr(String name, Syntax syntax, Object value) 146 { 147 AttributeType attrType = DirectoryServer.getAttributeTypeOrDefault(name, name, syntax); 148 return Attributes.create(attrType, String.valueOf(value)); 149 } 150 151 private String getState() 152 { 153 switch(lastState) 154 { 155 case NORMAL: 156 return "normal"; 157 case LOW: 158 return "low"; 159 case FULL: 160 return "full"; 161 default: 162 return null; 163 } 164 } 165 } 166 167 /** 168 * Helper class for building temporary list of handlers to notify on threshold hits. 169 * One object per directory per state will hold all the handlers matching directory and state. 170 */ 171 private class HandlerNotifier { 172 private File directory; 173 private int state; 174 /** printable list of handlers names, for reporting backend names in alert messages */ 175 private final StringBuilder diskNames = new StringBuilder(); 176 private final List<MonitoredDirectory> allHandlers = new ArrayList<>(); 177 178 private HandlerNotifier(File directory, int state) 179 { 180 this.directory = directory; 181 this.state = state; 182 } 183 184 private void notifyHandlers() 185 { 186 for (MonitoredDirectory mdElem : allHandlers) 187 { 188 switch (state) 189 { 190 case FULL: 191 mdElem.handler.diskFullThresholdReached(mdElem.getDirectory(), mdElem.getFullThreshold()); 192 break; 193 case LOW: 194 mdElem.handler.diskLowThresholdReached(mdElem.getDirectory(), mdElem.getLowThreshold()); 195 break; 196 case NORMAL: 197 mdElem.handler.diskSpaceRestored(mdElem.getDirectory(), mdElem.getLowThreshold(), 198 mdElem.getFullThreshold()); 199 break; 200 } 201 } 202 } 203 204 private boolean isEmpty() 205 { 206 return allHandlers.isEmpty(); 207 } 208 209 private void addHandler(MonitoredDirectory handler) 210 { 211 logger.trace("State change: %d -> %d", handler.lastState, state); 212 handler.lastState = state; 213 if (handler.handler != null) 214 { 215 allHandlers.add(handler); 216 } 217 appendName(diskNames, handler.instanceName); 218 } 219 220 private void appendName(StringBuilder strNames, String strVal) 221 { 222 if (strNames.length() > 0) 223 { 224 strNames.append(", "); 225 } 226 strNames.append(strVal); 227 } 228 } 229 230 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 231 232 private static final int NORMAL = 0; 233 private static final int LOW = 1; 234 private static final int FULL = 2; 235 private static final String INSTANCENAME = "Disk Space Monitor"; 236 private final HashMap<File, List<MonitoredDirectory>> monitoredDirs = new HashMap<>(); 237 238 /** 239 * Constructs a new DiskSpaceMonitor that will notify registered DiskSpaceMonitorHandler objects when filesystems 240 * on which configured directories reside, fall below the provided thresholds. 241 */ 242 public DiskSpaceMonitor() 243 { 244 } 245 246 /** 247 * Starts periodic monitoring of all registered directories. 248 */ 249 public void startDiskSpaceMonitor() 250 { 251 DirectoryServer.registerMonitorProvider(this); 252 DirectoryServer.registerShutdownListener(this); 253 scheduleUpdate(this, 0, 5, TimeUnit.SECONDS); 254 } 255 256 /** 257 * Registers or reconfigures a directory for monitoring. 258 * If possible, we will try to get and use the mountpoint where the directory resides and monitor it instead. 259 * If the directory is already registered for the same <code>handler</code>, simply change its configuration. 260 * @param instanceName A name for the handler, as used by cn=monitor 261 * @param directory The directory to monitor 262 * @param lowThresholdBytes Disk slow threshold expressed in bytes 263 * @param fullThresholdBytes Disk full threshold expressed in bytes 264 * @param handler The class requesting to be called when a transition in disk space occurs 265 */ 266 public void registerMonitoredDirectory(String instanceName, File directory, long lowThresholdBytes, 267 long fullThresholdBytes, DiskSpaceMonitorHandler handler) 268 { 269 File fsMountPoint; 270 try 271 { 272 fsMountPoint = getMountPoint(directory); 273 } 274 catch (IOException ioe) 275 { 276 logger.warn(ERR_DISK_SPACE_GET_MOUNT_POINT, directory.getAbsolutePath(), ioe.getLocalizedMessage()); 277 fsMountPoint = directory; 278 } 279 MonitoredDirectory newDSH = new MonitoredDirectory(directory, instanceName, INSTANCENAME, handler); 280 newDSH.setFullThreshold(fullThresholdBytes); 281 newDSH.setLowThreshold(lowThresholdBytes); 282 283 synchronized (monitoredDirs) 284 { 285 List<MonitoredDirectory> diskHelpers = monitoredDirs.get(fsMountPoint); 286 if (diskHelpers == null) 287 { 288 monitoredDirs.put(fsMountPoint, newArrayList(newDSH)); 289 } 290 else 291 { 292 for (MonitoredDirectory elem : diskHelpers) 293 { 294 if (elem.handler.equals(handler) && elem.getDirectory().equals(directory)) 295 { 296 elem.setFullThreshold(fullThresholdBytes); 297 elem.setLowThreshold(lowThresholdBytes); 298 return; 299 } 300 } 301 diskHelpers.add(newDSH); 302 } 303 DirectoryServer.registerMonitorProvider(newDSH); 304 } 305 } 306 307 private File getMountPoint(File directory) throws IOException 308 { 309 Path mountPoint = directory.getAbsoluteFile().toPath(); 310 Path parentDir = mountPoint.getParent(); 311 FileStore dirFileStore = Files.getFileStore(mountPoint); 312 /* 313 * Since there is no concept of mount point in the APIs, iterate on all parents of 314 * the given directory until the FileSystem Store changes (hint of a different 315 * device, hence a mount point) or we get to root, which works too. 316 */ 317 while (parentDir != null) 318 { 319 if (!Files.getFileStore(parentDir).equals(dirFileStore)) 320 { 321 return mountPoint.toFile(); 322 } 323 mountPoint = mountPoint.getParent(); 324 parentDir = parentDir.getParent(); 325 } 326 return mountPoint.toFile(); 327 } 328 329 /** 330 * Removes a directory from the set of monitored directories. 331 * 332 * @param directory The directory to stop monitoring on 333 * @param handler The class that requested monitoring 334 */ 335 public void deregisterMonitoredDirectory(File directory, DiskSpaceMonitorHandler handler) 336 { 337 synchronized (monitoredDirs) 338 { 339 340 List<MonitoredDirectory> directories = monitoredDirs.get(directory); 341 if (directories != null) 342 { 343 Iterator<MonitoredDirectory> itr = directories.iterator(); 344 while (itr.hasNext()) 345 { 346 MonitoredDirectory curDirectory = itr.next(); 347 if (curDirectory.handler.equals(handler)) 348 { 349 DirectoryServer.deregisterMonitorProvider(curDirectory); 350 itr.remove(); 351 } 352 } 353 if (directories.isEmpty()) 354 { 355 monitoredDirs.remove(directory); 356 } 357 } 358 } 359 } 360 361 /** {@inheritDoc} */ 362 @Override 363 public void initializeMonitorProvider(MonitorProviderCfg configuration) 364 throws ConfigException, InitializationException { 365 // Not used... 366 } 367 368 /** {@inheritDoc} */ 369 @Override 370 public String getMonitorInstanceName() { 371 return INSTANCENAME; 372 } 373 374 /** {@inheritDoc} */ 375 @Override 376 public List<Attribute> getMonitorData() { 377 return new ArrayList<>(); 378 } 379 380 /** {@inheritDoc} */ 381 @Override 382 public void run() 383 { 384 List<HandlerNotifier> diskFull = new ArrayList<>(); 385 List<HandlerNotifier> diskLow = new ArrayList<>(); 386 List<HandlerNotifier> diskRestored = new ArrayList<>(); 387 388 synchronized (monitoredDirs) 389 { 390 for (Entry<File, List<MonitoredDirectory>> dirElem : monitoredDirs.entrySet()) 391 { 392 File directory = dirElem.getKey(); 393 HandlerNotifier diskFullClients = new HandlerNotifier(directory, FULL); 394 HandlerNotifier diskLowClients = new HandlerNotifier(directory, LOW); 395 HandlerNotifier diskRestoredClients = new HandlerNotifier(directory, NORMAL); 396 try 397 { 398 long lastFreeSpace = directory.getUsableSpace(); 399 for (MonitoredDirectory handlerElem : dirElem.getValue()) 400 { 401 if (lastFreeSpace < handlerElem.getFullThreshold() && handlerElem.lastState < FULL) 402 { 403 diskFullClients.addHandler(handlerElem); 404 } 405 else if (lastFreeSpace < handlerElem.getLowThreshold() && handlerElem.lastState < LOW) 406 { 407 diskLowClients.addHandler(handlerElem); 408 } 409 else if (handlerElem.lastState != NORMAL) 410 { 411 diskRestoredClients.addHandler(handlerElem); 412 } 413 } 414 addToList(diskFull, diskFullClients); 415 addToList(diskLow, diskLowClients); 416 addToList(diskRestored, diskRestoredClients); 417 } 418 catch(Exception e) 419 { 420 logger.error(ERR_DISK_SPACE_MONITOR_UPDATE_FAILED, directory, e); 421 logger.traceException(e); 422 } 423 } 424 } 425 // It is probably better to notify handlers outside of the synchronized section. 426 sendNotification(diskFull, FULL, ALERT_DESCRIPTION_DISK_FULL); 427 sendNotification(diskLow, LOW, ALERT_TYPE_DISK_SPACE_LOW); 428 sendNotification(diskRestored, NORMAL, null); 429 } 430 431 private void addToList(List<HandlerNotifier> hnList, HandlerNotifier notifier) 432 { 433 if (!notifier.isEmpty()) 434 { 435 hnList.add(notifier); 436 } 437 } 438 439 private void sendNotification(List<HandlerNotifier> diskList, int state, String alert) 440 { 441 for (HandlerNotifier dirElem : diskList) 442 { 443 String dirPath = dirElem.directory.getAbsolutePath(); 444 String handlerNames = dirElem.diskNames.toString(); 445 long freeSpace = dirElem.directory.getFreeSpace(); 446 if (state == FULL) 447 { 448 DirectoryServer.sendAlertNotification(this, alert, 449 ERR_DISK_SPACE_FULL_THRESHOLD_REACHED.get(dirPath, handlerNames, freeSpace)); 450 } 451 else if (state == LOW) 452 { 453 DirectoryServer.sendAlertNotification(this, alert, 454 ERR_DISK_SPACE_LOW_THRESHOLD_REACHED.get(dirPath, handlerNames, freeSpace)); 455 } 456 else 457 { 458 logger.error(NOTE_DISK_SPACE_RESTORED.get(freeSpace, dirPath)); 459 } 460 dirElem.notifyHandlers(); 461 } 462 } 463 464 /** {@inheritDoc} */ 465 @Override 466 public DN getComponentEntryDN() 467 { 468 try 469 { 470 return DN.valueOf(INSTANCENAME); 471 } 472 catch (DirectoryException de) 473 { 474 return DN.NULL_DN; 475 } 476 } 477 478 /** {@inheritDoc} */ 479 @Override 480 public String getClassName() 481 { 482 return DiskSpaceMonitor.class.getName(); 483 } 484 485 /** {@inheritDoc} */ 486 @Override 487 public Map<String, String> getAlerts() 488 { 489 Map<String, String> alerts = new LinkedHashMap<>(); 490 alerts.put(ALERT_TYPE_DISK_SPACE_LOW, ALERT_DESCRIPTION_DISK_SPACE_LOW); 491 alerts.put(ALERT_TYPE_DISK_FULL, ALERT_DESCRIPTION_DISK_FULL); 492 return alerts; 493 } 494 495 /** {@inheritDoc} */ 496 @Override 497 public String getShutdownListenerName() 498 { 499 return INSTANCENAME; 500 } 501 502 /** {@inheritDoc} */ 503 @Override 504 public void processServerShutdown(LocalizableMessage reason) 505 { 506 synchronized (monitoredDirs) 507 { 508 for (Entry<File, List<MonitoredDirectory>> dirElem : monitoredDirs.entrySet()) 509 { 510 for (MonitoredDirectory handlerElem : dirElem.getValue()) 511 { 512 DirectoryServer.deregisterMonitorProvider(handlerElem); 513 } 514 } 515 } 516 } 517}