001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2006-2009 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS 026 */ 027package org.opends.server.extensions; 028 029import static org.forgerock.util.Reject.*; 030import static org.opends.messages.ConfigMessages.*; 031import static org.opends.server.config.ConfigConstants.*; 032import static org.opends.server.extensions.ExtensionsConstants.*; 033import static org.opends.server.util.ServerConstants.*; 034import static org.opends.server.util.StaticUtils.*; 035 036import java.io.File; 037import java.io.FileInputStream; 038import java.io.FileOutputStream; 039import java.io.IOException; 040import java.io.InputStream; 041import java.nio.file.Path; 042import java.security.MessageDigest; 043import java.util.*; 044import java.util.concurrent.ConcurrentHashMap; 045import java.util.concurrent.ConcurrentMap; 046import java.util.zip.GZIPInputStream; 047import java.util.zip.GZIPOutputStream; 048 049import org.forgerock.i18n.LocalizableMessage; 050import org.forgerock.i18n.LocalizableMessageBuilder; 051import org.forgerock.i18n.LocalizableMessageDescriptor.Arg1; 052import org.forgerock.i18n.slf4j.LocalizedLogger; 053import org.forgerock.opendj.config.server.ConfigChangeResult; 054import org.forgerock.opendj.config.server.ConfigException; 055import org.forgerock.opendj.ldap.ByteString; 056import org.forgerock.opendj.ldap.ConditionResult; 057import org.forgerock.opendj.ldap.ResultCode; 058import org.forgerock.opendj.ldap.SearchScope; 059import org.forgerock.util.Utils; 060import org.opends.server.admin.std.server.ConfigFileHandlerBackendCfg; 061import org.opends.server.api.AlertGenerator; 062import org.opends.server.api.Backupable; 063import org.opends.server.api.ClientConnection; 064import org.opends.server.api.ConfigAddListener; 065import org.opends.server.api.ConfigChangeListener; 066import org.opends.server.api.ConfigDeleteListener; 067import org.opends.server.api.ConfigHandler; 068import org.opends.server.config.ConfigEntry; 069import org.opends.server.core.AddOperation; 070import org.opends.server.core.DeleteOperation; 071import org.opends.server.core.DirectoryServer; 072import org.opends.server.core.ModifyDNOperation; 073import org.opends.server.core.ModifyOperation; 074import org.opends.server.core.SearchOperation; 075import org.opends.server.core.ServerContext; 076import org.opends.server.schema.GeneralizedTimeSyntax; 077import org.opends.server.tools.LDIFModify; 078import org.opends.server.types.*; 079import org.opends.server.util.BackupManager; 080import org.opends.server.util.LDIFException; 081import org.opends.server.util.LDIFReader; 082import org.opends.server.util.LDIFWriter; 083import org.opends.server.util.StaticUtils; 084import org.opends.server.util.TimeThread; 085 086/** 087 * This class defines a simple configuration handler for the Directory Server 088 * that will read the server configuration from an LDIF file. 089 */ 090public class ConfigFileHandler 091 extends ConfigHandler<ConfigFileHandlerBackendCfg> 092 implements AlertGenerator, Backupable 093{ 094 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 095 096 /** The fully-qualified name of this class. */ 097 private static final String CLASS_NAME = 098 "org.opends.server.extensions.ConfigFileHandler"; 099 100 /** 101 * The privilege array containing both the CONFIG_READ and CONFIG_WRITE 102 * privileges. 103 */ 104 private static final Privilege[] CONFIG_READ_AND_WRITE = 105 { 106 Privilege.CONFIG_READ, 107 Privilege.CONFIG_WRITE 108 }; 109 110 111 112 /** Indicates whether to maintain a configuration archive. */ 113 private boolean maintainConfigArchive; 114 115 /** Indicates whether to start using the last known good configuration. */ 116 private boolean useLastKnownGoodConfig; 117 118 /** 119 * A SHA-1 digest of the last known configuration. This should only be 120 * incorrect if the server configuration file has been manually edited with 121 * the server online, which is a bad thing. 122 */ 123 private byte[] configurationDigest; 124 125 /** 126 * The mapping that holds all of the configuration entries that have been read 127 * from the LDIF file. 128 */ 129 private ConcurrentMap<DN,ConfigEntry> configEntries; 130 131 /** The reference to the configuration root entry. */ 132 private ConfigEntry configRootEntry; 133 134 /** The set of base DNs for this config handler backend. */ 135 private DN[] baseDNs; 136 137 /** The maximum config archive size to maintain. */ 138 private int maxConfigArchiveSize; 139 140 /** 141 * The write lock used to ensure that only one thread can apply a 142 * configuration update at any given time. 143 */ 144 private final Object configLock = new Object(); 145 146 /** The path to the configuration file. */ 147 private String configFile; 148 149 /** The install root directory for the Directory Server. */ 150 private String serverRoot; 151 152 /** The instance root directory for the Directory Server. */ 153 private String instanceRoot; 154 155 /** 156 * Creates a new instance of this config file handler. No initialization 157 * should be performed here, as all of that work should be done in the 158 * <CODE>initializeConfigHandler</CODE> method. 159 */ 160 public ConfigFileHandler() 161 { 162 super(); 163 } 164 165 /** {@inheritDoc} */ 166 @Override 167 public void initializeConfigHandler(String configFile, boolean checkSchema) 168 throws InitializationException 169 { 170 // Determine whether we should try to start using the last known good 171 // configuration. If so, then only do so if such a file exists. If it 172 // doesn't exist, then fall back on the active configuration file. 173 this.configFile = configFile; 174 DirectoryEnvironmentConfig envConfig = DirectoryServer.getEnvironmentConfig(); 175 useLastKnownGoodConfig = envConfig.useLastKnownGoodConfiguration(); 176 File f; 177 if (useLastKnownGoodConfig) 178 { 179 f = new File(configFile + ".startok"); 180 if (! f.exists()) 181 { 182 logger.warn(WARN_CONFIG_FILE_NO_STARTOK_FILE, f.getAbsolutePath(), configFile); 183 useLastKnownGoodConfig = false; 184 f = new File(configFile); 185 } 186 else 187 { 188 logger.info(NOTE_CONFIG_FILE_USING_STARTOK_FILE, f.getAbsolutePath(), configFile); 189 } 190 } 191 else 192 { 193 f = new File(configFile); 194 } 195 196 try 197 { 198 if (! f.exists()) 199 { 200 LocalizableMessage message = ERR_CONFIG_FILE_DOES_NOT_EXIST.get( 201 f.getAbsolutePath()); 202 throw new InitializationException(message); 203 } 204 } 205 catch (InitializationException ie) 206 { 207 logger.traceException(ie); 208 209 throw ie; 210 } 211 catch (Exception e) 212 { 213 logger.traceException(e); 214 215 LocalizableMessage message = ERR_CONFIG_FILE_CANNOT_VERIFY_EXISTENCE.get(f.getAbsolutePath(), e); 216 throw new InitializationException(message); 217 } 218 219 220 // Check to see if a configuration archive exists. If not, then create one. 221 // If so, then check whether the current configuration matches the last 222 // configuration in the archive. If it doesn't, then archive it. 223 maintainConfigArchive = envConfig.maintainConfigArchive(); 224 maxConfigArchiveSize = envConfig.getMaxConfigArchiveSize(); 225 if (maintainConfigArchive && !useLastKnownGoodConfig) 226 { 227 try 228 { 229 configurationDigest = calculateConfigDigest(); 230 } 231 catch (DirectoryException de) 232 { 233 throw new InitializationException(de.getMessageObject(), de.getCause()); 234 } 235 236 File archiveDirectory = new File(f.getParent(), CONFIG_ARCHIVE_DIR_NAME); 237 if (archiveDirectory.exists()) 238 { 239 try 240 { 241 byte[] lastDigest = getLastConfigDigest(archiveDirectory); 242 if (! Arrays.equals(configurationDigest, lastDigest)) 243 { 244 writeConfigArchive(); 245 } 246 } catch (Exception e) {} 247 } 248 else 249 { 250 writeConfigArchive(); 251 } 252 } 253 254 255 256 // Fixme -- Should we add a hash or signature check here? 257 258 259 // See if there is a config changes file. If there is, then try to apply 260 // the changes contained in it. 261 File changesFile = new File(f.getParent(), CONFIG_CHANGES_NAME); 262 try 263 { 264 if (changesFile.exists()) 265 { 266 applyChangesFile(f, changesFile); 267 if (maintainConfigArchive) 268 { 269 configurationDigest = calculateConfigDigest(); 270 writeConfigArchive(); 271 } 272 } 273 } 274 catch (Exception e) 275 { 276 logger.traceException(e); 277 278 LocalizableMessage message = ERR_CONFIG_UNABLE_TO_APPLY_STARTUP_CHANGES.get( 279 changesFile.getAbsolutePath(), e); 280 throw new InitializationException(message, e); 281 } 282 283 284 // We will use the LDIF reader to read the configuration file. Create an 285 // LDIF import configuration to do this and then get the reader. 286 LDIFReader reader; 287 try 288 { 289 LDIFImportConfig importConfig = new LDIFImportConfig(f.getAbsolutePath()); 290 291 // FIXME -- Should we support encryption or compression for the config? 292 293 reader = new LDIFReader(importConfig); 294 } 295 catch (Exception e) 296 { 297 logger.traceException(e); 298 299 LocalizableMessage message = ERR_CONFIG_FILE_CANNOT_OPEN_FOR_READ.get( 300 f.getAbsolutePath(), e); 301 throw new InitializationException(message, e); 302 } 303 304 305 // Read the first entry from the configuration file. 306 Entry entry; 307 try 308 { 309 entry = reader.readEntry(checkSchema); 310 } 311 catch (LDIFException le) 312 { 313 logger.traceException(le); 314 315 close(reader); 316 317 LocalizableMessage message = ERR_CONFIG_FILE_INVALID_LDIF_ENTRY.get( 318 le.getLineNumber(), f.getAbsolutePath(), le); 319 throw new InitializationException(message, le); 320 } 321 catch (Exception e) 322 { 323 logger.traceException(e); 324 325 close(reader); 326 327 LocalizableMessage message = 328 ERR_CONFIG_FILE_READ_ERROR.get(f.getAbsolutePath(), e); 329 throw new InitializationException(message, e); 330 } 331 332 333 // Make sure that the provide LDIF file is not empty. 334 if (entry == null) 335 { 336 close(reader); 337 338 LocalizableMessage message = ERR_CONFIG_FILE_EMPTY.get(f.getAbsolutePath()); 339 throw new InitializationException(message); 340 } 341 342 343 // Make sure that the DN of this entry is equal to the config root DN. 344 try 345 { 346 DN configRootDN = DN.valueOf(DN_CONFIG_ROOT); 347 if (! entry.getName().equals(configRootDN)) 348 { 349 throw new InitializationException(ERR_CONFIG_FILE_INVALID_BASE_DN.get( 350 f.getAbsolutePath(), entry.getName(), DN_CONFIG_ROOT)); 351 } 352 } 353 catch (InitializationException ie) 354 { 355 logger.traceException(ie); 356 357 close(reader); 358 throw ie; 359 } 360 catch (Exception e) 361 { 362 logger.traceException(e); 363 364 close(reader); 365 366 // This should not happen, so we can use a generic error here. 367 LocalizableMessage message = ERR_CONFIG_FILE_GENERIC_ERROR.get(f.getAbsolutePath(), e); 368 throw new InitializationException(message, e); 369 } 370 371 372 // Convert the entry to a configuration entry and put it in the config 373 // hash. 374 configEntries = new ConcurrentHashMap<>(); 375 configRootEntry = new ConfigEntry(entry, null); 376 configEntries.put(entry.getName(), configRootEntry); 377 378 379 // Iterate through the rest of the configuration file and process the 380 // remaining entries. 381 while (true) 382 { 383 // Read the next entry from the configuration. 384 try 385 { 386 entry = reader.readEntry(checkSchema); 387 } 388 catch (LDIFException le) 389 { 390 logger.traceException(le); 391 392 close(reader); 393 394 LocalizableMessage message = ERR_CONFIG_FILE_INVALID_LDIF_ENTRY.get( 395 le.getLineNumber(), f.getAbsolutePath(), le); 396 throw new InitializationException(message, le); 397 } 398 catch (Exception e) 399 { 400 logger.traceException(e); 401 402 close(reader); 403 404 LocalizableMessage message = ERR_CONFIG_FILE_READ_ERROR.get(f.getAbsolutePath(), e); 405 throw new InitializationException(message, e); 406 } 407 408 409 // If the entry is null, then we have reached the end of the configuration 410 // file. 411 if (entry == null) 412 { 413 close(reader); 414 break; 415 } 416 417 418 // Make sure that the DN of the entry read doesn't already exist. 419 DN entryDN = entry.getName(); 420 if (configEntries.containsKey(entryDN)) 421 { 422 close(reader); 423 424 throw new InitializationException(ERR_CONFIG_FILE_DUPLICATE_ENTRY.get( 425 entryDN, reader.getLastEntryLineNumber(), f.getAbsolutePath())); 426 } 427 428 429 // Make sure that the parent DN of the entry read does exist. 430 DN parentDN = entryDN.parent(); 431 if (parentDN == null) 432 { 433 close(reader); 434 435 throw new InitializationException(ERR_CONFIG_FILE_UNKNOWN_PARENT.get( 436 entryDN, reader.getLastEntryLineNumber(), f.getAbsolutePath())); 437 } 438 439 ConfigEntry parentEntry = configEntries.get(parentDN); 440 if (parentEntry == null) 441 { 442 close(reader); 443 444 throw new InitializationException(ERR_CONFIG_FILE_NO_PARENT.get( 445 entryDN, reader.getLastEntryLineNumber(), f.getAbsolutePath(), parentDN)); 446 } 447 448 449 // Create the new configuration entry, add it as a child of the provided 450 // parent entry, and put it into the entry has. 451 try 452 { 453 ConfigEntry configEntry = new ConfigEntry(entry, parentEntry); 454 parentEntry.addChild(configEntry); 455 configEntries.put(entryDN, configEntry); 456 } 457 catch (Exception e) 458 { 459 // This should not happen. 460 logger.traceException(e); 461 462 close(reader); 463 464 LocalizableMessage message = ERR_CONFIG_FILE_GENERIC_ERROR.get(f.getAbsolutePath(), e); 465 throw new InitializationException(message, e); 466 } 467 } 468 469 470 // Get the server root 471 File rootFile = envConfig.getServerRoot(); 472 if (rootFile == null) 473 { 474 throw new InitializationException(ERR_CONFIG_CANNOT_DETERMINE_SERVER_ROOT.get( 475 ENV_VAR_INSTALL_ROOT)); 476 } 477 serverRoot = rootFile.getAbsolutePath(); 478 479 // Get the server instance root 480 File instanceFile = envConfig.getInstanceRoot(); 481 instanceRoot = instanceFile.getAbsolutePath(); 482 483 // Register with the Directory Server as an alert generator. 484 DirectoryServer.registerAlertGenerator(this); 485 486 // Register with the Directory Server as the backend that should be used 487 // when accessing the configuration. 488 baseDNs = new DN[] { configRootEntry.getDN() }; 489 490 try 491 { 492 // Set a backend ID for the config backend. Try to avoid potential 493 // conflict with user backend identifiers. 494 setBackendID("__config.ldif__"); 495 496 DirectoryServer.registerBaseDN(configRootEntry.getDN(), this, true); 497 } 498 catch (Exception e) 499 { 500 logger.traceException(e); 501 502 LocalizableMessage message = ERR_CONFIG_CANNOT_REGISTER_AS_PRIVATE_SUFFIX.get( 503 configRootEntry.getDN(), getExceptionMessage(e)); 504 throw new InitializationException(message, e); 505 } 506 } 507 508 509 510 /** 511 * Calculates a SHA-1 digest of the current configuration file. 512 * 513 * @return The calculated configuration digest. 514 * 515 * @throws DirectoryException If a problem occurs while calculating the 516 * digest. 517 */ 518 private byte[] calculateConfigDigest() 519 throws DirectoryException 520 { 521 InputStream inputStream = null; 522 try 523 { 524 MessageDigest sha1Digest = 525 MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1); 526 inputStream = new FileInputStream(configFile); 527 byte[] buffer = new byte[8192]; 528 while (true) 529 { 530 int bytesRead = inputStream.read(buffer); 531 if (bytesRead < 0) 532 { 533 break; 534 } 535 536 sha1Digest.update(buffer, 0, bytesRead); 537 } 538 return sha1Digest.digest(); 539 } 540 catch (Exception e) 541 { 542 LocalizableMessage message = ERR_CONFIG_CANNOT_CALCULATE_DIGEST.get( 543 configFile, stackTraceToSingleLineString(e)); 544 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 545 message, e); 546 } 547 finally 548 { 549 StaticUtils.close(inputStream); 550 } 551 } 552 553 554 555 /** 556 * Looks at the existing archive directory, finds the latest archive file, 557 * and calculates a SHA-1 digest of that file. 558 * 559 * @return The calculated digest of the most recent archived configuration 560 * file. 561 * 562 * @throws DirectoryException If a problem occurs while calculating the 563 * digest. 564 */ 565 private byte[] getLastConfigDigest(File archiveDirectory) 566 throws DirectoryException 567 { 568 int latestCounter = 0; 569 long latestTimestamp = -1; 570 String latestFileName = null; 571 for (String name : archiveDirectory.list()) 572 { 573 if (! name.startsWith("config-")) 574 { 575 continue; 576 } 577 578 int dotPos = name.indexOf('.', 7); 579 if (dotPos < 0) 580 { 581 continue; 582 } 583 584 int dashPos = name.indexOf('-', 7); 585 if (dashPos < 0) 586 { 587 try 588 { 589 ByteString ts = ByteString.valueOf(name.substring(7, dotPos)); 590 long timestamp = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(ts); 591 if (timestamp > latestTimestamp) 592 { 593 latestFileName = name; 594 latestTimestamp = timestamp; 595 latestCounter = 0; 596 continue; 597 } 598 } 599 catch (Exception e) 600 { 601 continue; 602 } 603 } 604 else 605 { 606 try 607 { 608 ByteString ts = ByteString.valueOf(name.substring(7, dashPos)); 609 long timestamp = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(ts); 610 int counter = Integer.parseInt(name.substring(dashPos+1, dotPos)); 611 612 if (timestamp > latestTimestamp) 613 { 614 latestFileName = name; 615 latestTimestamp = timestamp; 616 latestCounter = counter; 617 continue; 618 } 619 else if (timestamp == latestTimestamp && counter > latestCounter) 620 { 621 latestFileName = name; 622 latestTimestamp = timestamp; 623 latestCounter = counter; 624 continue; 625 } 626 } 627 catch (Exception e) 628 { 629 continue; 630 } 631 } 632 } 633 634 if (latestFileName == null) 635 { 636 return null; 637 } 638 File latestFile = new File(archiveDirectory, latestFileName); 639 640 try 641 { 642 MessageDigest sha1Digest = 643 MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1); 644 GZIPInputStream inputStream = 645 new GZIPInputStream(new FileInputStream(latestFile)); 646 byte[] buffer = new byte[8192]; 647 while (true) 648 { 649 int bytesRead = inputStream.read(buffer); 650 if (bytesRead < 0) 651 { 652 break; 653 } 654 655 sha1Digest.update(buffer, 0, bytesRead); 656 } 657 658 return sha1Digest.digest(); 659 } 660 catch (Exception e) 661 { 662 LocalizableMessage message = ERR_CONFIG_CANNOT_CALCULATE_DIGEST.get( 663 latestFile.getAbsolutePath(), stackTraceToSingleLineString(e)); 664 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 665 message, e); 666 } 667 } 668 669 670 671 /** 672 * Applies the updates in the provided changes file to the content in the 673 * specified source file. The result will be written to a temporary file, the 674 * current source file will be moved out of place, and then the updated file 675 * will be moved into the place of the original file. The changes file will 676 * also be renamed so it won't be applied again. 677 * <BR><BR> 678 * If any problems are encountered, then the config initialization process 679 * will be aborted. 680 * 681 * @param sourceFile The LDIF file containing the source data. 682 * @param changesFile The LDIF file containing the changes to apply. 683 * 684 * @throws IOException If a problem occurs while performing disk I/O. 685 * 686 * @throws LDIFException If a problem occurs while trying to interpret the 687 * data. 688 */ 689 private void applyChangesFile(File sourceFile, File changesFile) 690 throws IOException, LDIFException 691 { 692 // Create the appropriate LDIF readers and writer. 693 LDIFImportConfig importConfig = 694 new LDIFImportConfig(sourceFile.getAbsolutePath()); 695 importConfig.setValidateSchema(false); 696 LDIFReader sourceReader = new LDIFReader(importConfig); 697 698 importConfig = new LDIFImportConfig(changesFile.getAbsolutePath()); 699 importConfig.setValidateSchema(false); 700 LDIFReader changesReader = new LDIFReader(importConfig); 701 702 String tempFile = changesFile.getAbsolutePath() + ".tmp"; 703 LDIFExportConfig exportConfig = 704 new LDIFExportConfig(tempFile, ExistingFileBehavior.OVERWRITE); 705 LDIFWriter targetWriter = new LDIFWriter(exportConfig); 706 707 708 // Apply the changes and make sure there were no errors. 709 List<LocalizableMessage> errorList = new LinkedList<>(); 710 boolean successful = LDIFModify.modifyLDIF(sourceReader, changesReader, 711 targetWriter, errorList); 712 713 StaticUtils.close(sourceReader, changesReader, targetWriter); 714 715 if (! successful) 716 { 717 // FIXME -- Log each error message and throw an exception. 718 for (LocalizableMessage s : errorList) 719 { 720 logger.error(ERR_CONFIG_ERROR_APPLYING_STARTUP_CHANGE, s); 721 } 722 723 LocalizableMessage message = ERR_CONFIG_UNABLE_TO_APPLY_CHANGES_FILE.get(); 724 throw new LDIFException(message); 725 } 726 727 728 // Move the current config file out of the way and replace it with the 729 // updated version. 730 File oldSource = new File(sourceFile.getAbsolutePath() + ".prechanges"); 731 if (oldSource.exists()) 732 { 733 oldSource.delete(); 734 } 735 sourceFile.renameTo(oldSource); 736 new File(tempFile).renameTo(sourceFile); 737 738 // Move the changes file out of the way so it doesn't get applied again. 739 File newChanges = new File(changesFile.getAbsolutePath() + ".applied"); 740 if (newChanges.exists()) 741 { 742 newChanges.delete(); 743 } 744 changesFile.renameTo(newChanges); 745 } 746 747 /** {@inheritDoc} */ 748 @Override 749 public void finalizeConfigHandler() 750 { 751 finalizeBackend(); 752 try 753 { 754 DirectoryServer.deregisterBaseDN(configRootEntry.getDN()); 755 } 756 catch (Exception e) 757 { 758 logger.traceException(e); 759 } 760 } 761 762 /** {@inheritDoc} */ 763 @Override 764 public ConfigEntry getConfigRootEntry() 765 throws ConfigException 766 { 767 return configRootEntry; 768 } 769 770 /** {@inheritDoc} */ 771 @Override 772 public ConfigEntry getConfigEntry(DN entryDN) 773 throws ConfigException 774 { 775 return configEntries.get(entryDN); 776 } 777 778 /** {@inheritDoc} */ 779 @Override 780 public String getServerRoot() 781 { 782 return serverRoot; 783 } 784 785 /** {@inheritDoc} */ 786 @Override 787 public String getInstanceRoot() 788 { 789 return instanceRoot; 790 } 791 792 /** {@inheritDoc} */ 793 @Override 794 public void configureBackend(ConfigFileHandlerBackendCfg cfg, ServerContext serverContext) 795 throws ConfigException 796 { 797 // No action is required. 798 } 799 800 /** {@inheritDoc} */ 801 @Override 802 public void openBackend() throws ConfigException, InitializationException 803 { 804 // No action is required, since all initialization was performed in the 805 // initializeConfigHandler method. 806 } 807 808 /** {@inheritDoc} */ 809 @Override 810 public DN[] getBaseDNs() 811 { 812 return baseDNs; 813 } 814 815 /** {@inheritDoc} */ 816 @Override 817 public long getEntryCount() 818 { 819 return configEntries.size(); 820 } 821 822 /** {@inheritDoc} */ 823 @Override 824 public boolean isIndexed(AttributeType attributeType, IndexType indexType) 825 { 826 // All searches in this backend will always be considered indexed. 827 return true; 828 } 829 830 /** {@inheritDoc} */ 831 @Override 832 public ConditionResult hasSubordinates(DN entryDN) 833 throws DirectoryException 834 { 835 ConfigEntry baseEntry = configEntries.get(entryDN); 836 if (baseEntry != null) 837 { 838 return ConditionResult.valueOf(baseEntry.hasChildren()); 839 } 840 return ConditionResult.UNDEFINED; 841 } 842 843 /** {@inheritDoc} */ 844 @Override 845 public long getNumberOfEntriesInBaseDN(DN baseDN) throws DirectoryException 846 { 847 checkNotNull(baseDN, "baseDN must not be null"); 848 final ConfigEntry baseEntry = configEntries.get(baseDN); 849 if (baseEntry == null) 850 { 851 return -1; 852 } 853 854 long count = 1; 855 for (ConfigEntry child : baseEntry.getChildren().values()) 856 { 857 count += getNumberOfEntriesInBaseDN(child.getDN()); 858 count++; 859 } 860 return count; 861 } 862 863 /** {@inheritDoc} */ 864 @Override 865 public long getNumberOfChildren(DN parentDN) throws DirectoryException 866 { 867 checkNotNull(parentDN, "parentDN must not be null"); 868 final ConfigEntry baseEntry = configEntries.get(parentDN); 869 return baseEntry != null ? baseEntry.getChildren().size() : -1; 870 } 871 872 /** {@inheritDoc} */ 873 @Override 874 public Entry getEntry(DN entryDN) 875 throws DirectoryException 876 { 877 ConfigEntry configEntry = configEntries.get(entryDN); 878 if (configEntry == null) 879 { 880 return null; 881 } 882 883 return configEntry.getEntry().duplicate(true); 884 } 885 886 /** {@inheritDoc} */ 887 @Override 888 public boolean entryExists(DN entryDN) 889 throws DirectoryException 890 { 891 return configEntries.containsKey(entryDN); 892 } 893 894 /** {@inheritDoc} */ 895 @Override 896 public void addEntry(Entry entry, AddOperation addOperation) 897 throws DirectoryException 898 { 899 Entry e = entry.duplicate(false); 900 901 // If there is an add operation, then make sure that the associated user has 902 // both the CONFIG_READ and CONFIG_WRITE privileges. 903 if (addOperation != null) 904 { 905 ClientConnection clientConnection = addOperation.getClientConnection(); 906 if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE, 907 addOperation)) 908 { 909 LocalizableMessage message = ERR_CONFIG_FILE_ADD_INSUFFICIENT_PRIVILEGES.get(); 910 throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 911 message); 912 } 913 } 914 915 916 // Grab the config lock to ensure that only one config update may be in 917 // progress at any given time. 918 synchronized (configLock) 919 { 920 // Make sure that the target DN does not already exist. If it does, then 921 // fail. 922 DN entryDN = e.getName(); 923 if (configEntries.containsKey(entryDN)) 924 { 925 LocalizableMessage message = ERR_CONFIG_FILE_ADD_ALREADY_EXISTS.get(entryDN); 926 throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, message); 927 } 928 929 930 // Make sure that the entry's parent exists. If it does not, then fail. 931 DN parentDN = entryDN.parent(); 932 if (parentDN == null) 933 { 934 // The entry DN doesn't have a parent. This is not allowed. 935 LocalizableMessage message = ERR_CONFIG_FILE_ADD_NO_PARENT_DN.get(entryDN); 936 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message); 937 } 938 939 ConfigEntry parentEntry = configEntries.get(parentDN); 940 if (parentEntry == null) 941 { 942 // The parent entry does not exist. This is not allowed. 943 DN matchedDN = getMatchedDN(parentDN); 944 LocalizableMessage message = ERR_CONFIG_FILE_ADD_NO_PARENT.get(entryDN, parentDN); 945 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null); 946 } 947 948 949 // Encapsulate the provided entry in a config entry. 950 ConfigEntry newEntry = new ConfigEntry(e, parentEntry); 951 952 953 // See if the parent entry has any add listeners. If so, then iterate 954 // through them and make sure the new entry is acceptable. 955 List<ConfigAddListener> addListeners = parentEntry.getAddListeners(); 956 LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder(); 957 for (ConfigAddListener l : addListeners) 958 { 959 if (! l.configAddIsAcceptable(newEntry, unacceptableReason)) 960 { 961 LocalizableMessage message = ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER. 962 get(entryDN, parentDN, unacceptableReason); 963 throw new DirectoryException( 964 ResultCode.UNWILLING_TO_PERFORM, message); 965 966 } 967 } 968 969 970 // At this point, we will assume that everything is OK and proceed with 971 // the add. 972 try 973 { 974 parentEntry.addChild(newEntry); 975 configEntries.put(entryDN, newEntry); 976 writeUpdatedConfig(); 977 } 978 catch (org.opends.server.config.ConfigException ce) 979 { 980 logger.traceException(ce); 981 982 LocalizableMessage message = ERR_CONFIG_FILE_ADD_FAILED.get(entryDN, parentDN, getExceptionMessage(ce)); 983 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); 984 } 985 986 987 // Notify all the add listeners that the entry has been added. 988 final ConfigChangeResult aggregatedResult = new ConfigChangeResult(); 989 for (ConfigAddListener l : addListeners) // This is an iterator over a COWArrayList 990 { 991 if (addListeners.contains(l)) 992 { // ignore listeners that deregistered themselves 993 final ConfigChangeResult result = l.applyConfigurationAdd(newEntry); 994 aggregate(aggregatedResult, result); 995 handleConfigChangeResult(result, newEntry.getDN(), l.getClass().getName(), "applyConfigurationAdd"); 996 } 997 } 998 999 throwIfUnsuccessful(aggregatedResult, ERR_CONFIG_FILE_ADD_APPLY_FAILED); 1000 } 1001 } 1002 1003 /** {@inheritDoc} */ 1004 @Override 1005 public void deleteEntry(DN entryDN, DeleteOperation deleteOperation) 1006 throws DirectoryException 1007 { 1008 // If there is a delete operation, then make sure that the associated user 1009 // has both the CONFIG_READ and CONFIG_WRITE privileges. 1010 if (deleteOperation != null) 1011 { 1012 ClientConnection clientConnection = deleteOperation.getClientConnection(); 1013 if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE, 1014 deleteOperation)) 1015 { 1016 LocalizableMessage message = ERR_CONFIG_FILE_DELETE_INSUFFICIENT_PRIVILEGES.get(); 1017 throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 1018 message); 1019 } 1020 } 1021 1022 1023 // Grab the config lock to ensure that only one config update may be in 1024 // progress at any given time. 1025 synchronized (configLock) 1026 { 1027 // Get the target entry. If it does not exist, then fail. 1028 ConfigEntry entry = configEntries.get(entryDN); 1029 if (entry == null) 1030 { 1031 DN matchedDN = getMatchedDNForDescendantOfConfig(entryDN); 1032 LocalizableMessage message = ERR_CONFIG_FILE_DELETE_NO_SUCH_ENTRY.get(entryDN); 1033 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null); 1034 } 1035 1036 1037 // If the entry has children, then fail. 1038 if (entry.hasChildren()) 1039 { 1040 LocalizableMessage message = ERR_CONFIG_FILE_DELETE_HAS_CHILDREN.get(entryDN); 1041 throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF, message); 1042 } 1043 1044 1045 // Get the parent entry. If there isn't one, then it must be the config 1046 // root, which we won't allow. 1047 ConfigEntry parentEntry = entry.getParent(); 1048 if (parentEntry == null) 1049 { 1050 LocalizableMessage message = ERR_CONFIG_FILE_DELETE_NO_PARENT.get(entryDN); 1051 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); 1052 } 1053 1054 1055 // Get the delete listeners from the parent and make sure that they are 1056 // all OK with the delete. 1057 List<ConfigDeleteListener> deleteListeners = 1058 parentEntry.getDeleteListeners(); 1059 LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder(); 1060 for (ConfigDeleteListener l : deleteListeners) 1061 { 1062 if (! l.configDeleteIsAcceptable(entry, unacceptableReason)) 1063 { 1064 LocalizableMessage message = ERR_CONFIG_FILE_DELETE_REJECTED. 1065 get(entryDN, parentEntry.getDN(), unacceptableReason); 1066 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 1067 message); 1068 } 1069 } 1070 1071 1072 // At this point, we will assume that everything is OK and proceed with 1073 // the delete. 1074 try 1075 { 1076 parentEntry.removeChild(entryDN); 1077 configEntries.remove(entryDN); 1078 writeUpdatedConfig(); 1079 } 1080 catch (org.opends.server.config.ConfigException ce) 1081 { 1082 logger.traceException(ce); 1083 1084 LocalizableMessage message = ERR_CONFIG_FILE_DELETE_FAILED. 1085 get(entryDN, parentEntry.getDN(), getExceptionMessage(ce)); 1086 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); 1087 } 1088 1089 1090 // Notify all the delete listeners that the entry has been removed. 1091 final ConfigChangeResult aggregatedResult = new ConfigChangeResult(); 1092 for (ConfigDeleteListener l : deleteListeners) // This is an iterator over a COWArrayList 1093 { 1094 if (deleteListeners.contains(l)) 1095 { // ignore listeners that deregistered themselves 1096 final ConfigChangeResult result = l.applyConfigurationDelete(entry); 1097 aggregate(aggregatedResult, result); 1098 handleConfigChangeResult(result, entry.getDN(), l.getClass().getName(), "applyConfigurationDelete"); 1099 } 1100 } 1101 1102 throwIfUnsuccessful(aggregatedResult, ERR_CONFIG_FILE_DELETE_APPLY_FAILED); 1103 } 1104 } 1105 1106 /** {@inheritDoc} */ 1107 @Override 1108 public void replaceEntry(Entry oldEntry, Entry newEntry, 1109 ModifyOperation modifyOperation) throws DirectoryException 1110 { 1111 Entry e = newEntry.duplicate(false); 1112 1113 // If there is a modify operation, then make sure that the associated user 1114 // has both the CONFIG_READ and CONFIG_WRITE privileges. Also, if the 1115 // operation targets the set of root privileges then make sure the user has 1116 // the PRIVILEGE_CHANGE privilege. 1117 if (modifyOperation != null) 1118 { 1119 ClientConnection clientConnection = modifyOperation.getClientConnection(); 1120 if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE, 1121 modifyOperation)) 1122 { 1123 LocalizableMessage message = ERR_CONFIG_FILE_MODIFY_INSUFFICIENT_PRIVILEGES.get(); 1124 throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 1125 message); 1126 } 1127 1128 AttributeType privType = 1129 DirectoryServer.getAttributeTypeOrDefault(ATTR_DEFAULT_ROOT_PRIVILEGE_NAME); 1130 for (Modification m : modifyOperation.getModifications()) 1131 { 1132 if (m.getAttribute().getAttributeType().equals(privType)) 1133 { 1134 if (! clientConnection.hasPrivilege(Privilege.PRIVILEGE_CHANGE, 1135 modifyOperation)) 1136 { 1137 LocalizableMessage message = 1138 ERR_CONFIG_FILE_MODIFY_PRIVS_INSUFFICIENT_PRIVILEGES.get(); 1139 throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message); 1140 } 1141 1142 break; 1143 } 1144 } 1145 } 1146 1147 1148 // Grab the config lock to ensure that only one config update may be in 1149 // progress at any given time. 1150 synchronized (configLock) 1151 { 1152 // Get the DN of the target entry for future reference. 1153 DN entryDN = e.getName(); 1154 1155 1156 // Get the target entry. If it does not exist, then fail. 1157 ConfigEntry currentEntry = configEntries.get(entryDN); 1158 if (currentEntry == null) 1159 { 1160 DN matchedDN = getMatchedDNForDescendantOfConfig(entryDN); 1161 LocalizableMessage message = ERR_CONFIG_FILE_MODIFY_NO_SUCH_ENTRY.get(entryDN); 1162 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null); 1163 } 1164 1165 1166 // If the structural class is different between the current entry and the 1167 // new entry, then reject the change. 1168 if (! currentEntry.getEntry().getStructuralObjectClass().equals( 1169 newEntry.getStructuralObjectClass())) 1170 { 1171 LocalizableMessage message = ERR_CONFIG_FILE_MODIFY_STRUCTURAL_CHANGE_NOT_ALLOWED.get(entryDN); 1172 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message); 1173 } 1174 1175 1176 // Create a new config entry to use for the validation testing. 1177 ConfigEntry newConfigEntry = new ConfigEntry(e, currentEntry.getParent()); 1178 1179 1180 // See if there are any config change listeners registered for this entry. 1181 // If there are, then make sure they are all OK with the change. 1182 List<ConfigChangeListener> changeListeners = 1183 currentEntry.getChangeListeners(); 1184 LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder(); 1185 for (ConfigChangeListener l : changeListeners) 1186 { 1187 if (! l.configChangeIsAcceptable(newConfigEntry, unacceptableReason)) 1188 { 1189 LocalizableMessage message = ERR_CONFIG_FILE_MODIFY_REJECTED_BY_CHANGE_LISTENER. 1190 get(entryDN, unacceptableReason); 1191 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); 1192 } 1193 } 1194 1195 1196 // At this point, it looks like the change is acceptable, so apply it. 1197 // We'll just overwrite the core entry in the current config entry so that 1198 // we keep all the registered listeners, references to the parent and 1199 // children, and other metadata. 1200 currentEntry.setEntry(e); 1201 writeUpdatedConfig(); 1202 1203 1204 // Notify all the change listeners of the update. 1205 final ConfigChangeResult aggregatedResult = new ConfigChangeResult(); 1206 for (ConfigChangeListener l : changeListeners) // This is an iterator over a COWArrayList 1207 { 1208 if (changeListeners.contains(l)) 1209 { // ignore listeners that deregistered themselves 1210 final ConfigChangeResult result = l.applyConfigurationChange(currentEntry); 1211 aggregate(aggregatedResult, result); 1212 handleConfigChangeResult(result, currentEntry.getDN(), l.getClass().getName(), "applyConfigurationChange"); 1213 } 1214 } 1215 1216 throwIfUnsuccessful(aggregatedResult, ERR_CONFIG_FILE_MODIFY_APPLY_FAILED); 1217 } 1218 } 1219 1220 private void aggregate(final ConfigChangeResult aggregatedResult, ConfigChangeResult newResult) 1221 { 1222 if (newResult.getResultCode() != ResultCode.SUCCESS) 1223 { 1224 if (aggregatedResult.getResultCode() == ResultCode.SUCCESS) 1225 { 1226 aggregatedResult.setResultCode(newResult.getResultCode()); 1227 } 1228 1229 aggregatedResult.getMessages().addAll(newResult.getMessages()); 1230 } 1231 } 1232 1233 private void throwIfUnsuccessful(final ConfigChangeResult aggregatedResult, Arg1<Object> errMsg) 1234 throws DirectoryException 1235 { 1236 if (aggregatedResult.getResultCode() != ResultCode.SUCCESS) 1237 { 1238 String reasons = Utils.joinAsString(". ", aggregatedResult.getMessages()); 1239 LocalizableMessage message = errMsg.get(reasons); 1240 throw new DirectoryException(aggregatedResult.getResultCode(), message); 1241 } 1242 } 1243 1244 /** {@inheritDoc} */ 1245 @Override 1246 public void renameEntry(DN currentDN, Entry entry, 1247 ModifyDNOperation modifyDNOperation) 1248 throws DirectoryException 1249 { 1250 // If there is a modify DN operation, then make sure that the associated 1251 // user has both the CONFIG_READ and CONFIG_WRITE privileges. 1252 if (modifyDNOperation != null) 1253 { 1254 ClientConnection clientConnection = 1255 modifyDNOperation.getClientConnection(); 1256 if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE, 1257 modifyDNOperation)) 1258 { 1259 LocalizableMessage message = ERR_CONFIG_FILE_MODDN_INSUFFICIENT_PRIVILEGES.get(); 1260 throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 1261 message); 1262 } 1263 } 1264 1265 1266 // Modify DN operations will not be allowed in the configuration, so this 1267 // will always throw an exception. 1268 LocalizableMessage message = ERR_CONFIG_FILE_MODDN_NOT_ALLOWED.get(); 1269 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); 1270 } 1271 1272 /** {@inheritDoc} */ 1273 @Override 1274 public void search(SearchOperation searchOperation) 1275 throws DirectoryException 1276 { 1277 // Make sure that the associated user has the CONFIG_READ privilege. 1278 ClientConnection clientConnection = searchOperation.getClientConnection(); 1279 if (! clientConnection.hasPrivilege(Privilege.CONFIG_READ, searchOperation)) 1280 { 1281 LocalizableMessage message = ERR_CONFIG_FILE_SEARCH_INSUFFICIENT_PRIVILEGES.get(); 1282 throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 1283 message); 1284 } 1285 1286 1287 // First, get the base DN for the search and make sure that it exists. 1288 DN baseDN = searchOperation.getBaseDN(); 1289 ConfigEntry baseEntry = configEntries.get(baseDN); 1290 if (baseEntry == null) 1291 { 1292 DN matchedDN = getMatchedDNForDescendantOfConfig(baseDN); 1293 LocalizableMessage message = ERR_CONFIG_FILE_SEARCH_NO_SUCH_BASE.get(baseDN); 1294 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null); 1295 } 1296 1297 1298 // Get the scope for the search and perform the remainder of the processing 1299 // accordingly. Also get the filter since we will need it in all cases. 1300 SearchScope scope = searchOperation.getScope(); 1301 SearchFilter filter = searchOperation.getFilter(); 1302 switch (scope.asEnum()) 1303 { 1304 case BASE_OBJECT: 1305 // We are only interested in the base entry itself. See if it matches 1306 // and if so then return the entry. 1307 Entry e = baseEntry.getEntry().duplicate(true); 1308 if (filter.matchesEntry(e)) 1309 { 1310 searchOperation.returnEntry(e, null); 1311 } 1312 break; 1313 1314 1315 case SINGLE_LEVEL: 1316 // We are only interested in entries immediately below the base entry. 1317 // Iterate through them and return the ones that match the filter. 1318 for (ConfigEntry child : baseEntry.getChildren().values()) 1319 { 1320 e = child.getEntry().duplicate(true); 1321 if (filter.matchesEntry(e) && !searchOperation.returnEntry(e, null)) 1322 { 1323 break; 1324 } 1325 } 1326 break; 1327 1328 1329 case WHOLE_SUBTREE: 1330 // We are interested in the base entry and all its children. Use a 1331 // recursive process to achieve this. 1332 searchSubtree(baseEntry, filter, searchOperation); 1333 break; 1334 1335 1336 case SUBORDINATES: 1337 // We are not interested in the base entry, but we want to check out all 1338 // of its children. Use a recursive process to achieve this. 1339 for (ConfigEntry child : baseEntry.getChildren().values()) 1340 { 1341 if (! searchSubtree(child, filter, searchOperation)) 1342 { 1343 break; 1344 } 1345 } 1346 break; 1347 1348 1349 default: 1350 // The user provided an invalid scope. 1351 LocalizableMessage message = ERR_CONFIG_FILE_SEARCH_INVALID_SCOPE.get(scope); 1352 throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message); 1353 } 1354 } 1355 1356 private DN getMatchedDNForDescendantOfConfig(DN dn) 1357 { 1358 if (dn.isDescendantOf(configRootEntry.getDN())) 1359 { 1360 return getMatchedDN(dn); 1361 } 1362 return null; 1363 } 1364 1365 private DN getMatchedDN(DN dn) 1366 { 1367 DN parentDN = dn.parent(); 1368 while (parentDN != null) 1369 { 1370 if (configEntries.containsKey(parentDN)) 1371 { 1372 return parentDN; 1373 } 1374 1375 parentDN = parentDN.parent(); 1376 } 1377 return null; 1378 } 1379 1380 /** 1381 * Performs a subtree search starting at the provided base entry, returning 1382 * all entries anywhere in that subtree that match the provided filter. 1383 * 1384 * @param baseEntry The base entry below which to perform the search. 1385 * @param filter The filter to use to identify matching entries. 1386 * @param searchOperation The search operation to use to return entries to 1387 * the client. 1388 * 1389 * @return <CODE>true</CODE> if the search should continue, or 1390 * <CODE>false</CODE> if it should stop for some reason (e.g., the 1391 * time limit or size limit has been reached). 1392 * 1393 * @throws DirectoryException If a problem occurs during processing. 1394 */ 1395 private boolean searchSubtree(ConfigEntry baseEntry, SearchFilter filter, 1396 SearchOperation searchOperation) 1397 throws DirectoryException 1398 { 1399 Entry e = baseEntry.getEntry().duplicate(true); 1400 if (filter.matchesEntry(e) && !searchOperation.returnEntry(e, null)) 1401 { 1402 return false; 1403 } 1404 1405 for (ConfigEntry child : baseEntry.getChildren().values()) 1406 { 1407 if (! searchSubtree(child, filter, searchOperation)) 1408 { 1409 return false; 1410 } 1411 } 1412 1413 return true; 1414 } 1415 1416 /** {@inheritDoc} */ 1417 @Override 1418 public void writeUpdatedConfig() 1419 throws DirectoryException 1420 { 1421 // FIXME -- This needs support for encryption. 1422 1423 1424 // Calculate an archive for the current server configuration file and see if 1425 // it matches what we expect. If not, then the file has been manually 1426 // edited with the server online which is a bad thing. In that case, we'll 1427 // copy the current config off to the side before writing the new config 1428 // so that the manual changes don't get lost but also don't get applied. 1429 // Also, send an admin alert notifying administrators about the problem. 1430 if (maintainConfigArchive) 1431 { 1432 try 1433 { 1434 byte[] currentDigest = calculateConfigDigest(); 1435 if (! Arrays.equals(configurationDigest, currentDigest)) 1436 { 1437 File existingCfg = new File(configFile); 1438 File newConfigFile = new File(existingCfg.getParent(), 1439 "config.manualedit-" + 1440 TimeThread.getGMTTime() + ".ldif"); 1441 int counter = 2; 1442 while (newConfigFile.exists()) 1443 { 1444 newConfigFile = new File(newConfigFile.getAbsolutePath() + "." + 1445 counter++); 1446 } 1447 1448 FileInputStream inputStream = new FileInputStream(existingCfg); 1449 FileOutputStream outputStream = new FileOutputStream(newConfigFile); 1450 byte[] buffer = new byte[8192]; 1451 while (true) 1452 { 1453 int bytesRead = inputStream.read(buffer); 1454 if (bytesRead < 0) 1455 { 1456 break; 1457 } 1458 1459 outputStream.write(buffer, 0, bytesRead); 1460 } 1461 1462 StaticUtils.close(inputStream, outputStream); 1463 1464 LocalizableMessage message = 1465 WARN_CONFIG_MANUAL_CHANGES_DETECTED.get(configFile, newConfigFile 1466 .getAbsolutePath()); 1467 logger.warn(message); 1468 1469 DirectoryServer.sendAlertNotification(this, 1470 ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED, message); 1471 } 1472 } 1473 catch (Exception e) 1474 { 1475 logger.traceException(e); 1476 1477 LocalizableMessage message = 1478 ERR_CONFIG_MANUAL_CHANGES_LOST.get(configFile, 1479 stackTraceToSingleLineString(e)); 1480 logger.error(message); 1481 1482 DirectoryServer.sendAlertNotification(this, 1483 ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED, message); 1484 } 1485 } 1486 1487 1488 // Write the new configuration to a temporary file. 1489 String tempConfig = configFile + ".tmp"; 1490 try 1491 { 1492 LDIFExportConfig exportConfig = 1493 new LDIFExportConfig(tempConfig, ExistingFileBehavior.OVERWRITE); 1494 1495 // FIXME -- Add all the appropriate configuration options. 1496 writeLDIF(exportConfig); 1497 } 1498 catch (Exception e) 1499 { 1500 logger.traceException(e); 1501 1502 LocalizableMessage message = 1503 ERR_CONFIG_FILE_WRITE_CANNOT_EXPORT_NEW_CONFIG.get(tempConfig, stackTraceToSingleLineString(e)); 1504 logger.error(message); 1505 1506 DirectoryServer.sendAlertNotification(this, 1507 ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message); 1508 return; 1509 } 1510 1511 1512 // Delete the previous version of the configuration and rename the new one. 1513 try 1514 { 1515 File actualConfig = new File(configFile); 1516 File tmpConfig = new File(tempConfig); 1517 renameFile(tmpConfig, actualConfig); 1518 } 1519 catch (Exception e) 1520 { 1521 logger.traceException(e); 1522 1523 LocalizableMessage message = 1524 ERR_CONFIG_FILE_WRITE_CANNOT_RENAME_NEW_CONFIG.get(tempConfig, configFile, stackTraceToSingleLineString(e)); 1525 logger.error(message); 1526 1527 DirectoryServer.sendAlertNotification(this, 1528 ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message); 1529 return; 1530 } 1531 1532 configurationDigest = calculateConfigDigest(); 1533 1534 1535 // Try to write the archive for the new configuration. 1536 if (maintainConfigArchive) 1537 { 1538 writeConfigArchive(); 1539 } 1540 } 1541 1542 1543 1544 /** 1545 * Writes the current configuration to the configuration archive. This will 1546 * be a best-effort attempt. 1547 */ 1548 private void writeConfigArchive() 1549 { 1550 if (! maintainConfigArchive) 1551 { 1552 return; 1553 } 1554 1555 // Determine the path to the directory that will hold the archived 1556 // configuration files. 1557 File configDirectory = new File(configFile).getParentFile(); 1558 File archiveDirectory = new File(configDirectory, CONFIG_ARCHIVE_DIR_NAME); 1559 1560 1561 // If the archive directory doesn't exist, then create it. 1562 if (! archiveDirectory.exists()) 1563 { 1564 try 1565 { 1566 if (! archiveDirectory.mkdirs()) 1567 { 1568 LocalizableMessage message = ERR_CONFIG_FILE_CANNOT_CREATE_ARCHIVE_DIR_NO_REASON.get( 1569 archiveDirectory.getAbsolutePath()); 1570 logger.error(message); 1571 1572 DirectoryServer.sendAlertNotification(this, 1573 ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message); 1574 return; 1575 } 1576 } 1577 catch (Exception e) 1578 { 1579 logger.traceException(e); 1580 1581 LocalizableMessage message = 1582 ERR_CONFIG_FILE_CANNOT_CREATE_ARCHIVE_DIR.get(archiveDirectory 1583 .getAbsolutePath(), stackTraceToSingleLineString(e)); 1584 logger.error(message); 1585 1586 DirectoryServer.sendAlertNotification(this, 1587 ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message); 1588 return; 1589 } 1590 } 1591 1592 1593 // Determine the appropriate name to use for the current configuration. 1594 File archiveFile; 1595 try 1596 { 1597 String timestamp = TimeThread.getGMTTime(); 1598 archiveFile = new File(archiveDirectory, "config-" + timestamp + ".gz"); 1599 if (archiveFile.exists()) 1600 { 1601 int counter = 2; 1602 archiveFile = new File(archiveDirectory, 1603 "config-" + timestamp + "-" + counter + ".gz"); 1604 1605 while (archiveFile.exists()) 1606 { 1607 counter++; 1608 archiveFile = new File(archiveDirectory, 1609 "config-" + timestamp + "-" + counter + ".gz"); 1610 } 1611 } 1612 } 1613 catch (Exception e) 1614 { 1615 logger.traceException(e); 1616 1617 LocalizableMessage message = 1618 ERR_CONFIG_FILE_CANNOT_WRITE_CONFIG_ARCHIVE 1619 .get(stackTraceToSingleLineString(e)); 1620 logger.error(message); 1621 1622 DirectoryServer.sendAlertNotification(this, 1623 ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message); 1624 return; 1625 } 1626 1627 1628 // Copy the current configuration to the new configuration file. 1629 byte[] buffer = new byte[8192]; 1630 FileInputStream inputStream = null; 1631 GZIPOutputStream outputStream = null; 1632 try 1633 { 1634 inputStream = new FileInputStream(configFile); 1635 outputStream = new GZIPOutputStream(new FileOutputStream(archiveFile)); 1636 1637 int bytesRead = inputStream.read(buffer); 1638 while (bytesRead > 0) 1639 { 1640 outputStream.write(buffer, 0, bytesRead); 1641 bytesRead = inputStream.read(buffer); 1642 } 1643 } 1644 catch (Exception e) 1645 { 1646 logger.traceException(e); 1647 1648 LocalizableMessage message = 1649 ERR_CONFIG_FILE_CANNOT_WRITE_CONFIG_ARCHIVE 1650 .get(stackTraceToSingleLineString(e)); 1651 logger.error(message); 1652 1653 DirectoryServer.sendAlertNotification(this, 1654 ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message); 1655 return; 1656 } 1657 finally 1658 { 1659 StaticUtils.close(inputStream, outputStream); 1660 } 1661 1662 1663 // If we should enforce a maximum number of archived configurations, then 1664 // see if there are any old ones that we need to delete. 1665 if (maxConfigArchiveSize > 0) 1666 { 1667 String[] archivedFileList = archiveDirectory.list(); 1668 int numToDelete = archivedFileList.length - maxConfigArchiveSize; 1669 if (numToDelete > 0) 1670 { 1671 Set<String> archiveSet = new TreeSet<>(); 1672 for (String name : archivedFileList) 1673 { 1674 if (! name.startsWith("config-")) 1675 { 1676 continue; 1677 } 1678 1679 // Simply ordering by filename should work, even when there are 1680 // timestamp conflicts, because the dash comes before the period in 1681 // the ASCII character set. 1682 archiveSet.add(name); 1683 } 1684 1685 Iterator<String> iterator = archiveSet.iterator(); 1686 for (int i=0; i < numToDelete && iterator.hasNext(); i++) 1687 { 1688 File f = new File(archiveDirectory, iterator.next()); 1689 try 1690 { 1691 f.delete(); 1692 } catch (Exception e) {} 1693 } 1694 } 1695 } 1696 } 1697 1698 /** {@inheritDoc} */ 1699 @Override 1700 public void writeSuccessfulStartupConfig() 1701 { 1702 if (useLastKnownGoodConfig) 1703 { 1704 // The server was started with the "last known good" configuration, so we 1705 // shouldn't overwrite it with something that is probably bad. 1706 return; 1707 } 1708 1709 1710 String startOKFilePath = configFile + ".startok"; 1711 String tempFilePath = startOKFilePath + ".tmp"; 1712 String oldFilePath = startOKFilePath + ".old"; 1713 1714 1715 // Copy the current config file to a temporary file. 1716 File tempFile = new File(tempFilePath); 1717 FileInputStream inputStream = null; 1718 try 1719 { 1720 inputStream = new FileInputStream(configFile); 1721 1722 FileOutputStream outputStream = null; 1723 try 1724 { 1725 outputStream = new FileOutputStream(tempFilePath, false); 1726 1727 try 1728 { 1729 byte[] buffer = new byte[8192]; 1730 while (true) 1731 { 1732 int bytesRead = inputStream.read(buffer); 1733 if (bytesRead < 0) 1734 { 1735 break; 1736 } 1737 1738 outputStream.write(buffer, 0, bytesRead); 1739 } 1740 } 1741 catch (Exception e) 1742 { 1743 logger.traceException(e); 1744 logger.error(ERR_STARTOK_CANNOT_WRITE, configFile, tempFilePath, getExceptionMessage(e)); 1745 return; 1746 } 1747 } 1748 catch (Exception e) 1749 { 1750 logger.traceException(e); 1751 logger.error(ERR_STARTOK_CANNOT_OPEN_FOR_WRITING, tempFilePath, getExceptionMessage(e)); 1752 return; 1753 } 1754 finally 1755 { 1756 close(outputStream); 1757 } 1758 } 1759 catch (Exception e) 1760 { 1761 logger.traceException(e); 1762 logger.error(ERR_STARTOK_CANNOT_OPEN_FOR_READING, configFile, getExceptionMessage(e)); 1763 return; 1764 } 1765 finally 1766 { 1767 close(inputStream); 1768 } 1769 1770 1771 // If a ".startok" file already exists, then move it to an ".old" file. 1772 File oldFile = new File(oldFilePath); 1773 try 1774 { 1775 if (oldFile.exists()) 1776 { 1777 oldFile.delete(); 1778 } 1779 } 1780 catch (Exception e) 1781 { 1782 logger.traceException(e); 1783 } 1784 1785 File startOKFile = new File(startOKFilePath); 1786 try 1787 { 1788 if (startOKFile.exists()) 1789 { 1790 startOKFile.renameTo(oldFile); 1791 } 1792 } 1793 catch (Exception e) 1794 { 1795 logger.traceException(e); 1796 } 1797 1798 1799 // Rename the temp file to the ".startok" file. 1800 try 1801 { 1802 tempFile.renameTo(startOKFile); 1803 } catch (Exception e) 1804 { 1805 logger.traceException(e); 1806 logger.error(ERR_STARTOK_CANNOT_RENAME, tempFilePath, startOKFilePath, getExceptionMessage(e)); 1807 return; 1808 } 1809 1810 1811 // Remove the ".old" file if there is one. 1812 try 1813 { 1814 if (oldFile.exists()) 1815 { 1816 oldFile.delete(); 1817 } 1818 } 1819 catch (Exception e) 1820 { 1821 logger.traceException(e); 1822 } 1823 } 1824 1825 /** {@inheritDoc} */ 1826 @Override 1827 public Set<String> getSupportedControls() 1828 { 1829 return Collections.emptySet(); 1830 } 1831 1832 /** {@inheritDoc} */ 1833 @Override 1834 public Set<String> getSupportedFeatures() 1835 { 1836 return Collections.emptySet(); 1837 } 1838 1839 /** {@inheritDoc} */ 1840 @Override 1841 public boolean supports(BackendOperation backendOperation) 1842 { 1843 switch (backendOperation) 1844 { 1845 case BACKUP: 1846 case RESTORE: 1847 return true; 1848 1849 default: 1850 return false; 1851 } 1852 } 1853 1854 /** {@inheritDoc} */ 1855 @Override 1856 public void exportLDIF(LDIFExportConfig exportConfig) 1857 throws DirectoryException 1858 { 1859 // TODO We would need export-ldif to initialize this backend. 1860 writeLDIF(exportConfig); 1861 } 1862 1863 /** 1864 * Writes the current configuration to LDIF with the provided export 1865 * configuration. 1866 * 1867 * @param exportConfig The configuration to use for the export. 1868 * 1869 * @throws DirectoryException If a problem occurs while writing the LDIF. 1870 */ 1871 private void writeLDIF(LDIFExportConfig exportConfig) 1872 throws DirectoryException 1873 { 1874 LDIFWriter writer; 1875 try 1876 { 1877 writer = new LDIFWriter(exportConfig); 1878 writer.writeComment(INFO_CONFIG_FILE_HEADER.get(), 80); 1879 writeEntryAndChildren(writer, configRootEntry); 1880 } 1881 catch (Exception e) 1882 { 1883 logger.traceException(e); 1884 1885 LocalizableMessage message = ERR_CONFIG_LDIF_WRITE_ERROR.get(e); 1886 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 1887 } 1888 1889 try 1890 { 1891 writer.close(); 1892 } 1893 catch (Exception e) 1894 { 1895 logger.traceException(e); 1896 1897 LocalizableMessage message = ERR_CONFIG_FILE_CLOSE_ERROR.get(e); 1898 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 1899 } 1900 } 1901 1902 1903 1904 /** 1905 * Writes the provided entry and any children that it may have to the provided 1906 * LDIF writer. 1907 * 1908 * @param writer The LDIF writer to use to write the entry and its 1909 * children. 1910 * @param configEntry The configuration entry to write, along with its 1911 * children. 1912 * 1913 * @throws DirectoryException If a problem occurs while attempting to write 1914 * the entry or one of its children. 1915 */ 1916 private void writeEntryAndChildren(LDIFWriter writer, ConfigEntry configEntry) 1917 throws DirectoryException 1918 { 1919 try 1920 { 1921 // Write the entry itself to LDIF. 1922 writer.writeEntry(configEntry.getEntry()); 1923 } 1924 catch (Exception e) 1925 { 1926 logger.traceException(e); 1927 1928 LocalizableMessage message = ERR_CONFIG_FILE_WRITE_ERROR.get( 1929 configEntry.getDN(), e); 1930 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1931 message, e); 1932 } 1933 1934 1935 // See if the entry has any children. If so, then iterate through them and 1936 // write them and their children. We'll copy the entries into a tree map 1937 // so that we have a sensible order in the resulting LDIF. 1938 TreeMap<DN,ConfigEntry> childMap = new TreeMap<>(configEntry.getChildren()); 1939 for (ConfigEntry childEntry : childMap.values()) 1940 { 1941 writeEntryAndChildren(writer, childEntry); 1942 } 1943 } 1944 1945 /** {@inheritDoc} */ 1946 @Override 1947 public LDIFImportResult importLDIF(LDIFImportConfig importConfig, ServerContext serverContext) 1948 throws DirectoryException 1949 { 1950 LocalizableMessage message = ERR_CONFIG_FILE_UNWILLING_TO_IMPORT.get(); 1951 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); 1952 } 1953 1954 /** {@inheritDoc} */ 1955 @Override 1956 public void createBackup(BackupConfig backupConfig) throws DirectoryException 1957 { 1958 new BackupManager(getBackendID()).createBackup(this, backupConfig); 1959 } 1960 1961 /** {@inheritDoc} */ 1962 @Override 1963 public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException 1964 { 1965 new BackupManager(getBackendID()).removeBackup(backupDirectory, backupID); 1966 } 1967 1968 /** {@inheritDoc} */ 1969 @Override 1970 public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException 1971 { 1972 new BackupManager(getBackendID()).restoreBackup(this, restoreConfig); 1973 } 1974 1975 /** {@inheritDoc} */ 1976 @Override 1977 public DN getComponentEntryDN() 1978 { 1979 return configRootEntry.getDN(); 1980 } 1981 1982 /** {@inheritDoc} */ 1983 @Override 1984 public String getClassName() 1985 { 1986 return CLASS_NAME; 1987 } 1988 1989 /** {@inheritDoc} */ 1990 @Override 1991 public Map<String,String> getAlerts() 1992 { 1993 Map<String,String> alerts = new LinkedHashMap<>(); 1994 1995 alerts.put(ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, 1996 ALERT_DESCRIPTION_CANNOT_WRITE_CONFIGURATION); 1997 alerts.put(ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED, 1998 ALERT_DESCRIPTION_MANUAL_CONFIG_EDIT_HANDLED); 1999 alerts.put(ALERT_TYPE_MANUAL_CONFIG_EDIT_LOST, 2000 ALERT_DESCRIPTION_MANUAL_CONFIG_EDIT_LOST); 2001 2002 return alerts; 2003 } 2004 2005 2006 2007 /** 2008 * Examines the provided result and logs a message if appropriate. If the 2009 * result code is anything other than {@code SUCCESS}, then it will log an 2010 * error message. If the operation was successful but admin action is 2011 * required, then it will log a warning message. If no action is required but 2012 * messages were generated, then it will log an informational message. 2013 * 2014 * @param result The config change result object that 2015 * @param entryDN The DN of the entry that was added, deleted, or 2016 * modified. 2017 * @param className The name of the class for the object that generated the 2018 * provided result. 2019 * @param methodName The name of the method that generated the provided 2020 * result. 2021 */ 2022 public void handleConfigChangeResult(ConfigChangeResult result, DN entryDN, 2023 String className, String methodName) 2024 { 2025 if (result == null) 2026 { 2027 logger.error(ERR_CONFIG_CHANGE_NO_RESULT, className, methodName, entryDN); 2028 return; 2029 } 2030 2031 ResultCode resultCode = result.getResultCode(); 2032 boolean adminActionRequired = result.adminActionRequired(); 2033 2034 String messageBuffer = Utils.joinAsString(" ", result.getMessages()); 2035 if (resultCode != ResultCode.SUCCESS) 2036 { 2037 logger.error(ERR_CONFIG_CHANGE_RESULT_ERROR, className, methodName, 2038 entryDN, resultCode, adminActionRequired, messageBuffer); 2039 } 2040 else if (adminActionRequired) 2041 { 2042 logger.warn(WARN_CONFIG_CHANGE_RESULT_ACTION_REQUIRED, className, methodName, entryDN, messageBuffer); 2043 } 2044 else if (messageBuffer.length() > 0) 2045 { 2046 logger.debug(INFO_CONFIG_CHANGE_RESULT_MESSAGES, className, methodName, entryDN, messageBuffer); 2047 } 2048 } 2049 2050 /** {@inheritDoc} */ 2051 @Override 2052 public File getDirectory() 2053 { 2054 return getConfigFileInBackendContext().getParentFile(); 2055 } 2056 2057 private File getConfigFileInBackendContext() 2058 { 2059 // This may seem a little weird, but in some context, we only have access to 2060 // this class as a backend and not as the config handler. We need it as a 2061 // config handler to determine the path to the config file, so we can get 2062 // that from the Directory Server object. 2063 return new File(((ConfigFileHandler) DirectoryServer.getConfigHandler()).configFile); 2064 } 2065 2066 /** {@inheritDoc} */ 2067 @Override 2068 public ListIterator<Path> getFilesToBackup() 2069 { 2070 final List<Path> files = new ArrayList<>(); 2071 2072 // the main config file 2073 File theConfigFile = getConfigFileInBackendContext(); 2074 files.add(theConfigFile.toPath()); 2075 2076 // the files in archive directory 2077 File archiveDirectory = new File(getDirectory(), CONFIG_ARCHIVE_DIR_NAME); 2078 if (archiveDirectory.exists()) 2079 { 2080 for (File archiveFile : archiveDirectory.listFiles()) 2081 { 2082 files.add(archiveFile.toPath()); 2083 } 2084 } 2085 2086 return files.listIterator(); 2087 } 2088 2089 /** {@inheritDoc} */ 2090 @Override 2091 public boolean isDirectRestore() 2092 { 2093 return true; 2094 } 2095 2096 /** {@inheritDoc} */ 2097 @Override 2098 public Path beforeRestore() throws DirectoryException 2099 { 2100 // save current config files to a save directory 2101 return BackupManager.saveCurrentFilesToDirectory(this, getBackendID()); 2102 } 2103 2104 /** {@inheritDoc} */ 2105 @Override 2106 public void afterRestore(Path restoreDirectory, Path saveDirectory) throws DirectoryException 2107 { 2108 // restore was successful, delete save directory 2109 StaticUtils.recursiveDelete(saveDirectory.toFile()); 2110 } 2111 2112}