编者按:《通过 demo 学习 OpenStack 开发》专栏是刘陈泓的系列文章,专栏通过开发一个 demo 的形式来介绍一些参与 OpenStack 项目开发的必要的基础知识,希望帮助大家入门企业级 Python 项目的开发和 OpenStack 项目的开发。刘陈泓主要关注 OpenStack 的身份认证和计费领域。另外,还对云计算、分布式系统应用和开发感兴趣。
上一篇文章说到,我们将以实例的形式来继续讲述这个 API 服务的开发知识,这里会使用 Pecan 和 WSME 两个库。
设计 REST API
要开发 REST API 服务,我们首先需要设计一下这个服务。设计包括要实现的功能,以及接口的具体规范。我们这里要实现的是一个简单的用户管理接口,包括增删改查等功能。如果读者对 REST API 不熟悉,可以先从 Wiki 页面了解一下。
另外,为了方便大家阅读和理解,本系列的代码会放在 github 上, diabloneo/webdemo 。
Version of REST API
在 OpenStack 的项目中,都是在 URL 中表明这个 API 的版本号的,比如 Keystone 的 API 会有 _/v2.0_ 和 _/v3_ 的前缀,表明两个不同版本的 API;Magnum 项目目前的 API 则为 v1 版本。因为我们的 webdemo 项目才刚刚开始,所以我们也把我们的 API 版本设置为 v1,下文会说明怎么实现这个 version 号的设置。
REST API of Users
我们将要设计一个管理用户的 API,这个和 Keystone 的用户管理的 API 差不多,这里先列出每个 API 的形式,以及简要的内容说明。这里我们会把上面提到的 version 号也加入到 URL path 中,让读者能更容易联系起来。
GET /v1/users 获取所有用户的列表。
POST /v1/users 创建一个用户。
GET /v1/users/ 获取一个特定用户的详细信息。
PUT /v1/users/ 修改一个用户的详细信息。
DELETE /v1/users/ 删除一个用户。
这些就是我们要实现的用户管理的 API 了。其中,表示使用一个 UUID 字符串,这个是 OpenStack 中最经常被用来作为各种资源 ID 的形式,如下所示:
<span>In</span> [<span>5</span>]: import uuid <span>In</span> [<span>6</span>]: print uuid<span>.uuid</span>4() adb92482-baab-<span>4832</span>-<span>84</span>bc-f842f3eabd66 <span>In</span> [<span>7</span>]: print uuid<span>.uuid</span>4()<span>.hex</span> <span>29520</span>c88de6b4c76ae8deb48db0a71e7
因为是个 demo,所以我们设置一个用户包含的信息会比较简单,只包含 name 和 age。
使用 Pecan 搭建 API 服务的框架
接下来就要开始编码工作了。首先要把整个服务的框架搭建起来。我们会在软件包管理这篇文件中的代码基础上继续我们的 demo(所有这些代码在 github 的仓库里都能看到)。
代码目录结构
一般来说,OpenStack 项目中,使用 Pecan 来开发 API 服务时,都会在代码目录下有一个专门的 API 目录,用来保存 API 相关的代码。比如 Magnum 项目的 _magnum/api_,或者 Ceilometer 项目的 _ceilometer/api_ 等。我们的代码也遵守这个规范,让我们直接来看下我们的代码目录结构(#后面的表示注释):
-> ~/programming/python/webdemo/webdemo/api git:(master) ✗ $ tree . . ├── app<span>.py</span> <span># 这个文件存放 WSGI application 的入口 </span> ├── config<span>.py</span> <span># 这个文件存放 Pecan 的配置 </span> ├── controllers/ <span># 这个目录用来存放 Pecan 控制器的代码 </span> ├── hooks<span>.py</span> <span># 这个文件存放 Pecan 的 hooks 代码(本文中用不到)</span> └── __init__<span>.py</span>
这个在 API 服务 (3) 这篇文章中已经说明过了。
先让我们的服务跑起来
为了后面更好的开发,我们需要先让我们的服务在本地跑起来,这样可以方便自己做测试,看到代码的效果。不过要做到这点,还是有些复杂的。
必要的代码
首先,先创建 config.py 文件的内容:
app = { <span>'root'</span>: <span>'webdemo.api.controllers.root.RootController'</span>, <span>'modules'</span>: [<span>'webdemo.api'</span>], <span>'debug'</span>: <span>False</span>, }
就是包含了 Pecan 的最基本配置,其中指定了 root controller 的位置。然后看下 app.py 文件的内容,主要就是读取 config.py 中的配置,然后创建一个 WSGI application:
<span>import</span> pecan <span>from</span> webdemo.api <span>import</span> config <span>as</span> api_config <span><span>def</span> <span>get_pecan_config</span><span>()</span>:</span> filename = api_config.__file__.replace(<span>'.pyc'</span>, <span>'.py'</span>) <span>return</span> pecan.configuration.conf_from_file(filename) <span><span>def</span> <span>setup_app</span><span>()</span>:</span> config = get_pecan_config() app_conf = dict(config.app) app = pecan.make_app( app_conf.pop(<span>'root'</span>), logging=getattr(config, <span>'logging'</span>, {}), **app_conf ) <span>return</span> app
然后,我们至少还需要实现一下 root controller,也就是 _webdemo/api/controllers/root.py_ 这个文件中的RootController
类:
<span>from</span> pecan <span>import</span> rest <span>from</span> wsme <span>import</span> types <span>as</span> wtypes <span>import</span> wsmeext.pecan <span>as</span> wsme_pecan <span><span>class</span> <span>RootController</span><span>(rest.RestController)</span>:</span> <span>@wsme_pecan.wsexpose(wtypes.text)</span> <span><span>def</span> <span>get</span><span>(self)</span>:</span> <span>return</span> <span>"webdemo"</span>
本地测试服务器
为了继续开放的方便,我们要先创建一个 Python 脚本,可以启动一个单进程的 API 服务。这个脚本会放在 _webdemo/cmd/_ 目录下,名称是api.py(这目录和脚本名称也是惯例),来看看我们的 api.py 吧:
<span>from</span> wsgiref <span>import</span> simple_server <span>from</span> webdemo.api <span>import</span> app <span><span>def</span> <span>main</span><span>()</span>:</span> host = <span>'0.0.0.0'</span> port = <span>8080</span> application = app.setup_app() srv = simple_server.make_server(host, port, application) srv.serve_forever() <span>if</span> __name__ == <span>'__main__'</span>: main()
运行测试服务器的环境
要运行这个测试服务器,首先需要安装必要的包,并且设置正确的路径。在后面的文章中,我们将会知道,这个可以通过 tox 这个工具来实现。现在,我们先做个简单版本的,就是手动创建这个运行环境。
首先,完善一下requirements.txt这个文件,包含我们需要的包:
<span>pbr<2.0,></span>=<span>0.11 pecan WSME</span>
然后,我们手动创建一个 virtualenv 环境,并且安装 requirements.txt 中要求的包:
-> ~/programming/python/webdemo git:(master) ✗ $ virtualenv .venv New python executable in .venv/bin/python Installing setuptools, pip, wheel...done. -> ~/programming/python/webdemo git:(master) ✗ $ source .venv/bin/activate (.venv) -> ~/programming/python/webdemo git:(master) ✗ $ pip install -r requirement.txt ... Successfully installed Mako-1.0.3 MarkupSafe-0.23 WSME-0.8.0 WebOb- 1.5.1 WebTest-2.0.20 beautifulsoup4-4.4.1 logutils-0.3.3 netaddr- 0.7.18 pbr-1.8.1 pecan-1.0.3 pytz-2015.7 simplegeneric-0.8.1 singledispatch-3.4.0.3 six-1.10.0 waitress-0.8.10
启动我们的服务
启动服务需要技巧,因为我们的 webdemo 还没有安装到系统的 Python 路径中,也不在上面创建 virtualenv 环境中,所以我们需要通过指定PYTHONPATH这个环境变量来为 Python 程序增加库的查找路径:
(.venv)-> ~/programming/python/webdemo git:(master) ✗ $ PYTHONPATH=. python webdemo/cmd/api.py
现在测试服务器已经起来了,可以通过浏览器访问 http://localhost:8080/ 这个地址来查看结果。(你可能会发现,返回的是 XML 格式的结果,而我们想要的是 JSON 格式的。这个是 WSME 的问题,我们后面再来处理)。
到这里,我们的 REST API 服务的框架已经搭建完成,并且测试服务器也跑起来了。
用户管理 API 的实现
现在我们来实现我们在第一章设计的 API。这里先说明一下:我们会直接使用 Pecan 的 RestController 来实现 REST API,这样可以不用为每个接口指定接受的 method。
让 API 返回 JSON 格式的数据
现在,所有的 OpenStack 项目的 REST API 的返回格式都是使用 JSON 标准,所以我们也要这么做。那么有什么办法能够让 WSME 框架返回 JSON 数据呢?可以通过设置wsmeext.pecan.wsexpose()
的rest_content_types
参数来是先。这里,我们借鉴一段 Magnum 项目中的代码,把这段代码存放在文件 _webdemo/api/expose.py_ 中:
<span>import</span> wsmeext.pecan <span>as</span> wsme_pecan <span><span>def</span> <span>expose</span><span>(*args, **kwargs)</span>:</span> <span>"""Ensure that only JSON, and not XML, is supported."""</span> <span>if</span> <span>'rest_content_types'</span> <span>not</span> <span>in</span> kwargs: kwargs[<span>'rest_content_types'</span>] = (<span>'json'</span>,) <span>return</span> wsme_pecan.wsexpose(*args, **kwargs)
这样我们就封装了自己的expose
装饰器,每次都会设置响应的 content-type 为 JSON。上面的 root controller 代码也就可以修改为:
<span>from</span> pecan <span>import</span> rest <span>from</span> wsme <span>import</span> types <span>as</span> wtypes <span>from</span> webdemo.api <span>import</span> expose <span><span>class</span> <span>RootController</span><span>(rest.RestController)</span>:</span> <span>@expose.expose(wtypes.text)</span> <span><span>def</span> <span>get</span><span>(self)</span>:</span> <span>return</span> <span>"webdemo"</span>
再次运行我们的测试服务器,就可以返现返回值为 JSON 格式了。
实现 GET /v1
这个其实就是实现 v1 这个版本的 API 的路径前缀。在 Pecan 的帮助下,我们很容易实现这个,只要按照如下两步做即可:
- 先实现 v1 这个 controller
- 把 v1 controller 加入到 root controller 中
按照 OpenStack 项目的规范,我们会先建立一个 _webdemo/api/controllers/v1/_ 目录,然后将 v1 controller 放在这个目录下的一个文件中,假设我们就放在 _v1/controller.py_ 文件中,效果如下:
<span>from</span> pecan <span>import</span> rest <span>from</span> wsme <span>import</span> types <span>as</span> wtypes <span>from</span> webdemo.api <span>import</span> expose <span><span>class</span> <span>V1Controller</span><span>(rest.RestController)</span>:</span> <span>@expose.expose(wtypes.text)</span> <span><span>def</span> <span>get</span><span>(self)</span>:</span> <span>return</span> <span>'webdemo v1controller'</span>
然后把这个 controller 加入到 root controller 中:
... <span>from</span> webdemo.api.controllers.v1 <span>import</span> controller <span>as</span> v1_controller <span>from</span> webdemo.api <span>import</span> expose <span><span>class</span> <span>RootController</span><span>(rest.RestController)</span>:</span> v1 = v1_controller.V1Controller() <span>@expose.expose(wtypes.text)</span> <span><span>def</span> <span>get</span><span>(self)</span>:</span> <span>return</span> <span>"webdemo"</span>
此时,你访问http://localhost:8080/v1
就可以看到结果了。
实现 GET /v1/users
添加 users controller
这个 API 就是返回所有的用户信息,功能很简单。首先要添加 users controller 到上面的 v1 controller 中。为了不影响阅读体验,这里就不贴代码了,请看 github 上的示例代码。
使用 WSME 来规范 API 的响应值
上篇文章中,我们已经提到了 WSME 可以用来规范 API 的请求和响应的值,这里我们就要用上它。首先,我们要参考 OpenStack 的惯例来设计这个 API 的返回值:
{ "<span>users</span>": <span>[ { "<span>name</span>": <span><span>"Alice"</span></span>, "<span>age</span>": <span><span>30</span> </span>}, { "<span>name</span>": <span><span>"Bob"</span></span>, "<span>age</span>": <span><span>40</span> </span>} ] </span>}
其中 users 是一个列表,列表中的每个元素都是一个 user。那么,我们要如何使用 WSME 来规范我们的响应值呢?答案就是使用WSME的自定义类型。我们可以利用 WSME 的类型功能定义出一个 user 类型,然后再定义一个 user 的列表类型。最后,我们就可以使用上面的 expose 方法来规定这个 API 返回的是一个 user 的列表类型。
定义 user 类型和 user 列表类型
这里我们需要用到 WSME 的 Complex types 的功能,请先看一下文档 Types 。简单说,就是我们可以把 WSME 的基本类型组合成一个复杂的类型。我们的类型需要继承自wsme.types.Base
这个类。因为我们在本文只会实现一个 user 相关的 API,所以这里我们把所有的代码都放在 _webdemo/api/controllers/v1/users.py_ 文件中。来看下和 user 类型定义相关的部分:
<span>from</span> wsme <span>import</span> types <span>as</span> wtypes <span><span>class</span> <span>User</span><span>(wtypes.Base)</span>:</span> name = wtypes.text age = int <span><span>class</span> <span>Users</span><span>(wtypes.Base)</span>:</span> users = [User]
这里我们定义了class User
,表示一个用户信息,包含两个字段,name 是一个文本,age 是一个整型。class Users
表示一组用户信息,包含一个字段 users,是一个列表,列表的元素是上面定义的class User
。完成这些定义后,我们就使用 WSME 来检查我们的 API 是否返回了合格的值;另一方面,只要我们的 API 返回了这些类型,那么就能通过 WSME 的检查。我们先来完成利用 WSME 来检查 API 返回值的代码:
<span><span>class</span> <span>UsersController</span><span>(rest.RestController)</span>:</span> <span>@expose.expose(Users)</span> <span><span>def</span> <span>get</span><span>(self)</span>:</span> <span>pass</span>
这样就完成了 API 的返回值检查了。
实现 API 逻辑
我们现在来完成 API 的逻辑部分。不过为了方便大家理解,我们直接返回一个写好的数据,就是上面贴出来的那个。
<span><span>class</span> <span>UsersController</span><span>(rest.RestController)</span>:</span> <span>@expose.expose(Users)</span> <span><span>def</span> <span>get</span><span>(self)</span>:</span> user_info_list = [ { <span>'name'</span>: <span>'Alice'</span>, <span>'age'</span>: <span>30</span>, }, { <span>'name'</span>: <span>'Bob'</span>, <span>'age'</span>: <span>40</span>, } ] users_list = [User(**user_info) <span>for</span> user_info <span>in</span> user_info_list] <span>return</span> Users(users=users_list)
代码中,会先根据 user 信息生成 User 实例的列表users_list
,然后再生成 Users 实例。此时,重启测试服务器后,你就可以从浏览器访问http://localhost:8080/v1/users
,就能看到结果了。
实现 POST /v1/users
这个 API 会接收用户上传的一个 JSON 格式的数据,然后打印出来(实际中一般是存到数据库之类的),要求用户上传的数据符合 User 类型的规范,并且返回的状态码为 201。代码如下:
<span><span>class</span> <span>UsersController</span><span>(rest.RestController)</span>:</span> <span>@expose.expose(None, body=User, status_code=201)</span> <span><span>def</span> <span>post</span><span>(self, user)</span>:</span> <span>print</span> user
可以使用 curl 程序来测试:
~/programming/python/webdemo git:(master) -> $ curl -X POST http://localhost:8080/v1/users -H "Content-Type: application/json" -d '{"name": "Cook", "age": 50}' -v * Trying <span>127.0</span><span>.0</span><span>.1</span><span>...</span> * Connected to localhost (<span>127.0</span><span>.0</span><span>.1</span>) port <span>8080</span> ( > POST /v1/users HTTP/<span>1.1</span> > Host: localhost:<span>8080</span> > User-Agent: curl/<span>7.43</span><span>.0</span> > Accept: */* > Content-Type: application/json > Content-Length: <span>27</span> > * upload completely sent off: <span>27</span> out of <span>27</span> bytes * HTTP <span>1.0</span>, assume close after body < HTTP/<span>1.0</span> <span>201</span> Created < Date: Mon, <span>16</span> Nov <span>2015</span> <span>15</span>:<span>18</span>:<span>24</span> GMT < Server: WSGIServer/<span>0.1</span> Python/<span>2.7</span><span>.10</span> < Content-Length: <span>0</span> < * Closing connection <span>0</span>
同时,服务器上也会打印出:
<span>127.0</span><span>.0</span><span>.1</span> - - [<span>16</span>/Nov/<span>2015</span> <span>23</span>:<span>16</span>:<span>28</span>] <span>"POST /v1/users HTTP/1.1"</span> <span>201</span> <span>0</span> <webdemo<span>.api</span><span>.controllers</span><span>.v</span>1<span>.users</span><span>.User</span> object at <span>0x7f65e058d550</span>>
我们用 3 行代码就实现了这个 POST 的逻辑。现在来说明一下这里的秘密。expose
装饰器的第一个参数表示这个方法没有返回值;第三个参数表示这个 API 的响应状态码是 201,如果不加这个参数,在没有返回值的情况下,默认会返回 204。第二个参数要说明一下,这里用的是body=User
,你也可以直接写User
。使用body=User
这种形式,你可以直接发送符合User
规范的 JSON 字符串;如果是用expose(None, User, status_code=201)
那么你需要发送下面这样的数据:
{ "<span>user</span>": <span>{"<span>name</span>": <span><span>"Cook"</span></span>, "<span>age</span>": <span><span>50</span></span>} </span>}
你可以自己测试一下区别。要更多的了解本节提到的 expose 参数,请参考 WSM 文档 Functions 。
最后,你接收到一个创建用户请求时,一般会为这个用户分配一个 id。本文前面已经提到了 OpenStack 项目中一般使用 UUID。你可以修改一下上面的逻辑,为每个用户分配一个 UUID。
实现 GET /v1/users/
要实现这个 API,需要两个步骤:
- 在 UsersController 中解析出的部分,然后把请求传递给这个一个新的 UserController。从命名可以看出,UsersController 是针对多个用户的,UserController 是针对一个用户的。
- 在 UserController 中实现
get()
方法。
使用 _lookup() 方法
Pecan 的_lookup()
方法是 controller 中的一个特殊方法,Pecan 会在特定的时候调用这个方法来实现更灵活的 URL 路由。Pecan 还支持用户实现_default()
和_route()
方法。这些方法的具体说明,请阅读 Pecan 的文档: routing 。
我们这里只用到_lookup()
方法,这个方法会在 controller 中没有其他方法可以执行且没有_default()
方法的时候执行。比如上面的 UsersController 中,没有定义 _/v1/users/_ 如何处理,它只能返回 404;如果你定义了_lookup()
方法,那么它就会调用该方法。
_lookup()
方法需要返回一个元组,元组的第一个元素是下一个 controller 的实例,第二个元素是 URL path 中剩余的部分。
在这里,我们就需要在_lookup()
方法中解析出 UUID 的部分并传递给新的 controller 作为新的参数,并且返回剩余的 URL path。来看下代码:
<span><span>class</span> <span>UserController</span><span>(rest.RestController)</span>:</span> <span><span>def</span> <span>__init__</span><span>(self, user_id)</span>:</span> self.user_id = user_id <span><span>class</span> <span>UsersController</span><span>(rest.RestController)</span>:</span> <span>@pecan.expose()</span> <span><span>def</span> <span>_lookup</span><span>(self, user_id, *remainder)</span>:</span> <span>return</span> UserController(user_id), remainder
_lookup()
方法的形式为_lookup(self, user_id, *remainder)
,意思就是会把 _/v1/users/_ 中的部分作为user_id
这个参数,剩余的按照”/”分割为一个数组参数(这里 remainder 为空)。然后,_lookup()
方法里会初始化一个 UserController 实例,使用user_id
作为初始化参数。这么做之后,这个初始化的控制器就能知道是要查找哪个用户了。然后这个控制器会被返回,作为下一个控制被调用。请求的处理流程就这么转移到 UserController 中了。
实现 API 逻辑
实现前,我们要先修改一下我们返回的数据,里面需要增加一个 id 字段。对应的 User 定义如下:
<span>class</span> User(wtypes.Base): <span>id</span> = wtypes.<span>text</span> <span>name</span> = wtypes.<span>text</span> age = int
现在,完整的 UserController 代码如下:
<span><span>class</span> <span>UserController</span><span>(rest.RestController)</span>:</span> <span><span>def</span> <span>__init__</span><span>(self, user_id)</span>:</span> self.user_id = user_id <span>@expose.expose(User)</span> <span><span>def</span> <span>get</span><span>(self)</span>:</span> user_info = { <span>'id'</span>: self.user_id, <span>'name'</span>: <span>'Alice'</span>, <span>'age'</span>: <span>30</span>, } <span>return</span> User(**user_info)
使用 curl 来检查一下效果:
~/programming/python/webdemo git:(master) ✗ $ curl http://localhost:8080/v1/users/29520c88de6b4c76ae8deb48db0a71e7 {"age": 30, "id": "29520c88de6b4c76ae8deb48db0a71e7", "name": "Alice"}
定义 WSME 类型的技巧
你可能会有疑问:这里我们修改了 User 类型,增加了一个 id 字段,那么前面实现的 POST /v1/users 会不会失效呢?你可以自己测试一下。(答案是不会,因为这个类型里的字段都是可选的)。这里顺便讲两个技巧。
如何设置一个字段为强制字段
像下面这样做就可以了(你可以测试一下,改成这样后,不传递 id 的 _POST /v1/users_ 会失败):
<span><span>class</span> <span>User</span><span>(<span>wtypes</span>.<span>Base</span>)</span>: id = wtypes.wsattr<span>(<span>wtypes</span>.<span>text</span>, <span>mandatory</span>=<span>True</span>)</span> name = wtypes.text age = int</span>
如何检查一个可选字段的值是否存在
检查这个值是否为 None 是肯定不行的,需要检查这个值是否为wsme.Unset。
实现 PUT /v1/users/
这个和上一个 API 一样,不过_lookup()
方法已经实现过了,直接添加方法到 UserController 中即可:
<span><span>class</span> <span>UserController</span><span>(rest.RestController)</span>:</span> <span>@expose.expose(User, body=User)</span> <span><span>def</span> <span>put</span><span>(self, user)</span>:</span> user_info = { <span>'id'</span>: self.user_id, <span>'name'</span>: user.name, <span>'age'</span>: user.age + <span>1</span>, } <span>return</span> User(**user_info)
通过 curl 来测试:
-> ~/programming/python/webdemo git:(master) ✗ $ curl -X PUT http://localhost:8080/v1/users/29520c88de6b4c76ae8deb48db0a71e7 -H "Content-Type: application/json" -d '{"name": "Cook", "age": 50}' {"age": 51, "id": "29520c88de6b4c76ae8deb48db0a71e7", "name": "Cook"}%
实现 DELETE /v1/users/
同上,没有什么新的内容:
<span><span>class</span> <span>UserController</span><span>(rest.RestController)</span>:</span> <span>@expose.expose()</span> <span><span>def</span> <span>delete</span><span>(self)</span>:</span> <span>print</span> <span>'Delete user_id: %s'</span> % self.user_id
总结
到此为止,我们已经完成了我们的 API 服务了,虽然没有实际的逻辑,但是本文搭建起来的框架也是 OpenStack 中 API 服务的一个常用框架,很多大项目的 API 服务代码都和我们的 webdemo 长得差不多。最后再说一下,本文的代码在 github 上托管着: diabloneo/webdemo 。
现在我们已经了解了包管理和 API 服务了,那么接下来就要开始数据库相关的操作了。大部分 OpenStack 的项目都是使用非常著名的 sqlalchemy 库来实现数据库操作的,本系列接下来的文章就是要来说明数据库的相关知识和应用。
感谢魏星对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。
评论