經過demo學習OpenStack開發所需的基礎知識 -- API服務(3)

上一篇文章咱們瞭解了一個巨囉嗦的框架:Paste + PasteDeploy + Routes + WebOb。後來OpenStack社區的人受不了這麼囉嗦的代碼了,決定換一個框架,他們最終選中了Pecan。Pecan框架相比上一篇文章的囉嗦框架有以下好處:html

  • 不用本身寫WSGI application了node

  • 請求路由很容易就能夠實現了git

總的來講,用上Pecan框架之後,不少重複的代碼不用寫了,開發人員能夠專一於業務,也就是實現每一個API的功能。數據庫

Pecan

Pecan框架的目標是實現一個採用對象分發方式進行URL路由的輕量級Web框架。它很是專一於本身的目標,它的大部分功能都和URL路由以及請求和響應的處理相關,而不去實現模板、安全以及數據庫層,這些東西均可以經過其餘的庫來實現。關於Pecan的更多信息,能夠查看文檔:https://pecan.readthedocs.org/en/latest/index.html。本文以OpenStack的magnum項目爲例來講明Pecan項目在實際中的應用,可是本文不會詳細講解Pecan的各個方面,一些細節請讀者閱讀Pecan的文檔。json

項目中的代碼結構

使用Pecan框架時,OpenStack項目通常會把API服務的實現都放在一個api目錄下,好比magnum項目是這樣的:segmentfault

➜ ~/openstack/env/p/magnum git:(master) $ tree magnum/api
magnum/api
├── app.py
├── auth.py
├── config.py
├── controllers
│   ├── base.py
│   ├── __init__.py
│   ├── link.py
│   ├── root.py
│   └── v1
│       ├── base.py
│       ├── baymodel.py
│       ├── bay.py
│       ├── certificate.py
│       ├── collection.py
│       ├── container.py
│       ├── __init__.py
│       ├── magnum_services.py
│       ├── node.py
│       ├── pod.py
│       ├── replicationcontroller.py
│       ├── service.py
│       ├── types.py
│       ├── utils.py
│       └── x509keypair.py
├── expose.py
├── hooks.py
├── __init__.py
├── middleware
│   ├── auth_token.py
│   ├── __init__.py
│   └── parsable_error.py
├── servicegroup.py
└── validation.py

你也能夠在Ceilometer項目中看到相似的結構。介紹一下幾個主要的文件,這樣你之後看到一個使用Pecan的OpenStack項目時就會比較容易找到入口。api

  • app.py 通常包含了Pecan應用的入口,包含應用初始化代碼數組

  • config.py 包含Pecan的應用配置,會被app.py使用安全

  • controllers/ 這個目錄會包含全部的控制器,也就是API具體邏輯的地方app

  • controllers/root.py 這個包含根路徑對應的控制器

  • controllers/v1/ 這個目錄對應v1版本的API的控制器。若是有多個版本的API,你通常能看到v2等目錄。

代碼變少了:application的配置

Pecan的配置很容易,經過一個Python源碼式的配置文件就能夠完成基本的配置。這個配置的主要目的是指定應用程序的root,而後用於生成WSGI application。咱們來看Magnum項目的例子。Magnum項目有個API服務是用Pecan實現的,在magnum/api/config.py文件中能夠找到這個文件,主要內容以下:

app = {
    'root': 'magnum.api.controllers.root.RootController',
    'modules': ['magnum.api'],
    'debug': False,
    'hooks': [
        hooks.ContextHook(),
        hooks.RPCHook(),
        hooks.NoExceptionTracebackHook(),
    ],
    'acl_public_routes': [
        '/'
    ],
}

上面這個app對象就是Pecan的配置,每一個Pecan應用都須要有這麼一個名爲app的配置。app配置中最主要的就是root的值,這個值表示了應用程序的入口,也就是從哪一個地方開始解析HTTP的根path:/hooks對應的配置是一些Pecan的hook,做用相似於WSGI Middleware。

