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