通过demo学习OpenStack开发--API服务(2)

2015 年 12 月 29 日

编者按:《通过 demo 学习 OpenStack 开发》专栏是刘陈泓的系列文章,专栏通过开发一个 demo 的形式来介绍一些参与 OpenStack 项目开发的必要的基础知识,希望帮助大家入门企业级 Python 项目的开发和 OpenStack 项目的开发。刘陈泓主要关注 OpenStack 的身份认证和计费领域。另外,还对云计算、分布式系统应用和开发感兴趣。

本文会重点讲解 OpenStack 中使用的 API 开发框架的使用。但是本文的目的并不是覆盖这些框架的使用细节,而是通过说明重要的部分,降低初学者的入门的门槛。框架的使用细节都可以从文档中找到。说明一下,除非特殊说明,本文中的相对路径都是相对于项目源码目录的相对路径。

Paste + PasteDeploy + Routes + WebOb

我们在 API 服务 (1) 中已经提到了,这个框架只在早期开始的项目中使用,新的项目都已经转到 Pecan 框架了。但是,早期的项目都是比较核心的项目,因此我们还是要学会如何使用这个框架。我们会以 Keystone 项目为例,来说明如何阅读使用这个框架的开发的 API 代码。

重点在于确定 URL 路由

RESTful API 程序的主要特点就是 URL path 会和功能对应起来。这点从 API 文档就可以看得出来,比如用户管理的功能一般都放在 /user 这个路径下。因此,看一个 RESTful API 程序,一般都是看它实现了哪些 URL path,以及每个 path 对应了什么功能,这个一般都是由框架的 URL 路由功能负责的。所以,熟悉一个 RESTful API 程序的重点在于确定 URL 路由。本章所说的这个框架对于初学者的难点也是如何确定 URL 路由。

WSGI 入口和中间件

作为基础知识,你需要先了解一下 WSGI 的相关概念,可以参考这篇文章 WSGI 简介

WSGI 入口

在 API 服务 (1) 中提到了 WSGI 可以使用 Apache 进行部署,也可以使用 eventlet 进行部署。Keystone 项目同时提供了这两种方案的代码,也就是我们要找的 WSGI 的入口。

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对象。

keystone-all命令则是采用 _eventlet_ 来进行部署时的入口,可以从 setup.cfg 文件按中确定 keystone-all 命令的入口:

复制代码
[entry_points]
console_scripts =
keystone-all = keystone<span>.cmd</span><span>.all</span>:main
keystone-manage = keystone<span>.cmd</span><span>.manage</span>:main

从 setup.cfg 文件的 entry_points 部分可以看出,keystone-all 的入口是 _keystone/cmd/all.py_ 文件中的main()函数,这个函数的内容也很简单:

复制代码
<span><span>def</span> <span>main</span><span>()</span>:</span>
eventlet_server.run(possible_topdir)

main()函数的主要作用就是启动一个 eventlet_server,配置文件从possible_topdir中查找。因为 eventlet 的部署方式涉及到 eventlet 库的使用方法,本文不再展开说明。读者可以在学会确定 URL 路由后再回来看这个代码。下面,继续以 httpd/keystone.py 文件作为入口来说明如何阅读代码。

Paste 和 PasteDeploy

_httpd/keystone.py_ 中调用的initialize_application(name)函数载入了整个 WSGI 应用,这里主要用到了 Paste 和 PasteDeploy 库。

复制代码
<span><span>def</span> <span>initialize_application</span><span>(name)</span>:</span>
...
<span><span>def</span> <span>loadapp</span><span>()</span>:</span>
<span>return</span> keystone_service.loadapp(
<span>'config:%s'</span> % config.find_paste_config(), name)
_unused, application = common.setup_backends(
startup_application_fn=loadapp)
<span>return</span> 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 很关键

在上面的代码中我们可以看到,name这个变量从 _httpd/keystone.py_ 文件传递到initialize_application()函数,又被传递到keystone_service.loadapp()函数,最终被传递到paste.deploy.loadapp()函数。那么,这个name变量到底起什么作用呢?先把这个问题放在一边,我们后面再来解决它。

