經過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庫來實現數據庫操做的,本系列接下來的文章就是要來講明數據庫的相關知識和應用。

相關文章
相關標籤/搜索