经过demo学习OpenStack开发所需的基础知识 -- API服务(4)

上一篇文章说到,咱们将以实例的形式来继续讲述这个API服务的开发知识,这里会使用Pecan和WSME两个库。html

设计REST API

要开发REST API服务,咱们首先须要设计一下这个服务。设计包括要实现的功能,以及接口的具体规范。咱们这里要实现的是一个简单的用户管理接口,包括增删改查等功能。若是读者对REST API不熟悉,能够先从Wiki页面了解一下。python

另外,为了方便你们阅读和理解,本系列的代码会放在github上,diabloneo/webdemogit

Version of REST API

在OpenStack的项目中,都是在URL中代表这个API的版本号的,好比Keystone的API会有/v2.0/v3的前缀,代表两个不一样版本的API;Magnum项目目前的API则为v1版本。由于咱们的webdemo项目才刚刚开始,因此咱们也把咱们的API版本设置为v1,下文会说明怎么实现这个version号的设置。github

REST API of Users

咱们将要设计一个管理用户的API,这个和Keystone的用户管理的API差很少,这里先列出每一个API的形式,以及简要的内容说明。这里咱们会把上面提到的version号也加入到URL path中,让读者能更容易联系起来。web

GET /v1/users 获取全部用户的列表。sql

POST /v1/users 建立一个用户数据库

GET /v1/users/<UUID> 获取一个特定用户的详细信息。json

PUT /v1/users/<UUID> 修改一个用户的详细信息。segmentfault

DELETE /v1/users/<UUID> 删除一个用户。api

这些就是咱们要实现的用户管理的API了。其中,<UUID>表示使用一个UUID字符串,这个是OpenStack中最常常被用来做为各类资源ID的形式,以下所示:

In [5]: import uuid
In [6]: print uuid.uuid4()
adb92482-baab-4832-84bc-f842f3eabd66
In [7]: print uuid.uuid4().hex
29520c88de6b4c76ae8deb48db0a71e7

由于是个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.py           # 这个文件存放WSGI application的入口
├── config.py        # 这个文件存放Pecan的配置
├── controllers/     # 这个目录用来存放Pecan控制器的代码
├── hooks.py         # 这个文件存放Pecan的hooks代码(本文中用不到)
└── __init__.py

这个在API服务(3)这篇文章中已经说明过了。

先让咱们的服务跑起来

为了后面更好的开发,咱们须要先让咱们的服务在本地跑起来,这样能够方便本身作测试,看到代码的效果。不过要作到这点,仍是有些复杂的。

必要的代码

首先,先建立config.py文件的内容:

app = {
    'root': 'webdemo.api.controllers.root.RootController',
    'modules': ['webdemo.api'],
    'debug': False,
}

就是包含了Pecan的最基本配置,其中指定了root controller的位置。而后看下app.py文件的内容,主要就是读取config.py中的配置,而后建立一个WSGI application:

import pecan

from webdemo.api import config as api_config


def get_pecan_config():
    filename = api_config.__file__.replace('.pyc', '.py')
    return pecan.configuration.conf_from_file(filename)


def setup_app():
    config = get_pecan_config()

    app_conf = dict(config.app)
    app = pecan.make_app(
        app_conf.pop('root'),
        logging=getattr(config, 'logging', {}),
        **app_conf
    )

    return app

而后,咱们至少还须要实现一下root controller,也就是webdemo/api/controllers/root.py这个文件中的RootController类:

from pecan import rest
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan


class RootController(rest.RestController):

    @wsme_pecan.wsexpose(wtypes.text)
    def get(self):
        return "webdemo"

本地测试服务器

为了继续开放的方便,咱们要先建立一个Python脚本,能够启动一个单进程的API服务。这个脚本会放在webdemo/cmd/目录下,名称是api.py(这目录和脚本名称也是惯例),来看看咱们的api.py吧:

from wsgiref import simple_server

from webdemo.api import app


def main():
    host = '0.0.0.0'
    port = 8080

    application = app.setup_app()
    srv = simple_server.make_server(host, port, application)

    srv.serve_forever()


if __name__ == '__main__':
    main()

运行测试服务器的环境

要运行这个测试服务器,首先须要安装必要的包,而且设置正确的路径。在后面的文章中,咱们将会知道,这个能够经过tox这个工具来实现。如今,咱们先作个简单版本的,就是手动建立这个运行环境。

首先,完善一下requirements.txt这个文件,包含咱们须要的包:

pbr<2.0,>=0.11
pecan
WSME

而后,咱们手动建立一个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中:

import wsmeext.pecan as wsme_pecan


