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