有了app配置後,就可讓Pecan生成一個WSGI application。在Magnum項目中,magnum/api/app.py文件就是生成WSGI application的地方,咱們來看一下這個的主要內容:

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


def setup_app(config=None):
    if not config:
        config = get_pecan_config()

    app_conf = dict(config.app)

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

    return auth.install(app, CONF, config.app.acl_public_routes)

get_pecan_config()方法讀取咱們上面提到的config.py文件,而後返回一個pecan.configuration.Config對象。setup_app()函數首先調用get_pecan_config()函數獲取application的配置,而後調用pecan.make_app()函數建立了一個WSGI application,最後調用了 auth.install()函數(也就是magnum.api.auth.install()函數)爲剛剛生成的WSGI application加上Keystone的認證中間件(確保全部的請求都會經過Keystone認證)。

到這邊爲止,一個Pecan的WSGI application就已經準備好了,只要調用這個setup_app()函數就能得到。至於如何部署這個WSGI application,請參考WSGI簡介這篇文章。

從Magnum這個實際的例子能夠看出,使用了Pecan以後,咱們再也不須要本身寫那些冗餘的WSGI application代碼了,直接調用Pecan的make_app()函數就能完成這些工做。另外,對於以前使用PasteDeploy時用到的不少WSGI中間件,能夠選擇使用Pecan的hooks機制來實現,也選擇使用WSGI中間件的方式來實現。在Magnum的API服務就同時使用了這兩種方式。其實,Pecan還能夠和PasteDeploy一塊兒使用,Ceilometer項目就是這麼作的,你們能夠看看。

肯定路由變得容易了:對象分發式的路由

Pecan不只縮減了生成WSGI application的代碼,並且也讓開發人員更容易的指定一個application的路由。Pecan採用了一種對象分發風格(object-dispatch style)的路由模式。咱們直接經過例子來解釋這種路由模式,仍是以Magnum項目爲例。

上面提到了,Magnum的API服務的root是magnum.api.controllers.root.RootController。這裏的RootController的是一個類,咱們來看它的代碼:

class RootController(rest.RestController):

    _versions = ['v1']
    """All supported API versions"""

    _default_version = 'v1'
    """The default API version"""

    v1 = v1.Controller()

    @expose.expose(Root)
    def get(self):
        # NOTE: The reason why convert() it's being called for every
        #       request is because we need to get the host url from
        #       the request object to make the links.
        return Root.convert()

    @pecan.expose()
    def _route(self, args):
        """Overrides the default routing behavior.

        It redirects the request to the default version of the magnum API
        if the version number is not specified in the url.
        """

        if args[0] and args[0] not in self._versions:
            args = [self._default_version] + args
        return super(RootController, self)._route(args)

別看這個類這麼長,我來解釋一下你就懂了。首先,你能夠先忽略掉_route()函數,這個函數是用來覆蓋Pecan的默認路由實現的,在這裏去掉它不妨礙咱們理解Pecan(這裏的_route()函數的做用把全部請求重定向到默認的API版本去)。去掉_route()和其餘的東西后,整個類就變成這麼短:

class RootController(rest.RestController):
    v1 = v1.Controller()

    @expose.expose(Root)
    def get(self):
        return Root.convert()
  • 首先,你要記住,這個RootController對應的是URL中根路徑,也就是path中最左邊的/

  • RootController繼承自rest.RestController,是Pecan實現的RESTful控制器。這裏的get()函數表示,當訪問的是GET /時,由該函數處理。get()函數會返回一個WSME對象,表示一個形式化的HTTP Response,這個下面再講。get()函數上面的expose裝飾器是Pecan實現路由控制的一個方式,被expose的函數纔會被路由處理。

  • 這裏的v1 = v1.Controller()表示,當訪問的是GET /v1或者GET /v1/...時,請求由一個v1.Controller實例來處理。

爲了加深你們的理解,咱們再來看下v1.Controller的實現:

