/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2015 ForgeRock AS */ package org.opends.server.util; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import static org.opends.server.util.StaticUtils.*; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import org.opends.server.DirectoryServerTestCase; import org.opends.server.TestCaseUtils; import org.opends.server.api.Backupable; import org.opends.server.types.BackupConfig; import org.opends.server.types.BackupDirectory; import org.opends.server.types.DN; import org.opends.server.types.RestoreConfig; import org.opends.server.util.StaticUtils; import org.testng.Reporter; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @SuppressWarnings("javadoc") @Test(groups = { "precommit" }, sequential = true) public class BackupManagerTestCase extends DirectoryServerTestCase { private static final String ENTRY_DN = "dc=example,dc=com"; private static final String FILE_NAME_PREFIX = "file_"; private static final String BACKEND_ID = "backendID"; private static final String BACKUP_ID = "backupID"; @BeforeClass public void setUp() throws Exception { // Need the schema to be available, so make sure the server is started. // startFakeServer() is insufficient because we also need the CryptoManager to be initialized TestCaseUtils.startServer(); } @DataProvider Object[][] backupData() throws Exception { // For each case is provided // - a label identifying the case (not used in method but allow to identify easily the case in IDE) // - a mock of a backupable (building the mock also involves creating directory and files to backup) // - a backup config // - a restore config String label0 = "nohash"; Backupable backupable0 = buildBackupable(createSourceDirectory(label0), 3); BackupDirectory backupDir0 = buildBackupDir(label0); BackupConfig backupConfig0 = new BackupConfig(backupDir0, BACKUP_ID, false); RestoreConfig restoreConfig0 = new RestoreConfig(backupDir0, BACKUP_ID, false); String label1 = "unsignedhash"; Backupable backupable1 = buildBackupable(createSourceDirectory(label1), 3); BackupDirectory backupDir1 = buildBackupDir(label1); BackupConfig backupConfig1 = new BackupConfig(backupDir1, BACKUP_ID, false); backupConfig1.setHashData(true); RestoreConfig restoreConfig1 = new RestoreConfig(backupDir1, BACKUP_ID, false); String label2 = "signedhash"; Backupable backupable2 = buildBackupable(createSourceDirectory(label2), 3); BackupDirectory backupDir2 = buildBackupDir(label2); BackupConfig backupConfig2 = new BackupConfig(backupDir2, BACKUP_ID, false); backupConfig2.setHashData(true); backupConfig2.setSignHash(true); RestoreConfig restoreConfig2 = new RestoreConfig(backupDir2, BACKUP_ID, false); String label3 = "encrypted_compressed"; Backupable backupable3 = buildBackupable(createSourceDirectory(label3), 3); BackupDirectory backupDir3 = buildBackupDir(label3); BackupConfig backupConfig3 = new BackupConfig(backupDir3, BACKUP_ID, false); backupConfig3.setEncryptData(true); backupConfig3.setCompressData(true); RestoreConfig restoreConfig3 = new RestoreConfig(backupDir3, BACKUP_ID, false); // should perform a normal backup in absence of incremental base ID String label4 = "incremental_without_incrementalBaseID"; Backupable backupable4 = buildBackupable(createSourceDirectory(label4), 3); BackupDirectory backupDir4 = buildBackupDir(label4); BackupConfig backupConfig4 = new BackupConfig(backupDir4, BACKUP_ID, true); backupConfig4.setHashData(true); RestoreConfig restoreConfig4 = new RestoreConfig(backupDir4, BACKUP_ID, false); String label5 = "noFiles"; Backupable backupable5 = buildBackupable(createSourceDirectory(label5), 0); BackupDirectory backupDir5 = buildBackupDir(label5); BackupConfig backupConfig5 = new BackupConfig(backupDir5, BACKUP_ID, false); RestoreConfig restoreConfig5 = new RestoreConfig(backupDir5, BACKUP_ID, false); String label6 = "multiple_directories"; Backupable backupable6 = buildBackupableForMultipleDirectoriesCase(createSourceDirectory(label6), 3); BackupDirectory backupDir6 = buildBackupDir(label6); BackupConfig backupConfig6 = new BackupConfig(backupDir6, BACKUP_ID, false); RestoreConfig restoreConfig6 = new RestoreConfig(backupDir6, BACKUP_ID, false); return new Object[][] { { label0, backupable0, backupConfig0, restoreConfig0 }, { label1, backupable1, backupConfig1, restoreConfig1 }, { label2, backupable2, backupConfig2, restoreConfig2 }, { label3, backupable3, backupConfig3, restoreConfig3 }, { label4, backupable4, backupConfig4, restoreConfig4 }, { label5, backupable5, backupConfig5, restoreConfig5 }, { label6, backupable6, backupConfig6, restoreConfig6 }, }; } /** * This test encompasses creation, restore and remove of a backup. * * It allows to ensure that a backup can actually be restored. */ @Test(dataProvider="backupData") public void testCreateBackupThenRestoreThenRemove(String label, Backupable backupable, BackupConfig backupConfig, RestoreConfig restoreConfig) throws Exception { BackupManager backupManager = new BackupManager(BACKEND_ID); // create and check archive files backupManager.createBackup(backupable, backupConfig); String backupPath = backupConfig.getBackupDirectory().getPath(); assertThat(new File(backupPath, getArchiveFileName(BACKUP_ID))).exists(); assertThat(new File(backupPath, "backup.info")).exists(); // change content of directory to later check that backup is recovering everything removeBackedUpFiles(backupable); // restore and check list of files backupManager.restoreBackup(backupable, restoreConfig); assertAllFilesAreRestoredCorrectly(backupable); // remove the backup archive and check backupManager.removeBackup(backupConfig.getBackupDirectory(), BACKUP_ID); assertThat(new File(backupPath, getArchiveFileName(BACKUP_ID))).doesNotExist(); //cleanDirectories(sourceDirectory, backupPath); } /** * This test encompasses creation, restore and remove of an incremental backup. * * It allows to ensure that a backup can actually be restored. */ @Test() public void testCreateIncrementalBackupThenRestoreThenRemove() throws Exception { Path sourceDirectory = createSourceDirectory("incremental"); BackupDirectory backupDir = buildBackupDir("incremental"); BackupManager backupManager = new BackupManager(BACKEND_ID); // perform first backup with 2 files Backupable backupable0 = buildBackupable(sourceDirectory, 2); String initialBackupId = BACKUP_ID + "_0"; BackupConfig backupConfig0 = new BackupConfig(backupDir, initialBackupId, true); backupManager.createBackup(backupable0, backupConfig0); // check archive and info file String backupPath = backupDir.getPath(); assertThat(new File(backupPath, getArchiveFileName(initialBackupId))).exists(); assertThat(new File(backupPath, "backup.info")).exists(); // perform second backup with 4 files (2 initial files plus 2 new files) // now backup with id "backupID" should depend on backup with id "backupID_0" Backupable backupable1 = buildBackupable(sourceDirectory, 4); BackupConfig backupConfig1 = new BackupConfig(backupDir, BACKUP_ID, true); backupManager.createBackup(backupable1, backupConfig1); assertThat(new File(backupPath, getArchiveFileName(initialBackupId))).exists(); assertThat(new File(backupPath, getArchiveFileName(BACKUP_ID))).exists(); assertThat(new File(backupPath, "backup.info")).exists(); assertThat(new File(backupPath, "backup.info.save")).exists(); // change content of directory to later check that backup is recovering everything removeBackedUpFiles(backupable1); // restore and check list of files RestoreConfig restoreConfig = new RestoreConfig(backupDir, BACKUP_ID, false); backupManager.restoreBackup(backupable1, restoreConfig); assertAllFilesAreRestoredCorrectly(backupable1); // remove the backup archive and check backupManager.removeBackup(backupDir, BACKUP_ID); assertThat(new File(backupPath, getArchiveFileName(BACKUP_ID))).doesNotExist(); backupManager.removeBackup(backupDir, initialBackupId); assertThat(new File(backupPath, getArchiveFileName(initialBackupId))).doesNotExist(); cleanDirectories(sourceDirectory, backupPath); } @Test public void testCreateDirectoryWithNumericSuffix() throws Exception { File directory = TestCaseUtils.createTemporaryDirectory("createDirectory-"); String dirPath = directory.getAbsolutePath(); // delete the directory to ensure creation works fine when there is no directory directory.delete(); Path dir = BackupManager.createDirectoryWithNumericSuffix(dirPath, BACKUP_ID); assertThat(dir.toString()).isEqualTo(dirPath + "1"); Path dir2 = BackupManager.createDirectoryWithNumericSuffix(dirPath, BACKUP_ID); assertThat(dir2.toString()).isEqualTo(dirPath + "2"); recursiveDelete(dir.toFile()); recursiveDelete(dir2.toFile()); } @Test public void testSaveFilesToDirectory() throws Exception { Backupable backupable = buildBackupableForMultipleDirectoriesCase(createSourceDirectory("saveFiles-root"), 2); File rootDir = backupable.getDirectory(); File targetDir = TestCaseUtils.createTemporaryDirectory("saveFiles-target"); File actualTargetDir = BackupManager.saveFilesToDirectory(rootDir.toPath(), backupable.getFilesToBackup(), targetDir.getCanonicalPath(), BACKUP_ID).toFile(); // all files should have been saved in the target directory, with correct sub-path assertThat(new File(actualTargetDir, FILE_NAME_PREFIX+0)).exists(); assertThat(new File(actualTargetDir, FILE_NAME_PREFIX+1)).exists(); File subdir = new File(actualTargetDir, "subdir"); assertThat(new File(subdir, FILE_NAME_PREFIX+0)).exists(); assertThat(new File(subdir, FILE_NAME_PREFIX+1)).exists(); recursiveDelete(rootDir); recursiveDelete(targetDir); recursiveDelete(actualTargetDir); } private void cleanDirectories(Path sourceDirectory, String backupPath) { StaticUtils.recursiveDelete(sourceDirectory.toFile()); StaticUtils.recursiveDelete(new File(backupPath)); } private String getArchiveFileName(String backupId) { return "backup-" + BACKEND_ID + "-" + backupId; } private void assertAllFilesAreRestoredCorrectly(Backupable backupable) throws Exception { ListIterator<Path> files = backupable.getFilesToBackup(); while (files.hasNext()) { Path file = files.next(); assertThat(file.toFile()).exists(); assertThat(file.toFile()).hasContent(file.getFileName().toString()); } } private void removeBackedUpFiles(Backupable backupable) throws Exception { ListIterator<Path> it = backupable.getFilesToBackup(); while (it.hasNext()) { Path file = it.next(); Files.deleteIfExists(file); } } private BackupDirectory buildBackupDir(String label) throws Exception { File backupDirectory = TestCaseUtils.createTemporaryDirectory("backupDirectory-" + label + "-"); Reporter.log("Create backup directory:" + backupDirectory, true); BackupDirectory backupDir = new BackupDirectory(backupDirectory.getAbsolutePath(), DN.valueOf(ENTRY_DN)); return backupDir; } private Backupable buildBackupable(Path sourceDirectory, int numberOfFiles) throws Exception { List<Path> files = createFilesInDirectoryToBackup(sourceDirectory, numberOfFiles); Backupable backupable = mock(Backupable.class); when(backupable.getDirectory()).thenReturn(sourceDirectory.toFile()); when(backupable.getFilesToBackup()).thenReturn(files.listIterator()); when(backupable.isDirectRestore()).thenReturn(true); return backupable; } /** * Create files in source directory + additional files under a subdirectory of source directory */ private Backupable buildBackupableForMultipleDirectoriesCase(Path sourceDirectory, int numberOfFiles) throws Exception { List<Path> files = createFilesInDirectoryToBackup(sourceDirectory, numberOfFiles); // create an additional subdirectory with files Path subdir = sourceDirectory.resolve("subdir"); Files.createDirectory(subdir); List<Path> subdirFiles = createFilesInDirectoryToBackup(subdir, numberOfFiles); files.addAll(subdirFiles); Backupable backupable = mock(Backupable.class); when(backupable.getDirectory()).thenReturn(sourceDirectory.toFile()); when(backupable.getFilesToBackup()).thenReturn(files.listIterator()); return backupable; } private Path createSourceDirectory(String label) throws IOException { File sourceDirectory = TestCaseUtils.createTemporaryDirectory("dirToBackup-" + label + "-"); Reporter.log("Create directory to backup:" + sourceDirectory, true); return sourceDirectory.toPath(); } private List<Path> createFilesInDirectoryToBackup(Path directory, int numberOfFiles) throws Exception { List<Path> files = new ArrayList<>(); for (int i = 0; i < numberOfFiles; i++) { String filename = FILE_NAME_PREFIX + i; Path file = directory.resolve(filename); createFile(file, StaticUtils.getBytes(filename)); files.add(file); } return files; } private void createFile(Path file, byte[] content) throws Exception { OutputStream output = new FileOutputStream(file.toFile(), false); try { output.write(content); } finally { close(output); } } }