在部署hue+hbase thrift的过程中,遇到问题很多,主要做以下总结.
1. 代理权限问题:
代理过程中必须保证hue票据用户和hbase thrift票据用户都具有代理权限.
如果hue票据用户缺少代理权限, thrift日志会提示如*** can not do as ***
如果thrift票据用户缺少代理权限, thrift日志会提示重试超时, 并不会有明显的错误提示.
2. 认证及票据设置问题:
这是hue+hbase thrift最难调试的问题, 相关文档对该问题的描述也不清楚.
一般按照官方文档部署,在通过hue访问hbase表时,可能会遇到以下几个问题
1. 缺少tgt票据, 这个时候一般是hue票据指定的用户有误, 需要仔细检查hue用户
2. 页面提示401, 查看thrift日志,提示Caused by: KrbException: Specified version of key is not available.这个错误是因为在认证过程中发 生错误, 如果hue的日志中打印出authenticate_user(): Authorization header: Negotiate **** 这样的字样,说明client的认证是正确的, 则很 可能说明client在申请服务票据时,指定的服务名称有误. 在实际部署过程中反复的对相关配置做了检查, 没有发现错误, 所以尝试对hue的源码做分析.
源码分析
hue代码结构
apps 各个组件的操作函数
desktop 界面相关功能, 同时包含部分基础依赖
hbase组件代码入口为apps/hbase/src/hbase/api.py, 该文件包括获取集群信息, 安全设置, 提供抽象操作接口, 并通过调用apps/hbase/gen-py 下的thrift接口进行实际操作. 以getTableList为例
api.py
def getTableList(self, cluster):
# 返回客户端, 实际为gen-py/HBase.py, 其为hbase-thrift客户端,
client = self.connectCluster(cluster)
return [{'name': name, 'enabled': client.isTableEnabled(name, doas=self.user.username)} for name in client.getTa
bleNames(doas=self.user.username)]
def connectCluster(self, name):
# 获取安全相关信息, 格式为
# {kerberos_principal_short_name': kerberos_principal_short_name(/前面的部分),
# 'use_sasl': use_sasl(true)}
# 特别注意, 这部分通过读取/etc/hbase/conf下的配置获得,如果配置有误将报错
_security = self._get_security()
# 获取hbase集群的信息(Cluster_name:host:port), 这部分会抛出json异常, 这不是个错误
target = self.getCluster(name)
client = thrift_util.get_client(...)
return client
thrift_util.py
def get_client(klass, host, port, service_name, **kwargs):
# 获得connection config, 简单赋值
conf = ConnectionConfig(klass, host, port, service_name, **kwargs)
# A wrapper for a SuperClient
# SuperClient(object): A wrapper for a Thrift Client that causes it to automatically reconnect on failure.
# 这部分的调用链很长,主要是对连接进行包装, 包括错误重试,维护连接之类.最终返回
# TODO 补充调用链
return PooledClient(conf)
# 部分代码
def connect_to_thrift(conf):
if conf.transport_mode == 'http':
mode = THttpClient(conf.http_url)
if conf.transport_mode == 'http':
if conf.use_sasl and conf.mechanism != 'PLAIN':
mode.set_kerberos_auth()
else:
mode.set_basic_auth(conf.username, conf.password)
以上代码返回
apps/hbase/gen-py/Hbase.py Client类. Client内部包含一个thrift_.py/http_client对象,负责向hbase thrift发送请求.
整体上,Hbase.py负责生成相关调用的上下文,并通过调用http_client接口发送相关请求.http_client负责处理安全,错误处理等.
thrift_.py/http_client的实现相对比较简单,内部保存了一个rest/http_client对象,负责处理具体的请求.所以thrift与rest在底层实现上实际上公用了一套代码.以下以安全设置部分代码为例
thrift_.py/http_client
def set_kerberos_auth(self):
self._client.set_kerberos_auth()
rest/http_client
def set_kerberos_auth(self):
"""Set up kerberos auth for the client, based on the current ticket."""
self._session.auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
return self
requsets_kerberos/kerberos_.py
class HTTPKerberosAuth(AuthBase):
"""Attaches HTTP GSSAPI/Kerberos Authentication to the given Request
object."""
def __init__(self, mutual_authentication=REQUIRED, service="HTTP"):
self.context = {}
self.mutual_authentication = mutual_authentication
self.pos = None
self.service = service
def authenticate_user(self, response, **kwargs):
"""Handles user authentication with gssapi/kerberos"""
auth_header = self.generate_request_header(response)
if auth_header is None:
# GSS Failure, return existing response
return response
log.debug("authenticate_user(): Authorization header: {0}".format(
auth_header))
response.request.headers['Authorization'] = auth_header
# Consume the content so we can reuse the connection for the next
# request.
response.content
response.raw.release_conn()
_r = response.connection.send(response.request, **kwargs)
_r.history.append(response)
log.debug("authenticate_user(): returning {0}".format(_r))
return _r
def generate_request_header(self, response):
"""
Generates the GSSAPI authentication token with kerberos.
If any GSSAPI step fails, return None.
"""
host = urlparse(response.url).hostname
try:
# 这里在做实际认证时会进行拼接,具体拼接的位置还没找到,拼接结果为HTTP/hostname@REALM
result, self.context[host] = kerberos.authGSSClientInit(
"{0}@{1}".format(self.service, host))
except kerberos.GSSError:
log.exception("generate_request_header(): authGSSClientInit() failed:")
return None
if result < 1:
log.error("generate_request_header(): authGSSClientInit() failed: "
"{0}".format(result))
return None
try:
result = kerberos.authGSSClientStep(self.context[host],
_negotiate_value(response))
except kerberos.GSSError:
log.exception("generate_request_header(): authGSSClientStep() failed:")
return None
if result < 0:
log.error("generate_request_header(): authGSSClientStep() failed: "
"{0}".format(result))
return None
try:
gss_response = kerberos.authGSSClientResponse(self.context[host])
except kerberos.GSSError:
log.exception("generate_request_header(): authGSSClientResponse() "
"failed:")
return None
return "Negotiate {0}".format(gss_response)
到这里,就已经发现问题的根源,hue在与thrift做认证过程中,默认指定server_name为HTTP,所以在配置thrift的票据时必须指定服务名为HTTP,不然将导致认证错误(response 401),目前可见的代码中没有可以对server_name做配置的地方.这应该算是hue的一个不大不小的bug,有趣的是这个问题hue官方是知道的,在官方的说明中说是thrift使用的keytab文件中必须包含HTTP,但是没有做进一步的说明,这个相当的坑.
thrift使用的spnego协议
http://www.ibm.com/support/knowledgecenter/SS9H2Y_7.5.0/com.ibm.dp.doc/spnego_protocol.html
https://msdn.microsoft.com/en-us/library/ms995329.aspx