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 2006-2008 Sun Microsystems, Inc.
015 * Portions Copyright 2014-2016 ForgeRock AS.
016 */
017package org.opends.server.tasks;
018
019import static org.opends.messages.TaskMessages.*;
020import static org.opends.messages.ToolMessages.*;
021import static org.opends.server.config.ConfigConstants.*;
022import static org.opends.server.core.DirectoryServer.*;
023import static org.opends.server.util.ServerConstants.*;
024import static org.opends.server.util.StaticUtils.*;
025
026import java.io.File;
027import java.text.SimpleDateFormat;
028import java.util.ArrayList;
029import java.util.Date;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033import java.util.TimeZone;
034
035import org.forgerock.i18n.LocalizableMessage;
036import org.forgerock.i18n.slf4j.LocalizedLogger;
037import org.forgerock.opendj.config.server.ConfigException;
038import org.forgerock.opendj.ldap.ResultCode;
039import org.forgerock.opendj.ldap.schema.AttributeType;
040import org.opends.messages.Severity;
041import org.opends.messages.TaskMessages;
042import org.opends.server.admin.std.server.BackendCfg;
043import org.opends.server.api.Backend;
044import org.opends.server.api.Backend.BackendOperation;
045import org.opends.server.api.ClientConnection;
046import org.opends.server.backends.task.Task;
047import org.opends.server.backends.task.TaskState;
048import org.opends.server.config.ConfigEntry;
049import org.opends.server.core.DirectoryServer;
050import org.opends.server.core.LockFileManager;
051import org.opends.server.types.BackupConfig;
052import org.opends.server.types.BackupDirectory;
053import org.opends.server.types.DirectoryException;
054import org.opends.server.types.Entry;
055import org.opends.server.types.Operation;
056import org.opends.server.types.Privilege;
057
058/**
059 * This class provides an implementation of a Directory Server task that may be
060 * used to back up a Directory Server backend in a binary form that may be
061 * quickly archived and restored.
062 */
063public class BackupTask extends Task
064{
065
066  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
067
068
069
070  /** Stores mapping between configuration attribute name and its label. */
071  private static Map<String,LocalizableMessage> argDisplayMap = new HashMap<>();
072  static {
073    argDisplayMap.put(ATTR_TASK_BACKUP_ALL, INFO_BACKUP_ARG_BACKUPALL.get());
074    argDisplayMap.put(ATTR_TASK_BACKUP_COMPRESS, INFO_BACKUP_ARG_COMPRESS.get());
075    argDisplayMap.put(ATTR_TASK_BACKUP_ENCRYPT, INFO_BACKUP_ARG_ENCRYPT.get());
076    argDisplayMap.put(ATTR_TASK_BACKUP_HASH, INFO_BACKUP_ARG_HASH.get());
077    argDisplayMap.put(ATTR_TASK_BACKUP_INCREMENTAL, INFO_BACKUP_ARG_INCREMENTAL.get());
078    argDisplayMap.put(ATTR_TASK_BACKUP_SIGN_HASH, INFO_BACKUP_ARG_SIGN_HASH.get());
079    argDisplayMap.put(ATTR_TASK_BACKUP_BACKEND_ID, INFO_BACKUP_ARG_BACKEND_IDS.get());
080    argDisplayMap.put(ATTR_BACKUP_ID, INFO_BACKUP_ARG_BACKUP_ID.get());
081    argDisplayMap.put(ATTR_BACKUP_DIRECTORY_PATH, INFO_BACKUP_ARG_BACKUP_DIR.get());
082    argDisplayMap.put(ATTR_TASK_BACKUP_INCREMENTAL_BASE_ID, INFO_BACKUP_ARG_INC_BASE_ID.get());
083  }
084
085
086  // The task arguments.
087  private boolean backUpAll;
088  private boolean compress;
089  private boolean encrypt;
090  private boolean hash;
091  private boolean incremental;
092  private boolean signHash;
093  private List<String>  backendIDList;
094  private String  backupID;
095  private File    backupDirectory;
096  private String  incrementalBase;
097
098  private BackupConfig backupConfig;
099
100  /**
101   * All the backend configuration entries defined in the server mapped
102   * by their backend ID.
103   */
104  private Map<String,ConfigEntry> configEntries;
105
106  private ArrayList<Backend<?>> backendsToArchive;
107
108  /** {@inheritDoc} */
109  @Override
110  public LocalizableMessage getDisplayName() {
111    return INFO_TASK_BACKUP_NAME.get();
112  }
113
114  /** {@inheritDoc} */
115  @Override
116  public LocalizableMessage getAttributeDisplayName(String attrName) {
117    return argDisplayMap.get(attrName);
118  }
119
120  /** {@inheritDoc} */
121  @Override
122  public void initializeTask() throws DirectoryException
123  {
124    // If the client connection is available, then make sure the associated
125    // client has the BACKEND_BACKUP privilege.
126    Operation operation = getOperation();
127    if (operation != null)
128    {
129      ClientConnection clientConnection = operation.getClientConnection();
130      if (! clientConnection.hasPrivilege(Privilege.BACKEND_BACKUP, operation))
131      {
132        LocalizableMessage message = ERR_TASK_BACKUP_INSUFFICIENT_PRIVILEGES.get();
133        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
134                                     message);
135      }
136    }
137
138
139    Entry taskEntry = getTaskEntry();
140
141    AttributeType typeBackupAll = getAttributeType(ATTR_TASK_BACKUP_ALL);
142    AttributeType typeCompress = getAttributeType(ATTR_TASK_BACKUP_COMPRESS);
143    AttributeType typeEncrypt = getAttributeType(ATTR_TASK_BACKUP_ENCRYPT);
144    AttributeType typeHash = getAttributeType(ATTR_TASK_BACKUP_HASH);
145    AttributeType typeIncremental = getAttributeType(ATTR_TASK_BACKUP_INCREMENTAL);
146    AttributeType typeSignHash = getAttributeType(ATTR_TASK_BACKUP_SIGN_HASH);
147    AttributeType typeBackendID = getAttributeType(ATTR_TASK_BACKUP_BACKEND_ID);
148    AttributeType typeBackupID = getAttributeType(ATTR_BACKUP_ID);
149    AttributeType typeBackupDirectory = getAttributeType(ATTR_BACKUP_DIRECTORY_PATH);
150    AttributeType typeIncrementalBaseID = getAttributeType(ATTR_TASK_BACKUP_INCREMENTAL_BASE_ID);
151
152    backUpAll = TaskUtils.getBoolean(taskEntry.getAttribute(typeBackupAll), false);
153    compress = TaskUtils.getBoolean(taskEntry.getAttribute(typeCompress), false);
154    encrypt = TaskUtils.getBoolean(taskEntry.getAttribute(typeEncrypt), false);
155    hash = TaskUtils.getBoolean(taskEntry.getAttribute(typeHash), false);
156    incremental = TaskUtils.getBoolean(taskEntry.getAttribute(typeIncremental), false);
157    signHash = TaskUtils.getBoolean(taskEntry.getAttribute(typeSignHash), false);
158    backendIDList = TaskUtils.getMultiValueString(taskEntry.getAttribute(typeBackendID));
159    backupID = TaskUtils.getSingleValueString(taskEntry.getAttribute(typeBackupID));
160
161    String backupDirectoryPath = TaskUtils.getSingleValueString(taskEntry.getAttribute(typeBackupDirectory));
162    backupDirectory = new File(backupDirectoryPath);
163    if (! backupDirectory.isAbsolute())
164    {
165      backupDirectory = new File(DirectoryServer.getInstanceRoot(), backupDirectoryPath);
166    }
167
168    incrementalBase = TaskUtils.getSingleValueString(taskEntry.getAttribute(typeIncrementalBaseID));
169
170    configEntries = TaskUtils.getBackendConfigEntries();
171  }
172
173
174  /**
175   * Validate the task arguments and construct the list of backends to be
176   * archived.
177   * @return  true if the task arguments are valid.
178   */
179  private boolean argumentsAreValid()
180  {
181    // Make sure that either the backUpAll argument was provided or at least one
182    // backend ID was given.  They are mutually exclusive.
183    if (backUpAll)
184    {
185      if (!backendIDList.isEmpty())
186      {
187        logger.error(ERR_BACKUPDB_CANNOT_MIX_BACKUP_ALL_AND_BACKEND_ID,
188            ATTR_TASK_BACKUP_ALL, ATTR_TASK_BACKUP_BACKEND_ID);
189        return false;
190      }
191    }
192    else if (backendIDList.isEmpty())
193    {
194      logger.error(ERR_BACKUPDB_NEED_BACKUP_ALL_OR_BACKEND_ID,
195          ATTR_TASK_BACKUP_ALL, ATTR_TASK_BACKUP_BACKEND_ID);
196      return false;
197    }
198
199
200    // Use task id for backup id in case of recurring task.
201    if (super.isRecurring()) {
202      backupID = super.getTaskID();
203    }
204
205
206    // If no backup ID was provided, then create one with the current timestamp.
207    if (backupID == null)
208    {
209      SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_GMT_TIME);
210      dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
211      backupID = dateFormat.format(new Date());
212    }
213
214
215    // If the incremental base ID was specified, then make sure it is an
216    // incremental backup.
217    if (incrementalBase != null && ! incremental)
218    {
219      logger.error(ERR_BACKUPDB_INCREMENTAL_BASE_REQUIRES_INCREMENTAL, ATTR_TASK_BACKUP_INCREMENTAL_BASE_ID,
220              ATTR_TASK_BACKUP_INCREMENTAL);
221      return false;
222    }
223
224
225    // If the signHash option was provided, then make sure that the hash option
226    // was given.
227    if (signHash && !hash)
228    {
229      logger.error(ERR_BACKUPDB_SIGN_REQUIRES_HASH, ATTR_TASK_BACKUP_SIGN_HASH, ATTR_TASK_BACKUP_HASH);
230      return false;
231    }
232
233
234    // Make sure that the backup directory exists.  If not, then create it.
235    if (! backupDirectory.exists())
236    {
237      try
238      {
239        backupDirectory.mkdirs();
240      }
241      catch (Exception e)
242      {
243        LocalizableMessage message = ERR_BACKUPDB_CANNOT_CREATE_BACKUP_DIR.get(
244                backupDirectory.getPath(), getExceptionMessage(e));
245        System.err.println(message);
246        return false;
247      }
248    }
249
250    int numBackends = configEntries.size();
251
252
253    backendsToArchive = new ArrayList<>(numBackends);
254
255    if (backUpAll)
256    {
257      for (Map.Entry<String,ConfigEntry> mapEntry : configEntries.entrySet())
258      {
259        Backend<?> b = DirectoryServer.getBackend(mapEntry.getKey());
260        if (b != null && b.supports(BackendOperation.BACKUP))
261        {
262          backendsToArchive.add(b);
263        }
264      }
265    }
266    else
267    {
268      // Iterate through the set of requested backends and make sure they can
269      // be used.
270      for (String id : backendIDList)
271      {
272        Backend<?> b = DirectoryServer.getBackend(id);
273        if (b == null || configEntries.get(id) == null)
274        {
275          logger.error(ERR_BACKUPDB_NO_BACKENDS_FOR_ID, id);
276        }
277        else if (!b.supports(BackendOperation.BACKUP))
278        {
279          logger.warn(WARN_BACKUPDB_BACKUP_NOT_SUPPORTED, b.getBackendID());
280        }
281        else
282        {
283          backendsToArchive.add(b);
284        }
285      }
286
287      // It is an error if any of the requested backends could not be used.
288      if (backendsToArchive.size() != backendIDList.size())
289      {
290        return false;
291      }
292    }
293
294
295    // If there are no backends to archive, then print an error and exit.
296    if (backendsToArchive.isEmpty())
297    {
298      logger.warn(WARN_BACKUPDB_NO_BACKENDS_TO_ARCHIVE);
299      return false;
300    }
301
302
303    return true;
304  }
305
306
307  /**
308   * Archive a single backend, where the backend is known to support backups.
309   * @param b The backend to be archived.
310   * @param backupLocation The backup directory.
311   * @return true if the backend was successfully archived.
312   */
313  private boolean backupBackend(Backend<?> b, File backupLocation)
314  {
315    // Get the config entry for this backend.
316    BackendCfg cfg = TaskUtils.getConfigEntry(b);
317
318
319    // If the directory doesn't exist, then create it.  If it does exist, then
320    // see if it has a backup descriptor file.
321    BackupDirectory backupDir;
322    if (backupLocation.exists())
323    {
324      String descriptorPath = backupLocation.getPath() + File.separator +
325                              BACKUP_DIRECTORY_DESCRIPTOR_FILE;
326      File descriptorFile = new File(descriptorPath);
327      if (descriptorFile.exists())
328      {
329        try
330        {
331          backupDir = BackupDirectory.readBackupDirectoryDescriptor(
332               backupLocation.getPath());
333
334          // Check the current backup directory corresponds to the provided
335          // backend
336          if (! backupDir.getConfigEntryDN().equals(cfg.dn()))
337          {
338            logger.error(ERR_BACKUPDB_CANNOT_BACKUP_IN_DIRECTORY, b.getBackendID(), backupLocation.getPath(),
339                backupDir.getConfigEntryDN().rdn().getFirstAVA().getAttributeValue());
340            return false ;
341          }
342        }
343        catch (ConfigException ce)
344        {
345          logger.error(ERR_BACKUPDB_CANNOT_PARSE_BACKUP_DESCRIPTOR, descriptorPath, ce.getMessage());
346          return false;
347        }
348        catch (Exception e)
349        {
350          logger.error(ERR_BACKUPDB_CANNOT_PARSE_BACKUP_DESCRIPTOR, descriptorPath, getExceptionMessage(e));
351          return false;
352        }
353      }
354      else
355      {
356        backupDir = new BackupDirectory(backupLocation.getPath(), cfg.dn());
357      }
358    }
359    else
360    {
361      try
362      {
363        backupLocation.mkdirs();
364      }
365      catch (Exception e)
366      {
367        logger.error(ERR_BACKUPDB_CANNOT_CREATE_BACKUP_DIR, backupLocation.getPath(), getExceptionMessage(e));
368        return false;
369      }
370
371      backupDir = new BackupDirectory(backupLocation.getPath(),
372                                      cfg.dn());
373    }
374
375
376    // Create a backup configuration.
377    backupConfig = new BackupConfig(backupDir, backupID,
378                                                 incremental);
379    backupConfig.setCompressData(compress);
380    backupConfig.setEncryptData(encrypt);
381    backupConfig.setHashData(hash);
382    backupConfig.setSignHash(signHash);
383    backupConfig.setIncrementalBaseID(incrementalBase);
384
385
386    // Perform the backup.
387    try
388    {
389      DirectoryServer.notifyBackupBeginning(b, backupConfig);
390      b.createBackup(backupConfig);
391      DirectoryServer.notifyBackupEnded(b, backupConfig, true);
392    }
393    catch (DirectoryException de)
394    {
395      DirectoryServer.notifyBackupEnded(b, backupConfig, false);
396      logger.error(ERR_BACKUPDB_ERROR_DURING_BACKUP, b.getBackendID(), de.getMessageObject());
397      return false;
398    }
399    catch (Exception e)
400    {
401      DirectoryServer.notifyBackupEnded(b, backupConfig, false);
402      logger.error(ERR_BACKUPDB_ERROR_DURING_BACKUP, b.getBackendID(), getExceptionMessage(e));
403      return false;
404    }
405
406    return true;
407  }
408
409  /**
410   * Acquire a shared lock on a backend.
411   * @param b The backend on which the lock is to be acquired.
412   * @return true if the lock was successfully acquired.
413   */
414  private boolean lockBackend(Backend<?> b)
415  {
416    try
417    {
418      String        lockFile      = LockFileManager.getBackendLockFileName(b);
419      StringBuilder failureReason = new StringBuilder();
420      if (! LockFileManager.acquireSharedLock(lockFile, failureReason))
421      {
422        logger.error(ERR_BACKUPDB_CANNOT_LOCK_BACKEND, b.getBackendID(), failureReason);
423        return false;
424      }
425    }
426    catch (Exception e)
427    {
428      logger.error(ERR_BACKUPDB_CANNOT_LOCK_BACKEND, b.getBackendID(), getExceptionMessage(e));
429      return false;
430    }
431
432    return true;
433  }
434
435  /**
436   * Release a lock on a backend.
437   * @param b The backend on which the lock is held.
438   * @return true if the lock was successfully released.
439   */
440  private boolean unlockBackend(Backend<?> b)
441  {
442    try
443    {
444      String lockFile = LockFileManager.getBackendLockFileName(b);
445      StringBuilder failureReason = new StringBuilder();
446      if (! LockFileManager.releaseLock(lockFile, failureReason))
447      {
448        logger.warn(WARN_BACKUPDB_CANNOT_UNLOCK_BACKEND, b.getBackendID(), failureReason);
449        return false;
450      }
451    }
452    catch (Exception e)
453    {
454      logger.warn(WARN_BACKUPDB_CANNOT_UNLOCK_BACKEND, b.getBackendID(), getExceptionMessage(e));
455      return false;
456    }
457
458    return true;
459  }
460
461
462  /** {@inheritDoc} */
463  @Override
464  public void interruptTask(TaskState interruptState, LocalizableMessage interruptReason)
465  {
466    if (TaskState.STOPPED_BY_ADMINISTRATOR.equals(interruptState) &&
467            backupConfig != null)
468    {
469      addLogMessage(Severity.INFORMATION, TaskMessages.INFO_TASK_STOPPED_BY_ADMIN.get(
470      interruptReason));
471      setTaskInterruptState(interruptState);
472      backupConfig.cancel();
473    }
474  }
475
476
477  /** {@inheritDoc} */
478  @Override
479  public boolean isInterruptable() {
480    return true;
481  }
482
483
484  /** {@inheritDoc} */
485  @Override
486  protected TaskState runTask()
487  {
488    if (!argumentsAreValid())
489    {
490      return TaskState.STOPPED_BY_ERROR;
491    }
492
493    boolean multiple;
494    if (backUpAll)
495    {
496      // We'll proceed as if we're backing up multiple backends in this case
497      // even if there's just one.
498      multiple = true;
499    }
500    else
501    {
502      // See if there are multiple backends to archive.
503      multiple = backendsToArchive.size() > 1;
504    }
505
506
507    // Iterate through the backends to archive and back them up individually.
508    boolean errorsEncountered = false;
509    for (Backend<?> b : backendsToArchive)
510    {
511      if (isCancelled())
512      {
513        break;
514      }
515
516      // Acquire a shared lock for this backend.
517      if (!lockBackend(b))
518      {
519        errorsEncountered = true;
520        continue;
521      }
522
523
524      try
525      {
526        logger.info(NOTE_BACKUPDB_STARTING_BACKUP, b.getBackendID());
527
528
529        // Get the path to the directory to use for this backup.  If we will be
530        // backing up multiple backends (or if we are backing up all backends,
531        // even if there's only one of them), then create a subdirectory for
532        // each
533        // backend.
534        File backupLocation;
535        if (multiple)
536        {
537          backupLocation = new File(backupDirectory, b.getBackendID());
538        }
539        else
540        {
541          backupLocation = backupDirectory;
542        }
543
544
545        if (!backupBackend(b, backupLocation))
546        {
547          errorsEncountered = true;
548        }
549      }
550      finally
551      {
552        // Release the shared lock for the backend.
553        if (!unlockBackend(b))
554        {
555          errorsEncountered = true;
556        }
557      }
558    }
559
560
561    // Print a final completed message, indicating whether there were any errors
562    // in the process.  In this case it means that the backup could not be
563    // completed at least for one of the backends.
564    if (errorsEncountered)
565    {
566      logger.info(NOTE_BACKUPDB_COMPLETED_WITH_ERRORS);
567      return TaskState.STOPPED_BY_ERROR;
568    }
569    else if (isCancelled())
570    {
571      logger.info(NOTE_BACKUPDB_CANCELLED);
572      return getTaskInterruptState();
573    }
574    else
575    {
576      logger.info(NOTE_BACKUPDB_COMPLETED_SUCCESSFULLY);
577      return TaskState.COMPLETED_SUCCESSFULLY;
578    }
579  }
580
581
582}