/**
* Copyright 2016 StreamSets Inc.
*
* Licensed under the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.streamsets.pipeline.lib.hbase.common;
import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.streamsets.datacollector.security.HadoopSecurityUtil;
import com.streamsets.pipeline.api.ExecutionMode;
import com.streamsets.pipeline.api.Record;
import com.streamsets.pipeline.api.Stage;
import com.streamsets.pipeline.api.StageException;
import com.streamsets.pipeline.api.base.OnRecordErrorException;
import com.streamsets.pipeline.api.impl.Utils;
import com.streamsets.pipeline.stage.common.ErrorRecordHandler;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeys;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException;
import org.apache.hadoop.hbase.client.Row;
import org.apache.hadoop.hbase.regionserver.NoSuchColumnFamilyException;
import org.apache.hadoop.hbase.security.User;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.security.UserGroupInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.UndeclaredThrowableException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import static org.apache.hadoop.hbase.util.Strings.isEmpty;
public final class HBaseUtil {
private static final String TABLE_NAME = "tableName";
private static final Logger LOG = LoggerFactory.getLogger(HBaseUtil.class);
// master and region server principals are not defined in HBase constants, so do it here
private static final String MASTER_KERBEROS_PRINCIPAL = "hbase.master.kerberos.principal";
private static final String REGIONSERVER_KERBEROS_PRINCIPAL = "hbase.regionserver.kerberos.principal";
private static final String HBASE_CONF_DIR_CONFIG = "hbaseConfDir";
private static UserGroupInformation loginUgi;
private static UserGroupInformation userUgi;
private HBaseUtil() {
}
public static Configuration getHBaseConfiguration(
List<Stage.ConfigIssue> issues,
Stage.Context context,
String hbaseName,
String hbaseConfDir,
String tableName,
Map<String, String> hbaseConfigs
) {
Configuration hbaseConf = HBaseConfiguration.create();
if (hbaseConfDir != null && !hbaseConfDir.isEmpty()) {
File hbaseConfigDir = new File(hbaseConfDir);
if ((context.getExecutionMode() == ExecutionMode.CLUSTER_BATCH || context.getExecutionMode() == ExecutionMode.CLUSTER_YARN_STREAMING
|| context.getExecutionMode() == ExecutionMode.CLUSTER_MESOS_STREAMING) && hbaseConfigDir.isAbsolute()) {
//Do not allow absolute hdfs config directory in cluster mode
issues.add(
context.createConfigIssue(hbaseName, HBASE_CONF_DIR_CONFIG, Errors.HBASE_24, hbaseConfDir)
);
} else {
if (!hbaseConfigDir.isAbsolute()) {
hbaseConfigDir = new File(context.getResourcesDirectory(), hbaseConfDir).getAbsoluteFile();
}
if (!hbaseConfigDir.exists()) {
issues.add(context.createConfigIssue(hbaseName, HBASE_CONF_DIR_CONFIG, Errors.HBASE_19,
hbaseConfDir));
} else if (!hbaseConfigDir.isDirectory()) {
issues.add(context.createConfigIssue(hbaseName, HBASE_CONF_DIR_CONFIG, Errors.HBASE_20,
hbaseConfDir));
} else {
File hbaseSiteXml = new File(hbaseConfigDir, "hbase-site.xml");
if (hbaseSiteXml.exists()) {
if (!hbaseSiteXml.isFile()) {
issues.add(context.createConfigIssue(
hbaseName,
HBASE_CONF_DIR_CONFIG,
Errors.HBASE_21,
hbaseConfDir,
"hbase-site.xml"
)
);
}
hbaseConf.addResource(new Path(hbaseSiteXml.getAbsolutePath()));
}
}
}
}
for (Map.Entry<String, String> config : hbaseConfigs.entrySet()) {
hbaseConf.set(config.getKey(), config.getValue());
}
if (context.isPreview()) {
// by default the retry number is set to 35 which is too much for preview mode
LOG.debug("Setting HBase client retries to 3 for preview");
hbaseConf.set(HConstants.HBASE_CLIENT_RETRIES_NUMBER, "3");
}
if (tableName == null || tableName.isEmpty()) {
issues.add(context.createConfigIssue(hbaseName, TABLE_NAME, Errors.HBASE_05));
}
return hbaseConf;
}
public static void validateQuorumConfigs(
List<Stage.ConfigIssue> issues,
Stage.Context context,
String hbaseName,
String zookeeperQuorum,
String zookeeperParentZNode,
int clientPort
//String tableName
) {
if (isEmpty(zookeeperQuorum)) {
issues.add(context.createConfigIssue(hbaseName, "zookeeperQuorum", Errors.HBASE_04));
} else {
List<String> zkQuorumList = Lists.newArrayList(Splitter.on(",").trimResults().omitEmptyStrings().split(
zookeeperQuorum));
for (String hostName : zkQuorumList) {
try {
InetAddress.getByName(hostName);
} catch (UnknownHostException ex) {
LOG.warn(Utils.format("Cannot resolve host: '{}' from zookeeper quorum '{}', error: '{}'",
hostName,
zookeeperQuorum,
ex
), ex);
issues.add(context.createConfigIssue(hbaseName, "zookeeperQuorum", Errors.HBASE_39, hostName));
}
}
}
if (zookeeperParentZNode == null || zookeeperParentZNode.isEmpty()) {
issues.add(context.createConfigIssue(hbaseName, "zookeeperBaseDir",
Errors.HBASE_09));
}
if (clientPort == 0) {
issues.add(context.createConfigIssue(hbaseName, "clientPort", Errors.HBASE_13));
}
}
public static void validateSecurityConfigs(
List<Stage.ConfigIssue> issues,
Stage.Context context,
String hbaseName,
String hbaseUser,
Configuration hbaseConf,
boolean kerberosAuth
) {
try {
if (kerberosAuth) {
hbaseConf.set(User.HBASE_SECURITY_CONF_KEY, UserGroupInformation.AuthenticationMethod.KERBEROS.name());
hbaseConf.set(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION, UserGroupInformation.AuthenticationMethod.KERBEROS.name());
String defaultRealm = null;
if (hbaseConf.get(MASTER_KERBEROS_PRINCIPAL) == null) {
try {
defaultRealm = HadoopSecurityUtil.getDefaultRealm();
} catch (Exception e) {
LOG.error(Errors.HBASE_22.getMessage(), e.toString(), e);
issues.add(context.createConfigIssue(hbaseName, "masterPrincipal", Errors.HBASE_22, e.toString()));
}
hbaseConf.set(MASTER_KERBEROS_PRINCIPAL, "hbase/_HOST@" + defaultRealm);
}
if (hbaseConf.get(REGIONSERVER_KERBEROS_PRINCIPAL) == null) {
try {
if (defaultRealm == null) {
defaultRealm = HadoopSecurityUtil.getDefaultRealm();
}
} catch (Exception e) {
LOG.error(Errors.HBASE_23.getMessage(), e.toString(), e);
issues.add(context.createConfigIssue(hbaseName, "regionServerPrincipal", Errors.HBASE_23, e.toString()));
}
hbaseConf.set(REGIONSERVER_KERBEROS_PRINCIPAL, "hbase/_HOST@" + defaultRealm);
}
}
loginUgi = HadoopSecurityUtil.getLoginUser(hbaseConf);
userUgi = HadoopSecurityUtil.getProxyUser(
hbaseUser,
context,
loginUgi,
issues,
hbaseName,
"hbaseUser"
);
StringBuilder logMessage = new StringBuilder();
if (kerberosAuth) {
logMessage.append("Using Kerberos");
if (loginUgi.getAuthenticationMethod() != UserGroupInformation.AuthenticationMethod.KERBEROS) {
issues.add(context.createConfigIssue(hbaseName, "kerberosAuth", Errors.HBASE_16,
loginUgi.getAuthenticationMethod()));
}
} else {
logMessage.append("Using Simple");
hbaseConf.set(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION,
UserGroupInformation.AuthenticationMethod.SIMPLE.name());
}
LOG.info("Authentication Config: " + logMessage);
} catch (Exception ex) {
LOG.info("Error validating security configuration: " + ex, ex);
issues.add(context.createConfigIssue(hbaseName, null, Errors.HBASE_17, ex.toString(), ex));
}
}
public static UserGroupInformation getUGI() {
return userUgi;
}
public static HTableDescriptor checkConnectionAndTableExistence(
List<Stage.ConfigIssue> issues,
Stage.Context context,
Configuration hbaseConf,
String hbaseName,
String tableName
) throws IOException {
LOG.debug("Validating connection to hbase cluster and whether table " + tableName + " exists and is enabled");
HBaseAdmin hbaseAdmin = null;
HTableDescriptor hTableDescriptor = null;
try {
hbaseAdmin = new HBaseAdmin(hbaseConf);
if (!hbaseAdmin.tableExists(tableName)) {
issues.add(context.createConfigIssue(hbaseName, TABLE_NAME, Errors.HBASE_07, tableName));
} else if (!hbaseAdmin.isTableEnabled(tableName)) {
issues.add(context.createConfigIssue(hbaseName, TABLE_NAME, Errors.HBASE_08, tableName));
} else {
hTableDescriptor = hbaseAdmin.getTableDescriptor(TableName.valueOf(tableName));
}
} catch (Exception ex) {
LOG.warn("Received exception while connecting to cluster: ", ex);
issues.add(context.createConfigIssue(hbaseName, null, Errors.HBASE_06, ex.toString(), ex));
} finally {
if (hbaseAdmin != null) {
hbaseAdmin.close();
}
}
return hTableDescriptor;
}
public static void handleHBaseException(
Throwable t,
Iterator<Record> records,
ErrorRecordHandler errorRecordHandler
) throws StageException {
Throwable cause = t;
// Drill down to root cause
while((cause instanceof UncheckedExecutionException || cause instanceof UndeclaredThrowableException ||
cause instanceof ExecutionException) && cause.getCause() != null) {
cause = cause.getCause();
}
// Column is null or No such Column Family exception
if(cause instanceof NullPointerException || cause instanceof NoSuchColumnFamilyException) {
while(records.hasNext()) {
Record record = records.next();
errorRecordHandler.onError(new OnRecordErrorException(record, Errors.HBASE_37, cause));
}
} else {
LOG.error(Errors.HBASE_36.getMessage(), cause.toString(), cause);
throw new StageException(Errors.HBASE_36, cause.toString(), cause);
}
}
public static void handleHBaseException(
RetriesExhaustedWithDetailsException rex,
Record record,
Map<String, Record> rowKeyToRecord,
ErrorRecordHandler errorRecordHandler
) throws StageException {
for (int i = 0; i < rex.getNumExceptions(); i++) {
if (rex.getCause(i) instanceof NoSuchColumnFamilyException) {
Row r = rex.getRow(i);
Record errorRecord = record != null ? record : rowKeyToRecord.get(Bytes.toString(r.getRow()));
OnRecordErrorException exception =
new OnRecordErrorException(errorRecord, Errors.HBASE_10,
getErrorDescription(rex.getCause(i), r, rex.getHostnamePort(i)));
errorRecordHandler.onError(exception);
} else {
// If at least 1 non NoSuchColumnFamilyException exception,
// consider as stage exception
throw new StageException(Errors.HBASE_02, rex);
}
}
}
public static void setIfNotNull(Configuration conf, String property, String value) {
if(value != null) {
conf.set(property, value);
}
}
private static String getErrorDescription(Throwable t, Row row, String server) {
StringWriter errorWriter = new StringWriter();
PrintWriter pw = new PrintWriter(errorWriter);
pw.append("Exception from ").append(server).append(" for ").append(Bytes.toStringBinary(row.getRow()));
if (t != null) {
pw.println();
t.printStackTrace(pw);
}
pw.flush();
return errorWriter.toString();
}
public static HBaseColumn getColumn(String column) {
byte[][] parts = KeyValue.parseColumn(Bytes.toBytes(column));
byte[] cf;
byte[] qualifier;
if (parts.length == 2) {
cf = parts[0];
qualifier = parts[1];
return new HBaseColumn(cf, qualifier);
} else {
return null;
}
}
}