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}