class Controller(rest.RestController):
    """Version 1 API controller root."""

    bays = bay.BaysController()
    baymodels = baymodel.BayModelsController()
    containers = container.ContainersController()
    nodes = node.NodesController()
    pods = pod.PodsController()
    rcs = rc.ReplicationControllersController()
    services = service.ServicesController()
    x509keypairs = x509keypair.X509KeyPairController()
    certificates = certificate.CertificateController()

    @expose.expose(V1)
    def get(self):
        return V1.convert()

    ...

上面這個Controller也是繼承自rest.RestController。因此它的get函數表示,當訪問的是GET /v1的時候,要作的處理。而後,它還有不少類屬性,這些屬性分別表示不一樣URL路徑的控制器:

  • /v1/bays 由bays處理

  • /v1/baymodels 由baymodels處理

  • /v1/containers 由containers處理

其餘的都是相似的。咱們再繼續看bay.BaysController的代碼:

class BaysController(rest.RestController):
    """REST controller for Bays."""
    def __init__(self):
        super(BaysController, self).__init__()

    _custom_actions = {
        'detail': ['GET'],
    }

    def get_all(...):
    
    def detail(...):
    
    def get_one(...):
    
    def post(...):
    
    def patch(...):

    def delete(...):

這個controller中只有函數,沒有任何類屬性,並且沒有實現任何特殊方法,因此/v1/bays開頭的URL處理都在這個controller中終結。這個類會處理以下請求:

  • GET /v1/bays

  • GET /v1/bays/{UUID}

  • POST /v1/bays

  • PATCH /v1/bays/{UUID}

  • DELETE /v1/bays/{UUID}

  • GET /v1/bays/detail/{UUID}

看了上面的3個controller以後,你應該能大概明白Pecan是如何對URL進行路由的。這種路由方式就是對象分發:根據類屬性,包括數據屬性和方法屬性來決定如何路由一個HTTP請求。Pecan的文檔中對請求的路由有專門的描述,要想掌握Pecan的路由仍是要完整的看一下官方文檔。

內置RESTful支持

咱們上面舉例的controller都是繼承自pecan.rest.RestController,這種controller稱爲RESTful controller,專門用於實現RESTful API的,所以在OpenStack中使用特別多。Pecan還支持普通的controller,稱爲Generic controller。Generic controller繼承自object對象,默認沒有實現對RESTful請求的方法。簡單的說,RESTful controller幫咱們規定好了get_one(), get_all(), get(), post()等方法對應的HTTP請求,而Generic controller則沒有。關於這兩種controller的區別,能夠看官方文檔Writing RESTful Web Services with Generic Controllers,有很清楚的示例。

對於RestController中沒有預先定義好的方法,咱們能夠經過控制器的_custom_actions屬性來指定其能處理的方法。

class RootController(rest.RestController):
    _custom_actions = {
        'test': ['GET'],
    }

    @expose()
    def test(self):
        return 'hello'

上面這個控制器是一個根控制器,指定了/test路徑支持GET方法,效果以下:

$ curl http://localhost:8080/test
hello%

那麼HTTP請求和HTTP響應呢?

上面講了這麼多,咱們都沒有說明在Pecan中如何處理請求和如何返回響應。這個將在下一章中說明,同時咱們會引入一個新的庫WSME

WSME

Pecan對請求和響應的處理

在開始提到WSME以前,咱們先來看下Pecan本身對HTTP請求和響應的處理。這樣你能更好的理解爲何會再引入一個WSME庫。

Pecan框架爲每一個線程維護了單獨的請求和響應對象,你能夠直接在請求處理函數中訪問。pecan.requestpecan.response分別表明當前須要處理的請求和響應對象。你能夠直接操做這兩個對象,好比指定響應的狀態碼,就像下面這個例子同樣(例子來自官方文檔):

@pecan.expose()
def login(self):
    assert pecan.request.path == '/login'
    username = pecan.request.POST.get('username')
    password = pecan.request.POST.get('password')

    pecan.response.status = 403
    pecan.response.text = 'Bad Login!'

這個例子演示了訪問POST請求的參數以及返回403。你也能夠從新構造一個pecan.Response對象做爲返回值(例子來自官方文檔):

