/*
|
* CDDL HEADER START
|
*
|
* The contents of this file are subject to the terms of the
|
* Common Development and Distribution License, Version 1.0 only
|
* (the "License"). You may not use this file except in compliance
|
* with the License.
|
*
|
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
|
* or http://forgerock.org/license/CDDLv1.0.html.
|
* See the License for the specific language governing permissions
|
* and limitations under the License.
|
*
|
* When distributing Covered Code, include this CDDL HEADER in each
|
* file and include the License file at legal-notices/CDDLv1_0.txt.
|
* If applicable, add the following below this CDDL HEADER, with the
|
* fields enclosed by brackets "[]" replaced with your own identifying
|
* information:
|
* Portions Copyright [yyyy] [name of copyright owner]
|
*
|
* CDDL HEADER END
|
*
|
*
|
* Copyright 2010 Sun Microsystems, Inc.
|
* Portions Copyright 2014-2015 ForgeRock AS
|
*/
|
package org.opends.server.extensions;
|
|
import static org.opends.messages.CoreMessages.*;
|
import static org.opends.server.core.DirectoryServer.*;
|
import static org.opends.server.util.CollectionUtils.*;
|
import static org.opends.server.util.ServerConstants.*;
|
|
import java.io.File;
|
import java.io.IOException;
|
import java.nio.file.FileStore;
|
import java.nio.file.Files;
|
import java.nio.file.Path;
|
import java.util.ArrayList;
|
import java.util.HashMap;
|
import java.util.Iterator;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Map.Entry;
|
import java.util.concurrent.TimeUnit;
|
|
import org.forgerock.i18n.LocalizableMessage;
|
import org.forgerock.i18n.slf4j.LocalizedLogger;
|
import org.forgerock.opendj.config.server.ConfigException;
|
import org.forgerock.opendj.ldap.schema.Syntax;
|
import org.opends.server.admin.std.server.MonitorProviderCfg;
|
import org.opends.server.api.AlertGenerator;
|
import org.opends.server.api.DiskSpaceMonitorHandler;
|
import org.opends.server.api.MonitorProvider;
|
import org.opends.server.api.ServerShutdownListener;
|
import org.opends.server.core.DirectoryServer;
|
import org.opends.server.types.Attribute;
|
import org.opends.server.types.AttributeType;
|
import org.opends.server.types.Attributes;
|
import org.opends.server.types.DN;
|
import org.opends.server.types.DirectoryException;
|
import org.opends.server.types.InitializationException;
|
|
/**
|
* This class provides an application-wide disk space monitoring service.
|
* It provides the ability for registered handlers to receive notifications
|
* when the free disk space falls below a certain threshold.
|
*
|
* The handler will only be notified once when when the free space
|
* have dropped below any of the thresholds. Once the "full" threshold
|
* have been reached, the handler will not be notified again until the
|
* free space raises above the "low" threshold.
|
*/
|
public class DiskSpaceMonitor extends MonitorProvider<MonitorProviderCfg> implements Runnable, AlertGenerator,
|
ServerShutdownListener
|
{
|
/**
|
* Helper class for each requestor for use with cn=monitor reporting and users of a spcific mountpoint.
|
*/
|
private class MonitoredDirectory extends MonitorProvider<MonitorProviderCfg>
|
{
|
private volatile File directory;
|
private volatile long lowThreshold;
|
private volatile long fullThreshold;
|
private final DiskSpaceMonitorHandler handler;
|
private final String instanceName;
|
private final String baseName;
|
private int lastState;
|
|
private MonitoredDirectory(File directory, String instanceName, String baseName, DiskSpaceMonitorHandler handler)
|
{
|
this.directory = directory;
|
this.instanceName = instanceName;
|
this.baseName = baseName;
|
this.handler = handler;
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public String getMonitorInstanceName() {
|
return instanceName + "," + "cn=" + baseName;
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void initializeMonitorProvider(MonitorProviderCfg configuration)
|
throws ConfigException, InitializationException {
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public List<Attribute> getMonitorData() {
|
final List<Attribute> monitorAttrs = new ArrayList<>();
|
monitorAttrs.add(attr("disk-dir", getDefaultStringSyntax(), directory.getPath()));
|
monitorAttrs.add(attr("disk-free", getDefaultIntegerSyntax(), getFreeSpace()));
|
monitorAttrs.add(attr("disk-state", getDefaultStringSyntax(), getState()));
|
return monitorAttrs;
|
}
|
|
private File getDirectory() {
|
return directory;
|
}
|
|
private long getFreeSpace() {
|
return directory.getUsableSpace();
|
}
|
|
private long getFullThreshold() {
|
return fullThreshold;
|
}
|
|
private long getLowThreshold() {
|
return lowThreshold;
|
}
|
|
private void setFullThreshold(long fullThreshold) {
|
this.fullThreshold = fullThreshold;
|
}
|
|
private void setLowThreshold(long lowThreshold) {
|
this.lowThreshold = lowThreshold;
|
}
|
|
private Attribute attr(String name, Syntax syntax, Object value)
|
{
|
AttributeType attrType = DirectoryServer.getAttributeTypeOrDefault(name, name, syntax);
|
return Attributes.create(attrType, String.valueOf(value));
|
}
|
|
private String getState()
|
{
|
switch(lastState)
|
{
|
case NORMAL:
|
return "normal";
|
case LOW:
|
return "low";
|
case FULL:
|
return "full";
|
default:
|
return null;
|
}
|
}
|
}
|
|
/**
|
* Helper class for building temporary list of handlers to notify on threshold hits.
|
* One object per directory per state will hold all the handlers matching directory and state.
|
*/
|
private class HandlerNotifier {
|
private File directory;
|
private int state;
|
/** printable list of handlers names, for reporting backend names in alert messages */
|
private final StringBuilder diskNames = new StringBuilder();
|
private final List<MonitoredDirectory> allHandlers = new ArrayList<>();
|
|
private HandlerNotifier(File directory, int state)
|
{
|
this.directory = directory;
|
this.state = state;
|
}
|
|
private void notifyHandlers()
|
{
|
for (MonitoredDirectory mdElem : allHandlers)
|
{
|
switch (state)
|
{
|
case FULL:
|
mdElem.handler.diskFullThresholdReached(mdElem.getDirectory(), mdElem.getFullThreshold());
|
break;
|
case LOW:
|
mdElem.handler.diskLowThresholdReached(mdElem.getDirectory(), mdElem.getLowThreshold());
|
break;
|
case NORMAL:
|
mdElem.handler.diskSpaceRestored(mdElem.getDirectory(), mdElem.getLowThreshold(),
|
mdElem.getFullThreshold());
|
break;
|
}
|
}
|
}
|
|
private boolean isEmpty()
|
{
|
return allHandlers.isEmpty();
|
}
|
|
private void addHandler(MonitoredDirectory handler)
|
{
|
logger.trace("State change: %d -> %d", handler.lastState, state);
|
handler.lastState = state;
|
if (handler.handler != null)
|
{
|
allHandlers.add(handler);
|
}
|
appendName(diskNames, handler.instanceName);
|
}
|
|
private void appendName(StringBuilder strNames, String strVal)
|
{
|
if (strNames.length() > 0)
|
{
|
strNames.append(", ");
|
}
|
strNames.append(strVal);
|
}
|
}
|
|
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
|
|
private static final int NORMAL = 0;
|
private static final int LOW = 1;
|
private static final int FULL = 2;
|
private static final String INSTANCENAME = "Disk Space Monitor";
|
private final HashMap<File, List<MonitoredDirectory>> monitoredDirs = new HashMap<>();
|
|
/**
|
* Constructs a new DiskSpaceMonitor that will notify registered DiskSpaceMonitorHandler objects when filesystems
|
* on which configured directories reside, fall below the provided thresholds.
|
*/
|
public DiskSpaceMonitor()
|
{
|
}
|
|
/**
|
* Starts periodic monitoring of all registered directories.
|
*/
|
public void startDiskSpaceMonitor()
|
{
|
DirectoryServer.registerMonitorProvider(this);
|
DirectoryServer.registerShutdownListener(this);
|
scheduleUpdate(this, 0, 5, TimeUnit.SECONDS);
|
}
|
|
/**
|
* Registers or reconfigures a directory for monitoring.
|
* If possible, we will try to get and use the mountpoint where the directory resides and monitor it instead.
|
* If the directory is already registered for the same <code>handler</code>, simply change its configuration.
|
* @param instanceName A name for the handler, as used by cn=monitor
|
* @param directory The directory to monitor
|
* @param lowThresholdBytes Disk slow threshold expressed in bytes
|
* @param fullThresholdBytes Disk full threshold expressed in bytes
|
* @param handler The class requesting to be called when a transition in disk space occurs
|
*/
|
public void registerMonitoredDirectory(String instanceName, File directory, long lowThresholdBytes,
|
long fullThresholdBytes, DiskSpaceMonitorHandler handler)
|
{
|
File fsMountPoint;
|
try
|
{
|
fsMountPoint = getMountPoint(directory);
|
}
|
catch (IOException ioe)
|
{
|
logger.warn(ERR_DISK_SPACE_GET_MOUNT_POINT, directory.getAbsolutePath(), ioe.getLocalizedMessage());
|
fsMountPoint = directory;
|
}
|
MonitoredDirectory newDSH = new MonitoredDirectory(directory, instanceName, INSTANCENAME, handler);
|
newDSH.setFullThreshold(fullThresholdBytes);
|
newDSH.setLowThreshold(lowThresholdBytes);
|
|
synchronized (monitoredDirs)
|
{
|
List<MonitoredDirectory> diskHelpers = monitoredDirs.get(fsMountPoint);
|
if (diskHelpers == null)
|
{
|
monitoredDirs.put(fsMountPoint, newArrayList(newDSH));
|
}
|
else
|
{
|
for (MonitoredDirectory elem : diskHelpers)
|
{
|
if (elem.handler.equals(handler) && elem.getDirectory().equals(directory))
|
{
|
elem.setFullThreshold(fullThresholdBytes);
|
elem.setLowThreshold(lowThresholdBytes);
|
return;
|
}
|
}
|
diskHelpers.add(newDSH);
|
}
|
DirectoryServer.registerMonitorProvider(newDSH);
|
}
|
}
|
|
private File getMountPoint(File directory) throws IOException
|
{
|
Path mountPoint = directory.getAbsoluteFile().toPath();
|
Path parentDir = mountPoint.getParent();
|
FileStore dirFileStore = Files.getFileStore(mountPoint);
|
/*
|
* Since there is no concept of mount point in the APIs, iterate on all parents of
|
* the given directory until the FileSystem Store changes (hint of a different
|
* device, hence a mount point) or we get to root, which works too.
|
*/
|
while (parentDir != null)
|
{
|
if (!Files.getFileStore(parentDir).equals(dirFileStore))
|
{
|
return mountPoint.toFile();
|
}
|
mountPoint = mountPoint.getParent();
|
parentDir = parentDir.getParent();
|
}
|
return mountPoint.toFile();
|
}
|
|
/**
|
* Removes a directory from the set of monitored directories.
|
*
|
* @param directory The directory to stop monitoring on
|
* @param handler The class that requested monitoring
|
*/
|
public void deregisterMonitoredDirectory(File directory, DiskSpaceMonitorHandler handler)
|
{
|
synchronized (monitoredDirs)
|
{
|
|
List<MonitoredDirectory> directories = monitoredDirs.get(directory);
|
if (directories != null)
|
{
|
Iterator<MonitoredDirectory> itr = directories.iterator();
|
while (itr.hasNext())
|
{
|
MonitoredDirectory curDirectory = itr.next();
|
if (curDirectory.handler.equals(handler))
|
{
|
DirectoryServer.deregisterMonitorProvider(curDirectory);
|
itr.remove();
|
}
|
}
|
if (directories.isEmpty())
|
{
|
monitoredDirs.remove(directory);
|
}
|
}
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void initializeMonitorProvider(MonitorProviderCfg configuration)
|
throws ConfigException, InitializationException {
|
// Not used...
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public String getMonitorInstanceName() {
|
return INSTANCENAME;
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public List<Attribute> getMonitorData() {
|
return new ArrayList<>();
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void run()
|
{
|
List<HandlerNotifier> diskFull = new ArrayList<>();
|
List<HandlerNotifier> diskLow = new ArrayList<>();
|
List<HandlerNotifier> diskRestored = new ArrayList<>();
|
|
synchronized (monitoredDirs)
|
{
|
for (Entry<File, List<MonitoredDirectory>> dirElem : monitoredDirs.entrySet())
|
{
|
File directory = dirElem.getKey();
|
HandlerNotifier diskFullClients = new HandlerNotifier(directory, FULL);
|
HandlerNotifier diskLowClients = new HandlerNotifier(directory, LOW);
|
HandlerNotifier diskRestoredClients = new HandlerNotifier(directory, NORMAL);
|
try
|
{
|
long lastFreeSpace = directory.getUsableSpace();
|
for (MonitoredDirectory handlerElem : dirElem.getValue())
|
{
|
if (lastFreeSpace < handlerElem.getFullThreshold() && handlerElem.lastState < FULL)
|
{
|
diskFullClients.addHandler(handlerElem);
|
}
|
else if (lastFreeSpace < handlerElem.getLowThreshold() && handlerElem.lastState < LOW)
|
{
|
diskLowClients.addHandler(handlerElem);
|
}
|
else if (handlerElem.lastState != NORMAL)
|
{
|
diskRestoredClients.addHandler(handlerElem);
|
}
|
}
|
addToList(diskFull, diskFullClients);
|
addToList(diskLow, diskLowClients);
|
addToList(diskRestored, diskRestoredClients);
|
}
|
catch(Exception e)
|
{
|
logger.error(ERR_DISK_SPACE_MONITOR_UPDATE_FAILED, directory, e);
|
logger.traceException(e);
|
}
|
}
|
}
|
// It is probably better to notify handlers outside of the synchronized section.
|
sendNotification(diskFull, FULL, ALERT_DESCRIPTION_DISK_FULL);
|
sendNotification(diskLow, LOW, ALERT_TYPE_DISK_SPACE_LOW);
|
sendNotification(diskRestored, NORMAL, null);
|
}
|
|
private void addToList(List<HandlerNotifier> hnList, HandlerNotifier notifier)
|
{
|
if (!notifier.isEmpty())
|
{
|
hnList.add(notifier);
|
}
|
}
|
|
private void sendNotification(List<HandlerNotifier> diskList, int state, String alert)
|
{
|
for (HandlerNotifier dirElem : diskList)
|
{
|
String dirPath = dirElem.directory.getAbsolutePath();
|
String handlerNames = dirElem.diskNames.toString();
|
long freeSpace = dirElem.directory.getFreeSpace();
|
if (state == FULL)
|
{
|
DirectoryServer.sendAlertNotification(this, alert,
|
ERR_DISK_SPACE_FULL_THRESHOLD_REACHED.get(dirPath, handlerNames, freeSpace));
|
}
|
else if (state == LOW)
|
{
|
DirectoryServer.sendAlertNotification(this, alert,
|
ERR_DISK_SPACE_LOW_THRESHOLD_REACHED.get(dirPath, handlerNames, freeSpace));
|
}
|
else
|
{
|
logger.error(NOTE_DISK_SPACE_RESTORED.get(freeSpace, dirPath));
|
}
|
dirElem.notifyHandlers();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public DN getComponentEntryDN()
|
{
|
try
|
{
|
return DN.valueOf(INSTANCENAME);
|
}
|
catch (DirectoryException de)
|
{
|
return DN.NULL_DN;
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public String getClassName()
|
{
|
return DiskSpaceMonitor.class.getName();
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public Map<String, String> getAlerts()
|
{
|
Map<String, String> alerts = new LinkedHashMap<>();
|
alerts.put(ALERT_TYPE_DISK_SPACE_LOW, ALERT_DESCRIPTION_DISK_SPACE_LOW);
|
alerts.put(ALERT_TYPE_DISK_FULL, ALERT_DESCRIPTION_DISK_FULL);
|
return alerts;
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public String getShutdownListenerName()
|
{
|
return INSTANCENAME;
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void processServerShutdown(LocalizableMessage reason)
|
{
|
synchronized (monitoredDirs)
|
{
|
for (Entry<File, List<MonitoredDirectory>> dirElem : monitoredDirs.entrySet())
|
{
|
for (MonitoredDirectory handlerElem : dirElem.getValue())
|
{
|
DirectoryServer.deregisterMonitorProvider(handlerElem);
|
}
|
}
|
}
|
}
|
}
|