/* * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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-2009 Sun Microsystems, Inc. */ package org.opends.quicksetup.upgrader; import org.opends.messages.Message; import org.opends.messages.MessageBuilder; import static org.opends.messages.QuickSetupMessages.*; import org.opends.quicksetup.ApplicationException; import org.opends.quicksetup.ReturnCode; import org.opends.quicksetup.Installation; import org.opends.quicksetup.UserInteraction; import org.opends.quicksetup.Constants; import org.opends.quicksetup.util.ExternalTools; import org.opends.quicksetup.util.FileManager; import org.opends.quicksetup.util.Utils; import org.opends.quicksetup.util.OperationOutput; import org.opends.quicksetup.util.InProcessServerController; import org.opends.server.util.LDIFReader; import org.opends.server.util.ChangeRecordEntry; import org.opends.server.types.LDIFImportConfig; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; /** * This class handles migration of both schema and configuration * for an installation that is in the process of being upgraded. * * This class is intented to be used in two differnt stages of the * upgrade process: first, before installation's filesystem bits * have been upgraded, a call to both {@link #calculateConfigCustomizations()} * and {@link #calculateSchemaCustomizations()} should be made in * order to capture what changes will need to be made to the server * later on. Then once the filesystem bit have been upgraded calls to * {@link #migrateConfiguration()} and {@link #migrateSchema()} are * made to actually perform the migration. */ public class MigrationManager { static private final Logger LOG = Logger.getLogger(MigrationManager.class.getName()); /** Describes a particular component to be migrated. */ private enum Component { SCHEMA, CONFIGURATION } private Installation installation; private File backupDir; private UserInteraction ui; private boolean isSchemaCustomized; private boolean mustRunDSJavaProperties; /** * Creates a new parameterized instance. * @param installation describing the file layout of the installation * to be migrated * @param backupDir directory where diffs between the base configuration * and schema are to be stored * @param ui user interaction allowing this class to prompt the user * for feedback if necessary. This object can be null in which * case this class will throw an exception rather than prompt * the user for more information if necessary. */ public MigrationManager(Installation installation, File backupDir, UserInteraction ui) { if (installation == null) { throw new NullPointerException("installation cannot be null"); } if (backupDir == null) { throw new NullPointerException("backup directory cannot be null"); } this.installation = installation; this.backupDir = backupDir; this.ui = ui; } /** * Diffs the current and base schema for the * Installation associated with this class and * generates a diff file that can later be used to migrate * an upgraded server. * * @throws ApplicationException if there is a problem performing * the diff operation */ public void calculateSchemaCustomizations() throws ApplicationException { isSchemaCustomized = false; if (installation.getStatus().schemaHasBeenModified()) { LOG.log(Level.INFO, "Schema contains customizations that will " + "be migrated"); try { ldifDiff(installation.getBaseSchemaFile(), installation.getSchemaConcatFile(), getCustomSchemaDiffFile(), true); isSchemaCustomized = true; } catch (ApplicationException ae) { throw ae; } catch (Exception e) { throw ApplicationException.createFileSystemException( INFO_ERROR_DETERMINING_CUSTOM_SCHEMA.get(), e); } } else { LOG.log(Level.INFO, "No schema customizations to migrate"); } } /** * Diffs the current and base configuration for the * Installation associated with this class and * generates a diff file that can later be used to migrate * an upgraded server. * * @throws ApplicationException if there is a problem performing * the diff operation */ public void calculateConfigCustomizations() throws ApplicationException { try { if (installation.getCurrentConfiguration().hasBeenModified()) { LOG.log(Level.INFO, "Configuration contains customizations that will " + "be migrated"); try { ldifDiff(installation.getBaseConfigurationFile(), installation.getCurrentConfigurationFile(), getCustomConfigDiffFile(), false); } catch (ApplicationException ae) { throw ae; } catch (Exception e) { throw ApplicationException.createFileSystemException( INFO_ERROR_DETERMINING_CUSTOM_CONFIG.get(), e); } } else { LOG.log(Level.INFO, "No configuration customizations to migrate"); } } catch (IOException e) { throw ApplicationException.createFileSystemException( INFO_ERROR_DETERMINING_CUSTOM_CONFIG.get(), e); } mustRunDSJavaProperties = installation.getSetJavaHomeFile().exists(); } /** * Migrates a configuration using the diff file generated by a call * to {@link MigrationManager#calculateConfigCustomizations()}. * @throws ApplicationException if there is an error migrating the * configuration */ public void migrateConfiguration() throws ApplicationException { try { File configDiff = getCustomConfigDiffFile(); if (configDiff.exists()) { modify(configDiff, Component.CONFIGURATION); } } catch (ApplicationException ae) { throw ae; } catch (Exception e) { Message msg = INFO_ERROR_APPLYING_CUSTOM_CONFIG.get(); LOG.log(Level.INFO, msg.toString(), e); throw new ApplicationException( ReturnCode.IMPORT_ERROR, msg, e); } } /** * Tells whether the ADS backend file must be migrated or not. * This method assumes that the ADS backend file in the original install * has not been overwritten. * @return true if the tools properties file must be migrated * and false otherwise. * @throws IOException if there is an error accessing the installation * information. */ public boolean mustMigrateADS() throws IOException { return !installation.getADSBackendFile().exists(); } /** * Migrates the ADS backend file. * @param newADSFile the new ADS backend file. * @throws ApplicationException if there is an error copying the new * ADS backend file. */ public void migrateADS(File newADSFile) throws ApplicationException { FileManager fileManager = new FileManager(); fileManager.copy(newADSFile, installation.getConfigurationDirectory(), true); } /** * Tells whether the tools properties file must be migrated or not. * This method assumes that the tools properties file in the original install * has not been overwritten. * @return true if the tools properties file must be migrated * and false otherwise. */ public boolean mustMigrateToolProperties() { return !installation.getToolsPropertiesFile().exists(); } /** * Tells whether a set-java-home file exists and so if we have to run * dsjavaproperties to regenerate it. * @return true if the set-java-home file exists and * false otherwise. */ public boolean mustRunDSJavaProperties() { return mustRunDSJavaProperties; } /** * Migrates the tools properties file. * @param newPropertiesFile the new properties file. * @throws ApplicationException if there is an error copying the new * properties file. */ public void migrateToolPropertiesFile(File newPropertiesFile) throws ApplicationException { FileManager fileManager = new FileManager(); fileManager.copy(newPropertiesFile, installation.getConfigurationDirectory(), true); } /** * Migrates a schema using the diff file generated by a call * to {@link MigrationManager#calculateSchemaCustomizations()}. * @throws ApplicationException if there is an error migrating the * schema */ public void migrateSchema() throws ApplicationException { try { File schemaDiff = getCustomSchemaDiffFile(); if (schemaDiff.exists()) { modify(schemaDiff, Component.SCHEMA); } } catch (ApplicationException ae) { throw ae; } catch (Exception e) { Message msg = INFO_ERROR_APPLYING_CUSTOM_SCHEMA.get(); LOG.log(Level.INFO, msg.toString(), e); throw new ApplicationException( ReturnCode.IMPORT_ERROR, msg, e); } } /** * Returns a value to indicate whether the installation associated * with this class contains schema customizations. A call to * {@link #calculateSchemaCustomizations()} must first be made in * order for this value to be valid. * * @return boolean where true indicates schema customization */ public boolean isSchemaCustomized() { return isSchemaCustomized; } /** * Returns a value to indicate whether the installation associated * with this class contains configuration customizations. A call to * {@link #calculateConfigCustomizations()} must first be made in * order for this value to be valid. * * @return boolean where true indicates configuration customization * @throws java.io.IOException if there was a problem reading the * current configuration file. */ public boolean isConfigurationCustomized() throws IOException { return installation.getCurrentConfiguration().hasBeenModified(); } /** * Applies modifications contained in an LDIF file to the server. * * @param ldifFile LDIF file to apply * @param component being modified * @throws Exception if something goes wrong. This may be an * ApplicationException if the user cancels the application * altogether or an LDIFException or IOException should other * problems occur. */ private void modify(File ldifFile, Component component) throws Exception { InProcessServerController ipsc = new InProcessServerController(installation); LDIFImportConfig importCfg = new LDIFImportConfig( Utils.getPath(ldifFile)); LDIFReader ldifReader = new LDIFReader(importCfg); ArrayList remainingChanges = new ArrayList(); ChangeRecordEntry o = ldifReader.readChangeRecord(false); while (o != null) { remainingChanges.add(o); o = ldifReader.readChangeRecord(false); } ArrayList appliedChanges = new ArrayList(); // In the particular case of the schema, the order on which the // the modifications are applied is important. That is why we retry // until no modification works. while (remainingChanges.size() > 0) { Exception firstException = null; appliedChanges.clear(); for (ChangeRecordEntry cre : remainingChanges) { try { ipsc.modify(cre); appliedChanges.add(cre); } catch (Exception e) { if (firstException == null) { firstException = e; } } } remainingChanges.removeAll(appliedChanges); if ((firstException != null) && (appliedChanges.size() == 0)) { if (ui != null) { Message cancel = INFO_CANCEL_BUTTON_LABEL.get(); Message cont = INFO_CONTINUE_BUTTON_LABEL.get(); Message retry = INFO_RETRY_BUTTON_LABEL.get(); // Now changeList contains the changes that could not be applied: // just ask to confirm about the first element. Object r = ui.confirm( getModificationErrorSummary(component), getModificationErrorMessage(remainingChanges.get(0)), Message.raw(firstException.getLocalizedMessage()), INFO_ERROR_UPGRADE_MIGRATION.get(), UserInteraction.MessageType.ERROR, new Message[]{cancel, cont, retry}, cancel, null); if (cont.equals(r)) { remainingChanges.remove(0); } else if (retry.equals(r)) { // do nothing; will retry; } else { throw new ApplicationException( ReturnCode.CANCELED, INFO_UPGRADE_CANCELED.get(), firstException); } } else { throw firstException; } } } } private Message getModificationErrorSummary(Component c) { Message summary; switch(c) { case SCHEMA: summary = INFO_ERROR_UPGRADE_MIGRATION_SCHEMA.get(); break; case CONFIGURATION: summary = INFO_ERROR_UPGRADE_MIGRATION_CONFIG.get(); break; default: summary = Message.EMPTY; } return summary; } private Message getModificationErrorMessage(ChangeRecordEntry cre) { MessageBuilder msg = new MessageBuilder(); if (Utils.isCli()) { msg.append(Constants.LINE_SEPARATOR); } else { msg.append(Constants.HTML_LINE_BREAK); } if (cre != null) { switch (cre.getChangeOperationType()) { case MODIFY: if (Utils.isCli()) { msg.append(INFO_ERROR_UPGRADE_MIGRATION_MODIFY_CLI.get( cre.getDN().toNormalizedString())); } else { msg.append(INFO_ERROR_UPGRADE_MIGRATION_MODIFY.get( cre.getDN().toNormalizedString())); } break; case ADD: if (Utils.isCli()) { msg.append(INFO_ERROR_UPGRADE_MIGRATION_ADD_CLI.get( cre.getDN().toNormalizedString())); } else { msg.append(INFO_ERROR_UPGRADE_MIGRATION_ADD.get( cre.getDN().toNormalizedString())); } break; case DELETE: if (Utils.isCli()) { msg.append(INFO_ERROR_UPGRADE_MIGRATION_DELETE_CLI.get( cre.getDN().toNormalizedString())); } else { msg.append(INFO_ERROR_UPGRADE_MIGRATION_DELETE.get( cre.getDN().toNormalizedString())); } break; default: LOG.log(Level.INFO, "Unexpected change operation type " + cre.getChangeOperationType()); if (Utils.isCli()) { msg.append(INFO_ERROR_UPGRADE_MIGRATION_UNEXPECTED_CLI.get( cre.getDN().toNormalizedString())); } else { msg.append(INFO_ERROR_UPGRADE_MIGRATION_UNEXPECTED.get( cre.getDN().toNormalizedString())); } break; } } if (Utils.isCli()) { msg.append(Constants.LINE_SEPARATOR); msg.append(Constants.LINE_SEPARATOR); msg.append(INFO_ERROR_UPGRADE_MIGRATION_NOTE_CLI.get( Utils.getPath(backupDir))); } else { msg.append(Constants.HTML_LINE_BREAK); msg.append(Constants.HTML_LINE_BREAK); msg.append(INFO_ERROR_UPGRADE_MIGRATION_NOTE.get( Utils.getPath(backupDir))); } return msg.toMessage(); } private void ldifDiff(File source, File target, File output, boolean splitMods) throws ApplicationException, IOException, InterruptedException { ExternalTools et = new ExternalTools(installation); String[] args; if (splitMods) { args = new String[]{ "-o", Utils.getPath(output), "-O", "-S" }; } else { args = new String[]{ "-o", Utils.getPath(output), "-O" }; } OperationOutput oo = et.ldifDiff(source, target, args); int ret = oo.getReturnCode(); if (ret != 0) { throw new ApplicationException( ReturnCode.TOOL_ERROR, INFO_ERROR_LDIF_DIFF_TOOL_RETURN_CODE.get(Integer.toString(ret)), null); } } private File getCustomConfigDiffFile() throws IOException { return new File(backupDir, "config.custom.diff"); } private File getCustomSchemaDiffFile() throws IOException { return new File(backupDir, "schema.custom.diff"); } private File getCustomADSDiffFile() throws IOException { return new File(backupDir, "admin-backend.custom.diff"); } }