HDFS-12875. RBF: Complete logic for -readonly option of dfsrouteradmin add command. Contributed by Inigo Goiri.

This commit is contained in:
Inigo Goiri 2017-12-11 15:14:57 -08:00
parent 2316f52690
commit 5cd1056ad7
9 changed files with 275 additions and 38 deletions

View File

@ -103,8 +103,10 @@ import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifie
import org.apache.hadoop.hdfs.server.federation.resolver.ActiveNamenodeResolver; import org.apache.hadoop.hdfs.server.federation.resolver.ActiveNamenodeResolver;
import org.apache.hadoop.hdfs.server.federation.resolver.FederationNamespaceInfo; import org.apache.hadoop.hdfs.server.federation.resolver.FederationNamespaceInfo;
import org.apache.hadoop.hdfs.server.federation.resolver.FileSubclusterResolver; import org.apache.hadoop.hdfs.server.federation.resolver.FileSubclusterResolver;
import org.apache.hadoop.hdfs.server.federation.resolver.MountTableResolver;
import org.apache.hadoop.hdfs.server.federation.resolver.PathLocation; import org.apache.hadoop.hdfs.server.federation.resolver.PathLocation;
import org.apache.hadoop.hdfs.server.federation.resolver.RemoteLocation; import org.apache.hadoop.hdfs.server.federation.resolver.RemoteLocation;
import org.apache.hadoop.hdfs.server.federation.store.records.MountTable;
import org.apache.hadoop.hdfs.server.namenode.LeaseExpiredException; import org.apache.hadoop.hdfs.server.namenode.LeaseExpiredException;
import org.apache.hadoop.hdfs.server.namenode.NameNode.OperationCategory; import org.apache.hadoop.hdfs.server.namenode.NameNode.OperationCategory;
import org.apache.hadoop.hdfs.server.namenode.NotReplicatedYetException; import org.apache.hadoop.hdfs.server.namenode.NotReplicatedYetException;
@ -1982,6 +1984,17 @@ public class RouterRpcServer extends AbstractService implements ClientProtocol {
this.subclusterResolver); this.subclusterResolver);
} }
// We may block some write operations
if (opCategory.get() == OperationCategory.WRITE) {
// Check if the path is in a read only mount point
if (isPathReadOnly(path)) {
if (this.rpcMonitor != null) {
this.rpcMonitor.routerFailureReadOnly();
}
throw new IOException(path + " is in a read only mount point");
}
}
return location.getDestinations(); return location.getDestinations();
} catch (IOException ioe) { } catch (IOException ioe) {
if (this.rpcMonitor != null) { if (this.rpcMonitor != null) {
@ -1991,6 +2004,27 @@ public class RouterRpcServer extends AbstractService implements ClientProtocol {
} }
} }
/**
* Check if a path is in a read only mount point.
*
* @param path Path to check.
* @return If the path is in a read only mount point.
*/
private boolean isPathReadOnly(final String path) {
if (subclusterResolver instanceof MountTableResolver) {
try {
MountTableResolver mountTable = (MountTableResolver)subclusterResolver;
MountTable entry = mountTable.getMountPoint(path);
if (entry != null && entry.isReadOnly()) {
return true;
}
} catch (IOException e) {
LOG.error("Cannot get mount point: {}", e.getMessage());
}
}
return false;
}
/** /**
* Get the modification dates for mount points. * Get the modification dates for mount points.
* *

View File

@ -77,7 +77,7 @@ public class RouterAdmin extends Configured implements Tool {
public void printUsage() { public void printUsage() {
String usage = "Federation Admin Tools:\n" String usage = "Federation Admin Tools:\n"
+ "\t[-add <source> <nameservice> <destination> " + "\t[-add <source> <nameservice> <destination> "
+ "[-readonly] [-order HASH|LOCAL|RANDOM|HASH_ALL]]\n" + "[-readonly]\n"
+ "\t[-rm <source>]\n" + "\t[-rm <source>]\n"
+ "\t[-ls <path>]\n"; + "\t[-ls <path>]\n";
System.out.println(usage); System.out.println(usage);

View File

@ -425,7 +425,7 @@ Runs the DFS router. See [Router](./HDFSRouterFederation.html#Router) for more i
Usage: Usage:
hdfs dfsrouteradmin hdfs dfsrouteradmin
[-add <source> <nameservice> <destination>] [-add <source> <nameservice> <destination> [-readonly]]
[-rm <source>] [-rm <source>]
[-ls <path>] [-ls <path>]

View File

@ -184,6 +184,10 @@ For example, to create three mount points and list them:
[hdfs]$ $HADOOP_HOME/bin/hdfs dfsrouteradmin -add /data/app2 ns3 /data/app2 [hdfs]$ $HADOOP_HOME/bin/hdfs dfsrouteradmin -add /data/app2 ns3 /data/app2
[hdfs]$ $HADOOP_HOME/bin/hdfs dfsrouteradmin -ls [hdfs]$ $HADOOP_HOME/bin/hdfs dfsrouteradmin -ls
It also supports mount points that disallow writes:
[hdfs]$ $HADOOP_HOME/bin/hdfs dfsrouteradmin -add /readonly ns1 / -readonly
If a mount point is not set, the Router will map it to the default namespace `dfs.federation.router.default.nameserviceId`. If a mount point is not set, the Router will map it to the default namespace `dfs.federation.router.default.nameserviceId`.

View File

@ -20,7 +20,6 @@ package org.apache.hadoop.hdfs.server.federation;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -52,6 +51,9 @@ import org.apache.hadoop.hdfs.server.federation.resolver.FederationNamenodeServi
import org.apache.hadoop.hdfs.server.federation.resolver.NamenodeStatusReport; import org.apache.hadoop.hdfs.server.federation.resolver.NamenodeStatusReport;
import org.apache.hadoop.hdfs.server.protocol.NamespaceInfo; import org.apache.hadoop.hdfs.server.protocol.NamespaceInfo;
import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.security.AccessControlException;
import org.apache.hadoop.test.GenericTestUtils;
import com.google.common.base.Supplier;
/** /**
* Helper utilities for testing HDFS Federation. * Helper utilities for testing HDFS Federation.
@ -108,33 +110,41 @@ public final class FederationTestUtils {
return report; return report;
} }
public static void waitNamenodeRegistered(ActiveNamenodeResolver resolver, /**
String nsId, String nnId, FederationNamenodeServiceState finalState) * Wait for a namenode to be registered with a particular state.
throws InterruptedException, IllegalStateException, IOException { * @param resolver Active namenode resolver.
* @param nsId Nameservice identifier.
for (int loopCount = 0; loopCount < 20; loopCount++) { * @param nnId Namenode identifier.
if (loopCount > 0) { * @param finalState State to check for.
Thread.sleep(1000); * @throws Exception Failed to verify State Store registration of namenode
} * nsId:nnId for state.
*/
public static void waitNamenodeRegistered(
final ActiveNamenodeResolver resolver,
final String nsId, final String nnId,
final FederationNamenodeServiceState state) throws Exception {
GenericTestUtils.waitFor(new Supplier<Boolean>() {
@Override
public Boolean get() {
try {
List<? extends FederationNamenodeContext> namenodes = List<? extends FederationNamenodeContext> namenodes =
resolver.getNamenodesForNameserviceId(nsId); resolver.getNamenodesForNameserviceId(nsId);
if (namenodes != null) {
for (FederationNamenodeContext namenode : namenodes) { for (FederationNamenodeContext namenode : namenodes) {
// Check if this is the Namenode we are checking // Check if this is the Namenode we are checking
if (namenode.getNamenodeId() == nnId || if (namenode.getNamenodeId() == nnId ||
namenode.getNamenodeId().equals(nnId)) { namenode.getNamenodeId().equals(nnId)) {
if (finalState != null && !namenode.getState().equals(finalState)) { return state == null || namenode.getState().equals(state);
// Wrong state, wait a bit more
break;
} else {
// Found and verified
return;
} }
} }
} }
} catch (IOException e) {
// Ignore
} }
fail("Failed to verify State Store registration of " + nsId + " " + nnId + return false;
" for state " + finalState); }
}, 1000, 20 * 1000);
} }
public static boolean verifyDate(Date d1, Date d2, long precision) { public static boolean verifyDate(Date d1, Date d2, long precision) {

View File

@ -747,8 +747,7 @@ public class RouterDFSCluster {
} }
} }
public void waitNamenodeRegistration() public void waitNamenodeRegistration() throws Exception {
throws InterruptedException, IllegalStateException, IOException {
for (RouterContext r : this.routers) { for (RouterContext r : this.routers) {
Router router = r.router; Router router = r.router;
for (NamenodeContext nn : this.namenodes) { for (NamenodeContext nn : this.namenodes) {
@ -761,7 +760,7 @@ public class RouterDFSCluster {
public void waitRouterRegistrationQuorum(RouterContext router, public void waitRouterRegistrationQuorum(RouterContext router,
FederationNamenodeServiceState state, String nsId, String nnId) FederationNamenodeServiceState state, String nsId, String nnId)
throws InterruptedException, IOException { throws Exception {
LOG.info("Waiting for NN {} {} to transition to {}", nsId, nnId, state); LOG.info("Waiting for NN {} {} to transition to {}", nsId, nnId, state);
ActiveNamenodeResolver nnResolver = router.router.getNamenodeResolver(); ActiveNamenodeResolver nnResolver = router.router.getNamenodeResolver();
waitNamenodeRegistered(nnResolver, nsId, nnId, state); waitNamenodeRegistered(nnResolver, nsId, nnId, state);

View File

@ -69,6 +69,7 @@ public class TestMountTableResolver {
* ______file1.txt -> 4:/user/file1.txt * ______file1.txt -> 4:/user/file1.txt
* __usr * __usr
* ____bin -> 2:/bin * ____bin -> 2:/bin
* __readonly -> 2:/tmp
* *
* @throws IOException If it cannot set the mount table. * @throws IOException If it cannot set the mount table.
*/ */
@ -107,6 +108,12 @@ public class TestMountTableResolver {
// /user/a/demo/test/b // /user/a/demo/test/b
map = getMountTableEntry("3", "/user/test"); map = getMountTableEntry("3", "/user/test");
mountTable.addEntry(MountTable.newInstance("/user/a/demo/test/b", map)); mountTable.addEntry(MountTable.newInstance("/user/a/demo/test/b", map));
// /readonly
map = getMountTableEntry("2", "/tmp");
MountTable readOnlyEntry = MountTable.newInstance("/readonly", map);
readOnlyEntry.setReadOnly(true);
mountTable.addEntry(readOnlyEntry);
} }
@Before @Before
@ -152,6 +159,9 @@ public class TestMountTableResolver {
assertEquals("3->/user/test/a", assertEquals("3->/user/test/a",
mountTable.getDestinationForPath("/user/test/a").toString()); mountTable.getDestinationForPath("/user/test/a").toString());
assertEquals("2->/tmp/tesfile1.txt",
mountTable.getDestinationForPath("/readonly/tesfile1.txt").toString());
} }
private void compareLists(List<String> list1, String[] list2) { private void compareLists(List<String> list1, String[] list2) {
@ -166,8 +176,8 @@ public class TestMountTableResolver {
// Check getting all mount points (virtual and real) beneath a path // Check getting all mount points (virtual and real) beneath a path
List<String> mounts = mountTable.getMountPoints("/"); List<String> mounts = mountTable.getMountPoints("/");
assertEquals(3, mounts.size()); assertEquals(4, mounts.size());
compareLists(mounts, new String[] {"tmp", "user", "usr"}); compareLists(mounts, new String[] {"tmp", "user", "usr", "readonly"});
mounts = mountTable.getMountPoints("/user"); mounts = mountTable.getMountPoints("/user");
assertEquals(2, mounts.size()); assertEquals(2, mounts.size());
@ -212,9 +222,10 @@ public class TestMountTableResolver {
// Check listing the mount table records at or beneath a path // Check listing the mount table records at or beneath a path
List<MountTable> records = mountTable.getMounts("/"); List<MountTable> records = mountTable.getMounts("/");
assertEquals(8, records.size()); assertEquals(9, records.size());
compareRecords(records, new String[] {"/", "/tmp", "/user", "/usr/bin", compareRecords(records, new String[] {"/", "/tmp", "/user", "/usr/bin",
"user/a", "/user/a/demo/a", "/user/a/demo/b", "/user/b/file1.txt"}); "user/a", "/user/a/demo/a", "/user/a/demo/b", "/user/b/file1.txt",
"readonly"});
records = mountTable.getMounts("/user"); records = mountTable.getMounts("/user");
assertEquals(5, records.size()); assertEquals(5, records.size());
@ -229,6 +240,11 @@ public class TestMountTableResolver {
records = mountTable.getMounts("/tmp"); records = mountTable.getMounts("/tmp");
assertEquals(1, records.size()); assertEquals(1, records.size());
compareRecords(records, new String[] {"/tmp"}); compareRecords(records, new String[] {"/tmp"});
records = mountTable.getMounts("/readonly");
assertEquals(1, records.size());
compareRecords(records, new String[] {"/readonly"});
assertTrue(records.get(0).isReadOnly());
} }
@Test @Test
@ -237,7 +253,7 @@ public class TestMountTableResolver {
// 3 mount points are present /tmp, /user, /usr // 3 mount points are present /tmp, /user, /usr
compareLists(mountTable.getMountPoints("/"), compareLists(mountTable.getMountPoints("/"),
new String[] {"user", "usr", "tmp"}); new String[] {"user", "usr", "tmp", "readonly"});
// /tmp currently points to namespace 2 // /tmp currently points to namespace 2
assertEquals("2", mountTable.getDestinationForPath("/tmp/testfile.txt") assertEquals("2", mountTable.getDestinationForPath("/tmp/testfile.txt")
@ -248,7 +264,7 @@ public class TestMountTableResolver {
// Now 2 mount points are present /user, /usr // Now 2 mount points are present /user, /usr
compareLists(mountTable.getMountPoints("/"), compareLists(mountTable.getMountPoints("/"),
new String[] {"user", "usr"}); new String[] {"user", "usr", "readonly"});
// /tmp no longer exists, uses default namespace for mapping / // /tmp no longer exists, uses default namespace for mapping /
assertEquals("1", mountTable.getDestinationForPath("/tmp/testfile.txt") assertEquals("1", mountTable.getDestinationForPath("/tmp/testfile.txt")
@ -261,7 +277,7 @@ public class TestMountTableResolver {
// 3 mount points are present /tmp, /user, /usr // 3 mount points are present /tmp, /user, /usr
compareLists(mountTable.getMountPoints("/"), compareLists(mountTable.getMountPoints("/"),
new String[] {"user", "usr", "tmp"}); new String[] {"user", "usr", "tmp", "readonly"});
// /usr is virtual, uses namespace 1->/ // /usr is virtual, uses namespace 1->/
assertEquals("1", mountTable.getDestinationForPath("/usr/testfile.txt") assertEquals("1", mountTable.getDestinationForPath("/usr/testfile.txt")
@ -272,7 +288,7 @@ public class TestMountTableResolver {
// Verify the remove failed // Verify the remove failed
compareLists(mountTable.getMountPoints("/"), compareLists(mountTable.getMountPoints("/"),
new String[] {"user", "usr", "tmp"}); new String[] {"user", "usr", "tmp", "readonly"});
} }
@Test @Test
@ -304,7 +320,7 @@ public class TestMountTableResolver {
// Initial table loaded // Initial table loaded
testDestination(); testDestination();
assertEquals(8, mountTable.getMounts("/").size()); assertEquals(9, mountTable.getMounts("/").size());
// Replace table with /1 and /2 // Replace table with /1 and /2
List<MountTable> records = new ArrayList<>(); List<MountTable> records = new ArrayList<>();

View File

@ -143,6 +143,37 @@ public class TestRouterAdmin {
assertFalse(addResponse2.getStatus()); assertFalse(addResponse2.getStatus());
} }
@Test
public void testAddReadOnlyMountTable() throws IOException {
MountTable newEntry = MountTable.newInstance(
"/readonly", Collections.singletonMap("ns0", "/testdir"),
Time.now(), Time.now());
newEntry.setReadOnly(true);
RouterClient client = routerContext.getAdminClient();
MountTableManager mountTable = client.getMountTableManager();
// Existing mount table size
List<MountTable> records = getMountTableEntries(mountTable);
assertEquals(records.size(), mockMountTable.size());
// Add
AddMountTableEntryRequest addRequest =
AddMountTableEntryRequest.newInstance(newEntry);
AddMountTableEntryResponse addResponse =
mountTable.addMountTableEntry(addRequest);
assertTrue(addResponse.getStatus());
// New mount table size
List<MountTable> records2 = getMountTableEntries(mountTable);
assertEquals(records2.size(), mockMountTable.size() + 1);
// Check that we have the read only entry
MountTable record = getMountTableEntry("/readonly");
assertEquals("/readonly", record.getSourcePath());
assertTrue(record.isReadOnly());
}
@Test @Test
public void testRemoveMountTable() throws IOException { public void testRemoveMountTable() throws IOException {

View File

@ -0,0 +1,143 @@
/**
* 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.server.federation.router;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.Collections;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hdfs.server.federation.RouterConfigBuilder;
import org.apache.hadoop.hdfs.server.federation.RouterDFSCluster.NamenodeContext;
import org.apache.hadoop.hdfs.server.federation.RouterDFSCluster.RouterContext;
import org.apache.hadoop.hdfs.server.federation.StateStoreDFSCluster;
import org.apache.hadoop.hdfs.server.federation.resolver.MountTableManager;
import org.apache.hadoop.hdfs.server.federation.resolver.MountTableResolver;
import org.apache.hadoop.hdfs.server.federation.store.protocol.AddMountTableEntryRequest;
import org.apache.hadoop.hdfs.server.federation.store.protocol.AddMountTableEntryResponse;
import org.apache.hadoop.hdfs.server.federation.store.records.MountTable;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Test a router end-to-end including the MountTable.
*/
public class TestRouterMountTable {
private static StateStoreDFSCluster cluster;
private static NamenodeContext nnContext;
private static RouterContext routerContext;
private static MountTableResolver mountTable;
@BeforeClass
public static void globalSetUp() throws Exception {
// Build and start a federated cluster
cluster = new StateStoreDFSCluster(false, 1);
Configuration conf = new RouterConfigBuilder()
.stateStore()
.admin()
.rpc()
.build();
cluster.addRouterOverrides(conf);
cluster.startCluster();
cluster.startRouters();
cluster.waitClusterUp();
// Get the end points
nnContext = cluster.getRandomNamenode();
routerContext = cluster.getRandomRouter();
Router router = routerContext.getRouter();
mountTable = (MountTableResolver) router.getSubclusterResolver();
}
@AfterClass
public static void tearDown() {
if (cluster != null) {
cluster.stopRouter(routerContext);
cluster.shutdown();
cluster = null;
}
}
@Test
public void testReadOnly() throws Exception {
// Add a read only entry
MountTable readOnlyEntry = MountTable.newInstance(
"/readonly", Collections.singletonMap("ns0", "/testdir"));
readOnlyEntry.setReadOnly(true);
assertTrue(addMountTable(readOnlyEntry));
// Add a regular entry
MountTable regularEntry = MountTable.newInstance(
"/regular", Collections.singletonMap("ns0", "/testdir"));
assertTrue(addMountTable(regularEntry));
// Create a folder which should show in all locations
final FileSystem nnFs = nnContext.getFileSystem();
final FileSystem routerFs = routerContext.getFileSystem();
assertTrue(routerFs.mkdirs(new Path("/regular/newdir")));
FileStatus dirStatusNn =
nnFs.getFileStatus(new Path("/testdir/newdir"));
assertTrue(dirStatusNn.isDirectory());
FileStatus dirStatusRegular =
routerFs.getFileStatus(new Path("/regular/newdir"));
assertTrue(dirStatusRegular.isDirectory());
FileStatus dirStatusReadOnly =
routerFs.getFileStatus(new Path("/readonly/newdir"));
assertTrue(dirStatusReadOnly.isDirectory());
// It should fail writing into a read only path
try {
routerFs.mkdirs(new Path("/readonly/newdirfail"));
fail("We should not be able to write into a read only mount point");
} catch (IOException ioe) {
String msg = ioe.getMessage();
assertTrue(msg.startsWith(
"/readonly/newdirfail is in a read only mount point"));
}
}
/**
* Add a mount table entry to the mount table through the admin API.
* @param entry Mount table entry to add.
* @return If it was succesfully added.
* @throws IOException Problems adding entries.
*/
private boolean addMountTable(final MountTable entry) throws IOException {
RouterClient client = routerContext.getAdminClient();
MountTableManager mountTableManager = client.getMountTableManager();
AddMountTableEntryRequest addRequest =
AddMountTableEntryRequest.newInstance(entry);
AddMountTableEntryResponse addResponse =
mountTableManager.addMountTableEntry(addRequest);
// Reload the Router cache
mountTable.loadCache(true);
return addResponse.getStatus();
}
}