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}