from pecan import expose, Response

class RootController(object):

    @expose()
    def hello(self):
        return Response('Hello, World!', 202)

另外,HTTP請求的參數也會能夠做爲控制器方法的參數,仍是來看幾個官方文檔的例子:

class RootController(object):
    @expose()
    def index(self, arg):
        return arg

    @expose()
    def kwargs(self, **kwargs):
        return str(kwargs)

這個控制器中的方法直接返回了參數,演示了對GET請求參數的處理,效果是這樣的:

$ curl http://localhost:8080/?arg=foo
foo
$ curl http://localhost:8080/kwargs?a=1&b=2&c=3
{u'a': u'1', u'c': u'3', u'b': u'2'}

有時候,參數也多是URL的一部分,好比最後的一段path做爲參數,就像下面這樣:

class RootController(object):
    @expose()
    def args(self, *args):
        return ','.join(args)

效果是這樣的:

$ curl http://localhost:8080/args/one/two/three
one,two,three

另外,咱們還要看一下POST方法的參數如何處理(例子來自官方文檔):

class RootController(object):
    @expose()
    def index(self, arg):
        return arg

效果以下,就是把HTTP body解析成了控制器方法的參數:

$ curl -X POST "http://localhost:8080/" -H "Content-Type: application/x-www-form-urlencoded" -d "arg=foo"
foo

返回JSON仍是HTML?

若是你不是明確的返回一個Response對象,那麼Pecan中方法的返回內容類型就是由expose()裝飾器決定的。默認狀況下,控制器的方法返回的content-type是HTML。

class RootController(rest.RestController):
    _custom_actions = {
        'test': ['GET'],
    }

    @expose()
    def test(self):
        return 'hello'

效果以下:

$ curl -v http://localhost:8080/test
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Tue, 15 Sep 2015 14:31:28 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 5
< Content-Type: text/html; charset=UTF-8
<
* Closing connection 0
hello%

也可讓它返回JSON:

class RootController(rest.RestController):
    _custom_actions = {
        'test': ['GET'],
    }

    @expose('json')
    def test(self):
        return 'hello'

效果以下:

curl -v http://localhost:8080/test
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Tue, 15 Sep 2015 14:33:27 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 18
< Content-Type: application/json; charset=UTF-8
<
* Closing connection 0
{"hello": "world"}%

甚至,你還可讓一個控制器方法根據URL path的來決定是返回HTML仍是JSON:

class RootController(rest.RestController):
    _custom_actions = {
        'test': ['GET'],
    }

    @expose()
    @expose('json')
    def test(self):
        return json.dumps({'hello': 'world'})

返回JSON:

$ curl -v http://localhost:8080/test.json
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test.json HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Wed, 16 Sep 2015 14:26:27 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 24
< Content-Type: application/json; charset=UTF-8
<
* Closing connection 0
"{\"hello\": \"world\"}"%

返回HTML:

$ curl -v http://localhost:8080/test.html
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test.html HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Wed, 16 Sep 2015 14:26:24 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 18
< Content-Type: text/html; charset=UTF-8
<
* Closing connection 0
{"hello": "world"}%

這裏要注意一下:

  1. 同一個字符串做爲JSON返回和做爲HTML返回是不同的,仔細看一下HTTP響應的內容。

  2. 咱們的例子中在URL的最後加上了.html後綴或者.json後綴,請嘗試一下不加後綴的化是返回什麼?而後,調換一下兩個expose()的順序再試一下。

從上面的例子能夠看出,決定響應類型的主要是傳遞給expose()函數的參數,咱們看下expose()函數的完整聲明:

pecan.decorators.expose(template=None,
                        content_type='text/html',
                        generic=False)
  • template參數用來指定返回值的模板,若是是'json'就會返回JSON內容,這裏能夠指定一個HTML文件,或者指定一個mako模板。

  • content_type指定響應的content-type,默認值是'text/html'。

  • generic參數代表該方法是一個「泛型」方法,能夠指定多個不一樣的函數對應同一個路徑的不一樣的HTTP方法。

