001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2012-2015 ForgeRock AS.
026 */
027package org.opends.server.tools;
028
029import static org.opends.messages.ToolMessages.*;
030import static org.opends.server.config.ConfigConstants.*;
031import static org.opends.server.util.StaticUtils.*;
032
033import static com.forgerock.opendj.cli.ArgumentConstants.*;
034import static com.forgerock.opendj.cli.Utils.*;
035
036import java.io.OutputStream;
037import java.io.PrintStream;
038import java.text.DateFormat;
039import java.text.SimpleDateFormat;
040import java.util.ArrayList;
041import java.util.HashSet;
042import java.util.Iterator;
043import java.util.List;
044
045import org.forgerock.i18n.LocalizableMessage;
046import org.forgerock.i18n.slf4j.LocalizedLogger;
047import org.forgerock.opendj.config.server.ConfigException;
048import org.opends.server.admin.std.server.BackendCfg;
049import org.opends.server.api.Backend;
050import org.opends.server.api.Backend.BackendOperation;
051import org.opends.server.core.CoreConfigManager;
052import org.opends.server.core.DirectoryServer;
053import org.opends.server.core.LockFileManager;
054import org.opends.server.extensions.ConfigFileHandler;
055import org.opends.server.loggers.DebugLogger;
056import org.opends.server.loggers.ErrorLogPublisher;
057import org.opends.server.loggers.ErrorLogger;
058import org.opends.server.loggers.JDKLogging;
059import org.opends.server.loggers.TextErrorLogPublisher;
060import org.opends.server.loggers.TextWriter;
061import org.opends.server.protocols.ldap.LDAPAttribute;
062import org.opends.server.tasks.RestoreTask;
063import org.opends.server.tools.tasks.TaskTool;
064import org.opends.server.types.BackupDirectory;
065import org.opends.server.types.BackupInfo;
066import org.opends.server.types.DN;
067import org.opends.server.types.DirectoryException;
068import org.opends.server.types.InitializationException;
069import org.opends.server.types.NullOutputStream;
070import org.opends.server.types.RawAttribute;
071import org.opends.server.types.RestoreConfig;
072import org.opends.server.util.args.LDAPConnectionArgumentParser;
073
074import com.forgerock.opendj.cli.Argument;
075import com.forgerock.opendj.cli.ArgumentException;
076import com.forgerock.opendj.cli.BooleanArgument;
077import com.forgerock.opendj.cli.ClientException;
078import com.forgerock.opendj.cli.CommonArguments;
079import com.forgerock.opendj.cli.StringArgument;
080
081/**
082 * This program provides a utility that may be used to restore a binary backup
083 * of a Directory Server backend generated using the BackUpDB tool.  This will
084 * be a process that is intended to run separate from Directory Server and not
085 * internally within the server process (e.g., via the tasks interface).
086 */
087public class RestoreDB extends TaskTool {
088
089  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
090
091  /**
092   * The main method for RestoreDB tool.
093   *
094   * @param  args  The command-line arguments provided to this program.
095   */
096
097  public static void main(String[] args)
098  {
099    int retCode = mainRestoreDB(args, true, System.out, System.err);
100
101    if(retCode != 0)
102    {
103      System.exit(filterExitCode(retCode));
104    }
105  }
106
107  /**
108   * Processes the command-line arguments and invokes the restore process.
109   *
110   * @param  args  The command-line arguments provided to this program.
111   *
112   * @return The error code.
113   */
114  public static int mainRestoreDB(String[] args)
115  {
116    return mainRestoreDB(args, true, System.out, System.err);
117  }
118
119  /**
120   * Processes the command-line arguments and invokes the restore process.
121   *
122   * @param  args              The command-line arguments provided to this
123   *                           program.
124   * @param  initializeServer  Indicates whether to initialize the server.
125   * @param  outStream         The output stream to use for standard output, or
126   *                           {@code null} if standard output is not needed.
127   * @param  errStream         The output stream to use for standard error, or
128   *                           {@code null} if standard error is not needed.
129   *
130   * @return The error code.
131   */
132  public static int mainRestoreDB(String[] args, boolean initializeServer,
133                                  OutputStream outStream,
134                                  OutputStream errStream)
135  {
136    RestoreDB tool = new RestoreDB();
137    return tool.process(args, initializeServer, outStream, errStream);
138  }
139
140
141  /** Define the command-line arguments that may be used with this program. */
142  private BooleanArgument displayUsage;
143  private BooleanArgument listBackups;
144  private BooleanArgument verifyOnly;
145  private StringArgument  backupIDString;
146  private StringArgument  configClass;
147  private StringArgument  configFile;
148  private StringArgument  backupDirectory;
149
150
151  private int process(String[] args, boolean initializeServer,
152                      OutputStream outStream, OutputStream errStream)
153  {
154    PrintStream out = NullOutputStream.wrapOrNullStream(outStream);
155    PrintStream err = NullOutputStream.wrapOrNullStream(errStream);
156    JDKLogging.disableLogging();
157
158    // Create the command-line argument parser for use with this program.
159    LDAPConnectionArgumentParser argParser =
160            createArgParser("org.opends.server.tools.RestoreDB",
161                            INFO_RESTOREDB_TOOL_DESCRIPTION.get());
162
163
164    // Initialize all the command-line argument types and register them with the
165    // parser.
166    try
167    {
168      argParser.setShortToolDescription(REF_SHORT_DESC_RESTORE.get());
169
170      configClass =
171           new StringArgument("configclass", OPTION_SHORT_CONFIG_CLASS,
172                              OPTION_LONG_CONFIG_CLASS, true, false,
173                              true, INFO_CONFIGCLASS_PLACEHOLDER.get(),
174                              ConfigFileHandler.class.getName(), null,
175                              INFO_DESCRIPTION_CONFIG_CLASS.get());
176      configClass.setHidden(true);
177      argParser.addArgument(configClass);
178
179
180      configFile =
181           new StringArgument("configfile", 'f', "configFile", true, false,
182                              true, INFO_CONFIGFILE_PLACEHOLDER.get(), null,
183                              null,
184                              INFO_DESCRIPTION_CONFIG_FILE.get());
185      configFile.setHidden(true);
186      argParser.addArgument(configFile);
187
188      backupIDString =
189           new StringArgument("backupid", 'I', "backupID", false, false, true,
190                              INFO_BACKUPID_PLACEHOLDER.get(), null, null,
191                              INFO_RESTOREDB_DESCRIPTION_BACKUP_ID.get());
192      argParser.addArgument(backupIDString);
193
194
195      backupDirectory =
196           new StringArgument("backupdirectory", 'd', "backupDirectory", true,
197                              false, true, INFO_BACKUPDIR_PLACEHOLDER.get(),
198                              null, null,
199                              INFO_RESTOREDB_DESCRIPTION_BACKUP_DIR.get());
200      argParser.addArgument(backupDirectory);
201
202
203      listBackups = new BooleanArgument(
204              "listbackups", 'l', "listBackups",
205              INFO_RESTOREDB_DESCRIPTION_LIST_BACKUPS.get());
206      argParser.addArgument(listBackups);
207
208
209      verifyOnly = new BooleanArgument(
210              "verifyonly", OPTION_SHORT_DRYRUN,
211              OPTION_LONG_DRYRUN,
212              INFO_RESTOREDB_DESCRIPTION_VERIFY_ONLY.get());
213      argParser.addArgument(verifyOnly);
214
215
216      displayUsage = CommonArguments.getShowUsage();
217      argParser.addArgument(displayUsage);
218      argParser.setUsageArgument(displayUsage);
219    }
220    catch (ArgumentException ae)
221    {
222      printWrappedText(err, ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
223      return 1;
224    }
225
226    // Init the default values so that they can appear also on the usage.
227    try
228    {
229      argParser.getArguments().initArgumentsWithConfiguration();
230    }
231    catch (ConfigException ce)
232    {
233      // Ignore.
234    }
235
236    // Parse the command-line arguments provided to this program.
237    try
238    {
239      argParser.parseArguments(args);
240      validateTaskArgs();
241    }
242    catch (ArgumentException ae)
243    {
244      argParser.displayMessageAndUsageReference(err, ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
245      return 1;
246    }
247    catch (ClientException ce)
248    {
249      // No need to display the usage since the problem comes with a provided value.
250      printWrappedText(err, ce.getMessageObject());
251      return 1;
252    }
253
254
255    // If we should just display usage or version information,
256    // then print it and exit.
257    if (argParser.usageOrVersionDisplayed())
258    {
259      return 0;
260    }
261
262
263    if (listBackups.isPresent() && argParser.connectionArgumentsPresent()) {
264      printWrappedText(err, ERR_LDAP_CONN_INCOMPATIBLE_ARGS.get(listBackups.getLongIdentifier()));
265      return 1;
266    }
267
268    // Checks the version - if upgrade required, the tool is unusable
269    try
270    {
271      checkVersion();
272    }
273    catch (InitializationException e)
274    {
275      printWrappedText(err, e.getMessage());
276      return 1;
277    }
278
279    return process(argParser, initializeServer, out, err);
280  }
281
282
283  /** {@inheritDoc} */
284  @Override
285  public void addTaskAttributes(List<RawAttribute> attributes)
286  {
287    addAttribute(attributes, ATTR_BACKUP_DIRECTORY_PATH, backupDirectory);
288    addAttribute(attributes, ATTR_BACKUP_ID, backupIDString);
289    addAttribute(attributes, ATTR_TASK_RESTORE_VERIFY_ONLY, verifyOnly);
290  }
291
292  private void addAttribute(List<RawAttribute> attributes, String attrName, Argument arg)
293  {
294    if (arg.getValue() != null && !arg.getValue().equals(arg.getDefaultValue()))
295    {
296      attributes.add(new LDAPAttribute(attrName, arg.getValue()));
297    }
298  }
299
300  /** {@inheritDoc} */
301  @Override
302  public String getTaskObjectclass() {
303    return "ds-task-restore";
304  }
305
306  /** {@inheritDoc} */
307  @Override
308  public Class<?> getTaskClass() {
309    return RestoreTask.class;
310  }
311
312  /** {@inheritDoc} */
313  @Override
314  protected int processLocal(boolean initializeServer,
315                           PrintStream out,
316                           PrintStream err) {
317
318
319    // Perform the initial bootstrap of the Directory Server and process the
320    // configuration.
321    DirectoryServer directoryServer = DirectoryServer.getInstance();
322    if (initializeServer)
323    {
324      try
325      {
326        DirectoryServer.bootstrapClient();
327        DirectoryServer.initializeJMX();
328      }
329      catch (Exception e)
330      {
331        printWrappedText(err, ERR_SERVER_BOOTSTRAP_ERROR.get(getExceptionMessage(e)));
332        return 1;
333      }
334
335      try
336      {
337        directoryServer.initializeConfiguration(configClass.getValue(),
338                                                configFile.getValue());
339      }
340      catch (InitializationException ie)
341      {
342        printWrappedText(err, ERR_CANNOT_LOAD_CONFIG.get(ie.getMessage()));
343        return 1;
344      }
345      catch (Exception e)
346      {
347        printWrappedText(err, ERR_CANNOT_LOAD_CONFIG.get(getExceptionMessage(e)));
348        return 1;
349      }
350
351
352
353      // Initialize the Directory Server schema elements.
354      try
355      {
356        directoryServer.initializeSchema();
357      }
358      catch (ConfigException | InitializationException e)
359      {
360        printWrappedText(err, ERR_CANNOT_LOAD_SCHEMA.get(e.getMessage()));
361        return 1;
362      }
363      catch (Exception e)
364      {
365        printWrappedText(err, ERR_CANNOT_LOAD_SCHEMA.get(getExceptionMessage(e)));
366        return 1;
367      }
368
369
370      // Initialize the Directory Server core configuration.
371      try
372      {
373        CoreConfigManager coreConfigManager = new CoreConfigManager(directoryServer.getServerContext());
374        coreConfigManager.initializeCoreConfig();
375      }
376      catch (ConfigException | InitializationException e)
377      {
378        printWrappedText(err, ERR_CANNOT_INITIALIZE_CORE_CONFIG.get(e.getMessage()));
379        return 1;
380      }
381      catch (Exception e)
382      {
383        printWrappedText(err, ERR_CANNOT_INITIALIZE_CORE_CONFIG.get(getExceptionMessage(e)));
384        return 1;
385      }
386
387
388      // Initialize the Directory Server crypto manager.
389      try
390      {
391        directoryServer.initializeCryptoManager();
392      }
393      catch (ConfigException | InitializationException e)
394      {
395        printWrappedText(err, ERR_CANNOT_INITIALIZE_CRYPTO_MANAGER.get(e.getMessage()));
396        return 1;
397      }
398      catch (Exception e)
399      {
400        printWrappedText(err, ERR_CANNOT_INITIALIZE_CRYPTO_MANAGER.get(getExceptionMessage(e)));
401        return 1;
402      }
403
404
405      try
406      {
407        ErrorLogPublisher errorLogPublisher =
408            TextErrorLogPublisher.getToolStartupTextErrorPublisher(new TextWriter.STREAM(out));
409        ErrorLogger.getInstance().addLogPublisher(errorLogPublisher);
410        DebugLogger.getInstance().addPublisherIfRequired(new TextWriter.STREAM(out));
411      }
412      catch(Exception e)
413      {
414        err.println("Error installing the custom error logger: " +
415                    stackTraceToSingleLineString(e));
416      }
417    }
418
419
420    // Open the backup directory and make sure it is valid.
421    BackupDirectory backupDir;
422    try
423    {
424      backupDir = BackupDirectory.readBackupDirectoryDescriptor(
425                       backupDirectory.getValue());
426    }
427    catch (Exception e)
428    {
429      logger.error(ERR_RESTOREDB_CANNOT_READ_BACKUP_DIRECTORY, backupDirectory.getValue(), getExceptionMessage(e));
430      return 1;
431    }
432
433
434    // If we're just going to be listing backups, then do that now.
435    DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_LOCAL_TIME);
436    if (listBackups.isPresent())
437    {
438      for (BackupInfo backupInfo : backupDir.getBackups().values())
439      {
440        LocalizableMessage message = INFO_RESTOREDB_LIST_BACKUP_ID.get(
441                backupInfo.getBackupID());
442        out.println(message);
443
444        message = INFO_RESTOREDB_LIST_BACKUP_DATE.get(
445                dateFormat.format(backupInfo.getBackupDate()));
446        out.println(message);
447
448        message = INFO_RESTOREDB_LIST_INCREMENTAL.get(backupInfo.isIncremental());
449        out.println(message);
450
451        message = INFO_RESTOREDB_LIST_COMPRESSED.get(backupInfo.isCompressed());
452        out.println(message);
453
454        message = INFO_RESTOREDB_LIST_ENCRYPTED.get(backupInfo.isEncrypted());
455        out.println(message);
456
457        byte[] hash = backupInfo.getUnsignedHash();
458
459        message = INFO_RESTOREDB_LIST_HASHED.get(hash != null);
460        out.println(message);
461
462        byte[] signature = backupInfo.getSignedHash();
463
464        message = INFO_RESTOREDB_LIST_SIGNED.get(signature != null);
465        out.println(message);
466
467        StringBuilder dependencyList = new StringBuilder();
468        HashSet<String> dependencyIDs = backupInfo.getDependencies();
469        if (! dependencyIDs.isEmpty())
470        {
471          Iterator<String> iterator = dependencyIDs.iterator();
472          dependencyList.append(iterator.next());
473
474          while (iterator.hasNext())
475          {
476            dependencyList.append(", ");
477            dependencyList.append(iterator.next());
478          }
479        }
480        else
481        {
482          dependencyList.append("none");
483        }
484
485
486        message = INFO_RESTOREDB_LIST_DEPENDENCIES.get(dependencyList);
487        out.println(message);
488        out.println();
489      }
490
491      return 0;
492    }
493
494
495    // If a backup ID was specified, then make sure it is valid.  If none was
496    // provided, then choose the latest backup from the archive.  Encrypted
497    // or signed backups cannot be restored to a local (offline) server
498    // instance.
499    String backupID;
500    {
501      BackupInfo backupInfo = backupDir.getLatestBackup();
502      if (backupInfo == null)
503      {
504        logger.error(ERR_RESTOREDB_NO_BACKUPS_IN_DIRECTORY, backupDirectory.getValue());
505        return 1;
506      }
507      backupID = backupInfo.getBackupID();
508      if (backupIDString.isPresent())
509      {
510        backupID = backupIDString.getValue();
511        backupInfo = backupDir.getBackupInfo(backupID);
512        if (backupInfo == null)
513        {
514          logger.error(ERR_RESTOREDB_INVALID_BACKUP_ID, backupID, backupDirectory.getValue());
515          return 1;
516        }
517      }
518      if (backupInfo.isEncrypted() || null != backupInfo.getSignedHash()) {
519        logger.error(ERR_RESTOREDB_ENCRYPT_OR_SIGN_REQUIRES_ONLINE);
520        return 1;
521      }
522    }
523
524
525    // Get the DN of the backend configuration entry from the backup and load
526    // the associated backend from the configuration.
527    DN configEntryDN = backupDir.getConfigEntryDN();
528
529
530    // Get information about the backends defined in the server and determine
531    // which to use for the restore.
532    ArrayList<Backend>     backendList = new ArrayList<>();
533    ArrayList<BackendCfg> entryList   = new ArrayList<>();
534    ArrayList<List<DN>>    dnList      = new ArrayList<>();
535    BackendToolUtils.getBackends(backendList, entryList, dnList);
536
537
538    Backend     backend     = null;
539    int         numBackends = backendList.size();
540    for (int i=0; i < numBackends; i++)
541    {
542      Backend     b = backendList.get(i);
543      BackendCfg e = entryList.get(i);
544      if (e.dn().equals(configEntryDN))
545      {
546        backend     = b;
547        break;
548      }
549    }
550
551    if (backend == null)
552    {
553      logger.error(ERR_RESTOREDB_NO_BACKENDS_FOR_DN, backupDirectory.getValue(), configEntryDN);
554      return 1;
555    }
556    else if (!backend.supports(BackendOperation.RESTORE))
557    {
558      logger.error(ERR_RESTOREDB_CANNOT_RESTORE, backend.getBackendID());
559      return 1;
560    }
561
562
563    // Create the restore config object from the information available.
564    RestoreConfig restoreConfig = new RestoreConfig(backupDir, backupID,
565                                                    verifyOnly.isPresent());
566
567
568    // Acquire an exclusive lock for the backend.
569    try
570    {
571      String lockFile = LockFileManager.getBackendLockFileName(backend);
572      StringBuilder failureReason = new StringBuilder();
573      if (! LockFileManager.acquireExclusiveLock(lockFile, failureReason))
574      {
575        logger.error(ERR_RESTOREDB_CANNOT_LOCK_BACKEND, backend.getBackendID(), failureReason);
576        return 1;
577      }
578    }
579    catch (Exception e)
580    {
581      logger.error(ERR_RESTOREDB_CANNOT_LOCK_BACKEND, backend.getBackendID(), getExceptionMessage(e));
582      return 1;
583    }
584
585
586    // Perform the restore.
587    try
588    {
589      backend.restoreBackup(restoreConfig);
590    }
591    catch (DirectoryException de)
592    {
593      logger.error(ERR_RESTOREDB_ERROR_DURING_BACKUP, backupID, backupDir.getPath(), de.getMessageObject());
594    }
595    catch (Exception e)
596    {
597      logger.error(ERR_RESTOREDB_ERROR_DURING_BACKUP, backupID, backupDir.getPath(), getExceptionMessage(e));
598    }
599
600
601    // Release the exclusive lock on the backend.
602    try
603    {
604      String lockFile = LockFileManager.getBackendLockFileName(backend);
605      StringBuilder failureReason = new StringBuilder();
606      if (! LockFileManager.releaseLock(lockFile, failureReason))
607      {
608        logger.warn(WARN_RESTOREDB_CANNOT_UNLOCK_BACKEND, backend.getBackendID(), failureReason);
609      }
610    }
611    catch (Exception e)
612    {
613      logger.warn(WARN_RESTOREDB_CANNOT_UNLOCK_BACKEND, backend.getBackendID(), getExceptionMessage(e));
614    }
615    return 0;
616  }
617
618  /** {@inheritDoc} */
619  @Override
620  public String getTaskId() {
621    return backupIDString != null? backupIDString.getValue() : null;
622  }
623}