paste.ini

使用 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]
<span>use</span> = egg:Paste
/v2.<span>0</span> = public_api
/v3 = api_v3
/ = public_version_api
[composite:admin]
<span>use</span> = egg:Paste
/v2.<span>0</span> = 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_:

复制代码
{
<span>...</span>
<span>"extensions"</span>: {
<span>...</span>
<span>"python.exports"</span>: {
<span>"paste.composite_factory"</span>: {
<span>"cascade"</span>: <span>"paste.cascade:make_cascade"</span>,
<span>"urlmap"</span>: <span>"paste.urlmap:urlmap_factory"</span>
},
<span>...</span>
}

在这个文件中,你可以找到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]
<span>use</span> = egg:Paste
/v2.<span>0</span> = public_api
/v3 = api_v3
/ = public_version_api

路由的对象其实就是 paste.ini 中其他 secion 的名字,类型必须是 app 或者 pipeline。

section pipeline

pipeline 是把 filter 和 app 串起来的一种 section。它只有一个关键字就是pipeline。我们以 _api_v3_ 这个 pipeline 为例:

复制代码
[pipeline:api_v3]
<span># The last item in this pipeline must be service_v3 or an equivalent</span>
<span># application. It cannot be a filter.</span>
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<span>.filter</span>_factory = oslo_middleware<span>.sizelimit</span>:RequestBodySizeLimiter<span>.factory</span>

这个是 _api_v3_ 这个 pipeline 指定的第一个 filter,作用是限制请求的大小。其中的 paste.filter_factory 表示调用哪个函数来获得这个 filter 中间件。

section app

app 表示实现主要功能的应用,是一个标准的 WSGI application。

复制代码
[app:service_v3]
paste<span>.app</span>_factory = keystone<span>.service</span>: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:我们根据 Keystone 的 paste.ini 来说明这个 API 是如何被处理的:

  1. hostname:35357 这一部分是由 Web 服务器处理的,比如 Apache。然后,请求会被转到 WSGI 的入口,也就是 _httpd/keystone.py_ 中的application对象取处理。
  2. application对象是根据 paste.ini 中的配置来处理的。这里会先由 [composite:admin] 来处理(一般是 admin 监听 35357 端口,main 监听 5000 端口)。
  3. [composite:admin] 发现请求的 path 是 /v3 开头的,于是就把请求转发给 [pipeline:api_v3] 去处理,转发之前,会把 /v3 这个部分去掉。
  4. [pipeline:api_v3] 收到请求,path 是 /auth/tokens,然后开始调用各个 filter 来处理请求。最后会把请求交给 [app:service_v3] 进行处理。
  5. [app:service_v3] 收到请求,path 是 /auth/tokens,然后交给最终的 WSGI app 去处理。

下一步

到此为止,paste.ini 中的配置的所有工作都已经做完了。下面请求就要转移到最终的 app 内部去处理了。前面已经说过了,我们的重点是确定 URL 路由,那么现在还有一部分的 path 的路由还没确定,/auth/tokens,这个还需要下一步的工作。

中间件的实现

上面我们提到 paste.ini 中用到了许多的 WSGI 中间件,那么这些中间件是如何实现的呢?我们来看一个例子就知道了。

复制代码
[filter:build_auth_context]
paste<span>.filter</span>_factory = keystone<span>.middleware</span>:AuthContextMiddleware<span>.factory</span>

build_auth_context 这个中间件的作用是在 WSGI 的 environ 中添加 _KEYSTONE_AUTH_CONTEXT_ 这个键,包含的内容是认证信息的上下文。实现这个中间件的类继承关系如下:

复制代码
keystone<span>.middleware</span><span>.core</span><span>.AuthContextMiddleware</span>
-> keystone<span>.common</span><span>.wsgi</span><span>.Middleware</span>
-> keystone<span>.common</span><span>.wsgi</span><span>.Application</span>
-> keystone<span>.common</span><span>.wsgi</span><span>.BaseApplication</span>

这里实现的关键主要在前面两个类中。

_keystone.common.wsgi.Middleware_ 类实现了__call__()方法,这个就是 WSGI 中 application 端被调用时运行的方法。

复制代码
class Middleware(Application):
<span>...</span>
@webob.dec.wsgify()
def __call__(self, request):
<span>try</span>:
response = self.process_request(request)
<span>if</span> response:
<span>return</span> response
response = request.get_response(self.application)
<span>return</span> self.process_response(request, response)
except exceptin.Error as e:
<span>...</span>
<span>...</span>

__call__()方法实现为接收一个 request 对象,返回一个 response 对象的形式,然后使用 WebOB 模块的装饰器webob.dec.wsgify()将它变成标准的 WSGI application 接口。这里的 request 和 response 对象分别是webob.Requestwebob.Response。这里,__call__()方法内部调用了self.process_request()方法,这个方法在 _keystone.middleware.core.AuthContextMiddleware_ 中实现:

复制代码
class AuthContextMiddleware(wsgi.Middleware):
<span>...</span>
def process_request(self, request):
<span>...</span>
request.environ[authorization.AUTH_CONTEXT_ENV] = auth_context

这个函数会根据功能设计创建auth_context,然后赋值给`request.environ[‘KEYSTONE_AUTH_CONTEXT]“,这样就能通过 WSGI application 方法的 environ 传递到下一个 WSGI application 中去了。

最后的 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_ 的代码:

复制代码
<span><span>class</span> <span>Routers</span><span>(wsgi.RoutersBase)</span>:</span>
<span><span>def</span> <span>append_v3_routers</span><span>(self, mapper, routers)</span>:</span>
auth_controller = controllers.Auth()
self._add_resource(
mapper, auth_controller,
path=<span>'/auth/tokens'</span>,
get_action=<span>'validate_token'</span>,
head_action=<span>'check_token'</span>,
post_action=<span>'authenticate_for_token'</span>,
delete_action=<span>'revoke_token'</span>,
rel=json_home.build_v3_resource_relation(<span>'auth_tokens'</span>))
...

这里调用了自己 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):
<span>...</span>
<span>if</span> get_action:
getattr(controller, get_action)
mapper.connect(path, controller=controller, action=get_action,
conditions=dict(method=[<span>'GET'</span>]))
<span>...</span>

这个函数其实很简单,就是调用 mapper 对象的 connect 方法指定一个 path 的某些 method 的处理函数。

Keystone 项目的代码结构

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 项目的结构,其他项目即使用了同样的框架,也不一定是这么做的。

Keystone 中的路由汇总

每个模块都定义了自己的路由,但是这些路由最终要还是要通过一个 WSGI application 来调用的。上面已经提到了,在 Keystone 中,/v3 开头的请求最终都会交给keystone.service.v3_app_factory这个函数生成的 application 来处理。这个函数里也包含了路由最后分发的秘密,我们来看代码:

复制代码
def v3_app_factory(global_conf, **local_conf):
<span>...</span>
mapper = routes.Mapper()
<span>...</span>
router_modules = [auth,
assignment,
catalog,
credential,
identity,
policy,
resource]
<span>...</span>
<span>for</span> module <span>in</span> router_modules:
routers_instance = module.routers.Routers()
_routers.append(routers_instance)
routers_instance.append_v3_routers(mapper, sub_routers)
sub_routers.append(routers.VersionV3(<span>'public'</span>, _routers))
<span>return</span> wsgi.ComposingRouter(mapper, sub_routers)

v3_app_factory()函数中先遍历了所有的模块,将每个模块的路由都添加到同一个 mapper 对象中,然后把 mapper 对象作为参数用于初始化 _wsgi.ComposingRouter_ 对象,所以我们可以判断,这个 _wsgi.ComposingRouter_ 对象一定是一个 WSGI application,我们看看代码就知道了:

复制代码
<span><span>class</span> <span>Router</span><span>(object)</span>:</span>
<span>"""WSGI middleware that maps incoming requests to WSGI apps."""</span>
<span><span>def</span> <span>__init__</span><span>(self, mapper)</span>:</span>
self.map = mapper
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
self.map)
<span>@webob.dec.wsgify()</span>
<span><span>def</span> <span>__call__</span><span>(self, req)</span>:</span>
<span>return</span> self._router
...
<span><span>class</span> <span>ComposingRouter</span><span>(Router)</span>:</span>
<span><span>def</span> <span>__init__</span><span>(self, mapper=None, routers=None)</span>:</span>
...

上述代码证实了我们的猜测。这个 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 中的各种库,这些库的相关文档都可以在项目官网找到:。

另外,routes 库的项目官网


感谢魏星对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。

2015 年 12 月 29 日 17:343727

评论

发布
暂无评论
发现更多内容

人工智能不过尔尔,基于Python3深度学习库Keras/TensorFlow打造属于自己的聊天机器人(ChatRobot)

刘悦的技术博客

人工智能 tensorflow chatbot 聊天机器人 keras

智慧仓储管理系统,是否能解决购物狂欢节后新一轮爆仓危机?

一只数据鲸鱼

物联网 数据可视化 智慧物流 智慧仓储

没能进入大数据领域

escray

面经 面试经历 101次面试

扒开 SqlSession 的外衣

田维常

mybatis

区块链溯源平台优势,区块链溯源系统解决方案

13530558032

SGY奇点交易所系统源码开发

DV:19924636653

软件开发

智慧社区综合管理平台搭建,智慧平安城市建设

13530558032

周立齐出任电动车联合创始人:网红经济背后的病态消费心理

石头IT视角

什么是浮点数?

Kaito

计算机基础 浮点数

星域母子币系统软件开发|星域母子币APP开发

开發I852946OIIO

系统开发

如何基于 SDK 快速开发一款IoT App 控制智能灯(iOS 版)

IoT云工坊

ios App 物联网 IoT sdk

盘点 2020 | 10 天开发前台系统技术系列

老魚

CSS 前端 全栈 js 盘点2020

直播中不可缺少的一环-rtmp直播推流

anyRTC开发者

音视频 WebRTC CDN RTC RTMP

九环智能合约开发

V19927655815

APP开发

云视频技术领军人赵加雨:如何提升在线教育课堂互动体验

拍乐云Pano

音视频 在线教育 RTC 互动课堂 白板

高空立体云防控系统搭建,智能化平安小区建设方案

t13823115967

平安小区 智慧平安社区建设

应急指挥中心平台搭建,移动可视化指挥解决方案

t13823115967

可视化数据分析搭建 应急指挥

重点人员管控系统开发,情报研判系统开发

13530558032

英特尔力邀150家产业大咖推动Evo严苛认证,打造PC界的奥斯卡

intel001

快速接入 | 从 0 到 1 构建语音聊天室

拍乐云Pano

音视频 RTC 实时语音 语音聊天室 语聊房

抢先体验全新升级版Eternal Wallet!

Geek_c610c0

数字货币 数字货币钱包开发

从一个模糊词查询需求的处理方案讨论到一种极速匹配方案的实现

行如风

模糊匹配 双数组trie树 ahocorasick ac自动机 黑名单过滤

重磅|中国PostgreSQL分会与腾讯云战略合作协议签订

PostgreSQLChina

数据库 postgresql 软件 开源社区

如何通过 Serverless 轻松识别验证码?

Serverless Devs

人工智能 Serverless 云原生

一线大厂开源三份JDK+Spring+Mybatis源码笔记

Java架构追梦

Java spring 源码 jdk mybatis

限时!字节Java程序性能优化宝典开源,原来这才叫性能优化

996小迁

程序员 面试 性能优化 笔记

智能合约交易所系统开发

DV:19924636653

软件开发

为什么说rollup比webpack更适合打包库

fengxianqi

前端 Rollup webpack

从MongoID的生成讨论分布式唯一ID生成方案

行如风

雪花算法 分布式ID 全局唯一ID 流星算法

移动生态盘点与HMS生态解析

华章IT

华为 Android Studio 移动开发 HMS

字节二面跪拜“Redis源码”后,面试官直接推荐这份笔记!真是NB

比伯

Java 编程 架构 面试 程序人生

通过demo学习OpenStack开发--API服务(2)-InfoQ