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-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2012-2016 ForgeRock AS.
016 */
017package org.opends.server.tools;
018
019import static org.opends.messages.ToolMessages.*;
020import static org.opends.server.config.ConfigConstants.*;
021import static org.opends.server.util.StaticUtils.*;
022import static com.forgerock.opendj.cli.ArgumentConstants.*;
023import static com.forgerock.opendj.cli.CommonArguments.*;
024import static com.forgerock.opendj.cli.Utils.*;
025
026import java.io.OutputStream;
027import java.io.PrintStream;
028import java.text.DateFormat;
029import java.text.SimpleDateFormat;
030import java.util.ArrayList;
031import java.util.List;
032import java.util.Set;
033
034import org.forgerock.i18n.LocalizableMessage;
035import org.forgerock.i18n.slf4j.LocalizedLogger;
036import org.forgerock.opendj.ldap.DN;
037import org.forgerock.opendj.server.config.server.BackendCfg;
038import org.forgerock.util.Utils;
039import org.opends.server.api.Backend;
040import org.opends.server.api.Backend.BackendOperation;
041import org.opends.server.core.DirectoryServer;
042import org.opends.server.core.LockFileManager;
043import org.opends.server.loggers.JDKLogging;
044import org.opends.server.protocols.ldap.LDAPAttribute;
045import org.opends.server.tasks.RestoreTask;
046import org.opends.server.tools.tasks.TaskTool;
047import org.opends.server.types.BackupDirectory;
048import org.opends.server.types.BackupInfo;
049import org.opends.server.types.DirectoryException;
050import org.opends.server.types.InitializationException;
051import org.opends.server.types.NullOutputStream;
052import org.opends.server.types.RawAttribute;
053import org.opends.server.types.RestoreConfig;
054import org.opends.server.util.cli.LDAPConnectionArgumentParser;
055
056import com.forgerock.opendj.cli.Argument;
057import com.forgerock.opendj.cli.ArgumentException;
058import com.forgerock.opendj.cli.BooleanArgument;
059import com.forgerock.opendj.cli.ClientException;
060import com.forgerock.opendj.cli.StringArgument;
061
062/**
063 * This program provides a utility that may be used to restore a binary backup
064 * of a Directory Server backend generated using the BackUpDB tool.  This will
065 * be a process that is intended to run separate from Directory Server and not
066 * internally within the server process (e.g., via the tasks interface).
067 */
068public class RestoreDB extends TaskTool {
069
070  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
071
072  /**
073   * The main method for RestoreDB tool.
074   *
075   * @param  args  The command-line arguments provided to this program.
076   */
077
078  public static void main(String[] args)
079  {
080    int retCode = mainRestoreDB(args, true, System.out, System.err);
081
082    if(retCode != 0)
083    {
084      System.exit(filterExitCode(retCode));
085    }
086  }
087
088  /**
089   * Processes the command-line arguments and invokes the restore process.
090   *
091   * @param  args  The command-line arguments provided to this program.
092   *
093   * @return The error code.
094   */
095  public static int mainRestoreDB(String[] args)
096  {
097    return mainRestoreDB(args, true, System.out, System.err);
098  }
099
100  /**
101   * Processes the command-line arguments and invokes the restore process.
102   *
103   * @param  args              The command-line arguments provided to this
104   *                           program.
105   * @param  initializeServer  Indicates whether to initialize the server.
106   * @param  outStream         The output stream to use for standard output, or
107   *                           {@code null} if standard output is not needed.
108   * @param  errStream         The output stream to use for standard error, or
109   *                           {@code null} if standard error is not needed.
110   *
111   * @return The error code.
112   */
113  public static int mainRestoreDB(String[] args, boolean initializeServer,
114                                  OutputStream outStream,
115                                  OutputStream errStream)
116  {
117    RestoreDB tool = new RestoreDB();
118    return tool.process(args, initializeServer, outStream, errStream);
119  }
120
121
122  /** Define the command-line arguments that may be used with this program. */
123  private BooleanArgument displayUsage;
124  private BooleanArgument listBackups;
125  private BooleanArgument verifyOnly;
126  private StringArgument  backupIDString;
127  private StringArgument  configFile;
128  private StringArgument  backupDirectory;
129
130
131  private int process(String[] args, boolean initializeServer,
132                      OutputStream outStream, OutputStream errStream)
133  {
134    PrintStream out = NullOutputStream.wrapOrNullStream(outStream);
135    PrintStream err = NullOutputStream.wrapOrNullStream(errStream);
136    JDKLogging.disableLogging();
137
138    // Create the command-line argument parser for use with this program.
139    LDAPConnectionArgumentParser argParser =
140            createArgParser("org.opends.server.tools.RestoreDB",
141                            INFO_RESTOREDB_TOOL_DESCRIPTION.get());
142
143
144    // Initialize all the command-line argument types and register them with the
145    // parser.
146    try
147    {
148      argParser.setShortToolDescription(REF_SHORT_DESC_RESTORE.get());
149
150      configFile =
151              StringArgument.builder("configFile")
152                      .shortIdentifier('f')
153                      .description(INFO_DESCRIPTION_CONFIG_FILE.get())
154                      .hidden()
155                      .required()
156                      .valuePlaceholder(INFO_CONFIGFILE_PLACEHOLDER.get())
157                      .buildAndAddToParser(argParser);
158      backupIDString =
159              StringArgument.builder("backupID")
160                      .shortIdentifier('I')
161                      .description(INFO_RESTOREDB_DESCRIPTION_BACKUP_ID.get())
162                      .valuePlaceholder(INFO_BACKUPID_PLACEHOLDER.get())
163                      .buildAndAddToParser(argParser);
164      backupDirectory =
165              StringArgument.builder("backupDirectory")
166                      .shortIdentifier('d')
167                      .description(INFO_RESTOREDB_DESCRIPTION_BACKUP_DIR.get())
168                      .required()
169                      .valuePlaceholder(INFO_BACKUPDIR_PLACEHOLDER.get())
170                      .buildAndAddToParser(argParser);
171      listBackups =
172              BooleanArgument.builder("listBackups")
173                      .shortIdentifier('l')
174                      .description(INFO_RESTOREDB_DESCRIPTION_LIST_BACKUPS.get())
175                      .buildAndAddToParser(argParser);
176      verifyOnly =
177              BooleanArgument.builder(OPTION_LONG_DRYRUN)
178                      .shortIdentifier(OPTION_SHORT_DRYRUN)
179                      .description(INFO_RESTOREDB_DESCRIPTION_VERIFY_ONLY.get())
180                      .buildAndAddToParser(argParser);
181
182      displayUsage = showUsageArgument();
183      argParser.addArgument(displayUsage);
184      argParser.setUsageArgument(displayUsage);
185    }
186    catch (ArgumentException ae)
187    {
188      printWrappedText(err, ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
189      return 1;
190    }
191
192    // Init the default values so that they can appear also on the usage.
193    argParser.getArguments().initArgumentsWithConfiguration(argParser);
194
195    // Parse the command-line arguments provided to this program.
196    try
197    {
198      argParser.parseArguments(args);
199      validateTaskArgs();
200    }
201    catch (ArgumentException ae)
202    {
203      argParser.displayMessageAndUsageReference(err, ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
204      return 1;
205    }
206    catch (ClientException ce)
207    {
208      // No need to display the usage since the problem comes with a provided value.
209      printWrappedText(err, ce.getMessageObject());
210      return 1;
211    }
212
213
214    // If we should just display usage or version information,
215    // then print it and exit.
216    if (argParser.usageOrVersionDisplayed())
217    {
218      return 0;
219    }
220
221
222    if (listBackups.isPresent() && argParser.connectionArgumentsPresent()) {
223      printWrappedText(err, ERR_LDAP_CONN_INCOMPATIBLE_ARGS.get(listBackups.getLongIdentifier()));
224      return 1;
225    }
226
227    // Checks the version - if upgrade required, the tool is unusable
228    try
229    {
230      checkVersion();
231    }
232    catch (InitializationException e)
233    {
234      printWrappedText(err, e.getMessage());
235      return 1;
236    }
237
238    return process(argParser, initializeServer, out, err);
239  }
240
241
242  /** {@inheritDoc} */
243  @Override
244  public void addTaskAttributes(List<RawAttribute> attributes)
245  {
246    addAttribute(attributes, ATTR_BACKUP_DIRECTORY_PATH, backupDirectory);
247    addAttribute(attributes, ATTR_BACKUP_ID, backupIDString);
248    addAttribute(attributes, ATTR_TASK_RESTORE_VERIFY_ONLY, verifyOnly);
249  }
250
251  private void addAttribute(List<RawAttribute> attributes, String attrName, Argument arg)
252  {
253    if (arg.getValue() != null && !arg.getValue().equals(arg.getDefaultValue()))
254    {
255      attributes.add(new LDAPAttribute(attrName, arg.getValue()));
256    }
257  }
258
259  /** {@inheritDoc} */
260  @Override
261  public String getTaskObjectclass() {
262    return "ds-task-restore";
263  }
264
265  /** {@inheritDoc} */
266  @Override
267  public Class<?> getTaskClass() {
268    return RestoreTask.class;
269  }
270
271  /** {@inheritDoc} */
272  @Override
273  protected int processLocal(boolean initializeServer,
274                           PrintStream out,
275                           PrintStream err) {
276
277
278    if (initializeServer)
279    {
280      try
281      {
282        new DirectoryServer.InitializationBuilder(configFile.getValue())
283            .requireErrorAndDebugLogPublisher(out, err)
284            .initialize();
285      }
286      catch (InitializationException ie)
287      {
288        printWrappedText(err, ERR_CANNOT_INITIALIZE_SERVER_COMPONENTS.get(ie.getLocalizedMessage()));
289        return 1;
290      }
291    }
292
293
294    // Open the backup directory and make sure it is valid.
295    BackupDirectory backupDir;
296    try
297    {
298      backupDir = BackupDirectory.readBackupDirectoryDescriptor(
299                       backupDirectory.getValue());
300    }
301    catch (Exception e)
302    {
303      logger.error(ERR_RESTOREDB_CANNOT_READ_BACKUP_DIRECTORY, backupDirectory.getValue(), getExceptionMessage(e));
304      return 1;
305    }
306
307
308    // If we're just going to be listing backups, then do that now.
309    DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_LOCAL_TIME);
310    if (listBackups.isPresent())
311    {
312      for (BackupInfo backupInfo : backupDir.getBackups().values())
313      {
314        LocalizableMessage message = INFO_RESTOREDB_LIST_BACKUP_ID.get(
315                backupInfo.getBackupID());
316        out.println(message);
317
318        message = INFO_RESTOREDB_LIST_BACKUP_DATE.get(
319                dateFormat.format(backupInfo.getBackupDate()));
320        out.println(message);
321
322        message = INFO_RESTOREDB_LIST_INCREMENTAL.get(backupInfo.isIncremental());
323        out.println(message);
324
325        message = INFO_RESTOREDB_LIST_COMPRESSED.get(backupInfo.isCompressed());
326        out.println(message);
327
328        message = INFO_RESTOREDB_LIST_ENCRYPTED.get(backupInfo.isEncrypted());
329        out.println(message);
330
331        byte[] hash = backupInfo.getUnsignedHash();
332        message = INFO_RESTOREDB_LIST_HASHED.get(hash != null);
333        out.println(message);
334
335        byte[] signature = backupInfo.getSignedHash();
336        message = INFO_RESTOREDB_LIST_SIGNED.get(signature != null);
337        out.println(message);
338
339        StringBuilder dependencyList = new StringBuilder();
340        Set<String> dependencyIDs = backupInfo.getDependencies();
341        if (! dependencyIDs.isEmpty())
342        {
343          Utils.joinAsString(dependencyList, ", ", dependencyIDs);
344        }
345        else
346        {
347          dependencyList.append("none");
348        }
349
350
351        message = INFO_RESTOREDB_LIST_DEPENDENCIES.get(dependencyList);
352        out.println(message);
353        out.println();
354      }
355
356      return 0;
357    }
358
359
360    // If a backup ID was specified, then make sure it is valid.  If none was
361    // provided, then choose the latest backup from the archive.  Encrypted
362    // or signed backups cannot be restored to a local (offline) server
363    // instance.
364    String backupID;
365    {
366      BackupInfo backupInfo = backupDir.getLatestBackup();
367      if (backupInfo == null)
368      {
369        logger.error(ERR_RESTOREDB_NO_BACKUPS_IN_DIRECTORY, backupDirectory.getValue());
370        return 1;
371      }
372      backupID = backupInfo.getBackupID();
373      if (backupIDString.isPresent())
374      {
375        backupID = backupIDString.getValue();
376        backupInfo = backupDir.getBackupInfo(backupID);
377        if (backupInfo == null)
378        {
379          logger.error(ERR_RESTOREDB_INVALID_BACKUP_ID, backupID, backupDirectory.getValue());
380          return 1;
381        }
382      }
383      if (backupInfo.isEncrypted() || null != backupInfo.getSignedHash()) {
384        logger.error(ERR_RESTOREDB_ENCRYPT_OR_SIGN_REQUIRES_ONLINE);
385        return 1;
386      }
387    }
388
389
390    // Get the DN of the backend configuration entry from the backup and load
391    // the associated backend from the configuration.
392    DN configEntryDN = backupDir.getConfigEntryDN();
393
394
395    // Get information about the backends defined in the server and determine
396    // which to use for the restore.
397    List<Backend<?>> backendList = new ArrayList<>();
398    List<BackendCfg> entryList = new ArrayList<>();
399    List<List<DN>> dnList = new ArrayList<>();
400    BackendToolUtils.getBackends(backendList, entryList, dnList);
401
402
403    Backend<?> backend = null;
404    int numBackends = backendList.size();
405    for (int i=0; i < numBackends; i++)
406    {
407      Backend<?> b = backendList.get(i);
408      BackendCfg e = entryList.get(i);
409      if (e.dn().equals(configEntryDN))
410      {
411        backend     = b;
412        break;
413      }
414    }
415
416    if (backend == null)
417    {
418      logger.error(ERR_RESTOREDB_NO_BACKENDS_FOR_DN, backupDirectory.getValue(), configEntryDN);
419      return 1;
420    }
421    else if (!backend.supports(BackendOperation.RESTORE))
422    {
423      logger.error(ERR_RESTOREDB_CANNOT_RESTORE, backend.getBackendID());
424      return 1;
425    }
426
427
428    // Create the restore config object from the information available.
429    RestoreConfig restoreConfig = new RestoreConfig(backupDir, backupID,
430                                                    verifyOnly.isPresent());
431
432
433    // Acquire an exclusive lock for the backend.
434    try
435    {
436      String lockFile = LockFileManager.getBackendLockFileName(backend);
437      StringBuilder failureReason = new StringBuilder();
438      if (! LockFileManager.acquireExclusiveLock(lockFile, failureReason))
439      {
440        logger.error(ERR_RESTOREDB_CANNOT_LOCK_BACKEND, backend.getBackendID(), failureReason);
441        return 1;
442      }
443    }
444    catch (Exception e)
445    {
446      logger.error(ERR_RESTOREDB_CANNOT_LOCK_BACKEND, backend.getBackendID(), getExceptionMessage(e));
447      return 1;
448    }
449
450
451    // Perform the restore.
452    try
453    {
454      backend.restoreBackup(restoreConfig);
455    }
456    catch (DirectoryException de)
457    {
458      logger.error(ERR_RESTOREDB_ERROR_DURING_BACKUP, backupID, backupDir.getPath(), de.getMessageObject());
459    }
460    catch (Exception e)
461    {
462      logger.error(ERR_RESTOREDB_ERROR_DURING_BACKUP, backupID, backupDir.getPath(), getExceptionMessage(e));
463    }
464
465
466    // Release the exclusive lock on the backend.
467    try
468    {
469      String lockFile = LockFileManager.getBackendLockFileName(backend);
470      StringBuilder failureReason = new StringBuilder();
471      if (! LockFileManager.releaseLock(lockFile, failureReason))
472      {
473        logger.warn(WARN_RESTOREDB_CANNOT_UNLOCK_BACKEND, backend.getBackendID(), failureReason);
474      }
475    }
476    catch (Exception e)
477    {
478      logger.warn(WARN_RESTOREDB_CANNOT_UNLOCK_BACKEND, backend.getBackendID(), getExceptionMessage(e));
479    }
480    return 0;
481  }
482
483  /** {@inheritDoc} */
484  @Override
485  public String getTaskId() {
486    return backupIDString != null? backupIDString.getValue() : null;
487  }
488}