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 */
018package org.apache.hadoop.hbase.zookeeper;
019
020import java.io.IOException;
021import java.util.List;
022import java.util.Map.Entry;
023import java.util.Properties;
024import org.apache.commons.validator.routines.InetAddressValidator;
025import org.apache.hadoop.conf.Configuration;
026import org.apache.hadoop.hbase.HConstants;
027import org.apache.hadoop.util.StringUtils;
028import org.apache.yetus.audience.InterfaceAudience;
029
030import org.apache.hbase.thirdparty.com.google.common.base.Splitter;
031
032/**
033 * Utility methods for reading, and building the ZooKeeper configuration. The order and priority for
034 * reading the config are as follows: (1). Property with "hbase.zookeeper.property." prefix from
035 * HBase XML (2). other zookeeper related properties in HBASE XML
036 */
037@InterfaceAudience.Private
038public final class ZKConfig {
039
040  private static final String VARIABLE_START = "${";
041  private static final String ZOOKEEPER_JAVA_PROPERTY_PREFIX = "zookeeper.";
042
043  private ZKConfig() {
044  }
045
046  /**
047   * Make a Properties object holding ZooKeeper config. Parses the corresponding config options from
048   * the HBase XML configs and generates the appropriate ZooKeeper properties.
049   * @param conf Configuration to read from.
050   * @return Properties holding mappings representing ZooKeeper config file.
051   */
052  public static Properties makeZKProps(Configuration conf) {
053    return makeZKPropsFromHbaseConfig(conf);
054  }
055
056  /**
057   * Directly map all the hbase.zookeeper.property.KEY properties. Synchronize on conf so no loading
058   * of configs while we iterate
059   */
060  private static Properties extractZKPropsFromHBaseConfig(final Configuration conf) {
061    Properties zkProperties = new Properties();
062
063    synchronized (conf) {
064      for (Entry<String, String> entry : conf) {
065        String key = entry.getKey();
066        if (key.startsWith(HConstants.ZK_CFG_PROPERTY_PREFIX)) {
067          String zkKey = key.substring(HConstants.ZK_CFG_PROPERTY_PREFIX_LEN);
068          String value = entry.getValue();
069          // If the value has variables substitutions, need to do a get.
070          if (value.contains(VARIABLE_START)) {
071            value = conf.get(key);
072          }
073          zkProperties.setProperty(zkKey, value);
074        }
075      }
076    }
077
078    return zkProperties;
079  }
080
081  /**
082   * Make a Properties object holding ZooKeeper config. Parses the corresponding config options from
083   * the HBase XML configs and generates the appropriate ZooKeeper properties.
084   * @param conf Configuration to read from.
085   * @return Properties holding mappings representing ZooKeeper config file.
086   */
087  private static Properties makeZKPropsFromHbaseConfig(Configuration conf) {
088    Properties zkProperties = extractZKPropsFromHBaseConfig(conf);
089
090    // If clientPort is not set, assign the default.
091    if (zkProperties.getProperty(HConstants.CLIENT_PORT_STR) == null) {
092      zkProperties.put(HConstants.CLIENT_PORT_STR, HConstants.DEFAULT_ZOOKEEPER_CLIENT_PORT);
093    }
094
095    // Create the server.X properties.
096    int peerPort = conf.getInt("hbase.zookeeper.peerport", 2888);
097    int leaderPort = conf.getInt("hbase.zookeeper.leaderport", 3888);
098
099    final String[] serverHosts = conf.getStrings(HConstants.ZOOKEEPER_QUORUM, HConstants.LOCALHOST);
100    String serverHost;
101    String address;
102    String key;
103    for (int i = 0; i < serverHosts.length; ++i) {
104      if (serverHosts[i].contains(":")) {
105        serverHost = serverHosts[i].substring(0, serverHosts[i].indexOf(':'));
106      } else {
107        serverHost = serverHosts[i];
108      }
109      address = serverHost + ":" + peerPort + ":" + leaderPort;
110      key = "server." + i;
111      zkProperties.put(key, address);
112    }
113
114    return zkProperties;
115  }
116
117  /**
118   * Return the ZK Quorum servers string given the specified configuration
119   * @return Quorum servers String
120   */
121  private static String getZKQuorumServersStringFromHbaseConfig(Configuration conf) {
122    String defaultClientPort = Integer.toString(
123      conf.getInt(HConstants.ZOOKEEPER_CLIENT_PORT, HConstants.DEFAULT_ZOOKEEPER_CLIENT_PORT));
124
125    // Build the ZK quorum server string with "server:clientport" list, separated by ','
126    final String[] serverHosts = conf.getStrings(HConstants.ZOOKEEPER_QUORUM, HConstants.LOCALHOST);
127    return buildZKQuorumServerString(serverHosts, defaultClientPort);
128  }
129
130  /**
131   * Return the ZK Quorum servers string given the specified configuration.
132   * @return Quorum servers
133   */
134  public static String getZKQuorumServersString(Configuration conf) {
135    setZooKeeperClientSystemProperties(HConstants.ZK_CFG_PROPERTY_PREFIX, conf);
136    return getZKQuorumServersStringFromHbaseConfig(conf);
137  }
138
139  /**
140   * Build the ZK quorum server string with "server:clientport" list, separated by ','
141   * @param serverHosts a list of servers for ZK quorum
142   * @param clientPort  the default client port
143   * @return the string for a list of "server:port" separated by ","
144   */
145  public static String buildZKQuorumServerString(String[] serverHosts, String clientPort) {
146    StringBuilder quorumStringBuilder = new StringBuilder();
147    String serverHost;
148    InetAddressValidator validator = new InetAddressValidator();
149    for (int i = 0; i < serverHosts.length; ++i) {
150      if (serverHosts[i].startsWith("[")) {
151        int index = serverHosts[i].indexOf("]");
152        if (index < 0) {
153          throw new IllegalArgumentException(
154            serverHosts[i] + " starts with '[' but has no matching ']:'");
155        }
156        if (index + 2 == serverHosts[i].length()) {
157          throw new IllegalArgumentException(serverHosts[i] + " doesn't have a port after colon");
158        }
159        // check the IPv6 address e.g. [2001:db8::1]
160        String serverHostWithoutBracket = serverHosts[i].substring(1, index);
161        if (!validator.isValidInet6Address(serverHostWithoutBracket)) {
162          throw new IllegalArgumentException(serverHosts[i] + " is not a valid IPv6 address");
163        }
164        serverHost = serverHosts[i];
165        if ((index + 1 == serverHosts[i].length())) {
166          serverHost = serverHosts[i] + ":" + clientPort;
167        }
168      } else {
169        if (serverHosts[i].contains(":")) {
170          serverHost = serverHosts[i]; // just use the port specified from the input
171        } else {
172          serverHost = serverHosts[i] + ":" + clientPort;
173        }
174      }
175
176      if (i > 0) {
177        quorumStringBuilder.append(',');
178      }
179      quorumStringBuilder.append(serverHost);
180    }
181    return quorumStringBuilder.toString();
182  }
183
184  /**
185   * Verifies that the given key matches the expected format for a ZooKeeper cluster key. The Quorum
186   * for the ZK cluster can have one the following formats (see examples below):
187   * <ol>
188   * <li>s1,s2,s3 (no client port in the list, the client port could be obtained from
189   * clientPort)</li>
190   * <li>s1:p1,s2:p2,s3:p3 (with client port, which could be same or different for each server, in
191   * this case, the clientPort would be ignored)</li>
192   * <li>s1:p1,s2,s3:p3 (mix of (1) and (2) - if port is not specified in a server, it would use the
193   * clientPort; otherwise, it would use the specified port)</li>
194   * </ol>
195   * @param key the cluster key to validate
196   * @throws IOException if the key could not be parsed
197   */
198  public static void validateClusterKey(String key) throws IOException {
199    transformClusterKey(key);
200  }
201
202  /**
203   * Separate the given key into the three configurations it should contain: hbase.zookeeper.quorum,
204   * hbase.zookeeper.client.port and zookeeper.znode.parent
205   * @return the three configuration in the described order
206   */
207  public static ZKClusterKey transformClusterKey(String key) throws IOException {
208    List<String> parts = Splitter.on(':').splitToList(key);
209    String[] partsArray = parts.toArray(new String[parts.size()]);
210
211    if (partsArray.length == 3) {
212      if (!partsArray[2].matches("/.*[^/]")) {
213        throw new IOException("Cluster key passed " + key + " is invalid, the format should be:"
214          + HConstants.ZOOKEEPER_QUORUM + ":" + HConstants.ZOOKEEPER_CLIENT_PORT + ":"
215          + HConstants.ZOOKEEPER_ZNODE_PARENT);
216      }
217      return new ZKClusterKey(partsArray[0], Integer.parseInt(partsArray[1]), partsArray[2]);
218    }
219
220    if (partsArray.length > 3) {
221      // The quorum could contain client port in server:clientport format, try to transform more.
222      String zNodeParent = partsArray[partsArray.length - 1];
223      if (!zNodeParent.matches("/.*[^/]")) {
224        throw new IOException("Cluster key passed " + key + " is invalid, the format should be:"
225          + HConstants.ZOOKEEPER_QUORUM + ":" + HConstants.ZOOKEEPER_CLIENT_PORT + ":"
226          + HConstants.ZOOKEEPER_ZNODE_PARENT);
227      }
228
229      String clientPort = partsArray[partsArray.length - 2];
230
231      // The first part length is the total length minus the lengths of other parts and minus 2 ":"
232      int endQuorumIndex = key.length() - zNodeParent.length() - clientPort.length() - 2;
233      String quorumStringInput = key.substring(0, endQuorumIndex);
234      String[] serverHosts = quorumStringInput.split(",");
235
236      // The common case is that every server has its own client port specified - this means
237      // that (total parts - the ZNodeParent part - the ClientPort part) is equal to
238      // (the number of "," + 1) - "+ 1" because the last server has no ",".
239      if ((partsArray.length - 2) == (serverHosts.length + 1)) {
240        return new ZKClusterKey(quorumStringInput, Integer.parseInt(clientPort), zNodeParent);
241      }
242
243      // For the uncommon case that some servers has no port specified, we need to build the
244      // server:clientport list using default client port for servers without specified port.
245      return new ZKClusterKey(buildZKQuorumServerString(serverHosts, clientPort),
246        Integer.parseInt(clientPort), zNodeParent);
247    }
248
249    throw new IOException("Cluster key passed " + key + " is invalid, the format should be:"
250      + HConstants.ZOOKEEPER_QUORUM + ":" + HConstants.ZOOKEEPER_CLIENT_PORT + ":"
251      + HConstants.ZOOKEEPER_ZNODE_PARENT);
252  }
253
254  /**
255   * Get the key to the ZK ensemble for this configuration without adding a name at the end
256   * @param conf Configuration to use to build the key
257   * @return ensemble key without a name
258   */
259  public static String getZooKeeperClusterKey(Configuration conf) {
260    return getZooKeeperClusterKey(conf, null);
261  }
262
263  /**
264   * Get the key to the ZK ensemble for this configuration and append a name at the end
265   * @param conf Configuration to use to build the key
266   * @param name Name that should be appended at the end if not empty or null
267   * @return ensemble key with a name (if any)
268   */
269  public static String getZooKeeperClusterKey(Configuration conf, String name) {
270    String ensemble = conf.get(HConstants.ZOOKEEPER_QUORUM).replaceAll("[\\t\\n\\x0B\\f\\r]", "");
271    StringBuilder builder = new StringBuilder(ensemble);
272    builder.append(":");
273    builder.append(conf.get(HConstants.ZOOKEEPER_CLIENT_PORT));
274    builder.append(":");
275    builder.append(conf.get(HConstants.ZOOKEEPER_ZNODE_PARENT));
276    if (name != null && !name.isEmpty()) {
277      builder.append(",");
278      builder.append(name);
279    }
280    return builder.toString();
281  }
282
283  /**
284   * Standardize the ZK quorum string: make it a "server:clientport" list, separated by ','
285   * @param quorumStringInput a string contains a list of servers for ZK quorum
286   * @param clientPort        the default client port
287   * @return the string for a list of "server:port" separated by ","
288   */
289  public static String standardizeZKQuorumServerString(String quorumStringInput,
290    String clientPort) {
291    String[] serverHosts = quorumStringInput.split(",");
292    return buildZKQuorumServerString(serverHosts, clientPort);
293  }
294
295  // The Quorum for the ZK cluster can have one the following format (see examples below):
296  // (1). s1,s2,s3 (no client port in the list, the client port could be obtained from clientPort)
297  // (2). s1:p1,s2:p2,s3:p3 (with client port, which could be same or different for each server,
298  // in this case, the clientPort would be ignored)
299  // (3). s1:p1,s2,s3:p3 (mix of (1) and (2) - if port is not specified in a server, it would use
300  // the clientPort; otherwise, it would use the specified port)
301  public static class ZKClusterKey {
302    private String quorumString;
303    private int clientPort;
304    private String znodeParent;
305
306    ZKClusterKey(String quorumString, int clientPort, String znodeParent) {
307      this.quorumString = quorumString;
308      this.clientPort = clientPort;
309      this.znodeParent = znodeParent;
310    }
311
312    public String getQuorumString() {
313      return quorumString;
314    }
315
316    public int getClientPort() {
317      return clientPort;
318    }
319
320    public String getZnodeParent() {
321      return znodeParent;
322    }
323  }
324
325  /**
326   * Get the client ZK Quorum servers string
327   * @param conf the configuration to read
328   * @return Client quorum servers, or null if not specified
329   */
330  public static String getClientZKQuorumServersString(Configuration conf) {
331    setZooKeeperClientSystemProperties(HConstants.ZK_CFG_PROPERTY_PREFIX, conf);
332    String clientQuromServers = conf.get(HConstants.CLIENT_ZOOKEEPER_QUORUM);
333    if (clientQuromServers == null) {
334      return null;
335    }
336    int defaultClientPort =
337      conf.getInt(HConstants.ZOOKEEPER_CLIENT_PORT, HConstants.DEFAULT_ZOOKEEPER_CLIENT_PORT);
338    String clientZkClientPort =
339      Integer.toString(conf.getInt(HConstants.CLIENT_ZOOKEEPER_CLIENT_PORT, defaultClientPort));
340    // Build the ZK quorum server string with "server:clientport" list, separated by ','
341    final String[] serverHosts = StringUtils.getStrings(clientQuromServers);
342    return buildZKQuorumServerString(serverHosts, clientZkClientPort);
343  }
344
345  private static void setZooKeeperClientSystemProperties(String prefix, Configuration conf) {
346    Properties zkProperties = extractZKPropsFromHBaseConfig(conf);
347    for (Entry<Object, Object> entry : zkProperties.entrySet()) {
348      String key = entry.getKey().toString().trim();
349      String value = entry.getValue().toString().trim();
350      if (System.getProperty(ZOOKEEPER_JAVA_PROPERTY_PREFIX + key) == null) {
351        System.setProperty(ZOOKEEPER_JAVA_PROPERTY_PREFIX + key, value);
352      }
353    }
354  }
355}