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