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