/* * RHQ Management Platform * Copyright (C) 2005-2014 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.core.pc.drift; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static org.apache.commons.io.FileUtils.copyFile; import static org.apache.commons.io.FileUtils.deleteDirectory; import static org.apache.commons.io.FileUtils.touch; import static org.rhq.common.drift.FileEntry.addedFileEntry; import static org.rhq.common.drift.FileEntry.changedFileEntry; import static org.rhq.common.drift.FileEntry.removedFileEntry; import static org.rhq.core.domain.drift.DriftChangeSetCategory.COVERAGE; import static org.rhq.core.domain.drift.DriftChangeSetCategory.DRIFT; import static org.rhq.test.AssertUtils.assertCollectionMatchesNoOrder; import static org.rhq.test.AssertUtils.assertPropertiesMatch; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.rhq.common.drift.ChangeSetReader; import org.rhq.common.drift.ChangeSetReaderImpl; import org.rhq.common.drift.ChangeSetWriter; import org.rhq.common.drift.ChangeSetWriterImpl; import org.rhq.common.drift.FileEntry; import org.rhq.common.drift.Headers; import org.rhq.core.domain.drift.DriftDefinition; import org.rhq.core.system.OperatingSystemType; import org.rhq.core.system.SystemInfoFactory; public class DriftDetectorTest extends DriftTest { ScheduleQueue scheduleQueue; DriftClientTestStub driftClient; DriftDetector detector; boolean isWindows = (SystemInfoFactory.createSystemInfo().getOperatingSystemType() == OperatingSystemType.WINDOWS); @BeforeMethod public void initDetector() { driftClient = new DriftClientTestStub(); driftClient.setBaseDir(resourceDir); scheduleQueue = new ScheduleQueueImpl(); detector = new DriftDetector(scheduleQueue, changeSetMgr, driftClient); } @Test public void excludeEmptyDirsFromCoverageChangeSet() throws Exception { File confDir = mkdir(resourceDir, "conf"); File serverConf = createRandomFile(confDir, "server.conf"); // create an empty directory File libDir = mkdir(resourceDir, "lib"); assert (libDir.isDirectory()); DriftDefinition driftDef = driftDefinition("coverage-test", resourceDir.getAbsolutePath()); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), driftDef)); detector.run(); File changeSet = changeSet(driftDef.getName(), COVERAGE); Headers headers = createHeaders(driftDef, COVERAGE); List<FileEntry> expected = asList(addedFileEntry("conf/server.conf", sha256(serverConf), serverConf .lastModified(), serverConf.length())); assertHeaderEquals(changeSet, headers); assertFileEntriesMatch("Only files should be included in a change set.", expected, changeSet); } @Test public void includeMultipleFilesInDirInCoverageChangeSet() throws Exception { File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server-1.conf"); File server2Conf = createRandomFile(confDir, "server-2.conf"); DriftDefinition def = driftDefinition("multiple-files-test", resourceDir.getAbsolutePath()); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); File changeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> entries = asList(addedFileEntry("conf/server-1.conf", sha256(server1Conf), server1Conf .lastModified(), server1Conf.length()), addedFileEntry("conf/server-2.conf", sha256(server2Conf), server2Conf.lastModified(), server2Conf.length())); assertHeaderEquals(changeSet, createHeaders(def, COVERAGE)); assertFileEntriesMatch("Each file in a directory should be included in a coverage change set", entries, changeSet); } @Test public void includedSiblingDirsInCoverageChangeSet() throws Exception { File confDir = mkdir(resourceDir, "conf"); File serverConf = createRandomFile(confDir, "server.conf"); File libDir = mkdir(resourceDir, "lib"); File serverLib = createRandomFile(libDir, "server.jar"); DriftDefinition def = driftDefinition("sibling-dirs-test", resourceDir.getAbsolutePath()); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); File changeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> entries = asList(addedFileEntry("conf/server.conf", sha256(serverConf), serverConf .lastModified(), serverConf.length()), addedFileEntry("lib/server.jar", sha256(serverLib), serverLib .lastModified(), serverLib.length())); assertHeaderEquals(changeSet, createHeaders(def, COVERAGE)); assertFileEntriesMatch("A coverage change set should include files from multiple, sibling directories", entries, changeSet); } @Test public void includeNestedDirsInCoverageChangeSet() throws Exception { File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server-1.conf"); File subConfDir = mkdir(confDir, "subconf"); File server2Conf = createRandomFile(subConfDir, "server-2.conf"); DriftDefinition def = driftDefinition("nested-dirs-test", resourceDir.getAbsolutePath()); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); File changeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> entries = asList(addedFileEntry("conf/server-1.conf", sha256(server1Conf), server1Conf .lastModified(), server1Conf.length()), addedFileEntry("conf/subconf/server-2.conf", sha256(server2Conf), server2Conf.lastModified(), server2Conf.length())); assertHeaderEquals(changeSet, createHeaders(def, COVERAGE)); assertFileEntriesMatch("A coverage change set should include files in nested sub directories", entries, changeSet); } @Test public void updateScheduleAfterGeneratingCoverageChangeSet() throws Exception { DriftDefinition driftDef = driftDefinition("update-schedule-after-coverage-changeset", resourceDir .getAbsolutePath()); DriftDetectionSchedule schedule = new DriftDetectionSchedule(resourceId(), driftDef); File confDir = mkdir(resourceDir, "conf"); File serverConf = createRandomFile(confDir, "server.conf"); assert (serverConf.exists()); long currentTime = System.currentTimeMillis(); scheduleQueue.addSchedule(schedule); detector.run(); assertTrue(schedule.getNextScan() >= (currentTime + driftDef.getInterval()), "Failed to update schedule. next scan is " + schedule.getNextScan() + " and should be greater than " + (currentTime + driftDef.getInterval())); } @Test public void updateScheduleAfterGeneratingDriftChangeSet() throws Exception { } @Test public void doNotUpdateSnapshotOrGenerateDriftChangeSetIfNothingChanges() throws Exception { File confDir = mkdir(resourceDir, "conf"); File serverConf = createRandomFile(confDir, "server.conf"); assert (serverConf.exists()); DriftDefinition def = driftDefinition("nothing-to-update", resourceDir.getAbsolutePath()); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); File changeSet = changeSet(def.getName(), COVERAGE); String originalHash = sha256(changeSet); // Reset the schedule so that detection will run again the next time we call // detection.run() DriftDetectionSchedule schedule = scheduleQueue.remove(resourceId(), def); schedule.resetSchedule(); scheduleQueue.addSchedule(schedule); // Run the detector again. Note that nothing has changed so the snapshot should // remain the same and no drift change set file should be generated. detector.run(); String newHash = sha256(changeSet); assertEquals(newHash, originalHash, "The snapshot file should not have changed since there was no drift. "); File driftChangeSet = changeSet(def.getName(), DRIFT); assertFalse(driftChangeSet.exists(), "A drift change set file should not have been generated since there was " + "no drift"); } @Test public void skipDetectionForScheduledThatIsDisabled() throws Exception { DriftClientTestStub driftClient = new DriftClientTestStub() { { setBaseDir(resourceDir); } @Override public void sendChangeSetToServer(DriftDetectionSummary detectionSummary) { fail("Should not invoke drift client when drift definition is disabled"); } }; DriftDefinition def = driftDefinition("disabled-config-test", resourceDir.getAbsolutePath()); def.setEnabled(false); DriftDetectionSchedule schedule = new DriftDetectionSchedule(resourceId(), def); long nextScan = schedule.getNextScan(); File confDir = mkdir(resourceDir, "conf"); File server1Conf = new File(confDir, "server-1.conf"); touch(server1Conf); scheduleQueue.addSchedule(schedule); detector = new DriftDetector(scheduleQueue, changeSetMgr, driftClient); detector.run(); // make sure that the next scan time is not updated assertEquals(schedule.getNextScan(), nextScan, "The next scan time for the drift detection schedule should " + " not get updated if drift detection does not actually run for the definition."); } @Test public void doNotUpdateScheduleIfItIsTooEarlyToRunDetection() throws Exception { DriftDefinition driftDef = driftDefinition("schedule-not-ready-test", resourceDir.getAbsolutePath()); DriftDetectionSchedule schedule = new DriftDetectionSchedule(resourceId(), driftDef); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server.conf"); assert (server1Conf.exists()); scheduleQueue.addSchedule(schedule); detector.run(); long nextScan = schedule.getNextScan(); detector.run(); assertEquals(schedule.getNextScan(), nextScan, "The next scan time for the drift detection schedule should " + " not get updated if drift detection does not actually run for the definition."); } @Test public void reportMissingBaseDirWhenNoInitialSnapshotExists() throws Exception { final File basedir = new File(resourceDir, "conf"); DriftDefinition def = driftDefinition("basedir-does-not-exist", basedir.getAbsolutePath()); driftClient.setBaseDir(basedir); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); assertEquals(driftClient.getReportMissingBaseDirInvocationCount(), 1, "A missing base directory should be " + "reported to the server if no initial snapshot has already been generated."); assertEquals(driftClient.getSendChangeSetInvocationCount(), 0, "No initial change set should be sent to " + "the server if the base directory does not exist."); // verify that the initial change set was not generated File snapshot = changeSet(def.getName(), COVERAGE); assertFalse(snapshot.exists(), "An initial snapshot should not be written to disk if the base directory " + "does not exist."); } @Test public void skipDetectionWhenPreviousSnapshotFileExists() throws Exception { // The presence of a previous snapshot file means that the server has // not acknowledged that it has received and processed the change set. DriftDefinition def = driftDefinition("previous-snapshot-test", resourceDir.getAbsolutePath()); File confDir = mkdir(resourceDir, "conf"); createRandomFile(confDir, "server.conf"); DriftDetectionSchedule schedule = new DriftDetectionSchedule(resourceId(), def); scheduleQueue.addSchedule(schedule); detector.run(); // create some drift and generate a new snapshot createRandomFile(confDir, "server-1.conf"); schedule.resetSchedule(); detector.run(); File snapshot = changeSet(def.getName(), COVERAGE); String newHash = sha256(snapshot); File previousSnapshot = previousSnapshot(def.getName()); String oldHash = sha256(previousSnapshot); // create some drift and make sure drift detection does not run. createRandomFile(confDir, "server-2.conf"); schedule.resetSchedule(); // Tell driftClient to throw an exception if detector attempts to send // the change set report to the server. The detector should never call // driftClient in this scenario. driftClient.setFailingOnSendChangeSet(true); detector.run(); assertEquals(sha256(snapshot), newHash, "The snapshot should not have changed since the previous snapshot " + "is still on disk."); assertEquals(sha256(previousSnapshot), oldHash, "The previous snapshot should not have changed since " + "drift detection should not have run until the server acked the previous snapshot."); } @Test public void includeAddedFileInDriftChangeSet() throws Exception { DriftDefinition def = driftDefinition("file-added-drift-test", resourceDir.getAbsolutePath()); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server-1.conf"); ChangeSetWriter writer = changeSetMgr.getChangeSetWriter(resourceId(), createHeaders(def, COVERAGE)); writer.write(addedFileEntry("conf/server-1.conf", sha256(server1Conf), server1Conf.lastModified(), server1Conf .length())); writer.close(); // Create some drift File server2Conf = createRandomFile(confDir, "server-2.conf"); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); File driftChangeSet = changeSet(def.getName(), DRIFT); List<FileEntry> driftEntries = asList(addedFileEntry("conf/server-2.conf", sha256(server2Conf), server2Conf .lastModified(), server2Conf.length())); // verify that the drift change set was generated assertTrue(driftChangeSet.exists(), "Expected to find drift change set " + driftChangeSet.getPath()); assertHeaderEquals(driftChangeSet, createHeaders(def, DRIFT, 1)); assertFileEntriesMatch("The drift change set does not match the expected values", driftEntries, driftChangeSet); File coverageChangeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> coverageEntries = asList(addedFileEntry("conf/server-1.conf", sha256(server1Conf), server1Conf .lastModified(), server1Conf.length()), addedFileEntry("conf/server-2.conf", sha256(server2Conf), server2Conf.lastModified(), server2Conf.length())); // verify that the coverage change set was updated assertHeaderEquals(coverageChangeSet, createHeaders(def, COVERAGE, 1)); assertFileEntriesMatch("The coverage change set was not updated as expected", coverageEntries, coverageChangeSet); } @Test public void includeModifiedFileInDriftChangeSet() throws Exception { DriftDefinition def = driftDefinition("file-modified-drift-test", resourceDir.getAbsolutePath()); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server-1.conf"); String oldHash = sha256(server1Conf); ChangeSetWriter writer = changeSetMgr.getChangeSetWriter(resourceId(), createHeaders(def, COVERAGE)); writer.write(addedFileEntry("conf/server-1.conf", oldHash, server1Conf.lastModified(), server1Conf.length())); writer.close(); // create some drift server1Conf.delete(); server1Conf = createRandomFile(confDir, "server-1.conf", 48); String newHash = sha256(server1Conf); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); File driftChangeSet = changeSet(def.getName(), DRIFT); List<FileEntry> driftEntries = asList(changedFileEntry("conf/server-1.conf", oldHash, newHash, server1Conf .lastModified(), server1Conf.length())); // verify that the drift change set was generated assertTrue(driftChangeSet.exists(), "Expected to find drift change set " + driftChangeSet.getPath()); assertHeaderEquals(driftChangeSet, createHeaders(def, DRIFT, 1)); assertFileEntriesMatch("The drift change set does not match the expected values", driftEntries, driftChangeSet); File coverageChangeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> coverageEntries = asList(changedFileEntry("conf/server-1.conf", oldHash, newHash, server1Conf .lastModified(), server1Conf.length())); // verify that the coverage change set was updated assertHeaderEquals(coverageChangeSet, createHeaders(def, COVERAGE, 1)); assertFileEntriesMatch("The coverage change set was not updated as expected", coverageEntries, coverageChangeSet); } @Test(enabled = false) public void includeFiledAddedInNewDirectoryInDriftChangeSet() throws Exception { DriftDefinition def = driftDefinition("file-added-in-new-dir", resourceDir.getAbsolutePath()); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server-1.conf"); ChangeSetWriter writer = changeSetMgr.getChangeSetWriter(resourceId(), createHeaders(def, COVERAGE)); writer.write(addedFileEntry("conf/server-1.conf", sha256(server1Conf), server1Conf.lastModified(), server1Conf .length())); writer.close(); // create some drift File subconfDir = mkdir(confDir, "subconf"); File server2Conf = createRandomFile(subconfDir, "server-2.conf"); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); File driftChangeSet = changeSet(def.getName(), DRIFT); List<FileEntry> driftEntries = asList(addedFileEntry("conf/subconf/server-2.conf", sha256(server2Conf), server2Conf.lastModified(), server2Conf.length())); // verify that the drift change set was generated assertTrue(driftChangeSet.exists(), "Expected to find drift change set " + driftChangeSet.getPath()); assertHeaderEquals(driftChangeSet, createHeaders(def, DRIFT, 1)); assertFileEntriesMatch("The drift change set does not match the expected values", driftEntries, driftChangeSet); File coverageChangeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> coverageEntries = asList(addedFileEntry("conf/server-1.conf", sha256(server1Conf), server1Conf .lastModified(), server1Conf.length()), addedFileEntry("conf/subconf/server-2.conf", sha256(server2Conf), server2Conf.lastModified(), server2Conf.length())); // verify that the coverage change set was updated assertHeaderEquals(coverageChangeSet, createHeaders(def, COVERAGE, 1)); assertFileEntriesMatch("The coverage change set was not updated as expected", coverageEntries, coverageChangeSet); } @Test public void includeRemovedFileInDriftChangeSet() throws Exception { DriftDefinition def = driftDefinition("file-removed-drift-test", resourceDir.getAbsolutePath()); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server-1.conf"); File server2Conf = createRandomFile(confDir, "server-2.conf"); String server2ConfHash = sha256(server2Conf); ChangeSetWriter writer = changeSetMgr.getChangeSetWriter(resourceId(), createHeaders(def, COVERAGE)); writer.write(addedFileEntry("conf/server-1.conf", sha256(server1Conf), server1Conf.lastModified(), server1Conf .length())); writer.write(addedFileEntry("conf/server-2.conf", server2ConfHash, server2Conf.lastModified(), server2Conf .length())); writer.close(); // create some drift server2Conf.delete(); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); File driftChangeSet = changeSet(def.getName(), DRIFT); List<FileEntry> driftEntries = asList(removedFileEntry("conf/server-2.conf", server2ConfHash)); // verify that the drift change set was generated assertTrue(driftChangeSet.exists(), "Expected to find drift change set " + driftChangeSet.getPath()); assertHeaderEquals(driftChangeSet, createHeaders(def, DRIFT, 1)); assertFileEntriesMatch("The drift change set does not match the expected values", driftEntries, driftChangeSet); // verify that the coverage change set was updated File coverageChangeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> coverageEntries = asList(addedFileEntry("conf/server-1.conf", sha256(server1Conf), server1Conf .lastModified(), server1Conf.length())); assertHeaderEquals(coverageChangeSet, createHeaders(def, COVERAGE, 1)); assertFileEntriesMatch("The coverage change set was not updated as expected", coverageEntries, coverageChangeSet); } @Test public void reportDriftWhenBaseDirIsDeleted() throws Exception { File confDir = mkdir(resourceDir, "conf"); File serverConf = createRandomFile(confDir, "server.conf"); String serverConfHash = sha256(serverConf); DriftDefinition def = driftDefinition("delete-basedir-test", confDir.getAbsolutePath()); DriftDetectionSchedule schedule = new DriftDetectionSchedule(resourceId(), def); // generate the initial snapshot scheduleQueue.addSchedule(schedule); detector.run(); // Delete the base directory deleteDirectory(confDir); // re-run the detector schedule.resetSchedule(); detector.run(); // verify that the drift change set was generated File driftChangeSet = changeSet(def.getName(), DRIFT); List<FileEntry> driftEntries = asList(removedFileEntry("conf/server.conf", serverConfHash)); assertTrue(driftChangeSet.exists(), "Expected to find drift change set " + driftChangeSet.getPath()); assertHeaderEquals(driftChangeSet, createHeaders(def, DRIFT, 1)); assertFileEntriesMatch("The drift change set does not match the expected values", driftEntries, driftChangeSet); // verify that the snapshot was updated File currentSnapshot = changeSet(def.getName(), COVERAGE); List<FileEntry> snapshotEntries = emptyList(); assertHeaderEquals(currentSnapshot, createHeaders(def, COVERAGE, 1)); assertFileEntriesMatch("The current snapshot was not updated as expected", snapshotEntries, currentSnapshot); } @Test public void reportDriftWhenBaseDirIsAdded() throws Exception { } @Test public void includeFilesInRemovedDirectoryInDriftChangeSet() throws Exception { DriftDefinition def = driftDefinition("dir-removed-test", resourceDir.getAbsolutePath()); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server-1.conf"); String server1Hash = sha256(server1Conf); ChangeSetWriter writer = changeSetMgr.getChangeSetWriter(resourceId(), createHeaders(def, COVERAGE)); writer .write(addedFileEntry("conf/server-1.conf", server1Hash, server1Conf.lastModified(), server1Conf.length())); writer.close(); // create some drift server1Conf.delete(); confDir.delete(); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); File driftChangeSet = changeSet(def.getName(), DRIFT); List<FileEntry> driftEntries = asList(removedFileEntry("conf/server-1.conf", server1Hash)); // verify that the drift change set was generated assertTrue(driftChangeSet.exists(), "Expected to find drift change set " + driftChangeSet.getPath()); assertHeaderEquals(driftChangeSet, createHeaders(def, DRIFT, 1)); assertFileEntriesMatch("The drift change set does not match the expected values", driftEntries, driftChangeSet); // verify that the coverage change set was updated File coverageChangeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> coverageEntries = emptyList(); assertHeaderEquals(coverageChangeSet, createHeaders(def, COVERAGE, 1)); assertFileEntriesMatch("The coverage change set was not updated as expected", coverageEntries, coverageChangeSet); } @Test public void revertToPreviousSnapshotWhenSendingChangeSetFails() throws Exception { DriftDefinition def = driftDefinition("revert-snapshot-test", resourceDir.getAbsolutePath()); DriftDetectionSchedule schedule = new DriftDetectionSchedule(resourceId(), def); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server.conf"); assert (server1Conf.exists()); scheduleQueue.addSchedule(schedule); // generate the initial snapshot detector.run(); // Now generate a drift change set createRandomFile(confDir, "server-1.conf"); schedule.resetSchedule(); detector.run(); File changeSet = changeSet(def.getName(), COVERAGE); String currentHash = sha256(changeSet); // Need to delete the previous version snapshot file; otherwise, the // next detection run will be skipped. previousSnapshot(def.getName()).delete(); // generate some more drift, and fail on sending the change set // to the server createRandomFile(confDir, "server-2.conf"); schedule.resetSchedule(); driftClient.setFailingOnSendChangeSet(true); try { detector.run(); } catch (RuntimeException e) { } String newHash = sha256(changeSet); assertEquals(newHash, currentHash, "The snapshot file should be reverted if sending the new snapshot " + "to the server fails."); // The previous version file must be deleted on revert; otherwise, drift // detection will not run for the schedule if the previous version file // is found on disk. assertFalse(previousSnapshot(def.getName()).exists(), "The copy of the previous version snapshot file " + "should be deleted once we have reverted back to it and have a new, current snapsot file."); } @Test public void purgeSnapshotWhenSendingInitialChangeSetFails() throws Exception { // If we have just generated the initial change set and sending it to // the server fails, then there is no prior snapshot version to which // we can revert. We therefore need to purge the snapshot file and // allow DriftDetector to simply regenerate the initial change set again. DriftDefinition def = driftDefinition("purge-snapshot-test", resourceDir.getAbsolutePath()); File confDir = mkdir(resourceDir, "conf"); createRandomFile(confDir, "server.conf"); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); driftClient.setFailingOnSendChangeSet(true); try { detector.run(); } catch (RuntimeException e) { } assertFalse(changeSet(def.getName(), COVERAGE).exists(), "Snapshot file should be deleted when " + "only the initial change set has been generated and sending change send report to server fails"); } @Test public void ignoreFilesThatAreNotReadableForCoverageChangeSet() throws Exception { DriftDefinition def = driftDefinition("nonreadable-files-coverage", resourcesDir.getAbsolutePath()); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server-1.conf"); File server2Conf = createRandomFile(confDir, "server-2.conf"); setNotReadable(server2Conf); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); File changeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> entries = asList(addedFileEntry("conf/server-1.conf", sha256(server1Conf), server1Conf .lastModified(), server1Conf.length())); assertHeaderEquals(changeSet, createHeaders(def, COVERAGE)); assertFileEntriesMatch("Files that are non-readable should be skipped but other, readable file should still " + "be included in the change set", entries, changeSet); } @Test public void ignoreNewFilesThatAreNotReadableForDriftChangeSet() throws Exception { DriftDefinition def = driftDefinition("nonreadable-files-drfit", resourceDir.getAbsolutePath()); DriftDetectionSchedule schedule = new DriftDetectionSchedule(resourceId(), def); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server-1.conf"); String oldServer1Hash = sha256(server1Conf); scheduleQueue.addSchedule(schedule); detector.run(); // create some drift that includes a new file that is not readable server1Conf.delete(); server1Conf = createRandomFile(confDir, "server-1.conf", 48); String newServer1Hash = sha256(server1Conf); File server2Conf = createRandomFile(confDir, "server-2.conf"); setNotReadable(server2Conf); schedule.resetSchedule(); detector.run(); File driftChangeSet = changeSet(def.getName(), DRIFT); List<FileEntry> driftEntries = asList(changedFileEntry("conf/server-1.conf", oldServer1Hash, newServer1Hash, server1Conf.lastModified(), server1Conf.length())); // verify that the drift change set was generated assertTrue(driftChangeSet.exists(), "Expected to find drift change set " + driftChangeSet.getPath()); assertHeaderEquals(driftChangeSet, createHeaders(def, DRIFT, 1)); assertFileEntriesMatch("The drift change set does not match the expected values", driftEntries, driftChangeSet); // verify that the coverage change set was updated File coverageChangeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> coverageEntries = asList(changedFileEntry("conf/server-1.conf", oldServer1Hash, newServer1Hash, server1Conf.lastModified(), server1Conf.length())); assertHeaderEquals(coverageChangeSet, createHeaders(def, COVERAGE, 1)); assertFileEntriesMatch("The coverage change set was not updated as expected", coverageEntries, coverageChangeSet); } @Test public void markFileUnderDriftDetectionAsRemovedWhenItIsMadeNonReadable() throws Exception { DriftDefinition def = driftDefinition("file-made-nonreadable", resourceDir.getAbsolutePath()); DriftDetectionSchedule schedule = new DriftDetectionSchedule(resourceId(), def); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server-1.conf"); String server1Hash = sha256(server1Conf); scheduleQueue.addSchedule(schedule); detector.run(); // make the file non-readable and run the detector again setNotReadable(server1Conf); schedule.resetSchedule(); detector.run(); File driftChangeSet = changeSet(def.getName(), DRIFT); List<FileEntry> driftEntries = asList(removedFileEntry("conf/server-1.conf", server1Hash)); // verify that the drift change set was generated assertTrue(driftChangeSet.exists(), "Expected to find drift change set " + driftChangeSet.getPath()); assertHeaderEquals(driftChangeSet, createHeaders(def, DRIFT, 1)); assertFileEntriesMatch("The drift change set does not match the expected values", driftEntries, driftChangeSet); // verify that the coverage change set was updated File coverageChangeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> coverageEntries = emptyList(); assertHeaderEquals(coverageChangeSet, createHeaders(def, COVERAGE, 1)); assertFileEntriesMatch("The coverage change set was not updated as expected", coverageEntries, coverageChangeSet); } @Test public void doNotModifyPinnedSnapshotWhenDriftIsDetected() throws Exception { DriftDefinition driftDef = driftDefinition("do-not-modify-pinned-snapshot", resourceDir.getAbsolutePath()); driftDef.setPinned(true); DriftDetectionSchedule schedule = new DriftDetectionSchedule(resourceId(), driftDef); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server1.conf"); scheduleQueue.addSchedule(schedule); detector.run(); // When the initial snapshot is pinned, we need to generate it; otherwise, it should // be provided by the server. File currentSnapshot = changeSet(driftDef.getName(), COVERAGE); File pinnedSnapshot = new File(currentSnapshot.getParentFile(), "snapshot.pinned"); String originalPinnedHash = sha256(pinnedSnapshot); // generate some drift File server2Conf = createRandomFile(confDir, "server2.conf"); schedule.resetSchedule(); detector.run(); String newPinnedHash = sha256(pinnedSnapshot); assertEquals(newPinnedHash, originalPinnedHash, "When a snapshot is pinned, it should not get updated during " + "drift detection"); // We always generate/update the current snapshot so we still need to verify that it // was generated/updated correctly List<FileEntry> fileEntries = asList(addedFileEntry("conf/server1.conf", sha256(server1Conf), server1Conf .lastModified(), server1Conf.length()), addedFileEntry("conf/server2.conf", sha256(server2Conf), server2Conf.lastModified(), server2Conf.length())); assertHeaderEquals(currentSnapshot, createHeaders(driftDef, COVERAGE, 1)); assertFileEntriesMatch("The current snapshot file should still get updated even when using a pinned snapshot", fileEntries, currentSnapshot); } @Test public void updateCurrentSnapshotVersionNumberWhenUsingPinnedSnapshot() throws Exception { DriftDefinition driftDef = driftDefinition("update-snapshot-version-pinned", resourceDir.getAbsolutePath()); driftDef.setPinned(true); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server1.conf"); DriftDetectionSchedule schedule = new DriftDetectionSchedule(resourceId(), driftDef); scheduleQueue.addSchedule(schedule); detector.run(); // create some drift which should result in version 1 File server2Conf = createRandomFile(confDir, "server2.conf"); schedule.resetSchedule(); detector.run(); File currentSnapshot = changeSet(driftDef.getName(), COVERAGE); File previousSnapshot = new File(currentSnapshot.getParentFile(), "changeset.txt.previous"); previousSnapshot.delete(); // create some more drift which should result in version 2 File server3Conf = createRandomFile(confDir, "server3.conf"); schedule.resetSchedule(); detector.run(); // verify that the current snapshot was updated List<FileEntry> currentSnapshotEntries = asList(addedFileEntry("conf/server1.conf", sha256(server1Conf), server1Conf.lastModified(), server1Conf.length()), addedFileEntry("conf/server2.conf", sha256(server2Conf), server2Conf.lastModified(), server2Conf.length()), addedFileEntry("conf/server3.conf", sha256(server3Conf), server3Conf.lastModified(), server3Conf.length())); assertHeaderEquals(currentSnapshot, createHeaders(driftDef, COVERAGE, 2)); assertFileEntriesMatch("The current snapshot file should still get updated even when using a pinned snapshot", currentSnapshotEntries, currentSnapshot); // verify that the the drift/delta change set was generated File driftChangeSet = changeSet(driftDef.getName(), DRIFT); List<FileEntry> driftEntries = asList(addedFileEntry("conf/server2.conf", sha256(server2Conf), server2Conf .lastModified(), server2Conf.length()), addedFileEntry("conf/server3.conf", sha256(server3Conf), server3Conf.lastModified(), server3Conf.length())); assertHeaderEquals(driftChangeSet, createHeaders(driftDef, DRIFT, 2)); assertFileEntriesMatch("The drift change set was not generated correctly when using a pinned snapshot", driftEntries, driftChangeSet); } @Test public void generatePinnedSnapshotFileWhenInitialVersionIsPinned() throws Exception { DriftDefinition driftDef = driftDefinition("initial-snapshot-pinned-test", resourceDir.getAbsolutePath()); driftDef.setPinned(true); File confDir = mkdir(resourceDir, "conf"); File serverConf = createRandomFile(confDir, "server.conf"); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), driftDef)); detector.run(); File changeSet = changeSet(driftDef.getName(), COVERAGE); File pinnedSnapshot = new File(changeSet.getParentFile(), "snapshot.pinned"); List<FileEntry> entries = asList(addedFileEntry("conf/server.conf", sha256(serverConf), serverConf .lastModified(), serverConf.length())); assertTrue(changeSet.exists(), "An initial snapshot file should be generated even when it is pinned"); assertHeaderEquals(changeSet, createHeaders(driftDef, COVERAGE)); assertFileEntriesMatch("Initial snapshot entries are wrong for pinned snapshot", entries, changeSet); assertTrue(pinnedSnapshot.exists(), "Pinned snapshot file should be generated when initial version is pinned"); assertEquals(sha256(changeSet), sha256(pinnedSnapshot), "The contents of the pinned snapshot file and the " + "initial snapshot should be identical"); } @Test public void notifyServerOfRepeatChangeSet() throws Exception { final DriftDefinition driftDef = driftDefinition("repeat-changeset", resourceDir.getAbsolutePath()); driftDef.setId(1); driftDef.setPinned(true); File confDir = mkdir(resourceDir, "conf"); createRandomFile(confDir, "server1.conf"); DriftDetectionSchedule schedule = new DriftDetectionSchedule(resourceId(), driftDef); scheduleQueue.addSchedule(schedule); detector.run(); // generate some drift createRandomFile(confDir, "server2.conf"); schedule.resetSchedule(); detector.run(); File currentSnapshot = changeSet(driftDef.getName(), COVERAGE); String currentSnapshotHash = sha256(currentSnapshot); File driftChangeSet = changeSet(driftDef.getName(), DRIFT); String driftChangeSetHash = sha256(driftChangeSet); // We have to delete the previous version snapshot so that the detector will // run again. File previousSnapshot = previousSnapshot(driftDef.getName()); previousSnapshot.delete(); // Now do another drift detection run. This should re-detect the same drift that // was previously detected. It should not however, produce a new current snapshot // since there are no changes on the file system. final AtomicBoolean repeatChangeSetCalled = new AtomicBoolean(false); DriftClientTestStub driftClient = new DriftClientTestStub() { { setBaseDir(resourceDir); } @Override public void sendChangeSetToServer(DriftDetectionSummary detectionSummary) { fail("Do not send repeat change set to server."); } @Override public void repeatChangeSet(int resourceId, String driftDefName, int version) { repeatChangeSetCalled.set(true); assertEquals(resourceId, resourceId(), "The resource id for the repeat change set is wrong"); assertEquals(driftDefName, driftDef.getName(), "The drift definition name for the repeat change set " + "is wrong"); assertEquals(version, 1, "The snapshot version should not have changed since no new drift was detected"); } }; schedule.resetSchedule(); detector = new DriftDetector(scheduleQueue, changeSetMgr, driftClient); detector.run(); // verify that the current snapshot file has not changed assertEquals(sha256(currentSnapshot), currentSnapshotHash, "The current snapshot should not have been updated"); // verify that the drift change set has not changed assertEquals(sha256(driftChangeSet), driftChangeSetHash, "The drift change set file should not have changed"); // verify that notified the server assertTrue(repeatChangeSetCalled.get(), "Failed to notify server of repeat change set"); // verify that the previous version snapshot file has been deleted assertFalse(previousSnapshot.exists(), "There should be no previous version snapshot file because the " + "server has already acknowledged the current snapshot."); } @Test public void detectWhenResourceComesBackIntoCompliance() throws Exception { DriftDefinition driftDef = driftDefinition("back-into-compliance", resourceDir.getAbsolutePath()); driftDef.setPinned(true); File confDir = mkdir(resourceDir, "conf"); File serverConf = createRandomFile(confDir, "server.conf"); String serverConfHash = sha256(serverConf); Headers headers = createHeaders(driftDef, COVERAGE); // generate the pinned snapshot which is version zero File pinnedSnapshot = pinnedSnapshot(driftDef.getName()); ChangeSetWriter writer = new ChangeSetWriterImpl(pinnedSnapshot, headers); writer .write(addedFileEntry("conf/server.conf", serverConfHash, serverConf.lastModified(), serverConf.length())); writer.close(); // generate the current snapshot file. we will take a shortcut here by // just copying the pinned snapshot. We can do this since the current // snapshot will be identical to the pinned snapshot after the current // snapshot is first generated. File currentSnapshot = changeSet(driftDef.getName(), COVERAGE); copyFile(pinnedSnapshot, currentSnapshot); // now generate some drift causing the resource to go out of compliance File newServerConf = createRandomFile(confDir, "new_server.conf"); String newServerConfHash = sha256(newServerConf); assert (null != newServerConfHash); // do a drift detection run DriftDetectionSchedule schedule = new DriftDetectionSchedule(resourceId(), driftDef); scheduleQueue.addSchedule(schedule); // this run should produce version one detector.run(); // now put the resource back into compliance newServerConf.delete(); // do another drift detection run but first we have to delete the // previous snapshot file otherwise the detection scan will not run. previousSnapshot(driftDef.getName()).delete(); schedule.resetSchedule(); // this run should produce version two detector.run(); // verify that that current snapshot has been updated to reflect that // the resource is back in compliance. List<FileEntry> entries = asList(addedFileEntry("conf/server.conf", serverConfHash, serverConf.lastModified(), serverConf.length())); assertHeaderEquals(currentSnapshot, createHeaders(driftDef, COVERAGE, 2)); assertFileEntriesMatch("The entries in the current snapshot should match those in the pinned snapshot " + "once the resource has gone back into compliance.", entries, currentSnapshot); } @Test public void updateTimestampInfoNoDriftTest() throws Exception { DriftDefinition def = driftDefinition("update-timestamp-nodrift-test", resourceDir.getAbsolutePath()); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server-1.conf"); String server1Hash = sha256(server1Conf); ChangeSetWriter writer = changeSetMgr.getChangeSetWriter(resourceId(), createHeaders(def, COVERAGE)); writer.write(addedFileEntry("conf/server-1.conf", server1Hash, -1L, -1L)); writer.close(); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); // verify that no drift change set was generated File driftChangeSet = changeSet(def.getName(), DRIFT); assertFalse(driftChangeSet.exists(), "Expected no drift change set " + driftChangeSet.getPath()); // verify that the coverage change set was updated with timestamp info, version is still 0 File coverageChangeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> coverageEntries = asList(addedFileEntry("conf/server-1.conf", server1Hash, server1Conf .lastModified(), server1Conf.length())); assertHeaderEquals(coverageChangeSet, createHeaders(def, COVERAGE, 0)); assertFileEntriesMatch("The coverage change set was not updated as expected", coverageEntries, coverageChangeSet); } @Test public void updateTimestampInfoDriftTest() throws Exception { DriftDefinition def = driftDefinition("update-timestamp-drift-test", resourceDir.getAbsolutePath()); File confDir = mkdir(resourceDir, "conf"); File server1Conf = createRandomFile(confDir, "server-1.conf"); String server1Hash = sha256(server1Conf); File server2Conf = createRandomFile(confDir, "server-2.conf"); String server2Hash = sha256(server2Conf); ChangeSetWriter writer = changeSetMgr.getChangeSetWriter(resourceId(), createHeaders(def, COVERAGE)); writer.write(addedFileEntry("conf/server-1.conf", server1Hash, -1L, -1L)); writer.write(addedFileEntry("conf/server-2.conf", server2Hash, -1L, -1L)); writer.close(); // create some drift server1Conf.delete(); confDir.delete(); scheduleQueue.addSchedule(new DriftDetectionSchedule(resourceId(), def)); detector.run(); File driftChangeSet = changeSet(def.getName(), DRIFT); List<FileEntry> driftEntries = asList(removedFileEntry("conf/server-1.conf", server1Hash)); // verify that the drift change set was generated assertTrue(driftChangeSet.exists(), "Expected to find drift change set " + driftChangeSet.getPath()); assertHeaderEquals(driftChangeSet, createHeaders(def, DRIFT, 1)); assertFileEntriesMatch("The drift change set does not match the expected values", driftEntries, driftChangeSet); // verify that the coverage change set was updated with timestamp info and incremented version File coverageChangeSet = changeSet(def.getName(), COVERAGE); List<FileEntry> coverageEntries = asList(addedFileEntry("conf/server-2.conf", server2Hash, server2Conf .lastModified(), server2Conf.length())); assertHeaderEquals(coverageChangeSet, createHeaders(def, COVERAGE, 1)); assertFileEntriesMatch("The coverage change set was not updated as expected", coverageEntries, coverageChangeSet); } private void assertHeaderEquals(File changeSet, Headers expected) throws Exception { ChangeSetReader reader = new ChangeSetReaderImpl(new BufferedReader(new FileReader(changeSet))); Headers actual = reader.getHeaders(); assertPropertiesMatch(expected, actual, "Headers for " + changeSet.getPath() + " do not match " + "expected values"); } private void assertFileEntriesMatch(String msg, List<FileEntry> expected, File changeSet) throws Exception { List<FileEntry> actual = new ArrayList<FileEntry>(); ChangeSetReader reader = new ChangeSetReaderImpl(changeSet); for (FileEntry entry : reader) { actual.add(entry); } assertCollectionMatchesNoOrder(msg, expected, actual); } /** * Attempts to make a file non-readable. Windows does not support file permissions like * unix and linux platforms do. * * @param file The file to update */ private void setNotReadable(File file) { boolean setToReadable = file.setReadable(false); // not every win os (maybe none) supports this call, perform the test anyway, as best as possible if (!setToReadable) { if (isWindows) { file.delete(); } else { assertTrue(setToReadable, "Failed to make " + file.getPath() + " write only"); } } } }