| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * <p/> |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * <p/> |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.hadoop.hdfs.server.namenode; |
| |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Maps; |
| |
| import org.apache.hadoop.conf.Configuration; |
| import org.apache.hadoop.fs.CommonConfigurationKeys; |
| import org.apache.hadoop.fs.FileSystem; |
| import org.apache.hadoop.fs.Path; |
| import org.apache.hadoop.hdfs.HdfsConfiguration; |
| import org.apache.hadoop.hdfs.MiniDFSCluster; |
| import org.apache.hadoop.security.AccessControlException; |
| import org.apache.hadoop.hdfs.server.namenode.FSDirectory; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.Timeout; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.IOException; |
| import java.util.*; |
| |
| import static org.hamcrest.core.Is.is; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertThat; |
| import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.FS_PROTECTED_DIRECTORIES; |
| |
| /** |
| * Verify that the dfs.namenode.protected.directories setting is respected. |
| */ |
| public class TestProtectedDirectories { |
| static final Logger LOG = LoggerFactory.getLogger( |
| TestProtectedDirectories.class); |
| |
| @Rule |
| public Timeout timeout = new Timeout(300000); |
| |
| /** |
| * Start a namenode-only 'cluster' which is configured to protect |
| * the given list of directories. |
| * @param conf |
| * @param protectedDirs |
| * @param unProtectedDirs |
| * @return |
| * @throws IOException |
| */ |
| public MiniDFSCluster setupTestCase(Configuration conf, |
| Collection<Path> protectedDirs, |
| Collection<Path> unProtectedDirs) |
| throws Throwable { |
| // Initialize the configuration. |
| conf.set( |
| CommonConfigurationKeys.FS_PROTECTED_DIRECTORIES, |
| Joiner.on(",").skipNulls().join(protectedDirs)); |
| |
| // Start the cluster. |
| MiniDFSCluster cluster = |
| new MiniDFSCluster.Builder(conf).numDataNodes(0).build(); |
| |
| // Create all the directories. |
| try { |
| cluster.waitActive(); |
| FileSystem fs = cluster.getFileSystem(); |
| for (Path path : Iterables.concat(protectedDirs, unProtectedDirs)) { |
| fs.mkdirs(path); |
| } |
| return cluster; |
| } catch (Throwable t) { |
| cluster.shutdown(); |
| throw t; |
| } |
| } |
| |
| /** |
| * Initialize a collection of file system layouts that will be used |
| * as the test matrix. |
| * |
| * @return |
| */ |
| private Collection<TestMatrixEntry> createTestMatrix() { |
| Collection<TestMatrixEntry> matrix = new ArrayList<TestMatrixEntry>(); |
| |
| // single empty unprotected dir. |
| matrix.add(TestMatrixEntry.get() |
| .addUnprotectedDir("/1", true)); |
| |
| // Single empty protected dir. |
| matrix.add(TestMatrixEntry.get() |
| .addProtectedDir("/1", true)); |
| |
| // Nested unprotected dirs. |
| matrix.add(TestMatrixEntry.get() |
| .addUnprotectedDir("/1", true) |
| .addUnprotectedDir("/1/2", true) |
| .addUnprotectedDir("/1/2/3", true) |
| .addUnprotectedDir("/1/2/3/4", true)); |
| |
| // Non-empty protected dir. |
| matrix.add(TestMatrixEntry.get() |
| .addProtectedDir("/1", false) |
| .addUnprotectedDir("/1/2", true)); |
| |
| // Protected empty child of unprotected parent. |
| matrix.add(TestMatrixEntry.get() |
| .addProtectedDir("/1/2", true) |
| .addUnprotectedDir("/1/2", true)); |
| |
| // Protected empty child of protected parent. |
| // We should not be able to delete the parent. |
| matrix.add(TestMatrixEntry.get() |
| .addProtectedDir("/1", false) |
| .addProtectedDir("/1/2", true)); |
| |
| // One of each, non-nested. |
| matrix.add(TestMatrixEntry.get() |
| .addProtectedDir("/1", true) |
| .addUnprotectedDir("/a", true)); |
| |
| // Protected non-empty child of unprotected parent. |
| // Neither should be deletable. |
| matrix.add(TestMatrixEntry.get() |
| .addProtectedDir("/1/2", false) |
| .addUnprotectedDir("/1/2/3", true) |
| .addUnprotectedDir("/1", false)); |
| |
| // Protected non-empty child has unprotected siblings. |
| matrix.add(TestMatrixEntry.get() |
| .addProtectedDir("/1/2.2", false) |
| .addUnprotectedDir("/1/2.2/3", true) |
| .addUnprotectedDir("/1/2.1", true) |
| .addUnprotectedDir("/1/2.3", true) |
| .addUnprotectedDir("/1", false)); |
| |
| // Deeply nested protected child. |
| matrix.add(TestMatrixEntry.get() |
| .addProtectedDir("/1/2/3/4/5", false) |
| .addUnprotectedDir("/1/2/3/4/5/6", true) |
| .addUnprotectedDir("/1", false) |
| .addUnprotectedDir("/1/2", false) |
| .addUnprotectedDir("/1/2/3", false) |
| .addUnprotectedDir("/1/2/3/4", false)); |
| |
| // Disjoint trees. |
| matrix.add(TestMatrixEntry.get() |
| .addProtectedDir("/1/2", false) |
| .addProtectedDir("/a/b", false) |
| .addUnprotectedDir("/1/2/3", true) |
| .addUnprotectedDir("/a/b/c", true)); |
| |
| // The following tests exercise special cases in the path prefix |
| // checks and handling of trailing separators. |
| |
| // A disjoint non-empty protected dir has the same string prefix as the |
| // directory we are trying to delete. |
| matrix.add(TestMatrixEntry.get() |
| .addProtectedDir("/a1", false) |
| .addUnprotectedDir("/a1/a2", true) |
| .addUnprotectedDir("/a", true)); |
| |
| // The directory we are trying to delete has a non-empty protected |
| // child and we try to delete it with a trailing separator. |
| matrix.add(TestMatrixEntry.get() |
| .addProtectedDir("/a/b", false) |
| .addUnprotectedDir("/a/b/c", true) |
| .addUnprotectedDir("/a/", false)); |
| |
| // The directory we are trying to delete has an empty protected |
| // child and we try to delete it with a trailing separator. |
| matrix.add(TestMatrixEntry.get() |
| .addProtectedDir("/a/b", true) |
| .addUnprotectedDir("/a/", true)); |
| |
| return matrix; |
| } |
| |
| @Test |
| public void testReconfigureProtectedPaths() throws Throwable { |
| Configuration conf = new HdfsConfiguration(); |
| Collection<Path> protectedPaths = Arrays.asList(new Path("/a"), new Path( |
| "/b"), new Path("/c")); |
| Collection<Path> unprotectedPaths = Arrays.asList(); |
| |
| MiniDFSCluster cluster = setupTestCase(conf, protectedPaths, |
| unprotectedPaths); |
| |
| SortedSet<String> protectedPathsNew = new TreeSet<>( |
| FSDirectory.normalizePaths(Arrays.asList("/aa", "/bb", "/cc"), |
| FS_PROTECTED_DIRECTORIES)); |
| |
| String protectedPathsStrNew = "/aa,/bb,/cc"; |
| |
| NameNode nn = cluster.getNameNode(); |
| |
| // change properties |
| nn.reconfigureProperty(FS_PROTECTED_DIRECTORIES, protectedPathsStrNew); |
| |
| FSDirectory fsDirectory = nn.getNamesystem().getFSDirectory(); |
| // verify change |
| assertEquals(String.format("%s has wrong value", FS_PROTECTED_DIRECTORIES), |
| protectedPathsNew, fsDirectory.getProtectedDirectories()); |
| |
| assertEquals(String.format("%s has wrong value", FS_PROTECTED_DIRECTORIES), |
| protectedPathsStrNew, nn.getConf().get(FS_PROTECTED_DIRECTORIES)); |
| |
| // revert to default |
| nn.reconfigureProperty(FS_PROTECTED_DIRECTORIES, null); |
| |
| // verify default |
| assertEquals(String.format("%s has wrong value", FS_PROTECTED_DIRECTORIES), |
| new TreeSet<String>(), fsDirectory.getProtectedDirectories()); |
| |
| assertEquals(String.format("%s has wrong value", FS_PROTECTED_DIRECTORIES), |
| null, nn.getConf().get(FS_PROTECTED_DIRECTORIES)); |
| } |
| |
| @Test |
| public void testAll() throws Throwable { |
| for (TestMatrixEntry testMatrixEntry : createTestMatrix()) { |
| Configuration conf = new HdfsConfiguration(); |
| MiniDFSCluster cluster = setupTestCase( |
| conf, testMatrixEntry.getProtectedPaths(), |
| testMatrixEntry.getUnprotectedPaths()); |
| |
| try { |
| LOG.info("Running {}", testMatrixEntry); |
| FileSystem fs = cluster.getFileSystem(); |
| for (Path path : testMatrixEntry.getAllPathsToBeDeleted()) { |
| final long countBefore = cluster.getNamesystem().getFilesTotal(); |
| assertThat( |
| testMatrixEntry + ": Testing whether " + path + " can be deleted", |
| deletePath(fs, path), |
| is(testMatrixEntry.canPathBeDeleted(path))); |
| final long countAfter = cluster.getNamesystem().getFilesTotal(); |
| |
| if (!testMatrixEntry.canPathBeDeleted(path)) { |
| assertThat( |
| "Either all paths should be deleted or none", |
| countAfter, is(countBefore)); |
| } |
| } |
| } finally { |
| cluster.shutdown(); |
| } |
| } |
| } |
| |
| /** |
| * Verify that configured paths are normalized by removing |
| * redundant separators. |
| */ |
| @Test |
| public void testProtectedDirNormalization1() { |
| Configuration conf = new HdfsConfiguration(); |
| conf.set( |
| CommonConfigurationKeys.FS_PROTECTED_DIRECTORIES, |
| "/foo//bar"); |
| Collection<String> paths = FSDirectory.parseProtectedDirectories(conf); |
| assertThat(paths.size(), is(1)); |
| assertThat(paths.iterator().next(), is("/foo/bar")); |
| } |
| |
| /** |
| * Verify that configured paths are normalized by removing |
| * trailing separators. |
| */ |
| @Test |
| public void testProtectedDirNormalization2() { |
| Configuration conf = new HdfsConfiguration(); |
| conf.set( |
| CommonConfigurationKeys.FS_PROTECTED_DIRECTORIES, |
| "/a/b/,/c,/d/e/f/"); |
| Collection<String> paths = FSDirectory.parseProtectedDirectories(conf); |
| |
| for (String path : paths) { |
| assertFalse(path.endsWith("/")); |
| } |
| } |
| |
| /** |
| * Verify that configured paths are canonicalized. |
| */ |
| @Test |
| public void testProtectedDirIsCanonicalized() { |
| Configuration conf = new HdfsConfiguration(); |
| conf.set( |
| CommonConfigurationKeys.FS_PROTECTED_DIRECTORIES, |
| "/foo/../bar/"); |
| Collection<String> paths = FSDirectory.parseProtectedDirectories(conf); |
| assertThat(paths.size(), is(1)); |
| assertThat(paths.iterator().next(), is("/bar")); |
| } |
| |
| /** |
| * Verify that the root directory in the configuration is correctly handled. |
| */ |
| @Test |
| public void testProtectedRootDirectory() { |
| Configuration conf = new HdfsConfiguration(); |
| conf.set( |
| CommonConfigurationKeys.FS_PROTECTED_DIRECTORIES, "/"); |
| Collection<String> paths = FSDirectory.parseProtectedDirectories(conf); |
| assertThat(paths.size(), is(1)); |
| assertThat(paths.iterator().next(), is("/")); |
| } |
| |
| /** |
| * Verify that invalid paths in the configuration are filtered out. |
| * (Path with scheme, reserved path). |
| */ |
| @Test |
| public void testBadPathsInConfig() { |
| Configuration conf = new HdfsConfiguration(); |
| conf.set( |
| CommonConfigurationKeys.FS_PROTECTED_DIRECTORIES, |
| "hdfs://foo/,/.reserved/foo"); |
| Collection<String> paths = FSDirectory.parseProtectedDirectories(conf); |
| assertThat("Unexpected directories " + paths, |
| paths.size(), is(0)); |
| } |
| |
| /** |
| * Return true if the path was successfully deleted. False if it |
| * failed with AccessControlException. Any other exceptions are |
| * propagated to the caller. |
| * |
| * @param fs |
| * @param path |
| * @return |
| */ |
| private boolean deletePath(FileSystem fs, Path path) throws IOException { |
| try { |
| fs.delete(path, true); |
| return true; |
| } catch (AccessControlException ace) { |
| return false; |
| } |
| } |
| |
| private static class TestMatrixEntry { |
| // true if the path can be deleted. |
| final Map<Path, Boolean> protectedPaths = Maps.newHashMap(); |
| final Map<Path, Boolean> unProtectedPaths = Maps.newHashMap(); |
| |
| private TestMatrixEntry() { |
| } |
| |
| public static TestMatrixEntry get() { |
| return new TestMatrixEntry(); |
| } |
| |
| public Collection<Path> getProtectedPaths() { |
| return protectedPaths.keySet(); |
| } |
| |
| public Collection<Path> getUnprotectedPaths() { |
| return unProtectedPaths.keySet(); |
| } |
| |
| /** |
| * Get all paths to be deleted in sorted order. |
| * @return sorted collection of paths to be deleted. |
| */ |
| @SuppressWarnings("unchecked") // Path implements Comparable incorrectly |
| public Iterable<Path> getAllPathsToBeDeleted() { |
| // Sorting ensures deletion of parents is attempted first. |
| ArrayList<Path> combined = new ArrayList<>(); |
| combined.addAll(protectedPaths.keySet()); |
| combined.addAll(unProtectedPaths.keySet()); |
| Collections.sort(combined); |
| return combined; |
| } |
| |
| public boolean canPathBeDeleted(Path path) { |
| return protectedPaths.containsKey(path) ? |
| protectedPaths.get(path) : unProtectedPaths.get(path); |
| } |
| |
| |
| public TestMatrixEntry addProtectedDir(String dir, boolean canBeDeleted) { |
| protectedPaths.put(new Path(dir), canBeDeleted); |
| return this; |
| } |
| |
| public TestMatrixEntry addUnprotectedDir(String dir, boolean canBeDeleted) { |
| unProtectedPaths.put(new Path(dir), canBeDeleted); |
| return this; |
| } |
| |
| @Override |
| public String toString() { |
| return "TestMatrixEntry - ProtectedPaths=[" + |
| Joiner.on(", ").join(protectedPaths.keySet()) + |
| "]; UnprotectedPaths=[" + |
| Joiner.on(", ").join(unProtectedPaths.keySet()) + "]"; |
| } |
| } |
| } |