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