/*
* 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
*
*
* Portions Copyright 2013-2016 ForgeRock AS
*/
package org.opends.server.tools.upgrade;
import static javax.security.auth.callback.ConfirmationCallback.NO;
import static javax.security.auth.callback.ConfirmationCallback.YES;
import static javax.security.auth.callback.TextOutputCallback.*;
import static org.opends.messages.ToolMessages.*;
import static org.opends.server.tools.upgrade.FileManager.copy;
import static org.opends.server.tools.upgrade.Installation.CURRENT_CONFIG_FILE_NAME;
import static org.opends.server.tools.upgrade.UpgradeUtils.*;
import static org.opends.server.util.StaticUtils.isClassAvailable;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.security.auth.callback.TextOutputCallback;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldif.EntryReader;
import org.forgerock.util.Utils;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.tools.JavaPropertiesTool;
import org.opends.server.tools.RebuildIndex;
import org.opends.server.util.BuildVersion;
import org.opends.server.util.ChangeOperationType;
import org.opends.server.util.StaticUtils;
import com.forgerock.opendj.cli.ClientException;
import com.forgerock.opendj.cli.ReturnCode;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.TransactionConfig;
/** Factory methods for create new upgrade tasks. */
public final class UpgradeTasks
{
/** Logger for the upgrade. */
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
/** An errors counter in case of ignore errors mode. */
static int countErrors;
/** Contains all the indexes to rebuild. */
static Set<String> indexesToRebuild = new HashSet<>();
/** A flag to avoid rebuild single indexes if 'rebuild all' is selected. */
static boolean isRebuildAllIndexesIsPresent;
/** A flag for marking 'rebuild all' task accepted by user. */
static boolean isRebuildAllIndexesTaskAccepted;
/**
* Returns a new upgrade task which adds a config entry to the underlying
* config file.
*
* @param summary
* The summary of this upgrade task.
* @param ldif
* The LDIF record which will be applied to matching entries.
* @return A new upgrade task which applies an LDIF record to all
* configuration entries matching the provided filter.
*/
public static UpgradeTask addConfigEntry(final LocalizableMessage summary,
final String... ldif)
{
return updateConfigEntry(summary, null, ChangeOperationType.ADD, ldif);
}
/**
* This task copies the file placed in parameter within the config / schema
* folder. If the file already exists, it's overwritten.
*
* @param fileName
* The name of the file which need to be copied.
* @return A task which copy the the file placed in parameter within the
* config / schema folder. If the file already exists, it's
* overwritten.
*/
public static UpgradeTask copySchemaFile(final String fileName)
{
return new AbstractUpgradeTask()
{
@Override
public void perform(final UpgradeContext context) throws ClientException
{
final LocalizableMessage msg = INFO_UPGRADE_TASK_REPLACE_SCHEMA_FILE.get(fileName);
logger.debug(msg);
final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 0);
final File schemaFileTemplate =
new File(templateConfigSchemaDirectory, fileName);
try
{
context.notifyProgress(pnc.setProgress(20));
if (!schemaFileTemplate.exists() || schemaFileTemplate.length() == 0)
{
throw new IOException(ERR_UPGRADE_CORRUPTED_TEMPLATE
.get(schemaFileTemplate.getPath()).toString());
}
copy(schemaFileTemplate, configSchemaDirectory, true);
context.notifyProgress(pnc.setProgress(100));
}
catch (final IOException e)
{
manageTaskException(context, ERR_UPGRADE_COPYSCHEMA_FAILS.get(
schemaFileTemplate.getName(), e.getMessage()), pnc);
}
}
@Override
public String toString()
{
return INFO_UPGRADE_TASK_REPLACE_SCHEMA_FILE.get(fileName).toString();
}
};
}
/**
* This task copies the file placed in parameter within the config folder. If
* the file already exists, it's overwritten.
*
* @param fileName
* The name of the file which need to be copied.
* @return A task which copy the the file placed in parameter within the
* config folder. If the file already exists, it's overwritten.
*/
public static UpgradeTask addConfigFile(final String fileName)
{
return new AbstractUpgradeTask()
{
@Override
public void perform(final UpgradeContext context) throws ClientException
{
final LocalizableMessage msg = INFO_UPGRADE_TASK_ADD_CONFIG_FILE.get(fileName);
logger.debug(msg);
final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 0);
final File configFile = new File(templateConfigDirectory, fileName);
try
{
context.notifyProgress(pnc.setProgress(20));
copy(configFile, configDirectory, true);
context.notifyProgress(pnc.setProgress(100));
}
catch (final IOException e)
{
manageTaskException(context, ERR_UPGRADE_ADD_CONFIG_FILE_FAILS.get(
configFile.getName(), e.getMessage()), pnc);
}
}
@Override
public String toString()
{
return INFO_UPGRADE_TASK_ADD_CONFIG_FILE.get(fileName).toString();
}
};
}
/**
* Returns a new upgrade task which deletes a config entry from the underlying
* config file.
*
* @param summary
* The summary of this upgrade task.
* @param dnInLDIF
* The dn to delete in the form of LDIF.
* @return A new upgrade task which applies an LDIF record to all
* configuration entries matching the provided filter.
*/
public static UpgradeTask deleteConfigEntry(final LocalizableMessage summary,
final String dnInLDIF)
{
return updateConfigEntry(summary, null, ChangeOperationType.DELETE, dnInLDIF);
}
/**
* Returns a new upgrade task which applies an LDIF record to all
* configuration entries matching the provided filter.
*
* @param summary
* The summary of this upgrade task.
* @param filter
* The LDAP filter which configuration entries must match.
* @param ldif
* The LDIF record which will be applied to matching entries.
* @return A new upgrade task which applies an LDIF record to all
* configuration entries matching the provided filter.
*/
public static UpgradeTask modifyConfigEntry(final LocalizableMessage summary,
final String filter, final String... ldif)
{
return updateConfigEntry(summary, filter, ChangeOperationType.MODIFY, ldif);
}
/**
* This task adds a new attribute type (must exists in the original file) to
* the specified file placed in parameter. The destination must be a file
* contained in the config/schema folder. E.g : This example adds a new
* attribute type named 'etag' in the 00.core.ldif. The 'etag' attribute
* already exists in the 00-core.ldif template schema file.
*
* <pre>
* register("2.5.0.7192",
* newAttributeTypes(LocalizableMessage.raw("New attribute etag"),
* false, "00-core.ldif",
* "1.3.6.1.4.1.36733.2.1.1.59"));
* </pre>
*
* @param summary
* The summary of the task.
* @param fileName
* The file where to add the new attribute types. This file must be
* contained in the configuration/schema folder.
* @param attributeOids
* The OIDs of the new attributes to add to.
* @return An upgrade task which adds new attribute types, defined previously
* in the configuration template files, reads the definition
* and adds it onto the specified file in parameter.
*/
public static UpgradeTask newAttributeTypes(final LocalizableMessage summary,
final String fileName, final String... attributeOids)
{
return new AbstractUpgradeTask()
{
@Override
public void perform(final UpgradeContext context) throws ClientException
{
logger.debug(summary);
final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 20);
context.notifyProgress(pnc);
final File schemaFileTemplate =
new File(templateConfigSchemaDirectory, fileName);
final File pathDestination = new File(configSchemaDirectory, fileName);
try
{
final int changeCount =
updateSchemaFile(schemaFileTemplate, pathDestination,
attributeOids, null);
displayChangeCount(pathDestination.getPath(), changeCount);
context.notifyProgress(pnc.setProgress(100));
}
catch (final IOException | IllegalStateException e)
{
manageTaskException(context, ERR_UPGRADE_ADDATTRIBUTE_FAILS.get(
schemaFileTemplate.getName(), e.getMessage()), pnc);
}
}
@Override
public String toString()
{
return String.valueOf(summary);
}
};
}
/**
* This task adds a new object class (must exists in the original file) to the
* specified file placed in parameter. The destination must be a file
* contained in the config/schema folder.
*
* @param summary
* The summary of the task.
* @param fileName
* The file where to add the new object classes. This file must be
* contained in the configuration/schema folder.
* @param objectClassesOids
* The OIDs of the new object classes to add to.
* @return An upgrade task which adds new object classes, defined previously
* in the configuration template files,
* reads the definition and adds it onto the specified file in
* parameter.
*/
public static UpgradeTask newObjectClasses(final LocalizableMessage summary,
final String fileName, final String... objectClassesOids)
{
return new AbstractUpgradeTask()
{
@Override
public void perform(final UpgradeContext context) throws ClientException
{
logger.debug(summary);
final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 20);
context.notifyProgress(pnc);
final File schemaFileTemplate =
new File(templateConfigSchemaDirectory, fileName);
final File pathDestination = new File(configSchemaDirectory, fileName);
context.notifyProgress(pnc.setProgress(20));
try
{
final int changeCount =
updateSchemaFile(schemaFileTemplate, pathDestination,
null, objectClassesOids);
displayChangeCount(pathDestination.getPath(), changeCount);
context.notifyProgress(pnc.setProgress(100));
}
catch (final IOException e)
{
manageTaskException(context, ERR_UPGRADE_ADDOBJECTCLASS_FAILS.get(
schemaFileTemplate.getName(), e.getMessage()), pnc);
}
catch (final IllegalStateException e)
{
manageTaskException(context, ERR_UPGRADE_ADDATTRIBUTE_FAILS.get(
schemaFileTemplate.getName(), e.getMessage()), pnc);
}
}
@Override
public String toString()
{
return String.valueOf(summary);
}
};
}
/**
* Re-run the dsjavaproperties tool to rewrite the set-java-home script/batch file.
*
* @param summary
* The summary of the task.
* @return An upgrade task which runs dsjavaproperties.
*/
public static UpgradeTask rerunJavaPropertiesTool(final LocalizableMessage summary)
{
return new AbstractUpgradeTask()
{
@Override
public void perform(UpgradeContext context) throws ClientException
{
logger.debug(summary);
final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 50);
context.notifyProgress(pnc);
int returnValue = JavaPropertiesTool.mainCLI("--quiet");
context.notifyProgress(pnc.setProgress(100));
if (JavaPropertiesTool.ErrorReturnCode.SUCCESSFUL.getReturnCode() != returnValue &&
JavaPropertiesTool.ErrorReturnCode.SUCCESSFUL_NOP.getReturnCode() != returnValue) {
throw new ClientException(ReturnCode.ERROR_UNEXPECTED, ERR_UPGRADE_DSJAVAPROPERTIES_FAILED.get());
}
}
@Override
public String toString()
{
return String.valueOf(summary);
}
};
}
/**
* Creates a group of tasks which will only be invoked if the current version
* is more recent than the provided version. This may be useful in cases where
* a regression was introduced in version X and resolved in a later version Y.
* In this case, the provided upgrade tasks will only be invoked if the
* current version is between X (inclusive) and Y (exclusive).
*
* @param versionString
* The lower bound version. The upgrade tasks will not be applied if
* the current version is older than this version.
* @param tasks
* The group of tasks to invoke if the current version is equal to or
* more recent than {@code versionString}.
* @return An upgrade task which will only be invoked if the current version
* is more recent than the provided version.
*/
public static UpgradeTask regressionInVersion(final String versionString, final UpgradeTask... tasks)
{
final BuildVersion version = BuildVersion.valueOf(versionString);
return conditionalUpgradeTasks(new UpgradeCondition()
{
@Override
public boolean shouldPerformUpgradeTasks(final UpgradeContext context) throws ClientException
{
return context.getFromVersion().compareTo(version) >= 0;
}
@Override
public String toString()
{
return "Regression in version \"" + versionString + "\"";
}
}, tasks);
}
/**
* Creates a group of tasks which will only be invoked if the user confirms agreement. This may be
* useful in cases where a feature is deprecated and the upgrade is capable of migrating the
* configuration to the new replacement feature.
*
* @param message
* The confirmation message.
* @param tasks
* The group of tasks to invoke if the user agrees.
* @return An upgrade task which will only be invoked if the user confirms agreement.
*/
static UpgradeTask requireConfirmation(
final LocalizableMessage message, final int defaultResponse, final UpgradeTask... tasks)
{
return conditionalUpgradeTasks(new UpgradeCondition()
{
@Override
public boolean shouldPerformUpgradeTasks(final UpgradeContext context) throws ClientException
{
return context.confirmYN(INFO_UPGRADE_TASK_NEEDS_USER_CONFIRM.get(message), defaultResponse) == YES;
}
@Override
public String toString()
{
return INFO_UPGRADE_TASK_NEEDS_USER_CONFIRM.get(message).toString();
}
}, tasks);
}
/** Determines whether conditional tasks should be performed. */
interface UpgradeCondition
{
boolean shouldPerformUpgradeTasks(UpgradeContext context) throws ClientException;
}
static UpgradeTask conditionalUpgradeTasks(final UpgradeCondition condition, final UpgradeTask... tasks)
{
return new AbstractUpgradeTask()
{
private boolean shouldPerformUpgradeTasks = true;
@Override
public void prepare(final UpgradeContext context) throws ClientException
{
shouldPerformUpgradeTasks = condition.shouldPerformUpgradeTasks(context);
if (shouldPerformUpgradeTasks)
{
for (UpgradeTask task : tasks)
{
task.prepare(context);
}
}
}
@Override
public void perform(final UpgradeContext context) throws ClientException
{
if (shouldPerformUpgradeTasks)
{
for (UpgradeTask task : tasks)
{
task.perform(context);
}
}
}
@Override
public void postUpgrade(UpgradeContext context) throws ClientException
{
if (shouldPerformUpgradeTasks)
{
boolean isOk = true;
for (final UpgradeTask task : tasks)
{
if (isOk)
{
try
{
task.postUpgrade(context);
}
catch (ClientException e)
{
logger.error(LocalizableMessage.raw(e.getMessage()));
isOk = false;
}
}
else
{
task.postponePostUpgrade(context);
}
}
}
}
@Override
public String toString()
{
final StringBuilder sb = new StringBuilder();
sb.append(condition).append(" = ").append(shouldPerformUpgradeTasks).append('\n');
sb.append('[');
Utils.joinAsString(sb, "\n", (Object[]) tasks);
sb.append(']');
return sb.toString();
}
};
}
/**
* Creates a rebuild all indexes task.
*
* @param summary
* The summary of this upgrade task.
* @return An Upgrade task which rebuild all the indexes.
*/
public static UpgradeTask rebuildAllIndexes(final LocalizableMessage summary)
{
return new AbstractUpgradeTask()
{
private boolean isATaskToPerform;
@Override
public void prepare(UpgradeContext context) throws ClientException
{
Upgrade.setHasPostUpgradeTask(true);
// Requires answer from the user.
isATaskToPerform = context.confirmYN(summary, NO) == YES;
isRebuildAllIndexesIsPresent = true;
isRebuildAllIndexesTaskAccepted = isATaskToPerform;
}
@Override
public void postUpgrade(final UpgradeContext context) throws ClientException
{
if (!isATaskToPerform)
{
postponePostUpgrade(context);
}
}
@Override
public void postponePostUpgrade(UpgradeContext context) throws ClientException
{
context.notify(INFO_UPGRADE_ALL_REBUILD_INDEX_DECLINED.get(), TextOutputCallback.WARNING);
}
@Override
public String toString()
{
return String.valueOf(summary);
}
};
}
/**
* Creates a rebuild index task for a given single index. As this task is
* possibly lengthy, it's considered as a post upgrade task. This task is not
* mandatory; e.g not require user interaction, but could be required to get a
* fully functional server. <br />
* The post upgrade task just register the task. The rebuild indexes tasks are
* completed at the end of the upgrade process.
*
* @param summary
* A message describing why the index needs to be rebuilt and asking
* them whether or not they wish to perform this task after the
* upgrade.
* @param index
* The index to rebuild.
* @return The rebuild index task.
*/
public static UpgradeTask rebuildSingleIndex(final LocalizableMessage summary,
final String index)
{
return new AbstractUpgradeTask()
{
private boolean isATaskToPerform;
@Override
public void prepare(UpgradeContext context) throws ClientException
{
Upgrade.setHasPostUpgradeTask(true);
// Requires answer from the user.
isATaskToPerform = context.confirmYN(summary, NO) == YES;
}
@Override
public void postUpgrade(final UpgradeContext context) throws ClientException
{
if (isATaskToPerform)
{
indexesToRebuild.add(index);
}
else
{
postponePostUpgrade(context);
}
}
@Override
public void postponePostUpgrade(UpgradeContext context) throws ClientException
{
if (!isRebuildAllIndexesIsPresent)
{
context.notify(INFO_UPGRADE_REBUILD_INDEX_DECLINED.get(index), TextOutputCallback.WARNING);
}
}
@Override
public String toString()
{
return String.valueOf(summary);
}
};
}
/**
* This task is processed at the end of the upgrade, rebuilding indexes. If a
* rebuild all indexes has been registered before, it takes the flag
* relatively to single rebuild index.
*
* @return The post upgrade rebuild indexes task.
*/
public static UpgradeTask postUpgradeRebuildIndexes()
{
return new AbstractUpgradeTask()
{
@Override
public void postUpgrade(final UpgradeContext context) throws ClientException
{
LocalizableMessage message = null;
final List<String> args = new LinkedList<>();
if (isRebuildAllIndexesIsPresent && isRebuildAllIndexesTaskAccepted)
{
args.add("--rebuildAll");
message = INFO_UPGRADE_REBUILD_ALL.get();
}
else if (!indexesToRebuild.isEmpty()
&& !isRebuildAllIndexesTaskAccepted)
{
message = INFO_UPGRADE_REBUILD_INDEX_STARTS.get(indexesToRebuild);
// Adding all requested indexes.
for (final String indexToRebuild : indexesToRebuild)
{
args.add("-i");
args.add(indexToRebuild);
}
}
else
{
return;
}
// Startup message.
ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, message, 25);
logger.debug(message);
context.notifyProgress(pnc);
// Sets the arguments like the rebuild index command line.
args.addAll(Arrays.asList(
"-f",
new File(configDirectory, CURRENT_CONFIG_FILE_NAME).getAbsolutePath()));
/*
* Index(es) could be contained in several backends or none, If none,
* the post upgrade tasks succeed and a message is printed in the
* upgrade log file.
*/
final List<String> backends = UpgradeUtils.getIndexedBackendsFromConfig();
if (backends.isEmpty())
{
logger.debug(INFO_UPGRADE_REBUILD_INDEX_NO_BACKEND_FOUND);
logger.debug(INFO_UPGRADE_REBUILD_INDEX_DECLINED, indexesToRebuild);
context.notifyProgress(pnc.setProgress(100));
return;
}
for (final String be : backends)
{
args.add("-b");
args.add(be);
}
// Displays info about command line args for log only.
logger.debug(INFO_UPGRADE_REBUILD_INDEX_ARGUMENTS, args);
/*
* The rebuild-index process just display a status ok / fails. The
* logger stream contains all the log linked to this process. The
* complete process is not displayed in the upgrade console.
*/
final String[] commandLineArgs = args.toArray(new String[args.size()]);
final int result = new RebuildIndex().rebuildIndexesWithinMultipleBackends(
true, UpgradeLog.getPrintStream(), commandLineArgs);
if (result == 0)
{
logger.debug(INFO_UPGRADE_REBUILD_INDEX_ENDS);
context.notifyProgress(pnc.setProgress(100));
}
else
{
final LocalizableMessage msg = ERR_UPGRADE_PERFORMING_POST_TASKS_FAIL.get();
context.notifyProgress(pnc.setProgress(-100));
throw new ClientException(ReturnCode.ERROR_UNEXPECTED, msg);
}
}
@Override
public String toString()
{
return "Post upgrade rebuild indexes task";
}
};
}
/**
* Creates a file object representing config/upgrade/schema.ldif.current which
* the server creates the first time it starts if there are schema
* customizations.
*
* @return An upgrade task which upgrade the config/upgrade folder, creating a
* new schema.ldif.rev which is needed after schema customization for
* starting correctly the server.
*/
public static UpgradeTask updateConfigUpgradeFolder()
{
return new AbstractUpgradeTask()
{
@Override
public void perform(final UpgradeContext context) throws ClientException
{
final LocalizableMessage msg = INFO_UPGRADE_TASK_REFRESH_UPGRADE_DIRECTORY.get();
logger.debug(msg);
final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 20);
context.notifyProgress(pnc);
try
{
String toRevision = context.getToVersion().getRevision();
updateConfigUpgradeSchemaFile(configSchemaDirectory, toRevision);
context.notifyProgress(pnc.setProgress(100));
}
catch (final Exception ex)
{
manageTaskException(context, ERR_UPGRADE_CONFIG_ERROR_UPGRADE_FOLDER.get(ex.getMessage()), pnc);
}
}
@Override
public String toString()
{
return INFO_UPGRADE_TASK_REFRESH_UPGRADE_DIRECTORY.get().toString();
}
};
}
/**
* Renames the SNMP security config file if it exists. Since 2.5.0.7466 this
* file has been renamed.
*
* @param summary
* The summary of this upgrade task.
* @return An upgrade task which renames the old SNMP security config file if
* it exists.
*/
public static UpgradeTask renameSnmpSecurityConfig(final LocalizableMessage summary)
{
return new AbstractUpgradeTask()
{
@Override
public void perform(final UpgradeContext context) throws ClientException
{
/*
* Snmp config file contains old name in old version(like 2.4.5), in
* order to make sure the process will still work after upgrade, we need
* to rename it - only if it exists.
*/
final File snmpDir = UpgradeUtils.configSnmpSecurityDirectory;
if (snmpDir.exists())
{
ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 0);
try
{
final File oldSnmpConfig = new File(snmpDir, "opends-snmp.security");
if (oldSnmpConfig.exists())
{
context.notifyProgress(pnc.setProgress(20));
logger.debug(summary);
final File snmpConfig = new File(snmpDir, "opendj-snmp.security");
FileManager.rename(oldSnmpConfig, snmpConfig);
context.notifyProgress(pnc.setProgress(100));
}
}
catch (final Exception ex)
{
LocalizableMessage msg = ERR_UPGRADE_RENAME_SNMP_SECURITY_CONFIG_FILE.get(ex.getMessage());
manageTaskException(context, msg, pnc);
}
}
}
@Override
public String toString()
{
return String.valueOf(summary);
}
};
}
/**
* Removes the specified file from the file-system.
*
* @param file
* The file to be removed.
* @return An upgrade task which removes the specified file from the file-system.
*/
public static UpgradeTask deleteFile(final File file)
{
return new AbstractUpgradeTask()
{
@Override
public void perform(UpgradeContext context) throws ClientException
{
LocalizableMessage msg = INFO_UPGRADE_TASK_DELETE_FILE.get(file);
ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 0);
context.notifyProgress(pnc);
try
{
FileManager.deleteRecursively(file);
context.notifyProgress(pnc.setProgress(100));
}
catch (Exception e)
{
manageTaskException(context, LocalizableMessage.raw(e.getMessage()), pnc);
}
}
@Override
public String toString()
{
return INFO_UPGRADE_TASK_DELETE_FILE.get(file).toString();
}
};
}
/**
* Creates an upgrade task which is responsible for preparing local-db backend JE databases for a full rebuild once
* they have been converted to pluggable JE backends.
*
* @return An upgrade task which is responsible for preparing local-db backend JE databases.
*/
public static UpgradeTask migrateLocalDBBackendsToJEBackends() {
return new AbstractUpgradeTask() {
/** Properties of JE backends to be migrated. */
class Backend {
final String id;
final boolean isEnabled;
final Set<DN> baseDNs;
final File envDir;
final Map<String, String> renamedDbs = new HashMap<>();
private Backend(Entry config) {
id = config.parseAttribute("ds-cfg-backend-id").asString();
isEnabled = config.parseAttribute("ds-cfg-enabled").asBoolean(false);
baseDNs = config.parseAttribute("ds-cfg-base-dn").asSetOfDN();
String dbDirectory = config.parseAttribute("ds-cfg-db-directory").asString();
File backendParentDirectory = new File(dbDirectory);
if (!backendParentDirectory.isAbsolute()) {
backendParentDirectory = new File(getInstancePath(), dbDirectory);
}
envDir = new File(backendParentDirectory, id);
for (String db : Arrays.asList("compressed_attributes", "compressed_object_classes")) {
renamedDbs.put(db, new TreeName("compressed_schema", db).toString());
}
for (DN baseDN : baseDNs) {
renamedDbs.put(oldName(baseDN), newName(baseDN));
}
}
}
private final List<Backend> backends = new LinkedList<>();
/**
* Finds all the existing JE backends and determines if they can be migrated or not. It will not be possible to
* migrate a JE backend if the id2entry database name cannot easily be determined, which may happen because
* matching rules have changed significantly in 3.0.0.
*/
@Override
public void prepare(final UpgradeContext context) throws ClientException {
// Requires answer from the user.
if (context.confirmYN(INFO_UPGRADE_TASK_MIGRATE_JE_DESCRIPTION.get(), NO) != YES) {
throw new ClientException(ReturnCode.ERROR_USER_CANCELLED,
INFO_UPGRADE_TASK_MIGRATE_JE_CANCELLED.get());
}
final SearchRequest sr = Requests.newSearchRequest("", SearchScope.WHOLE_SUBTREE,
"(objectclass=ds-cfg-local-db-backend)");
try (final EntryReader entryReader = searchConfigFile(sr)) {
// Abort the upgrade if there are JE backends but no JE library.
if (entryReader.hasNext() && !isJeLibraryAvailable()) {
throw new ClientException(ReturnCode.CONSTRAINT_VIOLATION, INFO_UPGRADE_TASK_MIGRATE_JE_NO_JE_LIB.get());
}
while (entryReader.hasNext()) {
Backend backend = new Backend(entryReader.readEntry());
if (backend.isEnabled) {
abortIfBackendCannotBeMigrated(backend);
}
backends.add(backend);
}
} catch (IOException e) {
throw new ClientException(ReturnCode.APPLICATION_ERROR, INFO_UPGRADE_TASK_MIGRATE_CONFIG_READ_FAIL.get(), e);
}
}
private void abortIfBackendCannotBeMigrated(final Backend backend) throws ClientException {
Set<String> existingDatabases = JEHelper.listDatabases(backend.envDir);
for (DN baseDN : backend.baseDNs) {
final String oldName = oldName(baseDN);
if (!existingDatabases.contains(oldName)) {
LocalizableMessage msg = INFO_UPGRADE_TASK_MIGRATE_JE_UGLY_DN.get(backend.id, baseDN);
throw new ClientException(ReturnCode.CONSTRAINT_VIOLATION, msg);
}
}
}
/**
* Renames the compressed schema indexes and id2entry in a 2.x environment to
* the naming scheme used in 3.0.0. Before 3.0.0 JE databases were named as follows:
*
* 1) normalize the base DN
* 2) replace all non-alphanumeric characters with '_'
* 3) append '_'
* 4) append the index name.
*
* For example, id2entry in the base DN dc=white space,dc=com would be named
* dc_white_space_dc_com_id2entry. In 3.0.0 JE databases are named as follows:
*
* 1) normalize the base DN and URL encode it (' ' are converted to %20)
* 2) format as '/' + URL encoded base DN + '/' + index name.
*
* The matching rules in 3.0.0 are not compatible with previous versions, so we need
* to do a best effort attempt to figure out the old database name from a given base DN.
*/
@Override
public void perform(final UpgradeContext context) throws ClientException {
if (!isJeLibraryAvailable()) {
return;
}
for (Backend backend : backends) {
if (backend.isEnabled) {
ProgressNotificationCallback pnc = new ProgressNotificationCallback(
INFORMATION, INFO_UPGRADE_TASK_MIGRATE_JE_SUMMARY_1.get(backend.id), 0);
context.notifyProgress(pnc);
try {
JEHelper.migrateDatabases(backend.envDir, backend.renamedDbs);
context.notifyProgress(pnc.setProgress(100));
} catch (ClientException e) {
manageTaskException(context, e.getMessageObject(), pnc);
}
} else {
// Skip backends which have been disabled.
final ProgressNotificationCallback pnc = new ProgressNotificationCallback(
INFORMATION, INFO_UPGRADE_TASK_MIGRATE_JE_SUMMARY_5.get(backend.id), 0);
context.notifyProgress(pnc);
context.notifyProgress(pnc.setProgress(100));
}
}
}
private boolean isJeLibraryAvailable() {
return isClassAvailable("com.sleepycat.je.Environment");
}
private String newName(final DN baseDN) {
return new TreeName(baseDN.toNormalizedUrlSafeString(), "id2entry").toString();
}
private String oldName(final DN baseDN) {
String s = baseDN.toString();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
builder.append(Character.isLetterOrDigit(c) ? c : '_');
}
builder.append("_id2entry");
return builder.toString();
}
@Override
public String toString()
{
return INFO_UPGRADE_TASK_MIGRATE_JE_SUMMARY_1.get("%s").toString();
}
};
}
/**
* Creates backups of the local DB backends directories by renaming adding them a ".bak" suffix.
* e.g "userRoot" would become "userRoot.bak"
*/
static UpgradeTask renameLocalDBBackendDirectories()
{
return new AbstractUpgradeTask()
{
private boolean reimportRequired;
@Override
public void perform(UpgradeContext context) throws ClientException
{
try
{
Filter filter = Filter.equality("objectclass", "ds-cfg-local-db-backend");
SearchRequest findLocalDBBackends = Requests.newSearchRequest(DN.rootDN(), SearchScope.WHOLE_SUBTREE, filter);
try (final EntryReader jeBackends = searchConfigFile(findLocalDBBackends))
{
while (jeBackends.hasNext())
{
Upgrade.setHasPostUpgradeTask(true);
reimportRequired = true;
Entry jeBackend = jeBackends.readEntry();
File dbParent = UpgradeUtils.getFileForPath(jeBackend.parseAttribute("ds-cfg-db-directory").asString());
String id = jeBackend.parseAttribute("ds-cfg-backend-id").asString();
// Use canonical paths so that the progress message is more readable.
File dbDirectory = new File(dbParent, id).getCanonicalFile();
File dbDirectoryBackup = new File(dbParent, id + ".bak").getCanonicalFile();
if (dbDirectory.exists() && !dbDirectoryBackup.exists())
{
LocalizableMessage msg = INFO_UPGRADE_TASK_RENAME_JE_DB_DIR.get(dbDirectory, dbDirectoryBackup);
ProgressNotificationCallback pnc = new ProgressNotificationCallback(0, msg, 0);
context.notifyProgress(pnc);
boolean renameSucceeded = dbDirectory.renameTo(dbDirectoryBackup);
context.notifyProgress(pnc.setProgress(renameSucceeded ? 100 : -1));
}
}
}
}
catch (Exception e)
{
logger.error(LocalizableMessage.raw(e.getMessage()));
}
}
@Override
public void postUpgrade(UpgradeContext context) throws ClientException
{
postponePostUpgrade(context);
}
@Override
public void postponePostUpgrade(UpgradeContext context) throws ClientException
{
if (reimportRequired)
{
context.notify(INFO_UPGRADE_TASK_RENAME_JE_DB_DIR_WARNING.get(), TextOutputCallback.WARNING);
}
}
@Override
public String toString()
{
return INFO_UPGRADE_TASK_RENAME_JE_DB_DIR.get("%s", "%s").toString();
}
};
}
/** This inner classes causes JE to be lazily linked and prevents runtime errors if JE is not in the classpath. */
static final class JEHelper {
private static ClientException clientException(final File backendDirectory, final DatabaseException e) {
logger.error(LocalizableMessage.raw(StaticUtils.stackTraceToString(e)));
return new ClientException(ReturnCode.CONSTRAINT_VIOLATION,
INFO_UPGRADE_TASK_MIGRATE_JE_ENV_UNREADABLE.get(backendDirectory), e);
}
static Set<String> listDatabases(final File backendDirectory) throws ClientException {
try (Environment je = new Environment(backendDirectory, null)) {
Set<String> databases = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
databases.addAll(je.getDatabaseNames());
return databases;
} catch (DatabaseException e) {
throw clientException(backendDirectory, e);
}
}
static void migrateDatabases(final File envDir, final Map<String, String> renamedDbs) throws ClientException {
EnvironmentConfig config = new EnvironmentConfig().setTransactional(true);
try (Environment je = new Environment(envDir, config)) {
final Transaction txn = je.beginTransaction(null, new TransactionConfig());
try {
for (String dbName : je.getDatabaseNames()) {
String newDbName = renamedDbs.get(dbName);
if (newDbName != null) {
// id2entry or compressed schema should be kept
je.renameDatabase(txn, dbName, newDbName);
} else {
// This index will need rebuilding
je.removeDatabase(txn, dbName);
}
}
txn.commit();
} finally {
txn.abort();
}
} catch (DatabaseException e) {
throw JEHelper.clientException(envDir, e);
}
}
}
private static void displayChangeCount(final String fileName,
final int changeCount)
{
if (changeCount != 0)
{
logger.debug(INFO_UPGRADE_CHANGE_DONE_IN_SPECIFIC_FILE, fileName, changeCount);
}
else
{
logger.debug(INFO_UPGRADE_NO_CHANGE_DONE_IN_SPECIFIC_FILE, fileName);
}
}
private static void displayTaskLogInformation(final String summary,
final String filter, final String... ldif)
{
logger.debug(LocalizableMessage.raw(summary));
if (filter != null)
{
logger.debug(LocalizableMessage.raw(filter));
}
if (ldif != null)
{
logger.debug(LocalizableMessage.raw(Arrays.toString(ldif)));
}
}
private static void manageTaskException(final UpgradeContext context,
final LocalizableMessage message, final ProgressNotificationCallback pnc)
throws ClientException
{
countErrors++;
context.notifyProgress(pnc.setProgress(-100));
logger.error(message);
if (!context.isIgnoreErrorsMode())
{
throw new ClientException(ReturnCode.ERROR_UNEXPECTED, message);
}
}
private static UpgradeTask updateConfigEntry(final LocalizableMessage summary, final String filter,
final ChangeOperationType changeOperationType, final String... ldif)
{
return new AbstractUpgradeTask()
{
@Override
public void perform(final UpgradeContext context) throws ClientException
{
performConfigFileUpdate(summary, filter, changeOperationType, context, ldif);
}
@Override
public String toString()
{
return String.valueOf(summary);
}
};
}
private static void performConfigFileUpdate(final LocalizableMessage summary, final String filter,
final ChangeOperationType changeOperationType,
final UpgradeContext context, final String... ldif)
throws ClientException
{
displayTaskLogInformation(summary.toString(), filter, ldif);
final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 20);
context.notifyProgress(pnc);
try
{
final File configFile =
new File(configDirectory, Installation.CURRENT_CONFIG_FILE_NAME);
final Filter filterVal = filter != null ? Filter.valueOf(filter) : null;
final int changeCount = updateConfigFile(
configFile.getPath(), filterVal, changeOperationType, ldif);
displayChangeCount(configFile.getPath(), changeCount);
context.notifyProgress(pnc.setProgress(100));
}
catch (final Exception e)
{
manageTaskException(context, LocalizableMessage.raw(e.getMessage()), pnc);
}
}
static UpgradeTask clearReplicationDbDirectory()
{
return new AbstractUpgradeTask()
{
private File replicationDbDir;
@Override
public void prepare(final UpgradeContext context) throws ClientException
{
String replDbDir = readReplicationDbDirFromConfig();
if (replDbDir != null
&& context.confirmYN(INFO_UPGRADE_TASK_MIGRATE_CHANGELOG_DESCRIPTION.get(), NO) == YES)
{
replicationDbDir = new File(getInstancePath(), replDbDir).getAbsoluteFile();
}
// if replDbDir == null, then this is not an RS, there is no changelog DB to clear
}
private String readReplicationDbDirFromConfig() throws ClientException
{
final SearchRequest sr = Requests.newSearchRequest(
DN.valueOf("cn=replication server,cn=Multimaster Synchronization,cn=Synchronization Providers,cn=config"),
SearchScope.BASE_OBJECT, Filter.alwaysTrue());
try (final EntryReader entryReader = searchConfigFile(sr))
{
if (entryReader.hasNext())
{
final Entry replServerCfg = entryReader.readEntry();
return replServerCfg.parseAttribute("ds-cfg-replication-db-directory").asString();
}
return null;
}
catch (IOException e)
{
LocalizableMessage msg = INFO_UPGRADE_TASK_MIGRATE_CONFIG_READ_FAIL.get();
throw new ClientException(ReturnCode.APPLICATION_ERROR, msg, e);
}
}
@Override
public void perform(final UpgradeContext context) throws ClientException
{
if (replicationDbDir == null)
{
// there is no changelog DB to clear
return;
}
LocalizableMessage msg = INFO_UPGRADE_TASK_DELETE_CHANGELOG_SUMMARY.get(replicationDbDir);
ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 0);
context.notifyProgress(pnc);
try
{
FileManager.deleteRecursively(replicationDbDir);
context.notifyProgress(pnc.setProgress(100));
}
catch (ClientException e)
{
manageTaskException(context, e.getMessageObject(), pnc);
}
catch (Exception e)
{
manageTaskException(context, LocalizableMessage.raw(e.getLocalizedMessage()), pnc);
}
}
@Override
public String toString()
{
return INFO_UPGRADE_TASK_DELETE_CHANGELOG_SUMMARY.get(replicationDbDir).toString();
}
};
}
/** Prevent instantiation. */
private UpgradeTasks()
{
// Do nothing.
}
}