def expose(*args, **kwargs):
    """Ensure that only JSON, and not XML, is supported."""
    if 'rest_content_types' not in kwargs:
        kwargs['rest_content_types'] = ('json',)

    return wsme_pecan.wsexpose(*args, **kwargs)

这样咱们就封装了本身的expose装饰器,每次都会设置响应的content-type为JSON。上面的root controller代码也就能够修改成:

from pecan import rest
from wsme import types as wtypes

from webdemo.api import expose


class RootController(rest.RestController):

    @expose.expose(wtypes.text)
    def get(self):
        return "webdemo"

再次运行咱们的测试服务器,就能够返现返回值为JSON格式了。

实现 GET /v1

这个其实就是实现v1这个版本的API的路径前缀。在Pecan的帮助下,咱们很容易实现这个,只要按照以下两步作便可:

  • 先实现v1这个controller

  • 把v1 controller加入到root controller中

按照OpenStack项目的规范,咱们会先创建一个webdemo/api/controllers/v1/目录,而后将v1 controller放在这个目录下的一个文件中,假设咱们就放在v1/controller.py文件中,效果以下:

from pecan import rest
from wsme import types as wtypes

from webdemo.api import expose


class V1Controller(rest.RestController):

    @expose.expose(wtypes.text)
    def get(self):
        return 'webdemo v1controller'

而后把这个controller加入到root controller中:

...
from webdemo.api.controllers.v1 import controller as v1_controller
from webdemo.api import expose


class RootController(rest.RestController):
    v1 = v1_controller.V1Controller()

    @expose.expose(wtypes.text)
    def get(self):
        return "webdemo"

此时,你访问http://localhost:8080/v1就能够看到结果了。

实现 GET /v1/users

添加users controller

这个API就是返回全部的用户信息,功能很简单。首先要添加users controller到上面的v1 controller中。为了避免影响阅读体验,这里就不贴代码了,请看github上的示例代码。

使用WSME来规范API的响应值

上篇文章中,咱们已经提到了WSME能够用来规范API的请求和响应的值,这里咱们就要用上它。首先,咱们要参考OpenStack的惯例来设计这个API的返回值:

{
  "users": [
    {
      "name": "Alice",
      "age": 30
    },
    {
      "name": "Bob",
      "age": 40
    }
  ]
}

其中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类型定义相关的部分:

from wsme import types as wtypes


class User(wtypes.Base):
    name = wtypes.text
    age = int


class Users(wtypes.Base):
    users = [User]

这里咱们定义了class User,表示一个用户信息,包含两个字段,name是一个文本,age是一个整型。class Users表示一组用户信息,包含一个字段users,是一个列表,列表的元素是上面定义的class User。完成这些定义后,咱们就使用WSME来检查咱们的API是否返回了合格的值;另外一方面,只要咱们的API返回了这些类型,那么就能经过WSME的检查。咱们先来完成利用WSME来检查API返回值的代码:

class UsersController(rest.RestController):

    # expose方法的第一个参数表示返回值的类型
    @expose.expose(Users)
    def get(self):
        pass

这样就完成了API的返回值检查了。

实现API逻辑

咱们如今来完成API的逻辑部分。不过为了方便你们理解,咱们直接返回一个写好的数据,就是上面贴出来的那个。

class UsersController(rest.RestController):

    @expose.expose(Users)
    def get(self):
        user_info_list = [
            {
                'name': 'Alice',
                'age': 30,
            },
            {
                'name': 'Bob',
                'age': 40,
            }
        ]
        users_list = [User(**user_info) for user_info in user_info_list]
        return Users(users=users_list)

代码中,会先根据user信息生成User实例的列表users_list,而后再生成Users实例。此时,重启测试服务器后,你就能够从浏览器访问http://localhost:8080/v1/users,就能看到结果了。

实现 POST /v1/users

这个API会接收用户上传的一个JSON格式的数据,而后打印出来(实际中通常是存到数据库之类的),要求用户上传的数据符合User类型的规范,而且返回的状态码为201。代码以下:

class UsersController(rest.RestController):

    @expose.expose(None, body=User, status_code=201)
    def post(self, user):
        print 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 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /v1/users HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 27
>
* upload completely sent off: 27 out of 27 bytes
* HTTP 1.0, assume close after body
< HTTP/1.0 201 Created
< Date: Mon, 16 Nov 2015 15:18:24 GMT
< Server: WSGIServer/0.1 Python/2.7.10
< Content-Length: 0
<
* Closing connection 0

同时,服务器上也会打印出:

127.0.0.1 - - [16/Nov/2015 23:16:28] "POST /v1/users HTTP/1.1" 201 0
<webdemo.api.controllers.v1.users.User object at 0x7f65e058d550>

