001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Portions Copyright 2013-2016 ForgeRock AS 025 */ 026package org.opends.server.tools.upgrade; 027 028import static javax.security.auth.callback.ConfirmationCallback.NO; 029import static javax.security.auth.callback.ConfirmationCallback.YES; 030import static javax.security.auth.callback.TextOutputCallback.*; 031 032import static org.opends.messages.ToolMessages.*; 033import static org.opends.server.tools.upgrade.FileManager.copy; 034import static org.opends.server.tools.upgrade.Installation.CURRENT_CONFIG_FILE_NAME; 035import static org.opends.server.tools.upgrade.UpgradeUtils.*; 036import static org.opends.server.util.StaticUtils.isClassAvailable; 037 038import java.io.File; 039import java.io.IOException; 040import java.util.Arrays; 041import java.util.HashMap; 042import java.util.HashSet; 043import java.util.LinkedList; 044import java.util.List; 045import java.util.Map; 046import java.util.Set; 047import java.util.TreeSet; 048 049import javax.security.auth.callback.TextOutputCallback; 050 051import org.forgerock.i18n.LocalizableMessage; 052import org.forgerock.i18n.slf4j.LocalizedLogger; 053import org.forgerock.opendj.ldap.DN; 054import org.forgerock.opendj.ldap.Entry; 055import org.forgerock.opendj.ldap.Filter; 056import org.forgerock.opendj.ldap.SearchScope; 057import org.forgerock.opendj.ldap.requests.Requests; 058import org.forgerock.opendj.ldap.requests.SearchRequest; 059import org.forgerock.opendj.ldif.EntryReader; 060import org.forgerock.util.Utils; 061import org.opends.server.backends.pluggable.spi.TreeName; 062import org.opends.server.tools.JavaPropertiesTool; 063import org.opends.server.tools.RebuildIndex; 064import org.opends.server.util.BuildVersion; 065import org.opends.server.util.ChangeOperationType; 066import org.opends.server.util.StaticUtils; 067 068import com.forgerock.opendj.cli.ClientException; 069import com.forgerock.opendj.cli.ReturnCode; 070import com.sleepycat.je.DatabaseException; 071import com.sleepycat.je.Environment; 072import com.sleepycat.je.EnvironmentConfig; 073import com.sleepycat.je.Transaction; 074import com.sleepycat.je.TransactionConfig; 075 076/** Factory methods for create new upgrade tasks. */ 077public final class UpgradeTasks 078{ 079 /** Logger for the upgrade. */ 080 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 081 082 /** An errors counter in case of ignore errors mode. */ 083 static int countErrors; 084 085 /** Contains all the indexes to rebuild. */ 086 static Set<String> indexesToRebuild = new HashSet<>(); 087 088 /** A flag to avoid rebuild single indexes if 'rebuild all' is selected. */ 089 static boolean isRebuildAllIndexesIsPresent; 090 091 /** A flag for marking 'rebuild all' task accepted by user. */ 092 static boolean isRebuildAllIndexesTaskAccepted; 093 094 /** 095 * Returns a new upgrade task which adds a config entry to the underlying 096 * config file. 097 * 098 * @param summary 099 * The summary of this upgrade task. 100 * @param ldif 101 * The LDIF record which will be applied to matching entries. 102 * @return A new upgrade task which applies an LDIF record to all 103 * configuration entries matching the provided filter. 104 */ 105 public static UpgradeTask addConfigEntry(final LocalizableMessage summary, 106 final String... ldif) 107 { 108 return updateConfigEntry(summary, null, ChangeOperationType.ADD, ldif); 109 } 110 111 /** 112 * This task copies the file placed in parameter within the config / schema 113 * folder. If the file already exists, it's overwritten. 114 * 115 * @param fileName 116 * The name of the file which need to be copied. 117 * @return A task which copy the the file placed in parameter within the 118 * config / schema folder. If the file already exists, it's 119 * overwritten. 120 */ 121 public static UpgradeTask copySchemaFile(final String fileName) 122 { 123 return new AbstractUpgradeTask() 124 { 125 @Override 126 public void perform(final UpgradeContext context) throws ClientException 127 { 128 final LocalizableMessage msg = INFO_UPGRADE_TASK_REPLACE_SCHEMA_FILE.get(fileName); 129 logger.debug(msg); 130 131 final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 0); 132 133 final File schemaFileTemplate = 134 new File(templateConfigSchemaDirectory, fileName); 135 136 try 137 { 138 context.notifyProgress(pnc.setProgress(20)); 139 if (!schemaFileTemplate.exists() || schemaFileTemplate.length() == 0) 140 { 141 throw new IOException(ERR_UPGRADE_CORRUPTED_TEMPLATE 142 .get(schemaFileTemplate.getPath()).toString()); 143 } 144 copy(schemaFileTemplate, configSchemaDirectory, true); 145 context.notifyProgress(pnc.setProgress(100)); 146 } 147 catch (final IOException e) 148 { 149 manageTaskException(context, ERR_UPGRADE_COPYSCHEMA_FAILS.get( 150 schemaFileTemplate.getName(), e.getMessage()), pnc); 151 } 152 } 153 154 @Override 155 public String toString() 156 { 157 return INFO_UPGRADE_TASK_REPLACE_SCHEMA_FILE.get(fileName).toString(); 158 } 159 }; 160 } 161 162 /** 163 * This task copies the file placed in parameter within the config folder. If 164 * the file already exists, it's overwritten. 165 * 166 * @param fileName 167 * The name of the file which need to be copied. 168 * @return A task which copy the the file placed in parameter within the 169 * config folder. If the file already exists, it's overwritten. 170 */ 171 public static UpgradeTask addConfigFile(final String fileName) 172 { 173 return new AbstractUpgradeTask() 174 { 175 @Override 176 public void perform(final UpgradeContext context) throws ClientException 177 { 178 final LocalizableMessage msg = INFO_UPGRADE_TASK_ADD_CONFIG_FILE.get(fileName); 179 logger.debug(msg); 180 181 final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 0); 182 183 final File configFile = new File(templateConfigDirectory, fileName); 184 185 try 186 { 187 context.notifyProgress(pnc.setProgress(20)); 188 189 copy(configFile, configDirectory, true); 190 context.notifyProgress(pnc.setProgress(100)); 191 } 192 catch (final IOException e) 193 { 194 manageTaskException(context, ERR_UPGRADE_ADD_CONFIG_FILE_FAILS.get( 195 configFile.getName(), e.getMessage()), pnc); 196 } 197 } 198 199 @Override 200 public String toString() 201 { 202 return INFO_UPGRADE_TASK_ADD_CONFIG_FILE.get(fileName).toString(); 203 } 204 }; 205 } 206 207 /** 208 * Returns a new upgrade task which deletes a config entry from the underlying 209 * config file. 210 * 211 * @param summary 212 * The summary of this upgrade task. 213 * @param dnInLDIF 214 * The dn to delete in the form of LDIF. 215 * @return A new upgrade task which applies an LDIF record to all 216 * configuration entries matching the provided filter. 217 */ 218 public static UpgradeTask deleteConfigEntry(final LocalizableMessage summary, 219 final String dnInLDIF) 220 { 221 return updateConfigEntry(summary, null, ChangeOperationType.DELETE, dnInLDIF); 222 } 223 224 /** 225 * Returns a new upgrade task which applies an LDIF record to all 226 * configuration entries matching the provided filter. 227 * 228 * @param summary 229 * The summary of this upgrade task. 230 * @param filter 231 * The LDAP filter which configuration entries must match. 232 * @param ldif 233 * The LDIF record which will be applied to matching entries. 234 * @return A new upgrade task which applies an LDIF record to all 235 * configuration entries matching the provided filter. 236 */ 237 public static UpgradeTask modifyConfigEntry(final LocalizableMessage summary, 238 final String filter, final String... ldif) 239 { 240 return updateConfigEntry(summary, filter, ChangeOperationType.MODIFY, ldif); 241 } 242 243 /** 244 * This task adds a new attribute type (must exists in the original file) to 245 * the specified file placed in parameter. The destination must be a file 246 * contained in the config/schema folder. E.g : This example adds a new 247 * attribute type named 'etag' in the 00.core.ldif. The 'etag' attribute 248 * already exists in the 00-core.ldif template schema file. 249 * 250 * <pre> 251 * register("2.5.0.7192", 252 * newAttributeTypes(LocalizableMessage.raw("New attribute etag"), 253 * false, "00-core.ldif", 254 * "1.3.6.1.4.1.36733.2.1.1.59")); 255 * </pre> 256 * 257 * @param summary 258 * The summary of the task. 259 * @param fileName 260 * The file where to add the new attribute types. This file must be 261 * contained in the configuration/schema folder. 262 * @param attributeOids 263 * The OIDs of the new attributes to add to. 264 * @return An upgrade task which adds new attribute types, defined previously 265 * in the configuration template files, reads the definition 266 * and adds it onto the specified file in parameter. 267 */ 268 public static UpgradeTask newAttributeTypes(final LocalizableMessage summary, 269 final String fileName, final String... attributeOids) 270 { 271 return new AbstractUpgradeTask() 272 { 273 @Override 274 public void perform(final UpgradeContext context) throws ClientException 275 { 276 logger.debug(summary); 277 278 final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 20); 279 context.notifyProgress(pnc); 280 281 final File schemaFileTemplate = 282 new File(templateConfigSchemaDirectory, fileName); 283 284 final File pathDestination = new File(configSchemaDirectory, fileName); 285 try 286 { 287 final int changeCount = 288 updateSchemaFile(schemaFileTemplate, pathDestination, 289 attributeOids, null); 290 291 displayChangeCount(pathDestination.getPath(), changeCount); 292 293 context.notifyProgress(pnc.setProgress(100)); 294 } 295 catch (final IOException | IllegalStateException e) 296 { 297 manageTaskException(context, ERR_UPGRADE_ADDATTRIBUTE_FAILS.get( 298 schemaFileTemplate.getName(), e.getMessage()), pnc); 299 } 300 } 301 302 @Override 303 public String toString() 304 { 305 return String.valueOf(summary); 306 } 307 }; 308 } 309 310 /** 311 * This task adds a new object class (must exists in the original file) to the 312 * specified file placed in parameter. The destination must be a file 313 * contained in the config/schema folder. 314 * 315 * @param summary 316 * The summary of the task. 317 * @param fileName 318 * The file where to add the new object classes. This file must be 319 * contained in the configuration/schema folder. 320 * @param objectClassesOids 321 * The OIDs of the new object classes to add to. 322 * @return An upgrade task which adds new object classes, defined previously 323 * in the configuration template files, 324 * reads the definition and adds it onto the specified file in 325 * parameter. 326 */ 327 public static UpgradeTask newObjectClasses(final LocalizableMessage summary, 328 final String fileName, final String... objectClassesOids) 329 { 330 return new AbstractUpgradeTask() 331 { 332 @Override 333 public void perform(final UpgradeContext context) throws ClientException 334 { 335 logger.debug(summary); 336 337 final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 20); 338 context.notifyProgress(pnc); 339 340 final File schemaFileTemplate = 341 new File(templateConfigSchemaDirectory, fileName); 342 343 final File pathDestination = new File(configSchemaDirectory, fileName); 344 345 context.notifyProgress(pnc.setProgress(20)); 346 347 try 348 { 349 final int changeCount = 350 updateSchemaFile(schemaFileTemplate, pathDestination, 351 null, objectClassesOids); 352 353 displayChangeCount(pathDestination.getPath(), changeCount); 354 355 context.notifyProgress(pnc.setProgress(100)); 356 } 357 catch (final IOException e) 358 { 359 manageTaskException(context, ERR_UPGRADE_ADDOBJECTCLASS_FAILS.get( 360 schemaFileTemplate.getName(), e.getMessage()), pnc); 361 } 362 catch (final IllegalStateException e) 363 { 364 manageTaskException(context, ERR_UPGRADE_ADDATTRIBUTE_FAILS.get( 365 schemaFileTemplate.getName(), e.getMessage()), pnc); 366 } 367 } 368 369 @Override 370 public String toString() 371 { 372 return String.valueOf(summary); 373 } 374 }; 375 } 376 377 /** 378 * Re-run the dsjavaproperties tool to rewrite the set-java-home script/batch file. 379 * 380 * @param summary 381 * The summary of the task. 382 * @return An upgrade task which runs dsjavaproperties. 383 */ 384 public static UpgradeTask rerunJavaPropertiesTool(final LocalizableMessage summary) 385 { 386 return new AbstractUpgradeTask() 387 { 388 @Override 389 public void perform(UpgradeContext context) throws ClientException 390 { 391 logger.debug(summary); 392 393 final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 50); 394 context.notifyProgress(pnc); 395 396 int returnValue = JavaPropertiesTool.mainCLI("--quiet"); 397 context.notifyProgress(pnc.setProgress(100)); 398 399 if (JavaPropertiesTool.ErrorReturnCode.SUCCESSFUL.getReturnCode() != returnValue && 400 JavaPropertiesTool.ErrorReturnCode.SUCCESSFUL_NOP.getReturnCode() != returnValue) { 401 throw new ClientException(ReturnCode.ERROR_UNEXPECTED, ERR_UPGRADE_DSJAVAPROPERTIES_FAILED.get()); 402 } 403 } 404 405 @Override 406 public String toString() 407 { 408 return String.valueOf(summary); 409 } 410 }; 411 } 412 413 /** 414 * Creates a group of tasks which will only be invoked if the current version 415 * is more recent than the provided version. This may be useful in cases where 416 * a regression was introduced in version X and resolved in a later version Y. 417 * In this case, the provided upgrade tasks will only be invoked if the 418 * current version is between X (inclusive) and Y (exclusive). 419 * 420 * @param versionString 421 * The lower bound version. The upgrade tasks will not be applied if 422 * the current version is older than this version. 423 * @param tasks 424 * The group of tasks to invoke if the current version is equal to or 425 * more recent than {@code versionString}. 426 * @return An upgrade task which will only be invoked if the current version 427 * is more recent than the provided version. 428 */ 429 public static UpgradeTask regressionInVersion(final String versionString, final UpgradeTask... tasks) 430 { 431 final BuildVersion version = BuildVersion.valueOf(versionString); 432 return conditionalUpgradeTasks(new UpgradeCondition() 433 { 434 @Override 435 public boolean shouldPerformUpgradeTasks(final UpgradeContext context) throws ClientException 436 { 437 return context.getFromVersion().compareTo(version) >= 0; 438 } 439 440 @Override 441 public String toString() 442 { 443 return "Regression in version \"" + versionString + "\""; 444 } 445 }, tasks); 446 } 447 448 /** 449 * Creates a group of tasks which will only be invoked if the user confirms agreement. This may be 450 * useful in cases where a feature is deprecated and the upgrade is capable of migrating the 451 * configuration to the new replacement feature. 452 * 453 * @param message 454 * The confirmation message. 455 * @param tasks 456 * The group of tasks to invoke if the user agrees. 457 * @return An upgrade task which will only be invoked if the user confirms agreement. 458 */ 459 static UpgradeTask requireConfirmation( 460 final LocalizableMessage message, final int defaultResponse, final UpgradeTask... tasks) 461 { 462 return conditionalUpgradeTasks(new UpgradeCondition() 463 { 464 @Override 465 public boolean shouldPerformUpgradeTasks(final UpgradeContext context) throws ClientException 466 { 467 return context.confirmYN(INFO_UPGRADE_TASK_NEEDS_USER_CONFIRM.get(message), defaultResponse) == YES; 468 } 469 470 @Override 471 public String toString() 472 { 473 return INFO_UPGRADE_TASK_NEEDS_USER_CONFIRM.get(message).toString(); 474 } 475 }, tasks); 476 } 477 478 /** Determines whether conditional tasks should be performed. */ 479 interface UpgradeCondition 480 { 481 boolean shouldPerformUpgradeTasks(UpgradeContext context) throws ClientException; 482 } 483 484 static UpgradeTask conditionalUpgradeTasks(final UpgradeCondition condition, final UpgradeTask... tasks) 485 { 486 return new AbstractUpgradeTask() 487 { 488 private boolean shouldPerformUpgradeTasks = true; 489 490 @Override 491 public void prepare(final UpgradeContext context) throws ClientException 492 { 493 shouldPerformUpgradeTasks = condition.shouldPerformUpgradeTasks(context); 494 if (shouldPerformUpgradeTasks) 495 { 496 for (UpgradeTask task : tasks) 497 { 498 task.prepare(context); 499 } 500 } 501 } 502 503 @Override 504 public void perform(final UpgradeContext context) throws ClientException 505 { 506 if (shouldPerformUpgradeTasks) 507 { 508 for (UpgradeTask task : tasks) 509 { 510 task.perform(context); 511 } 512 } 513 } 514 515 @Override 516 public void postUpgrade(UpgradeContext context) throws ClientException 517 { 518 if (shouldPerformUpgradeTasks) 519 { 520 boolean isOk = true; 521 for (final UpgradeTask task : tasks) 522 { 523 if (isOk) 524 { 525 try 526 { 527 task.postUpgrade(context); 528 } 529 catch (ClientException e) 530 { 531 logger.error(LocalizableMessage.raw(e.getMessage())); 532 isOk = false; 533 } 534 } 535 else 536 { 537 task.postponePostUpgrade(context); 538 } 539 } 540 } 541 } 542 543 @Override 544 public String toString() 545 { 546 final StringBuilder sb = new StringBuilder(); 547 sb.append(condition).append(" = ").append(shouldPerformUpgradeTasks).append('\n'); 548 sb.append('['); 549 Utils.joinAsString(sb, "\n", (Object[]) tasks); 550 sb.append(']'); 551 return sb.toString(); 552 } 553 }; 554 } 555 556 /** 557 * Creates a rebuild all indexes task. 558 * 559 * @param summary 560 * The summary of this upgrade task. 561 * @return An Upgrade task which rebuild all the indexes. 562 */ 563 public static UpgradeTask rebuildAllIndexes(final LocalizableMessage summary) 564 { 565 return new AbstractUpgradeTask() 566 { 567 private boolean isATaskToPerform; 568 569 @Override 570 public void prepare(UpgradeContext context) throws ClientException 571 { 572 Upgrade.setHasPostUpgradeTask(true); 573 // Requires answer from the user. 574 isATaskToPerform = context.confirmYN(summary, NO) == YES; 575 isRebuildAllIndexesIsPresent = true; 576 isRebuildAllIndexesTaskAccepted = isATaskToPerform; 577 } 578 579 @Override 580 public void postUpgrade(final UpgradeContext context) throws ClientException 581 { 582 if (!isATaskToPerform) 583 { 584 postponePostUpgrade(context); 585 } 586 } 587 588 @Override 589 public void postponePostUpgrade(UpgradeContext context) throws ClientException 590 { 591 context.notify(INFO_UPGRADE_ALL_REBUILD_INDEX_DECLINED.get(), TextOutputCallback.WARNING); 592 } 593 594 @Override 595 public String toString() 596 { 597 return String.valueOf(summary); 598 } 599 }; 600 } 601 602 /** 603 * Creates a rebuild index task for a given single index. As this task is 604 * possibly lengthy, it's considered as a post upgrade task. This task is not 605 * mandatory; e.g not require user interaction, but could be required to get a 606 * fully functional server. <br /> 607 * The post upgrade task just register the task. The rebuild indexes tasks are 608 * completed at the end of the upgrade process. 609 * 610 * @param summary 611 * A message describing why the index needs to be rebuilt and asking 612 * them whether or not they wish to perform this task after the 613 * upgrade. 614 * @param index 615 * The index to rebuild. 616 * @return The rebuild index task. 617 */ 618 public static UpgradeTask rebuildSingleIndex(final LocalizableMessage summary, 619 final String index) 620 { 621 return new AbstractUpgradeTask() 622 { 623 private boolean isATaskToPerform; 624 625 @Override 626 public void prepare(UpgradeContext context) throws ClientException 627 { 628 Upgrade.setHasPostUpgradeTask(true); 629 // Requires answer from the user. 630 isATaskToPerform = context.confirmYN(summary, NO) == YES; 631 } 632 633 @Override 634 public void postUpgrade(final UpgradeContext context) throws ClientException 635 { 636 if (isATaskToPerform) 637 { 638 indexesToRebuild.add(index); 639 } 640 else 641 { 642 postponePostUpgrade(context); 643 } 644 } 645 646 @Override 647 public void postponePostUpgrade(UpgradeContext context) throws ClientException 648 { 649 if (!isRebuildAllIndexesIsPresent) 650 { 651 context.notify(INFO_UPGRADE_REBUILD_INDEX_DECLINED.get(index), TextOutputCallback.WARNING); 652 } 653 } 654 655 @Override 656 public String toString() 657 { 658 return String.valueOf(summary); 659 } 660 }; 661 } 662 663 /** 664 * This task is processed at the end of the upgrade, rebuilding indexes. If a 665 * rebuild all indexes has been registered before, it takes the flag 666 * relatively to single rebuild index. 667 * 668 * @return The post upgrade rebuild indexes task. 669 */ 670 public static UpgradeTask postUpgradeRebuildIndexes() 671 { 672 return new AbstractUpgradeTask() 673 { 674 @Override 675 public void postUpgrade(final UpgradeContext context) throws ClientException 676 { 677 LocalizableMessage message = null; 678 final List<String> args = new LinkedList<>(); 679 680 if (isRebuildAllIndexesIsPresent && isRebuildAllIndexesTaskAccepted) 681 { 682 args.add("--rebuildAll"); 683 message = INFO_UPGRADE_REBUILD_ALL.get(); 684 } 685 else if (!indexesToRebuild.isEmpty() 686 && !isRebuildAllIndexesTaskAccepted) 687 { 688 message = INFO_UPGRADE_REBUILD_INDEX_STARTS.get(indexesToRebuild); 689 690 // Adding all requested indexes. 691 for (final String indexToRebuild : indexesToRebuild) 692 { 693 args.add("-i"); 694 args.add(indexToRebuild); 695 } 696 } 697 else 698 { 699 return; 700 } 701 // Startup message. 702 ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, message, 25); 703 logger.debug(message); 704 context.notifyProgress(pnc); 705 706 // Sets the arguments like the rebuild index command line. 707 args.addAll(Arrays.asList( 708 "-f", 709 new File(configDirectory, CURRENT_CONFIG_FILE_NAME).getAbsolutePath())); 710 711 /* 712 * Index(es) could be contained in several backends or none, If none, 713 * the post upgrade tasks succeed and a message is printed in the 714 * upgrade log file. 715 */ 716 final List<String> backends = UpgradeUtils.getIndexedBackendsFromConfig(); 717 if (backends.isEmpty()) 718 { 719 logger.debug(INFO_UPGRADE_REBUILD_INDEX_NO_BACKEND_FOUND); 720 logger.debug(INFO_UPGRADE_REBUILD_INDEX_DECLINED, indexesToRebuild); 721 context.notifyProgress(pnc.setProgress(100)); 722 return; 723 } 724 725 for (final String be : backends) 726 { 727 args.add("-b"); 728 args.add(be); 729 } 730 731 // Displays info about command line args for log only. 732 logger.debug(INFO_UPGRADE_REBUILD_INDEX_ARGUMENTS, args); 733 734 /* 735 * The rebuild-index process just display a status ok / fails. The 736 * logger stream contains all the log linked to this process. The 737 * complete process is not displayed in the upgrade console. 738 */ 739 final String[] commandLineArgs = args.toArray(new String[args.size()]); 740 final int result = new RebuildIndex().rebuildIndexesWithinMultipleBackends( 741 true, UpgradeLog.getPrintStream(), commandLineArgs); 742 743 if (result == 0) 744 { 745 logger.debug(INFO_UPGRADE_REBUILD_INDEX_ENDS); 746 context.notifyProgress(pnc.setProgress(100)); 747 } 748 else 749 { 750 final LocalizableMessage msg = ERR_UPGRADE_PERFORMING_POST_TASKS_FAIL.get(); 751 context.notifyProgress(pnc.setProgress(-100)); 752 throw new ClientException(ReturnCode.ERROR_UNEXPECTED, msg); 753 } 754 } 755 756 @Override 757 public String toString() 758 { 759 return "Post upgrade rebuild indexes task"; 760 } 761 }; 762 } 763 764 /** 765 * Creates a file object representing config/upgrade/schema.ldif.current which 766 * the server creates the first time it starts if there are schema 767 * customizations. 768 * 769 * @return An upgrade task which upgrade the config/upgrade folder, creating a 770 * new schema.ldif.rev which is needed after schema customization for 771 * starting correctly the server. 772 */ 773 public static UpgradeTask updateConfigUpgradeFolder() 774 { 775 return new AbstractUpgradeTask() 776 { 777 @Override 778 public void perform(final UpgradeContext context) throws ClientException 779 { 780 final LocalizableMessage msg = INFO_UPGRADE_TASK_REFRESH_UPGRADE_DIRECTORY.get(); 781 logger.debug(msg); 782 783 final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 20); 784 context.notifyProgress(pnc); 785 786 try 787 { 788 String toRevision = context.getToVersion().getRevision(); 789 updateConfigUpgradeSchemaFile(configSchemaDirectory, toRevision); 790 791 context.notifyProgress(pnc.setProgress(100)); 792 } 793 catch (final Exception ex) 794 { 795 manageTaskException(context, ERR_UPGRADE_CONFIG_ERROR_UPGRADE_FOLDER.get(ex.getMessage()), pnc); 796 } 797 } 798 799 @Override 800 public String toString() 801 { 802 return INFO_UPGRADE_TASK_REFRESH_UPGRADE_DIRECTORY.get().toString(); 803 } 804 }; 805 } 806 807 /** 808 * Renames the SNMP security config file if it exists. Since 2.5.0.7466 this 809 * file has been renamed. 810 * 811 * @param summary 812 * The summary of this upgrade task. 813 * @return An upgrade task which renames the old SNMP security config file if 814 * it exists. 815 */ 816 public static UpgradeTask renameSnmpSecurityConfig(final LocalizableMessage summary) 817 { 818 return new AbstractUpgradeTask() 819 { 820 @Override 821 public void perform(final UpgradeContext context) throws ClientException 822 { 823 /* 824 * Snmp config file contains old name in old version(like 2.4.5), in 825 * order to make sure the process will still work after upgrade, we need 826 * to rename it - only if it exists. 827 */ 828 final File snmpDir = UpgradeUtils.configSnmpSecurityDirectory; 829 if (snmpDir.exists()) 830 { 831 ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 0); 832 try 833 { 834 final File oldSnmpConfig = new File(snmpDir, "opends-snmp.security"); 835 if (oldSnmpConfig.exists()) 836 { 837 context.notifyProgress(pnc.setProgress(20)); 838 logger.debug(summary); 839 840 final File snmpConfig = new File(snmpDir, "opendj-snmp.security"); 841 FileManager.rename(oldSnmpConfig, snmpConfig); 842 843 context.notifyProgress(pnc.setProgress(100)); 844 } 845 } 846 catch (final Exception ex) 847 { 848 LocalizableMessage msg = ERR_UPGRADE_RENAME_SNMP_SECURITY_CONFIG_FILE.get(ex.getMessage()); 849 manageTaskException(context, msg, pnc); 850 } 851 } 852 } 853 854 @Override 855 public String toString() 856 { 857 return String.valueOf(summary); 858 } 859 }; 860 } 861 862 /** 863 * Removes the specified file from the file-system. 864 * 865 * @param file 866 * The file to be removed. 867 * @return An upgrade task which removes the specified file from the file-system. 868 */ 869 public static UpgradeTask deleteFile(final File file) 870 { 871 return new AbstractUpgradeTask() 872 { 873 @Override 874 public void perform(UpgradeContext context) throws ClientException 875 { 876 LocalizableMessage msg = INFO_UPGRADE_TASK_DELETE_FILE.get(file); 877 ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 0); 878 context.notifyProgress(pnc); 879 try 880 { 881 FileManager.deleteRecursively(file); 882 context.notifyProgress(pnc.setProgress(100)); 883 } 884 catch (Exception e) 885 { 886 manageTaskException(context, LocalizableMessage.raw(e.getMessage()), pnc); 887 } 888 } 889 890 @Override 891 public String toString() 892 { 893 return INFO_UPGRADE_TASK_DELETE_FILE.get(file).toString(); 894 } 895 }; 896 } 897 898 /** 899 * Creates an upgrade task which is responsible for preparing local-db backend JE databases for a full rebuild once 900 * they have been converted to pluggable JE backends. 901 * 902 * @return An upgrade task which is responsible for preparing local-db backend JE databases. 903 */ 904 public static UpgradeTask migrateLocalDBBackendsToJEBackends() { 905 return new AbstractUpgradeTask() { 906 /** Properties of JE backends to be migrated. */ 907 class Backend { 908 final String id; 909 final boolean isEnabled; 910 final Set<DN> baseDNs; 911 final File envDir; 912 final Map<String, String> renamedDbs = new HashMap<>(); 913 914 private Backend(Entry config) { 915 id = config.parseAttribute("ds-cfg-backend-id").asString(); 916 isEnabled = config.parseAttribute("ds-cfg-enabled").asBoolean(false); 917 baseDNs = config.parseAttribute("ds-cfg-base-dn").asSetOfDN(); 918 String dbDirectory = config.parseAttribute("ds-cfg-db-directory").asString(); 919 File backendParentDirectory = new File(dbDirectory); 920 if (!backendParentDirectory.isAbsolute()) { 921 backendParentDirectory = new File(getInstancePath(), dbDirectory); 922 } 923 envDir = new File(backendParentDirectory, id); 924 for (String db : Arrays.asList("compressed_attributes", "compressed_object_classes")) { 925 renamedDbs.put(db, new TreeName("compressed_schema", db).toString()); 926 } 927 for (DN baseDN : baseDNs) { 928 renamedDbs.put(oldName(baseDN), newName(baseDN)); 929 } 930 } 931 } 932 933 private final List<Backend> backends = new LinkedList<>(); 934 935 /** 936 * Finds all the existing JE backends and determines if they can be migrated or not. It will not be possible to 937 * migrate a JE backend if the id2entry database name cannot easily be determined, which may happen because 938 * matching rules have changed significantly in 3.0.0. 939 */ 940 @Override 941 public void prepare(final UpgradeContext context) throws ClientException { 942 // Requires answer from the user. 943 if (context.confirmYN(INFO_UPGRADE_TASK_MIGRATE_JE_DESCRIPTION.get(), NO) != YES) { 944 throw new ClientException(ReturnCode.ERROR_USER_CANCELLED, 945 INFO_UPGRADE_TASK_MIGRATE_JE_CANCELLED.get()); 946 } 947 948 final SearchRequest sr = Requests.newSearchRequest("", SearchScope.WHOLE_SUBTREE, 949 "(objectclass=ds-cfg-local-db-backend)"); 950 try (final EntryReader entryReader = searchConfigFile(sr)) { 951 // Abort the upgrade if there are JE backends but no JE library. 952 if (entryReader.hasNext() && !isJeLibraryAvailable()) { 953 throw new ClientException(ReturnCode.CONSTRAINT_VIOLATION, INFO_UPGRADE_TASK_MIGRATE_JE_NO_JE_LIB.get()); 954 } 955 while (entryReader.hasNext()) { 956 Backend backend = new Backend(entryReader.readEntry()); 957 if (backend.isEnabled) { 958 abortIfBackendCannotBeMigrated(backend); 959 } 960 backends.add(backend); 961 } 962 } catch (IOException e) { 963 throw new ClientException(ReturnCode.APPLICATION_ERROR, INFO_UPGRADE_TASK_MIGRATE_CONFIG_READ_FAIL.get(), e); 964 } 965 } 966 967 private void abortIfBackendCannotBeMigrated(final Backend backend) throws ClientException { 968 Set<String> existingDatabases = JEHelper.listDatabases(backend.envDir); 969 for (DN baseDN : backend.baseDNs) { 970 final String oldName = oldName(baseDN); 971 if (!existingDatabases.contains(oldName)) { 972 LocalizableMessage msg = INFO_UPGRADE_TASK_MIGRATE_JE_UGLY_DN.get(backend.id, baseDN); 973 throw new ClientException(ReturnCode.CONSTRAINT_VIOLATION, msg); 974 } 975 } 976 } 977 978 /** 979 * Renames the compressed schema indexes and id2entry in a 2.x environment to 980 * the naming scheme used in 3.0.0. Before 3.0.0 JE databases were named as follows: 981 * 982 * 1) normalize the base DN 983 * 2) replace all non-alphanumeric characters with '_' 984 * 3) append '_' 985 * 4) append the index name. 986 * 987 * For example, id2entry in the base DN dc=white space,dc=com would be named 988 * dc_white_space_dc_com_id2entry. In 3.0.0 JE databases are named as follows: 989 * 990 * 1) normalize the base DN and URL encode it (' ' are converted to %20) 991 * 2) format as '/' + URL encoded base DN + '/' + index name. 992 * 993 * The matching rules in 3.0.0 are not compatible with previous versions, so we need 994 * to do a best effort attempt to figure out the old database name from a given base DN. 995 */ 996 @Override 997 public void perform(final UpgradeContext context) throws ClientException { 998 if (!isJeLibraryAvailable()) { 999 return; 1000 } 1001 1002 for (Backend backend : backends) { 1003 if (backend.isEnabled) { 1004 ProgressNotificationCallback pnc = new ProgressNotificationCallback( 1005 INFORMATION, INFO_UPGRADE_TASK_MIGRATE_JE_SUMMARY_1.get(backend.id), 0); 1006 context.notifyProgress(pnc); 1007 try { 1008 JEHelper.migrateDatabases(backend.envDir, backend.renamedDbs); 1009 context.notifyProgress(pnc.setProgress(100)); 1010 } catch (ClientException e) { 1011 manageTaskException(context, e.getMessageObject(), pnc); 1012 } 1013 } else { 1014 // Skip backends which have been disabled. 1015 final ProgressNotificationCallback pnc = new ProgressNotificationCallback( 1016 INFORMATION, INFO_UPGRADE_TASK_MIGRATE_JE_SUMMARY_5.get(backend.id), 0); 1017 context.notifyProgress(pnc); 1018 context.notifyProgress(pnc.setProgress(100)); 1019 } 1020 } 1021 } 1022 1023 private boolean isJeLibraryAvailable() { 1024 return isClassAvailable("com.sleepycat.je.Environment"); 1025 } 1026 1027 private String newName(final DN baseDN) { 1028 return new TreeName(baseDN.toNormalizedUrlSafeString(), "id2entry").toString(); 1029 } 1030 1031 private String oldName(final DN baseDN) { 1032 String s = baseDN.toString(); 1033 StringBuilder builder = new StringBuilder(); 1034 for (int i = 0; i < s.length(); i++) { 1035 char c = s.charAt(i); 1036 builder.append(Character.isLetterOrDigit(c) ? c : '_'); 1037 } 1038 builder.append("_id2entry"); 1039 return builder.toString(); 1040 } 1041 1042 @Override 1043 public String toString() 1044 { 1045 return INFO_UPGRADE_TASK_MIGRATE_JE_SUMMARY_1.get("%s").toString(); 1046 } 1047 }; 1048 } 1049 1050 /** 1051 * Creates backups of the local DB backends directories by renaming adding them a ".bak" suffix. 1052 * e.g "userRoot" would become "userRoot.bak" 1053 */ 1054 static UpgradeTask renameLocalDBBackendDirectories() 1055 { 1056 return new AbstractUpgradeTask() 1057 { 1058 private boolean reimportRequired; 1059 1060 @Override 1061 public void perform(UpgradeContext context) throws ClientException 1062 { 1063 try 1064 { 1065 Filter filter = Filter.equality("objectclass", "ds-cfg-local-db-backend"); 1066 SearchRequest findLocalDBBackends = Requests.newSearchRequest(DN.rootDN(), SearchScope.WHOLE_SUBTREE, filter); 1067 try (final EntryReader jeBackends = searchConfigFile(findLocalDBBackends)) 1068 { 1069 while (jeBackends.hasNext()) 1070 { 1071 Upgrade.setHasPostUpgradeTask(true); 1072 reimportRequired = true; 1073 1074 Entry jeBackend = jeBackends.readEntry(); 1075 File dbParent = UpgradeUtils.getFileForPath(jeBackend.parseAttribute("ds-cfg-db-directory").asString()); 1076 String id = jeBackend.parseAttribute("ds-cfg-backend-id").asString(); 1077 1078 // Use canonical paths so that the progress message is more readable. 1079 File dbDirectory = new File(dbParent, id).getCanonicalFile(); 1080 File dbDirectoryBackup = new File(dbParent, id + ".bak").getCanonicalFile(); 1081 if (dbDirectory.exists() && !dbDirectoryBackup.exists()) 1082 { 1083 LocalizableMessage msg = INFO_UPGRADE_TASK_RENAME_JE_DB_DIR.get(dbDirectory, dbDirectoryBackup); 1084 ProgressNotificationCallback pnc = new ProgressNotificationCallback(0, msg, 0); 1085 context.notifyProgress(pnc); 1086 boolean renameSucceeded = dbDirectory.renameTo(dbDirectoryBackup); 1087 context.notifyProgress(pnc.setProgress(renameSucceeded ? 100 : -1)); 1088 } 1089 } 1090 } 1091 } 1092 catch (Exception e) 1093 { 1094 logger.error(LocalizableMessage.raw(e.getMessage())); 1095 } 1096 } 1097 1098 @Override 1099 public void postUpgrade(UpgradeContext context) throws ClientException 1100 { 1101 postponePostUpgrade(context); 1102 } 1103 1104 @Override 1105 public void postponePostUpgrade(UpgradeContext context) throws ClientException 1106 { 1107 if (reimportRequired) 1108 { 1109 context.notify(INFO_UPGRADE_TASK_RENAME_JE_DB_DIR_WARNING.get(), TextOutputCallback.WARNING); 1110 } 1111 } 1112 1113 @Override 1114 public String toString() 1115 { 1116 return INFO_UPGRADE_TASK_RENAME_JE_DB_DIR.get("%s", "%s").toString(); 1117 } 1118 }; 1119 } 1120 1121 /** This inner classes causes JE to be lazily linked and prevents runtime errors if JE is not in the classpath. */ 1122 static final class JEHelper { 1123 private static ClientException clientException(final File backendDirectory, final DatabaseException e) { 1124 logger.error(LocalizableMessage.raw(StaticUtils.stackTraceToString(e))); 1125 return new ClientException(ReturnCode.CONSTRAINT_VIOLATION, 1126 INFO_UPGRADE_TASK_MIGRATE_JE_ENV_UNREADABLE.get(backendDirectory), e); 1127 } 1128 1129 static Set<String> listDatabases(final File backendDirectory) throws ClientException { 1130 try (Environment je = new Environment(backendDirectory, null)) { 1131 Set<String> databases = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); 1132 databases.addAll(je.getDatabaseNames()); 1133 return databases; 1134 } catch (DatabaseException e) { 1135 throw clientException(backendDirectory, e); 1136 } 1137 } 1138 1139 static void migrateDatabases(final File envDir, final Map<String, String> renamedDbs) throws ClientException { 1140 EnvironmentConfig config = new EnvironmentConfig().setTransactional(true); 1141 try (Environment je = new Environment(envDir, config)) { 1142 final Transaction txn = je.beginTransaction(null, new TransactionConfig()); 1143 try { 1144 for (String dbName : je.getDatabaseNames()) { 1145 String newDbName = renamedDbs.get(dbName); 1146 if (newDbName != null) { 1147 // id2entry or compressed schema should be kept 1148 je.renameDatabase(txn, dbName, newDbName); 1149 } else { 1150 // This index will need rebuilding 1151 je.removeDatabase(txn, dbName); 1152 } 1153 } 1154 txn.commit(); 1155 } finally { 1156 txn.abort(); 1157 } 1158 } catch (DatabaseException e) { 1159 throw JEHelper.clientException(envDir, e); 1160 } 1161 } 1162 } 1163 1164 private static void displayChangeCount(final String fileName, 1165 final int changeCount) 1166 { 1167 if (changeCount != 0) 1168 { 1169 logger.debug(INFO_UPGRADE_CHANGE_DONE_IN_SPECIFIC_FILE, fileName, changeCount); 1170 } 1171 else 1172 { 1173 logger.debug(INFO_UPGRADE_NO_CHANGE_DONE_IN_SPECIFIC_FILE, fileName); 1174 } 1175 } 1176 1177 private static void displayTaskLogInformation(final String summary, 1178 final String filter, final String... ldif) 1179 { 1180 logger.debug(LocalizableMessage.raw(summary)); 1181 if (filter != null) 1182 { 1183 logger.debug(LocalizableMessage.raw(filter)); 1184 } 1185 if (ldif != null) 1186 { 1187 logger.debug(LocalizableMessage.raw(Arrays.toString(ldif))); 1188 } 1189 } 1190 1191 private static void manageTaskException(final UpgradeContext context, 1192 final LocalizableMessage message, final ProgressNotificationCallback pnc) 1193 throws ClientException 1194 { 1195 countErrors++; 1196 context.notifyProgress(pnc.setProgress(-100)); 1197 logger.error(message); 1198 if (!context.isIgnoreErrorsMode()) 1199 { 1200 throw new ClientException(ReturnCode.ERROR_UNEXPECTED, message); 1201 } 1202 } 1203 1204 private static UpgradeTask updateConfigEntry(final LocalizableMessage summary, final String filter, 1205 final ChangeOperationType changeOperationType, final String... ldif) 1206 { 1207 return new AbstractUpgradeTask() 1208 { 1209 @Override 1210 public void perform(final UpgradeContext context) throws ClientException 1211 { 1212 performConfigFileUpdate(summary, filter, changeOperationType, context, ldif); 1213 } 1214 1215 @Override 1216 public String toString() 1217 { 1218 return String.valueOf(summary); 1219 } 1220 }; 1221 } 1222 1223 private static void performConfigFileUpdate(final LocalizableMessage summary, final String filter, 1224 final ChangeOperationType changeOperationType, 1225 final UpgradeContext context, final String... ldif) 1226 throws ClientException 1227 { 1228 displayTaskLogInformation(summary.toString(), filter, ldif); 1229 1230 final ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, summary, 20); 1231 context.notifyProgress(pnc); 1232 1233 try 1234 { 1235 final File configFile = 1236 new File(configDirectory, Installation.CURRENT_CONFIG_FILE_NAME); 1237 1238 final Filter filterVal = filter != null ? Filter.valueOf(filter) : null; 1239 final int changeCount = updateConfigFile( 1240 configFile.getPath(), filterVal, changeOperationType, ldif); 1241 1242 displayChangeCount(configFile.getPath(), changeCount); 1243 1244 context.notifyProgress(pnc.setProgress(100)); 1245 } 1246 catch (final Exception e) 1247 { 1248 manageTaskException(context, LocalizableMessage.raw(e.getMessage()), pnc); 1249 } 1250 } 1251 1252 static UpgradeTask clearReplicationDbDirectory() 1253 { 1254 return new AbstractUpgradeTask() 1255 { 1256 private File replicationDbDir; 1257 1258 @Override 1259 public void prepare(final UpgradeContext context) throws ClientException 1260 { 1261 String replDbDir = readReplicationDbDirFromConfig(); 1262 if (replDbDir != null 1263 && context.confirmYN(INFO_UPGRADE_TASK_MIGRATE_CHANGELOG_DESCRIPTION.get(), NO) == YES) 1264 { 1265 replicationDbDir = new File(getInstancePath(), replDbDir).getAbsoluteFile(); 1266 } 1267 // if replDbDir == null, then this is not an RS, there is no changelog DB to clear 1268 } 1269 1270 private String readReplicationDbDirFromConfig() throws ClientException 1271 { 1272 final SearchRequest sr = Requests.newSearchRequest( 1273 DN.valueOf("cn=replication server,cn=Multimaster Synchronization,cn=Synchronization Providers,cn=config"), 1274 SearchScope.BASE_OBJECT, Filter.alwaysTrue()); 1275 try (final EntryReader entryReader = searchConfigFile(sr)) 1276 { 1277 if (entryReader.hasNext()) 1278 { 1279 final Entry replServerCfg = entryReader.readEntry(); 1280 return replServerCfg.parseAttribute("ds-cfg-replication-db-directory").asString(); 1281 } 1282 return null; 1283 } 1284 catch (IOException e) 1285 { 1286 LocalizableMessage msg = INFO_UPGRADE_TASK_MIGRATE_CONFIG_READ_FAIL.get(); 1287 throw new ClientException(ReturnCode.APPLICATION_ERROR, msg, e); 1288 } 1289 } 1290 1291 @Override 1292 public void perform(final UpgradeContext context) throws ClientException 1293 { 1294 if (replicationDbDir == null) 1295 { 1296 // there is no changelog DB to clear 1297 return; 1298 } 1299 1300 LocalizableMessage msg = INFO_UPGRADE_TASK_DELETE_CHANGELOG_SUMMARY.get(replicationDbDir); 1301 ProgressNotificationCallback pnc = new ProgressNotificationCallback(INFORMATION, msg, 0); 1302 context.notifyProgress(pnc); 1303 try 1304 { 1305 FileManager.deleteRecursively(replicationDbDir); 1306 context.notifyProgress(pnc.setProgress(100)); 1307 } 1308 catch (ClientException e) 1309 { 1310 manageTaskException(context, e.getMessageObject(), pnc); 1311 } 1312 catch (Exception e) 1313 { 1314 manageTaskException(context, LocalizableMessage.raw(e.getLocalizedMessage()), pnc); 1315 } 1316 } 1317 1318 @Override 1319 public String toString() 1320 { 1321 return INFO_UPGRADE_TASK_DELETE_CHANGELOG_SUMMARY.get(replicationDbDir).toString(); 1322 } 1323 }; 1324 } 1325 1326 /** Prevent instantiation. */ 1327 private UpgradeTasks() 1328 { 1329 // Do nothing. 1330 } 1331}