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