diff --git a/hadoop-hdfs-project/hadoop-hdfs/CHANGES.txt b/hadoop-hdfs-project/hadoop-hdfs/CHANGES.txt index fa00d448d80..266937d88e3 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/CHANGES.txt +++ b/hadoop-hdfs-project/hadoop-hdfs/CHANGES.txt @@ -635,6 +635,9 @@ Release 2.6.0 - UNRELEASED HDFS-6506. Newly moved block replica been invalidated and deleted in TestBalancer. (Binglin Chang via cnauroth) + HDFS-6966. Add additional unit tests for encryption zones. + (Stephen Chu via wang) + BREAKDOWN OF HDFS-6134 AND HADOOP-10150 SUBTASKS AND RELATED JIRAS HDFS-6387. HDFS CLI admin tool for creating & deleting an diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/DFSTestUtil.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/DFSTestUtil.java index e3a265fcab0..9e442ef9ba0 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/DFSTestUtil.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/DFSTestUtil.java @@ -1359,7 +1359,8 @@ public class DFSTestUtil { } /** - * Helper function to create a key in the Key Provider. + * Helper function to create a key in the Key Provider. Defaults + * to the first indexed NameNode's Key Provider. * * @param keyName The name of the key to create * @param cluster The cluster to create it in @@ -1368,7 +1369,22 @@ public class DFSTestUtil { public static void createKey(String keyName, MiniDFSCluster cluster, Configuration conf) throws NoSuchAlgorithmException, IOException { - KeyProvider provider = cluster.getNameNode().getNamesystem().getProvider(); + createKey(keyName, cluster, 0, conf); + } + + /** + * Helper function to create a key in the Key Provider. + * + * @param keyName The name of the key to create + * @param cluster The cluster to create it in + * @param idx The NameNode index + * @param conf Configuration to use + */ + public static void createKey(String keyName, MiniDFSCluster cluster, + int idx, Configuration conf) + throws NoSuchAlgorithmException, IOException { + NameNode nn = cluster.getNameNode(idx); + KeyProvider provider = nn.getNamesystem().getProvider(); final KeyProvider.Options options = KeyProvider.options(conf); options.setDescription(keyName); options.setBitLength(128); diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestEncryptionZones.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestEncryptionZones.java index d0d29ea6d79..db3c085fe12 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestEncryptionZones.java +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestEncryptionZones.java @@ -17,8 +17,14 @@ */ package org.apache.hadoop.hdfs; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.io.StringReader; +import java.io.StringWriter; import java.security.PrivilegedExceptionAction; import java.util.Arrays; import java.util.List; @@ -52,10 +58,15 @@ import org.apache.hadoop.hdfs.protocol.LocatedBlocks; import org.apache.hadoop.hdfs.protocol.HdfsConstants.SafeModeAction; import org.apache.hadoop.hdfs.server.namenode.EncryptionFaultInjector; import org.apache.hadoop.hdfs.server.namenode.EncryptionZoneManager; +import org.apache.hadoop.hdfs.server.namenode.FSImageTestUtil; +import org.apache.hadoop.hdfs.server.namenode.NamenodeFsck; +import org.apache.hadoop.hdfs.tools.DFSck; +import org.apache.hadoop.hdfs.tools.offlineImageViewer.PBImageXmlWriter; import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.security.Credentials; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.token.Token; +import org.apache.hadoop.util.ToolRunner; import org.apache.hadoop.crypto.key.KeyProviderDelegationTokenExtension.DelegationTokenExtension; import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension.CryptoExtension; import org.apache.hadoop.io.Text; @@ -78,6 +89,11 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import org.xml.sax.InputSource; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; public class TestEncryptionZones { @@ -374,6 +390,7 @@ public class TestEncryptionZones { final Path allPathFile = new Path(allPath, "file1"); final Path nonEZDir = new Path(testRoot, "nonEZDir"); final Path nonEZFile = new Path(nonEZDir, "file1"); + final Path nonexistent = new Path("/nonexistent"); final int len = 8192; fsWrapper.mkdir(testRoot, new FsPermission((short) 0777), true); @@ -421,6 +438,9 @@ public class TestEncryptionZones { assertExceptionContains("Permission denied:", e); } + assertNull("expected null for nonexistent path", + userAdmin.getEncryptionZoneForPath(nonexistent)); + // Check operation with non-ez paths assertNull("expected null for non-ez path", userAdmin.getEncryptionZoneForPath(nonEZDir)); @@ -463,9 +483,15 @@ public class TestEncryptionZones { final Path testRoot = new Path("/tmp/TestEncryptionZones"); final Path pathFoo = new Path(testRoot, "foo"); final Path pathFooBaz = new Path(pathFoo, "baz"); + final Path pathFooBazFile = new Path(pathFooBaz, "file"); + final Path pathFooBar = new Path(pathFoo, "bar"); + final Path pathFooBarFile = new Path(pathFooBar, "file"); + final int len = 8192; wrapper.mkdir(pathFoo, FsPermission.getDirDefault(), true); dfsAdmin.createEncryptionZone(pathFoo, TEST_KEY); wrapper.mkdir(pathFooBaz, FsPermission.getDirDefault(), true); + DFSTestUtil.createFile(fs, pathFooBazFile, len, (short) 1, 0xFEED); + String contents = DFSTestUtil.readFile(fs, pathFooBazFile); try { wrapper.rename(pathFooBaz, testRoot); } catch (IOException e) { @@ -473,6 +499,13 @@ public class TestEncryptionZones { " an encryption zone.", e ); } + + // Verify that we can rename dir and files within an encryption zone. + assertTrue(fs.rename(pathFooBaz, pathFooBar)); + assertTrue("Rename of dir and file within ez failed", + !wrapper.exists(pathFooBaz) && wrapper.exists(pathFooBar)); + assertEquals("Renamed file contents not the same", + contents, DFSTestUtil.readFile(fs, pathFooBarFile)); } @Test(timeout = 60000) @@ -806,4 +839,158 @@ public class TestEncryptionZones { Assert.assertEquals(tokens[1], testToken); Assert.assertEquals(1, creds.numberOfTokens()); } + + /** + * Test running fsck on a system with encryption zones. + */ + @Test(timeout = 60000) + public void testFsckOnEncryptionZones() throws Exception { + final int len = 8196; + final Path zoneParent = new Path("/zones"); + final Path zone1 = new Path(zoneParent, "zone1"); + final Path zone1File = new Path(zone1, "file"); + fsWrapper.mkdir(zone1, FsPermission.getDirDefault(), true); + dfsAdmin.createEncryptionZone(zone1, TEST_KEY); + DFSTestUtil.createFile(fs, zone1File, len, (short) 1, 0xFEED); + ByteArrayOutputStream bStream = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(bStream, true); + int errCode = ToolRunner.run(new DFSck(conf, out), + new String[]{ "/" }); + assertEquals("Fsck ran with non-zero error code", 0, errCode); + String result = bStream.toString(); + assertTrue("Fsck did not return HEALTHY status", + result.contains(NamenodeFsck.HEALTHY_STATUS)); + + // Run fsck directly on the encryption zone instead of root + errCode = ToolRunner.run(new DFSck(conf, out), + new String[]{ zoneParent.toString() }); + assertEquals("Fsck ran with non-zero error code", 0, errCode); + result = bStream.toString(); + assertTrue("Fsck did not return HEALTHY status", + result.contains(NamenodeFsck.HEALTHY_STATUS)); + } + + /** + * Test correctness of successive snapshot creation and deletion + * on a system with encryption zones. + */ + @Test(timeout = 60000) + public void testSnapshotsOnEncryptionZones() throws Exception { + final int len = 8196; + final Path zoneParent = new Path("/zones"); + final Path zone = new Path(zoneParent, "zone"); + final Path zoneFile = new Path(zone, "zoneFile"); + fsWrapper.mkdir(zone, FsPermission.getDirDefault(), true); + dfsAdmin.allowSnapshot(zoneParent); + dfsAdmin.createEncryptionZone(zone, TEST_KEY); + DFSTestUtil.createFile(fs, zoneFile, len, (short) 1, 0xFEED); + String contents = DFSTestUtil.readFile(fs, zoneFile); + final Path snap1 = fs.createSnapshot(zoneParent); + final Path snap1Zone = new Path(snap1, zone.getName()); + assertEquals("Got unexpected ez path", zone.toString(), + dfsAdmin.getEncryptionZoneForPath(snap1Zone).getPath().toString()); + + // Now delete the encryption zone, recreate the dir, and take another snapshot + fsWrapper.delete(zone, true); + fsWrapper.mkdir(zone, FsPermission.getDirDefault(), true); + final Path snap2 = fs.createSnapshot(zoneParent); + final Path snap2Zone = new Path(snap2, zone.getName()); + assertNull("Expected null ez path", + dfsAdmin.getEncryptionZoneForPath(snap2Zone)); + + // Create the encryption zone again + dfsAdmin.createEncryptionZone(zone, TEST_KEY); + final Path snap3 = fs.createSnapshot(zoneParent); + final Path snap3Zone = new Path(snap3, zone.getName()); + assertEquals("Got unexpected ez path", zone.toString(), + dfsAdmin.getEncryptionZoneForPath(snap3Zone).getPath().toString()); + + // Verify contents of the snapshotted file + final Path snapshottedZoneFile = new Path( + snap1.toString() + "/" + zone.getName() + "/" + zoneFile.getName()); + assertEquals("Contents of snapshotted file have changed unexpectedly", + contents, DFSTestUtil.readFile(fs, snapshottedZoneFile)); + + // Now delete the snapshots out of order and verify the zones are still correct + fs.deleteSnapshot(zoneParent, snap2.getName()); + assertEquals("Got unexpected ez path", zone.toString(), + dfsAdmin.getEncryptionZoneForPath(snap1Zone).getPath().toString()); + assertEquals("Got unexpected ez path", zone.toString(), + dfsAdmin.getEncryptionZoneForPath(snap3Zone).getPath().toString()); + fs.deleteSnapshot(zoneParent, snap1.getName()); + assertEquals("Got unexpected ez path", zone.toString(), + dfsAdmin.getEncryptionZoneForPath(snap3Zone).getPath().toString()); + } + + /** + * Verify symlinks can be created in encryption zones and that + * they function properly when the target is in the same + * or different ez. + */ + @Test(timeout = 60000) + public void testEncryptionZonesWithSymlinks() throws Exception { + // Verify we can create an encryption zone over both link and target + final int len = 8192; + final Path parent = new Path("/parent"); + final Path linkParent = new Path(parent, "symdir1"); + final Path targetParent = new Path(parent, "symdir2"); + final Path link = new Path(linkParent, "link"); + final Path target = new Path(targetParent, "target"); + fs.mkdirs(parent); + dfsAdmin.createEncryptionZone(parent, TEST_KEY); + fs.mkdirs(linkParent); + fs.mkdirs(targetParent); + DFSTestUtil.createFile(fs, target, len, (short)1, 0xFEED); + String content = DFSTestUtil.readFile(fs, target); + fs.createSymlink(target, link, false); + assertEquals("Contents read from link are not the same as target", + content, DFSTestUtil.readFile(fs, link)); + fs.delete(parent, true); + + // Now let's test when the symlink and target are in different + // encryption zones + fs.mkdirs(linkParent); + fs.mkdirs(targetParent); + dfsAdmin.createEncryptionZone(linkParent, TEST_KEY); + dfsAdmin.createEncryptionZone(targetParent, TEST_KEY); + DFSTestUtil.createFile(fs, target, len, (short)1, 0xFEED); + content = DFSTestUtil.readFile(fs, target); + fs.createSymlink(target, link, false); + assertEquals("Contents read from link are not the same as target", + content, DFSTestUtil.readFile(fs, link)); + fs.delete(link, true); + fs.delete(target, true); + } + + /** + * Test running the OfflineImageViewer on a system with encryption zones. + */ + @Test(timeout = 60000) + public void testOfflineImageViewerOnEncryptionZones() throws Exception { + final int len = 8196; + final Path zoneParent = new Path("/zones"); + final Path zone1 = new Path(zoneParent, "zone1"); + final Path zone1File = new Path(zone1, "file"); + fsWrapper.mkdir(zone1, FsPermission.getDirDefault(), true); + dfsAdmin.createEncryptionZone(zone1, TEST_KEY); + DFSTestUtil.createFile(fs, zone1File, len, (short) 1, 0xFEED); + fs.setSafeMode(SafeModeAction.SAFEMODE_ENTER, false); + fs.saveNamespace(); + + File originalFsimage = FSImageTestUtil.findLatestImageFile(FSImageTestUtil + .getFSImage(cluster.getNameNode()).getStorage().getStorageDir(0)); + if (originalFsimage == null) { + throw new RuntimeException("Didn't generate or can't find fsimage"); + } + + // Run the XML OIV processor + StringWriter output = new StringWriter(); + PrintWriter pw = new PrintWriter(output); + PBImageXmlWriter v = new PBImageXmlWriter(new Configuration(), pw); + v.visit(new RandomAccessFile(originalFsimage, "r")); + final String xml = output.getBuffer().toString(); + SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); + parser.parse(new InputSource(new StringReader(xml)), new DefaultHandler()); + } + } diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestEncryptionZonesWithHA.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestEncryptionZonesWithHA.java new file mode 100644 index 00000000000..b6040045ac4 --- /dev/null +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/TestEncryptionZonesWithHA.java @@ -0,0 +1,121 @@ +/** + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.crypto.key.JavaKeyStoreProvider; +import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension; +import org.apache.hadoop.crypto.key.KeyProviderFactory; +import org.apache.hadoop.fs.permission.FsPermission; +import org.apache.hadoop.fs.FileSystemTestHelper; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hdfs.server.namenode.ha.HATestUtil; +import org.apache.hadoop.hdfs.client.HdfsAdmin; +import org.apache.hadoop.hdfs.server.namenode.NameNode; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; + +/** + * Tests interaction of encryption zones with HA failover. + */ +public class TestEncryptionZonesWithHA { + private Configuration conf; + private MiniDFSCluster cluster; + private NameNode nn0; + private NameNode nn1; + private DistributedFileSystem fs; + private HdfsAdmin dfsAdmin0; + private HdfsAdmin dfsAdmin1; + private FileSystemTestHelper fsHelper; + private File testRootDir; + + private final String TEST_KEY = "testKey"; + + + @Before + public void setupCluster() throws Exception { + conf = new Configuration(); + conf.setInt(DFSConfigKeys.DFS_HA_TAILEDITS_PERIOD_KEY, 1); + HAUtil.setAllowStandbyReads(conf, true); + fsHelper = new FileSystemTestHelper(); + String testRoot = fsHelper.getTestRootDir(); + testRootDir = new File(testRoot).getAbsoluteFile(); + conf.set(KeyProviderFactory.KEY_PROVIDER_PATH, + JavaKeyStoreProvider.SCHEME_NAME + "://file" + testRootDir + "/test.jks" + ); + + cluster = new MiniDFSCluster.Builder(conf) + .nnTopology(MiniDFSNNTopology.simpleHATopology()) + .numDataNodes(1) + .build(); + cluster.waitActive(); + cluster.transitionToActive(0); + + fs = (DistributedFileSystem)HATestUtil.configureFailoverFs(cluster, conf); + DFSTestUtil.createKey(TEST_KEY, cluster, 0, conf); + DFSTestUtil.createKey(TEST_KEY, cluster, 1, conf); + nn0 = cluster.getNameNode(0); + nn1 = cluster.getNameNode(1); + dfsAdmin0 = new HdfsAdmin(cluster.getURI(0), conf); + dfsAdmin1 = new HdfsAdmin(cluster.getURI(1), conf); + KeyProviderCryptoExtension nn0Provider = + cluster.getNameNode(0).getNamesystem().getProvider(); + fs.getClient().provider = nn0Provider; + } + + @After + public void shutdownCluster() throws IOException { + if (cluster != null) { + cluster.shutdown(); + } + } + + /** + * Test that encryption zones are properly tracked by the standby. + */ + @Test(timeout = 60000) + public void testEncryptionZonesTrackedOnStandby() throws Exception { + final int len = 8196; + final Path dir = new Path("/enc"); + final Path dirChild = new Path(dir, "child"); + final Path dirFile = new Path(dir, "file"); + fs.mkdir(dir, FsPermission.getDirDefault()); + dfsAdmin0.createEncryptionZone(dir, TEST_KEY); + fs.mkdir(dirChild, FsPermission.getDirDefault()); + DFSTestUtil.createFile(fs, dirFile, len, (short) 1, 0xFEED); + String contents = DFSTestUtil.readFile(fs, dirFile); + + // Failover the current standby to active. + HATestUtil.waitForStandbyToCatchUp(nn0, nn1); + cluster.shutdownNameNode(0); + cluster.transitionToActive(1); + + Assert.assertEquals("Got unexpected ez path", dir.toString(), + dfsAdmin1.getEncryptionZoneForPath(dir).getPath().toString()); + Assert.assertEquals("Got unexpected ez path", dir.toString(), + dfsAdmin1.getEncryptionZoneForPath(dirChild).getPath().toString()); + Assert.assertEquals("File contents after failover were changed", + contents, DFSTestUtil.readFile(fs, dirFile)); + } + +}