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