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