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