咱们用3行代码就实现了这个POST的逻辑。如今来讲明一下这里的秘密。expose装饰器的第一个参数表示这个方法没有返回值;第三个参数表示这个API的响应状态码是201,若是不加这个参数,在没有返回值的状况下,默认会返回204。第二个参数要说明一下,这里用的是body=User,你也能够直接写User。使用body=User这种形式,你能够直接发送符合User规范的JSON字符串;若是是用expose(None, User, status_code=201)那么你须要发送下面这样的数据:

{ "user": {"name": "Cook", "age": 50} }

你能够本身测试一下区别。要更多的了解本节提到的expose参数,请参考WSM文档Functions

最后,你接收到一个建立用户请求时,通常会为这个用户分配一个id。本文前面已经提到了OpenStack项目中通常使用UUID。你能够修改一下上面的逻辑,为每一个用户分配一个UUID。

实现 GET /v1/users/<UUID>

要实现这个API,须要两个步骤:

  1. 在UsersController中解析出<UUID>的部分,而后把请求传递给这个一个新的UserController。从命名能够看出,UsersController是针对多个用户的,UserController是针对一个用户的。

  2. 在UserController中实现get()方法。

使用_lookup()方法

Pecan的_lookup()方法是controller中的一个特殊方法,Pecan会在特定的时候调用这个方法来实现更灵活的URL路由。Pecan还支持用户实现_default()_route()方法。这些方法的具体说明,请阅读Pecan的文档:routing

咱们这里只用到_lookup()方法,这个方法会在controller中没有其余方法能够执行且没有_default()方法的时候执行。好比上面的UsersController中,没有定义/v1/users/<UUID>如何处理,它只能返回404;若是你定义了_lookup()方法,那么它就会调用该方法。

_lookup()方法须要返回一个元组,元组的第一个元素是下一个controller的实例,第二个元素是URL path中剩余的部分。

在这里,咱们就须要在_lookup()方法中解析出UUID的部分并传递给新的controller做为新的参数,而且返回剩余的URL path。来看下代码:

class UserController(rest.RestController):

    def __init__(self, user_id):
        self.user_id = user_id


class UsersController(rest.RestController):

    @pecan.expose()
    def _lookup(self, user_id, *remainder):
        return UserController(user_id), remainder

_lookup()方法的形式为_lookup(self, user_id, *remainder),意思就是会把/v1/users/<UUID>中的<UUID>部分做为user_id这个参数,剩余的按照"/"分割为一个数组参数(这里remainder为空)。而后,_lookup()方法里会初始化一个UserController实例,使用user_id做为初始化参数。这么作以后,这个初始化的控制器就能知道是要查找哪一个用户了。而后这个控制器会被返回,做为下一个控制被调用。请求的处理流程就这么转移到UserController中了。

实现API逻辑

实现前,咱们要先修改一下咱们返回的数据,里面须要增长一个id字段。对应的User定义以下:

class User(wtypes.Base):
    id = wtypes.text
    name = wtypes.text
    age = int

如今,完整的UserController代码以下:

class UserController(rest.RestController):

    def __init__(self, user_id):
        self.user_id = user_id

    @expose.expose(User)
    def get(self):
        user_info = {
            'id': self.user_id,
            'name': 'Alice',
            'age': 30,
        }
        return 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会失败):

class User(wtypes.Base):
    id = wtypes.wsattr(wtypes.text, mandatory=True)
    name = wtypes.text
    age = int

如何检查一个可选字段的值是否存在

检查这个值是否为None是确定不行的,须要检查这个值是否为wsme.Unset

实现 PUT /v1/users/<UUID>

这个和上一个API同样,不过_lookup()方法已经实现过了,直接添加方法到UserController中便可:

class UserController(rest.RestController):

    @expose.expose(User, body=User)
    def put(self, user):
        user_info = {
            'id': self.user_id,
            'name': user.name,
            'age': user.age + 1,
        }
        return 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/<UUID>

同上,没有什么新的内容:

class UserController(rest.RestController):

    @expose.expose()
    def delete(self):
        print 'Delete user_id: %s' % self.user_id

总结

到此为止,咱们已经完成了咱们的API服务了,虽然没有实际的逻辑,可是本文搭建起来的框架也是OpenStack中API服务的一个经常使用框架,不少大项目的API服务代码都和咱们的webdemo长得差很少。最后再说一下,本文的代码在github上托管着:diabloneo/webdemo

如今咱们已经了解了包管理和API服务了,那么接下来就要开始数据库相关的操做了。大部分OpenStack的项目都是使用很是著名的sqlalchemy库来实现数据库操做的,本系列接下来的文章就是要来讲明数据库的相关知识和应用。

相关文章
相关标签/搜索