You are viewing a plain text version of this content. The canonical link for it is here.
Posted to common-commits@hadoop.apache.org by br...@apache.org on 2014/11/17 22:25:13 UTC
hadoop git commit: HDFS-7146. NFS ID/Group lookup requires SSSD
enumeration on the server. Contributed by Yongjun Zhang
Repository: hadoop
Updated Branches:
refs/heads/trunk 81c9d17af -> 351c5561c
HDFS-7146. NFS ID/Group lookup requires SSSD enumeration on the server. Contributed by Yongjun Zhang
Project: http://git-wip-us.apache.org/repos/asf/hadoop/repo
Commit: http://git-wip-us.apache.org/repos/asf/hadoop/commit/351c5561
Tree: http://git-wip-us.apache.org/repos/asf/hadoop/tree/351c5561
Diff: http://git-wip-us.apache.org/repos/asf/hadoop/diff/351c5561
Branch: refs/heads/trunk
Commit: 351c5561c2fd380ab7746ca4e91d7b838e61e03f
Parents: 81c9d17
Author: Brandon Li <br...@apache.org>
Authored: Mon Nov 17 13:16:43 2014 -0800
Committer: Brandon Li <br...@apache.org>
Committed: Mon Nov 17 13:23:53 2014 -0800
----------------------------------------------------------------------
.../hadoop/security/ShellBasedIdMapping.java | 344 +++++++++++++++++--
.../security/TestShellBasedIdMapping.java | 61 ++++
.../hadoop/hdfs/nfs/nfs3/RpcProgramNfs3.java | 3 +-
hadoop-hdfs-project/hadoop-hdfs/CHANGES.txt | 3 +
4 files changed, 377 insertions(+), 34 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/hadoop/blob/351c5561/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ShellBasedIdMapping.java
----------------------------------------------------------------------
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ShellBasedIdMapping.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ShellBasedIdMapping.java
index 0502c74..768294d 100644
--- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ShellBasedIdMapping.java
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/ShellBasedIdMapping.java
@@ -40,6 +40,23 @@ import com.google.common.collect.HashBiMap;
* A simple shell-based implementation of {@link IdMappingServiceProvider}
* Map id to user name or group name. It does update every 15 minutes. Only a
* single instance of this class is expected to be on the server.
+ *
+ * The maps are incrementally updated as described below:
+ * 1. Initialize the maps as empty.
+ * 2. Incrementally update the maps
+ * - When ShellBasedIdMapping is requested for user or group name given
+ * an ID, or for ID given a user or group name, do look up in the map
+ * first, if it doesn't exist, find the corresponding entry with shell
+ * command, and insert the entry to the maps.
+ * - When group ID is requested for a given group name, and if the
+ * group name is numerical, the full group map is loaded. Because we
+ * don't have a good way to find the entry for a numerical group name,
+ * loading the full map helps to get in all entries.
+ * 3. Periodically refresh the maps for both user and group, e.g,
+ * do step 1.
+ * Note: for testing purpose, step 1 may initial the maps with full mapping
+ * when using constructor
+ * {@link ShellBasedIdMapping#ShellBasedIdMapping(Configuration, boolean)}.
*/
public class ShellBasedIdMapping implements IdMappingServiceProvider {
@@ -55,6 +72,8 @@ public class ShellBasedIdMapping implements IdMappingServiceProvider {
static final String MAC_GET_ALL_GROUPS_CMD = "dscl . -list /Groups PrimaryGroupID";
private final File staticMappingFile;
+ private StaticMapping staticMapping = null;
+ private boolean constructFullMapAtInit = false;
// Used for parsing the static mapping file.
private static final Pattern EMPTY_LINE = Pattern.compile("^\\s*$");
@@ -69,9 +88,18 @@ public class ShellBasedIdMapping implements IdMappingServiceProvider {
private BiMap<Integer, String> gidNameMap = HashBiMap.create();
private long lastUpdateTime = 0; // Last time maps were updated
-
+
+ /*
+ * Constructor
+ * @param conf the configuration
+ * @param constructFullMapAtInit initialize the maps with full mapping when
+ * true, otherwise initialize the maps to empty. This parameter is
+ * intended for testing only, its default is false.
+ */
+ @VisibleForTesting
public ShellBasedIdMapping(Configuration conf,
- final String defaultStaticIdMappingFile) throws IOException {
+ boolean constructFullMapAtInit) throws IOException {
+ this.constructFullMapAtInit = constructFullMapAtInit;
long updateTime = conf.getLong(
IdMappingConstant.USERGROUPID_UPDATE_MILLIS_KEY,
IdMappingConstant.USERGROUPID_UPDATE_MILLIS_DEFAULT);
@@ -84,22 +112,45 @@ public class ShellBasedIdMapping implements IdMappingServiceProvider {
timeout = updateTime;
}
- String staticFilePath = conf.get(IdMappingConstant.STATIC_ID_MAPPING_FILE_KEY,
- defaultStaticIdMappingFile);
+ String staticFilePath =
+ conf.get(IdMappingConstant.STATIC_ID_MAPPING_FILE_KEY,
+ IdMappingConstant.STATIC_ID_MAPPING_FILE_DEFAULT);
staticMappingFile = new File(staticFilePath);
-
+
updateMaps();
}
+ /*
+ * Constructor
+ * initialize user and group maps to empty
+ * @param conf the configuration
+ */
public ShellBasedIdMapping(Configuration conf) throws IOException {
- this(conf, IdMappingConstant.STATIC_ID_MAPPING_FILE_DEFAULT);
+ this(conf, false);
}
@VisibleForTesting
public long getTimeout() {
return timeout;
}
-
+
+ @VisibleForTesting
+ public BiMap<Integer, String> getUidNameMap() {
+ return uidNameMap;
+ }
+
+ @VisibleForTesting
+ public BiMap<Integer, String> getGidNameMap() {
+ return gidNameMap;
+ }
+
+ @VisibleForTesting
+ synchronized public void clearNameMaps() {
+ uidNameMap.clear();
+ gidNameMap.clear();
+ lastUpdateTime = Time.monotonicNow();
+ }
+
synchronized private boolean isExpired() {
return Time.monotonicNow() - lastUpdateTime > timeout;
}
@@ -153,13 +204,15 @@ public class ShellBasedIdMapping implements IdMappingServiceProvider {
}
/**
- * Get the whole list of users and groups and save them in the maps.
+ * Get the list of users or groups returned by the specified command,
+ * and save them in the corresponding map.
* @throws IOException
*/
@VisibleForTesting
- public static void updateMapInternal(BiMap<Integer, String> map, String mapName,
- String command, String regex, Map<Integer, Integer> staticMapping)
- throws IOException {
+ public static boolean updateMapInternal(BiMap<Integer, String> map,
+ String mapName, String command, String regex,
+ Map<Integer, Integer> staticMapping) throws IOException {
+ boolean updated = false;
BufferedReader br = null;
try {
Process process = Runtime.getRuntime().exec(
@@ -194,8 +247,9 @@ public class ShellBasedIdMapping implements IdMappingServiceProvider {
continue;
}
map.put(key, value);
+ updated = true;
}
- LOG.info("Updated " + mapName + " map size: " + map.size());
+ LOG.debug("Updated " + mapName + " map size: " + map.size());
} catch (IOException e) {
LOG.error("Can't update " + mapName + " map");
@@ -209,20 +263,31 @@ public class ShellBasedIdMapping implements IdMappingServiceProvider {
}
}
}
+ return updated;
}
- synchronized public void updateMaps() throws IOException {
- BiMap<Integer, String> uMap = HashBiMap.create();
- BiMap<Integer, String> gMap = HashBiMap.create();
-
+ private boolean checkSupportedPlatform() {
if (!OS.startsWith("Linux") && !OS.startsWith("Mac")) {
LOG.error("Platform is not supported:" + OS
+ ". Can't update user map and group map and"
+ " 'nobody' will be used for any user and group.");
- return;
+ return false;
}
-
- StaticMapping staticMapping = new StaticMapping(
+ return true;
+ }
+
+ private static boolean isInteger(final String s) {
+ try {
+ Integer.parseInt(s);
+ } catch(NumberFormatException e) {
+ return false;
+ }
+ // only got here if we didn't return false
+ return true;
+ }
+
+ private void initStaticMapping() throws IOException {
+ staticMapping = new StaticMapping(
new HashMap<Integer, Integer>(), new HashMap<Integer, Integer>());
if (staticMappingFile.exists()) {
LOG.info("Using '" + staticMappingFile + "' for static UID/GID mapping...");
@@ -231,24 +296,218 @@ public class ShellBasedIdMapping implements IdMappingServiceProvider {
LOG.info("Not doing static UID/GID mapping because '" + staticMappingFile
+ "' does not exist.");
}
+ }
+ /*
+ * Reset the maps to empty.
+ * For testing code, a full map may be re-constructed here when the object
+ * was created with constructFullMapAtInit being set to true.
+ */
+ synchronized public void updateMaps() throws IOException {
+ if (!checkSupportedPlatform()) {
+ return;
+ }
+
+ if (constructFullMapAtInit) {
+ loadFullMaps();
+ } else {
+ clearNameMaps();
+ }
+ }
+
+ synchronized private void loadFullUserMap() throws IOException {
+ if (staticMapping == null) {
+ initStaticMapping();
+ }
+ BiMap<Integer, String> uMap = HashBiMap.create();
if (OS.startsWith("Mac")) {
updateMapInternal(uMap, "user", MAC_GET_ALL_USERS_CMD, "\\s+",
staticMapping.uidMapping);
- updateMapInternal(gMap, "group", MAC_GET_ALL_GROUPS_CMD, "\\s+",
- staticMapping.gidMapping);
} else {
updateMapInternal(uMap, "user", GET_ALL_USERS_CMD, ":",
staticMapping.uidMapping);
+ }
+ uidNameMap = uMap;
+ lastUpdateTime = Time.monotonicNow();
+ }
+
+ synchronized private void loadFullGroupMap() throws IOException {
+ if (staticMapping == null) {
+ initStaticMapping();
+ }
+ BiMap<Integer, String> gMap = HashBiMap.create();
+
+ if (OS.startsWith("Mac")) {
+ updateMapInternal(gMap, "group", MAC_GET_ALL_GROUPS_CMD, "\\s+",
+ staticMapping.gidMapping);
+ } else {
updateMapInternal(gMap, "group", GET_ALL_GROUPS_CMD, ":",
staticMapping.gidMapping);
}
-
- uidNameMap = uMap;
gidNameMap = gMap;
lastUpdateTime = Time.monotonicNow();
}
+
+ synchronized private void loadFullMaps() throws IOException {
+ initStaticMapping();
+ loadFullUserMap();
+ loadFullGroupMap();
+ }
+
+ // search for id with given name, return "<name>:<id>"
+ // return
+ // getent group <name> | cut -d: -f1,3
+ // OR
+ // id -u <name> | awk '{print "<name>:"$1 }'
+ //
+ private String getName2IdCmdLinux(final String name, final boolean isGrp) {
+ String cmd;
+ if (isGrp) {
+ cmd = "getent group " + name + " | cut -d: -f1,3";
+ } else {
+ cmd = "id -u " + name + " | awk '{print \"" + name + ":\"$1 }'";
+ }
+ return cmd;
+ }
+ // search for name with given id, return "<name>:<id>"
+ private String getId2NameCmdLinux(final int id, final boolean isGrp) {
+ String cmd = "getent ";
+ cmd += isGrp? "group " : "passwd ";
+ cmd += String.valueOf(id) + " | cut -d: -f1,3";
+ return cmd;
+ }
+
+ // "dscl . -read /Users/<name> | grep UniqueID" returns "UniqueId: <id>",
+ // "dscl . -read /Groups/<name> | grep PrimaryGroupID" returns "PrimaryGoupID: <id>"
+ // The following method returns a command that uses awk to process the result,
+ // of these commands, and returns "<name> <id>", to simulate one entry returned by
+ // MAC_GET_ALL_USERS_CMD or MAC_GET_ALL_GROUPS_CMD.
+ // Specificially, this method returns:
+ // id -u <name> | awk '{print "<name>:"$1 }'
+ // OR
+ // dscl . -read /Groups/<name> | grep PrimaryGroupID | awk '($1 == "PrimaryGroupID:") { print "<name> " $2 }'
+ //
+ private String getName2IdCmdMac(final String name, final boolean isGrp) {
+ String cmd;
+ if (isGrp) {
+ cmd = "dscl . -read /Groups/" + name;
+ cmd += " | grep PrimaryGroupID | awk '($1 == \"PrimaryGroupID:\") ";
+ cmd += "{ print \"" + name + " \" $2 }'";
+ } else {
+ cmd = "id -u " + name + " | awk '{print \"" + name + " \"$1 }'";
+ }
+ return cmd;
+ }
+
+ // "dscl . -search /Users UniqueID <id>" returns
+ // <name> UniqueID = (
+ // <id>
+ // )
+ // "dscl . -search /Groups PrimaryGroupID <id>" returns
+ // <name> PrimaryGroupID = (
+ // <id>
+ // )
+ // The following method returns a command that uses sed to process the
+ // the result and returns "<name> <id>" to simulate one entry returned
+ // by MAC_GET_ALL_USERS_CMD or MAC_GET_ALL_GROUPS_CMD.
+ // For certain negative id case like nfsnobody, the <id> is quoted as
+ // "<id>", added one sed section to remove the quote.
+ // Specifically, the method returns:
+ // dscl . -search /Users UniqueID <id> | sed 'N;s/\\n//g;N;s/\\n//g' | sed 's/UniqueID =//g' | sed 's/)//g' | sed 's/\"//g'
+ // OR
+ // dscl . -search /Groups PrimaryGroupID <id> | sed 'N;s/\\n//g;N;s/\\n//g' | sed 's/PrimaryGroupID =//g' | sed 's/)//g' | sed 's/\"//g'
+ //
+ private String getId2NameCmdMac(final int id, final boolean isGrp) {
+ String cmd = "dscl . -search /";
+ cmd += isGrp? "Groups PrimaryGroupID " : "Users UniqueID ";
+ cmd += String.valueOf(id);
+ cmd += " | sed 'N;s/\\n//g;N;s/\\n//g' | sed 's/";
+ cmd += isGrp? "PrimaryGroupID" : "UniqueID";
+ cmd += " = (//g' | sed 's/)//g' | sed 's/\\\"//g'";
+ return cmd;
+ }
+
+ synchronized private void updateMapIncr(final String name,
+ final boolean isGrp) throws IOException {
+ if (!checkSupportedPlatform()) {
+ return;
+ }
+ if (isInteger(name) && isGrp) {
+ loadFullGroupMap();
+ return;
+ }
+
+ boolean updated = false;
+ if (staticMapping == null) {
+ initStaticMapping();
+ }
+
+ if (OS.startsWith("Linux")) {
+ if (isGrp) {
+ updated = updateMapInternal(gidNameMap, "group",
+ getName2IdCmdLinux(name, true), ":",
+ staticMapping.gidMapping);
+ } else {
+ updated = updateMapInternal(uidNameMap, "user",
+ getName2IdCmdLinux(name, false), ":",
+ staticMapping.uidMapping);
+ }
+ } else {
+ // Mac
+ if (isGrp) {
+ updated = updateMapInternal(gidNameMap, "group",
+ getName2IdCmdMac(name, true), "\\s+",
+ staticMapping.gidMapping);
+ } else {
+ updated = updateMapInternal(uidNameMap, "user",
+ getName2IdCmdMac(name, false), "\\s+",
+ staticMapping.uidMapping);
+ }
+ }
+ if (updated) {
+ lastUpdateTime = Time.monotonicNow();
+ }
+ }
+
+ synchronized private void updateMapIncr(final int id,
+ final boolean isGrp) throws IOException {
+ if (!checkSupportedPlatform()) {
+ return;
+ }
+
+ boolean updated = false;
+ if (staticMapping == null) {
+ initStaticMapping();
+ }
+
+ if (OS.startsWith("Linux")) {
+ if (isGrp) {
+ updated = updateMapInternal(gidNameMap, "group",
+ getId2NameCmdLinux(id, true), ":",
+ staticMapping.gidMapping);
+ } else {
+ updated = updateMapInternal(uidNameMap, "user",
+ getId2NameCmdLinux(id, false), ":",
+ staticMapping.uidMapping);
+ }
+ } else {
+ // Mac
+ if (isGrp) {
+ updated = updateMapInternal(gidNameMap, "group",
+ getId2NameCmdMac(id, true), "\\s+",
+ staticMapping.gidMapping);
+ } else {
+ updated = updateMapInternal(uidNameMap, "user",
+ getId2NameCmdMac(id, false), "\\s+",
+ staticMapping.uidMapping);
+ }
+ }
+ if (updated) {
+ lastUpdateTime = Time.monotonicNow();
+ }
+ }
+
@SuppressWarnings("serial")
static final class PassThroughMap<K> extends HashMap<K, K> {
@@ -335,7 +594,11 @@ public class ShellBasedIdMapping implements IdMappingServiceProvider {
Integer id = uidNameMap.inverse().get(user);
if (id == null) {
- throw new IOException("User just deleted?:" + user);
+ updateMapIncr(user, false);
+ id = uidNameMap.inverse().get(user);
+ if (id == null) {
+ throw new IOException("User just deleted?:" + user);
+ }
}
return id.intValue();
}
@@ -345,8 +608,11 @@ public class ShellBasedIdMapping implements IdMappingServiceProvider {
Integer id = gidNameMap.inverse().get(group);
if (id == null) {
- throw new IOException("No such group:" + group);
-
+ updateMapIncr(group, true);
+ id = gidNameMap.inverse().get(group);
+ if (id == null) {
+ throw new IOException("No such group:" + group);
+ }
}
return id.intValue();
}
@@ -355,9 +621,16 @@ public class ShellBasedIdMapping implements IdMappingServiceProvider {
checkAndUpdateMaps();
String uname = uidNameMap.get(uid);
if (uname == null) {
- LOG.warn("Can't find user name for uid " + uid
- + ". Use default user name " + unknown);
- uname = unknown;
+ try {
+ updateMapIncr(uid, false);
+ } catch (Exception e) {
+ }
+ uname = uidNameMap.get(uid);
+ if (uname == null) {
+ LOG.warn("Can't find user name for uid " + uid
+ + ". Use default user name " + unknown);
+ uname = unknown;
+ }
}
return uname;
}
@@ -366,9 +639,16 @@ public class ShellBasedIdMapping implements IdMappingServiceProvider {
checkAndUpdateMaps();
String gname = gidNameMap.get(gid);
if (gname == null) {
- LOG.warn("Can't find group name for gid " + gid
- + ". Use default group name " + unknown);
- gname = unknown;
+ try {
+ updateMapIncr(gid, true);
+ } catch (Exception e) {
+ }
+ gname = gidNameMap.get(gid);
+ if (gname == null) {
+ LOG.warn("Can't find group name for gid " + gid
+ + ". Use default group name " + unknown);
+ gname = unknown;
+ }
}
return gname;
}
http://git-wip-us.apache.org/repos/asf/hadoop/blob/351c5561/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestShellBasedIdMapping.java
----------------------------------------------------------------------
diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestShellBasedIdMapping.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestShellBasedIdMapping.java
index 808c3fd..ec8ac1d 100644
--- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestShellBasedIdMapping.java
+++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestShellBasedIdMapping.java
@@ -219,4 +219,65 @@ public class TestShellBasedIdMapping {
assertEquals(iug.getTimeout(),
IdMappingConstant.USERGROUPID_UPDATE_MILLIS_DEFAULT * 2);
}
+
+ @Test
+ public void testUpdateMapIncr() throws IOException {
+ Configuration conf = new Configuration();
+ conf.setLong(IdMappingConstant.USERGROUPID_UPDATE_MILLIS_KEY, 600000);
+ ShellBasedIdMapping refIdMapping =
+ new ShellBasedIdMapping(conf, true);
+ ShellBasedIdMapping incrIdMapping = new ShellBasedIdMapping(conf);
+
+ // Command such as "getent passwd <userName>" will return empty string if
+ // <username> is numerical, remove them from the map for testing purpose.
+ BiMap<Integer, String> uidNameMap = refIdMapping.getUidNameMap();
+ BiMap<Integer, String> gidNameMap = refIdMapping.getGidNameMap();
+
+ // Force empty map, to see effect of incremental map update of calling
+ // getUserName()
+ incrIdMapping.clearNameMaps();
+ uidNameMap = refIdMapping.getUidNameMap();
+ for (BiMap.Entry<Integer, String> me : uidNameMap.entrySet()) {
+ Integer id = me.getKey();
+ String name = me.getValue();
+ String tname = incrIdMapping.getUserName(id, null);
+ assertEquals(name, tname);
+ }
+ assertEquals(uidNameMap.size(), incrIdMapping.getUidNameMap().size());
+
+ // Force empty map, to see effect of incremental map update of calling
+ // getUid()
+ incrIdMapping.clearNameMaps();
+ for (BiMap.Entry<Integer, String> me : uidNameMap.entrySet()) {
+ Integer id = me.getKey();
+ String name = me.getValue();
+ Integer tid = incrIdMapping.getUid(name);
+ assertEquals(id, tid);
+ }
+ assertEquals(uidNameMap.size(), incrIdMapping.getUidNameMap().size());
+
+ // Force empty map, to see effect of incremental map update of calling
+ // getGroupName()
+ incrIdMapping.clearNameMaps();
+ gidNameMap = refIdMapping.getGidNameMap();
+ for (BiMap.Entry<Integer, String> me : gidNameMap.entrySet()) {
+ Integer id = me.getKey();
+ String name = me.getValue();
+ String tname = incrIdMapping.getGroupName(id, null);
+ assertEquals(name, tname);
+ }
+ assertEquals(gidNameMap.size(), incrIdMapping.getGidNameMap().size());
+
+ // Force empty map, to see effect of incremental map update of calling
+ // getGid()
+ incrIdMapping.clearNameMaps();
+ gidNameMap = refIdMapping.getGidNameMap();
+ for (BiMap.Entry<Integer, String> me : gidNameMap.entrySet()) {
+ Integer id = me.getKey();
+ String name = me.getValue();
+ Integer tid = incrIdMapping.getGid(name);
+ assertEquals(id, tid);
+ }
+ assertEquals(gidNameMap.size(), incrIdMapping.getGidNameMap().size());
+ }
}
http://git-wip-us.apache.org/repos/asf/hadoop/blob/351c5561/hadoop-hdfs-project/hadoop-hdfs-nfs/src/main/java/org/apache/hadoop/hdfs/nfs/nfs3/RpcProgramNfs3.java
----------------------------------------------------------------------
diff --git a/hadoop-hdfs-project/hadoop-hdfs-nfs/src/main/java/org/apache/hadoop/hdfs/nfs/nfs3/RpcProgramNfs3.java b/hadoop-hdfs-project/hadoop-hdfs-nfs/src/main/java/org/apache/hadoop/hdfs/nfs/nfs3/RpcProgramNfs3.java
index d96babf..f86dbec 100644
--- a/hadoop-hdfs-project/hadoop-hdfs-nfs/src/main/java/org/apache/hadoop/hdfs/nfs/nfs3/RpcProgramNfs3.java
+++ b/hadoop-hdfs-project/hadoop-hdfs-nfs/src/main/java/org/apache/hadoop/hdfs/nfs/nfs3/RpcProgramNfs3.java
@@ -173,8 +173,7 @@ public class RpcProgramNfs3 extends RpcProgram implements Nfs3Interface {
this.config = config;
config.set(FsPermission.UMASK_LABEL, "000");
- iug = new ShellBasedIdMapping(config,
- IdMappingConstant.STATIC_ID_MAPPING_FILE_DEFAULT);
+ iug = new ShellBasedIdMapping(config);
aixCompatMode = config.getBoolean(
NfsConfigKeys.AIX_COMPAT_MODE_KEY,
http://git-wip-us.apache.org/repos/asf/hadoop/blob/351c5561/hadoop-hdfs-project/hadoop-hdfs/CHANGES.txt
----------------------------------------------------------------------
diff --git a/hadoop-hdfs-project/hadoop-hdfs/CHANGES.txt b/hadoop-hdfs-project/hadoop-hdfs/CHANGES.txt
index dd2a2e0..8cbff7a 100644
--- a/hadoop-hdfs-project/hadoop-hdfs/CHANGES.txt
+++ b/hadoop-hdfs-project/hadoop-hdfs/CHANGES.txt
@@ -438,6 +438,9 @@ Release 2.7.0 - UNRELEASED
HDFS-7399. Lack of synchronization in
DFSOutputStream#Packet#getLastByteOffsetBlock() (vinayakumarb)
+ HDFS-7146. NFS ID/Group lookup requires SSSD enumeration on the server
+ (Yongjun Zhang via brandonli)
+
Release 2.6.0 - 2014-11-18
INCOMPATIBLE CHANGES