001/**
002 *
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *     http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019package org.apache.hadoop.hbase.zookeeper;
020
021import java.io.IOException;
022import java.util.Map.Entry;
023import java.util.Properties;
024
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.annotations.VisibleForTesting;
031
032/**
033 * Utility methods for reading, and building the ZooKeeper configuration.
034 *
035 * The order and priority for reading the config are as follows:
036 * (1). Property with "hbase.zookeeper.property." prefix from HBase XML
037 * (2). other zookeeper related properties in HBASE XML
038 */
039@InterfaceAudience.Private
040public final class ZKConfig {
041
042  private static final String VARIABLE_START = "${";
043
044  private ZKConfig() {
045  }
046
047  /**
048   * Make a Properties object holding ZooKeeper config.
049   * Parses the corresponding config options from the HBase XML configs
050   * and generates the appropriate ZooKeeper properties.
051   * @param conf Configuration to read from.
052   * @return Properties holding mappings representing ZooKeeper config file.
053   */
054  public static Properties makeZKProps(Configuration conf) {
055    return makeZKPropsFromHbaseConfig(conf);
056  }
057
058  /**
059   * Make a Properties object holding ZooKeeper config.
060   * Parses the corresponding config options from the HBase XML configs
061   * and generates the appropriate ZooKeeper properties.
062   *
063   * @param conf Configuration to read from.
064   * @return Properties holding mappings representing ZooKeeper config file.
065   */
066  private static Properties makeZKPropsFromHbaseConfig(Configuration conf) {
067    Properties zkProperties = new Properties();
068
069    // Directly map all of the hbase.zookeeper.property.KEY properties.
070    // Synchronize on conf so no loading of configs while we iterate
071    synchronized (conf) {
072      for (Entry<String, String> entry : conf) {
073        String key = entry.getKey();
074        if (key.startsWith(HConstants.ZK_CFG_PROPERTY_PREFIX)) {
075          String zkKey = key.substring(HConstants.ZK_CFG_PROPERTY_PREFIX_LEN);
076          String value = entry.getValue();
077          // If the value has variables substitutions, need to do a get.
078          if (value.contains(VARIABLE_START)) {
079            value = conf.get(key);
080          }
081          zkProperties.setProperty(zkKey, value);
082        }
083      }
084    }
085
086    // If clientPort is not set, assign the default.
087    if (zkProperties.getProperty(HConstants.CLIENT_PORT_STR) == null) {
088      zkProperties.put(HConstants.CLIENT_PORT_STR,
089          HConstants.DEFAULT_ZOOKEEPER_CLIENT_PORT);
090    }
091
092    // Create the server.X properties.
093    int peerPort = conf.getInt("hbase.zookeeper.peerport", 2888);
094    int leaderPort = conf.getInt("hbase.zookeeper.leaderport", 3888);
095
096    final String[] serverHosts = conf.getStrings(HConstants.ZOOKEEPER_QUORUM,
097                                                 HConstants.LOCALHOST);
098    String serverHost;
099    String address;
100    String key;
101    for (int i = 0; i < serverHosts.length; ++i) {
102      if (serverHosts[i].contains(":")) {
103        serverHost = serverHosts[i].substring(0, serverHosts[i].indexOf(':'));
104      } else {
105        serverHost = serverHosts[i];
106      }
107      address = serverHost + ":" + peerPort + ":" + leaderPort;
108      key = "server." + i;
109      zkProperties.put(key, address);
110    }
111
112    return zkProperties;
113  }
114
115  /**
116   * Return the ZK Quorum servers string given the specified configuration
117   *
118   * @param conf
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 =
127        conf.getStrings(HConstants.ZOOKEEPER_QUORUM, HConstants.LOCALHOST);
128    return buildZKQuorumServerString(serverHosts, defaultClientPort);
129  }
130
131  /**
132   * Return the ZK Quorum servers string given the specified configuration.
133   * @return Quorum servers
134   */
135  public static String getZKQuorumServersString(Configuration conf) {
136    return getZKQuorumServersStringFromHbaseConfig(conf);
137  }
138
139  /**
140   * Build the ZK quorum server string with "server:clientport" list, separated by ','
141   *
142   * @param serverHosts a list of servers for ZK quorum
143   * @param clientPort the default client port
144   * @return the string for a list of "server:port" separated by ","
145   */
146  public static String buildZKQuorumServerString(String[] serverHosts, String clientPort) {
147    StringBuilder quorumStringBuilder = new StringBuilder();
148    String serverHost;
149    for (int i = 0; i < serverHosts.length; ++i) {
150      if (serverHosts[i].contains(":")) {
151        serverHost = serverHosts[i]; // just use the port specified from the input
152      } else {
153        serverHost = serverHosts[i] + ":" + clientPort;
154      }
155      if (i > 0) {
156        quorumStringBuilder.append(',');
157      }
158      quorumStringBuilder.append(serverHost);
159    }
160    return quorumStringBuilder.toString();
161  }
162
163  /**
164   * Verifies that the given key matches the expected format for a ZooKeeper cluster key.
165   * The Quorum for the ZK cluster can have one the following formats (see examples below):
166   *
167   * <ol>
168   *   <li>s1,s2,s3 (no client port in the list, the client port could be obtained from
169   *       clientPort)</li>
170   *   <li>s1:p1,s2:p2,s3:p3 (with client port, which could be same or different for each server,
171   *       in this case, the clientPort would be ignored)</li>
172   *   <li>s1:p1,s2,s3:p3 (mix of (1) and (2) - if port is not specified in a server, it would use
173   *       the clientPort; otherwise, it would use the specified port)</li>
174   * </ol>
175   *
176   * @param key the cluster key to validate
177   * @throws IOException if the key could not be parsed
178   */
179  public static void validateClusterKey(String key) throws IOException {
180    transformClusterKey(key);
181  }
182
183  /**
184   * Separate the given key into the three configurations it should contain:
185   * hbase.zookeeper.quorum, hbase.zookeeper.client.port
186   * and zookeeper.znode.parent
187   * @param key
188   * @return the three configuration in the described order
189   * @throws IOException
190   */
191  public static ZKClusterKey transformClusterKey(String key) throws IOException {
192    String[] parts = key.split(":");
193
194    if (parts.length == 3) {
195      if (!parts[2].matches("/.*[^/]")) {
196        throw new IOException("Cluster key passed " + key + " is invalid, the format should be:" +
197            HConstants.ZOOKEEPER_QUORUM + ":" + HConstants.ZOOKEEPER_CLIENT_PORT + ":"
198            + HConstants.ZOOKEEPER_ZNODE_PARENT);
199      }
200      return new ZKClusterKey(parts [0], Integer.parseInt(parts [1]), parts [2]);
201    }
202
203    if (parts.length > 3) {
204      // The quorum could contain client port in server:clientport format, try to transform more.
205      String zNodeParent = parts [parts.length - 1];
206      if (!zNodeParent.matches("/.*[^/]")) {
207        throw new IOException("Cluster key passed " + key + " is invalid, the format should be:"
208            + HConstants.ZOOKEEPER_QUORUM + ":" + HConstants.ZOOKEEPER_CLIENT_PORT + ":"
209            + HConstants.ZOOKEEPER_ZNODE_PARENT);
210      }
211
212      String clientPort = parts [parts.length - 2];
213
214      // The first part length is the total length minus the lengths of other parts and minus 2 ":"
215      int endQuorumIndex = key.length() - zNodeParent.length() - clientPort.length() - 2;
216      String quorumStringInput = key.substring(0, endQuorumIndex);
217      String[] serverHosts = quorumStringInput.split(",");
218
219      // The common case is that every server has its own client port specified - this means
220      // that (total parts - the ZNodeParent part - the ClientPort part) is equal to
221      // (the number of "," + 1) - "+ 1" because the last server has no ",".
222      if ((parts.length - 2) == (serverHosts.length + 1)) {
223        return new ZKClusterKey(quorumStringInput, Integer.parseInt(clientPort), zNodeParent);
224      }
225
226      // For the uncommon case that some servers has no port specified, we need to build the
227      // server:clientport list using default client port for servers without specified port.
228      return new ZKClusterKey(
229          buildZKQuorumServerString(serverHosts, clientPort),
230          Integer.parseInt(clientPort),
231          zNodeParent);
232    }
233
234    throw new IOException("Cluster key passed " + key + " is invalid, the format should be:" +
235        HConstants.ZOOKEEPER_QUORUM + ":" + HConstants.ZOOKEEPER_CLIENT_PORT + ":"
236        + HConstants.ZOOKEEPER_ZNODE_PARENT);
237  }
238
239  /**
240   * Get the key to the ZK ensemble for this configuration without
241   * 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
251   * a name at the end
252   * @param conf Configuration to use to build the key
253   * @param name Name that should be appended at the end if not empty or null
254   * @return ensemble key with a name (if any)
255   */
256  public static String getZooKeeperClusterKey(Configuration conf, String name) {
257    String ensemble = conf.get(HConstants.ZOOKEEPER_QUORUM).replaceAll(
258        "[\\t\\n\\x0B\\f\\r]", "");
259    StringBuilder builder = new StringBuilder(ensemble);
260    builder.append(":");
261    builder.append(conf.get(HConstants.ZOOKEEPER_CLIENT_PORT));
262    builder.append(":");
263    builder.append(conf.get(HConstants.ZOOKEEPER_ZNODE_PARENT));
264    if (name != null && !name.isEmpty()) {
265      builder.append(",");
266      builder.append(name);
267    }
268    return builder.toString();
269  }
270
271  /**
272   * Standardize the ZK quorum string: make it a "server:clientport" list, separated by ','
273   * @param quorumStringInput a string contains a list of servers for ZK quorum
274   * @param clientPort the default client port
275   * @return the string for a list of "server:port" separated by ","
276   */
277  @VisibleForTesting
278  public static String standardizeZKQuorumServerString(String quorumStringInput,
279      String clientPort) {
280    String[] serverHosts = quorumStringInput.split(",");
281    return buildZKQuorumServerString(serverHosts, clientPort);
282  }
283
284  // The Quorum for the ZK cluster can have one the following format (see examples below):
285  // (1). s1,s2,s3 (no client port in the list, the client port could be obtained from clientPort)
286  // (2). s1:p1,s2:p2,s3:p3 (with client port, which could be same or different for each server,
287  //      in this case, the clientPort would be ignored)
288  // (3). s1:p1,s2,s3:p3 (mix of (1) and (2) - if port is not specified in a server, it would use
289  //      the clientPort; otherwise, it would use the specified port)
290  @VisibleForTesting
291  public static class ZKClusterKey {
292    private String quorumString;
293    private int clientPort;
294    private String znodeParent;
295
296    ZKClusterKey(String quorumString, int clientPort, String znodeParent) {
297      this.quorumString = quorumString;
298      this.clientPort = clientPort;
299      this.znodeParent = znodeParent;
300    }
301
302    public String getQuorumString() {
303      return quorumString;
304    }
305
306    public int getClientPort() {
307      return clientPort;
308    }
309
310    public String getZnodeParent() {
311      return znodeParent;
312    }
313  }
314
315  /**
316   * Get the client ZK Quorum servers string
317   * @param conf the configuration to read
318   * @return Client quorum servers, or null if not specified
319   */
320  public static String getClientZKQuorumServersString(Configuration conf) {
321    String clientQuromServers = conf.get(HConstants.CLIENT_ZOOKEEPER_QUORUM);
322    if (clientQuromServers == null) {
323      return null;
324    }
325    int defaultClientPort =
326        conf.getInt(HConstants.ZOOKEEPER_CLIENT_PORT, HConstants.DEFAULT_ZOOKEEPER_CLIENT_PORT);
327    String clientZkClientPort =
328        Integer.toString(conf.getInt(HConstants.CLIENT_ZOOKEEPER_CLIENT_PORT, defaultClientPort));
329    // Build the ZK quorum server string with "server:clientport" list, separated by ','
330    final String[] serverHosts = StringUtils.getStrings(clientQuromServers);
331    return buildZKQuorumServerString(serverHosts, clientZkClientPort);
332  }
333}