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}