/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2007-2010 Sun Microsystems, Inc.
* Portions Copyright 2011-2015 ForgeRock AS
* Portions Copyright 2012 profiq s.r.o.
*/
package org.opends.server.tools.dsreplication;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import javax.naming.NameAlreadyBoundException;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.NoPermissionException;
import javax.naming.directory.Attribute;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManager;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.i18n.LocalizableMessageDescriptor.Arg0;
import org.forgerock.i18n.LocalizableMessageDescriptor.Arg1;
import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigException;
import org.opends.admin.ads.*;
import org.opends.admin.ads.ADSContext.ADSPropertySyntax;
import org.opends.admin.ads.ADSContext.AdministratorProperty;
import org.opends.admin.ads.ADSContext.ServerProperty;
import org.opends.admin.ads.util.ApplicationTrustManager;
import org.opends.admin.ads.util.OpendsCertificateException;
import org.opends.admin.ads.util.PreferredConnection;
import org.opends.admin.ads.util.ServerLoader;
import org.opends.guitools.controlpanel.datamodel.BackendDescriptor;
import org.opends.guitools.controlpanel.datamodel.BaseDNDescriptor;
import org.opends.guitools.controlpanel.util.*;
import org.opends.quicksetup.ApplicationException;
import org.opends.quicksetup.Constants;
import org.opends.quicksetup.Installation;
import org.opends.quicksetup.event.ProgressUpdateEvent;
import org.opends.quicksetup.event.ProgressUpdateListener;
import org.opends.quicksetup.installer.Installer;
import org.opends.quicksetup.installer.InstallerHelper;
import org.opends.quicksetup.installer.PeerNotFoundException;
import org.opends.quicksetup.installer.offline.OfflineInstaller;
import org.opends.quicksetup.util.PlainTextProgressMessageFormatter;
import org.opends.server.admin.*;
import org.opends.server.admin.client.ManagementContext;
import org.opends.server.admin.client.ldap.JNDIDirContextAdaptor;
import org.opends.server.admin.client.ldap.LDAPManagementContext;
import org.opends.server.admin.std.client.*;
import org.opends.server.admin.std.meta.ReplicationDomainCfgDefn;
import org.opends.server.admin.std.meta.ReplicationServerCfgDefn;
import org.opends.server.admin.std.meta.ReplicationSynchronizationProviderCfgDefn;
import org.opends.server.core.DirectoryServer;
import org.opends.server.tasks.PurgeConflictsHistoricalTask;
import org.opends.server.tools.dsreplication.EnableReplicationUserData.EnableReplicationServerData;
import org.opends.server.tools.tasks.TaskEntry;
import org.opends.server.tools.tasks.TaskScheduleInteraction;
import org.opends.server.tools.tasks.TaskScheduleUserData;
import org.opends.server.types.DN;
import org.opends.server.types.InitializationException;
import org.opends.server.types.NullOutputStream;
import org.opends.server.types.OpenDsException;
import org.opends.server.util.BuildVersion;
import org.opends.server.util.ServerConstants;
import org.opends.server.util.SetupUtils;
import org.opends.server.util.StaticUtils;
import org.opends.server.util.cli.LDAPConnectionConsoleInteraction;
import org.opends.server.util.cli.PointAdder;
import com.forgerock.opendj.cli.Argument;
import com.forgerock.opendj.cli.ArgumentException;
import com.forgerock.opendj.cli.BooleanArgument;
import com.forgerock.opendj.cli.CliConstants;
import com.forgerock.opendj.cli.ClientException;
import com.forgerock.opendj.cli.CommandBuilder;
import com.forgerock.opendj.cli.ConsoleApplication;
import com.forgerock.opendj.cli.FileBasedArgument;
import com.forgerock.opendj.cli.IntegerArgument;
import com.forgerock.opendj.cli.MenuBuilder;
import com.forgerock.opendj.cli.MenuResult;
import com.forgerock.opendj.cli.ReturnCode;
import com.forgerock.opendj.cli.StringArgument;
import com.forgerock.opendj.cli.SubCommand;
import com.forgerock.opendj.cli.TabSeparatedTablePrinter;
import com.forgerock.opendj.cli.TableBuilder;
import com.forgerock.opendj.cli.TablePrinter;
import com.forgerock.opendj.cli.TextTablePrinter;
import com.forgerock.opendj.cli.ValidationCallback;
import static com.forgerock.opendj.cli.ArgumentConstants.*;
import static com.forgerock.opendj.cli.CliMessages.*;
import static com.forgerock.opendj.cli.Utils.*;
import static com.forgerock.opendj.util.OperatingSystem.*;
import static org.forgerock.util.Utils.*;
import static org.opends.admin.ads.util.ConnectionUtils.*;
import static org.opends.admin.ads.util.PreferredConnection.*;
import static org.opends.admin.ads.ServerDescriptor.getReplicationServer;
import static org.opends.admin.ads.ServerDescriptor.getServerRepresentation;
import static org.opends.admin.ads.ServerDescriptor.getSuffixDisplay;
import static org.opends.messages.AdminToolMessages.*;
import static org.opends.messages.QuickSetupMessages.*;
import static org.opends.messages.ToolMessages.INFO_TASK_TOOL_TASK_SUCESSFULL;
import static org.opends.messages.ToolMessages.INFO_TASK_TOOL_TASK_SCHEDULED_FUTURE;
import static org.opends.messages.ToolMessages.INFO_TASK_TOOL_RECURRING_TASK_SCHEDULED;
import static org.opends.quicksetup.util.Utils.*;
import static org.opends.server.tools.dsreplication.ReplicationCliArgumentParser.*;
import static org.opends.server.tools.dsreplication.ReplicationCliReturnCode.*;
import static org.opends.server.util.StaticUtils.*;
/**
* This class provides a tool that can be used to enable and disable replication
* and also to initialize the contents of a replicated suffix with the contents
* of another suffix. It also allows to display the replicated status of the
* different base DNs of the servers that are registered in the ADS.
*/
public class ReplicationCliMain extends ConsoleApplication
{
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
/** The fully-qualified name of this class. */
private static final String CLASS_NAME = ReplicationCliMain.class.getName();
/** Prefix for log files. */
public static final String LOG_FILE_PREFIX = "opendj-replication-";
/** Suffix for log files. */
public static final String LOG_FILE_SUFFIX = ".log";
/**
* Property used to call the dsreplication script and ReplicationCliMain to
* know which are the java properties to be used (those of dsreplication or
* those of dsreplication.offline).
*/
private static final String SCRIPT_CALL_STATUS = "org.opends.server.dsreplicationcallstatus";
/** The value set by the dsreplication script if it is called the first time. */
private static final String FIRST_SCRIPT_CALL = "firstcall";
private static final LocalizableMessage EMPTY_MSG = LocalizableMessage.raw("");
private boolean forceNonInteractive;
/** Always use SSL with the administration connector. */
private final boolean useSSL = true;
private final boolean useStartTLS = false;
/**
* The enumeration containing the different options we display when we ask
* the user to provide the subcommand interactively.
*/
private enum SubcommandChoice
{
/** Enable replication. */
ENABLE(ENABLE_REPLICATION_SUBCMD_NAME, INFO_REPLICATION_ENABLE_MENU_PROMPT.get()),
/** Disable replication. */
DISABLE(DISABLE_REPLICATION_SUBCMD_NAME, INFO_REPLICATION_DISABLE_MENU_PROMPT.get()),
/** Initialize replication. */
INITIALIZE(INITIALIZE_REPLICATION_SUBCMD_NAME, INFO_REPLICATION_INITIALIZE_MENU_PROMPT.get()),
/** Initialize All. */
INITIALIZE_ALL(INITIALIZE_ALL_REPLICATION_SUBCMD_NAME, INFO_REPLICATION_INITIALIZE_ALL_MENU_PROMPT.get()),
/** Pre external initialization. */
PRE_EXTERNAL_INITIALIZATION(PRE_EXTERNAL_INITIALIZATION_SUBCMD_NAME,
INFO_REPLICATION_PRE_EXTERNAL_INITIALIZATION_MENU_PROMPT.get()),
/** Post external initialization. */
POST_EXTERNAL_INITIALIZATION(POST_EXTERNAL_INITIALIZATION_SUBCMD_NAME,
INFO_REPLICATION_POST_EXTERNAL_INITIALIZATION_MENU_PROMPT.get()),
/** Replication status. */
STATUS(STATUS_REPLICATION_SUBCMD_NAME, INFO_REPLICATION_STATUS_MENU_PROMPT.get()),
/** Replication purge historical. */
PURGE_HISTORICAL(PURGE_HISTORICAL_SUBCMD_NAME, INFO_REPLICATION_PURGE_HISTORICAL_MENU_PROMPT.get()),
/** Cancel operation. */
CANCEL(null, null);
private final String name;
private LocalizableMessage prompt;
private SubcommandChoice(String name, LocalizableMessage prompt)
{
this.name = name;
this.prompt = prompt;
}
private LocalizableMessage getPrompt()
{
return prompt;
}
private String getName()
{
return name;
}
private static SubcommandChoice fromName(String subCommandName)
{
SubcommandChoice[] f = values();
for (SubcommandChoice subCommand : f)
{
if (subCommand.name.equals(subCommandName))
{
return subCommand;
}
}
return null;
}
}
/** The argument parser to be used. */
private ReplicationCliArgumentParser argParser;
private FileBasedArgument userProvidedAdminPwdFile;
private LDAPConnectionConsoleInteraction ci;
private CommandBuilder firstServerCommandBuilder;
/** The message formatter. */
private PlainTextProgressMessageFormatter formatter = new PlainTextProgressMessageFormatter();
/**
* Constructor for the ReplicationCliMain object.
*
* @param out the print stream to use for standard output.
* @param err the print stream to use for standard error.
*/
public ReplicationCliMain(PrintStream out, PrintStream err)
{
super(out, err);
}
/**
* The main method for the replication tool.
*
* @param args the command-line arguments provided to this program.
*/
public static void main(String[] args)
{
int retCode = mainCLI(args, true, System.out, System.err);
System.exit(retCode);
}
/**
* Parses the provided command-line arguments and uses that information to
* run the replication tool.
*
* @param args the command-line arguments provided to this program.
*
* @return The error code.
*/
public static int mainCLI(String[] args)
{
return mainCLI(args, true, System.out, System.err);
}
/**
* Parses the provided command-line arguments and uses that information to
* run the replication tool.
*
* @param args The command-line arguments provided to this
* program.
* @param initializeServer Indicates whether to initialize the server.
* @param outStream The output stream to use for standard output, or
* null if standard output is not
* needed.
* @param errStream The output stream to use for standard error, or
* null if standard error is not
* needed.
* @return The error code.
*/
public static int mainCLI(String[] args, boolean initializeServer,
OutputStream outStream, OutputStream errStream)
{
PrintStream out = NullOutputStream.wrapOrNullStream(outStream);
PrintStream err = NullOutputStream.wrapOrNullStream(errStream);
try
{
ControlPanelLog.initLogFileHandler(
File.createTempFile(LOG_FILE_PREFIX, LOG_FILE_SUFFIX));
} catch (Throwable t) {
System.err.println("Unable to initialize log");
t.printStackTrace();
}
ReplicationCliMain replicationCli = new ReplicationCliMain(out, err);
ReplicationCliReturnCode result = replicationCli.execute(args, initializeServer);
return result.getReturnCode();
}
/**
* Parses the provided command-line arguments and uses that information to
* run the replication tool.
*
* @param args the command-line arguments provided to this program.
* @param initializeServer Indicates whether to initialize the server.
*
* @return The error code.
*/
public ReplicationCliReturnCode execute(String[] args, boolean initializeServer)
{
// Create the command-line argument parser for use with this program.
try
{
createArgumenParser();
}
catch (ArgumentException ae)
{
errPrintln(ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
logger.error(LocalizableMessage.raw("Complete error stack:"), ae);
return CANNOT_INITIALIZE_ARGS;
}
try
{
argParser.getSecureArgsList().initArgumentsWithConfiguration();
}
catch (ConfigException ce)
{
// Ignore.
}
// Parse the command-line arguments provided to this program.
try
{
argParser.parseArguments(args);
}
catch (ArgumentException ae)
{
errPrintln(ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
errPrintln();
errPrintln(LocalizableMessage.raw(argParser.getUsage()));
logger.error(LocalizableMessage.raw("Complete error stack:"), ae);
return ERROR_USER_DATA;
}
// If we should just display usage or version information, then print it and exit.
if (argParser.usageOrVersionDisplayed())
{
return SUCCESSFUL_NOP;
}
// Checks the version - if upgrade required, the tool is unusable
try
{
BuildVersion.checkVersionMismatch();
}
catch (InitializationException e)
{
errPrintln(e.getMessageObject());
return CANNOT_INITIALIZE_ARGS;
}
// Check that the provided parameters are compatible.
LocalizableMessageBuilder buf = new LocalizableMessageBuilder();
argParser.validateOptions(buf);
if (buf.length() > 0)
{
errPrintln(buf.toMessage());
errPrintln(LocalizableMessage.raw(argParser.getUsage()));
return ERROR_USER_DATA;
}
if (initializeServer)
{
DirectoryServer.bootstrapClient();
// Bootstrap definition classes.
try
{
if (!ClassLoaderProvider.getInstance().isEnabled())
{
ClassLoaderProvider.getInstance().enable();
}
// Switch off class name validation in client.
ClassPropertyDefinition.setAllowClassValidation(false);
// Switch off attribute type name validation in client.
AttributeTypePropertyDefinition.setCheckSchema(false);
}
catch (InitializationException ie)
{
errPrintln(ie.getMessageObject());
return ERROR_INITIALIZING_ADMINISTRATION_FRAMEWORK;
}
}
if (argParser.getSecureArgsList().bindPasswordFileArg.isPresent())
{
try
{
userProvidedAdminPwdFile = new FileBasedArgument(
"adminPasswordFile", OPTION_SHORT_BINDPWD_FILE, "adminPasswordFile", false, false,
INFO_BINDPWD_FILE_PLACEHOLDER.get(), null, null,
INFO_DESCRIPTION_REPLICATION_ADMIN_BINDPASSWORDFILE.get());
userProvidedAdminPwdFile.getNameToValueMap().putAll(
argParser.getSecureArgsList().bindPasswordFileArg.getNameToValueMap());
}
catch (Throwable t)
{
throw new IllegalStateException("Unexpected error: " + t, t);
}
}
ci = new LDAPConnectionConsoleInteraction(this, argParser.getSecureArgsList());
ci.setDisplayLdapIfSecureParameters(false);
ReplicationCliReturnCode returnValue = SUCCESSFUL_NOP;
String subCommand = null;
final SubcommandChoice subcommandChoice = getSubcommandChoice(argParser.getSubCommand());
if (subcommandChoice != null)
{
subCommand = subcommandChoice.getName();
returnValue = execute(subcommandChoice);
}
else if (argParser.isInteractive())
{
final SubcommandChoice subCommandChoice = promptForSubcommand();
if (subCommandChoice == null || SubcommandChoice.CANCEL.equals(subCommandChoice))
{
return USER_CANCELLED;
}
subCommand = subCommandChoice.getName();
if (subCommand != null)
{
String[] newArgs = new String[args.length + 1];
newArgs[0] = subCommand;
System.arraycopy(args, 0, newArgs, 1, args.length);
// The server (if requested) has already been initialized.
return execute(newArgs, false);
}
}
else
{
errPrintln(ERR_REPLICATION_VALID_SUBCOMMAND_NOT_FOUND.get("--" + OPTION_LONG_NO_PROMPT));
errPrintln(LocalizableMessage.raw(argParser.getUsage()));
return ERROR_USER_DATA;
}
// Display the log file only if the operation is successful (when there
// is a critical error this is already displayed).
if (returnValue == SUCCESSFUL && displayLogFileAtEnd(subCommand))
{
File logFile = ControlPanelLog.getLogFile();
if (logFile != null)
{
println();
println(INFO_GENERAL_SEE_FOR_DETAILS.get(logFile.getPath()));
println();
}
}
return returnValue;
}
private SubcommandChoice getSubcommandChoice(SubCommand subCommand)
{
if (subCommand != null)
{
return SubcommandChoice.fromName(subCommand.getName());
}
return null;
}
private ReplicationCliReturnCode execute(SubcommandChoice subcommandChoice)
{
switch (subcommandChoice)
{
case ENABLE:
return enableReplication();
case DISABLE:
return disableReplication();
case INITIALIZE:
return initializeReplication();
case INITIALIZE_ALL:
return initializeAllReplication();
case PRE_EXTERNAL_INITIALIZATION:
return preExternalInitialization();
case POST_EXTERNAL_INITIALIZATION:
return postExternalInitialization();
case STATUS:
return statusReplication();
case PURGE_HISTORICAL:
return purgeHistorical();
default:
return SUCCESSFUL_NOP;
}
}
/**
* Prompts the user to give the Global Administrator UID.
*
* @param defaultValue
* the default value that will be proposed in the prompt message.
* @param logger
* the Logger to be used to log the error message.
* @return the Global Administrator UID as provided by the user.
*/
private String askForAdministratorUID(String defaultValue, LocalizedLogger logger)
{
try
{
return readInput(INFO_ADMINISTRATOR_UID_PROMPT.get(), defaultValue);
}
catch (ClientException ce)
{
logger.warn(LocalizableMessage.raw("Error reading input: " + ce, ce));
return defaultValue;
}
}
/**
* Prompts the user to give the Global Administrator password.
*
* @param logger
* the Logger to be used to log the error message.
* @return the Global Administrator password as provided by the user.
*/
private String askForAdministratorPwd(LocalizedLogger logger)
{
try
{
return new String(readPassword(INFO_ADMINISTRATOR_PWD_PROMPT.get()));
}
catch (ClientException ex)
{
logger.warn(LocalizableMessage.raw("Error reading input: " + ex, ex));
return null;
}
}
/**
* Commodity method used to repeatidly ask the user to provide an integer
* value.
*
* @param prompt
* the prompt message.
* @param defaultValue
* the default value to be proposed to the user.
* @param logger
* the logger where the errors will be written.
* @return the value provided by the user.
*/
private int askInteger(LocalizableMessage prompt, int defaultValue, LocalizedLogger logger)
{
int newInt = -1;
while (newInt == -1)
{
try
{
newInt = readInteger(prompt, defaultValue);
}
catch (ClientException ce)
{
newInt = -1;
logger.warn(LocalizableMessage.raw("Error reading input: " + ce, ce));
}
}
return newInt;
}
/**
* Interactively retrieves an integer value from the console.
*
* @param prompt
* The message prompt.
* @param defaultValue
* The default value.
* @return Returns the value.
* @throws ClientException
* If the value could not be retrieved for some reason.
*/
public final int readInteger(
LocalizableMessage prompt, final int defaultValue) throws ClientException
{
ValidationCallback callback = new ValidationCallback()
{
@Override
public Integer validate(ConsoleApplication app, String input)
throws ClientException
{
String ninput = input.trim();
if (ninput.length() == 0)
{
return defaultValue;
}
try
{
int i = Integer.parseInt(ninput);
if (i < 1)
{
throw new NumberFormatException();
}
return i;
}
catch (NumberFormatException e)
{
// Try again...
app.errPrintln();
app.errPrintln(ERR_BAD_INTEGER.get(ninput));
app.errPrintln();
return null;
}
}
};
if (defaultValue != -1)
{
prompt = INFO_PROMPT_SINGLE_DEFAULT.get(prompt, defaultValue);
}
return readValidatedInput(prompt, callback, CONFIRMATION_MAX_TRIES);
}
private boolean isFirstCallFromScript()
{
return FIRST_SCRIPT_CALL.equals(System.getProperty(SCRIPT_CALL_STATUS));
}
private void createArgumenParser() throws ArgumentException
{
argParser = new ReplicationCliArgumentParser(CLASS_NAME);
argParser.initializeParser(getOutputStream());
}
/**
* Based on the data provided in the command-line it enables replication
* between two servers.
* @return the error code if the operation failed and 0 if it was successful.
*/
private ReplicationCliReturnCode enableReplication()
{
EnableReplicationUserData uData = new EnableReplicationUserData();
if (argParser.isInteractive())
{
try
{
if (promptIfRequired(uData))
{
return enableReplication(uData);
}
else
{
return USER_CANCELLED;
}
}
catch (ReplicationCliException rce)
{
errPrintln();
errPrintln(getCriticalExceptionMessage(rce));
return rce.getErrorCode();
}
}
else
{
initializeWithArgParser(uData);
return enableReplication(uData);
}
}
/**
* Based on the data provided in the command-line it disables replication
* in the server.
* @return the error code if the operation failed and SUCCESSFUL if it was
* successful.
*/
private ReplicationCliReturnCode disableReplication()
{
DisableReplicationUserData uData = new DisableReplicationUserData();
if (argParser.isInteractive())
{
try
{
if (promptIfRequired(uData))
{
return disableReplication(uData);
}
else
{
return USER_CANCELLED;
}
}
catch (ReplicationCliException rce)
{
errPrintln();
errPrintln(getCriticalExceptionMessage(rce));
return rce.getErrorCode();
}
}
else
{
initializeWithArgParser(uData);
return disableReplication(uData);
}
}
/**
* Based on the data provided in the command-line initialize the contents
* of the whole replication topology.
* @return the error code if the operation failed and SUCCESSFUL if it was
* successful.
*/
private ReplicationCliReturnCode initializeAllReplication()
{
InitializeAllReplicationUserData uData =
new InitializeAllReplicationUserData();
if (argParser.isInteractive())
{
if (promptIfRequired(uData))
{
return initializeAllReplication(uData);
}
else
{
return USER_CANCELLED;
}
}
else
{
initializeWithArgParser(uData);
return initializeAllReplication(uData);
}
}
/**
* Based on the data provided in the command-line execute the pre external
* initialization operation.
* @return the error code if the operation failed and SUCCESSFUL if it was
* successful.
*/
private ReplicationCliReturnCode preExternalInitialization()
{
PreExternalInitializationUserData uData =
new PreExternalInitializationUserData();
if (argParser.isInteractive())
{
if (promptIfRequiredForPreOrPost(uData))
{
return preExternalInitialization(uData);
}
else
{
return USER_CANCELLED;
}
}
else
{
initializeWithArgParser(uData);
return preExternalInitialization(uData);
}
}
/**
* Based on the data provided in the command-line execute the post external
* initialization operation.
* @return the error code if the operation failed and SUCCESSFUL if it was
* successful.
*/
private ReplicationCliReturnCode postExternalInitialization()
{
PostExternalInitializationUserData uData =
new PostExternalInitializationUserData();
if (argParser.isInteractive())
{
if (promptIfRequiredForPreOrPost(uData))
{
return postExternalInitialization(uData);
}
else
{
return USER_CANCELLED;
}
}
else
{
initializeWithArgParser(uData);
return postExternalInitialization(uData);
}
}
/**
* Based on the data provided in the command-line it displays replication
* status.
* @return the error code if the operation failed and SUCCESSFUL if it was
* successful.
*/
private ReplicationCliReturnCode statusReplication()
{
StatusReplicationUserData uData = new StatusReplicationUserData();
if (argParser.isInteractive())
{
try
{
if (promptIfRequired(uData))
{
return statusReplication(uData);
}
else
{
return USER_CANCELLED;
}
}
catch (ReplicationCliException rce)
{
errPrintln();
errPrintln(getCriticalExceptionMessage(rce));
return rce.getErrorCode();
}
}
else
{
initializeWithArgParser(uData);
return statusReplication(uData);
}
}
/**
* Based on the data provided in the command-line it displays replication
* status.
* @return the error code if the operation failed and SUCCESSFUL if it was
* successful.
*/
private ReplicationCliReturnCode purgeHistorical()
{
final PurgeHistoricalUserData uData = new PurgeHistoricalUserData();
if (argParser.isInteractive())
{
if (promptIfRequired(uData))
{
return purgeHistorical(uData);
}
else
{
return USER_CANCELLED;
}
}
else
{
initializeWithArgParser(uData);
return purgeHistorical(uData);
}
}
/**
* Initializes the contents of the provided purge historical replication user
* data object with what was provided in the command-line without prompting to
* the user.
* @param uData the purge historical replication user data object to be
* initialized.
*/
private void initializeWithArgParser(PurgeHistoricalUserData uData)
{
PurgeHistoricalUserData.initializeWithArgParser(uData, argParser);
}
private ReplicationCliReturnCode purgeHistorical(PurgeHistoricalUserData uData)
{
return uData.isOnline()
? purgeHistoricalRemotely(uData)
: purgeHistoricalLocally(uData);
}
private ReplicationCliReturnCode purgeHistoricalLocally(
PurgeHistoricalUserData uData)
{
List baseDNs = uData.getBaseDNs();
checkSuffixesForLocalPurgeHistorical(baseDNs, false);
if (!baseDNs.isEmpty())
{
uData.setBaseDNs(baseDNs);
if (mustPrintCommandBuilder())
{
printNewCommandBuilder(PURGE_HISTORICAL_SUBCMD_NAME, uData);
}
try
{
return purgeHistoricalLocallyTask(uData);
}
catch (ReplicationCliException rce)
{
errPrintln();
errPrintln(getCriticalExceptionMessage(rce));
logger.error(LocalizableMessage.raw("Complete error stack:"), rce);
return rce.getErrorCode();
}
}
else
{
return HISTORICAL_CANNOT_BE_PURGED_ON_BASEDN;
}
}
private void printPurgeProgressMessage(PurgeHistoricalUserData uData)
{
String separator = formatter.getLineBreak().toString() + formatter.getTab();
println();
LocalizableMessage msg = formatter.getFormattedProgress(
INFO_PROGRESS_PURGE_HISTORICAL.get(separator,
joinAsString(separator, uData.getBaseDNs())));
print(msg);
println();
}
private ReplicationCliReturnCode purgeHistoricalLocallyTask(PurgeHistoricalUserData uData)
throws ReplicationCliException
{
ReplicationCliReturnCode returnCode = SUCCESSFUL;
if (isFirstCallFromScript())
{
// Launch the process: launch dsreplication in non-interactive mode with
// the recursive property set.
ArrayList args = new ArrayList();
args.add(getCommandLinePath(getCommandName()));
args.add(PURGE_HISTORICAL_SUBCMD_NAME);
args.add("--"+argParser.noPromptArg.getLongIdentifier());
args.add("--"+argParser.maximumDurationArg.getLongIdentifier());
args.add(String.valueOf(uData.getMaximumDuration()));
for (String baseDN : uData.getBaseDNs())
{
args.add("--"+argParser.baseDNsArg.getLongIdentifier());
args.add(baseDN);
}
ProcessBuilder pb = new ProcessBuilder(args);
// Use the java args in the script.
Map env = pb.environment();
env.put("RECURSIVE_LOCAL_CALL", "true");
try
{
Process process = pb.start();
ProcessReader outReader =
new ProcessReader(process, getOutputStream(), false);
ProcessReader errReader =
new ProcessReader(process, getErrorStream(), true);
outReader.startReading();
errReader.startReading();
int code = process.waitFor();
for (ReplicationCliReturnCode c : ReplicationCliReturnCode.values())
{
if (c.getReturnCode() == code)
{
returnCode = c;
break;
}
}
}
catch (Exception e)
{
LocalizableMessage msg = ERR_LAUNCHING_PURGE_HISTORICAL.get();
ReplicationCliReturnCode code = ERROR_LAUNCHING_PURGE_HISTORICAL;
throw new ReplicationCliException(
getThrowableMsg(msg, e), code, e);
}
}
else
{
printPurgeProgressMessage(uData);
LocalPurgeHistorical localPurgeHistorical =
new LocalPurgeHistorical(uData, this, formatter,
argParser.getConfigFile(),
argParser.getConfigClass());
returnCode = localPurgeHistorical.execute();
if (returnCode == SUCCESSFUL)
{
printSuccessMessage(uData, null);
}
}
return returnCode;
}
/**
* Returns an InitialLdapContext using the provided parameters. We try to
* guarantee that the connection is able to read the configuration.
*
* @param host
* the host name.
* @param port
* the port to connect.
* @param useSSL
* whether to use SSL or not.
* @param useStartTLS
* whether to use StartTLS or not.
* @param bindDn
* the bind dn to be used.
* @param pwd
* the password.
* @param connectTimeout
* the timeout in milliseconds to connect to the server.
* @param trustManager
* the trust manager.
* @return an InitialLdapContext connected.
* @throws NamingException
* if there was an error establishing the connection.
*/
private InitialLdapContext createAdministrativeContext(String host,
int port, boolean useSSL, boolean useStartTLS, String bindDn, String pwd,
int connectTimeout, ApplicationTrustManager trustManager)
throws NamingException
{
InitialLdapContext ctx;
String ldapUrl = getLDAPUrl(host, port, useSSL);
if (useSSL)
{
ctx = createLdapsContext(ldapUrl, bindDn, pwd, connectTimeout, null, trustManager, null);
}
else if (useStartTLS)
{
ctx = createStartTLSContext(ldapUrl, bindDn, pwd, connectTimeout, null, trustManager, null);
}
else
{
ctx = createLdapContext(ldapUrl, bindDn, pwd, connectTimeout, null);
}
if (!connectedAsAdministrativeUser(ctx))
{
throw new NoPermissionException(ERR_NOT_ADMINISTRATIVE_USER.get().toString());
}
return ctx;
}
/**
* Creates an Initial LDAP Context interacting with the user if the
* application is interactive.
*
* @param ci
* the LDAPConnectionConsoleInteraction object that is assumed to
* have been already run.
* @return the initial LDAP context or null if the user did not
* accept to trust the certificates.
* @throws ClientException
* if there was an error establishing the connection.
*/
private InitialLdapContext createInitialLdapContextInteracting(
LDAPConnectionConsoleInteraction ci) throws ClientException
{
return createInitialLdapContextInteracting(ci, isInteractive()
&& ci.isTrustStoreInMemory());
}
private OpendsCertificateException getCertificateRootException(Throwable t)
{
while (t != null)
{
t = t.getCause();
if (t instanceof OpendsCertificateException)
{
return (OpendsCertificateException) t;
}
}
return null;
}
/**
* Creates an Initial LDAP Context interacting with the user if the
* application is interactive.
*
* @param ci
* the LDAPConnectionConsoleInteraction object that is assumed to
* have been already run.
* @param promptForCertificate
* whether we should prompt for the certificate or not.
* @return the initial LDAP context or null if the user did not
* accept to trust the certificates.
* @throws ClientException
* if there was an error establishing the connection.
*/
private InitialLdapContext createInitialLdapContextInteracting(
LDAPConnectionConsoleInteraction ci, boolean promptForCertificate)
throws ClientException
{
// Interact with the user though the console to get
// LDAP connection information
String hostName = getHostNameForLdapUrl(ci.getHostName());
Integer portNumber = ci.getPortNumber();
String bindDN = ci.getBindDN();
String bindPassword = ci.getBindPassword();
TrustManager trustManager = ci.getTrustManager();
KeyManager keyManager = ci.getKeyManager();
InitialLdapContext ctx;
if (ci.useSSL())
{
String ldapsUrl = "ldaps://" + hostName + ":" + portNumber;
while (true)
{
try
{
ctx = createLdapsContext(ldapsUrl, bindDN, bindPassword, ci.getConnectTimeout(),
null, trustManager, keyManager);
ctx.reconnect(null);
break;
}
catch (NamingException e)
{
if (promptForCertificate)
{
OpendsCertificateException oce = getCertificateRootException(e);
if (oce != null)
{
String authType = null;
if (trustManager instanceof ApplicationTrustManager)
{
ApplicationTrustManager appTrustManager =
(ApplicationTrustManager) trustManager;
authType = appTrustManager.getLastRefusedAuthType();
}
if (ci.checkServerCertificate(oce.getChain(), authType, hostName))
{
// If the certificate is trusted, update the trust manager.
trustManager = ci.getTrustManager();
// Try to connect again.
continue;
}
else
{
// Assume user canceled.
return null;
}
}
}
if (e.getCause() != null)
{
if (!isInteractive()
&& !ci.isTrustAll()
&& (getCertificateRootException(e) != null
|| e.getCause() instanceof SSLHandshakeException))
{
LocalizableMessage message =
ERR_FAILED_TO_CONNECT_NOT_TRUSTED.get(hostName, portNumber);
throw new ClientException(ReturnCode.CLIENT_SIDE_CONNECT_ERROR, message);
}
if (e.getCause() instanceof SSLException)
{
LocalizableMessage message =
ERR_FAILED_TO_CONNECT_WRONG_PORT.get(hostName, portNumber);
throw new ClientException(
ReturnCode.CLIENT_SIDE_CONNECT_ERROR, message);
}
}
String hostPort =
ServerDescriptor.getServerRepresentation(hostName, portNumber);
LocalizableMessage message = getMessageForException(e, hostPort);
throw new ClientException(ReturnCode.CLIENT_SIDE_CONNECT_ERROR, message);
}
}
}
else if (ci.useStartTLS())
{
String ldapUrl = "ldap://" + hostName + ":" + portNumber;
while (true)
{
try
{
ctx = createStartTLSContext(ldapUrl, bindDN,
bindPassword, CliConstants.DEFAULT_LDAP_CONNECT_TIMEOUT, null,
trustManager, keyManager, null);
ctx.reconnect(null);
break;
}
catch (NamingException e)
{
if (promptForCertificate)
{
OpendsCertificateException oce = getCertificateRootException(e);
if (oce != null)
{
String authType = null;
if (trustManager instanceof ApplicationTrustManager)
{
ApplicationTrustManager appTrustManager =
(ApplicationTrustManager) trustManager;
authType = appTrustManager.getLastRefusedAuthType();
}
if (ci.checkServerCertificate(oce.getChain(), authType, hostName))
{
// If the certificate is trusted, update the trust manager.
trustManager = ci.getTrustManager();
// Try to connect again.
continue;
}
else
{
// Assume user cancelled.
return null;
}
}
else
{
LocalizableMessage message =
ERR_FAILED_TO_CONNECT.get(hostName, portNumber);
throw new ClientException(
ReturnCode.CLIENT_SIDE_CONNECT_ERROR, message);
}
}
LocalizableMessage message =
ERR_FAILED_TO_CONNECT.get(hostName, portNumber);
throw new ClientException(ReturnCode.CLIENT_SIDE_CONNECT_ERROR,
message);
}
}
}
else
{
String ldapUrl = "ldap://" + hostName + ":" + portNumber;
while (true)
{
try
{
ctx = createLdapContext(ldapUrl, bindDN, bindPassword,
CliConstants.DEFAULT_LDAP_CONNECT_TIMEOUT, null);
ctx.reconnect(null);
break;
}
catch (NamingException e)
{
LocalizableMessage message =
ERR_FAILED_TO_CONNECT.get(hostName, portNumber);
throw new ClientException(ReturnCode.CLIENT_SIDE_CONNECT_ERROR,
message);
}
}
}
return ctx;
}
private ReplicationCliReturnCode purgeHistoricalRemotely(
PurgeHistoricalUserData uData)
{
// Connect to the provided server
InitialLdapContext ctx = createAdministrativeContext(uData);
if (ctx == null)
{
return ERROR_CONNECTING;
}
try
{
List baseDNs = uData.getBaseDNs();
checkSuffixesForPurgeHistorical(baseDNs, ctx, false);
if (baseDNs.isEmpty())
{
return HISTORICAL_CANNOT_BE_PURGED_ON_BASEDN;
}
uData.setBaseDNs(baseDNs);
if (mustPrintCommandBuilder())
{
printNewCommandBuilder(PURGE_HISTORICAL_SUBCMD_NAME, uData);
}
try
{
return purgeHistoricalRemoteTask(ctx, uData);
}
catch (ReplicationCliException rce)
{
errPrintln();
errPrintln(getCriticalExceptionMessage(rce));
logger.error(LocalizableMessage.raw("Complete error stack:"), rce);
return rce.getErrorCode();
}
}
finally
{
close(ctx);
}
}
private InitialLdapContext createAdministrativeContext(MonoServerReplicationUserData uData)
{
final String bindDn = getAdministratorDN(uData.getAdminUid());
return createAdministrativeContext(uData, bindDn);
}
private InitialLdapContext createAdministrativeContext(MonoServerReplicationUserData uData, final String bindDn)
{
try
{
return createAdministrativeContext(uData.getHostName(), uData.getPort(),
useSSL, useStartTLS, bindDn,
uData.getAdminPwd(), getConnectTimeout(), getTrustManager());
}
catch (NamingException ne)
{
String hostPort = getServerRepresentation(uData.getHostName(), uData.getPort());
errPrintln();
errPrintln(getMessageForException(ne, hostPort));
logger.error(LocalizableMessage.raw("Complete error stack:"), ne);
return null;
}
}
private void printSuccessMessage(PurgeHistoricalUserData uData, String taskID)
{
println();
if (!uData.isOnline())
{
print(
INFO_PROGRESS_PURGE_HISTORICAL_FINISHED_PROCEDURE.get());
}
else if (uData.getTaskSchedule().isStartNow())
{
print(INFO_TASK_TOOL_TASK_SUCESSFULL.get(
INFO_PURGE_HISTORICAL_TASK_NAME.get(),
taskID));
}
else if (uData.getTaskSchedule().getStartDate() != null)
{
print(INFO_TASK_TOOL_TASK_SCHEDULED_FUTURE.get(
INFO_PURGE_HISTORICAL_TASK_NAME.get(),
taskID,
StaticUtils.formatDateTimeString(
uData.getTaskSchedule().getStartDate())));
}
else
{
print(INFO_TASK_TOOL_RECURRING_TASK_SCHEDULED.get(
INFO_PURGE_HISTORICAL_TASK_NAME.get(),
taskID));
}
println();
}
/**
* Launches the purge historical operation using the
* provided connection.
* @param ctx the connection to the server.
* @throws ReplicationCliException if there is an error performing the
* operation.
*/
private ReplicationCliReturnCode purgeHistoricalRemoteTask(
InitialLdapContext ctx,
PurgeHistoricalUserData uData)
throws ReplicationCliException
{
printPurgeProgressMessage(uData);
ReplicationCliReturnCode returnCode = SUCCESSFUL;
boolean taskCreated = false;
boolean isOver = false;
String dn = null;
String taskID = null;
while (!taskCreated)
{
BasicAttributes attrs = PurgeHistoricalUserData.getTaskAttributes(uData);
dn = PurgeHistoricalUserData.getTaskDN(attrs);
taskID = PurgeHistoricalUserData.getTaskID(attrs);
try
{
DirContext dirCtx = ctx.createSubcontext(dn, attrs);
taskCreated = true;
logger.info(LocalizableMessage.raw("created task entry: "+attrs));
dirCtx.close();
}
catch (NamingException ne)
{
logger.error(LocalizableMessage.raw("Error creating task "+attrs, ne));
LocalizableMessage msg = ERR_LAUNCHING_PURGE_HISTORICAL.get();
ReplicationCliReturnCode code = ERROR_LAUNCHING_PURGE_HISTORICAL;
throw new ReplicationCliException(
getThrowableMsg(msg, ne), code, ne);
}
}
// Wait until it is over
SearchControls searchControls = new SearchControls();
searchControls.setCountLimit(1);
searchControls.setSearchScope(SearchControls.OBJECT_SCOPE);
searchControls.setReturningAttributes(
new String[] {
"ds-task-log-message",
"ds-task-state",
"ds-task-purge-conflicts-historical-purged-values-count",
"ds-task-purge-conflicts-historical-purge-completed-in-time",
"ds-task-purge-conflicts-historical-purge-completed-in-time",
"ds-task-purge-conflicts-historical-last-purged-changenumber"
});
String filter = "objectclass=*";
String lastLogMsg = null;
// Polling only makes sense when we are recurrently scheduling a task
// or the task is being executed now.
while (!isOver && uData.getTaskSchedule().getStartDate() == null)
{
sleepCatchInterrupt(500);
try
{
NamingEnumeration res =
ctx.search(dn, filter, searchControls);
SearchResult sr = null;
try
{
sr = res.next();
}
finally
{
res.close();
}
String logMsg = getFirstValue(sr, "ds-task-log-message");
if (logMsg != null && !logMsg.equals(lastLogMsg))
{
logger.info(LocalizableMessage.raw(logMsg));
lastLogMsg = logMsg;
}
InstallerHelper helper = new InstallerHelper();
String state = getFirstValue(sr, "ds-task-state");
if (helper.isDone(state) || helper.isStoppedByError(state))
{
isOver = true;
LocalizableMessage errorMsg = getPurgeErrorMsg(lastLogMsg, state, ctx);
if (helper.isCompletedWithErrors(state))
{
logger.warn(LocalizableMessage.raw("Completed with error: "+errorMsg));
errPrintln(errorMsg);
}
else if (!helper.isSuccessful(state) ||
helper.isStoppedByError(state))
{
logger.warn(LocalizableMessage.raw("Error: "+errorMsg));
ReplicationCliReturnCode code = ERROR_LAUNCHING_PURGE_HISTORICAL;
throw new ReplicationCliException(errorMsg, code, null);
}
}
}
catch (NameNotFoundException x)
{
isOver = true;
}
catch (NamingException ne)
{
LocalizableMessage msg = ERR_POOLING_PURGE_HISTORICAL.get();
throw new ReplicationCliException(
getThrowableMsg(msg, ne), ERROR_CONNECTING, ne);
}
}
if (returnCode == SUCCESSFUL)
{
printSuccessMessage(uData, taskID);
}
return returnCode;
}
private LocalizableMessage getPurgeErrorMsg(String lastLogMsg, String state, InitialLdapContext ctx)
{
String server = getHostPort(ctx);
if (lastLogMsg != null)
{
return INFO_ERROR_DURING_PURGE_HISTORICAL_LOG.get(lastLogMsg, state, server);
}
return INFO_ERROR_DURING_PURGE_HISTORICAL_NO_LOG.get(state, server);
}
/**
* Checks that historical can actually be purged in the provided baseDNs
* for the server.
* @param suffixes the suffixes provided by the user. This Collection is
* updated with the base DNs that the user provided interactively.
* @param ctx connection to the server.
* @param interactive whether to ask the user to provide interactively
* base DNs if none of the provided base DNs can be purged.
*/
private void checkSuffixesForPurgeHistorical(Collection suffixes,
InitialLdapContext ctx, boolean interactive)
{
checkSuffixesForPurgeHistorical(suffixes, getReplicas(ctx), interactive);
}
/**
* Checks that historical can actually be purged in the provided baseDNs
* for the local server.
* @param suffixes the suffixes provided by the user. This Collection is
* updated with the base DNs that the user provided interactively.
* @param interactive whether to ask the user to provide interactively
* base DNs if none of the provided base DNs can be purged.
*/
private void checkSuffixesForLocalPurgeHistorical(Collection suffixes,
boolean interactive)
{
checkSuffixesForPurgeHistorical(suffixes, getLocalReplicas(), interactive);
}
private Collection getLocalReplicas()
{
Collection replicas = new ArrayList();
ConfigFromFile configFromFile = new ConfigFromFile();
configFromFile.readConfiguration();
Collection backends = configFromFile.getBackends();
for (BackendDescriptor backend : backends)
{
for (BaseDNDescriptor baseDN : backend.getBaseDns())
{
SuffixDescriptor suffix = new SuffixDescriptor();
suffix.setDN(baseDN.getDn().toString());
ReplicaDescriptor replica = new ReplicaDescriptor();
if (baseDN.getType() == BaseDNDescriptor.Type.REPLICATED)
{
replica.setReplicationId(baseDN.getReplicaID());
}
else
{
replica.setReplicationId(-1);
}
replica.setBackendName(backend.getBackendID());
replica.setSuffix(suffix);
suffix.setReplicas(Collections.singleton(replica));
replicas.add(replica);
}
}
return replicas;
}
private void checkSuffixesForPurgeHistorical(Collection suffixes, Collection replicas,
boolean interactive)
{
TreeSet availableSuffixes = new TreeSet();
TreeSet notReplicatedSuffixes = new TreeSet();
for (ReplicaDescriptor rep : replicas)
{
String dn = rep.getSuffix().getDN();
if (rep.isReplicated())
{
availableSuffixes.add(dn);
}
else
{
notReplicatedSuffixes.add(dn);
}
}
checkSuffixesForPurgeHistorical(suffixes, availableSuffixes, notReplicatedSuffixes, interactive);
}
private void checkSuffixesForPurgeHistorical(Collection suffixes,
Collection availableSuffixes,
Collection notReplicatedSuffixes,
boolean interactive)
{
if (availableSuffixes.isEmpty())
{
errPrintln();
errPrintln(ERR_NO_SUFFIXES_AVAILABLE_TO_PURGE_HISTORICAL.get());
suffixes.clear();
}
else
{
// Verify that the provided suffixes are configured in the servers.
TreeSet notFound = new TreeSet();
TreeSet alreadyNotReplicated = new TreeSet();
for (String dn : suffixes)
{
if (!containsDN(availableSuffixes, dn))
{
if (containsDN(notReplicatedSuffixes, dn))
{
alreadyNotReplicated.add(dn);
}
else
{
notFound.add(dn);
}
}
}
suffixes.removeAll(notFound);
suffixes.removeAll(alreadyNotReplicated);
if (!notFound.isEmpty())
{
errPrintln();
errPrintln(ERR_REPLICATION_PURGE_SUFFIXES_NOT_FOUND.get(toSingleLine(notFound)));
}
if (interactive)
{
askConfirmations(suffixes, availableSuffixes,
ERR_NO_SUFFIXES_AVAILABLE_TO_PURGE_HISTORICAL,
ERR_NO_SUFFIXES_SELECTED_TO_PURGE_HISTORICAL,
INFO_REPLICATION_PURGE_HISTORICAL_PROMPT);
}
}
}
private void askConfirmations(Collection suffixes,
Collection availableSuffixes, Arg0 noSuffixAvailableMsg,
Arg0 noSuffixSelectedMsg, Arg1