001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    package org.apache.hadoop.hdfs;
019    
020    import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_CLIENT_FAILOVER_PROXY_PROVIDER_KEY_PREFIX;
021    import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_HA_NAMENODE_ID_KEY;
022    import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_NAMENODE_RPC_ADDRESS_KEY;
023    import static org.apache.hadoop.hdfs.DFSConfigKeys.DFS_NAMENODE_SHARED_EDITS_DIR_KEY;
024    import static org.apache.hadoop.hdfs.protocol.HdfsConstants.HA_DT_SERVICE_PREFIX;
025    
026    import java.io.IOException;
027    import java.net.InetSocketAddress;
028    import java.net.URI;
029    import java.util.ArrayList;
030    import java.util.Collection;
031    import java.util.List;
032    import java.util.Map;
033    
034    import org.apache.commons.logging.Log;
035    import org.apache.commons.logging.LogFactory;
036    import org.apache.hadoop.HadoopIllegalArgumentException;
037    import org.apache.hadoop.conf.Configuration;
038    import org.apache.hadoop.fs.FileSystem;
039    import org.apache.hadoop.fs.Path;
040    import org.apache.hadoop.hdfs.protocol.ClientProtocol;
041    import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier;
042    import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenSelector;
043    import org.apache.hadoop.hdfs.server.namenode.NameNode;
044    import org.apache.hadoop.io.Text;
045    import org.apache.hadoop.ipc.RPC;
046    import org.apache.hadoop.ipc.RemoteException;
047    import org.apache.hadoop.ipc.StandbyException;
048    import org.apache.hadoop.security.SecurityUtil;
049    import org.apache.hadoop.security.UserGroupInformation;
050    import org.apache.hadoop.security.token.Token;
051    
052    import com.google.common.base.Joiner;
053    import com.google.common.base.Preconditions;
054    import com.google.common.collect.Lists;
055    
056    public class HAUtil {
057      
058      private static final Log LOG = 
059        LogFactory.getLog(HAUtil.class);
060      
061      private static final DelegationTokenSelector tokenSelector =
062          new DelegationTokenSelector();
063    
064      private HAUtil() { /* Hidden constructor */ }
065    
066      /**
067       * Returns true if HA for namenode is configured for the given nameservice
068       * 
069       * @param conf Configuration
070       * @param nsId nameservice, or null if no federated NS is configured
071       * @return true if HA is configured in the configuration; else false.
072       */
073      public static boolean isHAEnabled(Configuration conf, String nsId) {
074        Map<String, Map<String, InetSocketAddress>> addresses =
075          DFSUtil.getHaNnRpcAddresses(conf);
076        if (addresses == null) return false;
077        Map<String, InetSocketAddress> nnMap = addresses.get(nsId);
078        return nnMap != null && nnMap.size() > 1;
079      }
080    
081      /**
082       * Returns true if HA is using a shared edits directory.
083       *
084       * @param conf Configuration
085       * @return true if HA config is using a shared edits dir, false otherwise.
086       */
087      public static boolean usesSharedEditsDir(Configuration conf) {
088        return null != conf.get(DFS_NAMENODE_SHARED_EDITS_DIR_KEY);
089      }
090    
091      /**
092       * Get the namenode Id by matching the {@code addressKey}
093       * with the the address of the local node.
094       * 
095       * If {@link DFSConfigKeys#DFS_HA_NAMENODE_ID_KEY} is not specifically
096       * configured, this method determines the namenode Id by matching the local
097       * node's address with the configured addresses. When a match is found, it
098       * returns the namenode Id from the corresponding configuration key.
099       * 
100       * @param conf Configuration
101       * @return namenode Id on success, null on failure.
102       * @throws HadoopIllegalArgumentException on error
103       */
104      public static String getNameNodeId(Configuration conf, String nsId) {
105        String namenodeId = conf.getTrimmed(DFS_HA_NAMENODE_ID_KEY);
106        if (namenodeId != null) {
107          return namenodeId;
108        }
109        
110        String suffixes[] = DFSUtil.getSuffixIDs(conf, DFS_NAMENODE_RPC_ADDRESS_KEY,
111            nsId, null, DFSUtil.LOCAL_ADDRESS_MATCHER);
112        if (suffixes == null) {
113          String msg = "Configuration " + DFS_NAMENODE_RPC_ADDRESS_KEY + 
114              " must be suffixed with nameservice and namenode ID for HA " +
115              "configuration.";
116          throw new HadoopIllegalArgumentException(msg);
117        }
118        
119        return suffixes[1];
120      }
121    
122      /**
123       * Similar to
124       * {@link DFSUtil#getNameServiceIdFromAddress(Configuration, 
125       * InetSocketAddress, String...)}
126       */
127      public static String getNameNodeIdFromAddress(final Configuration conf, 
128          final InetSocketAddress address, String... keys) {
129        // Configuration with a single namenode and no nameserviceId
130        String[] ids = DFSUtil.getSuffixIDs(conf, address, keys);
131        if (ids != null && ids.length > 1) {
132          return ids[1];
133        }
134        return null;
135      }
136      
137      /**
138       * Get the NN ID of the other node in an HA setup.
139       * 
140       * @param conf the configuration of this node
141       * @return the NN ID of the other node in this nameservice
142       */
143      public static String getNameNodeIdOfOtherNode(Configuration conf, String nsId) {
144        Preconditions.checkArgument(nsId != null,
145            "Could not determine namespace id. Please ensure that this " +
146            "machine is one of the machines listed as a NN RPC address, " +
147            "or configure " + DFSConfigKeys.DFS_NAMESERVICE_ID);
148        
149        Collection<String> nnIds = DFSUtil.getNameNodeIds(conf, nsId);
150        String myNNId = conf.get(DFSConfigKeys.DFS_HA_NAMENODE_ID_KEY);
151        Preconditions.checkArgument(nnIds != null,
152            "Could not determine namenode ids in namespace '%s'. " +
153            "Please configure " +
154            DFSUtil.addKeySuffixes(DFSConfigKeys.DFS_HA_NAMENODES_KEY_PREFIX,
155                nsId),
156            nsId);
157        Preconditions.checkArgument(nnIds.size() == 2,
158            "Expected exactly 2 NameNodes in namespace '%s'. " +
159            "Instead, got only %s (NN ids were '%s'",
160            nsId, nnIds.size(), Joiner.on("','").join(nnIds));
161        Preconditions.checkState(myNNId != null && !myNNId.isEmpty(),
162            "Could not determine own NN ID in namespace '%s'. Please " +
163            "ensure that this node is one of the machines listed as an " +
164            "NN RPC address, or configure " + DFSConfigKeys.DFS_HA_NAMENODE_ID_KEY,
165            nsId);
166    
167        ArrayList<String> nnSet = Lists.newArrayList(nnIds);
168        nnSet.remove(myNNId);
169        assert nnSet.size() == 1;
170        return nnSet.get(0);
171      }
172    
173      /**
174       * Given the configuration for this node, return a Configuration object for
175       * the other node in an HA setup.
176       * 
177       * @param myConf the configuration of this node
178       * @return the configuration of the other node in an HA setup
179       */
180      public static Configuration getConfForOtherNode(
181          Configuration myConf) {
182        
183        String nsId = DFSUtil.getNamenodeNameServiceId(myConf);
184        String otherNn = getNameNodeIdOfOtherNode(myConf, nsId);
185        
186        // Look up the address of the active NN.
187        Configuration confForOtherNode = new Configuration(myConf);
188        NameNode.initializeGenericKeys(confForOtherNode, nsId, otherNn);
189        return confForOtherNode;
190      }
191    
192      /**
193       * This is used only by tests at the moment.
194       * @return true if the NN should allow read operations while in standby mode.
195       */
196      public static boolean shouldAllowStandbyReads(Configuration conf) {
197        return conf.getBoolean("dfs.ha.allow.stale.reads", false);
198      }
199      
200      public static void setAllowStandbyReads(Configuration conf, boolean val) {
201        conf.setBoolean("dfs.ha.allow.stale.reads", val);
202      }
203     
204      /**
205       * @return true if the given nameNodeUri appears to be a logical URI.
206       * This is the case if there is a failover proxy provider configured
207       * for it in the given configuration.
208       */
209      public static boolean isLogicalUri(
210          Configuration conf, URI nameNodeUri) {
211        String host = nameNodeUri.getHost();
212        String configKey = DFS_CLIENT_FAILOVER_PROXY_PROVIDER_KEY_PREFIX + "."
213            + host;
214        return conf.get(configKey) != null;
215      }
216    
217      /**
218       * Parse the file system URI out of the provided token.
219       */
220      public static URI getServiceUriFromToken(final String scheme,
221                                               Token<?> token) {
222        String tokStr = token.getService().toString();
223    
224        if (tokStr.startsWith(HA_DT_SERVICE_PREFIX)) {
225          tokStr = tokStr.replaceFirst(HA_DT_SERVICE_PREFIX, "");
226        }
227        return URI.create(scheme + "://" + tokStr);
228      }
229      
230      /**
231       * Get the service name used in the delegation token for the given logical
232       * HA service.
233       * @param uri the logical URI of the cluster
234       * @return the service name
235       */
236      public static Text buildTokenServiceForLogicalUri(URI uri) {
237        return new Text(HA_DT_SERVICE_PREFIX + uri.getHost());
238      }
239      
240      /**
241       * @return true if this token corresponds to a logical nameservice
242       * rather than a specific namenode.
243       */
244      public static boolean isTokenForLogicalUri(Token<?> token) {
245        return token.getService().toString().startsWith(HA_DT_SERVICE_PREFIX);
246      }
247      
248      /**
249       * Locate a delegation token associated with the given HA cluster URI, and if
250       * one is found, clone it to also represent the underlying namenode address.
251       * @param ugi the UGI to modify
252       * @param haUri the logical URI for the cluster
253       * @param nnAddrs collection of NNs in the cluster to which the token
254       * applies
255       */
256      public static void cloneDelegationTokenForLogicalUri(
257          UserGroupInformation ugi, URI haUri,
258          Collection<InetSocketAddress> nnAddrs) {
259        Text haService = HAUtil.buildTokenServiceForLogicalUri(haUri);
260        Token<DelegationTokenIdentifier> haToken =
261            tokenSelector.selectToken(haService, ugi.getTokens());
262        if (haToken != null) {
263          for (InetSocketAddress singleNNAddr : nnAddrs) {
264            // this is a minor hack to prevent physical HA tokens from being
265            // exposed to the user via UGI.getCredentials(), otherwise these
266            // cloned tokens may be inadvertently propagated to jobs
267            Token<DelegationTokenIdentifier> specificToken =
268                new Token.PrivateToken<DelegationTokenIdentifier>(haToken);
269            SecurityUtil.setTokenService(specificToken, singleNNAddr);
270            Text alias =
271                new Text(HA_DT_SERVICE_PREFIX + "//" + specificToken.getService());
272            ugi.addToken(alias, specificToken);
273            LOG.debug("Mapped HA service delegation token for logical URI " +
274                haUri + " to namenode " + singleNNAddr);
275          }
276        } else {
277          LOG.debug("No HA service delegation token found for logical URI " +
278              haUri);
279        }
280      }
281    
282      /**
283       * Get the internet address of the currently-active NN. This should rarely be
284       * used, since callers of this method who connect directly to the NN using the
285       * resulting InetSocketAddress will not be able to connect to the active NN if
286       * a failover were to occur after this method has been called.
287       * 
288       * @param fs the file system to get the active address of.
289       * @return the internet address of the currently-active NN.
290       * @throws IOException if an error occurs while resolving the active NN.
291       */
292      public static InetSocketAddress getAddressOfActive(FileSystem fs)
293          throws IOException {
294        if (!(fs instanceof DistributedFileSystem)) {
295          throw new IllegalArgumentException("FileSystem " + fs + " is not a DFS.");
296        }
297        // force client address resolution.
298        fs.exists(new Path("/"));
299        DistributedFileSystem dfs = (DistributedFileSystem) fs;
300        DFSClient dfsClient = dfs.getClient();
301        return RPC.getServerAddress(dfsClient.getNamenode());
302      }
303      
304      /**
305       * Get an RPC proxy for each NN in an HA nameservice. Used when a given RPC
306       * call should be made on every NN in an HA nameservice, not just the active.
307       * 
308       * @param conf configuration
309       * @param nsId the nameservice to get all of the proxies for.
310       * @return a list of RPC proxies for each NN in the nameservice.
311       * @throws IOException in the event of error.
312       */
313      public static List<ClientProtocol> getProxiesForAllNameNodesInNameservice(
314          Configuration conf, String nsId) throws IOException {
315        Map<String, InetSocketAddress> nnAddresses =
316            DFSUtil.getRpcAddressesForNameserviceId(conf, nsId, null);
317        
318        List<ClientProtocol> namenodes = new ArrayList<ClientProtocol>();
319        for (InetSocketAddress nnAddress : nnAddresses.values()) {
320          NameNodeProxies.ProxyAndInfo<ClientProtocol> proxyInfo = null;
321          proxyInfo = NameNodeProxies.createNonHAProxy(conf,
322              nnAddress, ClientProtocol.class,
323              UserGroupInformation.getCurrentUser(), false);
324          namenodes.add(proxyInfo.getProxy());
325        }
326        return namenodes;
327      }
328      
329      /**
330       * Used to ensure that at least one of the given HA NNs is currently in the
331       * active state..
332       * 
333       * @param namenodes list of RPC proxies for each NN to check.
334       * @return true if at least one NN is active, false if all are in the standby state.
335       * @throws IOException in the event of error.
336       */
337      public static boolean isAtLeastOneActive(List<ClientProtocol> namenodes)
338          throws IOException {
339        for (ClientProtocol namenode : namenodes) {
340          try {
341            namenode.getFileInfo("/");
342            return true;
343          } catch (RemoteException re) {
344            IOException cause = re.unwrapRemoteException();
345            if (cause instanceof StandbyException) {
346              // This is expected to happen for a standby NN.
347            } else {
348              throw re;
349            }
350          }
351        }
352        return false;
353      }
354    }