本文会重点讲解OpenStack中使用的API开发框架的使用。可是本文的目的并非覆盖这些框架的使用细节,而是经过说明重要的部分,下降初学者的入门的门槛。框架的使用细节均可以从文档中找到。说明一下,除非特殊说明,本文中的相对路径都是相对于项目源码目录的相对路径。python
咱们在API服务(1)中已经提到了,这个框架只在早期开始的项目中使用,新的项目都已经转到Pecan框架了。可是,早期的项目都是比较核心的项目,所以咱们仍是要学会如何使用这个框架。咱们会以Keystone项目为例,来讲明如何阅读使用这个框架的开发的API代码。web
RESTful API程序的主要特色就是URL path会和功能对应起来。这点从API文档就能够看得出来,好比用户管理的功能通常都放在/user这个路径下。所以,看一个RESTful API程序,通常都是看它实现了哪些URL path,以及每一个path对应了什么功能,这个通常都是由框架的URL路由功能负责的。因此,熟悉一个RESTful API程序的重点在于肯定URL路由。本章所说的这个框架对于初学者的难点也是如何肯定URL路由。json
做为基础知识,你须要先了解一下WSGI的相关概念,能够参考这篇文章WSGI简介。segmentfault
在API服务(1)中提到了WSGI可使用Apache进行部署,也可使用eventlet进行部署。Keystone项目同时提供了这两种方案的代码,也就是咱们要找的WSGI的入口。api
Keystone项目在httpd/目录下,存放了能够用于Apache服务器部署WSGI服务的文件。其中,wsgi-keystone.conf是一个mod_wsgi的示例配置文件,keystone.py则是WSGI应用程序的入口文件。httpd/keystone.py也就是咱们要找的入口文件之一。这个文件的内容很简单:服务器
import os from keystone.server import wsgi as wsgi_server name = os.path.basename(__file__) application = wsgi_server.initialize_application(name)
文件中建立了WSGI入口须要使用的application
对象。app
keystone-all命令则是采用eventlet来进行部署时的入口,能够从setup.cfg文件按中肯定keystone-all命令的入口:框架
[entry_points] console_scripts = keystone-all = keystone.cmd.all:main keystone-manage = keystone.cmd.manage:main
从setup.cfg文件的entry_points部分能够看出,keystone-all的入口是keystone/cmd/all.py文件中的main()
函数,这个函数的内容也很简单:python2.7
def main(): eventlet_server.run(possible_topdir)
main()
函数的主要做用就是启动一个eventlet_server,配置文件从possible_topdir
中查找。由于eventlet的部署方式涉及到eventlet库的使用方法,本文再也不展开说明。读者能够在学会肯定URL路由后再回来看这个代码。下面,继续以httpd/keystone.py文件做为入口来讲明如何阅读代码。ide
httpd/keystone.py中调用的initialize_application(name)
函数载入了整个WSGI应用,这里主要用到了Paste和PasteDeploy库。
def initialize_application(name): ... def loadapp(): return keystone_service.loadapp( 'config:%s' % config.find_paste_config(), name) _unused, application = common.setup_backends( startup_application_fn=loadapp) return application
上面是删掉无关代码后的initialize_application()
函数。config.find_paste_config()
用来查找PasteDeploy须要用到的WSGI配置文件,这个文件在源码中是etc/keystone-paste.ini文件,若是在线上环境中,通常是/etc/keystone-paste.init。keystone_service.loadapp()
函数内部则调用了paste.deploy.loadapp()
函数来加载WSGI应用,如何加载则使用了刚才提到的keystone-paste.ini文件,这个文件也是看懂整个程序的关键。
在上面的代码中咱们能够看到,name
这个变量从httpd/keystone.py文件传递到initialize_application()
函数,又被传递到keystone_service.loadapp()
函数,最终被传递到paste.deploy.loadapp()
函数。那么,这个name
变量到底起什么做用呢?先把这个问题放在一边,咱们后面再来解决它。
使用Paste和PasteDeploy模块来实现WSGI服务时,都须要一个paste.ini文件。这个文件也是Paste框架的精髓,这里须要重点说明一下这个文件如何阅读。
paste.ini文件的格式相似于INI格式,每一个section的格式为[type:name]。这里重要的是理解几种不一样type的section的做用。
composite: 这种section用于将HTTP请求分发到指定的app。
app: 这种section表示具体的app。
filter: 实现一个过滤器中间件。
pipeline: 用来把把一系列的filter串起来。
上面这些section是在keystone的paste.ini中用到的,下面详细介绍一下如何使用。这里须要用到WSGIMiddleware(WSGI中间件)的知识,能够在WSGI简介这篇文章中找到。
section composite
这种section用来决定如何分发HTTP请求。Keystone的paste.ini文件中有两个composite的section:
[composite:main] use = egg:Paste#urlmap /v2.0 = public_api /v3 = api_v3 / = public_version_api [composite:admin] use = egg:Paste#urlmap /v2.0 = admin_api /v3 = api_v3 / = admin_version_api
在composite seciont中,use是一个关键字,指定处理请求的代码。egg:Paste#urlmap
表示到Paste模块的egg-info中去查找urlmap关键字所对应的函数。在virtualenv环境下,是文件/lib/python2.7/site-packages/Paste-2.0.2.dist-info/metadata.json:
{ ... "extensions": { ... "python.exports": { "paste.composite_factory": { "cascade": "paste.cascade:make_cascade", "urlmap": "paste.urlmap:urlmap_factory" }, ... }
在这个文件中,你能够找到urlmap
对应的是paste.urlmap:urlmap_factory
,也就是paste/urlmap.py文件中的urlmap_factory()
函数。
composite section中其余的关键字则是urlmap_factory()
函数的参数,用于表示不一样的URL path前缀。urlmap_factory()
函数会返回一个WSGI app,其功能是根据不一样的URL path前缀,把请求路由给不一样的app。以[composite:main]为例:
[composite:main] use = egg:Paste#urlmap /v2.0 = public_api # /v2.0 开头的请求会路由给public_api处理 /v3 = api_v3 # /v3 开头的请求会路由个api_v3处理 / = public_version_api # / 开头的请求会路由给public_version_api处理
路由的对象其实就是paste.ini中其余secion的名字,类型必须是app或者pipeline。
section pipeline
pipeline是把filter和app串起来的一种section。它只有一个关键字就是pipeline。咱们以api_v3这个pipeline为例:
[pipeline:api_v3] # The last item in this pipeline must be service_v3 or an equivalent # application. It cannot be a filter. pipeline = sizelimit url_normalize request_id build_auth_context token_auth admin_token_auth json_body ec2_extension_v3 s3_extension simple_cert_extension revoke_extension federation_extension oauth1_extension endpoint_filter_extension endpoint_policy_extension service_v3
pipeline关键字指定了不少个名字,这些名字也是paste.ini文件中其余section的名字。请求会从最前面的section开始处理,一直向后传递。pipeline指定的section有以下要求:
最后一个名字对应的section必定要是一个app
非最后一个名字对应的section必定要是一个filter
section filter
filter是用来过滤请求和响应的,以WSGI中间件的方式实现。
[filter:sizelimit] paste.filter_factory = oslo_middleware.sizelimit:RequestBodySizeLimiter.factory
这个是api_v3这个pipeline指定的第一个filter,做用是限制请求的大小。其中的paste.filter_factory表示调用哪一个函数来得到这个filter中间件。
section app
app表示实现主要功能的应用,是一个标准的WSGI application。
[app:service_v3] paste.app_factory = keystone.service:v3_app_factory
paste.app_factory表示调用哪一个函数来得到这个app。
总结一下
paste.ini中这一大堆配置的做用就是把咱们用Python写的WSGI application和middleware串起来,规定好HTTP请求处理的路径。
name是用来肯定入口的
上面咱们提到了一个问题,就是name
变量的做用究竟是什么?name
变量表示paste.ini中一个section的名字,指定这个section做为HTTP请求处理的第一站。在Keystone的paste.ini中,请求必须先由[composite:main]或者[composite:admin]处理,因此在keystone项目中,name
的值必须是main或者admin。
上面提到的httpd/keystone.py文件中,name等于文件名的basename,因此实际部署中,必须把keystone.py重命名为main.py或者admin.py。
举个例子
通常状况下,从Keystone服务获取一个token时,会使用下面这个API:
POST http://hostname:35357/v3/auth/tokens
咱们根据Keystone的paste.ini来讲明这个API是如何被处理的:
hostname:35357 这一部分是由Web服务器处理的,好比Apache。而后,请求会被转到WSGI的入口,也就是httpd/keystone.py中的application
对象取处理。
application
对象是根据paste.ini中的配置来处理的。这里会先由[composite:admin]来处理(通常是admin监听35357端口,main监听5000端口)。
[composite:admin]发现请求的path是/v3开头的,因而就把请求转发给[pipeline:api_v3]去处理,转发以前,会把/v3这个部分去掉。
[pipeline:api_v3]收到请求,path是/auth/tokens,而后开始调用各个filter来处理请求。最后会把请求交给[app:service_v3]进行处理。
[app:service_v3]收到请求,path是/auth/tokens,而后交给最终的WSGI app去处理。
下一步
到此为止,paste.ini中的配置的全部工做都已经作完了。下面请求就要转移到最终的app内部去处理了。前面已经说过了,咱们的重点是肯定URL路由,那么如今还有一部分的path的路由还没肯定,/auth/tokens,这个还须要下一步的工做。
上面咱们提到paste.ini中用到了许多的WSGI中间件,那么这些中间件是如何实现的呢?咱们来看一个例子就知道了。
[filter:build_auth_context] paste.filter_factory = keystone.middleware:AuthContextMiddleware.factory
build_auth_context这个中间件的做用是在WSGI的environ中添加KEYSTONE_AUTH_CONTEXT这个键,包含的内容是认证信息的上下文。实现这个中间件的类继承关系以下:
keystone.middleware.core.AuthContextMiddleware -> keystone.common.wsgi.Middleware -> keystone.common.wsgi.Application -> keystone.common.wsgi.BaseApplication
这里实现的关键主要在前面两个类中。
keystone.common.wsgi.Middleware类实现了__call__()
方法,这个就是WSGI中application端被调用时运行的方法。
class Middleware(Application): ... @webob.dec.wsgify() 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 exceptin.Error as e: ... ...
__call__()方法实现为接收一个request对象,返回一个response对象的形式,而后使用WebOB模块的装饰器webob.dec.wsgify()
将它变成标准的WSGI application接口。这里的request和response对象分别是 webob.Request
和webob.Response
。这里,__call__()
方法内部调用了self.process_request()
方法,这个方法在keystone.middleware.core.AuthContextMiddleware中实现:
class AuthContextMiddleware(wsgi.Middleware): ... def process_request(self, request): ... request.environ[authorization.AUTH_CONTEXT_ENV] = auth_context
这个函数会根据功能设计建立auth_context
,而后赋值给request.environ['KEYSTONE_AUTH_CONTEXT
]`,这样就能经过WSGI application方法的environ传递到下一个WSGI application中去了。
上面咱们已经看到了,对于/v3开头的请求,在paste.ini中会被路由到[app:service_v3]这个section,会交给keystone.service:v3_app_factory
这个函数生成的application处理。最后这个application须要根据URL path中剩下的部分,/auth/tokens,来实现URL路由。从这里开始,就须要用到Routes模块了。
一样因为篇幅限制,咱们只能展现Routes模块的大概用法。Routes模块是用Python实现的相似Rails的URL路由系统,它的主要功能就是把path映射到对应的动做。
Routes模块的通常用法是建立一个Mapper
对象,而后调用该对象的connect()
方法把path和method映射到一个controller的某个action上,这里controller是一个自定义的类实例,action是表示controller对象的方法的字符串。通常调用的时候还会指定映射哪些方法,好比GET或者POST之类的。
举个例子,来看下keystone/auth/routers.py的代码:
class Routers(wsgi.RoutersBase): def append_v3_routers(self, mapper, routers): auth_controller = controllers.Auth() self._add_resource( mapper, auth_controller, path='/auth/tokens', get_action='validate_token', head_action='check_token', post_action='authenticate_for_token', delete_action='revoke_token', rel=json_home.build_v3_resource_relation('auth_tokens')) ...
这里调用了本身Keystone本身封装的_add_resource()方法批量为一个/auth/tokens这个path添加多个方法的处理函数。其中,controller是一个controllers.Auth实例,也就是 keystone.auth.controllers.Auth。其余的参数,咱们从名称能够猜出其做用是指定对应方法的处理函数,好比get_action用于指定GET方法的处理函数为validate_token。咱们再深刻一下,看下_add_resource()这个方法的实现:
def _add_resource(self, mapper, controller, path, rel, get_action=None, head_action=None, get_head_action=None, put_action=None, post_action=None, patch_action=None, delete_action=None, get_post_action=None, path_vars=None, status=json_home.Status.STABLE): ... if get_action: getattr(controller, get_action) # ensure the attribute exists mapper.connect(path, controller=controller, action=get_action, conditions=dict(method=['GET'])) ...
这个函数其实很简单,就是调用mapper对象的connect方法指定一个path的某些method的处理函数。
Keystone项目把每一个功能都分到单独的目录下,好比token相关的功能是放在keystone/token/目录下,assignment相关的功能是放在keystone/assignment/目录下。目录下都通常会有三个文件:routers.py, controllers.py, core.py。routers.py中实现了URL路由,把URL和controllers.py中的action对应起来;controllers.py中的action调用core.py中的底层接口实现RESTful API承诺的功能。因此,咱们要进一步肯定URL路由是如何作的,就要看routers.py文件。
注意,这个只是Keystone项目的结构,其余项目即便用了一样的框架,也不必定是这么作的。
每一个模块都定义了本身的路由,可是这些路由最终要仍是要经过一个WSGI application来调用的。上面已经提到了,在Keystone中,/v3开头的请求最终都会交给keystone.service.v3_app_factory
这个函数生成的application来处理。这个函数里也包含了路由最后分发的秘密,咱们来看代码:
def v3_app_factory(global_conf, **local_conf): ... mapper = routes.Mapper() ... router_modules = [auth, assignment, catalog, credential, identity, policy, resource] ... for module in router_modules: routers_instance = module.routers.Routers() _routers.append(routers_instance) routers_instance.append_v3_routers(mapper, sub_routers) # Add in the v3 version api sub_routers.append(routers.VersionV3('public', _routers)) return wsgi.ComposingRouter(mapper, sub_routers)
v3_app_factory()
函数中先遍历了全部的模块,将每一个模块的路由都添加到同一个mapper对象中,而后把mapper对象做为参数用于初始化wsgi.ComposingRouter对象,因此咱们能够判断,这个wsgi.ComposingRouter对象必定是一个WSGI application,咱们看看代码就知道了:
class Router(object): """WSGI middleware that maps incoming requests to WSGI apps.""" def __init__(self, mapper): self.map = mapper self._router = routes.middleware.RoutesMiddleware(self._dispatch, self.map) @webob.dec.wsgify() def __call__(self, req): return self._router ... class ComposingRouter(Router): def __init__(self, mapper=None, routers=None): ...
上述代码证明了咱们的猜想。这个ComposingRouter对象被调用时(在其父类Router中实现),会返回一个WSGI application。这个application中则使用了routes模块的中间件来实现了请求路由,在routes.middleware.RoutesMiddleware中实现。这里对path进行路由的结果就是返回各个模块的controllers.py中定义的controller。各个模块的controller都是一个WSGI application,这个你能够经过这些controller的类继承关系看出来。
可是这里只讲到了,routes模块把path映射到了一个controller,可是如何把对path的处理映射到controller的方法呢?这个能够从controller的父类keystone.common.wsgi.Application的实现看出来。这个Application类中使用了environ['wsgiorg.routing_args']
中的数据来肯定调用controller的哪一个方法,这些数据是由上面提到的routes.middleware.RoutesMiddleware设置的。
到这里咱们大概把Paste + PasteDeploy + Routes + WebOb这个框架的流程讲了一遍,从本文的长度你就能够看出这个框架有多啰嗦,用起来有多麻烦。下一篇文章咱们会讲Pecan框架,咱们的demo也将会使用Pecan框架来开发。
本文主要提到了Python Paste中的各类库,这些库的相关文档均可以在项目官网找到:http://pythonpaste.org/。
另外,routes库的项目官网是:https://routes.readthedocs.org/en/latest/