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