看過參數的解釋後,你應該能大概瞭解expose()函數是如何控制HTTP響應的內容和類型的。

用WSME來作什麼?

上面兩節已經說明了Pecan能夠比較好的處理HTTP請求中的參數以及控制HTTP返回值。那麼爲何咱們還須要WSME呢?由於Pecan在作下面這個事情的時候比較麻煩:請求參數和響應內容的類型檢查(英文簡稱就是typing)。固然,作是能夠作的,不過你須要本身訪問pecan.request和pecan.response,而後檢查指定的值的類型。WSME就是爲解決這個問題而生的,並且適用場景就是RESTful API。

WSME簡介

WSME的全稱是Web Service Made Easy,是專門用於實現REST服務的typing庫,讓你不須要直接操做請求和響應,並且恰好和Pecan結合得很是好,因此OpenStack的不少項目都使用了Pecan + WSME的組合來實現API(好吧,我看過的項目,用了Pecan的都用了WSME)。WSME的理念是:在大部分狀況下,Web服務的輸入和輸出對數據類型的要求都是嚴格的。因此它就專門解決了這個事情,而後把其餘事情都交給其餘框架去實現。所以,通常WSME都是和其餘框架配合使用的,支持Pecan、Flask等。WSME的文檔地址是http://wsme.readthedocs.org/en/latest/index.html

WSME的使用

用了WSME後的好處是什麼呢?WSME會自動幫你檢查HTTP請求和響應中的數據是否符合預先設定好的要求。WSME的主要方式是經過裝飾器來控制controller方法的輸入和輸出。WSME中主要使用兩個控制器:

  • @signature: 這個裝飾器用來描述一個函數的輸入和輸出。

  • @wsexpose: 這個裝飾器包含@signature的功能,同時會把函數的路由信息暴露給Web框架,效果就像Pecan的expose裝飾器。

這裏咱們結合Pecan來說解WSME的使用。先來看一個原始類型的例子:

from wsmeext.pecan import wsexpose

class RootController(rest.RestController):
    _custom_actions = {
        'test': ['GET'],
    }

    @wsexpose(int, int)
    def test(self, number):
        return number

若是不提供參數,訪問會失敗:

$ curl http://localhost:8080/test
{"debuginfo": null, "faultcode": "Client", "faultstring": "Missing argument: \"number\""}%

若是提供的參數不是整型,訪問也會失敗:

$ curl http://localhost:8080/test\?number\=a
{"debuginfo": null, "faultcode": "Client", "faultstring": "Invalid input for field/attribute number. Value: 'a'. unable to convert to int"}%

上面這些錯誤信息都是由WSME框架直接返回的,尚未執行到你寫的方法。若是請求正確,那麼會是這樣的:

$ curl -v http://localhost:8080/test\?number\=1
* Hostname was NOT found in DNS cache
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test?number=1 HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Wed, 16 Sep 2015 15:06:35 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 1
< Content-Type: application/json; charset=UTF-8
<
* Closing connection 0
1%

請注意返回的content-type,這裏返回JSON是由於咱們使用的wsexpose設置的返回類型是XML和JSON,而且JSON是默認值。上面這個例子就是WSME最簡單的應用了。

那麼如今有下面這些問題須要思考一下:

  • 若是想用POST的方式來傳遞參數,要怎麼作呢?提示:要閱讀WSME中@signature裝飾器的文檔。

  • 若是我但願使用/test/1這種方式來傳遞參數要怎麼作呢?提示:要閱讀Pecan文檔中關於路由的部分。

  • WSME中支持對哪些類型的檢查呢?WSME支持整型、浮點型、字符串、布爾型、日期時間等,甚至還支持用戶自定義類型。提示:要閱讀WSME文檔中關於類型的部分。

  • WSME支持數組類型麼?支持。

上面的問題其實也是不少人使用WSME的時候常常問的問題。咱們將在下一篇文章中使用Pecan + WSME來繼續開發咱們的demo,而且用代碼來回答上面全部的問題。

相關文章
相關標籤/搜索