1. admin_api 流水线
在上一章中, 我们讲了WEB服务器的创建及使用。这一章我们将进入Keystone的实际业务认证流程。
我们以admin_api做为一个实例,从Paste Deployment配置文件的流水线开始。
先来看看配置的流水线过程。
[pipeline:admin_api]
pipeline = access_log sizelimit url_normalize token_auth admin_token_auth xml_body json_body ec2_extension s3_extension crud_extension admin_service
可以看出, 这里有很多的流水线处理, 我们一个一个来简单过一下。
1.1 access_log
[filter:access_log]
paste.filter_factory = keystone.contrib.access:AccessLogMiddleware.factory
从这里可以看出, 这个点处理的是AccessLogMiddleware, 打开这个类, 所有的代码如下:
import webob.dec
from keystone.common import wsgi
from keystone import config
from keystone.openstack.common import log as logging
from keystone.openstack.common import timeutils
CONF = config.CONF
LOG = logging.getLogger('access')
APACHE_TIME_FORMAT = '%d/%b/%Y:%H:%M:%S'
APACHE_LOG_FORMAT = (
'%(remote_addr)s - %(remote_user)s [%(datetime)s] "%(method)s %(url)s '
'%(http_version)s" %(status)s %(content_length)s')
class AccessLogMiddleware(wsgi.Middleware):
"""Writes an access log to INFO."""
@webob.dec.wsgify
def __call__(self, request):
data = {
'remote_addr': request.remote_addr,
'remote_user': request.remote_user or '-',
'method': request.method,
'url': request.url,
'http_version': request.http_version,
'status': 500,
'content_length': '-'}
try:
response = request.get_response(self.application)
data['status'] = response.status_int
data['content_length'] = len(response.body) or '-'
finally:
# must be calculated *after* the application has been called
now = timeutils.utcnow()
# timeutils may not return UTC, so we can't hardcode +0000
data['datetime'] = '%s %s' % (now.strftime(APACHE_TIME_FORMAT),
now.strftime('%z') or '+0000')
LOG.info(APACHE_LOG_FORMAT % data)
return response
可以看出, 它里面就只是定义一个__call__ 的方法,然后写一条info log到日志文件中。但是这里, 却没有factory的方法。 我们继续打开它的父类。
class Middleware(Application):
@classmethod
def factory(cls, global_config, **local_config):
def _factory(app):
conf = global_config.copy()
conf.update(local_config)
return cls(app, **local_config)
return _factory
...
在这个类中, 我们找到了factory这个类的静态方法,它返回一个用于创建本类的一个方法。到此为止, access_log就已经结束。它的作用就是写一条info log.
1.2 sizelimit
[filter:sizelimit]
paste.filter_factory = keystone.middleware:RequestBodySizeLimiter.factory
class RequestBodySizeLimiter(wsgi.Middleware):
"""Limit the size of an incoming request."""
def __init__(self, *args, **kwargs):
super(RequestBodySizeLimiter, self).__init__(*args, **kwargs)
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
if req.content_length > CONF.max_request_body_size:
raise exception.RequestTooLarge()
if req.content_length is None and req.is_body_readable:
limiter = utils.LimitingReader(req.body_file,
CONF.max_request_body_size)
req.body_file = limiter
return self.application
这个看起来更简单, 只是检查下大小, 然后把内容读出来放到req.body_file中。 这样body_file就存放了HTTP请求的实际内容。
1.3 url_normalize
[filter:url_normalize]
paste.filter_factory = keystone.middleware:NormalizingFilter.factory
class NormalizingFilter(wsgi.Middleware):
"""Middleware filter to handle URL normalization."""
def process_request(self, request):
"""Normalizes URLs."""
# Removes a trailing slash from the given path, if any.
if (len(request.environ['PATH_INFO']) > 1 and
request.environ['PATH_INFO'][-1] == '/'):
request.environ['PATH_INFO'] = request.environ['PATH_INFO'][:-1]
# Rewrites path to root if no path is given.
elif not request.environ['PATH_INFO']:
request.environ['PATH_INFO'] = '/'
看到这里, 发现问题来了, 在这个类中, 我们没有定义__call__ 方法, 只是定义了一个process_request。 前面提到过, WSGI要求返回一个可调用的对象。这里NormalizingFilter明显不是一个可调用的对象。 这时我们又得返回到Middleware这个父类中。
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, request):
try:
response = self.process_request(request)
if response:
return response
response = request.get_response(self.application)
return self.process_response(request, response)
except exception.Error as e:
LOG.warning(e)
return render_exception(e,
user_locale=request.best_match_language())
except TypeError as e:
LOG.exception(e)
return render_exception(exception.ValidationError(e),
user_locale=request.best_match_language())
except Exception as e:
LOG.exception(e)
return render_exception(exception.UnexpectedError(exception=e),
user_locale=request.best_match_language())
很好,在这里我们找到了__call__ 方法, 这样NormalizingFilter本质上还是一个可调用的对象。然后我们再详细看看Middleware的__call__ 方法, 可以看出, 它先调用本类的process_request方法。如果这个方法有返回值。那么就作为一个Response对象返回给Client. 否则的话继续处理process_response。
到此, NormalizingFilter的process_request方法被调用, 然后把HTTP请求中的PATH_INFO给规范化,即转化为/aaa/bbb的形式, 以/打头, 把路径的最后一个/给去掉。NormalizingFilter的处理全部结束。
但是, 细心的读者应该发现, 在__call__ 方法中, 还调用了process_response方法。继续回到代码:
def process_request(self, request):
return None
def process_response(self, request, response):
return response
可以发现, 在Middleware中, 定义了两个默认的方法, 什么事情也不做, 但也不会破坏流水线的结构。
到此为此, 整个Middleware的方法也都全被呈现了出来。
1.4 token_auth
[filter:token_auth]
paste.filter_factory = keystone.middleware:TokenAuthMiddleware.factory
class TokenAuthMiddleware(wsgi.Middleware):
def process_request(self, request):
token = request.headers.get(AUTH_TOKEN_HEADER)
context = request.environ.get(CONTEXT_ENV, {})
context['token_id'] = token
if SUBJECT_TOKEN_HEADER in request.headers:
context['subject_token_id'] = (
request.headers.get(SUBJECT_TOKEN_HEADER))
request.environ[CONTEXT_ENV] = context
从这里可以看出, 它也只是把token_id从HTTP的请求中取出来,然后存入request.environ[CONTEXT_ENV]全局变量中, 以备后用。
1.5 admin_token_auth
[filter:admin_token_auth]
paste.filter_factory = keystone.middleware:AdminTokenAuthMiddleware.factory
class AdminTokenAuthMiddleware(wsgi.Middleware):
def process_request(self, request):
token = request.headers.get(AUTH_TOKEN_HEADER)
context = request.environ.get(CONTEXT_ENV, {})
context['is_admin'] = (token == CONF.admin_token)
request.environ[CONTEXT_ENV] = context
这是个和之前类似的中间件,取出HTTP请求中的token,然后存入request.environ[CONTEXT_ENV]全局变量中。
1.6 xml_body
[filter:xml_body]
paste.filter_factory = keystone.middleware:XmlBodyMiddleware.factory
class XmlBodyMiddleware(wsgi.Middleware):
def process_request(self, request):
"""Transform the request from XML to JSON."""
incoming_xml = 'application/xml' in str(request.content_type)
if incoming_xml and request.body:
request.content_type = 'application/json'
try:
request.body = jsonutils.dumps(
serializer.from_xml(request.body))
except Exception:
LOG.exception('Serializer failed')
e = exception.ValidationError(attribute='valid XML',
target='request body')
return wsgi.render_exception(e)
从代码中可以看出, 这个中间件的作用就是把XML请求转换为JSON数据类型。然后内部统一使用JSON的格式进行处理。
1.7 json_body
[filter:json_body]
paste.filter_factory = keystone.middleware:JsonBodyMiddleware.factory
class JsonBodyMiddleware(wsgi.Middleware):
def process_request(self, request):
# Abort early if we don't have any work to do
params_json = request.body
if not params_json:
return
# Reject unrecognized content types. Empty string indicates
# the client did not explicitly set the header
if request.content_type not in ('application/json', ''):
e = exception.ValidationError(attribute='application/json',
target='Content-Type header')
return wsgi.render_exception(e)
params_parsed = {}
try:
params_parsed = jsonutils.loads(params_json)
except ValueError:
e = exception.ValidationError(attribute='valid JSON',
target='request body')
return wsgi.render_exception(e)
finally:
if not params_parsed:
params_parsed = {}
params = {}
for k, v in params_parsed.iteritems():
if k in ('self', 'context'):
continue
if k.startswith('_'):
continue
params[k] = v
request.environ[PARAMS_ENV] = params
在这个中间件中, 先是对文档类型进行了判断,如果不是JSON的格式, 就抛出ValidationError的Exception。 联合之前的xml处理,可以得出, 如果不是XML或是JSON的格式。那么Keystone就不会进行处理, 换句话说, 它只处理XML和JSON的类型。
把JSON类型转换出来后,就把结果保存在request.environ[PARAMS_ENV]中, 后面的应该就会更方便的使用。
1.8 ec2_extension
[filter:ec2_extension]
paste.filter_factory = keystone.contrib.ec2:Ec2Extension.factory
class Ec2Extension(wsgi.ExtensionRouter):
def add_routes(self, mapper):
ec2_controller = controllers.Ec2Controller()
# validation
mapper.connect(
'/ec2tokens',
controller=ec2_controller,
action='authenticate',
conditions=dict(method=['POST']))
# crud
mapper.connect(
'/users/{user_id}/credentials/OS-EC2',
controller=ec2_controller,
action='create_credential',
conditions=dict(method=['POST']))
mapper.connect(
'/users/{user_id}/credentials/OS-EC2',
controller=ec2_controller,
action='get_credentials',
conditions=dict(method=['GET']))
mapper.connect(
'/users/{user_id}/credentials/OS-EC2/{credential_id}',
controller=ec2_controller,
action='get_credential',
conditions=dict(method=['GET']))
mapper.connect(
'/users/{user_id}/credentials/OS-EC2/{credential_id}',
controller=ec2_controller,
action='delete_credential',
conditions=dict(method=['DELETE']))
在这个中间件中, 可以看出它并没有定义process_request和process_response方法。 但是细心的你也应该发现这个中间件的父类也已经不是Middleware了, 而是ExtensionRouter, 这里我们先不管这个类的具体实现。从名字大概可以猜出这个中间件的作用就是Ec2接口增加路由,然后使用ec2_controller来处理EC2风格的认证请求。对于Router,我们将在下一章中继续解读。
1.9 s3_extension
[filter:s3_extension]
paste.filter_factory = keystone.contrib.s3:S3Extension.factory
class S3Extension(wsgi.ExtensionRouter):
def add_routes(self, mapper):
controller = S3Controller()
# validation
mapper.connect('/s3tokens',
controller=controller,
action='authenticate',
conditions=dict(method=['POST']))
这个中件间的作用和之前的EC2类似,只是增加一条路由
1.10 crud_extension
[filter:crud_extension]
paste.filter_factory = keystone.contrib.admin_crud:CrudExtension.factory
class CrudExtension(wsgi.ExtensionRouter):
"""Previously known as the OS-KSADM extension.
Provides a bunch of CRUD operations for internal data types.
"""
def add_routes(self, mapper):
tenant_controller = identity.controllers.Tenant()
user_controller = identity.controllers.User()
role_controller = identity.controllers.Role()
service_controller = catalog.controllers.Service()
endpoint_controller = catalog.controllers.Endpoint()
# Tenant Operations
mapper.connect(
'/tenants',
controller=tenant_controller,
action='create_project',
conditions=dict(method=['POST']))
mapper.connect(
'/tenants/{tenant_id}',
controller=tenant_controller,
action='update_project',
conditions=dict(method=['PUT', 'POST']))
mapper.connect(
'/tenants/{tenant_id}',
controller=tenant_controller,
action='delete_project',
conditions=dict(method=['DELETE']))
mapper.connect(
'/tenants/{tenant_id}/users',
controller=tenant_controller,
action='get_project_users',
conditions=dict(method=['GET']))
...
这个类的作用很明显, 就是给内部对象增加RESTful 风格的CRUD操作路由。 其中有五个控制器(tenant_controller, user_controller, role_controller, service_controller, endpoint_controller)。
1.11 admin_service
[app:admin_service]
paste.app_factory = keystone.service:admin_app_factory
@fail_gracefully
def admin_app_factory(global_conf, **local_conf):
conf = global_conf.copy()
conf.update(local_conf)
return wsgi.ComposingRouter(routes.Mapper(),
[identity.routers.Admin(),
token.routers.Router(),
routers.VersionV2('admin'),
routers.Extension()])
这个factory产生了一个ComposingRouter的对象,这是流水线的最后一级,也是业务的实际处理点。具休的实现我们以后再说。
下一章, 我们将解读Router的具体实现。